SystemVerilog · Module 7
Parameter Overrides — defparam & # Notation
Named vs positional overrides, why defparam is deprecated, type-checked parameters, string-parameter pitfalls, and hierarchy propagation.
Module 7 · Page 7.5
Two mechanisms let you change a module's parameters at instantiation time. One is modern, safe, and widely used. The other is legacy and actively discouraged. This page covers both — so you can write one and read the other — together with the elaboration timeline, hierarchical propagation patterns, type-checked parameters, verification implications, five real debugging labs, and twelve interview-ready questions.
Two Ways to Override a Parameter
When you instantiate a module that has parameters, you can change the parameter values for that specific instance. SystemVerilog gives you two mechanisms:
#( )notation — the modern, recommended method. Parameters are overridden inline, right at the instantiation point using#(.PARAM(value)). Safe, readable, and supported everywhere.defparam(legacy) — the old Verilog method. Parameters are overridden from anywhere in the file, not at the instantiation site. Confusing, hard to maintain, and formally deprecated. Read-only skill — never write new code with it.
# ( ) Notation — The Modern Way
The #( ) block is placed between the module name and the instance name at the point of instantiation. It has the same two styles as port connections: positional (values in order) and named (explicit parameter names). Named is strongly preferred for exactly the same reasons as named port connections — safe when parameters are reordered or added.
// Module being instantiated:
// module fifo #(parameter int DATA_WIDTH=8, parameter int DEPTH=16) (...);
// ════ NAMED # override — ALWAYS USE THIS ══════════════════════════
// Syntax: module_name #(.PARAM1(val1), .PARAM2(val2)) instance_name (ports);
fifo #(
.DATA_WIDTH(32), // override DATA_WIDTH to 32
.DEPTH (256) // override DEPTH to 256
) u_cpu_fifo (
.clk (clk),
.rst_n(rst_n)
// ... other ports
);
// ════ POSITIONAL # override — FRAGILE (avoid) ══════════════════════
// Values must be in the exact order the parameters were declared
fifo #(32, 256) u_pos_fifo (...);
// ↑DATA_WIDTH ↑DEPTH — order must match module definition
// ════ PARTIAL override — only one parameter, rest stay at default ══
fifo #(.DEPTH(64)) u_small_fifo (...);
// DATA_WIDTH uses default (8); only DEPTH is overridden to 64
// ════ Use default — no # block at all ══════════════════════════════
fifo u_default_fifo (...);
// Both DATA_WIDTH=8 and DEPTH=16 use their declared defaultsOverride with Expressions and Constants
// ── Top-level constants drive sub-module parameters ─────────────
localparam int SYS_DATA_W = 32;
localparam int CACHE_DEPTH = 512;
fifo #(
.DATA_WIDTH(SYS_DATA_W), // constant drives parameter
.DEPTH (CACHE_DEPTH / 2) // expression drives parameter
) u_cache_fifo (...);
// ── String parameter override for mode selection ─────────────────
// module uart_tx #(parameter string MODE = "SYNC") (...);
uart_tx #(.MODE("ASYNC")) u_async_uart (...); // override to async mode
uart_tx #(.MODE("SYNC")) u_sync_uart (...); // use default (same value)
// ── Passing a parent's own parameter down to a child ─────────────
// (covered in detail in Section 5 — Hierarchical Override)
module cpu_core #(parameter int BUS_WIDTH = 32) (...);
adder_n #(.WIDTH(BUS_WIDTH)) u_alu_add (...); // pass-through
endmoduledefparam (deprecated) — Legacy Verilog Method
defparam was the original Verilog mechanism for overriding parameters. Instead of putting the override at the instantiation point, defparam is a separate statement that can appear anywhere in the module hierarchy — even in a completely different file.
// ── defparam syntax ──────────────────────────────────────────────
// defparam hierarchical_path.PARAM_NAME = value;
module chip_top;
// The instance — NO parameter override at the instantiation site
fifo u_fifo (
.clk (clk),
.rst_n(rst_n)
);
// defparam sets the parameters — SEPARATE from the instance
defparam u_fifo.DATA_WIDTH = 32; // instance_name.PARAM_NAME = value
defparam u_fifo.DEPTH = 256;
// ── defparam can use hierarchical paths across multiple levels ──
// This is the REAL danger — modifying a parameter inside a
// sub-sub-module from the top level, invisibly:
defparam u_cpu.u_alu.u_adder.WIDTH = 64; // ← buried 3 levels deep!
endmoduleHow Parameter Override Works: Elaboration
Parameter overrides happen at elaboration — the compile phase where the tool builds the full module hierarchy before simulation or synthesis begins. No hardware exists yet; parameters are pure numbers. Understanding this timeline explains several rules that otherwise feel arbitrary.
Hierarchical Parameter Passing
In real designs, a top-level module defines a system-wide constant (like BUS_WIDTH = 32) and passes it down through every level of the hierarchy. Each intermediate module must accept it as a parameter and forward it to its children.
// ── Level 0: chip_top — defines the system constant ─────────────
module chip_top;
localparam int BUS_WIDTH = 64; // single source of truth
cpu_core #(
.BUS_WIDTH(BUS_WIDTH) // forward to child
) u_cpu (...);
endmodule
// ── Level 1: cpu_core — accepts and forwards the parameter ───────
module cpu_core #(
parameter int BUS_WIDTH = 32 // default 32, overridden to 64 above
) (...);
alu_unit #(.BUS_WIDTH(BUS_WIDTH)) u_alu (...); // forward down again
reg_file #(.BUS_WIDTH(BUS_WIDTH)) u_rf (...); // same parameter
endmodule
// ── Level 2: alu_unit — uses the parameter, derives localparams ──
module alu_unit #(
parameter int BUS_WIDTH = 32 // receives 64 at runtime
) (
input logic [BUS_WIDTH-1:0] a, b,
output logic [BUS_WIDTH-1:0] result
);
localparam int HALF_W = BUS_WIDTH / 2; // 32 — auto-computed
assign result = a + b;
endmoduleType-Checked Parameters — Catching Mistakes at Elaboration
When you declare a parameter with an explicit type (parameter int WIDTH = 8), the tool checks that the override value is compatible with that type. This catches entire classes of bugs at elaboration — before simulation even starts.
module fifo #(
parameter int DATA_WIDTH = 8, // must be a positive integer
parameter int unsigned DEPTH = 16, // must be unsigned — no negatives
parameter string IMPL = "LUT" // must be a string
) (...);
// ── Valid overrides ───────────────────────────────────────────────
fifo #(.DATA_WIDTH(32), .DEPTH(1024), .IMPL("BRAM")) u_ok (...);
// ── Tool will warn/error on these ────────────────────────────────
fifo #(.DATA_WIDTH(-8)) u_neg (...); // ❌ negative width — nonsensical
fifo #(.DEPTH(3.14)) u_real (...); // ❌ real value to int — truncated
fifo #(.IMPL(42)) u_int (...); // ❌ integer to string — type error
// ── Parameter constraints with assertions (extra safety) ─────────
module safe_fifo #(
parameter int DATA_WIDTH = 8,
parameter int DEPTH = 16
) (...);
// Catch illegal parameter values at elaboration time
initial begin
if (DATA_WIDTH < 1 || DATA_WIDTH > 128)
$fatal(1, "DATA_WIDTH must be 1..128, got %0d", DATA_WIDTH);
if (DEPTH < 2 || (DEPTH & (DEPTH-1)) != 0)
$fatal(1, "DEPTH must be a power of 2 >= 2, got %0d", DEPTH);
end
endmodule
// Now this crashes at time-0 with a clear message:
safe_fifo #(.DEPTH(100)) u_bad (...);
// Fatal: "DEPTH must be a power of 2 >= 2, got 100"String Parameters for Mode Selection
A parameter string lets you select between named behavioural modes at elaboration time — much more readable than a magic integer. Use this when a module has fundamentally different implementations depending on a configuration choice.
module ram_model #(
parameter int DATA_WIDTH = 32,
parameter int DEPTH = 256,
parameter string IMPL = "BEHAVIORAL" // "BEHAVIORAL" | "SRAM" | "ROM"
) (
input logic clk, we,
input logic [$clog2(DEPTH)-1:0] addr,
input logic [DATA_WIDTH-1:0] wdata,
output logic [DATA_WIDTH-1:0] rdata
);
generate
if (IMPL == "BEHAVIORAL") begin : gen_behav
logic [DATA_WIDTH-1:0] mem[0:DEPTH-1];
always_ff @(posedge clk) begin
if (we) mem[addr] <= wdata;
rdata <= mem[addr];
end
end else if (IMPL == "ROM") begin : gen_rom
// ROM: ignore writes, always read from pre-loaded array
logic [DATA_WIDTH-1:0] rom_mem[0:DEPTH-1];
initial $readmemh("rom_init.hex", rom_mem);
always_ff @(posedge clk) rdata <= rom_mem[addr];
end else begin : gen_unknown
initial $fatal(1, "ram_model: unknown IMPL='%s'", IMPL);
end
endgenerate
endmodule
// ── Select the mode at instantiation ─────────────────────────────
ram_model #(.DATA_WIDTH(32), .DEPTH(1024), .IMPL("BEHAVIORAL")) u_sim_ram (...);
ram_model #(.DATA_WIDTH(32), .DEPTH(256), .IMPL("ROM")) u_boot_rom(...);defparam vs # Notation — Full Comparison
| Aspect | # Notation (Named) | defparam (Legacy) |
|---|---|---|
| Where is it written? | At the instantiation point — inline | Anywhere in the module or file hierarchy |
| Self-documenting? | Yes — visible with the instance | No — override may be in a different file |
| Breaks on rename? | No — uses parameter name, not instance path | Yes — uses full hierarchical path; rename breaks silently |
| Named parameter support? | Yes — .PARAM(val) | No — uses hierarchical path |
| Tool support | Universal — all tools fully support it | Declining — some tools warn or restrict it |
| IEEE 1800 status | Primary, preferred mechanism | Formally deprecated since IEEE 1800-2012 |
| Use in new code? | Yes — always | Never write it; read it in legacy code only |
| Type checking? | Yes — with typed parameters | Limited — no type enforcement |
Common Mistakes
// ════ MISTAKE 1: Using positional #() when order is non-obvious ══
fifo #(32, 256) u_bad (...); // ❌ which is DATA_WIDTH, which is DEPTH?
// ✅ FIX: always use named
fifo #(.DATA_WIDTH(32), .DEPTH(256)) u_good (...); // ✅ obvious
// ════ MISTAKE 2: Forgetting the # block — using wrong defaults ═══
// Module: parameter int DATA_WIDTH = 8, DEPTH = 16
fifo u_oops (...); // ❌ forgot # — both use tiny defaults
// ✅ FIX: always write # even when only overriding one param
fifo #(.DEPTH(1024)) u_ok (...); // ✅ DEPTH=1024, DATA_WIDTH stays at 8
// ════ MISTAKE 3: Trying to override a localparam ════════════════
// Inside fifo: localparam int PTR_W = $clog2(DEPTH);
fifo #(.PTR_W(10)) u_bad2 (...); // ❌ Error: PTR_W is localparam — cannot override
// ✅ FIX: override the parameter it derives from
fifo #(.DEPTH(1024)) u_good2 (...);// ✅ PTR_W = $clog2(1024) = 10 — auto-computed
// ════ MISTAKE 4: Not validating parameter values ═════════════════
fifo #(.DEPTH(100)) u_bad3 (...); // ❌ 100 is not a power of 2 — pointer wraps wrong
// ✅ FIX: add $fatal check inside the module (see Section 6)Quick Reference — Parameter Override Cheat Sheet
// ── Named # override — ALWAYS USE THIS ────────────────────────
mod #(.P1(val1), .P2(val2)) u_inst (...);
// ── Partial override — only one param, rest use defaults ───────
mod #(.P2(val2)) u_inst (...);
// ── Use all defaults — no # block needed ───────────────────────
mod u_inst (...);
// ── Override with expression or constant ───────────────────────
mod #(.WIDTH(SYS_W), .DEPTH(CACHE_D / 2)) u_inst (...);
// ── Forward parent's parameter to child ────────────────────────
module parent #(parameter int W = 32) (...);
child #(.WIDTH(W)) u_c (...); // pass-through
endmodule
// ── Validate parameters at elaboration ─────────────────────────
initial begin
if (WIDTH < 1) $fatal(1, "WIDTH must be >= 1");
end
// ── defparam (LEGACY — read only) ──────────────────────────────
defparam u_inst.PARAM_NAME = value; // ← never write new code like this
// ── localparam cannot be overridden ────────────────────────────
// localparam int HALF = WIDTH/2; ← not in #() block — cannot be overriddenVerification Usage — Overrides Across the DUT/TB Boundary
Every override that varies a DUT width must be matched by an identical override on every TB component that talks to it. A bus driver hand-rolled at 32 bits cannot meaningfully test a DUT instantiated at 64 bits — but nothing in SystemVerilog stops you from connecting them. The cleanest production pattern is to feed a single config package to both the DUT and the TB stack.
// ── soc_widths_pkg.sv — the single source of truth ───────────
package soc_widths_pkg;
localparam int AXI_AW = 32;
localparam int AXI_DW = 128; // SoC-wide AXI data width
localparam int APB_AW = 16;
localparam int APB_DW = 32;
endpackage
// ── DUT instantiation — override from the package ───────────
import soc_widths_pkg::*;
axi_master_bfm #(
.AW(AXI_AW),
.DW(AXI_DW)
) u_dut ( .axi(axi_if_h), .clk, .rst_n );
// ── UVM agent + interface — same override values ──────────
import soc_widths_pkg::*;
axi_if #(.AW(AXI_AW), .DW(AXI_DW)) axi_if_h(.aclk(clk));
axi_driver #(.AW(AXI_AW), .DW(AXI_DW)) drv;
axi_monitor #(.AW(AXI_AW), .DW(AXI_DW)) mon;
axi_scoreboard #(.AW(AXI_AW), .DW(AXI_DW)) scb;
// Bump AXI_DW to 256 in the package → every override picks it up.
// No hunt-and-replace across 40+ instantiation sites.// Drop-in safety net: at TB build time, prove the DUT and TB
// were elaborated with matching widths. Catches mismatched
// overrides before stimulus starts.
initial begin
if ($bits(u_dut.axi.wdata) != $bits(drv.vif.wdata)) begin
$fatal(1, "AXI_DW mismatch: DUT=%0d TB=%0d",
$bits(u_dut.axi.wdata), $bits(drv.vif.wdata));
end
`uvm_info("WIDTH_OK", $sformatf("AXI_DW = %0d on both sides",
$bits(drv.vif.wdata)), UVM_LOW)
endSimulation Behavior — When the Override Takes Effect
Overrides are consumed during elaboration, before time 0. By the time the simulator's first Active region fires, every parameter has its final integer value baked into the netlist. There is no "parameter change event" at any simulation time — the value is constant from time 0 forward, and identical for every initial block that reads it.
module ram #(
parameter int AW = 8,
parameter int DW = 32
) ( input logic clk );
initial begin
$info("%m elaborated: AW=%0d DW=%0d (depth=%0d)",
AW, DW, 1 << AW);
end
endmodule
// Two overrides — each instance reports independently at time 0
ram #(.AW(10), .DW(64)) u_l1 (.clk);
ram #(.AW(14), .DW(128)) u_l2 (.clk);
// Expected $info output at time 0:
// [INFO] top.u_l1 elaborated: AW=10 DW=64 (depth=1024)
// [INFO] top.u_l2 elaborated: AW=14 DW=128 (depth=16384)
//
// Add this pattern at every parameterised module's initial — the elab log
// becomes a self-documenting record of what every instance ended up with.Waveform Analysis — How Override Values Become Visible Signals
A parameter is invisible in the waveform viewer (it's a constant, not a signal). What is visible is everything sized by it: the bit width of the address bus, the rollover point of a counter, the wrap point of a FIFO pointer. The same RTL source produces visibly different waveforms for different overrides — and that visible difference is your fastest check that the override took.
module ram #(
parameter int AW = 4,
parameter int DW = 8
) (
input logic clk, we,
input logic [AW-1:0] addr,
input logic [DW-1:0] wd,
output logic [DW-1:0] rd
);
logic [DW-1:0] mem [0:(1<<AW)-1];
always_ff @(posedge clk) begin
if (we) mem[addr] <= wd;
rd <= mem[addr];
end
endmodule
ram #(.AW(4), .DW(8)) u_small (...); // 16 × 8 — 16-byte buffer
ram #(.AW(10), .DW(32)) u_large (...); // 1024 × 32 — 4 KiBBoth instances driven by the same address generator counting up from 0. The override changes what you see on the address bus and what the RAM does at rollover.
cycle 0 1 2 3 ... 14 15 16 17 ...
u_small (AW=4, DW=8 — 4-bit addr, 8-bit data):
addr 00 01 02 03 ... 0E 0F 00 01 ← wraps every 16
mem write [0] [1] [2] [3] ... [14] [15] [0] [1]
rd[7:0] 8-bit value ← narrow data bus
u_large (AW=10, DW=32 — 10-bit addr, 32-bit data):
addr 000 001 002 003 ... 00E 00F 010 011 ← wraps every 1024
mem write [0] [1] [2] [3] ... [14] [15] [16] [17]
rd[31:0] 32-bit value ← wide data bus
Observation: identical address generator, identical clock —
• the two address buses display different bit widths in the viewer
• the two write activities target different memory depths
• u_small's write at cycle 16 lands on address 0 (rollover);
u_large's write at cycle 16 lands on address 16 (continuing).Industry Insights — Override Practices in Production Teams
Debugging Academy — 5 Real Parameter-Override Bugs
Each lab is a real failure mode that shipped at least once. Buggy code, the symptom in simulation or silicon, why it happened, and the fix.
defparam in Legacy File Survives the Build — Tool-Dependent Netlist
CROSS-FILE OVERRIDE// In legacy_overrides.sv (someone added this in 2008, no one removed it):
defparam top.u_soc.u_dma.u_buf.DEPTH = 256;
// In modern top.sv:
dma_buf #(.DEPTH(1024)) u_buf (...);
// Which value wins? Tool-dependent.RTL simulation on VCS shows DEPTH=1024. Regression on Questa shows DEPTH=256. Synthesis on Design Compiler picks one; Genus picks the other. Gate-level simulation mismatches RTL simulation. The team spends a week blaming the netlist compiler before finding the legacy defparam.
IEEE 1364 leaves resolution order between a defparam and an in-place named override implementation-defined. Different tools resolve in different orders. The two parameter mechanisms are not equivalent — they compete, and the tool decides who wins.
Delete every defparam in the codebase. Add a lint rule that fails CI on any new occurrence (Spyglass defparam_usage, Verilator -Wfatal-DEFPARAM). For each removed defparam, replace it with a named #() override at the original instantiation site. No exceptions, including in legacy code carved out of the build — the lint rule covers all source the tool reads.
Positional Override Drift After Parameter Reorder
POSITIONAL BREAKAGE// fifo.sv — original
module fifo #(
parameter int DATA_WIDTH = 8,
parameter int DEPTH = 16
) (...);
// Every parent uses positional override:
fifo #(32, 256) u_a (...); // DATA_WIDTH=32, DEPTH=256
// Six months later, fifo.sv refactored:
module fifo #(
parameter int DEPTH = 16, // ← reordered to "logical" position
parameter int DATA_WIDTH = 8
) (...);
// 40 parent instantiations still use:
fifo #(32, 256) u_a (...); // NOW: DEPTH=32, DATA_WIDTH=256 — silently invertedAll 40 FIFOs now operate at unintended depth and width. Tests that previously passed start hitting overflow or width-mismatch warnings. Bug surfaces unevenly because some FIFOs were "close enough" (e.g., 32 wide × 32 deep instead of 256 wide × 32 deep is still functional for short-lived data).
Positional override binds by index, not by name. A module-level reorder silently rewires every positional override. SystemVerilog's parameter naming isn't part of the contract when the caller uses positional binding.
Convert every override to named form. fifo #(.DATA_WIDTH(32), .DEPTH(256)) u_a (...). Add a lint rule rejecting positional parameter overrides (Spyglass parameter_overrides_positional). Refactor the codebase once; never see this bug again.
Override Typo — Module Silently Uses Default
SILENT DEFAULT// fifo module declares DEPTH
module fifo #(parameter int DEPTH = 16) (...);
// Parent typos the override:
fifo #(.DEPHT(1024)) u_buf (...);
// ^^^^^ should be DEPTH
// Most tools warn and proceed with default DEPTH=16.FIFO is 16 deep instead of 1024 deep. Backpressure on the producer kicks in after just 16 transactions — looks like a throughput regression. Engineering spends hours instrumenting the producer/consumer side before checking the FIFO parameter resolution.
SystemVerilog allows unknown parameter names in named override lists with at most a warning, not an error. Most tools have the warning suppressed because harmless cases produce thousands of them (e.g., wildcard overrides for unknown params).
Promote unknown-parameter-override from warning to error in CI: vcs +lint=PCWM -error=PCWM-L, Questa -pedanticerrors, Xcelium -LICQUEUE -wprintexit. Once enforced, the typo cannot silently ship.
Hierarchy Break — Parameter Stops Propagating Mid-Tree
PROPAGATION GAPmodule soc_top;
cpu_subsystem #(.DATA_W(128)) u_cpu (...);
endmodule
module cpu_subsystem #(parameter int DATA_W = 32) (...);
// Forgets to pass DATA_W down:
l1_cache u_l1 (...); // l1_cache.DATA_W = its default (often 64)
endmodule
module l1_cache #(parameter int DATA_W = 64) (...); // DATA_W never updatedCPU bus runs 128 bits, but L1 cache is 64 bits internally. The synthesis tool inserts byte-enable trimming at the boundary; gate-sim shows half the cache lines silently lose their upper 64 bits. Silicon validation finds the corruption on the most aggressive memory test.
Parameter propagation through hierarchy is manual. Each instantiation must explicitly pass parameters down; SystemVerilog does not inherit them automatically. A skipped propagation step truncates the override silently.
module cpu_subsystem #(parameter int DATA_W = 32) (...);
l1_cache #(.DATA_W(DATA_W)) u_l1 (...); // explicit propagation
endmoduleBetter still: source DATA_W from a shared package (soc_widths_pkg::DATA_W) as the default for every module's local parameter. Propagation gaps still happen, but the package default keeps every module in sync without manual passthrough.
String Parameter Override — Case Mismatch Picks Wrong Variant
CASE SENSITIVITYmodule reset_sync #(
parameter string RESET_TYPE = "SYNC" // or "ASYNC"
) (...);
generate
if (RESET_TYPE == "ASYNC") begin : async_reset
always_ff @(posedge clk or negedge rst_n) ...
end else begin : sync_reset
always_ff @(posedge clk) ...
end
endgenerate
endmodule
// Parent override:
reset_sync #(.RESET_TYPE("async")) u_rst (...); // lowercase typo!"async" does not equal "ASYNC", so the generate if falls through to the synchronous branch. The block uses synchronous reset where the integration plan said asynchronous. Power-on reset (which is asynchronous from the PMIC) doesn't reach the flops until the clock starts — which it can't, because the PLL is in reset. Deadlock at power-on.
String comparisons in SystemVerilog are case-sensitive. There is no implicit normalisation. The override is "technically valid" (string type, valid value) but doesn't match the magic strings the module compares against.
Inside the module, normalise the string before comparing (RESET_TYPE.tolower() == "async" — SystemVerilog string methods), or add an elaboration-time assertion that rejects any value outside the valid set: initial assert (RESET_TYPE inside {"SYNC", "ASYNC"}) else $fatal("Invalid RESET_TYPE: %s", RESET_TYPE);. Best practice: prefer enum-typed parameters over string parameters wherever possible, because enums catch the mismatch at elaboration.
Interview Q&A — 12 Questions on Parameter Overrides
Drawn from real interviews. Try to answer each before reading the response.
#() notation overrides parameters at the instantiation site, locally bound to one instance. defparam overrides from anywhere in the hierarchy by referencing the target instance by its full hierarchical path. The two mechanisms are not equivalent — they compete, and the IEEE LRM does not specify the resolution order if both apply to the same instance, so the elaborated design can differ between tools. Use named #() always; never use defparam.
Best Practices — Override Rules to Walk Away With
- Always use named overrides.
fifo #(.DEPTH(256))— neverfifo #(256). Named override is immune to parameter reorder. - Never use
defparam. Lint rule fails CI on any occurrence. Tool-dependent resolution order makesdefparama tape-out risk. - Promote unknown-parameter warning to error.
vcs +lint=PCWM -error=PCWM-L, Questa-pedanticerrors. Typos silently using defaults is the #1 override bug. - Source defaults from a shared package.
parameter int DATA_W = soc_widths_pkg::AXI_DATA_W. One change in the package propagates everywhere. - Propagate parameters explicitly at every hierarchy level. Child
#(.W(W))at everymodule ... #() ()instantiation. Missing propagation is silent truncation. - Add elaboration-time assertions for parameter invariants.
initial assert (DEPTH > 0) else $fatal(...);. Catch invalid overrides before time 0. - Prefer enum over string for mode parameters.
typedef enum {SYNC, ASYNC} mode_e. Eliminates case-sensitivity and typo bugs by construction. - Document the override contract per parameter. Legal range, dependencies, silicon-impact comment above each
parameterdeclaration. - Add a width-sanity initial in the TB. Compare
$bits()of DUT and TB signals at time 0;$fatalon mismatch. Catches missed override propagation before stimulus starts. - Run STA per shipped configuration. Each override produces a different netlist with a different critical path. A passing timing report on one width tells you nothing about the others.