Skip to content

VHDL · Chapter 6.7 · Combinational Logic Design

Arithmetic Circuits

Datapaths compute, and in VHDL that computation rides on the standard numeric package. The unsigned and signed types give you add, subtract, multiply, and comparison operators that synthesize to real adders, subtractors, and comparators. The care is in the details. You capture carry and overflow by resizing to one extra bit, keep operand widths consistent, and use relational operators to produce the booleans that steer multiplexers. This lesson builds the standard combinational arithmetic blocks on those numeric types, shows the hardware each operation becomes, and reinforces the width and signedness discipline that keeps arithmetic correct. By the end you can add, compare, and detect overflow with confidence, and you understand why signed and unsigned magnitudes lead to different circuits from the same operators.

Foundation14 min readVHDLArithmeticnumeric_stdAdderComparatorOverflow

1. Engineering intuition — numbers become adders

Arithmetic in hardware is just more combinational logic: an adder is a cone of gates that turns two input numbers into their sum; a comparator turns two numbers into a boolean. The only thing that makes arithmetic special is that you must tell VHDL the bits mean numbers — which is what unsigned and signed from numeric_std do (lesson 2.8). Once the types carry that meaning, +, -, and < build the obvious hardware. The recurring concerns are width (results can need an extra bit) and signedness (unsigned magnitude vs signed two's-complement give different circuits).

2. Formal explanation — operations, width, carry, and overflow

arithmetic_basics.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);
 
sum   <= std_logic_vector(a + b);                    -- 8-bit add, result wraps mod 256
diff  <= std_logic_vector(a - b);                    -- subtract
incr  <= std_logic_vector(a + 1);                    -- incrementer
 
-- CARRY: resize to 9 bits so the top bit is the carry-out.
signal s9 : unsigned(8 downto 0);
s9    <= resize(a, 9) + resize(b, 9);                -- 9-bit add
sum8  <= std_logic_vector(s9(7 downto 0));
carry <= s9(8);                                       -- carry-out
 
-- COMPARISON: relational operators produce a boolean (drives selection).
ge    <= '1' when a >= b else '0';

numeric_std arithmetic on equal-width operands wraps at the width boundary; to keep the carry (or detect overflow), resize the operands to one extra bit and read the top bit of the wider result. Relational operators (<, >=, =, …) return a boolean, which feeds conditions and muxes. Width discipline is essential: operands of a binary operator generally must match width (resize as needed).

3. Production RTL — an ALU slice with flags

alu_with_flags.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all;
alu : process (all)
  variable wide : unsigned(8 downto 0);              -- 9-bit intermediate for carry
begin
  result <= (others => '0'); carry_out <= '0'; zero <= '0';   -- defaults → latch-free
  case op is
    when "00" => wide := resize(unsigned(a), 9) + resize(unsigned(b), 9);
                 result <= std_logic_vector(wide(7 downto 0));
                 carry_out <= wide(8);                          -- carry from the 9th bit
    when "01" => result <= std_logic_vector(unsigned(a) - unsigned(b));
    when "10" => result <= a and b;
    when others => result <= a or b;
  end case;
  if result = x"00" then zero <= '1'; end if;                   -- zero flag
end process;

What hardware does this become? A combinational ALU: an adder/subtractor (the add uses a 9-bit intermediate so bit 8 is the carry), bitwise gates, a result mux over the opcode, and a zero-detect comparator. Defaults keep all outputs latch-free; numeric_std makes the arithmetic well-defined and portable (never the obsolete std_logic_unsigned).

4. Hardware interpretation — adder and comparator datapaths

adder with carry via resize and a comparator producing a booleana, b(unsigned/signed)operandsN+1 bit adderresize then addsum + carrylow N = sum, top bit =carrycomparatora >= b → booleanflag / selectdrives a mux/condition12
Combinational arithmetic maps directly to datapath hardware. An N-bit add of two numeric_std operands is an N-bit adder; resizing both operands to N+1 bits and reading the top bit of the result captures the carry-out (or, for signed, detects overflow). A relational operator is a comparator producing a boolean. Both are pure combinational cones — the result follows the operands after the propagation delay, with no state.

5. Simulation interpretation — results track operands

An 8-bit adder with carry: sum and carry follow the operands

8 cycles
An 8-bit adder with carry: sum and carry follow the operands0xC8 + 0x40 = 0x108 → sum=0x08, carry=10xC8 + 0x40 = 0x108 → …0xFF + 0x01 = 0x100 → sum=0x00, carry=1 (wrap captured by carry)0xFF + 0x01 = 0x100 → …a1010C8C8FFFF7F7Fb0505404001010101sum1515080800008080carry00111100t0t1t2t3t4t5t6t7
The combinational adder's sum and carry are pure functions of a and b: when the true sum exceeds 8 bits, sum holds the low byte and carry captures the overflow into the 9th bit (from the resize-to-9 trick). No clock, no memory — the outputs settle to the arithmetic result whenever the operands change.

6. Debugging example — the missing carry and the width mismatch

Expected: an add that detects overflow. Observed: the carry is always 0 (overflow lost), or the tool errors on a length mismatch. Root cause: the add was done at the operands' native width, so the carry simply wrapped away with no extra bit to hold it; or two different-width operands were added without resizing, which numeric_std rejects (no silent extension). Fix: resize both operands to N+1 bits and read the top bit for carry; resize operands to a common width before any binary operation. Engineering takeaway: carry/overflow lives in the bit above the operands — make room for it with resize, and keep operand widths equal.

carry_and_width_fix.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- BUG: native-width add loses the carry; mismatched widths are illegal.
-- carry <= ??? ;  sum <= std_logic_vector(a8 + b4);   -- length mismatch + no carry
-- FIX: common width, and one extra bit for carry.
wide  <= resize(a8, 9) + resize(b4, 9);                 -- equalise width, +1 for carry
sum   <= std_logic_vector(wide(7 downto 0));
carry <= wide(8);

7. Common mistakes & what to watch for

  • Losing carry/overflow. A native-width add wraps; resize to N+1 and read the top bit (unsigned carry); for signed, detect overflow from the sign bits.
  • Width mismatches. numeric_std never auto-extends; resize operands to a common width.
  • Mixing signed and unsigned. They are different circuits; convert deliberately (lesson 2.9).
  • Using std_logic_unsigned. Obsolete and non-portable; always numeric_std on unsigned/signed.
  • Doing arithmetic on std_logic_vector. Cast to unsigned/signed first; the vector has no +.

8. Engineering insight & continuity

Combinational arithmetic is "ordinary logic that happens to mean numbers," and numeric_std is what gives the bits that meaning so +, -, and < build the right datapath. The two habits that keep it correct are width discipline (resize to a common width, add a bit for carry/overflow) and signedness discipline (choose unsigned or signed deliberately). With routing, encoding, and now arithmetic covered, only one combinational topic remains — the one structure you must not build. The next lesson, Combinational Loops and Why to Avoid Them, shows how a signal that feeds back on itself with no register creates an undefined, unsynthesizable circuit, and why the fix — a register — is the doorway into sequential logic.