SystemVerilog functions and tasks share the same core characteristics as their Verilog counterparts, with a set of enhancements layered on top: ANSI-style declarations, default argument values, named-argument calls, and reference passing all work the same way for both. The one rule that never changes is that a function can't consume simulation time and can't call a task — no @, #, fork...join, or wait statements are allowed inside one — which is exactly the gap a task exists to fill. See Verilog Functions and Verilog Task for a refresher on the basics shared between the two languages.
14 min read | Intermediate Level
What You'll Learn
- The two restrictions every function has to follow: no time controls, and no calling a task
- How ANSI-C style declarations work, and the implicit same-named return variable every function gets
- How old-style
input/outputport declarations interact with that same implicit return variable - How to give an argument a default value, and call with some arguments named instead of positional
- The difference between passing arguments by value and by reference
- Why a subroutine with
staticlifetime can't accept arefargument - How
const refcombines the efficiency of reference passing with a compiler-enforced guarantee against modification - What makes a task different from a function — and why that's the one restriction a function can't get around
ANSI-C Style Declaration
A function can be called and used in two ways: assign its return value to a variable first, or use the returned value directly inside an expression. Every function also gets an implicit variable with the same name as the function itself, which can be assigned directly as an alternative to using return:
module tb;
// There are two ways to call the function:
initial begin
// 1. Call function and assign value to a variable, and then use variable
int s = sum(3, 4);
$display ("sum(3,4) = %0d", s);
// 2. Call function and directly use value returned
$display ("sum(5,9) = %0d", sum(5,9));
$display ("mul(3,1) = %0d", mul(3,1));
end
// This function returns value of type "byte", and accepts two
// arguments "x" and "y". A return variable of the same name as
// function is implicitly declared and hence "sum" can be directly
// assigned without having to declare a separate return variable
function byte sum (int x, int y);
sum = x + y;
endfunction
// Instead of assigning to "mul", the computed value can be returned
// using "return" keyword
function byte mul (int x, y);
return x * y;
endfunction
endmodule
Output
ncsim> run sum(3,4) = 7 sum(5,9) = 14 mul(3,1) = 3 ncsim: *W,RNQUIE: Simulation is complete.
sum assigns its own implicit return variable directly (sum = x + y;), while mul uses return x * y; instead — both are valid, equivalent ways to set what the function returns.
Quick Check
Q: If mul's body were changed from return x * y; to mul = x * y;, would mul(3,1) still return 3?
Show Answer
Yes. mul is the function's own implicit return variable, so assigning to it directly has exactly the same effect as using return — they're two different ways of doing the same thing.
Using Old-Style Declarations and Directions
The older, pre-ANSI-C style of declaring port directions is still valid in SystemVerilog. A function can accept input and output ports, in addition to (or instead of) using its own implicit return variable:
module tb;
initial begin
int res, s;
s = sum(5,9);
$display ("s = %0d", sum(5,9));
$display ("sum(5,9) = %0d", sum(5,9));
$display ("mul(3,1) = %0d", mul(3,1,res));
$display ("res = %0d", res);
end
// Function has an 8-bit return value and accepts two inputs
// and returns the result through its implicit return variable "sum"
function bit [7:0] sum;
input int x, y;
sum = x + y;
endfunction
// Same as above but ports are given inline
function byte mul (input int x, y, output int res);
res = x*y + 1;
return x * y;
endfunction
endmodule
Output
ncsim> run s = 14 sum(5,9) = 14 mul(3,1) = 3 res = 4 ncsim: *W,RNQUIE: Simulation is complete.
mul here is the interesting one: it both returns a value and writes to an explicit output port, and the two don't have to agree. res = x*y + 1 = 4 is written to the caller's res variable through the output port, while return x*y = 3 is a completely separate value handed back through the function's normal return mechanism. The call site's $display("mul(3,1) = %0d", mul(3,1,res)) uses the return value (3), while the later $display("res = %0d", res) reads whatever the output port actually wrote (4) — two independent channels out of the same function call.
sum has only two ports, x and y, and hands its result back through the implicit same-named return variable. It can't also declare an output port called sum: a function isn't allowed to declare another object with its own name, so the result has to come back through that implicit variable.
Quick Check
Q: In the mul example, if the call site only ever displayed res and never called $display("mul(3,1) = %0d", mul(3,1,res)), would res still end up as 4?
Show Answer
Yes. res is populated by the output port assignment (res = x*y + 1;) every time the function is called, completely independently of whether anything ever reads or displays the function's separate return value.
Default Argument Values
An ANSI-style argument can specify a default value with = expression. If the caller omits that argument — or leaves its position empty in a positional call — the compiler supplies the default instead:
module tb;
initial begin
int r1, r2, r3;
r1 = scale(5); // b uses its default of 10
r2 = scale(5, 3); // b explicitly overridden to 3
r3 = scale( , 3); // a uses its default of 1, b overridden to 3
$display("r1 = %0d r2 = %0d r3 = %0d", r1, r2, r3);
end
// "a" defaults to 1, "b" defaults to 10
function int scale(int a = 1, int b = 10);
return a * b;
endfunction
endmodule
Output
r1 = 50 r2 = 15 r3 = 3
Defaults only work with ANSI-style declarations, and a default expression is only evaluated on calls that actually use it — it's re-evaluated fresh each time, not computed once and cached.
Leaving an earlier argument empty, as in scale( , 3), is only legal if that argument has a default of its own. An argument with no default can never be skipped, whether by position or by leaving it blank. Defaults don't have to be trailing either — an argument with no default is allowed to sit between two arguments that do have defaults.
Calling with Named Arguments
Arguments can also be bound by name instead of position, using .argument_name(expression) at the call site. This makes it easy to override just one default out of several, in any order:
module tb;
initial begin
$display("scale(.b(3)) = %0d", scale(.b(3))); // a defaults to 1
$display("scale(.b(3), .a(5)) = %0d", scale(.b(3), .a(5))); // named args, any order
$display("scale(5, .b(3)) = %0d", scale(5, .b(3))); // positional then named
end
function int scale(int a = 1, int b = 10);
return a * b;
endfunction
endmodule
Output
scale(.b(3)) = 3 scale(.b(3), .a(5)) = 15 scale(5, .b(3)) = 15
Named arguments can appear in any order relative to each other, which is what makes scale(.b(3), .a(5)) and a hypothetical scale(.a(5), .b(3)) equivalent.
Positional and named arguments can be mixed in the same call, but every positional argument has to come before any named one. scale(5, .b(3)) is legal; writing the named argument first, as in scale(.b(3), 5), is a compile error.
Passing Arguments by Value
Pass by value is the default way arguments reach a subroutine. Each argument is copied into the subroutine's own local storage, and changes made to that local copy never propagate back to the caller:
module tb;
initial begin
int a, res;
// 1. Lets pick a random value from 1 to 10 and assign to "a"
a = $urandom_range(1, 10);
$display ("Before calling fn: a=%0d res=%0d", a, res);
// Function is called with "pass by value" which is the default mode
res = fn(a);
// Even if value of a is changed inside the function, it is not reflected here
$display ("After calling fn: a=%0d res=%0d", a, res);
end
// This function accepts arguments in "pass by value" mode
// and hence copies whatever arguments it gets into this local
// variable called "a".
function int fn(int a);
// Any change to this local variable is not
// reflected in the main variable declared above within the
// initial block
a = a + 5;
// Return some computed value
return a * 10;
endfunction
endmodule
Output
ncsim> run Before calling fn: a=2 res=0 After calling fn: a=2 res=70 ncsim: *W,RNQUIE: Simulation is complete.
The exact starting value shown here (2) depends on the simulation's random seed, since it comes from $urandom_range(1,10) — a different run could show a different number. What matters, and what will always hold regardless of seed, is that the caller's a stays completely unchanged after the call (still whatever it was before), even though the function's own local a was modified (a + 5) on the way to computing the return value.
Passing Arguments by Reference
A ref argument isn't copied — the subroutine gets a direct reference to the caller's own variable, so changes made inside are visible outside once the call returns. Reusing the exact same calling code as the pass-by-value example above, only the function itself changes:
// Use "ref" to make this function accept arguments by reference
// Also make the function automatic
function automatic int fn(ref int a);
// Any change to this local variable will be
// reflected in the main variable declared within the
// initial block
a = a + 5;
// Return some computed value
return a * 10;
endfunction
fn here must be declared automatic — it's illegal to pass an argument by reference to a subroutine with static lifetime, since a static subroutine's storage is meant to persist and be shared across every call, while a ref argument is tied to one specific caller's variable that only makes sense for that one call.
Output
ncsim> run Before calling fn: a=2 res=0 After calling fn: a=7 res=70 ncsim: *W,RNQUIE: Simulation is complete.
This time, a genuinely changes to 7 after the call — the exact same a = a + 5; line as the pass-by-value version, but now operating directly on the caller's own storage instead of a disposable local copy. res still comes out to 70, since the underlying calculation ((a+5) * 10) is identical either way — the only difference between the two examples is whether the caller's own a persists the change afterward.
Quick Check
Q: If the pass-by-value example's starting a had been 5 instead of 2, what would a and res show after the call, using the reference version of fn?
Show Answer
a = 10, res = 100. With ref, the caller's a is updated to 5 + 5 = 10 by the function, and res receives the return value 10 * 10 = 100.
Where Are Functions Used?
Functions are the right choice for anything that computes a value from its inputs instantly, with no waiting involved — calculating a checksum or CRC over a data packet, converting between data representations, or any small utility calculation a class needs. The moment something needs to wait for a clock edge or an event, it belongs in a task instead, since a function simply isn't allowed to consume time.
const ref: Reference Efficiency Without the Mutability Risk
Passing by reference avoids copying large data — genuinely valuable for a big array or a large struct, where copying on every call would be wasteful. But it also means the function can modify the caller's original data, whether that was actually intended or not. const ref gives you the efficiency of reference passing while removing that risk entirely: the argument still isn't copied, but the compiler refuses to let the function body write to it at all.
function automatic int sum_it (const ref int a []);
int total = 0;
foreach (a[i])
total += a[i];
// a[0] = 99; // Illegal: 'a' is const, this is a compile-time error
return total;
endfunction
module tb;
initial begin
int data[] = '{1, 2, 3, 4, 5};
$display ("sum = %0d", sum_it(data));
end
endmodule
This prints sum = 15. data is never copied into sum_it — it's accessed directly through the reference, exactly as efficient as a plain ref argument would be. But unlike plain ref, attempting a[0] = 99; inside the function body is rejected at compile time, not just discouraged by convention. const ref still requires the function to be automatic, for the same reason plain ref does — it's still fundamentally a reference-passing mode.
Quick Check
Q: If sum_it's argument were changed from const ref int a [] to plain ref int a [], would the function still work correctly?
Show Answer
Yes — sum_it never actually modifies a, so removing const wouldn't break anything at runtime. But it would silently remove the compiler's guarantee that it never will, even after a future edit — const ref documents and enforces that guarantee, plain ref doesn't.
Tasks
A task shares the same declaration syntax as a function — ANSI-style or old-style ports, default argument values, named-argument calls, pass by value, reference, or const ref — all of it works identically. What's different is what a task is allowed to do, and what it hands back to its caller.
Tasks Can Consume Time
Unlike a function, a task can contain @, #, wait, and fork...join statements, and it can call other tasks as well as functions. This is the one capability that decides whether something has to be a task rather than a function: anything that needs to wait belongs in a task.
module tb;
logic clk = 0;
always #5 clk = ~clk; // 10ns period clock
logic [7:0] bus = 8'hA5;
// Waits for "cycles" clock edges, then samples bus through an output port
task automatic wait_cycles_and_read(input int cycles, output logic [7:0] data);
repeat (cycles) @(posedge clk);
data = bus;
endtask
initial begin
logic [7:0] captured;
$display("t=%0t : calling task", $time);
wait_cycles_and_read(3, captured);
$display("t=%0t : captured = 0x%0h", $time, captured);
end
endmodule
Output
t=0 : calling task t=25 : captured = 0xa5
The call to wait_cycles_and_read doesn't return until three clock edges have actually happened in simulation — at t=5, t=15, and t=25 — which a function could never do.
No Return Value, No Implicit Same-Named Variable
A task has no return value at all, so it gets none of the implicit same-named variable a function gets. A bare return; with no expression is still legal inside a task, to exit early, but return some_value; is a compile error.
Since a task can't hand back a result through a return value, output and inout ports are the normal way a task communicates results to its caller — exactly how data works in wait_cycles_and_read above. A task can also declare several output ports at once, something a function's single return value can't do on its own.
Forking additional processes inside a task with fork...join_none or join_any can let the task return to its caller while those forked processes keep running in the background. See SystemVerilog fork join for how each join variant behaves.
Unlike a plain (non-void) function, a task's call can drop the parentheses entirely when no arguments need to be supplied — either because the task takes none, or because every argument has a default and the call wants to use them all. A plain function always needs at least an empty (), defaults or not.
Quick Check
Q: If wait_cycles_and_read were rewritten as a function instead of a task, keeping the exact same body, would it still compile?
Show Answer
No. The repeat (cycles) @(posedge clk); line consumes simulation time, which is illegal inside a function. Anything that needs to wait for an event has to be a task.
Common Beginner Mistakes
Mistake #1: Trying to consume time inside a function
Any @, #, wait, or fork...join statement inside a function is a compile error, and a function can't call a task either. Why? A function is meant to compute and return a value immediately, within the same simulation time step it was called — if what you need genuinely requires waiting, that's exactly what a task is for.
Mistake #2: Assuming a function's return value and an output port can't disagree
As the mul example shows, a function's own return value and an explicit output port argument are two independent channels — there's no rule that they have to compute the same result. Why? It's easy to assume they're automatically linked just because they come from the same function call, but each is set by its own separate assignment inside the function body.
Mistake #3: Passing a ref argument to a static subroutine
This is explicitly illegal. Why? A static subroutine's storage is shared across every call to it, while a ref argument is inherently tied to one specific caller's variable for one specific call — the two lifetimes are fundamentally incompatible, so the subroutine accepting a ref argument has to be automatic.
Mistake #4: Using plain ref when the function never needs to modify the argument
As the sum_it example shows, a large array only needs to be read, not written — yet plain ref still leaves the door open for a future edit to accidentally modify the caller's data. Why? const ref costs nothing extra in performance and makes the "read-only" intent a compiler-enforced guarantee instead of just a convention someone has to remember to follow.
Mistake #5: Putting a named argument before a positional one
scale(.b(3), 5) is a compile error — if both positional and named arguments appear in the same call, every positional argument has to come first.
Reorder the call so any named arguments come last, or just use named arguments for every argument in that call.
Mistake #6: Trying to return a value from a task
return some_value; inside a task is a compile error, since a task has no return value and no implicit same-named variable the way a function does.
Use a bare return; to exit a task early, and an output or inout port to hand back an actual result.
Practice Exercise
Challenge: Predict Two Independent Results
Given function byte combine (input int x, y, output int total); total = x + y; return x - y; endfunction, called as combine(10, 3, total), what does the call itself return, and what does total hold afterward?
Hint
Work out the output port's assignment and the return statement completely separately — they use different expressions and don't have to match.
Solution
The call returns 7 (return x - y = 10 - 3), and total ends up as 13 (total = x + y = 10 + 3) — two different numbers from the exact same call, exactly like mul's 3 and 4 earlier in this article.
Challenge: A Task With a Default Argument
Write a task pulse that drives a ref argument sig high, waits for a default of 2 clock edges (an input int cycles = 2 argument), then drives sig back low. Call it once using the default, and once overriding cycles to 5 with a named argument.
Hint
Combine what you already know about default argument values with repeat (cycles) @(posedge clk);, exactly like wait_cycles_and_read earlier in this article.
Solution
module tb;
logic clk = 0;
always #5 clk = ~clk;
logic sig;
task automatic pulse(ref logic sig, input int cycles = 2);
sig = 1;
repeat (cycles) @(posedge clk);
sig = 0;
endtask
initial begin
pulse(sig); // uses the default: 2 cycles
pulse(sig, .cycles(5)); // named argument overrides the default
end
endmodule
Because sig is passed by ref, the caller sees each change immediately (an output argument would only be copied back when the task returns). The first call holds sig high for 2 clock edges using the default, and the second holds it high for 5 using a named override — the same default-argument mechanics as scale earlier in this article, just applied to a task instead of a function.
Summary
In this tutorial, you learned:
- A function can't consume simulation time and can't call a task — it computes and returns a value immediately
- Every function has an implicit return variable sharing its own name, which can be assigned directly as an alternative to
return - Old-style
input/outputport declarations are still valid, and a function's return value and anyoutputport are independent, separately-assigned channels - An ANSI-style argument can specify a default value with
= expression, and a call can skip it by position (leaving that slot empty) or by name - Arguments can be bound by name with
.argument(expression)at the call site, in any order relative to each other — but positional arguments must come before named ones in the same call - Pass by value (the default) copies an argument in; changes inside the subroutine never affect the caller's original variable
- Pass by reference (
ref) shares the caller's actual variable directly, so changes inside are visible outside — but this requires the subroutine to beautomatic, neverstatic const refcombines reference-passing efficiency (no copy) with a compile-time guarantee that the argument can never be modified — the right default whenever a large argument only needs to be read- A task can consume simulation time and call other tasks or functions, unlike a function — but it has no return value and no implicit same-named variable, so results go through
output/inoutports instead
Continuing the Advanced Features Chapter
Continue with SystemVerilog Package.