Skip to content

VHDL · Chapter 2.6 · Data Types

Integer Types and Ranges

Not every value in a design is a bus. Indices, counts, loop bounds, and generics are numbers, and VHDL's integer is the abstract whole-number type for them, with no bit positions and no width, just a value with a range. This lesson shows why an unconstrained integer is 32 bits wide, how adding a range constraint shrinks it to exactly the hardware you want, and how it differs from the unsigned type of the numeric library, which is bits that stand for a number. It also covers the gotcha that catches everyone: exceeding an integer's range is a runtime error, not the silent wrap-around an unsigned value would give you, so a counter needs its range chosen with care.

Foundation14 min readVHDLintegerRangesSubtypesnumeric_stdCounters

1. Intuition — an abstract number, not a bus

std_logic_vector is wires; integer is a number. It has no bit positions you can index, no 'Z' or 'X', no width you declare — just a whole-number value. You use it wherever the design needs to count or index rather than route bits: loop variables, array indices, generics, and the value inside a counter.

Because it is abstract, the tool is free to implement it in as many or as few bits as the value needs — if you tell it the range. That single idea, range determines width, is what this lesson is about.

2. Range determines width

An unconstrained integer spans the full 32-bit signed range (about ±2.1 billion). Declared as a signal it implies 32 bits of hardware — almost always far more than you need. Adding a range constraint tells synthesis the real bounds, and it builds only the bits required:

integer range constraints mapping to synthesized bit widthsinteger range 0 to 1516 values4 bitssynthesised widthinteger range 0 to255256 values8 bitssynthesised widthinteger(unconstrained)full 32-bit range32 bitswasteful — avoid12
An integer's range constraint sets its synthesized width. 'integer range 0 to 15' needs 4 bits; 'integer range 0 to 255' needs 8 bits; an unconstrained 'integer' defaults to the full 32-bit signed range and synthesizes to 32 bits — almost always wasteful or rejected. Always constrain an integer's range so it becomes exactly the hardware you intend.
constrained_integers.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
signal idx   : integer range 0 to 15;     -- 4 bits — a small index
signal level : integer range 0 to 255;    -- 8 bits — a byte-sized count
-- signal big : integer;                   -- 32 bits — avoid in real signals
 
-- handy predefined subtypes:
signal n : natural;    -- integer range 0 to integer'high   (non-negative)
signal p : positive;   -- integer range 1 to integer'high   (strictly positive)

natural and positive are just constrained integer subtypes the standard predefines — use them to document intent (a count cannot be negative; a divisor cannot be zero).

3. Where integers belong — indices, loops, generics

integer is the natural type for anything that counts or selects, where you never need to touch individual bits:

integers_in_use.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- loop variable: an integer, ranged by the loop bounds
gen : for i in 0 to 7 generate
  y(i) <= a(i) xor b(i);
end generate;
 
-- array index: integers select elements
data_out <= mem(addr);            -- addr is an integer index into mem
 
-- generic: a compile-time integer parameter
entity fifo is
  generic ( DEPTH : positive := 16 );   -- positive integer, ranged and defaulted
  port ( ... );
end entity;

In each case the value is a number used to count or pick, not a bus to be wired — which is exactly what integer is for.

4. The range bound over time — and the wrap gotcha

A constrained integer counter behaves like any counter, but its range bound is a hard limit you must respect. The waveform shows a range 0 to 5 counter that wraps because the code explicitly resets it — without that reset, hitting 6 is a runtime error, not a wrap:

integer range 0 to 5 counter — counts, then wraps only because the code resets it

14 cycles
integer range 0 to 5 counter — counts, then wraps only because the code resets itcounts 0,1,2,3,4,5 within rangecounts 0,1,2,3,4,5 wit…at 5: code forces wrap to 0 (no 6 ever occurs)at 5: code forces wrap…clkcount00123455001122t0t1t2t3t4t5t6t7t8t9t10t11t12t13
The counter stays inside 0 to 5 because the RTL explicitly sets it back to 0 at the top. If the code instead let it reach 6, the simulator stops with a range error — a constrained integer does NOT silently wrap the way an unsigned does.
ranged_counter.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
signal count : integer range 0 to 5;
process (clk)
begin
  if rising_edge(clk) then
    if count = 5 then
      count <= 0;          -- explicit wrap — REQUIRED to stay in range
    else
      count <= count + 1;
    end if;
  end if;
end process;

5. Debugging example — "value 6 is out of range 0 to 5"

This is the integer gotcha, and the error message is exact:

the overflow that is an error, not a wrap
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
signal count : integer range 0 to 5;
-- count <= count + 1;   -- if count is already 5, this assigns 6
-- → simulation stops: "value 6 is out of the range 0 to 5 of signal count"

Read it literally: you drove a constrained integer past its declared bound, and VHDL treats that as an error — because a value outside the range is meaningless for that type, not a modulo wrap. The fix is to guard the increment (the if count = 5 above) or, if you actually want wrap-around arithmetic, use unsigned from numeric_std, which wraps by design:

if you want wrap-around, use unsigned
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
 
signal c : unsigned(2 downto 0);   -- 0..7, wraps 7 → 0 automatically
-- c <= c + 1;                      -- legal: unsigned arithmetic wraps, no error

6. Common mistakes & what to watch for

  • Leaving an integer unconstrained. A bare integer signal is 32 bits — wasteful, and often rejected by synthesis. Always give a range.
  • Expecting an integer to wrap. It does not — exceeding the range is a runtime error. Guard it, or use unsigned/signed for wrap-around.
  • Using integer where you need bit access. You cannot index an integer's bits; if you must slice or manipulate bits, it should be a vector (unsigned/signed/ std_logic_vector).
  • Forgetting natural / positive. They are free, documenting constraints (no negatives; no zero) the compiler then enforces.

7. Engineering insight

integer and unsigned are not redundant — they sit on opposite sides of the abstract/concrete line. integer is a pure number with a range: ideal for indices, loop bounds, and generics, where the value matters and the bits do not. unsigned/signed are bits that mean a number: ideal where width, bit access, or wrap-around matter. Constrain your integers so they synthesize small and so out-of-range bugs surface immediately, and reach for numeric_std the moment you need defined-width, wrapping arithmetic.

8. Summary & next step

VHDL's integer is an abstract whole-number type for counts, indices, loop variables, and generics. Unconstrained it is 32 bits; a range constraint shrinks it to the minimal synthesized width and is essentially mandatory in real RTL. It is not a bus — no bit access — and it does not wrap: exceeding the range is a runtime error, so use unsigned/ signed when you need wrap-around. natural and positive are predefined constrained subtypes worth using for intent.

You now have both worlds of scalar value: logic-valued (std_logic) and number-valued (integer). The chapter continues by tying them together — numeric_std's unsigned and signed, the types that let buses do arithmetic — and the conversions that move cleanly between all of them.