SystemVerilog · Module 6
Default Argument Values
Default values for function/task arguments, the ordering rule, named-argument calls, defaults from parameters, and API design patterns.
Module 6 · Page 6.6
Default argument values turn a complex 8-parameter task into something you can call with just one argument for the common case — and still have full control when you need it. This is how you write APIs that are both powerful and easy to use.
What Are Default Argument Values?
When you declare a task or function argument, you can give it a default value using an = sign. If the caller omits that argument, the default value is used automatically. If the caller provides a value, the provided value overrides the default.
This is the same concept as default function parameters in Python, C++, or Java — and it is equally transformative for SystemVerilog testbench APIs. A single protocol driver task can serve all test scenarios with minimal typing for the common case, while remaining fully configurable for edge cases.
Basic Syntax — How to Declare Defaults
Add = default_value after the argument name in the argument list. The default can be a literal, a constant, or an expression involving other arguments or parameters.
// ── Function with default arguments ──────────────────────────────
function automatic logic [7:0] calc_crc (
input logic [7:0] data, // ← required: no default
input logic [7:0] poly = 8'hD5, // ← optional: default polynomial
input logic [7:0] init = 8'hFF // ← optional: default init value
);
return data ^ poly ^ init; // simplified
endfunction
// ── Calling with defaults ──────────────────────────────────────────
calc_crc(8'hAA); // uses poly=0xD5, init=0xFF
calc_crc(8'hAA, 8'h07); // uses poly=0x07, init=0xFF
calc_crc(8'hAA, 8'h07, 8'h00); // all three provided
// ── Task with multiple defaults ───────────────────────────────────
task automatic apb_write (
input logic [31:0] addr, // required
input logic [31:0] data, // required
output logic err, // required (output can't have default)
input int timeout = 1000, // optional: 1000-cycle timeout
input bit verbose = 0, // optional: silent by default
input bit check = 1 // optional: check SLVERR by default
);
int cnt = 0;
@(posedge clk); psel = 1; paddr = addr; pwdata = data;
@(posedge clk); penable = 1;
@(posedge clk);
while (!pready && cnt < timeout) begin @(posedge clk); cnt++; end
err = pslverr;
if (verbose) $display("APB write 0x%h = 0x%h err=%b", addr, data, err);
if (check && err) $error("APB SLVERR on write to 0x%h", addr);
psel = 0; penable = 0;
endtask
// ── All three calling styles ───────────────────────────────────────
logic e;
apb_write(32'h0, 32'hFF, e); // timeout=1000, silent, check
apb_write(32'h0, 32'hFF, e, 500); // timeout=500, silent, check
apb_write(32'h0, 32'hFF, e, 500, 1); // timeout=500, verbose, check
apb_write(32'h0, 32'hFF, e, 500, 1, 0); // timeout=500, verbose, no-checkThe Ordering Rule — Defaults Must Come Last
Arguments with default values must be placed after all required arguments in the list. You cannot have a required argument after a default one — the compiler cannot determine which argument to apply when the caller omits a middle one.
// ❌ WRONG: required arg after default arg
task automatic bad_order (
input logic [31:0] addr,
input bit verbose = 0, // default
input logic [31:0] data // ❌ required AFTER default — illegal!
);
endtask
// ✅ CORRECT: all required args before all default args
task automatic good_order (
input logic [31:0] addr, // required ─┐
input logic [31:0] data, // required │ all required first
output logic err, // required ─┘
input bit verbose = 0, // optional ─┐
input int timeout = 1000 // optional │ defaults last
); // ─┘
endtaskNamed Argument Passing — Skip to Any Default
SystemVerilog allows you to name arguments when calling a task or function — just like named port connections on a module instantiation. This lets you override a specific default argument without providing all the ones before it.
// Task declaration
task automatic apb_write (
input logic [31:0] addr,
input logic [31:0] data,
output logic err,
input int timeout = 1000,
input bit verbose = 0,
input bit check = 1
);
endtask
logic e;
// ── Positional (must provide all preceding args) ──────────────────
apb_write(32'h0, 32'hFF, e, 1000, 1); // have to spell out timeout to get verbose
// ── Named (jump directly to any argument) ─────────────────────────
apb_write(.addr(32'h0), .data(32'hFF), .err(e), .verbose(1));
// ↑ timeout and check use defaults — verbose overridden to 1
apb_write(.addr(32'h0), .data(32'hFF), .err(e), .check(0));
// ↑ jump straight to 'check' — timeout=1000, verbose=0 by default
apb_write(.addr(32'h0), .data(32'hFF), .err(e), .timeout(500), .check(0));
// ↑ override timeout and check, verbose stays at default
// ── Named arguments can be in any order ───────────────────────────
apb_write(
.err (e),
.data (32'hFF), // order doesn't matter with named args
.addr (32'h0),
.verbose (1)
); // ✅ legal — same as the positional version abovePractical Examples — Real Testbench APIs
Here are four real-world patterns where default arguments dramatically simplify testbench code.
// ── Pattern 1: Wait with configurable timeout ─────────────────────
task automatic wait_for_done (
input int timeout_cyc = 10_000 // almost always fine with 10k cycles
);
int cnt = 0;
while (!done && cnt < timeout_cyc) begin
@(posedge clk); cnt++;
end
if (cnt == timeout_cyc) $fatal(1, "Timeout waiting for done");
endtask
wait_for_done(); // 10k cycle timeout
wait_for_done(500); // strict 500-cycle timeout for fast path
wait_for_done(100_000); // long timeout for slow peripheral
// ── Pattern 2: UART with configurable baud and stop bits ──────────
task automatic uart_send (
input logic [7:0] data,
input int baud_div = 434, // 115200 at 50 MHz
input int stop_bits = 1, // standard: 1 stop bit
input bit parity_en = 0 // no parity by default
);
// start bit
tx = 0; repeat(baud_div) @(posedge clk);
// data bits
for (int i = 0; i < 8; i++) begin
tx = data[i]; repeat(baud_div) @(posedge clk);
end
// parity bit (optional)
if (parity_en) begin tx = ^data; repeat(baud_div) @(posedge clk); end
// stop bits
tx = 1; repeat(stop_bits * baud_div) @(posedge clk);
endtask
uart_send(8'h41); // 'A' at 115200, no parity
uart_send(8'h41, 5208); // 'A' at 9600 baud
uart_send(8'h41, 434, 2, 1); // 115200, 2 stop bits, with parity
// ── Pattern 3: Memory transaction helper ─────────────────────────
function automatic logic [31:0] read_reg (
input logic [11:0] addr,
input int offset = 0, // byte offset into register
input logic [3:0] mask = 4'hF // all 4 bytes active
);
return regfile[addr + offset] & {{8{mask[3]}},{8{mask[2]}},{8{mask[1]}},{8{mask[0]}}};
endfunction
logic [31:0] v = read_reg(12'hA00); // offset=0, all bytes
logic [31:0] b = read_reg(12'hA00, 4); // 4-byte offset
logic [31:0] m = read_reg(12'hA00, 0, 4'h3); // low 2 bytes only
// ── Pattern 4: $display wrapper with severity control ─────────────
function automatic void log (
input string msg,
input string level = "INFO", // INFO/WARN/ERROR/DEBUG
input bit show = 1 // suppress if 0 (e.g. debug build)
);
if (show)
$display("[%s @ %0t] %s", level, $time, msg);
endfunction
log("Boot complete"); // [INFO @ 0] Boot complete
log("Parity mismatch", "WARN"); // [WARN @ 200] Parity mismatch
log("Debug trace", "DEBUG", DEBUG_EN); // shown only if DEBUG_EN=1Default Values from Parameters and Expressions
A default value is not limited to a literal constant. It can be any compile-time expression — including module parameters, localparams, $clog2, or even other arguments declared earlier in the same list.
// ── Module-level constant used as default ─────────────────────────
module tb;
localparam int DEFAULT_TIMEOUT = 5_000;
localparam int MAX_RETRIES = 3;
task automatic smart_write (
input logic [31:0] addr, data,
output logic err,
input int timeout = DEFAULT_TIMEOUT, // from localparam
input int retries = MAX_RETRIES // from localparam
);
// ... driver body
endtask
// Change DEFAULT_TIMEOUT once — all tasks using it update automatically
endmodule
// ── $clog2 as a default ───────────────────────────────────────────
parameter int DEPTH = 1024;
task automatic init_memory (
input int start_addr = 0,
input int num_entries = DEPTH, // from parameter
input int addr_width = $clog2(DEPTH) // computed at compile time
);
$display("Init %0d entries, %0d-bit addr", num_entries, addr_width);
endtask
init_memory(); // start=0, entries=1024, width=10
init_memory(512); // start=512, rest at defaults
init_memory(0, 256); // first 256 entriesDefault Values and output Arguments
output arguments technically can have defaults in SystemVerilog, but the default value has no practical effect — an output is written by the task and read by the caller after return. A caller that "omits" an output argument cannot receive the result. In practice, output arguments should always be required. The exception is input and inout which both support defaults meaningfully.
// ── input: most common use of defaults ───────────────────────────
task automatic send_burst (
input logic [31:0] start_addr, // required
input int burst_len = 4, // optional — 4 beats
input bit incr_addr = 1, // optional — auto-increment
input logic [1:0] size = 2'b10 // optional — 32-bit transfers
);
for (int i = 0; i < burst_len; i++) begin
@(posedge clk);
paddr = start_addr + (incr_addr ? i*4 : 0);
psize = size;
end
endtask
send_burst(32'h1000); // 4-beat, auto-increment, 32-bit
send_burst(32'h1000, 8); // 8-beat burst
send_burst(32'h1000, 1, 0); // single fixed-address transfer
// ── inout with default: counter with optional starting value ──────
task automatic count_errors (
inout int err_count = 0 // start at 0 if no argument given
);
if (pslverr) err_count++;
endtask
int my_errs;
count_errors(my_errs); // my_errs used as-is (must be initialised)Common Mistakes
// ════ MISTAKE 1: Required arg after default ════════════════════════
task automatic bad1 (
input logic a,
input bit verbose = 0, // default
input logic b // ❌ required after default — illegal
);
endtask
// ✅ FIX: move required before default
task automatic good1 (
input logic a,
input logic b,
input bit verbose = 0 // default at end
);
endtask
// ════ MISTAKE 2: Omitting a middle positional arg ══════════════════
task automatic send (
input logic [7:0] data,
input int delay = 10,
input bit check = 1
);
endtask
// Positional call: to get check=0 you MUST provide delay
send(8'hAA, 10, 0); // ← must spell out delay even if you want default
// ✅ FIX: use named args to jump directly to 'check'
send(.data(8'hAA), .check(0)); // delay stays at default 10 ✅
// ════ MISTAKE 3: Non-const expression as default ══════════════════
logic [31:0] current_addr;
task automatic bad3 (
input logic [31:0] addr = current_addr // ❌ runtime variable as default
);
endtask
// ✅ FIX: use a compile-time constant as default
task automatic good3 (
input logic [31:0] addr = 32'h0000_0000 // ✅ constant
);
endtaskQuick Reference — Default Arguments Cheat Sheet
// ── Declaration syntax ────────────────────────────────────────
task automatic my_task (
input <type> req_arg, // required — no default
input <type> opt_arg = value, // optional — has default
input <type> another = CONST // default from localparam/parameter
);
// ── Calling: positional (must provide all preceding args) ─────
my_task(req); // opt_arg and another use defaults
my_task(req, val1); // opt_arg=val1, another uses default
my_task(req, val1, val2); // all provided
// ── Calling: named (jump to any argument) ─────────────────────
my_task(.req_arg(req), .another(val2)); // opt_arg uses default
my_task(.another(val2), .req_arg(req)); // order doesn't matter
// ── Rules ─────────────────────────────────────────────────────
// 1. Required arguments MUST come before default arguments
// 2. Default value must be a compile-time constant expression
// 3. Named calling: jump to any argument, skip preceding ones
// 4. Cannot mix positional and named in the same call
// 5. output args cannot meaningfully have defaults
// 6. Default can use: literals, localparams, $clog2(), prev argsHow the Simulator Evaluates Default Values
Understanding when default values are substituted — and by what mechanism — prevents a class of subtle bugs where engineers expect runtime behavior but get compile-time constants.
Elaboration vs Simulation Time
SystemVerilog simulation has four distinct phases:
- Compilation — source is parsed and type-checked. Default expressions are checked for constant-ness here. A runtime variable as a default causes a compile error at this stage.
- Elaboration — the design hierarchy is built and parameters are resolved. Default values are substituted into the intermediate representation at every call site that omits that argument. After elaboration, there is no runtime "check if argument was provided" — the constant is simply baked in.
- Initialization — initial blocks and variable initial values are applied. Default argument values are not re-evaluated here; they were resolved at elaboration.
- Simulation — time advances. When a task is called without an argument, it receives the elaboration-time constant directly, with zero overhead — no branch, no optional check, no performance difference vs explicit supply.
What Expressions Are Legal as Defaults
| Expression type | Legal as default? | Example |
|---|---|---|
| Integer literal | ✅ Yes | = 1000 |
| Bit / logic literal | ✅ Yes | = 8'hFF |
| String literal | ✅ Yes | = "INFO" |
localparam / parameter | ✅ Yes | = DEFAULT_TIMEOUT |
Constant expression ($clog2, arithmetic) | ✅ Yes | = $clog2(DEPTH) |
| Previously declared argument in same list | ⚠ Tool-dependent | = prev_arg * 2 |
Module-level logic / integer variable | ❌ No | = current_addr |
| Function call return value (non-constant) | ❌ No | = get_base_addr() |
| Class member access | ❌ No | = cfg.timeout |
Advanced API Design Patterns
Default arguments are one tool for building clean testbench APIs. Here are four production-level patterns that combine defaults with other SystemVerilog features for maximum reuse and clarity.
Pattern A: Test Mode Flag — Normal vs Stress Path
// ── Normal driver: defaults match the golden-path spec ────────────
task automatic i2c_write (
input logic [6:0] dev_addr,
input logic [7:0] reg_addr,
input logic [7:0] data,
output logic ack,
input int scl_half_period = 50, // 100 kHz at 10 ns/cycle
input bit inject_nak = 0, // normal: device ACKs
input int stretch_cycles = 0, // no clock stretching
input bit corrupt_bit = 0 // no bit-flip injection
);
sda = 0; // start
repeat(scl_half_period) @(posedge clk);
// ... full I2C sequencing ...
if (inject_nak) sda = 1; // force NAK for error injection tests
if (corrupt_bit) sda = ~sda;
ack = !sda;
endtask
// Normal regression test: one argument for everything common
i2c_write(7'h48, 8'h01, 8'h3C, ack_bit);
// Stress test: inject NAK to test retry logic
i2c_write(.dev_addr(7'h48), .reg_addr(8'h01), .data(8'h3C),
.ack(ack_bit), .inject_nak(1));
// Slow slave: clock stretching
i2c_write(.dev_addr(7'h48), .reg_addr(8'h01), .data(8'h3C),
.ack(ack_bit), .stretch_cycles(200));
// Bit error injection test
i2c_write(.dev_addr(7'h48), .reg_addr(8'h01), .data(8'h3C),
.ack(ack_bit), .corrupt_bit(1));Pattern B: Layered Timeout Hierarchy
// Protocol-level timeouts as single source of truth
localparam int TO_CLK = 1; // 1 clock cycle
localparam int TO_FAST = 100; // fast peripheral response
localparam int TO_NORM = 5_000; // normal bus transaction
localparam int TO_SLOW = 50_000; // slow peripherals (flash, ADC)
localparam int TO_BOOT = 1_000_000;// boot sequence
// ── Low-level: uses fast timeout ──────────────────────────────────
task automatic wait_ack (
input int timeout = TO_FAST
);
int cnt = 0;
while (!ack && cnt < timeout) begin @(posedge clk); cnt++; end
if (!ack) $fatal(1, "ACK timeout after %0d cycles", timeout);
endtask
// ── Mid-level: wraps wait_ack, uses normal timeout ─────────────────
task automatic apb_transaction (
input logic [31:0] addr, data,
output logic err,
input int timeout = TO_NORM // inherits from hierarchy
);
@(posedge clk); psel = 1; paddr = addr; pwdata = data;
@(posedge clk); penable = 1;
wait_ack(timeout); // passes override down if provided
err = pslverr;
psel = 0; penable = 0;
endtask
// ── High-level: ADC readback, uses slow timeout ───────────────────
task automatic read_adc_result (
output logic [15:0] result,
input int timeout = TO_SLOW
);
logic err;
apb_transaction(32'hF000_0010, 32'h0, err, timeout);
result = prdata[15:0];
endtask
read_adc_result(adc_val); // uses TO_SLOW (50k cycles)
read_adc_result(adc_val, TO_FAST); // tight timeout for error injectionPattern C: Overloaded Verbosity — Silence in Regression, Detail in Debug
// Every call site must manage verbosity
apb_write(addr, data, e, 1000, 0);
apb_write(addr2, data2, e, 1000, 0);
apb_write(addr3, data3, e, 1000, 0);
// ... 40 more like this in every test
// Miss one → noisy log in regression
// Change one → inconsistent verbosity// verbose=0 by default: clean by default
apb_write(addr, data, e);
apb_write(addr2, data2, e);
apb_write(addr3, data3, e);
// Debug a specific transaction:
apb_write(.addr(addr), .data(data),
.err(e), .verbose(1));
// All other transactions: silent ✅Pattern D: Protocol Configuration Struct — When Defaults Become Unwieldy
When a task needs more than 5–6 default arguments, defaults become hard to read and maintain. At that point, replace them with a configuration struct with default field values:
// ── When you have too many defaults: use a struct ─────────────────
typedef struct {
int timeout = 5000;
bit verbose = 0;
bit check_err = 1;
int retries = 3;
bit inject_nak = 0;
logic [1:0] burst_type = 2'b01;
int scl_div = 50;
bit stretch_clk = 0;
} apb_cfg_t;
task automatic apb_write_v2 (
input logic [31:0] addr,
input logic [31:0] data,
output logic err,
input apb_cfg_t cfg = {} // empty braces → all struct defaults
);
// use cfg.timeout, cfg.verbose, cfg.retries, etc.
endtask
// ── All defaults: cleanest possible call ─────────────────────────
apb_write_v2(addr, data, e);
// ── Override a few fields cleanly ────────────────────────────────
apb_cfg_t stress_cfg;
stress_cfg.timeout = 100;
stress_cfg.inject_nak = 1;
apb_write_v2(addr, data, e, stress_cfg);
// ── Inline override with struct literal ──────────────────────────
apb_write_v2(addr, data, e,
'{default:0, timeout:500, check_err:1});Debugging Academy — Default Argument Bugs
These are the real bugs that appear in SystemVerilog testbenches when default arguments interact with the rest of the language in unexpected ways.
Timeout Fires on the First Call, Not When Expected
HIGH// Default timeout: 1000 cycles
task auto wait_resp(
input int timeout = 1000
);
// Test file uses localparam...
localparam int LONG_WAIT = 50_000;
// ...but accidentally uses variable
int dyn_timeout;
initial begin
dyn_timeout = 50_000;
// ❌ tries to use var as default
wait_resp(dyn_timeout);
// compiles but won't use LONG_WAIT
// as a default — it IS being passed
endA call intended to inherit the long wait period instead silently uses the short 1000-cycle default whenever the variable propagation is missed. Tests time out unexpectedly at the wrong point, often in unrelated regressions where the variable was never set.
The bug is actually a misunderstanding — wait_resp(dyn_timeout) works correctly as a positional call passing the variable. The real bug is when you change dyn_timeout in one place but forget to propagate it. Using a localparam as the default eliminates this entirely: every call site without an explicit argument inherits the localparam.
// Use localparam as default ✅
localparam int LONG_WAIT = 50_000;
task auto wait_resp(
input int timeout = LONG_WAIT
);
// Call with no argument
initial begin
wait_resp();
// ✅ uses LONG_WAIT at all sites
// Change localparam = update all
endNamed Argument Typo Silently Uses Default
HIGHtask auto apb_write(
input logic [31:0] addr, data,
output logic err,
input bit verbose = 0
);
// ❌ Wrong: 'Verbose' ≠ 'verbose'
apb_write(.addr(32'h0),
.data(32'hFF),
.err(e),
.Verbose(1)); // ← typo!
// Tool may warn or error
// verbose stays 0 silently!The intended verbose run produces zero debug output. Some simulators emit an error for an unknown named argument; others silently ignore it and the override is lost. The test report looks identical to a non-verbose run even though the engineer thought they enabled tracing.
Named arguments are case-sensitive in SystemVerilog. .Verbose(1) and .verbose(1) refer to different names. Most simulators emit an error for an unknown named argument, but some older tools quietly ignore it — and the intended override is silently lost. Always enable maximum compiler warnings, and grep your task declarations for exact case before using named args.
// ✅ Exact case match required
apb_write(.addr(32'h0),
.data(32'hFF),
.err(e),
.verbose(1)); // ✅
// Always compile with -Wall
// to catch named arg mismatches
// Most tools error on unknown
// named argument namesMixing Positional and Named Association in Same Call
MEDIUMtask auto send(
input logic [7:0] data,
input int delay = 10,
input bit check = 1
);
// ❌ Mixing: compile error
send(8'hAB, .check(0));
// positional 8'hAB then named .check
// → illegal in the same callCompile error from the simulator: "cannot mix positional and named argument association." The call site does not compile, but the message can be confusing for engineers who expect the language to "do what I mean."
The LRM prohibits mixing positional and named association in the same task/function call. If you want to use named association to skip a middle default, all arguments in that call must use named association — including the required ones. The compiler catches this; it will not silently accept mixed styles.
// ✅ Option 1: all named
send(.data(8'hAB), .check(0));
// ✅ Option 2: all positional
// (must provide delay too)
send(8'hAB, 10, 0);
// Rule: once you use .name(),
// ALL arguments must be namedDefault Overridden in a Loop Overwrites Preceding Iterations' Defaults
LOW// Engineer expects: iteration 0
// uses default 1000, iteration 1
// uses 500, etc.
int timeouts[] = '{0:1000, 1:500,
2:200};
for (int i=0; i<3; i++) begin
// ❌ This always passes timeouts[i]
// Never uses the "default"
wait_resp(timeouts[i]);
endThe "default" branch is never exercised — every iteration explicitly passes a value, so the declared default never applies. This is a conceptual bug rather than a simulator error, but it leads to unexpected test coverage gaps when reviewing waveforms.
Default argument values are per-call-site, not per-task. Each call that omits an argument independently receives the default. There is no mechanism to "reset" a default mid-execution. If you need conditional default-or-override logic, use an if statement at the call site, or pass a sentinel value (like -1) and check it inside the task body.
// ✅ Each call is independent
// Default applies when NO arg given
wait_resp(); // uses default
wait_resp(500); // overrides
wait_resp(200); // overrides
// In a loop that conditionally
// uses default:
for (int i=0; i<3; i++) begin
if (i==0) wait_resp();
else wait_resp(200*i);
endDefault on inout Modifies the Wrong Variable
HIGH// ❌ inout default creates temp var
task auto count_err(
inout int count = 0
);
if (pslverr) count++;
endtask
// Called without argument
int total_errs = 0;
// ❌ When no arg: count is a
// temporary local = 0, discarded
// at return. total_errs unchanged!
count_err(); // ← increment lostThe caller's accumulator never increments. No compile error, no simulation warning — the bug compiles cleanly. Error counters stay at zero even when pslverr fires repeatedly. The mismatch between expected and observed counts often only surfaces during final scoreboard comparisons at end-of-test.
When an inout argument is "omitted" (using its default), the simulator creates an internal temporary variable initialised to the default value. The task increments that temporary — but it is discarded at return. The caller's variable is never updated. This is the most dangerous default-argument bug because it compiles cleanly and produces no warning, yet the inout's write-back is silently lost. Always provide inout arguments explicitly.
// ✅ Always provide inout explicitly
int total_errs = 0;
count_err(total_errs); // ✅
// total_errs is incremented
// correctly via copy-in/copy-out
// Alternatively: use output
// if you don't need copy-in
// Never rely on inout defaultsInterview Q&A — Default Arguments
A default argument is a value declared with = expr after an argument name in a task or function header. If the caller does not provide that argument, the declared value is used. Both tasks and functions support default arguments. The default expression must be a compile-time constant — a literal, localparam, module parameter, or constant expression like $clog2(N). Runtime variables cannot be defaults. There is no restriction on direction for input arguments; output defaults are technically legal but practically useless.