Skip to main content

rustyn64_cpu/
sysad.rs

1//! The `SysAD` bus transaction model (T-11-008).
2//!
3//! The CPU reaches everything outside its caches through `SysAD`, a **packet
4//! protocol** — not a simple addressed read. A transaction is an *address cycle*
5//! carrying a command, then a *data cycle* carrying the payload, handshaked by
6//! `EOK` / `Pvalid` / `Evalid`, with an **unbounded** wait between them. That wait
7//! is where real RDRAM latency lives.
8//!
9//! # Why model it at all
10//!
11//! Neither reference emulator does. CEN64 completes the whole access atomically
12//! in zero emulated time and charges a flat constant (its own source says
13//! `// Currently using fixed values....`); ares charges different constants. They
14//! disagree on the value and neither derived it from a spec. Modeling the split
15//! is where this project can be better rather than equal — and it is what makes
16//! the bus access a point the scheduler can interleave the RCP around, which is
17//! the whole reason ADR 0007 models a pipeline.
18//!
19//! # Clock domain
20//!
21//! `SysAD` runs at `SClock` = `MClock` = **62.5 MHz**, so one bus cycle is 1.5
22//! `PCycle`s — 3 master ticks against the CPU's 2 (ADR 0006). A transaction is
23//! therefore *not* a whole number of CPU cycles, which is exactly the
24//! "**1 to 2** `PCycle`s: synchronize with `SClock`" indeterminacy the manual
25//! charges in Table 11-1.
26
27use serde::{Deserialize, Serialize};
28
29/// Which half of a transaction is on the wire.
30///
31/// # The polarity, and a contradiction that turns out not to be one
32///
33/// `docs/accuracy-ledger.md` recorded S-1: the User's Manual says command =
34/// `SysCmd4` **0** while the wiki's cheat sheet says **1**. Reading both
35/// carefully, they **agree on every bit value** and disagree only on English.
36///
37/// - UM §12.11.1: *"During address cycles \[`SysCmd4` = 0\] … contains a System
38///   interface command"*, and *"During data cycles \[`SysCmd4` = 1\]"*.
39/// - The wiki table gives read/write **requests** bit 4 = 0 (labeled "Data
40///   req") and data-carrying cycles bit 4 = 1 (labeled "Command").
41///
42/// So a request always has bit 4 clear and a data beat always has it set, in both
43/// sources. The wiki simply uses "Command" for the cycle the manual calls a data
44/// identifier. We follow the **manual's** naming, since it is the vendor spec and
45/// the rest of this crate cites it.
46#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
47pub enum Phase {
48    /// The address cycle: `SysCmd4 = 0`, carrying a command in `SysCmd(3:0)`.
49    Address,
50    /// The data cycle: `SysCmd4 = 1`, carrying a data identifier.
51    Data,
52}
53
54impl Phase {
55    /// `SysCmd4` for this phase.
56    #[must_use]
57    pub const fn syscmd4(self) -> bool {
58        matches!(self, Self::Data)
59    }
60}
61
62/// Transfer size, encoded in `SysCmd(1:0)` of a request.
63#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
64pub enum Width {
65    /// 32-bit single transfer. All 8/16/24/32-bit reads are issued as this; the
66    /// CPU does the shifting internally.
67    Single32,
68    /// 64-bit.
69    Single64,
70    /// 128-bit block — a D-cache line.
71    Block128,
72    /// 256-bit block — an I-cache line.
73    Block256,
74}
75
76impl Width {
77    /// Bytes moved.
78    #[must_use]
79    pub const fn bytes(self) -> u32 {
80        match self {
81            Self::Single32 => 4,
82            Self::Single64 => 8,
83            Self::Block128 => 16,
84            Self::Block256 => 32,
85        }
86    }
87
88    /// 32-bit beats on the bus, which is what the data phase costs.
89    ///
90    /// **The `VR4300`'s `SysAD` is 32 bits wide, not 64.** This is the headline
91    /// cost reduction against the `R4400` and it is easy to get wrong, because
92    /// the CPU is a 64-bit machine internally and most `MIPS` III documentation
93    /// describes a 64-bit external bus. The manual is explicit (§1.3): *"It
94    /// contains a **32-bit** multiplexed address/data bus, with per-byte parity …
95    /// It is not compatible with the System interface bus used on the `VR4400`"*,
96    /// and every signal reference in it is `SysAD(31:0)`.
97    ///
98    /// So a 128-bit `D-cache` line is **4** beats and a 256-bit `I-cache` line is
99    /// **8**, not 2 and 4. Halving these would make every cache fill finish twice
100    /// as fast as hardware.
101    #[must_use]
102    pub const fn beats(self) -> u32 {
103        self.bytes() / 4
104    }
105}
106
107/// The order a block transfer's 64-bit halves arrive in.
108#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
109pub enum BlockOrder {
110    /// Lowest address first.
111    Sequential,
112    /// The **requested** 64 bits first, then the 64 bits *below* it.
113    ///
114    /// Not "the ones after it" — this is the trap. A D-cache 128-bit read whose
115    /// address bit 4 is set returns the addressed half and then the *preceding*
116    /// half (`n64brew_wiki/markdown/SysAD Interface.md`, citing UM p.339).
117    SubBlock,
118}
119
120/// The ordering a read of `width` at `addr` uses.
121///
122/// D-cache 128-bit reads are sub-block ordered when address bit 4 is set;
123/// everything else, including all I-cache 256-bit reads, is sequential. Getting
124/// this wrong corrupts every other cache line fill and nothing else.
125#[must_use]
126pub const fn block_order(width: Width, addr: u32) -> BlockOrder {
127    match width {
128        Width::Block128 if addr & 0x10 != 0 => BlockOrder::SubBlock,
129        // I-cache reads are always 256-bit aligned and always sequential:
130        // "there is no smarts in the 4300i CPU to know if all of the cache entry
131        // is full", so the whole line is fetched in order.
132        _ => BlockOrder::Sequential,
133    }
134}
135
136/// A bus transaction in progress.
137///
138/// Modeled as a small state machine rather than an atomic operation, so the
139/// scheduler can advance the RCP *between* the address and data phases — the
140/// property that makes a device able to observe the bus mid-transaction.
141#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
142pub struct Transaction {
143    /// Physical address.
144    pub addr: u32,
145    /// Transfer size.
146    pub width: Width,
147    /// Is this a write? Writes are throttled by `EOK` rather than waiting for
148    /// `Evalid`.
149    pub write: bool,
150    /// Which phase is on the wire.
151    pub phase: Phase,
152    /// `SClock` cycles remaining in the current phase.
153    pub remaining: u32,
154}
155
156impl Transaction {
157    /// Begin a transaction. It starts in the address phase, which occupies one
158    /// `SClock` cycle.
159    #[must_use]
160    pub const fn begin(addr: u32, width: Width, write: bool) -> Self {
161        Self {
162            addr,
163            width,
164            write,
165            phase: Phase::Address,
166            remaining: 1,
167        }
168    }
169
170    /// Advance one `SClock` cycle. Returns `true` once the transaction completes.
171    ///
172    /// `service_cycles` is the target's latency — the unbounded wait between the
173    /// phases, and where `M` lives (`docs/accuracy-ledger.md` C-1). It is a
174    /// parameter rather than a constant precisely so it cannot be quietly tuned:
175    /// the caller must supply a value it can justify.
176    pub const fn step(&mut self, service_cycles: u32) -> bool {
177        if self.remaining > 0 {
178            self.remaining -= 1;
179        }
180        if self.remaining > 0 {
181            return false;
182        }
183        match self.phase {
184            Phase::Address => {
185                self.phase = Phase::Data;
186                // The target's latency, then one bus cycle per 32-bit beat.
187                // Saturating: `service_cycles` may ultimately be derived from
188                // guest-writable timing registers (PI/RI), so a hostile or
189                // careless value must not panic a debug build.
190                self.remaining = service_cycles.saturating_add(self.width.beats());
191                false
192            }
193            Phase::Data => true,
194        }
195    }
196
197    /// Total `SClock` cycles this transaction occupies, given a target latency.
198    ///
199    /// One address cycle, the target's service time, then one cycle per beat.
200    #[must_use]
201    pub const fn total_cycles(width: Width, service_cycles: u32) -> u32 {
202        1 + service_cycles + width.beats()
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    /// The polarity, settled. Both sources agree on the bit; they disagree only
211    /// on which cycle they call "command".
212    #[test]
213    fn syscmd4_is_clear_for_a_request_and_set_for_data() {
214        assert!(!Phase::Address.syscmd4(), "UM 12.11.1: address cycle = 0");
215        assert!(Phase::Data.syscmd4(), "UM 12.11.1: data cycle = 1");
216    }
217
218    /// **The bus is 32 bits wide.** Pinned because the natural assumption is 64
219    /// — the CPU is a 64-bit machine and most MIPS III material describes a
220    /// 64-bit external interface. The VR4300 narrowed it, and halving these
221    /// counts would make every cache fill complete twice as fast as hardware.
222    #[test]
223    fn the_bus_is_32_bits_wide_not_64() {
224        assert_eq!(Width::Single32.beats(), 1);
225        assert_eq!(Width::Single64.beats(), 2, "a 64-bit transfer is TWO beats");
226        assert_eq!(Width::Block128.beats(), 4, "a D-cache line is FOUR beats");
227        assert_eq!(Width::Block256.beats(), 8, "an I-cache line is EIGHT beats");
228    }
229
230    #[test]
231    fn widths_are_the_documented_transfer_sizes() {
232        assert_eq!(Width::Single32.bytes(), 4);
233        assert_eq!(Width::Single64.bytes(), 8);
234        assert_eq!(Width::Block128.bytes(), 16, "a D-cache line");
235        assert_eq!(Width::Block256.bytes(), 32, "an I-cache line");
236        // The bus is 32 bits wide, so beats are what the data phase costs.
237        assert_eq!(Width::Block256.beats(), 8);
238    }
239
240    /// **The sub-block ordering quirk.** A D-cache 128-bit read whose address
241    /// bit 4 is set returns the addressed 64 bits first, then the 64 bits
242    /// *below* — not the ones after. Getting this wrong corrupts every other
243    /// cache line fill and nothing else, which is a miserable way to find it.
244    #[test]
245    fn dcache_block_reads_use_sub_block_ordering_when_address_bit_4_is_set() {
246        // Bit 4 is the 0x10 bit, so it alternates every 16 bytes -- NOT every
247        // 8. My first version of this test asserted 0x...70 and 0x...78 were
248        // sequential; both have bit 4 set. Values below are computed, not
249        // eyeballed.
250        for addr in [0x0000u32, 0x0020, 0x0040, 0x1234_5660] {
251            assert_eq!(
252                block_order(Width::Block128, addr),
253                BlockOrder::Sequential,
254                "{addr:#X} has bit 4 clear"
255            );
256        }
257        for addr in [0x0010u32, 0x0030, 0x1234_5670, 0x1234_5678] {
258            assert_eq!(
259                block_order(Width::Block128, addr),
260                BlockOrder::SubBlock,
261                "{addr:#X} has bit 4 set"
262            );
263        }
264
265        // I-cache reads are ALWAYS sequential, whatever bit 4 says -- the CPU has
266        // no way to use a partial line, so it always fetches the whole thing in
267        // order.
268        for addr in [0x0000u32, 0x0010, 0x0020, 0x0030] {
269            assert_eq!(
270                block_order(Width::Block256, addr),
271                BlockOrder::Sequential,
272                "I-cache read at {addr:#X}"
273            );
274        }
275        // Single transfers have no ordering to get wrong.
276        assert_eq!(block_order(Width::Single32, 0x10), BlockOrder::Sequential);
277    }
278
279    /// A transaction is a state machine, not an atomic operation. It must pass
280    /// through the address phase before the data phase, so the scheduler has a
281    /// point at which to step the RCP.
282    #[test]
283    fn a_transaction_passes_through_both_phases() {
284        let mut t = Transaction::begin(0x1000, Width::Single32, false);
285        assert_eq!(t.phase, Phase::Address);
286
287        // The address cycle completes and hands over to the data phase.
288        assert!(!t.step(0), "one address cycle is not a whole transaction");
289        assert_eq!(t.phase, Phase::Data, "must reach the data phase");
290
291        // One beat for a 32-bit transfer.
292        assert!(
293            t.step(0),
294            "a zero-latency 32-bit transfer ends after its beat"
295        );
296    }
297
298    /// The inter-phase wait is unbounded, and it is where `M` lives. A slow
299    /// target must not shorten the transaction or complete it early.
300    #[test]
301    fn target_latency_extends_the_data_phase_and_nothing_else() {
302        for latency in [0u32, 1, 5, 20, 100] {
303            let mut t = Transaction::begin(0x1000, Width::Single32, false);
304            let mut cycles = 0;
305            while !t.step(latency) {
306                cycles += 1;
307                assert!(cycles < 1000, "transaction never completed");
308            }
309            cycles += 1;
310            assert_eq!(
311                cycles,
312                Transaction::total_cycles(Width::Single32, latency),
313                "latency {latency}"
314            );
315        }
316    }
317
318    /// Block transfers cost one bus cycle per 32-bit beat, so an I-cache line
319    /// costs meaningfully more than a D-cache line at the same latency.
320    #[test]
321    fn block_transfers_cost_one_cycle_per_beat() {
322        let m = 10;
323        assert_eq!(Transaction::total_cycles(Width::Single32, m), 1 + m + 1);
324        assert_eq!(Transaction::total_cycles(Width::Block128, m), 1 + m + 4);
325        assert_eq!(Transaction::total_cycles(Width::Block256, m), 1 + m + 8);
326        assert!(
327            Transaction::total_cycles(Width::Block256, m)
328                > Transaction::total_cycles(Width::Block128, m),
329            "an I-cache line is twice a D-cache line on the wire"
330        );
331    }
332
333    /// The transaction never completes during the address phase, whatever the
334    /// latency — a device must always get the chance to observe it mid-flight.
335    #[test]
336    fn a_transaction_can_never_complete_in_its_address_phase() {
337        for width in [
338            Width::Single32,
339            Width::Single64,
340            Width::Block128,
341            Width::Block256,
342        ] {
343            for latency in [0u32, 3, 50] {
344                let mut t = Transaction::begin(0x2000, width, false);
345                assert!(
346                    !t.step(latency),
347                    "{width:?} at latency {latency} completed atomically -- the \
348                     whole point is that it cannot"
349                );
350            }
351        }
352    }
353}