JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Talking to Devices: Controllers, Ports, and Registers

A keyboard, a disk, a network card, a screen — the CPU never touches any of them directly. It talks to a little go-between chip, the controller, through a handful of special registers, and the whole I/O system is built on that one conversation. This guide opens up that conversation and shows the three ways data actually crosses the gap.

The CPU never touches the device

All the way up this ladder we have followed the CPU as it ran processes, switched between them, juggled memory, and faulted pages in from disk. But every one of those stories quietly assumed something we never opened up: that the machine can actually talk to the outside world — read a key you press, paint a pixel, pull a block off a platter, send a packet. This rung is about that conversation. And the first surprising fact is that the CPU never touches a keyboard or a disk directly at all.

Between the CPU and each device sits a small dedicated chip called a device controller — a tiny specialist that speaks the device's own private language on one side and a clean, standard electrical interface on the other. Think of it as a receptionist for a department: you do not barge into the warehouse yourself, you hand a slip to the receptionist at the desk, and they deal with the messy interior. The CPU, the kernel, and you only ever talk to that receptionist. The collection of all these controllers, the wires they hang off, and the devices behind them is what we call the I/O hardware.

Why bother with a middleman? Because devices are wildly different — a disk and a microphone share almost nothing in how they actually move bits — yet the CPU wants to speak to all of them the same way. The controller absorbs the chaos. It handles the device's analog timing, its weird signaling, its quirks, and presents to the CPU a tidy set of mailboxes called registers. Reading and writing those registers is how the CPU talks to the device. So our whole job in this guide is to understand those mailboxes: where they live, what they hold, and how a command gets in and a result gets out.

Four little mailboxes: the controller's registers

Open up almost any controller and you find roughly the same four kinds of register, each just a small cell of bits the CPU can read or write. The data-in register is where the CPU reads bytes the device has produced (a key code, a sector you asked for). The data-out register is where the CPU writes bytes the device should consume (a character to print, a block to save). The status register holds flags the CPU reads to ask 'are you busy? is there data waiting? did something break?' And the control register is where the CPU writes a command — 'start a read,' 'reset,' 'enable interrupts' — to tell the device what to do.

   CPU / kernel                device controller            the device
  +-----------+              +-------------------+        +-----------+
  |  driver   |  --write-->  | [ control reg ]   | -----> | keyboard, |
  |  code     |              | [ status  reg ]   | <----- |  disk,    |
  |           |  <--read---  | [ data-in reg ]   |        |  NIC, ... |
  |           |  --write-->  | [ data-out reg ]  | -----> |           |
  +-----------+              +-------------------+        +-----------+
      ^  the CPU only ever pokes these 4 mailboxes;
         the controller does the messy real work on the right
The whole CPU-to-device conversation funnels through a handful of registers on the controller. The driver writes commands and reads status and data; the controller translates that into the device's real behavior.

With just these four, a complete interaction is possible. To send a character to a printer the CPU might: read the status register in a loop until the 'ready' flag is set, then write the character into the data-out register, then set the 'command-ready' bit in the control register to say 'go.' The controller then drops the 'ready' flag while it works, prints the character at its own slow pace, and raises 'ready' again when done. Four mailboxes, a simple handshake, and an entire printer is driven. Every device, no matter how exotic, is some elaboration of this same dance.

Where do the registers live? Ports vs. memory-mapped I/O

Now a real question: those registers are tiny cells of bits sitting out on a controller chip — so how does CPU code actually name them to read and write? There are two answers, and most machines use both. The first is a separate address space just for devices, reached with special instructions. Each register gets a number called an I/O port, and the CPU has dedicated 'in' and 'out' instructions: roughly 'read port 0x60' (the classic keyboard data port) or 'write 0x3F8.' These ports live in their own little world, completely apart from the memory addresses your programs use.

The second answer is more elegant and now dominant: memory-mapped I/O. Instead of a separate port space, the hardware wires each controller register onto a normal physical memory address. Reading address 0xFEC00000 does not touch any RAM chip — it reads a controller's status register; writing there pokes its control register. The beauty is that no special instructions are needed: the ordinary load and store instructions the CPU already uses for memory work for devices too. A driver can treat a control register like a variable. This is why it folds so naturally into the address-translation machinery you met two rungs ago.

Three ways to actually move the data

Knowing how to read and write a controller's registers, there is still a hard question: how does the CPU coordinate with a device that runs millions of times slower than it does? The CPU executes a billion-plus instructions a second; the printer is still warming up. Across the whole rung you will meet three strategies for this, in increasing cleverness — and the rest of this guide is a quick tour of all three so the later guides have somewhere to land.

The first and simplest is programmed I/O with polling: the CPU sits in a tight loop reading the status register over and over, asking 'ready yet? ready yet?' until the flag flips, then transfers one byte, then loops again for the next. It works, and it is dead simple, but it is a catastrophe for a slow device — the CPU busy-waits, burning millions of cycles doing nothing but asking. It is like standing at the microwave staring through the glass for the entire three minutes when you could be doing the dishes. Fine for a device that is almost always ready; ruinous otherwise.

The cure is to stop asking and let the device tap you on the shoulder. With interrupt-driven I/O, the CPU issues the command and then goes off to run other processes; when the device finally finishes, its controller raises an interrupt — an electrical doorbell — and the CPU drops what it is doing, jumps to a small handler that services the device, and then returns to whatever it was running. No more busy-waiting; the CPU does useful work in the gaps. This is the mechanism behind almost all real I/O, and guide three of this rung dissects the doorbell in loving detail: the vector that says who rang, the handler that answers, and the split into a fast top half and a deferred bottom half.

But there is still a hidden cost in interrupt-driven I/O: for a fast, bulk device like a disk, the CPU would have to copy every single byte through a data register itself — one interrupt and one copy per chunk, thousands of times for one file. So the third trick hands the grunt work to a separate copier chip. With direct memory access, or DMA, the CPU tells the DMA controller 'move 4 KB from the disk controller to this RAM address' and then walks away entirely. The DMA engine shuffles the whole block straight into memory on its own and raises just one interrupt at the very end, saying 'done.' The CPU is freed from the byte-by-byte drudgery; guide two contrasts polling, interrupts, and DMA head to head.

The software side: drivers, and one interface for everything

All this register-poking and interrupt-handling is device-specific, fiddly, and exactly the kind of thing you do not want scattered through the whole kernel. So the OS quarantines it inside a device driver: a chunk of code, one per controller kind, that knows that particular device's registers and quirks intimately, and exposes a small standard set of operations upward — open, read, write, close. Above the driver sits a device-independent I/O layer that speaks only that standard vocabulary, so the rest of the kernel can issue read(fd, buf, n) without ever knowing whether 'fd' is a disk, a keyboard, or a network socket.

This is the same abstraction move you have seen all the way up the ladder — the file descriptor that makes a file, a pipe, and a device all look like one stream of bytes is this very layer at work. To make the uniform interface tractable, the kernel sorts devices into a couple of broad families. A block device (like a disk) moves data in fixed-size chunks you can address and seek within; a character device (like a keyboard or a serial port) delivers a stream of bytes one at a time, in order, no seeking. Two families, a handful of standard operations, and suddenly thousands of wildly different gadgets all fit one set of system calls.

So the journey of a single byte from your keypress is now visible end to end: you press a key, the keyboard controller latches the code into its data register and raises an interrupt; the CPU jumps into the keyboard driver, which reads the data register and hands the byte up through the device-independent layer; eventually it surfaces from a read(fd, buf, n) call in your program. Hardware at the bottom, a thin driver bridging the gap, a uniform interface on top — the rest of this rung simply zooms in on each of those joints in turn.