Skip to content

Enumeration Types

FSM state encoding, enum methods, $cast, waveform naming, unique case.

Module 2 · Page 2.6

The Feature That Makes Waveforms Readable

Open any professional RTL design and you will find enums wherever finite state machines exist. The reason has nothing to do with syntax elegance — it's about debuggability. When a simulator displays FSM state values as symbolic names (AXI_BURST, WAIT_BRESP) instead of raw binary (3'b101), you can trace control flow in a waveform viewer in seconds. Without enums, every debug session starts with opening the RTL and mentally mapping encoding table entries to bit patterns.

Beyond waveforms, enum provides compile-time safety for case statements. Using unique case with an enum type causes the simulator to flag any state transitions into illegal (undefined) enum values, and synthesis tools issue warnings for missing case arms. These catches happen at compile or elaboration time — before your simulation even starts.

The two things engineers get wrong most often: assigning an integer directly to an enum variable (illegal without $cast), and forgetting that the default base type of an enum is int (2-state, 32-bit) — not a minimal-width logic. Both bite people the first time they actually look at the synthesized gate count.

How enum Works — The Mental Model

An enum defines a set of named constants and a variable type that can only hold one of those constants at a time. Under the hood, each name maps to an integer value (auto-assigned starting from 0, or explicitly specified). The enum variable stores that integer, but the simulator and synthesis tool know the name associated with each value.

The critical distinction: an enum variable is strongly typed within SV's type system. You cannot assign a raw integer to it without a cast. You can compare it to other enum values directly, pass it to functions expecting that enum type, and iterate over all values using the built-in methods. The type system enforces that only defined values are used — which catches off-by-one and copy-paste errors in state machine code.

  • Named constants — Each enum member is a named symbolic constant. The simulator displays names in waveforms. Code reads as state == IDLE instead of state == 2'b00.
  • Typed variable — An enum variable can only hold values that were declared in the enum. Assigning values outside the declared set requires $cast and will fail if the value is invalid.
  • Built-in iteration methods — first(), last(), next(), prev(), name(), num() — iterate over all values or get the string name of the current value.
  • Synthesis-aware — Synthesis tools understand enum base types and can apply one-hot, binary, or Gray encoding. Explicit base type (logic [N:0]) gives you full control over encoding width and style.

Syntax — Every Form and All Methods

SystemVerilog — enum Syntax & Methods
// ── BASIC enum ────────────────────────────────────────────────────
// enum [base_type] { member_list } [variable]
 
// Default base type = int (32-bit 2-state, signed) — BAD for synthesis
enum { IDLE, ACTIVE, DONE } state_bad;  // 32-bit int underneath — don't do this in RTL
 
// ── PREFERRED: explicit base type for RTL ─────────────────────────
typedef enum logic [1:0] {
ST_IDLE  = 2'b00,
ST_RUN   = 2'b01,
ST_DRAIN = 2'b10,
ST_ERROR = 2'b11
} fsm_state_t;
 
fsm_state_t state, next_state;   // variables of that type
 
// ── AUTO-NUMBERED (no explicit values) ───────────────────────────
typedef enum logic [2:0] {
IDLE, FETCH, DECODE, EXECUTE, WRITEBACK
} pipeline_t;   // IDLE=0, FETCH=1, DECODE=2, EXECUTE=3, WRITEBACK=4
 
// ── MIXED: some explicit, some auto ──────────────────────────────
typedef enum logic [3:0] {
OP_NOP   = 4'h0,
OP_ADD   = 4'h1,
OP_SUB   = 4'h2,
OP_LOAD  = 4'h8,   // explicit jump in value
OP_STORE = 4'h9    // auto: 4'h9
} opcode_t;
 
// ── OPERATIONS ────────────────────────────────────────────────────
state = ST_IDLE;
if (state == ST_RUN) $display("running");
if (state != ST_ERROR) $display("no error");
 
// ── BUILT-IN METHODS ──────────────────────────────────────────────
state.first()     // ST_IDLE (first declared member)
state.last()      // ST_ERROR (last declared member)
state.next()      // next member after current value
state.prev()      // previous member before current value
state.num()       // 4 (number of members in the enum)
state.name()      // "ST_IDLE" (string name of current value)
 
// ── int ↔ enum CONVERSION ─────────────────────────────────────────
int         raw = 2;
fsm_state_t s;
if (!$cast(s, raw))
$error("Invalid enum value %0d", raw);   // $cast returns 0 if value invalid
int         back = int'(state);             // enum → int: always safe
MethodReturnsDescription
first()enum valueFirst declared member regardless of current value
last()enum valueLast declared member
next()enum valueMember after current; wraps to first() after last()
prev()enum valueMember before current; wraps to last() before first()
num()intTotal count of declared members
name()stringString name of current value; empty string for non-member values

Visual — Encoding, Waveforms, and Value Tables

Enum Values and Their Encodings

Declaration: typedef enum logic [1:0] { ST_IDLE=2'b00, ST_RUN=2'b01, ST_DRAIN=2'b10, ST_ERROR=2'b11 } fsm_state_t

NameValue (logic [1:0])int' castWaveform showsname() returns
ST_IDLE2'b000ST_IDLE"ST_IDLE"
ST_RUN2'b011ST_RUN"ST_RUN"
ST_DRAIN2'b102ST_DRAIN"ST_DRAIN"
ST_ERROR2'b113ST_ERROR"ST_ERROR"

enum next() and prev() Traversal

Current statestate.next()state.prev()Wrap behavior
ST_IDLEST_RUNST_ERROR (wraps!)prev() on first wraps to last
ST_RUNST_DRAINST_IDLENormal
ST_DRAINST_ERRORST_RUNNormal
ST_ERRORST_IDLE (wraps!)ST_DRAINnext() on last wraps to first

Synthesis Encoding Options

Encoding4-state FSM bitsPowerAreaSpecify how
Binary (default)2 bitsLowMinimalDefault for enum logic [1:0]
One-hot4 bits (1 per state)HigherMore FFsSynthesis directive or explicit values
Gray code2 bitsLow switchingSame as binarySynthesis directive
Auto (tool choice)Tool decidesOptimizedOptimizedenum { A, B, C } default

Code Examples — FSMs to Protocol Opcodes

Example 1 — Beginner: Basic enum and Methods

Example 1 — enum Basics and Methods
module tb_enum_basics;
 
typedef enum logic [1:0] {
IDLE = 2'b00, RUN = 2'b01, DRAIN = 2'b10, ERR = 2'b11
} state_t;
 
state_t s = IDLE;
 
initial begin
 
// ── Basic operations ──────────────────────────────────────────
$display("Initial state: %s (%0d)", s.name(), int'(s));  // IDLE (0)
 
s = RUN;
$display("After  assign: %s (%0d)", s.name(), int'(s));  // RUN (1)
 
// ── Iteration methods ─────────────────────────────────────────
$display("first = %s",  s.first().name());  // IDLE
$display("last  = %s",  s.last().name());   // ERR
$display("next  = %s",  s.next().name());   // DRAIN (next after RUN)
$display("prev  = %s",  s.prev().name());   // IDLE (prev of RUN)
$display("num   = %0d", s.num());           // 4
 
// ── Iterate all members ───────────────────────────────────────
state_t e = s.first();
repeat (s.num()) begin
$display("  %s = %02b", e.name(), e);
e = e.next();
end
 
// ── $cast: int → enum (with validity check) ───────────────────
int raw = 2;
if ($cast(s, raw))
$display("Cast OK: %s", s.name());    // DRAIN
raw = 10;
if (!$cast(s, raw))
$display("Cast FAIL: %0d is not a valid state", raw);
 
$finish;
end
 
endmodule

Expected output:

Simulation Output
Initial state: IDLE (0)
After  assign: RUN (1)
first = IDLE
last  = ERR
next  = DRAIN
prev  = IDLE
num   = 4
IDLE = 00
RUN = 01
DRAIN = 10
ERR = 11
Cast OK: DRAIN
Cast FAIL: 10 is not a valid state

Example 2 — Intermediate: RTL FSM With unique case

Example 2 — FSM Using enum + unique case
typedef enum logic [1:0] {
S_IDLE  = 2'b00,
S_REQ   = 2'b01,
S_WAIT  = 2'b10,
S_DONE  = 2'b11
} req_state_t;
 
module axi_ctrl (
input  logic   clk, rst_n, start, done_i,
output logic   req_o, busy_o
);
req_state_t state, next_state;
 
// State register
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) state <= S_IDLE;
else        state <= next_state;
 
// Next-state logic — unique case ensures: all states covered, no overlaps
always_comb begin
next_state = state;    // default: hold current
req_o  = 1'b0;
busy_o = 1'b0;
 
unique case (state)
S_IDLE: if (start)  next_state = S_REQ;
S_REQ:  begin req_o = 1; busy_o = 1; next_state = S_WAIT; end
S_WAIT: begin         busy_o = 1;
if (done_i)  next_state = S_DONE; end
S_DONE:              next_state = S_IDLE;
endcase
end
 
endmodule
 
// Testbench: read enum state name directly — $display %s works with enum
module tb_axi_ctrl;
logic clk=0, rst_n, start, done_i, req_o, busy_o;
axi_ctrl dut(.*);
always #5 clk = ~clk;
 
// Monitor state using hierarchical reference
always @(posedge clk)
$display("t=%0t state=%s req=%b busy=%b", $time, dut.state.name(), req_o, busy_o);
 
initial begin
rst_n=0; start=0; done_i=0; #12; rst_n=1;
@(posedge clk); start=1; @(posedge clk); start=0;
@(posedge clk); done_i=1; @(posedge clk); done_i=0;
repeat(2) @(posedge clk);
$finish;
end
endmodule

Example 3 — Verification: Enum in Constraints and Scoreboards

Example 3 — enum in Constraints and Coverage
typedef enum logic [1:0] {
OKAY=2'b00, EXOKAY=2'b01, SLVERR=2'b10, DECERR=2'b11
} axi_resp_t;
 
typedef enum logic [1:0] {
FIXED=2'b00, INCR=2'b01, WRAP=2'b10
} axi_burst_t;
 
class axi_txn;
rand axi_burst_t burst;
rand axi_resp_t  resp;
rand logic [7:0]  len;
 
// Constrain using enum members — readable and type-safe
constraint no_wrap  { burst != WRAP; }
constraint ok_resp  { resp inside {OKAY, EXOKAY}; }
constraint len_c    { len inside {[1:16]}; }
 
function void print();
$display("burst=%s resp=%s len=%0d", burst.name(), resp.name(), len);
endfunction
endclass
 
// Scoreboard: compare enum responses by name for readable error messages
task automatic check_resp(input axi_resp_t exp, got);
if (exp !== got)
$error("RESP MISMATCH: expected=%s got=%s", exp.name(), got.name());
else
$display("PASS resp=%s", got.name());
endtask
 
module tb_enum_constraints;
initial begin
axi_txn t = new();
repeat(3) begin
void'(t.randomize()); t.print();
end
check_resp(OKAY, SLVERR);   // deliberate mismatch for demo
check_resp(OKAY, OKAY);
$finish;
end
endmodule

Example 4 — Corner Case: Invalid Values, $cast, and Coverage

Example 4 — $cast and Coverage with Enums
module tb_enum_corners;
 
typedef enum logic [1:0] {
A = 2'b00, B = 2'b01, C = 2'b10
// 2'b11 is NOT defined — it's an illegal value!
} abc_t;
 
abc_t val;
int   raw;
 
initial begin
 
// ── $cast with validity check ─────────────────────────────────
for (raw = 0; raw < 4; raw++) begin
if ($cast(val, raw))
$display("raw=%0d → val=%s", raw, val.name());
else
$display("raw=%0d → INVALID (not in enum)", raw);
end
// raw=0 → A, raw=1 → B, raw=2 → C, raw=3 → INVALID
 
// ── name() on undefined value returns "" ──────────────────────
// If an enum variable somehow holds an invalid value (via force/DPI/etc.)
// name() returns "" (empty string) — use this to detect invalid states
logic [1:0] bits = 2'b11;
val = abc_t'(bits);           // force-cast — bypasses validation!
$display("Illegal value name(): '%s'", val.name()); // ""
if (val.name() == "")
$error("FSM in illegal state: %02b", val);
 
// ── Coverage: iterate all enum values ────────────────────────
val = val.first();
repeat (val.num()) begin
$display("Covering: %s", val.name());
val = val.next();
end
 
$finish;
end
 
endmodule

Simulation Behavior — What the Tools See

Waveform Display: The Core Value of enum

When a signal is declared as an enum type, the simulator stores a mapping from integer values to symbolic names. In the waveform viewer, the signal displays as the symbolic name rather than the raw binary value. This is the single most impactful benefit of enum in debug workflows: reading IDLE → REQ → WAIT → DONE in a waveform is immediate. Reading 00 → 01 → 10 → 11 requires a lookup table in your head or the datasheet.

unique case and FSM Coverage

unique case on an enum type gives you two simulation-time checks: (1) the simulator warns if the expression ever holds a value not listed in any case arm (illegal state detection), and (2) the simulator warns if more than one case arm can match simultaneously (though this cannot happen with an enum if the values are unique, which they always are). With case (not unique), you lose both checks.

ConstructMissing arm behaviorIllegal value behaviorSynthesis
caseExecutes default, no warningNo detectionStandard mux
case + defaultDefault executesDefault catches itStandard mux with default
unique caseRuntime warning: no matchRuntime warning: illegal valueSynthesis may remove redundant logic
priority caseNo warning, implicit default=no-opNo detectionPriority encoder

Where enum Appears in Real Verification

Verification Patterns Using enum
// ── 1. AXI PROTOCOL FIELDS AS ENUMS ───────────────────────────────
typedef enum logic [1:0] { FIXED, INCR, WRAP }       axi_burst_t;
typedef enum logic [2:0] { SZ_1, SZ_2, SZ_4, SZ_8 }  axi_size_t;
typedef enum logic [1:0] { OKAY, EXOKAY, SLVERR, DECERR } axi_resp_t;
 
// ── 2. UVM PHASE TRACKER ──────────────────────────────────────────
typedef enum { BUILD, CONNECT, RUN, REPORT, FINAL } uvm_phase_t;
uvm_phase_t curr_phase = BUILD;
$display("Phase: %s", curr_phase.name());
 
// ── 3. CONSTRAINT WITH ENUM MEMBERS ──────────────────────────────
rand axi_burst_t burst;
constraint no_wrap_c { burst inside {FIXED, INCR}; }  // readable!
 
// ── 4. SCOREBOARD: compare and report with names ──────────────────
if (got_resp !== exp_resp)
$error("RESP: exp=%s got=%s", exp_resp.name(), got_resp.name());
 
// ── 5. COVERAGE: cover all enum values ────────────────────────────
// covergroup cg_burst;
//   cp_burst: coverpoint burst {
//     bins fixed = {FIXED};
//     bins incr  = {INCR};
//     bins wrap  = {WRAP};
//   }
// endgroup
 
// ── 6. MONITOR: decode incoming response ─────────────────────────
function automatic string decode_resp(logic [1:0] raw);
axi_resp_t r;
if ($cast(r, raw)) return r.name();
return $sformatf("UNKNOWN(%02b)", raw);
endfunction
 
// ── 7. ASSERTION: valid response check ────────────────────────────
// assert property (@(posedge clk) bvalid |-> bresp inside {OKAY, EXOKAY});

Bugs Engineers Hit With enum

Bug 1 — Default Base Type is int: 32-bit FSM State Register

Bug 1 — int Base Type in RTL enum
// BUGGY: default base type is int — synthesizes 32 flip-flops for state!
typedef enum { IDLE, RUN, DONE } state_t;   // int underneath = 32-bit
state_t state;
// Synthesis report shows: state uses 32 flip-flops
// Gate-sim shows 32-bit state register — overkill for a 3-state FSM
 
// FIXED: always specify the base type for RTL
typedef enum logic [1:0] {
IDLE = 2'b00, RUN = 2'b01, DONE = 2'b10
} state_ok_t;
// Now state register is 2 flip-flops — correct

Bug 2 — Direct Integer Assignment to enum Without $cast

Bug 2 — Integer Assignment Without $cast
typedef enum logic [1:0] { A, B, C } abc_t;
abc_t val;
int   raw = 1;
 
// BUGGY: direct int → enum assignment
val = raw;    // COMPILE ERROR or WARNING (tool-dependent)
// Most tools: "implicit type conversion may cause unexpected results"
 
// ALSO BUGGY: force-cast bypasses validation
val = abc_t'(raw);    // compiles but skips the validity check
// If raw=3 (not defined), val holds illegal value — name() returns ""
 
// CORRECT: use $cast with validity check
if (!$cast(val, raw))
$error("%0d is not a valid abc_t value", raw);

Bug 3 — Missing default in case on Partially-Defined Enum

Bug 3 — Latch Inference From Missing Case Arm
typedef enum logic [1:0] { S0, S1, S2 } s_t;
// Note: 2'b11 is unused — but synthesis tool doesn't know that
 
logic   out;
s_t     s;
 
// BUGGY: 3 arms for a 2-bit type = 4 possible values
// The 4th (2'b11) is uncovered → synthesis infers a latch for 'out'
always_comb
case (s)
S0: out = 0;
S1: out = 1;
S2: out = 0;
// missing 2'b11 → latch inferred!
endcase
 
// CORRECT option 1: add default
always_comb
case (s)
S0: out = 0; S1: out = 1; S2: out = 0;
default: out = 0;   // covers 2'b11 explicitly
endcase
 
// CORRECT option 2: use unique case (synthesis knows remaining states are illegal)
always_comb
unique case (s)
S0: out = 0; S1: out = 1; S2: out = 0;
endcase   // unique: synthesis knows other values won't occur

Bug 4 — next() Wraps Unexpectedly at Last Member

Bug 4 — next() Wraps at Boundary
typedef enum { INIT, ACTIVE, DONE } phase_t;
phase_t p = DONE;
 
// BUGGY: assuming next() stops at DONE
while (p != DONE) begin
$display("%s", p.name());
p = p.next();    // if started at DONE, next() wraps to INIT!
end
// The loop body never executes — p starts at DONE, condition false immediately
// But if you use next() carelessly thinking it "stops" at last member: infinite loop
 
// CORRECT: use num() to count explicitly
p = p.first();
repeat (p.num()) begin
$display("%s", p.name());
p = p.next();    // safe: exactly num() iterations
end

Interview Questions

Beginner Level

Q1: What is the default base type of an enum in SystemVerilog, and why does it matter for RTL? The default base type is int — 32-bit, 2-state, signed. In RTL this means a 3-state FSM declared with the default base type synthesizes to 32 flip-flops instead of 2. Always specify the base type explicitly for synthesizable enums: enum logic [1:0] { ... }. For testbench-only enums, the default int is acceptable. Q2: Why can't you assign an integer directly to an enum variable in SV? Enum is a distinct type — assigning a raw integer bypasses the type system and could assign a value not defined in the enum (an illegal state). SV requires the conversion to be explicit: either use $cast(enum_var, int_val) which checks validity and returns 0 on failure, or use a force-cast enum_t'(int_val) which bypasses validation (use only when you've verified the value externally). The $cast form is preferred because it exposes invalid value bugs immediately.

Intermediate Level

Q3: What does unique case add over plain case when used with an enum FSM?unique case adds two simulation-time checks: (1) a warning if the switch expression holds a value not listed in any case arm (illegal state detection), and (2) a warning if more than one arm matches (overlapping case items). For RTL synthesis, it also tells the tool that the case arms are mutually exclusive and exhaustive — the synthesizer can remove redundant state logic. Without unique, a partially-covered enum case may infer latches for combinational outputs.

Experienced Engineer Level

Q4: An FSM declared with enum logic [1:0] has 3 states defined but 4 possible 2-bit values. How do you prevent latch inference from the undefined 4th value in a combinational always_comb block? Three options: (1) Add a default case arm that drives the outputs to a known value — the synthesis tool sees all 4 cases covered, no latch. (2) Use unique case without a default — tells the synthesis tool that the 4th encoding is unreachable, so it can optimize freely. This is clean but requires the tool to trust the unique annotation. (3) Use a 3-state encoding that doesn't have "wasted" binary values — Gray code or one-hot avoids the partial-coverage issue entirely. Option 1 is the most portable; option 2 is preferred in modern flows.

Best Practices & Coding Guidelines

  • Always specify base type for RTL — enum logic [N:0] — always. The default int base type gives you 32 flip-flops for a 3-state machine. Never use the default in synthesizable code.
  • Use unique case for FSMs — unique case (state) is the standard for RTL FSMs. It catches illegal states at simulation, prevents latch inference at synthesis, and documents mutual exclusivity.
  • $cast for int→enum conversion — Never directly assign integers to enum variables. Use $cast() and check the return value. A failing cast means an illegal state was about to be loaded.
  • Use name() in error messages — In scoreboard mismatches and FSM debug messages: $error("exp=%s got=%s", exp.name(), got.name()). Human-readable state names in log files save hours of debugging.
Use caseCorrect approachAvoid
RTL FSM statetypedef enum logic [N:0] { ... }typedef enum { ... } (int base)
FSM combinational logicunique case (state)Plain case (latches on partial coverage)
int → enum$cast(e, i) + check returne = i (compile error/warning)
Iterate all valuesrepeat(e.num()) with e.next()While loop — next() wraps unexpectedly
Detect illegal statee.name() == ""No check — illegal states propagate silently
Print state in logstate.name()int'(state) — raw numbers in logs

Summary

Enum is the type that makes FSMs debuggable and FSM case statements safe. The waveform benefit alone justifies using it everywhere you have a state variable. The compile-time and runtime checks from unique case catch entire classes of FSM bugs before they reach simulation. The three things that burn engineers: forgetting the base type (32 flip-flops), assigning integers without $cast, and partial case coverage causing latch inference.

  • Always specify enum logic [N:0] in RTL. The default int base synthesizes to 32 flip-flops.
  • Use unique case for FSM next-state logic. Catches illegal states in simulation, prevents latch inference in synthesis.
  • Convert integers to enum with $cast. Always check the return value — a failing cast means an invalid state was incoming.
  • name() returns "" for illegal values. Use this to detect FSM corruption: if (state.name() == "") $error(...);
  • next() wraps. Iterate with repeat(e.num()), not a while loop with a stop-at-last assumption.