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.
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.
| Field | Bits | Value for a WRITE to 0x123 |
|---|---|---|
kind | 15:12 | 4'h2 |
addr | 11:0 | 12'h123 |
| whole header | 15:0 | 16'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.
width=16 header=2123 kind=5 addr=a10 raw after writing addr=5fff after a 20-bit assignment: kind=7 addr=123Check yourself
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
- Header field decoder Implement · Easy