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; \