rustyn64_cpu/exception.rs
1//! Exception dispatch — the epilogue and the vector table (T-12-002).
2//!
3//! Short, and every line of it is load-bearing. The hardware sequence is UM
4//! Fig. 6-14 (p. 201) for the general case and Fig. 6-15 (p. 203) for TLB
5//! refills.
6//!
7//! # The `EXL` gate is the whole point
8//!
9//! `EPC` and `Cause.BD` are written **only when `EXL` was already 0**. The
10//! flowchart's `EXL = 1?` test precedes the `EPC` write, commented *"Check for
11//! multiple exception"*, and UM §6.3.7 (p. 174) states the reason directly:
12//!
13//! > *"The EXL bit ... is set to 1 to keep the processor from overwriting the
14//! > address of the exception-causing instruction contained in the EPC
15//! > register in the event of another exception."*
16//!
17//! An implementation that always writes `EPC` passes **every** single-exception
18//! test and corrupts **every** nested one — and nesting is not exotic: UM §6.4.8
19//! (p. 188) describes a TLB refill handler taking a second TLB miss as the
20//! normal path.
21//!
22//! # The `0x080` trap
23//!
24//! UM Fig. 6-15 (p. 203) routes a refill with `EXL = 1` to offset `0x080`. That
25//! figure is **wrong**, and the manual contradicts it three times — see
26//! [`vector`] and accuracy-ledger S-3.
27
28use crate::cop0::{Cop0, reg};
29use crate::pipeline::Exception;
30use serde::{Deserialize, Serialize};
31
32/// `Cause.ExcCode` values (UM Table 6-2, p. 172).
33///
34/// The full architectural list, including the codes the N64 cannot reach, so
35/// that a value read out of `Cause` can always be named.
36pub mod exc_code {
37 /// Interrupt.
38 pub const INT: u64 = 0;
39 /// TLB modification.
40 pub const MOD: u64 = 1;
41 /// TLB miss on a load or instruction fetch.
42 pub const TLBL: u64 = 2;
43 /// TLB miss on a store.
44 pub const TLBS: u64 = 3;
45 /// Address error on a load or instruction fetch.
46 pub const ADEL: u64 = 4;
47 /// Address error on a store.
48 pub const ADES: u64 = 5;
49 /// Bus error on an instruction fetch.
50 pub const IBE: u64 = 6;
51 /// Bus error on a data reference.
52 pub const DBE: u64 = 7;
53 /// `SYSCALL`.
54 pub const SYS: u64 = 8;
55 /// `BREAK`.
56 pub const BP: u64 = 9;
57 /// Reserved instruction.
58 pub const RI: u64 = 10;
59 /// Coprocessor unusable.
60 pub const CPU: u64 = 11;
61 /// Arithmetic overflow.
62 pub const OV: u64 = 12;
63 /// Trap.
64 pub const TR: u64 = 13;
65 /// Floating-point exception.
66 pub const FPE: u64 = 15;
67 /// Watchpoint.
68 pub const WATCH: u64 = 23;
69}
70
71/// Which vector an exception uses.
72///
73/// Only three kinds exist, because the vector table has only three rows. Every
74/// exception that is not a TLB refill takes the general vector, including a TLB
75/// refill that arrives with `EXL` already set.
76#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
77pub enum VectorKind {
78 /// The common vector, offset `0x180`.
79 General,
80 /// 32-bit TLB refill, offset `0x000` — **only** with `EXL = 0`.
81 TlbRefill,
82 /// 64-bit TLB refill, offset `0x080` — **only** with `EXL = 0`.
83 XtlbRefill,
84 /// Cold reset, soft reset and NMI, which ignore `BEV` and go to
85 /// `0xBFC0_0000`.
86 Reset,
87}
88
89/// `Status.EXL`.
90const STATUS_EXL: u64 = 1 << 1;
91/// `Status.ERL`.
92const STATUS_ERL: u64 = 1 << 2;
93/// `Status.SR` (bit 20) — "Indicates a Soft Reset or NMI has occurred"
94/// (UM Fig. 6-6, the Self-Diagnostic Status Field).
95const STATUS_SR: u64 = 1 << 20;
96/// `Status.TS` (bit 21) — TLB shutdown, cleared by an NMI (UM Fig. 6-6).
97const STATUS_TS: u64 = 1 << 21;
98/// `Status.DS.BEV` (bit 22) — bootstrap exception vectors (UM Fig. 6-6).
99const STATUS_BEV: u64 = 1 << 22;
100
101/// The exception vector address (UM Tables 6-3/6-4, p. 181).
102///
103/// # The `EXL` rule, which is where implementations diverge
104///
105/// A TLB or XTLB refill uses its special vector **only when `EXL` is 0**. With
106/// `EXL` already set it takes the general vector at `0x180`. The manual says so
107/// three times:
108///
109/// - Tables 6-3/6-4 label the refill rows `TLB Miss, EXL=0` and
110/// `XTLB Miss, EXL=0`. There is no `EXL=1` refill row to select.
111/// - §6.4.8 (p. 187): *"All TLB Miss exceptions use these two special vectors
112/// when the EXL bit is set to 0 ... and they use the common exception vector
113/// when the EXL bit is set to 1."*
114/// - §6.4.8 (p. 188): *"This second exception goes to the common exception
115/// vector because the EXL bit of the Status register is set."*
116///
117/// **UM Fig. 6-15 (p. 203) disagrees and is wrong.** Its `EXL = 0? → No` arm
118/// leads to a box reading *"General Purpose Exception, Vec. Off. = 0x080"*,
119/// contradicting both tables, the prose twice, and Fig. 6-14 — which is the
120/// general-purpose handler and unconditionally uses `+ 0x180`. CEN64 routes to
121/// `0x180` with a source comment that `0x080` *"doesn't make any sense"*; it is
122/// right, and this is accuracy-ledger **S-3**.
123///
124/// Note also that UM p. 181's prose gives the `BEV = 1` general vector as
125/// `0x8000_0180`, which is a typo: per Table 6-4 the `BEV = 1` base is
126/// `0xBFC0_0200`, making the vector `0xBFC0_0380`. The 64-bit value in the same
127/// sentence is correct and proves it.
128#[must_use]
129pub const fn vector(status: u64, kind: VectorKind) -> u64 {
130 if matches!(kind, VectorKind::Reset) {
131 return 0xFFFF_FFFF_BFC0_0000;
132 }
133 let bev = status & STATUS_BEV != 0;
134 let exl = status & STATUS_EXL != 0;
135 let base: u64 = if bev {
136 0xFFFF_FFFF_BFC0_0200
137 } else {
138 0xFFFF_FFFF_8000_0000
139 };
140 // A refill with EXL already set is NOT a refill for vectoring purposes.
141 let offset: u64 = match kind {
142 VectorKind::TlbRefill if !exl => 0x000,
143 VectorKind::XtlbRefill if !exl => 0x080,
144 _ => 0x180,
145 };
146 base.wrapping_add(offset)
147}
148
149/// The `ExcCode` an exception reports.
150#[must_use]
151pub const fn exc_code_of(exc: Exception) -> u64 {
152 match exc {
153 // NMI writes no `Cause` at all -- `dispatch` intercepts it before this
154 // is consulted. It shares `INT` only to keep the match total; nothing
155 // reads it, and a test pins that `Cause` survives an NMI untouched.
156 Exception::Nmi | Exception::Interrupt => exc_code::INT,
157 // The direction matters: AdEL and AdES are different codes, and a
158 // handler distinguishes them.
159 Exception::AddressError { store: false } => exc_code::ADEL,
160 Exception::AddressError { store: true } => exc_code::ADES,
161 Exception::Overflow => exc_code::OV,
162 Exception::Syscall => exc_code::SYS,
163 Exception::Breakpoint => exc_code::BP,
164 Exception::Trap => exc_code::TR,
165 Exception::ReservedInstruction | Exception::CoprocessorReserved { .. } => exc_code::RI,
166 // Refill and Invalid share an ExcCode -- the handler tells them apart by
167 // which vector it was entered through, not by Cause.
168 Exception::TlbRefill { store: false, .. } | Exception::TlbInvalid { store: false } => {
169 exc_code::TLBL
170 }
171 Exception::TlbRefill { store: true, .. } | Exception::TlbInvalid { store: true } => {
172 exc_code::TLBS
173 }
174 Exception::TlbModified => exc_code::MOD,
175 Exception::CoprocessorUnusable { .. } => exc_code::CPU,
176 Exception::FloatingPoint => exc_code::FPE,
177 }
178}
179
180/// Which vector kind an exception takes.
181///
182/// Every exception this crate can currently raise takes the general vector; the
183/// refill kinds arrive with the TLB in T-12-004. Written as a function rather
184/// than assumed, so that adding a TLB exception forces a decision here.
185#[must_use]
186pub const fn vector_kind_of(exc: Exception) -> VectorKind {
187 match exc {
188 // NMI never reaches `vector_kind_of` through `dispatch` (it is
189 // intercepted first), but the match must stay total and Reset is the
190 // vector it would name.
191 Exception::Nmi => VectorKind::Reset,
192 Exception::Interrupt
193 | Exception::AddressError { .. }
194 | Exception::Overflow
195 | Exception::Syscall
196 | Exception::Breakpoint
197 | Exception::Trap
198 | Exception::ReservedInstruction
199 | Exception::CoprocessorReserved { .. }
200 // Invalid and Modified take the GENERAL vector: an entry was found, so
201 // there is nothing for a refill handler to refill.
202 | Exception::TlbInvalid { .. }
203 | Exception::CoprocessorUnusable { .. }
204 | Exception::FloatingPoint
205 | Exception::TlbModified => VectorKind::General,
206 // Only a genuine miss takes a refill vector, and only with EXL clear.
207 // Which of the two it is depends on the addressing width of the access
208 // that missed: 64-bit addressing has its own refill handler at 0x080,
209 // because the page-table walk it runs is a different one (it reads
210 // XContext, not Context).
211 Exception::TlbRefill { wide: true, .. } => VectorKind::XtlbRefill,
212 Exception::TlbRefill { .. } => VectorKind::TlbRefill,
213 }
214}
215
216/// Does this exception write `BadVAddr`?
217///
218/// **Only address errors and TLB exceptions.** UM §6.3.2 (p. 164) carries an
219/// explicit Caution that a Bus Error does *not* write it, because a bus error is
220/// not an address error — the address was fine, the transaction failed.
221#[must_use]
222pub const fn writes_bad_vaddr(exc: Exception) -> bool {
223 matches!(
224 exc,
225 Exception::AddressError { .. }
226 | Exception::TlbRefill { .. }
227 | Exception::TlbInvalid { .. }
228 | Exception::TlbModified
229 )
230}
231
232/// The result of dispatching an exception.
233#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
234pub struct Dispatch {
235 /// Where to fetch next.
236 pub vector: u64,
237 /// `PCycle`s the pipeline stalls.
238 ///
239 /// **2**, and documented rather than fitted: *"When a pipeline exception
240 /// condition occurs, the pipeline stalls for 2 `PCycles` and the instruction
241 /// causing the exception as well as all those that follow it in the pipeline
242 /// are aborted"* (UM §4.7, p. 114). Three files in this project once
243 /// recorded this as undocumented; see `docs/engineering-lessons.md` §3.3b.
244 pub stall_cycles: u32,
245}
246
247/// The documented stall on taking an exception (UM §4.7, p. 114).
248pub const EPILOGUE_STALL: u32 = 2;
249
250/// Perform the exception epilogue and return where to vector.
251///
252/// The order is the flowchart's (UM Fig. 6-14, p. 201), and it matters:
253///
254/// 1. `Cause.ExcCode` and `Cause.CE`.
255/// 2. `BadVAddr` — address errors and TLB exceptions only.
256/// 3. `EntryHi` / `Context` / `XContext` — TLB exceptions only (T-12-004).
257/// 4. **If `EXL` was 0**: `Cause.BD` and `EPC`. Otherwise both are left alone.
258/// 5. `EXL ← 1`.
259/// 6. `PC ← vector`.
260///
261/// `pc` is the faulting instruction's address and `in_delay_slot` says whether
262/// it sits in a branch delay slot; when it does, `EPC` gets **`pc - 4`** — the
263/// branch, not the delay-slot instruction — because that is where a handler must
264/// resume for the branch to be re-evaluated.
265/// The **NMI** exception (UM §6.4.6, p. 185).
266///
267/// Kept out of [`dispatch`] because almost none of that function applies. NMI
268/// writes `ErrorEPC`, not `EPC`; it sets `ERL`, not `EXL`; and it writes no
269/// `Cause` at all — *"the contents of all registers are preserved except for"*
270/// `ErrorEPC` and four `Status` bits. Routing it through the general path would
271/// overwrite `Cause.ExcCode` with a code NMI does not have.
272///
273/// It is **not maskable**: *"it occurs regardless of the settings of the EXL,
274/// ERL, and the IE bits in the Status register"*. The one condition it does
275/// respect is the instruction boundary — unlike Cold and Soft Reset, *"NMI is
276/// taken only at instruction boundaries"*, and the caches and memory are
277/// preserved.
278///
279/// `SR` is what distinguishes it from a reset afterwards: the manual notes
280/// there is otherwise *"no indication from the processor to differentiate
281/// between NMI & Soft Reset"*.
282pub fn nmi(cop0: &mut Cop0, pc: u64) -> Dispatch {
283 // The PC of the instruction boundary, so the handler can report where the
284 // machine was. No delay-slot adjustment: NMI is not attributed to an
285 // instruction the way a synchronous exception is.
286 cop0.set_hardware(reg::ERROR_EPC, pc);
287 let status = cop0.read(reg::STATUS);
288 cop0.set_hardware(
289 reg::STATUS,
290 (status & !STATUS_TS) | STATUS_ERL | STATUS_SR | STATUS_BEV,
291 );
292 Dispatch {
293 // Same location as Cold Reset, and unmapped/uncached so neither the
294 // cache nor the TLB needs to be valid to service it.
295 vector: vector(status, VectorKind::Reset),
296 stall_cycles: EPILOGUE_STALL,
297 }
298}
299
300/// Perform the exception epilogue and return where to vector.
301///
302/// The order is the flowchart's (UM Fig. 6-14, p. 201), and it matters:
303///
304/// 1. `Cause.ExcCode` and `Cause.CE`.
305/// 2. `BadVAddr` — address errors and TLB exceptions only.
306/// 3. `EntryHi` / `Context` / `XContext` — TLB exceptions only (T-12-004).
307/// 4. **If `EXL` was 0**: `Cause.BD` and `EPC`. Otherwise both are left alone.
308/// 5. `EXL ← 1`.
309/// 6. `PC ← vector`.
310///
311/// `pc` is the faulting instruction's address and `in_delay_slot` says whether
312/// it sits in a branch delay slot; when it does, `EPC` gets **`pc - 4`** — the
313/// branch, not the delay-slot instruction — because that is where a handler must
314/// resume for the branch to be re-evaluated.
315///
316/// [`nmi`] is intercepted at the top: none of the six steps above describe it.
317pub fn dispatch(
318 cop0: &mut Cop0,
319 exc: Exception,
320 pc: u64,
321 in_delay_slot: bool,
322 bad_vaddr: u64,
323) -> Dispatch {
324 if matches!(exc, Exception::Nmi) {
325 return nmi(cop0, pc);
326 }
327 let status = cop0.read(reg::STATUS);
328 let exl_was_set = status & STATUS_EXL != 0;
329
330 // 1. Cause.ExcCode. Written through `set_hardware` because ExcCode is
331 // read-only to software -- routing it through `write` would need the
332 // Cause write mask widened, which would also let MTC0 forge it.
333 let cause = cop0.read(reg::CAUSE);
334 let mut new_cause = (cause & !0x7C) | (exc_code_of(exc) << 2);
335 // `Cause.CE` (29:28) names the coprocessor for a Coprocessor Unusable
336 // exception. It is written on **every** exception -- the unit for a
337 // Coprocessor Unusable, zero otherwise -- not left alone for the others.
338 //
339 // This previously skipped the write for non-CpU exceptions, reasoning that
340 // `CE` is meaningless for them and that clearing it would erase a value the
341 // handler had not read yet. That reasoning is plausible and wrong: it leaves
342 // `CE` **stale**, so an exception following any Coprocessor Unusable reports
343 // that old unit number. n64-systemtest catches it exactly -- `TLB: Read
344 // after 4k page, expect TLBL` reads `Cause = 0x2000_0008` where hardware
345 // gives `0x8`, the difference being a `CE = 2` left over from the COP2
346 // usability check that ran earlier in the suite.
347 // `Cause.CE` names the coprocessor for BOTH kinds of coprocessor fault:
348 // unusable, and a reserved encoding inside a usable one. Only the first is
349 // obvious, and n64-systemtest compares the whole `Cause` register, so a
350 // missing `CE` on the second reads as a wrong exception.
351 let ce = match exc {
352 Exception::CoprocessorUnusable { unit } | Exception::CoprocessorReserved { unit } => {
353 u64::from(unit) & 0b11
354 }
355 _ => 0,
356 };
357 new_cause = (new_cause & !0x3000_0000) | (ce << 28);
358
359 // 4. EPC and Cause.BD, ONLY if EXL was clear. This is the gate; see the
360 // module docs. Note it also governs BD, not just EPC -- a stale BD with a
361 // fresh ExcCode would misreport the *first* exception's delay-slot state.
362 if exl_was_set {
363 // Deliberately nothing. The first exception's EPC is what the handler
364 // will eventually return to, and it must survive.
365 } else {
366 new_cause = if in_delay_slot {
367 new_cause | (1 << 31)
368 } else {
369 new_cause & !(1 << 31)
370 };
371 let epc = if in_delay_slot {
372 pc.wrapping_sub(4)
373 } else {
374 pc
375 };
376 cop0.set_hardware(reg::EPC, epc);
377 }
378 cop0.set_hardware(reg::CAUSE, new_cause);
379
380 // 2. BadVAddr -- and with it Context/XContext -- for the exceptions that
381 // define a faulting address.
382 //
383 // `Context.BadVPN2` and `XContext.BadVPN2` are NOT TLB-only. Hardware
384 // fills them from the same latch that feeds `BadVAddr`, so an **address
385 // error** updates them too, even though no TLB lookup took place.
386 // n64-systemtest pins this directly: on an unaligned `LW` it expects
387 // `Context = 0x0052_0000` for `BadVAddr = 0xA400_1A42`, which is exactly
388 // `(BadVAddr >> 13) << 4`. Gating these on the TLB exceptions leaves
389 // `Context` at zero and fails every unaligned-access test.
390 if writes_bad_vaddr(exc) {
391 cop0.set_hardware(reg::BAD_VADDR, bad_vaddr);
392 // Context: PTEBase (63:23) kept, BadVPN2 (22:4) = VA(31:13).
393 let ctx = cop0.read(reg::CONTEXT);
394 cop0.set_hardware(
395 reg::CONTEXT,
396 (ctx & 0xFFFF_FFFF_FF80_0000) | ((bad_vaddr >> 13) & 0x7_FFFF) << 4,
397 );
398 // XContext: PTEBase (63:33) kept, R (32:31) = VA(63:62),
399 // BadVPN2 (30:4) = VA(39:13).
400 let xctx = cop0.read(reg::XCONTEXT);
401 cop0.set_hardware(
402 reg::XCONTEXT,
403 (xctx & 0xFFFF_FFFE_0000_0000)
404 | (((bad_vaddr >> 62) & 0b11) << 31)
405 | (((bad_vaddr >> 13) & 0x7FF_FFFF) << 4),
406 );
407 }
408
409 // 3. EntryHi -- written whenever BadVAddr is, address errors INCLUDED. The UM
410 // (§6.4.7, p. 186 / Fig. 6-14) calls it "undefined" on an address error,
411 // but the hardware oracle pins a definite value: n64-systemtest's `tlb64`
412 // group triggers an AdEL from a 64-bit load and asserts
413 // `EntryHi == (VPN2 << 13) | (R << 62)` from the faulting address (ledger
414 // R-20). "Undefined" in the manual is a license, not a guarantee of
415 // non-writing, and the oracle wins (ADR 0005). This matches how Context /
416 // XContext are already filled on an address error two blocks up -- EntryHi
417 // was the one register wrongly held back. The old worry that a stale
418 // EntryHi corrupts a later `TLBWR` is unfounded: a real TLB miss reloads
419 // EntryHi before its handler writes the entry.
420 if writes_bad_vaddr(exc) {
421 let vpn2 = bad_vaddr & crate::tlb::VPN2_MASK;
422 let hi = cop0.read(reg::ENTRY_HI);
423 // The `R` field (63:62) comes from the faulting address too, not just
424 // `VPN2` -- leaving it zero puts every sign-extended kernel fault in
425 // region 0.
426 let region = bad_vaddr & 0xC000_0000_0000_0000;
427 // ASID is preserved; VPN2 and R are replaced.
428 cop0.set_hardware(reg::ENTRY_HI, (hi & crate::tlb::ASID_MASK) | vpn2 | region);
429 }
430
431 // 5. EXL. Setting it puts the CPU in Kernel mode with interrupts disabled,
432 // which is what makes the handler's own first instructions safe.
433 cop0.set_hardware(reg::STATUS, status | STATUS_EXL);
434
435 Dispatch {
436 vector: vector(status, vector_kind_of(exc)),
437 stall_cycles: EPILOGUE_STALL,
438 }
439}
440
441/// `ERET` — return from an exception (UM Ch. 16, p. 434).
442///
443/// Returns where to resume. Three rules, all easy to half-implement:
444///
445/// - If `Status.ERL` is set, resume at `ErrorEPC` and clear **`ERL`**; otherwise
446/// resume at `EPC` and clear **`EXL`**. Clearing the wrong one leaves the CPU
447/// stuck in kernel mode or returns to the wrong address.
448/// - **`LLbit` is always cleared**, which is the *only* thing besides cache
449/// invalidation that clears it (UM §3.1) — the other half of the `LL`/`SC`
450/// contract implemented in Sprint 1.
451/// - `ERET` has **no delay slot** and must not itself be placed in one.
452///
453/// The caller clears the link bit; this function reports that it must.
454#[must_use]
455pub fn eret(cop0: &mut Cop0) -> u64 {
456 let status = cop0.read(reg::STATUS);
457 if status & STATUS_ERL != 0 {
458 cop0.set_hardware(reg::STATUS, status & !STATUS_ERL);
459 cop0.read(reg::ERROR_EPC)
460 } else {
461 cop0.set_hardware(reg::STATUS, status & !STATUS_EXL);
462 cop0.read(reg::EPC)
463 }
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469
470 /// Baseline vectoring, both `BEV` values.
471 #[test]
472 fn the_vector_table_matches_the_manual() {
473 let bev0 = 0;
474 let bev1 = STATUS_BEV;
475 assert_eq!(vector(bev0, VectorKind::General), 0xFFFF_FFFF_8000_0180);
476 assert_eq!(vector(bev0, VectorKind::TlbRefill), 0xFFFF_FFFF_8000_0000);
477 assert_eq!(vector(bev0, VectorKind::XtlbRefill), 0xFFFF_FFFF_8000_0080);
478 assert_eq!(
479 vector(bev1, VectorKind::General),
480 0xFFFF_FFFF_BFC0_0380,
481 "UM p.181's prose says 0x8000_0180 here and is a typo; Table 6-4 wins"
482 );
483 assert_eq!(vector(bev1, VectorKind::TlbRefill), 0xFFFF_FFFF_BFC0_0200);
484 assert_eq!(vector(bev1, VectorKind::XtlbRefill), 0xFFFF_FFFF_BFC0_0280);
485 assert_eq!(
486 vector(bev1, VectorKind::Reset),
487 0xFFFF_FFFF_BFC0_0000,
488 "reset ignores BEV"
489 );
490 }
491
492 /// Accuracy-ledger **S-3**. A refill arriving with `EXL` already set takes
493 /// the **general** vector, not `0x080`.
494 ///
495 /// UM Fig. 6-15 (p. 203) says `0x080` and is wrong; Tables 6-3/6-4 and
496 /// §6.4.8 (twice) say otherwise, and Fig. 6-14 unconditionally uses `0x180`.
497 /// This test exists so nobody "fixes" it back to the figure.
498 #[test]
499 fn a_refill_with_exl_already_set_uses_the_general_vector() {
500 let exl = STATUS_EXL;
501 assert_eq!(
502 vector(exl, VectorKind::TlbRefill),
503 0xFFFF_FFFF_8000_0180,
504 "ledger S-3: NOT 0x8000_0000"
505 );
506 assert_eq!(
507 vector(exl, VectorKind::XtlbRefill),
508 0xFFFF_FFFF_8000_0180,
509 "ledger S-3: NOT 0x8000_0080 -- this is the value UM Fig. 6-15 gives"
510 );
511 // And with BEV too, since the two rules compose.
512 assert_eq!(
513 vector(exl | STATUS_BEV, VectorKind::XtlbRefill),
514 0xFFFF_FFFF_BFC0_0380
515 );
516 }
517
518 /// The ordinary epilogue: `EPC` gets the faulting PC, `BD` is clear, `EXL`
519 /// is set, and the `ExcCode` is right.
520 #[test]
521 fn a_first_exception_records_epc_and_sets_exl() {
522 let mut c = Cop0::new();
523 c.set_hardware(reg::STATUS, 0);
524 let d = dispatch(&mut c, Exception::Overflow, 0x8000_1000, false, 0);
525 assert_eq!(c.read(reg::EPC), 0x8000_1000);
526 assert_eq!((c.read(reg::CAUSE) >> 2) & 0x1F, exc_code::OV);
527 assert_eq!(c.read(reg::CAUSE) & (1 << 31), 0, "BD clear");
528 assert_ne!(c.read(reg::STATUS) & STATUS_EXL, 0, "EXL set");
529 assert_eq!(d.vector, 0xFFFF_FFFF_8000_0180);
530 assert_eq!(d.stall_cycles, 2, "UM §4.7 p.114");
531 }
532
533 /// In a delay slot, `EPC` points at the **branch** and `BD` is set.
534 #[test]
535 fn a_delay_slot_exception_reports_the_branch_address() {
536 let mut c = Cop0::new();
537 c.set_hardware(reg::STATUS, 0);
538 dispatch(&mut c, Exception::Overflow, 0x8000_1004, true, 0);
539 assert_eq!(
540 c.read(reg::EPC),
541 0x8000_1000,
542 "EPC = pc - 4, the branch itself"
543 );
544 assert_ne!(c.read(reg::CAUSE) & (1 << 31), 0, "BD set");
545 }
546
547 /// **The nested case, and the one that separates a correct epilogue from one
548 /// that merely passes.** With `EXL` already set, `EPC` and `BD` must not be
549 /// touched — otherwise the first exception's return address is destroyed.
550 ///
551 /// An implementation that always writes `EPC` passes every other test here.
552 #[test]
553 fn a_nested_exception_does_not_overwrite_epc_or_bd() {
554 let mut c = Cop0::new();
555 c.set_hardware(reg::STATUS, 0);
556
557 // First exception, in a delay slot: EPC = branch, BD = 1.
558 dispatch(&mut c, Exception::Overflow, 0x8000_1004, true, 0);
559 assert_eq!(c.read(reg::EPC), 0x8000_1000);
560 assert_ne!(c.read(reg::CAUSE) & (1 << 31), 0);
561
562 // Second exception while EXL is still set, NOT in a delay slot and at a
563 // completely different address. Both EPC and BD must survive.
564 dispatch(
565 &mut c,
566 Exception::AddressError { store: true },
567 0x8000_9999,
568 false,
569 0xDEAD,
570 );
571 assert_eq!(
572 c.read(reg::EPC),
573 0x8000_1000,
574 "the FIRST exception's EPC must survive (UM §6.3.7)"
575 );
576 assert_ne!(
577 c.read(reg::CAUSE) & (1 << 31),
578 0,
579 "and so must its BD -- a stale ExcCode with a fresh BD misreports it"
580 );
581 // ExcCode and BadVAddr, by contrast, DO update: they describe the
582 // current exception, not the return path.
583 assert_eq!((c.read(reg::CAUSE) >> 2) & 0x1F, exc_code::ADES);
584 assert_eq!(c.read(reg::BAD_VADDR), 0xDEAD);
585 }
586
587 /// **Refill and Invalid share an `ExcCode` and differ only in vector.** The
588 /// handler tells them apart by which entry point it was reached through, so
589 /// getting the vector wrong sends a page-protection fault to the refill
590 /// handler — which would refill a mapping that already exists.
591 ///
592 /// This is the one distinction `Cause` cannot express, so nothing but the
593 /// vector check can catch it.
594 #[test]
595 fn refill_and_invalid_share_an_exccode_but_not_a_vector() {
596 for store in [false, true] {
597 let refill = Exception::TlbRefill { store, wide: false };
598 let invalid = Exception::TlbInvalid { store };
599 assert_eq!(
600 exc_code_of(refill),
601 exc_code_of(invalid),
602 "the ExcCode cannot distinguish them"
603 );
604
605 assert_eq!(vector_kind_of(refill), VectorKind::TlbRefill);
606 assert_eq!(
607 vector_kind_of(invalid),
608 VectorKind::General,
609 "an entry WAS found, so there is nothing to refill"
610 );
611 assert_eq!(vector(0, refill_kind(store)), 0xFFFF_FFFF_8000_0000);
612 assert_eq!(
613 vector(0, vector_kind_of(invalid)),
614 0xFFFF_FFFF_8000_0180,
615 "Invalid must NOT reach the refill vector"
616 );
617 }
618 assert_eq!(
619 vector_kind_of(Exception::TlbModified),
620 VectorKind::General,
621 "Modified likewise -- the mapping exists, it is just not writable"
622 );
623 }
624
625 /// Helper for the test above: the refill kind, independent of direction.
626 const fn refill_kind(store: bool) -> VectorKind {
627 vector_kind_of(Exception::TlbRefill { store, wide: false })
628 }
629
630 /// **64-bit addressing selects the XTLB refill vector, at `0x080`.**
631 ///
632 /// The two refills carry the same `ExcCode` and differ only in entry point,
633 /// exactly as refill and invalid do -- and the handler behind `0x080` walks
634 /// a different page table (it reads `XContext`, not `Context`). Sending a
635 /// 64-bit miss to `0x000` runs the 32-bit walker over a 64-bit address.
636 #[test]
637 fn a_refill_in_64_bit_addressing_takes_the_xtlb_vector() {
638 for store in [false, true] {
639 let narrow = Exception::TlbRefill { store, wide: false };
640 let wide = Exception::TlbRefill { store, wide: true };
641 assert_eq!(
642 exc_code_of(narrow),
643 exc_code_of(wide),
644 "the ExcCode cannot distinguish them either"
645 );
646 assert_eq!(vector_kind_of(narrow), VectorKind::TlbRefill);
647 assert_eq!(vector_kind_of(wide), VectorKind::XtlbRefill);
648 assert_eq!(vector(0, vector_kind_of(narrow)), 0xFFFF_FFFF_8000_0000);
649 assert_eq!(vector(0, vector_kind_of(wide)), 0xFFFF_FFFF_8000_0080);
650 }
651 }
652
653 /// `AdEL` and `AdES` are different codes; conflating them loses information
654 /// a handler uses.
655 #[test]
656 fn address_error_reports_its_direction() {
657 assert_eq!(
658 exc_code_of(Exception::AddressError { store: false }),
659 exc_code::ADEL
660 );
661 assert_eq!(
662 exc_code_of(Exception::AddressError { store: true }),
663 exc_code::ADES
664 );
665 assert_ne!(exc_code::ADEL, exc_code::ADES);
666 }
667
668 /// `BadVAddr` is written for address errors and **not** for anything that is
669 /// not an addressing failure (UM §6.3.2 p. 164 Caution).
670 #[test]
671 fn bad_vaddr_is_written_only_for_addressing_failures() {
672 let mut c = Cop0::new();
673 c.set_hardware(reg::STATUS, 0);
674 dispatch(&mut c, Exception::Syscall, 0x8000_1000, false, 0x1234);
675 assert_eq!(
676 c.read(reg::BAD_VADDR),
677 0,
678 "SYSCALL is not an addressing failure"
679 );
680
681 let mut c = Cop0::new();
682 c.set_hardware(reg::STATUS, 0);
683 dispatch(
684 &mut c,
685 Exception::AddressError { store: false },
686 0x8000_1000,
687 false,
688 0x1234,
689 );
690 assert_eq!(c.read(reg::BAD_VADDR), 0x1234);
691 }
692
693 /// `ERET` clears `EXL` and returns to `EPC` in the ordinary case.
694 #[test]
695 fn eret_returns_to_epc_and_clears_exl() {
696 let mut c = Cop0::new();
697 c.set_hardware(reg::STATUS, STATUS_EXL);
698 c.set_hardware(reg::EPC, 0x8000_2000);
699 assert_eq!(eret(&mut c), 0x8000_2000);
700 assert_eq!(c.read(reg::STATUS) & STATUS_EXL, 0, "EXL cleared");
701 }
702
703 /// With `ERL` set, `ERET` uses `ErrorEPC` and clears **`ERL`**, leaving
704 /// `EXL` alone. Clearing the wrong bit is the classic version of this bug.
705 #[test]
706 fn eret_prefers_error_epc_when_erl_is_set() {
707 let mut c = Cop0::new();
708 c.set_hardware(reg::STATUS, STATUS_ERL | STATUS_EXL);
709 c.set_hardware(reg::EPC, 0x8000_2000);
710 c.set_hardware(reg::ERROR_EPC, 0x8000_3000);
711 assert_eq!(eret(&mut c), 0x8000_3000, "ErrorEPC wins when ERL is set");
712 let s = c.read(reg::STATUS);
713 assert_eq!(s & STATUS_ERL, 0, "ERL cleared");
714 assert_ne!(s & STATUS_EXL, 0, "EXL untouched -- clearing it is the bug");
715 }
716
717 /// Cold reset leaves `ERL` set (UM §6.4.4), so an `ERET` immediately after
718 /// reset takes the `ErrorEPC` path. Worth pinning because it is the state
719 /// the machine actually boots in.
720 #[test]
721 fn a_freshly_reset_cop0_erets_through_error_epc() {
722 let mut c = Cop0::new();
723 assert_ne!(c.read(reg::STATUS) & STATUS_ERL, 0, "reset sets ERL");
724 c.set_hardware(reg::ERROR_EPC, 0xBFC0_0000);
725 assert_eq!(eret(&mut c), 0xBFC0_0000);
726 }
727
728 /// **`Context.BadVPN2` is written on an ADDRESS ERROR, not only on a TLB
729 /// exception.** Hardware fills it from the same latch that feeds `BadVAddr`,
730 /// so no TLB lookup need have happened.
731 ///
732 /// n64-systemtest pins the exact value: an unaligned `LW` at
733 /// `0xA400_1A42` must leave `Context = 0x0052_0000`, which is
734 /// `(BadVAddr >> 13) << 4`. Gating this on the TLB exceptions leaves it at
735 /// zero and fails every unaligned-access test.
736 #[test]
737 fn an_address_error_writes_context_even_though_no_tlb_lookup_occurred() {
738 let mut c = Cop0::new();
739 c.set_hardware(reg::STATUS, 0);
740 c.set_hardware(reg::CONTEXT, 0);
741 dispatch(
742 &mut c,
743 Exception::AddressError { store: false },
744 0x8000_1000,
745 false,
746 0xFFFF_FFFF_A400_1A42,
747 );
748 assert_eq!(
749 c.read(reg::CONTEXT),
750 0x0052_0000,
751 "BadVPN2 = (BadVAddr >> 13) << 4"
752 );
753 }
754
755 /// `Context`'s `PTEBase` (63:23) is **preserved** across the update -- it is
756 /// software's page-table pointer, and clobbering it would send the refill
757 /// handler to the wrong table.
758 #[test]
759 fn an_address_error_preserves_the_context_pte_base() {
760 let mut c = Cop0::new();
761 c.set_hardware(reg::STATUS, 0);
762 c.set_hardware(reg::CONTEXT, 0xFFFF_FFFF_FF80_0000);
763 dispatch(
764 &mut c,
765 Exception::AddressError { store: false },
766 0x8000_1000,
767 false,
768 0xFFFF_FFFF_A400_1A42,
769 );
770 assert_eq!(
771 c.read(reg::CONTEXT),
772 0xFFFF_FFFF_FFD2_0000,
773 "PTEBase kept, BadVPN2 replaced"
774 );
775 }
776
777 /// **`EntryHi`'s VPN2/R IS written on an address error** (ledger R-20), the
778 /// same as `Context`/`XContext` -- the UM calls it "undefined" but the
779 /// hardware oracle (n64-systemtest `tlb64`) pins `(VPN2 << 13) | (R << 62)`
780 /// from the faulting address. ASID is preserved.
781 ///
782 /// Mutation guard: the address is chosen so both `VPN2` (bits 39:13) and the
783 /// region (bits 63:62) are non-zero, and the ASID is pre-seeded, so gating
784 /// `EntryHi` back to TLB-only, dropping the region, or clobbering the ASID
785 /// each turns the single assertion red.
786 #[test]
787 fn an_address_error_writes_entry_hi_vpn2_and_region() {
788 let mut c = Cop0::new();
789 c.set_hardware(reg::STATUS, 0);
790 // Pre-seed a non-zero ASID (low 8 bits) that must survive.
791 c.set_hardware(reg::ENTRY_HI, 0x5A);
792 let bad = 0xC000_00FF_0020_FFF4u64; // region 3, VPN2 non-zero, unaligned low bits irrelevant
793 dispatch(
794 &mut c,
795 Exception::AddressError { store: false },
796 0x8000_1000,
797 false,
798 bad,
799 );
800 let vpn = (bad >> 13) & 0x7FF_FFFF; // M27
801 let r = (bad >> 62) & 0b11;
802 let expected = (vpn << 13) | (r << 62) | 0x5A;
803 assert_eq!(
804 c.read(reg::ENTRY_HI),
805 expected,
806 "EntryHi = (VPN2 << 13) | (R << 62), ASID preserved",
807 );
808 }
809
810 /// **`Cause.CE` is written on every exception, not only on Coprocessor
811 /// Unusable.** Leaving it alone lets a stale unit number survive into an
812 /// unrelated exception.
813 ///
814 /// n64-systemtest reads `Cause = 0x2000_0008` for a `TLBL` where hardware
815 /// gives `0x8` — a `CE = 2` left over from an earlier COP2 usability check.
816 #[test]
817 fn a_later_exception_does_not_inherit_a_stale_cause_ce() {
818 let mut c = Cop0::new();
819 c.set_hardware(reg::STATUS, 0);
820 // A COP2 usability fault sets CE = 2.
821 dispatch(
822 &mut c,
823 Exception::CoprocessorUnusable { unit: 2 },
824 0x8000_1000,
825 false,
826 0,
827 );
828 assert_eq!(c.read(reg::CAUSE) & 0x3000_0000, 0x2000_0000, "CE = 2");
829
830 // Clear EXL so the next dispatch is a fresh exception, then take a TLB
831 // fault: CE must be back to zero.
832 c.set_hardware(reg::STATUS, 0);
833 dispatch(
834 &mut c,
835 Exception::TlbInvalid { store: false },
836 0x8000_2000,
837 false,
838 0x1234,
839 );
840 assert_eq!(
841 c.read(reg::CAUSE) & 0x3000_0000,
842 0,
843 "a TLBL must not inherit the COP2 unit number"
844 );
845 assert_eq!(c.read(reg::CAUSE) & 0x7C, 2 << 2, "and still reports TLBL");
846 }
847}