Simulation and Testing

Simulation

class pyrtl.Simulation(tracer=True, register_value_map=None, memory_value_map=None, default_value=0, block=None)[source]

A class for simulating Blocks of logic step by step.

A Simulation step works as follows:

  1. Registers are updated:

  1. (If this is the first step) With the default values passed in to the Simulation during instantiation and/or any reset_values specified in the individual Registers.

  2. (Otherwise) With their next values calculated in the previous step (r LogicNets).

  1. The new values of these Registers as well as the values of Block Inputs are propagated through the combinational logic.

  2. MemBlock writes are performed (@ LogicNets).

  3. The current values of all wires are recorded in the tracer.

  4. The next values for the Registers are saved, ready to be applied at the beginning of the next step.

Note that the Register values saved in the tracer after each simulation step are from before the Register has latched in its newly calculated values, since that latching occurs at the beginning of the next step.

In addition to the functions methods listed below, it is sometimes useful to reach into this class and access internal state directly. Of particular usefulness are:

  • .value: a map from every signal in the Block to its current simulation value.

  • .regvalue: a map from Register to its value on the next cycle.

  • .memvalue: a map from MemBlock.id (memid) to a dictionary of {address: value}.

__init__(tracer=True, register_value_map=None, memory_value_map=None, default_value=0, block=None)[source]

Creates a new circuit simulator.

Warning

Warning: Simulation initializes some things in __init__(), so changing items in the Block during Simulation will likely break the Simulation.

Parameters:
  • tracer (SimulationTrace, default: True) – Stores execution results. If None is passed, no tracer is used, which improves performance for long running simulations. If the default (True) is passed, Simulation will create a new SimulationTrace automatically, which can be referenced as tracer.

  • register_value_map (dict[Register, int] | None, default: None) – Defines the initial value for the Registers specified; overrides the Register’s reset_value.

  • memory_value_map (dict[MemBlock, dict[int, int]] | None, default: None) – Defines initial values for MemBlocks. Format: {memory: {address: value}}. memory is a MemBlock, address is the address of value

  • default_value (int, default: 0) – The value that all unspecified Registers and MemBlocks will initialize to (default 0). For Registers, this is the value that will be used if the particular Register doesn’t have a specified reset_value, and isn’t found in the register_value_map.

  • block (Block, default: None) – The hardware Block to be simulated (which might be of type PostSynthBlock). Defaults to the working_block.

inspect(w)[source]

Get the value of a WireVector in the current Simulation cycle.

Example:

>>> counter = pyrtl.Register(name="counter", bitwidth=3)
>>> counter.next <<= counter + 1

>>> sim = pyrtl.Simulation()
>>> sim.step()
>>> sim.inspect("counter")
0

>>> sim.step()
>>> sim.inspect("counter")
1
Parameters:

w (str) – The name of the WireVector to inspect (passing in a WireVector instead of a name is deprecated).

Raises:

KeyError – If w does not exist in the Simulation.

Return type:

int

Returns:

The value of w in the current Simulation cycle.

inspect_mem(mem)[source]

Get MemBlock values in the current Simulation cycle.

Note

This returns the current contents of the MemBlock. Modifying the returned dict will modify the Simulation’s state.

Example:

>>> mem = pyrtl.MemBlock(bitwidth=8, addrwidth=2)

>>> write_addr = pyrtl.Register(name="write_addr", bitwidth=2)
>>> write_addr.next <<= write_addr + 1
>>> mem[write_addr] <<= write_addr + 10

>>> sim = pyrtl.Simulation()
>>> sim.step_multiple(nsteps=4)
>>> sorted(sim.inspect_mem(mem).items())
[(0, 10), (1, 11), (2, 12), (3, 13)]
Parameters:

mem (MemBlock) – The memory to inspect.

Return type:

dict[int, int]

Returns:

A dict mapping from memory address to memory value.

step(provided_inputs=None)[source]

Take the simulation forward one cycle.

step causes the Block to be updated as follows, in order:

  1. Registers are updated with their next values computed at the end of the previous cycle.

  2. Inputs and these new Register values propagate through the combinational logic

  3. MemBlocks are updated

  4. The next values of the Registers are saved for use at the beginning of the next cycle.

All Input wires must be in the provided_inputs.

Example: if we have Inputs named a and x, we can call:

sim.step({'a': 1, 'x': 23})

to simulate a cycle where a == 1 and x == 23 respectively.

Parameters:

provided_inputs (dict[str, int] | None, default: None) – A dictionary mapping Input WireVectors to their values for this step.

step_multiple(provided_inputs=None, expected_outputs=None, nsteps=None, file=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>, stop_after_first_error=False)[source]

Take the simulation forward N cycles, based on provided_inputs for each cycle.

All Input wires must be in provided_inputs. Additionally, the length of the array of provided values for each Input must be the same.

When nsteps is specified, then it must be less than or equal to the number of values supplied for each Input when provided_inputs is non-empty. When provided_inputs is empty (which may be a legitimate case for a design that takes no Input), then nsteps will be used. When nsteps is not specified, then the simulation will take the number of steps equal to the number of values supplied for each Input.

Example: if we have Inputs named a and b and Output o, we can call:

sim.step_multiple({'a': [0,1], 'b': [23,32]}, {'o': [42, 43]})

to simulate 2 cycles. In the first cycle, a and b take on 0 and 23, respectively, and o is expected to have the value 42. In the second cycle, a and b take on 1 and 32, respectively, and o is expected to have the value 43.

If your values are all single digit, you can also specify them in a single string, e.g.:

sim.step_multiple({'a': '01', 'b': '01'})

will simulate 2 cycles. In the first cycle, a and b take on 0 and 0, respectively. In the second cycle, they take on 1 and 1, respectively.

If a design has no Inputs, use nsteps to specify the number of cycles to simulate:

>>> counter = pyrtl.Register(name="counter", bitwidth=8)
>>> counter.next <<= counter + 1

>>> sim = pyrtl.Simulation()
>>> sim.step_multiple(nsteps=3)
>>> sim.inspect("counter")
2
Parameters:
  • provided_inputs (dict[str, list[int]] | None, default: None) – A dictionary mapping WireVectors to their values for N steps.

  • expected_outputs (dict[str, list[int]] | None, default: None) – A dictionary mapping WireVectors to their expected values for N steps; use ? to indicate you don’t care what the value at that step is.

  • nsteps (int | None, default: None) – A number of steps to take (defaults to None, meaning step for each supplied input value in provided_inputs)

  • file (default: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>) – Where to write the output (if there are unexpected outputs detected).

  • stop_after_first_error (bool, default: False) – A boolean flag indicating whether to stop the simulation after encountering the first error (defaults to False).

tracer: SimulationTrace

Stores the simulation results for each cycle.

tracer is typically used to render simulation waveforms with render_trace(), for example:

sim = pyrtl.Simulation()
sim.step_multiple(nsteps=10)
sim.tracer.render_trace()

See SimulationTrace for more display options.

Fast (JIT to Python) Simulation

class pyrtl.FastSimulation(register_value_map=None, memory_value_map=None, default_value=0, tracer=True, block=None, code_file=None)[source]

Simulate a block by generating and running Python code.

FastSimulation re-implements Simulation, with slower start-up and faster execution. This can be a good trade-off when simulating large circuits, or simulating many cycles.

FastSimulation is a drop-in replacement for Simulation, so the two classes share the same interface. See Simulation for interface documentation, and more details about PyRTL simulations.

__init__(register_value_map=None, memory_value_map=None, default_value=0, tracer=True, block=None, code_file=None)[source]

The interfaces for FastSimulation and Simulation are nearly identical, so only the differences are described here. See Simulation.__init__() for descriptions of the remaining constructor arguments.

Note

This constructor generates Python code for the Block, so any changes to the circuit after instantiating a FastSimulation will not be reflected in the FastSimulation.

In addition to Simulation.__init__()’s arguments, this constructor additionally takes:

Parameters:

code_file (str | None, default: None) – The name of the file in which to store a copy of the generated Python code. By default, the generated code is not saved.

Compiled (JIT to C) Simulation

class pyrtl.CompiledSimulation(tracer=True, register_value_map=None, memory_value_map=None, default_value=0, block=None)[source]

Simulate a block by generating, compiling, and running C code.

CompiledSimulation provides significant execution speed improvements over FastSimulation, at the cost of even longer start-up time. Generally this will do better than FastSimulation for simulations requiring over 1000 steps.

CompiledSimulation is not built to be a debugging tool, though it may help with debugging. Note that only Input and Output wires can be traced with CompiledSimulation.

Note

For very large circuits, FastSimulation can sometimes be a better choice than CompiledSimulation because CompiledSimulation will generate an extremely large .c file, which can take prohibitively long to compile and optimize. FastSimulation will generate an extremely large .py file, but Python will interpret that generated code as needed, instead of trying to process all the generated code at once.

Warning

This code is still experimental, but has been used on designs of significant scale to good effect.

To use CompiledSimulation, you’ll need:

  • A 64-bit processor

  • GCC (tested on version 4.8.4)

  • A 64-bit build of Python

If using the multiplication operand, only some architectures are supported:

  • x86-64 / amd64

  • arm64 / aarch64

  • mips64 (untested)

default_value is currently only implemented for Registers, not MemBlocks.

CompiledSimulation is a drop-in replacement for Simulation, so the two classes share the same interface. See Simulation for interface documentation, and more details about PyRTL simulations.

Simulation Trace

class pyrtl.SimulationTrace(wires_to_track=None, block=None)[source]

Storage and presentation of simulation waveforms.

Simulation writes data from each simulation cycle to its Simulation.tracer, which is an instance of SimulationTrace.

Users can visualize this simulation data with methods like render_trace().

__init__(wires_to_track=None, block=None)[source]

Creates a new Simulation Trace

Parameters:
  • wires_to_track (list[WireVector] | None, default: None) – The wires that the tracer should track. If unspecified, will track all explicitly-named wires. If set to 'all', will track all wires, including internal wires.

  • block (Block, default: None) – Block containing logic to trace. Defaults to the working_block.

print_perf_counters(*trace_names, file=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>)[source]

Print performance counter statistics for trace_names.

This function prints the number of cycles where each trace’s value is one. This is useful for counting the number of times important events occur in a simulation, such as cache misses and branch mispredictions.

Parameters:
  • trace_names (str) – List of trace names. Each trace must be a single-bit wire.

  • file (default: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>) – The place to write output, defaults to stdout.

print_trace(file=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>, base=10, compact=False)[source]

Prints a list of wires and their current values.

Parameters:
  • base (int, default: 10) – The base the values are to be printed in.

  • compact (bool, default: False) – Whether to omit spaces in output lines.

print_vcd(file=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>, include_clock=False)[source]

Print the trace out as a VCD File for use in other tools.

Dumps the current trace to file as a value change dump file. Examples:

sim_trace.print_vcd()
sim_trace.print_vcd("my_waveform.vcd", include_clock=True)
Parameters:
  • file (default: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>) – File to open and output vcd dump to. Defaults to stdout.

  • include_clock (default: False) – Boolean specifying if the implicit clk should be included. Defaults to False.

render_trace(trace_list=None, file=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>, renderer=<pyrtl.simulation.WaveRenderer object>, symbol_len=None, repr_func=<built-in function hex>, repr_per_name=None, segment_size=1)[source]

Render the trace to a file using unicode and ASCII escape sequences.

The resulting output can be viewed directly on the terminal or viewed with less -R which should handle the ASCII escape sequences used in rendering.

Parameters:
  • trace_list (list[str] | None, default: None) – A list of signal names to be output in the specified order.

  • file (default: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>) – The place to write output, default to stdout.

  • renderer (WaveRenderer, default: <pyrtl.simulation.WaveRenderer object at 0x722a9cef8980>) – An object that translates traces into output bytes.

  • symbol_len (int | None, default: None) – The “length” of each rendered value in characters. If None, the length will be automatically set such that the largest represented value fits.

  • repr_func (Callable[[int], str], default: <built-in function hex>) – Function to use for representing each value in the trace. Examples include hex(), oct(), bin(), and str (for decimal), val_to_signed_integer() (for signed decimal) or the function returned by enum_name() (for IntEnum). Defaults to hex().

  • repr_per_name (dict[str, Callable[[int], str]] | None, default: None) – Map from signal name to a function that takes in the signal’s value and returns a user-defined representation. If a signal name is not found in the map, the argument repr_func will be used instead.

  • segment_size (int, default: 1) – Traces are broken in the segments of this number of cycles.

trace: dict[str, list[int]]

A dict mapping from a WireVector’s name to a list of its values in each cycle.

pyrtl.enum_name(EnumClass)[source]

Returns a function that returns the name of an enum.IntEnum value.

Use enum_name as a repr_func or repr_per_name for SimulationTrace.render_trace() to display enum.IntEnum names in traces, instead of their numeric value. Example:

>>> class State(enum.IntEnum):
...     FOO = 0
...     BAR = 1
>>> state = pyrtl.Input(name="state", bitwidth=1)

>>> sim = pyrtl.Simulation()
>>> sim.step_multiple({"state": [State.FOO, State.BAR]})
>>> sim.tracer.render_trace(repr_per_name={"state": pyrtl.enum_name(State)})

Which prints:

     │0  │1

state FOO│BAR
Parameters:

EnumClass (type) – enum to convert. This is the enum class, like State, not an enum value, like State.FOO or 1.

Return type:

Callable[[int], str]

Returns:

A function that accepts an enum value, like State.FOO or 1, and returns the value’s name as a string, like "FOO".

Wave Renderer

class pyrtl.simulation.WaveRenderer(constants)[source]

Render a SimulationTrace to the terminal.

Most users should not interact with this class directly, unless they are customizing trace appearance.

Export the PYRTL_RENDERER environment variable to change the default renderer. See the documentation for RendererConstants’ subclasses for valid values of PYRTL_RENDERER, as well as sample screenshots.

Try renderer-demo.py, which renders traces with different options, to see what works in your terminal.

__init__(constants)[source]

Instantiate a WaveRenderer.

Parameters:

constants (RendererConstants) – Subclass of RendererConstants that specifies the ASCII/Unicode characters to use for rendering waveforms.

class pyrtl.simulation.RendererConstants[source]

Abstract base class for renderer constants.

These constants determine which characters are used to render waveforms in a terminal.

Inheritance diagram of pyrtl.simulation.Utf8RendererConstants, pyrtl.simulation.Utf8AltRendererConstants, pyrtl.simulation.PowerlineRendererConstants, pyrtl.simulation.Cp437RendererConstants, pyrtl.simulation.AsciiRendererConstants

class pyrtl.simulation.PowerlineRendererConstants[source]

Bases: Utf8RendererConstants

Powerline renderer constants. Font must include powerline glyphs.

This render’s appearance is the most similar to a traditional logic analyzer. Single-bit WireVectors are rendered as square waveforms, with vertical rising and falling edges. Multi-bit WireVector values are rendered in reverse-video hexagons.

This renderer requires a terminal font that supports Powerline glyphs

Enable this renderer by default by setting the PYRTL_RENDERER environment variable to powerline:

export PYRTL_RENDERER=powerline
_images/pyrtl-renderer-demo-powerline.png
class pyrtl.simulation.Utf8RendererConstants[source]

Bases: RendererConstants

UTF-8 renderer constants. These should work in most terminals.

Single-bit WireVectors are rendered as square waveforms, with vertical rising and falling edges. Multi-bit WireVector values are rendered in reverse-video rectangles.

This is the default renderer on non-Windows platforms.

Enable this renderer by default by setting the PYRTL_RENDERER environment variable to utf-8:

export PYRTL_RENDERER=utf-8
_images/pyrtl-renderer-demo-utf-8.png
class pyrtl.simulation.Utf8AltRendererConstants[source]

Bases: RendererConstants

Alternative UTF-8 renderer constants.

Single-bit WireVectors are rendered as waveforms with sloped rising and falling edges. Multi-bit WireVector values are rendered in reverse-video rectangles.

Compared to Utf8RendererConstants, this renderer is more compact because it uses one character between cycles instead of two.

Enable this renderer by default by setting the PYRTL_RENDERER environment variable to utf-8-alt:

export PYRTL_RENDERER=utf-8-alt
_images/pyrtl-renderer-demo-utf-8-alt.png
class pyrtl.simulation.Cp437RendererConstants[source]

Bases: RendererConstants

Code page 437 renderer constants (for windows cmd compatibility).

Single-bit WireVectors are rendered as square waveforms, with vertical rising and falling edges. Multi-bit WireVector values are rendered between vertical bars.

Code page 437 is also known as 8-bit ASCII. This is the default renderer on Windows platforms.

Compared to Utf8RendererConstants, this renderer is more compact because it uses one character between cycles instead of two, but the wire names are vertically aligned at the bottom of each waveform.

Enable this renderer by default by setting the PYRTL_RENDERER environment variable to cp437:

export PYRTL_RENDERER=cp437
_images/pyrtl-renderer-demo-cp437.png
class pyrtl.simulation.AsciiRendererConstants[source]

Bases: RendererConstants

7-bit ASCII renderer constants. These should work anywhere.

Single-bit WireVectors are rendered as waveforms with sloped rising and falling edges. Multi-bit WireVector values are rendered between vertical bars.

Enable this renderer by default by setting the PYRTL_RENDERER environment variable to ascii:

export PYRTL_RENDERER=ascii
_images/pyrtl-renderer-demo-ascii.png