From c4558a02f01081e65a34f752a0f5a1039856ab27 Mon Sep 17 00:00:00 2001 From: Alex Forencich Date: Mon, 3 Feb 2025 15:02:48 -0800 Subject: [PATCH] lss: Add UART module and testbench Signed-off-by: Alex Forencich --- rtl/lss/taxi_uart.f | 4 + rtl/lss/taxi_uart.sv | 117 ++++++++++++++++++ rtl/lss/taxi_uart_rx.sv | 135 +++++++++++++++++++++ rtl/lss/taxi_uart_tx.sv | 106 ++++++++++++++++ tb/lss/taxi_uart/Makefile | 46 +++++++ tb/lss/taxi_uart/test_taxi_uart.py | 187 +++++++++++++++++++++++++++++ tb/lss/taxi_uart/test_taxi_uart.sv | 81 +++++++++++++ 7 files changed, 676 insertions(+) create mode 100644 rtl/lss/taxi_uart.f create mode 100644 rtl/lss/taxi_uart.sv create mode 100644 rtl/lss/taxi_uart_rx.sv create mode 100644 rtl/lss/taxi_uart_tx.sv create mode 100644 tb/lss/taxi_uart/Makefile create mode 100644 tb/lss/taxi_uart/test_taxi_uart.py create mode 100644 tb/lss/taxi_uart/test_taxi_uart.sv diff --git a/rtl/lss/taxi_uart.f b/rtl/lss/taxi_uart.f new file mode 100644 index 0000000..cffa74e --- /dev/null +++ b/rtl/lss/taxi_uart.f @@ -0,0 +1,4 @@ +taxi_uart.sv +taxi_uart_rx.sv +taxi_uart_tx.sv +../axis/taxi_axis_if.sv diff --git a/rtl/lss/taxi_uart.sv b/rtl/lss/taxi_uart.sv new file mode 100644 index 0000000..327735a --- /dev/null +++ b/rtl/lss/taxi_uart.sv @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2014-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4-Stream UART + */ +module taxi_uart # +( + parameter DATA_W = 8 +) +( + input wire logic clk, + input wire logic rst, + + /* + * AXI4-Stream input (sink) + */ + taxi_axis_if.snk s_axis_tx, + + /* + * AXI4-Stream output (source) + */ + taxi_axis_if.src m_axis_rx, + + /* + * UART interface + */ + input wire logic rxd, + output wire logic txd, + + /* + * Status + */ + output wire logic tx_busy, + output wire logic rx_busy, + output wire logic rx_overrun_error, + output wire logic rx_frame_error, + + /* + * Configuration + */ + input wire logic [15:0] prescale + +); + +taxi_uart_tx #( + .DATA_W(DATA_W) +) +uart_tx_inst ( + .clk(clk), + .rst(rst), + + /* + * AXI4-Stream input (sink) + */ + .s_axis_tx(s_axis_tx), + + /* + * UART interface + */ + .txd(txd), + + /* + * Status + */ + .busy(tx_busy), + + /* + * Configuration + */ + .prescale(prescale) +); + +taxi_uart_rx #( + .DATA_W(DATA_W) +) +uart_rx_inst ( + .clk(clk), + .rst(rst), + + /* + * AXI4-Stream output (source) + */ + .m_axis_rx(m_axis_rx), + + /* + * UART interface + */ + .rxd(rxd), + + /* + * Status + */ + .busy(rx_busy), + .overrun_error(rx_overrun_error), + .frame_error(rx_frame_error), + + /* + * Configuration + */ + .prescale(prescale) +); + +endmodule + +`resetall diff --git a/rtl/lss/taxi_uart_rx.sv b/rtl/lss/taxi_uart_rx.sv new file mode 100644 index 0000000..7382608 --- /dev/null +++ b/rtl/lss/taxi_uart_rx.sv @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2014-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4-Stream UART (RX) + */ +module taxi_uart_rx # +( + parameter DATA_W = 8 +) +( + input wire logic clk, + input wire logic rst, + + /* + * AXI4-Stream output (source) + */ + taxi_axis_if.src m_axis_rx, + + /* + * UART interface + */ + input wire logic rxd, + + /* + * Status + */ + output wire logic busy, + output wire logic overrun_error, + output wire logic frame_error, + + /* + * Configuration + */ + input wire logic [15:0] prescale + +); + +// check configuration +if (m_axis_rx.DATA_W != DATA_W) + $fatal(0, "Error: Interface parameter DATA_W mismatch (instance %m)"); + +logic [DATA_W-1:0] m_axis_tdata_reg = 0; +logic m_axis_tvalid_reg = 0; + +logic rxd_reg = 1; + +logic busy_reg = 0; +logic overrun_error_reg = 0; +logic frame_error_reg = 0; + +logic [DATA_W-1:0] data_reg = 0; +logic [18:0] prescale_reg = 0; +logic [3:0] bit_cnt_reg = 0; + +assign m_axis_rx.tdata = m_axis_tdata_reg; +assign m_axis_rx.tkeep = 1'b1; +assign m_axis_rx.tstrb = m_axis_rx.tkeep; +assign m_axis_rx.tvalid = m_axis_tvalid_reg; +assign m_axis_rx.tlast = 1'b1; + +assign busy = busy_reg; +assign overrun_error = overrun_error_reg; +assign frame_error = frame_error_reg; + +always_ff @(posedge clk) begin + rxd_reg <= rxd; + overrun_error_reg <= 0; + frame_error_reg <= 0; + + if (m_axis_rx.tvalid && m_axis_rx.tready) begin + m_axis_tvalid_reg <= 0; + end + + if (prescale_reg > 0) begin + prescale_reg <= prescale_reg - 1; + end else if (bit_cnt_reg > 0) begin + if (bit_cnt_reg > DATA_W+1) begin + if (!rxd_reg) begin + bit_cnt_reg <= bit_cnt_reg - 1; + prescale_reg <= {prescale, 3'd0}-1; + end else begin + bit_cnt_reg <= 0; + prescale_reg <= 0; + end + end else if (bit_cnt_reg > 1) begin + bit_cnt_reg <= bit_cnt_reg - 1; + prescale_reg <= {prescale, 3'd0}-1; + data_reg <= {rxd_reg, data_reg[DATA_W-1:1]}; + end else if (bit_cnt_reg == 1) begin + bit_cnt_reg <= bit_cnt_reg - 1; + if (rxd_reg) begin + m_axis_tdata_reg <= data_reg; + m_axis_tvalid_reg <= 1; + overrun_error_reg <= m_axis_tvalid_reg; + end else begin + frame_error_reg <= 1; + end + end + end else begin + busy_reg <= 0; + if (!rxd_reg) begin + prescale_reg <= {prescale, 2'd0}-2; + bit_cnt_reg <= DATA_W+2; + data_reg <= 0; + busy_reg <= 1; + end + end + + if (rst) begin + m_axis_tdata_reg <= 0; + m_axis_tvalid_reg <= 0; + rxd_reg <= 1; + prescale_reg <= 0; + bit_cnt_reg <= 0; + busy_reg <= 0; + overrun_error_reg <= 0; + frame_error_reg <= 0; + end +end + +endmodule + +`resetall diff --git a/rtl/lss/taxi_uart_tx.sv b/rtl/lss/taxi_uart_tx.sv new file mode 100644 index 0000000..a695986 --- /dev/null +++ b/rtl/lss/taxi_uart_tx.sv @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2014-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4-Stream UART (TX) + */ +module taxi_uart_tx # +( + parameter DATA_W = 8 +) +( + input wire logic clk, + input wire logic rst, + + /* + * AXI4-Stream input (sink) + */ + taxi_axis_if.snk s_axis_tx, + + /* + * UART interface + */ + output wire logic txd, + + /* + * Status + */ + output wire logic busy, + + /* + * Configuration + */ + input wire logic [15:0] prescale +); + +// check configuration +if (s_axis_tx.DATA_W != DATA_W) + $fatal(0, "Error: Interface parameter DATA_W mismatch (instance %m)"); + +logic s_axis_tready_reg = 0; + +logic txd_reg = 1; + +logic busy_reg = 0; + +logic [DATA_W:0] data_reg = 0; +logic [18:0] prescale_reg = 0; +logic [3:0] bit_cnt_reg = 0; + +assign s_axis_tx.tready = s_axis_tready_reg; + +assign txd = txd_reg; + +assign busy = busy_reg; + +always_ff @(posedge clk) begin + if (prescale_reg > 0) begin + s_axis_tready_reg <= 0; + prescale_reg <= prescale_reg - 1; + end else if (bit_cnt_reg == 0) begin + s_axis_tready_reg <= 1; + busy_reg <= 0; + + if (s_axis_tx.tvalid) begin + s_axis_tready_reg <= !s_axis_tready_reg; + prescale_reg <= {prescale, 3'd0}-1; + bit_cnt_reg <= DATA_W+1; + data_reg <= {1'b1, s_axis_tx.tdata}; + txd_reg <= 0; + busy_reg <= 1; + end + end else begin + if (bit_cnt_reg > 1) begin + bit_cnt_reg <= bit_cnt_reg - 1; + prescale_reg <= {prescale, 3'd0}-1; + {data_reg, txd_reg} <= {1'b0, data_reg}; + end else if (bit_cnt_reg == 1) begin + bit_cnt_reg <= bit_cnt_reg - 1; + prescale_reg <= {prescale, 3'd0}-1; + txd_reg <= 1; + end + end + + if (rst) begin + s_axis_tready_reg <= 0; + txd_reg <= 1; + prescale_reg <= 0; + bit_cnt_reg <= 0; + busy_reg <= 0; + end +end + +endmodule + +`resetall diff --git a/tb/lss/taxi_uart/Makefile b/tb/lss/taxi_uart/Makefile new file mode 100644 index 0000000..5eec62a --- /dev/null +++ b/tb/lss/taxi_uart/Makefile @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: CERN-OHL-S-2.0 +# +# Copyright (c) 2020-2025 FPGA Ninja, LLC +# +# Authors: +# - Alex Forencich + +TOPLEVEL_LANG = verilog + +SIM ?= verilator +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ns + +DUT = taxi_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/lss/$(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_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/lss/taxi_uart/test_taxi_uart.py b/tb/lss/taxi_uart/test_taxi_uart.py new file mode 100644 index 0000000..ca46b95 --- /dev/null +++ b/tb/lss/taxi_uart/test_taxi_uart.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: CERN-OHL-S-2.0 +""" + +Copyright (c) 2020-2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +import itertools +import logging +import os + +import cocotb_test.simulator + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge, Timer +from cocotb.regression import TestFactory + +from cocotbext.axi import AxiStreamSource, AxiStreamSink, AxiStreamBus +from cocotbext.uart import UartSource, UartSink + + +class TB: + 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.rxd, baud=baud, bits=len(dut.m_axis_rx.tdata), stop_bits=1) + self.uart_sink = UartSink(dut.txd, baud=baud, bits=len(dut.s_axis_tx.tdata), stop_bits=1) + + self.axis_source = AxiStreamSource(AxiStreamBus.from_entity(dut.s_axis_tx), dut.clk, dut.rst) + self.axis_sink = AxiStreamSink(AxiStreamBus.from_entity(dut.m_axis_rx), 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()]: + + await tb.axis_source.write(test_data) + + rx_data = bytearray() + + while len(rx_data) < len(test_data): + rx_data.extend(await tb.uart_sink.read()) + + tb.log.info("Read data: %s", rx_data) + + assert tb.uart_sink.empty() + + await Timer(2, 'us') + + 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()]: + + await tb.uart_source.write(test_data) + + rx_data = bytearray() + + while len(rx_data) < len(test_data): + rx_data.extend(await tb.axis_sink.read()) + + tb.log.info("Read data: %s", rx_data) + + assert tb.axis_sink.empty() + + await Timer(2, 'us') + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +def prbs31(state=0x7fffffff): + while True: + for i in range(8): + if bool(state & 0x08000000) ^ bool(state & 0x40000000): + state = ((state & 0x3fffffff) << 1) | 1 + else: + state = (state & 0x3fffffff) << 1 + yield state & 0xff + + +def size_list(): + return list(range(1, 16)) + [128] + + +def incrementing_payload(length): + return bytearray(itertools.islice(itertools.cycle(range(256)), length)) + + +def prbs_payload(length): + gen = prbs31() + return bytearray([next(gen) for x in range(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, prbs_payload]) + factory.generate_tests() + + +# cocotb-test + +tests_dir = os.path.abspath(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_uart(request): + dut = "taxi_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, "lss", f"{dut}.f"), + ] + + verilog_sources = process_f_files(verilog_sources) + + parameters = {} + + parameters['DATA_W'] = 8 + + 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/lss/taxi_uart/test_taxi_uart.sv b/tb/lss/taxi_uart/test_taxi_uart.sv new file mode 100644 index 0000000..a36f596 --- /dev/null +++ b/tb/lss/taxi_uart/test_taxi_uart.sv @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2025 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4-Stream FIFO testbench + */ +module test_taxi_uart # +( + /* verilator lint_off WIDTHTRUNC */ + parameter DATA_W = 8 + /* verilator lint_on WIDTHTRUNC */ +) +(); + +logic clk; +logic rst; + +taxi_axis_if #(.DATA_W(DATA_W)) s_axis_tx(); +taxi_axis_if #(.DATA_W(DATA_W)) m_axis_rx(); + +logic rxd; +logic txd; + +logic tx_busy; +logic rx_busy; +logic rx_overrun_error; +logic rx_frame_error; + +logic [15:0] prescale; + +taxi_uart #( + .DATA_W(DATA_W) +) +uut ( + .clk(clk), + .rst(rst), + + /* + * AXI4-Stream input (sink) + */ + .s_axis_tx(s_axis_tx), + + /* + * AXI4-Stream output (source) + */ + .m_axis_rx(m_axis_rx), + + /* + * UART interface + */ + .rxd(rxd), + .txd(txd), + + /* + * Status + */ + .tx_busy(tx_busy), + .rx_busy(rx_busy), + .rx_overrun_error(rx_overrun_error), + .rx_frame_error(rx_frame_error), + + /* + * Configuration + */ + .prescale(prescale) +); + +endmodule + +`resetall