Skip to content

Commit

Permalink
Add a initialize_registers option to output_to_verilog.
Browse files Browse the repository at this point in the history
`initialize_registers` initializes each register to its `reset_value` in
Verilog. This is useful for FPGAs, where initial register values can be encoded
directly in the bitstream.

Update `output_verilog_testbench` to only create the `tb_iter` variable when it
will be used.

Minor documentation improvements.
  • Loading branch information
fdxmw committed Jun 11, 2024
1 parent d3c3641 commit 0424a70
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 96 deletions.
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions docs/export.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Importing Verilog
-----------------

.. autofunction:: pyrtl.importexport.input_from_blif
.. autofunction:: pyrtl.importexport.input_from_verilog

Outputting for Visualization
----------------------------
Expand Down
6 changes: 3 additions & 3 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
217 changes: 126 additions & 91 deletions pyrtl/importexport.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
<https://github.com/YosysHQ/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:
Expand Down Expand Up @@ -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?)')
Expand All @@ -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):
Expand All @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
'''

Expand Down
Loading

0 comments on commit 0424a70

Please sign in to comment.