Chapter 9 of 16
Tasks and Functions: Reusable Behavior in SystemVerilog
Sort out the real difference between function and task, passing arguments by value vs. by reference (ref), default argument values, void functions, and a genuinely easy mistake to make: automatic vs. static lifetime.
Chapter 8's always_comb/always_ff/always_latch describe hardware behavior — each one models a piece of circuitry that's physically there, continuously reacting to its inputs. function and task, this chapter's subject, are a different kind of tool: not a description of hardware, but a way to package a piece of code — a calculation, a sequence of steps — so it can be called from multiple places instead of copy-pasted. Verification code in particular leans on this constantly, since a testbench has no hardware to describe in the first place, just behavior to reuse.
Like most programming languages, SystemVerilog has a mechanism for wrapping a piece of behavior into a reusable unit — except it offers two: function and task. Choosing between them isn't a style preference; it's dictated by one hard rule: whether the code is allowed to consume simulation time.
The core difference between function and task
function: executes in zero simulation time — it cannot contain#delay,@(event), orwaitstatements, anything that would advance simulation time. It must return a value (or be explicitly declaredvoid), and can appear inside an expression (e.g.y = parity(data) + 1;).task: can consume simulation time — it's free to use#delay,@(event), and wait for signal changes internally. It doesn't need to return a value, and can have multipleoutput/inoutarguments. It can only be called as a standalone statement, never inside an expression.
// function: computes a value instantly, usable inside an expression
function bit parity(input logic [7:0] data);
parity = ^data; // reduction XOR: XOR all the bits together
endfunction
initial begin
logic [7:0] d = 8'b1011_0110;
$display("parity = %b", parity(d));
end// task: advances through time, driving data onto pins bit by bit
task automatic drive_byte(input logic [7:0] data, ref logic sclk, ref logic sdata);
for (int i = 7; i >= 0; i--) begin
sdata = data[i];
#5 sclk = 1;
#5 sclk = 0;
end
endtaskA simple rule of thumb: need to compute a value instantly and use it inside an expression? Use a function. Need to do something that unfolds over time (like driving pins cycle by cycle)? Use a task.
Argument direction: input, output, inout
Arguments default to input (read-only, passed in by the caller). output writes a result back to the caller's variable; inout both reads an initial value and writes back:
task automatic sum_and_diff(input int a, input int b, output int sum, output int diff);
sum = a + b;
diff = a - b;
endtask
initial begin
int s, d;
sum_and_diff(10, 3, s, d);
$display("sum=%0d diff=%0d", s, d); // sum=13 diff=7
endPassing by value vs. by reference: ref
Arguments are passed by value by default — the call copies the actual argument into the parameter, and changes inside the function/task to the parameter don't affect the caller's original variable (output/inout are the exception, with their write-back defined explicitly by the language). But if the argument is a large array, queue, or struct, copying it on every call wastes performance, and sometimes you genuinely want the task to operate on the caller's original data directly — that's what ref is for, declaring pass by reference:
task automatic clear_queue(ref int q[$]);
q.delete(); // clears the caller's actual queue, not a copy of it
endtask
initial begin
int my_q[$] = '{1, 2, 3};
clear_queue(my_q);
$display("size = %0d", my_q.size()); // 0
endA ref argument has no copy overhead, and changes inside the task are reflected directly in the caller's variable — this is the standard way to pass large data structures around in verification code.
Default argument values
Arguments can declare default values just like in many modern languages — omit the corresponding actual argument at the call site to use the default:
function automatic int add(int a, int b = 10);
add = a + b;
endfunction
initial begin
$display("%0d", add(5)); // 15, b uses its default value of 10
$display("%0d", add(5, 20)); // 25, b passed explicitly
endvoid functions
If a function exists purely for its side effect (like printing a log line) and doesn't need to return a value, it can be declared void — it can then no longer appear inside an expression, only be called as a standalone statement, making it semantically closer to a task, while still following a function's rule of never consuming simulation time:
function void print_banner(string msg);
$display("==== %s ====", msg);
endfunction
initial begin
print_banner("hello");
endConversely, if a function does return a value but the caller doesn't need it this time, SystemVerilog requires explicitly saying "I know this returns something, and I'm intentionally not using it" — that's exactly what void'(inbox.pop_front()) meant back in the arrays chapter: pop_front() is a method with a return value (the popped element), and void'(...) explicitly casts that call's return value to void, telling the tool this was intentional rather than a mistake.
automatic vs. static lifetime: a genuinely easy mistake
This is the most important — and most commonly overlooked — point in this chapter. A task/function declared inside a module defaults to static lifetime, meaning its local variables (including its arguments) have only one storage location, shared across every call. If that task is called recursively, or called concurrently from multiple initial/always blocks, different calls will stomp on each other's local variables:
// Defaults to static: concurrent calls share the same local-variable storage
task count_up(int n);
for (int i = 0; i < n; i++) begin
#1;
$display("i = %0d", i);
end
endtask
initial begin
fork
count_up(3);
count_up(3);
join
endIn the code above, count_up has the default static lifetime, so the two concurrent calls share the same storage for i (and for the argument n too) — the two invocations overwrite each other's loop variable, producing garbled output instead of two independent runs of 0, 1, 2. The fix is simply adding the automatic keyword:
task automatic count_up(int n);
for (int i = 0; i < n; i++) begin
#1;
$display("i = %0d", i);
end
endtaskWith automatic, every call gets its own independent local-variable storage (the default behavior you'd expect from function calls in most software languages), so the two concurrent calls no longer interfere. Rule of thumb: you should almost always add automatic explicitly to a task/function, unless you genuinely need state shared across calls (rare, and usually better expressed some other way, like a module-level variable).
Summary
function | task | |
|---|---|---|
Can consume simulation time (#/@/wait)? | No | Yes |
| Must return a value? | Yes (or explicitly void) | No |
| Can appear inside an expression? | Yes | No |
Can have multiple output/inout arguments? | Possible but uncommon | Common |
- Arguments are passed by value by default; use
refto avoid copy overhead or to modify the caller's data directly. - Arguments can declare default values, used whenever the call site omits them.
voidfunctions are for pure side effects with no return value; for a call whose return value you're intentionally ignoring, usevoid'(...)to say so explicitly.- A
task/functiondeclared inside a module defaults tostatic(local variables shared across every call), which recursive or concurrent calls can corrupt; you should almost always addautomaticexplicitly so every call gets independent local variables.
Which statement accurately describes the difference between function and task?
What's the main effect of adding the ref keyword to a task/function argument?
A task/function declared inside a module defaults to a lifetime where local variables are shared across every call. Which keyword, added before the declaration, gives each call its own independent local variables? (lowercase)