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.

systemverilog
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.

systemverilog
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.

systemverilog
// 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 ( /* ... */ );
endmodule
diagram
ONE 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 40

Interview 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.