SystemVerilog · Module 6
Tasks vs. Functions — When to Use Which
The core distinction — timing controls, return values, calling syntax, and exactly when to pick each one.
Module 6 · Page 6.1
Both tasks and functions package reusable logic — but they operate on fundamentally different execution models. Functions are zero-time computations that synthesize to combinational hardware. Tasks are simulation processes that advance time. Get the choice wrong and you'll hit compile errors, non-deterministic race conditions, or static-variable corruption in concurrent testbenches. This page gives you the permanent mental model, debugging labs, and nine interview-ready Q&As.
The One Rule That Separates Them
Every task/function decision comes down to a single question: Does the code need to consume simulation time?
| Aspect | function — Zero Simulation Time | task — Can Consume Time |
|---|---|---|
| Execution time | Executes in zero simulation time | Can consume simulation time |
| Timing controls | Cannot contain #delay, @event, or wait | Can contain #delay, @event, wait |
| Can call a task? | No | Yes — tasks can call functions and other tasks |
| Return value | Returns exactly one value | Returns nothing (uses output/ref ports instead) |
| Call style | Called as part of an expression | Called as a standalone statement on its own line |
| RTL safety | Safe inside always_comb, assign, and RTL | Used in testbenches, stimulus, and protocol drivers |
function — Zero Time, One Return Value
A function is the right tool when you need to compute a value and hand it back to the caller — without any waiting. Think of it as a mathematical formula packaged with a name: you give it inputs, it gives you an output, instantly.
// ── Function: compute even parity of an 8-bit byte ──────────────
function automatic logic calc_parity (input logic [7:0] data);
return ^data; // XOR-reduce: 1 if odd number of 1s
endfunction
// ── How to CALL a function — used as part of an expression ───────
logic parity_bit;
parity_bit = calc_parity(tx_byte); // RHS of assignment
if (calc_parity(rx_byte) != rx_parity_in) // inside a condition
$error("Parity error!");
assign parity_out = calc_parity(data_bus); // continuous assignment — legal!
// ── Function: priority encoder ───────────────────────────────────
function automatic logic [2:0] priority_enc (input logic [7:0] req);
priority_enc = 3'b000; // default return value
for (int i = 7; i >= 0; i--)
if (req[i]) priority_enc = i[2:0];
endfunction
// ── Function inside always_comb — completely safe ────────────────
always_comb begin
encoded_out = priority_enc(request_bus); // zero-time call inside RTL
endtask — Can Wait, Can Return Multiple Values
A task is the right tool when your code needs to wait for something — a clock edge, a delay, an event, or a handshake. Tasks are the backbone of testbenches: every bus driver, every protocol transaction, every stimulus sequence is a task.
// ── Task: drive an APB write transaction ──────────────────────────
task automatic apb_write (
input logic [31:0] addr,
input logic [31:0] data,
output logic err // multiple return values via output ports
);
// Tasks CAN contain timing controls
@(posedge clk); // wait for clock edge ← legal in task
psel = 1; paddr = addr; pwrite = 1;
@(posedge clk); // setup phase
penable = 1; pwdata = data;
@(posedge clk); // access phase
err = pslverr;
psel = 0; penable = 0;
endtask
// ── How to CALL a task — always a standalone statement ───────────
logic write_err;
apb_write(32'h4000_0000, 32'hABCD_1234, write_err);
// ← NOT inside an expression. A task call stands alone on its own line.
// ── Task: wait for N clock cycles ────────────────────────────────
task automatic wait_cycles (input int n);
repeat(n) @(posedge clk);
endtask
// ── Task: stimulus sequence ───────────────────────────────────────
initial begin
wait_cycles(5); // wait 5 clocks
apb_write(32'h0, 32'hFF, write_err); // drive transaction
wait_cycles(10); // wait more
endThe Complete Ruleset — Side by Side
| Rule | function | task |
|---|---|---|
| Consumes simulation time? | Never — zero time always | Optional — can wait or not |
#delay inside? | Illegal — compiler error | Legal |
@event inside? | Illegal — compiler error | Legal |
wait() inside? | Illegal — compiler error | Legal |
| Can call a task? | No — illegal (tasks can consume time) | Yes — tasks can call other tasks |
| Can call a function? | Yes — functions call functions freely | Yes — tasks can always call functions |
| Return value? | Exactly one value via return | None — uses output/inout/ref ports |
| How is it called? | Inside an expression: x = fn(a) | As a statement: task(a, b); |
Inside always_comb / assign? | Yes — safe and common in RTL | No — timing controls break combinational semantics |
Inside initial / always? | Yes — as part of expression | Yes — as a statement |
| Fork-join spawning? | No — zero time, nothing to parallelise | Yes — fork task1(); task2(); join |
| Synthesisable? | Yes — if logic is combinational | No — timing controls are not synthesisable |
Real-World Use Cases — What Belongs Where
Always a function
Parity / CRC Computation
Pure calculation — input bytes in, checksum out. No waiting needed. function logic [7:0] crc8(input logic [63:0] data);
Type Conversion
Real to integer, signed to unsigned, grey code to binary. Zero-time transforms. Called in continuous assigns.
Combinational Lookup
Opcode decoder, priority encoder, address mapping. Returns a value. Works inside always_comb.
Assertion Helpers
Check if a value is in range, is one-hot, is power-of-two. Used in assert property conditions.
Always a task
Bus Transaction Driver
APB write, AXI read, I2C byte. Clocked operations that must wait for @(posedge clk) multiple times.
Delay / Reset Sequence
wait_cycles(n), apply reset for N clocks, then release. Pure timing — no computation.
Handshake Monitor
Wait for valid signal, capture data, wait for ready. Uses wait() or @(posedge valid).
Stimulus Generator
Send packet, wait for ack, check response, increment counter. A sequence of timed operations.
Calling Syntax — The Practical Difference
The most visible day-to-day difference between functions and tasks is how you call them. Functions are used inside expressions; tasks stand alone as complete statements. Mixing this up is the most common beginner error.
// ════ FUNCTION CALLS — used inside expressions ════════════════════
// ① Assignment RHS
parity = calc_parity(data);
// ② Condition in if/while
if (is_one_hot(req_bus)) $display("valid");
// ③ Continuous assign
assign encoded = priority_enc(request);
// ④ Inside $display / $error format string args
$display("CRC = %h", compute_crc(payload));
// ⑤ Port connection
adder u_add (.a(mask_bits(raw_a)), .b(b), .sum(sum));
// ⑥ Array index
lut_out = lut[hash_fn(key)];
// ════ TASK CALLS — always a complete statement ═════════════════════
// ① Standalone statement in initial/always
apb_write(32'h0, 32'hFF, err_out); // ← semicolon ends the statement
// ② Chained in a sequence
reset_dut();
wait_cycles(10);
apb_write(32'h1000, 32'hA, err_out);
// ③ Inside fork-join for parallel execution
fork
drive_master(); // runs concurrently
monitor_slave(); // runs concurrently
join
// ❌ WRONG: calling a task inside an expression
// parity = apb_write(addr, data, err); ← task, not function — compiler error
// ❌ WRONG: calling a function as a statement expecting side-effects-only
// calc_parity(data); ← legal syntax but result is discarded — waste
// ✅ If you want a function with no return use: void'(calc_parity(data));The Most Common Error — Timing Inside a Function
If you accidentally put a timing control inside a function, the simulator gives an immediate compile-time error. This section shows exactly what the error looks like and how to fix it.
// ════ WRONG: timing control inside a function ════════════════════
function automatic logic [31:0] read_reg_bad (input logic [11:0] addr);
@(posedge clk); // ❌ COMPILE ERROR: event control not allowed
return regfile[addr]; // in a function
endfunction
// ════ FIX ①: Convert to a task (if timing is truly needed) ═══════
task automatic read_reg_good (
input logic [11:0] addr,
output logic [31:0] rdata
);
@(posedge clk); // ✅ legal in task
rdata = regfile[addr];
endtask
// Usage:
logic [31:0] val;
read_reg_good(12'h00C, val); // task call — standalone statement
// ════ FIX ②: Remove timing if it wasn't actually needed ══════════
function automatic logic [31:0] read_reg_comb (input logic [11:0] addr);
return regfile[addr]; // ✅ pure combinational — no timing needed
endfunction
// Usage (as expression):
assign reg_out = read_reg_comb(reg_addr); // ✅ zero time, RTL-safeQuick Decision Guide — Bookmark This
// ════ USE function WHEN ═══════════════════════════════════════════
// ✔ Computing a value to use in an expression
// ✔ No timing controls needed (#, @, wait)
// ✔ Called inside assign, always_comb, if condition, port, array index
// ✔ The code is combinational / synthesisable
// ✔ You want to call it inside an assertion property
// Examples: calc_parity(), crc8(), gray2bin(), is_one_hot(), priority_enc()
// ════ USE task WHEN ══════════════════════════════════════════════
// ✔ Waiting for clock edges, events, or delays
// ✔ Driving a bus transaction (APB, AHB, AXI, I2C, SPI...)
// ✔ Generating stimulus sequences
// ✔ Returning multiple values (via output / ref ports)
// ✔ Forking parallel activities
// ✔ The code is testbench-only (not for synthesis)
// Examples: apb_write(), wait_cycles(), reset_dut(), send_packet()
// ════ SPECIAL CASES ══════════════════════════════════════════════
// Need no return AND no timing?
function automatic void log_event (input string msg);
$display("[%0t] %s", $time, msg); // void function — no return value
endfunction // called as void'(log_event("boot"));
// Need timing AND return a value?
task automatic read_and_check (input addr, output logic [31:0] data,
output logic ok);
@(posedge clk); // ← task handles the timing
data = regfile[addr]; // ← output port acts as return value
ok = (data != '0);
endtaskCommon Mistakes
// ════ MISTAKE 1: Calling a task inside a function ════════════════
function automatic void bad_fn();
wait_cycles(5); // ❌ wait_cycles is a task — illegal inside function
endfunction
// ✅ FIX: convert the outer function to a task
task automatic good_task();
wait_cycles(5); // ✅ task can call task
endtask
// ════ MISTAKE 2: Calling a task inside always_comb ═══════════════
always_comb begin
apb_write(addr, data, err); // ❌ task in always_comb: timing violation
end
// ✅ FIX: use only functions inside always_comb
always_comb begin
parity = calc_parity(data); // ✅ function call: zero-time, safe
end
// ════ MISTAKE 3: Using a task where a function is expected ════════
assign out = apb_write(addr, data, err); // ❌ task in continuous assign
// ✅ FIX: apb_write should be a task called in initial/always, not assign
// ════ MISTAKE 4: Expecting a task to return a value ══════════════
task automatic get_data (input addr);
return regfile[addr]; // ❌ tasks cannot return values with 'return'
endtask
// ✅ FIX: use output port
task automatic get_data (input addr, output logic [31:0] rdata);
@(posedge clk); rdata = regfile[addr]; // ✅ output port carries result
endtaskWhat the Rest of Module 6 Covers
This page gave you the decision framework. The rest of the module goes deep on each piece:
- 6.2 — Function Declarations & Return Values: Syntax deep-dive: typed returns, named return variable, multiple return paths, functions in RTL.
- 6.3 — Task Declarations & Time-Consuming Calls: Full task syntax, output/inout/ref ports, disabling tasks, and real protocol driver examples.
- 6.4 — Automatic vs. Static Lifetime: Why recursive calls and concurrent tasks require
automatic, and whenstaticis intentional. - 6.5 — Pass by Value vs. Pass by Reference: input/output/inout vs. ref — when copies are made and when the original is modified in place.
- 6.6 — Default Argument Values: Write APIs that are easy to call: required arguments up front, optional ones with defaults at the end.
- 6.7 — void Functions & System Tasks Overview: When functions don't return a value, and a tour of the system tasks you use daily (
$display,$error,$finish, and more).
Functions in RTL — Synthesizable Hardware Helpers
Functions are the correct way to share combinational logic across multiple parts of RTL. They are synthesizable, work inside always_comb, and the tool unrolls them into parallel hardware — exactly like inlining the code manually. Here are the patterns used in production RTL daily.
// ── Pattern 1: Parameterized parity with synthesis-safe function ──
function automatic logic parity #(parameter int W=8) (input logic [W-1:0] d);
return ^d; // XOR-reduce: synthesizes to a log₂(W)-level XOR tree
endfunction
always_comb tx_parity = parity #(32)(tx_data); // 32-bit parity in hardware
// ── Pattern 2: Gray code to binary (used in FIFO pointer sync) ───
function automatic logic [7:0] gray2bin (input logic [7:0] g);
logic [7:0] b;
b[7] = g[7];
for (int i = 6; i >= 0; i--)
b[i] = b[i+1] ^ g[i];
return b;
endfunction
always_comb rd_ptr_bin = gray2bin(rd_ptr_gray); // FIFO async domain crossing
// ── Pattern 3: Saturating add (avoids overflow) ───────────────────
function automatic logic [7:0] sat_add (input logic [7:0] a, b);
logic [8:0] wide = {1'b0,a} + {1'b0,b};
return wide[8] ? 8'hFF : wide[7:0];
endfunction
always_comb result = sat_add(accum, delta); // overflow → clamp at 0xFF
// ── Pattern 4: One-hot check (used in assertions) ─────────────────
function automatic logic is_one_hot (input logic [7:0] v);
return (v != 8'h00) && ((v & (v-1)) == 8'h00); // power-of-2 check
endfunction
// In RTL assertion:
assert property (@(posedge clk) bus_active |-> is_one_hot(grant))
else $error("Grant not one-hot: %b", grant);
// ── Pattern 5: CRC-8 function (used in protocol checksum RTL) ─────
function automatic logic [7:0] crc8_update (
input logic [7:0] crc_in, data_byte);
logic [7:0] tmp = crc_in ^ data_byte;
for (int i = 0; i < 8; i++)
tmp = tmp[7] ? (tmp << 1) ^ 8'h07 : (tmp << 1);
return tmp;
endfunction
always_ff @(posedge clk) begin
if (valid) crc_reg <= crc8_update(crc_reg, rx_byte); // pipelined CRC
end
// ── Pattern 6: AXI address decode (mutually exclusive regions) ────
function automatic logic [1:0] axi_decode (input logic [31:0] addr);
if (addr inside {[32'h0000_0000:32'h0FFF_FFFF]}) return 2'b00; // BOOT ROM
else if (addr inside {[32'h1000_0000:32'h1FFF_FFFF]}) return 2'b01; // SRAM
else if (addr inside {[32'h4000_0000:32'h4FFF_FFFF]}) return 2'b10; // Peripherals
else return 2'b11; // Error
endfunction
always_comb slave_sel = axi_decode(awaddr); // zero-time decode in comb logicTasks in Verification — The Complete Testbench Toolkit
Every production verification environment is built from tasks. Each testbench component — driver, monitor, scoreboard helper, protocol checker — is a task or a collection of tasks. Here are the canonical patterns used in enterprise DV environments.
module tb_soc;
logic clk = 0, rst_n;
logic [31:0] awaddr, wdata, rdata;
logic awvalid, awready, wvalid, wready, bvalid, bready;
logic arvalid, arready, rvalid, rready;
int pass_cnt=0, fail_cnt=0;
// ── ① Clock generator ────────────────────────────────────────
always #5 clk = ~clk;
// ── ② Reset generator task ───────────────────────────────────
task automatic apply_reset(input int cycles = 8);
rst_n = 0;
repeat(cycles) @(posedge clk);
rst_n = 1;
@(posedge clk); // settle after release
endtask
// ── ③ AXI write transaction task ─────────────────────────────
task automatic axi_write(
input logic [31:0] addr, data,
output logic [1:0] bresp
);
fork
begin : aw_phase
awaddr = addr; awvalid = 1;
@(posedge clk iff awready); awvalid = 0;
end
begin : w_phase
wdata = data; wvalid = 1;
@(posedge clk iff wready); wvalid = 0;
end
join
bready = 1;
@(posedge clk iff bvalid);
bready = 0;
endtask
// ── ④ AXI read transaction task ──────────────────────────────
task automatic axi_read(
input logic [31:0] addr,
output logic [31:0] data
);
araddr = addr; arvalid = 1;
@(posedge clk iff arready); arvalid = 0;
rready = 1;
@(posedge clk iff rvalid);
data = rdata; rready = 0;
endtask
// ── ⑤ Scoreboard check task ──────────────────────────────────
task automatic check(
input logic [31:0] got, expected,
input string test_name
);
if (got === expected) begin
$display("[PASS] %s: got=%h", test_name, got); pass_cnt++;
end else begin
$error("[FAIL] %s: got=%h exp=%h", test_name, got, expected);
fail_cnt++;
end
endtask
// ── ⑥ Handshake wait with timeout task ───────────────────────
task automatic wait_valid(ref logic sig, input int timeout=100);
fork
begin : wait_proc @(posedge clk iff sig); end
begin : timeout_proc repeat(timeout) @(posedge clk);
$fatal(1, "Timeout waiting for signal");
end
join_any
disable fork;
endtask
// ── Main test sequence ────────────────────────────────────────
initial begin
logic [31:0] rd_val; logic [1:0] bresp;
apply_reset(); // ① reset
axi_write(32'h4000_0000, 32'hCAFE_BABE, bresp); // ② write
axi_read (32'h4000_0000, rd_val); // ③ readback
check(rd_val, 32'hCAFE_BABE, "Reg readback"); // ④ verify
$display("DONE: pass=%0d fail=%0d", pass_cnt, fail_cnt);
$finish;
end
endmoduleSimulation Behavior — How Tasks and Functions Execute
Understanding exactly when each executes in the simulation timeline prevents a whole class of timing and ordering bugs. The fundamental difference: functions complete within a single delta cycle; tasks can span thousands of time steps.
Function call — zero time
Time T=100 T=100 T=100
clk posedge ───── ─────
caller ──────────fn_call──continues ← All at same T=100 (same delta)
parity computed available ← Instantaneous result
Task call — time advances
Time T=100 T=110 T=120 T=130
clk posedge posedge posedge posedge
caller ──────────task_call─────────────────resumes
task start→@clk→@clk→@clk→done
bus sigs setup──────enable──done
Caller suspended during T=100→T=130. Other processes can run.// ── Concurrent task calls via fork-join ──────────────────────────
// Each task runs as a separate simulation process
initial begin
apply_reset();
fork // Launch two tasks in parallel
drive_master_a(); // ← starts at same simulation time
drive_master_b(); // ← starts at same simulation time
join // wait for BOTH to complete
$display("Both masters done");
end
// ── CRITICAL: tasks must be automatic for this to work safely ────
// If drive_master_a and drive_master_b are static, they share local vars
// → race condition on temp variables, non-deterministic test results
task automatic drive_master_a();
int beat; // ← automatic: each call gets own copy
for (beat = 0; beat < 10; beat++) begin
@(posedge clk);
master_a_data = beat;
end
endtask
// ── The $strobe trick: view task output AFTER it settles ──────────
task automatic timed_check(input logic [31:0] expected);
@(posedge clk);
// $display at posedge: sees PRE-NBA value of FF outputs
$display("(display) dout=%h", dut.dout); // may show old value!
// $strobe at posedge: sees FINAL settled value (post-NBA)
$strobe("(strobe) dout=%h", dut.dout); // correct: use this
if (dut.dout !== expected)
$error("Mismatch: got %h exp %h", dut.dout, expected);
endtaskAutomatic vs Static — The Most Dangerous Default
By default, tasks and functions in SystemVerilog are static — their local variables are allocated once and shared across all concurrent calls. This is correct for modules with no concurrent callers, but catastrophic in testbenches where multiple initial blocks may call the same task simultaneously. Always use automatic in verification code.
task send_packet(input int id);
int i; // STATIC: one copy shared
for(i=0; i<8; i++) begin
@(posedge clk);
data = id * 8 + i; // Thread A sets i
end // Thread B overwrites i!
endtask
initial send_packet(1); // Thread A
initial send_packet(2); // Thread B — shares i!
// Result: i is corrupted → random datatask automatic send_packet(input int id);
int i; // AUTOMATIC: own copy
for(i=0; i<8; i++) begin
@(posedge clk);
data = id * 8 + i; // Thread A's own i
end // Thread B has its own
endtask
initial send_packet(1); // Thread A — own i
initial send_packet(2); // Thread B — own i
// Result: correct, independent iterationsDebugging Academy — 8 Real Task/Function Bugs
Task Called Inside Function — Compile Error (Wrong Refactoring)
COMPILE ERROR// ❌ BUG: engineer refactored wait_cycles into a function
function automatic void delay_fn(input int n);
wait_cycles(n); // ❌ wait_cycles is a task — COMPILE ERROR
// "Cannot call task from function"
endfunction
task automatic wait_cycles(input int n);
repeat(n) @(posedge clk);
endtask
// ✅ FIX: The outer wrapper must also be a task
task automatic delay_task(input int n);
wait_cycles(n); // ✅ task can call task
endtaskIf a function could call a task, it would implicitly consume simulation time — violating the zero-time guarantee. The compiler enforces this statically at compile time. The call chain propagates: if any function in a call chain eventually calls a task, the entire chain must become tasks.
Tasks can call tasks and functions. Functions can ONLY call functions. If you're refactoring and find yourself needing to call a task from a function, the function must become a task. This cascades up the call tree.
Task Call Inside always_comb — Timing Violation and Wrong Behavior
ALWAYS_COMB VIOLATION// ❌ BUG: task with @(posedge clk) inside always_comb
always_comb begin
drive_bus(data_in, addr); // ❌ drive_bus is a task with @event
// always_comb cannot contain timing controls
// Tool ERROR: "Event control not allowed in always_comb"
end
// ✅ FIX A: use a function (if the logic is purely combinational)
always_comb begin
bus_out = compute_bus_val(data_in, addr); // ✅ function: zero-time
end
// ✅ FIX B: move the task call out of always_comb into initial/always
initial begin
drive_bus(data_in, addr); // ✅ task called from initial — legal
endCompile-time error from the simulator/synthesis tool: "Event control not allowed in always_comb" or "Timing control inside combinational block". The block refuses to elaborate.
always_comb models combinational logic — it must complete evaluation in zero simulation time whenever any input changes. A task containing @event or #delay would require the block to suspend and resume, which is incompatible with combinational hardware semantics.
Replace the task with a pure function inside always_comb, or move the time-consuming task call into an initial/always block where suspending is legal.
Static Task with Concurrent Callers — Non-Deterministic Variable Corruption
STATIC VARIABLE RACE// ❌ BUG: no 'automatic' keyword — local vars are STATIC (shared)
task send_burst(input int id, len);
int beat; // STATIC: one memory location, shared by ALL callers
for (beat=0; beat<len; beat++) begin
@(posedge clk);
data = id * 100 + beat; // Thread A writes beat=3
end // Thread B overwrites beat=7
endtask
initial send_burst(1, 8); // Thread A: sends id=1 data
initial send_burst(2, 8); // Thread B: sends id=2 data — shares 'beat' with A!
// Result: 'beat' gets corrupted. data shows garbage sequence.
// Regression: PASS on run 1, FAIL on run 2 (no seed change!).
// Root cause: non-deterministic interleaving of @(posedge clk) yields.
// ✅ FIX: add 'automatic'
task automatic send_burst(input int id, len);
int beat; // AUTOMATIC: each call gets its own stack frame
for (beat=0; beat<len; beat++) begin
@(posedge clk);
data = id * 100 + beat; // Thread A's own beat, Thread B's own beat
end
endtaskIntermittent regression failure: same test, same seed, sometimes PASS, sometimes FAIL. No reproducible pattern. The corrupted variable produces garbage data, scoreboard mismatches, or stuck-state hangs that appear randomly across runs.
Without automatic, all local variables in the task are static — one memory location shared across every concurrent call. When the task yields at @(posedge clk), the simulator may interleave Thread A and Thread B, each overwriting the other's beat. The interleaving order is non-deterministic.
Add the automatic keyword to the task declaration. Each concurrent call then gets its own stack frame — independent copies of every local variable.
Function Output Not Assigned on All Paths — Implicit Zero Return
MISSING RETURN PATH// ❌ BUG: function returns correct value on path A, implicit 0 on path B
function automatic logic [7:0] decode_op(input logic [3:0] op);
if (op == 4'h0) return 8'hAA; // path A: explicit return
if (op == 4'h1) return 8'hBB; // path B: explicit return
// op == 4'h2..F: NO return → implicit return of 8'h00
// Simulator: returns 0. Lint: "implicit function return"
// Bug is invisible if test never exercises op > 1
endfunction
// ✅ FIX: always have an explicit final return
function automatic logic [7:0] decode_op(input logic [3:0] op);
if (op == 4'h0) return 8'hAA;
if (op == 4'h1) return 8'hBB;
return 8'hFF; // ✅ explicit default: unknown/illegal opcode
endfunctionFunction silently returns 0 (or X in 4-state) for any input not covered by an explicit return. Downstream logic receives 0 where a valid opcode response was expected. The bug is invisible until the test exercises an unhandled opcode.
The named return variable is initialized to 0 (or X) at function entry. If execution reaches endfunction without an explicit return, that initial value is what the caller receives. Lint tools flag this as "implicit function return".
Every function must have a return on every possible code path. Add an explicit default or fall-through return at the end. Enable the "missing return" lint rule in your flow.
Task Called From always_ff — Timing Control Inside Sequential Block
SEQUENTIAL BLOCK VIOLATION// ❌ BUG: always_ff cannot suspend and wait inside a task
task automatic sample_input(output logic [7:0] val);
@(posedge clk); // ← extra clock edge inside task!
val = data_in;
endtask
always_ff @(posedge clk) begin
sample_input(captured); // ❌ tool warning/error: timing in always_ff
end // Would consume 2 clock edges per always_ff trigger
// ✅ FIX: use always_ff directly for sequential capture
always_ff @(posedge clk) begin
captured <= data_in; // ✅ direct register capture — no task needed
end
// ✅ FIX: or use a pure function if the operation is combinational
always_ff @(posedge clk) begin
captured <= process_input(data_in); // ✅ function: zero-time, safe in ff
endOutputs appear one cycle late compared to the intended design. The simulator may also error with "Event control not allowed in always_ff" depending on tool strictness. Behavioral simulation appears correct only because the engineer compensated for the extra latency elsewhere.
The always_ff block triggers on a clock edge and the task immediately waits for the next clock edge. The result: the block effectively takes two cycles instead of one. This violates the always_ff timing model, which assumes all work completes within the current edge's NBA region.
Never use time-consuming tasks inside always_ff. Use a pure function for combinational operations called from always_ff. For multi-cycle sequential work, build a state machine where each cycle is one always_ff evaluation.
Task Output Port Not Driven on Error Path — Caller Gets Stale Value
UNDRIVEN OUTPUT PORT// ❌ BUG: task returns early on error without setting output port
task automatic read_reg(
input logic [11:0] addr,
output logic [31:0] rdata,
output logic error
);
if (addr > 12'hFFF) begin
error = 1;
return; // ❌ rdata never assigned on this path!
end
@(posedge clk);
rdata = regfile[addr];
error = 0;
endtask
// Caller: rdata has stale/undefined value when error=1
// Scoreboard may check rdata even when error=1 — false mismatch
// ✅ FIX: always drive all outputs before return
task automatic read_reg(
input logic [11:0] addr,
output logic [31:0] rdata,
output logic error
);
rdata = '0; error = 0; // ← default ALL outputs at entry
if (addr > 12'hFFF) begin
error = 1;
return; // ✅ rdata='0 is well-defined
end
@(posedge clk);
rdata = regfile[addr];
endtaskWhen the error path triggers, the caller observes a stale or random value on the unassigned output port. Scoreboards may check the data port even when error=1 and report false mismatches — wasting hours on chasing a bug that does not exist in the DUT.
SystemVerilog task output ports are not implicitly cleared on entry. If a code path exits via early return without writing to an output, the variable retains whatever value the caller passed in (or its last assignment). The result is undefined from the caller's perspective.
At the very top of every task, default all output ports to a well-defined value ('0 or a documented sentinel). Then any early return leaves the output in a known state.
Recursive Function Without Automatic — Stack Corruption
RECURSION BUG// ❌ BUG: static function cannot recurse correctly
function int factorial_bad(input int n);
if (n <= 1) return 1;
return n * factorial_bad(n-1); // ❌ n is static — all recursions share it!
endfunction
// factorial_bad(5) — n is shared: each recursive call overwrites same n
// Result: undefined. Tools may error or produce wrong value.
// ✅ FIX: add automatic — each recursion gets own stack frame
function automatic int factorial(input int n);
if (n <= 1) return 1;
return n * factorial(n-1); // ✅ each call has own n on its stack frame
endfunction
// Example output: factorial(5) = 120 ✅Recursive function returns an obviously wrong value — often 1 (the base case) or an unpredictable intermediate result. Behaviour may be simulator-dependent. The mathematically correct result is never produced.
Without automatic, the function has static storage: one shared copy of every local variable and argument. The inner recursive call overwrites the shared n. When the outer call resumes, its n has been clobbered.
Any function that calls itself — or that is called from multiple concurrent processes — must be automatic. A safe default: declare all testbench functions automatic.
Expecting Task to Return Value Like a Function — Output Port Ignored
WRONG USAGE PATTERN// ❌ BUG: task result used in expression — but tasks don't return values
task automatic read_and_check(input addr, output logic [31:0] data);
@(posedge clk); data = regfile[addr];
endtask
logic [31:0] val;
// ❌ Engineer tries to use result directly:
if (read_and_check(addr, val) == 32'hFF) // COMPILE ERROR: task in expression
$display("found");
// ✅ FIX: call task as statement, then use the output port separately
read_and_check(addr, val); // task call: standalone statement
if (val == 32'hFF) $display("found"); // use the output port result
// ✅ Or: if no timing needed, convert to function
function automatic logic [31:0] read_reg(input addr);
return regfile[addr]; // ✅ zero-time, usable in expression
endfunction
if (read_reg(addr) == 32'hFF) $display("found"); // ✅ function in expressionCompile-time error: "Task call not allowed in expression" or "Cannot use task call as operand". The code refuses to elaborate.
Tasks do not have a return value — they communicate results via output/inout/ref ports. The expression context (if (...), assign, +) expects a value, but the task call evaluates to nothing.
Call the task as a standalone statement first to populate its output port, then use that variable in the expression. If the operation is zero-time, convert the task into a function — functions return values and can be used inline.
Interview Q&A — Tasks and Functions
A function executes in zero simulation time; a task can consume simulation time.
Functions cannot contain any timing controls: no #delay, no @(event), no wait(). They execute instantaneously from the simulator's perspective — time T before the call, time T after the call.
Tasks can contain any timing controls. They can wait for clock edges, delays, and conditions. Simulation time advances while a task is executing.
This distinction determines everything else: functions are used for combinational computation (RTL and assertions), tasks are used for sequential time-consuming operations (testbench drivers, protocol monitors, stimulus generators).