Structures — Packed & Unpacked
Register field modeling, protocol frame packing, struct literals.
Module 2 · Page 2.7
The Type That Models Both Hardware Registers and Transaction Objects
Every register in your DUT has named fields. The AXI AW channel has AWADDR, AWLEN, AWSIZE, AWBURST. Without struct, your RTL has to manually track which bits hold which field, and every access looks like ctrl_reg[15:8] with a comment saying "this is the TIMEOUT field." With a packed struct, you write ctrl_reg.timeout and the compiler handles the bit positioning.
On the verification side, every UVM transaction class is conceptually an unpacked struct — a named collection of fields like address, data, response code, and burst type. The difference from a class is that structs have no methods, no inheritance, and are value types (assignment copies data, not a handle). That simplicity makes them perfect for lightweight data carriers passed between functions and tasks.
The packed vs unpacked distinction is the critical split in struct usage. Packed means the fields are laid out as a contiguous bit vector — you can treat the entire struct as a single wide signal, connect it to ports, do bitwise operations on it, and send it through an interface. Unpacked means each field has independent storage — you can use arrays, strings, class handles, and dynamic types as fields, but you lose the single-vector treatment.
Packed vs Unpacked — The Fundamental Split
A packed struct lays its fields out as a contiguous bit vector, MSB-first in declaration order. The first declared field occupies the most significant bits; the last occupies the least significant. The entire struct can be assigned to/from a logic vector of the same total width, passed through ports, and used in bitwise operations. Every field must have a fixed bit width — no strings, no dynamic arrays.
An unpacked struct stores each field independently, like a C struct. Fields are accessed by name but have no mandatory bit packing or ordering. You can use any type as a field: arrays, queues, strings, class handles, other structs. The struct cannot be treated as a single bit vector — you cannot assign it to logic [N:0] or connect it to a plain port.
- struct packed — contiguous vector — Fields form a contiguous bit vector. First field = MSB. Can be treated as logic [N:0]. Port-connectable. Synthesizable. Use for registers, protocol frames, hardware signals.
- struct (unpacked) — independent fields — Each field has independent storage. Any field type allowed including arrays, strings, class handles. Cannot be treated as a bit vector. Use for TB transactions, data models.
- Whole-struct copy — Both packed and unpacked support single-statement copy: s2 = s1. Creates a deep copy of all fields. Value type — modifying s2 does not affect s1.
- Struct literal '{} — Initialize with named or positional values: '{field: val} or '{val1, val2}. Named form is clearer and order-independent.
Syntax — Every Form You'll Use
// ── PACKED STRUCT ─────────────────────────────────────────────────
// Fields laid out as contiguous bit vector, MSB-first (first declared = MSB)
typedef struct packed {
logic [3:0] opcode; // bits [15:12] — MSB group
logic [3:0] dst; // bits [11:8]
logic [3:0] src; // bits [7:4]
logic [3:0] imm; // bits [3:0] — LSB group
} instr_t; // total = 16 bits
instr_t ins;
logic [15:0] raw_bits;
// Field access
ins.opcode = 4'hA;
ins.dst = 4'h3;
ins.src = 4'h1;
ins.imm = 4'h7;
// Packed struct ↔ logic vector (same total width)
raw_bits = ins; // 16'hA317 — direct assignment
ins = instr_t'(raw_bits); // logic → packed struct via cast
// ── UNPACKED STRUCT ───────────────────────────────────────────────
// Fields have independent storage; any type is allowed
typedef struct {
logic [31:0] addr;
logic [31:0] data;
bit is_write;
string label; // string: only in unpacked struct
int latency;
} txn_t;
txn_t t1, t2;
t1.addr = 32'hA000;
t1.data = 32'hCAFE;
t1.is_write = 1'b1;
t1.label = "WRITE_0";
t1.latency = 5;
t2 = t1; // deep copy — t2 is independent of t1
// ── STRUCT LITERAL ────────────────────────────────────────────────
instr_t ins2 = '{opcode:4'h5, dst:4'h2, src:4'h1, imm:4'h0};
txn_t t3 = '{addr:32'h1000, data:32'hFF, is_write:1, label:"RD", latency:3};
// Or positional (order must match declaration):
instr_t ins3 = '{4'h5, 4'h2, 4'h1, 4'h0}; // positional
// ── SIGNED PACKED STRUCT ──────────────────────────────────────────
typedef struct packed signed {
logic [15:0] real_part;
logic [15:0] imag_part;
} complex_t; // whole struct treated as signed 32-bit when used as vector
// ── NESTED STRUCT ─────────────────────────────────────────────────
typedef struct packed {
instr_t cmd; // nested packed struct
logic valid;
} pkt_t; // total = 17 bits
pkt_t pkt;
pkt.cmd.opcode = 4'hF; // nested field access
pkt.valid = 1'b1;| Feature | struct packed | struct (unpacked) |
|---|---|---|
| Memory layout | Contiguous bit vector | Independent field storage |
| First field position | MSB (highest bits) | No bit ordering |
| Assign to logic [N:0] | Yes — direct or via cast | No — type error |
| Port-connectable | Yes — as a packed type | No — port must use packed/plain types |
| Allowed field types | Only packed types (logic, bit, packed struct/enum) | Any type including string, array, class handle |
| Synthesizable | Yes | Partially (non-synthesizable field types excluded) |
| Bitwise operations on whole struct | Yes | No |
| Whole-struct copy | Yes | Yes |
Visual — Packed Bit Layout and Field Mapping
Packed Struct Bit Positions
Declaration order determines bit position — first declared = most significant bits. typedef struct packed { logic [3:0] op; logic [3:0] dst; logic [3:0] src; logic [3:0] imm; } instr_t
| Field | Width | Bit positions in 16-bit word | Access | ins = 16'hA317 → field value |
|---|---|---|---|---|
op | 4 | [15:12] — MSB | ins.op | 4'hA |
dst | 4 | [11:8] | ins.dst | 4'h3 |
src | 4 | [7:4] | ins.src | 4'h1 |
imm | 4 | [3:0] — LSB | ins.imm | 4'h7 |
| Whole struct | 16 | [15:0] | ins or cast to logic | 16'hA317 |
Struct Literal Forms
| Form | Example | Notes |
|---|---|---|
| Named (preferred) | '{op:4'hA, dst:4'h3, src:4'h1, imm:4'h7} | Order-independent, self-documenting |
| Positional | '{4'hA, 4'h3, 4'h1, 4'h7} | Must match declaration order exactly |
| Default fill | '{default: '0} | All fields to 0 |
| Partial named | '{op:4'hA, default:'0} | Set op, fill rest with 0 |
Code Examples — Register Models to Protocol Transactions
Example 1 — Beginner: Packed Struct as Register
module tb_packed_struct;
// 32-bit control register layout:
// [31:24] = timeout (8 bits) [23:16] = max_retry (8 bits)
// [15:8] = mode (8 bits) [7:0] = flags (8 bits)
typedef struct packed {
logic [7:0] timeout;
logic [7:0] max_retry;
logic [7:0] mode;
logic [7:0] flags;
} ctrl_reg_t;
ctrl_reg_t ctrl;
logic [31:0] raw;
initial begin
// ── Field-by-field assignment ─────────────────────────────────
ctrl.timeout = 8'd100;
ctrl.max_retry = 8'd3;
ctrl.mode = 8'h05;
ctrl.flags = 8'b0000_0001;
// ── Packed struct → raw bits ──────────────────────────────────
raw = ctrl;
$display("ctrl raw = 0x%08h", raw); // 0x64030501
$display("timeout = %0d", ctrl.timeout); // 100
$display("flags = %08b", ctrl.flags); // 00000001
// ── Assign raw bits to struct ─────────────────────────────────
raw = 32'hAABBCCDD;
ctrl = ctrl_reg_t'(raw);
$display("From raw 0xAABBCCDD:");
$display(" timeout = 0x%02h", ctrl.timeout); // AA
$display(" max_retry = 0x%02h", ctrl.max_retry); // BB
$display(" mode = 0x%02h", ctrl.mode); // CC
$display(" flags = 0x%02h", ctrl.flags); // DD
// ── Struct literal ────────────────────────────────────────────
ctrl = '{timeout:8'd50, max_retry:8'd5, mode:8'h02, flags:8'h00};
$display("Literal: 0x%08h", logic'(ctrl)); // 0x32050200
$finish;
end
endmoduleExample 2 — Intermediate: AXI Write Address Channel as Packed Struct
// AXI4 Write Address Channel beat — pack into single vector for monitoring
typedef struct packed {
logic [7:0] awid;
logic [31:0] awaddr;
logic [7:0] awlen;
logic [2:0] awsize;
logic [1:0] awburst;
} axi_aw_t; // total = 56 bits
module tb_axi_struct;
axi_aw_t aw_beat;
// Build a write beat for a 4-beat INCR burst at 0xA000_0000
initial begin
aw_beat = '{
awid: 8'h05,
awaddr: 32'hA000_0000,
awlen: 8'd3, // 4 beats (len+1)
awsize: 3'b010, // 4 bytes per beat
awburst: 2'b01 // INCR
};
$display("AW Beat:");
$display(" AWID = 0x%02h", aw_beat.awid);
$display(" AWADDR = 0x%08h", aw_beat.awaddr);
$display(" AWLEN = %0d (burst of %0d)", aw_beat.awlen, aw_beat.awlen+1);
$display(" AWBURST = %02b (INCR)", aw_beat.awburst);
// Pass the packed struct as a single 56-bit vector
logic [55:0] raw_beat = aw_beat;
$display("Packed: 0x%014h", raw_beat);
// Reconstruct from raw bits
axi_aw_t recovered = axi_aw_t'(raw_beat);
$display("Recovered addr: 0x%08h", recovered.awaddr);
$finish;
end
endmoduleExample 3 — Verification: Unpacked Struct Transaction Object
typedef enum logic [1:0] { OKAY, EXOKAY, SLVERR, DECERR } resp_t;
typedef struct {
logic [7:0] tid;
logic [31:0] addr;
logic [31:0] data []; // dynamic array — ONLY in unpacked struct
resp_t resp;
string label; // string — ONLY in unpacked struct
int latency;
} axi_resp_txn_t;
module tb_unpacked_struct;
task automatic print_txn(input axi_resp_txn_t t);
$display("[%s] tid=0x%h addr=0x%08h resp=%s lat=%0d",
t.label, t.tid, t.addr, t.resp.name(), t.latency);
foreach (t.data[i])
$display(" data[%0d] = 0x%08h", i, t.data[i]);
endtask
task automatic compare_txn(input axi_resp_txn_t exp, got);
if (exp.tid !== got.tid) $error("TID mismatch");
if (exp.addr !== got.addr) $error("ADDR mismatch");
if (exp.resp !== got.resp)
$error("RESP: exp=%s got=%s", exp.resp.name(), got.resp.name());
foreach (exp.data[i])
if (exp.data[i] !== got.data[i])
$error("data[%0d] mismatch: exp=0x%h got=0x%h", i, exp.data[i], got.data[i]);
$display("PASS: %s", exp.label);
endtask
initial begin
axi_resp_txn_t t1, t2;
t1.tid = 8'h07;
t1.addr = 32'hA000_0100;
t1.data = new[2]('{32'hAAAA, 32'hBBBB});
t1.resp = OKAY;
t1.label = "write_beat_0";
t1.latency = 8;
print_txn(t1);
t2 = t1; // whole-struct copy — t2 is independent
t2.data[0] = 32'hCCCC; // does NOT affect t1.data[0]
compare_txn(t1, t2); // will report data[0] mismatch
$finish;
end
endmoduleExample 4 — RTL: Packed Struct on Module Port
package axi_pkg;
typedef struct packed {
logic awvalid;
logic [31:0] awaddr;
logic [7:0] awlen;
logic [1:0] awburst;
} axi_aw_t; // 42 bits total
endpackage
import axi_pkg::*;
// DUT: receives packed struct on a single port
module axi_slave (
input logic clk,
input axi_aw_t aw, // packed struct port — one 42-bit input
output logic awready
);
assign awready = aw.awvalid; // access fields directly
endmodule
// Testbench: drive the packed struct port
module tb_port_struct;
import axi_pkg::*;
logic clk = 0;
axi_aw_t aw_drive;
logic awready;
axi_slave dut (.clk(clk), .aw(aw_drive), .awready(awready));
always #5 clk = ~clk;
initial begin
aw_drive = '{awvalid:1'b0, awaddr:32'h0, awlen:8'h0, awburst:2'b00};
@(posedge clk);
aw_drive.awvalid = 1'b1;
aw_drive.awaddr = 32'hA000_0000;
aw_drive.awlen = 8'd15;
aw_drive.awburst = 2'b01;
@(posedge clk);
$display("awready = %0b", awready); // 1
$finish;
end
endmoduleSimulation and Synthesis Behavior
Packed Struct in Synthesis
A packed struct synthesizes identically to a plain logic [N:0] of the same total width. The field names are purely a compile-time convenience — the synthesizer generates the same gates whether you write ctrl.timeout or ctrl_reg[31:24]. This means packed structs have zero overhead — no extra logic, no alignment padding. The field boundaries are exact, and accessing one field at synthesis is equivalent to a bit-slice of the underlying vector.
Unpacked Struct — Value Type Semantics
Unpacked struct assignment (t2 = t1) creates a deep copy of all fields. For fields that are themselves arrays or dynamic types, the copy is element-by-element — each field in t2 gets its own independent storage. After t2 = t1, modifying t2.data does not affect t1.data. This is value-type semantics — the same as copying all the fields manually one by one, but in one statement.
| Operation | Packed struct | Unpacked struct |
|---|---|---|
| Synthesizable | Yes | Depends on field types |
| Port connection | Yes — treated as packed vector | No — incompatible with plain ports |
| Bitwise operations (& | ^) | Yes — on whole struct as vector | No |
| Dynamic array field | Not allowed | Allowed |
| string field | Not allowed | Allowed |
| Assignment | Deep copy of bit vector | Deep copy of all fields |
| Comparison (== !=) | Whole-struct comparison | Whole-struct comparison (all fields) |
Where Structs Shape Real Verification Architecture
// ── 1. PACKED STRUCT FOR REGISTER MAP FIELD ACCESS ────────────────
typedef struct packed {
logic err_en; // [31]
logic [6:0] rsvd; // [30:24]
logic [7:0] timeout; // [23:16]
logic [15:0] base_addr; // [15:0]
} dma_ctrl_t;
dma_ctrl_t ctrl_shadow; // mirror of DUT register
logic [31:0] reg_readback;
// After reading DUT register via RAL:
ctrl_shadow = dma_ctrl_t'(reg_readback);
$display("err_en=%b timeout=%0d base=0x%04h",
ctrl_shadow.err_en, ctrl_shadow.timeout, ctrl_shadow.base_addr);
// ── 2. UNPACKED STRUCT FOR TRANSACTION QUEUES ─────────────────────
typedef struct {
logic [7:0] tid;
logic [31:0] addr;
logic [31:0] data;
bit is_write;
} beat_t;
beat_t exp_q [$]; // queue of struct objects
beat_t got_q [$];
// ── 3. PACKED STRUCT COMPARISON (uses === for X detection) ────────
function automatic bit beats_match(input beat_t e, g);
return (e.tid === g.tid && e.addr === g.addr && e.data === g.data);
endfunction
// ── 4. STRUCT IN COVERAGE ─────────────────────────────────────────
// covergroup cg_beat with function sample(beat_t b);
// cp_rw: coverpoint b.is_write;
// cp_addr: coverpoint b.addr[31:28]; // top nibble
// endgroup
// ── 5. NESTED PACKED STRUCT FOR AXI W BEAT ────────────────────────
typedef struct packed {
logic [7:0] wstrb;
logic [31:0] wdata;
logic wlast;
} axi_w_beat_t; // 41 bits total — directly connectable to AXI W channelBugs Engineers Hit With struct
Bug 1 — Wrong Field Order in Packed Struct: Silent Bit Misalignment
// Register spec: [31:24]=opcode, [23:16]=addr_hi, [15:0]=addr_lo
// BUGGY: fields declared in wrong order (LSB first)
typedef struct packed {
logic [15:0] addr_lo; // declared first → goes into bits [31:16] — WRONG!
logic [7:0] addr_hi; // bits [15:8] — WRONG!
logic [7:0] opcode; // bits [7:0] — WRONG!
} bad_reg_t;
bad_reg_t r = bad_reg_t'(32'hABCDEF12);
$display("opcode = 0x%02h", r.opcode); // 12 — WRONG, should be AB
// CORRECT: MSB field declared first
typedef struct packed {
logic [7:0] opcode; // bits [31:24] — MSB first
logic [7:0] addr_hi; // bits [23:16]
logic [15:0] addr_lo; // bits [15:0]
} good_reg_t;
good_reg_t g = good_reg_t'(32'hABCDEF12);
$display("opcode = 0x%02h", g.opcode); // AB — correctBug 2 — Dynamic Array in Packed Struct: Compile Error
// BUGGY: dynamic array and string inside packed struct
typedef struct packed {
logic [7:0] tid;
logic [31:0] data []; // COMPILE ERROR: dynamic array not allowed in packed struct
string label; // COMPILE ERROR: string not allowed in packed struct
} bad_t;
// FIXED option 1: use unpacked struct for variable-content data
typedef struct {
logic [7:0] tid;
logic [31:0] data []; // OK in unpacked
string label; // OK in unpacked
} good_t;
// FIXED option 2: if you need packed + variable data, use separate fields
typedef struct packed { logic [7:0] tid; logic [31:0] ctrl; } packed_part_t;
typedef struct {
packed_part_t hdr; // packed sub-struct
logic [31:0] payload []; // dynamic part in unpacked wrapper
} hybrid_t;Bug 3 — Unpacked Struct Port Connection Fails
typedef struct { // UNPACKED — note: no 'packed' keyword
logic [31:0] addr;
logic [31:0] data;
} txn_t;
// BUGGY: unpacked struct on a module port
module bad_module (input txn_t txn); // ERROR: port type must be packed
endmodule
// FIXED: use packed struct for port connections
typedef struct packed {
logic [31:0] addr;
logic [31:0] data;
} packed_txn_t;
module good_module (input packed_txn_t txn); // OK
$display("addr = 0x%08h", txn.addr);
endmoduleInterview Questions
Beginner Level
Q1: What is the difference between a packed and unpacked struct in SystemVerilog? A struct packed lays all fields as a contiguous bit vector — the first declared field occupies the MSB. It can be assigned to/from a logic [N:0] vector, connected to module ports, and used in bitwise operations. Only packed types (logic, bit, packed structs/enums) are allowed as fields. An unpacked struct gives each field independent storage — any type is allowed including dynamic arrays and strings. It cannot be treated as a single bit vector or connected to plain ports. Q2: In a packed struct, which field occupies the MSB? The first declared field occupies the most significant bits. In struct packed { logic [7:0] A; logic [7:0] B; } s, field A occupies bits [15:8] (MSB side) and B occupies bits [7:0]. Assigning raw value 0xABCD gives A=0xAB and B=0xCD. This matches standard hardware register documentation where bit 31 is described first.
Intermediate Level
Q3: Can you use a struct as a module port? What constraints apply? Yes, but only a packed struct. The port system in SV requires packed types (or plain net/variable types) on module boundaries. An unpacked struct cannot be a module port because it has no defined bit-level representation for signal connection. When a packed struct is used as a port, it is treated exactly like a logic [N:0] of the same total width — the field names are only accessible inside the module.
Experienced Engineer Level
Q4: A monitor captures an AXI W channel beat as a 41-bit logic vector. How do you extract the WDATA, WSTRB, and WLAST fields cleanly without hardcoded bit slices? Define a packed struct matching the wire layout: typedef struct packed { logic wlast; logic [3:0] wstrb; logic [31:0] wdata; } axi_w_t; (note order: first declared = MSB, so wlast goes at bit [40]). Then cast the captured bits: axi_w_t beat = axi_w_t'(captured_41_bits);. Access via beat.wdata, beat.wstrb, beat.wlast. When the spec changes (e.g., wdata widens to 64 bits), you update the typedef once and all access points update automatically.
Best Practices & Coding Guidelines
- Packed for hardware, unpacked for TB — Use struct packed for anything that maps to hardware: registers, protocol beats, port bundles. Use unpacked structs for TB transaction objects where fields may include arrays, strings, or latency counters.
- First declared = MSB in packed — Always verify field ordering against the register spec. Draw the bit-field diagram next to your struct declaration during code review. Wrong order = silent bit misassignment.
- Use typedef and package together — Define struct types with typedef in a shared package. Never duplicate struct definitions — one change in the package spec propagates everywhere immediately.
- Named struct literals over positional — '{addr:32'h0, data:32'h0} over '{32'h0, 32'h0}. Named is order-independent, self-documenting, and survives field additions without silent value shifts.
| Use case | Type | Reason |
|---|---|---|
| Hardware register field model | struct packed | Direct assignment to/from raw register value |
| Protocol frame (AXI/AHB beat) | struct packed | Port-connectable, synthesizable, single-vector treatment |
| UVM transaction / scoreboard entry | struct (unpacked) | Can hold dynamic arrays, strings, class handles |
| Function argument grouping | Either, depending on fields | Pass multiple related values as one parameter |
| Array of transactions | Unpacked struct | txn_t q [$] — queue of struct objects |
Summary
Structs bridge the gap between raw bit vectors and named data structures. Packed structs are hardware — they map directly to register fields and protocol frames with zero overhead and full synthesizability. Unpacked structs are software — they model transactions and data objects with maximum field type flexibility. The three things that cause bugs: wrong field order in a packed struct (silently misaligns every field), trying to put dynamic arrays or strings in a packed struct (compile error), and connecting an unpacked struct to a module port (type error).
- First declared field = MSB in packed struct. Declare fields in the same order as the register spec (MSB first).
- Packed structs can be assigned to/from logic [N:0]. Cast with
struct_t'(raw)or assign directly when types match. - Unpacked structs allow any field type including dynamic arrays, strings, and class handles — packed structs do not.
- Only packed structs can be module ports. Design interfaces with packed structs for signal bundles.
- Both types support whole-struct copy and struct literals. Use named literals for readability and resilience to field reordering.