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

19Talking to the World

Last chapter we built the surgeon's kit: a shift to build a mask, an OR to set a bit, an AND to clear or test one. That's the whole toolkit for touching one bit and leaving the other 31 alone. But I ended on a question, and this whole chapter is the answer. Every one of those operations happened inside the CPU, rearranging a register's own bits. So here's what should nag at you: the processor has no "turn on the light" instruction. There is no opcode for internet, none for touch, none for sound. It can add, it can move a number, it can jump. That's the whole vocabulary. So how does a machine that only ever shuffles numbers reach out and make an LED physically glow? The answer is a beautiful lie the memory system tells. A few special addresses aren't wired to RAM cells at all. They're wired straight to hardware. Store a number there and nothing gets saved. Instead a real switch flips, a pin drives, a light comes on. That trick is called memory-mapped I/O. It means the load and store you built chapters ago are already, secretly, the way a computer talks to the world. We'll follow one wire the whole way. We wake the hardware out of reset, aim a single pin, and drive it high with the exact 1 << n mask from last chapter. Then we blink an LED under a program you wrote. Then we stand at the bottom of the ladder and look all the way up.

01Some addresses aren't memory

Let's stand exactly where Chapter 18 left us. You can now reach into a register and set, clear, or test any single bit cleanly, with no carry and no collateral damage. But every one of those moves just rearranged bits inside the chip. To light an LED, that carefully-set bit has to escape the processor and reach a wire in the room. And the CPU's only tool for putting a number somewhere is the one you built back in Chapter 9: store, which writes a value to an address. Every store you've ever run landed in a RAM cell. So here is the whole trick of this chapter in one move: what if a few addresses aren't RAM? Pick an address below and store a number to it. Watch where it actually goes.

ADDRESS BUS — drag to pick an address (they all look like memory) → 0xF3 ADDRESS DECODER routes by address alone RAM (memory cells) empty LED MOTOR This address is NOT memory. One decoder line enables a device wire.
Predict, then hit STORE
Two of these addresses are wired to devices, not RAM. Can you find them?
Fig. 1. The MMIO lie. Every slot on the address bus looks like plain memory — just a number. Drag to pick one, guess whether a store lands in RAM or trips a device, then STORE. Most addresses save your byte in a memory cell. But a few are a trick: the address decoder raises a single output line that’s wired straight to a device, so the same store instruction turns on an LED or spins a motor instead of saving anything. That’s memory-mapped I/O — the memory system telling a lie.

That's memory-mapped I/O, and it's the keystone of everything that follows: a store to one of those addresses doesn't save anything. It does something. And look at what the CPU needed to pull that off: no new instruction. The same store, pointed at a different address, is how it drives the entire outside world. Now, the tempting way to file this is as an exception: a thin band of special addresses wired to devices, and ordinary RAM everywhere else. RAM behaves exactly as you expect — write a number and it's saved, read it back and there it is. Hold that frame loosely, because I think it's backwards. So here's the question I want you to answer before I do. How does the machine know which addresses are memory and which are hardware? Or, sharper: what would the CPU have to do differently to store to a device instead of to a RAM cell?

You can't, and neither can it. Watch a store from the CPU's own side and count what leaves. The address lines go out, carrying the number you named. The data lines go out, carrying the byte. One write-enable line goes high to say now. Three things out — and then nothing comes back. No confirmation, no receipt, no identity. A store is fire-and-forget. So go hunting inside the processor for the wire that would carry the difference between "that landed in a RAM cell" and "that just spun a motor". There isn't one. There's nothing to build it out of, because the CPU never finds out what answered. It isn't that the CPU tolerates devices. It's that the CPU is blind, and always was. Whatever sits on the far end of those wires is somebody else's decision, made in copper, outside the processor — by a router you already built.

Recall the address decoder from Chapter 9. It takes an address and raises exactly one output line, the one-hot line that selected a single RAM word. Then it stops. The decoder has no opinion about what sits on the far end of the line it raised. So ask what's out there. In Chapter 9 the answer was a RAM row every time — but that was a fact about what we had soldered on, not about what an address means. Wire a device's enable pin to one of those lines instead, and the decoder behaves identically, because as far as it can tell nothing changed. Same decoder, same one-hot rule, another customer on the far end of the wire. RAM was never the default. RAM is just the customer we wired up first.

ADDRESS BITS WHERE DOES THE HOT LINE GO? DECODER recall Ch.9 · one-hot RAM ARRAY idle — no word-line high PERIPHERAL EN EN = 0 — asleep
Click a bit to set the address — same decoder as Ch.9, still exactly one hot line. Now toggle where that line actually runs.
addr 101₂ = 5  →  line 5 is hot
line 5 hot → wired to RAM row 5
Fig. 2. The exact decoder from Chapter 9, unchanged: click a bit to set the address, and one gold line goes hot — still one-hot, still just AND gates. But now follow that single wire past the decoder's edge. Flick the switch and the same hot line lands on a RAM row's word-line, or on a peripheral's EN pin — nothing rewired inside the decoder, just a different customer soldered to the far end. That's why MMIO needed no new instruction: same store, same decoder, one wire pointed somewhere new.

I told you the CPU is blind, and that claim is doing a lot of work, so don't take it from me. Below is every wire a store drives, drawn from the CPU's own side of the chip edge. Probe each of them and count what leaves: the address, the data, one write-enable. Then hunt for the wire that could carry the answer back — the one that would whisper "that was RAM" or "that was a motor". Take your time. There is a prize for finding it.

CPU the whole package mid-STORE A0–A15 · address ×16 OUT → D0–D7 · data ×8 OUT → WE · write-enable ×1 OUT → a wire back, saying what answered…? × no such conductor exists behind: RAM ? store #1 · — ADDR DATA WE 0xF3C0 0x01 store #2 · — ADDR DATA WE 0xF3C0 0x01 DIFF
leaves the chip: —
click each wire group in the drawing — which way does it point?
captured: — · —
probe the wires crossing the edge
predict: does any wire point back IN?
Fig. 3. A store, watched from the CPU’s side of the package edge. Act 1: probe each wire group crossing the edge — address, data, write-enable — and the tally climbs to leaves the chip: 1 · 2 · 3; then press the gold button and sweep the whole package for the wire that would report back. The verdict is the chapter’s deepest fact made physical: return wires found: 0 — a store is fire-and-forget. Act 2 clinches it: hide RAM or a MOTOR behind the curtain, press STORE against each, and the widget stacks the two CPU-side cycles and computes the DIFF row — difference visible to the CPU: none. The processor could not be told what answered even if its designers had wanted it; there is no conductor to carry the news. So “which addresses are devices” is forced to live outside the CPU, in the decoder’s copper — and that is exactly why memory-mapped I/O needs no new instruction.

So nothing was invented. A wire was rerouted. The decoder still lights one line for one address; that line just happens to wake a peripheral instead of a memory cell. This leads to a strange and useful tell. If a device address has no memory cell behind it, then writing a number there and reading it back should not hand your number back. Try it. Write to a plain RAM address, then to a device address, and read each one.

value = 137 RAM · 0x2000 a real memory cell not written yet STORAGE DEVICE · 0xF000 printer status register not written yet SIDE EFFECT
pick a value, then WRITE it
predict: will 0xF000 hand your number back too?
Fig. 4. Same move, two addresses: pick a number, WRITE it to RAM at 0x2000 and to a DEVICE register at 0xF000, then READ BACK and compare. RAM hands back the exact number every time — there's a real cell sitting behind that address, holding it. The device address hands back a fixed status flag, no matter what you wrote — because there's no memory cell behind 0xF000; the write triggered a side effect (telling hardware to do something) instead of saving anything. That's the tell: an address that never echoes what you stored isn't storage — it's a command.

That experiment hands you a tempting shortcut: if it echoes, it's memory; if it doesn't, it's a device. Pocket that rule and real hardware will embarrass you within a week. Below are three addresses with their name-plates removed: probe each with a write, a read-back, and one eye on the lamp. Then sort them into storage and device — and find out which test actually survives.

byte = 0x89 · 137 card A addr = ? W — R — unprobed unfiled card B addr = ? W — R — unprobed unfiled card C addr = ? W — R — unprobed unfiled lamp · any pin
file each card
A
B
C
WRITE, READ BACK, then file A B C
one of these three addresses is wired to the lamp. echo won’t tell you which — probe, then file each card.
Fig. 5. Three unlabeled addresses, one lamp — and the lamp’s wire disappears into a shared rail, so only probing can tell you which card feeds it. Pick a byte, then per card press WRITE → and READ BACK → and watch the logs — the three cards do not behave alike, and the differences are the whole game. Then file each card — storage or device — and the verdict lands only after all three are placed; if a filing goes wrong, the reveal replays the evidence. “Doesn’t echo” was never the rule — behind 0x4000_0004 sits a perfectly honest register whose Q simply runs out to copper. The tell that survives sorting all three: did the write do something beyond remembering?

There's the tell. A write to RAM is storage: it faithfully remembers. A write to a device is a side effect: it makes something happen. Read that address back and you get the device's status rather than your number, because behind that one there's no cell to remember it — only wires. Now, careful: "device addresses never echo" is not the rule. Plenty of device addresses have a real storage cell behind them, hand your number straight back, and act on the world anyway. You just probed one: the mystery card that echoed your byte faithfully and lit the lamp anyway. The rule that survives both kinds is plainer. A write to a device does something beyond remembering — whether it also remembers is incidental. Now let's open one of these devices up and see what's actually inside.

02A peripheral is a bank of registers

A peripheral — the pins, a timer, a radio, a USB controller — sounds exotic. From the CPU's side it is almost boring, and the boredom is the design. Every peripheral is just a small block of registers, each pinned to its own address, laid out at a base address plus a fixed offset. The radio gets no private wires into the processor, and neither does anything else. To use a device you read and write its numbered registers, nothing more. Here's the register map of a GPIO block: a base, and a handful of offsets, each with one job.

So what is one of those registers, physically? Nothing you haven't already built. You have a register from earlier chapters: D inputs to feed it, a load enable to say take it now, Q outputs to read it back. Wire it up. The decoder's hot line for this address goes to the load enable. The data bus goes to the D inputs. That much is exactly how a RAM word works, and a store lands in the cells the same way it always did. Now here's the only change in the whole chapter. The Q outputs don't loop back to the bus. They run out to the pins. That's it. Nothing new is on the page — one wire has a different destination.

And right there a word quietly split in two. Through Chapter 18, a register meant a CPU register: a thing with a name, sitting in the datapath, that an instruction reaches directly. GPIO_OUT is not that animal. It's a device register: a thing with an address, sitting past the decoder, that only load and store can reach. Same word, opposite access. So if you've been wondering why we store to GPIO_OUT instead of just moving a value into it, that's the answer — there is no name to move it to. It lives out on the bus. That's also why driver code looks the way it does: a stream of loads and stores, hardly any arithmetic. Every touch of a peripheral is a trip down the address lines and back.

GPIOA · PERIPH BASE 0x4000_0000 base + offset +0x00 +0x00 DIR pin: in or out +0x04 OUT output level +0x08 SET force HIGH +0x0C CLR force LOW +0x10 IN sense pins +0x14 RESET_DONE ready flag one base address, six numbered slots — that's the whole peripheral
DIR 0x4000_0000
sets each pin: input or output
base + offset = address. That's why every peripheral looks like a patch of memory addresses: it's just a bank of registers, and driving hardware is nothing more than reading and writing them.
Fig. 6. A peripheral is just a base address plus a handful of offsets — click any row in the GPIOA register map and watch the offset add onto the base to give one exact address, with its job named beside it. SET and CLR aren't decoration: writing a 1 bit there forces that one pin HIGH or LOW without ever touching the others, so an interrupt can't race you the way it could if every pin lived in one shared OUT word. That's the whole trick behind "talking to hardware" — it's reading and writing numbered slots, nothing more.

That's the shape of every driver ever written: a base + offset table of registers, and a sequence of reads and writes to them. The one that reaches the world is the output register, and its wiring is the whole reason any of this works. Each bit of that register is soldered to one physical pin. Bit k of GPIO_OUT is pin k. Flip the bit, drive the wire. One promise before you play with it: what you're about to see is this block doing its job once it is awake. Getting it awake turns out to be a job of its own, and it's the next section's. Toggle bits below and watch the pins follow.

inside the chip — GPIO_OUT[7:0], the low 8 bits of the 32-bit register outside the chip — the physical header MSB · bit 7 LSB · bit 0 0 L P7 0 L P6 1 H P5 0 L P4 1 H P3 0 L P2 0 L P1 1 H P0 GPIO_OUT[7:0] = 00101001
pins HIGH: 0,3,5
click any cell OR pin — HIGH drives ~3.3V, LOW sits at 0V (GND)
aha: the wire isn't copying the bit — it is the bit. One cell, two names: bit k, pin k.
Fig. 7. The low eight bits of the 32-bit GPIO_OUT register — GPIO_OUT[7:0] — and the eight numbered pins they drive, written the way a number is written: bit 7 on the left, bit 0 on the right. Click a register cell, a pin pad, or hit "walk" — bit k and pin k switch in the same instant, because they aren't two things kept in sync; they're one storage cell with two names. Flip the bit, drive the wire.

So the register is the row of pins, mirrored into the address space. Set a bit and its pin goes high; clear it and the pin goes low. This is where last chapter's mask work stops being an exercise and starts driving copper. To light exactly one LED, you need to set exactly one bit of this register and leave the rest untouched. That is precisely what 1 << n and OR were for. But we can't drive the pin yet. Fresh out of power-on, the hardware is asleep.

03Waking the hardware: reset and the poll

Here's a detail almost every beginner trips on: when the chip first powers up, its peripherals aren't merely idle. They are held in reset: clamped in a known-dead state, unclocked, deaf to every register write you send. Write to GPIO_OUT right now and nothing happens, because the GPIO block isn't even awake to hear it. Your first job is to release its reset. Watch a peripheral come out of reset.

GPIO PERIPHERAL ASLEEP · held in reset CLK WR reset gate blocked ✕ GPIO_OUT[7:0] 0 0 0 0 0 0 0 0 GPIO_OUT[7:0] = 0x00 pin outside fresh silicon boots held in reset — unclocked, deaf to writes. the gate only opens once you release reset — then it latches. that's why brand-new hardware ignores you at power-on.
held in reset — writes ignored
a peripheral's registers are deaf until its reset line is released.
Fig. 8. Every peripheral wakes up held in reset: unclocked, deaf, its write gate shut. The eight cells here are the low 8 bits of GPIO_OUT, GPIO_OUT[7:0] — bit 7 on the left down to bit 0 on the right. Click write 0xFF now and the gate blocks it — GPIO_OUT[7:0] stays 0x00, nothing latches. Release reset and the gate swings open, the clock starts ticking; write again and it takes: 0xFF lands, the pin lights. Hold reset again and the register itself clears — that's why fresh hardware ignores every write until you let it out of reset first.

Releasing reset isn't instant, though. The hardware needs a few cycles to power up its internals and steady itself, and if you poke it too soon it's still not listening. So how does a program wait for a physical thing to become ready? It does the most literal thing imaginable: it polls. There's a RESET_DONE status bit that the hardware flips to 1 when it's truly awake. The CPU sits in a tight loop AND-testing that one bit, the exact test recipe from Chapter 18, spinning until it reads 1. Step through the spin.

poll STATUS until RESET_DONE = 1 busy-wait loop: read STATUS → AND with mask → loop while 0 b3 b2 b1 b0 STATUS & MASK (constant) 0 0 0 1 = OUT RESET_DONE (ignored) test: (STATUS & (1≪0)) ≠ 0 beat 00 spins 00 OUT = ? loop while OUT = 0 spin — reread & test again OUT≠0 → break CPU continues →
press Step ▸ to poll STATUS
each beat: the CPU rereads STATUS, ANDs it with the RESET_DONE mask, and loops back while the result is 0.
Fig. 9. The exact test from Ch18 — STATUS & (1<<0) — run over and over. Press Step ▸: each beat rereads the STATUS register (its other bits jitter with unrelated flags — the mask forces them all to 0, so only b0 ever survives the AND), tests whether the result is 0, and loops if it is. Nothing else happens on the CPU while this runs — no other instruction executes — which is exactly why it's called busy-wait polling. The moment RESET_DONE flips to 1, the loop breaks and the CPU moves on.

That's busy-wait polling: the processor does nothing else. Every cycle, it re-reads the status bit and asks "ready yet?" until the answer is yes. For a one-time boot check, that's a fine bargain. But look at what it costs. While the loop spins, every ounce of compute is burned staring at a single wire. When the thing you're waiting for is rare or slow — a key press, a packet, a byte from a sensor — spinning is a terrible deal. So the hardware offers a smarter arrangement: the interrupt. Instead of the CPU asking, the device tells.

POLL — CPU stuck re-reading a bit device ready CPU cycles, left → right polling (wasted) real work the tap (interrupt) wasted 8 · useful 8 of 16
poll burns 8 cycles asking
That's why interrupts exist — the hardware taps the CPU's shoulder only when there's real work, so it never has to keep asking.
Fig. 10. Sixteen CPU cycles, one strip. In poll mode the CPU spends every cycle before the device is ready just re-reading the same bit — orange ? cells, cycles it will never get back. Switch to interrupt mode and those same early cycles turn green: the CPU is off doing real work, and only gets a one-cycle gold tap the instant the device is actually ready. Drag the slider to move when the device becomes ready — the later it is, the more cycles polling throws away, while interrupt's cost stays pinned at one quick tap.

The strip shows the interrupt's price, one gold tap, but not its mechanics — and the mechanics sound impossible. The CPU is mid-program, deep in real work, when the device raises its line. Somehow it must drop everything, run code it has never fetched, then continue as if nothing happened. Step the moment yourself: watch where the machine stashes its place, and what brings it back.

an interrupt = save · jump · run · restore the current instruction always finishes first — the tap is taken at the boundary MAIN 0x20LOAD R1,[X] 0x21ADD R1,#1 0x22STORE [X],R1 0x23LOAD R2,[Y] 0x24STORE [Y],R2 HANDLER 0x80LOAD R7,[DEV] 0x81STORE [BUF],R7 0x82RET ③ the handler runs to RET device ready ② jump: PC ← registered addr PC 0x20 SAVED-PC handler addr · registered 0x80 ① save ④ restore 0x20
predict — which MAIN line runs next?
press Step ▸ — or ⚡ at any time
Step walks MAIN. The red button is the outside world — press it whenever you like, then keep stepping and watch the three registers at the bottom.
Fig. 11. The sentence the prose compresses, staged one move at a time. Step ▸ walks MAIN (0x200x24); press ⚡ device ready whenever you like — the tap is latched and taken at the next instruction boundary. Then watch the registers: ① the PC is copied into the SAVED-PC slot, ② the PC is loaded with 0x80 from the registered handler addr cell — parked there before the tap ever came — ③ the handler runs to RET, and at the gate you must predict which MAIN line runs next before ④ the restore lands. The reveal: PC ← SAVED-PC, the slot clears, and MAIN continues from the exact line it was about to run — unharmed and unaware. An interrupt is one register save and two jumps, nothing more.

That's the whole idea of an interrupt. The hardware taps the CPU on the shoulder. The processor saves its place, jumps to a little handler you registered, deals with the event, and resumes. That way it can do real work instead of spinning: polling asks a thousand times; an interrupt answers once, exactly when it matters. We'll blink with a simple poll-free loop, but now you know the door the real world knocks on. With the block awake, let's aim our pin.

04Aiming the pin: function and direction

A physical pin is expensive real estate, so chip designers make each one do many jobs. The same pin might be a UART transmit line, a PWM output, or plain I/O. Only one function can own the wire at a time, and something has to choose. You've met that something already: it's the multiplexer from Chapter 3. There's a function-select mux behind every pin, and its select bits decide which internal block gets the copper. Pick a function and watch the mux route it out.

chip edge UART TX PWM GPIO OUT MUX 0 0 select bits S1 S0 PIN PA9 UART voltage on that one wire 1 0
S1S0=00 → UART drives the pin
Same pin, same wire, same solder pad. The MUX (Ch3's multiplexer) just points a different internal block at it.
Fig. 12. One pin, three tenants. Flip the select bits and the Ch3 multiplexer behind the pad points a different internal block at the same physical pin. That's why one pin can be UART, PWM, or plain GPIO — the select bits pick who drives the wire. Watch the voltage trace on the right change personality with each choice: serial chatter, a steady pulse train, one flat level. Then set S1S0=11. No block is selected, nobody drives the copper, and the wire simply floats.

The most-reused block in this whole book shows up one last time, doing exactly what it always did: steer one of several inputs to a single output. To blink an LED we select plain software I/O, the function that lets our registers drive the pin directly. But there's one more switch to throw. A pin can either listen to the outside world (an input) or drive it (an output). A direction register holds one bit per pin to choose, and, of course, you set that bit with a shifted mask. Set the direction and see the pin change job.

MCU pin logic DIR bit (direction register) 0 internal drive request LOW HIGH-Z (disconnected) still listening ✕ blocked LED the world
DIR — the direction-register bit for this pin
try pushing this pin HIGH from inside the chip
input — pin only listens, LED dark
flip DIR to output, then drive HIGH, to light the LED
Fig. 13. Every pin has a DIR bit guarding a tri-state buffer between the chip's internal drive signal and the physical pin. Set DIR = 0 (input) and the buffer disconnects — High-Z — so the pin just listens; try drive: HIGH and it's blocked, the LED stays dark. Flip to DIR = 1 (output) and that same buffer closes the connection, so drive: HIGH now reaches straight through and the LED lights. That's why firmware sets a pin's direction register before it ever tries to drive it.

Leave a pin as an input and no matter how hard you write GPIO_OUT, the LED stays dark. You're shouting down a wire that's set only to listen — nothing you write reaches the pad, because the buffer between them is switched off. That disconnected state has a name: high-impedance, or High-Z. It doesn't mean "input". It means the chip isn't pushing that wire either way, high or low. The copper is left free to be moved by whatever is outside. Flip its direction bit to output and the pin now drives: the buffer connects, and the chip is on the wire. The block is awake, the function is plain I/O, the pin points outward. Everything is finally aimed. Time to pull the trigger.

05Drive it, clear it, light it

This is the moment the whole ladder has been climbing toward. To turn the LED on, you drive its pin high, and you do it with the exact recipe from Chapter 18. Build the mask 1 << n with a shift, then OR it into the output register: GPIO_OUT |= (1 << n). One bit goes high, its pin drives, the light comes on, and the other 31 pins don't so much as twitch. Store the mask and watch it light.

But hold on. "The pin drives high" is not yet an explanation of light. High relative to what? Follow the current and the last inch stops being magic. Driving the pin high closes a switch inside the chip that ties the pad to Vdd. The LED's other leg goes through a resistor to ground. So the instant that switch closes, there is a complete loop — Vdd, through the switch, out the pin, through the LED, through the resistor, into ground — and current runs around it. An LED is a diode built so that electrons crossing its junction shed their energy as photons rather than heat. Current through it is the light. Drive the pin low instead and the other switch closes: the pad is tied to ground, both legs of the LED sit at the same potential, no current flows, and the light goes out. None of that is new either. It is the same voltage-controlled switch this book opened with, still the only thing doing anything.

GPIO_OUT |= (1 << 0) lit 0 / 32 1<<n GPIO_OUT pins |= OR writes a 1 only where the mask has a 1. Every other bit keeps exactly what it held — 31 pins untouched.
pick a pin, then store the OR-mask
The mask is a lone 1 shifted to bit n. OR-ing it in drives that one pin high and leaves the rest alone — store again on another pin and this one stays lit.
Fig. 14. The Chapter 18 set-recipe, GPIO_OUT |= (1<<n), wired straight to copper. Slide n and a single 1 — the mask — slides to bit n. Press store and that lone 1 is OR-ed into the output register: bit n snaps to 1 and its LED lights, while every other bit keeps exactly the value it already held. Store on a second pin and the first stays lit — that's the whole point of OR. One single-1 mask sets one pin and touches no other; the other 31 are left completely alone.

There it is: a program setting a bit, and a real light answering. That single OR to a magic address is the entire bridge from arithmetic to the physical world. Now, turning it back off seems just as easy: GPIO_OUT &= ~(1 << n), the clear recipe. One statement, one line of C. It is not one thing. That clear is a read-modify-write: three separate trips to the bus: read the whole register into a CPU register. Flip one bit there. Write the whole thing back. Say GPIO_OUT holds 0b0001 — pin 0 is on. Your code runs the clear recipe on pin 3. In the gap, after your read and before your write-back, an interrupt fires and its handler turns on pin 1. What does the register hold when the dust settles? Guess before you read on. Most people say 0b0011: your pin 0 still on, the handler's pin 1 now on too. It holds 0b0001. The handler's bit is gone. Your CPU register still held the stale copy — 0b0001, read before the handler ran — and the write-back clobbers the live register with it. Your logic was fine. The window was the bug. So real chips give you a separate GPIO_OUT_CLR register with a cleaner rule: write a 1 to clear that pin, atomically, touching nothing else. Compare the two ways to switch a pin off.

reg &= ~(1<<3) — three trips ⚡ = click a slot before ① OUT→temp read the gap ② clear bit3 in the CPU ③ temp→OUT write-back after temp (CPU copy) temp = 0b–––– GPIO_OUT (live) 0 0 0 1 OUT  = 0b0001 pin 3 pin 1 pin 0 place the ⚡, predict, then step the three trips  
Your prediction — final OUT
predict final OUT, then step ▶
gold = the bit your code clears · blue = the ISR's pin 1 · move the ⚡ — can you find a landing spot that kills pin 1?
Fig. 15. The race, timed by you. GPIO_OUT starts at 0b0001 — pin 0 lit — while your code runs reg &= ~(1<<3) and an interrupt's handler sets pin 1. Click a slot to choose when the ⚡ fires, commit a prediction — 0b0011, 0b0001, or 0b1001 — then step the bus trips. Land the bolt before the read or after the write-back and everything survives: final 0b0011. Land it in the read→write gap and pin 1's lamp flashes on, then dies at the write-back — the stale copy stomps the live register and the answer is 0b0001, even though clearing bit 3 changed nothing at all. Your logic was fine; the window was the bug. Switch to write CLR and the same bolt finds no window anywhere — one atomic store, final 0b0011 from every landing spot.

That's why real GPIO blocks split set and clear into separate addresses: OUT_SET and OUT_CLR, each write-1-to-act. No read, no modify, no race. One atomic store changes exactly the bits you name — we can now turn our LED cleanly on and cleanly off. That's everything we need to make it blink, except for one small problem with time.

06Make it blink

First, a puzzle in a program's shape. Our "light it" program has run its last useful instruction, so the pin is high. What now? A CPU never just stops and sits politely; it can't not fetch. Left alone, the program counter would march off the end of your code into whatever bytes lie beyond and run garbage. So a "finished" program ends by trapping itself: a JMP to its own address. It's an infinite self-branch that fetches the same instruction forever, holding the machine (and the LED) exactly where it is. Watch the PC lock onto itself.

PC LD ADD OUT ?? ?? ?? 0 1 2 3 4 5 LED "JMP 3" means "go to address 3." We're already there — so it loops, forever.
click step to fetch the first byte
PC = program counter, the address the CPU fetches next. ?? = uninitialised memory — whatever bytes happen to be sitting there.
Fig. 16. A finished program can't just stop — fetching is the one thing a CPU cannot not do, and the PC always names a next address. So HALT is really JMP to itself. Park the PC on one address and it re-fetches that same address forever, holding the LED and the whole machine dead still. Then choose no trap and watch the other future: the PC marches straight into uninitialised memory, "executing" garbage.

A self-JMP is how you say "done" to a machine that can't stop. The LED holds steady because the program is pinned in place — but we don't want steady, we want blink: on, off, on, off. The obstacle is speed. The CPU flips the pin millions of times a second, far too fast for your eye, which would just see a dim, permanent glow. So we deliberately waste time. And if that stings, good. I spent a whole section telling you that spinning is a terrible deal, and now I'm about to spin on purpose. Both loops have the identical shape. Only the goal is opposite. Polling burns time you wanted to use; a delay burns time you wanted to lose. Waste is only waste when the time had somewhere better to be, and here the burnt cycles are the product. We load a big number into a register and count it down to zero — DEC then a jump-back-while-nonzero — Chapter 14's countdown, folded into one branch — doing nothing useful, just burning cycles until enough real time has passed to see. Run the delay and watch it stall.

the loop body: DEC R then JNZ loop 1 · the countdown register register R holds 0 load a value below 2 · DEC + JNZ, repeating DEC R JNZ loop R ≠ 0 → loop R = 0 → exit 3 · the pulse it produces output pin (LED) cycles burned so far 0 real time this burns 0 ns 1 GHz clock → 1 cycle ≈ 1 ns · JNZ = jump if not zero · DEC+JNZ = 2 cycles per pass
will load 933 · ≈1.87 µs
scrub R, then press Load to begin
A 1 GHz CPU ticks a billion times a second; your eye needs about 1/20 s to notice anything at all. That gap is why the loop wastes cycles — on purpose.
Fig. 17. The loop body is just two instructions: DEC R subtracts one from the register, JNZ loop jumps back to the top while R isn't zero yet. Press Load to set R and switch the output pin on, then Step through single passes or hit Run to burn the whole countdown at once. At 1 GHz each pass costs about 2 nanoseconds — scrub R small and Run finishes before you could ever notice; scrub it up toward a billion and the same two instructions, repeated, stretch into a real, visible pause before the LED finally switches off. Nothing is happening in that loop — and that's the point: doing nothing, on purpose, for long enough, is how software makes a blink a human can actually see.

One number is still missing from the blink: how big should R actually be? The figure above prices each pass at 2 cycles, and a 1 GHz clock prices each cycle at about 1 ns. Your eye sets the other side of the bargain: a state must hold for roughly 1/20 of a second to register at all. Scrub to the R you believe truly blinks, lock it in, then check it against the arithmetic — and see what a too-small R does to the LED.

what your EYE sees, not what the pin does 1 · eye view steady half-bright glow 2 · what this R buys time per phase (LED held on, then off) 1.00 µs blink rate 500K blinks/s the eye’s floor: ≈1/20 s per phase 3 · R on a log axis 1 1K 1M 1B 1 GHz → 1 cycle ≈ 1 ns · DEC+JNZ = 2 cycles per pass · beat slowed to watch
R = 501
scrub R, then lock your guess
One phase has to hold long enough for a human eye — about 1/20 s. Each pass of the loop burns 2 ns. How many passes is that? Scrub to your guess, then lock it.
◂ lock my R to check your guess — the target only appears after you commit
Fig. 18. The mysterious constant inside every first blink program is not handed down — it is derivable, and here you derive it. Scrub R and read the chain the figure keeps live: R × 2 cycles × 1 ns gives the time each phase holds, and the disc shows what an eye would make of it. The drawing tells you the eye's floor — about 1/20 s per phase — so aim for that, then press lock my R: only after you commit does the check write the division out and drop the target line on the axis. And mind the trap at the fast end: shrinking R never makes the LED blink faster to your eye — below the threshold it settles into a steady half-bright glow, because the eye averages the flicker. "Too fast" doesn't look fast. It looks permanently, dimly on.

A delay loop is honest waste: thousands of do-nothing cycles that stretch a blink into something an eye can catch. Now assemble the whole thing from parts you already own. Set the pin (OR a mask), delay, clear the pin (write the CLR register), delay again, then JMP back to the top, forever. That's a blinking LED, in five steps, every one of them something you built in an earlier chapter. Run the program.

program 0 SET   P0 ; LED on 1 DELAY #4 ; wait 2 CLR   P0 ; LED off 3 DELAY #4 ; wait 4 JMP   0 ; loop output pin P0 pc (line) 0 count
ready — click Step or Run
DELAY reuses the counting register from Ch.14; JMP just rewrites the PC. Five familiar steps, wired in a circle.
Fig. 19. Five lines, one loop: SET drives the pin HIGH, DELAY counts a register down to 0 (the same countdown loop you built in Ch.14), CLR drives it LOW, DELAY counts again, and JMP sends the PC back to line 0 — the blink's first instruction. The one-time setup — release the reset, select the mux function, set the direction — sits above this loop and never re-runs. Press Step to walk it one tick at a time or Run to watch the LED actually blink. Every instruction here was built in an earlier chapter; blinking is just five familiar steps wired into a circle.

Look at what that is: set, wait, clear, wait, loop. A mask from Chapter 18, a store to a magic address from this chapter, a countdown from Chapter 14, a jump from Chapter 13. There is no magic anywhere in it, just the machine doing precisely what we told it. One last question remains, and it's the one that turns this from a simulation into a chip on your desk: how did the program get into the machine in the first place?

07Flash, and the whole climb

You wrote the blink program on your laptop. The chip runs it on power-up with no laptop attached. Somewhere in between, the bytes have to cross a cable and be made to stick. That's the bootloader's job: a tiny program baked into the chip at the factory. When you plug in USB, it catches your compiled bytes and writes them into on-chip flash, a non-volatile memory. From then on, every time the chip powers on, the CPU starts fetching from flash and your program simply runs. Follow the bytes from laptop to running chip.

code.bin your laptop USB microcontroller chip CPU run bootloader FLASH · non-volatile 0 / 8 bytes AHA flash remembers without power
click Upload to send bytes over USB
upload writes bytes into flash, then flip power off and on — watch what it still remembers.
Fig. 20. Click Upload over USB and watch a byte ride the cable into the chip's bootloader, which writes eight bytes straight into flash. Now flip power OFF: the CPU, bootloader, and run LED all go dark — but the flash cells at the bottom stay lit, untouched. Flip power ON again and the CPU's fetch arrow lights up, pulling those same bytes straight out of flash and running them. That's the trick: flash is non-volatile — it needs no power to remember — so the program you sent over USB is still sitting there waiting, no matter how many times the chip loses power.

The load-bearing word there is non-volatile. Your program survives being unplugged because flash is a member of the ROM family from Chapter 11: it keeps its bits with the power off. That's the opposite of RAM, whose cells are two inverters chasing each other in a loop that only exists while power races through it. Cut the power and RAM forgets everything instantly. Cut the power to both and watch which one remembers.

POWER SUPPLY ON RAM needs power to hold a 1 FLASH holds a 1 with no power bits 0000 bits 0000
power ON — click cells, then cut power
that's why RAM forgets and flash remembers — RAM's loop needs power; flash's trapped charge doesn't.
Fig. 21. Click cells to set bits in the RAM block and the flash block, then pull the power lever. RAM's bit lives in a loop that only holds together while current keeps racing through it — cut the power and the loop stops, the charge drains, the bit is gone. Flash's bit is an electron parked behind an insulating gate, trapped with nowhere to leak to — the power can vanish and the charge just sits there, which is why flash remembers and RAM doesn't.

Volatile versus non-volatile: RAM is fast scratch that vanishes; flash is slower storage that persists. That's why your code lives in flash and your variables live in RAM. We've now told the entire story. But I've been showing it to you in pictures, and you shouldn't take a picture on faith. So let's do what we did every chapter: trace the whole MMIO machine one store at a time. Memory becomes a lookup table. A few addresses are wired to a device — the blink logic writes to them, and we watch the LED trace come out.

mem · store → router → device. run, then diff vs hardware. store = idle press Step or Run to execute the blink loop memory mem mem[0x08] · RAM 0 mem[0x40] · → LED 0 pin() = 0 the routed store reproduced the LED trace — bit for bit. sim ▷ hw  ▷
idle — press Step or Run
The router sends a store to 0x40 to the LED, everything else to RAM. Route it right and the trace follows the wire, store for store. Mis-route it and the LED never hears the store.
Fig. 22. Model the machine in code. Memory is just a lookup table: an address in, a value out. One address, 0x40, is wired to the LED; the router sends any store there to the device, everything else to plain RAM. Run the blink loop and the pin follows the store exactly as the wire does — routing by address is the whole mechanism. Mis-route the store to 0x08 and the LED never hears it: the trace diverges, and you can see exactly where the model would have lied.

The LED follows the same rule the hardware does, because the model is the hardware's logic: store to a magic address, trigger a side effect, and everything else is ordinary memory. No hand-waving; the same bits, both ways. And with that, the machine is complete. So take a breath and look back down. You started with one voltage-controlled switch that couldn't decide anything on its own, and here you are, blinking a real light with a program you wrote. Every layer between was the same idea composed again. Here is the whole ladder; you are at the top.

the rung below, wired to itself 9 a blinking LED — the machine speaks 8 a program — orders as numbers 7 a CPU — the fetch-do-repeat loop 6 a bus — shared wires + addresses 5 RAM — a grid of many numbers 4 a register — remembers one number 3 an adder — wires that do arithmetic 2 a gate — a switch answering yes/no 1 a transistor — one voltage-run switch you are here the blink loop: SET P0 ↓ all the way down: doped silicon — the sand
a blinking LED
Store one bit, push it out a pin — and the machine finally does something you can see.
built from: a register + one output pin
tap a name, then tap its rung — wrong rungs shake it back
rung 9 of 9 — the world
The gold ⟲ edge under a rung is the book's only move, drawn: the rung below, wired back onto itself — nine times, sand to light.
Fig. 23. The whole course on one ladder — and this time the book's thesis is drawn, not captioned: the small ⟲ edge under each rung is the wire that says this rung is the rung below, wired to itself. Click any rung and its edge lights gold, matching the gold built from line in the card. Press descend ▾ to walk from the top down to the sand, one keystone per beat; press name all and the labels blank to ? — place all nine names yourself (wrong rungs shake the name back) until the readout says 9/9 named and the ladder lights end to end. And the dot on rung 9 never stops: SET P0 · delay · CLR P0 · delay — the program this chapter wrote, still blinking over everything it stands on.

And that's the finale. We opened on a lie the memory system tells — that a handful of addresses aren't memory — and rode it all the way to a blinking LED. Memory-mapped I/O means the store you built long ago is how a computer touches the world: aim it at a device register and a physical switch flips. We woke a peripheral out of reset, polled a status bit until it was ready, routed a pin through the Chapter-3 mux, set its direction, and drove it high with the Chapter-18 mask. Then we wrapped set and clear around a Chapter-14 delay loop to make it blink, and stored the whole thing in non-volatile flash so it runs on power-up. Not one piece of that was new machinery. It was the ladder you already built, pointed outward. So here's the thing worth carrying away: a computer is not magic and never was. Gates steer electricity; memory is feedback holding still; a CPU is a ROM driving a datapath; and "software" is just numbers a machine has agreed to obey. Every machine you meet from here — a phone, a graphics card, the servers behind the internet — is these same rungs, stacked deeper and repeated wider. You can now stand at any rung of this ladder and see all the way down to the sand. That is the whole point of having climbed it.

iolinked.com
Written by Ajai Raj