State machines and sequences

Named states, @fsm, and multi-step transactions with @sequence

Encodings

An @encoding names a set of values over a Bits{N}:

@encoding Phase begin
  IDLE = 0
  RUN  = 1
  DONE = 2
end

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.AMBER
Bits{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).

TipIf you know Verilog

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
end

step 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.

  • @then starts the next step; @then NAME names it. The first step is always START.
  • @when cond at the head of a step holds there until cond is true, and runs the step’s body on that cycle. @then NAME @when cond on one line is the same as on two.
  • @delay n is n idle steps.
  • @repeat n begin ... end is its body n times over, unrolled — so it is steps rather than a counter, and its steps cannot be named.
  • @goto NAME in a body goes to that step, guard and all.
  • After the last step the sequence goes back to START, so a leading @when go re-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
end

The @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.