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;
|
//! Platform-independent GBA button state. Bits are pressed-high here;
|
||||||
//! the KEYINPUT register exposes their active-low hardware representation.
|
//! the KEYINPUT register exposes their active-low hardware representation.
|
||||||
|
|
||||||
|
mod keypad;
|
||||||
|
pub(crate) use keypad::Keypad;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
#[repr(u16)]
|
#[repr(u16)]
|
||||||
pub enum Button {
|
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,
|
//! Timer phase resets on enable or clock-source changes. Hardware startup delays,
|
||||||
//! shared prescaler phase, audio events, and interrupt delivery delays are not modeled.
|
//! shared prescaler phase, audio events, and interrupt delivery delays are not modeled.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
display::{Display, DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
display::{Display, DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
||||||
dma::{Dma, DmaError, Transfer, DMA_BASE, DMA_END},
|
dma::{Dma, DmaError, Transfer, DMA_BASE, DMA_END},
|
||||||
input::Buttons,
|
input::{Buttons, Keypad},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const DISPCNT: u32 = 0x0400_0000;
|
pub const DISPCNT: u32 = 0x0400_0000;
|
||||||
|
|
@ -47,6 +47,7 @@ pub const BLDCNT: u32 = 0x0400_0050;
|
||||||
pub const BLDALPHA: u32 = 0x0400_0052;
|
pub const BLDALPHA: u32 = 0x0400_0052;
|
||||||
pub const BLDY: u32 = 0x0400_0054;
|
pub const BLDY: u32 = 0x0400_0054;
|
||||||
pub const KEYINPUT: u32 = 0x0400_0130;
|
pub const KEYINPUT: u32 = 0x0400_0130;
|
||||||
|
pub const KEYCNT: u32 = 0x0400_0132;
|
||||||
pub const TIMER_BASE: u32 = 0x0400_0100;
|
pub const TIMER_BASE: u32 = 0x0400_0100;
|
||||||
pub const IE: u32 = 0x0400_0200;
|
pub const IE: u32 = 0x0400_0200;
|
||||||
pub const IF: u32 = 0x0400_0202;
|
pub const IF: u32 = 0x0400_0202;
|
||||||
|
|
@ -137,7 +138,7 @@ pub(crate) struct Io {
|
||||||
effects: crate::video::effects::Effects,
|
effects: crate::video::effects::Effects,
|
||||||
window_edges: crate::video::windows::WindowEdges,
|
window_edges: crate::video::windows::WindowEdges,
|
||||||
horizontal_windows: crate::video::windows::horizontal::HorizontalWindows,
|
horizontal_windows: crate::video::windows::horizontal::HorizontalWindows,
|
||||||
buttons: Buttons,
|
keypad: Keypad,
|
||||||
display: Display,
|
display: Display,
|
||||||
dma: Dma,
|
dma: Dma,
|
||||||
postflg: bool,
|
postflg: bool,
|
||||||
|
|
@ -146,7 +147,7 @@ pub(crate) struct Io {
|
||||||
|
|
||||||
impl Io {
|
impl Io {
|
||||||
pub(crate) fn mapped(address: u32) -> bool {
|
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> {
|
pub(crate) fn read8(&self, address: u32) -> Option<u8> {
|
||||||
|
|
@ -165,7 +166,8 @@ impl Io {
|
||||||
WINOUT => self.effects.outside,
|
WINOUT => self.effects.outside,
|
||||||
BLDCNT => self.effects.control,
|
BLDCNT => self.effects.control,
|
||||||
BLDALPHA => self.effects.alpha,
|
BLDALPHA => self.effects.alpha,
|
||||||
KEYINPUT => self.buttons.keyinput(),
|
KEYINPUT => self.keypad.keyinput(),
|
||||||
|
KEYCNT => self.keypad.control(),
|
||||||
0x0400_0100..=0x0400_010e => {
|
0x0400_0100..=0x0400_010e => {
|
||||||
let timer = &self.timers[((address - TIMER_BASE) / 4) as usize];
|
let timer = &self.timers[((address - TIMER_BASE) / 4) as usize];
|
||||||
if address & 2 == 0 {
|
if address & 2 == 0 {
|
||||||
|
|
@ -188,8 +190,10 @@ impl Io {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The memory bus validates every byte before calling this method.
|
/// 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) {
|
pub(crate) fn write8(&mut self, address: u32, value: u8) {
|
||||||
debug_assert!(Self::mapped(address));
|
debug_assert!(Self::mapped(address));
|
||||||
|
debug_assert!(!(KEYINPUT..=KEYCNT + 1).contains(&address));
|
||||||
if (DMA_BASE..=DMA_END).contains(&address) {
|
if (DMA_BASE..=DMA_END).contains(&address) {
|
||||||
self.dma.write8(address, value);
|
self.dma.write8(address, value);
|
||||||
return;
|
return;
|
||||||
|
|
@ -198,7 +202,7 @@ impl Io {
|
||||||
DISPCNT => self.dispcnt = replace_byte(self.dispcnt, address, value) & 0xfff7,
|
DISPCNT => self.dispcnt = replace_byte(self.dispcnt, address, value) & 0xfff7,
|
||||||
GREENSWAP if address & 1 == 0 => self.greenswap = value & 1 != 0,
|
GREENSWAP if address & 1 == 0 => self.greenswap = value & 1 != 0,
|
||||||
DISPSTAT => self.pending |= self.display.write_status(address & 1 != 0, value),
|
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 => {
|
BG0CNT..=BG3CNT => {
|
||||||
let index = ((address - BG0CNT) / 2) as usize;
|
let index = ((address - BG0CNT) / 2) as usize;
|
||||||
let control = &mut self.backgrounds[index].control;
|
let control = &mut self.backgrounds[index].control;
|
||||||
|
|
@ -294,7 +298,30 @@ impl Io {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_buttons(&mut self, buttons: Buttons) {
|
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 {
|
pub(crate) fn display_control(&self) -> u16 {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use crate::{
|
||||||
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
display::{DisplayPosition, CYCLES_PER_LINE, HBLANK_START, VISIBLE_LINES},
|
||||||
dma::{DmaError, DMA_BASE, DMA_END},
|
dma::{DmaError, DMA_BASE, DMA_END},
|
||||||
input::Buttons,
|
input::Buttons,
|
||||||
io::{Io, HALTCNT, POSTFLG},
|
io::{Io, HALTCNT, KEYINPUT, POSTFLG},
|
||||||
timing::{bus_cycles, AccessKind, AccessWidth, DataTiming, StepTiming},
|
timing::{bus_cycles, AccessKind, AccessWidth, DataTiming, StepTiming},
|
||||||
video::{self, sprites::pipeline, Framebuffer, VideoError},
|
video::{self, sprites::pipeline, Framebuffer, VideoError},
|
||||||
};
|
};
|
||||||
|
|
@ -325,7 +325,8 @@ impl Memory {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the current pressed-button state. This does not advance time.
|
/// 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) {
|
pub fn set_buttons(&mut self, buttons: Buttons) {
|
||||||
self.io.set_buttons(buttons);
|
self.io.set_buttons(buttons);
|
||||||
}
|
}
|
||||||
|
|
@ -424,6 +425,10 @@ impl Memory {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if address >> 24 == 0x04 {
|
if address >> 24 == 0x04 {
|
||||||
|
if address & !3 == KEYINPUT {
|
||||||
|
self.io.write_keypad(address, &bytes);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
for (offset, byte) in bytes.into_iter().enumerate() {
|
for (offset, byte) in bytes.into_iter().enumerate() {
|
||||||
let byte_address = address + offset as u32;
|
let byte_address = address + offset as u32;
|
||||||
if matches!(byte_address, POSTFLG | HALTCNT) && !self.can_write_power_control() {
|
if matches!(byte_address, POSTFLG | HALTCNT) && !self.can_write_power_control() {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use gba_core::{
|
||||||
dma::{DmaError, DMA_BASE, DMA_STRIDE},
|
dma::{DmaError, DMA_BASE, DMA_STRIDE},
|
||||||
input::Buttons,
|
input::Buttons,
|
||||||
io::{
|
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,
|
WAITCNT, WININ, WINOUT,
|
||||||
},
|
},
|
||||||
memory::{OAM_START, PALETTE_START},
|
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);
|
let (mut m, pc) = prepare(flags, false, 0x9f);
|
||||||
m.memory_mut().set_buttons(Buttons::from_bits(0x251));
|
m.memory_mut().set_buttons(Buttons::from_bits(0x251));
|
||||||
let keys = m.memory().read16(KEYINPUT).unwrap();
|
let keys = m.memory().read16(KEYINPUT).unwrap();
|
||||||
|
m.memory_mut().write16(KEYCNT, 0x4001).unwrap();
|
||||||
m.memory_mut().write16(GREENSWAP, 1).unwrap();
|
m.memory_mut().write16(GREENSWAP, 1).unwrap();
|
||||||
for address in [
|
for address in [
|
||||||
BG0CNT,
|
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(DISPSTAT).unwrap() & 0xff38, 0);
|
||||||
assert_eq!(m.memory().read16(KEYINPUT).unwrap(), keys);
|
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(POSTFLG).unwrap(), 1);
|
||||||
assert_eq!(m.memory().read16(GREENSWAP).unwrap(), 1);
|
assert_eq!(m.memory().read16(GREENSWAP).unwrap(), 1);
|
||||||
assert!(m.cycles() > cycles);
|
assert!(m.cycles() > cycles);
|
||||||
|
|
|
||||||
|
|
@ -279,10 +279,7 @@ fn keyinput_exposes_all_ten_buttons_active_low_and_ignores_writes() {
|
||||||
memory.set_buttons(Buttons::default());
|
memory.set_buttons(Buttons::default());
|
||||||
assert_eq!(memory.read16(KEYINPUT).unwrap(), 0x3ff);
|
assert_eq!(memory.read16(KEYINPUT).unwrap(), 0x3ff);
|
||||||
assert_eq!(memory.cycles(), 0);
|
assert_eq!(memory.cycles(), 0);
|
||||||
assert_eq!(
|
assert_eq!(memory.read16(KEYINPUT + 2).unwrap(), 0); // KEYCNT resets disabled.
|
||||||
memory.read16(KEYINPUT + 2),
|
|
||||||
Err(MemoryError::Unmapped(KEYINPUT + 2))
|
|
||||||
); // KEYCNT deferred.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
498
crates/gba-core/tests/keypad.rs
Normal file
498
crates/gba-core/tests/keypad.rs
Normal file
|
|
@ -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<u8> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
@ -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/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/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.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/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/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 |
|
| `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/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/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/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/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/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 |
|
| `crates/gba-core/tests/affine_backgrounds.rs` | Affine registers, maps, transforms, bitmap sampling, composition, and CPU demo |
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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.
|
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.
|
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.
|
GREENSWAP, KEYINPUT/button state, KEYCNT, POSTFLG, and BIOS IRQ communication words are not reset.
|
||||||
Keypad interrupt control is not implemented. Bit 7 covers only the listed supported registers.
|
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.
|
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.
|
IRQ delivery stays CPU-masked during the call. The service restores the caller's mask on return.
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
| `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 |
|
| `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 |
|
| `0x04000200` | IE | Enables selected interrupt sources; bits 0–13 are writable |
|
||||||
| `0x04000202` | IF | Latches requests; writing 1 clears the corresponding bit |
|
| `0x04000202` | IF | Latches requests; writing 1 clears the corresponding bit |
|
||||||
| `0x04000204` | WAITCNT | Configures first/second ROM access wait states; unused bits read as zero |
|
| `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.
|
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.
|
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.
|
`--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`.
|
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.
|
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.
|
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
|
## HALT and power-control registers
|
||||||
|
|
||||||
Writing HALTCNT with bit 7 clear requests HALT. Bits 0–6 are ignored.
|
Writing HALTCNT with bit 7 clear requests HALT. Bits 0–6 are ignored.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
`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.
|
`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.
|
`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.
|
Video RAM starts at `0x06000000` and contains 96 KiB.
|
||||||
Its 128 KiB mirror layout repeats the final 32 KiB in offsets `0x18000..0x1ffff`.
|
Its 128 KiB mirror layout repeats the final 32 KiB in offsets `0x18000..0x1ffff`.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
- [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.
|
- [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.
|
- [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 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 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).
|
- [GBATEK GBA DMA transfers](https://problemkaputt.de/gbatek-gba-dma-transfers.htm).
|
||||||
|
|
|
||||||
|
|
@ -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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- Sixteen active 32-bit CPU registers, with ARM7 mode-specific register banks.
|
||||||
- User, System, Supervisor, IRQ, FIQ, Abort, and Undefined modes.
|
- 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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
5. Expand graphics, audio, cartridge loading, and saves before testing Emerald compatibility.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
Instruction timing tests cover arithmetic, shifts, branches, transfers, block operations, multiply, and exception returns.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue