diff --git a/rtl/xfcp/taxi_xfcp_mod_axi.f b/rtl/xfcp/taxi_xfcp_mod_axi.f new file mode 100644 index 0000000..3ec0865 --- /dev/null +++ b/rtl/xfcp/taxi_xfcp_mod_axi.f @@ -0,0 +1,5 @@ +taxi_xfcp_mod_axi.sv +taxi_xfcp_mod_axil.sv +../axi/taxi_axi_if.sv +../axi/taxi_axil_if.sv +../axis/taxi_axis_if.sv diff --git a/rtl/xfcp/taxi_xfcp_mod_axi.sv b/rtl/xfcp/taxi_xfcp_mod_axi.sv new file mode 100644 index 0000000..14adf7f --- /dev/null +++ b/rtl/xfcp/taxi_xfcp_mod_axi.sv @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * XFCP AXI module + */ +module taxi_xfcp_mod_axi # +( + parameter logic [15:0] XFCP_ID_TYPE = 16'h8001, + parameter XFCP_ID_STR = "AXI Master", + parameter logic [8*16-1:0] XFCP_EXT_ID = 0, + parameter XFCP_EXT_ID_STR = "", + parameter COUNT_SIZE = 16 +) +( + input wire logic clk, + input wire logic rst, + + /* + * XFCP upstream port + */ + taxi_axis_if.snk up_xfcp_in, + taxi_axis_if.src up_xfcp_out, + + /* + * AXI master interface + */ + taxi_axi_if.wr_mst m_axi_wr, + taxi_axi_if.rd_mst m_axi_rd +); + +taxi_axil_if #( + .DATA_W(m_axi_wr.DATA_W), + .ADDR_W(m_axi_wr.ADDR_W), + .STRB_W(m_axi_wr.STRB_W) +) axil_if(); + +// AW +assign m_axi_wr.awid = '0; +assign m_axi_wr.awaddr = axil_if.awaddr; +assign m_axi_wr.awlen = '0; +assign m_axi_wr.awsize = 3'($clog2(m_axi_wr.STRB_W)); +assign m_axi_wr.awburst = 2'b01; +assign m_axi_wr.awlock = 1'b0; +assign m_axi_wr.awcache = 4'b0011; +assign m_axi_wr.awprot = axil_if.awprot; +assign m_axi_wr.awqos = 4'd0; +assign m_axi_wr.awregion = 4'd0; +assign m_axi_wr.awuser = axil_if.awuser; +assign m_axi_wr.awvalid = axil_if.awvalid; +assign axil_if.awready = m_axi_wr.awready; +// W +assign m_axi_wr.wdata = axil_if.wdata; +assign m_axi_wr.wstrb = axil_if.wstrb; +assign m_axi_wr.wlast = 1'b1; +assign m_axi_wr.wuser = axil_if.wuser; +assign m_axi_wr.wvalid = axil_if.wvalid; +assign axil_if.wready = m_axi_wr.wready; +// B +assign axil_if.bresp = m_axi_wr.bresp; +assign axil_if.buser = m_axi_wr.buser; +assign axil_if.bvalid = m_axi_wr.bvalid; +assign m_axi_wr.bready = axil_if.bready; +// AR +assign m_axi_rd.arid = '0; +assign m_axi_rd.araddr = axil_if.araddr; +assign m_axi_rd.arlen = '0; +assign m_axi_rd.arsize = 3'($clog2(m_axi_wr.STRB_W)); +assign m_axi_rd.arburst = 2'b01; +assign m_axi_rd.arlock = 1'b0; +assign m_axi_rd.arcache = 4'b0011; +assign m_axi_rd.arprot = axil_if.arprot; +assign m_axi_rd.arqos = 4'd0; +assign m_axi_rd.arregion = 4'd0; +assign m_axi_rd.aruser = axil_if.aruser; +assign m_axi_rd.arvalid = axil_if.arvalid; +assign axil_if.arready = m_axi_rd.arready; +// R +assign axil_if.rdata = m_axi_rd.rdata; +assign axil_if.rresp = m_axi_rd.rresp; +assign axil_if.ruser = m_axi_rd.ruser; +assign axil_if.rvalid = m_axi_rd.rvalid; +assign m_axi_rd.rready = axil_if.rready; + +taxi_xfcp_mod_axil #( + .XFCP_ID_TYPE(XFCP_ID_TYPE), + .XFCP_ID_STR(XFCP_ID_STR), + .XFCP_EXT_ID(XFCP_EXT_ID), + .XFCP_EXT_ID_STR(XFCP_EXT_ID_STR), + .COUNT_SIZE(COUNT_SIZE) +) +xfcp_mod_axil_inst ( + .clk(clk), + .rst(rst), + + /* + * XFCP upstream port + */ + .up_xfcp_in(up_xfcp_in), + .up_xfcp_out(up_xfcp_out), + + /* + * AXI lite master interface + */ + .m_axil_wr(axil_if), + .m_axil_rd(axil_if) +); + +endmodule + +`resetall diff --git a/tb/xfcp/taxi_xfcp_mod_axi/Makefile b/tb/xfcp/taxi_xfcp_mod_axi/Makefile new file mode 100644 index 0000000..82b726a --- /dev/null +++ b/tb/xfcp/taxi_xfcp_mod_axi/Makefile @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: CERN-OHL-S-2.0 +# +# Copyright (c) 2025 FPGA Ninja, LLC +# +# Authors: +# - Alex Forencich + +TOPLEVEL_LANG = verilog + +SIM ?= verilator +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +DUT = taxi_xfcp_mod_axi +COCOTB_TEST_MODULES = test_$(DUT) +COCOTB_TOPLEVEL = test_$(DUT) +MODULE = $(COCOTB_TEST_MODULES) +TOPLEVEL = $(COCOTB_TOPLEVEL) +VERILOG_SOURCES += $(COCOTB_TOPLEVEL).sv +VERILOG_SOURCES += ../../../rtl/xfcp/$(DUT).f + +# handle file list files +process_f_file = $(call process_f_files,$(addprefix $(dir $1),$(shell cat $1))) +process_f_files = $(foreach f,$1,$(if $(filter %.f,$f),$(call process_f_file,$f),$f)) +uniq_base = $(if $1,$(call uniq_base,$(foreach f,$1,$(if $(filter-out $(notdir $(lastword $1)),$(notdir $f)),$f,))) $(lastword $1)) +VERILOG_SOURCES := $(call uniq_base,$(call process_f_files,$(VERILOG_SOURCES))) + +# module parameters +export PARAM_COUNT_SIZE := 16 +export PARAM_AXI_DATA_W := 32 +export PARAM_AXI_ADDR_W := 32 +export PARAM_AXI_STRB_W := $(shell expr $(PARAM_AXI_DATA_W) / 8 ) + +ifeq ($(SIM), icarus) + PLUSARGS += -fst + + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v))) +else ifeq ($(SIM), verilator) + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v))) + + ifeq ($(WAVES), 1) + COMPILE_ARGS += --trace-fst + VERILATOR_TRACE = 1 + endif +endif + +include $(shell cocotb-config --makefiles)/Makefile.sim diff --git a/tb/xfcp/taxi_xfcp_mod_axi/test_taxi_xfcp_mod_axi.py b/tb/xfcp/taxi_xfcp_mod_axi/test_taxi_xfcp_mod_axi.py new file mode 100644 index 0000000..f8cead9 --- /dev/null +++ b/tb/xfcp/taxi_xfcp_mod_axi/test_taxi_xfcp_mod_axi.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: CERN-OHL-S-2.0 +""" + +Copyright (c) 2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +import itertools +import logging +import os +import struct +import sys + +import cocotb_test.simulator +import pytest + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge +from cocotb.regression import TestFactory + +from cocotbext.axi import AxiStreamBus, AxiStreamSource, AxiStreamSink +from cocotbext.axi import AxiBus, AxiRam + +try: + from xfcp import XfcpFrame +except ImportError: + # attempt import from current directory + sys.path.insert(0, os.path.join(os.path.dirname(__file__))) + try: + from xfcp import XfcpFrame + finally: + del sys.path[0] + + +class TB(object): + def __init__(self, dut): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + cocotb.start_soon(Clock(dut.clk, 8, units="ns").start()) + + self.axis_source = AxiStreamSource(AxiStreamBus.from_entity(dut.up_xfcp_in), dut.clk, dut.rst) + self.axis_sink = AxiStreamSink(AxiStreamBus.from_entity(dut.up_xfcp_out), dut.clk, dut.rst) + + self.axi_ram = AxiRam(AxiBus.from_entity(dut.m_axi), dut.clk, dut.rst, size=2**16) + + def set_idle_generator(self, generator=None): + if generator: + self.axis_source.set_pause_generator(generator()) + self.axi_ram.write_if.b_channel.set_pause_generator(generator()) + self.axi_ram.read_if.r_channel.set_pause_generator(generator()) + + def set_backpressure_generator(self, generator=None): + if generator: + self.axis_sink.set_pause_generator(generator()) + self.axi_ram.write_if.aw_channel.set_pause_generator(generator()) + self.axi_ram.write_if.w_channel.set_pause_generator(generator()) + self.axi_ram.read_if.ar_channel.set_pause_generator(generator()) + + async def reset(self): + self.dut.rst.setimmediatevalue(0) + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 1 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 0 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + + +async def run_test_write(dut, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + byte_lanes = tb.axi_ram.write_if.byte_lanes + + await tb.reset() + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + for length in range(1, byte_lanes*4): + for offset in range(byte_lanes): + tb.log.info("length %d, offset %d", length, offset) + addr = offset+0x1000 + test_data = bytearray([x % 256 for x in range(length)]) + + tb.axi_ram.write(addr-128, b'\xaa'*(length+256)) + + pkt = XfcpFrame() + pkt.ptype = 0x12 + pkt.payload = bytearray(struct.pack(' len(block): + return None + dec.extend(block[i:i+code-1]) + i += code-1 + if code < 255 and i < len(block): + dec.append(0) + + return bytes(dec) + + +class XfcpFrame(object): + def __init__(self, payload=b'', path=[], rpath=[], ptype=0): + self._payload = b'' + self.path = path + self.rpath = rpath + self.ptype = ptype + + if type(payload) is bytes: + self.payload = payload + if type(payload) is XfcpFrame: + self.payload = payload.payload + self.path = list(payload.path) + self.rpath = list(payload.rpath) + self.ptype = payload.ptype + + @property + def payload(self): + return self._payload + + @payload.setter + def payload(self, value): + self._payload = bytes(value) + + def build(self): + data = bytearray() + + for p in self.path: + data.extend(struct.pack('B', p)) + + if self.rpath: + data.extend(struct.pack('B', 0xFE)) + for p in self.rpath: + data.extend(struct.pack('B', p)) + + data.extend(struct.pack('B', 0xFF)) + + data.extend(struct.pack('B', self.ptype)) + + data.extend(self.payload) + + return data + + def build_cobs(self): + return cobs_encode(self.build())+b'\x00' + + @classmethod + def parse(cls, data): + data = bytes(data) + + i = 0 + + path = [] + rpath = [] + + while i < len(data) and data[i] < 0xFE: + path.append(data[i]) + i += 1 + + if data[i] == 0xFE: + i += 1 + while i < len(data) and data[i] < 0xFE: + rpath.append(data[i]) + i += 1 + + assert data[i] == 0xFF + i += 1 + + ptype = data[i] + i += 1 + + payload = data[i:] + + return cls(payload, path, rpath, ptype) + + @classmethod + def parse_cobs(cls, data): + return cls.parse(cobs_decode(bytes(data))) + + def __eq__(self, other): + if type(other) is XfcpFrame: + return (self.path == other.path and + self.rpath == other.rpath and + self.ptype == other.ptype and + self.payload == other.payload) + return False + + def __repr__(self): + return f"XfcpFrame(payload={self.payload!r}, path={self.path!r}, rpath={self.rpath!r}, ptype={self.ptype})"