mirror of https://gitlab.com/nakst/essence
891 lines
32 KiB
C++
891 lines
32 KiB
C++
// This file is part of the Essence operating system.
|
|
// It is released under the terms of the MIT license -- see LICENSE.md.
|
|
// Written by: nakst.
|
|
|
|
#include <module.h>
|
|
|
|
// TODO Inserting/removing CDs.
|
|
|
|
#define GENERAL_TIMEOUT (5000)
|
|
|
|
#define COMMAND_LIST_SIZE (0x400)
|
|
#define RECEIVED_FIS_SIZE (0x100)
|
|
#define PRDT_ENTRY_COUNT (0x48) // If one page each, this covers more than CC_ACTIVE_SECTION_SIZE. This must be a multiple of 8.
|
|
#define COMMAND_TABLE_SIZE (0x80 + PRDT_ENTRY_COUNT * 0x10)
|
|
|
|
// Global registers.
|
|
#define RD_REGISTER_CAP() pci->ReadBAR32(5, 0x00) // HBA capababilities.
|
|
#define RD_REGISTER_GHC() pci->ReadBAR32(5, 0x04) // Global host control.
|
|
#define WR_REGISTER_GHC(x) pci->WriteBAR32(5, 0x04, x)
|
|
#define RD_REGISTER_IS() pci->ReadBAR32(5, 0x08) // Interrupt status.
|
|
#define WR_REGISTER_IS(x) pci->WriteBAR32(5, 0x08, x)
|
|
#define RD_REGISTER_PI() pci->ReadBAR32(5, 0x0C) // Ports implemented.
|
|
#define RD_REGISTER_CAP2() pci->ReadBAR32(5, 0x24) // HBA capababilities extended.
|
|
#define RD_REGISTER_BOHC() pci->ReadBAR32(5, 0x28) // BIOS/OS handoff control and status.
|
|
#define WR_REGISTER_BOHC(x) pci->WriteBAR32(5, 0x28, x)
|
|
|
|
// Port-specific registers.
|
|
#define RD_REGISTER_PCLB(p) pci->ReadBAR32(5, 0x100 + (p) * 0x80) // Command list base address (low DWORD).
|
|
#define WR_REGISTER_PCLB(p, x) pci->WriteBAR32(5, 0x100 + (p) * 0x80, x)
|
|
#define RD_REGISTER_PCLBU(p) pci->ReadBAR32(5, 0x104 + (p) * 0x80) // Command list base address (high DWORD).
|
|
#define WR_REGISTER_PCLBU(p, x) pci->WriteBAR32(5, 0x104 + (p) * 0x80, x)
|
|
#define RD_REGISTER_PFB(p) pci->ReadBAR32(5, 0x108 + (p) * 0x80) // FIS base address (low DWORD).
|
|
#define WR_REGISTER_PFB(p, x) pci->WriteBAR32(5, 0x108 + (p) * 0x80, x)
|
|
#define RD_REGISTER_PFBU(p) pci->ReadBAR32(5, 0x10C + (p) * 0x80) // FIS base address (high DWORD).
|
|
#define WR_REGISTER_PFBU(p, x) pci->WriteBAR32(5, 0x10C + (p) * 0x80, x)
|
|
#define RD_REGISTER_PIS(p) pci->ReadBAR32(5, 0x110 + (p) * 0x80) // Interrupt status.
|
|
#define WR_REGISTER_PIS(p, x) pci->WriteBAR32(5, 0x110 + (p) * 0x80, x)
|
|
#define RD_REGISTER_PIE(p) pci->ReadBAR32(5, 0x114 + (p) * 0x80) // Interrupt enable.
|
|
#define WR_REGISTER_PIE(p, x) pci->WriteBAR32(5, 0x114 + (p) * 0x80, x)
|
|
#define RD_REGISTER_PCMD(p) pci->ReadBAR32(5, 0x118 + (p) * 0x80) // Command and status.
|
|
#define WR_REGISTER_PCMD(p, x) pci->WriteBAR32(5, 0x118 + (p) * 0x80, x)
|
|
#define RD_REGISTER_PTFD(p) pci->ReadBAR32(5, 0x120 + (p) * 0x80) // Task file data.
|
|
#define RD_REGISTER_PSIG(p) pci->ReadBAR32(5, 0x124 + (p) * 0x80) // Signature.
|
|
#define RD_REGISTER_PSSTS(p) pci->ReadBAR32(5, 0x128 + (p) * 0x80) // SATA status.
|
|
#define RD_REGISTER_PSCTL(p) pci->ReadBAR32(5, 0x12C + (p) * 0x80) // SATA control.
|
|
#define WR_REGISTER_PSCTL(p, x) pci->WriteBAR32(5, 0x12C + (p) * 0x80, x)
|
|
#define RD_REGISTER_PSERR(p) pci->ReadBAR32(5, 0x130 + (p) * 0x80) // SATA error.
|
|
#define WR_REGISTER_PSERR(p, x) pci->WriteBAR32(5, 0x130 + (p) * 0x80, x)
|
|
#define RD_REGISTER_PCI(p) pci->ReadBAR32(5, 0x138 + (p) * 0x80) // Command issue.
|
|
#define WR_REGISTER_PCI(p, x) pci->WriteBAR32(5, 0x138 + (p) * 0x80, x)
|
|
|
|
struct AHCIPort {
|
|
bool connected, atapi, ssd;
|
|
|
|
uint32_t *commandList;
|
|
uint8_t *commandTables;
|
|
|
|
size_t sectorBytes;
|
|
uint64_t sectorCount;
|
|
|
|
KWorkGroup *commandContexts[32]; // Set to indicate command in use.
|
|
uint64_t commandStartTimeStamps[32];
|
|
uint32_t runningCommands;
|
|
|
|
KSpinlock commandSpinlock;
|
|
KEvent commandSlotsAvailable;
|
|
|
|
char model[41];
|
|
};
|
|
|
|
struct AHCIController : KDevice {
|
|
KPCIDevice *pci;
|
|
|
|
uint32_t capabilities, capabilities2;
|
|
bool dma64Supported;
|
|
size_t commandSlotCount;
|
|
|
|
KTimer timeoutTimer;
|
|
|
|
#define MAX_PORTS (32)
|
|
AHCIPort ports[MAX_PORTS];
|
|
|
|
void Initialise();
|
|
bool Access(uintptr_t port, uint64_t offsetBytes, size_t countBytes, int operation,
|
|
KDMABuffer *buffer, uint64_t flags, KWorkGroup *dispatchGroup);
|
|
bool HandleIRQ();
|
|
bool SendSingleCommand(uintptr_t port);
|
|
void DumpState();
|
|
};
|
|
|
|
struct AHCIDrive : KBlockDevice {
|
|
AHCIController *controller;
|
|
uintptr_t port;
|
|
};
|
|
|
|
struct InterruptEvent {
|
|
uint64_t timeStamp;
|
|
uint32_t globalInterruptStatus;
|
|
uint32_t port0CommandsRunning;
|
|
uint32_t port0CommandsIssued;
|
|
bool complete;
|
|
};
|
|
|
|
volatile uintptr_t recentInterruptEventsPointer;
|
|
volatile InterruptEvent recentInterruptEvents[64];
|
|
|
|
void AHCIController::DumpState() {
|
|
uint64_t timeStamp = KGetTimeInMs();
|
|
|
|
EsPrint("AHCI controller state:\n");
|
|
|
|
EsPrint("\t--- Registers ---\n");
|
|
EsPrint("\t\tHBA capabilities: %x.\n", RD_REGISTER_CAP());
|
|
EsPrint("\t\tGlobal host control: %x.\n", RD_REGISTER_GHC());
|
|
EsPrint("\t\tInterrupt status: %x.\n", RD_REGISTER_IS());
|
|
EsPrint("\t\tPorts implemented: %x.\n", RD_REGISTER_PI());
|
|
EsPrint("\t\tHBA capabilities extended: %x.\n", RD_REGISTER_CAP2());
|
|
EsPrint("\t\tBIOS/OS handoff control and status: %x.\n", RD_REGISTER_BOHC());
|
|
|
|
for (uintptr_t i = 0; i < MAX_PORTS; i++) {
|
|
AHCIPort *port = ports + i;
|
|
|
|
if (!port->connected) {
|
|
continue;
|
|
}
|
|
|
|
EsPrint("\t--- Port %d ---\n", i);
|
|
EsPrint("\t\tCommand list base address (low DWORD): %x.\n", RD_REGISTER_PCLB(i));
|
|
EsPrint("\t\tCommand list base address (high DWORD): %x.\n", RD_REGISTER_PCLBU(i));
|
|
EsPrint("\t\tFIS base address (low DWORD): %x.\n", RD_REGISTER_PFB(i));
|
|
EsPrint("\t\tFIS base address (high DWORD): %x.\n", RD_REGISTER_PFBU(i));
|
|
EsPrint("\t\tInterrupt status: %x.\n", RD_REGISTER_PIS(i));
|
|
EsPrint("\t\tInterrupt enable: %x.\n", RD_REGISTER_PIE(i));
|
|
EsPrint("\t\tCommand and status: %x.\n", RD_REGISTER_PCMD(i));
|
|
EsPrint("\t\tTask file data: %x.\n", RD_REGISTER_PTFD(i));
|
|
EsPrint("\t\tSignature: %x.\n", RD_REGISTER_PSIG(i));
|
|
EsPrint("\t\tSATA status: %x.\n", RD_REGISTER_PSSTS(i));
|
|
EsPrint("\t\tSATA error: %x.\n", RD_REGISTER_PSERR(i));
|
|
EsPrint("\t\tCommand issue: %x.\n", RD_REGISTER_PCI(i));
|
|
EsPrint("\t\tATAPI: %d.\n", port->atapi);
|
|
EsPrint("\t\tBytes per sector: %D.\n", port->sectorBytes);
|
|
EsPrint("\t\tTotal capacity: %D.\n", port->sectorBytes * port->sectorCount);
|
|
EsPrint("\t\tCommand slots available: %d.\n", port->commandSlotsAvailable.state);
|
|
EsPrint("\t\tRunning commands: %x.\n", port->runningCommands);
|
|
|
|
uint8_t *receivedFIS = (uint8_t *) port->commandList + 0x400;
|
|
|
|
EsPrint("\t\tReceived FIS D2H register: type %X, interrupt %X, status %X, error %X, device/head %X, sector %x, sector count %x.\n",
|
|
receivedFIS[0x40 + 0], receivedFIS[0x40 + 1], receivedFIS[0x40 + 2], receivedFIS[0x40 + 3], receivedFIS[0x40 + 7],
|
|
(uint64_t) receivedFIS[0x40 + 4] | ((uint64_t) receivedFIS[0x40 + 5] << 8) | ((uint64_t) receivedFIS[0x40 + 6] << 16)
|
|
| ((uint64_t) receivedFIS[0x40 + 8] << 24) | ((uint64_t) receivedFIS[0x40 + 9] << 32) | ((uint64_t) receivedFIS[0x40 + 10] << 40),
|
|
(uint64_t) receivedFIS[0x40 + 12] | ((uint64_t) receivedFIS[0x40 + 13] << 8));
|
|
EsPrint("\t\tReceived FIS set device bits: type %X, interrupt %X, status %X, error %X.\n",
|
|
receivedFIS[0x58 + 0], receivedFIS[0x58 + 1], receivedFIS[0x58 + 2], receivedFIS[0x58 + 3]);
|
|
EsPrint("\t\tReceived FIS DMA setup: type %X, flags %X, buffer identifier low %x, buffer identifier high %x, buffer offset %x, transfer count %x.\n",
|
|
receivedFIS[0], receivedFIS[1], ((uint32_t *) receivedFIS)[1],
|
|
((uint32_t *) receivedFIS)[2], ((uint32_t *) receivedFIS)[4], ((uint32_t *) receivedFIS)[5]);
|
|
|
|
for (uintptr_t j = 0; j < 32; j++) {
|
|
if (~port->runningCommands & (1 << j)) continue;
|
|
|
|
EsPrint("\t\tCommand %d: started %dms ago for dispatch group %x.\n", j,
|
|
timeStamp - port->commandStartTimeStamps[j], port->commandContexts[j]);
|
|
|
|
EsPrint("\t\t\tDW0 %x: %d FIS bytes, %z, %z, %d PRDT entries.\n",
|
|
port->commandList[j * 8 + 0], (port->commandList[j * 8 + 0]) & 31,
|
|
((port->commandList[j * 8 + 0]) & (1 << 5)) ? "SATAPI" : "SATA",
|
|
((port->commandList[j * 8 + 0]) & (1 << 6)) ? "write" : "read",
|
|
port->commandList[j * 8 + 0] >> 16);
|
|
EsPrint("\t\t\tDW1 transferring %D.\n", port->commandList[j * 8 + 1]);
|
|
EsPrint("\t\t\tDW2/3 command table base address: %x.\n", *(uint64_t *) &port->commandList[j * 8 + 2]);
|
|
|
|
uint8_t *commandFIS = port->commandTables + COMMAND_TABLE_SIZE * j;
|
|
|
|
EsPrint("\t\t\tH2D FIS: type %X, command %X, features %X, device/head %X, features 2 %X, control %X, sector %x, sector count %x.\n",
|
|
commandFIS[0], commandFIS[2], commandFIS[3], commandFIS[7], commandFIS[11], commandFIS[15],
|
|
(uint64_t) commandFIS[4] | ((uint64_t) commandFIS[5] << 8) | ((uint64_t) commandFIS[6] << 16)
|
|
| ((uint64_t) commandFIS[8] << 24) | ((uint64_t) commandFIS[9] << 32) | ((uint64_t) commandFIS[10] << 40),
|
|
(uint64_t) commandFIS[12] | ((uint64_t) commandFIS[13] << 8));
|
|
|
|
uint8_t *atapiCommand = commandFIS + 64;
|
|
|
|
if (((port->commandList[j * 8 + 0]) & (1 << 5))) {
|
|
EsPrint("\t\t\tATAPI command: %X %X %X %X %X %X %X %X %X %X %X %X %X %X %X %X.\n",
|
|
atapiCommand[0], atapiCommand[1], atapiCommand[2], atapiCommand[3],
|
|
atapiCommand[4], atapiCommand[5], atapiCommand[6], atapiCommand[7],
|
|
atapiCommand[8], atapiCommand[9], atapiCommand[10], atapiCommand[11],
|
|
atapiCommand[12], atapiCommand[13], atapiCommand[14], atapiCommand[15]);
|
|
}
|
|
|
|
uint32_t *prdt = (uint32_t *) (commandFIS + 128);
|
|
|
|
for (uintptr_t k = 0; k < (port->commandList[j * 8 + 0] >> 16); k++) {
|
|
EsPrint("\t\t\tPRDT entry %d: base address %x, byte count %x%z.\n",
|
|
k, *(uint64_t *) (prdt + k * 4), (prdt[k * 4 + 3] & 0xFFFFFFF) + 1,
|
|
(prdt[k * 4 + 3] & 0x80000000) ? ", interrupt on completion" : "");
|
|
}
|
|
}
|
|
}
|
|
|
|
EsPrint("\t--- Most recent interrupts ---\n");
|
|
|
|
for (uintptr_t i = 0; i < sizeof(recentInterruptEvents) / sizeof(recentInterruptEvents[0]); i++) {
|
|
if (recentInterruptEventsPointer) recentInterruptEventsPointer--;
|
|
else recentInterruptEventsPointer = sizeof(recentInterruptEvents) / sizeof(recentInterruptEvents[0]) - 1;
|
|
|
|
volatile InterruptEvent *event = recentInterruptEvents + recentInterruptEventsPointer;
|
|
|
|
EsPrint("\t\tEvent %d: fired %dms ago, GIS %x, CI0 %x, CR0 %x%z.\n",
|
|
i, timeStamp - event->timeStamp,
|
|
event->globalInterruptStatus, event->port0CommandsIssued,
|
|
event->port0CommandsRunning, event->complete ? "" : ", incomplete");
|
|
}
|
|
}
|
|
|
|
bool AHCIController::Access(uintptr_t portIndex, uint64_t offsetBytes, size_t countBytes, int operation,
|
|
KDMABuffer *buffer, uint64_t, KWorkGroup *dispatchGroup) {
|
|
AHCIPort *port = ports + portIndex;
|
|
|
|
#if 0
|
|
if (operation == K_ACCESS_WRITE) {
|
|
KernelPanic("AHCIController::Access - Attempted write.\n");
|
|
}
|
|
#endif
|
|
|
|
// Find a command slot to use.
|
|
|
|
uintptr_t commandIndex = 0;
|
|
|
|
while (true) {
|
|
KSpinlockAcquire(&port->commandSpinlock);
|
|
|
|
uint32_t commandsAvailable = ~RD_REGISTER_PCI(portIndex);
|
|
bool found = false;
|
|
|
|
for (uintptr_t i = 0; i < commandSlotCount; i++) {
|
|
if ((commandsAvailable & (1 << i)) && !port->commandContexts[i]) {
|
|
commandIndex = i;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
KEventReset(&port->commandSlotsAvailable);
|
|
} else {
|
|
port->commandContexts[commandIndex] = dispatchGroup;
|
|
}
|
|
|
|
KSpinlockRelease(&port->commandSpinlock);
|
|
|
|
if (!found) {
|
|
KEventWait(&port->commandSlotsAvailable);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Setup the command FIS.
|
|
|
|
uint32_t countSectors = countBytes / port->sectorBytes;
|
|
uint64_t offsetSectors = offsetBytes / port->sectorBytes;
|
|
|
|
if (countSectors & ~0xFFFF) {
|
|
KernelPanic("AHCIController::Access - Too many sectors to read.\n");
|
|
}
|
|
|
|
uint32_t *commandFIS = (uint32_t *) (port->commandTables + COMMAND_TABLE_SIZE * commandIndex);
|
|
commandFIS[0] = 0x27 /* H2D */ | (1 << 15) /* command */ | ((operation == K_ACCESS_WRITE ? 0x35 /* write DMA 48 */ : 0x25 /* read DMA 48 */) << 16);
|
|
commandFIS[1] = (offsetSectors & 0xFFFFFF) | (1 << 30);
|
|
commandFIS[2] = (offsetSectors >> 24) & 0xFFFFFF;
|
|
commandFIS[3] = countSectors & 0xFFFF;
|
|
commandFIS[4] = 0;
|
|
|
|
// Setup the PRDT.
|
|
|
|
size_t prdtEntryCount = 0;
|
|
uint32_t *prdt = (uint32_t *) (port->commandTables + COMMAND_TABLE_SIZE * commandIndex + 0x80);
|
|
|
|
while (!KDMABufferIsComplete(buffer)) {
|
|
if (prdtEntryCount == PRDT_ENTRY_COUNT) {
|
|
KernelPanic("AHCIController::Access - Too many PRDT entries.\n");
|
|
}
|
|
|
|
KDMASegment segment = KDMABufferNextSegment(buffer);
|
|
|
|
prdt[0 + 4 * prdtEntryCount] = ES_PTR64_LS32(segment.physicalAddress);
|
|
prdt[1 + 4 * prdtEntryCount] = ES_PTR64_MS32(segment.physicalAddress);
|
|
prdt[2 + 4 * prdtEntryCount] = 0;
|
|
prdt[3 + 4 * prdtEntryCount] = (segment.byteCount - 1) | (segment.isLast ? (1 << 31) /* IRQ when done */ : 0);
|
|
|
|
prdtEntryCount++;
|
|
}
|
|
|
|
// Setup the command list entry, and issue the command.
|
|
|
|
port->commandList[commandIndex * 8 + 0] = 5 /* FIS is 5 DWORDs */ | (prdtEntryCount << 16) | (operation == K_ACCESS_WRITE ? (1 << 6) : 0);
|
|
port->commandList[commandIndex * 8 + 1] = 0;
|
|
|
|
// Setup SCSI command if ATAPI.
|
|
|
|
if (port->atapi) {
|
|
port->commandList[commandIndex * 8 + 0] |= (1 << 5) /* ATAPI */;
|
|
commandFIS[0] = 0x27 /* H2D */ | (1 << 15) /* command */ | (0xA0 /* packet */ << 16);
|
|
commandFIS[1] = countBytes << 8;
|
|
|
|
uint8_t *scsiCommand = (uint8_t *) commandFIS + 0x40;
|
|
EsMemoryZero(scsiCommand, 10);
|
|
scsiCommand[0] = 0xA8 /* READ (12) */;
|
|
scsiCommand[2] = (offsetSectors >> 0x18) & 0xFF;
|
|
scsiCommand[3] = (offsetSectors >> 0x10) & 0xFF;
|
|
scsiCommand[4] = (offsetSectors >> 0x08) & 0xFF;
|
|
scsiCommand[5] = (offsetSectors >> 0x00) & 0xFF;
|
|
scsiCommand[9] = countSectors;
|
|
}
|
|
|
|
// Start executing the command.
|
|
|
|
KSpinlockAcquire(&port->commandSpinlock);
|
|
port->runningCommands |= 1 << commandIndex;
|
|
__sync_synchronize();
|
|
WR_REGISTER_PCI(portIndex, 1 << commandIndex);
|
|
port->commandStartTimeStamps[commandIndex] = KGetTimeInMs();
|
|
KSpinlockRelease(&port->commandSpinlock);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool AHCIController::HandleIRQ() {
|
|
uint32_t globalInterruptStatus = RD_REGISTER_IS();
|
|
KernelLog(LOG_VERBOSE, "AHCI", "received IRQ", "Received IRQ with status: %x.\n", globalInterruptStatus);
|
|
if (!globalInterruptStatus) return false;
|
|
WR_REGISTER_IS(globalInterruptStatus);
|
|
|
|
volatile InterruptEvent *event = recentInterruptEvents + recentInterruptEventsPointer;
|
|
event->timeStamp = KGetTimeInMs();
|
|
event->globalInterruptStatus = globalInterruptStatus;
|
|
event->complete = false;
|
|
recentInterruptEventsPointer = (recentInterruptEventsPointer + 1) % (sizeof(recentInterruptEvents) / sizeof(recentInterruptEvents[0]));
|
|
|
|
bool commandCompleted = false;
|
|
|
|
for (uintptr_t i = 0; i < MAX_PORTS; i++) {
|
|
if (~globalInterruptStatus & (1 << i)) continue;
|
|
|
|
uint32_t interruptStatus = RD_REGISTER_PIS(i);
|
|
if (!interruptStatus) continue;
|
|
WR_REGISTER_PIS(i, interruptStatus);
|
|
|
|
AHCIPort *port = ports + i;
|
|
|
|
if (interruptStatus & ((1 << 30) | (1 << 29) | (1 << 28) | (1 << 27) | (1 << 26) | (1 << 24) | (1 << 23))) {
|
|
KernelLog(LOG_ERROR, "AHCI", "error IRQ", "Received IRQ error interrupt status bit set: %x.\n", interruptStatus);
|
|
|
|
KSpinlockAcquire(&port->commandSpinlock);
|
|
|
|
// Stop command processing.
|
|
|
|
WR_REGISTER_PCMD(i, RD_REGISTER_PCMD(i) & ~(1 << 0));
|
|
|
|
// Fail all outstanding commands.
|
|
|
|
for (uintptr_t j = 0; j < 32; j++) {
|
|
if (port->runningCommands & (1 << j)) {
|
|
port->commandContexts[j]->End(false /* failed */);
|
|
port->commandContexts[j] = nullptr;
|
|
}
|
|
}
|
|
|
|
port->runningCommands = 0;
|
|
KEventSet(&port->commandSlotsAvailable, true /* maybe already set */);
|
|
|
|
// Restart command processing.
|
|
|
|
WR_REGISTER_PSERR(i, 0xFFFFFFFF);
|
|
KTimeout timeout(5);
|
|
while ((RD_REGISTER_PCMD(i) & (1 << 15)) && !timeout.Hit());
|
|
WR_REGISTER_PCMD(i, RD_REGISTER_PCMD(i) | (1 << 0));
|
|
|
|
KSpinlockRelease(&port->commandSpinlock);
|
|
|
|
continue;
|
|
}
|
|
|
|
KSpinlockAcquire(&port->commandSpinlock);
|
|
|
|
uint32_t commandsIssued = RD_REGISTER_PCI(i);
|
|
|
|
if (i == 0) event->port0CommandsIssued = commandsIssued, event->port0CommandsRunning = port->runningCommands;
|
|
|
|
for (uintptr_t j = 0; j < 32; j++) {
|
|
if (~port->runningCommands & (1 << j)) continue; // Command not started.
|
|
if (commandsIssued & (1 << j)) continue; // Command still running.
|
|
|
|
// The command has completed.
|
|
|
|
port->commandContexts[j]->End(true /* success */);
|
|
port->commandContexts[j] = nullptr;
|
|
KEventSet(&port->commandSlotsAvailable, true /* maybe already set */);
|
|
port->runningCommands &= ~(1 << j);
|
|
|
|
commandCompleted = true;
|
|
}
|
|
|
|
KSpinlockRelease(&port->commandSpinlock);
|
|
}
|
|
|
|
if (commandCompleted) {
|
|
KSwitchThreadAfterIRQ();
|
|
}
|
|
|
|
event->complete = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
void TimeoutTimerHit(KAsyncTask *task) {
|
|
AHCIController *controller = EsContainerOf(AHCIController, timeoutTimer.asyncTask, task);
|
|
|
|
uint64_t currentTimeStamp = KGetTimeInMs();
|
|
|
|
for (uintptr_t i = 0; i < MAX_PORTS; i++) {
|
|
AHCIPort *port = controller->ports + i;
|
|
|
|
KSpinlockAcquire(&port->commandSpinlock);
|
|
|
|
for (uintptr_t j = 0; j < controller->commandSlotCount; j++) {
|
|
if ((port->runningCommands & (1 << j))
|
|
&& port->commandStartTimeStamps[j] + GENERAL_TIMEOUT < currentTimeStamp) {
|
|
KernelLog(LOG_ERROR, "AHCI", "command timeout", "Command %d on port %d timed out.\n", j, i);
|
|
|
|
port->commandContexts[j]->End(false /* failure */);
|
|
port->commandContexts[j] = nullptr;
|
|
port->runningCommands &= ~(1 << j);
|
|
|
|
// Don't set the commandSlotsAvailable event, since the controller still thinks the command is in use.
|
|
// TODO What happens if there are no commands left?
|
|
}
|
|
}
|
|
|
|
KSpinlockRelease(&port->commandSpinlock);
|
|
}
|
|
|
|
KTimerSet(&controller->timeoutTimer, GENERAL_TIMEOUT, TimeoutTimerHit, controller);
|
|
}
|
|
|
|
bool AHCIController::SendSingleCommand(uintptr_t port) {
|
|
KTimeout timeout(GENERAL_TIMEOUT);
|
|
|
|
// Wait for the port to be ready, then issue the command.
|
|
|
|
while ((RD_REGISTER_PTFD(port) & ((1 << 7) | (1 << 3))) && !timeout.Hit());
|
|
|
|
if (timeout.Hit()) {
|
|
KernelLog(LOG_ERROR, "AHCI", "port hung", "Port %d bits DRQ/BSY won't clear.\n", port);
|
|
return false;
|
|
}
|
|
|
|
__sync_synchronize();
|
|
WR_REGISTER_PCI(port, 1 << 0);
|
|
|
|
// Wait for command to complete.
|
|
|
|
bool complete = false;
|
|
|
|
while (!timeout.Hit()) {
|
|
if (~RD_REGISTER_PCI(port) & (1 << 0)) {
|
|
complete = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return complete;
|
|
}
|
|
|
|
void AHCIController::Initialise() {
|
|
// Perform BIOS/OS handoff, if necessary.
|
|
|
|
if (RD_REGISTER_CAP2() & (1 << 0)) {
|
|
KernelLog(LOG_INFO, "AHCI", "perform handoff", "Performing BIOS/OS handoff...\n");
|
|
|
|
WR_REGISTER_BOHC(RD_REGISTER_BOHC() | (1 << 1));
|
|
|
|
KTimeout timeout(25 /* ms */);
|
|
uint32_t status;
|
|
|
|
while (true) {
|
|
status = RD_REGISTER_BOHC();
|
|
if (~status & (1 << 0)) break;
|
|
if (timeout.Hit()) break;
|
|
}
|
|
|
|
if (status & (1 << 0)) {
|
|
KEvent event = {};
|
|
KernelLog(LOG_ERROR, "AHCI", "handoff error", "BIOS/OS handoff did not succeed, waiting 2 seconds to proceed...\n");
|
|
KEventWait(&event, 2000 /* ms */);
|
|
}
|
|
}
|
|
|
|
// Reset controller.
|
|
|
|
{
|
|
KTimeout timeout(GENERAL_TIMEOUT);
|
|
WR_REGISTER_GHC(RD_REGISTER_GHC() | (1 << 0));
|
|
while ((RD_REGISTER_GHC() & (1 << 0)) && !timeout.Hit());
|
|
|
|
if (timeout.Hit()) {
|
|
KernelLog(LOG_ERROR, "AHCI", "reset controller timeout", "The controller did not reset within the timeout.\n");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Register IRQ handler.
|
|
|
|
KIRQHandler handler = [] (uintptr_t, void *context) { return ((AHCIController *) context)->HandleIRQ(); };
|
|
|
|
if (!pci->EnableSingleInterrupt(handler, this, "AHCI")) {
|
|
KernelLog(LOG_ERROR, "AHCI", "IRQ registration failure", "Could not register intrrupt handler.\n");
|
|
return;
|
|
}
|
|
|
|
// Enable AHCI mode and interrupts.
|
|
|
|
WR_REGISTER_GHC(RD_REGISTER_GHC() | (1 << 31) | (1 << 1));
|
|
|
|
capabilities = RD_REGISTER_CAP();
|
|
capabilities2 = RD_REGISTER_CAP2();
|
|
commandSlotCount = ((capabilities >> 8) & 31) + 1;
|
|
dma64Supported = capabilities & (1 << 31);
|
|
|
|
#ifdef ES_BITS_64
|
|
if (!dma64Supported) {
|
|
KernelLog(LOG_ERROR, "AHCI", "controller cannot DMA", "The controller reports it cannot use 64-bit addresses in DMA transfer.\n");
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
// Work out which ports have drives connected.
|
|
|
|
size_t maximumNumberOfPorts = (capabilities & 31) + 1;
|
|
size_t portsFound = 0;
|
|
|
|
uint32_t portsImplemented = RD_REGISTER_PI();
|
|
|
|
for (uintptr_t i = 0; i < MAX_PORTS; i++) {
|
|
if (portsImplemented & (1 << i)) {
|
|
portsFound++;
|
|
|
|
if (portsFound <= maximumNumberOfPorts) {
|
|
ports[i].connected = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
KernelLog(LOG_INFO, "AHCI", "information", "Capabilities: %x, %x. Command slot count: %d. Implemented ports: %x.\n",
|
|
capabilities, capabilities2, commandSlotCount, portsImplemented);
|
|
|
|
// Setup the command lists, FISes and command tables.
|
|
|
|
for (uintptr_t i = 0; i < MAX_PORTS; i++) {
|
|
if (!ports[i].connected) continue;
|
|
|
|
size_t bytesNeeded = COMMAND_LIST_SIZE + RECEIVED_FIS_SIZE + COMMAND_TABLE_SIZE * commandSlotCount;
|
|
|
|
uint8_t *virtualAddress;
|
|
uintptr_t physicalAddress;
|
|
|
|
if (!MMPhysicalAllocateAndMap(bytesNeeded, K_PAGE_SIZE, dma64Supported ? 64 : 32, true, MM_REGION_NOT_CACHEABLE, &virtualAddress, &physicalAddress)) {
|
|
KernelLog(LOG_ERROR, "AHCI", "allocation failure", "Could not allocate physical memory for port %d.\n", i);
|
|
break;
|
|
}
|
|
|
|
ports[i].commandList = (uint32_t *) virtualAddress;
|
|
ports[i].commandTables = virtualAddress + COMMAND_LIST_SIZE + RECEIVED_FIS_SIZE;
|
|
|
|
// Set the registers to the physical addresses.
|
|
|
|
WR_REGISTER_PCLB(i, ES_PTR64_LS32(physicalAddress));
|
|
WR_REGISTER_PFB(i, ES_PTR64_LS32(physicalAddress + 0x400));
|
|
if (dma64Supported) WR_REGISTER_PCLBU(i, ES_PTR64_MS32(physicalAddress));
|
|
if (dma64Supported) WR_REGISTER_PFBU(i, ES_PTR64_MS32(physicalAddress + 0x400));
|
|
|
|
// Point each command list entry to the corresponding command table.
|
|
|
|
uint32_t *commandList = ports[i].commandList;
|
|
|
|
for (uintptr_t j = 0; j < commandSlotCount; j++) {
|
|
uintptr_t address = physicalAddress + COMMAND_LIST_SIZE + RECEIVED_FIS_SIZE + COMMAND_TABLE_SIZE * j;
|
|
commandList[j * 8 + 2] = ES_PTR64_LS32(address);
|
|
commandList[j * 8 + 3] = ES_PTR64_MS32(address);
|
|
}
|
|
|
|
// Reset the port.
|
|
|
|
KTimeout timeout(GENERAL_TIMEOUT);
|
|
|
|
const uint32_t runningBits = ((1 << 0 /* start */) | (1 << 4 /* receive FIS enable */)
|
|
| (1 << 15 /* command list running */) | (1 << 14 /* receive FIS running */));
|
|
|
|
while (true) {
|
|
uint32_t status = RD_REGISTER_PCMD(i);
|
|
|
|
if (!(status & runningBits) || timeout.Hit()) {
|
|
break;
|
|
}
|
|
|
|
// Stop command list processing and receive FIS.
|
|
WR_REGISTER_PCMD(i, status & ~((1 << 0) | (1 << 4)));
|
|
}
|
|
|
|
if (RD_REGISTER_PCMD(i) & runningBits) {
|
|
KernelLog(LOG_ERROR, "AHCI", "reset port timeout", "Resetting port %d timed out. PCMD: %x.\n",
|
|
i, RD_REGISTER_PCMD(i));
|
|
ports[i].connected = false;
|
|
continue;
|
|
}
|
|
|
|
// Clear IRQs.
|
|
|
|
WR_REGISTER_PIE(i, RD_REGISTER_PIE(i) & 0x0E3FFF00);
|
|
WR_REGISTER_PIS(i, RD_REGISTER_PIS(i));
|
|
|
|
// Enable receive FIS and activate the drive.
|
|
|
|
WR_REGISTER_PSCTL(i, RD_REGISTER_PSCTL(i) | (3 << 8)); // Disable transitions to partial and slumber states.
|
|
WR_REGISTER_PCMD(i, (RD_REGISTER_PCMD(i) & 0x0FFFFFFF)
|
|
| (1 << 1 /* spin up */) | (1 << 2 /* power on */) | (1 << 4 /* FIS receive */) | (1 << 28 /* activate */));
|
|
|
|
KTimeout linkTimeout(10);
|
|
|
|
while ((RD_REGISTER_PSSTS(i) & 0x0F) != 3 && !linkTimeout.Hit());
|
|
|
|
if ((RD_REGISTER_PSSTS(i) & 0x0F) != 3) {
|
|
KernelLog(LOG_ERROR, "AHCI", "activate port timeout", "Activating port %d timed out. PSSTS: %x.\n",
|
|
i, RD_REGISTER_PSSTS(i));
|
|
ports[i].connected = false;
|
|
continue;
|
|
}
|
|
|
|
// Clear errors.
|
|
|
|
WR_REGISTER_PSERR(i, RD_REGISTER_PSERR(i));
|
|
|
|
// Wait for device to be ready.
|
|
|
|
while ((RD_REGISTER_PTFD(i) & 0x88 /* BSY and DRQ */) && !timeout.Hit());
|
|
|
|
if (RD_REGISTER_PTFD(i) & 0x88) {
|
|
KernelLog(LOG_ERROR, "AHCI", "port ready timeout", "Port %d hung at busy state. PTFD: %x.\n",
|
|
i, RD_REGISTER_PTFD(i));
|
|
ports[i].connected = false;
|
|
continue;
|
|
}
|
|
|
|
// Start command list processing.
|
|
|
|
KernelLog(LOG_INFO, "AHCI", "start command processing", "Starting command processing for port %d...\n", i);
|
|
WR_REGISTER_PCMD(i, RD_REGISTER_PCMD(i) | (1 << 0));
|
|
|
|
// Enable interrupts.
|
|
|
|
KernelLog(LOG_INFO, "AHCI", "enable interrupts", "Enabling interrupts for port %d...\n", i);
|
|
WR_REGISTER_PIE(i, RD_REGISTER_PIE(i) | (1 << 5) /* descriptor complete */ | (1 << 0) /* D2H */
|
|
| (1 << 30) | (1 << 29) | (1 << 28) | (1 << 27) | (1 << 26) | (1 << 24) | (1 << 23) /* errors */);
|
|
}
|
|
|
|
// Read the status and signature for each implemented port to work out if it is connected.
|
|
|
|
for (uintptr_t i = 0; i < MAX_PORTS; i++) {
|
|
if (!ports[i].connected) continue;
|
|
|
|
uint32_t status = RD_REGISTER_PSSTS(i);
|
|
|
|
if ((status & 0x00F) != 0x003 || (status & 0x0F0) == 0x000 || (status & 0xF00) != 0x100) {
|
|
ports[i].connected = false;
|
|
KernelLog(LOG_INFO, "AHCI", "no drive", "No drive connected to port %d (1).\n", i);
|
|
continue;
|
|
}
|
|
|
|
uint32_t signature = RD_REGISTER_PSIG(i);
|
|
|
|
if (signature == 0x00000101) {
|
|
// SATA drive.
|
|
KernelLog(LOG_INFO, "AHCI", "found drive", "Found SATA drive on port %d.\n", i);
|
|
} else if (signature == 0xEB140101) {
|
|
// SATAPI drive.
|
|
ports[i].atapi = true;
|
|
KernelLog(LOG_INFO, "AHCI", "found drive", "Found SATAPI drive on port %d.\n", i);
|
|
} else if (!signature) {
|
|
// No drive connected.
|
|
ports[i].connected = false;
|
|
KernelLog(LOG_INFO, "AHCI", "no drive", "No drive connected to port %d (2).\n", i);
|
|
} else {
|
|
KernelLog(LOG_ERROR, "AHCI", "unrecognised drive signature", "Unrecognised drive signature %x on port %d.\n", signature, i);
|
|
ports[i].connected = false;
|
|
}
|
|
}
|
|
|
|
// Identify each connected drive.
|
|
|
|
uint16_t *identifyData;
|
|
uintptr_t identifyDataPhysical;
|
|
|
|
if (!MMPhysicalAllocateAndMap(0x200, K_PAGE_SIZE, dma64Supported ? 64 : 32, true, MM_REGION_NOT_CACHEABLE, (uint8_t **) &identifyData, &identifyDataPhysical)) {
|
|
KernelLog(LOG_ERROR, "AHCI", "allocation failure", "Could not allocate physical memory for identify data buffer.\n");
|
|
return;
|
|
}
|
|
|
|
KernelLog(LOG_INFO, "AHCI", "identify data", "Identify data buffer allocated at physical address %x.\n", identifyDataPhysical);
|
|
|
|
for (uintptr_t i = 0; i < MAX_PORTS; i++) {
|
|
if (!ports[i].connected) continue;
|
|
|
|
EsMemoryZero(identifyData, 0x200);
|
|
|
|
// Setup the command list entry.
|
|
|
|
ports[i].commandList[0] = 5 /* FIS is 5 DWORDs */ | (1 << 16) /* 1 PRDT entry */;
|
|
ports[i].commandList[1] = 0;
|
|
|
|
// Setup the command FIS.
|
|
|
|
uint8_t opcode = ports[i].atapi ? 0xA1 /* IDENTIFY PACKET */ : 0xEC /* IDENTIFY */;
|
|
uint32_t *commandFIS = (uint32_t *) ports[i].commandTables;
|
|
commandFIS[0] = 0x27 /* H2D */ | (1 << 15) /* command */ | (opcode << 16);
|
|
commandFIS[1] = commandFIS[2] = commandFIS[3] = commandFIS[4] = 0;
|
|
|
|
// Setup the PRDT.
|
|
|
|
uint32_t *prdt = (uint32_t *) (ports[i].commandTables + 0x80);
|
|
prdt[0] = ES_PTR64_LS32(identifyDataPhysical);
|
|
prdt[1] = ES_PTR64_MS32(identifyDataPhysical);
|
|
prdt[2] = 0;
|
|
prdt[3] = 0x200 - 1;
|
|
|
|
KernelLog(LOG_INFO, "AHCI", "identifying drive", "Sending IDENTIFY command to port %d...\n", i);
|
|
|
|
if (!SendSingleCommand(i)) {
|
|
KernelLog(LOG_ERROR, "AHCI", "identify failure", "Could not read identify data for port %d.\n", i);
|
|
WR_REGISTER_PCMD(i, RD_REGISTER_PCMD(i) & ~(1 << 0)); // Stop command processing.
|
|
ports[i].connected = false;
|
|
continue;
|
|
}
|
|
|
|
ports[i].sectorBytes = 0x200;
|
|
|
|
if ((identifyData[106] & (1 << 14)) && (~identifyData[106] & (1 << 15)) && (identifyData[106] & (1 << 12))) {
|
|
// Device has a logical sector size larger than 0x200 bytes.
|
|
ports[i].sectorBytes = (uint32_t) identifyData[117] | ((uint32_t) identifyData[118] << 16);
|
|
}
|
|
|
|
ports[i].sectorCount = ((uint64_t) identifyData[100] << 0) + ((uint64_t) identifyData[101] << 16)
|
|
+ ((uint64_t) identifyData[102] << 32) + ((uint64_t) identifyData[103] << 48);
|
|
|
|
if (!((identifyData[49] & (1 << 9)) && (identifyData[49] & (1 << 8)))) {
|
|
KernelLog(LOG_ERROR, "AHCI", "unsupported feature", "Drive on port %d does not support a required feature.\n", i);
|
|
ports[i].connected = false;
|
|
continue;
|
|
}
|
|
|
|
if (ports[i].atapi) {
|
|
// Send a read capacity command.
|
|
|
|
ports[i].commandList[0] = 5 /* FIS is 5 DWORDs */ | (1 << 16) /* 1 PRDT entry */ | (1 << 5) /* ATAPI */;
|
|
commandFIS[0] = 0x27 /* H2D */ | (1 << 15) /* command */ | (0xA0 /* packet */ << 16);
|
|
commandFIS[1] = 8 /* maximum byte count transfer */ << 8;
|
|
prdt[3] = 8 - 1;
|
|
|
|
uint8_t *scsiCommand = (uint8_t *) commandFIS + 0x40;
|
|
EsMemoryZero(scsiCommand, 10);
|
|
scsiCommand[0] = 0x25 /* READ CAPACITY (10) */;
|
|
|
|
if (!SendSingleCommand(i)) {
|
|
KernelLog(LOG_ERROR, "AHCI", "identify failure", "Could not read SCSI read capacity data for port %d.\n", i);
|
|
WR_REGISTER_PCMD(i, RD_REGISTER_PCMD(i) & ~(1 << 0)); // Stop command processing.
|
|
ports[i].connected = false;
|
|
continue;
|
|
}
|
|
|
|
uint8_t *capacity = (uint8_t *) identifyData;
|
|
|
|
ports[i].sectorCount = (((uint64_t) capacity[3] << 0) + ((uint64_t) capacity[2] << 8)
|
|
+ ((uint64_t) capacity[1] << 16) + ((uint64_t) capacity[0] << 24)) + 1;
|
|
ports[i].sectorBytes = ((uint64_t) capacity[7] << 0) + ((uint64_t) capacity[6] << 8)
|
|
+ ((uint64_t) capacity[5] << 16) + ((uint64_t) capacity[4] << 24);
|
|
}
|
|
|
|
if (ports[i].sectorCount <= 128 || (ports[i].sectorBytes & 0x1FF) || !ports[i].sectorBytes || ports[i].sectorBytes > 0x1000) {
|
|
KernelLog(LOG_ERROR, "AHCI", "unsupported feature", "Drive on port %d has invalid sector configuration (count: %d, size: %D).\n",
|
|
i, ports[i].sectorCount, ports[i].sectorBytes);
|
|
ports[i].connected = false;
|
|
continue;
|
|
}
|
|
|
|
for (uintptr_t j = 0; j < 20; j++) {
|
|
ports[i].model[j * 2 + 0] = identifyData[27 + j] >> 8;
|
|
ports[i].model[j * 2 + 1] = identifyData[27 + j] & 0xFF;
|
|
}
|
|
|
|
ports[i].model[40] = 0;
|
|
|
|
for (uintptr_t j = 39; j > 0; j--) {
|
|
if (ports[i].model[j] == ' ') {
|
|
ports[i].model[j] = 0;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
ports[i].ssd = identifyData[217] == 1;
|
|
|
|
for (uintptr_t i = 10; i < 20; i++) identifyData[i] = (identifyData[i] >> 8) | (identifyData[i] << 8);
|
|
for (uintptr_t i = 23; i < 27; i++) identifyData[i] = (identifyData[i] >> 8) | (identifyData[i] << 8);
|
|
for (uintptr_t i = 27; i < 47; i++) identifyData[i] = (identifyData[i] >> 8) | (identifyData[i] << 8);
|
|
|
|
KernelLog(LOG_INFO, "AHCI", "identified drive", "Identified drive on port %d; serial - '%s', firmware - '%s', model - '%s', sector size - %D, sector count - %d.\n",
|
|
i, 20, identifyData + 10, 8, identifyData + 23, 40, identifyData + 27, ports[i].sectorBytes, ports[i].sectorCount);
|
|
}
|
|
|
|
MMFree(MMGetKernelSpace(), identifyData);
|
|
MMPhysicalFree(identifyDataPhysical);
|
|
|
|
// Start the timeout timer.
|
|
|
|
KTimerSet(&timeoutTimer, GENERAL_TIMEOUT, TimeoutTimerHit, this);
|
|
|
|
// Register drives.
|
|
|
|
for (uintptr_t i = 0; i < MAX_PORTS; i++) {
|
|
if (!ports[i].connected) continue;
|
|
|
|
AHCIDrive *device = (AHCIDrive *) KDeviceCreate("AHCI drive", this, sizeof(AHCIDrive));
|
|
|
|
if (!device) {
|
|
KernelLog(LOG_ERROR, "AHCI", "allocation failure", "Could not create device for port %d.\n", i);
|
|
break;
|
|
}
|
|
|
|
device->controller = this;
|
|
device->port = i;
|
|
|
|
device->information.sectorSize = ports[i].sectorBytes;
|
|
device->information.sectorCount = ports[i].sectorCount;
|
|
device->maxAccessSectorCount = ports[i].atapi ? (65535 / device->information.sectorSize)
|
|
: ((PRDT_ENTRY_COUNT - 1 /* need one extra if not page aligned */) * K_PAGE_SIZE / device->information.sectorSize);
|
|
device->information.readOnly = ports[i].atapi;
|
|
EsAssert(sizeof(ports[i].model) <= sizeof(device->information.model));
|
|
EsMemoryCopy(device->information.model, ports[i].model, sizeof(ports[i].model));
|
|
device->information.modelBytes = sizeof(ports[i].model);
|
|
device->information.driveType = ports[i].atapi ? ES_DRIVE_TYPE_CDROM : ports[i].ssd ? ES_DRIVE_TYPE_SSD : ES_DRIVE_TYPE_HDD;
|
|
|
|
device->access = [] (KBlockDeviceAccessRequest request) {
|
|
AHCIDrive *drive = (AHCIDrive *) request.device;
|
|
|
|
request.dispatchGroup->Start();
|
|
|
|
if (!drive->controller->Access(drive->port, request.offset, request.count, request.operation,
|
|
request.buffer, request.flags, request.dispatchGroup)) {
|
|
request.dispatchGroup->End(false);
|
|
}
|
|
};
|
|
|
|
FSRegisterBlockDevice(device);
|
|
}
|
|
}
|
|
|
|
static void DeviceAttach(KDevice *_parent) {
|
|
KPCIDevice *parent = (KPCIDevice *) _parent;
|
|
|
|
AHCIController *device = (AHCIController *) KDeviceCreate("AHCI controller", parent, sizeof(AHCIController));
|
|
if (!device) return;
|
|
device->pci = parent;
|
|
|
|
device->dumpState = [] (KDevice *device) {
|
|
((AHCIController *) device)->DumpState();
|
|
};
|
|
|
|
// Enable PCI features.
|
|
parent->EnableFeatures(K_PCI_FEATURE_INTERRUPTS
|
|
| K_PCI_FEATURE_BUSMASTERING_DMA
|
|
| K_PCI_FEATURE_MEMORY_SPACE_ACCESS
|
|
| K_PCI_FEATURE_BAR_5);
|
|
|
|
// Initialise the controller.
|
|
device->Initialise();
|
|
}
|
|
|
|
KDriver driverAHCI = {
|
|
.attach = DeviceAttach,
|
|
};
|