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:
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.
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:
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:
Fixed-point choices¶
For introductory work:
- Decide the binary-point position on paper.
- Store the bits in
signedorunsigned. - Widen products and additions deliberately.
- 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_arithstd_logic_unsignedstd_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.