SystemVerilog · Module 9
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.
Module 9 · Page 9.12
What Parameterised Classes Are
You have written a solid FIFO wrapper class for APB transactions. Now the AXI team needs the same FIFO for AXI transactions. Without parameterisation, you copy the file, rename ApbTxn to AxiTxn, and maintain two identical files forever. Every bug fix gets applied twice — if you remember.
Parameterised classes solve this the same way parameterised modules solve it for RTL. You write the class once with a type placeholder. Each time you use it, you say which type to substitute. The compiler generates the appropriate version for each combination.
Syntax — Annotated
Parameterised Class — Every Part ExplainedclassGenFifo#(← #(...) opens the parameter listtypeT = int, ← type parameter, default intintDEPTH = 16← value parameter, default 16); ← #(...) closes hereT entries[DEPTH]; ← T and DEPTH used as normal type/valueint head, tail, count; function voidpush(T item); ← T is the argument type entries[tail % DEPTH] = item; tail++; count++; endfunctionfunctionTpop(); ← T is the return typeT item = entries[head % DEPTH]; head++; count--; return item; endfunctionendclass// ─── Specialise at instantiation — fill in T and DEPTH ────────GenFifo #(ApbTxn, 32) apb_q = new(); ← T=ApbTxn, DEPTH=32GenFifo #(AxiTxn, 64) axi_q = new(); ← T=AxiTxn, DEPTH=64GenFifo #(int ) int_q = new(); ← T=int, DEPTH=16 (defaults used) Two kinds of parameters — understand both:
| Kind | Keyword | What it does | Example |
|---|---|---|---|
| Type parameter | type T = int | Makes the class work with any data type — transaction class, integer, struct, etc. | T data; function void push(T item); |
| Value parameter | int DEPTH = 16 | Configures numeric constants — array sizes, bit widths, thresholds | T entries[DEPTH]; bit [ADDR_W-1:0] addr; |
Type Parameters — One Class, Any Transaction
The most powerful use in verification: a class that works with any transaction type you give it. Write a scoreboard, a mailbox wrapper, or a monitor queue once — then reuse it for every protocol in your project.
// One class, works with ANY transaction type ──────────────────
class TxnQueue #(type T = BaseTxn);
T items[$];
int max_size;
function new(int max = 0);
max_size = max; // 0 = unlimited
endfunction
function void push(T item);
if (max_size > 0 && items.size() >= max_size)
$fatal(1, "TxnQueue overflow");
items.push_back(item);
endfunction
function T pop();
if (items.size() == 0)
$fatal(1, "TxnQueue underflow");
pop = items.pop_front();
endfunction
function int size(); return items.size(); endfunction
function bit is_empty(); return (items.size() == 0); endfunction
endclass
module tb;
// Specialise once per protocol — zero duplicate code
TxnQueue #(ApbTxn) apb_q = new(64);
TxnQueue #(AxiTxn) axi_q = new();
TxnQueue #(I2cTxn) i2c_q = new(16);
initial begin
ApbTxn a = new(); void'(a.randomize());
apb_q.push(a);
$display("APB queue size: %0d", apb_q.size());
AxiTxn x = new(); void'(x.randomize());
axi_q.push(x);
$display("AXI queue size: %0d", axi_q.size());
// Type safety: cannot push an AxiTxn into apb_q
// apb_q.push(x); ← COMPILE ERROR: wrong type
end
endmoduleValue Parameters — Configurable Widths
Value parameters let you make numeric constants configurable — bus widths, FIFO depths, timeout values — without hard-coding them. The class body uses the parameter as if it were a localparameter.
class BusTxn #(
int ADDR_W = 32,
int DATA_W = 32
);
rand bit [ADDR_W-1:0] addr;
rand bit [DATA_W-1:0] data;
bit write;
int txn_id;
constraint c_align { addr[1:0] == 2'b00; }
function void display();
$display("[BusTxn A:%0d D:%0d] %s addr=0x%h data=0x%h",
ADDR_W, DATA_W,
write ? "WR" : "RD",
addr, data);
endfunction
endclass
// ── Different specialisations for different buses ─────────────
typedef BusTxn #(32, 32) Apb32Txn; // standard 32-bit APB
typedef BusTxn #(64, 64) Axi64Txn; // 64-bit AXI
typedef BusTxn #(32, 8) UartTxn; // 32-bit addr, 8-bit data
module tb;
Apb32Txn apb = new(); void'(apb.randomize()); apb.display();
Axi64Txn axi = new(); void'(axi.randomize()); axi.display();
UartTxn u = new(); void'(u.randomize()); u.display();
endmoduleBusTxn #(32, 32)Standard 32-bit APB/AXI-Lite — most common configurationBusTxn #(64, 64)64-bit AXI4 — full address and data widthBusTxn #(32, 8)UART-style — full address, byte-wide dataBusTxn #(16, 16)Compact MCU peripherals — narrow address and data
typedef — Give Specialisations a Clean Name
Writing TxnQueue #(ApbTxn, 64) everywhere gets verbose. A typedef gives a specialisation a short, readable name that you use like any ordinary type throughout your testbench.
// Create named specialisations once — use the short name everywhere
typedef TxnQueue #(ApbTxn, 64) ApbQueue;
typedef TxnQueue #(AxiTxn, 128) AxiQueue;
typedef BusTxn #(32, 32) ApbTxn32;
typedef BusTxn #(64, 64) Axi64Txn;
// Now use the clean names
ApbQueue apb_expected = new();
AxiQueue axi_expected = new();
// Much cleaner than:
// TxnQueue #(ApbTxn, 64) apb_expected = new();
// TxnQueue #(AxiTxn, 128) axi_expected = new();Real Verification Use Case — Generic Scoreboard
The most useful parameterised class in verification. One scoreboard class that works with any request and response type. Instantiate it once for APB, once for AXI, once for I2C.
// ── Generic scoreboard ────────────────────────────────────────
class GenScoreboard #(
type REQ = BaseTxn, // expected (driven) transaction type
type RSP = BaseTxn // actual (observed) transaction type
);
REQ expected_q[$];
RSP actual_q[$];
int pass_cnt, fail_cnt;
string sb_name;
function new(string name = "scoreboard");
sb_name = name;
pass_cnt = 0;
fail_cnt = 0;
endfunction
function void add_expected(REQ t);
expected_q.push_back(t);
endfunction
function void add_actual(RSP t);
actual_q.push_back(t);
endfunction
// Compare using the transaction's own compare() method
function void check_all();
int n = (expected_q.size() < actual_q.size()) ?
expected_q.size() : actual_q.size();
for (int i = 0; i < n; i++) begin
if (expected_q[i].compare(actual_q[i])) begin
pass_cnt++;
end else begin
fail_cnt++;
$error("[%s] Mismatch at index %0d", sb_name, i);
end
end
endfunction
function void report();
$display("[%s] PASS=%0d FAIL=%0d pending_exp=%0d",
sb_name, pass_cnt, fail_cnt,
expected_q.size());
endfunction
endclass
// ── Scoreboard typedefs for each protocol ─────────────────────
typedef GenScoreboard #(ApbTxn, ApbTxn) ApbScoreboard;
typedef GenScoreboard #(AxiTxn, AxiTxn) AxiScoreboard;
// ── Testbench ─────────────────────────────────────────────────
module tb;
ApbScoreboard apb_sb = new("APB_SB");
AxiScoreboard axi_sb = new("AXI_SB");
initial begin
ApbTxn e = new(), a = new();
void'(e.randomize()); void'(a.randomize());
apb_sb.add_expected(e);
apb_sb.add_actual(a);
apb_sb.check_all();
apb_sb.report();
end
endmoduleStatic Members in Parameterised Classes
Each specialisation of a parameterised class is treated as a separate class by the compiler. That means static members are not shared between specialisations — TxnQueue #(ApbTxn) has its own static counter, and TxnQueue #(AxiTxn) has a different one.
class Counter #(type T = int);
static int instance_count = 0;
int id;
function new();
id = instance_count++;
endfunction
endclass
module tb;
initial begin
Counter #(int) c1 = new();
Counter #(int) c2 = new();
Counter #(string) s1 = new();
// Counter#(int) has its own count: 2
$display("int counter: %0d", Counter #(int)::instance_count);
// Counter#(string) has its own count: 1 (separate)
$display("string counter: %0d", Counter #(string)::instance_count);
end
endmoduleFull Example — Generic Typed Mailbox
SystemVerilog's built-in mailbox is untyped — you can accidentally put the wrong object type in. A parameterised wrapper class fixes that with compile-time type safety.
// ── Type-safe mailbox wrapper ─────────────────────────────────
class TypedMailbox #(type T = BaseTxn);
local mailbox #(T) mbx; // uses SV's typed mailbox internally
function new(int bound = 0);
mbx = new(bound);
endfunction
// Blocking put — waits if mailbox is full
task put(T item);
mbx.put(item);
endtask
// Blocking get — waits if mailbox is empty
task get(output T item);
mbx.get(item);
endtask
// Non-blocking try_get
function bit try_get(output T item);
return mbx.try_get(item);
endfunction
function int num(); return mbx.num(); endfunction
endclass
// ── Typedefs for common use ───────────────────────────────────
typedef TypedMailbox #(ApbTxn) ApbMailbox;
typedef TypedMailbox #(AxiTxn) AxiMailbox;
// ── Testbench using both mailboxes ────────────────────────────
module tb;
ApbMailbox apb_mbx = new();
AxiMailbox axi_mbx = new();
initial fork
// Producer — puts APB transactions
begin
repeat(3) begin
ApbTxn t = new(); void'(t.randomize());
apb_mbx.put(t);
$display("Produced APB txn");
end
end
// Consumer — gets APB transactions
begin
repeat(3) begin
ApbTxn t;
apb_mbx.get(t);
t.display();
end
end
// apb_mbx.put(new AxiTxn()); ← COMPILE ERROR: type mismatch
join
endmoduleQuick Reference
| Operation | Syntax |
|---|---|
| Declare with type parameter | class MyClass #(type T = int); |
| Declare with value parameter | class MyClass #(int DEPTH = 16); |
| Declare with both | class MyClass #(type T = int, int N = 8); |
| Instantiate (specific type) | MyClass #(ApbTxn, 32) obj = new(); |
| Instantiate (defaults) | MyClass obj = new(); |
| Name a specialisation | typedef MyClass #(ApbTxn) ApbMyClass; |
| Access static member | MyClass #(ApbTxn)::static_prop |
| Static per specialisation | Each unique parameter combination has its own static storage |
Verification Usage — Where Parameterised Classes Power Real UVM
Open the UVM source tree and almost every reusable class is parameterised — uvm_sequence #(REQ, RSP), uvm_driver #(REQ), uvm_analysis_port #(T), uvm_tlm_fifo #(T), uvm_sequencer #(REQ, RSP). The keyword type is what lets one source file serve every protocol.
The UVM driver pattern
Every protocol-specific driver subclasses uvm_driver #(my_xact). The base provides seq_item_port typed correctly for the transaction; the child just implements run_phase. Without parameterisation, UVM would need a separate driver class per transaction kind — instead there is one source.
<code>class apb_driver extends uvm_driver #(apb_xact);
`uvm_component_utils(apb_driver)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
apb_xact req;
forever begin
seq_item_port.get_next_item(req); // req is typed apb_xact, no $cast
drive(req);
seq_item_port.item_done();
end
endtask
endclass</code>The analysis port + TLM FIFO pattern
Every monitor publishes transactions through uvm_analysis_port #(my_xact); every scoreboard subscribes via uvm_analysis_imp #(my_xact, sb_t). Because the port is parameterised, the type-checking is enforced at compile time — you cannot accidentally connect an AXI monitor to an APB scoreboard.
The generic scoreboard pattern
Project-specific scoreboards are themselves parameterised on transaction type plus a comparison policy. One generic implementation services every interface.
<code>class generic_scoreboard #(type T = uvm_sequence_item) extends uvm_scoreboard;
uvm_analysis_imp #(T, generic_scoreboard#(T)) ap;
T expected[$];
function void write(T t);
if (expected.size() == 0)
`uvm_error("SB", "got transaction with empty expected queue")
else if (!expected.pop_front().compare(t))
`uvm_error("SB", $sformatf("mismatch: %s", t.convert2string()))
endfunction
endclass
typedef generic_scoreboard#(axi_xact) axi_sb_t;
typedef generic_scoreboard#(apb_xact) apb_sb_t;</code>Simulation Behavior — How the Simulator Specialises Classes
Each specialisation is a distinct class
When the compiler sees scoreboard#(axi_xact) and scoreboard#(apb_xact), it generates two independent class definitions — separate vtables, separate static fields, separate type identifiers. $typename() on a handle returns names like scoreboard #(axi_xact) and scoreboard #(apb_xact); they are not related by inheritance.
Specialisation happens at elaboration, once per distinct parameter tuple
The compiler memoises specialisations — declaring 100 handles of scoreboard#(axi_xact) in 100 different files still results in exactly one class definition for that specialisation. The cost of parameterisation is paid per distinct parameter tuple, not per handle.
Static fields are per-specialisation
A line like static int next_id = 0; inside class transactor #(type T); creates one counter for transactor#(packet), a separate counter for transactor#(error_packet), and so on. If you need a shared counter across all specialisations, factor it into a separate non-parameterised class.
Type compatibility — different parameters = different types
You cannot assign a scoreboard#(axi_xact) handle to a scoreboard#(apb_xact) variable — these are unrelated classes, even though they came from the same source. The compiler treats them like module fifo #(W=8) and module fifo #(W=16): same template, but distinct instantiations with no implicit conversion.
<code>class queue_wrap #(type T = int);
T items[$];
static int total_created = 0;
function new();
total_created++;
endfunction
endclass
queue_wrap#(int) q1, q2;
q1 = new(); q2 = new();
// queue_wrap#(int)::total_created == 2</code><code>queue_wrap#(int) qi = new();
queue_wrap#(string) qs = new();
// queue_wrap#(int)::total_created == 1
// queue_wrap#(string)::total_created == 1
// They are separate static counters!
qs = qi; // ILLEGAL — type mismatch
// ^^
// queue_wrap#(int) is not
// queue_wrap#(string)</code>Waveform Analysis — Tracking Parameterised Classes in Debug
Parameterised classes leave no direct waveform footprint, but the specialisation showing up in $typename(this) is one of the most useful debug signals when tracking down "which specialisation actually fired."
Logging the specialisation
<code>class scoreboard #(type T = uvm_sequence_item) extends uvm_scoreboard;
function void write(T t);
$display("[%0t] %s::write txn=%s id=%0d",
$time, $typename(this), $typename(t), t.get_transaction_id());
// ... rest of the scoreboard logic ...
endfunction
endclass
// Log lines clearly identify which specialisation handled which txn:
// 100ns scoreboard #(axi_xact)::write txn=axi_xact id=1
// 150ns scoreboard #(apb_xact)::write txn=apb_xact id=42
// 200ns scoreboard #(axi_xact)::write txn=axi_xact id=2</code>ASCII view — mixed-protocol scoreboard logs
<code> 100ns scoreboard #(axi_xact)::write txn=axi_xact id=1
150ns scoreboard #(apb_xact)::write txn=apb_xact id=42
200ns scoreboard #(axi_xact)::write txn=axi_xact id=2
250ns scoreboard #(spi_xact)::write txn=spi_xact id=7
↑ ↑
specialisation payload type
that ran the call carried</code>Industry Insights — Hard-Won Lessons About Parameterised Classes
Debugging Academy — Five Real Bugs From Parameterised Classes
"Two scoreboards expected to share a counter — each shows count=1"
DEBUGA test creates one scoreboard#(axi_xact) and one scoreboard#(apb_xact), expecting them to share a class-wide instance counter. Each reports count = 1 instead of the expected count = 2.
<code>class scoreboard #(type T = uvm_sequence_item);
static int count = 0; // intended global counter
function new();
count++;
endfunction
endclass
scoreboard#(axi_xact) sb_a = new();
scoreboard#(apb_xact) sb_b = new();
// scoreboard#(axi_xact)::count == 1
// scoreboard#(apb_xact)::count == 1 — not 2!</code>Each specialisation gets its own static storage. The two specialisations are entirely separate classes from the simulator's view, so the static count in scoreboard#(axi_xact) is unrelated to the one in scoreboard#(apb_xact).
Move the shared counter into a separate non-parameterised class (e.g., a singleton scoreboard_stats) and have every specialisation increment it: function new(); scoreboard_stats::get().bump(); endfunction. The non-parameterised class has exactly one static storage cell regardless of how many scoreboard specialisations exist.
"Compile error: cannot assign queue_wrap#(int) to queue_wrap#(byte)"
DEBUGThe line qb = qi; is rejected with "cannot assign queue_wrap #(int) to queue_wrap #(byte) — incompatible types."
<code>class queue_wrap #(type T = int);
T items[$];
endclass
queue_wrap#(int) qi = new();
queue_wrap#(byte) qb;
qb = qi; // ERROR — different specialisations, no implicit conversion</code>Different parameter values produce unrelated classes. There is no implicit conversion between queue_wrap#(int) and queue_wrap#(byte) — the compiler treats them like two entirely different classes that happen to share a source file.
Either (a) use a single specialisation throughout (queue_wrap#(int) in both places, with explicit casts when extracting individual items), or (b) refactor to expose only the common interface through an abstract base class that both specialisations extend.
"Specialisation typo creates a parallel class — analysis port refuses to connect"
DEBUGAn uvm_analysis_port connection fails with "cannot connect uvm_analysis_port #(axi_xact) to uvm_analysis_imp #(axi_packet, ...)". The user typed axi_packet at one site instead of axi_xact.
<code>// monitor.sv
uvm_analysis_port #(axi_xact) ap;
// scoreboard.sv — typo!
uvm_analysis_imp #(axi_packet, my_sb) imp;
// env.sv
mon.ap.connect(sb.imp); // type-mismatch compile error</code>A single-character typo (axi_packet vs axi_xact) produced two specialisations of uvm_analysis_imp with incompatible types. The compiler caught it at connect-time, but the error message points at the connect site, not the original typo.
Always typedef shared specialisations in one header file and import them everywhere: typedef uvm_analysis_imp #(axi_xact, my_sb) axi_sb_imp_t;. A typo at the single typedef line surfaces immediately; downstream sites use the typedef name and stay consistent.
"Parameterised class fails to elaborate with cryptic 'type T is not defined' error"
DEBUGA new file using scoreboard#(my_xact) errors out with "type 'T' is not defined in this scope", even though my_xact is visibly declared.
<code>// In the file using scoreboard:
`include "scoreboard.svh" // includes the parameterised template
scoreboard#(my_xact) sb; // but my_xact isn't visible yet
// because xact.svh comes later
`include "my_xact.svh"</code>Parameterised class specialisation happens at the point of use, not at the template definition. The compiler needs the parameter type visible at the specialisation site. Including the template before the type definition means the type isn't in scope when the specialisation is elaborated.
Include the type's header file before any file that specialises on that type: include "my_xact.svh"` first, then include "scoreboard.svh". Better: put everything in a single package and use import` — packages resolve all internal references in one pass regardless of declaration order within the package.
"Default parameter behaves differently in two locations"
DEBUGscoreboard sb; declared in test A behaves correctly, but the same line in test B gets a different behaviour. The default parameter T = uvm_sequence_item appears to resolve differently.
<code>// test_A.sv — imports a custom uvm_sequence_item replacement
import my_pkg::uvm_sequence_item;
scoreboard sb; // T defaults to my_pkg::uvm_sequence_item
// test_B.sv — uses the real UVM library
import uvm_pkg::*;
scoreboard sb; // T defaults to uvm_pkg::uvm_sequence_item</code>The default value of a type parameter is resolved using name lookup at the specialisation site, not at the template definition site. Different imports in different scopes can make the same default parameter name resolve to different actual types.
Always be explicit at the specialisation site: scoreboard#(uvm_pkg::uvm_sequence_item) sb;. Fully-qualified names eliminate the ambiguity. As a general rule, never rely on default type parameters in code that crosses package boundaries.
Interview Q&A — Twelve Questions You Will Be Asked
A class that declares one or more parameters in its definition (class my_cls #(type T = int, int W = 8); ... endclass). Each combination of parameter values produces a distinct specialisation at elaboration time. Parameterised classes are the OOP equivalent of parameterised modules — write one source, instantiate many specialised variants.
Best Practices — Ten Rules for Parameterised Classes in Production
- Use parameterised classes whenever you would otherwise copy-paste a class and change one field's type. Drivers across protocols, scoreboards across transactions, generic FIFOs — the canonical use cases.
- Provide sensible defaults for every parameter.
type T = uvm_sequence_itemandint DEPTH = 16let users instantiate without specifying everything when the defaults are right for their case. - Always
typedefnon-trivial specialisations once, in one place. Avoids repetition, prevents specialisation drift across sites, makes typos surface immediately at the typedef line. - Order parameters from "varies most" to "varies least." Type first, value second, policy third. Keeps the common
typedefcalls short and lets unusual cases tack on extra parameters. - Treat
staticfields as per-specialisation, not class-wide. Use a separate non-parameterised singleton if you need state shared across all specialisations. - Never expect implicit conversion between specialisations.
scoreboard#(axi_xact)andscoreboard#(apb_xact)are unrelated classes — assigning one to the other is a compile error. - Put parameterised templates and the types they specialise on into the same
package. Avoids file-include ordering problems and gives packages a single coherent type system to resolve. - Print
$typename(this)in debug output. Parameterised$typenameincludes the full specialisation — invaluable when tracking down which variant fired or which two "identical" types failed to connect. - Document each parameter's purpose at the class declaration. A reviewer should understand from one comment block whether
Tmeans "transaction type," "payload type," or "callback type" without reading the class body. - Don't over-parameterise. If a parameter is never overridden in practice, it just adds noise. Make a class parameterised only when you actually expect two or more distinct specialisations to exist.