feat(serial): model disconnected multiplayer configuration
This commit is contained in:
parent
0b36ef1e24
commit
c5e19baed4
14 changed files with 500 additions and 44 deletions
|
|
@ -7,7 +7,10 @@ mod serial;
|
||||||
use crate::audio::Audio;
|
use crate::audio::Audio;
|
||||||
pub use crate::audio::{FIFO_A, FIFO_B, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, SOUND_START, WAVE_RAM};
|
pub use crate::audio::{FIFO_A, FIFO_B, SOUNDBIAS, SOUNDCNT_H, SOUNDCNT_X, SOUND_START, WAVE_RAM};
|
||||||
pub(crate) use serial::Serial;
|
pub(crate) use serial::Serial;
|
||||||
pub use serial::{JOYCNT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8};
|
pub use serial::{
|
||||||
|
JOYCNT, JOY_RECV, JOY_TRANS, RCNT, SIOCNT, SIODATA32, SIODATA8, SIOMLT_SEND, SIOMULTI0,
|
||||||
|
SIOMULTI1, SIOMULTI2, SIOMULTI3,
|
||||||
|
};
|
||||||
mod timer_step;
|
mod timer_step;
|
||||||
pub(crate) use timer_step::TimerStep;
|
pub(crate) use timer_step::TimerStep;
|
||||||
use timer_step::STAGED_IRQ_MASK;
|
use timer_step::STAGED_IRQ_MASK;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
//! Disconnected normal serial shifter with nominal internal clocks.
|
//! Disconnected serial: normal shifting and idle multiplayer child configuration.
|
||||||
use super::replace_byte;
|
use super::replace_byte;
|
||||||
|
|
||||||
pub const SIODATA32: u32 = 0x0400_0120;
|
pub const SIODATA32: u32 = 0x0400_0120;
|
||||||
|
pub const SIOMULTI0: u32 = SIODATA32;
|
||||||
|
pub const SIOMULTI1: u32 = 0x0400_0122;
|
||||||
|
pub const SIOMULTI2: u32 = 0x0400_0124;
|
||||||
|
pub const SIOMULTI3: u32 = 0x0400_0126;
|
||||||
pub const SIOCNT: u32 = 0x0400_0128;
|
pub const SIOCNT: u32 = 0x0400_0128;
|
||||||
pub const SIODATA8: u32 = 0x0400_012a;
|
pub const SIODATA8: u32 = 0x0400_012a;
|
||||||
|
pub const SIOMLT_SEND: u32 = SIODATA8;
|
||||||
pub const RCNT: u32 = 0x0400_0134;
|
pub const RCNT: u32 = 0x0400_0134;
|
||||||
pub const JOYCNT: u32 = 0x0400_0140;
|
pub const JOYCNT: u32 = 0x0400_0140;
|
||||||
pub const JOY_RECV: u32 = 0x0400_0150;
|
pub const JOY_RECV: u32 = 0x0400_0150;
|
||||||
|
|
@ -11,8 +16,9 @@ pub const JOY_TRANS: u32 = 0x0400_0154;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(crate) struct Serial {
|
pub(crate) struct Serial {
|
||||||
data: u32,
|
data: u32, // Shared SIODATA32 / SIOMULTI0-1 lanes.
|
||||||
send: u8,
|
multi_extra: u32, // SIOMULTI2-3; no connected parent can update these.
|
||||||
|
send: u16, // Shared low byte; high byte is accessible only in multiplayer format.
|
||||||
control: u16,
|
control: u16,
|
||||||
rcnt: u16,
|
rcnt: u16,
|
||||||
bits_left: u8,
|
bits_left: u8,
|
||||||
|
|
@ -22,14 +28,14 @@ pub(crate) struct Serial {
|
||||||
impl Serial {
|
impl Serial {
|
||||||
pub(crate) fn mapped(address: u32) -> bool {
|
pub(crate) fn mapped(address: u32) -> bool {
|
||||||
matches!(address,
|
matches!(address,
|
||||||
SIODATA32..=0x0400_0123 | SIOCNT..=0x0400_012b |
|
SIODATA32..=0x0400_012b |
|
||||||
RCNT..=0x0400_0135 | JOYCNT..=0x0400_0141 |
|
RCNT..=0x0400_0135 | JOYCNT..=0x0400_0141 |
|
||||||
JOY_RECV..=0x0400_0157)
|
JOY_RECV..=0x0400_0157)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn unsupported(address: u32, value: u8) -> Option<&'static str> {
|
pub(crate) fn unsupported(address: u32, value: u8) -> Option<&'static str> {
|
||||||
match address {
|
match address {
|
||||||
0x0400_0129 if value & 0x20 != 0 => Some("multiplayer/UART serial mode"),
|
0x0400_0129 if value & 0x30 == 0x30 => Some("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"),
|
||||||
JOYCNT if value & 0x40 != 0 => Some("Joybus interrupt enable"),
|
JOYCNT if value & 0x40 != 0 => Some("Joybus interrupt enable"),
|
||||||
|
|
@ -38,8 +44,14 @@ impl Serial {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn gpio(&self) -> bool {
|
||||||
|
self.rcnt & 0x8000 != 0
|
||||||
|
}
|
||||||
|
fn multiplayer_format(&self) -> bool {
|
||||||
|
self.control & 0x3000 == 0x2000
|
||||||
|
}
|
||||||
fn normal(&self) -> bool {
|
fn normal(&self) -> bool {
|
||||||
self.rcnt & 0x8000 == 0
|
!self.gpio() && !self.multiplayer_format()
|
||||||
}
|
}
|
||||||
fn busy(&self) -> bool {
|
fn busy(&self) -> bool {
|
||||||
self.control & 0x80 != 0
|
self.control & 0x80 != 0
|
||||||
|
|
@ -72,9 +84,21 @@ impl Serial {
|
||||||
}
|
}
|
||||||
let mut next = *self;
|
let mut next = *self;
|
||||||
next.merge(address, bytes);
|
next.merge(address, bytes);
|
||||||
|
if !next.multiplayer_format()
|
||||||
|
&& (0..bytes.len())
|
||||||
|
.any(|offset| (SIOMULTI2..SIOCNT).contains(&(address + offset as u32)))
|
||||||
|
{
|
||||||
|
return Some((
|
||||||
|
address,
|
||||||
|
bytes[0],
|
||||||
|
"multiplayer receive registers outside multiplayer mode",
|
||||||
|
));
|
||||||
|
}
|
||||||
// Clearing start always permits cancellation/reconfiguration. Other
|
// Clearing start always permits cancellation/reconfiguration. Other
|
||||||
// live changes after a shifted bit lack an independently verified model.
|
// live changes after a shifted bit lack an independently verified model.
|
||||||
if self.normal() && self.busy() && next.busy() {
|
// Check the written start bit before mode-specific read-only masking.
|
||||||
|
// A high-byte mode change must not silently cancel a clocked normal transfer.
|
||||||
|
if self.normal() && self.busy() && self.control_value(address, bytes) & 0x80 != 0 {
|
||||||
let changed_mode = self.normal() != next.normal();
|
let changed_mode = self.normal() != next.normal();
|
||||||
let changed_clock = (self.control ^ next.control) & 0x1003 != 0;
|
let changed_clock = (self.control ^ next.control) & 0x1003 != 0;
|
||||||
let data_touched = (0..bytes.len()).any(|offset| {
|
let data_touched = (0..bytes.len()).any(|offset| {
|
||||||
|
|
@ -108,18 +132,33 @@ impl Serial {
|
||||||
if (SIODATA32..SIODATA32 + 4).contains(&address) {
|
if (SIODATA32..SIODATA32 + 4).contains(&address) {
|
||||||
return Some(self.data.to_le_bytes()[(address - SIODATA32) as usize]);
|
return Some(self.data.to_le_bytes()[(address - SIODATA32) as usize]);
|
||||||
}
|
}
|
||||||
|
if (SIOMULTI2..SIOCNT).contains(&address) {
|
||||||
|
return self
|
||||||
|
.multiplayer_format()
|
||||||
|
.then(|| self.multi_extra.to_le_bytes()[(address - SIOMULTI2) as usize]);
|
||||||
|
}
|
||||||
let value = match address & !1 {
|
let value = match address & !1 {
|
||||||
SIOCNT => {
|
SIOCNT => {
|
||||||
|
let pins = if self.gpio() { self.gpio_pins() } else { 15 };
|
||||||
|
// SI is pulled high without a cable. Multiplayer drives SD high while idle.
|
||||||
|
// ID is a deterministic zero placeholder until a transfer (none can occur here).
|
||||||
self.control
|
self.control
|
||||||
| if self.normal() {
|
| (pins & 4)
|
||||||
4
|
| if self.multiplayer_format() {
|
||||||
|
(pins & 2) << 2
|
||||||
} else {
|
} else {
|
||||||
self.gpio_pins() & 4
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SIODATA8 => u16::from(self.send),
|
SIODATA8 => {
|
||||||
|
if self.multiplayer_format() {
|
||||||
|
self.send
|
||||||
|
} else {
|
||||||
|
self.send & 255
|
||||||
|
}
|
||||||
|
}
|
||||||
RCNT => {
|
RCNT => {
|
||||||
if self.normal() && address & 1 == 0 {
|
if !self.gpio() && address & 1 == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
(self.rcnt & !15) | self.gpio_pins()
|
(self.rcnt & !15) | self.gpio_pins()
|
||||||
|
|
@ -130,16 +169,40 @@ impl Serial {
|
||||||
Some(value.to_le_bytes()[(address & 1) as usize])
|
Some(value.to_le_bytes()[(address & 1) as usize])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn control_value(&self, address: u32, bytes: &[u8]) -> u16 {
|
||||||
|
let mut control = self.control;
|
||||||
|
for (offset, &value) in bytes.iter().enumerate() {
|
||||||
|
let at = address + offset as u32;
|
||||||
|
if (SIOCNT..SIOCNT + 2).contains(&at) {
|
||||||
|
control = replace_byte(control, at, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
control
|
||||||
|
}
|
||||||
|
|
||||||
fn merge(&mut self, address: u32, bytes: &[u8]) {
|
fn merge(&mut self, address: u32, bytes: &[u8]) {
|
||||||
|
// Select the final format first: a word can change mode and write SEND together.
|
||||||
|
let control = self.control_value(address, bytes);
|
||||||
|
self.control = control
|
||||||
|
& if control & 0x3000 == 0x2000 {
|
||||||
|
0x6f03
|
||||||
|
} else {
|
||||||
|
0x508b
|
||||||
|
};
|
||||||
for (offset, &value) in bytes.iter().enumerate() {
|
for (offset, &value) in bytes.iter().enumerate() {
|
||||||
let at = address + offset as u32;
|
let at = address + offset as u32;
|
||||||
if (SIODATA32..SIODATA32 + 4).contains(&at) {
|
if (SIODATA32..SIODATA32 + 4).contains(&at) {
|
||||||
let shift = (at - SIODATA32) * 8;
|
let shift = (at - SIODATA32) * 8;
|
||||||
self.data = (self.data & !(255 << shift)) | (u32::from(value) << shift);
|
self.data = (self.data & !(255 << shift)) | (u32::from(value) << shift);
|
||||||
|
} else if (SIOMULTI2..SIOCNT).contains(&at) {
|
||||||
|
let shift = (at - SIOMULTI2) * 8;
|
||||||
|
self.multi_extra =
|
||||||
|
(self.multi_extra & !(255 << shift)) | (u32::from(value) << shift);
|
||||||
} else {
|
} else {
|
||||||
match at & !1 {
|
match at & !1 {
|
||||||
SIOCNT => self.control = replace_byte(self.control, at, value) & 0x508b,
|
SIODATA8 if self.multiplayer_format() || at & 1 == 0 => {
|
||||||
SIODATA8 if at & 1 == 0 => self.send = value,
|
self.send = replace_byte(self.send, at, value);
|
||||||
|
}
|
||||||
RCNT => self.rcnt = replace_byte(self.rcnt, at, value) & 0xc1ff,
|
RCNT => self.rcnt = replace_byte(self.rcnt, at, value) & 0xc1ff,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -179,7 +242,7 @@ impl Serial {
|
||||||
if self.width() == 32 {
|
if self.width() == 32 {
|
||||||
self.data = (self.data << 1) | 1;
|
self.data = (self.data << 1) | 1;
|
||||||
} else {
|
} else {
|
||||||
self.send = (self.send << 1) | 1;
|
self.send = (self.send & 0xff00) | u16::from(((self.send as u8) << 1) | 1);
|
||||||
}
|
}
|
||||||
self.bits_left -= 1;
|
self.bits_left -= 1;
|
||||||
if self.bits_left == 0 {
|
if self.bits_left == 0 {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ mod iwram_bus;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod iwram_history_tests;
|
mod iwram_history_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
mod multiplayer_step_tests;
|
||||||
|
#[cfg(test)]
|
||||||
mod prefetch_tests;
|
mod prefetch_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod serial_step_tests;
|
mod serial_step_tests;
|
||||||
|
|
|
||||||
115
crates/gba-core/src/memory/multiplayer_step_tests.rs
Normal file
115
crates/gba-core/src/memory/multiplayer_step_tests.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
//! Original multiplayer mode/alias checks at CPU bus phases and during preflight.
|
||||||
|
use super::{timing_event_tests::prepared, *};
|
||||||
|
use crate::{
|
||||||
|
io::{IF, SIOCNT, SIOMLT_SEND, SIOMULTI0, SIOMULTI2},
|
||||||
|
machine::Machine,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mode_change_uses_normal_completion_phase_before_read_only_busy_masking() {
|
||||||
|
for elapsed in [56, 57] {
|
||||||
|
let (cpu, mut memory) = prepared(false, &[0xe5c10000], &[(0, 0x20), (1, SIOCNT + 1)]); // STRB r0,[r1]
|
||||||
|
memory.write16(SIOCNT, 0x4083).unwrap();
|
||||||
|
memory.advance_cycles(elapsed);
|
||||||
|
let serial = memory.io.serial;
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
let cpu = machine.cpu().clone();
|
||||||
|
if elapsed == 57 {
|
||||||
|
machine.step().unwrap(); // Normal completion, then multiplayer selection at cycle 64.
|
||||||
|
assert_eq!(machine.cycles(), 64);
|
||||||
|
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x200f);
|
||||||
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0x80);
|
||||||
|
assert_eq!(machine.memory().io.serial.next_event_cycles(), None);
|
||||||
|
} else {
|
||||||
|
let error = machine.step().unwrap_err();
|
||||||
|
assert!(error.to_string().contains("serial reconfiguration"));
|
||||||
|
assert_eq!(machine.step(), Err(error));
|
||||||
|
assert_eq!(machine.cpu(), &cpu);
|
||||||
|
assert_eq!(machine.cycles(), 56);
|
||||||
|
assert_eq!(machine.memory().io.serial, serial);
|
||||||
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arm_block_store_validates_receive_aliases_and_later_mode_before_committing_any_word() {
|
||||||
|
for unsupported in [false, true] {
|
||||||
|
let (cpu, mut memory) = prepared(
|
||||||
|
false,
|
||||||
|
&[0xe8a0000e],
|
||||||
|
&[
|
||||||
|
(0, SIOMULTI0),
|
||||||
|
(1, 0x12345678),
|
||||||
|
(2, 0xabcdef90),
|
||||||
|
(3, if unsupported { 0x98763000 } else { 0x98762000 }),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
memory.write16(SIOCNT, 0x2000).unwrap();
|
||||||
|
let serial = memory.io.serial;
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
let cpu = machine.cpu().clone();
|
||||||
|
if unsupported {
|
||||||
|
let error = machine.step().unwrap_err();
|
||||||
|
assert!(error.to_string().contains("UART serial mode"));
|
||||||
|
assert_eq!(machine.step(), Err(error));
|
||||||
|
assert_eq!(machine.memory().io.serial, serial);
|
||||||
|
assert_eq!(machine.cpu(), &cpu);
|
||||||
|
assert_eq!(machine.cycles(), 0);
|
||||||
|
} else {
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(machine.cpu().registers()[0], SIOMULTI0 + 12);
|
||||||
|
assert_eq!(machine.memory().read32(SIOMULTI0).unwrap(), 0x12345678);
|
||||||
|
assert_eq!(machine.memory().read32(SIOMULTI2).unwrap(), 0xabcdef90);
|
||||||
|
assert_eq!(machine.memory().read16(SIOMLT_SEND).unwrap(), 0x9876);
|
||||||
|
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x200c);
|
||||||
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||||
|
assert_eq!(machine.last_timing().data_cycles, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arm_block_load_observes_all_multiplayer_lanes_from_staged_registers() {
|
||||||
|
let (cpu, mut memory) = prepared(false, &[0xe8b0000e], &[(0, SIOMULTI0)]); // LDMIA r0!,{r1-r3}
|
||||||
|
memory.write32(SIOCNT, 0x98766083).unwrap();
|
||||||
|
memory.write32(SIOMULTI0, 0x12345678).unwrap();
|
||||||
|
memory.write32(SIOMULTI2, 0xabcdef90).unwrap();
|
||||||
|
let mut machine = Machine::new(cpu, memory);
|
||||||
|
machine.step().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
&machine.cpu().registers()[1..4],
|
||||||
|
&[0x12345678, 0xabcdef90, 0x9876600f]
|
||||||
|
);
|
||||||
|
assert_eq!(machine.cpu().registers()[0], SIOMULTI0 + 12);
|
||||||
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shadow_mode_changes_control_later_access_validation_and_preserve_atomicity() {
|
||||||
|
let (_, mut memory) = prepared(false, &[0xe1a00000], &[]);
|
||||||
|
memory
|
||||||
|
.write_words(&[(SIOCNT, 0x2000), (SIOMULTI2, 0x12345678), (SIOCNT, 0)])
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
memory.read32(SIOMULTI2),
|
||||||
|
Err(MemoryError::Unmapped(SIOMULTI2))
|
||||||
|
);
|
||||||
|
memory.write16(SIOCNT, 0x2000).unwrap();
|
||||||
|
assert_eq!(memory.read32(SIOMULTI2).unwrap(), 0x12345678);
|
||||||
|
let serial = memory.io.serial;
|
||||||
|
assert!(memory
|
||||||
|
.write_words(&[(0x02000000, 0xabcd), (SIOCNT, 0), (SIOMULTI2, 0)])
|
||||||
|
.is_err());
|
||||||
|
assert_eq!(memory.io.serial, serial);
|
||||||
|
assert_eq!(memory.read32(0x02000000).unwrap(), 0);
|
||||||
|
// A staged read cannot fall back to committed multiplayer data after leaving the mode.
|
||||||
|
memory.begin_timer_step();
|
||||||
|
memory.write16(SIOCNT, 0).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
memory.read32(SIOMULTI2),
|
||||||
|
Err(MemoryError::Unmapped(SIOMULTI2))
|
||||||
|
);
|
||||||
|
memory.discard_timer_step();
|
||||||
|
assert_eq!(memory.read32(SIOMULTI2).unwrap(), 0x12345678);
|
||||||
|
}
|
||||||
|
|
@ -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 + 1, 0x20, "multiplayer/UART"),
|
(SIOCNT + 1, 0x30, "UART serial mode"),
|
||||||
(RCNT + 1, 0xc0, "Joybus serial mode"),
|
(RCNT + 1, 0xc0, "Joybus serial mode"),
|
||||||
(RCNT + 1, 0x81, "GPIO serial interrupt"),
|
(RCNT + 1, 0x81, "GPIO serial interrupt"),
|
||||||
(JOYCNT, 0x40, "Joybus interrupt"),
|
(JOYCNT, 0x40, "Joybus interrupt"),
|
||||||
|
|
@ -95,7 +95,7 @@ fn unsupported_operations_have_specific_retryable_diagnostics() {
|
||||||
assert!(bus.write16(RCNT, 0xc0aa).is_err());
|
assert!(bus.write16(RCNT, 0xc0aa).is_err());
|
||||||
assert_eq!(bus.read16(RCNT).unwrap(), 0x805f); // Low byte was not committed.
|
assert_eq!(bus.read16(RCNT).unwrap(), 0x805f); // Low byte was not committed.
|
||||||
bus.write16(SIOCNT, 0x4008).unwrap();
|
bus.write16(SIOCNT, 0x4008).unwrap();
|
||||||
assert!(bus.write32(SIOCNT, 0xaabb2080).is_err());
|
assert!(bus.write32(SIOCNT, 0xaabb3080).is_err());
|
||||||
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x400c);
|
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x400c);
|
||||||
assert_eq!(bus.read16(SIODATA8).unwrap(), 0);
|
assert_eq!(bus.read16(SIODATA8).unwrap(), 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ fn arm_and_thumb_external_starts_complete_as_instructions_not_as_serial_transfer
|
||||||
assert_eq!(machine.memory().read16(IF).unwrap() & 0x80, 0);
|
assert_eq!(machine.memory().read16(IF).unwrap() & 0x80, 0);
|
||||||
} else {
|
} else {
|
||||||
let error = machine.step().unwrap_err();
|
let error = machine.step().unwrap_err();
|
||||||
assert!(error.to_string().contains("multiplayer/UART serial mode"));
|
assert!(error.to_string().contains("UART serial mode"));
|
||||||
assert_eq!(machine.cpu(), &cpu);
|
assert_eq!(machine.cpu(), &cpu);
|
||||||
assert_eq!(machine.cycles(), 0);
|
assert_eq!(machine.cycles(), 0);
|
||||||
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x1007);
|
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x1007);
|
||||||
|
|
@ -161,7 +161,7 @@ fn invalid_word_and_block_writes_do_not_leave_a_partial_external_start() {
|
||||||
let mut bus = memory();
|
let mut bus = memory();
|
||||||
bus.write16(SIOCNT, 0x5000).unwrap();
|
bus.write16(SIOCNT, 0x5000).unwrap();
|
||||||
bus.write16(SIODATA8, 0x12).unwrap();
|
bus.write16(SIODATA8, 0x12).unwrap();
|
||||||
assert!(bus.write32(SIOCNT, 0x00ab2080).is_err());
|
assert!(bus.write32(SIOCNT, 0x00ab3080).is_err());
|
||||||
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5004);
|
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x5004);
|
||||||
assert_eq!(bus.read16(SIODATA8).unwrap(), 0x12);
|
assert_eq!(bus.read16(SIODATA8).unwrap(), 0x12);
|
||||||
bus.write32(SIOCNT, 0x00ab5080).unwrap();
|
bus.write32(SIOCNT, 0x00ab5080).unwrap();
|
||||||
|
|
@ -188,7 +188,7 @@ fn dma_start_preserves_serial_wait_and_reports_only_dma_completion() {
|
||||||
bus.write32(DMA_BASE + 8, 0xc4000001).unwrap();
|
bus.write32(DMA_BASE + 8, 0xc4000001).unwrap();
|
||||||
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
let mut machine = Machine::new(Cpu::new(ROM_START), bus);
|
||||||
let error = machine.step().unwrap_err();
|
let error = machine.step().unwrap_err();
|
||||||
assert!(error.to_string().contains("multiplayer/UART serial mode"));
|
assert!(error.to_string().contains("UART serial mode"));
|
||||||
assert_eq!(machine.step(), Err(error));
|
assert_eq!(machine.step(), Err(error));
|
||||||
assert_eq!(machine.cycles(), 0);
|
assert_eq!(machine.cycles(), 0);
|
||||||
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 4);
|
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 4);
|
||||||
|
|
|
||||||
207
crates/gba-core/tests/serial_multiplayer.rs
Normal file
207
crates/gba-core/tests/serial_multiplayer.rs
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
//! Original disconnected multiplayer register tests; no connected peer or completed transfer.
|
||||||
|
use gba_core::{
|
||||||
|
cpu::Cpu,
|
||||||
|
dma::DMA_BASE,
|
||||||
|
io::{
|
||||||
|
HALTCNT, IE, IF, IME, RCNT, SIOCNT, SIODATA32, SIODATA8, SIOMLT_SEND, SIOMULTI0, SIOMULTI1,
|
||||||
|
SIOMULTI2, SIOMULTI3,
|
||||||
|
},
|
||||||
|
machine::{Machine, StepKind},
|
||||||
|
memory::{Memory, MemoryError, ROM_START},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn memory() -> Memory {
|
||||||
|
Memory::new(0xeaff_fffe_u32.to_le_bytes().to_vec()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_multiplayer_control_values_mask_status_and_cannot_start_a_disconnected_child() {
|
||||||
|
let mut bus = memory();
|
||||||
|
bus.write16(IE, 0x80).unwrap();
|
||||||
|
bus.write16(IME, 1).unwrap();
|
||||||
|
for value in 0..=u16::MAX {
|
||||||
|
if value & 0x3000 != 0x2000 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bus.write16(SIOCNT, value).unwrap();
|
||||||
|
// SI high (child), SD high (local idle drive); no valid assigned ID yet.
|
||||||
|
assert_eq!(bus.read16(SIOCNT).unwrap(), (value & 0x6f03) | 12);
|
||||||
|
assert_eq!(bus.read8(SIOCNT).unwrap() & 0xf0, 0); // ID placeholder, no error or busy.
|
||||||
|
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||||
|
}
|
||||||
|
bus.advance_cycles(u32::MAX);
|
||||||
|
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||||
|
assert!(!bus.irq_pending());
|
||||||
|
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x6f0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn receive_latches_have_independent_lanes_and_do_not_become_received_peer_data() {
|
||||||
|
let mut bus = memory();
|
||||||
|
bus.write16(SIOCNT, 0x2000).unwrap();
|
||||||
|
for address in [SIOMULTI0, SIOMULTI1, SIOMULTI2, SIOMULTI3] {
|
||||||
|
assert_eq!(bus.read16(address).unwrap(), 0); // Synthetic reset latches, not completed receives.
|
||||||
|
}
|
||||||
|
bus.write32(SIOMULTI0, 0x12345678).unwrap();
|
||||||
|
bus.write32(SIOMULTI2, 0x9abcdef0).unwrap();
|
||||||
|
bus.write8(SIOMULTI1 + 1, 0x55).unwrap();
|
||||||
|
bus.write16(SIOMULTI3, 0xaabb).unwrap();
|
||||||
|
assert_eq!(bus.read16(SIOMULTI0).unwrap(), 0x5678);
|
||||||
|
assert_eq!(bus.read16(SIOMULTI1).unwrap(), 0x5534);
|
||||||
|
assert_eq!(bus.read32(SIOMULTI2).unwrap(), 0xaabbdef0);
|
||||||
|
bus.write16(SIOMLT_SEND, 0xcdef).unwrap();
|
||||||
|
for rate in 0..4 {
|
||||||
|
bus.write16(SIOCNT, 0x6080 | rate).unwrap(); // Start is read-only for this disconnected child.
|
||||||
|
bus.advance_cycles(1000000);
|
||||||
|
assert_eq!(bus.read32(SIOMULTI0).unwrap(), 0x55345678);
|
||||||
|
assert_eq!(bus.read32(SIOMULTI2).unwrap(), 0xaabbdef0);
|
||||||
|
assert_eq!(bus.read16(SIOMLT_SEND).unwrap(), 0xcdef);
|
||||||
|
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn send_width_and_receive_aliases_follow_the_selected_format() {
|
||||||
|
let mut bus = memory();
|
||||||
|
bus.write32(SIOCNT, 0xabcd2000).unwrap(); // Final multiplayer format applies to all SEND lanes.
|
||||||
|
assert_eq!(bus.read16(SIOMLT_SEND).unwrap(), 0xabcd);
|
||||||
|
bus.write8(SIOMLT_SEND, 0x12).unwrap();
|
||||||
|
bus.write8(SIOMLT_SEND + 1, 0x34).unwrap();
|
||||||
|
assert_eq!(bus.read16(SIOMLT_SEND).unwrap(), 0x3412);
|
||||||
|
bus.write32(SIOMULTI0, 0x12345678).unwrap();
|
||||||
|
bus.write32(SIOMULTI2, 0x9abcdef0).unwrap();
|
||||||
|
bus.write16(SIOCNT, 0x1000).unwrap();
|
||||||
|
assert_eq!(bus.read32(SIODATA32).unwrap(), 0x12345678);
|
||||||
|
assert_eq!(bus.read16(SIODATA8).unwrap(), 0x12);
|
||||||
|
assert_eq!(bus.read32(SIOMULTI2), Err(MemoryError::Unmapped(SIOMULTI2)));
|
||||||
|
assert!(bus
|
||||||
|
.write32(SIOMULTI2, 0)
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("outside multiplayer mode"));
|
||||||
|
bus.write16(SIODATA8, 0xee56).unwrap(); // Existing normal mode ignores the upper write lane.
|
||||||
|
assert_eq!(bus.read16(SIODATA8).unwrap(), 0x56);
|
||||||
|
bus.write16(SIOCNT, 0x83).unwrap();
|
||||||
|
bus.advance_cycles(64); // Only the normal 8-bit register shifts.
|
||||||
|
bus.write16(SIOCNT, 0x2000).unwrap();
|
||||||
|
assert_eq!(bus.read16(SIOMLT_SEND).unwrap(), 0x34ff);
|
||||||
|
assert_eq!(bus.read32(SIOMULTI0).unwrap(), 0x12345678);
|
||||||
|
assert_eq!(bus.read32(SIOMULTI2).unwrap(), 0x9abcdef0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn idle_multiplayer_neither_wakes_halt_nor_advances_stop() {
|
||||||
|
for stop in [false, true] {
|
||||||
|
let mut bus = memory();
|
||||||
|
bus.write16(IE, 0x80).unwrap();
|
||||||
|
bus.write16(IME, 1).unwrap();
|
||||||
|
bus.write16(SIOCNT, 0x6083).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.halted(), !stop);
|
||||||
|
assert_eq!(machine.stopped(), stop);
|
||||||
|
assert_eq!(machine.cycles() == 0, stop);
|
||||||
|
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x600f);
|
||||||
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gpio_status_uses_pin_latches_but_cannot_create_a_multiplayer_parent_transfer() {
|
||||||
|
let mut bus = memory();
|
||||||
|
bus.write16(RCNT, 0x80f0).unwrap(); // All GPIO outputs low.
|
||||||
|
bus.write16(SIOCNT, 0x2083).unwrap();
|
||||||
|
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x2003);
|
||||||
|
bus.write16(RCNT, 0x80f6).unwrap(); // SI and SD high.
|
||||||
|
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x200f);
|
||||||
|
bus.write16(RCNT, 0).unwrap();
|
||||||
|
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x200f);
|
||||||
|
assert_eq!(bus.read8(RCNT), Err(MemoryError::Unmapped(RCNT))); // Do not guess SC pin samples.
|
||||||
|
bus.advance_cycles(1000000);
|
||||||
|
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn active_normal_mode_cannot_be_silently_cancelled_by_read_only_multiplayer_busy_masking() {
|
||||||
|
let mut bus = memory();
|
||||||
|
bus.write16(SIOCNT, 0x4083).unwrap();
|
||||||
|
bus.advance_cycles(8);
|
||||||
|
for (address, value) in [(SIOCNT + 1, 0x20), (SIOCNT + 1, 0x60)] {
|
||||||
|
assert!(bus
|
||||||
|
.write8(address, value)
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("serial reconfiguration"));
|
||||||
|
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x4087);
|
||||||
|
assert_eq!(bus.read8(SIODATA8).unwrap(), 1);
|
||||||
|
}
|
||||||
|
assert!(bus.write16(SIOCNT, 0x2083).is_err());
|
||||||
|
bus.write16(SIOCNT, 0x2000).unwrap(); // Explicitly clear start and select multiplayer together.
|
||||||
|
bus.advance_cycles(1000000);
|
||||||
|
assert_eq!(bus.read16(IF).unwrap(), 0);
|
||||||
|
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x200c);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uart_rejection_preserves_multiplayer_control_and_send_lanes() {
|
||||||
|
let mut bus = memory();
|
||||||
|
bus.write32(SIOCNT, 0xabcd6001).unwrap();
|
||||||
|
let error = bus.write32(SIOCNT, 0x12343083).unwrap_err();
|
||||||
|
assert!(error.to_string().contains("UART serial mode"));
|
||||||
|
assert_eq!(bus.write32(SIOCNT, 0x12343083), Err(error));
|
||||||
|
assert_eq!(bus.read16(SIOCNT).unwrap(), 0x600d);
|
||||||
|
assert_eq!(bus.read16(SIOMLT_SEND).unwrap(), 0xabcd);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn original_arm_and_thumb_word_stores_select_multiplayer_and_write_full_send_data() {
|
||||||
|
for thumb in [false, true] {
|
||||||
|
let code = if thumb {
|
||||||
|
vec![
|
||||||
|
0xe59f0010, 0xe59f1010, 0xe28f2001, 0xe12fff12, 0xe7fe6001, 0xeafffffe, SIOCNT,
|
||||||
|
0xabcd6083,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
0xe59f0008, 0xe59f1008, 0xe5801000, 0xeafffffe, SIOCNT, 0xabcd6083,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
let mut machine = Machine::new(cpu, bus);
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||||
|
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x600f);
|
||||||
|
assert_eq!(machine.memory().read16(SIOMLT_SEND).unwrap(), 0xabcd);
|
||||||
|
machine.memory_mut().advance_cycles(1000000);
|
||||||
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dma_mode_and_send_write_only_requests_its_own_completion_irq() {
|
||||||
|
let mut bus = memory();
|
||||||
|
bus.write32(0x02000000, 0xabcd6083).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);
|
||||||
|
assert_eq!(machine.step().unwrap(), StepKind::Dma { channel: 0 });
|
||||||
|
assert_eq!(machine.memory().read16(SIOCNT).unwrap(), 0x600f);
|
||||||
|
assert_eq!(machine.memory().read16(SIOMLT_SEND).unwrap(), 0xabcd);
|
||||||
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0x100);
|
||||||
|
machine.memory_mut().advance_cycles(1000000);
|
||||||
|
assert_eq!(machine.memory().read16(IF).unwrap(), 0x100);
|
||||||
|
}
|
||||||
|
|
@ -65,11 +65,13 @@ 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/serial.rs` | Disconnected normal serial shifter, internal clocks, external waiting, GPIO latches, and live-write validation |
|
| `crates/gba-core/src/io/serial.rs` | Disconnected normal serial shifter, internal clocks, external waiting, idle multiplayer registers, GPIO latches, and live-write 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/tests/serial_external.rs` | External-clock waiting, cancellation, HALT/STOP, ARM/Thumb/DMA starts, and atomic diagnostics |
|
||||||
| `crates/gba-core/tests/serial_internal.rs` | Nominal bit edges, widths/rates, cancellation, completion IRQs, HALT/STOP, and live-write limits |
|
| `crates/gba-core/tests/serial_internal.rs` | Nominal bit edges, widths/rates, cancellation, completion IRQs, HALT/STOP, and live-write limits |
|
||||||
|
| `crates/gba-core/tests/serial_multiplayer.rs` | Disconnected child status, receive/send lanes, mode aliases, ignored child start, HALT/STOP, and ARM/Thumb/DMA access |
|
||||||
|
| `crates/gba-core/src/memory/multiplayer_step_tests.rs` | Multiplayer mode changes at normal completion, block-transfer preflight, staged reads, and atomic diagnostics |
|
||||||
| `crates/gba-core/src/memory/serial_step_tests.rs` | Serial bus-phase observations, preflight validation, DMA, IF ordering, capture isolation, and rollback |
|
| `crates/gba-core/src/memory/serial_step_tests.rs` | Serial bus-phase observations, preflight validation, DMA, IF ordering, capture isolation, and rollback |
|
||||||
| `crates/gba-core/src/io/timer_step.rs` | Staged timer/audio/serial state, divider phase, timer/serial IF bits, and sound refill requests at ordered CPU/DMA bus phases |
|
| `crates/gba-core/src/io/timer_step.rs` | Staged timer/audio/serial state, divider phase, timer/serial 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 |
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,8 @@ Both modes call `bios::boot` with the loaded bytes.
|
||||||
The [original BIOS replacement](bios.md) executes its minimal boot before entering `0x08000000` in ARM System mode.
|
The [original BIOS replacement](bios.md) executes its minimal boot before entering `0x08000000` in ARM System mode.
|
||||||
There is no command-line option for external BIOS files.
|
There is no command-line option for external BIOS files.
|
||||||
Unsupported services remain diagnostics. RegisterRamReset now accepts sound/serial reset flags, including `r0=0xff`, within the initialization subset.
|
Unsupported services remain diagnostics. RegisterRamReset now accepts sound/serial reset flags, including `r0=0xff`, within the initialization subset.
|
||||||
Active sound and serial transfers still fail with device-specific diagnostics; accepting reset does not establish game compatibility.
|
Supported [sound](audio.md) and [serial](serial.md) behavior extends beyond reset, but remaining unsupported operations still produce diagnostics.
|
||||||
|
Accepting reset does not establish game compatibility.
|
||||||
|
|
||||||
## Terminal execution limits and results
|
## Terminal execution limits and results
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Disconnected serial subset
|
# Disconnected serial subset
|
||||||
|
|
||||||
The core supports disconnected normal-mode transfers, external-clock waiting, and general-purpose pins.
|
The core supports disconnected normal-mode transfers, external-clock waiting, idle multiplayer configuration, and general-purpose pins.
|
||||||
Internal clocks shift pulled-high input at nominal GBA rates. No link partner is emulated.
|
Internal clocks shift pulled-high input at nominal GBA rates. No link partner is emulated.
|
||||||
External clock edges, multiplayer, UART, Joybus communication, and GPIO interrupts remain unsupported.
|
External clock edges, connected multiplayer transfers, UART, Joybus communication, and GPIO interrupts remain unsupported.
|
||||||
Unsupported activity remains diagnostic rather than reporting a fabricated completion.
|
Unsupported activity remains diagnostic rather than reporting a fabricated completion.
|
||||||
|
|
||||||
## Registers
|
## Registers
|
||||||
|
|
@ -13,13 +13,13 @@ 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.
|
- `SIOCNT` at `0x04000128` retains normal 8/32-bit mode, IRQ configuration, clock selection, and the output-data bit.
|
||||||
The retained mask is `0x508b`. 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.
|
||||||
Start bit 7 requests a transfer. Bit 0 selects external or internal clock; bit 1 selects the internal rate.
|
Start bit 7 requests a transfer. Bit 0 selects external or internal clock; bit 1 selects the internal rate.
|
||||||
Completion or software cancellation clears start. Multiplayer/UART mode bit 13 remains unsupported.
|
Completion or software cancellation clears start. Multiplayer uses the separate format described below; UART remains diagnostic.
|
||||||
- `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.
|
||||||
In general-purpose mode, each input reads high through its pull-up. Each output reads its output latch.
|
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`.
|
`RCNT=0x8000` therefore reads back as `0x800f`, not `0x8000`.
|
||||||
Normal-mode low-byte pin readback remains unmapped rather than guessed.
|
Normal and multiplayer low-byte pin readback remains unmapped rather than guessed.
|
||||||
- `JOYCNT` permits clearing already-empty status flags. Joybus interrupt enable is unsupported.
|
- `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.
|
- `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.
|
Nonzero writes return an explicit error. Joybus status and data-transfer side effects are not implemented.
|
||||||
|
|
@ -95,6 +95,52 @@ The period arithmetic follows documented GBA rates. Activation phase, mid-transf
|
||||||
No external pin waveform, cable, connected partner, or serial-device protocol is implemented.
|
No external pin waveform, cable, connected partner, or serial-device protocol is implemented.
|
||||||
Original tests cover individual shifts, batching, cancellation, IRQ gating, HALT/STOP, bus phases, DMA, capture independence, and rollback.
|
Original tests cover individual shifts, batching, cancellation, IRQ gating, HALT/STOP, bus phases, DMA, capture independence, and rollback.
|
||||||
|
|
||||||
|
## Disconnected multiplayer configuration
|
||||||
|
|
||||||
|
With RCNT bit 15 clear, SIOCNT mode bits 12–13 equal to `2` select multiplayer format.
|
||||||
|
The retained control mask is `0x6f03`: baud selection, unused writable bits 8–11, mode, and IRQ enable.
|
||||||
|
This subset models an idle unit without a cable, not a parent with missing players.
|
||||||
|
|
||||||
|
- SI reads high in status bit 2, identifying a child rather than a parent.
|
||||||
|
- SD reads high in bit 3 because the local idle output is high. This does not establish connected peers.
|
||||||
|
- ID bits 4–5 read a deterministic zero placeholder. Hardware does not define the ID before a completed transfer.
|
||||||
|
Zero must not be interpreted as an assigned parent role; SI determines that role.
|
||||||
|
- Error bit 6 and busy bit 7 remain clear. No parent supplies the clock that starts a child transfer.
|
||||||
|
- Start is read-only for a child. Writing it cannot start a transfer, reset receive data, or request an IRQ.
|
||||||
|
|
||||||
|
All baud settings retain this idle behavior. Elapsed cycles, DMA, and repeated start writes cannot create a linked transfer.
|
||||||
|
No multiplayer event can wake HALT. STOP retains the configuration without advancing clocks.
|
||||||
|
When GPIO is selected, SI/SD status follows the existing GPIO pin model; multiplayer transfers still cannot run.
|
||||||
|
RCNT low-byte samples remain unmapped outside GPIO mode. Floating clock-pin timing is not guessed.
|
||||||
|
|
||||||
|
### Multiplayer data registers
|
||||||
|
|
||||||
|
`SIOMLT_SEND` at `0x0400012a` is a 16-bit latch in multiplayer format.
|
||||||
|
It shares its low byte with normal-mode SIODATA8. Normal upper-byte reads return zero and writes are ignored.
|
||||||
|
Normal 8-bit shifts change only the low byte. The hidden upper byte survives mode changes in this bounded model.
|
||||||
|
A word write to SIOCNT selects the final format before applying both SEND lanes.
|
||||||
|
|
||||||
|
`SIOMULTI0` through `SIOMULTI3` occupy halfwords at `0x04000120`, `0x04000122`, `0x04000124`, and `0x04000126`.
|
||||||
|
The first two alias SIODATA32. The last two are accessible only with multiplayer format selected, including under GPIO.
|
||||||
|
Byte, halfword, and word accesses preserve independent lanes.
|
||||||
|
Receive latches start at zero under the synthetic reset policy and retain data across mode changes.
|
||||||
|
Selecting multiplayer or writing child start does not replace them with `0xffff`, local SEND data, or invented peer data.
|
||||||
|
|
||||||
|
Receive writes follow GBATEK's R/W register description. The pinned mGBA implementation instead leaves these writes unhandled.
|
||||||
|
Physical receive-write behavior, reset values, and hidden upper-byte retention have not been independently measured.
|
||||||
|
These are bounded register-model choices, not evidence of successful reception or full firmware-reset accuracy.
|
||||||
|
|
||||||
|
### Validation and limits
|
||||||
|
|
||||||
|
Mode changes use staged serial state at the bus-completion phase.
|
||||||
|
A high-byte mode write cannot silently cancel a busy normal transfer through multiplayer's read-only start mask.
|
||||||
|
Clear start explicitly, or wait for normal completion. A completion IRQ already requested remains pending after mode selection.
|
||||||
|
Block-store preflight observes earlier mode writes and rejects later invalid accesses without committing preceding writes.
|
||||||
|
|
||||||
|
Original tests cover control masks, all baud selections, register lanes, aliases, GPIO, HALT/STOP, and ARM/Thumb/DMA stores.
|
||||||
|
Bus-phase tests cover normal completion, retryable diagnostics, block loads/stores, and mode-dependent staged reads.
|
||||||
|
There is no parent, cable topology, external clock input, multiplayer timeout, or linked-game compatibility claim.
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
@ -102,6 +148,8 @@ 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.
|
Clearing SIOCNT cancels a pending external-clock request. Unselected serial reset leaves that request pending.
|
||||||
|
The firmware writes SIODATA8 after selecting normal format. This does not clear the retained upper multiplayer SEND byte.
|
||||||
|
Full multiplayer reset side effects remain unverified; this increment does not extend the firmware reset sequence.
|
||||||
|
|
||||||
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.
|
||||||
|
|
@ -113,8 +161,10 @@ Inactive configuration bits do not wake HALT. RCNT word accesses still reject th
|
||||||
|
|
||||||
- [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, MSB-first shifting, rates, pulled-high SI, start/completion, IRQ selection, and external-clock waiting.
|
- [GBATEK normal serial mode](https://problemkaputt.de/gbatek-sio-normal-mode.htm): data widths, MSB-first shifting, rates, pulled-high SI, start/completion, IRQ selection, and external-clock waiting.
|
||||||
|
- [GBATEK multiplayer mode](https://problemkaputt.de/gbatek-sio-multi-player-mode.htm): child start restrictions, SI/SD status, undefined pre-transfer ID, register widths, and R/W receive registers.
|
||||||
|
- [LinkRawCable at c61bf351](https://github.com/afska/gba-link-connection/blob/c61bf351f68ad2d6e1c9d72d70e21bec19adfc0b/lib/LinkRawCable.hpp): public API usage for mode selection, SEND, receive words, and separate role/readiness/ID checks. Its disconnected-cable warning is not a pin-level measurement.
|
||||||
- [GBATEK mode summary](https://problemkaputt.de/gbatek-sio-control-registers-summary.htm): RCNT/SIOCNT mode selection.
|
- [GBATEK mode summary](https://problemkaputt.de/gbatek-sio-control-registers-summary.htm): RCNT/SIOCNT mode selection.
|
||||||
- [mGBA serial implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/sio.c): cross-checked normal transfer-duration arithmetic and completion IRQ handling. Its no-driver scheduled completion and zero receive data are not copied; they do not establish disconnected hardware behavior.
|
- [mGBA serial implementation at 3a5e34be](https://github.com/mgba-emu/mgba/blob/3a5e34be33dc7f8f707e5bc9db69e8a430046f21/src/gba/sio.c): cross-checked normal transfer-duration arithmetic and completion IRQ handling. Its no-driver scheduled completion and zero receive data are not copied; they do not establish disconnected hardware behavior. Multiplayer comparison covers SI/SD defaults, receive-write disagreement, and uncertain floating SC timing.
|
||||||
- [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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,13 +58,19 @@ The serial model now handles that unshifted transition and clocks normal 8/32-bi
|
||||||
It shifts disconnected high input, clears start at completion, and requests serial IRQs through the ordinary device transaction.
|
It shifts disconnected high input, clears start at completion, and requests serial IRQs through the ordinary device transaction.
|
||||||
No connected partner, fabricated peer data, or immediate completion was added.
|
No connected partner, fabricated peer data, or immediate completion was added.
|
||||||
|
|
||||||
Both runs now reach multiplayer mode selection:
|
Normal internal clocks then reached multiplayer selection (`SIOCNT=0x2000`, PC `0x082e4358`).
|
||||||
|
The disconnected multiplayer model now accepts that configuration and reports idle child status.
|
||||||
|
A child cannot start itself without a parent clock. No linked transfer, assigned player ID, or completion IRQ is fabricated.
|
||||||
|
The send register supports 16-bit data; receive latches do not become invented peer data.
|
||||||
|
See [the register model and evidence limits](../hardware/serial.md#disconnected-multiplayer-configuration).
|
||||||
|
|
||||||
|
Both terminal and native runs now reach a Game Pak GPIO control write:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Steps: 955967; instructions: 953287; IRQ entries: 80; DMA units: 2600; HALT idle: 0
|
Steps: 956055; instructions: 953375; IRQ entries: 80; DMA units: 2600; HALT idle: 0
|
||||||
Nominal cycles: 3514542
|
Nominal cycles: 3514932
|
||||||
PC=0x082e4358
|
PC=0x082e29f8
|
||||||
unsupported multiplayer/UART serial mode: write 0x20 at 0x04000129
|
read-only memory at 0x080000c8
|
||||||
```
|
```
|
||||||
|
|
||||||
The native run captures twelve startup frames. These do not establish a working title screen or gameplay.
|
The native run captures twelve startup frames. These do not establish a working title screen or gameplay.
|
||||||
|
|
@ -79,10 +85,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.
|
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 requirement is disconnected multiplayer-mode configuration and status, starting with SIOCNT mode value `0x2000`.
|
The next requirement is cartridge GPIO, starting with the control register at `0x080000c8`.
|
||||||
Research mode selection, absent-partner input levels, ready/error flags, and start behavior before extending the model.
|
Research the Game Pak data/direction/control registers and real-time clock protocol before implementing them.
|
||||||
Do not invent connected players or successful linked transfers to suppress the diagnostic.
|
Keep ordinary ROM writes read-only. Do not hide the failure by ignoring every cartridge write.
|
||||||
The normal-mode result does not establish linked gameplay or GPIO interrupt support.
|
This serial result does not establish linked gameplay or serial 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.
|
||||||
|
|
|
||||||
|
|
@ -142,9 +142,11 @@ 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 subset](hardware/serial.md) supports disconnected normal 8/32-bit transfers, external-clock waiting, and general-purpose pins.
|
The [serial subset](hardware/serial.md) supports disconnected normal 8/32-bit transfers, external-clock waiting, idle multiplayer configuration, and general-purpose pins.
|
||||||
Internal clocks shift high input at nominal rates, clear busy, and request completion IRQs. External requests retain busy without clock edges.
|
Internal clocks shift high input at nominal rates, clear busy, and request completion IRQs. External requests retain busy without clock edges.
|
||||||
Connected links, multiplayer/UART, Joybus, GPIO interrupts, and unverified mid-transfer reconfiguration remain unsupported.
|
Disconnected multiplayer exposes child/ready status and 16-bit send/receive registers without inventing transfers or completion IRQs.
|
||||||
|
Pre-transfer ID is a zero placeholder; receive-write behavior and reset/alias details lack physical-hardware validation.
|
||||||
|
Connected links, multiplayer transfers, UART, Joybus, GPIO interrupts, and unverified mid-transfer reconfiguration 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.
|
||||||
|
|
@ -158,7 +160,8 @@ 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 also accepts an external-clock serial request without inventing clock edges or completion.
|
It also accepts an external-clock serial request without inventing clock edges or completion.
|
||||||
It now also completes internally clocked normal transfers and next stops at multiplayer mode selection (`0x04000129`, PC `0x082e4358`).
|
It now also completes internally clocked normal transfers and selects idle multiplayer mode.
|
||||||
|
It next stops at a Game Pak GPIO control write (`0x080000c8`, PC `0x082e29f8`). Cartridge GPIO remains unimplemented.
|
||||||
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.
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,7 @@ They check errors for unsupported features and out-of-range fetches without chan
|
||||||
The CPU tile demo test verifies asset copies, every pixel, wrapping, opposite directions, reset, and VBlank synchronization.
|
The CPU tile demo test verifies asset copies, every pixel, wrapping, opposite directions, reset, and VBlank synchronization.
|
||||||
RegisterRamReset tests check full memory regions and all memory-flag combinations from ARM and Thumb.
|
RegisterRamReset tests check full memory regions and all memory-flag combinations from ARM and Thumb.
|
||||||
They verify forced blank, reserved flag bits, caller preservation, protected BIOS RAM, and selective serial/sound initialization.
|
They verify forced blank, reserved flag bits, caller preservation, protected BIOS RAM, and selective serial/sound initialization.
|
||||||
All-device reset returns from ARM and Thumb; active sound and serial operations remain separately tested diagnostics.
|
All-device reset returns from ARM and Thumb; supported sound/serial activity and remaining diagnostics have separate regressions.
|
||||||
Rendering tests check write-only display state, both identity matrices, and the effect of zeroed sprite attributes.
|
Rendering tests check write-only display state, both identity matrices, and the effect of zeroed sprite attributes.
|
||||||
Device tests check DMA address/count latches, stopped timer counters, cleared reloads, and IRQ acknowledgement.
|
Device tests check DMA address/count latches, stopped timer counters, cleared reloads, and IRQ acknowledgement.
|
||||||
Other checks cover partial clears, preserved input/device state, and callers whose own RAM code is erased.
|
Other checks cover partial clears, preserved input/device state, and callers whose own RAM code is erased.
|
||||||
|
|
@ -295,7 +295,10 @@ Timed and untimed execution are compared across all 65,536 Thumb encodings with
|
||||||
Normal serial tests cover both widths and internal rates, every bit edge, disconnected high input, batching, and completion IRQs.
|
Normal serial tests cover both widths and internal rates, every bit edge, disconnected high input, batching, and completion IRQs.
|
||||||
They check external-clock handoff, cancellation, phase retention, HALT bounds, STOP freeze/wake, and explicit live-write limits.
|
They check external-clock handoff, cancellation, phase retention, HALT bounds, STOP freeze/wake, and explicit live-write limits.
|
||||||
Bus-phase tests cover start/load timing, IF acknowledgement order, CPU/DMA rollback, block preflight, and capture independence.
|
Bus-phase tests cover start/load timing, IF acknowledgement order, CPU/DMA rollback, block preflight, and capture independence.
|
||||||
These tests validate the documented nominal serial model, not physical link-pin timing or multiplayer compatibility.
|
Disconnected multiplayer tests cover every control value in that format, all baud settings, send/receive lanes, and normal-mode aliases.
|
||||||
|
They verify idle child status, ignored start, no completion IRQ or HALT wake, GPIO sampling, and ARM/Thumb/DMA stores.
|
||||||
|
Staged tests check mode changes at normal completion, block loads/stores, rollback, and reads after leaving multiplayer format.
|
||||||
|
These tests validate the documented bounded serial model, not physical pin timing, receive-write behavior, or linked-game compatibility.
|
||||||
Timer/IRQ tests verify that changing WAITCNT changes when an interrupt is sampled.
|
Timer/IRQ tests verify that changing WAITCNT changes when an interrupt is sampled.
|
||||||
Timer tests cover all prescalers, reload changes, start/stop behavior, byte writes, large clock advances, and cascades.
|
Timer tests cover all prescalers, reload changes, start/stop behavior, byte writes, large clock advances, and cascades.
|
||||||
Ordered timer tests cover ARM/Thumb sampling, coherent byte lanes, block-transfer phases, reload/control ordering, and prescaler retention.
|
Ordered timer tests cover ARM/Thumb sampling, coherent byte lanes, block-transfer phases, reload/control ordering, and prescaler retention.
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,10 @@ The native Darwin window also starts and presents frames without a diagnostic.
|
||||||
This exposed and corrected missing firmware affine-scale initialization, without a game-specific patch.
|
This exposed and corrected missing firmware affine-scale initialization, without a game-specific patch.
|
||||||
Frame-based scenarios and the preparation adapter now make the result repeatable.
|
Frame-based scenarios and the preparation adapter now make the result repeatable.
|
||||||
A local Emerald run now passes its initial all-device reset, master sound enable, and pulse/noise triggers.
|
A local Emerald run now passes its initial all-device reset, master sound enable, and pulse/noise triggers.
|
||||||
It stops at GPIO serial interrupt control. The core has tested Direct Sound, both pulse channels, and noise channel 4.
|
It also passes normal serial transfers and idle multiplayer configuration, then stops at Game Pak GPIO control.
|
||||||
|
The core has tested Direct Sound, both pulse channels, and noise channel 4.
|
||||||
Wave channel 3, a continuous sample stream, and desktop audio output remain missing.
|
Wave channel 3, a continuous sample stream, and desktop audio output remain missing.
|
||||||
See [the runtime result](research/emerald-reset.md). Serial control-register semantics are the next demonstrated startup requirement.
|
See [the runtime result](research/emerald-reset.md). Cartridge GPIO and its real-time clock protocol are the next demonstrated startup investigation.
|
||||||
The separate save requirement remains cartridge SRAM, followed by safe host save persistence.
|
The separate save requirement remains cartridge SRAM, followed by safe host save persistence.
|
||||||
SRAM is byte-addressable battery-backed save memory. Flash and EEPROM remain separate protocols.
|
SRAM is byte-addressable battery-backed save memory. Flash and EEPROM remain separate protocols.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue