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 referenceaxi_aw_t, they are guaranteed to mean the same struct. - Derived constants live next to the base.
STRB_W = DATA_W / 8is declared right afterDATA_Win 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_Wis far more debuggable than a`AXI_DATA_Wmacro 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:
parameterin a package = a knob the user might want to reconfigure at compile time (a bus width, a depth, a frequency).localparamin 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 ofparameteritems (typically via simulator-specific flags);localparamcannot 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
localparam—STRB_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
localparamas well. These should never change at the project level; flagging themlocalparamsignals 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
logicvectors; unpacked types cannot. Pick the wrong one and the compile error appears at the port site, far from the typedef. - Parameterised
typedefis 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.
| Aspect | parameter in a package | localparam in a package |
|---|---|---|
| LRM semantics | Named 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 ports | Not possible — same reason |
| Tool-level override | Some tools accept command-line override flags (e.g. +define+ or simulator-specific options) | Never — immutable by the LRM |
| Use for | Bus widths, depths, frequencies — the project-wide "knobs" | Derived values (STRB_W = DATA_W / 8); protocol constants (response codes, magic numbers) |
| Convention | The values at the top of the parameter hierarchy | Everything computed from those values |
// ── 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;
endmodule4. typedef enum — Naming the Legal Values
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.
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 tools5. 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.
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 → struct5.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.
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
endclass6. 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.
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
endmoduleThe 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.
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)
// ...
endmoduleThe $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.
// ── 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];
endmoduleThe 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.
// ── 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);
endmoduleFive things this package gets right that every production package follows:
- Knobs first, derivations second, protocol constants third. Strict ordering at the top of the file.
- Every enum is base-typed
logic [N:0]— never baretypedef enum { ... }because the implicit base typeintmakes the enum 32 bits wide on a hardware port (mistake §10.5). spi_txn_tandspi_status_tarestruct packed— they appear on the controller's ports, so they must be packable.- Helper functions are
function automatic— the package-function discipline from 16.1. - Width-dependent fields use the package parameters/localparams directly (
logic [SPI_CS_W-1:0] cs_sel) — changeSPI_CS_COUNTfrom 4 to 16 in one place andcs_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
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.
// ❌ 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:
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 legalGuardrail: 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.
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.
// ❌ 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
endmoduleFix: use struct packed for any type that appears on a hardware port:
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.
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.
// ❌ 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:
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.
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.
// ❌ COMPILE ERROR
package bad_pkg;
localparam int STRB_W = DATA_W / 8; // DATA_W not yet declared
parameter int DATA_W = 64;
endpackageFix: declare base parameter values before the derived localparam values that reference them:
package good_pkg;
parameter int DATA_W = 64; // base — declared first
localparam int STRB_W = DATA_W / 8; // derived — declared after
endpackageGuardrail: top-of-file convention — knobs first (parameter), derivations second (localparam), protocol constants third. Strict ordering eliminates this class of bug.
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.
// ❌ 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
endmoduleFix: always size the enum's base type explicitly:
typedef enum logic [1:0] { A, B, C } my_e; // 2-bit port — what you wantedGuardrail: 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.
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.
// ❌ NOT VALID SV
package bad_pkg5;
typedef #(parameter int N = 8) logic [N-1:0] data_t;
endpackageFix: 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.
// 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
endpackageGuardrail: 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:
- Parameters & Localparams — the broader treatment of compile-time constants outside the package context.
- Parameterised Classes — the OOP construct behind the Approach C parameterised-typedef workaround.
- typedef class — Forward Declarations — the forward-reference mechanism for mutually-referencing classes; 16.4 explains why it breaks intra-package cycles but not cross-package ones.
13. Quick Reference
// ── 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 implicitintdefault makes ports 32 bits wide by accident.struct packed— hardware signals; contiguous bit layout; only bit-type fields; castable to/fromlogicvectors.struct(unpacked) — testbench objects; can holdstring/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.