Computing and the Command Line

Adding with Gates

Addition, the most basic thing a processor does, built from gates. The truth table for adding two bits gives a half adder, an XOR and an AND; a full adder adds the carry from the next column; and four full adders chained together make a ripple-carry adder whose carry ripples from right to left, with overflow falling off the end. A 40-line Python program builds every gate from NAND and checks the adder against all 256 four-bit sums. Where adders live in the processor's arithmetic logic unit.

  • 7 min
  • 7 steps
  • 2 questions
  • Lesson 67 of 80

In this lesson

  1. Adding two bits
  2. A full adder
  3. Four bits and more
  4. Simulating it
  5. Where adders live
  6. Your turn
  7. So

Adding two bits

Start with the smallest sum: one bit plus one bit. There are four cases 1:

A B sum carry
0 0 0 0
0 1 1 0
1 0 1 0
1 1 0 1

1 + 1 is 2, 10 in binary: sum bit 0, carry 1, the same as 5 + 5 in decimal leaving 0 and carrying 1. Read the columns as truth tables: sum is A XOR B, and carry is A AND B 1. Two gates, and that’s a half adder.

Top left, adding two bits: A B 0 0 gives sum 0 carry 0; 0 1 gives 1, 0; 1 0 gives 1, 0; 1 1 gives 0, 1; so sum = A XOR B and carry = A AND B, drawn as a half adder with an XOR gate and an AND gate. Top right, a full adder adds a carry in too: sum = A XOR B XOR carry_in; carry_out = (A AND B) OR (carry_in AND (A XOR B)); three inputs, A, B, and the carry from the column to the right, like carrying the 1 in pencil-and-paper sums. Bottom, a 4-bit ripple-carry adder adding 6 + 7: bit 0, A=0 B=1 in=0, sum 1; bit 1, A=1 B=1 in=0, sum 0, carry 1; bit 2, A=1 B=1 in=1, sum 1, carry 1; bit 3, A=0 B=0 in=1, sum 1, carry out 0. Carries ripple right to left; 0110 + 0111 = 1101, 13. A carry out of the leftmost adder doesn't fit in 4 bits: that's overflow.
XOR makes the sum bit, AND the carry; chain full adders and the carry ripples left. Credit: StudyCorner diagram · CC BY 4.0 · Source

Quick check

In a half adder, which gate produces the sum bit?

A full adder

A half adder handles the rightmost column. Every other column also has to add the carry coming in from the column to its right, so it needs three inputs: A, B, and carry in 1. That’s a full adder:

sum       = A XOR B XOR carry_in
carry_out = (A AND B) OR (carry_in AND (A XOR B))

The carry out is 1 when A and B are both 1, or when exactly one of them is 1 and there’s a carry coming in: exactly the cases where the column adds up to 2 or 3.

Four bits and more

To add 4-bit numbers, put four full adders side by side, one per column, and wire each one’s carry out to the next one’s carry in; the rightmost gets a carry in of 0 1. Adding 6 (0110) and 7 (0111) column by column, right to left:

  • bit 0: 0 + 1 = 1, carry 0
  • bit 1: 1 + 1 = 0, carry 1
  • bit 2: 1 + 1 + 1 = 1, carry 1
  • bit 3: 0 + 0 + 1 = 1, carry 0

Result 1101, 13. Each column can’t finish until the carry from its right arrives, so the carry ripples from the lowest bit to the highest: a ripple-carry adder 1. Any carry out of the leftmost column doesn’t fit, which is the overflow from module 1, seen from the inside.

Quick check

Why is it called a ripple-carry adder?

Simulating it

You can build all of this in a few lines of code. This program defines one gate, NAND, builds every other gate from it, then a full adder and a 4-bit adder, exactly as above. You don’t need to know Python to follow it; read the definitions as the formulas they are. Save it as gates.py:

# gates.py: logic gates and an adder, built from nothing but NAND.

def nand(a, b):
    return 0 if (a and b) else 1

def not_(a):
    return nand(a, a)

def and_(a, b):
    return not_(nand(a, b))

def or_(a, b):
    return nand(not_(a), not_(b))

def xor(a, b):
    return and_(or_(a, b), nand(a, b))

def full_adder(a, b, carry_in):
    """Add three bits; return (sum bit, carry out)."""
    s = xor(xor(a, b), carry_in)
    carry_out = or_(and_(a, b), and_(carry_in, xor(a, b)))
    return s, carry_out

def add4(a_bits, b_bits):
    """Add two 4-bit numbers given as lists, lowest bit first."""
    carry = 0
    result = []
    for a, b in zip(a_bits, b_bits):
        s, carry = full_adder(a, b, carry)
        result.append(s)
    return result, carry

def to_bits(n):
    return [(n >> i) & 1 for i in range(4)]

def from_bits(bits):
    return sum(bit << i for i, bit in enumerate(bits))

if __name__ == "__main__":
    print("A B | AND OR XOR NAND")
    for a in (0, 1):
        for b in (0, 1):
            print(a, b, "|", and_(a, b), "  ", or_(a, b), " ", xor(a, b), "  ", nand(a, b))
    print()
    for x, y in [(5, 3), (6, 7), (9, 9)]:
        bits, carry = add4(to_bits(x), to_bits(y))
        print(f"{x} + {y} = {from_bits(bits)}, carry out {carry}")

A few notes on reading it: def defines a function; not_, and_, and or_ have a trailing underscore because not, and, and or are Python’s own words; to_bits uses last lesson’s shift and mask, (n >> i) & 1, to pull out bit i. Run it:

me@linuxbox:~$ python3 gates.py
A B | AND OR XOR NAND
0 0 | 0    0   0    1
0 1 | 0    1   1    1
1 0 | 0    1   1    1
1 1 | 1    1   0    0

5 + 3 = 8, carry out 0
6 + 7 = 13, carry out 0
9 + 9 = 2, carry out 1

The truth tables match, built from NAND alone. And 9 + 9: 18 doesn’t fit in four bits (the most is 15), so the adder gives 2 with a carry out of 1, overflow, since 18 = 16 + 2. Checking every one of the 256 possible pairs against Python’s own +, counting the carry as 16, finds no mistakes.

Where adders live

Inside the processor, adders and the other arithmetic and logic circuits make up the arithmetic logic unit, the ALU: the part that executes add, subtract, AND, OR, compare, and so on 1. Adders also do quieter jobs, like stepping the program counter to the next instruction and working out memory addresses 1. Subtraction reuses the adder, adding the two’s complement negation (module 1) 1.

Real processors don’t use plain ripple carry for 64-bit numbers, since waiting for a carry to cross 64 columns is slow; they use circuits that work out carries ahead of time. The idea is the same.

Your turn

Exercises

  1. Add 0101 and 0011 by hand, column by column, writing each carry. Check with echo $((2#0101 + 2#0011)).
  2. Fill in a full adder’s truth table: all eight combinations of A, B, and carry in.
  3. Save gates.py and run it.
  4. Change the last loop to add (15, 1) and (8, 8). What happens, and why?
  5. Add this line at the end of the file (indented to match the other lines) to check all 256 sums: print(all(from_bits(add4(to_bits(x), to_bits(y))[0]) + 16 * add4(to_bits(x), to_bits(y))[1] == x + y for x in range(16) for y in range(16))).
Answers
  1. 1000, 8: carries out of bits 0, 1, and 2.
  2. sum is 1 for an odd number of 1s (one or three); carry out is 1 for two or three 1s.
  3. 15 + 1 = 0, carry out 1 and 8 + 8 = 0, carry out 1: both are 16, which needs a fifth bit.
  4. It prints True.

So

Adding two bits is XOR for the sum and AND for the carry: a half adder. A full adder adds a carry in as well, and chaining full adders, each carry out feeding the next carry in, gives a ripple-carry adder for numbers of any width; a carry out of the top is overflow. A short program built from a single NAND function reproduces every gate and adds correctly. In the processor, adders form part of the ALU and also step the program counter and calculate addresses.

Lesson complete

Nice work.

1day streak
0/1today's goal
–correct

Up next · 8 min

Memory from Feedback

Next lesson
Sources for this lesson
  1. 1
    Suzanne J. Matthews, Tia Newhall, Kevin C. Webb. Dive into Systems. No Starch Press (free online edition). 2022. verifiedCh. 4 Binary and Data Representation: bits as two voltage states, bytes (8 bits, 256 values, smallest addressable unit), words of 32 or 64 bits, n bits give 2^n values; decimal and binary place value with 0b and 0x prefixes; hexadecimal as four bits per digit; fixed storage sizes and unsigned ranges; two's complement with a negative-weighted top bit, one zero, range -2^(n-1) to 2^(n-1)-1, all ones is -1, negation by flipping bits and adding one; subtraction as adding the negation, reusing negation and addition circuits; overflow and the odometer analogy.