Skip to content

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-need.v — one machine, three ways to write it; only some are safe
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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 reads state and inputs, computes the next state, updates state <= next, and assigns the outputs — all with non-blocking assignment, all on the clock edge. It is compact and has no separate next_state signal. 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 one always_comb block that computes next_state and the outputs from state and 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 flow
FSM coding — one clocked box, one or two combinational cloudsstate registerCLOCKED · state <= next_state (NBA)next-state logicCOMB · always_comb, default-first (=)one-blockall folded into clocked · outputs REGISTEREDtwo-block(default)state reg + ONE comb (next + out)3-block · comboutputseparate comb block · Mealy, same cycle3-block · regoutputseparate clocked block · glitch-free, +1 cycle
The state register is the only clocked box (non-blocking). Next-state is a combinational cloud (full-assignment, default-first). Two-block folds outputs into that one cloud; three-block gives outputs their own block — combinational for same-cycle Mealy/Moore, or clocked for glitch-free registered outputs one cycle later. One-block folds everything into the clocked box, so its outputs are always registered. Same machine, same states; where the output logic lives sets its timing. Language-independent: identical in SystemVerilog, Verilog, and VHDL.

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

fsm_two_block.sv — two-block FSM (always_ff state reg + always_comb next/out)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

fsm_two_block_tb.sv — clocked self-check: three highs fire; a low resets (would expose a latch)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

fsm_two_block.v — two-block FSM in Verilog (posedge clk reg + always @(*))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
fsm_two_block_tb.v — clocked self-check; drives go, prints PASS/FAIL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

fsm_two_block.vhd — two-block FSM in VHDL (clocked process + combinational process)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
fsm_two_block_tb.vhd — clocked self-check (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

fsm_three_block.sv — three-block FSM (state reg + comb next-state + REGISTERED output)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

fsm_three_block_tb.sv — clocked self-check accounting for the registered-output cycle
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

fsm_three_block.v — three-block FSM in Verilog (registered output block)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
fsm_three_block_tb.v — clocked self-check for the registered output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

fsm_three_block.vhd — three-block FSM in VHDL (registered output process)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
fsm_three_block_tb.vhd — clocked self-check (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

fsm_bug.sv — BUGGY (missing default → latch) vs FIXED (default-first)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
fsm_bug_tb.sv — clocked self-check; the go-low-from-ARM1 path exposes the latch
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

fsm_bug.v — BUGGY vs FIXED in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
fsm_bug_tb.v — self-checking; the go-low path from ARM1 catches the latch
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

fsm_bug.vhd — BUGGY (no defaults) vs FIXED (default-first + when others)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
fsm_bug_tb.vhd — clocked self-check; sweep the go-low-from-ARM1 path (assert report)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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/fire against a reference. The three §4a testbenches do exactly this: SV assert (fire === exp_fire), Verilog if (fire !== exp_fire) $display("FAIL…"), VHDL assert 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 stale next_state and sticks. Every §4 testbench deliberately drives go low from ARM1, so binding it to the buggy module fails and binding it to the fixed module passes. Add a default/when others reachability 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 go stimulus 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 compare fire with 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_state and 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 fire is high in the same cycle the machine is in FIRE; for the registered output block, assert fire is high one cycle after next_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, state is IDLE and fire is deasserted on the next edge, and that the machine is re-armable — the §4 tests re-drive go high 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-firstnext_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.

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 / localparam your coding style declares.
  • Safe FSM Design — recovering from illegal states, where the default/when others arm 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):

11. Summary

  • An FSM is one clocked box surrounded by combinational clouds; coding style is just how many always blocks 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 the case overrides.
  • 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 = … and out = … (=) 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_state or 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 plus when 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.