An enum is a variable of some base type (default int, 2-state and 32-bit) restricted to named values. For RTL state machines always pin the base type, as in enum logic [1:0], which makes a 4-state register exactly as wide as synthesis needs.
Enums are strongly typed in one direction: assigning an enum to an int is free, but assigning an int to an enum needs a cast. A static cast such as state_e'(x) is unchecked and accepts any bit pattern, while $cast checks that the value is a named member. That asymmetry guards against absorbing illegal state encodings.
Method | Returns |
|---|---|
| First / last value in declaration order |
| Neighbor in declaration order, wrapping at the ends |
| Count of named values |
| Value name as a string, for self-describing prints |
The step methods follow declaration order and wrap around, so they never add 1 to the encoding. Monitors print st.name() instead of raw bits, and checkers iterate every legal value without hardcoding a list.
A packed struct lays its members out contiguously as one vector, with the first-declared field at the most-significant bits. Assign it to and from logic [N-1:0] freely, which makes packed structs the standard way to name bus payloads and packet headers.
An unpacked struct gives no layout guarantee, so it can never be treated as a vector. Its members can be any type, which suits testbench-side bookkeeping records. Packed-versus-unpacked layout detail lives in the arrays topic.
The streaming operator can reinterpret the same bits as bytes: {>> byte {h}} gives the cmd byte first, and {<< byte {h}} reverses the byte order. Shifts, Concatenation & Streaming runs both.
A packed union overlays its members on the same storage, so all members share one packed width. The classic use is one register viewed two ways: as a flat vector for the bus and as a field struct for the protocol layer.
Nothing records which view is currently valid, so reading the wrong member is silent garbage. Most production code pairs a packed union with an explicit discriminant field instead of relying on the type system.
A tagged union adds a hidden tag that the simulator checks, so reading the wrong member is an error instead of silent garbage. Neither free simulator supports tagged unions, which is one more reason most code uses a packed union plus an explicit field that says which view is valid.
next() return from DONE?IDLE. The step methods follow declaration order and wrap around at the ends.
Enum iteration, wrapping, and a checked int-to-enum cast, plus the packed struct round trip through a plain vector.
typedef enum logic [1:0] {IDLE = 2'b00, REQ = 2'b01,
GRANT = 2'b10, DONE = 2'b11} state_e;
module tb_enum;
state_e st;
int raw;
int errors = 0;
initial begin
st = st.first();
if (st !== IDLE) errors++;
if (st.num() != 4) errors++;
if (st.name() != "IDLE") errors++;
st = st.next(); // declaration order, not encoding + 1
if (st !== REQ) errors++;
st = DONE;
st = st.next(); // wraps past the last value
if (st !== IDLE) errors++;
st = st.prev(); // wraps back the other way
if (st !== DONE) errors++;
raw = 2;
if (!$cast(st, raw)) errors++; // 2 is a legal encoding
if (st !== GRANT) errors++;
raw = 4;
if ($cast(st, raw)) errors++; // 4 fits no 2-bit encoding
if (errors != 0) $fatal(1, "FAIL: enum checks saw %0d unexpected results", errors);
$display("PASS: enum walked declaration order, wrapped both ends, checked cast mapped 2 to GRANT");
$finish;
end
endmodulePASS: enum walked declaration order, wrapped both ends, checked cast mapped 2 to GRANTtypedef struct packed {
logic [7:0] cmd;
logic [23:0] addr;
} hdr_t;
module tb_struct;
hdr_t h;
logic [31:0] w;
int errors = 0;
initial begin
h.cmd = 8'hA5; // first field lands at the MSB
h.addr = 24'h1000;
w = h; // struct to vector: free
if (w !== 32'hA5001000) errors++;
h.addr = 24'hBEEF; // field update flows to the vector
w = h;
if (w !== 32'hA500BEEF) errors++;
h = w; // vector to struct: names the bits
if (h.cmd !== 8'hA5) errors++;
if (h.addr !== 24'hBEEF) errors++;
if (errors != 0) $fatal(1, "FAIL: struct checks saw %0d unexpected results", errors);
$display("PASS: packed struct matched the bus vector both ways with cmd at the top");
$finish;
end
endmodulePASS: packed struct matched the bus vector both ways with cmd at the topGRANT = 2'b10 second and DONE = 2'b11 third. What does next() return from GRANT?DONE. The step methods follow declaration order and wrap at the ends, independent of the encoded values.
cmd first and addr second for a 32-bit header. Which field owns bit 31?cmd. The first-declared field takes the most-significant bits, the opposite of many C bitfield habits.
One way to write it:
typedef enum logic [1:0] {DATA = 2'd0, CTRL = 2'd1, ERR = 2'd2} kind_e;
typedef struct packed {
kind_e kind; // header[7:6]
logic [1:0] dest; // header[5:4]
logic [3:0] len; // header[3:0]
} pkt_hdr_t;
module tb_pkt_hdr;
pkt_hdr_t h;
kind_e k;
logic [7:0] b;
int errors = 0;
initial begin
h.kind = DATA; h.dest = 2'd2; h.len = 4'd2;
b = h; // struct to byte
if (b !== 8'h22) errors++;
b = 8'h93; // 10_01_0011
h = b; // byte to struct
if (h.kind !== ERR || h.dest !== 2'd1 || h.len !== 4'd3) errors++;
k = h.kind; // name() on a plain enum variable
$display("header %h: kind=%s dest=%0d len=%0d", b, k.name(), h.dest, h.len);
if (errors == 0) $display("PASS: header packs to 22 and 93 unpacks to ERR, dest 1, len 3");
else $fatal(1, "FAIL: %0d header checks failed", errors);
$finish;
end
endmoduleheader 93: kind=ERR dest=1 len=3
PASS: header packs to 22 and 93 unpacks to ERR, dest 1, len 3Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. Icarus 13 cannot run the enum bench (it has no $cast); the struct and PacketFIFO header benches run on both.
Tagged unions are described but not run: neither free simulator supports them.
The PacketFIFO header bench prints the enum through a plain kind_e variable because Icarus 13 cannot call name() on a struct member directly.