◈ computers mapVol 1 · Ch 17/19
How Computers Work from the transistor up · chapter 17

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.

spec SAP-1 our 8-bit machine ARM chip RP2040 (Raspberry Pi Pico) scale word size 8-bit 32-bit registers 1 13 13× RAM 16 bytes 264 KB 16,896× clock speed 1 Hz 125 MHz 125M× fetch→decode→execute 3-step cycle 3-step cycle SAME
every row differs — some by millions×
← same 3-step engine underneath both — fetch, decode, execute never changed; only the numbers feeding it did.
Fig. 1. SAP-1 is the 8-bit computer we built from scratch earlier in this book; the RP2040 is the ARM Cortex-M0+ chip on a Raspberry Pi Pico — a real processor you can hold in your hand for a few dollars, and the one machine we'll measure against all chapter. Scan the table: its word is 4× wider, it has 13× the registers (SAP-1 has exactly one working register, the accumulator A), about 17 thousand times the RAM, and a clock 125 million times faster. Now click the punchline — every row that differs dims, and fetch → decode → execute lights up gold, because it's the one row that didn't change. That's why a real ARM chip isn't a different universe from the one you built: it runs the exact same three moves, just fed a lot more of everything.

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.

ARM (a real chip) SAP-1 (you built this) BUS BUS PC IR ALU REG FILE CONTROL UNIT fetch · decode · execute PC IR ALU ACC CONTROLLER fetch · decode · execute
ARM's PC = SAP-1's PC · same job
Every block on the left has a twin on the right doing the exact same job — a CPU is just datapath (these four) plus a control unit looping fetch→decode→execute. Nothing more. That's why 'CPU' is definable at all — and why you already built one.
Fig. 2. On the left, a real ARM core's block diagram, drawn the way Arm's own documentation lays it out. On the right, the SAP-1 you built over the last several chapters. Click any block on either side: a PC (program counter) points at both — the address of the next instruction to fetch. An IR (instruction register) holds both — the instruction currently being decoded. An ALU (arithmetic logic unit) does the adding and comparing on both. And Arm's register file (a whole shelf of scratch registers, R0–R12) is just SAP-1's single accumulator, multiplied — more scratch space, same idea. Every block sits on a bus (the shared wire that carries data between blocks, one value at a time), and every block is driven by a control unit stepping the same loop you wired by hand: fetch the instruction, decode what it means, execute it. Strip away Arm's marketing name, its clock speed, its billions of transistors — what's left is datapath plus control doing fetch-decode-execute. That's the whole definition. Nothing else qualifies as a CPU, and nothing more is required to be one — which is exactly what you 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.

n wires (the bit-width) — added one at a time 8 bits wide reachable patterns = 2ⁿ — shown on a log scale, so it stays on-screen 4 →16 8 →256 32 →4.29B 256 exactly 256 drawn at 1 pixel per pattern: already off the chart →
256 patterns — exactly one byte
this dial only counts how many patterns a width can name — whether those patterns point at memory (an address) or hold a number (a value) is a separate design question, not this one.
Fig. 3. Drag bit-width and watch the row of wires grow by exactly one tick at a time — the most boring possible motion. Now look at the readout underneath: it doesn't add, it doubles — 2ⁿ patterns for n wires, so 16 at 4 bits, 256 at 8 (the byte), 4.29 billion at 32 (a modern register). The bottom bar tries to draw that count at one pixel per pattern and runs out of chart before you even reach ten wires — which is exactly why the dial above it is log-scaled: it's the only way to keep an exponential in view. Note what this dial does not decide — how many patterns exist says nothing about whether you're naming memory addresses or numeric values; that's a separate question, asked next.

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.

BYTE = 2⁰ bytes = 1 ×1024 ×1024 ×1024 BYTE KB MB GB phone · 128 MB your toy · 16 B each dot = one more ×2 doubling (2ⁿ)
rung zero — your toy starts here
128 MB = the same byte, counted ~134,217,728 times — not a stranger thing, just a taller stack of the one thing you already built.
Fig. 4. Climb the byte's ×1024 ladder — BYTE → KB → MB → GB — and watch the doubling line show why your toy's 16 bytes and a phone's 128 MB are the same byte, counted a very different number of times.

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.

RAM · slow A 3 B 5 C 2 D 4 spill result the bus CPU ACC · one seat click Step to begin RAM trips this run: 0 best — 1-reg:— 16-reg:—
Pick a mode, then click Step to run the same sum, (A+B)+(C+D), one micro-op at a time.
ready — 1 accumulator
← that's why more registers make a machine faster — more hands means fewer trips to slow memory.
Fig. 5. Run the same small sum, (A+B)+(C+D), two ways. Click Step with 1 accumulator selected: every fetch lands in the same lone seat, so the moment two partial sums are alive at once, one gets spilled to RAM and reloaded — 7 round trips for four numbers. Switch to 16 registers and step again: each value gets its own seat, the adds happen register-to-register inside the CPU, and only the 4 inputs plus 1 answer ever touch RAM — 5 trips. Same arithmetic, same RAM — the only thing that changed is how many values the CPU could hold at once, and that's why more registers make a machine faster.

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.

hand kHz MHz 200M Pico clock rate: 1.0 Hz drag the gold knob tick indicator 1/sec ticks / sec — micro-steps
Drag the gold knob (or click the dial face) to sweep the clock rate from a hand's pace up past kHz into hundreds of MHz. It clicks into a detent at 125 MHz — the chip on a Raspberry Pi Pico.
hand-speed - you could count each tick
a tick is one micro-step. 1 MHz = a million of them a second — that's why a chip laps any hand on a panel.
Fig. 6. Drag the gold knob from hand-speed through kHz up to 200 MHz, and feel it click into the detent at 125 MHz — the clock of the chip on a Raspberry Pi Pico, the machine this chapter measures against. Each lap of the little tick-indicator hand is one tick — watch it go from a lazy sweep you could clock with a stopwatch to a spin so fast the lamp just glows steady, because your eye can no longer resolve individual ticks. The ticks/sec reading is the same number said the other way round, blurring as it climbs: one tick is one micro-step — not one instruction. How many ticks an instruction costs, and what that does to the real work per second, is the next figure's question.

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.

throughput (GIPS) = clock (GHz) ÷ CPI VS CHIP A 4.0 GHz CPI 6 ? CHIP B 2.2 GHz CPI 1 ? SAP-1 needed 5 cycles per instruction · CPI = 5
predict, then click a chip below
← that's why clock rate alone lies: speed is two numbers, clock and CPI, divided — never one number by itself.
Fig. 7. Two rival chips, four sliders: each chip's clock (cycles ticking per second) and its CPI (cycles the chip actually burns per instruction — SAP-1 needed 5). Predict which chip is faster, then click it — the bars reveal the real number, throughput = clock ÷ CPI, in billions of instructions per second (GIPS). Push Chip A's clock high but leave its CPI worse, and a chip ticking barely half as fast can still finish more instructions every second. That's why clock rate alone lies — speed is two numbers, clock and CPI, divided, never one number alone.

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.

TICKS 0 RETIRED 0 / 6 press Tick to run the instructions
Fetch Decode Execute stall
~1 retires per tick once full
Fig. 8. Each instruction slides diagonally through Fetch → Decode → Execute. One instruction takes 3 ticks to cross (its latency) — but they overlap, so once the pipeline is full a fresh one finishes every tick (its throughput). Tick to watch RETIRED climb by exactly 1 each cycle; add a 2nd pipeline to retire 2 at once; add a dependency and watch a bubble cost a whole tick. That overlap is why real chips hit ~1 cycle per instruction.

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.

⚠ THROTTLE 1 hot core — full clock 250W 200 150 100 50 0 thermal ceiling CLOCK (demanded) 1.0 GHz VOLTAGE needed 0.70 V TOTAL PACKAGE POWER 12 W 1 core doing all the work
12W — cool, plenty of headroom
power ∝ voltage² × clock — and voltage itself has to climb as clock climbs, so power runs away faster than the clock does. that runaway is the wall.
Fig. 9. Push the clock slider up: power doesn't climb in a straight line — it climbs like voltage² × clock, and the chip needs more voltage just to hold the higher clock, so the curve runs away and slams into the red thermal ceiling. That ceiling — not the logic, not the transistors — is what stops the clock from just going up forever. Flip to go multi-core: the same total work, spread across four slower, cooler cores, never gets near the wall — which is exactly why real CPUs stopped chasing GHz and started stacking cores.

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.

SOFTWARE STACK PH Photos app in Swift compiled by the Swift compiler lowers to ARM ARM ISA — the contract runs as ARM code SILICON M3 Apple M3 in a MacBook
ISA = Instruction Set Architecture, the one vocabulary every chip must understand. Tap or drag either card ↔.
Photos app + Apple M3 → same ISA
that’s why every app boils down to ARM — the ISA is the only language the silicon accepts
Fig. 10. Tap or drag either card — the app and language up top, the real silicon down below — and watch the gold line in the middle never move. Swap the app, swap the compiler, swap the manufacturer's chip: none of it touches the line, because both sides only ever meet at one shared seam, the ARM instruction set. That's why every app boils down to ARM — it's the only language the silicon accepts, and a compiler's whole job is lowering code onto exactly that seam.

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.

same task: put a value from memory into R0 LOAD R1, [MEM] ADD R0, R0, R1 op rd rs off op rd rs1 rs2 2 instructions · 32 bits each, fixed cycle → 1 2 3 4 5 6 7 8 LOAD ADD IF ID EX MEM WB IF ID stall EX MEM WB both done by cycle 7 — one load-use bubble same job, two philosophies — many uniform steps, or one complex step
IF fetch · ID decode · EX execute · MEM mem-access · WB write-back
RISC
CISC
instr count
2
1
encoding
32-bit ×2, fixed
variable, not fixed
pipeline
clean; 1 load-use bubble
stalls the pipe
RISC: 2 ops, fixed size, 1 bubble
Uniform, fixed-size ops slot cleanly into every pipeline stage — that's why ARM is RISC.
Fig. 11. The exact same job — get a value from memory into R0 — encoded two ways. RISC (Reduced Instruction Set Computer) does it as two instructions, LOAD then ADD, and — look closely — both boxes are the identical shape: same field layout, same 32-bit size, every time. That uniformity is the whole trick: each opcode (the bits naming the operation) drops into a fixed slot, so the pipeline — the assembly line that overlaps IF (fetch) → ID (decode) → EX (execute) → MEMWB (write-back) across instructions — never has to guess how big the next instruction is or how long it will take. CISC (Complex Instruction Set Computer) does the same job as one instruction, but that one instruction is fat: variable length, and its EX stage hides several serial micro-steps (compute the address, read memory, add, done) that can't be split across pipeline stages — so the next instruction sits blocked. Toggle it and watch cycle 8 arrive before CISC's second instruction even starts. "Reduced" was never about having fewer instructions — CISC has far fewer here. It's about every instruction being simple and the same shape, which is exactly the property deep pipelining needs. That's why ARM is RISC.

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.

ENCODING: ARM · 32-bit, uniform INSTRUCTION ENCODED WIDTH → every op fixed at 4 B MOV r0,#1 4 B ADD r1,r0,#2 4 B CMP r1,#10 4 B BNE loop 4 B LDR r2,[r3,#400] 4 B ✎ the aha pack the common ops — the whole program shrinks PROGRAM SIZE (bytes) ARM: 20 B program size: 20 bytes
uniform: every op fixed at 4 B
toggle the encoding — common ops collapse to 2 bytes each in Thumb, and the whole program's footprint shrinks with them. One op stays 4 bytes even in Thumb — that's the trade.
same scale used above and in the bar: 20px per byte
Fig. 12. Every ARM instruction is exactly 4 bytes — perfectly uniform, so the machine always knows the next instruction starts 4 bytes later. Click Thumb and watch the common ops — 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.

same ALU, same speed — different distance REG REG: 0/1 hop ALU ADDR DECODER word line CELL data bus RAM: 0/5 hops
click to race REG vs RAM into the ALU
aha — both signals move at the same speed. The register is wired beside the ALU, so there's almost no path to travel. RAM's path is a real trip: decoder → word line → cell → bus. Indirection is distance, and distance is time.
Fig. 13. Click race a read. The REG dot only has to cross the short wire already sitting beside the ALU — it lands almost the instant you click, one hop. The RAM dot leaves at the exact same speed, but its address has to reach a decoder (which picks exactly one word line), ride that line out to gate open one cell, then cross a data bus back to the same ALU — five hops instead of one. Nothing about RAM is intrinsically slow; it's just farther away, through more stages, and distance at a fixed speed is time. That's the whole reason a register read is "free" and a RAM read costs real cycles.

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.

the memory of a desktop-class chip reg cache RAM flash CPU climb to reveal — pick a tier — size: — speed: — · · · RAM touches: 0 cache hits: 0
climb the pyramid (tap a tier)
click a tier, or access a hot byte
aha: reuse the same byte and the request never goes past cache — RAM only gets touched on a miss.
Fig. 14. Four tiers, one trade — this is the memory of a desktop-class chip: click reg, cache, RAM, or flash to climb the pyramid — each rung is bigger and slower than the one above it, by anywhere from ten to several thousand times per step, and flash alone keeps its bits with the power off. Then hit access hot byte → repeatedly: the first request is a MISS that pays the full ~80 ns trip down to RAM, but every request after that is a HIT served in ~3 ns straight from cacheRAM never hears about it again. Hit ✕ evict cache and the next access misses once more. That's why chips don't pick one memory — they stack a hierarchy, and a cache that remembers your hot bytes buys back RAM's speed without giving up its size.

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.

CALL SITE A 04 JMP 0A 05 HLT CALL SITE B 08 JMP 0A 09 ADD 3 SHARED ROUTINE 0A OUT 0B JMP 05 PROGRAM COUNTER 04 RETURN ADDRESS no such register on SAP‑1
choose a caller + a return, then step →
Site A and Site B both jump into the same routine with a plain JMP. When the routine ends it has to jump back out — but back to where?
the routine's last line is fixed: JMP <addr> — one number, baked in before the program ever runs. Toggle it and watch which caller it betrays.
Fig. 15. Call sites 04 and 08 both jump into the same shared routine at 0A with a plain JMP — that part's easy, since JMP just loads a new address into the PC (program counter). The trouble is the routine's last line, JMP <addr>: that address is a single number baked into the instruction before the program ever runs, because SAP-1 has no register that recorded which caller jumped in. Toggle the return address and step through it — you aren't fixing the routine, you're only choosing which caller it abandons. A jump goes in; nothing remembers the way out.

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 (RAM) 0 1 2 3 limit SP SP = the stack pointer, one register push these, in order → 7 3 9 5 they pop back out → predict: what order will they pop?
sequence to push, in order: 7, 3, 9, 5
stack empty — SP sits above the top
PUSH all four, then POP them — watch the order flip.
Fig. 16. A stack is nothing but ordinary RAM plus one register, the stack pointer (SP), marking the top. It grows downward — from high addresses toward low. Hit PUSH and SP steps down one cell to point at the next free slot, where a value from the queue lands; hit POP and the top cell's value comes back out while SP steps back up. Push all four — 7, 3, 9, 5 — then pop all four: they return 5, 9, 3, 7, the exact reverse. That's last-in-first-out, and it costs nothing extra — one pointer, moved by one instruction each way, is the entire mechanism.

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.

Ready — PC sits on the CALL PROGRAM · in memory 0x40CALL sub 0x41next instr ← return addr 0x80sub: work… 0x81RET PC 0x40 SP empty this beat re-uses press Step to run the CALL STACK 0x41 SP CALL = STORE ▸ JMP RET  = LOAD ▸ JMP gold = the return address
CALL is next — press Step
Watch the reuses box on every beat: CALL and RET never invent a new move — each one is a store or a jump you already built, aimed at the stack.
Fig. 17. CALL and RET, taken apart. Step through and read the reuses box on every beat: a CALL is a STORE (push the return address 0x41 to the stack) then a JMP (write the routine's address into the PC); a RET is a LOAD (pop that address back) then a JMP (write it into the PC). No new instruction is invented — it's Chapter 13's store and jump, aimed at the stack.

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.

CALL STACK grows down ↓ SP main() the program ▲ stack limit ▲ STACK POINTER SP → main() FRAMES USED 0 / 6 NEXT ‘RET’ JUMPS BACK TO — stack empty — AHA — there is no slot 7. Running out of slots IS the overflow.
Stack empty. main() is running.
Predict first: push 8 calls into a 6-slot stack — what happens at call 7?
Fig. 18. Every CALL shoves a new frame onto the stack — and tucked inside it is that call's own return address (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.

1 switch 2 gate 3 adder 4 register 5 RAM 6 bus 7 CPU 8 program 9 chip
a real chip — you are here
it's all of this, shrunk onto one chip
nine rungs, one idea repeated: a real chip is every earlier rung, just built small enough to hold.
Fig. 19. The whole climb, one picture: nine rungs from a bare switch to a real chip — click any rung to recall its keystone idea. The last rung stays lit you are here: a Raspberry Pi Pico, a real dual-core Cortex-M0+ chip small enough to hold, built from nothing but the rungs below it. That's why you can now stand anywhere on this ladder — it's one unbroken climb from a single switch to the chip in your hand.

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.

iolinked.com
Written by Ajai Raj