Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assorted render_trace fixes and improvements: #440

Merged
merged 1 commit into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified docs/screenshots/pyrtl-statemachine.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/simtest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Wave Renderer
:members:
:special-members: __init__
:exclude-members: render_ruler_segment, render_val, val_to_str
.. autofunction:: pyrtl.simulation.enum_name
.. autoclass:: pyrtl.simulation.RendererConstants
.. autoclass:: pyrtl.simulation.Utf8RendererConstants
:show-inheritance:
Expand Down
2 changes: 1 addition & 1 deletion examples/example1-combologic.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
# Now all we need to do is print the trace results to the screen. Here we use
# "render_trace" with some size information.
print('--- One Bit Adder Simulation ---')
sim_trace.render_trace(symbol_len=5, segment_size=5)
sim_trace.render_trace(symbol_len=2)

a_value = sim.inspect(a)
print("The latest value of 'a' was: " + str(a_value))
Expand Down
155 changes: 90 additions & 65 deletions examples/example3-statemachine.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
""" Example 3: A State Machine built with conditional_assignment
"""Example 3: A State Machine built with conditional_assignment

In this example we describe how conditional_assignment works in the context
of a vending machine that will dispense an item when it has received 4
tokens. If a refund is requested, it returns the tokens.

In this example we describe how conditional_assignment works in the context of
a vending machine that will dispense an item when it has received 4 tokens.
If a refund is requested, it returns the tokens.
"""

import enum
import pyrtl

token_in = pyrtl.Input(1, 'token_in')
Expand All @@ -13,62 +15,79 @@
refund = pyrtl.Output(1, 'refund')
state = pyrtl.Register(3, 'state')

# First new step, let's enumerate a set of constants to serve as our states
WAIT, TOK1, TOK2, TOK3, DISPENSE, REFUND = [pyrtl.Const(x, bitwidth=3) for x in range(6)]

# Now we could build a state machine using just the registers and logic discussed
# in the earlier examples, but doing operations *conditionally* on some input is a pretty
# fundamental operation in hardware design. PyRTL provides an instance called
# "conditional_assignment" to provide a predicated update to a registers, wires, and memories.
# First new step, let's enumerate a set of constants to serve as our states
class State(enum.IntEnum):
WAIT = 0 # Waiting for first token.
TOK1 = 1 # Received first token, waiting for second token.
TOK2 = 2 # Received second token, waiting for third token.
TOK3 = 3 # Received third token, waiting for fourth token.
DISP = 4 # Received fourth token, dispense item.
RFND = 5 # Issue refund.


# Now we could build a state machine using just the registers and logic
# discussed in the earlier examples, but doing operations *conditionally* on
# some input is a pretty fundamental operation in hardware design. PyRTL
# provides an instance called "conditional_assignment" to provide a predicated
# update to a registers, wires, and memories.
#
# Conditional assignments are specified with a "|=" instead of a "<<=" operator. The
# conditional assignment is only valid in the context of a condition, and updates to those
# values only happens when that condition is true. In hardware this is implemented
# with a simple mux -- for people coming from software it is important to remember that this
# is describing a big logic function, **NOT** an "if-then-else" clause. All of these things will
# execute straight through when "build_everything" is called. More comments after the code.
# Conditional assignments are specified with a "|=" instead of a "<<="
# operator. The conditional assignment is only valid in the context of a
# condition, and updates to those values only happens when that condition is
# true. In hardware this is implemented with a simple mux -- for people coming
# from software it is important to remember that this is describing a big logic
# function, **NOT** an "if-then-else" clause. All of these things will execute
# straight through when "build_everything" is called. More comments after the
# code.
#
# One more thing: conditional_assignment might not always be the best item to use.
# If the update is simple, a regular 'mux(sel_wire, falsecase=f_wire, truecase=t_wire)'
# can be sufficient.

# One more thing: conditional_assignment might not always be the best solution.
# If the update is simple, a regular 'mux(sel_wire, falsecase=f_wire,
# truecase=t_wire)' can be sufficient.
with pyrtl.conditional_assignment:
with req_refund: # signal of highest precedence
state.next |= REFUND
state.next |= State.RFND
with token_in: # if token received, advance state in counter sequence
with state == WAIT:
state.next |= TOK1
with state == TOK1:
state.next |= TOK2
with state == TOK2:
state.next |= TOK3
with state == TOK3:
state.next |= DISPENSE # 4th token received, go to dispense
with pyrtl.otherwise: # token received but in state where we can't handle it
state.next |= REFUND
with state == State.WAIT:
state.next |= State.TOK1
with state == State.TOK1:
state.next |= State.TOK2
with state == State.TOK2:
state.next |= State.TOK3
with state == State.TOK3:
state.next |= State.DISP # 4th token received, go to dispense
with pyrtl.otherwise: # token received in unsupported state
state.next |= State.RFND
# unconditional transition from these two states back to wait state
# NOTE: the parens are needed because in Python the "|" operator is lower precedence
# than the "==" operator!
with (state == DISPENSE) | (state == REFUND):
state.next |= WAIT

dispense <<= state == DISPENSE
refund <<= state == REFUND

# A couple of other things to note: 1) A condition can be nested within another condition
# and the implied hardware is that the left-hand-side should only get that value if ALL of the
# encompassing conditions are satisfied. 2) Only one conditional at each level can be
# true meaning that all conditions are implicitly also saying that none of the prior conditions
# at the same level also have been true. The highest priority condition is listed first,
# and in a sense you can think about each other condition as an "elif". 3) If not every
# condition is enumerated, the default value for the register under those cases will be the
# same as it was the prior cycle ("state.next |= state" in this example). The default for a
# wirevector is 0. 4) There is a way to specify something like an "else" instead of "elif" and
# that is with an "otherwise" (as seen on the line above "state.next <<= REFUND"). This condition
# will be true if none of the other conditions at the same level were also true (for this example
# specifically, state.next will get REFUND when req_refund==0, token_in==1, and state is not in
# TOK1, TOK2, TOK3, or DISPENSE. Finally 5) not shown here, you can update multiple different
# registers, wires, and memories all under the same set of conditionals.

# NOTE: the parens are needed because in Python the "|" operator is lower
# precedence than the "==" operator!
with (state == State.DISP) | (state == State.RFND):
state.next |= State.WAIT

dispense <<= state == State.DISP
refund <<= state == State.RFND

# A few more notes:
#
# 1) A condition can be nested within another condition and the implied
# hardware is that the left-hand-side should only get that value if ALL of
# the encompassing conditions are satisfied.
# 2) Only one conditional at each level can be true meaning that all conditions
# are implicitly also saying that none of the prior conditions at the same
# level also have been true. The highest priority condition is listed first,
# and in a sense you can think about each other condition as an "elif".
# 3) If not every condition is enumerated, the default value for the register
# under those cases will be the same as it was the prior cycle ("state.next
# |= state" in this example). The default for a wirevector is 0.
# 4) There is a way to specify something like an "else" instead of "elif" and
# that is with an "otherwise" (as seen on the line above "state.next <<=
# State.RFND"). This condition will be true if none of the other conditions
# at the same level were also true (for this example specifically,
# state.next will get RFND when req_refund==0, token_in==1, and state is not
# in TOK1, TOK2, TOK3, or DISP.
# 5) Not shown here, but you can update multiple different registers, wires,
# and memories all under the same set of conditionals.

# A more artificial example might make it even more clear how these rules interact:
# with a:
Expand All @@ -85,9 +104,10 @@
sim_trace = pyrtl.SimulationTrace()
sim = pyrtl.Simulation(tracer=sim_trace)

# Rather than just give some random inputs, let's specify some specific 1-bit values.
# To make it easier to simulate it over several steps, we'll use sim.step_multiple,
# which takes in a dictionary mapping each input to its value on each step.
# Rather than just give some random inputs, let's specify some specific 1-bit
# values. To make it easier to simulate it over several steps, we'll use
# sim.step_multiple, which takes in a dictionary mapping each input to its
# value on each step.

sim_inputs = {
'token_in': '0010100111010000',
Expand All @@ -96,15 +116,20 @@

sim.step_multiple(sim_inputs)

# Also, to make our input/output easy to reason about let's specify an order to the traces
sim_trace.render_trace(trace_list=['token_in', 'req_refund', 'state', 'dispense', 'refund'])

# Finally, suppose you want to simulate your design and verify its output matches your
# expectations. sim.step_multiple also accepts as a second argument a dictionary mapping
# output wires to their expected value on each step. If during the simulation the
# actual and expected values differ, it will be reported to you! This might be useful
# if you have a working design which, after some tweaks, you'd like to test for functional
# equivalence, or as a basic sanity check.
# Also, to make our input/output easy to reason about let's specify an order to
# the traces. We also use `enum_name` to display the state names (WAIT, TOK1,
# ...) rather than their numbers (0, 1, ...).
sim_trace.render_trace(
trace_list=['token_in', 'req_refund', 'state', 'dispense', 'refund'],
repr_per_name={'state': pyrtl.enum_name(State)})

# Finally, suppose you want to simulate your design and verify its output
# matches your expectations. sim.step_multiple also accepts as a second
# argument a dictionary mapping output wires to their expected value on each
# step. If during the simulation the actual and expected values differ, it will
# be reported to you! This might be useful if you have a working design which,
# after some tweaks, you'd like to test for functional equivalence, or as a
# basic sanity check.

sim_outputs = {
'dispense': '0000000000001000',
Expand Down
2 changes: 1 addition & 1 deletion examples/example8-verilog.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
'y': random.choice([0, 1]),
'cin': random.choice([0, 1])
})
sim_trace.render_trace(symbol_len=5, segment_size=5)
sim_trace.render_trace(symbol_len=2)


# ---- Exporting to Verilog ----
Expand Down
9 changes: 5 additions & 4 deletions examples/introduction-to-hardware.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,11 @@ def attempt4_hardware_fibonacci(n, req, bitwidth):
sim_trace = pyrtl.SimulationTrace()
sim = pyrtl.Simulation(tracer=sim_trace)

sim.step({'n_in': 5, 'req_in': 1})
sim.step({'n_in': 7, 'req_in': 1})

sim.step({'n_in': 5, 'req_in': 0})
sim.step({'n_in': 0, 'req_in': 0})
while not sim.inspect('done_out'):
sim.step({'n_in': 5, 'req_in': 0})
sim.step({'n_in': 0, 'req_in': 0})

sim_trace.render_trace(trace_list=['n_in', 'req_in', 'fib_out', 'done_out'])
sim_trace.render_trace(
trace_list=['n_in', 'req_in', 'i', 'fib_out', 'done_out'], repr_func=int)
81 changes: 44 additions & 37 deletions examples/renderer-demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,50 @@
"""


def make_clock(n: int):
"""Make a clock signal that inverts every 'n' cycles."""
assert n > 0
first_state = pyrtl.Register(bitwidth=1, name=f'clock_0_{n}',
reset_value=1)
last_state = first_state
for i in range(1, n):
state = pyrtl.Register(bitwidth=1, name=f'clock_{i}_{n}')
state.next <<= last_state
last_state = state

first_state.next <<= ~last_state
return last_state


def make_counter(n: int):
"""Make a counter that increments every 'n' cycles."""
assert n > 0
first_state = pyrtl.Register(bitwidth=8, name=f'counter_0_{n}')
last_state = first_state
for i in range(1, n):
state = pyrtl.Register(bitwidth=8, name=f'counter_{i}_{n}')
state.next <<= last_state
last_state = state

first_state.next <<= last_state + pyrtl.Const(1)
return last_state


make_clock(n=1)
make_clock(n=2)
make_counter(n=1)
make_counter(n=2)

# Simulate 10 cycles.
def make_clock(period: int):
"""Make a clock signal that inverts every `period` cycles."""
assert period > 0

# Build a chain of registers.
first_reg = pyrtl.Register(bitwidth=1, name=f'clock_0_{period}',
reset_value=1)
last_reg = first_reg
for offset in range(1, period):
reg = pyrtl.Register(bitwidth=1, name=f'clock_{offset}_{period}')
reg.next <<= last_reg
last_reg = reg

# The first register's input is the inverse of the last register's output.
first_reg.next <<= ~last_reg
return last_reg


def make_counter(period: int, bitwidth=2):
"""Make a counter that increments every `period` cycles."""
assert period > 0

# Build a chain of registers.
first_reg = pyrtl.Register(bitwidth=bitwidth, name=f'counter_0_{period}')
last_reg = first_reg
for offset in range(1, period):
reg = pyrtl.Register(bitwidth=bitwidth,
name=f'counter_{offset}_{period}')
reg.next <<= last_reg
last_reg = reg

# The first register's input is the last register's output plus 1.
first_reg.next <<= last_reg + pyrtl.Const(1)
return last_reg


make_clock(period=1)
make_clock(period=2)
make_counter(period=1)
make_counter(period=2)

# Simulate 20 cycles.
sim = pyrtl.Simulation()
sim.step_multiple(nsteps=10)
sim.step_multiple(nsteps=20)

# Render the trace with a variety of rendering options.
renderers = {
Expand All @@ -62,7 +69,7 @@ def make_counter(n: int):
for i, name in enumerate(renderers):
constants, notes = renderers[name]
print(f'# {notes}')
print(f'export PYRTL_RENDERER={name}')
print(f'export PYRTL_RENDERER={name}\n')
sim.tracer.render_trace(
renderer=pyrtl.simulation.WaveRenderer(constants),
repr_func=int)
Expand Down
1 change: 1 addition & 0 deletions pyrtl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
from .simulation import Simulation
from .simulation import FastSimulation
from .simulation import SimulationTrace
from .simulation import enum_name
from .compilesim import CompiledSimulation

# block visualization output formats
Expand Down
Loading
Loading