From b0dfa5a31d04b11dab310d410aba7c2da570b93c Mon Sep 17 00:00:00 2001 From: Steven Herbst Date: Wed, 29 May 2024 12:57:57 -0700 Subject: [PATCH] GPIO connections in single-netlist mode (#228) --- examples/umi_gpio/README.md | 2 +- examples/umi_gpio/funcs.v | 39 ++++++ examples/umi_gpio/test.py | 140 +++++++++++++++++--- examples/umi_gpio/testbench.sv | 61 --------- setup.py | 2 +- switchboard/__init__.py | 1 + switchboard/autowrap.py | 231 ++++++++++++++++++++++++++++++--- switchboard/bitvector.py | 53 ++++---- switchboard/network.py | 142 +++++++++++++++++--- 9 files changed, 525 insertions(+), 146 deletions(-) create mode 100644 examples/umi_gpio/funcs.v delete mode 100644 examples/umi_gpio/testbench.sv diff --git a/examples/umi_gpio/README.md b/examples/umi_gpio/README.md index 6a434684..4d080020 100644 --- a/examples/umi_gpio/README.md +++ b/examples/umi_gpio/README.md @@ -21,4 +21,4 @@ Here's what the interaction looks like from the Python perspective (with referen Under the hood, these reads and writes are implemented using UMI transactions. For example, if you write `gpio.o[7:0] = 42`, a UMI write request is sent to a switchboard connection, indicating that bits 7-0 should be written to the value `42`. -From the RTL simulation perspective, bit-level interaction happens through instances of the `umi_gpio` module, which is provided by the switchboard repository and automatically included when using `SbDut`. Connect bit-level signals to be read from Python to the `gpio_in` port of `umi_gpio`, and similarly connect bit-level signals to be written from Python to the `gpio_out` port of `umi_gpio`. The width of the `gpio_in` port is set by the `IWIDTH` parameter and the width of the `gpio_out` port is set by the `OWIDTH` parameter; the values set for those parameters in RTL should match the values provided to the `UmiTxRx.gpio(...)` method when constructing a `UmiGpio` object. +From the RTL simulation perspective, bit-level interaction happens through instances of the `umi_gpio` module, which is provided by the switchboard repository. Connect bit-level signals to be read from Python to the `gpio_in` port of `umi_gpio`, and similarly connect bit-level signals to be written from Python to the `gpio_out` port of `umi_gpio`. The width of the `gpio_in` port is set by the `IWIDTH` parameter and the width of the `gpio_out` port is set by the `OWIDTH` parameter; the values set for those parameters in RTL should match the values provided to the `UmiTxRx.gpio(...)` method when constructing a `UmiGpio` object. diff --git a/examples/umi_gpio/funcs.v b/examples/umi_gpio/funcs.v new file mode 100644 index 00000000..fe005543 --- /dev/null +++ b/examples/umi_gpio/funcs.v @@ -0,0 +1,39 @@ +// Copyright (c) 2024 Zero ASIC Corporation +// This code is licensed under Apache License 2.0 (see LICENSE for details) + +`default_nettype none + +module funcs ( + input [7:0] a, + input [7:0] b, + output [7:0] c, + output [7:0] d, + input [127:0] e, + output [127:0] f, + output [127:0] g, + input h, + input i, + output j, + output k, + output l, + input [7:0] m, + input [7:0] n, + output [7:0] o, + input p, + output q, + inout vdd, + inout vss +); + + assign c = a + 8'd12; + assign d = b - 8'd34; + assign f = e; + assign g = ~e; + assign j = h ^ i; + assign k = h & i; + assign o = m + n; + assign q = p; + +endmodule + +`default_nettype wire diff --git a/examples/umi_gpio/test.py b/examples/umi_gpio/test.py index edbd0f0c..37e4ed70 100755 --- a/examples/umi_gpio/test.py +++ b/examples/umi_gpio/test.py @@ -5,26 +5,53 @@ # Copyright (c) 2024 Zero ASIC Corporation # This code is licensed under Apache License 2.0 (see LICENSE for details) -import random -from switchboard import UmiTxRx, SbDut import umi +import random +from switchboard import SbNetwork, sb_path def main(): - # build the simulator - dut = build_testbench() - - # create queues - umi = UmiTxRx("to_rtl.q", "from_rtl.q", fresh=True) + owidth = 512 + iwidth = 512 + + net = SbNetwork(cmdline=True, single_netlist=True) + + umi_gpio = net.instantiate(make_umi_gpio(net, owidth=owidth, iwidth=iwidth)) + funcs = net.instantiate(make_funcs(net)) + + net.connect(umi_gpio.gpio_out[7:0], funcs.a) + net.connect(funcs.b, umi_gpio.gpio_out[15:8]) + net.connect(umi_gpio.gpio_in[7:0], funcs.c) + net.connect(funcs.d, umi_gpio.gpio_in[15:8]) + net.connect(umi_gpio.gpio_out[127:0], funcs.e) + net.connect(funcs.f, umi_gpio.gpio_in[255:128]) + net.connect(umi_gpio.gpio_in[383:256], funcs.g) + net.connect(umi_gpio.gpio_out[128], funcs.h) + net.connect(funcs.i, umi_gpio.gpio_out[129]) + net.connect(umi_gpio.gpio_in[384], funcs.j) + net.connect(funcs.k, umi_gpio.gpio_in[385]) + net.connect(funcs.m[1:0], 3) + net.connect(umi_gpio.gpio_out[132:129], funcs.m[5:2]) + net.connect(2, funcs.m[7:6]) + net.connect(funcs.n, 33) + net.connect(umi_gpio.gpio_in[393:386], funcs.o) + net.connect(funcs.p, 1) + net.connect(umi_gpio.gpio_in[394], funcs.q) + net.connect(funcs.vss, 0) + + net.external(umi_gpio.udev_req, txrx='udev') + net.external(umi_gpio.udev_resp, txrx='udev') # launch the simulation - dut.simulate() + net.build() + net.simulate() # instantiate TX and RX queues. note that these can be instantiated without # specifying a URI, in which case the URI can be specified later via the # "init" method - gpio = umi.gpio(owidth=128, iwidth=384, init=0xcafed00d) + umi = net.intfs['udev'] + gpio = umi.gpio(owidth=owidth, iwidth=iwidth, init=0xcafed00d) print(f'Initial value: 0x{gpio.o[:]:x}') assert gpio.o[:] == 0xcafed00d @@ -55,8 +82,8 @@ def main(): stimulus = random.randint(0, (1 << 128) - 1) - gpio.o[:] = stimulus - print(f'Wrote gpio.o[:] = 0x{gpio.o[:]:032x}') + gpio.o[127:0] = stimulus + print(f'Wrote gpio.o[127:0] = 0x{gpio.o[127:0]:032x}') c = gpio.i[255:128] print(f'Read gpio.i[255:128] = 0x{c:032x}') @@ -66,20 +93,95 @@ def main(): print(f'Read gpio.i[383:256] = 0x{d:032x}') assert d == (~stimulus) & ((1 << 128) - 1) - print('PASS!') + for h in [0, 1]: + for i in [0, 1]: + gpio.o[128] = h + gpio.o[129] = i + j = gpio.i[384] + k = gpio.i[385] -def build_testbench(): - dut = SbDut(cmdline=True) + print(f'Wrote gpio.o[128]={h}, gpio.o[129]={i}') + print(f'Read gpio.i[384]={j}, gpio.i[385]={k}') - dut.input('testbench.sv') + assert j == h ^ i + assert k == h & i - dut.use(umi) - dut.add('option', 'library', 'umi') + gpio.o[132:129] = 0b1010 + o = gpio.i[393:386] + print(f'Wrote gpio.o[132:129]=0b1010, read gpio.i[393:386]={o}') + assert o == 204 + + q = gpio.i[394] + print(f'Read gpio.i[394]={q}') + assert q == 1 + + print('PASS!') - dut.build() - return dut +def make_umi_gpio(net, owidth, iwidth): + dw = 256 + aw = 64 + cw = 32 + + parameters = dict( + DW=dw, + AW=aw, + CW=cw, + OWIDTH=owidth, + IWIDTH=iwidth + ) + + interfaces = { + 'udev_req': dict(type='umi', dw=dw, aw=aw, cw=cw, direction='input'), + 'udev_resp': dict(type='umi', dw=dw, aw=aw, cw=cw, direction='output'), + 'gpio_out': dict(type='gpio', direction='output', width=owidth), + 'gpio_in': dict(type='gpio', direction='input', width=iwidth) + } + + resets = ['nreset'] + + block = net.make_dut('umi_gpio', parameters=parameters, interfaces=interfaces, resets=resets) + + block.use(umi) + block.add('option', 'library', 'umi') + + block.input(sb_path() / 'verilog' / 'common' / 'umi_gpio.v') + + return block + + +def make_funcs(net): + interfaces = { + 'a': dict(type='gpio', direction='input', width=8), + 'b': dict(type='gpio', direction='input', width=8), + 'c': dict(type='gpio', direction='output', width=8), + 'd': dict(type='gpio', direction='output', width=8), + 'e': dict(type='gpio', direction='input', width=128), + 'f': dict(type='gpio', direction='output', width=128), + 'g': dict(type='gpio', direction='output', width=128), + 'h': dict(type='gpio', direction='input', width=1), + 'i': dict(type='gpio', direction='input'), + 'j': dict(type='gpio', direction='output'), + 'k': dict(type='gpio', direction='output', width=1), + 'l': dict(type='gpio', direction='output', width=8), # intentionally unused + 'm': dict(type='gpio', direction='input', width=8), + 'n': dict(type='gpio', direction='input', width=8), + 'o': dict(type='gpio', direction='output', width=8), + 'p': dict(type='gpio', direction='input'), + 'q': dict(type='gpio', direction='output'), + 'vss': dict(type='gpio', direction='inout') + } + + tieoffs = { + 'vdd': "1'b1" + } + + block = net.make_dut('funcs', interfaces=interfaces, tieoffs=tieoffs, clocks=[]) + + block.input('funcs.v') + + return block if __name__ == '__main__': diff --git a/examples/umi_gpio/testbench.sv b/examples/umi_gpio/testbench.sv deleted file mode 100644 index 0e8846f8..00000000 --- a/examples/umi_gpio/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; - localparam integer IWIDTH=384; - localparam integer OWIDTH=128; - - `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; - wire [(IWIDTH-1):0] gpio_in; - wire [(OWIDTH-1):0] gpio_out; - - umi_gpio #( - .DW(DW), - .AW(AW), - .CW(CW), - .IWIDTH(IWIDTH), - .OWIDTH(OWIDTH) - ) umi_gpio_i ( - .* - ); - - always @(posedge clk) begin - nreset <= 1'b1; - end - - // operations - - assign gpio_in[ 7:0] = gpio_out[ 7:0] + 8'd12; - assign gpio_in[15:8] = gpio_out[15:8] - 8'd34; - - assign gpio_in[255:128] = gpio_out[127:0]; - assign gpio_in[383:256] = ~gpio_out[127:0]; - - // Waveforms - - `SB_SETUP_PROBES - -endmodule - -`default_nettype wire diff --git a/setup.py b/setup.py index aee2952a..b17dacb2 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.2.3" +__version__ = "0.2.4" ################################################################################# # parse_reqs, long_desc from https://github.com/siliconcompiler/siliconcompiler # diff --git a/switchboard/__init__.py b/switchboard/__init__.py index 81ced4d8..78fb2d09 100644 --- a/switchboard/__init__.py +++ b/switchboard/__init__.py @@ -21,3 +21,4 @@ from .axil import AxiLiteTxRx from .axi import AxiTxRx from .network import SbNetwork +from .switchboard import path as sb_path diff --git a/switchboard/autowrap.py b/switchboard/autowrap.py index 88161827..8c005885 100644 --- a/switchboard/autowrap.py +++ b/switchboard/autowrap.py @@ -9,10 +9,71 @@ from .umi import UmiTxRx from .axi import AxiTxRx from .axil import AxiLiteTxRx +from .bitvector import slice_to_msb_lsb from _switchboard import PySbTx, PySbRx +class WireExpr: + def __init__(self, width): + self.width = width + self.bindings = [] + + def bind(self, slice, wire): + # extract msb, lsb + msb, lsb = slice_to_msb_lsb(start=slice.start, stop=slice.stop, step=slice.step) + + # make sure that the slice fits in the width + assert 0 <= lsb <= self.width - 1 + assert 0 <= msb <= self.width - 1 + + if len(self.bindings) == 0: + self.bindings.append(((msb, lsb), wire)) + return + + for idx in range(len(self.bindings) - 1, -1, -1): + (msb_i, lsb_i), _ = self.bindings[idx] + if lsb < lsb_i: + assert msb < lsb_i, \ + f'bit assignments {msb_i}:{lsb_i} and {msb}:{lsb} overlap' + self.bindings.insert(idx + 1, ((msb, lsb), wire)) + break + else: + (msb_i, lsb_i), _ = self.bindings[0] + assert lsb > msb_i, \ + f'bit assignments {msb_i}:{lsb_i} and {msb}:{lsb} overlap' + self.bindings.insert(0, ((msb, lsb), wire)) + + def padded(self): + retval = [] + + for idx, ((msb, lsb), wire) in enumerate(self.bindings): + if idx == 0: + if msb != self.width - 1: + msb_pad = (self.width - 1) - msb + retval.append(f"{msb_pad}'b0") + + retval.append(wire) + + if idx < len(self.bindings) - 1: + lsb_pad = (lsb - 1) - self.bindings[idx + 1][0][0] + else: + lsb_pad = lsb + + if lsb_pad > 0: + retval.append(f"{lsb_pad}'b0") + + return retval + + def __str__(self): + padded = self.padded() + + if len(padded) == 1: + return padded[0] + else: + return '{' + ', '.join(padded) + '}' + + def normalize_interface(name, value): # copy before modifying value = deepcopy(value) @@ -64,6 +125,9 @@ def normalize_interface(name, value): if 'idw' not in value: value['idw'] = 8 + elif type == 'gpio': + if 'width' not in value: + value['width'] = 1 else: raise ValueError(f'Unsupported interface type: "{type}"') @@ -154,7 +218,17 @@ def normalize_resets(resets): def normalize_tieoff(key, value): - # placeholder for doing more interesting things in the future + if isinstance(value, dict): + value = deepcopy(value) + else: + value = {'value': value} + + if 'width' not in value: + value['width'] = 1 + + if 'wire' not in value: + value['wire'] = None + return key, value @@ -235,7 +309,56 @@ def autowrap( lines += [''] + # declare all GPIO output wires (makes things easier when an output is + # sent to multiple places or slices of it are used) + + wires['gpio'] = set() + + for instance in instances: + for name, value in interfaces[instance].items(): + type = value['type'] + direction = value['direction'] + + if not ((type == 'gpio') and (direction == 'output')): + continue + + wire = value['wire'] + + if wire is None: + # means that the output is unused + continue + + assert wire not in wires['gpio'] + + width = value['width'] + + lines += [tab + f'wire [{width-1}:0] {wire};'] + + wires['gpio'].add(wire) + + lines += [''] + for instance in instances: + # declare wires for tieoffs + + for key, value in tieoffs[instance].items(): + if value['value'] is None: + continue + + if value['wire'] is None: + value['wire'] = f'{instance}_tieoff_{key}' + + width = value['width'] + + lines += [ + tab + f'wire [{width-1}:0] {value["wire"]};', + tab + f'assign {value["wire"]} = {value["value"]};' + ] + + lines += [''] + + # declare wires for interfaces + for name, value in interfaces[instance].items(): type = value['type'] @@ -244,7 +367,7 @@ def autowrap( wire = value['wire'] - if wire not in wires[type]: + if (type != 'gpio') and (wire not in wires[type]): decl_wire = True wires[type].add(wire) else: @@ -311,6 +434,17 @@ def autowrap( lines += [tab + f'`SB_AXIL_S({wire}, {dw}, {aw}, "");'] else: raise Exception(f'Unsupported AXI-Lite direction: {direction}') + elif type == 'gpio': + if direction == 'input': + width = value['width'] + new_wire = f'{instance}_input_{name}' + lines += [ + tab + f'wire [{width-1}:0] {new_wire};', + tab + f'assign {new_wire} = {wire};' + ] + value['wire'] = new_wire + else: + pass else: raise Exception(f'Unsupported interface type: "{type}"') @@ -328,9 +462,6 @@ def autowrap( max_rst_dly = inst_max_rst_dly if max_rst_dly is not None: - max_rst_dly = max(max(reset['delay'] for reset in inst_resets) - for inst_resets in resets.values()) - lines += [ tab + f"reg [{max_rst_dly}:0] rstvec = '1;" '', @@ -379,6 +510,12 @@ def autowrap( connections += [f'`SB_AXI_CONNECT({name}, {wire})'] elif type_is_axil(type): connections += [f'`SB_AXIL_CONNECT({name}, {wire})'] + elif type_is_gpio(type): + if wire is None: + # unused output + connections += [f'.{name}()'] + else: + connections += [f'.{name}({wire})'] # clocks @@ -404,11 +541,12 @@ def autowrap( # tieoffs for key, value in tieoffs[instance].items(): - if value is None: - value = '' - else: - value = str(value) - connections += [f'.{key}({value})'] + wire = value.get('wire') + + if wire is None: + wire = '' + + connections += [f'.{key}({wire})'] for n, connection in enumerate(connections): if n != len(connections) - 1: @@ -478,6 +616,10 @@ def direction_is_output(direction): return direction.lower() in ['o', 'out', 'output'] +def direction_is_inout(direction): + return direction.lower() in ['inout'] + + def direction_is_manager(direction): return direction.lower() in ['m', 'manager', 'master', 'indicator'] @@ -487,11 +629,18 @@ def direction_is_subordinate(direction): def normalize_direction(type, direction): - if type_is_sb(type) or type_is_umi(type): + if type_is_const(type): + if direction_is_output(direction): + return 'output' + else: + raise Exception(f'Unsupported direction for interface type "{type}": "{direction}"') + elif type_is_sb(type) or type_is_umi(type) or type_is_gpio(type): if direction_is_input(direction): return 'input' elif direction_is_output(direction): return 'output' + elif direction_is_inout(direction): + return 'inout' else: raise Exception(f'Unsupported direction for interface type "{type}": "{direction}"') elif type_is_axi(type) or type_is_axil(type): @@ -505,18 +654,34 @@ def normalize_direction(type, direction): raise Exception(f'Unsupported interface type: "{type}"') -def directions_are_compatible(type, a, b): - a = normalize_direction(type, a) - b = normalize_direction(type, b) +def directions_are_compatible(type_a, a, type_b, b): + a = normalize_direction(type_a, a) + b = normalize_direction(type_b, b) + + if a == 'input': + return b in ['output', 'inout'] + elif a == 'output': + return b in ['input', 'inout'] + elif a == 'inout': + return b in ['input', 'output', 'inout'] + elif a == 'manager': + return b == 'subordinate' + elif a == 'subordinate': + return b == 'manager' + else: + raise Exception(f'Cannot determine if directions are compatible: {a} and {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'))) + +def types_are_compatible(a, b): + a = normalize_intf_type(a) + b = normalize_intf_type(b) + + if type_is_const(a): + return type_is_gpio(b) + elif type_is_const(b): + return type_is_gpio(a) else: - raise Exception(f'Unsupported interface type: "{type}"') + return a == b def polarity_is_positive(polarity): @@ -552,6 +717,22 @@ def type_is_axil(type): return type.lower() in ['axil'] +def type_is_input(type): + return type.lower() in ['i', 'in', 'input'] + + +def type_is_output(type): + return type.lower() in ['o', 'out', 'output'] + + +def type_is_gpio(type): + return type.lower() in ['gpio'] + + +def type_is_const(type): + return type.lower() in ['const', 'constant'] + + def normalize_intf_type(type): if type_is_sb(type): return 'sb' @@ -561,6 +742,14 @@ def normalize_intf_type(type): return 'axi' elif type_is_axil(type): return 'axil' + elif type_is_input(type): + return 'input' + elif type_is_output(type): + return 'output' + elif type_is_gpio(type): + return 'gpio' + elif type_is_const(type): + return 'const' else: raise ValueError(f'Unsupported interface type: "{type}"') diff --git a/switchboard/bitvector.py b/switchboard/bitvector.py index 709815db..b638d73c 100644 --- a/switchboard/bitvector.py +++ b/switchboard/bitvector.py @@ -22,9 +22,9 @@ def __setitem__(self, key, value): self.value = value return else: - msb, lsb = self.slice_to_msb_lsb(key.start, key.stop, key.step) + msb, lsb = slice_to_msb_lsb(key.start, key.stop, key.step) else: - msb, lsb = self.slice_to_msb_lsb(key, key) + msb, lsb = slice_to_msb_lsb(key, key) # generate mask with the right width mask = (1 << (msb - lsb + 1)) - 1 @@ -45,9 +45,9 @@ def __getitem__(self, key): if (key.start is None) and (key.stop is None) and (key.step is None): return self.value else: - msb, lsb = self.slice_to_msb_lsb(key.start, key.stop, key.step) + msb, lsb = slice_to_msb_lsb(key.start, key.stop, key.step) else: - msb, lsb = self.slice_to_msb_lsb(key, key) + msb, lsb = slice_to_msb_lsb(key, key) # generate mask with the right width mask = (1 << (msb - lsb + 1)) - 1 @@ -55,28 +55,6 @@ def __getitem__(self, key): # extract the value return (self.value >> lsb) & mask - def slice_to_msb_lsb(self, start=None, stop=None, step=None): - # set defaults - if start is None: - start = 0 - if stop is None: - stop = 0 - if step is None: - step = 1 - - if step != 1: - raise ValueError('Only step=1 allowed for slice indexing.') - - msb = start - lsb = stop - - if msb < lsb: - raise ValueError('MSB must be greater than or equal to LSB') - if lsb < 0: - raise ValueError('Negative LSB is not allowed.') - - return msb, lsb - def tobytes(self, n=None): # convert to a numpy byte array. if "n" is provided, # pad result to be "n" bytes. will error out if "n" @@ -109,3 +87,26 @@ def frombytes(arr): value |= (int(elem) & 0xff) << (i * 8) return BitVector(value) + + +def slice_to_msb_lsb(start=None, stop=None, step=None): + # set defaults + if start is None: + start = 0 + if stop is None: + stop = 0 + if step is None: + step = 1 + + if step != 1: + raise ValueError('Only step=1 allowed for slice indexing.') + + msb = start + lsb = stop + + if msb < lsb: + raise ValueError('MSB must be greater than or equal to LSB') + if lsb < 0: + raise ValueError('Negative LSB is not allowed.') + + return msb, lsb diff --git a/switchboard/network.py b/switchboard/network.py index 209cb99e..8a7ac548 100644 --- a/switchboard/network.py +++ b/switchboard/network.py @@ -1,25 +1,91 @@ # 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 itertools import count +from numbers import Integral 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, - autowrap) + autowrap, normalize_direction, WireExpr, types_are_compatible) from .cmdline import get_cmdline_args from _switchboard import delete_queues class SbIntf: - def __init__(self, inst, name): + def __init__(self, inst, name, width=None, indices=None): self.inst = inst self.name = name + self.width = width + + if (indices is None) and (width is not None): + indices = slice(width - 1, 0, 1) + + self.slice = indices + + @property + def intf_def(self): + return self.inst.block.intf_defs[self.name] + + @property + def wire_name(self): + return f'{self.inst.name}_{self.name}' + + def __getitem__(self, key): + if not isinstance(key, slice): + key = slice(key, key) + + return SbIntf(inst=self.inst, name=self.name, indices=key) + + def slice_as_str(self): + if self.slice is None: + return '' + else: + return f'[{self.slice.start}:{self.slice.stop}]' + + def compute_slice_width(self): + if self.slice.start is not None: + start = self.slice.start + else: + start = self.width - 1 + + if self.slice.stop is not None: + stop = self.slice.stop + else: + stop = 0 + + return start - stop + 1 + + +class ConstIntf: + def __init__(self, value): + self.value = value + + @property + def intf_def(self): + return dict( + type='const', + direction='output' + ) + + def value_as_str(self, width=None, format='decimal'): + if width is None: + width = '' + else: + width = str(width) + + if format.lower() == 'decimal': + return f"{width}'d{self.value}" + elif format.lower() == 'hex': + return f"{width}'h{hex(self.value)[2:]}" + elif format.lower() == 'hex': + return f"{width}'b{bin(self.value)[2:]}" + else: + raise Exception(f'Unsupported format: {format}') class SbInst: @@ -31,7 +97,8 @@ def __init__(self, name, block): for name, value in block.intf_defs.items(): self.mapping[name] = dict(uri=None, wire=None) - self.__setattr__(name, SbIntf(inst=self, name=name)) + width = block.intf_defs[name].get('width', None) + self.__setattr__(name, SbIntf(inst=self, name=name, width=width)) class SbNetwork: @@ -147,41 +214,82 @@ def instantiate(self, block, name: str = None): return self.insts[name] def connect(self, a, b, uri=None, wire=None): + # convert integer inputs into constant datatype + if isinstance(a, Integral): + a = ConstIntf(value=a) + if isinstance(b, Integral): + b = ConstIntf(value=b) + # 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] + intf_def_a = a.intf_def + intf_def_b = b.intf_def # 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 + assert types_are_compatible(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']) + direction_a = normalize_direction(type_a, intf_def_a['direction']) + direction_b = normalize_direction(type_b, intf_def_b['direction']) + assert directions_are_compatible( + type_a=type_a, a=direction_a, + type_b=type_b, b=direction_b + ) + + # indicate which is input vs. output. we have to look at both type a and + # type b since one may be a constant + if (type_a == 'gpio') or (type_b == 'gpio'): + if (direction_a == 'input') or (direction_b == 'output'): + input, output = a, b + direction_a, direction_b = 'input', 'output' + elif (direction_b == 'input') or (direction_a == 'output'): + input, output = b, a + direction_b, direction_a = 'input', 'output' + else: + raise Exception(f'Cannot infer connection direction with direction_a={direction_a}' + f' and direction_b={direction_b}') + + intf_def_a['direction'] = direction_a + intf_def_b['direction'] = direction_b # determine what the queue will be called that connects the two if wire is None: - wire = f'{a.inst.name}_{a.name}_conn_{b.inst.name}_{b.name}' + if (type_a != 'gpio') and (type_b != 'gpio'): + wire = f'{a.wire_name}_conn_{b.wire_name}' + elif not isinstance(output, ConstIntf): + wire = f'{output.inst.name}_{output.name}' - if uri is None: + if (uri is None) and (wire is not None): uri = wire if type_is_sb(type_a) or type_is_umi(type_a): uri = uri + '.q' - if not self.single_netlist: - # internal connection, no need to register it for cleanup + if (not self.single_netlist) and (type_a != 'gpio') and (type_b != 'gpio'): self.register_uri(type=type_a, uri=uri) # tell both instances what they are connected to - a.inst.mapping[a.name]['wire'] = wire - b.inst.mapping[b.name]['wire'] = wire + if (type_a != 'gpio') and (type_b != 'gpio'): + a.inst.mapping[a.name]['wire'] = wire + b.inst.mapping[b.name]['wire'] = wire - a.inst.mapping[a.name]['uri'] = uri - b.inst.mapping[b.name]['uri'] = uri + a.inst.mapping[a.name]['uri'] = uri + b.inst.mapping[b.name]['uri'] = uri + else: + if input.inst.mapping[input.name]['wire'] is None: + expr = WireExpr(input.intf_def['width']) + input.inst.mapping[input.name]['wire'] = expr + + if isinstance(output, ConstIntf): + input.inst.mapping[input.name]['wire'].bind( + input.slice, output.value_as_str(width=input.compute_slice_width())) + else: + input.inst.mapping[input.name]['wire'].bind( + input.slice, f'{wire}{output.slice_as_str()}') + output.inst.mapping[output.name]['wire'] = wire def build(self): unique_blocks = set(inst.block for inst in self.insts.values())