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
// ── 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| Method | Returns | Description |
|---|---|---|
first() | enum value | First declared member regardless of current value |
last() | enum value | Last declared member |
next() | enum value | Member after current; wraps to first() after last() |
prev() | enum value | Member before current; wraps to last() before first() |
num() | int | Total count of declared members |
name() | string | String 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
| Name | Value (logic [1:0]) | int' cast | Waveform shows | name() returns |
|---|---|---|---|---|
ST_IDLE | 2'b00 | 0 | ST_IDLE | "ST_IDLE" |
ST_RUN | 2'b01 | 1 | ST_RUN | "ST_RUN" |
ST_DRAIN | 2'b10 | 2 | ST_DRAIN | "ST_DRAIN" |
ST_ERROR | 2'b11 | 3 | ST_ERROR | "ST_ERROR" |
enum next() and prev() Traversal
| Current state | state.next() | state.prev() | Wrap behavior |
|---|---|---|---|
ST_IDLE | ST_RUN | ST_ERROR (wraps!) | prev() on first wraps to last |
ST_RUN | ST_DRAIN | ST_IDLE | Normal |
ST_DRAIN | ST_ERROR | ST_RUN | Normal |
ST_ERROR | ST_IDLE (wraps!) | ST_DRAIN | next() on last wraps to first |
Synthesis Encoding Options
| Encoding | 4-state FSM bits | Power | Area | Specify how |
|---|---|---|---|---|
| Binary (default) | 2 bits | Low | Minimal | Default for enum logic [1:0] |
| One-hot | 4 bits (1 per state) | Higher | More FFs | Synthesis directive or explicit values |
| Gray code | 2 bits | Low switching | Same as binary | Synthesis directive |
| Auto (tool choice) | Tool decides | Optimized | Optimized | enum { A, B, C } default |
Code Examples — FSMs to Protocol Opcodes
Example 1 — Beginner: Basic enum 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
endmoduleExpected 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 stateExample 2 — Intermediate: RTL FSM With 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
endmoduleExample 3 — Verification: Enum in Constraints and Scoreboards
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
endmoduleExample 4 — Corner Case: Invalid Values, $cast, and Coverage
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
endmoduleSimulation 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.
| Construct | Missing arm behavior | Illegal value behavior | Synthesis |
|---|---|---|---|
case | Executes default, no warning | No detection | Standard mux |
case + default | Default executes | Default catches it | Standard mux with default |
unique case | Runtime warning: no match | Runtime warning: illegal value | Synthesis may remove redundant logic |
priority case | No warning, implicit default=no-op | No detection | Priority encoder |
Where enum Appears in Real Verification
// ── 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
// 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 — correctBug 2 — Direct Integer Assignment to enum 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
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 occurBug 4 — next() Wraps Unexpectedly at Last Member
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
endInterview 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 case | Correct approach | Avoid |
|---|---|---|
| RTL FSM state | typedef enum logic [N:0] { ... } | typedef enum { ... } (int base) |
| FSM combinational logic | unique case (state) | Plain case (latches on partial coverage) |
| int → enum | $cast(e, i) + check return | e = i (compile error/warning) |
| Iterate all values | repeat(e.num()) with e.next() | While loop — next() wraps unexpectedly |
| Detect illegal state | e.name() == "" | No check — illegal states propagate silently |
| Print state in log | state.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 defaultintbase synthesizes to 32 flip-flops. - Use
unique casefor 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 withrepeat(e.num()), not a while loop with a stop-at-last assumption.