Skip to content

Types and numeric_std

VHDL is strongly typed. This initially feels strict, but it prevents silent width, sign, and interpretation bugs that are especially dangerous in hardware.

The IEEE logic types

Import:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

std_logic has nine values:

Value Meaning
U Uninitialized
X Forcing unknown or driver conflict
0, 1 Forcing logic levels
Z High impedance
W Weak unknown
L, H Weak low/high
- Do not care

In normal synchronous RTL, internal signals should settle to 0 or 1. An U or X is useful evidence of an initialization, reset, incomplete assignment, or multiple-driver error.

Warning

Do not hide unknowns by converting them carelessly to integers. Fix their cause.

Vector, unsigned, or signed?

Type Interpretation Preferred use
std_logic_vector Bits with no numeric meaning Buses, encoded fields, ports
unsigned Non-negative binary integer Counters, addresses, sizes
signed Two's-complement integer Signed data and DSP

Use the type that expresses intent. Arithmetic on std_logic_vector requires an explicit conversion because a bit pattern alone does not say whether it is signed.

signal raw_i    : std_logic_vector(7 downto 0);
signal count_u  : unsigned(7 downto 0);
signal sample_s : signed(7 downto 0);

count_u  <= unsigned(raw_i);
sample_s <= signed(raw_i);

These are reinterpretations, not size changes.

Conversion map

-- Integer to vector
u8 <= to_unsigned(integer_value, u8'length);
s8 <= to_signed(integer_value, s8'length);

-- Vector to integer
integer_value := to_integer(u8);
integer_value := to_integer(s8);

-- Numeric vector to plain logic vector
slv <= std_logic_vector(u8);
slv <= std_logic_vector(s8);

-- Plain logic vector to numeric vector
u8 <= unsigned(slv);
s8 <= signed(slv);

Never rely on an implicit sign

signed(x"FF") means −1 in 8-bit two's complement; unsigned(x"FF") means 255. The bits are identical, the arithmetic meaning is not.

Width rules

Addition generally returns the maximum operand width, not an automatic extra carry bit. Extend operands first:

full_sum <= ('0' & a_i) + ('0' & b_i);
sum_o    <= full_sum(a_i'range);
carry_o  <= full_sum(full_sum'high);

Use resize for explicit width control:

wide_u <= resize(narrow_u, wide_u'length); -- zero extension
wide_s <= resize(narrow_s, wide_s'length); -- sign extension
small  <= resize(wide_u, small'length);    -- truncation: verify it is intended

When reducing width, decide what should happen:

  • Wrap/truncate.
  • Saturate at minimum/maximum.
  • Report overflow.
  • Round, then truncate.

Never let the choice be accidental.

Natural and integer ranges

Integer types are useful for generics, counters with clear ranges, and testbench models.

signal index_q : natural range 0 to 15 := 0;

A constrained range documents intent and can reduce hardware. However, overflowing the declared range causes a simulation error. For bit-exact datapaths, unsigned and signed usually make width behavior clearer.

Arrays

Unconstrained array type

type sample_array_t is array (natural range <>) of signed(15 downto 0);
signal history : sample_array_t(0 to 7);

An unconstrained type can be reused at different lengths.

Two-dimensional structures

Prefer an array of vectors over manually flattening whenever synthesis/tool support is adequate:

type word_array_t is array (natural range <>) of std_logic_vector(7 downto 0);
signal registers : word_array_t(0 to 15);

Records

Records group related signals and make interfaces easier to evolve.

type stream_t is record
  valid : std_logic;
  ready : std_logic;
  data  : std_logic_vector(7 downto 0);
end record;

signal input_stream : stream_t;

For top-level FPGA ports, flat names are often easier to constrain. Records are particularly useful internally and in testbenches.

Enumerations

Enumerations are ideal for state machines:

type state_t is (IDLE, LOAD, RUN, DONE);
signal state_q, state_d : state_t := IDLE;

Vivado chooses an encoding unless an attribute or synthesis setting specifies one. Write behavior in terms of names, not assumed bit values.

Subtypes

A subtype constrains an existing type without creating an incompatible new type:

subtype byte_t is std_logic_vector(7 downto 0);
subtype percentage_t is natural range 0 to 100;

Fixed-point choices

For introductory work:

  1. Decide the binary-point position on paper.
  2. Store the bits in signed or unsigned.
  3. Widen products and additions deliberately.
  4. Create named functions for rounding and saturation.

IEEE fixed_pkg provides ufixed and sfixed for larger fixed-point projects, but learn width and binary-point reasoning first.

Example Q1.7 signed interpretation:

  • 8 total bits.
  • One sign/integer bit and seven fractional bits.
  • Stored integer 64 represents 0.5 because 64 / 128 = 0.5.

Arithmetic package rule

Use ieee.numeric_std. Avoid the old non-standard Synopsys packages:

  • std_logic_arith
  • std_logic_unsigned
  • std_logic_signed

Mixing them with numeric_std causes ambiguous overloads and non-portable code.

Type-design checklist

  • Does every vector have a documented meaning and bit order?
  • Are signed and unsigned values typed accordingly?
  • Are extensions performed before arithmetic that needs an extra bit?
  • Is every truncation, rounding, wrap, or saturation deliberate?
  • Are physical units represented clearly in names or types?
  • Can generics produce a zero or negative range? If so, add an assertion.