fix(serial): gate RCNT interrupt and Joybus controls by mode
This commit is contained in:
parent
8ae7b3cf72
commit
86f8663904
7 changed files with 203 additions and 20 deletions
|
|
@ -28,12 +28,14 @@ impl Inactive {
|
|||
|
||||
/// Value-dependent rejection is independent of device state, so batch
|
||||
/// stores can validate all values before committing any RAM/I/O writes.
|
||||
/// RCNT mode and IRQ-enable bits share the high byte. Each high-byte write
|
||||
/// supplies the complete selection, independent of the previous low byte.
|
||||
pub(super) fn unsupported(address: u32, value: u8) -> Option<&'static str> {
|
||||
match address {
|
||||
SIOCNT if value & 0x80 != 0 => Some("serial transfer start"),
|
||||
0x0400_0129 if value & 0x20 != 0 => Some("multiplayer/UART serial mode"),
|
||||
0x0400_0135 if value & 0x40 != 0 => Some("Joybus serial mode"),
|
||||
0x0400_0135 if value & 1 != 0 => Some("GPIO serial interrupt enable"),
|
||||
0x0400_0135 if value & 0xc0 == 0xc0 => Some("Joybus serial mode"),
|
||||
0x0400_0135 if value & 0x81 == 0x81 => Some("GPIO serial interrupt enable"),
|
||||
JOYCNT if value & 0x40 != 0 => Some("Joybus interrupt enable"),
|
||||
JOY_RECV..=0x0400_0157 if value != 0 => Some("Joybus data access beyond reset"),
|
||||
_ => None,
|
||||
|
|
@ -83,7 +85,7 @@ impl Inactive {
|
|||
self.serial_control = replace_byte(self.serial_control, address, value) & 0x500b
|
||||
}
|
||||
SIODATA8 if address & 1 == 0 => self.serial_send = value,
|
||||
RCNT => self.rcnt = replace_byte(self.rcnt, address, value) & 0x80ff,
|
||||
RCNT => self.rcnt = replace_byte(self.rcnt, address, value) & 0xc1ff,
|
||||
_ => {} // Unused/read-only bits have no effect.
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,8 +82,8 @@ fn unsupported_operations_have_specific_retryable_diagnostics() {
|
|||
for (address, value, description) in [
|
||||
(SIOCNT, 0x80, "serial transfer start"),
|
||||
(SIOCNT + 1, 0x20, "multiplayer/UART"),
|
||||
(RCNT + 1, 0x40, "Joybus serial mode"),
|
||||
(RCNT + 1, 1, "GPIO serial interrupt"),
|
||||
(RCNT + 1, 0xc0, "Joybus serial mode"),
|
||||
(RCNT + 1, 0x81, "GPIO serial interrupt"),
|
||||
(JOYCNT, 0x40, "Joybus interrupt"),
|
||||
(JOY_RECV, 1, "Joybus data access"),
|
||||
] {
|
||||
|
|
|
|||
167
crates/gba-core/tests/serial_control.rs
Normal file
167
crates/gba-core/tests/serial_control.rs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
//! Original mode-gating regressions. No link partner or transfer completion is modeled.
|
||||
use gba_core::{
|
||||
cpu::Cpu,
|
||||
dma::DMA_BASE,
|
||||
io::{HALTCNT, IE, IF, IME, RCNT, SIOCNT},
|
||||
machine::{Machine, StepKind},
|
||||
memory::{Memory, MemoryError, ROM_START},
|
||||
};
|
||||
|
||||
fn memory() -> Memory {
|
||||
Memory::new(0xeaff_fffe_u32.to_le_bytes().to_vec()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rcnt_high_byte_selection_is_independent_of_previous_mode_and_low_byte() {
|
||||
let mut bus = memory();
|
||||
for previous in [0x8000, 0x80f5, 0x0100, 0x4100] {
|
||||
for low in [0, 0x55, 0xff] {
|
||||
for high in 0..=u8::MAX {
|
||||
for halfword in [false, true] {
|
||||
bus.write16(RCNT, previous).unwrap();
|
||||
let before = (bus.read8(RCNT), bus.read8(RCNT + 1));
|
||||
let result = if halfword {
|
||||
bus.write16(RCNT, u16::from_le_bytes([low, high]))
|
||||
} else {
|
||||
bus.write8(RCNT + 1, high)
|
||||
};
|
||||
// Decode individual mode bits, separately from the implementation masks.
|
||||
let gpio_or_joybus = high & 0x80 != 0;
|
||||
let joybus = gpio_or_joybus && high & 0x40 != 0;
|
||||
let gpio_irq = gpio_or_joybus && high & 1 != 0;
|
||||
if joybus || gpio_irq {
|
||||
assert_eq!(
|
||||
result,
|
||||
Err(MemoryError::UnsupportedIo {
|
||||
address: RCNT + 1,
|
||||
value: high,
|
||||
operation: if joybus {
|
||||
"Joybus serial mode"
|
||||
} else {
|
||||
"GPIO serial interrupt enable"
|
||||
},
|
||||
})
|
||||
);
|
||||
assert_eq!((bus.read8(RCNT), bus.read8(RCNT + 1)), before);
|
||||
} else {
|
||||
result.unwrap();
|
||||
assert_eq!(bus.read8(RCNT + 1).unwrap(), high & 0xc1);
|
||||
if gpio_or_joybus {
|
||||
let latch = if halfword { low } else { previous as u8 };
|
||||
let outputs = latch >> 4;
|
||||
let pins = (latch & outputs | !outputs) & 15;
|
||||
assert_eq!(bus.read8(RCNT).unwrap(), (latch & 0xf0) | pins);
|
||||
} else {
|
||||
assert!(bus.read8(RCNT).is_err());
|
||||
}
|
||||
}
|
||||
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inactive_irq_and_joybus_bits_retain_latches_without_interrupts_or_halt_wake() {
|
||||
let mut bus = memory();
|
||||
bus.write16(IE, 0x80).unwrap();
|
||||
bus.write16(IME, 1).unwrap();
|
||||
bus.write16(SIOCNT, 0x5008).unwrap();
|
||||
bus.write16(RCNT, 0x80f5).unwrap(); // SI output high.
|
||||
bus.write8(HALTCNT, 0).unwrap();
|
||||
for high in [1, 0x41, 0x40, 0] {
|
||||
bus.write8(RCNT + 1, high).unwrap();
|
||||
bus.write8(RCNT, 0xf1).unwrap(); // SI latch low, but not in GPIO mode.
|
||||
bus.advance_cycles(1000000);
|
||||
assert_eq!(bus.read8(RCNT + 1).unwrap(), high);
|
||||
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x500c); // Serial input remains pulled high.
|
||||
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
|
||||
assert!(bus.halted());
|
||||
}
|
||||
bus.write8(RCNT + 1, 0x80).unwrap(); // GPIO, IRQ disabled; lower latches survived.
|
||||
assert_eq!(bus.read16(RCNT).unwrap(), 0x80f1);
|
||||
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5008); // GPIO SI output now drives low.
|
||||
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
|
||||
assert!(bus.halted());
|
||||
}
|
||||
|
||||
fn prepared_store(instruction: u32, address: u32, value: u32) -> Machine {
|
||||
let code = [
|
||||
0xe59f_0008, // LDR r0, destination
|
||||
0xe59f_1008, // LDR r1, value
|
||||
instruction,
|
||||
0xeaff_fffe,
|
||||
address,
|
||||
value,
|
||||
];
|
||||
let mut bus = Memory::new(code.into_iter().flat_map(u32::to_le_bytes).collect()).unwrap();
|
||||
bus.write16(RCNT, 0x80f5).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.step(&mut bus).unwrap();
|
||||
cpu.step(&mut bus).unwrap();
|
||||
Machine::new(cpu, bus)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_byte_and_halfword_writes_accept_inactive_bits_but_reject_active_modes_atomically() {
|
||||
for (instruction, address, shift) in [(0xe5c0_1000, RCNT + 1, 0), (0xe1c0_10b0, RCNT, 8)] {
|
||||
for high in [1, 0x41, 0x81, 0xc0, 0xc1] {
|
||||
let mut machine = prepared_store(instruction, address, high << shift);
|
||||
let cpu = machine.cpu().clone();
|
||||
if high & 0x80 == 0 {
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||
assert_eq!(machine.memory().read8(RCNT + 1).unwrap(), high as u8);
|
||||
assert!(machine.cycles() > 0);
|
||||
} else {
|
||||
let error = machine.step().unwrap_err();
|
||||
assert!(error.to_string().contains(if high & 0x40 != 0 {
|
||||
"Joybus serial mode"
|
||||
} else {
|
||||
"GPIO serial interrupt enable"
|
||||
}));
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
assert_eq!(machine.memory().read16(RCNT).unwrap(), 0x80f5);
|
||||
assert_eq!(machine.step(), Err(error));
|
||||
}
|
||||
assert_eq!(machine.memory().read16(IF).unwrap() & 0x80, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dma_halfword_rejection_is_retryable_then_inactive_configuration_completes() {
|
||||
for high in [0x81, 0xc0, 0xc1] {
|
||||
let mut bus = memory();
|
||||
bus.write16(RCNT, 0x80f5).unwrap();
|
||||
bus.write16(0x02000000, high << 8).unwrap();
|
||||
bus.write32(DMA_BASE, 0x02000000).unwrap();
|
||||
bus.write32(DMA_BASE + 4, RCNT).unwrap();
|
||||
bus.write32(DMA_BASE + 8, 0x80000001).unwrap(); // One halfword, immediate.
|
||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||
let cpu = machine.cpu().clone();
|
||||
let error = machine.step().unwrap_err();
|
||||
assert_eq!(machine.step(), Err(error));
|
||||
assert_eq!(machine.cpu(), &cpu);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
assert_eq!(machine.memory().read16(RCNT).unwrap(), 0x80f5);
|
||||
assert_eq!(machine.memory().read16(DMA_BASE + 10).unwrap(), 0x8000);
|
||||
machine.memory_mut().write16(0x02000000, 0x4100).unwrap();
|
||||
machine.step().unwrap();
|
||||
assert_eq!(machine.memory().read8(RCNT + 1).unwrap(), 0x41);
|
||||
assert_eq!(machine.memory().read16(DMA_BASE + 10).unwrap() & 0x8000, 0);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap() & 0x80, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rcnt_word_padding_remains_unmapped_without_partial_writes() {
|
||||
let mut bus = memory();
|
||||
bus.write16(RCNT, 0x80f5).unwrap();
|
||||
assert_eq!(
|
||||
bus.write32(RCNT, 0x4100),
|
||||
Err(MemoryError::Unmapped(RCNT + 2))
|
||||
);
|
||||
assert_eq!(bus.read16(RCNT).unwrap(), 0x80f5);
|
||||
}
|
||||
|
|
@ -66,6 +66,7 @@ The repository is a Cargo workspace with three crates:
|
|||
| `crates/gba-core/src/memory/audio_step_tests.rs` | Audio access phases, shadow validation, and CPU/DMA failure rollback |
|
||||
| `crates/gba-core/src/io/inactive.rs` | Disconnected serial initialization with explicit unsupported-activity validation |
|
||||
| `crates/gba-core/tests/inactive_devices.rs` | Device masks, wave RAM, GPIO pull-ups, serial data, and failed CPU/DMA write isolation |
|
||||
| `crates/gba-core/tests/serial_control.rs` | RCNT mode gating, inactive bits, byte/halfword lanes, CPU/DMA diagnostics, and interrupt isolation |
|
||||
| `crates/gba-core/src/io/timer_step.rs` | Staged timer/audio state, divider phase, timer IF bits, and sound refill requests at ordered CPU/DMA bus phases |
|
||||
| `crates/gba-core/src/memory/timer_step_tests.rs` | Timer access phases, coherent lanes, cascades, IF ordering, rollback, DMA, idle, and clock ownership |
|
||||
| `crates/gba-core/src/display.rs` | Display clock, scanline status, comparison edges, and display IRQ events |
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ Unsupported activity remains diagnostic rather than reporting a fabricated compl
|
|||
- `SIOCNT` at `0x04000128` retains normal 8/32-bit mode, IRQ configuration, clock selection, and the output-data bit.
|
||||
The retained mask is `0x500b`. The disconnected serial input is high, except when a general-purpose output drives it low.
|
||||
Transfer-start bit 7 and multiplayer/UART mode bit 13 return explicit errors.
|
||||
- `RCNT` at `0x04000134` retains general-purpose mode, output latches, and pin directions with mask `0x80ff`.
|
||||
- `RCNT` at `0x04000134` retains mode bits, interrupt configuration, output latches, and pin directions with mask `0xc1ff`.
|
||||
With bit 15 clear, bits 8 and 14 are writable latches without GPIO interrupt or Joybus effects.
|
||||
With bit 15 set, bit 14 selects Joybus; otherwise bit 8 enables GPIO interrupts. Both active configurations remain diagnostic.
|
||||
In general-purpose mode, each input reads high through its pull-up. Each output reads its output latch.
|
||||
`RCNT=0x8000` therefore reads back as `0x800f`, not `0x8000`.
|
||||
Joybus mode and GPIO interrupt enable return errors. Normal-mode low-byte pin readback remains unmapped rather than guessed.
|
||||
Normal-mode low-byte pin readback remains unmapped rather than guessed.
|
||||
- `JOYCNT` permits clearing already-empty status flags. Joybus interrupt enable is unsupported.
|
||||
- `JOY_RECV` and `JOY_TRANS` permit only zero writes and zero readback for reset initialization.
|
||||
Nonzero writes return an explicit error. Joybus status and data-transfer side effects are not implemented.
|
||||
|
|
@ -32,12 +34,15 @@ It uses ordinary ARM bus stores, not a host-side reset bypass.
|
|||
|
||||
Original tests cover independent byte lanes, input pull-ups, output directions, reset flag selection, and explicit unsupported operations.
|
||||
Whole-access and block-store validation prevent a rejected control value from committing preceding bytes or registers.
|
||||
RCNT selection bits share the high byte. Each high-byte write replaces the complete selection, so validation needs no retained-mode shadow.
|
||||
Mode-gating tests cover every high-byte value, previous modes, low-byte preservation, CPU stores, DMA retries, and unchanged interrupt state.
|
||||
Inactive configuration bits do not wake HALT. RCNT word accesses still reject the unmapped padding at `0x04000136`.
|
||||
|
||||
## References
|
||||
|
||||
- [GBATEK reset functions](https://problemkaputt.de/gbatek-bios-reset-functions.htm): reset flags, general-purpose selection, and the unconditional SIODATA32 side effect.
|
||||
- [GBATEK normal serial mode](https://problemkaputt.de/gbatek-sio-normal-mode.htm): data widths, input status, and control bits.
|
||||
- [GBATEK](https://mgba-emu.github.io/gbatek/): general-purpose pin directions and internal pull-ups.
|
||||
- [GBATEK normal serial mode](https://problemkaputt.de/gbatek-sio-normal-mode.htm): data widths, input status, control bits, and writable unused RCNT bits 8/14.
|
||||
- [GBATEK](https://mgba-emu.github.io/gbatek/): GBA general-purpose pin directions, internal pull-ups, SI falling-edge interrupts, and the SIO mode-selection table.
|
||||
- [mGBA BIOS implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/bios.c): functional serial reset defaults, not copied source.
|
||||
|
||||
Exact firmware write ordering and hardware pin timing remain unverified.
|
||||
|
|
|
|||
|
|
@ -33,13 +33,18 @@ The earlier reset-only implementation stopped at master sound enable (`0x0400008
|
|||
The Direct Sound increment then reached a PSG channel 1 trigger (`0x04000065`, PC `0x082e03bc`).
|
||||
Channel 1 support reached the channel 2 trigger (`0x0400006d`, PC `0x082e03c0`).
|
||||
Both pulse channels then reached the noise trigger (`0x0400007d`, PC `0x082e03c4`).
|
||||
With noise channel 4 implemented, both runs now stop at general-purpose input/output (GPIO) serial interrupt enable:
|
||||
Noise channel 4 support then reached an incorrectly rejected RCNT write: `0x0100` at `0x04000134`, PC `0x082e2aac`.
|
||||
RCNT bit 15 was clear, so bit 8 did not enable general-purpose input/output (GPIO) interrupts.
|
||||
GBATEK documents bits 8 and 14 as writable but unused in normal mode.
|
||||
The serial model now retains these inactive bits. Effective GPIO interrupt enable and Joybus selection still return explicit errors.
|
||||
|
||||
Both runs pass that write and now stop in the original BIOS replacement:
|
||||
|
||||
```text
|
||||
Steps: 395420; instructions: 393690; IRQ entries: 2; DMA units: 1728; HALT idle: 0
|
||||
Nominal cycles: 1328057
|
||||
PC=0x082e2aac
|
||||
unsupported GPIO serial interrupt enable: write 0x01 at 0x04000135
|
||||
Steps: 398019; instructions: 396289; IRQ entries: 2; DMA units: 1728; HALT idle: 0
|
||||
Nominal cycles: 1334271
|
||||
PC=0x00000378
|
||||
read-only memory at 0x00000000
|
||||
```
|
||||
|
||||
The native run captures five startup frames. These do not establish a working title screen or gameplay.
|
||||
|
|
@ -54,9 +59,10 @@ Workspace debug/release tests, strict Clippy, public ARM/Thumb/memory/BIOS check
|
|||
Native ROM-window tests and graphics smoke modes pass on Darwin arm64.
|
||||
The pinned Pong debug/release reports match.
|
||||
|
||||
The next observed requirement is [serial control-register behavior](../hardware/serial.md), specifically RCNT bit 8.
|
||||
Check mode gating and interrupt-enable semantics before changing the current rejection.
|
||||
This diagnostic alone does not establish that the game needs an active link transfer.
|
||||
The next investigation is the BIOS replacement's attempted write to address zero at PC `0x00000378`.
|
||||
Trace the service arguments and original firmware routine before deciding whether the caller or service behavior is wrong.
|
||||
The diagnostic alone does not establish the root cause. Do not make BIOS memory writable to bypass it.
|
||||
The corrected RCNT write does not establish a need for active link transfers or GPIO interrupts.
|
||||
|
||||
Noise now has deterministic counter clocks and shares tested length/envelope logic with the independent pulse channels.
|
||||
Sweep applies only to channel 1. Direct Sound has FIFO clocks and nominal DMA requests.
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ The [audio subset](hardware/audio.md) supports Direct Sound, both PSG pulse chan
|
|||
It does not yet provide a continuous sample stream. Wave channel 3 playback and unverified sound-DMA pairings remain diagnostic.
|
||||
PSG timing and signed mixing are nominal; hardware startup edges and analog behavior remain unverified.
|
||||
The [serial initialization subset](hardware/serial.md) supports idle normal data and disconnected general-purpose pins, not active transfers.
|
||||
RCNT retains inactive bits 8/14 in normal mode. Effective GPIO interrupt enable and Joybus selection remain diagnostic.
|
||||
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.
|
||||
STOP implements keypad wake only. Serial/Game Pak wake sources, oscillator restart delays, and exact wake timing remain unimplemented or unverified.
|
||||
|
|
@ -151,9 +152,10 @@ ROM files can run in bounded terminal mode or a window with the original BIOS re
|
|||
ROM windows have no demo-specific startup checks. Input is sampled between bounded execution slices.
|
||||
Audio, saves, and external BIOS loading remain unavailable. ROM loading does not establish commercial-game compatibility.
|
||||
A local Emerald run now completes its all-device reset and master sound enable.
|
||||
It also passes the pulse and noise triggers, then stops explicitly at GPIO serial interrupt enable (`0x04000135`).
|
||||
No title-screen, gameplay, or audible-game compatibility is established.
|
||||
Its captured startup frames do not establish title-screen or gameplay compatibility. See [the startup result](research/emerald-reset.md).
|
||||
It also passes the pulse/noise triggers and normal-mode RCNT configuration.
|
||||
It next stops on an attempted write to address zero in the BIOS replacement at PC `0x00000378`.
|
||||
The cause remains under investigation. No title-screen, gameplay, or audible-game compatibility is established.
|
||||
See [the startup result](research/emerald-reset.md) for the bounded run and next investigation.
|
||||
See [cartridge loading](hardware/cartridge.md) for file limits, step budgets, and exit status.
|
||||
The [ROM suite runner](rom-tests.md) has original regressions and a [pinned public ARM baseline](public-arm-tests.md).
|
||||
The pinned public ARM ROM passes its result checkpoint in debug and release builds.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue