Using Tasks and Functions in Verilog

By John
November 2, 2020

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 we will use to call 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

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

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

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.

We use the <arguments> field in the above example to declare the inputs to our function.

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. We can also simulate this example on EDA playground.

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

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.

We can also simulate this example using EDA playground.

// 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

// 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

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
func_out = addition(a, b);

Automatic Functions in Verilog

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

However, the automatic keyword was introduced in the verilog 2001 standard meaning that we can't write reentrant functions when working with the verilog 1995 standard.

When we declare a function as reentrant, 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 in it's entirety before it can use the function again.

This also means that the memory the function uses is never deallocated. 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 deallocated once the function has finished with it.

This 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. We can also simulate this example using EDA playground.

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

Verilog Task

We use verilog tasks to write small sections of code that we can reuse throughout our design.

Unlike functions, we can use time 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.

Verilog tasks can also have any number of inputs and can also generate any number of outputs. This is in contrast to functions which can only return a single value.

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 often write the code for tasks in the verilog module which will be used to call the task.

When we do this, we can also read or write any of the module's global variables inside of the task body.

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>);
  begin
    // Code which implements the task
  end
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 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.

In order to do this, we must declare a single time type input in our task.

We will generate the pulse on a global reg type signal so there is no need to declare any outputs for the task.

The verilog code below shows the implementation of this example using the two different styles of task. We can also simulate this example on EDA playground.

// Global variable declaration
reg pulse;

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

// Task implementation with IO declared in body
task pulse_generate;
  input time pulse_time;
  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 cause an error when we tried to compile it.

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 simply assign values to any signals that we have access to either as inputs or as global variables.

We can include and drive as many signals 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 design.

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 hand 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);

Automatic Tasks in Verilog

We can also use the automatic keyword with verilog tasks in order to make them reentrant. Again, this keyword was introduced in the verilog 2001 standard meaning it can't be used with verilog 1995 compatible code.

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 normal 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 icarus verilog 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 icarus verilog 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. After it has been created, it is then assigned the value of 1.

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

Exercises

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

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.

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

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

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

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

Write the code for a task which returns the sum of 2 numbers. However, the result should be delayed by a fixed amount of time before it is returned. The task should take three inputs - one of time type which sets the delay time and the 2 integers that will be summed together. In addition, the task should have one output which is the result of the summation.

task add_delay();
  // Declare the task IO
  input time del;
  input integer a;
  input integer b;
  output integer c;
  begin
    // Functional code of the task
    #del c = a + b;
  end
endtask
Enjoyed this post? Why not share it with others.

Leave a Reply

Your email address will not be published. Required fields are marked *

Subscribe

Join our mailing list and be the first to hear about our latest FPGA tutorials
Sign Up to our Mailing List
© 2024 FPGA Tutorial

Sign up free for exclusive content.

Don't Miss Out

We are about to launch exclusive video content. Sign up to hear about it first.

Close
The fpgatutorial.com site logo

Don't Miss Out

We are about to launch exclusive video content. Sign up to hear about it first.

Close