In this example we describe how ConditionalUpdate 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 [ ]:
import pyrtl
pyrtl.reset_working_block()
In [ ]:
token_in = pyrtl.Input(1, 'token_in')
req_refund = pyrtl.Input(1, 'req_refund')
dispense = pyrtl.Output(1, 'dispense')
refund = pyrtl.Output(1, 'refund')
state = pyrtl.Register(3, 'state')
First new step, let's enumerate a set of constant to serve as our states
In [ ]:
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 conditional on some input is a pretty fundamental operation in hardware design. PyRTL provides a class "ConditionalUpdate" 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 value in the context of a condition, and update 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: ConditionalUpdate 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.
In [ ]:
with pyrtl.conditional_assignment:
with req_refund: # signal of highest precedence
state.next |= REFUND
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
# 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
In [ ]:
dispense <<= state == DISPENSE
refund <<= state == REFUND
Now let's build and test our state machine.
In [ ]:
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. Recall that the sim.step method takes a dictionary mapping inputs to their values. We could just specify the input set directly as a dictionary but it gets pretty ugly -- let's use some python to parse them up.
In [ ]:
sim_inputs = {
'token_in': '0010100111010000',
'req_refund': '1100010000000000'
}
for cycle in range(len(sim_inputs['token_in'])):
sim.step({w: int(v[cycle]) for w, v in sim_inputs.items()})
Also, to make our input/output easy to reason about let's specify an order to the traces
In [ ]:
sim_trace.render_trace(trace_list=['token_in', 'req_refund', 'state', 'dispense', 'refund'])