Coding an FSM: states, encodings, the default
Code a state machine that has an answer for every value its register can hold, choose an encoding and measure it, and know what the synthesizer does to both.
- Lesson
- R3.2
- Time
- 30 min
- Runs on
- Your browser
- Check
- 5 questions
A five-state sequencer kept its state in three bits, so three of the eight codes had no name. An upset flipped one bit of RUN (010) and left 110 in the register. The case had no arm for 110, the next state was "stay", and the machine ignored every new job until reset.
- Cycle 1
Edge 7:
startwas high, so the state is LOAD andloadis 1. - Cycle 2
Edge 8: RUN, code 010. The upset follows: bit 2 flips and the register holds 110.
- Cycle 3
Edge 9: no arm matches 110, so
st_d = st_qstands. The register reloads 110. - Cycle 6
Edges 10 to 12:
startis high and the machine still holds 110. Only reset gets it out.
Measured on the bench, cycles e6 to e12. The run prints "2 jobs started": the third never begins.
Every code the register can hold
A state register of N bits can hold 2^N codes. The enum names five of them; the hardware does not know which ones have names. The next-state logic must say what happens in every code, including the ones no transition produces. That is the job of the default arm.
No default arm
st_d = st_q;
case (st_q)
...
DONE: st_d = IDLE;
endcase
In 101, 110 or 111 nothing matches, st_d keeps st_q, and the machine stays there.
A default arm
st_d = st_q;
case (st_q)
...
DONE: st_d = IDLE;
default: st_d = IDLE;
endcase
Any unnamed code goes to IDLE at the next edge. The upset costs one job, not the chip.
- A job
IDLE, LOAD, RUN, DRAIN, DONE and back: the only codes a correct run ever visits.
- An upset
Only an upset, a glitch or a missed reset puts the register in 101, 110 or 111. The default arm is the only way back.
Read the diagram as text
- IDLE 000: waits for start
- LOAD 001: one cycle
- RUN 010: until last
- DRAIN 011: one cycle
- DONE 100: one cycle
- 101, 110, 111: no name, no transition into them
- IDLE 000 → LOAD 001: start
- LOAD 001 → RUN 010: next edge
- RUN 010 → DRAIN 011: last
- DRAIN 011 → DONE 100: next edge
- DONE 100 → IDLE 000: next edge
- 101, 110, 111 → IDLE 000: default arm
Without the default arm, the dashed box has no way out.
The tools can point at the gap before any upset. Lint (Verilator 5.052) reports %Warning-CASEINCOMPLETE: Case values incompletely covered (example pattern 0x5) on the case line. Written as unique case, the same block makes Icarus print value is unhandled for priority or unique case statement when the register holds 110.
The house style for a state machine
An enum with a logic base type
typedef enum logic [2:0] {...} state_e;The simulator then rejects a bare number assigned to the state, and waveforms show names.A clocked block for the register
st_q <= st_dunder the asynchronous reset. It looks the same in every machine.A combinational block for the next state
st_d = st_q;first, so every path assignsst_dand no latch is possible. Thenunique case (st_q)with one arm per state and adefaultarm back to IDLE.Outputs from the state
Assigns or a third block decode the outputs. Moore or Mealy is the brief's choice (R3.1).
A one-block machine, with the next state and the outputs inside the clocked block, is legal. Its outputs are all registered, and its next-state logic is hidden among register updates. Reviews prefer separate blocks because each one does one job.
Encodings, and what the synthesizer did with yours
Binary numbers the states 0, 1, 2, ...: 3 flops for 5 states. One-hot gives each state its own flop, set only in that state: 5 flops, with outputs that are single register bits. Gray changes one bit per step and suits a state read by another clock domain (R6).
Press Synthesize on the lab and the report says 5 flops, although the enum has 3 bits. Yosys 0.70 recognized the state machine and re-encoded it. Its log, from a verbose run of the same flow, says so.
Found FSM state register seq5.st_q.
Recoding FSM `$fsm$\st_q$23' from module `\seq5' using `auto' encoding:
mapping auto encoding to `one-hot` for this FSM.
000 -> ----1
100 -> ---1-
010 -> --1--
001 -> -1---
011 -> 1----With and without the default arm, Yosys 0.70 wrote the same 11-cell netlist: the arm for codes the machine cannot reach was dropped. Simulated as gates, that netlist failed in a new way. When an upset cleared its one set bit, the state stayed 00000 and start never moved it.
The attribute (* fsm_encoding = "none" *) on the state register tells Yosys to leave the machine alone. Then the code you wrote is the circuit you get, default arm included.
| seq5 written as | Flops | Cells | Levels of logic |
|---|---|---|---|
| binary, no default arm, encoding kept | 3 | 17 | 4 |
| binary, with the default arm, encoding kept | 3 | 13 | 2 |
| one-hot, with the default arm, encoding kept | 5 | 27 | 5 |
| any of these without the attribute (re-encoded one-hot) | 5 | 11 | 2 |
Measured with Yosys 0.70 on this flow. "Encoding kept" means (* fsm_encoding = "none" *) on st_q. The default arm made the kept binary machine smaller.
The lab
| Port | Direction | Width | Meaning |
|---|---|---|---|
| clk | input | 1 | clock |
| rst_n | input | 1 | reset, asynchronous, active-low |
| start | input | 1 | start: LOAD one cycle, RUN until last, DRAIN one cycle, DONE one cycle, IDLE |
| last | input | 1 | ends RUN |
| load | output | 1 | Moore: load in LOAD |
| run | output | 1 | Moore: run in RUN |
| done | output | 1 | Moore: done in DONE |
- Clock
- rising edge
- Reset
rst_nasynchronous, active-low, state IDLE- Corner cases
- any unnamed state code returns to IDLE at the next edge
Run the lab: two jobs, the upset, then 110 until the end and "2 jobs started". Press Lint and read the CASEINCOMPLETE warning. Add default: st_d = IDLE; and run again: IDLE at e9, LOAD at e10, "3 jobs started". Synthesize: still 11 cells and 5 flops. Now write (* fsm_encoding = "none" *) in front of state_e st_q;: 13 cells, 3 flops, 2 levels. Last, change the enum to one-hot (IDLE = 5'b00001, ...): the bench still recovers, and Synthesize reports 27 cells, 5 flops, 5 levels.
Asked in interviews
Binary or one-hot, and when?
Binary uses the fewest flops; one-hot uses one flop per state and makes each output a register bit. Which one is smaller or shallower depends on the machine and the tool, so measure. Here the kept one-hot was larger.
What happens when the machine lands in an illegal state?
Whatever the next-state logic says for that code. With a default arm it returns to a known state. Check that the arm survives synthesis: a tool that re-encodes the machine can drop it.
Why an enum for the state?
The enum names the states and limits what can be assigned, so a typo in a constant is an error. Waveforms show names. Give it a logic base type of the right width.
Try it
The first job runs IDLE, LOAD, RUN, DRAIN, DONE, IDLE (edges 2 to 6). The second reaches RUN at edge 8, then the upset writes 110. From edge 9 on the register holds 110 and the request for a third job is ignored.
With the default arm, e9 shows 000, e10 shows 001 with load = 1, and the summary reads "3 jobs started". The bench forces 3'b110; after the switch to one-hot it lands as 00110, two bits set, which is just as illegal.
Synthesize the design to measure its cells and inspect the mapped gates.
e2 start=1 last=0 st=001 load=1 run=0 done=0 e3 start=0 last=0 st=010 load=0 run=1 done=0 e4 start=0 last=1 st=011 load=0 run=0 done=0 e5 start=0 last=0 st=100 load=0 run=0 done=1 e6 start=0 last=0 st=000 load=0 run=0 done=0 e7 start=1 last=0 st=001 load=1 run=0 done=0 e8 start=0 last=0 st=010 load=0 run=1 done=0 upset after e8: st_q = 110 e9 start=0 last=0 st=110 load=0 run=0 done=0 e10 start=1 last=1 st=110 load=0 run=0 done=0 e11 start=1 last=1 st=110 load=0 run=0 done=0 e12 start=1 last=1 st=110 load=0 run=0 done=0 2 jobs startedOriginal design · 11 cells · 5 flip-flops · 0 latches · 2 logic levels. Synthesis uses Yosys; counts depend on the toolchain.
Read included file: tb.sv
`timescale 1ns/1ps
module tb;
logic clk = 0;
always #5 clk = ~clk; // rising edges at 5, 15, 25, ...
logic rst_n = 1; // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
int edge_n = -1;
always @(posedge clk) edge_n = edge_n + 1;
logic start = 0, last = 0;
logic load, run, done;
int jobs = 0;
seq5 dut (.clk(clk), .rst_n(rst_n), .start(start), .last(last), .load(load), .run(run), .done(done));
// Advance to the next rising edge, wait for the design to settle, then print one line.
task automatic tick();
@(posedge clk); #1;
if (load) jobs++;
if (edge_n >= 2) $display("e%0d start=%b last=%b st=%b load=%b run=%b done=%b", edge_n, start, last, dut.st_q, load, run, done);
endtask
initial begin
#1 rst_n = 0; tick(); tick(); rst_n = 1; // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
start = 1; tick(); start = 0; // e2: LOAD
tick(); // e3: RUN
last = 1; tick(); last = 0; // e4: DRAIN
tick(); tick(); // e5: DONE, e6: IDLE
start = 1; tick(); start = 0; // e7: LOAD
tick(); // e8: RUN
#2 force dut.st_q = 3'b110; // an upset: bit 2 of RUN (010) flips
#1 release dut.st_q;
$display("upset after e8: st_q = 110");
tick(); // e9
start = 1; last = 1; // ask for a third job
tick(); tick(); tick(); // e10 to e12
$display("%0d jobs started", jobs);
$finish(0); // quiet: no "$finish called" line
end
endmodule
Check yourself
Remember
- An N-bit state register can hold 2^N codes; the next-state logic needs an answer for all of them, and the
defaultarm gives it. - House style: an enum with a logic base type, a clocked block for
st_q, and analways_combthat starts withst_d = st_qand ends with a default arm. - Binary uses the fewest flops, one-hot one flop per state; measure which is smaller, as the kept one-hot here was 27 cells against 13.
- Yosys 0.70 re-encodes a machine it recognizes as one-hot and drops unreachable codes, default arm included;
(* fsm_encoding = "none" *)keeps the code you wrote.