summaryrefslogtreecommitdiffstats
path: root/rsnix
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-07-31 21:29:49 +0200
committersalmonik <salmonik@tuta.io>2026-07-31 21:29:49 +0200
commit3997ec4d2d7d6f69b08d0c04ce8de0df03c52a45 (patch)
tree60c5b079103e74d919a32b24276c536e69ce82f7 /rsnix
parentahci: SATA driver + FAT32 persistent disk mounted at /disk (diff)
downloadrs-dos-3997ec4d2d7d6f69b08d0c04ce8de0df03c52a45.tar.gz
rs-dos-3997ec4d2d7d6f69b08d0c04ce8de0df03c52a45.zip
ahci: scan all ports; fat: mount first valid FAT32 disk
The kernel previously started only AHCI port 0, so any launch where the firmware/QEMU put something else on port 0 (e.g. the ESP from an implicit -drive file=fat:rw:esp) left the real disk unnoticed: 'unsupported sector size 0' and no mount. - ahci.rs: enumerate CAP.NP/PI ports, start every port that links up (empty ports settle at DET=0 and are skipped quickly), keep up to 8 disks, and expose disk_count()/select() for the filesystem - fat.rs: try each disk in port order and mount the first valid FAT32 volume; non-FAT disks are logged and skipped - scripts/boot.sh: attach the data disk on ide.0 and the ESP on ide.1 (DISK_IMG env var, default /tmp/disk.img), matching the Makefile - Verified with the disk on port 0 (Makefile style), port 5 (old-style command line), and with the ESP on port 0; fsck.fat stays clean
Diffstat (limited to 'rsnix')
-rw-r--r--rsnix/src/ahci.rs173
-rw-r--r--rsnix/src/fat.rs25
2 files changed, 134 insertions, 64 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 @@
15use crate::pci; 17use crate::pci;
16 18
17const SECTOR: usize = 512; 19const SECTOR: usize = 512;
20const MAX_DISKS: usize = 8;
18 21
19// ABAR (HBA memory space) offsets. 22// ABAR (HBA memory space) offsets.
23const CAP: usize = 0x00;
20const GHC: usize = 0x04; 24const GHC: usize = 0x04;
21const PI: usize = 0x0C; 25const PI: usize = 0x0C;
22const PORT_BASE: usize = 0x100; 26const PORT_BASE: usize = 0x100;
@@ -46,6 +50,7 @@ const ATA_READ_DMA_EXT: u8 = 0x25;
46const ATA_WRITE_DMA_EXT: u8 = 0x35; 50const 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)]
49struct Disk { 54struct 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
58static mut DISK: Option<Disk> = None; 63static mut DISKS: [Option<Disk>; MAX_DISKS] = [None; MAX_DISKS];
64static mut DISK_COUNT: usize = 0;
65static mut ACTIVE: usize = 0;
59 66
60fn reg(a: usize) -> u32 { 67fn 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
93pub fn init() -> bool { 100/// the disk if a usable device answered.
94 let Some((dev, func)) = find_ahci() else { 101fn 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.
209pub 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.
253pub 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.
259pub 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. 372fn 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.
328pub fn read_sectors(lba: u64, count: u32) -> Result<(), ()> { 377pub 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.
337pub fn write_sectors(lba: u64, count: u32) -> Result<(), ()> { 386pub 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.
346pub fn buf() -> *mut u8 { 395pub 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.
353pub fn sector_count() -> u64 { 402pub 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}
diff --git a/rsnix/src/fat.rs b/rsnix/src/fat.rs
index 70fa688..616a08f 100644
--- a/rsnix/src/fat.rs
+++ b/rsnix/src/fat.rs
@@ -838,9 +838,30 @@ pub fn remove(parent_dir_cluster: u32, name: &str) -> Result<(), ()> {
838 838
839// --- mount ------------------------------------------------------------------- 839// --- mount -------------------------------------------------------------------
840 840
841/// Parse the BPB, cache the FAT and validate the volume. Returns true if 841/// Try to mount a FAT32 volume on every attached disk, in port order, and
842/// a usable FAT32 volume is mounted. 842/// keep the first one that validates. Returns true if a usable FAT32
843/// volume is mounted.
843pub fn init() -> bool { 844pub fn init() -> bool {
845 let disks = crate::ahci::disk_count();
846 if disks == 0 {
847 crate::serial_println!("fat: no disk");
848 return false;
849 }
850 for d in 0..disks {
851 if !crate::ahci::select(d) {
852 continue;
853 }
854 if try_mount() {
855 return true;
856 }
857 crate::serial_println!("fat: no FAT32 volume on disk {}", d);
858 }
859 false
860}
861
862/// Parse the BPB of the selected disk, cache the FAT and validate the
863/// volume.
864fn try_mount() -> bool {
844 let mut bpb = [0u8; SECTOR]; 865 let mut bpb = [0u8; SECTOR];
845 if read_sector(0, &mut bpb).is_err() { 866 if read_sector(0, &mut bpb).is_err() {
846 crate::serial_println!("fat: no disk"); 867 crate::serial_println!("fat: no disk");