From 80fa0b5b80e3c6a535e351d75c2499bca1c39f33 Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Wed, 26 Aug 2026 18:01:49 +0100 Subject: [PATCH] Hold switchboard transactors in reset while the design is in reset autowrap generated a "rstvec" shift register for the DUT's resets, but never passed it to the transactors, so every transactor ran with its reset input tied to 1'b0. A DUT whose ready signals are high during reset (common for simple AXI-Lite register interfaces) accepts those transactions and never responds, hanging the simulation. Pass rstvec[max_rst_dly] to each transactor macro. That bit is the last one to de-assert, so it is the logical OR of every reset driven into the design, keeping the transactors quiet until the whole design is out of reset. The reset vector is now emitted before the interfaces, since the transactors reference it. The SB_AXI macros had no reset argument at all, leaving sb_axi_m's reset port unconnected; add one, along with QUEUE_TO_SB_SIM/SB_TO_QUEUE_SIM. All default to 1'b0, so hand-written testbenches are unaffected. Add an "axil_reset" example as a regression test: its DUT models a register interface whose handshake logic ignores reset, so all three ready signals are high from time zero, and it counts the transactions accepted while reset was asserted. With the fix that count is 0; with the fix reverted it is 1 and the test fails. Also fixes a missing "idw" argument in the SB_AXI_S invocation, wires up the reset in the hand-written axil_ram testbench, and adds a test that checks the generated wrapper holds each transactor in reset. Fixes #275 --- examples/axil_ram/testbench.sv | 2 +- examples/axil_reset/Makefile | 17 +++ examples/axil_reset/README.md | 20 ++++ examples/axil_reset/axil_reset_check.sv | 121 ++++++++++++++++++++++ examples/axil_reset/test.py | 107 +++++++++++++++++++ examples/test_autowrap.py | 58 +++++++++++ examples/test_examples.py | 2 + switchboard/autowrap.py | 92 +++++++++------- switchboard/verilog/common/switchboard.vh | 17 +-- 9 files changed, 392 insertions(+), 44 deletions(-) create mode 100755 examples/axil_reset/Makefile create mode 100644 examples/axil_reset/README.md create mode 100644 examples/axil_reset/axil_reset_check.sv create mode 100755 examples/axil_reset/test.py create mode 100644 examples/test_autowrap.py diff --git a/examples/axil_ram/testbench.sv b/examples/axil_ram/testbench.sv index 3c308ee8..a516d854 100644 --- a/examples/axil_ram/testbench.sv +++ b/examples/axil_ram/testbench.sv @@ -118,7 +118,7 @@ module testbench ( .ADDR_WIDTH (ADDR_WIDTH)) sb_axil_m_i ( .clk (clk), - .reset (1'b0), + .reset (~nreset), .m_axil_awaddr (s_axil_awaddr), .m_axil_awprot (s_axil_awprot), diff --git a/examples/axil_reset/Makefile b/examples/axil_reset/Makefile new file mode 100755 index 00000000..3be62855 --- /dev/null +++ b/examples/axil_reset/Makefile @@ -0,0 +1,17 @@ +# Copyright (c) 2024 Zero ASIC Corporation +# This code is licensed under Apache License 2.0 (see LICENSE for details) + +.PHONY: verilator +verilator: + ./test.py --tool verilator + +.PHONY: icarus +icarus: + ./test.py --tool icarus + +.PHONY: clean +clean: + rm -f queue-* *.q + rm -f *.vcd *.fst *.fst.hier + rm -rf obj_dir build + rm -f *.o *.vpi diff --git a/examples/axil_reset/README.md b/examples/axil_reset/README.md new file mode 100644 index 00000000..8127f4c8 --- /dev/null +++ b/examples/axil_reset/README.md @@ -0,0 +1,20 @@ +# axil_reset + +Regression test for [issue #275](https://github.com/zeroasiccorp/switchboard/issues/275). + +The DUT (`axil_reset_check.sv`) models a simple AXI-Lite register interface whose +handshake logic ignores reset, so all three `ready` signals are high from time +zero -- including while the design is still in reset. It counts every transaction +accepted while `rst` was asserted and returns that count for any read. + +A transactor that drives transactions without regard to reset gets them accepted +before the design is ready. In a real DUT those transactions are silently dropped +and the simulation hangs waiting for a response; here the count is non-zero and +the test fails instead. + +Run with: + +```console +make verilator +make icarus +``` diff --git a/examples/axil_reset/axil_reset_check.sv b/examples/axil_reset/axil_reset_check.sv new file mode 100644 index 00000000..7ffb4005 --- /dev/null +++ b/examples/axil_reset/axil_reset_check.sv @@ -0,0 +1,121 @@ +// AXI-Lite subordinate that models the class of DUT described in +// https://github.com/zeroasiccorp/switchboard/issues/275: a "simple axi lite +// register interface" whose handshake logic ignores reset, so "ready" is high +// by default -- including while the design is still in reset. +// +// A transactor that drives transactions without regard to reset will have them +// accepted before the design is ready. In a real DUT those transactions are +// silently dropped and the simulation hangs waiting for a response that never +// comes. To keep the failure observable rather than fatal, this model still +// responds to every transaction, and counts the ones that were accepted while +// reset was asserted. The count is readable over the same AXI-Lite port, so +// the testbench can assert that it is zero. + +// Copyright (c) 2026 Zero ASIC Corporation +// This code is licensed under Apache License 2.0 (see LICENSE for details) + +`default_nettype none + +module axil_reset_check #( + parameter DATA_WIDTH = 32, + parameter ADDR_WIDTH = 8, + parameter STRB_WIDTH = (DATA_WIDTH/8) +) ( + input wire clk, + input wire rst, + + // AXI-Lite subordinate interface + input wire [ADDR_WIDTH-1:0] s_axil_awaddr, + input wire [2:0] s_axil_awprot, + input wire s_axil_awvalid, + output wire s_axil_awready, + input wire [DATA_WIDTH-1:0] s_axil_wdata, + input wire [STRB_WIDTH-1:0] s_axil_wstrb, + input wire s_axil_wvalid, + output wire s_axil_wready, + output wire [1:0] s_axil_bresp, + output reg s_axil_bvalid = 1'b0, + input wire s_axil_bready, + input wire [ADDR_WIDTH-1:0] s_axil_araddr, + input wire [2:0] s_axil_arprot, + input wire s_axil_arvalid, + output wire s_axil_arready, + output reg [DATA_WIDTH-1:0] s_axil_rdata = 'b0, + output wire [1:0] s_axil_rresp, + output reg s_axil_rvalid = 1'b0, + input wire s_axil_rready +); + // unused inputs + /* verilator lint_off UNUSEDSIGNAL */ + wire [ADDR_WIDTH-1:0] unused_awaddr = s_axil_awaddr; + wire [2:0] unused_awprot = s_axil_awprot; + wire [DATA_WIDTH-1:0] unused_wdata = s_axil_wdata; + wire [STRB_WIDTH-1:0] unused_wstrb = s_axil_wstrb; + wire [ADDR_WIDTH-1:0] unused_araddr = s_axil_araddr; + wire [2:0] unused_arprot = s_axil_arprot; + /* verilator lint_on UNUSEDSIGNAL */ + + assign s_axil_bresp = 2'b00; + assign s_axil_rresp = 2'b00; + + // the write halves are tracked independently, and "ready" is simply the + // absence of anything outstanding. nothing here is gated on rst, which is + // the whole point of the model: at time zero all three ready signals are + // high, so a transactor that ignores reset gets its transactions accepted. + + reg aw_pending = 1'b0; + reg w_pending = 1'b0; + + assign s_axil_awready = !aw_pending; + assign s_axil_wready = !w_pending; + assign s_axil_arready = !s_axil_rvalid; + + wire aw_accepted = s_axil_awvalid && s_axil_awready; + wire w_accepted = s_axil_wvalid && s_axil_wready; + wire ar_accepted = s_axil_arvalid && s_axil_arready; + + // count transactions accepted while reset was asserted. deliberately not + // cleared by rst -- it is a record of what happened during reset. + localparam CNT_WIDTH = 8; + + reg [CNT_WIDTH-1:0] rst_xacts = 'b0; + + always @(posedge clk) begin + if (rst && (aw_accepted || ar_accepted)) begin + if (rst_xacts != {CNT_WIDTH{1'b1}}) begin + rst_xacts <= rst_xacts + 1'b1; + end + end + end + + always @(posedge clk) begin + // write address / data + if (aw_accepted) begin + aw_pending <= 1'b1; + end + + if (w_accepted) begin + w_pending <= 1'b1; + end + + // issue the write response once both halves have arrived + if (aw_pending && w_pending && !s_axil_bvalid) begin + s_axil_bvalid <= 1'b1; + aw_pending <= 1'b0; + w_pending <= 1'b0; + end else if (s_axil_bvalid && s_axil_bready) begin + s_axil_bvalid <= 1'b0; + end + + // every read returns the number of transactions accepted during reset + if (ar_accepted) begin + s_axil_rvalid <= 1'b1; + s_axil_rdata <= {{(DATA_WIDTH-CNT_WIDTH){1'b0}}, rst_xacts}; + end else if (s_axil_rvalid && s_axil_rready) begin + s_axil_rvalid <= 1'b0; + end + end + +endmodule + +`default_nettype wire diff --git a/examples/axil_reset/test.py b/examples/axil_reset/test.py new file mode 100755 index 00000000..702c22ac --- /dev/null +++ b/examples/axil_reset/test.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 + +# Regression test for https://github.com/zeroasiccorp/switchboard/issues/275: +# transactors must not drive transactions into a DUT that is still in reset. + +# Copyright (c) 2026 Zero ASIC Corporation +# This code is licensed under Apache License 2.0 (see LICENSE for details) + +import sys + +import numpy as np + +from siliconcompiler import Design + +from switchboard import SbDut +from switchboard.verilog.sim.switchboard_sim import SwitchboardSim + + +def main(): + # build the simulator + dut = build_testbench() + + # launch the simulation + dut.simulate() + + axil = dut.intfs['s_axil'] + + # the DUT counts every transaction accepted while it was in reset, and + # returns that count for any read. drive some traffic first, so that a + # transactor which ignores reset has something to get wrong. + for addr in range(0, 4 * dut.args.n, 4): + axil.write(addr % 256, np.uint32(addr)) + + rst_xacts = int(axil.read(0, np.uint32)) + + print(f'Transactions accepted during reset: {rst_xacts}') + + if rst_xacts == 0: + print("PASS!") + sys.exit(0) + else: + print(f'FAIL: {rst_xacts} transaction(s) were driven into the DUT while' + ' it was still in reset') + sys.exit(1) + + +class AxilResetCheck(Design): + + def __init__(self): + super().__init__("axil_reset_check") + + top_module = "axil_reset_check" + + self.set_dataroot("axil_reset", __file__) + + with self.active_fileset('rtl'): + self.set_topmodule(top_module) + self.add_depfileset(SwitchboardSim()) + self.add_file("axil_reset_check.sv") + + with self.active_fileset('verilator'): + self.set_topmodule(top_module) + self.add_depfileset(self, "rtl") + + with self.active_fileset('icarus'): + self.set_topmodule(top_module) + self.add_depfileset(self, "rtl") + + +def build_testbench(): + dw = 32 + aw = 8 + + parameters = dict( + DATA_WIDTH=dw, + ADDR_WIDTH=aw + ) + + interfaces = { + 's_axil': dict(type='axil', dw=dw, aw=aw, direction='subordinate') + } + + # a long reset makes the failure deterministic: without the fix the + # transactor drives a transaction on essentially the first clock edge + resets = [dict(name='rst', delay=8)] + + extra_args = { + '-n': dict(type=int, default=16, help='Number of writes to perform.') + } + + dut = SbDut( + design=AxilResetCheck(), + cmdline=True, + autowrap=True, + parameters=parameters, + interfaces=interfaces, + resets=resets, + extra_args=extra_args + ) + + dut.build() + + return dut + + +if __name__ == '__main__': + main() diff --git a/examples/test_autowrap.py b/examples/test_autowrap.py new file mode 100644 index 00000000..b4fdb4ff --- /dev/null +++ b/examples/test_autowrap.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python + +# Copyright (c) 2026 Zero ASIC Corporation +# This code is licensed under Apache License 2.0 (see LICENSE for details) + +import pytest + +from switchboard.autowrap import autowrap + + +def wrap(tmp_path, interfaces, resets): + filename = autowrap( + instances={'dut': 'dut'}, + parameters={'dut': {}}, + interfaces={'dut': interfaces}, + clocks={'dut': ['clk']}, + resets={'dut': resets}, + tieoffs={'dut': {}}, + filename=tmp_path / 'testbench.sv' + ) + + return filename.read_text() + + +# the transactors have to be held in reset for as long as any part of the design +# is in reset. otherwise transactions are driven into a DUT that hasn't come out +# of reset yet, which hangs the simulation if the DUT holds its "ready" signals +# high during reset. see https://github.com/zeroasiccorp/switchboard/issues/275 + +@pytest.mark.parametrize('name,intf,macro', [ + ('sb_in', dict(type='sb', dw=32, direction='input'), 'QUEUE_TO_SB_SIM'), + ('sb_out', dict(type='sb', dw=32, direction='output'), 'SB_TO_QUEUE_SIM'), + ('umi_in', dict(type='umi', dw=32, cw=32, aw=64, direction='input'), 'QUEUE_TO_UMI_SIM'), + ('umi_out', dict(type='umi', dw=32, cw=32, aw=64, direction='output'), 'UMI_TO_QUEUE_SIM'), + ('s_axi', dict(type='axi', dw=32, aw=16, idw=8, direction='subordinate'), 'SB_AXI_M'), + ('s_axil', dict(type='axil', dw=32, aw=16, direction='subordinate'), 'SB_AXIL_M'), +]) +def test_transactor_held_in_reset(tmp_path, name, intf, macro): + text = wrap(tmp_path, {name: intf}, [dict(name='rst', delay=8)]) + + line = next(line for line in text.splitlines() if macro in line) + + # the transactor reset is the last bit of the reset vector to be de-asserted, + # making it the logical OR of every reset driven into the design + assert line.rstrip().endswith('rstvec[8]);'), line + + # the reset vector has to be declared before the transactors that use it + assert text.index('rstvec = ') < text.index(macro) + + +def test_transactor_reset_tied_off_without_resets(tmp_path): + text = wrap(tmp_path, {'s_axil': dict(type='axil', dw=32, aw=16, + direction='subordinate')}, []) + + assert 'rstvec' not in text + + line = next(line for line in text.splitlines() if 'SB_AXIL_M' in line) + assert line.rstrip().endswith("1'b0);"), line diff --git a/examples/test_examples.py b/examples/test_examples.py index 8f2255bb..f6569785 100755 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -20,6 +20,8 @@ @pytest.mark.parametrize('path,expected,target', [ ['axil', 'PASS!', 'icarus'], ['axil', 'PASS!', 'verilator'], + ['axil_reset', 'PASS!', 'icarus'], + ['axil_reset', 'PASS!', 'verilator'], # ['minimal', 'PASS!', 'icarus'], # ['minimal', 'PASS!', 'verilator'], ['network', None, 'verilator'], diff --git a/switchboard/autowrap.py b/switchboard/autowrap.py index 43ffae18..9834de41 100644 --- a/switchboard/autowrap.py +++ b/switchboard/autowrap.py @@ -317,6 +317,46 @@ def autowrap( '' ] + # reset vector + + max_rst_dly = None + + for inst_resets in resets.values(): + if len(inst_resets) > 0: + # find the max reset delay for this instance + inst_max_rst_dly = max(reset['delay'] for reset in inst_resets) + + # update the overall max reset delay + if (max_rst_dly is None) or (inst_max_rst_dly > max_rst_dly): + max_rst_dly = inst_max_rst_dly + + if max_rst_dly is not None: + lines += [ + tab + f"reg [{max_rst_dly}:0] rstvec = '1;", + '', + tab + 'always @(posedge clk) begin' + ] + + if max_rst_dly > 0: + lines += [(2 * tab) + f"rstvec <= {{rstvec[{max_rst_dly - 1}:0], 1'b0}};"] + else: + lines += [(2 * tab) + "rstvec <= 1'b0;"] + + lines += [ + tab + 'end', + '' + ] + + # hold the switchboard transactors in reset until every reset in the + # design has been released. rstvec[max_rst_dly] is the last bit of the + # reset vector to be de-asserted, so it is the logical OR of all of the + # reset signals driven into the design. without this, transactions are + # driven into a DUT that is still in reset, which hangs the simulation if + # the DUT happens to hold its "ready" signals high during reset. + xactor_rst = f'rstvec[{max_rst_dly}]' + else: + xactor_rst = "1'b0" + # wire declarations wires = {} @@ -399,9 +439,11 @@ def autowrap( if external: if direction_is_input(direction): - lines += [tab + f'`QUEUE_TO_SB_SIM({wire}, {dw}, "");'] + lines += [tab + f'`QUEUE_TO_SB_SIM({wire}, {dw}, "", 1, clk, ' + f'{xactor_rst});'] elif direction_is_output(direction): - lines += [tab + f'`SB_TO_QUEUE_SIM({wire}, {dw}, "");'] + lines += [tab + f'`SB_TO_QUEUE_SIM({wire}, {dw}, "", 1, clk, ' + f'{xactor_rst});'] else: raise Exception(f'Unsupported SB direction: {direction}') elif type == 'umi': @@ -414,9 +456,11 @@ def autowrap( if external: if direction_is_input(direction): - lines += [tab + f'`QUEUE_TO_UMI_SIM({wire}, {dw}, {cw}, {aw}, "");'] + lines += [tab + f'`QUEUE_TO_UMI_SIM({wire}, {dw}, {cw}, {aw}, ' + f'"", 1, clk, {xactor_rst});'] elif direction_is_output(direction): - lines += [tab + f'`UMI_TO_QUEUE_SIM({wire}, {dw}, {cw}, {aw}, "");'] + lines += [tab + f'`UMI_TO_QUEUE_SIM({wire}, {dw}, {cw}, {aw}, ' + f'"", 1, clk, {xactor_rst});'] else: raise Exception(f'Unsupported UMI direction: {direction}') elif type == 'axi': @@ -429,9 +473,11 @@ def autowrap( if external: if direction_is_subordinate(direction): - lines += [tab + f'`SB_AXI_M({wire}, {dw}, {aw}, {idw}, "");'] + lines += [tab + f'`SB_AXI_M({wire}, {dw}, {aw}, {idw}, ' + f'"", 1, 1, clk, {xactor_rst});'] elif direction_is_manager(direction): - lines += [tab + f'`SB_AXI_S({wire}, {dw}, {aw}, "");'] + lines += [tab + f'`SB_AXI_S({wire}, {dw}, {aw}, {idw}, ' + f'"", 1, 1, clk, {xactor_rst});'] else: raise Exception(f'Unsupported AXI direction: {direction}') elif type == 'axil': @@ -443,9 +489,11 @@ def autowrap( if external: if direction_is_subordinate(direction): - lines += [tab + f'`SB_AXIL_M({wire}, {dw}, {aw}, "");'] + lines += [tab + f'`SB_AXIL_M({wire}, {dw}, {aw}, "", 1, 1, clk, ' + f'{xactor_rst});'] elif direction_is_manager(direction): - lines += [tab + f'`SB_AXIL_S({wire}, {dw}, {aw}, "");'] + lines += [tab + f'`SB_AXIL_S({wire}, {dw}, {aw}, "", 1, 1, clk, ' + f'{xactor_rst});'] else: raise Exception(f'Unsupported AXI-Lite direction: {direction}') elif type == 'gpio': @@ -486,34 +534,6 @@ def autowrap( lines += [''] - max_rst_dly = None - - for inst_resets in resets.values(): - if len(inst_resets) > 0: - # find the max reset delay for this instance - inst_max_rst_dly = max(reset['delay'] for reset in inst_resets) - - # update the overall max reset delay - if (max_rst_dly is None) or (inst_max_rst_dly > max_rst_dly): - max_rst_dly = inst_max_rst_dly - - if max_rst_dly is not None: - lines += [ - tab + f"reg [{max_rst_dly}:0] rstvec = '1;" - '', - tab + 'always @(posedge clk) begin' - ] - - if max_rst_dly > 0: - lines += [(2 * tab) + f"rstvec <= {{rstvec[{max_rst_dly - 1}:0], 1'b0}};"] - else: - lines += [(2 * tab) + "rstvec <= 1'b0;"] - - lines += [ - tab + 'end', - '' - ] - for instance, module in instances.items(): # start of the instantiation diff --git a/switchboard/verilog/common/switchboard.vh b/switchboard/verilog/common/switchboard.vh index 8ad57903..55f3c967 100644 --- a/switchboard/verilog/common/switchboard.vh +++ b/switchboard/verilog/common/switchboard.vh @@ -128,13 +128,14 @@ `define SB_OUTPUT(signal, dw) \ `SB_PORT(signal, dw, output, input) -`define SB_TO_QUEUE_SIM(signal, dw, file, rdymode=1, clk_signal=clk) \ +`define SB_TO_QUEUE_SIM(signal, dw, file, rdymode=1, clk_signal=clk, reset_sig=1'b0) \ sb_to_queue_sim #( \ .READY_MODE_DEFAULT(rdymode), \ .DW(dw), \ .FILE(file) \ ) signal``_sb_inst ( \ .clk(clk_signal), \ + .reset(reset_sig), \ .data(signal``_data), \ .dest(signal``_dest), \ .last(signal``_last), \ @@ -142,13 +143,14 @@ .valid(signal``_valid) \ ) -`define QUEUE_TO_SB_SIM(signal, dw, file, vldmode=1, clk_signal=clk) \ +`define QUEUE_TO_SB_SIM(signal, dw, file, vldmode=1, clk_signal=clk, reset_sig=1'b0) \ queue_to_sb_sim #( \ .VALID_MODE_DEFAULT(vldmode), \ .DW(dw), \ .FILE(file) \ ) signal``_sb_inst ( \ .clk(clk_signal), \ + .reset(reset_sig), \ .data(signal``_data), \ .dest(signal``_dest), \ .last(signal``_last), \ @@ -342,7 +344,7 @@ .a``_rvalid(b``_rvalid), \ .a``_rready(b``_rready) -`define SB_AXI(dir, signal, dw, aw, idw, file, vldmode=1, rdymode=1, clk_signal=clk) \ +`define SB_AXI(dir, signal, dw, aw, idw, file, vldmode=1, rdymode=1, clk_signal=clk, rst_signal='0)\ sb_axi_``dir #( \ .DATA_WIDTH(dw), \ .ADDR_WIDTH(aw), \ @@ -352,6 +354,7 @@ .FILE(file) \ ) signal``_sb_inst ( \ .clk(clk_signal), \ + .reset(rst_signal), \ .dir``_axi_awid(signal``_awid), \ .dir``_axi_awaddr(signal``_awaddr), \ .dir``_axi_awlen(signal``_awlen), \ @@ -389,11 +392,11 @@ .dir``_axi_rready(signal``_rready) \ ) -`define SB_AXI_M(signal, dw, aw, idw, file, vldmode=1, rdymode=1, clk_signal=clk) \ - `SB_AXI(m, signal, dw, aw, idw, file, vldmode, rdymode, clk_signal) +`define SB_AXI_M(signal, dw, aw, idw, file, vldmode=1, rdymode=1, clk_signal=clk, rst_signal=1'b0) \ + `SB_AXI(m, signal, dw, aw, idw, file, vldmode, rdymode, clk_signal, rst_signal) -`define SB_AXI_S(signal, dw, aw, idw, file, vldmode=1, rdymode=1, clk_signal=clk) \ - `SB_AXI(s, signal, dw, aw, idw, file, vldmode, rdymode, clk_signal) +`define SB_AXI_S(signal, dw, aw, idw, file, vldmode=1, rdymode=1, clk_signal=clk, rst_signal=1'b0) \ + `SB_AXI(s, signal, dw, aw, idw, file, vldmode, rdymode, clk_signal, rst_signal) `define SB_CREATE_CLOCK(clk_signal, period=10e-9, duty_cycle=0.5, max_rate=-1, start_delay=-1) \ wire clk_signal; \