I/O Hardware Interface: The Bridge to Reality

[!NOTE] What is I/O? Input/Output (I/O) is the communication between the information processing system (CPU/RAM) and the outside world (Disk, Network, Keyboard).

The CPU lives in a pristine world of registers and caches. To touch reality, it must communicate with devices. This chapter explores the Hardware Interface—the specific protocols and electrical pathways used to send commands to devices.


1. The Controller and The Bus

A CPU never talks directly to a disk platter or a network cable. It talks to a Device Controller.

The Device Controller

The controller is a chip (or set of chips) that physically manages the device.

  • Disk Controller: Converting logical block requests into voltage signals to spin a motor and move a magnetic head.
  • Network Controller (NIC): Converting packet data into electrical pulses on a copper wire.

The Bus Hierarchy

The Bus is the shared communication channel. Modern systems use a hierarchy:

  1. PCIe (Peripheral Component Interconnect Express): The high-speed backbone connecting the CPU to high-bandwidth devices (GPU, NVMe, 10GbE).
  2. DMI (Direct Media Interface): Connects the CPU to the PCH (Platform Controller Hub / Southbridge).
  3. USB/SATA: Legacy or slower buses managed by the PCH.

2. Communication Methods: How to Send a Command?

There are two primary ways the CPU can “talk” to a device controller.

A. Port-Mapped I/O (PMIO)

In this legacy method (common in x86), the CPU has a separate address space for I/O ports (0 to 65535).

  • Instructions: IN and OUT assembly instructions.
  • Analogy: PO Boxes. You go to a specific building (Port Space) and put a letter in Box #0x60.

Code Example: x86 Assembly (Port I/O)

Reading from the keyboard controller (Port 0x60).

section .text
global _start

_start:
    ; Read status from Port 0x64
    in al, 0x64
    test al, 1      ; Check if data is ready
    jz _start       ; Busy wait if not ready

    ; Read data from Port 0x60 (Keyboard Data)
    in al, 0x60

    ; AL now contains the scancode

B. Memory-Mapped I/O (MMIO)

This is the standard for modern high-performance devices (PCIe, GPUs).

  • Mechanism: The hardware maps device registers to physical memory addresses.
  • Analogy: Real Estate. The device buys a plot of land in the RAM address space. If you write to that address, you aren’t writing to RAM chips; you are writing directly to the device’s command register.
  • Pros:
    1. Efficiency: Use standard instructions (MOV, memcpy).
    2. Protection: OS can use Page Tables to protect device memory (User space vs Kernel space).

3. Interactive: The Memory Map Visualizer

Explore how physical memory is partitioned between RAM and Devices. Notice the “Hole” reserved for MMIO.

Physical Address Space

High Memory (> 4GB)
MMIO Hole 0xC0000000 - 0xFFFFFFFF
System RAM 0x00000000 - 0xBFFFFFFF

Select a Region

Click on the memory map to see what lives there.


4. The Challenge: Caching and Ordering

MMIO introduces a critical problem: Caching.

If the CPU writes to address 0xA0000 (The GPU command register), and the CPU Cache says “Oh, I’ll just store this in L1 cache and write it to RAM later,” the GPU will never receive the command.

Solution 1: Uncacheable Memory (UC)

The OS marks MMIO pages as Uncacheable in the Page Tables.

  • Reads/Writes go directly to the bus.
  • Slow but correct.

Solution 2: Memory Barriers

Modern CPUs reorder instructions for performance.

// Driver Code
data_port = 0xDEADBEEF; // Write Data
control_port = 0x1;     // Write "GO" Command

The CPU might reorder this! It might write “GO” before the data is ready. To prevent this, we use Memory Barriers (mfence in x86).


5. Code Example: Accessing MMIO in Go

In userspace (Linux), we can access MMIO via /dev/mem using mmap. This is how X11 or Wayland compositors often interact with hardware (or used to).

[!WARNING] This requires root privileges and is dangerous. Writing to the wrong address can crash the system instantly.

```go package main import ( "fmt" "os" "syscall" "unsafe" ) const ( // Hypothetical base address of a device (e.g., GPIO on Raspberry Pi) MMIO_BASE = 0x3F200000 MAP_SIZE = 4096 ) func main() { // 1. Open /dev/mem to access physical memory f, err := os.OpenFile("/dev/mem", os.O_RDWR|os.O_SYNC, 0) if err != nil { panic(err) } defer f.Close() // 2. Map the physical address into our virtual address space // PROT_READ|PROT_WRITE: We want to read and write // MAP_SHARED: Changes are visible to the hardware data, err := syscall.Mmap( int(f.Fd()), MMIO_BASE, MAP_SIZE, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED, ) if err != nil { panic(err) } defer syscall.Munmap(data) // 3. Cast the byte slice to a pointer we can use // Suppose the first 4 bytes are the "Mode Register" modeReg := (*uint32)(unsafe.Pointer(&data[0])) // 4. Write to the hardware fmt.Printf("Current Mode: 0x%X\n", *modeReg) fmt.Println("Setting Mode to OUTPUT...") // This assignment is an MMIO Write! // It goes over the bus to the device controller. *modeReg = 0x1 fmt.Printf("New Mode: 0x%X\n", *modeReg) } ```

6. Summary

  • Controllers bridge the gap between CPU and physical media.
  • Port I/O is legacy; MMIO is the modern standard.
  • MMIO maps device registers into physical address space.
  • Caching must be disabled for MMIO regions to ensure commands reach the hardware.