RTL Design Patterns · Chapter 4 · FSM Design
Two- & Three-Block FSM Coding
An FSM is three pieces: a state register, next-state logic, and output logic. Those three pieces can be written as one, two, or three always blocks, and the choice is not cosmetic. It decides how readable the machine is, whether outputs land in the cycle you expect or one late, and whether synthesis builds the combinational logic you meant or quietly infers a latch that makes the machine stick. The industry default is a single discipline: keep the clocked state register in its own block, keep next-state and output logic combinational, assign a default first, and drive every output on every path. This lesson codes the same small machine in the two-block and three-block styles, watches a missing default turn it into accidental storage, and builds every form in SystemVerilog, Verilog, and VHDL.
Intermediate16 min readRTL Design PatternsFSMTwo-Block FSMThree-Block FSMLatch InferenceSequential Logic
Chapter 4 · Section 4.3 · FSM Design
1. The Engineering Problem
In 4.1 you learned that every finite state machine is three things: a state register that remembers where the machine is, next-state logic that decides where it goes next, and output logic that decides what it drives. That is the model. It says nothing about how you write it — and there is more than one way.
Consider a tiny controller you will meet in real silicon: an arming sequence for a critical actuator. It must see the input go asserted for three consecutive cycles before it drives fire high; a single low cycle resets it. Four states — IDLE, ARM1, ARM2, FIRE — a next-state rule, and one output. You could fold the whole thing into a single clocked always block. You could split it into a clocked state register plus one combinational block for next-state-and-outputs. You could split it further, giving outputs their own block. All three describe the same machine and, written carefully, simulate identically.
They are not equivalent to read, and they are not equivalent to synthesize. The one-block form registers its outputs, so fire lands a cycle later than a reader of the state diagram expects. A carelessly written combinational block silently grows a latch and the machine freezes on some input sequence. And mixing blocking and non-blocking assignment across the clocked and combinational parts introduces simulation races that make the RTL and the gate netlist disagree. The choice of coding style is therefore an engineering decision with three axes — readability, output timing, and latch-safety — and the whole point of this section is to give you a default that gets all three right and a vocabulary to talk about the trade-offs.
// The SAME 4-state arming FSM (IDLE, ARM1, ARM2, FIRE) can be coded as:
// ONE block : everything in always @(posedge clk) — compact, outputs REGISTERED
// TWO blocks : clocked state reg + one always @(*) for next-state AND output
// THREE blocks: clocked state reg + next-state @(*) + SEPARATE output block
//
// The rules that decide whether the hardware is what you meant:
// - state register: state <= next_state; // CLOCKED, non-blocking (<=)
// - combinational: next_state = ...; // full-assignment (=), DEFAULT first
// - a comb block that leaves next_state or an output unassigned on some path
// => the tool infers a LATCH => the FSM sticks or holds stale output (§7).2. Mental Model
3. Pattern Anatomy
The FSM coding family is one clocked box, one or two combinational clouds, and a choice of where the outputs live.
The three styles, side by side.
- One-block. A single clocked
always: it readsstateand inputs, computes the next state, updatesstate <= next, and assigns the outputs — all with non-blocking assignment, all on the clock edge. It is compact and has no separatenext_statesignal. Its defining property: the outputs are registered (they come out of flops), so they are stable and glitch-free but arrive one cycle after the state that produced them. That surprises anyone reading it as a Moore diagram. - Two-block (the default). One clocked block for the state register (
state <= next_state, non-blocking) and onealways_combblock that computesnext_stateand the outputs fromstateand the inputs. Two blocks, one clocked and one combinational — the split matches the two kinds of hardware exactly, which is why it reads cleanly and why the latch question is confined to a single, obviously-combinational block. - Three-block. The two-block's combinational block is split in two: one block for
next_state, a separate block for the outputs. The output block can be combinational (Mealy or combinational-Moore outputs that follow the state this cycle) or clocked/registered (glitch-free Moore outputs, one cycle late). Three-block earns its keep when you want to choose the output timing independently of the next-state logic — most often to register the outputs for a clean, glitch-free interface.
Why two/three-block is the recommended default. Two reasons, both about clarity and safety. First, readability: the split mirrors the hardware — one clocked flop, one combinational cloud — so a reader sees the state register and the next-state logic as distinct things, exactly as they appear on a state diagram. Second, and more important, latch-safety by construction: the next-state logic lives in one obviously-combinational block, and the discipline for keeping it latch-free is a single, checkable rule — next_state = state; (or a defined default) as the very first statement, then the case overrides it. Every path now assigns next_state, so no latch can be inferred. The one-block form hides the next-state logic inside the clocked flop, where the latch question does not even arise but the registered-output surprise does.
The blocking-vs-non-blocking rule, restated for FSMs. It is the same rule as the register pattern, applied to the two kinds of hardware: the state register is sequential, so it uses non-blocking (state <= next_state) — this makes the state update sample the current next_state atomically and keeps the FSM race-free. The combinational next-state and output blocks are stateless, so they use full-assignment (next_state = …, out = … with =) inside always_comb/always @(*). Cross the rule and you break the machine: a clocked state register written with = races against anything else in the block; a combinational block written with <= computes stale values and simulates unlike its own synthesis.
Registered vs combinational output block — the tie to 4.2 (Moore/Mealy). Where the output logic lives is the Moore-vs-Mealy timing decision, now in code. A combinational output block makes outputs follow the state (Moore) or the state-and-inputs (Mealy) this cycle — fast, but potentially glitchy and, for Mealy, transparent to input glitches. A registered output block (a second clocked box) puts the outputs through flops — glitch-free and clean at a module boundary, at the cost of one cycle of latency. The three-block style exists precisely so you can make this choice deliberately instead of inheriting whatever the one-block form happens to give you (always registered).
FSM coding — one clocked box, one or two combinational clouds
data flowOne discipline closes the anatomy and it is the same one that governs a mux: a combinational block must assign every output on every path. In the next-state cloud that means next_state = state; (hold-in-place) as the first line, then the case overrides it; in a combinational output cloud it means out = <safe value>; first, then the overrides. Default first, then the case. Miss it and §7 happens — the cloud grows a latch, next_state (or an output) holds a stale value, and the FSM sticks on exactly the input sequence that hits the unassigned path.
4. Real RTL Implementation
This is the core of the page. We build the same 4-state arming FSM — IDLE → ARM1 → ARM2 → FIRE, advancing while go is high, resetting to IDLE the moment go drops, and asserting fire in FIRE — three ways: (a) two-block, (b) three-block, and (c) the missing-default latch bug, buggy vs fixed. Each is shown in SystemVerilog, Verilog, and VHDL, with an RTL block and a self-checking clocked testbench. Using one machine across every style is what makes the contrast clean: only the shape of the code changes, never the behaviour.
Two disciplines run through every block. The state register is clocked and uses non-blocking (<=); the combinational blocks use full-assignment (=) and assign a default first. Keep those two straight and the FSM is race-free and latch-free by construction; break either and you get pattern (c).
4a. The two-block FSM — state register + one combinational block
The default. One clocked block holds the state register with a synchronous reset. One always_comb block computes next_state and fire, and it opens with defaults — next_state = state and fire = 1'b0 — so every path assigns both signals and no latch can appear. fire is a Moore output here: it depends only on state, so it is high exactly in FIRE.
module fsm_two_block (
input logic clk,
input logic rst, // synchronous, active-high
input logic go, // arming input
output logic fire // Moore output: high only in FIRE
);
typedef enum logic [1:0] {IDLE, ARM1, ARM2, FIRE} state_t;
state_t state, next_state;
// BLOCK 1 — the state register. CLOCKED, non-blocking. The ONLY sequential
// element: it samples next_state on the rising edge and holds state a cycle.
always_ff @(posedge clk) begin
if (rst) state <= IDLE;
else state <= next_state; // <= : atomic state update, race-free
end
// BLOCK 2 — next-state AND output logic. COMBINATIONAL, full-assignment.
// DEFAULT-FIRST: next_state holds, fire is low, unless a branch overrides.
always_comb begin
next_state = state; // default: stay put -> latch-free
fire = 1'b0; // default: deasserted -> latch-free
unique case (state)
IDLE: if (go) next_state = ARM1;
ARM1: next_state = go ? ARM2 : IDLE;
ARM2: next_state = go ? FIRE : IDLE;
FIRE: begin
fire = 1'b1; // Moore: output is a function of state
next_state = go ? FIRE : IDLE;
end
endcase
end
endmoduleThe testbench is clocked: it generates a clock, drives a go sequence, and after each rising edge checks state and fire against a reference. Critically it includes the check that catches the latch — it drives go low from ARM1/ARM2 and asserts the machine returns to IDLE rather than holding a stale next_state.
module fsm_two_block_tb;
logic clk = 0, rst, go, fire;
int errors = 0;
always #5 clk = ~clk; // 100 MHz clock
fsm_two_block dut (.clk(clk), .rst(rst), .go(go), .fire(fire));
// Apply go for one cycle, step the clock, return fire.
task automatic step(input logic g, input logic exp_fire);
go = g; @(posedge clk); #1;
assert (fire === exp_fire)
else begin $error("fire=%b exp=%b", fire, exp_fire); errors++; end
endtask
initial begin
rst = 1; go = 0; @(posedge clk); #1; rst = 0;
// Three consecutive highs must reach FIRE (fire high on the 4th sample).
step(1'b1, 1'b0); // IDLE -> ARM1
step(1'b1, 1'b0); // ARM1 -> ARM2
step(1'b1, 1'b0); // ARM2 -> FIRE (fire seen next sample)
step(1'b1, 1'b1); // in FIRE, still going -> fire high
// A LOW must drop us back to IDLE — the check that would expose a latch:
step(1'b0, 1'b0); // FIRE -> IDLE, fire low
step(1'b1, 1'b0); // IDLE -> ARM1 again (machine not stuck)
step(1'b0, 1'b0); // ARM1 -> IDLE on a single low
if (errors == 0) $display("PASS: two-block FSM sequences and resets correctly");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog two-block form is identical in intent: reg for state/next_state with localparam state codes, always @(posedge clk) for the register, always @(*) for the combinational block. The default-first pair (next_state = state; fire = 1'b0;) does the latch-free work.
module fsm_two_block (
input wire clk,
input wire rst,
input wire go,
output reg fire
);
localparam [1:0] IDLE = 2'd0, ARM1 = 2'd1, ARM2 = 2'd2, FIRE = 2'd3;
reg [1:0] state, next_state;
// State register: clocked, non-blocking, synchronous reset.
always @(posedge clk) begin
if (rst) state <= IDLE;
else state <= next_state; // <= keeps the update atomic
end
// Next-state + output: combinational, full-assignment, DEFAULT FIRST.
always @(*) begin
next_state = state; // default: hold -> no latch
fire = 1'b0; // default: low -> no latch
case (state)
IDLE: if (go) next_state = ARM1;
ARM1: next_state = go ? ARM2 : IDLE;
ARM2: next_state = go ? FIRE : IDLE;
FIRE: begin fire = 1'b1; next_state = go ? FIRE : IDLE; end
default: next_state = IDLE; // recover from illegal state codes
endcase
end
endmodule module fsm_two_block_tb;
reg clk, rst, go;
wire fire;
integer errors;
initial clk = 0;
always #5 clk = ~clk;
fsm_two_block dut (.clk(clk), .rst(rst), .go(go), .fire(fire));
task step;
input g; input exp_fire;
begin
go = g; @(posedge clk); #1;
if (fire !== exp_fire) begin
$display("FAIL: fire=%b exp=%b", fire, exp_fire);
errors = errors + 1;
end
end
endtask
initial begin
errors = 0; rst = 1; go = 0; @(posedge clk); #1; rst = 0;
step(1'b1, 1'b0); // IDLE -> ARM1
step(1'b1, 1'b0); // ARM1 -> ARM2
step(1'b1, 1'b0); // ARM2 -> FIRE
step(1'b1, 1'b1); // FIRE, fire high
step(1'b0, 1'b0); // FIRE -> IDLE (would expose a stuck latch)
step(1'b1, 1'b0); // IDLE -> ARM1 (not stuck)
step(1'b0, 1'b0); // ARM1 -> IDLE on a single low
if (errors == 0) $display("PASS: two-block FSM correct (no latch, resets clean)");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the two-block form is two processes: a clocked process sensitive to clk (the state register, rising_edge(clk)) and a combinational process sensitive to state and go (process(all)) that assigns next_state and fire with a default first. when others => is VHDL's default arm; the leading assignments are its default-first discipline.
library ieee;
use ieee.std_logic_1164.all;
entity fsm_two_block is
port (
clk : in std_logic;
rst : in std_logic; -- synchronous, active-high
go : in std_logic;
fire : out std_logic -- Moore output
);
end entity;
architecture rtl of fsm_two_block is
type state_t is (IDLE, ARM1, ARM2, FIRE);
signal state, next_state : state_t;
begin
-- PROCESS 1 — state register. CLOCKED, signal assignment (VHDL's non-blocking).
reg_proc : process (clk)
begin
if rising_edge(clk) then
if rst = '1' then state <= IDLE;
else state <= next_state; -- atomic state update
end if;
end if;
end process;
-- PROCESS 2 — next-state + output. COMBINATIONAL, DEFAULT-FIRST assignment.
comb_proc : process (all)
begin
next_state <= state; -- default: hold -> no latch
fire <= '0'; -- default: low -> no latch
case state is
when IDLE => if go = '1' then next_state <= ARM1; end if;
when ARM1 => if go = '1' then next_state <= ARM2; else next_state <= IDLE; end if;
when ARM2 => if go = '1' then next_state <= FIRE; else next_state <= IDLE; end if;
when FIRE =>
fire <= '1'; -- Moore: output from state
if go = '1' then next_state <= FIRE; else next_state <= IDLE; end if;
when others => next_state <= IDLE;
end case;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity fsm_two_block_tb is
end entity;
architecture sim of fsm_two_block_tb is
signal clk : std_logic := '0';
signal rst, go, fire : std_logic;
procedure step (signal go_s : out std_logic;
signal clk_s : in std_logic;
constant g : in std_logic;
constant expf : in std_logic;
signal fire_s : in std_logic) is
begin
go_s <= g;
wait until rising_edge(clk_s);
wait for 1 ns;
assert fire_s = expf
report "fire mismatch vs expected" severity error;
end procedure;
begin
clk <= not clk after 5 ns; -- free-running clock
dut : entity work.fsm_two_block
port map (clk => clk, rst => rst, go => go, fire => fire);
stim : process
begin
rst <= '1'; go <= '0';
wait until rising_edge(clk); wait for 1 ns; rst <= '0';
step(go, clk, '1', '0', fire); -- IDLE -> ARM1
step(go, clk, '1', '0', fire); -- ARM1 -> ARM2
step(go, clk, '1', '0', fire); -- ARM2 -> FIRE
step(go, clk, '1', '1', fire); -- FIRE, fire high
step(go, clk, '0', '0', fire); -- FIRE -> IDLE (exposes a stuck latch)
step(go, clk, '1', '0', fire); -- IDLE -> ARM1 (not stuck)
step(go, clk, '0', '0', fire); -- ARM1 -> IDLE on a single low
report "fsm_two_block self-check complete" severity note;
wait;
end process;
end architecture;4b. The three-block FSM — separate the output logic
Same machine, but the output logic gets its own block. This buys you an independent choice of output timing. Below, fire is put in a registered (clocked) output block, so it is glitch-free at the module boundary — the trade is that fire now lags the state by one cycle. Block 1 is the state register, block 2 is combinational next-state (default-first), block 3 is the clocked output flop.
module fsm_three_block (
input logic clk,
input logic rst,
input logic go,
output logic fire // REGISTERED Moore output (glitch-free)
);
typedef enum logic [1:0] {IDLE, ARM1, ARM2, FIRE} state_t;
state_t state, next_state;
// BLOCK 1 — state register (clocked, non-blocking).
always_ff @(posedge clk) begin
if (rst) state <= IDLE;
else state <= next_state;
end
// BLOCK 2 — next-state ONLY (combinational, default-first). No outputs here.
always_comb begin
next_state = state; // default: hold -> latch-free
unique case (state)
IDLE: if (go) next_state = ARM1;
ARM1: next_state = go ? ARM2 : IDLE;
ARM2: next_state = go ? FIRE : IDLE;
FIRE: next_state = go ? FIRE : IDLE;
endcase
end
// BLOCK 3 — output ONLY, and here CLOCKED: fire is registered off next_state,
// so it is glitch-free but lands one cycle after the state that produces it.
always_ff @(posedge clk) begin
if (rst) fire <= 1'b0;
else fire <= (next_state == FIRE); // register the Moore condition
end
endmoduleThe clocked testbench accounts for the extra register delay: fire now asserts one sample later than in the two-block form. The self-check still drives a low from an armed state and confirms the machine unwinds to IDLE — the latch-exposing check survives the restyle.
module fsm_three_block_tb;
logic clk = 0, rst, go, fire;
int errors = 0;
always #5 clk = ~clk;
fsm_three_block dut (.clk(clk), .rst(rst), .go(go), .fire(fire));
task automatic step(input logic g, input logic exp_fire);
go = g; @(posedge clk); #1;
assert (fire === exp_fire)
else begin $error("fire=%b exp=%b", fire, exp_fire); errors++; end
endtask
initial begin
rst = 1; go = 0; @(posedge clk); #1; rst = 0;
step(1'b1, 1'b0); // IDLE -> ARM1
step(1'b1, 1'b0); // ARM1 -> ARM2
step(1'b1, 1'b0); // ARM2 -> FIRE (next_state==FIRE now)
step(1'b1, 1'b1); // registered fire appears one cycle later
step(1'b0, 1'b0); // go low: unwinds to IDLE, fire drops
step(1'b1, 1'b0); // re-arm (machine not stuck)
if (errors == 0) $display("PASS: three-block FSM, registered output timing correct");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog three-block form uses three always blocks — one posedge for the register, one @(*) for next-state (default-first), one posedge for the registered output.
module fsm_three_block (
input wire clk,
input wire rst,
input wire go,
output reg fire // registered output
);
localparam [1:0] IDLE = 2'd0, ARM1 = 2'd1, ARM2 = 2'd2, FIRE = 2'd3;
reg [1:0] state, next_state;
// State register.
always @(posedge clk) begin
if (rst) state <= IDLE;
else state <= next_state;
end
// Next-state only — combinational, default-first.
always @(*) begin
next_state = state; // default: hold -> no latch
case (state)
IDLE: if (go) next_state = ARM1;
ARM1: next_state = go ? ARM2 : IDLE;
ARM2: next_state = go ? FIRE : IDLE;
FIRE: next_state = go ? FIRE : IDLE;
default: next_state = IDLE;
endcase
end
// Output only — CLOCKED: fire is registered, glitch-free, one cycle late.
always @(posedge clk) begin
if (rst) fire <= 1'b0;
else fire <= (next_state == FIRE);
end
endmodule module fsm_three_block_tb;
reg clk, rst, go;
wire fire;
integer errors;
initial clk = 0;
always #5 clk = ~clk;
fsm_three_block dut (.clk(clk), .rst(rst), .go(go), .fire(fire));
task step;
input g; input exp_fire;
begin
go = g; @(posedge clk); #1;
if (fire !== exp_fire) begin
$display("FAIL: fire=%b exp=%b", fire, exp_fire);
errors = errors + 1;
end
end
endtask
initial begin
errors = 0; rst = 1; go = 0; @(posedge clk); #1; rst = 0;
step(1'b1, 1'b0); // IDLE -> ARM1
step(1'b1, 1'b0); // ARM1 -> ARM2
step(1'b1, 1'b0); // ARM2 -> FIRE
step(1'b1, 1'b1); // registered fire lands here
step(1'b0, 1'b0); // go low: unwind to IDLE, fire drops
step(1'b1, 1'b0); // re-arm (not stuck)
if (errors == 0) $display("PASS: three-block FSM registered output correct");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the three-block form is three processes: clocked state register, combinational next-state (process(all), default-first), and a clocked output process that registers the Moore condition.
library ieee;
use ieee.std_logic_1164.all;
entity fsm_three_block is
port (
clk : in std_logic;
rst : in std_logic;
go : in std_logic;
fire : out std_logic -- registered output
);
end entity;
architecture rtl of fsm_three_block is
type state_t is (IDLE, ARM1, ARM2, FIRE);
signal state, next_state : state_t;
begin
-- State register.
reg_proc : process (clk)
begin
if rising_edge(clk) then
if rst = '1' then state <= IDLE; else state <= next_state; end if;
end if;
end process;
-- Next-state only — combinational, default-first.
comb_proc : process (all)
begin
next_state <= state; -- default: hold -> no latch
case state is
when IDLE => if go = '1' then next_state <= ARM1; end if;
when ARM1 => if go = '1' then next_state <= ARM2; else next_state <= IDLE; end if;
when ARM2 => if go = '1' then next_state <= FIRE; else next_state <= IDLE; end if;
when FIRE => if go = '1' then next_state <= FIRE; else next_state <= IDLE; end if;
when others => next_state <= IDLE;
end case;
end process;
-- Output only — CLOCKED: registered, glitch-free, one cycle late.
out_proc : process (clk)
begin
if rising_edge(clk) then
if rst = '1' then fire <= '0';
elsif next_state = FIRE then fire <= '1';
else fire <= '0';
end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity fsm_three_block_tb is
end entity;
architecture sim of fsm_three_block_tb is
signal clk : std_logic := '0';
signal rst, go, fire : std_logic;
procedure step (signal go_s : out std_logic;
signal clk_s : in std_logic;
constant g : in std_logic;
constant expf : in std_logic;
signal fire_s : in std_logic) is
begin
go_s <= g;
wait until rising_edge(clk_s);
wait for 1 ns;
assert fire_s = expf
report "registered fire mismatch vs expected" severity error;
end procedure;
begin
clk <= not clk after 5 ns;
dut : entity work.fsm_three_block
port map (clk => clk, rst => rst, go => go, fire => fire);
stim : process
begin
rst <= '1'; go <= '0';
wait until rising_edge(clk); wait for 1 ns; rst <= '0';
step(go, clk, '1', '0', fire); -- IDLE -> ARM1
step(go, clk, '1', '0', fire); -- ARM1 -> ARM2
step(go, clk, '1', '0', fire); -- ARM2 -> FIRE
step(go, clk, '1', '1', fire); -- registered fire appears here
step(go, clk, '0', '0', fire); -- unwind to IDLE, fire drops
step(go, clk, '1', '0', fire); -- re-arm (not stuck)
report "fsm_three_block 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 FSM coding failure, shown as buggy vs fixed RTL in each language, then dramatized narratively in §7. The bug is always the same: the combinational next-state/output block omits the default (no leading next_state = state; out = 0; and no default/others), so on some state/input combination the block assigns nothing to next_state (or to fire). In combinational logic, "assign nothing" means "hold the old value," and holding with no clock is a latch. The FSM then sticks on the state where the assignment is missing, or the output holds stale — and it only shows on the input order that reaches the unassigned path.
// BUGGY: no default-first, and the case is incomplete on the OUTPUT and on some
// next_state paths. When state==ARM1 and go==0, next_state is unassigned
// (no else) => next_state LATCHES; and fire is only set in FIRE with no
// default, so fire LATCHES too. The machine sticks / holds stale fire.
module fsm_bad (
input logic clk, rst, go,
output logic fire
);
typedef enum logic [1:0] {IDLE, ARM1, ARM2, FIRE} state_t;
state_t state, next_state;
always_ff @(posedge clk)
if (rst) state <= IDLE; else state <= next_state;
always_comb begin
// NO default for next_state, NO default for fire -> latch bait
case (state)
IDLE: if (go) next_state = ARM1; // go==0: next_state unassigned
ARM1: if (go) next_state = ARM2; // go==0: next_state unassigned
ARM2: if (go) next_state = FIRE; // go==0: next_state unassigned
FIRE: begin fire = 1'b1; next_state = go ? FIRE : IDLE; end
// fire assigned ONLY in FIRE -> fire latches elsewhere
endcase
end
endmodule
// FIXED: default-first assigns next_state AND fire on EVERY path -> pure comb.
module fsm_good (
input logic clk, rst, go,
output logic fire
);
typedef enum logic [1:0] {IDLE, ARM1, ARM2, FIRE} state_t;
state_t state, next_state;
always_ff @(posedge clk)
if (rst) state <= IDLE; else state <= next_state;
always_comb begin
next_state = state; // DEFAULT FIRST -> next_state total
fire = 1'b0; // DEFAULT FIRST -> fire total
unique case (state)
IDLE: if (go) next_state = ARM1;
ARM1: next_state = go ? ARM2 : IDLE;
ARM2: next_state = go ? FIRE : IDLE;
FIRE: begin fire = 1'b1; next_state = go ? FIRE : IDLE; end
endcase
end
endmodule module fsm_bug_tb;
logic clk = 0, rst, go, fire;
int errors = 0;
always #5 clk = ~clk;
// Bind to fsm_good to PASS; to fsm_bad to watch the machine stick.
fsm_good dut (.clk(clk), .rst(rst), .go(go), .fire(fire));
task automatic step(input logic g, input logic exp_fire);
go = g; @(posedge clk); #1;
assert (fire === exp_fire)
else begin $error("fire=%b exp=%b", fire, exp_fire); errors++; end
endtask
initial begin
rst = 1; go = 0; @(posedge clk); #1; rst = 0;
step(1'b1, 1'b0); // IDLE -> ARM1
// The KILLER check: from ARM1, drop go. A correct FSM returns to IDLE.
// The buggy one LATCHES next_state (no assignment on this path) and sticks.
step(1'b0, 1'b0); // ARM1 -> IDLE (bug: holds ARM1)
step(1'b1, 1'b0); // IDLE -> ARM1 (bug: never left)
step(1'b1, 1'b0); // ARM1 -> ARM2
step(1'b1, 1'b0); // ARM2 -> FIRE
step(1'b1, 1'b1); // FIRE, fire high
step(1'b0, 1'b0); // FIRE -> IDLE, fire low (bug: fire holds)
if (errors == 0) $display("PASS: FSM sequences and unwinds (no latch)");
else $display("FAIL: %0d mismatches (latch: stuck state / stale fire)", errors);
$finish;
end
endmoduleThe Verilog buggy/fixed pair is the same story with reg and always @(*). Omitting the leading defaults and the else/default on some paths is what infers the latch; the fixed version restores next_state = state; fire = 1'b0; at the top.
// BUGGY: no default-first; ARM1/ARM2/IDLE leave next_state unassigned when go==0,
// and fire is set only in FIRE -> both latch. Machine sticks / holds fire.
module fsm_bad (input wire clk, rst, go, output reg fire);
localparam [1:0] IDLE=0, ARM1=1, ARM2=2, FIRE=3;
reg [1:0] state, next_state;
always @(posedge clk)
if (rst) state <= IDLE; else state <= next_state;
always @(*) begin
case (state) // NO defaults -> latch bait
IDLE: if (go) next_state = ARM1;
ARM1: if (go) next_state = ARM2;
ARM2: if (go) next_state = FIRE;
FIRE: begin fire = 1'b1; next_state = go ? FIRE : IDLE; end
endcase
end
endmodule
// FIXED: default-first assigns both signals on every path -> no latch.
module fsm_good (input wire clk, rst, go, output reg fire);
localparam [1:0] IDLE=0, ARM1=1, ARM2=2, FIRE=3;
reg [1:0] state, next_state;
always @(posedge clk)
if (rst) state <= IDLE; else state <= next_state;
always @(*) begin
next_state = state; // DEFAULT FIRST
fire = 1'b0; // DEFAULT FIRST
case (state)
IDLE: if (go) next_state = ARM1;
ARM1: next_state = go ? ARM2 : IDLE;
ARM2: next_state = go ? FIRE : IDLE;
FIRE: begin fire = 1'b1; next_state = go ? FIRE : IDLE; end
default: next_state = IDLE;
endcase
end
endmodule module fsm_bug_tb;
reg clk, rst, go;
wire fire;
integer errors;
initial clk = 0;
always #5 clk = ~clk;
// Bind to fsm_good to PASS; to fsm_bad to see the machine stick.
fsm_good dut (.clk(clk), .rst(rst), .go(go), .fire(fire));
task step;
input g; input exp_fire;
begin
go = g; @(posedge clk); #1;
if (fire !== exp_fire) begin
$display("FAIL: fire=%b exp=%b", fire, exp_fire);
errors = errors + 1;
end
end
endtask
initial begin
errors = 0; rst = 1; go = 0; @(posedge clk); #1; rst = 0;
step(1'b1, 1'b0); // IDLE -> ARM1
step(1'b0, 1'b0); // ARM1 -> IDLE (bug: latches, holds ARM1)
step(1'b1, 1'b0); // IDLE -> ARM1
step(1'b1, 1'b0); // ARM1 -> ARM2
step(1'b1, 1'b0); // ARM2 -> FIRE
step(1'b1, 1'b1); // FIRE, fire high
step(1'b0, 1'b0); // FIRE -> IDLE, fire low (bug: fire holds)
if (errors == 0) $display("PASS: FSM unwinds correctly (no latch)");
else $display("FAIL: %0d mismatches (stuck state / stale fire)", errors);
$finish;
end
endmoduleIn VHDL the same bug appears when the combinational process omits the leading defaults and leaves next_state/fire unassigned on some path (no else, no when others). VHDL's when others => plus the default-first assignments make the process total.
library ieee;
use ieee.std_logic_1164.all;
-- BUGGY: no default for next_state/fire; ARM1/ARM2/IDLE with go='0' leave
-- next_state unassigned, and fire only set in FIRE -> both LATCH.
entity fsm_bad is
port (clk, rst, go : in std_logic; fire : out std_logic);
end entity;
architecture rtl of fsm_bad is
type state_t is (IDLE, ARM1, ARM2, FIRE);
signal state, next_state : state_t;
begin
reg_proc : process (clk) begin
if rising_edge(clk) then
if rst = '1' then state <= IDLE; else state <= next_state; end if;
end if;
end process;
comb_proc : process (all) begin
case state is -- NO defaults -> latch bait
when IDLE => if go = '1' then next_state <= ARM1; end if;
when ARM1 => if go = '1' then next_state <= ARM2; end if;
when ARM2 => if go = '1' then next_state <= FIRE; end if;
when FIRE => fire <= '1';
if go = '1' then next_state <= FIRE; else next_state <= IDLE; end if;
when others => null;
end case;
end process;
end architecture;
-- FIXED: default-first assigns next_state AND fire on every path -> no latch.
entity fsm_good is
port (clk, rst, go : in std_logic; fire : out std_logic);
end entity;
architecture rtl of fsm_good is
type state_t is (IDLE, ARM1, ARM2, FIRE);
signal state, next_state : state_t;
begin
reg_proc : process (clk) begin
if rising_edge(clk) then
if rst = '1' then state <= IDLE; else state <= next_state; end if;
end if;
end process;
comb_proc : process (all) begin
next_state <= state; -- DEFAULT FIRST
fire <= '0'; -- DEFAULT FIRST
case state is
when IDLE => if go = '1' then next_state <= ARM1; end if;
when ARM1 => if go = '1' then next_state <= ARM2; else next_state <= IDLE; end if;
when ARM2 => if go = '1' then next_state <= FIRE; else next_state <= IDLE; end if;
when FIRE => fire <= '1';
if go = '1' then next_state <= FIRE; else next_state <= IDLE; end if;
when others => next_state <= IDLE;
end case;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity fsm_bug_tb is
end entity;
architecture sim of fsm_bug_tb is
signal clk : std_logic := '0';
signal rst, go, fire : std_logic;
procedure step (signal go_s : out std_logic;
signal clk_s : in std_logic;
constant g : in std_logic;
constant expf : in std_logic;
signal fire_s : in std_logic) is
begin
go_s <= g;
wait until rising_edge(clk_s);
wait for 1 ns;
assert fire_s = expf
report "latch/mismatch: fire /= expected" severity error;
end procedure;
begin
clk <= not clk after 5 ns;
-- Bind to fsm_good to PASS; to fsm_bad to observe the latch.
dut : entity work.fsm_good
port map (clk => clk, rst => rst, go => go, fire => fire);
stim : process
begin
rst <= '1'; go <= '0';
wait until rising_edge(clk); wait for 1 ns; rst <= '0';
step(go, clk, '1', '0', fire); -- IDLE -> ARM1
step(go, clk, '0', '0', fire); -- ARM1 -> IDLE (bug: latches, sticks)
step(go, clk, '1', '0', fire); -- IDLE -> ARM1
step(go, clk, '1', '0', fire); -- ARM1 -> ARM2
step(go, clk, '1', '0', fire); -- ARM2 -> FIRE
step(go, clk, '1', '1', fire); -- FIRE, fire high
step(go, clk, '0', '0', fire); -- FIRE -> IDLE, fire low (bug: holds)
report "fsm_bug self-check complete" severity note;
wait;
end process;
end architecture;Across all three languages the lesson is one sentence: in a combinational FSM block, assign next_state and every output on every path — a leading default in SystemVerilog/Verilog, defaults plus when others => in VHDL — or the block becomes a latch and the machine sticks.
5. Verification Strategy
Two FSMs coded in different styles that implement the same machine must be behaviourally indistinguishable at the state level, so the verification job is to (1) prove the sequence and reset behaviour, (2) prove the combinational blocks are latch-free, and (3) confirm the outputs land with the timing the style intends. The single invariant that captures a correct FSM is:
From any reachable state, the state register advances to exactly the next state the diagram specifies for the current inputs, and each output takes exactly the value the diagram (and the chosen output-block timing) specifies — no held-over state, no stale output.
Everything below is that invariant made checkable, and it maps directly onto the clocked testbenches in §4.
- Clocked, self-checking sequence test. Drive the input sequence that walks the machine through its states and, after each rising edge, assert
state/fireagainst a reference. The three §4a testbenches do exactly this: SVassert (fire === exp_fire), Verilogif (fire !== exp_fire) $display("FAIL…"), VHDLassert fire = expf report … severity error. Because the machine is small, the walk can cover every state and every legal transition. - The latch-exposing check — drive the path that hits the unassigned branch. The missing-default latch only shows on the input order that reaches the omitted assignment. Here that is go-low from an armed state: a correct FSM unwinds to
IDLE; the buggy one holds the stalenext_stateand sticks. Every §4 testbench deliberately drivesgolow fromARM1, so binding it to the buggy module fails and binding it to the fixed module passes. Add adefault/when othersreachability nudge so lint also reports "latch inferred" — the two signals (a stuck sequence and a lint latch warning) together pin the bug. - Style-equivalence: two-block vs three-block produce the same behaviour. Instantiate the two-block and three-block versions side by side under the same clock and
gostimulus and assert their state trajectories match cycle-for-cycle. Their outputs will differ by the deliberate one-cycle registered-output delay of the three-block form — so comparefirewith a one-cycle skew, and confirm that the only difference is that intended latency, not a functional divergence. - Assert the combinational block has no latch. Beyond simulation, run lint/CDC with a "no inferred latch" rule on the next-state and combinational-output blocks; a clean report is the static proof that every path assigns
next_stateand every output. This complements the dynamic check — lint finds the latch even on a state the directed sequence never reaches. - Confirm output timing matches the intended Moore/Mealy. For a combinational Moore output, assert
fireis high in the same cycle the machine is inFIRE; for the registered output block, assertfireis high one cycle afternext_state == FIRE. The §4b testbench encodes that extra cycle explicitly — getting the expected-value schedule right is itself the check that the output block has the timing the style promised. - Reset behaviour. Assert that after
rst,stateisIDLEandfireis deasserted on the next edge, and that the machine is re-armable — the §4 tests re-drivegohigh after a reset-to-IDLE to prove the machine did not silently stick.
6. Common Mistakes
Combinational block missing a default → inferred latch (the signature FSM bug). In the next-state or combinational-output block, if some state/input path does not assign next_state (an if with no else, a case with no default) or leaves an output unassigned, the tool infers a latch to hold the value — and the FSM sticks in a state or drives a stale output. It compiles clean and passes a casual sim; it fails on the exact input order that reaches the unassigned path (full post-mortem in §7; buggy-vs-fixed RTL for all three HDLs in §4c). The fix is always the same: default-first — next_state = state; and out = <safe>; at the top of the block, then the case overrides.
Mixing blocking and non-blocking across the two kinds of block. The state register is sequential and must use non-blocking (state <= next_state); a state register written with blocking = races against any other logic that reads state in the same time step, and the RTL sim can disagree with the gate netlist. Conversely, a combinational next-state/output block written with <= computes stale values (the NBA update is deferred) and simulates unlike what it synthesizes. The rule is mechanical: <= in the clocked state register, = in the combinational blocks — never the reverse.
The one-block surprise — registered outputs a cycle late. Fold everything into a single clocked always and the outputs come out of flops: fire asserts one cycle after the state that should produce it, because it is sampled on the same edge the state advances. That is correct for a registered interface but wrong if you (or a downstream block) expected the combinational Moore timing of a two-block form. Choose the one-block style because you want registered outputs — not by accident — and never read a one-block FSM as if its outputs were combinational.
Incomplete sensitivity list on the combinational block. In Verilog, listing only some signals in the combinational block's sensitivity list (always @(state) instead of always @(*)) makes the block re-evaluate only when those signals change, so it misses input changes and simulates like unintended state — a sim/synth mismatch that looks like a latch but is a stale-sim artifact. Always use always @(*) (Verilog) / always_comb (SystemVerilog) / process(all) (VHDL-2008) so the block is sensitive to everything it reads.
Assigning the same signal in two blocks. In the three-block style, if two blocks both assign fire (or both assign next_state), you get multiple drivers — an elaboration error, or, worse, a silent race in a permissive tool. Each signal must be driven by exactly one block: state by the register block, next_state by the next-state block, each output by exactly one output block.
Forgetting to make illegal states recover. A case over an enumerated state that omits a default/when others leaves illegal state encodings (reachable after an upset, or on power-up before reset) with no defined transition — another latch, and a machine that can lock up in an unused code. Add a default: next_state = IDLE; (or when others => next_state <= IDLE;) so any illegal state steers back to a safe one. (Safe-FSM recovery is the subject of a later section; the coding hook is: the default arm is both your latch guard and your illegal-state recovery.)
7. DebugLab
The FSM that armed but never disarmed — a missing default infers a latch
The engineering lesson: a combinational FSM block that fails to assign next_state or an output on some path does not compute "nothing" — it infers a latch that remembers the old value, turning a clean state machine into one with hidden storage that makes it stick. The tell is a bug that depends on input history ("the arm completes on two highs if go bounced"): history-dependence in logic you designed to be a stateless cloud means you built state you did not intend. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: default-assign next_state and every output at the top of every combinational block (a leading assignment, plus a default/when others arm), and drive the input path that reaches every branch in a clocked self-check — especially the "input dropped mid-sequence" path the happy-path smoke test skips.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses the "separate clocked from combinational, assign every path" habit.
Exercise 1 — Style for the requirement
For each requirement, state whether you'd code the FSM one-block, two-block, or three-block, and why in one line: (a) a small controller whose outputs feed straight into another block's combinational logic this cycle; (b) a controller whose outputs cross a module boundary and must be glitch-free; (c) a Mealy machine whose output must react to an input within the same cycle; (d) a machine where you want the outputs registered and don't mind — in fact want — the extra cycle. (Hint: the question is always where should the output logic live and with what timing?)
Exercise 2 — Find the latch on paper
You are handed a two-block FSM whose combinational block is a case (state) with no leading default and where two of the four states only assign next_state inside an if (go) with no else. (i) List exactly which signal(s) latch and on which state/input combinations. (ii) Give the single input sequence (a go waveform) that makes the machine visibly stick, and say why the happy-path smoke test misses it. (iii) Write the two lines that fix it and explain why they make every path total. (iv) State how the same bug and fix would read in VHDL.
Exercise 3 — Convert two-block to three-block, deliberately
Take the two-block arming FSM from §4a (combinational Moore fire). Convert it to three-block by pulling fire into its own registered output block. (i) Write the output block. (ii) State precisely how the observed timing of fire changes relative to the two-block version. (iii) Describe the change you must make to the testbench's expected-value schedule to keep it passing, and why the state-trajectory check needs no change. (iv) Name one interface situation where this registered version is the better choice and one where it is the wrong choice.
Exercise 4 — Diagnose the blocking/non-blocking swap
An engineer's FSM simulates correctly in some tools and wrongly in others, and the gate netlist disagrees with the RTL. Inspecting the code you find the state register uses blocking = and the combinational next-state block uses non-blocking <= — both assignments swapped from the rule. (i) Explain, for each block, what goes wrong (race in the register; stale/deferred values in the comb block) and why it is tool- and ordering-dependent. (ii) State the corrected assignment for each block. (iii) Explain why the sim/synth mismatch is more dangerous than a hard error — and what verification signal (beyond "it passed on my simulator") would have flagged it.
10. Related Tutorials
Continue in RTL Design Patterns (forward links unlock as they ship):
- FSM Fundamentals — Chapter 4.1; the three-piece FSM model (state register, next-state logic, output logic) this page teaches you to code cleanly.
- Moore vs Mealy Outputs — Chapter 4.2; the output-timing distinction that decides whether your output block is combinational or registered.
- FSM State Encoding — Chapter 4.4; binary, one-hot, and gray state codes — the
typedef enum/localparamyour coding style declares. - Safe FSM Design — recovering from illegal states, where the
default/when othersarm that guards against a latch also becomes your lock-up recovery. - FSM + Datapath — the control FSM steering a datapath; the pattern these coding styles scale up into.
- FSM Worked Example — an end-to-end machine built with the two-/three-block discipline from this page.
Backward / foundations this page builds on:
- The Register Pattern (D-FF) — Chapter 2.1; the sample-and-hold and the non-blocking discipline that governs the FSM's state register.
- Synchronous Reset — Chapter 2; the reset style used to bring the state register to
IDLE. - Multiplexers (2:1, N:1, one-hot) — Chapter 1.1; next-state logic is a mux over the current state, and it inherits the same default-assignment latch rule.
- The RTL Design Mindset — Chapter 0.2; the State → Control → Verification lenses this page applies to FSM coding.
Verilog v1 prerequisites (the grammar this pattern transcribes into):
- Case Statements — the construct at the heart of the next-state block, and where
defaultand latch inference live. - Blocking and Non-Blocking Assignments — why the state register uses
<=and the combinational blocks use=. - If-Else Statements — the conditional whose missing
elseis the classic latch source in a next-state block. - Initial and Always Blocks — the
always @(posedge clk)vsalways @(*)distinction the two-block split is built on.
11. Summary
- An FSM is one clocked box surrounded by combinational clouds; coding style is just how many
alwaysblocks you draw that with. The state register is the only sequential element — everything else is stateless next-state and output logic. One-block folds it all into a clocked block (outputs registered); two-block = state register + one combinational block for next-state and outputs; three-block = state register + next-state block + a separate output block. - Two-block is the default because it is readable and latch-safe. The split mirrors the hardware, so a reader sees the state diagram, and the latch question lives in one obviously-combinational block where the rule is mechanical:
next_state = state;(default) first, then thecaseoverrides. - The clocked box uses non-blocking; the clouds use full-assignment.
state <= next_state(<=) in the clocked state register keeps the update atomic and race-free;next_state = …andout = …(=) in the combinational blocks. Cross the rule and you get races or a sim/synth mismatch. - Where the output logic lives sets its timing — the Moore/Mealy decision in code. A combinational output block gives same-cycle outputs (matching the raw state diagram); a registered output block (a second clocked box) gives glitch-free outputs one cycle late. Three-block exists so you can make that choice deliberately instead of inheriting the one-block form's always-registered outputs.
- A combinational block missing a default infers a latch — the signature FSM bug. Leave
next_stateor an output unassigned on some path and the tool builds a latch: the machine sticks or the output goes stale, and it only fails on the input order that reaches the unassigned branch (§7). Assign every output on every path — leading default in SystemVerilog/Verilog, defaults pluswhen others =>in VHDL — and verify by driving the input-dropped-mid-sequence path in a clocked, self-checking testbench. Built and self-check-verified here in SystemVerilog, Verilog, and VHDL.