Files
nt-lite/code/drivers/storage/ahci/ahci.c
T

1190 lines
43 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ahci.c - AHCI driver, interrupt-driven with per-drive async IRP queue.
// Supports SATA drives only (no ATAPI). Registers each found drive with the
// kernel disk device registry via IoRegisterDiskDevice().
//
// Memory model:
// MmAllocateContiguous(n) returns a physical address for n*4K pages.
// hal->PhysToVirt(phys) converts to kernel virtual address.
// DMA buffers are bounce-buffered (allocated via MmAllocateContiguous) to
// avoid needing a VirtToPhys conversion for arbitrary kernel pointers.
//
// Async I/O model (IRP path):
// Each drive has a dedicated kernel worker thread (AhciWorker) that blocks
// on a per-drive counting semaphore. Callers submit IRPs through IoCallDriver,
// which dispatches to AHCI and enqueues the IRP. The worker dequeues
// one IRP at a time, runs the synchronous hardware transfer (blocking the
// worker, NOT the submitting thread), then writes irp->Status and fires
// irp->CompletionEvent / irp->CompletionFn.
//
// This gives true async I/O from the caller's perspective: IoSubmitIrp()
// returns STATUS_PENDING immediately. Multiple callers can have outstanding
// requests against different drives concurrently without stalling each other.
//
// NCQ extension (future work - see "NCQ TODO" comments):
// To saturate modern SSDs, replace the serialised single-slot model with
// Native Command Queuing:
// • Allocate per-slot bounce buffers (NUM_NCQ_SLOTS × SLOT_BOUNCE_BYTES).
// • Use ATA_CMD_READ_FPDMA_QUEUED / WRITE_FPDMA_QUEUED.
// • Encode the slot tag in FIS.countl bits 7:3.
// • Issue via port->sact (NCQ active) rather than port->ci.
// • In AhciIsr: completed slots = prev_sact ^ port->sact; look up
// slot_irp[slot] and fire each completion from within the ISR.
// • The worker becomes a pure dispatcher: grab a free slot, build the
// command, issue it, return immediately. No per-slot KeWaitForEvent.
//
// Interrupt model (unchanged):
// GHC.IE and per-port PORT_IE are enabled after each port is initialised.
// AhciIsr fires on the PCI IRQ line, captures/clears port status, and queues
// a per-drive DPC. The DPC signals completionEvent outside hard IRQ context.
// port_issue_cmd blocks the worker thread on KeWaitForEvent instead of
// spinning on CI. port_issue_cmd_poll is kept for early init (IDENTIFY)
// before the scheduler is live.
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <ntlite/ddi/io_driver.h>
#include <kernel/mm_phys.h>
#include <kernel/mm_virt.h>
#include <kernel/ke_debug.h>
#include <kernel/ke_event.h>
#include <kernel/ke_semaphore.h>
#include <kernel/ke_thread.h>
#include <kernel/ke_irq.h>
#include <kernel/ke_irp.h>
#include <kernel/ke_dpc.h>
// ---------------------------------------------------------------------------
typedef enum {
FIS_TYPE_REG_H2D = 0x27,
FIS_TYPE_REG_D2H = 0x34,
FIS_TYPE_DMA_ACT = 0x39,
FIS_TYPE_DMA_SETUP = 0x41,
FIS_TYPE_DATA = 0x46,
FIS_TYPE_BIST = 0x58,
FIS_TYPE_PIO_SETUP = 0x5F,
FIS_TYPE_DEV_BITS = 0xA1,
} FIS_TYPE;
#pragma pack(push, 1)
// Register FIS - Host to Device (H2D)
typedef struct {
uint8_t fis_type; // FIS_TYPE_REG_H2D
uint8_t pmport : 4;
uint8_t rsv0 : 3;
uint8_t c : 1; // 1 = Command, 0 = Control
uint8_t command;
uint8_t featurel;
// DW1
uint8_t lba0, lba1, lba2;
uint8_t device;
// DW2
uint8_t lba3, lba4, lba5;
uint8_t featureh;
// DW3
uint8_t countl, counth;
uint8_t icc;
uint8_t control;
// DW4
uint8_t rsv1[4];
} FIS_REG_H2D;
// Register FIS - Device to Host (D2H)
typedef struct {
uint8_t fis_type;
uint8_t pmport : 4;
uint8_t rsv0 : 2;
uint8_t i : 1;
uint8_t rsv1 : 1;
uint8_t status;
uint8_t error;
uint8_t lba0, lba1, lba2;
uint8_t device;
uint8_t lba3, lba4, lba5;
uint8_t rsv2;
uint8_t countl, counth;
uint8_t rsv3[2];
uint8_t rsv4[4];
} FIS_REG_D2H;
// DMA Setup FIS
typedef struct {
uint8_t fis_type;
uint8_t pmport : 4;
uint8_t rsv0 : 1;
uint8_t d : 1;
uint8_t i : 1;
uint8_t a : 1;
uint8_t rsved[2];
uint64_t DMAbufferID;
uint32_t rsvd;
uint32_t DMAbufOffset;
uint32_t TransferCount;
uint32_t resvd;
} FIS_DMA_SETUP;
// PIO Setup FIS
typedef struct {
uint8_t fis_type;
uint8_t pmport : 4;
uint8_t rsv0 : 1;
uint8_t d : 1;
uint8_t i : 1;
uint8_t rsv1 : 1;
uint8_t status;
uint8_t error;
uint8_t lba0, lba1, lba2;
uint8_t device;
uint8_t lba3, lba4, lba5;
uint8_t rsv2;
uint8_t countl, counth;
uint8_t rsv3;
uint8_t e_status;
uint16_t tc;
uint8_t rsv4[2];
} FIS_PIO_SETUP;
// Set Device Bits FIS
typedef struct {
uint8_t fis_type;
uint8_t pmport : 4;
uint8_t rsv0 : 2;
uint8_t i : 1;
uint8_t n : 1;
uint8_t statusl : 4;
uint8_t rsv1 : 4;
uint8_t statush : 4;
uint8_t rsv2 : 4;
uint8_t error;
uint32_t protocol;
} FIS_DEV_BITS;
// ---------------------------------------------------------------------------
// HBA structures (volatile - MMIO)
// ---------------------------------------------------------------------------
// Received FIS structure in host memory
typedef volatile struct {
FIS_DMA_SETUP dsfis; // 0x00 DMA Setup FIS
uint8_t pad0[4];
FIS_PIO_SETUP psfis; // 0x20 PIO Setup FIS
uint8_t pad1[12];
FIS_REG_D2H rfis; // 0x40 Register D2H FIS
uint8_t pad2[4];
FIS_DEV_BITS sdbfis; // 0x58 Set Device Bits FIS
uint8_t ufis[64]; // 0x60 Unknown FIS
uint8_t rsv[0x100-0xA0];
} HBA_FIS;
// Command List header (one entry = one slot)
typedef struct {
// DW0
uint8_t cfl : 5; // Command FIS length in DWORDs (2..16)
uint8_t a : 1; // ATAPI
uint8_t w : 1; // Write (H2D=1, D2H=0)
uint8_t p : 1; // Prefetchable
uint8_t r : 1; // Reset
uint8_t b : 1; // BIST
uint8_t c : 1; // Clear busy on R_OK
uint8_t rsv0 : 1;
uint8_t pmp : 4; // Port multiplier port
uint16_t prdtl; // PRDT length in entries
// DW1
volatile uint32_t prdbc; // PRDT byte count transferred
// DW2,3
uint32_t ctba; // Command table base address (128-byte aligned)
uint32_t ctbau; // Upper 32 bits
// DW4-7
uint32_t rsv1[4];
} HBA_CMD_HEADER;
// Physical Region Descriptor Table entry
typedef struct {
uint32_t dba; // Data base address
uint32_t dbau; // Upper 32 bits
uint32_t rsv0;
uint32_t dbc : 22; // Byte count (0-based, so actual = dbc+1)
uint32_t rsv1 : 9;
uint32_t i : 1; // Interrupt on completion
} HBA_PRDT_ENTRY;
// Command Table
typedef struct {
uint8_t cfis[64]; // Command FIS (up to 64 bytes)
uint8_t acmd[16]; // ATAPI command (12 or 16 bytes)
uint8_t rsv[48]; // Reserved
HBA_PRDT_ENTRY prdt_entry[1]; // PRDT entries (variable length)
} HBA_CMD_TBL;
#pragma pack(pop)
// Port registers
typedef volatile struct {
uint32_t clb; // 0x00 Command list base (1K-aligned)
uint32_t clbu; // 0x04 Command list base upper
uint32_t fb; // 0x08 FIS base (256-byte aligned)
uint32_t fbu; // 0x0C FIS base upper
uint32_t is; // 0x10 Interrupt status
uint32_t ie; // 0x14 Interrupt enable
uint32_t cmd; // 0x18 Command and status
uint32_t rsv0; // 0x1C Reserved
uint32_t tfd; // 0x20 Task file data
uint32_t sig; // 0x24 Signature
uint32_t ssts; // 0x28 SATA status
uint32_t sctl; // 0x2C SATA control
uint32_t serr; // 0x30 SATA error
uint32_t sact; // 0x34 SATA active
uint32_t ci; // 0x38 Command issue
uint32_t sntf; // 0x3C SATA notification
uint32_t fbs; // 0x40 FIS-based switch control
uint32_t rsv1[11];
uint32_t vendor[4];
} HBA_PORT;
// Generic Host Control registers (starts at ABAR)
typedef volatile struct {
uint32_t cap; // 0x00 Host capability
uint32_t ghc; // 0x04 Global host control
uint32_t is; // 0x08 Interrupt status
uint32_t pi; // 0x0C Ports implemented
uint32_t vs; // 0x10 Version
uint32_t ccc_ctl; // 0x14 CCC control
uint32_t ccc_pts; // 0x18 CCC ports
uint32_t em_loc; // 0x1C Enclosure mgmt location
uint32_t em_ctl; // 0x20 Enclosure mgmt control
uint32_t cap2; // 0x24 Extended capabilities
uint32_t bohc; // 0x28 BIOS/OS handoff control
uint8_t rsv[0xA0-0x2C];
uint8_t vendor[0x100-0xA0];
HBA_PORT ports[32]; // 0x100 Port control registers
} HBA_MEM;
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
// PCI
#define PCI_AHCI_CLASS 0x01
#define PCI_AHCI_SUBCLASS 0x06
#define PCI_CMD_REG 0x04
#define PCI_BAR5_REG 0x24
#define PCI_CMD_BUSMASTER (1 << 2)
#define PCI_CMD_MEMSPACE (1 << 1)
// GHC register bits
#define GHC_AE (1u << 31) // AHCI Enable
#define GHC_IE (1u << 1) // Interrupt Enable
#define GHC_HR (1u << 0) // HBA Reset
// BOHC register bits
#define BOHC_BOS (1u << 0) // BIOS-owned semaphore
#define BOHC_OOS (1u << 1) // OS-owned semaphore
#define BOHC_BB (1u << 4) // BIOS Busy
// CAP2 bits
#define CAP2_BOH (1u << 0) // BIOS/OS handoff capable
// Port CMD register bits
#define PORT_CMD_ST (1u << 0) // Start
#define PORT_CMD_FRE (1u << 4) // FIS Receive Enable
#define PORT_CMD_FR (1u << 14) // FIS Receive Running
#define PORT_CMD_CR (1u << 15) // Command List Running
#define PORT_CMD_POD (1u << 2) // Power On Device
#define PORT_CMD_SUD (1u << 1) // Spin-Up Device
#define PORT_CMD_ICC_ACTIVE (0x1u << 28)
// Port IS register bits (interrupt status)
#define PORT_IS_TFES (1u << 30) // Task File Error Status
#define PORT_IS_HBFS (1u << 29) // Host Bus Fatal Error
#define PORT_IS_HBDS (1u << 28) // Host Bus Data Error
#define PORT_IS_IFS (1u << 27) // Interface Fatal Error
// Port SSTS register
#define PORT_DET_PRESENT 0x3
#define PORT_IPM_ACTIVE 0x1
// Port signatures
#define SATA_SIG_ATA 0x00000101
#define SATA_SIG_ATAPI 0xEB140101
#define SATA_SIG_SEMB 0xC33C0101
#define SATA_SIG_PM 0x96690101
// ATA commands
#define ATA_CMD_READ_DMA_EX 0x25
#define ATA_CMD_WRITE_DMA_EX 0x35
#define ATA_CMD_IDENTIFY 0xEC
#define ATA_CMD_FLUSH_EXT 0xEA
// ATA status bits
#define ATA_SR_BUSY 0x80
#define ATA_SR_DRQ 0x08
#define ATA_SR_ERR 0x01
// IDENTIFY word offsets
#define ATA_IDENT_W83 83 // bit 10 = 48-bit LBA supported
#define ATA_IDENT_W100 100 // 48-bit LBA sector count (4 words)
#define ATA_IDENT_W60 60 // 28-bit LBA sector count (2 words)
// Layout constants (per port, single contiguous physical allocation)
// Offset 0: Command List 1024 bytes (32 × 32-byte headers)
// Offset 1024: Received FIS 256 bytes
// Offset 2048: Command Tables 32 × 256 bytes = 8192 bytes
// Total: 10240 bytes → 3 pages (12288 bytes)
#define PORT_PAGES_STRUCT 3
#define PORT_OFF_CMDLIST 0
#define PORT_OFF_FIS 1024
#define PORT_OFF_CMDTBLS 2048
#define PORT_CMDTBL_SIZE 256 // 64 CFIS + 16 ACMD + 48 rsv + 8×16 PRDT
#define PORT_PRDT_COUNT 8 // PRDT entries per command table
// Bounce buffer: 128 sectors × 512 bytes = 64 KB = 16 pages
#define BOUNCE_SECTORS 128
#define BOUNCE_BYTES (BOUNCE_SECTORS * 512)
#define BOUNCE_PAGES (BOUNCE_BYTES / 4096)
// Polling timeout (spin iterations)
#define POLL_TIMEOUT 1000000
#define MAX_AHCI_DRIVES 8
#define NUM_CMD_SLOTS 32
// Map MMIO physical range into the physical window as uncacheable.
// Must be called before any pointer dereference via PhysToVirt().
// AHCI MMIO layout: 0x100 GHC + 32*0x80 ports = 0x1100 bytes → 2 pages minimum.
// We map 8 pages (32KB) without probing the BAR size.
#define AHCI_MMIO_PAGES 8
// ---------------------------------------------------------------------------
// Driver-global state
// ---------------------------------------------------------------------------
static const HAL_API *g_hal;
static HBA_MEM *g_hba; // HBA MMIO base (set during init)
static uint8_t g_irq; // PCI IRQ line for this HBA
static DRIVER_OBJECT g_driver; // AHCI disk driver object
// Per-drive context
typedef struct {
HBA_PORT *port; // MMIO port registers (virtual)
uint64_t sectorCount; // Total LBA sectors on device
uint8_t portNo; // Physical port index (for ISR dispatch)
// Structures block (command list + FIS + cmd tables)
uint64_t structPhys; // Physical base
void *structVirt; // Virtual base
// Bounce buffer for DMA data
uint64_t bouncePhys;
void *bounceVirt;
// Signaled by the completion DPC when the port raises an interrupt.
// Auto-reset: one Wait consumes one completion.
EVENT *completionEvent;
// Last IS snapshot captured by the ISR (for error checking post-wait).
volatile uint32_t lastIs;
// Bottom-half used to wake the worker outside hard IRQ/DIRQL context.
KDPC completionDpc;
// -----------------------------------------------------------------------
// IRP queue - async dispatch
// -----------------------------------------------------------------------
// AhciWorker blocks here waiting for work. AHCI dispatch enqueues an IRP
// and signals the semaphore to wake the worker.
IRP_QUEUE irpQueue;
THREAD *workerThread;
DEVICE_OBJECT *deviceObject;
// NCQ TODO: for multi-slot NCQ, add:
// uint32_t slotMask; // bitmask of in-flight slots
// IRP *slotIrp[NUM_CMD_SLOTS]; // slot → pending IRP
// uint64_t slotBouncePhys[NUM_CMD_SLOTS];
// void *slotBounceVirt[NUM_CMD_SLOTS];
// volatile uint32_t prevSact; // previous port->sact snapshot
} AHCI_DRIVE;
static AHCI_DRIVE g_drives[MAX_AHCI_DRIVES];
static int g_driveCount;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static KSTATUS ahci_map_mmio(uint64_t physBase) {
for (int i = 0; i < AHCI_MMIO_PAGES; i++) {
uint64_t phys = physBase + (uint64_t)i * 4096;
uintptr_t virt = (uintptr_t)g_hal->PhysToVirt(phys);
uint64_t flags = PAGE_PRESENT | PAGE_WRITE | PAGE_NOCACHE;
KSTATUS st = MmMapPage(virt, phys, flags);
// Ignore STATUS_ALREADY_EXISTS - a prior mapping with different flags
// is possible for the first page if the bootloader was overly generous.
// Use MmSetPageProtect to force NOCACHE in that case.
if (st == STATUS_ALREADY_EXISTS) {
st = MmSetPageProtect(virt, 4096, flags);
}
if (!KSUCCESS(st)) {
DbgPrint("AHCI: MmMapPage failed for page %d (phys=0x%llx, st=0x%x)\n",
i, (unsigned long long)phys, st);
return st;
}
}
return STATUS_SUCCESS;
}
// Accessors for port structure memory at known offsets from structVirt/Phys
static inline HBA_CMD_HEADER *cmd_list(AHCI_DRIVE *drv) {
return (HBA_CMD_HEADER *)((uint8_t *)drv->structVirt + PORT_OFF_CMDLIST);
}
static inline uint64_t cmd_list_phys(AHCI_DRIVE *drv) {
return drv->structPhys + PORT_OFF_CMDLIST;
}
static inline uint64_t fis_phys(AHCI_DRIVE *drv) {
return drv->structPhys + PORT_OFF_FIS;
}
static inline HBA_CMD_TBL *cmd_tbl(AHCI_DRIVE *drv, int slot) {
return (HBA_CMD_TBL *)((uint8_t *)drv->structVirt
+ PORT_OFF_CMDTBLS
+ (uint32_t)slot * PORT_CMDTBL_SIZE);
}
static inline uint64_t cmd_tbl_phys(AHCI_DRIVE *drv, int slot) {
return drv->structPhys + PORT_OFF_CMDTBLS
+ (uint64_t)slot * PORT_CMDTBL_SIZE;
}
// ---------------------------------------------------------------------------
// Port engine start / stop (polling safe)
// ---------------------------------------------------------------------------
static void port_stop(HBA_PORT *port) {
// Clear ST (bit 0)
port->cmd &= ~PORT_CMD_ST;
// Wait for CR (bit 15) to clear (command list DMA stops)
for (int i = 0; i < POLL_TIMEOUT; i++) {
if (!(port->cmd & PORT_CMD_CR)) break;
}
// Clear FRE (bit 4)
port->cmd &= ~PORT_CMD_FRE;
// Wait for FR (bit 14) to clear
for (int i = 0; i < POLL_TIMEOUT; i++) {
if (!(port->cmd & PORT_CMD_FR)) break;
}
}
static void port_start(HBA_PORT *port) {
// Wait until CR is clear before setting ST
for (int i = 0; i < POLL_TIMEOUT; i++) {
if (!(port->cmd & PORT_CMD_CR)) break;
}
port->cmd |= PORT_CMD_FRE;
port->cmd |= PORT_CMD_ST;
}
// ---------------------------------------------------------------------------
// Port memory initialisation
// ---------------------------------------------------------------------------
static KSTATUS port_init_memory(AHCI_DRIVE *drv) {
uint64_t flags = PAGE_PRESENT | PAGE_WRITE | PAGE_NOCACHE;
drv->structPhys = MmAllocateContiguous(PORT_PAGES_STRUCT);
if (!drv->structPhys) return STATUS_NO_MEMORY;
drv->structVirt = g_hal->PhysToVirt(drv->structPhys);
// Force NOCACHE on DMA descriptor memory
MmSetPageProtect((uintptr_t)drv->structVirt,
PORT_PAGES_STRUCT * 4096, flags);
memset(drv->structVirt, 0, PORT_PAGES_STRUCT * 4096);
drv->bouncePhys = MmAllocateContiguous(BOUNCE_PAGES);
if (!drv->bouncePhys) return STATUS_NO_MEMORY;
drv->bounceVirt = g_hal->PhysToVirt(drv->bouncePhys);
// Bounce buffer: NOCACHE to ensure CPU writes reach physical memory
// before the HBA's DMA engine reads them
MmSetPageProtect((uintptr_t)drv->bounceVirt,
BOUNCE_PAGES * 4096, flags);
memset(drv->bounceVirt, 0, BOUNCE_BYTES);
HBA_PORT *port = drv->port;
port_stop(port);
// Set command list base
port->clb = (uint32_t)(cmd_list_phys(drv) & 0xFFFFFFFF);
port->clbu = (uint32_t)(cmd_list_phys(drv) >> 32);
// Set FIS base
port->fb = (uint32_t)(fis_phys(drv) & 0xFFFFFFFF);
port->fbu = (uint32_t)(fis_phys(drv) >> 32);
// Initialise all 32 command headers, pointing each to its command table
HBA_CMD_HEADER *hdrs = cmd_list(drv);
for (int i = 0; i < NUM_CMD_SLOTS; i++) {
hdrs[i].prdtl = PORT_PRDT_COUNT;
uint64_t tbl = cmd_tbl_phys(drv, i);
hdrs[i].ctba = (uint32_t)(tbl & 0xFFFFFFFF);
hdrs[i].ctbau = (uint32_t)(tbl >> 32);
}
// Clear any stale interrupts then start the port engine
port->serr = (uint32_t)-1;
port->is = (uint32_t)-1;
port_start(port);
return STATUS_SUCCESS;
}
// ---------------------------------------------------------------------------
// Issue a command - polling fallback (used during early init / IDENTIFY only,
// before the scheduler and interrupt handler are live).
// Returns STATUS_SUCCESS, STATUS_DEVICE_ERROR, or STATUS_TIMEOUT.
// ---------------------------------------------------------------------------
static KSTATUS port_issue_cmd_poll(AHCI_DRIVE *drv) {
HBA_PORT *port = drv->port;
// Clear pending interrupts before issuing
port->is = (uint32_t)-1;
// Wait for port to be idle (not BSY or DRQ)
int spin = 0;
while ((port->tfd & (ATA_SR_BUSY | ATA_SR_DRQ)) && spin < POLL_TIMEOUT)
spin++;
if (spin == POLL_TIMEOUT) {
DbgPrint("AHCI: port hung (pre-issue)\n");
return STATUS_TIMEOUT;
}
// Issue slot 0
port->ci = (1u << 0);
// Poll until slot 0 clears
while (true) {
if (!(port->ci & (1u << 0))) break;
if (port->is & (PORT_IS_TFES | PORT_IS_HBFS | PORT_IS_HBDS | PORT_IS_IFS)) {
DbgPrint("AHCI: error IS=0x%x TFD=0x%x SERR=0x%x\n",
port->is, port->tfd, port->serr);
port->serr = (uint32_t)-1;
port->is = (uint32_t)-1;
return STATUS_DEVICE_ERROR;
}
}
if (port->is & PORT_IS_TFES) {
DbgPrint("AHCI: TFES after completion TFD=0x%x\n", port->tfd);
port->is = (uint32_t)-1;
return STATUS_DEVICE_ERROR;
}
return STATUS_SUCCESS;
}
// ---------------------------------------------------------------------------
// Interrupt bottom half. The ISR owns hardware acknowledgement and status
// capture; this DPC wakes the worker thread after the hard IRQ has unwound.
// ---------------------------------------------------------------------------
static void AhciCompletionDpc(KDPC *dpc, void *context,
void *systemArgument1, void *systemArgument2) {
(void)dpc;
(void)systemArgument1;
(void)systemArgument2;
AHCI_DRIVE *drv = (AHCI_DRIVE *)context;
if (drv && drv->completionEvent)
KeSignalEvent(drv->completionEvent);
}
// ---------------------------------------------------------------------------
// ISR - called from KeIrqDispatch on the AHCI PCI IRQ line.
// Walks all registered drives, checks if their port raised an interrupt,
// captures IS, clears it, and queues the per-drive completion DPC.
// ---------------------------------------------------------------------------
static void AhciIsr(void) {
if (!g_hba) return;
// GHC.IS tells us which ports are pending.
uint32_t pending = g_hba->is;
if (!pending) return;
for (int i = 0; i < g_driveCount; i++) {
AHCI_DRIVE *drv = &g_drives[i];
uint32_t portBit = (1u << drv->portNo);
if (!(pending & portBit)) continue;
// Capture and clear port IS before queueing the bottom half. The
// waiter reads lastIs to check for errors without racing the next
// command. OR in case hardware reports multiple causes before the
// DPC releases the worker.
uint32_t portIs = drv->port->is;
drv->port->is = portIs; // W1C - write back to clear
drv->lastIs |= portIs;
// Clear the HBA-level bit.
g_hba->is = portBit;
KeInsertQueueDpc(&drv->completionDpc, (void *)0, (void *)0);
}
}
// ---------------------------------------------------------------------------
// Issue a command - interrupt-driven (runtime path).
// Caller has already built slot 0 via build_rw_cmd / port_identify.
// Blocks the current thread until the ISR signals completionEvent.
// ---------------------------------------------------------------------------
static KSTATUS port_issue_cmd(AHCI_DRIVE *drv) {
HBA_PORT *port = drv->port;
// Clear stale IS and snapshot before issuing so we don't mis-read a
// leftover value from a prior command.
port->is = (uint32_t)-1;
drv->lastIs = 0;
// Wait for port idle
int spin = 0;
while ((port->tfd & (ATA_SR_BUSY | ATA_SR_DRQ)) && spin < POLL_TIMEOUT)
spin++;
if (spin == POLL_TIMEOUT) {
DbgPrint("AHCI: port hung (pre-issue)\n");
return STATUS_TIMEOUT;
}
// Issue slot 0, then block on the event.
port->ci = (1u << 0);
KeWaitForEvent(drv->completionEvent, 0);
// The ISR has cleared IS - check for errors in the snapshot.
uint32_t is = drv->lastIs;
if (is & (PORT_IS_TFES | PORT_IS_HBFS | PORT_IS_HBDS | PORT_IS_IFS)) {
DbgPrint("AHCI: error IS=0x%x TFD=0x%x SERR=0x%x\n",
is, port->tfd, port->serr);
port->serr = (uint32_t)-1;
return STATUS_DEVICE_ERROR;
}
return STATUS_SUCCESS;
}
// ---------------------------------------------------------------------------
// Build a standard DMA read or write command in slot 0
// ---------------------------------------------------------------------------
static void build_rw_cmd(AHCI_DRIVE *drv, uint64_t lba, uint32_t sectors,
uint64_t bufPhys, bool write) {
HBA_CMD_HEADER *hdr = cmd_list(drv); // slot 0
HBA_CMD_TBL *tbl = cmd_tbl(drv, 0);
memset(tbl, 0, PORT_CMDTBL_SIZE);
// How many PRDT entries we need (each covers up to 8K = 16 sectors)
uint32_t prdtl = (sectors + 15) / 16;
if (prdtl > PORT_PRDT_COUNT) prdtl = PORT_PRDT_COUNT; // capped (see caller)
hdr->cfl = sizeof(FIS_REG_H2D) / 4;
hdr->w = write ? 1 : 0;
hdr->prdtl = (uint16_t)prdtl;
hdr->prdbc = 0;
// Fill PRDT entries
uint32_t remaining = sectors;
uint64_t buf = bufPhys;
for (uint32_t i = 0; i < prdtl; i++) {
uint32_t sects = (remaining > 16) ? 16 : remaining;
uint32_t bytes = sects * 512;
tbl->prdt_entry[i].dba = (uint32_t)(buf & 0xFFFFFFFF);
tbl->prdt_entry[i].dbau = (uint32_t)(buf >> 32);
tbl->prdt_entry[i].dbc = bytes - 1; // hardware adds 1
tbl->prdt_entry[i].i = (i == prdtl - 1) ? 1 : 0;
buf += bytes;
remaining -= sects;
}
// Build H2D FIS
FIS_REG_H2D *fis = (FIS_REG_H2D *)tbl->cfis;
memset(fis, 0, sizeof(*fis));
fis->fis_type = FIS_TYPE_REG_H2D;
fis->c = 1;
fis->command = write ? ATA_CMD_WRITE_DMA_EX : ATA_CMD_READ_DMA_EX;
fis->device = 1u << 6; // LBA mode
fis->lba0 = (uint8_t)(lba);
fis->lba1 = (uint8_t)(lba >> 8);
fis->lba2 = (uint8_t)(lba >> 16);
fis->lba3 = (uint8_t)(lba >> 24);
fis->lba4 = (uint8_t)(lba >> 32);
fis->lba5 = (uint8_t)(lba >> 40);
fis->countl = (uint8_t)(sectors & 0xFF);
fis->counth = (uint8_t)(sectors >> 8);
}
// ---------------------------------------------------------------------------
// IDENTIFY command - fills a 512-byte buffer and returns it
// ---------------------------------------------------------------------------
static KSTATUS port_identify(AHCI_DRIVE *drv) {
// Reuse bounce buffer for IDENTIFY data (512 bytes)
memset(drv->bounceVirt, 0, 512);
HBA_CMD_HEADER *hdr = cmd_list(drv);
HBA_CMD_TBL *tbl = cmd_tbl(drv, 0);
memset(tbl, 0, PORT_CMDTBL_SIZE);
hdr->cfl = sizeof(FIS_REG_H2D) / 4;
hdr->w = 0;
hdr->prdtl = 1;
hdr->prdbc = 0;
tbl->prdt_entry[0].dba = (uint32_t)(drv->bouncePhys & 0xFFFFFFFF);
tbl->prdt_entry[0].dbau = (uint32_t)(drv->bouncePhys >> 32);
tbl->prdt_entry[0].dbc = 512 - 1;
tbl->prdt_entry[0].i = 1;
FIS_REG_H2D *fis = (FIS_REG_H2D *)tbl->cfis;
memset(fis, 0, sizeof(*fis));
fis->fis_type = FIS_TYPE_REG_H2D;
fis->c = 1;
fis->command = ATA_CMD_IDENTIFY;
fis->device = 0;
// Poll: IDENTIFY runs before the scheduler and IRQ handler are live.
KSTATUS st = port_issue_cmd_poll(drv);
if (!KSUCCESS(st)) return st;
// Parse sector count from IDENTIFY data
const uint16_t *id = (const uint16_t *)drv->bounceVirt;
if (id[ATA_IDENT_W83] & (1u << 10)) {
// 48-bit LBA supported
drv->sectorCount = (uint64_t)id[ATA_IDENT_W100]
| ((uint64_t)id[ATA_IDENT_W100 + 1] << 16)
| ((uint64_t)id[ATA_IDENT_W100 + 2] << 32)
| ((uint64_t)id[ATA_IDENT_W100 + 3] << 48);
} else {
// 28-bit LBA
drv->sectorCount = (uint64_t)id[ATA_IDENT_W60]
| ((uint64_t)id[ATA_IDENT_W60 + 1] << 16);
}
return STATUS_SUCCESS;
}
// Core read logic
static KSTATUS do_read(AHCI_DRIVE *drv, uint64_t lba,
uint32_t count, void *buf) {
uint8_t *out = (uint8_t *)buf;
while (count > 0) {
uint32_t batch = (count > BOUNCE_SECTORS) ? BOUNCE_SECTORS : count;
build_rw_cmd(drv, lba, batch, drv->bouncePhys, false);
KSTATUS st = port_issue_cmd(drv);
if (!KSUCCESS(st)) return st;
memcpy(out, drv->bounceVirt, (size_t)batch * 512);
out += (size_t)batch * 512;
lba += batch;
count -= batch;
}
return STATUS_SUCCESS;
}
// Core write logic
static KSTATUS do_write(AHCI_DRIVE *drv, uint64_t lba,
uint32_t count, const void *buf) {
const uint8_t *in = (const uint8_t *)buf;
while (count > 0) {
uint32_t batch = (count > BOUNCE_SECTORS) ? BOUNCE_SECTORS : count;
memcpy(drv->bounceVirt, in, (size_t)batch * 512);
build_rw_cmd(drv, lba, batch, drv->bouncePhys, true);
KSTATUS st = port_issue_cmd(drv);
if (!KSUCCESS(st)) return st;
in += (size_t)batch * 512;
lba += batch;
count -= batch;
}
return STATUS_SUCCESS;
}
// ---------------------------------------------------------------------------
// Async IRP dispatch
// ---------------------------------------------------------------------------
// AhciDispatchReadWrite - DEVICE_OBJECT dispatch entry for disk IRPs.
// Accepted IRPs are queued to the per-drive worker and completed later with
// IoCompleteRequest(). Rejected IRPs are returned synchronously and are not
// completed.
static KSTATUS AhciDispatchReadWrite(DEVICE_OBJECT *device, IRP *irp) {
if (!device || !irp) return STATUS_INVALID_PARAMETER;
AHCI_DRIVE *drv = (AHCI_DRIVE *)device->DeviceExtension;
if (!drv) return STATUS_INVALID_PARAMETER;
if (irp->SectorCount == 0 || !irp->Buffer) return STATUS_INVALID_PARAMETER;
if (irp->Lba + irp->SectorCount > drv->sectorCount) return STATUS_INVALID_PARAMETER;
irp->Status = STATUS_FAILURE;
irp->Information = 0;
IrpQueueEnqueue(&drv->irpQueue, irp);
return STATUS_PENDING;
}
// AhciWorker - kernel thread, one per drive.
// Dequeues IRPs and executes them one at a time using the existing
// single-slot interrupt-driven path. Completion fires the IRP's event
// and/or callback so the original caller can observe the result without
// ever having blocked in a driver call.
//
// NCQ TODO: replace the serialised loop with a multi-slot dispatcher:
// 1. KeWaitSemaphore() to wait for a free slot AND a pending IRP.
// 2. Pick the oldest IRP from the queue.
// 3. Grab a free bit in drv->slotMask (under the slot spinlock).
// 4. Copy write data into slotBounce[slot], build the NCQ FIS (FPDMA),
// record slotIrp[slot] = irp, and issue via port->sact + port->ci.
// 5. Return immediately - the ISR drives all completions.
static void AhciWorker(uintptr_t arg) {
AHCI_DRIVE *drv = (AHCI_DRIVE *)arg;
while (true) {
// Block until AHCI dispatch signals a new IRP.
KeWaitSemaphore(drv->irpQueue.Semaphore);
IRP *irp = IrpQueueDequeue(&drv->irpQueue);
if (!irp) KePanic("AhciWorker: semaphore fired but queue is empty - IRP accounting bug\n");
bool write = !!(irp->Flags & IRP_FLAG_WRITE);
KSTATUS st;
if (write)
st = do_write(drv, irp->Lba, irp->SectorCount, irp->Buffer);
else
st = do_read (drv, irp->Lba, irp->SectorCount, irp->Buffer);
// Publish result then fire completion hooks through the common I/O path.
IoCompleteRequest(irp, st, KSUCCESS(st) ? ((size_t)irp->SectorCount * 512u) : 0);
}
}
// ---------------------------------------------------------------------------
// AHCI controller initialisation
// ---------------------------------------------------------------------------
static KSTATUS hba_reset(HBA_MEM *hba) {
// BIOS/OS handoff (spec §10.6.3)
if (hba->cap2 & CAP2_BOH) {
hba->bohc |= BOHC_OOS;
// Wait for BIOS to clear BOS
for (int i = 0; i < POLL_TIMEOUT; i++) {
if (!(hba->bohc & BOHC_BOS)) break;
}
// Give the BIOS another moment to clear Busy
for (int i = 0; i < 800000; i++) {
if (!(hba->bohc & BOHC_BB)) break;
}
}
// Enable AHCI mode before reset so the reset actually takes effect
hba->ghc |= GHC_AE;
// HBA reset
hba->ghc |= GHC_HR;
for (int i = 0; i < POLL_TIMEOUT; i++) {
if (!(hba->ghc & GHC_HR)) break;
}
if (hba->ghc & GHC_HR) {
DbgPrint("AHCI: HBA reset timed out\n");
return STATUS_TIMEOUT;
}
// Re-enable AHCI mode (reset cleared it), then enable global interrupts.
// GHC.IE lets the HBA assert its PCI interrupt line when a port fires.
hba->ghc |= GHC_AE | GHC_IE;
return STATUS_SUCCESS;
}
// Before memory init: only check physical link. Signature is not yet valid.
static bool port_link_up(HBA_PORT *port) {
uint32_t det = port->ssts & 0x0F;
uint32_t ipm = (port->ssts >> 8) & 0x0F;
return (det == PORT_DET_PRESENT && ipm == PORT_IPM_ACTIVE);
}
// After COMRESET + FIS receive: check signature for SATA disk.
static bool port_is_sata_disk(HBA_PORT *port) {
return port->sig == SATA_SIG_ATA;
}
static void ahci_delay_ms(uint32_t ms) {
for (uint32_t i = 0; i < ms * 1000; i++)
__outbyte(0x80, 0);
}
// Issue a COMRESET on the port, wait for link re-establishment and D2H FIS.
// FRE must already be set (port_init_memory does this) so the HBA can
// capture the signature FIS the device sends after the reset.
static KSTATUS port_comreset(AHCI_DRIVE *drv) {
HBA_PORT *port = drv->port;
port->cmd &= ~PORT_CMD_ST;
for (int i = 0; i < POLL_TIMEOUT; i++)
if (!(port->cmd & PORT_CMD_CR)) break;
// Assert COMRESET: SControl.DET = 1, hold for ≥ 1ms
port->sctl = (port->sctl & ~0x0Fu) | 0x1u;
ahci_delay_ms(1);
// Deassert: DET = 0
port->sctl = port->sctl & ~0x0Fu;
// Poll for DET = 3, up to 1 second
bool link_ok = false;
for (int i = 0; i < 1000; i++) {
if ((port->ssts & 0x0Fu) == PORT_DET_PRESENT) { link_ok = true; break; }
ahci_delay_ms(1);
}
if (!link_ok) {
DbgPrint("AHCI: COMRESET: link did not come back (ssts=0x%x)\n", port->ssts);
return STATUS_TIMEOUT;
}
port->serr = (uint32_t)-1;
port->is = (uint32_t)-1;
port->cmd |= PORT_CMD_ST;
// Wait up to 500ms for a valid signature (D2H FIS from device)
for (int i = 0; i < 500; i++) {
uint32_t sig = port->sig;
if (sig != 0xFFFFFFFFu && sig != 0x00000000u) break;
ahci_delay_ms(1);
}
DbgPrint("AHCI: port COMRESET done: sig=0x%08x ssts=0x%08x\n",
port->sig, port->ssts);
return STATUS_SUCCESS;
}
// PORT_IE bits we care about:
// DHRS (bit 0) - D2H Register FIS received - normal completion
// PSS (bit 1) - PIO Setup FIS received
// DSS (bit 2) - DMA Setup FIS received
// SDBS (bit 3) - Set Device Bits FIS received
// TFES (bit 30) - Task File Error Status - error
// HBFS (bit 29) - Host Bus Fatal error
// HBDS (bit 28) - Host Bus Data error
// IFS (bit 27) - Interface Fatal error
#define PORT_IE_NORMAL (0x0000000Fu)
#define PORT_IE_ERRORS (PORT_IS_TFES | PORT_IS_HBFS | PORT_IS_HBDS | PORT_IS_IFS)
#define PORT_IE_ALL (PORT_IE_NORMAL | PORT_IE_ERRORS)
KSTATUS AhciInitialize(const HAL_API *hal) {
g_hal = hal;
g_driveCount = 0;
memset(&g_driver, 0, sizeof(g_driver));
g_driver.Name = "ahci";
g_driver.MajorFunction[IRP_MJ_READ] = AhciDispatchReadWrite;
g_driver.MajorFunction[IRP_MJ_WRITE] = AhciDispatchReadWrite;
// -----------------------------------------------------------------------
// Locate AHCI controller via PCI class scan
// -----------------------------------------------------------------------
uint8_t pciBus, pciDev, pciFn;
if (!hal->Pci.FindClass(PCI_AHCI_CLASS, PCI_AHCI_SUBCLASS,
&pciBus, &pciDev, &pciFn)) {
DbgPrint("AHCI: no AHCI controller found\n");
return STATUS_NOT_FOUND;
}
DbgPrint("AHCI: controller at PCI %02x:%02x.%x\n", pciBus, pciDev, pciFn);
// -----------------------------------------------------------------------
// Enable Bus Master + Memory Space in PCI command register
// -----------------------------------------------------------------------
uint32_t cmd = hal->Pci.Read32(pciBus, pciDev, pciFn, PCI_CMD_REG);
cmd |= (PCI_CMD_BUSMASTER | PCI_CMD_MEMSPACE); cmd &= ~(1 << 10); cmd |= (1 << 2);
hal->Pci.Write32(pciBus, pciDev, pciFn, PCI_CMD_REG, cmd);
// -----------------------------------------------------------------------
// Read PCI IRQ line
// -----------------------------------------------------------------------
g_irq = hal->Pci.GetIrqLine(pciBus, pciDev, pciFn);
if (g_irq == 0xFF) {
DbgPrint("AHCI: no PCI IRQ wired - cannot use interrupt-driven mode\n");
return STATUS_NOT_SUPPORTED;
}
DbgPrint("AHCI: PCI IRQ line = %u\n", (unsigned)g_irq);
// -----------------------------------------------------------------------
// Read ABAR (BAR5) and map as uncacheable
// -----------------------------------------------------------------------
uint32_t bar5 = hal->Pci.Read32(pciBus, pciDev, pciFn, PCI_BAR5_REG);
uint64_t abarPhys = (uint64_t)(bar5 & ~0xFFFu);
KSTATUS st = ahci_map_mmio(abarPhys);
if (!KSUCCESS(st)) return st;
g_hba = (HBA_MEM *)hal->PhysToVirt(abarPhys);
DbgPrint("AHCI: ABAR phys=0x%llx version=0x%08x\n",
(unsigned long long)abarPhys, g_hba->vs);
// -----------------------------------------------------------------------
// HBA reset + BIOS handoff (also enables GHC.IE)
// -----------------------------------------------------------------------
st = hba_reset(g_hba);
if (!KSUCCESS(st)) return st;
// -----------------------------------------------------------------------
// Log capability summary
// -----------------------------------------------------------------------
uint32_t cap = g_hba->cap;
uint32_t nPorts = (cap & 0x1F) + 1;
uint32_t nSlots = ((cap >> 8) & 0x1F) + 1;
bool s64a = !!(cap & (1u << 31));
DbgPrint("AHCI: cap=0x%08x ports=%u slots=%u 64bit=%d\n",
cap, nPorts, nSlots, (int)s64a);
// -----------------------------------------------------------------------
// Enumerate implemented ports
// -----------------------------------------------------------------------
uint32_t pi = g_hba->pi;
for (uint32_t portno = 0; portno < 32 && g_driveCount < MAX_AHCI_DRIVES; portno++) {
if (!(pi & (1u << portno))) continue;
HBA_PORT *port = &g_hba->ports[portno];
// Phase 1: physical link check
if (!port_link_up(port)) {
DbgPrint("AHCI: port %u: no link (ssts=0x%08x)\n", portno, port->ssts);
continue;
}
DbgPrint("AHCI: port %u: link up, initializing...\n", portno);
int driveIdx = g_driveCount;
AHCI_DRIVE *drv = &g_drives[driveIdx];
memset(drv, 0, sizeof(*drv));
drv->port = port;
drv->portNo = (uint8_t)portno;
// Phase 2: allocate command list, FIS buffer, bounce buffer
st = port_init_memory(drv);
if (!KSUCCESS(st)) {
DbgPrint("AHCI: port %u: memory init failed (0x%x)\n", portno, st);
continue;
}
// Phase 3: COMRESET - forces device to re-send D2H FIS for valid SIG
st = port_comreset(drv);
if (!KSUCCESS(st)) {
DbgPrint("AHCI: port %u: COMRESET failed\n", portno);
continue;
}
// Phase 4: filter to SATA disks
if (!port_is_sata_disk(port)) {
DbgPrint("AHCI: port %u: not a SATA disk (sig=0x%08x), skipping\n",
portno, port->sig);
continue;
}
// Phase 5: create per-drive completion event (auto-reset, initially clear)
st = KeEventCreate(EventAutoReset, 0, &drv->completionEvent);
if (!KSUCCESS(st)) {
DbgPrint("AHCI: port %u: event create failed (0x%x)\n", portno, st);
continue;
}
KeInitializeDpc(&drv->completionDpc, AhciCompletionDpc, drv);
// Phase 5b: create the IRP queue semaphore and initialise the queue.
// The semaphore starts at 0; each submitted IRP increments it by one.
SEMAPHORE *irpSem = (SEMAPHORE *)0;
st = KeSemaphoreCreate(0, 0x7FFFFFFF, &irpSem);
if (!KSUCCESS(st)) {
DbgPrint("AHCI: port %u: semaphore create failed (0x%x)\n", portno, st);
continue;
}
IrpQueueInit(&drv->irpQueue, irpSem);
// Phase 6: IDENTIFY (polled - scheduler not yet running)
st = port_identify(drv);
if (!KSUCCESS(st)) {
DbgPrint("AHCI: port %u: IDENTIFY failed (0x%x)\n", portno, st);
continue;
}
DbgPrint("AHCI: drive %d: %llu sectors (%llu MB)\n",
driveIdx,
(unsigned long long)drv->sectorCount,
(unsigned long long)(drv->sectorCount / 2048));
// Phase 7: enable per-port interrupts.
// Clear any stale IS first so we don't get a spurious signal immediately.
port->is = (uint32_t)-1;
port->ie = PORT_IE_ALL;
// Phase 8: create and register a disk DEVICE_OBJECT.
st = IoCreateDevice(&g_driver, 0, &drv->deviceObject);
if (!KSUCCESS(st)) {
DbgPrint("AHCI: drive %d: IoCreateDevice failed (0x%x)\n", driveIdx, st);
continue;
}
drv->deviceObject->DeviceExtension = drv;
drv->deviceObject->SectorCount = drv->sectorCount;
drv->deviceObject->AlignmentRequirement = 512;
st = IoRegisterDiskDevice((uint8_t)driveIdx, drv->deviceObject);
if (KSUCCESS(st)) {
DbgPrint("AHCI: drive %d registered\n", driveIdx);
g_driveCount++;
} else {
DbgPrint("AHCI: IoRegisterDiskDevice(%d) failed (0x%x)\n", driveIdx, st);
IoDeleteDevice(drv->deviceObject);
drv->deviceObject = (DEVICE_OBJECT*)0;
continue;
}
// Phase 9: spawn the per-drive IRP worker thread.
// The worker owns do_read / do_write exclusively once the scheduler
// is live; it is the only thread that may call port_issue_cmd().
st = KeCreateKernelThread((uintptr_t)AhciWorker, (uintptr_t)drv,
PriorityHigh, &drv->workerThread);
if (!KSUCCESS(st)) {
DbgPrint("AHCI: drive %d: worker thread create failed (0x%x)\n",
driveIdx, st);
// Fatal for this device: all disk access is IRP-based now, so
// leave the device registered but clearly fail during testing.
}
}
if (g_driveCount == 0) {
DbgPrint("AHCI: no drives found\n");
return STATUS_NOT_FOUND;
}
// -----------------------------------------------------------------------
// Register the ISR. All ports on this HBA share one PCI IRQ line.
// Do this after all drives are initialised so the ISR sees valid state.
// -----------------------------------------------------------------------
KeIrqRegister(g_irq, AhciIsr);
DbgPrint("AHCI: ISR registered on IRQ %u; initialised %d drive(s)\n",
(unsigned)g_irq, g_driveCount);
return STATUS_SUCCESS;
}
KSTATUS DriverEntry(const HAL_API *hal) {
DbgPrint("AHCI: driver loading...\n");
KSTATUS st = AhciInitialize(hal);
if (!KSUCCESS(st))
DbgPrint("AHCI: init failed: 0x%x\n", st);
return st;
}