Skip to content

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.

the standard arithmetic preamble
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
library ieee;
use ieee.std_logic_1164.all;   -- std_logic, std_logic_vector
use ieee.numeric_std.all;      -- unsigned, signed, +, -, *, resize, conversions

3. 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.

the byte 1111_1111 interpreted as unsigned 255, signed -1, or a value-less std_logic_vectorread as magnituderead as 2's-compread as bits only1111_1111eight std_logic wiresunsignedmagnitude = 255signedtwo's complement = −1std_logic_vectorno numeric value12
One identical byte, 1111_1111, read three ways. As numeric_std unsigned it is the magnitude 255. As numeric_std signed it is two's complement −1 (the top bit is the sign). As a plain std_logic_vector it has NO numeric value — it is a bag of bits and arithmetic on it is undefined. The wires are physically the same; only the TYPE decides the meaning, and therefore which hardware (unsigned vs signed adder/comparator) synthesis builds.

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:

numeric_std_arithmetic.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 an integer.

And resize changes width safely — zero-extending or truncating an unsigned, sign-extending a signed:

resize.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 bits

5. 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 cycles
unsigned(7 downto 0) counter — 254, 255, then wraps to 0 with no errorcnt reaches 255 (max for 8 bits)cnt reaches 255 (max f…255 + 1 wraps to 0 — by design, no error255 + 1 wraps to 0 — b…clkcnt2532532542542552550011t0t1t2t3t4t5t6t7t8t9
An unsigned counter wraps modulo 2^width automatically: 255 → 0. Compare lesson 2.6, where a constrained integer exceeding its range STOPS the simulation. unsigned/signed give defined-width, wrap-around arithmetic; integer gives abstract, range-checked counting. Same counter idea, two different type contracts.

6. 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:

the width fault and its fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 first

The 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:

do not mix signedness silently
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 add

7. Common mistakes & what to watch for

  • Reaching for std_logic_unsigned. It is obsolete and non-standard. Use numeric_std and keep arithmetic on unsigned/signed.
  • Width mismatches. numeric_std never auto-extends; resize operands to a common width.
  • Mixing signed and unsigned. They are different interpretations; convert deliberately.
  • to_unsigned without a width. It needs the target width: to_unsigned(value, N).
  • Forgetting wrap. unsigned/signed wrap 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 to unsigned/signed first (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.