Skip to main content

Structures and Unions

Plain Verilog has no way to group related signals into one named, reusable type — a full adder's three inputs (a, b, cin) are just three separate declarations, with nothing in the language connecting them. SystemVerilog's struct fixes this directly, borrowed almost unchanged from C.

typedef struct {
logic a;
logic b;
logic cin;
} adder_inputs_t;

adder_inputs_t in;
in.a = 1'b1;
in.b = 1'b0;
in.cin = 1'b1;

Once in is declared as an adder_inputs_t, its three fields move together as one unit — a function can take a single adder_inputs_t argument instead of three separate bit arguments, and a testbench can randomize all three fields with one randomize() call on the struct rather than three separate ones (randomization and constraints are covered later in this topic).

Packed vs. unpacked structs​

By default, a struct is unpacked — its fields have no guaranteed contiguous bit layout, and the struct as a whole can't be treated as a single vector. Adding the packed keyword changes that:

typedef struct packed {
logic valid;
logic [2:0] opcode;
logic [7:0] data;
} instruction_t;

instruction_t instr;
instr = 12'hAB3; // legal — packed struct can be assigned as one bit vector
logic [11:0] raw = instr; // and read back out as one bit vector

A packed struct guarantees its fields are laid out contiguously, MSB-first, with a fixed total width (1 + 3 + 8 = 12 bits here) — which means the whole struct can be assigned to and from a plain vector, is synthesizable, and can be used directly as a port or register type. An unpacked struct has no such guarantee and exists purely as a simulation/testbench convenience for grouping data — reach for packed whenever the struct needs to cross into real RTL (e.g., as a bus signal), and leave structs unpacked when they're pure testbench bookkeeping (e.g., a struct holding a test's expected results, which never becomes hardware).

union: one storage, multiple views​

A union declares several fields that all share the same physical storage — writing one field and reading a different one reinterprets the same bits, exactly like a C union:

typedef union packed {
logic [31:0] word;
logic [3:0][7:0] bytes; // same 32 bits, viewed as four 8-bit lanes
} word_view_t;

word_view_t w;
w.word = 32'hDEAD_BEEF;
// w.bytes[0] is now 8'hEF, w.bytes[3] is now 8'hDE — same storage, different view

Unions are far less common in everyday testbench code than structs — their main use is exactly this kind of reinterpretation (viewing one register as a whole word in one context and as individual bytes in another) without an explicit bit-slicing expression every time.

All fields of a packed union must be the same total bit width — the compiler rejects a packed union mixing, say, a 32-bit field with a 16-bit field, since there would be no single consistent bit layout to assign or read back as a vector. Practically all unions used in real testbench/RTL code are packed for exactly this reason: an unpacked union has no such size requirement, but reading a field other than the one most recently written is implementation-defined rather than portable across simulators.

What's next​

Structs group different-typed related fields together. The next page covers arrays — collections of same-typed elements — starting with the packed/unpacked distinction that parallels the one just covered for structs.