HomeSystemVerilog VerificationDay 4 — Tasks & Functions
DAY 4 · SV FUNDAMENTALS

SystemVerilog Tasks vs Functions — automatic, void, ref, const ref

By EcrioniX · Updated Jun 12, 2026

SystemVerilog tasks and functions are the building blocks of every testbench. Understanding the difference between them — especially time-consuming tasks vs zero-time functions, automatic vs static lifetime, and ref vs const ref arguments — separates engineers who write correct re-entrant testbenches from those who spend days debugging mysterious simulation races.

What is the difference between a task and a function in SystemVerilog?

The single most important rule: tasks can consume simulation time; functions cannot. A function must execute and return in the same simulation time step it was called — it cannot contain @(posedge clk), #delay, wait(), or any other time-consuming statement. A task has no such restriction and is the correct construct for driving bus transactions, waiting for acknowledgements, and implementing protocol state machines.

FeatureTaskFunction
Consumes time✅ Yes — can contain @, #, wait()❌ No — zero simulation time
Return value❌ No return value (uses output args)✅ Returns a single value (or void)
Can call tasks✅ Yes❌ No (would violate zero-time rule)
Can call functions✅ Yes✅ Yes
Argument directionsinput, output, inout, ref, const refinput, ref, const ref (no output in SV2012+: use ref)
Default lifetime (module)staticstatic
Default lifetime (class)automaticautomatic
Typical useDrive UART/AXI/SPI transactions, wait for response, watchdogCRC calculation, data packing, scoreboard checks

What is automatic lifetime in SystemVerilog and why does it matter?

When a task or function has static lifetime (the default in modules), all its local variables live in a single shared storage location. If two simulation threads call the same task simultaneously, they share those variables and corrupt each other. This is the classic re-entrancy bug: you fork two calls to the same task, they run in parallel, and both overwrite the same loop counter or return variable.

Automatic lifetime creates a fresh stack frame for each call, just like a C function. Local variables are private to each invocation. In module-based testbenches, always declare tasks and functions as automatic if they will be called from forked threads.

Static lifetime bug: the silent disaster

If you have task drive_bus(input [7:0] data) in a module (static by default), and your testbench forks 4 threads all calling drive_bus simultaneously, the local variable data is shared. All 4 threads will read/write the same memory location, producing completely wrong results with no error message. Fix: task automatic drive_bus(...).

How do argument directions work in SystemVerilog tasks and functions?

input — pass by value, read-only inside

The default. A copy of the caller's value is passed in. The task/function cannot modify the caller's original variable.

output — write-only inside, value returned on exit

The argument is written inside the task and the value is copied back to the caller's variable when the task finishes. Not the same as a return value — it is copied back at exit, not continuously.

inout — read and write

The value is copied in on call, can be read and modified inside, and is copied back on exit. Use sparingly — ref is usually clearer and more efficient.

ref — true pass-by-reference

ref passes the actual variable, not a copy. The task/function operates directly on the caller's variable. Critical for large arrays — passing a 10,000-element array by input copies every element; passing by ref copies a pointer. Also allows the task to modify the caller's variable continuously (not just at exit).

const ref — read-only pass-by-reference

const ref is like ref but the function cannot modify the variable. This is the right choice for large arrays passed to a read-only analysis function: efficient (no copy) and safe (cannot accidentally modify).

When to use each argument direction

What are void functions in SystemVerilog?

A void function (function void check_result(...)) performs an action but returns no value. It is preferred over a task for operations that must consume zero simulation time and do not return a value — like writing to a log, incrementing a counter, or performing an assertion check. The compiler enforces that a void function cannot contain time-consuming statements, catching errors early.

How do recursive functions work in SystemVerilog?

Recursive functions require automatic lifetime — each recursive call needs its own stack frame. A static function would have one shared set of local variables, making recursion impossible. Declare function automatic int factorial(int n) and the simulator correctly creates a new stack frame per call. Never attempt recursion with a static function — the results will be wrong or the simulator may crash.

How do fork...join and disable fork work inside tasks?

Tasks can contain fork...join, fork...join_any, and fork...join_none to spawn parallel threads. This is the foundation of testbench parallelism:

disable fork kills all threads spawned in the current scope. The classic watchdog timer pattern uses fork...join_any with two threads: one doing the actual work, one counting timeout cycles. Whichever finishes first wins. Then disable fork kills the other.

Complete tasks and functions demonstration

tasks_demo.sv
// ============================================================
// tasks_demo.sv — Tasks, Functions, automatic, ref, recursive
// EcrioniX · SV Verification Course · Day 4
// ============================================================

// --- Standalone automatic task: drive a UART byte ---
// 'automatic' is critical if called from multiple fork threads
task automatic drive_uart(
  input logic       clk,
  ref   logic       tx,
  input logic [7:0] data,
  input int         baud_cycles   // cycles per UART bit
);
  // Start bit (low)
  tx = 1'b0;
  repeat(baud_cycles) @(posedge clk);
  // 8 data bits, LSB first
  for (int i = 0; i < 8; i++) begin
    tx = data[i];
    repeat(baud_cycles) @(posedge clk);
  end
  // Stop bit (high)
  tx = 1'b1;
  repeat(baud_cycles) @(posedge clk);
endtask

// --- Void function: check expected vs actual ----
function automatic void check_response(
  input logic [7:0] actual,
  input logic [7:0] expected,
  input string      test_name
);
  if ($isunknown(actual)) begin
    $error("[%s] DUT output has X: %0b", test_name, actual);
    return;
  end
  if (actual !== expected)
    $error("[%s] FAIL: got 8'h%0h, expected 8'h%0h", test_name, actual, expected);
  else
    $display("[%s] PASS: 8'h%0h", test_name, actual);
endfunction

// --- Pure calculation function: CRC-8 ---
function automatic logic [7:0] calc_crc8(
  const ref logic [7:0] data_arr[],  // large array passed by const ref
  input int             len
);
  logic [7:0] crc = 8'hFF;
  for (int i = 0; i < len; i++) begin
    crc = crc ^ data_arr[i];
    for (int b = 0; b < 8; b++) begin
      if (crc[7]) crc = (crc << 1) ^ 8'h07;
      else        crc = crc << 1;
    end
  end
  return crc;
endfunction

// --- Recursive function: requires automatic! ---
function automatic longint factorial(input int n);
  if (n <= 1) return 1;
  else        return n * factorial(n - 1);
endfunction

// --- Watchdog timer task ---
// Fork two threads: work thread + timeout thread
// join_any stops when first finishes; disable fork kills the other
task automatic with_timeout(
  input  int     timeout_cycles,
  input  logic   clk
);
  bit timed_out = 0;
  fork
    begin  // -- thread 1: the work (caller fills this in externally) --
      // Placeholder: wait for DUT done signal
      @(posedge clk);  // replace with actual done condition
    end
    begin  // -- thread 2: timeout counter --
      repeat(timeout_cycles) @(posedge clk);
      timed_out = 1;
      $error("WATCHDOG: operation timed out after %0d cycles", timeout_cycles);
    end
  join_any
  disable fork;         // kill whichever thread is still running
  if (!timed_out) $display("Operation completed within timeout.");
endtask

// ============================================================
module tasks_demo;

  logic clk = 0;
  always #5 clk = ~clk;  // 100 MHz clock

  logic tx;

  initial begin
    // --- Factorial: recursive automatic function ---
    $display("5! = %0d", factorial(5));    // 120
    $display("10! = %0d", factorial(10));  // 3628800

    // --- CRC-8 on a data buffer ---
    logic [7:0] buf[] = new[4];
    buf = '{8'hDE, 8'hAD, 8'hBE, 8'hEF};
    $display("CRC-8 = 8'h%0h", calc_crc8(buf, 4));

    // --- Check function ---
    check_response(8'hA5, 8'hA5, "read_test_1"); // PASS
    check_response(8'hFF, 8'h00, "read_test_2"); // FAIL

    // --- Drive UART byte --- (tx driven by ref)
    tx = 1'b1; // idle high
    @(posedge clk);
    drive_uart(clk, tx, 8'h55, 10);  // 10 cycles per bit
    $display("UART transmission complete");

    // --- Two parallel UART drives (automatic ensures no corruption) ---
    fork
      drive_uart(clk, tx, 8'hAA, 10);
      // In real TB: second UART instance would drive its own tx2
    join

    $finish;
  end

endmodule

Day 4 takeaways

Frequently Asked Questions

What is the difference between a task and a function in SystemVerilog?

Tasks can consume simulation time (contain @, #delay, wait()) and use output arguments to return multiple values. Functions execute in zero simulation time and return exactly one value (or void). If you need to drive a bus transaction or wait for a response, use a task. If you need to compute a value (CRC, parity, address decode), use a function.

What is automatic lifetime in SystemVerilog?

Automatic lifetime means each call gets its own private stack frame with independent local variables. Static lifetime (module default) shares one set of variables across all concurrent calls — breaking re-entrant testbenches. Always use task automatic and function automatic in module-based testbenches with forked threads. Class methods are automatic by default.

When should I use ref arguments in SystemVerilog?

Use ref for large arrays to avoid the cost of copying all elements. Use const ref for large arrays that are read-only. Use ref when you need the task to update the caller's variable continuously (not just on exit). Avoid ref for simple scalar inputs — input is cleaner and safer.

How do I implement a task timeout in SystemVerilog?

Use fork...join_any with two threads — one doing the real work, one counting timeout cycles with repeat(N) @(posedge clk). After join_any returns (whichever thread finished first), call disable fork to kill the remaining thread. Check a timed_out flag to know which one won.

Previous
← Day 3: Arrays, Queues & Associative Arrays

← Full course roadmap