feat(serial): retain disconnected external-clock requests

This commit is contained in:
0xrsydn 2026-10-07 03:58:04 +07:00
commit d329c76dc3
9 changed files with 299 additions and 22 deletions

View file

@ -1,5 +1,5 @@
//! GBA I/O subset: DMA, timers, interrupts, WAITCNT, display, keypad input, //! GBA I/O subset: DMA, timers, interrupts, WAITCNT, display, keypad input,
//! Direct Sound, and disconnected serial initialization. //! Direct Sound, and disconnected serial state with external-clock waiting.
//! Timers share a free-running prescaler phase. Hardware startup/write delays //! Timers share a free-running prescaler phase. Hardware startup/write delays
//! and interrupt delivery delays are not modeled. //! and interrupt delivery delays are not modeled.

View file

@ -1,4 +1,4 @@
//! Disconnected serial initialization state. Active transfers remain diagnostic. //! Disconnected serial state, including external-clock requests that cannot progress.
use super::replace_byte; use super::replace_byte;
@ -32,7 +32,9 @@ impl Inactive {
/// supplies the complete selection, independent of the previous low byte. /// supplies the complete selection, independent of the previous low byte.
pub(super) fn unsupported(address: u32, value: u8) -> Option<&'static str> { pub(super) fn unsupported(address: u32, value: u8) -> Option<&'static str> {
match address { match address {
SIOCNT if value & 0x80 != 0 => Some("serial transfer start"), // Clock source and start share the low byte. With no external
// clock edges, an external-clock request only latches start/busy.
SIOCNT if value & 0x81 == 0x81 => Some("internally clocked serial transfer"),
0x0400_0129 if value & 0x20 != 0 => Some("multiplayer/UART serial mode"), 0x0400_0129 if value & 0x20 != 0 => Some("multiplayer/UART serial mode"),
0x0400_0135 if value & 0xc0 == 0xc0 => Some("Joybus serial mode"), 0x0400_0135 if value & 0xc0 == 0xc0 => Some("Joybus serial mode"),
0x0400_0135 if value & 0x81 == 0x81 => Some("GPIO serial interrupt enable"), 0x0400_0135 if value & 0x81 == 0x81 => Some("GPIO serial interrupt enable"),
@ -82,7 +84,7 @@ impl Inactive {
} }
match address & !1 { match address & !1 {
SIOCNT => { SIOCNT => {
self.serial_control = replace_byte(self.serial_control, address, value) & 0x500b self.serial_control = replace_byte(self.serial_control, address, value) & 0x508b
} }
SIODATA8 if address & 1 == 0 => self.serial_send = value, SIODATA8 if address & 1 == 0 => self.serial_send = value,
RCNT => self.rcnt = replace_byte(self.rcnt, address, value) & 0xc1ff, RCNT => self.rcnt = replace_byte(self.rcnt, address, value) & 0xc1ff,

View file

@ -288,6 +288,28 @@ fn serial_sound_reset_flags_are_selective_and_all_devices_return() {
} }
} }
#[test]
fn serial_reset_cancels_an_external_clock_request_only_when_selected() {
use gba_core::io::{RCNT, SIOCNT};
for thumb in [false, true] {
for flags in [0, 0x20, 0x40, 0x80, 0xff] {
let (mut m, pc) = prepare(flags, thumb, 0x1f);
m.memory_mut().write16(RCNT, 0).unwrap();
m.memory_mut().write16(SIOCNT, 0x5080).unwrap();
complete(&mut m, pc);
assert_eq!(
m.memory().read16(SIOCNT).unwrap(),
if flags & 0x20 != 0 { 4 } else { 0x5084 }
);
assert_eq!(
m.memory().read8(RCNT + 1).unwrap(),
if flags & 0x20 != 0 { 0x80 } else { 0 }
);
assert_eq!(m.memory().read16(IF).unwrap() & 0x80, 0);
}
}
}
#[test] #[test]
fn caller_registers_flags_modes_and_interrupt_masks_are_preserved() { fn caller_registers_flags_modes_and_interrupt_masks_are_preserved() {
for thumb in [false, true] { for thumb in [false, true] {

View file

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

View file

@ -0,0 +1,225 @@
//! Original disconnected external-clock probes. No partner, clock edges, or completion is invented.
use gba_core::{
cpu::Cpu,
dma::DMA_BASE,
io::{HALTCNT, IE, IF, IME, RCNT, SIOCNT, SIODATA32, SIODATA8},
machine::{Machine, StepKind},
memory::{Memory, MemoryError, ROM_START},
};
fn memory() -> Memory {
Memory::new(0xeaff_fffe_u32.to_le_bytes().to_vec()).unwrap()
}
#[test]
fn every_low_control_byte_retains_external_busy_but_rejects_internal_start_atomically() {
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
);
}
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
}
}
}
}
#[test]
fn disconnected_external_requests_wait_at_both_widths_and_rates_until_software_cancels() {
let mut bus = memory();
bus.write16(IE, 0x80).unwrap();
bus.write16(IME, 1).unwrap();
for control in [0x4080, 0x4082, 0x5080, 0x5082] {
bus.write32(SIODATA32, 0x12345678).unwrap();
bus.write16(SIODATA8, 0xab).unwrap();
bus.write16(SIOCNT, control).unwrap();
for cycles in [1, 63, 64, 512, 2048, 1000000, u32::MAX] {
bus.advance_cycles(cycles);
assert_eq!(bus.read16(SIOCNT).unwrap(), control | 4);
assert_eq!(bus.read32(SIODATA32).unwrap(), 0x12345678);
assert_eq!(bus.read16(SIODATA8).unwrap(), 0xab);
assert_eq!(bus.read16(IF).unwrap() & 0x80, 0);
assert!(!bus.irq_pending());
}
// Rewriting start does not manufacture a clock edge or transfer completion.
bus.write16(SIOCNT, control).unwrap();
bus.write8(SIOCNT, 8).unwrap(); // Explicit software cancellation; SO high.
assert_eq!(bus.read16(SIOCNT).unwrap(), (control & 0x5000) | 12);
assert_eq!(bus.read32(SIODATA32).unwrap(), 0x12345678);
assert_eq!(bus.read16(SIODATA8).unwrap(), 0xab);
}
}
#[test]
fn gpio_selection_does_not_turn_a_retained_start_bit_into_a_transfer_or_irq() {
let mut bus = memory();
bus.write16(RCNT, 0x8040).unwrap(); // SI output low, GPIO IRQ disabled.
bus.write16(SIOCNT, 0x5080).unwrap();
bus.advance_cycles(1000000);
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5080);
bus.write16(RCNT, 0).unwrap(); // Disconnected normal SI is pulled high.
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);
bus.write16(SIOCNT, 0x5001).unwrap(); // Cancel first, then select internal clock while idle.
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5005);
}
#[test]
fn external_request_does_not_wake_halt_or_stop() {
for stop in [false, true] {
let mut bus = memory();
bus.write16(IE, 0x80).unwrap();
bus.write16(IME, 1).unwrap();
bus.write16(SIOCNT, 0x5080).unwrap();
bus.write8(HALTCNT, if stop { 0x80 } else { 0 }).unwrap();
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
for _ in 0..100 {
assert_eq!(
machine.step().unwrap(),
if stop {
StepKind::StopIdle
} else {
StepKind::HaltIdle
}
);
}
assert_eq!(machine.stopped(), stop);
assert_eq!(machine.halted(), !stop);
assert_eq!(machine.memory().read16(IF).unwrap() & 0x80, 0);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x5084);
assert_eq!(machine.cycles() == 0, stop);
}
}
fn prepared(thumb: bool, instruction: u32, value: u32) -> Machine {
let code = if thumb {
vec![
0xe59f0010, 0xe59f1010, 0xe28f2001, 0xe12fff12, 0xe7fe8001, 0xeafffffe, SIOCNT, value,
]
} else {
vec![
0xe59f0008,
0xe59f1008,
instruction,
0xeafffffe,
SIOCNT,
value,
]
};
let mut bus = Memory::new(code.into_iter().flat_map(u32::to_le_bytes).collect()).unwrap();
let mut cpu = Cpu::new(ROM_START);
for _ in 0..if thumb { 4 } else { 2 } {
cpu.step(&mut bus).unwrap();
}
bus.write16(SIOCNT, 0x1003).unwrap();
Machine::new(cpu, bus)
}
#[test]
fn arm_and_thumb_external_starts_complete_as_instructions_not_as_serial_transfers() {
for thumb in [false, true] {
for value in [0x5084, 0x5081] {
let mut machine = prepared(thumb, 0xe1c010b0, value); // STRH r1,[r0]
let cpu = machine.cpu().clone();
if value == 0x5084 {
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x5084);
assert!(machine.cycles() > 0);
for _ in 0..100 {
machine.step().unwrap();
}
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x5084);
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_eq!(machine.cpu(), &cpu);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x1007);
assert_eq!(machine.step(), Err(error));
}
}
}
}
#[test]
fn invalid_word_and_block_writes_do_not_leave_a_partial_external_start() {
let mut bus = memory();
bus.write16(SIOCNT, 0x5000).unwrap();
bus.write16(SIODATA8, 0x12).unwrap();
assert!(bus.write32(SIOCNT, 0x00ab2080).is_err());
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5004);
assert_eq!(bus.read16(SIODATA8).unwrap(), 0x12);
bus.write32(SIOCNT, 0x00ab5080).unwrap();
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5084);
assert_eq!(bus.read16(SIODATA8).unwrap(), 0xab);
let mut machine = prepared(false, 0xe8a00006, 0x00ab5080); // STMIA r0!,{r1,r2}; second word unmapped.
let before = machine.cpu().clone();
let error = machine.step().unwrap_err();
assert!(error.to_string().contains("unmapped memory at 0x0400012c"));
assert_eq!(machine.cpu(), &before);
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x1007);
assert_eq!(machine.memory().read16(SIODATA8).unwrap(), 0);
assert_eq!(machine.step(), Err(error));
}
#[test]
fn dma_start_preserves_serial_wait_and_reports_only_dma_completion() {
let mut bus = memory();
bus.write32(0x02000000, 0x00ab5081).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_eq!(machine.step(), Err(error));
assert_eq!(machine.cycles(), 0);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 4);
assert_eq!(machine.memory().read16(SIODATA8).unwrap(), 0);
machine
.memory_mut()
.write32(0x02000000, 0x00ab5080)
.unwrap();
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x5084);
assert_eq!(machine.memory().read16(SIODATA8).unwrap(), 0xab);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x100); // DMA IRQ, not serial IRQ.
machine.memory_mut().advance_cycles(1000000);
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x5084);
assert_eq!(machine.memory().read16(IF).unwrap(), 0x100);
}

View file

@ -65,9 +65,10 @@ The repository is a Cargo workspace with three crates:
| `crates/gba-core/tests/pulse_sound/channel2.rs` | Channel 2 mapping, register gaps, independent state, shared clocks, and sum-before-rounding mixing | | `crates/gba-core/tests/pulse_sound/channel2.rs` | Channel 2 mapping, register gaps, independent state, shared clocks, and sum-before-rounding mixing |
| `crates/gba-core/tests/direct_sound.rs` | FIFO lanes/reset, signed mixing, timer selection, DMA refill, and HALT/STOP | | `crates/gba-core/tests/direct_sound.rs` | FIFO lanes/reset, signed mixing, timer selection, DMA refill, and HALT/STOP |
| `crates/gba-core/src/memory/audio_step_tests.rs` | Audio access phases, shadow validation, and CPU/DMA failure rollback | | `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/src/io/inactive.rs` | Disconnected serial registers, external-clock waiting, and 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/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/tests/serial_control.rs` | RCNT mode gating, inactive bits, byte/halfword lanes, CPU/DMA diagnostics, and interrupt isolation |
| `crates/gba-core/tests/serial_external.rs` | External-clock waiting, cancellation, HALT/STOP, ARM/Thumb/DMA starts, and atomic diagnostics |
| `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/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/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 | | `crates/gba-core/src/display.rs` | Display clock, scanline status, comparison edges, and display IRQ events |

View file

@ -1,7 +1,7 @@
# Disconnected serial initialization subset # Disconnected serial subset
The core supports idle normal-mode data registers and disconnected general-purpose pins. The core supports normal-mode data registers, disconnected external-clock waiting, and general-purpose pins.
It does not support link transfers, multiplayer, UART, Joybus communication, or external serial interrupts. It does not support internally clocked transfers, external clock edges, multiplayer, UART, Joybus communication, or external serial interrupts.
Unsupported activity remains diagnostic rather than reporting a fabricated completion. Unsupported activity remains diagnostic rather than reporting a fabricated completion.
## Registers ## Registers
@ -9,8 +9,9 @@ Unsupported activity remains diagnostic rather than reporting a fabricated compl
- `SIODATA32` at `0x04000120` is a four-byte latch in the supported normal-mode subset. - `SIODATA32` at `0x04000120` is a four-byte latch in the supported normal-mode subset.
- `SIODATA8` at `0x0400012a` retains its low byte; its upper byte reads zero. - `SIODATA8` at `0x0400012a` retains its low byte; its upper byte reads zero.
- `SIOCNT` at `0x04000128` retains normal 8/32-bit mode, IRQ configuration, clock selection, and the output-data bit. - `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. The retained mask is `0x508b`. 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. Start bit 7 can latch when clock-source bit 0 is clear. Software clears bit 7 to cancel the request.
Setting both bits 7 and 0 returns an internally-clocked-transfer diagnostic. Multiplayer/UART mode bit 13 remains unsupported.
- `RCNT` at `0x04000134` retains mode bits, interrupt configuration, output latches, and pin directions with mask `0xc1ff`. - `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 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. With bit 15 set, bit 14 selects Joybus; otherwise bit 8 enables GPIO interrupts. Both active configurations remain diagnostic.
@ -25,12 +26,29 @@ GPIO means general-purpose input/output. UART means universal asynchronous recei
No external serial device is connected. Elapsed cycles alone do not change serial data or generate an IRQ. No external serial device is connected. Elapsed cycles alone do not change serial data or generate an IRQ.
Configuration that hardware accepts in other modes can still be rejected by this intentionally limited subset. Configuration that hardware accepts in other modes can still be rejected by this intentionally limited subset.
## External-clock waiting
In normal 8-bit or 32-bit mode, setting start with clock-source bit 0 clear requests externally clocked communication.
No device is connected, so no external clock edges arrive. Start remains set; data does not shift and no completion IRQ occurs.
The internal-rate selector, bit 1, does not supply clocks when bit 0 is clear.
CPU cycles, timer overflows, display events, and DMA activity cannot manufacture external serial clocks.
Repeated start writes do not complete the request. Software can cancel it by clearing start, including through BIOS serial reset.
General-purpose mode does not shift serial data. A retained external-clock start bit has no link-port effect there.
Returning to normal mode still cannot complete a transfer without external clock edges.
Selecting internal clock while start is set remains diagnostic, including after an external request.
This is a disconnected waiting model, not an implementation of a connected link or an external clock-input API.
Original tests cover both widths, rate settings, control lanes, cancellation, GPIO selection, HALT/STOP, and ARM/Thumb stores.
DMA tests distinguish its own completion IRQ from a serial IRQ. Invalid word/block stores cannot leave a partial start request.
## BIOS reset ## BIOS reset
RegisterRamReset always clears the low halfword of SIODATA32, even without bit 5. RegisterRamReset always clears the low halfword of SIODATA32, even without bit 5.
The upper halfword remains unchanged in this functional subset. The upper halfword remains unchanged in this functional subset.
With bit 5 selected, the firmware clears SIOCNT and SIODATA8, selects general-purpose inputs, and clears the supported Joybus reset registers. With bit 5 selected, the firmware clears SIOCNT and SIODATA8, selects general-purpose inputs, and clears the supported Joybus reset registers.
It uses ordinary ARM bus stores, not a host-side reset bypass. It uses ordinary ARM bus stores, not a host-side reset bypass.
Clearing SIOCNT cancels a pending external-clock request. Unselected serial reset leaves that request pending.
Original tests cover independent byte lanes, input pull-ups, output directions, reset flag selection, and explicit unsupported operations. 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. Whole-access and block-store validation prevent a rejected control value from committing preceding bytes or registers.
@ -41,7 +59,8 @@ Inactive configuration bits do not wake HALT. RCNT word accesses still reject th
## References ## References
- [GBATEK reset functions](https://problemkaputt.de/gbatek-bios-reset-functions.htm): reset flags, general-purpose selection, and the unconditional SIODATA32 side effect. - [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, control bits, and writable unused RCNT bits 8/14. - [GBATEK normal serial mode](https://problemkaputt.de/gbatek-sio-normal-mode.htm): data widths, input status, writable unused RCNT bits 8/14, and external-clock slave start/wait/timeout behavior.
- [mGBA serial implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/sio.c): compared normal-mode control and completion paths. Its no-driver scheduled completion does not establish disconnected external-clock hardware behavior; this core does not copy that fallback.
- [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. - [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. - [mGBA BIOS implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/bios.c): functional serial reset defaults, not copied source.

View file

@ -48,13 +48,18 @@ The original copy service still executes all its reads and writes. No ROM patch
Host writes remain strict. Reference-emulator comparisons and original tests support this bounded bus behavior. Host writes remain strict. Reference-emulator comparisons and original tests support this bounded bus behavior.
The trace stayed local; no game code or assets were added to the repository. The trace stayed local; no game code or assets were added to the repository.
Both runs pass the copy and now stop at an unsupported serial-transfer start: The copy correction reached a serial-transfer start at PC `0x082e6e5c`.
A local register trace found RCNT zero and a SIOCNT write of `0x5084`: 32-bit mode, IRQ enabled, external clock selected.
With no connected clock source, this request must wait rather than complete on elapsed CPU cycles.
The [serial subset](../hardware/serial.md#external-clock-waiting) now retains the request without shifting data or raising a serial IRQ.
Both runs pass that request and next stop when software selects internal clock while start remains set:
```text ```text
Steps: 426406; instructions: 424636; IRQ entries: 2; DMA units: 1768; HALT idle: 0 Steps: 426523; instructions: 424753; IRQ entries: 2; DMA units: 1768; HALT idle: 0
Nominal cycles: 1416670 Nominal cycles: 1416980
PC=0x082e6e5c PC=0x082e6eb8
unsupported serial transfer start: write 0x84 at 0x04000128 unsupported internally clocked serial transfer: write 0x85 at 0x04000128
``` ```
The native run captures five startup frames. These do not establish a working title screen or gameplay. The native run captures five startup frames. These do not establish a working title screen or gameplay.
@ -69,10 +74,11 @@ Workspace debug/release tests, strict Clippy, public ARM/Thumb/memory/BIOS check
Native ROM-window tests and graphics smoke modes pass on Darwin arm64. Native ROM-window tests and graphics smoke modes pass on Darwin arm64.
The pinned Pong debug/release reports match. The pinned Pong debug/release reports match.
The next investigation is the serial-transfer start at SIOCNT (`0x04000128`). The next requirement is internally clocked normal serial communication, including switching from a pending external-clock request.
Check RCNT mode, clock selection, disconnected-input behavior, and completion/IRQ timing before implementing a transfer model. Implement nominal shift periods, disconnected input sampling, completion, and IRQ behavior with deterministic tests.
Keep completion consistent with HALT, STOP, instruction timing, and diagnostic rollback.
Do not invent a connected partner or immediate completion to suppress the diagnostic. Do not invent a connected partner or immediate completion to suppress the diagnostic.
The corrected inactive RCNT write does not establish a need for GPIO interrupts. The external-clock waiting result does not establish linked gameplay or GPIO interrupt support.
Noise now has deterministic counter clocks and shares tested length/envelope logic with the independent pulse channels. 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. Sweep applies only to channel 1. Direct Sound has FIFO clocks and nominal DMA requests.

View file

@ -142,7 +142,8 @@ There is no host audio output or cartridge save support.
The [audio subset](hardware/audio.md) supports Direct Sound, both PSG pulse channels, noise channel 4, idle wave channel 3 state, and instantaneous digital mixing. The [audio subset](hardware/audio.md) supports Direct Sound, both PSG pulse channels, noise channel 4, idle wave channel 3 state, and instantaneous digital mixing.
It does not yet provide a continuous sample stream. Wave channel 3 playback and unverified sound-DMA pairings remain diagnostic. 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. 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. The [serial subset](hardware/serial.md) supports normal data, disconnected external-clock waiting, and general-purpose pins.
External requests retain start/busy without completion or IRQ. Internally clocked transfers and connected links remain unsupported.
RCNT retains inactive bits 8/14 in normal mode. Effective GPIO interrupt enable and Joybus selection remain diagnostic. 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. 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. Keypad IRQs use a documented polling model; hardware retrigger edges and asynchronous input timing remain unverified.
@ -155,7 +156,8 @@ Audio, saves, and external BIOS loading remain unavailable. ROM loading does not
A local Emerald run now completes its all-device reset and master sound enable. A local Emerald run now completes its all-device reset and master sound enable.
It also passes the pulse/noise triggers and normal-mode RCNT configuration. It also passes the pulse/noise triggers and normal-mode RCNT configuration.
It also completes a CpuSet call to address zero through the corrected ignored BIOS-write behavior. It also completes a CpuSet call to address zero through the corrected ignored BIOS-write behavior.
It next stops at an unsupported serial-transfer start (`0x04000128`, PC `0x082e6e5c`). It also accepts an external-clock serial request without inventing clock edges or completion.
It next stops when software selects internal serial clock (`0x04000128`, PC `0x082e6eb8`).
No title-screen, gameplay, or audible-game compatibility is established. 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 [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. See [cartridge loading](hardware/cartridge.md) for file limits, step budgets, and exit status.