From fccb1d5dc0e39c1d84b401e59bd083ca8f4229ee Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Wed, 9 Jun 2021 11:00:15 -0700 Subject: [PATCH 1/6] Ability to import specific module from Verilog/model from BLIF without making its io the block's IO --- pyrtl/importexport.py | 90 ++++++++++++++++----- tests/test_importexport.py | 158 +++++++++++++++++++++++++++++++++++++ 2 files changed, 230 insertions(+), 18 deletions(-) diff --git a/pyrtl/importexport.py b/pyrtl/importexport.py index 9ada8f1c..3d1f40a7 100644 --- a/pyrtl/importexport.py +++ b/pyrtl/importexport.py @@ -95,7 +95,8 @@ def twire(self, x): return s -def input_from_blif(blif, block=None, merge_io_vectors=True, clock_name='clk', top_model=None): +def input_from_blif(blif, block=None, merge_io_vectors=True, clock_name='clk', + top_model=None, as_block=True): """ Read an open BLIF file or string as input, updating the block appropriately. :param blif: An open BLIF file to read @@ -104,8 +105,14 @@ def input_from_blif(blif, block=None, merge_io_vectors=True, clock_name='clk', t by a indexing subscript (e.g. 1-bit wires 'a[0]' and 'a[1]') will be combined into a single Input/Output (e.g. a 2-bit wire 'a'). :param clock_name: The name of the clock (defaults to 'clk') - :param top_model: name of top-level model to instantiate; if None, defaults to first model + :param top_model: Name of top-level model to instantiate; if None, defaults to first model listed in the BLIF + :param as_block: if True, the model's io wires will be PyRTL Input/Output WireVectors + on the block; otherwise, they will be normal WireVectors accessible in the returned + Model object. + + :return: Either a Block, if as_block is True, or a Model object where each + io wire is accessible as a field by that wire's original name in the model. If merge_io_vectors is True, then given 1-bit Input wires 'a[0]' and 'a[1]', these wires will be combined into a single 2-bit Input wire 'a' that can be accessed @@ -190,7 +197,16 @@ def SLiteral(x): # Begin actually reading and parsing the BLIF file result = parser.parseString(blif_string, parseAll=True) ff_clk_set = set([]) - models = {} # model name -> model, for subckt instantiation + model_refs = {} # model name -> model, for subckt instantiation + toplevel_io_map = {} # original_name -> wire + + def toplevel_io(typ, bitwidth, name): + if as_block: + wire = typ(bitwidth=bitwidth, name=name, block=block) + else: + wire = WireVector(bitwidth=bitwidth, block=block) + toplevel_io_map[name] = wire + return wire def extract_inputs(subckt): if subckt.is_top: @@ -204,11 +220,11 @@ def extract_inputs(subckt): if input_name in subckt.clk_set: continue elif bitwidth == 1: - wire_in = Input(bitwidth=1, name=input_name, block=block) + wire_in = toplevel_io(Input, 1, input_name) subckt.add_input(input_name, wire_in) block.add_wirevector(wire_in) elif merge_io_vectors: - wire_in = Input(bitwidth=bitwidth, name=input_name, block=block) + wire_in = toplevel_io(Input, bitwidth, input_name) for i in range(bitwidth): bit_name = input_name + '[' + str(i) + ']' bit_wire = WireVector(bitwidth=1, block=block) @@ -217,7 +233,7 @@ def extract_inputs(subckt): else: for i in range(bitwidth): bit_name = input_name + '[' + str(i) + ']' - wire_in = Input(bitwidth=1, name=bit_name, block=block) + wire_in = toplevel_io(Input, 1, bit_name) subckt.add_input(bit_name, wire_in) block.add_wirevector(wire_in) else: @@ -249,12 +265,13 @@ def extract_outputs(subckt): # the only thing that changes is what underlying wire is used. if bitwidth == 1: bit_internal = WireVector(bitwidth=1, block=block) - bit_out = Output(bitwidth=1, name=output_name, block=block) + bit_out = toplevel_io(Output, 1, output_name) bit_out <<= bit_internal # NOTE this is important: redirecting user-visible name to internal wire subckt.add_output(output_name, bit_internal) elif merge_io_vectors: - wire_out = Output(bitwidth=bitwidth, name=output_name, block=block) + wire_out = toplevel_io(Output, bitwidth, output_name) + bit_list = [] for i in range(bitwidth): bit_name = output_name + '[' + str(i) + ']' @@ -266,7 +283,7 @@ def extract_outputs(subckt): for i in range(bitwidth): bit_name = output_name + '[' + str(i) + ']' bit_internal = WireVector(bitwidth=1, block=block) - bit_out = Output(bitwidth=1, name=bit_name, block=block) + bit_out = toplevel_io(Output, 1, bit_name) bit_out <<= bit_internal # NOTE this is important: redirecting user-visible name to internal wire subckt.add_output(bit_name, bit_internal) @@ -390,7 +407,7 @@ def get_formal_connected_to_parent_clocks(): return clks formal_clks = get_formal_connected_to_parent_clocks() - subckt = Subcircuit(models[command['model_name']], clk_set=formal_clks, block=block) + subckt = Subcircuit(model_refs[command['model_name']], clk_set=formal_clks, block=block) instantiate(subckt) for fa in command['formal_actual_list']: formal = fa['formal'] @@ -420,11 +437,27 @@ def instantiate(subckt): for model in result: if not top_model: top_model = model['model_name'] - models[model['model_name']] = model + model_refs[model['model_name']] = model - top = Subcircuit(models[top_model], is_top=True, clk_set={clock_name}, block=block) + if top_model not in model_refs.keys(): + raise PyrtlError("Model named '%s' not found in the BLIF." % top_model) + + top = Subcircuit(model_refs[top_model], is_top=True, clk_set={clock_name}, block=block) instantiate(top) + if as_block: + return block + else: + class Model: + # Goes beyond a namedtuple, so you can now use <<= on the members, + # which is needed for connecting to input wires. + def __init__(self, name, io): + self.name = name + for wire_name, wire in io.items(): + setattr(self, wire_name, wire) + + return Model(top_model, toplevel_io_map) + # ---------------------------------------------------------------- # ___ __ __ __ @@ -433,13 +466,19 @@ def instantiate(subckt): # -def input_from_verilog(verilog, clock_name='clk', toplevel=None, leave_in_dir=None, block=None): +def input_from_verilog(verilog, clock_name='clk', toplevel=None, parameters={}, as_block=True, + leave_in_dir=None, block=None): """ Read an open Verilog file or string as input via Yosys conversion, updating the block. :param verilog: An open Verilog file to read :param clock_name: The name of the clock (defaults to 'clk') - :param toplevel: Name of top-level module to instantiate; if None, defaults to first model - defined in the Verilog file + :param toplevel: Name of top-level module to instantiate; if None, will be determined + automatically by Yosys. Must supply parameters if trying to instantiate a + parameterized module. + :param parameters: Dictionary mapping parameter names to values for the given toplevel module + :param as_block: if True, the module's io wires will be PyRTL Input/Output WireVectors + on the block; otherwise, they will be normal WireVectors accessible in the returned + Model object. :param bool leave_in_dir: If True, save the intermediate BLIF file created in the given directory :param block: The block where the logic will be added @@ -484,13 +523,26 @@ def input_from_verilog(verilog, clock_name='clk', toplevel=None, leave_in_dir=No yosys_arg_template = ( "-p " "read_verilog %s; " + "%s" "synth %s; " "setundef -zero -undriven; " "opt; " "write_blif %s; " ) + parameters_str = "" + if parameters: + if toplevel is None: + raise PyrtlError("Must supply top-level module name if parameters are given.") + + parameters_str = "chparam " + parameters_str += \ + " ".join("-set " + str(k) + " " + str(v) for k, v + in parameters.items()) + parameters_str += " " + toplevel + "; " + yosys_arg = yosys_arg_template % (tmp_verilog_path, + parameters_str, ('-top ' + toplevel) if toplevel is not None else '-auto-top', tmp_blif_path) @@ -499,17 +551,19 @@ def input_from_verilog(verilog, clock_name='clk', toplevel=None, leave_in_dir=No os.close(temp_bd) # call yosys on the script, and grab the output _yosys_output = subprocess.check_output(['yosys', yosys_arg]) - with open(tmp_blif_path) as blif: - input_from_blif(blif, block=block, clock_name=clock_name, top_model=toplevel) except (subprocess.CalledProcessError, ValueError) as e: print('Error with call to yosys...', file=sys.stderr) print('---------------------------------------------', file=sys.stderr) - print(str(e.output).replace('\\n', '\n'), file=sys.stderr) + print(str(e).replace('\\n', '\n'), file=sys.stderr) print('---------------------------------------------', file=sys.stderr) raise PyrtlError('Yosys callfailed') except OSError as e: print('Error with call to yosys...', file=sys.stderr) raise PyrtlError('Call to yosys failed (not installed or on path?)') + else: + with open(tmp_blif_path) as blif: + return input_from_blif(blif, block=block, merge_io_vectors=True, + clock_name=clock_name, top_model=toplevel, as_block=as_block) finally: os.remove(tmp_verilog_path) if leave_in_dir is None: diff --git a/tests/test_importexport.py b/tests/test_importexport.py index cc859225..9b792049 100644 --- a/tests/test_importexport.py +++ b/tests/test_importexport.py @@ -311,6 +311,57 @@ .end """ # noqa +multimodel_blif = """\ +# Generated by Yosys 0.9+2406 (git sha1 aee43936, clang 11.0.3 -fPIC -Os) + +.model C +.inputs +.outputs out +.names $false +.names $true +1 +.names $undef +.subckt A x[0]=$false x[1]=$false x[2]=$false x[3]=$false x[4]=$false x[5]=$false x[6]=$false y=$false z[0]=w1 z[1]=$auto$hierarchy.cc:1250:execute$5[0] z[2]=$auto$hierarchy.cc:1250:execute$5[1] z[3]=$auto$hierarchy.cc:1250:execute$5[2] z[4]=$auto$hierarchy.cc:1250:execute$5[3] +.subckt B i[0]=w1 i[1]=$false i[2]=$false i[3]=$false i[4]=$false i[5]=$false i[6]=$false i[7]=$false i[8]=$false j[0]=out j[1]=$auto$hierarchy.cc:1250:execute$4 +.names $false r1 +1 1 +.names $false r2 +1 1 +.end + +.model A +.inputs x[0] x[1] x[2] x[3] x[4] x[5] x[6] y +.outputs z[0] z[1] z[2] z[3] z[4] +.names $false +.names $true +1 +.names $undef +.names y x[0] z[0] +11 1 +.names $false z[1] +1 1 +.names $false z[2] +1 1 +.names $false z[3] +1 1 +.names $false z[4] +1 1 +.end + +.model B +.inputs i[0] i[1] i[2] i[3] i[4] i[5] i[6] i[7] i[8] +.outputs j[0] j[1] +.names $false +.names $true +1 +.names $undef +.names i[0] j[1] +0 1 +.names i[0] j[0] +1 1 +.end +""" # noqa + class TestInputFromBlif(unittest.TestCase): def setUp(self): @@ -563,6 +614,31 @@ def test_blif_with_multiple_modules_unmerged_io(self): self.assertEqual(sim.inspect('s[3]'), (res & 0x8) >> 3) self.assertEqual(sim.inspect('cout'), (res & 0x10) >> 4) + def test_blif_import_single_submodule_not_as_block(self): + C = pyrtl.input_from_blif(multimodel_blif, top_model="C", as_block=False) + self.assertEqual(C.out.bitwidth, 1) + self.assertNotIsInstance(C.out, pyrtl.Output) + + A = pyrtl.input_from_blif(multimodel_blif, top_model="A", as_block=False) + self.assertEqual(A.x.bitwidth, 7) + self.assertEqual(A.y.bitwidth, 1) + self.assertEqual(A.z.bitwidth, 5) + self.assertNotIsInstance(A.x, pyrtl.Input) + self.assertNotIsInstance(A.y, pyrtl.Input) + self.assertNotIsInstance(A.z, pyrtl.Output) + + # Verify we can connect things + in1 = pyrtl.Input(2, 'in1') + A.x <<= in1 + A.y <<= in1[0] + out = pyrtl.Output(6, 'out') + out <<= C.out + A.z + + sim = pyrtl.Simulation() + sim.step_multiple({ + 'in1': [0, 1, 2, 3], + }) + def test_blif_with_clock_passing(self): pyrtl.input_from_blif(clock_passing_blif) a, b, c = [ @@ -1645,6 +1721,34 @@ def test_existing_reset_wire_without_add_reset(self): endmodule """ +verilog_input_multi_module_parameterized = """\ +module A(x, y, z); + parameter in_size = 4; + parameter out_size = 3; + input [in_size-1:0] x; + input y; + output [out_size-1:0] z; + + assign z = x[2:0] & y; +endmodule + +module B(i, j); + parameter in_size = 9; + input [in_size-1:0] i; + output [1:0] j; + + assign j = {~i[0], i[0]}; +endmodule + +module C(out); + output out; + reg r1, r2; + wire w1, w2; + A a(r1, r2, w1); + B b(w1, out); +endmodule +""" + class TestVerilogInput(unittest.TestCase): def setUp(self): @@ -1686,6 +1790,60 @@ def test_import_multi_module_auto_select_top_module(self): sim.step_multiple(nsteps=5) self.assertEqual(sim.tracer.trace['o'], [0, 2, 0, 2, 0]) + def test_import_module_with_parameters(self): + pyrtl.input_from_verilog(verilog_input_multi_module_parameterized, toplevel='A', + parameters={'in_size': 8, 'out_size': 5}) + block = pyrtl.working_block() + self.assertEqual(block.get_wirevector_by_name('x').bitwidth, 8) + self.assertEqual(block.get_wirevector_by_name('z').bitwidth, 5) + self.assertIsInstance(block.get_wirevector_by_name('x'), pyrtl.Input) + self.assertIsInstance(block.get_wirevector_by_name('z'), pyrtl.Output) + sim = pyrtl.Simulation() + x_inputs = [8, 3, 7, 34] + y_inputs = [1, 1, 1, 0] + sim.step_multiple({'x': x_inputs, 'y': y_inputs}) + self.assertEqual(sim.tracer.trace['z'], + [x & 7 & y for x, y in zip(x_inputs, y_inputs)]) + + def test_import_module_with_parameters_not_as_block(self): + B = pyrtl.input_from_verilog(verilog_input_multi_module_parameterized, toplevel='B', + parameters={'in_size': 4}, as_block=False) + block = pyrtl.working_block() + self.assertIsNone(block.get_wirevector_by_name('i'), 4) + self.assertIsNone(block.get_wirevector_by_name('j'), 2) + self.assertEqual(B.i.bitwidth, 4) + self.assertEqual(B.j.bitwidth, 2) + self.assertNotIsInstance(B.i, pyrtl.Input) + self.assertNotIsInstance(B.j, pyrtl.Output) + i_in = pyrtl.Input(4, 'i_in') + j_out = pyrtl.Output(2, 'j_out') + B.i <<= i_in + j_out <<= B.j + sim = pyrtl.Simulation() + sim.step_multiple({'i_in': [0b1001, 0b0100]}) + self.assertEqual(sim.tracer.trace['j_out'], [0b01, 0b10]) + + def test_import_module_multiple_times(self): + foo1 = pyrtl.input_from_verilog(verilog_input_multi_module, toplevel='foo', as_block=False) + foo2 = pyrtl.input_from_verilog(verilog_input_multi_module, toplevel='foo', as_block=False) + block = pyrtl.working_block() + in1, in2, in3 = pyrtl.input_list('in1/1 in2/1 in3/1') + foo1.a <<= in1 + foo1.b <<= in2 + foo2.a <<= foo1.o + foo2.b <<= in3 + pyrtl.probe(foo2.o, 'out') + sim = pyrtl.Simulation() + sim.step_multiple({ + 'in1': [0, 0, 0, 0, 1, 1, 1, 1], + 'in2': [0, 0, 1, 1, 0, 0, 1, 1], + 'in3': [0, 1, 0, 1, 0, 1, 0, 1], + }) + self.assertEqual( + sim.tracer.trace['out'], + [0, 1, 1, 2, 1, 2, 0, 1] + ) + def test_error_import_bad_file(self): with self.assertRaisesRegex(pyrtl.PyrtlError, "input_from_verilog expecting either open file or string"): From e3cbf0c397f87cd91fd8a14a18384add5ba90e4d Mon Sep 17 00:00:00 2001 From: Luis Vega Date: Fri, 23 Jul 2021 10:07:20 -0700 Subject: [PATCH 2/6] improve rom verilog output --- pyrtl/importexport.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pyrtl/importexport.py b/pyrtl/importexport.py index 9ada8f1c..ea01dcc5 100644 --- a/pyrtl/importexport.py +++ b/pyrtl/importexport.py @@ -758,16 +758,18 @@ def _to_verilog_memories(file, block, varname): memories = {n.op_param[1] for n in block.logic_subset('m@')} for m in sorted(memories, key=lambda m: m.id): print(' // Memory mem_{}: {}'.format(m.id, m.name), file=file) - print(' always @(posedge clk)', file=file) - print(' begin', file=file) - for net in _net_sorted(block.logic_subset('@'), varname): - if net.op_param[1] == m: - t = (varname(net.args[2]), net.op_param[0], - varname(net.args[0]), varname(net.args[1])) - print((' if (%s) begin\n' - ' mem_%s[%s] <= %s;\n' - ' end') % t, file=file) - print(' end', file=file) + writes = _net_sorted(block.logic_subset('@'), varname) + if writes: + print(' always @(posedge clk)', file=file) + print(' begin', file=file) + for net in writes: + if net.op_param[1] == m: + t = (varname(net.args[2]), net.op_param[0], + varname(net.args[0]), varname(net.args[1])) + print((' if (%s) begin\n' + ' mem_%s[%s] <= %s;\n' + ' end') % t, file=file) + print(' end', file=file) for net in _net_sorted(block.logic_subset('m'), varname): if net.op_param[1] == m: dest = varname(net.dests[0]) From 041397677aaa76f4ab69c4da991b4c2ec1ef9e37 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Wed, 4 Aug 2021 01:25:30 -0700 Subject: [PATCH 3/6] Improve Verilog output when a memory doesn't have any writes --- pyrtl/importexport.py | 27 ++++++++-------- tests/test_importexport.py | 65 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 13 deletions(-) diff --git a/pyrtl/importexport.py b/pyrtl/importexport.py index ea01dcc5..f9a24ccb 100644 --- a/pyrtl/importexport.py +++ b/pyrtl/importexport.py @@ -758,24 +758,25 @@ def _to_verilog_memories(file, block, varname): memories = {n.op_param[1] for n in block.logic_subset('m@')} for m in sorted(memories, key=lambda m: m.id): print(' // Memory mem_{}: {}'.format(m.id, m.name), file=file) - writes = _net_sorted(block.logic_subset('@'), varname) + writes = [net for net in _net_sorted(block.logic_subset('@'), varname) + if net.op_param[1] == m] if writes: print(' always @(posedge clk)', file=file) print(' begin', file=file) for net in writes: - if net.op_param[1] == m: - t = (varname(net.args[2]), net.op_param[0], - varname(net.args[0]), varname(net.args[1])) - print((' if (%s) begin\n' - ' mem_%s[%s] <= %s;\n' - ' end') % t, file=file) + t = (varname(net.args[2]), net.op_param[0], + varname(net.args[0]), varname(net.args[1])) + print((' if (%s) begin\n' + ' mem_%s[%s] <= %s;\n' + ' end') % t, file=file) print(' end', file=file) - for net in _net_sorted(block.logic_subset('m'), varname): - if net.op_param[1] == m: - dest = varname(net.dests[0]) - m_id = net.op_param[0] - index = varname(net.args[0]) - print(' assign {:s} = mem_{}[{:s}];'.format(dest, m_id, index), file=file) + reads = [net for net in _net_sorted(block.logic_subset('m'), varname) + if net.op_param[1] == m] + for net in reads: + dest = varname(net.dests[0]) + m_id = net.op_param[0] + index = varname(net.args[0]) + print(' assign {:s} = mem_{}[{:s}];'.format(dest, m_id, index), file=file) print('', file=file) diff --git a/tests/test_importexport.py b/tests/test_importexport.py index cc859225..258b3090 100644 --- a/tests/test_importexport.py +++ b/tests/test_importexport.py @@ -1384,6 +1384,56 @@ def test_blif_nor_gate_correct(self): """ +verilog_output_mems_with_no_writes = """\ +// Generated automatically via PyRTL +// As one initial test of synthesis, map to FPGA with: +// yosys -p "synth_xilinx -top toplevel" thisfile.v + +module toplevel(clk, rst, in1, out1); + input clk; + input rst; + input[2:0] in1; + output[7:0] out1; + + reg[7:0] mem_0[7:0]; //tmp0 + reg[7:0] mem_1[255:0]; //tmp1 + + wire const_0_1; + wire[7:0] const_1_42; + wire[7:0] tmp2; + + initial begin + mem_0[0]=8'ha; + mem_0[1]=8'h14; + mem_0[2]=8'h1e; + mem_0[3]=8'h28; + mem_0[4]=8'h32; + mem_0[5]=8'h3c; + mem_0[6]=8'h0; + mem_0[7]=8'h0; + end + + // Combinational + assign const_0_1 = 1; + assign const_1_42 = 42; + assign out1 = tmp2; + + // Memory mem_0: tmp0 + assign tmp2 = mem_0[in1]; + + // Memory mem_1: tmp1 + always @(posedge clk) + begin + if (const_0_1) begin + mem_1[tmp2] <= const_1_42; + end + end + +endmodule + +""" + + verilog_output_counter_no_reset = """\ // Generated automatically via PyRTL // As one initial test of synthesis, map to FPGA with: @@ -1568,6 +1618,21 @@ def test_textual_consistency_large(self): self.assertEqual(buffer.getvalue(), verilog_output_large) + def test_mems_with_no_writes(self): + rdata = {0: 10, 1: 20, 2: 30, 3: 40, 4: 50, 5: 60} + rom = pyrtl.RomBlock(8, 3, rdata, pad_with_zeros=True) + mem = pyrtl.MemBlock(8, 8) + in1 = pyrtl.Input(3, 'in1') + out1 = pyrtl.Output(8, 'out1') + w = rom[in1] + out1 <<= w + mem[w] <<= 42 + + buffer = io.StringIO() + pyrtl.output_to_verilog(buffer) + + self.assertEqual(buffer.getvalue(), verilog_output_mems_with_no_writes) + def check_counter_text(self, add_reset, expected): r = pyrtl.Register(4, reset_value=2) r.next <<= r + 1 From 298fb4b6d43e6606839edeea3f9f99be9d1db72d Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Wed, 9 Jun 2021 11:00:15 -0700 Subject: [PATCH 4/6] Ability to import specific module from Verilog/model from BLIF without making its io the block's IO --- pyrtl/importexport.py | 90 ++++++++++++++++----- tests/test_importexport.py | 158 +++++++++++++++++++++++++++++++++++++ 2 files changed, 230 insertions(+), 18 deletions(-) diff --git a/pyrtl/importexport.py b/pyrtl/importexport.py index f9a24ccb..dcb84647 100644 --- a/pyrtl/importexport.py +++ b/pyrtl/importexport.py @@ -95,7 +95,8 @@ def twire(self, x): return s -def input_from_blif(blif, block=None, merge_io_vectors=True, clock_name='clk', top_model=None): +def input_from_blif(blif, block=None, merge_io_vectors=True, clock_name='clk', + top_model=None, as_block=True): """ Read an open BLIF file or string as input, updating the block appropriately. :param blif: An open BLIF file to read @@ -104,8 +105,14 @@ def input_from_blif(blif, block=None, merge_io_vectors=True, clock_name='clk', t by a indexing subscript (e.g. 1-bit wires 'a[0]' and 'a[1]') will be combined into a single Input/Output (e.g. a 2-bit wire 'a'). :param clock_name: The name of the clock (defaults to 'clk') - :param top_model: name of top-level model to instantiate; if None, defaults to first model + :param top_model: Name of top-level model to instantiate; if None, defaults to first model listed in the BLIF + :param as_block: if True, the model's io wires will be PyRTL Input/Output WireVectors + on the block; otherwise, they will be normal WireVectors accessible in the returned + Model object. + + :return: Either a Block, if as_block is True, or a Model object where each + io wire is accessible as a field by that wire's original name in the model. If merge_io_vectors is True, then given 1-bit Input wires 'a[0]' and 'a[1]', these wires will be combined into a single 2-bit Input wire 'a' that can be accessed @@ -190,7 +197,16 @@ def SLiteral(x): # Begin actually reading and parsing the BLIF file result = parser.parseString(blif_string, parseAll=True) ff_clk_set = set([]) - models = {} # model name -> model, for subckt instantiation + model_refs = {} # model name -> model, for subckt instantiation + toplevel_io_map = {} # original_name -> wire + + def toplevel_io(typ, bitwidth, name): + if as_block: + wire = typ(bitwidth=bitwidth, name=name, block=block) + else: + wire = WireVector(bitwidth=bitwidth, block=block) + toplevel_io_map[name] = wire + return wire def extract_inputs(subckt): if subckt.is_top: @@ -204,11 +220,11 @@ def extract_inputs(subckt): if input_name in subckt.clk_set: continue elif bitwidth == 1: - wire_in = Input(bitwidth=1, name=input_name, block=block) + wire_in = toplevel_io(Input, 1, input_name) subckt.add_input(input_name, wire_in) block.add_wirevector(wire_in) elif merge_io_vectors: - wire_in = Input(bitwidth=bitwidth, name=input_name, block=block) + wire_in = toplevel_io(Input, bitwidth, input_name) for i in range(bitwidth): bit_name = input_name + '[' + str(i) + ']' bit_wire = WireVector(bitwidth=1, block=block) @@ -217,7 +233,7 @@ def extract_inputs(subckt): else: for i in range(bitwidth): bit_name = input_name + '[' + str(i) + ']' - wire_in = Input(bitwidth=1, name=bit_name, block=block) + wire_in = toplevel_io(Input, 1, bit_name) subckt.add_input(bit_name, wire_in) block.add_wirevector(wire_in) else: @@ -249,12 +265,13 @@ def extract_outputs(subckt): # the only thing that changes is what underlying wire is used. if bitwidth == 1: bit_internal = WireVector(bitwidth=1, block=block) - bit_out = Output(bitwidth=1, name=output_name, block=block) + bit_out = toplevel_io(Output, 1, output_name) bit_out <<= bit_internal # NOTE this is important: redirecting user-visible name to internal wire subckt.add_output(output_name, bit_internal) elif merge_io_vectors: - wire_out = Output(bitwidth=bitwidth, name=output_name, block=block) + wire_out = toplevel_io(Output, bitwidth, output_name) + bit_list = [] for i in range(bitwidth): bit_name = output_name + '[' + str(i) + ']' @@ -266,7 +283,7 @@ def extract_outputs(subckt): for i in range(bitwidth): bit_name = output_name + '[' + str(i) + ']' bit_internal = WireVector(bitwidth=1, block=block) - bit_out = Output(bitwidth=1, name=bit_name, block=block) + bit_out = toplevel_io(Output, 1, bit_name) bit_out <<= bit_internal # NOTE this is important: redirecting user-visible name to internal wire subckt.add_output(bit_name, bit_internal) @@ -390,7 +407,7 @@ def get_formal_connected_to_parent_clocks(): return clks formal_clks = get_formal_connected_to_parent_clocks() - subckt = Subcircuit(models[command['model_name']], clk_set=formal_clks, block=block) + subckt = Subcircuit(model_refs[command['model_name']], clk_set=formal_clks, block=block) instantiate(subckt) for fa in command['formal_actual_list']: formal = fa['formal'] @@ -420,11 +437,27 @@ def instantiate(subckt): for model in result: if not top_model: top_model = model['model_name'] - models[model['model_name']] = model + model_refs[model['model_name']] = model - top = Subcircuit(models[top_model], is_top=True, clk_set={clock_name}, block=block) + if top_model not in model_refs.keys(): + raise PyrtlError("Model named '%s' not found in the BLIF." % top_model) + + top = Subcircuit(model_refs[top_model], is_top=True, clk_set={clock_name}, block=block) instantiate(top) + if as_block: + return block + else: + class Model: + # Goes beyond a namedtuple, so you can now use <<= on the members, + # which is needed for connecting to input wires. + def __init__(self, name, io): + self.name = name + for wire_name, wire in io.items(): + setattr(self, wire_name, wire) + + return Model(top_model, toplevel_io_map) + # ---------------------------------------------------------------- # ___ __ __ __ @@ -433,13 +466,19 @@ def instantiate(subckt): # -def input_from_verilog(verilog, clock_name='clk', toplevel=None, leave_in_dir=None, block=None): +def input_from_verilog(verilog, clock_name='clk', toplevel=None, parameters={}, as_block=True, + leave_in_dir=None, block=None): """ Read an open Verilog file or string as input via Yosys conversion, updating the block. :param verilog: An open Verilog file to read :param clock_name: The name of the clock (defaults to 'clk') - :param toplevel: Name of top-level module to instantiate; if None, defaults to first model - defined in the Verilog file + :param toplevel: Name of top-level module to instantiate; if None, will be determined + automatically by Yosys. Must supply parameters if trying to instantiate a + parameterized module. + :param parameters: Dictionary mapping parameter names to values for the given toplevel module + :param as_block: if True, the module's io wires will be PyRTL Input/Output WireVectors + on the block; otherwise, they will be normal WireVectors accessible in the returned + Model object. :param bool leave_in_dir: If True, save the intermediate BLIF file created in the given directory :param block: The block where the logic will be added @@ -484,13 +523,26 @@ def input_from_verilog(verilog, clock_name='clk', toplevel=None, leave_in_dir=No yosys_arg_template = ( "-p " "read_verilog %s; " + "%s" "synth %s; " "setundef -zero -undriven; " "opt; " "write_blif %s; " ) + parameters_str = "" + if parameters: + if toplevel is None: + raise PyrtlError("Must supply top-level module name if parameters are given.") + + parameters_str = "chparam " + parameters_str += \ + " ".join("-set " + str(k) + " " + str(v) for k, v + in parameters.items()) + parameters_str += " " + toplevel + "; " + yosys_arg = yosys_arg_template % (tmp_verilog_path, + parameters_str, ('-top ' + toplevel) if toplevel is not None else '-auto-top', tmp_blif_path) @@ -499,17 +551,19 @@ def input_from_verilog(verilog, clock_name='clk', toplevel=None, leave_in_dir=No os.close(temp_bd) # call yosys on the script, and grab the output _yosys_output = subprocess.check_output(['yosys', yosys_arg]) - with open(tmp_blif_path) as blif: - input_from_blif(blif, block=block, clock_name=clock_name, top_model=toplevel) except (subprocess.CalledProcessError, ValueError) as e: print('Error with call to yosys...', file=sys.stderr) print('---------------------------------------------', file=sys.stderr) - print(str(e.output).replace('\\n', '\n'), file=sys.stderr) + print(str(e).replace('\\n', '\n'), file=sys.stderr) print('---------------------------------------------', file=sys.stderr) raise PyrtlError('Yosys callfailed') except OSError as e: print('Error with call to yosys...', file=sys.stderr) raise PyrtlError('Call to yosys failed (not installed or on path?)') + else: + with open(tmp_blif_path) as blif: + return input_from_blif(blif, block=block, merge_io_vectors=True, + clock_name=clock_name, top_model=toplevel, as_block=as_block) finally: os.remove(tmp_verilog_path) if leave_in_dir is None: diff --git a/tests/test_importexport.py b/tests/test_importexport.py index 258b3090..bde6dd1a 100644 --- a/tests/test_importexport.py +++ b/tests/test_importexport.py @@ -311,6 +311,57 @@ .end """ # noqa +multimodel_blif = """\ +# Generated by Yosys 0.9+2406 (git sha1 aee43936, clang 11.0.3 -fPIC -Os) + +.model C +.inputs +.outputs out +.names $false +.names $true +1 +.names $undef +.subckt A x[0]=$false x[1]=$false x[2]=$false x[3]=$false x[4]=$false x[5]=$false x[6]=$false y=$false z[0]=w1 z[1]=$auto$hierarchy.cc:1250:execute$5[0] z[2]=$auto$hierarchy.cc:1250:execute$5[1] z[3]=$auto$hierarchy.cc:1250:execute$5[2] z[4]=$auto$hierarchy.cc:1250:execute$5[3] +.subckt B i[0]=w1 i[1]=$false i[2]=$false i[3]=$false i[4]=$false i[5]=$false i[6]=$false i[7]=$false i[8]=$false j[0]=out j[1]=$auto$hierarchy.cc:1250:execute$4 +.names $false r1 +1 1 +.names $false r2 +1 1 +.end + +.model A +.inputs x[0] x[1] x[2] x[3] x[4] x[5] x[6] y +.outputs z[0] z[1] z[2] z[3] z[4] +.names $false +.names $true +1 +.names $undef +.names y x[0] z[0] +11 1 +.names $false z[1] +1 1 +.names $false z[2] +1 1 +.names $false z[3] +1 1 +.names $false z[4] +1 1 +.end + +.model B +.inputs i[0] i[1] i[2] i[3] i[4] i[5] i[6] i[7] i[8] +.outputs j[0] j[1] +.names $false +.names $true +1 +.names $undef +.names i[0] j[1] +0 1 +.names i[0] j[0] +1 1 +.end +""" # noqa + class TestInputFromBlif(unittest.TestCase): def setUp(self): @@ -563,6 +614,31 @@ def test_blif_with_multiple_modules_unmerged_io(self): self.assertEqual(sim.inspect('s[3]'), (res & 0x8) >> 3) self.assertEqual(sim.inspect('cout'), (res & 0x10) >> 4) + def test_blif_import_single_submodule_not_as_block(self): + C = pyrtl.input_from_blif(multimodel_blif, top_model="C", as_block=False) + self.assertEqual(C.out.bitwidth, 1) + self.assertNotIsInstance(C.out, pyrtl.Output) + + A = pyrtl.input_from_blif(multimodel_blif, top_model="A", as_block=False) + self.assertEqual(A.x.bitwidth, 7) + self.assertEqual(A.y.bitwidth, 1) + self.assertEqual(A.z.bitwidth, 5) + self.assertNotIsInstance(A.x, pyrtl.Input) + self.assertNotIsInstance(A.y, pyrtl.Input) + self.assertNotIsInstance(A.z, pyrtl.Output) + + # Verify we can connect things + in1 = pyrtl.Input(2, 'in1') + A.x <<= in1 + A.y <<= in1[0] + out = pyrtl.Output(6, 'out') + out <<= C.out + A.z + + sim = pyrtl.Simulation() + sim.step_multiple({ + 'in1': [0, 1, 2, 3], + }) + def test_blif_with_clock_passing(self): pyrtl.input_from_blif(clock_passing_blif) a, b, c = [ @@ -1710,6 +1786,34 @@ def test_existing_reset_wire_without_add_reset(self): endmodule """ +verilog_input_multi_module_parameterized = """\ +module A(x, y, z); + parameter in_size = 4; + parameter out_size = 3; + input [in_size-1:0] x; + input y; + output [out_size-1:0] z; + + assign z = x[2:0] & y; +endmodule + +module B(i, j); + parameter in_size = 9; + input [in_size-1:0] i; + output [1:0] j; + + assign j = {~i[0], i[0]}; +endmodule + +module C(out); + output out; + reg r1, r2; + wire w1, w2; + A a(r1, r2, w1); + B b(w1, out); +endmodule +""" + class TestVerilogInput(unittest.TestCase): def setUp(self): @@ -1751,6 +1855,60 @@ def test_import_multi_module_auto_select_top_module(self): sim.step_multiple(nsteps=5) self.assertEqual(sim.tracer.trace['o'], [0, 2, 0, 2, 0]) + def test_import_module_with_parameters(self): + pyrtl.input_from_verilog(verilog_input_multi_module_parameterized, toplevel='A', + parameters={'in_size': 8, 'out_size': 5}) + block = pyrtl.working_block() + self.assertEqual(block.get_wirevector_by_name('x').bitwidth, 8) + self.assertEqual(block.get_wirevector_by_name('z').bitwidth, 5) + self.assertIsInstance(block.get_wirevector_by_name('x'), pyrtl.Input) + self.assertIsInstance(block.get_wirevector_by_name('z'), pyrtl.Output) + sim = pyrtl.Simulation() + x_inputs = [8, 3, 7, 34] + y_inputs = [1, 1, 1, 0] + sim.step_multiple({'x': x_inputs, 'y': y_inputs}) + self.assertEqual(sim.tracer.trace['z'], + [x & 7 & y for x, y in zip(x_inputs, y_inputs)]) + + def test_import_module_with_parameters_not_as_block(self): + B = pyrtl.input_from_verilog(verilog_input_multi_module_parameterized, toplevel='B', + parameters={'in_size': 4}, as_block=False) + block = pyrtl.working_block() + self.assertIsNone(block.get_wirevector_by_name('i'), 4) + self.assertIsNone(block.get_wirevector_by_name('j'), 2) + self.assertEqual(B.i.bitwidth, 4) + self.assertEqual(B.j.bitwidth, 2) + self.assertNotIsInstance(B.i, pyrtl.Input) + self.assertNotIsInstance(B.j, pyrtl.Output) + i_in = pyrtl.Input(4, 'i_in') + j_out = pyrtl.Output(2, 'j_out') + B.i <<= i_in + j_out <<= B.j + sim = pyrtl.Simulation() + sim.step_multiple({'i_in': [0b1001, 0b0100]}) + self.assertEqual(sim.tracer.trace['j_out'], [0b01, 0b10]) + + def test_import_module_multiple_times(self): + foo1 = pyrtl.input_from_verilog(verilog_input_multi_module, toplevel='foo', as_block=False) + foo2 = pyrtl.input_from_verilog(verilog_input_multi_module, toplevel='foo', as_block=False) + block = pyrtl.working_block() + in1, in2, in3 = pyrtl.input_list('in1/1 in2/1 in3/1') + foo1.a <<= in1 + foo1.b <<= in2 + foo2.a <<= foo1.o + foo2.b <<= in3 + pyrtl.probe(foo2.o, 'out') + sim = pyrtl.Simulation() + sim.step_multiple({ + 'in1': [0, 0, 0, 0, 1, 1, 1, 1], + 'in2': [0, 0, 1, 1, 0, 0, 1, 1], + 'in3': [0, 1, 0, 1, 0, 1, 0, 1], + }) + self.assertEqual( + sim.tracer.trace['out'], + [0, 1, 1, 2, 1, 2, 0, 1] + ) + def test_error_import_bad_file(self): with self.assertRaisesRegex(pyrtl.PyrtlError, "input_from_verilog expecting either open file or string"): From 750cc707d62c71945e2ba3a5db461d3b6f42d1a2 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Wed, 4 Aug 2021 12:14:32 -0700 Subject: [PATCH 5/6] Don't generate nands on BLIF import --- pyrtl/importexport.py | 10 +++++++--- tests/test_importexport.py | 8 ++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/pyrtl/importexport.py b/pyrtl/importexport.py index f9a24ccb..909d5190 100644 --- a/pyrtl/importexport.py +++ b/pyrtl/importexport.py @@ -325,8 +325,11 @@ def twire(w): output_wire = twire(netio[2]) output_wire <<= twire(netio[0]) | twire(netio[1]) # or gate elif command['cover_list'].asList() == ['0-', '1', '-0', '1']: + # nand is not really a PyRTL primitive and so should only be added to a netlist + # via a call to nand_synth(). We instead convert it to ~(a & b) rather than + # (~a | ~b) as would be generated if handled by the else case below. output_wire = twire(netio[2]) - output_wire <<= twire(netio[0]).nand(twire(netio[1])) # nand gate + output_wire <<= ~(twire(netio[0]) & twire(netio[1])) # nand gate -> not+and gates elif command['cover_list'].asList() == ['10', '1', '01', '1']: output_wire = twire(netio[2]) output_wire <<= twire(netio[0]) ^ twire(netio[1]) # xor gate @@ -334,8 +337,9 @@ def twire(w): # Although the following is fully generic and thus encompasses all of the # special cases after the simple wire case above, we leave the above in because # they are commonly found and lead to a slightly cleaner (though equivalent) netlist, - # because we can use nand/xor primitives, or avoid the extra fluff of concat/select - # wires that might be created implicitly as part of rtl_all/rtl_any. + # because we can use the xor primitive/save a gate when converting the nand, or avoid + # the extra fluff of concat/select wires that might be created implicitly as part of + # rtl_all/rtl_any. def convert_val(ix, val): wire = twire(netio[ix]) if val == '0': diff --git a/tests/test_importexport.py b/tests/test_importexport.py index 258b3090..33e73c75 100644 --- a/tests/test_importexport.py +++ b/tests/test_importexport.py @@ -663,7 +663,9 @@ def test_blif_or_gate_correct(self): }) self.assertEqual(sim.tracer.trace['o'], [0, 1, 1, 1]) - def test_blif_nand_gate_correct(self): + def test_blif_nand_gate_to_primitives_correct(self): + # This tests that there should be no NAND gates generated during BLIF import; + # they should be converted to AND+NOT. blif = """\ .model Top .inputs a b @@ -675,7 +677,9 @@ def test_blif_nand_gate_correct(self): """ pyrtl.input_from_blif(blif) block = pyrtl.working_block() - self.assertEqual(len(block.logic_subset('n')), 1) + self.assertEqual(len(block.logic_subset('n')), 0) + self.assertEqual(len(block.logic_subset('&')), 1) + self.assertEqual(len(block.logic_subset('~')), 1) sim = pyrtl.Simulation() sim.step_multiple({ 'a': '0011', From 5d628c69206286c9c6f77d6bed6595b4b2d2e996 Mon Sep 17 00:00:00 2001 From: Michael Christensen Date: Wed, 9 Jun 2021 11:00:15 -0700 Subject: [PATCH 6/6] Ability to import specific module from Verilog/model from BLIF without making its io the block's IO --- pyrtl/importexport.py | 90 ++++++++++++++++----- tests/test_importexport.py | 158 +++++++++++++++++++++++++++++++++++++ 2 files changed, 230 insertions(+), 18 deletions(-) diff --git a/pyrtl/importexport.py b/pyrtl/importexport.py index 909d5190..f31aa94e 100644 --- a/pyrtl/importexport.py +++ b/pyrtl/importexport.py @@ -95,7 +95,8 @@ def twire(self, x): return s -def input_from_blif(blif, block=None, merge_io_vectors=True, clock_name='clk', top_model=None): +def input_from_blif(blif, block=None, merge_io_vectors=True, clock_name='clk', + top_model=None, as_block=True): """ Read an open BLIF file or string as input, updating the block appropriately. :param blif: An open BLIF file to read @@ -104,8 +105,14 @@ def input_from_blif(blif, block=None, merge_io_vectors=True, clock_name='clk', t by a indexing subscript (e.g. 1-bit wires 'a[0]' and 'a[1]') will be combined into a single Input/Output (e.g. a 2-bit wire 'a'). :param clock_name: The name of the clock (defaults to 'clk') - :param top_model: name of top-level model to instantiate; if None, defaults to first model + :param top_model: Name of top-level model to instantiate; if None, defaults to first model listed in the BLIF + :param as_block: if True, the model's io wires will be PyRTL Input/Output WireVectors + on the block; otherwise, they will be normal WireVectors accessible in the returned + Model object. + + :return: Either a Block, if as_block is True, or a Model object where each + io wire is accessible as a field by that wire's original name in the model. If merge_io_vectors is True, then given 1-bit Input wires 'a[0]' and 'a[1]', these wires will be combined into a single 2-bit Input wire 'a' that can be accessed @@ -190,7 +197,16 @@ def SLiteral(x): # Begin actually reading and parsing the BLIF file result = parser.parseString(blif_string, parseAll=True) ff_clk_set = set([]) - models = {} # model name -> model, for subckt instantiation + model_refs = {} # model name -> model, for subckt instantiation + toplevel_io_map = {} # original_name -> wire + + def toplevel_io(typ, bitwidth, name): + if as_block: + wire = typ(bitwidth=bitwidth, name=name, block=block) + else: + wire = WireVector(bitwidth=bitwidth, block=block) + toplevel_io_map[name] = wire + return wire def extract_inputs(subckt): if subckt.is_top: @@ -204,11 +220,11 @@ def extract_inputs(subckt): if input_name in subckt.clk_set: continue elif bitwidth == 1: - wire_in = Input(bitwidth=1, name=input_name, block=block) + wire_in = toplevel_io(Input, 1, input_name) subckt.add_input(input_name, wire_in) block.add_wirevector(wire_in) elif merge_io_vectors: - wire_in = Input(bitwidth=bitwidth, name=input_name, block=block) + wire_in = toplevel_io(Input, bitwidth, input_name) for i in range(bitwidth): bit_name = input_name + '[' + str(i) + ']' bit_wire = WireVector(bitwidth=1, block=block) @@ -217,7 +233,7 @@ def extract_inputs(subckt): else: for i in range(bitwidth): bit_name = input_name + '[' + str(i) + ']' - wire_in = Input(bitwidth=1, name=bit_name, block=block) + wire_in = toplevel_io(Input, 1, bit_name) subckt.add_input(bit_name, wire_in) block.add_wirevector(wire_in) else: @@ -249,12 +265,13 @@ def extract_outputs(subckt): # the only thing that changes is what underlying wire is used. if bitwidth == 1: bit_internal = WireVector(bitwidth=1, block=block) - bit_out = Output(bitwidth=1, name=output_name, block=block) + bit_out = toplevel_io(Output, 1, output_name) bit_out <<= bit_internal # NOTE this is important: redirecting user-visible name to internal wire subckt.add_output(output_name, bit_internal) elif merge_io_vectors: - wire_out = Output(bitwidth=bitwidth, name=output_name, block=block) + wire_out = toplevel_io(Output, bitwidth, output_name) + bit_list = [] for i in range(bitwidth): bit_name = output_name + '[' + str(i) + ']' @@ -266,7 +283,7 @@ def extract_outputs(subckt): for i in range(bitwidth): bit_name = output_name + '[' + str(i) + ']' bit_internal = WireVector(bitwidth=1, block=block) - bit_out = Output(bitwidth=1, name=bit_name, block=block) + bit_out = toplevel_io(Output, 1, bit_name) bit_out <<= bit_internal # NOTE this is important: redirecting user-visible name to internal wire subckt.add_output(bit_name, bit_internal) @@ -394,7 +411,7 @@ def get_formal_connected_to_parent_clocks(): return clks formal_clks = get_formal_connected_to_parent_clocks() - subckt = Subcircuit(models[command['model_name']], clk_set=formal_clks, block=block) + subckt = Subcircuit(model_refs[command['model_name']], clk_set=formal_clks, block=block) instantiate(subckt) for fa in command['formal_actual_list']: formal = fa['formal'] @@ -424,11 +441,27 @@ def instantiate(subckt): for model in result: if not top_model: top_model = model['model_name'] - models[model['model_name']] = model + model_refs[model['model_name']] = model - top = Subcircuit(models[top_model], is_top=True, clk_set={clock_name}, block=block) + if top_model not in model_refs.keys(): + raise PyrtlError("Model named '%s' not found in the BLIF." % top_model) + + top = Subcircuit(model_refs[top_model], is_top=True, clk_set={clock_name}, block=block) instantiate(top) + if as_block: + return block + else: + class Model: + # Goes beyond a namedtuple, so you can now use <<= on the members, + # which is needed for connecting to input wires. + def __init__(self, name, io): + self.name = name + for wire_name, wire in io.items(): + setattr(self, wire_name, wire) + + return Model(top_model, toplevel_io_map) + # ---------------------------------------------------------------- # ___ __ __ __ @@ -437,13 +470,19 @@ def instantiate(subckt): # -def input_from_verilog(verilog, clock_name='clk', toplevel=None, leave_in_dir=None, block=None): +def input_from_verilog(verilog, clock_name='clk', toplevel=None, parameters={}, as_block=True, + leave_in_dir=None, block=None): """ Read an open Verilog file or string as input via Yosys conversion, updating the block. :param verilog: An open Verilog file to read :param clock_name: The name of the clock (defaults to 'clk') - :param toplevel: Name of top-level module to instantiate; if None, defaults to first model - defined in the Verilog file + :param toplevel: Name of top-level module to instantiate; if None, will be determined + automatically by Yosys. Must supply parameters if trying to instantiate a + parameterized module. + :param parameters: Dictionary mapping parameter names to values for the given toplevel module + :param as_block: if True, the module's io wires will be PyRTL Input/Output WireVectors + on the block; otherwise, they will be normal WireVectors accessible in the returned + Model object. :param bool leave_in_dir: If True, save the intermediate BLIF file created in the given directory :param block: The block where the logic will be added @@ -488,13 +527,26 @@ def input_from_verilog(verilog, clock_name='clk', toplevel=None, leave_in_dir=No yosys_arg_template = ( "-p " "read_verilog %s; " + "%s" "synth %s; " "setundef -zero -undriven; " "opt; " "write_blif %s; " ) + parameters_str = "" + if parameters: + if toplevel is None: + raise PyrtlError("Must supply top-level module name if parameters are given.") + + parameters_str = "chparam " + parameters_str += \ + " ".join("-set " + str(k) + " " + str(v) for k, v + in parameters.items()) + parameters_str += " " + toplevel + "; " + yosys_arg = yosys_arg_template % (tmp_verilog_path, + parameters_str, ('-top ' + toplevel) if toplevel is not None else '-auto-top', tmp_blif_path) @@ -503,17 +555,19 @@ def input_from_verilog(verilog, clock_name='clk', toplevel=None, leave_in_dir=No os.close(temp_bd) # call yosys on the script, and grab the output _yosys_output = subprocess.check_output(['yosys', yosys_arg]) - with open(tmp_blif_path) as blif: - input_from_blif(blif, block=block, clock_name=clock_name, top_model=toplevel) except (subprocess.CalledProcessError, ValueError) as e: print('Error with call to yosys...', file=sys.stderr) print('---------------------------------------------', file=sys.stderr) - print(str(e.output).replace('\\n', '\n'), file=sys.stderr) + print(str(e).replace('\\n', '\n'), file=sys.stderr) print('---------------------------------------------', file=sys.stderr) raise PyrtlError('Yosys callfailed') except OSError as e: print('Error with call to yosys...', file=sys.stderr) raise PyrtlError('Call to yosys failed (not installed or on path?)') + else: + with open(tmp_blif_path) as blif: + return input_from_blif(blif, block=block, merge_io_vectors=True, + clock_name=clock_name, top_model=toplevel, as_block=as_block) finally: os.remove(tmp_verilog_path) if leave_in_dir is None: diff --git a/tests/test_importexport.py b/tests/test_importexport.py index 33e73c75..e4579a13 100644 --- a/tests/test_importexport.py +++ b/tests/test_importexport.py @@ -311,6 +311,57 @@ .end """ # noqa +multimodel_blif = """\ +# Generated by Yosys 0.9+2406 (git sha1 aee43936, clang 11.0.3 -fPIC -Os) + +.model C +.inputs +.outputs out +.names $false +.names $true +1 +.names $undef +.subckt A x[0]=$false x[1]=$false x[2]=$false x[3]=$false x[4]=$false x[5]=$false x[6]=$false y=$false z[0]=w1 z[1]=$auto$hierarchy.cc:1250:execute$5[0] z[2]=$auto$hierarchy.cc:1250:execute$5[1] z[3]=$auto$hierarchy.cc:1250:execute$5[2] z[4]=$auto$hierarchy.cc:1250:execute$5[3] +.subckt B i[0]=w1 i[1]=$false i[2]=$false i[3]=$false i[4]=$false i[5]=$false i[6]=$false i[7]=$false i[8]=$false j[0]=out j[1]=$auto$hierarchy.cc:1250:execute$4 +.names $false r1 +1 1 +.names $false r2 +1 1 +.end + +.model A +.inputs x[0] x[1] x[2] x[3] x[4] x[5] x[6] y +.outputs z[0] z[1] z[2] z[3] z[4] +.names $false +.names $true +1 +.names $undef +.names y x[0] z[0] +11 1 +.names $false z[1] +1 1 +.names $false z[2] +1 1 +.names $false z[3] +1 1 +.names $false z[4] +1 1 +.end + +.model B +.inputs i[0] i[1] i[2] i[3] i[4] i[5] i[6] i[7] i[8] +.outputs j[0] j[1] +.names $false +.names $true +1 +.names $undef +.names i[0] j[1] +0 1 +.names i[0] j[0] +1 1 +.end +""" # noqa + class TestInputFromBlif(unittest.TestCase): def setUp(self): @@ -563,6 +614,31 @@ def test_blif_with_multiple_modules_unmerged_io(self): self.assertEqual(sim.inspect('s[3]'), (res & 0x8) >> 3) self.assertEqual(sim.inspect('cout'), (res & 0x10) >> 4) + def test_blif_import_single_submodule_not_as_block(self): + C = pyrtl.input_from_blif(multimodel_blif, top_model="C", as_block=False) + self.assertEqual(C.out.bitwidth, 1) + self.assertNotIsInstance(C.out, pyrtl.Output) + + A = pyrtl.input_from_blif(multimodel_blif, top_model="A", as_block=False) + self.assertEqual(A.x.bitwidth, 7) + self.assertEqual(A.y.bitwidth, 1) + self.assertEqual(A.z.bitwidth, 5) + self.assertNotIsInstance(A.x, pyrtl.Input) + self.assertNotIsInstance(A.y, pyrtl.Input) + self.assertNotIsInstance(A.z, pyrtl.Output) + + # Verify we can connect things + in1 = pyrtl.Input(2, 'in1') + A.x <<= in1 + A.y <<= in1[0] + out = pyrtl.Output(6, 'out') + out <<= C.out + A.z + + sim = pyrtl.Simulation() + sim.step_multiple({ + 'in1': [0, 1, 2, 3], + }) + def test_blif_with_clock_passing(self): pyrtl.input_from_blif(clock_passing_blif) a, b, c = [ @@ -1714,6 +1790,34 @@ def test_existing_reset_wire_without_add_reset(self): endmodule """ +verilog_input_multi_module_parameterized = """\ +module A(x, y, z); + parameter in_size = 4; + parameter out_size = 3; + input [in_size-1:0] x; + input y; + output [out_size-1:0] z; + + assign z = x[2:0] & y; +endmodule + +module B(i, j); + parameter in_size = 9; + input [in_size-1:0] i; + output [1:0] j; + + assign j = {~i[0], i[0]}; +endmodule + +module C(out); + output out; + reg r1, r2; + wire w1, w2; + A a(r1, r2, w1); + B b(w1, out); +endmodule +""" + class TestVerilogInput(unittest.TestCase): def setUp(self): @@ -1755,6 +1859,60 @@ def test_import_multi_module_auto_select_top_module(self): sim.step_multiple(nsteps=5) self.assertEqual(sim.tracer.trace['o'], [0, 2, 0, 2, 0]) + def test_import_module_with_parameters(self): + pyrtl.input_from_verilog(verilog_input_multi_module_parameterized, toplevel='A', + parameters={'in_size': 8, 'out_size': 5}) + block = pyrtl.working_block() + self.assertEqual(block.get_wirevector_by_name('x').bitwidth, 8) + self.assertEqual(block.get_wirevector_by_name('z').bitwidth, 5) + self.assertIsInstance(block.get_wirevector_by_name('x'), pyrtl.Input) + self.assertIsInstance(block.get_wirevector_by_name('z'), pyrtl.Output) + sim = pyrtl.Simulation() + x_inputs = [8, 3, 7, 34] + y_inputs = [1, 1, 1, 0] + sim.step_multiple({'x': x_inputs, 'y': y_inputs}) + self.assertEqual(sim.tracer.trace['z'], + [x & 7 & y for x, y in zip(x_inputs, y_inputs)]) + + def test_import_module_with_parameters_not_as_block(self): + B = pyrtl.input_from_verilog(verilog_input_multi_module_parameterized, toplevel='B', + parameters={'in_size': 4}, as_block=False) + block = pyrtl.working_block() + self.assertIsNone(block.get_wirevector_by_name('i'), 4) + self.assertIsNone(block.get_wirevector_by_name('j'), 2) + self.assertEqual(B.i.bitwidth, 4) + self.assertEqual(B.j.bitwidth, 2) + self.assertNotIsInstance(B.i, pyrtl.Input) + self.assertNotIsInstance(B.j, pyrtl.Output) + i_in = pyrtl.Input(4, 'i_in') + j_out = pyrtl.Output(2, 'j_out') + B.i <<= i_in + j_out <<= B.j + sim = pyrtl.Simulation() + sim.step_multiple({'i_in': [0b1001, 0b0100]}) + self.assertEqual(sim.tracer.trace['j_out'], [0b01, 0b10]) + + def test_import_module_multiple_times(self): + foo1 = pyrtl.input_from_verilog(verilog_input_multi_module, toplevel='foo', as_block=False) + foo2 = pyrtl.input_from_verilog(verilog_input_multi_module, toplevel='foo', as_block=False) + block = pyrtl.working_block() + in1, in2, in3 = pyrtl.input_list('in1/1 in2/1 in3/1') + foo1.a <<= in1 + foo1.b <<= in2 + foo2.a <<= foo1.o + foo2.b <<= in3 + pyrtl.probe(foo2.o, 'out') + sim = pyrtl.Simulation() + sim.step_multiple({ + 'in1': [0, 0, 0, 0, 1, 1, 1, 1], + 'in2': [0, 0, 1, 1, 0, 0, 1, 1], + 'in3': [0, 1, 0, 1, 0, 1, 0, 1], + }) + self.assertEqual( + sim.tracer.trace['out'], + [0, 1, 1, 2, 1, 2, 0, 1] + ) + def test_error_import_bad_file(self): with self.assertRaisesRegex(pyrtl.PyrtlError, "input_from_verilog expecting either open file or string"):