28 KiB
CPU: ARM7TDMI interpreter
Instruction semantics, processor modes, exceptions, and nominal timing as implemented in crates/gba-core/src/cpu/.
Instruction demo and execution rules
The --cpu-demo command first runs the original instruction demo for 40 steps, starting at 0x08000000 in ARM state.
It counts r0 down from three while incrementing r1.
SUBS updates the flags. BNE repeats the loop until the zero flag is set.
CMP checks the result, and MOVEQ sets r2 to 42.
The program then stores 42 in work RAM at 0x02000000 and loads it into r4.
The program sets the stack pointer to 0x03000100 in internal work RAM.
A subroutine saves r4 and the link register using STMDB sp!.
It shifts r4 left into r5 and multiplies r4 by r5 into r6.
SWP writes 3528 to work RAM and returns the old value, 42, in r7.
The subroutine clears r4, then uses LDMIA sp! to restore r4 and return through the saved PC.
The program then uses BX to enter Thumb code at 0x08000064.
A Thumb subroutine saves its return address, multiplies six by seven, and returns using POP {pc}.
The Thumb code stores 42 at 0x02000004, checks it, and uses BX to return to ARM.
It finishes with r0=42, r1=7, r2=42, r4=42, r5=84, r6=3528, and r7=42.
The stack pointer returns to 0x03000100.
The trace labels each step as Arm or Thumb and shows flags in NZCV order.
The final flags are 0110.
Its last instruction branches to itself. The host stops after the fixed step count.
The command then runs an 11-step exception demo with a separate CPU and memory.
ARM and Thumb code each execute SWI and enter the ARM Supervisor handler through vector 0x08.
The original test handler increments r10 and uses MOVS pc, lr to restore the caller's status.
The trace shows processor modes, instruction states, CPSR values, and two completed handler calls.
This test image is not a Nintendo BIOS and does not implement BIOS services.
A failed condition skips the instruction without changing registers or flags, except for advancing r15. For subtraction, carry means no unsigned borrow occurred. Logical flag-setting operations preserve overflow and take carry from the shifter. Unshifted operands preserve the previous carry flag. Arithmetic operations compute carry and overflow from the arithmetic result, not the shifter.
The CPU stores the next instruction address in r15.
In ARM state, ordinary operand reads add eight bytes. Register-specified shifts use a twelve-byte offset.
Storing r15 in ARM state also uses a twelve-byte offset.
Thumb empty-register-list stores use PC plus six; they must not use the ordinary Thumb operand offset.
Thumb operand reads use PC plus four. Literal loads and PC-relative addresses additionally align down to a word.
BX selects the instruction set; ordinary PC writes keep the current instruction set.
On ARM7, Thumb POP {pc} remains in Thumb even if the loaded address has bit zero clear.
Cpu::new starts in ARM System mode with interrupts enabled. It bypasses hardware boot.
Cpu::at_reset() starts at vector zero in ARM Supervisor mode, with IRQ and FIQ disabled.
Both constructors zero registers deterministically; real reset register values are not all defined.
Cpu::instruction_set(), mode(), cpsr(), and spsr() report the current processor state.
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.
Thumb STMIA Rb!, {} stores executing PC+6 at the word-aligned base, then adds 64 to the unaligned base.
The shared empty PUSH {} path also stores PC+6, retaining its existing full-descending addressing and 64-byte stack adjustment.
The stored PC keeps bit 1; neither instruction word-aligns this value. Both stores advance execution by two bytes and preserve flags.
Empty Thumb loads still align the loaded PC to a halfword and remain in Thumb state.
PUSH {lr} is not an empty list and stores the original link register with a four-byte stack adjustment.
The PC+6 correction agrees with public Thumb test 229 and mGBA's shared empty-list store path.
These forms retain nominal single-word access costs and the existing atomic-error policy; exact bus timing remains unverified.
LDM suppresses writeback when the base register is in the list.
With writeback enabled, STM stores the old base only when that register is first in the list.
Otherwise, STM stores the updated base.
ARM and Thumb instruction buffering
ARM and Thumb execution retain fetched instructions in a CPU-owned buffer, independent of later RAM contents. Let W be the instruction width: four bytes for ARM, two for Thumb. At cold entry, a successful step reads the current instruction P and samples P+W and P+2W before instruction effects. During sequential execution, the current and next instructions come from retained results; only P+2W is newly sampled. Success advances the buffer. CPU/DMA stores and host writes do not replace already buffered instructions. The existing r15 convention is unchanged: it stores the next executing address, not the hardware fetch address.
A successful branch or PC-writing refill samples target T and T+W after instruction effects, using the resulting instruction state. This includes taken branches, BX, PC loads, status-restoring returns, and software interrupts. PC writes refill even when the destination equals fallthrough. Untaken conditional branches retain sequential buffering. The first target instruction samples T+2W before execution. State transitions replace the buffer with the destination pair. Thumb BL's prefix advances sequentially; only its suffix refills. Ordinary Thumb PC writes preserve Thumb state. The buffer records both expected PC and instruction state. A state mismatch cannot reinterpret retained ARM words as Thumb halfwords.
Machine IRQ entry first samples and discards the old-state P+2W fetch, then captures the ARM vector pair.
The discarded fetch drives actual IWRAM lanes but does not execute an instruction or commit a BIOS instruction snapshot.
Missing source bytes cannot prevent IRQ entry. Missing vector bytes fail only when the handler instruction becomes current.
Cpu::enter_exception and Cpu::take_interrupt have no memory parameter: they invalidate the buffer and leave vector sampling to the next step.
An unexpected PC discontinuity starts from current mapped bytes rather than reusing a mismatched buffer.
Fetch samples use strict mapped reads, without protected-data or unused-memory fallback. Unavailable lookahead is retained as a deferred diagnostic; it cannot fail an otherwise valid current instruction or branch. A buffered fetch error fails only if execution reaches that slot. No automatic hardware abort is generated. A failed instruction preserves all CPU state, including the old buffer; its speculative advance is discarded. A successful branch discards abandoned-path errors and replaces them with target fetch results.
CPU clones and equality include the buffer. Architectural state checks are separate from full-state rollback checks in tests.
Cpu::invalidate_pipeline() explicitly discards buffered instructions without changing registers, flags, or device clocks.
Use it after debugger code repair or when attaching different memory to a retained CPU.
Do not call it for ordinary CPU/DMA stores: that would hide self-modifying-code behavior.
This is instruction buffering, not a complete timed fetch pipeline. Sampling adds no data accesses or independent device-clock updates. Code timing charges the source fetch and target pair once, with a DMA resume override on the source access. Instruction buffering and supported bus history now consume the same mapped fetch samples. Complete fetch-driven bus history, access-kind sequencing, per-access device updates, and DMA arbitration remain incomplete. See the ARM evidence and diagnostic policy, Thumb extension, and shared fetch samples.
ARM unused-memory data reads
During ARM execution, data reads from 0x00004000..0x01ffffff and 0x10000000..0xffffffff return an open-bus value.
Open bus means the bus retains previously driven data. GBATEK identifies this ARM value as the instruction word at PC+8.
These high addresses do not mirror BIOS, RAM, or cartridge addresses.
The interpreter samples mapped bytes at PC+8 before executing each ARM instruction. That one fetch supplies both the new instruction-buffer entry and the unused-memory snapshot. All unused-memory reads within that instruction use the same snapshot. Byte and halfword reads select their addressed lanes; the CPU applies normal rotation and sign extension. Base writeback, register aliases, block loads, PC loads, and flags retain their existing instruction semantics. Each data access pays the nominal unused-region cost of one cycle, plus normal code and internal costs. Snapshot reads add no data accesses or device cycles. This is not a simulated prefetch pipeline or Game Pak prefetch.
The snapshot uses strict mapped reads, without recursive open-bus fallback.
If any snapshot byte is unavailable, an unused-memory data read retains its Unmapped diagnostic.
Missing lookahead does not fail ordinary instructions, mapped loads, or skipped conditional loads.
The CPU clears the access context after each instruction, including errors.
Host inspection, ROM-suite memory assertions, instruction fetches, and DMA do not inherit this context.
Limits remain explicit:
- Thumb open bus uses the supported region rules below, including persistent internal-RAM lane history.
- BIOS-protected reads use separate retained ARM PC+8 or aligned Thumb PC+4 word snapshots.
- Unused/write-only I/O reads, general DMA-to-CPU bus handoff, and disabled-RAM reads are not modeled here. DMA channel data is separate and never overrides a CPU snapshot.
- Missing BIOS, truncated ROM, unsupported I/O, and save-memory accesses retain their existing diagnostics.
- Unused-memory writes remain diagnostics rather than ignored hardware writes. Swaps cannot silently discard their write.
- ARM/Thumb instruction buffering is persistent, but these data snapshots remain a separate bounded model. Thumb IWRAM bus-history samples do not share storage with retained instructions. DMA updates IWRAM lanes at instruction boundaries, not during CPU accesses.
This subset lets public ARM test 362 complete. That test does not assert the loaded data value. Original regressions check the values separately. See the public ARM result.
Thumb unused-memory data reads
Thumb data reads use the same unused address ranges and lane rules as ARM reads above. The snapshot depends on the newly fetched region at P+4, even when P lies in the preceding region. P below is the executing instruction address, not the visible r15 operand.
| Fetch region | Snapshot |
|---|---|
| External work RAM, palette RAM, video RAM, all three ROM windows | Halfword at P+4, repeated in both word lanes |
| BIOS and object attribute memory (OAM) | Full word at (P+4) & ~3 |
| Internal work RAM (IWRAM) | PC+4 replaces one halfword in the persistent IWRAM latch; the other lanes retain their values |
| Other code regions | Unsupported |
For BIOS/OAM, aligned instructions expose halfwords at P+4 and P+6. Instructions at two-byte-only alignment expose halfwords at P+2 and P+4. The two lanes can differ. A universal repeated-halfword rule would be incorrect.
The pipeline's new fetch supplies the bus snapshot without rereading its instruction bytes.
BIOS/OAM samples include the other halfword of the aligned bus word, but retain only the selected halfword as an instruction.
Only the required mapped bytes are sampled. A 16-bit region needs two lookahead bytes, not four.
Normal memory mirrors apply to these bytes, including physical RAM/video-memory wrap within a mapped region.
Crossing a 16 MiB region boundary does not by itself make the snapshot unknown.
Palette-to-VRAM fetches repeat the new halfword; VRAM-to-OAM fetches expose the new aligned word.
OAM-to-ROM and mapped ROM-window crossings repeat the fetched ROM halfword.
An EWRAM-to-IWRAM fetch drives its addressed local lane and requires the other lanes to be known.
For example, P=0x02fffffc needs prior high-lane history; P=0x02fffffe can establish both halves through its fill/refill and lookahead.
Missing lookahead, unknown IWRAM lanes, and unsupported I/O fetch observations still produce diagnostics for unused-memory loads.
Ordinary instructions and mapped loads can execute without a usable unused-memory snapshot.
See the boundary evidence and original tests.
All loads in one instruction share the snapshot, including block loads and stack loads. Normal sign extension, unaligned-load rotation, writeback, register aliases, and PC-load semantics still apply. Snapshots add no nominal cycles. Host reads, instruction fetches, DMA, and unsupported stores do not gain fallback behavior. Protected BIOS reads use separate retained history, not the current ROM/RAM snapshot.
GBATEK and nocash's open-bus findings document these region differences. IWRAM needs prior bus lanes, including IWRAM data-read/write changes and possible DMA effects. Do not substitute P+2 for that history: it is only the usual case, not a general rule. The original Nintendo DS also differs from GBA-family IWRAM behavior; this core targets GBA.
Bus history remains bounded despite sharing samples with the persistent instruction buffer. Refill timing, general DMA-to-CPU transitions, unused/write-only I/O, and disabled RAM remain incomplete. Original regressions validate the documented formulas. No physical-hardware or independent public Thumb open-bus pass is claimed.
Sequential IWRAM history
The emulator retains a separate IWRAM lane value and known-bit mask, independent of CPU state or executing region. Every successful ARM IWRAM fetch drives a word; every Thumb IWRAM fetch drives its addressed halfword. IWRAM data reads and writes update their addressed byte, halfword, or word lanes, including accesses from non-IWRAM code. The recorded value is raw aligned bus data, before CPU rotation or sign extension. Accesses to other regions, state changes, and exception entry do not erase this local latch. See the evidence and implementation scope.
History starts unknown. An unused-memory load requires all word lanes to be known, including for byte/halfword loads. Cold pipeline filling samples the current instruction, next instruction, and lookahead in order. A mapped Thumb IWRAM fill therefore establishes both halves before the first data read. A retained pipeline advances only its new lookahead; old instruction slots do not drive the bus again. Debugger invalidation does not erase the latch, but the following cold fill updates it from current mapped bytes. Host inspection and setup writes do not seed or change this emulated history.
Each instruction stages all new fetches and data-access changes. Success commits history regardless of the resulting PC, instruction state, or region. Failure preserves the previous committed history. Block-transfer data accesses stage updates in order; unused-memory reads still use one entry snapshot per instruction. This preserves the existing atomic diagnostic policy without adding bus or device cycles. Missing or unsupported Thumb lookahead still leaves that instruction's unused-memory snapshot unknown. Mapped boundary fetches into IWRAM use its resulting local lanes, even while the current instruction executes elsewhere. Failed or other-region observations do not erase independently known IWRAM lanes.
Thumb IWRAM refill history
After a successful refill instruction, the emulator uses the resulting PC and instruction state. Thumb target fetches occur at T and T+2; ARM target fetches occur at T and T+4. Each successful IWRAM fetch drives its own lanes, in order. Other-region or unavailable fetches do not erase local history. Normal physical RAM mirrors apply. A complete Thumb IWRAM target pair establishes both word lanes; the first target instruction then samples T+4. An ARM IWRAM target pair leaves the second fetched word in the latch.
This applies to BX, taken branches, BL suffixes, PC writes, PC loads, and status-restoring returns. Saved instruction state, not the target's low bit alone, controls exception-return sampling. Stack and block-load returns finish their data accesses before the target samples. Failed instructions do not sample a target or replace committed history.
Target samples use strict mapped reads and add no nominal data or device cycles. The same captured target pair fills the CPU buffer and updates IWRAM history; no second target read occurs. A failed target fetch remains a later instruction diagnostic. IRQ entry updates IWRAM lanes if its discarded old-state fetch addresses IWRAM. Its subsequent ARM BIOS vector pair does not erase those lanes. A BIOS handler can still change those lanes through actual IWRAM data accesses.
The refill evidence and scope distinguish these samples from a fully timed pipeline. Sub-instruction DMA ordering and complete bus sequencing remain incomplete.
DMA effects on Thumb IWRAM continuations
Within the instruction-boundary scheduler, DMA finishes before the next CPU fetch. Each successful DMA unit updates IWRAM source lanes, then IWRAM destination lanes, even before the first CPU instruction. Word accesses replace all lanes. Halfword accesses replace only the addressed halfword. Other memory regions do not replace the local IWRAM value, even when DMA's channel data changes. A resumed Thumb PC+4 fetch then replaces one halfword before the instruction's unused-memory snapshot is taken. A cold fill instead drives all its newly fetched instructions.
Channel halfword duplication is not a full-word IWRAM access. A blocked source below work RAM does not drive IWRAM; an IWRAM destination drives the actual halfword or word written. Multiple units and channel preemption update lanes in access order, not channel-number order. Host inspection/setup cannot change them.
History changes commit only after a unit succeeds. A failed unit retains the previous successful unit's history. A failed resumed instruction discards its staged changes, not the completed DMA history. No expected PC or Thumb continuation is needed to record DMA activity.
This implements local lane effects, not a general last-DMA-value override. Sub-instruction arbitration, DMA during CPU internal cycles, and exact resume timing remain incomplete. See the source evidence and limits.
Processor status and exceptions
User and System share registers. Each exception mode has its own stack pointer, link register, and SPSR.
Fast interrupt mode (FIQ) also banks r8–r12. Other modes share those registers.
MRS reads status. MSR writes selected fields; User mode can only change CPSR arithmetic flags.
Reserved status bits read as zero. Unsupported status fields have no effect.
Invalid CPSR modes and attempts to change its Thumb bit through MSR return diagnostics.
Use BX or an exception return to change instruction state.
An exception saves CPSR in the destination mode's SPSR and writes that mode's link register.
Entry selects ARM state, masks interrupt requests (IRQ), and branches to the exception vector.
FIQ entry also masks FIQ. Ordinary IRQ entry preserves the FIQ mask.
MOVS pc, lr, SUBS pc, lr, #offset, and LDM with S and PC restore CPSR from SPSR.
Return alignment follows the saved instruction state, not target bit zero.
Result-writing PC status returns fail in User/System modes because those modes have no SPSR.
An SPSR can contain invalid mode bits, but a return using those bits fails without changing CPU state.
ARM TST, TEQ, CMP, and CMN encodings with unused destination bits set to R15 restore CPSR when an SPSR exists.
These compare/status forms do not write the arithmetic result to PC. Execution continues at the next sequential address.
In User/System modes, they perform ordinary test/compare flag updates without a mode change.
Invalid saved modes still produce an atomic diagnostic. Failed conditions do not restore or validate status.
Nominal timing remains one sequential code access, plus an internal cycle for register-specified shifts; there is no refill cost.
This fixes the mode-switch expectation in public jsmolka/gba-tests ARM test 234 and agrees with mGBA's shared ALU flag handling.
Instruction buffering is discarded on a state change without an ordinary refill.
Instruction-state-changing compare forms and exact pipeline behavior remain hardware-unverified.
Other S-bit block transfers access User registers while using the current mode's base register. User-bank writeback, S-bit empty lists, and User-mode S-bit transfers return diagnostics. These restrictions avoid unpredictable or unverified forms.
Cpu::enter_exception explicitly supports software interrupt, undefined instruction, prefetch abort, data abort, IRQ, and FIQ entry.
For synchronous exceptions, call it with PC at the faulting instruction.
For IRQ/FIQ, call it between instructions, with PC at the next instruction.
Cpu::take_interrupt(irq, fiq) samples supplied interrupt lines between steps and respects masks and FIQ priority.
Machine connects timer, display, and DMA requests to the GBA IRQ line. GBA devices do not generate FIQ.
Memory and unsupported-instruction errors do not automatically enter exceptions.
Memory::with_bios(rom, bios) accepts exactly 16 KiB of caller-supplied vector code.
Memory::new(rom) leaves the BIOS area unmapped.
Without vector code, SWI enters Supervisor mode, then the next fetch reports an unmapped-memory error.
Neither constructor provides Nintendo BIOS services or initializes BIOS-managed RAM.
bios::boot(rom) separately opts into the original firmware subset described in bios.md.
Instruction and bus timing
The timing model uses ARM7 cycle summaries:
- S: sequential memory access.
- N: non-sequential memory access.
- I: internal CPU cycle, always one clock cycle.
An ordinary arithmetic instruction costs 1S. A register-specified shift adds 1I, even for a zero shift amount.
Loads cost a code S access, a data N access, and 1I. Stores use N for code and data.
Block transfers charge N for the first data word and S for each following word.
Loads add 1I. Loading PC also adds the branch refill cost.
Branches, PC writes, software interrupts, and IRQ entry charge one source fetch plus a target N+S pair.
The source fetch uses the incoming state at P+2W. The target pair uses the resulting state at T and T+W.
The first target instruction charges T+2W when it executes; the refill does not charge it early.
A skipped conditional instruction only pays its code S access.
Thumb BL charges one source fetch for each half; only its suffix adds the target N+S pair.
Multiply costs vary with the incoming multiplier's upper bytes; accumulate and long forms add internal cycles.
ARM uses Rs for this calculation. Thumb multiply uses the incoming destination register.
Source code costs use the new instruction fetch address: P+8 for ARM and P+4 for Thumb. The incoming instruction state selects the width. The sampled fetch address selects the memory region and ROM wait-state window. A 128 KiB ROM boundary therefore forces N timing when lookahead reaches it, not when execution reaches it later. This applies to cold and retained pipelines without adding separate startup-fill costs. See fetch-address timing and refill timing for evidence and limits.
Data costs use the addresses and widths of actual CPU bus calls, after alignment handling. BIOS, internal work RAM, OAM, and supported I/O accesses cost one cycle. External work RAM costs 3 cycles for byte/halfword accesses and 6 for words. Palette and video RAM cost 1 cycle for byte/halfword accesses and 2 for words, without display contention. ROM costs include one cycle plus the configured wait states for each 16-bit transfer. A ROM word uses two halfword accesses; its second halfword always uses sequential timing. Accesses at 128 KiB ROM boundaries force non-sequential timing for the first halfword.
WAITCNT resets to zero and supports byte, halfword, and word access.
The writable mask is 0x5fff; the Game Pak type flag reads as GBA, and the upper halfword reads as zero.
A CPU write to WAITCNT does not retroactively change that instruction's code-access cost.
The new settings apply to subsequent code accesses.
Nominal CPU resume after DMA
A successful DMA unit marks the next CPU instruction's nominal code access non-sequential. This applies even when DMA only accesses RAM. Consecutive units retain one pending resume, not multiple penalties. The source fetch uses its actual lookahead address, incoming width, and current WAITCNT with N instead of S. This includes branches and accepted IRQ entry; target pairs still use N then S. ARM word accesses retain an S cost for their second halfword. Stores and ROM boundaries that already use N do not receive an additional cost.
Successful timed or untimed CPU execution consumes pending resume, including skipped conditions and internal-memory instructions. Successful machine IRQ entry also consumes it. Failed CPU/DMA steps preserve it. Host access, clock-only advancement, and HALT/STOP idle do not consume it. A failed DMA unit cannot create pending resume. DMA WAITCNT writes apply before resume; CPU WAITCNT writes still use the previous code-access settings.
A resumed refill can have two N accesses: the old-PC source fetch and the first target fetch. The resume changes the source access kind; it does not append another access. This remains nominal timing, not a per-access pipeline. See the evidence and limitations.
Important timing limits:
- Source S/N kinds follow instruction-local summaries, not complete neighboring instruction/data-access sequencing.
- Code costs follow actual source and target-pair addresses, but cold current/decode fills have no separate startup charge.
- Refill cost calculation does not read target bytes. ARM/Thumb instruction-buffer and local IWRAM bus-history samples add no extra cycles.
- Target-pair sampling cannot fail the branch early. An invalid branch target still fails on the following instruction fetch.
- Game Pak prefetch is not implemented. WAITCNT bit 14 is stored but does not accelerate execution.
- PHI and SRAM wait fields are stored; PHI output and SRAM mapping are not implemented.
- External work RAM timing is fixed. The undocumented memory-control register is not implemented.
- Exact DMA startup/resumption delays, display-bus contention, shared timer prescaler phase, and timer startup delays remain unmodeled.