Tasks, functions and reusable definitions

Learning objectives
  • Use functions for reusable combinational calculations
  • Use tasks for testbench procedures
  • Choose parameters and localparams for constants
  • Apply include files and macros without hiding design intent
1 Objective→ 2 Learn→ 3 Example→ 4 Practice→ 5 Verify→ 6 Continue
How to study this lessonUnderstand the idea first. Use the example to see the behaviour, practise where provided, then make sure you can explain the result before continuing.
Inputs
→
function
zero-time calculation
→
value
Testbench tasks may also control stimulus over time.
Concept diagram: Tasks, functions and reusable definitions

Explanation

Functions calculate one value without consuming time

function [7:0] saturating_add8;
    input [7:0] a;
    input [7:0] b;
    reg   [8:0] wide_sum;
    begin
        wide_sum = {1'b0, a} + {1'b0, b};
        if (wide_sum[8])
            saturating_add8 = 8'hFF;
        else
            saturating_add8 = wide_sum[7:0];
    end
endfunction

Use a function when the operation is conceptually combinational and returns one result. A synthesizable function must avoid delays, event controls, and simulation system tasks. Inputs and local variables should make widths explicit.

Call it as part of an expression:

assign sum_sat = saturating_add8(a, b);

Tasks organize procedures

Tasks can have input, output, and inout arguments and may return multiple results. In testbenches they can wait for events and simplify repeated stimulus.

task drive_and_check;
    input [7:0] stimulus;
    input [7:0] expected;
    begin
        @(negedge clk);
        data_in = stimulus;
        @(posedge clk);
        #1;
        if (data_out !== expected)
            $fatal(1, "input=%h expected=%h got=%h",
                   stimulus, expected, data_out);
    end
endtask

The @, #1, display, and fatal behavior make this a testbench task. Do not mix such code into synthesizable RTL.

Task and function lifetime

Traditional Verilog tasks and functions are static by default: local storage can be shared across overlapping calls. That is harmless for a simple combinational function called once at a time, but can corrupt results when a task is re-entered or called concurrently. The automatic keyword gives each active call its own local storage:

task automatic compare_byte;
    input [7:0] actual;
    input [7:0] expected;
    begin
        if (actual !== expected)
            $display("Mismatch expected=%02h actual=%02h", expected, actual);
    end
endtask

Use automatic lifetime for re-entrant testbench helpers when supported by the selected Verilog mode. Confirm simulator compatibility instead of assuming a construct accepted in SystemVerilog mode is accepted in every Verilog mode.

Constants have different ownership

  • parameter: parent-visible configuration.
  • localparam: internal constant that cannot be overridden.
  • macro: global text substitution after preprocessing.

Prefer parameters, localparams, and functions when they express the requirement. A macro does not understand Verilog scope or types and can produce surprising precedence.

`define DOUBLE_BAD(x) x + x

For DOUBLE_BAD(a & b), substitution and operator precedence may not express the intended grouping. If a macro is required, parenthesize arguments and the whole result, and keep its scope controlled.

Include discipline

Use include files for shared declarations only under a clear project convention. Do not rely on a hidden compile-directory search path. File lists should specify include directories, and include guards should prevent accidental duplicate definitions.

Worked example

The saturating adder first widens both operands so carry cannot disappear. The function returns 8'hFF when carry is set. For 8'd200 + 8'd100, the mathematical sum is 300 and the returned saturated value is 255.

Check yourself

Why should a synthesizable function not contain # or @? When is a testbench task more readable than copied stimulus? Why can a text macro create precedence problems?