Chapter 1. Enhanced Generics

We start our tour of the new features in VHDL-2008 with one of the most significant changes in the language, enhanced generics. All earlier versions of VHDL since VHDL-87 have included generic constants, which are interface constants for design entities and components. They are widely used in models to represent timing parameters and to control the widths of vector ports. When we instantiate an entity or component, we supply values for the generic constants for that instance. The generic constants in the generic list are called the formal generics, and the values we supply in the generic map are called the actual generics. Most of the time, generic constants are referred to just as “generics,” since the only kind of generics are constants.

In VHDL-2008, generics are enhanced in several significant ways. First, we can declare generic types, subprograms, and packages, as well as generic constants. Second, we can declare generics on packages and subprograms, as well as on entities and components. The rationale for extending generics in these ways is to increase productivity by allowing us to declare reusable entities, packages, and subprograms that deal with different types of data and that can be specialized to perform different actions. In this chapter, we will describe each of the new kinds of generics and the new places in which we can declare generics.

Generic Types

Generic types allow us to define a type that can be used for ports and internal declarations of an entity, but without specifying the particular type. When we instantiate the entity, we specify the actual type to be used for that instance. As we will see later, generic types can also be specified for packages and subprograms, not just for entities and components.

We can declare a formal generic type in a generic list in the following way:

type identifier

The identifier is the name of the formal generic type, and can be used within the rest of the entity in the same way as a normally declared type. When we instantiate the entity, we specify a subtype as the actual generic type. This can take the form of a type name, a type name followed by a constraint, or a subtype attribute.

Example 1.1. A generic multiplexer

A multiplexer selects between two data inputs and copies the value of the selected input to the output. The behavior of the multiplexer is independent of the type of data on the inputs and output. So we can use a formal generic type to represent the type of the data. The entity declaration is:

entity generic_mux2 is
  generic ( type data_type  );
  port    ( sel : in bit; a, b : in data_type;
            z : out data_type );
end entity generic_mux2 ;

The name data_type is the formal generic type that stands for some type, as yet unspecified, used for the data inputs a and b and for the data output z. An architecture body for the multiplexer is:

architecture rtl of mux2 is
begin
  z  <= a when sel = '0' else b;
end architecture rtl;

The assignment statement simply copies the value of either a or b to the output z. It is sensitive to all of the inputs. So whenever a,b, or sel change, the assignment will be re-evaluated. In any instance of the multiplexer, changes on a and b are determined using the predefined equality operator for the actual type in that instance.

We can instantiate the entity to get a multiplexer for bit signals as follows:

signal sel_bit, a_bit, b_bit, z_bit : bit;
. . .

bit_mux : entity work.generic_mux2(rtl)
  generic map   ( data_type => bit )
  port map      ( sel => sel_bit, a => a_bit, b => b_bit,
                  z => z_bit );

Similarly, we can instantiate the same entity to get a multiplexer for signals of other types, including user-defined types.

type msg_packet is record
  src, dst  : unsigned(7 downto 0) ;
  pkt_type  : bit_vector(2 downto 0);
  length    : unsigned(4 downto 0);
  payload   : byte_vector(0 to 31);
  checksum  : unsigned(7 downto 0);
end record  msg_packet;
signal pkt_sel : bit;
signal pkt_in1, pkt_in2, pkt_out : msg_pkt;
. . .

pkt_mux : entity work.generic_mux2(rt1)
  generic map ( data_type => msg_packet )
  port map    ( sel => pkt_sel,
                a => pkt_in1, b => pkt_in2, z => pkt_out );

VHDL-2008 defines a number of rules covering formal generic types and the ways they can be used. The formal generic type name can potentially represent any constrained type, except a file type or a protected type. The entity can only assume that operations available for all such types are applicable, namely: assignment; allocation using new; type qualification and type conversion; and equality and inequality operations. The formal generic type cannot be used as the type of a file element or an attribute. Moreover, it can only be used as the type of an explicitly declared constant or a signal (including a port) if the actual type is not an access type and does not contain a subelement of an access type. For signals, the predefined equality operator of the actual type is used for driver update and event detection.

If we have a formal generic type T, we can use it to declare signals, variables, and constants within the entity and architecture, and we can write signal and variable assignments for objects of the type. For example, the following shows signals declared using T:

signal s1, s2 : T;
. . .

s1 <= s2 after 10 ns;

and the following shows variables declared using T:

variable v1, v2, temp : T;
. . .

temp := v1;  v1 := v2;  v2 := temp;

Since signal and variable declarations require constrained subtypes, the actual type provided in an instance must be a constrained type if the formal type is used in this way. If the actual type is not constrained, an error occurs in the instantiation. If the formal generic type is not used in any way requiring it to be constrained, then the actual type in an instance need not be constrained.

For both variables and signals, the default initial value is determined using the actual type in an instance, using the normal rules for the actual type. Thus, if the actual type is a scalar type, the default initial value is the leftmost value of the type, and if the actual type is a composite type, the default initial value is an aggregate of the default initial values for the respective element types.

Declaring constants of a formal generic type might at first seem impossible, since we can’t specify an initial value if we don’t know the actual type. However, we can use the formal generic type to declare a formal generic constant, and then use that within the entity, for example:

entity e is
  generic ( type T; constant init_val : T );
  port    ( . . . );
end entity e;

architecture a of e is
begin
  p : process is
    variable v : T := init_val;
  begin
    . . .
  end process p;
end architecture a;

The actual value for the generic constant is provided when the entity is instantiated, and must be of the type specified as the actual generic type. For example, we might instantiate the entity e within a larger design as follows:

my_e : entity work.e(a)
  generic map ( T => std_ulogic_vector(3 downto 0),
                init_val => "ZZZZ" );

We can also use this technique to provide values for initializing variables and signals declared to be of the formal generic type. Note that the generic list in this entity makes use of one generic (T) in the declaration of another generic (init_val). This was illegal in previous versions of VHDL, but is now legal in VHDL-2008 (see Section 9.1).

One thing that we cannot do with formal generic types is apply operations that are not defined for all types. For example, we cannot use the “+” operator to add to values of a formal generic type, since the actual type in an instance may not be a numeric type. Similarly, we cannot perform array indexing, or apply most attributes. This may at first seem an onerous restriction, but it does mean that a VHDL analyzer can check the entity and architecture for correctness in isolation, independently of any particular instantiation. It also means we don’t get any surprises when we subsequently analyze an instance of the entity. Fortunately, as we will see in Section 1.5, there are ways of providing operations to an instance for use on values of the actual type.

Example 1.2. Illegal use of formal generic types

Suppose we want to define a generic counter that can be used to count values of types such as integer, unsigned, signed, and so on. We can declare the entity as follows:

entity generic_counter is
  generic ( type    count_type;
            constant reset_value : count_type );
  port    ( clk, reset : in  bit;
            data       : out count_type );
end entity genenic_counter;

We might then try to define an architecture as:

architecture rtl of generic_counter is
begin
  count : process (clk) is
  begin
    if rising_edge(clk) then
      if reset = '1' then
        data <= reset_value;
      else
        data <= data + 1;  -- Illegal
      end if;
    end if;
  end process count;
end architecture rtl;

The problem is that the “+” operator to add 1 to a value is not defined for all types that might be supplied as actual types. Hence, the analyzer will indicate an error in the expression where the operator is applied. To illustrate why this should be an error, suppose some time after the entity and architecture have been written, we try to instantiate them in a design as follows:

type traffic_light_color is (red, yellow, green);
. . .

cycle_lights : entity work.generic_counter(rtl)
  generic map ( count_type  => traffic_light_color,
                reset_value => red )
  port map    ( . . . );

The process in the instance would have to apply the “+” operator to a value of the actual generic type, in this case, traffic_light_color. That application would fail, since there is no such operator defined. We will revise this example in Section 1.5 to show how to supply such an operator to the instance.

Note in passing that the process in this example reads the value of the out-mode parameter data in an expression. While this was illegal in earlier versions of VHDL, it is legal in VHDL-2008 (see Section 6.3).

When we declare a generic constant in a generic list, we can specify a default value that is used if no actual value is provided in an instance. For generic types, there is no means of specifying a default type. That means that we must always specify an actual type in an instance. Since the type of objects in VHDL is considered to be a very important property, the language designers decided to insist on the actual type being explicitly specified.

Generic Lists in Packages

One of the new places in which we can write generic lists in VHDL-2008 is in package declarations. A package with a generic list takes the form:

package identifier is
  generic ( . . . ) ;

   . . . -- declarations within the package

end package identifier;

The package body corresponding to such a package is unchanged; we don’t repeat the generic list there. Within the generic list, we can declare formal generic constants and formal generic types, just as we can in a generic list of an entity or component. We can then use those formal generics in the declarations within the package.

A package with a generic list is called an uninstantiated package. Unlike a simple package with no generic list, we cannot refer to the declarations in an uninstantiated package with selected names or use clauses. Instead, the uninstantiated package serves as a form of template that we must instantiate separately. We make an instance with a package instantiation of the form:

package identifier is new uninstantiated_package_name
  generic map ( . . . );

The identifier is the name for the package instance, and the generic map supplies actual generics for the formal generics defined by the uninstantiated package. If all of the formal generics have defaults, we can omit the generic map to imply use of the defaults. (As we mentioned in Section 1.1, if any of the formal generics is a generic type, it cannot have a default. In that case, we could not omit the generic map in the package instance.) Once we have instantiated the package, we can then refer to names declared within it with selected names and use clauses with the instance name as the prefix.

For now, we will assume that the uninstantiated package and the package instance are declared as design units and stored in a design library. We will refine this assumption in Section 1.3.

Example 1.3. A package for stacks of data

We can write a package that defines a data type and operations for fixed-sized stacks of data. A given stack has a specified capacity and stores data of a specified type. The capacity and type are specified as formal generics of the package, as follows:

package generic_stacks is
  generic ( size : positive; type element_type );
  type stack_array is array (O to size-1) of element_type;
  type stack_type is record
    SP   : integer range 0 to size-1;
    store : stack_array;
  end record stack_type;

  procedure push (s : inout stack_type; e : in element_type);
  procedure pop  (s : inout stack_type; e : out element_type);

end package generic_stacks;

The corresponding package body is:

package body generic_stacks is

  procedure push (s : inout stack_type; e : in element_type) is
  begin
    s.store(s.SP) := e;
    s.SP := (s.SP + 1) mod size;
  end procedure push;

  procedure pop (s : inout stack_type; e : out element_type) is
  begin
    s.SP := (s.SP - 1) mod size;
    e := s.store(s.SP);
  end procedure pop;

end package body generic_stacks;

The uninstantiated package defines types stack_array and stack_type for representing stacks, and operations to push and pop elements. The formal generic constant size is used to determine the size of the array for storing elements, and the formal generic type element_type is the type of elements to be stored, pushed and popped.

We cannot refer to items in this uninstantiated package directly, since there is no specification of the actual size and element type. Thus, for example, we cannot write the following:

use work.generic_stacks.all ;  -- Illegal
. . .
variable my_stack : work.generic_stacks.stack_type;  -- Illegal

Instead, we must instantiate the package and provide actual generics for that instance. For example, we might declare the following as a design unit for a CPU design:

library IEEE; use IEEE.numeric_std.all ;
package address_stacks is new work.generic_stacks
  generic map ( size => 8,
                element_type => unsigned(23 downto 0) );

If we analyze this instantiation into our working library, we can refer to it in other design units, for example:

architecture behavior of CPU is
  use work.address_stacks.all ;
  . . .
begin
  interpret_instructions : process is
    variable return_address_stack : stack_type;
    variable PC : unsigned(23 downto 0);
    . . .
  begin
    . . .
    case opcode is
      when jsb =>     push(return_address_stack, PC);
                   PC <= jump_target;
      when ret => pop(return_address_stack, PC);
      . . .
    end case;
    . . .
  end process interpret_instructions;
end architecture behavior;

This architecture includes a use clause that makes names declared in the package instance address_stacks visible. References to stack_type, push and pop in the architecture thus refer to the declarations in the address_stacks package instance.

We can declare multiple instances of a given uninstantiated package, each with different actual generics. The packages instances are distinct, even though they declare similarly named items internally. For example, we might declare two instances of the generic_stacks package from Example 1.3 as follows:

package address_stacks is new work.generic_stacks
  generic map ( size => 8,
                element_type => unsigned(23 downto 0) );

package operand_stacks is new work.generic_stacks
  generic map ( size => 16, element_type => real );

If we then wrote a use clause in a design unit:

use work.address_stacks.all, work.operand_stacks.all ;

the names from the two package instances would all be ambiguous. This is an application of the existing rule in VHDL that, if two packages declare the same name and both are “used,” we cannot refer to the simple name, since it is ambiguous. Instead, we need to use selected names to distinguish between the versions declared in the two package instances. So, for example, we could write:

use work.address_stacks, work.operand_stacks;

to make the package names visible without prefixing them with the library name work, and then declare variables and use operations as follows:

variable return_address_stack : address_stacks.stack;
variable PC                   : unsigned(23 downto 0);
variable FP_operand_stack     : operand_stacks.stack;
variable TOS_operand          : real;
. . .
address_stacks.push(return_address_stack, PC);
operand_stacks.pop(FP_operand_stack, TOS_operand);

An important aspect of VHDL’s strong-typing philosophy is that two types introduced by two separate type declarations are considered to be distinct, even if they are structurally the same. Thus the two types declared as

type T1 is array (1 to 10) of integer;
type T2 is array (1 to 10) of integer;

are distinct, and we cannot assign a value of type T1 to an object of type T2. This same principle applies to formal generic types. Within an entity or a package that declares a formal generic type, that type is considered to be distinct from every other type, including other formal generic types. So, for example, we cannot assign a value declared to be of one formal generic type to an object declared to be of another formal generic type.

The fact that two formal generic types are distinct can lead to interesting situations when the actual types provided are the same (or are subtypes of the same base type). Ambiguity can arise between overloaded operations declared using the formal generic types. This kind of situation is not likely to happen in common use cases, but it is worth exploring to demonstrate the way overloading works in the presence of formal generic types.

Suppose we declare a package with two formal generic types, as follows:

package generic_pkg is
  generic ( type T1; type T2 );

  procedure proc ( × : T1 );
  procedure proc ( × : T2 );
  procedure proc ( × : bit );

end package generic_pkg;

Within the package, T1 and T2 are distinct from each other and from the type bit, so the procedure proc is overloaded three times. The uninstantiated package can be analyzed without error. If we instantiate the package as follows:

package integer_boolean_pkg is new work.generic_pkg
  generic map ( T1 => integer, T2 => boolean );

we can successfully resolve the overloading for the following three calls to procedures in the package instance:

work.integer_boolean_pkg.proc(3);
work.integer_boolean_pkg.proc(false);
work.integer_boolean_pkg.proc('1'),

On the other hand, if we instantiate the package as

package integer_bit_pkg is new work.generic_pkg
  generic map ( T1 => integer, T2 => bit );

the following call is ambiguous:

work.integer_bit_pkg.proc('1'),

It could be a call to the second or third of the three overloaded versions of proc in the package instance. Similarly, if we instantiate the package as

package integer_integer_pkg is new work.generic_pkg
  generic map ( T1 => integer, T2 => integer );

the following call is ambiguous:

work.integer_integer_pkg.proc(3);

This could be a call to the first or second of the three overloaded versions of proc. The point to gain from these examples is that overload resolution depends on the actual types denoted by the formal generic types in the instances. Depending on the actual types, calls to overloaded subprograms may be resolvable for some instances and ambiguous for others.

The final aspect of packages with generic lists is that we can also include a generic map in a package, following the generic list. Such a package is called a generic-mapped package, and has the form

package identifier is
  generic ( . . . );
  generic map ( . . . );

  . . .  -- declarations within the package

end package identifier;

The generic list defines the generics, and the generic map aspect provides actual values and type for those generics. While VHDL-2008 allows us to write a generic-mapped package explicitly, we would not normally do so. Rather, the feature is included in the language as a definitional aid. An instantiation of an uninstantiated package is defined in terms of an equivalent generic-mapped package that is a copy of the uninstantiated package, together with the generic map from the instantiation. This is analogous to the way in which an entity instantiation is defined in terms of a block statement that merges the generic and port lists of the entity with the generic map and port map of the instantiation. Since generic-mapped packages are not a feature intended for regular use, we won’t dwell on them further. We simply mention them here to raise awareness, since the occasional error message from an analyzer might hint at them.

Local Packages

In earlier versions of VHDL, packages can only be declared as design units. They are separately analyzed into a design library, and can be referenced by any other design unit that names the library. Thus, they are globally visible. In VHDL-2008, packages can also be declared locally within the declarative region of an entity, architecture, block, process, subprogram, protected type body, or enclosing package. This allows the visibility of the package to be contained to just the enclosing declarative region. Moreover, since declarations written in a package body are not visible outside the package, we can use local packages to provide controlled access to locally declared items.

Example 1.4. Sequential item numbering

Suppose we need to generate test cases in a design, with each test case having a unique identification number. We can declare a package locally within a stimulus-generator process. The package encapsulates a variable that tracks the next identification number to be assigned, and provides an operation to yield the next number. The process outline is:

stim_gen : process is

  package ID_manager is
    impure function get_ID return natural ;
  end package ID_manager;

  package body ID_manager is
    variable next_ID : natural := 0;
    impure function get_ID return natural is
      variable result : natural;
    begin
      result := next_ID;
      next_ID := next_ID + 1;
      return result;
    end function get_ID;
  end package body ID_manager;
  . . .

begin
  . . .
  test_case.ID := ID_manager.get_ID;
  ID_manager.next_ID := 0;  -- Illegal
  . . .
end process stim_gen;

The variable next_ID is declared in the package body, and so is not visible outside the package. The only way to access it is using the get_ID function provided by the package declaration. This is shown in the first assignment statement within the process body. The package name is used as a prefix in the selected name for the function. The second assignment statement is illegal, since the variable is not visible at that point. The package provides a measure of safety against inadvertent corruption of the data state.

We can write use clauses for locally declared packages. Thus, we could follow the package declaration in this example with the use clause

use ID_manager.all;

and then rewrite the assignment in the process as

test_case.ID := get_ID;

By writing the package locally within the process, it is only available in the process. Thus, we have achieved greater separation of concerns than had we written the package as a design unit, making it globally visible. Moreover, since the package is local to a process, there can be no concurrent access by multiple processes. Thus, the encapsulated variable can be an ordinary non-shared variable. If the package were declared as a global design unit, there could be concurrent calls to the get_ID function. As a consequence, the variable would have to be declared as a shared variable of a protected type. This would significantly complicate the design.

As Example 1.4 illustrates, if a package declared within a declarative region requires a body, then the body must come after the package declaration in the same region. If the enclosing region is itself a package, then we write the inner package declaration within the enclosing package declaration, and the inner package body within the outer package body. If the inner package requires a body, then the outer package requires a body as a consequence.

A locally declared package need not be just a simple package. It can be an uninstantiated package with a generic list (or, indeed, a generic-mapped package with both generic list and generic map). In that case, we must instantiate the package so that we can refer to items in the instance. The same rules apply to locally declared uninstantiated packages and instances as apply to globally declared packages.

Example 1.5. Package for wrapping items with item numbers

We can revise the package from Example 1.4 to make it deal with test cases of generic type, and to wrap each test case in a record together with a unique ID number. The numbers are unique across test cases of all types. We achieve this by keeping the previous package as an outer package encapsulating the next_ID variable. Within that package, we declare an uninstantiated package for wrapping test cases. The process outline containing the packages is:

stim_gen : process is

  package ID_manager is

    package ID_wrappers is
      generic ( type test_case_type );
      type wrapped_test_case is record
        test_case : test_case_type;
        ID        : natural ;
      end record wrapped_test_case;
      impure function wrap_test_case
                         ( test_case : test_case_type )
                         return wrapped_test_case;
    end package ID_wrappers;

  end package ID_manager;

  package body ID_manager is

    variable next_ID : natural := 0;

    package body ID_wrappers is
      impure function wrap_test_case
                         ( test_case : test_case_type )
                         return wrapped_test_case is
        variable result : wrapped_test_case;
      begin
        result.test_case := test_case;
        result.ID := next_ID;
        next_ID := next_ID + 1;
        return result;
      end function wrap_test_case;
    end package body ID_wrappers;

  end package body ID_manager;

  use ID_manager.ID_wrappers;
  package word_wrappers is new ID_wrappers
    generic map ( test_case_type => unsigned(32 downto 0) );
  package real_wrappers is new ID_wrappers
    generic map ( test_case_type => real );

  variable next_word_test : word_wrappers.wrapped_test_case;
  variable next_real_test : real_wrappers.wrapped_test_case;

begin
  . . .
  next_word_test := word_wrappers.wrap_test_case(X"0440CF00");
  next_real_test := real_wrappers.wrap_test_case(3.14159);
  . . .
end process stim_gen;

The process declares two instances of the uninstantiated package ID_wrappers, one for a test-case type of unsigned, and another for a test-case type of real. The process then refers to the wrapped_test_case type and the wrap_test_case function declared in each instance.

Example 1.5 exposes a number of important points about packages. First, a package declared within an enclosing region is just another declared item, and is subject to the normal scope and visibility rules. In the example, the ID_wrappers package is declared within an enclosing package, and so can be referred to with a selected name and made visible by a use clause.

Second, in the case of package instantiations, any name referenced within the uninstantiated package keeps its meaning in each instance. In the example, the name next_ID referenced within the uninstantiated package ID_wrappers, refers to the variable declared in the ID_manager package. So, within each of the package instances, word_wrappers and real_wrappers, the same variable is referenced. Importantly, had the process also declared an item called next_ID outside the packages but before the instances, that name would not be “captured” by the instances. They still refer to the same variable nested within the ID_manager package. The only exception to this rule for interpreting names is that the name of the uninstantiated package itself, when referenced within the package, is interpreted in an instance as a reference to the instance. This allows us to use an expanded name for an item declared within the uninstantiated package, and to have it interpreted appropriately in the instance. The rules for name interpretation illustrate quite definitely that package instantiation is different in semantics from file inclusion, as is used for C header files. The benefit of the VHDL-2008 approach is that names always retain the meaning they are given at the point of declaration, and so we avoid unwanted surprises.

The third point is that local instantiation of an uninstantiated package is a common use case, whether the uninstantiated package be locally declared, as in the example, or globally declared as a design unit. The advantage of local instantiation is that it allows use of a locally declared type as the actual for a formal generic type. Were local instantiation not possible, the actual type would have to be declared in a global package in order to use it in a global package instantiation. Thus, local instantiation improves modularity and information hiding in a design.

Example 1.6. Local stack package instantiation

In Example 1.3, we declared an uninstantiated package for stacks as a design unit. We can instantiate the package to deal with stacks of a type declared locally within a subprogram that performs a depth-first search of a directed acyclic graph (DAG) consisting of vertices and edges, as follows:

subprogram analyze_network ( network : network_type ) is

  type vertex_type is . . .;
  type edge_type is . . . ;
  constant max_diameter : positive := 30;

  package vertex_stacks is new work.generic_stacks
    generic map ( size => max_diameter,
                  element_type => vertex_type );
  use vertext_stacks.all ;

  variable current_vertex    : vertex_type;
  variable pending_vertices : stack_type;

begin
  . . .
  push(pending_stacks, current_vertex);
  . . .
end subprogram analyze_network;

The data types used to represent the DAG for analyzing a network are the local concern of the subprogram. By instantiating the generic_stacks package locally, there is no need to expose the data types outside the subprogram.

Generic Lists in Subprograms

The second new place in which we can write generic lists in VHDL-2008 is in subprogram (procedure and function) declarations. A procedure with a generic list takes the form:

procedure identifier
  generic   (. . .)
  parameter ( . . . ) is
  . . .  -- declarations
begin
  . . .  -- statements
end procedure identifier;

Similarly, a function with a generic list takes the form:

function  identifier
  generic   (. . .)
  parameter (. . .) return  result_type is
  . . .  -- declarations
begin
  . . .  -- statements
end function identifier;

We use terminology analogous to that for packages to refer to subprograms with generics. Thus, a subprogram with a generic list is called an uninstantiated subprogram. Note that the new keyword parameter is included to make the demarcation between the generic list and the parameter list clear. For backward compatibility, including the keyword is optional. We expect that designers will omit it for subprograms without generics and include it or not as a matter of taste for uninstantiated subprograms.

VHDL allows us to declare a subprogram in two parts, one consisting just of the specification, and the other consisting of the specification together with the body. We can separate a subprogram in this way within a given declarative part, for example, in order to declare mutually recursive subprograms. In the case of subprograms declared in packages, we are required to separate the subprogram specification into the package declaration and to repeat the specification together with the subprogram body in the package body. In the case of uninstantiated subprograms, the generic list is part of the subprogram specification. Thus, if we separate the declaration, we must write the generic list and parameter list in the specification, and then repeat both together with the body. Using a text editor to copy and paste the specification into the body makes this easy.

We cannot call an uninstantiated subprogram directly. We can think of it as a template that we must instantiate with a subprogram instantiation to get a real subprogram that we can call. For a procedure, the instantiation is of the form:

procedure  identifier  is new  uninstantiated_procedure_name
  generic map ( . . . );

and for a function, the instantiation is of the form

function  identifier  is new  uninstantiated_function_name
  generic map ( . . . );

In both cases, the identifier is the name for the subprogram instance, and the generic map supplies actual generics for the formal generics defined by the uninstantiated subprogram. If all of the formal generics have defaults, we can omit the generic map to imply use of the defaults. Once we have instantiated the subprogram, we can then use the instance name to call the instance.

Example 1.7. Generic swap procedure

The way in which we swap the values of two variables does not depend on the types of the variables. Hence, we can write a swap procedure with the type as a formal generic, as follows:

procedure swap
  generic   ( type T )
  parameter ( a, b : inout T ) is
  variable temp : T;
begin
  temp := a; a := b; b := temp;
end procedure swap;

We can now instantiate the procedure to get versions for various types:

procedure int_swap is new swap
  generic map ( T => integer );
procedure vec_swap is new swap
  generic map ( T => bit_vector(0 to 7) );

and call them to swap values of variables:

variable a_int, b_int : integer;
variable a_vec, b_vec : bit_vector(0 to 7);
. . .

int_swap(a_int, b_int);
vec_swap(a_vec, b_vec);

We can’t just call the swap procedure directly, as follows:

swap(a_int, b_int); -- Illegal

since it is an uninstantiated procedure. Note also that we can’t instantiate the swap procedure with an unconstrained type as the actual generic type, since the procedure internally uses the type to declare a variable. Thus, the following would produce an error:

procedure string_swap is new swap generic map ( T => string );

since there is no specification of the index bounds for the variable temp declared within swap.

Example 1.8. Setup timing check procedure

Suppose we are developing a package of generic operations for timing checks on signals. We include a generic procedure that determines whether a signal meets a setup time constraint. The package declaration is:

package timing_pkg is
  procedure check_setup
    generic ( type signal_type;
              type clk_type; clk_active_value : clk_type;
              T_su : delay_length )
    ( signal s : signal_type; signal clk : clk_type );
  . . .
end package timing_pkg;

The package body contains a body for the procedure:

package body timing_pkg is
  procedure check_setup
    generic ( type signal_type;
              type clk_type; clk_active_value : clk_type;
              T_su : delay_length )
    ( signal s : signal_type; signal clk : clk_type ) is
  begin
    if clk'event and clk = clk_active_value then
      assert s'last_event >= T_su
        report "Setup time violation" severity error;
    end if;
  end procedure check_setup;
  . . .
end package body timing_pkg;

We can now instantiate the procedure to get versions that check the constraint for signals of different types and for different setup time parameters:

use work.timing_pkg.al1;
procedure check_normal_setup is new check_setup
  generic map  ( signal_type => std_ulogic,
                 clk_type => std_ulogic,
                 clk_active_value => '1',
                 T_su => 200ps );
procedure check_normal_setup is new check_setup
  generic map  ( signal_type => std_ulogic_vector,
                 clk_type => std-ulogic,
                 clk_active_value => '1',
                 T_su => 200ps );
procedure check_long_setup is new check_setup
  generic map  ( signal_type => std_ulogic_vector,
                 clk_type => std_ulogic,
                 clk_active_value => '1',
                 T_su => 300ps );

Note that the procedure check_normal_setup is now overloaded, once for a std_ulogic parameter and once for a std_ulogic_vector parameter. We can apply these functions to signals of std_ulogic and std_ulogic_vector types, as follows:

signal status : std_ulogic;
signal data_in, result : std_ulogic_vector(23 downto 0);
. . .

check_normal_setup(status, clk);
check_normal_setup(result, clk);
check_long_setup(data_in, clk);
. . .

In each case, the active value for the clock signal and the setup time interval value are bound into the definition of the procedure instance. We do not need to provide the values as separate parameters.

VHDL-2008 allows us to declare uninstantiated subprograms and to instantiate them in most places where we can currently declare simple subprograms. That includes declaring uninstantiated subprograms as methods of protected types, and declaring instances of subprograms as methods. Since most reasonable use cases for doing this involve use of generic action procedures, we will defer further consideration to Section 1.5, where we introduce generic subprograms.

VHDL allows us to overload subprograms, and uses the parameter and result type profiles to distinguish among them based on the types of parameters in a call. Where we need to name a subprogram other than in a call, we can write a signature to indicate which overloaded version we mean. The signature lists the parameter types and, for functions, the return type, all enclosed in square brackets. This information is sufficient to distinguish one version of an overloaded subprogram from other versions. We can use a signature in attribute specifications, attribute names, and alias declarations. Subprogram instantiations, introduced in VHDL-2008, are a further place in which we name a subprogram. If the uninstantiated subprogram is overloaded, we can include a signature in an instantiation to indicate which uninstantiated version we mean. In such cases, the uninstantiated subprograms typically have one or more parameters of a formal generic type. We use the formal generic type name in the signature. For example, if we have two uninstantiated subprograms declared as

procedure combine
  generic   ( type T )
  parameter ( x : T; value : bit );
procedure combine
  generic   ( type T )
  parameter ( x : T; value : integer );

the procedure name combine is overloaded. We can use a signature in an instantiation as follows:

procedure combine_vec_with_bit is new combine[T, bit]
  generic map ( T => bit_vector );

VHDL-2008 specifies that a formal generic type name of an uninstantiated subprogram is made visible within a signature in an instantiation of the subprogram. Thus, in this example, the signature distinguishes between the two uninstantiated subprograms, since only one of them has a profile with Tfor the first parameter and bit for the second. The T in the signature refers to the formal generic type for that version of the subprogram.

As with packages, we can also include a generic map in a subprogram, following the generic list. Such a subprogram is called a generic-mapped subprogram. A generic-mapped procedure has the form

procedure identifier
  generic (. . .)
  generic map (. . .)
  parameter ( . . . ) is
  . . .  -- declarations
begin
  . . . -- statements
end procedure identifier;

and a generic-mapped function has the form

function  identifier
  generic (. . .)
  generic map (. . .)
  parameter (. . .) return  result_type is
  . . .  -- declarations
begin
  . . .  -- statements
end function identifier;

The generic list defines the generics, and the generic map aspect provides actual values and type for those generics. Like generic-mapped packages, we would not normally write a generic-mapped subprogram explicitly, since the feature is included in the language as a definitional aid. Hence, we won’t dwell on them further, but simply mention them here to raise awareness in case an analyzer produces a seemingly cryptic error message.

Generic Subprograms

As well as generic constants and types, VHDL-2008 allows us to declare generic subprograms. We declare a formal generic subprogram in a generic list, representing some subprogram yet to be specified, and include calls to the formal generic subprogram within the unit that has the generic list. When we instantiate the unit, we supply an actual subprogram for that instance. Each call to the formal generic subprogram represents a call to the actual subprogram in the instance. The way we declare a formal generic subprogram is to write a subprogram specification in the generic list. The specification must be for a simple subprogram; that is, the subprogram must not contain a generic list itself.

We will illustrate formal generic subprograms with a number of examples based on typical use cases. One important use case is to supply an operation for use with a formal generic type declared in the same generic list as the subprogram. Recall, from our discussion in Section 1.1, that the only operations we can assume for a formal generic type are those defined for all actual types, such as assignment, equality and inequality. We can use a formal generic subprogram to explicitly provide further operations.

Example 1.9. Supplying an operator for use with a formal generic type

In Example 1.2, we attempted to define a counter that could count with a variety of types. However, our attempt failed because we could not use the “+” operator to increment the count value. We can rectify this by declaring a formal generic function for incrementing the count value:

entity generic_counter is
  generic ( type    count_type;
            constant reset_value : count_type;
            function increment ( x : count_type )
                               return count_type );
  port ( clk, reset : in  bit;
         data  : out count_type );
end entity generic_counter;

We can then use the increment function in the architecture:

architecture rtl of generic_counter is
begin
  count : process (clk) is
  begin
    if rising_edge(clk) then
      if reset = '1' then
        data <= reset_value;
      else
        data <= increment(data);
      end if;
    end if;
  end process count;
end architecture rtl ;

Having revised the counter in this way, we can instantiate it with various types. For example, to create a counter for unsigned values, we define a function, addl, to increment using the “+” operator on unsigned values and provide it as the actual for the increment generic.

use IEEE.numeric_std.all ;
function add1 ( arg : unsigned ) return unsigned is
begin
  return arg + 1;
end function add1;

signal clk, reset : bit;
signal count_val  : unsigned(15 downto 0);
. . .

counter : entity work.generic_counter(rtl)
  generic map ( count_type  => unsigned(15 downto 0),
                reset_value => (others => '0'),
                increment  => add1 )  -- addl is the
                                      -- actual function
  port map ( clk => clk, reset => reset, data => count_val );

In the instance, we specify a subtype of unsigned as the actual type for the formal generic type count_type. That subtype is then used as the subtype of the formal generic constant reset_value in the instance, so the actual value is a vector of 16 elements. The subtype is also used for the parameters of the formal generic function increment in the instance, so we must provide an actual function with a matching profile. The addl function meets that requirement, since it has unsigned as its parameter and result type. Within the instance, whenever the process calls the increment function, the actual function addl is called.

We can instantiate the same entity to create a counter for the traffic_light_colour type defined in Example 1.2. Again, we define a function, next_color, to increment a value of the type, and provide the function as the actual for the increment generic.

type traffic_light_color is (red, yellow, green);
function next_color ( arg  : traffic_light_color )
                    return traffic_light_color is
begin
  if arg = traffic_light_color'high then
    return traffic_light_color'1ow;
  else
    return traffic_light_color'succ(arg);
  end if;
end function next_color;
signal east_light : traffic_light_color;
. . .

east_counter : work.genenic_counter(rtl)
  generic map ( count_type  => traffic_light_color,
                reset_value => red,
                increment   => next_color ) -- next_color is the
                                            -- actual function
  port map ( clk => clk, reset => reset, data => east_light );

When we declare a formal generic subprogram in a generic list, we can specify a default subprogram that is to be used in an instance if no actual generic subprogram is provided. The declaration is of the form

generic list ( . . . ;
               subprogram_specification is subprogram_name;
               . . . );

The subprogram that we name must be visible at that point. It might be declared before the uninstantiated unit, or it can be another formal generic subprogram declared earlier in the same generic list. In the case of an uninstantiated package, we cannot name a subprogram declared in the package as a default subprogram, since items declared within the package are not visible before they are declared.

Example 1.10. Error reporting in a package

Suppose we are developing a package defining operations to be used in a design and need to report errors that arise while performing operations. We can declare a formal generic procedure in the package to allow separate specification of the error-reporting action. We can also declare a default procedure that simply issues a report message. We need to declare the default action procedure separately from the package so that we can name it in the generic list. We will declare it in a utility package:

package error_utility_pkg is
  procedure report_error ( report_string   : string;
                           report_severity : severity_level );
end package error_utility_pkg;
package body error_utility_pkg is
  procedure report_error ( report_string   : string;
                           report_severity : severity_level ) is
  begin
    report report_string severity report_severity;
  end procedure report_error;
end package body error_utility_pkg;

We can now declare the operations package:

package operations is
  generic ( procedure error_action
              ( report_string   : string;
                report_severity : seven'ty_level )
              is work.error_utility_pkg.report_error );
  procedure step1 ( . . . );
  . . .

end package operations;

package body operations is

  procedure step1 ( . . . ) is
  begin
    . . .
    if something_is_wrong then
      error_action("Something is wrong in step1", error);
    end if;
    . . .
  end procedure step1;
  . . .

end package body operations;

If issuing a report message is sufficient for a given design, it can instantiate the operations package without providing an actual generic subprogram:

package reporting_operations is new work.operations;
use reporting_operations.all ;
. . .

step1 ( . . . );

If something goes wrong during execution of step1 in this instance, the call to error_action results in a call to the default generic subprogram report_error defined in the utility package. Another design might need to log error messages to a file. The design can declare a procedure to deal with error messages as follows:

use std.textio.all;
file log_file : text open write_mode is "error.log";
procedure log_error ( report_string   : string;
                      report_severity : severity_level ) is
  variable L : line;
begin
  write(L, severity_level'image(report_severity);
  write(L, string'(":");
  write(L, report_string) ;
  writeline(log_file, L);
end procedure log_error;

The design can then instantiate the operations package with this procedure as the actual generic procedure:

package logging_operations is new work. operations
  generic map   (  error_action => log_error  );
use logging_operations .all ;
. . .

stepl ( . . . );

In this instance, when something goes wrong in step1, the call to error_action results in a call to the procedure log_error, which writes the error details to the log file. Since the actual procedure is declared in the context of the instantiating design, it has access to items declared in that context, including the file object log_file. By providing this procedure as the actual generic procedure to the package instance, the instance is able to “import” that context via the actual procedure.

In many use cases where an operation is required for a formal generic type, there may be an overloaded version of the operation defined for the actual generic type at the point of instantiation. VHDL-2008 provides a way to indicate that the default for a generic subprogram is a subprogram, directly visible at the point of instantiation, with the same name as the formal generic subprogram and a matching profile. We use the box symbol (“<>”) in place of a default subprogram name in the generic declaration. For example, we might write the following in a generic list of a package:

function minimum ( L, R : T ) return T is <>

If, when we instantiate the package, we omit an actual generic function, and there is a visible function named minimum with the required profile, then that function is used. Normally, the parameter type T used in the declaration of the formal generic subprogram is itself a formal generic type declared earlier in the generic list. We provide an actual type for T in the instance, and that determines the parameter type expected for the visible default subprogram. If we define the formal generic subprogram with the same name and similar profile to a predefined operation, we can often rely on a predefined operation being visible and appropriate for use as the default subprogram. We will illustrate this with an example.

Example 1.11. Dictionaries implemented as binary search trees

The following package defines an abstract data type for dictionaries implemented as binary search trees. A dictionary contains elements that are each identified by a key value. The formal generic function key_of determines the key for a given element. No default function is provided, so we must supply an actual function on instantiation of the package. The formal function “<” is used to compare key values. The default function is specified using the “<>” notation, so if an appropriate function named “<” is directly visible at the point of instantiation, we don’t need to specify an actual function.

package dictionaries is
  generic ( type element_type;
            type key_type    ;
            function key_of ( E : element_type )
                            return key_type;
            function "<"  ( L, R  : key_type  )
                         return boolean is <> ) ;

  type  dictionary_type;

  -- tree_record and structure of dictionary_type are private
  type tree_record  is record
    left_subtree, right_subtree : dictionary_type;
    element :  element_type;
  end record tree_record;
  type dictionary_type  is access tree_record ;

  procedure lookup ( dictionary : in  dictionary_type;
                     lookup_key : in  key_type;
                     element : out element_type;
                     found  : out boolean ) ;

  procedure search_and_insert ( dictionary : in  dictionary_type;
                                element   : in  element_type;
                                already_present : out boolean ) ;

end package dictionaries ;

The package body is shown below, with the body of the search_and_insert procedure omitted for brevity.

package body dictionaries is

  procedure lookup ( dictionary  : in  dictionary_type;
                     lookup_key  : in  key_type;
                     element   : out element_type;
                     found   : out boolean ) is
    variable current_subtree : dictionary_type := dictionary;
  begin
    found := false;
    while current_subtree /= null loop
      if lookup_key < key_of ( current_subtree. element ) then
        lookup ( current_subtree. left_subtree, lookup_key,
                 element, found );
      elsif key_of( current_subtree.element ) < lookup_key then
        lookup ( current_subtree. right_subtree, lookup_key,
                 element,   found );
      else
        found := true;
        element := current_subtree.elernent;
        return;
      end if;
    end loop;
  end procedure lookup;

  procedure search_and_insert ( dictionary : in dictionary-type;
                                element   : in element-type;
                              already-present : out boolean ) is
    . . .

end package body dictionaries ;

In the function lookup, we use the formal generic function key_of to get the key for a candidate element in the dictionary. We compare the key with the value of the lookup_key parameter using the formal generic function "<".

Suppose we require a dictionary of test patterns that use time values as keys. We can instantiate the dictionaries package using our test-pattern type as the actual for element_type and time as the actual for key_type. We need to declare a function to get the time key for a test pattern:

type test_pattern_type is . . .;

function test_id_of ( test_pattern : in test_pattern_type )
                    return time is
begin
  return . . . ;
end function test_id_of;

We don’t need to define a function for use as the actual for the formal generic function "<". Since the predefined function "<". operating on time values is directly visible at the point of instantiation, it can be used implicitly as the actual function. As a result, the test patterns will be sorted into ascending order of time in the dictionary. We can write the package instantiation as:

package test_pattern_dictionaries is new work. dictionaries
  generic map ( element_type => test_pattern_type,
                key_type => time,
                key_of => test_id_of );

We can then call the operations defined in the instance:

use test_pattern_dictionaries.all;
variable test_set : dictionary_type;
variable generated_test, sought_test : test_pattern_type;
variable was_present : boolean;
. . .

search_and_insert ( test_set, generated_test, was_present );
assert not was-present
  report "Test at " & time ' image(test_id_of(generated-test))
                    & " previously generated";
. . .
lookup ( test_set, 10 ns, sought_test, was_present );
assert was_present
  report "Test at 10 ns not found i n  test set";

Example 1.12. Dictionary traversal with an action procedure

We can augment the dictionary abstract data type with an operation for traversing a dictionary to apply an action to each element. We define the traversal procedure as an uninstantiated procedure within the uninstantiated dictionaries package:

package dictionaries is
  generic ( . . . );
  . . .
  procedure traverse
    generic   ( procedure action ( element : in element-type ) )
    parameter ( dictionary : in dictionary_type );

end package dictionaries ;

package body dictionaries is
  . . .
  procedure traverse
    generic   (  procedure action ( element : in element_type ) )
    parameter (  dictionary : in dictionary_type ) is
  begin
    if dictionary = null then
      return;
    end if ;
    traverse ( dictionary . left_subtree );
    action   ( dictionary . element );
    traverse ( dictionary . right_subtree );
  end procedure traverse;

end package body dictionaries;

Given this augmented package and the same instance as in Example 1.11, we can use the traverse procedure to count the number of elements in a dictionary. We first declare an action procedure:

variable test_pattern_count : natural := 0;
procedure count_a_test_pattern
            ( test_pattern : in test_pattern_type ) is
begin
  test_pattern_count := test_pattern_count + 1;
end procedure count-a-test-pattern;

We need to include the parameter, even though it is not used, since the profile of the action procedure must match that of the formal generic procedure. We instantiate the traverse procedure in the declarative part of the design:

procedure count_test_patterns is new traverse
  generic map ( action => count_a_test_pattern );

and then call the instance:

count_test_patterns(test_set);
assert test_pattern_count > 0
  report "The test patterns have gone missing!";

We can use a separate instantiation of the traverse procedure to perform a different action. For example, if we need to dump a list of test patterns to a file in order of their time, we would define an action procedure:

type test_pattern_file is file of test_pattern_type;
file dump_file : test_pattern_file;
procedure dump_a_test_pattern
             ( test_pattern : in test_pattern_type ) is
begin
  write(dump_file, test_pattern);
end procedure dump_a_test_pattern;

In this case, the parameter to the action procedure is used. We instantiate the traverse procedure in the declarative part of the design:

procedure dump_test_patterns is new traverse
  generic map ( action => dump_a_test_pattern );

and then call the instance:

file_open(dump_file, "test_patterns.dmp", write_mode);
dump_test_patterns(test_set);
file_close(dump_file);

The recursive traverse procedure in Example 1.12 further illustrates the rules we mentioned in Section 1.3 for interpreting names in uninstantiated units. The reference to the name traverse within that procedure is interpreted, in each instance of the procedure, as a reference to the instance. Thus each instance is properly recursive. This is the only situation where we can write a call to an uninstantiated subprogram.

In each of the examples we have seen, the subprogram that we provide as an actual, either explicitly or implicitly, for a formal generic subprogram has the same parameter and result type profile as the formal. In fact, the rule is stronger than that. The actual and formal subprograms must have conforming profiles, which means both are procedures or both are functions; the parameter and result type profiles of the two subprograms are the same; and corresponding parameters have the same class (signal, variable, constant, or file) and mode (in, out, or inout). The purpose of these rules is to ensure that a call to the formal subprogram will be legal for whatever actual subprogram is provided. As a counter example, suppose the formal subprogram had a signal parameter of a given type, and the actual subprogram had a variable parameter of the same type. A call to the formal subprogram would provide a signal as the actual parameter. However, the actual subprogram would expect a variable, and would perform variable assignments on it. This is clearly an error, even though the parameter and result type profiles of the two subprograms match. The additional requirements for profile conformance avoid this kind of error.

There are two further rules relating to the parameters of generic subprograms. The first is that, if a formal parameter of a formal generic subprogram has a default value, that value is used when an actual parameter is omitted, regardless of whether the corresponding formal parameter of the actual subprogram has a default value. An example will help clarify this. Suppose we declare an entity with a formal generic subprogram, and a corresponding architecture, as follows:

entity up_down_counter is
  generic ( type T;
            function add ( x  : T; by : integer := 1 ) return T )
  port ( . . . );
end entity up_down_counter;

architecture rtl of up_down_counter is
begin
  count : process (clk) is
  begin
    if rising_edge(c1k) then
      if mode = "1" then
        count_value <= add(count_value); -- use default value
      else
        count_value <= add(count_value, -1);
      end if;
    end if;
  end process count;
end architecture rtl;

The formal generic subprogram add has a parameter by with the default value 1. In the first call to add within the architecture, we omit a value for by, so the default value 1 is used. This allows an analyzer to compile the call with the default value independently of any instantiation of the enclosing entity that we might write subsequently. For example, suppose we instantiate the entity with an actual generic subprogram declared as follows:

function add_int ( a : integer; incr : integer := 0 )
                 return integer is
begin
  return a + incr;
end function add_int ;
. . .

int-counter : entity work.up_down_counter(rtl)
  generic map ( T => integer; add => add_int )
  port map ( . . . );

In this instance, the actual generic subprogram associated with add has the default value 0 for its second parameter. Despite this, the first call to the subprogram in the architecture still uses the default value 1 for the by parameter, since that is what is declared for the formal generic subprogram.

The rule dealing with default values for parameters also applies to the case where the parameter of the formal generic subprogram has no default value. In that case, a call must supply a value, even if the actual generic subprogram in an instance has a default value for the parameter. For example, in the up_down_counter entity, had we declared the formal generic function add as follows:

function add ( x : T; by : integer ) return T

the first call within the architecture would have to specify an actual value for the by parameter. The fact that the function add_int supplied as the actual generic subprogram in the instance has a default value for its second parameter cannot be used within the architecture.

The second rule relating to parameters of generic subprograms is that the parameter-subtype constraints of the actual subprogram apply when the subprogram is called, not the parameter-subtype constraints of the formal subprogram. To illustrate, suppose we instantiate the up_down_counter entity with a different function, as follows:

function add_nat ( a : natural; incr : natural := 0 )
                 return natural is
begin
  return a + incr;
end function add_nat;
. . .

nat-counter : entity work.up_down_counter(rtl)
  generic map ( T => natural; add => add_nat )
  port map ( . . . );

In this instance, the second parameter of the actual generic subprogram is of the base type integer with a range constraint requiring the value to be non-negative. The second call within the architecture provides the value -1 for the parameter. While this conforms to the constraint on the by parameter of the formal generic subprogram, it does not conform to the constraint on the corresponding parameter of the actual generic subprogram in the instance. Hence, when the function is called with that value in the instance, an error occurs.

Uninstantiated Methods in Protected Types

We now return to a discussion of the relationship between uninstantiated subprograms and protected types, mentioned in passing in Section 1.4. We build on our discussion of generic subprograms to provide motivating examples of the relationships that can occur. There are two cases to consider. The first is declaration of an instance of an uninstantiated subprogram as a method of a protected type, and the second is declaration of an uninstantiated subprogram within a protected type.

Starting with the first case, if we have an uninstantiated subprogram declared outside a protected type, and we declare an instance of the subprogram within the protected type declaration, the instance becomes a method of the protected type. The scheme is

procedure uninstantiated_name
  generic ( . . . )
  parameter ( . . . );

type PT  is protected
  . . .
  procedure instance_name is new uninstantiated-name
    generic map ( . . . );
  . . .
end protected PT;

We can declare a shared variable of the protected type and call the method:

shared variable SV : PT;
. . .

SV. instance_name ( . . . ) ;

On the face of it, there seems no purpose to this scheme. The uninstantiated subprogram, being outside the protected type, cannot refer to the items encapsulated within the protected type. So there would appear to be no reason for instantiating the subprogram in the protected type. However, we can provide controlled access to the encapsulated items via a method of the protected type provided as an actual generic subprogram to the instance. The refinement to the scheme is:

procedure uninstantiated_name
  generic ( . . .; formal_generic_subprogram; . . . )
  parameter ( . . . );

type PT is protected
  method_declaration;
  procedure instance_name is new  uninstantiated_name
    generic map ( . . . , method_name, . . . ) ;
  . . .
end protected PT;

In this scheme, the method has access to the encapsulated items within the protected type. When the instance invokes the actual generic subprogram, the method is called.

Example 1.13. Test-vector set with tracing

Suppose we have an uninstantiated subprogram that gets a test-vector value corresponding to a specified time and that writes the vector value to the standard output file. The procedure has a formal generic subprogram representing the action to perform to get the test vector.

procedure trace_test_vector is
  generic ( impure function get_test_vector
                                ( vector_time : time )
                                return test_vector )
  parameter ( vector_time : time ) is
  variable vector : test_vector;
  use std.textio.al1;
  variable L : line;
begin
  write(L, now) ;
  write(L, string' (": "));
  vector   := get_test_vector(vector_time);
  . . .   -- write test vector
  writeline(output, L) ;
end procedure trace;

We can declare a protected type representing a set of test vectors to be applied at various times. The protected type has a method for getting a test vector for a specific time. We include an instance of the trace_test_vector procedure as a method to trace a test vector from the particular set represented by a shared variable of the protected type. The protected type declaration is:

type test_set is protected
  . . .
  impure function get_vector_for_time ( vector_time : time )
                                      return test_vector;

   procedure trace_for_time is new trace_test_vector
     generic map ( get_test_vector => get_vector_for_time );

end protected test_set;

We might declare two shared variables of this protected type, representing two distinct sets of test vectors:

shared variable main_test_set, extra_test_set : test-set;

If we invoke the trace_for_time method on one of the shared variables:

main_test_set.trace_for_time(100 ns);

the instance of the trace_test_vector procedure invokes the actual subprogram provided for the instance of the protected type. That is, it invokes the get_vector_for_time method associated with the shared variable main_test_set. If, on the other hand, we invoke the trace-for-time method on the other shared variable:

extra_test_set.trace_for_time(100 ns);

the instance of the trace_test_vector procedure invokes the get_vector_for_time method associated with the shared variable extra_test_set. What this reveals is that each shared variable of the protected type binds its get_vector_for_time method, which has access to the shared variable’s state, as the actual generic procedure in its instance of the trace_test_vector procedure. That instance, provided as a method of the shared variable, thus has indirect access to the shared variable’s state.

The second case to consider is declaration of an uninstantiated subprogram within a protected type. That uninstantiated procedure is not itself a method, since it cannot be called. However, it can be instantiated within the protected type to provide a method. Moreover, each shared variable of the protected type contains a declaration of the uninstantiated subprogram. That subprogram can be instantiated, giving a subprogram that has access to the items encapsulated in the shared variable. We will illustrate these mechanisms with an example.

Example 1.14. Stimulus list with visitor traversal

For a design requiring signed stimulus values, we can declare a procedure for displaying a signed value to the standard output file, as follows:

procedure output_signed ( value : in signed ) is
  use std. textio.al1;
  variable L : line;
begin
  write(L, value);
  writeline(output, L);
end procedure output_signed;

We also declare a protected type for a list of signed stimulus values:

type signed-stimulus_list is protected
   . . .

   procedure traverse_with_in_parameter
     generic ( procedure visit ( param : in signed ) );

   procedure output_all is new traverse_with_in_parameter
     generic map ( visit => output_signed );

end protected signed_stimulus_list;

The protected type includes an uninstantiated procedure to apply a visitor procedure to each element in the list of signed values. It instantiates the traversal procedure to provide a method that displays each element. We can use this protected type to declare a shared variable and then invoke the method to display its element values:

shared variable list1 : signed_stimulus_list;
. . .

list1.output_all;

Suppose now we want to use the traversal procedure to accumulate the sum of element in a list so that we can calculate the average value. We can provide another action procedure and use it in a further instantiation of the traversal procedure:

variable sum, average : signed(31 downto 0);
variable count : natural := 0;

procedure accumulate_signed ( value : in signed ) is
begin
  sum := sum + value;
  count := count + 1;
end procedure accumulate_signed;

procedure accumulate_all_list1 is
  new list1.traverse_with_in_parameter
    generic map ( visit => accumulate_signed );
. . .

accumulate_all_list1;
average := sum / count;

In this case, the instance is a procedure declared externally to the protected type. However, since it is an instance of a subprogram defined within the shared variable list 1, the instance has access to the encapsulated items within list 1 . The instance accumulate_all_list 1 thus applies the accumulate_signed visitor procedure to each element within list 1.

If we want to calculate the average value of any list of elements, we need to wrap these declarations up in a procedure that has a shared variable as a parameter. That includes declaring the instance of the traversal procedure within the outer procedure. The complete procedure would be:

procedure calculate_average
  ( variable list : inout signed_stimulus_list
    variable average : out signed ) is

  variable sum : signed(average'range) ;
  variable count : natural := 0;

  procedure accumulate_signed ( value : in signed ) is
  begin
    sum := sum + va1ue;
    count := count + 1;
  end procedure accumulate_signed;

  procedure accumulate_all is
    new list . traverse_with_in_parameter
      generic map ( visit => accumulate_signed );

begin
  accumulate_all;
  average := sum  / count;
end procedure calculate_average;

In this case, the instance of the traversal procedure is also declared externally to the protected type. However, it is an instance of the subprogram defined within the shared variable list provided as a parameter to the calculate_average procedure. Logically, each time the calculate_average procedure is called, a new instance of the traversal procedure is defined particular to the actual shared variable provided as the parameter. The instance thus applies the local accumulate_signed visitor procedure to each element within the actual shared variable.

Generic Packages

One of the common uses of packages is to declare an abstract data type (ADT), consisting of a named type and a collection of operations on values of the type. We have seen in Section 1.2 that we can include a generic list in a package declaration to make the package reusable for different actual types and operations. Often, the package for an ADT is reusable in this way.

Suppose we have an ADT specified in a package with generics, and we want to provide a further package extending the types and operations of the ADT. To make the extension package reusable, we would have to provide a generic type to specify an instance of the ADT named type, along with generic subprograms for each of the ADT operations. If the ADT has many operations, specifying them as actual generic subprograms in every instance of the extension package would be extremely onerous. To avoid this, VHDL-2008 allows us to specify an instance of the ADT package as a formal generic package of the extension package. Once we’ve instantiated the ADT package, we then provide that instance as the actual generic package of the extension package.

There are three forms of formal generic package declaration that we can write in a generic list. The first form is:

generic ( . . .;
          package formal_pkg_name is new uninstantiated_pkg_name
            generic map ( < > );
          . . . );

In this case, formal_pkg_name represents an instance of the uninstantiated_pkg_name package, for use within the enclosing unit containing the generic list. In most use cases, the enclosing unit is itself an uninstantiated package. However, we can also specify formal generic packages in the generic lists of entities and subprograms. When we instantiate the enclosing unit, we provide an actual package corresponding to the formal generic package. The actual package must be an instance of the named uninstantiated packge. The box notation “<>” written in the generic map of the formal generic package specifies that the actual package is allowed to be any instance of the named uninstantiated package. We use this form when the enclosing unit does not depend on the particular actual generics defined for the actual generic package.

No doubt, all of this discussion of packages within packages and generics at different levels can become confusing. The best way to motivate the need for formal generic packages and to sort out the relationships between the pieces is with an example.

Example 1.15. Fixed-point complex numbers

VHDL-2008 defines a new package, fixed_generic_pkg (described in Section 8.4), for fixed-point numbers represented as vectors of std_logic elements. The package is an uninstantiated package, with generic constants specifying how to round results, how to handle overflow, the number of guard bits for maintaining precision, and whether to issue warnings. The package defines types ufixed and sfixed for unsigned and signed fixed-point numbers; and numerous arithmetic, conversion and input/output operations. We can instantiate the package with values for the actual generic constants to get a version with the appropriate behavior for our specific design needs.

Now suppose we wish to build upon the fixed-point package to define fixed-point complex numbers, represented in Cartesian form with fixed-point real and imaginary parts. We want the two parts of a complex number to have the same left and right index bounds, implying the same range and precision for the two parts. We can achieve that constraint by defining the complex-number type and operations in a package with formal generic constants for the index bounds. The complex-number type is defined using the sfixed type from an instance of the fixed-point package, and the complex-number operations need to use fixed-point operations from that instance. Thus, we include a formal generic package in the generic list of the complex-number package, as follows:

library IEEE;
package complex_generic_pkg is
  generic ( left, right : integer;
               package fixed_pkg_for_complex is
                 new IEEE. fixed_generic_pkg
                   generic map ( <>) );

  use fixed_pkg_for_complex.all;

  type complex is record
    re, im : sfixed(left downto right);
  end record;

  function "-"  ( z : complex ) return complex;
  function conj ( z : complex ) return complex;
  function "+"  ( l : complex;  r : complex ) return complex;
  function "-"  ( l : complex;  r : complex ) return complex;
  function "*"  ( l : complex;  r : complex ) return complex;
  function "/"  ( l : complex;  r : complex ) return complex;

end package complex_generic_pkg;

Within the complex_generic_pkg package, the formal generic package fixed_pkg_for_complex represents an instance of the fixed_generic_pkg package. The box notation in the generic map indicates that any instance of fixed_generic_pkg will be appropriate as an actual package. The use clause makes items defined in the fixed_pkg_for_complex instance visible, so that sfixed can be used in the declaration of type complex. The generic constants left and right are used to specify the index bounds of the two record elements. The operations defined for sfixed in the fixed_pkg_for_complex instance are also used to implement the complex-number operations in the package body for complex_generic_pkg, as follows:

package body fixed_complex_pkg is
  function "-" ( z : complex ) return complex is
  begin
    return ( -z.re, -z. im );
  end function "-";
  . . .
end package body fixed_complex_pkg;

In the "-" operation for type complex, the "-" operation for type sfixed is applied to each of the real and imaginary parts. The other operations use the sfixed operations similarly.

In a design, we can instantiate both the fixed-point package and the complex-number package according to our design needs, for example:

package dsp_fixed_pkg is new IEEE.fixed_generic_pkg
  generic map ( fixed_rounding_style => true,
                fixed_overflow_style => true,
                fixed_guard_bits => 3,
                no_warning => false );

package dsp_complex_pkg is new work. complex_generic_pkg
  generic map ( left => 3, right => -12,
                fixed_pkg_for_complex => dsp_fixed_pkg );

The first instantiation defines an instance of the fixed-point package, which provides the type sfixed and operations with the required behavior. The second instantiation defines an instance of the complex-number package with left and right bounds of 3 and -12 for the real and imaginary parts. The type sfixed and the corresponding operations used within this instance of the complex-number package are provided by the actual generic package dsp_fixed_pkg. We can use the packages to declare variables and apply operations as follows:

use dsp_fixed_pkg.all, dsp_complex_pkg.all;
variable a, b, z : complex
variable c : sfixed;
. . .

z := a + conj(b);
z := (c * z.re, c * z.im);

The second form of formal generic package that we can write in a generic list is:

generic ( . . .;
           package formal_pkg_name is new uninstantiated_pkg_name
             generic map ( actual_generics );
           . . . );

Again, formal_pkg_name represents an instance of the uninstantiated_pkg_name package, for use within the enclosing unit containing the generic list. The actual generics provided in the generic map of the formal generic package specify that the actual package must be an instance of the named uninstantiated package with those same actual generics. We generally use this form when the enclosing unit also has another formal generic package defined earlier in its generic list. The latter generic is expected to have a generic package that is the same instance as the actual for the earlier generic package. No doubt that statement is unfathomable due to the packages within packages within packages. An example, building on Example 1.15, will help to motivate the need for the language feature and show how it may be used.

Example 1.16. Mathematical operations on fixed point complex numbers

In Example 1.15, we defined a package for complex number that provided a complex-number type and basic arithmetic operations. We can build upon this package to define a further package for more advanced mathematical operations on complex values. We will also use a package of advanced mathematical operations defined for fixed-point values:

package fixed_math_ops is
  generic ( package fixed_pkg_for_math is
              new IEEE. fixed_generic_pkg
                generic map (<>) ) ;

  use fixed_pkg_for_math.all;

  function sqrt ( x : sfixed ) return sfixed;
  function exp ( x : sfixed ) return sfixed;
  . . .

end package fixed_math_ops ;

This package has a formal generic package for an instance of the fixed_generic_pkg package, since the operations it applies to the function parameters of type sfixed must be performed using the behavior defined for the sfixed type in the package instance proving the type. This is a similar scenario to that described in Example 1.15.

The advanced complex-number operations must be performed using the same sfixed type and basic fixed-point operations used to define the complex-number type and operations. It must also use the advanced fixed-point operations and the complex-number type and operations, with those types and operations being based on the same sfixed type and basic fixed-point operations. Thus, the advance complex-number package must have formal generic packages for the fixed-point package, the fixed-point mathematical operations package, and the complex-number package, as follows:

package complex_math_ops is
  generic ( left, right : integer;
             package fixed_pkg_for_complex_math is
               new IEEE. fixed_generic_pkg
                 generic map (<>);
             package fixed_math_ops is
               new work.fixed_math_ops
                 generic map ( fixed_pkg_for_math =>
                                 fixed_pkg_for_complex_math );
             package complex_pkg is
                new work. complex_generic_pkg
                   generic map ( left => left, right => right,
                                  fixed_pkg_for_complex =>
                                      fixed_pkg_for_complex_math ) );
   use fixed_pkg_for_complex_math.all,
       fixed_math_ops . all , complex_pkg . all ;
   function "abs" ( z : complex ) return sfixed;
   function arg   ( z : complex ) return sfixed;
   function sqrt  ( z : complex ) return complex;
   . . .

end package complex_math_ops;

The package body is

package body complex_math_ops is

  function "abs" ( z : complex ) return sfixed is
  begin
    return sqrt(z.re * z.re + z.im * z.im);
  end function "abs";
  . . .

end package body complex_math_ops;

We can now instantiate the packages for a given design. For example, given the instances dsp_fixed_pkg and dsp_complex_pkg declared in Example 1.15, we can also declare instances of the advanced fixed-point operations package and the advanced complex operations package:

package dsp_fixed_math_ops is new work.fixed_math_ops
  generic map ( fixed_pkg_for_math => dsp_fixed_pkg ) ;

package dsp_complex_math_ops is new work. complex_math_ops
  generic map ( left => 3, right => -12,
                fixed_pkg_for_complex_math => dsp_fixed_pkg,
                fixed_math_ops => dsp_fixed_math_ops,
                complex_pkg    => dsp_complex_pkg );

The third form of formal generic package that we can write in a generic list is:

generic ( . . .;
          package formal_pkg_name is new uninstantiated_pkg_name
            generic map ( default );
          . . . );

This form is similar in usage to the second form, but replaces the actual generics with the reserved word default. We can use this third form when the named uninstantiated package has defaults for all of its formal generics. The actual package must then be an instance of the named uninstantiated package with all of the actual generics being the same as the defaults. Those actual generics (for the actual generic package) can be either explicitly specified when the actual package is instantiated, or they can be implied by leaving the actual generics unassociated. Thus, this third form is really just a notational convenience, as it saves us writing out the defaults again as actual generics in the generic map of the formal generic package.

While generic packages might seem to be rather complex to put into practice, we envisage that most of the time packages using generic packages will be developed by personnel in support of design teams. They would normally provide source code templates for designers to instantiate the packages, including instantiating any dependent packages as actual generics. Thus, the designers would be largely insulated from the complexity.

For the developers of such packages, however, there are a number of rules relating to formal and actual generic packages. As we have mentioned, the actual package corresponding to a formal generic package must be an instance of the named uninstantiated package. To summarize the rules relating to the generic map in the formal generic package:

  • If the generic map of the formal generic package uses the box (“<>“) symbol, the actual generic package can be any instance of the named uninstantiated package.

  • If the formal generic package declaration includes a generic map with actual generics, then the actual generics in the actual package’s instantiation must match the actual generics in the formal generic package declaration.

  • If the formal generic package declaration includes a generic map with the reserved word default, then the actual generics in the actual package’s instantiation must match the default generics in the generic list of the named uninstantiated package.

The meaning of the term “match” applied to actual generics depends on what kind of generics are being matched. For generic constants, the actuals must be the same value. It doesn’t matter whether that value is specified as a literal, a named constant, or any other expression. For a generic type, the actuals must denote the same subtype; that is, they must denote the same base type and the same constraints. Constraints on a subtype include range constraints, index ranges and directions, and element subtypes. For generic subprograms, the actuals must refer to the same subprogram, and for generic packages, the actuals must refer to the same instance of a specified uninstantiated package.

In the case of a default generic subprogram implied by a box symbol in the generic list of the named uninstantiated package, the actual subprogram must be the subprogram of the same name and conforming profile directly visible at the point where the formal generic package is declared. For example, if an uninstantiated package is declared as

package pkg1 is
  generic ( function "<" ( L, R : integer )
                         return boolean is  <> ) ) ;
  . . .
end package pkg1;

we can declare a second package as follows:

package pkg2 is
  generic ( package instl is new pkg1 generic map   ( default ) ) ;
  . . .
end package pkg2;

In this case, any package provided as an actual for instl must be an instance of pkg1, such as the following:

package ascending_pkgl is new pkg1
  generic map ( T => integer ) ;

Since the predefined "<" function for integer is visible at the point of declaring ascending_pkg1, that function is used as the actual for the generic function "<" in the instance of pkg1. At the place of declaring the formal generic package inst1 within the generic list of pkg2, the predefined “<” function for integer is also directly visible, so it is this function that must be matched as the actual for "<" in any instance of pkg1 supplied as an actual for inst1. Thus, the following instantiation of pgk2 is legal:

package integer_pkg2 is new pkg2
  generic map ( inst1 => ascending_pkg1 ) ;

Use Case: Generic Memories

In this use case, we will explore the use of extended generics for modeling memories. We will develop a package of operations on memories, with the memory address width and depth specified by generic constants, and the address and data types specified using generic types. The package declares a type for signals representing RAM storage, and operations to read, write, load and dump RAM contents. The package declaration is:

library IEEE;
use IEEE.std_logic_ll64.std_logic_vector;

package memories is
  generic ( width : positive;
               depth : positive;
               type address_type;
               type data_type;
               pure function to_integer (a : address_type)
                 return natural is <>;
               pure function to_address_type (a : natural)
                 return address_type is <>;
               pure function to_std_logic_vector (d : data_type)
                 return std_logic_vector is <>;

               pure function to_data_type (d : std_logic_vector)
                 return data_type is <> );

  type RAM_type is array (0 to 2**depth - 1) of data_type;

  procedure read_RAM (signal   RAM     : in  RAM_type;
                      constant address : in  address_type;
                      signal   data    : out data_type);

  procedure write_RAM (signal   RAM     : out RAM_type;
                      constant address : in  address_type;
                      constant data    : in  data_type);

  type format_type is (binary, hex);

  procedure load_RAM  (signal   RAM            : out RAM_type;
                      constant file_name      : in  string;
                      constant format         : in  format_type;
                      constant start_address  : in  address_type
                        := to_address_type(0);
                      constant finish_address : in  address_type
                        := to_address_type(2**depth - 1);
                      variable ok             : out boolean) ;

  procedure dump_RAM  (signal   RAM            : in  RAM_type;
                      constant file_name      : in  string;
                      constant format         : in  format_type;
                      constant start_address  : in  address_type
                        := to_address_type(0);
                      constant finish_address : in  address_type
                        := to_address_type(2**depth - 1);
                      variable ok             : out boolean) ;

end package memories ;

The formal generic constants width and depth specify the bit width of memory data and addresses, respectively. The formal generic types address_type and data_type are used for memory addresses and data, respectively. The memory has 2depth locations, indexed from 0 to 2depth - 1, each storing a data_type value. Since we need to use integer values to index an array storing the memory contents, we need a function to convert an address to an integer; hence, the formal generic function to_integer. We also specify formal generic functions for use in the load and dump operations: to convert from an integer to an address_type value, to convert from a std_logic_vector value to a data_type value, and to convert from a data_type value to a std_logic_vector value. The reason for the last two is that the load and dump operations will read and write data values using the same formatting as that used for std_logic_vector values.

The type RAM_type is an array type used in models for signals representing RAM contents. The procedures read_RAM and write_RAM each have a signal parameter of this type, as well as parameters for the address and data. The load_RAM and dump_RAM procedures also have a RAM_type signal parameter, and load from or store to a file whose name is specified in the file_name parameter. The start and finish addresses are specified as parameters, with default values specified as integers converted to address_type values using the formal generic conversion functions. The ok parameter indicates whether the operation was successful.

The package body is:

package body memories is

  procedure read_RAM (signal   RAM     : in  RAM_type;
                    constant address : in  address_type;
                    signal   data    : out data_type) is
  begin
    assert to_integer(address) <= 2**depth  - 1;
    data <= RAM(to_integer(address));
  end procedure read_RAM;

  procedure write_RAM (signal   RAM     : out RAM_type;
                    constant address : in  address_type;
                    signal   data    : in  data_type) is
  begin
    assert to_integer(address) <= 2**depth  - 1;
    RAM(to_integer(address)) <= data;
  end procedure write_RAM;

  use std.textio.all;

  procedure load_RAM (signal   RAM            : out RAM_type;
                    constant file_name      : in  string;
                    constant format         : in  format_type;
                    constant start_address  : in  address_type
                      := to_address_type(0) ;
                    constant finish_address : in  address_type
                      := to_address_type(2**depth - 1) ;
                    variable  ok            : out boolean)   is
    file load_file    : text;
    variable status   : file_open_status;
  begin
    ok  := false;
    file_open(f => load_file, external_name => file_name,
              open_kind => read_mode, status => status);
    if status /= open_ok then
      return;
    end if;
    -- code to read and parse memory file contents
    . . .
    file_close(f => load_file);
    ok := true;
  end procedure load_RAM;

  procedure dump_RAM (signal   RAM            : in RAM_type;
                    constant file_name      : in string;
                    constant format         : in format_type;
                    constant start_address  : in address_type
                      := to_address_type(0);
                    constant finish-address : in address-type
                      := to_address_type(2**depth - 1);
                    variable ok             : out boolean) is
    file dump_file   : text;
    variable status  : file_open_status;
  begin
    ok := false;
    file-open(f => dump_file, external_name  => file_name,
              open_kind => write_mode, status => status);
    if status /= open_ok  then
      return;
    end if;
    -- code to write memory file contents
    . . .
    file_close(f => dump_file);
    ok := true;
  end procedure dump_RAM;

end package body memories;

The read_RAM procedure asserts that the address, converted to an integer value, lies within the index range of the memory array type. It uses the converted address to index the RAM signal, and assigns the indexed element value to the data signal parameter. The write_RAM procedure is similar, but updates the indexed element using the data parameter value.

The load_RAM procedure attempts to open the file named by the file_name parameter. If it succeeds, the procedure then reads the file contents. Assuming the file is in Verilog memory format, the data values are read as std_logic_vector values, each of the width specified by the width generic. These values are converted to data_type values using the formal generic conversion function to_data_type. The dump_RAM procedure is similar, but converts data_type values to std_logic_vector values using the to_std_logic_vector formal generic function.

There are several common cases for address and data types. Specifically, addresses are commonly of type natural, std_logic_vector, or unsigned; and data values are commonly of type natural, std_logic_vector, unsigned, or signed. In support of these common cases, we can define a package of conversion functions as follows:

library IEEE;
use IEEE.std_logic_ll64.std_logic_vector;
use IEEE. numeric_std. unsigned;

package memories_support is
  generic ( width : positive;
            depth : positive );

  -- Conversions for common actual types for address_type:
  -- natural , std_logic_vector, unsigned
  pure function to_integer (a: natural) return natural;

  -- to_integer [std_logic_vector return natural]
  -- provided by ieee. numeric_std_unsigned

  -- to_integer [unsigned return natural]
  -- provided by ieee. numeric_std

  pure function to_address_type (a: natural) return natural;

  pure function to_address_type (a: natural)
                                return std_logic_vector;

  pure function to_address_type (a: natural) return unsigned;

  -- Conversions for common actual types for data_type:
  -- natural, std_logic_vector, unsigned, signed

  pure function to_std_logic_vector (d : natural)
                                      return std_logic_vector;

  pure function to_std_logic_vector (d : std_logic_vector)
                                      return std_logic_vector;

  pure function to_std_logic_vector (d : unsigned)
                                      return std_logic_vector;

  pure function to_std_logic_vector (d : signed)
                                      return std_logic_vector;

  pure function to_data_type (d : std_logic_vector)
                               return natural;

  pure function to_data_type (d : std_logic_vector)
                               return std_logic_vector;

  pure function to_data_type (d : std_logic_vector)
                               return unsigned;

  pure function to_data_type (d : std_logic_vector)
                               return signed;

end package memories_support;

The package has width and depth generic constants, since these are needed to determine the vector length for conversions from integer values to vector values. No to_integer conversions are needed for std_logic_vector or unsigned, since these are provided by the numeric_std_unsigned and numeric_std packages, respectively. The function bodies are either identities or wrappers around type conversions or conversion functions, as shown in the corresponding package body:

package body memories_support is

  pure function to_integer (a: natural) return natural is
   begin
      return a;
  end function to_integer;

  pure function to_address_type (a : natural) return natural is
  begin
      return a;
  end function to_address_type;

  pure function to_address_type (a : natural)
                                        return std_logic_vector is
  begin
     return IEEE.numeric_std_unsigned.to_stdlogicvector(a,
                                                                depth);
  end function to_address_type;

  pure function to_address_type (a : natural) return unsigned is
  begin
     return IEEE.numeric_std.to_unsigned(a, depth);
  end function to_address_type;

  pure function to_std_logic_vector (d : natural)
                                          return std_logic_vector is
  begin
     return IEEE. numeric_std_unsigned(d, width) ;
  end function to_std_logic_vector;
   pure function to_std_logic_vector (d : std_logic_vector)
                                            return std_logic_vector is
  begin
     return d;
  end function to_std_logic_vector;

  pure function to_std_logic_vector (d : unsigned)
                                              return std_logic_vector is
  begin
     return IEEE. std_logic_1164. std_logic_vector(d);
  end function to_std_logic_vector;

  pure function to_std_logic_vector (d : signed)
                                              return std_logic_vector is
  begin
     return IEEE. std_logic_1164. std_logic_vector(d);
  end function to_std_logic_vector;

  pure function to_data_type (d : std_logic_vector)
                                    return natural is
  begin
     return IEEE.numeric_std_unsigned.to_integer(d);
  end function to_data_type;

  pure function to_data_type (d : std_logic_vector)
                                    return std_logic_vector is
  begin
    return d;
  end function to_data_type;

  pure function to_data_type (d : std_logic_vector)
                                    return unsigned is
  begin
    return IEEE.numeric_std.unsigned(d);
  end function to_data_type;

  pure function to_data_type (d : std_logic_vector)
                                    return signed is
  begin
     return IEEE.numeric_std.signed(d);
  end function to_data_type;

end package body memories_support;

To illustrate use of the memory packages, we develop a memory model for a flow-through SSRAM with the following entity declaration:

library IEEE;
use IEEE.std_logic_ll64.all, IEEE.numeric_std.all;

entity test_RAM  is
   generic ( width : positive; depth : positive;
                 hex_file_name : string := "" );
   port ( clk      : in  std_logic;
                 en, we   : in  std_logic;
                 addr     : in  unsigned(depth-1 downto 0);
                 data_in  : in  std_logic_vector(width-1 downto 0) ;
                 data_out : out std_logic_vector(width-1 downto 0) ) ;
end entity test_RAM;

The address is an unsigned vector of depth bits, and the data input and output are std_logic_vector ports of width bits each. The generic hex_file_name allows us to specify a file from which to initialize the memory. If the name is an empty string, no initialization is done. The architecture body is:

   architecture rtl of test_RAM is

  package my_memories_support is new work.memories_support
    generic map ( width => width, depth => depth );
  use my_memories_support.all;

  package my_memories is new memories
    generic map
      ( width        => width,
        depth        => depth,
        address_type => unsigned(depth-1 downto 0),
        data_type    => std_logic_vector(width-1 downto 0) );
  use my_memories.all;

  signal RAM : RAM_type;

 begin

    RAM_init : process is
      variable ok : boolean ;
    begin
      if hex_file_name /= "" then
        load_RAM(RAM => RAM,
                 file_name => hex_file_name, fomat => hex,
                 ok => ok);
        assert ok report "Error loading RAM from " & hex_file_name;
     end if;
     wait;
  end process RAM_init;
  RAM_proc : process (clk) is
  begin
    if rising_edge(clk) then
      if en = '1' then
         if wr = '1' then
            write_RAM(RAM, addr, data_in);
            data_out <= data_in;
         else
            read_RAM(RAM, addr, data_out);
         end if;
     end if;
   end if;
 end process RAM_proc;
end architecture rtl;

Since we’re using address and data types that are catered for by the support package, we instantiate that package in the architecture body, providing the memory depth and width values as actual generic constants. The use clause for the instance makes all of the required conversion functions for the address and data types directly visible. Next, we instantiate the memories package, providing the memory depth and width values as actuals for the generic constants, and the address and data port subtypes as actuals for the address_type and data_type formal generic types. Since functions with the required names and signatures are directly visible, they are used as actuals for the formal generic functions. The use clause makes the type and the procedures from the package instance directly visible. We use RAM_type as the type in a signal declaration for the SSRAM storage. The RAM_init process checks the value of the hex-file-name generic, and if it is not empty, calls the load_RAM procedure to initialize the SSRAM contents. The RAM_proc process uses the write_RAM and read_RAM procedures to implement memory operations.

 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.127.141