Tasks, functions and arguments
Choose between a task and a function, and pick the argument direction that lets a routine see the caller's variables at the moment it needs them.
- Lesson
- 3.1
- Time
- 20 min
- Runs on
- Verilator
- Check
- 5 questions
A driver task waits for the DUT's ready before it sends a write. The waveform shows ready rising in cycle 3, yet the task keeps waiting until the test's 8-cycle limit runs out and reports a hang. The task was declared wait_ready(input bit rdy), so it received a copy of ready taken when the call started, and that copy stays 0 forever.
The DUT raises ready in cycle 3. The task only ever looks at rdy, the value copied in at the call.
Functions compute, tasks take time
SystemVerilog gives you two kinds of routine. A function runs in zero simulation time and usually returns a value, so you can call it inside an expression such as if (parity(d) != p). A task is a statement that may wait, for a clock edge with @, a delay with # or a condition with wait.
The compiler enforces the difference. A function body may hold no delay, event control or wait, so it always finishes in the same time step it started. A function declared void returns nothing and is called as a statement.
| Function | Task | |
|---|---|---|
Can wait (#, @, wait) | No | Yes |
| Gives back a result | A return value, or none if void | Only through output, inout or ref arguments |
| Called as | An expression, or a statement if void | A statement |
| Typical testbench use | Predict an expected value, decode a field, compare two items | Drive a bus handshake, wait for a response |
Pick by asking one question: does the routine need simulation time to pass?
Choose by what the routine models. Anything that spans clock cycles is a task, and anything that computes an answer from its inputs is a function. Reference models and checkers make good functions, because you can then call them from anywhere, including from inside another function.
A task can call functions and other tasks, and it can leave early with a bare return;. To call a non-void function only for its side effects, cast the call to void, as in void'(f(x)). The cast tells the reader that the result is dropped on purpose.
When arguments are copied
Every argument has a direction, and the direction decides when values move between the caller and the routine. An input is copied in when the call starts. An output is copied back to the caller when the routine returns, and an inout is copied both ways.
Nothing moves in between. While a task waits, an input keeps the value it had at the call, whatever happens to the caller's variable afterwards. Likewise, the caller sees nothing written to an output until the task returns.
| Direction | Copied in | Copied out | Sees later changes by the caller |
|---|---|---|---|
input | at the call | never | no |
output | never | at return | no |
inout | at the call | at return | no |
ref | no copy: the routine uses the caller's variable | no copy: writes land at once | yes |
Argument directions and when their values move.
Copy-back at return is often exactly what you want. A monitor task that assembles a transaction over several cycles hands it back through an output, so the caller never sees a half-built transaction. The trouble starts only when a routine must react to values that change while it waits.
The default direction is input. An argument written without a direction takes the direction of the argument before it, so in task get_pair(output int a, int b) both a and b are outputs. Write every direction out and you never have to remember this rule.
Seeing the caller's variable with ref
A ref argument passes the caller's variable itself instead of a copy. Reads inside the routine see its current value, and writes reach the caller at once, even while the task is still waiting.
That is the fix for the opening story. Declared with ref bit rdy, the task reads the live ready on every pass through its loop and sees it rise.
Input argument
task automatic wait_ready(input bit rdy);
while (!rdy)
@(posedge clk); // rdy never changes
endtask
Waits forever once it starts with rdy at 0, because the loop tests the copy taken at the call.
Reference argument
task automatic wait_ready(ref bit rdy);
while (!rdy)
@(posedge clk); // reads the caller's ready
endtask
Returns on the first rising edge where the caller's ready is 1.
Two rules come with ref. The routine must be automatic, a lifetime that lesson 3.2 explains, and the caller must pass a variable, so a net such as a wire cannot be passed by reference.
Use const ref when a routine reads a large array but must never change it. The array is not copied at the call, and the compiler rejects any write to it inside the routine.
In a full testbench, a driver usually reaches DUT pins through a virtual interface, which Module 6 covers. It reads the live signal for the same reason a ref argument does.
Defaults and names in a call
An argument can have a default value in its declaration, as in task automatic send(int addr, int len = 1). A call may leave that argument out, and the routine then receives 1.
You can also bind arguments by name, as in send(.addr(32'h40), .len(4)). Named binding keeps long argument lists readable. The call also stays correct when someone later inserts a new argument in the middle of the list.
Defaults let a routine grow without breaking old calls. Suppose a driver gains a new argument, bit check_resp = 1. Every existing call keeps working and gets the old behaviour, and only the tests that need the new option pass it by name.
Try it
Both tasks start at time 0, when ready is 0. The ref version reads the caller's ready on every edge and returns on edge 4. The input version keeps testing its copy, so it runs out of edges at t=75.
Now change ref to const ref in wait_ref and run it again: it still works, because the task only reads rdy. Then add rdy = 0; inside it and read the compiler error.
t=35 ref argument: ready seen on edge 4 t=75 input argument: edges=-1, gave upCheck yourself
Remember
- A function finishes in zero time and can return a value; a task is a statement that may wait.
- An
inputis copied in at the call and anoutputis copied back at return; nothing moves while the routine runs. - A
refargument is the caller's variable, so the routine sees its changes and its writes land at once. refneeds anautomaticroutine and a variable as the actual argument; useconst reffor large read-only arrays.- An argument with no direction inherits the one before it, so write every direction out.
Practice
- Popcount function Implement · Easy