Chapter 3
How does it add?

  1. The browser
  2. The network
  3. Software and languages
  4. The CPU
  5. Memory
  6. Arithmetic
  7. Logic gates
  8. The switch
Figure 3.1: Where we are in the climb from transistor to browser: the arithmetic rung. This chapter wires gates into an adder and a small arithmetic-logic unit.

Chapter 2 built XOR: a gate whose output is 1 exactly when its two inputs differ. That phrasing sounds abstract, but “they differ” is another way of saying “one is 0 and one is 1,” which is the same as saying the sum of the two bits is 1 without a carry. You already have the adder; you just have not called it that yet.

3.1 Combinational logic

Every circuit in this chapter is combinational: its output is a pure function of its present inputs, with no memory of any earlier input. Feed it the same inputs twice and it gives the same output twice. This is in contrast to the memory circuits that Chapter 4 will build from these same gates, which can remember an earlier input. Here the output is always and only a function of right-now inputs. That constraint makes the circuits much easier to reason about: trace the signal forward, and you know everything.

3.2 The half adder

The half adder is the simplest binary adder. It takes two single bits and returns two bits: a sum and a carry. Work through the four possible inputs and the outputs fall into place. When both inputs are 0 the sum is 0 and there is no carry. When exactly one input is 1 the sum is 1 and there is still no carry. When both inputs are 1 the sum wraps around to 0 but there is a carry of 1, encoding the fact that 1 + 1 = 2 = 102.

The sum column is exactly the XOR truth table, and the carry column is exactly the AND truth table. The half adder needs no new gate. XOR from Chapter 2 pays off immediately.

Figure 3.2 shows the gate-level circuit: XOR feeds the sum output and AND feeds the carry output, both driven by the same two inputs.

half-adderhalf-adder
Figure 3.2: The half adder. Both gates share the same inputs a and b. XOR produces the sum bit; AND produces the carry bit. No new gate is needed.

Example 3-1. Half adder: sum is XOR, carry is AND

def half_adder(a: int, b: int) -> tuple[int, int]:
    """Sum and carry of two bits: the sum is XOR, the carry is AND."""
    return xor_(a, b), and_(a, b)1

3.3 The full adder

The half adder has one weakness: it cannot accept a carry coming in from a less-significant bit position. Chaining two half adders together fixes this. The first half adder adds a and b, producing an intermediate sum and an intermediate carry. The second half adder adds that intermediate sum to the carry-in cin, producing the final sum and a second carry. Because at most one of the two intermediate carries can be 1 at a time, a simple OR of the two carries gives the correct carry-out.

Figure 3.3 shows the construction: two half-adder blocks feeding one OR gate.

full-adderfull-adder
Figure 3.3: The full adder as two half adders and an OR. The first half adder combines a and b; the second combines that sum with cin. The OR merges the two possible carries into a single carry-out.

Example 3-2. Full adder: two half adders and an OR

def full_adder(a: int, b: int, cin: int) -> tuple[int, int]:
    """Add three bits (a, b, and a carry-in) from two half adders and an OR."""
    sum1, carry1 = half_adder(a, b)1
    sum2, carry2 = half_adder(sum1, cin)1
    return sum2, or_(carry1, carry2)2

3.4 The ripple-carry adder

A single full adder handles one bit position. To add two n-bit numbers, chain n full adders in series: feed the carry-out of each stage into the carry-in of the next. The lowest stage receives a carry-in of 0; the highest stage’s carry-out becomes the extra top bit of the result. That is the complete ripple-carry adder. Figure 3.4 shows a four-stage chain drawn so that the carry path is impossible to miss.

ripple-carryripple-carry
Figure 3.4: A four-bit ripple-carry adder, drawn in written-number order with the most significant bit on the left. Data inputs ai and bi enter each full-adder block from the top; the carry chain (red) flows right to left from the carry-out of each stage to the carry-in of the next. Bit 0 receives a fixed carry-in of 0; bit 3 emits the overflow carry-out. The carry chain is what makes the circuit slow at scale: each stage must wait for the stage to its right to settle before its own carry-out is valid.

The interactive version needs the in-browser runtime.

The cost of the ripple-carry design is its latency. Before bit position k can produce a valid carry, every stage to its right must have settled. For wide adders, the critical path runs the full width of the carry chain, one full adder deep per bit. Real processors replace this design with carry-lookahead or prefix adders that compute carries in parallel, but the ripple-carry adder is the right starting point because the logic is transparent: you can trace the carry with your finger.

3.5 Choosing an operation: the multiplexer and the ALU

An arithmetic-logic unit does not just add. It computes several candidate results, one per operation, and then selects one based on a control signal called the opcode. The device that does the selecting is a multiplexer.

A 2-to-1 multiplexer takes two data inputs and one select line. When the select is LOW it passes the first input; when HIGH it passes the second. Built from NOT, AND, and OR, it is a two-way switch made of gates.

Example 3-3. 2-to-1 multiplexer: select one of two inputs

def mux(sel: int, a: int, b: int) -> int:
    """Two-to-one multiplexer: a when sel is LOW, b when sel is HIGH."""
    return or_(and_(not_(sel), a), and_(sel, b))1

Three 2-to-1 muxes compose into a 4-to-1 mux. With a 2-bit opcode the 4-to-1 mux selects one of four inputs, which is exactly what a minimal ALU needs: one output per supported operation.

The ALU below supports four operations: add (opcode 00), bitwise AND (01), bitwise OR (10), and bitwise XOR (11). It computes all four results for every bit position and uses a 4-to-1 mux per bit to pick the winner.

Example 3-4. A minimal ALU: four operations, one opcode, one mux per bit

def alu(op_hi: int, op_lo: int, a_bits: list[int], b_bits: list[int]) -> list[int]:
    """A tiny ALU. The opcode (op_hi, op_lo) selects 0=add, 1=and, 2=or, 3=xor;
    every candidate result is computed and a 4:1 mux picks one per bit. The add
    result is one bit wider (its carry-out), so the bitwise results are padded
    with a LOW to match."""
    add = ripple_add(a_bits, b_bits)1
    a = a_bits + [LOW]
    b = b_bits + [LOW]
    return [mux4(op_hi, op_lo, add[i], and_(a[i], b[i]), or_(a[i], b[i]),2
                 xor_(a[i], b[i])) for i in range(len(add))]

Arithmetic can compute, but it cannot remember; the next chapter gives the machine state.

Exercises

PreviousNext
✎ Suggest an edit