Skip to main content

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}