Using Tasks and Functions in Verilog

Share on facebook
Share on twitter
Share on linkedin

Table of Contents

In this post we look at how we use tasks and functions in verilog. Collectively, these are known as subprograms and they allow us to write verilog code which is reusable.

As with most programming languages, we should try to make as much of our verilog code as possible reusable. This allows us to reduce development time for future projects as we can more easily port code from one design to another.

Whilst functions should be familiar to anyone with experience in other programming languages, tasks are less common in other languages.

There are two main differences between functions and tasks.

When we write a verilog function, it performs a calculation and returns a single value.

In contrast, a verilog task executes a number of sequential statements but doesn’t return a value. Instead, the task can have an unlimited number of outputs

In addition to this, verilog functions execute immediately and can’t contain time consuming constructs such as delays, posedge macros or wait statements

A verilog task , on the other hand, can contain time consuming constructs.

We will discuss both of these constructs in depth in the rest of this post. This includes giving examples of how we write and call functions and tasks in verilog.

Verilog Function

In verilog, a function is a subprogram which takes one or more input values, performs some calculation and returns an output value.

We use functions to implement small portions of code which we want to use in multiple places in our design.

By using a function instead of repeating the same code in several places, we make our code more maintainable.

We write the code for functions in the verilog module which will make use of the function.

The code snippet below shows the general syntax for a function in verilog.

// First function declaration style - inline arguments
function <return_type> <name> (input <arguments>); 
  // Declaration of local variables
  begin
    // function code
  end
endfunction : <name>

// Second function declaration style - arguments in body
function <return_type> <name>;
  (input <arguments>);
  // Declaration of local variables
  begin
    // function code
  end
endfunction : <name>

We must give every function a name, as denoted by the <name> field above.

We can also include the <name> field label after using the endfunction keyword. However, this is not mandatory.

We can either declare the inputs inline with the function declaration or as part of the function body. The method we use to declare the input arguments has no affect on the performance of the function.

However, when we use inline declaration we can also omit the begin and end keywords if we want to.

The inputs are passed to the function using the <arguments> field in the above example.

We use the <return_type> field to declare which verilog data type the function returns. If we exclude this part of the function declaration, then the function will return a 1 bit value by default.

When we return a value we do it by assigning a value to the name of the function. The code snippet below shows how we would simply return the input to a function.

function integer easy_example (input integer a);
  easy_example = a;
endfunction : easy_example

Rules for Using Functions in Verilog

Although functions are often fairly simple, there are a few basic rules which we must follow when we write a verilog function.

One of the most important rules of a function is that they can’t contain any time consuming constructs such as delays, posedge macros or wait statements.

When we want to write a subprogram which consumes time we should use a verilog task instead.

As a result of this, we are also not able to call tasks from within a function. In contrast, we can call another function from within the body of a function.

As functions execute immediately, we can only use blocking assignment in our verilog functions.

When we write functions in verilog, we can declare and use local variables. This means that we can declare variables in the function which can’t be accessed outside of the function it is declared in.

In addition to this, we can also access all global variables within a verilog function.

For example, if we declare a function within a module block then all of the variables declared in that module can be accessed and modified by the function.

The table below summarises the rules for using a function in verilog.

Rules for Using Functions in Verilog
Verilog functions can have one or more input arguments
Functions can only return one value
Functions can not use time consuming constructs such as posedge, wait or delays (#)
We can’t call tasks from within a function
We can call other functions from within a function
Non-blocking assignment can’t be used within a function
Local variables can be declared and used inside of the function
We can access and modify global variables from inside a verilog function
If we don’t specify a return type, the function will return a single bit

Verilog Function Example

To better demonstrate how to use a verilog function, let’s consider a basic example.

For this example, we will write a function which takes 2 input arguments and returns the sum of them.

We use verilog integer types for the input arguments and the return types.

We must also make use of the verilog addition operator in order to calculate the sum of the inputs.

The code snippet below shows the implementation of this example function in verilog.

As we have previously discussed, there are two methods we can use to declare verilog functions and both of these are shown in the code below.

// Using inline declaration of the inputs
function integer addition (input integer in_a, in_b);
  // Return the sum of the two inputs
  addition = in_a + in_b;
endfunction : addition

// Declaring the inputs in the function body
function integer addition;
  input integer in_a;
  input integer in_b;
  begin
    // Return the sum of the two inputs
    addition = in_a + in_b;
  end
endfunction : addition

Calling a Function in Verilog

When we want to use a function in another part of our verilog design, we have to call it. The method we use to do this is similar to other programming languages.

When we call a function we pass parameters to the function in the same order as we declared them. This is known as positional association and it means that the order we declare our arguments in is very important.

The code snippet below shows how we would use positional association to call the addition example function.

In the example below, in_a would map to the a argument and in_b would map to b.

// Calling a verilog function using positional association
func_out = addition(a, b);

Automatic Functions in Verilog

We can also use the verilog automatic keyword to declare a function as reentrant.

This means that the variables and arguments within the function are dynamically allocated. In contrast, normal functions use static allocation for internal variables and arguments.

When we we write a normal function, all of the memory which is used to perform the processing of the function is allocated only once. This is process is known as static memory allocation in computer science.

As a result of this, our simulation software must execute the function fully before it can use the function again.

This also means that the memory the function uses is never freed. As a result of this, any values stored in this memory will maintain their value between calls to the function.

In contrast, functions which use the automatic keyword allocate memory whenever the function is called. The memory is then freed once the function has finished with it.

This is process is known as automatic or dynamic memory allocation in computer science.

As a result of this, our simulation software can execute multiple instances of an automatic function .

We can use the automatic keyword to write recursive functions in verilog. This means we can create functions which call themselves to perform a calculation.

As an example, one common use case for recursive functions is calculating the factorial of a given number.

The code snippet below shows how we would use the automatic keyword to write a recursive function in verilog.

function automatic integer factorial (input integer a);
  begin
    if (a > 1) begin
      factorial = a * factorial(a - 1);
    end
    else begin
      factorial = 1;
    end
  end
endfunction : factorial

Verilog Task

Just like functions, we use tasks to implement small sections of code which we can reuse throughout our design.

In verilog, a task can have any number of inputs and can also generate any number of outputs. This is in contrast to functions which can only have one output.

Unlike functions, we can also use timing consuming constructs such as wait, posedge or delays (#) within a task.

As a result of this, we can use both blocking and non-blocking assignment in verilog tasks.

These features mean tasks are best used to implement simple pieces of code which are repeated several times in our design. A good example of this would be driving the pins on a known interface, such as SPI or I2C.

We write the code for tasks in the verilog module which will make use of the task.

We can also create global tasks which are shared by all modules in a given file. To do this we simply write the code for the task outside of the module declarations in the file.

The code snippet below shows the general syntax for a task in verilog.

As with functions, there are two ways in which we can declare a task but the performance of both approaches is the same.

// Task syntax using inline IO
task <name> (<io_list>);
  // Code which implements the task
endtask

// Task syntax with IO declared in the task body
task <name>;
  <io_list>
  begin
    // Code which implements the task
  end
endtask

We must give every task a name, as denoted by the <name> field above.

When we write tasks which declare the IO within the task body, we must use the begin and end keywords as well.

However, when we use inline declaration for the IO, we can omit the begin and end keywords.

When we write tasks in verilog, we can declare and use local variables. This means that we can create variables in the task which can’t be accessed outside of the task it is declared in.

In addition to this, we can also access all global variables within a verilog task.

Unlike verilog functions, we can call another task from within a task. We can also make calls to functions from within a task.

Verilog Task Example

Let’s consider a simple example to better demonstrate how to write a verilog task.

For this example, we will write a basic task which can be used to generate a pulse. The length of the pulse can be specified when we call the task in our design.

To do this we require one input, which determines how long the pulse is, and an output for the generated pulse.

The verilog code below shows the implementation of this example using the two different styles of task.

// Task implementation using inline declaration of IO
task pulse_generate(input time pulse_length, output pulse);
  pulse = 1'b1
  #pulse_time
  pulse = 1'b0;
endtask

// Task implementation with IO declared in body
task pulse_generate;
  input time pulse_length;
  output pulse;
  begin
    pulse = 1'b1;
    #pulse_time
    pulse = 1'b0;
  end
endtask

Although this example is quite simple, we can see here how we can use the verilog delay operator (#) in a task. If we attempted to write this code in a function, this would be illegal.

We can also see from this example that we don’t return a value in the same way as we do with a function.

Instead, we must declare any outputs which we use in the task declaration.

We can include and drive as many outputs as we want when we write a task in verilog.

Calling a Task in Verilog

As with functions, we must call a task when we want to use it in another part of our verilog code.

The method we use to do this is similar to the method used to call a function.

However, there is one important difference between calling tasks and functions in verilog.

When we call a task in verilog, we can’t use it as part of an expression in the same way as we can a function.

We should instead think of task calls as being a short way of including a block of code into our design.

As with functions, we use positional association to pass paramaters to the task when we call it.

This simply means that we pass parameters to the task in the same order as we declared them when we wrote the task code.

The code snippet below shows how we would use positional association to call the pulse_generate task which we previously considered.

In this case, the pulse_length input is mapped to the pulse_time variable and the pulse output is mapped to the pulse_out variable.

// Calling a task using positional association
generate_pulse(pulse_time, pulse_out);

Automatic Tasks in Verilog

We can also use the automatic keyword with verilog tasks in order to make them reentrant.

As we talked about previously, using the automatic keyword means that our simulation tool uses dynamic memory allocation.

As with functions, tasks use static memory allocation by default which means that only one instance of a task can be run by the simulation software.

In contrast, tasks which use the automatic keyword allocate memory whenever the task is called. The memory is then freed once the task has finished with it.

Let’s consider a basic example to show automatic tasks are used and how they differ from normals task.

For this example, we will use a simple task which increments the value of a local variable by a given amount.

We can then run this a number of times in a simulation tool to see how the local variable behaves using an automatic task and a non automatic task.

The code below shows how we write a static task to implement this example.

// Task which performs the increment
task increment(input integer incr);
  integer i = 1;
  i = i + incr;
  $display("Result of increment = %0d", i);
endtask
 
// Run the task three times
initial begin
  increment(1);
  increment(2);
  increment(3);
end

Running this code in the Mentor QuestaSim simulation tool results in the following output:

# Result of increment = 2
# Result of increment = 4
# Result of increment = 7

As we can see from this, the value of the local variable i is static and stored in a single memory location.

As a result of this, the value of i is persistent and it maintains it’s value between calls to the task.

When we call the task we are incrementing the value which is already stored in the given memory location.

The code snippet below shows the same task except that this time we use the automatic keyword.

// Automatic task which performs the increment 
task automatic increment(input integer incr);
  integer i = 1;
  i = i + incr;
  $display("Result of increment = %0d", i);
endtask
 
// Run the task three times
initial begin
  increment(1);
  increment(2);
  increment(3);
end

Running this code in the Mentor QuestaSim simulation tool results in the following output:

# Result of increment = 2
# Result of increment = 3
# Result of increment = 4

From this we can now see how the local variable i is dynamic and is created whenever the task is called.

When the task has finished running, the dynamically allocated memory is freed and the local variable no longer exists.

As a result of this, we create a new variable and assign it to a value of 1 whenever we call the task.

Exercises

There are two main differences between tasks and functions, what are they?

show answer

A task can have ore than one output but a function can only have one. A function can not consume time but a task can.

hide answer

What is the difference between an automatic function and a normal function in verilog?

show answer

Normal verilog functions use static memory allocation whereas automatic functions use dynamic memory allocation.

hide answer

Write the code for a function which takes 3 integer inputs and returns the product of them.

show answer
function integer product(input integer a, b, c);
begin
  product = a * b * c;
endfunction : product
hide answer

Write the code for a task which can generate a fixed number of pulses. The task should take three inputs – one which sets the high time of the pulse, one which sets the low time of the pulse and another which determines how many pulses should be generated. (TIP: Use a for loop to generate the pulses).

show answer
task pulse_generate;
  // Task IO
  input time pulse_high;
  input time pulse_low;
  input integer num_pulses;
  output pulse;
  // Local loop variable
  integer i;
  begin
    for (i = 0; i < num_pulses; i = i + 1) begin
      #pulse_low pulse = 1'b1;
      #pulse_high pulse = 1'b0;
    end
  end
endtask
hide answer

Enjoyed this post? Why not share it with others

Share on facebook
Share on twitter
Share on linkedin

Leave a Comment

ENJOYING THIS ARTICLE?

Why not join our mailing list and be the first to hear about our latest FPGA tutorials

Close