Skip to content

RTL Design Patterns · Chapter 4 · FSM Design

State Encoding (binary/gray/one-hot)

An FSM state register holds a number, one per state, and something has to decide what those numbers are. That decision is state encoding, and it changes cost and robustness without changing what the machine does. Binary packs the states into the fewest flops but pays for a wide decoder. Gray makes adjacent states differ by one bit, cutting switching power where the state walk is linear. One-hot spends one flop per state so that exactly one bit is high, making the decode trivial and fast and a great fit for flop-rich FPGA fabric. The catch with one-hot is that a glitch or reset gap can drop it into an illegal multi-hot or all-zero state that a naive decode never survives. This lesson encodes the same FSM three ways, proves they behave identically, weighs the area, speed, and power trade-offs, and shows the safe-recovery discipline, in SystemVerilog, Verilog, and VHDL.

Intermediate14 min readRTL Design PatternsState EncodingOne-HotGray CodeFSM DesignSynthesis Trade-off

Chapter 4 · Section 4.4 · FSM Design

1. The Engineering Problem

You have an FSM that works. It came out of 4.1 as three clean pieces — a state register, next-state logic, output logic — and it passes every functional test: it walks its states in the right order and raises the right outputs. You wrote the states as an enumerated type and never thought about the numbers behind the names, because functionally the numbers do not matter. And that is exactly right: the encoding does not change what the machine does. So why is it a flagship topic?

Because three different engineers hand you three builds of the same machine and they are not the same hardware. The first synthesized the states densely into log2(N) flops; it is small, but the timing report shows the critical path running through a wide next-state decoder, and on a fast clock that decode is the bottleneck. The second targeted an FPGA and let the tool spread the states one-per-flop; timing closes easily and the decode is trivial, but it burns far more flip-flops, and a field unit came back from a high-radiation deployment stuck in a state that the FSM diagram does not even contain — two state bits high at once, and the machine never recovered. The third encoded a long linear count-like sequence so that each step flips only one bit; the dynamic power dropped measurably because far fewer flops toggle per cycle. Same specification, same behaviour in simulation, three genuinely different chips — smaller versus faster versus lower-power, robust versus fragile — and the only thing that differed was the numbers assigned to the states.

That is the engineering content of this page: state encoding is a deliberate cost-and-robustness choice, not a formatting detail. It trades area (how many flops the state register costs) against speed (how deep the next-state and output decode is) against power (how many flops toggle per transition) against safety (how a stray illegal code is handled). Choose it on purpose — binary when flops are precious and the decode is fine, one-hot when the decode is the critical path and flops are cheap (an FPGA), gray where the sequence is linear and switching power matters — and treat it as the synthesis attribute it actually is.

the-need.v — same five states, three sets of numbers, three different chips
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // Five states. Functionally identical however you number them:
   // BINARY : IDLE=000 S1=001 S2=010 S3=011 S4=100   -> 3 flops, WIDE decode
   // GRAY   : IDLE=000 S1=001 S2=011 S3=010 S4=110   -> 3 flops, 1-bit steps (low switching)
   // ONEHOT : IDLE=00001 S1=00010 S2=00100 S3=01000 S4=10000 -> 5 flops, TRIVIAL decode
 
   // The machine's BEHAVIOUR is the same in all three. What changes:
   //   flops in the state register (area), decode depth (speed), flops toggling (power),
   //   and how a stray ILLEGAL code (a multi-hot / all-zero one-hot value) is survived.
   // Encoding is a design/synthesis choice, not a functional one — this page makes it deliberate.

2. Mental Model

3. Pattern Anatomy

The three FSM pieces from 4.1 are unchanged. Encoding is entirely a choice about the numeric codes the state register holds — and, as a consequence, about how wide the next-state and output decode has to be, how many flops toggle per transition, and how many illegal codes exist.

Binary encoding — the dense default. Number the N states 0, 1, 2, … in a ceil(log2(N))-bit register. This is the smallest possible state register — three flops cover up to eight states — and it is what a synthesis tool defaults to on an ASIC flow, where flops cost area. The cost lives in the decode: because the state is packed, every next-state and output decision must un-pack it, and asking IS THE MACHINE IN STATE S3 is a full log2(N)-bit comparison. For a machine with many states and complex output logic, that decode can be wide and become the critical path.

Gray encoding — one bit at a time. Number the states so that each state differs from the next one by exactly a single bit — the gray code you met with gray counters in 3.3. It uses the same log2(N) flops as binary, so it is no bigger, and its payoff is switching power: when the machine walks a linear sequence of states, only one flop toggles per transition instead of the up-to-log2(N) flops a binary count can flip (think 011 → 100, three bits at once, versus a one-bit gray step). Gray is the right encoding for FSMs whose state graph is a line — a counter-like walk, a shift sequence, a phase machine — and it also gives the CDC benefit gray counters give: a sampled-mid-transition value is only ever off by one adjacent state, never a wild binary-multi-bit intermediate. Its limit is honesty: a general FSM graph with branches and merges cannot keep single-bit adjacency on every edge, so gray's guarantee only fully holds on the linear portions.

One-hot encoding — one flop per state. Give each state its own flop; exactly one bit is high at a time (IDLE = 00001, S1 = 00010, …). The register is now N flops — the most expensive — but the decode nearly disappears: IS THE MACHINE IN STATE S3 is the single wire onehot[3], and the next-state logic is a set of small, shallow gates rather than a decoder. That makes one-hot fast and FPGA-friendly, because FPGAs are rich in flops and one-hot turns the state register's bits directly into the select lines of the output and next-state muxes (see 1.1). The catch is structural: only N of the 2^N possible register values are legal one-hot codes, so any stray value — two bits high (multi-hot) or none (all-zero) — is an illegal state the decode was not written to handle, and this is the failure §7 is built around.

The same machine, three alphabets — flops on one side, decode on the other

data flow
The same machine, three alphabets — flops on one side, decode on the otherlog2(N)log2(N)Nflopsun-packonebitrare codes5 abstract statesIDLE, S1, S2, S3, S4 — behaviour fixedbinary encoding3 flops, dense, WIDE decodegray encoding3 flops, 1-bit steps, low switchingone-hot encoding5 flops, ONE bit/state, trivial decodenext-state /outputwide compare vs single-bit testillegal codesone-hot: multi-hot / all-zero risk
Start from the same five abstract states — the behaviour is fixed. Binary and gray both spend log2(N) flops; binary packs the codes densely (a wide decoder un-packs them) while gray orders them so adjacent states differ by one bit (fewer flops toggle, lower switching power on a linear walk). One-hot spends N flops so exactly one bit is high per state, which turns every 'am I in state X' question into a single-bit test — a trivial, fast decode — but leaves only N legal codes out of 2^N, so a stray multi-hot or all-zero value is an illegal state. Same machine, different cost and robustness. This mapping is identical in SystemVerilog, Verilog, and VHDL.

The trade-off is easiest to hold as a table. Read it as where each encoding spends its budget — flops, decode depth, switching, illegal-code exposure — not as a ranking, because the right choice depends on the target and the state graph.

EncodingState registerNext-state / output decodeSwitching powerIllegal-code exposureBest when
Binaryceil(log2(N)) flops — smallestwidest — must un-pack the code (a compare per state test)up to log2(N) flops toggle per steponly the unused codes when N is not a power of twoflops are precious (ASIC area), decode is not the critical path
Grayceil(log2(N)) flops — same as binarysimilar to binary (still packed)lowest on a linear walk — one flop toggles per adjacent stepsame unused-code exposure as binarythe state graph is linear/counter-like; switching power or CDC-sampling matters
One-hotN flops — largesttrivial — one-bit test per state, shallow/fastone flop clears and one sets per step (2 toggles)highest — only N legal of 2^N codes (multi-hot / all-zero)decode/timing is the bottleneck; flops are cheap (FPGA); need a flat, fast machine

Two anatomy points close the section. First, encoding is a synthesis attribute, not just RTL you hand-write. You can hand-code any encoding (a localparam/enum with explicit codes, a one-hot vector), or you can write the states abstractly and tell the tool which encoding to use via an attribute — syn_encoding / fsm_encoding in the constraints, or a synthesis directive — and let it re-encode. Both are legitimate; the point is the encoding is a knob, and the tool will often re-encode an enumerated FSM to one-hot automatically on an FPGA target. Second, the reset value must be a legal code. In binary that is automatic; in one-hot the reset must set exactly the idle bit (5'b00001), never all-zero — an all-zero reset is itself an illegal one-hot state, which is one of the two failures §7 examines.

4. Real RTL Implementation

This is the core of the page. We take one small machine — a five-state controller IDLE → S1 → S2 → S3 → S4 → IDLE that raises a one-cycle done in S4, advanced by a go/stall input — and encode it two ways: (a) binary (localparam/enum in log2(N) flops with a decoded output), and (b) one-hot (one bit per state, single-bit decode), then (c) show the one-hot illegal-state bug beside its safe-recovery fix. Each is in SystemVerilog, Verilog, and VHDL with an RTL block and a self-checking clocked testbench, and the binary and one-hot testbenches drive the same stimulus and assert identical behaviour — the proof that encoding is a cost choice, not a functional one. Gray is encoded and explained for the linear-sequence case where its one-bit-per-step advantage is real.

Two conventions run through every block. The reset is a synchronous, active-high rst (as in 2.3) returning the machine to IDLE; in the one-hot version, reset sets the register to the legal idle code …0001, never all-zero. The state names describe the step; the numeric codes are the only thing that changes between (a) and (b).

4a. Binary encoding — the dense default (log2(N) flops, decoded output)

The binary machine numbers five states into three flops and decodes the output with a compare against the S4 code. This is the smallest register; the output and next-state logic pay for it by un-packing the code.

fsm_binary.sv — 5-state controller, BINARY encoding (3 flops, decoded output)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module fsm_binary (
       input  logic clk,
       input  logic rst,                         // synchronous, active-high
       input  logic go,                          // advance when high
       input  logic stall,                       // hold when high (go ignored)
       output logic done                         // 1-cycle pulse in S4
   );
       // BINARY codes: 5 states packed into ceil(log2(5)) = 3 flops.
       typedef enum logic [2:0] {
           IDLE = 3'd0, S1 = 3'd1, S2 = 3'd2, S3 = 3'd3, S4 = 3'd4
       } state_t;
       state_t state, next_state;
 
       // NEXT-STATE: advance on go unless stalled. Behaviour is encoding-independent.
       always_comb begin
           next_state = state;                   // default hold (latch-free)
           if (go && !stall) unique case (state)
               IDLE: next_state = S1;
               S1:   next_state = S2;
               S2:   next_state = S3;
               S3:   next_state = S4;
               S4:   next_state = IDLE;
               default: next_state = IDLE;        // unused codes 5,6,7 -> recover to IDLE
           endcase
       end
 
       // STATE register: synchronous reset to the IDLE code.
       always_ff @(posedge clk)
           if (rst) state <= IDLE;
           else     state <= next_state;
 
       // OUTPUT: MOORE, decoded from the packed state. In binary this is a COMPARE
       // (state == S4) — the decode a one-hot version replaces with a single-bit test.
       assign done = (state == S4);
   endmodule

The clocked, self-checking testbench walks the machine through a full lap plus a stall, and checks done cycle by cycle against a reference of when it should pulse.

fsm_binary_tb.sv — clocked self-check: done pulses only when the machine is in S4
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module fsm_binary_tb;
       logic clk = 1'b0, rst, go, stall, done;
       int   errors = 0;
 
       fsm_binary dut (.clk(clk), .rst(rst), .go(go), .stall(stall), .done(done));
 
       always #5 clk = ~clk;
 
       task step(input logic g, input logic s, input logic exp);
           go = g; stall = s;
           @(posedge clk); #1;                   // sample after the edge that loaded state
           assert (done === exp)
               else begin $error("go=%b stall=%b done=%b exp=%b", g, s, done, exp); errors++; end
       endtask
 
       initial begin
           rst = 1'b1; go = 1'b0; stall = 1'b0;
           @(posedge clk); #1; rst = 1'b0;
           // IDLE->S1->S2->S3->S4 (done here) ->IDLE, then a STALL holds in place.
           step(1'b1, 1'b0, 1'b0);               // now S1
           step(1'b1, 1'b0, 1'b0);               // now S2
           step(1'b1, 1'b0, 1'b0);               // now S3
           step(1'b1, 1'b0, 1'b1);               // now S4 -> done
           step(1'b1, 1'b1, 1'b0);               // stall in S4 -> hold, no advance
           step(1'b1, 1'b0, 1'b0);               // now IDLE
           if (errors == 0) $display("PASS: binary FSM done pulses exactly in S4");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog form is the same machine with localparam codes and reg/wire typing; the output is a compare off the packed state.

fsm_binary.v — BINARY 5-state controller in Verilog (3 flops, decoded output)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module fsm_binary (
       input  wire clk,
       input  wire rst,
       input  wire go,
       input  wire stall,
       output wire done
   );
       localparam [2:0] IDLE = 3'd0, S1 = 3'd1, S2 = 3'd2, S3 = 3'd3, S4 = 3'd4;
       reg [2:0] state, next_state;
 
       always @(*) begin
           next_state = state;                   // default hold => no latch
           if (go && !stall) begin
               case (state)
                   IDLE:    next_state = S1;
                   S1:      next_state = S2;
                   S2:      next_state = S3;
                   S3:      next_state = S4;
                   S4:      next_state = IDLE;
                   default: next_state = IDLE;    // unused codes 5..7 -> recover to IDLE
               endcase
           end
       end
 
       always @(posedge clk)
           if (rst) state <= IDLE;
           else     state <= next_state;
 
       // MOORE output decoded from the packed state: a COMPARE against the S4 code.
       assign done = (state == S4);
   endmodule
fsm_binary_tb.v — self-checking Verilog TB (done only in S4, PASS/FAIL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module fsm_binary_tb;
       reg  clk, rst, go, stall;
       wire done;
       integer errors;
 
       fsm_binary dut (.clk(clk), .rst(rst), .go(go), .stall(stall), .done(done));
 
       initial clk = 1'b0;
       always #5 clk = ~clk;
 
       task step(input g, input s, input exp);
           begin
               go = g; stall = s;
               @(posedge clk); #1;
               if (done !== exp) begin
                   $display("FAIL: go=%b stall=%b done=%b exp=%b", g, s, done, exp);
                   errors = errors + 1;
               end
           end
       endtask
 
       initial begin
           errors = 0;
           rst = 1'b1; go = 1'b0; stall = 1'b0;
           @(posedge clk); #1; rst = 1'b0;
           step(1'b1, 1'b0, 1'b0);   // S1
           step(1'b1, 1'b0, 1'b0);   // S2
           step(1'b1, 1'b0, 1'b0);   // S3
           step(1'b1, 1'b0, 1'b1);   // S4 -> done
           step(1'b1, 1'b1, 1'b0);   // stall -> hold
           step(1'b1, 1'b0, 1'b0);   // IDLE
           if (errors == 0) $display("PASS: binary FSM done pulses exactly in S4");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the states are an enumerated type; the synthesis tool assigns binary codes by default (and can be told otherwise via an fsm_encoding attribute). The output is a concurrent compare against S4.

fsm_binary.vhd — BINARY 5-state controller in VHDL (enumerated type, default codes)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity fsm_binary is
       port (
           clk   : in  std_logic;
           rst   : in  std_logic;                          -- synchronous, active-high
           go    : in  std_logic;
           stall : in  std_logic;
           done  : out std_logic                           -- 1-cycle pulse in S4
       );
   end entity;
 
   architecture rtl of fsm_binary is
       type state_t is (IDLE, S1, S2, S3, S4);             -- tool encodes; default binary
       signal state, next_state : state_t;
   begin
       -- NEXT-STATE: advance on go unless stalled (behaviour is encoding-independent).
       process (state, go, stall)
       begin
           next_state <= state;                            -- default hold
           if go = '1' and stall = '0' then
               case state is
                   when IDLE => next_state <= S1;
                   when S1   => next_state <= S2;
                   when S2   => next_state <= S3;
                   when S3   => next_state <= S4;
                   when S4   => next_state <= IDLE;
               end case;
           end if;
       end process;
 
       -- STATE register: synchronous reset to IDLE.
       process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then state <= IDLE; else state <= next_state; end if;
           end if;
       end process;
 
       -- MOORE output decoded from the state (a compare against S4).
       done <= '1' when state = S4 else '0';
   end architecture;
fsm_binary_tb.vhd — self-checking VHDL TB (assert done each cycle, severity error)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity fsm_binary_tb is
   end entity;
 
   architecture sim of fsm_binary_tb is
       signal clk   : std_logic := '0';
       signal rst   : std_logic := '1';
       signal go    : std_logic := '0';
       signal stall : std_logic := '0';
       signal done  : std_logic;
   begin
       dut : entity work.fsm_binary port map (clk => clk, rst => rst, go => go, stall => stall, done => done);
 
       clkgen : process
       begin
           clk <= '0'; wait for 5 ns;
           clk <= '1'; wait for 5 ns;
       end process;
 
       stim : process
           procedure step(g, s, exp : std_logic) is
           begin
               go <= g; stall <= s;
               wait until rising_edge(clk); wait for 1 ns;
               assert done = exp report "binary FSM done mismatch this cycle" severity error;
           end procedure;
       begin
           rst <= '1'; go <= '0'; stall <= '0';
           wait until rising_edge(clk); wait for 1 ns; rst <= '0';
           step('1', '0', '0');   -- S1
           step('1', '0', '0');   -- S2
           step('1', '0', '0');   -- S3
           step('1', '0', '1');   -- S4 -> done
           step('1', '1', '0');   -- stall -> hold
           step('1', '0', '0');   -- IDLE
           report "fsm_binary self-check complete" severity note;
           wait;
       end process;
   end architecture;

4b. One-hot encoding — the same machine, one flop per state (single-bit decode)

Now the identical machine, one-hot: five flops, exactly one high. Watch what happens to the decode — every state test is a single bit (state[S4_BIT]), and the reset value is the legal idle code, never all-zero.

fsm_onehot.sv — SAME controller, ONE-HOT encoding (5 flops, single-bit decode)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module fsm_onehot (
       input  logic clk,
       input  logic rst,
       input  logic go,
       input  logic stall,
       output logic done
   );
       // ONE-HOT: one flop per state; index each state's bit. Exactly one bit is high.
       localparam int IDLE = 0, S1 = 1, S2 = 2, S3 = 3, S4 = 4;
       logic [4:0] state, next_state;            // 5 flops (vs 3 in binary)
 
       // NEXT-STATE: shift the hot bit one step on go-and-not-stall. This IS the
       // next-state logic — no decoder needed, the hot bit already names the state.
       always_comb begin
           next_state = state;                   // default hold
           if (go && !stall) begin
               next_state = 5'b0;                // build the new one-hot from scratch
               if (state[IDLE]) next_state[S1]   = 1'b1;
               if (state[S1])   next_state[S2]   = 1'b1;
               if (state[S2])   next_state[S3]   = 1'b1;
               if (state[S3])   next_state[S4]   = 1'b1;
               if (state[S4])   next_state[IDLE] = 1'b1;
           end
       end
 
       // STATE register: synchronous reset to the LEGAL idle code (never all-zero).
       always_ff @(posedge clk)
           if (rst) state <= 5'b00001;           // = one-hot IDLE, a valid code
           else     state <= next_state;
 
       // OUTPUT: MOORE, but now the decode is a SINGLE BIT — no compare. This is the
       // whole appeal of one-hot: 'am I in S4' is just state[S4].
       assign done = state[S4];
   endmodule

The one-hot testbench drives the same stimulus as 4a and checks the same expected done sequence — a machine-checked proof that the two encodings implement identical behaviour.

fsm_onehot_tb.sv — SAME stimulus as binary; assert IDENTICAL done behaviour
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module fsm_onehot_tb;
       logic clk = 1'b0, rst, go, stall, done;
       int   errors = 0;
 
       fsm_onehot dut (.clk(clk), .rst(rst), .go(go), .stall(stall), .done(done));
 
       always #5 clk = ~clk;
 
       task step(input logic g, input logic s, input logic exp);
           go = g; stall = s;
           @(posedge clk); #1;
           // Same expectation vector as fsm_binary_tb => behaviour is encoding-independent.
           assert (done === exp)
               else begin $error("go=%b stall=%b done=%b exp=%b", g, s, done, exp); errors++; end
           // Structural invariant of a HEALTHY one-hot register: exactly one bit set.
           assert ($onehot(dut.state))
               else $error("state not one-hot: %b", dut.state);
       endtask
 
       initial begin
           rst = 1'b1; go = 1'b0; stall = 1'b0;
           @(posedge clk); #1; rst = 1'b0;
           step(1'b1, 1'b0, 1'b0);   // S1
           step(1'b1, 1'b0, 1'b0);   // S2
           step(1'b1, 1'b0, 1'b0);   // S3
           step(1'b1, 1'b0, 1'b1);   // S4 -> done (SAME cycle as binary)
           step(1'b1, 1'b1, 1'b0);   // stall -> hold
           step(1'b1, 1'b0, 1'b0);   // IDLE
           if (errors == 0) $display("PASS: one-hot FSM matches binary done, register stays one-hot");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog one-hot form is the same shift-the-hot-bit machine with a bit-indexed vector; the output is a single-bit select and the reset loads the legal idle code.

fsm_onehot.v — ONE-HOT controller in Verilog (5 flops, single-bit decode)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module fsm_onehot (
       input  wire clk,
       input  wire rst,
       input  wire go,
       input  wire stall,
       output wire done
   );
       localparam IDLE = 0, S1 = 1, S2 = 2, S3 = 3, S4 = 4;
       reg [4:0] state, next_state;
 
       always @(*) begin
           next_state = state;                   // default hold
           if (go && !stall) begin
               next_state = 5'b0;
               if (state[IDLE]) next_state[S1]   = 1'b1;
               if (state[S1])   next_state[S2]   = 1'b1;
               if (state[S2])   next_state[S3]   = 1'b1;
               if (state[S3])   next_state[S4]   = 1'b1;
               if (state[S4])   next_state[IDLE] = 1'b1;
           end
       end
 
       always @(posedge clk)
           if (rst) state <= 5'b00001;           // legal idle code, never all-zero
           else     state <= next_state;
 
       // Single-bit decode: the whole point of one-hot.
       assign done = state[S4];
   endmodule
fsm_onehot_tb.v — SAME stimulus; check done matches binary, register stays one-hot
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module fsm_onehot_tb;
       reg  clk, rst, go, stall;
       wire done;
       integer errors;
 
       fsm_onehot dut (.clk(clk), .rst(rst), .go(go), .stall(stall), .done(done));
 
       initial clk = 1'b0;
       always #5 clk = ~clk;
 
       task step(input g, input s, input exp);
           begin
               go = g; stall = s;
               @(posedge clk); #1;
               if (done !== exp) begin
                   $display("FAIL: go=%b stall=%b done=%b exp=%b", g, s, done, exp);
                   errors = errors + 1;
               end
               // one-hot invariant: (v & (v-1))==0 && v!=0 (exactly one bit set)
               if (!((dut.state & (dut.state - 5'b1)) == 5'b0 && dut.state != 5'b0)) begin
                   $display("FAIL: state not one-hot: %b", dut.state);
                   errors = errors + 1;
               end
           end
       endtask
 
       initial begin
           errors = 0;
           rst = 1'b1; go = 1'b0; stall = 1'b0;
           @(posedge clk); #1; rst = 1'b0;
           step(1'b1, 1'b0, 1'b0);   // S1
           step(1'b1, 1'b0, 1'b0);   // S2
           step(1'b1, 1'b0, 1'b0);   // S3
           step(1'b1, 1'b0, 1'b1);   // S4 -> done
           step(1'b1, 1'b1, 1'b0);   // stall
           step(1'b1, 1'b0, 1'b0);   // IDLE
           if (errors == 0) $display("PASS: one-hot matches binary done, register stays one-hot");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the one-hot state is a std_logic_vector; each state test reads one index, and the reset sets the legal idle bit.

fsm_onehot.vhd — ONE-HOT controller in VHDL (std_logic_vector, single-bit decode)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity fsm_onehot is
       port (
           clk   : in  std_logic;
           rst   : in  std_logic;
           go    : in  std_logic;
           stall : in  std_logic;
           done  : out std_logic
       );
   end entity;
 
   architecture rtl of fsm_onehot is
       -- ONE-HOT: one bit per state; indices name the states.
       constant IDLE : natural := 0;
       constant S1   : natural := 1;
       constant S2   : natural := 2;
       constant S3   : natural := 3;
       constant S4   : natural := 4;
       signal state, next_state : std_logic_vector(4 downto 0);
   begin
       process (state, go, stall)
       begin
           next_state <= state;                            -- default hold
           if go = '1' and stall = '0' then
               next_state <= (others => '0');
               if state(IDLE) = '1' then next_state(S1)   <= '1'; end if;
               if state(S1)   = '1' then next_state(S2)   <= '1'; end if;
               if state(S2)   = '1' then next_state(S3)   <= '1'; end if;
               if state(S3)   = '1' then next_state(S4)   <= '1'; end if;
               if state(S4)   = '1' then next_state(IDLE) <= '1'; end if;
           end if;
       end process;
 
       process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then state <= "00001";         -- legal idle code
               else                state <= next_state;
               end if;
           end if;
       end process;
 
       -- Single-bit decode.
       done <= state(S4);
   end architecture;
fsm_onehot_tb.vhd — SAME stimulus; assert done matches binary, register stays one-hot
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity fsm_onehot_tb is
   end entity;
 
   architecture sim of fsm_onehot_tb is
       signal clk   : std_logic := '0';
       signal rst   : std_logic := '1';
       signal go    : std_logic := '0';
       signal stall : std_logic := '0';
       signal done  : std_logic;
   begin
       dut : entity work.fsm_onehot port map (clk => clk, rst => rst, go => go, stall => stall, done => done);
 
       clkgen : process
       begin
           clk <= '0'; wait for 5 ns;
           clk <= '1'; wait for 5 ns;
       end process;
 
       stim : process
           procedure step(g, s, exp : std_logic) is
           begin
               go <= g; stall <= s;
               wait until rising_edge(clk); wait for 1 ns;
               assert done = exp report "one-hot FSM done /= binary this cycle" severity error;
           end procedure;
       begin
           rst <= '1'; go <= '0'; stall <= '0';
           wait until rising_edge(clk); wait for 1 ns; rst <= '0';
           step('1', '0', '0');   -- S1
           step('1', '0', '0');   -- S2
           step('1', '0', '0');   -- S3
           step('1', '0', '1');   -- S4 -> done
           step('1', '1', '0');   -- stall
           step('1', '0', '0');   -- IDLE
           report "fsm_onehot self-check complete" severity note;
           wait;
       end process;
   end architecture;

4c. Gray encoding — one bit per step on a linear sequence

Where the state graph is a line — the IDLE → S1 → S2 → S3 → S4 walk above is nearly one, minus the wrap — gray codes make each forward step flip a single flop, cutting switching. Below is the gray codebook for the walk and the single change it makes to the binary machine: swap the localparam codes for gray codes. Nothing else changes, because encoding is a cost choice, not a behaviour change.

fsm_gray.sv — gray codebook for the linear walk (one flop toggles per forward step)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // GRAY codes for the linear walk IDLE->S1->S2->S3->S4. Each ADJACENT step flips ONE bit:
   //   IDLE=000  S1=001  S2=011  S3=010  S4=110
   //   000->001 : bit0    001->011 : bit1    011->010 : bit0    010->110 : bit2
   // Same 3 flops as binary, but a binary count (011->100) would flip THREE bits at the
   // S3->S4 boundary; gray flips one. On a long linear sequence that is real dynamic power.
   localparam logic [2:0] IDLE = 3'b000, S1 = 3'b001, S2 = 3'b011, S3 = 3'b010, S4 = 3'b110;
   // Drop these codes into fsm_binary.sv in place of the 3'd0..3'd4 codes; behaviour is
   // identical (same next-state graph, same 'done = (state == S4)'). Only the toggling —
   // and hence the switching power on the linear portion of the walk — changes.

Gray's benefit is real only where adjacency holds. The forward walk here is linear, so each S(k) → S(k+1) step is a single-bit flip; but the wrap S4 → IDLE (110 → 000) flips two bits, and any branch in a busier FSM breaks single-bit adjacency on that edge. This is why gray is the encoding of choice for counter-like, phase-like, pointer-like machines (and the async-FIFO gray pointers of the CDC chapters), and why it is not a general-purpose FSM encoding: a machine with rich branching cannot keep one-bit steps on every transition. Its second, quieter benefit is the CDC one from 3.3 — a gray-coded state sampled by another clock mid-transition can only be read as one of the two adjacent states, never a wild multi-bit binary intermediate.

4d. The one-hot illegal-state bug — naive decode vs safe recovery

Pattern (d) is the flagship failure, shown as buggy vs fixed RTL in each language and dramatized in §7. A one-hot register has only N legal codes out of 2^N; an SEU, a glitch on a next-state term, or a reset that missed a flop can leave it multi-hot (two bits high) or all-zero (zero-hot). The buggy decode assumed exactly one bit is ever high, so it neither detects the illegal code nor recovers — the machine can output garbage and never return to a legal state. The fix detects the non-one-hot condition and forces a recovery to IDLE, previewing 4.5's safe-FSM discipline.

fsm_onehot_safe.sv — BUGGY (naive decode, no illegal handling) vs FIXED (detect + recover)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: the next-state logic ONLY moves the hot bit when it finds the EXPECTED single
   //        bit set. If the register is ever MULTI-HOT (SEU/glitch) or ALL-ZERO (reset gap),
   //        NONE of the 'if (state[k])' terms fire OR several fire — next_state can stay
   //        illegal forever, and 'done = state[S4]' can read high in a bogus state. No recovery.
   module fsm_onehot_bad (
       input  logic clk, rst, go, stall,
       output logic done
   );
       localparam int IDLE = 0, S1 = 1, S2 = 2, S3 = 3, S4 = 4;
       logic [4:0] state, next_state;
       always_comb begin
           next_state = state;
           if (go && !stall) begin
               next_state = 5'b0;
               if (state[IDLE]) next_state[S1]   = 1'b1;
               if (state[S1])   next_state[S2]   = 1'b1;
               if (state[S2])   next_state[S3]   = 1'b1;
               if (state[S3])   next_state[S4]   = 1'b1;
               if (state[S4])   next_state[IDLE] = 1'b1;
               // ILLEGAL code -> next_state stays 0 (all-zero) or multi-hot: NO recovery.
           end
       end
       always_ff @(posedge clk)
           if (rst) state <= 5'b00001;
           else     state <= next_state;
       assign done = state[S4];                  // trusts one-hotness blindly
   endmodule
 
   // FIXED: DETECT a non-one-hot register and force recovery to a known legal state (IDLE).
   //        'unique0 if / $onehot' expresses the exactly-one-hot INTENT; the else clause is
   //        the safety net that makes the machine SELF-HEALING (see 4.5, Safe FSM Design).
   module fsm_onehot_good (
       input  logic clk, rst, go, stall,
       output logic done
   );
       localparam int IDLE = 0, S1 = 1, S2 = 2, S3 = 3, S4 = 4;
       logic [4:0] state, next_state;
       always_comb begin
           next_state = state;
           if (!$onehot(state)) begin
               next_state = 5'b00001;            // illegal (multi-hot/zero-hot) -> recover to IDLE
           end else if (go && !stall) begin
               next_state = 5'b0;
               if (state[IDLE]) next_state[S1]   = 1'b1;
               if (state[S1])   next_state[S2]   = 1'b1;
               if (state[S2])   next_state[S3]   = 1'b1;
               if (state[S3])   next_state[S4]   = 1'b1;
               if (state[S4])   next_state[IDLE] = 1'b1;
           end
       end
       always_ff @(posedge clk)
           if (rst) state <= 5'b00001;
           else     state <= next_state;
       // Qualify the output so a transient illegal code cannot assert done.
       assign done = state[S4] && $onehot(state);
   endmodule

The self-checking testbench forces an illegal one-hot value into the register and checks that the fixed machine detects it and recovers to a legal state within a cycle, while the buggy one can get stuck.

fsm_onehot_safe_tb.sv — force a multi-hot/zero-hot state, assert recovery (bind _good)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module fsm_onehot_safe_tb;
       logic clk = 1'b0, rst, go, stall, done;
       int   errors = 0;
 
       // Bind to fsm_onehot_good: it detects the illegal code and recovers to IDLE.
       // fsm_onehot_bad, given the same forced state, can stay illegal (never one-hot again).
       fsm_onehot_good dut (.clk(clk), .rst(rst), .go(go), .stall(stall), .done(done));
 
       always #5 clk = ~clk;
 
       initial begin
           rst = 1'b1; go = 1'b0; stall = 1'b0;
           @(posedge clk); #1; rst = 1'b0;
           @(posedge clk); #1;
 
           // FORCE an illegal MULTI-HOT state (two bits high) — simulate an SEU/glitch.
           force dut.state = 5'b01010;
           @(posedge clk); #1; release dut.state;
           // After release, the safe machine must have driven a LEGAL one-hot next_state.
           @(posedge clk); #1;
           assert ($onehot(dut.state))
               else begin $error("no recovery from multi-hot: state=%b", dut.state); errors++; end
 
           // FORCE an illegal ALL-ZERO (zero-hot) state.
           force dut.state = 5'b00000;
           @(posedge clk); #1; release dut.state;
           @(posedge clk); #1;
           assert ($onehot(dut.state))
               else begin $error("no recovery from zero-hot: state=%b", dut.state); errors++; end
           assert (dut.state === 5'b00001)
               else begin $error("did not recover to IDLE: state=%b", dut.state); errors++; end
 
           if (errors == 0) $display("PASS: safe one-hot FSM recovers from illegal states to IDLE");
           else             $display("FAIL: %0d mismatches (no illegal-state recovery)", errors);
           $finish;
       end
   endmodule

The Verilog buggy/fixed pair uses the classic one-hot check (v & (v-1)) == 0 && v != 0 instead of $onehot; the safe version recovers to the idle code when that check fails.

fsm_onehot_safe.v — BUGGY (no illegal handling) vs FIXED (detect + recover) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: no detection of a non-one-hot register -> can stay illegal forever.
   module fsm_onehot_bad (
       input  wire clk, rst, go, stall,
       output wire done
   );
       localparam IDLE = 0, S1 = 1, S2 = 2, S3 = 3, S4 = 4;
       reg [4:0] state, next_state;
       always @(*) begin
           next_state = state;
           if (go && !stall) begin
               next_state = 5'b0;
               if (state[IDLE]) next_state[S1]   = 1'b1;
               if (state[S1])   next_state[S2]   = 1'b1;
               if (state[S2])   next_state[S3]   = 1'b1;
               if (state[S3])   next_state[S4]   = 1'b1;
               if (state[S4])   next_state[IDLE] = 1'b1;
           end
       end
       always @(posedge clk)
           if (rst) state <= 5'b00001;
           else     state <= next_state;
       assign done = state[S4];
   endmodule
 
   // FIXED: detect non-one-hot ((v & (v-1)) != 0 || v == 0) and recover to IDLE.
   module fsm_onehot_good (
       input  wire clk, rst, go, stall,
       output wire done
   );
       localparam IDLE = 0, S1 = 1, S2 = 2, S3 = 3, S4 = 4;
       reg [4:0] state, next_state;
       wire is_onehot = ((state & (state - 5'b1)) == 5'b0) && (state != 5'b0);
       always @(*) begin
           next_state = state;
           if (!is_onehot) begin
               next_state = 5'b00001;            // illegal -> recover to IDLE
           end else if (go && !stall) begin
               next_state = 5'b0;
               if (state[IDLE]) next_state[S1]   = 1'b1;
               if (state[S1])   next_state[S2]   = 1'b1;
               if (state[S2])   next_state[S3]   = 1'b1;
               if (state[S3])   next_state[S4]   = 1'b1;
               if (state[S4])   next_state[IDLE] = 1'b1;
           end
       end
       always @(posedge clk)
           if (rst) state <= 5'b00001;
           else     state <= next_state;
       assign done = state[S4] && is_onehot;     // qualified so an illegal code cannot pulse done
   endmodule
fsm_onehot_safe_tb.v — force illegal state, assert recovery (bind fsm_onehot_good)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module fsm_onehot_safe_tb;
       reg  clk, rst, go, stall;
       wire done;
       integer errors;
       wire is_onehot = ((dut.state & (dut.state - 5'b1)) == 5'b0) && (dut.state != 5'b0);
 
       fsm_onehot_good dut (.clk(clk), .rst(rst), .go(go), .stall(stall), .done(done));
 
       initial clk = 1'b0;
       always #5 clk = ~clk;
 
       initial begin
           errors = 0;
           rst = 1'b1; go = 1'b0; stall = 1'b0;
           @(posedge clk); #1; rst = 1'b0;
           @(posedge clk); #1;
 
           // Force a MULTI-HOT illegal state.
           force dut.state = 5'b01010;
           @(posedge clk); #1; release dut.state;
           @(posedge clk); #1;
           if (!is_onehot) begin
               $display("FAIL: no recovery from multi-hot: state=%b", dut.state);
               errors = errors + 1;
           end
 
           // Force an ALL-ZERO illegal state.
           force dut.state = 5'b00000;
           @(posedge clk); #1; release dut.state;
           @(posedge clk); #1;
           if (dut.state !== 5'b00001) begin
               $display("FAIL: did not recover to IDLE: state=%b", dut.state);
               errors = errors + 1;
           end
 
           if (errors == 0) $display("PASS: safe one-hot FSM recovers from illegal states to IDLE");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the safe version counts the set bits (or tests against the legal codes) to detect a non-one-hot register and drives the recovery.

fsm_onehot_safe.vhd — BUGGY (no handling) vs FIXED (detect + recover) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: no illegal-state detection -> a stray multi-hot/zero-hot code can persist.
   entity fsm_onehot_bad is
       port ( clk, rst, go, stall : in std_logic; done : out std_logic );
   end entity;
   architecture rtl of fsm_onehot_bad is
       constant IDLE : natural := 0; constant S1 : natural := 1; constant S2 : natural := 2;
       constant S3 : natural := 3;   constant S4 : natural := 4;
       signal state, next_state : std_logic_vector(4 downto 0);
   begin
       process (state, go, stall)
       begin
           next_state <= state;
           if go = '1' and stall = '0' then
               next_state <= (others => '0');
               if state(IDLE) = '1' then next_state(S1)   <= '1'; end if;
               if state(S1)   = '1' then next_state(S2)   <= '1'; end if;
               if state(S2)   = '1' then next_state(S3)   <= '1'; end if;
               if state(S3)   = '1' then next_state(S4)   <= '1'; end if;
               if state(S4)   = '1' then next_state(IDLE) <= '1'; end if;
           end if;
       end process;
       process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then state <= "00001"; else state <= next_state; end if;
           end if;
       end process;
       done <= state(S4);
   end architecture;
 
   -- FIXED: count set bits; if not exactly one, recover to IDLE.
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
   entity fsm_onehot_good is
       port ( clk, rst, go, stall : in std_logic; done : out std_logic );
   end entity;
   architecture rtl of fsm_onehot_good is
       constant IDLE : natural := 0; constant S1 : natural := 1; constant S2 : natural := 2;
       constant S3 : natural := 3;   constant S4 : natural := 4;
       signal state, next_state : std_logic_vector(4 downto 0);
       function is_onehot(v : std_logic_vector) return boolean is
           variable cnt : natural := 0;
       begin
           for i in v'range loop
               if v(i) = '1' then cnt := cnt + 1; end if;
           end loop;
           return cnt = 1;
       end function;
   begin
       process (state, go, stall)
       begin
           next_state <= state;
           if not is_onehot(state) then
               next_state <= "00001";                  -- illegal -> recover to IDLE
           elsif go = '1' and stall = '0' then
               next_state <= (others => '0');
               if state(IDLE) = '1' then next_state(S1)   <= '1'; end if;
               if state(S1)   = '1' then next_state(S2)   <= '1'; end if;
               if state(S2)   = '1' then next_state(S3)   <= '1'; end if;
               if state(S3)   = '1' then next_state(S4)   <= '1'; end if;
               if state(S4)   = '1' then next_state(IDLE) <= '1'; end if;
           end if;
       end process;
       process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then state <= "00001"; else state <= next_state; end if;
           end if;
       end process;
       done <= state(S4) when is_onehot(state) else '0';
   end architecture;
fsm_onehot_safe_tb.vhd — force illegal state, assert recovery (bind fsm_onehot_good)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity fsm_onehot_safe_tb is
   end entity;
 
   architecture sim of fsm_onehot_safe_tb is
       signal clk   : std_logic := '0';
       signal rst   : std_logic := '1';
       signal go    : std_logic := '0';
       signal stall : std_logic := '0';
       signal done  : std_logic;
 
       function is_onehot(v : std_logic_vector) return boolean is
           variable cnt : natural := 0;
       begin
           for i in v'range loop
               if v(i) = '1' then cnt := cnt + 1; end if;
           end loop;
           return cnt = 1;
       end function;
   begin
       -- Bind to the safe machine; force illegal states via a hierarchical alias if the
       -- simulator allows, or (portably) drive an equivalent recovery path. Here we assert
       -- the RECOVERY PROPERTY: after any illegal code the machine returns to a one-hot state.
       dut : entity work.fsm_onehot_good port map (clk => clk, rst => rst, go => go, stall => stall, done => done);
 
       clkgen : process
       begin
           clk <= '0'; wait for 5 ns;
           clk <= '1'; wait for 5 ns;
       end process;
 
       stim : process
       begin
           rst <= '1'; go <= '0'; stall <= '0';
           wait until rising_edge(clk); wait for 1 ns; rst <= '0';
           -- Force an illegal value onto the state signal (VHDL '<< signal >>' force in tools
           -- that support it; otherwise use the simulator's force command in the run script):
           << signal .fsm_onehot_safe_tb.dut.state : std_logic_vector >> <= force "01010";  -- multi-hot
           wait until rising_edge(clk); wait for 1 ns;
           << signal .fsm_onehot_safe_tb.dut.state : std_logic_vector >> <= release;
           wait until rising_edge(clk); wait for 1 ns;
           assert is_onehot(<< signal .fsm_onehot_safe_tb.dut.state : std_logic_vector >>)
               report "no recovery from multi-hot illegal state" severity error;
           report "fsm_onehot_safe self-check complete" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: a one-hot state register has only N legal codes out of 2^N, so a robust design detects a non-one-hot value and forces recovery to a known legal state — and qualifies its outputs so a transient illegal code cannot assert them (the safe-FSM discipline developed fully in 4.5).

5. Verification Strategy

Verifying an encoding choice is not "does the FSM work" — every encoding of a correct machine works functionally — it is two distinct obligations: prove the encodings are behaviourally identical, and prove the fragile encoding (one-hot) is robust to illegal codes. That reframes the whole test.

Two encodings of the same machine must produce identical outputs on identical stimulus; and a one-hot register must always be exactly one-hot, or detect that it is not and recover to a known state.

  • Cross-encoding equivalence — the primary check. Drive the same stimulus into the binary and one-hot DUTs and assert their outputs match cycle by cycle. The §4a and §4b testbenches share the same step() stimulus and the same expected done sequence; running both and diffing (or instantiating both and asserting done_binary === done_onehot each cycle) is the machine-checked proof that encoding is a cost choice, not a behaviour change. This is the single most important property this page teaches.
  • The one-hot invariant, asserted continuously. For every cycle a healthy one-hot machine is running, the register must satisfy exactly-one-bit-set: SV assert ($onehot(state)), Verilog (state & (state-1)) == 0 && state != 0, VHDL a set-bit count of one. The §4b testbenches assert this each cycle. A break in this invariant is the illegal-state bug surfacing.
  • Forced illegal-state / recovery test — the flagship check. You cannot wait for an SEU; you inject one. The §4d testbench uses force/release (or the simulator's force command) to drive a multi-hot (01010) and an all-zero (00000) value into the state register, then asserts the fixed machine returns to a legal one-hot code (specifically IDLE) within a cycle — and that the buggy machine can stay stuck. This is the direct test of the safe-recovery fix.
  • Output qualification. Assert that a transient illegal code cannot pulse the output: with the fixed done = state[S4] && $onehot(state), force a code with S4's bit set alongside another bit and confirm done stays low. This proves the output is guarded, not just the state.
  • Reset-value legality. A specific check that after reset the one-hot register is the legal idle code (00001), never all-zero — an all-zero reset is itself an illegal one-hot state, and forgetting it is the most common way a one-hot FSM starts life broken.
  • Conceptual invariants. Three properties must always hold: (i) for any input sequence, the binary and one-hot machines emit identical outputs (functional equivalence across encodings); (ii) the one-hot register is always exactly one-hot, or the machine detects otherwise and recovers within a bounded number of cycles (robustness); (iii) no illegal transient code asserts an output (output qualification). Property (ii) is the one a naive one-hot machine silently violates.
  • What sim can and cannot show, and expected waveform. A functional RTL sim will not spontaneously produce an SEU or a reset-gap glitch — you must inject the illegal code (as §4d does) to exercise recovery; in the field these come from radiation, metastability on a poorly-synchronized input, or a reset that did not reach every flop. On a correct waveform of the safe machine: the state is a walking single bit through 00001, 00010, 00100, 01000, 10000; done pulses only on 10000; and immediately after an injected illegal code the next legal state is 00001 (IDLE) — the visual signature of self-healing.

6. Common Mistakes

Treating encoding as behaviour, not cost. The first and most conceptual error: believing one-hot or gray "fixes" or "breaks" the machine's function. It does not — every encoding of a correct FSM computes the same outputs. Encoding changes area, speed, power, and robustness, never behaviour. If two encodings of your machine disagree functionally, you have an actual bug (an unhandled illegal code, a reset gap), not an encoding difference.

One-hot reset to all-zero. Resetting a one-hot register to 0 (or forgetting to reset it at all) leaves it in an illegal zero-hot state, so the machine starts life broken and — with a naive decode — never reaches a legal state. The reset value of a one-hot register must be the legal idle code with exactly one bit set (00001). This is the single most common one-hot mistake.

A naive one-hot decode that assumes exactly-one-hot with no recovery. Writing next-state and output logic that trusts the register is always one-hot (the §7 / §4d bug). One stray multi-hot or all-zero code — from an SEU, a glitch, or a reset gap — and the machine can drive garbage and never recover, because nothing detects the illegal code. A robust one-hot FSM detects a non-one-hot register and forces recovery to a known state, and qualifies its outputs (4.5).

Reaching for gray on a branchy FSM. Gray's one-bit-per-step benefit only holds where the state graph is linear. Applying gray to a general FSM with branches and merges cannot keep single-bit adjacency on every edge, so you pay the encoding effort for a benefit you only partly get. Gray belongs on counter-like, phase-like, pointer-like machines (and CDC gray pointers), not on arbitrary control graphs.

Hand-coding an encoding the synthesis tool would have chosen — or fighting the tool's choice. Encoding is a synthesis attribute (syn_encoding / fsm_encoding). On an FPGA the tool often re-encodes an enumerated FSM to one-hot automatically for timing; hand-forcing binary there can cost you speed, and hand-forcing one-hot on an area-critical ASIC block can cost you flops. Know whether you or the tool owns the encoding, set the attribute deliberately, and check the synthesis report for what actually got built.

Forgetting one-hot's flop cost scales with state count. One-hot spends one flop per state, so a 64-state machine is 64 flops of state register versus 6 for binary. On a flop-rich FPGA that is fine; on an area-critical ASIC block with many states it is a real cost. One-hot is not free speed — it is speed bought with flops, and the bill grows linearly with the number of states.

7. DebugLab

The FSM that never came back — a one-hot register knocked into an illegal state

The engineering lesson: every state encoding has illegal codes, and a robust FSM plans for them. One-hot has the most — only N legal of 2^N, so a single upset can wedge a naive decode forever; binary has the few unused codes when N is not a power of two, which an incomplete case both leaves undefined and turns into a latch. The two tells are unmistakable: a control FSM that locks up rarely, environment-dependently, and clears only on full reset is sitting in an illegal state its decode never handled; a synthesis latch warning on the next-state logic is an incomplete case leaving illegal (or even legal) codes unassigned. Both fixes are the same move — detect or default every code and give it a defined path back to a known legal state, and qualify outputs so a transient illegal code cannot assert them — and both are identical in intent across SystemVerilog, Verilog, and VHDL. This robustness discipline is exactly what 4.5 (Safe FSM Design) develops in full; encoding is where it first bites, because the encoding decides how many illegal codes there are.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "choose the encoding on purpose, and plan for its illegal codes" habit.

Exercise 1 — Encode one machine three ways and count the cost

Take the six-state machine A → B → C → D → E → F → A. (a) Write a binary encoding (how many flops?), a gray encoding (order the codes so each forward step flips one bit — where does adjacency break?), and a one-hot encoding. (b) For each, write the output equation for a done that is high in state F, and note whether it is a compare or a single-bit test. (c) Count the illegal codes each encoding has (values the register can hold that are not legal states), and state which encoding has the most and why.

Exercise 2 — Pick the encoding for the target and the graph

For each scenario, choose binary, gray, or one-hot and justify in one line, then name the specific cost the wrong choice would incur: (a) a 32-state control FSM on an area-critical ASIC block; (b) an 8-state controller on an FPGA whose next-state decode is the reported critical path; (c) a linear 16-step phase sequence in a power-sensitive design; (d) a state whose value is sampled by a second, asynchronous clock domain.

Exercise 3 — Make a one-hot FSM self-healing

Start from a naive one-hot FSM (walks the hot bit, done = state[S4], no illegal handling). (a) Add logic that detects a non-one-hot register and recovers to IDLE, and qualify done so a transient illegal code cannot assert it. (b) Describe a testbench that injects a multi-hot and an all-zero value (via force/release) and asserts recovery within one cycle. (c) Explain why the reset value must be 00001 and not 00000, and what breaks if it is 00000.

Exercise 4 — Explain why binary is not automatically safe

A colleague argues one-hot is fragile but binary is safe because "all the codes are used." (a) Show this is false for a five-state machine in three flops: which codes are unused, and what happens if the register lands on one? (b) Explain how an incomplete case on the binary state produces both a latch and undefined behaviour on the unused codes. (c) Give the single one-line change that makes the binary next-state logic latch-free and self-recovering, and explain why the same principle (default every code to a known state) applies to every encoding.

Continue in RTL Design Patterns (forward FSM links unlock as they ship):

  • Safe FSM Design & Latch-Free Outputs — Chapter 4.5; the full safe-FSM discipline this page previews — detecting illegal states, forcing recovery, the safe synthesis attribute, and keeping the output logic latch-free across all encodings.
  • FSM + Datapath — Chapter 4.6; where a one-hot state's single-bit decode becomes the direct select line for datapath muxes and enables, turning the encoding choice into a datapath-control choice.
  • FSM Worked Example — a full FSM built end to end, where the encoding decision made here is one of the concrete design choices.

Backward / in-track dependencies:

  • FSM Fundamentals — Chapter 4.1; the three-piece FSM model (state register + next-state logic + output logic) this page encodes — encoding is entirely about the numeric codes the state register holds.
  • Moore vs Mealy — Chapter 4.2; the output tap this page decodes — a one-hot state makes a Moore output a single-bit test.
  • FSM Coding Styles (1/2/3-process) — Chapter 4.3; how the state-register / next-state / output split is organized, and where the encoded state signal lives in each style.
  • Encoding Conversions — Chapter 1.7; the binary-gray-one-hot conversions themselves — the codebook this page assigns to states.
  • Gray Counters — Chapter 3.3; the one-bit-per-step gray sequence this page borrows for linear state walks and CDC-safe state sampling.
  • Multiplexers (2:1, N:1, one-hot) — Chapter 1.1; the one-hot-select mux whose select lines a one-hot state register drives directly (single-bit decode = one-hot select).
  • The Register Pattern (D-FF) — Chapter 2.1; the flops the state register is built from — one-hot spends N, binary/gray spend log2(N).
  • Synchronous Reset — Chapter 2.3; the sampled-on-the-edge reset that must load a legal idle code (never all-zero one-hot).

Verilog v1 prerequisites (the grammar this pattern transcribes into):

  • Case Statements — the case (state) that decodes a binary state, and why a default arm covers illegal/unused codes.
  • Parameter — the localparam state codes that carry the encoding (binary, gray, or one-hot).
  • Bitwise Operators — the state & (state-1) one-hot check and the single-bit state[k] decode.
  • Blocking and Non-Blocking Assignments — why the state register uses non-blocking (<=) while the combinational next-state and decode logic use blocking (=).

11. Summary

  • State encoding is a cost-and-robustness choice, not a behaviour change. Every encoding of a correct FSM computes the same outputs; the encoding only decides the state register's area (flops), the decode's speed, the transition power, and how many illegal codes exist. If two encodings disagree functionally, you have a real bug, not an encoding difference.
  • Binary = fewest flops, widest decode. ceil(log2(N)) flops pack the states densely (the ASIC-friendly default), but the next-state and output logic must un-pack the code, so every state test is a compare and the decode can be the critical path.
  • Gray = same flops, lowest switching on a linear walk. Ordering codes so adjacent states differ by one bit cuts the flops that toggle per step — real dynamic power savings on counter-like / phase-like / pointer machines — and makes cross-domain sampling safe; it does not help a branchy graph, where single-bit adjacency cannot hold on every edge.
  • One-hot = N flops, trivial fast decode, FPGA-friendly, illegal-state risk. One flop per state turns every state test into a single-bit read, keeping the logic shallow and fast (and flop-rich FPGAs love it), but it spends N flops and leaves only N legal codes of 2^N, so a stray multi-hot or all-zero value is an illegal state — the flagship failure (§7) — that a naive decode never recovers from.
  • Every encoding has illegal codes, and a robust FSM plans for them. One-hot has the most (2^N − N); binary has the few unused codes when N is not a power of two, which an incomplete case both leaves undefined and turns into a latch. Detect or default every code, give it a defined path back to a known legal state, qualify outputs against transient illegal codes, and reset one-hot to a legal idle code (never all-zero) — the safe-FSM discipline developed fully in 4.5. Treat encoding as the synthesis attribute it is, choose it for the target (FPGA vs ASIC) and the graph, and verify binary and one-hot are identical — built and self-check-verified here in SystemVerilog, Verilog, and VHDL.