Export SystemRDL to PyBind11 modules for Python-based hardware testing
Project description
PeakRDL-pybind11
PeakRDL-pybind11 generates Python bindings from SystemRDL register descriptions so a Python-fluent person — bring-up engineer, test author, FW dev, lab automator, debugger — can drive a chip from a REPL or a Jupyter notebook with dir(soc) and a docstring as the only documentation needed.
Status: the API documented in this README is the design target. The current release ships the foundation (exporter, generated bindings, pluggable masters, the context-manager pattern) and is converging on the surface described here. The full design lives in
docs/IDEAL_API_SKETCH.md. Sections marked aspirational are roadmap items; please file an issue if you hit a gap.
Mental model
A generated module is a typed tree of nodes that mirrors the RDL hierarchy. Every node knows its path, its absolute address, its kind (AddrMap, RegFile, Reg, Field, Mem, InterruptGroup, Alias, Signal), its metadata, and which master serves its address range.
SoC
├── AddrMap (peripherals)
│ ├── RegFile (uart[0..3])
│ │ ├── Reg (control)
│ │ │ └── Field (enable, baudrate, parity)
│ │ ├── Reg (status) ← side-effecting reads (rclr) are flagged
│ │ └── InterruptGroup ← auto-detected from intr_state/enable/test
│ └── Mem (sram) ← buffer-protocol, ndarray, slice
└── Master (the bus, pluggable)
Nodes are descriptors, not values. They produce values by reading. Values produced by reads are typed wrappers (RegisterValue, FieldValue) that behave like ints, decode enums, and round-trip cleanly back into writes.
There are exactly four primitive operations:
reg.read() # → RegisterValue (1 bus read)
reg.write(value) # raw write (1 bus write, no read)
reg.modify(**fields) # RMW (1 read + 1 write)
reg.poke(value) # explicit "I know what I'm doing", same as .write()
field.read() reads the parent register and slices. field.write(v) is shorthand for reg.modify(field=v) — one read, one write. The names are chosen so the bus cost is the obvious reading.
For non-clearing inspection of registers with read-side effects, use peek(). Side-effect-only operations are first-class: clear(), set(), pulse(), acknowledge().
Features
- Hierarchy & discovery — typed tree mirroring RDL. Navigate by attribute, by index, or by path string.
soc.find(0x4000_1004),soc.walk(kind=Reg),soc.find_by_name("control", glob=True). - Atomic multi-field updates —
modify(**fields)is the canonical RMW; kwargs map to fields by name and are type-checked against generated.pyistubs. - Interrupts as a first-class group —
INTR_STATE/INTR_ENABLE/INTR_TESTtrios are auto-detected and exposed asInterruptGroupnodes withwait(),clear(),pending(),enable_all(), ISR-friendly iteration, and an asyncio variant. - Memory as numpy-aware
MemView—mem[10:20]is a live view;.copy()/.read()snapshots into a one-burstndarray. Buffer-protocol and__array__interop everywhere. - Snapshots & diff —
soc.snapshot()for whole-SoC golden-state captures, JSON/pickle round-trip,snap2.diff(snap1)for change tables,soc.restore(snap)for replay. - Jupyter widgets —
_repr_html_on every node renders a field table with side-effect badges, color-coded access modes, and click-to-expand source location.node.watch(period=0.1)produces a refreshing live monitor. - Pluggable masters with tracing & replay — Mock, OpenOCD, SSH, simulator, and a
RecordingMaster/ReplayMasterpair. Transactions are reified objects you can script. - Type-safe stubs — generated
.pyideclares enum types, field literals, andUnpack[TypedDict]formodify(...). IDE completion is the spec; mypy/pyright catch unknown field names and out-of-range values. - Typed values —
RegisterValue/FieldValueare immutable, hashable, picklable, JSON-serializable. Mutation goes through.replace(**fields)and never through assignment. - Side effects loud, not silent —
reprshows⚠ rclr,↻ singlepulse,✱ sticky,⚡ volatile.with soc.no_side_effects():blocks destructive reads.--strict-fields=falseopt-out exists for C-driver porting and emits aDeprecationWarningon every loose assignment.
Quick example
The first three vignettes from docs/IDEAL_API_SKETCH.md §24:
Bring-up
from MyChip import MyChip
from MyChip.uart import BaudRate
from peakrdl_pybind11.masters import OpenOCDMaster
soc = MyChip.create(master=OpenOCDMaster())
print(soc.uart.control)
soc.uart.control.modify(enable=1, baudrate=BaudRate.BAUD_115200)
soc.uart.data.write(ord('A'))
soc.uart.interrupts.tx_done.wait(timeout=1.0)
soc.uart.interrupts.tx_done.clear()
Test
def test_uart_loopback(soc):
with soc.uart.control as r:
r.enable = 1
r.loopback = 1
r.baudrate = BaudRate.BAUD_115200
soc.uart.data.write(0xA5)
soc.uart.status.rx_ready.wait_for(True, timeout=0.1)
assert soc.uart.data.read() == 0xA5
soc.uart.interrupts.clear_all()
Bulk memory init
import numpy as np
soc.ram[:] = 0
soc.ram[0:64] = np.arange(64, dtype=np.uint32) * 4
checksum = np.bitwise_xor.reduce(np.asarray(soc.ram)[0:1024])
modify(**fields) is the canonical multi-field update; the with soc.uart.control as r: block is the secondary form for staging many writes (and for cases where one staged value is read back to compute another).
Installation
pip install peakrdl-pybind11
Usage
Command line
peakrdl pybind11 input.rdl -o output_dir --soc-name MySoC --top top_addrmap --gen-pyi
CLI options
--soc-name— name of the generated SoC module (default: derived from input file)--top— top-level address map node to export (default: top-level node)--gen-pyi— generate.pyistub files for type hints (enabled by default)--split-bindings COUNT— split bindings into multiple files for parallel compilation when register count exceeds COUNT. Default: 100. Set to 0 to disable. Ignored when--split-by-hierarchyis used.--split-by-hierarchy— split bindings by addrmap/regfile hierarchy instead of by register count. Keeps related registers together; recommended for designs with clear hierarchical structure.
For large register maps, compilation can be slow. PeakRDL-pybind11 emits CMake projects that compile split files in parallel and uses -O1 even for debug builds to reduce template-heavy build times — see COMPILATION_OPTIMIZATIONS.md for the full breakdown.
Python API
from peakrdl_pybind11 import Pybind11Exporter
from systemrdl import RDLCompiler
# Compile SystemRDL
rdl = RDLCompiler()
rdl.compile_file("input.rdl")
root = rdl.elaborate()
# Export to PyBind11
exporter = Pybind11Exporter()
exporter.export(root, "output_dir", soc_name="MySoC")
# For large designs, enable hierarchical binding splitting (recommended)
exporter.export(root, "output_dir", soc_name="MySoC", split_by_hierarchy=True)
Using generated modules
import MySoC
from MySoC.uart import BaudRate, Parity
from peakrdl_pybind11.masters import MockMaster
soc = MySoC.create(master=MockMaster())
# Read a register — typed value with decoded fields in repr
v = soc.uart.control.read()
print(v)
# Atomic multi-field update — one read, one write, type-checked from .pyi
soc.uart.control.modify(enable=1, baudrate=BaudRate.BAUD_115200, parity=Parity.NONE)
# Wait for an interrupt
soc.uart.interrupts.tx_done.wait(timeout=1.0)
soc.uart.interrupts.tx_done.clear()
# Stage many writes with the secondary context-manager form
with soc.uart.control as r:
r.enable = 1
r.baudrate = BaudRate.BAUD_115200
if r.parity.read() == Parity.NONE:
r.parity = Parity.EVEN
# 1 read + 1 write hits the bus on exit
# Snapshot & diff
snap1 = soc.snapshot()
do_thing()
snap2 = soc.snapshot()
print(snap2.diff(snap1))
Requirements
- Python >= 3.10
- systemrdl-compiler >= 1.30.1
- NumPy — hard runtime dependency (bursts, bulk reads, buffer protocol, snapshot frames)
- Jinja2
- CMake >= 3.15 (for building generated modules)
- C++11 compatible compiler (for building generated modules)
- pybind11 (runtime dependency for generated code)
Benchmarks
Performance benchmarks measure export and build times across realistic register maps:
# Run fast export benchmarks
python benchmarks/run_benchmarks.py fast
# Run all benchmarks
pytest benchmarks/ --benchmark-only
# See all benchmark options
python benchmarks/run_benchmarks.py
See benchmarks/README.md for detailed documentation.
Documentation
Full documentation is available at ReadTheDocs. The full design sketch lives in docs/IDEAL_API_SKETCH.md.
To build the documentation locally:
pip install -e .[docs]
cd docs
make html
The built documentation will be in docs/_build/html/.
License
This project is licensed under the GPL-3.0 License — see the LICENSE file for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file peakrdl_pybind11-0.7.2.tar.gz.
File metadata
- Download URL: peakrdl_pybind11-0.7.2.tar.gz
- Upload date:
- Size: 267.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d24a6b9fa1f6c5448d630e9bf36e13e07e79e48e36679340981ff8b6e3e829b5
|
|
| MD5 |
270e76bbeb31d886c557e8fd7653cc2d
|
|
| BLAKE2b-256 |
88813d9b4d839d2ddf000391b93c2162fdbd665c1381dc15da66e457413aa94a
|
Provenance
The following attestation bundles were made for peakrdl_pybind11-0.7.2.tar.gz:
Publisher:
release.yml on arnavsacheti/PeakRDL-pybind11
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
peakrdl_pybind11-0.7.2.tar.gz -
Subject digest:
d24a6b9fa1f6c5448d630e9bf36e13e07e79e48e36679340981ff8b6e3e829b5 - Sigstore transparency entry: 1522811289
- Sigstore integration time:
-
Permalink:
arnavsacheti/PeakRDL-pybind11@9e0e584f6febdb978ca168ecfb2afd1aeebad4fa -
Branch / Tag:
refs/tags/v0.7.2 - Owner: https://github.com/arnavsacheti
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9e0e584f6febdb978ca168ecfb2afd1aeebad4fa -
Trigger Event:
push
-
Statement type:
File details
Details for the file peakrdl_pybind11-0.7.2-py3-none-any.whl.
File metadata
- Download URL: peakrdl_pybind11-0.7.2-py3-none-any.whl
- Upload date:
- Size: 276.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f15b73f538bc080c4968c08a3342bd4de4055299a2c8b963de6ae67d9af1142
|
|
| MD5 |
61a8cfeffc77f21c42a033c8919bcbc6
|
|
| BLAKE2b-256 |
152b218e2d01a8cbf71177a4540c76fca0af78c55b6e6e90a91ffe07f441f9ce
|
Provenance
The following attestation bundles were made for peakrdl_pybind11-0.7.2-py3-none-any.whl:
Publisher:
release.yml on arnavsacheti/PeakRDL-pybind11
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
peakrdl_pybind11-0.7.2-py3-none-any.whl -
Subject digest:
7f15b73f538bc080c4968c08a3342bd4de4055299a2c8b963de6ae67d9af1142 - Sigstore transparency entry: 1522811308
- Sigstore integration time:
-
Permalink:
arnavsacheti/PeakRDL-pybind11@9e0e584f6febdb978ca168ecfb2afd1aeebad4fa -
Branch / Tag:
refs/tags/v0.7.2 - Owner: https://github.com/arnavsacheti
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9e0e584f6febdb978ca168ecfb2afd1aeebad4fa -
Trigger Event:
push
-
Statement type: