Skip to content

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.

Side-effect only codeNeeds timingcontrols (#/@/wait)?NOvoidfunctionAdvantages of void function:Zero-time guaranteedCallable inside always_comb / assignYEStaskTask advantage:Can wait for events/delaysUse if zero-time not guaranteed
Figure 1 — When you need side-effect-only code (no return value): choose void function if no timing is needed; choose task if timing controls are required.
SystemVerilog — void function: declaration, calling, and void'()
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
end

System 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$display$write $writef$strobe $monitor$displayh/b/o$fopen $fdisplaySeverity Messages$info (lowest)$warning$error$fatal (stops sim)Simulation Control$finish (exit sim)$stop (pause)$exitTime Functions$time (integer)$realtime (real)$stime (32-bit)Randomization$random (signed)$urandom (unsigned)$urandom_range$srandom (seed)File & Memory$readmemh$readmemb$writememh$dumpfile $dumpvarsUtility Functions$clog2 $bits $size$cast $typename $test$plusargs$value$plusargs$isunknown$countones$dimensionsFormat specifiers for $display: %0d (decimal) %h (hex) %b (binary) %o (octal) %s (string) %t (time) %e (exp) %g (shortest)Prefix 0 removes leading zeros: %0d | Width: %8h | Zero-pad: %08h
Figure 2 — System tasks and functions grouped by category. The $ prefix identifies all built-in simulator routines.

Display & Logging — Printing from Simulation

SystemVerilog — $display, $write, $strobe, $monitor
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── $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 sequences

Severity 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.

SystemVerilog — $info, $warning, $error, $fatal
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── $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 TaskSeveritySimulation Continues?Exit Code Impact
$infoInformationalYesNo impact
$warningWarningYesMay flag warning in exit
$errorErrorYesNon-zero exit code — marks FAIL
`$fatal(012, msg)`Fatal

Simulation Control — $finish, $stop, $exit

SystemVerilog — $finish, $stop, $exit
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── $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;
end

Randomization & Time Functions

SystemVerilog — Random and time system functions
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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

SystemVerilog — $readmemh, $dumpvars, $clog2, $bits, and more
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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

SystemVerilog — void Function + System Task Cheat Sheet
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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.

RegionWhat HappensPrint tasks that fire here
ActiveBlocking 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
ObservedSVA assertions sample signal values (after NBA settles)
PostponedRead-only access to final stable values; last region of the time step$strobe, $monitor
Event Timeline — posedge clk at time 100ns
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 ────────────────────────────────────────────
SystemVerilog — $display vs $strobe: seeing the difference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
end

void 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

SystemVerilog — Structured logger (replaces raw $display calls)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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 end

Pattern B — Scoreboard Check Method

SystemVerilog — Scoreboard compare: void function with ref args
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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

SystemVerilog — APB monitor: void callbacks for observed transactions
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endmodule

Pattern D — Coverage Sampling Wrapper

SystemVerilog — Coverage group sampling via void function
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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

SystemVerilog — $readmemh: production ROM loader with validation
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endmodule

Pattern B — Plusargs-Driven Test Configuration

SystemVerilog — $test$plusargs / $value$plusargs: runtime test config
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endmodule

Pattern C — $cast for Type-Safe Enum Handling

SystemVerilog — $cast: safe enum conversion with error detection
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── $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 string

Debugging 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.

1

$display instead of $error: test passes when it should fail

SILENT FAILURE
Buggy Code
Checker using $display — silent CI pass
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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!
endfunction
Symptom

Checker 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.

Root Cause

$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.

Fix
Checker using $error — non-zero exit on mismatch
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ✅ 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 ✅
endfunction

Rule: every checker comparison must call $error on mismatch — no exceptions.

2

$display reads pre-NBA flip-flop value, logs wrong data

NBA TIMING
Buggy Code
$display on FF output reads stale state
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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
end
Symptom

The 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.

Root Cause

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.

Fix
Use $strobe to see post-NBA value
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ✅ 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
end

Switch to $strobe or observe signals in a waveform viewer, where you always see the correct post-NBA value.

3

$monitor left active forever, floods log and kills performance

LOG OVERLOAD
Buggy Code
Unbounded $monitor on busy bus
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ $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 slower
Symptom

Simulation 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.

Root Cause

$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.

Fix
Bracket $monitor with $monitoroff/$monitoron
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ✅ 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
end

Use $monitoroff/$monitoron to bracket specific debug windows, or use a coverpoint or assertion instead of $monitor for regression-safe observation.

4

$stop in committed testbench hangs CI runner

CI HANG
Buggy Code
$stop committed — CI never returns
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ $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
end
Symptom

CI 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.

Root Cause

$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.

Fix
$fatal terminates immediately, CI-safe
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ✅ $fatal stops immediately
initial begin
    run_stimulus();
    if (error_count > 0)
        $fatal(1,
            "%0d errors — FAIL",
            error_count);
    else
        $info("TEST PASSED");
    $finish;
end

Git hook solution: add a pre-commit hook that greps for $stop in testbench files and rejects the commit if found.

5

$readmemh silent partial load: uninitialized entries cause X-propagation

X-PROPAGATION
Buggy Code
No validation after $readmemh
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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 bug
Symptom

DUT 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.

Root Cause

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.

Fix
Validate and zero-init after $readmemh
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ✅ 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.