Skip to content

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.

the-need.v — one output, one omission, one latch that holds
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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 flow
How a missing default becomes a latch — the inference, step by stepsome state skips OUTviolatestool must decideinfersstorageclose the blankcase over stateoutput OUT assigned only in some armsan unassignedpatha state where OUT is left undrivencombinationalruleevery output must be driven on every pathassign nothing =holdno clock, keep old valueinferred latchlevel-sensitive storage on OUTdefault-first fixOUT = safe default at top, then override
Follow the path a missing assignment takes to a latch. The case assigns OUT in some arms but not all; there exists a path (a state with no arm, or an arm that skips OUT) where OUT is left undriven. A combinational block must drive every output on every path, so the tool hits a contradiction: on this path the code says keep the old value, which with no clock is a level-sensitive latch, and it infers one gated by that path. The fix breaks the chain at its source: assign OUT a safe default as the very first statement, so every path drives OUT and the unassigned path cannot exist. Default-first is not a style preference; it is the structural guarantee that no blank is left for a latch to fill. Identical in SystemVerilog, Verilog, and VHDL.

Default-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 flow
Illegal-state recovery — every unused encoding routes home to the safe statestartadvancedoneno legal armrecoverpreserveIDLE (safe state)reset lands here; recovery targetARMlegal working stateACCESSlegal working state — req pulses hereillegal encodingunused code — reached by glitch / SEUdefault armwhen others => next_state = IDLEsynth keep / safeattrstops the tool pruning the recovery
The legal machine walks IDLE to ARM to ACCESS and home. The safety is what happens off that path: any illegal encoding — an unused state code a glitch or SEU can land in — has no legal case arm, so the default (VHDL when others) arm catches it and routes next_state to IDLE, the safe state, and the machine recovers on the next clock edge instead of locking forever. Because the illegal encodings are unreachable in the pure-function view, a synthesis tool may prune the recovery arm as dead logic; a synthesis keep or a safe state-machine attribute on the register preserves it so the recovery survives to silicon. Reset and the recovery share one target — the safe state — which is why IDLE is usually both. Identical in SystemVerilog, Verilog, and VHDL; only the pragma spelling differs.

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

seq_latchfree.sv — default-first: every output and next_state driven on every path
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
   endmodule

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

seq_latchfree_tb.sv — clocked self-check: req pulses once in ACCESS and does NOT hold stale
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

seq_latchfree.v — default-first in Verilog (next_state and req both defaulted)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
seq_latchfree_tb.v — self-checking: req high only in ACCESS, drops immediately after (no latch)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

seq_latchfree.vhd — default-first in VHDL (process(all), both signals defaulted)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
seq_latchfree_tb.vhd — self-checking VHDL TB (req high only in ACCESS, drops after)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

seq_safe.sv — illegal-state recovery: default arm routes any illegal code to the SAFE state
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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;
   endmodule

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

seq_safe_tb.sv — clocked self-check: force an ILLEGAL state, assert recovery to IDLE
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

seq_safe.v — illegal-state recovery in Verilog (default arm -> IDLE, keep attribute)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
seq_safe_tb.v — self-checking: force illegal state, assert return to IDLE (PASS/FAIL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

seq_safe.vhd — illegal-state recovery in VHDL (explicit encoding + when others -> IDLE)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
seq_safe_tb.vhd — self-checking: force illegal '11', assert recovery to IDLE (severity error)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

latch_bug.sv — BUGGY (missing default -> req latch) vs FIXED (default-first)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
latch_bug_tb.sv — the check that catches the latch: drive the path that skips req, assert it did NOT hold
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

latch_bug.v — BUGGY (no default -> latch) vs FIXED (default-first) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
latch_bug_tb.v — self-checking: assert req drops after ACCESS (a latch would hold it)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

latch_bug.vhd — BUGGY (no default assign -> latch) vs FIXED (default-first) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
latch_bug_tb.vhd — self-checking VHDL: assert req drops after ACCESS (a latch would hold it)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 req goes high in ACCESS — it is that req goes low again the moment the machine leaves ACCESS. Drive the path that transitions out of the asserting state and assert the output did not hold its previous value: SV assert (req === 1'b0) in the next state, Verilog if (req !== 1'b0) $display("FAIL…"), VHDL assert req = '0' … severity error. On a latch-free machine req drops 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/release the 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 force 2'b11 and assert state === 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_state is 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 req warning) 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 IDLE cleanly and req must not carry over). On a correct waveform, req is a single-cycle pulse in ACCESS and flat-low everywhere else, and a forced illegal state shows the register snapping to IDLE on 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.

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 if without an else is 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_comb block 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 case that assigns an output (or next_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 a LATCH inferred line in the synthesis log.
  • Default-first makes the latch impossible by construction. Assign every output and next_state a 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 others arm the machine has no way out and hangs until a watchdog reset. Give the next-state case a recovering default to a known safe state (usually IDLE), so an illegal state self-corrects on the next edge — and default next_state to 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 / safe attribute on the state register. Never use full_case/parallel_case to silence a latch: they hide it without defining behaviour and make simulation and synthesis disagree. Write a real default.
  • 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.