Skip to content

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

The base type still governs behaviourbyte 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 vs distinct type.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 ranges

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

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

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

ranges size the hardware.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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;
a range constraint on a base type producing a sized signal in synthesis and a bounds check in simulationconstrainsizes widthenforcesboundinteger (base)32 bits, unboundedrange 0 to 1023the constraintsynthesis10-bit signal — sized tothe rangesimulationout-of-range value →run-time error12
A subtype is a base type plus a constraint, and the constraint does two concrete jobs. In synthesis, a scalar range fixes the bit width: integer range 0 to 1023 becomes a 10-bit signal, not the 32 bits a bare integer would infer — smaller registers, smaller adders, easier timing. In simulation, the same range is an enforced bound: any value driven outside 0..1023 is a run-time error. The base type still defines behaviour; the constraint sizes the hardware and guards the value.

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:

sized arithmetic subtypes.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

the range violation
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

natural catches a negative the moment it happens
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 subtype with type. A subtype stays compatible with its base (no cast); a type is a distinct, incompatible type. Reach for subtype for widths and ranges; type only when you want incompatibility.
  • Leaving integers unconstrained. A bare integer infers 32 bits. Give counters and indices a range (or use natural/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 0 is an empty range (a to/downto slip) — scalar integer ranges almost always ascend (0 to 7); downto is 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.