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}