summaryrefslogtreecommitdiffstats
path: root/rsnix/src/ahci.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--rsnix/src/ahci.rs355
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
15use crate::pci;
16
17const SECTOR: usize = 512;
18
19// ABAR (HBA memory space) offsets.
20const GHC: usize = 0x04;
21const PI: usize = 0x0C;
22const PORT_BASE: usize = 0x100;
23const PORT_STRIDE: usize = 0x80;
24
25// Port register offsets (relative to the port's base).
26const P_CLB: usize = 0x00;
27const P_CLBU: usize = 0x04;
28const P_FB: usize = 0x08;
29const P_FBU: usize = 0x0C;
30const P_IS: usize = 0x10;
31const P_CMD: usize = 0x18;
32const P_TFD: usize = 0x20;
33const P_SSTS: usize = 0x28;
34const P_SCTL: usize = 0x2C;
35const P_CI: usize = 0x38;
36
37const CMD_ST: u32 = 1 << 0; // start
38const CMD_SUD: u32 = 1 << 1; // spin-up device
39const CMD_FRE: u32 = 1 << 4; // FIS receive enable
40const CMD_DMAE: u32 = 1 << 23; // DMA enable (QEMU/ICH9) — required or DMA commands silently transfer nothing
41const CMD_FR: u32 = 1 << 14; // FIS receive running
42const CMD_CR: u32 = 1 << 15; // command list running
43
44const ATA_IDENTIFY: u8 = 0xEC;
45const ATA_READ_DMA_EXT: u8 = 0x25;
46const ATA_WRITE_DMA_EXT: u8 = 0x35;
47
48/// One attached disk: register map + DMA structures.
49struct 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
58static mut DISK: Option<Disk> = None;
59
60fn reg(a: usize) -> u32 {
61 unsafe { core::ptr::read_volatile(a as *const u32) }
62}
63
64fn 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).
69fn 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.
93pub 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.
231fn 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.
328pub 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.
337pub 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.
346pub 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.
353pub fn sector_count() -> u64 {
354 unsafe { DISK.as_ref() }.map(|d| d.sectors).unwrap_or(0)
355}