Chapter 6. Modeling Enhancements

One of the main purposes of VHDL is modeling the behavior of hardware. This chapter describes a number of features in VHDL-2008 that make the modeling task easier. All of the modeling tasks described here can be expressed in earlier versions of VHDL, but not as succinctly.

Signal Expressions in Port Maps

When we instantiate a component in VHDL, we write a port map to specify the signals connected to the ports of the instance. If an input port is to be tied to a fixed value, we can write an expression in the port map in place of a signal name. In earlier versions of VHDL, the expression was required to be static; that is, the expression’s value could not change during execution of the model. A common example is a simple literal value representing tying an unused input high or low:

inst : component adder32
  port map ( . . ., carry_in => '0', . . . );

In VHDL-2008, the rules for writing an expression in a port map are generalized to include nonstatic expressions involving the values of signals. This allows us to include a small amount of functional logic in a port map, and avoids the need to express the logic with a separate assignment statement and an intermediate signal. If the expression is not static, the port association is defined to be equivalent to association with an anonymous signal that is the target of a signal assignment with the expression on the right-hand side.

Example 6.1. Select logic in a port map

Suppose an I/O controller connected to a CPU bus is to be enabled when bus control signals indicate a read from I/O address space and the bus address matches the controller’s address. We can include the select logic in the port map for the controller instance:

io_ctrl_1 : entity work.io_controller(rtl)
  port map ( en => rd_en and io_sel and addr ?= io_base,
              . . . );

This is a much more succinct way of expressing the model than the equivalent:

signal en_tmp : std_ulogic;
. . .

en_tmp <= rd_en and io_sel and addr ?= io_base;

io_ctrl_1 : entity work.io_controller(rtl)
  port map ( en => en_tmp,
              . . . );

Most of the time, it is a straightforward matter to determine whether an expression in a port map is static, denoting a fixed value for the port, or nonstatic, implying connection to additional logic. However, if the expression is in the form of a function call applied to a signal name, there are two possible interpretations, one as a nonstatic expression implying connection to logic, and the other as a conversion function implying a change of representation of a value. An example of the latter is:

signal s : signed;
component abstract_ALU is
   port ( a : in integer; . . . );
end component;
. . .

ALU : component abstract_ALU
   port map ( a => to_integer(s), . . . );

The reason for making the distinction is that the interpretation as a nonstatic expression introduces a delta delay between the input signal changing value and the port changing value, whereas the interpretation as application of a conversion function does not. If we were to write a function with one parameter representing some computational logic, for example:

function increment ( x : unsigned ) return unsigned;

and use it in a port map:

op_counter : component reg16
  port map ( d_in => increment(op_count), . . . );

it would not be clear how to interpret the expression. Under the rules of earlier versions of VHDL, the expression would be interpreted as a conversion function, which is not what we want. To make the intention explicit, we can include the reserved work inertial in the port association to imply an inertial signal assignment of the expression to the anonymous intermediate signal. Thus, we would write the port map as

op_counter  : component reg16
  port map ( d_in => inertial increment(op_count), . . . );

Under the VHDL-2008 rules, if we omit the reserved word and the expression can be interpreted as application of a conversion function or a type conversion, then that interpretation takes precedence.

All Signals in Sensitivity List

The sensitivity list of a process statement specifies the signals that cause the process to resume execution. For a process representing combinational logic, we should include all of the input signals of the logic in the sensitivity list. If we omit an input signal, a synthesis tool would infer latch-based storage, since the output would remain unchanged when the omitted signal changed.

Using earlier versions of VHDL, ensuring that all input signals are included in a sensitivity list is cumbersome and error prone. We must carefully check the body of the process and any subprograms called by the process to assemble the list of signals that are read. If we subsequently revise the model code, we must remember to update each process’s sensitivity list if we update the process’s statements. In VHDL-2008, writing and maintaining processes representing combinational logic is much simpler. We can replace the list of signals in the sensitivity list with the reserved word all, indicating that the process is sensitive to all signals read by the process.

Example 6.2. Combinational logic for a finite-state machine

One place where assembling the sensitivity list for a combinational process often causes problems is the next-state and output logic for a finite-state machine. The logic has, as inputs, the current state signal and signals whose values determine the next state and the output values. An example is:

next_state_logic : process (all ) is
begin
  out1 <= '0'; out2 <= '0'; . . .
  case current_state is
    when idle =>
       out1 <= '1';
       if in1 and not in2 then
         out2 <= '1';
         next_state <= busy1;
       elsif in1 and in2 then
         next_state <= busy2;
       else
         next_state  <= idle;
       end if;
    . . .
  end case;
end process next_state;

As we revise the finite-state machine, we might include more signals as inputs. Using the reserved word all instead of explicitly listing the input signals makes the process easier to write and maintain.

The list of signals to which the process is made sensitive also includes signals read in subprograms called directly or indirectly by the process. A proviso, however, is that a subprogram declared in a separate design unit from the process must only read its signal parameters. It cannot read signals declared globally in packages or signals identified using external names (see Section 2.1). The rationale for this restriction is to ensure the analyzer can assemble the sensitivity list for the process just by analyzing the enclosing design unit. It also means that we can perform a similar analysis to understand a process. We don’t get any surprises from unexpected external signals becoming inputs to the logic implied by a process.

Reading Out-Mode Ports and Parameters

In many designs, we compute a value and assign it to an output port, and then use the value in further computations within the design. There are two ways in which we might do this. On one hand, the value might be used to implement further behavior of the design. An example is a circuit with both active-high and active-low outputs. We derive a value for the active-high output, and then negate it for the active-low output. One the other hand, the value might be used in verification of the design’s functionality. The computation using the value is passive and does not imply additional hardware.

VHDL provides two modes for output ports, out and buffer. An out-mode port is intended to be used for cases where there is no internal use of the port’s value. Prior to VHDL-2008, we could not read the value of an out-mode port. A buffer-mode port is intended to be used for cases where the port’s driving value is used internally. As the name suggests, a hardware buffer is implied between the driver assigning values to the port and the external connections. The value used internally is taken from the inside of the buffer.

Prior to VHDL-2002, there were somewhat restrictive rules governing connection of buffer-mode ports of a component instance to the buffer-mode and out-mode ports of an enclosing entity. These restrictions made it very difficult to use buffer-mode ports effectively, so designers largely ignored them. It became common practice to declare an internal signal in a design, to use that signal internally, and to include a separate signal assignment to drive the signal’s value onto an out-mode port.

In VHDL-2002, the rules for buffer-mode ports were relaxed, allowing them to be connected to external out-mode and buffer-mode ports. It is no longer necessary to resort to an internal signal when a port’s driving value is needed internally as an input to further logic. Nonetheless, the practice persists, both for backward compatibility with previous versions of VHDL, and because many designers are not aware of the change.

Example 6.3. True and complement outputs for a flip-flop

A flip-flop having both true (active-high) and complement (active-low) outputs can be modeled using out-mode ports and an internal signal as follows:

entity Dff  is
  port ( clk, d : in bit; q, q_n : out bit );
end entity Dff;

architecture rtl of Dff is
  signal q_int : bit
begin
  ff : process (clk) is
  begin
    if clk = '1' then
       q_int <= d;
    end if ;
  end process ff;
  q      <=  q_int;
  q_n    <= not q_int;
end architecture rtl;

By deriving the active-low output from the active-high internal signal, we avoid having a synthesis tool infer two separate flip-flops. Under the relaxed rules for buffer-mode ports in VHDL-2002, we can rewrite this model as follows:

entity Dff is
  port (clk, d : in bit; q : buffer bit; q_n : out bit );
end entity Dff;

architecture rtl of Dff is
begin
  ff : process (clk) is
  begin
     if clk = '1' then
        q <= d;
     end if ;
  end process ff;
  q_n <= not q;
end architecture rtl;

In VHDL-2008, the restriction on reading out-mode ports is also removed. The value of an out-mode port seen internally is the same as the value being driven onto the port. Thus, an out-mode port has the same behavior in VHDL-2008 as a buffer-mode port. However, the rationale for allowing reading of an out-mode port is to support verification of a design’s behavior, as opposed to implying a hardware buffer. We should choose between out and buffer modes for a port depending on whether we intend to imply hardware buffering or passive verification, respectively. The choice of port mode documents our intention. Note that this choice would be made as a convention, rather than being enforced by the language definition. A tool has no way of distinguishing between the two ways of using a port.

Example 6.4. Reading an out-mode Port for verification

Suppose we wish to verify that the outputs of a device are all ’2’ within a required interval of the device being disabled and remain all ’2’ until the device is enabled. The output values are not required internally to implement any functionality for the device. Hence, we declare the output ports using out mode, as follows:

entity device is
  generic ( T_z : delay_length );
  port ( en : in std_ulogic;
         d_out : out std_ulogic_vector(7 downto 0); . . . );
end entity device;

We can read the values driven onto the output ports in verification code in the architecture:

architecture verifying of device is
begin
  d_out <= . . . when to_x01(en) = '1' else
              . . . when to_x01(en) = '0' else
              "XXXXXXXX";
  assert (to_x01(en'delayed(T_z)) = '0' and d_out = "ZZZZZZZZ")
           or to_x01(en) = '1';
end architecture verifying;

VHDL-2008 also removes the restriction on reading the value of an out-mode parameter of a procedure. Out-mode parameters can be either signal parameters or variable parameters. In the case of an out-mode signal parameter, when we call the procedure and pass an actual signal, the procedure is passed a reference to the actual signal. The value we get when we read the parameter within the procedure is the current value of the actual signal.

In the case of an out-mode variable parameter, the rules are slightly different. The formal parameter of the procedure is treated like a local variable, and is initialized when the procedure is called. It does not take on the value of the actual parameter variable at that stage. As we make assignments to the parameter within the procedure, the local value is updated. When we read the parameter, we get that local value. Eventually, when the procedure returns, the final value of the parameter is copied to the actual variable.

To illustrate this behavior, consider the following procedure:

procedure do_funny ( p : out positive ) is
  variable tmp : positive;

begin
  tmp := p;
  p := tmp * 2;
end procedure do_funny;

Suppose we call the procedure as follows:

variable v : positive := 10;
. . .

do_funny(v)

When the procedure is called, the parameter p is initialized, not with the value of v, but with the default initial value for positive, namely, 1. Thus, the value read and assigned to tmp is 1, and the value assigned to p in the last statement is 2. When the procedure returns, the final value of p, namely, 2, is copied out to v.

Example 6.5. Checking an out-mode status parameter

A procedure to read multiple items from a line of input might have an out-mode parameter to indicate whether items were read successfully. We can use the parameter as the actual to textio read procedures, and check that each call succeeded before continuing, as follows:

procedure read_time_and_int  ( L : inout line;
                               T : out time; I : out integer;
                               good : out boolean ) is
begin
  read(L, T, good);
  if not good then
    report "Read of time failed" severity failure;
    return;
  end  if;
  read(L, I, good) ;
  if not good then
    report "Read of integer failed" severity failure;
  end  if;
end procedure read_time_and_int;

If the procedure could not read the value of the out-mode parameter good, it would have to declare an internal variable to pass to the read procedures, and copy the value to good upon returning. The ability to read the out-mode parameter makes the procedure simpler and easier to maintain.

Slices in Aggregates

An array aggregate provides a way of forming an array from a collection of elements. In earlier versions of VHDL, we could only form an aggregate from individual elements. In VHDL-2008, the rules are extended to allow us to form an aggregate from a mixture of individual elements and slices of the array. For example, a bit_vector array aggregate could be written as follows:

('0', "1001", '1')

This forms a 6-element vector from the single element ‘O’, the vector value “1001” and the single element ‘1’. The vector value forms a slice of the final aggregate value. The effect is similar to concatenating the element and array values. While this illustrates the idea, a more powerful use of the feature involves writing an aggregate as the target of an assignment statement. We can write a signal assignment target in the form of an aggregate of signal names. In VHDL-2008, the names can be a mixture of element-typed signals and array-typed signals. Elements of the right-hand-side value are assigned to the matching signals or signal elements.

Example 6.6. Binary addition with carry out

We can add two 16-bit unsigned numbers to get a 17-bit result, with the most-significant bit being the carry out of the addition, and the least-significant 16 bits being the sum. We can assign to separate signals representing the carry out and the sum as follows:

signal a, b, sum : unsigned(l5 downto 0);
signal c_out  : std_ulogi c;
. . .

(c_out, sum) <= ('0' & a) + ('0' & b);

The operands a and b are zero-extended and added to produce a 17-bit result. This is assigned to the aggregate target comprising the scalar signal c out and the 16-bit vector sum. Since we use positional association in the aggregate, c out matches the leftmost element and sum matches the rightmost 16 elements.

We can also use named association in array aggregates. If we want to include an association for a slice of the aggregate, we specify a range of index values for the slice. For example, given signals declared as follows:

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

We can 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. When we include slices in an aggregate, the direction of the aggregate’s index range is taken from the direction of ranges used to specify the slices. Thus, in this above example, the direction is descending, since “2 downto 0" and "6 downto 4” are both descending ranges. All the ranges used in this way must have the same direction. Thus, it would be illegal to write the example using “2 downto 0" for one range and “4 to 6” for the other.

Bit-String Literals

VHDL allows us to use a bit-string literal to specify a value for a vector of ‘0’ and ‘1’ elements in binary, octal, or hexadecimal form. The rules for bit-string literals in earlier versions of VHDL were somewhat restrictive. In particular, the resulting vector value had to be a multiple of three in length for octal literals or a multiple of four in length for hexadecimal literals. Moreover, every bit had to be stated explicitly, as there was no provision for zero extension or sign extension. Finally, only ‘0’ and ‘1’ elements could be specified. There was no provision for “metalogical” values, such as ‘XI or ‘Z’.

VHDL-2008 enhances bit-string literals considerably. First, we can specify elements other than just ‘0’ and ‘1’. In an octal literal, any non-octal-digit character is expanded to three occurrences of that character in the vector value. Thus, the literal O"3XZ4" represents the vector value "011XXXZZZ100". Similarly, in a hexadecimal literal any non-hexadecimal-digit character is expanded to four occurrences of the character. Thus, the literal X"A3-" represents the vector value '10100011———'. In a binary literal, any non-bit character just represents itself in the vector. Thus, B"00UU" represents the vector "OOUU". While this may seem vacuous at first, the benefit of allowing this in binary literals will become clear when we look at other enhancements.

Note that expansion of non-digit characters does not extend to embedded underscores, which we might add for readability. Thus, O"3_X" represents "01lXXX", not "011XXX". Also, expansion on non-digit characters is not limited to those defined in std_ulogic, though that is the most common use case. We could write the literal XMO#?F" to represent the string value "0000####????1111"

The second enhancement is provision for specifying the exact length of the vector represented by a literal. This allows us to specify vectors whose length is not a multiple or three (for octal) or four (for hexadecimal). We do so by writing the length immediately before the base specifier, with no intervening space. For example, the literal 7X"3CW represents the 7-element vector "0111100", 80'S" represents "00000101", and 10B"X" represents "000000000X". If the final length of the vector is longer than that implied by the digits, the vector is padded on the left with ‘0’ elements. If the final length is less than that implied by the digits, the left-most elements of the vector are truncated, provided they are all ‘0’. An error occurs if any non-‘0’ elements are truncated, as they would be in the literal 8X"90F".

The third enhancement is provision for specifying whether the literal represents an unsigned or signed number. We represent an signed number using one of the base specifiers UB, UO, or UX. These are the same as the ordinary base specifiers B, 0, and X. When a sized unsigned literal is extended, it is padded with ‘0’ elements, and when elements are truncated, they must be all ‘0’.

We represent a signed number using one of the base specifiers SB, SO, or SX. The rules for extension and truncation are based on those for sign extension and truncation of 2s-complement binary numbers. When a sized signed literal is extended, each element of padding on the left is a replication of the leftmost element prior to padding. For example, the literal 1OSX"71" is extended to "0001110001", 1 0SX"88" is extended to "1110001000", and 10SX"WO" is extended to "WWWWWW0000". When a sized signed literal is truncated, all of the elements removed at the left must be the same as the leftmost remaining element. For example, the literal 6SXn16" is truncated to "01011O", 6SXWE8" is truncated to "101000", and 6SXNH3" is truncated to "HH0011" However, 6SX"28" is invalid, since, prior to truncation, the vector would be "00101000". The two leftmost elements removed are each ‘O’, which differs from the leftmost remaining ‘1’ element. The literal would have to be written as 6SX"E8" for this reason. The rationale for this rule is that it prevents the signed numeric value represented by the literal being inadvertently changed by the truncation.

The remaining enhancement is provision for specifying a vector value in decimal. For this, we use the base specifier D. All of the characters in the literal must then be decimal digits (or underscores); we cannot specify other characters, such as ’2’ or ‘XI, since it would not be clear which elements of the vector would correspond to those characters. If we omit a size specification in a decimal bit-string literal, the number of elements represented is the smallest number that can encode the value. For example, the literal D"23" represents the vector "10111", D"64" represents " 1000000", and D"0003" represents "1 1". A decimal bit-string literal is treated as representing an unsigned number. If the literal must be extended, the vector is padded on the left with ‘0’ elements. For example, 1 2DM10" represents "000000001010". It can never be legal to specify a size requiring truncation, since the leftmost element prior to truncation is always ‘1’.

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

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