Using Tasks and Functions in Verilog
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.