diff --git a/crates/gba-core/src/io.rs b/crates/gba-core/src/io.rs index c28afc6..17e1467 100644 --- a/crates/gba-core/src/io.rs +++ b/crates/gba-core/src/io.rs @@ -1,5 +1,5 @@ //! 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 //! and interrupt delivery delays are not modeled. diff --git a/crates/gba-core/src/io/inactive.rs b/crates/gba-core/src/io/inactive.rs index d011def..8f16e99 100644 --- a/crates/gba-core/src/io/inactive.rs +++ b/crates/gba-core/src/io/inactive.rs @@ -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; @@ -32,7 +32,9 @@ impl Inactive { /// 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"), + // 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_0135 if value & 0xc0 == 0xc0 => Some("Joybus serial mode"), 0x0400_0135 if value & 0x81 == 0x81 => Some("GPIO serial interrupt enable"), @@ -82,7 +84,7 @@ impl Inactive { } match address & !1 { 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, RCNT => self.rcnt = replace_byte(self.rcnt, address, value) & 0xc1ff, diff --git a/crates/gba-core/tests/bios/ram_reset.rs b/crates/gba-core/tests/bios/ram_reset.rs index d4363a8..539aa93 100644 --- a/crates/gba-core/tests/bios/ram_reset.rs +++ b/crates/gba-core/tests/bios/ram_reset.rs @@ -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] fn caller_registers_flags_modes_and_interrupt_masks_are_preserved() { for thumb in [false, true] { diff --git a/crates/gba-core/tests/inactive_devices.rs b/crates/gba-core/tests/inactive_devices.rs index c515e12..2c28588 100644 --- a/crates/gba-core/tests/inactive_devices.rs +++ b/crates/gba-core/tests/inactive_devices.rs @@ -80,7 +80,7 @@ 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, 0x80, "serial transfer start"), + (SIOCNT, 0x81, "internally clocked serial transfer"), (SIOCNT + 1, 0x20, "multiplayer/UART"), (RCNT + 1, 0xc0, "Joybus serial mode"), (RCNT + 1, 0x81, "GPIO serial interrupt"), diff --git a/crates/gba-core/tests/serial_external.rs b/crates/gba-core/tests/serial_external.rs new file mode 100644 index 0000000..81e70ea --- /dev/null +++ b/crates/gba-core/tests/serial_external.rs @@ -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); +} diff --git a/docs/architecture.md b/docs/architecture.md index 39dcb0b..4893708 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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/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/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/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/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 | diff --git a/docs/hardware/serial.md b/docs/hardware/serial.md index d0f2b59..c73dc3b 100644 --- a/docs/hardware/serial.md +++ b/docs/hardware/serial.md @@ -1,7 +1,7 @@ -# Disconnected serial initialization subset +# Disconnected serial subset -The core supports idle normal-mode data registers and disconnected general-purpose pins. -It does not support link transfers, multiplayer, UART, Joybus communication, or external serial interrupts. +The core supports normal-mode data registers, disconnected external-clock waiting, and general-purpose pins. +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. ## 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. - `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. - 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. + The retained mask is `0x508b`. The disconnected serial input is high, except when a general-purpose output drives it low. + 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`. 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. @@ -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. 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 RegisterRamReset always clears the low halfword of SIODATA32, even without bit 5. 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. 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. 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 - [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. - [mGBA BIOS implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/bios.c): functional serial reset defaults, not copied source. diff --git a/docs/research/emerald-reset.md b/docs/research/emerald-reset.md index 71e2430..b9e50df 100644 --- a/docs/research/emerald-reset.md +++ b/docs/research/emerald-reset.md @@ -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. 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 -Steps: 426406; instructions: 424636; IRQ entries: 2; DMA units: 1768; HALT idle: 0 -Nominal cycles: 1416670 -PC=0x082e6e5c -unsupported serial transfer start: write 0x84 at 0x04000128 +Steps: 426523; instructions: 424753; IRQ entries: 2; DMA units: 1768; HALT idle: 0 +Nominal cycles: 1416980 +PC=0x082e6eb8 +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. @@ -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. The pinned Pong debug/release reports match. -The next investigation is the serial-transfer start at SIOCNT (`0x04000128`). -Check RCNT mode, clock selection, disconnected-input behavior, and completion/IRQ timing before implementing a transfer model. +The next requirement is internally clocked normal serial communication, including switching from a pending external-clock request. +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. -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. Sweep applies only to channel 1. Direct Sound has FIFO clocks and nominal DMA requests. diff --git a/docs/status.md b/docs/status.md index d78d003..fba89e2 100644 --- a/docs/status.md +++ b/docs/status.md @@ -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. 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. +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. 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. @@ -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. 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 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. 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.