VHDL is a strongly typed language, meaning that you cannot simply assign a literal value or object of one type to an object of another type. To allow the transfer of data between objects of different types, VHDL includes type conversion features for types that are closely related. VHDL also allows type conversion functions to be written for types that are not closely related. In addition, VHDL includes type mark features to help specify (or qualify) the type of a literal value when the context or format of the literal makes its type ambiguous.

 

Explicit type conversions

The simplest type conversions are explicit type conversions, which are only allowed between closely related types. Two types are said to be closely related when they are either abstract numeric types (integers or floating points), or if they are array types of the same dimensions and share the same types (or the element types themselves are closely related) for all elements in the array. In the case of two arrays, it is not necessary for the arrays to have the same direction. If two subtypes share the same base type, then no explicit type conversion is required.

 

The following example demonstrates implicit and explicit type conversions:

 

architecture example of typeconv is

    type array1 is array(0 to 7) of std_logic;

    type array2 is array(7 downto 0) of std_logic;

    subtype array3 is std_logic_vector(0 to 7);

    subtype array4 is std_logic_vector(7 downto 0);

    signal a1: array1;

    signal a2: array2;

    signal a3: array3;

    signal a4: array4;

 

begin

    a2 <= array2(a1);   -- explicit type conversion

    a4 <= a3;                -- no explicit type conversion needed

end example;

 

Type conversion functions

To convert data from one type to an unrelated type (such as from an integer type to an array type), you must make use of a type conversion function. Type conversion functions may be obtained from standard libraries (such as the IEEE 1164 library), from vendor-specific libraries (such as those supplied by synthesis tool vendors), or you can write you own type conversion functions.

 

A type conversion function is a function that accepts one argument of a specified type and returns the equivalent value in another type.

The following two functions are examples of type conversion functions that convert between integer and array (std_ulogic_vector) data types:

 

  -------------------------------------------------------------------------

    -- Convert a std_ulogic_vector to an unsigned integer

    --

    function to_uint (a: std_ulogic_vector) return integer is

        alias av: std_ulogic_vector (1 to a'length) is a;

        variable val: integer := 0;

        variable b: integer := 1;

    begin

        for i in a'length downto 1 loop

            if (av(i) = '1') then    -- if LSB is '1',

                val := val + b;       -- add value for current bit position

            end if;

            b := b * 2;    -- Shift left 1 bit

        end loop;

 

        return val;

    end to_uint;

 

-------------------------------------------------------- 

    -- Convert an integer to a std_ulogic_vector

    --

    function to_vector (size: integer; val: integer) return std_ulogic_vector is

        variable vec: std_ulogic_vector (1 to size);

        variable a: integer;

    begin

        a := val;

        for i in size downto 1 loop

            if ((a mod 2) = 1) then

                vec(i) := '1';

            else

                vec(i) := '0';

            end if;

            a := a / 2;

        end loop;

        return vec;

    end to_vector;

 

The following example (a loadable counter) demonstrates how these two functions could be used:

 

library ieee;

use ieee.std_logic_1164.all;

 

library types;       -- Type conversions have been compiled into library 'types'

use types.conversions.all;

 

entity count16 is

    port (Clk,Rst,Load: in std_ulogic;

          Data: in std_ulogic_vector(3 downto 0);

          Count: out std_ulogic_vector(3 downto 0));

end count16;

 

architecture count16a of count16 is

begin

    process(Rst,Clk)

        variable Q: integer range 0 to 15;

    begin

        if Rst = '1' then                -- Asynchronous reset

            Q := 0;

        elsif rising_edge(Clk) then

            if Load = '1' then

                Q := to_uint(Data);  -- Convert vector to integer

            elsif Q = 15 then

                Q := 0;

            else

                Q := Q + 1;

            end if;

        end if;

 

        Count <= to_vector(4,Q);         -- Convert integer to vector

                                         -- for use outside the process.

    end process;

end count16a;

 

In this example, the interface specified in the entity port list uses standard logic data types, including a std_ulogic_vector array data type for the counter output. Because there are no arithmetic operations defined for the std_ulogic_vector data type, it is necessary to introduce an intermediate integer variable and convert the Data input from a std_ulogic_vector type to an integer when assigning it to the intermediate variable, and to convert the intermediate variable back to a std_ulogic_vector array type when assigning it to the Count output.

 

Synthesis note:

The preceding example is unlikely to be synthesizable because the type conversion functions (to_uint() and to_vector()) are written using unconstrained integers. As a practical matter, you should never write an arbitrary-width type conversion function that you intend to use in a synthesizable design description. Instead, you should make use of type conversion functions provided by your synthesis vendor or use the 1076.3 signed or unsigned type (see the section Using Standard Logic).

 

Another common application of type conversion functions is the conversion of string data read from a file to array or record data types suitable for use as stimulus in a test bench. The following function accepts data in the form of a fixed-length string and converts it, character by character, into a record data type:

 

    type test_record is record

        CE: std_ulogic;  -- Clock enable

        Set: std_ulogic;  -- Preset

        Din: std_ulogic;  -- Binary data input

        Doutput: std_ulogic_vector (15 downto 0);  -- Expected output

    end record;

 

    function str_to_record(s: string(18 downto 0)) return test_record is

        variable temp: test_record;

    begin

        case s(18) is

            when '1' => temp.CE := '1';

            when '0' => temp.CE := '0';

            when others => temp.CE = 'X';

        end case;

        case s(17) is

            when '1' => temp.Set := '1';

            when '0' => temp.Set := '0';

            when others => temp.Set = 'X';

        end case;

        case s(16) is

            when '1' => temp.Din := '1';

            when '0' => temp.Din := '0';

            when others => temp.Din = 'X';

        end case;

        for i in 15 downto 0 loop

            case s(i) is

                when '1' => temp.Doutput := '1';

                when '0' => temp.Doutput := '0';

                when others => temp.Doutput = 'X';

            end case;

        end loop;

        return temp;

    end str_to_record;

 

There are many applications of type conversion functions, and many possible ways to write them. If you are writing a synthesizable design description, you should (whenever possible) make use of type conversions that have been provided to you by your synthesis vendor, as type conversion functions can be difficult (in some cases impossible) for synthesis tools to handle.

 

Ambiguous literal types

Functions and procedures in VHDL are uniquely identified not only by their names, but also by the types of their arguments. (See Subprogram Overloading in Chapter 7, Creating Modular Designs.)  This means that you can, for example, write two functions to perform similar tasks, but on different types of input data. The ability to overload functions and procedures can lead to ambiguities when functions are called, if the types of one or more arguments are not explicitly stated.

 

For example, consider two type conversion functions with the following interface declarations:

 

function to_integer (vec: bit_vector) return integer is

    . . .

end to_uint;

 

function to_integer (s: string) return integer is

    . . .

end to_uint;

 

If you were to write an assignment statement such as:

 

architecture ambiguous of my_entity is

    signal Int35: integer;

begin

    Int35 <= to_integer("00100011");   -- This will produce an error

    . . .

end ambiguous;

 

then the compiler would produce an error message because it would be unable to determine which of the two functions is appropriate—the literal "00100011" could be either a string or bit_vector data type.

 

To remove data type ambiguity in such cases, you have two options: you can either introduce an intermediate constant, signal or variable, as in:

 

architecture unambiguous1 of my_entity is

    constant Vec35: bit_vector := "00100011";

    signal Int35: integer;

begin

    Int35 <= to_integer(Vec35);

    . . .

end unambiguous1;

 

or introduce a type mark to qualify the argument, as in:

 

architecture unambiguous2 of my_entity is

    signal Int35: integer;

begin

    Int35 <= to_integer(bit_vector'"00100011");

    . . .

end unambiguous2;

 

Resolved and unresolved types

A signal requires resolution whenever it is simultaneously driven with more than one value. By default, data types (whether standard types or types you define) are unresolved, resulting in errors being generated when there are multiple values being driven onto signals of those types. These error messages may be the desired behavior, as it is usually a design error when such conditions occur. If you actually intend to drive a signal with multiple values (as in the case of a bus interface), then you will need to use a resolved data type.

Data types are resolved only when a resolution function has been included as a part of their definition. A resolution function is a function that specifies, for all possible combinations of one or more input values (expressed as an array of the data type being resolved), what the resulting (resolved) value will be.

 

The following sample package defines a resolved data type consisting of four possible values, '0', 1', 'X' and 'Z'. The resolution function covers all possible combinations of input values and specifies the resolved value corresponding to each combination:

 

package types is

    type xbit is ( '0',  -- Logical  0

                         '1',  -- Logical  1

                         'X',  -- Unknown      

                         'Z'   -- High Impedance );

 

    -- unconstrained array is required for the resolution function...

    type xbit_vector is array ( natural range <> ) of xbit;

                                    

    -- resolution function...

    function resolve_xbit ( v : xbit_vector ) return xbit;

 

    -- resolved logic type...

    subtype xbit_resolved is resolve_xbit xbit;

 

end types;

 

package body types is

 

    -- Define resolutions as a table...

 

    type xbit_table is array(xbit, xbit) of xbit;

    constant resolution_table: xbit_table := (

    --         0    1    X    Z

            ( '0', 'X', 'X', '0' ), --  0

            ( 'X', '1', 'X', '1' ), --  1

            ( 'X', 'X', 'X', 'X' ), --  X

            ( '0', '1', 'X', 'Z' ) --  Z

     );

        

    function resolve_xbit ( v: xbit_vector ) return xbit is

        variable result: xbit;

    begin

        -- test for single driver

        if (v'length = 1) then

            result := v(v'low); -- Return the same value if only 1 value

        else

            result := 'Z';

            for i in v'range loop

                result := resolution_table(result, v(i));

            end loop;

        end if;

        return result;

    end resolve_xbit;

 

end types;

 

The resolution function is invoked automatically whenever a signal of the associated type is driven with one or more values. The array argument v represents all of the values being driven onto the signal at any given time.

 

With the types xbit and xbit_resolved defined in this way, the resolved data type xbit_resolved can be used for situations in which resolutions are required. The following example shows how the resolved type xbit_resolved could be used to describe the operation of a pair of three-state signals driving a common signal:

 

use work.types.all;

entity threestate is

    port (en1, en2: in xbit_resolved;

            A,B: in xbit_resolved;

            O: out xbit_resolved);

end threestate;

 

architecture sample of threestate is

    signal tmp1,tmp2: xbit_resolved;

begin

 

    tmp1 <= A when en1 else 'Z';

    tmp2 <= B when en2 else 'Z';

 

    O <= tmp1;

    O <= tmp2;

 

end sample;

 

In this example, the output O could be driven with various combinations of the values of A and B and the value 'Z', depending on the states of the two inputs en1 and en2. The resolution function takes care of calculating the correct value for O for any of these combinations during simulation.

 

More information about resolved types, in particular the standard resolved types std_logic and std_logic_vector, can be found in the section Using Standard Logic.