// ide.c — IDE/ATA driver: PIO (sync fallback) + Bus Master DMA (async) // // Architecture // ------------ // The DMA path mirrors AHCI exactly: // SubmitIrp → enqueues IRP + signals gIrpSemaphore → returns STATUS_PENDING // IdeWorker → dequeues IRP, calls IdeDmaRw(), fires completion hooks // IdeIsr → clears BM status, signals gDmaEvent to unblock the worker // // Graceful degradation: if Bus Master init fails (no PCI IDE controller, // BAR4 not assigned, PRDT allocation above 4 GB, etc.) SubmitIrp is left // NULL. HalDiskDispatch sees NULL and falls back to the synchronous PIO // path with no changes at the call sites. // // PRDT layout // ----------- // One page allocated via MmAllocateContiguous. We build up to PRDT_MAX_ENTRIES // entries by walking the transfer buffer in page-sized chunks and looking up // each page's physical address via MmGetPhysicalAddress. Adjacent physical // pages are coalesced into a single PRD entry where possible. // // LBA48 vs LBA28 // -------------- // DMA commands exist in both modes: CMD_READ/WRITE_DMA_EXT (0x25/0x35) for // LBA48 and CMD_READ/WRITE_DMA (0xC8/0xCA) for LBA28. The existing // gSupportsLba48 flag picks the right variant. #include #include #include #include #include #include #include #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // IDE register map (primary channel, base 0x1F0) // --------------------------------------------------------------------------- #define IDE_DATA 0x1F0 #define IDE_ERR 0x1F1 #define IDE_SECCNT 0x1F2 #define IDE_LBA0 0x1F3 #define IDE_LBA1 0x1F4 #define IDE_LBA2 0x1F5 #define IDE_DRIVE 0x1F6 #define IDE_STATUS 0x1F7 #define IDE_CMD 0x1F7 #define IDE_ALT 0x3F6 #define IDE_BUSY 0x80 #define IDE_READY 0x40 #define IDE_ERR_F 0x01 #define IDE_DRQ 0x08 // ATA commands #define CMD_READ_EXT 0x24 // PIO 48-bit read #define CMD_WRITE_EXT 0x34 // PIO 48-bit write #define CMD_READ_DMA_EXT 0x25 // DMA 48-bit read #define CMD_WRITE_DMA_EXT 0x35 // DMA 48-bit write #define CMD_READ_DMA 0xC8 // DMA 28-bit read #define CMD_WRITE_DMA 0xCA // DMA 28-bit write #define CMD_IDENT 0xEC // IDE primary IRQ #define IDE_IRQ_PRIMARY 14 // --------------------------------------------------------------------------- // Bus Master IDE register offsets (relative to gBmBase, primary channel) // --------------------------------------------------------------------------- #define BM_CMD_REG 0x00 // command #define BM_STATUS_REG 0x02 // status #define BM_PRDT_REG 0x04 // PRDT physical address (32-bit) // BM_CMD_REG bits #define BM_CMD_START 0x01 // start DMA transfer #define BM_CMD_READ 0x08 // 1 = disk→memory (read), 0 = memory→disk (write) // BM_STATUS_REG bits #define BM_STATUS_ACTIVE 0x01 // DMA active #define BM_STATUS_ERROR 0x02 // DMA error (write 1 to clear) #define BM_STATUS_IRQ 0x04 // interrupt pending (write 1 to clear) // --------------------------------------------------------------------------- // Physical Region Descriptor (PRDT entry, 8 bytes) // The PRDT itself must not cross a 64 KB boundary and must be 4-byte aligned. // We allocate a full page so both constraints are trivially satisfied. // --------------------------------------------------------------------------- #pragma pack(push, 1) typedef struct { uint32_t physAddr; // physical base of this memory region uint16_t byteCount; // byte count; 0 means 64 KB uint16_t flags; // bit 15: End Of Table } PRD; #pragma pack(pop) #define PRD_EOT 0x8000u // 512 entries × 8 bytes = 4096 bytes = exactly one page; supports 512×64 KB = 32 MB #define PRDT_MAX_ENTRIES 512 // --------------------------------------------------------------------------- // Module state // --------------------------------------------------------------------------- static const HAL_API *gHal; static DISK_DRIVER gDrv; static uint64_t gSectorCount = 0; static bool gSupportsLba48 = false; // DMA hardware state static uint16_t gBmBase = 0; // Bus Master I/O base port (BAR4 & ~3) static PRD *gPrdtVirt = NULL; // kernel VA of PRDT page static uint32_t gPrdtPhys = 0; // 32-bit physical address of PRDT // Async worker synchronisation static IRP_QUEUE gIrpQueue; static SEMAPHORE *gIrpSemaphore = NULL; static EVENT *gDmaEvent = NULL; // auto-reset; ISR signals after each xfer // --------------------------------------------------------------------------- // Bus Master I/O helpers // --------------------------------------------------------------------------- static inline uint8_t BmRead8 (uint8_t reg) { return __inbyte (gBmBase + reg); } static inline void BmWrite8 (uint8_t reg, uint8_t v) { __outbyte (gBmBase + reg, v); } static inline void BmWrite32(uint8_t reg, uint32_t v) { __outdword(gBmBase + reg, v); } // --------------------------------------------------------------------------- // PIO polling helpers (unchanged) // --------------------------------------------------------------------------- static void IdeWait(void) { while (__inbyte(IDE_STATUS) & IDE_BUSY) KeYield(); } static KSTATUS IdeWaitData(void) { for (int i = 0; i < 4; i++) __inbyte(IDE_ALT); // 400 ns delay while (true) { uint8_t st = __inbyte(IDE_STATUS); if (st & IDE_ERR_F) return STATUS_DEVICE_ERROR; if (!(st & IDE_BUSY) && (st & IDE_DRQ)) return STATUS_SUCCESS; KeYield(); } } // --------------------------------------------------------------------------- // PIO read/write — sync fallback when DMA is unavailable // --------------------------------------------------------------------------- static KSTATUS IdeRead(void *ctx, uint64_t lba, uint32_t count, void *buf) { (void)ctx; if (lba + count > gSectorCount) return STATUS_INVALID_PARAMETER; uint16_t *p = (uint16_t *)buf; for (uint32_t i = 0; i < count; i++, lba++) { IdeWait(); if (gSupportsLba48) { __outbyte(IDE_DRIVE, 0x40); __outbyte(IDE_SECCNT, 0); __outbyte(IDE_LBA0, (lba >> 24) & 0xFF); __outbyte(IDE_LBA1, (lba >> 32) & 0xFF); __outbyte(IDE_LBA2, (lba >> 40) & 0xFF); __outbyte(IDE_SECCNT, 1); __outbyte(IDE_LBA0, (uint8_t)lba); __outbyte(IDE_LBA1, (uint8_t)(lba >> 8)); __outbyte(IDE_LBA2, (uint8_t)(lba >> 16)); __outbyte(IDE_CMD, CMD_READ_EXT); } else { __outbyte(IDE_DRIVE, 0xE0 | ((lba >> 24) & 0xF)); __outbyte(IDE_SECCNT, 1); __outbyte(IDE_LBA0, (uint8_t)lba); __outbyte(IDE_LBA1, (uint8_t)(lba >> 8)); __outbyte(IDE_LBA2, (uint8_t)(lba >> 16)); __outbyte(IDE_CMD, 0x20); } if (IdeWaitData() != STATUS_SUCCESS) return STATUS_DEVICE_ERROR; for (int w = 0; w < 256; w++) *p++ = __inword(IDE_DATA); } return STATUS_SUCCESS; } static KSTATUS IdeWrite(void *ctx, uint64_t lba, uint32_t count, const void *buf) { (void)ctx; if (lba + count > gSectorCount) return STATUS_INVALID_PARAMETER; const uint16_t *p = (const uint16_t *)buf; for (uint32_t i = 0; i < count; i++, lba++) { IdeWait(); if (gSupportsLba48) { __outbyte(IDE_DRIVE, 0x40); __outbyte(IDE_SECCNT, 0); __outbyte(IDE_LBA0, (lba >> 24) & 0xFF); __outbyte(IDE_LBA1, (lba >> 32) & 0xFF); __outbyte(IDE_LBA2, (lba >> 40) & 0xFF); __outbyte(IDE_SECCNT, 1); __outbyte(IDE_LBA0, (uint8_t)lba); __outbyte(IDE_LBA1, (uint8_t)(lba >> 8)); __outbyte(IDE_LBA2, (uint8_t)(lba >> 16)); __outbyte(IDE_CMD, CMD_WRITE_EXT); } else { __outbyte(IDE_DRIVE, 0xE0 | ((lba >> 24) & 0xF)); __outbyte(IDE_SECCNT, 1); __outbyte(IDE_LBA0, (uint8_t)lba); __outbyte(IDE_LBA1, (uint8_t)(lba >> 8)); __outbyte(IDE_LBA2, (uint8_t)(lba >> 16)); __outbyte(IDE_CMD, 0x30); } IdeWait(); for (int w = 0; w < 256; w++) __outword(IDE_DATA, *p++); __outbyte(IDE_ALT, 0); // flush write cache if (IdeWaitData() != STATUS_SUCCESS) return STATUS_DEVICE_ERROR; } return STATUS_SUCCESS; } // --------------------------------------------------------------------------- // BuildPrdt — fill gPrdtVirt[] for a transfer. // // Walks the virtual buffer in page-aligned chunks, looks up each chunk's // physical address via MmGetPhysicalAddress, and coalesces adjacent physical // pages into a single PRD entry where possible. This is correct regardless of // whether the pool gave us physically contiguous pages. // // Constraints enforced: // - Each PRD entry covers at most one 4 KB physical page (never crosses // a page boundary), so the physical address is valid for the whole chunk. // - Total byte count per entry is ≤ 64 KB (the PRD limit). // - Physical address must fit in 32 bits (BM IDE hardware limit). // // Returns the number of PRD entries written, or 0 on error. // --------------------------------------------------------------------------- static uint32_t BuildPrdt(void *virtBuf, uint32_t byteLen) { uint32_t nPrd = 0; uintptr_t va = (uintptr_t)virtBuf; uint32_t remain = byteLen; while (remain > 0) { if (nPrd >= PRDT_MAX_ENTRIES) { DbgPrint("IDE DMA: transfer too large for PRDT (%u entries)\n", nPrd); return 0; } // Bytes from va to the end of its 4 KB page (never straddles a page). uint32_t pageOff = (uint32_t)(va & 0xFFF); uint32_t inPage = 0x1000 - pageOff; uint32_t chunk = (remain < inPage) ? remain : inPage; PHYSICAL_ADDRESS phys = MmGetPhysicalAddress(va); if (phys >> 32) { // Physical address above 4 GB — BM IDE's 32-bit PRDT can't reach it. DbgPrint("IDE DMA: buffer page at VA 0x%llx has phys 0x%llx > 4 GB\n", (uint64_t)va, (uint64_t)phys); return 0; } uint32_t phys32 = (uint32_t)phys; // Try to coalesce with the previous PRD entry if the physical regions // are adjacent and the combined size stays within the 64 KB limit. if (nPrd > 0) { PRD *prev = &gPrdtVirt[nPrd - 1]; uint32_t prevSz = prev->byteCount ? prev->byteCount : 0x10000u; uint32_t prevEnd = prev->physAddr + prevSz; uint32_t combined = prevSz + chunk; if (prevEnd == phys32 && combined <= 0x10000u) { prev->byteCount = (uint16_t)(combined == 0x10000u ? 0u : combined); va += chunk; remain -= chunk; continue; } } gPrdtVirt[nPrd].physAddr = phys32; gPrdtVirt[nPrd].byteCount = (uint16_t)(chunk == 0x10000u ? 0u : chunk); gPrdtVirt[nPrd].flags = 0; nPrd++; va += chunk; remain -= chunk; } if (nPrd > 0) gPrdtVirt[nPrd - 1].flags |= PRD_EOT; return nPrd; } // --------------------------------------------------------------------------- // IDE ISR (IRQ 14 — primary channel) // // Must be fast — runs with interrupts disabled. We only clear the hardware // interrupt bits here and signal gDmaEvent to wake the worker thread. // The worker reads the final status and fires IRP completion hooks. // --------------------------------------------------------------------------- static void IdeIsr(void) { uint8_t bmSt = BmRead8(BM_STATUS_REG); if (!(bmSt & BM_STATUS_IRQ)) return; // not our interrupt (spurious or secondary channel) // Stop the Bus Master engine. BmWrite8(BM_CMD_REG, 0x00); // Clear interrupt and error sticky bits (write-1-to-clear). BmWrite8(BM_STATUS_REG, BM_STATUS_IRQ | BM_STATUS_ERROR); // Read IDE status and alt-status to fully acknowledge the device interrupt. (void)__inbyte(IDE_ALT); (void)__inbyte(IDE_STATUS); // Wake the worker thread; it will read the final BM status and complete // the IRP. gDmaEvent is auto-reset so exactly one waiter is released. KeSignalEvent(gDmaEvent); } // --------------------------------------------------------------------------- // IdeDmaRw — program the hardware and block until the ISR completes it. // // Called exclusively from IdeWorker. Has full control of the IDE/BM // registers; no other thread touches them while the worker is running. // Falls back to PIO inline if BuildPrdt fails (address space issue). // --------------------------------------------------------------------------- static KSTATUS IdeDmaRw(uint64_t lba, uint32_t count, void *buf, bool write) { uint32_t byteLen = count * 512u; uint32_t nPrd = BuildPrdt(buf, byteLen); if (nPrd == 0) { // Graceful inline PIO fallback for awkward buffers. DbgPrint("IDE DMA: BuildPrdt failed — falling back to PIO\n"); return write ? IdeWrite(NULL, lba, count, buf) : IdeRead (NULL, lba, count, buf); } // ---- Program Bus Master ----------------------------------------------- // Load PRDT physical address. BmWrite32(BM_PRDT_REG, gPrdtPhys); // Clear any leftover interrupt/error bits from the previous transfer. BmWrite8(BM_STATUS_REG, BM_STATUS_IRQ | BM_STATUS_ERROR); // Set direction: BM_CMD_READ = disk→memory; 0 = memory→disk. // Do NOT set BM_CMD_START yet — spec says program ATA regs first. BmWrite8(BM_CMD_REG, write ? 0x00 : BM_CMD_READ); // ---- Program ATA registers ------------------------------------------- IdeWait(); if (gSupportsLba48) { __outbyte(IDE_DRIVE, 0x40); __outbyte(IDE_SECCNT, (uint8_t)(count >> 8)); // sector count high byte __outbyte(IDE_LBA0, (uint8_t)(lba >> 24)); __outbyte(IDE_LBA1, (uint8_t)(lba >> 32)); __outbyte(IDE_LBA2, (uint8_t)(lba >> 40)); __outbyte(IDE_SECCNT, (uint8_t)count); // sector count low byte __outbyte(IDE_LBA0, (uint8_t)lba); __outbyte(IDE_LBA1, (uint8_t)(lba >> 8)); __outbyte(IDE_LBA2, (uint8_t)(lba >> 16)); __outbyte(IDE_CMD, write ? CMD_WRITE_DMA_EXT : CMD_READ_DMA_EXT); } else { __outbyte(IDE_DRIVE, 0xE0 | ((lba >> 24) & 0xF)); __outbyte(IDE_SECCNT, (uint8_t)count); __outbyte(IDE_LBA0, (uint8_t)lba); __outbyte(IDE_LBA1, (uint8_t)(lba >> 8)); __outbyte(IDE_LBA2, (uint8_t)(lba >> 16)); __outbyte(IDE_CMD, write ? CMD_WRITE_DMA : CMD_READ_DMA); } // ---- Start DMA -------------------------------------------------------- // Set Start bit. Direction bit is already set from above. BmWrite8(BM_CMD_REG, (write ? 0x00 : BM_CMD_READ) | BM_CMD_START); // Block the worker thread until the ISR fires. KeWaitForEvent(gDmaEvent); // ---- Read result ------------------------------------------------------- // ISR already stopped the BM engine and cleared BM_STATUS_IRQ. // We still need to check BM_STATUS_ERROR and IDE_ERR_F. uint8_t bmSt = BmRead8(BM_STATUS_REG); uint8_t ideSt = __inbyte(IDE_STATUS); if (bmSt & BM_STATUS_ERROR) { DbgPrint("IDE DMA: BM error (bmStatus=0x%02x, ideStatus=0x%02x, ideErr=0x%02x)\n", bmSt, ideSt, __inbyte(IDE_ERR)); return STATUS_DEVICE_ERROR; } if (ideSt & IDE_ERR_F) { DbgPrint("IDE DMA: ATA error (ideStatus=0x%02x, ideErr=0x%02x)\n", ideSt, __inbyte(IDE_ERR)); return STATUS_DEVICE_ERROR; } return STATUS_SUCCESS; } // --------------------------------------------------------------------------- // IdeWorker — kernel thread, one per IDE controller. // // Blocks on gIrpSemaphore. Each IrpQueueEnqueue increments it by one, so // the worker wakes once per submitted IRP. Mirrors AhciWorker exactly. // --------------------------------------------------------------------------- static void IdeWorker(uintptr_t arg) { (void)arg; while (true) { KeWaitSemaphore(gIrpSemaphore); IRP *irp = IrpQueueDequeue(&gIrpQueue); if (!irp) KePanic("IdeWorker: semaphore fired but IRP queue is empty — accounting bug\n"); if (irp->Lba + irp->SectorCount > gSectorCount) { irp->Status = STATUS_INVALID_PARAMETER; } else { bool write = !!(irp->Flags & IRP_FLAG_WRITE); irp->Status = IdeDmaRw(irp->Lba, irp->SectorCount, irp->Buffer, write); } if (irp->CompletionEvent) KeSignalEvent(irp->CompletionEvent); if (irp->CompletionFn) irp->CompletionFn(irp, irp->CompletionCtx); } } // --------------------------------------------------------------------------- // IdeSubmitIrp — async entry point wired into DISK_DRIVER.SubmitIrp. // Enqueues the IRP and immediately returns STATUS_PENDING. // --------------------------------------------------------------------------- static KSTATUS IdeSubmitIrp(void *ctx, IRP *irp) { (void)ctx; IrpQueueEnqueue(&gIrpQueue, irp); return STATUS_PENDING; } // --------------------------------------------------------------------------- // IdeDmaInit — locate the PCI IDE controller, grab BAR4, allocate the PRDT, // create sync objects, and register IRQ 14. // // Returns true on success. On any failure, logs a message and returns false; // DriverEntry then leaves DISK_DRIVER.SubmitIrp = NULL so the dispatch layer // falls back to the synchronous PIO path automatically. // --------------------------------------------------------------------------- static bool IdeDmaInit(void) { // ---- Find PCI IDE controller (class 0x01, subclass 0x01) -------------- uint8_t bus, dev, fn; if (!gHal->Pci.FindClass(0x01, 0x01, &bus, &dev, &fn)) { DbgPrint("IDE DMA: No PCI IDE controller found — using PIO\n"); return false; } // ---- Read BAR4 (Bus Master base, I/O space) --------------------------- // PCI config offset 0x20. Bit 0 must be 1 for I/O BAR. uint32_t bar4 = gHal->Pci.Read32(bus, dev, fn, 0x20); if (!(bar4 & 0x1)) { DbgPrint("IDE DMA: BAR4 is a memory BAR (0x%08x) — unexpected, using PIO\n", bar4); return false; } uint16_t bmBase = (uint16_t)(bar4 & 0xFFFC); if (bmBase == 0) { DbgPrint("IDE DMA: BAR4 is zero (firmware didn't assign it) — using PIO\n"); return false; } DbgPrint("IDE DMA: Bus Master base = 0x%04X (bus %u, dev %u, fn %u)\n", bmBase, bus, dev, fn); // ---- Enable Bus Mastering in PCI Command register (bit 2) ------------ uint32_t pciCmd = gHal->Pci.Read32(bus, dev, fn, 0x04); gHal->Pci.Write32(bus, dev, fn, 0x04, pciCmd | (1u << 2)); // ---- Allocate PRDT page (must be 32-bit physical, 4-byte aligned) ---- PHYSICAL_ADDRESS prdtPhys = MmAllocateContiguous(1); if (!prdtPhys) { DbgPrint("IDE DMA: Failed to allocate PRDT page\n"); return false; } if (prdtPhys >> 32) { // MmAllocateContiguous returned memory above 4 GB — BM IDE can't // address it. This shouldn't happen on normal x86 systems with early // boot allocations but we check defensively. DbgPrint("IDE DMA: PRDT page is above 4 GB (0x%llx) — using PIO\n", (uint64_t)prdtPhys); return false; } void *prdtVirt = gHal->PhysToVirt(prdtPhys); DbgPrint("IDE DMA: PRDT at phys 0x%08x, virt %p\n", (uint32_t)prdtPhys, prdtVirt); // ---- Create synchronisation objects ------------------------------------ SEMAPHORE *sem = NULL; if (!KSUCCESS(KeSemaphoreCreate(0, 0x7FFFFFFF, &sem))) { DbgPrint("IDE DMA: Failed to create IRP semaphore\n"); return false; } EVENT *dmaEv = NULL; if (!KSUCCESS(KeEventCreate(EventAutoReset, 0, &dmaEv))) { DbgPrint("IDE DMA: Failed to create DMA completion event\n"); return false; } // ---- Commit module state ----------------------------------------------- gBmBase = bmBase; gPrdtPhys = (uint32_t)prdtPhys; gPrdtVirt = (PRD *)prdtVirt; gIrpSemaphore = sem; gDmaEvent = dmaEv; IrpQueueInit(&gIrpQueue, sem); // ---- Register IRQ 14 -------------------------------------------------- KeIrqRegister(IDE_IRQ_PRIMARY, IdeIsr); DbgPrint("IDE DMA: IRQ %u registered\n", IDE_IRQ_PRIMARY); // ---- Spawn worker thread ----------------------------------------------- KSTATUS st = KeCreateKernelThread((uintptr_t)IdeWorker, 0, PriorityHigh, (THREAD **)NULL); if (!KSUCCESS(st)) { DbgPrint("IDE DMA: Worker thread creation failed (0x%x)\n", st); // Not fatal for init — the PRDT/IRQ are live but async callers will // never get completions. Better to surface the failure clearly. return false; } DbgPrint("IDE DMA: Init complete\n"); return true; } // --------------------------------------------------------------------------- // DriverEntry // --------------------------------------------------------------------------- KSTATUS DriverEntry(const HAL_API *hal) { DbgPrint("IDE: Init\n"); gHal = hal; // ---- IDENTIFY drive (same as before) ---------------------------------- __outbyte(IDE_DRIVE, 0xA0); for (int i = 0; i < 4; i++) __inbyte(IDE_ALT); __outbyte(IDE_SECCNT, 0); __outbyte(IDE_LBA0, 0); __outbyte(IDE_LBA1, 0); __outbyte(IDE_LBA2, 0); __outbyte(IDE_CMD, CMD_IDENT); if (__inbyte(IDE_STATUS) == 0) { DbgPrint("IDE: No drive on primary channel\n"); return STATUS_NOT_FOUND; } int timeout = 10000; while (__inbyte(IDE_STATUS) & IDE_BUSY) { if (--timeout == 0) return STATUS_DEVICE_ERROR; KeYield(); } if (__inbyte(IDE_LBA1) != 0 || __inbyte(IDE_LBA2) != 0) { DbgPrint("IDE: Not a standard ATA drive (ATAPI?)\n"); return STATUS_NOT_SUPPORTED; } timeout = 10000; while (true) { uint8_t st = __inbyte(IDE_STATUS); if (st & IDE_ERR_F) return STATUS_DEVICE_ERROR; if (st & IDE_DRQ) break; if (--timeout == 0) return STATUS_DEVICE_ERROR; KeYield(); } uint16_t ident[256]; for (int i = 0; i < 256; i++) ident[i] = __inword(IDE_DATA); gSupportsLba48 = (ident[83] & (1 << 10)) != 0; if (gSupportsLba48) { gSectorCount = *(uint64_t *)&ident[100]; DbgPrint("IDE: LBA48, %llu sectors\n", gSectorCount); } else { gSectorCount = ((uint32_t)ident[61] << 16) | ident[60]; DbgPrint("IDE: LBA28, %u sectors\n", (uint32_t)gSectorCount); } if (gSectorCount == 0) return STATUS_NOT_FOUND; // ---- Try to initialise Bus Master DMA --------------------------------- // On success, wire in IdeSubmitIrp. On failure, leave it NULL so // HalDiskDispatch falls back to the synchronous PIO callbacks. bool dmaOk = IdeDmaInit(); gDrv.Ctx = NULL; gDrv.ReadSectors = IdeRead; gDrv.WriteSectors = IdeWrite; gDrv.SubmitIrp = dmaOk ? IdeSubmitIrp : NULL; gDrv.SectorCount = gSectorCount; DbgPrint("IDE: Registered (%s)\n", dmaOk ? "DMA" : "PIO fallback"); return IoRegisterDisk(0, &gDrv); }