Talking to the world — I/O and interrupts
A computer that only talks to itself isn't very useful. Meet the I/O controller — how the keyboard, the LED on the front panel, the timer chip, and a thousand other things get onto the CPU's bus. Plus the trick that lets a peripheral pull the CPU's attention right when it needs to.
You now have a CPU, ROM, RAM, and a clock. The CPU runs programs, ROM remembers them, RAM holds the working values, the clock keeps everyone on the beat. That’s a complete self-contained computer.
But a computer that doesn’t talk to anything outside itself is a beautiful, sealed black box that nobody can tell anything to or get anything out of. There has to be one more chip on the board: the one that deals with the outside world.
That’s the I/O controller.
Memory-mapped I/O — peripherals are just on the bus
Here’s the surprise. The I/O chip doesn’t need a special set of wires. It sits on the same address bus + data bus + R/W line that the RAM and ROM share. The CPU has no idea it’s any different from them.
When the CPU runs LDA $D012, the chip-select decoder on the board
says “address $D012 belongs to the I/O chip”, the I/O chip responds
with a byte, and the CPU stuffs it into A. From the CPU’s perspective,
it just read a byte from “memory” — same LDA instruction, same
number of cycles. From the I/O chip’s perspective, it just put the
current state of the keyboard on the data lines. It’s not actually a
memory chip. It’s a keyboard controller wearing a memory-shaped
costume.
This trick has a name: memory-mapped I/O. The 6502 has no
dedicated input/output instructions. There’s no IN or OUT. There’s
just the same LDA and STA you already know, pointed at addresses
that happen to be wired to peripherals instead of RAM.
Look at the map again — the middle band is the I/O region:
Different chips on the board respond to different sub-ranges of the
I/O territory. A keyboard controller might own $D000–$D00F. A
timer might own $D010–$D01F. A serial port $D020–$D02F. Each
one is a tiny chip wired to listen for its address range and
respond. The 6502 doesn’t know or care; the address decoder on the
board is the part that routes the request.
Inputs — reading the world
Suppose the keyboard controller owns address $D012, and the byte it
puts there reflects which keyboard key is currently pressed. Reading
it is just this:
LDA $D012 ; A := whichever byte the keyboard chip is showing right now
That’s the whole keyboard read. Every time the CPU executes that line, the keyboard controller looks at the keyboard pins, encodes the state, drops it on the data lines. If you want to keep up with the keyboard, you keep running that instruction.
Or imagine a timer chip owns $D040 and the byte at that address is
“the current value of a free-running counter.” LDA $D040 gives you a
high-resolution timer. LDA $D041 reads it again — a different
value, because time has passed.
Outputs — pushing on the world
The other direction works the same way. Suppose $D000 is wired to
eight LEDs on the front panel, one bit per LED. Writing to it
directly drives those LEDs:
LDA #%10100101 ; pattern: lights 7, 5, 2, 0 on
STA $D000 ; M[$D000] := A → LEDs match A's bits
Same STA instruction we used to write to RAM in lesson 1. Different
address, very different consequence: a row of physical lights changes
state. Or a relay clicks. Or a sound chip starts playing a tone. From
the CPU’s perspective it’s just a memory write. From the outside
world’s perspective it’s the entire user interface.
That’s the deep payoff of lesson 1’s “every bit is a wire” framing. Some of those wires don’t lead to other memory cells. Some lead to LEDs, motors, speakers, or pins on a connector. The CPU writes a byte and the world moves.
The polling problem
Here’s an obvious problem. If the only way for the CPU to know “did
the keyboard chip just see a key press?” is to keep reading $D012
over and over and check whether the value changed, it has to spend
cycles looking. If it’s busy doing something else (running a
calculation, drawing a frame, anything) it might miss a fast event
entirely.
This is called polling. It works fine when you don’t mind the overhead. It also wastes a lot of clock cycles asking “anything yet? anything yet? anything yet?” most of which come back “no.”
What if a peripheral could just… interrupt the CPU?
A look at the CPU’s pins
Before we get to interrupts, look at the CPU physically. The 6502 is a 40-pin chip — every “wire” we’ve named so far is a literal lead sticking out of the package. The address bus is 16 pins. The data bus is 8. R/W is one. The clock pins are right there. So are IRQ, NMI, and RESET — actual pieces of metal that another chip can yank on.
The colors map to roles: the same sky / green / amber / rose we’ve
been using everywhere else. The cluster of rose pins on the right
side — RES, R/W, IRQ, NMI — is the control bus, and the
last two on that list are what the rest of this lesson is about.
Interrupts — a peripheral grabs the CPU’s attention
The IRQ line (Interrupt ReQuest) is the wire we just looked at. Like the R/W line, it’s just one wire — but it goes the other direction. Peripherals use it to talk to the CPU, between clock cycles, asking for attention.
The mechanism is a limited-time subroutine — a chunk of code the CPU runs because a peripheral asked, not because the program planned for it. It comes in, does its job, gets out, and the CPU picks up the main program exactly where it left it.
A keypress, end to end
Concrete walkthrough. Someone presses a key on the keyboard:
- Person presses a key. Voltage on a wire physically changes.
- The keyboard chip detects the change. It sees the transition
and prepares a byte describing it — say
$41for the ‘A’ key. - The keyboard chip asserts IRQ. It pulls the CPU’s IRQ pin low. (Look at the chip diagram above — that’s a literal pin moving.)
- The CPU, between instructions, notices. Whatever it was working on in the main program pauses.
- The CPU saves state. Pushes
PCandPonto the stack so it can come back later. (Remember the stack pointer from lesson 2? This is where it earns its keep.) - The CPU looks up the ISR address in ROM. It reads
$FFFEand$FFFF— the IRQ vector — same kind of “look here for the address” trick as the RESET vector. These bytes live in the ROM at the very top of memory. - The CPU jumps to the ISR. Now it’s executing the keypress handler instead of the main program.
- The ISR reads
$D012. “What does the keyboard chip have?” → gets$41. - The ISR does the bare minimum — drops the byte into a buffer the main program will check on its own time.
RTI(“ReTurn from Interrupt”) popsPCandPback off the stack.- The CPU picks up exactly where it was, like nothing happened.
Visually:
The main program (green) is running. The peripheral asserts IRQ. The
CPU’s attention shifts to the ISR (rose). The ISR runs. RTI
executes. The CPU pops back to the main program and continues. The
main program doesn’t have to know anything about it — as long as
the ISR saves and restores the registers it touches, the main program
sees zero side effects of the visit.
The ISR has to be quick
This is the part new programmers reliably get wrong, so it gets its own section.
Every cycle the ISR takes is a cycle the main program isn’t getting. And while the CPU is in this ISR, three things are queuing up against it:
- Other IRQs. If another peripheral asserts IRQ while you’re handling this one, that one waits. Stay in the ISR too long and events back up.
- Time-critical work in the main program. The frame you’re drawing, the audio sample you’re feeding to a sound chip, the serial bit you’re shifting out — all of it is on hold.
- NMI (the urgent kind we’ll meet in a moment) can still fire, and it takes priority over your IRQ.
The temptation, when you have the keypress right there in your hand, is to do the whole job in the ISR: decode the key, look it up in a translation table, dispatch a command, update the screen, redraw the cursor — all from inside the interrupt handler. It’s right there, after all. Why not?
Because every microsecond you spend in the ISR is a microsecond stolen from whatever the main program was actually doing. Rendering hiccups. Audio glitches. Lost or delayed IRQs from other devices. The classic symptom is “the music goes weird whenever I hit a key” — you’re stealing audio cycles to do keyboard work.
The rule is simple: read the byte, stash it somewhere the main program will see, return. Let the main program handle the decoding, the lookups, the redraw, on its own schedule. The ISR is the courier, not the desk.
In normal engineering terms this is called queueing the work for the main loop to process — the ISR drops the trigger into a queue (or buffer, or flag), and a separate piece of code consumes it on its own schedule. You’ll see this pattern from microcontrollers all the way up to operating systems: the interrupt handler is a tiny producer; the main loop is the (relatively unhurried) consumer.
A typical ISR
One quirk of the 6502 family that’s worth calling out: when an IRQ
fires, the chip only saves PC and P automatically. The other
registers — A, X, Y — keep whatever values they had in the
main program. Whatever the ISR touches, the programmer is
responsible for saving and restoring. If the ISR modifies A
without first pushing it on the stack, the main program comes back
to a corrupted accumulator and quietly does the wrong thing.
This is not universal. Many newer CPUs (ARM Cortex-M, x86 with modern OS support, etc.) push the full register set automatically on entry to an interrupt handler — at the cost of some cycles and some stack space. The 6502 keeps it minimal: it gives you the bare mechanism, and you decide what’s worth saving. Cheap, fast, and your problem.
Pseudocode that follows that rule:
service_irq:
PHA ; save A on the stack
TXA
PHA ; save X (the 6502 has no PHX; route via A)
TYA
PHA ; save Y (same reason)
LDA $D012 ; "what's the keyboard chip got?" — read the byte
STA key_buffer ; drop it where the main program will pick it up
PLA
TAY ; restore Y
PLA
TAX ; restore X
PLA ; restore A
RTI ; pop PC and P → resume main program
Push at the top, do the minimum, pop in reverse order, RTI. The push/pop dance is why we have a stack at all — it’s the natural shape for “save several things, do something, restore them in the right order.”
(The original 6502 only has PHA and PHP natively, so X and Y
get routed through A. Newer 6500-family chips like the 65C02 added
PHX and PHY. Tiny detail, but a good example of how each chip
in the family adds operations even when the concepts are
identical — see the callout in lesson 2.)
NMI — the urgent kind
The 6502 has two interrupt lines, not one. The other is NMI
(Non-Maskable Interrupt), wired to its own vector at $FFFA–$FFFB.
The difference:
- IRQ is “polite.” Programs can disable IRQs during a critical
section using the
SEIinstruction (Set Interrupt-disable flag in P). While disabled, IRQs are queued but not serviced. The program re-enables them withCLIwhen it’s safe. - NMI can’t be disabled. As the name says — non-maskable. When the NMI line is pulsed, the CPU services it at the end of the current instruction no matter what.
NMI is reserved for “the kind of thing where dropping it would be catastrophic.” Things like: a monitor’s vertical blank pulse on the NES (drawing must finish before the next frame starts), or a watchdog timer on industrial hardware (the system has 50ms to react or it gets reset).
For now: most peripherals get IRQ. Critical timing stuff gets NMI.
Why this matters
You now have the full picture of how a computer talks to the world:
- Memory-mapped I/O lets every peripheral pretend to be memory. No special instructions needed.
- Polling lets the program ask “anything new?” by re-reading those addresses.
- Interrupts let peripherals demand attention without the program having to check.
- The IRQ vector at
$FFFE–$FFFF(and NMI vector at$FFFA–$FFFB) tell the CPU where to find the handler — same trick as the RESET vector from lesson 3, applied to a different event.
This is the seam between hardware and software. Above it: programs. Below it: chips and wires. The 6502 (and every CPU after it) sits right on the seam, with one foot in each.
What’s next
We’ve put bytes onto LEDs as a simple output. But “an LED on the front panel” is a long way from “a screen full of letters and graphics.” The display is the most demanding peripheral on most systems — and on a 6502 system, it’s its own kind of beast.
Next lesson: the video chip — the peripheral that takes a region of memory and turns it into a picture you can actually look at.