diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c66dfe2b..7f754922 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -52,6 +52,10 @@ jobs: - name: Check out Git repository uses: actions/checkout@v4 + # note that "sb_loopback.v" is not included in linting. verible doesn't + # seem to be able to take into account the macro definitions in the port + # list, so linting unfortunately needs to be disabled for that file. + - name: Lint with Verible run: | find . \( \ @@ -64,6 +68,7 @@ jobs: -or -path "./examples/deps/*" \ -or -name "axil_interconnect_wrap_1x2.v" \ -or -name "picorv32.v" \ + -or -name "sb_loopback.v" \ \) > files.txt cat files.txt verible-verilog-lint \ diff --git a/examples/README.md b/examples/README.md index 22c41fd4..e7e82de8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,3 +20,5 @@ The [python](python) example is similar to the [minimal](minimal) example, excep We also provide a mechanism for bridging switchboard connections over TCP, which may be useful if you're running a simulation or FPGA-based emulator on one machine, but want to interact with it from another machine. The [tcp example](tcp) shows how to set this up; it's mostly a matter of calling `start_tcp_bridge` on the server and client sides. Switchboard also supports mixed-signal simulation by using Xyce in conjunction with a digital simulator. The [xyce example](xyce) shows how this can be set up by instantiating a SPICE subcircuit as an ordinary Verilog module and calling `SbDut.input_analog()` to configure analog/digital interfaces. + +For a preview of switchboard's new features to automatically generate Verilog wrappers and construct networks of simulations dynamically, have a look at the [network](network) example. diff --git a/examples/axi/README.md b/examples/axi/README.md index 7ef96424..1b346c05 100644 --- a/examples/axi/README.md +++ b/examples/axi/README.md @@ -16,24 +16,20 @@ PASS! `make icarus` runs the example using Icarus Verilog as the digital simulator. -In the Verilog implementation, [testbench.sv](testbench.sv) instantiates a switchboard module that acts as an AXI manager. +In the Python script [test.py](test.py), an AXI interface is specified using the `interfaces` argument of `SbDut`. The name of the interface is `s_axi`, corresponding to the AXI port prefix in the `axi_ram` implementation. After simulation starts, the AXI interface object is retrieved from the `SbDut.intfs` dictionary. -```verilog -`include "switchboard.vh" +```python +interfaces = { + 's_axi': dict(type='axi', dw=dw, aw=aw, idw=idw, direction='subordinate') +} ... -`SB_AXI_M(axi, DATA_WIDTH, ADDR_WIDTH, ID_WIDTH, "axi"); -``` - -Based on the first argument, `axi`, the module instance is called `axi_sb_inst` and it connects to AXI signals starting with the prefix `axi`. The next three arguments specify the widths of the data, address, and ID buses, respectively. The last argument indicates that the switchboard queues to be used start with the prefix `axi`: `axi-aw.q`, `axi-w.q`, `axi-b.q`, `axi-ar.q`, `axi-r.q`. +dut = SbDut('axi_ram', ..., interfaces=interfaces, ...) -Various optional macro arguments can fine-tune the behavior, for example changing the clock signal name, which defaults to `clk`. - -In the Python script [test.py](test.py), a corresponding `AxiTxRx` object is created, using the same shorthand for connecting to all 5 queues. +... -```python -axi = AxiTxRx('axi', data_width=..., addr_width=..., id_width=...) +axi = dut.intfs['s_axi'] # type: AxiTxRx ``` As with `UmiTxRx`, this object may be used to issue read and write transactions involving numpy scalars and arrays. Under the hood, each transaction may be converted to multiple cycles of AXI transactions, with the write strobe automatically calculated in each cycle. diff --git a/examples/axi/test.py b/examples/axi/test.py index a5e50cd5..d1068b09 100755 --- a/examples/axi/test.py +++ b/examples/axi/test.py @@ -9,21 +9,23 @@ import random import numpy as np -from switchboard import SbDut, AxiTxRx +from switchboard import SbDut def main(): # build the simulator dut = build_testbench() - # create the queues - axi = AxiTxRx('axi', data_width=32, addr_width=13, id_width=8, max_beats=dut.args.max_beats) + # wire up max-beats argument + dut.intf_defs['s_axi']['max_beats'] = dut.args.max_beats # launch the simulation dut.simulate() # run the test: write to random addresses and read back in a random order + axi = dut.intfs['s_axi'] + addr_bytes = (axi.addr_width + 7) // 8 model = np.zeros((1 << axi.addr_width,), dtype=np.uint8) @@ -70,6 +72,22 @@ def main(): def build_testbench(): + dw = 32 + aw = 13 + idw = 8 + + parameters = dict( + DATA_WIDTH=dw, + ADDR_WIDTH=aw, + ID_WIDTH=idw, + ) + + interfaces = { + 's_axi': dict(type='axi', dw=dw, aw=aw, idw=idw, direction='subordinate') + } + + resets = [dict(name='rst', delay=8)] + extra_args = { '-n': dict(type=int, default=10000, help='Number of' ' words to write as part of the test.'), @@ -79,7 +97,8 @@ def build_testbench(): ' number of beats to use in AXI transfers.') } - dut = SbDut(cmdline=True, extra_args=extra_args) + dut = SbDut('axi_ram', autowrap=True, cmdline=True, extra_args=extra_args, + parameters=parameters, interfaces=interfaces, resets=resets) dut.register_package_source( 'verilog-axi', @@ -88,7 +107,6 @@ def build_testbench(): ) dut.input('rtl/axi_ram.v', package='verilog-axi') - dut.input('testbench.sv') dut.add('tool', 'verilator', 'task', 'compile', 'warningoff', ['WIDTHEXPAND', 'CASEINCOMPLETE', 'WIDTHTRUNC', 'TIMESCALEMOD']) diff --git a/examples/axi/testbench.sv b/examples/axi/testbench.sv deleted file mode 100644 index 8a6ef0a5..00000000 --- a/examples/axi/testbench.sv +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2024 Zero ASIC Corporation -// This code is licensed under Apache License 2.0 (see LICENSE for details) - -`default_nettype none - -`include "switchboard.vh" - -module testbench ( - `ifdef VERILATOR - input clk - `endif -); - `ifndef VERILATOR - `SB_CREATE_CLOCK(clk) - `endif - - // Declare AXI wires - - localparam DATA_WIDTH = 32; - localparam ADDR_WIDTH = 13; - localparam ID_WIDTH = 8; - - `SB_AXI_WIRES(axi, DATA_WIDTH, ADDR_WIDTH, ID_WIDTH); - - // Instantiate DUT - - wire rst; - - axi_ram #( - .DATA_WIDTH(DATA_WIDTH), - .ADDR_WIDTH(ADDR_WIDTH), - .ID_WIDTH(ID_WIDTH) - ) axi_ram_i ( - .clk(clk), - .rst(rst), - `SB_AXI_CONNECT(s_axi, axi) - ); - - // Instantiate switchboard module - - `SB_AXI_M(axi, DATA_WIDTH, ADDR_WIDTH, ID_WIDTH, "axi"); - - // Initialize RAM to zeros for easy comparison against a behavioral model - - localparam VALID_ADDR_WIDTH = ADDR_WIDTH - $clog2(DATA_WIDTH/8); - - initial begin - for (int i=0; i<2**VALID_ADDR_WIDTH; i=i+1) begin - axi_ram_i.mem[i] = 0; - end - end - - // Generate reset signal - - reg [7:0] rst_vec = 8'hFF; - - always @(posedge clk) begin - rst_vec <= {rst_vec[6:0], 1'b0}; - end - - assign rst = rst_vec[7]; - - // Set up waveform probing - - `SB_SETUP_PROBES - -endmodule - -`default_nettype wire diff --git a/examples/axil/README.md b/examples/axil/README.md index c57b22df..e60fec16 100644 --- a/examples/axil/README.md +++ b/examples/axil/README.md @@ -16,24 +16,20 @@ PASS! `make icarus` runs the example using Icarus Verilog as the digital simulator. -In the Verilog implementation, [testbench.sv](testbench.sv) instantiates a switchboard module that acts as an AXI-Lite manager. +In the Python script [test.py](test.py), an AXI-Lite interface is specified using the `interfaces` argument of `SbDut`. The name of the interface is `s_axil`, corresponding to the AXI-Lite port prefix in the `axil_ram` implementation. After simulation starts, the AXI-Lite interface object is retrieved from the `SbDut.intfs` dictionary. -```verilog -`include "switchboard.vh" +```python +interfaces = { + 's_axil': dict(type='axil', dw=dw, aw=aw, direction='subordinate') +} ... -`SB_AXIL_M(axil, DATA_WIDTH, ADDR_WIDTH, "axil"); -``` - -Based on the first argument, `axil`, the module instance is called `axil_sb_inst` and it connects to AXI-Lite signals starting with the prefix `axil`. The next two arguments specify the widths of the data and address buses, respectively. The last argument indicates that the switchboard queues to be used start with the prefix `axil`: `axil-aw.q`, `axil-w.q`, `axil-b.q`, `axil-ar.q`, `axil-r.q`. +dut = SbDut('axill_ram', ..., interfaces=interfaces, ...) -Various optional macro arguments can fine-tune the behavior, for example changing the clock signal name, which defaults to `clk`. - -In the Python script [test.py](test.py), a corresponding `AxiLiteTxRx` object is created, using the same shorthand for connecting to all 5 queues. +... -```python -axil = AxiLiteTxRx('axil', data_width=..., addr_width=...) +axil = dut.intfs['s_axil'] # type: AxiLiteTxRx ``` As with `UmiTxRx`, this object may be used to issue read and write transactions involving numpy scalars and arrays. Under the hood, each transaction may be converted to multiple cycles of AXI transactions, with the write strobe automatically calculated in each cycle. diff --git a/examples/axil/test.py b/examples/axil/test.py index 2a6678e8..0ba973b4 100755 --- a/examples/axil/test.py +++ b/examples/axil/test.py @@ -9,21 +9,20 @@ import random import numpy as np -from switchboard import SbDut, AxiLiteTxRx +from switchboard import SbDut def main(): # build the simulator dut = build_testbench() - # create the queues - axil = AxiLiteTxRx('axil', data_width=32, addr_width=13) - # launch the simulation dut.simulate() # run the test: write to random addresses and read back in a random order + axil = dut.intfs['s_axil'] + addr_bytes = (axil.addr_width + 7) // 8 model = np.zeros((1 << axil.addr_width,), dtype=np.uint8) @@ -70,16 +69,29 @@ def main(): def build_testbench(): + dw = 32 + aw = 13 + + parameters = dict( + DATA_WIDTH=dw, + ADDR_WIDTH=aw + ) + + interfaces = { + 's_axil': dict(type='axil', dw=dw, aw=aw, direction='subordinate') + } + + resets = [dict(name='rst', delay=8)] + extra_args = { '-n': dict(type=int, default=10000, help='Number of' ' words to write as part of the test.'), '--max-bytes': dict(type=int, default=10, help='Maximum' - ' number of bytes in any single read/write.'), - '--max-beats': dict(type=int, default=256, help='Maximum' - ' number of beats to use in AXI transfers.') + ' number of bytes in any single read/write.') } - dut = SbDut(cmdline=True, extra_args=extra_args) + dut = SbDut('axil_ram', autowrap=True, cmdline=True, extra_args=extra_args, + parameters=parameters, interfaces=interfaces, resets=resets) dut.register_package_source( 'verilog-axi', @@ -88,7 +100,6 @@ def build_testbench(): ) dut.input('rtl/axil_ram.v', package='verilog-axi') - dut.input('testbench.sv') dut.add('tool', 'verilator', 'task', 'compile', 'warningoff', ['WIDTHTRUNC', 'TIMESCALEMOD']) diff --git a/examples/axil/testbench.sv b/examples/axil/testbench.sv deleted file mode 100644 index 446dc4c3..00000000 --- a/examples/axil/testbench.sv +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2024 Zero ASIC Corporation -// This code is licensed under Apache License 2.0 (see LICENSE for details) - -`default_nettype none - -`include "switchboard.vh" - -module testbench ( - `ifdef VERILATOR - input clk - `endif -); - `ifndef VERILATOR - `SB_CREATE_CLOCK(clk) - `endif - - // Declare AXI-Lite wires - - localparam DATA_WIDTH = 32; - localparam ADDR_WIDTH = 13; - - `SB_AXIL_WIRES(axil, DATA_WIDTH, ADDR_WIDTH); - - // Instantiate DUT - - wire rst; - - axil_ram #( - .DATA_WIDTH(DATA_WIDTH), - .ADDR_WIDTH(ADDR_WIDTH) - ) axil_ram_i ( - .clk(clk), - .rst(rst), - `SB_AXIL_CONNECT(s_axil, axil) - ); - - // Instantiate switchboard module - - `SB_AXIL_M(axil, DATA_WIDTH, ADDR_WIDTH, "axil"); - - // Initialize RAM to zeros for easy comparison against a behavioral model - - localparam VALID_ADDR_WIDTH = ADDR_WIDTH - $clog2(DATA_WIDTH/8); - - initial begin - for (int i=0; i<2**VALID_ADDR_WIDTH; i=i+1) begin - axil_ram_i.mem[i] = 0; - end - end - - // Generate reset signal - - reg [7:0] rst_vec = 8'hFF; - - always @(posedge clk) begin - rst_vec <= {rst_vec[6:0], 1'b0}; - end - - assign rst = rst_vec[7]; - - // Set up waveform probing - - `SB_SETUP_PROBES - -endmodule - -`default_nettype wire diff --git a/examples/common/verilog/sb_loopback.v b/examples/common/verilog/sb_loopback.v new file mode 100644 index 00000000..587db06a --- /dev/null +++ b/examples/common/verilog/sb_loopback.v @@ -0,0 +1,34 @@ +// Copyright (c) 2024 Zero ASIC Corporation +// This code is licensed under Apache License 2.0 (see LICENSE for details) + +`default_nettype none + +`include "switchboard.vh" + +module sb_loopback #( + parameter DW=256, + parameter [7:0] INCREMENT=1 +) ( + input clk, + + `SB_INPUT(in, DW), + `SB_OUTPUT(out, DW) +); + + // loopback with increment + + genvar i; + generate + for (i=0; i<(DW/8); i=i+1) begin + assign out_data[(i*8) +: 8] = in_data[(i*8) +: 8] + INCREMENT; + end + endgenerate + + assign out_dest = in_dest; + assign out_last = in_last; + assign out_valid = in_valid; + assign in_ready = out_ready; + +endmodule + +`default_nettype wire diff --git a/examples/network/Makefile b/examples/network/Makefile new file mode 100755 index 00000000..bd44d7f4 --- /dev/null +++ b/examples/network/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 --start-delay 1 --max-rate 1e3 + +.PHONY: icarus +icarus: + ./test.py --tool icarus --start-delay 1 --max-rate 1e3 + +.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/network/README.md b/examples/network/README.md new file mode 100644 index 00000000..a3079d2b --- /dev/null +++ b/examples/network/README.md @@ -0,0 +1,35 @@ +# network example + +This example demonstrates two new switchboard features: the ability to automatically generate Verilog wrappers wiring up DUT ports to switchboard modules, and the ability to dynamically construct networks of simulations that communication through switchboard connections. + +The system in this example consists of an AXI-Lite memory connected to a UMI <-> AXI-Lite bridge from the UMI repository. The UMI input and output of that bridge are each connected to a UMI FIFO. The idea is that a Python script will send UMI transactions that are buffered by the FIFOs, converted to AXI-Lite by the bridge, and executed by the AXI-Lite memory. + +image + +What is unusual here is that each of these four modules is run in a separate Verilator simulation. Three simulator builds are run: one for the FIFO, one for the bridge, and one for the memory. When the network simulation starts, two instances of the FIFO simulator are started, and one instance each of the other simulators are started. + +Looking at the implementation in [test.py](test.py), the first step is to create an `SbNetwork` object. Then, create `SbDut` objects for each unique module in the network (in this case, three). For each of these objects, use the new `autowrap=True` feature and specify the interfaces on the module using the `interfaces` argument. `interfaces` is a dictionary mapping the prefix of an interface to a dictionary of properties. That dictionary should at a minimum contain keys for `type` (`axi`, `axil`, `umi`, `sb`) and direction (`input`/`output` for `umi` and `sb`, `manager`/`subordinate` for `axi` and `axil`). Other properties include `dw`, `cw`, `aw`, `idw`. + +Other arguments related to `autowrap` include: +* `parameters`: dictionary mapping the name of the module parameter to its value +* `clocks`: list of strings indicating inputs on the module that should be connected to the generated simulation clock +* `resets`: list of strings indicating inputs on the module that should be connected to the generated reset signal. Optionally, entries in the list can be a dictionary that contains additional properties such as polarity. +* `tieoffs`: dictionary mapping the name of a module input to the value it should be wired to. In the future, it will be possible to program tieoffs at runtime instead of having them fixed for a simulation build. This is make it possible to build a module once and instantiate it multiple times with different tieoffs. + +Once the `SbDut` objects are created, they are instantiated one or more times in the network the `SbNetwork.instantiate()`. The `instantiate()` method returns an object with properties named after the interfaces. This allows instances to be wired up the the `SbNetwork.connect()` method, e.g. + +```python +net.connect(umi_fifo_i.umi_out, umi2axil_i.udev_req) +net.connect(umi_fifo_o.umi_in, umi2axil_i.udev_resp) +``` + +For interfaces that should be made available to the Python script, mark them external, e.g. + +```python +net.external(umi_fifo_i.umi_in, txrx='umi') +net.external(umi_fifo_o.umi_out, txrx='umi') +``` + +The optional `txrx` argument is UMI-specific and means that the two interaces should be presented under a single `UmiTxRx` interface called `umi` (rather than two interfaces called `umi_in` and `umi_out`). + +After calling `SbNetwork.build()` and `SbNetwork.simulate()`, the externally visible interfaces can be retrieved from the `SbNetwork.intfs` dictionary, just as for `SbDut`. diff --git a/examples/network/test.py b/examples/network/test.py new file mode 100755 index 00000000..da0c68ac --- /dev/null +++ b/examples/network/test.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 + +# Example showing how to wire up various modules using SbNetwork + +# Copyright (c) 2024 Zero ASIC Corporation +# This code is licensed under Apache License 2.0 (see LICENSE for details) + +import numpy as np + +import umi +from switchboard import SbDut, SbNetwork + +from pathlib import Path +THIS_DIR = Path(__file__).resolve().parent + + +def main(): + # create network + + net = SbNetwork(cmdline=True) + + # create the building blocks + + umi_fifo = make_umi_fifo(args=net.args) + umi2axil = make_umi2axil(args=net.args) + axil_ram = make_axil_ram(args=net.args) + + # connect them together + + umi_fifo_i = net.instantiate(umi_fifo) + umi_fifo_o = net.instantiate(umi_fifo) + umi2axil_i = net.instantiate(umi2axil) + axil_ram_i = net.instantiate(axil_ram) + + net.connect(umi_fifo_i.umi_out, umi2axil_i.udev_req) + net.connect(umi_fifo_o.umi_in, umi2axil_i.udev_resp) + + net.connect(umi2axil_i.axi, axil_ram_i.s_axil) + + net.external(umi_fifo_i.umi_in, txrx='umi') + net.external(umi_fifo_o.umi_out, txrx='umi') + + # build simulator + + net.build() + + # launch the simulation + + net.simulate() + + # interact with the simulation + + umi = net.intfs['umi'] + + wraddr = 0x10 + wrdata = 0xdeadbeef + + umi.write(wraddr, np.uint32(wrdata)) + + print(f'Wrote addr=0x{wraddr:x} data=0x{wrdata:x}') + + rdaddr = wraddr + + rddata = umi.read(rdaddr, np.uint32) + + print(f'Read addr=0x{rdaddr:x} data=0x{rddata:x}') + + assert wrdata == rddata + + +def make_umi_fifo(args): + dw = 256 + aw = 64 + cw = 32 + + parameters = dict( + DW=dw, + AW=aw, + CW=cw + ) + + tieoffs = dict( + bypass="1'b0", + chaosmode="1'b0", + fifo_full=None, + fifo_empty=None, + vdd="1'b1", + vss="1'b0" + ) + + interfaces = { + 'umi_in': dict(type='umi', dw=dw, aw=aw, cw=cw, direction='input'), + 'umi_out': dict(type='umi', dw=dw, aw=aw, cw=cw, direction='output') + } + + clocks = [ + 'umi_in_clk', + 'umi_out_clk' + ] + + resets = [ + 'umi_in_nreset', + 'umi_out_nreset' + ] + + dut = SbDut('umi_fifo', autowrap=True, parameters=parameters, interfaces=interfaces, + clocks=clocks, resets=resets, tieoffs=tieoffs, args=args) + + dut.use(umi) + dut.add('option', 'library', 'umi') + dut.add('option', 'library', 'lambdalib_stdlib') + dut.add('option', 'library', 'lambdalib_ramlib') + + return dut + + +def make_axil_ram(args): + dw = 64 + aw = 13 + + parameters = dict( + DATA_WIDTH=dw, + ADDR_WIDTH=aw + ) + + interfaces = { + 's_axil': dict(type='axil', dw=dw, aw=aw, direction='subordinate') + } + + resets = [dict(name='rst', delay=8)] + + dut = SbDut('axil_ram', autowrap=True, parameters=parameters, interfaces=interfaces, + resets=resets, args=args) + + dut.register_package_source( + 'verilog-axi', + 'git+https://github.com/alexforencich/verilog-axi.git', + '38915fb' + ) + + dut.input('rtl/axil_ram.v', package='verilog-axi') + + dut.add('tool', 'verilator', 'task', 'compile', 'warningoff', + ['WIDTHTRUNC', 'TIMESCALEMOD']) + + return dut + + +def make_umi2axil(args): + dw = 64 + aw = 64 + cw = 32 + + parameters = dict( + DW=dw, + AW=aw, + CW=cw + ) + + interfaces = { + 'udev_req': dict(type='umi', dw=dw, aw=aw, cw=cw, direction='input', txrx='umi'), + 'udev_resp': dict(type='umi', dw=dw, aw=aw, cw=cw, direction='output', txrx='umi'), + 'axi': dict(type='axil', dw=dw, aw=aw, direction='manager') + } + + resets = ['nreset'] + + dut = SbDut('umi2axilite', autowrap=True, parameters=parameters, interfaces=interfaces, + resets=resets, args=args) + + dut.use(umi) + dut.add('option', 'library', 'umi') + dut.add('option', 'library', 'lambdalib_stdlib') + dut.add('option', 'library', 'lambdalib_ramlib') + + return dut + + +if __name__ == '__main__': + main() diff --git a/examples/requirements.txt b/examples/requirements.txt index c5141237..2e95f4df 100644 --- a/examples/requirements.txt +++ b/examples/requirements.txt @@ -1,2 +1,2 @@ # Examples dependencies -umi @ git+https://github.com/zeroasiccorp/umi.git@ab0920ec84948204ce944e6c146239abb4a7bfd6 +umi @ git+https://github.com/zeroasiccorp/umi.git@f6d8fea9e6270b89a2c38860a4af512383cbc6ef diff --git a/examples/stream/client.cc b/examples/stream/client.cc index 52394512..d22e4beb 100644 --- a/examples/stream/client.cc +++ b/examples/stream/client.cc @@ -13,8 +13,8 @@ int main() { // initialize connections - tx.init("client2rtl.q"); - rx.init("rtl2client.q"); + tx.init("in.q"); + rx.init("out.q"); // initialize the packet sb_packet txp; @@ -27,46 +27,40 @@ int main() { uint64_t txcount = 0; uint64_t rxcount = 0; - int exit_code = 0; + int success = 1; - while ((txcount < iterations) && (rxcount < iterations)) { - if (tx.send(txp)) { + while ((txcount < iterations) || (rxcount < iterations)) { + if ((txcount < iterations) && tx.send(txp)) { // increment transmit counter txcount++; // update packet memcpy(txp.data, &txcount, sizeof(txcount)); } - if (rx.recv(rxp)) { + if ((rxcount < iterations) && rx.recv(rxp)) { // make sure that the packet is correct uint64_t tmp; memcpy(&tmp, rxp.data, sizeof(tmp)); - if (tmp != (rxcount + 42)) { + + uint64_t expected = rxcount + 0x0101010101010101; + + if (tmp != expected) { printf("*** ERROR: data mismatch, got %" PRId64 " but expected %" PRId64 "\n", tmp, - rxcount); - exit_code = 1; - break; + expected); + success = 0; } - // increment the receive counter rxcount++; } } - // send a packet that will end the test - - for (int i = 0; i < 32; i++) { - txp.data[i] = 0xff; - } - tx.send_blocking(txp); - // declare test as having passed or failed for regression testing purposes - if (exit_code == 0) { + if (success) { printf("PASS!\n"); + return 0; } else { printf("FAIL\n"); + return 1; } - - return exit_code; } diff --git a/examples/stream/test.py b/examples/stream/test.py index dd2110f1..f3dba851 100755 --- a/examples/stream/test.py +++ b/examples/stream/test.py @@ -5,32 +5,32 @@ # Copyright (c) 2024 Zero ASIC Corporation # This code is licensed under Apache License 2.0 (see LICENSE for details) -import time - from pathlib import Path -from switchboard import delete_queues, binary_run, SbDut +from switchboard import binary_run, SbDut THIS_DIR = Path(__file__).resolve().parent +COMMON_DIR = THIS_DIR.parent / 'common' def main(): # build the simulator - dut = SbDut(cmdline=True) - dut.input('testbench.sv') - dut.build() - # clean up old queues if present - delete_queues(['client2rtl.q', 'rtl2client.q']) + interfaces = { + 'in': dict(type='sb', direction='input'), + 'out': dict(type='sb', direction='output') + } + + dut = SbDut('sb_loopback', autowrap=True, cmdline=True, interfaces=interfaces) + dut.input(COMMON_DIR / 'verilog' / 'sb_loopback.v') + dut.build() - # start client and chip - # this order yields a smaller waveform file + # start chip and client + dut.simulate() client = binary_run(THIS_DIR / 'client') - time.sleep(1) - chip = dut.simulate() # wait for client and chip to complete - client.wait() - chip.wait() + retcode = client.wait() + assert retcode == 0 if __name__ == '__main__': diff --git a/examples/stream/testbench.sv b/examples/stream/testbench.sv deleted file mode 100644 index e234252a..00000000 --- a/examples/stream/testbench.sv +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2024 Zero ASIC Corporation -// This code is licensed under Apache License 2.0 (see LICENSE for details) - -`include "switchboard.vh" - -module testbench ( - `ifdef VERILATOR - input clk - `endif -); - `ifndef VERILATOR - `SB_CREATE_CLOCK(clk) - `endif - - localparam integer DW=256; - - // SB RX port - - `SB_WIRES(sb_rx, DW); - `QUEUE_TO_SB_SIM(sb_rx, DW, "client2rtl.q"); - - // SB TX port - - `SB_WIRES(sb_tx, DW); - `SB_TO_QUEUE_SIM(sb_tx, DW, "rtl2client.q"); - - // custom modification of packet - - genvar i; - generate - assign sb_tx_data[63:0] = sb_rx_data[63:0] + 64'd42; - endgenerate - - assign sb_tx_dest = sb_rx_dest; - assign sb_tx_last = sb_rx_last; - assign sb_tx_valid = sb_rx_valid; - assign sb_rx_ready = sb_tx_ready; - - // Waveforms - - `SB_SETUP_PROBES - - // $finish - - always @(posedge clk) begin - if (sb_rx_valid && ((&sb_rx_data) == 1'b1)) begin - $finish; - end - end - -endmodule diff --git a/examples/test_examples.py b/examples/test_examples.py index 5050795c..1c14dcd0 100755 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -19,6 +19,8 @@ ['axil', 'PASS!', 'verilator'], ['minimal', 'PASS!', 'icarus'], ['minimal', 'PASS!', 'verilator'], + ['network', None, 'verilator'], + ['network', None, 'icarus'], ['python', 'PASS!', None], ['router', 'PASS!', None], ['stream', 'PASS!', None], diff --git a/examples/umi_fifo/README.md b/examples/umi_fifo/README.md index f25cc4a4..00490cd8 100644 --- a/examples/umi_fifo/README.md +++ b/examples/umi_fifo/README.md @@ -36,4 +36,3 @@ This example highlights some convenient features of switchboard for lower-level 2. The `UmiTxRx.send()` method sends a single `PyUmiPacket` through a switchboard connection. When `blocking=False`, switchboard will return immediately if the connection can't accept the packet and return `False`, rather than blocking until the packet can be sent. 3. The `UmiTxRx.recv()` method receives a single `PyUmiPacket` from a switchboard connection. When `blocking=False`, switchboard will return `None` immediately if there isn't a packet, rather than blocking until there is. 4. The `==` and `!=` operators can be used to compare `PyUmiPacket` objects. - diff --git a/examples/umi_fifo/test.py b/examples/umi_fifo/test.py index 97ff45f3..37831c02 100755 --- a/examples/umi_fifo/test.py +++ b/examples/umi_fifo/test.py @@ -5,7 +5,7 @@ # Copyright (c) 2024 Zero ASIC Corporation # This code is licensed under Apache License 2.0 (see LICENSE for details) -from switchboard import UmiTxRx, random_umi_packet, SbDut +from switchboard import random_umi_packet, SbDut import umi @@ -13,12 +13,11 @@ def main(): # build the simulator dut = build_testbench() - # create queues - umi = UmiTxRx('to_rtl.q', 'from_rtl.q', fresh=True) - # launch the simulation dut.simulate() + umi = dut.intfs['umi'] + n_sent = 0 n_recv = 0 txq = [] @@ -45,14 +44,48 @@ def main(): def build_testbench(): + dw = 256 + aw = 64 + cw = 32 + + parameters = dict( + DW=dw, + AW=aw, + CW=cw + ) + + tieoffs = dict( + bypass="1'b0", + chaosmode="1'b0", + fifo_full=None, + fifo_empty=None, + vdd="1'b1", + vss="1'b0" + ) + + interfaces = { + 'umi_in': dict(type='umi', dw=dw, aw=aw, cw=cw, direction='input', txrx='umi'), + 'umi_out': dict(type='umi', dw=dw, aw=aw, cw=cw, direction='output', txrx='umi') + } + + clocks = [ + 'umi_in_clk', + 'umi_out_clk' + ] + + resets = [ + 'umi_in_nreset', + 'umi_out_nreset' + ] + extra_args = { '-n': dict(type=int, default=3, help='Number of' ' transactions to send into the FIFO during the test.') } - dut = SbDut(cmdline=True, extra_args=extra_args) - - dut.input('testbench.sv') + dut = SbDut('umi_fifo', autowrap=True, cmdline=True, extra_args=extra_args, + parameters=parameters, interfaces=interfaces, clocks=clocks, resets=resets, + tieoffs=tieoffs) dut.use(umi) dut.add('option', 'library', 'umi') diff --git a/examples/umi_fifo/testbench.sv b/examples/umi_fifo/testbench.sv deleted file mode 100644 index 683ceeb0..00000000 --- a/examples/umi_fifo/testbench.sv +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2024 Zero ASIC Corporation -// This code is licensed under Apache License 2.0 (see LICENSE for details) - -`default_nettype none - -`include "switchboard.vh" - -module testbench ( - `ifdef VERILATOR - input clk - `endif -); - `ifndef VERILATOR - `SB_CREATE_CLOCK(clk) - `endif - - localparam integer DW=256; - localparam integer AW=64; - localparam integer CW=32; - - `SB_UMI_WIRES(udev_req, DW, CW, AW); - `QUEUE_TO_UMI_SIM(udev_req, DW, CW, AW, "to_rtl.q"); - - `SB_UMI_WIRES(udev_resp, DW, CW, AW); - `UMI_TO_QUEUE_SIM(udev_resp, DW, CW, AW, "from_rtl.q"); - - reg nreset = 1'b0; - - umi_fifo #( - .DW(DW), - .CW(CW), - .AW(AW) - ) umi_fifo_i ( - .bypass(1'b1), - .chaosmode(1'b0), - .fifo_full(), - .fifo_empty(), - // input - .umi_in_clk(clk), - .umi_in_nreset(nreset), - `SB_UMI_CONNECT(umi_in, udev_req), - // output - .umi_out_clk(clk), - .umi_out_nreset(nreset), - `SB_UMI_CONNECT(umi_out, udev_resp), - // supplies - .vdd(1'b1), - .vss(1'b0) - ); - - always @(posedge clk) begin - nreset <= 1'b1; - end - - // Waveforms - - `SB_SETUP_PROBES - -endmodule - -`default_nettype wire diff --git a/examples/umi_fifo_flex/.gitignore b/examples/umi_fifo_flex/.gitignore deleted file mode 100644 index 378eac25..00000000 --- a/examples/umi_fifo_flex/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build diff --git a/examples/umi_fifo_flex/test.py b/examples/umi_fifo_flex/test.py index 012ef75a..c81ccc9e 100755 --- a/examples/umi_fifo_flex/test.py +++ b/examples/umi_fifo_flex/test.py @@ -5,7 +5,7 @@ # Copyright (c) 2024 Zero ASIC Corporation # This code is licensed under Apache License 2.0 (see LICENSE for details) -from switchboard import SbDut, UmiTxRx, umi_loopback +from switchboard import SbDut, umi_loopback import umi @@ -13,25 +13,59 @@ def main(): # build simulator dut = build_testbench() - # create queues - umi = UmiTxRx("to_rtl.q", "from_rtl.q", fresh=True) - # launch the simulation dut.simulate() # randomly write data + umi = dut.intfs['umi'] umi_loopback(umi, dut.args.n) def build_testbench(): + idw = 256 + odw = 64 + aw = 64 + cw = 32 + + parameters = dict( + IDW=idw, + ODW=odw, + AW=aw, + CW=cw + ) + + tieoffs = dict( + bypass="1'b0", + chaosmode="1'b0", + fifo_full=None, + fifo_empty=None, + vdd="1'b1", + vss="1'b0" + ) + + interfaces = { + 'umi_in': dict(type='umi', dw=idw, aw=aw, cw=cw, direction='input', txrx='umi'), + 'umi_out': dict(type='umi', dw=odw, aw=aw, cw=cw, direction='output', txrx='umi') + } + + clocks = [ + 'umi_in_clk', + 'umi_out_clk' + ] + + resets = [ + 'umi_in_nreset', + 'umi_out_nreset' + ] + extra_args = { '-n': dict(type=int, default=3, help='Number of' ' transactions to send into the FIFO during the test.') } - dut = SbDut(cmdline=True, extra_args=extra_args) - - dut.input('testbench.sv') + dut = SbDut('umi_fifo_flex', autowrap=True, cmdline=True, extra_args=extra_args, + parameters=parameters, interfaces=interfaces, clocks=clocks, resets=resets, + tieoffs=tieoffs) dut.use(umi) dut.add('option', 'library', 'umi') diff --git a/examples/umi_fifo_flex/testbench.sv b/examples/umi_fifo_flex/testbench.sv deleted file mode 100644 index ebfe9bd8..00000000 --- a/examples/umi_fifo_flex/testbench.sv +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2024 Zero ASIC Corporation -// This code is licensed under Apache License 2.0 (see LICENSE for details) - -`default_nettype none - -`include "switchboard.vh" - -module testbench ( - `ifdef VERILATOR - input clk - `endif -); - `ifndef VERILATOR - `SB_CREATE_CLOCK(clk) - `endif - - parameter integer IDW=256; - parameter integer ODW=64; - parameter integer AW=64; - parameter integer CW=32; - - `SB_UMI_WIRES(udev_req, IDW, CW, AW); - `QUEUE_TO_UMI_SIM(udev_req, IDW, CW, AW, "to_rtl.q"); - - `SB_UMI_WIRES(udev_resp, ODW, CW, AW); - `UMI_TO_QUEUE_SIM(udev_resp, ODW, CW, AW, "from_rtl.q"); - - reg nreset = 1'b0; - - umi_fifo_flex #( - .IDW(IDW), - .ODW(ODW) - ) umi_fifo_flex_i ( - .bypass(1'b0), - .chaosmode(1'b0), - .fifo_full(), - .fifo_empty(), - // Input UMI - .umi_in_clk(clk), - .umi_in_nreset(nreset), - `SB_UMI_CONNECT(umi_in, udev_req), - // Output UMI - .umi_out_clk(clk), - .umi_out_nreset(nreset), - `SB_UMI_CONNECT(umi_out, udev_resp), - // Supplies - .vdd(1'b1), - .vss(1'b0) - ); - - always @(posedge clk) begin - nreset <= 1'b1; - end - - // Waveforms - - `SB_SETUP_PROBES - -endmodule - -`default_nettype wire diff --git a/examples/umi_splitter/Makefile b/examples/umi_splitter/Makefile index 3be62855..76c9dcf6 100755 --- a/examples/umi_splitter/Makefile +++ b/examples/umi_splitter/Makefile @@ -15,3 +15,4 @@ clean: rm -f *.vcd *.fst *.fst.hier rm -rf obj_dir build rm -f *.o *.vpi + rm -f testbench.sv diff --git a/examples/umi_splitter/test.py b/examples/umi_splitter/test.py index 41e5edb2..787e0dc5 100755 --- a/examples/umi_splitter/test.py +++ b/examples/umi_splitter/test.py @@ -5,7 +5,7 @@ # Copyright (c) 2024 Zero ASIC Corporation # This code is licensed under Apache License 2.0 (see LICENSE for details) -from switchboard import UmiTxRx, random_umi_packet, SbDut +from switchboard import SbDut, random_umi_packet import umi @@ -13,16 +13,15 @@ def main(): # build the simulator dut = build_testbench() - # create queues - umi_in = UmiTxRx("in.q", "", fresh=True) - umi_out = [ - UmiTxRx("", "out0.q", fresh=True), - UmiTxRx("", "out1.q", fresh=True) - ] - # launch the simulation dut.simulate() + umi_in = dut.intfs['umi_in'] + umi_out = [ + dut.intfs['umi_resp_out'], + dut.intfs['umi_req_out'] + ] + # main loop tx_rep = [] tx_req = [] @@ -61,14 +60,23 @@ def main(): def build_testbench(): + dw = 256 + aw = 64 + cw = 32 + + interfaces = { + 'umi_in': dict(type='umi', dw=dw, aw=aw, cw=cw, direction='input'), + 'umi_resp_out': dict(type='umi', dw=dw, aw=aw, cw=cw, direction='output'), + 'umi_req_out': dict(type='umi', dw=dw, aw=aw, cw=cw, direction='output') + } + extra_args = { '-n': dict(type=int, default=3, help='Number of' - ' transactions to send into the FIFO during the test.') + ' transactions to send into the splitter during the test.') } - dut = SbDut(cmdline=True, extra_args=extra_args) - - dut.input('testbench.sv') + dut = SbDut('umi_splitter', autowrap=True, cmdline=True, extra_args=extra_args, + interfaces=interfaces, clocks=[]) dut.use(umi) dut.add('option', 'library', 'umi') diff --git a/examples/umi_splitter/testbench.sv b/examples/umi_splitter/testbench.sv deleted file mode 100644 index db2f6e74..00000000 --- a/examples/umi_splitter/testbench.sv +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2024 Zero ASIC Corporation -// This code is licensed under Apache License 2.0 (see LICENSE for details) - -`default_nettype none - -`include "switchboard.vh" - -module testbench ( - `ifdef VERILATOR - input clk - `endif -); - `ifndef VERILATOR - `SB_CREATE_CLOCK(clk) - `endif - - localparam integer DW=256; - localparam integer AW=64; - localparam integer CW=32; - - // UMI input - - `SB_UMI_WIRES(umi_in, DW, CW, AW); - `QUEUE_TO_UMI_SIM(umi_in, DW, CW, AW, "in.q"); - - // UMI output (response) - - `SB_UMI_WIRES(umi_resp_out, DW, CW, AW); - `UMI_TO_QUEUE_SIM(umi_resp_out, DW, CW, AW, "out0.q"); - - // UMI output (request) - - `SB_UMI_WIRES(umi_req_out, DW, CW, AW); - `UMI_TO_QUEUE_SIM(umi_req_out, DW, CW, AW, "out1.q"); - - // UMI splitter - - umi_splitter umi_splitter_i ( - .* - ); - - // Waveforms - - `SB_SETUP_PROBES - -endmodule - -`default_nettype wire diff --git a/examples/umiram/.gitignore b/examples/umiram/.gitignore index 5d3cad89..b051c6c5 100644 --- a/examples/umiram/.gitignore +++ b/examples/umiram/.gitignore @@ -1,2 +1 @@ client -build diff --git a/python/switchboard_pybind.cc b/python/switchboard_pybind.cc index 27cb5a52..3bd6e68b 100644 --- a/python/switchboard_pybind.cc +++ b/python/switchboard_pybind.cc @@ -307,22 +307,11 @@ struct PyUmiPacket { // check_signals() should be called within any loops where the C++ // code is waiting for something to happen. this ensures that -// the binding doesn't hang after the user presses Ctrl-C. the -// value "100000" is a balance between responsiveness and speed: -// setting it lower allows the binding to respond faster Ctrl-C, -// but incurs more overhead due to more frequent invocation of -// PyErr_CheckSignals(). +// the binding doesn't hang after the user presses Ctrl-C. void check_signals() { - static int count = 0; - - if (count == 100000) { - count = 0; - if (PyErr_CheckSignals() != 0) { - throw pybind11::error_already_set(); - } - } else { - count++; + if (PyErr_CheckSignals() != 0) { + throw pybind11::error_already_set(); } } @@ -373,13 +362,13 @@ struct PySbRxPcie { class PySbTx { public: - PySbTx(std::string uri = "", bool fresh = false) { - init(uri, fresh); + PySbTx(std::string uri = "", bool fresh = false, double max_rate = -1) { + init(uri, fresh, max_rate); } - void init(std::string uri, bool fresh = false) { + void init(std::string uri, bool fresh = false, double max_rate = -1) { if (uri != "") { - m_tx.init(uri, 0, fresh); + m_tx.init(uri, 0, fresh, max_rate); } } @@ -419,6 +408,7 @@ class PySbTx { while (!m_tx.send(p)) { check_signals(); } + return true; } } @@ -431,13 +421,13 @@ class PySbTx { class PySbRx { public: - PySbRx(std::string uri = "", bool fresh = false) { - init(uri, fresh); + PySbRx(std::string uri = "", bool fresh = false, double max_rate = -1) { + init(uri, fresh, max_rate); } - void init(std::string uri, bool fresh = false) { + void init(std::string uri, bool fresh = false, double max_rate = -1) { if (uri != "") { - m_rx.init(uri, 0, fresh); + m_rx.init(uri, 0, fresh, max_rate); } } @@ -505,16 +495,17 @@ static inline void progressbar_done(void) { class PyUmi { public: - PyUmi(std::string tx_uri = "", std::string rx_uri = "", bool fresh = false) { - init(tx_uri, rx_uri, fresh); + PyUmi(std::string tx_uri = "", std::string rx_uri = "", bool fresh = false, + double max_rate = -1) { + init(tx_uri, rx_uri, fresh, max_rate); } - void init(std::string tx_uri, std::string rx_uri, bool fresh = false) { + void init(std::string tx_uri, std::string rx_uri, bool fresh = false, double max_rate = -1) { if (tx_uri != "") { - m_tx.init(tx_uri, 0, fresh); + m_tx.init(tx_uri, 0, fresh, max_rate); } if (rx_uri != "") { - m_rx.init(rx_uri, 0, fresh); + m_rx.init(rx_uri, 0, fresh, max_rate); } } @@ -979,16 +970,18 @@ PYBIND11_MODULE(_switchboard, m) { .def(py::self != py::self); py::class_(m, "PySbTx") - .def(py::init(), py::arg("uri") = "", py::arg("fresh") = false) + .def(py::init(), py::arg("uri") = "", py::arg("fresh") = false, + py::arg("max_rate") = -1) .def("init", &PySbTx::init, PySbTx_init_docstring, py::arg("uri") = "", - py::arg("fresh") = false) + py::arg("fresh") = false, py::arg("max_rate") = -1) .def("send", &PySbTx::send, PySbTx_send_docstring, py::arg("py_packet"), py::arg("blocking") = true); py::class_(m, "PySbRx") - .def(py::init(), py::arg("uri") = "", py::arg("fresh") = false) + .def(py::init(), py::arg("uri") = "", py::arg("fresh") = false, + py::arg("max_rate") = -1) .def("init", &PySbRx::init, PySbRx_init_docstring, py::arg("uri") = "", - py::arg("fresh") = false) + py::arg("fresh") = false, py::arg("max_rate") = -1) .def("recv", &PySbRx::recv, PySbRx_recv_docstring, py::arg("blocking") = true); py::class_(m, "PySbTxPcie") @@ -1004,10 +997,10 @@ PYBIND11_MODULE(_switchboard, m) { py::arg("bar_num") = 0, py::arg("bdf") = ""); py::class_(m, "PyUmi") - .def(py::init(), py::arg("tx_uri") = "", - py::arg("rx_uri") = "", py::arg("fresh") = false) + .def(py::init(), py::arg("tx_uri") = "", + py::arg("rx_uri") = "", py::arg("fresh") = false, py::arg("max_rate") = -1) .def("init", &PyUmi::init, PyUmi_init_docstring, py::arg("tx_uri") = "", - py::arg("rx_uri") = "", py::arg("fresh") = false) + py::arg("rx_uri") = "", py::arg("fresh") = false, py::arg("max_rate") = -1) .def("send", &PyUmi::send, PyUmi_send_docstring, py::arg("py_packet"), py::arg("blocking") = true) .def("recv", &PyUmi::recv, PyUmi_recv_docstring, py::arg("blocking") = true) diff --git a/setup.py b/setup.py index f90e3fd5..ba124627 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import setup, find_packages from pybind11.setup_helpers import Pybind11Extension, build_ext -__version__ = "0.1.0" +__version__ = "0.2.0" ################################################################################# # parse_reqs, long_desc from https://github.com/siliconcompiler/siliconcompiler # diff --git a/switchboard/__init__.py b/switchboard/__init__.py index b90b1d99..81ced4d8 100644 --- a/switchboard/__init__.py +++ b/switchboard/__init__.py @@ -20,3 +20,4 @@ from .sbtcp import start_tcp_bridge from .axil import AxiLiteTxRx from .axi import AxiTxRx +from .network import SbNetwork diff --git a/switchboard/autowrap.py b/switchboard/autowrap.py new file mode 100644 index 00000000..64d3ba9b --- /dev/null +++ b/switchboard/autowrap.py @@ -0,0 +1,640 @@ +# Tool for automatically wrapping a DUT with switchboard interfaces + +# Copyright (c) 2024 Zero ASIC Corporation +# This code is licensed under Apache License 2.0 (see LICENSE for details) + +from pathlib import Path +from copy import deepcopy + +from .umi import UmiTxRx +from .axi import AxiTxRx +from .axil import AxiLiteTxRx + +from _switchboard import PySbTx, PySbRx + + +def normalize_interface(name, value): + # copy before modifying + value = deepcopy(value) + + assert isinstance(value, dict) + + if 'type' not in value: + value['type'] = 'sb' + + assert 'type' in value + value['type'] = normalize_intf_type(value['type']) + type = value['type'] + + assert 'direction' in value + value['direction'] = normalize_direction(type=type, direction=value['direction']) + + if type == 'sb': + if 'dw' not in value: + value['dw'] = 256 + if 'uri' not in value: + value['uri'] = f'{name}.q' + elif type == 'umi': + if 'dw' not in value: + value['dw'] = 256 + if 'aw' not in value: + value['aw'] = 64 + if 'cw' not in value: + value['cw'] = 32 + if 'txrx' not in value: + value['txrx'] = None + if 'uri' not in value: + value['uri'] = f'{name}.q' + elif type in ['axi', 'axil']: + if 'dw' not in value: + value['dw'] = 32 + if 'aw' not in value: + value['aw'] = 16 + if 'uri' not in value: + value['uri'] = name + + if type == 'axi': + # settings that only apply to AXI, not AXI-Lite + + if 'idw' not in value: + value['idw'] = 8 + else: + raise ValueError(f'Unsupported interface type: "{type}"') + + return name, value + + +def normalize_interfaces(interfaces): + if interfaces is None: + interfaces = {} + + retval = {} + + for name, value in interfaces.items(): + name, value = normalize_interface(name, value) + retval[name] = value + + return retval + + +def normalize_clock(clock): + # copy before modifying + clock = deepcopy(clock) + + if isinstance(clock, str): + clock = dict(name=clock) + + assert isinstance(clock, dict) + + return clock + + +def normalize_clocks(clocks): + if clocks is None: + clocks = ['clk'] + + if isinstance(clocks, str): + clocks = [clocks] + + retval = [] + + for clock in clocks: + clock = normalize_clock(clock) + retval.append(clock) + + return retval + + +def normalize_reset(reset): + # copy before modifying + reset = deepcopy(reset) + + if isinstance(reset, str): + reset = {'name': reset} + + assert 'name' in reset + + name = reset['name'] + + if 'polarity' not in reset: + if (('nreset' in name) or ('resetn' in name) + or ('nrst' in name) or ('rstn' in name)): + reset['polarity'] = 'negative' + else: + reset['polarity'] = 'positive' + else: + reset['polarity'] = normalize_polarity(reset['polarity']) + + if 'delay' not in reset: + reset['delay'] = 0 + + return reset + + +def normalize_resets(resets): + if resets is None: + resets = [] + + if isinstance(resets, str): + resets = [resets] + + retval = [] + + for reset in resets: + reset = normalize_reset(reset) + retval.append(reset) + + return retval + + +def normalize_tieoff(key, value): + # placeholder for doing more interesting things in the future + return key, value + + +def normalize_tieoffs(tieoffs): + if tieoffs is None: + tieoffs = {} + + retval = {} + + for key, value in tieoffs.items(): + key, value = normalize_tieoff(key, value) + retval[key] = value + + return retval + + +def normalize_parameter(key, value): + # placeholder for doing more interesting things in the future + return key, value + + +def normalize_parameters(parameters): + if parameters is None: + parameters = {} + + retval = {} + + for key, value in parameters.items(): + key, value = normalize_parameter(key, value) + retval[key] = value + + return retval + + +def autowrap( + design, + toplevel='testbench', + parameters=None, + interfaces=None, + clocks=None, + resets=None, + tieoffs=None, + filename=None, + nl='\n', + tab=' ' +): + # normalize inputs + + parameters = normalize_parameters(parameters) + interfaces = normalize_interfaces(interfaces) + clocks = normalize_clocks(clocks) + resets = normalize_resets(resets) + tieoffs = normalize_tieoffs(tieoffs) + + # build up output lines + + lines = [] + + lines += [ + '`default_nettype none', + '', + '`include "switchboard.vh"', + '', + f'module {toplevel} (', + tab + '`ifdef VERILATOR', + (2 * tab) + 'input clk', + tab + '`endif', + ');', + tab + '`ifndef VERILATOR', + (2 * tab) + '`SB_CREATE_CLOCK(clk)', + tab + '`endif', + '' + ] + + lines += [''] + + for name, value in interfaces.items(): + type = value['type'] + direction = value['direction'] + + if type == 'sb': + dw = value['dw'] + + lines += [tab + f'`SB_WIRES({name}, {dw});'] + + if direction_is_input(direction): + lines += [tab + f'`QUEUE_TO_SB_SIM({name}, {dw}, "");'] + elif direction_is_output(direction): + lines += [tab + f'`SB_TO_QUEUE_SIM({name}, {dw}, "");'] + else: + raise Exception(f'Unsupported SB direction: {direction}') + elif type == 'umi': + dw = value['dw'] + cw = value['cw'] + aw = value['aw'] + + lines += [tab + f'`SB_UMI_WIRES({name}, {dw}, {cw}, {aw});'] + + if direction_is_input(direction): + lines += [tab + f'`QUEUE_TO_UMI_SIM({name}, {dw}, {cw}, {aw}, "");'] + elif direction_is_output(direction): + lines += [tab + f'`UMI_TO_QUEUE_SIM({name}, {dw}, {cw}, {aw}, "");'] + else: + raise Exception(f'Unsupported UMI direction: {direction}') + elif type == 'axi': + dw = value['dw'] + aw = value['aw'] + idw = value['idw'] + + lines += [tab + f'`SB_AXI_WIRES({name}, {dw}, {aw}, {idw});'] + + if direction_is_subordinate(direction): + lines += [tab + f'`SB_AXI_M({name}, {dw}, {aw}, {idw}, "");'] + elif direction_is_manager(direction): + lines += [tab + f'`SB_AXI_S({name}, {dw}, {aw}, "");'] + else: + raise Exception(f'Unsupported AXI direction: {direction}') + elif type == 'axil': + dw = value['dw'] + aw = value['aw'] + + lines += [tab + f'`SB_AXIL_WIRES({name}, {dw}, {aw});'] + + if direction_is_subordinate(direction): + lines += [tab + f'`SB_AXIL_M({name}, {dw}, {aw}, "");'] + elif direction_is_manager(direction): + lines += [tab + f'`SB_AXIL_S({name}, {dw}, {aw}, "");'] + else: + raise Exception(f'Unsupported AXI-Lite direction: {direction}') + else: + raise Exception(f'Unsupported interface type: "{type}"') + + lines += [''] + + if len(resets) > 0: + max_rst_dly = max(reset['delay'] for reset in resets) + + 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', + '' + ] + + if len(parameters) > 0: + lines += [tab + f'{design} #('] + for n, (key, value) in enumerate(parameters.items()): + line = (2 * tab) + f'.{key}({value})' + + if n != len(parameters) - 1: + line += ',' + + lines += [line] + lines += [tab + f') {design}_i ('] + else: + lines += [tab + f'{design} {design}_i ('] + + connections = [] + + # interfaces + + for name, value in interfaces.items(): + type = value['type'] + + if type_is_sb(type): + connections += [f'`SB_CONNECT({name}, {name})'] + elif type_is_umi(type): + connections += [f'`SB_UMI_CONNECT({name}, {name})'] + elif type_is_axi(type): + connections += [f'`SB_AXI_CONNECT({name}, {name})'] + elif type_is_axil(type): + connections += [f'`SB_AXIL_CONNECT({name}, {name})'] + + # clocks + + for clock in clocks: + connections += [f'.{clock["name"]}(clk)'] + + # resets + + for reset in resets: + name = reset['name'] + polarity = reset['polarity'] + delay = reset['delay'] + + if polarity_is_positive(polarity): + value = f'rstvec[{delay}]' + elif polarity_is_negative(polarity): + value = f'~rstvec[{delay}]' + else: + raise ValueError(f'Unsupported reset polarity: "{polarity}"') + + connections += [f'.{name}({value})'] + + # tieoffs + + for key, value in tieoffs.items(): + if value is None: + value = '' + else: + value = str(value) + connections += [f'.{key}({value})'] + + for n, connection in enumerate(connections): + if n != len(connections) - 1: + connection += ',' + lines += [(2 * tab) + connection] + + lines += [tab + ');'] + lines += [''] + + # initialize queue connections + + lines += [ + tab + 'string uri_sb_value;', + '', + tab + 'initial begin', + (2 * tab) + '/* verilator lint_off IGNOREDRETURN */' + ] + + for name, value in interfaces.items(): + lines += [ + (2 * tab) + f'if($value$plusargs("{name}=%s", uri_sb_value)) begin', + (3 * tab) + f'{name}_sb_inst.init(uri_sb_value);', + (2 * tab) + 'end' + ] + + lines += [ + (2 * tab) + '/* verilator lint_on IGNOREDRETURN */', + tab + 'end' + ] + + lines += [''] + + lines += [tab + '`SB_SETUP_PROBES'] + lines += [''] + + lines += ['endmodule'] + + lines += [''] + + lines += ['`default_nettype wire'] + + if filename is None: + filename = 'testbench.sv' + + filename = Path(filename).resolve() + + with open(filename, 'w') as f: + for line in lines: + f.write(line + nl) + + return filename + + +def direction_is_input(direction): + return direction.lower() in ['i', 'in', 'input'] + + +def direction_is_output(direction): + return direction.lower() in ['o', 'out', 'output'] + + +def direction_is_manager(direction): + return direction.lower() in ['m', 'manager', 'master', 'indicator'] + + +def direction_is_subordinate(direction): + return direction.lower() in ['s', 'subordinate', 'slave', 'target'] + + +def normalize_direction(type, direction): + if type_is_sb(type) or type_is_umi(type): + if direction_is_input(direction): + return 'input' + elif direction_is_output(direction): + return 'output' + else: + raise Exception(f'Unsupported direction for interface type "{type}": "{direction}"') + elif type_is_axi(type) or type_is_axil(type): + if direction_is_manager(direction): + return 'manager' + elif direction_is_subordinate(direction): + return 'subordinate' + else: + raise Exception(f'Unsupported direction for interface type "{type}": "{direction}"') + else: + raise Exception(f'Unsupported interface type: "{type}"') + + +def directions_are_compatible(type, a, b): + a = normalize_direction(type, a) + b = normalize_direction(type, b) + + if type_is_sb(type) or type_is_umi(type): + return (((a == 'input') and (b == 'output')) + or ((a == 'output') and (b == 'input'))) + elif type_is_axi(type) or type_is_axil(type): + return (((a == 'manager') and (b == 'subordinate')) + or ((a == 'subordinate') and (b == 'manager'))) + else: + raise Exception(f'Unsupported interface type: "{type}"') + + +def polarity_is_positive(polarity): + return polarity.lower() in ['+', 'p', 'plus', 'positive'] + + +def polarity_is_negative(polarity): + return polarity.lower() in ['-', 'n', 'minus', 'negative'] + + +def normalize_polarity(polarity): + if polarity_is_positive(polarity): + return 'positive' + elif polarity_is_negative(polarity): + return 'negative' + else: + raise ValueError(f'Unsupported reset polarity: "{polarity}"') + + +def type_is_sb(type): + return type.lower() in ['sb', 'switchboard'] + + +def type_is_umi(type): + return type.lower() in ['umi'] + + +def type_is_axi(type): + return type.lower() in ['axi'] + + +def type_is_axil(type): + return type.lower() in ['axil'] + + +def normalize_intf_type(type): + if type_is_sb(type): + return 'sb' + elif type_is_umi(type): + return 'umi' + elif type_is_axi(type): + return 'axi' + elif type_is_axil(type): + return 'axil' + else: + raise ValueError(f'Unsupported interface type: "{type}"') + + +def create_intf_objs(intf_defs, fresh=True, max_rate=-1): + intf_objs = {} + + umi_txrx = {} + + for name, value in intf_defs.items(): + type = value['type'] + + if type.lower() in ['umi']: + txrx = value['txrx'] + + if txrx is not None: + if txrx not in umi_txrx: + umi_txrx[txrx] = dict(tx_uri=None, rx_uri=None) + + if 'srcaddr' in value: + umi_txrx[txrx]['srcaddr'] = value['srcaddr'] + + if 'posted' in value: + umi_txrx[txrx]['posted'] = value['posted'] + + if 'max_bytes' in value: + umi_txrx[txrx]['max_bytes'] = value['max_bytes'] + + if 'max_rate' in value: + umi_txrx[txrx]['max_rate'] = value['max_rate'] + else: + # use default if not set for this particular interface + umi_txrx[txrx]['max_rate'] = max_rate + + direction = value['direction'] + + if direction.lower() in ['i', 'in', 'input']: + umi_txrx[txrx]['tx_uri'] = value['uri'] + elif direction.lower() in ['o', 'out', 'output']: + umi_txrx[txrx]['rx_uri'] = value['uri'] + else: + raise Exception(f'Unsupported UMI direction: {direction}') + else: + intf_objs[name] = create_intf_obj(value, fresh=fresh, max_rate=max_rate) + else: + intf_objs[name] = create_intf_obj(value, fresh=fresh, max_rate=max_rate) + + for key, value in umi_txrx.items(): + intf_objs[key] = UmiTxRx(**value, fresh=fresh) + + return intf_objs + + +def create_intf_obj(value, fresh=True, max_rate=-1): + type = value['type'] + direction = value['direction'] + + if type_is_sb(type): + kwargs = {} + + if 'max_rate' in value: + kwargs['max_rate'] = value['max_rate'] + else: + # use default if not set for this particular interface + kwargs['max_rate'] = max_rate + + if direction_is_input(direction): + obj = PySbTx(value['uri'], fresh=fresh, **kwargs) + elif direction_is_output(direction): + obj = PySbRx(value['uri'], fresh=fresh, **kwargs) + else: + raise Exception(f'Unsupported SB direction: "{direction}"') + elif type_is_umi(type): + kwargs = {} + + if 'max_rate' in value: + kwargs['max_rate'] = value['max_rate'] + else: + # use default if not set for this particular interface + kwargs['max_rate'] = max_rate + + if direction_is_input(direction): + obj = UmiTxRx(tx_uri=value['uri'], fresh=fresh, **kwargs) + elif direction_is_output(direction): + obj = UmiTxRx(rx_uri=value['uri'], fresh=fresh, **kwargs) + else: + raise Exception(f'Unsupported UMI direction: "{direction}"') + elif type_is_axi(type): + kwargs = {} + + if 'prot' in value: + kwargs['prot'] = value['prot'] + + if 'id' in value: + kwargs['id'] = value['id'] + + if 'size' in value: + kwargs['size'] = value['size'] + + if 'max_beats' in value: + kwargs['max_beats'] = value['max_beats'] + + if 'max_rate' in value: + kwargs['max_rate'] = value['max_rate'] + else: + # use default if not set for this particular interface + kwargs['max_rate'] = max_rate + + if direction_is_subordinate(direction): + obj = AxiTxRx(uri=value['uri'], data_width=value['dw'], + addr_width=value['aw'], id_width=value['idw'], **kwargs) + else: + raise Exception(f'Unsupported AXI direction: "{direction}"') + elif type_is_axil(type): + kwargs = {} + + if 'prot' in value: + kwargs['prot'] = value['prot'] + + if 'max_rate' in value: + kwargs['max_rate'] = value['max_rate'] + else: + # use default if not set for this particular interface + kwargs['max_rate'] = max_rate + + if direction_is_subordinate(direction): + obj = AxiLiteTxRx(uri=value['uri'], data_width=value['dw'], + addr_width=value['aw'], **kwargs) + else: + raise Exception(f'Unsupported AXI-Lite direction: "{direction}"') + else: + raise Exception(f'Unsupported interface type: "{type}"') + + return obj diff --git a/switchboard/axi.py b/switchboard/axi.py index 6ec6bb93..672dd360 100644 --- a/switchboard/axi.py +++ b/switchboard/axi.py @@ -24,7 +24,8 @@ def __init__( size: int = None, max_beats: int = 256, resp_expected: str = 'OKAY', - queue_suffix: str = '.q' + queue_suffix: str = '.q', + max_rate: float = -1 ): """ Parameters @@ -97,11 +98,11 @@ def __init__( self.default_resp_expected = resp_expected # create the queues - self.aw = PySbTx(f'{uri}-aw{queue_suffix}', fresh=fresh) - self.w = PySbTx(f'{uri}-w{queue_suffix}', fresh=fresh) - self.b = PySbRx(f'{uri}-b{queue_suffix}', fresh=fresh) - self.ar = PySbTx(f'{uri}-ar{queue_suffix}', fresh=fresh) - self.r = PySbRx(f'{uri}-r{queue_suffix}', fresh=fresh) + self.aw = PySbTx(f'{uri}-aw{queue_suffix}', fresh=fresh, max_rate=max_rate) + self.w = PySbTx(f'{uri}-w{queue_suffix}', fresh=fresh, max_rate=max_rate) + self.b = PySbRx(f'{uri}-b{queue_suffix}', fresh=fresh, max_rate=max_rate) + self.ar = PySbTx(f'{uri}-ar{queue_suffix}', fresh=fresh, max_rate=max_rate) + self.r = PySbRx(f'{uri}-r{queue_suffix}', fresh=fresh, max_rate=max_rate) @property def strb_width(self): @@ -244,7 +245,7 @@ def write( # transmit the write address self.aw.send(self.pack_addr(addr & addr_mask, prot=prot, size=size, - len=beats - 1, id=id)) + len=beats - 1, id=id), True) for beat in range(beats): # find the offset into the data bus for this beat. bytes below @@ -268,14 +269,14 @@ def write( last = 1 else: last = 0 - self.w.send(self.pack_w(data, strb=strb, last=last)) + self.w.send(self.pack_w(data, strb=strb, last=last), True) # increment pointers bytes_sent += bytes_this_beat addr += bytes_this_beat # wait for response - resp, id = self.unpack_b(self.b.recv()) + resp, id = self.unpack_b(self.b.recv(True)) # decode the response resp = decode_resp(resp) @@ -415,7 +416,7 @@ def read( # transmit read address self.ar.send(self.pack_addr(addr & addr_mask, prot=prot, size=size, - len=beats - 1, id=id)) + len=beats - 1, id=id), True) for _ in range(beats): # find the offset into the data bus for this beat. bytes below @@ -426,7 +427,7 @@ def read( bytes_this_beat = min(bytes_to_read - bytes_read, (1 << size) - offset) # wait for response - data, resp, id, last = self.unpack_r(self.r.recv()) + data, resp, id, last = self.unpack_r(self.r.recv(True)) retval[bytes_read:bytes_read + bytes_this_beat] = \ data = data[offset:offset + bytes_this_beat] @@ -553,3 +554,18 @@ def decode_resp(resp: Integral): assert 0 <= resp <= 3, 'response code out of range' return ['OKAY', 'EXOKAY', 'SLVERR', 'DECERR'][resp] + + +def axi_uris(prefix, suffix='.q'): + # returns a list of the URIs associated with a given AXI or AXI-Lite + # prefix. For example, axi_uris('axi') returns ['axi-aw.q', 'axi-w.q', + # 'axi-b.q', 'axi-ar.q', 'axi-r.q']. Changing the optional suffix + # argument changes the file extension assumed in generating this list. + + return [ + f'{prefix}-aw{suffix}', + f'{prefix}-w{suffix}', + f'{prefix}-b{suffix}', + f'{prefix}-ar{suffix}', + f'{prefix}-r{suffix}' + ] diff --git a/switchboard/axil.py b/switchboard/axil.py index b040123a..ea7c1b7a 100644 --- a/switchboard/axil.py +++ b/switchboard/axil.py @@ -20,7 +20,8 @@ def __init__( addr_width: int = 16, prot: int = 0, resp_expected: str = 'OKAY', - queue_suffix: str = '.q' + queue_suffix: str = '.q', + max_rate: float = -1 ): """ Parameters @@ -73,11 +74,11 @@ def __init__( self.default_resp_expected = resp_expected # create the queues - self.aw = PySbTx(f'{uri}-aw{queue_suffix}', fresh=fresh) - self.w = PySbTx(f'{uri}-w{queue_suffix}', fresh=fresh) - self.b = PySbRx(f'{uri}-b{queue_suffix}', fresh=fresh) - self.ar = PySbTx(f'{uri}-ar{queue_suffix}', fresh=fresh) - self.r = PySbRx(f'{uri}-r{queue_suffix}', fresh=fresh) + self.aw = PySbTx(f'{uri}-aw{queue_suffix}', fresh=fresh, max_rate=max_rate) + self.w = PySbTx(f'{uri}-w{queue_suffix}', fresh=fresh, max_rate=max_rate) + self.b = PySbRx(f'{uri}-b{queue_suffix}', fresh=fresh, max_rate=max_rate) + self.ar = PySbTx(f'{uri}-ar{queue_suffix}', fresh=fresh, max_rate=max_rate) + self.r = PySbRx(f'{uri}-r{queue_suffix}', fresh=fresh, max_rate=max_rate) @property def strb_width(self): @@ -194,17 +195,17 @@ def write( pack = pack.to_bytes((self.addr_width + 3 + 7) // 8, 'little') pack = np.frombuffer(pack, dtype=np.uint8) pack = PySbPacket(data=pack, flags=1, destination=0) - self.aw.send(pack) + self.aw.send(pack, True) # write data and strobe pack = np.empty((data_bytes + strb_bytes,), dtype=np.uint8) pack[offset:offset + bytes_this_cycle] = data_this_cycle pack[data_bytes:data_bytes + strb_bytes] = strb pack = PySbPacket(data=pack, flags=1, destination=0) - self.w.send(pack) + self.w.send(pack, True) # wait for response - pack = self.b.recv() + pack = self.b.recv(True) pack = pack.data.tobytes() pack = int.from_bytes(pack, 'little') @@ -315,10 +316,10 @@ def read( pack = pack.to_bytes((self.addr_width + 3 + 7) // 8, 'little') pack = np.frombuffer(pack, dtype=np.uint8) pack = PySbPacket(data=pack, flags=1, destination=0) - self.ar.send(pack) + self.ar.send(pack, True) # wait for response - pack = self.r.recv() + pack = self.r.recv(True) data = pack.data[offset:offset + bytes_this_cycle] resp = pack.data[data_bytes] & 0b11 diff --git a/switchboard/cmdline.py b/switchboard/cmdline.py new file mode 100644 index 00000000..486b5a51 --- /dev/null +++ b/switchboard/cmdline.py @@ -0,0 +1,126 @@ +# Copyright (c) 2024 Zero ASIC Corporation +# This code is licensed under Apache License 2.0 (see LICENSE for details) + +def get_cmdline_args( + tool: str = 'verilator', + trace: bool = True, + trace_type: str = 'vcd', + frequency: float = 100e6, + period: float = None, + max_rate: float = -1, + start_delay: float = None, + fast: bool = False, + extra_args: dict = None +): + """ + Sets up and runs a command-line option parser (argparse) using the arguments + provided as defaults. The object returned is an argparse.Namespace object, + which is the same object type returned by ArgumentParser.parse_args() + + This function is used in SbNetwork and SbDut. It should generally be + called only once, at the top level of the simulation. + + Parameters + ---------- + tool: string, optional + Which tool to use to compile simulator. Options are "verilator" or + "icarus". + + trace: bool, optional + If true, a waveform dump file will be produced using the file type + specified by `trace_type`. + + trace_type: str, optional + File type for the waveform dump file. Defaults to vcd. + + frequency: float, optional + If provided, the default frequency of the clock generated in the testbench, + in seconds. + + period: float, optional + If provided, the default period of the clock generated in the testbench, + in seconds. + + max_rate: float, optional + If provided, the maximum real-world rate that the simulation is allowed to run + at, in Hz. Can be useful to encourage time-sharing between many processes and + for performance modeling when latencies are large and/or variable. A value of + "-1" means that the rate-limiting feature is disabled. + + start_delay: float, optional + If provided, the real-world time to delay before the first clock tick in the + simulation. Can be useful to make sure that programs start at approximately + the same time and to prevent simulations from stepping on each other's toes + when starting up. + + fast: bool, optional + If True, the simulation binary will not be rebuilt if an existing one is found. + The setting here can be overridden when build() is called by setting its argument + with the same name. + + extra_args: dict, optional + If provided and cmdline=True, a dictionary of additional command line arguments + to be made available. The keys of the dictionary are the arguments ("-n", "--test", + etc.) and the values are themselves dictionaries that contain keyword arguments + accepted by argparse ("action": "store_true", "default": 42, etc.) + """ + + from argparse import ArgumentParser + + parser = ArgumentParser() + + if not trace: + parser.add_argument('--trace', action='store_true', help='Probe' + ' waveforms during simulation.') + else: + parser.add_argument('--no-trace', action='store_true', help='Do not' + ' probe waveforms during simulation. This can improve build time' + ' and run time, but reduces visibility.') + + parser.add_argument('--trace-type', type=str, choices=['vcd', 'fst'], + default=trace_type, help='File type for waveform probing.') + + if not fast: + parser.add_argument('--fast', action='store_true', help='Do not build' + ' the simulator binary if it has already been built.') + else: + parser.add_argument('--rebuild', action='store_true', help='Build the' + ' simulator binary even if it has already been built.') + + parser.add_argument('--tool', type=str, choices=['verilator', 'icarus'], + default=tool, help='Name of the simulator to use.') + + group = parser.add_mutually_exclusive_group() + group.add_argument('--period', type=float, default=period, + help='Period of the clk signal in seconds. Automatically set if' + ' --frequency is provided.') + group.add_argument('--frequency', type=float, default=frequency, + help='Frequency of the clk signal in Hz. Automatically set if' + ' --period is provided.') + + parser.add_argument('--max-rate', type=float, default=max_rate, + help='Maximum real-world rate that the simulation is allowed to run at, in Hz.') + + parser.add_argument('--start-delay', type=float, default=start_delay, + help='Delay before starting simulation, in seconds. Can be useful to prevent' + ' simulations from stepping on each others toes when starting up.') + + if extra_args is not None: + for k, v in extra_args.items(): + parser.add_argument(k, **v) + + args = parser.parse_args() + + # standardize boolean flags + + if trace: + args.trace = not args.no_trace + del args.no_trace + + if fast: + args.fast = not args.rebuild + del args.rebuild + + # return arguments + + return args diff --git a/switchboard/cpp/switchboard.hpp b/switchboard/cpp/switchboard.hpp index e7bd0132..5c0e424e 100644 --- a/switchboard/cpp/switchboard.hpp +++ b/switchboard/cpp/switchboard.hpp @@ -5,6 +5,7 @@ #define __SWITCHBOARD_HPP__ #include +#include #include #include #include @@ -27,6 +28,43 @@ struct sb_packet { uint8_t data[SB_DATA_SIZE]; } __attribute__((packed)); +static inline long max_rate_timestamp_us() { + return std::chrono::duration_cast( + std::chrono::high_resolution_clock::now().time_since_epoch()) + .count(); +} + +static inline void max_rate_tick(long& last_us, long min_period_us) { + if (min_period_us > 0) { + // measure the time now + + long now_us = max_rate_timestamp_us(); + + // sleep if needed + + if (last_us != -1) { + long dt_us = now_us - last_us; + + if (dt_us < min_period_us) { + long sleep_us = min_period_us - dt_us; + std::this_thread::sleep_for(std::chrono::microseconds(sleep_us)); + } + } + + // update the time stamp. it is not enough to set last_us = now_us, + // due to the sleep_for invocation + + last_us = max_rate_timestamp_us(); + } +} + +static inline void start_delay(double value) { + if (value > 0) { + int value_us = (value * 1.0e6) + 0.5; + std::this_thread::sleep_for(std::chrono::microseconds(value_us)); + } +} + class SB_base { public: SB_base() : m_active(false), m_q(NULL) {} @@ -35,11 +73,11 @@ class SB_base { deinit(); } - void init(std::string uri, size_t capacity = 0, bool fresh = false) { - init(uri.c_str(), capacity, fresh); + void init(std::string uri, size_t capacity = 0, bool fresh = false, double max_rate = -1) { + init(uri.c_str(), capacity, fresh, max_rate); } - void init(const char* uri, size_t capacity = 0, bool fresh = false) { + void init(const char* uri, size_t capacity = 0, bool fresh = false, double max_rate = -1) { // Default to one page of capacity if (capacity == 0) { capacity = spsc_capacity(getpagesize()); @@ -52,6 +90,9 @@ class SB_base { m_q = spsc_open(uri, capacity); m_active = true; + m_timestamp_us = -1; + + set_max_rate(max_rate); } void deinit(void) { @@ -79,6 +120,14 @@ class SB_base { return m_q->shm; } + void set_max_rate(double max_rate) { + if (max_rate > 0) { + m_min_period_us = (1.0e6 / max_rate) + 0.5; + } else { + m_min_period_us = -1; + } + } + protected: void check_active(void) { if (!m_active) { @@ -88,6 +137,8 @@ class SB_base { bool m_auto_deinit; bool m_active; + long m_min_period_us; + long m_timestamp_us; spsc_queue* m_q; }; @@ -97,12 +148,22 @@ class SBTX : public SB_base { bool send(sb_packet& p) { check_active(); + max_rate_tick(m_timestamp_us, m_min_period_us); return spsc_send(m_q, &p, sizeof p); } void send_blocking(sb_packet& p) { - while (!send(p)) { - std::this_thread::yield(); + bool success = false; + + while (!success) { + success = send(p); + + if ((!success) && (m_min_period_us == -1)) { + // maintain old behavior if max_rate isn't specified, + // i.e. yield on every iteration that the send isn't + // successful + std::this_thread::yield(); + } } } @@ -118,23 +179,35 @@ class SBRX : public SB_base { bool recv(sb_packet& p) { check_active(); + max_rate_tick(m_timestamp_us, m_min_period_us); return spsc_recv(m_q, &p, sizeof p); } bool recv() { check_active(); sb_packet dummy_p; + max_rate_tick(m_timestamp_us, m_min_period_us); return spsc_recv(m_q, &dummy_p, sizeof dummy_p); } void recv_blocking(sb_packet& p) { - while (!recv(p)) { - std::this_thread::yield(); + bool success = false; + + while (!success) { + success = recv(p); + + if ((!success) && (m_min_period_us == -1)) { + // maintain old behavior if max_rate isn't specified, + // i.e. yield on every iteration that the send isn't + // successful + std::this_thread::yield(); + } } } bool recv_peek(sb_packet& p) { check_active(); + max_rate_tick(m_timestamp_us, m_min_period_us); return spsc_recv_peek(m_q, &p, sizeof p); } }; diff --git a/switchboard/cpp/umisb.hpp b/switchboard/cpp/umisb.hpp index a9a4bc72..c313b1d7 100644 --- a/switchboard/cpp/umisb.hpp +++ b/switchboard/cpp/umisb.hpp @@ -278,7 +278,11 @@ static inline bool umisb_send(T& x, SBTX& tx, bool blocking = true, void (*loop) // finish sending the header packet if (!header_sent) { - while (!tx.send(p)) { + bool success = false; + + while (!success) { + success = tx.send(p); + if (loop) { loop(); } @@ -306,7 +310,11 @@ static inline bool umisb_recv(T& x, SBRX& rx, bool blocking = true, void (*loop) return false; } } else { - while (!rx.recv(p)) { + bool success = false; + + while (!success) { + success = rx.recv(p); + if (loop) { loop(); } diff --git a/switchboard/dpi/switchboard_dpi.cc b/switchboard/dpi/switchboard_dpi.cc index 7129e446..edf68fe9 100644 --- a/switchboard/dpi/switchboard_dpi.cc +++ b/switchboard/dpi/switchboard_dpi.cc @@ -95,3 +95,25 @@ void pi_time_taken(double* t) { (std::chrono::duration_cast(stop_time - start_time).count()); start_time = std::chrono::steady_clock::now(); } + +void pi_start_delay(double value) { + // WARNING: not tested yet since Icarus Verilog uses VPI and Verilator + // uses start_delay in main(), not through DPI + start_delay(value); +} + +void pi_max_rate_tick(svBitVecVal* t_us_vec, svBitVecVal* min_period_us_vec) { + // WARNING: not tested yet since Icarus Verilog uses VPI and Verilator + // uses max_rate_tick in main(), not through DPI + + // retrieve the previous timestamp and minimum period + long t_us, min_period_us; + memcpy(&t_us, t_us_vec, 8); + memcpy(&min_period_us, min_period_us_vec, 8); + + // call the underlying switchboard function + max_rate_tick(t_us, min_period_us); + + // store the new timestamp + memcpy(t_us_vec, &t_us, 8); +} diff --git a/switchboard/gpio.py b/switchboard/gpio.py index 3081684d..d52d01d9 100644 --- a/switchboard/gpio.py +++ b/switchboard/gpio.py @@ -88,6 +88,7 @@ def __getitem__(self, key): class UmiGpio(object): def __init__(self, iwidth, owidth, init, dstaddr, srcaddr, posted, max_bytes, umi): + self.i = UmiGpioInput( width=iwidth, dstaddr=dstaddr, diff --git a/switchboard/icarus.py b/switchboard/icarus.py index 911603b5..2445f282 100644 --- a/switchboard/icarus.py +++ b/switchboard/icarus.py @@ -10,7 +10,7 @@ from .util import plusargs_to_args, binary_run from .switchboard import path as sb_path -from subprocess import check_output, STDOUT +from subprocess import check_output, STDOUT, CalledProcessError def run(command: list, cwd: str = None) -> str: @@ -38,7 +38,11 @@ def icarus_build_vpi( cmd += ldflags cmd += [str(sbdir / 'vpi' / f'{name}_vpi.cc')] - return run(cmd, cwd) + try: + run(cmd, cwd) + except CalledProcessError as e: + print(e.output) + raise def icarus_find_vpi(cwd: Union[str, Path] = None, name: str = 'switchboard') -> Path: @@ -78,4 +82,4 @@ def icarus_run(vvp, plusargs=None, modules=None, extra_args=None, **kwargs): raise TypeError('extra_args must be a list') args += extra_args - return binary_run(bin='vvp', args=args, **kwargs, print_command=True) + return binary_run(bin='vvp', args=args, **kwargs) diff --git a/switchboard/loopback.py b/switchboard/loopback.py index 0553dfd1..a19aac07 100644 --- a/switchboard/loopback.py +++ b/switchboard/loopback.py @@ -14,7 +14,11 @@ from .umi import UmiTxRx, random_umi_packet -def umi_loopback(umi: UmiTxRx, packets: Union[Integral, Iterable, Iterator] = 10, **kwargs): +def umi_loopback( + umi: UmiTxRx, + packets: Union[Integral, Iterable, Iterator] = 10, + **kwargs +): """ Performs a loopback test by sending packets into a block and checking that the packets received back are equivalent under the UMI split/merge rules. @@ -44,6 +48,8 @@ def umi_loopback(umi: UmiTxRx, packets: Union[Integral, Iterable, Iterator] = 10 """ + # input validation + if isinstance(packets, Integral): if packets <= 0: raise ValueError(f'The number of packets must be positive (got packets={packets}).') @@ -79,7 +85,7 @@ def umi_loopback(umi: UmiTxRx, packets: Union[Integral, Iterable, Iterator] = 10 try: txp = next(packets) if pbar is not None: - pbar.update() + pbar.update(0) except StopIteration: raise ValueError('The argument "packets" is empty.') @@ -104,8 +110,6 @@ def umi_loopback(umi: UmiTxRx, packets: Union[Integral, Iterable, Iterator] = 10 try: txp = next(packets) - if pbar is not None: - pbar.update() except StopIteration: txp = None @@ -146,5 +150,8 @@ def umi_loopback(umi: UmiTxRx, packets: Union[Integral, Iterable, Iterator] = 10 rx_partial = None rx_set = None + if pbar is not None: + pbar.update() + if pbar is not None: pbar.close() diff --git a/switchboard/network.py b/switchboard/network.py new file mode 100644 index 00000000..2f1050b1 --- /dev/null +++ b/switchboard/network.py @@ -0,0 +1,251 @@ +# Copyright (c) 2024 Zero ASIC Corporation +# This code is licensed under Apache License 2.0 (see LICENSE for details) + +from copy import deepcopy +from itertools import count + +from .sbdut import SbDut +from .axi import axi_uris +from .autowrap import (directions_are_compatible, normalize_intf_type, + type_is_umi, type_is_sb, create_intf_objs, type_is_axi, type_is_axil) +from .cmdline import get_cmdline_args + +from _switchboard import delete_queues + + +class SbIntf: + def __init__(self, inst, name): + self.inst = inst + self.name = name + + +class SbInst: + def __init__(self, name, block): + self.name = name + self.block = block + self.mapping = {} + + for name, value in block.intf_defs.items(): + self.mapping[name] = None + self.__setattr__(name, SbIntf(inst=self, name=name)) + + +class SbNetwork: + def __init__(self, cmdline=False, tool: str = 'verilator', trace: bool = False, + trace_type: str = 'vcd', frequency: float = 100e6, period: float = None, + max_rate: float = None, start_delay: float = None, fast: bool = False, + extra_args: dict = None, cleanup: bool = True, args=None): + + self.insts = {} + + self.inst_name_set = set() + self.inst_name_counters = {} + + self.uri_set = set() + self.uri_counters = {} + + self.intf_defs = {} + + if cmdline: + self.args = get_cmdline_args(tool=tool, trace=trace, trace_type=trace_type, + frequency=frequency, period=period, fast=fast, max_rate=max_rate, + start_delay=start_delay, extra_args=extra_args) + elif args is not None: + self.args = args + else: + self.args = None + + if self.args is not None: + trace = self.args.trace + trace_type = self.args.trace_type + fast = self.args.fast + tool = self.args.tool + frequency = self.args.frequency + period = self.args.period + max_rate = self.args.max_rate + start_delay = self.args.start_delay + + # save settings + + self.tool = tool + self.trace = trace + self.trace_type = trace_type + self.fast = fast + + if (period is None) and (frequency is not None): + period = 1 / frequency + self.period = period + self.max_rate = max_rate + self.start_delay = start_delay + + if cleanup: + import atexit + + def cleanup_func(uri_set=self.uri_set): + if len(uri_set) > 0: + delete_queues(list(uri_set)) + + atexit.register(cleanup_func) + + def instantiate(self, block: SbDut, name: str = None): + # generate a name if needed + if name is None: + name = self.generate_inst_name(prefix=block.dut) + + # make sure the name hasn't been used already + assert name not in self.inst_name_set + + # add the name to the set of names in use + self.inst_name_set.add(name) + + # create the instance object + self.insts[name] = SbInst(name=name, block=block) + + # return the instance object + return self.insts[name] + + def connect(self, a, b, uri=None): + # retrieve the two interface definitions + intf_def_a = a.inst.block.intf_defs[a.name] + intf_def_b = b.inst.block.intf_defs[b.name] + + # make sure that the interfaces are the same + type_a = normalize_intf_type(intf_def_a['type']) + type_b = normalize_intf_type(intf_def_b['type']) + assert type_a == type_b + + # make sure that the directions are compatible + assert directions_are_compatible(type=type_a, + a=intf_def_a['direction'], b=intf_def_b['direction']) + + # determine what the queue will be called that connects the two + + if uri is None: + uri = f'{a.inst.name}_{a.name}_conn_{b.inst.name}_{b.name}' + + if type_is_sb(type_a) or type_is_umi(type_a): + uri = uri + '.q' + + self.register_uri(type=type_a, uri=uri) + + # tell both instances what they are connected to + a.inst.mapping[a.name] = uri + b.inst.mapping[b.name] = uri + + def build(self): + unique_blocks = set(inst.block for inst in self.insts.values()) + + for block in unique_blocks: + block.build() + + def external(self, intf, name=None, txrx=None, uri=None): + # make a copy of the interface definition since we will be modifying it + + intf_def = deepcopy(intf.inst.block.intf_defs[intf.name]) + + # generate URI if needed + + type = intf_def['type'] + + if uri is None: + uri = f'{intf.inst.name}_{intf.name}' + + if type_is_sb(type) or type_is_umi(type): + uri = uri + '.q' + + # register the URI to make sure it doesn't collide with anything else + + self.register_uri(type=type, uri=uri) + + # propagate information about the URI mapping + + intf_def['uri'] = uri + + intf.inst.mapping[intf.name] = uri + + # set txrx + + intf_def['txrx'] = txrx + + # set max rate + + if self.max_rate is not None: + intf_def['max_rate'] = self.max_rate + + # save interface + + if name is None: + name = intf.name + + self.intf_defs[name] = intf_def + + def simulate(self): + # create interface objects + + self.intfs = create_intf_objs(self.intf_defs) + + if self.start_delay is not None: + import time + start = time.time() + + insts = self.insts.values() + + try: + from tqdm import tqdm + insts = tqdm(insts) + except ModuleNotFoundError: + pass + + for inst in insts: + block = inst.block + + for intf_name, uri in inst.mapping.items(): + # check that the interface is wired up + + if uri is None: + raise Exception(f'{inst.name}.{intf_name} not connected') + + block.intf_defs[intf_name]['uri'] = uri + + # calculate the start delay for this process by measuring the + # time left until the start delay for the whole network is over + + if self.start_delay is not None: + now = time.time() + dt = now - start + if dt < self.start_delay: + start_delay = self.start_delay - dt + else: + start_delay = None + else: + start_delay = None + + # launch an instance of simulation + block.simulate(run=inst.name, intf_objs=False, start_delay=start_delay) + + def generate_inst_name(self, prefix): + if prefix not in self.inst_name_counters: + self.inst_name_counters[prefix] = count(0) + + counter = self.inst_name_counters[prefix] + + while True: + name = f'{prefix}_{next(counter)}' + + if name not in self.inst_name_set: + break + + return name + + def register_uri(self, type, uri, fresh=True): + if type_is_axi(type) or type_is_axil(type): + uris = axi_uris(uri) + else: + uris = [uri] + + assert self.uri_set.isdisjoint(uris) + + self.uri_set.update(uris) + + if fresh: + delete_queues(uris) diff --git a/switchboard/sbdut.py b/switchboard/sbdut.py index ff4113e4..16f56ee0 100644 --- a/switchboard/sbdut.py +++ b/switchboard/sbdut.py @@ -23,6 +23,9 @@ from .util import plusargs_to_args, binary_run from .xyce import xyce_flags from .ams import make_ams_spice_wrapper, make_ams_verilog_wrapper, parse_spice_subckts +from .autowrap import (normalize_clocks, normalize_interfaces, normalize_resets, normalize_tieoffs, + normalize_parameters, create_intf_objs) +from .cmdline import get_cmdline_args import siliconcompiler from siliconcompiler.flows import dvflow @@ -43,12 +46,23 @@ def __init__( xyce: bool = False, frequency: float = 100e6, period: float = None, + max_rate: float = -1, + start_delay: float = None, timeunit: str = None, timeprecision: str = None, warnings: List[str] = None, cmdline: bool = False, fast: bool = False, - extra_args: dict = None + extra_args: dict = None, + autowrap: bool = False, + parameters=None, + interfaces=None, + clocks=None, + resets=None, + tieoffs=None, + buildroot=None, + builddir=None, + args=None ): """ Parameters @@ -81,10 +95,25 @@ def __init__( xyce: bool, optional If True, compile for xyce co-simulation. + frequency: float, optional + If provided, the default frequency of the clock generated in the testbench, + in seconds. + period: float, optional If provided, the default period of the clock generated in the testbench, in seconds. + max_rate: float, optional + If provided, the maximum real-world rate that the simulation is allowed to run + at, in Hz. Can be useful to encourage time-sharing between many processes and + for performance modeling when latencies are large and/or variable. + + start_delay: float, optional + If provided, the real-world time to delay before the first clock tick in the + simulation. Can be useful to make sure that programs start at approximately + the same time and to prevent simulations from stepping on each other's toes + when starting up. + warnings: List[str], optional If provided, a list of tool-specific warnings to enable. If not provided, a default set of warnings will be included. Warnings can be disabled by setting this argument @@ -98,69 +127,43 @@ def __init__( If True, the simulation binary will not be rebuilt if an existing one is found. The setting here can be overridden when build() is called by setting its argument with the same name. + + extra_args: dict, optional + If provided and cmdline=True, a dictionary of additional command line arguments + to be made available. The keys of the dictionary are the arguments ("-n", "--test", + etc.) and the values are themselves dictionaries that contain keyword arguments + accepted by argparse ("action": "store_true", "default": 42, etc.) """ # call the super constructor - super().__init__(design) + if autowrap: + toplevel = 'testbench' + else: + toplevel = design + + super().__init__(toplevel) # parse command-line options if desired if cmdline: - from argparse import ArgumentParser - - parser = ArgumentParser() - - if not trace: - parser.add_argument('--trace', action='store_true', help='Probe' - ' waveforms during simulation.') - else: - parser.add_argument('--no-trace', action='store_true', help='Do not' - ' probe waveforms during simulation. This can improve build time' - ' and run time, but reduces visibility.') - - parser.add_argument('--trace-type', type=str, choices=['vcd', 'fst'], - default=trace_type, help='File type for waveform probing.') - - if not fast: - parser.add_argument('--fast', action='store_true', help='Do not build' - ' the simulator binary if it has already been built.') - else: - parser.add_argument('--rebuild', action='store_true', help='Build the' - ' simulator binary even if it has already been built.') - - parser.add_argument('--tool', type=str, choices=['verilator', 'icarus'], - default=tool, help='Name of the simulator to use.') - - group = parser.add_mutually_exclusive_group() - group.add_argument('--period', type=float, default=period, - help='Period of the clk signal in seconds. Automatically set if' - ' --frequency is provided.') - group.add_argument('--frequency', type=float, default=frequency, - help='Frequency of the clk signal in Hz. Automatically set if' - ' --period is provided.') - - if extra_args is not None: - for k, v in extra_args.items(): - parser.add_argument(k, **v) - - self.args = parser.parse_args() - - if not trace: - trace = self.args.trace - else: - trace = not self.args.no_trace + self.args = get_cmdline_args(tool=tool, trace=trace, trace_type=trace_type, + frequency=frequency, period=period, fast=fast, max_rate=max_rate, + start_delay=start_delay, extra_args=extra_args) + elif args is not None: + self.args = args + else: + self.args = None + if self.args is not None: + trace = self.args.trace trace_type = self.args.trace_type - - if not fast: - fast = self.args.fast - else: - fast = not self.args.rebuild - + fast = self.args.fast tool = self.args.tool frequency = self.args.frequency period = self.args.period + max_rate = self.args.max_rate + start_delay = self.args.start_delay # input validation @@ -180,12 +183,37 @@ def __init__( if (period is None) and (frequency is not None): period = 1 / frequency self.period = period + self.max_rate = max_rate + self.start_delay = start_delay self.timeunit = timeunit self.timeprecision = timeprecision + self.autowrap = autowrap + self.dut = design + self.parameters = normalize_parameters(parameters) + self.intf_defs = normalize_interfaces(interfaces) + self.clocks = normalize_clocks(clocks) + self.resets = normalize_resets(resets) + self.tieoffs = normalize_tieoffs(tieoffs) + + # initialization + + self.intfs = {} + # simulator-agnostic settings + if builddir is None: + if buildroot is None: + buildroot = 'build' + + buildroot = Path(buildroot).resolve() + + builddir = buildroot / metadata_str(design=design, parameters=parameters, tool=tool, + trace=trace, trace_type=trace_type) + + self.set('option', 'builddir', str(Path(builddir).resolve())) + if fpga: # library dirs self.set('option', 'ydir', sb_path() / 'verilog' / 'fpga') @@ -353,6 +381,20 @@ def build(self, cwd: str = None, fast: bool = None): if sim is not None: return sim + # build the wrapper if needed + if self.autowrap: + from .autowrap import autowrap + + filename = Path(self.get('option', 'builddir')).resolve() / 'testbench.sv' + + filename.parent.mkdir(exist_ok=True, parents=True) + + autowrap(design=self.dut, parameters=self.parameters, + interfaces=self.intf_defs, clocks=self.clocks, resets=self.resets, + tieoffs=self.tieoffs, filename=filename) + + self.input(filename) + # if we get to this point, then we need to rebuild # the simulation binary self.run() @@ -367,7 +409,11 @@ def simulate( cwd: str = None, trace: bool = None, period: float = None, - frequency: float = None + frequency: float = None, + max_rate: float = None, + start_delay: float = None, + run: str = None, + intf_objs: bool = True ) -> subprocess.Popen: """ Parameters @@ -395,6 +441,14 @@ def simulate( in seconds. """ + # set up interfaces if needed + + if max_rate is None: + max_rate = self.max_rate + + if intf_objs: + self.intfs = create_intf_objs(self.intf_defs, max_rate=max_rate) + # set defaults if plusargs is None: @@ -415,6 +469,9 @@ def simulate( if period is None: period = self.period + if start_delay is None: + start_delay = self.start_delay + # build the simulation if necessary sim = self.build(cwd=cwd, fast=True) @@ -424,13 +481,30 @@ def simulate( # since logic in the testbench can use that flag to enable/disable # waveform dumping in a simulator-agnostic manner. - if trace and ('trace' not in plusargs) and ('+trace' not in args): - plusargs.append('trace') + if trace: + carefully_add_plusarg(key='trace', args=args, plusargs=plusargs) + + if period is not None: + carefully_add_plusarg(key='period', value=period, args=args, plusargs=plusargs) + + if max_rate is not None: + carefully_add_plusarg(key='max-rate', value=max_rate, args=args, plusargs=plusargs) + + if start_delay is not None: + carefully_add_plusarg( + key='start-delay', value=start_delay, args=args, plusargs=plusargs) + + # add plusargs that define queue connections + + for name, value in self.intf_defs.items(): + plusargs += [(name, value['uri'])] + + # run-specific configurations (if running the same simulator build multiple times + # in parallel) - if ((period is not None) - and ('period' not in plusargs) - and not any(elem.startswith('+period') for elem in args)): - plusargs.append(('period', period)) + if run is not None: + dumpfile = f'{run}.{self.trace_type}' + plusargs.append(('dumpfile', dumpfile)) # run the simulation @@ -596,3 +670,46 @@ def input_analog( ) self.input(verilog_wrapper) + + +def metadata_str(design: str, tool: str, trace: bool, trace_type: str, + parameters: dict = None) -> Path: + + opts = [] + + opts += [design] + + if parameters is not None: + for k, v in parameters.items(): + opts += [k, v] + + opts += [tool] + + if trace: + assert trace_type is not None + opts += [trace_type] + + return '-'.join(str(opt) for opt in opts) + + +def carefully_add_plusarg(key, args, plusargs, value=None): + for plusarg in plusargs: + if isinstance(plusarg, (list, tuple)): + if (len(plusarg) >= 1) and (key == plusarg[0]): + return + elif key == plusarg: + return + + if f'+{key}' in args: + return + + if any(elem.startswith(f'+{key}+') for elem in args): + return + + if any(elem.startswith(f'+{key}=') for elem in args): + return + + if value is None: + plusargs.append(key) + else: + plusargs.append((key, value)) diff --git a/switchboard/umi.py b/switchboard/umi.py index 0d4acb5b..3fbea52c 100644 --- a/switchboard/umi.py +++ b/switchboard/umi.py @@ -20,7 +20,8 @@ class UmiTxRx: def __init__(self, tx_uri: str = None, rx_uri: str = None, srcaddr: Union[int, Dict[str, int]] = 0, posted: bool = False, - max_bytes: int = None, fresh: bool = False, error: bool = True): + max_bytes: int = None, fresh: bool = False, error: bool = True, + max_rate: float = -1): """ Parameters ---------- @@ -62,7 +63,7 @@ def __init__(self, tx_uri: str = None, rx_uri: str = None, if rx_uri is None: rx_uri = "" - self.umi = PyUmi(tx_uri, rx_uri, fresh) + self.umi = PyUmi(tx_uri, rx_uri, fresh, max_rate=max_rate) if srcaddr is not None: # convert srcaddr default to a dictionary if necessary diff --git a/switchboard/util.py b/switchboard/util.py index 9d684fdf..d4d0b341 100644 --- a/switchboard/util.py +++ b/switchboard/util.py @@ -68,6 +68,12 @@ def stop_bin(p=p, stop_timeout=stop_timeout, use_sigint=use_sigint): # and sigint didn't work (or we didn't try it) p.terminate() + # note: tried combining all process terminations into a single + # function registered with atexit, but it didn't appear to make + # a difference when running with a few hundred verilator sim + # processes - still took a few dozen milliseconds to stop each + # simulator. + atexit.register(stop_bin) return p diff --git a/switchboard/verilator/testbench.cc b/switchboard/verilator/testbench.cc index 7ee43a27..d6f739e8 100644 --- a/switchboard/verilator/testbench.cc +++ b/switchboard/verilator/testbench.cc @@ -13,6 +13,7 @@ // For changing the clock period #include #include +#include #include // Include common routines @@ -21,6 +22,9 @@ // Include model header, generated from Verilating "top.v" #include "Vtestbench.h" +// Include switchboard functions +#include "switchboard.hpp" + // Legacy function required only so linking works on Cygwin and MSVC++ double sc_time_stamp() { return 0; @@ -33,6 +37,33 @@ void sigint_handler(int unused) { got_sigint = 1; } +std::string extract_plusarg_value(const char* match, const char* name) { + if (match) { + std::string full = std::string(match); + std::string prefix = "+" + std::string(name) + "="; + size_t len = prefix.size(); + // match requirements: there must be at least one character after + // the prefix, and the argument must start with the prefix, + // ignoring the last character of the prefix, which can be + // anything (typically "=" or "+") + if ((full.size() >= (len + 1)) && (full.substr(0, len - 1) == prefix.substr(0, len - 1))) { + return std::string(match).substr(len); + } + } + + // if we get here, return an empty string + return ""; +} + +template void parse_plusarg(const char* match, const char* name, T& result) { + std::string value = extract_plusarg_value(match, name); + + if (value != "") { + std::istringstream iss(value); + iss >> result; + } +} + int main(int argc, char** argv, char** env) { // Prevent unused variable warnings if (false && argc && argv && env) {} @@ -57,20 +88,15 @@ int main(int argc, char** argv, char** env) { // parse the clock period, if provided double period = 10e-9; - const char* flag = contextp->commandArgsPlusMatch("period"); - if (flag) { - std::string full = std::string(flag); - std::string prefix = "+period="; - size_t len = prefix.size(); - // match requirements: there must be at least one character after - // the prefix, and the argument must start with the prefix, - // ignoring the last character of the prefix, which can be - // anything (typically "=" or "+") - if ((full.size() >= (len + 1)) && (full.substr(0, len - 1) == prefix.substr(0, len - 1))) { - std::string rest = std::string(flag).substr(len); - period = std::stod(rest); - } - } + const char* period_match = contextp->commandArgsPlusMatch("period"); + parse_plusarg(period_match, "period", period); + + // parse the maximum simulation rate, if provided. convert it to a target + // period in microseconds + + double max_rate = -1; + const char* rate_match = contextp->commandArgsPlusMatch("max-rate"); + parse_plusarg(rate_match, "max-rate", max_rate); // convert the clock period an integer, scaling by the time precision uint64_t iperiod = std::round(period * std::pow(10.0, -1.0 * contextp->timeprecision())); @@ -84,8 +110,22 @@ int main(int argc, char** argv, char** env) { // Set up Ctrl-C handler signal(SIGINT, sigint_handler); + // Optional delay before setting up main loop + + double start_delay_value = -1; + const char* delay_match = contextp->commandArgsPlusMatch("start-delay"); + parse_plusarg(delay_match, "start-delay", start_delay_value); + + start_delay(start_delay_value); + // Main loop + + long t_us = -1; + long min_period_us = (1.0e6 / max_rate) + 0.5; + while (!(contextp->gotFinish() || got_sigint)) { + max_rate_tick(t_us, min_period_us); + contextp->timeInc(duration0); top->clk = 1; top->eval(); diff --git a/switchboard/verilog/common/switchboard.vh b/switchboard/verilog/common/switchboard.vh index 649450ff..5e449b77 100644 --- a/switchboard/verilog/common/switchboard.vh +++ b/switchboard/verilog/common/switchboard.vh @@ -9,331 +9,355 @@ // ref: https://stackoverflow.com/a/15376637 `define STRINGIFY(x) `"x`" -`define SB_UMI_WIRES(signal, dw, cw, aw) \ - wire signal``_valid; \ - wire [((cw)-1): 0] signal``_cmd; \ - wire [((aw)-1): 0] signal``_dstaddr; \ - wire [((aw)-1): 0] signal``_srcaddr; \ - wire [((dw)-1): 0] signal``_data; \ +`define SB_UMI_WIRES(signal, dw, cw, aw) \ + wire signal``_valid; \ + wire [((cw)-1): 0] signal``_cmd; \ + wire [((aw)-1): 0] signal``_dstaddr; \ + wire [((aw)-1): 0] signal``_srcaddr; \ + wire [((dw)-1): 0] signal``_data; \ wire signal``_ready // alias for SB_UMI_WIRES, keep for backwards compatibility -`define UMI_PORT_WIRES_WIDTHS(prefix, dw, cw, aw) \ +`define UMI_PORT_WIRES_WIDTHS(prefix, dw, cw, aw) \ `SB_UMI_WIRES(prefix, dw, cw, aw) -`define QUEUE_TO_UMI_SIM(signal, dw, cw, aw, file, vldmode=1, clk_signal=clk) \ - queue_to_umi_sim #( \ - .VALID_MODE_DEFAULT(vldmode), \ - .DW(dw), \ - .CW(cw), \ - .AW(aw), \ - .FILE(file) \ - ) signal``_sb_inst ( \ - .clk(clk_signal), \ - .data(signal``_data), \ - .srcaddr(signal``_srcaddr), \ - .dstaddr(signal``_dstaddr), \ - .cmd(signal``_cmd), \ - .ready(signal``_ready), \ - .valid(signal``_valid) \ +`define QUEUE_TO_UMI_SIM(signal, dw, cw, aw, file, vldmode=1, clk_signal=clk) \ + queue_to_umi_sim #( \ + .VALID_MODE_DEFAULT(vldmode), \ + .DW(dw), \ + .CW(cw), \ + .AW(aw), \ + .FILE(file) \ + ) signal``_sb_inst ( \ + .clk(clk_signal), \ + .data(signal``_data), \ + .srcaddr(signal``_srcaddr), \ + .dstaddr(signal``_dstaddr), \ + .cmd(signal``_cmd), \ + .ready(signal``_ready), \ + .valid(signal``_valid) \ ) -`define UMI_TO_QUEUE_SIM(signal, dw, cw, aw, file, rdymode=1, clk_signal=clk) \ - umi_to_queue_sim #( \ - .READY_MODE_DEFAULT(rdymode), \ - .DW(dw), \ - .CW(cw), \ - .AW(aw), \ - .FILE(file) \ - ) signal``_sb_inst ( \ - .clk(clk_signal), \ - .data(signal``_data), \ - .srcaddr(signal``_srcaddr), \ - .dstaddr(signal``_dstaddr), \ - .cmd(signal``_cmd), \ - .ready(signal``_ready), \ - .valid(signal``_valid) \ +`define UMI_TO_QUEUE_SIM(signal, dw, cw, aw, file, rdymode=1, clk_signal=clk) \ + umi_to_queue_sim #( \ + .READY_MODE_DEFAULT(rdymode), \ + .DW(dw), \ + .CW(cw), \ + .AW(aw), \ + .FILE(file) \ + ) signal``_sb_inst ( \ + .clk(clk_signal), \ + .data(signal``_data), \ + .srcaddr(signal``_srcaddr), \ + .dstaddr(signal``_dstaddr), \ + .cmd(signal``_cmd), \ + .ready(signal``_ready), \ + .valid(signal``_valid) \ ) -`define SB_UMI_CONNECT(a, b) \ - .a``_valid(b``_valid), \ - .a``_cmd(b``_cmd), \ - .a``_dstaddr(b``_dstaddr), \ - .a``_srcaddr(b``_srcaddr), \ - .a``_data(b``_data), \ +`define SB_UMI_CONNECT(a, b) \ + .a``_valid(b``_valid), \ + .a``_cmd(b``_cmd), \ + .a``_dstaddr(b``_dstaddr), \ + .a``_srcaddr(b``_srcaddr), \ + .a``_data(b``_data), \ .a``_ready(b``_ready) -`define SWITCHBOARD_SIM_PORT(prefix, dw) \ - `SB_UMI_WIRES(prefix``_req, dw, 32, 64); \ - `SB_UMI_WIRES(prefix``_resp, dw, 32, 64); \ - `QUEUE_TO_UMI_SIM(prefix``_req, dw, 32, 64); \ +`define SWITCHBOARD_SIM_PORT(prefix, dw) \ + `SB_UMI_WIRES(prefix``_req, dw, 32, 64); \ + `SB_UMI_WIRES(prefix``_resp, dw, 32, 64); \ + `QUEUE_TO_UMI_SIM(prefix``_req, dw, 32, 64); \ `UMI_TO_QUEUE_SIM(prefix``_resp, dw, 32, 64) -`define SB_WIRES(signal, dw) \ - wire [((dw)-1):0] signal``_data; \ - wire [31:0] signal``_dest; \ - wire signal``_last; \ - wire signal``_valid; \ +`define SB_WIRES(signal, dw) \ + wire [((dw)-1):0] signal``_data; \ + wire [31:0] signal``_dest; \ + wire signal``_last; \ + wire signal``_valid; \ wire signal``_ready -`define SB_TO_QUEUE_SIM(signal, dw, file, rdymode=1, clk_signal=clk) \ - sb_to_queue_sim #( \ - .READY_MODE_DEFAULT(rdymode), \ - .DW(dw), \ - .FILE(file) \ - ) signal``_sb_inst ( \ - .clk(clk_signal), \ - .data(signal``_data), \ - .dest(signal``_dest), \ - .last(signal``_last), \ - .ready(signal``_ready), \ - .valid(signal``_valid) \ +`define SB_CONNECT(a, b) \ + .a``_data(b``_data), \ + .a``_dest(b``_dest), \ + .a``_last(b``_last), \ + .a``_valid(b``_valid), \ + .a``_ready(b``_ready) + +`define SB_PORT(signal, dw, i, o) \ + i wire [((dw)-1):0] signal``_data, \ + i wire [31:0] signal``_dest, \ + i wire signal``_last, \ + i wire signal``_valid, \ + o wire signal``_ready + +`define SB_INPUT(signal, dw) \ + `SB_PORT(signal, dw, input, output) + +`define SB_OUTPUT(signal, dw) \ + `SB_PORT(signal, dw, output, input) + +`define SB_TO_QUEUE_SIM(signal, dw, file, rdymode=1, clk_signal=clk) \ + sb_to_queue_sim #( \ + .READY_MODE_DEFAULT(rdymode), \ + .DW(dw), \ + .FILE(file) \ + ) signal``_sb_inst ( \ + .clk(clk_signal), \ + .data(signal``_data), \ + .dest(signal``_dest), \ + .last(signal``_last), \ + .ready(signal``_ready), \ + .valid(signal``_valid) \ ) -`define QUEUE_TO_SB_SIM(signal, dw, file, vldmode=1, clk_signal=clk) \ - queue_to_sb_sim #( \ - .VALID_MODE_DEFAULT(vldmode), \ - .DW(dw), \ - .FILE(file) \ - ) signal``_sb_inst ( \ - .clk(clk_signal), \ - .data(signal``_data), \ - .dest(signal``_dest), \ - .last(signal``_last), \ - .ready(signal``_ready), \ - .valid(signal``_valid) \ +`define QUEUE_TO_SB_SIM(signal, dw, file, vldmode=1, clk_signal=clk) \ + queue_to_sb_sim #( \ + .VALID_MODE_DEFAULT(vldmode), \ + .DW(dw), \ + .FILE(file) \ + ) signal``_sb_inst ( \ + .clk(clk_signal), \ + .data(signal``_data), \ + .dest(signal``_dest), \ + .last(signal``_last), \ + .ready(signal``_ready), \ + .valid(signal``_valid) \ ) -`define SB_AXIL_WIRES(signal, dw, aw) \ - wire [((aw)-1):0] signal``_awaddr; \ - wire [2:0] signal``_awprot; \ - wire signal``_awvalid; \ - wire signal``_awready; \ - wire [((dw)-1):0] signal``_wdata; \ - wire [(((dw)/8)-1):0] signal``_wstrb; \ - wire signal``_wvalid; \ - wire signal``_wready; \ - wire [1:0] signal``_bresp; \ - wire signal``_bvalid; \ - wire signal``_bready; \ - wire [((aw)-1):0] signal``_araddr; \ - wire [2:0] signal``_arprot; \ - wire signal``_arvalid; \ - wire signal``_arready; \ - wire [((dw))-1:0] signal``_rdata; \ - wire [1:0] signal``_rresp; \ - wire signal``_rvalid; \ +`define SB_AXIL_WIRES(signal, dw, aw) \ + wire [((aw)-1):0] signal``_awaddr; \ + wire [2:0] signal``_awprot; \ + wire signal``_awvalid; \ + wire signal``_awready; \ + wire [((dw)-1):0] signal``_wdata; \ + wire [(((dw)/8)-1):0] signal``_wstrb; \ + wire signal``_wvalid; \ + wire signal``_wready; \ + wire [1:0] signal``_bresp; \ + wire signal``_bvalid; \ + wire signal``_bready; \ + wire [((aw)-1):0] signal``_araddr; \ + wire [2:0] signal``_arprot; \ + wire signal``_arvalid; \ + wire signal``_arready; \ + wire [((dw))-1:0] signal``_rdata; \ + wire [1:0] signal``_rresp; \ + wire signal``_rvalid; \ wire signal``_rready -`define SB_AXIL_CONNECT(a, b) \ - .a``_awaddr(b``_awaddr), \ - .a``_awprot(b``_awprot), \ - .a``_awvalid(b``_awvalid), \ - .a``_awready(b``_awready), \ - .a``_wdata(b``_wdata), \ - .a``_wstrb(b``_wstrb), \ - .a``_wvalid(b``_wvalid), \ - .a``_wready(b``_wready), \ - .a``_bresp(b``_bresp), \ - .a``_bvalid(b``_bvalid), \ - .a``_bready(b``_bready), \ - .a``_araddr(b``_araddr), \ - .a``_arprot(b``_arprot), \ - .a``_arvalid(b``_arvalid), \ - .a``_arready(b``_arready), \ - .a``_rdata(b``_rdata), \ - .a``_rresp(b``_rresp), \ - .a``_rvalid(b``_rvalid), \ +`define SB_AXIL_CONNECT(a, b) \ + .a``_awaddr(b``_awaddr), \ + .a``_awprot(b``_awprot), \ + .a``_awvalid(b``_awvalid), \ + .a``_awready(b``_awready), \ + .a``_wdata(b``_wdata), \ + .a``_wstrb(b``_wstrb), \ + .a``_wvalid(b``_wvalid), \ + .a``_wready(b``_wready), \ + .a``_bresp(b``_bresp), \ + .a``_bvalid(b``_bvalid), \ + .a``_bready(b``_bready), \ + .a``_araddr(b``_araddr), \ + .a``_arprot(b``_arprot), \ + .a``_arvalid(b``_arvalid), \ + .a``_arready(b``_arready), \ + .a``_rdata(b``_rdata), \ + .a``_rresp(b``_rresp), \ + .a``_rvalid(b``_rvalid), \ .a``_rready(b``_rready) -`define SB_AXIL_M(signal, dw, aw, file, vldmode=1, rdymode=1, clk_signal=clk) \ - sb_axil_m #( \ - .DATA_WIDTH(dw), \ - .ADDR_WIDTH(aw), \ - .VALID_MODE_DEFAULT(vldmode), \ - .READY_MODE_DEFAULT(rdymode), \ - .FILE(file) \ - ) signal``_sb_inst ( \ - .clk(clk_signal), \ - .m_axil_awaddr(signal``_awaddr), \ - .m_axil_awprot(signal``_awprot), \ - .m_axil_awvalid(signal``_awvalid), \ - .m_axil_awready(signal``_awready), \ - .m_axil_wdata(signal``_wdata), \ - .m_axil_wstrb(signal``_wstrb), \ - .m_axil_wvalid(signal``_wvalid), \ - .m_axil_wready(signal``_wready), \ - .m_axil_bresp(signal``_bresp), \ - .m_axil_bvalid(signal``_bvalid), \ - .m_axil_bready(signal``_bready), \ - .m_axil_araddr(signal``_araddr), \ - .m_axil_arprot(signal``_arprot), \ - .m_axil_arvalid(signal``_arvalid), \ - .m_axil_arready(signal``_arready), \ - .m_axil_rdata(signal``_rdata), \ - .m_axil_rresp(signal``_rresp), \ - .m_axil_rvalid(signal``_rvalid), \ - .m_axil_rready(signal``_rready) \ +`define SB_AXIL(dir, signal, dw, aw, file, vldmode=1, rdymode=1, clk_signal=clk) \ + sb_axil_``dir #( \ + .DATA_WIDTH(dw), \ + .ADDR_WIDTH(aw), \ + .VALID_MODE_DEFAULT(vldmode), \ + .READY_MODE_DEFAULT(rdymode), \ + .FILE(file) \ + ) signal``_sb_inst ( \ + .clk(clk_signal), \ + .dir``_axil_awaddr(signal``_awaddr), \ + .dir``_axil_awprot(signal``_awprot), \ + .dir``_axil_awvalid(signal``_awvalid), \ + .dir``_axil_awready(signal``_awready), \ + .dir``_axil_wdata(signal``_wdata), \ + .dir``_axil_wstrb(signal``_wstrb), \ + .dir``_axil_wvalid(signal``_wvalid), \ + .dir``_axil_wready(signal``_wready), \ + .dir``_axil_bresp(signal``_bresp), \ + .dir``_axil_bvalid(signal``_bvalid), \ + .dir``_axil_bready(signal``_bready), \ + .dir``_axil_araddr(signal``_araddr), \ + .dir``_axil_arprot(signal``_arprot), \ + .dir``_axil_arvalid(signal``_arvalid), \ + .dir``_axil_arready(signal``_arready), \ + .dir``_axil_rdata(signal``_rdata), \ + .dir``_axil_rresp(signal``_rresp), \ + .dir``_axil_rvalid(signal``_rvalid), \ + .dir``_axil_rready(signal``_rready) \ ) -`define SB_AXI_WIRES(signal, dw, aw, idw) \ - wire [(idw)-1:0] signal``_awid; \ - wire [(aw)-1:0] signal``_awaddr; \ - wire [7:0] signal``_awlen; \ - wire [2:0] signal``_awsize; \ - wire [1:0] signal``_awburst; \ - wire signal``_awlock; \ - wire [3:0] signal``_awcache; \ - wire [2:0] signal``_awprot; \ - wire signal``_awvalid; \ - wire signal``_awready; \ - wire [(dw)-1:0] signal``_wdata; \ - wire [((dw)/8)-1:0] signal``_wstrb; \ - wire signal``_wlast; \ - wire signal``_wvalid; \ - wire signal``_wready; \ - wire [(idw)-1:0] signal``_bid; \ - wire [1:0] signal``_bresp; \ - wire signal``_bvalid; \ - wire signal``_bready; \ - wire [(idw)-1:0] signal``_arid; \ - wire [(aw)-1:0] signal``_araddr; \ - wire [7:0] signal``_arlen; \ - wire [2:0] signal``_arsize; \ - wire [1:0] signal``_arburst; \ - wire signal``_arlock; \ - wire [3:0] signal``_arcache; \ - wire [2:0] signal``_arprot; \ - wire signal``_arvalid; \ - wire signal``_arready; \ - wire [(idw)-1:0] signal``_rid; \ - wire [(dw)-1:0] signal``_rdata; \ - wire [1:0] signal``_rresp; \ - wire signal``_rlast; \ - wire signal``_rvalid; \ +`define SB_AXIL_M(signal, dw, aw, file, vldmode=1, rdymode=1, clk_signal=clk) \ + `SB_AXIL(m, signal, dw, aw, file, vldmode, rdymode, clk_signal) + +`define SB_AXIL_S(signal, dw, aw, file, vldmode=1, rdymode=1, clk_signal=clk) \ + `SB_AXIL(s, signal, dw, aw, file, vldmode, rdymode, clk_signal) + +`define SB_AXI_WIRES(signal, dw, aw, idw) \ + wire [(idw)-1:0] signal``_awid; \ + wire [(aw)-1:0] signal``_awaddr; \ + wire [7:0] signal``_awlen; \ + wire [2:0] signal``_awsize; \ + wire [1:0] signal``_awburst; \ + wire signal``_awlock; \ + wire [3:0] signal``_awcache; \ + wire [2:0] signal``_awprot; \ + wire signal``_awvalid; \ + wire signal``_awready; \ + wire [(dw)-1:0] signal``_wdata; \ + wire [((dw)/8)-1:0] signal``_wstrb; \ + wire signal``_wlast; \ + wire signal``_wvalid; \ + wire signal``_wready; \ + wire [(idw)-1:0] signal``_bid; \ + wire [1:0] signal``_bresp; \ + wire signal``_bvalid; \ + wire signal``_bready; \ + wire [(idw)-1:0] signal``_arid; \ + wire [(aw)-1:0] signal``_araddr; \ + wire [7:0] signal``_arlen; \ + wire [2:0] signal``_arsize; \ + wire [1:0] signal``_arburst; \ + wire signal``_arlock; \ + wire [3:0] signal``_arcache; \ + wire [2:0] signal``_arprot; \ + wire signal``_arvalid; \ + wire signal``_arready; \ + wire [(idw)-1:0] signal``_rid; \ + wire [(dw)-1:0] signal``_rdata; \ + wire [1:0] signal``_rresp; \ + wire signal``_rlast; \ + wire signal``_rvalid; \ wire signal``_rready; -`define SB_AXI_CONNECT(a, b) \ - .a``_awid(b``_awid), \ - .a``_awaddr(b``_awaddr), \ - .a``_awlen(b``_awlen), \ - .a``_awsize(b``_awsize), \ - .a``_awburst(b``_awburst), \ - .a``_awlock(b``_awlock), \ - .a``_awcache(b``_awcache), \ - .a``_awprot(b``_awprot), \ - .a``_awvalid(b``_awvalid), \ - .a``_awready(b``_awready), \ - .a``_wdata(b``_wdata), \ - .a``_wstrb(b``_wstrb), \ - .a``_wlast(b``_wlast), \ - .a``_wvalid(b``_wvalid), \ - .a``_wready(b``_wready), \ - .a``_bid(b``_bid), \ - .a``_bresp(b``_bresp), \ - .a``_bvalid(b``_bvalid), \ - .a``_bready(b``_bready), \ - .a``_arid(b``_arid), \ - .a``_araddr(b``_araddr), \ - .a``_arlen(b``_arlen), \ - .a``_arsize(b``_arsize), \ - .a``_arburst(b``_arburst), \ - .a``_arlock(b``_arlock), \ - .a``_arcache(b``_arcache), \ - .a``_arprot(b``_arprot), \ - .a``_arvalid(b``_arvalid), \ - .a``_arready(b``_arready), \ - .a``_rid(b``_rid), \ - .a``_rdata(b``_rdata), \ - .a``_rresp(b``_rresp), \ - .a``_rlast(b``_rlast), \ - .a``_rvalid(b``_rvalid), \ +`define SB_AXI_CONNECT(a, b) \ + .a``_awid(b``_awid), \ + .a``_awaddr(b``_awaddr), \ + .a``_awlen(b``_awlen), \ + .a``_awsize(b``_awsize), \ + .a``_awburst(b``_awburst), \ + .a``_awlock(b``_awlock), \ + .a``_awcache(b``_awcache), \ + .a``_awprot(b``_awprot), \ + .a``_awvalid(b``_awvalid), \ + .a``_awready(b``_awready), \ + .a``_wdata(b``_wdata), \ + .a``_wstrb(b``_wstrb), \ + .a``_wlast(b``_wlast), \ + .a``_wvalid(b``_wvalid), \ + .a``_wready(b``_wready), \ + .a``_bid(b``_bid), \ + .a``_bresp(b``_bresp), \ + .a``_bvalid(b``_bvalid), \ + .a``_bready(b``_bready), \ + .a``_arid(b``_arid), \ + .a``_araddr(b``_araddr), \ + .a``_arlen(b``_arlen), \ + .a``_arsize(b``_arsize), \ + .a``_arburst(b``_arburst), \ + .a``_arlock(b``_arlock), \ + .a``_arcache(b``_arcache), \ + .a``_arprot(b``_arprot), \ + .a``_arvalid(b``_arvalid), \ + .a``_arready(b``_arready), \ + .a``_rid(b``_rid), \ + .a``_rdata(b``_rdata), \ + .a``_rresp(b``_rresp), \ + .a``_rlast(b``_rlast), \ + .a``_rvalid(b``_rvalid), \ .a``_rready(b``_rready) -`define SB_AXI_M(signal, dw, aw, idw, file, vldmode=1, rdymode=1, clk_signal=clk) \ - sb_axi_m #( \ - .DATA_WIDTH(dw), \ - .ADDR_WIDTH(aw), \ - .ID_WIDTH(idw), \ - .VALID_MODE_DEFAULT(vldmode), \ - .READY_MODE_DEFAULT(rdymode), \ - .FILE(file) \ - ) signal``_sb_inst ( \ - .clk(clk_signal), \ - .m_axi_awid(signal``_awid), \ - .m_axi_awaddr(signal``_awaddr), \ - .m_axi_awlen(signal``_awlen), \ - .m_axi_awsize(signal``_awsize), \ - .m_axi_awburst(signal``_awburst), \ - .m_axi_awlock(signal``_awlock), \ - .m_axi_awcache(signal``_awcache), \ - .m_axi_awprot(signal``_awprot), \ - .m_axi_awvalid(signal``_awvalid), \ - .m_axi_awready(signal``_awready), \ - .m_axi_wdata(signal``_wdata), \ - .m_axi_wstrb(signal``_wstrb), \ - .m_axi_wlast(signal``_wlast), \ - .m_axi_wvalid(signal``_wvalid), \ - .m_axi_wready(signal``_wready), \ - .m_axi_bid(signal``_bid), \ - .m_axi_bresp(signal``_bresp), \ - .m_axi_bvalid(signal``_bvalid), \ - .m_axi_bready(signal``_bready), \ - .m_axi_arid(signal``_arid), \ - .m_axi_araddr(signal``_araddr), \ - .m_axi_arlen(signal``_arlen), \ - .m_axi_arsize(signal``_arsize), \ - .m_axi_arburst(signal``_arburst), \ - .m_axi_arlock(signal``_arlock), \ - .m_axi_arcache(signal``_arcache), \ - .m_axi_arprot(signal``_arprot), \ - .m_axi_arvalid(signal``_arvalid), \ - .m_axi_arready(signal``_arready), \ - .m_axi_rid(signal``_rid), \ - .m_axi_rdata(signal``_rdata), \ - .m_axi_rresp(signal``_rresp), \ - .m_axi_rlast(signal``_rlast), \ - .m_axi_rvalid(signal``_rvalid), \ - .m_axi_rready(signal``_rready) \ +`define SB_AXI(dir, signal, dw, aw, idw, file, vldmode=1, rdymode=1, clk_signal=clk) \ + sb_axi_``dir #( \ + .DATA_WIDTH(dw), \ + .ADDR_WIDTH(aw), \ + .ID_WIDTH(idw), \ + .VALID_MODE_DEFAULT(vldmode), \ + .READY_MODE_DEFAULT(rdymode), \ + .FILE(file) \ + ) signal``_sb_inst ( \ + .clk(clk_signal), \ + .dir``_axi_awid(signal``_awid), \ + .dir``_axi_awaddr(signal``_awaddr), \ + .dir``_axi_awlen(signal``_awlen), \ + .dir``_axi_awsize(signal``_awsize), \ + .dir``_axi_awburst(signal``_awburst), \ + .dir``_axi_awlock(signal``_awlock), \ + .dir``_axi_awcache(signal``_awcache), \ + .dir``_axi_awprot(signal``_awprot), \ + .dir``_axi_awvalid(signal``_awvalid), \ + .dir``_axi_awready(signal``_awready), \ + .dir``_axi_wdata(signal``_wdata), \ + .dir``_axi_wstrb(signal``_wstrb), \ + .dir``_axi_wlast(signal``_wlast), \ + .dir``_axi_wvalid(signal``_wvalid), \ + .dir``_axi_wready(signal``_wready), \ + .dir``_axi_bid(signal``_bid), \ + .dir``_axi_bresp(signal``_bresp), \ + .dir``_axi_bvalid(signal``_bvalid), \ + .dir``_axi_bready(signal``_bready), \ + .dir``_axi_arid(signal``_arid), \ + .dir``_axi_araddr(signal``_araddr), \ + .dir``_axi_arlen(signal``_arlen), \ + .dir``_axi_arsize(signal``_arsize), \ + .dir``_axi_arburst(signal``_arburst), \ + .dir``_axi_arlock(signal``_arlock), \ + .dir``_axi_arcache(signal``_arcache), \ + .dir``_axi_arprot(signal``_arprot), \ + .dir``_axi_arvalid(signal``_arvalid), \ + .dir``_axi_arready(signal``_arready), \ + .dir``_axi_rid(signal``_rid), \ + .dir``_axi_rdata(signal``_rdata), \ + .dir``_axi_rresp(signal``_rresp), \ + .dir``_axi_rlast(signal``_rlast), \ + .dir``_axi_rvalid(signal``_rvalid), \ + .dir``_axi_rready(signal``_rready) \ ) -`define SB_CREATE_CLOCK(clk_signal) \ - `ifdef SB_XYCE \ - timeunit 1s; \ - timeprecision 1fs; \ - `define SB_DELAY(t) #(t) \ - `else \ - timeunit 1ns; \ - timeprecision 1ns; \ - `define SB_DELAY(t) #((t)*1e9) \ - `endif \ - \ - real period = 10e-9; \ - \ - initial begin \ - void'($value$plusargs("period=%f", period)); \ - end \ - \ - reg clk_signal; \ - always begin \ - clk_signal = 1'b0; \ - `SB_DELAY(0.5 * period); \ - clk_signal = 1'b1; \ - `SB_DELAY(0.5 * period); \ - end +`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_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_CREATE_CLOCK(clk_signal, period=10e-9, duty_cycle=0.5, max_rate=-1, start_delay=-1) \ + wire clk_signal; \ + \ + sb_clk_gen #( \ + .DEFAULT_PERIOD(period), \ + .DEFAULT_DUTY_CYCLE(duty_cycle), \ + .DEFAULT_MAX_RATE(max_rate), \ + .DEFAULT_START_DELAY(start_delay) \ + ) clk_signal``_sb_inst ( \ + .clk(clk_signal) \ + ); -`define SB_SETUP_PROBES \ - `ifdef SB_TRACE \ - initial begin \ - if ($test$plusargs("trace")) begin \ - `ifdef SB_TRACE_FST \ - $dumpfile("testbench.fst"); \ - `else \ - $dumpfile("testbench.vcd"); \ - `endif \ - $dumpvars(0, testbench); \ - end \ - end \ +`define SB_SETUP_PROBES \ + `ifdef SB_TRACE \ + string dumpfile_sb_value; \ + initial begin \ + if ($test$plusargs("trace")) begin \ + if ($value$plusargs("dumpfile=%s", dumpfile_sb_value)) begin \ + $dumpfile(dumpfile_sb_value); \ + end else begin \ + `ifdef SB_TRACE_FST \ + $dumpfile("testbench.fst"); \ + `else \ + $dumpfile("testbench.vcd"); \ + `endif \ + end \ + $dumpvars(0, testbench); \ + end \ + end \ `endif `endif // SWITCHBOARD_VH_ diff --git a/switchboard/verilog/sim/sb_axil_s.sv b/switchboard/verilog/sim/sb_axil_s.sv new file mode 100644 index 00000000..063a2886 --- /dev/null +++ b/switchboard/verilog/sim/sb_axil_s.sv @@ -0,0 +1,174 @@ +// Copyright (c) 2024 Zero ASIC Corporation +// This code is licensed under Apache License 2.0 (see LICENSE for details) + +`default_nettype none + +module sb_axil_s #( + // AXI settings + parameter DATA_WIDTH = 32, + parameter ADDR_WIDTH = 16, + parameter STRB_WIDTH = (DATA_WIDTH/8), + + // Switchboard settings + parameter integer VALID_MODE_DEFAULT=1, + parameter integer READY_MODE_DEFAULT=1, + parameter FILE="" +) ( + input wire clk, + + // AXI lite master interface + // adapted from https://github.com/alexforencich/verilog-axi + 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 wire s_axil_bvalid, + 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 wire [DATA_WIDTH-1:0] s_axil_rdata, + output wire [1:0] s_axil_rresp, + output wire s_axil_rvalid, + input wire s_axil_rready +); + // AW channel + + sb_to_queue_sim #( + .READY_MODE_DEFAULT(READY_MODE_DEFAULT), + .DW(ADDR_WIDTH + 3) + ) aw_channel ( + .clk(clk), + .data({s_axil_awprot, s_axil_awaddr}), + .dest(), + .last(), + .valid(s_axil_awvalid), + .ready(s_axil_awready) + ); + + // W channel + + sb_to_queue_sim #( + .READY_MODE_DEFAULT(READY_MODE_DEFAULT), + .DW(DATA_WIDTH + STRB_WIDTH) + ) w_channel ( + .clk(clk), + .data({s_axil_wstrb, s_axil_wdata}), + .dest(), + .last(), + .valid(s_axil_wvalid), + .ready(s_axil_wready) + ); + + // B channel + + queue_to_sb_sim #( + .VALID_MODE_DEFAULT(VALID_MODE_DEFAULT), + .DW(2) + ) b_channel ( + .clk(clk), + .data(s_axil_bresp), + .dest(), + .last(), + .valid(s_axil_bvalid), + .ready(s_axil_bready) + ); + + // AR channel + + sb_to_queue_sim #( + .READY_MODE_DEFAULT(READY_MODE_DEFAULT), + .DW(ADDR_WIDTH + 3) + ) ar_channel ( + .clk(clk), + .data({s_axil_arprot, s_axil_araddr}), + .dest(), + .last(), + .valid(s_axil_arvalid), + .ready(s_axil_arready) + ); + + // R channel + + queue_to_sb_sim #( + .VALID_MODE_DEFAULT(VALID_MODE_DEFAULT), + .DW(DATA_WIDTH + 2) + ) r_channel ( + .clk(clk), + .data({s_axil_rresp, s_axil_rdata}), + .dest(), + .last(), + .valid(s_axil_rvalid), + .ready(s_axil_rready) + ); + + // handle differences between simulators + + `ifdef __ICARUS__ + `define SB_START_FUNC task + `define SB_END_FUNC endtask + `else + `define SB_START_FUNC function void + `define SB_END_FUNC endfunction + `endif + + `SB_START_FUNC init(input string uri); + string s; + + /* verilator lint_off IGNOREDRETURN */ + $sformat(s, "%0s-aw.q", uri); + aw_channel.init(s); + + $sformat(s, "%0s-w.q", uri); + w_channel.init(s); + + $sformat(s, "%0s-b.q", uri); + b_channel.init(s); + + $sformat(s, "%0s-ar.q", uri); + ar_channel.init(s); + + $sformat(s, "%0s-r.q", uri); + r_channel.init(s); + /* verilator lint_on IGNOREDRETURN */ + `SB_END_FUNC + + `SB_START_FUNC set_valid_mode(input integer value); + /* verilator lint_off IGNOREDRETURN */ + b_channel.set_valid_mode(value); + r_channel.set_valid_mode(value); + /* verilator lint_on IGNOREDRETURN */ + `SB_END_FUNC + + `SB_START_FUNC set_ready_mode(input integer value); + /* verilator lint_off IGNOREDRETURN */ + aw_channel.set_ready_mode(value); + w_channel.set_ready_mode(value); + ar_channel.set_ready_mode(value); + /* verilator lint_on IGNOREDRETURN */ + `SB_END_FUNC + + // initialize + + initial begin + if (FILE != "") begin + /* verilator lint_off IGNOREDRETURN */ + init(FILE); + /* verilator lint_on IGNOREDRETURN */ + end + end + + // clean up macros + + `undef SB_START_FUNC + `undef SB_END_FUNC + +endmodule + +`default_nettype wire diff --git a/switchboard/verilog/sim/sb_clk_gen.sv b/switchboard/verilog/sim/sb_clk_gen.sv new file mode 100644 index 00000000..7eda5a16 --- /dev/null +++ b/switchboard/verilog/sim/sb_clk_gen.sv @@ -0,0 +1,89 @@ +// Module for generating a simulation clock, needed for non-Verilator simulations + +// Copyright (c) 2024 Zero ASIC Corporation +// This code is licensed under Apache License 2.0 (see LICENSE for details) + +module sb_clk_gen #( + parameter real DEFAULT_PERIOD = 10e-9, + parameter real DEFAULT_DUTY_CYCLE = 0.5, + parameter real DEFAULT_MAX_RATE = -1, + parameter real DEFAULT_START_DELAY = -1 +) ( + output wire clk +); + // configure timing + + `ifdef SB_XYCE + timeunit 1s; + timeprecision 1fs; + `define SB_DELAY(t) #(t) + `else + timeunit 1ns; + timeprecision 1ns; + `define SB_DELAY(t) #((t)*1e9) + `endif + + // import external functions + + `ifdef __ICARUS__ + `define SB_EXT_FUNC(x) $``x`` + `else + `define SB_EXT_FUNC(x) x + + import "DPI-C" function void pi_start_delay ( + input real value + ); + + import "DPI-C" function void pi_max_rate_tick ( + inout signed [63:0] t_us, + input signed [63:0] min_period_us + ); + `endif + + // read in command-line arguments + + real period = DEFAULT_PERIOD; + real duty_cycle = DEFAULT_DUTY_CYCLE; + real max_rate = DEFAULT_MAX_RATE; + real start_delay = DEFAULT_START_DELAY; + + reg signed [63:0] t_us = -(64'sd1); + reg signed [63:0] min_period_us = -(64'sd1); + + initial begin + void'($value$plusargs("period=%f", period)); + void'($value$plusargs("duty-cycle=%f", duty_cycle)); + void'($value$plusargs("start-delay=%f", start_delay)); + + void'($value$plusargs("max-rate=%f", max_rate)); + + if (max_rate > 0) begin + min_period_us = 1.0e6 / max_rate; // rounds according to LRM + end + end + + // main clock generation code + + reg clk_r; + assign clk = clk_r; + + initial begin + `SB_EXT_FUNC(pi_start_delay)(start_delay); + + forever begin + `SB_EXT_FUNC(pi_max_rate_tick)(t_us, min_period_us); + + clk_r = 1'b0; + `SB_DELAY((1.0 - duty_cycle) * period); + + clk_r = 1'b1; + `SB_DELAY(duty_cycle * period); + end + end + + // clean up macros + + `undef SB_EXT_FUNC + `undef SB_DELAY + +endmodule diff --git a/switchboard/vpi/switchboard_vpi.cc b/switchboard/vpi/switchboard_vpi.cc index c207599b..d60bd461 100644 --- a/switchboard/vpi/switchboard_vpi.cc +++ b/switchboard/vpi/switchboard_vpi.cc @@ -306,6 +306,102 @@ PLI_INT32 pi_time_taken(PLI_BYTE8* userdata) { return 0; } +PLI_INT32 pi_start_delay(PLI_BYTE8* userdata) { + (void)userdata; // unused + + // get arguments + vpiHandle args_iter; + std::vector argh; + { + vpiHandle systfref; + systfref = vpi_handle(vpiSysTfCall, NULL); + args_iter = vpi_iterate(vpiArgument, systfref); + for (size_t i = 0; i < 1; i++) { + argh.push_back(vpi_scan(args_iter)); + } + } + + // get value + double value; + { + t_vpi_value argval; + argval.format = vpiRealVal; + vpi_get_value(argh[0], &argval); + value = argval.value.real; + } + + // call the underlying switchboard function + start_delay(value); + + // clean up + vpi_free_object(args_iter); + + // return value unused? + return 0; +} + +PLI_INT32 pi_max_rate_tick(PLI_BYTE8* userdata) { + (void)userdata; // unused + + // get arguments + vpiHandle args_iter; + std::vector argh; + { + vpiHandle systfref; + systfref = vpi_handle(vpiSysTfCall, NULL); + args_iter = vpi_iterate(vpiArgument, systfref); + for (size_t i = 0; i < 2; i++) { + argh.push_back(vpi_scan(args_iter)); + } + } + + // get the timestamp + long t_us = 0; + { + t_vpi_value argval; + argval.format = vpiVectorVal; + vpi_get_value(argh[0], &argval); + + t_us |= argval.value.vector[1].aval & 0xffffffff; + t_us <<= 32; + t_us |= argval.value.vector[0].aval & 0xffffffff; + } + + // get max rate + double max_rate; + { + t_vpi_value argval; + argval.format = vpiRealVal; + vpi_get_value(argh[1], &argval); + max_rate = argval.value.real; + } + + // call the underlying switchboard function + max_rate_tick(t_us, max_rate); + + // set the timestamp + { + t_vpi_value argval; + argval.format = vpiVectorVal; + s_vpi_vecval vecval[2]; // two 32-bit words + argval.value.vector = vecval; + + argval.value.vector[0].aval = t_us & 0xffffffff; + argval.value.vector[0].bval = 0; + + argval.value.vector[1].aval = (t_us >> 32) & 0xffffffff; + argval.value.vector[1].bval = 0; + + vpi_put_value(argh[0], &argval, NULL, vpiNoDelay); + } + + // clean up + vpi_free_object(args_iter); + + // return value unused? + return 0; +} + // macro that creates a function to register PLI functions #define VPI_REGISTER_FUNC_NAME(name) register_##name @@ -323,10 +419,13 @@ VPI_REGISTER_FUNC(pi_sb_tx_init) VPI_REGISTER_FUNC(pi_sb_recv) VPI_REGISTER_FUNC(pi_sb_send) VPI_REGISTER_FUNC(pi_time_taken) +VPI_REGISTER_FUNC(pi_start_delay) +VPI_REGISTER_FUNC(pi_max_rate_tick) void (*vlog_startup_routines[])(void) = { VPI_REGISTER_FUNC_NAME(pi_sb_rx_init), VPI_REGISTER_FUNC_NAME(pi_sb_tx_init), VPI_REGISTER_FUNC_NAME(pi_sb_recv), VPI_REGISTER_FUNC_NAME(pi_sb_send), - VPI_REGISTER_FUNC_NAME(pi_time_taken), + VPI_REGISTER_FUNC_NAME(pi_time_taken), VPI_REGISTER_FUNC_NAME(pi_start_delay), + VPI_REGISTER_FUNC_NAME(pi_max_rate_tick), 0 // last entry must be 0 };