Skip to content

SystemVerilog · Module 2

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.

Named field types are declared with typedef, and where several layouts must share storage a union is the related construct. 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

SystemVerilog — struct Syntax
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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;
Featurestruct packedstruct (unpacked)
Memory layoutContiguous bit vectorIndependent field storage
First field positionMSB (highest bits)No bit ordering
Assign to logic [N:0]Yes — direct or via castNo — type error
Port-connectableYes — as a packed typeNo — port must use packed/plain types
Allowed field typesOnly packed types (logic, bit, packed struct/enum)Any type including string, array, class handle
SynthesizableYesPartially (non-synthesizable field types excluded)
Bitwise operations on whole structYesNo
Whole-struct copyYesYes

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

FieldWidthBit positions in 16-bit wordAccessins = 16'hA317 → field value
op4[15:12] — MSBins.op4'hA
dst4[11:8]ins.dst4'h3
src4[7:4]ins.src4'h1
imm4[3:0] — LSBins.imm4'h7
Whole struct16[15:0]ins or cast to logic16'hA317

Struct Literal Forms

FormExampleNotes
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

Example 1 — Packed Struct: Control Register
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
 
endmodule

Example 2 — Intermediate: AXI Write Address Channel as Packed Struct

Example 2 — AXI Protocol Beat as Packed Struct
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Example 3 — Verification: Unpacked Struct Transaction Object

Example 3 — Unpacked Struct for Verification Transactions
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
 
endmodule

Example 4 — RTL: Packed Struct on Module Port

Example 4 — Packed Struct as Module Port
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endmodule

Simulation 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.

OperationPacked structUnpacked struct
SynthesizableYesDepends on field types
Port connectionYes — treated as packed vectorNo — incompatible with plain ports
Bitwise operations (& | ^)Yes — on whole struct as vectorNo
Dynamic array fieldNot allowedAllowed
string fieldNot allowedAllowed
AssignmentDeep copy of bit vectorDeep copy of all fields
Comparison (== !=)Whole-struct comparisonWhole-struct comparison (all fields)

Where Structs Shape Real Verification Architecture

Verification Patterns Using struct
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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 channel

Bugs Engineers Hit With struct

Bug 1 — Wrong Field Order in Packed Struct: Silent Bit Misalignment

Bug 1 — Reversed Field Order Misaligns Bits
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 — correct

Bug 2 — Dynamic Array in Packed Struct: Compile Error

Bug 2 — Invalid Field Type in Packed Struct
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Bug 3 — Unpacked Struct Cannot Be a Module Port
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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);
endmodule
Wire-order declaration places wlast at the MSB and wdata at the LSBs matching the interface, while spec-table-order declaration shifts every field by five bit positionsInterface wire layout[36] wlast · [35:32] wstrb · [31:0]wdataDeclared in WIRE orderwlast, wstrb, wdata — first membertakes the MSBsFields land correctlywlast=[36] wstrb=[35:32]wdata=[31:0]Declared in SPEC-TABLE orderwdata, wstrb, wlast — payload listedfirstEvery field shifted by 5wdata=[36:5] wstrb=[4:1] wlast=[0]Both are 37 bitsthe cast is width-correct, sonothing warns12
Figure 1 — the same three fields declared two ways, and the layouts that result. A packed struct places its first declared member in the most significant bits, so declaring wlast, wstrb, wdata in that order produces the wire layout an AXI W beat actually has: wlast at bit 36, wstrb at 35:32, wdata at 31:0. Declaring the same three fields in specification-table order — wdata first, because it is the field the document leads with — produces a struct of exactly the same 37-bit width whose fields sit five positions away from where the interface puts them. Both typedefs are legal SystemVerilog and the cast from a 37-bit capture compiles either way, so nothing warns; the payload simply picks up its neighbours in its top bits and loses its own bottom bits. Declaration order is the layout specification, not a documentation convenience.

A Runnable Proof — Where Each Field Actually Lands

The page states that a packed struct lays its fields MSB-first in declaration order. That is the rule the whole type depends on, and it is worth proving rather than trusting, because an off-by-one in a field's position produces data that is wrong in a way no compiler can see.

packed_struct_layout_proof.sv — field positions, checked
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module packed_struct_layout_proof;
 
  // First declared field occupies the MOST significant bits.
  typedef struct packed {
    logic [7:0]  a;      // bits [23:16]
    logic [7:0]  b;      // bits [15:8]
    logic [7:0]  c;      // bits [7:0]
  } three_byte_t;
 
  // A realistic case: an AXI W-channel beat, 37 bits wide.
  typedef struct packed {
    logic        wlast;     // bit  [36]
    logic [3:0]  wstrb;     // bits [35:32]
    logic [31:0] wdata;     // bits [31:0]
  } axi_w_t;
 
  three_byte_t s;
  axi_w_t      beat;
  logic [23:0] raw;
  int fails = 0;
 
  task automatic expect (input string what, input bit cond);
    if (cond) $display("  PASS  %s", what);
    else begin $display("  FAIL  %s", what); fails++; end
  endtask
 
  initial begin
    $display("\n1. Total width is the sum of the fields");
    expect("three_byte_t is 24 bits", $bits(three_byte_t) == 24);
    expect("axi_w_t is 37 bits",      $bits(axi_w_t)      == 37);
 
    $display("\n2. A packed struct IS a vector - assignment both ways");
    raw = 24'hAA_BB_CC;
    s   = raw;                       // no cast needed: same packed width
    $display("  raw = %h -> a=%h b=%h c=%h", raw, s.a, s.b, s.c);
    expect("first declared field takes the MSBs (a == AA)", s.a == 8'hAA);
    expect("middle field (b == BB)",                        s.b == 8'hBB);
    expect("last declared field takes the LSBs (c == CC)",  s.c == 8'hCC);
 
    $display("\n3. ...and the reverse direction agrees");
    s.a = 8'h11; s.b = 8'h22; s.c = 8'h33;
    expect("struct reads back as one vector", 24'(s) == 24'h11_22_33);
 
    $display("\n4. Field positions are exactly where the rule says");
    // Slicing the struct as a vector must match the named field.
    expect("a occupies [23:16]", 24'(s)[23:16] == s.a);
    expect("b occupies [15:8]",  24'(s)[15:8]  == s.b);
    expect("c occupies [7:0]",   24'(s)[7:0]   == s.c);
 
    $display("\n5. The AXI beat - the case that motivates all of this");
    beat = 37'( {1'b1, 4'hF, 32'hDEAD_BEEF} );
    $display("  wlast=%b wstrb=%h wdata=%h", beat.wlast, beat.wstrb, beat.wdata);
    expect("wlast is the MSB",  beat.wlast == 1'b1);
    expect("wstrb next",        beat.wstrb == 4'hF);
    expect("wdata the LSBs",    beat.wdata == 32'hDEAD_BEEF);
 
    $display("\n%0s (%0d failures)\n",
             fails == 0 ? "ALL CHECKS PASSED" : "CHECKS FAILED", fails);
    if (fails) $fatal(1, "packed_struct_layout_proof failed");
    $finish;
  end
endmodule

Section 4 is the one worth running once yourself. Slicing the struct as a vector and comparing against the named field is the direct statement of the layout rule, and it is also the check that catches a typedef whose field order does not match the wire order it was written to describe.

1

A monitor decoded every AXI beat with the fields in the wrong places

PACKED-STRUCT-FIELD-ORDER
Symptom

An AXI write monitor reconstructed beats whose WDATA was plausible but wrong, WSTRB was almost always 4'h0 or 4'hF, and WLAST was asserted on approximately every beat rather than at the end of a burst.

The scoreboard reported data mismatches on every write burst. Because WLAST looked asserted constantly, the first hypothesis was that the DUT was terminating bursts early, and two days went into examining burst-length handling in the design.

Reads were completely clean, which was treated as evidence that the monitor was fine and the write path was broken.

Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The wire layout, from the interface, is:
//   [36]     WLAST
//   [35:32]  WSTRB
//   [31:0]   WDATA
 
// The typedef was written in the order the fields appear in the SPEC TABLE,
// which lists WDATA first because it is the most important field.
typedef struct packed {
  logic [31:0] wdata;      // <- declared FIRST, so it takes bits [36:5]
  logic [3:0]  wstrb;      // <- bits [4:1]
  logic        wlast;      // <- bit  [0]
} axi_w_t;
 
axi_w_t beat = axi_w_t'(captured_37_bits);
Diagnostic Evidence

Printing the raw capture beside the decoded fields made it obvious in one beat:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  captured = 37'h1_F_DEADBEEF        (wlast=1, wstrb=F, wdata=DEADBEEF)
 
  decoded with the buggy typedef:
    beat.wdata = 32'hBF56DF7D        <- bits [36:5] of the capture
    beat.wstrb = 4'h7                <- bits [4:1]
    beat.wlast = 1'b1                <- bit  [0]

Every field is shifted by five bit positions. wdata picked up wlast and wstrb in its top bits and lost the bottom five bits of the real data, which is why the value looked like plausible data rather than obvious garbage — it was mostly the right bits in the wrong place.

The WLAST-always-asserted symptom followed directly: bit 0 of the capture is the least significant bit of WDATA, which is high on roughly half of random data — so wlast read as asserted about half the time and, on the ASCII-ish payloads this test used, nearly always.

The reads being clean was a coincidence that cost a day: the read-channel typedef had been written by someone else, in wire order, and was correct.

Root Cause

A packed struct lays its fields MSB-first in declaration order. The typedef was written in the order the fields appear in the specification's field table, which lists the payload first for readability, and that order is the reverse of the wire order.

Nothing catches this. The struct is 37 bits either way, so the cast from a 37-bit vector is width-correct and legal; the compiler has no knowledge of what the bits are supposed to mean. Both typedefs are valid SystemVerilog describing different layouts, and only one of them matches the interface.

The bug is also self-consistent, which is what makes it survive review. If both the monitor and the driver used the same wrong typedef, the testbench would talk to itself perfectly and only disagree with the DUT — and if a scoreboard compared driver-side and monitor-side transactions rather than against the interface, even that disagreement could vanish.

The lesson is that a packed struct is a layout specification, not a documentation convenience. Writing it in reading order rather than bit order produces a type that is correct in isolation and wrong at the boundary.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Declare in WIRE order: most significant field first.
typedef struct packed {
  logic        wlast;      // bit  [36]
  logic [3:0]  wstrb;      // bits [35:32]
  logic [31:0] wdata;      // bits [31:0]
} axi_w_t;

Verifying it does not need the DUT. Assert the field positions directly against the vector, which is a compile-and-run check that takes four lines:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initial begin
  axi_w_t t;
  t = 37'( {1'b1, 4'hF, 32'hDEAD_BEEF} );
  assert (t.wlast == 1'b1 && t.wstrb == 4'hF && t.wdata == 32'hDEAD_BEEF)
    else $fatal(1, "axi_w_t field order does not match the wire layout");
end

Two habits prevent recurrence.

Add a width assertion beside every packed typedef used at a boundary. $bits(axi_w_t) == 37 catches the related failure where a field is added or widened and the struct silently stops matching the interface — a change that otherwise shifts every field below it.

Write packed structs in bit order and comment the ranges, as in the corrected version above. The comment is not decoration: it is the only place the intended layout is stated in a form a reviewer can check against the interface without mentally recomputing the packing.

For the type rules this rests on see 4-state versus 2-state types; for the fixed-width array dimensions that interact with packed layout, fixed-size arrays.

Interview Questions

A packed struct lays all its fields out as one contiguous bit vector, first declared field in the most significant bits. It can be assigned to and from a logic [N:0] of the same width, connected to module ports, sliced, and used in bitwise operations. Every field must itself be a packed type — logic, bit, packed structs, enums with a packed base — so no strings, no dynamic arrays, no class handles.

An unpacked struct gives each field independent storage, like a C struct. Any type is allowed, including queues, strings and class handles, and the price is that it has no single bit-level representation: it cannot be cast to a vector or connected to a plain port.

The practical split is what the struct is for. Modelling something that exists on wires — a register, a protocol beat, a port bundle — means packed, because the bit layout is the specification. Carrying a group of values around a testbench means unpacked, because the convenience of arbitrary field types matters more than a layout nobody will look at.

Both are value types: s2 = s1 copies the contents, and modifying s2 does not affect s1. That is the main thing distinguishing a struct from a class, where assignment copies a handle.

The first declared field. In struct packed { logic [7:0] a; logic [7:0] b; } s, a occupies bits [15:8] and b occupies [7:0], so assigning 16'hABCD gives a = 8'hAB and b = 8'hCD.

The convention matches how hardware documentation is written: a register table lists bit 31 first, and a packed struct written in the same order produces the same layout. That is the whole reason the rule is MSB-first rather than the other way round.

The failure it invites is writing the typedef in the order a specification table lists the fields, which is often payload-first for readability rather than bit-order. The struct is the correct total width either way, so the cast from a captured vector is legal, and every field lands shifted. That is the Debug Lab above.

The check is one line and worth having beside any boundary typedef: cast a known value in and assert the fields come out where you expect.

Only a packed struct. Module ports require a type with a defined bit-level representation, and an unpacked struct has none — its fields have independent storage with no mandated packing.

At the boundary a packed struct port behaves exactly like a logic [N:0] of the same total width. The connection is by position and width, not by field name, so a module declaring input axi_w_t beat can be driven by any 37-bit expression. The field names are a convenience inside each module; they are not checked across the connection.

That has a consequence worth stating plainly: connecting two modules that use different typedefs of the same width compiles and runs, and silently reorders the fields. The type system does not protect the boundary, only the arithmetic.

The defence is to share one typedef from a package rather than declaring it in each module, so there is exactly one definition of the layout and both sides import it. A width assertion — $bits(axi_w_t) == 37 — catches the case where the shared typedef changes and one side was not recompiled.

Packed structs synthesise directly: they are a naming convention over a bit vector, so ctrl_reg.timeout becomes exactly the same hardware as ctrl_reg[15:8]. There is no cost and no inferred logic — the compiler resolves field access at elaboration.

Unpacked structs are synthesisable only in the restricted sense that a synthesis tool may accept one whose fields are all synthesisable types, treating it as a bundle of separate signals. Support varies, and an unpacked struct containing a queue, a string or a class handle is not synthesisable under any tool.

The reliable guidance is to treat packed as the RTL type and unpacked as the testbench type. That maps onto the reason each exists: RTL needs a layout, testbenches need convenience.

One detail worth knowing for RTL: a packed struct made of logic fields is 4-state, so it initialises to X and shows unreset state honestly. Declaring the fields bit makes it 2-state, faster to simulate, and silently zero at time zero — the same trade discussed for scalar types, and the same reason to prefer logic for anything that touches the DUT.

Almost certainly the typedef declares its fields in the wrong order — written in the order a specification table lists them rather than in wire order, most significant field first.

The reason it is hard to spot is that nothing is illegal. The struct is the correct total width, so the cast from the captured vector is width-correct and compiles; the compiler has no idea what the bits are supposed to mean. Two typedefs describing different layouts are both valid SystemVerilog.

The symptom is distinctive once you know it: fields are shifted, not garbage. A payload field picks up neighbouring fields in its top bits and loses its own bottom bits, so the value looks plausible rather than obviously wrong. A one-bit flag declared at the wrong end reads as roughly random data, which is why a LAST-type signal appearing asserted on most beats is a strong hint at this specific bug.

Diagnose by printing the raw capture beside the decoded fields on a single transaction. The bit offset between what you expect and what you get is the sum of the widths of the misplaced fields, which points straight at the typedef.

Note that a self-consistent testbench hides it entirely: if the driver and monitor share the wrong typedef they agree with each other and disagree only with the DUT.

When it is a value rather than an object — no methods, no inheritance, no randomisation, no factory involvement.

Structs are value types, so assignment copies the contents. That removes an entire class of aliasing bug: two handles to the same transaction, where modifying one silently changes the other, is impossible with a struct. For a small data carrier passed between functions, that safety is worth more than the features it lacks.

Classes earn their place the moment you need what they provide: rand fields and constraints, factory overrides so a test can substitute a derived type, do_copy/do_compare/convert2string for the UVM automation, or inheritance so a base sequence item can be extended per protocol. A UVM sequence item has to be a class, because the whole sequencer/driver mechanism is built on uvm_object.

A useful middle pattern is a class that contains a packed struct: the class supplies randomisation and factory support, and the packed field carries the exact wire layout for driving and sampling. That keeps the layout specification in one checkable place while leaving the methodology machinery to the class.

Where This Is Specified

  • IEEE 1800-2023 §7.2 — Structures. Declaration syntax, the packed qualifier, and the rule that a packed structure is represented as a contiguous set of bits with the first declared member in the most significant position.
  • IEEE 1800-2023 §7.2.1 — Packed structures. The restriction that every member of a packed structure must itself be a packed type, and the resulting ability to treat the structure as an integral value.
  • IEEE 1800-2023 §6.22 — Type compatibility. Assignment compatibility between a packed structure and an integral value of the same width, and the equivalence rules that govern struct-to-struct assignment.
  • IEEE 1800-2023 §23.3.3 — Port declarations. The requirement that a port have an integral or otherwise connectable type, which admits packed structures and excludes unpacked ones.
  • IEEE 1800-2023 §20.6 — Expression size system function. $bits, used above to assert that a boundary typedef still matches its interface width.

Best Practices and 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 caseTypeReason
Hardware register field modelstruct packedDirect assignment to/from raw register value
Protocol frame (AXI/AHB beat)struct packedPort-connectable, synthesizable, single-vector treatment
UVM transaction / scoreboard entrystruct (unpacked)Can hold dynamic arrays, strings, class handles
Function argument groupingEither, depending on fieldsPass multiple related values as one parameter
Array of transactionsUnpacked structtxn_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.

Part of SystemVerilog Fundamentals·Data Types·Lesson 11 of 53

View program

Continue learning