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
Blocksof logic step by step.A
Simulationstep works as follows:Registersare updated:
(If this is the first step) With the default values passed in to the
Simulationduring instantiation and/or anyreset_valuesspecified in the individualRegisters.(Otherwise) With their next values calculated in the previous step (
rLogicNets).
The new values of these
Registersas well as the values ofBlockInputsare propagated through the combinational logic.The current values of all wires are recorded in the
tracer.The next values for the
Registersare saved, ready to be applied at the beginning of the next step.
Note that the
Registervalues saved in thetracerafter each simulation step are from before theRegisterhas 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 theBlockto its current simulation value..regvalue: a map fromRegisterto its value on the next cycle..memvalue: a map fromMemBlock.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:
Simulationinitializes some things in__init__(), so changing items in theBlockduringSimulationwill likely break theSimulation.- Parameters:
tracer (
SimulationTrace, default:True) – Stores execution results. IfNoneis passed, notraceris used, which improves performance for long running simulations. If the default (True) is passed,Simulationwill create a newSimulationTraceautomatically, which can be referenced astracer.register_value_map (
dict[Register,int] |None, default:None) – Defines the initial value for theRegistersspecified; overrides theRegister’sreset_value.memory_value_map (
dict[MemBlock,dict[int,int]] |None, default:None) – Defines initial values forMemBlocks. Format:{memory: {address: value}}.memoryis aMemBlock,addressis the address ofvaluedefault_value (
int, default:0) – The value that all unspecifiedRegistersandMemBlockswill initialize to (default0). ForRegisters, this is the value that will be used if the particularRegisterdoesn’t have a specifiedreset_value, and isn’t found in theregister_value_map.block (
Block, default:None) – The hardwareBlockto be simulated (which might be of typePostSynthBlock). Defaults to the working_block.
- inspect(w)[source]¶
Get the value of a
WireVectorin the currentSimulationcycle.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 theWireVectorto inspect (passing in aWireVectorinstead of a name is deprecated).- Raises:
KeyError – If
wdoes not exist in theSimulation.- Return type:
- Returns:
The value of
win the currentSimulationcycle.
- inspect_mem(mem)[source]¶
Get
MemBlockvalues in the currentSimulationcycle.Note
This returns the current contents of the
MemBlock. Modifying the returneddictwill modify theSimulation’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)]
- step(provided_inputs=None)[source]¶
Take the simulation forward one cycle.
stepcauses theBlockto be updated as follows, in order:Registersare updated with theirnextvalues computed at the end of the previous cycle.Inputsand these newRegistervalues propagate through the combinational logicMemBlocksare updatedThe
nextvalues of theRegistersare saved for use at the beginning of the next cycle.
All
Inputwires must be in theprovided_inputs.Example: if we have
Inputsnamedaandx, we can call:sim.step({'a': 1, 'x': 23})
to simulate a cycle where
a == 1andx == 23respectively.
- 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
Ncycles, based onprovided_inputsfor each cycle.All
Inputwires must be inprovided_inputs. Additionally, the length of the array of provided values for eachInputmust be the same.When
nstepsis specified, then it must be less than or equal to the number of values supplied for eachInputwhenprovided_inputsis non-empty. Whenprovided_inputsis empty (which may be a legitimate case for a design that takes noInput), thennstepswill be used. Whennstepsis not specified, then the simulation will take the number of steps equal to the number of values supplied for eachInput.Example: if we have
InputsnamedaandbandOutputo, we can call:sim.step_multiple({'a': [0,1], 'b': [23,32]}, {'o': [42, 43]})
to simulate 2 cycles. In the first cycle,
aandbtake on0and23, respectively, andois expected to have the value42. In the second cycle,aandbtake on1and32, respectively, andois expected to have the value43.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,
aandbtake on0and0, respectively. In the second cycle, they take on1and1, respectively.If a design has no
Inputs, usenstepsto 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 mappingWireVectorsto their values forNsteps.expected_outputs (
dict[str,list[int]] |None, default:None) – A dictionary mappingWireVectorsto their expected values forNsteps; 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 toNone, meaning step for each supplied input value inprovided_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 toFalse).
-
tracer:
SimulationTrace¶ Stores the simulation results for each cycle.
traceris typically used to render simulation waveforms withrender_trace(), for example:sim = pyrtl.Simulation() sim.step_multiple(nsteps=10) sim.tracer.render_trace()
See
SimulationTracefor 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.
FastSimulationre-implementsSimulation, with slower start-up and faster execution. This can be a good trade-off when simulating large circuits, or simulating many cycles.FastSimulationis a drop-in replacement forSimulation, so the two classes share the same interface. SeeSimulationfor 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
FastSimulationandSimulationare nearly identical, so only the differences are described here. SeeSimulation.__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 aFastSimulationwill not be reflected in theFastSimulation.In addition to
Simulation.__init__()’s arguments, this constructor additionally takes:
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.
CompiledSimulationprovides significant execution speed improvements overFastSimulation, at the cost of even longer start-up time. Generally this will do better thanFastSimulationfor simulations requiring over 1000 steps.CompiledSimulationis not built to be a debugging tool, though it may help with debugging. Note that onlyInputandOutputwires can be traced withCompiledSimulation.Note
For very large circuits,
FastSimulationcan sometimes be a better choice thanCompiledSimulationbecauseCompiledSimulationwill generate an extremely large.cfile, which can take prohibitively long to compile and optimize.FastSimulationwill generate an extremely large.pyfile, 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/amd64arm64/aarch64mips64(untested)
default_valueis currently only implemented forRegisters, notMemBlocks.CompiledSimulationis a drop-in replacement forSimulation, so the two classes share the same interface. SeeSimulationfor 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.
Simulationwrites data from each simulation cycle to itsSimulation.tracer, which is an instance ofSimulationTrace.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) –Blockcontaining 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.
- 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 tostdout.include_clock (default:
False) – Boolean specifying if the implicitclkshould be included. Defaults toFalse.
- 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. IfNone, 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 includehex(),oct(),bin(), andstr(for decimal),val_to_signed_integer()(for signed decimal) or the function returned byenum_name()(forIntEnum). Defaults tohex().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 argumentrepr_funcwill be used instead.segment_size (
int, default:1) – Traces are broken in the segments of this number of cycles.
- pyrtl.enum_name(EnumClass)[source]¶
Returns a function that returns the name of an
enum.IntEnumvalue.Use
enum_nameas arepr_funcorrepr_per_nameforSimulationTrace.render_trace()to displayenum.IntEnumnames 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
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_RENDERERenvironment variable to change the default renderer. See the documentation forRendererConstants’ subclasses for valid values ofPYRTL_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 ofRendererConstantsthat 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.
- class pyrtl.simulation.PowerlineRendererConstants[source]¶
Bases:
Utf8RendererConstantsPowerline renderer constants. Font must include powerline glyphs.
This render’s appearance is the most similar to a traditional logic analyzer. Single-bit
WireVectorsare rendered as square waveforms, with vertical rising and falling edges. Multi-bitWireVectorvalues 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_RENDERERenvironment variable topowerline:export PYRTL_RENDERER=powerline
- class pyrtl.simulation.Utf8RendererConstants[source]¶
Bases:
RendererConstantsUTF-8 renderer constants. These should work in most terminals.
Single-bit
WireVectorsare rendered as square waveforms, with vertical rising and falling edges. Multi-bitWireVectorvalues are rendered in reverse-video rectangles.This is the default renderer on non-Windows platforms.
Enable this renderer by default by setting the
PYRTL_RENDERERenvironment variable toutf-8:export PYRTL_RENDERER=utf-8
- class pyrtl.simulation.Utf8AltRendererConstants[source]¶
Bases:
RendererConstantsAlternative UTF-8 renderer constants.
Single-bit
WireVectorsare rendered as waveforms with sloped rising and falling edges. Multi-bitWireVectorvalues 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_RENDERERenvironment variable toutf-8-alt:export PYRTL_RENDERER=utf-8-alt
- class pyrtl.simulation.Cp437RendererConstants[source]¶
Bases:
RendererConstantsCode page 437 renderer constants (for windows
cmdcompatibility).Single-bit
WireVectorsare rendered as square waveforms, with vertical rising and falling edges. Multi-bitWireVectorvalues 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_RENDERERenvironment variable tocp437:export PYRTL_RENDERER=cp437
- class pyrtl.simulation.AsciiRendererConstants[source]¶
Bases:
RendererConstants7-bit ASCII renderer constants. These should work anywhere.
Single-bit
WireVectorsare rendered as waveforms with sloped rising and falling edges. Multi-bitWireVectorvalues are rendered between vertical bars.Enable this renderer by default by setting the
PYRTL_RENDERERenvironment variable toascii:export PYRTL_RENDERER=ascii