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.
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;
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;
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.
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.
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.
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:
| Left bound of index range of dimension |
| Right bound of index range of dimension |
| Lower bound of index range of dimension |
| Upper bound of index range of dimension |
| Index range of dimension |
| Reverse of index range of dimension |
| Length of index range of dimension |
| true if index range of dimension |
| The element subtype of |
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.
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.
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.
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 => ' '),
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 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);
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);
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";
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.
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.
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.
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.
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"
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 ...
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 ...
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.
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"
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.
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.
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.
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.
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.
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;
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.
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;
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.
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.
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.
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.
[4.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. | |
[4.1] Write an array type declaration for an array of bit values, indexed by standard-logic values. Then write a declaration for a constant, | |
[4.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.2] Write a declaration for a subtype of | |
[4.2] Write a type declaration for an unconstrained array of | |
[4.2] Write a for loop to count the number of ‘1’ elements in a bit-vector variable | |
[4.3] An 8-bit vector | |
[4.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. | [4.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 |
10. | [4.1] Develop a model for a priority encoder with a 16-element bit-vector input port, an output port of type |
11. | [4.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. | [4.2/4.3] Develop a model for a general and-or-invert gate, with two standard-logic vector input ports |
13. | [4.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. |
18.118.195.56