1190 lines
43 KiB
C
1190 lines
43 KiB
C
// 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;
|
||
}
|