An Introduction to VHDL Data Types

In this post, we talk about the most commonly used data types in VHDL. We will also look at how we perform conversions between these types.

VHDL is considered to be a strongly typed language. This means every signal or port which we declare must use either one of the predefined VHDL types or a custom type which we have created.

The type which we use defines the characteristics of our data. We can use types which interpret data purely as logical values, for example. We can also use types which interpret our data as if it were a numeric value.

Whenever we assign data to a signal, the data which we assign must adhere to the rules of the types. The code will not compile correctly if we attempt to mix incompatible data types. As a result, it is often necessary to explicitly perform type conversions in VHDL.

Basic VHDL Types

With a few exceptions, every signal or port in a VHDL design fundamentally consists of one or more logical bits. We use the different types in VHDL to tell our tools how this collection of bits should be interpreted.

This means that the simplest type we can use in VHDL consists of a single logical bit. There are actually two different types we can use for this purpose.

Let’s take a closer look at both of these types.

bit Type in VHDL

The bit type is the simplest of all types in VHDL. We use this type to model a single logical value within our FPGA. The bit type can only ever have a value or either 1b or 0b.

The code snippet below shows the method we use to declare a bit type signal in VHDL.

signal <signal_name> : bit;

When we assign single bit data types, we use apostrophes (‘) to represent the data. For example, if we want to set a single bit to 1b we would assign it to ‘1’ in our code.

In the post on entities and architectures in VHDL, we saw how we use signals to model connections in our design. When we assign data to signals we use the <= symbol. We also use this symbol when assigning data to a port.

In VHDL, we can also use variables to model wires in our design. When we assign data to a variable we use the := symbol. We discuss variables in more depth in the post on VHDL process blocks.

The code snippet below shows how we can assign values to a signal or port which uses the bit type.

-- Assigning a bit type to logical 1
bit_example <= '1';
-- Assigning a bit type to logical 0
bit_example <= '0';

std_logic Type in VHDL

The other type which we can use to model a single bit in our FPGA is the std_logic type. Although this is similar to the bit type, we have a greater range of values which we can assign to our signal when we use this type.

The code snippet below shows how we declare a std_logic type signal in VHDL.

signal <signal_name> : std_logic;

In a digital circuit, we are mainly concerned with binary data. However, we can also use drivers which set signals to values other than 0b or 1b. It is possible to drive the output of an FPGA pin to high impedance, for example.

The std_logic type attempts to capture this broader range of possibilities. In addition to this, it also models conditions where the logic value is unpredictable. This typically occurs due to errors in our design.

The table below shows the full list of values that the std_logic type can take in VHDL.

ValueDescription
‘U’Uninitialised signal which hasn’t been assigned a value yet.
‘X’Unknown value as it’s impossible to determine the value.
‘0’Logic level 0
‘1’Logic level 1
‘Z’High impedance
‘W’Weak signal, it’s not possible to determine the logic level
‘L’Signal has a weak pull down meaning it should go to 0
‘H’Weakly pulled down signal that should probably go to 1
‘-‘Don’t care.

As with the bit type, we assign data to a std_logic type signal using apostrophes (‘) to represent the data. The code snippet below shows how we can assign values to a signal or port which uses the std_logic type.

-- Assigning a std_logic type to logical 1
sl_example <= '1';
-- Assigning a std_logic type to logical 0
sl_example <= '0';
-- Assigning a std_logic type to high impedance
sl_example <= 'z';

Uninitialised or Unknown Values

The std_logic type not only lets us model high impedance signals but also models unknown values. We can get unknown or uninitialised values in our design under two circumstances.

The first circumstance is the simplest to understand. If we have signals in our design which aren’t assigned a value at the start of simulation, they will show as uninitialized until they are assigned data.

In the screen shot below we can see how an uninitialized signal looks in a simulation environment. This screen shot is taken from the Vivado design tool which shows the uninitialized signals in the design as orange in the wave viewer.

In this instance, the design uses a PLL to generate the internal clock signals. We can see here that the error signal is uninitialized until the PLL starts outputting a clock.

The second case when we can get unknown values occurs when we drive a signal from more than one source. As an example, consider the simple circuit diagram below which features 2 D type flip flops driving the same wire.

Circuit digram showing 2 D type flip flops which have their outputs connected together

We should never intentionally design a digitial circuit which connects the output of 2 flip flops like this. The reason for this is that the behaviour of the circuit is not deterministic.

To demonstrate this, what would we expect the value of the signal to be if one fo the flip flops drove the output to 1b and the other drove it to 0b?

Actually, the answer is that we simply don’t known. If we can’t answer this question then it is quite clear that we have non-deterministic behaviour in our circuit.

When we write VHDL code, it is possible to create a non-deterministic circuit such as this one. This typically occurs if we assign data to a signal in more than one concurrent statement or process. This would lead to a signal which has an unknown value as our simulator can’t determine what binary value it will actually take.

Resolving std_logic Signals

When we design a digital circuit, there are occasions when we need to use circuits which have multiple drivers. For example, we may declare a port as an inout type so that we can use a bidirectional bus connected to an external flash device.

In VHDL, the std_logic type uses a concept known as resolution to allow us to use signals with multiple drivers.

To understand how resolution works in VHDL, we need to consider the drive strength of a signal. In a physical circuit, drive strength refers to the maximum amount of current it can deliver. A weak pull-up resistor can obviously deliver much less current than a MOSFET could.

The resolution function models this concept of drive strength to determine what value a signal should take when it is driven by multiple sources. To do this, a different effective drive strength is assigned to each of the possible states that a std_logic type can take.

When we drive a signal with two different values, the state with the highest drive strength takes precedent.

If we drive the signal with a mixture of ‘0’ and ‘1’ then the signal is assigned to the unknown (‘U’) state. This is because the ‘0’ and ‘1’ states have the same effective drive strength. When we use a mixture of the ‘L’ and ‘H’ states, the signal resolves to ‘W’ rather than ‘U’.

The table below shows the modelled drive strength of the different std_logic states.

The code snippet below gives some basic examples which demonstrate the functionality of the resolution function.

-- The example signal will be assigned to '1'
example <= 'z';
example <= '1';
-- The example signal will be assigned to 'H'
example <= 'z';
example <= 'h';
-- As 'L' and 'H' have the same drive strength
-- The example signal is assigned to 'W'
example <= 'l';
example <= 'h';
-- As 0 and 1 have the same drive strength
-- The example signal is assigned to 'U'
example <= '0';
example <= '1';

std_logic vs bit Type

Although we use the bit type and std_logic type to model the exact same thing in our design, the std_logic type is much more commonly used.

The main reason for this is that it provides a more realistic model of signals in a digital system. This is largely because it allows us to model various high impedance states.

Another advantage of the std_logic type is that the uninitialised state makes it easier to find signals which are not correctly driven. This is especially useful for finding bugs in circuits which feature a reset value.

Despite this, one advantage that the bit type has it that it will cause a compilation error when we design a circuit which features multiple drivers. In contrast to this, the std_logic type will compile code with multiple drivers.

However, it is normally easy to find bugs which arise due to the use of multiple drivers during simulation. This explains why the extra flexibility of the std_logic type makes it a more popular choice than the bit type despite this draw back.

There is also a std_ulogic type in VHDL which causes compilation errors when using multiple drivers. However, this type has no resolution function and is less commonly used that the std_logic type as a result.

VHDL Vector Types

The two types which we have looked at so far allow us to model single bits in our VHDL designs. However, we often use data buses which consist of multiple bits when we design digital circuits.

In VHDL, we can use vector types to model multiple bit buses. These vectors all consist of a number of bits which are modeled in a similar way to the std_logic or bit types.

Let’s take a closer look at the most commonly used vector types in VHDL.

std_logic_vector and bit_vector Types

The most basic type of vector we can use in VHDL are made up of a number of bit or std_logic types. The code snippet below shows how we declare a vector type signal in VHDL.

-- General syntax for declaring a bit_vector signal
signal <signal_name> : bit_vector(<range>);
-- General syntax for declaring a std_logic_vector signal
signal <signal_name> : std_logic_vector(<range>);

The <range> field is used to determine the number of bits in the vector and the location of the most significant and least significant bits. We use the downto and to keywords to describe the range value in VHDL.

When we use the downto keyword, the msb is the left most bit in the signal. When we use the to keyword, the msb is the right most bit of the signal. The code snippet below shows how we would declare an 8 bit signal using both keywords.

-- 8 bit std_logic_vector using downto 
signal le_sl_example : std_logic_vector(7 downto 0);
-- 8 bit bit_vector using downto 
signal le_bit_example : bit_vector(7 downto 0);
-- 8 bit std_logic_vector using to 
signal le_sl_example : std_logic_vector(0 to 7);
-- 8 bit bit_vector using to 
signal le_bit_example : bit_vector(0 to 7);

Assigning Data Values

When we assign data to vector types in VHDL we use quotation marks (“) instead of apostrophes. We can also specify data using hexadecimal notation by appending an x to the start of the data. However, this only works if the number of bits in the vector is a factor of four.

The code snippet below gives some examples of how we assign data to vector types in VHDL.

-- Assigning a value of 11b to a std_logic_vector
example <= "11";
-- Assigning a hex value to a std_logic_vector
example <= x"aa";

When we are working with the VHDL-2008 standard we can also assign vector data using an octal number. This works in the same way as hex formatted data except we must replace the x with an o. The code snippet below gives an example of this.

-- Assigning an octal value to a std_logic_vector
-- This is only valid in VHDL-2008
example <= o"55";

We can also assign data to slices or single bits of the vector. To do this we must specify the range of bits we are assigning. The code snippet below shows the general syntax for this.

<signal_name>(<range>) <= <data>;

The <range> field in the above code snippet uses either the downto or to keyword to specify the slice we are assigning. It works in the exact same as the range field we talked about when declaring a vector type signal.

If we want to assign a single bit of the vector then we replace the range value with the index of the bit. However, in this case we must remember that we are assigning a single bit of data. This means that the data value must be enclosed by apostrophes rather than quotation marks.

The VHDL code snippet below shows some examples of assigning data to slices of a std_logic_vector type.

-- Assign data to a four bit slice of a vector type
slv_example(4 downto 1) <= x"f";
-- Assign data to a single element in a vector type
slv_example(0) <= '0';

When we use bit slicing in this way, we will get compilation errors if we use an invalid range. For example, if we attempt to assign four bit data to a 3 bit slice this will cause an error.

Finally, there is one more useful function which we can use in VHDL to assign all of the bits of a vector to 1 or 0. We use the others keyword for this, as shown in the code snippet below.

-- Set all the elements in a vector type to 0
example <= (others => '0');

Signed and Unsigned VHDL Types

There are two more vector types which we often use in VHDL – signed and unsigned. In order to use these types, we need to include the numeric_std package from the standard ieee library.

When we use the signed type, the data is interpreted as a 2’s complement number. This is in contrast to the unsigned type which is a normal binary number. This means that we can assign negative values to the signed type but not the unsigned type.

The VHDL code snippet below shows the general syntax for declaring a signal of both signed and unsigned type.

-- Declaring an unsigned type signal
signal <signal_name> : unsigned(<range>);
-- Declaring a signed type signal
signal <signal_name> : signed(<range>);

The signed and unsigned types are similar to the std_logic_vector type. However, we can also perform maths operations on them. In addition to this, we can also assign numerical data to them.

The code snippet below shows the two ways we can assign a value of four. This shows how it is possible to assign data either as a binary value or a base 10 integer type value.

-- Assigning an unsigned type with a binary value
example <= x"4";
-- Assigning an unsigned data type with a numeric value
example <= 4;

The ieee.numeric_std VHDL library defines a number of mathematical operators which we can use with the signed and unsigned types. The table below shows the arithmetic operators we can use with these types.

OperatorDescription
+addition
subtraction
*multiplication
/division
modmodulus
remsigned modulus

The code snippet below shows how we use each of these arithmetic operators in practise.

-- Returns the value of a plus b
y <= a + b;
-- Returns the value of a minus b
y <= a - b;
-- Returns the value of a multiplied by b
y <= a * b;
-- Returns the value of a divided by b
y <= a / b;
-- Returns the modulus of a divided by b
y <= a mod b;

These arithmetic operators require some consideration when we use them with synthesizable code though.

The plus, minus and multiplication operators can all be synthesized by most modern tools. However, this can often result in sub-optimal logical performance. As a result, it can be necessary to design logic circuits which specifically perform these functions.

We should never use the modulus or divide operators for synthesizable code as most tools will be unable to handle them.

Integer Type

The integer data type is used to express a value which is a whole number in VHDL. This is similar to the integer type in other programming languages such as C.

The code snippet below shows the general syntax for declaring an integer type signal in VHDL. The <range> field is optional and we use this to limit the values the range of values integer can take in our VHDL design.

signal <signal_name> : integer <range>;

The integer type is similar to both the signed and unsigned types. We can use the integer type to express numbers and perform basic arithmetic operations.

The table below shows the mathematical operators we can use with the integer type.

OperatorDescription
+addition
subtraction
*multiplication
/division
modmodulus
remsigned modulus

We don’t directly deal with bits when we are working with the integer type in VHDL. This is one of the key defining features which separates it from the signed and unsigned types.

As a result of this, we can’t assign binary, hex or decimal type data to an integer. Instead, we always use a numeric value to assign data.

As with most programming lanaguges, the integer type in VHDL is 32-bits wide by default.

However, we can limit the range of the integer to save resources in our FPGA when writing VHDL code. For example, we may require a signal which counts from 0 to 150. Therefore, we can implement this as an 8 bit integer within our FPGA.

In order to limit the range of the integer, we specify the valid values the integer can take. We use the downto and to VHDL keywords which we have seen before to specify this range.

The code snippet below shows how we would declare an integer signal in VHDL which has a valid range from 0 to 150.

signal range_example : integer range 0 to 150;

We can also use 2 integer subtypes in VHDL – natural and positive. The positive subtype can take the any positive integer value. The natural type is the same as the positive type except that it can also be assigned to 0.

VHDL Type Conversions

When we write VHDL code, we often have to convert between data types. There are two general methods which are available to us for this.

The first method is to simply cast the signal to the correct type. We can use this method to convert between the signed, unsigned and std_logic_vector VHDL data types.

The code snippet below shows the general syntax which we use to cast signals or data.

-- Casting to a std_logic_vector type
slv_example <= std_logic_vector(<data>);
-- Casting to an unsigned type
us_example <= unsigned(<data>);
-- Casting to a signed type
s_example <= signed(<data>);

The second way we convert VHDL data types is through the use of a function. We normally use this method to convert between the signed or unsigned types and the integer type.

In order to use a suitable conversion function, we need to include either the numeric_std or std_logic_arith packages. Both of these packages are available in the IEEE library.

Although many engineers still use it, the std_logic_arith package is not officially supported by the IEEE standards and we should avoid using it. Therefore, we will only consider the functions which are included in the numeric_std package in this post.

The image below summarises the methods we use to convert between different data types in VHDL.

A chart showing the methods used to convert the different VHDL data types

Let’s look in more detail at the way we convert between the different data types in VHDL.

Convert unsigned to std_logic_vector

To convert an unsigned type to a std_logic_vector we can simply cast the signal. However, whenever we do a cast we need to make sure that the signals have the same number of bits. If we don’t do this then we will get an error.

The VHDL code below shows an example of casting the unsigned type to a std_logic_vector type.

signal slv_example : std_logic_vector(3 downto 0);
signal us_example  : unsigned(3 downto 0);
slv_example <= std_logic_vector(us_example);

Convert unsigned to signed

As with the unsigned to std_logic_vector conversion, we can simply cast an unsigned type to a signed type. Again, we need to make sure that the signals have the same number of bits.

The code snippet below shows an example of casting the unsigned type to a signed type.

signal s_example   : signed(3 downto 0);
signal us_example  : unsigned(3 downto 0);
s_example <= signed(us_example);

Convert unsigned to integer

When we want to convert the unsigned type to an integer we have to use the to_integer function. This is a part of the numeric_std package in the ieee library so we must include this library and package in our code.

The code snippet below shows how we would include this library and package in our design.

library ieee;
  use ieee.numeric_std.all;

After we have included the relevant package we can simply call the function to perform the required conversion. The VHDL code below shows an example where we use the to_integer function to convert an unsigned type to an integer.

signal int_example : integer;
signal us_example  : unsigned(3 downto 0);
int_example <= to_integer(us_example);

Convert signed to std_logic_vector

To convert a signed type to a std_logic_vector we can use a basic cast. We will need to make sure that the two signals have the same number of bits otherwise we will get an error.

The VHDL code below gives an example which shows how we convert the signed type to a std_logic_vector.

signal slv_example : std_logic_vector(3 downto 0);
signal s_example  : signed(3 downto 0);
slv_example <= std_logic_vector(s_example);

Convert signed to unsigned

As with the signed to std_logic_vector conversion, we can use a simple cast to convert a signed type to an unsigned type. Again, we need to make sure that the signals have the same number of bits.

The code snippet below shows an example of casting the signed type to an unsigned type.

signal s_example   : signed(3 downto 0);
signal us_example  : unsigned(3 downto 0);
us_example <= unsigned(s_example);

Convert signed to integer

When we want to convert the signed type to an integer we have to use the to_integer function. This is a part of the numeric_std package in the ieee library so we must include this library and package in our code.

The code snippet below shows how we would include this library and package in our design.

library ieee;
  use ieee.numeric_std.all;

After we have included the relevant package we can simply call the function to perform the required conversion. The VHDL code below shows an example where we use the to_integer function to convert a signed type to an integer.

signal int_example : integer;
signal s_example   : signed(3 downto 0);
int_example <= to_integer(s_example);

Convert std_logic_vector to unsigned

We can use a simple cast to convert a std_logic_vector type into an unsigned type. However, we must take care to ensure that the signals have the same number of bits otherwise we will get an error.

The VHDL code below shows an example of casting a std_logic_vector type to an unsigned type.

signal slv_example : std_logic_vector(3 downto 0);
signal us_example  : unsigned(3 downto 0);
us_example <= unsigned(slv_example);

Convert std_logic_vector to signed

We can also use a simple cast to convert a std_logic_vector type into a signed type. Again, we must take care to ensure that the signals have the same number of bits.

The code snippet below shows an example of casting a std_logic_vector type to a signed type.

signal slv_example : std_logic_vector(3 downto 0);
signal s_example   : signed(3 downto 0);
s_example <= signed(slv_example);

Convert std_logic_vector to integer

We can’t directly convert between the std_logic_vector and integer types in VHDL. The reason for this is that VHDL doesn’t know how to interpret the std_logic_vector type as a numerical value.

To overcome this problem, we must firstly cast the std_logic_vector to either a signed or unsigned type. We can then use the to_integer function from the numeric_std package to convert the signed or unsigned type to an integer.

The code snippet below shows how we would include the ieee library and numeric_std package in our design.

library ieee;
  use ieee.numeric_std.all;

The VHDL code below shows how we would convert a std_logic_vector to an integer. It is quite typical to see the cast and the function call in one line as shown in the example below.

signal int_example : integer;
signal slv_example : std_logic_vector(3 downto 0);
-- Convert to an integer after casting to unsigned
int_example <= to_integer(unsigned(slv_example));
-- Convert to an integer after casting to signed
int_example <= to_integer(signed(slv_example));

Convert integer to unsigned

To convert an integer type to an unsigned type, we use the to_unsigned function. This is a part of the numeric_std package in the ieee library so we must include this library and package in our code

The code snippet below shows how we would include this library and package in our design.

library ieee;
  use ieee.numeric_std.all;

The to_unsigned function take two arguments. The first is the integer value which we want to convert to an unsigned type.

The second argument is the number of bits in the resultant unsigned signal. We typically use the length attribute to calculate this automatically for us.

The code snippet below shows the general syntax for the to_unsigned function.

to_unsigned( <value>, <bits> );

The VHDL example below shows how we use the to_unsigned function to convert an integer to an unsigned type.

signal int_example : integer;
signal us_example  : unsigned(3 downto 0);
us_example <= to_unsigned(int_example, us_example'length);

Convert integer to signed

To convert and integer type to a signed type, we use the to_signed function. This is a part of the numeric_std package in the ieee library so we must include this library and package in our code

The code snippet below shows how we would include this library and package in our design.

library ieee;
  use ieee.numeric_std.all;

The to_signed function is similar to the to_unsigned function which we previously discussed. Again, this function takes two arguments.

The first argument is the integer value which we want to convert to a signed type.

The second argument is the number of bits in the resultant signed value. We typically use the length attribute to calculate this automatically for us.

The code snippet below shows the general syntax for the to_signed function.

to_signed( <value>, <bits> );

The VHDL example below shows how we use the to_signed function to convert an integer to a signed type.

signal int_example : integer;
signal s_example   : signed(3 downto 0);
s_example <= to_signed(int_example, s_example'length);

Convert integer to std_logic_vector

We can’t directly convert between the std_logic_vector and integer types in VHDL. The reason for this is that VHDL doesn’t know how to interpret the std_logic_vector type as a numerical value

To overcome this problem, we must firstly convert the integer to either a signed or unsigned type. We do this using the to_signed and to_unsigned functions which we have previously talked about.

As these functions are a part of the numeric_std package, we must include this in our design. The code snippet below shows how we would include the relevant library and package in our design.

library ieee;
  use ieee.numeric_std.all;

Once we have converted the integer to a signed or unsigned type, we can then cast the resultant signal into a std_logic_vector.

The VHDL code below gives an example which shows how we convert an integer to a std_logic_vector. It is quite typical to see the cast and the function call in one line as shown in the example below.

signal slv_example : std_logic_vector(3 downto 0);
signal int_example : integer;
-- Using the to_unsigned function to convert an integer to a std_logic_vector
slv_example <= std_logic_vector(to_unsigned(int_example, slv_example'length));
-- Using the to_signed function to convert an integer to a std_logic_vector
slv_example <= std_logic_vector(to_signed(int_example, slv_example'length));

Exercises

What is the main difference between the bit and and std_logic types?

The std_logic type can take on more values which allows it to model high impedance states.

What is the difference between the std_logic_vector type and the signed/unsigned types?

We can assign numeric values to the signed and unsigned types. We can also perform arithmetic operations on these types.

Which library and package must we include in our VHDL design if we want to use the signed and unsigned types

The numeric_std package fromt he ieee library

What is the difference between the integer types and the signed/unsigned types?

We can only assign numeric values to integer types whereas we can assign both numeric and binary data to signed and unsigned types.

What is the difference between the integer type and the positive type?

The positive type can only be assigned positive, whole numbers. The integer can also accept negative numbers.

Which function do we use to convert either the unsigned or signed types into an integer. Which library and package do we need to include to use this function.

We use the to_integer function to convert the signed and unsigned types to an integer. This function can be found in the numeric_std package which is a part of the ieee library.

Write some code which converts an 8 bit signed signal to a std_logic_vector, then converts the resultant std_logic_vector to an unsigned type.

signal s_example   : signed(7 downto 0);
signal slv_example : std_logic_vector(7 downto 0);
signal us_example  : unsigned(7 downto 0);
-- Cast the signed type to a std_logic_vector
slv_example <= std_logic_vector(s_example);
-- Cast the std_logic_vector to an unsigned type
us_example <= unsigned(slv_example);

Write some VHDL code which converts an integer type to an 8 bit std_logic_vector. The integer should be treated as an unsigned number.

signal slv_example : std_logic_vector(7 downto 0);
signal int_example : integer;
slv_example <= std_logic_vector(to_unsigned(int_example, slv_example'length));