Digital Design / Module 6: Finite state machines
38. Debugging and safe FSMs
Handle stuck states and unused encodings.
See it move
The idea
FSMs fail in a few recognisable ways. They hang in a state, take a wrong branch, or reach an encoding that should never occur. Knowing these patterns makes debugging far faster.
Finding a hang
- Display the state by name in your waveform, not as a number.
- Find the state it got stuck in.
- List what could cause it to leave.
- See which of those signals is missing. It's often a that the machine waits for and never receives, or a pulse that came while it was looking elsewhere.
Missed pulses are especially common. If a one-cycle done pulse arrives while the FSM is in a different state, it is lost for good, and the machine then waits forever. Fix this by holding the signal until it is acknowledged.
Illegal states
A 5-state binary FSM has 3 unused codes. It shouldn't reach them, but a bit upset, a timing failure or a bad reset could put it there. If nothing handles them, the machine can stay stuck until reset. Try the upset button in the animation above.
default: next = IDLE; // recover from any illegal stateOne warning: synthesis may treat unreachable states as and remove this logic. If you need real recovery, use your tool's safe-FSM option, and check the result. A safety-critical design would also raise an error, rather than quietly resume.
Assertions
Assertions catch errors at their origin, not later.
assert property (@(posedge clk) disable iff (!rst_n)
state inside {IDLE, LOAD, RUN, DONE});
assert property (@(posedge clk) disable iff (!rst_n)
(state == RUN) |-> ##[1:1000] (state == DONE));The second one flags a hang automatically.
Common mistake
Treating a hang as a timing issue and slowing down the clock. Trace the logic first. Most hangs are a wrong assumption about when a signal arrives.
Key ideas
- Show state names in waveforms.
- Unused encodings need a recovery path.
- Assertions catch illegal states early.
Your exercise
Find the bug
This code has one deliberate bug. Read the symptom, then click the line you think is wrong.
The fix
3'd4: state <= 3'd0;
default: state <= 3'd0;Why it failed
With no branch for the unused codes, the state register keeps its value, so an upset can hang the machine forever. A default branch gives a way out. In real designs you should also check that synthesis keeps this logic, since it may treat those states as unreachable.
Interview questions
coreWhat happens on an illegal state?
Without a recovery path it may lock up; send it to a safe state, and flag an error where it matters.
basicA 5-state FSM is encoded in binary and, separately, one-hot. How many illegal codes does each encoding have?
Binary uses 3 flops, so there are 8 codes, 5 legal and 3 illegal. One-hot uses 5 flops, so there are 2^5 = 32 codes, of which only the 5 with exactly one bit set are legal, leaving 27 illegal. One-hot is simple to decode, but a single bit flip always produces an illegal code, with zero or two bits set.
basicAn FSM in simulation stops making progress. What are your first debugging steps?
Show the state by name in the waveform, find the state it is stuck in, then list every condition that would make it leave that state. Check which of those signals never arrived. Often it is a handshake that never came, or a one-cycle pulse that arrived while the FSM was in a different state. Tracing the logic comes before any change to the clock or the timing.
coreThis controller sometimes hangs in WAIT forever, but only with a fast peripheral. Why?
always @(posedge clk)
if (rst) state <= IDLE;
else case (state)
IDLE: if (start) state <= REQ;
REQ: state <= WAIT; // req goes out here
WAIT: if (done) state <= IDLE; // done is a 1-cycle pulse
default: state <= IDLE;
endcaseThe done pulse lasts one cycle. A fast peripheral can raise it in the same cycle the FSM is in REQ, and the FSM only looks at done while in WAIT, so that pulse is lost and WAIT never ends. Simulated with done arriving during REQ, the FSM stayed in WAIT indefinitely. The fix is to hold done in a sticky flag until the FSM consumes it, or to have the peripheral hold done until it is acknowledged.
coreThis 5-state counter FSM has no default branch. What happens if a bit flip puts it in state 110?
always @(posedge clk)
if (rst) st <= 3'd0;
else case (st)
3'd0: st <= 3'd1;
3'd1: st <= 3'd2;
3'd2: st <= 3'd3;
3'd3: st <= 3'd4;
3'd4: st <= 3'd0;
endcaseNo case item matches 110, and there is no default, so the state register keeps its value: the machine is stuck at 110 until a reset. In simulation, forcing 110 left it at 110 on every following clock. Adding default: st <= 3'd0 sends it back to 000 on the next clock, after which it cycles normally. Because this is a clocked block, a missing default creates no latch either, so nothing in the logic draws attention to the gap.
coreYou added a default branch that sends illegal states to IDLE. Is that guaranteed to survive synthesis?
Not necessarily. Synthesis tools can see that the illegal codes are unreachable from reset and may treat them as don't-cares, which can remove or simplify the recovery logic. If recovery really matters, use the tool's safe-FSM setting, or equivalent, and check the netlist or the tool report to confirm it was kept. Simulation of the RTL can't tell you this, because it simulates the code, not the optimised gates.
coreWrite a plain Verilog-2005 check that reports an error if the FSM sits in WAIT for more than 20 clocks.
reg [5:0] wait_cnt;
always @(posedge clk)
if (rst || state != WAIT) wait_cnt <= 0;
else begin
wait_cnt <= wait_cnt + 1;
if (wait_cnt == 20)
$display("ERROR %0t: stuck in WAIT", $time);
endCount consecutive cycles in WAIT and reset the count whenever the state is anything else. Print an error when the count reaches the limit. In simulation of the hanging controller, this check fired once the machine had spent over 20 clocks in WAIT, and stayed silent for the fixed version. It catches a hang at its origin instead of many microseconds later, when some downstream check finally notices missing data.
advancedA colleague says an FSM hang went away when they halved the clock frequency, so it must be a timing problem. What do you say?
That is weak evidence. In a functional simulation there are no gate delays, yet a missed-pulse hang still happens at any clock period, because it depends on the order of events in cycles, not on nanoseconds. A slower clock may only change when an external event lands relative to the FSM's states, which can hide the bug instead of fixing it. Trace the logic first: find the stuck state and the exit signal that never came.
advancedFor a safety-critical controller, is it enough for an illegal state to quietly return to IDLE?
Usually not. Quiet recovery hides the fact that something went wrong, such as an upset, a timing failure or a bad reset. A safety design typically goes to a defined safe state, raises an error flag or interrupt, and may log the event. Some designs also use encodings where a single bit flip can be detected, for example one-hot with a check that exactly one bit is set, or added parity, so that the error is noticed at once.