SystemVerilog Series · SV-10

SystemVerilog Series — SV-10: Tasks, Functions & Argument Passing — VLSI Trainers
SystemVerilog Series · SV-10

Tasks, Functions & Argument Passing

All SV additions to tasks and functions: new port directions including ref and const ref, void functions, the return statement, default argument values, named argument passing, optional parentheses, and the DPI import/export mechanism for calling C from SV and vice versa.

💡 What SV Adds to Tasks & Functions

Verilog-2001 tasks and functions were functional but had significant limitations — no pass-by-reference, no default arguments, no void functions, and no early exit with return. SystemVerilog brings all of these, plus named argument passing and the DPI bridge to C.

Verilog-2001 — what was missing

  • Only pass-by-value — large arrays copied every call
  • No ref arguments — no shared write-back
  • Functions always had to return a value
  • No return statement — only exit was endfunction
  • No default argument values
  • Arguments always positional
  • Functions could not have output or inout ports
  • No C-language calling bridge

SystemVerilog additions (§10)

  • ref — pass by reference (zero copy, shared write-back)
  • const ref — read-only pass by reference
  • void functions — no return value
  • return [expr] — early exit with optional value
  • Default argument values (arg = default_expr)
  • Named argument passing (.name(value))
  • Functions can now have output and inout ports
  • DPI import/export for C interoperability

Tasks — New Capabilities

The core task model is unchanged: tasks can consume simulation time (timing controls are legal) and can have any number of input, output, inout, and now ref arguments. SystemVerilog adds three improvements.

1 — Default direction and type rules

// Verilog-2001 style (still works)
task mytask1(output int x, input logic y);
  // ...
endtask

// SV rule 1: default direction is INPUT if none given
// SV rule 2: once a direction is given, subsequent args inherit it
task mytask3(a, b, output logic [15:0] u, v);
// a: input logic (default direction, default type)
// b: input logic (inherits both from a)
// u: output logic [15:0] (explicit)
// v: output logic [15:0] (inherits direction + type from u)
endtask

// SV rule 3: default type is 'logic' for task arguments
task mytask_default(a, b);  // a: input logic, b: input logic
endtask

// Arrays as formal arguments
task mytask4(input [3:0][7:0] a, b[3:0], output [3:0][7:0] y[1:0]);
// a: input [3:0][7:0] a        (scalar of that packed type)
// b: input [3:0][7:0] b[3:0]  (4-element unpacked array of that type)
// y: output [3:0][7:0] y[1:0] (2-element unpacked array)
endtask

2 — begin…end is optional

// Verilog-2001: required begin...end for multiple statements
task old_style;
  begin
    $display("a");
    $display("b");
  end
endtask

// SV: begin...end not required — still sequential without it
task sv_style;
  $display("a");
  $display("b");
endtask

// Also legal: task with no statements at all
task empty_task;
endtask

3 — return exits early

task automatic send_pkt(input int payload[]);
  if (payload.size() == 0) return;  // exit immediately — no value
  transmit(payload);
endtask

4 — Per-argument lifetime override

// A static task can have individual automatic arguments or local vars
task static mixed_lifetime(input int shared, automatic int private_copy);
  // shared: static — same storage across all active calls
  // private_copy: automatic — each call gets its own stack slot
endtask

// An automatic task can have individual static arguments or local vars
task automatic with_counter(input int x);
  static int call_count = 0;  // shared counter — survives across calls
  call_count++;
  $display("Call #%0d: x=%0d", call_count, x);
endtask

Functions — New Capabilities

Functions in Verilog-2001 could not have timing controls and always had to return a value by assigning to the function name. SystemVerilog extends functions in several important ways.

Functions now have output and inout ports

// SV: functions can have output and inout arguments
// (illegal in Verilog-2001)
function logic [15:0] myfunc3(int a, b, output logic [15:0] u, v);
// a, b: input int (default direction + type)
// u, v: output logic [15:0]
// function returns logic [15:0]
endfunction

// Both ANSI and non-ANSI styles are legal
function logic [15:0] myfunc1(int x, int y); // ANSI style
endfunction

function logic [15:0] myfunc2;  // non-ANSI style
  input int x;
  input int y;
endfunction
Functions with output/inout/ref cannot be used everywhere. Calling a function that has output, inout, or ref arguments is illegal in: an event expression (@), a procedural continuous assignment (assign x = f(y)), or any expression that is not inside a procedural statement. However, a const ref argument is legal in all those contexts.

Arrays as function arguments

function [3:0][7:0] myfunc4(input [3:0][7:0] a, b[3:0]);
// a: scalar of packed [3:0][7:0]
// b: 4-element unpacked array of [3:0][7:0]
// return: packed [3:0][7:0]
endfunction

void Functions & the return Statement

Returning values — three legal patterns

// Pattern 1 (Verilog-2001 style): assign to function name
function [15:0] myfunc_v(input [7:0] x, y);
  myfunc_v = x * y - 1;  // return value is the function name
endfunction

// Pattern 2 (SV new): return statement
function [15:0] myfunc_sv(input [7:0] x, y);
  return x * y - 1;       // cleaner — same result
endfunction

// Pattern 3: early return with guard clause
function automatic int find_first(int arr[], int target);
  foreach(arr[i])
    if(arr[i] == target) return i;  // early return on first match
  return -1;                          // not found
endfunction
return overrides function-name assignment. If the same function body both assigns to the function name and uses return, the return value takes precedence. You cannot mix them to produce different values for different paths — use either style consistently. For struct/union return types, hierarchical names starting with the function name inside the function refer to members of the return value.

void functions — no return value

// void function: declared as type void, no return value
function void myprint(int a);
  $display("value = %0d", a);
  // no return statement needed
  // return; is legal but return expr; is illegal
endfunction

// Non-void function called as an expression
result = myfunc1(c, d);          // expression context — OK
x = b + myfunc1(c, d);           // inside expression — OK

// void function called as a statement
myprint(a);                        // statement — OK (type void)
// myfunc1(c,d);                   // WARNING: result discarded — use void' cast

🗑 Discarding Function Return Values

In Verilog-2001, calling a non-void function as a statement (discarding the return value) generated a warning. SystemVerilog provides an explicit, clean way to discard a return value using a cast to void.

// Discard the return value explicitly — no warning
void'(some_function());

// Common example: call randomize() but don't check the return (1=success)
void'(pkt.randomize());

// Another example: $cast returns 1 on success, 0 on failure
// If you don't care about the return, cast away
void'($cast(derived_ptr, base_ptr));
Prefer checking return values. void'(pkt.randomize()) is legal but risky — if randomisation fails (constraints are unsatisfiable), the return value is 0 and you never know. In testbench code, use assert(pkt.randomize()) so the simulation errors immediately on failure instead of continuing with un-randomised data.

Argument Directions

Tasks and functions in SV share the same four argument directions. Functions gain output, inout, and ref as new capabilities compared to Verilog-2001.

input
Copied in at call time. Changes inside the subroutine do not affect the caller. Default direction.
output
Copied out at return time. Initialised to the type’s default inside the subroutine.
inout
Copied in at call, copied out at return. Changes are visible to the caller only when the subroutine returns.
ref
No copy. Caller and subroutine share the same storage. Changes visible immediately. New in SV.
DirectionCopy at call?Copy at return?Changes visible when?Tasks?Functions?
inputYesNoNever visible to caller
outputNo (default init)YesAt return✓ (SV new)
inoutYesYesAt return✓ (SV new)
refNo copyNo copyImmediately✓ (SV new)

📋 Pass by Value

Pass by value (the Verilog-2001 default) copies every argument into the subroutine’s local storage area. For small data this is fine. For large arrays it copies megabytes of data on every call.

// 1000 bytes copied every call — expensive
function int crc_slow(byte packet[1000:1]);
  for(int j=1; j<=1000; j++)
    crc_slow ^= packet[j];
endfunction

// Changes to packet inside the function do NOT affect the caller's array
byte my_pkt[1000:1];
int  k = crc_slow(my_pkt);  // my_pkt is unchanged

🔗 Pass by Reference (ref)

A ref argument passes a reference to the caller’s variable — no copy is made. Both the caller and the subroutine operate directly on the same memory location. Changes inside the subroutine are immediately visible to the caller (not just at return time).

// Same CRC — now with ref: zero-copy, and changes visible immediately
function int crc_fast(ref byte packet[1000:1]);
  for(int j=1; j<=1000; j++)
    crc_fast ^= packet[j];
endfunction

// The CALL SITE looks identical — ref is transparent to the caller
byte packet1[1000:1];
int  k = crc_fast(packet1); // same syntax as pass-by-value call

// Immediate visibility example
task automatic increment(ref int val);
  val++;              // write-back visible to caller IMMEDIATELY (not at return)
endtask

int x = 5;
increment(x);       // x is now 6 — change happened immediately inside the task

// ref vs inout — the timing difference matters for tasks with delays
task automatic delayed_update(ref int r, inout int io);
  r++;   // IMMEDIATELY visible outside
  #10;
  io++;  // will be copied out when task returns — NOT immediately
endtask

ref argument restrictions

  • Only variables can be passed by ref — nets (wire, tri) cannot.
  • Types must match exactly — no implicit casting or promotion for ref arguments. Fixed arrays cannot be mixed with dynamic arrays.
  • ref cannot be combined with any other direction qualifier — ref input int a is a compile error.
  • For class handles passed as ref: changes to the handle itself (pointing it to a new object) are visible to the caller, in addition to changes to the object’s contents.
  • Only usable in tasks and functions declared as automatic (or inside automatic programs/classes).
ref cannot be used in static tasks. A static task has a single shared storage area. Passing by reference in a static task would mean multiple concurrent callers would all share the same reference slot — undefined behaviour. Always use automatic tasks/functions when using ref arguments.

🔒 const ref

const ref gives the zero-copy performance of ref while preventing the subroutine from modifying the argument. It is ideal for large read-only inputs like packet arrays passed to display or check functions.

// const ref: zero-copy, read-only inside the function
task show(const ref byte [] data);
  for(int j=0; j < data.size; j++)
    $display(data[j]);           // read — OK
  // data[0] = 0;               // COMPILE ERROR — const ref cannot be written
endtask

// Advantage over const ref over plain input for large arrays:
// input: copies all 1000 bytes → expensive
// const ref: no copy, enforces read-only → efficient and safe
function int hash(const ref byte pkt[1000:1]);
  int h = 0;
  foreach(pkt[i]) h ^= pkt[i];
  return h;
endfunction
const ref is the only ref form legal in event expressions. Because it cannot modify the variable, a function with only const ref arguments (and no output/inout) can be called inside @(f(x)) event expressions. This enables efficient per-element method calls in always_comb sensitivity lists.

📌 Default Argument Values

Arguments can declare a default value. When the call omits that argument (or passes an empty placeholder ,), the default expression is used. Default values are evaluated in the caller’s scope each time the subroutine is called.

task read(int j = 0, int k, int data = 1);
endtask
// j: default=0, k: no default (must be given), data: default=1

// All legal call forms:
read(  , 5   );  // j=0,  k=5, data=1   (j and data use defaults)
read(2, 5   );  // j=2,  k=5, data=1   (data uses default)
read(  , 5, );  // j=0,  k=5, data=1   (empty = default for j and data)
read(  , 5, 7);  // j=0,  k=5, data=7
read(1, 5, 2);  // j=1,  k=5, data=2   (all explicit)
// read( , );     // ERROR: k has no default value

// Default evaluated at call time in CALLER's scope
int global_base = 100;
function int adjust(int x, int base = global_base);
  return x + base;
endfunction

global_base = 200;
adjust(5);     // base = 200 (current value of global_base at this call)
Default values are only available with ANSI-style declarations. Non-ANSI style (separate direction declarations inside the subroutine body) does not support default values. Also, output ports cannot have default values — only input, inout, and ref arguments can.

📌 Named Argument Passing

Arguments can be passed by name using the .name(value) syntax, mirroring named port connections on module instances. This makes it easy to pass non-consecutive defaults and to self-document which argument is which.

function int fun(int j = 1, string s = "no");
endfunction

// All legal calling forms — named, positional, mixed
fun(.j(2), .s("yes"));  // fun(2, "yes")   — fully named
fun(.s("yes")       );  // fun(1, "yes")   — j uses default
fun(       , "yes" );  // fun(1, "yes")   — j placeholder, s positional
fun(.j(2)            );  // fun(2, "no")    — s uses default
fun(.s("yes"), .j(2));  // fun(2, "yes")   — named in any order
fun(.s(       ), .j());  // fun(1, "no")    — empty = default for both
fun(2             );  // fun(2, "no")    — positional
fun(              );  // fun(1, "no")    — all defaults

// Mixed positional + named: positional must come FIRST
fun(2, .s("yes"));   // OK — 2 is positional (j), then .s is named
// fun(.s("yes"), 2); // ILLEGAL — named before positional
Named arguments behave like module parameters. If an argument has a default and you pass it by name, you can omit it entirely in the call. If it has no default, omitting it or passing it empty (even by name) is a compile error. Named argument order in the call does not have to match the declaration order — only positional arguments must match.

() Optional Parentheses

When a task or function declares no arguments, or when all its arguments have default values, the trailing parentheses () in the call are optional.

task no_args;       // no arguments
endtask

task all_defaults(int x=0, int y=0);
endtask

// All of these are legal:
no_args;           // no parentheses
no_args();         // parentheses — also fine
all_defaults;      // all defaults — parentheses optional
all_defaults();    // parentheses — also fine

Import and Export Functions (DPI)

The Direct Programming Interface (DPI) provides a bridge between SystemVerilog and C/C++. import brings a C function into SV scope; export makes an SV function callable from C.

DPI syntax
// Import: call C from SV
import "DPI" function int  c_add(int a, int b);
import "DPI" task         c_delay(int ns);
import "DPI" my_c_func = function int sv_name(int x);
// last form: C function name is "my_c_func", SV sees it as "sv_name"

// Export: call SV from C
export "DPI" function sv_checker;
export "DPI" c_name = function sv_func;
// last form: C calls "c_name", which maps to SV function "sv_func"

Three import qualifiers

import “DPI”
Unqualified. May have side effects but cannot read/write SV signals except through arguments. Cannot invoke exported SV functions.
pure
No side effects. Result depends only on inputs. Cannot read/write global or static data. Compiler may cache or eliminate calls.
context
Full access: can invoke exported SV functions, access SV signals via VPI/other interfaces, read/write any data. Runs in the scope of the import declaration.
// Pure function: compiler may optimise duplicate calls
import "DPI" pure function int c_sqrt(int x);

// Context function: can access SV signals from C
import "DPI" context function void c_dump_state();

// Call imported functions just like any SV function
int root = c_sqrt(64);  // calls C sqrt(64) = 8
c_dump_state();         // calls C function that accesses SV state

// Export example
function automatic int sv_check(int x);
  return x > 0;
endfunction
export "DPI" function sv_check;  // C can now call sv_check()

DPI signature rules

  • For any given C function name, all import/export declarations anywhere in the design must have identical signatures (return type, argument count, argument types, directions).
  • Only one import or export of a given SV function name per scope.
  • Only SV functions (not class methods, not tasks) can be exported.
  • pure requires a non-void function with no output or inout arguments.
  • Context imports have a built-in scope: they always run in the SV scope of the module/interface where the import declaration appears, not where the call is made.

📋 Quick Reference

SV additions to tasks and functions at a glance

FeatureTasksFunctionsNotes
Default directioninputinputSubsequent args inherit direction from previous
Default typelogiclogic
output portsAlwaysNew in SVCannot be used in event expressions or continuous assign
inout portsAlwaysNew in SVSame restriction as output
ref portsNew in SVNew in SVAutomatic only; no implicit cast; no other qualifier combo
const ref portsNew in SVNew in SVLegal everywhere including event expressions
void returnN/ANew in SVCalled as a statement
return statementNew in SVNew in SVtask: no expr; non-void function: expr required
Default arg valuesNew in SVNew in SVANSI style only; output cannot have defaults
Named arg passingNew in SVNew in SVNamed must come after positional in a mixed call
Optional ()New in SVNew in SVWhen no args or all have defaults
begin…end optionalNew in SVNew in SVStatements still execute sequentially
DPI import/exportimport onlyBothOnly functions can be exported

Argument direction quick guide

  • Read-only input, small: input (copy, cheap for scalars)
  • Read-only input, large array: const ref (zero copy, read-only, legal everywhere)
  • Read-write sharing, immediate visibility: ref (requires automatic subroutine)
  • Write-back at return, not immediate: inout
  • Write-only output at return: output
Coming next: SV-11 covers Section 11 — Classes: the full object-oriented model — class declarations, object handles, constructors, static properties and methods, this, inheritance with extends, super, virtual methods, polymorphism, abstract classes, parameterised classes, and scope resolution.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top