3

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 "+").

Harry
  • 884
  • 1
  • 8
  • 19
  • *In short, my question is whether my approach (described below) is reasonable, or could it be done a better way?* Do you have a specific programming question? Consider investigating synthesis vendor support for generic types and package instantiation before getting too involved. An alternative might be authoring a package generator or (a) template(s) for a smart editor. Type REAL isn't synthesis eligible. –  Dec 29 '20 at 05:42
  • Is your goal synthesis or simulation? Synth tools generally have less feature support than simulators as many constructs may not be appropriate for synthesis. And some features take a long time to see support from anyone due to lack of demand. For example- packages as generics has only seen support in Aldec 11.0 (2019), where otherwise it has had good 2008 support. Your example also seems a lot of work to make one language behave like another, when time may be better spent using the language using its own strengths. – Tricky Dec 29 '20 at 08:37
  • @user1155120 The programming question is really two-fold: (i) Does the VHDL language support what I am trying to do, or is there some fundamental limitation that will cause it to fail in more complex scenarios and (ii) Is my specific approach well structured - in the way that the official VHDL matrix packages might be structured if/when they are released. (I'm not worried about vendor support - see my response to Tricky). – Harry Dec 29 '20 at 09:46
  • @user1155120 I use type REAL very extensively in synthesizable VHDL code - for example, to calculate low-level parameters based on high-level generics, or to accurately calculate values stored in LUTs (prior to conversion to fixed point). – Harry Dec 29 '20 at 09:46
  • @Tricky I am not interested in synthesis or simulation, only the capabilities of the VHDL language. There is a circular problem if developers only use features of the language that are supported by tools, and tool vendors only add support for features that are under demand. – Harry Dec 29 '20 at 09:46
  • @Tricky I agree with your comment about playing to the strengths of each language. However, it looks like this functionality will eventually be provided natively by the VHDL language. I have done some quite big test designs (using my hacked attempt at mimicking this functionality) and found it to be extremely powerful and easy to use. Large systems of design equations can be solved directly in VHDL instead of having to switch between languages (and tool chains) for every parameter change. To be honest, VHDL-2008 seems to be a lot more powerful than I thought it was. – Harry Dec 29 '20 at 10:00
  • Have you looked into using protected types? They provide some oo like functionality and we're greatly improved with vhdl2019 allowing generics on the type and removing alot of restrictions. See [here](https://vhdlwhiz.com/vhdl-2019/) for a good 2019 overview. Aldec will have a few key features supported with their v12.0 release. – Tricky Dec 29 '20 at 11:15
  • @Tricky I have only looked superficially at protected types. Thanks for the tip and for the link - a really nice, concise overview. VHDL-2019 looks amazing - even some of the very "minor" tweaks look like significant quality-of-life improvements for the developer. I just hope tool vendors will someday see the value in better language support. – Harry Dec 29 '20 at 11:45
  • Have you read Ashenden's book? In VHDL-2008 when you pass a type as a generic, the package will not know it is an array type. Hence you have to pass what you need, but this also includes how to iterate the type (such as looping an array or indexing a matrix). Ashenden's book probably has some insight as to how to structure your problem to do this. VHDL-2019 has new syntax allow us to work with array types as generics. – Jim Lewis Dec 29 '20 at 19:46
  • @JimLewis I have read some of the book, but haven't found that part yet. However, I think I did see what you're referring to in two SO posts. [This one](https://stackoverflow.com/questions/40595461/how-to-pass-an-array-type-as-generic-type-parameter-to-a-vhdl-package) is what led me to define the array types in a (tiny) generic package, then create *unique* package instances per element type (to avoid type conflicts). And in [this one](https://stackoverflow.com/questions/43324918/vhdl-function-procedure-for-any-type-of-array), I think Paebbels' answer mentions the exact changes you refer to. – Harry Dec 29 '20 at 21:42
  • @JimLewis It looks like VHDL-2019 really solves this whole problem in an elegant way. My "matrix package" above is a rather ugly hack, basically because I couldn't find neat workarounds to these VHDL-2008 limitations. The future is bright (pending tool support)! – Harry Dec 29 '20 at 21:42
  • I think you almost have it. Copy the contents of matrix_base_pkg into matrix_pkg. Remove matrix_base_pkg as a generic from matrix_pkg. Update types package. And I think you have it. – Jim Lewis Dec 30 '20 at 02:35
  • @JimLewis I actually tried that first, but it presented a problem if I wanted to use any non-template array/matrix type as an input or output of any function in the package. For example, a Matlab-like equality comparison A = B should return a boolean matrix. Or, similarly, Matlab-like matrix indexing should take an integer array/matrix of indices as input (as well as the matrix to be indexed). If the array types are defined inside matrix_pkg, then those specific template instantiations of matrix_pkg don't exist yet (and I couldn't see any way to "forward declare" them, as one might in C++). – Harry Dec 30 '20 at 04:05
  • @JimLewis To clarify, this is the equality comparison function prototype: `function "="(a,b : Matrix_t) return BooleanMatrix_t;`. I somehow need the compiler to know what `BooleanMatrix_t` is, regardless of what the template element type `Value_t` is. Perhaps I missed a trick here. – Harry Dec 30 '20 at 04:14
  • Function "="(a,b : Matrix_t) return BooleanMatrix_t;` doesn't match the definition of an operator overload. IEEE Std 1076-2008 9.2.3 Relational operators "The result type of each ordinary relational operator (=, /=, <, <=, >, and >=) is the predefined type BOOLEAN. ... Two composite values of the same type are equal if and only if for each element of the left operand there is a matching element of the right operand and vice versa, and the values of matching elements are equal, as given by the predefined equality operator for the element type." Perhaps an identifier designated function instead? –  Dec 30 '20 at 09:34
  • (This is where Tricky's comment came from.) "Your example also seems a lot of work to make one language behave like another, when time may be better spent using the language using its own strengths." Non Boolean relational operators seems a bit of a stretch for a language change. –  Dec 30 '20 at 09:35
  • @user1155120 That's fine, it's just a matter of renaming: "=" can be changed to return BOOLEAN instead of `BooleanMatrix_t`. In Matlab, this function is called `isequal`, so my prototype looked like this: `function isequal(a,b : Matrix_t) return boolean;`. However, even after `isequal` is renamed to `"="`, I still want to achieve the functionality of the version that returns BooleanMatrix_t. – Harry Dec 30 '20 at 10:09
  • @user1155120 This per-element relational operator is only one example. Another example is that I need to index matrices (of arbitrary element type) using matrices of indices (of integer element type). In other packages (not related to matrices), the same construct may be needed for other reasons. – Harry Dec 30 '20 at 10:13
  • @user1155120 I didn't quite follow your "... a bit of a stretch for a language change" comment. Changing languages (between VHDL and Matlab) is exactly what I am trying to avoid. In my test design, I was able to eliminate dependency on Matlab by solving all the design equations directly in VHDL. That means a customer buying my IP core can just change one top-level generic and the design is completely restructured automatically. – Harry Dec 30 '20 at 10:20
  • Continuing from my previous comment. The Function "="(a,b : Matrix_t) return BooleanMatrix_t;` would also need to be defined in the generic package that creates Matrix_t. BooleanMatrix_t would be defined in a separate package and referenced by use clause by matrix_pkg. That way each instance uses the same type for BooleanMatrix_t. – Jim Lewis Jan 05 '21 at 05:05

0 Answers0