diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-01 15:00:47 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-01 15:00:47 +0200 |
| commit | d562e9df2bc5e69bba7298a9439e2adca5c18faf (patch) | |
| tree | 8d763e60d512e7a4230413f13caaf9153cb2ec54 /rsnix/src/ahci.rs | |
| parent | klog: [ssss.mmm] millisecond timestamps from the PIT (diff) | |
| download | rs-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.rs | 131 |
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)] |
| 54 | struct Disk { | 54 | struct 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 | ||
| 62 | static mut DISKS: [Option<Disk>; MAX_DISKS] = [None; MAX_DISKS]; | 63 | static mut DISKS: [Option<Disk>; MAX_DISKS] = [None; MAX_DISKS]; |
| 63 | static mut DISK_COUNT: usize = 0; | 64 | static mut DISK_COUNT: usize = 0; |
| 64 | static mut ACTIVE: usize = 0; | 65 | static 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. | ||
| 71 | const MAX_PORTS: usize = 32; | ||
| 72 | static mut ABAR: usize = 0; | ||
| 73 | static mut NP: u32 = 0; | ||
| 74 | static mut IMPL_PORTS: u32 = 0; | ||
| 75 | static mut PORT_UP: [bool; MAX_PORTS] = [false; MAX_PORTS]; | ||
| 76 | |||
| 66 | fn reg(a: usize) -> u32 { | 77 | fn 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. | ||
| 282 | fn 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). | ||
| 261 | pub fn disk_count() -> usize { | 296 | pub 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. | ||
| 303 | pub 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). | ||
| 325 | pub 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.) | ||
| 368 | pub 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. |
| 267 | pub fn select(idx: usize) -> bool { | 374 | pub 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 { |
