diff options
Diffstat (limited to 'rsnix/src/ahci.rs')
| -rw-r--r-- | rsnix/src/ahci.rs | 173 |
1 files changed, 111 insertions, 62 deletions
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 @@ | |||
| 2 | // AHCI — SATA host-controller driver (polling, DMA, no interrupts). | 2 | // AHCI — SATA host-controller driver (polling, DMA, no interrupts). |
| 3 | // | 3 | // |
| 4 | // Finds the chipset's AHCI controller (class 1 / subclass 6 / prog-if 1, | 4 | // Finds the chipset's AHCI controller (class 1 / subclass 6 / prog-if 1, |
| 5 | // e.g. the ICH9 on q35) on PCI bus 0, maps its ABAR (BAR5), starts port 0 | 5 | // e.g. the ICH9 on q35) on PCI bus 0, maps its ABAR (BAR5), starts every |
| 6 | // and exposes sector-level reads/writes for the FAT32 filesystem. | 6 | // implemented port that has a device and exposes sector-level reads/writes |
| 7 | // for the FAT32 filesystem. Multiple disks are supported; the filesystem | ||
| 8 | // selects one via `select()` and tries each in turn until one mounts. | ||
| 7 | // | 9 | // |
| 8 | // Commands are issued through command-slot 0 with one 4 KiB DMA buffer; | 10 | // Commands are issued through command-slot 0 with one 4 KiB DMA buffer per |
| 9 | // completion is polled via PxCI (no ISR, no MSI). Everything is | 11 | // port; completion is polled via PxCI (no ISR, no MSI). Everything is |
| 10 | // identity-mapped, so "physical" DMA addresses are plain kernel pointers. | 12 | // identity-mapped, so "physical" DMA addresses are plain kernel pointers. |
| 11 | // The display is never involved, so this is a straightforward polling | 13 | // The display is never involved, so this is a straightforward polling |
| 12 | // loop like the rest of the single-threaded kernel. | 14 | // loop like the rest of the single-threaded kernel. |
| @@ -15,8 +17,10 @@ | |||
| 15 | use crate::pci; | 17 | use crate::pci; |
| 16 | 18 | ||
| 17 | const SECTOR: usize = 512; | 19 | const SECTOR: usize = 512; |
| 20 | const MAX_DISKS: usize = 8; | ||
| 18 | 21 | ||
| 19 | // ABAR (HBA memory space) offsets. | 22 | // ABAR (HBA memory space) offsets. |
| 23 | const CAP: usize = 0x00; | ||
| 20 | const GHC: usize = 0x04; | 24 | const GHC: usize = 0x04; |
| 21 | const PI: usize = 0x0C; | 25 | const PI: usize = 0x0C; |
| 22 | const PORT_BASE: usize = 0x100; | 26 | const PORT_BASE: usize = 0x100; |
| @@ -46,6 +50,7 @@ const ATA_READ_DMA_EXT: u8 = 0x25; | |||
| 46 | const ATA_WRITE_DMA_EXT: u8 = 0x35; | 50 | const ATA_WRITE_DMA_EXT: u8 = 0x35; |
| 47 | 51 | ||
| 48 | /// One attached disk: register map + DMA structures. | 52 | /// One attached disk: register map + DMA structures. |
| 53 | #[derive(Clone, Copy)] | ||
| 49 | struct Disk { | 54 | struct Disk { |
| 50 | port: usize, // port register base = ABAR + 0x100 | 55 | port: usize, // port register base = ABAR + 0x100 |
| 51 | clb: usize, // command list, 1024 B (32 × 32 B), 256 B aligned | 56 | clb: usize, // command list, 1024 B (32 × 32 B), 256 B aligned |
| @@ -55,7 +60,9 @@ struct Disk { | |||
| 55 | sectors: u64, // total sectors (LBA48 from IDENTIFY) | 60 | sectors: u64, // total sectors (LBA48 from IDENTIFY) |
| 56 | } | 61 | } |
| 57 | 62 | ||
| 58 | static mut DISK: Option<Disk> = None; | 63 | static mut DISKS: [Option<Disk>; MAX_DISKS] = [None; MAX_DISKS]; |
| 64 | static mut DISK_COUNT: usize = 0; | ||
| 65 | static mut ACTIVE: usize = 0; | ||
| 59 | 66 | ||
| 60 | fn reg(a: usize) -> u32 { | 67 | fn reg(a: usize) -> u32 { |
| 61 | unsafe { core::ptr::read_volatile(a as *const u32) } | 68 | unsafe { core::ptr::read_volatile(a as *const u32) } |
| @@ -88,48 +95,15 @@ fn find_ahci() -> Option<(u8, u8)> { | |||
| 88 | None | 95 | None |
| 89 | } | 96 | } |
| 90 | 97 | ||
| 91 | /// Probe, map and start the first AHCI port. Returns true if a disk is | 98 | /// Bring up one port: stop any firmware state, wire up its DMA structures, |
| 92 | /// ready for sector I/O. Called once at boot before the filesystem init. | 99 | /// wait for the link, start the command engine and run IDENTIFY. Returns |
| 93 | pub fn init() -> bool { | 100 | /// the disk if a usable device answered. |
| 94 | let Some((dev, func)) = find_ahci() else { | 101 | fn start_port(abar: usize, port_idx: u32) -> Option<Disk> { |
| 95 | return false; | 102 | let port = abar + PORT_BASE + port_idx as usize * PORT_STRIDE; |
| 96 | }; | ||
| 97 | // Enable memory space + bus mastering so MMIO and DMA work. | ||
| 98 | let cmd_reg = pci::config_read(0, dev, func, 0x04); | ||
| 99 | pci::config_write(0, dev, func, 0x04, cmd_reg | 0x7); | ||
| 100 | |||
| 101 | let Some(abar) = pci::bar64(0, dev, func, 5) else { | ||
| 102 | return false; | ||
| 103 | }; | ||
| 104 | crate::serial_println!( | ||
| 105 | "ahci: controller at {:02x}:{:02x}.{} abar={:#x}", | ||
| 106 | 0, | ||
| 107 | dev, | ||
| 108 | func, | ||
| 109 | abar | ||
| 110 | ); | ||
| 111 | |||
| 112 | // AHCI enable (GHC.AE) before touching port registers. | ||
| 113 | wreg(abar as usize + GHC, reg(abar as usize + GHC) | (1 << 31)); | ||
| 114 | |||
| 115 | // Pick the first implemented port. | ||
| 116 | let pi = reg(abar as usize + PI); | ||
| 117 | let mut port_idx = 0u32; | ||
| 118 | while port_idx < 32 && (pi >> port_idx) & 1 == 0 { | ||
| 119 | port_idx += 1; | ||
| 120 | } | ||
| 121 | if port_idx >= 32 { | ||
| 122 | crate::serial_println!("ahci: no ports implemented (pi={:#x})", pi); | ||
| 123 | return false; | ||
| 124 | } | ||
| 125 | let port = abar as usize + PORT_BASE + port_idx as usize * PORT_STRIDE; | ||
| 126 | 103 | ||
| 127 | // DMA structures: one 8 KiB allocation → page 0 = cmd list + FIS + cmd | 104 | // DMA structures: one 8 KiB allocation → page 0 = cmd list + FIS + cmd |
| 128 | // table, page 1 = data buffer. | 105 | // table, page 1 = data buffer. |
| 129 | let Some(mem) = crate::memory::alloc_pages(2) else { | 106 | let mem = crate::memory::alloc_pages(2)?; |
| 130 | crate::serial_println!("ahci: cannot allocate DMA pages"); | ||
| 131 | return false; | ||
| 132 | }; | ||
| 133 | let clb = mem; | 107 | let clb = mem; |
| 134 | let fis = mem + 1024; | 108 | let fis = mem + 1024; |
| 135 | let ct = mem + 1280; | 109 | let ct = mem + 1280; |
| @@ -165,13 +139,18 @@ pub fn init() -> bool { | |||
| 165 | 139 | ||
| 166 | // Allow device detection (clear PxSCTL.DET/IPM), then wait for the | 140 | // Allow device detection (clear PxSCTL.DET/IPM), then wait for the |
| 167 | // link to come up (PxSSTS.DET == 3, communication established). | 141 | // link to come up (PxSSTS.DET == 3, communication established). |
| 142 | // A port with no device settles at DET == 0 quickly — give up early. | ||
| 168 | wreg(port + P_SCTL, 0); | 143 | wreg(port + P_SCTL, 0); |
| 169 | let mut link_ok = false; | 144 | let mut link_ok = false; |
| 170 | for _ in 0..10_000_000 { | 145 | for i in 0..10_000_000u32 { |
| 171 | if reg(port + P_SSTS) & 0xF == 3 { | 146 | let det = reg(port + P_SSTS) & 0xF; |
| 147 | if det == 3 { | ||
| 172 | link_ok = true; | 148 | link_ok = true; |
| 173 | break; | 149 | break; |
| 174 | } | 150 | } |
| 151 | if det == 0 && i > 100_000 { | ||
| 152 | break; // settled with no device | ||
| 153 | } | ||
| 175 | } | 154 | } |
| 176 | if !link_ok { | 155 | if !link_ok { |
| 177 | crate::serial_println!( | 156 | crate::serial_println!( |
| @@ -179,7 +158,7 @@ pub fn init() -> bool { | |||
| 179 | port_idx, | 158 | port_idx, |
| 180 | reg(port + P_SSTS) | 159 | reg(port + P_SSTS) |
| 181 | ); | 160 | ); |
| 182 | return false; | 161 | return None; |
| 183 | } | 162 | } |
| 184 | 163 | ||
| 185 | // Start the command engine (DMAE required for DMA commands). | 164 | // Start the command engine (DMAE required for DMA commands). |
| @@ -196,8 +175,8 @@ pub fn init() -> bool { | |||
| 196 | }; | 175 | }; |
| 197 | // IDENTIFY DEVICE as a sanity check + to learn the capacity. | 176 | // IDENTIFY DEVICE as a sanity check + to learn the capacity. |
| 198 | if issue(&d, ATA_IDENTIFY, 0, 1, false).is_err() { | 177 | if issue(&d, ATA_IDENTIFY, 0, 1, false).is_err() { |
| 199 | crate::serial_println!("ahci: IDENTIFY failed"); | 178 | crate::serial_println!("ahci: IDENTIFY failed on port {}", port_idx); |
| 200 | return false; | 179 | return None; |
| 201 | } | 180 | } |
| 202 | let id = unsafe { core::slice::from_raw_parts(d.buf as *const u16, 256) }; | 181 | let id = unsafe { core::slice::from_raw_parts(d.buf as *const u16, 256) }; |
| 203 | d.sectors = (id[100] as u64) | 182 | d.sectors = (id[100] as u64) |
| @@ -220,10 +199,72 @@ pub fn init() -> bool { | |||
| 220 | d.sectors / 2048, | 199 | d.sectors / 2048, |
| 221 | model | 200 | model |
| 222 | ); | 201 | ); |
| 202 | Some(d) | ||
| 203 | } | ||
| 204 | |||
| 205 | /// Probe, map and start every implemented port that has a device. Returns | ||
| 206 | /// true if at least one disk is ready for sector I/O. Called once at boot | ||
| 207 | /// before the filesystem init; the filesystem then calls `select()` to pick | ||
| 208 | /// which disk to read. | ||
| 209 | pub fn init() -> bool { | ||
| 210 | let Some((dev, func)) = find_ahci() else { | ||
| 211 | return false; | ||
| 212 | }; | ||
| 213 | // Enable memory space + bus mastering so MMIO and DMA work. | ||
| 214 | let cmd_reg = pci::config_read(0, dev, func, 0x04); | ||
| 215 | pci::config_write(0, dev, func, 0x04, cmd_reg | 0x7); | ||
| 216 | |||
| 217 | let Some(abar) = pci::bar64(0, dev, func, 5) else { | ||
| 218 | return false; | ||
| 219 | }; | ||
| 220 | crate::serial_println!( | ||
| 221 | "ahci: controller at {:02x}:{:02x}.{} abar={:#x}", | ||
| 222 | 0, | ||
| 223 | dev, | ||
| 224 | func, | ||
| 225 | abar | ||
| 226 | ); | ||
| 227 | |||
| 228 | // AHCI enable (GHC.AE) before touching port registers. | ||
| 229 | wreg(abar as usize + GHC, reg(abar as usize + GHC) | (1 << 31)); | ||
| 230 | |||
| 231 | // Scan implemented ports (CAP.NP + 1 of them, filtered by PI). | ||
| 232 | let cap = reg(abar as usize + CAP); | ||
| 233 | let np = ((cap & 0x1F) + 1).min(32); | ||
| 234 | let pi = reg(abar as usize + PI); | ||
| 235 | for port_idx in 0..np { | ||
| 236 | if (pi >> port_idx) & 1 == 0 { | ||
| 237 | continue; | ||
| 238 | } | ||
| 239 | if let Some(d) = start_port(abar as usize, port_idx) { | ||
| 240 | unsafe { | ||
| 241 | DISKS[DISK_COUNT] = Some(d); | ||
| 242 | DISK_COUNT += 1; | ||
| 243 | } | ||
| 244 | } | ||
| 245 | } | ||
| 223 | unsafe { | 246 | unsafe { |
| 224 | DISK = Some(d); | 247 | ACTIVE = 0; |
| 248 | } | ||
| 249 | unsafe { DISK_COUNT > 0 } | ||
| 250 | } | ||
| 251 | |||
| 252 | /// Number of disks found at boot. | ||
| 253 | pub fn disk_count() -> usize { | ||
| 254 | unsafe { DISK_COUNT } | ||
| 255 | } | ||
| 256 | |||
| 257 | /// Make `idx` the disk the sector API reads/writes. Returns false if the | ||
| 258 | /// index is out of range. | ||
| 259 | pub fn select(idx: usize) -> bool { | ||
| 260 | unsafe { | ||
| 261 | if idx < DISK_COUNT { | ||
| 262 | ACTIVE = idx; | ||
| 263 | true | ||
| 264 | } else { | ||
| 265 | false | ||
| 266 | } | ||
| 225 | } | 267 | } |
| 226 | true | ||
| 227 | } | 268 | } |
| 228 | 269 | ||
| 229 | /// Issue one command through slot 0 and poll for completion. | 270 | /// 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<(), ()> | |||
| 246 | } | 287 | } |
| 247 | 288 | ||
| 248 | // Host-to-device FIS (20 bytes) at the command table start. | 289 | // Host-to-device FIS (20 bytes) at the command table start. |
| 290 | // Layout (SATA spec, cross-checked against QEMU + Linux libahci): | ||
| 291 | // 0 type 0x27, 1 flags, 2 command, 3 features low, | ||
| 292 | // 4-6 LBA[23:0], 7 device|LBA[27:24], 8-10 LBA[31:48], | ||
| 293 | // 11 features high, 12-13 sector count, 14 ICC, 15 control. | ||
| 294 | // Flags byte: only the C bit (0x80). QEMU rejects H2D FISes with | ||
| 295 | // bits 6:4 set (reserved); the transfer direction is signalled by | ||
| 296 | // the command-header W bit instead, which issue() sets below. | ||
| 249 | let fis = d.ct as *mut u8; | 297 | let fis = d.ct as *mut u8; |
| 250 | unsafe { | 298 | unsafe { |
| 251 | *fis = 0x27; // FIS type H2D | 299 | *fis = 0x27; // FIS type H2D |
| 252 | *(fis.add(1)) = 0x80; // C only — QEMU rejects flags bits 6:4 | 300 | *(fis.add(1)) = 0x80; // C only — QEMU rejects flags bits 6:4 |
| 253 | *(fis.add(2)) = cmd; | 301 | *(fis.add(2)) = cmd; |
| 254 | // FIS field layout (SATA spec; verified against QEMU/Linux): | ||
| 255 | // 4-6 LBA[23:0], 7 device|LBA[27:24], 8-10 LBA[31:48], | ||
| 256 | // 11 features high, 12-13 sector count. | ||
| 257 | *(fis.add(4)) = lba as u8; | 302 | *(fis.add(4)) = lba as u8; |
| 258 | *(fis.add(5)) = (lba >> 8) as u8; | 303 | *(fis.add(5)) = (lba >> 8) as u8; |
| 259 | *(fis.add(6)) = (lba >> 16) as u8; | 304 | *(fis.add(6)) = (lba >> 16) as u8; |
| @@ -324,32 +369,36 @@ fn issue(d: &Disk, cmd: u8, lba: u64, count: u32, write: bool) -> Result<(), ()> | |||
| 324 | Ok(()) | 369 | Ok(()) |
| 325 | } | 370 | } |
| 326 | 371 | ||
| 327 | /// DMA `count` sectors starting at `lba` into the internal buffer. | 372 | fn active() -> Option<&'static Disk> { |
| 373 | unsafe { DISKS.get(ACTIVE).and_then(|d| d.as_ref()) } | ||
| 374 | } | ||
| 375 | |||
| 376 | /// DMA `count` sectors starting at `lba` into the selected disk's buffer. | ||
| 328 | pub fn read_sectors(lba: u64, count: u32) -> Result<(), ()> { | 377 | pub fn read_sectors(lba: u64, count: u32) -> Result<(), ()> { |
| 329 | let d = unsafe { DISK.as_ref() }.ok_or(())?; | 378 | let d = active().ok_or(())?; |
| 330 | if lba + count as u64 > d.sectors { | 379 | if lba + count as u64 > d.sectors { |
| 331 | return Err(()); | 380 | return Err(()); |
| 332 | } | 381 | } |
| 333 | issue(d, ATA_READ_DMA_EXT, lba, count, false) | 382 | issue(d, ATA_READ_DMA_EXT, lba, count, false) |
| 334 | } | 383 | } |
| 335 | 384 | ||
| 336 | /// DMA `count` sectors starting at `lba` out of the internal buffer. | 385 | /// DMA `count` sectors starting at `lba` out of the selected disk's buffer. |
| 337 | pub fn write_sectors(lba: u64, count: u32) -> Result<(), ()> { | 386 | pub fn write_sectors(lba: u64, count: u32) -> Result<(), ()> { |
| 338 | let d = unsafe { DISK.as_ref() }.ok_or(())?; | 387 | let d = active().ok_or(())?; |
| 339 | if lba + count as u64 > d.sectors { | 388 | if lba + count as u64 > d.sectors { |
| 340 | return Err(()); | 389 | return Err(()); |
| 341 | } | 390 | } |
| 342 | issue(d, ATA_WRITE_DMA_EXT, lba, count, true) | 391 | issue(d, ATA_WRITE_DMA_EXT, lba, count, true) |
| 343 | } | 392 | } |
| 344 | 393 | ||
| 345 | /// Pointer to the internal 4 KiB DMA buffer. | 394 | /// Pointer to the selected disk's 4 KiB DMA buffer. |
| 346 | pub fn buf() -> *mut u8 { | 395 | pub fn buf() -> *mut u8 { |
| 347 | unsafe { DISK.as_ref() } | 396 | active() |
| 348 | .map(|d| d.buf as *mut u8) | 397 | .map(|d| d.buf as *mut u8) |
| 349 | .unwrap_or(core::ptr::null_mut()) | 398 | .unwrap_or(core::ptr::null_mut()) |
| 350 | } | 399 | } |
| 351 | 400 | ||
| 352 | /// Total sector count of the attached disk. | 401 | /// Total sector count of the selected disk. |
| 353 | pub fn sector_count() -> u64 { | 402 | pub fn sector_count() -> u64 { |
| 354 | unsafe { DISK.as_ref() }.map(|d| d.sectors).unwrap_or(0) | 403 | active().map(|d| d.sectors).unwrap_or(0) |
| 355 | } | 404 | } |
