summaryrefslogtreecommitdiffstats
path: root/rsnix/src/ahci.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-01 15:00:47 +0200
committersalmonik <salmonik@tuta.io>2026-08-01 15:00:47 +0200
commitd562e9df2bc5e69bba7298a9439e2adca5c18faf (patch)
tree8d763e60d512e7a4230413f13caaf9153cb2ec54 /rsnix/src/ahci.rs
parentklog: [ssss.mmm] millisecond timestamps from the PIT (diff)
downloadrs-dos-d562e9df2bc5e69bba7298a9439e2adca5c18faf.tar.gz
rs-dos-d562e9df2bc5e69bba7298a9439e2adca5c18faf.zip
rsnix: runtime device detection — rescan for hotplugged/removed disks
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/<UID> 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.
Diffstat (limited to '')
-rw-r--r--rsnix/src/ahci.rs131
1 files changed, 119 insertions, 12 deletions
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;
52/// One attached disk: register map + DMA structures. 52/// One attached disk: register map + DMA structures.
53#[derive(Clone, Copy)] 53#[derive(Clone, Copy)]
54struct Disk { 54struct Disk {
55 port: usize, // port register base = ABAR + 0x100 55 port: usize, // port register base = ABAR + 0x100
56 clb: usize, // command list, 1024 B (32 × 32 B), 256 B aligned 56 port_idx: u32, // implemented-port index this disk lives on (rescan bookkeeping)
57 ct: usize, // command table, 256 B, 128 B aligned 57 clb: usize, // command list, 1024 B (32 × 32 B), 256 B aligned
58 buf: usize, // 4 KiB DMA data buffer (page aligned) 58 ct: usize, // command table, 256 B, 128 B aligned
59 sectors: u64, // total sectors (LBA48 from IDENTIFY) 59 buf: usize, // 4 KiB DMA data buffer (page aligned)
60 sectors: u64, // total sectors (LBA48 from IDENTIFY)
60} 61}
61 62
62static mut DISKS: [Option<Disk>; MAX_DISKS] = [None; MAX_DISKS]; 63static mut DISKS: [Option<Disk>; MAX_DISKS] = [None; MAX_DISKS];
63static mut DISK_COUNT: usize = 0; 64static mut DISK_COUNT: usize = 0;
64static mut ACTIVE: usize = 0; 65static mut ACTIVE: usize = 0;
65 66
67// Controller state kept for runtime rescan: ABAR + the implemented-port
68// bitmap, plus the last-known device presence (PxSSTS.DET == 3) per port.
69// `poll_dirty` compares live DET against this, so a hotplug is noticed
70// with a handful of MMIO reads; the full re-scan only runs on a change.
71const MAX_PORTS: usize = 32;
72static mut ABAR: usize = 0;
73static mut NP: u32 = 0;
74static mut IMPL_PORTS: u32 = 0;
75static mut PORT_UP: [bool; MAX_PORTS] = [false; MAX_PORTS];
76
66fn reg(a: usize) -> u32 { 77fn reg(a: usize) -> u32 {
67 unsafe { core::ptr::read_volatile(a as *const u32) } 78 unsafe { core::ptr::read_volatile(a as *const u32) }
68} 79}
@@ -173,6 +184,7 @@ fn start_port(abar: usize, port_idx: u32) -> Option<Disk> {
173 184
174 let mut d = Disk { 185 let mut d = Disk {
175 port, 186 port,
187 port_idx,
176 clb, 188 clb,
177 ct, 189 ct,
178 buf, 190 buf,
@@ -238,16 +250,24 @@ pub fn init() -> bool {
238 250
239 // Scan implemented ports (CAP.NP + 1 of them, filtered by PI). 251 // Scan implemented ports (CAP.NP + 1 of them, filtered by PI).
240 let cap = reg(abar as usize + CAP); 252 let cap = reg(abar as usize + CAP);
241 let np = ((cap & 0x1F) + 1).min(32); 253 let np = ((cap & 0x1F) + 1).min(MAX_PORTS as u32);
242 let pi = reg(abar as usize + PI); 254 let pi = reg(abar as usize + PI);
255 unsafe {
256 ABAR = abar as usize;
257 NP = np;
258 IMPL_PORTS = pi;
259 }
243 for port_idx in 0..np { 260 for port_idx in 0..np {
244 if (pi >> port_idx) & 1 == 0 { 261 if (pi >> port_idx) & 1 == 0 {
245 continue; 262 continue;
246 } 263 }
264 let det = reg(abar as usize + PORT_BASE + port_idx as usize * PORT_STRIDE + P_SSTS) & 0xF;
265 unsafe { PORT_UP[port_idx as usize] = det == 3; }
247 if let Some(d) = start_port(abar as usize, port_idx) { 266 if let Some(d) = start_port(abar as usize, port_idx) {
248 unsafe { 267 // The device answered IDENTIFY — it's definitely present.
249 DISKS[DISK_COUNT] = Some(d); 268 unsafe { PORT_UP[port_idx as usize] = true; }
250 DISK_COUNT += 1; 269 if !place_disk(d) {
270 crate::serial_println!("ahci: port {} up but disk table full", port_idx);
251 } 271 }
252 } 272 }
253 } 273 }
@@ -257,16 +277,103 @@ pub fn init() -> bool {
257 unsafe { DISK_COUNT > 0 } 277 unsafe { DISK_COUNT > 0 }
258} 278}
259 279
260/// Number of disks found at boot. 280/// Record a started disk in the table, reusing a tombstoned (None) slot so
281/// indices stay stable across plug/unplug cycles. Returns false if full.
282fn place_disk(d: Disk) -> bool {
283 unsafe {
284 for i in 0..MAX_DISKS {
285 if DISKS[i].is_none() {
286 DISKS[i] = Some(d);
287 DISK_COUNT = DISK_COUNT.max(i + 1);
288 return true;
289 }
290 }
291 }
292 false
293}
294
295/// Number of disk slots in use (high-water mark; tombstones included).
261pub fn disk_count() -> usize { 296pub fn disk_count() -> usize {
262 unsafe { DISK_COUNT } 297 unsafe { DISK_COUNT }
263} 298}
264 299
300/// Cheap runtime hotplug probe: true if any implemented port's device
301/// presence (PxSSTS.DET == 3) differs from the last known state. A few
302/// MMIO reads — safe to call on every shell prompt.
303pub fn poll_dirty() -> bool {
304 let abar = unsafe { ABAR };
305 if abar == 0 {
306 return false;
307 }
308 let (np, pi) = (unsafe { NP }, unsafe { IMPL_PORTS });
309 for port_idx in 0..np {
310 if (pi >> port_idx) & 1 == 0 {
311 continue;
312 }
313 let det = reg(abar + PORT_BASE + port_idx as usize * PORT_STRIDE + P_SSTS) & 0xF;
314 if (det == 3) != unsafe { PORT_UP[port_idx as usize] } {
315 return true;
316 }
317 }
318 false
319}
320
321/// Re-scan the controller's ports for arrivals/removals. Starts ports
322/// that now have a link, tombstones (None) the slots of ports whose device
323/// vanished, and refreshes the poll state. Slot indices never shift, so
324/// the filesystem's disk handles stay valid. Returns (added, removed).
325pub fn rescan() -> (usize, usize) {
326 let abar = unsafe { ABAR };
327 if abar == 0 {
328 return (0, 0);
329 }
330 let (np, pi) = (unsafe { NP }, unsafe { IMPL_PORTS });
331 let mut added = 0usize;
332 let mut removed = 0usize;
333 for port_idx in 0..np {
334 if (pi >> port_idx) & 1 == 0 {
335 continue;
336 }
337 let port = abar + PORT_BASE + port_idx as usize * PORT_STRIDE;
338 let det = reg(port + P_SSTS) & 0xF;
339 let up = det == 3;
340 let slot = disks()
341 .iter()
342 .position(|d| d.is_some_and(|d| d.port_idx == port_idx));
343 let up_prev = unsafe { PORT_UP[port_idx as usize] };
344 unsafe { PORT_UP[port_idx as usize] = up; }
345 if up == up_prev {
346 continue;
347 }
348 if up {
349 // Arrival: full bring-up on this port.
350 if let Some(d) = start_port(abar, port_idx) {
351 if place_disk(d) {
352 added += 1;
353 } else {
354 crate::serial_println!("ahci: port {} up but disk table full", port_idx);
355 }
356 }
357 } else if let Some(i) = slot {
358 // Removal: tombstone the slot; indices stay stable.
359 unsafe { DISKS[i] = None; }
360 crate::serial_println!("ahci: port {} down — device removed", port_idx);
361 removed += 1;
362 }
363 }
364 (added, removed)
365}
366
367/// Is the disk at index `idx` still attached? (False for tombstones.)
368pub fn is_up(idx: usize) -> bool {
369 disks().get(idx).is_some_and(|d| d.is_some())
370}
371
265/// Make `idx` the disk the sector API reads/writes. Returns false if the 372/// Make `idx` the disk the sector API reads/writes. Returns false if the
266/// index is out of range. 373/// index is out of range or its slot is a tombstone.
267pub fn select(idx: usize) -> bool { 374pub fn select(idx: usize) -> bool {
268 unsafe { 375 unsafe {
269 if idx < DISK_COUNT { 376 if idx < DISK_COUNT && DISKS[idx].is_some() {
270 ACTIVE = idx; 377 ACTIVE = idx;
271 true 378 true
272 } else { 379 } else {