Digital Logic/Multiplexers, Decoders, and the ALU

Lesson 4.31,917 words

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.

A 2:1 mux (left) selects d0 or d1 with one select bit s; a 4:1 mux (right) selects one of four inputs with two select bits s1 s0. The select value, read as a binary number, names the chosen input.

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.

A 4:1 mux built from gates. The select bits s1 s0 are decoded into four one-hot lines; each ANDs with one data input; the four AND outputs OR into y. Only the line matching the select value passes its data.

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.

A 2-to-4 decoder. The 2-bit input a1 a0 drives exactly one of four output lines high — the one whose index equals the input. Each output is a minterm: y2 is high only when a1 a0 = 10.

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.

A one-bit full adder as two function blocks. A 3-input XOR computes the parity (sum = a XOR b XOR cin); a majority block computes the carry out (1 when at least two of a, b, cin are 1). Each input fans to both blocks.

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.

A 4-bit ripple-carry adder. Each full adder FAi adds ai, bi, and the carry ci from the stage below, producing sum si and carry c(i+1). The carry chain runs right-to-left from c0 to c4.

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.

A carry-lookahead tree over 16 bits. Each 4-bit block computes its internal carries plus a group (G, P) in parallel; the second-level unit computes c4, c8, c12 from the four (G, P) pairs and c0. Two levels cover 16 bits; three cover 64. Depth grows with log n, not n.

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.

An ALU. Two word operands A and B enter; a control code selects which operation drives the result; condition flags (zero, sign, carry, overflow) report properties of that result.

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 .

A one-bit ALU slice. AND, OR, and the adder's sum are all computed in parallel; the op mux selects one as the result. The invert mux feeds the adder b or NOT b, so invert = 1 with carry-in 1 turns add into subtract. Chaining cout to the next slice's cin ripples the design to n bits.

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.

alu.hclc
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 ╌╌