From 597b02881bbc89f4c6d282b84ffcbe675a185cf4 Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Mon, 5 Oct 2026 18:00:46 +0700 Subject: [PATCH] feat(input): implement KEYCNT keypad interrupt control Add a dependency-free keypad state machine in gba-core, map KEYCNT, latch IF bit 12, and wake HALT through the existing interrupt gates. Sample complete CPU/DMA control writes once so transient byte values cannot create false requests. Document the mGBA-style polling policy and its unverified hardware retrigger limits. Cover every button-state/selection pair, register masks, repeated requests, byte/word writes, failed-store isolation, DMA priority, ARM/Thumb IRQ return, BIOS IntrWait, and retained BIOS reset state. Follow the restructured workspace and modular documentation. Validated on Darwin arm64: workspace and core/demo tests pass in debug and release, with Clippy, rustdoc, ten native window smoke tests, and terminal demos passing. --- crates/gba-core/src/input.rs | 3 + crates/gba-core/src/input/keypad.rs | 88 +++++ crates/gba-core/src/io.rs | 41 +- crates/gba-core/src/memory.rs | 9 +- crates/gba-core/tests/bios/ram_reset.rs | 4 +- crates/gba-core/tests/graphics.rs | 5 +- crates/gba-core/tests/keypad.rs | 498 ++++++++++++++++++++++++ docs/architecture.md | 2 + docs/hardware/bios.md | 4 +- docs/hardware/timers-irq.md | 57 ++- docs/hardware/video.md | 4 +- docs/references.md | 6 +- docs/status.md | 6 +- docs/testing.md | 4 + 14 files changed, 710 insertions(+), 21 deletions(-) create mode 100644 crates/gba-core/src/input/keypad.rs create mode 100644 crates/gba-core/tests/keypad.rs diff --git a/crates/gba-core/src/input.rs b/crates/gba-core/src/input.rs index a540971..9e5eacf 100644 --- a/crates/gba-core/src/input.rs +++ b/crates/gba-core/src/input.rs @@ -1,6 +1,9 @@ //! Platform-independent GBA button state. Bits are pressed-high here; //! the KEYINPUT register exposes their active-low hardware representation. +mod keypad; +pub(crate) use keypad::Keypad; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u16)] pub enum Button { diff --git a/crates/gba-core/src/input/keypad.rs b/crates/gba-core/src/input/keypad.rs new file mode 100644 index 0000000..8fca15d --- /dev/null +++ b/crates/gba-core/src/input/keypad.rs @@ -0,0 +1,88 @@ +//! Functional keypad IRQ sampling from input snapshots and control writes. +//! OR requests on every matching sample. AND suppresses identical matching +//! snapshots, following mGBA's polling model. Exact hardware edges are unverified. + +use super::Buttons; + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub(crate) struct Keypad { + buttons: Buttons, + control: u16, + last_match: Option, +} + +impl Keypad { + pub(crate) fn keyinput(&self) -> u16 { + self.buttons.keyinput() + } + + pub(crate) fn control(&self) -> u16 { + self.control + } + + pub(crate) fn set_buttons(&mut self, buttons: Buttons) -> bool { + self.buttons = buttons; + self.sample() + } + + pub(crate) fn write_control(&mut self, value: u16) -> bool { + let value = value & 0xc3ff; + let newly_selected = (value & !self.control) & 0x03ff; + // A newly selected held key must not be hidden by an earlier AND sample. + if let Some(previous) = &mut self.last_match { + *previous &= !newly_selected; + } + self.control = value; + self.sample() + } + + fn sample(&mut self) -> bool { + if self.control & 0x4000 == 0 { + return false; + } + let selected = self.control & 0x03ff; + let pressed = self.buttons.bits(); + let all = self.control & 0x8000 != 0; + let matched = if all { + pressed & selected == selected + } else { + pressed & selected != 0 + }; + if !matched { + self.last_match = None; + return false; + } + let previous = self.last_match.replace(pressed); + !all || previous != Some(pressed) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn all_masks_and_button_states_match_independent_per_button_logic() { + for selected in 0..1024 { + for pressed in 0..1024 { + let mut any = false; + let mut all = true; + for bit in 0..10 { + if selected & (1 << bit) != 0 { + any |= pressed & (1 << bit) != 0; + all &= pressed & (1 << bit) != 0; + } + } + for (condition, expected) in [(0, any), (0x8000, all)] { + let mut keypad = Keypad::default(); + assert!(!keypad.set_buttons(Buttons::from_bits(pressed))); + assert_eq!( + keypad.write_control(selected | condition | 0x4000), + expected + ); + assert_eq!(keypad.keyinput(), !pressed & 0x03ff); + } + } + } + } +} diff --git a/crates/gba-core/src/io.rs b/crates/gba-core/src/io.rs index 0215e9a..9921c8c 100644 --- a/crates/gba-core/src/io.rs +++ b/crates/gba-core/src/io.rs @@ -1,11 +1,11 @@ -//! GBA I/O subset: DMA, timers, interrupts, WAITCNT, display, and polled input. +//! GBA I/O subset: DMA, timers, interrupts, WAITCNT, display, and keypad input. //! Timer phase resets on enable or clock-source changes. Hardware startup delays, //! shared prescaler phase, audio events, and interrupt delivery delays are not modeled. use crate::{ display::{Display, DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES}, dma::{Dma, DmaError, Transfer, DMA_BASE, DMA_END}, - input::Buttons, + input::{Buttons, Keypad}, }; pub const DISPCNT: u32 = 0x0400_0000; @@ -47,6 +47,7 @@ pub const BLDCNT: u32 = 0x0400_0050; pub const BLDALPHA: u32 = 0x0400_0052; pub const BLDY: u32 = 0x0400_0054; pub const KEYINPUT: u32 = 0x0400_0130; +pub const KEYCNT: u32 = 0x0400_0132; pub const TIMER_BASE: u32 = 0x0400_0100; pub const IE: u32 = 0x0400_0200; pub const IF: u32 = 0x0400_0202; @@ -137,7 +138,7 @@ pub(crate) struct Io { effects: crate::video::effects::Effects, window_edges: crate::video::windows::WindowEdges, horizontal_windows: crate::video::windows::horizontal::HorizontalWindows, - buttons: Buttons, + keypad: Keypad, display: Display, dma: Dma, postflg: bool, @@ -146,7 +147,7 @@ pub(crate) struct Io { impl Io { pub(crate) fn mapped(address: u32) -> bool { - matches!(address, 0x0400_0000..=0x0400_0057 | DMA_BASE..=DMA_END | 0x0400_0100..=0x0400_010f | 0x0400_0130..=0x0400_0131 | 0x0400_0200..=0x0400_020b | POSTFLG..=0x0400_0303) + matches!(address, 0x0400_0000..=0x0400_0057 | DMA_BASE..=DMA_END | 0x0400_0100..=0x0400_010f | 0x0400_0130..=0x0400_0133 | 0x0400_0200..=0x0400_020b | POSTFLG..=0x0400_0303) } pub(crate) fn read8(&self, address: u32) -> Option { @@ -165,7 +166,8 @@ impl Io { WINOUT => self.effects.outside, BLDCNT => self.effects.control, BLDALPHA => self.effects.alpha, - KEYINPUT => self.buttons.keyinput(), + KEYINPUT => self.keypad.keyinput(), + KEYCNT => self.keypad.control(), 0x0400_0100..=0x0400_010e => { let timer = &self.timers[((address - TIMER_BASE) / 4) as usize]; if address & 2 == 0 { @@ -188,8 +190,10 @@ impl Io { } /// The memory bus validates every byte before calling this method. + /// Keypad accesses use write_keypad instead, preserving transfer boundaries. pub(crate) fn write8(&mut self, address: u32, value: u8) { debug_assert!(Self::mapped(address)); + debug_assert!(!(KEYINPUT..=KEYCNT + 1).contains(&address)); if (DMA_BASE..=DMA_END).contains(&address) { self.dma.write8(address, value); return; @@ -198,7 +202,7 @@ impl Io { DISPCNT => self.dispcnt = replace_byte(self.dispcnt, address, value) & 0xfff7, GREENSWAP if address & 1 == 0 => self.greenswap = value & 1 != 0, DISPSTAT => self.pending |= self.display.write_status(address & 1 != 0, value), - VCOUNT | KEYINPUT => {} // Read-only; writes are ignored. + VCOUNT => {} // Read-only; writes are ignored. BG0CNT..=BG3CNT => { let index = ((address - BG0CNT) / 2) as usize; let control = &mut self.backgrounds[index].control; @@ -294,7 +298,30 @@ impl Io { } pub(crate) fn set_buttons(&mut self, buttons: Buttons) { - self.buttons = buttons; + if self.keypad.set_buttons(buttons) { + self.pending |= 1 << 12; + } + self.wake_if_requested(); + } + + /// Commit one validated byte/halfword/word access in the keypad block. + /// KEYINPUT writes are ignored. Sample once after both KEYCNT bytes merge, + /// never on a transient low-byte value during a halfword or word write. + pub(crate) fn write_keypad(&mut self, address: u32, bytes: &[u8]) { + debug_assert!(address >= KEYINPUT && address + bytes.len() as u32 <= KEYCNT + 2); + let mut control = self.keypad.control(); + let mut touched = false; + for (offset, byte) in bytes.iter().enumerate() { + let address = address + offset as u32; + if (KEYCNT..=KEYCNT + 1).contains(&address) { + control = replace_byte(control, address, *byte); + touched = true; + } + } + if touched && self.keypad.write_control(control) { + self.pending |= 1 << 12; + } + self.wake_if_requested(); } pub(crate) fn display_control(&self) -> u16 { diff --git a/crates/gba-core/src/memory.rs b/crates/gba-core/src/memory.rs index be71766..e2ddea0 100644 --- a/crates/gba-core/src/memory.rs +++ b/crates/gba-core/src/memory.rs @@ -4,7 +4,7 @@ use crate::{ display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES}, dma::{DmaError, DMA_BASE, DMA_END}, input::Buttons, - io::{Io, HALTCNT, POSTFLG}, + io::{Io, HALTCNT, KEYINPUT, POSTFLG}, timing::{bus_cycles, AccessKind, AccessWidth, DataTiming, StepTiming}, video::{self, sprites::pipeline, Framebuffer, VideoError}, }; @@ -325,7 +325,8 @@ impl Memory { } /// Replace the current pressed-button state. This does not advance time. - /// KEYCNT and keypad interrupts are not implemented yet. + /// Sample KEYCNT and latch a matching keypad IRQ immediately, without cycles. + /// This can wake HALT; CPU IRQ delivery occurs on a later Machine::step. pub fn set_buttons(&mut self, buttons: Buttons) { self.io.set_buttons(buttons); } @@ -424,6 +425,10 @@ impl Memory { }, ); if address >> 24 == 0x04 { + if address & !3 == KEYINPUT { + self.io.write_keypad(address, &bytes); + return Ok(()); + } for (offset, byte) in bytes.into_iter().enumerate() { let byte_address = address + offset as u32; if matches!(byte_address, POSTFLG | HALTCNT) && !self.can_write_power_control() { diff --git a/crates/gba-core/tests/bios/ram_reset.rs b/crates/gba-core/tests/bios/ram_reset.rs index a406959..51f9f65 100644 --- a/crates/gba-core/tests/bios/ram_reset.rs +++ b/crates/gba-core/tests/bios/ram_reset.rs @@ -4,7 +4,7 @@ use gba_core::{ dma::{DmaError, DMA_BASE, DMA_STRIDE}, input::Buttons, io::{ - BG0CNT, BG2PA, BG2PD, BG3PA, BG3PD, BLDALPHA, BLDCNT, DISPCNT, GREENSWAP, KEYINPUT, + BG0CNT, BG2PA, BG2PD, BG3PA, BG3PD, BLDALPHA, BLDCNT, DISPCNT, GREENSWAP, KEYCNT, KEYINPUT, WAITCNT, WININ, WINOUT, }, memory::{OAM_START, PALETTE_START}, @@ -236,6 +236,7 @@ fn io_reset_clears_readable_controls_but_keeps_input_postflg_and_green_swap() { let (mut m, pc) = prepare(flags, false, 0x9f); m.memory_mut().set_buttons(Buttons::from_bits(0x251)); let keys = m.memory().read16(KEYINPUT).unwrap(); + m.memory_mut().write16(KEYCNT, 0x4001).unwrap(); m.memory_mut().write16(GREENSWAP, 1).unwrap(); for address in [ BG0CNT, @@ -277,6 +278,7 @@ fn io_reset_clears_readable_controls_but_keeps_input_postflg_and_green_swap() { } assert_eq!(m.memory().read16(DISPSTAT).unwrap() & 0xff38, 0); assert_eq!(m.memory().read16(KEYINPUT).unwrap(), keys); + assert_eq!(m.memory().read16(KEYCNT).unwrap(), 0x4001); // Current reset subset preserves KEYCNT. assert_eq!(m.memory().read16(POSTFLG).unwrap(), 1); assert_eq!(m.memory().read16(GREENSWAP).unwrap(), 1); assert!(m.cycles() > cycles); diff --git a/crates/gba-core/tests/graphics.rs b/crates/gba-core/tests/graphics.rs index 63f9bb5..3cb5a45 100644 --- a/crates/gba-core/tests/graphics.rs +++ b/crates/gba-core/tests/graphics.rs @@ -279,10 +279,7 @@ fn keyinput_exposes_all_ten_buttons_active_low_and_ignores_writes() { memory.set_buttons(Buttons::default()); assert_eq!(memory.read16(KEYINPUT).unwrap(), 0x3ff); assert_eq!(memory.cycles(), 0); - assert_eq!( - memory.read16(KEYINPUT + 2), - Err(MemoryError::Unmapped(KEYINPUT + 2)) - ); // KEYCNT deferred. + assert_eq!(memory.read16(KEYINPUT + 2).unwrap(), 0); // KEYCNT resets disabled. } #[test] diff --git a/crates/gba-core/tests/keypad.rs b/crates/gba-core/tests/keypad.rs new file mode 100644 index 0000000..feb6fa6 --- /dev/null +++ b/crates/gba-core/tests/keypad.rs @@ -0,0 +1,498 @@ +use gba_core::{ + bios, + cpu::{Cpu, CpuError, InstructionSet, Mode}, + dma::DMA_BASE, + input::Buttons, + io::{HALTCNT, IE, IF, IME, KEYCNT, KEYINPUT, TIMER_BASE}, + machine::{Machine, MachineError, StepKind}, + memory::{Memory, MemoryError, BIOS_SIZE, ROM_START}, +}; + +const IRQ: u16 = 1 << 12; + +fn words(code: &[u32]) -> Vec { + code.iter().flat_map(|v| v.to_le_bytes()).collect() +} + +fn bus() -> Memory { + Memory::new(vec![]).unwrap() +} + +fn press(m: &mut Memory, buttons: u16) { + m.set_buttons(Buttons::from_bits(buttons)); +} + +fn pending(m: &Memory) -> bool { + m.read16(IF).unwrap() & IRQ != 0 +} + +fn ack(m: &mut Memory) { + m.write16(IF, IRQ).unwrap(); + assert!(!pending(m)); +} + +#[test] +fn register_reset_masks_byte_merges_and_combined_word_access() { + let mut m = bus(); + assert_eq!(m.read32(KEYINPUT).unwrap(), 0x03ff); + for value in 0..=u16::MAX { + m.write16(KEYCNT, value).unwrap(); + assert_eq!(m.read16(KEYCNT).unwrap(), value & 0xc3ff); + } + m.write16(KEYCNT, 0).unwrap(); + m.write8(KEYCNT + 1, 0xff).unwrap(); + assert_eq!(m.read16(KEYCNT).unwrap(), 0xc300); + m.write8(KEYCNT, 0xa5).unwrap(); + assert_eq!(m.read16(KEYCNT).unwrap(), 0xc3a5); + assert_eq!(m.read8(KEYCNT).unwrap(), 0xa5); + assert_eq!(m.read8(KEYCNT + 1).unwrap(), 0xc3); + press(&mut m, 0x201); + m.write32(KEYINPUT, 0x0123_ffff).unwrap(); + assert_eq!(m.read32(KEYINPUT).unwrap(), 0x0123_01fe); + m.write16(KEYINPUT, 0).unwrap(); + assert_eq!(m.read32(KEYINPUT).unwrap(), 0x0123_01fe); + assert_eq!(m.cycles(), 0); + assert_eq!(m.read32(KEYCNT), Err(MemoryError::Unaligned(KEYCNT))); + for address in [KEYCNT + 2, KEYCNT + 0x400] { + assert_eq!(m.read8(address), Err(MemoryError::Unmapped(address))); + assert_eq!(m.write8(address, 0), Err(MemoryError::Unmapped(address))); + } +} + +#[test] +fn every_button_can_request_irq_independently_of_ie_and_ime() { + for bit in 0..10 { + for mode in [0, 0x8000] { + let mut m = bus(); + m.write16(KEYCNT, 0x4000 | mode | (1 << bit)).unwrap(); + for other in 0..10 { + press(&mut m, if other == bit { 0 } else { 1 << other }); + assert!(!pending(&m)); + } + press(&mut m, 1 << bit); + assert!(pending(&m)); + assert!(!m.irq_pending()); + m.write16(IE, IRQ).unwrap(); + assert!(!m.irq_pending()); + m.write16(IME, 1).unwrap(); + assert!(m.irq_pending()); + } + } +} + +#[test] +fn or_mode_requests_on_each_matching_input_sample_but_not_on_clock_updates() { + let mut m = bus(); + m.write16(KEYCNT, 0x4003).unwrap(); + for buttons in [1, 1, 3, 2, 2] { + press(&mut m, buttons); + assert!(pending(&m)); + ack(&mut m); + m.advance_cycles(12345); + assert!(!pending(&m)); + assert_eq!(m.read16(KEYCNT).unwrap(), 0x4003); + } + press(&mut m, 4); + assert!(!pending(&m)); + press(&mut m, 0); + assert!(!pending(&m)); +} + +#[test] +fn and_mode_requires_selected_keys_but_allows_extra_keys_and_suppresses_identical_samples() { + let mut m = bus(); + m.write16(KEYCNT, 0xc003).unwrap(); + for buttons in [0, 1, 2, 4] { + press(&mut m, buttons); + assert!(!pending(&m)); + } + for buttons in [3, 7, 3] { + press(&mut m, buttons); + assert!(pending(&m)); // Includes changes to unselected keys in the polling model. + ack(&mut m); + press(&mut m, buttons); + assert!(!pending(&m)); + m.write16(KEYCNT, 0xc003).unwrap(); + assert!(!pending(&m)); + } + press(&mut m, 1); + assert!(!pending(&m)); + press(&mut m, 3); + assert!(pending(&m)); +} + +#[test] +fn empty_masks_follow_boolean_or_and_rules() { + for buttons in [0, 1, 0x3ff] { + let mut m = bus(); + press(&mut m, buttons); + m.write16(KEYCNT, 0x4000).unwrap(); + assert!(!pending(&m)); + m.write16(KEYCNT, 0xc000).unwrap(); + assert!(pending(&m)); + ack(&mut m); + press(&mut m, buttons); + assert!(!pending(&m)); + } +} + +#[test] +fn held_keys_are_sampled_on_control_writes_and_new_selection_rearms_and() { + let mut m = bus(); + press(&mut m, 3); + m.write16(KEYCNT, 0x0001).unwrap(); + assert!(!pending(&m)); + m.write16(KEYCNT, 0xc001).unwrap(); + assert!(pending(&m)); + ack(&mut m); + m.write16(KEYCNT, 0xc003).unwrap(); + assert!(pending(&m)); // B was newly selected while already held. + ack(&mut m); + m.write16(KEYCNT, 0xc003).unwrap(); + assert!(!pending(&m)); + m.write16(KEYCNT, 0xc007).unwrap(); + assert!(!pending(&m)); // Newly selected, but not all selected keys held. + m.write16(KEYCNT, 0xc003).unwrap(); + assert!(pending(&m)); +} + +#[test] +fn disabling_or_releasing_keys_does_not_acknowledge_if_and_disabled_sampling_keeps_history() { + let mut m = bus(); + press(&mut m, 1); + m.write16(KEYCNT, 0xc001).unwrap(); + assert!(pending(&m)); + m.write16(KEYCNT, 0x8001).unwrap(); + press(&mut m, 0); + assert!(pending(&m)); + ack(&mut m); + press(&mut m, 1); + m.write16(KEYCNT, 0xc001).unwrap(); + assert!(!pending(&m)); // No matching-state history update while disabled. + press(&mut m, 0); + press(&mut m, 1); + assert!(pending(&m)); +} + +#[test] +fn halfword_and_word_control_writes_never_sample_intermediate_byte_values() { + for word in [false, true] { + for (old, new, buttons) in [(0x4002, 0x0001, 1), (0xc001, 0x4000, 0)] { + let mut m = bus(); + press(&mut m, buttons); + m.write16(KEYCNT, old).unwrap(); + assert!(!pending(&m)); + if word { + m.write32(KEYINPUT, u32::from(new) << 16).unwrap(); + } else { + m.write16(KEYCNT, new).unwrap(); + } + assert!(!pending(&m)); + assert_eq!(m.read16(KEYCNT).unwrap(), new); + } + } + // Separate byte stores really do expose the intermediate enabled mask. + let mut m = bus(); + press(&mut m, 1); + m.write16(KEYCNT, 0x4002).unwrap(); + m.write8(KEYCNT, 1).unwrap(); + assert!(pending(&m)); + m.write8(KEYCNT + 1, 0).unwrap(); + assert!(pending(&m)); +} + +#[test] +fn keyinput_writes_and_reads_do_not_resample_an_acknowledged_request() { + let mut m = bus(); + m.write16(KEYCNT, 0x4001).unwrap(); + press(&mut m, 1); + ack(&mut m); + m.write8(KEYINPUT, 0).unwrap(); + m.write8(KEYINPUT + 1, 0).unwrap(); + m.write16(KEYINPUT, 0).unwrap(); + assert_eq!(m.read16(KEYINPUT).unwrap(), 0x3fe); + assert_eq!(m.read32(KEYINPUT).unwrap(), 0x4001_03fe); + assert!(!pending(&m)); + // An actual KEYCNT write is a new sample, even if its value is unchanged. + m.write16(KEYCNT, 0x4001).unwrap(); + assert!(pending(&m)); +} + +fn machine(masked: bool) -> Machine { + let mut m = Machine::new( + Cpu::new(ROM_START), + Memory::new(words(&[ + if masked { 0xe321_f09f } else { 0xe321_f01f }, + 0xe280_0001, + 0xeaff_fffe, + ])) + .unwrap(), + ); + m.step().unwrap(); + m +} + +#[test] +fn keypad_wakes_halt_only_through_ie_and_ignores_ime_and_cpu_masks_for_wake() { + for masked in [false, true] { + for ie in [0, IRQ] { + for ime in [0, 1] { + let mut m = machine(masked); + m.memory_mut().write16(KEYCNT, 0x4001).unwrap(); + m.memory_mut().write16(IE, ie).unwrap(); + m.memory_mut().write16(IME, ime).unwrap(); + m.memory_mut().write8(HALTCNT, 0).unwrap(); + let before = m.cpu().clone(); + let cycles = m.cycles(); + let timing = m.last_timing(); + press(m.memory_mut(), 1); + assert!(pending(m.memory())); + assert_eq!(m.cpu(), &before); + assert_eq!(m.cycles(), cycles); + assert_eq!(m.last_timing(), timing); + assert_eq!(m.halted(), ie == 0); + let expected = if ie == 0 { + StepKind::HaltIdle + } else if ime != 0 && !masked { + StepKind::IrqEntry + } else { + StepKind::Instruction + }; + assert_eq!(m.step().unwrap(), expected); + } + } + } +} + +#[test] +fn pending_keypad_request_wakes_on_ie_enable_and_ack_does_not_restore_halt() { + let mut m = machine(false); + m.memory_mut().write16(KEYCNT, 0x4001).unwrap(); + m.memory_mut().write8(HALTCNT, 0).unwrap(); + press(m.memory_mut(), 1); + assert!(m.halted()); + m.memory_mut().write16(IE, IRQ).unwrap(); + assert!(!m.halted()); + ack(m.memory_mut()); + assert!(!m.halted()); + assert_eq!(m.step().unwrap(), StepKind::Instruction); +} + +#[test] +fn control_write_can_wake_halt_with_an_already_held_key() { + let mut m = machine(false); + press(m.memory_mut(), 1); + m.memory_mut().write16(IE, IRQ).unwrap(); + m.memory_mut().write8(HALTCNT, 0).unwrap(); + m.memory_mut().write16(KEYCNT, 0x4001).unwrap(); + assert!(!m.halted()); + assert_eq!(m.step().unwrap(), StepKind::Instruction); // IME remains zero. +} + +#[test] +fn keypad_irq_entry_and_return_restore_arm_and_thumb_instruction_state() { + for thumb in [false, true] { + let rom = if thumb { + let mut code = words(&[0xe28f_0001, 0xe12f_ff10]); + code.extend(0x2107_u16.to_le_bytes()); + code + } else { + words(&[0xe3a0_1007]) + }; + let bios = 0xe25e_f004_u32.to_le_bytes().repeat(BIOS_SIZE / 4); + let mut mem = Memory::with_bios(rom, bios).unwrap(); + let mut cpu = Cpu::new(ROM_START); + if thumb { + cpu.step(&mut mem).unwrap(); + cpu.step(&mut mem).unwrap(); + } + let pc = cpu.pc(); + let status = cpu.cpsr(); + mem.write16(KEYCNT, 0x4001).unwrap(); + mem.write16(IE, IRQ).unwrap(); + mem.write16(IME, 1).unwrap(); + press(&mut mem, 1); + let mut m = Machine::new(cpu, mem); + assert_eq!(m.step().unwrap(), StepKind::IrqEntry); + assert_eq!(m.cpu().mode(), Mode::Irq); + assert_eq!(m.cpu().spsr(), Some(status)); + assert_eq!(m.cpu().registers()[14], pc + 4); + assert!(pending(m.memory())); + ack(m.memory_mut()); + m.step().unwrap(); + assert_eq!(m.cpu().pc(), pc); + assert_eq!(m.cpu().cpsr(), status); + assert_eq!( + m.cpu().instruction_set(), + if thumb { + InstructionSet::Thumb + } else { + InstructionSet::Arm + } + ); + m.step().unwrap(); + assert_eq!(m.cpu().registers()[1], 7); + } +} + +fn prepared(instruction: u32, destination: u32, value: u32) -> Machine { + let mut mem = Memory::new(words(&[ + 0xe59f_0008, + 0xe59f_1008, + instruction, + 0xeaff_fffe, + destination, + value, + ])) + .unwrap(); + let mut cpu = Cpu::new(ROM_START); + cpu.step(&mut mem).unwrap(); + cpu.step(&mut mem).unwrap(); + Machine::new(cpu, mem) +} + +#[test] +fn cpu_store_samples_keys_then_irq_enters_on_the_following_step() { + for (instruction, address, value) in [ + (0xe1c0_10b0, KEYCNT, 0x4001), + (0xe580_1000, KEYINPUT, 0x4001_ffff), + ] { + let mut m = prepared(instruction, address, value); + press(m.memory_mut(), 1); + m.memory_mut().write16(IE, IRQ).unwrap(); + m.memory_mut().write16(IME, 1).unwrap(); + assert_eq!(m.step().unwrap(), StepKind::Instruction); + assert_eq!(m.cpu().pc(), ROM_START + 12); + assert_eq!(m.memory().read16(KEYCNT).unwrap(), 0x4001); + assert_eq!(m.memory().read16(KEYINPUT).unwrap(), 0x3fe); + assert_eq!(m.step().unwrap(), StepKind::IrqEntry); + } +} + +#[test] +fn failed_block_store_does_not_commit_key_control_irq_history_or_halt_wake() { + // STMIA r0!,{r1,r2}: keypad word is valid, next word (serial RCNT) is not mapped. + let mut m = prepared(0xe8a0_0006, KEYINPUT, 0x4001_0000); + press(m.memory_mut(), 1); + m.memory_mut().write16(IE, IRQ).unwrap(); + let before = m.cpu().clone(); + let cycles = m.cycles(); + let timing = m.last_timing(); + assert_eq!( + m.step(), + Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped( + KEYINPUT + 4 + )))) + ); + assert_eq!(m.cpu(), &before); + assert_eq!(m.cycles(), cycles); + assert_eq!(m.last_timing(), timing); + assert_eq!(m.memory().read16(KEYCNT).unwrap(), 0); + assert!(!pending(m.memory())); + m.memory_mut().write8(HALTCNT, 0).unwrap(); + assert!(m.halted()); + m.memory_mut().write16(KEYCNT, 0xc001).unwrap(); + assert!(pending(m.memory())); // Failure did not consume the first AND sample. + assert!(!m.halted()); +} + +#[test] +fn dma_control_write_samples_once_and_ready_dma_precedes_irq() { + for word in [false, true] { + let mut m = machine(false); + press(m.memory_mut(), 1); + m.memory_mut().write16(IE, IRQ).unwrap(); + m.memory_mut().write16(IME, 1).unwrap(); + let value = if word { 0x4001_ffff } else { 0x4001 }; + m.memory_mut().write32(0x0200_0000, value).unwrap(); + m.memory_mut().write32(DMA_BASE, 0x0200_0000).unwrap(); + m.memory_mut() + .write32(DMA_BASE + 4, if word { KEYINPUT } else { KEYCNT }) + .unwrap(); + // Fixed destination, two units. IRQ from first unit must wait for second. + m.memory_mut() + .write32(DMA_BASE + 8, if word { 0x8440_0002 } else { 0x8040_0002 }) + .unwrap(); + let before = m.cpu().clone(); + assert_eq!(m.step().unwrap(), StepKind::Dma { channel: 0 }); + assert!(pending(m.memory())); + assert_eq!(m.cpu(), &before); + assert_eq!(m.step().unwrap(), StepKind::Dma { channel: 0 }); + assert_eq!(m.cpu(), &before); + assert_eq!(m.step().unwrap(), StepKind::IrqEntry); + } +} + +#[test] +fn keypad_and_timer_requests_coexist_and_acknowledge_independently() { + let mut m = bus(); + m.write32(TIMER_BASE, 0x00c0_ffff).unwrap(); + m.advance_cycles(1); + m.write16(TIMER_BASE + 2, 0).unwrap(); + m.write16(KEYCNT, 0x4001).unwrap(); + press(&mut m, 1); + assert_eq!(m.read16(IF).unwrap(), IRQ | 8); + ack(&mut m); + assert_eq!(m.read16(IF).unwrap(), 8); + press(&mut m, 1); + m.write16(IF, 8).unwrap(); + assert_eq!(m.read16(IF).unwrap(), IRQ); +} + +#[test] +fn original_bios_intrwait_can_sleep_until_keypad_callback_records_the_request() { + let rom = words(&[0xe3a0_0001, 0xe3a0_1a01, 0xef04_0000, 0xeaff_fffe]); + let mut m = bios::boot(rom).unwrap(); + for _ in 0..100 { + if m.cpu().pc() == ROM_START + 8 { + break; + } + m.step().unwrap(); + } + assert_eq!(m.cpu().pc(), ROM_START + 8); + let before = m.cpu().clone(); + let callback = words(&[ + 0xe280_0c02, + 0xe1d0_10b2, + 0xe1c0_10b2, + 0xe59f_2010, + 0xe1d2_30b0, + 0xe183_3001, + 0xe1c2_30b0, + 0xe12f_ff1e, + 0xe1a0_0000, + bios::IRQ_FLAGS, + ]); + for (i, byte) in callback.into_iter().enumerate() { + m.memory_mut().write8(0x0300_1000 + i as u32, byte).unwrap(); + } + m.memory_mut() + .write32(bios::IRQ_HANDLER, 0x0300_1000) + .unwrap(); + m.memory_mut().write16(KEYCNT, 0x4001).unwrap(); + m.memory_mut().write16(IE, IRQ).unwrap(); + for _ in 0..200 { + if m.halted() { + break; + } + m.step().unwrap(); + } + assert!(m.halted()); + press(m.memory_mut(), 1); + assert!(!m.halted()); + let mut irq_entries = 0; + for _ in 0..300 { + if m.cpu().pc() == ROM_START + 12 { + break; + } + if m.step().unwrap() == StepKind::IrqEntry { + irq_entries += 1; + } + } + assert_eq!(irq_entries, 1); + assert_eq!(m.cpu().pc(), ROM_START + 12); + assert_eq!(m.cpu().cpsr(), before.cpsr()); + assert_eq!(&m.cpu().registers()[..15], &before.registers()[..15]); + assert_eq!(m.memory().read16(IF).unwrap(), 0); + assert_eq!(m.memory().read16(bios::IRQ_FLAGS).unwrap(), 0); +} diff --git a/docs/architecture.md b/docs/architecture.md index 49bfcc4..ffcf60c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -61,6 +61,7 @@ The repository is a Cargo workspace with three crates: | `crates/gba-core/src/video/sprites/pipeline.rs` | Two prepared row buffers, cycle40 events, and bounded capture-disabled advancement | | `crates/gba-core/src/video/sprites/budget.rs` | Nominal per-row work allowance, partial canvas prefixes, and independent arithmetic tests | | `crates/gba-core/src/input.rs` | Platform-independent GBA button state | +| `crates/gba-core/src/input/keypad.rs` | KEYCNT mask, OR/AND matching, and keypad IRQ sampling history | | `crates/gba-demos/src/graphics_demo.rs` | Original ARM bitmap program and shared bounded frame runner | | `crates/gba-demos/src/tile_demo.rs` | Original ARM tile/sprite program, palettes, tiles, maps, and OAM image | | `crates/gba-demos/src/affine_demo.rs` | Shared ARM background program builder and Mode 2 assets | @@ -84,6 +85,7 @@ The repository is a Cargo workspace with three crates: | `crates/gba-core/tests/timers.rs` | Timer rules, interrupt registers, and a cycle-by-cycle reference | | `crates/gba-core/tests/machine.rs` | Device IRQ entry/return, clock policy, and I/O failure atomicity | | `crates/gba-core/tests/timing.rs` | WAITCNT fields, bus costs, and timer/IRQ timing integration | +| `crates/gba-core/tests/keypad.rs` | Keypad register widths, IRQ sampling, CPU/DMA ordering, HALT wake-up, diagnostics, and BIOS IntrWait | | `crates/gba-core/tests/graphics.rs` | Video memory, rendering, KEYINPUT, and CPU-driven graphics integration | | `crates/gba-core/tests/tiles.rs` | Mode 0 registers, maps, palettes, composition, diagnostics, and CPU tile/sprite demo | | `crates/gba-core/tests/affine_backgrounds.rs` | Affine registers, maps, transforms, bitmap sampling, composition, and CPU demo | diff --git a/docs/hardware/bios.md b/docs/hardware/bios.md index a2893b0..1c565d9 100644 --- a/docs/hardware/bios.md +++ b/docs/hardware/bios.md @@ -166,8 +166,8 @@ Bit 7 runs after the selected RAM clears: Timer counter reads retain the stopped count until a later enable loads the cleared reload value. This is normal timer-register behavior, not a host-side replacement of device state. Read-only display status and VCOUNT continue to reflect the advancing display clock. -GREENSWAP, KEYINPUT/button state, POSTFLG, and BIOS IRQ communication words are not reset. -Keypad interrupt control is not implemented. Bit 7 covers only the listed supported registers. +GREENSWAP, KEYINPUT/button state, KEYCNT, POSTFLG, and BIOS IRQ communication words are not reset. +Bit 7 covers only the listed registers. A later matching input sample can request keypad IRQ again after IF acknowledgement. Without bit 7, device configuration remains unchanged except for DISPCNT. IRQ delivery stays CPU-masked during the call. The service restores the caller's mask on return. diff --git a/docs/hardware/timers-irq.md b/docs/hardware/timers-irq.md index 20a25e6..ff5c226 100644 --- a/docs/hardware/timers-irq.md +++ b/docs/hardware/timers-irq.md @@ -38,6 +38,8 @@ Mapped input/output (I/O) registers: | --- | --- | --- | | `0x04000100` + `4*n` | Timer n counter/reload | Reads the counter; writes the separate reload value | | `0x04000102` + `4*n` | Timer n control | Prescaler, count-up, local IRQ enable, and start/stop | +| `0x04000130` | KEYINPUT | Ten active-low button bits; read-only | +| `0x04000132` | KEYCNT | Button selection, keypad IRQ enable, and OR/AND condition | | `0x04000200` | IE | Enables selected interrupt sources; bits 0–13 are writable | | `0x04000202` | IF | Latches requests; writing 1 clears the corresponding bit | | `0x04000204` | WAITCNT | Configures first/second ROM access wait states; unused bits read as zero | @@ -61,7 +63,7 @@ Byte, halfword, and word accesses are supported for this register subset. Timer startup delays and shared prescaler phase are not modeled. The provisional timer model resets its private prescaler phase on enable or clock-source changes. -Audio events, STOP, and interrupt sources other than timers, display events, and DMA completion are not implemented. +Audio events, STOP, and interrupt sources other than timers, display events, DMA completion, and keypad input are not implemented. `--timer-demo` executes original ARM code that configures Timer 0, IE, and IME. The timer overflows after 16 supplied cycles and enters the original handler through vector `0x18`. @@ -70,6 +72,59 @@ The demo ends after 21 steps and 153 nominal cycles with one handler call, using These counts test the timing model; they are not hardware timing measurements. Its test handler deliberately changes r2 and r10; it does not implement the Nintendo BIOS calling convention. +## Keypad interrupt control + +`KEYCNT` resets to zero and stores only bits selected by `0xc3ff`: + +| Bits | Meaning | +| --- | --- | +| 0–9 | Select A, B, Select, Start, Right, Left, Up, Down, R, and L | +| 10–13 | Unused; read as zero | +| 14 | Enable keypad interrupt requests | +| 15 | Zero: OR condition; one: AND condition | + +OR matches when any selected button is pressed. +AND matches when every selected button is pressed; unselected pressed buttons do not prevent a match. +An empty selection never matches OR and always matches AND. + +A matching enabled sample can latch IF bit 12, independently of IE, IME, and CPSR.I. +Changing input or disabling KEYCNT does not clear an existing request. +Software acknowledges IF by writing bit 12 as one. +IE bit 12 gates HALT wake-up. IME and CPSR.I gate IRQ delivery, not wake-up or request latching. +Ready DMA units still take priority over CPU IRQ entry. +The original BIOS IntrWait can consume keypad events through the normal IRQ callback contract. + +### Sampling policy and limits + +The current functional model follows mGBA's polling behavior, not a verified hardware edge detector: + +- Each `Memory::set_buttons` call samples the input, including repeated identical values. +- Each successful write touching KEYCNT samples after the complete register update. +- OR requests on every matching enabled sample, so another identical input sample can reassert an acknowledged request. +- AND remembers the complete pressed-button snapshot and suppresses an identical matching sample. +- A different matching snapshot requests again, including changes to unselected buttons. +- An enabled nonmatching sample clears that history. Disabled samples retain it. +- Newly selected buttons are removed from the remembered snapshot before testing a control write. + A newly selected held button can therefore cause another AND request. +- Reads, KEYINPUT-only writes, IF acknowledgement, and clock advancement do not sample the keypad. + +This policy makes IRQ generation depend on input sampling, not only on elapsed emulated time. +Host input calls consume no emulated cycles and do not modify CPU registers. +They can wake HALT immediately; `Machine::step` subsequently samples the IRQ line. +The desktop demos currently supply frame-based input snapshots. +Physical key-event timing, switch bounce, exact hardware retrigger rules, and synchronization delays remain unverified. +STOP still returns a diagnostic; this change does not implement STOP wake-up. + +Byte writes merge with the untouched KEYCNT byte. +Halfword writes, and word writes at KEYINPUT, evaluate the final KEYCNT value once. +They do not generate requests from temporary byte-by-byte values. +The lower half of a word write remains a read-only KEYINPUT write. +CPU, DMA, and host/debug writes use the same path. +Failed writes and failed block stores leave control, IRQ history, pending flags, and HALT state unchanged. + +The current BIOS SoftReset and RegisterRamReset subset preserve KEYCNT and button state. +RegisterRamReset bit 7 still acknowledges IF, but a later matching input sample can request keypad IRQ again. + ## HALT and power-control registers Writing HALTCNT with bit 7 clear requests HALT. Bits 0–6 are ignored. diff --git a/docs/hardware/video.md b/docs/hardware/video.md index 9c5b89e..1da79aa 100644 --- a/docs/hardware/video.md +++ b/docs/hardware/video.md @@ -8,7 +8,9 @@ Rendering behavior implemented in `crates/gba-core/src/video/`, plus button inpu `GREENSWAP` at `0x04000002` exchanges the green channels of adjacent pixel pairs when bit zero is set. `KEYINPUT` at `0x04000130` exposes ten active-low buttons. Unused bits read as zero; writes are ignored. `Memory::set_buttons(Buttons)` supplies pressed-high host input. The I/O layer converts it to active-low register bits. -`KEYCNT` and keypad IRQs are not implemented; accesses to KEYCNT return unmapped-memory diagnostics. +`KEYCNT` at `0x04000132` selects buttons, OR/AND matching, and keypad interrupt enable. +Input samples and control writes can latch IF bit 12 and wake HALT without advancing the clock. +See [keypad interrupt control](timers-irq.md#keypad-interrupt-control) for the sampling policy and limits. Video RAM starts at `0x06000000` and contains 96 KiB. Its 128 KiB mirror layout repeats the final 32 KiB in offsets `0x18000..0x1ffff`. diff --git a/docs/references.md b/docs/references.md index 9c6988c..ee2ac06 100644 --- a/docs/references.md +++ b/docs/references.md @@ -45,7 +45,11 @@ Hardware references used for the core: - [NanoBoyAdvance composition](https://github.com/nba-emu/NanoBoyAdvance/blob/master/src/nba/src/hw/ppu/merge.cc), reviewed for palette lookup and horizontal mosaic after sprite preparation. - [mGBA window renderer](https://github.com/mgba-emu/mgba/blob/master/src/gba/renderers/video-software.c), reviewed for inverted bounds and vertical edge flags. - [NanoBoyAdvance window implementation](https://github.com/nba-emu/NanoBoyAdvance/blob/master/src/nba/src/hw/ppu/window.cc), reviewed for persistent vertical flags, four-cycle horizontal comparisons, offscreen columns, and end-edge precedence. -- [GBATEK keypad input](https://problemkaputt.de/gbatek-gba-keypad-input.htm). +- [GBATEK keypad input](https://problemkaputt.de/gbatek-gba-keypad-input.htm), for KEYINPUT/KEYCNT fields and selected-button OR/AND matching. +- [mGBA keypad sampling](https://github.com/mgba-emu/mgba/blob/master/src/gba/gba.c) and [control writes](https://github.com/mgba-emu/mgba/blob/master/src/gba/io.c), reviewed for repeated OR requests, AND snapshot suppression, and newly selected held keys. +- [NanoBoyAdvance keypad implementation](https://github.com/nba-emu/NanoBoyAdvance/blob/master/src/nba/src/hw/keypad/keypad.cc), compared for control access widths; its AND/retrigger behavior differs from the chosen polling model. +- [mGBA keypad timing issue](https://github.com/mgba-emu/mgba/issues/2490), for frontend frame-based input timing limits. +- [Tonc hardware interrupts](https://gbadev.net/tonc/interrupts.html), for keypad source enable and IF/IE bit 12. - [GBATEK memory mirrors and video byte writes](https://problemkaputt.de/gbatek-gba-unpredictable-things.htm). - [GBATEK GBA memory map and bus widths](https://problemkaputt.de/gbatek-gba-memory-map.htm). - [GBATEK GBA DMA transfers](https://problemkaputt.de/gbatek-gba-dma-transfers.htm). diff --git a/docs/status.md b/docs/status.md index 7eb58b3..083c289 100644 --- a/docs/status.md +++ b/docs/status.md @@ -24,6 +24,7 @@ Update this file when a feature lands or a limit is removed. - Alpha blending, brightness increase/decrease, and semi-transparent sprites using five-bit color arithmetic. - 96 KiB video RAM, 1 KiB palette RAM, and 1 KiB object attribute memory, with mirrors and byte-write behavior. - Active-low `KEYINPUT` for all ten GBA buttons, independent of the window library. +- `KEYCNT` selection and OR/AND matching, keypad IRQ latching, and HALT wake-up through IE bit 12. - Original ARM graphics demo that uses DMA, BIOS VBlank waiting, and a ROM-side IRQ callback. - Sixteen active 32-bit CPU registers, with ARM7 mode-specific register banks. - User, System, Supervisor, IRQ, FIQ, Abort, and Undefined modes. @@ -95,7 +96,8 @@ Horizontal window comparators retain four-cycle event history. Sprite rows prepare one line ahead with a whole-row cycle40 sample and nominal per-row work limits. Individual fetch timing, hardware-accurate work cutoffs, video-bus contention, and per-pixel color composition remain unimplemented. There is no sound or cartridge save support. -Mapped I/O covers DMA, timers, interrupts, WAITCNT, display control/status, background control/scroll/affine registers, window/effect registers, KEYINPUT, POSTFLG, and HALTCNT. +Mapped I/O covers DMA, timers, interrupts, WAITCNT, display control/status, background control/scroll/affine registers, window/effect registers, KEYINPUT/KEYCNT, POSTFLG, and HALTCNT. +Keypad IRQs use a documented polling model; hardware retrigger edges and asynchronous input timing remain unverified. The presentation framebuffer is separate from emulated video RAM. All named graphics demos connect desktop input and rendering to the emulated CPU. The default display test remains host-generated. The executable does not accept ROM files yet. @@ -110,7 +112,7 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa 1. Validate CPU behavior with public ARM7TDMI test programs before claiming instruction compatibility. 2. Refine nominal timing with a fetch pipeline, Game Pak prefetch, per-access device updates, and verified timer/IRQ delays. -3. Add keypad interrupt control and remaining device registers; extend BIOS reset coverage as sound/serial support becomes available. +3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available. Add STOP and remaining DMA device modes with verified timing and wake-up behavior. 4. Replace nominal sprite work limits with verified individual fetch timing; add background fetch timing and per-pixel composition. 5. Expand graphics, audio, cartridge loading, and saves before testing Emerald compatibility. diff --git a/docs/testing.md b/docs/testing.md index 774b0ea..d59b028 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -148,6 +148,10 @@ Frame tests verify consecutive VBlank events, step limits, error handling, and i Graphics tests check CPU-generated pixels, movement, edge clamping, opposite directions, colors, and reset behavior. Video tests cover mirrors, byte-write rules, RGB555 layout, backdrop, forced blank, green swap, and unsupported modes. Input tests cover all 1,024 button combinations, read-only KEYINPUT behavior, keyboard mapping, and focus loss. +Keypad tests compare every button-state/selection pair with independent per-button OR/AND logic. +They check all KEYCNT values, byte merging, complete-word sampling, empty masks, and repeated request behavior. +Integration tests cover CPU/DMA writes, interrupt masks, HALT wake-up, ARM/Thumb IRQ return, and original BIOS IntrWait. +Failed-store tests check register, IRQ-history, CPU, and clock preservation. BIOS reset tests verify retained KEYCNT state. The bounded graphics frame runner is checked for instruction errors and timeouts. Timing tests cover every ROM wait-state setting, access width, ROM window, and 128 KiB boundary handling. Instruction timing tests cover arithmetic, shifts, branches, transfers, block operations, multiply, and exception returns.