Skip to main content

rustysnes_cart/coproc/
hg51b.rs

1//! The Hitachi HG51B S169 core — the CX4 coprocessor's CPU (Mega Man X2, Mega Man X3).
2//!
3//! Clean-room port of ares' `HG51B` component (ISC, `component/processor/hg51b/`). Unlike the
4//! NEC DSP family (a fully separate chip program dumped to firmware), the HG51B's PROGRAM lives
5//! in the cartridge ROM the user already owns — it fetches 256-word pages into a 2-page on-chip
6//! instruction cache from cart ROM through the same bus the S-CPU sees (`cache()`), architecturally
7//! closer to this project's GSU/SA-1 ports than to DSP-1. Only a small 3 KiB **data ROM** (a
8//! trig/sqrt constant lookup table, `cx4.rom`) is a genuine external chip dump.
9//!
10//! Fixed-width 16-bit instruction word; ~30 real mnemonics (ALU/shift/branch/load-store/RAM-ROM
11//! access) decoded from the top nibble (see `dispatch`). A 3 KiB data RAM (`$000-$BFF`, folded
12//! from a `$000-$FFF` address space per the real chip's `>=$C00 -> -$400` quirk) and a 1024-entry
13//! 24-bit data ROM back the math tables; 16 general-purpose 24-bit registers; an 8-deep hardware
14//! call stack; a DMA unit and a suspend/wait state machine round out the chip.
15
16// Chip-name jargon (HG51B, CX4, GPR, ...) is not Rust code; the register/IO/cache state is
17// naturally dense with small bitfields and hardware-mirrored casts.
18#![allow(
19    clippy::doc_markdown,
20    clippy::struct_excessive_bools,
21    clippy::cast_possible_truncation,
22    clippy::cast_possible_wrap,
23    clippy::cast_sign_loss,
24    clippy::similar_names,
25    // The register/ALU/IO methods below are a direct, dense port of ares' hardware register
26    // switch statements; several happen not to touch runtime-only state and so LOOK const-
27    // eligible to clippy, but marking them const would be cosmetic noise against the source of
28    // truth's own (non-const) shape and buys nothing since none are called from a const context.
29    clippy::missing_const_for_fn
30)]
31
32use alloc::boxed::Box;
33
34use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
35
36/// The host-facing memory surface the HG51B core reads/writes through.
37///
38/// The S-CPU's cart ROM (shared, read-only from the chip's side) and any other
39/// externally-addressable region a specific board wires up (CX4 has none beyond ROM;
40/// `read`/`write`/`is_rom`/`is_ram` mirror ares' `HG51B::{read,write,isROM,isRAM}` virtual hooks).
41pub trait Hg51bBus {
42    /// Whether `address` (a chip-relative linear address) is cart ROM.
43    fn is_rom(&self, address: u32) -> bool;
44    /// Whether `address` is chip-visible RAM (CX4 has none; always `false`).
45    fn is_ram(&self, address: u32) -> bool;
46    /// Read a byte at a chip-relative linear address (cache refill, DMA, bus-port reads).
47    fn read(&mut self, address: u32) -> u8;
48    /// Write a byte at a chip-relative linear address (DMA only for CX4 — no chip-side RAM).
49    fn write(&mut self, address: u32, data: u8);
50}
51
52#[derive(Debug, Clone, Copy, Default)]
53struct Registers {
54    pb: u16,  // program bank (15-bit)
55    pc: u8,   // program counter (within the 256-word cached page)
56    n: bool,  // negative
57    z: bool,  // zero
58    c: bool,  // carry
59    v: bool,  // overflow
60    i: bool,  // interrupt pending (latched by an enabled halt)
61    a: u32,   // accumulator (24-bit)
62    p: u16,   // page register (15-bit)
63    mul: u64, // multiplier result (48-bit)
64    mdr: u32, // bus memory data register (24-bit)
65    rom: u32, // data-ROM read buffer (24-bit)
66    ram: u32, // data-RAM read/write buffer (24-bit)
67    mar: u32, // bus memory address register (24-bit)
68    dpr: u32, // data-RAM address pointer (24-bit)
69    gpr: [u32; 16],
70}
71
72#[derive(Debug, Clone, Copy, Default)]
73struct CacheState {
74    enable: bool,
75    page: bool,
76    lock: [bool; 2],
77    address: [u32; 2],
78    base: u32,
79    pb: u16,
80    pc: u8,
81}
82
83#[derive(Debug, Clone, Copy, Default)]
84struct DmaState {
85    enable: bool,
86    source: u32,
87    target: u32,
88    length: u16,
89}
90
91#[derive(Debug, Clone, Copy, Default)]
92struct BusState {
93    enable: bool,
94    reading: bool,
95    writing: bool,
96    pending: u32,
97    address: u32,
98}
99
100#[derive(Debug, Clone, Copy, Default)]
101struct Wait {
102    rom: u32,
103    ram: u32,
104}
105
106#[derive(Debug, Clone, Copy, Default)]
107struct Suspend {
108    enable: bool,
109    duration: u32,
110}
111
112#[derive(Debug, Clone, Copy)]
113struct Io {
114    lock: bool,
115    halt: bool, // starts true (chip idle until the host writes cache.pc)
116    irq: bool,  // false = enabled, true = disabled
117    rom_mapping: bool,
118    vector: [u8; 32],
119    wait: Wait,
120    suspend: Suspend,
121    cache: CacheState,
122    dma: DmaState,
123    bus: BusState,
124}
125
126impl Default for Io {
127    fn default() -> Self {
128        Self {
129            lock: false,
130            halt: true,
131            irq: false,
132            rom_mapping: true,
133            vector: [0; 32],
134            wait: Wait::default(),
135            suspend: Suspend::default(),
136            cache: CacheState::default(),
137            dma: DmaState::default(),
138            bus: BusState::default(),
139        }
140    }
141}
142
143/// The HG51B S169 core (CX4's CPU).
144///
145/// Free-runs synchronously to its next halt/wait state via [`Hg51b::run_until_halt`] — the same
146/// run-to-completion host-sync pattern this project's GSU (`Go`-bit) and DSP-1 (`RQM`) engines
147/// use, since the pc-write trigger (`$7f4f` while halted) is the only observable coupling to the
148/// S-CPU (`docs/cart.md` §CX4).
149pub struct Hg51b {
150    r: Registers,
151    io: Io,
152    program_ram: Box<[[u16; 256]; 2]>,
153    data_rom: Box<[u32; 1024]>,
154    data_ram: Box<[u8; 3072]>,
155    stack: [u32; 8],
156    data_rom_loaded: bool,
157    /// Guards against a runaway/malformed program looping forever inside one host trigger.
158    instructions_run: u64,
159}
160
161/// Hard cap on instructions executed per [`Hg51b::run_until_halt`] call — a runaway or malformed
162/// program halts the host call rather than the emulator (mirrors the GSU/DSP-1 engines' caps).
163const RUN_CAP: u64 = 20_000_000;
164
165impl core::fmt::Debug for Hg51b {
166    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
167        f.debug_struct("Hg51b")
168            .field("halted", &self.io.halt)
169            .field("data_rom_loaded", &self.data_rom_loaded)
170            .field("pb", &self.r.pb)
171            .field("pc", &self.r.pc)
172            .field("instructions_run", &self.instructions_run)
173            .finish_non_exhaustive()
174    }
175}
176
177impl Default for Hg51b {
178    fn default() -> Self {
179        Self::new()
180    }
181}
182
183impl Hg51b {
184    /// Construct a powered-off HG51B (inert until [`Hg51b::load_data_rom`] supplies the constant
185    /// table — `docs/adr/0003`).
186    #[must_use]
187    pub fn new() -> Self {
188        Self {
189            r: Registers::default(),
190            io: Io::default(),
191            program_ram: Box::new([[0u16; 256]; 2]),
192            data_rom: Box::new([0u32; 1024]),
193            data_ram: Box::new([0u8; 3072]),
194            stack: [0; 8],
195            data_rom_loaded: false,
196            instructions_run: 0,
197        }
198    }
199
200    /// Load the 3072-byte (1024 x 24-bit, 3 bytes/word little-endian) data-ROM constant table
201    /// (`cx4.rom`). Returns `false` (and leaves the chip inert) if the dump is the wrong size.
202    pub fn load_data_rom(&mut self, bytes: &[u8]) -> bool {
203        if bytes.len() < self.data_rom.len() * 3 {
204            return false;
205        }
206        for (i, word) in self.data_rom.iter_mut().enumerate() {
207            let o = i * 3;
208            *word =
209                u32::from(bytes[o]) | u32::from(bytes[o + 1]) << 8 | u32::from(bytes[o + 2]) << 16;
210        }
211        self.data_rom_loaded = true;
212        true
213    }
214
215    /// Whether the data-ROM constant table has been supplied (the chip is functional).
216    #[must_use]
217    pub const fn data_rom_loaded(&self) -> bool {
218        self.data_rom_loaded
219    }
220
221    /// Total instructions executed since power-on (debugger/diagnostics).
222    #[must_use]
223    pub const fn instructions_run(&self) -> u64 {
224        self.instructions_run
225    }
226
227    // --- Host-visible chip-relative address space (used by the board's addressDRAM/addressIO). --
228
229    /// Read the 3 KiB data RAM at a chip-relative offset (`$000-$FFF`, folded per the `>=$C00`
230    /// hardware quirk — see `fold_dram`).
231    #[must_use]
232    pub fn read_dram(&self, offset: u32) -> u8 {
233        let a = Self::fold_dram(offset);
234        if a >= 0xC00 {
235            0
236        } else {
237            self.data_ram[a as usize]
238        }
239    }
240
241    /// Write the 3 KiB data RAM at a chip-relative offset (see [`Self::read_dram`]).
242    pub fn write_dram(&mut self, offset: u32, data: u8) {
243        let a = Self::fold_dram(offset);
244        if a < 0xC00 {
245            self.data_ram[a as usize] = data;
246        }
247    }
248
249    const fn fold_dram(offset: u32) -> u32 {
250        let a = offset & 0xFFF;
251        if a >= 0xC00 { a - 0x400 } else { a }
252    }
253
254    /// Read the fixed IO register block (`$7F40-$7FEF`, DMA/cache/wait/IRQ/vector/GPR-mirror
255    /// controls — ares `HitachiDSP::readIO`). `local` is the address already folded to
256    /// `0x7C00 | (address & 0x3FF)` by the caller (the board owns the bus-window decode).
257    #[must_use]
258    pub fn read_io(&mut self, local: u32) -> u8 {
259        match local {
260            0x7F40 => byte(self.io.dma.source, 0),
261            0x7F41 => byte(self.io.dma.source, 1),
262            0x7F42 => byte(self.io.dma.source, 2),
263            0x7F43 => (self.io.dma.length & 0xFF) as u8,
264            0x7F44 => (self.io.dma.length >> 8) as u8,
265            0x7F45 => byte(self.io.dma.target, 0),
266            0x7F46 => byte(self.io.dma.target, 1),
267            0x7F47 => byte(self.io.dma.target, 2),
268            0x7F48 => u8::from(self.io.cache.page),
269            0x7F49 => byte(self.io.cache.base, 0),
270            0x7F4A => byte(self.io.cache.base, 1),
271            0x7F4B => byte(self.io.cache.base, 2),
272            0x7F4C => u8::from(self.io.cache.lock[0]) | (u8::from(self.io.cache.lock[1]) << 1),
273            0x7F4D => (self.io.cache.pb & 0xFF) as u8,
274            0x7F4E => (self.io.cache.pb >> 8) as u8,
275            0x7F4F => self.io.cache.pc,
276            0x7F50 => (self.io.wait.ram as u8) | ((self.io.wait.rom as u8) << 4),
277            0x7F51 => u8::from(self.io.irq),
278            0x7F52 => u8::from(self.io.rom_mapping),
279            0x7F53 | 0x7F54 | 0x7F55 | 0x7F56 | 0x7F57 | 0x7F59 | 0x7F5B | 0x7F5C | 0x7F5D
280            | 0x7F5E | 0x7F5F => {
281                u8::from(self.io.suspend.enable)
282                    | (u8::from(self.r.i) << 1)
283                    | (u8::from(self.running()) << 6)
284                    | (u8::from(self.busy()) << 7)
285            }
286            0x7F60..=0x7F7F => self.io.vector[(local & 0x1F) as usize],
287            0x7F80..=0x7FAF | 0x7FC0..=0x7FEF => {
288                let a = local & 0x3F;
289                byte(self.r.gpr[(a / 3) as usize], (a % 3) as u8)
290            }
291            _ => 0,
292        }
293    }
294
295    /// Write the fixed IO register block (see [`Self::read_io`]). `bus` supplies the chip-ROM
296    /// access this may need to kick off (a cache-page refill on the pc-write trigger).
297    pub fn write_io(&mut self, local: u32, data: u8, bus: &mut impl Hg51bBus) {
298        match local {
299            0x7F40 => set_byte(&mut self.io.dma.source, 0, data),
300            0x7F41 => set_byte(&mut self.io.dma.source, 1, data),
301            0x7F42 => set_byte(&mut self.io.dma.source, 2, data),
302            0x7F43 => self.io.dma.length = (self.io.dma.length & 0xFF00) | u16::from(data),
303            0x7F44 => self.io.dma.length = (self.io.dma.length & 0x00FF) | (u16::from(data) << 8),
304            0x7F45 => set_byte(&mut self.io.dma.target, 0, data),
305            0x7F46 => set_byte(&mut self.io.dma.target, 1, data),
306            0x7F47 => {
307                set_byte(&mut self.io.dma.target, 2, data);
308                if self.io.halt {
309                    self.io.dma.enable = true;
310                    self.run_until_halt(bus);
311                }
312            }
313            0x7F48 => {
314                self.io.cache.page = data & 1 != 0;
315                if self.io.halt {
316                    self.io.cache.enable = true;
317                    self.run_until_halt(bus);
318                }
319            }
320            0x7F49 => set_byte(&mut self.io.cache.base, 0, data),
321            0x7F4A => set_byte(&mut self.io.cache.base, 1, data),
322            0x7F4B => set_byte(&mut self.io.cache.base, 2, data),
323            0x7F4C => {
324                self.io.cache.lock[0] = data & 1 != 0;
325                self.io.cache.lock[1] = data & 2 != 0;
326            }
327            0x7F4D => self.io.cache.pb = (self.io.cache.pb & 0xFF00) | u16::from(data),
328            0x7F4E => {
329                self.io.cache.pb = (self.io.cache.pb & 0x00FF) | (u16::from(data & 0x7F) << 8);
330            }
331            0x7F4F => {
332                self.io.cache.pc = data;
333                if self.io.halt {
334                    self.io.halt = false;
335                    self.r.pb = self.io.cache.pb;
336                    self.r.pc = self.io.cache.pc;
337                    self.run_until_halt(bus);
338                }
339            }
340            0x7F50 => {
341                self.io.wait.ram = u32::from(data & 7);
342                self.io.wait.rom = u32::from((data >> 4) & 7);
343            }
344            0x7F51 => self.io.irq = data & 1 != 0,
345            0x7F52 => self.io.rom_mapping = data & 1 != 0,
346            0x7F53 => {
347                self.io.lock = false;
348                self.io.halt = true;
349            }
350            0x7F55 => {
351                self.io.suspend.enable = true;
352                self.io.suspend.duration = 0;
353            }
354            0x7F56 => {
355                self.io.suspend.enable = true;
356                self.io.suspend.duration = 32;
357            }
358            0x7F57 => {
359                self.io.suspend.enable = true;
360                self.io.suspend.duration = 64;
361            }
362            0x7F58 => {
363                self.io.suspend.enable = true;
364                self.io.suspend.duration = 96;
365            }
366            0x7F59 => {
367                self.io.suspend.enable = true;
368                self.io.suspend.duration = 128;
369            }
370            0x7F5A => {
371                self.io.suspend.enable = true;
372                self.io.suspend.duration = 160;
373            }
374            0x7F5B => {
375                self.io.suspend.enable = true;
376                self.io.suspend.duration = 192;
377            }
378            0x7F5C => {
379                self.io.suspend.enable = true;
380                self.io.suspend.duration = 224;
381            }
382            0x7F5D => self.io.suspend.enable = false,
383            0x7F5E => self.r.i = false,
384            0x7F60..=0x7F7F => self.io.vector[(local & 0x1F) as usize] = data,
385            0x7F80..=0x7FAF | 0x7FC0..=0x7FEF => {
386                let a = local & 0x3F;
387                set_byte(&mut self.r.gpr[(a / 3) as usize], (a % 3) as u8, data);
388            }
389            _ => {}
390        }
391    }
392
393    /// Whether the chip is doing anything at all (cache/dma/bus pending, or not halted).
394    #[must_use]
395    pub const fn running(&self) -> bool {
396        self.io.cache.enable || self.io.dma.enable || self.io.bus.pending > 0 || !self.io.halt
397    }
398
399    /// Whether the chip is mid-cache/dma/bus-access (narrower than [`Self::running`]).
400    #[must_use]
401    pub const fn busy(&self) -> bool {
402        self.io.cache.enable || self.io.dma.enable || self.io.bus.pending > 0
403    }
404
405    /// Whether the host IRQ line should be asserted (raised when the chip halts with IRQs
406    /// enabled — `docs/cart.md` §CX4's execution-model note).
407    #[must_use]
408    pub const fn irq_pending(&self) -> bool {
409        self.r.i
410    }
411
412    /// Run the chip to its next halt (or `RUN_CAP` instructions, whichever comes first) —
413    /// the host-sync run-to-completion pattern (see the struct doc).
414    pub fn run_until_halt(&mut self, bus: &mut impl Hg51bBus) {
415        if !self.data_rom_loaded {
416            return;
417        }
418        let mut n = 0u64;
419        // Mirrors ares' `main()` dispatch order: lock/suspend/cache/dma are each serviced to
420        // completion EVEN WHILE HALTED (a DMA or cache-page load triggered by a host write while
421        // the chip is halted — `$7F47`/`$7F48` — must still run; only bare instruction execution
422        // is gated on `!halt`), so the loop condition is "any of those is pending", not just
423        // "not halted".
424        while (self.io.lock
425            || self.io.suspend.enable
426            || self.io.cache.enable
427            || self.io.dma.enable
428            || !self.io.halt)
429            && n < RUN_CAP
430        {
431            self.main(bus);
432            n += 1;
433            self.instructions_run += 1;
434        }
435    }
436
437    fn main(&mut self, bus: &mut impl Hg51bBus) {
438        if self.io.lock {
439            return self.step(1);
440        }
441        if self.io.suspend.enable {
442            return self.suspend();
443        }
444        if self.io.cache.enable {
445            self.cache(bus);
446            return;
447        }
448        if self.io.dma.enable {
449            return self.dma(bus);
450        }
451        if self.io.halt {
452            return self.step(1);
453        }
454        self.execute(bus);
455    }
456
457    fn step(&mut self, clocks: u32) {
458        if !self.io.bus.enable {
459            return;
460        }
461        if self.io.bus.pending > clocks {
462            self.io.bus.pending -= clocks;
463        } else {
464            self.io.bus.enable = false;
465            self.io.bus.pending = 0;
466        }
467    }
468
469    /// The bus-port async access completion, deferred here since our `run_until_halt` is
470    /// synchronous rather than clock-ticked: performed inline the instant `step` would have
471    /// cleared `bus.pending` in ares' cycle-ticked model.
472    fn finish_bus_access(&mut self, bus: &mut impl Hg51bBus) {
473        if self.io.bus.enable && self.io.bus.pending == 0 {
474            self.io.bus.enable = false;
475            if self.io.bus.reading {
476                self.io.bus.reading = false;
477                self.r.mdr = u32::from(bus.read(self.io.bus.address));
478            }
479            if self.io.bus.writing {
480                self.io.bus.writing = false;
481                bus.write(self.io.bus.address, self.r.mdr as u8);
482            }
483        }
484    }
485
486    fn wait(&self, address: u32, bus: &impl Hg51bBus) -> u32 {
487        if bus.is_rom(address) {
488            return 1 + self.io.wait.rom;
489        }
490        if bus.is_ram(address) {
491            return 1 + self.io.wait.ram;
492        }
493        1
494    }
495
496    fn execute(&mut self, bus: &mut impl Hg51bBus) {
497        if !self.cache(bus) {
498            self.io.halt = true;
499            return;
500        }
501        let opcode = self.program_ram[usize::from(self.io.cache.page)][usize::from(self.r.pc)];
502        self.advance(bus);
503        self.step(1);
504        self.dispatch(opcode, bus);
505    }
506
507    fn advance(&mut self, bus: &mut impl Hg51bBus) {
508        let (pc, overflow) = self.r.pc.overflowing_add(1);
509        self.r.pc = pc;
510        if overflow {
511            if self.io.cache.page {
512                self.io.halt = true;
513                return;
514            }
515            self.io.cache.page = true;
516            if self.io.cache.lock[usize::from(self.io.cache.page)] {
517                self.io.halt = true;
518                return;
519            }
520            self.r.pb = self.r.p;
521            if !self.cache(bus) {
522                self.io.halt = true;
523            }
524        }
525    }
526
527    fn suspend(&mut self) {
528        if self.io.suspend.duration == 0 {
529            self.step(1);
530            return;
531        }
532        self.step(self.io.suspend.duration);
533        self.io.suspend.duration = 0;
534        self.io.suspend.enable = false;
535    }
536
537    /// Refill the requested cache page from cart ROM if not already resident. Returns `false`
538    /// (chip halts) if both pages are locked/unavailable.
539    fn cache(&mut self, bus: &mut impl Hg51bBus) -> bool {
540        let address = self.io.cache.base.wrapping_add(u32::from(self.r.pb) * 512);
541        if self.io.cache.address[usize::from(self.io.cache.page)] == address {
542            self.io.cache.enable = false;
543            return true;
544        }
545        self.io.cache.page = !self.io.cache.page;
546        if self.io.cache.address[usize::from(self.io.cache.page)] == address {
547            self.io.cache.enable = false;
548            return true;
549        }
550        if self.io.cache.lock[usize::from(self.io.cache.page)] {
551            self.io.cache.page = !self.io.cache.page;
552        }
553        if self.io.cache.lock[usize::from(self.io.cache.page)] {
554            self.io.cache.enable = false;
555            return false;
556        }
557
558        self.io.cache.address[usize::from(self.io.cache.page)] = address;
559        let mut a = address;
560        for offset in 0..256usize {
561            self.step(self.wait(a, bus));
562            let lo = bus.read(a);
563            a = a.wrapping_add(1);
564            let hi = bus.read(a);
565            a = a.wrapping_add(1);
566            self.program_ram[usize::from(self.io.cache.page)][offset] =
567                u16::from(lo) | (u16::from(hi) << 8);
568        }
569        self.io.cache.enable = false;
570        true
571    }
572
573    fn dma(&mut self, bus: &mut impl Hg51bBus) {
574        for offset in 0..u32::from(self.io.dma.length) {
575            let source = self.io.dma.source.wrapping_add(offset) & 0xFF_FFFF;
576            let target = self.io.dma.target.wrapping_add(offset) & 0xFF_FFFF;
577            if bus.is_rom(source) && bus.is_rom(target) {
578                self.io.lock = true;
579                return;
580            }
581            if bus.is_ram(source) && bus.is_ram(target) {
582                self.io.lock = true;
583                return;
584            }
585            self.step(self.wait(source, bus));
586            let data = bus.read(source);
587            self.step(self.wait(target, bus));
588            bus.write(target, data);
589        }
590        self.io.dma.enable = false;
591    }
592
593    // --- Register-index space (readRegister/writeRegister; distinct from the host IO block). --
594
595    fn read_register(&mut self, address: u16, bus: &mut impl Hg51bBus) -> u32 {
596        let v = match address {
597            0x01 => (self.r.mul >> 24) as u32 & 0xFF_FFFF,
598            0x02 => self.r.mul as u32 & 0xFF_FFFF,
599            0x03 => self.r.mdr,
600            0x08 => self.r.rom,
601            0x0C => self.r.ram,
602            0x13 => self.r.mar,
603            0x1C => self.r.dpr,
604            0x20 => u32::from(self.r.pc),
605            0x28 => u32::from(self.r.p),
606            0x2E => {
607                self.io.bus.enable = true;
608                self.io.bus.reading = true;
609                self.io.bus.pending = 1 + self.io.wait.rom;
610                self.io.bus.address = self.r.mar;
611                self.finish_bus_access(bus);
612                0
613            }
614            0x2F => {
615                self.io.bus.enable = true;
616                self.io.bus.reading = true;
617                self.io.bus.pending = 1 + self.io.wait.ram;
618                self.io.bus.address = self.r.mar;
619                self.finish_bus_access(bus);
620                0
621            }
622            // 0x50 (the all-zero constant) falls through to the wildcard arm below.
623            0x51 => 0xFF_FFFF,
624            0x52 => 0x00_FF00,
625            0x53 => 0xFF_0000,
626            0x54 => 0x00_FFFF,
627            0x55 => 0xFF_FF00,
628            0x56 => 0x80_0000,
629            0x57 => 0x7F_FFFF,
630            0x58 => 0x00_8000,
631            0x59 => 0x00_7FFF,
632            0x5A => 0xFF_7FFF,
633            0x5B => 0xFF_FF7F,
634            0x5C => 0x01_0000,
635            0x5D => 0xFE_FFFF,
636            0x5E => 0x00_0100,
637            0x5F => 0x00_FEFF,
638            0x60..=0x7F => self.r.gpr[usize::from(address & 0xF)],
639            _ => 0,
640        };
641        v & 0xFF_FFFF
642    }
643
644    fn write_register(&mut self, address: u16, data: u32, bus: &mut impl Hg51bBus) {
645        let data = data & 0xFF_FFFF;
646        match address {
647            0x01 => self.r.mul = (self.r.mul & 0x00_FF_FF_FF) | (u64::from(data) << 24),
648            0x02 => self.r.mul = (self.r.mul & 0xFF_FF_FF_00_00_00) | u64::from(data),
649            0x03 => self.r.mdr = data,
650            0x08 => self.r.rom = data,
651            0x0C => self.r.ram = data,
652            0x13 => self.r.mar = data,
653            0x1C => self.r.dpr = data,
654            0x20 => self.r.pc = data as u8,
655            0x28 => self.r.p = data as u16 & 0x7FFF,
656            0x2E => {
657                self.io.bus.enable = true;
658                self.io.bus.writing = true;
659                self.io.bus.pending = 1 + self.io.wait.rom;
660                self.io.bus.address = self.r.mar;
661                self.finish_bus_access(bus);
662            }
663            0x2F => {
664                self.io.bus.enable = true;
665                self.io.bus.writing = true;
666                self.io.bus.pending = 1 + self.io.wait.ram;
667                self.io.bus.address = self.r.mar;
668                self.finish_bus_access(bus);
669            }
670            0x60..=0x7F => self.r.gpr[usize::from(address & 0xF)] = data,
671            _ => {}
672        }
673    }
674
675    /// Write this core's mutable state — every register, the IO block, the two cached program
676    /// pages, the 3 KiB data RAM, and the 8-deep call stack — into an `"HG51"` section. The data
677    /// ROM (`cx4.rom`, firmware) is deliberately NOT written, per `docs/adr/0003`'s "never embed a
678    /// chip-ROM dump in a save-state" posture: it is reloaded fresh via [`Self::load_data_rom`]
679    /// before a matching [`Self::load_state`] call. `instructions_run` (a debugger counter, not
680    /// emulated-hardware state) is also omitted.
681    pub fn save_state(&self, w: &mut SaveWriter) {
682        w.section(*b"HG51", |s| {
683            s.write_u16(self.r.pb);
684            s.write_u8(self.r.pc);
685            s.write_bool(self.r.n);
686            s.write_bool(self.r.z);
687            s.write_bool(self.r.c);
688            s.write_bool(self.r.v);
689            s.write_bool(self.r.i);
690            s.write_u32(self.r.a);
691            s.write_u16(self.r.p);
692            s.write_u64(self.r.mul);
693            s.write_u32(self.r.mdr);
694            s.write_u32(self.r.rom);
695            s.write_u32(self.r.ram);
696            s.write_u32(self.r.mar);
697            s.write_u32(self.r.dpr);
698            for &g in &self.r.gpr {
699                s.write_u32(g);
700            }
701            s.write_bool(self.io.lock);
702            s.write_bool(self.io.halt);
703            s.write_bool(self.io.irq);
704            s.write_bool(self.io.rom_mapping);
705            s.write_bytes(&self.io.vector);
706            s.write_u32(self.io.wait.rom);
707            s.write_u32(self.io.wait.ram);
708            s.write_bool(self.io.suspend.enable);
709            s.write_u32(self.io.suspend.duration);
710            s.write_bool(self.io.cache.enable);
711            s.write_bool(self.io.cache.page);
712            s.write_bool(self.io.cache.lock[0]);
713            s.write_bool(self.io.cache.lock[1]);
714            s.write_u32(self.io.cache.address[0]);
715            s.write_u32(self.io.cache.address[1]);
716            s.write_u32(self.io.cache.base);
717            s.write_u16(self.io.cache.pb);
718            s.write_u8(self.io.cache.pc);
719            s.write_bool(self.io.dma.enable);
720            s.write_u32(self.io.dma.source);
721            s.write_u32(self.io.dma.target);
722            s.write_u16(self.io.dma.length);
723            s.write_bool(self.io.bus.enable);
724            s.write_bool(self.io.bus.reading);
725            s.write_bool(self.io.bus.writing);
726            s.write_u32(self.io.bus.pending);
727            s.write_u32(self.io.bus.address);
728            for page in self.program_ram.iter() {
729                for &word in page {
730                    s.write_u16(word);
731                }
732            }
733            for &byte in self.data_ram.iter() {
734                s.write_u8(byte);
735            }
736            for &word in &self.stack {
737                s.write_u32(word);
738            }
739        });
740    }
741
742    /// The inverse of [`Self::save_state`].
743    ///
744    /// # Errors
745    /// [`SaveStateError`] on truncated/corrupt input (the section framing itself already rejects
746    /// a wrong tag or a truncated read) or a section with unconsumed trailing bytes. Every
747    /// hardware register narrower than its Rust storage type is masked to its real width on load
748    /// (`pb`/`p`/`cache.pb` are 15-bit, `a`/`mdr`/`rom`/`ram`/`mar`/`dpr`/`gpr` are 24-bit, `mul`
749    /// is 48-bit, `wait.rom`/`wait.ram` are 3-bit) — the same "apply the engine's own
750    /// normal-operation width invariant on load" reasoning already applied to the NEC DSP engine's
751    /// `pc`/`rp`/`dp`/`sp`, extended here to every register (this core happens to have no field
752    /// whose valid range is narrower than its type in a way that risks an out-of-bounds array
753    /// index, but an unmasked wide value would still desync subsequent arithmetic from hardware
754    /// behavior, which matters for save-state fidelity even without a panic risk).
755    pub fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
756        let mut s = r.expect_section(*b"HG51")?;
757        self.r.pb = s.read_u16()? & 0x7FFF;
758        self.r.pc = s.read_u8()?;
759        self.r.n = s.read_bool()?;
760        self.r.z = s.read_bool()?;
761        self.r.c = s.read_bool()?;
762        self.r.v = s.read_bool()?;
763        self.r.i = s.read_bool()?;
764        self.r.a = s.read_u32()? & 0xFF_FFFF;
765        self.r.p = s.read_u16()? & 0x7FFF;
766        self.r.mul = s.read_u64()? & 0xFF_FF_FF_FF_FF_FF;
767        self.r.mdr = s.read_u32()? & 0xFF_FFFF;
768        self.r.rom = s.read_u32()? & 0xFF_FFFF;
769        self.r.ram = s.read_u32()? & 0xFF_FFFF;
770        self.r.mar = s.read_u32()? & 0xFF_FFFF;
771        self.r.dpr = s.read_u32()? & 0xFF_FFFF;
772        for g in &mut self.r.gpr {
773            *g = s.read_u32()? & 0xFF_FFFF;
774        }
775        self.io.lock = s.read_bool()?;
776        self.io.halt = s.read_bool()?;
777        self.io.irq = s.read_bool()?;
778        self.io.rom_mapping = s.read_bool()?;
779        self.io.vector.copy_from_slice(s.read_bytes(32)?);
780        self.io.wait.rom = s.read_u32()? & 7;
781        self.io.wait.ram = s.read_u32()? & 7;
782        self.io.suspend.enable = s.read_bool()?;
783        self.io.suspend.duration = s.read_u32()?;
784        self.io.cache.enable = s.read_bool()?;
785        self.io.cache.page = s.read_bool()?;
786        self.io.cache.lock[0] = s.read_bool()?;
787        self.io.cache.lock[1] = s.read_bool()?;
788        self.io.cache.address[0] = s.read_u32()?;
789        self.io.cache.address[1] = s.read_u32()?;
790        self.io.cache.base = s.read_u32()?;
791        self.io.cache.pb = s.read_u16()? & 0x7FFF;
792        self.io.cache.pc = s.read_u8()?;
793        self.io.dma.enable = s.read_bool()?;
794        self.io.dma.source = s.read_u32()?;
795        self.io.dma.target = s.read_u32()?;
796        self.io.dma.length = s.read_u16()?;
797        self.io.bus.enable = s.read_bool()?;
798        self.io.bus.reading = s.read_bool()?;
799        self.io.bus.writing = s.read_bool()?;
800        self.io.bus.pending = s.read_u32()?;
801        self.io.bus.address = s.read_u32()?;
802        for page in self.program_ram.iter_mut() {
803            for word in page.iter_mut() {
804                *word = s.read_u16()?;
805            }
806        }
807        for byte in self.data_ram.iter_mut() {
808            *byte = s.read_u8()?;
809        }
810        for word in &mut self.stack {
811            *word = s.read_u32()?;
812        }
813        if s.remaining() != 0 {
814            return Err(SaveStateError::Invalid(alloc::format!(
815                "HG51 section has {} trailing byte(s)",
816                s.remaining()
817            )));
818        }
819        Ok(())
820    }
821}
822
823const fn byte(v: u32, i: u8) -> u8 {
824    (v >> (i * 8)) as u8
825}
826
827fn set_byte(v: &mut u32, i: u8, data: u8) {
828    let shift = i * 8;
829    *v = (*v & !(0xFF << shift)) | (u32::from(data) << shift);
830}
831
832include!("hg51b_instructions.rs");