├── tests ├── __init__.py ├── test_examples.py └── rtllib │ ├── test_adders.py │ ├── test_barrel.py │ ├── test_libutils.py │ ├── test_prngs.py │ └── test_multipliers.py ├── .python-version ├── pyrtl ├── rtllib │ ├── __init__.py │ ├── barrel.py │ ├── libutils.py │ └── testingutils.py ├── pyrtlexceptions.py └── __init__.py ├── docs ├── brand │ └── pyrtl_logo.png ├── images │ └── gcd-graph.png ├── screenshots │ ├── pyrtl-counter.png │ ├── pyrtl-statemachine.png │ ├── pyrtl-renderer-demo-ascii.png │ ├── pyrtl-renderer-demo-cp437.png │ ├── pyrtl-renderer-demo-utf-8.png │ ├── pyrtl-renderer-demo-powerline.png │ └── pyrtl-renderer-demo-utf-8-alt.png ├── regmem.rst ├── export.rst ├── analysis.rst ├── rtllib.rst ├── simtest.rst ├── blocks.rst ├── README.md ├── conf.py ├── helpers.rst ├── index.rst ├── release │ └── README.md └── basic.rst ├── codecov.yml ├── examples ├── Makefile ├── README.md ├── example0-minimum-viable-hardware.py ├── example7-synth-timing.py ├── renderer-demo.py ├── example5-introspection.py ├── example2-counter.py ├── example1-combologic.py ├── example1.1-signed-numbers.py ├── example8-verilog.py ├── example6-memory.py ├── example3-statemachine.py ├── example1.2-wire-struct.py ├── example4-debuggingtools.py └── introduction-to-hardware.py ├── .github └── workflows │ ├── python-test.yml │ └── python-release.yml ├── .gitignore ├── .coveragerc ├── .readthedocs.yaml ├── justfile ├── LICENSE.md ├── ipynb-examples ├── example0-minimum-viable-hardware.ipynb ├── renderer-demo.ipynb ├── example5-introspection.ipynb ├── example7-synth-timing.ipynb └── example2-counter.ipynb ├── pyproject.toml ├── CHANGELOG.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.14 2 | -------------------------------------------------------------------------------- /pyrtl/rtllib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/brand/pyrtl_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/HEAD/docs/brand/pyrtl_logo.png -------------------------------------------------------------------------------- /docs/images/gcd-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/HEAD/docs/images/gcd-graph.png -------------------------------------------------------------------------------- /docs/screenshots/pyrtl-counter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/HEAD/docs/screenshots/pyrtl-counter.png -------------------------------------------------------------------------------- /docs/screenshots/pyrtl-statemachine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/HEAD/docs/screenshots/pyrtl-statemachine.png -------------------------------------------------------------------------------- /docs/screenshots/pyrtl-renderer-demo-ascii.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/HEAD/docs/screenshots/pyrtl-renderer-demo-ascii.png -------------------------------------------------------------------------------- /docs/screenshots/pyrtl-renderer-demo-cp437.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/HEAD/docs/screenshots/pyrtl-renderer-demo-cp437.png -------------------------------------------------------------------------------- /docs/screenshots/pyrtl-renderer-demo-utf-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/HEAD/docs/screenshots/pyrtl-renderer-demo-utf-8.png -------------------------------------------------------------------------------- /docs/screenshots/pyrtl-renderer-demo-powerline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/HEAD/docs/screenshots/pyrtl-renderer-demo-powerline.png -------------------------------------------------------------------------------- /docs/screenshots/pyrtl-renderer-demo-utf-8-alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/HEAD/docs/screenshots/pyrtl-renderer-demo-utf-8-alt.png -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true 9 | precision: 1 10 | round: up 11 | -------------------------------------------------------------------------------- /examples/Makefile: -------------------------------------------------------------------------------- 1 | PYTHON=python 2 | PY_FILES=$(wildcard *.py) 3 | IPYNB_FILES=$(addprefix ../ipynb-examples/, $(PY_FILES:.py=.ipynb)) 4 | 5 | all: $(IPYNB_FILES) 6 | 7 | # Convert a PyRTL example Python script to a Jupyter notebook. 8 | ../ipynb-examples/%.ipynb: %.py tools/to_ipynb.py 9 | $(PYTHON) tools/to_ipynb.py $< $@ 10 | -------------------------------------------------------------------------------- /docs/regmem.rst: -------------------------------------------------------------------------------- 1 | Registers and Memories 2 | ====================== 3 | 4 | Registers 5 | --------- 6 | 7 | .. autoclass:: pyrtl.Register 8 | :members: 9 | :show-inheritance: 10 | :special-members: __init__ 11 | 12 | Memories 13 | -------- 14 | 15 | .. autoclass:: pyrtl.MemBlock 16 | :members: 17 | :special-members: __init__, __getitem__, __setitem__ 18 | 19 | ROMs 20 | ---- 21 | 22 | .. autoclass:: pyrtl.RomBlock 23 | :members: 24 | :show-inheritance: 25 | :special-members: __init__, __getitem__ 26 | -------------------------------------------------------------------------------- /pyrtl/pyrtlexceptions.py: -------------------------------------------------------------------------------- 1 | """The set of error types thrown by PyRTL.""" 2 | 3 | # ----------------------------------------------------------------- 4 | # ___ __ __ __ __ ___ __ ___ __ 5 | # |__ |__) |__) / \ |__) | \ / |__) |__ /__` 6 | # |___ | \ | \ \__/ | \ | | | |___ .__/ 7 | # 8 | 9 | 10 | class PyrtlError(Exception): 11 | """Raised on any user-facing error in this module.""" 12 | 13 | pass 14 | 15 | 16 | class PyrtlInternalError(Exception): 17 | """Raised on any PyRTL internal failure.""" 18 | 19 | pass 20 | -------------------------------------------------------------------------------- /.github/workflows/python-test.yml: -------------------------------------------------------------------------------- 1 | name: Run Python tests 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: astral-sh/setup-uv@v6 14 | with: 15 | enable-cache: true 16 | - name: Install apt dependencies 17 | run: sudo apt install -y yosys 18 | - name: Run tests 19 | run: uv run just tests 20 | - name: Upload coverage to Codecov 21 | uses: codecov/codecov-action@v4 22 | env: 23 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 24 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # PyRTL's Examples 2 | 3 | PyRTL's examples are Python scripts that demonstrate various PyRTL features. 4 | These scripts can be run with `python $SCRIPT_FILE_NAME`. 5 | 6 | Each script is converted to an equivalent Jupyter notebook in the 7 | `ipynb-examples` directory. These conversions are done by the `to_ipynb.py` 8 | script in the `examples/tools` directory. 9 | 10 | If you update an example script, be sure to update its corresponding Jupyter 11 | notebook. These updates are handled by the `Makefile` in this directory, so all 12 | Jupyter notebooks can be updated by running `make`. 13 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import subprocess 4 | 5 | import pytest 6 | 7 | import pyrtl 8 | 9 | """ 10 | Tests all of the files in the example folder 11 | 12 | Note that this file is structure dependent, so don't forget to change it if the relative 13 | location of the examples changes 14 | """ 15 | 16 | 17 | @pytest.mark.parametrize( 18 | "file", glob.iglob(os.path.dirname(__file__) + "/../examples/*.py") 19 | ) 20 | def test_all_examples(file): 21 | pyrtl.reset_working_block() 22 | try: 23 | subprocess.check_output(["python", file]) 24 | except subprocess.CalledProcessError as e: 25 | raise e 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # coverage 4 | .coverage/ 5 | coverage.xml 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Packages 11 | *.egg-info 12 | dist 13 | build 14 | parts 15 | bin 16 | var 17 | sdist 18 | .installed.cfg 19 | lib 20 | lib64 21 | __pycache__ 22 | 23 | # Unit test / coverage reports 24 | .coverage 25 | .tox 26 | 27 | # Translations 28 | *.mo 29 | 30 | # Mr Developer 31 | .mr.developer.cfg 32 | .project 33 | .pydevproject 34 | 35 | # pycharm 36 | .idea 37 | 38 | # Apple crap 39 | .DS_Store 40 | 41 | # Stuff specifically for PyRTL 42 | spice.net 43 | docs/_build 44 | 45 | # Verilog files 46 | # There must be a very good reason for someone to add a verilog file to the repo 47 | .v 48 | 49 | # ipynb 50 | ipynb-examples/.ipynb_checkpoints 51 | 52 | # VS Code 53 | .vscode 54 | 55 | # Python venv 56 | pyvenv.cfg 57 | 58 | # Jupyter 59 | .jupyterlite.doit.db 60 | _output 61 | -------------------------------------------------------------------------------- /examples/example0-minimum-viable-hardware.py: -------------------------------------------------------------------------------- 1 | # # A simple combinational logic example. 2 | # 3 | # Create an 8-bit adder that adds `a` and `b`. Check if the sum is greater than `5`. 4 | import pyrtl 5 | 6 | # Define `Inputs` and `Outputs`. 7 | a = pyrtl.Input(bitwidth=8, name="a") 8 | b = pyrtl.Input(bitwidth=8, name="b") 9 | 10 | q = pyrtl.Output(bitwidth=8, name="q") 11 | gt5 = pyrtl.Output(bitwidth=1, name="gt5") 12 | 13 | # Define the logic that connects the `Inputs` to the `Outputs`. 14 | sum = a + b # Makes an 8-bit adder. 15 | q <<= sum # Connects the adder's output to the `q` output pin. 16 | gt5 <<= sum > 5 # Does a comparison and connects the result to the `gt5` output pin. 17 | 18 | # Simulate various values for `a` and `b` over 5 cycles. 19 | sim = pyrtl.Simulation() 20 | sim.step_multiple({"a": [0, 1, 2, 3, 4], "b": [2, 2, 3, 3, 4]}) 21 | 22 | # Display simulation traces as waveforms. 23 | sim.tracer.render_trace() 24 | -------------------------------------------------------------------------------- /docs/export.rst: -------------------------------------------------------------------------------- 1 | Exporting and Importing Designs 2 | =============================== 3 | 4 | Exporting Hardware Designs 5 | -------------------------- 6 | 7 | .. autofunction:: pyrtl.output_to_verilog 8 | 9 | .. autofunction:: pyrtl.output_to_firrtl 10 | 11 | Exporting Testbenches 12 | --------------------- 13 | 14 | .. autofunction:: pyrtl.output_verilog_testbench 15 | 16 | Importing Verilog 17 | ----------------- 18 | 19 | .. autofunction:: pyrtl.input_from_blif 20 | .. autofunction:: pyrtl.input_from_verilog 21 | 22 | Outputting for Visualization 23 | ---------------------------- 24 | 25 | .. autofunction:: pyrtl.output_to_trivialgraph 26 | .. autofunction:: pyrtl.output_to_graphviz 27 | .. autofunction:: pyrtl.graphviz_detailed_namer 28 | .. autofunction:: pyrtl.output_to_svg 29 | .. autofunction:: pyrtl.block_to_graphviz_string 30 | .. autofunction:: pyrtl.block_to_svg 31 | .. autofunction:: pyrtl.trace_to_html 32 | .. autofunction:: pyrtl.net_graph 33 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | source = pyrtl 4 | 5 | [report] 6 | # Regexes for lines to exclude from consideration 7 | exclude_lines = 8 | # Have to re-enable the standard pragma 9 | pragma: no cover 10 | 11 | # ignore pass statements that are not run: 12 | # sometimes you need them to make a block (especially for unittests) 13 | # need to be really careful with this one because 'pass' shows up in other places as well 14 | \spass\s 15 | \spass$ 16 | 17 | # Don't complain about missing debug-only code: 18 | def __repr__ 19 | if self\.debug 20 | 21 | # Don't complain if tests don't hit defensive assertion code: 22 | raise AssertionError 23 | raise NotImplementedError 24 | 25 | # Don't complain if non-runnable code isn't run: 26 | if 0: 27 | if __name__ == .__main__.: 28 | 29 | ignore_errors = True 30 | 31 | omit = 32 | .tox/* 33 | */tests/* 34 | */ctypes/* 35 | six.py 36 | pyparsing.py 37 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the version of Python and other tools you might need 8 | build: 9 | # Use the latest Ubuntu LTS version available on Read the Docs. 10 | os: ubuntu-lts-latest 11 | tools: 12 | # Use the latest 3.x version available on Read the Docs. 13 | python: "3" 14 | apt_packages: 15 | - graphviz 16 | jobs: 17 | pre_create_environment: 18 | - asdf plugin add uv 19 | - asdf install uv latest 20 | - asdf global uv latest 21 | create_environment: 22 | - uv venv "${READTHEDOCS_VIRTUALENV_PATH}" 23 | install: 24 | - UV_PROJECT_ENVIRONMENT="${READTHEDOCS_VIRTUALENV_PATH}" uv sync --frozen 25 | 26 | # Build documentation in the docs/ directory with Sphinx 27 | sphinx: 28 | configuration: docs/conf.py 29 | 30 | # Optionally build your docs in additional formats such as PDF 31 | formats: 32 | - pdf 33 | -------------------------------------------------------------------------------- /docs/analysis.rst: -------------------------------------------------------------------------------- 1 | .. PyRTL analysis master file 2 | 3 | .. _analysis_and_optimization: 4 | 5 | Analysis and Optimization 6 | ========================= 7 | 8 | Tools for analyzing and optimizing aspects of PyRTL designs. 9 | 10 | Estimation 11 | ---------- 12 | 13 | .. autoclass:: pyrtl.TimingAnalysis 14 | :members: 15 | :special-members: __init__ 16 | 17 | .. autofunction:: pyrtl.area_estimation 18 | .. autofunction:: pyrtl.distance 19 | .. autofunction:: pyrtl.fanout 20 | .. autoclass:: pyrtl.analysis.PathsResult 21 | :members: 22 | .. autofunction:: pyrtl.paths 23 | .. autofunction:: pyrtl.yosys_area_delay 24 | 25 | Optimization 26 | ------------ 27 | 28 | .. autofunction:: pyrtl.optimize 29 | 30 | Synthesis 31 | --------- 32 | 33 | .. autofunction:: pyrtl.synthesize 34 | 35 | .. autoclass:: pyrtl.PostSynthBlock 36 | :show-inheritance: 37 | :members: 38 | 39 | Individual Passes 40 | ----------------- 41 | 42 | .. autofunction:: pyrtl.common_subexp_elimination 43 | .. autofunction:: pyrtl.constant_propagation 44 | .. autofunction:: pyrtl.nand_synth 45 | .. autofunction:: pyrtl.and_inverter_synth 46 | .. autofunction:: pyrtl.one_bit_selects 47 | .. autofunction:: pyrtl.two_way_concat 48 | -------------------------------------------------------------------------------- /docs/rtllib.rst: -------------------------------------------------------------------------------- 1 | RTL Library 2 | =========== 3 | 4 | Useful circuits, functions, and utilities. 5 | 6 | Multiplexers 7 | ------------ 8 | 9 | .. automodule:: pyrtl.rtllib.muxes 10 | :members: 11 | :exclude-members: MultiSelector 12 | 13 | Adders 14 | ------ 15 | 16 | .. automodule:: pyrtl.rtllib.adders 17 | :members: 18 | :undoc-members: 19 | 20 | Multipliers 21 | ----------- 22 | 23 | .. automodule:: pyrtl.rtllib.multipliers 24 | :members: 25 | 26 | Barrel Shifter 27 | -------------- 28 | 29 | .. automodule:: pyrtl.rtllib.barrel 30 | :members: 31 | :undoc-members: 32 | 33 | Matrix 34 | ------ 35 | 36 | .. automodule:: pyrtl.rtllib.matrix 37 | :members: 38 | :special-members: __init__, __len__, __getitem__, __reversed__, __add__, __sub__, __mul__, __matmul__, __pow__ 39 | :exclude-members: multiply 40 | 41 | Pseudo-Random Numbers 42 | --------------------- 43 | 44 | .. automodule:: pyrtl.rtllib.prngs 45 | :members: 46 | 47 | AES-128 48 | ------- 49 | 50 | .. autoclass:: pyrtl.rtllib.aes.AES 51 | :members: 52 | 53 | Testing Utilities 54 | ----------------- 55 | 56 | .. automodule:: pyrtl.rtllib.testingutils 57 | :members: 58 | :exclude-members: generate_in_wire_and_values, sim_and_ret_out, sim_and_ret_outws, sim_multicycle, multi_sim_multicycle 59 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | # PyRTL uses `just` instead of `make` because: 2 | # * `make` is not installed by default on Windows. 3 | # * `uv` can install `just` on all supported platforms from PyPI. 4 | presubmit: tests docs 5 | 6 | tests: 7 | # Run `pytest` with the latest version of Python supported by PyRTL, 8 | # which is the default for `uv`. The default is set with `uv python 9 | # pin` and written to `.python_version`. 10 | uv run pytest -n auto --cov=pyrtl --cov-report=xml 11 | 12 | # Run `pytest` in an isolated virtual environment, with the earliest 13 | # version of Python supported by PyRTL. 14 | uv run --python=3.10 --isolated pytest -n auto 15 | 16 | # Run `ruff format` to check that code is formatted properly. 17 | # 18 | # If this fails, try running 19 | # 20 | # $ uv run ruff format 21 | # 22 | # to automatically reformat any changed code. 23 | uv run ruff format --diff 24 | 25 | # Run `ruff check` to check for lint errors. 26 | # 27 | # If this fails, try running 28 | # 29 | # $ uv run ruff check --fix 30 | # 31 | # to automatically apply fixes, when possible. 32 | uv run ruff check 33 | 34 | docs: 35 | # Run `sphinx-build` to generate documentation. 36 | # 37 | # Output: docs/_build/html/index.html 38 | uv run sphinx-build -M html docs/ docs/_build 39 | -------------------------------------------------------------------------------- /docs/simtest.rst: -------------------------------------------------------------------------------- 1 | Simulation and Testing 2 | ====================== 3 | 4 | Simulation 5 | ---------- 6 | 7 | .. autoclass:: pyrtl.Simulation 8 | :members: 9 | :special-members: __init__ 10 | 11 | Fast (JIT to Python) Simulation 12 | ------------------------------- 13 | 14 | .. autoclass:: pyrtl.FastSimulation 15 | :members: 16 | :special-members: __init__ 17 | 18 | Compiled (JIT to C) Simulation 19 | ------------------------------ 20 | 21 | .. autoclass:: pyrtl.CompiledSimulation 22 | :members: 23 | :exclude-members: run 24 | 25 | Simulation Trace 26 | ---------------- 27 | 28 | .. autoclass:: pyrtl.SimulationTrace 29 | :members: 30 | :special-members: __init__ 31 | :exclude-members: add_fast_step, add_step 32 | 33 | .. autofunction:: pyrtl.enum_name 34 | 35 | Wave Renderer 36 | ------------- 37 | 38 | .. autoclass:: pyrtl.simulation.WaveRenderer 39 | :members: 40 | :special-members: __init__ 41 | :exclude-members: render_ruler_segment, render_val, val_to_str 42 | .. autoclass:: pyrtl.simulation.RendererConstants 43 | .. autoclass:: pyrtl.simulation.PowerlineRendererConstants 44 | :show-inheritance: 45 | .. autoclass:: pyrtl.simulation.Utf8RendererConstants 46 | :show-inheritance: 47 | .. autoclass:: pyrtl.simulation.Utf8AltRendererConstants 48 | :show-inheritance: 49 | .. autoclass:: pyrtl.simulation.Cp437RendererConstants 50 | :show-inheritance: 51 | .. autoclass:: pyrtl.simulation.AsciiRendererConstants 52 | :show-inheritance: 53 | -------------------------------------------------------------------------------- /docs/blocks.rst: -------------------------------------------------------------------------------- 1 | Block and Logic Nets 2 | ===================== 3 | 4 | :class:`.Block` and :class:`.LogicNet` are lower level PyRTL abstractions for 5 | representing a hardware design. Most users won't need to understand them, 6 | unless they are implementing :ref:`analysis_and_optimization` passes or 7 | modifying PyRTL itself. 8 | 9 | :ref:`gate_graphs` are an alternative representation that makes it easier to 10 | write analysis passes. 11 | 12 | Blocks 13 | ------ 14 | 15 | .. autoclass:: pyrtl.Block 16 | :members: 17 | :exclude-members: sanity_check_memblock, sanity_check_memory_sync, sanity_check_net, sanity_check_wirevector 18 | 19 | .. _working_block: 20 | 21 | ``working_block`` 22 | ^^^^^^^^^^^^^^^^^ 23 | 24 | Most PyRTL operations operate on the global ``working_block`` by default. PyRTL 25 | provides several functions to inspect and manipulate the ``working_block``: 26 | 27 | .. autofunction:: pyrtl.working_block 28 | 29 | .. autofunction:: pyrtl.reset_working_block 30 | 31 | .. autofunction:: pyrtl.set_working_block 32 | 33 | .. autofunction:: pyrtl.temp_working_block 34 | 35 | LogicNets 36 | --------- 37 | 38 | .. autoclass:: pyrtl.LogicNet 39 | :members: 40 | :undoc-members: 41 | 42 | .. _gate_graphs: 43 | 44 | GateGraphs 45 | ---------- 46 | 47 | .. automodule:: pyrtl.gate_graph 48 | 49 | .. autoclass:: pyrtl.Gate 50 | :members: 51 | :special-members: __str__ 52 | 53 | .. autoclass:: pyrtl.GateGraph 54 | :members: 55 | :special-members: __init__, __iter__, __str__ 56 | 57 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Regents of the University of California 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of PyRTL nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /tests/rtllib/test_adders.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | import random 3 | import unittest 4 | 5 | import pyrtl 6 | import pyrtl.rtllib.testingutils as utils 7 | from pyrtl.rtllib import adders 8 | 9 | 10 | class TestDocTests(unittest.TestCase): 11 | """Test documentation examples.""" 12 | 13 | def test_doctests(self): 14 | failures, tests = doctest.testmod(m=pyrtl.rtllib.adders) 15 | self.assertGreater(tests, 0) 16 | self.assertEqual(failures, 0) 17 | 18 | 19 | class TestAdders(unittest.TestCase): 20 | @classmethod 21 | def setUpClass(cls): 22 | random.seed(8492049) 23 | 24 | def setUp(self): 25 | pyrtl.reset_working_block() 26 | 27 | def tearDown(self): 28 | pyrtl.reset_working_block() 29 | 30 | def adder2_t_base_1(self, adder_func): 31 | self.adder_t_base(adder_func, max_bitwidth=34, num_wires=2) 32 | 33 | def adder_t_base(self, adder_func, **kwargs): 34 | wires, vals = utils.make_inputs_and_values( 35 | dist=utils.inverse_power_dist, **kwargs 36 | ) 37 | outwire = pyrtl.Output(name="test") 38 | outwire <<= adder_func(*wires) 39 | 40 | out_vals = utils.sim_and_ret_out(outwire, wires, vals) 41 | true_result = [sum(cycle_vals) for cycle_vals in zip(*vals, strict=True)] 42 | self.assertEqual(out_vals, true_result) 43 | 44 | def test_kogge_stone_1(self): 45 | self.adder2_t_base_1(adders.kogge_stone) 46 | 47 | def test_ripple_1(self): 48 | self.adder2_t_base_1(adders.ripple_add) 49 | 50 | def test_carrylookahead_1(self): 51 | self.adder2_t_base_1(adders.cla_adder) 52 | 53 | def test_carry_save_1(self): 54 | self.adder_t_base(adders.carrysave_adder, exact_bitwidth=32, num_wires=3) 55 | 56 | def test_fast_group_adder_1(self): 57 | wires, vals = utils.make_inputs_and_values( 58 | max_bitwidth=12, num_wires=7, dist=utils.inverse_power_dist 59 | ) 60 | outwire = pyrtl.Output(name="test") 61 | outwire <<= adders.fast_group_adder(wires) 62 | 63 | out_vals = utils.sim_and_ret_out(outwire, wires, vals) 64 | true_result = [sum(cycle_vals) for cycle_vals in zip(*vals, strict=True)] 65 | self.assertEqual(out_vals, true_result) 66 | 67 | 68 | if __name__ == "__main__": 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /examples/example7-synth-timing.py: -------------------------------------------------------------------------------- 1 | # # Example 7: Timing Analysis and Optimization 2 | # 3 | # After building a circuit, one might want to analyze or simplify the hardware. PyRTL 4 | # provides this functionality, as demonstrated by this example. 5 | 6 | import pyrtl 7 | 8 | # ## Part 1: Timing Analysis 9 | # 10 | # Timing and area usage are key considerations of any hardware block that one makes. 11 | # PyRTL provides functions to do these operations. 12 | # 13 | # Creating a sample hardware block 14 | pyrtl.reset_working_block() 15 | const_wire = pyrtl.Const(6, bitwidth=4) 16 | in_wire2 = pyrtl.Input(bitwidth=4, name="input2") 17 | out_wire = pyrtl.Output(bitwidth=5, name="output") 18 | out_wire <<= const_wire + in_wire2 19 | 20 | # Now we will do the timing analysis as well as print out the critical path 21 | # 22 | # Generating timing analysis information 23 | print("Pre Synthesis:") 24 | timing = pyrtl.TimingAnalysis() 25 | timing.print_max_length() 26 | 27 | # We are also able to print out the critical paths as well as get them back as an array. 28 | critical_path_info = timing.critical_path() 29 | 30 | # ## Part 2: Area Analysis 31 | # 32 | # PyRTL also provides estimates for the area that would be used up if the circuit was 33 | # printed as an ASIC. 34 | logic_area, mem_area = pyrtl.area_estimation(tech_in_nm=65) 35 | est_area = logic_area + mem_area 36 | print("Estimated Area of block", est_area, "sq mm") 37 | 38 | 39 | # ## Part 3: Synthesis 40 | # 41 | # Synthesis is the operation of reducing the circuit into simpler components. The base 42 | # synthesis function breaks down the more complex logic operations into logic gates 43 | # (keeping registers and memories intact) as well as reduces all combinatorial logic 44 | # into operations that only use 1-bitwidth wires. 45 | # 46 | # This synthesis allows for PyRTL to make optimizations to the net structure as well as 47 | # prepares it for further transformations on the PyRTL toolchain. 48 | pyrtl.synthesize() 49 | 50 | print("\nPre Optimization:") 51 | timing = pyrtl.TimingAnalysis() 52 | timing.print_max_length() 53 | for net in pyrtl.working_block().logic: 54 | print(str(net)) 55 | 56 | 57 | # ## Part 4: Optimization 58 | # 59 | # PyRTL has functions built-in to eliminate unnecessary logic from the circuit. These 60 | # functions are all done with a simple call: 61 | _ = pyrtl.optimize() 62 | 63 | # Now to see the difference 64 | print("\nPost Optimization:") 65 | timing = pyrtl.TimingAnalysis() 66 | timing.print_max_length() 67 | 68 | for net in pyrtl.working_block().logic: 69 | print(str(net)) 70 | 71 | # As we can see, the number of nets in the circuit was drastically reduced by the 72 | # optimization algorithm. 73 | -------------------------------------------------------------------------------- /.github/workflows/python-release.yml: -------------------------------------------------------------------------------- 1 | # See PyRTL's release documentation in docs/release/README.md 2 | # 3 | # This file configures GitHub actions for building distribution archives and 4 | # uploading them to PyPI. 5 | # 6 | # This configuration is based on the "Publishing package distribution releases 7 | # using GitHub Actions" tutorial at 8 | # https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ 9 | 10 | name: Build and publish release 11 | 12 | on: push 13 | 14 | jobs: 15 | # Verify that distribution archives can be built on every push. 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: astral-sh/setup-uv@v6 22 | with: 23 | enable-cache: true 24 | - name: Build distribution archives 25 | run: uv build 26 | - name: Upload distribution archives 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: python-distribution-archives 30 | path: dist/ 31 | compression-level: 0 32 | 33 | # Publish distribution archive to TestPyPI on tag pushes. 34 | publish-testpypi: 35 | # Only publish to TestPyPI on tag pushes. 36 | if: startsWith(github.ref, 'refs/tags/') 37 | needs: 38 | - build 39 | runs-on: ubuntu-latest 40 | environment: 41 | name: testpypi 42 | url: https://test.pypi.org/p/pyrtl 43 | permissions: 44 | # Required for trusted publishing. 45 | id-token: write 46 | 47 | steps: 48 | - name: Download distribution archives 49 | uses: actions/download-artifact@v4 50 | with: 51 | name: python-distribution-archives 52 | path: dist/ 53 | - name: Publish distribution archives 54 | uses: pypa/gh-action-pypi-publish@release/v1 55 | with: 56 | repository-url: https://test.pypi.org/legacy/ 57 | 58 | # Publish distribution archive to PyPI on tag pushes. The 'pypi' environment 59 | # requires manual approval on GitHub, so this job won't start automatically. 60 | publish-pypi: 61 | # Only publish to PyPI on tag pushes. 62 | if: startsWith(github.ref, 'refs/tags/') 63 | needs: 64 | - build 65 | runs-on: ubuntu-latest 66 | environment: 67 | name: pypi 68 | url: https://pypi.org/p/pyrtl 69 | permissions: 70 | # Required for trusted publishing. 71 | id-token: write 72 | 73 | steps: 74 | - name: Download distribution archives 75 | uses: actions/download-artifact@v4 76 | with: 77 | name: python-distribution-archives 78 | path: dist/ 79 | - name: Publish distribution archives 80 | uses: pypa/gh-action-pypi-publish@release/v1 81 | -------------------------------------------------------------------------------- /examples/renderer-demo.py: -------------------------------------------------------------------------------- 1 | # # Render traces with various `WaveRenderer` options. 2 | 3 | # Run this demo to see which options work well in your terminal. 4 | import pyrtl 5 | 6 | 7 | # Make some clock dividers and counters. 8 | def make_clock(period: int): 9 | """Make a clock signal that inverts every `period` cycles.""" 10 | assert period > 0 11 | 12 | # Build a chain of registers. 13 | first_reg = pyrtl.Register(bitwidth=1, name=f"clock_0_{period}", reset_value=1) 14 | last_reg = first_reg 15 | for offset in range(1, period): 16 | reg = pyrtl.Register(bitwidth=1, name=f"clock_{offset}_{period}") 17 | reg.next <<= last_reg 18 | last_reg = reg 19 | 20 | # The first register's input is the inverse of the last register's output. 21 | first_reg.next <<= ~last_reg 22 | return last_reg 23 | 24 | 25 | def make_counter(period: int, bitwidth: int = 2): 26 | """Make a counter that increments every `period` cycles.""" 27 | assert period > 0 28 | 29 | # Build a chain of registers. 30 | first_reg = pyrtl.Register(bitwidth=bitwidth, name=f"counter_0_{period}") 31 | last_reg = first_reg 32 | for offset in range(1, period): 33 | reg = pyrtl.Register(bitwidth=bitwidth, name=f"counter_{offset}_{period}") 34 | reg.next <<= last_reg 35 | last_reg = reg 36 | 37 | # The first register's input is the last register's output plus 1. 38 | first_reg.next <<= last_reg + pyrtl.Const(1) 39 | return last_reg 40 | 41 | 42 | make_clock(period=1) 43 | make_clock(period=2) 44 | make_counter(period=1) 45 | make_counter(period=2) 46 | 47 | # Simulate 20 cycles. 48 | sim = pyrtl.Simulation() 49 | sim.step_multiple(nsteps=20) 50 | 51 | # Render the trace with a variety of rendering options. 52 | renderers = { 53 | "powerline": ( 54 | pyrtl.simulation.PowerlineRendererConstants(), 55 | "Requires a font with powerline glyphs", 56 | ), 57 | "utf-8": ( 58 | pyrtl.simulation.Utf8RendererConstants(), 59 | "Unicode, default non-Windows renderer", 60 | ), 61 | "utf-8-alt": ( 62 | pyrtl.simulation.Utf8AltRendererConstants(), 63 | "Unicode, alternate display option", 64 | ), 65 | "cp437": ( 66 | pyrtl.simulation.Cp437RendererConstants(), 67 | "Code page 437 (8-bit ASCII), default Windows renderer", 68 | ), 69 | "ascii": (pyrtl.simulation.AsciiRendererConstants(), "Basic 7-bit ASCII renderer"), 70 | } 71 | 72 | for name, (constants, notes) in renderers.items(): 73 | print(f"# {notes}") 74 | print(f"export PYRTL_RENDERER={name}\n") 75 | sim.tracer.render_trace( 76 | renderer=pyrtl.simulation.WaveRenderer(constants), repr_func=int 77 | ) 78 | print() 79 | -------------------------------------------------------------------------------- /examples/example5-introspection.py: -------------------------------------------------------------------------------- 1 | # # Example 5: Making use of PyRTL and Introspection. 2 | import pyrtl 3 | 4 | 5 | # The following example shows how PyRTL can be used to make some interesting hardware 6 | # structures using Python introspection. In particular, this example makes a N-stage 7 | # pipeline structure. Any specific pipeline is then a derived class of `SimplePipeline` 8 | # where methods with names starting with `stage` are stages, and new members with names 9 | # not starting with `_` are to be registered for the next stage. 10 | # 11 | # ## Pipeline builder with auto generation of pipeline registers. 12 | class SimplePipeline: 13 | def __init__(self): 14 | self._pipeline_register_map = {} 15 | self._current_stage_num = 0 16 | stage_list = [method for method in dir(self) if method.startswith("stage")] 17 | for stage in sorted(stage_list): 18 | stage_method = getattr(self, stage) 19 | stage_method() 20 | self._current_stage_num += 1 21 | 22 | def __getattr__(self, name): 23 | try: 24 | return self._pipeline_register_map[self._current_stage_num][name] 25 | except KeyError as exc: 26 | msg = ( 27 | f'error, no pipeline register "{name}" defined for stage ' 28 | f"{self._current_stage_num}" 29 | ) 30 | raise pyrtl.PyrtlError(msg) from exc 31 | 32 | def __setattr__(self, name, value): 33 | if name.startswith("_"): 34 | # do not do anything tricky with variables starting with '_' 35 | object.__setattr__(self, name, value) 36 | else: 37 | next_stage = self._current_stage_num + 1 38 | pipereg_id = f"{self._current_stage_num} to {next_stage}" 39 | rname = f"pipereg_{pipereg_id}_name" 40 | new_pipereg = pyrtl.Register(bitwidth=len(value), name=rname) 41 | if next_stage not in self._pipeline_register_map: 42 | self._pipeline_register_map[next_stage] = {} 43 | self._pipeline_register_map[next_stage][name] = new_pipereg 44 | new_pipereg.next <<= value 45 | 46 | 47 | # ## A very simple pipeline to show how registers are inferred. 48 | class SimplePipelineExample(SimplePipeline): 49 | def __init__(self): 50 | self._loopback = pyrtl.WireVector(1, "loopback") 51 | super().__init__() 52 | 53 | def stage0(self): 54 | self.n = ~self._loopback 55 | 56 | def stage1(self): 57 | self.n = self.n 58 | 59 | def stage2(self): 60 | self.n = self.n 61 | 62 | def stage3(self): 63 | self.n = self.n 64 | 65 | def stage4(self): 66 | self._loopback <<= self.n 67 | 68 | 69 | simplepipeline = SimplePipelineExample() 70 | print(pyrtl.working_block()) 71 | # ## Simulation of the core 72 | sim = pyrtl.Simulation() 73 | sim.step_multiple({}, nsteps=15) 74 | sim.tracer.render_trace() 75 | -------------------------------------------------------------------------------- /tests/rtllib/test_barrel.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | import random 3 | import unittest 4 | 5 | import pyrtl 6 | from pyrtl.rtllib.barrel import Direction, barrel_shifter 7 | 8 | 9 | class TestDocTests(unittest.TestCase): 10 | """Test documentation examples.""" 11 | 12 | def test_doctests(self): 13 | failures, tests = doctest.testmod(m=pyrtl.rtllib.barrel) 14 | self.assertGreater(tests, 0) 15 | self.assertEqual(failures, 0) 16 | 17 | 18 | class TestBarrel(unittest.TestCase): 19 | def setUp(self): 20 | pyrtl.reset_working_block() 21 | self.inp_val = pyrtl.Input(8, "inp_val") 22 | self.inp_shift = pyrtl.Input(2, "inp_shift") 23 | self.out_zeros = pyrtl.Output(18, "out_zeros") 24 | self.out_ones = pyrtl.Output(18, "out_ones") 25 | 26 | def test_shift_left(self): 27 | random.seed(777906373) 28 | self.out_zeros <<= barrel_shifter( 29 | self.inp_val, bit_in=0, direction=Direction.LEFT, shift_dist=self.inp_shift 30 | ) 31 | self.out_ones <<= barrel_shifter( 32 | self.inp_val, bit_in=1, direction=Direction.LEFT, shift_dist=self.inp_shift 33 | ) 34 | 35 | sim = pyrtl.Simulation() 36 | vals = [random.randint(0, 20) for v in range(20)] 37 | shifts = [random.randint(0, 3) for s in range(20)] 38 | for i in range(len(vals)): 39 | sim.step({self.inp_val: vals[i], self.inp_shift: shifts[i]}) 40 | base_sum = vals[i] * pow(2, shifts[i]) 41 | self.assertEqual(sim.inspect(self.out_zeros), base_sum) 42 | self.assertEqual( 43 | sim.inspect(self.out_ones), base_sum + pow(2, shifts[i]) - 1 44 | ) 45 | 46 | def test_shift_right(self): 47 | random.seed(777906374) 48 | self.out_zeros <<= barrel_shifter( 49 | self.inp_val, bit_in=0, direction=Direction.RIGHT, shift_dist=self.inp_shift 50 | ) 51 | self.out_ones <<= barrel_shifter( 52 | self.inp_val, bit_in=1, direction=Direction.RIGHT, shift_dist=self.inp_shift 53 | ) 54 | 55 | sim = pyrtl.Simulation() 56 | vals = [random.randint(0, 20) for v in range(20)] 57 | shifts = [random.randint(0, 3) for s in range(20)] 58 | for i in range(len(vals)): 59 | sim.step({self.inp_val: vals[i], self.inp_shift: shifts[i]}) 60 | base_sum = int(vals[i] / pow(2, shifts[i])) 61 | self.assertEqual( 62 | sim.inspect(self.out_zeros), base_sum, f"failed on value {vals[i]}" 63 | ) 64 | extra_sum = sum( 65 | [pow(2, len(self.inp_val) - b - 1) for b in range(shifts[i])] 66 | ) 67 | self.assertEqual( 68 | sim.inspect(self.out_ones), 69 | base_sum + extra_sum, 70 | f"failed on value {vals[i]}", 71 | ) 72 | 73 | 74 | if __name__ == "__main__": 75 | unittest.main() 76 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # PyRTL's Documentation 2 | 3 | PyRTL's documentation is published to [Read the Docs](https://readthedocs.org/) 4 | at https://pyrtl.readthedocs.io/ . There is a 5 | [build dashboard](https://readthedocs.org/projects/pyrtl/builds/) 6 | and the main configuration file is 7 | [`.readthedocs.yaml`](https://github.com/UCSBarchlab/PyRTL/blob/development/.readthedocs.yaml) 8 | in the repository's root directory. 9 | 10 | PyRTL's documentation is in this `docs` directory. It is built with 11 | [Sphinx](https://www.sphinx-doc.org/en/master/), and written in 12 | [reStructuredText](https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html). 13 | The main Sphinx configuration file is 14 | [`docs/conf.py`](https://github.com/UCSBarchlab/PyRTL/blob/development/docs/conf.py). 15 | 16 | Most of PyRTL's documentation is automatically extracted from Python 17 | docstrings, see [docstring 18 | formating](https://www.sphinx-doc.org/en/master/usage/domains/python.html) for 19 | supported directives and fields. Sphinx parses [Python type 20 | annotations](https://docs.python.org/3/library/typing.html), so put type 21 | information in annotations instead of docstrings. 22 | 23 | Follow the instructions on this page to build a local copy of PyRTL's 24 | documentation. This is useful for verifying that PyRTL's documentation still 25 | renders correctly after making a local change. 26 | 27 | There is additional PyRTL documentation in the [`gh-pages` 28 | branch](https://github.com/UCSBarchlab/PyRTL/tree/gh-pages). This additional 29 | documentation is pushed to https://ucsbarchlab.github.io/PyRTL/ by the 30 | `pages-build-deployment` GitHub Action. This additional documentation is 31 | written HTML and is not described further in this README. 32 | 33 | ## Testing Documentation Examples 34 | 35 | PyRTL's documentation contains many examples that are tested with 36 | [`doctest`](https://docs.python.org/3/library/doctest.html). It is important to 37 | test these examples so we can be sure that they keep working as we change the 38 | code. These tests run via test fixtures called `TestDocTest`, see the example 39 | in 40 | [`test_core.py`](https://github.com/UCSBarchlab/PyRTL/blob/development/tests/test_core.py). 41 | 42 | When adding a new `doctest`, you'll need to to add a preceding comment block 43 | that imports PyRTL and resets the working block before running your new 44 | `doctest`. This comment block contains additional code necessary for `doctest` 45 | to successfully run the test, but the lines are commented out because they are 46 | not worth showing in every example. These blocks look like: 47 | 48 | ``` 49 | .. doctest only:: 50 | 51 | >>> import pyrtl 52 | >>> pyrtl.reset_working_block() 53 | ``` 54 | 55 | ## Running Sphinx 56 | 57 | Run Sphinx with the provided 58 | [`justfile`](https://github.com/UCSBarchlab/PyRTL/blob/development/justfile), 59 | from the repository's root directory: 60 | 61 | ```shell 62 | # Run Sphinx to build PyRTL's documentation. 63 | $ uv run just docs 64 | ``` 65 | 66 | This builds a local copy of PyRTL's documentation in `docs/_build/html`. 67 | `docs/_build/html/index.html` is the home page. 68 | -------------------------------------------------------------------------------- /ipynb-examples/example0-minimum-viable-hardware.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | " # A simple combinational logic example.\n", 8 | "\n", 9 | " Create an 8-bit adder that adds `a` and `b`. Check if the sum is greater than `5`.\n" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": { 16 | "collapsed": true 17 | }, 18 | "outputs": [], 19 | "source": [ 20 | "%pip install pyrtl\n", 21 | "\n", 22 | "import pyrtl\n", 23 | "\n", 24 | "pyrtl.reset_working_block()\n" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | " Define [Inputs](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.Input) and [Outputs](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.Output).\n" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "metadata": { 38 | "collapsed": true 39 | }, 40 | "outputs": [], 41 | "source": [ 42 | "a = pyrtl.Input(bitwidth=8, name=\"a\")\n", 43 | "b = pyrtl.Input(bitwidth=8, name=\"b\")\n", 44 | "\n", 45 | "q = pyrtl.Output(bitwidth=8, name=\"q\")\n", 46 | "gt5 = pyrtl.Output(bitwidth=1, name=\"gt5\")\n" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": {}, 52 | "source": [ 53 | " Define the logic that connects the [Inputs](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.Input) to the [Outputs](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.Output).\n" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": null, 59 | "metadata": { 60 | "collapsed": true 61 | }, 62 | "outputs": [], 63 | "source": [ 64 | "sum = a + b # Makes an 8-bit adder.\n", 65 | "q <<= sum # Connects the adder's output to the `q` output pin.\n", 66 | "gt5 <<= sum > 5 # Does a comparison and connects the result to the `gt5` output pin.\n" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | " Simulate various values for `a` and `b` over 5 cycles.\n" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": null, 79 | "metadata": { 80 | "collapsed": true 81 | }, 82 | "outputs": [], 83 | "source": [ 84 | "sim = pyrtl.Simulation()\n", 85 | "sim.step_multiple({\"a\": [0, 1, 2, 3, 4], \"b\": [2, 2, 3, 3, 4]})\n" 86 | ] 87 | }, 88 | { 89 | "cell_type": "markdown", 90 | "metadata": {}, 91 | "source": [ 92 | " Display simulation traces as waveforms.\n" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "metadata": { 99 | "collapsed": true 100 | }, 101 | "outputs": [], 102 | "source": [ 103 | "sim.tracer.render_trace()\n" 104 | ] 105 | } 106 | ], 107 | "metadata": { 108 | "kernelspec": { 109 | "display_name": "Python 3", 110 | "language": "python", 111 | "name": "python3" 112 | }, 113 | "language_info": { 114 | "codemirror_mode": { 115 | "name": "ipython", 116 | "version": 3 117 | }, 118 | "file_extension": ".py", 119 | "mimetype": "text/x-python", 120 | "name": "python", 121 | "nbconvert_exporter": "python", 122 | "pygments_lexer": "ipython3", 123 | "version": "3.6.4" 124 | } 125 | }, 126 | "nbformat": 4, 127 | "nbformat_minor": 2 128 | } -------------------------------------------------------------------------------- /pyrtl/rtllib/barrel.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic shifting is defined in PyRTL's core library, see: 3 | 4 | - :func:`.shift_left_logical` 5 | 6 | - :func:`.shift_right_logical` 7 | 8 | - :func:`.shift_right_arithmetic` 9 | 10 | :func:`barrel_shifter` should only be used when more complex shifting behavior is 11 | required. 12 | """ 13 | 14 | from enum import IntEnum 15 | 16 | from pyrtl.wire import WireVector, WireVectorLike 17 | 18 | 19 | class Direction(IntEnum): 20 | """Assigns names to each shift direction, to improve code readability.""" 21 | 22 | RIGHT = 0 23 | LEFT = 1 24 | 25 | 26 | def barrel_shifter( 27 | bits_to_shift: WireVector, 28 | bit_in: WireVectorLike, 29 | direction: WireVectorLike, 30 | shift_dist: WireVector, 31 | wrap_around=0, 32 | ) -> WireVector: 33 | """Create a barrel shifter. 34 | 35 | .. doctest only:: 36 | 37 | >>> import pyrtl 38 | >>> pyrtl.reset_working_block() 39 | 40 | Example:: 41 | 42 | >>> bits_to_shift = pyrtl.Input(name="input", bitwidth=8) 43 | >>> shift_dist = pyrtl.Input(name="shift_dist", bitwidth=3) 44 | >>> output = pyrtl.Output(name="output") 45 | 46 | >>> output <<= pyrtl.rtllib.barrel.barrel_shifter( 47 | ... bits_to_shift, 48 | ... bit_in=1, 49 | ... direction=pyrtl.rtllib.barrel.Direction.RIGHT, 50 | ... shift_dist=shift_dist) 51 | 52 | >>> sim = pyrtl.Simulation() 53 | >>> sim.step(provided_inputs={"input": 0x55, "shift_dist": 4}) 54 | >>> hex(sim.inspect("output")) 55 | '0xf5' 56 | 57 | :param bits_to_shift: :class:`.WireVector` with the value to shift. 58 | :param bit_in: A 1-bit :class:`.WireVector` representing the value to shift in. 59 | :param direction: A one bit :class:`.WireVector` representing the shift direction 60 | (``0`` = shift right, ``1`` = shift left). If ``direction`` is constant, use 61 | :class:`Direction` to improve code readability (``direction=Direction.RIGHT`` 62 | instead of ``direction=0``). 63 | :param shift_dist: :class:`.WireVector` representing the amount to shift. 64 | :param wrap_around: ****currently not implemented**** 65 | 66 | :return: The shifted :class:`.WireVector`. 67 | """ 68 | from pyrtl import concat, select # just for readability 69 | 70 | if wrap_around != 0: 71 | raise NotImplementedError 72 | 73 | # Implement with logN stages pyrtl.muxing between shifted and un-shifted values 74 | final_width = len(bits_to_shift) 75 | val = bits_to_shift 76 | append_val = bit_in 77 | 78 | for i in range(len(shift_dist)): 79 | shift_amt = pow(2, i) # stages shift 1,2,4,8,... 80 | if shift_amt < final_width: 81 | newval = select( 82 | direction, 83 | concat(val[:-shift_amt], append_val), # shift left 84 | concat(append_val, val[shift_amt:]), 85 | ) # shift right 86 | val = select( 87 | shift_dist[i], 88 | truecase=newval, # if bit of shift is 1, do the shift 89 | falsecase=val, 90 | ) # otherwise, don't 91 | # the value to append grows exponentially, but is capped at full width 92 | append_val = concat(append_val, append_val)[:final_width] 93 | else: 94 | # if we are shifting this much, all the data is gone 95 | val = select( 96 | shift_dist[i], 97 | truecase=append_val, # if bit of shift is 1, do the shift 98 | falsecase=val, 99 | ) # otherwise, don't 100 | 101 | return val 102 | -------------------------------------------------------------------------------- /tests/rtllib/test_libutils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import pyrtl 4 | import pyrtl.rtllib.testingutils as utils 5 | from pyrtl.rtllib import libutils 6 | 7 | 8 | class TestPartitionWire(unittest.TestCase): 9 | def test_successful_partition(self): 10 | w = pyrtl.WireVector(24) 11 | partitioned_w = libutils.partition_wire(w, 4) 12 | self.assertEqual(len(partitioned_w), 6) 13 | for wire in partitioned_w: 14 | self.assertIsInstance(wire, pyrtl.WireVector) 15 | 16 | def test_failing_partition(self): 17 | w = pyrtl.WireVector(14) 18 | with self.assertRaises(pyrtl.PyrtlError): 19 | libutils.partition_wire(w, 4) 20 | 21 | def test_partition_sim(self): 22 | pyrtl.reset_working_block() 23 | wires, vals = utils.make_inputs_and_values(exact_bitwidth=32, num_wires=1) 24 | out_wires = [pyrtl.Output(8, "o" + str(i)) for i in range(4)] 25 | partitioned_w = libutils.partition_wire(wires[0], 8) 26 | for p_wire, o_wire in zip(partitioned_w, out_wires, strict=True): 27 | o_wire <<= p_wire 28 | 29 | out_vals = utils.sim_and_ret_outws(wires, vals) 30 | partitioned_vals = [ 31 | [(val >> i) & 0xFF for i in (0, 8, 16, 24)] for val in vals[0] 32 | ] 33 | true_vals = tuple(zip(*partitioned_vals, strict=True)) 34 | for index, wire in enumerate(out_wires): 35 | self.assertEqual(tuple(out_vals[wire.name]), true_vals[index]) 36 | 37 | 38 | class TestStringConversion(unittest.TestCase): 39 | def test_simple_conversion(self): 40 | self.assertEqual([0xA7, 0x23], libutils.str_to_int_array("a7 23")) 41 | 42 | def test_binary_conversion(self): 43 | result = libutils.str_to_int_array("0100 0110 010", base=2) 44 | self.assertEqual(result, [4, 6, 2]) 45 | 46 | def test_empty(self): 47 | result = libutils.str_to_int_array("") 48 | self.assertEqual(result, []) 49 | 50 | def test_multiline(self): 51 | text = """ 52 | 374 1c 53 | a 54 | 34 76""" 55 | result = libutils.str_to_int_array(text) 56 | self.assertEqual([0x374, 0x1C, 0xA, 0x34, 0x76], result) 57 | 58 | def test_invalid_str(self): 59 | with self.assertRaises(ValueError): 60 | libutils.str_to_int_array("hello") 61 | 62 | def test_invalid_bin_str(self): 63 | with self.assertRaises(ValueError): 64 | libutils.str_to_int_array("0313", 2) 65 | 66 | def test_no_override(self): 67 | with self.assertRaises(ValueError): 68 | libutils.str_to_int_array("0x0313", 2) 69 | 70 | 71 | class TestTwosComp(unittest.TestCase): 72 | def setUp(self): 73 | pyrtl.reset_working_block() 74 | self.in1, self.in2 = (pyrtl.Input(8, "in" + str(i)) for i in range(1, 3)) 75 | self.out = pyrtl.Output(9, "out") 76 | 77 | def test_inverse_functionality(self): 78 | for i in range(20): 79 | self.assertEqual( 80 | i * 3, 81 | libutils.rev_twos_comp_repr(libutils.twos_comp_repr(i * 3, 16), 16), 82 | ) 83 | 84 | def test_low_bw_error(self): 85 | with self.assertRaises(pyrtl.PyrtlError): 86 | libutils.twos_comp_repr(-40, 6) 87 | with self.assertRaises(pyrtl.PyrtlError): 88 | libutils.rev_twos_comp_repr(88, 6) 89 | with self.assertRaises(pyrtl.PyrtlError): 90 | libutils.rev_twos_comp_repr(8, 4) 91 | 92 | def test_twos_comp_sim(self): 93 | self.out <<= self.in1 + self.in2 94 | sim = pyrtl.Simulation() 95 | for i in range(10): 96 | sim.step({"in1": i, "in2": libutils.twos_comp_repr(-2 * i, 8)}) 97 | self.assertEqual(-i, libutils.rev_twos_comp_repr(sim.inspect("out"), 8)) 98 | 99 | 100 | if __name__ == "__main__": 101 | unittest.main() 102 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # See PyRTL's release documentation in docs/release/README.md 2 | # 3 | # This file configures PyRTL's distribution archives, which are built and 4 | # uploaded to PyPI with GitHub actions. 5 | # 6 | # This configuration is roughly based on the "Packaging Python Projects" 7 | # tutorial at 8 | # https://packaging.python.org/en/latest/tutorials/packaging-projects/ 9 | 10 | [build-system] 11 | requires = ["hatchling", "hatch-vcs"] 12 | build-backend = "hatchling.build" 13 | 14 | [project] 15 | name = "pyrtl" 16 | # hatch-vcs determines the version number from the latest git tag. 17 | dynamic = ["version"] 18 | authors = [ 19 | {name="Timothy Sherwood", email="sherwood@cs.ucsb.edu"}, 20 | {name="John Clow"}, 21 | {name="UCSBarchlab"}, 22 | ] 23 | description = "RTL-level Hardware Design and Simulation Toolkit" 24 | readme = "README.md" 25 | license = {file = "LICENSE.md"} 26 | requires-python = ">=3.10" 27 | classifiers = [ 28 | "Development Status :: 4 - Beta", 29 | "Environment :: Console", 30 | "Intended Audience :: Developers", 31 | "Intended Audience :: Education", 32 | "Intended Audience :: Science/Research", 33 | "License :: OSI Approved :: BSD License", 34 | "Natural Language :: English", 35 | "Operating System :: OS Independent", 36 | "Programming Language :: Python :: 3", 37 | "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", 38 | "Topic :: System :: Hardware", 39 | ] 40 | dependencies = [] 41 | 42 | [project.optional-dependencies] 43 | # Required by `input_from_blif`. 44 | blif = ["pyparsing"] 45 | # Required by `block_to_svg`. 46 | svg = ["graphviz"] 47 | 48 | [project.urls] 49 | Homepage = "http://ucsbarchlab.github.io/PyRTL/" 50 | GitHub = "https://github.com/UCSBarchlab/PyRTL" 51 | Documentation = "https://pyrtl.readthedocs.io/" 52 | Changelog = "https://github.com/UCSBarchlab/PyRTL/blob/development/CHANGELOG.md" 53 | 54 | [tool.hatch.version] 55 | source = "vcs" 56 | 57 | [tool.ruff] 58 | exclude = ["ipynb-examples"] 59 | 60 | [tool.ruff.lint] 61 | # See https://docs.astral.sh/ruff/rules/ 62 | select = [ 63 | # pycodestyle 64 | "E", "W", 65 | # Pyflakes 66 | "F", 67 | # pyupgrade 68 | "UP", 69 | # flake8-bugbear 70 | "B", 71 | # flake8-comprehensions 72 | "C4", 73 | # flake8-errmsg 74 | "EM", 75 | # flake8-future-annotations 76 | "FA", 77 | # flake8-return 78 | "RET", 79 | # flake8-simplify 80 | "SIM", 81 | # flake8-unused-arguments 82 | "ARG", 83 | # flynt 84 | "FLY", 85 | # isort 86 | "I", 87 | # pylint 88 | "PLE", "PLW", "PLC", 89 | # refurb 90 | "FURB", 91 | # ruff-specific checks 92 | "RUF" 93 | ] 94 | ignore = [ 95 | # Use ternary operator: 96 | # Resulting code is harder to read. 97 | "SIM108", 98 | # Use a single `with` statement with multiple contexts instead of nested 99 | # `with` statements: 100 | # conditional_assignment is harder to read when combined. 101 | "SIM117", 102 | # Using the global statement to update ___ is discouraged: 103 | # PyRTL uses many globals. 104 | "PLW0603", 105 | # `for` loop variable ___ overwritten by assignment target: 106 | # Fixing this introduces more variables, which is harder to read. 107 | "PLW2901", 108 | # `import` should be at the top-level of a file: 109 | # PyRTL uses many function-level imports to avoid circular dependencies. 110 | "PLC0415", 111 | # String/docstring/comment contains ambiguous 112 | "RUF001", "RUF002", "RUF003", 113 | # `__all__` is not sorted 114 | "RUF022", 115 | ] 116 | 117 | [dependency-groups] 118 | dev = [ 119 | "furo>=2025.9.25", 120 | "graphviz>=0.21", 121 | "pyparsing>=3.2.5", 122 | "pytest>=8.4.2", 123 | "pytest-cov>=7.0.0", 124 | "pytest-xdist>=3.8.0", 125 | "ruff>=0.14.2", 126 | "rust-just>=1.43.0", 127 | "sphinx>=7.4.7", 128 | "sphinx-autodoc-typehints>=2.3.0", 129 | "sphinx-copybutton>=0.5.2", 130 | ] 131 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("..")) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = "PyRTL" 22 | copyright = "2025, Timothy Sherwood" 23 | author = "Timothy Sherwood" 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | master_doc = "index" 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | "sphinx.ext.autodoc", 34 | "sphinx.ext.inheritance_diagram", 35 | "sphinx.ext.intersphinx", 36 | "sphinx.ext.viewcode", 37 | "sphinx_autodoc_typehints", 38 | "sphinx_copybutton", 39 | ] 40 | 41 | graphviz_output_format = "svg" 42 | 43 | # Omit redundant method names in right sidebar (step() instead of Simulation.step()). 44 | toc_object_entries_show_parents = "hide" 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ["_templates"] 48 | 49 | # List of patterns, relative to source directory, that match files and 50 | # directories to ignore when looking for source files. 51 | # This pattern also affects html_static_path and html_extra_path. 52 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 53 | 54 | # Enable links to Python standard library classes (str, list, dict, etc). 55 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} 56 | 57 | # sphinx_copybutton excludes line numbers, prompts, and outputs. 58 | copybutton_exclude = ".linenos, .gp, .go" 59 | 60 | # sphinx-autodoc-typehints configuration: Always display Unions with vertical bars, 61 | # show default values, and don't document :rtype: None. 62 | always_use_bars_union = True 63 | typehints_defaults = "comma" 64 | typehints_document_rtype_none = False 65 | 66 | # -- Options for HTML output ------------------------------------------------- 67 | 68 | # The theme to use for HTML and HTML Help pages. See the documentation for 69 | # a list of builtin themes. 70 | # 71 | html_theme = "furo" 72 | html_theme_options = { 73 | "sidebar_hide_name": True, 74 | # For view/edit this page buttons. 75 | "source_repository": "https://github.com/UCSBarchlab/pyrtl", 76 | "source_branch": "development", 77 | "source_directory": "docs/", 78 | # Add a GitHub repository link to the footer. 79 | "footer_icons": [ 80 | { 81 | "name": "GitHub", 82 | "url": "https://github.com/UCSBarchlab/pyrtl", 83 | "html": """ 84 | 85 | 86 | 87 | """, # noqa: E501 88 | "class": "", 89 | }, 90 | ], 91 | } 92 | html_logo = "brand/pyrtl_logo.png" 93 | 94 | # Force a light blue background color for inheritance-diagrams. The default is 95 | # transparent, which does not work well with Furo's dark mode. 96 | inheritance_graph_attrs = { 97 | "bgcolor": "aliceblue", 98 | } 99 | -------------------------------------------------------------------------------- /docs/helpers.rst: -------------------------------------------------------------------------------- 1 | Helper Functions 2 | ================ 3 | 4 | Cutting and Extending WireVectors 5 | --------------------------------- 6 | 7 | The functions below provide ways of combining, slicing, and extending 8 | :class:`WireVectors<.WireVector>` in ways that are often useful in hardware 9 | design. The functions below extend those member functions of the 10 | :class:`.WireVector` class itself (which provides support for the Python 11 | builtin :func:`len`, slicing e.g. ``wire[3:6]``, 12 | :meth:`~.WireVector.zero_extended`, :meth:`~.WireVector.sign_extended`, and 13 | many operators such as addition and multiplication). 14 | 15 | .. autofunction:: pyrtl.concat 16 | .. autofunction:: pyrtl.concat_list 17 | .. autofunction:: pyrtl.match_bitwidth 18 | .. autofunction:: pyrtl.truncate 19 | .. autofunction:: pyrtl.chop 20 | .. autofunction:: pyrtl.wire_struct 21 | .. autofunction:: pyrtl.wire_matrix 22 | 23 | Coercion to WireVector 24 | ---------------------- 25 | 26 | In PyRTL there is only one function in charge of coercing values into 27 | :class:`WireVectors<.WireVector>`, and that is :func:`.as_wires`. This function 28 | is called in almost all helper functions and classes to manage the mixture of 29 | constants and :class:`WireVectors<.WireVector>` that naturally occur in 30 | hardware development. 31 | 32 | See :ref:`wirevector_coercion` for examples and more details. 33 | 34 | .. autofunction:: pyrtl.as_wires 35 | 36 | Control Flow Hardware 37 | --------------------- 38 | 39 | .. autofunction:: pyrtl.mux 40 | .. autofunction:: pyrtl.select 41 | .. autofunction:: pyrtl.enum_mux 42 | .. autoclass:: pyrtl.helperfuncs.MatchedFields 43 | :members: 44 | :undoc-members: 45 | .. autofunction:: pyrtl.match_bitpattern 46 | .. autofunction:: pyrtl.bitfield_update 47 | .. autofunction:: pyrtl.bitfield_update_set 48 | 49 | Interpreting Vectors of Bits 50 | ---------------------------- 51 | 52 | Under the hood, every single `value` a PyRTL design operates on is a bit vector 53 | (which is, in turn, simply an integer of bounded power-of-two size. 54 | Interpreting these bit vectors as humans, and turning human understandable 55 | values into their corresponding bit vectors, can both be a bit of a pain. The 56 | functions below do not create any hardware but rather help in the process of 57 | reasoning about bit vector representations of human understandable values. 58 | 59 | .. autofunction:: pyrtl.val_to_signed_integer 60 | .. autoclass:: pyrtl.helperfuncs.ValueBitwidthTuple 61 | :members: value, bitwidth 62 | .. autofunction:: pyrtl.infer_val_and_bitwidth 63 | .. autofunction:: pyrtl.val_to_formatted_str 64 | .. autofunction:: pyrtl.formatted_str_to_val 65 | .. autofunction:: pyrtl.log2 66 | 67 | Debugging 68 | --------- 69 | 70 | .. autofunction:: pyrtl.set_debug_mode 71 | .. autofunction:: pyrtl.probe 72 | .. autofunction:: pyrtl.rtl_assert 73 | 74 | Reductions 75 | ---------- 76 | 77 | .. autofunction:: pyrtl.and_all_bits 78 | .. autofunction:: pyrtl.or_all_bits 79 | .. autofunction:: pyrtl.xor_all_bits 80 | .. autofunction:: pyrtl.parity 81 | .. autofunction:: pyrtl.rtl_any 82 | .. autofunction:: pyrtl.rtl_all 83 | 84 | .. _extended_logic_and_arithmetic: 85 | 86 | Extended Logic and Arithmetic 87 | ----------------------------- 88 | 89 | The functions below provide ways of comparing and arithmetically combining 90 | :class:`WireVectors<.WireVector>` in ways that are often useful in hardware 91 | design. The functions below extend those member functions of the 92 | :class:`.WireVector` class itself (which provides support for unsigned 93 | addition, subtraction, multiplication, comparison, and many others). 94 | 95 | .. autofunction:: pyrtl.signed_add 96 | .. autofunction:: pyrtl.signed_sub 97 | .. autofunction:: pyrtl.signed_mult 98 | .. autofunction:: pyrtl.signed_lt 99 | .. autofunction:: pyrtl.signed_le 100 | .. autofunction:: pyrtl.signed_gt 101 | .. autofunction:: pyrtl.signed_ge 102 | .. autofunction:: pyrtl.shift_left_logical 103 | .. autofunction:: pyrtl.shift_left_arithmetic 104 | .. autofunction:: pyrtl.shift_right_logical 105 | .. autofunction:: pyrtl.shift_right_arithmetic 106 | 107 | Encoders and Decoders 108 | --------------------- 109 | 110 | .. autofunction:: pyrtl.one_hot_to_binary 111 | .. autofunction:: pyrtl.binary_to_one_hot 112 | -------------------------------------------------------------------------------- /examples/example2-counter.py: -------------------------------------------------------------------------------- 1 | # # Example 2: A Counter with Ripple Carry Adder. 2 | # 3 | # This next example shows how you make stateful things with registers and more complex 4 | # hardware structures with functions. We generate a 3-bit ripple carry adder, building 5 | # off of the 1-bit adder from Example 1, and then hook it to a register to count 6 | # up modulo 8. 7 | import pyrtl 8 | 9 | 10 | # Let's just dive right in. 11 | # 12 | # A function in PyRTL is nothing special -- it just so happens that the statements it 13 | # encapsulate tell PyRTL to build some hardware. 14 | def one_bit_add( 15 | a: pyrtl.WireVector, b: pyrtl.WireVector, carry_in: pyrtl.WireVector 16 | ) -> tuple[pyrtl.WireVector, pyrtl.WireVector]: 17 | assert len(a) == len(b) == 1 # len returns the bitwidth 18 | sum = a ^ b ^ carry_in 19 | carry_out = a & b | a & carry_in | b & carry_in 20 | return sum, carry_out 21 | 22 | 23 | # If we call `one_bit_add` above with the arguments `x`, `y`, and `z`, it will make a 24 | # one-bit adder to add those values together, returning `WireVectors` for `sum` and 25 | # `carry_out` as applied to `x`, `y`, and `z`. If I call it again on `i`, `j`, and `k` 26 | # it will build a new one-bit adder for those inputs and return the resulting `sum` and 27 | # `carry_out` for that adder. 28 | # 29 | # While PyRTL actually provides an `+` operator for `WireVectors` which generates 30 | # adders, a ripple carry adder is something people can understand easily but has enough 31 | # structure to be mildly interesting. Let's define an adder of arbitrary length 32 | # recursively and (hopefully) Pythonically. More comments after the code. 33 | def ripple_add( 34 | a: pyrtl.WireVector, b: pyrtl.WireVector, carry_in: pyrtl.WireVector = 0 35 | ) -> tuple[pyrtl.WireVector, pyrtl.WireVector]: 36 | a, b = pyrtl.match_bitwidth(a, b) 37 | # This function is a function that allows us to match the bitwidth of multiple 38 | # different wires. By default, it zero extends the shorter bits 39 | if len(a) == 1: 40 | return one_bit_add(a, b, carry_in) 41 | lsb, ripple_carry = one_bit_add(a[0], b[0], carry_in) 42 | msbits, carry_out = ripple_add(a[1:], b[1:], ripple_carry) 43 | return pyrtl.concat(msbits, lsb), carry_out 44 | 45 | 46 | # ## The above code breaks down into two cases: 47 | # 48 | # 1. If `a` is one-bit wide, just do a `one_bit_add`. 49 | # 2. Otherwise, do a `one_bit_add` on the least significant bits, `ripple_add` the rest, 50 | # and then stick the results back together into one `WireVector`. 51 | # 52 | # ## A couple interesting features of PyRTL can be seen here: 53 | # 54 | # * WireVectors can be indexed like lists, with `[0]` accessing the least significant 55 | # bit and `[1:]` accessing the remaining bits. Python slicing is supported, with the 56 | # usual `start:stop:stop` syntax. 57 | # * While you can add two lists together in Python, a `WireVector` + `WireVector` means 58 | # "make an adder". To concatenate the bits of two `WireVectors` one needs to use 59 | # `concat()`. 60 | # * Finally, if we look at `carry_in` it seems to have a default value of the integer 61 | # `0` but is a `WireVector` at other times. Python supports polymorphism throughout 62 | # and PyRTL will cast integers and some other types to `WireVectors` when it can. 63 | # 64 | # Now let's build a 3-bit counter from our N-bit ripple carry adder. 65 | counter = pyrtl.Register(bitwidth=3, name="counter") 66 | sum, carry_out = ripple_add(counter, pyrtl.Const("1'b1")) 67 | counter.next <<= sum 68 | 69 | # ## A few new things in the above code: 70 | # 71 | # * The two remaining types of basic `WireVectors`, `Const` and `Register`, both appear. 72 | # `Const`, unsurprisingly, is just for holding constants (such as the `0` in 73 | # `ripple_add`), but here we create one explicitly with a Verilog-like string which 74 | # includes both the value and the bitwidth. 75 | # * `Registers` are just like `WireVectors`, except their updates are delayed to the 76 | # next clock cycle. This is made explicit in the syntax through the property `.next` 77 | # which should always be set for registers. 78 | # * In this simple example, we make the counter's value on the next cycle equal to the 79 | # counter's value this cycle plus one. 80 | # 81 | # Now let's run the bugger. No need for `Inputs`, as this circuit doesn't have any. 82 | # Finally we'll print the trace to the screen and check that it counts up correctly. 83 | sim = pyrtl.Simulation() 84 | for cycle in range(15): 85 | sim.step() 86 | assert sim.value[counter] == cycle % 8 87 | sim.tracer.render_trace() 88 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. Only 4 | releases published to PyPI are tracked here. No release candidates! 5 | 6 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 7 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## [0.12] - 2025-07-28 10 | 11 | ### Added 12 | 13 | - [`GateGraph`](https://pyrtl.readthedocs.io/en/latest/blocks.html#module-pyrtl.gate_graph) is an alternative PyRTL logic representation, designed to simplify analysis. 14 | 15 | ### Changed 16 | 17 | - Rewrote [`output_to_verilog`](https://pyrtl.readthedocs.io/en/latest/export.html#pyrtl.output_to_verilog) and [`output_verilog_testbench`](https://pyrtl.readthedocs.io/en/latest/export.html#pyrtl.output_verilog_testbench). The new implementation's output should be much easier to read: 18 | - Single-use expressions are inlined. 19 | - Try mangling unusable `WireVector` and `MemBlock` names first, before assigning them entirely new names. 20 | - Add comments to the generated Verilog that show the un-mangled names. 21 | - Many documentation improvements: 22 | - Most methods and functions now have examples. 23 | - Consistently use canonical top-level `pyrtl.*` names, rather than module-level names (`pyrtl.WireVector`, not `pyrtl.wire.WireVector`). 24 | - Enabled `intersphinx` for clickable standard library references (`list`, `dict`, etc). 25 | - Set up `doctest` for examples, to verify that documentation examples still work. 26 | - Switched from `pylint` and `pycodestyle` to `ruff`: 27 | - Applied many `ruff` fixes. 28 | - Reformatted the code with `ruff format`. 29 | - Updated `tox` to run `ruff check` and `ruff format`. 30 | 31 | ### Removed 32 | 33 | - Removed remaining Python 2 support. 34 | 35 | ### Fixed 36 | 37 | - Fixed XOR implementation in `and_inverter_synth` pass ([@EdwinChang24](https://github.com/EdwinChang24)) 38 | - `output_verilog_testbench` should not re-initialize RomBlocks. 39 | - `FastSimulation` was not setting `init_memvalue` correctly (renamed to `SimulationTrace.memory_value_map`). 40 | - Specify bitwidths for Verilog initial register and memory values. They were previously unsized constants, which are implicitly 32-bit signed, which could cause surprises. 41 | 42 | ## [0.11.3] - 2025-06-12 43 | 44 | ### Added 45 | 46 | - An optimization pass to [optimize inverter chains](https://github.com/UCSBarchlab/PyRTL/blob/d5f8dbe53f54e61e1d54722449e4894b885243c7/pyrtl/passes.py#L130) ([@gaborszita](https://github.com/gaborszita)) 47 | - `one_hot_to_binary` encoder ([documentation](https://pyrtl.readthedocs.io/en/latest/helpers.html#pyrtl.one_hot_to_binary)) ([@vaaniarora](https://github.com/vaaniarora)) 48 | - `binary_to_one_hot` decoder ([documentation](https://pyrtl.readthedocs.io/en/latest/helpers.html#pyrtl.binary_to_one_hot)) ([@vaaniarora](https://github.com/vaaniarora)) 49 | 50 | ### Changed 51 | 52 | - More support for signed integers: Signed integers can now be used in `RomBlock`'s 53 | `romdata`, `Simulation`'s `mem_value_map` 54 | - And Verilog-style register reset values ([@PrajwalVandana](https://github.com/PrajwalVandana)) 55 | - Improved documentation: 56 | - [conditional_assignment](https://pyrtl.readthedocs.io/en/latest/basic.html#module-pyrtl.conditional) 57 | - [WireVector equality](https://pyrtl.readthedocs.io/en/latest/basic.html#wirevector-equality) 58 | 59 | ### Fixed 60 | 61 | - Use iteration instead of recursion to avoid stack overflow in `find_producer` ([@gaborszita](https://github.com/gaborszita)) 62 | 63 | ## [0.11.2] - 2024-07-16 64 | 65 | ### Added 66 | 67 | - Added an `initialize_registers` option to `output_to_verilog` 68 | ([documentation](https://pyrtl.readthedocs.io/en/latest/export.html#pyrtl.output_to_verilog)) 69 | 70 | ### Changed 71 | 72 | - Improved handling of signed integers. 73 | 74 | ### Fixed 75 | 76 | - Fixed a `wire_matrix` bug involving single-element matrices of `Inputs` or `Registers`. 77 | 78 | ## [0.11.1] - 2024-04-22 79 | 80 | ### Added 81 | 82 | - Named `WireVector` slices with `wire_struct` and `wire_matrix`. See documentation: 83 | - [wire_struct](https://pyrtl.readthedocs.io/en/latest/helpers.html#pyrtl.wire_struct) 84 | - [wire_matrix](https://pyrtl.readthedocs.io/en/latest/helpers.html#pyrtl.wire_matrix) 85 | 86 | ### Changed 87 | 88 | - Major changes to `render_trace` visualization. See [examples and documentation](https://pyrtl.readthedocs.io/en/latest/simtest.html#wave-renderer) 89 | - Many documentation and release process improvements. 90 | 91 | ### Fixed 92 | 93 | - Python 3.11 compatibility. 94 | 95 | ### Removed 96 | 97 | - Python 2.7 support. 98 | -------------------------------------------------------------------------------- /pyrtl/rtllib/libutils.py: -------------------------------------------------------------------------------- 1 | import pyrtl 2 | 3 | 4 | def match_bitwidth(*args: pyrtl.WireVector): 5 | # TODO: allow for custom bit extension functions 6 | """Matches the bitwidth of all of the input arguments. 7 | 8 | .. WARNING:: 9 | 10 | Use :func:`.match_bitwidth` instead. 11 | 12 | :param args: input arguments 13 | 14 | :return: tuple of ``args`` in order with extended bits 15 | """ 16 | return pyrtl.match_bitwidth(*args) 17 | 18 | 19 | def partition_wire( 20 | wire: pyrtl.WireVector, partition_size: int 21 | ) -> list[pyrtl.WireVector]: 22 | """Partitions a wire into a list of ``N`` wires of size ``partition_size``. 23 | 24 | The ``wire``'s bitwidth must be evenly divisible by ``partition_size``. 25 | 26 | .. WARNING:: 27 | 28 | Use :func:`.wire_matrix` or :func:`.chop` instead. 29 | 30 | :param wire: Wire to partition. 31 | :param partition_size: Integer representing size of each partition. 32 | """ 33 | if len(wire) % partition_size != 0: 34 | msg = ( 35 | f"Wire {wire} cannot be evenly partitioned into items of size " 36 | f"{partition_size}" 37 | ) 38 | raise pyrtl.PyrtlError(msg) 39 | return [ 40 | wire[offset : offset + partition_size] 41 | for offset in range(0, len(wire), partition_size) 42 | ] 43 | 44 | 45 | def str_to_int_array(string, base=16): 46 | """ 47 | Converts a string to an array of integer values according to the base specified (int 48 | numbers must be whitespace delimited). 49 | 50 | Example: ``"13 a3 3c" => [0x13, 0xa3, 0x3c]`` 51 | 52 | .. WARNING:: 53 | 54 | Use a :class:`list` comprehension instead:: 55 | 56 | >>> hex_string = "13 a3 3c" 57 | 58 | >>> int_list = [int(s, base=16) for s in hex_string.split()] 59 | 60 | >>> int_list 61 | [19, 163, 60] 62 | >>> [hex(i) for i in int_list] 63 | ['0x13', '0xa3', '0x3c'] 64 | 65 | 66 | :return: [int] 67 | """ 68 | int_strings = string.split() 69 | return [int(int_str, base) for int_str in int_strings] 70 | 71 | 72 | def twos_comp_repr(val: int, bitwidth: int) -> int: 73 | """Converts a value to its two's-complement (positive) integer representation using 74 | a given bitwidth (only converts the value if it is negative). 75 | 76 | .. WARNING:: 77 | 78 | Use :func:`.infer_val_and_bitwidth` instead. 79 | 80 | :param val: Integer literal to convert to two's complement 81 | :param bitwidth: Size of val in bits 82 | """ 83 | correctbw = abs(val).bit_length() + 1 84 | if bitwidth < correctbw: 85 | msg = "please choose a larger target bitwidth" 86 | raise pyrtl.PyrtlError(msg) 87 | if val >= 0: 88 | return val 89 | return (~abs(val) & (2**bitwidth - 1)) + 1 # flip the bits and add one 90 | 91 | 92 | def rev_twos_comp_repr(val: int, bitwidth: int) -> int: 93 | """Takes a two's-complement represented value and converts it to a signed integer 94 | based on the provided ``bitwidth``. 95 | 96 | .. WARNING:: 97 | 98 | Use :func:`.val_to_signed_integer` instead. 99 | """ 100 | valbl = val.bit_length() 101 | if bitwidth < val.bit_length() or val == 2 ** (bitwidth - 1): 102 | msg = "please choose a larger target bitwidth" 103 | raise pyrtl.PyrtlError(msg) 104 | if bitwidth == valbl: # MSB is a 1, value is negative 105 | return -( 106 | (~val & (2**bitwidth - 1)) + 1 107 | ) # flip the bits, add one, and make negative 108 | return val 109 | 110 | 111 | def _shifted_reg_next(reg: pyrtl.Register, direct: str, num: int = 1): 112 | """Creates a shifted :attr:`.Register.next` property for shifted (left or right) 113 | register. 114 | 115 | Use: ``myReg.next = shifted_reg_next(myReg, 'l', 4)`` 116 | 117 | .. WARNING:: 118 | 119 | Use :func:`.shift_left_logical` or :func:`.shift_right_logical` instead. 120 | 121 | :param direct: Direction of shift, either ``l`` or ``r``. 122 | :param num: Number of bit positions to shift. 123 | 124 | :return: Register containing reg's (shifted) next state 125 | """ 126 | if direct == "l": 127 | if num >= len(reg): 128 | return 0 129 | return pyrtl.concat(reg, pyrtl.Const(0, num)) 130 | if direct == "r": 131 | if num >= len(reg): 132 | return 0 133 | return reg[num:] 134 | msg = "direction must be specified with 'direct' parameter as either 'l' or 'r'" 135 | raise pyrtl.PyrtlError(msg) 136 | -------------------------------------------------------------------------------- /ipynb-examples/renderer-demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | " # Render traces with various `WaveRenderer` options.\n" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | " Run this demo to see which options work well in your terminal.\n" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": { 21 | "collapsed": true 22 | }, 23 | "outputs": [], 24 | "source": [ 25 | "%pip install pyrtl\n", 26 | "\n", 27 | "import pyrtl\n", 28 | "\n", 29 | "\n", 30 | "pyrtl.reset_working_block()\n" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "metadata": {}, 36 | "source": [ 37 | " Make some clock dividers and counters.\n" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "metadata": { 44 | "collapsed": true 45 | }, 46 | "outputs": [], 47 | "source": [ 48 | "def make_clock(period: int):\n", 49 | " \"\"\"Make a clock signal that inverts every `period` cycles.\"\"\"\n", 50 | " assert period > 0\n", 51 | "\n", 52 | " # Build a chain of registers.\n", 53 | " first_reg = pyrtl.Register(bitwidth=1, name=f\"clock_0_{period}\", reset_value=1)\n", 54 | " last_reg = first_reg\n", 55 | " for offset in range(1, period):\n", 56 | " reg = pyrtl.Register(bitwidth=1, name=f\"clock_{offset}_{period}\")\n", 57 | " reg.next <<= last_reg\n", 58 | " last_reg = reg\n", 59 | "\n", 60 | " # The first register's input is the inverse of the last register's output.\n", 61 | " first_reg.next <<= ~last_reg\n", 62 | " return last_reg\n", 63 | "\n", 64 | "\n", 65 | "def make_counter(period: int, bitwidth: int = 2):\n", 66 | " \"\"\"Make a counter that increments every `period` cycles.\"\"\"\n", 67 | " assert period > 0\n", 68 | "\n", 69 | " # Build a chain of registers.\n", 70 | " first_reg = pyrtl.Register(bitwidth=bitwidth, name=f\"counter_0_{period}\")\n", 71 | " last_reg = first_reg\n", 72 | " for offset in range(1, period):\n", 73 | " reg = pyrtl.Register(bitwidth=bitwidth, name=f\"counter_{offset}_{period}\")\n", 74 | " reg.next <<= last_reg\n", 75 | " last_reg = reg\n", 76 | "\n", 77 | " # The first register's input is the last register's output plus 1.\n", 78 | " first_reg.next <<= last_reg + pyrtl.Const(1)\n", 79 | " return last_reg\n", 80 | "\n", 81 | "\n", 82 | "make_clock(period=1)\n", 83 | "make_clock(period=2)\n", 84 | "make_counter(period=1)\n", 85 | "make_counter(period=2)\n" 86 | ] 87 | }, 88 | { 89 | "cell_type": "markdown", 90 | "metadata": {}, 91 | "source": [ 92 | " Simulate 20 cycles.\n" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "metadata": { 99 | "collapsed": true 100 | }, 101 | "outputs": [], 102 | "source": [ 103 | "sim = pyrtl.Simulation()\n", 104 | "sim.step_multiple(nsteps=20)\n" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | " Render the trace with a variety of rendering options.\n" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "metadata": { 118 | "collapsed": true 119 | }, 120 | "outputs": [], 121 | "source": [ 122 | "renderers = {\n", 123 | " \"powerline\": (\n", 124 | " pyrtl.simulation.PowerlineRendererConstants(),\n", 125 | " \"Requires a font with powerline glyphs\",\n", 126 | " ),\n", 127 | " \"utf-8\": (\n", 128 | " pyrtl.simulation.Utf8RendererConstants(),\n", 129 | " \"Unicode, default non-Windows renderer\",\n", 130 | " ),\n", 131 | " \"utf-8-alt\": (\n", 132 | " pyrtl.simulation.Utf8AltRendererConstants(),\n", 133 | " \"Unicode, alternate display option\",\n", 134 | " ),\n", 135 | " \"cp437\": (\n", 136 | " pyrtl.simulation.Cp437RendererConstants(),\n", 137 | " \"Code page 437 (8-bit ASCII), default Windows renderer\",\n", 138 | " ),\n", 139 | " \"ascii\": (pyrtl.simulation.AsciiRendererConstants(), \"Basic 7-bit ASCII renderer\"),\n", 140 | "}\n", 141 | "\n", 142 | "for name, (constants, notes) in renderers.items():\n", 143 | " print(f\"# {notes}\")\n", 144 | " print(f\"export PYRTL_RENDERER={name}\\n\")\n", 145 | " sim.tracer.render_trace(\n", 146 | " renderer=pyrtl.simulation.WaveRenderer(constants), repr_func=int\n", 147 | " )\n", 148 | " print()\n" 149 | ] 150 | } 151 | ], 152 | "metadata": { 153 | "kernelspec": { 154 | "display_name": "Python 3", 155 | "language": "python", 156 | "name": "python3" 157 | }, 158 | "language_info": { 159 | "codemirror_mode": { 160 | "name": "ipython", 161 | "version": 3 162 | }, 163 | "file_extension": ".py", 164 | "mimetype": "text/x-python", 165 | "name": "python", 166 | "nbconvert_exporter": "python", 167 | "pygments_lexer": "ipython3", 168 | "version": "3.6.4" 169 | } 170 | }, 171 | "nbformat": 4, 172 | "nbformat_minor": 2 173 | } -------------------------------------------------------------------------------- /ipynb-examples/example5-introspection.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | " # Example 5: Making use of PyRTL and Introspection.\n" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": { 14 | "collapsed": true 15 | }, 16 | "outputs": [], 17 | "source": [ 18 | "%pip install pyrtl\n", 19 | "\n", 20 | "import pyrtl\n", 21 | "\n", 22 | "\n", 23 | "pyrtl.reset_working_block()\n" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | " The following example shows how PyRTL can be used to make some interesting hardware\n", 31 | " structures using Python introspection. In particular, this example makes a N-stage\n", 32 | " pipeline structure. Any specific pipeline is then a derived class of `SimplePipeline`\n", 33 | " where methods with names starting with `stage` are stages, and new members with names\n", 34 | " not starting with `_` are to be registered for the next stage.\n", 35 | "\n", 36 | " ## Pipeline builder with auto generation of pipeline registers.\n" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": { 43 | "collapsed": true 44 | }, 45 | "outputs": [], 46 | "source": [ 47 | "class SimplePipeline:\n", 48 | " def __init__(self):\n", 49 | " self._pipeline_register_map = {}\n", 50 | " self._current_stage_num = 0\n", 51 | " stage_list = [method for method in dir(self) if method.startswith(\"stage\")]\n", 52 | " for stage in sorted(stage_list):\n", 53 | " stage_method = getattr(self, stage)\n", 54 | " stage_method()\n", 55 | " self._current_stage_num += 1\n", 56 | "\n", 57 | " def __getattr__(self, name):\n", 58 | " try:\n", 59 | " return self._pipeline_register_map[self._current_stage_num][name]\n", 60 | " except KeyError as exc:\n", 61 | " msg = (\n", 62 | " f'error, no pipeline register \"{name}\" defined for stage '\n", 63 | " f\"{self._current_stage_num}\"\n", 64 | " )\n", 65 | " raise pyrtl.PyrtlError(msg) from exc\n", 66 | "\n", 67 | " def __setattr__(self, name, value):\n", 68 | " if name.startswith(\"_\"):\n", 69 | " # do not do anything tricky with variables starting with '_'\n", 70 | " object.__setattr__(self, name, value)\n", 71 | " else:\n", 72 | " next_stage = self._current_stage_num + 1\n", 73 | " pipereg_id = f\"{self._current_stage_num} to {next_stage}\"\n", 74 | " rname = f\"pipereg_{pipereg_id}_name\"\n", 75 | " new_pipereg = pyrtl.Register(bitwidth=len(value), name=rname)\n", 76 | " if next_stage not in self._pipeline_register_map:\n", 77 | " self._pipeline_register_map[next_stage] = {}\n", 78 | " self._pipeline_register_map[next_stage][name] = new_pipereg\n", 79 | " new_pipereg.next <<= value\n" 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "metadata": {}, 85 | "source": [ 86 | " ## A very simple pipeline to show how registers are inferred.\n" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": null, 92 | "metadata": { 93 | "collapsed": true 94 | }, 95 | "outputs": [], 96 | "source": [ 97 | "class SimplePipelineExample(SimplePipeline):\n", 98 | " def __init__(self):\n", 99 | " self._loopback = pyrtl.WireVector(1, \"loopback\")\n", 100 | " super().__init__()\n", 101 | "\n", 102 | " def stage0(self):\n", 103 | " self.n = ~self._loopback\n", 104 | "\n", 105 | " def stage1(self):\n", 106 | " self.n = self.n\n", 107 | "\n", 108 | " def stage2(self):\n", 109 | " self.n = self.n\n", 110 | "\n", 111 | " def stage3(self):\n", 112 | " self.n = self.n\n", 113 | "\n", 114 | " def stage4(self):\n", 115 | " self._loopback <<= self.n\n", 116 | "\n", 117 | "\n", 118 | "simplepipeline = SimplePipelineExample()\n", 119 | "print(pyrtl.working_block())\n" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "metadata": {}, 125 | "source": [ 126 | " ## Simulation of the core\n" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": null, 132 | "metadata": { 133 | "collapsed": true 134 | }, 135 | "outputs": [], 136 | "source": [ 137 | "sim = pyrtl.Simulation()\n", 138 | "sim.step_multiple({}, nsteps=15)\n", 139 | "sim.tracer.render_trace()\n" 140 | ] 141 | } 142 | ], 143 | "metadata": { 144 | "kernelspec": { 145 | "display_name": "Python 3", 146 | "language": "python", 147 | "name": "python3" 148 | }, 149 | "language_info": { 150 | "codemirror_mode": { 151 | "name": "ipython", 152 | "version": 3 153 | }, 154 | "file_extension": ".py", 155 | "mimetype": "text/x-python", 156 | "name": "python", 157 | "nbconvert_exporter": "python", 158 | "pygments_lexer": "ipython3", 159 | "version": "3.6.4" 160 | } 161 | }, 162 | "nbformat": 4, 163 | "nbformat_minor": 2 164 | } -------------------------------------------------------------------------------- /examples/example1-combologic.py: -------------------------------------------------------------------------------- 1 | # # Example 1: A simple combination logic block example. 2 | # 3 | # This example declares a block of hardware with three one-bit inputs, (`a`,`b`,`c`) and 4 | # two one-bit outputs (`sum`, `cout`). The logic declared is a simple one-bit adder and 5 | # the definition uses some of the most common parts of PyRTL. The adder is then 6 | # simulated on random data, the wave form is printed to the screen, and the resulting 7 | # trace is compared to a "correct" addition. 8 | import random 9 | 10 | import pyrtl 11 | 12 | # The basic idea of PyRTL is to specify a hardware block by defining wires and the 13 | # operations performed on those wires. The current working block, an instance of a class 14 | # devilishly named `Block`, is implicit in all of the code below -- it is easiest to 15 | # start with the way wires work. 16 | # 17 | # ## Step 1: Define Logic 18 | # 19 | # One of the most fundamental types in PyRTL is the `WireVector` which acts very much 20 | # like a Python list of 1-bit wires. Unlike a normal list, though, the number of bits is 21 | # explicitly declared. 22 | temp1 = pyrtl.WireVector(bitwidth=1, name="temp1") 23 | 24 | # Both arguments are optional and default to a `bitwidth` of 1 and a unique `name` 25 | # generated by PyRTL that starts with `tmp`. 26 | temp2 = pyrtl.WireVector() 27 | 28 | # `Input` and `Output` are two special types of `WireVectors`, which specify the 29 | # interface to the hardware block. 30 | a = pyrtl.Input(1, "a") 31 | b = pyrtl.Input(1, "b") 32 | c = pyrtl.Input(1, "c") 33 | 34 | sum = pyrtl.Output(1, "sum") 35 | carry_out = pyrtl.Output(1, "carry_out") 36 | 37 | # Okay, let's build a one-bit adder. To do this we need to use the assignment operator, 38 | # which is `<<=`. This takes an already declared wire and "connects" it to another 39 | # already declared wire. Let's start with the `sum` bit, which is of course just the xor 40 | # of the three inputs: 41 | sum <<= a ^ b ^ c 42 | 43 | # The `carry_out` bit would just be `carry_out <<= a & b | a & c | b & c` but let's 44 | # break that down a bit to see what is really happening. What if we want to give names 45 | # to the intermediate signals in the middle of that computation? When you take `a & b` 46 | # in PyRTL, what that really means is "make an AND gate, connect one input to `a` and 47 | # the other to `b` and return the result of the gate". The result of that AND gate can 48 | # then be assigned to `temp1` or it can be used like any other Python variable. 49 | temp1 <<= a & b # connect the result of a & b to the pre-allocated WireVector 50 | temp2 <<= a & c 51 | temp3 = b & c # temp3 IS the result of b & c (this is the first mention of temp3) 52 | carry_out <<= temp1 | temp2 | temp3 53 | 54 | # You can access the working block through `working_block()`, and for most things one 55 | # block is all you will need. Example 2 discusses this in more detail, but for now we 56 | # can just print the block to see that in fact it looks like the hardware we described. 57 | # The format is a bit weird, but roughly translates to a list of gates (the `w` gates 58 | # are just wires). The ins and outs of the gates are formatted as 59 | # `{name}/{bitwidth}{WireVectorType}`. 60 | print("--- One Bit Adder Implementation ---") 61 | print(pyrtl.working_block()) 62 | 63 | # ## Step 2: Simulate Design 64 | # 65 | # Okay, let's simulate our one-bit adder. 66 | sim = pyrtl.Simulation() 67 | 68 | # Now all we need to do is call `step()` to simulate each clock cycle of our design. We 69 | # just need to pass in some input each cycle, which is a dictionary mapping inputs (the 70 | # *names* of the inputs, not the actual instances of `Input`) to their value for that 71 | # signal in the current cycle. In this simple example, we can just specify a random 72 | # value of 0 or 1 with Python's random module. We call step 15 times to simulate 15 73 | # cycles. 74 | for _cycle in range(15): 75 | sim.step( 76 | {"a": random.randrange(2), "b": random.randrange(2), "c": random.randrange(2)} 77 | ) 78 | 79 | # Now all we need to do is print the trace results to the screen. Here we use 80 | # `render_trace()` with some size information. 81 | print("\n--- One Bit Adder Simulation ---") 82 | sim.tracer.render_trace(symbol_len=2) 83 | 84 | a_value = sim.inspect(a) 85 | print("The latest value of 'a' was: ", a_value) 86 | 87 | # ## Step 3: Verification of Simulated Design 88 | # 89 | # Finally, let's check the trace to make sure that `sum` and `carry_out` are actually 90 | # the right values when compared to Python's addition operation. Note that all the 91 | # simulation is done at this point and we are just checking the waveform, but there is 92 | # no reason you could not do this at simulation time if you had a really long-running 93 | # design. 94 | for cycle in range(15): 95 | # Note that we are doing all arithmetic on values, NOT `WireVectors` here. We can 96 | # add the inputs together to get a value for the result: 97 | add_result = ( 98 | sim.tracer.trace["a"][cycle] 99 | + sim.tracer.trace["b"][cycle] 100 | + sim.tracer.trace["c"][cycle] 101 | ) 102 | # We can select off the bits and compare: 103 | python_sum = add_result & 0x1 104 | python_cout = (add_result >> 1) & 0x1 105 | if ( 106 | python_sum != sim.tracer.trace["sum"][cycle] 107 | or python_cout != sim.tracer.trace["carry_out"][cycle] 108 | ): 109 | msg = "This Example is Broken!!!" 110 | raise Exception(msg) 111 | -------------------------------------------------------------------------------- /examples/example1.1-signed-numbers.py: -------------------------------------------------------------------------------- 1 | # # Example 1.1: Working with signed integers. 2 | # 3 | # This example demonstrates: 4 | # * Correct addition of signed integers with `signed_add()`. 5 | # * Displaying signed integers in traces with `val_to_signed_integer()`. 6 | # 7 | # Signed integers are represented in two's complement. 8 | # https://en.wikipedia.org/wiki/Two%27s_complement 9 | import pyrtl 10 | 11 | # ## Let's start with unsigned addition. 12 | # 13 | # Add all combinations of two unsigned 2-bit inputs. 14 | a = pyrtl.Input(bitwidth=2, name="a") 15 | b = pyrtl.Input(bitwidth=2, name="b") 16 | 17 | unsigned_sum = pyrtl.Output(bitwidth=3, name="unsigned_sum") 18 | unsigned_sum <<= a + b 19 | 20 | # Try all combinations of {0, 1, 2, 3} + {0, 1, 2, 3}. 21 | unsigned_inputs = { 22 | "a": [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3], 23 | "b": [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3], 24 | } 25 | 26 | sim = pyrtl.Simulation() 27 | sim.step_multiple(provided_inputs=unsigned_inputs) 28 | 29 | # In this trace, `unsigned_sum` is the sum of all combinations of 30 | # {0, 1, 2, 3} + {0, 1, 2, 3}. For example: 31 | # * cycle 0 shows 0 + 0 = 0 32 | # * cycle 1 shows 0 + 1 = 1 33 | # * cycle 15 shows 3 + 3 = 6 34 | print( 35 | "Unsigned addition. Each cycle adds a different combination of numbers.\n" 36 | "unsigned_sum == a + b" 37 | ) 38 | sim.tracer.render_trace(repr_func=int) 39 | 40 | # ## Re-interpreting `a`, `b`, and `unsigned_sum` as signed is **incorrect**. 41 | # 42 | # Use `val_to_signed_integer()` to re-interpret the previous simulation results 43 | # as signed integers. But the results are **INCORRECT**, because `unsigned_sum` 44 | # performed unsigned addition with `+`. For example: 45 | # 46 | # * cycle 2 shows 0 + -2 = 2 47 | # * cycle 13 shows -1 + 1 = -4 48 | # 49 | # `unsigned_sum` is incorrect because PyRTL must extend `a` and `b` to the 50 | # sum's bitwidth before adding, but PyRTL zero-extends by default, instead of 51 | # sign-extending. Zero-extending is correct when `a` and `b` are unsigned, but 52 | # sign-extending is correct when `a` and `b` are signed. Use `signed_add()` to 53 | # sign-extend `a` and `b`, as demonstrated next. 54 | print( 55 | "\nUse `val_to_signed_integer()` to re-interpret the previous simulation results as" 56 | "\nsigned integers. But re-interpreting the previous trace as signed integers \n" 57 | "produces INCORRECT RESULTS!" 58 | ) 59 | sim.tracer.render_trace(repr_func=pyrtl.val_to_signed_integer) 60 | 61 | # ## Use `signed_add()` to correctly add signed integers. 62 | # 63 | # `signed_add()` works by sign-extending its inputs to the sum's bitwidth before 64 | # adding. There are many `signed_*` functions for signed operations, like 65 | # `signed_sub()`, `signed_mult()`, `signed_lt()`, and so on. 66 | pyrtl.reset_working_block() 67 | 68 | a = pyrtl.Input(bitwidth=2, name="a") 69 | b = pyrtl.Input(bitwidth=2, name="b") 70 | 71 | signed_sum = pyrtl.Output(bitwidth=3, name="signed_sum") 72 | signed_sum <<= pyrtl.signed_add(a, b) 73 | 74 | # Try all combinations of {-2, -1, 0, 1} + {-2, -1, 0, 1}. 75 | signed_inputs = { 76 | "a": [-2, -2, -2, -2, -1, -1, -1, -1, 0, 0, 0, 0, 1, 1, 1, 1], 77 | "b": [-2, -1, 0, 1, -2, -1, 0, 1, -2, -1, 0, 1, -2, -1, 0, 1], 78 | } 79 | sim = pyrtl.Simulation() 80 | sim.step_multiple(provided_inputs=signed_inputs) 81 | 82 | # In this trace, `signed_sum` is the sum of all combinations of 83 | # {-2, -1, 0, 1} + {-2, -1, 0, 1}. For example: 84 | # * cycle 0 shows -2 + -2 = -4 85 | # * cycle 1 shows -2 + -1 = -3 86 | # * cycle 15 shows 1 + 1 = 2 87 | print( 88 | "\nReset the simulation and use `signed_add()` to correctly add signed integers.\n" 89 | "signed_sum == signed_add(a, b)" 90 | ) 91 | sim.tracer.render_trace(repr_func=pyrtl.val_to_signed_integer) 92 | 93 | # ## Manually sign-extend inputs to correctly add signed integers with `+`. 94 | # 95 | # Instead of using `signed_add()`, we can manually sign-extend the inputs with 96 | # `sign_extended()` and `truncate()` the output to correctly add signed integers with 97 | # `+`. Use this trick to implement other signed arithmetic operations. 98 | pyrtl.reset_working_block() 99 | 100 | a = pyrtl.Input(bitwidth=2, name="a") 101 | b = pyrtl.Input(bitwidth=2, name="b") 102 | 103 | # Using `+` produces the correct result for signed addition here because we 104 | # manually extend `a` and `b` to 3 bits. The result of 105 | # 106 | # a.sign_extended(3) + b.sign_extended(3) 107 | # 108 | # is now 4 bits, but we truncate it to 3 bits. This truncation is subtle, but important! 109 | # The addition's full 4 bit result would not be correct. 110 | sign_extended_sum = pyrtl.Output(bitwidth=3, name="sign_extended_sum") 111 | extended_a = a.sign_extended(bitwidth=3) 112 | extended_b = b.sign_extended(bitwidth=3) 113 | sign_extended_sum <<= (extended_a + extended_b).truncate(bitwidth=3) 114 | 115 | # Try all combinations of {-2, -1, 0, 1} + {-2, -1, 0, 1}. 116 | signed_inputs = { 117 | "a": [-2, -2, -2, -2, -1, -1, -1, -1, 0, 0, 0, 0, 1, 1, 1, 1], 118 | "b": [-2, -1, 0, 1, -2, -1, 0, 1, -2, -1, 0, 1, -2, -1, 0, 1], 119 | } 120 | sim = pyrtl.Simulation() 121 | sim.step_multiple(provided_inputs=signed_inputs) 122 | 123 | print( 124 | "\nInstead of using `signed_add()`, we can also manually sign extend the inputs to" 125 | "\ncorrectly add signed integers with `+`.\nsign_extended_sum == " 126 | "(a.sign_extended(3) + b.sign_extended(3)).truncate(3)" 127 | ) 128 | sim.tracer.render_trace(repr_func=pyrtl.val_to_signed_integer) 129 | -------------------------------------------------------------------------------- /examples/example8-verilog.py: -------------------------------------------------------------------------------- 1 | # # Example 8: Interfacing with Verilog. 2 | # 3 | # PyRTL provides utilities to import from, and export to, Verilog. Simulation traces can 4 | # also be exported to `vcd` files. 5 | import io 6 | import random 7 | 8 | import pyrtl 9 | 10 | # ## Importing From Verilog 11 | # 12 | # Sometimes it is useful to pull in components written in Verilog to be used as 13 | # subcomponents of PyRTL designs, or for analysis in PyRTL. PyRTL supports the standard 14 | # `blif` format: https://www.ece.cmu.edu/~ee760/760docs/blif.pdf 15 | # 16 | # Many tools support outputting hardware designs to `blif`, including the open source 17 | # project `yosys`. `blif` files can then be imported either as a string or directly from 18 | # a file name by the function `input_from_blif()`. Here is a simple example of a 1-bit 19 | # full adder imported and then simulated from `blif`: 20 | full_adder_blif = """ 21 | .model full_adder 22 | .inputs x y cin 23 | .outputs sum cout 24 | .names $false 25 | .names $true 26 | 1 27 | .names y $not$FA.v:12$3_Y 28 | 0 1 29 | .names x $not$FA.v:11$1_Y 30 | 0 1 31 | .names cin $not$FA.v:15$6_Y 32 | 0 1 33 | .names ind3 ind4 sum 34 | 1- 1 35 | -1 1 36 | .names $not$FA.v:15$6_Y ind2 ind3 37 | 11 1 38 | .names x $not$FA.v:12$3_Y ind1 39 | 11 1 40 | .names ind2 $not$FA.v:16$8_Y 41 | 0 1 42 | .names cin $not$FA.v:16$8_Y ind4 43 | 11 1 44 | .names x y $and$FA.v:19$11_Y 45 | 11 1 46 | .names ind0 ind1 ind2 47 | 1- 1 48 | -1 1 49 | .names cin ind2 $and$FA.v:19$12_Y 50 | 11 1 51 | .names $and$FA.v:19$11_Y $and$FA.v:19$12_Y cout 52 | 1- 1 53 | -1 1 54 | .names $not$FA.v:11$1_Y y ind0 55 | 11 1 56 | .end 57 | """ 58 | 59 | pyrtl.input_from_blif(full_adder_blif) 60 | # Find the `WireVectors` corresponding to wires named in the `blif` file. 61 | x, y, cin = [pyrtl.working_block().get_wirevector_by_name(s) for s in ["x", "y", "cin"]] 62 | 63 | # Simulate the logic with random input values: 64 | sim = pyrtl.Simulation() 65 | for _cycle in range(15): 66 | sim.step( 67 | {"x": random.randrange(2), "y": random.randrange(2), "cin": random.randrange(2)} 68 | ) 69 | # Only display the `Input` and `Output` `WireVectors` for clarity. 70 | input_vectors = pyrtl.working_block().wirevector_subset(pyrtl.Input) 71 | output_vectors = pyrtl.working_block().wirevector_subset(pyrtl.Output) 72 | sim.tracer.render_trace(trace_list=[*input_vectors, *output_vectors], symbol_len=2) 73 | 74 | # ## Exporting to Verilog 75 | # 76 | # To demonstrate Verilog export, we create a simple 3-bit counter. This is like the 77 | # counter in `example2`, except this one can be reset at any time by asserting `zero`. 78 | pyrtl.reset_working_block() 79 | 80 | zero = pyrtl.Input(1, "zero") 81 | counter_output = pyrtl.Output(3, "counter_output") 82 | counter = pyrtl.Register(3, "counter") 83 | counter.next <<= pyrtl.select(zero, 0, counter + 1) 84 | counter_output <<= counter 85 | 86 | # The `select()` statement resets the `counter` to `0` in the next cycle when the `zero` 87 | # signal goes high, otherwise the `counter`'s next value is just `counter + 1`. 88 | # 89 | # The constants `0` and `1` are automatically `zero_extended()` to the proper length. 90 | # Let's export this resettable counter to a Verilog file and see what is looks like 91 | # (here we are using `StringIO` just to print it to a string for demo purposes; most 92 | # likely you will want to pass a normal `open` file). 93 | print("\n--- PyRTL Representation ---") 94 | print(pyrtl.working_block()) 95 | 96 | print("\n--- Verilog for the Counter ---") 97 | with io.StringIO() as verilog_file: 98 | pyrtl.output_to_verilog(verilog_file) 99 | print(verilog_file.getvalue()) 100 | 101 | print("\n--- Simulation Results ---") 102 | sim = pyrtl.Simulation(tracer=pyrtl.SimulationTrace([counter_output, zero])) 103 | for _cycle in range(15): 104 | sim.step({"zero": random.choice([0, 0, 0, 1])}) 105 | sim.tracer.render_trace() 106 | 107 | # We already did the "hard" work of generating a test input for this simulation, so we 108 | # might want to reuse that work when we take this design through a Verilog toolchain. 109 | # `output_verilog_testbench()` grabs the `Inputs` used in the `SimulationTrace` and sets 110 | # them up in a standard Verilog testbench. 111 | print("\n--- Verilog for the TestBench (first 10 lines) ---") 112 | with io.StringIO() as testbench_file: 113 | pyrtl.output_verilog_testbench( 114 | dest_file=testbench_file, simulation_trace=sim.tracer 115 | ) 116 | for i, line in enumerate(testbench_file.getvalue().split("\n")): 117 | if i == 10: 118 | break 119 | print(line) 120 | print("...") 121 | 122 | # ## Transformations 123 | # 124 | # Now let's talk about transformations of the hardware block. Many times when you are 125 | # doing some hardware-level analysis you might wish to ignore higher level things like 126 | # multi-bit wirevectors, adds, concatenation, etc, and just think about wires and basic 127 | # gates. PyRTL supports "lowering" of designs into this more restricted set of 128 | # functionality though the function `synthesize()`. Once we lower a design to this form 129 | # we can then apply basic optimizations like constant propagation and dead wire 130 | # elimination as well. By printing it out to Verilog we can see exactly how the design 131 | # changed. 132 | print("\n--- Optimized Single-bit Verilog for the Counter ---") 133 | pyrtl.synthesize() 134 | pyrtl.optimize() 135 | 136 | with io.StringIO() as verilog_file: 137 | pyrtl.output_to_verilog(verilog_file) 138 | print(verilog_file.getvalue()) 139 | -------------------------------------------------------------------------------- /tests/rtllib/test_prngs.py: -------------------------------------------------------------------------------- 1 | import random 2 | import unittest 3 | from itertools import islice 4 | 5 | import pyrtl 6 | from pyrtl.rtllib import prngs 7 | 8 | 9 | class TestPrngs(unittest.TestCase): 10 | def setUp(self): 11 | pyrtl.reset_working_block() 12 | 13 | @staticmethod 14 | def fibonacci_lfsr(seed): 15 | lfsr = seed 16 | while True: 17 | bit = (lfsr >> 126 ^ lfsr >> 125) & 1 18 | lfsr = lfsr << 1 | bit 19 | yield bit 20 | 21 | @staticmethod 22 | def xoroshiro128plus(seed): 23 | mask = 2**64 - 1 24 | s = [seed & mask, seed >> 64] 25 | while True: 26 | s0 = s[0] 27 | s1 = s[1] 28 | word = (s1 + s0) & mask 29 | s1 ^= s0 30 | s[0] = (s0 << 55 | s0 >> 9) ^ s1 ^ s1 << 14 31 | s[1] = s1 << 36 | s1 >> 28 32 | yield word 33 | 34 | def test_prng_lfsr(self): 35 | seed = pyrtl.Input(127, "seed") 36 | load, req = pyrtl.Input(1, "load"), pyrtl.Input(1, "req") 37 | rand = pyrtl.Output(128, "rand") 38 | rand <<= prngs.prng_lfsr(128, load, req, seed) 39 | sim = pyrtl.Simulation() 40 | in_vals = [random.randrange(1, 2**127) for i in range(5)] 41 | 42 | for trial in range(5): 43 | true_val = 0 44 | for bit in islice(TestPrngs.fibonacci_lfsr(in_vals[trial]), 128): 45 | true_val = true_val << 1 | bit 46 | sim.step({"load": 1, "req": 0, "seed": in_vals[trial]}) 47 | sim.step({"load": 0, "req": 1, "seed": 0x0}) 48 | sim.step({"load": 0, "req": 0, "seed": 0x0}) 49 | circuit_out = sim.inspect(rand) 50 | self.assertEqual( 51 | circuit_out, 52 | true_val, 53 | f"\nAssertion failed on trial {trial}\n" 54 | f"Expected value: {hex(true_val)}\nGotten value: {hex(circuit_out)}", 55 | ) 56 | 57 | def test_prng_xoroshiro128(self): 58 | seed = pyrtl.Input(128, "seed") 59 | load, req = pyrtl.Input(1, "load"), pyrtl.Input(1, "req") 60 | ready = pyrtl.Output(1, "ready") 61 | rand = pyrtl.Output(128, "rand") 62 | ready_out, rand_out = prngs.prng_xoroshiro128(128, load, req, seed) 63 | ready <<= ready_out 64 | rand <<= rand_out 65 | sim = pyrtl.Simulation() 66 | in_vals = [random.randrange(1, 2**128) for i in range(5)] 67 | 68 | for trial in range(5): 69 | true_val = 0 70 | for word in islice(TestPrngs.xoroshiro128plus(in_vals[trial]), 2): 71 | true_val = true_val << 64 | word 72 | sim.step({"load": 1, "req": 0, "seed": in_vals[trial]}) 73 | sim.step({"load": 0, "req": 1, "seed": 0x0}) 74 | for _cycle in range(2, 4): 75 | sim.step({"load": 0, "req": 0, "seed": 0x0}) 76 | circuit_out = sim.inspect(rand) 77 | self.assertEqual( 78 | circuit_out, 79 | true_val, 80 | f"\nAssertion failed on trial {trial}\n" 81 | f"Expected value: {hex(true_val)}\nGotten value: {hex(circuit_out)}", 82 | ) 83 | 84 | for ready_signal in sim.tracer.trace["ready"][:3]: 85 | self.assertEqual(ready_signal, 0) 86 | self.assertEqual(sim.tracer.trace["ready"][3], 1) 87 | self.assertEqual(sim.tracer.trace["ready"][4], 0) 88 | 89 | def test_csprng_trivium(self): 90 | """ 91 | Trivium test vectors retrived from: 92 | https://www.sheffield.ac.uk/polopoly_fs/1.12164!/file/eSCARGOt_full_datasheet_v1.3.pdf 93 | bit ordering is modified to adapt to the Pyrtl implementation 94 | """ 95 | in_vector = pyrtl.Input(160, "in_vector") 96 | load, req = pyrtl.Input(1, "load"), pyrtl.Input(1, "req") 97 | ready = pyrtl.Output(1, "ready") 98 | out_vector = pyrtl.Output(128, "out_vector") 99 | ready_out, rand_out = prngs.csprng_trivium(128, load, req, in_vector) 100 | ready <<= ready_out 101 | out_vector <<= rand_out 102 | sim = pyrtl.Simulation() 103 | 104 | in_vals = [ 105 | 0x0100000000000000000000000000000000000000, 106 | 0x0A09080706050403020100000000000000000000, 107 | 0xFFFEFDFCFBFAF9F8F7F600000000000000000000, 108 | 0xFAA75401AE5B08B5620FC760F9922BC45DF68F28, 109 | 0xF5A24FFCA95603B05D0ABE57F08922BB54ED861F, 110 | ] 111 | 112 | true_vals = [ 113 | 0x1CD761FFCEB05E39F5B18F5C22042AB0, 114 | 0x372E6B86524AFA71B5FEE86D5CEBB07D, 115 | 0xC100BACA274287277FF49B9FB512AF1C, 116 | 0xCB5996FCFF373A953FC169E899E02F46, 117 | 0xF142D1DF4B36C7652CBA2E4A22EE51A0, 118 | ] 119 | 120 | for trial in range(5): 121 | sim.step({"load": 1, "req": 0, "in_vector": in_vals[trial]}) 122 | for _cycle in range(1, 20): 123 | sim.step({"load": 0, "req": 0, "in_vector": 0x0}) 124 | sim.step({"load": 0, "req": 1, "in_vector": 0x0}) 125 | for _cycle in range(21, 23): 126 | sim.step({"load": 0, "req": 0, "in_vector": 0x0}) 127 | circuit_out = sim.inspect(out_vector) 128 | self.assertEqual( 129 | circuit_out, 130 | true_vals[trial], 131 | f"\nAssertion failed on trial {trial}\n" 132 | f"Expected value: {hex(true_vals[trial])}\n" 133 | f"Gotten value: {hex(circuit_out)}", 134 | ) 135 | 136 | for ready_signal in sim.tracer.trace["ready"][:19]: 137 | self.assertEqual(ready_signal, 0) 138 | self.assertEqual(sim.tracer.trace["ready"][19], 1) 139 | 140 | for ready_signal in sim.tracer.trace["ready"][20:22]: 141 | self.assertEqual(ready_signal, 0) 142 | self.assertEqual(sim.tracer.trace["ready"][22], 1) 143 | self.assertEqual(sim.tracer.trace["ready"][23], 0) 144 | 145 | 146 | if __name__ == "__main__": 147 | unittest.main() 148 | -------------------------------------------------------------------------------- /ipynb-examples/example7-synth-timing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | " # Example 7: Timing Analysis and Optimization\n", 8 | "\n", 9 | " After building a circuit, one might want to analyze or simplify the hardware. PyRTL\n", 10 | " provides this functionality, as demonstrated by this example.\n" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": { 17 | "collapsed": true 18 | }, 19 | "outputs": [], 20 | "source": [ 21 | "%pip install pyrtl\n", 22 | "\n", 23 | "import pyrtl\n", 24 | "\n", 25 | "pyrtl.reset_working_block()\n" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | " ## Part 1: Timing Analysis\n", 33 | "\n", 34 | " Timing and area usage are key considerations of any hardware block that one makes.\n", 35 | " PyRTL provides functions to do these operations.\n", 36 | "\n", 37 | " Creating a sample hardware block\n" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "metadata": { 44 | "collapsed": true 45 | }, 46 | "outputs": [], 47 | "source": [ 48 | "pyrtl.reset_working_block()\n", 49 | "const_wire = pyrtl.Const(6, bitwidth=4)\n", 50 | "in_wire2 = pyrtl.Input(bitwidth=4, name=\"input2\")\n", 51 | "out_wire = pyrtl.Output(bitwidth=5, name=\"output\")\n", 52 | "out_wire <<= const_wire + in_wire2\n" 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "metadata": {}, 58 | "source": [ 59 | " Now we will do the timing analysis as well as print out the critical path\n", 60 | "\n", 61 | " Generating timing analysis information\n" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": { 68 | "collapsed": true 69 | }, 70 | "outputs": [], 71 | "source": [ 72 | "print(\"Pre Synthesis:\")\n", 73 | "timing = pyrtl.TimingAnalysis()\n", 74 | "timing.print_max_length()\n" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | " We are also able to print out the critical paths as well as get them back as an array.\n" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "metadata": { 88 | "collapsed": true 89 | }, 90 | "outputs": [], 91 | "source": [ 92 | "critical_path_info = timing.critical_path()\n" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | " ## Part 2: Area Analysis\n", 100 | "\n", 101 | " PyRTL also provides estimates for the area that would be used up if the circuit was\n", 102 | " printed as an ASIC.\n" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": null, 108 | "metadata": { 109 | "collapsed": true 110 | }, 111 | "outputs": [], 112 | "source": [ 113 | "logic_area, mem_area = pyrtl.area_estimation(tech_in_nm=65)\n", 114 | "est_area = logic_area + mem_area\n", 115 | "print(\"Estimated Area of block\", est_area, \"sq mm\")\n" 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "metadata": {}, 121 | "source": [ 122 | " ## Part 3: Synthesis\n", 123 | "\n", 124 | " Synthesis is the operation of reducing the circuit into simpler components. The base\n", 125 | " synthesis function breaks down the more complex logic operations into logic gates\n", 126 | " (keeping registers and memories intact) as well as reduces all combinatorial logic\n", 127 | " into operations that only use 1-bitwidth wires.\n", 128 | "\n", 129 | " This synthesis allows for PyRTL to make optimizations to the net structure as well as\n", 130 | " prepares it for further transformations on the PyRTL toolchain.\n" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "metadata": { 137 | "collapsed": true 138 | }, 139 | "outputs": [], 140 | "source": [ 141 | "pyrtl.synthesize()\n", 142 | "\n", 143 | "print(\"\\nPre Optimization:\")\n", 144 | "timing = pyrtl.TimingAnalysis()\n", 145 | "timing.print_max_length()\n", 146 | "for net in pyrtl.working_block().logic:\n", 147 | " print(str(net))\n" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | " ## Part 4: Optimization\n", 155 | "\n", 156 | " PyRTL has functions built-in to eliminate unnecessary logic from the circuit. These\n", 157 | " functions are all done with a simple call:\n" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "metadata": { 164 | "collapsed": true 165 | }, 166 | "outputs": [], 167 | "source": [ 168 | "_ = pyrtl.optimize()\n" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "metadata": {}, 174 | "source": [ 175 | " Now to see the difference\n" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": null, 181 | "metadata": { 182 | "collapsed": true 183 | }, 184 | "outputs": [], 185 | "source": [ 186 | "print(\"\\nPost Optimization:\")\n", 187 | "timing = pyrtl.TimingAnalysis()\n", 188 | "timing.print_max_length()\n", 189 | "\n", 190 | "for net in pyrtl.working_block().logic:\n", 191 | " print(str(net))\n" 192 | ] 193 | }, 194 | { 195 | "cell_type": "markdown", 196 | "metadata": {}, 197 | "source": [ 198 | " As we can see, the number of nets in the circuit was drastically reduced by the\n", 199 | " optimization algorithm.\n" 200 | ] 201 | } 202 | ], 203 | "metadata": { 204 | "kernelspec": { 205 | "display_name": "Python 3", 206 | "language": "python", 207 | "name": "python3" 208 | }, 209 | "language_info": { 210 | "codemirror_mode": { 211 | "name": "ipython", 212 | "version": 3 213 | }, 214 | "file_extension": ".py", 215 | "mimetype": "text/x-python", 216 | "name": "python", 217 | "nbconvert_exporter": "python", 218 | "pygments_lexer": "ipython3", 219 | "version": "3.6.4" 220 | } 221 | }, 222 | "nbformat": 4, 223 | "nbformat_minor": 2 224 | } -------------------------------------------------------------------------------- /examples/example6-memory.py: -------------------------------------------------------------------------------- 1 | # # Example 6: Memories in PyRTL 2 | # 3 | # One important part of many circuits is the ability to have data in locations that are 4 | # persistent over clock cycles. Previous examples have used `Register` `WireVectors`, 5 | # which are great for storing a small amount of data for a single clock cycle. However, 6 | # PyRTL also has other ways to store data, namely `MemBlocks` and `RomBlocks`. 7 | 8 | import random 9 | 10 | import pyrtl 11 | 12 | # ## Part 1: `MemBlocks` 13 | # 14 | # `MemBlocks` are a way to store multiple sets of data for extended periods of time. 15 | # Below we will make two `MemBlocks` and test that they behave identically given the 16 | # same inputs. 17 | mem1 = pyrtl.MemBlock(bitwidth=32, addrwidth=3, name="mem1") 18 | mem2 = pyrtl.MemBlock(32, 3, "mem2") 19 | 20 | # One memory will receive the write address from an `Input`, the other, a `Register`: 21 | waddr = pyrtl.Input(3, "waddr") 22 | count = pyrtl.Register(3, "count") 23 | 24 | # To make sure that the two memories take the same `Inputs`, we will use same write 25 | # data, write enable, and read addr values: 26 | wdata = pyrtl.Input(32, "wdata") 27 | we = pyrtl.Input(1, "we") 28 | raddr = pyrtl.Input(3, "raddr") 29 | 30 | # We will be grabbing data from each of the two `MemBlocks` so we need two different 31 | # output wires to see the results: 32 | rdata1 = pyrtl.Output(32, "rdata1") 33 | rdata2 = pyrtl.Output(32, "rdata2") 34 | 35 | # ### Ports 36 | # 37 | # Data is read from and written to `MemBlocks` via ports. There are two types of ports: 38 | # read ports and write ports. Each memory can have multiple read and write ports, but a 39 | # `MemBlock` must have at least one of each. Below, we will make one read port for each 40 | # of the two memories: 41 | 42 | rdata1 <<= mem1[raddr] 43 | rdata2 <<= mem2[raddr] 44 | 45 | # ### Write Enable Bit 46 | # 47 | # For the write ports, we will do something different. Sometimes you don't want the 48 | # memories to always accept the data and address on the write port. The write enable bit 49 | # allows us to disable the write port as long as the enable bit's value is zero, giving 50 | # us complete control over whether the `MemBlock` will accept the data. 51 | mem1[waddr] <<= pyrtl.MemBlock.EnabledWrite(wdata, we) # Uses input wire 52 | mem2[count] <<= pyrtl.MemBlock.EnabledWrite(wdata, we) # Uses count register 53 | 54 | # Now we will finish up the circuit. We will increment the `count` register on each 55 | # write 56 | count.next <<= pyrtl.select(we, truecase=count + 1, falsecase=count) 57 | 58 | # We will also verify that the two write addresses are always the same 59 | validate = pyrtl.Output(1, "validate") 60 | validate <<= waddr == count 61 | 62 | # Now it is time to simulate the circuit. First we will set up the values for all of the 63 | # inputs. Write 1 through 8 into the eight addresses (`addrwidth == 3`), then read the 64 | # data back out: 65 | simvals = { 66 | "we": "00111111110000000000000000", 67 | "waddr": "00012345670000000000000000", 68 | "wdata": "00123456789990000000000000", 69 | "raddr": "00000000000000000123456777", 70 | } 71 | 72 | # For simulation purposes, we can give the spots in memory an initial value. Note that 73 | # in the actual circuit, the values are initially undefined. Below, we are building the 74 | # data with which to initialize memory. 75 | mem1_init = dict.fromkeys(range(8), 9) 76 | mem2_init = dict.fromkeys(range(8), 9) 77 | memvals = {mem1: mem1_init, mem2: mem2_init} 78 | 79 | # Now run the simulation like before. Note the adding of the `memory_value_map`. 80 | print("---------MemBlocks----------") 81 | print(pyrtl.working_block()) 82 | sim = pyrtl.Simulation(memory_value_map=memvals) 83 | sim.step_multiple(simvals) 84 | sim.tracer.render_trace() 85 | 86 | # Cleanup in preparation for the `RomBlock` example 87 | pyrtl.reset_working_block() 88 | 89 | 90 | # ## Part 2: RomBlocks 91 | # 92 | # `RomBlocks` are another type of memory. Unlike `MemBlocks`, `RomBlocks` are read-only 93 | # and therefore only have read ports. They are used to store predefined data. 94 | # 95 | # There are two different ways to define the data stored in a `RomBlock`, either through 96 | # passing a function or though a list or tuple: 97 | def rom_data_func(address: int) -> int: 98 | return 31 - 2 * address 99 | 100 | 101 | rom_data_array = [rom_data_func(a) for a in range(16)] 102 | 103 | # Now we will make the `RomBlocks`. `RomBlocks` are similar to `MemBlocks`, but because 104 | # they are read-only, they also need to be passed a set of data to be initialized as. 105 | rom1 = pyrtl.RomBlock(bitwidth=5, addrwidth=4, romdata=rom_data_func) 106 | rom2 = pyrtl.RomBlock(5, 4, rom_data_array) 107 | 108 | rom_add_1, rom_add_2 = pyrtl.Input(4, "rom_in"), pyrtl.Input(4, "rom_in_2") 109 | 110 | rom_out_1, rom_out_2 = pyrtl.Output(5, "rom_out_1"), pyrtl.Output(5, "rom_out_2") 111 | rom_out_3, cmp_out = pyrtl.Output(5, "rom_out_3"), pyrtl.Output(1, "cmp_out") 112 | 113 | # Because `Output` `WireVectors` cannot be used as the source for other nets, in order 114 | # to use the `RomBlock` outputs in two different places, we must instead assign them to 115 | # a temporary variable. 116 | temp1 = rom1[rom_add_1] 117 | temp2 = rom2[rom_add_1] 118 | 119 | rom_out_3 <<= rom2[rom_add_2] 120 | 121 | # Now we connect the rest of the outputs together: 122 | rom_out_1 <<= temp1 123 | rom_out_2 <<= temp2 124 | 125 | cmp_out <<= temp1 == temp2 126 | 127 | # Repeatability is very useful, but we also don't want the hassle of typing out a set of 128 | # values to test. One approach is to use a fixed `random` seed: 129 | print("\n---------RomBlocks----------") 130 | print(pyrtl.working_block()) 131 | random.seed(4839483) 132 | 133 | # Now we will create a new set of simulation values. In this case, since we want to use 134 | # simulation values that are larger than `9` we cannot use the trick used in previous 135 | # examples to parse values, so we pass lists of integers instead. 136 | simvals = { 137 | "rom_in": [1, 11, 4, 2, 7, 8, 2, 4, 5, 13, 15, 3, 4, 4, 4, 8, 12, 13, 2, 1], 138 | "rom_in_2": [random.randrange(16) for i in range(20)], 139 | } 140 | 141 | # Now run the simulation like before. Note that for `RomBlocks`, we do not supply a 142 | # `memory_value_map` because `RomBlock` get their data when they are constructed via 143 | # `romdata`. 144 | sim = pyrtl.Simulation() 145 | sim.step_multiple(simvals) 146 | sim.tracer.render_trace() 147 | -------------------------------------------------------------------------------- /pyrtl/__init__.py: -------------------------------------------------------------------------------- 1 | # Import order matters in this file. 2 | # isort: skip_file 3 | 4 | # error types thrown 5 | from .pyrtlexceptions import PyrtlError, PyrtlInternalError 6 | 7 | # core rtl constructs 8 | from .core import ( 9 | LogicNet, 10 | Block, 11 | PostSynthBlock, 12 | working_block, 13 | reset_working_block, 14 | set_working_block, 15 | temp_working_block, 16 | set_debug_mode, 17 | ) 18 | 19 | # convenience classes for building hardware 20 | from .wire import WireVector, Input, Output, Const, Register 21 | 22 | from .gate_graph import GateGraph, Gate 23 | 24 | # helper functions 25 | from .helperfuncs import ( 26 | input_list, 27 | output_list, 28 | register_list, 29 | wirevector_list, 30 | log2, 31 | truncate, 32 | match_bitpattern, 33 | bitpattern_to_val, 34 | chop, 35 | val_to_signed_integer, 36 | val_to_formatted_str, 37 | formatted_str_to_val, 38 | infer_val_and_bitwidth, 39 | probe, 40 | rtl_assert, 41 | check_rtl_assertions, 42 | find_loop, 43 | find_and_print_loop, 44 | wire_struct, 45 | wire_matrix, 46 | one_hot_to_binary, 47 | binary_to_one_hot, 48 | ) 49 | 50 | from .corecircuits import ( 51 | and_all_bits, 52 | or_all_bits, 53 | xor_all_bits, 54 | rtl_any, 55 | rtl_all, 56 | mux, 57 | select, 58 | concat, 59 | concat_list, 60 | parity, 61 | tree_reduce, 62 | as_wires, 63 | match_bitwidth, 64 | enum_mux, 65 | bitfield_update, 66 | bitfield_update_set, 67 | signed_add, 68 | signed_sub, 69 | signed_mult, 70 | signed_lt, 71 | signed_le, 72 | signed_gt, 73 | signed_ge, 74 | shift_left_arithmetic, 75 | shift_right_arithmetic, 76 | shift_left_logical, 77 | shift_right_logical, 78 | ) 79 | 80 | # memory blocks 81 | from .memory import MemBlock, RomBlock 82 | 83 | # conditional updates 84 | from .conditional import conditional_assignment, otherwise, currently_under_condition 85 | 86 | # block simulation support 87 | from .simulation import Simulation, FastSimulation, SimulationTrace, enum_name 88 | from .compilesim import CompiledSimulation 89 | 90 | # block visualization output formats 91 | from .visualization import ( 92 | output_to_trivialgraph, 93 | graphviz_detailed_namer, 94 | output_to_graphviz, 95 | output_to_svg, 96 | block_to_graphviz_string, 97 | block_to_svg, 98 | trace_to_html, 99 | net_graph, 100 | ) 101 | 102 | # import from and export to file format routines 103 | from .importexport import ( 104 | input_from_verilog, 105 | output_to_verilog, 106 | output_verilog_testbench, 107 | input_from_blif, 108 | output_to_firrtl, 109 | input_from_iscas_bench, 110 | ) 111 | 112 | # different transform passes 113 | from .passes import ( 114 | common_subexp_elimination, 115 | constant_propagation, 116 | synthesize, 117 | nand_synth, 118 | and_inverter_synth, 119 | optimize, 120 | one_bit_selects, 121 | two_way_concat, 122 | direct_connect_outputs, 123 | two_way_fanout, 124 | ) 125 | 126 | from .transform import ( 127 | net_transform, 128 | wire_transform, 129 | copy_block, 130 | clone_wire, 131 | replace_wires, 132 | replace_wire_fast, 133 | ) 134 | 135 | # analysis and estimation functions 136 | from .analysis import ( 137 | area_estimation, 138 | TimingAnalysis, 139 | yosys_area_delay, 140 | paths, 141 | distance, 142 | fanout, 143 | ) 144 | 145 | __all__ = [ 146 | # pyrtlexceptions 147 | "PyrtlError", 148 | "PyrtlInternalError", 149 | # core 150 | "LogicNet", 151 | "Block", 152 | "PostSynthBlock", 153 | "working_block", 154 | "reset_working_block", 155 | "set_working_block", 156 | "temp_working_block", 157 | "set_debug_mode", 158 | # wire 159 | "WireVector", 160 | "Input", 161 | "Output", 162 | "Const", 163 | "Register", 164 | # gate_graph 165 | "GateGraph", 166 | "Gate", 167 | # helperfuncs 168 | "input_list", 169 | "output_list", 170 | "register_list", 171 | "wirevector_list", 172 | "log2", 173 | "truncate", 174 | "match_bitpattern", 175 | "bitpattern_to_val", 176 | "chop", 177 | "val_to_signed_integer", 178 | "val_to_formatted_str", 179 | "formatted_str_to_val", 180 | "infer_val_and_bitwidth", 181 | "probe", 182 | "rtl_assert", 183 | "check_rtl_assertions", 184 | "find_loop", 185 | "find_and_print_loop", 186 | "wire_struct", 187 | "wire_matrix", 188 | "one_hot_to_binary", 189 | "binary_to_one_hot", 190 | # corecircuits 191 | "and_all_bits", 192 | "or_all_bits", 193 | "xor_all_bits", 194 | "rtl_any", 195 | "rtl_all", 196 | "mux", 197 | "select", 198 | "concat", 199 | "concat_list", 200 | "parity", 201 | "tree_reduce", 202 | "as_wires", 203 | "match_bitwidth", 204 | "enum_mux", 205 | "bitfield_update", 206 | "bitfield_update_set", 207 | "signed_add", 208 | "signed_sub", 209 | "signed_mult", 210 | "signed_lt", 211 | "signed_le", 212 | "signed_gt", 213 | "signed_ge", 214 | "shift_left_arithmetic", 215 | "shift_right_arithmetic", 216 | "shift_left_logical", 217 | "shift_right_logical", 218 | # memory 219 | "MemBlock", 220 | "RomBlock", 221 | # conditional 222 | "conditional_assignment", 223 | "otherwise", 224 | "currently_under_condition", 225 | # simulation 226 | "Simulation", 227 | "FastSimulation", 228 | "SimulationTrace", 229 | "enum_name", 230 | # compilesim 231 | "CompiledSimulation", 232 | # visualization 233 | "output_to_trivialgraph", 234 | "graphviz_detailed_namer", 235 | "output_to_graphviz", 236 | "output_to_svg", 237 | "block_to_graphviz_string", 238 | "block_to_svg", 239 | "trace_to_html", 240 | "net_graph", 241 | # importexport 242 | "input_from_verilog", 243 | "output_to_verilog", 244 | "output_verilog_testbench", 245 | "input_from_blif", 246 | "output_to_firrtl", 247 | "input_from_iscas_bench", 248 | # passes 249 | "common_subexp_elimination", 250 | "constant_propagation", 251 | "synthesize", 252 | "nand_synth", 253 | "and_inverter_synth", 254 | "optimize", 255 | "one_bit_selects", 256 | "two_way_concat", 257 | "direct_connect_outputs", 258 | "two_way_fanout", 259 | # transform 260 | "net_transform", 261 | "wire_transform", 262 | "copy_block", 263 | "clone_wire", 264 | "replace_wires", 265 | "replace_wire_fast", 266 | # analysis 267 | "area_estimation", 268 | "TimingAnalysis", 269 | "yosys_area_delay", 270 | "paths", 271 | "distance", 272 | "fanout", 273 | ] 274 | -------------------------------------------------------------------------------- /examples/example3-statemachine.py: -------------------------------------------------------------------------------- 1 | # # Example 3: A State Machine built with `conditional_assignment` 2 | # 3 | # In this example we describe how `conditional_assignment` works in the context of a 4 | # vending machine that will dispense an item when it has received 4 tokens. If a refund 5 | # is requested, it returns the tokens. 6 | import enum 7 | 8 | import pyrtl 9 | 10 | # Define `Inputs`, `Outputs`, and a `Register` to keep track of the vending machine's 11 | # current state. 12 | token_in = pyrtl.Input(1, "token_in") 13 | req_refund = pyrtl.Input(1, "req_refund") 14 | 15 | dispense = pyrtl.Output(1, "dispense") 16 | refund = pyrtl.Output(1, "refund") 17 | 18 | state = pyrtl.Register(3, "state") 19 | 20 | 21 | # First new step, let's enumerate a set of constants to serve as our states 22 | class State(enum.IntEnum): 23 | WAIT = 0 # Waiting for first token. 24 | TOK1 = 1 # Received first token, waiting for second token. 25 | TOK2 = 2 # Received second token, waiting for third token. 26 | TOK3 = 3 # Received third token, waiting for fourth token. 27 | DISP = 4 # Received fourth token, dispense item. 28 | RFND = 5 # Issue refund. 29 | 30 | 31 | # Now we could build a state machine using just the `Registers` and logic discussed in 32 | # prior examples, but doing operations **conditionally** on some input is a pretty 33 | # fundamental operation in hardware design. PyRTL provides `conditional_assignment` to 34 | # provide a predicated update to `Registers`, `WireVectors`, and `MemBlocks`. 35 | # 36 | # `conditional_assignments` are specified with the `|=` operator instead of the usual 37 | # `<<=` operator. The `conditional_assignment` is only valid in the context of a 38 | # condition, and updates to those values only happens when that condition is `True`. In 39 | # hardware this is implemented with a simple `mux()` -- for people coming from software 40 | # it is important to remember that this is describing a big logic function, **NOT** an 41 | # "if-then-else" clause. The `conditional_assignment` below just builds hardware 42 | # multiplexers. Nothing conditional actually happens until the `Simulation` is run. 43 | # 44 | # One more thing: `conditional_assignment` might not always be the best solution. For 45 | # simple updates, a regular 46 | # 47 | # select(sel_wire, truecase=t_wire, falsecase=f_wire) 48 | # 49 | # can be easier to read. 50 | with pyrtl.conditional_assignment: 51 | with req_refund: # signal of highest precedence 52 | state.next |= State.RFND 53 | with token_in: # if token received, advance state in counter sequence 54 | with state == State.WAIT: 55 | state.next |= State.TOK1 56 | with state == State.TOK1: 57 | state.next |= State.TOK2 58 | with state == State.TOK2: 59 | state.next |= State.TOK3 60 | with state == State.TOK3: 61 | state.next |= State.DISP # 4th token received, go to dispense 62 | with pyrtl.otherwise: # token received in unsupported state 63 | state.next |= State.RFND 64 | # unconditional transition from these two states back to wait state 65 | 66 | # NOTE: the parens are needed because in Python the "|" operator is lower 67 | # precedence than the "==" operator! 68 | with (state == State.DISP) | (state == State.RFND): 69 | state.next |= State.WAIT 70 | 71 | dispense <<= state == State.DISP 72 | refund <<= state == State.RFND 73 | 74 | # A few more notes: 75 | # 76 | # 1. A condition can be nested within another condition and the implied hardware is that 77 | # the left-hand-side should only get that value if ALL of the encompassing conditions 78 | # are satisfied. 79 | # 2. Only one conditional at each level can be `True`, so all conditions imply that NONE 80 | # of the prior conditions at the same level are `True`. The highest priority 81 | # condition is listed first, and in a sense you can think about each other condition 82 | # as an `elif`. 83 | # 3. If a `WireVector`'s value is not specified for some combination of conditions, 84 | # `conditional_assignment` will supply a default value. By default,`Registers` will 85 | # retain their value from the prior cycle ("state.next |= state" in this example). 86 | # `WireVectors` default to `0`. 87 | # 4. There is a way to specify something like an `else` instead of `elif` and that is 88 | # with an `otherwise` (as seen on the line above "state.next <<= State.RFND"). This 89 | # condition will be `True` if none of the other conditions at the same level were 90 | # also `True`. For this example specifically, `state.next` will get `RFND` when 91 | # `req_refund==0`, and `token_in==1`, and state is not any of {`WAIT`, `TOK1`, 92 | # `TOK2`, or `TOK3`}. 93 | # 5. Not shown here, but you can update multiple `Registers`, `WireVectors`, and 94 | # `MemBlocks` within one `conditional_assignment`. 95 | # 96 | # A more artificial example might make it even more clear how these rules interact: 97 | # 98 | # with a: 99 | # r.next |= 1 # ← When `a` is `True`. 100 | # with d: 101 | # r2.next |= 2 # ← When `a` is `True` and `d` is `True`. 102 | # with pyrtl.otherwise: 103 | # r2.next |= 3 # ← When `a` is `True` and `d` is `False`. 104 | # with b == c: 105 | # r.next |= 0 # ← When `a` is `False` and `b == c`. 106 | # 107 | # Now let's build and test our state machine. 108 | sim = pyrtl.Simulation() 109 | 110 | # Rather than just give some random inputs, let's specify some specific 1-bit values. To 111 | # make it easier to `Simulate` over several steps, we'll use `step_multiple()`, which 112 | # takes in a `dict` mapping each `Input` to its value in each cycle. 113 | sim_inputs = {"token_in": "0010100111010000", "req_refund": "1100010000000000"} 114 | sim.step_multiple(sim_inputs) 115 | 116 | # Also, to make our input/output easy to reason about let's specify an order to the 117 | # traces with `trace_list`. We also use `enum_name` to display the state names (`WAIT`, 118 | # `TOK1`, ...) rather than their numbers (0, 1, ...). 119 | sim.tracer.render_trace( 120 | trace_list=["token_in", "req_refund", "state", "dispense", "refund"], 121 | repr_per_name={"state": pyrtl.enum_name(State)}, 122 | ) 123 | 124 | # Finally, suppose you want to simulate your design and verify its output matches your 125 | # expectations. `step_multiple()` also accepts as a second argument a `dict` mapping 126 | # output wires to their expected value in each cycle. If during the `Simulation` the 127 | # actual and expected values differ, it will be reported to you! This might be useful if 128 | # you have a working design which, after some tweaks, you'd like to test for functional 129 | # equivalence, or as a basic sanity check. 130 | expected_sim_outputs = {"dispense": "0000000000001000", "refund": "0111001000000000"} 131 | sim = pyrtl.Simulation() 132 | sim.step_multiple(sim_inputs, expected_sim_outputs) 133 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | PYRTL 3 | ===== 4 | 5 | A collection of classes providing simple `RTL 6 | `_ specification, 7 | simulation, tracing, and testing suitable for teaching and research. 8 | Simplicity, usability, clarity, and extensibility rather than performance or 9 | optimization is the overarching goal. With PyRTL you can use the full power of 10 | Python to describe complex synthesizable digital designs, simulate and test 11 | them, and export them to `Verilog `_. 12 | 13 | Quick links 14 | =========== 15 | * Get an overview from the `PyRTL Project Webpage `_ 16 | * Read through `Example PyRTL Code `_ 17 | * File a `Bug or Issue Report `_ 18 | * Contribute to project on `GitHub `_ 19 | 20 | Installation 21 | ============ 22 | 23 | **Automatic installation**:: 24 | 25 | pip install pyrtl 26 | 27 | PyRTL is listed in `PyPI `_ and can be 28 | installed with :program:`pip` or :program:`pip3`. 29 | 30 | Design, Simulate, and Inspect in 15 lines 31 | ========================================= 32 | 33 | .. code-block:: 34 | :linenos: 35 | 36 | import pyrtl 37 | 38 | a = pyrtl.Input(8,'a') # input "pins" 39 | b = pyrtl.Input(8,'b') 40 | q = pyrtl.Output(8,'q') # output "pins" 41 | gt5 = pyrtl.Output(1,'gt5') 42 | 43 | result = a + b # makes an 8-bit adder 44 | q <<= result # assigns output of adder to out pin 45 | gt5 <<= result > 5 # does a comparison, assigns that to different pin 46 | 47 | # simulate and output the resulting waveform to the terminal 48 | sim = pyrtl.Simulation() 49 | sim.step_multiple({'a':[0,1,2,3,4], 'b':[2,2,3,3,4]}) 50 | sim.tracer.render_trace() 51 | 52 | After you have PyRTL installed, you should be able to cut and paste the above 53 | into a file and run it with Python. The result you should see, drawn right into 54 | the terminal, is the output of the simulation. While a great deal of work has 55 | gone into making hardware design in PyRTL as friendly as possible, please don't 56 | mistake that for a lack of depth. You can just as easily export to Verilog or 57 | other hardware formats, view results with your favorite waveform viewer, build 58 | hardware transformation passes, run JIT-accelerated simulations, design, test, 59 | verify hugely complex digital systems, and much more. Most critically of all it 60 | is easy to extend with your own approaches to digital hardware development as 61 | you find necessary. 62 | 63 | 64 | Overview of PyRTL 65 | ================= 66 | 67 | If you are brand new to PyRTL we recommend that you start with the `PyRTL Code 68 | Examples `_ 69 | which will show you most of the core functionality in the context of a complete 70 | design. 71 | 72 | ``WireVectors`` 73 | --------------- 74 | 75 | Perhaps the most important class to understand is :class:`.WireVector`, which is the 76 | basic type from which you build all hardware. If you are coming to PyRTL from Verilog, a 77 | :class:`.WireVector` is closest to a multi-bit `wire`. Every new :class:`.WireVector` 78 | builds a set of wires which you can then connect with other :class:`.WireVector` through 79 | overloaded operations such as :meth:`~.WireVector.__add__` or 80 | :meth:`~.WireVector.__or__`. 81 | 82 | A bunch of other related classes, including :class:`.Input`, :class:`.Output`, 83 | :class:`.Const`, and :class:`.Register` are all derived from 84 | :class:`.WireVector`. Coupled with :class:`.MemBlock` (and :class:`.RomBlock`), 85 | this is all a user needs to create a functional hardware design. 86 | 87 | .. inheritance-diagram:: pyrtl.WireVector 88 | pyrtl.Input 89 | pyrtl.Output 90 | pyrtl.Const 91 | pyrtl.Register 92 | :parts: 1 93 | 94 | After specifying a hardware design, there are then options to simulate your 95 | design right in PyRTL, synthesize it down to primitive 1-bit operations, 96 | optimize it, and export it to Verilog (along with a testbench). 97 | 98 | Simulation 99 | ---------- 100 | 101 | PyRTL provides tools for simulation and viewing simulation traces. Simulation 102 | is how your hardware is "executed" for the purposes of testing, and three 103 | different classes help you do that: :class:`.Simulation`, 104 | :class:`.FastSimulation` and :class:`.CompiledSimulation`. All three have 105 | `almost` the same interface and, except for a few debugging cases, can be used 106 | interchangeably. Typically one starts with :class:`.Simulation` and then moves 107 | up to :class:`.FastSimulation` when performance begins to matter. 108 | 109 | Both :class:`.Simulation` and :class:`.FastSimulation` store a list of each 110 | wire's value in each cycle in :attr:`.Simulation.tracer`, which is an instance 111 | of :class:`.SimulationTrace`. Traces can then be rendered to the terminal with 112 | :meth:`.SimulationTrace.render_trace`. 113 | :class:`SimulationTraces<.SimulationTrace>` can be handled in other ways, for 114 | example they can be extracted as a test bench with 115 | :func:`.output_verilog_testbench`, or exported to a VCD file with 116 | :meth:`~.SimulationTrace.print_vcd`. 117 | 118 | Optimization 119 | ------------ 120 | 121 | :class:`.WireVector` and :class:`.MemBlock` are just "sugar" over a core set of 122 | primitives, and the final design is built up incrementally as a graph of these 123 | primitives. :class:`WireVectors<.WireVector>` connects these "primitives", 124 | which connect to other :class:`WireVectors<.WireVector>`. Each primitive is a 125 | :class:`.LogicNet`, and a :class:`.Block` is a graph of 126 | :class:`LogicNets<.LogicNet>`. Typically a full design is stored in a single 127 | :class:`.Block`. The function :func:`.working_block()` returns the block on 128 | which we are implicitly working. Hardware transforms may make a new 129 | :class:`.Block` from an old one. For example, see :class:`.PostSynthBlock`. 130 | 131 | Errors 132 | ------ 133 | 134 | Finally, when things go wrong you may hit an :class:`Exception`, neither of which is 135 | likely recoverable automatically (which is why we limited them to only two types). 136 | :class:`.PyrtlError` is intended to capture end user errors such as invalid constant 137 | strings and mis-matched bitwidths. In contrast, :class:`.PyrtlInternalError` captures 138 | internal invariants and assertions over the core logic graph which should never be 139 | encountered when constructing designs in the normal ways. If you hit a confusing 140 | :class:`.PyrtlError` or any :class:`.PyrtlInternalError` feel free to file an issue. 141 | 142 | .. autoclass:: pyrtl.PyrtlError 143 | :members: 144 | 145 | .. autoclass:: pyrtl.PyrtlInternalError 146 | :members: 147 | 148 | Reference Guide 149 | =============== 150 | .. toctree:: 151 | :maxdepth: 2 152 | 153 | basic 154 | regmem 155 | simtest 156 | helpers 157 | blocks 158 | analysis 159 | export 160 | rtllib 161 | PyRTL on GitHub 162 | 163 | Index 164 | ===== 165 | * :ref:`genindex` 166 | -------------------------------------------------------------------------------- /docs/release/README.md: -------------------------------------------------------------------------------- 1 | # PyRTL release process documentation 2 | 3 | See [Packaging Python 4 | Projects](https://packaging.python.org/en/latest/tutorials/packaging-projects/) 5 | for an overview of Python packaging. PyRTL's `pyproject.toml` configuration is 6 | based on this tutorial. 7 | 8 | See [Publishing package distribution releases using GitHub 9 | Actions](https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/) 10 | for an overview of distributing Python packages with GitHub Actions. PyRTL's 11 | `python-release.yml` configuration is based on this tutorial. 12 | 13 | ## PyRTL versioning 14 | 15 | PyRTL uses [semantic versioning](https://semver.org/). All version numbers are 16 | of the form `MAJOR.MINOR.PATCH`, with an optional `rcN` suffix for release 17 | candidates, starting from `rc0`. Valid examples include `0.11.1` and 18 | `0.11.0rc0`. Never reuse a version number. 19 | 20 | Releases with a `rc` suffix are Release Candidates. Use release candidates for 21 | pre-releases, and to test the release process. `pip` will not install release 22 | candidates by default. Use: 23 | ```shell 24 | $ pip install --pre pyrtl 25 | ``` 26 | to install the latest release candidate. 27 | 28 | The rest of this document assumes that the new PyRTL version number is 29 | `$NEW_VERSION`. 30 | 31 | See [PEP 440](https://peps.python.org/pep-0440/) for more details on version 32 | numbers. 33 | 34 | ## Building a new release 35 | 36 | 1. Clone the PyRTL repository: 37 | ```shell 38 | $ git clone git@github.com:UCSBarchlab/PyRTL.git pyrtl 39 | $ cd pyrtl 40 | ``` 41 | 2. Tag the new version: 42 | ```shell 43 | $ git tag $NEW_VERSION 44 | ``` 45 | 3. Push this change to GitHub. Tags are not pushed by default, so use: 46 | ```shell 47 | $ git push origin $NEW_VERSION 48 | ``` 49 | 50 | The `python-release.yml` GitHub workflow should detect the new tag, build a new 51 | release, and upload the new release to TestPyPI. Check workflow status at 52 | https://github.com/UCSBarchlab/PyRTL/actions 53 | 54 | ### Testing a new TestPyPI release 55 | 56 | Check the [TestPyPI release 57 | history](https://test.pypi.org/project/pyrtl/#history). You should see your 58 | `$NEW_VERSION` at the top of the page. 59 | 60 | Test this TestPyPI release by creating a new Python virtual environment 61 | (`venv`): 62 | ```shell 63 | $ python3 -m venv pyrtl-release-test 64 | $ . pyrtl-release-test/bin/activate 65 | ``` 66 | 67 | Then install and test the new release from TestPyPI: 68 | ```shell 69 | $ pip install --index-url https://test.pypi.org/simple/ --no-deps pyrtl 70 | ``` 71 | If you created a `rc` release candidate, don't forget to add the `--pre` flag: 72 | ```shell 73 | $ pip install --index-url https://test.pypi.org/simple/ --no-deps --pre pyrtl 74 | ``` 75 | 76 | ### Deploying a new PyPI release 77 | 78 | If the new release looks good, approve the 'Publish distribution archives on 79 | PyPI' workflow on GitHub to deploy the new release to PyPI. 80 | 81 | ### Testing a new PyPI release 82 | 83 | Check the [PyPI release history](https://pypi.org/project/pyrtl/#history). You 84 | should see your `$NEW_VERSION` at the top of the page. 85 | 86 | Test this PyPI release by installing and testing the new release from PyPI: 87 | ```shell 88 | $ pip install pyrtl 89 | ``` 90 | If you created a `rc` release candidate, don't forget to add the `--pre` flag: 91 | ```shell 92 | $ pip install --pre pyrtl 93 | ``` 94 | 95 | ## Read the Docs Versioning 96 | 97 | Read the Docs builds documentation for each PyRTL release. Available versions 98 | can be seen on [PyRTL's 99 | dashboard](https://readthedocs.org/projects/pyrtl/versions/), or in the bottom 100 | right [flyout 101 | menu](https://docs.readthedocs.io/en/stable/glossary.html#term-flyout-menu) on 102 | each documentation page. 103 | 104 | After building a new release, check the new release's documentation on [PyRTL's 105 | Read the Docs dashboard](https://readthedocs.org/projects/pyrtl/versions/). 106 | 107 | Versioned documentation builds are triggered by the creation of git tags, and 108 | versions for new releases are automatically activated by the Read the Docs 109 | "Activate new version" [automation 110 | rule](https://docs.readthedocs.io/en/stable/automation-rules.html). The "Hide 111 | release candidates" automation rule hides release candidates from the bottom 112 | right flyout menu. 113 | 114 | After a full release (not a release candidate), deactivate the documentation 115 | for any corresponding release candidates on the dashboard. 116 | 117 | ## Manually building and publishing a new release 118 | 119 | The following manual steps should be automated by 120 | `.github/workflows/python-release.yml`. Manual release instructions are 121 | provided below in case a release needs to be built without GitHub workflow 122 | automation. If the automation is working, you shouldn't need to run these 123 | commands. 124 | 125 | ### Manual build 126 | 127 | Build distribution archive: 128 | 129 | ```shell 130 | $ uv build 131 | ``` 132 | 133 | This produces two files in `dist/`: a `.whl` file and a `.tar.gz` file. 134 | 135 | ### Manual publish on TestPyPI 136 | 137 | 1. If necessary, create a TestPyPI API token, by going to 138 | https://test.pypi.org/manage/account/ and clicking 'Add API token'. 139 | 2. Upload distribution archive to TestPyPI: 140 | ```shell 141 | $ uv run --with twine twine upload --repository testpypi dist/* 142 | ``` 143 | 3. Enter your API token when prompted. 144 | 4. Check the new release's status at https://test.pypi.org/project/pyrtl/#history 145 | 5. Install and test the new release from TestPyPI by following the [Testing a 146 | new TestPyPI release](#testing-a-new-testpypi-release) instructions above. 147 | 148 | ### Manual publish on PyPI 149 | 150 | > :warning: The next steps update the official PyRTL release on PyPI, which 151 | > affects anyone running `pip install pyrtl`. Proceed with caution! 152 | 153 | 1. If necessary, create a PyPI API token, by going to 154 | https://pypi.org/manage/account/ and clicking 'Add API token'. 155 | 2. Upload distribution archive to PyPI: 156 | ```shell 157 | $ uv run --with twine twine upload dist/* 158 | ``` 159 | 3. Enter your API token when prompted. 160 | 4. Check the new release's status at https://pypi.org/project/pyrtl/#history 161 | 5. Install and test the new release from PyPI by following the [Testing a new 162 | PyPI release](#testing-a-new-pypi-release) instructions above. 163 | 164 | ## Fixing Mistakes 165 | 166 | First assess the magnitude of the problem. If the new release is unusable or is 167 | unsafe to use, yank the bad release, then publish a fixed release. If the new 168 | release has a smaller problem, and is mostly usable, just publish a fixed 169 | release. 170 | 171 | ### Yanking A Bad Release 172 | 173 | If the new release is unusable, [yank the 174 | release](https://pypi.org/help/#yanked). This can be done by a project owner on 175 | PyPI, by clicking 'Manage project' then 'Options', then Yank'. When a release 176 | is yanked, it remains available to anyone requesting exactly the yanked 177 | version, so a project with a pinned requirement on PyRTL won't break. A yanked 178 | version will not be installed in any other case, so a user running `pip install 179 | pyrtl` will never receive a yanked version. 180 | 181 | ### Publishing A Fixed Release 182 | 183 | Fix the problem in the code, then publish a new release with a new version 184 | number by following the instructions above. Do not attempt to reuse an existing 185 | version number. 186 | -------------------------------------------------------------------------------- /pyrtl/rtllib/testingutils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import random 4 | from collections.abc import Callable 5 | 6 | import pyrtl 7 | 8 | 9 | def calculate_max_and_min_bitwidths(max_bitwidth=None, exact_bitwidth=None): 10 | if max_bitwidth is not None: 11 | min_bitwidth = 1 12 | elif exact_bitwidth is not None: 13 | min_bitwidth = max_bitwidth = exact_bitwidth 14 | else: 15 | msg = "A max or exact bitwidth must be specified" 16 | raise pyrtl.PyrtlError(msg) 17 | return min_bitwidth, max_bitwidth 18 | 19 | 20 | def inverse_power_dist(bitwidth): 21 | # Note that this is not uniformly distributed 22 | return int(2 ** random.uniform(0, bitwidth) - 1) 23 | 24 | 25 | def uniform_dist(bitwidth): 26 | return random.randrange(2**bitwidth) 27 | 28 | 29 | def make_inputs_and_values( 30 | num_wires: int, 31 | max_bitwidth: int | None = None, 32 | exact_bitwidth: int | None = None, 33 | dist: Callable[[int], int] = uniform_dist, 34 | test_vals: int = 20, 35 | ) -> tuple[list[pyrtl.Input], list[list[int]]]: 36 | """Generates multiple :class:`.Input` wires and their test values. 37 | 38 | The generated list of test values is a list of lists. The inner lists each represent 39 | the values of a single :class:`.Input` wire, for each :class:`.Simulation` cycle. 40 | 41 | :param num_wires: Number of :class:`Inputs<.Input>` to generate. 42 | :param max_bitwidth: If specified, generate :class:`Inputs<.Input>` with random 43 | :attr:`~.WireVector.bitwidth` in the range ``[1, max_bitwidth)``. 44 | :param exact_bitwidth: If specified, generate :class:`Inputs<.Input>` with 45 | :attr:`~.WireVector.bitwidth` ``exact_bitwidth``. 46 | :param dist: Function to generate the random :class:`.Input` values. 47 | :param test_vals: Number of random :class:`.Input` values to generate. 48 | 49 | :return: ``(inputs, values)``, where ``inputs`` is a :class:`list` of 50 | :class:`Inputs<.Input>`, and ``values`` is a list of values for each 51 | ``input``. 52 | """ 53 | min_bitwidth, max_bitwidth = calculate_max_and_min_bitwidths( 54 | max_bitwidth, exact_bitwidth 55 | ) 56 | wires, vals = list( 57 | zip( 58 | *( 59 | an_input_and_vals( 60 | random.randrange(min_bitwidth, max_bitwidth + 1), 61 | test_vals, 62 | random_dist=dist, 63 | ) 64 | for i in range(num_wires) 65 | ), 66 | strict=True, 67 | ) 68 | ) 69 | return wires, vals 70 | 71 | 72 | def an_input_and_vals( 73 | bitwidth: int, 74 | test_vals: int = 20, 75 | name: str = "", 76 | random_dist: Callable[[int], int] = uniform_dist, 77 | ) -> tuple[pyrtl.Input, list[int]]: 78 | """Generate an :class:`.Input` wire and random test values for it. 79 | 80 | :param bitwidth: The bitwidth of the random values to generate. 81 | :param test_vals: Number of random values to generate per :class:`.Input`. 82 | :param name: Name of the returned :class:`.Input`. 83 | 84 | :return: ``(input_wire, test_values)``, where ``input_wire`` is an :class:`.Input`, 85 | and ``test_values`` is a list of random test values for ``input_wire``. 86 | """ 87 | input_wire = pyrtl.Input(bitwidth, name=name) # Creating a new input wire 88 | test_vals = [random_dist(bitwidth) for i in range(test_vals)] 89 | return input_wire, test_vals 90 | 91 | 92 | # deprecated name 93 | generate_in_wire_and_values = an_input_and_vals 94 | 95 | 96 | def make_consts( 97 | num_wires: int, 98 | max_bitwidth: int | None = None, 99 | exact_bitwidth: int | None = None, 100 | random_dist: Callable[[int], int] = inverse_power_dist, 101 | ) -> tuple[list[pyrtl.Const], list[int]]: 102 | """Generate random :class:`.Const` values. 103 | 104 | :param num_wires: Number of :class:`Consts<.Const>` to generate. 105 | :param max_bitwidth: If specified, generate :class:`Consts<.Const>` with random 106 | :attr:`~.WireVector.bitwidth` in the range ``[1, max_bitwidth)``. 107 | :param exact_bitwidth: If specified, generate :class:`Consts<.Const>` with 108 | :attr:`~.WireVector.bitwidth` ``exact_bitwidth``. 109 | :param random_dist: Function to generate the random :class:`.Const` values. 110 | 111 | :return: ``(consts, values)``, where ``consts`` is a :class:`list` of 112 | :class:`Consts<.Const>`, and ``values`` is a list of each ``const``'s 113 | value. 114 | """ 115 | min_bitwidth, max_bitwidth = calculate_max_and_min_bitwidths( 116 | max_bitwidth, exact_bitwidth 117 | ) 118 | bitwidths = [ 119 | random.randrange(min_bitwidth, max_bitwidth + 1) for i in range(num_wires) 120 | ] 121 | wires = [pyrtl.Const(random_dist(b), b) for b in bitwidths] 122 | vals = [w.val for w in wires] 123 | return wires, vals 124 | 125 | 126 | def sim_and_ret_out( 127 | outwire: pyrtl.WireVector, inwires: list[pyrtl.WireVector], invals: list[list[int]] 128 | ) -> list[int]: 129 | """Run a simulation with ``invals`` for ``inwires`` and return ``outwire``'s values. 130 | 131 | .. WARNING:: 132 | 133 | Use :meth:`.Simulation.step_multiple` instead:: 134 | 135 | sim = pyrtl.Simulation() 136 | sim.step_multiple(provided_inputs=dict(zip(inwires, invals))) 137 | output = sim.tracer.trace[outwire.name] 138 | 139 | :param outwire: The wire to return the values of in each simulation cycle. 140 | :param inwires: A list of :class:`.Input` wires to provide values for. 141 | :param invals: A list of :class:`.Input` value lists. 142 | 143 | :return: A list of ``outwire``'s values in each simulation cycle. 144 | """ 145 | # Pulling the value of outwire straight from the log 146 | return sim_and_ret_outws(inwires, invals)[outwire.name] 147 | 148 | 149 | def sim_and_ret_outws( 150 | inwires: list[pyrtl.WireVector], invals: list[list[int]] 151 | ) -> dict[str, list[int]]: 152 | """Run a simulation with ``invals`` for ``inwires`` and return all wire values. 153 | 154 | .. WARNING:: 155 | 156 | Use :meth:`.Simulation.step_multiple` instead:: 157 | 158 | sim = pyrtl.Simulation() 159 | sim.step_multiple(provided_inputs=dict(zip(inwires, invals))) 160 | outputs = sim.tracer.trace 161 | 162 | :param inwires: A list of :class:`.Input` wires to provide values for. 163 | :param invals: A list of :class:`.Input` value lists. 164 | 165 | :return: A :class:`dict` mapping from a :class:`WireVector`'s name to a 166 | :class:`list` of its values in each cycle. 167 | """ 168 | sim = pyrtl.Simulation() 169 | sim.step_multiple(provided_inputs=dict(zip(inwires, invals, strict=True))) 170 | return sim.tracer.trace 171 | 172 | 173 | def sim_multicycle(in_dict, hold_dict, hold_cycles, sim=None): 174 | if sim is None: 175 | sim = pyrtl.Simulation() 176 | sim.step(in_dict) 177 | for _i in range(hold_cycles): 178 | sim.step(hold_dict) 179 | return sim.tracer.trace[-1] 180 | 181 | 182 | def multi_sim_multicycle(in_dict, hold_dict, hold_cycles, sim=None): 183 | if sim is None: 184 | sim = pyrtl.Simulation() 185 | cycles = len(next(iter(in_dict.values()))) 186 | for cycle in range(cycles): 187 | current_dict = {wire: values[cycle] for wire, values in in_dict} 188 | cur_result = sim_multicycle(current_dict, hold_dict, hold_cycles, sim) 189 | if cycle == 0: 190 | results = {wire: [result_val] for wire, result_val in cur_result} 191 | else: 192 | for wire, result_val in cur_result: 193 | results[wire].append(result_val) 194 | return results 195 | -------------------------------------------------------------------------------- /ipynb-examples/example2-counter.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | " # Example 2: A Counter with Ripple Carry Adder.\n", 8 | "\n", 9 | " This next example shows how you make stateful things with registers and more complex\n", 10 | " hardware structures with functions. We generate a 3-bit ripple carry adder, building\n", 11 | " off of the 1-bit adder from Example 1, and then hook it to a register to count\n", 12 | " up modulo 8.\n" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "metadata": { 19 | "collapsed": true 20 | }, 21 | "outputs": [], 22 | "source": [ 23 | "%pip install pyrtl\n", 24 | "\n", 25 | "import pyrtl\n", 26 | "\n", 27 | "\n", 28 | "pyrtl.reset_working_block()\n" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | " Let's just dive right in.\n", 36 | "\n", 37 | " A function in PyRTL is nothing special -- it just so happens that the statements it\n", 38 | " encapsulate tell PyRTL to build some hardware.\n" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "metadata": { 45 | "collapsed": true 46 | }, 47 | "outputs": [], 48 | "source": [ 49 | "def one_bit_add(\n", 50 | " a: pyrtl.WireVector, b: pyrtl.WireVector, carry_in: pyrtl.WireVector\n", 51 | ") -> tuple[pyrtl.WireVector, pyrtl.WireVector]:\n", 52 | " assert len(a) == len(b) == 1 # len returns the bitwidth\n", 53 | " sum = a ^ b ^ carry_in\n", 54 | " carry_out = a & b | a & carry_in | b & carry_in\n", 55 | " return sum, carry_out\n" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | " If we call `one_bit_add` above with the arguments `x`, `y`, and `z`, it will make a\n", 63 | " one-bit adder to add those values together, returning [WireVectors](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.WireVector) for `sum` and\n", 64 | " `carry_out` as applied to `x`, `y`, and `z`. If I call it again on `i`, `j`, and `k`\n", 65 | " it will build a new one-bit adder for those inputs and return the resulting `sum` and\n", 66 | " `carry_out` for that adder.\n", 67 | "\n", 68 | " While PyRTL actually provides an `+` operator for [WireVectors](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.WireVector) which generates\n", 69 | " adders, a ripple carry adder is something people can understand easily but has enough\n", 70 | " structure to be mildly interesting. Let's define an adder of arbitrary length\n", 71 | " recursively and (hopefully) Pythonically. More comments after the code.\n" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "metadata": { 78 | "collapsed": true 79 | }, 80 | "outputs": [], 81 | "source": [ 82 | "def ripple_add(\n", 83 | " a: pyrtl.WireVector, b: pyrtl.WireVector, carry_in: pyrtl.WireVector = 0\n", 84 | ") -> tuple[pyrtl.WireVector, pyrtl.WireVector]:\n", 85 | " a, b = pyrtl.match_bitwidth(a, b)\n", 86 | " # This function is a function that allows us to match the bitwidth of multiple\n", 87 | " # different wires. By default, it zero extends the shorter bits\n", 88 | " if len(a) == 1:\n", 89 | " return one_bit_add(a, b, carry_in)\n", 90 | " lsb, ripple_carry = one_bit_add(a[0], b[0], carry_in)\n", 91 | " msbits, carry_out = ripple_add(a[1:], b[1:], ripple_carry)\n", 92 | " return pyrtl.concat(msbits, lsb), carry_out\n" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | " ## The above code breaks down into two cases:\n", 100 | "\n", 101 | " 1. If `a` is one-bit wide, just do a `one_bit_add`.\n", 102 | " 2. Otherwise, do a `one_bit_add` on the least significant bits, `ripple_add` the rest,\n", 103 | " and then stick the results back together into one [WireVector](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.WireVector).\n", 104 | "\n", 105 | " ## A couple interesting features of PyRTL can be seen here:\n", 106 | "\n", 107 | " * WireVectors can be indexed like lists, with `[0]` accessing the least significant\n", 108 | " bit and `[1:]` accessing the remaining bits. Python slicing is supported, with the\n", 109 | " usual `start:stop:stop` syntax.\n", 110 | " * While you can add two lists together in Python, a [WireVector](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.WireVector) + [WireVector](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.WireVector) means\n", 111 | " \"make an adder\". To concatenate the bits of two [WireVectors](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.WireVector) one needs to use\n", 112 | " [concat()](https://pyrtl.readthedocs.io/en/latest/helpers.html#pyrtl.concat).\n", 113 | " * Finally, if we look at `carry_in` it seems to have a default value of the integer\n", 114 | " `0` but is a [WireVector](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.WireVector) at other times. Python supports polymorphism throughout\n", 115 | " and PyRTL will cast integers and some other types to [WireVectors](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.WireVector) when it can.\n", 116 | "\n", 117 | " Now let's build a 3-bit counter from our N-bit ripple carry adder.\n" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": null, 123 | "metadata": { 124 | "collapsed": true 125 | }, 126 | "outputs": [], 127 | "source": [ 128 | "counter = pyrtl.Register(bitwidth=3, name=\"counter\")\n", 129 | "sum, carry_out = ripple_add(counter, pyrtl.Const(\"1'b1\"))\n", 130 | "counter.next <<= sum\n" 131 | ] 132 | }, 133 | { 134 | "cell_type": "markdown", 135 | "metadata": {}, 136 | "source": [ 137 | " ## A few new things in the above code:\n", 138 | "\n", 139 | " * The two remaining types of basic [WireVectors](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.WireVector), [Const](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.Const) and [Register](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.Register), both appear.\n", 140 | " [Const](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.Const), unsurprisingly, is just for holding constants (such as the `0` in\n", 141 | " `ripple_add`), but here we create one explicitly with a Verilog-like string which\n", 142 | " includes both the value and the bitwidth.\n", 143 | " * [Registers](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.Register) are just like [WireVectors](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.WireVector), except their updates are delayed to the\n", 144 | " next clock cycle. This is made explicit in the syntax through the property `.next`\n", 145 | " which should always be set for registers.\n", 146 | " * In this simple example, we make the counter's value on the next cycle equal to the\n", 147 | " counter's value this cycle plus one.\n", 148 | "\n", 149 | " Now let's run the bugger. No need for [Inputs](https://pyrtl.readthedocs.io/en/latest/basic.html#pyrtl.Input), as this circuit doesn't have any.\n", 150 | " Finally we'll print the trace to the screen and check that it counts up correctly.\n" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": null, 156 | "metadata": { 157 | "collapsed": true 158 | }, 159 | "outputs": [], 160 | "source": [ 161 | "sim = pyrtl.Simulation()\n", 162 | "for cycle in range(15):\n", 163 | " sim.step()\n", 164 | " assert sim.value[counter] == cycle % 8\n", 165 | "sim.tracer.render_trace()\n" 166 | ] 167 | } 168 | ], 169 | "metadata": { 170 | "kernelspec": { 171 | "display_name": "Python 3", 172 | "language": "python", 173 | "name": "python3" 174 | }, 175 | "language_info": { 176 | "codemirror_mode": { 177 | "name": "ipython", 178 | "version": 3 179 | }, 180 | "file_extension": ".py", 181 | "mimetype": "text/x-python", 182 | "name": "python", 183 | "nbconvert_exporter": "python", 184 | "pygments_lexer": "ipython3", 185 | "version": "3.6.4" 186 | } 187 | }, 188 | "nbformat": 4, 189 | "nbformat_minor": 2 190 | } -------------------------------------------------------------------------------- /examples/example1.2-wire-struct.py: -------------------------------------------------------------------------------- 1 | # # Example 1.2: `wire_struct` and `wire_matrix()`. 2 | # 3 | # This example demonstrates named slicing with `wire_struct` and `wire_matrix()`: 4 | # * `wire_struct`: Named `WireVector` slices. Names are alphanumeric, like a Python 5 | # namedtuple. 6 | # * `wire_matrix()`: Indexed `WireVector` slices. Indices are integers, like a Python 7 | # list. 8 | import inspect 9 | 10 | import pyrtl 11 | 12 | # ## Motivation 13 | # 14 | # `wire_struct` and `wire_matrix()` are syntactic sugar. They make PyRTL code easier to 15 | # read and write, but they do not introduce any new functionality. We start with a 16 | # motivating example that does not use `wire_struct` or `wire_matrix()`, point out some 17 | # common problems, then rewrite the example with `wire_struct` to address those 18 | # problems. 19 | # 20 | # This example deals with 24-bit pixel data. These 24 bits are composed of three 21 | # concatenated components: 22 | # 23 | # pixel: 24 | # ┌───────┬───────┬───────┐ 25 | # │ red │ green │ blue │ 26 | # └┬─────┬┴┬─────┬┴┬─────┬┘ 27 | # 23 16 15 8 7 0 28 | # 29 | # In other words: 30 | # * The `red` component is in `pixel[16:24]` 31 | # * The `green` component is in `pixel[8:16]` 32 | # * And the `blue` component is in `pixel[0:8]` 33 | # 34 | # Accept a 24-bit pixel as a PyRTL `Input`: 35 | pixel_in = pyrtl.Input(name="pixel_in", bitwidth=24) 36 | 37 | # Split the pixel data into `red`, `green`, and `blue` components. The indices are 38 | # manually calculated and hardcoded below. These calculations are prone to off-by-one 39 | # errors. If we wanted to change the pixel data format (for example, adding an 8-bit 40 | # `alpha` channel), we may need to update all these indices. 41 | red_in = pixel_in[16:24] 42 | red_in.name = "red_in" 43 | green_in = pixel_in[8:16] 44 | green_in.name = "green_in" 45 | blue_in = pixel_in[0:8] 46 | blue_in.name = "blue_in" 47 | 48 | # Do some calculations on the individual components: 49 | red_out = red_in + 0x11 50 | green_out = green_in + 0x22 51 | blue_out = blue_in + 0x33 52 | 53 | # `red_out`, `green_out`, and `blue_out` are 9 bits wide, so they must be truncated back 54 | # down to 8 bits before concatenating. 55 | assert red_out.bitwidth == 9 56 | assert green_out.bitwidth == 9 57 | assert blue_out.bitwidth == 9 58 | 59 | # Now `concat()` the processed components into a new output pixel. The components must 60 | # be specified in the correct order, and we must `truncate()` them to the correct 61 | # bitwidth. 62 | pixel_out = pyrtl.concat( 63 | red_out.truncate(8), green_out.truncate(8), blue_out.truncate(8) 64 | ) 65 | assert pixel_out.bitwidth == 24 66 | pixel_out.name = "pixel_out" 67 | 68 | # Test this slicing and concatenation logic. 69 | print("Pixel slicing and concatenation without wire_struct:") 70 | sim = pyrtl.Simulation() 71 | sim.step_multiple(provided_inputs={"pixel_in": [0x112233, 0xAABBCC]}) 72 | sim.tracer.render_trace( 73 | trace_list=["pixel_in", "red_in", "green_in", "blue_in", "pixel_out"] 74 | ) 75 | 76 | # ## Introducing `wire_struct` 77 | # 78 | # Start over, and rebuild the example with `wire_struct`. 79 | pyrtl.reset_working_block() 80 | 81 | 82 | # This `Pixel` class is the single source of truth that specifies how the `red`, 83 | # `green`, and `blue` components are packed into 24 bits. A `Pixel` instance can do one 84 | # of two things: 85 | # 86 | # 1. Slice a 24-bit value into `red`, `green`, and `blue` components, OR 87 | # 2. Pack `red`, `green`, and `blue` components into a 24-bit value. 88 | # 89 | # Because this definition is the single source of truth, we can change the pixel data 90 | # format just by changing this class definition. For example, try making the `red` 91 | # component the last component, instead of the first component. 92 | @pyrtl.wire_struct 93 | class Pixel: 94 | # The most significant bits are specified first, so the `red` component is the 8 95 | # most significant bits, indices 16..23 96 | red: 8 97 | # The `green` component is the 8 middle bits, indices 8..15 98 | green: 8 99 | # The `blue` component is the 8 least significant bits, indices 0..7 100 | blue: 8 101 | 102 | 103 | # `pixel_in` is a `Pixel` constructed from a 24-bit `Input`, option (1) above. The 104 | # 24-bit `Input` will be sliced into 8-bit components, and those components can be 105 | # manipulated as `pixel_in.red`, `pixel_in.green`, and `pixel_in.blue` in the Python 106 | # code below. 107 | # 108 | # Because this `Pixel` has a name, the 8-bit components will be assigned the names 109 | # `"pixel_in.red"`, `"pixel_in.green"`, and `"pixel_in.blue"`. These component names are 110 | # included in the `trace_list` below. 111 | pixel_in = Pixel(name="pixel_in", concatenated_type=pyrtl.Input) 112 | assert pixel_in.bitwidth == 24 113 | assert pixel_in.red.bitwidth == 8 114 | assert pixel_in.green.bitwidth == 8 115 | assert pixel_in.blue.bitwidth == 8 116 | 117 | # Repeat our calculations with the named components. Components are accessed with "dot" 118 | # notation (`.green`), like a Python namedtuple. There are no more hardcoded indices 119 | # like `[8:16]` in this example. 120 | red_out = pixel_in.red + 0x11 121 | green_out = pixel_in.green + 0x22 122 | blue_out = pixel_in.blue + 0x33 123 | 124 | # `pixel_out` is a `Pixel` constructed from components, option (2) above. Components 125 | # with more than 8 bits are implicitly truncated to 8 bits. 126 | # 127 | # Python keyword arguments are used for `red`, `green`, and `blue` here, so the order 128 | # does not matter. We arbitrarily provide the value of the `green` component first. 129 | pixel_out = Pixel(name="pixel_out", green=green_out, red=red_out, blue=blue_out) 130 | assert pixel_out.bitwidth == 24 131 | 132 | # Test this new version of our slicing and concatenation logic. 133 | print("\nPixel slicing and concatenation with wire_struct:") 134 | sim = pyrtl.Simulation() 135 | sim.step_multiple(provided_inputs={"pixel_in": [0x112233, 0xAABBCC]}) 136 | sim.tracer.render_trace( 137 | trace_list=[ 138 | "pixel_in", 139 | "pixel_in.red", 140 | "pixel_in.green", 141 | "pixel_in.blue", 142 | "pixel_out", 143 | ] 144 | ) 145 | 146 | # ## Introducing `wire_matrix()` 147 | # 148 | # `wire_matrix()` is like `wire_struct`, except the components are indexed by integers, 149 | # like a list. 150 | # 151 | # Start over. 152 | pyrtl.reset_working_block() 153 | 154 | # `PixelPair` is a pair of two `Pixels`. This also shows how `wire_matrix()` and 155 | # `wire_struct` work together - `PixelPair` is a `wire_struct` nested in a 156 | # `wire_matrix()`. 157 | PixelPair = pyrtl.wire_matrix(component_schema=Pixel, size=2) 158 | 159 | # `wire_matrix()` returns a class! 160 | assert inspect.isclass(PixelPair) 161 | 162 | # Create a `PixelPair` that decomposes a 48-bit input into two 24-bit `Pixels`. Each 163 | # 24-bit `Pixel` can be further decomposed into 8-bit `red`, `green`, and `blue` 164 | # components. This `PixelPair` has been assigned a name, so all its components are 165 | # named, as seen in the simulation trace below. 166 | pixel_pair_in = PixelPair(name="pixel_pair_in", concatenated_type=pyrtl.Input) 167 | assert pixel_pair_in.bitwidth == 48 168 | assert pixel_pair_in[0].bitwidth == 24 169 | assert pixel_pair_in[0].red.bitwidth == 8 170 | 171 | # Do some calculations on the `Pixel` components. Components are accessed with 172 | # square-bracket notation (`[1]`), like a Python list. 173 | pixel_pair_out_values = [ 174 | Pixel( 175 | red=pixel_pair_in[0].red + 0x10, 176 | green=pixel_pair_in[0].green, 177 | blue=pixel_pair_in[0].blue, 178 | ), 179 | pixel_pair_in[1] + 1, 180 | ] 181 | 182 | # Pack the new component values into a new `PixelPair`. 183 | pixel_pair_out = PixelPair(name="pixel_pair_out", values=pixel_pair_out_values) 184 | 185 | # Test out `PixelPair`. 186 | print("\nPixelPair slicing and concatenating with wire_matrix:") 187 | sim = pyrtl.Simulation() 188 | sim.step_multiple(provided_inputs={"pixel_pair_in": [0x112233AABBCC, 0x445566778899]}) 189 | sim.tracer.render_trace( 190 | trace_list=[ 191 | "pixel_pair_in", 192 | "pixel_pair_in[0]", 193 | "pixel_pair_in[0].red", 194 | "pixel_pair_in[0].green", 195 | "pixel_pair_in[0].blue", 196 | "pixel_pair_in[1]", 197 | "pixel_pair_in[1].red", 198 | "pixel_pair_in[1].green", 199 | "pixel_pair_in[1].blue", 200 | "pixel_pair_out", 201 | ] 202 | ) 203 | 204 | # These examples hopefully show how `wire_matrix()` and `wire_struct` result in code 205 | # that's easier to write correctly and easier to read. 206 | # 207 | # `wire_struct` and `wire_matrix()` have many more features, but this example should 208 | # demonstrate the main ideas. See the documentation for more details: 209 | # 210 | # https://pyrtl.readthedocs.io/en/latest/helpers.html#pyrtl.wire_struct 211 | # 212 | # https://pyrtl.readthedocs.io/en/latest/helpers.html#pyrtl.wire_matrix 213 | -------------------------------------------------------------------------------- /examples/example4-debuggingtools.py: -------------------------------------------------------------------------------- 1 | # # Example 4: Debugging 2 | # 3 | # Debugging is half the coding process in software, and in PyRTL, it's no different. 4 | # PyRTL provides some additional challenges when it comes to debugging, as a problem may 5 | # surface long after the error was made. Fortunately, PyRTL comes with various features 6 | # to help you find mistakes. 7 | import io 8 | import random 9 | 10 | import pyrtl 11 | from pyrtl.rtllib.adders import kogge_stone 12 | from pyrtl.rtllib.multipliers import tree_multiplier 13 | 14 | # This example covers debugging strategies for PyRTL. For general Python debugging, we 15 | # recommend healthy use of the `assert` statement, and use of `pdb` for tracking down 16 | # bugs. However, PyRTL introduces some new complexities because the place where 17 | # functionality is defined (when you construct and operate on PyRTL classes) is separate 18 | # in time from where that functionality is executed (i.e. during `Simulation`). Thus, 19 | # sometimes it hard to track down where a `WireVector` might have come from, or what 20 | # exactly it is doing. 21 | # 22 | # In this example, we build a circuit that adds up three `Input` values. However, 23 | # instead of building an add function ourselves or using the built-in "+" function in 24 | # PyRTL, we will instead use the `kogge_stone()` adders in `rtllib`, PyRTL's standard 25 | # library. 26 | in1 = pyrtl.Input(8, "in1") 27 | in2 = pyrtl.Input(8, "in2") 28 | in3 = pyrtl.Input(8, "in3") 29 | 30 | out = pyrtl.Output(10, "out") 31 | 32 | add1_out = kogge_stone(in1, in2) 33 | add2_out = kogge_stone(add1_out, in2) 34 | out <<= add2_out 35 | 36 | # The most basic way of debugging PyRTL is to connect a value to an output wire and use 37 | # `Simulation` to trace the output. A simple `print` statement won't work because the 38 | # `WireVectors` do not carry values while we are building the hardware. 39 | # 40 | # If we want to check the result of the first addition, we can connect an `Output` wire 41 | # to the result wire of the first adder: 42 | debug_out = pyrtl.Output(9, "debug_out") 43 | debug_out <<= add1_out 44 | 45 | # Now simulate the circuit. Let's create some random inputs to feed our adder. 46 | random.seed(93729473) # used to make random calls deterministic for this example 47 | vals1 = [random.randrange(256) for _ in range(20)] 48 | vals2 = [random.randrange(256) for _ in range(20)] 49 | vals3 = [random.randrange(256) for _ in range(20)] 50 | 51 | sim = pyrtl.Simulation() 52 | sim.step_multiple({"in1": vals1, "in2": vals2, "in3": vals3}) 53 | 54 | # In order to get the result data, you do not need to print a waveform of the trace. You 55 | # always have the option to just pull the data out of the `Simulation`'s `tracer` 56 | # directly: 57 | print("---- Inputs and debug_out ----") 58 | print("in1: ", sim.tracer.trace["in1"]) 59 | print("in2: ", sim.tracer.trace["in2"]) 60 | print("debug_out: ", sim.tracer.trace["debug_out"]) 61 | print("\n") 62 | 63 | # Below, I am using the ability to directly retrieve the trace data to verify the 64 | # correctness of the first adder 65 | for cycle in range(len(vals1)): 66 | actual = sim.tracer.trace["debug_out"][cycle] 67 | expected = sim.tracer.trace["in1"][cycle] + sim.tracer.trace["in2"][cycle] 68 | assert actual == expected 69 | 70 | 71 | # ## `probe()` 72 | # 73 | # Now that we have built some stuff, let's clear it so we can try again in a different 74 | # way. We can start by clearing all of the hardware from the current working block with 75 | # `reset_working_block()`. The working block is a global structure that keeps track of 76 | # all the hardware you have built thus far. A "reset" will clear it so we can start 77 | # fresh. 78 | pyrtl.reset_working_block() 79 | 80 | # In this example, we will be multiplying two numbers using `tree_multiplier()`. Again, 81 | # create the two inputs and an output 82 | in1 = pyrtl.Input(8, "in1") 83 | in2 = pyrtl.Input(8, "in2") 84 | 85 | out1 = pyrtl.Output(8, "out1") 86 | out2 = pyrtl.Output(8, "out2") 87 | 88 | multout = tree_multiplier(in1, in2) 89 | 90 | # The following line will create a `probe()` named `std_probe` for later use, like an 91 | # output. 92 | pyrtl.probe(multout, "std_probe") 93 | 94 | # We could also do the same thing during assignment. The next command will create a 95 | # `probe()` (named `stdout_probe`) that refers to `multout`, and returns the wire 96 | # `multout`. This achieves virtually the same thing as the `probe()` above, but it is 97 | # done during assignment, so we skip a step by probing the wire before the 98 | # multiplication. The `probe()` returns `multout`, the original wire, and `out1` will be 99 | # assigned `multout * 2`. 100 | out1 <<= pyrtl.probe(multout, "stdout_probe") * 2 101 | 102 | # `probe()` can also be used with other operations like this: 103 | pyrtl.probe(multout + 32, "adder_probe") 104 | 105 | # or this: 106 | pyrtl.probe(multout[2:7], "select_probe") 107 | 108 | # or, similarly: 109 | # (this will create a probe of `multout` while passing `multout[2:16]` to `out2`) 110 | out2 <<= pyrtl.probe(multout)[2:16] # notice probe names are not absolutely necessary 111 | 112 | # As one can see, `probe()` can be used on any wire any time, such as before or during 113 | # its operation, assignment, etc. 114 | # 115 | # Now on to the simulation... For variation, we'll recreate the random inputs: 116 | vals1 = [random.randrange(10) for _ in range(10)] 117 | vals2 = [random.randrange(10) for _ in range(10)] 118 | 119 | sim = pyrtl.Simulation() 120 | sim.step_multiple({"in1": vals1, "in2": vals2}) 121 | 122 | # Now we will show the values of the inputs and probes and look at that, we didn't need 123 | # to make any outputs! (although `probe()` did). 124 | print("---- Using Probes ----") 125 | sim.tracer.render_trace(repr_func=str) 126 | sim.tracer.print_trace() 127 | 128 | # Say we wanted more information about one of those probes above at declaration. We 129 | # could have used `set_debug_mode()` before their creation, like so: 130 | print("\n--- Probe w/ debugging: ---") 131 | pyrtl.set_debug_mode() 132 | pyrtl.probe(multout - 16, "debugsubtr_probe") 133 | pyrtl.set_debug_mode(debug=False) 134 | 135 | 136 | # ## `WireVector` Stack Trace 137 | # 138 | # Another case that might arise is that a certain wire is causing an error to occur in 139 | # your program. `WireVector` Stack Traces allow you to find out more about where a 140 | # particular `WireVector` was made in your code. With this enabled the `WireVector` will 141 | # store exactly were it was created, which should help with issues where there is a 142 | # problem with an identified wire. 143 | # 144 | # Like above, just add the following line before the relevant `WireVector` might be made 145 | # or at the beginning of the program. 146 | pyrtl.set_debug_mode() 147 | 148 | test_out = pyrtl.Output(9, "test_out") 149 | test_out <<= kogge_stone(in1, in2) 150 | 151 | # Now to retrieve information 152 | wire_trace = test_out.init_call_stack 153 | 154 | # This data is generated using the `traceback.format_stack()` call from the Python 155 | # standard library's `traceback` module (look at the Python standard library docs for 156 | # details on the function). Therefore, the stack traces are stored as a list with the 157 | # outermost call first. 158 | print("---- Stack Trace ----") 159 | for frame in wire_trace: 160 | print(frame) 161 | 162 | # ### Storage of Additional Debug Data 163 | # 164 | # WARNING: the debug information generated by the following two processes are not 165 | # guaranteed to be preserved when functions (eg. `synthesize()`) are done over the 166 | # `Block`. 167 | # 168 | # However, if the stack trace does not give you enough information about the 169 | # `WireVector`, you can also embed additional information into the wire itself. Two ways 170 | # of doing so are by changing the name of the `WireVector`, or by adding your own custom 171 | # metadata to the `WireVector`. 172 | # 173 | # So far, each `Input` and `Output` WireVector` have been given their own names, but 174 | # normal `WireVectors` can also be given names by supplying the name argument to the 175 | # constructor. 176 | dummy_wv = pyrtl.WireVector(1, name="blah") 177 | 178 | # A `WireVector`'s `name` can also be changed later: 179 | dummy_wv.name = "argh" 180 | 181 | # Also, because of the flexible nature of Python, you can also add custom properties to 182 | # the `WireVector`: 183 | dummy_wv.my_custom_property_name = "John Clow is great" 184 | dummy_wv.custom_value_028493 = 13 185 | 186 | # Remove the `WireVector` from the `Block` to prevent problems with the rest of this 187 | # example. 188 | pyrtl.working_block().remove_wirevector(dummy_wv) 189 | 190 | # ## Trivial Graph Format 191 | # 192 | # Finally, there is a handy way to view your hardware creations as a graph. The function 193 | # `output_to_trivialgraph()` will render your hardware in a format that you can then 194 | # open with the free software "yEd" (http://en.wikipedia.org/wiki/YEd). There are 195 | # options under the "hierarchical" rendering to draw something that looks quite like a 196 | # circuit. 197 | # 198 | # Also see `output_to_svg()`. 199 | pyrtl.working_block().sanity_check() 200 | 201 | # So that `output_to_trivial_graph()` will work. 202 | pyrtl.passes._remove_unused_wires(pyrtl.working_block()) 203 | 204 | print("\n--- Trivial Graph Format (first 10 lines) ---") 205 | with io.StringIO() as tgf: 206 | pyrtl.output_to_trivialgraph(tgf) 207 | for i, line in enumerate(tgf.getvalue().split("\n")): 208 | if i == 10: 209 | break 210 | print(line) 211 | 212 | print("...") 213 | -------------------------------------------------------------------------------- /docs/basic.rst: -------------------------------------------------------------------------------- 1 | Wires and Logic 2 | =============== 3 | 4 | Wires define the relationship between logic blocks in PyRTL. They are treated 5 | like normal wires in traditional RTL systems except the :class:`.Register` 6 | wire. Logic is then created when wires are combined with one another using the 7 | provided operators. For example, if ``a`` and ``b`` are both of type 8 | :class:`.WireVector`, then ``a + b`` will make an adder, plug ``a`` and ``b`` 9 | into the inputs of that adder, and return a new :class:`.WireVector` which is 10 | the output of that adder. :class:`.Block` stores the description of the 11 | hardware as you build it. 12 | 13 | :class:`.Input`, :class:`.Output`, :class:`.Const`, and :class:`.Register` all 14 | derive from :class:`.WireVector`. :class:`.Input` represents an input pin, 15 | serving as a placeholder for an external value provided during 16 | :class:`.Simulation`. :class:`.Output` represents an output pin, which does not 17 | drive any wires in the design. :class:`.Const` is useful for specifying 18 | hard-wired values and :class:`.Register` is how sequential elements are created 19 | (they all have an implicit clock). 20 | 21 | .. inheritance-diagram:: pyrtl.WireVector 22 | pyrtl.Input 23 | pyrtl.Output 24 | pyrtl.Const 25 | pyrtl.Register 26 | :parts: 1 27 | 28 | WireVector 29 | ---------- 30 | 31 | .. autoclass:: pyrtl.WireVector 32 | :members: 33 | :special-members: __init__, __add__, __sub__, __mul__, __getitem__, 34 | __len__, __ilshift__, __invert__, __and__, __or__, __xor__, __lt__, 35 | __le__, __eq__, __ne__, __gt__, __ge__, __len__, __ior__ 36 | 37 | Input Pins 38 | ---------- 39 | 40 | .. autoclass:: pyrtl.Input 41 | :members: 42 | :show-inheritance: 43 | 44 | Output Pins 45 | ----------- 46 | 47 | .. autoclass:: pyrtl.Output 48 | :members: 49 | :show-inheritance: 50 | 51 | Constants 52 | --------- 53 | 54 | .. autoclass:: pyrtl.Const 55 | :members: 56 | :show-inheritance: 57 | :special-members: __init__ 58 | 59 | .. _conditional_assignment: 60 | 61 | Conditional Assignment 62 | ---------------------- 63 | 64 | .. autodata:: pyrtl.conditional_assignment 65 | 66 | :class:`WireVectors<.WireVector>`, :class:`Registers<.Register>`, and 67 | :class:`MemBlocks<.MemBlock>` can be conditionally assigned values based on predicates. 68 | 69 | Conditional assignments are written with `Python with statements 70 | `_, using two 71 | context managers: 72 | 73 | #. :data:`.conditional_assignment`, which provides the framework for specifying 74 | conditional assignments. 75 | #. :data:`.otherwise`, which specifies the 'fall through' case. 76 | 77 | Conditional assignments are easiest to understand with an example:: 78 | 79 | r1 = pyrtl.Register(bitwidth=8) 80 | r2 = pyrtl.Register(bitwidth=8) 81 | w = pyrtl.WireVector(bitwidth=8) 82 | mem = pyrtl.MemBlock(bitwidth=8, addrwidth=4) 83 | 84 | a = pyrtl.Input(bitwidth=1) 85 | b = pyrtl.Input(bitwidth=1) 86 | c = pyrtl.Input(bitwidth=1) 87 | d = pyrtl.Input(bitwidth=1) 88 | 89 | with pyrtl.conditional_assignment: 90 | with a: 91 | # Set when a is True. 92 | r1.next |= 1 93 | mem[0] |= 2 94 | with b: 95 | # Set when a and b are both True. 96 | r2.next |= 3 97 | with c: 98 | # Set when a is False and c is True. 99 | r1.next |= 4 100 | r2.next |= 5 101 | with pyrtl.otherwise: 102 | # Set when a and c are both False. 103 | r2.next |= 6 104 | 105 | with d: 106 | # Set when d is True. A `with` block after an `otherwise` starts a new 107 | # set of conditional assignments. 108 | w |= 7 109 | 110 | This :data:`.conditional_assignment` is equivalent to:: 111 | 112 | r1.next <<= pyrtl.select(a, 1, pyrtl.select(c, 4, r1)) 113 | r2.next <<= pyrtl.select(a, pyrtl.select(b, 3, r2), pyrtl.select(c, 5, 6)) 114 | w <<= pyrtl.select(d, 7, 0) 115 | mem[0] <<= pyrtl.MemBlock.EnabledWrite(data=2, enable=a) 116 | 117 | Conditional assignments are generally recommended over nested :func:`.select` statements 118 | because conditional assignments are easier to read and write. 119 | 120 | :data:`.conditional_assignment` accepts an optional ``default`` argument that 121 | maps from :class:`.WireVector` to its default value for the 122 | :data:`.conditional_assignment` block. ``defaults`` are not supported for 123 | :class:`.MemBlock`. See :ref:`conditional_assignment_defaults` for more details. 124 | 125 | See `the state machine example 126 | `_ 127 | for more examples of :data:`.conditional_assignment`. 128 | 129 | .. autodata:: pyrtl.otherwise 130 | 131 | Context manager implementing PyRTL's ``otherwise`` under :data:`.conditional_assignment`. 132 | 133 | .. autofunction:: pyrtl.currently_under_condition 134 | 135 | .. _conditional_assignment_defaults: 136 | 137 | Conditional Assignment Defaults 138 | ------------------------------- 139 | 140 | Every PyRTL wire, register, and memory must have a value in every cycle. PyRTL does not 141 | support "don't care" or ``X`` values. To satisfy this requirement, conditional 142 | assignment must always assign a value to every wire in a :data:`.conditional_assignment` 143 | block, even if the :data:`.conditional_assignment` does not specify a value. This can 144 | happen when: 145 | 146 | #. A condition is ``True``, but no value is specified for a wire or register in that 147 | condition's ``with`` block. In the example above, no value is specified for ``r1`` in 148 | the :data:`.otherwise` block. 149 | #. No conditions are ``True``, and there is no :data:`.otherwise` block. In the example 150 | above, there is no :data:`.otherwise` block to for the case when ``d`` is ``False``, 151 | so no value is specified for ``w`` when ``d`` is ``False``. 152 | 153 | When this happens for a wire, ``0`` is assigned as a default value. See how a ``0`` 154 | appears in the final :func:`.select` in the equivalent example above. 155 | 156 | When this happens for a register, the register's current value is assigned as a default 157 | value. See how ``r1`` and ``r2`` appear within the :func:`.select` s in the first and second 158 | lines of the example above. 159 | 160 | When this happens for a memory, the memory's write port is disabled. See how the example 161 | above uses a :class:`.EnabledWrite` to disable writes to ``mem[0]`` when ``a`` is 162 | ``False``. 163 | 164 | These default values can be changed by passing a ``defaults`` dict to 165 | :data:`.conditional_assignment`, as seen in this example:: 166 | 167 | # Most instructions advance the program counter (`pc`) by one instruction. A few 168 | # instructions change `pc` in special ways. 169 | pc = pyrtl.Register(bitwidth=32) 170 | instr = pyrtl.WireVector(bitwidth=32) 171 | res = pyrtl.WireVector(bitwidth=32) 172 | 173 | op = instr[:7] 174 | ADD = 0b0110011 175 | JMP = 0b1101111 176 | 177 | # Use conditional_assignment's `defaults` to advance `pc` by one instruction by 178 | # default. 179 | with pyrtl.conditional_assignment(defaults={pc: pc + 1}): 180 | with op == ADD: 181 | res |= instr[15:20] + instr[20:25] 182 | # pc.next will be updated to pc + 1 183 | with op == JMP: 184 | pc.next |= pc + instr[7:] 185 | # res will be set to 0 186 | 187 | .. WARNING:: 188 | :data:`.conditional_assignment` ``defaults`` are not supported for 189 | :class:`.MemBlock`. 190 | 191 | The Conditional Assigment Operator (``|=``) 192 | ------------------------------------------- 193 | 194 | Conditional assignments are written with the ``|=`` 195 | (:meth:`~.WireVector.__ior__`) operator, and not the usual ``<<=`` 196 | (:meth:`~.WireVector.__ilshift__`) operator. 197 | 198 | * The ``|=`` operator is a *conditional* assignment. Conditional assignments can only be 199 | written in a :data:`.conditional_assignment` block. 200 | * The ``<<=`` operator is an *unconditional* assignment, *even if* it is written in a 201 | :data:`.conditional_assignment` block. 202 | 203 | Consider this example:: 204 | 205 | w1 = pyrtl.WireVector() 206 | w2 = pyrtl.WireVector() 207 | with pyrtl.conditional_assignment: 208 | with a: 209 | w1 |= 1 210 | w2 <<= 2 211 | 212 | Which is equivalent to:: 213 | 214 | w1 <<= pyrtl.select(a, 1, 0) 215 | w2 <<= 2 216 | 217 | This behavior may seem undesirable, but consider this example:: 218 | 219 | def make_adder(x: pyrtl.WireVector) -> pyrtl.WireVector: 220 | output = pyrtl.WireVector(bitwidth=a.bitwidth + 1) 221 | output <<= x + 2 222 | return output 223 | 224 | w = pyrtl.WireVector() 225 | with pyrtl.conditional_assignment: 226 | with a: 227 | w |= make_adder(b) 228 | 229 | Which is equivalent to:: 230 | 231 | # The assignment to `output` in `make_adder` is unconditional. 232 | w <<= pyrtl.select(a, make_adder(b), 0) 233 | 234 | In this example the ``<<=`` in ``make_adder`` should be unconditional, even though 235 | ``make_adder`` is called from a :data:`.conditional_assignment`, because the top-level 236 | assignment to ``w`` is already conditional. Making the lower-level assignment to 237 | ``output`` conditional would not make sense, especially if ``output`` is used elsewhere 238 | in the circuit. 239 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 3 | 4 | PyRTL 5 | ===== 6 | 7 | [![PyPI version](https://badge.fury.io/py/pyrtl.svg)](http://badge.fury.io/py/pyrtl) 8 | [![Build Status](https://github.com/UCSBarchlab/PyRTL/actions/workflows/python-test.yml/badge.svg)](https://github.com/UCSBarchlab/PyRTL/actions/workflows/python-test.yml) 9 | [![Code Coverage](https://codecov.io/github/UCSBarchlab/PyRTL/coverage.svg?branch=development)](https://codecov.io/github/UCSBarchlab/PyRTL?branch=development) 10 | [![Documentation Status](https://readthedocs.org/projects/pyrtl/badge/?version=latest)](http://pyrtl.readthedocs.org/en/latest/?badge=latest) 11 | [![Binder](https://mybinder.org/badge.svg)](https://mybinder.org/v2/gh/UCSBarchlab/PyRTL/development?filepath=%2Fipynb-examples%2F) 12 | 13 | PyRTL provides a collection of classes for Pythonic [register-transfer 14 | level](https://en.wikipedia.org/wiki/Register-transfer_level) design, 15 | simulation, tracing, and testing suitable for teaching and research. 16 | Simplicity, usability, clarity, and extensibility rather than performance or 17 | optimization is the overarching goal. Features include: 18 | 19 | * Elaboration-through-execution, meaning all of Python can be used including 20 | introspection 21 | * Design, instantiate, and simulate all in one file and without leaving Python 22 | * Export to, or import from, common HDLs (BLIF-in, Verilog-out currently 23 | supported) 24 | * Examine execution with waveforms on the terminal or export to `.vcd` as 25 | projects scale 26 | * Elaboration, synthesis, and basic optimizations all included 27 | * Small and well-defined internal core structure means writing new transforms 28 | is easier 29 | * Batteries included means many useful components are already available and 30 | more are coming every week 31 | 32 | What README would be complete without a screenshot? Below you can see the 33 | waveform rendered right on the terminal for a small state machine written in 34 | PyRTL. 35 | 36 | ![Command-line waveform for PyRTL state machine](https://github.com/UCSBarchlab/PyRTL/blob/development/docs/screenshots/pyrtl-statemachine.png?raw=true "PyRTL State Machine Screenshot") 37 | 38 | ### Tutorials and Documentation 39 | 40 | * For users, more info and demo code is available on the [PyRTL project web 41 | page](http://ucsbarchlab.github.io/PyRTL/). 42 | * Try the examples in the 43 | [`examples`](https://github.com/UCSBarchlab/PyRTL/tree/development/examples) 44 | directory. You can also [try the examples on 45 | MyBinder](https://mybinder.org/v2/gh/UCSBarchlab/PyRTL/development?filepath=%2Fipynb-examples%2F). 46 | * Full reference documentation is available at https://pyrtl.readthedocs.io/ 47 | 48 | ### Package Contents 49 | 50 | If you are just getting started with PyRTL it is suggested that you start with 51 | the 52 | [`examples`](https://github.com/UCSBarchlab/PyRTL/tree/development/examples) 53 | first to get a sense of the "thinking with PyRTLs" required to design hardware 54 | in this way. If you are looking for a deeper understanding, dive into the code 55 | for the object `Block`. It is the core data structure at the heart of PyRTL and 56 | defines its semantics at a high level -- everything is converted to or from the 57 | small, simple set of primitives defined there. 58 | 59 | The package contains the following files and directories: 60 | * [`pyrtl`](https://github.com/UCSBarchlab/PyRTL/tree/development/pyrtl) 61 | The src directory for the module. 62 | * [`pyrtl/rtllib/`](https://github.com/UCSBarchlab/PyRTL/tree/development/pyrtl/rtllib) 63 | Finished PyRTL libraries which are hopefully both useful and documented. 64 | * [`examples`](https://github.com/UCSBarchlab/PyRTL/tree/development/examples) 65 | A set of hardware design examples that show the main idea behind PyRTL. 66 | * [`tests`](https://github.com/UCSBarchlab/PyRTL/tree/development/tests) 67 | A set of unit tests for PyRTL which you can run with `pytest`. 68 | * [`docs`](https://github.com/UCSBarchlab/PyRTL/tree/development/docs) 69 | Location of the Sphinx documentation. 70 | 71 | ### The PyRTL Development Environment 72 | 73 | All PyRTL developers should use the exact same tools to avoid confusing 74 | situations where a test fails only on one person's computer, or the generated 75 | documentation looks weird on another person's computer. 76 | 77 | PyRTL uses [`uv`](https://docs.astral.sh/uv/) to ensure everyone's development 78 | environments are consistent. `uv` manages the installation and versioning for 79 | all other PyRTL developer tools, like `pytest` and `ruff`. 80 | 81 | So to set up a PyRTL development environment, you only need to install `uv`, by 82 | following the 83 | [`uv` installation instructions](https://docs.astral.sh/uv/getting-started/installation/) 84 | 85 | After installing [`uv`](https://docs.astral.sh/uv/), you can run all the tests 86 | with: 87 | 88 | ```shell 89 | $ uv run just tests 90 | ``` 91 | 92 | And you can generate the Sphinx documentation with: 93 | 94 | ```shell 95 | $ uv run just docs 96 | ``` 97 | 98 | `uv` will download and install Python and any required `pip` packages as 99 | needed. `uv` caches installed software so future `uv` invocations will be fast. 100 | 101 | ### Contributing to PyRTL 102 | 103 | *Picking a first project* 104 | 105 | * One of the earliest things you should submit is a unit test that hits some 106 | [uncovered lines of code in 107 | PyRTL](https://codecov.io/github/UCSBarchlab/PyRTL?branch=development). For 108 | example, pick a `PyrtlError` that is not covered and add a unit test in 109 | [`tests`](https://github.com/UCSBarchlab/PyRTL/tree/development/tests) 110 | that will hit it. 111 | * After you have that down check in the [PyRTL 112 | Issues](https://github.com/UCSBarchlab/PyRTL/issues) list for a feature that 113 | is marked as "beginner friendly". 114 | * Once you have that down, ask for access to the PyRTL-research repo where we 115 | keep a list of more advanced features and designs that could use more help! 116 | 117 | *Coding style* 118 | 119 | * All major functionality should have unit tests covering and documenting their 120 | use 121 | * All public functions and methods should have useful docstrings 122 | * All code needs to conform to 123 | [PEP8](https://www.python.org/dev/peps/pep-0008/) conventions 124 | * No new root-level dependencies on external libs, import locally if required 125 | for special functions 126 | 127 | *Workflow* 128 | 129 | * A useful reference for working with Git is this [Git 130 | tutorial](https://www.atlassian.com/git/tutorials/) 131 | * A useful Git Fork workflow for working on this repo is [found 132 | here](http://blog.scottlowe.org/2015/01/27/using-fork-branch-git-workflow/) 133 | * The `development` branch is the primary stable working branch (everyone is 134 | invited to submit pull requests) 135 | * Bugs and minor enhancements tracked directly through the [issue 136 | tracker](https://github.com/UCSBarchlab/PyRTL/issues) 137 | * When posting a bug please post a small chunk of code that captures the bug, 138 | e.g. [Issue #56](https://github.com/UCSBarchlab/PyRTL/issues/56) 139 | * When pushing a fix to a bug or enhancement please reference issue number in 140 | commit message, e.g. [Fix to Issue 141 | #56](https://github.com/UCSBarchlab/PyRTL/commit/1d5730db168a9e4490c580cb930075715468047a) 142 | * Before sending a pull request, please run: 143 | 144 | ```shell 145 | $ uv run just presubmit 146 | ``` 147 | 148 | to verify that all tests pass and that documentation can be generated with 149 | your changes. 150 | 151 | *Documentation* 152 | 153 | * All important functionality should have an executable example in 154 | [`examples`](https://github.com/UCSBarchlab/PyRTL/tree/development/examples) 155 | * All classes should have a block comment with high level description of the 156 | class 157 | * All functions should follow the following (Sphinx parsable) docstring format: 158 | ```python 159 | """One Line Summary (< 80 chars) of the function, followed by period. 160 | 161 | A long description of what this function does. Talk about what the user 162 | should expect from this function and also what the users needs to do to use 163 | the function (this part is optional). 164 | 165 | :param param_name : Description of this parameter. 166 | :param param_name : Longer parameter descriptions take up a newline with four 167 | leading spaces like this. 168 | :return: Description of function's return value. 169 | """ 170 | # Developer Notes (Optional): 171 | # 172 | # These would be anything that the user does not need to know in order to use 173 | # the functions. 174 | # These notes can include internal workings of the function, the logic behind 175 | # it, or how to extend it. 176 | ``` 177 | * Sphinx parses [Python type 178 | annotations](https://docs.python.org/3/library/typing.html), so put type 179 | information into annotations instead of docstrings. 180 | * The Sphinx-generated documentation is published to 181 | https://pyrtl.readthedocs.io/ 182 | * PyRTL's Sphinx build process is documented in 183 | [`docs/README.md`](https://github.com/UCSBarchlab/PyRTL/blob/development/docs/README.md). 184 | * PyRTL's release process is documented in 185 | [`docs/release/README.md`](https://github.com/UCSBarchlab/PyRTL/blob/development/docs/release/README.md). 186 | 187 | ### Using PyRTL 188 | 189 | We love to hear from users about their projects, and if there are issues we 190 | will try our best to push fixes quickly. You can read more about how we have 191 | been using it in our research at UCSB both in simulation and on FPGAs in [our 192 | PyRTL paper at FPL](http://www.cs.ucsb.edu/~sherwood/pubs/FPL-17-pyrtl.pdf). 193 | 194 | ### Related Projects 195 | 196 | It is always important to point out that PyRTL builds on the ideas of several 197 | other related projects as we all share the common goal of trying to make 198 | hardware design a better experience! You can read more about those 199 | relationships on our [PyRTL project web 200 | page](http://ucsbarchlab.github.io/PyRTL/). 201 | -------------------------------------------------------------------------------- /examples/introduction-to-hardware.py: -------------------------------------------------------------------------------- 1 | # # Introduction to Hardware Design 2 | 3 | # This code works through the hardware design process with the audience of software 4 | # developers more in mind. We start with the simple problem of designing a Fibonacci 5 | # sequence calculator (http://oeis.org/A000045). 6 | import pyrtl 7 | 8 | 9 | # ## Software Fibonacci implementation 10 | def software_fibonacci(n: int): 11 | """An ordinary Python function to return the `n`th Fibonacci number.""" 12 | a = 0 13 | b = 1 14 | for _ in range(n): 15 | a, b = b, a + b 16 | return a 17 | 18 | 19 | # `software_fibonacci` is an iterative Fibonacci implementation. It repeatedly adds `a` 20 | # and `b` to calculate the `n`th number in the sequence. 21 | print("n software_fibonacci(n)") 22 | print("─────────────────────────") 23 | for n in range(10): 24 | print(n, " ", software_fibonacci(n)) 25 | 26 | 27 | # ## Hardware Fibonacci implementation, first attempt 28 | # 29 | # So let's convert this into some hardware that computes the same thing. Our first go 30 | # will be to just replace the `0` and `1` with `WireVectors` to see what happens. 31 | def attempt1_hardware_fibonacci(n: int) -> pyrtl.WireVector: 32 | a = pyrtl.Const(0) 33 | b = pyrtl.Const(1) 34 | for _ in range(n): 35 | a, b = b, a + b 36 | return a 37 | 38 | 39 | # The above looks really nice but does not really represent a hardware implementation 40 | # of Fibonacci. Let's reason through the code, line by line, to figure out what 41 | # it would actually build. 42 | # 43 | # a = pyrtl.Const(0) # ← This makes a `WireVector` with `bitwidth=1` that is 44 | # # driven by a zero. Thus `a` is a `WireVector`. Seems 45 | # # good. 46 | # b = pyrtl.Const(1) # ← Just like above, `b` is a `WireVector` driven by `1`. 47 | # for i in range(n): # ← Okay, here is where things start to go off the rails a 48 | # # bit. This says to perform the following code `n` 49 | # # times, but the value `n` is passed as an argument and 50 | # # is not something that is evaluated in the hardware; it 51 | # # is evaluated when you run the PyRTL program which 52 | # # generates (or more specifically elaborates) the 53 | # # hardware. Thus the hardware we are building will have 54 | # # the value of `n` built into the hardware and won't 55 | # # actually be a run-time parameter. Loops are useful for 56 | # # building large repetitive hardware structures, but 57 | # # they CAN'T be used to represent hardware that should 58 | # # do a computation iteratively. Instead we will need 59 | # # some `Registers` to build a state machine. 60 | # a, b = b, a + b # ← Let's break this apart. In the first cycle, `a` is 61 | # # `Const(0) and `b` is `Const(1)`, so `(a + b)` builds 62 | # # an adder with `Const(0)` and `Const(1)` as inputs. 63 | # # So `(b, a + b)` in the first iteration evaluates to: 64 | # # `(Const(1), result_of_adding(Const(0), Const(1)))`. 65 | # # At the end of the first iteration, `a` and `b` refer 66 | # # to those two constant values. In each following 67 | # # iteration more adders are built and the Python 68 | # # variables `a` and `b` are bound to larger and larger 69 | # # trees of adders. But all the adder inputs are 70 | # # constants! 71 | # return a # ← The final thing that is returned then is the last 72 | # # output from this tree of adders which all have 73 | # # `Consts` as inputs. Thus this hardware is hard-wired 74 | # # to produce only the exact the value of the `n`th 75 | # # Fibonacci number, where `n` is hard-coded into the 76 | # # design! This is most likely not what you want. 77 | # 78 | # ## Hardware Fibonacci implementation, second attempt 79 | # 80 | # So let's try a different approach. Let's specify two registers (`a` and `b`) and then 81 | # we can update those values as we iteratively compute the `n`th Fibonacci number cycle 82 | # by cycle. 83 | def attempt2_hardware_fibonacci(bitwidth: int) -> pyrtl.WireVector: 84 | a = pyrtl.Register(bitwidth, "a") 85 | b = pyrtl.Register(bitwidth, "b") 86 | 87 | a.next <<= b 88 | b.next <<= a + b 89 | 90 | return a 91 | 92 | 93 | # This is looking much better. Two registers, `a` and `b`, store the values from which 94 | # we can compute the series. The line `a.next <<= b` means that the value of `a` in the 95 | # next cycle should be simply be `b` from the current cycle. The line `b.next <<= a + b` 96 | # says to build an adder with inputs of `a` and `b` from the current cycle and assign 97 | # the value to `b` in the next cycle. A visual representation of the hardware built is 98 | # as such: 99 | # 100 | # ┌─────┐ ┌─────┐ 101 | # │ │ │ │ 102 | # ▼ │ ▼ │ 103 | # ▕▔▔▔▔▔▏ │ ▕▔▔▔▔▔▏ │ 104 | # ▕ a ▏ │ ▕ b ▏ │ 105 | # ▕▁▁▁▁▁▏ │ ▕▁▁▁▁▁▏ │ 106 | # │ │ │ │ 107 | # │ └─────┤ │ 108 | # │ │ │ 109 | # ▼ ▼ │ 110 | # ╲▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔╱ │ 111 | # ╲ adder ╱ │ 112 | # ╲▁▁▁▁▁▁▁▁▁▁▁╱ │ 113 | # │ │ 114 | # └───────────┘ 115 | # 116 | # Note that in the picture the register `a` and `b` each have a `WireVector` which is 117 | # the current value (shown flowing out of the bottom of the register) and an "input" 118 | # which is giving the value that should be the value of the register in the following 119 | # cycle (shown flowing into the top of the register) which are `b` and `a + b` 120 | # respectively. When we say `return a` what we are returning is a reference to the 121 | # register `a` in the picture above. 122 | # 123 | # ## Hardware Fibonacci implementation, third attempt 124 | # 125 | # Of course one problem is that we don't know when we are done. How do we know we 126 | # reached the `n`th number in the sequence? Well, we need to add a `Register` to keep 127 | # count and see if we are done. 128 | def attempt3_hardware_fibonacci( 129 | n: pyrtl.WireVector, 130 | ) -> tuple[pyrtl.WireVector, pyrtl.WireVector]: 131 | a = pyrtl.Register(bitwidth=n.bitwidth, name="a") 132 | b = pyrtl.Register(bitwidth=n.bitwidth, name="b") 133 | counter = pyrtl.Register(bitwidth=n.bitwidth, name="counter") 134 | 135 | counter.next <<= counter + 1 136 | a.next <<= b 137 | b.next <<= a + b 138 | 139 | return a, counter == n 140 | 141 | 142 | # This is very similar to the example before, except that now we have a register 143 | # `counter` which keeps track of the iteration that we are on: 144 | # counter.next <<= counter + 1 145 | # The function now returns two values, a reference to the register `a` and a reference 146 | # to a single bit that tells us if we are done. That bit is calculated by comparing 147 | # `counter` to the to a WireVector `n` that is passed in to see if they are the same. 148 | # 149 | # ## Hardware Fibonacci implementation, fourth attempt 150 | # 151 | # Finally, we need a way to indicate that we want a new Fibonacci number. We'll add 152 | # another input, `start`, which when high sets our `local_n` register and resets the 153 | # others. Now our ending condition occurs when the current iteration `counter` is equal 154 | # to the locally stored `local_n`. 155 | def attempt4_hardware_fibonacci( 156 | n: pyrtl.WireVector, start: pyrtl.WireVector 157 | ) -> tuple[pyrtl.WireVector, pyrtl.WireVector]: 158 | a = pyrtl.Register(bitwidth=n.bitwidth, name="a") 159 | b = pyrtl.Register(bitwidth=n.bitwidth, name="b") 160 | counter = pyrtl.Register(bitwidth=n.bitwidth, name="counter") 161 | local_n = pyrtl.Register(bitwidth=n.bitwidth, name="local_n") 162 | local_done = pyrtl.WireVector(bitwidth=1, name="local_done") 163 | 164 | with pyrtl.conditional_assignment: 165 | with start: 166 | local_n.next |= n 167 | counter.next |= 0 168 | a.next |= 0 169 | b.next |= 1 170 | with pyrtl.otherwise: 171 | counter.next |= counter + 1 172 | a.next |= b 173 | b.next |= a + b 174 | local_done <<= counter == local_n 175 | return a, local_done 176 | 177 | 178 | # ## Simulating the hardware Fibonacci implementation 179 | # 180 | # This is now far enough along that we can simulate the design and see what happens. We 181 | # begin by connecting our input and output wires to the implementation, stepping once 182 | # with the `start` signal high to signify we're starting a new Fibonacci request, and 183 | # then continuing to `step()` until `done` is asserted. Note that although the Fibonacci 184 | # implementation only uses the value of `n` when `start` is high, we must still provide 185 | # a value for `n` (and all other `Inputs` tracked by the `Simulation`) for each 186 | # `step()`. 187 | print("\nHardware Fibonacci Simulation:") 188 | n = pyrtl.Input(bitwidth=8, name="n") 189 | start = pyrtl.Input(bitwidth=1, name="start") 190 | fib_out = pyrtl.Output(bitwidth=8, name="fib") 191 | done_out = pyrtl.Output(bitwidth=1, name="done") 192 | 193 | fib, done = attempt4_hardware_fibonacci(n, start) 194 | fib_out <<= fib 195 | done_out <<= done 196 | 197 | sim = pyrtl.Simulation() 198 | 199 | sim.step({"n": 7, "start": 1}) 200 | 201 | sim.step({"n": 0, "start": 0}) 202 | while not sim.inspect("done"): 203 | sim.step({"n": 0, "start": 0}) 204 | 205 | sim.tracer.render_trace( 206 | trace_list=["n", "start", "counter", "fib", "done"], repr_func=int 207 | ) 208 | 209 | assert sim.inspect("fib") == software_fibonacci(7) 210 | -------------------------------------------------------------------------------- /tests/rtllib/test_multipliers.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | import random 3 | import unittest 4 | 5 | import pyrtl 6 | import pyrtl.rtllib.testingutils as utils 7 | from pyrtl.rtllib import adders, multipliers 8 | 9 | 10 | class TestDocTests(unittest.TestCase): 11 | """Test documentation examples.""" 12 | 13 | def test_doctests(self): 14 | failures, tests = doctest.testmod(m=pyrtl.rtllib.multipliers) 15 | self.assertGreater(tests, 0) 16 | self.assertEqual(failures, 0) 17 | 18 | 19 | class TestSimpleMult(unittest.TestCase): 20 | def setUp(self): 21 | pyrtl.reset_working_block() 22 | 23 | def test_trivial_case(self): 24 | self.mult_t_base(1, 5) 25 | 26 | def test_trivial_case_2(self): 27 | self.mult_t_base(2, 1) 28 | 29 | def test_trivial_case_3(self): 30 | self.mult_t_base(1, 1) 31 | 32 | def test_simple_mult_1(self): 33 | self.mult_t_base(5, 7) 34 | 35 | def test_simple_mult_2(self): 36 | self.mult_t_base(2, 9) 37 | 38 | def mult_t_base(self, len_a, len_b): 39 | a, b, reset = ( 40 | pyrtl.Input(len_a, "a"), 41 | pyrtl.Input(len_b, "b"), 42 | pyrtl.Input(1, "reset"), 43 | ) 44 | product, done = pyrtl.Output(name="product"), pyrtl.Output(name="done") 45 | m_prod, m_done = multipliers.simple_mult(a, b, reset) 46 | product <<= m_prod 47 | done <<= m_done 48 | self.assertEqual(len(product), len_a + len_b) 49 | 50 | xvals = [int(random.uniform(0, 2**len_a - 1)) for i in range(20)] 51 | yvals = [int(random.uniform(0, 2**len_b - 1)) for i in range(20)] 52 | true_result = [i * j for i, j in zip(xvals, yvals, strict=True)] 53 | mult_results = [] 54 | 55 | for x_val, y_val in zip(xvals, yvals, strict=True): 56 | sim = pyrtl.Simulation() 57 | sim.step({a: x_val, b: y_val, reset: 1}) 58 | while not sim.inspect("done"): 59 | sim.step({a: 0, b: 0, reset: 0}) 60 | 61 | # Extracting the values and verifying correctness 62 | mult_results.append(sim.inspect("product")) 63 | self.assertEqual(mult_results, true_result) 64 | 65 | 66 | class TestComplexMult(unittest.TestCase): 67 | def setUp(self): 68 | pyrtl.reset_working_block() 69 | 70 | def test_trivial_case(self): 71 | with self.assertRaises(pyrtl.PyrtlError): 72 | self.mult_t_base(1, 5, 2) 73 | 74 | def test_trivial_case_2(self): 75 | with self.assertRaises(pyrtl.PyrtlError): 76 | self.mult_t_base(2, 1, 5) 77 | 78 | def test_trivial_case_3(self): 79 | self.mult_t_base(1, 1, 1) 80 | 81 | def test_complex_mult_1(self): 82 | self.mult_t_base(5, 7, 3) 83 | 84 | def test_complex_mult_2(self): 85 | self.mult_t_base(10, 12, 3) 86 | 87 | def test_complex_mult_3(self): 88 | with self.assertRaises(pyrtl.PyrtlError): 89 | self.mult_t_base(2, 9, 4) 90 | 91 | def test_complex_mult_4(self): 92 | with self.assertRaises(pyrtl.PyrtlError): 93 | self.mult_t_base(8, 4, 6) 94 | 95 | def mult_t_base(self, len_a, len_b, shifts): 96 | a, b = pyrtl.Input(len_a, "a"), pyrtl.Input(len_b, "b") 97 | reset = pyrtl.Input(1, "reset") 98 | product, done = pyrtl.Output(name="product"), pyrtl.Output(name="done") 99 | m_prod, m_done = multipliers.complex_mult(a, b, shifts, reset) 100 | product <<= m_prod 101 | done <<= m_done 102 | self.assertEqual(len(product), len_a + len_b) 103 | 104 | xvals = [int(random.uniform(0, 2**len_a - 1)) for i in range(20)] 105 | yvals = [int(random.uniform(0, 2**len_b - 1)) for i in range(20)] 106 | true_result = [i * j for i, j in zip(xvals, yvals, strict=True)] 107 | mult_results = [] 108 | 109 | for x_val, y_val in zip(xvals, yvals, strict=True): 110 | sim = pyrtl.Simulation() 111 | sim.step({a: x_val, b: y_val, reset: 1}) 112 | while not sim.inspect("done"): 113 | sim.step({a: 0, b: 0, reset: 0}) 114 | 115 | # Extracting the values and verifying correctness 116 | mult_results.append(sim.inspect("product")) 117 | self.assertEqual(mult_results, true_result) 118 | 119 | 120 | class TestWallace(unittest.TestCase): 121 | @classmethod 122 | def setUpClass(cls): 123 | # this is to ensure reproducibility 124 | random.seed(777906376) 125 | 126 | def setUp(self): 127 | pyrtl.reset_working_block() 128 | 129 | def mult_t_base(self, len_a, len_b, **mult_args): 130 | # Creating the logic nets 131 | a, b = pyrtl.Input(len_a, "a"), pyrtl.Input(len_b, "b") 132 | product = pyrtl.Output(name="product") 133 | product <<= multipliers.tree_multiplier(a, b, **mult_args) 134 | 135 | self.assertEqual(len(product), len_a + len_b) 136 | 137 | # creating the testing values and the correct results 138 | xvals = [int(random.uniform(0, 2**len_a - 1)) for i in range(20)] 139 | yvals = [int(random.uniform(0, 2**len_b - 1)) for i in range(20)] 140 | true_result = [i * j for i, j in zip(xvals, yvals, strict=True)] 141 | 142 | # Setting up and running the tests 143 | sim = pyrtl.Simulation() 144 | for cycle in range(len(xvals)): 145 | sim.step({a: xvals[cycle], b: yvals[cycle]}) 146 | 147 | # Extracting the values and verifying correctness 148 | multiplier_result = sim.tracer.trace[product.name] 149 | self.assertEqual(multiplier_result, true_result) 150 | 151 | def test_trivial_case(self): 152 | self.mult_t_base(1, 5) 153 | 154 | def test_trivial_case_2(self): 155 | self.mult_t_base(2, 1) 156 | 157 | def test_trivial_case_3(self): 158 | self.mult_t_base(1, 1) 159 | 160 | def test_wallace_tree_1(self): 161 | self.mult_t_base(5, 7) 162 | 163 | def test_wallace_tree_2(self): 164 | self.mult_t_base(2, 9) 165 | 166 | def test_dada_tree(self): 167 | self.mult_t_base(5, 10, reducer=adders.dada_reducer) 168 | 169 | def test_fma_1(self): 170 | wires, vals = utils.make_inputs_and_values( 171 | exact_bitwidth=10, num_wires=3, dist=utils.inverse_power_dist 172 | ) 173 | test_w = multipliers.fused_multiply_adder( 174 | wires[0], 175 | wires[1], 176 | wires[2], 177 | False, 178 | reducer=adders.dada_reducer, 179 | adder_func=adders.ripple_add, 180 | ) 181 | self.assertEqual(len(test_w), 20) 182 | outwire = pyrtl.Output(21, "test") 183 | outwire <<= test_w 184 | 185 | out_vals = utils.sim_and_ret_out(outwire, wires, vals) 186 | true_result = [ 187 | vals[0][cycle] * vals[1][cycle] + vals[2][cycle] 188 | for cycle in range(len(vals[0])) 189 | ] 190 | self.assertEqual(out_vals, true_result) 191 | 192 | def test_gen_fma_1(self): 193 | wires, vals = utils.make_inputs_and_values( 194 | max_bitwidth=8, num_wires=8, dist=utils.inverse_power_dist 195 | ) 196 | # mixing tuples and lists solely for readability purposes 197 | mult_pairs = [(wires[0], wires[1]), (wires[2], wires[3]), (wires[4], wires[5])] 198 | add_wires = (wires[6], wires[7]) 199 | 200 | outwire = pyrtl.Output(name="test") 201 | outwire <<= multipliers.generalized_fma(mult_pairs, add_wires, signed=False) 202 | 203 | out_vals = utils.sim_and_ret_out(outwire, wires, vals) 204 | true_result = [ 205 | vals[0][cycle] * vals[1][cycle] 206 | + vals[2][cycle] * vals[3][cycle] 207 | + vals[4][cycle] * vals[5][cycle] 208 | + vals[6][cycle] 209 | + vals[7][cycle] 210 | for cycle in range(len(vals[0])) 211 | ] 212 | self.assertEqual(out_vals, true_result) 213 | 214 | 215 | class TestSignedTreeMult(unittest.TestCase): 216 | @classmethod 217 | def setUpClass(cls): 218 | # this is to ensure reproducibility 219 | random.seed(777906375) 220 | 221 | def setUp(self): 222 | pyrtl.reset_working_block() 223 | 224 | def mult_t_base(self, len_a, len_b, **mult_args): 225 | # Creating the logic nets 226 | a, b = pyrtl.Input(len_a, "a"), pyrtl.Input(len_b, "b") 227 | product = pyrtl.Output(name="product") 228 | product <<= multipliers.signed_tree_multiplier(a, b, **mult_args) 229 | 230 | self.assertEqual(len(product), len_a + len_b) 231 | 232 | # creating the testing values and the correct results 233 | bound_a = 2 ** (len_a - 1) - 1 234 | bound_b = 2 ** (len_b - 1) - 1 235 | xvals = [int(random.uniform(-bound_a, bound_a)) for i in range(20)] 236 | yvals = [int(random.uniform(-bound_b, bound_b)) for i in range(20)] 237 | true_result = [i * j for i, j in zip(xvals, yvals, strict=True)] 238 | 239 | # Setting up and running the tests 240 | sim = pyrtl.Simulation() 241 | for cycle in range(len(xvals)): 242 | sim.step({a: xvals[cycle], b: yvals[cycle]}) 243 | 244 | # Extracting the values and verifying correctness 245 | multiplier_result = [ 246 | pyrtl.val_to_signed_integer(p, len(product)) 247 | for p in sim.tracer.trace[product.name] 248 | ] 249 | self.assertEqual(multiplier_result, true_result) 250 | 251 | def test_small_bitwidth_error(self): 252 | with self.assertRaises(pyrtl.PyrtlError): 253 | self.mult_t_base(1, 1) 254 | 255 | def test_trivial_case(self): 256 | self.mult_t_base(2, 3) 257 | 258 | def test_trivial_case_2(self): 259 | self.mult_t_base(4, 4) 260 | 261 | def test_trivial_case_3(self): 262 | self.mult_t_base(3, 4) 263 | 264 | def test_wallace_tree_1(self): 265 | self.mult_t_base(10, 3) 266 | 267 | def test_wallace_tree_2(self): 268 | self.mult_t_base(8, 8) 269 | 270 | def test_dada_tree(self): 271 | self.mult_t_base(5, 10, reducer=adders.dada_reducer) 272 | 273 | 274 | if __name__ == "__main__": 275 | unittest.main() 276 | --------------------------------------------------------------------------------