diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-31 19:53:53 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-31 19:53:53 +0200 |
| commit | dc23d2f22cafeb6b97ba460789a834a6c586314d (patch) | |
| tree | 987bec11922073923c4c42bda84c0f670884dbca /rsnix/src/ahci.rs | |
| parent | vt: defer scrolls and batch framebuffer pushes (diff) | |
| download | rs-dos-dc23d2f22cafeb6b97ba460789a834a6c586314d.tar.gz rs-dos-dc23d2f22cafeb6b97ba460789a834a6c586314d.zip | |
ahci: SATA driver + FAT32 persistent disk mounted at /disk
- ahci.rs: polling AHCI on PCI class 1/6/1, bus mastering + ABAR map,
port 0 up, IDENTIFY + READ/WRITE DMA EXT through one 4KiB DMA buffer
- fat.rs: BPB parse, whole-FAT RAM cache with write-back to both copies,
LFN read/write, dirs, multi-cluster files, FSInfo free-count sync,
space-padded dot entries ('..' = cluster 0 under root), fsck-clean
- fs.rs: /disk mount node + 24-slot ephemeral node pool; ls/cat/hexdump/
echo/mkdir/touch/rm/edit all work on disk paths unchanged
- QEMU: explicit bus assignment (disk=ide.0, ESP=ide.1); the implicit
fat:rw:esp drive was stealing AHCI port 0
- Debugged against QEMU source: header DW0 is CFL in bits 4:0 / PRDTL in
bits 31:16 (Linux libahci layout), FIS count at bytes 12-13, device at
byte 7; QEMU rejects H2D FIS flags bits 6:4, so direction is the
header W bit only
- Verified: full shell round-trip on /disk, reboot persistence (poweroff
then mdir/mtype and a fresh boot), fsck.fat clean
Diffstat (limited to 'rsnix/src/ahci.rs')
| -rw-r--r-- | rsnix/src/ahci.rs | 355 |
1 files changed, 355 insertions, 0 deletions
diff --git a/rsnix/src/ahci.rs b/rsnix/src/ahci.rs new file mode 100644 index 0000000..83d0d27 --- /dev/null +++ b/rsnix/src/ahci.rs | |||
| @@ -0,0 +1,355 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // AHCI — SATA host-controller driver (polling, DMA, no interrupts). | ||
| 3 | // | ||
| 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 | ||
| 6 | // and exposes sector-level reads/writes for the FAT32 filesystem. | ||
| 7 | // | ||
| 8 | // Commands are issued through command-slot 0 with one 4 KiB DMA buffer; | ||
| 9 | // completion is polled via PxCI (no ISR, no MSI). Everything is | ||
| 10 | // identity-mapped, so "physical" DMA addresses are plain kernel pointers. | ||
| 11 | // The display is never involved, so this is a straightforward polling | ||
| 12 | // loop like the rest of the single-threaded kernel. | ||
| 13 | // --------------------------------------------------------------------------- | ||
| 14 | |||
| 15 | use crate::pci; | ||
| 16 | |||
| 17 | const SECTOR: usize = 512; | ||
| 18 | |||
| 19 | // ABAR (HBA memory space) offsets. | ||
| 20 | const GHC: usize = 0x04; | ||
| 21 | const PI: usize = 0x0C; | ||
| 22 | const PORT_BASE: usize = 0x100; | ||
| 23 | const PORT_STRIDE: usize = 0x80; | ||
| 24 | |||
| 25 | // Port register offsets (relative to the port's base). | ||
| 26 | const P_CLB: usize = 0x00; | ||
| 27 | const P_CLBU: usize = 0x04; | ||
| 28 | const P_FB: usize = 0x08; | ||
| 29 | const P_FBU: usize = 0x0C; | ||
| 30 | const P_IS: usize = 0x10; | ||
| 31 | const P_CMD: usize = 0x18; | ||
| 32 | const P_TFD: usize = 0x20; | ||
| 33 | const P_SSTS: usize = 0x28; | ||
| 34 | const P_SCTL: usize = 0x2C; | ||
| 35 | const P_CI: usize = 0x38; | ||
| 36 | |||
| 37 | const CMD_ST: u32 = 1 << 0; // start | ||
| 38 | const CMD_SUD: u32 = 1 << 1; // spin-up device | ||
| 39 | const CMD_FRE: u32 = 1 << 4; // FIS receive enable | ||
| 40 | const CMD_DMAE: u32 = 1 << 23; // DMA enable (QEMU/ICH9) — required or DMA commands silently transfer nothing | ||
| 41 | const CMD_FR: u32 = 1 << 14; // FIS receive running | ||
| 42 | const CMD_CR: u32 = 1 << 15; // command list running | ||
| 43 | |||
| 44 | const ATA_IDENTIFY: u8 = 0xEC; | ||
| 45 | const ATA_READ_DMA_EXT: u8 = 0x25; | ||
| 46 | const ATA_WRITE_DMA_EXT: u8 = 0x35; | ||
| 47 | |||
| 48 | /// One attached disk: register map + DMA structures. | ||
| 49 | struct Disk { | ||
| 50 | port: usize, // port register base = ABAR + 0x100 | ||
| 51 | clb: usize, // command list, 1024 B (32 × 32 B), 256 B aligned | ||
| 52 | fis: usize, // FIS receive area, 256 B aligned | ||
| 53 | ct: usize, // command table, 256 B, 128 B aligned | ||
| 54 | buf: usize, // 4 KiB DMA data buffer (page aligned) | ||
| 55 | sectors: u64, // total sectors (LBA48 from IDENTIFY) | ||
| 56 | } | ||
| 57 | |||
| 58 | static mut DISK: Option<Disk> = None; | ||
| 59 | |||
| 60 | fn reg(a: usize) -> u32 { | ||
| 61 | unsafe { core::ptr::read_volatile(a as *const u32) } | ||
| 62 | } | ||
| 63 | |||
| 64 | fn wreg(a: usize, v: u32) { | ||
| 65 | unsafe { core::ptr::write_volatile(a as *mut u32, v) } | ||
| 66 | } | ||
| 67 | |||
| 68 | /// Scan bus 0 for the AHCI controller (SATA, AHCI prog-if). | ||
| 69 | fn find_ahci() -> Option<(u8, u8)> { | ||
| 70 | for dev in 0..32u8 { | ||
| 71 | for func in 0..8u8 { | ||
| 72 | let id = pci::config_read(0, dev, func, 0); | ||
| 73 | if id == 0xFFFF_FFFF { | ||
| 74 | if func == 0 { | ||
| 75 | break; | ||
| 76 | } | ||
| 77 | continue; | ||
| 78 | } | ||
| 79 | let cc = pci::config_read(0, dev, func, 0x08); | ||
| 80 | let class = (cc >> 24) as u8; | ||
| 81 | let subclass = (cc >> 16) as u8; | ||
| 82 | let prog_if = (cc >> 8) as u8; | ||
| 83 | if class == 0x01 && subclass == 0x06 && prog_if == 0x01 { | ||
| 84 | return Some((dev, func)); | ||
| 85 | } | ||
| 86 | } | ||
| 87 | } | ||
| 88 | None | ||
| 89 | } | ||
| 90 | |||
| 91 | /// Probe, map and start the first AHCI port. Returns true if a disk is | ||
| 92 | /// ready for sector I/O. Called once at boot before the filesystem init. | ||
| 93 | pub fn init() -> bool { | ||
| 94 | let Some((dev, func)) = find_ahci() else { | ||
| 95 | return false; | ||
| 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 | |||
| 127 | // DMA structures: one 8 KiB allocation → page 0 = cmd list + FIS + cmd | ||
| 128 | // table, page 1 = data buffer. | ||
| 129 | let Some(mem) = crate::memory::alloc_pages(2) else { | ||
| 130 | crate::serial_println!("ahci: cannot allocate DMA pages"); | ||
| 131 | return false; | ||
| 132 | }; | ||
| 133 | let clb = mem; | ||
| 134 | let fis = mem + 1024; | ||
| 135 | let ct = mem + 1280; | ||
| 136 | let buf = mem + 4096; | ||
| 137 | |||
| 138 | // Stop the port if firmware left it running. | ||
| 139 | let mut cmd = reg(port + P_CMD); | ||
| 140 | if cmd & CMD_ST != 0 { | ||
| 141 | wreg(port + P_CMD, cmd & !CMD_ST); | ||
| 142 | let mut i = 0; | ||
| 143 | while reg(port + P_CMD) & CMD_CR != 0 && i < 1_000_000 { | ||
| 144 | i += 1; | ||
| 145 | } | ||
| 146 | cmd = reg(port + P_CMD); | ||
| 147 | } | ||
| 148 | if cmd & CMD_FRE != 0 { | ||
| 149 | wreg(port + P_CMD, cmd & !CMD_FRE); | ||
| 150 | let mut i = 0; | ||
| 151 | while reg(port + P_CMD) & CMD_FR != 0 && i < 1_000_000 { | ||
| 152 | i += 1; | ||
| 153 | } | ||
| 154 | } | ||
| 155 | unsafe { | ||
| 156 | core::ptr::write_bytes(clb as *mut u8, 0, 1024); | ||
| 157 | core::ptr::write_bytes(fis as *mut u8, 0, 256); | ||
| 158 | core::ptr::write_bytes(ct as *mut u8, 0, 256); | ||
| 159 | } | ||
| 160 | |||
| 161 | wreg(port + P_CLB, clb as u32); | ||
| 162 | wreg(port + P_CLBU, 0); | ||
| 163 | wreg(port + P_FB, fis as u32); | ||
| 164 | wreg(port + P_FBU, 0); | ||
| 165 | |||
| 166 | // Allow device detection (clear PxSCTL.DET/IPM), then wait for the | ||
| 167 | // link to come up (PxSSTS.DET == 3, communication established). | ||
| 168 | wreg(port + P_SCTL, 0); | ||
| 169 | let mut link_ok = false; | ||
| 170 | for _ in 0..10_000_000 { | ||
| 171 | if reg(port + P_SSTS) & 0xF == 3 { | ||
| 172 | link_ok = true; | ||
| 173 | break; | ||
| 174 | } | ||
| 175 | } | ||
| 176 | if !link_ok { | ||
| 177 | crate::serial_println!( | ||
| 178 | "ahci: no device on port {} (ssts={:#x})", | ||
| 179 | port_idx, | ||
| 180 | reg(port + P_SSTS) | ||
| 181 | ); | ||
| 182 | return false; | ||
| 183 | } | ||
| 184 | |||
| 185 | // Start the command engine (DMAE required for DMA commands). | ||
| 186 | wreg(port + P_CMD, CMD_FRE | CMD_SUD | CMD_DMAE); | ||
| 187 | wreg(port + P_CMD, CMD_ST | CMD_FRE | CMD_SUD | CMD_DMAE); | ||
| 188 | |||
| 189 | let mut d = Disk { | ||
| 190 | port, | ||
| 191 | clb, | ||
| 192 | fis, | ||
| 193 | ct, | ||
| 194 | buf, | ||
| 195 | sectors: 0, | ||
| 196 | }; | ||
| 197 | // IDENTIFY DEVICE as a sanity check + to learn the capacity. | ||
| 198 | if issue(&d, ATA_IDENTIFY, 0, 1, false).is_err() { | ||
| 199 | crate::serial_println!("ahci: IDENTIFY failed"); | ||
| 200 | return false; | ||
| 201 | } | ||
| 202 | let id = unsafe { core::slice::from_raw_parts(d.buf as *const u16, 256) }; | ||
| 203 | d.sectors = (id[100] as u64) | ||
| 204 | | ((id[101] as u64) << 16) | ||
| 205 | | ((id[102] as u64) << 32) | ||
| 206 | | ((id[103] as u64) << 48); | ||
| 207 | if d.sectors == 0 { | ||
| 208 | d.sectors = id[60] as u64; // LBA28 fallback | ||
| 209 | } | ||
| 210 | let mut model = [0u8; 40]; | ||
| 211 | for i in 0..20 { | ||
| 212 | model[i * 2] = (id[27 + i] >> 8) as u8; | ||
| 213 | model[i * 2 + 1] = id[27 + i] as u8; | ||
| 214 | } | ||
| 215 | let model = core::str::from_utf8(&model).unwrap_or("?").trim(); | ||
| 216 | crate::serial_println!( | ||
| 217 | "ahci: port {} up — {} sectors (~{} MB) \"{}\"", | ||
| 218 | port_idx, | ||
| 219 | d.sectors, | ||
| 220 | d.sectors / 2048, | ||
| 221 | model | ||
| 222 | ); | ||
| 223 | unsafe { | ||
| 224 | DISK = Some(d); | ||
| 225 | } | ||
| 226 | true | ||
| 227 | } | ||
| 228 | |||
| 229 | /// Issue one command through slot 0 and poll for completion. | ||
| 230 | /// `count` is sectors (1-8); data flows through the disk's DMA buffer. | ||
| 231 | fn issue(d: &Disk, cmd: u8, lba: u64, count: u32, write: bool) -> Result<(), ()> { | ||
| 232 | if count == 0 || count > 8 { | ||
| 233 | return Err(()); | ||
| 234 | } | ||
| 235 | // Wait for slot 0 to be free. | ||
| 236 | let mut i = 0; | ||
| 237 | while reg(d.port + P_CI) & 1 != 0 && i < 10_000_000 { | ||
| 238 | i += 1; | ||
| 239 | } | ||
| 240 | if reg(d.port + P_CI) & 1 != 0 { | ||
| 241 | return Err(()); | ||
| 242 | } | ||
| 243 | |||
| 244 | unsafe { | ||
| 245 | core::ptr::write_bytes(d.ct as *mut u8, 0, 256); | ||
| 246 | } | ||
| 247 | |||
| 248 | // Host-to-device FIS (20 bytes) at the command table start. | ||
| 249 | let fis = d.ct as *mut u8; | ||
| 250 | unsafe { | ||
| 251 | *fis = 0x27; // FIS type H2D | ||
| 252 | *(fis.add(1)) = 0x80; // C only — QEMU rejects flags bits 6:4 | ||
| 253 | *(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; | ||
| 258 | *(fis.add(5)) = (lba >> 8) as u8; | ||
| 259 | *(fis.add(6)) = (lba >> 16) as u8; | ||
| 260 | *(fis.add(7)) = 0x40 | (((lba >> 24) & 0x0F) as u8); | ||
| 261 | *(fis.add(8)) = (lba >> 24) as u8; | ||
| 262 | *(fis.add(9)) = (lba >> 32) as u8; | ||
| 263 | *(fis.add(10)) = (lba >> 40) as u8; | ||
| 264 | *(fis.add(11)) = 0; // features high | ||
| 265 | *(fis.add(12)) = count as u8; | ||
| 266 | *(fis.add(13)) = (count >> 8) as u8; | ||
| 267 | *(fis.add(14)) = 0; // ICC | ||
| 268 | *(fis.add(15)) = 0; // control | ||
| 269 | } | ||
| 270 | |||
| 271 | // PRD entry at the command table +0x80. | ||
| 272 | let prd = (d.ct + 0x80) as *mut u32; | ||
| 273 | unsafe { | ||
| 274 | *prd = d.buf as u32; | ||
| 275 | *prd.add(1) = 0; | ||
| 276 | *prd.add(2) = 0; | ||
| 277 | *prd.add(3) = (count * SECTOR as u32) - 1; // DBC, no I bit | ||
| 278 | } | ||
| 279 | |||
| 280 | // Command header 0. Layout per Linux libahci / QEMU AHCICmdHdr: | ||
| 281 | // DW0 bits 4:0 = CFL (FIS length in dwords), bit 6 = W, | ||
| 282 | // bits 31:16 = PRDTL; DW1 status; DW2/3 = command table address. | ||
| 283 | let hdr = d.clb as *mut u32; | ||
| 284 | unsafe { | ||
| 285 | *hdr = 5 | (1 << 16) | if write { 1 << 6 } else { 0 }; | ||
| 286 | *hdr.add(1) = 0; | ||
| 287 | *hdr.add(2) = d.ct as u32; | ||
| 288 | *hdr.add(3) = 0; | ||
| 289 | } | ||
| 290 | |||
| 291 | // Issue and poll. | ||
| 292 | wreg(d.port + P_CI, 1); | ||
| 293 | let mut i = 0; | ||
| 294 | while reg(d.port + P_CI) & 1 != 0 && i < 100_000_000 { | ||
| 295 | i += 1; | ||
| 296 | } | ||
| 297 | if reg(d.port + P_CI) & 1 != 0 { | ||
| 298 | crate::serial_println!( | ||
| 299 | "ahci: command {:#x} lba={} cnt={} timed out (tfd={:#x} ci={:#x} cmd={:#x} prdbc={:#x})", | ||
| 300 | cmd, | ||
| 301 | lba, | ||
| 302 | count, | ||
| 303 | reg(d.port + P_TFD), | ||
| 304 | reg(d.port + P_CI), | ||
| 305 | reg(d.port + P_CMD), | ||
| 306 | unsafe { *(d.clb as *const u32).add(1) } | ||
| 307 | ); | ||
| 308 | return Err(()); | ||
| 309 | } | ||
| 310 | // Error? Clear any port error bits either way. | ||
| 311 | if reg(d.port + P_TFD) & 1 != 0 { | ||
| 312 | wreg(d.port + 0x30, 0xFFFF_FFFF); // PxSERR: clear | ||
| 313 | crate::serial_println!( | ||
| 314 | "ahci: command {:#x} error (tfd={:#x})", | ||
| 315 | cmd, | ||
| 316 | reg(d.port + P_TFD) | ||
| 317 | ); | ||
| 318 | return Err(()); | ||
| 319 | } | ||
| 320 | let is = reg(d.port + P_IS); | ||
| 321 | if is != 0 { | ||
| 322 | wreg(d.port + P_IS, is); // write-1-to-clear | ||
| 323 | } | ||
| 324 | Ok(()) | ||
| 325 | } | ||
| 326 | |||
| 327 | /// DMA `count` sectors starting at `lba` into the internal buffer. | ||
| 328 | pub fn read_sectors(lba: u64, count: u32) -> Result<(), ()> { | ||
| 329 | let d = unsafe { DISK.as_ref() }.ok_or(())?; | ||
| 330 | if lba + count as u64 > d.sectors { | ||
| 331 | return Err(()); | ||
| 332 | } | ||
| 333 | issue(d, ATA_READ_DMA_EXT, lba, count, false) | ||
| 334 | } | ||
| 335 | |||
| 336 | /// DMA `count` sectors starting at `lba` out of the internal buffer. | ||
| 337 | pub fn write_sectors(lba: u64, count: u32) -> Result<(), ()> { | ||
| 338 | let d = unsafe { DISK.as_ref() }.ok_or(())?; | ||
| 339 | if lba + count as u64 > d.sectors { | ||
| 340 | return Err(()); | ||
| 341 | } | ||
| 342 | issue(d, ATA_WRITE_DMA_EXT, lba, count, true) | ||
| 343 | } | ||
| 344 | |||
| 345 | /// Pointer to the internal 4 KiB DMA buffer. | ||
| 346 | pub fn buf() -> *mut u8 { | ||
| 347 | unsafe { DISK.as_ref() } | ||
| 348 | .map(|d| d.buf as *mut u8) | ||
| 349 | .unwrap_or(core::ptr::null_mut()) | ||
| 350 | } | ||
| 351 | |||
| 352 | /// Total sector count of the attached disk. | ||
| 353 | pub fn sector_count() -> u64 { | ||
| 354 | unsafe { DISK.as_ref() }.map(|d| d.sectors).unwrap_or(0) | ||
| 355 | } | ||
