VHDL-2008 Packages
I have felt that the VHDL implementation of packages was perhaps clumsy, and unnatural by the standards and expectations of modern languages. This does of course overlook that purpose of VHDL is to describe the implementation of logic (a "hardware description language") rather than programme software. More recent developments in the language have been to "improve" the behavioural parts for verification rather than for synthesis. So what have we now got?
Package Inheritance
The plan is to create a 'base' package with a type and one function (or procedure) and then 'extend' it with an additional function (or procedure). The example here is chosen for simplicity of illustration and is hence a little contrived, go with it.
library ieee;
use ieee.std_logic_1164.all;
package base_pkg is
-- Vector width
constant width_c : positive := 8;
subtype slv_vector_t is std_logic_vector(width_c-1 downto 0);
-- Bitwise reverse a std_logic_vector
function reverse (v : slv_vector_t) return slv_vector_t;
end package;
package body base_pkg is
function reverse (v : slv_vector_t) return slv_vector_t is
variable ret : slv_vector_t;
begin
for i in v'range loop
ret(i) := v(v'high-i);
end loop;
return ret;
end function;
end package body;
This package defines a vector of a specified length and a 'reverse' operation on it.
package inherit_pkg1 is
package inst_pkg is new work.base_pkg;
-- Without the type alias, th function alias would need to include the package
-- alias reverse is inst_pkg.reverse [inst_pkg.slv_vector_t return inst_pkg.slv_vector_t];
alias slv_vector_t is inst_pkg.slv_vector_t;
alias reverse is inst_pkg.reverse[slv_vector_t return slv_vector_t];
-- Barrel shift a std_logic_vector
-- * s > 0 -> rotate left
-- * s < 0 -> rotate right
--
function shift (
v : inst_pkg.slv_vector_t;
s : integer := 1
) return inst_pkg.slv_vector_t;
end package;
package body inherit_pkg1 is
function shift (
v : inst_pkg.slv_vector_t;
s : integer := 1
) return inst_pkg.slv_vector_t is
variable ret : inst_pkg.slv_vector_t;
begin
for i in v'range loop
ret(i) := v((i - s) mod v'length);
end loop;
return ret;
end function;
end package body;
This package uses the 'base' package and defines a 'barrel shift' operation on it. There's no immediate utility in either packages except for illustration purposes. You'll note I avoid the gratuitous use of "use lib.package.all" so often seen in VHDL. Especially when there are many packages which are typically used only a few times, I like to retain the association between the type, function or procedure name and the package in which it is defined. This makes it easier to find the definition as you know what package name you are searching for. It also prevents name clashes when two different packages define a function with the same name and type usage that causes namespace clashes. Here I use the explicit long form in order to illustrate how the path names change with different situations.
Aside: I feel this coding style is the non-lazy alternative to placing absolutely everything in a context all the time whether you need it or not. The combination of never using a context plus avoiding use clauses for absolutely everything ends up with code having a short and simple library section. This aids copy and paste of code as it is obvious which libraries are required without searching through nested contexts, usually including the same stuff multiple times because we're human and therefore too lazy to use context the way I believe they were initially envisaged, i.e. not for boilerplate library sections. See OSVVM for good examples.
entity test_inherit_pkg is
end entity;
library ieee;
use ieee.std_logic_1164.std_ulogic;
architecture test1 of test_inherit_pkg is
signal v1 : work.inherit_pkg1.inst_pkg.slv_vector_t;
signal v2 : work.inherit_pkg1.inst_pkg.slv_vector_t;
signal v3 : work.inherit_pkg1.inst_pkg.slv_vector_t;
signal v4 : work.inherit_pkg1.inst_pkg.slv_vector_t;
begin
v1 <= "10110010";
v2 <= work.inherit_pkg1.shift(v1, 2);
v3 <= "10000010";
v4 <= work.inherit_pkg1.inst_pkg.reverse(v3);
end architecture;
As the package is not generic, we do not need to create a local instantiation of it and we can use it directly from library work as above. Later we will make this generic, and here it illustrates how the path names change.
library ieee;
use ieee.std_logic_1164.std_ulogic;
architecture test2 of test_inherit_pkg is
-- Create a new local package
package inst_pkg is new work.inherit_pkg1;
signal v1 : inst_pkg.inst_pkg.slv_vector_t;
signal v2 : inst_pkg.inst_pkg.slv_vector_t;
signal v3 : inst_pkg.inst_pkg.slv_vector_t;
signal v4 : inst_pkg.inst_pkg.slv_vector_t;
begin
v1 <= "10110010";
v2 <= inst_pkg.shift(v1, 2);
v3 <= "10000010";
v4 <= inst_pkg.inst_pkg.reverse(v3);
end architecture;
This simple test bench illustrates that the 'base' package is very much included in the second, rather than the second extending the base. We can create something of an illusion of package extension through the use of a few aliases as follows.
package inherit_pkg2 is
package inst_pkg is new work.base_pkg;
-- Without the type alias, th function alias would need to include the package
-- alias reverse is inst_pkg.reverse [inst_pkg.slv_vector_t return inst_pkg.slv_vector_t];
alias slv_vector_t is inst_pkg.slv_vector_t;
alias reverse is inst_pkg.reverse[slv_vector_t return slv_vector_t];
-- Barrel shift a std_logic_vector
-- * s > 0 -> rotate left
-- * s < 0 -> rotate right
--
function shift (
v : slv_vector_t;
s : integer := 1
) return slv_vector_t;
end package;
package body inherit_pkg2 is
function shift (
v : slv_vector_t;
s : integer := 1
) return slv_vector_t is
variable ret : slv_vector_t;
begin
for i in v'range loop
ret(i) := v((i - s) mod v'length);
end loop;
return ret;
end function;
end package body;
This solution uses an alias for each type, function or procedure in the 'base' package. Its a little more work to set up, but saves having to refer explicitly to the 'base' package instantiation inside second package, i.e. inst_pkg.inst_pkg.slv_vector_t becomes inst_pkg.slv_vector_t.
library ieee;
use ieee.std_logic_1164.std_ulogic;
architecture test3 of test_inherit_pkg is
-- Create a new local package
package inst_pkg is new work.inherit_pkg2;
signal v1 : inst_pkg.slv_vector_t;
signal v2 : inst_pkg.slv_vector_t;
signal v3 : inst_pkg.slv_vector_t;
signal v4 : inst_pkg.slv_vector_t;
begin
v1 <= "10110010";
v2 <= inst_pkg.shift(v1, 2);
v3 <= "10000010";
v4 <= inst_pkg.reverse(v3);
end architecture;
It now feels more like extension rather than inclusion.
library ieee;
use ieee.std_logic_1164.all;
package base_pkg is
-- Vector width
constant width_c : positive := 8;
subtype slv_vector_t is std_logic_vector(width_c-1 downto 0);
-- Bitwise reverse a std_logic_vector
function reverse (v : slv_vector_t) return slv_vector_t;
end package;
package body base_pkg is
function reverse (v : slv_vector_t) return slv_vector_t is
variable ret : slv_vector_t;
begin
for i in v'range loop
ret(i) := v(v'high-i);
end loop;
return ret;
end function;
end package body;
Now we can also add (slightly gratuitously) a generic to the package to increase its utility by allowing the type to scale in the number of bits.
package inherit_gpkg2 is
generic(width_g : positive := 8);
package inst_pkg is new work.base_gpkg
generic map (width_g => width_g);
-- Without the type alias, th function alias would need to include the package
-- alias reverse is inst_pkg.reverse [inst_pkg.slv_vector_t return inst_pkg.slv_vector_t];
alias slv_vector_t is inst_pkg.slv_vector_t;
alias reverse is inst_pkg.reverse[slv_vector_t return slv_vector_t];
-- Barrel shift a std_logic_vector
-- * s > 0 -> rotate left
-- * s < 0 -> rotate right
--
function shift (
v : slv_vector_t;
s : integer := 1
) return slv_vector_t;
end package;
package body inherit_gpkg2 is
function shift (
v : slv_vector_t;
s : integer := 1
) return slv_vector_t is
variable ret : slv_vector_t;
begin
for i in v'range loop
ret(i) := v((i - s) mod v'length);
end loop;
return ret;
end function;
end package body;
In this example the type is defined in the 'base' package, and the additional function defined does not need to use the generic, it is just passed through to the 'base' package. Now we can alter the width of the vectors and scale the operations on them. Clearly this example is contrived as scaling the operations could already be achieved using unconstrained vector types.
We can create a specific version of this package for all to use with the following code. Note this is perhaps more useful when the generic is instead a type like std_logic_vector or unsigned which might be re-used by many more projects, rather than an value for a vector width.
package inst_pkg is new work.inherit_gpkg2
generic map (width_g => 11);
use work.inst_pkg.all;
I personally prefer to keep all scopes tight, which in this case avoids littering the generally available packages with random 'stuff'. It also aids future support and maintainability by limiting what is dependent on a piece of code. Hence I prefer the locally instantiated package variation given below.
library ieee;
use ieee.std_logic_1164.std_ulogic;
architecture test4 of test_inherit_pkg is
-- Create a new local package
package inst_pkg is new work.inherit_gpkg2
generic map (width_g => 11);
signal v1 : inst_pkg.slv_vector_t;
signal v2 : inst_pkg.slv_vector_t;
signal v3 : inst_pkg.slv_vector_t;
signal v4 : inst_pkg.slv_vector_t;
begin
v1 <= "10110010010";
v2 <= inst_pkg.shift(v1, 2);
v3 <= "10000010100";
v4 <= inst_pkg.reverse(v3);
end architecture;
We can achieve something approaching package extension by including type, function and procedure aliases. The generic parameters on the package, which can include types too, might be considered something closer to 'templating' in modern OO languages. VHDL will always be limited by "what would Ada do?"
Protected Types
These have been designed to be precisely like classes. So how close do they get? I often find I need a Boolean shared between multiple processes, the old way to do this was a shared variable, but there were race conditions associated with using them those. The solution is protected types, which implement a 'monitor' on protected type 'method' calls in order to avoid two processes entering a critical region. Therefore the use of 'methods' in the protected type induce some notion of atomic actions. Anyway, I want a Boolean variable shared, and this is the replacement for shared variable bool : boolean;, so you can see its a bit more verbose for getting the basics up and running.
package prot_type_pkg is
type bool_pt is protected
procedure set(i : boolean);
impure function get return boolean;
end protected;
end package;
package body prot_type_pkg is
type bool_pt is protected body
variable bool : boolean := false;
procedure set(i : boolean) is
begin
bool := i;
end procedure;
impure function get return boolean is
begin
return bool;
end function;
end protected body;
end package body;
I firstly note the initial value of my shared variable is out of my control, it is set within the protected type and not at the time of instantiation of the shared variable. The set() method will need to be called after instantiation and at the start of the test bench's actions.
entity test_prot_type_pkg is
end entity;
architecture test of test_prot_type_pkg is
shared variable bool : work.prot_type_pkg.bool_pt;
begin
set_p : process
begin
-- Initialise at start if protectwed type's initial value is not as required.
bool.set(true);
while true loop
wait for 10 ns;
bool.set(true);
wait for 10 ns;
end loop;
end process;
unset_p : process
begin
bool.set(false);
wait for 20 ns;
end process;
end architecture;
There's no concept of inheritance with protected types. Using packages does not aid with extending VHDL's equivalent to a class. Okay, this is VHDL and based on Ada rather than a more modern OO software language. These are the limits.
Conclusions
Packages can include other packages to augment functionality, and can with a bit more work be made to appear as an extension. Protected types are intended to be the OO class equivalent in VHDL, and do provide OO encapsulation but do not provide inheritance.