Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations35
    • SystemVerilog Data Types
      • 4-State vs 2-State Types
      • Nets vs Variables
      • Integer Types & Casting
      • Enums, Structs & Unions
      • X/Z Propagation & Pessimism
      • Strings, Reals & Special Types
    • Arrays: Packed, Dynamic, Queues & Associative
    • Procedural Code & Scheduling
    • Operators, Expressions & Width Rules
    • Packages, Typedefs & Compilation
    • Interfaces, Modports & Clocking Blocks
  • Object-Oriented SystemVerilog for Verification35
  • Constraint Randomization — Interview-Critical Deep Dive66
  • SystemVerilog Assertions — Interview-Critical Deep Dive63
  • Functional Coverage in SystemVerilog28
  • Testbench Architecture in SystemVerilog38
  • Advanced Language & Integration30
  • Senior & Interview Preparation34
  • Principal-Level Verification28
Loading lesson

‹SystemVerilog Data Types
  1. Topics
  2. SystemVerilog
  3. SystemVerilog Language Foundations
  4. SystemVerilog Data Types
  5. Enums, Structs & Unions

Part 1 · Language Foundations · Intermediate

Enums, Structs & Unions

Enums with base types and methods, packed structs and unions, and tagged-union limits.

On this page

    Why this matters

    A state machine printed state 2 and nobody could say which state that was. Named values, and headers with named fields instead of bit slices, turn raw bits into something a log can explain.

    After this lesson you can: declare an enum with a fixed base type and step through it, lay out a packed struct and convert it to and from a vector, and use a packed union safely.

    Enums with real base types

    An enum is a variable of some base type (default int, 2-state and 32-bit) restricted to named values. For RTL state machines always pin the base type, as in enum logic [1:0], which makes a 4-state register exactly as wide as synthesis needs.

    Enums are strongly typed in one direction: assigning an enum to an int is free, but assigning an int to an enum needs a cast. A static cast such as state_e'(x) is unchecked and accepts any bit pattern, while $cast checks that the value is a named member. That asymmetry guards against absorbing illegal state encodings.

    Method

    Returns

    first() / last()

    First / last value in declaration order

    next() / prev()

    Neighbor in declaration order, wrapping at the ends

    num()

    Count of named values

    name()

    Value name as a string, for self-describing prints

    The step methods follow declaration order and wrap around, so they never add 1 to the encoding. Monitors print st.name() instead of raw bits, and checkers iterate every legal value without hardcoding a list.

    Packed structs are shaped vectors

    A packed struct lays its members out contiguously as one vector, with the first-declared field at the most-significant bits. Assign it to and from logic [N-1:0] freely, which makes packed structs the standard way to name bus payloads and packet headers.

    An unpacked struct gives no layout guarantee, so it can never be treated as a vector. Its members can be any type, which suits testbench-side bookkeeping records. Packed-versus-unpacked layout detail lives in the arrays topic.

    A packed struct is a vector with names: the first field declared sits at the top.

    The streaming operator can reinterpret the same bits as bytes: {>> byte {h}} gives the cmd byte first, and {<< byte {h}} reverses the byte order. Shifts, Concatenation & Streaming runs both.

    Unions share storage

    A packed union overlays its members on the same storage, so all members share one packed width. The classic use is one register viewed two ways: as a flat vector for the bus and as a field struct for the protocol layer.

    Nothing records which view is currently valid, so reading the wrong member is silent garbage. Most production code pairs a packed union with an explicit discriminant field instead of relying on the type system.

    A tagged union adds a hidden tag that the simulator checks, so reading the wrong member is an error instead of silent garbage. Neither free simulator supports tagged unions, which is one more reason most code uses a packed union plus an explicit field that says which view is valid.

    Testbench

    Predict first The enum below declares IDLE, REQ, GRANT, DONE in that order. What does next() return from DONE?

    IDLE. The step methods follow declaration order and wrap around at the ends.

    Enum iteration, wrapping, and a checked int-to-enum cast, plus the packed struct round trip through a plain vector.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    typedef enum logic [1:0] {IDLE = 2'b00, REQ = 2'b01,
                              GRANT = 2'b10, DONE = 2'b11} state_e;
    
    module tb_enum;
      state_e st;
      int raw;
      int errors = 0;
    
      initial begin
        st = st.first();
        if (st !== IDLE) errors++;
        if (st.num() != 4) errors++;
        if (st.name() != "IDLE") errors++;
        st = st.next();                        // declaration order, not encoding + 1
        if (st !== REQ) errors++;
        st = DONE;
        st = st.next();                        // wraps past the last value
        if (st !== IDLE) errors++;
        st = st.prev();                        // wraps back the other way
        if (st !== DONE) errors++;
        raw = 2;
        if (!$cast(st, raw)) errors++;         // 2 is a legal encoding
        if (st !== GRANT) errors++;
        raw = 4;
        if ($cast(st, raw)) errors++;          // 4 fits no 2-bit encoding
        if (errors != 0) $fatal(1, "FAIL: enum checks saw %0d unexpected results", errors);
        $display("PASS: enum walked declaration order, wrapped both ends, checked cast mapped 2 to GRANT");
        $finish;
      end
    endmodule
    Terminal
    PASS: enum walked declaration order, wrapped both ends, checked cast mapped 2 to GRANT
    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    typedef struct packed {
      logic [7:0]  cmd;
      logic [23:0] addr;
    } hdr_t;
    
    module tb_struct;
      hdr_t h;
      logic [31:0] w;
      int errors = 0;
    
      initial begin
        h.cmd = 8'hA5;                         // first field lands at the MSB
        h.addr = 24'h1000;
        w = h;                                 // struct to vector: free
        if (w !== 32'hA5001000) errors++;
        h.addr = 24'hBEEF;                     // field update flows to the vector
        w = h;
        if (w !== 32'hA500BEEF) errors++;
        h = w;                                 // vector to struct: names the bits
        if (h.cmd !== 8'hA5) errors++;
        if (h.addr !== 24'hBEEF) errors++;
        if (errors != 0) $fatal(1, "FAIL: struct checks saw %0d unexpected results", errors);
        $display("PASS: packed struct matched the bus vector both ways with cmd at the top");
        $finish;
      end
    endmodule
    Terminal
    PASS: packed struct matched the bus vector both ways with cmd at the top

    Common pitfall

    The default enum base type is int: 2-state and 32 bits wide, so an unreset FSM fakes a legal state instead of showing X. Pin enum logic [N:0] in RTL and never assume next() adds 1 to the encoding.

    Interview

    “Why are tagged unions safer than plain unions, and why do teams still use plain ones?” The tag makes a wrong-member read a runtime error instead of silent garbage, but limited tool support keeps most code on packed unions with an explicit discriminant.

    Check yourself An enum declares GRANT = 2'b10 second and DONE = 2'b11 third. What does next() return from GRANT?

    DONE. The step methods follow declaration order and wrap at the ends, independent of the encoded values.

    Check yourself A packed struct declares cmd first and addr second for a 32-bit header. Which field owns bit 31?

    cmd. The first-declared field takes the most-significant bits, the opposite of many C bitfield habits.

    Project step

    Describe the PacketFIFO header byte as a packed struct of kind_e kind, dest and len, with kind_e an enum of DATA, CTRL and ERR. Check that the Part 0 header (DATA, dest 2, len 2) packs to 8'h22, and that the byte 8'h93 unpacks to ERR, dest 1, len 3.

    One way to write it:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    typedef enum logic [1:0] {DATA = 2'd0, CTRL = 2'd1, ERR = 2'd2} kind_e;
    
    typedef struct packed {
      kind_e      kind;   // header[7:6]
      logic [1:0] dest;   // header[5:4]
      logic [3:0] len;    // header[3:0]
    } pkt_hdr_t;
    
    module tb_pkt_hdr;
      pkt_hdr_t   h;
      kind_e      k;
      logic [7:0] b;
      int errors = 0;
      initial begin
        h.kind = DATA; h.dest = 2'd2; h.len = 4'd2;
        b = h;                                   // struct to byte
        if (b !== 8'h22) errors++;
        b = 8'h93;                               // 10_01_0011
        h = b;                                   // byte to struct
        if (h.kind !== ERR || h.dest !== 2'd1 || h.len !== 4'd3) errors++;
        k = h.kind;                              // name() on a plain enum variable
        $display("header %h: kind=%s dest=%0d len=%0d", b, k.name(), h.dest, h.len);
        if (errors == 0) $display("PASS: header packs to 22 and 93 unpacks to ERR, dest 1, len 3");
        else $fatal(1, "FAIL: %0d header checks failed", errors);
        $finish;
      end
    endmodule
    Terminal
    header 93: kind=ERR dest=1 len=3
    PASS: header packs to 22 and 93 unpacks to ERR, dest 1, len 3

    Remember

    • Pin the enum base type in RTL (enum logic [1:0]); the default is a 2-state int.

    • next() and prev() follow declaration order and wrap; $cast checks an int against the enum.

    • In a packed struct the first field declared takes the top bits.

    Rule of thumb: a packed struct is a vector with names: anything a vector can do, it can do.

    How this was checked

    Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. Icarus 13 cannot run the enum bench (it has no $cast); the struct and PacketFIFO header benches run on both.

    Tagged unions are described but not run: neither free simulator supports them.

    The PacketFIFO header bench prints the enum through a plain kind_e variable because Icarus 13 cannot call name() on a struct member directly.

    Related topics

    • Next: Strings, Reals & Special Types

    Previous

    10 / 363

    Next: X/Z Propagation & Pessimism

    Up next

    Up nextX/Z Propagation & Pessimism

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • 4-State vs 2-State Types
    • Nets vs Variables
    • Strings, Reals & Special Types