From 3997ec4d2d7d6f69b08d0c04ce8de0df03c52a45 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 31 Jul 2026 21:29:49 +0200 Subject: ahci: scan all ports; fat: mount first valid FAT32 disk The kernel previously started only AHCI port 0, so any launch where the firmware/QEMU put something else on port 0 (e.g. the ESP from an implicit -drive file=fat:rw:esp) left the real disk unnoticed: 'unsupported sector size 0' and no mount. - ahci.rs: enumerate CAP.NP/PI ports, start every port that links up (empty ports settle at DET=0 and are skipped quickly), keep up to 8 disks, and expose disk_count()/select() for the filesystem - fat.rs: try each disk in port order and mount the first valid FAT32 volume; non-FAT disks are logged and skipped - scripts/boot.sh: attach the data disk on ide.0 and the ESP on ide.1 (DISK_IMG env var, default /tmp/disk.img), matching the Makefile - Verified with the disk on port 0 (Makefile style), port 5 (old-style command line), and with the ESP on port 0; fsck.fat stays clean --- rsnix/src/ahci.rs | 173 +++++++++++++++++++++++++++++++++++------------------- rsnix/src/fat.rs | 25 +++++++- 2 files changed, 134 insertions(+), 64 deletions(-) (limited to 'rsnix/src') diff --git a/rsnix/src/ahci.rs b/rsnix/src/ahci.rs index 83d0d27..ea1a886 100644 --- a/rsnix/src/ahci.rs +++ b/rsnix/src/ahci.rs @@ -2,11 +2,13 @@ // AHCI — SATA host-controller driver (polling, DMA, no interrupts). // // Finds the chipset's AHCI controller (class 1 / subclass 6 / prog-if 1, -// e.g. the ICH9 on q35) on PCI bus 0, maps its ABAR (BAR5), starts port 0 -// and exposes sector-level reads/writes for the FAT32 filesystem. +// e.g. the ICH9 on q35) on PCI bus 0, maps its ABAR (BAR5), starts every +// implemented port that has a device and exposes sector-level reads/writes +// for the FAT32 filesystem. Multiple disks are supported; the filesystem +// selects one via `select()` and tries each in turn until one mounts. // -// Commands are issued through command-slot 0 with one 4 KiB DMA buffer; -// completion is polled via PxCI (no ISR, no MSI). Everything is +// Commands are issued through command-slot 0 with one 4 KiB DMA buffer per +// port; completion is polled via PxCI (no ISR, no MSI). Everything is // identity-mapped, so "physical" DMA addresses are plain kernel pointers. // The display is never involved, so this is a straightforward polling // loop like the rest of the single-threaded kernel. @@ -15,8 +17,10 @@ use crate::pci; const SECTOR: usize = 512; +const MAX_DISKS: usize = 8; // ABAR (HBA memory space) offsets. +const CAP: usize = 0x00; const GHC: usize = 0x04; const PI: usize = 0x0C; const PORT_BASE: usize = 0x100; @@ -46,6 +50,7 @@ const ATA_READ_DMA_EXT: u8 = 0x25; const ATA_WRITE_DMA_EXT: u8 = 0x35; /// One attached disk: register map + DMA structures. +#[derive(Clone, Copy)] struct Disk { port: usize, // port register base = ABAR + 0x100 clb: usize, // command list, 1024 B (32 × 32 B), 256 B aligned @@ -55,7 +60,9 @@ struct Disk { sectors: u64, // total sectors (LBA48 from IDENTIFY) } -static mut DISK: Option = None; +static mut DISKS: [Option; MAX_DISKS] = [None; MAX_DISKS]; +static mut DISK_COUNT: usize = 0; +static mut ACTIVE: usize = 0; fn reg(a: usize) -> u32 { unsafe { core::ptr::read_volatile(a as *const u32) } @@ -88,48 +95,15 @@ fn find_ahci() -> Option<(u8, u8)> { None } -/// Probe, map and start the first AHCI port. Returns true if a disk is -/// ready for sector I/O. Called once at boot before the filesystem init. -pub fn init() -> bool { - let Some((dev, func)) = find_ahci() else { - return false; - }; - // Enable memory space + bus mastering so MMIO and DMA work. - let cmd_reg = pci::config_read(0, dev, func, 0x04); - pci::config_write(0, dev, func, 0x04, cmd_reg | 0x7); - - let Some(abar) = pci::bar64(0, dev, func, 5) else { - return false; - }; - crate::serial_println!( - "ahci: controller at {:02x}:{:02x}.{} abar={:#x}", - 0, - dev, - func, - abar - ); - - // AHCI enable (GHC.AE) before touching port registers. - wreg(abar as usize + GHC, reg(abar as usize + GHC) | (1 << 31)); - - // Pick the first implemented port. - let pi = reg(abar as usize + PI); - let mut port_idx = 0u32; - while port_idx < 32 && (pi >> port_idx) & 1 == 0 { - port_idx += 1; - } - if port_idx >= 32 { - crate::serial_println!("ahci: no ports implemented (pi={:#x})", pi); - return false; - } - let port = abar as usize + PORT_BASE + port_idx as usize * PORT_STRIDE; +/// Bring up one port: stop any firmware state, wire up its DMA structures, +/// wait for the link, start the command engine and run IDENTIFY. Returns +/// the disk if a usable device answered. +fn start_port(abar: usize, port_idx: u32) -> Option { + let port = abar + PORT_BASE + port_idx as usize * PORT_STRIDE; // DMA structures: one 8 KiB allocation → page 0 = cmd list + FIS + cmd // table, page 1 = data buffer. - let Some(mem) = crate::memory::alloc_pages(2) else { - crate::serial_println!("ahci: cannot allocate DMA pages"); - return false; - }; + let mem = crate::memory::alloc_pages(2)?; let clb = mem; let fis = mem + 1024; let ct = mem + 1280; @@ -165,13 +139,18 @@ pub fn init() -> bool { // Allow device detection (clear PxSCTL.DET/IPM), then wait for the // link to come up (PxSSTS.DET == 3, communication established). + // A port with no device settles at DET == 0 quickly — give up early. wreg(port + P_SCTL, 0); let mut link_ok = false; - for _ in 0..10_000_000 { - if reg(port + P_SSTS) & 0xF == 3 { + for i in 0..10_000_000u32 { + let det = reg(port + P_SSTS) & 0xF; + if det == 3 { link_ok = true; break; } + if det == 0 && i > 100_000 { + break; // settled with no device + } } if !link_ok { crate::serial_println!( @@ -179,7 +158,7 @@ pub fn init() -> bool { port_idx, reg(port + P_SSTS) ); - return false; + return None; } // Start the command engine (DMAE required for DMA commands). @@ -196,8 +175,8 @@ pub fn init() -> bool { }; // IDENTIFY DEVICE as a sanity check + to learn the capacity. if issue(&d, ATA_IDENTIFY, 0, 1, false).is_err() { - crate::serial_println!("ahci: IDENTIFY failed"); - return false; + crate::serial_println!("ahci: IDENTIFY failed on port {}", port_idx); + return None; } let id = unsafe { core::slice::from_raw_parts(d.buf as *const u16, 256) }; d.sectors = (id[100] as u64) @@ -220,10 +199,72 @@ pub fn init() -> bool { d.sectors / 2048, model ); + Some(d) +} + +/// Probe, map and start every implemented port that has a device. Returns +/// true if at least one disk is ready for sector I/O. Called once at boot +/// before the filesystem init; the filesystem then calls `select()` to pick +/// which disk to read. +pub fn init() -> bool { + let Some((dev, func)) = find_ahci() else { + return false; + }; + // Enable memory space + bus mastering so MMIO and DMA work. + let cmd_reg = pci::config_read(0, dev, func, 0x04); + pci::config_write(0, dev, func, 0x04, cmd_reg | 0x7); + + let Some(abar) = pci::bar64(0, dev, func, 5) else { + return false; + }; + crate::serial_println!( + "ahci: controller at {:02x}:{:02x}.{} abar={:#x}", + 0, + dev, + func, + abar + ); + + // AHCI enable (GHC.AE) before touching port registers. + wreg(abar as usize + GHC, reg(abar as usize + GHC) | (1 << 31)); + + // Scan implemented ports (CAP.NP + 1 of them, filtered by PI). + let cap = reg(abar as usize + CAP); + let np = ((cap & 0x1F) + 1).min(32); + let pi = reg(abar as usize + PI); + for port_idx in 0..np { + if (pi >> port_idx) & 1 == 0 { + continue; + } + if let Some(d) = start_port(abar as usize, port_idx) { + unsafe { + DISKS[DISK_COUNT] = Some(d); + DISK_COUNT += 1; + } + } + } unsafe { - DISK = Some(d); + ACTIVE = 0; + } + unsafe { DISK_COUNT > 0 } +} + +/// Number of disks found at boot. +pub fn disk_count() -> usize { + unsafe { DISK_COUNT } +} + +/// Make `idx` the disk the sector API reads/writes. Returns false if the +/// index is out of range. +pub fn select(idx: usize) -> bool { + unsafe { + if idx < DISK_COUNT { + ACTIVE = idx; + true + } else { + false + } } - true } /// Issue one command through slot 0 and poll for completion. @@ -246,14 +287,18 @@ fn issue(d: &Disk, cmd: u8, lba: u64, count: u32, write: bool) -> Result<(), ()> } // Host-to-device FIS (20 bytes) at the command table start. + // Layout (SATA spec, cross-checked against QEMU + Linux libahci): + // 0 type 0x27, 1 flags, 2 command, 3 features low, + // 4-6 LBA[23:0], 7 device|LBA[27:24], 8-10 LBA[31:48], + // 11 features high, 12-13 sector count, 14 ICC, 15 control. + // Flags byte: only the C bit (0x80). QEMU rejects H2D FISes with + // bits 6:4 set (reserved); the transfer direction is signalled by + // the command-header W bit instead, which issue() sets below. let fis = d.ct as *mut u8; unsafe { *fis = 0x27; // FIS type H2D *(fis.add(1)) = 0x80; // C only — QEMU rejects flags bits 6:4 *(fis.add(2)) = cmd; - // FIS field layout (SATA spec; verified against QEMU/Linux): - // 4-6 LBA[23:0], 7 device|LBA[27:24], 8-10 LBA[31:48], - // 11 features high, 12-13 sector count. *(fis.add(4)) = lba as u8; *(fis.add(5)) = (lba >> 8) as u8; *(fis.add(6)) = (lba >> 16) as u8; @@ -324,32 +369,36 @@ fn issue(d: &Disk, cmd: u8, lba: u64, count: u32, write: bool) -> Result<(), ()> Ok(()) } -/// DMA `count` sectors starting at `lba` into the internal buffer. +fn active() -> Option<&'static Disk> { + unsafe { DISKS.get(ACTIVE).and_then(|d| d.as_ref()) } +} + +/// DMA `count` sectors starting at `lba` into the selected disk's buffer. pub fn read_sectors(lba: u64, count: u32) -> Result<(), ()> { - let d = unsafe { DISK.as_ref() }.ok_or(())?; + let d = active().ok_or(())?; if lba + count as u64 > d.sectors { return Err(()); } issue(d, ATA_READ_DMA_EXT, lba, count, false) } -/// DMA `count` sectors starting at `lba` out of the internal buffer. +/// DMA `count` sectors starting at `lba` out of the selected disk's buffer. pub fn write_sectors(lba: u64, count: u32) -> Result<(), ()> { - let d = unsafe { DISK.as_ref() }.ok_or(())?; + let d = active().ok_or(())?; if lba + count as u64 > d.sectors { return Err(()); } issue(d, ATA_WRITE_DMA_EXT, lba, count, true) } -/// Pointer to the internal 4 KiB DMA buffer. +/// Pointer to the selected disk's 4 KiB DMA buffer. pub fn buf() -> *mut u8 { - unsafe { DISK.as_ref() } + active() .map(|d| d.buf as *mut u8) .unwrap_or(core::ptr::null_mut()) } -/// Total sector count of the attached disk. +/// Total sector count of the selected disk. pub fn sector_count() -> u64 { - unsafe { DISK.as_ref() }.map(|d| d.sectors).unwrap_or(0) + active().map(|d| d.sectors).unwrap_or(0) } diff --git a/rsnix/src/fat.rs b/rsnix/src/fat.rs index 70fa688..616a08f 100644 --- a/rsnix/src/fat.rs +++ b/rsnix/src/fat.rs @@ -838,9 +838,30 @@ pub fn remove(parent_dir_cluster: u32, name: &str) -> Result<(), ()> { // --- mount ------------------------------------------------------------------- -/// Parse the BPB, cache the FAT and validate the volume. Returns true if -/// a usable FAT32 volume is mounted. +/// Try to mount a FAT32 volume on every attached disk, in port order, and +/// keep the first one that validates. Returns true if a usable FAT32 +/// volume is mounted. pub fn init() -> bool { + let disks = crate::ahci::disk_count(); + if disks == 0 { + crate::serial_println!("fat: no disk"); + return false; + } + for d in 0..disks { + if !crate::ahci::select(d) { + continue; + } + if try_mount() { + return true; + } + crate::serial_println!("fat: no FAT32 volume on disk {}", d); + } + false +} + +/// Parse the BPB of the selected disk, cache the FAT and validate the +/// volume. +fn try_mount() -> bool { let mut bpb = [0u8; SECTOR]; if read_sector(0, &mut bpb).is_err() { crate::serial_println!("fat: no disk"); -- cgit v1.2.3