Skip to content

AHCI

AHCI is the register interface most SATA controllers expose to software today, the DMA-based successor to ATA/IDE in PIO mode, standardized by Intel so that a single driver written against the specification works across chipsets from different vendors without vendor-specific code. Locating and mapping an AHCI controller is a PCI enumeration problem covered elsewhere; this article picks up once BAR5, the ABAR (AHCI Base Address Register), is mapped and readable, and covers the registers, data structures, and command sequence a driver uses to move data through a port.

The first 256 bytes of the ABAR hold registers describing the controller as a whole rather than any individual port. CAP (offset 0x00) reports capabilities fixed in hardware: the number of ports implemented, the number of command slots per port, and whether the controller supports features such as staggered spin-up or native command queuing. GHC (0x04), the Global HBA Control register, holds the AHCI Enable bit that must be set before any port register has defined behavior, along with an HBA Reset bit that returns the entire controller to its power-on state. IS (0x08) is a bitmask of pending port interrupts, one bit per port, letting a driver with a single shared interrupt line determine which port raised it without reading every port’s own status register. PI (0x0C), the Ports Implemented register, is a bitmask identifying which of the up to 32 possible ports actually exist on this controller; a bit being clear there means the corresponding port’s register block, present in memory, corresponds to no physical hardware and must not be touched.

Each implemented port gets its own 128-byte register block starting at offset 0x100 plus 0x80 times the port number. PxCLB/PxCLBU hold the physical address of that port’s command list, split across two 32-bit registers for the low and high halves on a 64-bit system. PxFB/PxFBU likewise address the port’s FIS receive area, a small buffer the controller writes incoming Frame Information Structures into automatically. PxCI, the Command Issue register, is where a driver sets a bit to hand a command slot to the controller for execution; the controller clears that bit itself once the command completes, giving software a direct way to poll for completion without a separate status field. PxIS and PxIE are the port’s own interrupt status and interrupt enable registers, covering conditions specific to that port: a completed command, a task file error, or a device presence change on hot-pluggable hardware. PxTFD, the Task File Data register, mirrors the legacy ATA status and error registers, letting driver code that already understands ATA status bits (BSY, DRQ, ERR) reuse that knowledge here. PxSSTS, PxSCTL, and PxSERR report and control the SATA physical link itself, independent of AHCI: link speed negotiated, whether a device is currently detected, and any link-layer errors, respectively.

A port’s command list is an array of up to 32 command headers, each 32 bytes, one per command slot. A command header holds the physical address of a command table, the number of PRDT entries that table contains, and a handful of flags describing the command: its length in the FIS it carries, whether it’s a write, and whether it uses ATAPI packet commands rather than ordinary ATA ones. The command table itself holds the actual command FIS to send to the device, an optional ATAPI command block, and a Physical Region Descriptor Table (PRDT): a list of physical address and byte-count pairs describing where in memory the data for this command should be read from or written to. A single command’s data does not need to occupy contiguous physical memory; splitting it across multiple PRDT entries is precisely what lets a driver hand the controller a scatter-gather list built from whatever pages a buffer happens to occupy, rather than requiring a contiguous physical allocation for every I/O.

struct h2d_register_fis {
uint8_t fis_type; // 0x27
uint8_t pm_port : 4;
uint8_t rsv0 : 3;
uint8_t c : 1; // 1 = command, 0 = control
uint8_t command;
uint8_t featurel;
uint8_t lba0, lba1, lba2;
uint8_t device;
uint8_t lba3, lba4, lba5;
uint8_t featureh;
uint16_t count;
uint8_t icc;
uint8_t control;
uint32_t rsv1;
};

Frame Information Structures are the unit of communication between host and device on the SATA link itself; AHCI’s command tables and receive areas are built around exchanging them rather than around the older parallel-ATA task file model directly, even though several FIS fields (LBA, sector count, command byte) map onto the same concepts. A Register FIS - Host to Device, type 0x27, is what a driver builds to issue an ordinary ATA command: it carries the command byte, LBA, sector count, and device selection fields a driver already needs to fill in. The controller’s response, a Register FIS - Device to Host (type 0x34), reports the resulting status and error registers and is written by the controller into the port’s FIS receive area rather than returned as a value from any register read. A DMA Setup FIS (0x41) and PIO Setup FIS (0x5F) precede the data phase of DMA and PIO commands respectively, and a Set Device Bits FIS (0xA1) lets a device update status and raise an interrupt without a full register FIS, used heavily by native command queuing to report which of several outstanding commands just completed.

A port begins in an idle, unconfigured state and must be started explicitly before it accepts commands. Software first sets PxCLB/PxCLBU and PxFB/PxFBU to point at driver-allocated, physically contiguous memory for the command list and FIS receive area. PxCMD.FRE (FIS Receive Enable) is set first, telling the controller it may begin writing incoming FISes into the receive area; only after that is PxCMD.ST (Start) set, which tells the controller to begin processing commands from the command list. The controller acknowledges each with a corresponding status bit, PxCMD.FR and PxCMD.CR, and a driver that sets ST without first confirming FR became set, or without FRE set at all, risks a port that appears to accept commands but never signals their completion, since the Register FIS carrying status back from the device has nowhere defined to land.

Reading data begins with a driver locating a free command slot, one whose bit is clear in both PxCI and the controller’s own command-slot-busy tracking, and filling in that slot’s command header and table: the FIS length in DWORDs, the PRDT entry count, and a pointer to a command table containing a Register FIS with the READ DMA EXT command byte, the target LBA, and the sector count, alongside a PRDT entry pointing at the destination buffer.

cmd_header->prdtl = 1;
cmd_header->cfl = sizeof(struct h2d_register_fis) / 4;
cmd_header->w = 0; // read, not write
cmd_table->prdt[0].dba = (uint32_t)(uintptr_t)dest_buffer;
cmd_table->prdt[0].dbau = (uint32_t)((uintptr_t)dest_buffer >> 32);
cmd_table->prdt[0].dbc = (sector_count * 512) - 1; // byte count minus 1
cmd_table->prdt[0].i = 1; // interrupt on completion
fis->fis_type = 0x27;
fis->c = 1;
fis->command = 0x25; // READ DMA EXT
// LBA and count fields set here

With the slot prepared, the driver sets the corresponding bit in PxCI, handing the command to the controller. The controller processes it asynchronously, moves the data via DMA directly into the buffer the PRDT described, writes a Register FIS - Device to Host into the receive area reporting completion status, raises the port’s interrupt if enabled, and clears the driver’s bit in PxCI. A driver polling instead of using interrupts watches that same bit clear rather than any separate “done” flag, since command completion and PxCI clearing are the same event by design.

Native Command Queuing (NCQ) lets a port have multiple commands outstanding simultaneously, issued through PxSACT (the SATA Active register) and PxCI together rather than one at a time, allowing a device’s own firmware to reorder pending reads and writes for better mechanical efficiency on rotating media. NCQ commands use a different command byte (READ FMA QUEUED/WRITE FMA QUEUED, or their EXT variants) and completion is reported through Set Device Bits FISes rather than a single Register FIS per command, since more than one command’s status may need reporting close together. A controller’s CAP register bit for NCQ support, and a corresponding identify-device bit reported by the drive itself, both need checking before a driver assumes queuing is available; issuing NCQ commands to a device or controller that doesn’t support them produces undefined results rather than a clean rejection.

Setting PxCMD.ST before PxCMD.FRE is a common source of a port that silently never completes a command: the Register FIS carrying completion status has no receive area configured to land in yet, so the command executes on the device but its result is lost, leaving PxCI set forever with no interrupt and no error indication. A command table’s PRDT byte count field is defined as one less than the actual byte count transferred, a detail easy to get wrong by exactly one byte and one that produces a transfer that is silently short rather than an outright error. Finally, PxSERR accumulates SATA link-layer errors independently of whether any command is in flight, and a driver checking only PxTFD for errors after each command can miss link resets or CRC errors that PxSERR would have reported directly, particularly around hot-plug events on ports that support them.

  1. ^ Intel, Serial ATA Advanced Host Controller Interface (AHCI) 1.3.1 Specification (defines every register and structure described above)
  2. ^ OSDev Wiki, “AHCI” (a hobbyist implementation reference with sample driver code)
  • PCI: locating and mapping the controller this article’s registers live behind.
  • Port I/O versus Memory-Mapped I/O: the memory-mapped model every register in this article follows.
  • VFS: the abstraction layer a block-level driver like this one eventually feeds into.
  • ATA/IDE in PIO Mode: the simpler, polling-based predecessor this interface’s DMA model replaces.
  • NVMe: the queue-pair-based successor that replaced this interface as the default for modern SSDs.
  • Floppy Disk Controller: a legacy storage driver with no practical relevance today, unlike this one.