Chapter 4. Composite Data Types and Operations

Now that we have seen the basic data types and sequential operations from which the behavioral part of a VHDL model is formed, it is time to look at composite data types. We first mentioned them in the classification of data types in Chapter 2. Composite data objects consist of related collections of data elements in the form of either an array or a record. We can treat an object of a composite type as a single object or manipulate its constituent elements individually. In this chapter, we see how to define composite types and how to manipulate them using operators and sequential statements.

Arrays

An array consists of a collection of values, all of which are of the same type as each other. The position of each element in an array is given by a scalar value called its index. To create an array object in a model, we first define an array type in a type declaration. The syntax rule for an array type definition is

   array_type_definition ⇐
      array ( discrete_range { , ... } ) of element_subtype_indication

This defines an array type by specifying one or more index ranges (the list of discrete ranges) and the element type or subtype.

Recall from previous chapters that a discrete range is a subset of values from a discrete type (an integer or enumeration type), and that it can be specified as shown by the simplified syntax rule

   discrete_range ⇐
      discrete_subtype_indication
      | simple_expression ( to | downto ) simple_expression

Recall also that a subtype indication can be just the name of a previously declared type (a type mark) and can include a range constraint to limit the set of values from that type, as shown by the simplified rule

   subtype_indication ⇐
      type_mark [ range simple_expression ( to | downto ) simple_expression ]

We illustrate these rules for defining arrays with a series of examples. We start with single-dimensional arrays, in which there is just one index range. Here is a simple example to start off with, showing the declaration of an array type to represent words of data:

   type word is array (0 to 31) of bit;

Each element is a bit, and the elements are indexed from 0 up to 31. An alternative declaration of a word type, more appropriate for “little-endian” systems, is

   type word is array (31 downto 0) of bit;

The difference here is that index values start at 31 for the leftmost element in values of this type and continue down to 0 for the rightmost.

The index values of an array do not have to be numeric. For example, given this declaration of an enumeration type:

   type controller_state is (initial, idle, active, error);

we could then declare an array as follows:

   type state_counts is array (idle to error) of natural;

This kind of array type declaration relies on the type of the index range being clear from the context. If there were more than one enumeration type with values idle and error, it would not be clear which one to use for the index type. To make it clear, we can use the alternative form for specifying the index range, in which we name the index type and include a range constraint. The previous example could be rewritten as

   type state_counts is
    array (controller_state range idle to error) of natural;

If we need an array element for every value in an index type, we need only name the index type in the array declaration without specifying the range. For example:

   subtype coeff_ram_address is integer range 0 to 63;
   type coeff_array is array (coeff_ram_address) of real;

Once we have declared an array type, we can define objects of that type, including constants, variables and signals. For example, using the types declared above, we can declare variables as follows:

   variable buffer_register, data_register : word;
   variable counters : state_counts;
   variable coeff : coeff_array;

Each of these objects consists of the collection of elements described by the corresponding type declaration. An individual element can be used in an expression or as the target of an assignment by referring to the array object and supplying an index value, for example:

   coeff(0) := 0.0;

If active is a variable of type controller_state, we can write

   counters(active) := counters(active) + 1;

An array object can also be used as a single composite object. For example, the assignment

   data_register := buffer_register;

copies all of the elements of the array buffer_register into the corresponding elements of the array data_register.

Example 4.1. A memory module for real-number coefficients

The following is a model for a memory that stores 64 real-number coefficients, initialized to 0.0. We assume the type coeff_ram_address is previously declared as above. The architecture body contains a process with an array variable representing the coefficient storage. When the process starts, it initializes the array using a for loop. It then repetitively waits for any of the input ports to change. When rd is ‘1’, the array is indexed using the address value to read a coefficient. When wr is ‘1’, the address value is used to select which coefficient to change.

   entity coeff_ram is
    port ( rd, wr : in bit;  addr : in coeff_ram_address;
           d_in : in real;  d_out : out real );
   end entity coeff_ram;
   --------------------------------------------------
   architecture abstract of coeff_ram is
   begin
    memory : process is
      type coeff_array is array (coeff_ram_address) of real;
      variable coeff : coeff_array;
    begin
      for index in coeff_ram_address loop
        coeff(index) := 0.0;
      end loop;
      loop
        wait on rd, wr, addr, d_in;
        if rd then
          d_out <= coeff(addr);
        end if;
        if wr then
          coeff(addr) := d_in;
        end if;
      end loop;
    end process memory;
   end architecture abstract;

Multidimensional Arrays

VHDL also allows us to create multidimensional arrays, for example, to represent matrices or tables indexed by more than one value. A multidimensional array type is declared by specifying a list of index ranges, as shown by the syntax rule on page 95. For example, we might include the following type declarations in a model for a finite-state machine:

   type symbol is ('a', 't', 'd', 'h', digit, cr, error);
   type state is range 0 to 6;

   type transition_matrix is array (state, symbol) of state;

Each index range can be specified as shown above for single-dimensional arrays. The index ranges for each dimension need not all be from the same type, nor have the same direction. An object of a multidimensional array type is indexed by writing a list of index values to select an element. For example, if we have a variable declared as

   variable transition_table : transition_matrix;

we can index it as follows:

   transition_table(5, 'd'),

Example 4.2. Transformation matrices

In three-dimensional graphics, a point in space may be represented using a three-element vector [x, y, z] of coordinates. Transformations, such as scaling, rotation and reflection, may be done by multiplying a vector by a 3 × 3 transformation matrix to get a new vector representing the transformed point. We can write VHDL type declarations for points and transformation matrices:

   type point is array (1 to 3) of real;
   type matrix is array (1 to 3, 1 to 3) of real;

We can use these types to declare point variables p and q and a matrix variable transform:

   variable p, q : point;
   variable transform : matrix;

The transformation can be applied to the point p to produce a result in q with the following statements:

   for i in 1 to 3 loop
    q(i) := 0.0;
    for j in 1 to 3 loop
      q(i) := q(i) + transform(i, j) * p(j);
    end loop;
   end loop;

Array Aggregates

We have seen how we can write literal values of scalar types. Often we also need to write literal array values, for example, to initialize a variable or constant of an array type. We can do this using a VHDL construct called an array aggregate, according to the syntax rule

   aggregate ⇐ ( ( [ choices => ] expression ) { , ... } )

Let us look first at the form of aggregate without the choices part. It simply consists of a list of the elements enclosed in parentheses, for example:

   type point is array (1 to 3) of real;
   constant origin : point := (0.0, 0.0, 0.0);
   variable view_point : point := (10.0, 20.0, 0.0);

This form of array aggregate uses positional association to determine which value in the list corresponds to which element of the array. The first value is the element with the leftmost index, the second is the next index to the right, and so on, up to the last value, which is the element with the rightmost index. There must be a one-to-one correspondence between values in the aggregate and elements in the array.

An alternative form of aggregate uses named association, in which the index value for each element is written explicitly using the choices part shown in the syntax rule. The choices may be specified in exactly the same way as those in alternatives of a case statement, discussed in Chapter 3. As a reminder, here is the syntax rule for choices:

   choices ⇐ ( simple_expression | discrete_range | others ) { | ... }

For example, the variable declaration and initialization could be rewritten as

   variable view_point : point := (1 => 10.0, 2 => 20.0, 3 => 0.0);

The main advantage of named association is that it gives us more flexibility in writing aggregates for larger arrays. To illustrate this, let us return to the coefficient memory described above. The type declaration was

   type coeff_array is array (coeff_ram_address) of real;

Suppose we want to declare the coefficient variable, initialize the first few locations to some non-zero value and initialize the remainder to zero. Following are a number of ways of writing aggregates that all have the same effect:

   variable coeff : coeff_array
             := (0 => 1.6, 1 => 2.3, 2 => 1.6, 3 to 63 => 0.0);

Here we are using a range specification to initialize the bulk of the array value to zero.

   variable coeff : coeff_array
             := (0 => 1.6, 1 => 2.3, 2 => 1.6, others => 0.0);

The keyword others stands for any index value that has not been previously mentioned in the aggregate. If the keyword others is used, it must be the last choice in the aggregate.

   variable coeff : coeff_array
             := (0 | 2 => 1.6, 1 => 2.3, others => 0.0);

The “|” symbol can be used to separate a list of index values, for which all elements have the same value.

Note that we may not mix positional and named association in an array aggregate, except for the use of an others choice in the final postion. Thus, the following aggregate is illegal:

   variable coeff : coeff_array
             := (1.6, 2.3, 2 => 1.6, others => 0.0);  -- illegal

We can also use aggregates to write multidimensional array values. In this case, we treat the array as though it were an array of arrays, writing an array aggregate for each of the leftmost index values first.

Example 4.3. Transition matrix for a modem finite-state machine

We can use a two-dimensional array to represent the transition matrix of a finite-state machine (FSM) that interprets simple modem commands. A command must consist of the string “atd” followed by a string of digits and a cr character, or the string “ath” followed by cr. The state transition diagram is shown in Figure 4.1. The symbol “other” represents a character other than ‘a’, ‘t’, ‘d’, ‘h’, a digit or cr.

An outline of a process that implements the FSM is:

   modem_controller : process is

    type symbol is ('a', 't', 'd', 'h', digit, cr, other);
    type symbol_string is array (1 to 20) of symbol;
    type state is range 0 to 6;
    type transition_matrix is array (state, symbol) of state;

    constant next_state : transition_matrix :=
      ( 0 => ('a' => 1, others => 6),
        1 => ('t' => 2, others => 6),
        2 => ('d' => 3, 'h' => 5, others => 6),
        3 => (digit => 4, others => 6),
        4 => (digit => 4, cr => 0, others => 6),
        5 => (cr => 0, others => 6),
        6 => (cr => 0, others => 6) );

    variable command : symbol_string;
    variable current_state : state := 0;

   begin
    ...
    for index in 1 to 20 loop
      current_state := next_state( current_state, command(index) );
      case current_state is
        ...
      end case;
    end loop;
    ...
   end process modem_controller;

The type declarations for symbol and state represent the command symbols and the states for the FSM. The transition matrix, next_state, is a two-dimensional array constant indexed by the state and symbol type. An element at position (i, j) in this matrix indicates the next state the FSM should move to when it is in state i and the next input symbol is j. The matrix is initialized according to the transition diagram. The process uses the current_state variable and successive input symbols as indices into the transition matrix to determine the next state. For each transition, it performs some action based on the new state. The actions are implemented within the case statement.

The state transition diagram for a modem command finite-state machine. State 0 is the initial state. The machine returns to this state after recognizing a correct command. State 6 is the error state, to which the machine goes if it detects an illegal or unexpected character.

Figure 4.1. The state transition diagram for a modem command finite-state machine. State 0 is the initial state. The machine returns to this state after recognizing a correct command. State 6 is the error state, to which the machine goes if it detects an illegal or unexpected character.

In the array aggregates we have seen so far, each value in the aggregate corresponds to a single array element. VHDL also allows an alternate form of aggregate in which we write a combination of individual element values and sub-array values. The element values and sub-array values are joined together to form a complete array value. For example, given the declarations

   type byte is array (7 downto 0) of bit;
   variable d_reg : byte;
   variable a, b : bit;

we could assign an aggregate value as follows:

   d_reg := (a, "1001", b, "00");

The aggregate represents a value consisting of an element taken from the bit variable a, followed by a sub-array of the four bits, an element taken from the bit variable b, and a further sub-array of two bits. We could also write this using named association:

   d_reg := (7 => a, 6 downto 3 => "1001",
             2 => b, 1 downto 0 => "00");

Note that when we write an aggregate containing a sub-array using named association, the choice for the sub-array must take the form of a discrete range, the number of choice values in the range must be the same as the number of elements in the sub-array, and the direction of the range must match the context in which the aggregate appears. Thus, in the preceding example, we used descending ranges for the choices, since the aggregate is assigned to a variable with a descending range.

Another place in which we may use an aggregate is the target of a variable assignment or a signal assignment. The full syntax rule for a variable assignment statement is

   variable_assignment_statement ⇐
      [ label : ] ( name | aggregate ) := expression ;

Aggregate target names can also be used in the conditional and selected forms of variable assignments. If the target is an aggregate, it must contain a variable name at each position. Furthermore, the expression on the right-hand side of the assignment must produce a composite value of the same type as the target aggregate. Each element of the right-hand side is assigned to the corresponding variable in the target aggregate. The variable names in the target aggregate can represent a combination of array elements and sub-arrays. For a sub-array variable, the corresponding elements of the right-hand side are assigned to the variable elements.

The full syntax rule for a signal assignment also allows the target to be in the form of an aggregate, with a signal name at each position in the aggregate. We can use assignments of this form to split a composite value among a number of scalar signals. For example, if we have a variable flag_reg, which is a four-element bit vector, we can perform the following signal assignment to four signals of type bit:

   ( z_flag, n_flag, v_flag, c_flag ) <= flag_reg;

Since the right-hand side is a bit vector, the target is taken as a bit-vector aggregate. The leftmost element of flag_reg is assigned to z_flag, the second element of flag_reg is assigned to n_flag, and so on. This form of multiple assignment is much more compact to write than four separate assignment statements.

As another example, suppose we have signals declared as follows:

   signal status_reg : bit_vector(7 downto 0);
   signal int_priority, cpu_priority : bit_vector(2 downto 0);
   signal int_enable, cpu_mode : bit;

where the type bit_vector is declared to be an array of bit elements (see Section 4.2). We can then write the assignment:

   (2 downto 0 => int_priority,
    6 downto 4 => cpu_priority,
    3 => int_en, 7 => cpu_mode) <= status_reg;

This specifies that the bits of the status_reg value are assigned in left-to-right order to cpu_mode, cpu_priority, int_en, and int_priority, respectively.

VHDL-87, -93, and -2002

These earlier versions to not allow aggregates with sub-arrays. Instead, each value in an aggregate must be an individual element value. Similarly, where an aggregate of names is used as the target of an assignment, each name must be an object of the same type as the elements of the right-hand side expression.

Array Attributes

In Chapter 2 we saw that attributes could be used to refer to information about scalar types. There are also attributes applicable to array types; they refer to information about the index ranges. Array attributes can also be applied to array objects, such as constants, variables and signals, to refer to information about the types of the objects. Given some array type or object A, and an integer N between 1 and the number of dimensions of A, VHDL defines the following attributes:

A‘left(N)

Left bound of index range of dimension N of A

A‘right(N)

Right bound of index range of dimension N of A

A‘low(N)

Lower bound of index range of dimension N of A

A‘high(N)

Upper bound of index range of dimension N of A

A‘range(N)

Index range of dimension N of A

A‘reverse_range(N)

Reverse of index range of dimension N of A

A‘length(N)

Length of index range of dimension N of A

A‘ascending(N)

true if index range of dimension N of A is an ascending range, false otherwise

A‘element

The element subtype of A

For example, given the array declaration

   type A is array (1 to 4, 31 downto 0) of boolean;

some attribute values are

   A'left(1) = 1                A'low(1) = 1
   A'right(2) = 0               A'high(2) = 31
   A'range(1) is 1 to 4         A'reverse_range(2) is 0 to 31
   A'length(1) = 4              A'length(2) = 32
   A'ascending(1) = true        A'ascending(2) = false
   A'element is boolean

For all of these attributes (except ‘element), to refer to the first dimension (or if there is only one dimension), we can omit the dimension number in parentheses, for example:

   A'low = 1          A'length = 4

In the next section, we see how these array attributes may be used to deal with array ports. We will also see, in Chapter 6, how they may be used with subprogram parameters that are arrays. Another major use is in writing for loops to iterate over elements of an array. For example, given an array variable free_map that is an array of bits, we can write a for loop to count the number of ‘1’ bits without knowing the actual size of the array:

   count := 0;
   for index in free_map'range loop
    if free_map(index) then
      count := count + 1;
    end if;
   end loop;

The ‘range and ‘reverse_range attributes can be used in any place in a VHDL model where a range specification is required, as an alternative to specifying the left and right bounds and the range direction. Thus, we may use the attributes in type and subtype definitions, in subtype constraints, in for loop parameter specifications, in case statement choices and so on. The advantage of taking this approach is that we can specify the size of the array in one place in the model and in all other places use array attributes. If we need to change the array size later for some reason, we need only change the model in one place.

The ‘element attribute allows us to declare objects of the same type as the elements of an array. We will see examples where that is useful in the next section.

VHDL-87, -93, and -2002

The array attribute ‘element is not provided in these earlier versions of VHDL.

VHDL-87

The array attribute ‘ascending is not provided in VHDL-87.

Unconstrained Array Types

The array types we have seen so far in this chapter are called constrained arrays, since the type definition constrains index values to be within a specific range. VHDL also allows us to define unconstrained array types, in which we just indicate the type of the index values, without specifying bounds. An unconstrained array type definition is described by the alternate syntax rule

   array_type_definition ⇐
      array ( ( type_mark range <> ) { , ... } ) of element_subtype_indication

The symbol “<>”, often called “box,” can be thought of as a placeholder for the index range, to be filled in later when the type is used. An example of an unconstrained array type declaration is

   type sample is array (natural range <>) of integer;

An important point to understand about unconstrained array types is that when we declare an object of such a type, we need to provide a constraint that specifies the index bounds. We can do this in several ways. One way is to provide the constraint when an object is created, for example:

   variable short_sample_buf : sample(0 to 63);

This indicates that index values for the variable short_sample_buf are natural numbers in the ascending range 0 to 63. Another way to specify the constraint is to declare a subtype of the unconstrained array type. Objects can then be created using this subtype, for example:

   subtype long_sample is sample(0 to 255);
   variable new_sample_buf, old_sample_buf : long_sample;

These are both examples of a new form of subtype indication that we have not yet seen. The syntax rule is

   subtype_indication ⇐ type_mark [ ( discrete_range { , ... } ) ]

The type mark is the name of the unconstrained array type, and the discrete range specifications constrain the index type to a subset of values used to index array elements. Each discrete range must be of the same type as the corresponding index type.

When we declare a constant of an unconstrained array type, there is a third way in which we can provide a constraint. We can infer it from the expression used to initialize the constant. If the initialization expression is an array aggregate written using named association, the index values in the aggregate imply the index range of the constant. For example, in the constant declaration

   constant lookup_table : sample
             := ( 1 => 23, 3 => -16, 2 => 100, 4 => 11);

the index range is 1 to 4.

If the expression is an aggregate using positional association, the index value of the first element is assumed to be the leftmost value in the array subtype. For example, in the constant declaration

   constant beep_sample : sample
             := ( 127, 63, 0, -63, -127, -63, 0, 63 );

the index range is 0 to 7, since the index subtype is natural. The index direction is ascending, since natural is defined to be an ascending range.

Predefined Array Types

VHDL predefines a number of unconstrained array types. In many models, these types are sufficient to represent our data. We list the predefined array types in this section.

Strings

VHDL provides a predefined unconstrained array type called string, declared as

   type string is array (positive range <>) of character;

In principle the index range for a constrained string may be either an ascending or descending range, with any positive integers for the index bounds. However, most applications simply use an ascending range starting from 1. For example:

   constant LCD_display_len : positive := 20;
   subtype LCD_display_string is string(1 to LCD_display_len);
   variable LCD_display : LCD_display_string := (others => ' '),

Boolean Vectors, Integer Vectors, Real Vectors, and Time Vectors

VHDL also provides predefined unconstrained types for arrays of boolean, integer, real, and time elements, respectively. They are declared as:

   type boolean_vector is array (natural range <>) of boolean;
   type integer_vector is array (natural range <>) of integer;
   type real_vector is array (natural range <>) of real;
   type time_vector is array (natural range <>) of time;

These types can be used to represent collections of data of the respective element types. For example, a subtype representing a collection of comparator thresholds can be declared as:

   subtype thresholds is integer_vector(15 downto 0);

Alternatively, we can supply the constraint for a vector when an object is declared, for example:

   variable max_temperatures : real_vector(1 to 10);

The time_vector type, in particular, is useful for specifying collection of timing parameters. For example, we can declare a constant representing individual propagation delays for each of eight output bits as follows:

   constant Tpd_result : time_vector
    := (0 to 3 => 100 ps, 4 to 7 => 150 ps);

VHDL-87, -93, and -2002

The types boolean_vector, integer_vector, real_vector, and time_vector are not predefined in these earlier versions of VHDL. Instead, they (or similar types) must be explicitly defined.

Bit Vectors

VHDL provides a further predefined unconstrained array type called bit_vector, declared as

   type bit_vector is array (natural range <>) of bit;

This type can be used to represent words of data at the architectural level of modeling. For example, subtypes for representing bytes of data in a little-endian processor might be declared as

   subtype byte is bit_vector(7 downto 0);

Alternatively, we can supply the constraint when an object is declared, for example:

   variable channel_busy_register : bit_vector(1 to 4);

Standard-Logic Arrays

The standard-logic package std_logic_1164 provides an unconstrained array type for vectors of standard-logic values. It is declared as

   type std_ulogic_vector is array ( natural range <> ) of std_ulogic;

This type can be used in a way similar to bit vectors, but provides more detail in representing the electrical levels used in a design. We can define subtypes of the standard-logic vector type, for example:

   subtype std_ulogic_word is std_ulogic_vector(0 to 31);

Or we can directly create an object of the standard-logic vector type:

   signal csr_offset : std_ulogic_vector(2 downto 1);

String and Bit-String Literals

In Chapter 1, we saw that a string literal may be used to write a value representing a sequence of characters. We can use a string literal in place of an array aggregate for a value of type string. For example, we can initialize a string constant as follows:

   constant ready_message  : string := "Ready        ";

We can also use string literals for any other one-dimensional array type whose elements are of an enumeration type that includes characters. The IEEE standard-logic array type std_ulogic_vector is an example. Thus we could declare and initialize a variable as follows:

   variable current_test : std_ulogic_vector(0 to 13)
             := "ZZZZZZZZZZ----";

In Chapter 1 we also saw bit-string literals as a way of writing a sequence of bit values. Bit strings can be used in place of array aggregates to write values of bit-vector types. For example, the variable channel_busy_register defined above may be initialized with an assignment:

   channel_busy_register := b"0000";

We can also use bit-string literals for other one-dimensional array types whose elements are of an enumeration type that includes the characters ‘0’ and ‘1’. Each character in the bit-string literal represents one, three or four successive elements of the array value, depending on whether the base specified in the literal is binary, octal or hexadecimal. Again, using std_ulogic_vector as an example type, we can write a constant declaration using a bit-string literal:

   constant all_ones : std_ulogic_vector(15 downto 0) := X"FFFF";

VHDL-87

In VHDL-87, bit-string literals may only be used as literals for array types in which the elements are of type bit. The predefined type bit_vector is such a type. However, the standard-logic type std_ulogic_vector is not. We may use string literals for array types such as std_ulogic_vector.

Unconstrained Array Element Types

In the preceding examples of unconstrained array types, the elements were all of scalar subtypes. In general, arrays can have elements of almost any type, including other array types. The array element types can themselves be constrained or unconstrained. Strictly, we just use the term unconstrained to refer to an array type in which the top-level type has its index range unspecified and the element type, if an array type, is also unconstrained. For example, the type sample that we declared as

   type sample is array (natural range <>) of integer;

is unconstrained, since its index range is unspecified and the element type is scalar rather than an array type. If we declare a type for a collection of samples:

   type sample_set is array (positive range <>) of sample;

this type also is unconstrained. The top-level array type, sample_set, has an unspecified index range, and the element type is unconstrained. There is no constraint information specified at any level of the type’s hierarchy.

We use the term fully constrained for a type in which all index ranges are constrained at all levels of the type’s hierarchy. For example, the type that we declared for points,

   type point is array (1 to 3) of real;

is fully constrained, since there is only one index range, and it is constrained to be 1 to 3. Similarly, if we declare a type for a line segment determined by two points:

   type line_segment is array (1 to 2) of point;

that type is also fully constrained. It constrains the top-level index range to 1 to 2 and the element index range to 1 to 3.

In between unconstrained and fully constrained types, we have what are called partially constrained types. Such types have one or more index ranges unspecified and others that are constrained. For example, we can declare a type for fixed-sized collections of samples:

   type dozen_samples is array (1 to 12) of sample;

The top-level index range is constrained to 1 to 12, but the element index range is unspecified. Similarly, we can declare a type for a path consisting of an unspecified number of points:

   type path is array (positive range <>) of point;

The top-level index range is unspecified, but the element index range is 1 to 3.

We mentioned that when we declare an object of an array type, we must provide constraints for index ranges. If the type used in the declaration is fully constrained, the constraints from the type provide sufficient information. On the other hand, if the type is unconstrained or partially constrained, we need to provide the missing constraints. We saw how to do that for the simple case of an unconstrained array of scalar elements. We do something similar for an unconstrained array with array elements. For example, to declare a variable of type sample_set to store 100 samples each of 20 values, we can write

   variable main_sample_set : sample_set(1 to 100)(1 to 20);

The first index range, 1 to 100, is used for the top level of the type’s hierarchy, and the second index range, 1 to 20, is used for the second level. We can use the same type for a second object with a different size:

   variable alt_sample_set : sample_set(1 to 50)(1 to 10);

This represents a collection of 50 samples, each with 10 values. Both variables are of the same type, sample_set, but they are of different sizes and have different element array sizes.

If we are to use a partially constrained type to declare an object, we specify constraints only for the index ranges not specified by the type. We can use the preceding notation for the case of the top-level index range being the one we have to specify. For example, we can declare a variable for a path connecting five points:

   variable short_path : path(5 downto 1);

The index range 5 down to 1 is used for the top level of the type’s hierarchy, and the constraint 1 to 3 for the second level comes from the type itelf.

For the case in which we need to specify only a second-level index range, we need to use the reserved word open in place of the top-level index range. An example is

   variable bakers_samples : dozen_samples(open)(0 to 9);

In this example, we write open for the top-level index range, since the type itself specifies 1 to 12 for that index range. Using the word open allows us to advance to the second-level index range, where we specify 0 to 9 as the constraint.

We can use this notation in subtype declarations as well as in declarations of objects. For example, if we want to declare several objects with the same constraints, we could write the subtype declaration

   subtype dozen_short_samples is dozen_samples(open)(0 to 9);

Since this subtype is fully constrained, we can use it to declare objects without adding any further constraint information.

When we declare a subtype, we do not need to constrain all of the index ranges to make a fully constrained subtype. We can add index constraints for some of the index ranges to declare partially constrained subtypes. For example:

   subtype short_sample_set is sample_set(open)(0 to 9);

In this case, we use the word open to skip over the unspecified index range at the top level and add a constraint at the second level. The top-level index range remains unspecified, and so the subtype is partially constrained. When using this subtype to declare an object, we would have to specify the top-level index range for the object, for example:

   variable unprocessed_samples : short_sample_set(1 to 20);

We mentioned earlier that, when declaring a constant of an unconstrained array type, we can infer the index range of the constant from the value of the expression used to initialize the constant. That applies not only to unconstrained arrays of scalar elements, but also to unconstrained arrays of array elements and to partially constrained arrays. For example, if we write:

   constant default_sample_pair : sample_set
             := ( 1 => (39, 25, 6, -4, 5),
                  2 => (9, 52, 100, 0, -1),
                  3 => (0, 0, 0, 0, 0) );

the top-level index range is inferred to be 1 to 3 (from the choices in the outer level of the aggregate), and the second-level index range is inferred to be 0 to 4 (from the fact that the index subtype of sample is natural). Similarly, if we write:

   constant default_samples : short_sample_set
             := ( 1 to 20 => (0, 0, 0, 0, 0, 0, 0, 0, 0, 0) );

the top-level index range is inferred to be 1 to 20 (from the choice in the outer level of the aggregate), but the second-level index range is 0 to 9, specified in the subtype.

Unconstrained Array Ports

An important use of an unconstrained array type is to specify the type of an array port. This use allows us to write an entity interface in a general way, so that it can connect to array signals of any size or with any range of index values. When we instantiate the entity, the index bounds of the array signal connected to the port are used as the bounds of the port. If the port type is an unconstrained array of unconstrained array elements, the index ranges at all levels of the type’s hierarchy come from the connected signal.

Example 4.4. Multiple-input and gates

Suppose we wish to model a family of and gates, each with a different number of inputs. We declare the entity interface as shown below. The input port is of the unconstrained type bit_vector. The architecture body includes a process that is sensitive to changes on the input port. When any element changes, the process performs a logical and operation across the input array. It uses the ‘range attribute to determine the index range of the array, since the index range is not known until the entity is instantiated.

   entity and_multiple is
    port ( i : in bit_vector;  y : out bit );
   end entity and_multiple;
   --------------------------------------------------
   architecture behavioral of and_multiple is
   begin
    and_reducer : process ( i ) is
      variable result : bit;
    begin
      result := '1';
      for index in i'range loop
        result := result and i(index);
      end loop;
      y <= result;
    end process and_reducer;
   end architecture behavioral;

To illustrate the use of the multiple-input gate entity, suppose we have the following signals:

   signal count_value : bit_vector(7 downto 0);
   signal terminal_count : bit;

We instantiate the entity, connecting its input port to the bit-vector signal:

   tc_gate : entity work.and_multiple(behavioral)
    port map ( i => count_value, y => terminal_count);

For this instance, the input port is constrained by the index range of the signal. The instance acts as an eight-input and gate.

Example 4.5. Multiple-input and-or-invert gates

We can model a family of and-or-invert gates with varying numbers of groups of input bits. Each group of input bits is and-ed, the results are or-ed, and the final result negated to derive the output. The inputs are represented using an array of arrays:

   type bv_array is array (natural range <>) of bit_vector;

Each array element of type bit_vector is a group of bits that are to be and-ed together. The entity and architecture are:

   entity and_or_invert is
    port ( a : in bv_array; z : out bit );
   end entity and_or_invert;
   architecture behavioral of and_or_invert is
    reducer : process ( a ) is
      variable and_result, result : bit;
    begin
      result := '0rsquo;;
      for i in A'range loop
        and_result := '1';
        for j in A'element'range loop
          and_result := and_result and a(i)(j);
        end loop;
        result := result or and_result;
      end loop;
      z <= not result;
    end process reducer;
   end architecture behavioral;

The reducer process contains an outer loop that iterates over the array of groups. It uses the input array’s top-level index range to govern the loop parameter. The inner loop iterates over the bits in a group represented by a particular element of a. This loop uses the index range of the elements of a, given by the ‘range attribute of the ‘element attribute of a. Note that we could have used the reducing and operator, described in Section 4.3.1, in place of the inner loop, as follows:

   and_result := and a(i);

For this example, however, we have included the loop to illustrate use of the ‘element attribute in cases where the index ranges of array elements are determined by the signal connected to the port of an instance.

We can also use partially constrained types for ports of entities. In such cases, the index ranges of the signal connected to the port are used to determine the those bounds of the port that are not specified in the port’s subtype. For example, if we declare an entity using the subtype short_sample_set subtype from Section 4.2.2:

   entity sample_processor is
    port ( samples : in short_sample_set; ... );
   end entity sample_processor;

and instantiate it as follows:

   signal sensor_samples : short_sample_set(1 to 3);
   ...
   preprocessor : entity sample_processor
    port map ( samples => sensor_samples, ... );

then, for the preprocessor instance, the top-level index range of the samples port would be 1 to 3, obtained from the samples signal. The second-level index range for the samples port would be 0 to 9, obtained from the subtype short_sample_set, the declared subtype of the port.

Array Operations and Referencing

Although an array is a collection of values, much of the time we operate on arrays one element at a time, using the operators described in Chapter 2. However, if we are working with one-dimensional arrays of scalar values, we can use some of the operators to operate on whole arrays. In this section, we describe the operators that can be applied to arrays and introduce a number of other ways in which arrays can be referenced.

Logical Operators

The logical operators (and, or, nand, nor, xor and xnor) can be applied to two one-dimensional arrays of bit or Boolean elements. That includes the predefined types bit_vector and boolean vector, as well as any array types with bit or boolean elements that we might declare. The operators can also be applied to two arrays of type std_ulogic_vector defined by the std_logic_1164 package. In each case, the operands must be of the same length and type, and the result is computed by applying the operator to matching elements from each array to produce an array of the same length. Elements are matched starting from the leftmost position in each array. An element at a given position from the left in one array is matched with the element at the same position from the left in the other array. The operator not can also be applied to a single array of any of these types, with the result being an array of the same length and type as the operand. The following declarations and statements illustrate this use of logical operators when applied to bit vectors:

   subtype pixel_row is bit_vector (0 to 15);
   variable current_row, mask : pixel_row;

   current_row := current_row and not mask;
   current_row := current_row xor X"FFFF";

The logical operators and, or, nand, nor, xor and xnor can also be applied to a pair of operands, one of which is a one-dimensional arrays of the types mentioned above, and the other of which is a scalar of the same type as the array elements. The result is an array of the same length and type as the array operand. Each element of the result is computed by applying the operator to the scalar operand and the corresponding element of the array operand. Some examples are

   B"0011" and '1' = B"0011" B"0011" and '0' = B"0000"

   B"0011" xor '1' = B"1100" B"0011" xor '0' = B"0011"

Example 4.6. Select signal for a data bus

Suppose the outputs of three registers are provided on std_ulogic_vector signals a, b, and c. We can use three select signals, a_sel, b_sel and c_sel, to select among the register outputs for assignment to a data bus signal. The declarations and statements are

   signal a, b, c, data_bus : std_ulogic_vector(31 downto 0);
   signal a_sel, b_sel, c_sel : std_ulogic;
   ...
   data_bus <= (a and a_sel) or (b and b_sel) or (c and c_sel);

Further, these operators can be applied in unary form to a single one-dimensional array of the types mentioned above, reducing the array to a single scalar result of the same type as the array elements. The reduction and, or, and xor operators form the logical and, or, and exclusive or, respectively of the array elements. Thus:

   and "0110" = '0' and '1' and '1' and '0' = '0'
    or "0110" = '0'  or '1'  or '1'  or '0' = '1'
   xor "0110" = '0' xor '1' xor '1' xor '0' = '0'

In each case, if the array has only one element, the result is the value of that element. If the array is a null array (that is, it has no elements), the result of the and operator is ‘1’, and the result of the or and xor operators is ‘0’.

The reduction nand, nor, and xnor operators are the negation of the reduction and, or, and xor operators, respectively. Thus:

   nand "0110" = not ('0" and '1' and '1' and '0') = not '0' = '1'
    nor "0110" = not ('0"  or '1'  or '1'  or '0') = not '1' = '0'
   xnor "0110" = not ('0" xor '1' xor '1' xor '0') = not '0' = '1'

In each case, application to a single-element array produces the negation of the element value. Application of nand to a null array produces ‘0’ and application of nor or xnor to a null array produces ‘1’.

The logical reduction operators have the same precedence as the unary not and abs operators. In the absence of parentheses, they are evaluated before binary operators. So the expression:

   and A or B

involves applying the reduction and operator to A, then applying the binary or operator to the result and B. In some cases, we need to include parentheses to make an expression legal. For example, the expression:

   and not X

is not legal without parentheses, since we cannot chain unary operators. Instead, we must write the expression as:

   and (not X)

Example 4.7. Parity reduction

Given a signal data of type bit_vector, we can calculate parity using the reduction xor operator:

   parity <= xor data;

Since reduction operators have higher precedence than binary logical operators, the following two statements produce the same value:

   parity_error1 <= (xor data) and received_parity;
   parity_error2 <= xor data and received_parity;

However, the parentheses make the first form more readable.

VHDL-87

The logical operator xnor is not provided in VHDL-87.

Shift Operators

The shift operators introduced in Chapter 2 (sll, srl, sla, sra, rol and ror) can be used with a one-dimensional array of bit or Boolean values as the left operand and an integer value as the right operand. The sll, srl, rol and ror operators can be used with a left operand of type std_ulogic_vector, defined in the std_logic_1164 package. A shift-left logical operation shifts the elements in the array n places to the left (n being the right operand), filling in the vacated positions with ‘0’ or false and discarding the leftmost n elements. If n is negative, the elements are instead shifted to the right. Some examples are

   B"10001010" sll 3 = B"01010000"    B"10001010" sll -2 = B"00100010"

The shift-right logical operation similarly shifts elements n positions to the right for positive n, or to the left for negative n, for example:

   B"10010111" srl 2 = B"00100101"    B"10010111" srl -6 = B"11000000"

The next two shift operations, shift-left arithmetic and shift-right arithmetic, operate similarly, but instead of filling vacated positions with ‘0’ or false, they fill them with a copy of the element at the end being vacated, for example:

   B"01001011" sra 3 = B"00001001"    B"10010111" sra 3 = B"11110010"
   B"00001100" sla 2 = B"00110000"    B"00010001" sla 2 = B"01000111"

As with the logical shifts, if n is negative, the shifts work in the opposite direction, for example:

   B"00010001" sra -2 = B"01000111"    B"00110000" sla -2 = B"00001100"

A rotate-left operation moves the elements of the array n places to the left, transferring the n elements from the left end of the array around to the vacated positions at the right end. A rotate-right operation does the same, but in the opposite direction. As with the shift operations, a negative right argument reverses the direction of rotation. Some examples are

   B"10010011" rol 1 = B"00100111"    B"10010011" ror 1 = B"11001001"

VHDL-87

The shift operators sll, srl, sla, sra, rol and ror are not provided in VHDL-87.

Relational Operators

Relational operators can also be applied to one-dimensional arrays. For the ordinary relation operators (“=”, “/=”, “<”, “<=”, “>” and “=>”), the array elements can be of any discrete type. The two operands need not be of the same length, so long as they have the same element type. The “=” and “/=” operators are quite straightforward. If two arrays have the same length and the corresponding elements are pairwise equal, then the arrays are equal. The “/=” operator is simply the negation of the “=” operator. The way the remaining operators work can be most easily seen when they are applied to strings of characters, in which case they are compared according to case-sensitive dictionary ordering.

To see how dictionary comparison can be generalized to one-dimensional arrays of other element types, let us consider the “<” operator applied to two arrays a and b. If both a and b have length 0, a < b is false. If a has length 0, and b has non-zero length, then a < b. Alternatively, if both a and b have non-zero length, then a < b if a(1) < b(1), or if a(1) = b(1) and the rest of a < the rest of b. In the remaining case, where a has non-zero length and b has length 0, a < b is false. Comparison using the other relational operators is performed analogously.

An important point that follows from this definition of “<” ordering is that, if we apply it to bit vectors representing binary-coded numbers, it does not correspond to numeric ordering. For example, consider the comparison B“001000” < B“10”. Since the first element of the left operand is ‘0’ and the first element of the right operand is ‘1’, and since ‘0’ < ‘1’, the result of the comparison is true. However, if we interpret the vectors as unsigned binary numbers, the left operand represents 8 and the right operand represents 2. We would then expect the comparison to yield false. Clearly, the predefined relational operators are inappropriate for this interpretation of bit vectors. We will see in Chapter 9 how we can perform the right kind of comparisons on binary-coded numbers.

In Section 2.2.5, we introduced the matching equality (“?=”) and inequality (“?/=”) operators for comparing bit or std_ulogic operands. These operators can also be applied to operands that are one-dimensional arrays of bit or std_ulogic elements to yield a std_ulogic result. The operands must be of the same type and length. For the “?=” operator, corresponding elements are compared using the scalar version of the operator, and the results reduced using the logical and reduction operator. The “?/=” operator is computed similarly, with the not operator applied to the reduced and result.

Example 4.8. Chip-select and address decoding

We can write a Boolean equation for a std_ulogic select signal that includes chip-select control signals and an address signal. We can use the “?=” operator, which returns a std_ulogic result. We can combine that result with the std_ulogic control signals to produce a std_ulogic form of the Boolean equation:

   dev_sel1 <= cs1 and not ncs2 and addr ?= X"A5";

We can also use this form of expression in a condition, since the value is implictly converted to boolean (see Section 2.2.5):

   if cs1 and not ncs2 and addr ?= X"A5" then
     ...

or similarly:

   if cs1 and ncs2 ?= '0' and addr ?= X"A5" then
    ...

VHDL-87, -93, and -2002

The matching relational operators are not provided in these versions of VHDL. The assignment in the above example would have to be written as:

   dev_sel1 <= '1' when cs1 = '1' and
                       ncs2 = '0' and addr = X"A5" else '0';

Similarly, since condition values are not implicitly converted to boolean, the conditions in the if statements would be written as:

   if cs1 = '1' and ncs2 = '0' and addr = X"A5" then
    ...

or

   if (cs1 and not ncs2) = '1' and addr = X"A5" then
    ...

Maximum and Minimum Operations

In Chapter 2, we introduced the maximum and minimum operations for scalar types. These operations can also be applied to arrays of discrete-type elements. The “<” operator is defined for such arrays, and the maximum and minimum operations are defined in terms of the “<” operator. Thus, for example:

   minimum(B"0001", B"0110") = B"0001"
   maximum(B"001000", B"10") = B"10"

Note that the same argument that we made above about comparing binary-coded numbers using the “<” operator applies to use of the maximum and minimum operations. Again, we will see in Chapter 9 how to perform the operation correctly for binary-coded numbers.

The maximum and minimum operations are further defined as reduction operations on one-dimensional arrays of any scalar type. The maximum function of this form returns the largest element in the array, and the minimum function returns the smallest element in the array. Again, the comparisons are performed using the predefined “<” operator for the element type. Thus,

   maximum(string'("WXYZ")) = 'Z'
   minimum(string'("WXYZ")) = 'W'
   maximum(time_vector'(10 ns, 50 ns, 20 ns)) = 50 ns
   minimum(time_vector'(10 ns, 50 ns, 20 ns)) = 10 ns

For a null array (one with no elements), the maximum function returns the smallest value of the element type, and the minimum function returns the largest value of the element type.

VHDL-87, -93, and -2002

The maximum and minimum operations are not provided in earlier versions of VHDL.

The Concatenation Operator

The one remaining operator that can be applied to one-dimensional arrays is the concatenation operator (“&”), which joins two array values end to end. For example, when applied to bit vectors, it produces a new bit vector with length equal to the sum of the lengths of the two operands. Thus, b“0000” & b“1111” produces b“0000_1111”.

The concatenation operator can be applied to two operands, one of which is an array and the other of which is a single scalar element. It can also be applied to two scalar values to produce an array of length 2. Some examples are

   "abc" & 'd'   = "abcd"
   'w'   & "xyz" = "wxyz"
   'A'   & 'b'   = "ab"

To_String Operations

In Section 2.5, we described the predefined to_string operation on scalar types. To_string can also be applied to values of one-dimensional array types that contain only character-literal elements. Examples of such types include the predefined types bit_vector and string and the type std_ulogic_vector defined in std_logic_1164. Applying to_string to a string value is not of much use, since it just yields the operand unchanged. For other array types, the operation yields a string value with the same characters as the operand. This can be useful for including such array values in message strings. For example:

   signal x : bit_vector(7 downto 0);
   ...
   report "Trace: x = " & to_string(x);

The value of x is an array of bit elements, whereas the report statement expects a string value, which is an array of character elements. The to_string operation deals with the conversion. Thus, if x has the bit_vector value “00110101”, the result of the to_string operation would be the character string “00110101”.

VHDL also provides operations, to_ostring and to_hstring, for converting bit_vector operands to strings in octal and hexadecimal form, respectively. To_ostring takes each group of three bits from the operand, starting from the right, and includes the corresponding octal-digit character in the result. If the operand is not a multiple of three in length, additional ‘0’ bits are assumed on the left of the operand. Some examples are:

   to_ostring(B"101_011_000") = "530"
   to_ostring( B"11_000_111") = "307"

The to_hstring operation similarly takes each group of four bits and includes the corresponding hexadecimal-digit character in the result. The digits A to F appear in uppercase. Some examples are:

   to_hstring(B"0110_1100") = "6C"
   to_hstring(  B"11_0101") = "35"

Note that we don’t necessarily need a separate to_bstring operation, since the to_string operation would serve that purpose. However, in the interest of completeness and consistency, VHDL does provide a to_bstring operation with exactly the same behavior. (In fact, to_bstring is an alias for to_string. We discuss aliases in detail in Chapter 11.) In addition, VHDL provides alternate names for all of these operations: to_binary_string, to_octal_string and to_hex_string. Some designers may consider these to be more readable than the shorter names. Their use is a matter of taste or organizational coding style.

Array Slices

Often we want to refer to a contiguous subset of elements of an array, but not the whole array. We can do this using slice notation, in which we specify the left and right index values of part of an array object. For example, given arrays a1 and a2 declared as follows:

   type array1 is array (1 to 100) of integer;
   type array2 is array (100 downto 1) of integer;

   variable a1 : array1;
   variable a2 : array2;

we can refer to the array slice a1(11 to 20), which is an array of 10 elements having the indices 11 to 20. Similarly, the slice a2(50 downto 41) is an array of 10 elements but with a descending index range. Note that the slices a1(10 to 1) and a2(1 downto 10) are null slices, since the index ranges specified are null. Furthermore, the ranges specified in the slice must have the same direction as the original array. Thus we may not legally write a1(10 downto 1) or a2(1 to 10).

Example 4.9. A byte-swapper module

We can write a behavioral model for a byte-swapper that has one input port and one output port, each of which is a bit vector of subtype halfword, declared as follows:

   subtype halfword is bit_vector(0 to 15);

The entity and architecture are:

   entity byte_swap is
    port (input : in halfword;  output : out halfword);
   end entity byte_swap;
   --------------------------------------------------
   architecture behavior of byte_swap is
   begin
    swap : process (input)
    begin
      output(8 to 15) <= input(0 to 7);
      output(0 to 7) <= input(8 to 15);
    end process swap;
   end architecture behavior;

The process in the architecture body swaps the two bytes of input with each other. It shows how the slice notation can be used for signal array objects in signal assignment statements.

VHDL-87

In VHDL-87, the range specified in a slice may have the opposite direction to that of the index range of the array. In this case, the slice is a null slice.

Array Type Conversions

In Chapter 2 we introduced the idea of type conversion of a numeric value of one type to a value of a so-called closely related type. A value of an array type can also be converted to a value of another array type, provided the array types are closely related. Two array types are closely related if they have the same number of dimensions and their element types can be converted (that is, the element types are closely related). The type conversion simply produces a new array value of the specified type containing the converted elements of the original array in the same order.

To illustrate the idea of type-converting array values, suppose we have the following declarations in a model:

   subtype name is string(1 to 20);
   type display_string is array (integer range 0 to 19) of character;

   variable item_name : name;
   variable display : display_string;

We cannot directly assign the value of item_name to display, since the types are different. However, we can write the assignment using a type conversion:

   display := display_string(item_name);

This produces a new array, with the left element having index 0 and the right element having index 19, which is compatible with the assignment target.

The rule that the element types of the converted expression and the target type must be convertible allows us to perform the following conversions:

   subtype integer_vector_10 is integer_vector(1 to 10);
   subtype real_vector_10 is real_vector(1 to 10);

   variable i_vec : integer_vector_10;
   variable r_vec : real_vector_10;
   ...

   i_vec := integer_vector_10(r_vec);
   r_vec := real_vector_10(i_vec);

Since we can convert between values of type integer and real, we can also convert between arrays with integer and real elements, respectively. Each element of the converted expression is converted to the element subtype of the target type.

The index ranges need not all be numeric for a type conversion to be performed. For example, suppose we have array types and signals declared as follows:

   type exception_type is (int, ovf, div0, undef, trap);
   type exception_vector is array (exception_type) of bit;
   signal d_in, d_out: bit_vector(31 downto 0);
   signal exception_reg : exception_vector;

Then the type conversion:

   exception_vector( d_in(4 downto 0) )

yields a vector of bits indexed from int to trap, with each element being the matching element of the slice of d_in, from left to right. Since the element types for the expression and the target type are both bit, conversion of the elements is trivial.

The above examples illustrate the case of a type conversion in which the target type is fully constrained, specifying the index range for the result. In general, we can convert to a target type that is fully constrained, partially constrained, or unconstrained. (We described array index constraints in Section 4.2.2.) If the target type of the conversion specifies an index range at any level of the type’s hierarchy, that index range is used, as in the examples. On the other hand, if the target type leaves some index range or ranges unspecified (that is, the target type is unconstrained or partially constrained), the correponding index range or ranges for the result depend on the index subtypes. If the index subtypes of the original array and the target type are both numeric, the index range of the result has the same numeric bounds and direction as the index range of the original array. For example, in the type conversion

   integer_vector(r_vec)

the index range of the result is 1 to 10, taken from the index range of r_vec. If, however, one or both index ranges are enumeration types, the index range of the result is determined from the index subtype of the target type. The direction is the same as that of the index subtype, the left bound is the leftmost value in the index subtype, and the right bound depends on the number of elements. For example, in the type conversion

   bit_vector( exception_reg )

the index range of the result comes from the index subtype defined for bit_vector, namely, natural. The subtype natural is declared to be an ascending range with a left bound of 0. This direction and left bound are used as the direction and left bound of the type-conversion result. The right bound comes from the number of elements. Thus, the result is a bit_vector value indexed from 0 to 4.

A common case in which we do not need a type conversion is the assignment of an array value of one subtype to an array signal or variable of a different subtype of the same base type. This occurs where the index ranges of the target and the operand have different bounds or directions. VHDL automatically includes an implicit subtype conversion in the assignment. For example, given the subtypes and variables declared thus:

   subtype big_endian_upper_halfword is bit_vector(0 to 15);
   subtype little_endian_upper_halfword is bit_vector(31 downto 16);

   variable big : big_endian_upper_halfword;
   variable little : little_endian_upper_halfword;

we could make the following assignments without including explicit type conversions:

   big := little;
   little := big;

A final point to make about conversions relates to type qualification, introduced in Section 2.3.2. There, we mentioned that the operand of a type qualification has to be of the specified type, but not necessarily of the specific subtype. In the case of type qualification for arrays, we can qualify an array operand as being of some different subtype to that of the operand. The operand is then converted to that subtype, as described above.

VHDL-87, -93, and -2002

The rules for array type conversions in these earlier versions of VHDL were more strict. Array types were only closely related if they had the same element type, the same number of dimensions and index types that could be type converted. The restriction on index subtypes effectively meant that both had to be numeric or both had to be the same enumeration type. In addition, for type qualification, no subtype conversion was performed. If the qualified type was a constrained array subtype, the operand had to have exactly the index range or ranges specified in the subtype.

Arrays in Case Statements

In Section 3.2, we introduced case statements, and described a number of requirements on the selector expression and choices. In our examples, we just used scalar types for the selector expression. However, VHDL also allows us to write a selector expression of a one-dimensional character array type, that is, an array type whose element type includes character literals. Examples of such types are bit_vector, std_ulogic_vector, and similar types. The choices are typically string literals, bit-string literals, or, less commonly, aggregates, and all must have the same length. When the case statement is executed, the value of the selector expression is checked to ensure it has the same length as the choices, and then compared with the choices to determine which alternative to execute. When the case statement is executed, the value of the expression must have the same length as the choices. For example, in the following:

   variable s : bit_vector(3 downto 0);
   variable c : bit;
   ...
   case c & s is
    when "00000" => ...
    when "10000" => ...
    when others  => ...
   end case;

all of the choices (except the others choice) are of length five, so that determines the required length for the result of the concatenation.

We saw in Section 4.1.2 that we can include the word others in an array aggregate to refer to all elements not identified in the preceding part of the aggregate. If we write an aggregate containing others as a choice in a case statement, the index range of the case expression must be locally static, that is, determined during the analysis phase of design processing. For example, we can write a case statement as follows:

   variable s : bit_vector(3 downto 0);
   ...
   case s is
    ('0', others => '1') => ...
    ('1', others => '0') => ...
    ...
   end case;

In this example, the index range of the expression s can be determined at analysis time as being 3 downto 0. That means the analyzer can use the index range for the choice values. If the analyzer cannot work out the index range for the case expression, it cannot determine the index values represented by others in the aggregates.

VHDL-87, -93, and -2002

In these versions of VHDL, if the selector expression in a case statement was of an array type, the index range had to be locally static, regardless of whether the choices used the word others or not. Thus, the expression c & s in the example above would be illegal. Instead, we would have to write the example as:

   variable s : bit_vector(3 downto 0);
   variable c : bit;
   subtype bv5 is bit_vector(0 to 4);
   ...
   case bv5'(c & s) is
    ...
   end case;

Matching Case Statements

In the previous subsection, we saw how we can write a case statement with a selector expression of an array type. The choices are compared for exact equality with the expression value to select an alternative to execute. If the type of the case expression and choices is a vector of std_ulogic values, exact comparison is not always what we want. In particular, we would like to be able to include don’t care elements (‘–’) in the choices to indicate that we don’t care about some elements of the selector expression when selecting an alternative. We can do this with a new form of case statement, called a matching case statement, that uses the predefined “?=” operator described in Section 4.3.3 to compare choice values with the expression value. We include a question mark symbol after the keyword case, as follows:

   case? expression is
    ...
   end case?;

The most common use of a matching case statement is with an expression of a vector type whose elements are std_ulogic values, such as the standard type std_ulogic_vector defined in the std_logic_1164 package. It also includes vector types that we might define. With a case expression of such a type, we can write choice values that include ‘–’ elements to specify don’t care matching.

Example 4.10. A priority arbiter

Suppose we have vectors of request and grant values, declared as follows:

   variable request, grant : std_ulogic_vector(0 to 3);

We can use a matching case statement in a priority arbiter, with request 0 having highest priority:

   case? request is
    when "1---" => grant := "1000";
    when "01--" => grant := "0100";
    when "001-" => grant := "0010";
    when "0001" => grant := "0001";
    when others => grant := "0000";
   end case?;

Each choice is compared with the case expression using the predefined “?=” operator. Thus, the first choice matches values “1000”, “1001”, “100X”, “H000”, and so on, and similarly for the remaining choices. This is a much more succinct way of describing the arbiter than using an ordinary case statement. Moreover, unlike a sequence of tests in an if statement, it does not imply chained decision logic.

When we use a matching case statement with a vector-type expression, the value of the expression must not include any ‘–’ elements. (This is different from the choice values, which can include ‘–’ elements.) The reason is that an expression value with a ‘–’ element would match multiple choice values, making selection of an alternative ambiguous. Normally, this rule is not a problem, since we don’t usually assign ‘–’ values to signals or variables. They usually just occur in literal values for comparison and in test bench assertions.

In an ordinary case statement, we need to include choices for all possible values of the case expression. A related rule applies in a matching case statement. Each possible value of the case expression, except those that include any ‘–’ elements, must be represented by exactly one choice. By “represented,” we mean that comparison of the choice and the expression value using the “?=” operator yields ‘1’. Hence, our choice values would generally just include ‘0’, ‘1’, and ‘–’ elements, matching with ‘0’, ‘L’, ‘1’, ‘H’ elements in the case expression value. We could also include ‘L’ and ‘H’ elements in a choice. However, we would not include ‘U’, ‘X’, ‘W’, or ‘Z’ choice elements, since they only ever produce ‘U’ or ‘X’ results, and so never match. As with an ordinary case statement, we can include an others choice to represent expression values not otherwise represented. Unlike an ordinary case statement, a choice can represent multiple expression values if it contains a ‘–’ element.

We mentioned that a vector type including std_ulogic values is the most common type for a matching case statement. Less commonly, we can write a selector expression of type std_ulogic, bit, or a vector of bit elements (such as bit_vector). These are the other types for which the “?=” operator is predefined. For std_ulogic expressions, the choice values would typically be either ‘0’ (matching an expression value of ‘0’ or ‘L’) or ‘1’ (matching an expression value of ‘1’ or ‘H’). We would not write a choice of ‘–’, since that would match all expression values, preventing us from selecting distinct alternatives. For case expressions of type bit or a vector of bit elements, a matching case statement has exactly the same behavior as an ordinary case statement. VHDL allows matching case statements of this form to allow synthesizable models to be written uniformly regardless of whether bit or std_ulogic data types are used.

VHDL-87, -93, and -2002

These versions of VHDL do not provide matching case statements.

Matching Selected Variable Assignments

Selected variable assignments, introduced in Section 3.2.1, are a shorthand notation for variable assignments within case statements. There is an analogous shorthand for variable assignments within matching case statements. We simply include a “?” symbol after the select keyword to indicate that the implied case statement is a matching case statement instead of an ordinary case statement. The rules covering the type of the selector expression and the way in which choices are matched then apply to the selected assignment.

Example 4.11. A revised model for the priority arbiter

We can rewrite the priority arbiter from Example 4.10 using a matching selected assignment as follows:

   with request select?
    grant := "1000" when "1---",
             "0100" when "01--",
             "0010" when "001-",
             "0001" when "0001",
             "0000" when others;

VHDL-87, -93, and -2002

These versions of VHDL do not provide the matching selected variable assignment notation.

Records

In this section, we discuss the second class of composite types, records. We start with record types, and return to record natures subsequently, since there are some significant differences between them. A record is a composite value comprising elements that may be of different types from one another. Each element is identified by a name, which is unique within the record. This name is used to select the element from the record value. The syntax rule for a record type definition is

   record_type_definition ⇐
      record
          ( identifier { , ... } : subtype_indication ; )
          { ... }
      end record [ identifier ]

Each of the names in the identifier lists declares an element of the indicated type or subtype. Recall that the curly brackets in the syntax rule indicate that the enclosed part may be repeated indefinitely. Thus, we can include several elements of different types within the record. The identifier at the end of the record type definition, if included, must repeat the name of the record type.

VHDL-87

The record type name may not be included at the end of a record type definition in VHDL-87.

The following is an example record type declaration and variable declarations using the record type:

   type time_stamp is record
      seconds : integer range 0 to 59;
      minutes : integer range 0 to 59;
      hours : integer range 0 to 23;
    end record time_stamp;
   variable sample_time, current_time : time_stamp;

Whole record values can be assigned using assignment statements, for example:

   sample_time := current_time;

We can also refer to an element in a record using a selected name, for example:

   sample_hour := sample_time.hours;

In the expression on the right of the assignment symbol, the prefix before the dot names the record value, and the suffix after the dot selects the element from the record. A selected name can also be used on the left side of an assignment to identify a record element to be modified, for example:

   current_time.seconds := clock mod 60;

Example 4.12. Representing CPU instructions and data using records

In the early stages of designing a new instruction set for a CPU, we don’t want to commit to an encoding of opcodes and operands within an instruction word. Instead we use a record type to represent the components of an instruction. We illustrate this in an outline of a system-level behavioral model of a CPU and memory that uses record types to represent instructions and data:

   architecture system_level of computer is

    type opcodes is
           (add, sub, addu, subu, jmp, breq, brne, ld, st, ...);
    type reg_number is range 0 to 31;
    constant r0 : reg_number := 0;  constant r1 : reg_number := 1;
    ...
    type instruction is record
        opcode : opcodes;
        source_reg1, source_reg2, dest_reg : reg_number;
        displacement : integer;
      end record instruction;

    type word is record
        instr : instruction;
        data : bit_vector(31 downto 0);
      end record word;

    signal address : natural;
    signal read_word, write_word : word;
    signal mem_read, mem_write : bit := '0';
    signal mem_ready : bit := '0';

   begin
    cpu : process is
      variable instr_reg : instruction;
      variable PC : natural;
      ...    -- other declarations for register file, etc.
    begin
      address <= PC;
      mem_read <= '1';
      wait until mem_ready;
      instr_reg := read_word.instr;
      mem_read <= '0';
      PC := PC + 4;
      case instr_reg.opcode is  -- execute the instruction
        ...
      end case;
    end process cpu;

    memory : process is
      subtype address_range is natural range 0 to 2**14 - 1;
      type memory_array is array (address_range) of word;
      variable store : memory_array :=
        (  0  =>    ( ( ld, r0, r0, r2, 40 ),  X"00000000" ),
           1  =>    ( ( breq, r2, r0, r0, 5 ), X"00000000" ),
          ...
          40  =>    ( ( nop, r0, r0, r0, 0 ),  X"FFFFFFFE"),
          others => ( ( nop, r0, r0, r0, 0 ),  X"00000000") );
    begin
      ...
    end process memory;

   end architecture system_level;

The record type instruction represents the information to be included in each instruction of a program and includes the opcode, source and destination register numbers and a displacement. The record type word represents a word stored in memory. Since a word might represent an instruction or data, elements are included in the record for both possibilities. Unlike many conventional programming languages, VHDL does not provide variant parts in record values. The record type word illustrates how composite data values can include elements that are themselves composite values. The signals in the model are used for the address, data and control connections between the CPU and the memory.

Within the CPU process the variable instr_reg represents the instruction register containing the current instruction to be executed. The process fetches a word from memory and copies the instruction element from the record into the instruction register. It then uses the opcode field of the value to determine how to execute the instruction.

The memory process contains a variable that is an array of word records representing the memory storage. The array is initialized with a program and data. Words representing instructions are initialized with a record aggregate containing an instruction record aggregate and a bit vector, which is ignored. Similarly, words representing data are initialized with an aggregate containing an instruction aggregate, which is ignored, and the bit vector of data.

Record Aggregates

We can use a record aggregate to write a literal value of a record type, for example, to initialize a record variable or constant. Using a record aggregate is analogous to using an array aggregate for writing a literal value of an array type (see Section 4.1.2). A record aggregate is formed by writing a list of the elements enclosed in parentheses. An aggregate using positional association lists the elements in the same order as they appear in the record type declaration. For example, given the record type time_stamp shown above, we can initialize a constant as follows:

   constant midday : time_stamp := (0, 0, 12);

We can also use named association, in which we identify each element in the aggregate by its name. The order of elements identified using named association does not affect the aggregate value. The example above could be rewritten as

   constant midday : time_stamp
             := (hours => 12, minutes => 0, seconds => 0);

Unlike array aggregates, we can mix positional and named association in record aggregates, provided all of the named elements follow any positional elements. We can also use the symbols “|” and others when writing choices. Here are some more examples, using the types instruction and time_stamp declared above:

   constant nop_instr : instruction :=
      ( opcode => addu,
        source_reg1 | source_reg2 | dest_reg => 0,
        displacement => 0 );
   variable latest_event : time_stamp
             := (others => 0); -- initially midnight

Note that unlike array aggregates, we can’t use a range of values to identify elements in a record aggregate, since the elements are identified by names, not indexed by a discrete range.

Unconstrained Record Element Types

In Section 4.4.2, we showed how the element type of an array type can be unconstrained, leading to unconstrained and partially constrained array types. In a similar way, an element of a record type can be of an unconstrained or partially constrained composite type. We use the terms unconstrained, partially constrained, and fully constrained to describe a record type in an analogous way to the use of the terms for array types. Specifically, an unconstrained record type is one that has one or more elements of composite types, all of which are unconstrained. For example, the following type

   type test_vector is record
      id : natural;
      stimulus : bit_vector;
      response : bit_vector;
    end record test_vector;

is unconstrained, since the elements stimulus and response are both of an unconstrained array type.

A fully constrained record type is one in which all composite elements (if any) are fully constrained. For example, the types instruction and word in Example 4.12 are both fully constrained. The type instruction has no composite elements, so there is no place in the type’s hierarchy where a constraint is needed. The type word has one element, data, of a composite subtype, which is fully constrained.

As with array types, a partially constrained record type is one that is neither unconstrained nor fully constrained. It may have a mix of unconstrained, partially constrained, and fully constrained elements, or it may just have one or more partially constrained elements. For example, if we declare a fully constrained record type

   type test_times is record
      stimulus_time : time;
      response_delay : delay_length;
    end record test_times;

we can use this and the test_vector type as element types for a larger record type:

   type test_application is record
      test_to_apply : test_vector;
      application_times : test_times;
    end record;

Since the test_to_apply element is unconstrained and the application_times element is fully constrained, the test_application type is partially constrained.

As another example, recall the types sample and dozen_samples defined in Section 4.2.2 as

   type sample is array (natural range <>) of integer;
   type dozen_samples is array (1 to 12) of sample;

If we declare a type as follows:

   type analyzed_samples is record
      samples : dozen_samples;
      result : real;
    end record analyzed_samples;

this type is partially constrained, since it has just one composite element, samples, that is itself partially constrained.

We have mentioned that, when we declare an object of an array type, we must provide constraints for any index ranges that remain unspecified by the type. This rule applies to composite types in general, including record types. If any of the record elements are arrays, their index ranges must be specified. For a fully constrained record type, the type itself specifies the index ranges, if any. Thus, in Example 4.12, we were able to declare signals and variables of the fully constrained types instruction and word without supplying any further information beyond the type names.

For an unconstrained or partially constrained record type, we need to fill in any unspecified index ranges, and we need to specify which element of the type each index range constrains. The way in which we do so is illustrated by the following declaration using the unconstrained type test_vector:

   variable next_test_vector : test_vector(stimulus(0 to 7),
                                          response(0 to 9));

For those elements of the type that we need to constrain, we write the element name followed by the index constraint. If the index range to be constrained is nested more deeply within the type’s hierarchy, we can nest the constraint notation. For example, to declare a variable of type test_application, we could write:

   variable scheduled_test :
             test_application(test_to_apply(stimulus(0 to 7),
                                            response(0 to 9)));

Since the application_times element of the test_application type is fully constrained, we do not mention it in the record constraint. As a second example, the declaration

   variable analysis : analyzed_samples(samples(open)(1 to 100));

constrains the samples element of the record type. The constraint uses the word open to skip over the top-level index range of the element, since that index range is specified by the element type dozen_samples. The nested index range is constrained to be 1 to 100.

Just as we did for array types, we can use this notation to declare subtypes of record types. Some examples are:

   subtype byte_test_vector is test_vector(stimulus(7 downto 0),
                                          response(7 downto 0));
   subtype analyzed_short_samples is
            analyzed_samples(samples(open)(1 to 100));

We do not need to constrain every element. For example, if we write:

   subtype test_application_word is
            test_application(test_to_apply(stimulus(0 to 31)));

only the stimulus element of the nested record element is constrained, and so the subtype test_application_word remains partially constrained. This is equivalent to writing

   subtype test_application_word is
            test_application(test_to_apply(stimulus(0 to 31),
                                           response(open)));

We could then further constrain it as follows:

   subtype test_application_word_byte is
            test_application_word(test_to_apply(response(0 to 7)));

or equivalently:

   subtype test_application_word_byte is
            test_application_word(test_to_apply(stimulus(open),
                                                response(0 to 7)));

Finally, the rule that index ranges for a constant can be inferred from the initial value also applies to a constant of an unconstrained or partially constrained record type. For example, if we declare a constant as follows:

   constant first_test_vector : test_vector
             := (id => 0,
                 stimulus => B"100010",
                 response => B"00000001");

the index ranges for the stimulus and response elements are inferred to be 0 to 5 and 0 to 7, respectively, since they are both of type bit_vector, which has natural as its index subtype.

Exercises

1.

[Exercises4.1] Write an array type declaration for an array of 30 integers, and a variable declaration for a variable of the type. Write a for loop to calculate the average of the array elements.

2.

[Exercises4.1] Write an array type declaration for an array of bit values, indexed by standard-logic values. Then write a declaration for a constant, std_ulogic_to_bit, of this type that maps standard-logic values to the corresponding bit value. (Assume unknown values map to ‘0’.) Given a standard-logic vector v1 and a bit-vector variable v2, both indexed from 0 to 15, write a for loop that uses the constant std_ulogic_to_bit to map the standard-logic vector to the bit vector.

3.

[Exercises4.1] The data on a diskette is arranged in 18 sectors per track, 80 tracks per side and two sides per diskette. A computer system maintains a map of free sectors. Write a three-dimensional array type declaration to represent such a map, with a ‘1’ element representing a free sector and a ‘0’ element representing an occupied sector. Write a set of nested for loops to scan a variable of this type to find the location of the first free sector.

4.

[Exercises4.2] Write a declaration for a subtype of std_ulogic_vector, representing a byte. Declare a constant of this subtype, with each element having the value ‘Z’.

5.

[Exercises4.2] Write a type declaration for an unconstrained array of time_vector elements, indexed by positive values. Then write a subtype declaration representing an array with 4 unconstrained elements. Last, write a variable declaration using the subtype with each element having 10 subelements.

6.

[Exercises4.2] Write a for loop to count the number of ‘1’ elements in a bit-vector variable v.

7.

[Exercises4.3] An 8-bit vector v1 representing a two’s-complement binary integer can be sign-extended into a 32-bit vector v2 by copying it to the leftmost eight positions of v2, then performing an arithmetic right shift to move the eight bits to the rightmost eight positions. Write variable assignment statements that use slicing and shift operations to express this procedure.

8.

[Exercises4.4] Write a record type declaration for a test stimulus record containing a stimulus bit vector of three bits, a delay value and an expected response bit vector of eight bits.

9.

[Exercises4.1] Develop a model for a register file that stores 16 words of 32 bits each. The register file has data input and output ports, each of which is a 32-bit word; read-address and write-address ports, each of which is an integer in the range 0 to 15; and a write-enable port of type bit. The data output port reflects the content of the location whose address is given by the read-address port. When the write-enable port is ‘1’, the input data is written to the register file at the location whose address is given by the write-address port.

10.

[Exercises4.1] Develop a model for a priority encoder with a 16-element bit-vector input port, an output port of type natural that encodes the index of the leftmost ‘1’ value in the input and an output of type bit that indicates whether any input elements are ‘1’.

11.

[Exercises4.2] Write a package that declares an unconstrained array type whose elements are integers. Use the type in an entity declaration for a module that finds the maximum of a set of numbers. The entity has an input port of the unconstrained array type and an integer output. Develop a behavioral architecture body for the entity. How should the module behave if the actual array associated with the input port is empty (i.e., of zero length)?

12.

[Exercises4.2/4.3] Develop a model for a general and-or-invert gate, with two standard-logic vector input ports a and b and a standard-logic output port y. The output of the gate is

Exercises

13.

[Exercises4.4] Develop a model of a 3-to-8 decoder and a test bench to exercise the decoder. In the test bench, declare the record type that you wrote for Exercise 8 and a constant array of test record values. Initialize the array to a set of test vectors for the decoder, and use the vectors to perform the test.

 

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

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