From 194a686bdac8a55dd91074425d4a16c504908150 Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Tue, 4 Mar 2025 22:05:11 -0800 Subject: [PATCH] xfcp: Add XFCP UART interface module and testbench Signed-off-by: Alex Forencich --- rtl/xfcp/taxi_xfcp_if_uart.f | 5 + rtl/xfcp/taxi_xfcp_if_uart.sv | 190 +++++++++++++++++ tb/xfcp/taxi_xfcp_if_uart/Makefile | 47 ++++ .../test_taxi_xfcp_if_uart.py | 201 ++++++++++++++++++ .../test_taxi_xfcp_if_uart.sv | 65 ++++++ tb/xfcp/taxi_xfcp_if_uart/xfcp.py | 1 + tb/xfcp/xfcp.py | 159 ++++++++++++++ 7 files changed, 668 insertions(+) create mode 100644 rtl/xfcp/taxi_xfcp_if_uart.f create mode 100644 rtl/xfcp/taxi_xfcp_if_uart.sv create mode 100644 tb/xfcp/taxi_xfcp_if_uart/Makefile create mode 100644 tb/xfcp/taxi_xfcp_if_uart/test_taxi_xfcp_if_uart.py create mode 100644 tb/xfcp/taxi_xfcp_if_uart/test_taxi_xfcp_if_uart.sv create mode 120000 tb/xfcp/taxi_xfcp_if_uart/xfcp.py create mode 100644 tb/xfcp/xfcp.py diff --git a/rtl/xfcp/taxi_xfcp_if_uart.f b/rtl/xfcp/taxi_xfcp_if_uart.f new file mode 100644 index 0000000..370bece --- /dev/null +++ b/rtl/xfcp/taxi_xfcp_if_uart.f @@ -0,0 +1,5 @@ +taxi_xfcp_if_uart.sv +../lss/taxi_uart.f +../axis/taxi_axis_fifo.sv +../axis/taxi_axis_cobs_encode.f +../axis/taxi_axis_cobs_decode.sv \ No newline at end of file diff --git a/rtl/xfcp/taxi_xfcp_if_uart.sv b/rtl/xfcp/taxi_xfcp_if_uart.sv new file mode 100644 index 0000000..1371c77 --- /dev/null +++ b/rtl/xfcp/taxi_xfcp_if_uart.sv @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2017-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * XFCP Interface (UART) + */ +module taxi_xfcp_if_uart #( + parameter TX_FIFO_DEPTH = 512, + parameter RX_FIFO_DEPTH = 512 +) +( + input wire logic clk, + input wire logic rst, + + /* + * UART interface + */ + input wire logic uart_rxd, + output wire logic uart_txd, + + /* + * XFCP downstream port + */ + taxi_axis_if.snk dn_xfcp_in, + taxi_axis_if.src dn_xfcp_out, + + /* + * Configuration + */ + input wire logic [15:0] prescale +); + +taxi_axis_if #(.DATA_W(8), .LAST_EN(0)) uart_tx(), uart_rx(); +taxi_axis_if #(.DATA_W(8), .LAST_EN(1), .USER_EN(1), .USER_W(1)) fifo_tx(), fifo_rx(); + +taxi_uart +uart_inst ( + .clk(clk), + .rst(rst), + + /* + * AXI4-Stream input (sink) + */ + .s_axis_tx(uart_tx), + + /* + * AXI4-Stream output (source) + */ + .m_axis_rx(uart_rx), + + /* + * UART interface + */ + .rxd(uart_rxd), + .txd(uart_txd), + + /* + * Status + */ + .tx_busy(), + .rx_busy(), + .rx_overrun_error(), + .rx_frame_error(), + + /* + * Configuration + */ + .prescale(prescale) +); + +taxi_axis_cobs_encode #( + .APPEND_ZERO(1) +) +cobs_encode_inst ( + .clk(clk), + .rst(rst), + + /* + * AXI4-Stream input (sink) + */ + .s_axis(fifo_tx), + + /* + * AXI4-Stream output (source) + */ + .m_axis(uart_tx) +); + +taxi_axis_cobs_decode +cobs_decode_inst ( + .clk(clk), + .rst(rst), + + /* + * AXI4-Stream input (sink) + */ + .s_axis(uart_rx), + + /* + * AXI4-Stream output (source) + */ + .m_axis(fifo_rx) +); + +taxi_axis_fifo #( + .DEPTH(TX_FIFO_DEPTH), + .FRAME_FIFO(1), + .DROP_BAD_FRAME(1), + .DROP_WHEN_FULL(0) +) +tx_fifo_inst ( + .clk(clk), + .rst(rst), + + /* + * AXI4-Stream input (sink) + */ + .s_axis(dn_xfcp_in), + + /* + * AXI4-Stream output (source) + */ + .m_axis(fifo_tx), + + /* + * Pause + */ + .pause_req(1'b0), + .pause_ack(), + + /* + * Status + */ + .status_depth(), + .status_depth_commit(), + .status_overflow(), + .status_bad_frame(), + .status_good_frame() +); + +taxi_axis_fifo #( + .DEPTH(RX_FIFO_DEPTH), + .FRAME_FIFO(1), + .DROP_BAD_FRAME(1), + .DROP_WHEN_FULL(1) +) +rx_fifo_inst ( + .clk(clk), + .rst(rst), + + /* + * AXI4-Stream input (sink) + */ + .s_axis(fifo_rx), + + /* + * AXI4-Stream output (source) + */ + .m_axis(dn_xfcp_out), + + /* + * Pause + */ + .pause_req(1'b0), + .pause_ack(), + + /* + * Status + */ + .status_depth(), + .status_depth_commit(), + .status_overflow(), + .status_bad_frame(), + .status_good_frame() +); + +endmodule + +`resetall diff --git a/tb/xfcp/taxi_xfcp_if_uart/Makefile b/tb/xfcp/taxi_xfcp_if_uart/Makefile new file mode 100644 index 0000000..8d20d39 --- /dev/null +++ b/tb/xfcp/taxi_xfcp_if_uart/Makefile @@ -0,0 +1,47 @@ +# 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_if_uart +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_TX_FIFO_DEPTH := 512 +export PARAM_RX_FIFO_DEPTH := 512 + +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_if_uart/test_taxi_xfcp_if_uart.py b/tb/xfcp/taxi_xfcp_if_uart/test_taxi_xfcp_if_uart.py new file mode 100644 index 0000000..865a233 --- /dev/null +++ b/tb/xfcp/taxi_xfcp_if_uart/test_taxi_xfcp_if_uart.py @@ -0,0 +1,201 @@ +#!/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 sys + +import cocotb_test.simulator + +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.uart import UartSource, UartSink + +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, baud=3e6): + 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.uart_source = UartSource(dut.uart_rxd, baud=baud, bits=8, stop_bits=1) + self.uart_sink = UartSink(dut.uart_txd, baud=baud, bits=8, stop_bits=1) + + self.axis_source = AxiStreamSource(AxiStreamBus.from_entity(dut.dn_xfcp_in), dut.clk, dut.rst) + self.axis_sink = AxiStreamSink(AxiStreamBus.from_entity(dut.dn_xfcp_out), dut.clk, dut.rst) + + dut.prescale.setimmediatevalue(int(1/8e-9/baud/8)) + + 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_tx(dut, payload_lengths=None, payload_data=None): + + tb = TB(dut) + + await tb.reset() + + for test_data in [payload_data(x) for x in payload_lengths()]: + + pkt = XfcpFrame() + pkt.path = [1, 2, 3] + pkt.rpath = [4] + pkt.ptype = 1 + pkt.payload = test_data + + await tb.axis_source.write(pkt.build()) + + rx_data = bytearray() + while True: + b = await tb.uart_sink.read(1) + if b[0] == 0: + break + rx_data.extend(b) + + rx_pkt = XfcpFrame.parse_cobs(rx_data) + + print(rx_pkt) + assert rx_pkt == pkt + + assert tb.uart_sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_rx(dut, payload_lengths=None, payload_data=None): + + tb = TB(dut) + + await tb.reset() + + for test_data in [payload_data(x) for x in payload_lengths()]: + + pkt = XfcpFrame() + pkt.path = [1, 2, 3] + pkt.rpath = [4] + pkt.ptype = 1 + pkt.payload = test_data + + await tb.uart_source.write(pkt.build_cobs()) + + rx_frame = await tb.axis_sink.recv() + rx_pkt = XfcpFrame.parse(rx_frame.tdata) + + print(rx_pkt) + assert rx_pkt == pkt + + assert tb.axis_sink.empty() + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +def cycle_pause(): + return itertools.cycle([1, 1, 1, 0]) + + +def size_list(): + return list(range(1, 16)) + [128] + + +def incrementing_payload(length): + return bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + +if cocotb.SIM_NAME: + + for test in [run_test_tx, run_test_rx]: + factory = TestFactory(test) + factory.add_option("payload_lengths", [size_list]) + factory.add_option("payload_data", [incrementing_payload]) + factory.generate_tests() + + +# cocotb-test + +tests_dir = os.path.dirname(__file__) +rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', '..', 'rtl')) + + +def process_f_files(files): + lst = {} + for f in files: + if f[-2:].lower() == '.f': + with open(f, 'r') as fp: + l = fp.read().split() + for f in process_f_files([os.path.join(os.path.dirname(f), x) for x in l]): + lst[os.path.basename(f)] = f + else: + lst[os.path.basename(f)] = f + return list(lst.values()) + + +def test_taxi_xfcp_if_uart(request): + + dut = "taxi_xfcp_if_uart" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = module + + verilog_sources = [ + os.path.join(tests_dir, f"{toplevel}.sv"), + os.path.join(rtl_dir, "xfcp", f"{dut}.f"), + ] + + verilog_sources = process_f_files(verilog_sources) + + parameters = {} + + parameters['TX_FIFO_DEPTH'] = 512 + parameters['RX_FIFO_DEPTH'] = 512 + + extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()} + + sim_build = os.path.join(tests_dir, "sim_build", + request.node.name.replace('[', '-').replace(']', '')) + + cocotb_test.simulator.run( + simulator="verilator", + python_search=[tests_dir], + verilog_sources=verilog_sources, + toplevel=toplevel, + module=module, + parameters=parameters, + sim_build=sim_build, + extra_env=extra_env, + ) diff --git a/tb/xfcp/taxi_xfcp_if_uart/test_taxi_xfcp_if_uart.sv b/tb/xfcp/taxi_xfcp_if_uart/test_taxi_xfcp_if_uart.sv new file mode 100644 index 0000000..ffd8bb7 --- /dev/null +++ b/tb/xfcp/taxi_xfcp_if_uart/test_taxi_xfcp_if_uart.sv @@ -0,0 +1,65 @@ +// 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 Interface (UART) testbench + */ +module test_taxi_xfcp_if_uart # +( + /* verilator lint_off WIDTHTRUNC */ + parameter TX_FIFO_DEPTH = 512, + parameter RX_FIFO_DEPTH = 512 + /* verilator lint_on WIDTHTRUNC */ +) +(); + +logic clk; +logic rst; + +logic uart_rxd; +logic uart_txd; + +taxi_axis_if #(.DATA_W(8), .LAST_EN(1), .USER_EN(1), .USER_W(1)) dn_xfcp_in(), dn_xfcp_out(); + +logic [15:0] prescale; + +taxi_xfcp_if_uart #( + .TX_FIFO_DEPTH(TX_FIFO_DEPTH), + .RX_FIFO_DEPTH(RX_FIFO_DEPTH) +) +uut ( + .clk(clk), + .rst(rst), + + /* + * UART interface + */ + .uart_rxd(uart_rxd), + .uart_txd(uart_txd), + + /* + * XFCP downstream port + */ + .dn_xfcp_in(dn_xfcp_in), + .dn_xfcp_out(dn_xfcp_out), + + /* + * Configuration + */ + .prescale(prescale) +); + +endmodule + +`resetall diff --git a/tb/xfcp/taxi_xfcp_if_uart/xfcp.py b/tb/xfcp/taxi_xfcp_if_uart/xfcp.py new file mode 120000 index 0000000..7b61bc4 --- /dev/null +++ b/tb/xfcp/taxi_xfcp_if_uart/xfcp.py @@ -0,0 +1 @@ +../xfcp.py \ No newline at end of file diff --git a/tb/xfcp/xfcp.py b/tb/xfcp/xfcp.py new file mode 100644 index 0000000..174fbb0 --- /dev/null +++ b/tb/xfcp/xfcp.py @@ -0,0 +1,159 @@ +# SPDX-License-Identifier: MIT +""" + +Copyright (c) 2017-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +import struct + + +def cobs_encode(block): + block = bytes(block) + enc = bytearray() + + seg = bytearray() + code = 1 + + new_data = True + + for b in block: + if b == 0: + enc.append(code) + enc.extend(seg) + code = 1 + seg = bytearray() + new_data = True + else: + code += 1 + seg.append(b) + new_data = True + if code == 255: + enc.append(code) + enc.extend(seg) + code = 1 + seg = bytearray() + new_data = False + + if new_data: + enc.append(code) + enc.extend(seg) + + return bytes(enc) + + +def cobs_decode(block): + block = bytes(block) + dec = bytearray() + + code = 0 + + i = 0 + + if 0 in block: + return None + + while i < len(block): + code = block[i] + i += 1 + if i+code-1 > 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})"