Skip to content

Enums, Typedefs, Structs & Unions

Data Types covers the built-in scalar and array types. This page covers the constructs you use to build your own types — the ones that turn a transaction from "a pile of loose bit variables" into a single, self-documenting, type-checked object.

enum — named states instead of magic numbers

typedef enum bit [1:0] {
  IDLE   = 2'b00,
  READ   = 2'b01,
  WRITE  = 2'b10,
  ERROR  = 2'b11
} state_e;

state_e cur_state = IDLE;

if (cur_state == READ) ...
$display("state = %s", cur_state.name());   // prints "READ", not "1"

An enum gives you named, type-checked values instead of encoding state as a bare integer. Two benefits matter specifically for verification: %s formatting and .name() print the symbolic name automatically (compare debugging a waveform/log showing state=2'b01 against one showing state=READ), and assigning an out-of-range or wrong-enum-type value is a compile-time or runtime error instead of a silently-accepted integer — a real class of "typo'd the wrong constant" bugs caught for free.

state_e s = IDLE;
s = s.next();     // WRITE... wait — next() walks declaration order, not encoded value order
s = s.prev();
int n = s.num();  // number of enum values in this type

.next()/.prev() step through the enum in declaration order, not numeric value order — worth checking explicitly if your enum's declared values aren't already sequential, since it's an easy source of an off-by-one bug when the encoding was chosen for hardware reasons (e.g. a one-hot state encoding) rather than for readability.

Always give an enum an explicit base type

enum { IDLE, READ, WRITE } without a base type defaults to a plain int — usually fine, but explicit enum bit [1:0] {...} documents (and enforces) the intended hardware width, which matters the moment this enum is used to drive or compare against an actual DUT signal rather than staying purely inside testbench bookkeeping.

typedef — naming a type once, using it everywhere

typedef bit [31:0] addr_t;
typedef bit [7:0]  byte_t;

addr_t base_addr;
byte_t payload [0:63];

typedef doesn't create a new distinct type the way enum does — it's purely an alias. Its value is consistency and refactoring safety: if every driver, monitor, and scoreboard in a testbench declares addresses as addr_t instead of repeating bit [31:0] everywhere, widening the address bus later is a one-line change instead of a project-wide find-and-replace across dozens of files that's easy to miss an instance of.

typedef struct packed {
  bit [7:0]  addr;
  bit [15:0] data;
  bit        parity;
} apb_txn_s;

apb_txn_s t;
t.addr = 8'h20;
t.data = 16'hBEEF;
t.parity = ^{t.addr, t.data};   // reduction XOR over the concatenation — see Operators & Expressions

A struct bundles related fields into one named object — the natural fit for a bus transaction, where "address, data, and parity travel together" is the actual domain concept, not three independent variables that happen to be used near each other. packed structs are stored as one contiguous bit vector (so the whole struct can be cast to/from a plain bit vector, useful for driving a DUT's flattened bus signals directly from one struct field); unpacked structs (the default, no packed keyword) store each field independently and allow member types that can't be packed at all, like a string or a dynamic array field.

A struct is not a class

Structs are value types: assigning one struct variable to another copies every field, unlike a class handle assignment (see OOP & Classes), which aliases the same object. This is often exactly what you want for a lightweight bus-transaction struct passed by value through a checker, but it's the opposite behavior of what a class-based sequence item does — know which one you're holding before relying on assignment semantics either way.

union — one storage location, multiple views

typedef union packed {
  bit [31:0]        raw;
  struct packed {
    bit [7:0] byte3, byte2, byte1, byte0;
  } bytes;
} word_u;

word_u w;
w.raw = 32'hDEADBEEF;
$display("byte0 = %h", w.bytes.byte0);   // EF — same underlying bits, viewed as 4 bytes

A union overlays multiple member declarations onto the same storage — writing through one member and reading through another reinterprets the same bits rather than copying between separate locations. This is far less common in day-to-day testbench code than struct, but it's the natural tool for exactly one recurring case: viewing one register or bus word as either "one 32-bit value" or "four separate bytes" depending on which access granularity a given piece of code cares about, without manual bit-slicing in either direction.

string — the type behind your log messages

string msg = "transaction failed";
msg = {msg, ": bad parity"};        // concatenation via {}
if (msg.substr(0, 10) == "transaction") ...
int len = msg.len();
string upper = msg.toupper();

string is a genuine dynamic type (not a fixed-width character array), garbage-collected like a class handle, with a small built-in method set (len(), substr(), toupper()/tolower(), compare(), putc()/getc(), atoi() and friends for numeric conversion). It shows up constantly in testbench infrastructure — UVM's own reporting macros (uvm_info/uvm_error) take string messages — but essentially never in synthesizable RTL, which has no hardware representation for a variable-length character sequence.