VHDL · Chapter 2.9 · Data Types
Type Conversions and Casting
VHDL will not quietly turn an integer into a logic vector for you, because strong typing means every change of type is explicit, written out in the code. That sounds tedious until you see the structure: there are only two mechanisms and one small map to learn. Casts move between closely related array types, since unsigned, signed, and the standard logic vector share the same bits, so the cast is a free relabel of wires. Conversion functions such as to-integer, to-unsigned, and to-signed move between genuinely different representations. This lesson draws the canonical numeric-std conversion map, shows every direction in working code, and fixes the error every VHDL beginner hits: assigning an integer straight to a logic vector.
Foundation14 min readVHDLType ConversionCastingnumeric_stdresizeTypes
1. Intuition — strong typing means convert on purpose
VHDL has no implicit conversions. An integer, an unsigned, a signed, and a
std_logic_vector are four different types, and the compiler will not move a value between
them on your behalf — you must say so. This is lesson 2.1's strong typing again: the friction
is the feature, because every reinterpretation of bits is visible and intentional.
The good news is that the whole subject reduces to two mechanisms and a single small map.
2. Two mechanisms — casts vs conversion functions
-- (1) TYPE CAST — between closely related array types that share a representation.
-- unsigned, signed, and std_logic_vector are all "array of std_logic" of the
-- same length, so the cast just RELABELS the wires. It costs no hardware.
slv <= std_logic_vector(u); -- unsigned → std_logic_vector
u <= unsigned(slv); -- std_logic_vector → unsigned
s <= signed(slv); -- std_logic_vector → signed
-- (2) CONVERSION FUNCTION — between genuinely different representations
-- (the abstract integer has no width; numeric types do), so a function is needed.
u <= to_unsigned(int, 8); -- integer → unsigned(8)
s <= to_signed(int, 8); -- integer → signed(8)
int <= to_integer(u); -- unsigned → integer
int <= to_integer(s); -- signed → integerThe rule of thumb: same shape, different label → cast; different shape → function.
unsigned/signed/std_logic_vector are the same shape (sized arrays of bits), so they cast
into each other. integer is a different shape (an abstract number, no width), so reaching it
always needs to_integer, and leaving it always needs to_unsigned/to_signed with a width.
3. The conversion map
Memorise this picture and the everyday confusion disappears. Four types, and the labelled edge tells you the exact cast or function to write:
The casts between std_logic_vector, unsigned, and signed are bidirectional because they
are the same wires. The edges to integer are functions because a number has no width going
out (to_integer) and needs a width coming in (to_unsigned(_, N) / to_signed(_, N)).
4. Worked conversions — every direction
A single block that exercises the whole map, in idiomatic numeric_std:
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- ... inside an architecture:
signal slv : std_logic_vector(7 downto 0);
signal u : unsigned(7 downto 0);
signal s : signed(7 downto 0);
signal n : integer range 0 to 255;
-- casts (same bits, free):
u <= unsigned(slv); -- slv → unsigned
s <= signed(slv); -- slv → signed
slv <= std_logic_vector(u); -- unsigned → slv
slv <= std_logic_vector(s); -- signed → slv
-- functions (to/from the abstract integer):
n <= to_integer(u); -- unsigned → integer
u <= to_unsigned(n, 8); -- integer → unsigned(8)
s <= to_signed(-1, 8); -- integer → signed(8) = 1111_1111
-- the common compound move: integer straight onto a bus, via unsigned:
slv <= std_logic_vector(to_unsigned(n, 8)); -- integer → unsigned(8) → slvThat last line is the one you will write constantly: a number you computed needs to drive an
8-bit bus, so you go integer → to_unsigned(_, 8) → std_logic_vector(_). Two steps, because
the map has no direct integer → std_logic_vector edge.
5. Width changes belong to resize, not casting
A cast never changes width — unsigned(slv) keeps all 8 bits. To change width, use
resize (lesson 2.8), which knows how to extend or truncate each numeric type correctly:
signal small : unsigned(3 downto 0);
signal bus8 : std_logic_vector(7 downto 0);
bus8 <= std_logic_vector(resize(small, 8)); -- widen (zero-extend) THEN relabel to slvOrder matters: do the numeric resize while the value is still unsigned/signed (so the
extension respects signedness), then cast to std_logic_vector for the bus.
6. Debugging example — the #1 beginner error
Almost everyone's first numeric_std error is assigning a number directly to a bus:
signal slv : std_logic_vector(7 downto 0);
signal n : integer range 0 to 255;
-- slv <= n;
-- → error: "cannot assign integer to std_logic_vector" / type mismatch.
-- There is NO implicit conversion and NO direct integer→slv edge on the map.The fix walks the map explicitly — integer into unsigned (with a width), then cast to the
vector:
slv <= std_logic_vector(to_unsigned(n, 8)); -- integer → unsigned(8) → std_logic_vectorThe mirror-image error is reading a bus as a number directly (n <= slv;). Same cause, same
shape of fix: n <= to_integer(unsigned(slv)); — cast the bus to unsigned, then
to_integer.
7. Common mistakes & what to watch for
- Assigning
integertostd_logic_vector(or back) directly. No such edge — route throughunsigned/signed. to_unsigned/to_signedwithout a width. They require the target width:to_unsigned(n, 8).- Expecting a cast to change width. Casts relabel only; use
resizefor width, before the final cast so signedness is honoured. - Casting
signed↔unsignedwithout thinking.unsigned(s)/signed(u)reinterpret the same bits — legal, but the numeric meaning flips (−1 becomes 255). Do it only when you mean to. - Falling back to
std_logic_unsignedto "avoid" conversions. That obsolete package (lesson 2.8) hides the conversion but breaks portability. Keep the explicitnumeric_stdconversions.
8. Engineering insight
Strong typing turns "convert types" from guesswork into a tiny, learnable graph. Once the map
in Section 3 is in your head, every conversion is mechanical: identify the source and
destination types, read the edge, write the cast or function. The deeper lesson is where to
convert — keep module ports and storage as std_logic_vector (it makes no false claim about
meaning), lift to unsigned/signed only inside the datapath where you compute, and touch
integer mainly for indices, ranges, and generics. Conversions then cluster at those
boundaries, exactly where a reader expects to see the reinterpretation spelled out — which is
the whole reason VHDL makes them explicit.
9. Summary & next step
VHDL has no implicit conversions: moving between integer, unsigned, signed, and
std_logic_vector is always explicit. Casts relabel the same bits between the array types
(std_logic_vector/unsigned/signed); functions cross to and from the abstract integer
(to_integer out, to_unsigned/to_signed with a width in). There is no direct
integer ↔ std_logic_vector edge — route through unsigned/signed — and resize (not a
cast) changes width. The compound move std_logic_vector(to_unsigned(n, 8)) becomes second
nature.
That completes the scalar and numeric side of VHDL's type system — logic values, integers,
enumerations, the numeric_std types, and the conversions between them. The chapter turns next
to composite types, where many values are grouped into one: array types (indexed
collections like memories and register files) and then records (named bundles of mixed
fields) — the structures that scale a design from single signals to organised data.