SystemVerilog Task and Function: A Practical Guide
In SystemVerilog, a task and a function are the two ways to package reusable procedural code — the subroutines of the language. Knowing when to reach for each, and how their arguments and variable lifetimes behave, is one of the first skills every RTL and verification engineer needs. This guide walks through the full picture of the SystemVerilog task function pair with small, runnable examples you can read top to bottom.
The headline distinction is simple: a function must execute in zero simulation time and returns a value, while a task may consume time — it can contain # delays, @ event controls and wait statements — and returns nothing through a return value. Everything else (arguments, lifetimes, void returns) builds on that one rule.
Task vs function at a glance
| Property | Function | Task |
|---|---|---|
Can consume time (#, @, wait) | No | Yes |
| Returns a value | Yes (or void) | No return value |
| Can call a task | No | Yes |
| Can call a function | Yes | Yes |
| Argument directions | input output inout ref | input output inout ref |
| Usable inside an expression | Yes | No |
Because a function finishes instantly, it is the right tool for pure computation — decoding, arithmetic, address translation. A task is the right tool when you need to drive stimulus over time, such as a bus write that spans several clock edges.
Functions: returning a value, or none at all
A function declares a return type, takes zero or more arguments, and hands back a result. You can return a value either by assigning to the function name or by using an explicit return statement — the modern, readable choice. Because a function call is an expression, it can appear anywhere a value is expected.
module tb;
// function that computes and returns a value
function int square(int x);
return x * x;
endfunction
// void function: does work, returns nothing
function void show(string label, int val);
$display("%-12s = %0d", label, val);
endfunction
initial begin
int a;
int r;
a = 5;
r = square(a);
show("square(5)", r);
show("square(9)", square(9)); // call used as an expression
$finish;
end
endmodulesquare(5) = 25 square(9) = 81 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpaln3w0r8/func_basics.sv:19: $finish called at 0 (1s)
When a function performs an action but has nothing meaningful to hand back — printing, updating a scoreboard, asserting a check — declare it void. A void function can be called as a standalone statement, exactly like a task, yet it is still guaranteed to take zero time. In the example above, show is a void function used purely for its side effect of printing.
Automatic versus static lifetime
Every variable inside a subroutine has a lifetime that decides whether its storage is shared across calls or created fresh each time. A static variable has a single copy that persists for the whole simulation; an automatic variable is allocated on entry and discarded on return.
In a module, program or package, a subroutine is static by default, which means a plain local variable keeps its value between calls — a frequent source of bugs when the routine is called from several places. Marking the routine automatic gives each call (and each concurrent invocation) its own private storage. The two counters below are identical except for that one keyword.
module tb;
// STATIC function: one shared copy of count survives every call
function int static_counter();
static int count = 0; // initialised once, at time 0
count++;
return count;
endfunction
// AUTOMATIC function: fresh count allocated on each call
function automatic int auto_counter();
int count = 0; // re-created and re-initialised every call
count++;
return count;
endfunction
initial begin
int s1, s2, s3;
int a1, a2, a3;
s1 = static_counter();
s2 = static_counter();
s3 = static_counter();
a1 = auto_counter();
a2 = auto_counter();
a3 = auto_counter();
$display("static_counter : %0d %0d %0d", s1, s2, s3);
$display("auto_counter : %0d %0d %0d", a1, a2, a3);
$finish;
end
endmodulestatic_counter : 1 2 3 auto_counter : 1 1 1 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpjdhptby4/lifetime.sv:27: $finish called at 0 (1s)
The static counter climbs 1, 2, 3 because the single count survives; the automatic counter reports 1, 1, 1 because its count is reborn and re-initialised on every call. As a rule of thumb, make reusable routines — especially anything called from multiple initial or always blocks or forked threads — automatic, and reserve static for deliberate state that must persist.
You can also override the lifetime of a single variable: an automatic function may declare a static variable inside it, and vice versa, so you keep fine-grained control.
Argument directions: input, output, inout and ref
Both tasks and functions accept four argument directions. The default, when you write none, is input.
input— copied in when the call starts; changes inside are not seen by the caller.output— copied back to the caller when the subroutine returns.inout— copied in at the start and back out at the end.ref— a reference to the caller’s actual variable, so reads and writes happen in place with no copying.
The ref direction matters for two reasons: it lets a subroutine update the caller’s variable the instant it changes (not only at return), and it avoids copying large arrays or queues. Use const ref when you want the no-copy efficiency but must guarantee the data is read-only.
module tb;
// 'ref' passes the actual variable, so updates are visible immediately
task automatic push_all(ref int q[$], input int values[]);
foreach (values[i])
q.push_back(values[i]); // the caller's queue grows in place
endtask
// 'const ref' avoids copying a large array but forbids modifying it
function automatic int sum_all(const ref int data[]);
int total = 0;
foreach (data[i])
total += data[i];
return total;
endfunction
endmoduleNote that ref arguments require an automatic context, and some simulators restrict where they are allowed; copying with input and output is always portable.
Tasks that consume time
The one thing a task can do that a function cannot is advance simulation time. A task body may contain delays, wait for events, or block on a clock edge, which makes tasks the natural home for stimulus generators and bus drivers. The task below waits for a number of cycles, then drives an output flag back to the caller.
module tb;
// A task CAN consume time; a function cannot.
// input args are copied in; the output arg is copied back when the task ends.
task automatic gen_pulse(input int cycles, output bit done);
done = 1'b0;
repeat (cycles) #5; // burn time: 'cycles' steps of 5 time units
done = 1'b1;
$display("[t=%0t] pulse done after %0d cycles", $time, cycles);
endtask
bit finished;
initial begin
finished = 1'b0;
$display("[t=%0t] calling gen_pulse...", $time);
gen_pulse(3, finished); // blocks here until the task returns
$display("[t=%0t] finished = %0b", $time, finished);
$finish;
end
endmodule[t=0] calling gen_pulse... [t=15] pulse done after 3 cycles [t=15] finished = 1 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp8f3idgxb/task_delay.sv:18: $finish called at 15 (1s)
Calling the task blocks the calling process until the task returns — notice the timestamps jump from 0 to 15. If you need the caller to continue immediately, you would launch the task inside a fork ... join_none block. A function could never contain that repeat loop with a delay, because a function must complete in the same time step it started.
Practical guidelines
- Use a function for zero-time computation and a task whenever timing control is involved.
- Declare reusable subroutines
automaticunless you specifically need persistent state. - Prefer an explicit
returnover assigning to the subroutine name — it reads better and exits early cleanly. - Use
voidfunctions for action-only helpers so they cannot accidentally slip time into your datapath. - Pass large arrays and queues by
ref(orconst ref) to avoid silent, costly copies.
Quick check
Why can a function never contain a #10 delay?
A function is defined to execute in zero simulation time so that it can be used inside an expression. Any construct that advances time — #, @ or wait — is therefore illegal in a function and must live in a task instead.
A local variable keeps its value between two calls of the same routine. What lifetime does it have?
Static. A static variable has one shared copy for the whole simulation. To get fresh storage on every call, declare the subroutine (or the variable) automatic.
What is the difference between an output argument and a ref argument?
An output is copied back to the caller only when the subroutine returns, while a ref is a live reference to the caller’s variable, so updates are visible immediately and no copy is made.
When would you declare a function void?
When it performs an action — printing, updating a scoreboard, running a check — but has no value to return. It is then called as a statement, like a task, yet still consumes zero simulation time.
Share your question in comments or talk to our mentor team for batch guidance.
Compile and simulate this in the browser — no install. Edit the code and re-run instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses