VHDL · Chapter 16.5 · Synthesis and RTL Implementation
Synthesizing Arithmetic
Arithmetic is where coding choices translate most directly into silicon cost. Add and subtract synthesize to adders built from carry chains, which use dedicated fast-carry logic on FPGAs. Multiply synthesizes to a multiplier, which on an FPGA maps to a dedicated DSP block rather than a sea of lookup tables. Results grow in width, since an add needs one extra bit for the carry and a multiply needs the sum of the operand widths, and getting that wrong truncates silently. Whether the hardware is signed or unsigned comes entirely from the numeric type you chose, not the operator. Smart structure pays off, because a constant multiply reduces to cheap shift-and-add and a multiply or divide by a power of two is just a shift, while division by a non-constant value is expensive and usually avoided. This lesson covers how each operator maps to hardware and the tricks that keep arithmetic efficient.
Foundation15 min readVHDLSynthesisArithmeticAddersDSPnumeric_std
1. Engineering intuition — operators are real, sized hardware
An arithmetic operator is not free notation — it is a block of gates with a size and a cost. A + is an adder
whose delay grows with width along a carry chain; a * is a multiplier, one of the most expensive structures on
the chip, which is exactly why FPGAs provide dedicated DSP blocks for it. And the result of arithmetic is
wider than its inputs: add two N-bit numbers and the true answer is N+1 bits (the carry); multiply two N-bit
numbers and it is 2N bits. Treat operators as the sized hardware they are — mind the width growth, know that *
costs a DSP, and prefer cheap forms (shifts, shift-add) when you can — and your arithmetic stays both correct
(no silent truncation) and efficient.
2. Formal explanation — operators, width growth, and signedness
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all;
signal a, b : unsigned(7 downto 0);
-- + / - → ADDER (carry chain). Result needs ONE extra bit for the carry.
sum <= resize(a, 9) + resize(b, 9); -- 8+8 → 9 bits (keep the carry)
-- * → MULTIPLIER (→ DSP block on FPGA). Result width = SUM of operand widths.
prod <= a * b; -- 8 × 8 → 16 bits (unsigned'length + unsigned'length)
-- SIGNED vs UNSIGNED comes from the TYPE, not the operator:
-- unsigned a*b → unsigned multiply ; signed a*b → two's-complement multiply.
-- STRENGTH REDUCTION (cheap forms):
y <= a * 4; -- multiply by power of 2 → SHIFT LEFT by 2 (free)
y <= shift_right(a, 1); -- divide by 2 → SHIFT RIGHT (free)
z <= a * 5; -- CONSTANT multiply → shift-and-add ((a<<2)+a), cheap
-- DIVISION by a NON-CONSTANT (a / b) → expensive iterative hardware; AVOID or use a dedicated divider.+/- → adders (carry chains), * → multipliers (→ DSP on FPGA). Results grow: add → +1 bit,
multiply → sum of widths — resize to keep them. Signed/unsigned is set by the numeric_std type.
Constant/power-of-two multiplies reduce to shifts/shift-add; non-constant division is costly and best
avoided.
3. Production usage — efficient, correct arithmetic
-- Keep widths MINIMAL but sufficient: size results to hold the true value, no more.
signal acc : unsigned(15 downto 0);
process (clk) begin
if rising_edge(clk) then
acc <= acc + resize(sample, acc'length); -- accumulate; result width holds growth
end if;
end process;
-- Let * map to a DSP block; pipeline big multiplies for timing (16.6):
process (clk) begin
if rising_edge(clk) then
prod_r <= a * b; -- registered → DSP with its register stage, better timing
end if;
end process;
-- Prefer constant / shift forms over general multiply/divide where possible:
scaled <= shift_left(x, 3); -- ×8 via shift (no multiplier)
avg <= shift_right(x + y, 1); -- (x+y)/2 via shift (no divider)What hardware does this become? a + b is an adder along the carry chain; a * b infers a DSP block (or
a LUT multiplier if DSPs are exhausted) — and registering the product lets the tool use the DSP's internal
pipeline register for better timing. shift_left(x,3) and shift_right(...,1) are wiring (no arithmetic
hardware at all), so power-of-two scaling is free, and a*5 becomes a small shift-and-add instead of a full
multiplier. The cost differences are large: a shift is free, a constant multiply is cheap, a general multiply is a
DSP, and a non-constant divide is a big iterative block — choose the form deliberately.
4. Structural interpretation — operators to primitives, with width growth
5. Simulation interpretation — add carry and multiply width growth
Add needs +1 bit (carry); multiply needs sum-of-widths
4 cycles6. Debugging example — silent truncation from under-sized results
Expected: correct arithmetic results. Observed: sums wrap around (a large addition gives a small number),
or a product is wildly wrong — yet there is no error, just bad data. Root cause: the result was under-sized
— an add assigned to a same-width target dropped the carry, or a multiply assigned to an N-bit signal kept
only the low N bits of the 2N-bit product. (A related bug: using unsigned where the data is signed, so the
sign is misinterpreted.) Fix: size results to hold the true value — resize operands and target to N+1
for an add's carry, and to the sum of widths for a multiply — and choose signed/unsigned to match the
data's meaning (16.5/11.6). Engineering takeaway: arithmetic results grow — an add needs one extra bit, a
multiply needs the sum of widths — so size targets accordingly or the carry/high bits are silently truncated.
-- BUG: 8-bit sum target drops the carry; 8-bit product keeps only low bits.
-- sum <= a + b; -- wraps at 0x100
-- prod <= a * b; -- 8-bit target loses the upper 8 bits of a 16-bit product
-- FIX: size results to hold growth.
sum <= resize(a,9) + resize(b,9); -- 9-bit sum keeps the carry
prod <= a * b; -- prod declared 16 bits (a'length + b'length)7. Common mistakes & what to watch for
- Under-sized results. Add needs +1 bit (carry); multiply needs sum of widths — size targets with
resizeor truncation corrupts silently. - Wrong signedness.
signedvsunsigned(thenumeric_stdtype) sets the arithmetic; mismatched type misinterprets the value. - General multiply/divide where a shift works. Power-of-two scaling is a shift (free); a constant multiply is shift-and-add — far cheaper than a DSP.
- Non-constant division in RTL. Very expensive/iterative; avoid, or use a dedicated divider with known latency.
- Not registering big multiplies. Register products so the tool uses the DSP's pipeline register for timing (16.6).
8. Engineering insight & continuity
Arithmetic maps to sized hardware: +/- → adders on carry chains (+1 bit for the carry), * → multipliers
that become DSP blocks (width = sum of operands), with signed/unsigned set by the numeric_std type.
Strength reduction — shifts for powers of two, shift-and-add for constants — keeps it cheap, while non-constant
division is best avoided. Size results to prevent silent truncation. The cost of arithmetic is not just area but
delay — a wide adder or multiplier can be the slowest path in the design — which leads directly into the next
lesson, Timing, Critical Paths, and Pipelining: how propagation delay sets the clock frequency and how to break
long paths.