VHDL · Chapter 2.8 · Data Types
numeric_std: signed and unsigned
A standard logic vector is a bag of bits. It can carry data but cannot do arithmetic, because nothing says what its bits mean. The numeric standard package fixes that with two types that are the same bits plus a numeric interpretation. The unsigned type reads the bits as a plain magnitude, while signed reads them as two's complement, and now addition, subtraction, multiplication, and comparison all work. This lesson covers why this is the only package you should use for arithmetic, how the identical byte can mean 255 or minus one depending on its type, how the resize and to-integer helpers move between widths and integers, and why an unsigned counter wraps by design where a constrained integer would stop with an error.
Foundation15 min readVHDLnumeric_stdunsignedsignedArithmeticresize
1. Intuition — bits that mean a number
Lesson 2.4 ended on a rule: a std_logic_vector is a bag of bits with no numeric meaning,
so slv_a + slv_b is not defined. Arithmetic needs a type that says how to read the bits.
That is what numeric_std provides:
unsigned— the bits are a plain magnitude (0 … 2ⁿ−1).signed— the bits are two's complement (−2ⁿ⁻¹ … 2ⁿ⁻¹−1).
Both are arrays of std_logic exactly like std_logic_vector — the wires are identical. The
type is the only thing that changes, and that type is what lets synthesis build the right
adder, subtractor, or comparator.
2. The one true package
There is exactly one standard, IEEE-blessed arithmetic package: numeric_std. Use it and
nothing else.
library ieee;
use ieee.std_logic_1164.all; -- std_logic, std_logic_vector
use ieee.numeric_std.all; -- unsigned, signed, +, -, *, resize, conversions3. Same bits, different meaning
The whole idea in one picture: take the byte 1111_1111. As unsigned it is 255; as signed
(two's complement) it is −1; as a std_logic_vector it is no number at all — just eight bits.
This is why choosing the type is a hardware decision: an unsigned comparator and a signed
comparator are different circuits, even though they compare the same wires.
4. Arithmetic, width, and the integer bridge
With numeric_std, operators and helpers do the real work:
signal a, b, sum : unsigned(7 downto 0);
signal cnt : unsigned(3 downto 0);
a <= to_unsigned(200, 8); -- integer → unsigned(8): the value 200
b <= to_unsigned( 60, 8); -- the value 60
sum <= a + b; -- unsigned addition → 260 mod 256 = 4 (wraps in 8 bits)
cnt <= cnt + 1; -- counts; "+ 1" is unsigned arithmetic, not a vector op
-- moving back to a plain number (e.g. for an array index or a generic):
-- to_integer(unsigned'(...)) gives a VHDL integer (lesson 2.6)Two helpers form the bridge to the abstract integer of lesson 2.6:
to_unsigned(int, width)/to_signed(int, width)— number into a sized vector type.to_integer(u)/to_integer(s)— sized vector type back to aninteger.
And resize changes width safely — zero-extending or truncating an unsigned, sign-extending
a signed:
signal small : unsigned(3 downto 0);
signal big : unsigned(7 downto 0);
big <= resize(small, 8); -- 4 → 8 bits, zero-extended (unsigned)
small <= resize(big, 4); -- 8 → 4 bits, truncates the high bits5. Counting with wrap — the contrast with a constrained integer
unsigned arithmetic wraps by design: an 8-bit unsigned at 255 plus 1 rolls to 0, no
error, every time. That is the deliberate opposite of lesson 2.6, where a constrained
integer range 0 to 5 past its bound is a runtime error. Choose the type for the behaviour
you want: defined-width wrapping (unsigned) or range-checked counting (integer).
unsigned(7 downto 0) counter — 254, 255, then wraps to 0 with no error
10 cycles6. Debugging example — "operands have different lengths"
The most common numeric_std error is a width mismatch. Assignment and most operators
require both sides to be the same width — there is no silent extension:
signal a8 : unsigned(7 downto 0);
signal b4 : unsigned(3 downto 0);
-- a8 <= a8 + b4;
-- → error: "arguments of '+' have different lengths (8 vs 4)" / length mismatch
-- numeric_std will NOT auto-extend b4; you must say how.
a8 <= a8 + resize(b4, 8); -- explicit: zero-extend b4 to 8 bits firstThe matching trap is mixing signed and unsigned, which do not interoperate directly
because their meanings differ. Convert intentionally (Section 4 and lesson 2.9) rather than
hoping an overload exists:
signal u : unsigned(7 downto 0);
signal s : signed(7 downto 0);
-- s <= s + u; -- no such '+' (signed + unsigned) — type error
s <= s + signed(resize(u, 8)); -- decide the meaning explicitly, then add7. Common mistakes & what to watch for
- Reaching for
std_logic_unsigned. It is obsolete and non-standard. Usenumeric_stdand keep arithmetic onunsigned/signed. - Width mismatches.
numeric_stdnever auto-extends;resizeoperands to a common width. - Mixing
signedandunsigned. They are different interpretations; convert deliberately. to_unsignedwithout a width. It needs the target width:to_unsigned(value, N).- Forgetting wrap.
unsigned/signedwrap silently at the width boundary — if you need a carry/overflow flag, compute it (e.g. resize to N+1 and inspect the top bit). - Doing arithmetic on a
std_logic_vector. Cast it tounsigned/signedfirst (lesson 2.9); the vector itself has no+.
8. Engineering insight
numeric_std draws a clean line between wires and numbers. std_logic_vector is the
wire layer — routing, slicing, concatenation — with no arithmetic. unsigned and signed are
the same wires with a numeric contract layered on top, and that contract is exactly what
synthesis needs to pick a magnitude adder versus a two's-complement adder. Keep data as
std_logic_vector at your module boundaries (it says nothing misleading about meaning) and
cast to unsigned/signed only where you actually compute. That discipline — vector at the
edges, numeric in the datapath — keeps interfaces honest and arithmetic explicit, and it is
why numeric_std (not the legacy packages) is the universal choice.
9. Summary & next step
numeric_std adds unsigned (magnitude) and signed (two's complement) — std_logic_vector
plus a numeric meaning — enabling +, -, *, and comparison. It is the only standard
arithmetic package; std_logic_arith/std_logic_unsigned are obsolete. The same bits read as
255, −1, or nothing depending on type; resize, to_unsigned/to_signed, and to_integer
move between widths and integers; and unsigned/signed wrap by design where a constrained
integer errors. Watch width mismatches and never mix signedness silently.
You now have three representations of a value — abstract integer, magnitude unsigned, and
two's-complement signed — plus the raw std_logic_vector bus. The next lesson ties them all
together: the type conversions and casts that move cleanly between integers, vectors, and
the numeric_std types — the single most error-prone corner of everyday VHDL, made routine.