RTL Design Patterns · Chapter 4 · FSM Design
Safe FSMs & Latch-Free Outputs
The single most common FSM bug engineers ship is an inferred latch: a combinational block that computes an output or a next-state but forgets to assign it on some path, so the synthesis tool builds hidden storage that holds a stale value across cycles. The symptom is nasty, intermittent, and order-dependent, and it passes casual simulation. Its close sibling is a machine that has no answer for a state it should never reach, so a glitch or a soft error drives it into an illegal encoding and it locks up forever. Both come from the same omission, and both have the same two-part cure. First, default-first: assign every output and next-state a safe default at the top of the block, then override per state, so a latch is impossible by construction. Second, handle every state: give the case a default arm that recovers any illegal encoding to a known safe state. Built and verified in SystemVerilog, Verilog, and VHDL.
Intermediate16 min readRTL Design PatternsSafe FSMLatch InferenceDefault-FirstIllegal State RecoverySafe State
Chapter 4 · Section 4.5 · FSM Design
1. The Engineering Problem
You are building a small controller — a memory-refresh sequencer, say — with a handful of states and one output, req, that must pulse high for exactly one cycle in the ACCESS state and stay low everywhere else. You write the combinational output logic as a case over the state, list the states you care about, and assign req where it matters. It simulates. req pulses when you expect. It passes review and goes into the block.
Weeks later, integration reports something strange. On most input sequences req behaves. But on one particular path through the machine — enter ACCESS, then take a branch you rarely exercise — req comes up high and stays high into the next state, a state where it should be low, for a cycle or two, until some later assignment happens to clear it. The bug is intermittent, it depends on the exact order of states you visit, and it does not reproduce in the quick directed test that only ever walks the happy path. Someone finally opens the synthesis log and finds the tell: a LATCH inferred for signal req. The output you believed was pure combinational logic is actually a latch, and it has been holding the old value of req on every path where your case forgot to assign it.
That is the first and most common way an FSM goes wrong, and it has nothing to do with your state diagram being incorrect. The transitions are right; the machine visits the right states in the right order. The failure is that a combinational block did not assign an output on every path, and "assign nothing, no clock" is the definition of a latch — the tool built storage to remember the value you left undriven. The second way is subtler and rarer but far more dangerous: your state register is two bits, you use three of the four encodings, and one day a glitch or a soft error flips the register into the fourth — an illegal state your case has no arm for. With no default, the next-state logic leaves next_state unassigned there too (another latch), and the machine is now stuck in a state it can never leave. It will sit there, dead, until a watchdog resets the whole chip.
Both failures are the same omission wearing two faces: a combinational block that does not account for every path and every state. The cure is two disciplines this page makes reflexive. Default-first — assign every output and next-state a safe default at the top of the block — makes the latch impossible. Handle every state — a default (VHDL when others) arm that recovers illegal encodings to a known safe state — makes the lock-up impossible. Everything below builds, verifies, and prices those two disciplines.
// A refresh sequencer. `req` should pulse high ONLY in ACCESS, low everywhere else.
// WRONG — the case assigns req only where it is high; other states leave it UNASSIGNED:
// always @(*) begin
// case (state)
// ACCESS: req = 1'b1; // ...and every other state? req is UNDRIVEN
// endcase // "assign nothing" => LATCH: req HOLDS its old value
// end // -> req stays high into states where it must be low
// Plus: state is 2 bits, only IDLE/ARM/ACCESS used. A glitch into the 4th encoding
// has no case arm and no default => next_state UNDRIVEN there => the FSM LOCKS UP.
// RIGHT — default-first + a recovering default (the pattern this page builds):
// assign a safe default to req and next_state FIRST, then override per state; give
// the case a `default: next_state = IDLE;` so an illegal state RECOVERS to a safe one.2. Mental Model
3. Pattern Anatomy
The FSM is the same three pieces from 4.1 — a state register, next-state logic, and output logic — and "safe" is a property of the two combinational pieces: they must be total (assign everything, everywhere) and recovering (route illegal states home).
The latch-inference mechanism — what a missing assignment actually builds. A combinational block models pure logic: its outputs are a function of its inputs right now, with no memory. The synthesis tool enforces that by requiring every output to be assigned on every path through the block. When it finds a path where an output is not assigned — a state with no case arm, or an arm that assigns some outputs but not others — it faces a contradiction: the language says "combinational," but the code says "on this path, keep the old value." The only hardware that keeps an old value with no clock is a level-sensitive latch, so the tool infers one, gated by the condition that leads to the unassigned path. The output is now stored: it holds its previous value whenever that path is taken, and updates only when a path that does assign it is active. That is the inferred latch — unclocked storage the RTL never asked for, on a signal you believed was combinational.
How a missing default becomes a latch — the inference, step by step
data flowDefault-first — the total-assignment discipline. The cure for the latch is a single, checkable rule: assign every output and next_state a safe default as the first statement of the combinational block, then let the case override per state. After the default line, every signal is driven no matter which arm runs (or if none does), so no path is silent and no latch can be inferred. The default value matters: a one-cycle strobe defaults to 0 (deasserted) and is raised only in the state that pulses it; next_state defaults to the current state (hold) or, in a safe machine, to the safe state. Default-first also makes the code read correctly — the reader sees the deasserted, safe baseline first, then only the deliberate exceptions.
Handle every state — illegal-state recovery and the safe state. A state register of W bits has 2^W encodings; an FSM with M < 2^W legal states leaves 2^W − M encodings illegal — unreachable in normal operation, but reachable by a glitch, a soft error, or an SEU flipping a bit. A safe FSM gives the next-state case a default arm (VHDL when others) that routes any unlisted encoding to a known good state — the safe state, usually IDLE. Now an illegal state is not a dead end: on the next clock edge the machine transitions out of it to the safe state and resumes correct operation. The cost is a little extra next-state logic (the default arm) and, subtly, the requirement that the tool not optimize the illegal states away — because from a pure-function view those encodings are unreachable, a synthesis tool may prune the recovery logic as dead. You defend the safe recovery with a synthesis keep / safe attribute on the state register so the tool preserves the recovery arm and the defined behaviour of the illegal encodings.
Illegal-state recovery — every unused encoding routes home to the safe state
data flowTwo anti-patterns close the anatomy. The first is trusting full_case / parallel_case pragmas to silence the latch: full_case tells the tool "treat this case as complete, no latch," but it does not define what happens on the missing values — so synthesis assumes they never occur and prunes the logic, while simulation (which does not honor the pragma the same way) may see them and behave differently. That sim/synth divergence is worse than the latch it was meant to hide; the safe fix is a real default, never a pragma. The second is a safe state that is unreachable and gets optimized away — the recovery arm you wrote, deleted by the tool because nothing legal reaches it — which is why the keep/safe attribute is part of the pattern, not an optional extra.
4. Real RTL Implementation
This is the core of the page. We build one small controller — a refresh sequencer that walks IDLE → ARM → ACCESS → IDLE, starting when start is high and pulsing a one-cycle req strobe in ACCESS — and show three things in SystemVerilog, Verilog, and VHDL, each with an RTL block and a self-checking clocked testbench: (a) a latch-free FSM (default-first outputs plus a total next-state block), (b) a safe FSM with illegal-state recovery (default / when others → the safe state, with the synth-keep note), and (c) the missing-default latch bug beside its latch-free / safe fix (buggy vs fixed). Using one machine across all three makes the contrast clean: only the completeness of the combinational blocks changes, never the intended behaviour.
Two disciplines run through every block. The reset is a synchronous, active-high rst that returns the machine to IDLE (the safe state). The combinational blocks use full-assignment (= / VHDL variable-or-signal assignment) and assign a default first: next_state = state (or the safe state) and every output to its deasserted value, at the very top — so no path is silent and no latch can be inferred.
4a. The latch-free FSM — default-first outputs and a total next-state block
The latch-free machine opens its combinational block with defaults for both next_state and req, then the case overrides only where needed. Because the very first two statements drive every signal, no state arm can leave anything undriven, and the block is combinational by construction.
module seq_latchfree (
input logic clk,
input logic rst, // synchronous, active-high
input logic start, // kicks the sequence off from IDLE
output logic req // 1-cycle strobe: high ONLY in ACCESS
);
typedef enum logic [1:0] {IDLE, ARM, ACCESS} state_t;
state_t state, next_state;
// COMBINATIONAL next-state + output. DEFAULT-FIRST is the whole safety of this
// block: next_state and req are BOTH assigned before the case runs, so every path
// drives every signal and NO latch can be inferred on either one.
always_comb begin
next_state = state; // default: hold -> next_state never undriven
req = 1'b0; // default: deasserted -> req never undriven
unique case (state)
IDLE: if (start) next_state = ARM;
ARM: next_state = ACCESS;
ACCESS: begin
req = 1'b1; // the ONE place req is high (Moore output)
next_state = IDLE;
end
endcase
end
// STATE register: synchronous reset to the safe state.
always_ff @(posedge clk)
if (rst) state <= IDLE;
else state <= next_state;
endmoduleThe self-checking testbench walks the machine through a full cycle and, crucially, includes the check that catches a latch: after ACCESS (where req is high) it advances to a state where req must be low and asserts that req did not hold its high value. On a latch-free machine req drops immediately; on a latched one it would linger — so this check is exactly what a missing default would fail.
module seq_latchfree_tb;
logic clk = 1'b0, rst, start, req;
int errors = 0;
seq_latchfree dut (.clk(clk), .rst(rst), .start(start), .req(req));
always #5 clk = ~clk;
task automatic step(input logic s, input logic exp_req, input string tag);
start = s;
@(posedge clk); #1; // sample after the edge that loaded the state
assert (req === exp_req)
else begin $error("%s: req=%b exp=%b (latch/hold?)", tag, req, exp_req); errors++; end
endtask
initial begin
rst = 1'b1; start = 1'b0;
@(posedge clk); #1; rst = 1'b0;
step(1'b1, 1'b0, "IDLE->ARM"); // in IDLE: req low
step(1'b0, 1'b0, "ARM: req low"); // in ARM: req low
step(1'b0, 1'b1, "ACCESS: req HIGH");// in ACCESS: req pulses high
// THE LATCH CHECK: next state is IDLE, where req MUST be low. A latched req
// would hold the ACCESS high value here. Assert it dropped.
step(1'b0, 1'b0, "back in IDLE: req must NOT hold high");
if (errors == 0) $display("PASS: req pulses once in ACCESS, no stale hold (latch-free)");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog form is the same machine with localparam state codes and reg/wire typing. The two default assignments (next_state = state; req = 1'b0;) at the top of the always @(*) block do the identical job.
module seq_latchfree (
input wire clk,
input wire rst,
input wire start,
output reg req
);
localparam IDLE = 2'd0, ARM = 2'd1, ACCESS = 2'd2;
reg [1:0] state, next_state;
// DEFAULT-FIRST: both signals driven before the case -> no latch on either.
always @(*) begin
next_state = state; // default: hold
req = 1'b0; // default: deasserted
case (state)
IDLE: if (start) next_state = ARM;
ARM: next_state = ACCESS;
ACCESS: begin
req = 1'b1;
next_state = IDLE;
end
default: next_state = IDLE; // (also recovers illegal codes — see 4b)
endcase
end
always @(posedge clk)
if (rst) state <= IDLE;
else state <= next_state;
endmodule module seq_latchfree_tb;
reg clk, rst, start;
wire req;
integer errors;
seq_latchfree dut (.clk(clk), .rst(rst), .start(start), .req(req));
initial clk = 1'b0;
always #5 clk = ~clk;
task check(input exp_req, input [127:0] tag);
begin
@(posedge clk); #1;
if (req !== exp_req) begin
$display("FAIL: %0s req=%b exp=%b (latch/hold?)", tag, req, exp_req);
errors = errors + 1;
end
end
endtask
initial begin
errors = 0;
rst = 1'b1; start = 1'b0;
@(posedge clk); #1; rst = 1'b0;
start = 1'b1; check(1'b0, "IDLE"); // req low in IDLE
start = 1'b0; check(1'b0, "ARM"); // req low in ARM
check(1'b1, "ACCESS"); // req pulses high
// Latch check: now IDLE again, req must have DROPPED (a latch would hold high).
check(1'b0, "IDLE-after");
if (errors == 0) $display("PASS: req pulses once, no stale hold (latch-free)");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the combinational logic is a process sensitive to all; the default assignments to next_state and req are the first two statements, and process(all) guarantees the sensitivity list can never be the source of a phantom latch either.
library ieee;
use ieee.std_logic_1164.all;
entity seq_latchfree is
port (
clk : in std_logic;
rst : in std_logic; -- synchronous, active-high
start : in std_logic;
req : out std_logic -- 1-cycle strobe, high in ACCESS
);
end entity;
architecture rtl of seq_latchfree is
type state_t is (IDLE, ARM, ACCESS);
signal state, next_state : state_t;
begin
-- COMBINATIONAL: default-first. next_state and req are both assigned before the
-- case, so every path drives both -> no latch can be inferred on either.
comb : process (all)
begin
next_state <= state; -- default: hold
req <= '0'; -- default: deasserted
case state is
when IDLE => if start = '1' then next_state <= ARM; end if;
when ARM => next_state <= ACCESS;
when ACCESS => req <= '1'; next_state <= IDLE;
end case;
end process;
-- STATE register: synchronous reset to the safe state.
reg_p : process (clk)
begin
if rising_edge(clk) then
if rst = '1' then state <= IDLE; else state <= next_state; end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity seq_latchfree_tb is
end entity;
architecture sim of seq_latchfree_tb is
signal clk : std_logic := '0';
signal rst : std_logic := '1';
signal start : std_logic := '0';
signal req : std_logic;
begin
dut : entity work.seq_latchfree port map (clk => clk, rst => rst, start => start, req => req);
clkgen : process
begin
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end process;
stim : process
procedure check(exp : std_logic; tag : string) is
begin
wait until rising_edge(clk); wait for 1 ns;
assert req = exp report "req mismatch (" & tag & ") -- latch/hold?" severity error;
end procedure;
begin
rst <= '1'; start <= '0';
wait until rising_edge(clk); wait for 1 ns; rst <= '0';
start <= '1'; check('0', "IDLE");
start <= '0'; check('0', "ARM");
check('1', "ACCESS");
-- Latch check: back in IDLE, req must have dropped (a latch would hold '1').
check('0', "IDLE-after");
report "seq_latchfree self-check complete" severity note;
wait;
end process;
end architecture;4b. The safe FSM — illegal-state recovery to a known safe state
The latch-free machine is correct on the states it lists, but it has three legal states in a two-bit register — the fourth encoding is illegal. The safe machine adds a default (VHDL when others) arm to the next-state logic that routes any illegal encoding to IDLE, the safe state, so a glitch or SEU into that fourth code recovers on the next edge. The synthesis keep on the state register is what stops the tool pruning that recovery as dead logic.
// The FSM keep/safe attribute tells synthesis to PRESERVE the illegal-state decoding
// and the recovery arm (otherwise, being unreachable in the legal machine, it can be
// optimized away). Tool-specific: e.g. (* fsm_safe_state = "reset_state" *) or a keep.
module seq_safe (
input logic clk,
input logic rst,
input logic start,
output logic req
);
typedef enum logic [1:0] {IDLE, ARM, ACCESS} state_t;
(* fsm_encoding = "safe" *) state_t state, next_state; // synth: build recovery logic
always_comb begin
next_state = IDLE; // DEFAULT = the SAFE state (not just hold):
// any path that falls through recovers home
req = 1'b0; // default-first output -> latch-free
unique0 case (state) // unique0: no assertion if state is illegal
IDLE: next_state = start ? ARM : IDLE;
ARM: next_state = ACCESS;
ACCESS: begin req = 1'b1; next_state = IDLE; end
default: next_state = IDLE; // ILLEGAL state -> recover to SAFE state
endcase
end
always_ff @(posedge clk)
if (rst) state <= IDLE;
else state <= next_state;
endmoduleNote the two safety choices: the default assignment to next_state is now the safe state IDLE (not merely state), so even a fall-through recovers; and the explicit default: arm documents the illegal-state recovery. unique0 (rather than unique) is deliberate — it does not fire a simulation assertion when state is one of the illegal encodings, because the whole point is that illegal states can occur and must be handled, not flagged as impossible. The testbench forces the state register into the illegal encoding and asserts the machine returns to IDLE.
module seq_safe_tb;
logic clk = 1'b0, rst, start, req;
int errors = 0;
seq_safe dut (.clk(clk), .rst(rst), .start(start), .req(req));
always #5 clk = ~clk;
initial begin
rst = 1'b1; start = 1'b0;
@(posedge clk); #1; rst = 1'b0;
@(posedge clk); #1;
// Inject a fault: force the state register into the ILLEGAL 4th encoding (2'b11),
// as a glitch/SEU would. Then release the force and let the machine run.
force dut.state = 2'b11; // sim-only: model an SEU into an illegal code
#1;
release dut.state;
// On the NEXT edge the default arm must route next_state to IDLE (the safe state).
@(posedge clk); #1;
assert (dut.state === dut.IDLE)
else begin $error("no recovery: state=%b exp IDLE (illegal-state lock-up!)", dut.state); errors++; end
// And after recovery the machine must resume normal operation.
start = 1'b1; @(posedge clk); #1; // IDLE -> ARM
start = 1'b0; @(posedge clk); #1; // ARM -> ACCESS
assert (req === 1'b1)
else begin $error("post-recovery req wrong: req=%b exp 1", req); errors++; end
if (errors == 0) $display("PASS: illegal state recovers to IDLE, machine resumes");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog safe machine adds the same default: next_state = IDLE; recovery arm and a keep/fsm synthesis attribute; the testbench uses force/release on the state register the same way.
module seq_safe (
input wire clk,
input wire rst,
input wire start,
output reg req
);
localparam IDLE = 2'd0, ARM = 2'd1, ACCESS = 2'd2;
// (* fsm_encoding = "safe" *) / keep -> preserve the illegal-state recovery logic.
(* fsm_encoding = "safe" *) reg [1:0] state, next_state;
always @(*) begin
next_state = IDLE; // default = SAFE state -> fall-through recovers
req = 1'b0; // default-first output -> latch-free
case (state)
IDLE: next_state = start ? ARM : IDLE;
ARM: next_state = ACCESS;
ACCESS: begin req = 1'b1; next_state = IDLE; end
default: next_state = IDLE; // 2'b11 (illegal) -> recover to SAFE state
endcase
end
always @(posedge clk)
if (rst) state <= IDLE;
else state <= next_state;
endmodule module seq_safe_tb;
reg clk, rst, start;
wire req;
integer errors;
seq_safe dut (.clk(clk), .rst(rst), .start(start), .req(req));
initial clk = 1'b0;
always #5 clk = ~clk;
initial begin
errors = 0;
rst = 1'b1; start = 1'b0;
@(posedge clk); #1; rst = 1'b0;
@(posedge clk); #1;
// Model an SEU: force the register into the illegal 4th code, then release.
force dut.state = 2'b11;
#1; release dut.state;
@(posedge clk); #1; // default arm must route to IDLE
if (dut.state !== 2'd0) begin
$display("FAIL: no recovery, state=%b exp IDLE (lock-up!)", dut.state);
errors = errors + 1;
end
// Resume normal operation after recovery.
start = 1'b1; @(posedge clk); #1; // IDLE -> ARM
start = 1'b0; @(posedge clk); #1; // ARM -> ACCESS
if (req !== 1'b1) begin
$display("FAIL: post-recovery req=%b exp 1", req);
errors = errors + 1;
end
if (errors == 0) $display("PASS: illegal state recovers to IDLE, machine resumes");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the recovery is the when others => arm — the exact counterpart of the Verilog default — and the safe-state attribute is a synthesis-tool attribute on the state signal. Because the enumerated state_t has only three values, the testbench models the illegal code by driving a std_logic_vector state register instead; here the machine uses an explicit two-bit encoding so an illegal code is representable and when others can catch it.
library ieee;
use ieee.std_logic_1164.all;
entity seq_safe is
port (
clk : in std_logic;
rst : in std_logic;
start : in std_logic;
req : out std_logic
);
end entity;
architecture rtl of seq_safe is
-- Explicit 2-bit encoding so an ILLEGAL code ("11") is representable and catchable.
constant IDLE : std_logic_vector(1 downto 0) := "00";
constant ARM : std_logic_vector(1 downto 0) := "01";
constant ACCESS : std_logic_vector(1 downto 0) := "10";
signal state, next_state : std_logic_vector(1 downto 0);
-- synth attribute (tool-specific) preserves the recovery for the unused "11" code:
attribute fsm_encoding : string;
attribute fsm_encoding of state : signal is "safe";
begin
comb : process (all)
begin
next_state <= IDLE; -- default = SAFE state
req <= '0'; -- default-first output
case state is
when IDLE => if start = '1' then next_state <= ARM; else next_state <= IDLE; end if;
when ARM => next_state <= ACCESS;
when ACCESS => req <= '1'; next_state <= IDLE;
when others => next_state <= IDLE; -- "11" illegal -> recover to SAFE
end case;
end process;
reg_p : process (clk)
begin
if rising_edge(clk) then
if rst = '1' then state <= IDLE; else state <= next_state; end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity seq_safe_tb is
end entity;
architecture sim of seq_safe_tb is
signal clk : std_logic := '0';
signal rst : std_logic := '1';
signal start : std_logic := '0';
signal req : std_logic;
constant IDLE : std_logic_vector(1 downto 0) := "00";
begin
dut : entity work.seq_safe port map (clk => clk, rst => rst, start => start, req => req);
clkgen : process
begin
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end process;
stim : process
begin
rst <= '1'; start <= '0';
wait until rising_edge(clk); wait for 1 ns; rst <= '0';
wait until rising_edge(clk); wait for 1 ns;
-- Model an SEU: force the register into the illegal "11" code, then release.
<< signal dut.state : std_logic_vector(1 downto 0) >> <= force "11";
wait for 1 ns;
<< signal dut.state : std_logic_vector(1 downto 0) >> <= release;
wait until rising_edge(clk); wait for 1 ns; -- when others must route to IDLE
assert dut.state = IDLE
report "no recovery: state /= IDLE (illegal-state lock-up!)" severity error;
-- Resume normal operation after recovery.
start <= '1'; wait until rising_edge(clk); wait for 1 ns; -- IDLE -> ARM
start <= '0'; wait until rising_edge(clk); wait for 1 ns; -- ARM -> ACCESS
assert req = '1'
report "post-recovery req /= 1" severity error;
report "seq_safe self-check complete" severity note;
wait;
end process;
end architecture;4c. The missing-default latch bug — buggy vs fixed, in all three HDLs
Pattern (c) is the signature failure, shown as buggy vs fixed RTL and dramatized in §7. The bug is the same everywhere: a combinational output case assigns req only in the state where it is high and has no default, so every other state leaves req unassigned — and "assign nothing, no clock" is a latch that holds req's old value. The fix is default-first: assign req = 0 at the top, so every path drives it.
// BUGGY: req is assigned ONLY in ACCESS; every other state leaves it undriven and there
// is no default -> the tool infers a LATCH on req that HOLDS its old value, so
// req stays high into states where it must be low. next_state has the same hole.
module seq_bad (
input logic clk, rst, start,
output logic req
);
typedef enum logic [1:0] {IDLE, ARM, ACCESS} state_t;
state_t state, next_state;
always_comb begin
case (state) // NO default line for req or next_state
IDLE: begin next_state = start ? ARM : IDLE; end // req UNASSIGNED here
ARM: begin next_state = ACCESS; end // req UNASSIGNED here
ACCESS: begin req = 1'b1; next_state = IDLE; end // req high only here
endcase // -> LATCH on req (and on next_state's holes)
end
always_ff @(posedge clk)
if (rst) state <= IDLE; else state <= next_state;
endmodule
// FIXED: default-first. req = 0 and next_state = IDLE assigned before the case, and a
// default arm recovers illegal states -> no latch, safe by construction.
module seq_good (
input logic clk, rst, start,
output logic req
);
typedef enum logic [1:0] {IDLE, ARM, ACCESS} state_t;
state_t state, next_state;
always_comb begin
next_state = IDLE; // default = safe state
req = 1'b0; // default-first -> req never undriven
case (state)
IDLE: next_state = start ? ARM : IDLE;
ARM: next_state = ACCESS;
ACCESS: begin req = 1'b1; next_state = IDLE; end
default: next_state = IDLE; // illegal-state recovery
endcase
end
always_ff @(posedge clk)
if (rst) state <= IDLE; else state <= next_state;
endmodule module latch_bug_tb;
logic clk = 1'b0, rst, start, req;
int errors = 0;
// Bind to seq_good to PASS; to seq_bad to see req HOLD high (the latch) after ACCESS.
seq_good dut (.clk(clk), .rst(rst), .start(start), .req(req));
always #5 clk = ~clk;
initial begin
rst = 1'b1; start = 1'b0;
@(posedge clk); #1; rst = 1'b0;
start = 1'b1; @(posedge clk); #1; // IDLE -> ARM (req must be low)
start = 1'b0; @(posedge clk); #1; // ARM -> ACCESS (req goes high)
assert (req === 1'b1) else begin $error("ACCESS: req should be high"); errors++; end
@(posedge clk); #1; // ACCESS -> IDLE: req MUST DROP.
// THE LATCH CHECK: on seq_bad, req latched high in ACCESS and HOLDS here; assert
// it is low. This single check is the whole difference between buggy and fixed.
assert (req === 1'b0)
else begin $error("req HELD stale high in IDLE -> latch inferred!"); errors++; end
if (errors == 0) $display("PASS: req drops after ACCESS -> no latch (default-first)");
else $display("FAIL: %0d mismatches (missing-default latch)", errors);
$finish;
end
endmoduleThe Verilog buggy/fixed pair is the same story with reg outputs and always @(*). Driving the machine past ACCESS into a state where req must be low is what exposes the latch — the held-high req is the whole bug.
// BUGGY: no default assignment; req assigned only in ACCESS -> latch on req that holds.
module seq_bad (
input wire clk, rst, start,
output reg req
);
localparam IDLE = 2'd0, ARM = 2'd1, ACCESS = 2'd2;
reg [1:0] state, next_state;
always @(*) begin
case (state) // NO default -> req/next_state have holes
IDLE: next_state = start ? ARM : IDLE; // req UNASSIGNED
ARM: next_state = ACCESS; // req UNASSIGNED
ACCESS: begin req = 1'b1; next_state = IDLE; end
endcase // -> LATCH on req
end
always @(posedge clk)
if (rst) state <= IDLE; else state <= next_state;
endmodule
// FIXED: default-first + recovering default -> no latch, illegal states recover.
module seq_good (
input wire clk, rst, start,
output reg req
);
localparam IDLE = 2'd0, ARM = 2'd1, ACCESS = 2'd2;
reg [1:0] state, next_state;
always @(*) begin
next_state = IDLE; // default = safe state
req = 1'b0; // default-first -> no latch on req
case (state)
IDLE: next_state = start ? ARM : IDLE;
ARM: next_state = ACCESS;
ACCESS: begin req = 1'b1; next_state = IDLE; end
default: next_state = IDLE; // illegal-state recovery
endcase
end
always @(posedge clk)
if (rst) state <= IDLE; else state <= next_state;
endmodule module latch_bug_tb;
reg clk, rst, start;
wire req;
integer errors;
// Bind to seq_good to PASS; to seq_bad to observe req HOLD high (the latch).
seq_good dut (.clk(clk), .rst(rst), .start(start), .req(req));
initial clk = 1'b0;
always #5 clk = ~clk;
initial begin
errors = 0;
rst = 1'b1; start = 1'b0;
@(posedge clk); #1; rst = 1'b0;
start = 1'b1; @(posedge clk); #1; // IDLE -> ARM
start = 1'b0; @(posedge clk); #1; // ARM -> ACCESS
if (req !== 1'b1) begin $display("FAIL: ACCESS req should be high"); errors = errors + 1; end
@(posedge clk); #1; // ACCESS -> IDLE: req MUST drop
if (req !== 1'b0) begin
$display("FAIL: req HELD stale high in IDLE -> latch inferred!");
errors = errors + 1;
end
if (errors == 0) $display("PASS: req drops after ACCESS -> no latch");
else $display("FAIL: %0d mismatches (missing-default latch)", errors);
$finish;
end
endmoduleIn VHDL the same bug appears when the combinational process omits the default assignment (or the case omits when others on an incomplete list). The fix is identical in spirit: assign req and next_state at the top of the process, then let the case override.
library ieee;
use ieee.std_logic_1164.all;
-- BUGGY: req is only driven in ACCESS; no default assignment before the case, so on
-- IDLE/ARM req is not driven -> the synthesis tool infers a LATCH on req.
entity seq_bad is
port ( clk, rst, start : in std_logic; req : out std_logic );
end entity;
architecture rtl of seq_bad is
type state_t is (IDLE, ARM, ACCESS);
signal state, next_state : state_t;
begin
comb : process (all)
begin
case state is -- NO default assignment for req
when IDLE => if start = '1' then next_state <= ARM; else next_state <= IDLE; end if;
when ARM => next_state <= ACCESS;
when ACCESS => req <= '1'; next_state <= IDLE;
end case; -- req undriven in IDLE/ARM -> LATCH
end process;
reg_p : process (clk)
begin
if rising_edge(clk) then
if rst = '1' then state <= IDLE; else state <= next_state; end if;
end if;
end process;
end architecture;
-- FIXED: default-first. req and next_state are assigned before the case -> no latch.
library ieee;
use ieee.std_logic_1164.all;
entity seq_good is
port ( clk, rst, start : in std_logic; req : out std_logic );
end entity;
architecture rtl of seq_good is
type state_t is (IDLE, ARM, ACCESS);
signal state, next_state : state_t;
begin
comb : process (all)
begin
next_state <= IDLE; -- default = safe state
req <= '0'; -- default-first -> no latch on req
case state is
when IDLE => if start = '1' then next_state <= ARM; end if;
when ARM => next_state <= ACCESS;
when ACCESS => req <= '1'; next_state <= IDLE;
end case;
end process;
reg_p : process (clk)
begin
if rising_edge(clk) then
if rst = '1' then state <= IDLE; else state <= next_state; end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity latch_bug_tb is
end entity;
architecture sim of latch_bug_tb is
signal clk : std_logic := '0';
signal rst : std_logic := '1';
signal start : std_logic := '0';
signal req : std_logic;
begin
-- Bind to seq_good to PASS; to seq_bad to observe req HOLD high (the latch).
dut : entity work.seq_good port map (clk => clk, rst => rst, start => start, req => req);
clkgen : process
begin
clk <= '0'; wait for 5 ns;
clk <= '1'; wait for 5 ns;
end process;
stim : process
begin
rst <= '1'; start <= '0';
wait until rising_edge(clk); wait for 1 ns; rst <= '0';
start <= '1'; wait until rising_edge(clk); wait for 1 ns; -- IDLE -> ARM
start <= '0'; wait until rising_edge(clk); wait for 1 ns; -- ARM -> ACCESS
assert req = '1' report "ACCESS: req should be high" severity error;
wait until rising_edge(clk); wait for 1 ns; -- ACCESS -> IDLE
-- Latch check: req MUST have dropped; a latched req would hold '1' here.
assert req = '0'
report "req HELD stale high in IDLE -> latch inferred!" severity error;
report "latch_bug self-check complete" severity note;
wait;
end process;
end architecture;Across all three languages the lesson is one sentence: a combinational block must assign every output and next-state on every path (default-first) and give the case a recovering default arm for every illegal state — miss the first and you get a latch, miss the second and you get a lock-up.
5. Verification Strategy
Safe-FSM verification is not "does the machine sequence correctly" — a machine can walk its legal path perfectly and still carry a latch or lock up on an illegal state. It has two distinct properties, each needing its own check.
No combinational output or next-state may hold a stale value across a state where it should change (no latch); and any illegal state the register can enter must return to a known safe state (no lock-up).
- Self-checking, latch-catching stimulus. The decisive test is not that
reqgoes high inACCESS— it is thatreqgoes low again the moment the machine leavesACCESS. Drive the path that transitions out of the asserting state and assert the output did not hold its previous value: SVassert (req === 1'b0)in the next state, Verilogif (req !== 1'b0) $display("FAIL…"), VHDLassert req = '0' … severity error. On a latch-free machinereqdrops immediately; on a latched one it lingers — so this single check, in the §4a and §4c testbenches, is the machine-checked proof there is no inferred latch on the output. - Force an illegal state and assert recovery. A functional stimulus can never reach an illegal encoding through legal transitions — that is what makes it illegal. So the testbench must inject one:
force/releasethe state register into the illegal code (SV/Verilog) or a VHDL external-name force, modelling the SEU/glitch, then assert the machine returns to the safe state on the next edge — the §4b testbenches force2'b11and assertstate === IDLE. Without the recovering default arm this check fails (the machine holds the illegal state forever); with it, recovery is one edge. This is the only way to verify the recovery logic, since the recovery path is unreachable by construction. - Outputs are never stale — a general invariant. Beyond the single strobe, sweep every state and assert each output equals its specified value for that state, not a leftover from a prior state. A stale output is the tell of a latch on a signal the directed happy-path test never re-checks; a per-state output table asserted every cycle catches it. The invariant: the value of every combinational output depends only on the current state and inputs — never on the state history.
- Conceptual invariants. Three properties must always hold: (i) every combinational output and
next_stateis a pure function of the current state and inputs (no latch — no dependence on history); (ii) from every reachable and every unreachable encoding, the machine reaches a legal state within a bounded number of cycles (liveness / no lock-up); (iii) reset and illegal-state recovery share one destination — the safe state — so the machine has exactly one guaranteed way home. Property (ii) over the unreachable states is what the illegal-state force test checks. - The latch is a synthesis-log / lint concern too. Be honest about the limit: whether a latch was inferred is ultimately reported by the tools, not only by the waveform. The primary detection is the synthesis log (a
LATCH inferred for reqwarning) and a lint check (a rule that flags any combinational block with an incompletely-assigned output). The self-checking TB catches the behavioural symptom (the stale hold); the synthesis/lint report catches the structural cause. Sign-off uses both — never conclude "no latch" from a passing functional test alone if the synthesis log warns. - Corner cases and expected waveform. Exercise: the transition out of every asserting state (the latch check, done for each output); reset asserted mid-sequence (the machine must snap to the safe state and drive its outputs to their reset values, not hold); each illegal encoding forced in turn (all must recover); and back-to-back runs (the machine must re-enter
IDLEcleanly andreqmust not carry over). On a correct waveform,reqis a single-cycle pulse inACCESSand flat-low everywhere else, and a forced illegal state shows the register snapping toIDLEon the very next edge — the visual signature of a machine that is both latch-free and self-recovering.
6. Common Mistakes
A missing default in a combinational output block — the inferred latch. The signature bug (§7). A case that assigns an output only in the states where it is active, with no default assignment before it, leaves the output undriven on every other path, and the tool infers a latch that holds the stale value. It is intermittent and state-order-dependent — it only misbehaves on paths that hit the unassigned arm — and it passes the happy-path directed test. The fix is reflexive: assign every output a safe default as the first statement of the block, then override per state. Default-first, always.
No default (VHDL when others) arm — the illegal-state lock-up. A next-state case that lists only the legal states leaves illegal encodings undriven; a glitch or SEU into one has no defined next state, so the machine holds the illegal state (another latch) and never leaves — a permanent lock-up that only a watchdog reset clears. Always give the next-state case a default/when others arm that routes to a known safe state, so any illegal encoding recovers on the next edge. Safety is not "illegal states can't happen"; it is "when one does, the machine gets home."
Relying on full_case / parallel_case pragmas to silence the latch. full_case tells synthesis "assume this case is complete" so it stops inferring the latch — but it does not define behaviour on the missing values, so synthesis prunes them as don't-cares while simulation may still exercise them, and the two disagree. parallel_case similarly tells the tool to ignore overlap it should honor. Both create a sim/synth divergence worse than the latch they hide, and a machine that is correct in RTL sim but wrong in silicon. Never use them to fix a latch; write a real default.
Defaulting next_state to state (hold) instead of the safe state in a safe machine. Defaulting next_state = state keeps the block latch-free, but if the machine is in an illegal state, holding means holding the illegal state — the lock-up returns. In a safety-critical machine the next-state default should be the safe state (IDLE) so a fall-through recovers rather than persists. Latch-free is necessary; recovering is the extra step that makes it safe.
Assuming an unreachable safe/recovery state survives synthesis. You wrote the default: next_state = IDLE; recovery arm — but from the tool's pure-function view, the illegal encodings are unreachable, so it may prune the recovery logic (and even re-encode the state register) as dead. The recovery you carefully wrote is then absent from the netlist. Preserve it with a synthesis keep / safe state-machine attribute on the state register, so the tool builds and keeps the illegal-state decoding and the recovery arm. A safe FSM that gets optimized back to an unsafe one is a classic RTL-vs-silicon trap.
Trusting RTL simulation alone to prove there is no latch. A zero-delay RTL sim of a latch-inferred design often looks fine on the happy path, because the stale hold only shows on specific state orders and the latch behaves like a wire when the assigning path runs. The structural fact — a latch was inferred — is reported by the synthesis log and lint, not necessarily by a casual waveform. Never sign off "no latch" from a passing functional test; read the synthesis warnings and run the lint rule that flags incomplete combinational assignment.
7. DebugLab
The strobe that will not go low — an output latch from a missing default, and the FSM that locks forever
The engineering lesson: a combinational block is a total function — it must drive every output on every path (or a latch appears) and define a transition for every state including the illegal ones (or the machine can lock). The two tells are unmistakable: an output that holds a stale value into a state where it should change, with a LATCH inferred line in the synthesis log, is a missing default on that output; a rare, un-reproducible hang that only a full reset clears is an illegal state with no recovery arm. Both fixes are the same move in spirit — make the block complete: assign every output and next-state a safe default first (default-first kills the latch), and give the next-state case a default/when others arm to a known safe state, preserved with a synthesis keep (the recovering default kills the lock-up). Both are identical across SystemVerilog, Verilog, and VHDL — only the spelling of "default" changes. A safe FSM is not one that never enters a bad state; it is one that always drives every signal and always finds its way home.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses the "make the combinational block total and recovering" habit.
Exercise 1 — Find and kill the latch
You are given a combinational output block for a 4-state FSM that drives three outputs (req, busy, done), where each output is assigned in only some of the case arms and there is no default block. (a) For each of the three outputs, list the states on which it is left unassigned, and state exactly which signal(s) the tool will infer a latch on. (b) Rewrite the block default-first so no latch can appear, choosing the correct safe default for each output and justifying it. (c) Explain why the happy-path directed test might pass even with the latches present, and design the specific stimulus that would expose each stale hold.
Exercise 2 — Make an unsafe FSM safe
Start from a next-state case that lists only the legal states of a machine whose register has one unused encoding, with no default/when others arm. (a) Add the illegal-state recovery so any illegal code routes to the safe state, and state which state you chose as safe and why. (b) Explain what a glitch into the illegal encoding does before your change and after it, cycle by cycle. (c) Name the synthesis attribute you must add and explain precisely what goes wrong in the netlist if you omit it — including why the recovery arm can disappear.
Exercise 3 — Why the pragma is a trap
An engineer silences a latch warning by adding full_case to the case instead of a default. (a) Explain what full_case tells the synthesis tool and what it does not tell it. (b) Construct a concrete input/state scenario in which the RTL simulation and the synthesized gate netlist produce different behaviour for this design, and explain why. (c) State the correct fix and explain why it removes the divergence that the pragma introduced.
Exercise 4 — Trace the latch and the recovery
For the refresh sequencer (IDLE → ARM → ACCESS → IDLE, req high in ACCESS): (a) with the buggy output block (no default on req), write out the value of req cycle by cycle for the sequence start=1, then idle for three cycles — mark the cycle(s) where req holds a stale high and explain each. (b) With the safe block, force the state register into the illegal encoding on a chosen cycle and write out state and req for the next three cycles, showing the recovery. (c) Explain why (a)'s stale hold and (b)'s lock-up are, at bottom, the same omission — a combinational block that did not assign on every path/state.
10. Related Tutorials
Continue in RTL Design Patterns (forward FSM links unlock as they ship):
- FSM State Encoding (binary vs one-hot) — Chapter 4.4; how the state you decode is encoded, why one-hot changes which codes are "illegal," and how safe-state recovery interacts with unused one-hot codes.
- FSM + Datapath — Chapter 4.6; where the latch-free output discipline earns its keep driving real datapath controls, and why a stale control is worse than a stale flag.
- FSM Worked Example — Chapter 4.7; a full FSM built end-to-end with default-first outputs and illegal-state recovery applied throughout.
Backward / in-track dependencies:
- FSM Fundamentals — Chapter 4.1; the three-piece FSM model (state register + next-state logic + output logic) this page keeps latch-free and safe.
- FSM Coding Styles (1/2/3-process) — Chapter 4.3; where the default-first discipline was introduced — this page is its safety payoff, extended to outputs and illegal states.
- Moore vs Mealy — Chapter 4.2; the output logic you keep latch-free here is exactly the Moore/Mealy output tap, and a latched Mealy output is a distinct hazard.
- Multiplexers (2:1, N:1, one-hot) — Chapter 1.1; the same incomplete-case latch, seen first on a mux — this page is that latch on an FSM output.
- The Register Pattern (D-FF) — Chapter 2.1; the state register a glitch/SEU flips into an illegal encoding, and the flop the safe machine recovers with.
- Synchronous Reset — Chapter 2.3; the sampled-on-the-edge reset that establishes the safe state both reset and recovery share.
- Reset Strategy — Chapter 2.5; the reset the safe state depends on, and the recovery-vs-reset relationship for getting the machine home.
Verilog v1 prerequisites (the grammar this pattern transcribes into):
- Case Statements — the
case (state)whose default arm is the whole of illegal-state recovery, and whose completeness keeps outputs latch-free. - If-Else Statements — the conditional inside each state arm, and how an
ifwithout anelseis another way to leave a signal unassigned. - Blocking and Non-Blocking Assignments — why the state register uses non-blocking (
<=) while the default-first combinational block uses blocking (=). - Initial and Always Blocks — the
always @(*)/always_combblock whose completeness is the entire subject of this page.
11. Summary
- The most common FSM bug is an inferred latch, and it comes from an incomplete combinational block. A
casethat assigns an output (ornext_state) only on some paths leaves it undriven on the others, and "assign nothing, no clock" is a level-sensitive latch that holds the stale value — intermittent, state-order-dependent, and flagged by aLATCH inferredline in the synthesis log. - Default-first makes the latch impossible by construction. Assign every output and
next_statea safe default as the first statement of the block, then override per state. Every path now drives every signal, so no path is silent and no latch can be inferred — the same discipline that keeps a mux and any FSM coding style latch-free, applied to every output. - A missing illegal-state arm lets a glitch lock the machine forever. A register with unused encodings can be flipped into an illegal state by a glitch or SEU; with no
default/when othersarm the machine has no way out and hangs until a watchdog reset. Give the next-statecasea recovering default to a known safe state (usuallyIDLE), so an illegal state self-corrects on the next edge — and defaultnext_stateto the safe state, not merely to hold. - Safety has a synthesis cost, and pragmas are a trap. The recovery arm is extra next-state logic, and because the illegal states are unreachable in the pure-function view the tool may optimize the recovery away — preserve it with a synthesis keep /
safeattribute on the state register. Never usefull_case/parallel_caseto silence a latch: they hide it without defining behaviour and make simulation and synthesis disagree. Write a realdefault. - Verify both properties, and trust the tools too. Prove no latch by driving out of each asserting state and asserting the output dropped (not held); prove recovery by forcing the register into an illegal code and asserting it returns to the safe state within one edge — the recovery path is otherwise unreachable. Back both with the synthesis log and lint. Built and self-check-verified here in SystemVerilog, Verilog, and VHDL: a safe FSM always drives every signal on every path and always finds its way home.