Skip to content

SystemVerilog · Module 6

Task Declarations & Time-Consuming Calls

Task anatomy, timing controls (#, @, wait), arguments, disable, fork-join, class tasks, and APB driver patterns.

Module 6 · Page 6.3

Tasks are the workhorses of every testbench — every bus driver, every stimulus generator, every protocol transaction is a task. This page covers the full task syntax, every timing control form, output arguments, how to run tasks in parallel, the simulator mechanics that make tasks behave the way they do, the static-task bug that haunts every verification engineer, advanced production patterns, five real debugging labs, and ten interview-ready questions.

Anatomy of a Task — Every Part Labelled

A task looks similar to a function but has key structural differences: no return type, all three argument directions are allowed, and timing controls can appear anywhere in the body.

SystemVerilog — annotated task body (APB write)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ① 'task' + 'automatic' — always use for testbench tasks
// ② input args — copy-in at call, read-only inside task
// ③ output arg — written by task, copy-out on return
// ④ Timing controls (@, #, wait) — suspend the process
// ⑤ Output written here — caller reads it after return
task automatic apb_write (
    input  logic [31:0] addr,
    input  logic [31:0] wdata,
    output logic        slverr
);
    @(posedge clk);
    psel = 1; paddr = addr; pwrite = 1;
    @(posedge clk);
    penable = 1; pwdata = wdata;
    @(posedge clk);
    slverr = pslverr;        // write to output arg
    psel = 0; penable = 0;
endtask

The Three Timing Controls Inside Tasks

Tasks are the only place in SystemVerilog where you can write timing controls that suspend execution and let simulation time advance. There are three forms, each waiting for a different condition.

SystemVerilog — all three timing controls in tasks
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── ① #delay — wait a fixed number of time units ─────────────────
task automatic apply_reset (input int reset_cycles);
    rst_n = 1'b0;
    #(reset_cycles * CLK_PERIOD);   // wait N clock periods in time units
    rst_n = 1'b1;
    #(2 * CLK_PERIOD);              // 2-cycle settling time
endtask
 
 
// ── ② @event — wait for a signal change or edge ─────────────────
task automatic wait_cycles (input int n);
    repeat(n) @(posedge clk);       // wait exactly N rising clock edges
endtask
 
task automatic wait_negedge ();
    @(negedge clk);                 // wait for falling edge
endtask
 
task automatic wait_signal_change (input logic sig);
    @(sig);                         // wait for ANY change on sig (posedge or negedge)
endtask
 
task automatic wait_rising (input logic sig);
    @(posedge sig);                 // wait for 0→1 transition on sig
endtask
 
 
// ── ③ wait() — level-sensitive wait ─────────────────────────────
task automatic wait_for_ready ();
    wait(ready == 1'b1);            // suspend until ready is HIGH
    @(posedge clk);                 // then align to clock
endtask
 
task automatic wait_for_idle ();
    wait(state == IDLE);            // wait until FSM is idle
endtask
 
 
// ── Mixing all three in one task ─────────────────────────────────
task automatic uart_send_byte (input logic [7:0] data);
    wait(tx_ready);                 // wait until transmitter is ready
    @(posedge clk); tx_data = data; tx_valid = 1;
    @(posedge clk); tx_valid = 0;   // deassert after one cycle
    #100;                           // guard time before next byte
endtask

Task Arguments — All Three Directions

Unlike functions (which only use input), tasks support all three argument directions: input, output, and inout. This is how a task communicates results back to the caller without using a return value.

SystemVerilog — task with input, output, and inout arguments
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Task with all three argument directions ───────────────────────
task automatic apb_read (
    input  logic [31:0] addr,     // ① input: address to read (caller → task)
    output logic [31:0] rdata,    // ② output: data read back  (task → caller)
    output logic        slverr    // ② output: error flag       (task → caller)
);
    @(posedge clk);
    psel   = 1; paddr = addr; pwrite = 0;
    @(posedge clk);
    penable = 1;
    @(posedge clk);
    // Write results to output ports before returning
    rdata  = prdata;     // captured from DUT
    slverr = pslverr;
    psel = 0; penable = 0;
endtask
 
// ── Caller reads the output ports after task returns ─────────────
logic [31:0] read_val;
logic        err;
 
apb_read(32'h1000, read_val, err);   // read_val and err updated on return
$display("Read: %h  err=%b", read_val, err);
 
 
// ── inout: a counter that increments across multiple calls ────────
task automatic send_and_count (
    input  logic [7:0] data,
    inout  int         tx_count   // read current count, increment, write back
);
    @(posedge clk); tx_reg = data;
    tx_count = tx_count + 1;       // inout: read AND write
endtask
 
int total_sent = 0;
send_and_count(8'hAA, total_sent);  // total_sent = 1 after call
send_and_count(8'hBB, total_sent);  // total_sent = 2 after call
DirectionData FlowInside Task Can...Use When
inputCaller → Task (copy-in)Read only — cannot assignPassing stimulus, addresses, data to send
outputTask → Caller (copy-out)Write only — undefined until writtenReturning read data, status, error flags
inoutBoth directionsRead AND writeIncrementing a shared counter, modifying a state variable
refShared alias — no copyRead AND write (immediate)Large arrays, when caller must see updates as they happen (covered in 6.5)

Complete Protocol Driver — APB Bus

Here is a production-quality APB (Advanced Peripheral Bus) driver with separate write and read tasks, proper handshaking, error checking, and a watchdog timeout — exactly as you would write it in a real project.

SystemVerilog — complete APB bus driver (write + read + watchdog)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── APB interface signals (module-level) ─────────────────────────
logic        pclk, presetn;
logic        psel, penable, pwrite, pready, pslverr;
logic [31:0] paddr, pwdata, prdata;
 
localparam int TIMEOUT_CYCLES = 1000;
 
 
// ── APB Write task ────────────────────────────────────────────────
task automatic apb_write (
    input  logic [31:0] addr,
    input  logic [31:0] data,
    output logic        err
);
    int timeout_cnt = 0;
 
    // ① SETUP phase — drive address and control
    @(posedge pclk);
    psel = 1; paddr = addr; pwdata = data; pwrite = 1; penable = 0;
 
    // ② ACCESS phase — assert penable
    @(posedge pclk);
    penable = 1;
 
    // ③ Wait for PREADY with timeout protection
    @(posedge pclk);
    while (!pready && timeout_cnt < TIMEOUT_CYCLES) begin
        @(posedge pclk);
        timeout_cnt++;
    end
 
    if (timeout_cnt == TIMEOUT_CYCLES)
        $fatal(1, "APB write timeout at addr=0x%h", addr);
 
    // ④ Capture result, deassert bus
    err    = pslverr;
    psel   = 0; penable = 0; pwrite = 0;
 
    if (err) $error("APB SLVERR on write to 0x%h", addr);
endtask
 
 
// ── APB Read task ─────────────────────────────────────────────────
task automatic apb_read (
    input  logic [31:0] addr,
    output logic [31:0] rdata,
    output logic        err
);
    int timeout_cnt = 0;
    @(posedge pclk);
    psel = 1; paddr = addr; pwrite = 0; penable = 0;
    @(posedge pclk);
    penable = 1;
    @(posedge pclk);
    while (!pready && timeout_cnt < TIMEOUT_CYCLES) begin
        @(posedge pclk); timeout_cnt++;
    end
    if (timeout_cnt == TIMEOUT_CYCLES) $fatal(1, "APB read timeout");
    rdata  = prdata;
    err    = pslverr;
    psel   = 0; penable = 0;
endtask
 
 
// ── Test sequence using the tasks ─────────────────────────────────
initial begin
    logic [31:0] rd_data;
    logic        wr_err, rd_err;
 
    wait_cycles(5);                             // wait 5 clocks after reset
    apb_write(32'h0000_0010, 32'hCAFE_0001, wr_err);
    apb_read (32'h0000_0010, rd_data, rd_err);
    $display("Readback: %h (err=%b)", rd_data, rd_err);
    $finish;
end

Disabling a Task — Stopping Mid-Execution

Sometimes you need to abort a running task before it finishes — for example, when a timeout fires, when an error is detected, or when a fork-join branch completes and you no longer need the other branch. SystemVerilog provides disable task_name; to immediately terminate a named task.

SystemVerilog — disable task and disable fork
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Named task that can be disabled ──────────────────────────────
task automatic long_transaction;
    begin : txn_body              // ← named block inside task
        @(posedge clk); drive_addr();
        @(posedge clk); drive_data();
        repeat(10) @(posedge clk); // long wait
        capture_response();
    end
endtask
 
// Disable from outside — stops the task immediately
initial begin
    fork
        long_transaction();          // runs until disabled
        begin
            #500;
            disable long_transaction; // abort after 500 time units
        end
    join
end
 
 
// ── disable fork — stop ALL threads spawned by fork ──────────────
initial begin
    fork
        drive_stimulus();
        monitor_bus();
        begin
            #10000;
            $display("Simulation timeout!");
            disable fork;               // kills all threads in this fork
        end
    join_any                          // exit when any one thread finishes
    disable fork;                     // clean up remaining threads
end

Fork-Join with Tasks — Parallel Execution

Tasks can be launched in parallel using fork...join, fork...join_any, and fork...join_none. Each thread in the fork runs its own task concurrently.

SystemVerilog — fork-join forms with tasks
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── fork...join: wait for ALL tasks ─────────────────────────────
fork
    drive_master_port();    // both tasks start at the same time
    drive_slave_port();
join                        // resume only when BOTH finish
$display("Both ports done");
 
 
// ── fork...join_any: race to finish + cleanup ────────────────────
fork
    wait_for_ack();         // waits until ack received
    begin
        #5000;
        $display("TIMEOUT waiting for ack!");
    end
join_any                    // fires when FIRST branch completes
disable fork;               // kill the other branch
$display("Ack received OR timeout");
 
 
// ── fork...join_none: fire and forget ───────────────────────────
fork
    background_monitor();   // runs in background — never blocks caller
join_none                   // caller continues immediately
$display("Monitor started in background");
 
// ── Practical: run multiple transactions in parallel ─────────────
initial begin
    logic e0, e1, e2;
    fork
        apb_write(32'h0, 32'h1, e0);  // 3 writes in parallel
        apb_write(32'h4, 32'h2, e1);
        apb_write(32'h8, 32'h3, e2);
    join                               // all three must finish
    $display("All writes done: errs=%b%b%b", e0, e1, e2);
end

Tasks Inside Classes — UVM Foundation

Tasks are not limited to modules — they can be defined inside class bodies. This is the foundation of UVM: every driver, every monitor, and every sequence is a class with tasks as its primary interface. Class tasks declared in a class are always automatic by default.

SystemVerilog — tasks inside a class (UVM pattern preview)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Class-based driver: tasks as primary interface ────────────────
class ApbDriver;
 
    // Interface handle (virtual interface — not covered yet)
    virtual apb_if vif;
 
    // Tasks are always automatic inside a class
    task write (input logic [31:0] addr, data,
                output logic         err);
        @(posedge vif.pclk);
        vif.psel = 1; vif.paddr = addr;
        vif.pwdata = data; vif.pwrite = 1;
        @(posedge vif.pclk);
        vif.penable = 1;
        @(posedge vif.pclk);
        err = vif.pslverr;
        vif.psel = 0; vif.penable = 0;
    endtask
 
    task run();     // called by the testbench to start the driver
        logic [31:0] stimulus[$];    // stimulus queue
        logic wr_err;
        foreach (stimulus[i])
            write(32'h1000 + i, stimulus[i], wr_err);
    endtask
 
endclass
 
// Usage in testbench:
ApbDriver drv = new();
drv.vif = vif_handle;
drv.run();              // task call on a class object

Common Mistakes

SystemVerilog — task mistakes and fixes
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ════ MISTAKE 1: Reading an output port inside the task ══════════
task automatic bad1 (input logic a, output logic b);
    if (b == 1) ...;    // ❌ reading 'b' before writing it — undefined
endtask
// ✅ FIX: write to output first, or use inout if you need both
task automatic good1 (input logic a, inout logic b);
    if (b == 1) ...;    // ✅ inout: read current value, then write new value
    b = a;
endtask
 
 
// ════ MISTAKE 2: Missing automatic — shared static variables ══════
task bad2 (input int n);   // ❌ static: concurrent calls share 'n'
    repeat(n) @(posedge clk);
endtask
// ✅ FIX: always use automatic for testbench tasks
task automatic good2 (input int n);  // ✅ each call gets its own copy of 'n'
    repeat(n) @(posedge clk);
endtask
 
 
// ════ MISTAKE 3: Using task in always_comb ════════════════════════
always_comb begin
    apb_write(32'h0, data, err);  // ❌ task in always_comb — timing violation
end
// ✅ FIX: tasks belong in initial/always blocks, not always_comb
initial begin
    apb_write(32'h0, data, err);  // ✅ correct context
end
 
 
// ════ MISTAKE 4: Calling fork in join_any without disable fork ════
fork
    wait_ack();
    #5000;
join_any
// ❌ the losing branch still runs in the background!
// ✅ FIX: always disable fork after join_any
fork
    wait_ack();
    #5000;
join_any
disable fork;   // ✅ kills the background thread cleanly

Quick Reference — Task Cheat Sheet

SystemVerilog — task quick reference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Task declaration ─────────────────────────────────────────
task automatic task_name (
    input  <type> in_arg,    // caller → task (read-only inside)
    output <type> out_arg,   // task → caller (write-only inside)
    inout  <type> io_arg     // both ways
);
    // timing controls, logic, other task/function calls
endtask [: task_name]
 
// ── Timing controls ──────────────────────────────────────────
#10;                         // wait 10 time units
@(posedge clk);              // wait for rising clock edge
@(negedge clk);              // wait for falling clock edge
@(valid);                    // wait for any change on 'valid'
wait(ready == 1);            // level-sensitive wait
repeat(n) @(posedge clk);    // wait N clock cycles
 
// ── Calling a task ───────────────────────────────────────────
task_name(arg1, arg2);       // standalone statement — NOT in expression
 
// ── Parallel execution ───────────────────────────────────────
fork
    task_a(); task_b();
join           // wait for ALL
join_any       // wait for FIRST; then disable fork;
join_none      // return immediately; threads background
 
// ── Stopping tasks ───────────────────────────────────────────
disable task_name;           // kill a named task
disable fork;                // kill all forked threads
 
// ── Key rules ────────────────────────────────────────────────
// • Always use 'automatic' for testbench tasks
// • Tasks have no return value — use output/inout/ref ports
// • Tasks cannot be used inside expressions
// • Tasks are not synthesisable (if they contain timing)
// • Always 'disable fork' after 'join_any'

Simulation Mechanics — How the Simulator Executes a Task

Understanding what the simulator actually does when it encounters a task call separates engineers who merely write tasks from engineers who can debug them under pressure. A task is not a subroutine in the C sense — it is a suspendable process that participates in the event-driven simulation schedule alongside every other process in the design.

The APB Write Timing Walk-Through

Let us trace exactly what happens during a three-clock APB write transaction. Each @(posedge clk) suspends the task process and releases the simulator to advance time and evaluate everything else. The walk-through below shows the actual signal activity that results.

SystemVerilog — APB write task signal timeline (CLK_PERIOD = 10 ns)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// clk      ____/‾‾\____/‾‾\____/‾‾\____/‾‾\____
// psel     ________/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\________
// penable  ________________/‾‾‾‾‾‾‾‾\________
// pready   ________________________/‾‾‾‾\____
// Process  RUN▸SUSP@1 RUN▸SUSP@2 RUN▸SUSP@3  RUN→RET
//           │           │           │           │
//           T=0        T=10        T=20        T=30     (ns)

Delta Cycles: Zero-Time Ripple Evaluation

Within a single simulation timestamp, combinational logic re-evaluates in delta cycles — zero-duration steps that settle propagation ripple. If you drive a signal and immediately read a combinational output that depends on it, you may read the old value. This is the subtlest task-related bug in testbench writing.

SystemVerilog — delta cycle interaction in a task
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// DUT has a combinational path: data_out = f(data_in)
logic [7:0] data_in, data_out;
assign data_out = data_in ^ 8'hFF;   // combinational — updates in delta cycle
 
task automatic drive_and_check_wrong();
    data_in = 8'h0A;                   // drive at time T, delta 0
    // ❌ data_out still reflects OLD value here — assign hasn't fired yet
    $display("[WRONG] T=%0t: data_out=%h", $time, data_out);
    // data_out updates at T, delta 1 — we read it at T, delta 0
endtask
 
 
task automatic drive_and_check_correct();
    data_in = 8'h0A;
    #0;                               // ✅ advance 1 delta cycle (zero real time)
    // Now assign has fired — data_out = 0x0A ^ 0xFF = 0xF5
    $display("[CORRECT] T=%0t: data_out=%h", $time, data_out);
endtask
 
 
task automatic drive_and_check_best();
    @(posedge clk);
    data_in = 8'h0A;
    @(posedge clk);                   // ✅ best: wait full cycle — combinational fully settled
    $display("[BEST] stable: data_out=%h", data_out);
endtask
 
// Expected output:
// [WRONG]   T=5: data_out=xx  (or old value — tool-dependent)
// [CORRECT] T=5: data_out=f5
// [BEST]    T=15: data_out=f5

The Static Task Bug — A Real Silicon Project Debugging Story

This is the bug that haunts every verification engineer who forgets the automatic keyword. It does not crash your simulation — it silently produces wrong results. It appears intermittently. It disappears when you add $display statements to debug it. It took three days to find on a real SoC project. Here is exactly how it works.

❌ Static Task — The Silent Bug
No automatic — shared storage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task apb_write (
    // No 'automatic' keyword!
    input logic [31:0] addr,
    input logic [31:0] data
);
    // local_addr is STATIC — one copy
    // shared by ALL concurrent calls
    logic [31:0] local_addr;
 
    local_addr = addr;  // ← Thread A writes 0x100
    @(posedge clk);     // ← SUSPEND (Thread B runs)
    // Thread B writes local_addr = 0x200
    // Thread A resumes — local_addr is NOW 0x200!
    paddr = local_addr; // ← WRONG: sends 0x200 not 0x100
    @(posedge clk);
endtask
 
fork
    apb_write(32'h100, 32'hAABB); // Thread A
    apb_write(32'h200, 32'hCCDD); // Thread B
join
// Both writes go to 0x200!
✅ Automatic Task — Correct
With automatic — private stack frame
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task automatic apb_write (
    input logic [31:0] addr,
    input logic [31:0] data
);
    // local_addr is AUTOMATIC — each call
    // gets its own private copy on the stack
    logic [31:0] local_addr;
 
    local_addr = addr;  // Thread A stores 0x100
    @(posedge clk);     // SUSPEND
    // Thread B has ITS OWN local_addr = 0x200
    // Thread A resumes — its local_addr is still 0x100
    paddr = local_addr; // ← CORRECT: 0x100
    @(posedge clk);
endtask
 
fork
    apb_write(32'h100, 32'hAABB); // Thread A
    apb_write(32'h200, 32'hCCDD); // Thread B
join
// Each goes to its correct address.

Real Verification Architecture — How Tasks Compose a Testbench

In production verification environments, tasks are the building blocks of the entire testbench infrastructure. Every testbench component — clock generator, reset sequencer, bus driver, bus monitor, scoreboard checker — is ultimately a set of tasks calling each other. Understanding how they fit together is the foundation for reading and writing UVM environments.

Testbench ComponentPrimary TaskTiming ProfilePurpose
Clock Generatorgen_clock()Infinite loop with #delayDrives the design clock forever
Reset Sequencerapply_reset()N clock cyclesAssert/deassert reset at startup
Bus Driverwrite(), read()3–N clock cycles per transactionDrives protocol-correct bus transactions
Bus Monitormonitor_bus()Infinite loop, samples each cycleCaptures all bus activity non-intrusively
Scoreboardcheck_result()Zero-time (no delays)Compares expected vs actual — zero-time because it only reads signals
Timeout Watchdoginline fork branch#MAX_TIME then $fatalKills simulation if it hangs
SystemVerilog — complete testbench skeleton using tasks
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Complete testbench skeleton: clock, reset, driver, monitor ────
module tb_top;
 
  localparam CLK_PERIOD   = 10;   // ns
  localparam RESET_CYCLES = 8;
  localparam MAX_SIM_TIME = 100_000;
 
  logic        clk, rst_n;
  logic        psel, penable, pwrite, pready, pslverr;
  logic [31:0] paddr, pwdata, prdata;
 
  // ── ① Clock generator ──────────────────────────────────────────
  task automatic gen_clock;
    clk = 0;
    forever #(CLK_PERIOD/2) clk = ~clk;
  endtask
 
  // ── ② Reset sequencer ──────────────────────────────────────────
  task automatic apply_reset;
    rst_n = 1'b0;
    repeat(RESET_CYCLES) @(posedge clk);
    @(negedge clk);    // deassert on falling edge — avoids setup/hold issues
    rst_n = 1'b1;
    repeat(2) @(posedge clk);   // 2-cycle post-reset settling
  endtask
 
  // ── ③ APB Bus driver ───────────────────────────────────────────
  task automatic apb_write(
    input  logic [31:0] addr, data,
    output logic        err
  );
    int timeout = 0;
    @(negedge clk);              // drive on negedge to avoid setup violations
    psel = 1; paddr = addr; pwdata = data; pwrite = 1; penable = 0;
    @(negedge clk); penable = 1;
    @(posedge clk);
    while (!pready && ++timeout < 100) @(posedge clk);
    if (timeout == 100) $fatal(1, "APB write timeout @ 0x%h", addr);
    err = pslverr;
    @(negedge clk); psel = 0; penable = 0; pwrite = 0;
  endtask
 
  // ── ④ Bus monitor (runs forever in background) ─────────────────
  task automatic bus_monitor;
    forever begin
      @(posedge clk);
      if (psel && penable && pready) begin
        if (pwrite)
          $display("%0t [MON] WRITE addr=%h data=%h err=%b",
                   $time, paddr, pwdata, pslverr);
        else
          $display("%0t [MON] READ  addr=%h data=%h err=%b",
                   $time, paddr, prdata, pslverr);
      end
    end
  endtask
 
  // ── ⑤ Timeout watchdog ─────────────────────────────────────────
  task automatic watchdog;
    #MAX_SIM_TIME;
    $fatal(1, "Simulation timeout at %0t ns", $time);
  endtask
 
  // ── ⑥ Top-level test ───────────────────────────────────────────
  initial begin
    logic [31:0] rd_val;
    logic wr_err, rd_err;
 
    fork
      gen_clock();          // background — runs forever
      bus_monitor();        // background — runs forever
      watchdog();           // background — kills if test hangs
    join_none               // launch all three, don't wait
 
    apply_reset();          // blocks until reset complete
 
    apb_write(32'h0000_0100, 32'hCAFE_F00D, wr_err);
    apb_write(32'h0000_0104, 32'h1234_5678, wr_err);
 
    $display("[TEST] All writes done. Simulation PASS.");
    $finish;
  end
 
endmodule

Advanced Task Patterns Used in Real Projects

Beyond the basics, experienced verification engineers use a set of well-established task patterns that appear repeatedly across projects. Recognising these patterns and knowing when to apply them is a hallmark of a senior DV engineer.

Pattern 1 — Timeout-Protected Transaction Wrapper

SystemVerilog — timeout wrapper (fork-join_any + disable fork)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Reusable timeout wrapper: run any task with a deadline ────────
task automatic run_with_timeout (
    input int timeout_cycles,
    input string label
);
    int elapsed;
    fork
        begin : work
            apb_write(32'h1000, 32'hABCD, err);
            // add more work here — whatever the transaction is
        end
        begin : watchdog
            elapsed = 0;
            while (elapsed++ < timeout_cycles) @(posedge clk);
            $error("%s timed out after %0d cycles", label, timeout_cycles);
        end
    join_any      // whichever finishes first
    disable fork; // kill the other branch cleanly
endtask

Pattern 2 — Self-Checking Task

SystemVerilog — write_and_verify pattern
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task automatic write_and_verify (
    input  logic [31:0] addr,
    input  logic [31:0] exp_data
);
    logic [31:0] rd_data;
    logic wr_err, rd_err;
 
    apb_write(addr, exp_data, wr_err);   // write
    apb_read (addr, rd_data,  rd_err);   // read back
 
    if (wr_err || rd_err)
        $error("Bus error: wr=%b rd=%b @ 0x%h", wr_err, rd_err, addr);
    else if (rd_data !== exp_data)
        $error("MISMATCH @ 0x%h: wrote 0x%h got 0x%h", addr, exp_data, rd_data);
    else
        $display("PASS @ 0x%h: 0x%h", addr, rd_data);
endtask

Pattern 3 — Mailbox-Driven Driver (UVM Preview)

SystemVerilog — mailbox-driven driver (UVM preview)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A mailbox decouples transaction generation from bus driving.
// The generator puts transactions in; the driver task pulls and sends.
mailbox #(logic [31:0]) tx_mbx = new(0);   // unbounded mailbox
 
task automatic mailbox_driver;
    logic [31:0] data;
    logic err;
    int   idx = 0;
    forever begin
        tx_mbx.get(data);      // blocks until item available
        apb_write(32'h2000 + idx * 4, data, err);
        if (err) $error("Write error on txn %0d", idx);
        idx++;
    end
endtask
 
task automatic stimulus_generator;
    foreach (logic [31:0] payload[] = '{32'h1, 32'h2, 32'h3, 32'h4}) begin
        tx_mbx.put(payload);    // non-blocking put into mailbox
    end
endtask

Pattern 4 — Retry with Back-off

SystemVerilog — retry with exponential back-off
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task automatic apb_write_retry (
    input  logic [31:0] addr, data,
    input  int          max_retries = 3,
    output logic        final_err
);
    logic err;
    int   attempt = 0;
    do begin
        apb_write(addr, data, err);
        if (err && attempt < max_retries) begin
            $warning("SLVERR on attempt %0d — retrying", attempt);
            repeat(4 * (attempt + 1)) @(posedge clk); // exponential back-off
        end
    end while (err && ++attempt <= max_retries);
    final_err = err;
endtask

Debugging Academy — 5 Real Task Bugs Dissected

Every bug below has been observed in real verification projects. Each one is preceded by a symptom description that mirrors what you would actually see in simulation before you know the root cause. Work through each one as a diagnostic exercise before reading the explanation.

1

Static Variable Corruption in Parallel Fork Threads

SEVERITY: HIGH
Symptom

Scoreboard reports register mismatches for 3 of 10 parallel writes. The failing addresses change across regression runs. Adding a $display after the address assignment makes the failures disappear. Waveform shows paddr toggling correctly but the scoreboard sees transactions going to the wrong addresses.

Buggy Code

task apb_write(input logic [31:0] addr, data);missing the automatic keyword. All local variables including the addr copy-in register are stored in a single static location shared by all concurrent calls.

Root Cause

Static tasks have a single storage location for all local variables. When two concurrent calls both execute local_addr = addr but are then interleaved at the @(posedge clk) suspension point, the second call overwrites the value that the first call stored. On resumption, the first call reads the corrupted value.

Fix

task automatic apb_write(...) — each call now gets its own private stack frame. Arguments and locals are independent across all concurrent invocations.

2

Ghost Thread Scoreboard Errors After Test Completes

SEVERITY: MEDIUM
Symptom

Test appears to pass — "$finish reached, test PASS" is printed. But 200 ns later, the simulation suddenly prints "$error: TIMEOUT waiting for ack!" and terminates abnormally. CI marks the test as FAIL.

Buggy Code

The join_any fires when wait_for_ack() finishes but the timeout branch is still running in the background. It fires 500 ns after the test finishes.

Missing disable fork after join_any
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fork
  wait_for_ack();    // completes at T=500
  begin
    #1000;           // ← timeout branch, never killed!
    $error("TIMEOUT waiting for ack!");
  end
join_any
// ❌ Missing: disable fork;
$display("Test PASS"); $finish;
Root Cause

join_any only exits the join construct when the first thread finishes — it does not kill the other threads. They continue running as background processes. Even after $finish, some simulators will still execute pending background events before terminating.

Fix

Add disable fork; immediately after every join_any. This is an unconditional rule: join_any is always followed by disable fork.

3

Simulation Deadlock — wait() on a Signal Never Driven

SEVERITY: HIGH
Symptom

Simulation runs forever. Memory usage grows. No output appears after "apply_reset() complete". Ctrl-C in Questa shows the active process is suspended at wait(dut.status_valid == 1). The simulation time has stopped advancing.

Buggy Code

dut.status_valid is driven by a register block that is only enabled when a specific mode bit is set. The test forgot to write the mode register before calling this task.

wait() that blocks forever
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task automatic wait_for_status;
  wait(dut.status_valid == 1);  // ← blocks forever
  @(posedge clk);
endtask
Root Cause

wait() on a condition that is never satisfied causes the process to suspend permanently. Unlike an event wait (@signal), wait() does not time out — it waits indefinitely. With no other active processes to advance time, the simulation stalls.

Fix

Two-part fix: (1) Write the mode register before calling the task. (2) Add a timeout wrapper around every wait():

Wrap wait() with a timeout watchdog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fork
  begin wait(dut.status_valid); end
  begin #MAX_WAIT; $fatal(1, "status_valid never asserted"); end
join_any
disable fork;
4

Race Condition: Output Argument Captured One Cycle Too Early

SEVERITY: MEDIUM
Symptom

An APB read task occasionally returns stale data. The waveform shows prdata is correct one cycle after the task captures it — as if the task is sampling one clock cycle too early. The bug disappears when simulation runs with a slower clock or with +delay options enabled.

Buggy Code

The task samples pready and prdata on the same clock edge it asserted penable. The DUT's response requires one pipeline stage — it asserts pready on the following posedge.

Sampling on the same edge as penable assert
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task automatic apb_read_buggy(...);
  @(posedge pclk);
  penable = 1;
  @(posedge pclk);
  // ❌ Sampling pready and prdata on the SAME posedge
  // that drives penable. DUT needs one cycle to respond.
  if (pready) rdata = prdata;
endtask
Root Cause

Simulation-time race: both the testbench and the DUT's always_ff respond to the same posedge in the same delta-cycle step. The testbench reads prdata before the DUT's flip-flops have updated it. This is the "testbench reads before RTL updates" race class.

Fix

Sample on the next posedge:

Wait one more posedge for DUT response
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
@(posedge pclk); penable = 1;
@(posedge pclk);             // wait for DUT response
rdata = prdata;

Alternatively, drive on negedge and sample on posedge for maximum setup margin. This is the standard APB protocol timing.

5

output Argument Holds X When Task Is disable-d Mid-Execution

SEVERITY: LOW
Symptom

After a fork...join_any with a timeout branch, the caller reads the task's output argument and sees X. The checker flags a spurious failure. The task never reached the line where it wrote to the output port.

Buggy Code

The caller's rdata variable is never written because the task was killed by disable fork before reaching the assignment.

Task killed before writing outputs
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task automatic apb_read(output logic [31:0] rdata, output logic err);
  @(posedge clk); psel = 1; ...
  @(posedge clk); penable = 1;
  @(posedge clk);
  // timeout fires HERE — task killed before this line:
  rdata = prdata;   // ← never executed
  err   = pslverr;  // ← never executed
endtask
Root Cause

output arguments are only copied out when the task returns normally. If a task is killed by disable, the copy-out never happens. The caller's variables retain whatever value they had before the call (which may be X for uninitialized logic).

Fix

Initialise output arguments at declaration time:

Default outputs to deterministic values
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task automatic apb_read(
    output logic [31:0] rdata     = '0,
    output logic        err       = 1'b1,
    output logic        completed = 1'b0
);
    // ... transaction body ...
    completed = 1'b1;   // ← set at the very end
endtask

Then even if the task is killed, the caller sees a deterministic default rather than X. The extra completed flag tells the caller whether the task finished normally.

Interview Q&A — Tasks & Timing Controls

The critical difference is time consumption. A function is zero-time — it evaluates combinationally and returns in the same simulation step it was called. A task can consume simulation time by containing timing controls (@, #, wait). Structurally: functions have a return value and only accept input arguments; tasks have no return value and accept input, output, and inout. Tasks can call functions; functions cannot call tasks (because tasks can advance time, breaking the zero-time constraint of functions).