Structs and unions

Describe a packet header as a packed struct whose field order matches the wire, and read the same bits two ways with a union.

Lesson
1.3
Time
20 min
Runs on
Your browser
Check
5 questions

A testbench sends a WRITE to address 0x123, and the DUT performs a READ from 0x232 instead. The header spec puts a 4-bit kind in bits 15:12 and a 12-bit addr below it. The testbench struct declared addr first, and in a packed struct the first field is the most significant. The two fields traded places on the wire, and no compiler warning said so.

Header struct declared with addr first
012tb kindWRITEREADWRITEtb addr1234560ffhdr bus [15:0]123245610ff2DUT kind = bus[15:12]1 (READ)4 (?)0 (IDLE)DUT addr = bus[11:0]232561ff2WRITE 0x123 becomes READ 0x232

Encoding: IDLE = 0, READ = 1, WRITE = 2. Each header is shifted by one nibble.

A struct groups named fields

A struct bundles related fields under one name, so instead of slicing hdr[11:0] all over the testbench you write hdr.addr.

With typedef, the struct becomes a reusable type: typedef struct packed { logic [3:0] kind; logic [11:0] addr; } hdr_t;. You then declare hdr_t h; wherever the testbench needs a header.

A packed struct is stored as one vector whose width is the sum of its field widths, here 4 + 12 = 16 bits. You can assign it to a logic [15:0] bus, compare it with == and print it in hex.

The first field is the most significant

In a packed struct, the first declared field occupies the top bits, and each later field sits directly below the one before it. The last field always ends at bit 0.

FieldBitsValue for a WRITE to 0x123
kind15:124'h2
addr11:012'h123
whole header15:016'h2123

hdr_t with kind declared first.

Reverse the declaration and the same field values produce 16'h1232, yet both versions compile and look reasonable. Only the spec decides which one is right, so copy the field order from its bit table, top bit first.

Fields in "reading" order

typedef struct packed {
  logic [11:0] addr;   // bits 15:4
  logic [3:0]  kind;   // bits 3:0
} hdr_t;

A WRITE to 0x123 goes out as 16'h1232, which the DUT reads as a READ from 0x232.

Fields in spec order, top bit first

typedef struct packed {
  logic [3:0]  kind;   // bits 15:12
  logic [11:0] addr;   // bits 11:0
} hdr_t;

A WRITE to 0x123 goes out as 16'h2123, which matches the spec.

Packed and unpacked structs

Every field of a packed struct must itself be packed: logic, bit, packed arrays, enums or other packed structs. A string, a real number or an unpacked array cannot go inside one.

A struct declared without packed is unpacked, which means it can hold fields of any type but is a record rather than a vector. Use unpacked structs for testbench bookkeeping, and packed structs for anything that travels on a wire.

A union is one set of bits with several names

A union gives several names to the same storage. In a packed union, every member must have the same width, so each member is a different view of the same bits.

typedef union packed { hdr_t f; logic [15:0] raw; } hdr_u; lets you write the bus into u.raw and then read u.f.kind and u.f.addr without any slicing.

Writing one member changes what every other member reads. After u.raw = 16'h5A10, for example, u.f.kind reads as 5 and u.f.addr reads as 12'hA10.

Unions suit fields that the spec defines in more than one way. A 32-bit word might be a header in one bus phase and raw data in the next, and the union gives each reading its own field names.

A union does not record which member was written last, so nothing stops you from reading bits as a header when they were meant as something else. Keep union reads close to the code that captures the bus.

Structs inside structs

A packed struct can contain another packed struct. A packet made of a header and a data word is typedef struct packed { hdr_t hdr; logic [31:0] data; } pkt_t;, which is 48 bits wide.

The same ordering rule applies at every level: hdr is declared first, so it fills bits 47 to 32, and data fills bits 31 to 0. Inside hdr, the kind field is still the top nibble.

You reach a nested field with a chain of names, such as p.hdr.addr. Because the whole packet is one vector, p == expected compares all 48 bits at once, and the error message can then name the field that differs.

Check the width

Assigning a wider value to a packed struct keeps only the low bits and reports no error. For example, assigning 20'hB7123 to a 16-bit hdr_t quietly stores 16'h7123.

$bits(hdr_t) returns the width of the struct, so compare it with the bus width once at the start of the test. A field added later then cannot shift the layout without causing a failure.

Try it

Setting the two fields builds 16'h2123, with kind on top. Writing u.raw and reading u.f shows the union sharing one set of bits, and writing u.f.addr changes u.raw.

The last line drops the top 4 bits of the 20-bit value without any message. Swap the two fields in hdr_t, predict every line, then run it.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: width=16 header=2123 kind=5 addr=a10 raw after writing addr=5fff after a 20-bit assignment: kind=7 addr=123

Check yourself

Predict the outputPredict the exact output.
module tb;
  typedef struct packed {
    logic [1:0] op;
    logic [5:0] len;
  } cmd_t;
  cmd_t c;
  initial begin
    c.op  = 2'b10;
    c.len = 6'd5;
    $display("%h", c);
  end
endmodule
Conceptu is a packed union of hdr_t f (kind in bits 15:12, addr in bits 11:0) and logic [15:0] raw. After u.raw = 16'h0000; u.f.kind = 4'hF;, what is u.raw?
ConceptYou need a testbench record that holds a string label and a 12-bit address. Which declaration is legal?
Read the waveformThe header bus uses hdr_t with kind in bits 15:12 and addr in bits 11:0, where WRITE is 2. In which cycle does the bus carry a WRITE to address 0x123?
Header bus
0123hdr_validhdr [15:0]1232212331232321

One header per cycle.

Spot the bugEach option is the body of function automatic logic [15:0] pack(logic [3:0] kind, logic [11:0] addr), for a header with kind in bits 15:12. Which option builds the wrong header?

Remember

  • A packed struct is one vector; its first declared field occupies the most significant bits.
  • Copy the field order from the spec, top bit first; a reversed struct compiles without any warning.
  • Packed structs hold only packed fields; use an unpacked struct for strings and other testbench data.
  • Members of a packed union share the same bits, so writing one member changes what the others read.
  • Assigning a wider value to a packed struct silently drops the top bits; check the width with $bits.

Practice