17Up the Ladder: Real CPUs
Last chapter you did something that should still feel a little unreal. You built a whole CPU from transistors, named it SAP-1, and ran real programs on it. Then I left you on the edge of a claim: the processor in your phone is not different in kind from the toy in your hands. Let's make good on that, carefully, because it's true in a way that's genuinely startling. Your machine has 16 bytes of RAM and ticks at whatever speed your finger can punch the clock. The first iPhone had 128 million bytes and a chip stepping hundreds of millions of times a second. That's not a small gap. It's roughly eight million-fold more memory. So the honest question is this: is that a different machine, or the same machine, scaled? Here's the answer we're going to earn, block by block. It runs the same three moves per instruction — fetch, decode, execute — that you already wired by hand. Your toy spends five clock ticks walking those three moves, which is a tick count and not two extra moves. Hold the two numbers apart; we'll come back to why the difference is the whole game. Everything a real chip adds is more of everything: more bits per word, more registers, more RAM, a faster clock, a smarter contract for handing work to the silicon. There's also exactly one genuinely new rung that your toy was missing. It's a stack, the small trick that finally lets code call other code and come back. We'll measure the gap exactly, climb it piece by piece, and then build that last missing rung ourselves.
01The same five moves
Let's start by putting the two machines on one table and staring at the numbers. The gap really is enormous, and I don't want to soften it. On the left, the SAP-1 you built: an 8-bit word, 16 bytes of RAM, one accumulator, and a clock you drive by hand. On the right, an ARM Cortex-class chip: a 32-bit word, memory in the megabytes, sixteen registers, hundreds of millions of ticks a second. Sixteen is the count I'll hold to all chapter. ARM names them R0–R15, and thirteen of those are free for whatever you want to keep in them. The other three already have jobs, and we'll meet two of them by name before the chapter is out. Every single row is bigger, often by a factor of a million. And yet the bottom row is identical: both fetch an instruction, decode it, execute it, repeat. Scan the rows, then flip to the punchline.
That last row is the whole chapter in one line: fetch · decode · execute, the loop from Chapter 13, running on both machines. So a "real CPU" isn't a fundamentally different animal. It's your animal, fed. Which means we owe ourselves a clean definition of the word we've been throwing around.
A CPU — a processor — is not a magic box. It is exactly two things wired together. First, a datapath: the registers, the ALU, and the bus, the parts that hold and move and crunch numbers. Second, a control unit: the ROM-driven brain from Chapter 15 that raises the right control lines in the right order. Datapath plus control, running fetch-decode-execute over a stored program — that's the entire definition. Anything that does it is a CPU. Take a real ARM block diagram and peel its labels back onto the blocks you already built.
Same skeleton. The ARM chip has a wider bus and a fancier control unit. But there's the program counter you built, the instruction register, the ALU, the register file, and the RAM interface — arranged exactly as you arranged them. You are not looking at something new. You are looking at your own machine with the volume turned up. So let's go turn the knobs, one at a time.
02More of everything
Start with the knob that changes the machine most deeply: word size, the number of bits the machine handles at once. It is not the biggest number in that table. Look back and you'll see it's the smallest row of the lot: 8 bits to 32 bits is a factor of 4, while the RAM and clock rows are where the "million-fold" actually comes from. Word size leads anyway, because every other row lives inside the ceiling it sets — what the machine can name, and how big a number it can hold. It's worth feeling precisely, because the whole point here is that these widths get conflated when they shouldn't be. Each wire you add doubles the number of distinct patterns. That's just 2ⁿ, the same doubling you met with the byte in Chapter 1. So a 4-bit address names 16 slots, an 8-bit value reaches 256, and a 32-bit word reaches about 4.29 billion. Drag the width up and watch the count explode.
Notice the three widths never had to match. SAP-1's address was 4 bits (16 cells), while its value was 8 bits (0–255), and a modern chip's are wider still. "How many things can I name" and "how big a number can I hold" are separate questions, answered by separate bundles of wires. And once numbers get that big, we need names for the piles of them.
A byte is 8 bits — you've known that since Chapter 1. Everything above it is just that byte, stacked. A kilobyte (KB) is about a thousand bytes, a megabyte (MB) about a thousand of those, and a gigabyte (GB) a thousand more. (The exact rung is 1024, not 1000, because memory is addressed in powers of two, and 2¹⁰ = 1024.) So "128 MB" isn't exotic. It's your byte, counted about 134 million times. Climb the scale from your toy's 16 bytes up to a phone.
The phone is not made of stranger stuff than your toy. It's made of more of the same stuff — the identical byte you built, laid out a hundred million times over. There's one more "more" that matters out of proportion to its size, and it's about registers.
Your SAP-1 had exactly one working register: the accumulator, A. So every intermediate value had to be shuttled back to slow RAM and fetched again. A real ARM chip has a register file: sixteen fast registers right next to the ALU. Why does that matter so much? Because every value you can keep in a register is a value you didn't have to go fetch from memory. More hands means fewer trips. Watch the same little computation run with one register, then with several.
One accumulator was a bottleneck: a machine forever putting things down to pick other things up. Sixteen registers let it keep a whole working set in hand at once. That's a real speedup, and it points straight at the next question: how fast is "fast," and what sets the ceiling?
03Faster — and the bill it comes with
Back in Chapter 12 the clock was a square wave from an oscillator, and you drove your toy by hand. A real chip's clock ticks on its own, and fast. Clock rate is measured in hertz (Hz), meaning ticks per second, and a chip runs at megahertz (millions) or gigahertz (billions). One tick pushes the machine through one micro-step, so more ticks per second means more work per second. Crank the clock from hand-speed up to hundreds of MHz and watch the instruction counter blur.
But here's the trap that clock rate sets, one marketing has fallen into for decades: a faster clock is not automatically a faster machine. Remember that one instruction on your toy took five ticks, not one — two to fetch, up to three to execute. That ratio has a name: CPI, cycles per instruction. Real throughput is clock rate ÷ CPI, so a chip with a slower clock but a lower CPI can flat-out win. Two numbers, not one.
Where does a CPI of five even come from? Not from the work. From the waiting. Take your toy's five ticks one at a time and ask which part of the machine is actually doing something. On the two fetch ticks, the program counter and the memory path are working and the ALU sits idle. On the execute ticks, the ALU is working and the program counter and the memory path sit idle. At every tick, about half your machine is asleep. That's the hole worth filling. But notice the price before we fill it. Two instructions can only be in flight at once if fetch and execute can move values at the same moment, and the shared bus you built carries one value at a time. So the shared bus has to go. A real chip pays for separate paths, and what it buys back is the right to keep both halves of the machine busy.
So the real game isn't just "tick faster." It's "get CPI down." And the trick that took real chips from a CPI of five toward a CPI near one is the most beautiful idea in this section: pipelining. Instead of finishing one instruction before starting the next, you overlap them. Think of a laundry line where, while one load is drying, the next is already washing and a third is being sorted. Fetch, decode, and execute become stations, and a different instruction sits in each. Run the pipeline and watch the throughput climb.
Every stage is busy every tick. So once the pipe is full, you finish roughly one instruction per cycle, even though each individual instruction still takes several. Push it further and run two pipelines side by side. That's superscalar, more than one instruction retired per tick. (One catch, to be honest about it: instructions that depend on each other cause stalls, and fighting those is most of what a modern chip's complexity is spent on.) So why not just pipeline harder and clock to the moon? Because of the bill.
Every tick, millions of transistors switch, and switching costs energy. The dynamic power a chip burns rises with the frequency. Worse, it rises with the voltage squared, and you need more voltage to switch faster. So doubling the clock can more than double the heat. The ceiling on clock rate isn't logic. It's thermal: the chip literally cooks. Push the clock slider up and watch the power meter climb toward the red.
That heat wall is the reason chips stopped just clocking faster around the mid-2000s and went multi-core instead — more machines at a sane clock, rather than one machine on fire. Speed, it turns out, is an engineering trade, not a free lunch. Now, all these instructions we keep clocking: where is the list of them written down, and who agrees on what they mean?
04The contract every chip signs
In Chapter 16 you learned that a byte means nothing until something decodes it. Meaning lives in the opcode table you read it against. Widen that idea to the whole machine and you get the single most important word in this chapter: the ISA, the instruction set architecture. The ISA is the exact list of instructions a chip understands, what their bit patterns are, and what each one does. And it is a contract. Above the line sits every app, every language, every compiler. Below it sits the silicon. Neither side needs to know the other's guts. They only need to agree on the contract in the middle. See both sides meet at the line.
Start with something small. Write c = a + b in Swift, and the silicon never sees that line. What reaches it is four fixed-size ARM instructions: load a into a register, load b into another, ADD them, store the result back into c. One line of yours, four boxes of the machine's own kind. The program that does that translating is a compiler. That's why all the app code on your phone "boils down to ARM." ARM is the ISA the silicon accepts, so a compiler's entire job is to translate your Swift or C++ down onto that fixed contract. The ISA is the seam that lets a billion programs run on one chip, and one program run on a billion chips. And ARM belongs to a particular style of contract worth naming.
ARM is RISC, a Reduced Instruction Set Computer. The word "reduced" trips people up. It's not about having fewer instructions. It's about keeping each one simple and uniform: fixed-size, do-one-thing, easy for the hardware to pipeline. The opposite camp is CISC (Complex Instruction Set — think classic x86), where single instructions can do elaborate multi-step work. Fewer-but-fancier against more-but-simpler. One heads-up before you look. The pipeline here has five stages, not the three you just ran. Nothing changed underneath: real pipelines split the work finer, and the two extra stages, MEM for the trip to memory and WB for writing the result back to a register, were always in there, hiding inside that single Execute box. Weigh the two against each other.
RISC's simple, regular instructions are exactly what makes deep pipelining and low CPI tractable. The two ideas fit together like a key in a lock. But there's a cost to fixed-size instructions: they can be wasteful of memory, since a tiny operation still eats a full 4-byte slot. ARM's answer is a clever hedge called Thumb.
Thumb lets the machine use variable-length instructions. The most common operations get a compact 16-bit (2-byte) form, while the rest stay 32-bit. You trade a little of RISC's perfect uniformity for a lot of code density: the whole program shrinks, which matters enormously on a small chip with little memory. Toggle between the fat uniform encoding and the packed Thumb one and watch the program's size drop.
MOV, ADD, CMP, BNE — collapse into a compact 2-byte form, while LDR's big offset still needs the full 32 bits. That's Thumb: a variable-length encoding (16 or 32 bits per instruction) that trades ARM's tidy uniformity for density — the same program, packed into 12 bytes instead of 20, fits in less memory.So even "RISC" is a set of deliberate trades, not a dogma: uniform where uniformity buys speed, packed where density buys space. Which brings us to a trade you've been quietly assuming since Chapter 8, one worth finally making explicit: why is a register so much faster than RAM?
05Why a register beats RAM
We keep saying "registers are fast, RAM is slow" as if it were a law of nature. It isn't. It's a consequence of how you built them. A register (Chapter 8) is a row of flip-flops sitting right beside the ALU, its output already wired straight in. Reading it is instant. Reading RAM (Chapter 9) means something slower. You drive an address onto the bus. It threads through the decoder that lights exactly one word line. That line gates the cell onto the bus, and the value travels back. That whole indirection is the latency. Race a register read against a RAM read and see where the time goes.
Distance is time, and indirection is distance. So chips face a hard bind. Registers are fast, but there are only a handful. RAM is huge, but slow. You can't have big and fast at once. The way out is not to pick one. It's to stack them.
This is the memory hierarchy, and it's a pyramid. At the tip sit a few registers: fastest and tiniest. Below them is cache: small, fast memory that keeps the hot bytes close and hides RAM's latency. Then comes main RAM: big and slower. At the base is flash storage: enormous and slow, but non-volatile (it survives power-off, unlike everything above it). Each layer trades speed for size. Climb the pyramid and read the access times.
Caching is the quiet hero of modern performance. But ask what it's betting on. Look at a loop you ran on SAP-1: the same handful of cells, hit again and again, while most of your 16 bytes sit untouched. That's not luck. Programs reuse the same bytes over and over — the name for it is locality — so keeping the recent ones one layer up hides most of RAM's slowness. Run a program with no reuse, one that touches every byte once and never returns, and the cache buys nothing. Cache is fast because of how programs behave, not because of what it's made of. Registers, cache, RAM, flash — every one of them is memory you already understand, just tuned to a different point on the speed-versus-size curve. Which leaves exactly one thing a real CPU can do that your toy cannot, and it's the rung we've been saving.
06The one rung your toy was missing: the stack
Here's a problem SAP-1 genuinely cannot solve, and feeling the wall is the point. Suppose you write a useful little routine — say, "multiply" — and you want to use it from two different places in your program. You can JMP to it easily enough. But when it finishes, where does it jump back to? The routine has no idea who called it. A plain jump goes in, but nothing remembers the way out. Try to call one routine from two callers and watch it get lost.
So try the cheapest thing that could possibly work. Before you jump in, drop the address you want to come back to — the instruction right after the jump — into one spare register. The routine ends by jumping to whatever that register holds. Call it from the top of your program and it comes back to the top. Call it from the bottom and it comes back to the bottom. Same routine, no edits, both callers home. That register has a name, the link register, and this is not a teaching toy. It is exactly what a real ARM does. Its BL instruction, branch-with-link, writes the return address into LR, one of the three registers we said already had a job. The routine returns by jumping back through it. The wall is gone. Now break it. Let the routine call a second routine. That call is another BL, and it writes its return address into the same LR, straight over yours. The inner routine gets home fine. The outer one has nothing left to come back to. One slot holds one way out, and the second call spends it.
So one slot is not enough. We need somewhere to save the return address that doesn't fall over the moment a routine calls another routine, which calls another. We need a scratchpad that hands things back in the reverse order it took them: the innermost way out first, the outermost way out last, because that is the order they stop being needed. That structure is a stack.
A stack is a region of RAM plus one extra register, the stack pointer (SP), that always points at the top. It has two operations. PUSH writes a value at the top and moves the SP to make room. POP reads the top value and moves the SP back. That's it. And it gives you last-in, first-out order for free: the most recent thing you pushed is the first thing you pop. Push a few numbers, then pop them, and watch the order reverse.
Now the thing that's easy to miss, and it is the real answer to the question this chapter opened with. Go back and try to write PUSH using the store you actually built. You can't. SAP-1's STA carries its address baked into the instruction: the cell it writes to was decided when you wrote the program, and it never changes. PUSH has to write to whichever cell the SP names right now. The address has to come from a register instead of from the instruction. Same story on the way home: your JMP takes its destination from the instruction, but a return has to take it from a cell in RAM. In the datapath, that's one change — let the SP drive the address bus in place of the instruction's address field — and it has a name: indirection, addressing memory through a pointer instead of by a number written down in advance. Your toy has no wire for it. The chip in your pocket does. A stack is only ordinary RAM plus a convention; the pointer is the hardware.
The stack pointer is the whole mechanism: one register that remembers where the top is, so PUSH and POP never have to think. Now stop before the figure and build the rest yourself. You own a store. You own a JMP. You've just watched a routine that couldn't find its way home, and you now have somewhere to put the way home. What two moves, in what order? Take a second on it. If you said save the way back, then jump, you have just designed CALL, and it is nothing new. It PUSHes the return address (the instruction right after the call) onto the stack, then JMPs to the routine. Its mirror, RET, undoes exactly that: it POPs that address back into the program counter. Two moves you already own — a store and a jump — stacked on the stack. Step a CALL and a RET beat by beat.
You didn't read that anywhere. You built it, out of parts already in your hands. That's why the sentence is yours to say and not mine: CALL and RET are not new hardware. They're the store from Chapter 13 and the JMP from Chapter 13, aimed at a stack instead of a named variable. Which is to say: aimed through the pointer. The return address is just a number saved in a cell, the same number-in-a-cell you've stored a hundred times. And the instant you have CALL and RET, something powerful comes along for free.
Because each CALL pushes its own return address, routines can call routines can call routines. That's nesting, and the stack unwinds in the exact reverse order it wound up. Push the same idea to its limit and a routine can even call itself. That's recursion, and it works only because every call gets its own private slot on the stack. Watch the call stack grow as routines nest, then unwind as each RET pops back. And notice the failure mode: nest too deep, run out of stack room, and you get a stack overflow — the most literal error name in all of computing.
ret →), the line to jump back to when it finishes. Push greet, then format, then pad: the stack grows downward, the stack pointer (SP) tracking the newest frame. Hit RET and the newest frame pops first — calls unwind in exact reverse order (that's LIFO: last in, first out). Flip to recursion and a routine calls itself; each self-call still gets its own slot — which is precisely why recursion works. Now drag the depth past 6: the SP runs off the bottom, past the hard stack limit, into memory it doesn't own. That's a literal stack overflow — you simply ran out of slots.That's the last rung. With a stack, your toy grows the one capability it was missing — subroutines, nesting, and recursion. And now it really is, feature for feature, the same idea as the chip in your pocket. So let's step back and look at the whole ladder you've climbed, from a single voltage-controlled switch all the way up to a chip you could buy — a Raspberry Pi Pico, a real Cortex-M0+, small enough to hold. You are here.
That's Chapter 17. We measured the gap between your toy and a real chip and found it was all scale — more bits, more RAM counted in MB and GB, more registers, a faster clock paid for in heat, a lower CPI bought with pipelining — sitting on top of the exact same fetch-decode-execute you built. We named the seam that makes it all portable, the ISA, the hardware/software contract, and saw why ARM's RISC style and its memory hierarchy are the shapes they are. And we built the one rung SAP-1 lacked: a stack, and CALL/RET on top of it, which turned out to be nothing but a store and a jump you already owned. So the startling claim from the start is now just… true: a real CPU is your CPU, scaled. But notice what that CPU has only ever done: move numbers and do math. It has never lit a pixel, felt a touch, or blinked a light. Reaching the physical world means learning to poke individual bits — set this one, clear that one, test a status bit — and whole-number arithmetic, where a carry ripples across everything, simply can't do that. For that we need a new, sharper toolkit: hexadecimal, the bitwise operations, and the shift. That's next.