Skip to content

SystemVerilog · Module 2

Unions & Tagged Unions

Packed unions, overlapping bit fields, tagged union type safety, protocol packet decoding.

Module 2 · Page 2.8

When the Same Bits Mean Different Things

Protocol headers are the canonical union use case. An Ethernet frame header can be parsed as a packed struct with named fields (destination MAC, source MAC, EtherType), or the same bytes can be viewed as a raw byte array for CRC calculation. Both interpretations occupy exactly the same memory — the union gives you both views simultaneously.

In hardware, this maps directly to multi-granularity bus access. A 32-bit register can be written as a word (one 32-bit write) or read back as four individual bytes. A union with a word_t member and a byte_t [4] member expresses exactly that semantics. Both members share the same 32 bits — writing through one member immediately affects what you read through the other.

The tagged union is the safer variant. It adds an implicit type tag that tracks which member was last written. Reading a member that wasn't written last is a runtime error — the simulator enforces type safety at the cost of one extra tag field. In practice, tagged unions are used more in verification testbenches (where type safety is more important than raw performance) than in RTL (where the extra tag bits add overhead).

Three Union Variants — Packed, Unpacked, Tagged

SystemVerilog has three union forms. The most common in real projects is union packed. The tagged variant is useful in verification but rarely seen in RTL. Plain unpacked unions exist but are almost never used since they sacrifice the key advantage (overlapping storage) without gaining anything.

union packed

All members share the same contiguous bit storage. All members must have the same total width. Read through any member at any time — you see the same bits interpreted differently. Most useful for multi-granularity data access.

union (unpacked)

Members may have different sizes (the union size = largest member). No bit-level overlapping guarantee in simulation. Rarely used — if you need overlapping bits, use packed; if you need different types, use tagged.

union tagged

Type-safe variant. Simulator tracks which member was last written via a hidden tag. Reading wrong member is a runtime error. Members can have different types and sizes. Used for variant/discriminated union patterns in TB.

union packed vs struct packed

Struct: fields occupy adjacent bits — total width = sum of all. Union: fields overlap the same bits — total width = width of one member (all must be equal).

Syntax — All Three Forms

SystemVerilog — union Syntax Forms
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── PACKED UNION: all members same total width ────────────────────
typedef union packed {
  logic [31:0]  word;       // 32-bit view
  logic [1:0][15:0] half; // two 16-bit halves (packed array)
  logic [3:0][7:0]  bytes;// four 8-bit bytes  (packed array)
} word32_t;
 
word32_t   data;
data.word = 32'hAABBCCDD;
$display("word    = 0x%08h", data.word);      // AABBCCDD
$display("half[1] = 0x%04h", data.half[1]);  // AABB (upper half)
$display("half[0] = 0x%04h", data.half[0]);  // CCDD (lower half)
$display("bytes[3]= 0x%02h", data.bytes[3]); // AA (MSB byte)
$display("bytes[0]= 0x%02h", data.bytes[0]); // DD (LSB byte)
 
// ── PACKED UNION WITH STRUCT ──────────────────────────────────────
typedef struct packed {
  logic [7:0] op;
  logic [7:0] dst;
  logic [7:0] src;
  logic [7:0] imm;
} instr_fields_t;
 
typedef union packed {
  logic [31:0] raw;          // raw bits view
  instr_fields_t fields;      // named field view (same total width: 32)
} instr_t;
 
instr_t ins;
ins.raw          = 32'hA5_03_01_FF;  // write raw
ins.fields.op    = 8'hA5;             // or write via fields
 
// ── TAGGED UNION: type-safe, members can differ in type/size ──────
typedef union tagged {
  int           IntVal;
  real          RealVal;
  logic [31:0] BitsVal;
  string        StrVal;
} variant_t;
 
variant_t v;
 
// Write: use tagged assignment syntax
v = tagged IntVal 42;                    // set tag=IntVal, value=42
v = tagged RealVal 3.14;               // set tag=RealVal, value=3.14
v = tagged StrVal "hello";             // set tag=StrVal
 
// Read: use .member (valid ONLY for the tag that was last written)
$display("%s", v.StrVal);              // OK: tag is StrVal
// $display("%0d", v.IntVal);         FATAL: tag is StrVal, not IntVal
 
// Pattern match with tagged union (type-safe switch)
case (v) matches
  tagged IntVal  .i : $display("int: %0d", i);
  tagged RealVal .r : $display("real: %f", r);
  tagged StrVal  .s : $display("string: %s", s);
  default:            $display("other");
endcase
Propertyunion packedunion (unpacked)union tagged
Members share storage?Yes — exactlyLargest member's size allocatedNo — each member separate (tag tracked)
All members same width?RequiredNot requiredNot required
Read wrong member?Allowed — sees bits from last writeAllowed — implementation-definedFatal runtime error
Non-packed field typesNot allowedAllowedAllowed (any type)
SynthesizableYesPartiallyNo
Port-connectableYes (packed)NoNo

Visual — Overlapping Bits and Tagged Type Tracking

Packed Union Memory Overlap

Value stored: 32'hAABBCCDD. Every member view reads from the same 32 bits:

Bits[31:24][23:16][15:8][7:0]
RawAA BB CC DD
.word32'hAABBCCDD
.half[1]16'hAABB
.half[0]16'hCCDD
.bytes[3]8'hAA
.bytes[0]8'hDD

Tagged Union — Tag Tracks Active Member

AssignmentActive tagValid readInvalid read (fatal)
v = tagged IntVal 42IntValv.IntVal = 42v.RealVal, v.StrVal
v = tagged RealVal 3.14RealValv.RealVal = 3.14v.IntVal, v.StrVal
v = tagged StrVal "hi"StrValv.StrVal = "hi"v.IntVal, v.RealVal
UninitializedNone / undefinedNoneAny member read is fatal

Union vs Struct — Same Bits, Different Semantics

Type32-bit exampleTotal bitsMembers relationship
struct packed{ logic [15:0] A; logic [15:0] B; }32 (16+16)A occupies [31:16], B occupies [15:0] — adjacent
union packed{ logic [31:0] word; logic [1:0][15:0] halves; }32 (both)word and halves both occupy the same 32 bits

Code Examples — Multi-Granularity Access to Protocol Decoding

Example 1 — Beginner: Multi-Granularity Register Access

Example 1 — Packed Union: Word/Byte Access
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_packed_union;
 
  typedef union packed {
    logic [31:0]        word;   // full 32-bit access
    logic [1:0][15:0]   half;   // two 16-bit halves
    logic [3:0][7:0]    bytes;  // four bytes
  } bus32_t;
 
  bus32_t bus;
 
  initial begin
 
    // ── Write as word, read back as bytes ─────────────────────────
    bus.word = 32'hAABBCCDD;
    $display("Word = 0x%08h", bus.word);
    foreach (bus.bytes[i])
      $display("  byte[%0d] = 0x%02h", i, bus.bytes[i]);
    // byte[3]=AA byte[2]=BB byte[1]=CC byte[0]=DD
 
    // ── Write a byte, read back as word ──────────────────────────
    bus.bytes[3] = 8'hFF;   // only modify MSB byte
    $display("After byte[3]=FF: word = 0x%08h", bus.word);
    // FFBBCCDD — only byte[3] changed
 
    // ── Swap upper and lower halves ───────────────────────────────
    bus.word = 32'hAABBCCDD;
    logic [15:0] tmp = bus.half[1];  // save upper: AABB
    bus.half[1] = bus.half[0];        // upper = lower: CCDD
    bus.half[0] = tmp;                 // lower = saved: AABB
    $display("Swapped halves: 0x%08h", bus.word);   // CCDDAABB
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Word = 0xAABBCCDD
  byte[3] = 0xAA
  byte[2] = 0xBB
  byte[1] = 0xCC
  byte[0] = 0xDD
After byte[3]=FF: word = 0xFFBBCCDD
Swapped halves: 0xCCDDAABB

Example 2 — Intermediate: Protocol Packet Decoder Using Union

Example 2 — Union for Packet Format Decoding
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A command packet: 32 bits, two possible layouts based on opcode[31:28]
// If opcode[31:28] == 4'hA: ALU format  {op[31:28], dst[27:24], src1[23:20], src2[19:16], imm[15:0]}
// If opcode[31:28] == 4'hB: MEM format  {op[31:28], rsvd[27:25], base[24:8], offset[7:0]}
 
typedef struct packed {
  logic [3:0]  op;
  logic [3:0]  dst;
  logic [3:0]  src1;
  logic [3:0]  src2;
  logic [15:0] imm;
} alu_fmt_t;
 
typedef struct packed {
  logic [3:0]  op;
  logic [2:0]  rsvd;
  logic [16:0] base;
  logic [7:0]  offset;
} mem_fmt_t;
 
typedef union packed {
  alu_fmt_t   alu;
  mem_fmt_t   mem;
  logic [31:0] raw;
} cmd_t;   // all members = 32 bits
 
module tb_packet_decode;
 
  function automatic void decode(logic [31:0] pkt_bits);
    cmd_t cmd;
    cmd.raw = pkt_bits;   // load raw bits
 
    case (cmd.alu.op)     // op field is same position in both formats
      4'hA: $display("ALU: dst=%0h src1=%0h src2=%0h imm=%0h",
              cmd.alu.dst, cmd.alu.src1, cmd.alu.src2, cmd.alu.imm);
      4'hB: $display("MEM: base=0x%05h offset=0x%02h",
              cmd.mem.base, cmd.mem.offset);
      default: $display("UNKNOWN op=0x%h", cmd.alu.op);
    endcase
  endfunction
 
  initial begin
    decode(32'hA3_1_2_0005);   // ALU: op=A dst=3 src1=1 src2=2 imm=5
    decode(32'hB0_10000_42);    // MEM: op=B base=... offset=42h
    $finish;
  end
 
endmodule

Example 3 — Verification: Tagged Union as Type-Safe Variant

Example 3 — Tagged Union for Polymorphic Event Log
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_tagged_union;
 
  // Tagged union: one event log that holds different event types
  typedef union tagged {
    logic [31:0] WriteEvent;   // holds write data
    logic [31:0] ReadEvent;    // holds read data
    logic [7:0]  ErrorCode;    // holds error code
  } sim_event_t;
 
  sim_event_t log [$];
 
  function automatic string format_event(input sim_event_t e);
    case (e) matches
      tagged WriteEvent .d : return $sformatf("WR  data=0x%08h", d);
      tagged ReadEvent  .d : return $sformatf("RD  data=0x%08h", d);
      tagged ErrorCode  .c : return $sformatf("ERR code=0x%02h",  c);
    endcase
    return "UNKNOWN";
  endfunction
 
  initial begin
    sim_event_t e;
 
    // Log a write, a read, and an error
    e = tagged WriteEvent 32'hCAFE_BABE; log.push_back(e);
    e = tagged ReadEvent  32'h1234_5678; log.push_back(e);
    e = tagged ErrorCode  8'hFF;         log.push_back(e);
 
    $display("Event log (%0d entries):", log.size());
    foreach (log[i])
      $display("  [%0d] %s", i, format_event(log[i]));
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Event log (3 entries):
  [0] WR  data=0xCAFEBABE
  [1] RD  data=0x12345678
  [2] ERR code=0xFF

Example 4 — Corner Case: RTL Register with Packed Union View

Example 4 — Union in RTL Register Model
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// STATUS register — can be read as whole word or as individual fields
typedef struct packed {
  logic       err_flag;   // [31]
  logic [6:0]  rsvd;       // [30:24]
  logic [7:0]  err_code;   // [23:16]
  logic [15:0] count;      // [15:0]
} status_fields_t;
 
typedef union packed {
  status_fields_t  fields;
  logic [31:0]     raw;
} status_reg_t;
 
module tb_union_reg;
  status_reg_t sr;
 
  initial begin
    // Scoreboard: read raw register, decode fields immediately
    sr.raw = 32'h8005_0064;   // simulated register readback
 
    $display("err_flag = %0b", sr.fields.err_flag);  // 1 (bit 31 = 1)
    $display("err_code = 0x%02h", sr.fields.err_code); // 05
    $display("count    = %0d",    sr.fields.count);     // 100 (0x0064)
 
    // Clear error: write raw, preserving other bits
    sr.fields.err_flag = 1'b0;
    sr.fields.err_code = 8'h00;
    $display("After clear: raw = 0x%08h", sr.raw);  // 0x0000_0064
 
    $finish;
  end
endmodule

Simulation Behavior — Storage, Safety, and Synthesis

Packed Union Storage Model

In simulation, a packed union maintains a single block of N bits (where N = the common width of all members). Every member read and write accesses the same underlying bit storage. There is no "last written member" tracking — you can write through .word and immediately read through .bytes[0] without any error. This is the expected behavior: the union expresses "the same bits, seen differently."

Tagged Union — The Tag Overhead

Tagged unions add a hidden tag field managed by the simulator. The tag records which member was last written. Any attempt to read a member whose tag does not match causes a fatal simulation error — "union tag mismatch." This makes tagged unions safe but adds memory overhead (the tag field) and prevents them from being used in synthesizable RTL. They belong exclusively in testbench code where type safety matters more than area.

ScenarioPacked unionTagged union
Write member A, read member BOK — B sees A's bits interpreted differentlyFatal: tag mismatch
Write member A, read member AOKOK
Uninitialized readReturns X (logic type)Fatal: no active tag
Synthesis supportYes — same as packed vectorNo — tag has no hardware equivalent
Port connectionYes (packed)No
Dynamic member sizesNo — all must be same widthYes

Where Unions Appear in Real Verification

Verification Patterns Using union
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 1. REGISTER SHADOW: raw readback + field decode ────────────────
typedef union packed { ctrl_fields_t fields; logic [31:0] raw; } ctrl_reg_t;
ctrl_reg_t shadow;
shadow.raw = bus_readback;           // assign raw readback
$display("timeout=%0d", shadow.fields.timeout);  // immediate field access
 
// ── 2. OPCODE DISAMBIGUATION: packet format decoder ────────────────
cmd_t pkt;
pkt.raw = received_bits;
case (pkt.raw[31:28])
  4'hA: $display("ALU: dst=%0h", pkt.alu.dst);
  4'hB: $display("MEM: offset=%0h", pkt.mem.offset);
endcase
 
// ── 3. TAGGED UNION: type-safe event dispatch ─────────────────────
sim_event_t evt;
evt = tagged WriteEvent data;
// later, safely extract:
if (evt matches tagged WriteEvent .d)
  scoreboard.record_write(d);
 
// ── 4. BYTE LANE STROBE MODELING ──────────────────────────────────
typedef union packed {
  logic [31:0]       word;
  logic [3:0][7:0]   lanes;
} wdata_t;
 
wdata_t wd;
logic [3:0] strb;
 
wd.word = 32'hAABBCCDD;
// Apply byte strobes — only write enabled bytes
foreach (wd.lanes[i])
  if (!strb[i]) wd.lanes[i] = mem_contents[i];  // preserve masked bytes

Bugs Engineers Hit With Unions

Bug 1 — Packed Union Members with Mismatched Widths

Bug 1 — Mismatched Member Widths
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// BUGGY: members have different widths
typedef union packed {
  logic [31:0] word;    // 32 bits
  logic [15:0] half;    // 16 bits — COMPILE ERROR: must match
} bad_union_t;
 
// FIXED option 1: all members same width
typedef union packed {
  logic [31:0]        word;   // 32
  logic [1:0][15:0]   halves; // 32 (2×16)
  logic [3:0][7:0]    bytes;  // 32 (4×8)
} good_union_t;
 
// FIXED option 2: use tagged union if members genuinely differ in size
typedef union tagged {
  logic [31:0] word;
  logic [15:0] half;   // different size is OK in tagged union
} tagged_ok_t;

Bug 2 — Reading Wrong Member in Packed Union (Intentional vs Accidental)

Bug 2 — Understanding Cross-Member Read in Packed Union
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
typedef union packed {
  logic [31:0] word;
  logic [3:0][7:0] bytes;
} u_t;
 
u_t u;
u.word = 32'h11223344;
 
// INTENTIONAL: read via different member — this is THE POINT of packed union
$display("bytes[0] = 0x%02h", u.bytes[0]);  // 0x44 — correct and intended
 
// ACCIDENTAL: using wrong interpretation
// Example: struct member field order bug in union — accidental struct selection
typedef struct packed { logic [15:0] A; logic [15:0] B; } ab_t;
typedef struct packed { logic [15:0] X; logic [15:0] Y; } xy_t;
 
typedef union packed { ab_t ab; xy_t xy; } combo_t;
combo_t c;
c.ab.A = 16'hAAAA; c.ab.B = 16'hBBBB;
$display("c.xy.X = 0x%04h", c.xy.X);  // AAAA — X and A are same bits
$display("c.xy.Y = 0x%04h", c.xy.Y);  // BBBB — Y and B are same bits
// No bug here — this is correct union behavior. Just be aware it's intentional.

Bug 3 — Tagged Union: Read Before Write (Fatal)

Bug 3 — Tagged Union Access Without Setting Tag
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
typedef union tagged { int I; real R; } num_t;
num_t v;
 
// BUGGY: reading before writing — no active tag
$display("%0d", v.I);    // FATAL: "read from uninitialized tagged union"
 
// BUGGY: reading wrong member
v = tagged I 42;
$display("%f", v.R);    // FATAL: tag is I, not R — "union tag mismatch"
 
// CORRECT: always write before read, match member to tag
v = tagged R 3.14;
$display("%f", v.R);    // OK: 3.140000
 
// CORRECT: use case-matches to safely dispatch
case (v) matches
  tagged I .i : $display("int: %0d", i);
  tagged R .r : $display("real: %f", r);  // executes this branch
endcase

Interview Questions

Beginner Level

Q1: What is the key difference between a union packed and a struct packed? In a struct packed, fields occupy adjacent, non-overlapping bit positions — total size equals the sum of all fields. In a union packed, all members share the same bit storage — total size equals the (mandatory equal) width of any one member. Writing through one member changes what you read through any other. Q2: What constraint applies to the members of a packed union? All members of a packed union must have exactly the same total bit width. If any member has a different width, the compiler reports an error. This constraint exists because a packed union represents a single block of N bits viewed in multiple ways — every view must cover exactly those N bits. If sizes differed, the bit mapping would be ambiguous.

Intermediate Level

Q3: What is a tagged union and what problem does it solve compared to a regular union? A tagged union adds a hidden type tag that the simulator tracks. It records which member was last written. Reading a member whose tag doesn't match causes a fatal runtime error. This solves the type safety problem of regular unions — in a regular packed union, you can accidentally read via the wrong member and get nonsense bits with no error. Tagged unions enforce that you only read the data you actually wrote. Members can have different types and sizes (unlike packed unions). The cost: no synthesis support and a small memory overhead for the tag.

Experienced Engineer Level

Q4: A scoreboard captures a 32-bit bus value and needs to decode it as either an instruction format or a data format based on bit [31]. How would you model this cleanly using a union? Define two packed structs for the two formats (both totaling 32 bits), then wrap them in a packed union with a logic [31:0] raw member: typedef union packed { instr_fmt_t instr; data_fmt_t data; logic [31:0] raw; } bus_word_t; Capture: bw.raw = captured_bits. Then dispatch: if (bw.raw[31]) decode_as_instruction(bw.instr); else decode_as_data(bw.data);. No separate bit-slice extraction or manual repacking — the union gives you all views simultaneously at zero cost.

Best Practices & Coding Guidelines

Always include a raw member

Include logic [N:0] raw in every packed union. This gives you a clean way to load raw bits from a bus or compare the entire value without field-by-field operations.

Verify all member widths match

Before declaring a packed union, manually count the total bits for each member. A struct member's total = sum of its field widths. Mismatch is a compile error, but catching it conceptually first saves time.

Tagged union for variant TB types

When a testbench needs to store and dispatch different event or transaction types safely, use a tagged union. The runtime tag-mismatch errors catch programming mistakes early.

case-matches for tagged union dispatch

Always use case (v) matches for dispatching on tagged unions. Avoid direct member access (v.member) without first verifying the tag — use matches which is both the check and the extract.

Use caseUnion typeNotes
Multi-granularity register (word/byte)Packed unionInclude raw member; all views same width
Protocol packet format disambiguationPacked union of structs + rawCapture raw, dispatch via field[31:28] etc.
Variant event/transaction log in TBTagged unionType-safe dispatch with case-matches
Different-width views of same dataTagged unionPacked union requires equal widths; tagged allows different
Synthesizable multi-view registerPacked unionTagged union is not synthesizable

Summary

Unions solve a specific problem: the same bits, interpreted differently. Packed unions are the hardware-friendly form — equal-width members, all sharing the same storage, synthesizable, port-connectable. The union-of-struct pattern (raw + named views) is the most practical application: load raw bits from a bus, decode fields through struct members. Tagged unions are the type-safe TB variant — different member sizes allowed, runtime tag enforcement, ideal for polymorphic event logs and variant types where type safety matters more than area.

  • Packed union: all members must be the same total width. The union stores one N-bit block; all members are different views of it.
  • Include a raw member in every packed union. It gives you a clean single-assignment entry point and a full-value comparison path.
  • Regular packed union has no type safety. Cross-member reads are legal and intentional — the point is multiple views of the same bits.
  • Tagged union tracks the active member. Cross-member reads are fatal. Use case matches for safe dispatch.
  • Tagged union is TB-only. Not synthesizable. Use packed union for RTL; tagged union for verification variant types.