A Verilog task is a named, reusable block of procedural code that can take inputs, return any number of results through output and inout arguments, and, unlike a function, contain timing controls such as # delays, @ event controls and wait. Tasks are used mostly in testbenches, to package sequences that take simulation time, such as driving a bus transaction or applying a reset.

12 min read | Beginner Level

What You'll Learn

  • How to declare and call a task, with inputs, outputs and timing controls
  • How static and automatic tasks differ when calls overlap
  • When to use a task instead of a function, and what tasks synthesize into
  • How to stop a running task with disable, and avoid common task bugs

Syntax

A task can declare its arguments in the body (Style 1) or in a port list after the name (Style 2). Arguments can be input, output or inout, and a task can also have no arguments at all.


// Style 1
task [name];
	input  [port_list];
	inout  [port_list];
	output [port_list];
	begin
		[statements]
	end
endtask

// Style 2
task [name] (input [port_list], inout [port_list], output [port_list]);
	begin
		[statements]
	end
endtask

// Empty port list
task [name] ();
	begin
		[statements]
	end
endtask
  • name - The task name, used to call it (required)
  • input - Values copied into the task when it is called
  • output - Values copied out to the caller's variables when the task finishes
  • inout - Copied in at the call and copied out at the end
  • statements - The task body; it may contain delays, event controls and calls to other tasks and functions

The empty parentheses in task name (); are SystemVerilog syntax. In Verilog, a task without arguments is declared as task name; and called as name;. Most simulators accept both.

A task is called as a statement on its own, not as part of an expression, because it does not return a value. Each argument in the call is matched to the task's arguments in the order they were declared.

Static Task

This task adds two 8-bit values and returns the result through an output argument. Both declaration styles below describe the same task.


	task sum (input [7:0] a, b, output [7:0] c);
		begin
			c = a + b;
		end
	endtask
// or
	task sum;
		input  [7:0] a, b;
		output [7:0] c;
		begin
			c = a + b;
		end
	endtask

	reg [7:0] x, y, z;

	initial begin
		x = 3;
		y = 4;
		sum (x, y, z);    // After the call, z = 7
	end

By default, a Verilog task is static: there is only one copy of its arguments and internal variables, and every call uses that same copy. If two calls of the same task run at the same time, for example from two initial blocks, they read and write the same variables. This example shows the effect:


module tb;

  initial display();
  initial display();
  initial display();
  initial display();

  // This is a static task
  task display();
    integer i = 0;
    i = i + 1;
    $display("i=%0d", i);
  endtask
endmodule
Output
xcelium> run
i=1
i=2
i=3
i=4
xmsim: *W,RNQUIE: Simulation is complete.

All four calls share one variable i. In a static task, the initial value = 0 is applied only once, at the start of simulation, so each call adds 1 to the value left by the previous call.

Automatic Task

Declaring a task automatic gives every call its own copy of the task's arguments and variables, created when the call starts. Calls that overlap in time no longer interfere with each other.


module tb;

  initial display();
  initial display();
  initial display();
  initial display();

  // Note that the task is now automatic
  task automatic display();
    integer i = 0;
    i = i + 1;
    $display("i=%0d", i);
  endtask
endmodule
Output
xcelium> run
i=1
i=1
i=1
i=1
xmsim: *W,RNQUIE: Simulation is complete.

Each call now creates a new i, initialized to 0, so every call prints 1.

Declaring a variable with an initial value inside a task, as in integer i = 0;, is SystemVerilog syntax, and these examples rely on its rule that a static variable is initialized once while an automatic one is initialized on every call. In Verilog, declare integer i; and assign it in the task body.

Quick Check

Q: A testbench calls a static task send_byte from two initial blocks, and both calls are waiting on a # delay at the same time. What can go wrong?

Show Answer

Both calls share the same argument and local variables. When the second call starts, it overwrites the first call's byte value, so the first call continues with the second call's data after its delay. Declaring the task automatic fixes this.

Using Tasks in Testbenches

The main use of tasks is to package a timed sequence of signal changes, so the stimulus code reads as a list of operations instead of a list of signal assignments. This testbench task performs one write on a simple register interface:


module tb;
  reg        clk = 0;
  reg        wr_en;
  reg  [7:0] wr_addr, wr_data;

  always #5 clk = ~clk;

  // Drive one write transaction: one clock cycle with wr_en high
  task write_reg (input [7:0] addr, input [7:0] data);
    begin
      @(posedge clk);          // Wait for a clock edge: allowed in a task
      wr_en   <= 1'b1;         // Drive module-level signals directly
      wr_addr <= addr;
      wr_data <= data;
      @(posedge clk);
      wr_en   <= 1'b0;         // End of the transaction
    end
  endtask

  initial begin
    wr_en = 0;
    write_reg(8'h10, 8'hA5);   // Each call takes two clock cycles
    write_reg(8'h14, 8'h3C);
    #20 $finish;
  end
endmodule

The task drives wr_en, wr_addr and wr_data directly instead of through output arguments. A task can read and write any variable of the module it is declared in, and the signals change at the moment of each assignment, which is what a bus interface needs.

Where are Tasks Used?

Verification teams wrap every bus protocol the design supports in tasks such as write_reg, read_reg and send_packet. These collections, often called bus functional models, let a test configure a peripheral or stream traffic through a network block in a few readable lines, while the tasks handle the clock-by-clock protocol details.

Tasks in Synthesizable Code

A task can be used in synthesizable RTL if it contains no timing controls. Like a function, it is inlined at each call: synthesis copies its body into the calling always block, so each call builds its own logic. Because a function is simpler and can be used in expressions, most designers use functions for synthesizable code and keep tasks for testbenches. See Verilog Functions.

Global tasks

A task declared outside all modules can be called from any module in the same compilation unit:


// This task is outside all modules
task display();
  $display("Hello World !");
endtask

module des;
  initial begin
    display();
  end
endmodule
Output
xcelium> run
Hello World !
xmsim: *W,RNQUIE: Simulation is complete.

Declaring a task outside a module is a SystemVerilog feature. In Verilog, every task must be declared inside a module. To share one task between several modules, put it in a separate file and add it to each module with `include.

If the task is declared inside module des, it is local to that module. Another module can still call it with a hierarchical reference through the instance name:


module tb;
	des u0();

	initial begin
		u0.display();  // Reached with a hierarchical name, because 'tb' has no task called display
	end
endmodule

module des;
	initial begin
		display(); 	// Task definition is local to the module
	end

	task display();
		$display("Hello World");
	endtask
endmodule
Output
xcelium> run
Hello World
Hello World
xmsim: *W,RNQUIE: Simulation is complete.

The message appears twice: once from the initial block inside des, and once from tb calling u0.display(). See Verilog Hierarchical Reference Scope for more on hierarchical names.

Reset and Initialization Sequences

Almost every testbench starts with the same steps: hold reset for a number of cycles, release it, then program a set of configuration registers. Teams keep these as shared tasks, such as apply_reset and init_config, so every test starts the design the same way and a change to the reset sequence is made in one place.

Difference between function and task

FunctionTask
Cannot contain timing controls (#, @, wait), so it completes in zero simulation timeCan contain timing controls and may complete at a later simulation time
Cannot call a task, because the task might contain timing controlsCan call other tasks and functions
Needs at least one input, and cannot have output or inout argumentsCan have zero or more arguments of any direction
Returns a single value, so it can be used in an expressionReturns no value; results come back through output arguments, and the call is a separate statement

The compiler enforces these rules. Here a function contains a #10 delay:


module tb;
  reg signal;

  initial wait_for_1(signal);

  function wait_for_1(reg signal);
    #10;
  endfunction
endmodule
Output
    #10;
    |
xmvlog: *E,BADFCN (testbench.sv,7|4): illegal time/event control statement within a function or final block or analog initial block [10.3.4(IEEE)].

To wait inside a reusable block of code, declare it as a task instead.

Disable Task

A running task, or a named block inside it, can be stopped with the disable statement. Execution then continues with the statement after the disabled block.


module tb;

  initial display();

  initial begin
  	// After 50 time units, disable a particular named
  	// block T_DISPLAY inside the task called 'display'
    #50 disable display.T_DISPLAY;
  end

  task display();
    begin : T_DISPLAY
      $display("[%0t] T_Task started", $time);
      #100;
      $display("[%0t] T_Task ended", $time);
    end

    begin : S_DISPLAY
      #10;
      $display("[%0t] S_Task started", $time);
      #20;
      $display("[%0t] S_Task ended", $time);
    end
  endtask
endmodule
Output
xcelium> run
[0] T_Task started
[60] S_Task started
[80] S_Task ended
xmsim: *W,RNQUIE: Simulation is complete.

Block T_DISPLAY starts at 0 and waits for 100 time units, but it is disabled at 50, so its "ended" message never prints. The task continues with S_DISPLAY at 50, which waits 10 more units and starts at 60, then ends at 80. Writing disable display; instead would stop the whole task, including S_DISPLAY.

Quick Check

Q: Can you write result = sum(x, y, z); to call the sum task from the Static Task section?

Show Answer

No. A task does not return a value, so it cannot appear in an expression or on the right-hand side of an assignment. Call it as a statement, sum(x, y, z);, and read the result from z. To use the result in an expression, write a function instead.

Timeouts in Testbenches

Tests often wait for a design event, such as an interrupt or a "done" flag, that might never come if the design is broken. A common pattern starts the wait in one block and a timer in another, and whichever finishes first uses disable to stop the other. This lets the test report a timeout error instead of hanging forever.

Common Beginner Mistakes

Mistake #1: Expecting output arguments to change during the task

Wrong: task pulse (output sig); begin sig = 1; #10; sig = 0; end endtask, called as pulse(wr_en);
Right: Assign the module-level signal directly inside the task: wr_en = 1; #10; wr_en = 0;

Why? An output argument is copied back to the caller's variable only when the task finishes. wr_en never goes high; it only receives the final value 0 at the end. Signals that must change while the task runs have to be assigned directly.

Mistake #2: Calling a static task from overlapping threads

Wrong: Two initial blocks calling the same static send_byte task at the same time
Right: Declare it as task automatic send_byte ...

Why? A static task has one shared copy of its arguments and variables. A second call that starts while the first is still waiting overwrites the first call's data, as the static task example shows. The simulation runs without errors, but the stimulus is wrong.

Mistake #3: Using a task where a function is needed

Wrong: assign y = sum(a, b, c); or if (check(data)) ... with tasks
Right: Use a function for any computation whose result is used in an expression

Why? A task call is a statement and returns no value, so it cannot be used in assign statements, conditions or other expressions. The compiler reports an error.

Mistake #4: Putting timing controls in a task used in RTL

Wrong: Calling a task that contains @(posedge clk) or #5 from a design's always block
Right: Keep RTL tasks free of timing, and describe multi-cycle behavior with registers and a state machine

Why? Synthesis inlines the task into the always block, and hardware cannot pause partway through a block. Synthesis tools reject or ignore the timing controls, so the netlist does not match the simulation. Tasks with timing belong in testbenches.

Practice Exercise

Challenge: A Reusable Reset Task

In a testbench with a clock clk and an active-low reset rstn, write a task apply_reset with one input, cycles. It should drive rstn low, wait for cycles rising clock edges, then drive rstn high. Call it once at the start of the test with 5 cycles.

Hint

Assign rstn directly inside the task rather than through an output argument, so it changes while the task runs. repeat (cycles) @(posedge clk); waits for the given number of edges.

Solution

module tb;
  reg clk = 0;
  reg rstn;

  always #5 clk = ~clk;

  // Hold reset for a given number of clock cycles, then release it
  task apply_reset (input integer cycles);
    begin
      rstn = 1'b0;                     // Assert reset immediately
      repeat (cycles) @(posedge clk);  // Timing control: allowed in a task
      rstn <= 1'b1;                    // Release just after the last edge
    end
  endtask

  initial begin
    apply_reset(5);                    // Reset held for 5 rising edges
    // ... rest of the test ...
    #100 $finish;
  end
endmodule

The task finishes at the fifth rising edge, so the test continues with the design out of reset. It must be a task, not a function, because it waits for clock edges. Since it assigns rstn directly, the reset is visible to the design for the whole time the task runs.

Summary

In this tutorial, you learned:

  • A task is a reusable block of procedural code that can contain delays, event controls and wait
  • Tasks return results through output and inout arguments, which are copied back only when the task ends
  • A task call is a statement, so it cannot be used in an expression
  • Static tasks share one copy of their variables; automatic tasks give each call its own copy
  • Tasks are mainly used in testbenches to package timed sequences such as bus transactions and resets
  • A task without timing controls is synthesizable and is inlined at each call, like a function
  • disable stops a running task or a named block inside it
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5