A counter can count; a shift register can slide. But most hardware needs to behave — to be
in the middle of something: waiting for a request, half-way through a transaction, holding a traffic
light on yellow. The universal tool for "being in the middle of something" is the
finite state machine (FSM), an idea you have already met as
An FSM in RTL is always the same three pieces of hardware, and good style writes them as three separate blocks:
always_comb block answering "given the
current state and the inputs, where do I go at the next edge?";
Here is a complete traffic-light controller. Note the typedef enum: it gives the
states names, so the code (and the waveform viewer!) says YELLOW instead of
2'b01:
The line next = state; at the top of the comb block is load-bearing: it means every
path through the case assigns next something, so no latch can be
inferred and no transition can be silently forgotten. The default arm is the escape
hatch for states that should never occur — with two bits encoding three states, the fourth encoding
2'b11 exists physically, and a cosmic ray really can put you there.
Before writing the case, a designer sketches the state diagram — and
the RTL above is nothing but this picture transcribed:
Reading rule: circles are states, arrows are transitions, and an arrow is taken at a clock edge
when its condition is true. If no arrow's condition is true, the machine holds its state — which is
exactly what next = state; says in the code. The diagram and the always_comb
block must contain identical information; when they disagree, one of them is a bug.
Two classic flavours, named after their 1950s inventors, differing only in where outputs come from:
go = (state == GREEN) above) — outputs change only at clock edges, clean and
glitch-calm;grant = req && (state == IDLE)) — outputs can react within the same cycle,
one cycle earlier, at the cost of combinational paths running input-to-output straight through the
machine.Practical advice: default to Moore for control outputs (predictable timing), reach for Mealy when that one-cycle head start genuinely matters. Many real controllers are quietly a mixture.
There is also a choice of state encoding — what bit pattern each named state gets:
GREEN = 3'b001, YELLOW = 3'b010, RED = 3'b100) — more
flops but trivially simple next-state logic. FPGA tools love one-hot, because FPGAs are dripping
with flip-flops and starved for logic; ASIC tools often prefer binary. Synthesis tools will
happily re-encode your enum either way — the names in your code do not change.
A serial line (a UART) idles at 1; a frame begins when the sender pulls the line to
0 — the start bit. The receiver's front end is a tiny FSM that must
tell a real start bit from a stray glitch, by sampling the line again half a bit-time later:
Same skeleton, different diagram: this is the whole craft. Design the picture, transcribe the
case, choose Moore or Mealy for each output. Everything from here to a working CPU
controller is this pattern with more circles.
Our two-phase simulator handles FSMs beautifully: the state object holds the state register (plus a
dwell timer), and next() is the always_comb block. Settle, then
snap:
Watch the rhythm: three cycles of RED, four of GREEN, two of
YELLOW, forever. The go column changes only when the state does — the
Moore signature.
always_ff), next-state
logic (always_comb), and output logic;typedef enum; reset puts the machine in a known state;next = state; as the comb block's first line covers every path — no latches, no
forgotten transitions;Surprisingly small. A cache controller might have a dozen states; a PCIe link-training machine, specified in the standard as an actual state diagram, has around twenty-odd substates and is considered a monster. The reason is not that big machines are impossible — it is that nobody can review a 200-state diagram. When a control problem grows, designers split it into several communicating FSMs (one per concern) or move regular structure into counters and datapaths, keeping each machine small enough to draw on one whiteboard. A useful rule of thumb from industry: if your state diagram no longer fits on a page, it is not one machine — it is several machines in a trench coat.
The classic FSM bug: you add a state, but forget one transition arm — some (state, input)
combination that assigns next nothing. Two evils follow. In a block not guarded by a
default assignment, the synthesizer must make next remember its old value:
an inferred latch, exactly the pathology from
CONFIRM until power-off — a real and embarrassingly common way for hardware to hang.
The two-line vaccine: next = state; at the top of the block (every path assigned,
holding is explicit), and a default: arm sending illegal states somewhere safe. Write
both, every time, even when "obviously" unnecessary.