VHDL includes few built-in types, but offers a number of additional types through extension packages. Two of the most widely used types are the std_logic and std_ulogic. The difference between them is that the former is resolved, while the latter isn’t.

Before we go on to investigate what it means that a type is resolved, let’s first look at the traits that the two types share in common.

Bit and boolean are part of the standard package, requiring no imports to use them. But std_logic and std_ulogic can only be used after importing the IEEE 1164 package. To import the IEEE 1164 package, simply add these lines to the top of the VHDL file:

library ieee;
use ieee.std_logic_1164.all;

The two types std_logic and std_ulogic both have in common that they can represent the following values:

‘1’ Logic 1
‘0’ Logic 0
‘Z’ High impedance
‘W’ Weak signal, can’t tell if 0 or 1
‘L’ Weak 0, pulldown
‘H’ Weak 1, pullup
‘-‘ Don’t care
‘U’ Uninitialized
‘X’ Unknown, multiple drivers

Most of the time you will use '1' and '0' to indicate a logic high or low value. And 'U' will be used for representing uninitialized values, such as RAM content at startup. Occasionally you will see the 'X' value, indicating some sort of driver conflict.

std_ulogic – The unresolved type

Let’s first have a look at the std_ulogic type, the unresolved version of the two. Below is an excerpt of the type declaration taken from an implementation of the std_logic_1164 package.

TYPE std_ulogic IS ( 'U',  -- Uninitialized
					 'X',  -- Forcing  Unknown
					 '0',  -- Forcing  0
					 '1',  -- Forcing  1
					 'Z',  -- High Impedance   
					 'W',  -- Weak     Unknown
					 'L',  -- Weak     0       
					 'H',  -- Weak     1       
					 '-'   -- Don't care
				   );

The std_ulogic is simply an enumerated type which lists the possible values as enumeration literals. When a signal or variable is declared with std_ulogic as the type, it can represent any one of these values, and none other.

The 'U' value is the first of the listed values. That’s the reason why a signal of the std_ulogic type will get the value 'U' if given no initial value. By default, an uninitialized signal will get the leftmost value from the type declaration. That’s how VHDL works.

What does it mean that a type is unresolved?

If multiple drivers are driving different values onto a signal of the std_ulogic type, that’s not going to work. Conflicting drivers is an error in VHDL. The simulation won’t compile or the design won’t synthesize. Driver conflicts are not resolved with the std_ulogic type. The type doesn’t have a built-in mechanism to determine what the signal value should be, so it’s an error.

Consider this example where two processes attempt to drive a signal of std_ulogic type:

library ieee;
use ieee.std_logic_1164.all;

entity UnresolvedTb is
end entity;

architecture sim of UnresolvedTb is
    signal Sig1 : std_ulogic := '0';
begin

	-- Driver A
	Sig1 <= '0';

	-- Driver B
	Sig1 <= '1' after 20 ns;
	
end architecture;

If we try to compile this in ModeSim, it reports the following error:

# ** Error: C:/proj/tb.vhd(8): Nonresolved signal 'Sig1' has multiple sources.
#   Drivers:
#     C:/proj/tb.vhd(12):Conditional signal assignment line__12
#     C:proj/tb.vhd(15):Conditional signal assignment line__15
# ** Error: C:/proj/tb.vhd(17): VHDL Compiler exiting

std_logic – The resolved type

The std_logic can represent the same values as the std_ulogic, but it’s a resolved type. What does that mean? To find out, let’s once again refer to the implementation of the standard. Below is the declaration taken from the IEEE 1164 package.

SUBTYPE std_logic IS resolved std_ulogic;

The subtype keyword in VHDL is normally used for declaring a type with a limited range from the base type. But it can also be used for specifying a resolution function. That’s what the above statement is saying. The std_logic is a subtype of std_ulogic, and the name of the resolution function is “resolved”.

Now, let’s examine the “resolved” function:

FUNCTION resolved ( s : std_ulogic_vector ) RETURN std_ulogic IS
	VARIABLE result : std_ulogic := 'Z';
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 resolved;

It accepts one parameter, an array of std_ulogic representing all the simultaneous drivers. It then compares all the elements in the vector to each other, one by one, reducing them to a single std_ulogic value which is returned.

For comparing the values, what seems to be a different function named “resolution_table” is called. But it’s actually a two-dimensional array:

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' )  -- | - |
	);

The resolution function is what governs the final value that a signal will have in case of multiple drivers.

This is the same example code as before, but with the std_ulogic converted to std_logic:

library ieee;
use ieee.std_logic_1164.all;

entity ResolvedTb is
end entity;

architecture sim of ResolvedTb is
    signal Sig1 : std_logic := '0';
begin

	-- Driver A
	Sig1 <= '0';

	-- Driver B
	Sig1 <= '1' after 20 ns;
	
end architecture;

The code now compiles in ModelSim without errors. When we run it, it produces this waveform:
Waveform showing the resolved signal going from '0' to 'X' after 20 ns

At first, only Driver A is driving a '0' value onto Sig1. After 20 nanoseconds, Driver B starts driving '1' onto the same signal. The conflicting values were resolved according to the resolution table, resulting in the signal getting the value 'X':

Annotated resolution table showing how the values '0' and '1' were resolved to 'X'

Why use std_logic?

The std_logic is generally preferred over the built-in bit or boolean types in VHDL. This is because they give us more information than the simple '1' or '0', on or off. If you prefer to have the simulator treat multiple drivers as hard errors and stop at the first occurrence, you could of course use std_ulogic.

Unintended multiple drivers probably won’t go undetected, even when using the std_logic type. You will likely see the “metavalue detected” warnings in the simulator transcript window as the 'X' values propagate through the datapath.

std_logic has become the most used type for internal and external interfaces because it accurately models the behaviors that a digital electrical signal can exhibit. The std_logic type offers a number of benefits and use cases including the following.

Uninitialized values

Tracking of uninitialized values is useful for detecting missing resets and the use of uninitialized data. Since block RAM cannot have have reset values, they will contain only 'U' values at simulation start. This makes it easy for us to spot uninitialized RAM content being used in the simulation waveform. With a two-value type like bit or boolean, such errors would go undetected.

Tri-state logic

Implementing tri-state logic is best done with a resolved signal like std_logic. The high impedance state 'Z' is normally used for releasing the bus while another driver has control of it. In VHDL, when a process begins to drive a value onto a signal, there is no undo. There exists no keyword to stop driving. The only way to release the signal is by driving a weaker value which can by overridden by another driver.

Modeling external interfaces

To accurately model an external signal, a boolean value is not enough. External digital interfaces can exhibit behaviors like pull-up or pull-down logic. Without the 'H' and 'L' values, it would be difficult to simulate interfaces that rely on pulling like I2C or SPI.

Propagating errors downstream

A strategy that will make erroneous usage of modules easier to detect is setting the outputs to 'X' when the data is invalid, for example when the valid output is '0'. Downstream modules that sample the outputs at the wrong time will receive the 'X' value. This will cause a simulation error or metavalue warning, rather than an invalid '0' or '1' silently being used. It can prevent errors that are otherwise difficult to detect.

We did this when implementing a multiplexer in the Case-When tutorial.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.