fix(cpu): support single-load writeback register aliases
Allow ARM LDR/LDRB/LDRH/LDRSB/LDRSH to share non-PC base and destination registers with writeback. Keep the loaded value after computing addresses from incoming operands. Preserve PC restrictions, unsupported user transfers, store behavior, and atomic memory diagnostics. Add original regressions for index modes, offsets, widths, alignment, sign extension, all active banks, timing, I/O/IRQ ordering, and failures. The unchanged pinned public ARM ROM progresses past tests 360 and 361, then fails at test 362 on an unmapped 0x80000000 read; no full-suite pass is claimed. Verified on Darwin arm64: debug/release workspace and core suites, Python adapter tests, Clippy, rustdoc, native ROM-window checks, and all ten demo smoke tests. Document the new behavior and next memory-access blocker.
This commit is contained in:
parent
a28b830b38
commit
ab625dc55d
11 changed files with 367 additions and 12 deletions
|
|
@ -20,6 +20,8 @@ mod exception_tests;
|
|||
#[cfg(test)]
|
||||
mod instruction_tests;
|
||||
#[cfg(test)]
|
||||
mod load_alias_tests;
|
||||
#[cfg(test)]
|
||||
mod status_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ impl Cpu {
|
|||
let byte = instruction & (1 << 22) != 0;
|
||||
let load = instruction & (1 << 20) != 0;
|
||||
if (!pre && write_bit) // LDRT/STRT access semantics are not implemented.
|
||||
|| (write_back && (base_register == 15 || (load && base_register == destination)))
|
||||
|| (write_back && base_register == 15)
|
||||
|| (byte && destination == 15)
|
||||
{
|
||||
return Err(self.unsupported(instruction));
|
||||
|
|
@ -186,6 +186,8 @@ impl Cpu {
|
|||
if write_back {
|
||||
self.registers[base_register] = adjusted;
|
||||
}
|
||||
// ARM7 load aliases keep the loaded value, not the updated base. All
|
||||
// operands and the effective address were captured before either write.
|
||||
if load && destination != 15 {
|
||||
self.registers[destination] = value;
|
||||
}
|
||||
|
|
@ -210,7 +212,7 @@ impl Cpu {
|
|||
|| (!load && kind != 1)
|
||||
|| (!pre && write_bit)
|
||||
|| destination == 15
|
||||
|| (write_back && (base_register == 15 || (load && base_register == destination)))
|
||||
|| (write_back && base_register == 15)
|
||||
{
|
||||
return Err(self.unsupported(instruction));
|
||||
}
|
||||
|
|
@ -240,6 +242,7 @@ impl Cpu {
|
|||
if write_back {
|
||||
self.registers[base_register] = adjusted;
|
||||
}
|
||||
// Halfword and signed load aliases also let the loaded value win.
|
||||
if load {
|
||||
self.registers[destination] = value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -504,7 +504,6 @@ fn invalid_or_unimplemented_encodings_do_not_change_state_or_memory() {
|
|||
for instruction in [
|
||||
0xe1a0_0f11, // Rs=pc (register shift)
|
||||
0xe3b0_f001, // status restore via MOVS pc
|
||||
0xe490_0004, // LDR r0,[r0],#4 (base/destination overlap)
|
||||
0xe5bf_1000, // LDR with PC writeback
|
||||
0xe4b0_1004, // LDRT user-mode transfer
|
||||
0xe5d0_f000, // LDRB pc
|
||||
|
|
|
|||
319
crates/gba-core/src/cpu/load_alias_tests.rs
Normal file
319
crates/gba-core/src/cpu/load_alias_tests.rs
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
//! Original regressions for ARM7 single-load base/destination writeback aliases.
|
||||
use super::*;
|
||||
use crate::{
|
||||
io::{IE, IF, IME, TIMER_BASE},
|
||||
machine::{Machine, MachineError, StepKind},
|
||||
memory::ROM_START,
|
||||
timing::{bus_cycles, AccessKind, AccessWidth},
|
||||
};
|
||||
|
||||
const RAM: u32 = 0x0200_0100;
|
||||
const DATA: u32 = 0xf233_80a5;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Kind {
|
||||
Word,
|
||||
Byte,
|
||||
Half,
|
||||
SignedByte,
|
||||
SignedHalf,
|
||||
}
|
||||
const KINDS: [Kind; 5] = [
|
||||
Kind::Word,
|
||||
Kind::Byte,
|
||||
Kind::Half,
|
||||
Kind::SignedByte,
|
||||
Kind::SignedHalf,
|
||||
];
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Index {
|
||||
Offset,
|
||||
Pre,
|
||||
Post,
|
||||
}
|
||||
|
||||
fn load(kind: Kind, index: Index, up: bool, register_offset: bool, register: u32) -> u32 {
|
||||
let p = u32::from(!matches!(index, Index::Post)) << 24;
|
||||
let w = u32::from(matches!(index, Index::Pre)) << 21;
|
||||
let common = 0xe010_0000 | p | w | u32::from(up) << 23 | register << 16 | register << 12;
|
||||
match kind {
|
||||
Kind::Word | Kind::Byte => {
|
||||
common
|
||||
| 0x0400_0000
|
||||
| u32::from(matches!(kind, Kind::Byte)) << 22
|
||||
| if register_offset { 0x0200_0101 } else { 4 }
|
||||
} // r1 LSL #2, or #4
|
||||
_ => {
|
||||
common
|
||||
| 0x90
|
||||
| match kind {
|
||||
Kind::Half => 0x20,
|
||||
Kind::SignedByte => 0x40,
|
||||
_ => 0x60,
|
||||
}
|
||||
| if register_offset { 1 } else { 0x0040_0004 }
|
||||
} // r1, or split immediate #4
|
||||
}
|
||||
}
|
||||
|
||||
fn memory(instruction: u32) -> Memory {
|
||||
let mut memory = Memory::new(instruction.to_le_bytes().to_vec()).unwrap();
|
||||
memory.write32(RAM, DATA).unwrap();
|
||||
memory
|
||||
}
|
||||
|
||||
// Independent byte-lane expectations for [a5,80,33,f2], including odd ARM7 loads.
|
||||
fn expected(kind: Kind, low: usize) -> u32 {
|
||||
(match kind {
|
||||
Kind::Word => [0xf233_80a5, 0xa5f2_3380, 0x80a5_f233, 0x3380_a5f2],
|
||||
Kind::Byte => [0xa5, 0x80, 0x33, 0xf2],
|
||||
Kind::Half => [0x80a5, 0xa500_0080, 0xf233, 0x3300_00f2],
|
||||
Kind::SignedByte => [0xffff_ffa5, 0xffff_ff80, 0x33, 0xffff_fff2],
|
||||
Kind::SignedHalf => [0xffff_80a5, 0xffff_ff80, 0xffff_f233, 0xffff_fff2],
|
||||
})[low]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_aliases_keep_loaded_values_for_all_widths_offsets_and_index_modes() {
|
||||
for kind in KINDS {
|
||||
for index in [Index::Offset, Index::Pre, Index::Post] {
|
||||
for up in [false, true] {
|
||||
for register_offset in [false, true] {
|
||||
for low in 0..4 {
|
||||
let address = RAM + low as u32;
|
||||
let base = match index {
|
||||
Index::Post => address,
|
||||
_ if up => address - 4,
|
||||
_ => address + 4,
|
||||
};
|
||||
let mut bus = memory(load(kind, index, up, register_offset, 0));
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.apply_status(0xf000_00df, Mode::System);
|
||||
cpu.registers[0] = base;
|
||||
cpu.registers[1] = if matches!(kind, Kind::Word | Kind::Byte) {
|
||||
1
|
||||
} else {
|
||||
4
|
||||
};
|
||||
let mut after = cpu.clone();
|
||||
after.registers[0] = expected(kind, low);
|
||||
after.registers[15] += 4;
|
||||
let timing = cpu.step_timed(&mut bus).unwrap();
|
||||
assert_eq!(cpu, after, "{kind:?} {index:?} up={up} register_offset={register_offset} low={low}");
|
||||
assert_eq!(bus.read32(RAM).unwrap(), DATA);
|
||||
assert_eq!(bus.cycles(), 0);
|
||||
assert_eq!(
|
||||
timing.code_cycles,
|
||||
bus_cycles(0, ROM_START, AccessWidth::Word, AccessKind::Sequential)
|
||||
);
|
||||
assert_eq!(
|
||||
timing.data_cycles,
|
||||
if matches!(kind, Kind::Word) { 6 } else { 3 }
|
||||
);
|
||||
assert_eq!(timing.internal_cycles, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_aliases_modify_only_the_active_register_in_every_cpu_bank() {
|
||||
let modes = [
|
||||
Mode::User,
|
||||
Mode::System,
|
||||
Mode::Fiq,
|
||||
Mode::Irq,
|
||||
Mode::Supervisor,
|
||||
Mode::Abort,
|
||||
Mode::Undefined,
|
||||
];
|
||||
for kind in KINDS {
|
||||
for mode in modes {
|
||||
for register in 0..15 {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
for bank in modes {
|
||||
cpu.switch_mode(bank);
|
||||
for r in 8..15 {
|
||||
cpu.registers[r] = 0x1000 * bank as u32 + r as u32;
|
||||
}
|
||||
}
|
||||
cpu.apply_status(0x9000_00c0 | mode as u32, mode);
|
||||
cpu.registers[register] = RAM - 4;
|
||||
let mut after = cpu.clone();
|
||||
after.registers[register] = expected(kind, 0);
|
||||
after.registers[15] += 4;
|
||||
cpu.step(&mut memory(load(
|
||||
kind,
|
||||
Index::Pre,
|
||||
true,
|
||||
false,
|
||||
register as u32,
|
||||
)))
|
||||
.unwrap();
|
||||
assert_eq!(cpu, after, "{kind:?} {mode:?} r{register}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_offsets_use_incoming_values_and_wrapping_address_arithmetic() {
|
||||
for kind in KINDS {
|
||||
let mut instruction = load(kind, Index::Pre, true, true, 0);
|
||||
if matches!(kind, Kind::Word | Kind::Byte) {
|
||||
instruction &= !0x100;
|
||||
} // unshifted r1
|
||||
let mut bus = memory(instruction);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = 0xffff_fffc;
|
||||
cpu.registers[1] = RAM + 4;
|
||||
cpu.step(&mut bus).unwrap();
|
||||
assert_eq!(cpu.registers[0], expected(kind, 0));
|
||||
assert_eq!(cpu.registers[1], RAM + 4);
|
||||
}
|
||||
// All three registers alias: address uses old r0 + (old r0 LSR #24).
|
||||
let mut bus = memory(0xe7b0_0c20);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM;
|
||||
cpu.step(&mut bus).unwrap();
|
||||
assert_eq!(cpu.registers[0], expected(Kind::Word, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_index_does_not_read_or_validate_the_discarded_writeback_address() {
|
||||
for kind in KINDS {
|
||||
let mut instruction = load(kind, Index::Post, true, true, 0);
|
||||
if matches!(kind, Kind::Word | Kind::Byte) {
|
||||
instruction &= !0x100;
|
||||
}
|
||||
let mut bus = memory(instruction);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM;
|
||||
cpu.registers[1] = 0x7e00_0000; // adjusted address is unmapped 0x80000100
|
||||
cpu.step(&mut bus).unwrap();
|
||||
assert_eq!(cpu.registers[0], expected(kind, 0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_alias_reads_preserve_register_banks_device_state_and_clock() {
|
||||
for kind in KINDS {
|
||||
for index in [Index::Pre, Index::Post] {
|
||||
let mut bus = memory(load(kind, index, true, false, 0));
|
||||
bus.write32(TIMER_BASE, 0x00c0_fffe).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.apply_status(0x9000_00d1, Mode::Fiq);
|
||||
cpu.registers[0] = 0x0100_0000;
|
||||
let before = cpu.clone();
|
||||
let display = bus.display_position();
|
||||
let counter = bus.read16(TIMER_BASE).unwrap();
|
||||
let mut machine = Machine::new(cpu, bus);
|
||||
let address = if matches!(index, Index::Pre) {
|
||||
0x0100_0004
|
||||
} else {
|
||||
0x0100_0000
|
||||
};
|
||||
assert_eq!(
|
||||
machine.step(),
|
||||
Err(MachineError::Cpu(CpuError::Memory(MemoryError::Unmapped(
|
||||
address
|
||||
))))
|
||||
);
|
||||
assert_eq!(machine.cpu(), &before);
|
||||
assert_eq!(machine.cycles(), 0);
|
||||
assert_eq!(machine.last_timing().total(), 0);
|
||||
assert_eq!(machine.memory().display_position(), display);
|
||||
assert_eq!(machine.memory().read16(TIMER_BASE).unwrap(), counter);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 0);
|
||||
assert_eq!(machine.memory().read32(RAM).unwrap(), DATA);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_io_load_reads_before_device_progress_and_irq_delivery() {
|
||||
let mut bus = memory(0xe5b0_0004); // LDR r0,[r0,#4]!
|
||||
bus.write32(TIMER_BASE, 0x00c0_fffa).unwrap();
|
||||
bus.write16(IE, 8).unwrap();
|
||||
bus.write16(IME, 1).unwrap();
|
||||
let expected = bus.read32(TIMER_BASE).unwrap();
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = TIMER_BASE - 4;
|
||||
let mut machine = Machine::new(cpu, bus);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::Instruction);
|
||||
assert_eq!(machine.cpu().registers()[0], expected);
|
||||
assert_eq!(machine.cpu().pc(), ROM_START + 4);
|
||||
assert_eq!(machine.cycles(), 10); // ROM boundary: 8 code + 1 I/O data + 1 internal
|
||||
assert_ne!(machine.memory().read32(TIMER_BASE).unwrap(), expected);
|
||||
assert_eq!(machine.memory().read16(IF).unwrap(), 8);
|
||||
assert_eq!(machine.step().unwrap(), StepKind::IrqEntry);
|
||||
assert_eq!(machine.cpu().registers()[0], expected);
|
||||
assert_eq!(machine.cpu().pc(), 0x18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skipped_alias_loads_do_not_access_memory_or_apply_writeback() {
|
||||
for kind in KINDS {
|
||||
for index in [Index::Pre, Index::Post] {
|
||||
let mut bus = memory(load(kind, index, true, false, 0) & 0x0fff_ffff); // EQ, Z clear
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = 0x0100_0000;
|
||||
let mut after = cpu.clone();
|
||||
after.registers[15] += 4;
|
||||
let timing = cpu.step_timed(&mut bus).unwrap();
|
||||
assert_eq!(cpu, after);
|
||||
assert_eq!(timing.data_cycles, 0);
|
||||
assert_eq!(timing.internal_cycles, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pc_writeback_and_user_transfer_encodings_remain_diagnostics() {
|
||||
for kind in KINDS {
|
||||
for instruction in [
|
||||
load(kind, Index::Pre, true, false, 15),
|
||||
load(kind, Index::Post, true, false, 0) | 1 << 21,
|
||||
] {
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM;
|
||||
let before = cpu.clone();
|
||||
assert!(matches!(
|
||||
cpu.step(&mut memory(instruction)),
|
||||
Err(CpuError::UnsupportedInstruction { .. })
|
||||
));
|
||||
assert_eq!(cpu, before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_stores_still_store_the_original_base_before_writeback() {
|
||||
for kind in [Kind::Word, Kind::Byte, Kind::Half] {
|
||||
for index in [Index::Pre, Index::Post] {
|
||||
for up in [false, true] {
|
||||
let instruction = load(kind, index, up, false, 0) & !(1 << 20);
|
||||
let mut bus = memory(instruction);
|
||||
let mut cpu = Cpu::new(ROM_START);
|
||||
cpu.registers[0] = RAM;
|
||||
let adjusted = if up { RAM + 4 } else { RAM - 4 };
|
||||
let address = if matches!(index, Index::Pre) {
|
||||
adjusted
|
||||
} else {
|
||||
RAM
|
||||
};
|
||||
cpu.step(&mut bus).unwrap();
|
||||
assert_eq!(cpu.registers[0], adjusted);
|
||||
match kind {
|
||||
Kind::Word => assert_eq!(bus.read32(address).unwrap(), RAM),
|
||||
Kind::Byte => assert_eq!(bus.read8(address).unwrap(), RAM as u8),
|
||||
Kind::Half => assert_eq!(bus.read16(address).unwrap(), RAM as u16),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
assert_eq!(cpu.cpsr(), 0x1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ The repository is a Cargo workspace with three crates:
|
|||
| `crates/gba-core/src/cpu/thumb.rs` | Thumb decoding and execution |
|
||||
| `crates/gba-core/src/cpu/status.rs` | Processor modes, register banks, and status transfers |
|
||||
| `crates/gba-core/src/cpu/exception.rs` | Reset state, exception entry, and interrupt sampling |
|
||||
| `crates/gba-core/src/cpu/load_alias_tests.rs` | Original single-load alias regressions for indexing, banks, widths, alignment, I/O, timing, and diagnostics |
|
||||
| `crates/gba-core/src/cpu/compare_psr_tests.rs` | Original Rd=15 test/compare status restoration, flags, bank, and timing regressions |
|
||||
| `crates/gba-core/src/cpu/status_tests.rs`, `crates/gba-core/src/cpu/exception_tests.rs` | Status, banking, exception, and return tests |
|
||||
| `crates/gba-core/src/cpu/thumb_tests.rs` | Thumb instructions, state switching, and decoder checks |
|
||||
|
|
|
|||
|
|
@ -49,6 +49,18 @@ Both constructors zero registers deterministically; real reset register values a
|
|||
Each call to `Cpu::step(&mut memory)` executes one ARM word or one Thumb halfword, not one hardware cycle.
|
||||
The two halves of Thumb `BL` execute separately. A standalone suffix uses the existing link register.
|
||||
|
||||
ARM single loads allow base and destination to share r0–r14, including with pre-indexed or post-indexed writeback.
|
||||
This applies to `LDR`, `LDRB`, `LDRH`, `LDRSB`, and `LDRSH`.
|
||||
The loaded value takes precedence over the updated base. Addresses and register offsets use incoming register values.
|
||||
Pre-indexed loads access the adjusted address; post-indexed loads access the original address.
|
||||
The discarded post-indexed writeback address is not a second memory access.
|
||||
Normal alignment, word rotation, odd-halfword behavior, and sign extension still apply.
|
||||
Loads preserve CPSR and use normal load timing. Device clocks advance after the read, with IRQ sampling on the next step.
|
||||
Read diagnostics leave CPU state and device clocks unchanged under the existing atomic-error policy.
|
||||
Stores with matching base/source still store the original base value before writeback.
|
||||
PC writeback, byte/halfword PC destinations, and unimplemented user-transfer encodings remain diagnostics.
|
||||
These alias rules implement ARM7 compatibility behavior, not a recommendation for portable ARM assembly.
|
||||
|
||||
Block transfers assign the lowest-numbered register to the lowest memory address.
|
||||
They align addresses down without rotating loaded words, and preserve low base bits during writeback.
|
||||
On ARM7, an empty register list transfers PC but uses a 64-byte span for addressing and writeback.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
The headless runner now executes the unmodified ARM ROM from [jsmolka/gba-tests](https://github.com/jsmolka/gba-tests).
|
||||
This is independent public test code, not a complete ARM7TDMI conformance suite or a new hardware measurement.
|
||||
The full public ARM test still fails. The first discovered CPU error is fixed; the next unsupported instruction is recorded below.
|
||||
The full public ARM test still fails. Compare/status and load/writeback alias errors are fixed; the next memory-access failure is recorded below.
|
||||
|
||||
## Prepare and run
|
||||
|
||||
|
|
@ -78,14 +78,26 @@ User/System forms still use ordinary test/compare flags because those modes have
|
|||
Original regressions cover all four test/compare operations, exception banks, flags, masks, operands, timing, conditions, and invalid saved modes.
|
||||
See [CPU status behavior](hardware/cpu.md#processor-status-and-exceptions).
|
||||
|
||||
After the fix:
|
||||
After the compare/status fix, before load/writeback alias support:
|
||||
|
||||
- Execution progresses past tests 234 and 235 and the PSR-transfer section.
|
||||
- After 861 successful steps, the runner reports unsupported instruction `0xe5b00004` at `0x0800132c`.
|
||||
- This is `LDR r0, [r0, #4]!` in upstream test 360, with base and destination equal.
|
||||
- The CPU is in System/ARM state, and r12 remains zero. The final completion checkpoint has not been reached.
|
||||
|
||||
The load/writeback alias case is the next compatibility task. Do not weaken the assertion or label this run a pass.
|
||||
After load/writeback alias support:
|
||||
|
||||
- Tests 360 and 361 complete without taking their failure paths.
|
||||
- After 877 successful steps, the runner reports `unmapped memory at 0x80000000`.
|
||||
- The current instruction is `0xe7b12060` at `0x08001384`: `LDR r2, [r1, r0, RRX]!`, in test 362.
|
||||
- With carry set and r0/r1 zero, the shifted offset correctly selects `0x80000000`.
|
||||
- The unsupported bus read prevents completion. r12 remains zero, but that alone is not a passing result.
|
||||
|
||||
Single-load aliases now retain the loaded value rather than the updated base.
|
||||
Original regressions also cover byte, halfword, and signed loads, every non-PC register bank, index modes, offsets, alignment, timing, and failures.
|
||||
The public halfword alias tests later in the ROM have not yet been reached; their source and mGBA's load ordering support the same rule.
|
||||
The next compatibility task is to verify high-address read behavior and implement the appropriate memory mapping or open-bus behavior.
|
||||
Do not bypass test 362, invent a return value, or weaken the result assertion.
|
||||
No public Thumb, timing, memory, BIOS, graphics, or unsafe suite has been claimed as passing.
|
||||
These results do not establish Pokémon Emerald compatibility.
|
||||
|
||||
|
|
@ -94,5 +106,6 @@ These results do not establish Pokémon Emerald compatibility.
|
|||
- [Pinned ARM entry and result flow](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/arm/arm.asm).
|
||||
- [Pinned test/result macros](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/lib/macros.inc).
|
||||
- [Tests 234 and 235](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/arm/data_processing.asm).
|
||||
- [Load/writeback alias tests](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/arm/single_transfer.asm).
|
||||
- [Load/writeback alias and shifted-offset tests](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/arm/single_transfer.asm).
|
||||
- [Halfword load/writeback alias expectations](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/arm/halfword_transfer.asm).
|
||||
- [Pinned license](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/LICENSE).
|
||||
|
|
|
|||
|
|
@ -61,11 +61,13 @@ Hardware references used for the core:
|
|||
- [GBATEK status transfers](https://problemkaputt.de/gbatek-arm-opcodes-psr-transfer-mrs-msr.htm).
|
||||
- [GBATEK CPU exceptions](https://problemkaputt.de/gbatek-arm-cpu-exceptions.htm).
|
||||
- [GBATEK ARM data processing](https://problemkaputt.de/gbatek-arm-opcodes-data-processing-alu.htm), including R15/status notes. Its legacy `{P}` description alone does not establish ARM7 behavior.
|
||||
- [mGBA ARM instruction implementation](https://github.com/mgba-emu/mgba/blob/master/src/arm/isa-arm.c), reviewed for Rd=15 test/compare SPSR restoration, User/System fallback, and no result write/refill.
|
||||
- [mGBA ARM instruction implementation](https://github.com/mgba-emu/mgba/blob/master/src/arm/isa-arm.c), reviewed for Rd=15 test/compare SPSR restoration, User/System fallback, and no result write/refill. Also reviewed its word/byte/halfword/signed load ordering: writeback precedes the destination result.
|
||||
- [jsmolka/gba-tests](https://github.com/jsmolka/gba-tests/tree/a7113b67e63f83a9b321696ddd7042ccfad6c881), pinned for independent ARM testing. Reviewed its MIT license, startup, result macros, compare/status tests, and load/writeback alias tests.
|
||||
- [armwrestler-gba-fixed](https://github.com/destoer/armwrestler-gba-fixed) and [arm7wrestler](https://github.com/Arisotura/arm7wrestler), considered as test sources; not integrated.
|
||||
- [SingleStepTests ARM7TDMI](https://github.com/SingleStepTests/ARM7TDMI), considered as an experimental, emulator-generated test source; not integrated.
|
||||
- [GBATEK ARM single data transfers](https://problemkaputt.de/gbatek-arm-opcodes-memory-single-data-transfer-ldr-str-pld.htm).
|
||||
- [GBATEK ARM single data transfers](https://problemkaputt.de/gbatek-arm-opcodes-memory-single-data-transfer-ldr-str-pld.htm), for address calculation, indexing, load extension, and nominal timing.
|
||||
- [Arm armasm LDR constraints](https://support.arm.com/documentation/dui0801/l/A32-and-T32-Instructions/LDR--register-offset---A32-), reviewed to distinguish portable assembly restrictions from the ARM7 alias behavior tested here.
|
||||
- [Pinned halfword alias tests](https://github.com/jsmolka/gba-tests/blob/a7113b67e63f83a9b321696ddd7042ccfad6c881/arm/halfword_transfer.asm), for loaded-value precedence in pre/post-indexed `LDRH` aliases.
|
||||
- [GBATEK memory alignment](https://problemkaputt.de/gbatek-arm-cpu-memory-alignments.htm).
|
||||
- [GBATEK multiply instructions](https://problemkaputt.de/gbatek-arm-opcodes-multiply-and-multiply-accumulate-mul-mla.htm).
|
||||
- [GBATEK block transfers](https://www.problemkaputt.de/gbatek-arm-opcodes-memory-block-data-transfer-ldm-stm.htm).
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ They do not hash ROM contents; record source revisions and file checksums alongs
|
|||
## Next validation stage
|
||||
|
||||
The first public integration is [jsmolka/gba-tests ARM](public-arm-tests.md), with a verified result-register checkpoint and a known failing baseline.
|
||||
Fix its next unsupported load/writeback alias case before claiming the full ARM ROM passes.
|
||||
Investigate its next unsupported high-address read at test 362 before claiming the full ARM ROM passes.
|
||||
For further public ARM7TDMI/GBA tests, inspect their source, license, entry assumptions, and result protocol.
|
||||
Pin source revisions and build instructions before comparing emulator changes.
|
||||
Adapt suites to verified completion addresses and result locations; do not guess them or treat timeouts as passes.
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ Update this file when a feature lands or a limit is removed.
|
|||
- `B` and `BL` with signed relative displacement and conditional execution.
|
||||
- `BX` switches between ARM and Thumb using target bit zero.
|
||||
- `LDR`, `STR`, `LDRB`, `STRB`, `LDRH`, `STRH`, `LDRSB`, and `LDRSH`.
|
||||
- Single-load base/destination aliases retain the loaded value over writeback for r0–r14, across word, byte, halfword, and signed loads.
|
||||
- Pre/post-indexed addressing, positive/negative offsets, and base-register writeback.
|
||||
- `LDM`/`STM` in increment-after, increment-before, decrement-after, and decrement-before modes.
|
||||
- Stack save/restore, PC loads, empty register lists, and ARM7 base-register overlap rules.
|
||||
|
|
@ -113,7 +114,7 @@ ROM windows have no demo-specific startup checks. Input is sampled between bound
|
|||
Audio, saves, and external BIOS loading remain unavailable. ROM loading does not establish commercial-game compatibility.
|
||||
See [cartridge loading](hardware/cartridge.md) for file limits, step budgets, and exit status.
|
||||
The [ROM suite runner](rom-tests.md) has original regressions and a [pinned public ARM baseline](public-arm-tests.md).
|
||||
The public ARM ROM now gets past its compare/status tests but stops at test 360, a load/writeback base-destination alias.
|
||||
The public ARM ROM now gets past its compare/status and word-load alias tests but stops at test 362 on an unmapped read from `0x80000000`.
|
||||
It requires known completion addresses and has no debug-port protocol, scripted input, or rendered-image assertions.
|
||||
|
||||
Unmapped reads return errors instead of hardware open-bus values.
|
||||
|
|
@ -124,7 +125,7 @@ Writes to cartridge addresses return errors instead of modeling cartridge hardwa
|
|||
|
||||
## Next steps
|
||||
|
||||
1. Implement verified load/writeback alias behavior exposed by public ARM test 360; rerun the pinned suite and fix subsequent failures.
|
||||
1. Verify high-address read behavior exposed by public ARM test 362; implement the appropriate memory mapping or open-bus behavior and rerun the pinned suite.
|
||||
Add further public suites only with verified result protocols before claiming instruction compatibility.
|
||||
2. Refine nominal timing with a fetch pipeline, Game Pak prefetch, per-access device updates, and verified timer/IRQ delays.
|
||||
3. Validate keypad retrigger behavior with hardware tests and add remaining device registers; extend BIOS reset coverage as sound/serial support becomes available.
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ direnv exec . python3 -B -m unittest discover -s tools -p 'test_*.py' -v
|
|||
|
||||
They verify pinned URLs, hashes, sizes, checkpoint validation, offline copies, destination safety, and non-overwriting output.
|
||||
Compare/status CPU regressions verify saved flags/masks, bank switching, sequential PC/timing, User/System fallback, and atomic diagnostics.
|
||||
Load-alias regressions cover widths, signed/unaligned values, immediate/register offsets, pre/post-indexing, every non-PC register bank, and address wrapping.
|
||||
They verify loaded-value precedence, preserved flags, unchanged stores, nominal timing, I/O reads before device progress, deferred IRQ delivery, and atomic errors.
|
||||
The public ARM ROM progresses past tests 360–361 but still fails on the high-address read in test 362.
|
||||
|
||||
ROM-runner tests use temporary files containing original instructions, without Nintendo assets or a desktop session.
|
||||
They check file-size boundaries, bounded reads, short reads, I/O errors, paths, symlinks, and read-only loading.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue