Input/Output Systems
Part of Basic Computing
I/O systems are the interfaces through which a computer communicates with the external world — reading switches and keyboards, writing to displays and printers, exchanging data with storage devices.
Why This Matters
Without I/O, a computer is a closed loop: it can compute but cannot interact. The engineering challenge of I/O is bridging the speed and signaling differences between the processor (operating at millions of cycles per second) and peripheral devices (mechanical keyboards at human speed, serial links at kilobits per second, displays requiring precise timing).
I/O design also reveals a fundamental tradeoff: should the CPU actively check whether a device is ready (polling), or should the device interrupt the CPU only when it needs attention? This polling vs. interrupt tradeoff reappears in every embedded system, operating system, and network design.
Understanding I/O at the hardware level demystifies device drivers, hardware abstraction layers, and real-time system design. Every “device not responding” error and every printer timeout represents an I/O system failure that can be understood and fixed given this foundation.
Memory-Mapped I/O
In memory-mapped I/O, peripheral devices are assigned addresses in the processor’s memory address space. The CPU reads from and writes to peripherals exactly as it reads from and writes to RAM — using the same LOAD and STORE instructions.
Implementation: the address decoder monitors the address bus. When an address in the I/O range is asserted (for example, 0xFF00–0xFFFF), instead of selecting a RAM chip, it selects the appropriate peripheral register. The peripheral presents its data on the data bus for reads, or accepts data from the data bus for writes.
Example assignments for a simple system:
- 0xFF00: keyboard status register (bit 0 = key available)
- 0xFF01: keyboard data register (ASCII code of last key pressed)
- 0xFF02: display control register
- 0xFF03: display data register (write ASCII char to display)
- 0xFF04: timer counter register (read current count)
- 0xFF05: timer control register
To read a keypress:
WAIT: LOAD R0, $FF00 ; read keyboard status
AND R0, #1 ; test bit 0 (key available)
JZ WAIT ; if 0, no key yet, loop
LOAD R0, $FF01 ; key available: read ASCII codeMemory-mapped I/O simplifies the CPU (no special IN/OUT instructions needed) and simplifies programming (I/O uses the same addressing model as data access).
Port-Mapped I/O
An alternative: separate I/O address space accessed with dedicated IN and OUT instructions. The CPU has an I/O bus distinct from the memory bus, with a separate 8-bit or 16-bit I/O address space. This is the model used by Intel x86 processors.
Advantage: I/O devices cannot accidentally alias with real memory addresses. Disadvantage: requires additional instructions and hardware for I/O cycle signaling.
For hand-built systems, memory-mapped I/O is almost always simpler to implement. Avoid port-mapped I/O unless specifically needed for compatibility with existing software or peripherals.
Polling
Polling is the simplest I/O synchronization: the CPU repeatedly reads a status register and loops until the device signals it is ready.
Example polling loop for UART transmit:
TX_WAIT: LOAD R0, UART_STATUS ; read transmitter status
AND R0, #TXRDY ; test transmit-ready bit
JZ TX_WAIT ; if not ready, poll again
STORE R1, UART_DATA ; write byte to transmitThe CPU spends all its time in the polling loop, doing nothing useful while waiting. For slow devices (keyboard, printer), this wastes enormous CPU time. For fast devices (memory) or when real-time response is critical, polling is acceptable.
Polling is simple to implement and debug: behavior is deterministic and timing is explicit. Use polling for initial bring-up of any I/O interface, then optimize with interrupts if necessary.
Interrupt-Driven I/O
Interrupts allow a peripheral device to signal the CPU asynchronously — the CPU executes its main program, and when a device needs attention (key pressed, byte received), the device asserts an interrupt line. The CPU suspends its current activity, saves its state, jumps to an interrupt service routine (ISR), handles the device, restores state, and resumes.
Hardware requirements:
- Interrupt request line from peripheral to CPU
- CPU interrupt enable/disable mechanism (status register bit)
- Interrupt vector: address of ISR, stored at a fixed location in memory
- Save/restore registers mechanism (push/pop on stack)
Minimal interrupt ISR for keyboard:
KBD_ISR: PUSH R0 ; save registers used
LOAD R0, $FF01 ; read keyboard data
STORE R0, KBD_BUF ; save to buffer
LOAD R0, $FF00 ; acknowledge interrupt (clears status bit)
POP R0 ; restore registers
RTI ; return from interrupt (restores PC and flags)Adding interrupt support to a hand-built CPU requires:
- A flip-flop to latch the interrupt request (set by device, cleared by ISR)
- Logic to detect interrupt request at the end of each instruction cycle when interrupts are enabled
- Automatic push of PC and status register, then load PC from interrupt vector
- RTI instruction to pop PC and status register
This is moderately complex to add to a minimal CPU design but dramatically improves responsiveness for interactive systems.
DMA (Direct Memory Access)
For high-speed data transfers (disk, audio, video), even interrupt-driven I/O is too slow — the CPU cannot process bytes fast enough one at a time. Direct Memory Access (DMA) lets a peripheral transfer data directly to RAM without CPU involvement.
A DMA controller takes control of the bus (the CPU gives up bus control), reads from the peripheral, and writes directly to memory, byte by byte or in bursts. When the transfer completes, the DMA controller interrupts the CPU to signal completion.
For a hand-built simple CPU, DMA is complex and usually unnecessary. Implement polling first, then interrupts, then consider DMA only if performance requirements demand it (e.g., real-time audio or disk streaming).