Chapter 8. Resolved Signals

Throughout the previous chapters we have studiously avoided considering the case of multiple output ports connecting one signal. The problem that arises in such a case is determining the final value of the signal when multiple sources drive it. In this chapter we discuss resolved signals, the mechanism provided by VHDL for modeling such cases.

Basic Resolved Signals

If we consider a real digital system with two outputs driving one signal, we can fairly readily determine the resulting value based on some analog circuit theory. The signal is driven to some intermediate state, depending on the drive capacities of the conflicting drivers. This intermediate state may or may not represent a valid logic state. Usually we only connect outputs in a design if at most one is active at a time, and the rest are in some high-impedance state. In this case, the resulting value should be the driving value of the single active output. In addition, we include some form of “pull-up” that determines the value of the signal when all outputs are inactive.

While this simple approach is satisfactory for some models, there are other cases where we need to go further. One of the reasons for simulating a model of a design is to detect errors such as multiple simultaneously active connected outputs. In this case, we need to extend the simple approach to detect such errors. Another problem arises when we are modeling at a higher level of abstraction and are using more complex types. We need to specify what, if anything, it means to connect multiple outputs of an enumeration type together.

The approach taken by VHDL is a very general one: the language requires the designer to specify precisely what value results from connecting multiple outputs. It does this through resolved signals, which are an extension of the basic signals we have used in previous chapters. A resolved signal includes in its definition a function, called the resolution function, that is used to calculate the final signal value from the values of all of its sources.

Let us see how this works by developing an example. We can model the values driven by a tristate output using a simple extension to the predefined type bit, for example:

   type tri_state_logic is ('0', '1', 'Z'),

The extra value, ‘Z’, is used by an output to indicate that it is in the high-impedance state. Next, we need to write a function that takes a collection of values of this type, representing the values driven by a number of outputs, and return the resulting value to be applied to the connected signal. For this example, we assume that at most one driver is active (‘0’ or ‘1’) at a time and that the rest are all driving ‘Z’. The difficulty with writing the function is that we should not restrict it to a fixed number of input values. We can avoid this by giving it a single parameter that is an unconstrained array of tri_state_logic values, defined by the type declaration

   type tri_state_logic_array is
     array (integer range <>) of tri_state_logic;

The declaration of the resolution function is

   function resolve_tri_state_logic
     ( values : in tri_state_logic_array )
     return tri_state_logic is
     variable result : tri_state_logic := 'Z';
   begin
     for index in values'range loop
       if values(index) /= 'Z' then
         result := values(index);
       end if;
     end loop;
     return result;
   end function resolve_tri_state_logic;

The final step to making a resolved signal is to declare the signal, as follows:

   signal s1 : resolve_tri_state_logic tri_state_logic;

This declaration is almost identical to a normal signal declaration, but with the addition of the resolution function name before the signal type. The signal still takes on values from the type tri_state_logic, but inclusion of a function name indicates that the signal is a resolved signal, with the named function acting as the resolution function. The fact that s1 is resolved means that we are allowed to have more than one source for it in the design. (Sources include drivers within processes and output ports of components associated with the signal.) When a transaction is scheduled for the signal, the value is not applied to the signal directly. Instead, the values of all sources connected to the signal, including the new value from the transaction, are formed into an array and passed to the resolution function. The result returned by the function is then applied to the signal as its new value.

Let us look at the syntax rule that describes the VHDL mechanism we have used in the above example. It is an extension of the rules for the subtype indication, which we first introduced in Chapter 2 and 4. The combined rule is

   subtype_indication  ⇐
       [ resolution_function_name ]
      type_mark    [ range ( range_attribute_name
   
                          | simple_expression ( to | downto ) simple_expression )
                  | ( discrete_range { , ... } ) ]

This rule shows that a subtype indication can optionally include the name of a function to be used as a resolution function. Given this new rule, we can include a resolution function name anywhere that we specify a type to be used for a signal. For example, we could write a separate subtype declaration that includes a resolution function name, defining a resolved subtype, then use this subtype to declare a number of resolved signals, as follows:

   subtype resolved_logic is
     resolve_tri_state_logic tri_state_logic;

   signal s2, s3 : resolved_logic;

The subtype resolved_logic is a resolved subtype of tri_state_logic, with resolve_tri_state_logic acting as the resolution function. The signals s2 and s3 are resolved signals of this subtype. Where a design makes extensive use of resolved signals, it is good practice to define resolved subtypes and use them to declare the signals and ports in the design.

The resolution function for a resolved signal is also invoked to initialize the signal. At the start of a simulation, the drivers for the signal are initialized to the expression included in the signal declaration, or to the default initial value for the signal type if no initialization expression is given. The resolution function is then invoked using these driver values to determine the initial value for the signal. In this way, the signal always has a properly resolved value, right from the start of the simulation.

Let us now return to the tristate logic type we introduced earlier. In the previous example, we assumed that at most one driver is ‘0’ or ‘1’ at a time. In a more realistic model, we need to deal with the possibility of driver conflicts, in which one source drives a resolved signal with the value ‘0’ and another drives it with the value ‘1’. In some logic families, such driver conflicts cause an indeterminate signal value. We can represent this indeterminate state with a fourth value of the logic type, ‘X’, often called an unknown value. This gives us a complete and consistent multivalued logic type, which we can use to describe signal values in a design in more detail than we can using just bit values.

Example 8.1. A four-state multivalued logic type and its use

The following package declaration and body define a four-state multivalued logic type.

   package MVL4 is

    -- unresolved logic type
    type MVL4_ulogic is ('X', '0', '1', 'Z'),

    type MVL4_ulogic_vector is
      array (natural range <>) of MVL4_ulogic;

    function resolve_MVL4
      ( contribution : MVL4_ulogic_vector ) return MVL4_ulogic;
   
    subtype MVL4_logic is resolve_MVL4 MVL4_ulogic;

    function "not" ( r : MVL4_ulogic ) return MVL4_ulogic;
    function "and" ( l, r : MVL4_ulogic ) return MVL4_ulogic;
    function "or"  ( l, r : MVL4_ulogic ) return MVL4_ulogic;
    ...
    function to_X01 ( a : MVL4_ulogic ) return MVL4_ulogic;

    function "??" ( r : MVL4_ulogic ) return boolean;

   end package MVL4;
   --------------------------------------------------
   package body MVL4 is

    type table is
      array (MVL4_ulogic, MVL4_ulogic) of MVL4_ulogic;

    constant resolution_table : table :=
      -- 'X' '0' '1' 'Z'
      --  ------------------
      ( ( 'X', 'X', 'X', 'X' ),    -- 'X'
        ( 'X', '0', 'X', '0' ),    -- '0'
        ( 'X', 'X', '1', '1' ),    -- '1'
        ( 'X', '0', '1', 'Z' ) );  -- 'Z'

    function resolve_MVL4 ( contribution : MVL4_ulogic_vector )
                          return MVL4_ulogic is
      variable result : MVL4_ulogic := 'Z';
    begin
      for index in contribution'range loop
        result := resolution_table(result, contribution(index));
      end loop;
      return result;
    end function resolve_MVL4;

    function "not" ( r : MVL4_ulogic ) return MVL4_ulogic is
    begin
      case r is
        when '1' => return '0';
        when '0' => return '1';
        when others => return 'X';
      end case;
    end function "not";

    function "and" ( l, r : MVL4_ulogic ) return MVL4_ulogic is ...

    function "or"  ( l, r : MVL4_ulogic ) return MVL4_ulogic is ...
    ...
   
    function to_X01 ( a : MVL4_ulogic ) return MVL4_ulogic is
    begin
      case a is
        when '0' | '1' => return a;
        when 'X' | 'Z' => return 'X';
      end case;
    end function to_X01;

    function "??" ( r : MVL4_ulogic ) return boolean is
    begin
      return r = '1';
    end function "??";

   end package body MVL4;

The constant resolution_table is a lookup table used to determine the value resulting from two source contributions to a signal of the resolved logic type. The resolution function uses this table, indexing it with each element of the array passed to the function. If any source contributes ‘X’, or if there are two sources with conflicting ‘0’ and ‘1’ contributions, the result is ‘X’. If one or more sources are ‘0’ and the remainder ‘Z’, the result is ‘0’. Similarly, if one or more sources are ‘1’ and the remainder ‘Z’, the result is ‘1’. If all sources are ‘Z’, the result is ‘Z’. The lookup table is a compact way of representing this set of rules. The package also declares overloaded versions of the logical operators and other functions. The to_X01 function ensures that a value is either a proper logic value (‘0’ or ‘1’) or an unknown value. The overloaded “??” operator allows us to use values of the logic type in conditions with implicit conversion to boolean.

We can use this package in a design for a tristate buffer. The entity declaration and a behavioral architecture body are

   use work.MVL4.all;

   entity tri_state_buffer is
     port ( a, enable : in MVL4_ulogic;  y : out MVL4_ulogic );
   end entity tri_state_buffer;
   --------------------------------------------------
   architecture behavioral of tri_state_buffer is
   begin

     y <= to_X01(a) when enable else
         'Z' when not enable else
         'X';

   end architecture behavioral;

When the buffer is enabled, the buffer copies the input to the output, but with a ‘Z’ input value changed to ‘X’ by the to_X01 function. When the buffer is not enabled, it drives the value ‘Z’ on its output. If the enable port is not a proper logic level, both conditions are false, so the buffer drives the unknown value on its output.

An architecture body for a logic block that uses the tristate buffer is:

   use work.MVL4.all;

   architecture gate_level of misc_logic is

     signal src1, src1_enable : MVL4_ulogic;
     signal src2, src2_enable : MVL4_ulogic;
     signal selected_val : MVL4_logic;
     ...

   begin

     src1_buffer : entity work.tri_state_buffer(behavioral)
       port map ( a => src1, enable => src1_enable,
                  y => selected_val );

     src2_buffer : entity work.tri_state_buffer(behavioral)
       port map ( a => src2, enable => src2_enable,
                  y => selected_val );
     ...

   end architecture gate_level;

The signal selected_val is a resolved signal of the multivalued logic type. It is driven by the two buffer output ports. The resolution function for the signal is used to determine the final value of the signal whenever a new transaction is applied to either of the buffer outputs.

Composite Resolved Subtypes

The above examples have all shown resolved subtypes of scalar enumeration types. In fact, VHDL’s resolution mechanism is more general. We can use it to define a resolved subtype of any type that we can legally use as the type of a signal. Thus, we can define resolved integer subtypes, resolved composite subtypes and others. In the latter case, the resolution function is passed an array of composite values and must determine the final composite value to be applied to the signal.

Example 8.2. A package for a resolved array subtype

The package declaration and body below define a resolved array subtype. Each element of an array value of this subtype can be ‘X’, ‘0’, ‘1’ or ‘Z’. The unresolved type uword is an unconstrained array of these values. The resolution function has an unconstrained array parameter consisting of elements of type uword. The function uses the lookup table to resolve corresponding elements from each of the contributing sources and produces an array result. The subtype word is the final resolved array subtype.

   package words is
   
    type X01Z is ('X', '0', '1', 'Z'),
    type uword is array (natural range <>) of X01Z;

    type uword_vector is array (natural range <>) of uword;

    function resolve_word
      ( contribution : uword_vector ) return uword;

    subtype word is resolve_word uword;

   end package words;
   --------------------------------------------------
   package body words is

    type table is array (X01Z, X01Z) of X01Z;

    constant resolution_table : table :=
      -- 'X' '0' '1' 'Z'
      --  ------------------
      ( ( 'X', 'X', 'X', 'X' ),    -- 'X'
        ( 'X', '0', 'X', '0' ),    -- '0'
        ( 'X', 'X', '1', '1' ),    -- '1'
        ( 'X', '0', '1', 'Z' ) );  -- 'Z'

    function resolve_word
      ( contribution : uword_vector ) return uword is
      variable result : uword(contribution'element'range)
                 := (others => 'Z'),
    begin
      for index in contribution'range loop
        for element in result'range loop
          result(element) :=
            resolution_table( result(element),
                              contribution(index)(element) );
        end loop;
      end loop;
      return result;
    end function resolve_word;

   end package body words;

We can use these types to declare array ports in entity declarations and resolved array signals with multiple sources. For example, the following CPU entity and memory entity have bidirectional data ports of the unresolved array type.

   use work.words.all;

   entity cpu is
    port ( address : out uword(23 downto 0);
           data : inout uword(31 downto 0);  ... );
   end entity cpu;
   
   --------------------------------------------------
   use work.words.all;

   entity memory is
    port ( address : in uword(23 downto 0);
           data : inout uword(31 downto 0); ... );
   end entity memory;

The architecture body for a computer system declares a signal of the resolved subtype and connects it to the data ports of the instances of the CPU and memory:

   architecture top_level of computer_system is

     use work.words.all;

     signal address : uword(23 downto 0);
     signal data : word(31 downto 0);
     ...

   begin

     the_cpu : entity work.cpu(behavioral)
       port map ( address, data, ... );
 
    the_memory : entity work.memory(behavioral)
       port map ( address, data, ... );
     ...

   end architecture top_level;

A resolved composite subtype works well provided every source for a resolved signal of the subtype is connected to every element of the signal. For the data signal shown in the example, every source must be a 32-element array and must connect to all 32 elements of the data signal. However, in a realistic computer system, sources are not always connected in this way. For example, we may wish to connect an 8-bit-wide device to the low-order eight bits of a 32-bit-wide data bus. We might attempt to express such a connection in a component instantiation statement, as follows:

   boot_rom : entity work.ROM(behavioral)
     port map ( a => address, d => data(24 to 31), ... );  -- illegal

If we add this statement to the architecture body in Example 8.2, we have two sources for elements 0 to 23 of the data signal and three for elements 24 to 31. A problem arises when resolving the signal, since we are unable to construct an array containing the contributions from the sources. For this reason, VHDL does not allow us to write such a description; it is illegal.

The solution to this problem is to describe the data signal as an array of resolved elements, rather than as a resolved array of elements. One way of doing this is to declare an array type whose elements are values of the MVL4_logic type, shown in Example 8.2. The array type declaration is

   type MVL4_logic_vector is array (natural range <>) of MVL4_logic;

This approach has the advantage that the array type is unconstrained, so we can use it to create signals of different widths, each element of which is resolved. The problem, however, is that the type MVL4_logic_vector is distinct from the type MVL4_ulogic_vector, since they are defined by separate type declarations. Neither is a subtype of the other. Hence we cannot legally associate a signal of type MVL4_logic_vector with a port of type MVL4_ulogic_vector, or a signal of type MVL4_ulogic_vector with a port of type MVL4_logic_vector.

A better way to describe arrays of resolved elements is to declare an array subtype in which we associate the resolution function with the elements. In our example, the array base type is MVL4_ulogic_vector, which has unresolved elements. We declare the subtype as:

   subtype MVL4_logic_vector is (resolve_MVL4) std_ulogic_vector;

The parentheses around the resolution function name, resolve_MVL4, indicates that the resolution function is associated with each element of the array type, rather than with the array type as a whole. Since MVL4_logic_vector is now a subtype of MVL4_ulogic_vector, not a distinct type, we can freely assign and associate signals and ports of the two types.

This example illustrates a more general form of resolution indication to be included in a subtype indication or signal declaration, rather than just naming a resolution function by itself. The syntax rule is:

   resolution_indication  ⇐
      resolution_function_name
      | (  resolution_indication
          | (record_element_identifier resolution_indication ) , {., ... ...})

If we want to associate a resolution function with an entire subtype, the resolution indication just consists of the resolution function name, as in the declaration of MVL4_logic:

   subtype MVL4_logic is resolve_MVL4 MVL4_ulogic;

The resolution indication here is just the resolution function name, resolve_MVL4. In the case of an array whose elements are to be resolved, we write the resolution function name in parentheses, as in the declaration of MVL4_logic_vector. We can also resolve the elements of an array type that is itself an array element type. For example, given the following declaration:

   type unresolved_RAM_content_type is
     array (natural range <>) of MVL4_ulogic_vector;

we can declare a subtype with resolved nested elements:

   subtype RAM_content_type is
    ((resolve_MVL4)) unresolved_RAM_content_type;

The degree of nesting of parentheses indicates how deeply nested in the type structure the resolution function is associated. Two levels indicate that the resolution function is associated with the elements of the elements of the type.

If we have a record type, one of whose elements is to be resolved, we include the element name in the resolution indication. For example, given the following record type with no associated resolution information:

   type unresolved_status_type is record
     valid : MVL4_ulogic;
     dirty : MVL4_ulogic;
     tag : MVL4_ulogic_vector;
   end record unresolved_status_type;

we can declare a subtype with the valid element resolved by the function wired_and as follows:

   subtype status_resolved_valid is
    (valid wired_and) unresolved_status_type;

We can include resolution functions with multiple elements of the record type by listing the element names and the resolution function associated with each, for example:

   subtype status_resolved_flags is
    (valid wired_and, dirty wired_or) unresolved_status_type;

For a record element that is itself of a composite type, we can associate a resolution function with subelements of the record element by writing a parenthesized resolution indication for the element. Thus, to resolve the elements of the tag element of the above record type, we would declare a subtype as follows:

   subtype status_resolved_tag is
     (tag(resolve_MVL4)) unresolved_status_type;

We could combine all of these examples together, resolving all of the scalar subelements, as follows:

   subtype resolved_status_type is
    ( tag(resolve_MVL4),
      valid wired_and,
      dirty wired_or ) unresolved_status_type;

This declaration illustrates that we do not have to write the resolution indications for the record elements in the same order as the declaration of elements in the record types. The record element names in the resolution indication determine the element with which the resolution function is associated.

Example 8.3. Connection to parts of a bus signal

Let us assume that the type MVL4_logic_vector described above has been added to the package MVL4. Below are entity declarations for a ROM entity and a single in-line memory module (SIMM), using the MVL4_ulogic_vector type for their data ports. The data port of the SIMM is 32 bits wide, whereas the data port of the ROM is only 8 bits wide.

   use work.MVL4.all;

   entity ROM is
     port ( a : in MVL4_ulogic_vector(15 downto 0);
            d : out MVL4_ulogic_vector(7 downto 0);
            rd : in MVL4_ulogic );
   end entity ROM;
   --------------------------------------------------
   use work.MVL4.all;
   entity SIMM is
     port ( a : in MVL4_ulogic_vector(9 downto 0);
            d : inout MVL4_ulogic_vector(31 downto 0);
            ras, cas, we, cs : in MVL4_ulogic );
   end entity SIMM;

The following architecture body uses these two entities. It declares a signal, internal_data, of the MVL4_logic_vector type, representing 32 individually resolved elements. The SIMM entity is instantiated with its data port connected to all 32 internal data elements. The ROM entity is instantiated with its data port connected to the rightmost eight elements of the internal data signal. When any of these elements is resolved, the resolution function is passed contributions from the corresponding elements of the SIMM and ROM data ports. When any of the remaining elements of the internal data signal are resolved, they have one less contribution, since they are not connected to any element of the ROM data port.

   architecture detailed of memory_subsystem is

    signal internal_data : MVL4_logic_vector(31 downto 0);
    ...
   begin

     boot_ROM : entity work.ROM(behavioral)
       port map ( a => internal_addr(15 downto 0),
                  d => internal_data(7 downto 0),
                  rd => ROM_select );
     main_mem : entity work.SIMM(behavioral)
       port map ( a => main_mem_addr, d => internal_data, ... );
     ...
   
   end architecture detailed;

Summary of Resolved Subtypes

At this point, let us summarize the important points about resolved signals and their resolution functions. Resolved signals of resolved subtypes are the only means by which we may connect a number of sources together, since we need a resolution function to determine the final value of the signal or port from the contributing values. The resolution function must take a single parameter that is a one-dimensional unconstrained array of values of the signal type, and must return a value of the signal type. The index type of the array does not matter, so long as it contains enough index values for the largest possible collection of sources connected together. For example, an array type declared as follows is inadequate if the resolved signal has five sources:

   type small_int is range 1 to 4;
   type small_array is array (small_int range <>) of ... ;

The resolution function must be a pure function; that is, it must not have any side effects. This requirement is a safety measure to ensure that the function always returns a predictable value for a given set of source values. Furthermore, since the source values may be passed in any order within the array, the function should be commutative; that is, its result should be independent of the order of the values. When the design is simulated, the resolution function is called whenever any of the resolved signal’s sources is active. The function is passed an array of all of the current source values, and the result it returns is used to update the signal value. When the design is synthesized, the resolution function specifies the way in which the synthesized hardware should combine values from multiple sources for a resolved signal.

IEEE std_logic_1164 Resolved Subtypes

In previous chapters we have used the IEEE standard multivalued logic package, std_logic_1164. We are now in a position to describe all of the types and subtypes provided by the package. We defer a full description of the operations provided by the package to Chapter 9, in which we also describe other standard packages based on the standard logic types. First, recall that the package provides the basic type std_ulogic, defined as

   type std_ulogic is ('U', 'X', '0', '1', 'Z', 'W', 'L', 'H', '-'),

and an array type std_ulogic_vector, defined as

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

We have not mentioned it before, but the “u” in “ulogic” stands for unresolved. These types serve as the basis for the declaration of the resolved subtype std_logic, defined as follows:

   function resolved ( s : std_ulogic_vector ) return std_ulogic;
   
   subtype std_logic is resolved std_ulogic;

The standard-logic package also declares an array subtype of standard-logic elements, analogous to the bit_vector type, for use in declaring array signals:

   subtype std_logic_vector (resolved) std_ulogic_vector;

The standard defines the resolution function resolved as follows:

   type stdlogic_table is array (std_ulogic, std_ulogic) of std_ulogic;
   constant resolution_table : stdlogic_table :=
    -- ---------------------------------------------
    -- 'U', 'X', '0', '1', 'Z', 'W', 'L', 'H', '-'
    -- ---------------------------------------------
    ( ( 'U', 'U', 'U', 'U', 'U', 'U', 'U', 'U', 'U' ),  -- 'U'
      ( 'U', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X' ),  -- 'X'
      ( 'U', 'X', '0', 'X', '0', '0', '0', '0', 'X' ),  -- '0'
      ( 'U', 'X', 'X', '1', '1', '1', '1', '1', 'X' ),  -- '1'
      ( 'U', 'X', '0', '1', 'Z', 'W', 'L', 'H', 'X' ),  -- 'Z'
      ( 'U', 'X', '0', '1', 'W', 'W', 'W', 'W', 'X' ),  -- 'W'
      ( 'U', 'X', '0', '1', 'L', 'W', 'L', 'W', 'X' ),  -- 'L'
      ( 'U', 'X', '0', '1', 'H', 'W', 'W', 'H', 'X' ),  -- 'H'
      ( 'U', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X' )   -- '-'
    );

   function resolved ( s : std_ulogic_vector ) return std_ulogic is
     variable result : std_ulogic := 'Z';  -- weakest state default
   begin
     if s'length = 1 then
       return s(s'low);
     else
       for i in s'range loop
         result := resolution_table(result, s(i));
       end loop;
     end if;
     return result;
   end function resolved;

VHDL tools are allowed to provide built-in implementations of this function to improve performance. The function uses the constant resolution_table to resolve the driving values. If there is only one driving value, the function returns that value unchanged. If the function is passed an empty array, it returns the value ‘Z’. (The circumstances under which a resolution function may be invoked with an empty array will be covered in Section 23.1.) The value of resolution_table shows exactly what is meant by “forcing” driving values (‘X’, ‘0’ and ‘1’) and “weak” driving values (‘W’, ‘L’ and ‘H’). If one driver of a resolved signal drives a forcing value and another drives a weak value, the forcing value dominates. On the other hand, if both drivers drive different values with the same strength, the result is the unknown value of that strength (‘X’ or ‘W’). The high-impedance value, ‘Z’, is dominated by forcing and weak values. If a “don’t care” value (‘–’) is to be resolved with any other value, the result is the unknown value ‘X’. The interpretation of the “don’t care” value is that the model has not made a choice about its output state. Finally, if an “uninitialized” value (‘U’) is to be resolved with any other value, the result is ‘U’, indicating that the model has not properly initialized all outputs.

In addition to this multivalued logic subtype, the package std_logic_1164 declares a number of subtypes for more restricted multivalued logic modeling. The subtype declarations are

   subtype X01 is
     resolved std_ulogic range 'X' to '1'; -- ('X','0','1')
   subtype X01Z is
     resolved std_ulogic range 'X' to 'Z'; -- ('X','0','1','Z')
   subtype UX01 is
     resolved std_ulogic range 'U' to '1'; -- ('U','X','0','1')
   subtype UX01Z is
     resolved std_ulogic range 'U' to 'Z'; -- ('U','X','0','1','Z')

Each of these is a closed subtype; that is, the result of resolving values in each case is a value within the range of the subtype. The subtype X01Z corresponds to the type MVL4 we introduced in Example 8.1.

Resolved Signals, Ports, and Parameters

In the previous discussion of resolved signals, we have limited ourselves to the simple case where a number of drivers or output ports of component instances drive a signal. Any input port connected to the resolved signal gets the final resolved value as the port value when a transaction is performed. We now look in more detail at the case of ports of mode inout being connected to a resolved signal. The question to answer here is, What value is seen by the input side of such a port? Is it the value driven by the component instance or the final value of the resolved signal connected to the port? In fact, it is the latter. An inout port models a connection in which the driver contributes to the associated signal’s value, and the input side of the component senses the actual signal rather than using the driving value.

Example 8.4. Distributed synchronization using a wired-and signal

Some asynchronous bus protocols use a distributed synchronization mechanism based on a wired-and control signal. This is a single signal driven by each module using active-low open-collector or open-drain drivers and pulled up by the bus terminator. If a number of modules on the bus need to wait until all are ready to proceed with some operation, they use the control signal as follows. Initially, all modules drive the signal to the ‘0’ state. When each is ready to proceed, it turns off its driver (‘Z’) and monitors the control signal. So long as any module is not yet ready, the signal remains at ‘0’. When all modules are ready, the bus terminator pulls the signal up to the ‘1’ state. All modules sense this change and proceed with the operation.

Following is an entity declaration for a bus module that has a port of the unresolved type std_ulogic for connection to such a synchronization control signal.

   library ieee;  use ieee.std_logic_1164.all;

   entity bus_module is
     port ( synch : inout std_ulogic;  ... );
   end entity bus_module;

The architecture body for a system comprising several such modules is outlined below. The control signal is pulled up by a concurrent signal assignment statement, which acts as a source with a constant driving value of ‘H’. This is a value having a weak strength, which is overridden by any other source that drives ‘0’. It can pull the signal high only when all other sources drive ‘Z’.

   architecture top_level of bus_based_system is
     signal synch_control : std_logic;
     ...
   begin

     synch_control_pull_up : synch_control <= 'H';

     bus_module_1 : entity work.bus_module(behavioral)
       port map ( synch => synch_control, ... );

     bus_module_2 : entity work.bus_module(behavioral)
       port map ( synch => synch_control, ... );
     ...
   end architecture top_level;

An outline of a behavioral architecture body for the bus module is:

   architecture behavioral of bus_module is
   begin

     behavior : process is
       ...
    begin
       synch <= '0'  after Tdelay_synch;
       ...
       -- ready to start operation
       synch <= 'Z' after Tdelay_synch;
       wait until synch = 'H';
       -- proceed with operation
       ...
    end process behavior;

   end architecture behavioral;

Each instance initially drives its synchronization port with ‘0’. This value is passed up through the port and used as the contribution to the resolved signal from the entity instance. When an instance is ready to proceed with its operation, it changes its driving value to ‘Z’, modeling an open-collector or open-drain driver being turned off. The process then suspends until the value seen on the synchronization port changes to ‘H’. If other instances are still driving ‘0’, their contributions dominate, and the value of the signal stays ‘0’. When all other instances eventually change their contributions to ‘Z’, the value ‘H’ contributed by the pull-up statement dominates, and the value of the signal changes to ‘H’. This value is passed back down through the ports of each instance, and the processes all resume.

Resolved Ports

Just as a signal declared with a signal declaration can be of a resolved subtype, so too can a port declared in an interface list of an entity. This is consistent with all that we have said about ports appearing just like signals to an architecture body. Thus if the architecture body contains a number of processes that must drive a port or a number of component instances that must connect outputs to a port, the port must be resolved. The final value driven by the resolved port is determined by resolving all of the sources within the architecture body. For example, we might declare an entity with a resolved port as follows:

   library ieee;  use ieee.std_logic_1164.all;

   entity IO_section is
     port ( data_ack : inout std_logic;  ... );
   end entity IO_section;

The architecture body corresponding to this entity might instantiate a number of I/O controller components, each with their data acknowledge ports connected to the data_ack port of the entity. Each time any of the controllers updates its data acknowledge port, the standard-logic resolution function is invoked. It determines the driving value for the data_ack port by resolving the driving values from all controllers.

Example 8.5. A memory system with tristate buses

We can write a model for a memory system composed of multiple memory devices with tristate data buses. The entity declaration for the memory devices is

   library ieee; use ieee.std_logic_1164.all;
   entity memory_256Kx8 is
     port ( ce_n, oe_n, we_n : in std_ulogic;
            a : in std_ulogic_vector(17 downto 0);
            d : inout std_ulogic_vector(7 downto 0) );
   end entity memory_256Kx8;

The d port of the entity has unresolved elements because there is only one source for each element within the memory device. The entity declaration for the memory system is

   library ieee; use ieee.std_logic_1164.all;
   entity memory_1Mx8 is
     port ( ce_n, oe_n, we_n : in std_ulogic;
            a : in std_ulogic_vector(19 downto 0);
            d : inout std_logic_vector(7 downto 0) );
   end entity memory_1Mx8;

In this case, the d port is of type std_logic_vector with resolved elements, since internally there are multiple sources, one per memory device. The structural architecture is

   architecture struct of memory_1Mx8 is
     signal ce_decoded_n : std_ulogic_vector(3 downto 0);
   begin

     with a(19 downto 18) select
       ce_decoded_n <= "1110" when "00",
                       "1101" when "01",
                       "1011" when "10",
                       "0111" when "11",
                       "XXXX" when others;

     chip0 : component memory_256Kx8
       port map ( ce_n => ce_decoded_n(0),
                  oe_n => oe_n, we_n => we_n,
                  a => a(17 downto 0), d => d );

     chip1 : component memory_256Kx8
       port map ( ce_n => ce_decoded_n(1),
                  oe_n => oe_n, we_n => we_n,
                  a => a(17 downto 0), d => d );
    ...
    end architecture struct;

We can connect the d port of each memory-device instance directly to the d port of the memory system. The contributions of all of the instances are resolved to form the driving value of the memory system’s d port. Had we inadvertently declared the d port of the memory system entity to be of type std_ulogic_vector, the analyzer would detect the error arising from multiple sources connected to the unresolved elements.

If it happens that the actual signal associated with a resolved port in an enclosing architecture body is itself a resolved signal, then the signal’s resolution function will be called separately after the port’s resolution function has determined the port’s driving value. Note that the signal in the enclosing architecture body may use a different resolution function from the connected port, although in practice most designs use the one function for resolution of all signals of a given subtype.

An extension of the above scenario is a design in which there are several levels of hierarchy, with a process nested at the deepest level generating a value to be passed out through resolved ports to a signal at the top level. At each level, a resolution function is called to determine the driving value of the port at that level. The value finally determined for the signal at the top level is called the effective value of the signal. It is passed back down the hierarchy of ports as the effective value of each in-mode or inout-mode port. This value is used on the input side of each port.

Example 8.6. Hierarchical resolution in a computer system model

Figure 8.1 shows the hierarchical organization for a single-board computer system, consisting of a frame buffer for a video display, an input/output controller section, a CPU/memory section and a bus expansion block. These are all sources for the resolved data bus signal. The CPU/memory section in turn comprises a memory block and a CPU/cache block. Both of these act as sources for the data port, so it must be a resolved port. The cache has two sections, both of which act as sources for the data port of the CPU/cache block. Hence, this port must also be resolved.

Let us consider the case of one of the cache sections updating its data port. The new driving value is resolved with the current driving value from the other cache section to determine the driving value of the CPU/cache block data port. This result is then resolved with the current driving value of the memory block to determine the driving value of the CPU/memory section. Next, this driving value is resolved with the current driving values of the other top-level sections to determine the effective value of the data bus signal. The final step involves propagating this signal value back down the hierarchy for use as the effective value of each of the data ports. Thus, a module that reads the value of its data port will see the final resolved value of the data bus signal. This value is not necessarily the same as the driving value it contributes.

A hierarchical block diagram of a single-board computer system, showing the hierarchical connections of the resolved data bus ports to the data bus signal.

Figure 8.1. A hierarchical block diagram of a single-board computer system, showing the hierarchical connections of the resolved data bus ports to the data bus signal.

In Chapter 5, we indicated that we can read the value of a buffer-mode or out-mode port within an architecture of an entity. There is an important distinction between this and reading the value of an inout-mode port. As we mentioned above, the value seen internally for an inout-mode port is the effective value of the externally connected signal. Information is passed from the external connection into the architecture, as suggested by the “in” part of the inout port mode. In contrast, when we read a buffer-mode or out-mode port, the value seen internally is the driving value of the port. There is no transmission of information into the architecture. We use a buffer-mode or out-mode port for designs that have buffered internal connections or that read the value of the port for internal verification, for example, using assertions.

Example 8.7. Verification of tristate disconnection timing

Suppose we wish to verify that the outputs of a device are all ‘Z’ within a required interval of the device being disabled and remain all ‘Z’ 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
     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
     constant T_z : delay_length := 200 ps;
   begin
     d_out <= ... when en else
              ... when not en else
              "XXXXXXXX";
     assert en or (not en'delayed(T_z) and d_out ?= "ZZZZZZZZ");
   end architecture verifying;

Driving Value Attribute

Since the value seen on a signal or on an inout-mode port may be different from the value driven by a process, VHDL provides an attribute, ‘driving_value, that allows the process to read the value it contributes to the prefix signal. For example, if a process has a driver for a resolved signal s, it may be driving s with the value ‘Z’ from a previously executed signal assignment statement, but the resolution function for s may have given it the value ‘0’. The process can refer to s‘driving_value to retrieve the value ‘Z’. Note that a process can only use this attribute to determine its own contribution to a signal; it cannot directly find out another process’s contribution.

VHDL-87

The ‘driving_value attribute is not provided in VHDL-87.

Resolved Signal Parameters

Let us now return to the topic of subprograms with signal parameters and see how they behave in the presence of resolved signals. Recall that when a procedure with an out-mode signal parameter is called, the procedure is passed a reference to the caller’s driver for the actual signal. Any signal assignment statements performed within the procedure body are actually performed on the caller’s driver. If the actual signal parameter is a resolved signal, the values assigned by the procedure are used to resolve the signal value. No resolution takes place within the procedure. In fact, the procedure need not be aware that the actual signal is resolved.

In the case of reading a signal parameter to a function or procedure, a reference to the actual signal parameter is passed when the subprogram is called, and the subprogram uses the actual value of the signal. If the signal is resolved, the subprogram sees the value determined after resolution. In the case of an inout signal parameter, a procedure is passed references to both the signal and its driver, and no resolution is performed internally to the procedure.

Example 8.8. Procedures for distributed synchronization

We can encapsulate the distributed synchronization protocol described in Example 8.4 in a set of procedures, each with a single signal parameter, as follows:

   procedure init_synchronize ( signal synch : out std_logic ) is
   begin
     synch <= '0';
   end procedure init_synchronize;

   procedure begin_synchronize ( signal synch : inout std_logic;
                              Tdelay : in delay_length := 0 fs ) is
   begin
     synch <= 'Z' after Tdelay;
     wait until synch;
   end procedure begin_synchronize;

   procedure end_synchronize ( signal synch : inout std_logic;
                            Tdelay : in delay_length := 0 fs ) is

   begin
     synch <= '0' after Tdelay;
   
     wait until not synch;
   end procedure end_synchronize;

Suppose a process uses a resolved signal barrier of subtype std_logic to synchronize with other processes. The process can use the procedures to implement the protocol as follows:

   synchronized_module : process is
    ...
   begin
    init_synchronize(barrier);
    ...
    loop
      ...
      begin_synchronize(barrier);
      ...    -- perform operation, synchronized with other processes
      end_synchronize(barrier);
      ...
    end loop;
   end process synchronized_module;

The process has a driver for barrier, since the procedure calls associate the signal as an actual parameter with formal parameters of mode out and inout. A reference to this driver is passed to init_synchronize, which assigns the value ‘0’ on behalf of the process. This value is used in the resolution of barrier. When the process is ready to start its synchronized operation, it calls begin_synchronize, passing references to its driver for barrier and to the actual signal itself. The procedure uses the driver to assign the value ‘Z’ on behalf of the process and then waits until the actual signal changes to ‘H’. When the transaction on the driver matures, its value is resolved with other contributions from other processes and the result applied to the signal. This final value is used by the wait statement in the procedure to determine whether to resume the calling process. If the value is ‘H’, the process resumes, the procedure returns to the caller and the operation goes ahead. When the process completes the operation, it calls end_synchronize to reset barrier back to ‘0’.

Exercises

1.

[Exercises8.1] Suppose there are four drivers connected to a resolved signal that uses the resolution function resolve_tri_state_logic shown on page 268. What is the resolved value of the signal if the four drivers contribute these values:

  1. ‘Z’, ‘1’, ‘Z’, ‘Z’?

  2. ‘0’, ‘Z’, ‘Z, ‘0’?

  3. ‘Z’, ‘1’, ‘Z’, ‘0’?

2.

[Exercises 8.1] Rewrite the following resolved signal declaration as a subtype declaration followed by a signal declaration using the subtype.

   signal synch_control : wired_and tri_state_logic := '0';

3.

[Exercises 8.1] What is the initial value of the following signal of the type MVL4_logic defined in How is that value derived?

   signal int_req : MVL4_logic;

4.

[Exercises 8.1] Does the result of the resolution function defined in Example 8.1 depend on the order of contributions from drivers in the array passed to the function?

5.

[Exercises 8.1] Suppose we define a resolved array subtype byte that is a subtype of word, defined in Example 8.2, with 8 elements. We then declare a signal of type byte with three drivers. What is the resolved value of the signal if the three drivers contribute these values:

  1. “ZZZZZZZZ”, “ZZZZ0011”, “ZZZZZZZZ”?

  2. “XXXXZZZZ”, “ZZZZZZZZ”, “00000011”?

  3. “00110011”, “ZZZZZZZZ”, “ZZZZ1111”?

6.

[Exercises 8.1] Suppose a signal is declared as

   signal data_bus : MVL4_logic_vector(0 to 15);

where MVL4_logic_vector is as described on page 275, and the following signal assignments are each executed in different processes:

   data_bus <= "ZZZZZZZZZZZZZZZZ";
   data_bus(0 to 7) <= "XXXXZZZZ";
   data_bus(8 to 15) <= "00111100";

What is the resolved signal value after all of the transactions have been performed?

7.

[Exercises 8.1] Suppose there are four drivers connected to a signal of type std_logic. What is the resolved value of the signal if the four drivers contribute these values:

  1. ‘Z’, ‘0’, ‘Z’, ‘H’?

  2. ‘H’, ‘Z’, ‘W’, ‘0’?

  3. ‘Z’, ‘W’, ‘L’, ‘H’?

  4. ‘U’, ‘0’, ‘Z’, ‘1’?

  5. ‘Z’, ‘Z’, ‘Z’, ‘–’?

8.

[Exercises 8.2] Figure 8.2 is a timing diagram for the system with two bus modules using the wired-and synchronization signal described in Example 8.4. The diagram shows the driving values contributed by each of the bus modules to the synch_control signal. Complete the diagram by drawing the resolved waveform for synch_control. Indicate the times at which each bus module proceeds with its internal operation.

Timing diagram for wired-and synchronization.

Figure 8.2. Timing diagram for wired-and synchronization.

9.

[Timing diagram for wired-and synchronization.8.2] Suppose all of the modules in the hierarchy of Figure 8.1 use resolved ports for their data connections. If the Mem, Cache, Serial and DMA modules all update their data drivers in the same simulation cycle, how many times is the resolution function invoked to determine the final resolved values of the data signals?

10.

[Timing diagram for wired-and synchronization. 8.2] Suppose a process in a model drives a bidirectional port synch_T of type std_logic. Write a signal assignment statement that inverts the process’s contribution to the port.

11.

[Timing diagram for wired-and synchronization. 8.1] Develop a model that includes two processes, each of which drives a signal of the type MVL4_logic described in Example 8.1. Experiment with your simulator to see if it allows you to trace the invocation and execution of the resolution function.

12.

[Timing diagram for wired-and synchronization. 8.1] Develop a model of an inverter with an open-collector output of type std_ulogic, and a model of a pull-up resistor that drives its single std_ulogic port with the value ‘H’. Test the models in a test bench that connects the outputs of a number of inverter instances to a signal of type std_logic, pulled up with a resistor instance. Verify that the circuit implements the active-low wired-or operation.

13.

[Timing diagram for wired-and synchronization. 8.1] Develop a behavioral model of an 8-bit-wide bidirectional transceiver, such as the 74245 family of components. The transceiver has two bidirectional data ports, a and b; an active-low output-enable port, oe_n; and a direction port, dir. When oe_n is low and dir is low, data is received from b to a. When oe_n is low and dir is high, data is transmitted from a to b. When oe_n is high, both a and b are high impedance. Assume a propagation delay of 5 ns for all output changes.

14.

[Timing diagram for wired-and synchronization. 8.1] Many combinatorial logic functions can be implemented in integrated circuits using pass transistors acting as switches. While a pass transistor is, in principle, a bidirectional device, for many circuits it is sufficient to model it as a unidirectional device. Develop a model of a unidirectional pass transistor switch, with an input port, an output port and an enable port, all of type std_ulogic. When the enable input is ‘H’ or ‘1’, the input value is passed to the output, but with weak drive strength. When the enable input is ‘L’ or ‘0’, the output is high impedance. If the enable input is at an unknown level, the output is unknown, except that its drive strength is weak.

15.

[Timing diagram for wired-and synchronization. 8.1] Develop a behavioral model of a tristate buffer with data input, data output and enable ports, all of type std_ulogic. The propagation time from data input to data output when the buffer is enabled is 4 ns. The turn-on delay from the enable port is 3 ns, and the turn-off delay is 3.5 ns. Use the buffer and any other necessary gate models in a structural model of the 8-bit transceiver described in Exercise 13.

16.

[Timing diagram for wired-and synchronization. 8.1] Use the unidirectional pass transistor model of Exercise 14 in a structural model of a four-input multiplexer. The multiplexer has select inputs s0 and s1. Pass transistors are used to construct the multiplexer as shown in Figure 8.3.

A multiplexer constructed of pass transistors.

Figure 8.3. A multiplexer constructed of pass transistors.

17.

[A multiplexer constructed of pass transistors. 8.1] Develop a model of a distributed priority arbiter for a shared bus in a multiprocessor computer system. Each bus requester has a request priority, R, between 0 and 31, with 0 indicating the most urgent request and 31 indicating no request. Priorities are binary-encoded using 5-bit vectors, with bit 4 being the most-significant bit and bit 0 being the least-significant bit. The standard-logic values ‘H’ and ‘1’ both represent the binary digit 1, and the standard-logic value ‘0’ represents the binary digit 0. All requesters can drive and sense a 5-bit arbitration bus, A, which is pulled up to ‘H’ by the bus terminator. The requesters each use A and their own priority to compute the minimum of all priorities by comparing the binary digits of priorities as follows. For each bit position i:

  • if (R4...i = A4...i + 1) and (Ri = 0) : drive Ai with ‘0’ after Tpd

  • if (R4...i + 1A4...i + 1) or (Ri = 1) : drive Ai with ‘Z’ after Tpd

Tpd is the propagation delay between sensing a value on A and driving a resulting value on A. When the value on A has stabilized, it is the minimum of all request priorities. The requester with R = A wins the arbitration. If you are not convinced that the distributed minimization scheme operates as required, trace its execution for various combinations of priority values.

18.

[A multiplexer constructed of pass transistors.] Develop a behavioral model of a telephone keypad controller. The controller has outputs c1 to c3 and inputs r1 to r4, connected to the 12 switches of a touch-tone telephone as shown in Figure 8.4.

Keypad switch connections for a touch-tone telephone.

Figure 8.4. Keypad switch connections for a touch-tone telephone.

Each key in the keypad is a single-pole switch that shorts the row signal to the column signal when the key is pressed. Due to the mechanical construction of the switch, “switch bounce” occurs when the key is pressed. Several intermittent contacts are made between the signals over a period of up to 5 ms before a sustained contact is made. Bounce also occurs when the key is released. Several intermittent contacts may occur over the same period before sustained release is achieved.

The keypad controller scans the keypad by setting each of the column signals to ‘0’ in turn. While a given column signal is ‘0’, the controller examines each of the row inputs. If a row input is ‘H’, the switch between the column and the row is open. If the row input is ‘0’, the switch is closed. The entire keypad is scanned once every millisecond.

The controller generates a set of column outputs c1_out to c3_out and a set of row outputs r1_out to r4_out. A valid switch closure is indicated by exactly one column output and exactly one row output going to ‘1’ at the same time. The controller filters out spurious switch closures due to switch bounce and ignores multiple concurrent switch closures.

19.

[Keypad switch connections for a touch-tone telephone.] The IEEE standard-logic type models two drive strengths: forcing and weak. This is insufficient to model detailed operation of circuits at the switch level. For example, in circuits that store a charge on the gate terminal of a MOS transistor, we need to distinguish the weaker capacitive drive strength of the stored value from the resistive strength of a value transmitted through a pass transistor. Develop a package that defines a resolved type similar to std_logic, with forcing, resistive and capacitive strengths for 0, 1 and unknown values.

20.

[Keypad switch connections for a touch-tone telephone.] Exercise 19 describes a logic type that incorporates three drive strengths. If we need to model switch level circuits in finer detail, we can extend the type to deal with an arbitrary number of drive strengths. Each time a signal is transmitted through a pass transistor, its drive strength is diminished. We can model this by representing a logic value as a record containing the bit value (‘0’, ‘1’ or unknown) and an integer representing the strength. We use 0 to represent power supply strength and a positive integer n to represent the strength of a signal after being transmitted through n pass transistors from the power supply. A normal driver has strength 1, to reflect the fact that it derives the driving value by turning on a transistor connected to one or the other power supply rail. (This scheme is described by Smith and Acosta in [14].)

Develop a package that defines a resolved type based on this scheme. Include functions for separating the bit value and strength components of a combined value, for constructing a combined value from separate bit value and strength components and for weakening the strength component of a combined value. Use the package to model a pass transistor component. Then use the pass transistor in a model of an eight-input multiplexer similar to the four-input multiplexer of Exercise 16.

21.

[Keypad switch connections for a touch-tone telephone.] Self-timed asynchronous systems use handshaking to synchronize operation of interacting modules. In such systems, it is sometimes necessary to synchronize a number of modules at a rendezvous. Each module waits until all modules are ready to perform an operation. When all are ready, the operation commences. A scheme for rendezvous synchronization of a number of modules using three wired-and control signals was first proposed by Sutherland et al. for the TRIMOSBUS [15] and was subsequently adopted for use in the arbitration protocol of the IEEE Futurebus [11].

Develop a high-level model of a system that uses the three-wire synchronization scheme. You should include a package to support your model. The package should include a type definition for a record containing the three synchronization wires and a pair of procedures, one to wait for a rendezvous and another to leave the rendezvous after completion of the operation. The procedures should have a bidirectional signal parameter for the three-wire record and should determine the state of the synchronization protocol from the parameter value.

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

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