├── .coveragerc ├── .github └── workflows │ ├── python-release.yml │ └── python-test.yml ├── .gitignore ├── .readthedocs.yaml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── codecov.yml ├── docs ├── Makefile ├── README.md ├── analysis.rst ├── basic.rst ├── blocks.rst ├── brand │ └── pyrtl_logo.png ├── conf.py ├── export.rst ├── helpers.rst ├── images │ └── gcd-graph.png ├── index.rst ├── regmem.rst ├── release │ ├── README.md │ └── requirements.txt ├── requirements.in ├── requirements.txt ├── rtllib.rst ├── screenshots │ ├── pyrtl-counter.png │ ├── pyrtl-renderer-demo-ascii.png │ ├── pyrtl-renderer-demo-cp437.png │ ├── pyrtl-renderer-demo-powerline.png │ ├── pyrtl-renderer-demo-utf-8-alt.png │ ├── pyrtl-renderer-demo-utf-8.png │ └── pyrtl-statemachine.png └── simtest.rst ├── examples ├── example0-minimum-viable-hardware.py ├── example1-combologic.py ├── example1.1-signed-numbers.py ├── example1.2-wire-struct.py ├── example2-counter.py ├── example3-statemachine.py ├── example4-debuggingtools.py ├── example5-introspection.py ├── example6-memory.py ├── example7-synth-timing.py ├── example8-verilog.py ├── example9-transforms-draft.py ├── introduction-to-hardware.py └── renderer-demo.py ├── ipynb-examples ├── example1-combologic.ipynb ├── example2-counter.ipynb ├── example3-statemachine.ipynb ├── example4-debuggingtools.ipynb ├── example5-introspection.ipynb ├── example6-memory.ipynb ├── example7-synth-timing.ipynb ├── example8-verilog.ipynb └── introduction-to-hardware.ipynb ├── pyproject.toml ├── pyrtl ├── __init__.py ├── analysis.py ├── compilesim.py ├── conditional.py ├── core.py ├── corecircuits.py ├── helperfuncs.py ├── importexport.py ├── memory.py ├── passes.py ├── pyrtlexceptions.py ├── rtllib │ ├── __init__.py │ ├── adders.py │ ├── aes.py │ ├── barrel.py │ ├── libutils.py │ ├── matrix.py │ ├── multipliers.py │ ├── muxes.py │ ├── prngs.py │ └── testingutils.py ├── simulation.py ├── transform.py ├── visualization.py └── wire.py ├── requirements.txt ├── tests ├── __init__.py ├── rtllib │ ├── test_adders.py │ ├── test_aes.py │ ├── test_barrel.py │ ├── test_libutils.py │ ├── test_matrix.py │ ├── test_multipliers.py │ ├── test_muxes.py │ └── test_prngs.py ├── test_analysis.py ├── test_compilesim.py ├── test_conditional.py ├── test_core.py ├── test_examples.py ├── test_helperfuncs.py ├── test_importexport.py ├── test_memblock.py ├── test_passes.py ├── test_signed.py ├── test_simulation.py ├── test_transform.py ├── test_visualization.py └── test_wire.py └── tox.ini /.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 | -------------------------------------------------------------------------------- /.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 | - name: Set up Python 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: "3.x" 25 | - name: Install pypa/build 26 | run: python3 -m pip install build 27 | - name: Build distribution archives 28 | run: python3 -m build 29 | - name: Upload distribution archives 30 | uses: actions/upload-artifact@v4 31 | with: 32 | name: python-distribution-archives 33 | path: dist/ 34 | compression-level: 0 35 | 36 | # Publish distribution archive to TestPyPI on tag pushes. 37 | publish-testpypi: 38 | # Only publish to TestPyPI on tag pushes. 39 | if: startsWith(github.ref, 'refs/tags/') 40 | needs: 41 | - build 42 | runs-on: ubuntu-latest 43 | environment: 44 | name: testpypi 45 | url: https://test.pypi.org/p/pyrtl 46 | permissions: 47 | # Required for trusted publishing. 48 | id-token: write 49 | 50 | steps: 51 | - name: Download distribution archives 52 | uses: actions/download-artifact@v4 53 | with: 54 | name: python-distribution-archives 55 | path: dist/ 56 | - name: Publish distribution archives 57 | uses: pypa/gh-action-pypi-publish@release/v1 58 | with: 59 | repository-url: https://test.pypi.org/legacy/ 60 | 61 | # Publish distribution archive to PyPI on tag pushes. The 'pypi' environment 62 | # requires manual approval on GitHub, so this job won't start automatically. 63 | publish-pypi: 64 | # Only publish to PyPI on tag pushes. 65 | if: startsWith(github.ref, 'refs/tags/') 66 | needs: 67 | - build 68 | runs-on: ubuntu-latest 69 | environment: 70 | name: pypi 71 | url: https://pypi.org/p/pyrtl 72 | permissions: 73 | # Required for trusted publishing. 74 | id-token: write 75 | 76 | steps: 77 | - name: Download distribution archives 78 | uses: actions/download-artifact@v4 79 | with: 80 | name: python-distribution-archives 81 | path: dist/ 82 | - name: Publish distribution archives 83 | uses: pypa/gh-action-pypi-publish@release/v1 84 | -------------------------------------------------------------------------------- /.github/workflows/python-test.yml: -------------------------------------------------------------------------------- 1 | name: Run Python tests 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | # When changing the following line, be sure to update `envlist` in 13 | # tox.ini 14 | python-version: [3.9, 3.13] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: python3 -m pip install tox tox-gh-actions 24 | - name: Test with tox 25 | run: tox 26 | - name: Upload coverage to Codecov 27 | uses: codecov/codecov-action@v4 28 | env: 29 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 30 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 | 17 | # Build documentation in the docs/ directory with Sphinx 18 | sphinx: 19 | configuration: docs/conf.py 20 | 21 | # Optionally build your docs in additional formats such as PDF 22 | formats: 23 | - pdf 24 | 25 | # Optionally set the requirements required to build your docs 26 | python: 27 | install: 28 | - requirements: docs/requirements.txt 29 | -------------------------------------------------------------------------------- /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.11.2] - 2024-07-16 10 | 11 | ### Added 12 | 13 | - Added an `initialize_registers` option to `output_to_verilog` 14 | ([documentation](https://pyrtl.readthedocs.io/en/latest/export.html#pyrtl.importexport.output_to_verilog)) 15 | 16 | ### Changed 17 | 18 | - Improved handling of signed integers. 19 | 20 | ### Fixed 21 | 22 | - Fixed a `wire_matrix` bug involving single-element matrices of `Inputs` or `Registers`. 23 | 24 | ## [0.11.1] - 2024-04-22 25 | 26 | ### Added 27 | 28 | - Named `WireVector` slices with `wire_struct` and `wire_matrix`. See documentation: 29 | - [wire_struct](https://pyrtl.readthedocs.io/en/latest/helpers.html#pyrtl.helperfuncs.wire_struct) 30 | - [wire_matrix](https://pyrtl.readthedocs.io/en/latest/helpers.html#pyrtl.helperfuncs.wire_matrix) 31 | 32 | ### Changed 33 | 34 | - Major changes to `render_trace` visualization. See [examples and 35 | documentation](https://pyrtl.readthedocs.io/en/latest/simtest.html#wave-renderer) 36 | - Many documentation and release process improvements. 37 | 38 | ### Fixed 39 | 40 | - Python 3.11 compatibility. 41 | 42 | ### Removed 43 | 44 | - Python 2.7 support. 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 `examples/` directory. You can also [try the examples 43 | on 44 | MyBinder](https://mybinder.org/v2/gh/UCSBarchlab/PyRTL/development?filepath=%2Fipynb-examples%2F). 45 | * Full reference documentation is available at https://pyrtl.readthedocs.io/ 46 | 47 | ### Package Contents 48 | 49 | If you are just getting started with PyRTL it is suggested that you start with 50 | the `examples/` first to get a sense of the "thinking with PyRTLs" required to 51 | design hardware in this way. If you are looking for a deeper understanding, 52 | dive into the code for the object `Block`. It is the core data structure at the 53 | heart of PyRTL and defines its semantics at a high level -- everything is 54 | converted to or from the small, simple set of primitives defined there. 55 | 56 | The package contains the following files and directories: 57 | * **`pyrtl/`** The src directory for the module 58 | * **`pyrtl/rtllib/`** Finished PyRTL libraries which are hopefully both useful 59 | and documented 60 | * **`examples/`** A set of hardware design examples that show the main idea 61 | behind pyrtl 62 | * **`tests/`** A set of unit tests for PyRTL which you can run with `pytest` 63 | * **`docs/`** Location of the sphinx documentation 64 | 65 | Testing requires the Python packages `tox` and `pytest`. Once installed a 66 | complete test of the system should be possible with the simple command `tox` 67 | and nothing more. 68 | 69 | ### Contributing to PyRTL 70 | 71 | *Picking a first project* 72 | 73 | * One of the earliest things you should submit is a unit test that hits some 74 | [uncovered lines of code in 75 | PyRTL](https://codecov.io/github/UCSBarchlab/PyRTL?branch=development). For 76 | example, pick a `PyrtlError` that is not covered and add a unit test in 77 | `tests/` that will hit it. 78 | * After you have that down check in the [PyRTL 79 | Issues](https://github.com/UCSBarchlab/PyRTL/issues) list for a feature that 80 | is marked as "beginner friendly". 81 | * Once you have that down, ask for access to the PyRTL-research repo where we 82 | keep a list of more advanced features and designs that could use more help! 83 | 84 | *Coding style* 85 | 86 | * All major functionality should have unit tests covering and documenting their 87 | use 88 | * All public functions and methods should have useful docstrings 89 | * All code needs to conform to 90 | [PEP8](https://www.python.org/dev/peps/pep-0008/) conventions 91 | * No new root-level dependencies on external libs, import locally if required 92 | for special functions 93 | 94 | *Workflow* 95 | 96 | * A useful reference for working with Git is this [Git 97 | tutorial](https://www.atlassian.com/git/tutorials/) 98 | * A useful Git Fork workflow for working on this repo is [found 99 | here](http://blog.scottlowe.org/2015/01/27/using-fork-branch-git-workflow/) 100 | * The `development` branch is the primary stable working branch (everyone is 101 | invited to submit pull requests) 102 | * Bugs and minor enhancements tracked directly through the [issue 103 | tracker](https://github.com/UCSBarchlab/PyRTL/issues) 104 | * When posting a bug please post a small chunk of code that captures the bug, 105 | e.g. [Issue #56](https://github.com/UCSBarchlab/PyRTL/issues/56) 106 | * When pushing a fix to a bug or enhancement please reference issue number in 107 | commit message, e.g. [Fix to Issue 108 | #56](https://github.com/UCSBarchlab/PyRTL/commit/1d5730db168a9e4490c580cb930075715468047a) 109 | 110 | *Documentation* 111 | 112 | * All important functionality should have an executable example in `examples/` 113 | * All classes should have a block comment with high level description of the 114 | class 115 | * All functions should follow the following (Sphinx parsable) docstring format: 116 | ```python 117 | """One Line Summary (< 80 chars) of the function, followed by period. 118 | 119 | :param param_name : Description of this parameter. 120 | :param param_name : Longer parameter descriptions take up a newline with four 121 | leading spaces like this. 122 | :return: Description of function's return value. 123 | 124 | A long description of what this function does. Talk about what the user 125 | should expect from this function and also what the users needs to do to use 126 | the function (this part is optional). 127 | 128 | """ 129 | 130 | # Developer Notes (Optional): 131 | # 132 | # These would be anything that the user does not need to know in order to use 133 | # the functions. 134 | # These notes can include internal workings of the function, the logic behind 135 | # it, or how to extend it. 136 | ``` 137 | * Sphinx parses [Python type 138 | annotations](https://docs.python.org/3/library/typing.html), so put type 139 | information into annotations instead of docstrings. 140 | * The Sphinx-generated documentation is published to 141 | https://pyrtl.readthedocs.io/ 142 | * PyRTL's Sphinx build process is documented in 143 | [`docs/README.md`](https://github.com/UCSBarchlab/PyRTL/blob/development/docs/README.md). 144 | * PyRTL's release process is documented in 145 | [`docs/release/README.md`](https://github.com/UCSBarchlab/PyRTL/blob/development/docs/release/README.md). 146 | 147 | ### Using PyRTL 148 | 149 | We love to hear from users about their projects, and if there are issues we 150 | will try our best to push fixes quickly. You can read more about how we have 151 | been using it in our research at UCSB both in simulation and on FPGAs in [our 152 | PyRTL paper at FPL](http://www.cs.ucsb.edu/~sherwood/pubs/FPL-17-pyrtl.pdf). 153 | 154 | ### Related Projects 155 | 156 | It is always important to point out that PyRTL builds on the ideas of several 157 | other related projects as we all share the common goal of trying to make 158 | hardware design a better experience! You can read more about those 159 | relationships on our [PyRTL project web 160 | page](http://ucsbarchlab.github.io/PyRTL/). 161 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation. 2 | 3 | # You can set these variables from the command line, and also 4 | # from the environment for the first two. 5 | SPHINXOPTS ?= 6 | SPHINXBUILD ?= sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Route the 'html' target to Sphinx using the "make mode" option. $(O) is 11 | # meant as a shortcut for $(SPHINXOPTS). 12 | html: Makefile 13 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | requirements.txt: requirements.in FORCE 16 | pip install --upgrade pip-tools 17 | pip-compile --upgrade requirements.in 18 | 19 | FORCE: 20 | -------------------------------------------------------------------------------- /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) 19 | for supported directives and fields. Sphinx parses [Python type 20 | annotations](https://docs.python.org/3/library/typing.html), so put type 21 | information into 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 28 | [`gh-pages` branch](https://github.com/UCSBarchlab/PyRTL/tree/gh-pages). 29 | This additional documentation is pushed to https://ucsbarchlab.github.io/PyRTL/ 30 | by the `pages-build-deployment` GitHub Action. The additional documentation is 31 | written in [GitHub MarkDown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax), 32 | and is not described further in this README. 33 | 34 | ## Installing Sphinx 35 | 36 | Sphinx and its dependencies are all pinned to specific versions for 37 | [reproducible documentation builds](https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html). 38 | This avoids problems where documentation builds randomly fail due to bugs or 39 | incompatibilities in the newest version of Sphinx or one of its 40 | dependencies. 41 | 42 | Use of an environment manager like [`conda`](https://docs.conda.io/en/latest/) 43 | or [`virtualenv`](https://virtualenv.pypa.io/en/latest/) is strongly 44 | recommended. To install Sphinx locally, run the following commands from the 45 | repository root: 46 | 47 | ```shell 48 | # Install Sphinx. 49 | $ pip install --upgrade -r docs/requirements.txt 50 | ``` 51 | 52 | ## Installing Graphviz 53 | 54 | [Install graphviz](https://www.graphviz.org/download/#executable-packages). Use 55 | of a package manager like 56 | [`apt`](https://ubuntu.com/server/docs/package-management) or 57 | [`brew`](https://brew.sh/) is strongly recommended. Instructions vary depending 58 | on your operating system, see the installation link for details. 59 | 60 | ## Running Sphinx 61 | 62 | Run Sphinx with the provided [`docs/Makefile`](https://github.com/UCSBarchlab/PyRTL/blob/development/docs/Makefile): 63 | 64 | ```shell 65 | # Run Sphinx to build PyRTL's documentation. 66 | $ make -C docs 67 | ``` 68 | 69 | A local copy of PyRTL's documentation should be available in 70 | `docs/_build/html`. `docs/_build/html/index.html` is the home page. 71 | 72 | ## Updating Sphinx 73 | 74 | To update the pinned version of Sphinx, run 75 | 76 | ```shell 77 | # Run pip-compile to generate docs/requirements.txt from docs/requirements.in. 78 | $ make -C docs requirements.txt 79 | ``` 80 | 81 | It's a good idea to update the pinned version of Sphinx whenever you update the 82 | documentation. 83 | -------------------------------------------------------------------------------- /docs/analysis.rst: -------------------------------------------------------------------------------- 1 | .. PyRTL analysis master file 2 | 3 | .. default-domain:: pyrtl.rtllib 4 | 5 | ========================= 6 | Analysis and Optimization 7 | ========================= 8 | 9 | Tools for analyzing and optimizing aspects of PyRTL designs. 10 | 11 | Estimation 12 | ---------- 13 | 14 | .. automodule:: pyrtl.analysis 15 | :members: 16 | :special-members: 17 | :undoc-members: 18 | :exclude-members: __dict__,__weakref__,__module__ 19 | 20 | Optimization 21 | ------------ 22 | 23 | .. autofunction:: pyrtl.passes.optimize 24 | 25 | Synthesis 26 | --------- 27 | 28 | .. autofunction:: pyrtl.passes.synthesize 29 | 30 | .. autoclass:: pyrtl.core.PostSynthBlock 31 | :show-inheritance: 32 | 33 | Individual Passes 34 | ----------------- 35 | 36 | .. autofunction:: pyrtl.passes.common_subexp_elimination 37 | .. autofunction:: pyrtl.passes.constant_propagation 38 | .. autofunction:: pyrtl.passes.nand_synth 39 | .. autofunction:: pyrtl.passes.and_inverter_synth 40 | .. autofunction:: pyrtl.passes.one_bit_selects 41 | .. autofunction:: pyrtl.passes.two_way_concat 42 | -------------------------------------------------------------------------------- /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 simulation. 16 | :class:`.Output` represents an output pin, which does not drive any wires in 17 | the design. :class:`.Const` is useful for specifying hard-wired values and 18 | :class:`.Register` is how sequential elements are created (they all have an 19 | implicit clock). 20 | 21 | .. inheritance-diagram:: pyrtl.wire.WireVector 22 | pyrtl.wire.Input 23 | pyrtl.wire.Output 24 | pyrtl.wire.Const 25 | pyrtl.wire.Register 26 | :parts: 1 27 | 28 | WireVector 29 | ---------- 30 | 31 | .. autoclass:: pyrtl.wire.WireVector 32 | :members: 33 | :special-members: __init__, __add__, __sub__, __mul__, __getitem___, 34 | __len__, __ilshift__, __and__, __or__, __xor__, __lt__, 35 | __le__, __eq__, __ne__, __gt__, __ge__, __len__ 36 | 37 | Input Pins 38 | ---------- 39 | 40 | .. autoclass:: pyrtl.wire.Input 41 | :members: 42 | :show-inheritance: 43 | :special-members: __init__ 44 | 45 | Output Pins 46 | ----------- 47 | 48 | .. autoclass:: pyrtl.wire.Output 49 | :members: 50 | :show-inheritance: 51 | :special-members: __init__ 52 | 53 | Constants 54 | --------- 55 | 56 | .. autoclass:: pyrtl.wire.Const 57 | :members: 58 | :show-inheritance: 59 | :special-members: __init__ 60 | 61 | Conditionals 62 | ------------ 63 | 64 | .. automodule:: pyrtl.conditional 65 | :members: 66 | :show-inheritance: 67 | :special-members: 68 | :undoc-members: 69 | :exclude-members: __dict__,__weakref__,__module__ 70 | -------------------------------------------------------------------------------- /docs/blocks.rst: -------------------------------------------------------------------------------- 1 | Logic Nets and Blocks 2 | ===================== 3 | 4 | LogicNets 5 | --------- 6 | 7 | .. autoclass:: pyrtl.core.LogicNet 8 | 9 | 10 | Blocks 11 | ------ 12 | 13 | .. autoclass:: pyrtl.core.Block 14 | 15 | .. autofunction:: pyrtl.core.working_block 16 | 17 | .. autofunction:: pyrtl.core.reset_working_block 18 | 19 | .. autofunction:: pyrtl.core.set_working_block 20 | 21 | .. autofunction:: pyrtl.core.temp_working_block 22 | 23 | .. autofunction:: pyrtl.core.Block.add_wirevector 24 | 25 | .. autofunction:: pyrtl.core.Block.remove_wirevector 26 | 27 | .. autofunction:: pyrtl.core.Block.add_net 28 | 29 | .. autofunction:: pyrtl.core.Block.get_memblock_by_name 30 | 31 | .. autofunction:: pyrtl.core.Block.wirevector_subset 32 | 33 | .. autofunction:: pyrtl.core.Block.logic_subset 34 | 35 | .. autofunction:: pyrtl.core.Block.get_wirevector_by_name 36 | 37 | .. autofunction:: pyrtl.core.Block.net_connections 38 | 39 | .. autofunction:: pyrtl.core.Block.sanity_check 40 | -------------------------------------------------------------------------------- /docs/brand/pyrtl_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/d5f8dbe53f54e61e1d54722449e4894b885243c7/docs/brand/pyrtl_logo.png -------------------------------------------------------------------------------- /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 | sys.path.insert(0, os.path.abspath('..')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'PyRTL' 21 | copyright = '2024, Timothy Sherwood' 22 | author = 'Timothy Sherwood' 23 | 24 | # -- General configuration --------------------------------------------------- 25 | 26 | master_doc = 'index' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.inheritance_diagram', 34 | 'sphinx.ext.viewcode', 35 | 'sphinx_autodoc_typehints', 36 | 'sphinx_copybutton', 37 | ] 38 | 39 | graphviz_output_format = 'svg' 40 | 41 | # Omit redundant method names in right sidebar (step() instead of Simulation.step()). 42 | toc_object_entries_show_parents = 'hide' 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # List of patterns, relative to source directory, that match files and 48 | # directories to ignore when looking for source files. 49 | # This pattern also affects html_static_path and html_extra_path. 50 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 51 | 52 | primary_domain = 'py' 53 | 54 | # -- Options for HTML output ------------------------------------------------- 55 | 56 | # The theme to use for HTML and HTML Help pages. See the documentation for 57 | # a list of builtin themes. 58 | # 59 | html_theme = "furo" 60 | html_theme_options = { 61 | 'sidebar_hide_name': True, 62 | } 63 | html_logo = 'brand/pyrtl_logo.png' 64 | 65 | # Force a light blue background color for inheritance-diagrams. The default is 66 | # transparent, which does not work well with Furo's dark mode. 67 | inheritance_graph_attrs = { 68 | 'bgcolor': 'aliceblue', 69 | } 70 | -------------------------------------------------------------------------------- /docs/export.rst: -------------------------------------------------------------------------------- 1 | Exporting and Importing Designs 2 | =============================== 3 | 4 | Exporting Hardware Designs 5 | -------------------------- 6 | 7 | .. autofunction:: pyrtl.importexport.output_to_verilog 8 | 9 | .. autofunction:: pyrtl.importexport.output_to_firrtl 10 | 11 | Exporting Testbenches 12 | --------------------- 13 | 14 | .. autofunction:: pyrtl.importexport.output_verilog_testbench 15 | 16 | Importing Verilog 17 | ----------------- 18 | 19 | .. autofunction:: pyrtl.importexport.input_from_blif 20 | .. autofunction:: pyrtl.importexport.input_from_verilog 21 | 22 | Outputting for Visualization 23 | ---------------------------- 24 | 25 | .. autofunction:: pyrtl.visualization.output_to_trivialgraph 26 | .. autofunction:: pyrtl.visualization.output_to_graphviz 27 | .. autofunction:: pyrtl.visualization.graphviz_detailed_namer 28 | .. autofunction:: pyrtl.visualization.output_to_svg 29 | .. autofunction:: pyrtl.visualization.block_to_graphviz_string 30 | .. autofunction:: pyrtl.visualization.block_to_svg 31 | .. autofunction:: pyrtl.visualization.trace_to_html 32 | .. autofunction:: pyrtl.visualization.net_graph 33 | -------------------------------------------------------------------------------- /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 ``len``, slicing e.g. ``wire[3:6]``, 12 | :meth:`~pyrtl.wire.WireVector.zero_extended`, 13 | :meth:`~pyrtl.wire.WireVector.sign_extended`, and many operators such as 14 | addition and multiplication). 15 | 16 | .. autofunction:: pyrtl.corecircuits.concat 17 | .. autofunction:: pyrtl.corecircuits.concat_list 18 | .. autofunction:: pyrtl.corecircuits.match_bitwidth 19 | .. autofunction:: pyrtl.helperfuncs.truncate 20 | .. autofunction:: pyrtl.helperfuncs.chop 21 | .. autofunction:: pyrtl.helperfuncs.wire_struct 22 | .. autofunction:: pyrtl.helperfuncs.wire_matrix 23 | 24 | Coercion to WireVector 25 | ---------------------- 26 | 27 | In PyRTL there is only one function in charge of coercing values into 28 | :class:`WireVectors<.WireVector>`, and that is :func:`.as_wires`. This 29 | function is called in almost all helper functions and classes to manage the 30 | mixture of constants and WireVectors that naturally occur in hardware 31 | development. 32 | 33 | .. autofunction:: pyrtl.corecircuits.as_wires 34 | 35 | Control Flow Hardware 36 | --------------------- 37 | 38 | .. autofunction:: pyrtl.corecircuits.mux 39 | .. autofunction:: pyrtl.corecircuits.select 40 | .. autofunction:: pyrtl.corecircuits.enum_mux 41 | .. autofunction:: pyrtl.corecircuits.bitfield_update 42 | .. autofunction:: pyrtl.corecircuits.bitfield_update_set 43 | .. autofunction:: pyrtl.helperfuncs.match_bitpattern 44 | 45 | Creating Lists of WireVectors 46 | ----------------------------- 47 | 48 | .. autofunction:: pyrtl.helperfuncs.input_list 49 | .. autofunction:: pyrtl.helperfuncs.output_list 50 | .. autofunction:: pyrtl.helperfuncs.register_list 51 | .. autofunction:: pyrtl.helperfuncs.wirevector_list 52 | 53 | Interpreting Vectors of Bits 54 | ---------------------------- 55 | 56 | Under the hood, every single `value` a PyRTL design operates on is a bit vector 57 | (which is, in turn, simply an integer of bounded power-of-two size. 58 | Interpreting these bit vectors as humans, and turning human understandable 59 | values into their corresponding bit vectors, can both be a bit of a pain. The 60 | functions below do not create any hardware but rather help in the process of 61 | reasoning about bit vector representations of human understandable values. 62 | 63 | .. autofunction:: pyrtl.helperfuncs.val_to_signed_integer 64 | .. autofunction:: pyrtl.helperfuncs.val_to_formatted_str 65 | .. autofunction:: pyrtl.helperfuncs.formatted_str_to_val 66 | .. autoclass:: pyrtl.helperfuncs.ValueBitwidthTuple 67 | :members: value, bitwidth 68 | .. autofunction:: pyrtl.helperfuncs.infer_val_and_bitwidth 69 | .. autofunction:: pyrtl.helperfuncs.log2 70 | 71 | Debugging 72 | --------- 73 | 74 | .. autofunction:: pyrtl.core.set_debug_mode 75 | .. autofunction:: pyrtl.helperfuncs.probe 76 | .. autofunction:: pyrtl.helperfuncs.rtl_assert 77 | .. autofunction:: pyrtl.helperfuncs.check_rtl_assertions 78 | 79 | Reductions 80 | ---------- 81 | 82 | .. autofunction:: pyrtl.corecircuits.and_all_bits 83 | .. autofunction:: pyrtl.corecircuits.or_all_bits 84 | .. autofunction:: pyrtl.corecircuits.xor_all_bits 85 | .. autofunction:: pyrtl.corecircuits.parity 86 | .. autofunction:: pyrtl.corecircuits.rtl_any 87 | .. autofunction:: pyrtl.corecircuits.rtl_all 88 | 89 | Extended Logic and Arithmetic 90 | ----------------------------- 91 | 92 | The functions below provide ways of comparing and arithmetically combining 93 | :class:`WireVectors<.WireVector>` in ways that are often useful in hardware 94 | design. The functions below extend those member functions of the 95 | :class:`.WireVector` class itself (which provides support for unsigned 96 | addition, subtraction, multiplication, comparison, and many others). 97 | 98 | .. autofunction:: pyrtl.corecircuits.signed_add 99 | .. autofunction:: pyrtl.corecircuits.signed_sub 100 | .. autofunction:: pyrtl.corecircuits.signed_mult 101 | .. autofunction:: pyrtl.corecircuits.signed_lt 102 | .. autofunction:: pyrtl.corecircuits.signed_le 103 | .. autofunction:: pyrtl.corecircuits.signed_gt 104 | .. autofunction:: pyrtl.corecircuits.signed_ge 105 | .. autofunction:: pyrtl.corecircuits.shift_left_arithmetic 106 | .. autofunction:: pyrtl.corecircuits.shift_right_arithmetic 107 | .. autofunction:: pyrtl.corecircuits.shift_left_logical 108 | .. autofunction:: pyrtl.corecircuits.shift_right_logical 109 | 110 | Encoders and Decoders 111 | --------------------- 112 | 113 | .. autofunction:: pyrtl.helperfuncs.one_hot_to_binary 114 | .. autofunction:: pyrtl.helperfuncs.binary_to_one_hot 115 | 116 | -------------------------------------------------------------------------------- /docs/images/gcd-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/d5f8dbe53f54e61e1d54722449e4894b885243c7/docs/images/gcd-graph.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | PYRTL 3 | ===== 4 | 5 | A collection of classes providing simple RTL specification, simulation, 6 | tracing, and testing suitable for teaching and research. Simplicity, usability, 7 | clarity, and extensibility rather than performance or optimization is the 8 | overarching goal. With PyRTL you can use the full power of Python to describe 9 | complex synthesizable digital designs, simulate and test them, and export them 10 | to Verilog. 11 | 12 | Quick links 13 | =========== 14 | * Get an overview from the `PyRTL Project Webpage `_ 15 | * Read through `Example PyRTL Code `_ 16 | * File a `Bug or Issue Report `_ 17 | * Contribute to project on `GitHub `_ 18 | 19 | Installation 20 | ============ 21 | 22 | **Automatic installation**:: 23 | 24 | pip install pyrtl 25 | 26 | PyRTL is listed in `PyPI `_ and can be 27 | installed with :program:`pip` or :program:`pip3`. If the above command fails 28 | due to insufficient permissions, you may need to do ``sudo pip install pyrtl`` 29 | (to install as superuser) or ``pip install --user pyrtl`` (to install as a 30 | normal user). 31 | 32 | PyRTL is tested to work with Python 3.8+. 33 | 34 | Design, Simulate, and Inspect in 15 lines 35 | ========================================= 36 | 37 | .. code-block:: 38 | :linenos: 39 | 40 | import pyrtl 41 | 42 | a = pyrtl.Input(8,'a') # input "pins" 43 | b = pyrtl.Input(8,'b') 44 | q = pyrtl.Output(8,'q') # output "pins" 45 | gt5 = pyrtl.Output(1,'gt5') 46 | 47 | result = a + b # makes an 8-bit adder 48 | q <<= result # assigns output of adder to out pin 49 | gt5 <<= result > 5 # does a comparison, assigns that to different pin 50 | 51 | # simulate and output the resulting waveform to the terminal 52 | sim = pyrtl.Simulation() 53 | sim.step_multiple({'a':[0,1,2,3,4], 'b':[2,2,3,3,4]}) 54 | sim.tracer.render_trace() 55 | 56 | After you have PyRTL installed, you should be able to cut and paste the above 57 | into a file and run it with Python. The result you should see, drawn right 58 | into the terminal, is the output of the simulation. While a great deal of work 59 | has gone into making hardware design in PyRTL as friendly as possible, please 60 | don't mistake that for a lack of depth. You can just as easily export to 61 | Verilog or other hardware formats, view results with your favorite waveform 62 | viewer, build hardware transformation passes, run JIT-accelerated simulations, 63 | design, test, and even verify hugely complex digital systems, and much more. 64 | Most critically of all it is easy to extend with your own approaches to digital 65 | hardware development as you find necessary. 66 | 67 | 68 | Overview of PyRTL 69 | ================= 70 | 71 | If you are brand new to PyRTL we recommend that you start with the `PyRTL Code 72 | Examples `_ 73 | which will show you most of the core functionality in the context of a complete 74 | design. 75 | 76 | PyRTL Classes: 77 | -------------- 78 | 79 | Perhaps the most important class to understand is :class:`.WireVector`, which 80 | is the basic type from which you build all hardware. If you are coming to 81 | PyRTL from Verilog, a :class:`.WireVector` is closest to a multi-bit `wire`. 82 | Every new :class:`.WireVector` builds a set of wires which you can then connect 83 | with other :class:`.WireVector` through overloaded operations such as 84 | `addition` or `bitwise or`. A bunch of other related classes, including 85 | :class:`.Input`, :class:`.Output`, :class:`.Const`, and :class:`.Register` are 86 | all derived from :class:`.WireVector`. Coupled with :class:`.MemBlock` (and 87 | :class:`.RomBlock`), this is all a user needs to create a functional hardware 88 | design. 89 | 90 | .. inheritance-diagram:: pyrtl.wire.WireVector 91 | pyrtl.wire.Input 92 | pyrtl.wire.Output 93 | pyrtl.wire.Const 94 | pyrtl.wire.Register 95 | :parts: 1 96 | 97 | After specifying a hardware design, there are then options to simulate your 98 | design right in PyRTL, synthesize it down to primitive 1-bit operations, 99 | optimize it, and export it to Verilog (along with a testbench). 100 | 101 | Simulation 102 | ^^^^^^^^^^ 103 | 104 | PyRTL provides tools for simulation and viewing simulation traces. Simulation 105 | is how your hardware is "executed" for the purposes of testing, and three 106 | different classes help you do that: :class:`.Simulation`, 107 | :class:`.FastSimulation` and :class:`.CompiledSimulation`. All three have 108 | `almost` the same interface and, except for a few debugging cases, can be used 109 | interchangeably. Typically one starts with :class:`.Simulation` and then moves 110 | up to :class:`.FastSimulation` when performance begins to matter. 111 | 112 | Both :class:`.Simulation` and :class:`.FastSimulation` take an instance of 113 | :class:`.SimulationTrace` as an argument (or makes an empty 114 | :class:`.SimulationTrace` by default), which stores a list of the signals as 115 | they are simulated. This trace can then be rendered to the terminal with 116 | :class:`.WaveRenderer`, although unless there are some problems with the 117 | default configurations, most end users should not need to even be aware of 118 | :class:`.WaveRenderer`. The examples describe other ways that the trace may be 119 | handled, including extraction as a test bench and export to a VCD file. 120 | 121 | Optimization 122 | ^^^^^^^^^^^^ 123 | 124 | :class:`.WireVector` and :class:`.MemBlock` are just "sugar" over a core set of 125 | primitives, and the final design is built up incrementally as a graph of these 126 | primitives. :class:`WireVectors<.WireVector>` connects these "primitives", 127 | which connect to other :class:`WireVectors<.WireVector>`. Each primitive is a 128 | :class:`.LogicNet`, and a :class:`.Block` is a graph of 129 | :class:`LogicNets<.LogicNet>`. Typically a full design is stored in a single 130 | :class:`.Block`. The function :func:`.working_block()` returns the block on 131 | which we are implicitly working. Hardware transforms may make a new 132 | :class:`.Block` from an old one. For example, see :class:`.PostSynthBlock`. 133 | 134 | Errors 135 | ^^^^^^ 136 | 137 | Finally, when things go wrong you may hit on one of two ``Exceptions``, neither 138 | of which is likely recoverable automatically (which is why we limited them to 139 | only two). The intention is that ``PyrtlError`` is intended to capture end 140 | user errors such as invalid constant strings and mis-matched bitwidths. In 141 | contrast, ``PyrtlInternalError`` captures internal invariants and assertions 142 | over the core logic graph which should never be hit when constructing designs 143 | in the normal ways. If you hit a confusing ``PyrtlError`` or any 144 | ``PyrtlInternalError`` feel free to file an issue. 145 | 146 | 147 | Reference Guide 148 | =============== 149 | .. toctree:: 150 | :maxdepth: 2 151 | 152 | basic 153 | regmem 154 | simtest 155 | blocks 156 | helpers 157 | analysis 158 | export 159 | rtllib 160 | 161 | Index 162 | ===== 163 | * :ref:`genindex` 164 | -------------------------------------------------------------------------------- /docs/regmem.rst: -------------------------------------------------------------------------------- 1 | Registers and Memories 2 | ====================== 3 | 4 | Registers 5 | --------- 6 | 7 | .. autoclass:: pyrtl.wire.Register 8 | :members: 9 | :show-inheritance: 10 | :special-members: __init__ 11 | 12 | Memories 13 | -------- 14 | 15 | .. autoclass:: pyrtl.memory.MemBlock 16 | :members: 17 | :special-members: __init__, __getitem__, __setitem__ 18 | 19 | ROMs 20 | ---- 21 | 22 | .. autoclass:: pyrtl.memory.RomBlock 23 | :members: 24 | :show-inheritance: 25 | :special-members: __init__, __getitem__ 26 | -------------------------------------------------------------------------------- /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 | 1. Update build tools: 128 | ```shell 129 | $ pip install --upgrade -r release/requirements.txt 130 | ``` 131 | 2. Build distribution archive: 132 | ```shell 133 | $ python3 -m build 134 | ``` 135 | This produces two files in `dist/`: a `.whl` file and a `.tar.gz` file. 136 | 137 | ### Manual publish on TestPyPI 138 | 139 | 1. If necessary, create a TestPyPI API token, by going to 140 | https://test.pypi.org/manage/account/ and clicking 'Add API token'. 141 | 2. Upload distribution archive to TestPyPI: 142 | ```shell 143 | $ twine upload --repository testpypi dist/* 144 | ``` 145 | 3. Enter your API token when prompted. 146 | 4. Check the new release's status at https://test.pypi.org/project/pyrtl/#history 147 | 5. Install and test the new release from TestPyPI by following the [Testing a 148 | new TestPyPI release](#testing-a-new-testpypi-release) instructions above. 149 | 150 | ### Manual publish on PyPI 151 | 152 | > :warning: The next steps update the official PyRTL release on PyPI, which 153 | > affects anyone running `pip install pyrtl`. Proceed with caution! 154 | 155 | 1. If necessary, create a PyPI API token, by going to 156 | https://pypi.org/manage/account/ and clicking 'Add API token'. 157 | 2. Upload distribution archive to PyPI: 158 | ```shell 159 | $ twine upload dist/* 160 | ``` 161 | 3. Enter your API token when prompted. 162 | 4. Check the new release's status at https://pypi.org/project/pyrtl/#history 163 | 5. Install and test the new release from PyPI by following the [Testing a new 164 | PyPI release](#testing-a-new-pypi-release) instructions above. 165 | 166 | ## Fixing Mistakes 167 | 168 | First assess the magnitude of the problem. If the new release is unusable or is 169 | unsafe to use, yank the bad release, then publish a fixed release. If the new 170 | release has a smaller problem, and is mostly usable, just publish a fixed 171 | release. 172 | 173 | ### Yanking A Bad Release 174 | 175 | If the new release is unusable, [yank the 176 | release](https://pypi.org/help/#yanked). This can be done by a project owner on 177 | PyPI, by clicking 'Manage project' then 'Options', then Yank'. When a release 178 | is yanked, it remains available to anyone requesting exactly the yanked 179 | version, so a project with a pinned requirement on PyRTL won't break. A yanked 180 | version will not be installed in any other case, so a user running `pip install 181 | pyrtl` will never receive a yanked version. 182 | 183 | ### Publishing A Fixed Release 184 | 185 | Fix the problem in the code, then publish a new release with a new version 186 | number by following the instructions above. Do not attempt to reuse an existing 187 | version number. 188 | -------------------------------------------------------------------------------- /docs/release/requirements.txt: -------------------------------------------------------------------------------- 1 | build 2 | twine 3 | -------------------------------------------------------------------------------- /docs/requirements.in: -------------------------------------------------------------------------------- 1 | furo 2 | sphinx 3 | sphinx-autodoc-typehints 4 | sphinx-copybutton 5 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.13 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | alabaster==1.0.0 8 | # via sphinx 9 | babel==2.16.0 10 | # via sphinx 11 | beautifulsoup4==4.12.3 12 | # via furo 13 | certifi==2024.12.14 14 | # via requests 15 | charset-normalizer==3.4.1 16 | # via requests 17 | docutils==0.21.2 18 | # via sphinx 19 | furo==2024.8.6 20 | # via -r requirements.in 21 | idna==3.10 22 | # via requests 23 | imagesize==1.4.1 24 | # via sphinx 25 | jinja2==3.1.5 26 | # via sphinx 27 | markupsafe==3.0.2 28 | # via jinja2 29 | packaging==24.2 30 | # via sphinx 31 | pygments==2.19.1 32 | # via 33 | # furo 34 | # sphinx 35 | requests==2.32.3 36 | # via sphinx 37 | snowballstemmer==2.2.0 38 | # via sphinx 39 | soupsieve==2.6 40 | # via beautifulsoup4 41 | sphinx==8.1.3 42 | # via 43 | # -r requirements.in 44 | # furo 45 | # sphinx-autodoc-typehints 46 | # sphinx-basic-ng 47 | # sphinx-copybutton 48 | sphinx-autodoc-typehints==3.0.0 49 | # via -r requirements.in 50 | sphinx-basic-ng==1.0.0b2 51 | # via furo 52 | sphinx-copybutton==0.5.2 53 | # via -r requirements.in 54 | sphinxcontrib-applehelp==2.0.0 55 | # via sphinx 56 | sphinxcontrib-devhelp==2.0.0 57 | # via sphinx 58 | sphinxcontrib-htmlhelp==2.1.0 59 | # via sphinx 60 | sphinxcontrib-jsmath==1.0.1 61 | # via sphinx 62 | sphinxcontrib-qthelp==2.0.0 63 | # via sphinx 64 | sphinxcontrib-serializinghtml==2.0.0 65 | # via sphinx 66 | urllib3==2.3.0 67 | # via requests 68 | -------------------------------------------------------------------------------- /docs/rtllib.rst: -------------------------------------------------------------------------------- 1 | .. PyRTL rtllib master file 2 | 3 | .. default-domain:: pyrtl.rtllib 4 | 5 | =========== 6 | RTL Library 7 | =========== 8 | 9 | Useful circuits, functions, and testing utilities. 10 | 11 | Adders 12 | ------ 13 | 14 | .. automodule:: pyrtl.rtllib.adders 15 | :members: 16 | :special-members: 17 | :undoc-members: 18 | :exclude-members: __dict__,__weakref__,__module__ 19 | 20 | AES-128 21 | ------- 22 | 23 | .. automodule:: pyrtl.rtllib.aes 24 | :members: 25 | :undoc-members: 26 | :exclude-members: __dict__,__weakref__,__module__ 27 | 28 | Barrel 29 | ------ 30 | 31 | .. automodule:: pyrtl.rtllib.barrel 32 | :members: 33 | :special-members: 34 | :undoc-members: 35 | :exclude-members: __dict__,__weakref__,__module__ 36 | 37 | Library Utilities 38 | ----------------- 39 | 40 | .. automodule:: pyrtl.rtllib.libutils 41 | :members: 42 | :special-members: 43 | :undoc-members: 44 | :exclude-members: __dict__,__weakref__,__module__ 45 | 46 | Multipliers 47 | ----------- 48 | 49 | .. automodule:: pyrtl.rtllib.multipliers 50 | :members: 51 | :special-members: 52 | :undoc-members: 53 | :exclude-members: __dict__,__weakref__,__module__ 54 | 55 | Muxes 56 | ----- 57 | 58 | .. automodule:: pyrtl.rtllib.muxes 59 | :members: 60 | :special-members: 61 | :undoc-members: 62 | :exclude-members: __dict__,__weakref__,__module__,__exit__ 63 | 64 | Matrix 65 | ------ 66 | 67 | .. automodule:: pyrtl.rtllib.matrix 68 | :members: 69 | :special-members: __init__ 70 | :undoc-members: 71 | :exclude-members: __dict__,__weakref__,__module__ 72 | 73 | Testing Utilities 74 | ----------------- 75 | 76 | .. automodule:: pyrtl.rtllib.testingutils 77 | :members: 78 | :special-members: 79 | :exclude-members: __dict__,__weakref__,__module__ 80 | -------------------------------------------------------------------------------- /docs/screenshots/pyrtl-counter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/d5f8dbe53f54e61e1d54722449e4894b885243c7/docs/screenshots/pyrtl-counter.png -------------------------------------------------------------------------------- /docs/screenshots/pyrtl-renderer-demo-ascii.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/d5f8dbe53f54e61e1d54722449e4894b885243c7/docs/screenshots/pyrtl-renderer-demo-ascii.png -------------------------------------------------------------------------------- /docs/screenshots/pyrtl-renderer-demo-cp437.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/d5f8dbe53f54e61e1d54722449e4894b885243c7/docs/screenshots/pyrtl-renderer-demo-cp437.png -------------------------------------------------------------------------------- /docs/screenshots/pyrtl-renderer-demo-powerline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/d5f8dbe53f54e61e1d54722449e4894b885243c7/docs/screenshots/pyrtl-renderer-demo-powerline.png -------------------------------------------------------------------------------- /docs/screenshots/pyrtl-renderer-demo-utf-8-alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/d5f8dbe53f54e61e1d54722449e4894b885243c7/docs/screenshots/pyrtl-renderer-demo-utf-8-alt.png -------------------------------------------------------------------------------- /docs/screenshots/pyrtl-renderer-demo-utf-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/d5f8dbe53f54e61e1d54722449e4894b885243c7/docs/screenshots/pyrtl-renderer-demo-utf-8.png -------------------------------------------------------------------------------- /docs/screenshots/pyrtl-statemachine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/d5f8dbe53f54e61e1d54722449e4894b885243c7/docs/screenshots/pyrtl-statemachine.png -------------------------------------------------------------------------------- /docs/simtest.rst: -------------------------------------------------------------------------------- 1 | Simulation and Testing 2 | ====================== 3 | 4 | Simulation 5 | ---------- 6 | 7 | .. autoclass:: pyrtl.simulation.Simulation 8 | :members: 9 | :special-members: __init__ 10 | 11 | Fast (JIT to Python) Simulation 12 | ------------------------------- 13 | 14 | .. autoclass:: pyrtl.simulation.FastSimulation 15 | :members: 16 | :special-members: __init__ 17 | 18 | Compiled (JIT to C) Simulation 19 | ------------------------------ 20 | 21 | .. autoclass:: pyrtl.compilesim.CompiledSimulation 22 | :members: 23 | :special-members: __init__ 24 | 25 | Simulation Trace 26 | ---------------- 27 | 28 | .. autoclass:: pyrtl.simulation.SimulationTrace 29 | :members: 30 | :special-members: __init__ 31 | 32 | Wave Renderer 33 | ------------- 34 | 35 | .. autoclass:: pyrtl.simulation.WaveRenderer 36 | :members: 37 | :special-members: __init__ 38 | :exclude-members: render_ruler_segment, render_val, val_to_str 39 | .. autofunction:: pyrtl.simulation.enum_name 40 | .. autoclass:: pyrtl.simulation.PowerlineRendererConstants 41 | :show-inheritance: 42 | .. autoclass:: pyrtl.simulation.Utf8RendererConstants 43 | :show-inheritance: 44 | .. autoclass:: pyrtl.simulation.Utf8AltRendererConstants 45 | :show-inheritance: 46 | .. autoclass:: pyrtl.simulation.Cp437RendererConstants 47 | :show-inheritance: 48 | .. autoclass:: pyrtl.simulation.AsciiRendererConstants 49 | :show-inheritance: 50 | -------------------------------------------------------------------------------- /examples/example0-minimum-viable-hardware.py: -------------------------------------------------------------------------------- 1 | import pyrtl 2 | 3 | # "pin" input/outputs 4 | a = pyrtl.Input(8, 'a') 5 | b = pyrtl.Input(8, 'b') 6 | q = pyrtl.Output(8, 'q') 7 | gt5 = pyrtl.Output(1, 'gt5') 8 | 9 | sum = a + b # makes an 8-bit adder 10 | q <<= sum # assigns output of adder to out pin 11 | gt5 <<= sum > 5 # does a comparison, assigns that to different pin 12 | 13 | # the simulation and waveform output 14 | sim = pyrtl.Simulation() 15 | sim.step_multiple({'a': [0, 1, 2, 3, 4], 'b': [2, 2, 3, 3, 4]}) 16 | sim.tracer.render_trace() 17 | -------------------------------------------------------------------------------- /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, 4 | (a,b,c) and two one-bit outputs (sum, cout). The logic declared is a 5 | simple one-bit adder and the definition uses some of the most common 6 | parts of PyRTL. The adder is then simulated on random data, the 7 | wave form is printed to the screen, and the resulting trace is 8 | compared to a "correct" addition. If the result is correct then a 0 9 | is returned, else 1. 10 | """ 11 | 12 | import random 13 | import pyrtl 14 | 15 | # The basic idea of PyRTL is to specify the component of a some hardware block 16 | # through the declaration of wires and operations on those wires. The current 17 | # working block, an instance of a class devilishly named "Block", is implicit 18 | # in all of the below code -- it is easiest to start with the way wires work. 19 | 20 | # --- Step 1: Define Logic ------------------------------------------------- 21 | 22 | # One of the most fundamental types in PyRTL is the "WireVector" which acts 23 | # very much like a Python list of 1-bit wires. Unlike a normal list, though, the 24 | # number of bits is explicitly declared. 25 | temp1 = pyrtl.WireVector(bitwidth=1, name='temp1') 26 | 27 | # Both arguments are in fact optional and default to a bitwidth of 1 and a unique 28 | # name generated by PyRTL starting with 'tmp' 29 | temp2 = pyrtl.WireVector() 30 | 31 | # Two special types of WireVectors are Input and Output, which are used to specify 32 | # an interface to the hardware block. 33 | a, b, c = pyrtl.Input(1, 'a'), pyrtl.Input(1, 'b'), pyrtl.Input(1, 'c') 34 | sum, carry_out = pyrtl.Output(1, 'sum'), pyrtl.Output(1, 'carry_out') 35 | 36 | # Okay, let's build a one-bit adder. To do this we need to use the assignment 37 | # operator, which is '<<='. This takes an already declared wire and "connects" 38 | # it to some other already declared wire. Let's start with the sum bit, which is 39 | # of course just the xor of the three inputs 40 | sum <<= a ^ b ^ c 41 | 42 | # The carry_out bit would just be "carry_out <<= a & b | a & c | b & c" but let's break 43 | # than down a bit to see what is really happening. What if we want to give names 44 | # to the partial signals in the middle of that computation? When you take 45 | # "a & b" in PyRTL, what that really means is "make an AND gate, connect one input 46 | # to 'a' and the other to 'b' and return the result of the gate". The result of 47 | # that AND gate can then be assigned to temp1 or it can be used like any other 48 | # Python variable. 49 | 50 | temp1 <<= a & b # connect the result of a & b to the pre-allocated wirevector 51 | temp2 <<= a & c 52 | temp3 = b & c # temp3 IS the result of b & c (this is the first mention of temp3) 53 | carry_out <<= temp1 | temp2 | temp3 54 | 55 | # You can access the working block through pyrtl.working_block(), and for most 56 | # things one block is all you will need. Example 2 discusses this in more detail, 57 | # but for now we can just print the block to see that in fact it looks like the 58 | # hardware we described. The format is a bit weird, but roughly translates to 59 | # a list of gates (the 'w' gates are just wires). The ins and outs of the gates 60 | # are printed 'name'/'bitwidth''WireVectorType' 61 | 62 | print('--- One Bit Adder Implementation ---') 63 | print(pyrtl.working_block()) 64 | print() 65 | 66 | # --- Step 2: Simulate Design ----------------------------------------------- 67 | 68 | # Okay, let's simulate our one-bit adder. To keep track of the output of 69 | # the simulation we need to make a new "SimulationTrace" and a "Simulation" 70 | # that then uses that trace. 71 | 72 | sim_trace = pyrtl.SimulationTrace() 73 | sim = pyrtl.Simulation(tracer=sim_trace) 74 | 75 | # Now all we need to do is call "sim.step" to simulate each clock cycle of our 76 | # design. We just need to pass in some input each cycle, which is a dictionary 77 | # mapping inputs (the *names* of the inputs, not the actual Input instances) 78 | # to their value for that signal each cycle. In this simple example, we 79 | # can just specify a random value of 0 or 1 with Python's random module. We 80 | # call step 15 times to simulate 15 cycles. 81 | 82 | for cycle in range(15): 83 | sim.step({ 84 | 'a': random.choice([0, 1]), 85 | 'b': random.choice([0, 1]), 86 | 'c': random.choice([0, 1]) 87 | }) 88 | 89 | # Now all we need to do is print the trace results to the screen. Here we use 90 | # "render_trace" with some size information. 91 | print('--- One Bit Adder Simulation ---') 92 | sim_trace.render_trace(symbol_len=2) 93 | 94 | a_value = sim.inspect(a) 95 | print("The latest value of 'a' was: " + str(a_value)) 96 | 97 | # --- Step 3: Verification of Simulated Design --------------------------------------- 98 | 99 | # Now finally, let's check the trace to make sure that sum and carry_out are actually 100 | # the right values when compared to Python's addition operation. Note that 101 | # all the simulation is done at this point and we are just checking the waveform, 102 | # but there is no reason you could not do this at simulation time if you had a 103 | # really long-running design. 104 | 105 | for cycle in range(15): 106 | # Note that we are doing all arithmetic on values, NOT wirevectors, here. 107 | # We can add the inputs together to get a value for the result 108 | add_result = (sim_trace.trace['a'][cycle] 109 | + sim_trace.trace['b'][cycle] 110 | + sim_trace.trace['c'][cycle]) 111 | # We can select off the bits and compare 112 | python_sum = add_result & 0x1 113 | python_cout = (add_result >> 1) & 0x1 114 | if (python_sum != sim_trace.trace['sum'][cycle] 115 | or python_cout != sim_trace.trace['carry_out'][cycle]): 116 | print('This Example is Broken!!!') 117 | exit(1) 118 | -------------------------------------------------------------------------------- /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 | 10 | """ 11 | 12 | import pyrtl 13 | 14 | # Let's start with unsigned addition. 15 | # ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 16 | # Add all combinations of two unsigned 2-bit inputs. 17 | a = pyrtl.Input(bitwidth=2, name='a') 18 | b = pyrtl.Input(bitwidth=2, name='b') 19 | 20 | unsigned_sum = pyrtl.Output(bitwidth=3, name='unsigned_sum') 21 | unsigned_sum <<= a + b 22 | 23 | # Try all combinations of {0, 1, 2, 3} + {0, 1, 2, 3}. 24 | unsigned_inputs = { 25 | 'a': [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3], 26 | 'b': [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] 27 | } 28 | 29 | sim = pyrtl.Simulation() 30 | sim.step_multiple(provided_inputs=unsigned_inputs) 31 | # In this trace, `unsigned_sum` is the sum of all combinations of 32 | # {0, 1, 2, 3} + {0, 1, 2, 3}. For example: 33 | # • cycle 0 shows 0 + 0 = 0 34 | # • cycle 1 shows 0 + 1 = 1 35 | # • cycle 15 shows 3 + 3 = 6 36 | print('Unsigned addition. Each cycle adds a different combination of ' 37 | 'numbers.\nunsigned_sum == a + b') 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('\nUse `val_to_signed_integer` to re-interpret the previous simulation ' 55 | 'results as\nsigned integers. But re-interpreting the previous trace as ' 56 | 'signed integers \nproduces INCORRECT RESULTS!') 57 | sim.tracer.render_trace(repr_func=pyrtl.val_to_signed_integer) 58 | 59 | # Use `signed_add` to correctly add signed integers. 60 | # ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 61 | # `signed_add` works by sign-extending its inputs to the sum's bitwidth before 62 | # adding. There are many `signed_*` functions for signed operations, like 63 | # `signed_sub`, `signed_mul`, `signed_lt`, and so on. 64 | pyrtl.reset_working_block() 65 | 66 | a = pyrtl.Input(bitwidth=2, name='a') 67 | b = pyrtl.Input(bitwidth=2, name='b') 68 | 69 | signed_sum = pyrtl.Output(bitwidth=3, name='signed_sum') 70 | signed_sum <<= pyrtl.signed_add(a, b) 71 | 72 | # Try all combinations of {-2, -1, 0, 1} + {-2, -1, 0, 1}. 73 | signed_inputs = { 74 | 'a': [-2, -2, -2, -2, -1, -1, -1, -1, 0, 0, 0, 0, 1, 1, 1, 1], 75 | 'b': [-2, -1, 0, 1, -2, -1, 0, 1, -2, -1, 0, 1, -2, -1, 0, 1] 76 | } 77 | sim = pyrtl.Simulation() 78 | sim.step_multiple(provided_inputs=signed_inputs) 79 | 80 | # In this trace, `signed_sum` is the sum of all combinations of 81 | # {-2, -1, 0, 1} + {-2, -1, 0, 1}. For example: 82 | # • cycle 0 shows -2 + -2 = -4 83 | # • cycle 1 shows -2 + -1 = -3 84 | # • cycle 15 shows 1 + 1 = 2 85 | print('\nReset the simulation and use `signed_add` to correctly add signed ' 86 | 'integers.\nsigned_sum == signed_add(a, b)') 87 | sim.tracer.render_trace(repr_func=pyrtl.val_to_signed_integer) 88 | 89 | # Manually sign-extend inputs to correctly add signed integers with `+`. 90 | # ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 91 | # Instead of using `signed_add`, we can manually sign-extend the inputs and 92 | # truncate the output to correctly add signed integers with `+`. Use this trick 93 | # to implement other signed arithmetic operations. 94 | pyrtl.reset_working_block() 95 | 96 | a = pyrtl.Input(bitwidth=2, name='a') 97 | b = pyrtl.Input(bitwidth=2, name='b') 98 | 99 | # Using `+` produces the correct result for signed addition here because we 100 | # manually extend `a` and `b` to 3 bits. The result of `a.sign_extended(3) + 101 | # b.sign_extended(3)` is now 4 bits, but we truncate it to 3 bits. This 102 | # truncation is subtle, but important! The addition's full 4 bit result would 103 | # not be correct. 104 | sign_extended_sum = pyrtl.Output(bitwidth=3, name='sign_extended_sum') 105 | extended_a = a.sign_extended(bitwidth=3) 106 | extended_b = b.sign_extended(bitwidth=3) 107 | sign_extended_sum <<= (extended_a + extended_b).truncate(bitwidth=3) 108 | 109 | # Try all combinations of {-2, -1, 0, 1} + {-2, -1, 0, 1}. 110 | signed_inputs = { 111 | 'a': [-2, -2, -2, -2, -1, -1, -1, -1, 0, 0, 0, 0, 1, 1, 1, 1], 112 | 'b': [-2, -1, 0, 1, -2, -1, 0, 1, -2, -1, 0, 1, -2, -1, 0, 1] 113 | } 114 | sim = pyrtl.Simulation() 115 | sim.step_multiple(provided_inputs=signed_inputs) 116 | 117 | print('\nInstead of using `signed_add`, we can also manually sign extend the ' 118 | 'inputs to\ncorrectly add signed integers with `+`.\n' 119 | 'sign_extended_sum == (a.sign_extended(3) + b.sign_extended(3))' 120 | '.truncate(3)') 121 | sim.tracer.render_trace(repr_func=pyrtl.val_to_signed_integer) 122 | -------------------------------------------------------------------------------- /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 list. 7 | 8 | """ 9 | 10 | import inspect 11 | import pyrtl 12 | 13 | # Motivation 14 | # ▔▔▔▔▔▔▔▔▔▔ 15 | # wire_struct and wire_matrix are syntactic sugar. They make PyRTL code easier to read 16 | # and write, but they do not introduce any new functionality. We start with a motivating 17 | # example that does not use wire_struct or wire_matrix, point out some common problems, 18 | # then rewrite the example with wire_struct to address those 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 down 54 | # 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 concatenate 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 bitwidth. 61 | pixel_out = pyrtl.concat( 62 | red_out.truncate(8), green_out.truncate(8), blue_out.truncate(8) 63 | ) 64 | assert pixel_out.bitwidth == 24 65 | pixel_out.name = "pixel_out" 66 | 67 | # Test this slicing and concatenation logic. 68 | print("Pixel slicing and concatenation without wire_struct:") 69 | sim = pyrtl.Simulation() 70 | sim.step_multiple(provided_inputs={"pixel_in": [0x112233, 0xAABBCC]}) 71 | sim.tracer.render_trace( 72 | trace_list=["pixel_in", "red_in", "green_in", "blue_in", "pixel_out"] 73 | ) 74 | 75 | # Introducing wire_struct 76 | # ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 77 | # Start over, and rebuild the example with wire_struct. 78 | pyrtl.reset_working_block() 79 | 80 | 81 | # This Pixel class is the single source of truth that specifies how the `red`, `green`, 82 | # and `blue` components are packed into 24 bits. A Pixel instance can do one of two 83 | # things: 84 | # (1). Slice a 24-bit value into `red`, `green`, and `blue` components, OR 85 | # (2). Pack `red`, `green`, and `blue` components into a 24-bit value. 86 | # 87 | # Because this definition is the single source of truth, we can change the pixel data 88 | # format just by changing this class definition. For example, try making the `red` 89 | # component the last component, instead of the first component. 90 | @pyrtl.wire_struct 91 | class Pixel: 92 | # The most significant bits are specified first, so the "red" component is the 8 93 | # most significant bits, indices 16..23 94 | red: 8 95 | # The "green" component is the 8 middle bits, indices 8..15 96 | green: 8 97 | # The "blue" component is the 8 least significant bits, indices 0..7 98 | blue: 8 99 | 100 | 101 | # pixel_in is a Pixel constructed from a 24-bit Input, option (1) above. The 24-bit 102 | # input will be sliced into 8-bit components, and those components can be manipulated as 103 | # pixel_in.red, pixel_in.green, and pixel_in.blue in the Python code below. 104 | # 105 | # Because this Pixel has a name, the 8-bit components will be assigned the names 106 | # "pixel_in.red", "pixel_in.green", and "pixel_in.blue". These component names are 107 | # included in the `trace_list` below. 108 | pixel_in = Pixel(name="pixel_in", concatenated_type=pyrtl.Input) 109 | assert pixel_in.bitwidth == 24 110 | assert pixel_in.red.bitwidth == 8 111 | assert pixel_in.green.bitwidth == 8 112 | assert pixel_in.blue.bitwidth == 8 113 | 114 | # Repeat our calculations with the named components. Components are accessed with "dot" 115 | # notation (.green), like a Python namedtuple. There are no more hardcoded indices like 116 | # [8:16] in this example. 117 | red_out = pixel_in.red + 0x11 118 | green_out = pixel_in.green + 0x22 119 | blue_out = pixel_in.blue + 0x33 120 | 121 | # pixel_out is a Pixel constructed from components, option (2) above. Components with 122 | # more than 8 bits are implicitly truncated to 8 bits. 123 | # 124 | # Python keyword arguments are used for `red`, `green`, and `blue` here, so the order 125 | # does not matter. We arbitrarily provide the value of the `green` component first. 126 | pixel_out = Pixel(name="pixel_out", green=green_out, red=red_out, blue=blue_out) 127 | assert pixel_out.bitwidth == 24 128 | 129 | # Test this new version of our slicing and concatenation logic. 130 | print("\nPixel slicing and concatenation with wire_struct:") 131 | sim = pyrtl.Simulation() 132 | sim.step_multiple(provided_inputs={"pixel_in": [0x112233, 0xAABBCC]}) 133 | sim.tracer.render_trace( 134 | trace_list=[ 135 | "pixel_in", 136 | "pixel_in.red", 137 | "pixel_in.green", 138 | "pixel_in.blue", 139 | "pixel_out", 140 | ] 141 | ) 142 | 143 | # Introducing wire_matrix 144 | # ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ 145 | # wire_matrix is like wire_struct, except the components are indexed by integers, like 146 | # an array. 147 | 148 | # Start over. 149 | pyrtl.reset_working_block() 150 | 151 | # PixelPair is a pair of two Pixels. This also shows how wire_matrix and wire_struct 152 | # work together - PixelPair is a wire_struct nested in a wire_matrix. 153 | PixelPair = pyrtl.wire_matrix(component_schema=Pixel, size=2) 154 | 155 | # wire_matrix() returns a class! 156 | assert inspect.isclass(PixelPair) 157 | 158 | # Create a PixelPair that decomposes a 48-bit input into two 24-bit Pixels. Each 24-bit 159 | # Pixel can be further decomposed into 8-bit components. This PixelPair has been 160 | # assigned a name, so all its components are named, as seen in the simulation trace 161 | # below. 162 | pixel_pair_in = PixelPair(name="pixel_pair_in", concatenated_type=pyrtl.Input) 163 | assert pixel_pair_in.bitwidth == 48 164 | assert pixel_pair_in[0].bitwidth == 24 165 | assert pixel_pair_in[0].red.bitwidth == 8 166 | 167 | # Do some calculations on the Pixel components. Components are accessed with 168 | # square-bracket notation ([1]), like a Python list. 169 | pixel_pair_out_values = [ 170 | Pixel( 171 | red=pixel_pair_in[0].red + 0x10, 172 | green=pixel_pair_in[0].green, 173 | blue=pixel_pair_in[0].blue, 174 | ), 175 | pixel_pair_in[1] + 1, 176 | ] 177 | 178 | # Pack the new component values into a new PixelPair. 179 | pixel_pair_out = PixelPair(name="pixel_pair_out", values=pixel_pair_out_values) 180 | 181 | # Test out PixelPair. 182 | print("\nPixelPair slicing and concatenating with wire_matrix:") 183 | sim = pyrtl.Simulation() 184 | sim.step_multiple(provided_inputs={"pixel_pair_in": [0x112233AABBCC, 0x445566778899]}) 185 | sim.tracer.render_trace( 186 | trace_list=[ 187 | "pixel_pair_in", 188 | "pixel_pair_in[0]", 189 | "pixel_pair_in[0].red", 190 | "pixel_pair_in[0].green", 191 | "pixel_pair_in[0].blue", 192 | "pixel_pair_in[1]", 193 | "pixel_pair_in[1].red", 194 | "pixel_pair_in[1].green", 195 | "pixel_pair_in[1].blue", 196 | "pixel_pair_out", 197 | ] 198 | ) 199 | 200 | # These examples hopefully show how wire_matrix and wire_struct result in code that's 201 | # easier to write correctly and easier to read. 202 | # 203 | # wire_struct and wire_matrix have many more features, but this example should 204 | # demonstrate the main ideas. See the documentation for more details: 205 | # 206 | # https://pyrtl.readthedocs.io/en/latest/helpers.html#pyrtl.helperfuncs.wire_struct 207 | # https://pyrtl.readthedocs.io/en/latest/helpers.html#pyrtl.helperfuncs.wire_matrix 208 | -------------------------------------------------------------------------------- /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 4 | and more complex hardware structures with functions. We generate 5 | a 3-bit ripple carry adder building off of the 1-bit adder from 6 | the prior example, and then hook it to a register to count up modulo 8. 7 | """ 8 | 9 | import pyrtl 10 | 11 | 12 | # Let's just dive right in. 13 | 14 | def one_bit_add(a, b, carry_in): 15 | assert len(a) == len(b) == 1 # len returns the bitwidth 16 | sum = a ^ b ^ carry_in 17 | carry_out = a & b | a & carry_in | b & carry_in 18 | return sum, carry_out 19 | 20 | # A function in PyRTL is nothing special -- it just so happens that the statements 21 | # it encapsulate tell PyRTL to build some hardware. If we call "one_bit_add" 22 | # above with the arguments "x", "y", and "z", it will make a one-bit adder to add 23 | # those values together and return the wires for sum and carry_out as applied to "x", 24 | # "y", and "z". If I call it again on "i", "j", and "k" it will build a new one-bit 25 | # adder for those inputs and return the resulting sum and carry_out for that adder. 26 | 27 | 28 | # While PyRTL actually provides an "+" operator for WireVectors which generates 29 | # adders, a ripple carry adder is something people can understand easily but has 30 | # enough structure to be mildly interesting. Let's define an adder of arbitrary 31 | # length recursively and (hopefully) Pythonically. More comments after the code. 32 | 33 | def ripple_add(a, b, carry_in=0): 34 | a, b = pyrtl.match_bitwidth(a, b) 35 | # This function is a function that allows us to match the bitwidth of multiple 36 | # different wires. By default, it zero extends the shorter bits 37 | if len(a) == 1: 38 | sumbits, carry_out = one_bit_add(a, b, carry_in) 39 | else: 40 | lsbit, ripplecarry = one_bit_add(a[0], b[0], carry_in) 41 | msbits, carry_out = ripple_add(a[1:], b[1:], ripplecarry) 42 | sumbits = pyrtl.concat(msbits, lsbit) 43 | return sumbits, carry_out 44 | 45 | # The above code breaks down into two cases. 1) If the size of the inputs 46 | # is one-bit just do one_bit_add. 2) If they are more than one bit, do 47 | # a one-bit add on the least significant bits, a ripple carry on the rest, 48 | # and then stick the results back together into one WireVector. A couple 49 | # interesting features of PyRTL can be seen here: WireVectors can be indexed 50 | # like lists, with [0] accessing the least significant bit and [1:] being an 51 | # example of the use of Python slicing syntax. While you can add two lists 52 | # together in Python, a WireVector + Wirevector means "make an adder", so to 53 | # concatenate the bits of two vectors one needs to use "concat". Finally, 54 | # if we look at "carry_in" it seems to have a default value of the integer "0" but 55 | # is a WireVector at other times. Python supports polymorphism throughout 56 | # and PyRTL will cast integers and some other types to WireVectors when it can. 57 | 58 | 59 | # Now let's build a 3-bit counter from our N-bit ripple carry adder. 60 | 61 | counter = pyrtl.Register(bitwidth=3, name='counter') 62 | sum, carry_out = ripple_add(counter, pyrtl.Const("1'b1")) 63 | counter.next <<= sum 64 | 65 | # A couple new things in the above code. The two remaining types of basic 66 | # WireVectors, Const and Register, both appear. Const, unsurprisingly, is just for 67 | # holding constants (such as the 0 in ripple_add), but here we create one directly 68 | # from a Verilog-like string which includes both the value and the bitwidth. 69 | # Registers are just like wires, except their updates are delayed to the next 70 | # clock cycle. This is made explicit in the syntax through the property '.next' 71 | # which should always be set for registers. In this simple example, we make the 72 | # counter's value on the next cycle equal to the counter's value this cycle plus one. 73 | 74 | # Now let's run the bugger. No need for inputs, as it doesn't have any. 75 | # Finally we'll print the trace to the screen and check that it counts up correctly. 76 | 77 | sim_trace = pyrtl.SimulationTrace() 78 | sim = pyrtl.Simulation(tracer=sim_trace) 79 | for cycle in range(15): 80 | sim.step() 81 | assert sim.value[counter] == cycle % 8 82 | sim_trace.render_trace() 83 | -------------------------------------------------------------------------------- /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 4 | of a vending machine that will dispense an item when it has received 4 5 | tokens. If a refund is requested, it returns the tokens. 6 | 7 | """ 8 | 9 | import enum 10 | import pyrtl 11 | 12 | token_in = pyrtl.Input(1, 'token_in') 13 | req_refund = pyrtl.Input(1, 'req_refund') 14 | dispense = pyrtl.Output(1, 'dispense') 15 | refund = pyrtl.Output(1, 'refund') 16 | state = pyrtl.Register(3, 'state') 17 | 18 | 19 | # First new step, let's enumerate a set of constants to serve as our states 20 | class State(enum.IntEnum): 21 | WAIT = 0 # Waiting for first token. 22 | TOK1 = 1 # Received first token, waiting for second token. 23 | TOK2 = 2 # Received second token, waiting for third token. 24 | TOK3 = 3 # Received third token, waiting for fourth token. 25 | DISP = 4 # Received fourth token, dispense item. 26 | RFND = 5 # Issue refund. 27 | 28 | 29 | # Now we could build a state machine using just the registers and logic 30 | # discussed in the earlier examples, but doing operations *conditionally* on 31 | # some input is a pretty fundamental operation in hardware design. PyRTL 32 | # provides an instance called "conditional_assignment" to provide a predicated 33 | # update to a registers, wires, and memories. 34 | # 35 | # Conditional assignments are specified with a "|=" instead of a "<<=" 36 | # operator. The conditional assignment is only valid in the context of a 37 | # condition, and updates to those values only happens when that condition is 38 | # true. In hardware this is implemented with a simple mux -- for people coming 39 | # from software it is important to remember that this is describing a big logic 40 | # function, **NOT** an "if-then-else" clause. All of these things will execute 41 | # straight through when "build_everything" is called. More comments after the 42 | # code. 43 | # 44 | # One more thing: conditional_assignment might not always be the best solution. 45 | # If the update is simple, a regular 'mux(sel_wire, falsecase=f_wire, 46 | # truecase=t_wire)' can be sufficient. 47 | with pyrtl.conditional_assignment: 48 | with req_refund: # signal of highest precedence 49 | state.next |= State.RFND 50 | with token_in: # if token received, advance state in counter sequence 51 | with state == State.WAIT: 52 | state.next |= State.TOK1 53 | with state == State.TOK1: 54 | state.next |= State.TOK2 55 | with state == State.TOK2: 56 | state.next |= State.TOK3 57 | with state == State.TOK3: 58 | state.next |= State.DISP # 4th token received, go to dispense 59 | with pyrtl.otherwise: # token received in unsupported state 60 | state.next |= State.RFND 61 | # unconditional transition from these two states back to wait state 62 | 63 | # NOTE: the parens are needed because in Python the "|" operator is lower 64 | # precedence than the "==" operator! 65 | with (state == State.DISP) | (state == State.RFND): 66 | state.next |= State.WAIT 67 | 68 | dispense <<= state == State.DISP 69 | refund <<= state == State.RFND 70 | 71 | # A few more notes: 72 | # 73 | # 1) A condition can be nested within another condition and the implied 74 | # hardware is that the left-hand-side should only get that value if ALL of 75 | # the encompassing conditions are satisfied. 76 | # 2) Only one conditional at each level can be true meaning that all conditions 77 | # are implicitly also saying that none of the prior conditions at the same 78 | # level also have been true. The highest priority condition is listed first, 79 | # and in a sense you can think about each other condition as an "elif". 80 | # 3) If not every condition is enumerated, the default value for the register 81 | # under those cases will be the same as it was the prior cycle ("state.next 82 | # |= state" in this example). The default for a wirevector is 0. 83 | # 4) There is a way to specify something like an "else" instead of "elif" and 84 | # that is with an "otherwise" (as seen on the line above "state.next <<= 85 | # State.RFND"). This condition will be true if none of the other conditions 86 | # at the same level were also true (for this example specifically, 87 | # state.next will get RFND when req_refund==0, token_in==1, and state is not 88 | # in TOK1, TOK2, TOK3, or DISP. 89 | # 5) Not shown here, but you can update multiple different registers, wires, 90 | # and memories all under the same set of conditionals. 91 | 92 | # A more artificial example might make it even more clear how these rules interact: 93 | # with a: 94 | # r.next |= 1 <-- when a is true 95 | # with d: 96 | # r2.next |= 2 <-- when a and d are true 97 | # with otherwise: 98 | # r2.next |= 3 <-- when a is true and d is false 99 | # with b == c: 100 | # r.next |= 0 <-- when a is not true and b==c is true 101 | 102 | # Now let's build and test our state machine. 103 | 104 | sim_trace = pyrtl.SimulationTrace() 105 | sim = pyrtl.Simulation(tracer=sim_trace) 106 | 107 | # Rather than just give some random inputs, let's specify some specific 1-bit 108 | # values. To make it easier to simulate it over several steps, we'll use 109 | # sim.step_multiple, which takes in a dictionary mapping each input to its 110 | # value on each step. 111 | 112 | sim_inputs = { 113 | 'token_in': '0010100111010000', 114 | 'req_refund': '1100010000000000' 115 | } 116 | 117 | sim.step_multiple(sim_inputs) 118 | 119 | # Also, to make our input/output easy to reason about let's specify an order to 120 | # the traces. We also use `enum_name` to display the state names (WAIT, TOK1, 121 | # ...) rather than their numbers (0, 1, ...). 122 | sim_trace.render_trace( 123 | trace_list=['token_in', 'req_refund', 'state', 'dispense', 'refund'], 124 | repr_per_name={'state': pyrtl.enum_name(State)}) 125 | 126 | # Finally, suppose you want to simulate your design and verify its output 127 | # matches your expectations. sim.step_multiple also accepts as a second 128 | # argument a dictionary mapping output wires to their expected value on each 129 | # step. If during the simulation the actual and expected values differ, it will 130 | # be reported to you! This might be useful if you have a working design which, 131 | # after some tweaks, you'd like to test for functional equivalence, or as a 132 | # basic sanity check. 133 | 134 | sim_outputs = { 135 | 'dispense': '0000000000001000', 136 | 'refund': '0111001000000000' 137 | } 138 | 139 | # Note that you don't need to explicitly supply a tracer to Simulation(); it 140 | # will create one internally for you if needed. 141 | sim = pyrtl.Simulation() 142 | 143 | sim.step_multiple(sim_inputs, sim_outputs) 144 | -------------------------------------------------------------------------------- /examples/example4-debuggingtools.py: -------------------------------------------------------------------------------- 1 | """ Example 4: Debugging 2 | 3 | Debugging is half the coding process in software, and in PyRTL, it's no 4 | different. PyRTL provides some additional challenges when it comes to 5 | debugging, as a problem may surface long after the error was made. Fortunately, 6 | PyRTL comes with various features to help you find mistakes. 7 | """ 8 | 9 | import random 10 | import io 11 | from pyrtl.rtllib import adders, multipliers 12 | import pyrtl 13 | 14 | random.seed(93729473) # used to make random calls deterministic for this example 15 | 16 | 17 | # This example covers debugging strategies for PyRTL. For general Python debugging, 18 | # we recommend healthy use of the "assert" statement, and use of "pdb" for 19 | # tracking down bugs. However, PyRTL introduces some new complexities because 20 | # the place where functionality is defined (when you construct and operate 21 | # on PyRTL classes) is separate in time from where that functionality is executed 22 | # (i.e. during simulation). Thus, sometimes it hard to track down where a wire 23 | # might have come from, or what exactly it is doing. 24 | 25 | # In this example specifically, we will be building a circuit that adds up three values. 26 | # However, instead of building an add function ourselves or using the 27 | # built-in "+" function in PyRTL, we will instead use the Kogge-Stone adders 28 | # in RtlLib, the standard library for PyRTL. 29 | 30 | # Building three inputs 31 | in1, in2, in3 = (pyrtl.Input(8, "in" + str(x)) for x in range(1, 4)) 32 | out = pyrtl.Output(10, "out") 33 | 34 | add1_out = adders.kogge_stone(in1, in2) 35 | add2_out = adders.kogge_stone(add1_out, in2) 36 | out <<= add2_out 37 | 38 | # The most basic way of debugging PyRTL is to connect a value to an output wire 39 | # and use the simulation to trace the output. A simple "print" statement doesn't work 40 | # because the values in the wires are not populated during *creation* time 41 | 42 | # If we want to check the result of the first addition, we can connect an output wire 43 | # to the result wire of the first adder 44 | 45 | debug_out = pyrtl.Output(9, "debug_out") 46 | debug_out <<= add1_out 47 | 48 | # Now simulate the circuit. Let's create some random inputs to feed our adder. 49 | 50 | vals1 = [int(2**random.uniform(1, 8) - 2) for _ in range(20)] 51 | vals2 = [int(2**random.uniform(1, 8) - 2) for _ in range(20)] 52 | vals3 = [int(2**random.uniform(1, 8) - 2) for _ in range(20)] 53 | 54 | sim_trace = pyrtl.SimulationTrace() 55 | sim = pyrtl.Simulation(tracer=sim_trace) 56 | sim.step_multiple({ 57 | 'in1': vals1, 58 | 'in2': vals2, 59 | 'in3': vals3 60 | }) 61 | 62 | # In order to get the result data, you do not need to print a waveform of the trace. 63 | # You always have the option to just pull the data out of the tracer directly 64 | print("---- Inputs and debug_out ----") 65 | print("in1: ", str(sim_trace.trace['in1'])) 66 | print("in2: ", str(sim_trace.trace['in2'])) 67 | print("debug_out: ", str(sim_trace.trace['debug_out'])) 68 | print('\n') 69 | 70 | # Below, I am using the ability to directly retrieve the trace data to 71 | # verify the correctness of the first adder 72 | 73 | for i in range(len(vals1)): 74 | assert sim_trace.trace['debug_out'][i] == sim_trace.trace['in1'][i] + sim_trace.trace['in2'][i] 75 | 76 | 77 | # --- Probe ---- 78 | 79 | # Now that we have built some stuff, let's clear it so we can try again in a 80 | # different way. We can start by clearing all of the hardware from the current working 81 | # block. The working block is a global structure that keeps track of all the 82 | # hardware you have built thus far. A "reset" will clear it so we can start fresh. 83 | pyrtl.reset_working_block() 84 | print("---- Using Probes ----") 85 | 86 | # In this example, we will be multiplying two numbers using tree_multiplier() 87 | # Again, create the two inputs and an output 88 | in1, in2 = (pyrtl.Input(8, "in" + str(x)) for x in range(1, 3)) 89 | out1, out2 = (pyrtl.Output(8, "out" + str(x)) for x in range(1, 3)) 90 | 91 | multout = multipliers.tree_multiplier(in1, in2) 92 | 93 | # The following line will create a probe named 'std_probe" for later use, like an output. 94 | pyrtl.probe(multout, 'std_probe') 95 | 96 | # We could also do the same thing during assignment. The next command will 97 | # create a probe (named 'stdout_probe') that refers to multout (returns the wire multout). 98 | # This achieves virtually the same thing as 4 lines above, but it is done during assignment, 99 | # so we skip a step by probing the wire before the multiplication. 100 | # The probe returns multout, the original wire, and out will be assigned multout * 2 101 | out1 <<= pyrtl.probe(multout, 'stdout_probe') * 2 102 | 103 | # probe can also be used with other operations like this: 104 | pyrtl.probe(multout + 32, 'adder_probe') 105 | 106 | # or this: 107 | pyrtl.probe(multout[2:7], 'select_probe') 108 | 109 | # or, similarly: 110 | # (this will create a probe of multout while passing multout[2:16] to out2) 111 | out2 <<= pyrtl.probe(multout)[2:16] # notice probe names are not absolutely necessary 112 | 113 | # As one can see, probe can be used on any wire any time, 114 | # such as before or during its operation, assignment, etc. 115 | 116 | # Now on to the simulation... 117 | # For variation, we'll recreate the random inputs: 118 | vals1 = [int(2**random.uniform(1, 8) - 2) for _ in range(10)] 119 | vals2 = [int(2**random.uniform(1, 8) - 2) for _ in range(10)] 120 | 121 | sim_trace = pyrtl.SimulationTrace() 122 | sim = pyrtl.Simulation(tracer=sim_trace) 123 | sim.step_multiple({ 124 | 'in1': vals1, 125 | 'in2': vals2, 126 | }) 127 | 128 | # Now we will show the values of the inputs and probes 129 | # and look at that, we didn't need to make any outputs! 130 | # (although we did, to demonstrate the power and convenience of probes) 131 | sim_trace.render_trace() 132 | sim_trace.print_trace() 133 | 134 | print("--- Probe w/ debugging: ---") 135 | # Say we wanted to have gotten more information about 136 | # one of those probes above at declaration. 137 | # We could have used pyrtl.set_debug_mode() before their creation, like so: 138 | pyrtl.set_debug_mode() 139 | pyrtl.probe(multout - 16, 'debugsubtr_probe') 140 | pyrtl.set_debug_mode(debug=False) 141 | 142 | 143 | # ---- WireVector Stack Trace ---- 144 | 145 | # Another case that might arise is that a certain wire is causing an error to occur 146 | # in your program. WireVector Stack Traces allow you to find out more about where a particular 147 | # WireVector was made in your code. With this enabled the WireVector will 148 | # store exactly were it was created, which should help with issues where 149 | # there is a problem with an identified wire. 150 | 151 | # Like above, just add the following line before the relevant WireVector 152 | # might be made or at the beginning of the program. 153 | 154 | pyrtl.set_debug_mode() 155 | 156 | print("---- Stack Trace ----") 157 | test_out = pyrtl.Output(9, "test_out") 158 | test_out <<= adders.kogge_stone(in1, in2) 159 | 160 | # Now to retrieve information 161 | wire_trace = test_out.init_call_stack 162 | 163 | # This data is generated using the traceback.format_stack() call from the Python 164 | # standard library's Traceback module (look at the Python standard library docs for 165 | # details on the function). Therefore, the stack traces are stored as a list with the 166 | # outermost call first. 167 | 168 | for frame in wire_trace: 169 | print(frame) 170 | 171 | # Storage of Additional Debug Data 172 | 173 | # ------------------------------------ 174 | # WARNING: the debug information generated by the following two processes are 175 | # not guaranteed to be preserved when functions (eg. pyrtl.synthesize() ) are 176 | # done over the block. 177 | # ------------------------------------ 178 | 179 | # However, if the stack trace does not give you enough information about the 180 | # WireVector, you can also embed additional information into the wire itself 181 | # Two ways of doing so is either through manipulating the name of the 182 | # WireVector, or by adding your own custom metadata to the WireVector. 183 | 184 | 185 | # So far, each input and output WireVector have been given their own names, but 186 | # normal WireVectors can also be given names by supplying the name argument to 187 | # the constructor 188 | 189 | dummy_wv = pyrtl.WireVector(1, name="blah") 190 | 191 | # Also, because of the flexible nature of Python, you can also add custom 192 | # properties to the WireVector. 193 | 194 | dummy_wv.my_custom_property_name = "John Clow is great" 195 | dummy_wv.custom_value_028493 = 13 196 | 197 | # removing the WireVector from the block to prevent problems with the rest of 198 | # this example 199 | pyrtl.working_block().remove_wirevector(dummy_wv) 200 | 201 | # ---- Trivial Graph Format ---- 202 | 203 | # Finally, there is a handy way to view your hardware creations as a graph. 204 | # The function output_to_trivialgraph will render your hardware in a format that 205 | # you can then open with the free software "yEd" 206 | # (http://en.wikipedia.org/wiki/YEd). There are options under the 207 | # "hierarchical" rendering to draw something that looks quite like a circuit. 208 | 209 | pyrtl.working_block().sanity_check() 210 | pyrtl.passes._remove_unused_wires(pyrtl.working_block()) # so that trivial_graph() will work 211 | 212 | print("--- Trivial Graph Format ---") 213 | with io.StringIO() as tgf: 214 | pyrtl.output_to_trivialgraph(tgf) 215 | print(tgf.getvalue()) 216 | -------------------------------------------------------------------------------- /examples/example5-introspection.py: -------------------------------------------------------------------------------- 1 | """ Example 5: Making use of PyRTL and Introspection. """ 2 | 3 | import pyrtl 4 | 5 | 6 | # The following example shows how PyRTL can be used to make some interesting 7 | # hardware structures using Python introspection. In particular, this example 8 | # makes a N-stage pipeline structure. Any specific pipeline is then a derived 9 | # class of SimplePipeline where methods with names starting with "stage" are 10 | # stages, and new members with names not starting with "_" are to be registered 11 | # for the next stage. 12 | 13 | class SimplePipeline(object): 14 | """ Pipeline builder with auto generation of pipeline registers. """ 15 | 16 | def __init__(self): 17 | self._pipeline_register_map = {} 18 | self._current_stage_num = 0 19 | stage_list = [method for method in dir(self) if method.startswith('stage')] 20 | for stage in sorted(stage_list): 21 | stage_method = getattr(self, stage) 22 | stage_method() 23 | self._current_stage_num += 1 24 | 25 | def __getattr__(self, name): 26 | try: 27 | return self._pipeline_register_map[self._current_stage_num][name] 28 | except KeyError: 29 | raise pyrtl.PyrtlError( 30 | 'error, no pipeline register "%s" defined for stage %d' 31 | % (name, self._current_stage_num)) 32 | 33 | def __setattr__(self, name, value): 34 | if name.startswith('_'): 35 | # do not do anything tricky with variables starting with '_' 36 | object.__setattr__(self, name, value) 37 | else: 38 | next_stage = self._current_stage_num + 1 39 | pipereg_id = str(self._current_stage_num) + 'to' + str(next_stage) 40 | rname = 'pipereg_' + pipereg_id + '_' + name 41 | new_pipereg = pyrtl.Register(bitwidth=len(value), name=rname) 42 | if next_stage not in self._pipeline_register_map: 43 | self._pipeline_register_map[next_stage] = {} 44 | self._pipeline_register_map[next_stage][name] = new_pipereg 45 | new_pipereg.next <<= value 46 | 47 | 48 | class SimplePipelineExample(SimplePipeline): 49 | """ A very simple pipeline to show how registers are inferred. """ 50 | 51 | def __init__(self): 52 | self._loopback = pyrtl.WireVector(1, 'loopback') 53 | super(SimplePipelineExample, self).__init__() 54 | 55 | def stage0(self): 56 | self.n = ~ self._loopback 57 | 58 | def stage1(self): 59 | self.n = self.n 60 | 61 | def stage2(self): 62 | self.n = self.n 63 | 64 | def stage3(self): 65 | self.n = self.n 66 | 67 | def stage4(self): 68 | self._loopback <<= self.n 69 | 70 | 71 | simplepipeline = SimplePipelineExample() 72 | print(pyrtl.working_block()) 73 | # Simulation of the core 74 | sim_trace = pyrtl.SimulationTrace() 75 | sim = pyrtl.Simulation(tracer=sim_trace) 76 | sim.step_multiple({}, nsteps=15) 77 | sim_trace.render_trace() 78 | -------------------------------------------------------------------------------- /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 4 | locations that are persistent over clock cycles. In previous examples, 5 | we have shown the Register WireVector, which is great for storing 6 | a small amount of data for a single clock cycle. However, PyRTL also 7 | has other ways to store data, namely memories and ROMs. 8 | """ 9 | 10 | import random 11 | 12 | import pyrtl 13 | from pyrtl import * 14 | 15 | # --- Part 1: Memories ------------------------------------------------------- 16 | 17 | # Memories are a way to store multiple sets of data for extended periods of 18 | # time. Below we will make two instances of the same memory to test 19 | # that the same thing happens to two different memories using the same 20 | # inputs 21 | 22 | mem1 = MemBlock(bitwidth=32, addrwidth=3, name='mem') 23 | mem2 = MemBlock(32, 3, 'mem') 24 | 25 | # One memory will receive the write address from an input, the other, a register 26 | waddr = Input(3, 'waddr') 27 | count = Register(3, 'count') 28 | 29 | # In order to make sure that the two memories take the same inputs, 30 | # we will use same write data, write enable, and read addr values 31 | wdata = Input(32, 'wdata') 32 | we = Input(1, 'we') 33 | raddr = Input(3, 'raddr') 34 | 35 | # We will be grabbing data from each of the two memory blocks so we need 36 | # two different output wires to see the results 37 | 38 | rdata1 = Output(32, 'rdata1') 39 | rdata2 = Output(32, 'rdata2') 40 | 41 | # Ports 42 | # The way of sending data to and from a memory block is through the 43 | # use of a port. There are two types of ports: read ports and write ports. 44 | # Each memory can have multiple read and write ports, but it doesn't make 45 | # sense for one to have either 0 read ports or 0 write ports. Below, we 46 | # will make one read port for each of the two memories 47 | 48 | rdata1 <<= mem1[raddr] 49 | rdata2 <<= mem2[raddr] 50 | 51 | # Write Enable Bit 52 | # For the write ports, we will do something different. Sometimes you don't 53 | # want the memories to always accept the data and address on the write port. 54 | # The write enable bit allows us to disable the write port as long as the 55 | # value is zero, giving us complete control over whether to accept the data. 56 | 57 | WE = MemBlock.EnabledWrite 58 | mem1[waddr] <<= WE(wdata, we) # Uses input wire 59 | mem2[count] <<= WE(wdata, we) # Uses count register 60 | 61 | # Now we will finish up the circuit 62 | # We will increment count register on each write 63 | 64 | count.next <<= select(we, truecase=count + 1, falsecase=count) 65 | 66 | # We will also verify that the two write addresses are always the same 67 | 68 | validate = Output(1, 'validate') 69 | validate <<= waddr == count 70 | 71 | # Now it is time to simulate the circuit. First we will set up the values 72 | # for all of the inputs. 73 | # Write 1 through 8 into the eight registers, then read back out 74 | simvals = { 75 | 'we': "00111111110000000000000000", 76 | 'waddr': "00012345670000000000000000", 77 | 'wdata': "00123456789990000000000000", 78 | 'raddr': "00000000000000000123456777" 79 | } 80 | 81 | # For simulation purposes, we can give the spots in memory an initial value. 82 | # Note that in the actual circuit, the values are initially undefined. 83 | # Below, we are building the data with which to initialize memory. 84 | mem1_init = {addr: 9 for addr in range(8)} 85 | mem2_init = {addr: 9 for addr in range(8)} 86 | 87 | # The simulation only recognizes initial values of memories when they are in a 88 | # dictionary composed of memory : mem_values pairs. 89 | memvals = {mem1: mem1_init, mem2: mem2_init} 90 | 91 | # Now run the simulation like before. Note the adding of the memory 92 | # value map. 93 | print("---------memories----------") 94 | print(pyrtl.working_block()) 95 | sim_trace = pyrtl.SimulationTrace() 96 | sim = pyrtl.Simulation(tracer=sim_trace, memory_value_map=memvals) 97 | sim.step_multiple(simvals) 98 | sim_trace.render_trace() 99 | 100 | # Cleanup in preparation for the ROM example 101 | pyrtl.reset_working_block() 102 | 103 | # --- Part 2: ROMs ----------------------------------------------------------- 104 | 105 | # ROMs are another type of memory. Unlike normal memories, ROMs are read-only 106 | # and therefore only have read ports. They are used to store predefined data. 107 | 108 | # There are two different ways to define the data stored in the ROMs, 109 | # either through passing a function or though a list or tuple. 110 | 111 | 112 | def rom_data_func(address): 113 | return 31 - 2 * address 114 | 115 | 116 | rom_data_array = [rom_data_func(a) for a in range(16)] 117 | 118 | # Now we will make the ROM blocks. ROM blocks are similar to memory blocks, 119 | # but because they are read-only, they also need to be passed a set of 120 | # data to be initialized as. 121 | 122 | # FIXME: rework how memassigns work to account for more read ports 123 | rom1 = RomBlock(bitwidth=5, addrwidth=4, romdata=rom_data_func, max_read_ports=10) 124 | rom2 = RomBlock(5, 4, rom_data_array, max_read_ports=10) 125 | 126 | rom_add_1, rom_add_2 = Input(4, "rom_in"), Input(4, "rom_in_2") 127 | 128 | rom_out_1, rom_out_2 = Output(5, "rom_out_1"), Output(5, "rom_out_2") 129 | rom_out_3, cmp_out = Output(5, "rom_out_3"), Output(1, "cmp_out") 130 | 131 | # Because Output WireVectors cannot be used as the source for other nets, 132 | # in order to use the ROM outputs in two different places, we must instead 133 | # assign them to a temporary variable. 134 | 135 | temp1 = rom1[rom_add_1] 136 | temp2 = rom2[rom_add_1] 137 | 138 | rom_out_3 <<= rom2[rom_add_2] 139 | 140 | # now we will connect the rest of the outputs together 141 | 142 | rom_out_1 <<= temp1 143 | rom_out_2 <<= temp2 144 | 145 | cmp_out <<= temp1 == temp2 146 | 147 | # One of the things that is useful to have is repeatability, However, we 148 | # also don't want the hassle of typing out a set of values to test. One 149 | # solution in this case is to seed random and then pulling out 'random' 150 | # numbers from it. 151 | print("---------roms----------") 152 | print(pyrtl.working_block()) 153 | random.seed(4839483) 154 | 155 | # Now we will create a new set of simulation values. In this case, since we 156 | # want to use simulation values that are larger than 9 we cannot use the 157 | # trick used in previous examples to parse values. The two ways we are doing 158 | # it below are both valid ways of making larger values 159 | 160 | simvals = { 161 | 'rom_in': [1, 11, 4, 2, 7, 8, 2, 4, 5, 13, 15, 3, 4, 4, 4, 8, 12, 13, 2, 1], 162 | 'rom_in_2': [random.randrange(0, 16) for i in range(20)] 163 | } 164 | 165 | # Now run the simulation like before. Note that for ROMs, we do not 166 | # supply a memory value map because ROMs are defined with the values 167 | # predefined. 168 | 169 | sim_trace = pyrtl.SimulationTrace() 170 | sim = pyrtl.Simulation(tracer=sim_trace) 171 | sim.step_multiple(simvals) 172 | sim_trace.render_trace() 173 | -------------------------------------------------------------------------------- /examples/example7-synth-timing.py: -------------------------------------------------------------------------------- 1 | """ Example 7: Reduction and Speed Analysis 2 | 3 | After building a circuit, one might want to do some stuff to reduce the 4 | hardware into simpler nets as well as analyze various metrics of the 5 | hardware. This functionality is provided in the Passes part of PyRTL 6 | and will be demonstrated here. 7 | """ 8 | 9 | 10 | import pyrtl 11 | 12 | # --- Part 1: Timing Analysis ------------------------------------------------ 13 | 14 | # Timing and area usage are key considerations of any hardware block that one 15 | # makes. PyRTL provides functions to do these operations. 16 | 17 | # Creating a sample hardware block 18 | pyrtl.reset_working_block() 19 | const_wire = pyrtl.Const(6, bitwidth=4) 20 | in_wire2 = pyrtl.Input(bitwidth=4, name="input2") 21 | out_wire = pyrtl.Output(bitwidth=5, name="output") 22 | out_wire <<= const_wire + in_wire2 23 | 24 | 25 | # Now we will do the timing analysis as well as print out the critical path 26 | 27 | # Generating timing analysis information 28 | print("Pre Synthesis:") 29 | timing = pyrtl.TimingAnalysis() 30 | timing.print_max_length() 31 | 32 | # We are also able to print out the critical paths as well as get them 33 | # back as an array. 34 | critical_path_info = timing.critical_path() 35 | 36 | # --- Part 2: Area Analysis -------------------------------------------------- 37 | 38 | # PyRTL also provides estimates for the area that would be used up if the 39 | # circuit was printed as an ASIC. 40 | 41 | logic_area, mem_area = pyrtl.area_estimation(tech_in_nm=65) 42 | est_area = logic_area + mem_area 43 | print("Estimated Area of block", est_area, "sq mm") 44 | print() 45 | 46 | 47 | # --- Part 3: Synthesis ------------------------------------------------------ 48 | 49 | # Synthesis is the operation of reducing the circuit into simpler components. 50 | # The base synthesis function breaks down the more complex logic operations 51 | # into logic gates (keeping registers and memories intact) as well as reduces 52 | # all combinatorial logic into operations that only use 1-bitwidth wires. 53 | # 54 | # This synthesis allows for PyRTL to make optimizations to the net structure 55 | # as well as prepares it for further transformations on the PyRTL toolchain. 56 | 57 | pyrtl.synthesize() 58 | 59 | print("Pre Optimization:") 60 | timing = pyrtl.TimingAnalysis() 61 | timing.print_max_length() 62 | for net in pyrtl.working_block().logic: 63 | print(str(net)) 64 | print() 65 | 66 | 67 | # --- Part 4: Optimization ---------------------------------------------------- 68 | 69 | # PyRTL has functions built-in to eliminate unnecessary logic from the 70 | # circuit. These functions are all done with a simple call: 71 | pyrtl.optimize() 72 | 73 | # Now to see the difference 74 | print("Post Optimization:") 75 | timing = pyrtl.TimingAnalysis() 76 | timing.print_max_length() 77 | 78 | for net in pyrtl.working_block().logic: 79 | print(str(net)) 80 | 81 | # As we can see, the number of nets in the circuit was drastically reduced by 82 | # the optimization algorithm. 83 | -------------------------------------------------------------------------------- /examples/example8-verilog.py: -------------------------------------------------------------------------------- 1 | """ Example 8: Interfacing with Verilog. 2 | 3 | While there is much more about PyRTL design to discuss, at some point somebody 4 | might ask you to do something with your code other than have it print 5 | pretty things out to the terminal. We provide import from and export to 6 | Verilog of designs, export of waveforms to VCD, and a set of transforms 7 | that make doing netlist-level transforms and analysis directly in PyRTL easy. 8 | """ 9 | 10 | import random 11 | import io 12 | import pyrtl 13 | 14 | # ---- Importing From Verilog ---- 15 | 16 | # Sometimes it is useful to pull in components written in Verilog to be used 17 | # as subcomponents of PyRTL designs or to be subject to analysis written over 18 | # the PyRTL core. One standard format supported by PyRTL is "blif" format: 19 | # https://www.ece.cmu.edu/~ee760/760docs/blif.pdf 20 | 21 | # Many tools supoprt outputting hardware designs to this format, including the 22 | # free open source project "Yosys". Blif files can then be imported either 23 | # as a string or directly from a file name by the function input_from_blif. 24 | # Here is a simple example of a 1-bit full adder imported and then simulated 25 | # from this blif format. 26 | 27 | full_adder_blif = """ 28 | # Generated by Yosys 0.3.0+ (git sha1 7e758d5, clang 3.4-1ubuntu3 -fPIC -Os) 29 | .model full_adder 30 | .inputs x y cin 31 | .outputs sum cout 32 | .names $false 33 | .names $true 34 | 1 35 | .names y $not$FA.v:12$3_Y 36 | 0 1 37 | .names x $not$FA.v:11$1_Y 38 | 0 1 39 | .names cin $not$FA.v:15$6_Y 40 | 0 1 41 | .names ind3 ind4 sum 42 | 1- 1 43 | -1 1 44 | .names $not$FA.v:15$6_Y ind2 ind3 45 | 11 1 46 | .names x $not$FA.v:12$3_Y ind1 47 | 11 1 48 | .names ind2 $not$FA.v:16$8_Y 49 | 0 1 50 | .names cin $not$FA.v:16$8_Y ind4 51 | 11 1 52 | .names x y $and$FA.v:19$11_Y 53 | 11 1 54 | .names ind0 ind1 ind2 55 | 1- 1 56 | -1 1 57 | .names cin ind2 $and$FA.v:19$12_Y 58 | 11 1 59 | .names $and$FA.v:19$11_Y $and$FA.v:19$12_Y cout 60 | 1- 1 61 | -1 1 62 | .names $not$FA.v:11$1_Y y ind0 63 | 11 1 64 | .end 65 | """ 66 | 67 | pyrtl.input_from_blif(full_adder_blif) 68 | # Have to find the actual wire vectors generated from the names in the blif file 69 | x, y, cin = [pyrtl.working_block().get_wirevector_by_name(s) for s in ['x', 'y', 'cin']] 70 | io_vectors = pyrtl.working_block().wirevector_subset((pyrtl.Input, pyrtl.Output)) 71 | 72 | # We are only going to trace the input and output vectors for clarity 73 | sim_trace = pyrtl.SimulationTrace(wires_to_track=io_vectors) 74 | # Now simulate the logic with some random inputs 75 | sim = pyrtl.Simulation(tracer=sim_trace) 76 | for i in range(15): 77 | # here we actually generate random booleans for the inputs 78 | sim.step({ 79 | 'x': random.choice([0, 1]), 80 | 'y': random.choice([0, 1]), 81 | 'cin': random.choice([0, 1]) 82 | }) 83 | sim_trace.render_trace(symbol_len=2) 84 | 85 | 86 | # ---- Exporting to Verilog ---- 87 | 88 | # However, not only do we want to have a method to import from Verilog, we also 89 | # want a way to export it back out to Verilog as well. To demonstrate PyRTL's 90 | # ability to export in Verilog, we will create a sample 3-bit counter. However 91 | # unlike the example in example2, we extend it to be synchronously resetting. 92 | 93 | pyrtl.reset_working_block() 94 | 95 | zero = pyrtl.Input(1, 'zero') 96 | counter_output = pyrtl.Output(3, 'counter_output') 97 | counter = pyrtl.Register(3, 'counter') 98 | counter.next <<= pyrtl.mux(zero, counter + 1, 0) 99 | counter_output <<= counter 100 | 101 | # The counter gets 0 in the next cycle if the "zero" signal goes high, otherwise just 102 | # counter + 1. Note that both "0" and "1" are bit extended to the proper length and 103 | # here we are making use of that native add operation. Let's dump this bad boy out 104 | # to a Verilog file and see what is looks like (here we are using StringIO just to 105 | # print it to a string for demo purposes; most likely you will want to pass a normal 106 | # open file). 107 | 108 | print("--- PyRTL Representation ---") 109 | print(pyrtl.working_block()) 110 | print() 111 | 112 | print("--- Verilog for the Counter ---") 113 | with io.StringIO() as vfile: 114 | pyrtl.output_to_verilog(vfile) 115 | print(vfile.getvalue()) 116 | 117 | print("--- Simulation Results ---") 118 | sim_trace = pyrtl.SimulationTrace([counter_output, zero]) 119 | sim = pyrtl.Simulation(tracer=sim_trace) 120 | for cycle in range(15): 121 | sim.step({'zero': random.choice([0, 0, 0, 1])}) 122 | sim_trace.render_trace() 123 | 124 | # We already did the "hard" work of generating a test input for this simulation, so 125 | # we might want to reuse that work when we take this design through a Verilog toolchain. 126 | # The class OutputVerilogTestbench grabs the inputs used in the simulation trace 127 | # and sets them up in a standard verilog testbench. 128 | 129 | print("--- Verilog for the TestBench ---") 130 | with io.StringIO() as tbfile: 131 | pyrtl.output_verilog_testbench(dest_file=tbfile, simulation_trace=sim_trace) 132 | print(tbfile.getvalue()) 133 | 134 | 135 | # Now let's talk about transformations of the hardware block. Many times when you are 136 | # doing some hardware-level analysis you might wish to ignore higher level things like 137 | # multi-bit wirevectors, adds, concatenation, etc. and just think about wires and basic 138 | # gates. PyRTL supports "lowering" of designs into this more restricted set of functionality 139 | # though the function "synthesize". Once we lower a design to this form we can then apply 140 | # basic optimizations like constant propagation and dead wire elimination as well. By 141 | # printing it out to Verilog we can see exactly how the design changed. 142 | 143 | print("--- Optimized Single-bit Verilog for the Counter ---") 144 | pyrtl.synthesize() 145 | pyrtl.optimize() 146 | 147 | with io.StringIO() as vfile: 148 | pyrtl.output_to_verilog(vfile) 149 | print(vfile.getvalue()) 150 | -------------------------------------------------------------------------------- /examples/example9-transforms-draft.py: -------------------------------------------------------------------------------- 1 | 2 | import pyrtl 3 | from pyrtl import transform 4 | 5 | 6 | def insert_random_inversions(rate=0.5): 7 | import random 8 | 9 | def randomly_replace(wire): 10 | if random.random() < rate: 11 | new_src, new_dst = transform.clone_wire(wire), transform.clone_wire(wire) 12 | new_dst <<= ~new_src 13 | return new_src, new_dst 14 | return wire, wire 15 | 16 | transform.wire_transform(randomly_replace) 17 | 18 | 19 | def probe_wire_if(condition_func): 20 | """ 21 | 22 | :param condition_func: (logic net) -> bool 23 | :return: 24 | """ 25 | 26 | def add_probe_if(wire): 27 | if condition_func(wire): 28 | pyrtl.probe(wire) 29 | return wire, wire 30 | 31 | transform.wire_transform(add_probe_if) 32 | -------------------------------------------------------------------------------- /examples/introduction-to-hardware.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ Introduction to Hardware Design 3 | 4 | This code works through the hardware design process with the 5 | audience of software developers more in mind. We start with the simple 6 | problem of designing a Fibonacci sequence calculator (http://oeis.org/A000045). 7 | """ 8 | 9 | import pyrtl 10 | 11 | 12 | def software_fibonacci(n): 13 | """ A normal old Python function to return the Nth Fibonacci number. """ 14 | a, b = 0, 1 15 | for i in range(n): 16 | a, b = b, a + b 17 | return a 18 | 19 | # Iterative implementation of Fibonacci, just iteratively adds a and b to 20 | # calculate the nth number in the sequence. 21 | # >> [software_fibonacci(x) for x in range(10)] 22 | # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] 23 | 24 | 25 | # So let's convert this into some hardware that computes the same thing. 26 | # Our first go will be to just replace the 0 and 1 with WireVectors to see 27 | # what happens. 28 | 29 | def attempt1_hardware_fibonacci(n, bitwidth): 30 | a = pyrtl.Const(0) 31 | b = pyrtl.Const(1) 32 | for i in range(n): 33 | a, b = b, a + b 34 | return a 35 | 36 | # The above looks really nice but does not really represent a hardware implementation 37 | # of Fibonacci. Let's reason through the code, line by line, to figure out what 38 | # it would actually build. 39 | # a = pyrtl.Const(0) -- This makes a WireVector of bitwidth=1 that is driven by 40 | # a zero. Thus "a" is a WireVector. Seems good. 41 | # b = pyrtl.Const(1) -- Just like above, "b" is a WireVector driven by 1 42 | # for i in range(n): -- Okay, here is where things start to go off the rails a bit. 43 | # This says to perform the following code 'n' times, but the 44 | # value 'n' is passed as an input and is not something that is 45 | # evaluated in the hardware; it is evaluated when you run the 46 | # PyRTL program which generates (or more specifically elaborates) 47 | # the hardware. Thus the hardware we are building will have 48 | # the value of 'n' built into the hardware and won't actually 49 | # be a run-time parameter. Loops are really useful for building 50 | # large repetitive hardware structures, but they CAN'T be used 51 | # to represent hardware that should do a computation iteratively. 52 | # Instead we are going to need to use some registers to build a 53 | # state machine. 54 | # a, b = b, a + b -- Let's break this apart. In the first cycle b is Const(1) and 55 | # (a + b) builds an adder with a (Const(0)) and b (Const(1)) as 56 | # inputs. Thus (b, a + b) in the first iteration is: 57 | # ( Const(1), result_of_adding( Const(0), Const(1) ) 58 | # At the end of the first iteration "a" and "b" refer to those 59 | # two constant values. In each following iteration more 60 | # adders are built and the names "a" and "b" are bound to larger 61 | # and larger trees of adders but all the inputs are constants! 62 | # return a -- The final thing that is returned then is the last output from 63 | # this tree of adders which all have Consts as inputs. Thus 64 | # this hardware is hard-wired to find only and exactly the value 65 | # of Fibonacci of the value N specified at design time! Probably 66 | # not what you are intending. 67 | 68 | 69 | # So let's try a different approach. Let's specify two registers ("a" and "b") and then we 70 | # can update those values as we iteratively compute Fibonacci of N cycle by cycle. 71 | 72 | def attempt2_hardware_fibonacci(n, bitwidth): 73 | a = pyrtl.Register(bitwidth, 'a') 74 | b = pyrtl.Register(bitwidth, 'b') 75 | 76 | a.next <<= b 77 | b.next <<= a + b 78 | 79 | return a 80 | 81 | # This is looking much better. Two registers, "a" and "b", store the values from which we 82 | # can compute the series. The line "a.next <<= b" means that the value of "a" in the next 83 | # cycle should be simply be "b" from the current cycle. The line "b.next <<= a + b" says 84 | # to build an adder with inputs of "a" and "b" from the current cycle and assign the value 85 | # to "b" in the next cycle. A visual representation of the hardware built is as such: 86 | # 87 | # ┌─────┐ ┌─────┐ 88 | # │ │ │ │ 89 | # ▼ │ ▼ │ 90 | # ▕▔▔▔▔▔▏ │ ▕▔▔▔▔▔▏ │ 91 | # ▕ a ▏ │ ▕ b ▏ │ 92 | # ▕▁▁▁▁▁▏ │ ▕▁▁▁▁▁▏ │ 93 | # │ │ │ │ 94 | # │ └─────┤ │ 95 | # │ │ │ 96 | # ▼ ▼ │ 97 | # ╲▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔╱ │ 98 | # ╲ adder ╱ │ 99 | # ╲▁▁▁▁▁▁▁▁▁▁▁╱ │ 100 | # │ │ 101 | # └───────────┘ 102 | # 103 | # Note that in the picture the register "a" and "b" each have a WireVector which is 104 | # the current value (shown flowing out of the bottom of the register) and an "input" 105 | # which is giving the value that should be the value of the register in the following 106 | # cycle (shown flowing into the top of the register) which are "b" and "a + b" respectively. 107 | # When we say "return a" what we are returning is a reference to the register "a" in 108 | # the picture above. 109 | 110 | 111 | # Of course one problem is that we don't know when we are done. How do we know we 112 | # reached the "Nth" number in the sequence? Well, we need to add a register to 113 | # count up and see if we are done. 114 | 115 | def attempt3_hardware_fibonacci(n, bitwidth): 116 | a = pyrtl.Register(bitwidth, 'a') 117 | b = pyrtl.Register(bitwidth, 'b') 118 | i = pyrtl.Register(bitwidth, 'i') 119 | 120 | i.next <<= i + 1 121 | a.next <<= b 122 | b.next <<= a + b 123 | 124 | return a, i == n 125 | 126 | # This is very similar to the example before, except that now we have a register "i" 127 | # which keeps track of the iteration that we are on (i.next <<= i + 1). The function 128 | # now returns two values, a reference to the register "a" and a reference to a single 129 | # bit that tells us if we are done. That bit is calculated by comparing "i" to the 130 | # to a wirevector "n" that is passed in to see if they are the same. 131 | 132 | 133 | # Finally, we need a way to indicate that we want a new Fibonacci number. 134 | # We'll add another input, "req", which when high sets our "local_n" register and 135 | # resets the others. Now our ending condition occurs when the current iteration "i" is 136 | # equal to the locally stored "local_n". 137 | 138 | def attempt4_hardware_fibonacci(n, req, bitwidth): 139 | a = pyrtl.Register(bitwidth, 'a') 140 | b = pyrtl.Register(bitwidth, 'b') 141 | i = pyrtl.Register(bitwidth, 'i') 142 | local_n = pyrtl.Register(bitwidth, 'local_n') 143 | done = pyrtl.WireVector(bitwidth=1, name='done') 144 | 145 | with pyrtl.conditional_assignment: 146 | with req: 147 | local_n.next |= n 148 | i.next |= 0 149 | a.next |= 0 150 | b.next |= 1 151 | with pyrtl.otherwise: 152 | i.next |= i + 1 153 | a.next |= b 154 | b.next |= a + b 155 | done <<= i == local_n 156 | return a, done 157 | 158 | 159 | # This is now far enough along that we can simulate the design and see what happens. 160 | # We begin by connecting our input and output wires to the implementation, 161 | # stepping once with the 'req' signal high to signify we're beginning a 162 | # a new request for a value, and then continuing to step until 'done' is emitted. 163 | # Note that although the Fibonacci implementation only uses the value of 'n' 164 | # when 'req' is high, we must still provide a value for 'n' (and all other inputs 165 | # tracked by the simulator) for each step. 166 | 167 | BITWIDTH = 8 168 | 169 | n_in = pyrtl.Input(BITWIDTH, 'n_in') 170 | req_in = pyrtl.Input(1, 'req_in') 171 | fib_out = pyrtl.Output(BITWIDTH, 'fib_out') 172 | done_out = pyrtl.Output(1, 'done_out') 173 | 174 | output = attempt4_hardware_fibonacci(n_in, req_in, len(n_in)) 175 | fib_out <<= output[0] 176 | done_out <<= output[1] 177 | 178 | sim_trace = pyrtl.SimulationTrace() 179 | sim = pyrtl.Simulation(tracer=sim_trace) 180 | 181 | sim.step({'n_in': 7, 'req_in': 1}) 182 | 183 | sim.step({'n_in': 0, 'req_in': 0}) 184 | while not sim.inspect('done_out'): 185 | sim.step({'n_in': 0, 'req_in': 0}) 186 | 187 | sim_trace.render_trace( 188 | trace_list=['n_in', 'req_in', 'i', 'fib_out', 'done_out'], repr_func=int) 189 | -------------------------------------------------------------------------------- /examples/renderer-demo.py: -------------------------------------------------------------------------------- 1 | import pyrtl 2 | 3 | """Render traces with various WaveRenderer options. 4 | 5 | Run this demo to see which options work well in your terminal. 6 | 7 | """ 8 | 9 | 10 | def make_clock(period: int): 11 | """Make a clock signal that inverts every `period` cycles.""" 12 | assert period > 0 13 | 14 | # Build a chain of registers. 15 | first_reg = pyrtl.Register(bitwidth=1, name=f'clock_0_{period}', 16 | reset_value=1) 17 | last_reg = first_reg 18 | for offset in range(1, period): 19 | reg = pyrtl.Register(bitwidth=1, name=f'clock_{offset}_{period}') 20 | reg.next <<= last_reg 21 | last_reg = reg 22 | 23 | # The first register's input is the inverse of the last register's output. 24 | first_reg.next <<= ~last_reg 25 | return last_reg 26 | 27 | 28 | def make_counter(period: int, bitwidth=2): 29 | """Make a counter that increments every `period` cycles.""" 30 | assert period > 0 31 | 32 | # Build a chain of registers. 33 | first_reg = pyrtl.Register(bitwidth=bitwidth, name=f'counter_0_{period}') 34 | last_reg = first_reg 35 | for offset in range(1, period): 36 | reg = pyrtl.Register(bitwidth=bitwidth, 37 | name=f'counter_{offset}_{period}') 38 | reg.next <<= last_reg 39 | last_reg = reg 40 | 41 | # The first register's input is the last register's output plus 1. 42 | first_reg.next <<= last_reg + pyrtl.Const(1) 43 | return last_reg 44 | 45 | 46 | make_clock(period=1) 47 | make_clock(period=2) 48 | make_counter(period=1) 49 | make_counter(period=2) 50 | 51 | # Simulate 20 cycles. 52 | sim = pyrtl.Simulation() 53 | sim.step_multiple(nsteps=20) 54 | 55 | # Render the trace with a variety of rendering options. 56 | renderers = { 57 | 'powerline': (pyrtl.simulation.PowerlineRendererConstants(), 58 | 'Requires a font with powerline glyphs'), 59 | 'utf-8': (pyrtl.simulation.Utf8RendererConstants(), 60 | 'Unicode, default non-Windows renderer'), 61 | 'utf-8-alt': (pyrtl.simulation.Utf8AltRendererConstants(), 62 | 'Unicode, alternate display option'), 63 | 'cp437': (pyrtl.simulation.Cp437RendererConstants(), 64 | 'Code page 437 (8-bit ASCII), default Windows renderer'), 65 | 'ascii': (pyrtl.simulation.AsciiRendererConstants(), 66 | 'Basic 7-bit ASCII renderer'), 67 | } 68 | 69 | for i, name in enumerate(renderers): 70 | constants, notes = renderers[name] 71 | print(f'# {notes}') 72 | print(f'export PYRTL_RENDERER={name}\n') 73 | sim.tracer.render_trace( 74 | renderer=pyrtl.simulation.WaveRenderer(constants), 75 | repr_func=int) 76 | print() 77 | -------------------------------------------------------------------------------- /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." 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "This next example shows how you make stateful things with registers\n", 15 | "and more complex hardware structures with functions. We generate\n", 16 | "a **3-bit ripple carry adder** building off of the 1-bit adder from\n", 17 | "the prior example, and then hook it to a register to count up modulo 8." 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "metadata": { 24 | "collapsed": true 25 | }, 26 | "outputs": [], 27 | "source": [ 28 | "import pyrtl" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "metadata": { 35 | "collapsed": true 36 | }, 37 | "outputs": [], 38 | "source": [ 39 | "pyrtl.reset_working_block()" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "A **function in PyRTL** is nothing special -- it just so happens that the statements\n", 47 | "it encapsulate tell PyRTL to build some hardware." 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": null, 53 | "metadata": { 54 | "collapsed": true 55 | }, 56 | "outputs": [], 57 | "source": [ 58 | "def one_bit_add(a, b, carry_in):\n", 59 | " assert len(a) == len(b) == 1 # len returns the bitwidth\n", 60 | " sum = a ^ b ^ carry_in\n", 61 | " carry_out = a & b | a & carry_in | b & carry_in\n", 62 | " return sum, carry_out" 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "metadata": {}, 68 | "source": [ 69 | "If we call *one_bit_add*\n", 70 | "above with the arguments *x*, *y*, and *z* it will make a **one-bit adder to add\n", 71 | "those values together** and return the wires for sum and carry_out as applied to *x*,\n", 72 | "*y*, and *z*. If I call it again on *i*, *j*, and *k* it will build a **new one-bit\n", 73 | "adder** for those inputs and return the resulting sum and carry_out for that adder." 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "While PyRTL actually provides an \"+\" operator for wirevectors which generates\n", 81 | "adders, a **ripple carry adder** is something people can understand easily but has\n", 82 | "enough structure to be mildly interesting. Let's **define an adder of arbitrary\n", 83 | "length** recursively and (hopefully) pythonically. More comments after the code." 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "metadata": { 90 | "collapsed": true 91 | }, 92 | "outputs": [], 93 | "source": [ 94 | "def ripple_add(a, b, carry_in=0):\n", 95 | " a, b = pyrtl.match_bitwidth(a, b)\n", 96 | " # this function is a function that allows us to match the bitwidth of multiple\n", 97 | " # different wires. By default, it zero extends the shorter bits\n", 98 | " if len(a) == 1:\n", 99 | " sumbits, carry_out = one_bit_add(a, b, carry_in)\n", 100 | " else:\n", 101 | " lsbit, ripplecarry = one_bit_add(a[0], b[0], carry_in)\n", 102 | " msbits, carry_out = ripple_add(a[1:], b[1:], ripplecarry)\n", 103 | " sumbits = pyrtl.concat(msbits, lsbit)\n", 104 | " return sumbits, carry_out" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "#### The above code breaks down into two cases:\n", 112 | "* If the size of the inputs is one-bit just do one_bit_add.\n", 113 | "* if they are more than one bit, do a one-bit add on the least significant bits, a ripple carry on the rest, and then stick the results back together into one WireVector." 114 | ] 115 | }, 116 | { 117 | "cell_type": "markdown", 118 | "metadata": {}, 119 | "source": [ 120 | "#### A couple interesting features of PyRTL can be seen here:\n", 121 | "* WireVectors can be indexed like lists, with [0] accessing the least significant bit and [1:] being an example of the use of Python slicing syntax.\n", 122 | "* While you can add two lists together in python a WireVector + Wirevector means \"make an adder\" so to concatenate the bits of two vectors one need to use \"concat\".\n", 123 | "* If we look at \"cin\" it seems to have a default value of the integer \"0\" but is a WireVector at other times.Python supports polymorphism throughout and PyRTL will cast integers and some other types to WireVectors when it can." 124 | ] 125 | }, 126 | { 127 | "cell_type": "markdown", 128 | "metadata": {}, 129 | "source": [ 130 | "Now let's **build a 3-bit counter** from our N-bit ripple carry adder." 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "metadata": { 137 | "collapsed": true 138 | }, 139 | "outputs": [], 140 | "source": [ 141 | "counter = pyrtl.Register(bitwidth=3, name='counter')\n", 142 | "sum, carry_out = ripple_add(counter, pyrtl.Const(\"1'b1\"))\n", 143 | "counter.next <<= sum" 144 | ] 145 | }, 146 | { 147 | "cell_type": "markdown", 148 | "metadata": {}, 149 | "source": [ 150 | "#### A couple new things in the above code:\n", 151 | "* The two remaining types of basic WireVectors, Const and Register, both appear. Const, unsurprisingly, is just for holding constants (such as the 0 in ripple_add), but here we create one directly from a Verilog-like string which includes both the value and the bitwidth.\n", 152 | "* Registers are just like wires, except their updates are delayed to the next clock cycle. This is made explicit in the syntax through the property '.next' which should always be set for registers.\n", 153 | "* In this simple example, we take counter next cycle equal to counter this cycle plus one." 154 | ] 155 | }, 156 | { 157 | "cell_type": "markdown", 158 | "metadata": {}, 159 | "source": [ 160 | "Now let's **run the bugger**. No need for inputs, it doesn't have any, but let's\n", 161 | "**throw in an assert** to check that it really counts up modulo 8. Finally we'll\n", 162 | "**print the trace** to the screen." 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": null, 168 | "metadata": { 169 | "collapsed": true 170 | }, 171 | "outputs": [], 172 | "source": [ 173 | "sim_trace = pyrtl.SimulationTrace()\n", 174 | "sim = pyrtl.Simulation(tracer=sim_trace)\n", 175 | "for cycle in range(15):\n", 176 | " sim.step({})\n", 177 | " assert sim.value[counter] == cycle % 8\n", 178 | "sim_trace.render_trace()" 179 | ] 180 | } 181 | ], 182 | "metadata": { 183 | "kernelspec": { 184 | "display_name": "Python 3", 185 | "language": "python", 186 | "name": "python3" 187 | }, 188 | "language_info": { 189 | "codemirror_mode": { 190 | "name": "ipython", 191 | "version": 3 192 | }, 193 | "file_extension": ".py", 194 | "mimetype": "text/x-python", 195 | "name": "python", 196 | "nbconvert_exporter": "python", 197 | "pygments_lexer": "ipython3", 198 | "version": "3.6.3" 199 | } 200 | }, 201 | "nbformat": 4, 202 | "nbformat_minor": 2 203 | } 204 | -------------------------------------------------------------------------------- /ipynb-examples/example3-statemachine.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Example 3: A State Machine built with ConditionalUpdate" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "In this example we describe how **ConditionalUpdate** works in the context of\n", 15 | "a vending machine that will dispense an item when it has received 4 tokens.\n", 16 | "If a refund is requested, it returns the tokens." 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "metadata": { 23 | "collapsed": true 24 | }, 25 | "outputs": [], 26 | "source": [ 27 | "import pyrtl\n", 28 | "pyrtl.reset_working_block()" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "metadata": { 35 | "collapsed": true 36 | }, 37 | "outputs": [], 38 | "source": [ 39 | "token_in = pyrtl.Input(1, 'token_in')\n", 40 | "req_refund = pyrtl.Input(1, 'req_refund')\n", 41 | "dispense = pyrtl.Output(1, 'dispense')\n", 42 | "refund = pyrtl.Output(1, 'refund')\n", 43 | "state = pyrtl.Register(3, 'state')" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "First new step, let's **enumerate a set of constant to serve as our states**" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "metadata": { 57 | "collapsed": true 58 | }, 59 | "outputs": [], 60 | "source": [ 61 | "WAIT, TOK1, TOK2, TOK3, DISPENSE, REFUND = [pyrtl.Const(x, bitwidth=3) for x in range(6)]" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "Now we could build a state machine using just the registers and logic discussed\n", 69 | "in the earlier examples, but doing operations *conditional* on some input is a pretty\n", 70 | "fundamental operation in hardware design. **PyRTL provides a class \"ConditionalUpdate\"**\n", 71 | "to provide a predicated update to a registers, wires, and memories.\n", 72 | "\n", 73 | "**Conditional assignments** are specified with a *\"|=\"* instead of a *\"<<=\"* operator. The\n", 74 | "conditional assignment is only value in the context of a condition, and update to those\n", 75 | "values only happens when that condition is true. In hardware this is implemented\n", 76 | "with a simple mux -- for people coming from software it is important to remember that this\n", 77 | "is describing a big logic function NOT an \"if-then-else\" clause. All of these things will\n", 78 | "execute straight through when *build_everything* is called. More comments after the code.\n", 79 | "\n", 80 | "**One more thing:** ConditionalUpdate might not always be the best item to use.\n", 81 | "if the update is simple, a regular mux(sel_wire, falsecase=f_wire, truecase=t_wire)\n", 82 | "can be sufficient." 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": null, 88 | "metadata": { 89 | "collapsed": true 90 | }, 91 | "outputs": [], 92 | "source": [ 93 | "with pyrtl.conditional_assignment:\n", 94 | " with req_refund: # signal of highest precedence\n", 95 | " state.next |= REFUND\n", 96 | " with token_in: # if token received, advance state in counter sequence\n", 97 | " with state == WAIT:\n", 98 | " state.next |= TOK1\n", 99 | " with state == TOK1:\n", 100 | " state.next |= TOK2\n", 101 | " with state == TOK2:\n", 102 | " state.next |= TOK3\n", 103 | " with state == TOK3:\n", 104 | " state.next |= DISPENSE # 4th token received, go to dispense\n", 105 | " with pyrtl.otherwise: # token received but in state where we can't handle it\n", 106 | " state.next |= REFUND\n", 107 | " # unconditional transition from these two states back to wait state\n", 108 | " # NOTE: the parens are needed because in Python the \"|\" operator is lower precedence\n", 109 | " # than the \"==\" operator!\n", 110 | " with (state == DISPENSE) | (state == REFUND):\n", 111 | " state.next |= WAIT" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "metadata": { 118 | "collapsed": true 119 | }, 120 | "outputs": [], 121 | "source": [ 122 | "dispense <<= state == DISPENSE\n", 123 | "refund <<= state == REFUND" 124 | ] 125 | }, 126 | { 127 | "cell_type": "markdown", 128 | "metadata": {}, 129 | "source": [ 130 | "#### A couple of other things to note:\n", 131 | "* A condition can be nested within another condition and the implied hardware is that the left-hand-side should only get that value if ALL of the encompassing conditions are satisfied.\n", 132 | "* Only one conditional at each level can be true meaning that all conditions are implicitly also saying that none of the prior conditions at the same level also have been true. The highest priority condition is listed first, and in a sense you can think about each other condition as an \"elif\".\n", 133 | "* If not every condition is enumerated, the default value for the register under those cases will be the same as it was the prior cycle (\"state.next |= state\" in this example). The default for a wirevector is 0.\n", 134 | "* There is a way to specify something like an \"else\" instead of \"elif\" and that is with an \"otherwise\" (as seen on the line above \"state.next <<= REFUND\"). This condition will be true if none of the other conditions at the same level were also true (for this example specifically state.next will get REFUND when req_refund==0, token_in==1, and state is not in TOK1, TOK2, TOK3, or DISPENSE.\n", 135 | "* Not shown here, you can update multiple different registers, wires, and memories all under the same set of conditionals." 136 | ] 137 | }, 138 | { 139 | "cell_type": "markdown", 140 | "metadata": {}, 141 | "source": [ 142 | "#### A more artificial example might make it even more clear how these rules interact:\n", 143 | "```python \n", 144 | "with a:\n", 145 | " r.next |= 1 <-- when a is true\n", 146 | " with d:\n", 147 | " r2.next |= 2 <-- when a and d are true\n", 148 | " with otherwise:\n", 149 | " r2.next |= 3 <-- when a is true and d is false\n", 150 | "with b == c:\n", 151 | " r.next |= 0 <-- when a is not true and b == c is true\n", 152 | "```" 153 | ] 154 | }, 155 | { 156 | "cell_type": "markdown", 157 | "metadata": {}, 158 | "source": [ 159 | "Now let's **build and test our state machine**." 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": null, 165 | "metadata": { 166 | "collapsed": true 167 | }, 168 | "outputs": [], 169 | "source": [ 170 | "sim_trace = pyrtl.SimulationTrace()\n", 171 | "sim = pyrtl.Simulation(tracer=sim_trace)" 172 | ] 173 | }, 174 | { 175 | "cell_type": "markdown", 176 | "metadata": {}, 177 | "source": [ 178 | "Rather than just give some random inputs, let's **specify some specific 1 bit values**. Recall\n", 179 | "that the sim.step method takes a dictionary mapping inputs to their values. We could just\n", 180 | "specify the input set directly as a dictionary but it gets pretty ugly -- let's use some python\n", 181 | "to parse them up." 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": null, 187 | "metadata": { 188 | "collapsed": true 189 | }, 190 | "outputs": [], 191 | "source": [ 192 | "sim_inputs = {\n", 193 | " 'token_in': '0010100111010000',\n", 194 | " 'req_refund': '1100010000000000'\n", 195 | " }\n", 196 | "\n", 197 | "for cycle in range(len(sim_inputs['token_in'])):\n", 198 | " sim.step({w: int(v[cycle]) for w, v in sim_inputs.items()})" 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "metadata": {}, 204 | "source": [ 205 | "Also, to make our input/output easy to reason about let's **specify an order to the traces**\n" 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": null, 211 | "metadata": { 212 | "collapsed": true 213 | }, 214 | "outputs": [], 215 | "source": [ 216 | "sim_trace.render_trace(trace_list=['token_in', 'req_refund', 'state', 'dispense', 'refund'])" 217 | ] 218 | } 219 | ], 220 | "metadata": { 221 | "kernelspec": { 222 | "display_name": "Python 3", 223 | "language": "python", 224 | "name": "python3" 225 | }, 226 | "language_info": { 227 | "codemirror_mode": { 228 | "name": "ipython", 229 | "version": 3 230 | }, 231 | "file_extension": ".py", 232 | "mimetype": "text/x-python", 233 | "name": "python", 234 | "nbconvert_exporter": "python", 235 | "pygments_lexer": "ipython3", 236 | "version": "3.6.3" 237 | } 238 | }, 239 | "nbformat": 4, 240 | "nbformat_minor": 2 241 | } 242 | -------------------------------------------------------------------------------- /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." 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "The following example shows how pyrtl can be used to make some interesting\n", 15 | "hardware structures using **python introspection**. In particular, this example\n", 16 | "makes a **N-stage pipeline structure**. Any specific pipeline is then a derived\n", 17 | "class of *SimplePipeline* where methods with names starting with *\"stage\"* are\n", 18 | "stages, and new members with names not starting with \"\\_\" are to be registered\n", 19 | "for the next stage." 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "### Pipeline builder with auto generation of pipeline registers." 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": { 33 | "collapsed": true 34 | }, 35 | "outputs": [], 36 | "source": [ 37 | "import pyrtl\n", 38 | "\n", 39 | "pyrtl.reset_working_block()\n", 40 | "\n", 41 | "class SimplePipeline(object):\n", 42 | " def __init__(self):\n", 43 | " self._pipeline_register_map = {}\n", 44 | " self._current_stage_num = 0\n", 45 | " stage_list = [method for method in dir(self) if method.startswith('stage')]\n", 46 | " for stage in sorted(stage_list):\n", 47 | " stage_method = getattr(self, stage)\n", 48 | " stage_method()\n", 49 | " self._current_stage_num += 1\n", 50 | " \n", 51 | " def __getattr__(self, name):\n", 52 | " try:\n", 53 | " return self._pipeline_register_map[self._current_stage_num][name]\n", 54 | " except KeyError:\n", 55 | " raise pyrtl.PyrtlError(\n", 56 | " 'error, no pipeline register \"%s\" defined for stage %d'\n", 57 | " % (name, self._current_stage_num))\n", 58 | "\n", 59 | " def __setattr__(self, name, value):\n", 60 | " if name.startswith('_'):\n", 61 | " # do not do anything tricky with variables starting with '_'\n", 62 | " object.__setattr__(self, name, value)\n", 63 | " else:\n", 64 | " next_stage = self._current_stage_num + 1\n", 65 | " pipereg_id = str(self._current_stage_num) + 'to' + str(next_stage)\n", 66 | " rname = 'pipereg_' + pipereg_id + '_' + name\n", 67 | " new_pipereg = pyrtl.Register(bitwidth=len(value), name=rname)\n", 68 | " if next_stage not in self._pipeline_register_map:\n", 69 | " self._pipeline_register_map[next_stage] = {}\n", 70 | " self._pipeline_register_map[next_stage][name] = new_pipereg\n", 71 | " new_pipereg.next <<= value " 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "### A very simple pipeline to show how registers are inferred." 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": { 85 | "collapsed": true 86 | }, 87 | "outputs": [], 88 | "source": [ 89 | "class SimplePipelineExample(SimplePipeline):\n", 90 | " def __init__(self):\n", 91 | " self._loopback = pyrtl.WireVector(1, 'loopback')\n", 92 | " super(SimplePipelineExample, self).__init__()\n", 93 | "\n", 94 | " def stage0(self):\n", 95 | " self.n = ~ self._loopback\n", 96 | "\n", 97 | " def stage1(self):\n", 98 | " self.n = self.n\n", 99 | "\n", 100 | " def stage2(self):\n", 101 | " self.n = self.n\n", 102 | "\n", 103 | " def stage3(self):\n", 104 | " self.n = self.n\n", 105 | "\n", 106 | " def stage4(self):\n", 107 | " self._loopback <<= self.n" 108 | ] 109 | }, 110 | { 111 | "cell_type": "markdown", 112 | "metadata": {}, 113 | "source": [ 114 | "### Simulation of the core" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": null, 120 | "metadata": {}, 121 | "outputs": [], 122 | "source": [ 123 | "simplepipeline = SimplePipelineExample()\n", 124 | "\n", 125 | "sim_trace = pyrtl.SimulationTrace()\n", 126 | "sim = pyrtl.Simulation(tracer=sim_trace)\n", 127 | "\n", 128 | "for cycle in range(15):\n", 129 | " sim.step({})\n", 130 | "sim_trace.render_trace()" 131 | ] 132 | } 133 | ], 134 | "metadata": { 135 | "kernelspec": { 136 | "display_name": "Python 3", 137 | "language": "python", 138 | "name": "python3" 139 | }, 140 | "language_info": { 141 | "codemirror_mode": { 142 | "name": "ipython", 143 | "version": 3 144 | }, 145 | "file_extension": ".py", 146 | "mimetype": "text/x-python", 147 | "name": "python", 148 | "nbconvert_exporter": "python", 149 | "pygments_lexer": "ipython3", 150 | "version": "3.6.3" 151 | } 152 | }, 153 | "nbformat": 4, 154 | "nbformat_minor": 2 155 | } 156 | -------------------------------------------------------------------------------- /ipynb-examples/example7-synth-timing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Example 7: Reduction and Speed Analysis" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "After building a circuit, one might want to do some stuff to **reduce the\n", 15 | "hardware into simpler nets** as well as **analyze various metrics of the\n", 16 | "hardware**. This functionality is provided in the Passes part of PyRTL\n", 17 | "and will demonstrated here." 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "metadata": { 24 | "collapsed": true 25 | }, 26 | "outputs": [], 27 | "source": [ 28 | "import pyrtl" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "## Part 1: Timing Analysis" 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "**Timing and area usage** are key considerations of any hardware block that one\n", 43 | "makes.\n", 44 | "\n", 45 | "PyRTL provides functions to do these opertions" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": null, 51 | "metadata": { 52 | "collapsed": true 53 | }, 54 | "outputs": [], 55 | "source": [ 56 | "# Creating a sample harware block\n", 57 | "pyrtl.reset_working_block()\n", 58 | "const_wire = pyrtl.Const(6, bitwidth=4)\n", 59 | "in_wire2 = pyrtl.Input(bitwidth=4, name=\"input2\")\n", 60 | "out_wire = pyrtl.Output(bitwidth=5, name=\"output\")\n", 61 | "out_wire <<= const_wire + in_wire2" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "### Now we will do the timing analysis as well as print out the critical path" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": null, 74 | "metadata": {}, 75 | "outputs": [], 76 | "source": [ 77 | "# Generating timing analysis information\n", 78 | "print(\"Pre Synthesis:\")\n", 79 | "timing = pyrtl.TimingAnalysis()\n", 80 | "timing.print_max_length()" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "We are also able to **print out the critical paths** as well as get them\n", 88 | "back as an array." 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "critical_path_info = timing.critical_path()" 98 | ] 99 | }, 100 | { 101 | "cell_type": "markdown", 102 | "metadata": {}, 103 | "source": [ 104 | "## Part 2: Area Analysis" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "PyRTL also provides **estimates for the area** that would be used up if the\n", 112 | "circuit was printed as an **ASIC**" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "logic_area, mem_area = pyrtl.area_estimation(tech_in_nm=65)\n", 122 | "est_area = logic_area + mem_area\n", 123 | "print(\"Estimated Area of block\", est_area, \"sq mm\")\n", 124 | "print()" 125 | ] 126 | }, 127 | { 128 | "cell_type": "markdown", 129 | "metadata": {}, 130 | "source": [ 131 | "## Part 3: Synthesis" 132 | ] 133 | }, 134 | { 135 | "cell_type": "markdown", 136 | "metadata": {}, 137 | "source": [ 138 | "Synthesis is the operation of **reducing the circuit into simpler components**\n", 139 | "The base synthesis function breaks down the more complex logic operations\n", 140 | "into logic gates (keeps registers and memories intact) as well as **reduces\n", 141 | "all combinatorial logic into ops that only use one bitwidth wires**\n", 142 | "\n", 143 | "This synthesis allows for PyRTL to make **optimizations to the net structure**\n", 144 | "as well as prepares it for further transformations on the PyRTL Toolchain" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": null, 150 | "metadata": {}, 151 | "outputs": [], 152 | "source": [ 153 | "pyrtl.synthesize()\n", 154 | "\n", 155 | "print(\"Pre Optimization:\")\n", 156 | "timing = pyrtl.TimingAnalysis()\n", 157 | "timing.print_max_length()\n", 158 | "for net in pyrtl.working_block().logic:\n", 159 | " print(str(net))\n", 160 | "print()" 161 | ] 162 | }, 163 | { 164 | "cell_type": "markdown", 165 | "metadata": {}, 166 | "source": [ 167 | "## Part 4: Optimization" 168 | ] 169 | }, 170 | { 171 | "cell_type": "markdown", 172 | "metadata": {}, 173 | "source": [ 174 | "PyRTL has functions built in to **eliminate unnecessary logic** from the\n", 175 | "circuit.\n", 176 | "\n", 177 | "These functions are all done with a simple call:" 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": null, 183 | "metadata": {}, 184 | "outputs": [], 185 | "source": [ 186 | "pyrtl.optimize()" 187 | ] 188 | }, 189 | { 190 | "cell_type": "markdown", 191 | "metadata": {}, 192 | "source": [ 193 | "### Now to see the difference" 194 | ] 195 | }, 196 | { 197 | "cell_type": "code", 198 | "execution_count": null, 199 | "metadata": {}, 200 | "outputs": [], 201 | "source": [ 202 | "print(\"Post Optimization:\")\n", 203 | "timing = pyrtl.TimingAnalysis()\n", 204 | "timing.print_max_length()" 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": null, 210 | "metadata": {}, 211 | "outputs": [], 212 | "source": [ 213 | "for net in pyrtl.working_block().logic:\n", 214 | " print(str(net))" 215 | ] 216 | }, 217 | { 218 | "cell_type": "markdown", 219 | "metadata": {}, 220 | "source": [ 221 | "### As we can see, the number of nets in the circuit were drastically reduced by the optimization algorithm." 222 | ] 223 | } 224 | ], 225 | "metadata": { 226 | "kernelspec": { 227 | "display_name": "Python 3", 228 | "language": "python", 229 | "name": "python3" 230 | }, 231 | "language_info": { 232 | "codemirror_mode": { 233 | "name": "ipython", 234 | "version": 3 235 | }, 236 | "file_extension": ".py", 237 | "mimetype": "text/x-python", 238 | "name": "python", 239 | "nbconvert_exporter": "python", 240 | "pygments_lexer": "ipython3", 241 | "version": "3.6.3" 242 | } 243 | }, 244 | "nbformat": 4, 245 | "nbformat_minor": 2 246 | } 247 | -------------------------------------------------------------------------------- /ipynb-examples/example8-verilog.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Example 8: Interfacing with Verilog." 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "While there is much more about PyRTL design to discuss, at some point somebody\n", 15 | "might ask you to do something with your code other than have it print\n", 16 | "pretty things out to the terminal. We provide **import from and export to\n", 17 | "Verilog of designs**, export of **waveforms to VCD**, and a set of **transforms\n", 18 | "that make doing netlist-level transforms and analyis directly in pyrtl easy.**" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": { 25 | "collapsed": true 26 | }, 27 | "outputs": [], 28 | "source": [ 29 | "import random\n", 30 | "import io\n", 31 | "import pyrtl" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "metadata": { 38 | "collapsed": true 39 | }, 40 | "outputs": [], 41 | "source": [ 42 | "pyrtl.reset_working_block()" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "## Importing From Verilog" 50 | ] 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "metadata": {}, 55 | "source": [ 56 | "Sometimes it is useful to pull in components written in Verilog to be used\n", 57 | "as subcomponents of PyRTL designs or to be subject to analysis written over\n", 58 | "the PyRTL core. One standard format supported by PyRTL is **\"blif\" format:**\n", 59 | "https://www.ece.cmu.edu/~ee760/760docs/blif.pdf\n", 60 | "\n", 61 | "Many tools support outputing hardware designs to this format, including the\n", 62 | "free open source project \"Yosys\". Blif files can then be imported either\n", 63 | "as a string or directly from a file name by the function input_from_blif.\n", 64 | "Here is a simple example of a **1 bit full adder imported and then simulated**\n", 65 | "from this blif format." 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": null, 71 | "metadata": { 72 | "collapsed": true 73 | }, 74 | "outputs": [], 75 | "source": [ 76 | "full_adder_blif = \"\"\"\n", 77 | "# Generated by Yosys 0.3.0+ (git sha1 7e758d5, clang 3.4-1ubuntu3 -fPIC -Os)\n", 78 | ".model full_adder\n", 79 | ".inputs x y cin\n", 80 | ".outputs sum cout\n", 81 | ".names $false\n", 82 | ".names $true\n", 83 | "1\n", 84 | ".names y $not$FA.v:12$3_Y\n", 85 | "0 1\n", 86 | ".names x $not$FA.v:11$1_Y\n", 87 | "0 1\n", 88 | ".names cin $not$FA.v:15$6_Y\n", 89 | "0 1\n", 90 | ".names ind3 ind4 sum\n", 91 | "1- 1\n", 92 | "-1 1\n", 93 | ".names $not$FA.v:15$6_Y ind2 ind3\n", 94 | "11 1\n", 95 | ".names x $not$FA.v:12$3_Y ind1\n", 96 | "11 1\n", 97 | ".names ind2 $not$FA.v:16$8_Y\n", 98 | "0 1\n", 99 | ".names cin $not$FA.v:16$8_Y ind4\n", 100 | "11 1\n", 101 | ".names x y $and$FA.v:19$11_Y\n", 102 | "11 1\n", 103 | ".names ind0 ind1 ind2\n", 104 | "1- 1\n", 105 | "-1 1\n", 106 | ".names cin ind2 $and$FA.v:19$12_Y\n", 107 | "11 1\n", 108 | ".names $and$FA.v:19$11_Y $and$FA.v:19$12_Y cout\n", 109 | "1- 1\n", 110 | "-1 1\n", 111 | ".names $not$FA.v:11$1_Y y ind0\n", 112 | "11 1\n", 113 | ".end\n", 114 | "\"\"\"" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": null, 120 | "metadata": { 121 | "collapsed": true 122 | }, 123 | "outputs": [], 124 | "source": [ 125 | "pyrtl.input_from_blif(full_adder_blif)\n", 126 | "# have to find the actual wire vectors generated from the names in the blif file\n", 127 | "x, y, cin = [pyrtl.working_block().get_wirevector_by_name(s) for s in ['x', 'y', 'cin']]\n", 128 | "io_vectors = pyrtl.working_block().wirevector_subset((pyrtl.Input, pyrtl.Output))" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": null, 134 | "metadata": { 135 | "collapsed": true 136 | }, 137 | "outputs": [], 138 | "source": [ 139 | "# we are only going to trace the input and output vectors for clarity\n", 140 | "sim_trace = pyrtl.SimulationTrace(wires_to_track=io_vectors)" 141 | ] 142 | }, 143 | { 144 | "cell_type": "markdown", 145 | "metadata": {}, 146 | "source": [ 147 | "### Now simulate the logic with some random inputs" 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": null, 153 | "metadata": {}, 154 | "outputs": [], 155 | "source": [ 156 | "sim = pyrtl.Simulation(tracer=sim_trace)\n", 157 | "for i in range(15):\n", 158 | " # here we actually generate random booleans for the inputs\n", 159 | " sim.step({\n", 160 | " 'x': random.choice([0, 1]),\n", 161 | " 'y': random.choice([0, 1]),\n", 162 | " 'cin': random.choice([0, 1])\n", 163 | " })\n", 164 | "sim_trace.render_trace(symbol_len=5, segment_size=5)" 165 | ] 166 | }, 167 | { 168 | "cell_type": "markdown", 169 | "metadata": {}, 170 | "source": [ 171 | "## Exporting to Verilog\n", 172 | "\n", 173 | "However, not only do we want to have a method to import from Verilog, we also\n", 174 | "want a way to **export it back out to Verilog** as well. To demonstrate PyRTL's\n", 175 | "ability to export in Verilog, we will create a **sample 3-bit counter**. However\n", 176 | "unlike the example in example2, we extend it to be **synchronously resetting**." 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": null, 182 | "metadata": { 183 | "collapsed": true 184 | }, 185 | "outputs": [], 186 | "source": [ 187 | "pyrtl.reset_working_block()\n", 188 | "\n", 189 | "zero = pyrtl.Input(1, 'zero')\n", 190 | "counter_output = pyrtl.Output(3, 'counter_output')\n", 191 | "counter = pyrtl.Register(3, 'counter')\n", 192 | "counter.next <<= pyrtl.mux(zero, counter + 1, 0)\n", 193 | "counter_output <<= counter" 194 | ] 195 | }, 196 | { 197 | "cell_type": "markdown", 198 | "metadata": {}, 199 | "source": [ 200 | "The counter **gets 0 in the next cycle if the \"zero\" signal goes high, otherwise just\n", 201 | "counter + 1.** Note that both \"0\" and \"1\" are bit extended to the proper length and\n", 202 | "here we are making use of that native add operation.\n", 203 | "\n", 204 | "Let's dump this bad boy out\n", 205 | "to a verilog file and see what is looks like (here we are using StringIO just to\n", 206 | "print it to a string for demo purposes, most likely you will want to pass a normal\n", 207 | "open file)." 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": null, 213 | "metadata": { 214 | "collapsed": true 215 | }, 216 | "outputs": [], 217 | "source": [ 218 | "print(\"--- PyRTL Representation ---\")\n", 219 | "print(pyrtl.working_block())\n", 220 | "print()\n", 221 | "\n", 222 | "print(\"--- Verilog for the Counter ---\")\n", 223 | "with io.StringIO() as vfile:\n", 224 | " pyrtl.OutputToVerilog(vfile)\n", 225 | " print(vfile.getvalue())\n", 226 | "\n", 227 | "print(\"--- Simulation Results ---\")\n", 228 | "sim_trace = pyrtl.SimulationTrace([counter_output, zero])\n", 229 | "sim = pyrtl.Simulation(tracer=sim_trace)\n", 230 | "for cycle in range(15):\n", 231 | " sim.step({'zero': random.choice([0, 0, 0, 1])})\n", 232 | "sim_trace.render_trace()" 233 | ] 234 | }, 235 | { 236 | "cell_type": "markdown", 237 | "metadata": {}, 238 | "source": [ 239 | "We already did the *\"hard\" work* of generating a test input for this simulation so\n", 240 | "we might want to reuse that work when we take this design through a verilog toolchain.\n", 241 | "The class *__OutputVerilogTestbench__ grabs the inputs used in the simulation trace\n", 242 | "and sets them up in a standard verilog testbench.*" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": null, 248 | "metadata": { 249 | "collapsed": true 250 | }, 251 | "outputs": [], 252 | "source": [ 253 | "print(\"--- Verilog for the TestBench ---\")\n", 254 | "with io.StringIO() as tbfile:\n", 255 | " pyrtl.output_verilog_testbench(dest_file=tbfile, simulation_trace=sim_trace)\n", 256 | " print(tbfile.getvalue())" 257 | ] 258 | }, 259 | { 260 | "cell_type": "markdown", 261 | "metadata": {}, 262 | "source": [ 263 | "### Now let's talk about transformations of the hardware block. \n", 264 | "Many times when you are\n", 265 | "doing some hardware-level analysis you might wish to ignore higher level things like\n", 266 | "multi-bit wirevectors, adds, concatination, etc. and just thing about wires and basic\n", 267 | "gates. **PyRTL supports \"lowering\" of designs** into this more restricted set of functionality\n", 268 | "though the function *\"synthesize\".* Once we lower a design to this form we can then **apply\n", 269 | "basic optimizations** like constant propgation and dead wire elimination as well. By\n", 270 | "printing it out to verilog we can see exactly how the design changed." 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": null, 276 | "metadata": { 277 | "collapsed": true 278 | }, 279 | "outputs": [], 280 | "source": [ 281 | "print(\"--- Optimized Single-bit Verilog for the Counter ---\")\n", 282 | "pyrtl.synthesize()\n", 283 | "pyrtl.optimize()\n", 284 | "\n", 285 | "with io.StringIO() as vfile:\n", 286 | " pyrtl.OutputToVerilog(vfile)\n", 287 | " print(vfile.getvalue())" 288 | ] 289 | } 290 | ], 291 | "metadata": { 292 | "kernelspec": { 293 | "display_name": "Python 3", 294 | "language": "python", 295 | "name": "python3" 296 | }, 297 | "language_info": { 298 | "codemirror_mode": { 299 | "name": "ipython", 300 | "version": 3 301 | }, 302 | "file_extension": ".py", 303 | "mimetype": "text/x-python", 304 | "name": "python", 305 | "nbconvert_exporter": "python", 306 | "pygments_lexer": "ipython3", 307 | "version": "3.6.3" 308 | } 309 | }, 310 | "nbformat": 4, 311 | "nbformat_minor": 2 312 | } 313 | -------------------------------------------------------------------------------- /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.8" 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 | 41 | [project.optional-dependencies] 42 | # Required by `input_from_blif`. 43 | blif = ["pyparsing"] 44 | # Required by `block_to_svg`. 45 | svg = ["graphviz"] 46 | 47 | [project.urls] 48 | Homepage = "http://ucsbarchlab.github.io/PyRTL/" 49 | GitHub = "https://github.com/UCSBarchlab/PyRTL" 50 | Documentation = "https://pyrtl.readthedocs.io/" 51 | Changelog = "https://github.com/UCSBarchlab/PyRTL/blob/development/CHANGELOG.md" 52 | 53 | [tool.hatch.version] 54 | source = "vcs" 55 | -------------------------------------------------------------------------------- /pyrtl/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # error types thrown 3 | from .pyrtlexceptions import PyrtlError 4 | from .pyrtlexceptions import PyrtlInternalError 5 | 6 | # core rtl constructs 7 | from .core import LogicNet 8 | from .core import Block 9 | from .core import PostSynthBlock 10 | from .core import working_block 11 | from .core import reset_working_block 12 | from .core import set_working_block 13 | from .core import temp_working_block 14 | from .core import set_debug_mode 15 | 16 | # convenience classes for building hardware 17 | from .wire import WireVector 18 | from .wire import Input, Output 19 | from .wire import Const 20 | from .wire import Register 21 | 22 | # helper functions 23 | from .helperfuncs import input_list 24 | from .helperfuncs import output_list 25 | from .helperfuncs import register_list 26 | from .helperfuncs import wirevector_list 27 | from .helperfuncs import log2 28 | from .helperfuncs import truncate 29 | from .helperfuncs import match_bitpattern 30 | from .helperfuncs import bitpattern_to_val 31 | from .helperfuncs import chop 32 | from .helperfuncs import val_to_signed_integer 33 | from .helperfuncs import val_to_formatted_str 34 | from .helperfuncs import formatted_str_to_val 35 | from .helperfuncs import infer_val_and_bitwidth 36 | from .helperfuncs import probe 37 | from .helperfuncs import rtl_assert 38 | from .helperfuncs import check_rtl_assertions 39 | from .helperfuncs import find_loop 40 | from .helperfuncs import find_and_print_loop 41 | from .helperfuncs import wire_struct 42 | from .helperfuncs import wire_matrix 43 | from .helperfuncs import one_hot_to_binary 44 | from .helperfuncs import binary_to_one_hot 45 | 46 | from .corecircuits import and_all_bits 47 | from .corecircuits import or_all_bits 48 | from .corecircuits import xor_all_bits 49 | from .corecircuits import rtl_any 50 | from .corecircuits import rtl_all 51 | from .corecircuits import mux 52 | from .corecircuits import select 53 | from .corecircuits import concat 54 | from .corecircuits import concat_list 55 | from .corecircuits import parity 56 | from .corecircuits import tree_reduce 57 | from .corecircuits import as_wires 58 | from .corecircuits import match_bitwidth 59 | from .corecircuits import enum_mux 60 | from .corecircuits import bitfield_update 61 | from .corecircuits import bitfield_update_set 62 | from .corecircuits import signed_add 63 | from .corecircuits import signed_sub 64 | from .corecircuits import signed_mult 65 | from .corecircuits import signed_lt 66 | from .corecircuits import signed_le 67 | from .corecircuits import signed_gt 68 | from .corecircuits import signed_ge 69 | from .corecircuits import shift_left_arithmetic 70 | from .corecircuits import shift_right_arithmetic 71 | from .corecircuits import shift_left_logical 72 | from .corecircuits import shift_right_logical 73 | 74 | # memory blocks 75 | from .memory import MemBlock 76 | from .memory import RomBlock 77 | 78 | # conditional updates 79 | from .conditional import conditional_assignment 80 | from .conditional import otherwise 81 | from .conditional import currently_under_condition 82 | 83 | # block simulation support 84 | from .simulation import Simulation 85 | from .simulation import FastSimulation 86 | from .simulation import SimulationTrace 87 | from .simulation import enum_name 88 | from .compilesim import CompiledSimulation 89 | 90 | # block visualization output formats 91 | from .visualization import output_to_trivialgraph 92 | from .visualization import graphviz_detailed_namer 93 | from .visualization import output_to_graphviz 94 | from .visualization import output_to_svg 95 | from .visualization import block_to_graphviz_string 96 | from .visualization import block_to_svg 97 | from .visualization import trace_to_html 98 | from .visualization import net_graph 99 | 100 | # import from and export to file format routines 101 | from .importexport import input_from_verilog 102 | from .importexport import output_to_verilog 103 | from .importexport import OutputToVerilog 104 | from .importexport import output_verilog_testbench 105 | from .importexport import input_from_blif 106 | from .importexport import output_to_firrtl 107 | from .importexport import input_from_iscas_bench 108 | 109 | # different transform passes 110 | from .passes import common_subexp_elimination 111 | from .passes import constant_propagation 112 | from .passes import synthesize 113 | from .passes import nand_synth 114 | from .passes import and_inverter_synth 115 | from .passes import optimize 116 | from .passes import one_bit_selects 117 | from .passes import two_way_concat 118 | from .passes import direct_connect_outputs 119 | from .passes import two_way_fanout 120 | 121 | from .transform import net_transform 122 | from .transform import wire_transform 123 | from .transform import copy_block 124 | from .transform import clone_wire 125 | from .transform import replace_wires 126 | from .transform import replace_wire_fast 127 | 128 | # analysis and estimation functions 129 | from .analysis import area_estimation 130 | from .analysis import TimingAnalysis 131 | from .analysis import yosys_area_delay 132 | from .analysis import paths 133 | from .analysis import distance 134 | from .analysis import fanout 135 | -------------------------------------------------------------------------------- /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 | pass 13 | 14 | 15 | class PyrtlInternalError(Exception): 16 | """ Raised on any PyRTL internal failure. """ 17 | pass 18 | -------------------------------------------------------------------------------- /pyrtl/rtllib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/d5f8dbe53f54e61e1d54722449e4894b885243c7/pyrtl/rtllib/__init__.py -------------------------------------------------------------------------------- /pyrtl/rtllib/barrel.py: -------------------------------------------------------------------------------- 1 | import pyrtl 2 | import math 3 | 4 | 5 | def barrel_shifter(bits_to_shift, bit_in, direction, shift_dist, wrap_around=0): 6 | """ Create a barrel shifter that operates on data based on the wire width. 7 | 8 | :param bits_to_shift: the input wire 9 | :param bit_in: the 1-bit wire giving the value to shift in 10 | :param direction: a one bit WireVector representing shift direction 11 | (0 = shift down, 1 = shift up) 12 | :param shift_dist: WireVector representing offset to shift 13 | :param wrap_around: ****currently not implemented**** 14 | :return: shifted WireVector 15 | """ 16 | from pyrtl import concat, select # just for readability 17 | 18 | if wrap_around != 0: 19 | raise NotImplementedError 20 | 21 | # Implement with logN stages pyrtl.muxing between shifted and un-shifted values 22 | final_width = len(bits_to_shift) 23 | val = bits_to_shift 24 | append_val = bit_in 25 | 26 | for i in range(len(shift_dist)): 27 | shift_amt = pow(2, i) # stages shift 1,2,4,8,... 28 | if shift_amt < final_width: 29 | newval = select(direction, 30 | concat(val[:-shift_amt], append_val), # shift up 31 | concat(append_val, val[shift_amt:])) # shift down 32 | val = select(shift_dist[i], 33 | truecase=newval, # if bit of shift is 1, do the shift 34 | falsecase=val) # otherwise, don't 35 | # the value to append grows exponentially, but is capped at full width 36 | append_val = concat(append_val, append_val)[:final_width] 37 | else: 38 | # if we are shifting this much, all the data is gone 39 | val = select(shift_dist[i], 40 | truecase=append_val, # if bit of shift is 1, do the shift 41 | falsecase=val) # otherwise, don't 42 | 43 | return val 44 | -------------------------------------------------------------------------------- /pyrtl/rtllib/libutils.py: -------------------------------------------------------------------------------- 1 | import pyrtl 2 | 3 | 4 | def match_bitwidth(*args): 5 | # TODO: allow for custom bit extension functions 6 | """ Matches the bitwidth of all of the input arguments. 7 | 8 | :param WireVector args: input arguments 9 | :return: tuple of `args` in order with extended bits 10 | """ 11 | return pyrtl.match_bitwidth(*args) 12 | 13 | 14 | def partition_wire(wire, partition_size): 15 | """ Partitions a wire into a list of N wires of size `partition_size`. 16 | 17 | :param wire: Wire to partition 18 | :param partition_size: Integer representing size of each partition 19 | 20 | The `wire`'s bitwidth must be evenly divisible by `parition_size`. 21 | """ 22 | if len(wire) % partition_size != 0: 23 | raise pyrtl.PyrtlError("Wire {} cannot be evenly partitioned into items of size {}" 24 | .format(wire, partition_size)) 25 | return [wire[offset:offset + partition_size] for offset in range(0, len(wire), partition_size)] 26 | 27 | 28 | def str_to_int_array(string, base=16): 29 | """ 30 | Converts a string to an array of integer values according to the 31 | base specified (int numbers must be whitespace delimited).\n 32 | Example: ``"13 a3 3c" => [0x13, 0xa3, 0x3c]`` 33 | 34 | :return: [int] 35 | """ 36 | 37 | int_strings = string.split() 38 | return [int(int_str, base) for int_str in int_strings] 39 | 40 | 41 | def twos_comp_repr(val, bitwidth): 42 | """Converts a value to its two's-complement (positive) integer 43 | representation using a given bitwidth (only converts the value if it is 44 | negative). 45 | 46 | :param val: Integer literal to convert to two's complement 47 | :param bitwidth: Size of val in bits 48 | 49 | For use with :py:meth:`.Simulation.step` etc. in passing negative numbers, 50 | which it does not accept. 51 | 52 | """ 53 | correctbw = abs(val).bit_length() + 1 54 | if bitwidth < correctbw: 55 | raise pyrtl.PyrtlError("please choose a larger target bitwidth") 56 | if val >= 0: 57 | return val 58 | else: 59 | return (~abs(val) & (2 ** bitwidth - 1)) + 1 # flip the bits and add one 60 | 61 | 62 | def rev_twos_comp_repr(val, bitwidth): 63 | """ 64 | Takes a two's-complement represented value and 65 | converts it to a signed integer based on the provided `bitwidth`. 66 | For use with :py:meth:`.Simulation.inspect` etc. when expecting negative numbers, 67 | which it does not recognize 68 | """ 69 | valbl = val.bit_length() 70 | if bitwidth < val.bit_length() or val == 2 ** (bitwidth - 1): 71 | raise pyrtl.PyrtlError("please choose a larger target bitwidth") 72 | if bitwidth == valbl: # MSB is a 1, value is negative 73 | return -((~val & (2 ** bitwidth - 1)) + 1) # flip the bits, add one, and make negative 74 | else: 75 | return val 76 | 77 | 78 | def _shifted_reg_next(reg, direct, num=1): 79 | """ 80 | Creates a shifted 'next' property for shifted (left or right) register.\n 81 | Use: `myReg.next = shifted_reg_next(myReg, 'l', 4)` 82 | 83 | :param string direct: direction of shift, either 'l' or 'r' 84 | :param int num: number of shifts 85 | :return: Register containing reg's (shifted) next state 86 | """ 87 | if direct == 'l': 88 | if num >= len(reg): 89 | return 0 90 | else: 91 | return pyrtl.concat(reg, pyrtl.Const(0, num)) 92 | elif direct == 'r': 93 | if num >= len(reg): 94 | return 0 95 | else: 96 | return reg[num:] 97 | else: 98 | raise pyrtl.PyrtlError("direction must be specified with 'direct'" 99 | "parameter as either 'l' or 'r'") 100 | -------------------------------------------------------------------------------- /pyrtl/rtllib/muxes.py: -------------------------------------------------------------------------------- 1 | import pyrtl 2 | 3 | 4 | def prioritized_mux(selects, vals): 5 | """ Returns the value in the first wire for which its select bit is 1 6 | 7 | :param [WireVector] selects: a list of WireVectors signaling whether 8 | a wire should be chosen 9 | :param [WireVector] vals: values to return when the corresponding select 10 | value is 1 11 | :return: WireVector 12 | 13 | If none of the `selects` are high, the last `val` is returned 14 | """ 15 | if len(selects) != len(vals): 16 | raise pyrtl.PyrtlError("Number of select and val signals must match") 17 | if len(vals) == 0: 18 | raise pyrtl.PyrtlError("Must have a signal to mux") 19 | if len(vals) == 1: 20 | return vals[0] 21 | else: 22 | half = len(vals) // 2 23 | return pyrtl.select(pyrtl.rtl_any(*selects[:half]), 24 | truecase=prioritized_mux(selects[:half], vals[:half]), 25 | falsecase=prioritized_mux(selects[half:], vals[half:])) 26 | 27 | 28 | def _is_equivalent(w1, w2): 29 | if isinstance(w1, pyrtl.Const) & isinstance(w2, pyrtl.Const): 30 | return (w1.val == w2.val) & (w1.bitwidth == w2.bitwidth) 31 | return w1 is w2 32 | 33 | 34 | SparseDefault = "default" 35 | 36 | 37 | def sparse_mux(sel, vals): 38 | """ Mux that avoids instantiating unnecessary mux_2s when possible. 39 | 40 | :param WireVector sel: Select wire, determines what is selected on a given cycle 41 | :param dict[int, WireVector] vals: dictionary of values at mux inputs 42 | :return: WireVector that signifies the change 43 | 44 | This mux supports not having a full specification. Indices that are not 45 | specified are treated as don't-cares 46 | 47 | It also supports a specified default value, SparseDefault 48 | """ 49 | import numbers 50 | 51 | max_val = 2**len(sel) - 1 52 | if SparseDefault in vals: 53 | default_val = vals[SparseDefault] 54 | del vals[SparseDefault] 55 | for i in range(max_val + 1): 56 | if i not in vals: 57 | vals[i] = default_val 58 | 59 | for key in vals.keys(): 60 | if not isinstance(key, numbers.Integral): 61 | raise pyrtl.PyrtlError("value %s nust be either an integer or 'default'" % str(key)) 62 | if key < 0 or key > max_val: 63 | raise pyrtl.PyrtlError("value %s is out of range of the sel wire" % str(key)) 64 | 65 | return _sparse_mux(sel, vals) 66 | 67 | 68 | def _sparse_mux(sel, vals): 69 | """ Mux that avoids instantiating unnecessary mux_2s when possible. 70 | 71 | :param WireVector sel: Select wire, determines what is selected on a given cycle 72 | :param {int: WireVector} vals: dictionary to store the values that are 73 | :return: Wirevector that signifies the change 74 | 75 | This mux supports not having a full specification. indices that are not 76 | specified are treated as Don't Cares 77 | """ 78 | items = list(vals.values()) 79 | if len(vals) <= 1: 80 | if len(vals) == 0: 81 | raise pyrtl.PyrtlError("Needs at least one parameter for val") 82 | return items[0] 83 | 84 | if len(sel) == 1: 85 | try: 86 | false_result = vals[0] 87 | true_result = vals[1] 88 | except KeyError: 89 | raise pyrtl.PyrtlError("Failed to retrieve values for smartmux. " 90 | "The length of sel might be wrong") 91 | else: 92 | half = 2**(len(sel) - 1) 93 | 94 | first_dict = {indx: wire for indx, wire in vals.items() if indx < half} 95 | second_dict = {indx - half: wire for indx, wire in vals.items() if indx >= half} 96 | if not len(first_dict): 97 | return sparse_mux(sel[:-1], second_dict) 98 | if not len(second_dict): 99 | return sparse_mux(sel[:-1], first_dict) 100 | 101 | false_result = sparse_mux(sel[:-1], first_dict) 102 | true_result = sparse_mux(sel[:-1], second_dict) 103 | if _is_equivalent(false_result, true_result): 104 | return true_result 105 | return pyrtl.select(sel[-1], falsecase=false_result, truecase=true_result) 106 | 107 | 108 | class MultiSelector(object): 109 | """ The MultiSelector allows you to specify multiple wire value results 110 | for a single select wire. 111 | 112 | Useful for processors, finite state machines and other places where the 113 | result of many wire values are determined by a common wire signal 114 | (such as a 'state' wire). 115 | 116 | Example:: 117 | 118 | with muxes.MultiSelector(select, res0, res1, res2, ...) as ms: 119 | ms.option(val1, data0, data1, data2, ...) 120 | ms.option(val2, data0_2, data1_2, data2_2, ...) 121 | 122 | This means that when the ``select`` wire equals the ``val1`` wire 123 | the results will have the values in ``data0, data1, data2, ...`` 124 | (all ints are converted to wires) 125 | """ 126 | def __init__(self, signal_wire, *dest_wires): 127 | self._final = False 128 | self.dest_wires = dest_wires 129 | self.signal_wire = signal_wire 130 | self.instructions = [] 131 | self.dest_instrs_info = {dest_w: [] for dest_w in dest_wires} 132 | 133 | def __enter__(self): 134 | """ For compatibility with `with` statements, which is the recommended 135 | method of using a MultiSelector. 136 | """ 137 | return self 138 | 139 | def __exit__(self, exc_type, exc_val, exc_tb): 140 | if exc_type is None: 141 | self.finalize() 142 | else: 143 | print("The MultiSelector was not finalized due to uncaught exception") 144 | 145 | def _check_finalized(self): 146 | if self._final: 147 | raise pyrtl.PyrtlError("Cannot change InstrConnector, already finalized") 148 | 149 | def option(self, select_val, *data_signals): 150 | self._check_finalized() 151 | instr, ib = pyrtl.infer_val_and_bitwidth(select_val, self.signal_wire.bitwidth) 152 | if instr in self.instructions: 153 | raise pyrtl.PyrtlError("instruction %s already exists" % str(select_val)) 154 | self.instructions.append(instr) 155 | self._add_signal(data_signals) 156 | 157 | def default(self, *data_signals): 158 | self._check_finalized() 159 | self.instructions.append(SparseDefault) 160 | self._add_signal(data_signals) 161 | 162 | def _add_signal(self, data_signals): 163 | self._check_finalized() 164 | if len(data_signals) != len(self.dest_wires): 165 | raise pyrtl.PyrtlError("Incorrect number of data_signals for " 166 | "instruction received {} , expected {}" 167 | .format(len(data_signals), len(self.dest_wires))) 168 | 169 | for dw, sig in zip(self.dest_wires, data_signals): 170 | data_signal = pyrtl.as_wires(sig, dw.bitwidth) 171 | self.dest_instrs_info[dw].append(data_signal) 172 | 173 | def finalize(self): 174 | """ Connects the wires. 175 | """ 176 | self._check_finalized() 177 | self._final = True 178 | 179 | for dest_w, values in self.dest_instrs_info.items(): 180 | mux_vals = dict(zip(self.instructions, values)) 181 | dest_w <<= sparse_mux(self.signal_wire, mux_vals) 182 | 183 | 184 | def demux(select): 185 | """ Demultiplexes a wire of arbitrary bitwidth 186 | 187 | :param WireVector select: indicates which wire to set on 188 | :return (WireVector, ...): a tuple of wires corresponding to each demultiplexed wire 189 | """ 190 | if len(select) == 1: 191 | return _demux_2(select) 192 | 193 | wires = demux(select[:-1]) 194 | sel = select[-1] 195 | not_select = ~sel 196 | zero_wires = tuple(not_select & w for w in wires) 197 | one_wires = tuple(sel & w for w in wires) 198 | return zero_wires + one_wires 199 | 200 | 201 | def _demux_2(select): 202 | assert len(select) == 1 203 | return ~select, select 204 | -------------------------------------------------------------------------------- /pyrtl/rtllib/testingutils.py: -------------------------------------------------------------------------------- 1 | import pyrtl 2 | import random 3 | 4 | """ testcase_utils 5 | 6 | This file (intentionally misspelled) is created to store common utility 7 | functions used for the test cases. 8 | 9 | I am documenting this rather well because users have 10 | a good reason to look at it - John Clow 11 | """ 12 | 13 | 14 | def calcuate_max_and_min_bitwidths(max_bitwidth=None, exact_bitwidth=None): 15 | if max_bitwidth is not None: 16 | min_bitwidth = 1 17 | elif exact_bitwidth is not None: 18 | min_bitwidth = max_bitwidth = exact_bitwidth 19 | else: 20 | raise pyrtl.PyrtlError("A max or exact bitwidth must be specified") 21 | return min_bitwidth, max_bitwidth 22 | 23 | 24 | def inverse_power_dist(bitwidth): 25 | # Note that this is not uniformly distributed 26 | return int(2**random.uniform(0, bitwidth) - 1) 27 | 28 | 29 | def uniform_dist(bitwidth): 30 | return random.randrange(2**bitwidth) 31 | 32 | 33 | def make_inputs_and_values(num_wires, max_bitwidth=None, exact_bitwidth=None, 34 | dist=uniform_dist, test_vals=20): 35 | """ Generates multiple input wires and sets of test values for 36 | testing purposes 37 | 38 | :param function dist: function to generate the random values 39 | :return: wires; list of values for the wires 40 | 41 | The list of values is a list of lists. The interior lists represent the 42 | values of a single wire for all of the simulation cycles 43 | 44 | """ 45 | min_bitwidth, max_bitwidth = calcuate_max_and_min_bitwidths(max_bitwidth, exact_bitwidth) 46 | wires, vals = list(zip(*( 47 | an_input_and_vals(random.randrange(min_bitwidth, max_bitwidth + 1), test_vals, 48 | random_dist=dist) for i in range(num_wires)))) 49 | return wires, vals 50 | 51 | 52 | def an_input_and_vals(bitwidth, test_vals=20, name='', 53 | random_dist=uniform_dist): 54 | """ Generates an input wire and a set of test values for 55 | testing purposes 56 | 57 | :param bitwidth: The bitwidth of the value to be generated 58 | :param int test_vals: number of values to generate per wire 59 | :param name: name for the input wire to be generated 60 | :return: tuple of `input_wire`, `test_values` 61 | """ 62 | input_wire = pyrtl.Input(bitwidth, name=name) # Creating a new input wire 63 | test_vals = [random_dist(bitwidth) for i in range(test_vals)] 64 | return input_wire, test_vals 65 | 66 | 67 | # deprecated name 68 | generate_in_wire_and_values = an_input_and_vals 69 | 70 | 71 | def make_consts(num_wires, max_bitwidth=None, exact_bitwidth=None, random_dist=inverse_power_dist): 72 | """ 73 | :return: [Const_wires]; [Const_vals] 74 | """ 75 | min_bitwidth, max_bitwidth = calcuate_max_and_min_bitwidths(max_bitwidth, exact_bitwidth) 76 | bitwidths = [random.randrange(min_bitwidth, max_bitwidth + 1) for i in range(num_wires)] 77 | wires = [pyrtl.Const(random_dist(b), b) for b in bitwidths] 78 | vals = [w.val for w in wires] 79 | return wires, vals 80 | 81 | 82 | def sim_and_ret_out(outwire, inwires, invals): 83 | """ Simulates the net using `inwires` and `invals`, and returns the output array. 84 | Used for rapid test development. 85 | 86 | :param outwire: The wire to return the output of 87 | :param inwires: a list of wires to read in from (`[Input, ...]`) 88 | :param invals: a list of input value lists (`[ [int, ...], ...]`) 89 | :return: a list of values from the output wire simulation result 90 | """ 91 | # Pulling the value of outwire straight from the log 92 | return sim_and_ret_outws(inwires, invals)[outwire.name] 93 | 94 | 95 | def sim_and_ret_outws(inwires, invals): 96 | """ Simulates the net using `inwires` and `invals`, and returns the output array. 97 | Used for rapid test development. 98 | 99 | :param inwires: a list of wires to read in from (`[Input, ...]`) 100 | :param invals: a list of input value lists (`[[int, ...], ...]`) 101 | :return: a list of values from the output wire simulation result 102 | """ 103 | sim_trace = pyrtl.SimulationTrace() # Creating a logger for the simulator 104 | sim = pyrtl.Simulation(tracer=sim_trace) # Creating the simulation 105 | for cycle in range(len(invals[0])): 106 | sim.step({wire.name: val[cycle] for wire, val in zip(inwires, invals)}) 107 | 108 | return sim_trace.trace # Pulling the value of wires straight from the trace 109 | 110 | 111 | def sim_multicycle(in_dict, hold_dict, hold_cycles, sim=None): 112 | # TODO: write param and return descriptions 113 | """ Simulation of a circuit that takes multiple cycles to complete. 114 | 115 | :param in_dict: 116 | :param hold_dict: 117 | :param hold_cycles: 118 | :param sim: 119 | :return: 120 | """ 121 | if sim is None: 122 | sim = pyrtl.Simulation(tracer=pyrtl.SimulationTrace()) 123 | sim.step(in_dict) 124 | for i in range(hold_cycles): 125 | sim.step(hold_dict) 126 | return sim.tracer.trace[-1] 127 | 128 | 129 | def multi_sim_multicycle(in_dict, hold_dict, hold_cycles, sim=None): 130 | # TODO: write param and return descriptions 131 | """ Simulates a circuit that takes multiple cycles to complete multiple times. 132 | 133 | :param in_dict: `{in_wire: [in_values, ...], ...}` 134 | :param hold_dict: `{hold_wire: hold_value}` The hold values for the 135 | :param hold_cycles: 136 | :param sim: 137 | :return: 138 | """ 139 | if sim is None: 140 | sim = pyrtl.Simulation(tracer=pyrtl.SimulationTrace()) 141 | cycles = len(list(in_dict.values())[0]) 142 | for cycle in range(cycles): 143 | current_dict = {wire: values[cycle] for wire, values in in_dict} 144 | cur_result = sim_multicycle(current_dict, hold_dict, hold_cycles, sim) 145 | if cycle == 0: 146 | results = {wire: [result_val] for wire, result_val in cur_result} 147 | else: 148 | for wire, result_val in cur_result: 149 | results[wire].append(result_val) 150 | return results 151 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pycodestyle 2 | pylint 3 | pyparsing 4 | pytest 5 | pytest-cov 6 | pytest-xdist 7 | tox 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSBarchlab/PyRTL/d5f8dbe53f54e61e1d54722449e4894b885243c7/tests/__init__.py -------------------------------------------------------------------------------- /tests/rtllib/test_adders.py: -------------------------------------------------------------------------------- 1 | import random 2 | import unittest 3 | 4 | import pyrtl 5 | import pyrtl.rtllib.testingutils as utils 6 | from pyrtl.rtllib import adders 7 | 8 | 9 | class TestAdders(unittest.TestCase): 10 | 11 | @classmethod 12 | def setUpClass(cls): 13 | random.seed(8492049) 14 | 15 | def setUp(self): 16 | pyrtl.reset_working_block() 17 | 18 | def tearDown(self): 19 | pyrtl.reset_working_block() 20 | 21 | def adder2_t_base_1(self, adder_func): 22 | self.adder_t_base(adder_func, max_bitwidth=34, num_wires=2) 23 | 24 | def adder_t_base(self, adder_func, **kwargs): 25 | wires, vals = utils.make_inputs_and_values(dist=utils.inverse_power_dist, **kwargs) 26 | outwire = pyrtl.Output(name="test") 27 | outwire <<= adder_func(*wires) 28 | 29 | out_vals = utils.sim_and_ret_out(outwire, wires, vals) 30 | true_result = [sum(cycle_vals) for cycle_vals in zip(*vals)] 31 | self.assertEqual(out_vals, true_result) 32 | 33 | def test_kogge_stone_1(self): 34 | self.adder2_t_base_1(adders.kogge_stone) 35 | 36 | def test_ripple_1(self): 37 | self.adder2_t_base_1(adders.ripple_add) 38 | 39 | def test_carrylookahead_1(self): 40 | self.adder2_t_base_1(adders.cla_adder) 41 | 42 | def test_carry_save_1(self): 43 | self.adder_t_base(adders.carrysave_adder, exact_bitwidth=32, num_wires=3) 44 | 45 | def test_fast_group_adder_1(self): 46 | wires, vals = utils.make_inputs_and_values(max_bitwidth=12, num_wires=7, 47 | dist=utils.inverse_power_dist) 48 | outwire = pyrtl.Output(name="test") 49 | outwire <<= adders.fast_group_adder(wires) 50 | 51 | out_vals = utils.sim_and_ret_out(outwire, wires, vals) 52 | true_result = [sum(cycle_vals) for cycle_vals in zip(*vals)] 53 | self.assertEqual(out_vals, true_result) 54 | -------------------------------------------------------------------------------- /tests/rtllib/test_barrel.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import random 3 | 4 | import pyrtl 5 | from pyrtl.rtllib import barrel 6 | 7 | 8 | class TestBarrel(unittest.TestCase): 9 | # @classmethod 10 | # def setUpClass(cls): 11 | # # this is to ensure reproducibility 12 | # random.seed(777906374) 13 | 14 | def setUp(self): 15 | pyrtl.reset_working_block() 16 | self.inp_val = pyrtl.Input(8, 'inp_val') 17 | self.inp_shift = pyrtl.Input(2, 'inp_shift') 18 | self.out_zeros = pyrtl.Output(18, 'out_zeros') 19 | self.out_ones = pyrtl.Output(18, 'out_ones') 20 | 21 | def test_shift_left(self): 22 | random.seed(777906373) 23 | zero = pyrtl.Const(0, 1) 24 | one = pyrtl.Const(1, 1) 25 | self.out_zeros <<= barrel.barrel_shifter(self.inp_val, zero, one, self.inp_shift) 26 | self.out_ones <<= barrel.barrel_shifter(self.inp_val, one, one, self.inp_shift) 27 | sim_trace = pyrtl.SimulationTrace() 28 | sim = pyrtl.Simulation(tracer=sim_trace) 29 | vals = [random.randint(0, 20) for v in range(20)] 30 | shifts = [random.randint(0, 3) for s in range(20)] 31 | for i in range(len(vals)): 32 | sim.step({ 33 | self.inp_val: vals[i], 34 | self.inp_shift: shifts[i] 35 | }) 36 | base_sum = vals[i] * pow(2, shifts[i]) 37 | self.assertEqual(sim.inspect(self.out_zeros), base_sum) 38 | self.assertEqual(sim.inspect(self.out_ones), base_sum + pow(2, shifts[i]) - 1) 39 | 40 | def test_shift_right(self): 41 | random.seed(777906374) 42 | zero = pyrtl.Const(0, 1) 43 | one = pyrtl.Const(1, 1) 44 | self.out_zeros <<= barrel.barrel_shifter(self.inp_val, zero, zero, self.inp_shift) 45 | self.out_ones <<= barrel.barrel_shifter(self.inp_val, one, zero, self.inp_shift) 46 | sim_trace = pyrtl.SimulationTrace() 47 | sim = pyrtl.Simulation(tracer=sim_trace) 48 | vals = [random.randint(0, 20) for v in range(20)] 49 | shifts = [random.randint(0, 3) for s in range(20)] 50 | for i in range(len(vals)): 51 | sim.step({ 52 | self.inp_val: vals[i], 53 | self.inp_shift: shifts[i] 54 | }) 55 | base_sum = int(vals[i] / pow(2, shifts[i])) 56 | self.assertEqual(sim.inspect(self.out_zeros), base_sum, "failed on value %d" % vals[i]) 57 | extra_sum = sum([pow(2, len(self.inp_val) - b - 1) for b in range(shifts[i])]) 58 | self.assertEqual(sim.inspect(self.out_ones), base_sum + extra_sum, 59 | "failed on value %d" % vals[i]) 60 | -------------------------------------------------------------------------------- /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 | 10 | def test_successful_partition(self): 11 | w = pyrtl.WireVector(24) 12 | partitioned_w = libutils.partition_wire(w, 4) 13 | self.assertEqual(len(partitioned_w), 6) 14 | for wire in partitioned_w: 15 | self.assertIsInstance(wire, pyrtl.WireVector) 16 | 17 | def test_failing_partition(self): 18 | w = pyrtl.WireVector(14) 19 | with self.assertRaises(pyrtl.PyrtlError): 20 | partitioned_w = libutils.partition_wire(w, 4) 21 | 22 | def test_partition_sim(self): 23 | pyrtl.reset_working_block() 24 | wires, vals = utils.make_inputs_and_values(exact_bitwidth=32, num_wires=1) 25 | out_wires = [pyrtl.Output(8, 'o' + str(i)) for i in range(4)] 26 | partitioned_w = libutils.partition_wire(wires[0], 8) 27 | for p_wire, o_wire in zip(partitioned_w, out_wires): 28 | o_wire <<= p_wire 29 | 30 | out_vals = utils.sim_and_ret_outws(wires, vals) 31 | partitioned_vals = [[(val >> i) & 0xff for i in (0, 8, 16, 24)] for val in vals[0]] 32 | true_vals = tuple(zip(*partitioned_vals)) 33 | for index, wire in enumerate(out_wires): 34 | self.assertEqual(tuple(out_vals[wire.name]), true_vals[index]) 35 | 36 | 37 | class TestStringConversion(unittest.TestCase): 38 | 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(i * 3, libutils.rev_twos_comp_repr( 80 | libutils.twos_comp_repr(i * 3, 16), 16)) 81 | 82 | def test_low_bw_error(self): 83 | with self.assertRaises(pyrtl.PyrtlError): 84 | libutils.twos_comp_repr(-40, 6) 85 | with self.assertRaises(pyrtl.PyrtlError): 86 | libutils.rev_twos_comp_repr(88, 6) 87 | with self.assertRaises(pyrtl.PyrtlError): 88 | libutils.rev_twos_comp_repr(8, 4) 89 | 90 | def test_twos_comp_sim(self): 91 | self.out <<= self.in1 + self.in2 92 | sim_trace = pyrtl.SimulationTrace() 93 | sim = pyrtl.Simulation(tracer=sim_trace) 94 | for i in range(10): 95 | sim.step({ 96 | 'in1': i, 97 | 'in2': libutils.twos_comp_repr(-2 * i, 8) 98 | }) 99 | self.assertEqual(-i, libutils.rev_twos_comp_repr(sim.inspect('out'), 8)) 100 | -------------------------------------------------------------------------------- /tests/rtllib/test_multipliers.py: -------------------------------------------------------------------------------- 1 | import random 2 | import unittest 3 | 4 | import pyrtl 5 | import pyrtl.rtllib.testingutils as utils 6 | from pyrtl.rtllib import multipliers, adders, libutils 7 | 8 | 9 | class TestSimpleMult(unittest.TestCase): 10 | 11 | def setUp(self): 12 | pyrtl.reset_working_block() 13 | 14 | def test_trivial_case(self): 15 | self.mult_t_base(1, 5) 16 | 17 | def test_trivial_case_2(self): 18 | self.mult_t_base(2, 1) 19 | 20 | def test_trivial_case_3(self): 21 | self.mult_t_base(1, 1) 22 | 23 | def test_simple_mult_1(self): 24 | self.mult_t_base(5, 7) 25 | 26 | def test_simple_mult_2(self): 27 | self.mult_t_base(2, 9) 28 | 29 | def mult_t_base(self, len_a, len_b): 30 | a, b, reset = pyrtl.Input(len_a, "a"), pyrtl.Input(len_b, "b"), pyrtl.Input(1, 'reset') 31 | product, done = pyrtl.Output(name="product"), pyrtl.Output(name="done") 32 | m_prod, m_done = multipliers.simple_mult(a, b, reset) 33 | product <<= m_prod 34 | done <<= m_done 35 | self.assertEqual(len(product), len_a + len_b) 36 | 37 | xvals = [int(random.uniform(0, 2 ** len_a - 1)) for i in range(20)] 38 | yvals = [int(random.uniform(0, 2 ** len_b - 1)) for i in range(20)] 39 | true_result = [i * j for i, j in zip(xvals, yvals)] 40 | mult_results = [] 41 | 42 | for x_val, y_val, true_res in zip(xvals, yvals, true_result): 43 | sim_trace = pyrtl.SimulationTrace() 44 | sim = pyrtl.Simulation(tracer=sim_trace) 45 | sim.step({a: x_val, b: y_val, reset: 1}) 46 | for cycle in range(len(a) + 1): 47 | sim.step({a: 0, b: 0, reset: 0}) 48 | 49 | # Extracting the values and verifying correctness 50 | mult_results.append(sim.inspect("product")) 51 | self.assertEqual(sim.inspect("done"), 1) 52 | self.assertEqual(mult_results, true_result) 53 | 54 | 55 | class TestComplexMult(unittest.TestCase): 56 | 57 | def setUp(self): 58 | pyrtl.reset_working_block() 59 | 60 | def test_trivial_case(self): 61 | with self.assertRaises(pyrtl.PyrtlError): 62 | self.mult_t_base(1, 5, 2) 63 | 64 | def test_trivial_case_2(self): 65 | with self.assertRaises(pyrtl.PyrtlError): 66 | self.mult_t_base(2, 1, 5) 67 | 68 | def test_trivial_case_3(self): 69 | self.mult_t_base(1, 1, 1) 70 | 71 | def test_complex_mult_1(self): 72 | self.mult_t_base(5, 7, 3) 73 | 74 | def test_complex_mult_2(self): 75 | self.mult_t_base(10, 12, 3) 76 | 77 | def test_complex_mult_3(self): 78 | with self.assertRaises(pyrtl.PyrtlError): 79 | self.mult_t_base(2, 9, 4) 80 | 81 | def test_complex_mult_4(self): 82 | with self.assertRaises(pyrtl.PyrtlError): 83 | self.mult_t_base(8, 4, 6) 84 | 85 | def mult_t_base(self, len_a, len_b, shifts): 86 | a, b = pyrtl.Input(len_a, 'a'), pyrtl.Input(len_b, 'b') 87 | reset = pyrtl.Input(1, 'reset') 88 | product, done = pyrtl.Output(name='product'), pyrtl.Output(name='done') 89 | m_prod, m_done = multipliers.complex_mult(a, b, shifts, reset) 90 | product <<= m_prod 91 | done <<= m_done 92 | self.assertEqual(len(product), len_a + len_b) 93 | 94 | xvals = [int(random.uniform(0, 2 ** len_a - 1)) for i in range(20)] 95 | yvals = [int(random.uniform(0, 2 ** len_b - 1)) for i in range(20)] 96 | true_result = [i * j for i, j in zip(xvals, yvals)] 97 | mult_results = [] 98 | 99 | for x_val, y_val, true_res in zip(xvals, yvals, true_result): 100 | sim_trace = pyrtl.SimulationTrace() 101 | sim = pyrtl.Simulation(tracer=sim_trace) 102 | sim.step({a: x_val, b: y_val, reset: 1}) 103 | if shifts <= len_a: 104 | length = len_a // shifts + (1 if len_a % shifts == 0 else 2) 105 | else: 106 | length = len_a + 1 107 | for cycle in range(length): 108 | sim.step({a: 0, b: 0, reset: 0}) 109 | 110 | # Extracting the values and verifying correctness 111 | mult_results.append(sim.inspect('product')) 112 | self.assertEqual(sim.inspect('done'), 1) 113 | self.assertEqual(mult_results, true_result) 114 | 115 | 116 | class TestWallace(unittest.TestCase): 117 | 118 | @classmethod 119 | def setUpClass(cls): 120 | # this is to ensure reproducibility 121 | random.seed(777906376) 122 | 123 | def setUp(self): 124 | pyrtl.reset_working_block() 125 | 126 | def mult_t_base(self, len_a, len_b, **mult_args): 127 | # Creating the logic nets 128 | a, b = pyrtl.Input(len_a, "a"), pyrtl.Input(len_b, "b") 129 | product = pyrtl.Output(name="product") 130 | product <<= multipliers.tree_multiplier(a, b, **mult_args) 131 | 132 | self.assertEqual(len(product), len_a + len_b) 133 | 134 | # creating the testing values and the correct results 135 | xvals = [int(random.uniform(0, 2 ** len_a - 1)) for i in range(20)] 136 | yvals = [int(random.uniform(0, 2 ** len_b - 1)) for i in range(20)] 137 | true_result = [i * j for i, j in zip(xvals, yvals)] 138 | 139 | # Setting up and running the tests 140 | sim_trace = pyrtl.SimulationTrace() 141 | sim = pyrtl.Simulation(tracer=sim_trace) 142 | for cycle in range(len(xvals)): 143 | sim.step({a: xvals[cycle], b: yvals[cycle]}) 144 | 145 | # Extracting the values and verifying correctness 146 | multiplier_result = sim_trace.trace[product.name] 147 | self.assertEqual(multiplier_result, true_result) 148 | 149 | def test_trivial_case(self): 150 | self.mult_t_base(1, 5) 151 | 152 | def test_trivial_case_2(self): 153 | self.mult_t_base(2, 1) 154 | 155 | def test_trivial_case_3(self): 156 | self.mult_t_base(1, 1) 157 | 158 | def test_wallace_tree_1(self): 159 | self.mult_t_base(5, 7) 160 | 161 | def test_wallace_tree_2(self): 162 | self.mult_t_base(2, 9) 163 | 164 | def test_dada_tree(self): 165 | self.mult_t_base(5, 10, reducer=adders.dada_reducer) 166 | 167 | def test_fma_1(self): 168 | wires, vals = utils.make_inputs_and_values(exact_bitwidth=10, num_wires=3, 169 | dist=utils.inverse_power_dist) 170 | test_w = multipliers.fused_multiply_adder(wires[0], wires[1], wires[2], False, 171 | reducer=adders.dada_reducer, 172 | adder_func=adders.ripple_add) 173 | self.assertEqual(len(test_w), 20) 174 | outwire = pyrtl.Output(21, "test") 175 | outwire <<= test_w 176 | 177 | out_vals = utils.sim_and_ret_out(outwire, wires, vals) 178 | true_result = [vals[0][cycle] * vals[1][cycle] + vals[2][cycle] 179 | for cycle in range(len(vals[0]))] 180 | self.assertEqual(out_vals, true_result) 181 | 182 | def test_gen_fma_1(self): 183 | wires, vals = utils.make_inputs_and_values(max_bitwidth=8, num_wires=8, 184 | dist=utils.inverse_power_dist) 185 | # mixing tuples and lists solely for readability purposes 186 | mult_pairs = [(wires[0], wires[1]), (wires[2], wires[3]), (wires[4], wires[5])] 187 | add_wires = (wires[6], wires[7]) 188 | 189 | outwire = pyrtl.Output(name="test") 190 | outwire <<= multipliers.generalized_fma(mult_pairs, add_wires, signed=False) 191 | 192 | out_vals = utils.sim_and_ret_out(outwire, wires, vals) 193 | true_result = [vals[0][cycle] * vals[1][cycle] + vals[2][cycle] * vals[3][cycle] 194 | + vals[4][cycle] * vals[5][cycle] + vals[6][cycle] + vals[7][cycle] 195 | for cycle in range(len(vals[0]))] 196 | self.assertEqual(out_vals, true_result) 197 | 198 | 199 | class TestSignedTreeMult(unittest.TestCase): 200 | @classmethod 201 | def setUpClass(cls): 202 | # this is to ensure reproducibility 203 | random.seed(777906375) 204 | 205 | def setUp(self): 206 | pyrtl.reset_working_block() 207 | 208 | def mult_t_base(self, len_a, len_b, **mult_args): 209 | # Creating the logic nets 210 | a, b = pyrtl.Input(len_a, "a"), pyrtl.Input(len_b, "b") 211 | product = pyrtl.Output(name="product") 212 | product <<= multipliers.signed_tree_multiplier(a, b, **mult_args) 213 | 214 | self.assertEqual(len(product), len_a + len_b) 215 | 216 | # creating the testing values and the correct results 217 | bound_a = 2 ** (len_a - 1) - 1 218 | bound_b = 2 ** (len_b - 1) - 1 219 | xvals = [int(random.uniform(-bound_a, bound_a)) for i in range(20)] 220 | yvals = [int(random.uniform(-bound_b, bound_b)) for i in range(20)] 221 | true_result = [i * j for i, j in zip(xvals, yvals)] 222 | 223 | # Setting up and running the tests 224 | sim_trace = pyrtl.SimulationTrace() 225 | sim = pyrtl.Simulation(tracer=sim_trace) 226 | for cycle in range(len(xvals)): 227 | sim.step({ 228 | a: libutils.twos_comp_repr(xvals[cycle], len_a), 229 | b: libutils.twos_comp_repr(yvals[cycle], len_b) 230 | }) 231 | 232 | # Extracting the values and verifying correctness 233 | multiplier_result = [libutils.rev_twos_comp_repr(p, len(product)) 234 | for p in sim_trace.trace[product.name]] 235 | self.assertEqual(multiplier_result, true_result) 236 | 237 | def test_small_bitwidth_error(self): 238 | with self.assertRaises(pyrtl.PyrtlError): 239 | self.mult_t_base(1, 1) 240 | 241 | def test_trivial_case(self): 242 | self.mult_t_base(2, 3) 243 | 244 | def test_trivial_case_2(self): 245 | self.mult_t_base(4, 4) 246 | 247 | def test_trivial_case_3(self): 248 | self.mult_t_base(3, 4) 249 | 250 | def test_wallace_tree_1(self): 251 | self.mult_t_base(10, 3) 252 | 253 | def test_wallace_tree_2(self): 254 | self.mult_t_base(8, 8) 255 | 256 | def test_dada_tree(self): 257 | self.mult_t_base(5, 10, reducer=adders.dada_reducer) 258 | -------------------------------------------------------------------------------- /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 | 11 | def setUp(self): 12 | pyrtl.reset_working_block() 13 | 14 | @staticmethod 15 | def fibonacci_lfsr(seed): 16 | lfsr = seed 17 | while True: 18 | bit = (lfsr >> 126 ^ lfsr >> 125) & 1 19 | lfsr = lfsr << 1 | bit 20 | yield bit 21 | 22 | @staticmethod 23 | def xoroshiro128plus(seed): 24 | mask = 2**64 - 1 25 | s = [seed & mask, seed >> 64] 26 | while True: 27 | s0 = s[0] 28 | s1 = s[1] 29 | word = (s1 + s0) & mask 30 | s1 ^= s0 31 | s[0] = (s0 << 55 | s0 >> 9) ^ s1 ^ s1 << 14 32 | s[1] = s1 << 36 | s1 >> 28 33 | yield word 34 | 35 | def test_prng_lfsr(self): 36 | seed = pyrtl.Input(127, 'seed') 37 | load, req = pyrtl.Input(1, 'load'), pyrtl.Input(1, 'req') 38 | rand = pyrtl.Output(128, 'rand') 39 | rand <<= prngs.prng_lfsr(128, load, req, seed) 40 | sim_trace = pyrtl.SimulationTrace() 41 | sim = pyrtl.Simulation(tracer=sim_trace) 42 | in_vals = [random.randrange(1, 2**127) for i in range(5)] 43 | 44 | for trial in range(5): 45 | true_val = 0 46 | for bit in islice(TestPrngs.fibonacci_lfsr(in_vals[trial]), 128): 47 | true_val = true_val << 1 | bit 48 | sim.step({'load': 1, 'req': 0, 'seed': in_vals[trial]}) 49 | sim.step({'load': 0, 'req': 1, 'seed': 0x0}) 50 | sim.step({'load': 0, 'req': 0, 'seed': 0x0}) 51 | circuit_out = sim.inspect(rand) 52 | self.assertEqual(circuit_out, true_val, 53 | "\nAssertion failed on trial {}\nExpected value: {}\nGotten value: {}" 54 | .format(trial, hex(true_val), hex(circuit_out))) 55 | 56 | def test_prng_xoroshiro128(self): 57 | seed = pyrtl.Input(128, 'seed') 58 | load, req = pyrtl.Input(1, 'load'), pyrtl.Input(1, 'req') 59 | ready = pyrtl.Output(1, 'ready') 60 | rand = pyrtl.Output(128, 'rand') 61 | ready_out, rand_out = prngs.prng_xoroshiro128(128, load, req, seed) 62 | ready <<= ready_out 63 | rand <<= rand_out 64 | sim_trace = pyrtl.SimulationTrace() 65 | sim = pyrtl.Simulation(tracer=sim_trace) 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(circuit_out, true_val, 78 | "\nAssertion failed on trial {}\nExpected value: {}\nGotten value: {}" 79 | .format(trial, hex(true_val), hex(circuit_out))) 80 | 81 | for ready_signal in sim_trace.trace['ready'][:3]: 82 | self.assertEqual(ready_signal, 0) 83 | self.assertEqual(sim_trace.trace['ready'][3], 1) 84 | self.assertEqual(sim_trace.trace['ready'][4], 0) 85 | 86 | def test_csprng_trivium(self): 87 | """ 88 | Trivium test vectors retrived from: 89 | https://www.sheffield.ac.uk/polopoly_fs/1.12164!/file/eSCARGOt_full_datasheet_v1.3.pdf 90 | bit ordering is modified to adapt to the Pyrtl implementation 91 | """ 92 | in_vector = pyrtl.Input(160, 'in_vector') 93 | load, req = pyrtl.Input(1, 'load'), pyrtl.Input(1, 'req') 94 | ready = pyrtl.Output(1, 'ready') 95 | out_vector = pyrtl.Output(128, 'out_vector') 96 | ready_out, rand_out = prngs.csprng_trivium(128, load, req, in_vector) 97 | ready <<= ready_out 98 | out_vector <<= rand_out 99 | sim_trace = pyrtl.SimulationTrace() 100 | sim = pyrtl.Simulation(tracer=sim_trace) 101 | 102 | in_vals = [0x0100000000000000000000000000000000000000, 103 | 0x0a09080706050403020100000000000000000000, 104 | 0xfffefdfcfbfaf9f8f7f600000000000000000000, 105 | 0xfaa75401ae5b08b5620fc760f9922bc45df68f28, 106 | 0xf5a24ffca95603b05d0abe57f08922bb54ed861f, ] 107 | 108 | true_vals = [0x1cd761ffceb05e39f5b18f5c22042ab0, 109 | 0x372e6b86524afa71b5fee86d5cebb07d, 110 | 0xc100baca274287277ff49b9fb512af1c, 111 | 0xcb5996fcff373a953fc169e899e02f46, 112 | 0xf142d1df4b36c7652cba2e4a22ee51a0, ] 113 | 114 | for trial in range(5): 115 | sim.step({'load': 1, 'req': 0, 'in_vector': in_vals[trial]}) 116 | for cycle in range(1, 20): 117 | sim.step({'load': 0, 'req': 0, 'in_vector': 0x0}) 118 | sim.step({'load': 0, 'req': 1, 'in_vector': 0x0}) 119 | for cycle in range(21, 23): 120 | sim.step({'load': 0, 'req': 0, 'in_vector': 0x0}) 121 | circuit_out = sim.inspect(out_vector) 122 | self.assertEqual(circuit_out, true_vals[trial], 123 | "\nAssertion failed on trial {}\nExpected value: {}\nGotten value: {}" 124 | .format(trial, hex(true_vals[trial]), hex(circuit_out))) 125 | 126 | for ready_signal in sim_trace.trace['ready'][:19]: 127 | self.assertEqual(ready_signal, 0) 128 | self.assertEqual(sim_trace.trace['ready'][19], 1) 129 | 130 | for ready_signal in sim_trace.trace['ready'][20:22]: 131 | self.assertEqual(ready_signal, 0) 132 | self.assertEqual(sim_trace.trace['ready'][22], 1) 133 | self.assertEqual(sim_trace.trace['ready'][23], 0) 134 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import subprocess 4 | import pyrtl 5 | import pytest 6 | 7 | 8 | """ 9 | Tests all of the files in the example folder 10 | 11 | Note that this file is structure dependent, so don't forget to change it if the 12 | relative location of the examples changes 13 | """ 14 | 15 | 16 | @pytest.mark.parametrize( 17 | 'file', 18 | glob.iglob(os.path.dirname(__file__) + "/../examples/*.py")) 19 | def test_all_examples(file): 20 | pyrtl.reset_working_block() 21 | try: 22 | output = subprocess.check_output(['python', file]) 23 | except subprocess.CalledProcessError as e: 24 | raise e 25 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | # When changing the following line, be sure to update 'python-version' in 3 | # .github/workflows/python-test.yml 4 | envlist = py{3.9,3.13}-{test}, pycodestyle 5 | 6 | [gh-actions] 7 | python = 8 | 3.9: py3.9 9 | 3.13: py3.13, pycodestyle 10 | 11 | [testenv] 12 | deps = test: -rrequirements.txt 13 | pycodestyle: pycodestyle 14 | 15 | envdir = 16 | py3.9: {toxworkdir}/3.9 17 | py3.13: {toxworkdir}/3.13 18 | pycodestyle: {toxworkdir}/pycodestyle 19 | 20 | setenv = 21 | PYTHONPATH = {toxinidir} 22 | 23 | commands = 24 | test: pytest --cov=pyrtl --cov-report=xml -n auto {posargs} 25 | test: pylint -E pyrtl/ 26 | pycodestyle: pycodestyle --max-line-length=100 --ignore=W503 pyrtl/ 27 | pycodestyle: pycodestyle --max-line-length=100 --ignore=W503 examples/ 28 | pycodestyle: pycodestyle --max-line-length=100 --ignore=W503 tests/ 29 | --------------------------------------------------------------------------------