Skip to content

SystemVerilog · Module 16

Package-Level Parameters & Types

SystemVerilog package-level parameters and typedefs — the canonical pattern for shared protocol constants and transaction types. parameter vs localparam (knobs vs derivations), every typedef family (enum, packed/unpacked struct, packed union, type alias), the $bits derivation idiom, the no-parameterised-typedef rule with its parameterised-class workaround, and the SPI-package pattern every production VIP follows.

Module 16 · Page 16.3

Packages are the canonical home for shared constants and type definitions in SystemVerilog. Understanding how parameter differs from localparam inside a package, how derived constants propagate via $bits(), and how to build every form of typedef (enum, packed struct, unpacked struct, union, alias) correctly is the difference between a scalable design and one where a single width change breaks twenty files. This lesson covers each construct, the order-of-declaration rule that bites every beginner, the packed-vs-unpacked distinction that determines whether a type can appear on a hardware port, the $bits() idiom that lets derived widths cascade automatically, and the SV-doesn't-allow-parameterised-typedef rule together with its parameterised-class workaround.

1. Engineering Problem — Why Packages Own the Constants and Types

Before packages, engineers scattered constants and types across header files included with `include, or repeated the same typedef in every module that needed it. Either approach creates the same maintenance trap: change one value and hunt down every copy. A 32→64-bit data-width upgrade on a real SoC routinely touched 30+ files; one missed file produced a silent width mismatch that survived simulation and surfaced as a tape-out bug.

A package gives shared declarations a single authoritative location. The DUT, the interface, the testbench driver, the scoreboard — all import the same constants and types from the same package. Change DATA_W in one file and every consumer picks it up at the next compile without editing anything else. Add a new field to a transaction struct in one file and every component that handles that struct sees it.

The architectural payoff is bigger than just convenience. Putting parameters and types in a package gives you four properties that no `include workaround can match:

  • Single source of truth. One file owns each constant. Tools, reviewers, and search engines all know where to look.
  • Type consistency. Modules and classes share the same typedef, not independently-redeclared copies. The compiler enforces structural compatibility — if two files reference axi_aw_t, they are guaranteed to mean the same struct.
  • Derived constants live next to the base. STRB_W = DATA_W / 8 is declared right after DATA_W in the same file; the relationship is documented at the declaration site and is always correct by construction.
  • Tool-friendly. Linters, formal tools, and synthesis tools can trace every constant and type back to its named package scope — pkg_name::AXI_DATA_W is far more debuggable than a `AXI_DATA_W macro that vanishes after preprocessing.

This lesson covers the patterns that make these properties stick.

2. Mental Model — parameter for Knobs, localparam for Derivations

The picture every engineer carries:

parameter in a package = a knob the user might want to reconfigure at compile time (a bus width, a depth, a frequency). localparam in a package = a value derived from those knobs, or a protocol constant that must never change. Neither can be overridden via the #() mechanism — packages are never instantiated, so the #() syntax does not apply. Some tools support command-line override of parameter items (typically via simulator-specific flags); localparam cannot be overridden even by tools.

The discipline that follows is mechanical:

  • Declare the knobs first as parameter. These are the values the project might want to change.
  • Declare the derived values immediately after as localparamSTRB_W = DATA_W / 8, MAX_LEN = (1 << LEN_W). The dependency on the knob is visible at the declaration site.
  • Declare protocol constants (response codes, magic numbers, fixed-width opcodes) as localparam as well. These should never change at the project level; flagging them localparam signals that intent.

The order matters. SystemVerilog requires declarations to be in textual order — a localparam that references a parameter declared later in the same package is a compile error. Get the order right once at the top of the file and everything downstream cascades cleanly.

Four invariants this picture preserves:

  • Package parameters are project-wide defaults, not per-instance overrides. If you need different widths in two instances of the same module, the parameter belongs on the module, not in the package.
  • $bits() on any package typedef is a compile-time constant. It can appear anywhere a constant expression is legal: port widths, parameter defaults, array bounds, generate conditions.
  • Packed vs unpacked is the most important distinction in SV types. Packed types can appear on hardware ports and cast to/from logic vectors; unpacked types cannot. Pick the wrong one and the compile error appears at the port site, far from the typedef.
  • Parameterised typedef is not valid SV syntax. The workaround is a parameterised class acting as a type container — covered in §7.

3. parameter vs localparam in a Package

The two keywords both declare compile-time constants. The difference is intent and the override semantics tools apply.

Aspectparameter in a packagelocalparam in a package
LRM semanticsNamed compile-time constant; signals "this value is intended to be configurable"Named compile-time constant that is explicitly immutable; signals "do not attempt to override this"
Override via #()Not possible — packages have no portsNot possible — same reason
Tool-level overrideSome tools accept command-line override flags (e.g. +define+ or simulator-specific options)Never — immutable by the LRM
Use forBus widths, depths, frequencies — the project-wide "knobs"Derived values (STRB_W = DATA_W / 8); protocol constants (response codes, magic numbers)
ConventionThe values at the top of the parameter hierarchyEverything computed from those values
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── axi4_cfg_pkg.sv — clear parameter hierarchy ────────────────────
package axi4_cfg_pkg;
 
    // ── "Knobs" — values that may be reconfigured at compile time ──
    parameter int AXI_DATA_W = 64;    // primary bus data width
    parameter int AXI_ADDR_W = 40;    // address width
    parameter int AXI_ID_W   = 8;     // transaction ID width
    parameter int AXI_LEN_W  = 8;     // burst length field width
 
    // ── Derived constants — computed from knobs, immutable ─────────
    localparam int AXI_STRB_W  = AXI_DATA_W / 8;
    localparam int AXI_WDATA_B = AXI_DATA_W / 8;     // bytes per beat
    localparam int AXI_MAX_LEN = (1 << AXI_LEN_W);   // max burst length
 
    // ── Protocol fixed constants — never user-configurable ─────────
    localparam logic [1:0] RESP_OKAY   = 2'b00;
    localparam logic [1:0] RESP_EXOKAY = 2'b01;
    localparam logic [1:0] RESP_SLVERR = 2'b10;
    localparam logic [1:0] RESP_DECERR = 2'b11;
 
endpackage : axi4_cfg_pkg
 
// ── Consumer ───────────────────────────────────────────────────────
module axi_slave
    import axi4_cfg_pkg::*;
(
    input  logic [AXI_DATA_W-1:0] wdata,
    input  logic [AXI_STRB_W-1:0] wstrb,    // derived localparam — visible too
    output logic [1:0]            bresp
);
    assign bresp = RESP_OKAY;
endmodule

Enums in a package give meaningful names to the legal values of a field — opcodes, states, response codes. They also give the compiler enough information to warn when an assignment is not a valid member of the set, which bare logic values cannot do.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package opcode_pkg;
 
    // ── Basic enum — base type defaults to int (32 bits — usually wrong)
    typedef enum {
        OP_NOP   = 0,
        OP_ADD   = 1,
        OP_SUB   = 2,
        OP_AND   = 3,
        OP_OR    = 4,
        OP_XOR   = 5,
        OP_SHIFT = 6
    } opcode_e;                            // _e suffix convention
 
    // ── Sized enum — hardware-friendly, maps to exact bit width ────
    typedef enum logic [2:0] {
        ST_IDLE   = 3'b000,
        ST_FETCH  = 3'b001,
        ST_DECODE = 3'b010,
        ST_EXEC   = 3'b011,
        ST_WB     = 3'b100
    } cpu_state_e;
 
    // ── One-hot encoding — common in FSMs ──────────────────────────
    typedef enum logic [4:0] {
        ARB_IDLE = 5'b00001,
        ARB_GNT0 = 5'b00010,
        ARB_GNT1 = 5'b00100,
        ARB_GNT2 = 5'b01000,
        ARB_GNT3 = 5'b10000
    } arb_state_e;
 
    // ── Enum without explicit values — auto-increments from 0 ──────
    typedef enum logic [1:0] {
        PRIO_LOW, PRIO_MED, PRIO_HIGH, PRIO_CRIT
    } priority_e;
 
endpackage : opcode_pkg
 
// ── Using package enums ────────────────────────────────────────────
module cpu_fsm
    import opcode_pkg::*;
(
    input  logic       clk, rst_n,
    output cpu_state_e state
);
    cpu_state_e next_state;
 
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) state <= ST_IDLE;
        else        state <= next_state;
    end
 
    always_comb begin
        unique case (state)
            ST_IDLE  : next_state = ST_FETCH;
            ST_FETCH : next_state = ST_DECODE;
            ST_DECODE: next_state = ST_EXEC;
            ST_EXEC  : next_state = ST_WB;
            ST_WB    : next_state = ST_IDLE;
            default  : next_state = ST_IDLE;
        endcase
    end
endmodule
 
// ── Casting between enum and logic ─────────────────────────────────
logic [2:0] raw_bits = 3'b010;
opcode_pkg::cpu_state_e decoded;
 
decoded = opcode_pkg::cpu_state_e'(raw_bits);    // explicit cast — safe
// Without the cast: assigning logic to enum is a type error in strict tools

5. typedef struct — Packed for Hardware, Unpacked for Verification

Structs bundle multiple fields into a single named type. The keyword packed is the single most important modifier in the entire typedef family — it determines whether the type can appear on a hardware port.

5.1 Packed Structs — Hardware Signals

A struct packed lays its fields out contiguously in bit order, MSB first. The whole struct can be assigned to a plain logic vector of the same total width and cast back. Only bit-typed fields are allowed: logic, bit, and other packed types. This is the form you use whenever the struct represents a bus channel, a packet header, or any hardware signal.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package axi4_types_pkg;
    import axi4_cfg_pkg::*;                // bring in DATA_W, ADDR_W, etc.
 
    // ── AW channel — packed: maps to a single logic vector ─────────
    typedef struct packed {
        logic [AXI_ID_W-1:0]   awid;
        logic [AXI_ADDR_W-1:0] awaddr;
        logic [AXI_LEN_W-1:0]  awlen;
        logic [2:0]            awsize;
        logic [1:0]            awburst;
        logic                  awvalid;
    } axi_aw_t;
 
    // ── W channel ──────────────────────────────────────────────────
    typedef struct packed {
        logic [AXI_DATA_W-1:0] wdata;
        logic [AXI_STRB_W-1:0] wstrb;
        logic                  wlast;
        logic                  wvalid;
    } axi_w_t;
 
    // ── B channel ──────────────────────────────────────────────────
    typedef struct packed {
        logic [AXI_ID_W-1:0] bid;
        logic [1:0]          bresp;
        logic                bvalid;
    } axi_b_t;
 
    // ── Full write bundle — struct of structs ──────────────────────
    typedef struct packed {
        axi_aw_t aw;
        axi_w_t  w;
    } axi_wr_req_t;
 
endpackage : axi4_types_pkg
 
// ── Packed struct in module signals ────────────────────────────────
module axi_master
    import axi4_types_pkg::*;
(
    output axi_aw_t aw,           // entire AW channel as one port
    output axi_w_t  w,
    input  axi_b_t  b
);
    localparam int AW_BITS = $bits(axi_aw_t);   // $bits works on packed types
 
    // Field access via dot notation
    assign aw.awburst = 2'b01;    // INCR burst
    assign aw.awsize  = 3'd3;     // 8-byte beats
endmodule
 
// ── Casting packed struct to/from logic vector ─────────────────────
logic [$bits(axi_aw_t)-1:0] raw;
axi4_types_pkg::axi_aw_t    aw_decoded;
 
aw_decoded = axi4_types_pkg::axi_aw_t'(raw);    // raw bits → struct

5.2 Unpacked Structs — Verification Objects

A struct without the packed keyword is unpacked. Fields are not laid out contiguously; the type cannot be cast to a logic vector and cannot appear on a hardware port. In exchange, unpacked structs can hold any type: string, int, class handles, queues, dynamic arrays.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package tb_types_pkg;
 
    // ── Unpacked struct — fields may be any type ───────────────────
    typedef struct {                       // no 'packed' keyword
        logic [31:0] addr;
        logic [63:0] data;
        int          timestamp;            // int allowed in unpacked only
        string       tag;                  // string allowed in unpacked only
        bit          is_write;
    } txn_record_t;
 
    typedef enum logic [1:0] { CHK_PASS, CHK_FAIL, CHK_SKIP } chk_result_e;
 
    typedef struct {
        string       test_name;
        chk_result_e result;
        int          error_count;
    } test_report_t;
 
endpackage : tb_types_pkg
 
// ── Scoreboard using the package types ─────────────────────────────
class Scoreboard;
    import tb_types_pkg::*;
 
    txn_record_t  exp_q[$];                // queue of expected transactions
    txn_record_t  got_q[$];                // queue of observed transactions
    test_report_t report;
 
    function void check(txn_record_t exp, txn_record_t got);
        if (exp.addr !== got.addr || exp.data !== got.data) begin
            report.result = CHK_FAIL;
            report.error_count++;
        end
    endfunction
endclass

6. typedef union packed — Overlapping Views

A union packed overlays multiple fields on the same storage. All members must be exactly the same total bit width. This lets you interpret the same signal as different field breakdowns — the canonical pattern for instruction encoding (RISC-V R-type vs I-type), protocol header parsing, and register packing.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package instr_pkg;
 
    // ── R-type instruction — 32 bits ───────────────────────────────
    typedef struct packed {
        logic [6:0] funct7;
        logic [4:0] rs2;
        logic [4:0] rs1;
        logic [2:0] funct3;
        logic [4:0] rd;
        logic [6:0] opcode;
    } r_type_t;                            // 7+5+5+3+5+7 = 32 bits
 
    // ── I-type instruction — 32 bits ───────────────────────────────
    typedef struct packed {
        logic [11:0] imm12;
        logic [4:0]  rs1;
        logic [2:0]  funct3;
        logic [4:0]  rd;
        logic [6:0]  opcode;
    } i_type_t;                            // 12+5+3+5+7 = 32 bits
 
    // ── Union — same 32 bits, viewed as R-type, I-type, or raw word
    typedef union packed {
        r_type_t     r;                    // R-format view
        i_type_t     i;                    // I-format view
        logic [31:0] raw;                  // raw 32-bit view
    } instr_t;                             // all members MUST be 32 bits
 
endpackage : instr_pkg
 
// ── Decode unit uses the union ─────────────────────────────────────
module decode
    import instr_pkg::*;
(
    input  instr_t instr,
    output logic   is_rtype
);
    assign is_rtype = (instr.r.opcode == 7'b0110011);
 
    always_comb begin
        unique case (instr.r.opcode)
            7'b0110011:                    // R-type — use instr.r fields
                $display("R-type: rd=%0d rs1=%0d rs2=%0d",
                         instr.r.rd, instr.r.rs1, instr.r.rs2);
            7'b0010011:                    // I-type — use instr.i fields
                $display("I-type: rd=%0d rs1=%0d imm=%0d",
                         instr.i.rd, instr.i.rs1, $signed(instr.i.imm12));
            default: $display("raw=%08h", instr.raw);
        endcase
    end
endmodule

The all-members-same-width rule is enforced at the typedef site — a packed union with mismatched widths is a compile error at the declaration, not at the use site.

7. Type Aliases and $bits — Auto-Derived Widths

Beyond enum, struct, and union, typedef in a package can create simple bit-vector aliases that give semantic names to common signal widths. Combined with $bits(), derived widths can be computed automatically — a single width change in the package cascades correctly through every consumer.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package bus_types_pkg;
    parameter int DATA_W = 64;
    parameter int ADDR_W = 40;
 
    // ── Simple bit-vector aliases ──────────────────────────────────
    typedef logic [DATA_W-1:0]    data_t;
    typedef logic [ADDR_W-1:0]    addr_t;
    typedef logic [DATA_W/8-1:0]  strb_t;
 
    // ── Struct built from the aliases ──────────────────────────────
    typedef struct packed {
        addr_t addr;
        data_t data;
        strb_t strb;
        logic  valid;
        logic  last;
    } beat_t;
 
    // ── Derived localparam from $bits() ────────────────────────────
    localparam int BEAT_BITS  = $bits(beat_t);          // total beat width
    localparam int BEAT_BYTES = BEAT_BITS / 8;          // bytes per beat
    localparam int FIFO_DEPTH = 2 ** $clog2(BEAT_BYTES * 4);
 
endpackage : bus_types_pkg
 
// ── Consumer — widths cascade from the single parameter ───────────
module beat_fifo
    import bus_types_pkg::*;
(
    input  logic  clk, rst_n,
    input  beat_t din,
    input  logic  push,
    output beat_t dout,
    output logic  full, empty
);
    beat_t mem [FIFO_DEPTH];               // FIFO_DEPTH computed from $bits(beat_t)
    // ...
endmodule

The $bits() operator returns a compile-time constant. It can appear anywhere a constant expression is legal: port widths, parameter defaults, localparam expressions, array bounds, generate conditions. The compile-time-constant property is what makes the cascade work: change DATA_W from 64 to 128, and BEAT_BITS, BEAT_BYTES, FIFO_DEPTH, and every consumer-side derived width recomputes automatically at the next compile.

8. Parameterised Types — The class Workaround

SystemVerilog does not allow parameterised typedef at package scope. typedef #(parameter N) ... is not valid syntax. When you need a type whose width is itself a parameter, the standard workaround is a parameterised class with a static typedef inside — the class acts as a namespace that carries the type.

Approach A — Fixed-width package typedef

The simplest, and what the vast majority of real designs use. parameter int W = 32; typedef logic [W-1:0] data_t; — the width is fixed when the package compiles. Use when the width is project-wide and truly constant.

Approach B — Module-local typedef

module dut #(parameter int W = 32); typedef logic [W-1:0] data_t; — each instance specialises its own data_t. Use when per-instance width matters; the type cannot be shared with another module's ports.

Approach C — Parameterised class as type container

class TypeOf #(int W = 32); typedef logic [W-1:0] data_t; endclass, reached via my_pkg::TypeOf#(64)::data_t. The formal SV answer, but with the deepest tool-support variance — verify on your simulator before relying on it.

Approach A covers most real designs; reach for Approach B when each instance genuinely needs its own specialisation; reserve Approach C for the rare case neither fits, after confirming your simulator handles class-scoped typedef specialisation.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Approach C — parameterised class as a type factory ─────────────
package generic_types_pkg;
 
    class FifoTypes #(int DATA_W = 32, int DEPTH = 16);
        typedef logic [DATA_W-1:0]       data_t;
        typedef logic [$clog2(DEPTH):0]  ptr_t;       // pointer needs +1 bit
        typedef struct packed {
            data_t entry;
            ptr_t  rd_ptr;
            ptr_t  wr_ptr;
        } fifo_status_t;
    endclass
 
endpackage : generic_types_pkg
 
// ── Consumer — specialise the class to get the needed types ───────
module sync_fifo #(
    parameter int DATA_W = 32,
    parameter int DEPTH  = 16
);
    // Create a local alias for the specialised type set
    typedef generic_types_pkg::FifoTypes #(DATA_W, DEPTH) T;
 
    T::data_t        mem [DEPTH];          // element storage
    T::ptr_t         rd_ptr, wr_ptr;       // pointers
    T::fifo_status_t status;
endmodule
 
// ── Approach B — local typedef inside each module (more common) ───
module simple_fifo #(
    parameter int DATA_W = 8,
    parameter int DEPTH  = 4
);
    typedef logic [DATA_W-1:0] data_t;     // local typedef — straightforward
    data_t mem [DEPTH];
endmodule

The decision rule in practice: prefer Approach A when the width really is project-wide; reach for Approach B when each instance owns its width and the type does not need to leave the module; only use Approach C when neither A nor B fits and you have verified your simulator handles class-scoped typedef specialisation correctly.

9. Real-World Example — Complete SPI Protocol Package

The pattern every protocol VIP follows: parameters → derived localparams → enums → packed structs → utility functions, all in one cohesive file.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── File: spi_pkg.sv ───────────────────────────────────────────────
package spi_pkg;
 
    // ── Configuration knobs (parameter — user may override at compile)
    parameter int SPI_DATA_W   = 8;        // bits per SPI frame
    parameter int SPI_CLK_DIV  = 4;        // SCLK = system_clk / CLK_DIV
    parameter int SPI_CS_COUNT = 4;        // number of chip selects
 
    // ── Derived constants (localparam — computed, immutable) ───────
    localparam int SPI_HALF_CLK = SPI_CLK_DIV / 2;
    localparam int SPI_CS_W     = $clog2(SPI_CS_COUNT);
    localparam int SPI_CNT_W    = $clog2(SPI_DATA_W);
 
    // ── Clock polarity/phase (CPOL, CPHA) mode ─────────────────────
    typedef enum logic [1:0] {
        SPI_MODE0 = 2'b00,                 // CPOL=0, CPHA=0
        SPI_MODE1 = 2'b01,                 // CPOL=0, CPHA=1
        SPI_MODE2 = 2'b10,                 // CPOL=1, CPHA=0
        SPI_MODE3 = 2'b11                  // CPOL=1, CPHA=1
    } spi_mode_e;
 
    typedef enum logic {
        SPI_MSB_FIRST = 1'b0,
        SPI_LSB_FIRST = 1'b1
    } spi_bit_order_e;
 
    // ── Transaction descriptor ─────────────────────────────────────
    typedef struct packed {
        logic [SPI_CS_W-1:0]   cs_sel;     // which CS to assert
        spi_mode_e             mode;
        spi_bit_order_e        bit_order;
        logic [SPI_DATA_W-1:0] tx_data;
    } spi_txn_t;
 
    // ── Status from controller back to SW ──────────────────────────
    typedef struct packed {
        logic [SPI_DATA_W-1:0] rx_data;
        logic                  busy;
        logic                  done;
        logic                  rx_valid;
    } spi_status_t;
 
    // ── Utility — extract CPOL/CPHA from mode ──────────────────────
    function automatic logic get_cpol(input spi_mode_e m);
        return m[1];
    endfunction
 
    function automatic logic get_cpha(input spi_mode_e m);
        return m[0];
    endfunction
 
    // ── Utility — reverse bit order for LSB-first transfers ────────
    function automatic logic [SPI_DATA_W-1:0] reverse_bits(
        input logic [SPI_DATA_W-1:0] d
    );
        for (int i = 0; i < SPI_DATA_W; i++)
            reverse_bits[i] = d[SPI_DATA_W-1-i];
    endfunction
 
endpackage : spi_pkg
 
// ── DUT imports the package ────────────────────────────────────────
module spi_controller
    import spi_pkg::*;
(
    input  logic                    clk, rst_n,
    input  spi_txn_t                txn_in,
    input  logic                    txn_valid,
    output spi_status_t             status,
    output logic                    sclk, mosi,
    input  logic                    miso,
    output logic [SPI_CS_COUNT-1:0] cs_n
);
    logic cpol, cpha;
    assign cpol = get_cpol(txn_in.mode);
    assign cpha = get_cpha(txn_in.mode);
endmodule

Five things this package gets right that every production package follows:

  1. Knobs first, derivations second, protocol constants third. Strict ordering at the top of the file.
  2. Every enum is base-typed logic [N:0] — never bare typedef enum { ... } because the implicit base type int makes the enum 32 bits wide on a hardware port (mistake §10.5).
  3. spi_txn_t and spi_status_t are struct packed — they appear on the controller's ports, so they must be packable.
  4. Helper functions are function automatic — the package-function discipline from 16.1.
  5. Width-dependent fields use the package parameters/localparams directly (logic [SPI_CS_W-1:0] cs_sel) — change SPI_CS_COUNT from 4 to 16 in one place and cs_sel, cs_n, and every consumer-side reference re-elaborates correctly.

10. Debug Lab — Six Package-Type Bugs

The bugs every reviewer flags.

Package parameters & types — bugs every engineer makes once

1. Trying to override a package parameter with #()

Symptom: An engineer writes spi_pkg #(.SPI_DATA_W(16)) u_pkg(); expecting per-site configurability — and gets a syntax error because packages are not instantiable.

Cause: The #() override syntax requires an instantiation; packages have no ports and are never instantiated. The package parameter is a single project-wide value.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ INVALID — packages cannot be instantiated
spi_pkg #(.SPI_DATA_W(16)) u_pkg();

Fix: if per-instance width is needed, the parameter belongs on the module, not the package:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module spi_controller #(
    parameter int DATA_W = spi_pkg::SPI_DATA_W   // module param defaults to pkg value
)(...);
endmodule
 
spi_controller #(.DATA_W(16)) u_alt(...);        // per-instance override now legal

Guardrail: package = project-wide defaults; module/class parameters = per-instance configuration. If you find yourself wanting to override a package parameter, the value belongs on the module instead.

2. Unpacked struct in a hardware port

Symptom: Tool error at the port declaration — "unpacked struct type not allowed in port" or similar. The error message often confuses beginners because the package and module both compile in isolation.

Cause: A struct without the packed keyword is unpacked. Unpacked types have no contiguous bit layout, cannot be cast to logic vectors, and cannot appear on hardware ports.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ TOOL ERROR
package bad_pkg;
    typedef struct {                       // unpacked — no 'packed'
        logic [31:0] data;
        int          count;                // int not allowed in packed anyway
    } bad_t;
endpackage
 
module bad_mod (input bad_pkg::bad_t req); // ERROR: unpacked struct in port
endmodule

Fix: use struct packed for any type that appears on a hardware port:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
typedef struct packed {
    logic [31:0] data;
    logic [15:0] count;                    // logic (not int) — packable
} good_t;

Guardrail: if the type can be packed (all fields are bit types), packed is almost always the right choice. Reserve unpacked structs for testbench objects that hold string/int/class handles.

3. Packed union members with different widths

Symptom: Compile error — "packed union members must have the same number of bits" — at the typedef declaration.

Cause: A union packed overlays all members on the same storage. The LRM requires every member to have the same total bit width because the union is a single bit-vector with multiple interpretations.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ COMPILE ERROR
typedef union packed {
    logic [31:0] word;
    logic [7:0]  byte_val;                 // 8 bits ≠ 32 bits
} bad_union_t;

Fix: pad the smaller members to the union's total width, or restructure as a struct packed if the fields really do not overlap:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
typedef union packed {
    logic [31:0]                 word;
    struct packed {
        logic [23:0] pad;
        logic [7:0]  byte_val;
    } as_byte;
} good_union_t;

Guardrail: the all-members-same-width rule is non-negotiable. If you can't make the widths match, you don't want a union — you want a struct of multiple fields, possibly with an enum discriminator.

4. Forward use of a `localparam` before its base `parameter`

Symptom: Compile error — "identifier DATA_W not declared" at the localparam line, even though DATA_W is declared two lines below in the same package.

Cause: SystemVerilog requires declarations to be in textual order. A localparam that references a parameter declared later in the same package is a forward reference and is not legal.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ COMPILE ERROR
package bad_pkg;
    localparam int STRB_W = DATA_W / 8;    // DATA_W not yet declared
    parameter  int DATA_W = 64;
endpackage

Fix: declare base parameter values before the derived localparam values that reference them:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package good_pkg;
    parameter  int DATA_W = 64;            // base — declared first
    localparam int STRB_W = DATA_W / 8;    // derived — declared after
endpackage

Guardrail: top-of-file convention — knobs first (parameter), derivations second (localparam), protocol constants third. Strict ordering eliminates this class of bug.

5. Unsized enum in a port — accidental 32-bit width

Symptom: A port declared with bad_pkg4::my_e ends up 32 bits wide when the engineer intended 2 bits. The wider port silently passes simulation; the unintended high-order bits become an X-propagation source on real silicon.

Cause: A typedef enum { ... } without an explicit base type defaults to int — which is 32 bits. The literal values A, B, C only need 2 bits to encode, but the port is sized to the base type, not to the number of literals.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ subtle bug — port is 32 bits, not 2
package bad_pkg4;
    typedef enum { A, B, C } my_e;         // base type defaults to int (32 bits)
endpackage
 
module m (input bad_pkg4::my_e e);         // 32-bit port for a 2-value enum
endmodule

Fix: always size the enum's base type explicitly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
typedef enum logic [1:0] { A, B, C } my_e;  // 2-bit port — what you wanted

Guardrail: every package-level enum should be typedef enum logic [N:0] { ... } — never bare typedef enum { ... }. The explicit base type makes the on-the-wire width obvious to every reader.

6. Attempting parameterised typedef directly

Symptom: Compile error at the typedef line — "syntax error near #" — when trying to write a parameterised typedef at package scope.

Cause: SystemVerilog does not provide a parameterised typedef construct. typedef #(parameter N = 8) logic [N-1:0] data_t; is not valid syntax.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ NOT VALID SV
package bad_pkg5;
    typedef #(parameter int N = 8) logic [N-1:0] data_t;
endpackage

Fix: pick one of the three approaches from §8. Most often Approach A (fixed width via package parameter — works for project-wide widths) or Approach B (module-local typedef — works for per-instance specialisation) is the right choice. Approach C (parameterised class as type container) is the formal answer but has uneven tool support.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Approach A — usual choice
package good_pkg;
    parameter int DATA_W = 32;
    typedef logic [DATA_W-1:0] data_t;     // width fixed at pkg compile time
endpackage

Guardrail: if you find yourself reaching for parameterised typedef, ask whether the type really needs per-instance configurability. Most of the time the width is project-wide and Approach A is enough.

11. Q & A

The questions that come up in code review and interviews.

12. Cross-References & What's Next

This lesson covered every form of typedef that lives in a package, the parameter / localparam split, the $bits() derivation idiom, and the parameterised-typedef workaround.

  • Previous: 16.2 — import — Explicit & Wildcard — how consumers reach package members; the placement rules that determine where typed ports can use package types.
  • Next: 16.4 — Package Dependencies & Compilation Order — the build-order discipline that keeps multi-package projects deterministic; the dependency graph as a DAG, circular dependencies and how to break them with a shared base package, and the canonical file-list patterns.

Related material elsewhere in the curriculum:

13. Quick Reference

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Parameters ─────────────────────────────────────────────────────
parameter  int DATA_W = 64;          // knob — possible tool-level override
localparam int STRB_W = DATA_W/8;    // derived — explicitly immutable
// NEITHER can be overridden via #()  — packages are not instantiated
// Declare base parameters BEFORE the derived localparams — order matters
 
// ── Enum ───────────────────────────────────────────────────────────
typedef enum logic [N-1:0] { A=0, B=1 } myenum_e;   // ALWAYS size base type
// _e suffix for enum types; ALL_CAPS for literals; use unique case in FSMs
my_e val = my_e'(raw_bits);          // cast logic → enum (explicit)
 
// ── Packed struct (hardware signal) ────────────────────────────────
typedef struct packed {
    logic [A-1:0] f1;
    logic [B-1:0] f2;
} mystruct_t;
// _t suffix; only bit-type fields; $bits(mystruct_t) → total width
 
// ── Unpacked struct (testbench / class) ────────────────────────────
typedef struct { int count; string name; logic flag; } tb_rec_t;
// Holds any type; cannot appear on hardware port; no $bits() constraint
 
// ── Packed union (overlapping interpretations) ─────────────────────
typedef union packed { struct_a_t a; struct_b_t b; logic [N-1:0] raw; } u_t;
// All members MUST have identical total bit width
 
// ── Type alias ─────────────────────────────────────────────────────
typedef logic [DATA_W-1:0] data_t;
localparam int DATA_BITS = $bits(data_t);    // $bits on typedef = const int
 
// ── Eight rules to live by ─────────────────────────────────────────
// 1.  parameter in a package = configurable; localparam = explicitly immutable.
// 2.  Declare base parameters BEFORE the localparams that reference them.
// 3.  Always size enum base types: 'typedef enum logic [N:0]' — never bare.
// 4.  Use 'struct packed' for hardware; plain 'struct' for testbench objects.
// 5.  All packed union members must have identical bit widths.
// 6.  $bits(pkg_type) is a compile-time constant — legal in port/param widths.
// 7.  Parameterised typedef is NOT valid SV — use parameterised class workaround.
// 8.  Naming: _e for enums, _t for structs/unions/aliases, ALL_CAPS for literals.

14. Summary

Package-level parameters and typedefs are the single most important productivity multiplier in a SystemVerilog codebase. parameter holds the project-wide knobs (bus widths, depths, frequencies); localparam holds everything derived from those knobs and every protocol constant that must never change. The order of declaration is mandatory: base parameters first, derived localparams second.

The four typedef families each have a specific role:

  • enum logic [N:0] — named literals with an explicitly sized base type. Always size the base; the implicit int default makes ports 32 bits wide by accident.
  • struct packed — hardware signals; contiguous bit layout; only bit-type fields; castable to/from logic vectors.
  • struct (unpacked) — testbench objects; can hold string/int/class handles/queues; not allowed on hardware ports.
  • union packed — overlapping views of the same bits; every member must have the same total width.

$bits() on a packed typedef is a compile-time constant, enabling the cascade pattern where a single width change in the package re-elaborates every consumer-side derived width automatically. Parameterised typedef is not valid SV; the workarounds are fixed-width package typedefs (most common), module-local typedefs (per-instance), or parameterised classes as type containers (advanced).

The SPI package in §9 shows the complete production pattern: knobs → derivations → enums → packed structs → utility functions, all in one cohesive file. Every UVM environment and every protocol VIP is built on this pattern. The next page (16.4) closes Module 16 with the cross-package dependency discipline that keeps multi-package builds deterministic.