Skip to content

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

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

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

plus to adder with carry bit, times to DSP multiplier with width growth, shifts as free wiring+ / −→ adder (carry chain), +1bit×→ DSP multiplier, width =sum× / ÷ by 2^n→ shift (free wiring)sized hardwarecarry chains / DSP / wires12
Arithmetic operators map to sized hardware primitives with characteristic width growth. Addition and subtraction become adders built on carry chains, and the result needs one extra bit to hold the carry. Multiplication becomes a multiplier that on FPGAs maps to a dedicated DSP block, and the result width is the sum of the operand widths. Signed versus unsigned behavior comes from the numeric_std type. Strength reduction replaces expensive operators with cheap ones — power-of-two multiply/divide become shifts (free wiring), a constant multiply becomes shift-and-add — while non-constant division is an expensive iterative block to avoid. This is an operator-to-hardware structure; the waveform below shows add carry-out and multiply width growth.

5. Simulation interpretation — add carry and multiply width growth

Add needs +1 bit (carry); multiply needs sum-of-widths

4 cycles
Add needs +1 bit (carry); multiply needs sum-of-widths0xFF + 0x01 = 0x100 → needs the 9th bit (carry); 8-bit result would wrap to 0x000xFF + 0x01 = 0x100 → …0x80 * 0x80 = 0x4000 → 16-bit result; truncating to 8 bits loses everything0x80 * 0x80 = 0x4000 →…a (8b)FF800FA0b (8b)01800F02sum (9b)10010001E0A2prod (16b)00FF400000E10140t0t1t2t3
Width growth is not optional. 0xFF + 0x01 overflows 8 bits to 0x100, so the sum must be 9 bits or the carry is lost. 0x80 × 0x80 = 0x4000 needs all 16 bits of an 8×8 product. Sizing results with resize (add: +1 bit) and full product width (multiply: sum of widths) is what keeps arithmetic correct; under-sizing truncates silently.

6. 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.

size_the_result.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 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 resize or truncation corrupts silently.
  • Wrong signedness. signed vs unsigned (the numeric_std type) 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.