Module 9: SystemVerilog for Design & Verification ยท 9.1 SystemVerilog Design Enhancements

typedef, struct, union and enum

The last two articles were about signals and blocks. This one is about data.

Every FSM in Section 5.6 used localparam to give a bit pattern a name, localparam [1:0] IDLE = 2'b00. That works, but the name and the encoding are two separate ideas bundled into one declaration, and nothing stops a typo (2'b01 where you meant 2'b10) from silently attaching the wrong pattern to the right-sounding name. enum separates the two, and struct does the same job for groups of related signals that classic Verilog can only model as separate, independently declared wires with no declared relationship.

enum: names that are enforced

typedef enum logic [1:0] {
    IDLE    = 2'b00,
    ACTIVE  = 2'b01,
    DONE    = 2'b10,
    ERR_ST  = 2'b11
} state_t;

state_t state, next_state;

Compare with the localparam pattern from Section 5.6. The encoding is still explicit, you can still specify exact bit patterns including one-hot or Gray encodings, but state and next_state are declared with the type state_t rather than a bare reg [1:0]. That buys something classic Verilog cannot: assigning an enum variable from a plain integer or bit vector is a type error in SystemVerilog, so state <= 2'b11 fails to compile and only the named members are accepted. A typo in a state assignment is caught at compile time rather than as a functional mismatch much later.

The encoding is still yours

A common misconception: enum does not force you to give up control over the bit encoding, the way a software enum might. Every encoding decision from Section 5.6, binary, one-hot, Gray, and the safe-FSM default for illegal states, still applies:

typedef enum logic [3:0] {
    S_RED    = 4'b0001,
    S_GREEN  = 4'b0010,
    S_YELLOW = 4'b0100,
    S_PED    = 4'b1000
} traffic_state_t;   // explicitly one-hot, as in Section 5.6

always_comb begin
    next_state = state;   // safe default, same discipline as before
    unique case (state)
        S_RED:    if (timer_done) next_state = S_GREEN;
        S_GREEN:  if (timer_done) next_state = S_YELLOW;
        S_YELLOW: if (timer_done) next_state = S_PED;
        S_PED:    if (timer_done) next_state = S_RED;
        default:  next_state = S_RED;   // safe-FSM recovery, as in Section 5.6
    endcase
end

unique case is the SystemVerilog replacement for the full_case and parallel_case pragmas Section 5.3 told you to avoid. It makes the same claim, exactly one branch matches, but the simulator checks it and warns when it is violated. That is the honest replacement Section 5.3 promised.

Classic Verilog has no way to say “these three signals belong together” beyond naming convention. struct makes the relationship part of the type:

typedef struct packed {
    logic [7:0] opcode;
    logic [7:0] operand_a;
    logic [7:0] operand_b;
    logic       valid;
} instruction_t;

instruction_t current_instr;

// Access individual fields by name: self-documenting, no manual bit-slicing
assign alu_op = current_instr.opcode;
assign valid_now = current_instr.valid;

The packed keyword matters for hardware. It guarantees the fields are laid out as one contiguous bit vector in a fixed order, which is what you need to pass the bundle through a wide port or store it in a register. An unpacked struct is more of a software-style organisation tool without that guarantee, and is used far more in verification code (Section 9.2) than in synthesizable RTL.

Passing the struct through a port replaces four separate, easy-to-miscount connections with one:

module decode_stage (
    input  instruction_t instr,   // one port, four fields with a declared relationship
    output logic [7:0]   alu_result
);
    // instr.opcode, instr.operand_a, instr.operand_b, instr.valid
endmodule

union: the same bits, two views

A struct gives each field its own bits. A union does the opposite: every member shares the same bits, viewed through different types, which is occasionally what a hardware register needs:

typedef union packed {
    logic [31:0]        raw_word;
    struct packed {
        logic [15:0] high_half;
        logic [15:0] low_half;
    } halves;
} status_reg_t;

status_reg_t status;
assign status.raw_word = 32'hDEAD_BEEF;
// status.halves.high_half now reads 16'hDEAD, status.halves.low_half reads 16'hBEEF:
// same 32 bits, no copying, a different named view onto the same storage

This is a narrower tool than struct. Reach for it when you need to treat the same bits as either one wide value or several named sub-fields, without maintaining two separate signals you would have to keep in step yourself.

typedef: what makes these reusable

typedef is what let state_t and instruction_t become named, reusable types rather than one-off anonymous declarations. Defined once, usually in a shared package mirroring the common/ directory convention from Section 8.1, the same instruction_t can be used in every module that passes instructions around, so all of them agree on the same field layout in the same order. Processor and bus RTL leans on this constantly: an instruction format, a bus transaction and a packet header are all naturally struct-shaped, and defining them once removes a whole class of integration bug where two modules’ hand-written bit slicing of “the same” signal quietly disagrees about a field boundary.

Practice Exercise

Convert this state declaration to a SystemVerilog enum, preserving the one-hot encoding.

localparam [2:0] S_IDLE = 3'b001, S_LOAD = 3'b010, S_DONE = 3'b100;
reg [2:0] state, next_state;
Solution
typedef enum logic [2:0] {
    S_IDLE = 3'b001,
    S_LOAD = 3'b010,
    S_DONE = 3'b100
} state_t;

state_t state, next_state;

The encoding is preserved exactly, still one-hot, still the same three patterns, but state and next_state are now typed as state_t rather than a bare reg [2:0], so an assignment of any value outside the three named members is a type error, which the localparam version had no way to enforce.

enum separates a state’s name from its encoding while leaving the encoding under your control, and an enum-typed signal can be checked against its declared set of legal values. struct gives related signals a declared relationship, with packed structs mapping onto contiguous hardware bit vectors. typedef makes both reusable across a project. In the next article we extend the same “declare the relationship” idea from data types to entire multi-signal module interfaces.

โ† always_ff, always_comb and always_latch Interfaces and Modports โ†’