Finite State Machines are the backbone of digital control logic — protocol controllers, parsers, sequencers, and arbiters all reduce to FSMs. This tutorial covers Moore vs Mealy, encoding styles, the canonical 3-always coding pattern, and four complete examples ready to synthesize.
| Property | Moore | Mealy |
|---|---|---|
| Output depends on | State only | State AND current input |
| Output changes | Only on clock edge | Combinationally (can glitch) |
| Response latency | One extra cycle | Same cycle as input |
| Typical state count | More states needed | Fewer states |
| Glitch risk | None | Yes — register output to fix |
| Synthesis preference | Simpler and safer | Faster but needs care |
| Encoding | Flip-flops needed | Next-state logic | Best for |
|---|---|---|---|
| Binary | log₂(N) | More complex | ASIC, many states |
| One-hot | N | Simpler (single-bit checks) | FPGA, fewer states |
| Gray code | log₂(N) | Moderate | Low power, CDC crossing |
// Binary encoding (synthesis tool assigns by default) parameter IDLE = 2'b00, FETCH = 2'b01, EXEC = 2'b10, DONE = 2'b11; reg [1:0] state, next_state; // One-hot encoding parameter IDLE = 4'b0001, FETCH = 4'b0010, EXEC = 4'b0100, DONE = 4'b1000; reg [3:0] state, next_state;
The 3-always pattern separates concerns cleanly and is the most widely accepted FSM template in RTL design:
// Block 1 — State Register (sequential) always @(posedge clk or negedge rst_n) if (!rst_n) state <= IDLE; else state <= next_state; // Block 2 — Next-State Logic (combinational) always @(*) begin next_state = state; // default: stay in current state case (state) IDLE: if (start) next_state = FETCH; FETCH: next_state = EXEC; EXEC: if (done) next_state = IDLE; default: next_state = IDLE; // self-correcting endcase end // Block 3 — Output Logic (combinational — Moore) always @(*) begin out = 0; // default outputs case (state) IDLE: out = 1; EXEC: out = 0; default: out = 0; endcase end
next_state = state and out = 0 before the case statement prevents synthesis from inferring latches when a case branch omits an assignment.A classic Moore FSM — outputs (RED, YELLOW, GREEN) depend only on the current state, not on external inputs during a state.
module traffic_light ( input clk, rst_n, output reg [1:0] light // 2'b00=RED, 2'b01=YELLOW, 2'b10=GREEN ); parameter RED = 2'd0, GREEN = 2'd1, YELLOW = 2'd2; reg [1:0] state, next_state; reg [4:0] cnt; // timer (counts clock cycles) // State register always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= RED; cnt <= 0; end else begin state <= next_state; cnt <= cnt + 1; end end // Next-state logic always @(*) begin next_state = state; case (state) RED: if (cnt == 5'd29) next_state = GREEN; // 30 cycles red GREEN: if (cnt == 5'd19) next_state = YELLOW; // 20 cycles green YELLOW: if (cnt == 5'd4) next_state = RED; // 5 cycles yellow default: next_state = RED; endcase end // Output logic (Moore — state only) always @(*) begin case (state) RED: light = 2'b00; GREEN: light = 2'b10; YELLOW: light = 2'b01; default: light = 2'b00; endcase end endmodule
Detects the pattern 101 in a serial bit stream. Output det goes high the same cycle the third bit is received.
// States: track how many consecutive bits of "101" matched // S0=start, S1=got_1, S2=got_10, DONE on "101" module seq_det_101 ( input clk, rst_n, din, output det ); localparam S0 = 2'd0, S1 = 2'd1, S2 = 2'd2; reg [1:0] state, next_state; reg det_reg; // State register always @(posedge clk or negedge rst_n) if (!rst_n) state <= S0; else state <= next_state; // Next-state + Mealy output (combinational) always @(*) begin next_state = S0; det_reg = 0; case (state) S0: if (din) next_state = S1; // got first 1 else next_state = S0; S1: if (!din) next_state = S2; // got 10 else next_state = S1; // got 11 → still at S1 S2: if (din) begin // got 101 ! det_reg = 1; // Mealy output next_state = S1; // overlapping: "1" could be next end else next_state = S0; default: next_state = S0; endcase end assign det = det_reg; endmodule
din=1 completes the pattern, we go back to S1 (not S0) because the detected '1' could be the start of the next '101'. This is overlapping sequence detection.A simplified UART receiver that detects the start bit, samples 8 data bits, and validates the stop bit. Clock runs at 8× the baud rate (oversampling by 8).
module uart_rx #(parameter CLK_PER_BIT=868) ( input clk, rst_n, rx, output reg [7:0] data, output reg valid ); localparam IDLE = 2'd0, START = 2'd1, DATA = 2'd2, STOP = 2'd3; reg [1:0] state; reg [9:0] clk_cnt; reg [2:0] bit_cnt; reg [7:0] shift_reg; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state<=IDLE; clk_cnt<=0; bit_cnt<=0; valid<=0; end else begin valid <= 0; case (state) IDLE: if (!rx) begin // start bit detected (low) state <= START; clk_cnt <= 0; end START: if (clk_cnt == CLK_PER_BIT/2-1) begin clk_cnt <= 0; bit_cnt <= 0; state <= DATA; end else clk_cnt <= clk_cnt + 1; DATA: if (clk_cnt == CLK_PER_BIT-1) begin shift_reg <= {rx, shift_reg[7:1]}; clk_cnt <= 0; if (bit_cnt == 7) state <= STOP; else bit_cnt <= bit_cnt + 1; end else clk_cnt <= clk_cnt + 1; STOP: if (clk_cnt == CLK_PER_BIT-1) begin data <= shift_reg; valid <= rx; // valid only if stop bit is high state <= IDLE; clk_cnt <= 0; end else clk_cnt <= clk_cnt + 1; endcase end end endmodule
One-hot is ideal for FPGAs because the next-state logic for each state only needs to check a single bit. Here is the same 3-state FSM rewritten with one-hot encoding:
module fsm_onehot (input clk, rst_n, go, done, output busy); localparam [2:0] IDLE = 3'b001, WORK = 3'b010, FINISH= 3'b100; reg [2:0] state; always @(posedge clk or negedge rst_n) if (!rst_n) state <= IDLE; else case (1'b1) // match against whichever bit is set state[0]: state <= go ? WORK : IDLE; state[1]: state <= done ? FINISH : WORK; state[2]: state <= IDLE; default: state <= IDLE; endcase assign busy = state[1]; // WORK state — direct bit check endmodule
The case (1'b1) idiom checks which bit in state is 1 and dispatches accordingly — every branch has a single-bit condition, which maps directly to fast look-up table (LUT) logic on FPGAs.
| Practice | Why it matters |
|---|---|
Always include a default case | Self-correcting FSM; prevents latch inference; synthesis can use don't-cares only if you explicitly cover them |
| Set default outputs before case statement | Prevents latch inference in the output block |
Use localparam for state names | Readable code; prevents accidental override by defparam |
| Register Mealy outputs | Eliminates glitches at the cost of one extra cycle latency |
| Reset to a known state | Avoids undefined startup behavior and X propagation |
Use $display in testbench to trace states | Far easier to debug transitions in simulation than waveforms alone |
| Assert each state is mutually exclusive in one-hot | Detects bit-flip errors early in simulation |
next_state = IDLE only inside case branches and omit the default assignment before the case, any uncovered state causes synthesis to infer a latch for next_state. Always assign before the case: next_state = state;