diff --git a/.github/workflows/pages-deploy.yml b/.github/workflows/pages-deploy.yml new file mode 100644 index 00000000..dd89a169 --- /dev/null +++ b/.github/workflows/pages-deploy.yml @@ -0,0 +1,36 @@ +name: Deploy to GitHub Pages + +on: + push: + paths: + - www/** + - docs/images/** + - docs/screenshots/** + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: astral-sh/setup-uv@v6 + - name: Generate pages with Sphinx + run: uv run just www + - name: Upload static files as artifact + id: deployment + uses: actions/upload-pages-artifact@v3 + with: + path: www/_build/html + + deploy: + needs: build + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index 58a97bb2..04c87c97 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: astral-sh/setup-uv@v6 with: enable-cache: true diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index e1a0cad7..82a41c39 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: astral-sh/setup-uv@v6 with: enable-cache: true diff --git a/README.md b/README.md index e6e3797f..e274763f 100644 --- a/README.md +++ b/README.md @@ -13,25 +13,25 @@ PyRTL PyRTL provides a collection of classes for Pythonic [register-transfer level](https://en.wikipedia.org/wiki/Register-transfer_level) design, simulation, tracing, and testing suitable for teaching and research. -Simplicity, usability, clarity, and extensibility rather than performance or -optimization is the overarching goal. Features include: +Simplicity, usability, clarity, and extensibility are overarching goals, rather +than performance or optimization. Features include: * Elaboration-through-execution, meaning all of Python can be used including - introspection -* Design, instantiate, and simulate all in one file and without leaving Python + introspection. +* Design, instantiate, and simulate all in one file and without leaving Python. * Export to, or import from, common HDLs (BLIF-in, Verilog-out currently - supported) -* Examine execution with waveforms on the terminal or export to `.vcd` as - projects scale -* Elaboration, synthesis, and basic optimizations all included + supported). +* Examine execution with waveforms on the terminal or export to + [`.vcd`](https://en.wikipedia.org/wiki/Value_change_dump) as projects scale. +* Elaboration, synthesis, and basic optimizations all included. * Small and well-defined internal core structure means writing new transforms - is easier -* Batteries included means many useful components are already available and - more are coming every week + is easier. +* Batteries included means many useful components are already available. What README would be complete without a screenshot? Below you can see the -waveform rendered right on the terminal for a small state machine written in -PyRTL. +waveform rendered right on the terminal for a [small state +machine](https://github.com/UCSBarchlab/PyRTL/blob/development/examples/example3-statemachine.py) +written in PyRTL. ![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") @@ -43,44 +43,46 @@ PyRTL. [`examples`](https://github.com/UCSBarchlab/PyRTL/tree/development/examples) directory. You can also [try the examples on MyBinder](https://mybinder.org/v2/gh/UCSBarchlab/PyRTL/development?filepath=%2Fipynb-examples%2F). -* Full reference documentation is available at https://pyrtl.readthedocs.io/ +* [Full reference documentation](https://pyrtl.readthedocs.io/) is available. ### Package Contents -If you are just getting started with PyRTL it is suggested that you start with -the -[`examples`](https://github.com/UCSBarchlab/PyRTL/tree/development/examples) -first to get a sense of the "thinking with PyRTLs" required to design hardware -in this way. If you are looking for a deeper understanding, dive into the code -for the object `Block`. It is the core data structure at the heart of PyRTL and -defines its semantics at a high level -- everything is converted to or from the -small, simple set of primitives defined there. +If you are just getting started with PyRTL, try starting with the +[`examples`](https://github.com/UCSBarchlab/PyRTL/tree/development/examples) to +get a sense of the "thinking with PyRTLs" required to design hardware in this +way. If you are looking for a deeper understanding, dive into the code for the +object [`Block`](https://pyrtl.readthedocs.io/en/latest/blocks.html#blocks). It +is the core data structure at the heart of PyRTL and defines its semantics at a +high level -- everything is converted to or from the small, simple set of +primitives defined there. The package contains the following files and directories: * [`pyrtl`](https://github.com/UCSBarchlab/PyRTL/tree/development/pyrtl) - The src directory for the module. + The module's source code. * [`pyrtl/rtllib/`](https://github.com/UCSBarchlab/PyRTL/tree/development/pyrtl/rtllib) Finished PyRTL libraries which are hopefully both useful and documented. * [`examples`](https://github.com/UCSBarchlab/PyRTL/tree/development/examples) - A set of hardware design examples that show the main idea behind PyRTL. + A set of hardware design examples that show the main ideas behind PyRTL. * [`tests`](https://github.com/UCSBarchlab/PyRTL/tree/development/tests) A set of unit tests for PyRTL which you can run with `pytest`. * [`docs`](https://github.com/UCSBarchlab/PyRTL/tree/development/docs) - Location of the Sphinx documentation. + Documentation written in + [Sphinx](https://www.sphinx-doc.org/en/master/index.html) + [reStructuredText](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#rst-primer). -### The PyRTL Development Environment +### PyRTL Development Environment -All PyRTL developers should use the exact same tools to avoid confusing +All PyRTL developers should use the same tool versions to avoid confusing situations where a test fails only on one person's computer, or the generated documentation looks weird on another person's computer. -PyRTL uses [`uv`](https://docs.astral.sh/uv/) to ensure everyone's development -environments are consistent. `uv` manages the installation and versioning for -all other PyRTL developer tools, like `pytest` and `ruff`. +PyRTL uses [`uv`](https://docs.astral.sh/uv/) to ensure all developers work in +the same environment. `uv` manages the installation and versioning for all +other PyRTL developer tools, like `pytest` and `ruff`. -So to set up a PyRTL development environment, you only need to install `uv`, by -following the -[`uv` installation instructions](https://docs.astral.sh/uv/getting-started/installation/) +To set up a PyRTL development environment, you only need to install `uv`, by +following the [`uv` installation +instructions](https://docs.astral.sh/uv/getting-started/installation/). After installing [`uv`](https://docs.astral.sh/uv/), you can run all the tests with: @@ -108,77 +110,78 @@ needed. `uv` caches installed software so future `uv` invocations will be fast. example, pick a `PyrtlError` that is not covered and add a unit test in [`tests`](https://github.com/UCSBarchlab/PyRTL/tree/development/tests) that will hit it. -* After you have that down check in the [PyRTL - Issues](https://github.com/UCSBarchlab/PyRTL/issues) list for a feature that - is marked as "beginner friendly". +* After you have that down check [PyRTL + Issues](https://github.com/UCSBarchlab/PyRTL/issues) for a feature that is + marked as "beginner friendly". * Once you have that down, ask for access to the PyRTL-research repo where we - keep a list of more advanced features and designs that could use more help! + keep experimental features and designs that could use more help! *Coding style* * All major functionality should have unit tests covering and documenting their - use -* All public functions and methods should have useful docstrings + use. +* All public functions and methods should have useful docstrings. * All code needs to conform to - [PEP8](https://www.python.org/dev/peps/pep-0008/) conventions + [PEP8](https://www.python.org/dev/peps/pep-0008/) conventions. * No new root-level dependencies on external libs, import locally if required - for special functions + for special functions. *Workflow* -* A useful reference for working with Git is this [Git - tutorial](https://www.atlassian.com/git/tutorials/) -* A useful Git Fork workflow for working on this repo is [found - here](http://blog.scottlowe.org/2015/01/27/using-fork-branch-git-workflow/) +* [This Git tutorial](https://www.atlassian.com/git/tutorials/) is a useful + reference for working with Git. +* [This blog + post](http://blog.scottlowe.org/2015/01/27/using-fork-branch-git-workflow/) + covers a useful Git Fork workflow for working on PyRTL. * The `development` branch is the primary stable working branch (everyone is - invited to submit pull requests) -* Bugs and minor enhancements tracked directly through the [issue - tracker](https://github.com/UCSBarchlab/PyRTL/issues) -* When posting a bug please post a small chunk of code that captures the bug, - e.g. [Issue #56](https://github.com/UCSBarchlab/PyRTL/issues/56) -* When pushing a fix to a bug or enhancement please reference issue number in + invited to submit pull requests)., +* Bugs and minor enhancements tracked directly through [GitHub + Issues](https://github.com/UCSBarchlab/PyRTL/issues). +* When posting a bug please include a small code sample that triggers the bug, + e.g. [Issue #56](https://github.com/UCSBarchlab/PyRTL/issues/56). +* When pushing a fix to a bug or enhancement please reference the Issue in the commit message, e.g. [Fix to Issue - #56](https://github.com/UCSBarchlab/PyRTL/commit/1d5730db168a9e4490c580cb930075715468047a) + #56](https://github.com/UCSBarchlab/PyRTL/commit/1d5730db168a9e4490c580cb930075715468047a). * Before sending a pull request, please run: ```shell $ uv run just presubmit ``` - to verify that all tests pass and that documentation can be generated with - your changes. + to verify that all tests pass and that all documentation can be generated + with your changes. *Documentation* * All important functionality should have an executable example in - [`examples`](https://github.com/UCSBarchlab/PyRTL/tree/development/examples) -* All classes should have a block comment with high level description of the - class -* All functions should follow the following (Sphinx parsable) docstring format: + [`examples`](https://github.com/UCSBarchlab/PyRTL/tree/development/examples). + +* All classes should have a [docstring](https://peps.python.org/pep-0257) that + describes the class at a high level. + +* All methods and functions should include a docstring in the following + Sphinx-parsable format: ```python - """One Line Summary (< 80 chars) of the function, followed by period. + """One Line Summary (< 88 chars) of the function, followed by period. - A long description of what this function does. Talk about what the user - should expect from this function and also what the users needs to do to use - the function (this part is optional). + A longer description of what this function does and does not do. Describe any + assumptions or invariants. Provide an example for user-facing functions. - :param param_name : Description of this parameter. - :param param_name : Longer parameter descriptions take up a newline with four + :param param1: Description of this parameter. + :param param2: Longer parameter descriptions take up a newline with four leading spaces like this. + + :raises Exception: If this function raises an exception, explain when that + occurs here. + :return: Description of function's return value. """ - # Developer Notes (Optional): - # - # These would be anything that the user does not need to know in order to use - # the functions. - # These notes can include internal workings of the function, the logic behind - # it, or how to extend it. ``` * Sphinx parses [Python type annotations](https://docs.python.org/3/library/typing.html), so put type information into annotations instead of docstrings. * The Sphinx-generated documentation is published to - https://pyrtl.readthedocs.io/ + https://pyrtl.readthedocs.io/ . * PyRTL's Sphinx build process is documented in [`docs/README.md`](https://github.com/UCSBarchlab/PyRTL/blob/development/docs/README.md). * PyRTL's release process is documented in diff --git a/docs/README.md b/docs/README.md index 52f03c75..8b674342 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,11 +24,9 @@ Follow the instructions on this page to build a local copy of PyRTL's documentation. This is useful for verifying that PyRTL's documentation still renders correctly after making a local change. -There is additional PyRTL documentation in the [`gh-pages` -branch](https://github.com/UCSBarchlab/PyRTL/tree/gh-pages). This additional -documentation is pushed to https://ucsbarchlab.github.io/PyRTL/ by the -`pages-build-deployment` GitHub Action. This additional documentation is -written HTML and is not described further in this README. +There is additional PyRTL documentation in [GitHub +Pages](https://ucsbarchlab.github.io/PyRTL/), see +[`www/README.md`](https://github.com/UCSBarchlab/PyRTL/blob/development/www/README.md). ## Testing Documentation Examples diff --git a/docs/conf.py b/docs/conf.py index c9d4bc56..fab1848c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,8 +23,6 @@ # -- Project information ----------------------------------------------------- project = "PyRTL" -copyright = "2026, Timothy Sherwood" -author = "Timothy Sherwood" # -- General configuration --------------------------------------------------- @@ -96,6 +94,9 @@ ], } html_logo = "brand/pyrtl_logo.png" +html_show_sphinx = False +html_show_copyright = False +html_show_sourcelink = False # Force a light blue background color for inheritance-diagrams. The default is # transparent, which does not work well with Furo's dark mode. diff --git a/docs/images/gcd-graph.png b/docs/images/gcd-graph.png deleted file mode 100644 index f8985ff9..00000000 Binary files a/docs/images/gcd-graph.png and /dev/null differ diff --git a/docs/screenshots/adder.png b/docs/screenshots/adder.png new file mode 100644 index 00000000..1bfa0236 Binary files /dev/null and b/docs/screenshots/adder.png differ diff --git a/docs/screenshots/fir.png b/docs/screenshots/fir.png new file mode 100644 index 00000000..ef766649 Binary files /dev/null and b/docs/screenshots/fir.png differ diff --git a/docs/screenshots/gcd.png b/docs/screenshots/gcd.png new file mode 100644 index 00000000..b2b697c7 Binary files /dev/null and b/docs/screenshots/gcd.png differ diff --git a/docs/screenshots/index-demo.png b/docs/screenshots/index-demo.png index 4f6ccc97..9b7e3e8a 100644 Binary files a/docs/screenshots/index-demo.png and b/docs/screenshots/index-demo.png differ diff --git a/docs/screenshots/maxn.png b/docs/screenshots/maxn.png new file mode 100644 index 00000000..76d529ef Binary files /dev/null and b/docs/screenshots/maxn.png differ diff --git a/docs/screenshots/mul.png b/docs/screenshots/mul.png new file mode 100644 index 00000000..45ed64f8 Binary files /dev/null and b/docs/screenshots/mul.png differ diff --git a/docs/screenshots/pyrtl-counter.png b/docs/screenshots/pyrtl-counter.png index c4737e0c..11e4339b 100644 Binary files a/docs/screenshots/pyrtl-counter.png and b/docs/screenshots/pyrtl-counter.png differ diff --git a/docs/screenshots/pyrtl-statemachine.png b/docs/screenshots/pyrtl-statemachine.png index a72bc502..2247f582 100644 Binary files a/docs/screenshots/pyrtl-statemachine.png and b/docs/screenshots/pyrtl-statemachine.png differ diff --git a/docs/screenshots/render_trace.png b/docs/screenshots/render_trace.png index 347734fa..979f7b52 100644 Binary files a/docs/screenshots/render_trace.png and b/docs/screenshots/render_trace.png differ diff --git a/justfile b/justfile index 406833f3..f290fce6 100644 --- a/justfile +++ b/justfile @@ -1,7 +1,7 @@ # PyRTL uses `just` instead of `make` because: # * `make` is not installed by default on Windows. # * `uv` can install `just` on all supported platforms from PyPI. -presubmit: tests docs +presubmit: tests docs www tests: # Run `pytest` with the latest version of Python supported by PyRTL, @@ -36,3 +36,9 @@ docs: # # Output: docs/_build/html/index.html uv run sphinx-build -M html docs/ docs/_build + +www: + # Run `sphinx-build` to generate github.io webpage. + # + # Output: www/_build/html/index.html + uv run sphinx-build -M html www/ www/_build diff --git a/pyproject.toml b/pyproject.toml index c42d638a..8127d23c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,4 +127,5 @@ dev = [ "sphinx>=7.4.7", "sphinx-autodoc-typehints>=2.3.0", "sphinx-copybutton>=0.5.2", + "sphinx-design>=0.6.1", ] diff --git a/tests/test_examples.py b/tests/test_examples.py index 0a741ed0..e786e84d 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1,4 +1,5 @@ import glob +import itertools import os import subprocess @@ -6,20 +7,21 @@ import pyrtl -""" -Tests all of the files in the example folder - -Note that this file is structure dependent, so don't forget to change it if the relative -location of the examples changes -""" - @pytest.mark.parametrize( - "file", glob.iglob(os.path.dirname(__file__) + "/../examples/*.py") + "file", + itertools.chain( + glob.iglob(os.path.dirname(__file__) + "/../examples/*.py"), + glob.iglob(os.path.dirname(__file__) + "/../www/*.py"), + ), ) def test_all_examples(file): + """Test all scripts in ``examples/`` and ``www/``. + + This just checks that all scripts terminate with exit status 0. + """ pyrtl.reset_working_block() try: subprocess.check_output(["python", file]) - except subprocess.CalledProcessError as e: - raise e + except subprocess.CalledProcessError: + pytest.fail(f"Failed to execute {file}") diff --git a/uv.lock b/uv.lock index 2dfe81b7..cac79b06 100644 --- a/uv.lock +++ b/uv.lock @@ -321,7 +321,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -553,6 +553,8 @@ dev = [ { name = "sphinx-autodoc-typehints", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx-autodoc-typehints", version = "3.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-copybutton" }, + { name = "sphinx-design", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx-design", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] [package.metadata] @@ -575,6 +577,7 @@ dev = [ { name = "sphinx", specifier = ">=7.4.7" }, { name = "sphinx-autodoc-typehints", specifier = ">=2.3.0" }, { name = "sphinx-copybutton", specifier = ">=0.5.2" }, + { name = "sphinx-design", specifier = ">=0.6.1" }, ] [[package]] @@ -879,6 +882,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" }, ] +[[package]] +name = "sphinx-design" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/69/b34e0cb5336f09c6866d53b4a19d76c227cdec1bbc7ac4de63ca7d58c9c7/sphinx_design-0.6.1.tar.gz", hash = "sha256:b44eea3719386d04d765c1a8257caca2b3e6f8421d7b3a5e742c0fd45f84e632", size = 2193689, upload-time = "2024-08-02T13:48:44.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/43/65c0acbd8cc6f50195a3a1fc195c404988b15c67090e73c7a41a9f57d6bd/sphinx_design-0.6.1-py3-none-any.whl", hash = "sha256:b11f37db1a802a183d61b159d9a202314d4d2fe29c163437001324fe2f19549c", size = 2215338, upload-time = "2024-08-02T13:48:42.106Z" }, +] + +[[package]] +name = "sphinx-design" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/7b/804f311da4663a4aecc6cf7abd83443f3d4ded970826d0c958edc77d4527/sphinx_design-0.7.0.tar.gz", hash = "sha256:d2a3f5b19c24b916adb52f97c5f00efab4009ca337812001109084a740ec9b7a", size = 2203582, upload-time = "2026-01-19T13:12:53.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/cf/45dd359f6ca0c3762ce0490f681da242f0530c49c81050c035c016bfdd3a/sphinx_design-0.7.0-py3-none-any.whl", hash = "sha256:f82bf179951d58f55dca78ab3706aeafa496b741a91b1911d371441127d64282", size = 2220350, upload-time = "2026-01-19T13:12:51.077Z" }, +] + [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" diff --git a/www/README.md b/www/README.md new file mode 100644 index 00000000..39eef71c --- /dev/null +++ b/www/README.md @@ -0,0 +1,40 @@ +# PyRTL's GitHub Pages Webpage + +PyRTL's GitHub Pages webpage is at https://ucsbarchlab.github.io/PyRTL/ . + +The sources for this webpage are in this `www` directory. The page is built +with [Sphinx](https://www.sphinx-doc.org/en/master/), and written in +[reStructuredText](https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html). +The main Sphinx configuration file is +[`www/conf.py`](https://github.com/UCSBarchlab/PyRTL/blob/development/www/conf.py), +and source for the main page is +[`www/index.rst`](https://github.com/UCSBarchlab/PyRTL/blob/development/www/index.rst) + +Follow the instructions on this page to build a local copy of PyRTL's webpage. +This is useful for verifying that PyRTL's webpage still renders correctly after +making a local change. + +There is additional PyRTL documentation in [Read the +Docs](https://pyrtl.readthedocs.io/), see +[`docs/README.md`](https://github.com/UCSBarchlab/PyRTL/blob/development/docs/README.md). + +## Running Sphinx + +Run Sphinx with the provided +[`justfile`](https://github.com/UCSBarchlab/PyRTL/blob/development/justfile), +from the repository's root directory: + +```shell +# Run Sphinx to build PyRTL's webpage. +$ uv run just www +``` + +This builds a local copy of PyRTL's webpage in `www/_build/html`. +`www/_build/html/index.html` is the home page. + +## GitHub Actions Workflow + +When a commit is pushed that changes `www/`, the +[`pages-deploy`](https://github.com/UCSBarchlab/PyRTL/blob/development/.github/workflows/pages-deploy.yml) +workflow automatically runs Sphinx to regenerate the webpage and deploy it to +GitHub Pages. diff --git a/www/adder.py b/www/adder.py new file mode 100644 index 00000000..4b17638a --- /dev/null +++ b/www/adder.py @@ -0,0 +1,37 @@ +import pyrtl + + +def fa( + x: pyrtl.WireVector, y: pyrtl.WireVector, cin: pyrtl.WireVector +) -> tuple[pyrtl.WireVector, pyrtl.WireVector]: + """Full adder.""" + sum = x ^ y ^ cin + cout = x & y | y & cin | x & cin + return sum, cout + + +def adder( + a: pyrtl.WireVector, b: pyrtl.WireVector, cin: pyrtl.WireVector +) -> tuple[pyrtl.WireVector, pyrtl.WireVector]: + """n-bit ripple carry adder with carry in and carry out.""" + a, b = pyrtl.match_bitwidth(a, b) + + sum = [None] * a.bitwidth + for i in range(a.bitwidth): + sum[i], cout = fa(a[i], b[i], cin) + cin = cout + + full_sum = pyrtl.concat_list(sum) + return full_sum, cout + + +a = pyrtl.Input(name="a", bitwidth=4) +b = pyrtl.Input(name="b", bitwidth=4) +sum = pyrtl.Output(name="sum", bitwidth=8) + +sum_, cout_ = adder(a, b, pyrtl.Const(0)) +sum <<= sum_ + +sim = pyrtl.Simulation() +sim.step_multiple({"a": [1, 2, 3], "b": [2, 3, 4]}) +sim.tracer.render_trace() diff --git a/www/conf.py b/www/conf.py new file mode 100644 index 00000000..56aed23f --- /dev/null +++ b/www/conf.py @@ -0,0 +1,51 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- + +project = "PyRTL" + +# -- General configuration --------------------------------------------------- + +master_doc = "index" + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ["sphinx.ext.intersphinx", "sphinx_copybutton", "sphinx_design"] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# Enable links to Python standard library classes (str, list, dict, etc). +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "pyrtl": ("https://pyrtl.readthedocs.io/en/latest/", None), +} + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_baseurl = "https://ucsbarchlab.github.io/PyRTL" +html_theme = "furo" +html_theme_options = { + "sidebar_hide_name": True, + # For view/edit this page buttons. + "source_repository": "https://github.com/UCSBarchlab/pyrtl", + "source_branch": "development", + "source_directory": "www/", +} +html_title = "PyRTL" +html_logo = "../docs/brand/pyrtl_logo.png" +html_show_sphinx = False +html_show_copyright = False +html_show_sourcelink = False diff --git a/www/fir.py b/www/fir.py new file mode 100644 index 00000000..81d72e02 --- /dev/null +++ b/www/fir.py @@ -0,0 +1,22 @@ +import pyrtl + + +def fir(x: pyrtl.WireVector, bs: list[int]): + rwidth = x.bitwidth # Bitwidth of the registers. + ntaps = len(bs) # Number of coefficients. + + zs = [x] + [pyrtl.Register(rwidth) for _ in range(ntaps - 1)] + for i in range(1, ntaps): + zs[i].next <<= zs[i - 1] + + # Produce the final sum of products. + return sum(z * b for z, b in zip(zs, bs, strict=True)) + + +x = pyrtl.Input(name="x", bitwidth=8) +y = pyrtl.Output(name="y", bitwidth=8) +y <<= fir(x, bs=[0, 1]) + +sim = pyrtl.Simulation() +sim.step_multiple({"x": [0, 9, 18, 8, 17, 7, 16, 6, 15, 5]}) +sim.tracer.render_trace() diff --git a/www/gcd.py b/www/gcd.py new file mode 100644 index 00000000..5a78cad9 --- /dev/null +++ b/www/gcd.py @@ -0,0 +1,39 @@ +import pyrtl + + +def gcd( + a: pyrtl.WireVector, b: pyrtl.WireVector, begin: pyrtl.WireVector +) -> tuple[pyrtl.WireVector, pyrtl.WireVector]: + x = pyrtl.Register(bitwidth=a.bitwidth) + y = pyrtl.Register(bitwidth=b.bitwidth) + done = pyrtl.WireVector(bitwidth=1) + + with pyrtl.conditional_assignment: + with begin: + x.next |= a + y.next |= b + with x > y: + x.next |= x - y + with y > x: + y.next |= y - x + with pyrtl.otherwise: + done |= True + return x, done + + +a = pyrtl.Input(name="a", bitwidth=8) +b = pyrtl.Input(name="b", bitwidth=8) +begin = pyrtl.Input(name="begin", bitwidth=1) + +x = pyrtl.Output(name="x", bitwidth=8) +done = pyrtl.Output(name="done", bitwidth=1) + +x_, done_ = gcd(a, b, begin) +x <<= x_ +done <<= done_ + +sim = pyrtl.Simulation() +sim.step({"a": 12, "b": 9, "begin": True}) +while not sim.inspect("done"): + sim.step({"a": 0, "b": 0, "begin": False}) +sim.tracer.render_trace() diff --git a/www/index.rst b/www/index.rst new file mode 100644 index 00000000..d5046489 --- /dev/null +++ b/www/index.rst @@ -0,0 +1,338 @@ +===== +PyRTL +===== + +PyRTL is a Python library for register-transfer-level hardware design and +simulation. Get started with: + +.. code-block:: shell + + $ pip install pyrtl + +PyRTL Features +============== + +PyRTL provides a collection of classes for Pythonic `register-transfer level +`_ design, simulation, +tracing, and testing suitable for teaching and research. Simplicity, usability, +clarity, and extensibility are overarching goals, rather than performance or +optimization. Features include: + +* Elaboration-through-execution, meaning all of Python can be used, including + introspection. +* Design, instantiate, and simulate all in one file, and without leaving Python. +* Export to, or import from, common HDLs (BLIF-in, Verilog-out currently + supported). +* Examine execution with waveforms in a terminal or export to `.vcd + `_ as projects scale. +* Elaboration, synthesis, and basic optimizations all included. +* Small and well-defined internal core structure means writing new transforms + is easier. +* Batteries included means many useful components are already available. + +.. card:: :octicon:`rocket` New in `PyRTL 1.0.0 `_ + + The new :mod:`pyrtl.rtllib.float` module generates floating point hardware! + +Simple PyRTL Examples +===================== + +Here are some simple examples of PyRTL in action. These examples implement the +same functionality as those highlighted in the wonderful related work `Chisel +`_, which in turn allows us to see the stylistic +differences between the approaches. + +.. tab-set:: + + .. tab-item:: GCD + + A greatest common denominator calculator: ``gcd`` generates a sequential + circuit that saves inputs ``a`` and ``b`` when ``begin`` goes high, and + then, while ``begin`` is low, calculates the GCD with `Euclid's algorithm + `_. The function + returns two :class:`WireVectors `, one which holds the + GCD when the computation is ``done``, and the other which is a boolean + ``done`` signal. + + The code below provides everything needed to instantiate, simulate, and + visualize the resulting design. + + .. literalinclude:: gcd.py + + .. image:: ../docs/screenshots/gcd.png + + .. tab-item:: FIR + + A finite impulse response filter: ``fir`` generates a sequential circuit + that accepts inputs ``x`` and a list of coefficients ``bs``. From the + `Wikipedia FIR description + `_, the list + ``zs`` is the registers required to implement the delay. ``fir`` returns + an output ``y`` which is the resulting sum of products and is valid every + cycle (since the design is naturally fully pipelined). + + .. literalinclude:: fir.py + + .. image:: ../docs/screenshots/fir.png + + .. tab-item:: MaxN + + ``max_n`` generates hardware that identifies the largest of N input + values. This example makes use of Python's `notation for handling + multiple inputs + `_ + by packing them into a :class:`list`. It also demonstrates that the full + power of Python is available to you in PyRTL, including functional tools + like :func:`~functools.reduce`, which is used to chain together multiple + ``max_2`` elements into a bigger ``max_n``. + + .. literalinclude:: maxn.py + + .. image:: ../docs/screenshots/maxn.png + + .. tab-item:: Mul + + ``mul`` generates a small 4 x 4 multiplier with a simple `ROM + `_ lookup. The first two + lines simply check that the inputs are each 4-bits wide. ``romdata`` is a + Python function that calculates the values we want stored in the ROM, as + a function of the ROM address. :class:`~pyrtl.RomBlock` automatically + initializes the ROM with values computed by ``romdata``. The generated + hardware simply :func:`concats ` the two 4-bit inputs into + an 8-bit ROM address and returns the value stored in the ROM at that + address. + + .. literalinclude:: mul.py + + .. image:: ../docs/screenshots/mul.png + + .. tab-item:: Adder + + The classic ripple-carry adder: ``adder`` generates a ripple carry adder + of arbitrary length including both carry in and carry out. The `full + adder `_ + (``fa``) takes 1-bit inputs and produces 1-bit outputs. We iteratively + create full adders and link the carry in of each new full adder to the + carry out of the last full adder. ``adder``'s ``sum`` is a Python + :class:`list` that keeps track of the wires carrying the sum bits. The + final ``full_sum`` is produced by concatenating the wires in ``sum`` with + :func:`~pyrtl.concat_list`. + + .. literalinclude:: adder.py + + .. image:: ../docs/screenshots/adder.png + +The 10,000 Foot Overview +======================== + +At a high level, PyRTL builds hardware that you `explicitly define`. If you are +looking for a tool to take your random Python code and turn it into hardware, +you will have to look elsewhere: this is `not` `HLS +`_. Instead, PyRTL helps +you concisely and precisely describe a digital hardware structure, which you +already have worked out in detail, in Python. + +PyRTL restricts you to a set of reasonable digital designs practices: the clock +and resets are implicit, block memories are synchronous by default, there are +no "undriven" states, and un-registered feedback loops are not allowed. Instead +of worrying about these "analog-ish" tricks that are horrible ideas in modern +processes anyways, PyRTL lets you treat hardware design like a software +problem: build recursive hardware, write introspective containers, and have fun +building digital designs again! + +To the user it provides a set of Python classes that allow them to Pythonically +express their hardware designs. For example, with :class:`~pyrtl.WireVector` +you get a structure that acts very much like a Python list of 1-bit wires, so +``mywire[:-1]`` selects everything except the most-significant-bit. Of course +you can :meth:`add `, :meth:`subtract +`, and :meth:`multiply ` +these :class:`WireVectors `, or :func:`~pyrtl.concat` multiple +bit-vectors end-to-end as well. + +You can even put :class:`WireVectors ` in Python collections +and process them in bulk. For example, if ``x`` is a :class:`list` of +:class:`WireVectors `, and you want to multiply each of them +by 2 and sum them into a :class:`~pyrtl.WireVector` ``y``:: + + y = sum([elem * 2 for elem in x]) + +Hardware comprehensions are surprisingly useful. We'll cover an example in more +detail below, but if you just want to play around with PyRTL `try Jupyter +Notebooks on any of our examples on MyBinder +`_. + +Hello N-bit Ripple-Carry Adder! +=============================== + +While adders are a builtin primitive for PyRTL, most people doing RTL are +familiar with the idea of a `Ripple-Carry Adder +`_ and so it is useful to +see how you might express one in PyRTL. Rather than the typical `Verilog +introduction to fixed 4-bit adders +`_, let's go ahead and build an +`arbitrary` bitwidth adder. + +.. literalinclude:: ripple-carry.py + +The code above includes an adder generator with Python-style slices on wires +(``ripple_add``), an instantiation of a :class:`~pyrtl.Register` (used as a +counter with ``ripple_add``), and all the code needed to simulate the design, +generate a waveform, and render it to the terminal. `Example 2 +`_'s +comments describe this code in much more detail. When you run it, it should +look like this (you can see the counter going from 0 to 7 and repeating): + +.. image:: ../docs/screenshots/pyrtl-counter.png + +A Few Gotchas +============= + +While Python is an amazing language, DSLs in Python are always forced to make a +few compromises which can sometimes catch users in some unexpected ways. Watch +out for these "somewhat surprising features": + +* PyRTL never uses any of the "in-place arithmetic assignments" such as ``+=`` + or ``&=`` in the traditional ways. Instead only ``<<=`` and ``|=`` are + defined and they are used for wire-assignment and conditional-wire-assignment + respectively (more on both of these in `Example 3 + `_). + + If you declare:: + + x = WireVector(bitwidth=3) + + and:: + + y = WireVector(bitwidth=5) + + how do you assign ``x`` the value of ``y + 1``? If you do ``x = y + 1`` that + will replace the old definition of ``x`` entirely. Instead you need to write + ``x <<= y + 1`` which you can read as "``x`` gets its value from ``y + 1``". + +* The example above also shows off another aspect of PyRTL. The + :attr:`~pyrtl.WireVector.bitwidth` of ``y`` is 5. The + :attr:`~pyrtl.WireVector.bitwidth` of ``y + 1`` is actually 6 (PyRTL infers + this automatically). But then when you assign ``x <<= y + 1`` you are taking a + 6-bit value and assigning it to a 3-bit value. This is completely legal, and + the value will be :meth:`truncated `, so only the + least significant bits will be assigned. Mind your bitwidths! + +* PyRTL's :class:`WireVectors ` overloads many useful + operators, including ``==`` and ``<`` which evaluate to a new + :class:`~pyrtl.WireVector` with :attr:`~pyrtl.WireVector.bitwidth` 1 to hold + the result of the comparison. The bitwise operators ``&``, ``|``, ``~`` and + ``^`` are also defined (however logic operations such as ``and`` and ``not`` + are not defined). A really tricky gotcha happens when you start combining + these operators. Consider:: + + do_it = ready & state == 3 + + In Python, the bitwise ``&`` operator has higher precedence than ``==``, thus Python + parses this as:: + + do_it = (ready & state) == 3 + + which is probably not what you intended! Make sure to use parentheses when + using comparisons with logic operations to be clear:: + + do_it = ready & (state == 3) + +* In PyRTL, all :class:`WireVectors ` are `unsigned`, so + :class:`~pyrtl.WireVector` comparisons with ``<`` are unsigned comparisons. + You must explicitly call functions like :func:`~pyrtl.signed_lt` for signed + comparisons. Similarly, if you pass a :class:`~pyrtl.WireVector` to a + function that requires more bits that you have provided, the + :class:`~pyrtl.WireVector` will be zero-extended by default. You must + explicitly call :meth:`~pyrtl.WireVector.sign_extended` to sign-extend. + `Example 1.1 + `_ + provides more examples of signed arithmetic in PyRTL. + +Related Projects +================ + +`Amaranth (previously nMigen) `_ + Another Python hardware project providing an open-source toolchain that has + a lot of wonderful stuff for working with FPGAs in particular. It has + support for evaluation board definitions, a System-on-Chip toolkit, and + more. I think it has a similar philosophy of trying to be easy to learn and + use and simplify the design of complex hardware with reusable components. + Amaranth (at the time of writing) has much better support on the back end + for a variety of real devices and low level stuff like managing clock + domains, but I think PyRTL provides some value in getting going right in + the command line and how it handles memories etc. I would be eager to see + the power of these tools combined in some way! + +`Chisel `_ + A project with similar goals to PyRTL but based on Scala instead of Python. + Scala provides some very helpful embedded language features and a rich type + system. Chisel is (like PyRTL) a elaborate-through-execution hardware + design language. With support for signed types, named hierarchies of wires + useful for hardware protocols, and a neat control structure call ``when`` + that inspired our :data:`~pyrtl.conditional_assignment` contexts, Chisel is + a powerful tool used in some great research projects including RISC-V. + Unlike Chisel, PyRTL has concentrated on a simple to use and complete tool + chain which is useful for instructional projects, and provides a clearly + defined and relatively easy-to-manipulate intermediate structure in the + class :class:`~pyrtl.Block` which allows rapid prototyping of hardware + analysis routines which can then be co-designed with the architecture. + +`SpinalHDL `_ + A different approach to HDL in Scala, very much aligned with the way PyRTL + is built. Invented independently, it is neat to see the convergent + evolution which, I think, points to something deeper about hardware design. + It has a lot of support and really well thought out structures. + +`MyHDL `_ + Another neat Python hardware project built around generators and + decorators. The semantics of this embedded language are close to Verilog + and unlike PyRTL, MyHDL allows asynchronous logic and higher level + modeling. Much like Verilog, only a structural "convertible subset" of the + language can be automatically synthesized into real hardware. PyRTL + requires all logic to be both synchronous and synthesizable which avoids a + common trap for beginners, it elaborates the design during execution + allowing the full power of Python in describing recursive or complex + hardware structures, and it allows for hardware synthesis, simulation, test + bench creation, and optimization all in the same framework. + +`Yosys `_ + An open source tool for Verilog RTL synthesis. It supports a huge subset of + Verilog-2005 and provides a basic set of synthesis algorithms. The goals of + this tool are quite different from PyRTL, but the two play together very + nicely. PyRTL's :func:`~pyrtl.output_to_verilog` produces Verilog that can + be synthesized with Yosys. Similarly, PyRTL's + :func:`~pyrtl.input_from_verilog` uses Yosys to synthesize complex Verilog + designs to a simple library of gates, before importing into PyRTL. + +`PyMTL3 (a.k.a. Mamba) `_ + A beta stage "open-source Python-based hardware generation, simulation, and + verification framework with multi-level hardware modeling support". One of + the neat things about this project is that they are trying to allow + simulation, modeling, and verification at multiple different levels of the + design from the functional level, the cycle-close level, and down to the + register-transfer level (where PyRTL really is built to play). Like MyHDL + they do some meta-programming tricks like parsing the Python AST to allow + executable software descriptions to be (under certain restrictions, sort of + like Verilog) automatically converted into implementable hardware. PyRTL, + on the other hand, is about providing a limited and composable set of data + structures for specifying an RTL implementation, thus avoiding the + distinction between synthesizable and non-synthesizable code (the execution + is the elaboration step). + +`ClaSH `_ + An embedded hardware description language in Haskell. Like PyRTL it + provides an approach suitable for both combinational and synchronous + sequential circuits and transforms these high-level descriptions to + low-level synthesizable Verilog HDL. Unlike PyRTL, designs are statically + typed (like VHDL), yet with a very high degree of type inference, enabling + both safe and fast prototying using concise descriptions. If you like + functional programming and hardware also check out `Lava + `_. + +.. toctree:: + :hidden: + + ReadTheDocs Documentation + GitHub Project + PyRTL Examples + PyRTL Notebook Examples diff --git a/www/maxn.py b/www/maxn.py new file mode 100644 index 00000000..cec198ac --- /dev/null +++ b/www/maxn.py @@ -0,0 +1,22 @@ +from functools import reduce + +import pyrtl + + +def max_n(*inputs): + def max_2(x, y): + return pyrtl.select(x > y, x, y) + + return reduce(max_2, inputs) + + +a = pyrtl.Input(name="a", bitwidth=8) +b = pyrtl.Input(name="b", bitwidth=8) +c = pyrtl.Input(name="c", bitwidth=8) +max = pyrtl.Output(name="max", bitwidth=8) + +max <<= max_n(a, b, c) + +sim = pyrtl.Simulation() +sim.step_multiple({"a": [1, 5, 9], "b": [2, 6, 7], "c": [3, 4, 8]}) +sim.tracer.render_trace() diff --git a/www/mul.py b/www/mul.py new file mode 100644 index 00000000..c65b8695 --- /dev/null +++ b/www/mul.py @@ -0,0 +1,23 @@ +import pyrtl + + +def mul(x: pyrtl.WireVector, y: pyrtl.WireVector) -> pyrtl.WireVector: + assert x.bitwidth == 4 + assert y.bitwidth == 4 + + def romdata(addr: int) -> int: + return (addr >> 4) * (addr & 0xF) + + tbl = pyrtl.RomBlock(bitwidth=8, addrwidth=8, romdata=romdata) + return tbl[pyrtl.concat(x, y)] + + +a = pyrtl.Input(name="a", bitwidth=4) +b = pyrtl.Input(name="b", bitwidth=4) +product = pyrtl.Output(name="product", bitwidth=8) + +product <<= mul(a, b) + +sim = pyrtl.Simulation() +sim.step_multiple({"a": [1, 2, 3], "b": [2, 3, 4]}) +sim.tracer.render_trace() diff --git a/www/ripple-carry.py b/www/ripple-carry.py new file mode 100644 index 00000000..21d7d7e2 --- /dev/null +++ b/www/ripple-carry.py @@ -0,0 +1,34 @@ +import pyrtl + + +def one_bit_add( + a: pyrtl.WireVector, b: pyrtl.WireVector, carry_in: pyrtl.WireVector | int +) -> tuple[pyrtl.WireVector, pyrtl.WireVector]: + assert len(a) == len(b) == 1 # `len` returns the bitwidth. + sum = a ^ b ^ carry_in # WireVector operators build the hardware. + carry_out = a & b | a & carry_in | b & carry_in + return sum, carry_out + + +def ripple_add( + a: pyrtl.WireVector, b: pyrtl.WireVector, carry_in: pyrtl.WireVector | int = 0 +) -> tuple[pyrtl.WireVector, pyrtl.WireVector]: + a, b = pyrtl.match_bitwidth(a, b) + if len(a) == 1: + sumbits, carry_out = one_bit_add(a, b, carry_in) + else: + lsbit, ripplecarry = one_bit_add(a[0], b[0], carry_in) + msbits, carry_out = ripple_add(a[1:], b[1:], ripplecarry) + sumbits = pyrtl.concat(msbits, lsbit) + return sumbits, carry_out + + +# Instantiate an adder into a 3-bit counter. +counter = pyrtl.Register(name="counter", bitwidth=3) +sum, carry_out = ripple_add(counter, pyrtl.Const(1)) +counter.next <<= sum + +# Simulate the instantiated design for 15 cycles. +sim = pyrtl.Simulation() +sim.step_multiple(nsteps=15) +sim.tracer.render_trace()