SystemVerilog · Module 6
void Functions & System Tasks Overview
Functions with no return value, the full system task catalogue ($display, $strobe, $monitor, $finish, randomization, file I/O), and timing semantics.
Module 6 · Page 6.7
Sometimes you need a function that does something but returns nothing. Sometimes you need to call a built-in simulator routine. This page covers void functions and gives you a complete, practical reference for every system task you will use daily — from $display to $readmemh — along with production verification patterns, timing semantics, debugging labs, and interview-ready answers.
void Functions — Action Without a Return Value
A void function is declared with function automatic void. It performs an action — logging, updating state, printing — but returns nothing to the caller. It is the right choice when you want the zero-time guarantee of a function (no timing controls) but have no value to return.
// ── void function: no return value ───────────────────────────────
function automatic void log_event (input string msg, input string level = "INFO");
$display("[%s @ %0t] %s", level, $time, msg);
endfunction
function automatic void set_flag (ref bit flag, input bit val);
flag = val; // modifies caller's variable directly (ref)
endfunction
function automatic void clear_array (ref logic [7:0] arr[]);
foreach (arr[i]) arr[i] = 8'h00;
endfunction
// ── Calling a void function: two styles ─────────────────────────
// Style 1: call as a statement (most common)
log_event("Boot complete");
log_event("Parity error", "ERROR");
// Style 2: void'() — discard a non-void function's return value
// Sometimes you call a function but don't need the result
void'(randomize()); // randomize() returns 1/0 — discard result
void'(std::randomize(data)); // same pattern
// Without void'(), some simulators warn about discarded return values
// void'() silences the warning explicitly — "I know, I'm discarding on purpose"
// ── void function is safe inside always_comb ─────────────────────
always_comb begin
result = a + b;
log_event("Computed sum"); // ✅ void function: zero time, legal here
// wait_cycles(5); ← task: would be ILLEGAL in always_comb
endSystem Tasks — Built-in Simulator Routines
SystemVerilog provides a rich set of built-in tasks and functions starting with $. These are called system tasks (the ones that act as statements) and system functions (the ones that return a value). You have been using them since page 6.1 — this section gives you the complete reference, organised by category.
Display & Logging — Printing from Simulation
// ── $display: print + newline, at current simulation time ─────────
$display("Hello world");
$display("addr=0x%h data=%0d flag=%b", addr, data, flag);
$display("Time: %0t State: %s", $time, state_name);
$display("Hex8: %08h", val); // zero-padded 8-digit hex
// ── $write: print WITHOUT newline ────────────────────────────────
$write("a=%0d ", a);
$write("b=%0d ", b);
$display(""); // end the line
// ── $strobe: print at END of current time step (after NBA updates)
// Use when you need to see the final value after all NBA assignments
always_ff @(posedge clk) q <= d;
$strobe("q=%b (final)", q); // shows q's value after NBA update
// ── $monitor: fires every time any listed variable changes ───────
// Only one $monitor can be active per initial block at a time
initial
$monitor("@%0t: a=%b b=%b sum=%0d", $time, a, b, sum);
// ── Format specifier cheat sheet ─────────────────────────────────
// %d / %0d — decimal (0 removes leading spaces)
// %h / %H — hexadecimal (lowercase / uppercase)
// %b — binary
// %o — octal
// %s — string
// %t — simulation time (uses `timescale)
// %e / %f — real: scientific / fixed-point
// %g — real: shorter of %e or %f
// %p — unpacked arrays / structs (pretty-print)
// \n \t \\ \" — escape sequencesSeverity Messages — $info, $warning, $error, $fatal
The four severity system tasks are the correct way to report test results. Each one adds a severity level to the message and integrates with the simulator's pass/fail tracking — unlike $display, which is always neutral.
// ── $info: informational — never fails the simulation ─────────────
$info("Test started: apb_basic_write");
$info("Coverage: %0d%%", coverage_pct);
// ── $warning: simulation continues, exit code may flag warning ────
$warning("FIFO almost full — %0d/%0d", count, DEPTH);
$warning("Latency %0d cycles exceeds target of %0d", actual, target);
// ── $error: simulation continues, exit code flags failure ─────────
// Use in checkers and scoreboards
$error("Data mismatch at addr 0x%h: exp=0x%h got=0x%h", addr, exp, got);
if (pslverr) $error("APB slave error on write to 0x%h", paddr);
// ── $fatal: immediately stops the simulation ──────────────────────
// First argument is verbosity level (0, 1, or 2)
$fatal(1, "Timeout waiting for DUT ready after %0d cycles", cycle_count);
$fatal(0, "Configuration error: DEPTH must be power of 2, got %0d", DEPTH);
// Level 0: minimal output. Level 1: message + location. Level 2: full traceback
// ── Comparison: $display vs $error ────────────────────────────────
// $display("FAIL: mismatch"); ← neutral — simulator doesn't count this
// $error("FAIL: mismatch"); ← counted as error — affects exit code
// Always use severity tasks for pass/fail reporting| System Task | Severity | Simulation Continues? | Exit Code Impact |
|---|---|---|---|
$info | Informational | Yes | No impact |
$warning | Warning | Yes | May flag warning in exit |
$error | Error | Yes | Non-zero exit code — marks FAIL |
| `$fatal(0 | 1 | 2, msg)` | Fatal |
Simulation Control — $finish, $stop, $exit
// ── $finish: end simulation cleanly ──────────────────────────────
// 0 = no message, 1 = simulation time+counts, 2 = + memory usage
initial begin
// run tests...
$finish; // clean exit, default message
// or:
$finish(2); // verbose exit with memory stats
end
// ── $stop: pause simulation (interactive mode) ───────────────────
// Opens the simulator prompt — for debugging only, not in CI
if (error_count > 0) $stop;
// ── Best practice: controlled finish ────────────────────────────
initial begin
logic pass = 1;
// ... run stimulus and collect results
if (error_count == 0)
$info("TEST PASSED — %0d transactions checked", tx_count);
else
$error("TEST FAILED — %0d errors out of %0d", error_count, tx_count);
$finish;
endRandomization & Time Functions
// ── Random number generation ─────────────────────────────────────
logic [31:0] r;
// $random: returns signed 32-bit (can be negative)
r = $random; // any 32-bit signed value
r = $random(42); // seed=42, reproducible sequence
// $urandom: returns UNSIGNED 32-bit
r = $urandom; // any 32-bit unsigned value
// $urandom_range: unsigned random within a range
r = $urandom_range(0, 255); // 0 to 255 inclusive
r = $urandom_range(100, 200); // 100 to 200
r = $urandom_range(1, 1<<16); // 1 to 65536
// $srandom: set the random seed (for reproducible runs)
$srandom(12345); // seed the PRNG
$srandom($time); // seed from time (truly random)
// ── Time functions ────────────────────────────────────────────────
longint t1 = $time; // integer sim time (in time units)
real t2 = $realtime; // real-valued sim time
int t3 = $stime; // 32-bit truncated time
$display("At time %0t:", $time); // %t uses timescale units
$display("Real: %0f ns", $realtime);
// $timeformat — controls how %t displays
$timeformat(-9, 2, " ns", 10); // nanoseconds, 2 decimal places
// After this: $display("%t", $time) shows e.g. " 100.50 ns"File & Memory & Utility Functions
// ── Memory initialisation ─────────────────────────────────────────
logic [31:0] rom [0:255];
$readmemh("program.hex", rom); // load hex file into rom[]
$readmemb("data.bin", rom); // load binary file
$readmemh("partial.hex", rom, 0, 63); // load addresses 0..63 only
$writememh("dump.hex", rom); // save rom[] to file
// ── VCD waveform dumping ──────────────────────────────────────────
initial begin
$dumpfile("sim.vcd"); // set output file
$dumpvars(0, tb); // dump all vars in 'tb' (depth 0=all)
$dumpvars(1, u_dut); // dump 1 level deep in u_dut
$dumpvars(0, sig_a, sig_b); // dump specific signals
end
// ── Compile-time utility functions ───────────────────────────────
$clog2(256) // → 8 (bits needed to address 256 locations)
$bits(logic [7:0]) // → 8 (number of bits in type)
$bits(my_struct) // → total packed bit width of struct
$size(my_array) // → number of elements in first dimension
$dimensions(arr) // → number of dimensions
$typename(my_var) // → string name of the type
// ── Runtime utility ──────────────────────────────────────────────
$isunknown(val) // → 1 if any bit is X or Z
$countones(val) // → number of 1 bits (popcount)
$onehot(val) // → 1 if exactly one bit is 1
$onehot0(val) // → 1 if at most one bit is 1 (zero also valid)
// ── $cast — runtime type check / conversion ───────────────────────
typedef enum {A, B, C} my_enum_t;
int raw = 2;
my_enum_t e;
if (!$cast(e, raw)) // try to cast 'raw' to enum
$error("Invalid enum value: %0d", raw);
// ── Plusargs — pass arguments from command line ───────────────────
int seed;
if ($test$plusargs("SEED"))
$value$plusargs("SEED=%d", seed); // +SEED=12345 on command line
$srandom(seed);Complete Quick Reference
// ── void function ─────────────────────────────────────────────
function automatic void my_fn (...); // no return value
my_fn(args); // call as statement
void'(any_fn(args)); // discard non-void result
// ── Display ──────────────────────────────────────────────────
$display("fmt", args); // print + newline
$write("fmt", args); // print, no newline
$strobe("fmt", args); // print at end of time step
$monitor("fmt", args); // print on any signal change
// ── Severity (use these for pass/fail — not $display) ─────────
$info("msg", args); // informational — no exit impact
$warning("msg", args); // warning — may flag in exit code
$error("msg", args); // error — non-zero exit, sim continues
$fatal(1, "msg", args); // fatal — stops sim immediately
// ── Control ──────────────────────────────────────────────────
$finish; // end simulation (normal exit)
$stop; // pause simulation (debug)
// ── Time ─────────────────────────────────────────────────────
$time // current sim time (integer)
$realtime // current sim time (real)
$timeformat(-9,2," ns",10) // ns display with 2 decimals
// ── Random ───────────────────────────────────────────────────
$urandom // unsigned 32-bit random
$urandom_range(lo, hi) // unsigned random in [lo, hi]
$srandom(seed) // seed the PRNG
// ── Memory / file ────────────────────────────────────────────
$readmemh("f.hex", mem) // load hex file into array
$readmemb("f.bin", mem) // load binary file
$dumpfile("sim.vcd") // set VCD output file
$dumpvars(0, tb) // dump all signals in tb
// ── Utility ──────────────────────────────────────────────────
$clog2(N) // ceiling log2 (compile-time)
$bits(type_or_var) // number of bits
$isunknown(val) // 1 if any bit is X or Z
$countones(val) // popcount
$cast(dest, src) // safe dynamic type cast
$typename(x) // type name as string$display vs $strobe vs $monitor — Timing Decoded
All three print values from simulation, but they fire at different points in the simulation event queue. Using the wrong one gives you values that appear plausible but are sampled before critical assignments have completed — leading to debugging sessions that chase phantom bugs.
The Simulation Event Queue — Four Regions That Matter
At every simulation time step, the simulator processes events in a fixed order. Understanding which region each print task occupies is the key to reading simulation output correctly.
| Region | What Happens | Print tasks that fire here |
|---|---|---|
| Active | Blocking assignments evaluate; module output ports update; procedural statements run | $display, $write |
| NBA (Non-Blocking Assignment Update) | All <= assignments in the current time step take effect simultaneously | — |
| Observed | SVA assertions sample signal values (after NBA settles) | — |
| Postponed | Read-only access to final stable values; last region of the time step | $strobe, $monitor |
Time 100ns: posedge clk event fires
│
├─ ACTIVE REGION ──────────────────────────────────────────────────────────────
│ always_ff @(posedge clk): evaluates RHS → q_new = d (queues NBA for q)
│ always_comb: re-evaluates → sum = a + b
│ $display("q=%b", q); fires NOW → prints OLD value of q ⚠
│
├─ NBA REGION ─────────────────────────────────────────────────────────────────
│ q ← q_new NBA update applied (q now has new value)
│ (if this NBA changed other signals → re-enters Active region)
│
├─ OBSERVED REGION ────────────────────────────────────────────────────────────
│ SVA assertions sample q → sees NEW value ✅
│
└─ POSTPONED REGION ───────────────────────────────────────────────────────────
$strobe("q=%b", q); fires HERE → prints NEW value ✅
$monitor fires if q changed in this step → prints NEW value ✅
─── Time advances to next event ────────────────────────────────────────────// ── The classic NBA observation bug ──────────────────────────────
module tb_display_strobe;
logic clk = 0, d = 0, q = 0;
always #5 clk = ~clk;
// DUT: simple flip-flop
always_ff @(posedge clk)
q <= d;
initial begin
// At time 0: d=0, q=0
#6; d = 1; // d becomes 1 at time 6
#4; // posedge at time 10
// ❌ $display: Active region — reads q BEFORE NBA update
$display("[display] @%0t q=%b (sees OLD value)", $time, q);
// → [display] @10 q=0 ← OLD value! NBA hasn't applied yet
// ✅ $strobe: Postponed region — reads q AFTER NBA update
$strobe("[strobe] @%0t q=%b (sees NEW value)", $time, q);
// → [strobe] @10 q=1 ← NEW value ✅
#20; $finish;
end
// $monitor: fires in Postponed region when q changes
initial
$monitor("[monitor] @%0t q=%b", $time, q);
// → fires at time 10 after NBA: [monitor] @10 q=1
endmodule
// ── $monitor: one active per scope, fires on ANY listed signal change
initial begin
$monitor("@%0t: a=%h b=%h out=%h", $time, a, b, out);
// fires every time step where a, b, or out changes
// Only ONE $monitor per scope — second $monitor replaces first
$monitoroff; // pause monitoring
// ... bulk stimulus ...
$monitoron; // resume monitoring
endvoid Functions in Production Verification
void functions are the backbone of every well-structured testbench. Here are four production patterns that appear in real DV environments — from directed-test benches to full UVM-based SoC verification.
Pattern A — Structured Logger with Severity and Verbosity Control
// ── Production logger: used in place of raw $display everywhere ───
package tb_logger_pkg;
typedef enum {LOG_DEBUG=0, LOG_INFO=1, LOG_WARN=2, LOG_ERROR=3, LOG_FATAL=4} log_level_t;
int verbosity = LOG_INFO; // default: show INFO and above
int error_count = 0;
string log_prefix = "";
function automatic void log (
input log_level_t level,
input string component,
input string msg
);
string tag;
if (level < log_level_t'(verbosity)) return; // filter by verbosity
case (level)
LOG_DEBUG: tag = "DEBUG";
LOG_INFO: tag = "INFO ";
LOG_WARN: tag = "WARN ";
LOG_ERROR: tag = "ERROR";
LOG_FATAL: tag = "FATAL";
endcase
if (level == LOG_ERROR) begin
error_count++;
$error("[%s][%s @%0t] %s", tag, component, $time, msg);
end else if (level == LOG_FATAL) begin
$fatal(1, "[%s][%s @%0t] %s", tag, component, $time, msg);
end else begin
$display("[%s][%s @%0t] %s", tag, component, $time, msg);
end
endfunction
function automatic void report_summary();
if (error_count == 0)
$info("TEST PASSED — no errors detected");
else
$error("TEST FAILED — %0d error(s)", error_count);
endfunction
endpackage
// ── Usage in testbench ────────────────────────────────────────────
import tb_logger_pkg::*;
log(LOG_INFO, "APB_DRV", "Write 0x100 = 0xFF");
log(LOG_WARN, "CHECKER", "Latency exceeded target");
log(LOG_ERROR, "CHECKER", "Data mismatch at addr 0x100"); // increments error_count
log(LOG_DEBUG, "MONITOR", "psel=1 paddr=0x100"); // filtered if verbosity>DEBUG
report_summary(); // prints PASS/FAIL at endPattern B — Scoreboard Check Method
// ── APB scoreboard check: void fn updates stats and logs errors ───
function automatic void check_apb_read (
input logic [31:0] addr,
input logic [31:0] expected,
input logic [31:0] actual,
ref int pass_count,
ref int fail_count
);
if (actual === expected) begin
pass_count++;
$info("PASS: addr=0x%h exp=0x%h got=0x%h", addr, expected, actual);
end else begin
fail_count++;
$error("FAIL: addr=0x%h exp=0x%h got=0x%h diff=0x%h",
addr, expected, actual, expected ^ actual);
end
endfunction
// ── Usage — called from monitor callback ──────────────────────────
int pass_cnt = 0, fail_cnt = 0;
// Compare all 16 register locations
foreach (reg_addr[i])
check_apb_read(reg_addr[i], ref_model[i], dut_readback[i],
pass_cnt, fail_cnt);
$display("Scoreboard: %0d passed, %0d failed", pass_cnt, fail_cnt);Pattern C — Protocol Monitor Event Reporter
// ── APB bus monitor — void functions act as event callbacks ───────
module apb_monitor #(parameter VERBOSE=0) (
input clk, psel, penable, pwrite, pready, pslverr,
input logic [31:0] paddr, pwdata, prdata
);
function automatic void on_write_complete (
input logic [31:0] addr, data,
input logic err
);
if (VERBOSE)
$display("[APB_MON @%0t] WR addr=0x%08h data=0x%08h err=%b",
$time, addr, data, err);
if (err) $error("APB SLVERR on write to 0x%h", addr);
endfunction
function automatic void on_read_complete (
input logic [31:0] addr, data,
input logic err
);
if (VERBOSE)
$display("[APB_MON @%0t] RD addr=0x%08h data=0x%08h err=%b",
$time, addr, data, err);
endfunction
always_ff @(posedge clk) begin
if (psel && penable && pready) begin
if (pwrite) on_write_complete(paddr, pwdata, pslverr);
else on_read_complete (paddr, prdata, pslverr);
end
end
endmodulePattern D — Coverage Sampling Wrapper
// ── Coverage sampled through void function — centralizes logging ──
covergroup apb_cg;
cp_addr: coverpoint paddr[7:0] {
bins low = {[8'h00:8'h3F]};
bins mid = {[8'h40:8'h7F]};
bins high = {[8'h80:8'hFF]};
}
cp_rw: coverpoint pwrite;
cx_addr_rw: cross cp_addr, cp_rw;
endgroup
apb_cg cg_inst = new();
function automatic void sample_coverage (
input logic [31:0] addr,
input logic rw
);
paddr = addr;
pwrite = rw;
cg_inst.sample();
if (cg_inst.get_coverage() > 99.0)
$info("APB coverage reached %.1f%% — goal met!", cg_inst.get_coverage());
endfunction
// Called from monitor after each observed APB transaction
sample_coverage(observed_addr, observed_rw);Advanced System Task Patterns
Beyond the basics, system tasks enable powerful testbench infrastructure: configurable test environments, selective debugging, and deterministic test reproduction across multiple runs.
Pattern A — ROM Loader with Error Detection
// ── ROM-based testbench with $readmemh ────────────────────────────
module tb_rom_loader;
parameter int ROM_DEPTH = 256;
parameter int ROM_WIDTH = 32;
logic [ROM_WIDTH-1:0] rom [0:ROM_DEPTH-1];
logic [ROM_WIDTH-1:0] expected [0:ROM_DEPTH-1];
function automatic void load_and_validate (
input string hex_file,
input string label
);
int unknown_count = 0;
// Load the hex file
$readmemh(hex_file, rom);
// Check for any X/Z values (indicates file was shorter than array)
foreach (rom[i]) begin
if ($isunknown(rom[i])) unknown_count++;
end
if (unknown_count > 0)
$warning("[%s] %0d/%0d entries have X/Z (file shorter than array)",
label, unknown_count, ROM_DEPTH);
else
$info("[%s] Loaded %0d entries from %s OK",
label, ROM_DEPTH, hex_file);
endfunction
initial begin
load_and_validate("program.hex", "ROM");
load_and_validate("expected.hex", "REF");
// ... run simulation ...
end
endmodulePattern B — Plusargs-Driven Test Configuration
// ── Command-line configurable testbench ──────────────────────────
// Run with: vcs +SEED=42 +VERBOSE +NUM_TX=500 +DUMP_VCD
module tb_configurable;
int seed = 0;
int num_tx = 100; // default
bit verbose = 0;
string test_name = "default";
initial begin
// Parse plusargs
if ($test$plusargs("VERBOSE")) verbose = 1;
if ($test$plusargs("SEED"))
$value$plusargs("SEED=%d", seed);
if ($test$plusargs("NUM_TX"))
$value$plusargs("NUM_TX=%d", num_tx);
if ($test$plusargs("TEST"))
$value$plusargs("TEST=%s", test_name);
// Apply configuration
$srandom(seed);
$info("Config: test=%s seed=%0d num_tx=%0d verbose=%0b",
test_name, seed, num_tx, verbose);
// Conditional VCD dump
if ($test$plusargs("DUMP_VCD")) begin
$dumpfile("sim.vcd");
$dumpvars(1, tb_configurable); // 1 level only
$info("VCD dump enabled: sim.vcd");
end
// ... run num_tx transactions ...
$finish;
end
endmodulePattern C — $cast for Type-Safe Enum Handling
// ── $cast: safe runtime type conversion ──────────────────────────
typedef enum logic [2:0] {
APB_IDLE = 3'b000,
APB_SETUP = 3'b001,
APB_ACCESS = 3'b010,
APB_ERROR = 3'b011
} apb_state_t;
function automatic void decode_state (
input logic [2:0] raw_state,
output apb_state_t state_out
);
if (!$cast(state_out, raw_state)) begin
$error("Invalid state encoding 0x%h — DUT in illegal state!", raw_state);
state_out = APB_IDLE; // safe fallback
end
endfunction
// ── Safer than direct cast: $cast returns 0 if conversion is invalid
logic [2:0] dut_state_raw;
apb_state_t dut_state;
decode_state(dut_state_raw, dut_state);
// A direct cast like apb_state_t'(raw_state) has undefined behavior
// if raw_state holds a value not in the enum — $cast catches this.
$display("DUT state: %s", dut_state.name()); // .name() gives enum stringDebugging Academy — System Task Bugs
These are the bugs that silently corrupt simulation results, cause false passes, or kill regression runs — collected from real project post-mortems.
$display instead of $error: test passes when it should fail
SILENT FAILURE// ❌ Checker using $display
function auto void check(
input logic [31:0] exp, got
);
if (got !== exp)
$display("FAIL: exp=%h got=%h",
exp, got);
// ← simulation exit code: 0
// ← CI: PASS ← WRONG!
endfunctionChecker logs hundreds of "FAIL" messages, but the simulation exit code is 0 and the CI pipeline marks the test as PASS. The bug ships to silicon because the regression report shows green even though real mismatches occurred during the run.
$display is a neutral print — it never affects the simulation exit code. CI pipelines check the exit code to determine pass/fail. A checker built on $display can log hundreds of mismatches and the test still "passes." This bug shipped to silicon on a real project because the checker was inherited from a Verilog-era testbench that predated $error.
// ✅ Checker using $error
function auto void check(
input logic [31:0] exp, got
);
if (got !== exp)
$error("FAIL: exp=%h got=%h",
exp, got);
// ← simulation exit code: 1
// ← CI: FAIL ← correct ✅
endfunctionRule: every checker comparison must call $error on mismatch — no exceptions.
$display reads pre-NBA flip-flop value, logs wrong data
NBA TIMING// ❌ Reading FF output with $display
always_ff @(posedge clk)
state <= next_state;
// In checker:
always @(posedge clk) begin
$display("state=%s",
state.name());
// ← Prints OLD state!
// NBA update not done yet
endThe log shows the FSM state one cycle behind the actual hardware behaviour. It looks like the state machine is stuck or skipping transitions, but the waveform viewer (which always shows post-NBA values) confirms the state is updating correctly. The bug is only in the log, not the design.
Non-blocking assignments (<=) queue their updates for the NBA region, which executes after the Active region where $display fires. At the posedge clk event, $display reads the current (pre-clock) value of state — the flip-flop update hasn't happened yet. The log shows the state one cycle behind.
// ✅ Use $strobe for FF outputs
always_ff @(posedge clk)
state <= next_state;
// In checker:
always @(posedge clk) begin
$strobe("state=%s",
state.name());
// ← Prints NEW state ✅
// Fires after NBA update
endSwitch to $strobe or observe signals in a waveform viewer, where you always see the correct post-NBA value.
$monitor left active forever, floods log and kills performance
LOG OVERLOAD// ❌ $monitor never turned off
initial
$monitor("@%0t: paddr=%h",
$time, paddr);
// Fires on every paddr change
// 10M transactions = 10M lines
// Log file: 2GB+
// Simulation: 3x slowerSimulation slows down by 3× compared to a run without the monitor. CI disk fills up with multi-gigabyte log files. Job sometimes fails on out-of-disk before the test even completes. The "real" simulation work is fast — the I/O is the bottleneck.
$monitor fires in the Postponed region on every time step where a listed signal changes. For a busy bus with 10M transactions, this generates 10M+ log lines — gigabytes of output that slow simulation significantly (I/O is the bottleneck) and fill disk on CI machines.
// ✅ Enable only for debug window
initial begin
$monitoroff; // off by default
// ... stimulus ...
@(posedge trigger);
$monitoron; // enable window
$monitor("@%0t: paddr=%h",
$time, paddr);
@(posedge done);
$monitoroff; // done
endUse $monitoroff/$monitoron to bracket specific debug windows, or use a coverpoint or assertion instead of $monitor for regression-safe observation.
$stop in committed testbench hangs CI runner
CI HANG// ❌ $stop committed to repo
initial begin
run_stimulus();
if (error_count > 0) begin
$display("Debug me!");
$stop; // ← waits for input
// CI job: hangs here
// Timeout: 2 hours
// Team: blocked
end
endCI job runs forever and is eventually killed by the timeout watchdog (often 2 hours). All downstream regression slots are blocked. Investigating the dangling simulator process shows it sitting idle in interactive mode, waiting for user input that will never arrive in the headless CI environment.
$stop suspends simulation and waits for an interactive user command — it never returns in a non-interactive CI environment. The simulator process stays alive indefinitely, consuming a CI runner slot and blocking other jobs. The error was introduced during local debug and accidentally committed.
// ✅ $fatal stops immediately
initial begin
run_stimulus();
if (error_count > 0)
$fatal(1,
"%0d errors — FAIL",
error_count);
else
$info("TEST PASSED");
$finish;
endGit hook solution: add a pre-commit hook that greps for $stop in testbench files and rejects the commit if found.
$readmemh silent partial load: uninitialized entries cause X-propagation
X-PROPAGATION// ❌ No load validation
logic [31:0] mem[0:255];
$readmemh("boot.hex", mem);
// boot.hex has 128 entries
// mem[128..255] = XXXX
// DUT reads those addresses
// → X-propagation into DUT
// → Simulation: all X outputs
// → Appears as DUT bugDUT outputs become X for what should be valid memory reads. It looks like an RTL bug, but the RTL is fine — the memory it's reading from contains X for any address beyond the loaded portion of the hex file. Hours of debug chase the wrong target.
When a hex file contains fewer entries than the declared array size, $readmemh leaves unloaded entries with their initial value (typically X). If the DUT later accesses those addresses, it receives X — which propagates through combinational logic and produces X outputs. This is often mistaken for a DUT RTL bug.
// ✅ Check for X after load
logic [31:0] mem[0:255];
$readmemh("boot.hex", mem);
// Zero-init remaining entries
foreach (mem[i])
if ($isunknown(mem[i]))
mem[i] = 32'h0;
$info("Memory loaded and validated");Either zero-initialize the array before loading, or use $isunknown to check and report/fix any unloaded entries after the call.
Interview Q&A — void Functions & System Tasks
A void function is declared with function automatic void and performs an action (logging, updating state, printing) without returning a value. The key difference from a task: a void function cannot contain timing controls (#, @, wait) and therefore executes in zero simulation time. This makes it callable from always_comb blocks, continuous assignments, and SVA assertions — contexts where time advance is illegal. Use a void function when your code needs to do something side-effectful but has no return value and no timing dependency. Use a task when you need to wait for clock edges or delays.