Using Protected Types and Shared Variables in VHDL

By John
May 30, 2020

In this post we look at shared variables and protected types in VHDL. These techniques allow us to incorporate aspects of object orientation into our code which helps us to write code which is more maintainable. At the end of the post there is a full example to show how we use the protected type in VHDL.

Many modern programming languages make use of object orientation. This is a programming approach which involves writing code to define a data structure and all operations which can be performed on it.

We use a single unit in our program to encapsulate the data structure and their associated operations. We normally refer to these closed units as being objects.

In languages such as C++ or Java, we use classes to define objects. These are a collection of different variables which determine the state of our object and a number of methods (or functions) which allow us to modify the state.

We then write more complex programs by instantiating a number of these objects and defining how they interact with each other.

Object oriented code has a number of advantages over other approaches, such as improved code reusability and maintainability, which can improve our productivity.

VHDL was not designed to be a primarily an object oriented language. However, the VHDL-2002 standard introduced a protected type which allows us to create object oriented style code.

In this post, we will discuss protected types in more detail. We will also look at the closely related concept of shared variables.

This post doesn't provide an extensive discussion of the concepts associated with object oriented programming. This is because it is not necessary to have a full understanding of these concepts in order to use the techniques we discuss here.

However, it is beneficial to have a full understanding of object oriented concepts and techniques in the long term. You can find a simple explanation of the terms and methods associated with object oriented coding techniques here or here.

VHDL Shared Variables

In a previous post we saw how we can declare a variable within a process block. The scope of any variables which we declare this way is limited to the process where they are declared.

Another type of variable was introduced as part of the VHDL-93 standard - shared variables. The VHDL-2008 standard further refined shared variables to make their usage safer.

Shared variables are exactly the same as normal variables in VHDL except that they can be used in more than one process. This means their value is always updated immediately after assignment.

The shared variable is particularly useful in modern testbenches, where we often create high level data structures which define test stimulus for the FPGA.

We then need to pass this test data to other processes in our test bench which can generate the inputs or check the outputs. We often use a shared variable for this purpose when we write testbenches in VHDL.

Unlike normal variables, we can declare shared variables in the architecture, as we would with a signal. This allows more than one process in our code to have access to them.

The code snippet below shows the syntax we use to declare shared variables. After we have declared our variables, we assign and read them in the exact same way as a normal variable.

shared variable <variable_name> : <type>;

Mutual Access

Although shared variables provide a useful function in VHDL, they also come with a downside.

As our processes can access the shared variable simultaneously, we may have instances where multiple processes attempt to modify data at the same time.

This leads to unpredictable behaviour in our code as we can't be certain our data will be modified as expected. As a result, the behaviour of our code is no longer deterministic.

To demonstrate how this can cause a problem, let's consider a basic example where a shared variable acts as a counter in our VHDL design.

Suppose that we have two processes which both try to increment the value of this shared variable at the same time.

This operation typically involves our CPU reading the variable from memory, updating the value and then writing the new data back to the original memory location.

If one process completes this process before the other has a chance to read the variable, then the code will work as expected.

However, a problem occurs when both processes read the variable before either writes the updated value into memory.

In this case, both processes will increment the original value by one and then write it back to the memory location.

Clearly we do not want this behavior as the variable is incremented by one rather than two.

VHDL Protected Type

In VHDL, the protected type is the language feature which most closely resembles the concept of objects. We use protected types in VHDL to implement the encapsulation of variables as well as the procedures and functions associated with them. In addition to this, they also ensure exclusive access to their data members.

The protected type was introduced in VHDL-2002 to overcome the problems which can occur with shared variables. The VHDL-2008 standard made it mandatory for all shared variables to be a protected type.

As protected types exhibit several properties of object orientation, they are useful for designing FPGA test benches. Making frequent use of them can help to improve the maintainability and reusability of our code.

In fact, the source code for the OSVVM library makes extensive use of protected types to implement its underlying functionality. This library is one of the most popular simulation tools in VHDL.

We use protected type to define a collection of variables and VHDL subprograms which we want to group together in order to model a given object in our VHDL code.

As with packages, we split the code for a protected type into two different parts.

We use the first part to declare the protected type and a separate protected body to implement our type.

Anything which we write in the declaration is the public interface to the protected type.

In contrast to this, any variables, functions or procedures we declare in the protected body are private. This means we can't access these variables and methods outside of the protected body unless.

We also implement any methods which we declared in the protected type declaration in the protected body. These methods are visible outside of the protected body.

We typically define protected types in packages, meaning we write the protected body inside the package body.

However, it is also possible to write protected types inside of architectures. In this case, we write the the protected body immediately after the declaration.

Writing a Protected Type in VHDL

When declaring a protected type in VHDL, we only need to declare the procedures and functions which we want to use externally. This is similar to the way we declare public object members in other programming languages.

The code snippet below shows the way in which we declare protected types.

type <type_name> is protected
  -- Definition of functions and procedures
end protected <type_name>;

We use the protected body to implement our procedures and functions using the same approach we have seen before in package bodies.We also use this to declare any variables we use in our protected type. We do this using the exact same method we have seen before.

The code snippet below shows the general syntax for a protected body.

type <type_name> is protected body
  -- Variable declarations

  -- Function and procedure implementation
end protected <type_name>;

Once we have written code to create our protected type, we can then create instances of it elsewhere in our VHDL code. To do this, we must create either a variable or shared variable which acts as an instance of the object.

The code snippet below shows the way we declare object instances. This code snippet also shows the syntax we use to call a method which is associated with our object.

-- Create an object instance as a variable
variable <variable_name> : <type_name>;

-- Create a object instance as a shared variable
shared variable <variable_name> : <type_name>;

-- Call one of the objects methods
<variable_name>.<method>(<arguments>);

Accessing Variables

Protected types use exclusive access to ensure that only one process can modify the underlying data at a time.

As a result of this, we can't directly access these variables from outside of the protected type. Instead, we must access or modify the variables in inside a protected type using a subprogram.

This means that we often need to include getters and setters in our code.

We use getters and setters to either get the value of some variable (getters) or set the value of a variable (setters).

These types of method are common in languages such as Java or C++ which make extensive use of object orientation.

In VHDL, we always use procedures to write setters whilst functions are normally used for getters.

The code snippet below shows an example of a setter.

procedure set_example_variable(set_value : integer) is
begin
  example_variable := set_value;
end procedure set_example_variable;

This is actually as complicated as a setter function will ever get. Here we would have already declared the "example_variable" as a shared variable within the VHDL protected body.

The code snippet below shows the code which would implement the getter method for the same variable. Again, this is as complicated as getter function will ever get.

impure function get_example_variable return integer is   begin
  return example_variable;
end function get_example_variable;

All subprograms defined in the protected type body have full access to internal variables. This means that there is no need to specify them as formal input parameters.

However, we do have to declare all functions in the protected type as impure if they access a shared variable. This is shown in the code snippet above which implements our example getter function

VHDL Protected Type Example

Let's consider a basic example in order to demonstrate how we can create object oriented code.

For this example, we will create an object which implements a basic synchronous counter.

As this is a trivial example, we could just as easily implement it in a process. However, it provides a way for us to look at the basic syntax we require to create a protected type in VHDL.

We will also see how we can use the object in our code after we have created it.

Writing Our VHDL Protected Type

When creating an object, the first thing we have to do is declare all the subprograms which are part of it. We will do this in a package which we will later use to declare our variables and implement our methods.

Our protected type will use two procedures, one which resets the counter and one which increments it by a given value. In addition, we will also declare one function which simply acts as a getter.

The VHDL code below shows how we declare the protected type with the required methods.

type counter_t is protected
  procedure reset;
  procedure increment (value : integer);
  impure function get_value return integer;
end protected counter_t;

Now that we have declared our protected type, we need to write the code which implements it.

We code the implementation of our protected type using a protected body. We must write the code for the protected body in the body of our package.

Our example also has one variable which we use to store the current value of our counter. We also need to declare this in our protected body.

The code snippet below shows the implementation of our protected body.

type counter_t is protected body

  -- Variable to store the counter value
  variable counter_val  : integer := 0;
 
  -- Implementation of our reset procedure
  -- This simply resets the counter to 0
  procedure reset is
  begin
    counter_val := 0;
  end procedure reset;

  -- Implementation of the increment function
  procedure increment (value : integer) is
  begin
    counter_val := counter_val + value;
  end procedure increment;

  --Implementation of the getter function
  impure function get_value return integer is
  begin
    return counter_val;
  end function get_value;
    
end protected body counter_t;

Using our Protected Type

After writing our protected type, we can create an instance of it to use in our code.

If we are going to use the object in more than one process, we need to create this instance outside of a process and declare it as a shared variable.

When we only use the object in one process, then we can simply create it inside of our process.

In either case, the process for declaring it is exactly the same as for other signals or variables.

For our example, we only use our object in one process. This means that we can simply declare it as if it were a normal variable.

Once we have declared our object, we can then access all of the functions and procedures associated with it.

For our example, the counter generates a short pulse whenever it is triggered by a clock.

We control the length of this pulse through a constant which we declare outside of the process. We then create a loop inside our process which increments the counter object every 1 ns. The loop stops when our counter value is equal the value of our constant.

The code snippet below shows the process which performs these tasks.

pulse_process:
process is
  variable count_object : counter_t;
begin
  -- Wait for a trigger event
  wait until rising_edge(trigger);

  -- Reset the counter before using it
  count_object.reset;
    
  -- Start generating a pulse
  -- The length of the pulse is determined by
  -- a constant outside the process
  pulse <= '1';
    
  while count_object.get_value < pulse_width loop
    count_object.increment(1);
    wait for 1 ns;
  end loop;
    
  pulse <= '0';

end process pulse_process;

This is a trivial example which will simply generate a single pulse of a given width every time it is triggered. However, this does show us the general methodology required for creating and using a protected type.

Complete Code

We have to split the code for this example over two files - one for the package and one for our functional code. The contents of both of these files are listed in full below.

However, you can also find these source files on eda playground where it is possible to simulate them.

The source code below shows the declaration of our protected type within a package.

package counter_example_pkg is
  type counter_t is protected
    procedure reset;
    procedure increment (value : integer);
    impure function get_value return integer;
  end protected counter_t;
end package counter_example_pkg;

The source code below shows the full package body for our protected type example.

Note that we would include this code in the same file as our package declaration above. We have split them up in this case as it makes them easier to read and understand.

package body counter_example_pkg is

  type counter_t is protected body
    -- Variable to store the counter value
    variable counter_val  : integer := 0;
 
    -- Implementation of our reset procedure
    -- This simply resets the counter to 0
    procedure reset is
    begin
      counter_val := 0;
    end procedure reset;

    -- Implementation of the increment function
    procedure increment (value : integer) is
    begin
      counter_val := counter_val + value;
    end procedure increment;

    --Implementation of the getter function
    impure function get_value return integer is
    begin
      return counter_val;
    end function get_value;
  end protected body counter_t;

end package body counter_example_pkg;

Finally, we would have a file which implements the actual functional code. This is the code which we would use to create instances of our object and is also the file we'd use for simulations. The source code below shows this functional code.

library ieee;
  use ieee.std_logic_1164.all;

library work;
  use work.counter_example_pkg.all;

entity counter_example is
end entity counter_example;
 
architecture behav of counter_example is

  -- Constant to control the pulse length
  constant pulse_width : integer := 100;

  -- Trigger and pulse signals
  signal trigger : std_logic := '0';
  signal pulse   : std_logic := '0';

begin

  -- Create a pulse train on the trigger signal
  trigger <= not trigger after 500 ns;

  pulse_process:
  process is
	variable count_object : counter_t;
  begin
    -- Wait for a trigger event
    wait until rising_edge(trigger);

    -- Reset the counter before using it
    count_object.reset;
    
    -- Start generating a pulse
    -- The length of the pulse is determined by
    -- a constant outside the process
    pulse <= '1';
    while count_object.get_value < pulse_width loop
      count_object.increment(1);
      wait for 1 ns;
    end loop;
    pulse <= '0';
  end process pulse_process;

end architecture behav;

Exercises

What is the main difference between a variable and a shared variable?

Shared variables can be used in multiple process blocks whereas a variable can only be used in one process.

Write the code which declares an 8 bit std_logic_vector as a shared variable.

shared variable example : std_logic_vector(7 downto 0);

What are the two separate parts we use to write a protected type? What do we use them for when writing a protected type?

We use the protected type declaration to declare all the subprograms which form the external interface to our protected type. The protected body is used to implement the subprograms and to declare variables which can't be externally accessed.

Why do we use getters and setters in a protected type?

We use getters and setters to access variables in our protected type. We have to do this as our variables are not directly accessible in order to ensure mutual access to them.

Write the code for a basic protected type which contains a single integer variable, a getter and a setter method.

-- Declare the interface ot the protected type
type example_t is protected
  procedure set_value (value : integer);
  impure function get_value return integer;
end protected example_t;
 
-- Implement the getters and setter code in the protected body
protected body example_t is
  -- Declare the integer type variable in the body
  variable example_int : integer;
 
  -- Implementation of the setter type
  procedure set_value(value : integer) is
  begin
    example_int := value;
  end procedure set_value;
 
  -- Implementation of the getter type
  impure function get_value return integer is
  begin
    return counter_val;
  end function get_value;
 
end protected body example_t;

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