rustyn64_cart/pi.rs
1//! The PI (Peripheral Interface) DMA engine (T-14-001).
2//!
3//! Moves bytes between RDRAM and the cartridge address space. Pulled **forward
4//! from Phase 5 into Phase 1** because n64-systemtest loads the rest of its own
5//! ELF from cart through PI, so the Phase 1 exit criterion — and with it the
6//! v0.2.0 cut criterion — is unreachable without it.
7//!
8//! # Register map
9//!
10//! From `n64brew_wiki/markdown/Peripheral Interface.md`:
11//!
12//! | Address | Register | Effect |
13//! | --- | --- | --- |
14//! | `0x0460_0000` | `PI_DRAM_ADDR` | RDRAM side of the transfer |
15//! | `0x0460_0004` | `PI_CART_ADDR` | cart side |
16//! | `0x0460_0008` | `PI_RD_LEN` | **write triggers** RDRAM → cart |
17//! | `0x0460_000C` | `PI_WR_LEN` | **write triggers** cart → RDRAM |
18//! | `0x0460_0010` | `PI_STATUS` | read: busy flags; write: reset / clear IRQ |
19//!
20//! # The two rules that bite
21//!
22//! - **Length is `len + 1` bytes.** Writing 0 transfers one byte, not zero. An
23//! implementation that transfers `len` is short by one on every single DMA,
24//! which corrupts the *last* byte of every block — a failure that looks like
25//! memory corruption rather than a DMA bug.
26//! - **`RD` and `WR` are named from the cartridge's point of view**, so
27//! `PI_WR_LEN` — the one everything actually uses — moves data **cart →
28//! RDRAM**. Getting them the wrong way round makes the first ROM load write
29//! the ROM's own image over itself with uninitialized RDRAM.
30
31use serde::{Deserialize, Serialize};
32
33/// Base of the PI register block.
34pub const PI_BASE: u32 = 0x0460_0000;
35
36/// `PI_DRAM_ADDR`.
37pub const PI_DRAM_ADDR: u32 = 0x0460_0000;
38/// `PI_CART_ADDR`.
39pub const PI_CART_ADDR: u32 = 0x0460_0004;
40/// `PI_RD_LEN` — writing it starts an RDRAM → cart transfer.
41pub const PI_RD_LEN: u32 = 0x0460_0008;
42/// `PI_WR_LEN` — writing it starts a cart → RDRAM transfer.
43pub const PI_WR_LEN: u32 = 0x0460_000C;
44/// `PI_STATUS`.
45pub const PI_STATUS: u32 = 0x0460_0010;
46/// `PI_BSD_DOM1_LAT` — domain-1 latency. DOM2's block follows at `+0x10`.
47pub const PI_BSD_DOM1_LAT: u32 = 0x0460_0014;
48/// `PI_BSD_DOM1_PWD` — domain-1 pulse width.
49pub const PI_BSD_DOM1_PWD: u32 = 0x0460_0018;
50/// `PI_BSD_DOM1_PGS` — domain-1 page size (`2^(PGS+2)` bytes).
51pub const PI_BSD_DOM1_PGS: u32 = 0x0460_001C;
52/// `PI_BSD_DOM1_RLS` — domain-1 release.
53pub const PI_BSD_DOM1_RLS: u32 = 0x0460_0020;
54/// `PI_BSD_DOM2_LAT` — domain-2 latency (the SRAM / `FlashRAM` bus).
55pub const PI_BSD_DOM2_LAT: u32 = 0x0460_0024;
56/// `PI_BSD_DOM2_PWD` — domain-2 pulse width.
57pub const PI_BSD_DOM2_PWD: u32 = 0x0460_0028;
58/// `PI_BSD_DOM2_PGS` — domain-2 page size.
59pub const PI_BSD_DOM2_PGS: u32 = 0x0460_002C;
60/// `PI_BSD_DOM2_RLS` — domain-2 release (end of the register block).
61pub const PI_BSD_DOM2_RLS: u32 = 0x0460_0030;
62
63/// `PI_STATUS` bit 0 — a DMA is in progress.
64pub const STATUS_DMA_BUSY: u32 = 1 << 0;
65/// `PI_STATUS` bit 1 — an I/O transfer is in progress.
66pub const STATUS_IO_BUSY: u32 = 1 << 1;
67/// `PI_STATUS` bit 3 — the PI interrupt is asserted.
68pub const STATUS_INTERRUPT: u32 = 1 << 3;
69
70/// `PI_STATUS` write bit 0 — reset the controller and abort any DMA.
71pub const STATUS_W_RESET: u32 = 1 << 0;
72/// `PI_STATUS` write bit 1 — clear the PI interrupt.
73pub const STATUS_W_CLR_INTR: u32 = 1 << 1;
74
75/// A transfer the PI has been asked to perform.
76#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
77pub struct Transfer {
78 /// RDRAM address.
79 pub dram: u32,
80 /// Cartridge address.
81 pub cart: u32,
82 /// Byte count — already `len + 1`, so this is the real length.
83 pub len: u32,
84 /// Direction: cart → RDRAM (a `PI_WR_LEN` write).
85 pub to_dram: bool,
86}
87
88/// The PI register file and DMA state.
89#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
90pub struct Pi {
91 dram_addr: u32,
92 cart_addr: u32,
93 /// Set while a transfer is outstanding.
94 busy: bool,
95 /// The PI interrupt line, which the MI aggregates into `Cause.IP2`.
96 interrupt: bool,
97 /// Per-domain bus timing (index 0 = DOM1, 1 = DOM2), each a register the
98 /// game/IPL2 programs. LAT/PWD are 8-bit, PGS 4-bit, RLS 2-bit. They store
99 /// and read back here; deriving DMA duration from them (the non-instant
100 /// completion) is a follow-up that reads these fields.
101 dom_lat: [u8; 2],
102 dom_pwd: [u8; 2],
103 dom_pgs: [u8; 2],
104 dom_rls: [u8; 2],
105}
106
107impl Pi {
108 /// Power-on state.
109 #[must_use]
110 pub const fn new() -> Self {
111 Self {
112 dram_addr: 0,
113 cart_addr: 0,
114 busy: false,
115 interrupt: false,
116 dom_lat: [0; 2],
117 dom_pwd: [0; 2],
118 dom_pgs: [0; 2],
119 dom_rls: [0; 2],
120 }
121 }
122
123 /// Map a PI register address to its `(domain, field)` if it names a BSD
124 /// timing register. `domain` is 0 (DOM1, base `0x14`) or 1 (DOM2, base
125 /// `0x24`); `field` is 0=LAT, 1=PWD, 2=PGS, 3=RLS at `+0/4/8/C`.
126 const fn dom_field(addr: u32) -> Option<(usize, usize)> {
127 let off = (addr & !3).wrapping_sub(PI_BSD_DOM1_LAT);
128 // DOM1 spans 0x00..0x10 from LAT; DOM2 the next 0x10.
129 let (domain, within) = if off < 0x10 {
130 (0, off)
131 } else if off < 0x20 {
132 (1, off - 0x10)
133 } else {
134 return None;
135 };
136 Some((domain, (within / 4) as usize))
137 }
138
139 /// Is the PI asserting its interrupt?
140 #[must_use]
141 pub const fn interrupt(&self) -> bool {
142 self.interrupt
143 }
144
145 /// Read a PI register.
146 #[must_use]
147 pub const fn read(&self, addr: u32) -> u32 {
148 match addr & !3 {
149 PI_DRAM_ADDR => self.dram_addr,
150 PI_CART_ADDR => self.cart_addr,
151 PI_STATUS => {
152 let mut s = 0;
153 if self.busy {
154 // Both flags together: software polls `io_busy` (as
155 // n64-systemtest's ISViewer does) and `dma_busy`
156 // interchangeably to mean "the PI is occupied".
157 s |= STATUS_DMA_BUSY | STATUS_IO_BUSY;
158 }
159 if self.interrupt {
160 s |= STATUS_INTERRUPT;
161 }
162 s
163 }
164 // The BSD domain timing registers store and read back (masked to
165 // their field widths). The length registers read back as 0x7F on
166 // hardware (N64brew *Peripheral Interface* §PI_RD_LEN/PI_WR_LEN);
167 // returning 0 for those is a documented simplification, not modeled.
168 addr => {
169 if let Some((d, field)) = Self::dom_field(addr) {
170 match field {
171 0 => self.dom_lat[d] as u32,
172 1 => self.dom_pwd[d] as u32,
173 2 => (self.dom_pgs[d] & 0xF) as u32,
174 _ => (self.dom_rls[d] & 0x3) as u32,
175 }
176 } else {
177 0
178 }
179 }
180 }
181 }
182
183 /// Write a PI register, returning a [`Transfer`] if the write started one.
184 ///
185 /// The transfer is **returned rather than performed** because the PI does
186 /// not own RDRAM — the Bus does. Performing it here would need the engine to
187 /// hold a reference back to the Bus that owns it, which is the cycle the
188 /// whole architecture is built to avoid.
189 pub const fn write(&mut self, addr: u32, val: u32) -> Option<Transfer> {
190 match addr & !3 {
191 PI_DRAM_ADDR => {
192 // Bits 2:0 are ignored: the RDRAM side is **doubleword**
193 // aligned, not halfword. Masking only bit 0 lets a transfer
194 // start mid-doubleword, which silently shifts every byte of a
195 // DMA whose caller relied on the hardware aligning it.
196 self.dram_addr = val & 0x00FF_FFF8;
197 None
198 }
199 PI_CART_ADDR => {
200 self.cart_addr = val & 0xFFFF_FFFE;
201 None
202 }
203 PI_RD_LEN => Some(self.start(val, false)),
204 PI_WR_LEN => Some(self.start(val, true)),
205 PI_STATUS => {
206 if val & STATUS_W_RESET != 0 {
207 self.busy = false;
208 }
209 if val & STATUS_W_CLR_INTR != 0 {
210 self.interrupt = false;
211 }
212 None
213 }
214 addr => {
215 // The BSD domain timing registers store the written value
216 // (masked to their field widths). Deriving DMA duration from
217 // them is a follow-up; the DMA still completes immediately.
218 if let Some((d, field)) = Self::dom_field(addr) {
219 match field {
220 0 => self.dom_lat[d] = val as u8,
221 1 => self.dom_pwd[d] = val as u8,
222 2 => self.dom_pgs[d] = (val & 0xF) as u8,
223 _ => self.dom_rls[d] = (val & 0x3) as u8,
224 }
225 }
226 None
227 }
228 }
229 }
230
231 /// Begin a transfer of `len + 1` bytes.
232 const fn start(&mut self, len: u32, to_dram: bool) -> Transfer {
233 self.busy = true;
234 // "+1" is the rule everything gets wrong once: writing 0 transfers
235 // ONE byte. Being short by one corrupts the last byte of every
236 // block, which presents as memory corruption rather than a DMA bug.
237 let len = (len & 0x00FF_FFFF) + 1;
238 let t = Transfer {
239 dram: self.dram_addr,
240 cart: self.cart_addr,
241 len,
242 to_dram,
243 };
244 // **Both address registers ADVANCE by the transfer length.** Hardware
245 // walks them as the DMA proceeds, so software reads back the address
246 // *past* the block it just moved -- which is how a driver chains
247 // transfers without rewriting the address each time.
248 //
249 // Leaving them put makes every chained DMA re-transfer the first block,
250 // and n64-systemtest checks the delta directly: after a 0x10-byte
251 // transfer it expects `PI_CART_ADDR` to have moved by `0x10`, and for a
252 // written length of 1 (two bytes) by `0x2`.
253 //
254 // Advanced here rather than on completion because this DMA is
255 // instantaneous; the observable end state is identical, and charging it
256 // real time later moves this line rather than rewriting it.
257 self.dram_addr = self.dram_addr.wrapping_add(len);
258 self.cart_addr = self.cart_addr.wrapping_add(len);
259 t
260 }
261
262 /// Mark the current transfer complete and raise the PI interrupt.
263 ///
264 /// Separate from [`Pi::write`] because completion is a *timing* event: the
265 /// Bus performs the copy and then tells the PI it is done, which is where a
266 /// non-instant DMA will later charge its cycles.
267 pub const fn complete(&mut self) {
268 self.busy = false;
269 self.interrupt = true;
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 /// **Length is `len + 1`.** Writing 0 transfers one byte.
278 #[test]
279 fn a_transfer_is_len_plus_one_bytes() {
280 let mut pi = Pi::new();
281 let t = pi.write(PI_WR_LEN, 0).expect("started");
282 assert_eq!(t.len, 1, "writing 0 transfers ONE byte, not zero");
283 let t = pi.write(PI_WR_LEN, 0xFF).expect("started");
284 assert_eq!(t.len, 0x100);
285 }
286
287 /// `RD`/`WR` are named from the **cartridge's** point of view, so `WR_LEN`
288 /// moves cart → RDRAM. Reversing them makes the first ROM load overwrite the
289 /// image with uninitialized RDRAM.
290 #[test]
291 fn wr_len_moves_cart_to_dram_and_rd_len_the_other_way() {
292 let mut pi = Pi::new();
293 assert!(
294 pi.write(PI_WR_LEN, 0).expect("started").to_dram,
295 "PI_WR_LEN loads INTO RDRAM"
296 );
297 assert!(
298 !pi.write(PI_RD_LEN, 0).expect("started").to_dram,
299 "PI_RD_LEN writes out to the cart"
300 );
301 }
302
303 /// Only the length writes start a transfer; the address writes do not.
304 #[test]
305 fn only_a_length_write_starts_a_transfer() {
306 let mut pi = Pi::new();
307 assert!(pi.write(PI_DRAM_ADDR, 0x1000).is_none());
308 assert!(pi.write(PI_CART_ADDR, 0x1000_0000).is_none());
309 assert!(pi.write(PI_STATUS, 0).is_none());
310 let t = pi.write(PI_WR_LEN, 15).expect("started");
311 assert_eq!(t.dram, 0x1000, "the addresses latched first are used");
312 assert_eq!(t.cart, 0x1000_0000);
313 assert_eq!(t.len, 16);
314 }
315
316 /// **Both address registers advance by the transfer length.** Software reads
317 /// back the address past the block it just moved, which is how a driver
318 /// chains transfers; leaving them put makes every chained DMA re-send the
319 /// first block. n64-systemtest checks the delta directly.
320 #[test]
321 fn a_transfer_advances_both_address_registers_by_its_length() {
322 let mut pi = Pi::new();
323 pi.write(PI_DRAM_ADDR, 0x1000);
324 pi.write(PI_CART_ADDR, 0x1000_0000);
325 pi.write(PI_WR_LEN, 0x0F); // 0x10 bytes
326 assert_eq!(pi.read(PI_DRAM_ADDR), 0x1010, "moved by the LENGTH, 0x10");
327 assert_eq!(pi.read(PI_CART_ADDR), 0x1000_0010);
328
329 // A written length of 1 is a TWO-byte transfer, so the delta is 2.
330 let mut pi = Pi::new();
331 pi.write(PI_CART_ADDR, 0x1000_0000);
332 pi.write(PI_WR_LEN, 1);
333 assert_eq!(pi.read(PI_CART_ADDR), 0x1000_0002);
334 }
335
336 /// Busy is visible through **both** status flags, because software polls
337 /// them interchangeably — `n64-systemtest`'s `ISViewer` waits on `io_busy`.
338 #[test]
339 fn busy_is_visible_through_both_status_flags() {
340 let mut pi = Pi::new();
341 assert_eq!(pi.read(PI_STATUS) & (STATUS_DMA_BUSY | STATUS_IO_BUSY), 0);
342 pi.write(PI_WR_LEN, 0);
343 let s = pi.read(PI_STATUS);
344 assert_ne!(s & STATUS_DMA_BUSY, 0);
345 assert_ne!(s & STATUS_IO_BUSY, 0, "ISViewer polls io_busy specifically");
346 pi.complete();
347 assert_eq!(pi.read(PI_STATUS) & (STATUS_DMA_BUSY | STATUS_IO_BUSY), 0);
348 }
349
350 /// Completion raises the PI interrupt, and only a `STATUS` write with the
351 /// clear bit takes it down. A DMA that never raises leaves software that
352 /// waits on the interrupt hung forever.
353 #[test]
354 fn completion_raises_an_interrupt_that_only_software_clears() {
355 let mut pi = Pi::new();
356 pi.write(PI_WR_LEN, 0);
357 assert!(!pi.interrupt());
358 pi.complete();
359 assert!(pi.interrupt(), "completion raises the PI interrupt");
360 assert_ne!(pi.read(PI_STATUS) & STATUS_INTERRUPT, 0);
361
362 // Another DMA does not clear it...
363 pi.write(PI_WR_LEN, 0);
364 assert!(pi.interrupt(), "still asserted");
365 // ...only the explicit clear does.
366 pi.write(PI_STATUS, STATUS_W_CLR_INTR);
367 assert!(!pi.interrupt());
368 }
369
370 /// **The BSD domain timing registers store and read back**, masked to their
371 /// field widths (LAT/PWD 8-bit, PGS 4-bit, RLS 2-bit), for both DOM1 and
372 /// DOM2 independently. IPL2 programs DOM1 from the ROM header (LAT 64, PWD
373 /// 18, PGS 7, RLS 3 for official ROMs — N64brew *Peripheral Interface*
374 /// §Domains), so a game that reads them back must see what it wrote.
375 #[test]
376 fn the_bsd_domain_timing_registers_store_and_read_back() {
377 let mut pi = Pi::new();
378 // DOM1 to the official-ROM values.
379 pi.write(PI_BSD_DOM1_LAT, 64);
380 pi.write(PI_BSD_DOM1_PWD, 18);
381 pi.write(PI_BSD_DOM1_PGS, 7);
382 pi.write(PI_BSD_DOM1_RLS, 3);
383 assert_eq!(pi.read(PI_BSD_DOM1_LAT), 64);
384 assert_eq!(pi.read(PI_BSD_DOM1_PWD), 18);
385 assert_eq!(pi.read(PI_BSD_DOM1_PGS), 7);
386 assert_eq!(pi.read(PI_BSD_DOM1_RLS), 3);
387 // PGS is 4-bit and RLS 2-bit — the high bits are dropped.
388 pi.write(PI_BSD_DOM1_PGS, 0xFF);
389 assert_eq!(pi.read(PI_BSD_DOM1_PGS), 0xF);
390 pi.write(PI_BSD_DOM1_RLS, 0xFF);
391 assert_eq!(pi.read(PI_BSD_DOM1_RLS), 0x3);
392 // DOM2 is independent of DOM1 across every field.
393 pi.write(PI_BSD_DOM2_LAT, 0x20);
394 assert_eq!(pi.read(PI_BSD_DOM2_LAT), 0x20);
395 pi.write(PI_BSD_DOM2_PWD, 0x21);
396 assert_eq!(pi.read(PI_BSD_DOM2_PWD), 0x21);
397 pi.write(PI_BSD_DOM2_PGS, 0xFF);
398 assert_eq!(pi.read(PI_BSD_DOM2_PGS), 0xF, "DOM2 PGS is 4-bit");
399 assert_eq!(
400 pi.read(PI_BSD_DOM1_LAT),
401 64,
402 "DOM2 write did not touch DOM1"
403 );
404 assert_eq!(
405 pi.read(PI_BSD_DOM1_PWD),
406 0x12,
407 "DOM2 write did not touch DOM1 PWD"
408 );
409 assert_eq!(pi.read(PI_BSD_DOM2_RLS), 0, "unwritten DOM2 RLS reads 0");
410 }
411
412 /// The RDRAM address ignores bits 2:0 — the DRAM side is **doubleword**
413 /// aligned. Masking only bit 0 lets a transfer start mid-doubleword and
414 /// silently shifts every byte of it.
415 #[test]
416 fn the_dram_address_is_doubleword_aligned() {
417 let mut pi = Pi::new();
418 for probe in [0x1001u32, 0x1002, 0x1004, 0x1007] {
419 pi.write(PI_DRAM_ADDR, probe);
420 assert_eq!(
421 pi.write(PI_WR_LEN, 0).expect("started").dram,
422 0x1000,
423 "{probe:#X} must round down to the doubleword"
424 );
425 }
426 }
427}