| name | osdev-storage |
| description | Storage device drivers for OS kernels. ATA PIO mode, AHCI/SATA, NVMe, and DMA-based disk I/O. Register-level programming for reading and writing disk sectors. Use when implementing disk drivers, block device layers, or storage controller interfaces. |
| origin | MCC |
Storage Drivers for x86 Kernels
Complete reference for ATA PIO, AHCI (SATA), and NVMe storage controllers in bare-metal x86 and x86-64 kernels.
When to Use
- Reading disk sectors during early boot (ATA PIO)
- Writing an AHCI/SATA driver with DMA support
- Implementing an NVMe driver for modern SSDs
- Building a block device abstraction layer
- Debugging disk I/O failures or timeout issues
- Choosing between storage interfaces for your kernel
ATA PIO Mode
Why Start Here
ATA PIO is the simplest disk interface. It requires no DMA setup, no PCI enumeration, and no memory-mapped I/O. The CPU transfers every byte through IO ports. This makes it perfect for early boot when you have no memory allocator and no interrupt infrastructure. It is also painfully slow -- the CPU is 100% occupied during transfers. Switch to AHCI or NVMe once your kernel has DMA and interrupt support.
IO Ports
Two ATA buses are standard, each with a master and slave drive:
| Bus | IO Base | Control | IRQ |
|---|
| Primary | 0x1F0-0x1F7 | 0x3F6 | IRQ 14 |
| Secondary | 0x170-0x177 | 0x376 | IRQ 15 |
Register offsets from IO base:
| Offset | Read | Write |
|---|
| +0 | Data (16-bit) | Data (16-bit) |
| +1 | Error | Features |
| +2 | Sector Count | Sector Count |
| +3 | LBA Low (bits 0-7) | LBA Low |
| +4 | LBA Mid (bits 8-15) | LBA Mid |
| +5 | LBA High (bits 16-23) | LBA High |
| +6 | Drive/Head | Drive/Head |
| +7 | Status | Command |
Status Register Flags
Bit 7: BSY -- Drive is busy (wait for this to clear before any operation)
Bit 6: DRDY -- Drive is ready
Bit 5: DF -- Drive fault
Bit 4: SRV -- Service request
Bit 3: DRQ -- Data request (data is ready to transfer)
Bit 1: IDX -- Index mark
Bit 0: ERR -- Error occurred (read Error register for details)
Polling rule: Always wait for BSY to clear. Then check DRQ (for data transfers) or ERR/DF (for errors). Never send a new command while BSY is set.
28-bit LBA Read
void ata_pio_read(uint16_t io_base, uint8_t drive, uint32_t lba,
uint8_t sector_count, uint16_t *buffer) {
outb(io_base + 6, 0xE0 | (drive << 4) | ((lba >> 24) & 0x0F));
outb(io_base + 2, sector_count);
outb(io_base + 3, lba & 0xFF);
outb(io_base + 4, (lba >> 8) & 0xFF);
outb(io_base + 5, (lba >> 16) & 0xFF);
outb(io_base + 7, 0x20);
for (uint8_t s = 0; s < sector_count; s++) {
uint8_t status;
do {
status = inb(io_base + 7);
} while ((status & 0x80) || !(status & 0x08));
if (status & 0x01) return;
for (int i = 0; i < 256; i++)
buffer[s * + i] = inw(io_base);
}
}
Why 256 words, not 512 bytes: ATA data transfers use 16-bit IO port reads (inw/insw). Each inw reads 2 bytes, so 256 reads = 512 bytes = 1 sector. Using inb would be half speed and some controllers do not support 8-bit data reads.
Why 0xE0 in drive select: Bits 7-5 = 111 selects LBA mode. Bit 4 selects master (0) or slave (1). Bits 3-0 are LBA bits 24-27.
400ns Delay Rule
After selecting a different drive (master vs slave), you must wait ~400ns before reading the status register. The standard trick: read the Alternate Status register (control port) 4 times, discarding the results. Each IO read takes at least 100ns.
void ata_400ns_delay(uint16_t ctrl_port) {
inb(ctrl_port); inb(ctrl_port); inb(ctrl_port); inb(ctrl_port);
}
See references/ata-pio.md for 48-bit LBA, write operations, IDENTIFY command, ATAPI detection, and cache flush.
AHCI (SATA)
Why AHCI Over ATA PIO
| Feature | ATA PIO | AHCI |
|---|
| Data transfer | CPU copies every byte via IO ports | DMA -- device transfers directly to memory |
| CPU usage | 100% during transfer | Near zero during transfer |
| Command queuing | One command at a time | Up to 32 outstanding commands (NCQ) |
| Hot-plug | No | Yes |
| Multiple ports | 2 buses, 2 drives each | Up to 32 ports |
| Speed | ~16 MB/s theoretical max | 6 Gbps (SATA III) |
Discovery
Find the AHCI controller by scanning PCI for class 01 (mass storage), subclass 06 (SATA), programming interface 01 (AHCI). BAR5 (ABAR) contains the MMIO base address for the HBA registers.
uint32_t abar = pci_config_read(bus, dev, func, 0x24);
Memory Structures
AHCI uses a hierarchy of memory structures:
HBA Memory Registers (ABAR)
+-- Generic Host Control (GHC, ports implemented, etc.)
+-- Port 0 Registers
| +-- Command List Base Address --> Command List (32 command headers)
| | +-- Command Header 0 --> Command Table
| | | +-- Command FIS (H2D register FIS)
| | | +-- PRDT (Physical Region Descriptor Table)
| | +-- Command Header 1 --> ...
| +-- FIS Base Address --> Received FIS structure
+-- Port 1 Registers
...
Why this complexity exists: AHCI separates command metadata (command headers) from command data (command tables) so the controller can scan all 32 pending commands without fetching their full payloads. The PRDT is a scatter-gather list so you do not need physically contiguous buffers.
Port Initialization
For each active port:
- Stop the command engine (clear PxCMD.ST and PxCMD.FRE, wait for PxCMD.CR and PxCMD.FR to clear)
- Allocate and zero memory for the command list (1 KB, 1 KB aligned) and received FIS (256 bytes, 256 byte aligned)
- Set PxCLB (and PxCLBU for 64-bit) to the command list physical address
- Set PxFB (and PxFBU for 64-bit) to the received FIS physical address
- Clear PxSERR by writing 0xFFFFFFFF to it
- Enable desired interrupts in PxIE
- Start the command engine (set PxCMD.FRE, then PxCMD.ST)
Sending a Read Command
- Find a free command slot (check PxCI and PxSACT for unused bits)
- Set up the command header: CFIS length (5 DWORDs for H2D FIS), PRDT entry count, read bit
- Build the command table: H2D Register FIS with command=0x25 (READ DMA EXT), LBA, sector count
- Fill PRDT entries with buffer physical addresses and byte counts
- Set the command slot bit in PxCI to issue the command
- Wait for completion (PxCI bit clears, or interrupt fires)
See references/ahci-driver.md for HBA register map, port registers, C struct definitions, FIS types, PRDT format, and complete read command implementation.
NVMe
Why NVMe
NVMe was designed from scratch for flash storage over PCIe. Unlike AHCI (which wraps the legacy SATA command set), NVMe uses a queue-based interface that maps naturally to modern hardware:
- Direct PCIe attachment -- no SATA link layer, no HBA abstraction
- 65535 IO queues with 65536 entries each -- massive parallelism
- 4 KB command granularity -- aligned with flash page sizes
- Submission/completion queue pairs -- producer-consumer pattern in shared memory
Controller Discovery
NVMe controllers are PCI class 01 (storage), subclass 08 (NVMe). BAR0 (64-bit) contains the MMIO register space.
Controller Registers
| Offset | Name | Description |
|---|
| 0x00 | CAP | Controller capabilities (queue sizes, timeout, doorbell stride) |
| 0x08 | VS | Version |
| 0x14 | CC | Controller configuration (enable, IO command set, page size) |
| 0x1C | CSTS | Controller status (ready bit) |
| 0x24 | AQA | Admin queue attributes (submission and completion queue sizes) |
| 0x28 | ASQ | Admin submission queue base address (physical, page-aligned) |
| 0x30 | ACQ | Admin completion queue base address (physical, page-aligned) |
| 0x1000+ | Doorbells | Submission tail / completion head doorbells |
Doorbell offsets: 0x1000 + (2 * queue_id) * doorbell_stride for submission tail, 0x1000 + (2 * queue_id + 1) * doorbell_stride for completion head. The doorbell stride is in CAP bits 35:32 (as a power of 2 in DWORDs).
Initialization Sequence
- Disable controller: Clear CC.EN, wait for CSTS.RDY to become 0
- Configure admin queues: Set AQA with queue sizes, ASQ and ACQ with physical addresses of pre-allocated queue buffers
- Enable controller: Set CC.EN (also set CC.IOSQES=6, CC.IOCQES=4 for 64/16 byte entries), wait for CSTS.RDY to become 1
- Identify controller: Send Identify command (opcode 0x06, CNS=1) on admin queue
- Identify namespace: Send Identify command (opcode 0x06, CNS=0, NSID=1) to get capacity and LBA size
- Create IO completion queue: Admin command opcode 0x05
- Create IO submission queue: Admin command opcode 0x01 (must reference the completion queue)
Queue Mechanics
Each queue is a circular buffer in memory. The driver writes commands to the submission queue and advances the tail doorbell. The controller processes commands and writes results to the completion queue.
Submission Queue (driver writes, controller reads):
[head ...... tail] -- driver writes at tail, controller reads from head
Completion Queue (controller writes, driver reads):
[head ...... tail] -- controller writes at tail, driver reads from head
Phase bit trick: Each completion entry has a phase bit. The controller flips it each time it wraps around the queue. The driver uses this to detect new completions without a separate head pointer from the controller. Start expecting phase=1, flip expected phase on wrap.
Sending IO Commands
A submission queue entry is 64 bytes:
DWORD 0: Opcode (byte 0), FUSE (byte 1), Command ID (bytes 2-3)
DWORD 1: Namespace ID
DWORD 6-7: PRP1 (Physical Region Page 1 -- data buffer address)
DWORD 8-9: PRP2 (PRP2 or PRP list pointer for multi-page transfers)
DWORD 10-15: Command-specific
For a Read command (opcode 0x02): DWORD 10-11 = starting LBA, DWORD 12 bits 0-15 = number of logical blocks minus 1.
See references/nvme-driver.md for complete CAP register bitfields, submission/completion entry structs, admin command details, and PRP list construction.
Block Device Abstraction
Wrap all storage drivers behind a common interface so filesystems do not care which hardware is underneath:
typedef struct block_device {
const char *name;
uint32_t sector_size;
uint64_t sector_count;
int (*read_sectors)(struct block_device *dev, uint64_t lba,
uint32_t count, void *buffer);
int (*write_sectors)(struct block_device *dev, uint64_t lba,
uint32_t count, const void *buffer);
int (*get_info)(struct block_device *dev, );
void *driver_data;
} block_device_t;
Registration pattern: Each driver (ATA, AHCI, NVMe) detects its devices and registers a block_device_t with the block layer. The filesystem or partition parser calls read_sectors / write_sectors without knowing the underlying driver.
Common Pitfalls
| Pitfall | Consequence | Fix |
|---|
| Not waiting for BSY to clear before ATA command | Command ignored or corrupted | Always poll status before and after commands |
Using inb instead of inw for ATA data | Half the data, controller may hang | Use 16-bit reads (inw / insw) |
| Forgetting cache flush after ATA write | Data silently lost on power loss | Send command 0xE7 after writes |
| AHCI command list not aligned | HBA ignores or misreads commands | Command list: 1 KB aligned. FIS: 256 B aligned |
| Giving virtual addresses to AHCI PRDT | DMA writes to wrong physical memory | Always use physical addresses in PRDT entries |
| NVMe not disabling controller before configuring | Admin queue pointers ignored | Clear CC.EN and wait for CSTS.RDY=0 first |
| NVMe wrong completion queue phase bit | Driver misses completions or reads stale entries | Track and flip expected phase on wrap |
| No bus master enable in PCI command register | DMA silently does nothing | Set bit 2 of PCI config offset 0x04 |
| AHCI port not stopped before reconfiguring | HBA may read stale command list | Clear ST and FRE, wait for CR and FR to clear |
Related Skills
osdev-device-drivers -- PCI enumeration (needed to find AHCI/NVMe), serial for debugging
osdev-interrupts -- IRQ handler setup for disk completion interrupts
osdev-memory -- physical memory allocator, page table mapping for MMIO and DMA buffers
osdev-boot -- getting to protected/long mode before you can access disk controllers
osdev-toolchain -- QEMU disk image setup, debugging with GDB