User-Defined Types with typedef
Named aliases, parameterized types, forward declarations, package-level typedefs.
Module 2 · Page 2.5
The Feature That Makes Large Designs Maintainable
Consider a 32-bit SoC that uses a 32-bit address bus everywhere — in the AXI agent, the scoreboard, the register model, the monitor, the coverage model. Every one of these components has declarations like logic [31:0] addr. Now the product team decides to extend the address space to 40 bits for the next silicon revision. Without typedef, you search-replace across dozens of files and hope you didn't miss any. With typedef logic [39:0] addr_t in a package, you change exactly one line.
That's the maintenance story. The readability story is just as important. A function signature like task drive(addr_t addr, data_t data, byte_t strb) is self-documenting. The names tell you what each parameter represents without consulting the module documentation. Compare that to task drive(logic [39:0] addr, logic [31:0] data, logic [3:0] strb) — technically identical, but every reader has to mentally decode the widths.
What typedef Actually Does
A typedef creates a new name for an existing type. It does not create a new type — the compiler treats the alias and the original type as fully interchangeable. addr_t variables can be assigned to logic [31:0] variables without any cast. The alias is purely for human readability and one-place-change maintainability.
This is meaningfully different from how types work in strongly-typed languages like Ada or VHDL where a typedef-equivalent creates a distinct type that cannot be mixed. In SV, typedef is purely a naming convenience — there is no type-safety enforcement between a typedef and its underlying type.
- Simple aliases — Give descriptive names to bit vectors: addr_t, data_t, byte_t. Width changes propagate from a single definition.
- Complex type aliases — Name struct, enum, union, and array types so they can be used cleanly in function signatures, port lists, and class fields.
- Forward declarations — typedef class my_class; declares a class name before its full definition — needed for circular class references.
- Parameterized types — typedef logic [WIDTH-1:0] data_t; inside a parameterized module creates width-flexible types driven by the module parameter.
Syntax — Every Form You'll Use
// ── BASIC SYNTAX ──────────────────────────────────────────────────
// typedef existing_type new_name;
// ── 1. Simple bit-vector aliases ─────────────────────────────────
typedef logic [7:0] byte_t;
typedef logic [15:0] hword_t;
typedef logic [31:0] word_t;
typedef logic [63:0] dword_t;
typedef logic [31:0] addr_t; // semantic: this is an address
typedef logic [31:0] data_t; // semantic: this is data
typedef logic [3:0] strb_t; // AXI write strobe
// ── 2. Signed integer aliases ─────────────────────────────────────
typedef logic signed [7:0] int8_t;
typedef logic signed [31:0] int32_t;
// ── 3. Array type aliases ─────────────────────────────────────────
typedef byte_t packet_t [64]; // 64-byte fixed packet
typedef byte_t dyn_buf_t []; // dynamic byte buffer
typedef word_t reg_file_t [32]; // 32-entry register file
// ── 4. Typedef for struct (defined before the struct) ─────────────
typedef struct packed {
logic [7:0] opcode;
logic [31:0] addr;
logic [31:0] data;
} axi_cmd_t;
// ── 5. Typedef for enum ───────────────────────────────────────────
typedef enum logic [1:0] {
IDLE, ACTIVE, DRAIN, ERROR
} fsm_state_t;
// ── 6. Forward declaration for class (circular reference) ─────────
typedef class axi_transaction; // declare name before full definition
class axi_scoreboard;
axi_transaction pending [$]; // can reference before axi_transaction is defined
endclass
class axi_transaction; // now define it
axi_scoreboard sb_handle; // can reference scoreboard too
endclass
// ── 7. Parameterized module with local typedef ─────────────────────
module fifo #(parameter int DATA_W = 8, DEPTH = 16) (…);
typedef logic [DATA_W-1:0] data_t; // width-flexible inside module
typedef data_t mem_t [DEPTH];
mem_t fifo_mem;
endmodule| typedef form | Example | Use case |
|---|---|---|
| Simple type alias | typedef logic [31:0] addr_t | Readable port names, one-place width change |
| Signed alias | typedef logic signed [7:0] int8_t | Signed arithmetic with descriptive names |
| Array alias | typedef byte_t pkt_t [64] | Passing arrays cleanly between functions |
| Struct alias | typedef struct packed {...} cmd_t | Protocol transaction types |
| Enum alias | typedef enum {...} state_t | FSM states with type name |
| Forward class declaration | typedef class my_class | Resolve circular class dependencies |
| Parameterized local | typedef logic [W-1:0] data_t | Inside parameterized modules |
Visual — Before and After: What typedef Does for Readability
Without typedef vs With typedef
| Without typedef | With typedef (from a shared package) |
|---|---|
task drive(logic [39:0] a, logic [31:0] d, logic [3:0] s); | task drive(addr_t a, data_t d, strb_t s); |
logic [39:0] req_addr, resp_addr; | addr_t req_addr, resp_addr; |
logic [31:0] mem [256]; | data_t mem [256]; |
function logic [31:0] read_reg(logic [39:0] addr); | function data_t read_reg(addr_t addr); |
Width Change Propagation — The Maintenance Win
| Scenario | Without typedef | With typedef |
|---|---|---|
| Address bus 32→40 bits | Search-replace logic [31:0] in every file — miss one and get a width mismatch bug | Change one line: typedef logic [39:0] addr_t |
| Data bus 32→64 bits | Update every function signature, every variable declaration, every port | Change one line: typedef logic [63:0] data_t |
| TID from 4 bits to 8 bits | Multiple replacements, easy to miss edge cases | typedef logic [7:0] tid_t |
Code Examples — Aliases to Production Packages
Example 1 — Beginner: Basic Type Aliases
module tb_typedef_basic;
// Define aliases — normally these go in a package
typedef logic [7:0] byte_t;
typedef logic [31:0] addr_t;
typedef logic [31:0] data_t;
typedef int count_t; // int alias — for semantics
// Use the aliases — identical behavior to the underlying types
addr_t base_addr = 32'hA000_0000;
data_t reg_val = 32'hCAFE_BABE;
count_t txn_count = 0;
byte_t byte_arr [4];
// typedef is transparent — you can mix with the original type freely
logic [31:0] raw_addr;
raw_addr = base_addr; // OK: addr_t is just logic [31:0]
// Array alias
typedef byte_t pkt_t [8]; // 8-byte packet type
pkt_t pkt1;
pkt_t pkt2;
initial begin
foreach (pkt1[i]) pkt1[i] = i * 16;
pkt2 = pkt1; // whole-array copy using the type name
$display("addr = 0x%08h", base_addr);
$display("data = 0x%08h", reg_val);
$display("pkt2[3] = 0x%02h", pkt2[3]); // 0x30
txn_count++;
$display("count = %0d", txn_count);
$finish;
end
endmoduleExample 2 — Intermediate: Shared Bus Types Package
// ── bus_types_pkg.sv ─────────────────────────────────────────────
package bus_types_pkg;
// ── Fundamental bus widths ────────────────────────────────────
typedef logic [31:0] addr_t; // 32-bit address bus
typedef logic [31:0] data_t; // 32-bit data bus
typedef logic [3:0] strb_t; // byte enables
typedef logic [7:0] tid_t; // transaction ID
typedef logic [1:0] resp_t; // AXI response: OKAY/EXOKAY/SLVERR/DECERR
// ── Composite types ───────────────────────────────────────────
typedef struct packed {
tid_t id;
addr_t addr;
data_t data;
strb_t strb;
resp_t resp;
} axi_txn_t;
// ── Constants using the types ─────────────────────────────────
const addr_t PERIPH_BASE = 32'hA000_0000;
const addr_t MEM_BASE = 32'h0000_0000;
// ── Array types ───────────────────────────────────────────────
typedef data_t reg_file_t [32]; // 32-entry register file
typedef data_t dyn_data_t []; // dynamic data array (for bursts)
endpackage
// ── axi_driver.sv — import and use the package ────────────────────
import bus_types_pkg::*;
class axi_driver;
task automatic write(
input addr_t addr,
input data_t data,
input strb_t strb,
input tid_t id,
output resp_t resp
);
$display("[DRV] WRITE tid=%0h addr=0x%08h data=0x%08h strb=%04b",
id, addr, data, strb);
resp = 2'b00; // OKAY
endtask
endclassExample 3 — Verification: Parameterized Module with Local typedef
// Parameterized FIFO — internal type adapts to parameters
module sync_fifo #(
parameter int DATA_W = 8,
parameter int DEPTH = 16,
parameter int ADDR_W = $clog2(DEPTH)
) (
input logic clk, rst_n,
input logic wr_en, rd_en,
input logic [DATA_W-1:0] wr_data,
output logic [DATA_W-1:0] rd_data,
output logic full, empty
);
// Local typedefs — adapt to module parameters
typedef logic [DATA_W-1:0] data_t;
typedef logic [ADDR_W-1:0] ptr_t;
data_t mem [DEPTH]; // storage using data_t
ptr_t wr_ptr = 0; // write pointer using ptr_t
ptr_t rd_ptr = 0;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
wr_ptr <= 0; rd_ptr <= 0;
end else begin
if (wr_en && !full) begin mem[wr_ptr] <= wr_data; wr_ptr <= wr_ptr + 1; end
if (rd_en && !empty) begin rd_ptr <= rd_ptr + 1; end
end
assign rd_data = mem[rd_ptr];
assign full = (wr_ptr + 1 == rd_ptr);
assign empty = (wr_ptr == rd_ptr);
endmoduleExample 4 — Forward Declaration for Circular Class References
// Problem: Class A references Class B, and Class B references Class A
// Without forward declaration, whichever comes first fails to compile
// SOLUTION: forward declare both classes first
typedef class axi_monitor;
typedef class axi_scoreboard;
class axi_monitor;
axi_scoreboard sb; // OK: scoreboard declared above even if not defined yet
task capture();
// ... capture logic ...
// sb.check(...)
endtask
endclass
class axi_scoreboard;
axi_monitor mon; // OK: monitor forward-declared
int mismatch_cnt = 0;
function void check(logic [31:0] exp, got);
if (exp !== got) mismatch_cnt++;
endfunction
endclass
module tb_forward_decl;
initial begin
axi_monitor mon = new();
axi_scoreboard sb = new();
mon.sb = sb; // connect after construction
$display("Forward declaration resolved successfully");
$finish;
end
endmoduleSimulation and Synthesis Behavior
No Runtime Cost — Pure Compile-Time Feature
typedef has zero impact on simulation performance or synthesized hardware. It is resolved entirely at compile time — the simulator and synthesis tool see the underlying type. There is no type tag, no runtime type checking, no additional memory. It is purely a pre-processor level naming substitution, similar to a C typedef.
Scope Rules
A typedef is visible only within the scope where it is declared: module, class, package, or compilation unit. Declaring typedefs in a package and importing that package with import pkg::* is the standard pattern for sharing types across an entire design. Without a package, each file needs its own typedef — meaning you're back to the multi-file search-replace problem.
| Scope | Visibility | Recommended for |
|---|---|---|
| Package-level | Anywhere the package is imported | Project-wide bus types — the standard approach |
| Module-level | Inside the module only | Local types for parameterized modules |
| Class-level | Inside the class only | Class-specific internal types |
| Compilation unit | All files in the same compilation unit | Small projects; not recommended for large designs |
Where typedef Shapes Real Verification Architecture
// ── 1. TRANSACTION CLASS WITH TYPED FIELDS ─────────────────────────
import bus_types_pkg::*;
class axi_seq_item;
rand addr_t addr;
rand data_t data;
rand strb_t strb;
rand tid_t id;
rand resp_t resp;
// Field names document their purpose; no raw logic[N:0] clutter
endclass
// ── 2. SCOREBOARD WITH TYPED REFERENCE MODEL ──────────────────────
class reg_scoreboard;
reg_file_t exp_regs; // 32-entry data_t array
reg_file_t got_regs;
function void compare_all();
foreach (exp_regs[i])
if (exp_regs[i] !== got_regs[i])
$error("reg[%0d] mismatch: exp=%h got=%h", i, exp_regs[i], got_regs[i]);
endfunction
endclass
// ── 3. FUNCTION THAT WORKS WITH PROTOCOL TYPE ─────────────────────
function automatic string format_txn(input axi_txn_t t);
return $sformatf("id=%h addr=0x%08h data=0x%08h resp=%02b",
t.id, t.addr, t.data, t.resp);
endfunction
// ── 4. CONSTRAINED RAND WITH TYPED FIELDS ─────────────────────────
class burst_txn;
rand dyn_data_t data; // dynamic array of data_t
rand int unsigned len;
constraint data_sz { len inside {[1:16]}; data.size() == len; }
endclass
// ── 5. PORT DECLARATION WITH TYPED SIGNALS ────────────────────────
interface axi_if (input logic clk);
import bus_types_pkg::*;
addr_t awaddr; // typed port — width from typedef, not magic number
data_t wdata;
strb_t wstrb;
resp_t bresp;
endinterfaceBugs and Pitfalls With typedef
Bug 1 — typedef Doesn't Enforce Semantic Separation
typedef logic [31:0] addr_t;
typedef logic [31:0] data_t;
addr_t req_addr;
data_t wr_data;
// BUGGY: accidentally swapped — passes without any warning
task drive(input addr_t a, input data_t d);
/* ... */
endtask
drive(wr_data, req_addr); // swapped! Compiles and runs silently
// Both are logic[31:0] underneath — no type mismatch detected
// PREVENTION: code review + waveform check + naming conventions
// SV typedef cannot prevent this — it's a documentation tool, not a type enforcerBug 2 — typedef in Module Without Package: Not Shared
// BUGGY: typedef inside a module — not visible to other files
module dut;
typedef logic [31:0] addr_t; // only visible inside 'dut'
endmodule
class axi_driver;
addr_t req_addr; // COMPILE ERROR: addr_t not visible here
endclass
// CORRECT: define typedefs in a package
package types_pkg;
typedef logic [31:0] addr_t; // visible wherever types_pkg is imported
endpackage
import types_pkg::*;
class axi_driver_ok;
addr_t req_addr; // OK: imported from package
endclassBug 3 — Width Change Not Propagating: typedef in Wrong Scope
// types_pkg.sv has the canonical typedef
// package types_pkg;
// typedef logic [31:0] addr_t; ← needs to become [39:0]
// endpackage
// PROBLEM: one file has its own local typedef instead of importing
module old_module;
typedef logic [31:0] addr_t; // local override — doesn't use the package!
addr_t base = 32'hA000; // still 32-bit after package was updated to 40-bit
// Width mismatch bug: this module connects to updated 40-bit ports elsewhere
endmodule
// PREVENTION: never redeclare typedefs locally if they exist in a package
// Always import: "import types_pkg::*;" — don't copy-paste typedefs
module correct_module;
import types_pkg::*;
addr_t base = 40'hA0_0000_0000; // automatically 40-bit after package update
endmoduleInterview Questions
Beginner Level
Q1: What does typedef do in SystemVerilog? Does it create a new type?typedef creates a named alias for an existing type. It does not create a new type — the compiler treats the alias and the original as fully interchangeable. You can assign an addr_t to a data_t variable without any cast if both are aliases for the same underlying type. typedef is purely a compile-time naming mechanism with no runtime overhead. Q2: Where is the best place to put typedefs in a large verification project? In a shared package, imported by every file that needs the types. This is the standard professional pattern. The package acts as a single source of truth for type widths — a change propagates to all files automatically. Never duplicate typedefs across files; duplicate definitions diverge when widths change, creating silent mismatch bugs.
Intermediate Level
Q3: What is a forward declaration with typedef and when do you need it?typedef class my_class; declares a class name before its full definition. It is needed when two classes have a circular dependency — Class A contains a handle to Class B, and Class B contains a handle to Class A. Without forward declarations, the compiler sees the second reference before the class is defined and errors. Forward declarations tell the compiler "this name will be defined later, trust me." Both classes must be fully defined before any of their methods can be called.
Experienced Engineer Level
Q4: A project updates the address bus from 32 to 40 bits. The RTL and DUT were updated but the testbench still uses 32-bit addresses in some places. Why didn't the typedef change fix everything? Some testbench files either (1) had their own local typedef declarations instead of importing from the package, (2) used logic [31:0] directly without using the typedef at all, or (3) imported the package but also had local overrides that shadowed the updated typedef. The fix: audit every file for raw logic [31:0] address declarations and replace with addr_t. Enable linting rules that warn on undeclared type usage and prohibit local typedef redefinitions of package-level types.
Best Practices & Coding Guidelines
- All types in a package — Never scatter typedefs across individual files. One package (project_types_pkg) holds everything. Import everywhere with import pkg::*.
- Use _t suffix consistently — All type aliases end in _t: addr_t, data_t, txn_t. This instantly distinguishes type names from signal names in code reviews.
- Never duplicate across files — If a typedef exists in a package, never copy-paste it into a module or class. Always import. Duplicates diverge silently when widths change.
- Parameterized modules: local typedef — Inside parameterized modules, define local typedefs using parameters: typedef logic [DATA_W-1:0] data_t. This makes the code self-documenting and width-flexible simultaneously.
| Scenario | Correct approach | Avoid |
|---|---|---|
| Project-wide bus width | One typedef in a shared package | Duplicating in every file |
| Parameterized module internal type | Local typedef using parameter | Hardcoded widths inside module |
| Circular class references | typedef class forward declaration | Reordering classes (breaks other refs) |
| Array of structs in a TB | typedef struct {...} txn_t; txn_t q [$]; | Inline anonymous struct definition |
| Naming convention | addr_t, data_t, state_t | Mixed conventions like ADDR, DataType |
Summary
typedef is the simplest feature in this chapter but it has an outsized impact on code quality in large projects. A well-designed types package means width changes cost one edit, port lists are self-documenting, and type mismatches between RTL and TB are caught at compile time rather than hunting mismatch bugs in simulation.
- typedef creates an alias, not a new type. The compiler treats the alias and original as identical — no type enforcement between semantically different aliases.
- Put all typedefs in a shared package. This is the only way to guarantee width changes propagate everywhere.
- Use the
_tsuffix. It distinguishes type names from signal names instantly and consistently. - Local typedef in parameterized modules creates width-flexible internal types that adapt to module parameters.
- Forward declarations resolve circular class dependencies. Declare with
typedef classbefore the full definition.