A finite state machine built from parameter or localparam constants compiles and simulates fine, but it gives up two things SystemVerilog can offer for free: strong type checking on the state variable, and a built-in safety net for missed states.

Enumerated types and the unique case statement together close both gaps, which is why they are the standard way to model FSMs in SystemVerilog RTL.

8 min read | Intermediate Level

What You'll Learn

  • How to declare FSM states as an enumerated type with an explicit 4-state base type
  • Why unique case is the right choice for state decode logic, and what it checks for you
  • Why FSM decode logic rarely needs an explicit enum cast, and where to look if it does
  • A classic FSM lock-up bug caused by enum default values, and the two ways to fix it

Declaring States as an Enumerated Type

A state register is really just a variable restricted to a small, named set of legal values - exactly what an enumerated type is for. Declaring it with typedef enum, an explicit base type, and explicit bit values documents the encoding directly in the state declaration instead of in a separate block of parameter lines:


typedef enum logic [1:0] {
  IDLE = 2'b00,
  READ = 2'b01,
  BUSY = 2'b10,
  DONE = 2'b11
} state_t;

state_t state, next_state;

always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) state <= IDLE;
  else        state <= next_state;

Two choices in that declaration matter more than they look. First, the base type is logic, a 4-state type, instead of the enum default of int. Second, every label has an explicit value instead of letting SystemVerilog number them 0, 1, 2, 3 automatically.

Both choices keep the RTL model's encoding visible and predictable instead of leaving it to whatever the tool happens to pick. That matters for comparing a pre-synthesis simulation against the gate-level netlist, and for the reset bug described later in this article.

Decoding State with unique case

Next-state and output logic read naturally as a case statement over the state variable. Adding the unique modifier turns that case statement into a semantic check, not just a style choice:


always_comb begin
  next_state = state;               // default: hold current state
  unique case (state)
    IDLE: if (start)     next_state = READ;
    READ:                 next_state = BUSY;
    BUSY: if (mem_ready) next_state = DONE;
    DONE:                 next_state = IDLE;
  endcase
end

always_comb begin
  {mem_req, done} = '0;
  unique case (state)
    READ: mem_req = 1'b1;
    DONE: done     = 1'b1;
    default: ;                      // IDLE and BUSY drive nothing extra
  endcase
end

unique case gives you three things at once.

First, it tells synthesis the branches can be decoded in parallel, with no priority chain - the same hint as a parallel_case pragma.

Second, it flags a run-time warning if two branches ever match the same value at once.

Third, and most useful for an enumerated state variable, it flags a run-time warning if the case expression takes a value that no branch covers. That is exactly the "full coverage" check a full_case pragma gives synthesis, except unique case enforces it in simulation too, instead of only at synthesis time.

Because state is restricted to the four labels in state_t, listing those four branches is already exhaustive. There is no need for a default branch that assigns an X "just in case," the way a plain reg-based state register would need one.

The enumerated type and unique case already guarantee that any value state can legally hold is covered. The one exception is simulation time zero: state is a 4-state variable, so it is x until reset first assigns it, and each always_comb block runs once at time zero. Since x matches none of the labels, unique case can issue a one-time violation report before reset. That report is expected and harmless; it can be silenced during reset with the assertion control system tasks, or avoided by using unique0 case, which does not report when nothing matches.

If an FSM uses one-hot encoding, some codebases write the case expression as the constant 1'b1 and each case item as a bit-select of the state register, such as state[READ_BIT]. This "reversed" case style can synthesize to more efficient decode logic for one-hot state machines, though the ordinary style above works correctly for any encoding.

Casting Into the State Variable

next_state can only ever be assigned a label from state_t, since enumerated types are strongly typed. The unique case block above already only assigns full labels on every branch, so this never comes up for ordinary FSM decode logic written that way.

It only matters if a next state is ever computed from an expression instead of chosen in a case branch - for example, stepping to the next encoding with next_state = state_t'(state + 1); instead of a case item.

That needs an explicit cast, because the result of arithmetic on a state variable is an integer expression, which is not automatically assignment-compatible with the enum type. The built-in .next method needs no cast, since it already returns a value of the enum type.

See SystemVerilog Enumeration for the full casting rules and the built-in first()/last()/next()/prev()/num()/name() methods.

Common Beginner Mistakes

Mistake #1: A default enum base type hides an FSM lock-up at time zero

An enum declared without an explicit base type defaults to int, a 2-state type that starts simulation at 0 - the same value as the first label in the list.

If the decode logic is written as always @(state) rather than always_comb, that block never fires before the first real transition, because state hasn't changed yet from its initial value. The design can sit locked on the first state indefinitely, even after reset, since reset simply reassigns the same value the variable already holds.

Declaring the enum with an explicit 4-state base type (logic, as in the example above) fixes this: simulation now starts at X instead of 0, so the first legal assignment from reset is a genuine transition. Switching the decode block to always_comb fixes it independently, since always_comb always executes once at time zero regardless of whether anything in its sensitivity list has changed yet.

Mistake #2: Assigning a bit-select of a one-hot state variable directly

Clearing a one-hot next_state to all zeros and then setting a single bit, a common style with plain reg state registers, does not work with an enumerated type - a bare '0 is not a label in the enumeration.

Setting one bit of an enum variable can also leave it holding a combination that was never declared.

Assign a full label from the type's list on every branch instead of touching individual bits, and let the unique case default (next_state = state; in the example above) cover the "no change" case.

Quick Check

Q: Why doesn't the FSM in this article need a default branch inside its unique case statements?

Show Answer

Because state is of type state_t, it can only ever legally hold one of the four declared labels once reset has assigned it. Before that, a 4-state state is x.

Listing all four case items already covers every value the variable can hold, so there is no remaining value for a default branch to catch. unique case will flag a run-time warning if that assumption is ever violated.

Practice Exercise

Challenge: Add an Error State

Extend the state_t enumeration with a fifth label, ERROR, that the FSM enters from BUSY if a signal mem_err is asserted, and that only returns to IDLE after an external clear_err signal. Update the next-state unique case block accordingly.

Hint

Add the new label to the typedef enum list with its own explicit 2-bit value, then add one more case item for BUSY that checks mem_err before the existing mem_ready check, plus a new case item for ERROR itself.

Solution

typedef enum logic [2:0] {
  IDLE  = 3'b000,
  READ  = 3'b001,
  BUSY  = 3'b010,
  DONE  = 3'b011,
  ERROR = 3'b100
} state_t;

always_comb begin
  next_state = state;
  unique case (state)
    IDLE:  if (start)                next_state = READ;
    READ:                             next_state = BUSY;
    BUSY:  if (mem_err)               next_state = ERROR;
           else if (mem_ready)        next_state = DONE;
    DONE:                             next_state = IDLE;
    ERROR: if (clear_err)             next_state = IDLE;
  endcase
end

Widening the base type to 3 bits was necessary because the original 2-bit encoding had no room left for a fifth label.

Every branch still assigns a full label, and the unique case still has no need for a default, since the five listed items remain exhaustive for a 5-value enumeration.

Summary

In this tutorial, you learned:

  • Declare FSM states as a typedef enum with an explicit 4-state base type and explicit label values, rather than relying on the defaults
  • unique case gives parallel-decode synthesis behavior plus run-time warnings for overlapping or uncovered case values
  • FSM decode logic only assigns full labels, so it rarely needs a cast - that only comes up when computing a next state from an expression rather than a case branch
  • A 2-state (or default int) enum combined with an edge-sensitive decode block can lock an FSM up at time zero - fix it with a 4-state base type, always_comb, or both
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5