@encoding Phase begin
IDLE = 0
RUN = 1
DONE = 2
endState machines and sequences
Named states, @fsm, and multi-step transactions with @sequence
Encodings
An @encoding names a set of values over a Bits{N}:
Give the values where they matter — a protocol tag, or a state whose bits drive something directly — or leave them out and let them be numbered. encoding = :onehot or :gray picks a scheme when the values do not matter but the layout does:
@encoding Lamp encoding=:onehot begin
RED; AMBER; GREEN
end
Lamp.AMBERBits{3}(0x2)
Outside a block, a value of the encoding is a number, written Phase.RUN. encname(Phase, v) gives the name of a value, for printing it, and a state may carry a docstring that statedoc reads.
State machines
A register declared with an encoding as its type is a Bits of the encoding’s width that knows its encoding. Inside a block, a bare state name then means the state wherever it meets that register — assigned to it, compared with it, as its default, in a @state label:
@quartz struct Machine
@in go::Bool
@out y::Bits{8}
state::Phase = IDLE
n::Bits{8} = 0
end
@on Machine posedge(clk) begin
@fsm state begin
@state IDLE
go && (state ← RUN)
@state RUN
n ← n + 1
n == 5 && (state ← DONE)
@otherwise
state ← IDLE
end
y ← n
end@fsm expands to an if/elseif chain over the states, so nothing about simulation or emission changes. What it adds is the checking: every state must have a branch or there must be an @otherwise; no state may appear twice; a name the encoding does not define is an error. The Verilog gets a case over localparams, so the state machine reads by name there too.
Six clocks after go, the machine has counted to five and moved on:
let m = step(Machine(); go=true)
for _ in 1:6
m = step(m; go=false)
end
m.state == Phase.DONE, m.y
end(true, Bits{8}(0x05))
A state name that is also a field, an input or a local is an error — a bare name may not mean two things. state ← go ? RUN : IDLE works; so does handing a state to a method that writes it straight into the register: send(DONE) for @method send(s) = (state ← s).
An @encoding is a set of localparams and a @fsm is a case statement, with the checks a linter would do. The one difference to remember: a bare state name only resolves against a register of that encoding’s type. Anywhere else, write Phase.RUN.
Sequences
Logic that does one thing after another over several cycles — a bus transaction, a handshake, a conversion — is a state machine whose states are steps. @sequence writes it as the steps:
@quartz struct Writer
@in go::Bool
@in nack::Bool
@in z::Bits{8}
@out busy::Bool
step::Step
x::Bits{8} = 0
y::Bits{8} = 0
end
@on Writer posedge(clk) begin
@sequence Xfer step begin
@when go # START: wait for go, then
x ← y
@then SEND # next cycle
y ← z
@repeat 4 begin # four cycles of
y ← y + 1
nack && @goto START # unless told to give up
end
@delay 2 # two idle cycles
@then @when z == 5 # wait here until z is 5
x ← 4
end # and back to START
busy ← step != Xfer.START
endstep is a Step register the struct declares, and the only register the sequence uses: it starts at START, goes back to START on a @reset, is sixteen bits in the Julia model, and is emitted as wide as the steps need. A Bits{N} field serves as well, when it is wide enough. A step ends where @then, @delay or @repeat starts the next; the statements between are one step: one cycle, all writes in parallel, then on to the next.
@thenstarts the next step;@then NAMEnames it. The first step is alwaysSTART.@when condat the head of a step holds there untilcondis true, and runs the step’s body on that cycle.@then NAME @when condon one line is the same as on two.@delay nisnidle steps.@repeat n begin ... endis its bodyntimes over, unrolled — so it is steps rather than a counter, and its steps cannot be named.@goto NAMEin a body goes to that step, guard and all.- After the last step the sequence goes back to
START, so a leading@when gore-arms it.
Xfer is an @encoding of the steps, made by the block: Xfer.START, Xfer.SEND, and Xfer.step_2… for the unnamed ones. Inside the body a label is bare; outside it — in another block, or in a test — it is written with the sequence’s name. Two clocks after go, the sequence is at SEND:
let m = step(Writer(); go=true, nack=false, z=Bits{8}(9))
m = step(m; go=false, nack=false, z=Bits{8}(9))
m.step == Xfer.SEND, m.busy
end(false, true)
The trace shows the step by name, and the Verilog is a case over localparams with a default that returns to START, so a register value that is no step recovers.
A UART transmitter
The transmitter from the introduction is a sequence. A byte goes out as a start bit, eight data bits, a parity bit and a stop bit, each held for one bit time. The bit time is a Timeout, reloaded at every step, so the sequence waits on expired rather than counting; and send is sampled into an Edge, so a rising edge sends one frame rather than a held level sending frames back to back:
const BIT_TIME = 9 # clocks per bit, less one: 100 kbaud from 1 MHz
@quartz struct UartTx
# interface wires
@in data::Bits{8} # 8-bit data to transmit
@in send::Bool # on rising edge of send
@in rst::Bool active=:low # reset signal, asserted low
@out tx::Bool = true # TX pin of the UART
@out busy::Bool active=:low # busy signal, asserted low
# internal state
step::Step # state machine step
send_e::Edge # edge detector for send input
shift::Bits{8} # transmit shift register
parity::Bool # parity bit
baud_timer::Timeout{7} # timer to control baud rate
end
@on UartTx posedge(clk) begin
@reset(rst) # reset module when rst is asserted
send_e ← send
@sequence Frame step begin
@when rose(send_e) # wait for a send strobe, then
shift ← data
parity ← isodd(popcount(data)) # a Julia function, computed in hardware
tx ← false # start bit
baud_timer ← BIT_TIME
@repeat 8 begin
@when expired(baud_timer) # one bit time later
tx ← shift[0] # a data bit, LSB first
shift ← shift >> 1
baud_timer ← BIT_TIME
end
@then @when expired(baud_timer)
tx ← parity # transmit parity bit
baud_timer ← BIT_TIME
@then @when expired(baud_timer)
tx ← true # stop bit
baud_timer ← BIT_TIME
@then @when expired(baud_timer) # hold it, then back to waiting for send
end
busy ← step != Frame.START
endThe @repeat 8 unrolls into eight steps, one per data bit — twelve in all, so the emitted step register is four bits wide. Run it at its real rate in a simulation and the frame is on the pin:
sim = Simulation(UartTx(); clocks=(clk=1MHz,), watch="tx")
out = @run sim begin
sim.data = Bits{8}('A')
advance_by(5µs)
sim.send = true
advance_by(5µs)
sim.send = false
advance_by(120µs)
end
plot(out.tx)Start bit low, 'A' is 0x41 sent LSB first, an even parity of 0, then the stop bit high.
@sequence expands to an @fsm over its steps, so nothing about simulation or emission changes. @then, @when, @delay and @repeat belong at the top level of the sequence or of a @repeat body; one inside an if is an error.
Next
Pulses, timeouts and edges: Pulse, Timeout and Edge.