From d562e9df2bc5e69bba7298a9439e2adca5c18faf Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 1 Aug 2026 15:00:47 +0200 Subject: rsnix: runtime device detection — rescan for hotplugged/removed disks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: make the boot discovery pipeline re-runnable and diff it against live hardware state. - ahci: store ABAR/NP/port bitmap; poll_dirty() (cheap PxSSTS.DET check per port) and rescan() which starts newly-present ports and tombstones the slots of removed ones. Slots never shift, so disk indices stay valid. select() now rejects tombstoned slots. - fat: Volume gets a mounted flag; rescan() unmounts volumes whose disk left and mounts every disk without a mounted volume (re-trying failed boot mounts). UID dedup ignores tombstones so a re-plugged disk keeps its /disk/ name. - fs: init's mount loop factored into rescan_disks(), which lazily creates /disk, ensures a mount node per mounted volume and tombstones nodes of unmounted ones. - shell: 'rescan' command + automatic deep rescan before each prompt when ahci::poll_dirty() sees a port-presence change. Rescan events flow through serial_println so they land in klog too. Removal (PxSSTS) is a real-hardware path: QEMU q35 rejects disk hotplug on the ich9-ahci IDE buses, and PCIe device_del is a guest-cooperative protocol this kernel does not implement. --- rsnix/src/ahci.rs | 131 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 119 insertions(+), 12 deletions(-) (limited to 'rsnix/src/ahci.rs') diff --git a/rsnix/src/ahci.rs b/rsnix/src/ahci.rs index 9a49431..9ed803d 100644 --- a/rsnix/src/ahci.rs +++ b/rsnix/src/ahci.rs @@ -52,17 +52,28 @@ 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 - ct: usize, // command table, 256 B, 128 B aligned - buf: usize, // 4 KiB DMA data buffer (page aligned) - sectors: u64, // total sectors (LBA48 from IDENTIFY) + port: usize, // port register base = ABAR + 0x100 + port_idx: u32, // implemented-port index this disk lives on (rescan bookkeeping) + clb: usize, // command list, 1024 B (32 × 32 B), 256 B aligned + ct: usize, // command table, 256 B, 128 B aligned + buf: usize, // 4 KiB DMA data buffer (page aligned) + sectors: u64, // total sectors (LBA48 from IDENTIFY) } static mut DISKS: [Option; MAX_DISKS] = [None; MAX_DISKS]; static mut DISK_COUNT: usize = 0; static mut ACTIVE: usize = 0; +// Controller state kept for runtime rescan: ABAR + the implemented-port +// bitmap, plus the last-known device presence (PxSSTS.DET == 3) per port. +// `poll_dirty` compares live DET against this, so a hotplug is noticed +// with a handful of MMIO reads; the full re-scan only runs on a change. +const MAX_PORTS: usize = 32; +static mut ABAR: usize = 0; +static mut NP: u32 = 0; +static mut IMPL_PORTS: u32 = 0; +static mut PORT_UP: [bool; MAX_PORTS] = [false; MAX_PORTS]; + fn reg(a: usize) -> u32 { unsafe { core::ptr::read_volatile(a as *const u32) } } @@ -173,6 +184,7 @@ fn start_port(abar: usize, port_idx: u32) -> Option { let mut d = Disk { port, + port_idx, clb, ct, buf, @@ -238,16 +250,24 @@ pub fn init() -> bool { // 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 np = ((cap & 0x1F) + 1).min(MAX_PORTS as u32); let pi = reg(abar as usize + PI); + unsafe { + ABAR = abar as usize; + NP = np; + IMPL_PORTS = pi; + } for port_idx in 0..np { if (pi >> port_idx) & 1 == 0 { continue; } + let det = reg(abar as usize + PORT_BASE + port_idx as usize * PORT_STRIDE + P_SSTS) & 0xF; + unsafe { PORT_UP[port_idx as usize] = det == 3; } if let Some(d) = start_port(abar as usize, port_idx) { - unsafe { - DISKS[DISK_COUNT] = Some(d); - DISK_COUNT += 1; + // The device answered IDENTIFY — it's definitely present. + unsafe { PORT_UP[port_idx as usize] = true; } + if !place_disk(d) { + crate::serial_println!("ahci: port {} up but disk table full", port_idx); } } } @@ -257,16 +277,103 @@ pub fn init() -> bool { unsafe { DISK_COUNT > 0 } } -/// Number of disks found at boot. +/// Record a started disk in the table, reusing a tombstoned (None) slot so +/// indices stay stable across plug/unplug cycles. Returns false if full. +fn place_disk(d: Disk) -> bool { + unsafe { + for i in 0..MAX_DISKS { + if DISKS[i].is_none() { + DISKS[i] = Some(d); + DISK_COUNT = DISK_COUNT.max(i + 1); + return true; + } + } + } + false +} + +/// Number of disk slots in use (high-water mark; tombstones included). pub fn disk_count() -> usize { unsafe { DISK_COUNT } } +/// Cheap runtime hotplug probe: true if any implemented port's device +/// presence (PxSSTS.DET == 3) differs from the last known state. A few +/// MMIO reads — safe to call on every shell prompt. +pub fn poll_dirty() -> bool { + let abar = unsafe { ABAR }; + if abar == 0 { + return false; + } + let (np, pi) = (unsafe { NP }, unsafe { IMPL_PORTS }); + for port_idx in 0..np { + if (pi >> port_idx) & 1 == 0 { + continue; + } + let det = reg(abar + PORT_BASE + port_idx as usize * PORT_STRIDE + P_SSTS) & 0xF; + if (det == 3) != unsafe { PORT_UP[port_idx as usize] } { + return true; + } + } + false +} + +/// Re-scan the controller's ports for arrivals/removals. Starts ports +/// that now have a link, tombstones (None) the slots of ports whose device +/// vanished, and refreshes the poll state. Slot indices never shift, so +/// the filesystem's disk handles stay valid. Returns (added, removed). +pub fn rescan() -> (usize, usize) { + let abar = unsafe { ABAR }; + if abar == 0 { + return (0, 0); + } + let (np, pi) = (unsafe { NP }, unsafe { IMPL_PORTS }); + let mut added = 0usize; + let mut removed = 0usize; + for port_idx in 0..np { + if (pi >> port_idx) & 1 == 0 { + continue; + } + let port = abar + PORT_BASE + port_idx as usize * PORT_STRIDE; + let det = reg(port + P_SSTS) & 0xF; + let up = det == 3; + let slot = disks() + .iter() + .position(|d| d.is_some_and(|d| d.port_idx == port_idx)); + let up_prev = unsafe { PORT_UP[port_idx as usize] }; + unsafe { PORT_UP[port_idx as usize] = up; } + if up == up_prev { + continue; + } + if up { + // Arrival: full bring-up on this port. + if let Some(d) = start_port(abar, port_idx) { + if place_disk(d) { + added += 1; + } else { + crate::serial_println!("ahci: port {} up but disk table full", port_idx); + } + } + } else if let Some(i) = slot { + // Removal: tombstone the slot; indices stay stable. + unsafe { DISKS[i] = None; } + crate::serial_println!("ahci: port {} down — device removed", port_idx); + removed += 1; + } + } + (added, removed) +} + +/// Is the disk at index `idx` still attached? (False for tombstones.) +pub fn is_up(idx: usize) -> bool { + disks().get(idx).is_some_and(|d| d.is_some()) +} + /// Make `idx` the disk the sector API reads/writes. Returns false if the -/// index is out of range. +/// index is out of range or its slot is a tombstone. pub fn select(idx: usize) -> bool { unsafe { - if idx < DISK_COUNT { + if idx < DISK_COUNT && DISKS[idx].is_some() { ACTIVE = idx; true } else { -- cgit v1.2.3