PYRTL

A collection of classes providing simple RTL specification, simulation, tracing, and testing suitable for teaching and research. Simplicity, usability, clarity, and extensibility rather than performance or optimization is the overarching goal. With PyRTL you can use the full power of Python to describe complex synthesizable digital designs, simulate and test them, and export them to Verilog.

Installation

Automatic installation:

pip install pyrtl

PyRTL is listed in PyPI and can be installed with pip or pip3.

Design, Simulate, and Inspect in 15 lines

 1import pyrtl
 2
 3a = pyrtl.Input(8,'a')  # input "pins"
 4b = pyrtl.Input(8,'b')
 5q = pyrtl.Output(8,'q')  # output "pins"
 6gt5 = pyrtl.Output(1,'gt5')
 7
 8result = a + b  # makes an 8-bit adder
 9q <<= result  # assigns output of adder to out pin
10gt5 <<= result > 5  # does a comparison, assigns that to different pin
11
12# simulate and output the resulting waveform to the terminal
13sim = pyrtl.Simulation()
14sim.step_multiple({'a':[0,1,2,3,4], 'b':[2,2,3,3,4]})
15sim.tracer.render_trace()

After you have PyRTL installed, you should be able to cut and paste the above into a file and run it with Python. The result you should see, drawn right into the terminal, is the output of the simulation. While a great deal of work has gone into making hardware design in PyRTL as friendly as possible, please don’t mistake that for a lack of depth. You can just as easily export to Verilog or other hardware formats, view results with your favorite waveform viewer, build hardware transformation passes, run JIT-accelerated simulations, design, test, verify hugely complex digital systems, and much more. Most critically of all it is easy to extend with your own approaches to digital hardware development as you find necessary.

Overview of PyRTL

If you are brand new to PyRTL we recommend that you start with the PyRTL Code Examples which will show you most of the core functionality in the context of a complete design.

WireVectors

Perhaps the most important class to understand is WireVector, which is the basic type from which you build all hardware. If you are coming to PyRTL from Verilog, a WireVector is closest to a multi-bit wire. Every new WireVector builds a set of wires which you can then connect with other WireVector through overloaded operations such as __add__() or __or__().

A bunch of other related classes, including Input, Output, Const, and Register are all derived from WireVector. Coupled with MemBlock (and RomBlock), this is all a user needs to create a functional hardware design.

Inheritance diagram of pyrtl.WireVector, pyrtl.Input, pyrtl.Output, pyrtl.Const, pyrtl.Register

After specifying a hardware design, there are then options to simulate your design right in PyRTL, synthesize it down to primitive 1-bit operations, optimize it, and export it to Verilog (along with a testbench).

Simulation

PyRTL provides tools for simulation and viewing simulation traces. Simulation is how your hardware is “executed” for the purposes of testing, and three different classes help you do that: Simulation, FastSimulation and CompiledSimulation. All three have almost the same interface and, except for a few debugging cases, can be used interchangeably. Typically one starts with Simulation and then moves up to FastSimulation when performance begins to matter.

Both Simulation and FastSimulation store a list of each wire’s value in each cycle in Simulation.tracer, which is an instance of SimulationTrace. Traces can then be rendered to the terminal with SimulationTrace.render_trace(). SimulationTraces can be handled in other ways, for example they can be extracted as a test bench with output_verilog_testbench(), or exported to a VCD file with print_vcd().

Optimization

WireVector and MemBlock are just “sugar” over a core set of primitives, and the final design is built up incrementally as a graph of these primitives. WireVectors connects these “primitives”, which connect to other WireVectors. Each primitive is a LogicNet, and a Block is a graph of LogicNets. Typically a full design is stored in a single Block. The function working_block() returns the block on which we are implicitly working. Hardware transforms may make a new Block from an old one. For example, see PostSynthBlock.

Errors

Finally, when things go wrong you may hit an Exception, neither of which is likely recoverable automatically (which is why we limited them to only two types). PyrtlError is intended to capture end user errors such as invalid constant strings and mis-matched bitwidths. In contrast, PyrtlInternalError captures internal invariants and assertions over the core logic graph which should never be encountered when constructing designs in the normal ways. If you hit a confusing PyrtlError or any PyrtlInternalError feel free to file an issue.

class pyrtl.PyrtlError[source]

Raised on any user-facing error in this module.

class pyrtl.PyrtlInternalError[source]

Raised on any PyRTL internal failure.

Reference Guide

Index