I have written a VHDL "matrix" package with the aim of achieving Matlab-like functionality in VHDL. It uses VHDL-2008 package generics to support arbitrary data types and unconstrained arrays to support arbitrary matrix dimensions.
In short, my question is whether my approach (described below) is reasonable, or could it be done a better way? My main objective is to write the VHDL code as generically as possible (something roughly akin to a C++ class template
). I mostly want to better understand how to use the "new stuff" of VHDL-2008 (the matrix package is just a usage example - the matrix specifics are not so important).
Some Background...
I am aware that this exact functionality has been discussed for inclusion in the VHDL standard for at least a decade (see, for example, a brief mention in 2010 and some ongoing discussion in 2014). It seems like the source code was even publicly available at some point, but the only copy I can find now is attached to this Xilinx forum post. Unfortunately, this early draft doesn't use VHDL-2008 constructs (such as generic packages), but this was top of the list of Suggested Changes in 2014. I'm not sure whether this has been done yet - the official download link given in the user guide and the eda-twiki pages appears to be dead.
Related Discussions...
I have tried to do my homework, but web searches didn't bear much fruit:
- This SO question mentions the idea of using VHDL-2008 generic types on subprograms (but not packages) to mimic C++ function templates, but doesn't go into any detail.
- This SO question asks about using VHDL-2008 generic types on entities (but not packages) to mimic C++ class templates, but the simple use case was ultimately solved using ordinary generic constants.
- This SO question has some good detail about declaring generic constants (but not types) on packages.
- Chapter 1.6 of this book gives an example "complex_generic_pkg", which possibly covers much of what I need, but I found it difficult to follow.
Implementation
I have stripped out a lot of functionality to keep things short, but this code is complete and compiles under Modelsim 2020.1 (free Intel Starter Edition) as follows:
vcom -2008 matrix_base_pkg.vhd
vcom -2008 types_pkg.vhd
vcom -2008 matrix_pkg.vhd
matrix_base_pkg.vhd:
package matrix_base_pkg is
generic (
-- Matrix element type.
type Value_t
);
type Array_t is array(integer range<>) of Value_t;
type Matrix_t is array(integer range<>) of Array_t;
end package;
This just defines the basic storage type. I used array-of-arrays (but could also have used 2D arrays). The idea of this tiny package is that I want to make one unique instantiation per data type, so the internal Array_t
and Matrix_t
types are unique. (Multiple identical package instantiations would create "different" types in the eyes of the compiler).
I shoved a few example instantiations at the bottom of matrix_base_pkg.vhd
:
-- REAL
package real_matrix_base_pkg is new work.matrix_base_pkg
generic map(
Value_t => real
);
-- COMPLEX
library ieee;
use ieee.math_complex.all;
package complex_matrix_base_pkg is new work.matrix_base_pkg
generic map(
Value_t => complex
);
-- INTEGER
package integer_matrix_base_pkg is new work.matrix_base_pkg
generic map(
Value_t => integer
);
-- NATURAL
package natural_matrix_base_pkg is new work.matrix_base_pkg
generic map(
Value_t => natural
);
-- POSITIVE
package positive_matrix_base_pkg is new work.matrix_base_pkg
generic map(
Value_t => positive
);
-- BOOLEAN
package boolean_matrix_base_pkg is new work.matrix_base_pkg
generic map(
Value_t => boolean
);
For convenience, I defined some aliases and put them in types_pkg.vhd
:
use work.integer_matrix_base_pkg.all;
use work.natural_matrix_base_pkg.all;
use work.positive_matrix_base_pkg.all;
use work.real_matrix_base_pkg.all;
use work.boolean_matrix_base_pkg.all;
package types_pkg is
alias BooleanArray_t is work.boolean_matrix_base_pkg.Array_t;
alias BooleanMatrix_t is work.boolean_matrix_base_pkg.Matrix_t;
alias IntegerArray_t is work.integer_matrix_base_pkg.Array_t;
alias IntegerMatrix_t is work.integer_matrix_base_pkg.Matrix_t;
alias NaturalArray_t is work.natural_matrix_base_pkg.Array_t;
alias NaturalMatrix_t is work.natural_matrix_base_pkg.Matrix_t;
alias PositiveArray_t is work.positive_matrix_base_pkg.Array_t;
alias PositiveMatrix_t is work.positive_matrix_base_pkg.Matrix_t;
alias RealArray_t is work.real_matrix_base_pkg.Array_t;
alias RealMatrix_t is work.real_matrix_base_pkg.Matrix_t;
end package;
Up to this point, this may seem long-winded compared to just copy-pasting separate definitions for each data type. But the point is that we can now implement a lot of functionality once (and debug, document and maintain it once).
The bulk of the implementation is then in matrix_pkg.vhd
. I've deleted everything except "+" functions, since these are sufficient to demonstrate the general idea. I've left behind several unused package generics, such as "*" and "-":
use work.types_pkg.all;
package matrix_pkg is
generic (
-- Matrix storage "type".
package base_pkg is new work.matrix_base_pkg generic map(<>);
-- Note: For standard types, the compiler will find the functions below (due to "<>").
function "+"(a,b : base_pkg.Value_t) return base_pkg.Value_t is <>;
function "*"(a,b : base_pkg.Value_t) return base_pkg.Value_t is <>;
function "-"(a : base_pkg.Value_t) return base_pkg.Value_t is <>;
function "-"(a,b : base_pkg.Value_t) return base_pkg.Value_t is <>;
function to_string(x : base_pkg.Value_t) return string is <>
);
-- Shorthand aliases
alias Value_t is base_pkg.Value_t;
alias Array_t is base_pkg.Array_t;
alias Matrix_t is base_pkg.Matrix_t;
function "+"(a,b : Array_t) return Array_t;
function "+"(a,b : Matrix_t) return Matrix_t;
function "+"(a : Value_t; b : Array_t) return Array_t;
function "+"(a : Array_t; b : Value_t) return Array_t;
function "+"(a : Value_t; b : Matrix_t) return Matrix_t;
function "+"(a : Matrix_t; b : Value_t) return Matrix_t;
end package;
package body matrix_pkg is
function "+"(a,b : Array_t) return Array_t is
variable v : Array_t(a'range);
begin
for i in a'range loop
v(i) := a(i) + b(i);
end loop;
return v;
end function;
function "+"(a,b : Matrix_t) return Matrix_t is
variable v : Matrix_t(a'range)(a(a'low)'range);
begin
for i in a'range loop
for j in a(a'low)'range loop
v(i)(j) := a(i)(j) + b(i)(j);
end loop;
end loop;
return v;
end function;
function "+"(a : Value_t; b : Array_t) return Array_t is
variable v : Array_t(b'range);
begin
for i in b'range loop
v(i) := a + b(i);
end loop;
return v;
end function;
function "+"(a : Array_t; b : Value_t) return Array_t is
begin
return b + a;
end function;
function "+"(a : Value_t; b : Matrix_t) return Matrix_t is
variable v : Matrix_t(b'range)(b(b'low)'range);
begin
for i in b'range loop
for j in b(b'low)'range loop
v(i)(j) := a + b(i)(j);
end loop;
end loop;
return v;
end function;
function "+"(a : Matrix_t; b : Value_t) return Matrix_t is
begin
return b + a;
end function;
function "-"(a,b : Array_t) return Array_t is
variable v : Array_t(a'range);
begin
for i in a'range loop
v(i) := a(i) - b(i);
end loop;
return v;
end function;
end package body;
--------------------
-- Instantiations --
--------------------
package real_matrix_pkg is new work.matrix_pkg
generic map(
base_pkg => work.real_matrix_base_pkg
);
package integer_matrix_pkg is new work.matrix_pkg
generic map(
base_pkg => work.integer_matrix_base_pkg
);
package natural_matrix_pkg is new work.matrix_pkg
generic map(
base_pkg => work.natural_matrix_base_pkg
);
package positive_matrix_pkg is new work.matrix_pkg
generic map(
base_pkg => work.positive_matrix_base_pkg
);
Again, I shoved some example package instantiations at the end of the file (in reality, they might go somewhere else). I have lazily excluded a package instantiation for the ieee.math_complex.complex
type because that package doesn't include a to_string
function, so I need to provide one via the corresponding package generic.
The part I am most unsure about is the contents of generic ( ... )
. I'm not sure if this is well structured. However, it does seem nice that the compiler will automatically find the generic functions (like "+" and "-") if they exist. And, apparently, neither "=" nor "/=" need to be written at all.
Usage is pretty straightforward, for example:
use work.integer_matrix_pkg.all;
...
...
variable A : IntegerMatrix_t(0 to 27)(0 to 8);
...
...
A := A + 123;
...
...
etc.
Of course, many more Matlab-like functions can be implemented in reality (not just "+").