Skip to main content

Functions and Tasks

Functions and tasks package a piece of procedural code under a name, so it can be called from multiple places instead of copy-pasted. They look similar and are frequently confused, but a small set of rules separates what each one is allowed to do — rules that exist because of what each one is meant to model in hardware.

Functions​

A function computes and returns a single value from its inputs, executes in zero simulation time, and cannot contain any timing control:

function automatic parity_calc;
input [7:0] data;
integer i;
begin
parity_calc = 0;
for (i = 0; i < 8; i = i + 1)
parity_calc = parity_calc ^ data[i];
end
endfunction

Called just like a built-in operator, wherever an expression is expected:

assign p = parity_calc(byte_in);

parity_calc is exactly the loop-based parity computation from Loops, now packaged so it can be called by name from anywhere in the module instead of re-writing the loop each time. A function synthesizes to the same unrolled combinational hardware its body would produce inline — calling it in three places produces three separate copies of that XOR chain, not one shared piece of hardware with three connections into it, the same way for-loop unrolling from the previous page works.

Function rules:

  • Must have at least one input, and returns exactly one value (implicitly named after the function itself, as parity_calc is assigned above).
  • Cannot contain #delay, @(event), or wait — a function must complete in zero simulation time, which is also exactly why it can only ever describe combinational logic, never anything clocked.
  • Can call other functions, but cannot call a task (since a task might consume time, which would break a function's zero-time guarantee).

Tasks​

A task can have any number of inputs, outputs, and inouts, can contain timing control, and is called as its own statement rather than used inside an expression:

task automatic drive_and_check;
input [7:0] expected;
output pass;
begin
@(posedge clk); // timing control — not allowed in a function
pass = (dut_out == expected);
end
endtask
initial begin
drive_and_check(8'hA5, result);
end

Because a task can contain @(posedge clk), #delay, and multiple outputs, it's the natural fit for testbench code — driving a sequence of stimulus and checking a response over multiple clock cycles is exactly the kind of multi-output, time-consuming operation a function structurally can't express. Tasks are correspondingly rare in synthesizable RTL for that same reason: real hardware described as "wait some cycles, then do this" needs an actual clocked always block or state machine behind it — a task itself doesn't synthesize into a piece of physical timing control, it's a way of organizing procedural code, and any task called from within synthesizable RTL still has to reduce to ordinary synthesizable statements once inlined.

automatic vs. static (the default)​

By default, a Verilog function or task's local variables are static — there is exactly one copy of them, shared across every call, even multiple simultaneous calls (which matters most for recursion, and for tasks called from more than one concurrent process). Declaring automatic (as both examples above do) gives each call its own private copy of local variables instead, exactly like a normal function call in a software language:

function automatic parity_calc; // safe to call recursively or concurrently
Default to automatic

Static-by-default is a historical Verilog quirk (SystemVerilog, by contrast, defaults functions/tasks to automatic) that causes real bugs the moment a function or task is called from more than one place concurrently — one call's in-progress local variables can be silently overwritten by another call's. Writing automatic explicitly on every function and task removes that entire failure mode, at the cost of a bit of recursion/re-entrancy support most RTL doesn't need. There's essentially never a good reason to rely on Verilog's static default deliberately.

What's next​

Functions and tasks are pure procedural code — no timing behavior beyond what a task explicitly waits for. The next page covers timing controls — #delay, @(event), wait — directly: what each one means, and why every one of them is a simulation concept with no equivalent in real, synthesized silicon.