Multiplexers, Decoders, and the ALU
The combinational building blocks that make a datapath. We build the 2:1 and 4:1 multiplexer and tie it back to HCL's case expression, the n-to-2^n decoder, a one-bit full adder (sum is XOR, carry is majority), the ripple-carry adder that chains them, and finally the ALU — a function unit that selects among add, sub, and, and xor under a control input and exposes condition flags.
╌╌╌╌
The last lesson
described combinational logic in HCL and claimed the case expression is a
multiplexer. This lesson cashes that in. We build the standard combinational
function units (multiplexers, decoders, adders, and finally the arithmetic
logic unit) that together form the computational core of a processor's datapath.
Every block here is pure combinational logic: a Boolean function of its current
inputs.
The multiplexer: a data selector
A multiplexer (mux) routes one of several data inputs to a single output, chosen by a set of select lines. A mux has two data inputs , one select bit , and output . As a Boolean function, : when the first AND passes and the second is killed; when the reverse. A mux needs two select bits to name one of four inputs.
Inside, a mux is one AND per data input — each gated by the select combination that names it — feeding one OR. Writing the two select bits as , the Boolean form is
Each product is a data input ANDed with a decoded select: the four select combinations are the one-hot outputs of a 2-to-4 decoder. So a mux is a decoder driving a bank of AND gates whose outputs OR together — one figure below made of parts from this same lesson.
Trace a selection. With , the decoded line is and the other three are ; the third AND passes while the others force , so the OR sees only and . Change the select to and the second AND opens instead, routing . The select value, read as a binary number, is the index of the data input that reaches the output — a data selector, exactly as promised.
This is the hardware behind HCL's case: a case with selects compiles to a
mux tree, with the top-of-list priority resolving overlapping selects. Mutually
exclusive selects, like a one-hot control field, give a clean mux where
the select code names the chosen input directly. Muxes are everywhere in a
datapath: choosing whether the ALU's second operand comes from a register or an
immediate, choosing which value to write back, choosing the next instruction
address.
The decoder: select one of lines
A decoder takes an -bit input and asserts exactly one of output lines — the one whose index equals the input value. It is one-hot: output line is iff the input equals , all others . Each output is one minterm of the input bits, so a -to- decoder is four AND gates fed by the inputs and their complements.
Decoders pick out one item from a numbered set: the natural job is address
decoding, turning a register number or memory address into a single select this word
line, which is how the register file and memory array in the next two
lessons choose which storage cell to touch.
The full adder
Arithmetic starts at one bit column. Adding two bits and plus an incoming carry produces a sum bit and an outgoing carry. This is a full adder, and its two outputs are functions we have already met:
The sum is when an odd number of the three inputs are : that is the three-input XOR (parity). The carry is when at least two of the three inputs are : the majority function from lesson 1. Check the corner: with , sum and , i.e. , sum carry . Correct.
Ripple-carry: chaining full adders
To add two -bit numbers, place full adders side by side, one per bit position, and feed each adder's carry-out into the next adder's carry-in. The carry ripples from the low bit to the high bit. The carry into bit is the overall carry-in (set it to for subtraction, below), and the carry out of the top bit is the overall carry-out.
Ripple-carry is correct but slow, and the critical-path accounting of the last lesson says exactly how slow. Each stage's carry-out is the majority function, a two-level AND-OR circuit, so the carry chain costs about 2 gate delays per bit: settles at , at , and at . The top sum bit needs plus one more XOR, so a 64-bit ripple adder is roughly gate delays deep. At 25 ps per level that is ns for one addition: a 300 MHz clock in a machine whose other blocks settle ten times faster. The delay is linear in , and the adder sits on the critical path of nearly every instruction. Real adders spend gates to buy depth.
Carry-lookahead: computing carries in parallel
The ripple is slow because each carry waits for the previous one. But bit 's relationship to the carry is determined before the carry arrives. From and alone:
- if , the column generates a carry no matter what comes in: ;
- if exactly one of is , the column propagates an incoming carry: .
Every and is ready after one gate level, all in parallel. The carry recurrence then unrolls by substitution:
Read aloud: bit generated a carry, or it propagated one that bit generated, or both columns propagated the original . Each right-hand side is a two-level AND-OR circuit of signals available after one level, so all four carries of a 4-bit group settle in about 3 gate delays, independent of one another, no ripple.
The unrolling cannot continue forever: written this way is an OR of 17 terms, one an AND of 16 inputs, and the fan-in cost of such wide gates eats the savings. So the trick is applied hierarchically. A 4-bit group condenses itself into a group generate and group propagate,
which record whether the group produces a carry and whether it passes one along — the same two facts as for a single column, now at group scale. A second-level lookahead unit treats the groups as super-columns and computes every group's carry-in from the pairs and in two more levels.
Count the depth for 64 bits: one level for all , two for group s, two per tree level for the carries coming back down, one XOR for the sums: on the order of a dozen gate delays regardless of width, against for the ripple. At 25 ps per level that is roughly ns instead of . The price is the lookahead logic itself, a few extra gates per bit; every serious adder pays it. The pattern — replace a linear chain with a -depth tree — is the same one the equality net of the previous lesson used, and it recurs throughout hardware design.
The ALU: a function unit with a control input
An arithmetic logic unit packages several operations into one block and uses a control input to select which result appears at the output. The standard teaching ALU (CS:APP's) takes two word operands and and a small function code, and produces a result plus condition flags.
Internally the ALU computes all the operations in parallel and a final mux
picks the selected one — exactly the case-to-mux pattern. Two details make it
practical. First, subtraction reuses the adder: ,
so feeding the adder the complement of and setting the carry-in turns
add into subtract with no extra adder. Second, the flags are computed from the
result: ZF (zero) is when every result bit is , SF (sign) is the top
result bit, CF (carry) is the adder's carry-out, and OF (overflow) signals
signed overflow. These flags are what conditional branches later test. Not every
ISA keeps all four: Y86-64's condition codes hold only ZF, SF, and OF — it
has no unsigned branches — while CF is needed in ISAs like x86-64, whose
unsigned ja/jb family
reads it.
One bit of the ALU
Slice the ALU at a single bit position and every idea in this lesson appears in
one small circuit. The slice computes AND, OR, and the full adder's sum for its
bit, all three at once, and an operation mux steers one to the result. An
invert control sits in front of the adder's input, choosing between
and ; drive invert = 1 and together and the
chained slices compute .
Work one subtraction through the 4-bit version. Compute : , , so , and with the adders produce . The carry out of the top bit falls off the end, leaving . Discarding that carry is the modular wraparound of two's-complement arithmetic, and the ALU's OF logic checks the top two carries to flag the cases where wraparound changes the sign incorrectly.
word aluResult = [
fn == ALU_ADD : A + B;
fn == ALU_SUB : A - B;
fn == ALU_AND : A & B;
fn == ALU_XOR : A ^ B;
];
bool ZF = (aluResult == 0); /* zero flag: result is all zeros */
The HCL reads as a four-way case on the function code. The gates it stands for:
an adder/subtractor, an AND array, an XOR array, and a
mux steering one result out under fn, with a zero-detect net on the side.
Faster adders and hardware multiply
CS:APP builds the ripple adder, sketches lookahead, and moves on. The public adder literature is deeper, and the same generate/propagate algebra underlies all of it.
Prefix adders. The recurrence is an instance of a
parallel prefix computation: define a combine operator on pairs,
, and every carry is a prefix
sum
under . Because is associative, the prefixes can be
computed by a balanced tree in depth — which is what carry-lookahead
does, stated abstractly. Different tree shapes trade depth against wiring:
Kogge–Stone (1973) is shallowest and widest (lots of wires, minimal depth),
Brent–Kung (1982) is deeper but far sparser, and Ladner–Fischer and
Han–Carlson sit between. A modern 64-bit adder in a high-frequency core is
usually a hybrid of these, chosen to fit the wire budget of its slot. All of them
compute exactly the carries this lesson unrolled by hand; they differ only in how
they share the sub-products.
Multiplication. The ALU here does add, subtract, AND, XOR — not multiply, because multiply does not fit the two-level template. An multiply is shifted partial products summed, and the public techniques attack both halves. Booth's algorithm (1951) and its radix-4 modified form roughly halve the number of partial products by recoding the multiplier. A Wallace tree (1964) or Dadda tree (1965) then sums those partial products in depth using carry-save adders — full adders wired to defer the carry instead of rippling it — collapsing the sum to two numbers that one final fast adder combines. That is why a hardware multiply, naively additions, costs only a handful of gate levels more than a single add.
Where the ALU went. The single-function-code ALU is the SISD picture. Modern cores replicate it: a superscalar processor has several integer ALUs plus separate multiply, divide, and floating-point units, and SIMD vector units (x86's AVX-512, ARM's SVE) apply one operation across many lanes of a wide register in parallel — the same one-bit slice tiled 512 ways with the carry chains broken at lane boundaries. The condition flags this lesson emits are still the interface a branch reads, unchanged since the 8086.
So far every block forgets its inputs the instant they change. The next lesson adds memory by deliberately introducing the feedback combinational logic forbade.
╌╌ END ╌╌