Part 1 · Language Foundations · Intermediate
typedef, parameter & localparam
Type naming discipline, parameter vs localparam scope, type parameters, and parameterized widths flowing through a testbench.
typedef — naming types is an engineering act
typedef binds a name to a type so the meaning is written once and the structure follows it. The payoff is threefold. Maintainability: change addr_t from 32 to 40 bits in one package line and every port, variable, and queue using it follows. Type identity: as covered in the package lesson, a typedef in a package is one type everywhere, which mailboxes, queues, and virtual-interface-adjacent code require. Readability: addr_t tells a reader what a value is for; logic [31:0] tells them only how wide it is. The discipline that follows: every bus, every enum, every struct that crosses a file boundary gets a typedef, and the typedef lives in a package.
package mem_pkg;
parameter int ADDR_W = 32;
parameter int DATA_W = 64;
typedef logic [ADDR_W-1:0] addr_t;
typedef logic [DATA_W-1:0] data_t;
typedef enum logic [1:0] { OKAY, EXOKAY, SLVERR, DECERR } resp_e;
typedef struct packed {
addr_t addr;
data_t data;
resp_e resp;
} mem_txn_t;
endpackage
// Width change? Edit ADDR_W once. Every addr_t in the
// DUT ports, monitor, scoreboard, and coverage follows.parameter vs localparam
Both declare elaboration-time constants; the difference is who may override them . A parameter on a module, interface, or class header is part of its public contract — instantiations may override it with #(...) or defparam (avoid the latter). A localparam is internal and cannot be overridden — use it for derived values that must stay consistent with the public parameters, like a count of address bits computed from a depth. Inside a package, the distinction collapses: package parameters can never be overridden, so parameter and localparam behave identically there. The convention is still to write the intent: tunable-in-spirit values as parameter, derived values as localparam.
module fifo #(
parameter int DEPTH = 16, // public: user may override
parameter int DATA_W = 32
)(
input logic clk, rst_n,
input logic [DATA_W-1:0] wdata,
// ...
);
// derived: MUST track DEPTH, so it is not overridable
localparam int PTR_W = $clog2(DEPTH);
logic [PTR_W:0] wr_ptr, rd_ptr; // extra bit for full/empty
endmodule
// instantiation overrides the public contract only:
fifo #(.DEPTH(64), .DATA_W(8)) u_fifo ( /* ... */ );
// PTR_W recomputes to 6 automatically — it cannot drift.Type parameters and widths flowing through a TB
Parameters can carry types , not just values: parameter type T = logic [7:0] lets one module, interface, or class body work with any payload type the instantiator supplies. This is the static-world cousin of class parameterization, and it is how a generic scoreboard or FIFO model avoids being rewritten per protocol. The companion discipline in a testbench: widths defined once in the package flow into the interface, the DUT instantiation, and the transaction class — so a width mismatch becomes impossible rather than merely unlikely. The parameterized-interfaces lesson in the interfaces topic shows the wiring in full.
// generic comparator usable for any transaction type
module scoreboard_m #(
parameter type TXN_T = logic [31:0]
)(
input TXN_T expected, actual,
output logic match
);
assign match = (expected == actual);
endmodule
// widths flow from one package through the whole bench:
module tb_top;
import mem_pkg::*; // ADDR_W, DATA_W, mem_txn_t
mem_if #(.ADDR_W(ADDR_W), .DATA_W(DATA_W)) bus (clk);
dut #(.ADDR_W(ADDR_W), .DATA_W(DATA_W)) u_dut (.bus(bus));
scoreboard_m #(.TXN_T(mem_txn_t)) u_sb ( /* ... */ );
endmoduleONE SOURCE OF TRUTH FOR WIDTHS
mem_pkg (ADDR_W, DATA_W, mem_txn_t)
│
┌────────┼─────────────┬───────────────┐
▼ ▼ ▼ ▼
interface DUT params txn class coverage bins
mem_if #(ADDR_W..) uses addr_t [0 : 2**ADDR_W-1]
│
└─ change ADDR_W once → everything recompiles consistently
hardcode 32 anywhere → silent truncation when it becomes 40Interview angle
"parameter vs localparam?" — overridable contract vs internal derived constant; in packages neither is overridable.
"What is a type parameter?" — parameter type T lets modules/interfaces/classes be generic over a type at elaboration.
"How do you keep TB and DUT widths in sync?" — single package parameters flowing into interface, DUT, and classes.
Key takeaways
typedef every type that crosses a file boundary, and put the typedef in a package.
parameter = overridable public contract; localparam = derived internal constant ($clog2 results, etc.).
Package parameters are never overridable — parameter and localparam are equivalent there.
Type parameters (parameter type T) make modules and interfaces generic; widths should have one package source of truth.
Common pitfalls
Hardcoding a width in one corner of the TB while the package parameter changes — silent truncation.
Using parameter for a derived value like $clog2(DEPTH) — an override desynchronizes it from DEPTH.
Re-declaring structurally identical types per file instead of one package typedef — type-compatibility errors.
Using defparam for overrides — deprecated, action-at-a-distance; use #(.NAME(value)) at instantiation.