diff --git a/docs/README.md b/docs/README.md index cb03c3a2..1aa05b80 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,7 +15,7 @@ The main Sphinx configuration file is Most of PyRTL's documentation is automatically extracted from Python docstrings, see [docstring -formating](https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#signatures) +formating](https://www.sphinx-doc.org/en/master/usage/domains/python.html) for supported directives and fields. Sphinx parses [Python type annotations](https://docs.python.org/3/library/typing.html), so put type information into annotations instead of docstrings. diff --git a/docs/export.rst b/docs/export.rst index bf4da4fc..08106c96 100644 --- a/docs/export.rst +++ b/docs/export.rst @@ -17,6 +17,7 @@ Importing Verilog ----------------- .. autofunction:: pyrtl.importexport.input_from_blif +.. autofunction:: pyrtl.importexport.input_from_verilog Outputting for Visualization ---------------------------- diff --git a/docs/requirements.txt b/docs/requirements.txt index 69a2cd58..8dfe4be0 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -10,7 +10,7 @@ babel==2.15.0 # via sphinx beautifulsoup4==4.12.3 # via furo -certifi==2024.2.2 +certifi==2024.6.2 # via requests charset-normalizer==3.3.2 # via requests @@ -26,7 +26,7 @@ jinja2==3.1.4 # via sphinx markupsafe==2.1.5 # via jinja2 -packaging==24.0 +packaging==24.1 # via sphinx pygments==2.18.0 # via @@ -45,7 +45,7 @@ sphinx==7.3.7 # sphinx-autodoc-typehints # sphinx-basic-ng # sphinx-copybutton -sphinx-autodoc-typehints==2.1.0 +sphinx-autodoc-typehints==2.1.1 # via -r requirements.in sphinx-basic-ng==1.0.0b2 # via furo diff --git a/pyrtl/importexport.py b/pyrtl/importexport.py index d6b792c6..0a025cb3 100644 --- a/pyrtl/importexport.py +++ b/pyrtl/importexport.py @@ -6,6 +6,8 @@ accordingly, or write information from the Block out to the file. """ +from __future__ import annotations + import re import collections import tempfile @@ -14,9 +16,10 @@ import sys import functools import operator +import typing from .pyrtlexceptions import PyrtlError, PyrtlInternalError -from .core import working_block, _NameSanitizer +from .core import working_block, _NameSanitizer, Block from .wire import WireVector, Input, Output, Const, Register, next_tempvar_name from .corecircuits import concat_list, rtl_all, rtl_any, select from .memory import RomBlock @@ -95,31 +98,35 @@ def twire(self, x): return s -def input_from_blif(blif, block=None, merge_io_vectors=True, clock_name='clk', top_model=None): - """ Read an open BLIF file or string as input, updating the block appropriately. +def input_from_blif( + blif, block: Block = None, merge_io_vectors: bool = True, + clock_name: str = 'clk', top_model: str = None): + """Read an open BLIF file or string as input, updating the block appropriately. - :param blif: An open BLIF file to read - :param Block block: The block where the logic will be added - :param bool merge_io_vectors: If True, Input/Output wires whose names differ only - 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 str 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 - listed in the BLIF + :param blif: An open BLIF file to read. + :param block: The block where the logic will be added. + :param merge_io_vectors: If True, :py:class:`.Input`/:py:class:`.Output` wires whose + names differ only by a indexing subscript (e.g. 1-bit wires ``a[0]`` and + ``a[1]``) will be combined into a single :py:class:`.Input`/:py:class:`.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 listed + in the BLIF. - 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 - by name ``a`` in the block. Otherwise if `merge_io_vectors` is False, the original 1-bit - wires will be Input wires of the block. This holds similarly for Outputs. + If ``merge_io_vectors`` is ``True``, then given 1-bit :py:class:`.Input` wires + ``a[0]`` and ``a[1]``, these wires will be combined into a single 2-bit + :py:class:`.Input` wire ``a`` that can be accessed by name ``a`` in the block. + Otherwise if ``merge_io_vectors`` is ``False``, the original 1-bit wires will be + :py:class:`.Input` wires of the block. This holds similarly for :py:class:`.Output`. This assumes the following: - * There is only one single shared clock and reset * Output is generated by Yosys with formals in a particular order - It currently supports multi-module (unflattened) BLIF, though we recommend importing a - flattened BLIF with a single module when possible. - It currently ignores the reset signal (which it assumes is input only to the flip flops). + It currently supports multi-module (unflattened) BLIF, though we recommend importing + a flattened BLIF with a single module when possible. It currently ignores the reset + signal (which it assumes is input only to the flip flops). + """ import pyparsing from pyparsing import (Word, Literal, OneOrMore, ZeroOrMore, @@ -522,23 +529,28 @@ def instantiate(subckt): # -def input_from_verilog(verilog, clock_name='clk', toplevel=None, leave_in_dir=None, block=None): - """ Read an open Verilog file or string as input via Yosys conversion, updating the block. +def input_from_verilog( + verilog, clock_name: str = 'clk', toplevel: str = None, + leave_in_dir: bool = None, block: 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 leave_in_dir: If True, save the intermediate BLIF file created in the given + directory. + :param: The block where the logic will be added. + + Note: This function is essentially a wrapper for :py:func:`input_from_blif`, with + the added convenience of turning the Verilog into BLIF for import for you. This + function passes a set of commands to Yosys as a script that normally produces BLIF + files that can be successuflly imported into PyRTL via :py:func:`input_from_blif`. If the + Yosys conversion fails here, we recommend you create your own custom Yosys script to + try and produce BLIF yourself. Then you can import BLIF directly via + :py:func:`input_from_blif`. - :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 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 - - Note: This function is essentially a wrapper for `input_from_blif()`, with the added convenience - of turning the Verilog into BLIF for import for you. This function passes a set of commands to - Yosys as a script that normally produces BLIF files that can be successuflly imported into - PyRTL via `input_from_blif()`. If the Yosys conversion fails here, we recommend you create your - own custom Yosys script to try and produce BLIF yourself. Then you can import BLIF directly via - `input_from_blif()`. """ # Dev Notes: @@ -595,7 +607,7 @@ def input_from_verilog(verilog, clock_name='clk', toplevel=None, leave_in_dir=No print('---------------------------------------------', file=sys.stderr) print(str(e.output).replace('\\n', '\n'), file=sys.stderr) print('---------------------------------------------', file=sys.stderr) - raise PyrtlError('Yosys callfailed') + raise PyrtlError('Yosys call failed') except OSError as e: print('Error with call to yosys...', file=sys.stderr) raise PyrtlError('Call to yosys failed (not installed or on path?)') @@ -605,16 +617,27 @@ def input_from_verilog(verilog, clock_name='clk', toplevel=None, leave_in_dir=No os.remove(tmp_blif_path) -def output_to_verilog(dest_file, add_reset=True, block=None): - """ A function to walk the block and output it in Verilog format to the open file. +def output_to_verilog(dest_file, add_reset: typing.Union[bool, str] = True, + block: Block = None, initialize_registers: bool = False): + """A function to walk the block and output it in Verilog format to the open file. + + :param dest_file: Open file where the Verilog output will be written. + :param add_reset: If reset logic should be added. Allowable options are: ``False`` + (meaning no reset logic is added), ``True`` (default, for adding synchronous + reset logic), and ``'asynchronous'`` (for adding asynchronous reset logic). The + reset input will be named ``rst``, and when ``rst`` is high, registers will be + reset to their ``reset_value``. + :param initialize_registers: Initialize Verilog registers to their ``reset_value``. + When this argument is ``True``, a register like ``Register(name='foo', + bitwidth=8, reset_value=4)`` generates Verilog like ``reg[7:0] foo = 8'd4;``. + :param block: Block to be walked and exported. - :param dest_file: Open file where the Verilog output will be written - :param Union[bool, str] add_reset: If reset logic should be added. Allowable options are: - False (meaning no reset logic is added), True (default, for adding synchronous - reset logic), and `asynchronous` (for adding asynchronous reset logic). - :param block: Block to be walked and exported + The Verilog module will be named ``toplevel``, with a clock input named ``clk``. + + When possible, wires keep their names in the Verilog output. Wire names that do not + satisfy Verilog's naming requirements, and wires that conflict with Verilog keywords + are given new temporary names in the Verilog output. - The registers will be set to their `reset_value`, if specified, otherwise 0. """ if not isinstance(add_reset, bool): @@ -637,7 +660,7 @@ def output_to_verilog(dest_file, add_reset=True, block=None): def varname(wire): return internal_names[wire.name] - _to_verilog_header(file, block, varname, add_reset) + _to_verilog_header(file, block, varname, add_reset, initialize_registers) _to_verilog_combinational(file, block, varname) _to_verilog_sequential(file, block, varname, add_reset) _to_verilog_memories(file, block, varname) @@ -695,7 +718,7 @@ def _verilog_block_parts(block): return inputs, outputs, registers, wires, memories -def _to_verilog_header(file, block, varname, add_reset): +def _to_verilog_header(file, block, varname, add_reset, initialize_registers): """ Print the header of the verilog implementation. """ def name_sorted(wires): @@ -735,8 +758,15 @@ def name_list(wires): memsize_str = _verilog_vector_size_decl(1 << m.addrwidth) print(' reg{:s} mem_{}{:s}; //{}'.format(memwidth_str, m.id, memsize_str, m.name), file=file) - for w in name_sorted(registers): - print(' reg{:s} {:s};'.format(_verilog_vector_decl(w), varname(w)), file=file) + for reg in name_sorted(registers): + register_initialization = '' + if initialize_registers: + reset_value = 0 + if reg.reset_value is not None: + reset_value = reg.reset_value + register_initialization = f" = {reg.bitwidth}'d{reset_value}" + print(f' reg{_verilog_vector_decl(reg)} {varname(reg)}' + f'{register_initialization};', file=file) if (memories or registers): print('', file=file) @@ -873,43 +903,47 @@ def _to_verilog_footer(file): print('endmodule\n', file=file) -def output_verilog_testbench(dest_file, simulation_trace=None, toplevel_include=None, - vcd="waveform.vcd", cmd=None, add_reset=True, block=None): +def output_verilog_testbench( + dest_file, simulation_trace=None, toplevel_include: str = None, + vcd: str = "waveform.vcd", cmd: str = None, + add_reset: typing.Union[bool, str] = True, block: Block = None): """Output a Verilog testbench for the block/inputs used in the simulation trace. :param dest_file: an open file to which the test bench will be printed. - - :param SimulationTrace simulation_trace: a simulation trace from which the - inputs will be extracted for inclusion in the test bench. The test - bench generated will just replay the inputs played to the simulation - cycle by cycle. The default values for all registers and memories will - be based on the trace, otherwise they will be initialized to 0. - :param str toplevel_include: name of the file containing the toplevel - module this testbench is testing. If not None, an `include` directive - will be added to the top. - :param str vcd: By default the testbench generator will include a command - in the testbench to write the output of the testbench execution to a - .vcd file (via `$dumpfile`), and this parameter is the string of the - name of the file to use. If None is specified instead, then no - `dumpfile` will be used. - :param str cmd: The string passed as cmd will be copied verbatim into the - testbench just before the end of each cycle. This is useful for doing - things like printing specific values out during testbench evaluation - (e.g. ``cmd='$display("%d", out);'`` will instruct the testbench to - print the value of `out` every cycle which can then be compared easy - with a reference). - :param Union[bool, str] add_reset: If reset logic should be - added. Allowable options are: False (meaning no reset logic is added), - True (default, for adding synchronous reset logic), and `asynchronous` - (for adding asynchronous reset logic). The value passed in here should - match the argument passed to :func:`.output_to_verilog`. - :param Block block: Block containing design to test. - - If `add_reset` is not False, a `rst` wire is added and will passed as an - input to the instantiated toplevel module. The `rst` wire will be held low - in the testbench, because initialization here occurs via the `initial` - block. It is provided for consistency with :func:`.output_to_verilog`. + :param SimulationTrace simulation_trace: a simulation trace from which the inputs + will be extracted for inclusion in the test bench. The test bench generated will + just replay the inputs played to the simulation cycle by cycle. The default + values for all registers and memories will be based on the trace, otherwise they + will be initialized to 0. + :param toplevel_include: name of the file containing the toplevel module this + testbench is testing. If not ``None``, an `include` directive will be added to + the top. + :param vcd: By default the testbench generator will include a command in the + testbench to write the output of the testbench execution to a ``.vcd`` file (via + `$dumpfile`), and this parameter is the name of the file to write. If ``None`` + is specified, then no `dumpfile` will be used. + :param cmd: The string passed as ``cmd`` will be copied verbatim into the testbench + just before the end of each cycle. This is useful for doing things like printing + specific values during testbench evaluation. For example, ``cmd='$display("%d", + out);'`` will instruct the testbench to print the value of `out` every cycle, + which can be compared with a reference. + :param add_reset: If reset logic should be added. Allowable options are: ``False`` + (meaning no reset logic is added), ``True`` (default, for adding synchronous + reset logic), and ``'asynchronous'`` (for adding asynchronous reset logic). The + value passed in here should match the argument passed to + :func:`.output_to_verilog`. + + :param block: Block containing design to test. + + If ``add_reset`` is not False, a ``rst`` input wire is added to the instantiated + ``toplevel`` module. The ``rst`` wire will be held low in the testbench, because + initialization here occurs via the ``initial`` block. ``add_reset`` is provided for + consistency with :py:func:`output_to_verilog`. + + This function *only* generates the Verilog testbench. The Verilog module must be + generated separately by calling :py:func:`output_to_verilog`, see the + ``toplevel_include`` parameter and Example 2 below. The test bench does not return any values. @@ -1001,7 +1035,8 @@ def default_value(): print('', file=dest_file) # Declare an integer used for init of memories - print(' integer tb_iter;', file=dest_file) + if len(memories) > 0: + print(' integer tb_iter;', file=dest_file) # Instantiate logic block io_list = ['clk'] + name_list(name_sorted(inputs)) + name_list(name_sorted(outputs)) @@ -1047,9 +1082,9 @@ def default_value(): ver_name[w.name], "{:d}'d".format(len(w)), simulation_trace.trace[w.name][i]), file=dest_file) + print('\n #10', file=dest_file) if cmd: print(' %s' % cmd, file=dest_file) - print('\n #10', file=dest_file) # Footer print(' $finish;', file=dest_file) @@ -1063,15 +1098,15 @@ def default_value(): # | | | \ | \ | |___ # -def output_to_firrtl(open_file, rom_blocks=None, block=None): +def output_to_firrtl(open_file, rom_blocks: list[RomBlock] = None, block: Block = None): """Output the block as FIRRTL code to the output file. - :param open_file: File to write to - :param rom_blocks: List of ROM blocks to be initialized - :param block: Block to use (defaults to working block) + :param open_file: File to write to. + :param rom_blocks: List of ROM blocks to be initialized. + :param block: Block to use (defaults to working block). - If ROM is initialized in PyRTL code, you can pass in the `rom_blocks` as a - list `[rom1, rom2, ...]`. + If ROM is initialized in PyRTL code, you can pass in the ``rom_blocks`` as a + list ``[rom1, rom2, ...]``. """ block = working_block(block) @@ -1230,10 +1265,10 @@ def output_to_firrtl(open_file, rom_blocks=None, block=None): # | .__/ \__ /~~\ .__/ |__) |___ | \| \__ | | -def input_from_iscas_bench(bench, block=None): +def input_from_iscas_bench(bench, block: Block = None): ''' Import an ISCAS .bench file - :param file bench: an open ISCAS .bench file to read + :param bench: an open ISCAS .bench file to read :param block: block to add the imported logic (defaults to current working block) ''' diff --git a/tests/test_importexport.py b/tests/test_importexport.py index 10311173..3e94b163 100644 --- a/tests/test_importexport.py +++ b/tests/test_importexport.py @@ -1676,6 +1676,16 @@ def test_existing_reset_wire_without_add_reset(self): pyrtl.output_to_verilog(buffer, add_reset=False) self.assertEqual(buffer.getvalue(), verilog_custom_reset) + def test_register_reset_value(self): + reg0 = pyrtl.Register(name='register0', bitwidth=8, reset_value=0) + reg1 = pyrtl.Register(name='register1', bitwidth=4, reset_value=1) + + buffer = io.StringIO() + pyrtl.output_to_verilog(buffer, add_reset=False, initialize_registers=True) + + self.assertTrue("reg[7:0] register0 = 8'd0" in buffer.getvalue()) + self.assertTrue("reg[3:0] register1 = 4'd1" in buffer.getvalue()) + verilog_input_counter = """\ module counter (clk, rst, en, count); @@ -1872,7 +1882,6 @@ def test_error_import_bad_file(self): reg clk; reg rst; - integer tb_iter; toplevel block(.clk(clk), .rst(rst)); always