VHDL · Chapter 2.12 · Data Types
Subtypes and Range Constraints
Strong typing stops you mixing kinds of values, and a subtype lets you go further and pin down which values of a kind are legal: an integer that is only 0 to 1023, a vector that is exactly 8 bits, or a count that can never be negative. A subtype is a base type plus a constraint, and crucially it is not a new type, so it stays compatible with its base and needs no conversion. This lesson shows how natural and positive are just predefined integer subtypes, why a constrained logic vector is simply the array narrowed to a range, how a range constraint hands synthesis the exact bit width while bounds checks run in simulation, and how the right subtype turns a whole class of bugs into a compile-time or sim-time error.
Foundation15 min readVHDLSubtypesConstraintsRangesStrong TypingTypes
1. Intuition — a base type, narrowed
A type answers "what kind of value is this?" A subtype answers a finer question: "which values of that kind are allowed?" It is a base type with a constraint bolted on — a range for a scalar, a width for an array — and nothing else:
subtype byte is std_logic_vector(7 downto 0); -- the array, constrained to 8 bits
subtype small is integer range 0 to 15; -- integer, constrained to 0..15
subtype level is integer range -8 to 7; -- a signed 4-bit range
signal b : byte; -- exactly 8 bits, every bit a std_logic
signal n : small; -- an integer the tool knows fits in 4 bitsThe base type still governs behaviour — byte is a std_logic_vector, so it does everything a
vector does — but the constraint restricts the legal values. The subtype is the contract narrowed.
2. A subtype is not a new type
The defining property, and the reason to reach for subtype rather than type: a subtype stays
assignment-compatible with its base. A value of the subtype is a value of the base type, so it
moves between them with no conversion:
subtype small is integer range 0 to 15; -- a SUBTYPE of integer
signal i : integer;
signal s : small;
i <= s; -- OK: small is just integer, narrowed — no cast
s <= i; -- OK to compile; sim CHECKS that i is in 0..15 at run time
-- Contrast a DISTINCT type, which is incompatible even if it looks identical:
type apples is range 0 to 100;
type oranges is range 0 to 100;
-- signal a : apples; signal o : oranges;
-- a <= o; -- ERROR: apples and oranges are different types, despite equal rangesSo the choice is deliberate: use subtype when you want a narrower flavour of an existing type
that still interoperates (the common case — widths, ranges, natural); use type only when you
want a genuinely separate type the compiler must keep apart (rare, e.g. preventing apples-to-oranges
unit mixups).
3. You already use subtypes
Two integer subtypes are predefined and you have used both — natural and positive are nothing more
than integer with a range constraint:
subtype natural is integer range 0 to integer'high; -- 0 and up
subtype positive is integer range 1 to integer'high; -- 1 and up
-- which is why a generic count is a natural, not a bare integer:
generic ( DEPTH : positive := 16 ); -- DEPTH < 1 is now a compile/elab error
constant AW : natural := 4; -- an address width can't be negativeLikewise every sized bus you have written — std_logic_vector(31 downto 0) — is the unconstrained
array std_logic_vector (lesson 2.10) with an index constraint supplying the range. "A type with
a range in parentheses" is, under the hood, "a base type with a constraint" — the subtype idea you are
meeting by name here.
4. Hardware — a range sizes the silicon; checks live in simulation
A scalar range constraint does real work in synthesis: it tells the tool the exact number of bits.
An unconstrained integer is 32 bits; constrain it and the tool builds only what the range needs:
signal big : integer; -- 32 bits inferred — usually wasteful
signal addr : integer range 0 to 1023; -- 0..1023 → 10 bits, not 32
subtype idx is integer range 0 to 7; -- 0..7 → 3 bits
signal sel : idx;The crucial distinction: the width inference is real hardware, but the range check is a
simulation/elaboration behaviour, not a gate. Synthesised silicon does not contain a comparator that
faults when addr hits 1100; the range exists so the tools catch the violation before silicon. Use
it as a design-time guard rail, never as a substitute for the logic that actually keeps a value in
range. (Because a subtype is a static, elaboration-time constraint — width and bounds, not timing —
there is no clocked behaviour to draw here, so this lesson, like records, uses a diagram and no
waveform.)
5. Subtypes with numeric_std
Subtypes constrain numeric_std's unsigned and signed exactly as they do std_logic_vector —
which is how you give an arithmetic bus a meaningful, sized name:
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
subtype addr_t is unsigned(9 downto 0); -- a 10-bit address, as a number
subtype byte_u is unsigned(7 downto 0); -- an 8-bit unsigned
signal a : addr_t;
signal next_a : addr_t;
next_a <= a + 1; -- numeric_std arithmetic on the sized subtype (lesson 2.8)Naming the subtype (addr_t) documents intent at every use and keeps the width defined in one place —
change addr_t and every signal of that subtype follows.
6. Debugging example — the out-of-range assignment
The most useful thing a range constraint does is fail loudly in simulation the instant a value leaves its bounds — turning a silent overflow into a pinpointed error:
signal sel : integer range 0 to 7; -- legal: 0..7
signal idx : integer; -- unbounded
-- sel <= idx; -- if idx = 9 at run time:
-- Simulation aborts: "value 9 out of range 0 to 7 for subtype of integer".That message points straight at the bug — far better than a std_logic_vector quietly wrapping 9
to "001" and corrupting a downstream index with no warning. The natural/positive subtypes give
you the same guard for the most common case — a value that must never go negative:
signal count : natural; -- 0 .. integer'high
-- count <= count - 1; -- when count is already 0, this underflows to -1
-- Simulation aborts: "value -1 out of range 0 to 2147483647".
-- A bare 'integer' would have silently gone to -1 and broken an address later.The fix is not to widen the subtype to hide the error — it is to add the logic that keeps the value in
range (if count /= 0 then count <= count - 1; end if;). The subtype's job was to find the missing
guard; your logic's job is to be the guard.
7. Common mistakes & what to watch for
- Confusing
subtypewithtype. Asubtypestays compatible with its base (no cast); atypeis a distinct, incompatible type. Reach forsubtypefor widths and ranges;typeonly when you want incompatibility. - Leaving integers unconstrained. A bare
integerinfers 32 bits. Give counters and indices a range (or usenatural/positive) so the tool sizes them — and so out-of-range bugs surface. - Treating the range check as hardware. The bounds check runs in simulation/elaboration, not in the synthesised gates. Keep the logic that actually constrains the value.
- Widening a subtype to silence a violation. An out-of-range error is the tool finding a real bug. Fix the logic, don't loosen the bound.
- Range direction mismatches.
integer range 7 downto 0is an empty range (ato/downtoslip) — scalar integer ranges almost always ascend (0 to 7);downtois for vector indices. - Forgetting subtypes are static. A subtype constraint must be locally static for synthesis; deriving a width from a non-constant at elaboration can be rejected. Size from generics/constants.
8. Engineering insight
A subtype is where strong typing meets right-sized hardware. Strong typing keeps kinds apart; a
subtype goes one level finer and encodes how much and which range directly into the type, so two
things happen for free. Synthesis sizes the silicon to the declared range — a 10-bit address instead
of a 32-bit one, smaller registers and adders and easier timing — and simulation enforces the range,
converting a class of silent overflow and out-of-bounds bugs into a precise, located error. And it
does this while staying compatible with the base type, so it costs you no conversions. The discipline
is small and high-leverage: name your widths and ranges as subtypes (addr_t, byte, natural),
and the type system both documents your intent and defends it. That is the throughline of the whole
Data Types chapter — describe the values precisely, and the compiler, the simulator, and the
synthesiser all work for you.
9. Summary & next step
A subtype is a base type plus a constraint — a range for a scalar, an index/width for an array — and,
critically, not a new type, so it stays assignment-compatible with its base and needs no conversion
(unlike a distinct type). natural and positive are predefined integer subtypes; every sized bus
is a constrained array. A scalar range hands synthesis the exact bit width and gives simulation an
enforced bound, so unconstrained integers waste bits and hide overflows while a tight subtype sizes
the hardware and surfaces the bug. Name your widths and ranges as subtypes to document and defend
intent in one place.
That completes the core of the type system — names (enums), repetition (arrays), composition
(records), and now precise constraints (subtypes). The chapter closes with physical types and
time — the dimensioned types that measure delay and drive testbench timing — before the curriculum
turns to how signals actually behave as hardware over the clock.