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.
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>;
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.
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.
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>);
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
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.
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;
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.
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;
What is the main difference between a variable and a shared variable?show answer
Shared variables can be used in multiple process blocks whereas a variable can only be used in one process.hide answer
Write the code which declares an 8 bit std_logic_vector as a shared variable.show answer
shared variable example : std_logic_vector(7 downto 0);hide answer
What are the two separate parts we use to write a protected type? What do we use them for when writing a protected type?show answer
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.hide answer
Why do we use getters and setters in a protected type?show answer
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.hide answer
Write the code for a basic protected type which contains a single integer variable, a getter and a setter method.show answer
-- 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;
Leave a Reply