feat(serial): clock disconnected normal transfers and completion IRQs

This commit is contained in:
0xrsydn 2026-10-07 04:17:46 +07:00
commit 0b36ef1e24
17 changed files with 860 additions and 196 deletions

View file

@ -80,7 +80,6 @@ fn idle_normal_serial_data_and_control_have_distinct_widths() {
fn unsupported_operations_have_specific_retryable_diagnostics() {
let mut bus = memory();
for (address, value, description) in [
(SIOCNT, 0x81, "internally clocked serial transfer"),
(SIOCNT + 1, 0x20, "multiplayer/UART"),
(RCNT + 1, 0xc0, "Joybus serial mode"),
(RCNT + 1, 0x81, "GPIO serial interrupt"),

View file

@ -4,7 +4,7 @@ use gba_core::{
dma::DMA_BASE,
io::{HALTCNT, IE, IF, IME, RCNT, SIOCNT, SIODATA32, SIODATA8},
machine::{Machine, StepKind},
memory::{Memory, MemoryError, ROM_START},
memory::{Memory, ROM_START},
};
fn memory() -> Memory {
@ -12,36 +12,22 @@ fn memory() -> Memory {
}
#[test]
fn every_low_control_byte_retains_external_busy_but_rejects_internal_start_atomically() {
fn every_low_control_byte_retains_normal_control_bits_without_immediate_completion() {
let mut bus = memory();
for high in [0, 0x10, 0x40, 0x50] {
for previous in [0x0003, 0x1000, 0x4080, 0x5082] {
for low in 0..=u8::MAX {
bus.write16(SIOCNT, previous).unwrap();
let before = bus.read16(SIOCNT).unwrap();
let result = bus.write8(SIOCNT, low);
if low & 0x80 != 0 && low & 1 != 0 {
assert_eq!(
result,
Err(MemoryError::UnsupportedIo {
address: SIOCNT,
value: low,
operation: "internally clocked serial transfer",
})
);
assert_eq!(bus.read16(SIOCNT).unwrap(), before);
} else {
result.unwrap();
assert_eq!(
bus.read16(SIOCNT).unwrap(),
(previous & 0x5000) | u16::from(low & 0x8b) | 4
);
bus.write8(SIOCNT + 1, high).unwrap();
assert_eq!(
bus.read16(SIOCNT).unwrap(),
(u16::from(high) << 8) | u16::from(low & 0x8b) | 4
);
}
bus.write8(SIOCNT, low).unwrap();
assert_eq!(
bus.read16(SIOCNT).unwrap(),
(previous & 0x5000) | u16::from(low & 0x8b) | 4
);
bus.write8(SIOCNT + 1, high).unwrap();
assert_eq!(
bus.read16(SIOCNT).unwrap(),
(u16::from(high) << 8) | u16::from(low & 0x8b) | 4
);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
}
}
@ -85,9 +71,9 @@ fn gpio_selection_does_not_turn_a_retained_start_bit_into_a_transfer_or_irq() {
bus.advance_cycles(1000000);
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5084);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
// Clock-source changes with start still set remain explicit diagnostics.
assert!(bus.write8(SIOCNT, 0x81).is_err());
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5084);
// Selecting internal clock starts the pending request, but not instant completion.
bus.write8(SIOCNT, 0x81).unwrap();
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5085);
bus.write16(SIOCNT, 0x5001).unwrap(); // Cancel first, then select internal clock while idle.
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5005);
}
@ -146,7 +132,7 @@ fn prepared(thumb: bool, instruction: u32, value: u32) -> Machine {
#[test]
fn arm_and_thumb_external_starts_complete_as_instructions_not_as_serial_transfers() {
for thumb in [false, true] {
for value in [0x5084, 0x5081] {
for value in [0x5084, 0x7080] {
let mut machine = prepared(thumb, 0xe1c010b0, value); // STRH r1,[r0]
let cpu = machine.cpu().clone();
if value == 0x5084 {
@ -160,9 +146,7 @@ fn arm_and_thumb_external_starts_complete_as_instructions_not_as_serial_transfer
assert_eq!(machine.memory().read16(IF).unwrap() & 0x80, 0);
} else {
let error = machine.step().unwrap_err();
assert!(error
.to_string()
.contains("internally clocked serial transfer"));
assert!(error.to_string().contains("multiplayer/UART serial mode"));
assert_eq!(machine.cpu(), &cpu);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x1007);
@ -198,15 +182,13 @@ fn invalid_word_and_block_writes_do_not_leave_a_partial_external_start() {
#[test]
fn dma_start_preserves_serial_wait_and_reports_only_dma_completion() {
let mut bus = memory();
bus.write32(0x02000000, 0x00ab5081).unwrap();
bus.write32(0x02000000, 0x00ab7080).unwrap();
bus.write32(DMA_BASE, 0x02000000).unwrap();
bus.write32(DMA_BASE + 4, SIOCNT).unwrap();
bus.write32(DMA_BASE + 8, 0xc4000001).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
let error = machine.step().unwrap_err();
assert!(error
.to_string()
.contains("internally clocked serial transfer"));
assert!(error.to_string().contains("multiplayer/UART serial mode"));
assert_eq!(machine.step(), Err(error));
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 4);

View file

@ -0,0 +1,231 @@
//! Original nominal serial clocks and disconnected-input regressions.
use gba_core::{
cpu::Cpu,
input::Buttons,
io::{HALTCNT, IE, IF, IME, KEYCNT, RCNT, SIOCNT, SIODATA32, SIODATA8},
machine::{Machine, StepKind},
memory::{Memory, ROM_START},
};
fn memory() -> Memory {
Memory::new(0xeaff_fffe_u32.to_le_bytes().to_vec()).unwrap()
}
fn value(bus: &Memory, wide: bool) -> u32 {
if wide {
bus.read32(SIODATA32).unwrap()
} else {
u32::from(bus.read8(SIODATA8).unwrap())
}
}
#[test]
fn every_shift_edge_samples_high_and_completion_occurs_after_exactly_eight_or_thirty_two_bits() {
for wide in [false, true] {
for fast in [false, true] {
for irq in [false, true] {
for seed in [0, u32::MAX, 0x12345678, 0x80000001] {
let mut bus = memory();
bus.write32(SIODATA32, seed).unwrap();
bus.write8(SIODATA8, seed as u8).unwrap();
let control = 0x81
| if wide { 0x1000 } else { 0 }
| if fast { 2 } else { 0 }
| if irq { 0x4000 } else { 0 };
bus.write16(SIOCNT, control).unwrap();
let width = if wide { 32 } else { 8 };
let period = if fast { 8 } else { 64 };
let mut bits: Vec<bool> =
(0..width).rev().map(|bit| seed & (1 << bit) != 0).collect();
for index in 0..width {
let expected = bits
.iter()
.fold(0u32, |word, bit| (word << 1) | u32::from(*bit));
bus.advance_cycles(period - 1);
assert_eq!(value(&bus, wide), expected);
assert_ne!(bus.read16(SIOCNT).unwrap() & 0x80, 0);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
bus.advance_cycles(1);
bits.remove(0);
bits.push(true);
let expected = bits
.iter()
.fold(0u32, |word, bit| (word << 1) | u32::from(*bit));
assert_eq!(value(&bus, wide), expected);
assert_eq!(bus.read16(SIOCNT).unwrap() & 0x80 != 0, index + 1 < width);
}
assert_eq!(bus.read16(IF).unwrap() & 0x80, if irq { 0x80 } else { 0 });
assert_eq!(bus.read16(SIOCNT).unwrap(), (control & !0x80) | 4);
if wide {
assert_eq!(bus.read8(SIODATA8).unwrap(), seed as u8);
} else {
assert_eq!(bus.read32(SIODATA32).unwrap(), seed);
}
bus.write16(IF, 0x80).unwrap();
bus.advance_cycles(u32::MAX);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
}
}
}
}
}
#[test]
fn bulk_and_single_cycle_advancement_match_at_every_phase_and_large_batches() {
for control in [0x4081, 0x4083, 0x5081, 0x5083] {
for cycles in [0, 1, 7, 8, 63, 64, 65, 255, 256, 511, 512, 2047, 2048, 4096] {
let mut bulk = memory();
let mut single = memory();
for bus in [&mut bulk, &mut single] {
bus.write32(SIODATA32, 0x12345678).unwrap();
bus.write8(SIODATA8, 0x96).unwrap();
bus.write16(SIOCNT, control).unwrap();
}
bulk.advance_cycles(cycles);
for _ in 0..cycles {
single.advance_cycles(1);
}
for address in [SIOCNT, SIODATA8, IF] {
assert_eq!(bulk.read16(address), single.read16(address));
}
assert_eq!(bulk.read32(SIODATA32), single.read32(SIODATA32));
}
}
}
#[test]
fn external_to_internal_transition_starts_a_fresh_period_and_same_start_writes_preserve_phase() {
let mut bus = memory();
bus.write16(SIOCNT, 0x5080).unwrap();
bus.advance_cycles(1000000);
bus.write8(SIOCNT, 0x81).unwrap();
bus.advance_cycles(63);
assert_eq!(bus.read32(SIODATA32).unwrap(), 0);
bus.write16(SIOCNT, 0x5085).unwrap(); // Read/modify/write includes read-only SI.
bus.advance_cycles(1);
assert_eq!(bus.read32(SIODATA32).unwrap(), 1);
bus.advance_cycles(64 * 31 - 1);
assert_ne!(bus.read16(SIOCNT).unwrap() & 0x80, 0);
bus.advance_cycles(1);
assert_eq!(bus.read32(SIODATA32).unwrap(), u32::MAX);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0x80);
}
#[test]
fn cancellation_allows_reconfiguration_and_irq_enable_is_sampled_at_completion() {
let mut bus = memory();
bus.write16(SIOCNT, 0x81).unwrap();
bus.advance_cycles(65);
assert_eq!(bus.read8(SIODATA8).unwrap(), 1);
let before = bus.read16(SIOCNT).unwrap();
for (address, data) in [
(SIOCNT, 0x83),
(SIOCNT + 1, 0x10),
(SIODATA8, 0x42),
(RCNT + 1, 0x80),
] {
let error = bus.write8(address, data).unwrap_err();
assert!(error.to_string().contains("serial reconfiguration"));
assert_eq!(bus.read16(SIOCNT).unwrap(), before);
assert_eq!(bus.read8(SIODATA8).unwrap(), 1);
}
bus.write8(SIOCNT + 1, 0x40).unwrap(); // IRQ enable does not restart the current bit.
bus.advance_cycles(447);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0x80);
bus.write16(IF, 0x80).unwrap();
bus.write16(SIOCNT, 0x4083).unwrap();
bus.advance_cycles(8);
bus.write32(SIOCNT, 0x00aa0001).unwrap(); // Cancel and replace data atomically.
bus.advance_cycles(1000000);
assert_eq!(bus.read8(SIODATA8).unwrap(), 0xaa);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
bus.write16(SIOCNT, 0x4083).unwrap();
bus.advance_cycles(63);
bus.write8(SIOCNT + 1, 0).unwrap(); // Disable IRQ one cycle before completion.
bus.advance_cycles(1);
assert_eq!(bus.read8(SIODATA8).unwrap(), 0xff);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
}
#[test]
fn gpio_gates_the_clock_and_leaving_normal_mode_requires_cancellation() {
let mut bus = memory();
bus.write16(RCNT, 0x8000).unwrap();
bus.write16(SIOCNT, 0x4083).unwrap();
bus.advance_cycles(1000000);
assert_eq!(bus.read8(SIODATA8).unwrap(), 0);
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.write16(RCNT, 0).unwrap();
assert!(bus.write16(RCNT, 0x8000).is_err());
bus.advance_cycles(7);
assert_eq!(bus.read8(SIODATA8).unwrap(), 0);
bus.advance_cycles(1);
assert_eq!(bus.read8(SIODATA8).unwrap(), 1);
bus.write16(SIOCNT, 0).unwrap();
bus.write16(RCNT, 0x8000).unwrap();
bus.advance_cycles(1000000);
assert_eq!(bus.read8(SIODATA8).unwrap(), 1);
assert_eq!(bus.read16(IF).unwrap(), 0);
}
#[test]
fn halt_stops_at_serial_completion_and_delivery_obeys_ime() {
for ime in [0, 1] {
let mut bus = memory();
bus.write16(IE, 0x80).unwrap();
bus.write16(IME, ime).unwrap();
bus.write16(SIOCNT, 0x4083).unwrap();
bus.write8(HALTCNT, 0).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::HaltIdle);
assert_eq!(machine.cycles(), 64);
assert!(!machine.halted());
assert_eq!(machine.memory().read16(IF).unwrap(), 0x80);
assert_eq!(
machine.step().unwrap(),
if ime == 0 {
StepKind::Instruction
} else {
StepKind::IrqEntry
}
);
}
}
#[test]
fn serial_and_timer_requests_latch_and_acknowledge_independently() {
let mut bus = memory();
bus.write32(gba_core::io::TIMER_BASE, 0x00c0ffc0).unwrap();
bus.write16(SIOCNT, 0x4083).unwrap();
bus.advance_cycles(64);
assert_eq!(bus.read16(IF).unwrap(), 0x88);
bus.write16(IF, 0x80).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 8);
bus.write16(IF, 8).unwrap();
assert_eq!(bus.read16(IF).unwrap(), 0);
bus.write16(gba_core::io::TIMER_BASE + 2, 0).unwrap();
bus.write16(SIOCNT, 0x5081).unwrap();
bus.advance_cycles(u32::MAX);
assert_eq!(bus.read32(SIODATA32).unwrap(), u32::MAX);
assert_eq!(bus.read16(IF).unwrap(), 0x80);
}
#[test]
fn stop_freezes_a_partial_bit_until_keypad_wake() {
let mut bus = memory();
bus.write16(IE, 0x1080).unwrap();
bus.write16(KEYCNT, 0x4001).unwrap();
bus.write16(SIOCNT, 0x4083).unwrap();
bus.advance_cycles(63);
bus.write8(HALTCNT, 0x80).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
assert_eq!(machine.step().unwrap(), StepKind::StopIdle);
machine.memory_mut().advance_cycles(u32::MAX);
assert_eq!(machine.cycles(), 63);
assert_eq!(machine.memory().read8(SIODATA8).unwrap(), 0x7f);
assert_eq!(machine.memory().read16(IF).unwrap() & 0x80, 0);
machine.memory_mut().set_buttons(Buttons::from_bits(1));
assert!(!machine.stopped());
machine.memory_mut().advance_cycles(1);
assert_eq!(machine.memory().read8(SIODATA8).unwrap(), 0xff);
assert_eq!(machine.memory().read16(IF).unwrap() & 0x80, 0x80);
}