Logic Design - Functions and Tasks [Verilog]

[Edit of Image1]

Introduction

Hey it's a me again @drifter1!

Today we continue with the Logic Design series on Verilog to cover some more advanced topics. More specifically, we will talk about Functions and Tasks, which when used carefully can improve the readability of our synthesizable Verilog RTL code.

So, without further ado, let's dive straight into it!


Functions

Functions in Verilog are similar to the functions in programming languages like C. They can be used in order to reduce the amount of lines of Verilog RTL. Repetitive sections of code may be placed within them. Those functions can then be called with different inputs each time, computations are performed and the result is returned.

Function Declaration Syntax

A function begins with the function keyword, followed by an optional automatic keyword, an return type and the function name, and ends with the endfunction keyword. The automatic keyword is used in recursive functions or cases where a function will be executed concurrently by more than one processes. It makes the items declared within the function dynamically allocate rather than being shared between the different invocations.

The inputs are declared after the function's name similar to how they are declared in modules. Thus, it's possible to define the inputs within parentheses like a port list after the function name or in a separate line, as shown below.

function [automatic] return_type function_name ([inputs]);
    // statements
endfunction

function [automatic] return_type function_name;
    input [inputs];
    //statements
endfunction

Similar to always blocks, the statements of a function should be enclosed within begin-end keywords, if there are more than one statements.

Function Result

When declaring a function, an internal variable with the same name as the function is also created implicitly. Hence, one cannot declare a variable with the same name as the function. The return value of the function is assigned to that exact internal variable, as shown below.

function_name = result;

It's important to note that the result (output) is assigned at the end of function execution. Thus, it's wise to put that statement at the end of the function.

Calling Functions

A function call is similar to how one would call functions in most programming languages. It's the name of the function followed by the inputs or parameters in parentheses, as shown below.

function_name ([parameters])

Such a call can be inserted into most expressions.

Function Rules

  • Functions cannot contain any time or delay related statements and are meant to be used for modeling combinational logic only.
  • They can have any number of inputs, but only return one result, which is driven to the internal variable with the same name as the function.
  • The port list of a function cannot have any output or inout port.
  • From within a functions's body only functions can be called and not tasks.
  • Variables declared within a function are local to that function.
  • Functions may take input or drive output to global variables.
  • A function can be defined within a different file, and be included elsewhere using the `include compiler directive.

Function Example

Let's implement the S1 calculation of SHA-256 as a function. From Wikipedia's SHA-256 pseudocode, we know that S1 is calculated as follows:

S1 := (e rightrotate 6) xor (e rightrotate 11) xor (e rightrotate 25)

where both S1 and e are 32-bit unsigned integers.

In Verilog, the function for that simple purpose is:

function [31 : 0] s1 (input e);
    s1 = {e[5  : 0], e[31 :  6]} ^
         {e[10 : 0], e[31 : 11]} ^
         {e[24 : 0], e[31 : 25]};
endfunction

Calling it in the section of the RTL where temp1 is to be calculated is also quite simple:

temp1 = h + s1(e) + ch + k[i] + w[i];

Of course this could be done differently. For example, by simply defining a wire or register with the name s1. But, that way the code is more reusable. These sha256-related utility functions may be declared in sha_utils.v and included within the "core" RTL code using:

`include "sha_utils.v"

Tasks

Tasks in Verilog are similar to procedures or subroutines which are used throughout programming languages. As a syntax goes, they are quite similar to functions. The two main differences are that tasks may drive more than one output and that they can include time-related statements.

Task Declaration Syntax

Tasks begins with the task keyword, followed by an optional automatic keyword and afterwards the task name. They end with the endtask keyword.

The port list is defined after the task name, again using two styles:

task [automatic] task_name ([port list]);
    // statements
endtask

task [automatic] task_name;
    input [inputs];
    inout [inouts];
    output [outputs];
    //statements
endtask

Task Types

The default task type is static, which makes all variables to be shared across the different invocations of the same task, if that task is executed concurrently by different processes.

When applying the automatic keyword a task becomes dynamic. Basically, in that case, the variables declared within the task are dynamically allocated rather than being shared between the different invocations.

Tasks may also be defined outside a module. Such tasks are called global and can be called from within any module.

Calling Tasks

Tasks are called with a statement by their name followed by the parameters (or port assignment) in parentheses:

task_name([port_list]);

They must be specifically called as statements and cannot be used within expressions like functions!

Task Rules

  • Tasks can include timing delays, such as posedge, negedge or the # delay. Thus, they can be used for modeling both combinational and sequential logic.
  • Tasks can have any number of inputs, inouts and outputs.
  • Variables declared within a task are local to that task.
  • Tasks may take input or drive output to global variables.
  • Tasks can call other tasks and functions.
  • Tasks must be called with a statement and cannot be used within expressions like functions.
  • A task can be defined within a different file, and be included elsewhere using the `include compiler directive.

Task Example

The S1 calculation function from before, can be turned into a task as follows:

task s1_calc (output s1, input e);
    s1 = {e[5  : 0], e[31 :  6]} ^
         {e[10 : 0], e[31 : 11]} ^
         {e[24 : 0], e[31 : 25]};
endtask

Calling it is now not as simple as before though. Let's drive the result to a wire with identifier s1. That way, calculating temp1 now looks like this:

s1_calc(s1, e);
temp1 = h + s1 + ch + k[i] + w[i];

Of course, using a task in that case makes no sense at all, but it gives you an idea of the syntax!


RESOURCES:

References

  1. http://www.asic-world.com/verilog/veritut.html
  2. https://www.chipverify.com/verilog/verilog-tutorial
  3. https://www.javatpoint.com/verilog

Images

  1. https://www.flickr.com/photos/creative_stock/5227842611

Block diagrams and other visualizations were made using draw.io


Previous articles of the series


Final words | Next up

And this is actually it for today's post!

Next time we will get into some more features of Verilog that haven't been covered yet...

See Ya!

Keep on drifting!

H2
H3
H4
3 columns
2 columns
1 column
Join the conversation now