An Introduction to Functions in SystemVerilog

By John
July 23, 2021

In this post we look at functions and how we can use them to write SystemVerilog code which is reusable.

In the next post in the series, we talk about the tasks in SystemVerilog. Collectively, tasks and functions are known as subprograms.

As with most programming languages, we should try to make as much of our SystemVerilog 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.

Subprograms are one of the main tools which we use to write reusable code in SystemVerilog.

When we write a SystemVerilog function, it performs a calculation and returns up to one value. We can then call this function in multiple places in our code instead of having to replicate code.

SystemVerilog functions execute immediately, meaning that they can't contain time consuming constructs such as delays, posedge macros or wait statements.

When we need to write reusable code which consumes time, we should instead make use of a SystemVerilog task.

In the rest of this post, we will discuss the use of SystemVerilog functions in more detail.

As the behavior of functions is largely inherited from verilog, you can skip much of this post if you are already familiar with verilog.

However, you may still wish to read the sections on automatic variables and passing parameters by reference as these are new features in SystemVerilog.

SystemVerilog Functions

In SystemVerilog, 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 can write the code for function inside a module, class or package in SystemVerilog.

The code snippet below shows the general syntax we use to write a function.

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

// 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. This allows us to write more concise code.

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

If needed, we can also use this field to assign default values to our inputs. We do this by assigning the argument a value as part of the declaration.

When we assign a default value to an argument, we can call the function without passing data to this argument.

We use the <return_type> field to declare which SystemVerilog data type the function returns.

When we want our function to return data, we use the return keyword as shown in the example code below. We can also simulate this example on EDA playground.

function int easy_example (input int a);
  return a;
endfunction : easy_example

In some instances, we may wish to write a function which doesn't return a value. We can do this by by replacing the <return_type> field with the void keyword when we declare the function.

For example, we may wish to write a function which simply prints the current time in the simulation but doesn't return any data. The code snippet below shows how we would use the void keyword to write this function.

function void print_time();
  $display("Simulation time = %0t", $time);
endfunction : print_time

Rules for Using a Function in SystemVerilog

Although functions are often fairly simple, there are a few basic rules which we must follow when we write a SystemVerilog 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 SystemVerilog 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 SystemVerilog functions.

When we write functions in SystemVerilog, we can declare and use local variables. This means that we can declare variables in a function which can't be accessed in other parts of our design.

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

For example, if we declare a function within a module block then our function can access and modify all of the variables declared in that module.

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

Rules for Using Functions in SystemVerilog
SystemVerilog functions can have one or more input arguments
Functions can return at most one value
We use the void keyword as the return type in functions which don't return a 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 function

SystemVerilog Function Example

To better demonstrate how to use a SystemVerilog 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 SystemVerilog int types for the input arguments and the return types.

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

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

As we have previously discussed, we can use two methods to declare SystemVerilog functions. The code snippet below shows both of these methods.

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

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

We can also simulate this example on EDA playground.

Calling a Function in SystemVerilog

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

There are actually two different methods which we can use to call a function in SystemVerilog. The difference between the two methods is the way that we pass parameters to the function.

The first method which we can use is known as positional association. When we use this approach, we pass parameters to the function in the same order as we declared them.

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 SystemVerilog function using positional association
func_out = addition(a, b);

The second method we can use to pass paramters to a function is known as named association.

When we use named association in SystemVerilog, we explicitly define the name of the argument we are passing data to. Unlike positional association, the order in which we declare the arguments is not important.

The code snippet below shows how we would use named 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 SystemVerilog function using positional association
func_out = addition(.in_a (a), .in_b (b));

Automatic Functions in SystemVerilog

We can also use the SystemVerilog automatic keyword to declare a function as reentrant, as shown in the code snippet below.

function automatic <return_type> <name> (input <arguments>); 
  // Declaration of local variables
  // Function code
endfunction : <name>

When we declare a function as reentrant, the variables and arguments which we declare in the function will be dynamically allocated.

In contrast, normal functions use static allocation for internal variables and arguments. This means that all of the memory which is required to perform the processing of the function is allocated only once at the start of the simulation.

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 any memory which is allocated to the function will never be deallocated. As a result of this, any values stored in this memory will maintain their value between calls to the function.

In contrast, our simulator allocates memory to automatic functions whenever we call the function. Once our function has finished executing, this memory will be deallocated.

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

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

Automatic Variables inside a Function

We can also use the automatic keyword to create variables inside of a SystemVerilog function, as is shown in the code snippet below.

// General syntax to declare an automatic variable in systemverilog
automatic <type_name> <size> <variable_name>;

When we do this, we create variables which use dynamic memory allocation. As a result of this, these variables have a much shorter lifetime than normal variables.

As with functions, when we declare a variable inside a function it uses static memory allocation.

This means that our simulator allocates memory once at the start of our simulation. In addition, this memory will not be deallocated while our simulation is executing.

As a result of this, when we write data to this variable it will be stored until our simulation finishes executing or we write new data to it.

In addition to this, any values stored in this memory will maintain their value between calls to the function.

In contrast to this, when we create an automatic variable in SystemVerilog it uses dynamic memory allocation.

This means that our simulator allocates memory to store the variable whenever we call the function.

Once the function has finished executing, this memory is then deallocated again.

We can see from this that the difference between static and automatic variables is their lifetime.

When we declare a static variable, we are telling our tools that we want it to exist for the entire simulation.

In contrast, when we declare a dynamic variable we are telling our tools that we want to limit the lifetime so that it only exists for as long as our function is executing.

In SystemVerilog, we can declare and use static variables in both static and automatic functions or tasks.

Likewise, we can declare and use automatic variables in both static and automatic functions.

Automatic Variable Example

To better demonstarte the difference between static and automatic variables let's consider a basic exmaple.

For this example, we will write a function which contains two local variables that we increment every time we call the function.

However, one of these variables will be a static variable whilst the other will be an automatic variable.

The SystemVerilog code below shows how this example function is writen.

  function void auto_example();
    automatic int auto_var = 0;
    int static_var = 0;
    auto_var++;
    static_var++;
    $display("Automatic variable = %0d", auto_var);
    $display("Static variable = %0d", static_var);
  endfunction : auto_example

We can then simulate this in EDA playground by writing a loop which calls the function three times. The console output below shows the result of this simulation.

Automatic variable = 1
Static variable = 1
Automatic variable = 1
Static variable = 2
Automatic variable = 1
Static variable = 3

We can see from this how the static variable is incremented from 1 to 3 in this simulation. This shows how our simulator retains the value of this variable even after the function has finished execution.

In contrast, we can see that the automatic variable remains at the value of 1 throughout the simulation.

The reason for this is that our simulator allocates memory to the automatic variable every time we call the function. Once the function finishes execution, this memory is then deallocated and the value of the variable is lost.

This simple example shows how the two different types of variable have different lifetimes in SystemVerilog.

Passing Parameters by Reference

In the section on calling a function in SystemVerilog, we saw how we pass parameters to a function when we call it.

By default, all of our parameters are passed to the function by value when we call a function in SystemVerilog. This means that the value of each parameter is copied into the function.

As a result, we can modify these values and the changes will not be visible outside of the function.

However, we can also pass parameters to a SystemVerilog function by reference. When we use this method, we are effectively passing a pointer to the data rather than the actual data itself.

When we pass a parameter by reference, we don't create a local copy of the data inside of the function.

Instead, we pass a memory address which tells the function where it can find the data.

Any changes which we make in the function affect the data in this memory location. As a result of this, any changes we make to the data are also visible to the rest of our program.

The SystemVerilog code below shows the general syntax we use to declare functions which pass data by reference.

function automatic <return_type> <name> (ref <argument>); 
  // function code
endfunction : <name>

When we want to pass a value by reference, we use the ref keyword in front of the argument rather than the input keyword.

We use the ref keyword for each argument which we want to pass by reference. This means that we can use a mixture of passing by value and passing by reference in SystemVerilog.

In the above example, we use an automatic function rather than a static function. The reason for this is that we can only pass parameters by reference to functions which we declare as automatic in SystemVerilog.

Passing by Reference Example

In order to better understand how passing by reference works in SystemVerilog, let's consider a simple example.

In this example, we will write a function which takes two arguments and increments their value by one.

We will pass one of the function parameters by reference and one by value. After incrementing the values, our function will exit without returning any data.

The code snippet below shows how we would write this function in SystemVerilog.

  function automatic void ref_vs_value (ref a, input b);
    a++;
    b++;
  endfunction

We can then use the code below to run a simple simulation which demonstrates how our function affects the two arguments differently.

  initial begin
    $display("Before function call a = %0d, b = %0d", a, b);
    ref_vs_value(a, b);
    $display("After function call a = %0d, b = %0d", a, b);
  end

Running this example on EDA playground results in the output shown below.

Before function call a = 1, b = 1
After function call a = 2, b = 1

As we can see from this example, our function modifies the value of the parameter when we pass data by reference.

As we discussed, this is because we are passing a memory location to the function and any changes to the memory are visible to the rest of our program.

In contrast, our function does not affect the value of the parameter when we pass data to the function by value.

Again, we expect this behavior as the data is copied into the function and changes to it are not visible in the rest of our program.

Exercises

Which type of assignment can we use inside of a SystemVerilog function?

We can only use blocking assignment inside a function.

Which keyword to we use when we write a function that doesn't return a value.

We use the void keyword instead of specifying the return type.

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

Normal SystemVerilog functions use static memory allocation whereas automatic functions use dynamic memory allocation

What is the difference between passing a parameter by value and by reference?

When we pass a parameter by value, it is copied into the function and any modifications are not visible in the rest of our design. When we pass a parameter by reference, our function gets a pointer to the data and changes to the data are visible in the rest of our design.

Write the code for a function which takes 3 int arguments and returns the product of them. You can write this function using either the pass by reference or pass by value method.

// Using pass by value
function int product(input int a, b, c);
  return a * b * c;
endfunction : product

// Using pass by reference
function int product(ref int a, b, c);
  return a * b * c;
endfunction : product

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