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.
This commit is contained in:
parent
7d396fea35
commit
597b02881b
14 changed files with 710 additions and 21 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
88
crates/gba-core/src/input/keypad.rs
Normal file
88
crates/gba-core/src/input/keypad.rs
Normal file
|
|
@ -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<u16>,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<u8> {
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue