Skip to main content

rustysnes_cart/coproc/
upd77c25.rs

1//! The NEC µPD77C25 / µPD96050 LLE engine — the shared NEC DSP core.
2//!
3//! One engine backs **six** SNES coprocessors (`docs/cart.md` §"the shared NEC core"): the
4//! `uPD7725` revision is the DSP-1/1A/1B + DSP-2/3/4 chip; the `uPD96050` revision is the
5//! ST010 / ST011. Each chip differs only in its firmware (program ROM + data ROM) and its
6//! register widths, so this is a single firmware-parameterized core — implement it once, drive
7//! each chip's dumped program/data ROM through it.
8//!
9//! This is a clean-room re-implementation of ares' `uPD96050` component (ISC) in safe
10//! `no_std` Rust: the NEC DSP instruction word is a hardware fact, and the decode here mirrors
11//! the published instruction encoding (OP / RT / JP / LD), not ares' source layout. The host
12//! interface (the DR data register, the SR status register, and the DP data-RAM port) is the
13//! memory-mapped surface the SNES CPU sees.
14//!
15//! ## Host synchronization model
16//!
17//! The real chip free-runs on its own ~7.6 MHz oscillator and hand-shakes the SNES CPU purely
18//! through the **RQM** ("request for master") status bit: the DSP raises RQM when it wants the
19//! host to service the data register, the host's access clears it, and the DSP spins on a
20//! `JRQM`/`JNRQM` wait loop until serviced. Because RQM is the *only* observable coupling
21//! between the two clocks (DSP-1 games always poll `SR.rqm`, never a wall-clock cycle count),
22//! [`Upd77c25::run_until_rqm`] advances the engine to its next parked state after every host
23//! data-register access. This keeps the bus boundary byte-exact and fully deterministic
24//! (`docs/adr/0004`) without a free-running per-master-clock tick.
25
26// The NEC DSP treats every register as both a signed and an unsigned view of the same 16 bits,
27// so the faithful port is dense with deliberate bit-pattern casts; the status/condition-flag
28// registers are naturally bitfields of single-bit flags; and the chip-name jargon (µPD77C25,
29// uPD7725, …) is not Rust code. These lints are noise for hardware-register emulation here.
30#![allow(
31    clippy::cast_possible_truncation,
32    clippy::cast_possible_wrap,
33    clippy::cast_sign_loss,
34    clippy::struct_excessive_bools,
35    clippy::similar_names,
36    clippy::verbose_bit_mask,
37    clippy::doc_markdown,
38    clippy::missing_const_for_fn
39)]
40
41use alloc::boxed::Box;
42use alloc::vec;
43
44use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
45
46/// Which NEC DSP variant the firmware targets — selects the register widths.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum Revision {
49    /// µPD7725 — DSP-1/1A/1B, DSP-2, DSP-3, DSP-4. 2 K×24 program, 1 K×16 data ROM.
50    Upd7725,
51    /// µPD96050 — ST010, ST011. 16 K×24 program, 2 K×16 data ROM, 2 K×16 data RAM.
52    Upd96050,
53}
54
55impl Revision {
56    /// Program-counter width mask (program-ROM address space).
57    const fn pc_mask(self) -> u16 {
58        match self {
59            Self::Upd7725 => 0x07FF,  // 11-bit → 2048 words
60            Self::Upd96050 => 0x3FFF, // 14-bit → 16384 words
61        }
62    }
63
64    /// ROM-pointer width mask (data-ROM address space).
65    const fn rp_mask(self) -> u16 {
66        match self {
67            Self::Upd7725 => 0x03FF,  // 10-bit → 1024 words
68            Self::Upd96050 => 0x07FF, // 11-bit → 2048 words
69        }
70    }
71
72    /// Data-pointer width mask (data-RAM address space, internal instruction view).
73    const fn dp_mask(self) -> u16 {
74        match self {
75            Self::Upd7725 => 0x00FF,  // 8-bit → 256 words
76            Self::Upd96050 => 0x07FF, // 11-bit → 2048 words
77        }
78    }
79
80    /// Program-ROM word count.
81    const fn program_words(self) -> usize {
82        match self {
83            Self::Upd7725 => 2048,
84            Self::Upd96050 => 16384,
85        }
86    }
87
88    /// Data-ROM word count.
89    const fn data_words(self) -> usize {
90        match self {
91            Self::Upd7725 => 1024,
92            Self::Upd96050 => 2048,
93        }
94    }
95}
96
97/// The six condition/ALU flags carried per accumulator (`flags.a` / `flags.b`).
98#[derive(Debug, Clone, Copy, Default)]
99struct Flag {
100    ov0: bool, // overflow 0
101    ov1: bool, // overflow 1
102    z: bool,   // zero
103    c: bool,   // carry
104    s0: bool,  // sign 0
105    s1: bool,  // sign 1
106}
107
108impl Flag {
109    fn to_bits(self) -> u16 {
110        u16::from(self.ov0)
111            | u16::from(self.ov1) << 1
112            | u16::from(self.z) << 2
113            | u16::from(self.c) << 3
114            | u16::from(self.s0) << 4
115            | u16::from(self.s1) << 5
116    }
117}
118
119/// The 16-bit status register (SR). Only the high byte is exposed to the host on a read.
120#[derive(Debug, Clone, Copy, Default)]
121struct Status {
122    p0: bool,   // output port 0
123    p1: bool,   // output port 1
124    ei: bool,   // enable interrupts
125    sic: bool,  // serial input control
126    soc: bool,  // serial output control
127    drc: bool,  // data-register size (0 = 16-bit, 1 = 8-bit)
128    dma: bool,  // data-register DMA mode
129    drs: bool,  // data-register status (1 = active)
130    usf0: bool, // user flag 0
131    usf1: bool, // user flag 1
132    rqm: bool,  // request for master
133    siack: bool,
134    soack: bool,
135}
136
137impl Status {
138    fn to_bits(self) -> u16 {
139        let drs = self.drs && !self.drc; // when DRC=1, DRS reads 0
140        u16::from(self.p0)
141            | u16::from(self.p1) << 1
142            | u16::from(self.ei) << 7
143            | u16::from(self.sic) << 8
144            | u16::from(self.soc) << 9
145            | u16::from(self.drc) << 10
146            | u16::from(self.dma) << 11
147            | u16::from(drs) << 12
148            | u16::from(self.usf0) << 13
149            | u16::from(self.usf1) << 14
150            | u16::from(self.rqm) << 15
151    }
152
153    fn set_bits(&mut self, data: u16) {
154        self.p0 = data & 1 != 0;
155        self.p1 = data & 1 << 1 != 0;
156        self.ei = data & 1 << 7 != 0;
157        self.sic = data & 1 << 8 != 0;
158        self.soc = data & 1 << 9 != 0;
159        self.drc = data & 1 << 10 != 0;
160        self.dma = data & 1 << 11 != 0;
161        self.drs = data & 1 << 12 != 0;
162        self.usf0 = data & 1 << 13 != 0;
163        self.usf1 = data & 1 << 14 != 0;
164        self.rqm = data & 1 << 15 != 0;
165    }
166}
167
168/// The NEC µPD77C25 / µPD96050 LLE engine.
169///
170/// Construct with [`Upd77c25::new`], load a dumped firmware image with
171/// [`Upd77c25::load_firmware`], then drive the host interface through the DR/SR/DP methods. The
172/// engine is inert (open-bus reads) until a firmware image is loaded — the honesty posture of
173/// `docs/adr/0003`: a chip-ROM-dump-dependent coprocessor is non-functional without the dump,
174/// never silently degraded.
175#[derive(Debug, Clone)]
176pub struct Upd77c25 {
177    revision: Revision,
178    firmware_loaded: bool,
179    /// Count of host DR/SR port accesses (diagnostics / the debugger).
180    host_accesses: u64,
181
182    program_rom: Box<[u32]>,
183    data_rom: Box<[u16]>,
184    data_ram: Box<[u16]>,
185
186    stack: [u16; 16],
187    pc: u16,
188    rp: u16,
189    dp: u16,
190    sp: u8,
191    si: u16,
192    so: u16,
193    k: i16,
194    l: i16,
195    m: i16,
196    n: i16,
197    a: i16,
198    b: i16,
199    tr: u16,
200    trb: u16,
201    dr: u16,
202    sr: Status,
203    flag_a: Flag,
204    flag_b: Flag,
205}
206
207impl Upd77c25 {
208    /// A hard cap on instructions executed per host-driven catch-up, so a wedged firmware can
209    /// never hang the emulator. A single DSP-1 command settles in well under this.
210    const RUN_CAP: u32 = 0x10_0000;
211
212    /// Construct a powered-on engine for `revision` with zeroed (unloaded) firmware.
213    #[must_use]
214    pub fn new(revision: Revision) -> Self {
215        let program_rom = vec![0u32; revision.program_words()].into_boxed_slice();
216        let data_rom = vec![0u16; revision.data_words()].into_boxed_slice();
217        // ares sizes the data RAM at 2048 words for both revisions; the instruction view masks
218        // it to the revision's DP width. Keep the full array and mask host accesses to it.
219        let data_ram = vec![0u16; 2048].into_boxed_slice();
220        let mut me = Self {
221            revision,
222            firmware_loaded: false,
223            host_accesses: 0,
224            program_rom,
225            data_rom,
226            data_ram,
227            stack: [0; 16],
228            pc: 0,
229            rp: 0,
230            dp: 0,
231            sp: 0,
232            si: 0,
233            so: 0,
234            k: 0,
235            l: 0,
236            m: 0,
237            n: 0,
238            a: 0,
239            b: 0,
240            tr: 0,
241            trb: 0,
242            dr: 0,
243            sr: Status::default(),
244            flag_a: Flag::default(),
245            flag_b: Flag::default(),
246        };
247        me.power();
248        me
249    }
250
251    /// Reset all programmer-visible state (NEC power-on). Firmware contents are retained.
252    pub fn power(&mut self) {
253        self.stack = [0; 16];
254        self.pc = 0;
255        self.rp = 0;
256        self.dp = 0;
257        self.sp = 0;
258        self.si = 0;
259        self.so = 0;
260        self.k = 0;
261        self.l = 0;
262        self.m = 0;
263        self.n = 0;
264        self.a = 0;
265        self.b = 0;
266        self.tr = 0;
267        self.trb = 0;
268        self.dr = 0;
269        self.sr = Status::default();
270        self.flag_a = Flag::default();
271        self.flag_b = Flag::default();
272    }
273
274    /// Whether a firmware image has been loaded (the chip is functional).
275    #[must_use]
276    pub const fn firmware_loaded(&self) -> bool {
277        self.firmware_loaded
278    }
279
280    /// Load a packed NEC DSP firmware dump.
281    ///
282    /// The dump is the program ROM (one little-endian 24-bit word per instruction) immediately
283    /// followed by the data ROM (one little-endian 16-bit word per entry) — exactly ares'
284    /// `upd7725.program.rom` + `upd7725.data.rom` concatenation. For the µPD7725 (DSP-1) that is
285    /// `2048×3 + 1024×2 = 8192` bytes (`dsp1.rom` / `dsp1b.rom`).
286    ///
287    /// Returns `false` (and loads nothing) if `bytes` is too short for this revision. On success
288    /// the engine is powered on and run to its first parked (RQM-set) state, ready for the first
289    /// host command.
290    pub fn load_firmware(&mut self, bytes: &[u8]) -> bool {
291        let prog_bytes = self.program_rom.len() * 3;
292        let data_bytes = self.data_rom.len() * 2;
293        if bytes.len() < prog_bytes + data_bytes {
294            return false;
295        }
296        for (i, word) in self.program_rom.iter_mut().enumerate() {
297            let o = i * 3;
298            *word =
299                u32::from(bytes[o]) | u32::from(bytes[o + 1]) << 8 | u32::from(bytes[o + 2]) << 16;
300        }
301        for (i, word) in self.data_rom.iter_mut().enumerate() {
302            let o = prog_bytes + i * 2;
303            *word = u16::from(bytes[o]) | u16::from(bytes[o + 1]) << 8;
304        }
305        self.firmware_loaded = true;
306        self.power();
307        // Prime the engine to its first "ready for a command" parked state.
308        self.run_until_rqm();
309        true
310    }
311
312    // --- Host interface (the memory-mapped DR / SR / DP surface). ---
313
314    /// Read the status register (the high byte the host sees at the SR port).
315    #[must_use]
316    pub fn read_sr(&self) -> u8 {
317        if !self.firmware_loaded {
318            return 0;
319        }
320        (self.sr.to_bits() >> 8) as u8
321    }
322
323    /// Write the status register port. The NEC chip ignores host SR writes (no-op), matching
324    /// hardware; kept for surface symmetry.
325    pub fn write_sr(&mut self, _data: u8) {}
326
327    /// Read a byte from the data register, then catch the engine up to its next parked state.
328    #[must_use]
329    pub fn read_dr(&mut self) -> u8 {
330        if !self.firmware_loaded {
331            return 0;
332        }
333        self.host_accesses += 1;
334        let value = if self.sr.drc {
335            // 8-bit transfer.
336            self.sr.rqm = false;
337            self.dr as u8
338        } else if self.sr.drs {
339            // 16-bit transfer, high byte (completes the word).
340            self.sr.rqm = false;
341            self.sr.drs = false;
342            (self.dr >> 8) as u8
343        } else {
344            // 16-bit transfer, low byte (begins the word).
345            self.sr.drs = true;
346            self.dr as u8
347        };
348        self.run_until_rqm();
349        value
350    }
351
352    /// Write a byte to the data register, then catch the engine up to its next parked state.
353    pub fn write_dr(&mut self, data: u8) {
354        if !self.firmware_loaded {
355            return;
356        }
357        self.host_accesses += 1;
358        if self.sr.drc {
359            // 8-bit transfer.
360            self.sr.rqm = false;
361            self.dr = (self.dr & 0xFF00) | u16::from(data);
362        } else if self.sr.drs {
363            // 16-bit transfer, high byte (completes the word).
364            self.sr.rqm = false;
365            self.sr.drs = false;
366            self.dr = u16::from(data) << 8 | (self.dr & 0x00FF);
367        } else {
368            // 16-bit transfer, low byte (begins the word).
369            self.sr.drs = true;
370            self.dr = (self.dr & 0xFF00) | u16::from(data);
371        }
372        self.run_until_rqm();
373    }
374
375    /// Read a byte from the data-RAM host port (`address` is the byte address into the window).
376    #[must_use]
377    pub fn read_dp(&self, address: u16) -> u8 {
378        if !self.firmware_loaded {
379            return 0;
380        }
381        let hi = address & 1 != 0;
382        let word = self.data_ram[usize::from((address >> 1) & 2047)];
383        if hi { (word >> 8) as u8 } else { word as u8 }
384    }
385
386    /// Write a byte to the data-RAM host port (`address` is the byte address into the window).
387    pub fn write_dp(&mut self, address: u16, data: u8) {
388        if !self.firmware_loaded {
389            return;
390        }
391        let hi = address & 1 != 0;
392        let slot = &mut self.data_ram[usize::from((address >> 1) & 2047)];
393        if hi {
394            *slot = (*slot & 0x00FF) | u16::from(data) << 8;
395        } else {
396            *slot = (*slot & 0xFF00) | u16::from(data);
397        }
398    }
399
400    /// Advance the engine until it parks waiting for the host (RQM set), capped at `RUN_CAP`
401    /// instructions. A no-op when already parked or when no firmware is loaded.
402    pub fn run_until_rqm(&mut self) {
403        if !self.firmware_loaded {
404            return;
405        }
406        let mut budget = Self::RUN_CAP;
407        while !self.sr.rqm && budget > 0 {
408            self.exec();
409            budget -= 1;
410        }
411    }
412
413    // --- The instruction core. ---
414
415    fn set_pc(&mut self, value: u16) {
416        self.pc = value & self.revision.pc_mask();
417    }
418
419    /// Execute one instruction, then update the M/N multiplier outputs (NEC pipelines a
420    /// signed `K×L` multiply each cycle).
421    pub fn exec(&mut self) {
422        let opcode = self.program_rom[usize::from(self.pc)];
423        self.set_pc(self.pc.wrapping_add(1));
424        match opcode >> 22 {
425            0 => self.exec_op(opcode),
426            1 => self.exec_rt(opcode),
427            2 => self.exec_jp(opcode),
428            _ => self.exec_ld(opcode),
429        }
430
431        let result = i32::from(self.k) * i32::from(self.l); // sign + 30-bit product
432        self.m = (result >> 15) as i16; // sign + top 15 bits
433        self.n = (result << 1) as i16; // low 15 bits + zero
434    }
435
436    #[allow(clippy::too_many_lines)]
437    fn exec_op(&mut self, opcode: u32) {
438        let pselect = (opcode >> 20) & 0x3;
439        let alu = (opcode >> 16) & 0xF;
440        let asl = (opcode >> 15) & 0x1;
441        let dpl = (opcode >> 13) & 0x3;
442        let dphm = ((opcode >> 9) & 0xF) as u16;
443        let rpdcr = (opcode >> 8) & 0x1;
444        let src = (opcode >> 4) & 0xF;
445        let dst = opcode & 0xF;
446
447        let idb: u16 = match src {
448            0 => self.trb,
449            1 => self.a as u16,
450            2 => self.b as u16,
451            3 => self.tr,
452            4 => self.dp,
453            5 => self.rp,
454            6 => self.data_rom[usize::from(self.rp)],
455            7 => 0x8000 - u16::from(self.flag_a.s1), // ASL ignored; always SA1
456            8 => {
457                self.sr.rqm = true;
458                self.dr
459            }
460            9 => self.dr,
461            10 => self.sr.to_bits(),
462            11 | 12 => self.si,
463            13 => self.k as u16,
464            14 => self.l as u16,
465            _ => self.data_ram[usize::from(self.dp & self.revision.dp_mask())],
466        };
467
468        if alu != 0 {
469            let mut p: u16 = match pselect {
470                0 => self.data_ram[usize::from(self.dp & self.revision.dp_mask())],
471                1 => idb,
472                2 => self.m as u16,
473                _ => self.n as u16,
474            };
475
476            let (q, mut flag, c) = if asl == 0 {
477                (self.a as u16, self.flag_a, self.flag_b.c)
478            } else {
479                (self.b as u16, self.flag_b, self.flag_a.c)
480            };
481            let cu = u16::from(c);
482
483            let r: u16 = match alu {
484                1 => q | p,
485                2 => q & p,
486                3 => q ^ p,
487                4 => q.wrapping_sub(p),
488                5 => q.wrapping_add(p),
489                6 => q.wrapping_sub(p).wrapping_sub(cu),
490                7 => q.wrapping_add(p).wrapping_add(cu),
491                8 => {
492                    p = 1;
493                    q.wrapping_sub(1)
494                }
495                9 => {
496                    p = 1;
497                    q.wrapping_add(1)
498                }
499                10 => !q,
500                11 => (q >> 1) | (q & 0x8000), // SHR1 (ASR)
501                12 => (q << 1) | cu,           // SHL1 (ROL)
502                13 => (q << 2) | 3,            // SHL2
503                14 => (q << 4) | 15,           // SHL4
504                _ => q.rotate_left(8),         // XCHG (byte swap)
505            };
506
507            flag.z = r == 0;
508            flag.s0 = r & 0x8000 != 0;
509            if !flag.ov1 {
510                flag.s1 = flag.s0;
511            }
512
513            match alu {
514                1 | 2 | 3 | 10 | 13 | 14 | 15 => {
515                    flag.ov0 = false;
516                    flag.ov1 = false;
517                    flag.c = false;
518                }
519                11 => {
520                    flag.ov0 = false;
521                    flag.ov1 = false;
522                    flag.c = q & 1 != 0;
523                }
524                12 => {
525                    flag.ov0 = false;
526                    flag.ov1 = false;
527                    flag.c = q >> 15 != 0;
528                }
529                // SUB/ADD/SBB/ADC/DEC/INC.
530                _ => {
531                    let carries = q ^ p ^ r;
532                    let second = if alu & 1 != 0 { r } else { q };
533                    let overflow = (q ^ r) & (p ^ second);
534                    let ov0 = overflow & 0x8000 != 0;
535                    flag.ov1 = if ov0 && flag.ov1 {
536                        flag.s0 == flag.s1
537                    } else {
538                        ov0 || flag.ov1
539                    };
540                    flag.ov0 = ov0;
541                    flag.c = (carries ^ overflow) & 0x8000 != 0;
542                }
543            }
544
545            if asl == 0 {
546                self.a = r as i16;
547                self.flag_a = flag;
548            } else {
549                self.b = r as i16;
550                self.flag_b = flag;
551            }
552        }
553
554        // The move field is an embedded LD: id = idb, dst = dst.
555        self.exec_ld(u32::from(idb) << 6 | dst);
556
557        if dst != 4 {
558            // if LD did not write DP
559            match dpl {
560                1 => self.dp = (self.dp & 0xF0) + (self.dp.wrapping_add(1) & 0x0F), // DPINC
561                2 => self.dp = (self.dp & 0xF0) + (self.dp.wrapping_sub(1) & 0x0F), // DPDEC
562                3 => self.dp &= 0xF0,                                               // DPCLR
563                _ => {}
564            }
565            self.dp ^= dphm << 4;
566            self.dp &= self.revision.dp_mask();
567        }
568
569        if dst != 5 {
570            // if LD did not write RP
571            if rpdcr != 0 {
572                self.rp = self.rp.wrapping_sub(1) & self.revision.rp_mask();
573            }
574        }
575    }
576
577    fn exec_rt(&mut self, opcode: u32) {
578        self.exec_op(opcode);
579        self.sp = self.sp.wrapping_sub(1) & 0xF;
580        self.pc = self.stack[usize::from(self.sp)] & self.revision.pc_mask();
581    }
582
583    fn exec_jp(&mut self, opcode: u32) {
584        let brch = (opcode >> 13) & 0x1FF;
585        let na = ((opcode >> 2) & 0x7FF) as u16;
586        let bank = (opcode & 0x3) as u16;
587        let jp = (self.pc & 0x2000) | bank << 11 | na;
588
589        let taken = match brch {
590            0x000 => {
591                self.set_pc(self.so);
592                return;
593            }
594            0x080 => !self.flag_a.c,
595            0x082 => self.flag_a.c,
596            0x084 => !self.flag_b.c,
597            0x086 => self.flag_b.c,
598            0x088 => !self.flag_a.z,
599            0x08a => self.flag_a.z,
600            0x08c => !self.flag_b.z,
601            0x08e => self.flag_b.z,
602            0x090 => !self.flag_a.ov0,
603            0x092 => self.flag_a.ov0,
604            0x094 => !self.flag_b.ov0,
605            0x096 => self.flag_b.ov0,
606            0x098 => !self.flag_a.ov1,
607            0x09a => self.flag_a.ov1,
608            0x09c => !self.flag_b.ov1,
609            0x09e => self.flag_b.ov1,
610            0x0a0 => !self.flag_a.s0,
611            0x0a2 => self.flag_a.s0,
612            0x0a4 => !self.flag_b.s0,
613            0x0a6 => self.flag_b.s0,
614            0x0a8 => !self.flag_a.s1,
615            0x0aa => self.flag_a.s1,
616            0x0ac => !self.flag_b.s1,
617            0x0ae => self.flag_b.s1,
618            0x0b0 => self.dp & 0x0F == 0x00,
619            0x0b1 => self.dp & 0x0F != 0x00,
620            0x0b2 => self.dp & 0x0F == 0x0F,
621            0x0b3 => self.dp & 0x0F != 0x0F,
622            0x0b4 => !self.sr.siack,
623            0x0b6 => self.sr.siack,
624            0x0b8 => !self.sr.soack,
625            0x0ba => self.sr.soack,
626            0x0bc => !self.sr.rqm,
627            0x0be => self.sr.rqm,
628            0x100 => {
629                self.set_pc(jp & !0x2000); // LJMP
630                return;
631            }
632            0x101 => {
633                self.set_pc(jp | 0x2000); // HJMP
634                return;
635            }
636            0x140 => {
637                self.stack[usize::from(self.sp)] = self.pc;
638                self.sp = self.sp.wrapping_add(1) & 0xF;
639                self.set_pc(jp & !0x2000); // LCALL
640                return;
641            }
642            0x141 => {
643                self.stack[usize::from(self.sp)] = self.pc;
644                self.sp = self.sp.wrapping_add(1) & 0xF;
645                self.set_pc(jp | 0x2000); // HCALL
646                return;
647            }
648            _ => return,
649        };
650        if taken {
651            self.set_pc(jp);
652        }
653    }
654
655    fn exec_ld(&mut self, opcode: u32) {
656        let id = (opcode >> 6) as u16;
657        let dst = opcode & 0xF;
658        match dst {
659            0 => {}
660            1 => self.a = id as i16,
661            2 => self.b = id as i16,
662            3 => self.tr = id,
663            4 => self.dp = id & self.revision.dp_mask(),
664            5 => self.rp = id & self.revision.rp_mask(),
665            6 => {
666                self.dr = id;
667                self.sr.rqm = true;
668            }
669            7 => {
670                let v = (self.sr.to_bits() & 0x907C) | (id & !0x907C);
671                self.sr.set_bits(v);
672            }
673            8 | 9 => self.so = id,
674            10 => self.k = id as i16,
675            11 => {
676                self.k = id as i16;
677                self.l = self.data_rom[usize::from(self.rp)] as i16;
678            }
679            12 => {
680                self.l = id as i16;
681                self.k =
682                    self.data_ram[usize::from((self.dp | 0x40) & self.revision.dp_mask())] as i16;
683            }
684            13 => self.l = id as i16,
685            14 => self.trb = id,
686            _ => self.data_ram[usize::from(self.dp & self.revision.dp_mask())] = id,
687        }
688    }
689
690    // --- Test / debug accessors. ---
691
692    /// Count of host DR port accesses since power-on (diagnostics).
693    #[must_use]
694    pub const fn host_accesses(&self) -> u64 {
695        self.host_accesses
696    }
697
698    /// The current program counter (for unit tests / the debugger).
699    #[must_use]
700    pub const fn pc(&self) -> u16 {
701        self.pc
702    }
703
704    /// The RQM (request-for-master) status bit (for unit tests / the debugger).
705    #[must_use]
706    pub const fn rqm(&self) -> bool {
707        self.sr.rqm
708    }
709
710    /// Read a data-RAM word directly (for unit tests / save-state inspection).
711    #[must_use]
712    pub fn data_ram_word(&self, index: usize) -> u16 {
713        self.data_ram[index & 2047]
714    }
715
716    /// The condition flags packed (`flags.a` in the low 6 bits, `flags.b` in the next 6) — for
717    /// unit-test vectors derived from a reference trace.
718    #[must_use]
719    pub fn flags_packed(&self) -> u16 {
720        self.flag_a.to_bits() | self.flag_b.to_bits() << 6
721    }
722
723    /// Write this engine's mutable state — every register + the 2048-word data RAM — into a
724    /// `"NDSP"` section. Firmware (`program_rom`/`data_rom`) is deliberately NOT written: it is
725    /// never embedded in a save-state (the same "never carry a ROM/firmware byte" posture
726    /// `docs/adr/0003` already applies elsewhere), reloaded fresh via [`Self::load_firmware`]
727    /// before a matching [`Self::load_state`] call. `host_accesses` (a debugger counter, not
728    /// emulated-hardware state) is also omitted — restoring it to a stale value would misrepresent
729    /// activity that happened after the save, not before it.
730    pub fn save_state(&self, w: &mut SaveWriter) {
731        w.section(*b"NDSP", |s| {
732            for &word in &self.stack {
733                s.write_u16(word);
734            }
735            s.write_u16(self.pc);
736            s.write_u16(self.rp);
737            s.write_u16(self.dp);
738            s.write_u8(self.sp);
739            s.write_u16(self.si);
740            s.write_u16(self.so);
741            s.write_u16(self.k.cast_unsigned());
742            s.write_u16(self.l.cast_unsigned());
743            s.write_u16(self.m.cast_unsigned());
744            s.write_u16(self.n.cast_unsigned());
745            s.write_u16(self.a.cast_unsigned());
746            s.write_u16(self.b.cast_unsigned());
747            s.write_u16(self.tr);
748            s.write_u16(self.trb);
749            s.write_u16(self.dr);
750            write_status(s, self.sr);
751            write_flag(s, self.flag_a);
752            write_flag(s, self.flag_b);
753            for &word in &self.data_ram {
754                s.write_u16(word);
755            }
756        });
757    }
758
759    /// The inverse of [`Self::save_state`].
760    ///
761    /// # Errors
762    /// [`SaveStateError`] on truncated/corrupt input (the section framing itself already rejects
763    /// a wrong tag or a truncated read).
764    pub fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
765        let mut s = r.expect_section(*b"NDSP")?;
766        for slot in &mut self.stack {
767            *slot = s.read_u16()?;
768        }
769        // pc/rp/dp/sp are used as UNCHECKED array indices elsewhere in this engine
770        // (`program_rom[pc]`, `data_rom[rp]`, `stack[sp]`; `dp` is re-masked at every use site
771        // already, but masking it here too keeps every pointer register consistently normalized
772        // on load) because normal execution NEVER produces an out-of-range value — every mutator
773        // masks on write (`set_pc`, the DPINC/DPDEC/DPCLR ops, the call/return stack-pointer
774        // wrap). Untrusted save-state bytes carry no such guarantee, so apply the identical masks
775        // here: this is the engine's own normal-operation invariant, not new validation logic,
776        // and (being width masks on genuinely N-bit hardware registers, not a "must be one of a
777        // few enum-like values" semantic constraint) masking is the hardware-accurate transform,
778        // not a silent-degradation shortcut.
779        self.pc = s.read_u16()? & self.revision.pc_mask();
780        self.rp = s.read_u16()? & self.revision.rp_mask();
781        self.dp = s.read_u16()? & self.revision.dp_mask();
782        self.sp = s.read_u8()? & 0xF;
783        self.si = s.read_u16()?;
784        self.so = s.read_u16()?;
785        self.k = s.read_u16()?.cast_signed();
786        self.l = s.read_u16()?.cast_signed();
787        self.m = s.read_u16()?.cast_signed();
788        self.n = s.read_u16()?.cast_signed();
789        self.a = s.read_u16()?.cast_signed();
790        self.b = s.read_u16()?.cast_signed();
791        self.tr = s.read_u16()?;
792        self.trb = s.read_u16()?;
793        self.dr = s.read_u16()?;
794        self.sr = read_status(&mut s)?;
795        self.flag_a = read_flag(&mut s)?;
796        self.flag_b = read_flag(&mut s)?;
797        for slot in &mut self.data_ram {
798            *slot = s.read_u16()?;
799        }
800        if s.remaining() != 0 {
801            return Err(SaveStateError::Invalid(alloc::format!(
802                "NDSP section has {} trailing byte(s)",
803                s.remaining()
804            )));
805        }
806        Ok(())
807    }
808}
809
810fn write_flag(s: &mut SaveWriter, f: Flag) {
811    s.write_bool(f.ov0);
812    s.write_bool(f.ov1);
813    s.write_bool(f.z);
814    s.write_bool(f.c);
815    s.write_bool(f.s0);
816    s.write_bool(f.s1);
817}
818
819fn read_flag(s: &mut SaveReader) -> Result<Flag, SaveStateError> {
820    Ok(Flag {
821        ov0: s.read_bool()?,
822        ov1: s.read_bool()?,
823        z: s.read_bool()?,
824        c: s.read_bool()?,
825        s0: s.read_bool()?,
826        s1: s.read_bool()?,
827    })
828}
829
830fn write_status(s: &mut SaveWriter, st: Status) {
831    s.write_bool(st.p0);
832    s.write_bool(st.p1);
833    s.write_bool(st.ei);
834    s.write_bool(st.sic);
835    s.write_bool(st.soc);
836    s.write_bool(st.drc);
837    s.write_bool(st.dma);
838    s.write_bool(st.drs);
839    s.write_bool(st.usf0);
840    s.write_bool(st.usf1);
841    s.write_bool(st.rqm);
842    s.write_bool(st.siack);
843    s.write_bool(st.soack);
844}
845
846fn read_status(s: &mut SaveReader) -> Result<Status, SaveStateError> {
847    Ok(Status {
848        p0: s.read_bool()?,
849        p1: s.read_bool()?,
850        ei: s.read_bool()?,
851        sic: s.read_bool()?,
852        soc: s.read_bool()?,
853        drc: s.read_bool()?,
854        dma: s.read_bool()?,
855        drs: s.read_bool()?,
856        usf0: s.read_bool()?,
857        usf1: s.read_bool()?,
858        rqm: s.read_bool()?,
859        siack: s.read_bool()?,
860        soack: s.read_bool()?,
861    })
862}
863
864#[cfg(test)]
865mod tests {
866    use super::*;
867    use alloc::vec;
868    use alloc::vec::Vec;
869
870    /// Pack a µPD7725 firmware image from a list of 24-bit program words (data ROM left zero).
871    fn synth_firmware(program: &[u32]) -> Vec<u8> {
872        // 2048 program words × 3 bytes (LE) + 1024 data words × 2 bytes (LE) = 8192 bytes.
873        let mut prog = vec![0u32; 2048];
874        prog[..program.len()].copy_from_slice(program);
875        let mut bytes = Vec::with_capacity(8192);
876        for w in prog {
877            bytes.push((w & 0xFF) as u8);
878            bytes.push((w >> 8 & 0xFF) as u8);
879            bytes.push((w >> 16 & 0xFF) as u8);
880        }
881        bytes.resize(8192, 0); // append the zeroed 1024-word data ROM
882        bytes
883    }
884
885    // --- Opcode constructors (the published NEC DSP encoding). ---
886
887    /// `LD` immediate: `id → dst`. Top two bits select the standalone-LD form.
888    const fn ld(id: u16, dst: u32) -> u32 {
889        0b11 << 22 | (id as u32) << 6 | dst
890    }
891    /// `OP` with an ALU op + a move-field destination.
892    const fn op(pselect: u32, alu: u32, src: u32, dst: u32, dpl: u32) -> u32 {
893        pselect << 20 | alu << 16 | dpl << 13 | src << 4 | dst
894    }
895    /// `LJMP` to `target`.
896    const fn ljmp(target: u32) -> u32 {
897        0b10 << 22 | 0x100 << 13 | (target & 0x7FF) << 2
898    }
899
900    #[test]
901    fn alu_add_sub_multiply_and_dataram_moves() {
902        // A hand-assembled program: 5+3 → RAM[0]; 2−3 (borrow) → RAM[1]; (7×6)<<1 via the
903        // multiplier → RAM[2]; then load DR=0xBEEF. Exercises LD/OP decode, ADD/SUB flags, the
904        // K×L pipeline, the move field, and DPINC pointer stepping.
905        let program = [
906            ld(5, 1),           // 0: A = 5
907            ld(3, 3),           // 1: TR = 3
908            op(1, 5, 3, 0, 0),  // 2: A = A + TR = 8        (ADD, p=idb=TR)
909            op(0, 0, 1, 15, 1), // 3: RAM[dp=0] = A; DPINC  (move A→RAM, dp→1)
910            ld(2, 1),           // 4: A = 2
911            op(1, 4, 3, 0, 0),  // 5: A = A − TR = 0xFFFF    (SUB, borrow)
912            op(0, 0, 1, 15, 1), // 6: RAM[dp=1] = A; DPINC  (dp→2)
913            ld(7, 10),          // 7: K = 7
914            ld(6, 13),          // 8: L = 6  ⇒ N = (7×6)<<1 = 84
915            ld(0, 1),           // 9: A = 0
916            op(3, 5, 0, 0, 0),  // 10: A = A + N = 84        (ADD, p=N)
917            op(0, 0, 1, 15, 1), // 11: RAM[dp=2] = A; DPINC (dp→3)
918            ld(0xBEEF, 6),      // 12: DR = 0xBEEF, RQM=1
919        ];
920        let mut eng = Upd77c25::new(Revision::Upd7725);
921        // Load the program directly (bypass the run-to-rqm prime so we can single-step).
922        let fw = synth_firmware(&program);
923        let prog_bytes = eng.program_rom.len() * 3;
924        for (i, word) in eng.program_rom.iter_mut().enumerate() {
925            let o = i * 3;
926            *word = u32::from(fw[o]) | u32::from(fw[o + 1]) << 8 | u32::from(fw[o + 2]) << 16;
927        }
928        eng.firmware_loaded = true;
929        let _ = prog_bytes;
930
931        for _ in 0..13 {
932            eng.exec();
933        }
934
935        assert_eq!(eng.data_ram_word(0), 8, "5+3");
936        assert_eq!(eng.data_ram_word(1), 0xFFFF, "2-3 borrow");
937        assert_eq!(eng.data_ram_word(2), 84, "(7*6)<<1 via the K*L multiplier");
938
939        // DR readout: 16-bit transfer, low byte then high byte.
940        assert!(eng.rqm(), "DR load raised RQM");
941        assert_eq!(eng.dr & 0xFF, 0xEF);
942        assert_eq!(eng.dr >> 8, 0xBE);
943    }
944
945    #[test]
946    fn dr_handshake_and_run_until_rqm() {
947        // Program: load DR=0x1234 (raises RQM), then loop. run_until_rqm must park at the LD; a
948        // full 16-bit DR read must hand back 0x34 then 0x12 and re-arm via the loop.
949        let program = [ld(0x1234, 6), ljmp(0)];
950        let mut eng = Upd77c25::new(Revision::Upd7725);
951        assert!(eng.load_firmware(&synth_firmware(&program)));
952        assert!(eng.firmware_loaded());
953        // Priming ran until the LD parked with RQM set.
954        assert!(eng.rqm());
955        assert_eq!(eng.read_sr() & 0x80, 0x80, "SR high bit = RQM");
956
957        assert_eq!(eng.read_dr(), 0x34); // low byte (drs set, rqm held)
958        assert_eq!(eng.read_dr(), 0x12); // high byte (rqm cleared, then re-armed by the loop)
959        assert!(eng.rqm(), "loop re-loaded DR and re-raised RQM");
960    }
961
962    #[test]
963    fn inert_until_firmware() {
964        let mut eng = Upd77c25::new(Revision::Upd7725);
965        assert!(!eng.firmware_loaded());
966        assert_eq!(eng.read_sr(), 0);
967        assert_eq!(eng.read_dr(), 0);
968        eng.run_until_rqm(); // no-op, must not hang
969        assert!(!eng.load_firmware(&[0u8; 100])); // too short
970    }
971
972    #[test]
973    fn out_of_range_pointer_registers_are_masked_not_panicked_on() {
974        // A hand-edited/corrupted save-state can claim any 16-bit pc/rp/sp — real hardware would
975        // never produce a value wider than the register, so load_state must mask (not trust) them
976        // before they're used as unchecked array indices (program_rom[pc]/data_rom[rp]/stack[sp]).
977        let mut w = SaveWriter::new();
978        w.section(*b"NDSP", |s| {
979            for _ in 0..16 {
980                s.write_u16(0);
981            } // stack
982            s.write_u16(0xFFFF); // pc
983            s.write_u16(0xFFFF); // rp
984            s.write_u16(0xFFFF); // dp
985            s.write_u8(0xFF); // sp
986            for _ in 0..10 {
987                s.write_u16(0);
988            } // si/so/k/l/m/n/a/b/tr/trb
989            s.write_u16(0); // dr
990            for _ in 0..13 {
991                s.write_bool(false);
992            } // sr
993            for _ in 0..12 {
994                s.write_bool(false);
995            } // flag_a + flag_b
996            for _ in 0..2048 {
997                s.write_u16(0);
998            } // data_ram
999        });
1000        let bytes = w.into_bytes();
1001
1002        let mut eng = Upd77c25::new(Revision::Upd7725);
1003        let mut r = SaveReader::new(&bytes);
1004        eng.load_state(&mut r).unwrap();
1005
1006        assert!(eng.pc <= Revision::Upd7725.pc_mask());
1007        assert!(eng.rp <= Revision::Upd7725.rp_mask());
1008        assert!(eng.dp <= Revision::Upd7725.dp_mask());
1009        assert!(eng.sp <= 0xF);
1010        // Exercise the indices that would have panicked pre-fix.
1011        let _ = eng.program_rom[usize::from(eng.pc)];
1012        let _ = eng.data_rom[usize::from(eng.rp)];
1013        let _ = eng.stack[usize::from(eng.sp)];
1014    }
1015}