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.
Continue learning
Related tutorials
- Related topic
Parameters & Localparams
parameter vs localparam, $clog2 derivation, typed parameters, naming conventions, hierarchical sizing, and 5 debugging labs across the parameter contract.
- Related topic
Parameterised Classes
Type and value parameters, per-specialisation static storage, the UVM driver/scoreboard/analysis-port template pattern, and why typedef is mandatory for non-trivial specialisations.
- Related topic
typedef class — Forward Declarations
Breaking circular references between mutually-referencing classes, request/response pairs, callback patterns, and the orphan-forward trap that surfaces as cryptic 'incomplete type' errors.
Standards & specifications
- Governing standard
- IEEE Std 1800 (SystemVerilog)(opens IEEE in a new tab)
Defines SystemVerilog language semantics — syntax, data types, scheduling and the behaviour a conforming simulator must produce. It does not define tool-specific synthesis support or vendor methodology.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the SystemVerilog curriculum.
