A typedef binds a name to a type so the meaning is written once and the structure follows it. Change addr_t from 32 to 40 bits in one package line and every port, variable and queue using it follows.
The name also carries intent. addr_t tells a reader what a value is for, while logic [31:0] tells them only how wide it is. The discipline that follows: every type 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
// A width change edits ADDR_W once. Every addr_t in the
// DUT ports and the testbench follows it. Hardcoding
// 32 in one corner instead would truncate silently at 40.A parameter on a module header is part of its public contract: instantiations may override it with hash-parenthesis binding. A localparam is internal and cannot be overridden, which is where derived values belong.
Inside a package the distinction collapses, because package parameters can never be overridden. The convention still records intent: tunable-in-spirit values stay parameters, computed values become localparams. Never use defparam for overrides; it is deprecated action at a distance.
The fifo in the next section uses both: DEPTH and DATA_W are public parameters, and PTR_W is a localparam computed from DEPTH with $clog2, so no instantiation can make the two disagree.
DEPTH 64. What is PTR_W, and how many bits wide are wr_ptr and rd_ptr?PTR_W is 6, because 2 to the 6th is 64. The pointers are 7 bits: [PTR_W:0] adds one extra bit, which is how the fifo tells full from empty.
Widths defined once in a package flow into the interface, the DUT and the checker. A mismatch becomes impossible rather than merely unlikely, because nothing is written down twice.
The testbench below proves it three ways. All-ones fills exactly the package widths, a register with package-typed ports carries an address, and a 64-deep fifo override fills, holds and drains in order.
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;
endpackage
// DEPTH is a power of two of at least 2, so PTR_W is at least 1.
module fifo #(
parameter int DEPTH = 16, // public: the user may override
parameter int DATA_W = 32
)(
input logic clk,
input logic rst_n,
input logic wr_en,
input logic rd_en,
input logic [DATA_W-1:0] wdata,
output logic [DATA_W-1:0] rdata,
output logic full,
output logic empty
);
localparam int PTR_W = $clog2(DEPTH); // derived: tracks DEPTH, cannot be overridden
logic [DATA_W-1:0] mem [DEPTH];
logic [PTR_W:0] wr_ptr, rd_ptr; // extra bit tells full from empty
assign empty = (wr_ptr == rd_ptr);
assign full = (wr_ptr[PTR_W] != rd_ptr[PTR_W]) &&
(wr_ptr[PTR_W-1:0] == rd_ptr[PTR_W-1:0]);
assign rdata = mem[rd_ptr[PTR_W-1:0]];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr <= '0;
rd_ptr <= '0;
end else begin
if (wr_en && !full) begin
mem[wr_ptr[PTR_W-1:0]] <= wdata;
wr_ptr <= wr_ptr + 1;
end
if (rd_en && !empty) rd_ptr <= rd_ptr + 1;
end
end
endmodule
module addr_reg (
input logic clk,
input mem_pkg::addr_t din,
output mem_pkg::addr_t qout
);
always_ff @(posedge clk) qout <= din;
endmodule
PASS: one package sets widths and a 64-deep fifo fills, holds and drains in orderParameters can also carry types: parameter type T lets one body work with any payload the instantiator supplies. The sketch below shows the shape.
// snippet: one body, any payload type, bound at elaboration
module compare_m #(parameter type TXN_T = logic [31:0]) (
input TXN_T expected,
input TXN_T actual,
output logic match
);
assign match = (expected == actual);
endmodule
// instantiation picks the transaction type:
// compare_m #(.TXN_T(mem_txn_t)) u_cmp (.expected(e), .actual(a), .match(m));The pointers no longer track the depth, so full and empty misbehave. A derived value must be a localparam so no override can desynchronize it.
The extra-bit full and empty comparison only wraps correctly when the pointer space is exactly twice the depth. Other depths need a different flag scheme.
One way to write it; add it after the fifo module above:
// snippet: add after the fifo module in the listing above
module tb_pkt_fifo_v0;
logic clk = 0;
always #5 clk = ~clk;
logic rst_n = 0, wr_en = 0, rd_en = 0, full, empty;
logic [8:0] wdata, rdata; // {last, byte}
logic [8:0] pkt [0:3];
int errors = 0;
fifo #(.DEPTH(4), .DATA_W(9)) dut (.*); // PacketFIFO RTL v0
initial begin
pkt[0] = 9'h022; pkt[1] = 9'h0A5; pkt[2] = 9'h03C; pkt[3] = 9'h1BB;
@(negedge clk) rst_n = 1;
foreach (pkt[i]) begin
@(negedge clk);
wr_en = 1;
wdata = pkt[i];
end
@(negedge clk) wr_en = 0;
if (!full) errors++; // four entries: full
foreach (pkt[i]) begin
if (rdata !== pkt[i]) errors++;
$display("read %h last=%b", rdata[7:0], rdata[8]);
rd_en = 1;
@(negedge clk);
end
rd_en = 0;
if (!empty) errors++;
if (errors == 0) $display("PASS: fifo #(.DEPTH(4), .DATA_W(9)) carried the packet in order, full after 4, empty after");
else $fatal(1, "FAIL: %0d PacketFIFO v0 checks failed", errors);
$finish;
end
endmoduleRun with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. Icarus 13 (-g2012) runs them too, with the same output.
The PacketFIFO v0 step was run with the fifo module above it on both simulators and printed the PASS line fifo #(.DEPTH(4), .DATA_W(9)) carried the packet in order, full after 4, empty after.