Carl
all lessons

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.

cpu 6502 io interrupts irq

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.

6502 — DIP-40 pinout Each lead going off the chip is one wire we've named. Address pins (sky), data pins (green), clock pins (amber), and control pins (rose) are the ones we've been talking about.
6502 MOS Technology 1 GND 2 RDY 3 Φ1 4 IRQ 5 NC 6 NMI 7 SYNC 8 Vcc 9 A0 10 A1 11 A2 12 A3 13 A4 14 A5 15 A6 16 A7 17 A8 18 A9 19 A10 20 A11 40 RES 39 Φ2 38 SO 37 Φ0 36 NC 35 NC 34 R/W 33 D0 32 D1 31 D2 30 D3 29 D4 28 D5 27 D6 26 D7 25 A15 24 A14 23 A13 22 A12 21 GND
Address (A0–A15) — 16 wires saying where
Data (D0–D7) — 8 wires saying what
Clock (Φ0 in, Φ1/Φ2 out) — the system clock
Control (R/W, IRQ, NMI, RES) — coordination
Power & misc — keep the chip alive

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:

  1. Person presses a key. Voltage on a wire physically changes.
  2. The keyboard chip detects the change. It sees the transition and prepares a byte describing it — say $41 for the ‘A’ key.
  3. 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.)
  4. The CPU, between instructions, notices. Whatever it was working on in the main program pauses.
  5. The CPU saves state. Pushes PC and P onto the stack so it can come back later. (Remember the stack pointer from lesson 2? This is where it earns its keep.)
  6. The CPU looks up the ISR address in ROM. It reads $FFFE and $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.
  7. The CPU jumps to the ISR. Now it’s executing the keypress handler instead of the main program.
  8. The ISR reads $D012. “What does the keyboard chip have?” → gets $41.
  9. The ISR does the bare minimum — drops the byte into a buffer the main program will check on its own time.
  10. RTI (“ReTurn from Interrupt”) pops PC and P back off the stack.
  11. The CPU picks up exactly where it was, like nothing happened.

Visually:

Timeline The CPU is running the main program. A peripheral pulls IRQ. The CPU saves state, jumps to the ISR, runs it, then returns and picks up exactly where it left off.
main
ISR
IRQ asserted RTI returns

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 SEI instruction (Set Interrupt-disable flag in P). While disabled, IRQs are queued but not serviced. The program re-enables them with CLI when 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.