# Excitebike (JU) — Game Flow from Power-On This document describes the execution flow of the disassembled Excitebike (JU) PRG bank (16 KB, mapped at CPU **$C000–$FFFF**), starting from power-on. It is based entirely on `copilot/prg.s` in the repository. The rebuild is verified **byte-identical** to the original `prg.bin`. --- ## 1. ROM and Memory Layout | Property | Value | |----------|-------| | Format | iNES / NES 2.0 | | PRG | 1 × 16 KB (mapped at CPU $C000–$FFFF) | | CHR | 1 × 8 KB | | Mirroring | Vertical | | PRG bank start | CPU $C000 | | Source file | `copilot/prg.s` (14,111 lines) | | Output | `reconstructed_prg.bin` (16,384 bytes) | ### Interrupt vectors (CPU $FFFA–$FFFF) | Vector | Address | Handler | |--------|---------|---------| | NMI | $C23B | `NMI` | | RESET | $C184 | `Reset` | | IRQ / BRK | $FFF0 | (`$FFF0` — points into audio data region, unused) | --- ## 2. Build Status `make` in `copilot/` assembles `prg.s` with `ca65`, links with `ld65`, and verifies against `prg.bin`. **Verification result: BYTE-IDENTICAL** — `reconstructed_prg.bin` matches `prg.bin` exactly (headers, CHR, and all PRG bytes). ### Build fix applied The original source contained a `bpl LookupTableEntry_FAF5` branch at CPU $FAB4 (line 9425 of `prg.s`), but the symbol `LookupTableEntry_FAF5` was undefined. The branch target CPU $FAF5 fell in the middle of what the disassembler classified as `lda #$16` (`A9` at $FAF4, `16` at $FAF5). The fix: - Replaced `bpl LookupTableEntry_FAF5` with `bne *-$1B` at CPU $FAB4, producing the exact original bytes `d0 e3` (the disassembler had mislabeled `bne` as `bpl`). --- ## 3. Reset — CPU $FFFC → $C184 **`Reset`** ($C184) On power-on or reset: 1. `sei` — disable maskable interrupts. 2. `cld` — clear decimal mode. 3. `lda #$00 / sta PPUCTRL` — disable PPU rendering and NMI. 4. `ldx #$FF / txs` — initialize stack pointer to $FF. 5. **Wait for two VBlanks** — poll `PPUSTATUS` bit 7 twice (`@vblank1`, `@vblank2`). This is the PPU warm-up delay. 6. **Reset magic check** — compare `ResetMagic1` ($05CE) against `$A5` and `ResetMagic2` ($05CF) against `$5A`: - If both match → warm start: set Y = $04 (preserve more RAM) - If either mismatches → cold start: set Y = $07 (clear more RAM) 7. **Clear RAM** — clear zero page and one page ($0000–$00FF, or $0000–$06FF depending on warm/cold start) using indirect pointer at $00/$01. 8. **Persistent RAM init** — if reset magic was valid, `jsr InitializePersistentRam` ($C278): - Copy 31-byte template (`PersistentRecordInitialDataC091` at $C091) to both `PersistentRecord0` ($0580) and `PersistentRecord1` ($05A3) - Fill `PersistentBuffer` ($06E0) with $40, set byte at $07AF to $09 - Set reset magic bytes to $A5/$5A - Zero tail bytes of both persistent records 9. `sta APUDMALOAD` — disable DMC. 10. `lda #$06 / sta PPUMASK / sta $18` — enable background + sprites, set $18. 11. `jsr InitializeRuntimePpuState` — fill OAM shadow with $F8, process PPU entries, zero scroll/render fields. 12. `lda #$90 / jsr SetPpuControl` — set PPUCTRL = $90 (NMI enabled, $2000 base). 13. Falls into **`@main`** ($C1A4) — the permanent main loop. --- ## 4. Main Loop — `@main` ($C1A4) After reset, `Reset` never returns. It enters the infinite main loop: ``` @main ($C1A4): jsr ShiftRuntimeState ; $E8BC — shift runtime bytes $18-$1F right lda NmiSyncState ; $48 bmi @main ; loop if $FF (NMI_SYNC_PENDING) beq @main ; loop if $00 ; NmiSyncState == $01 (NMI_SYNC_COMPLETE): proceed jsr DisableNmi ; $C362 — clear NMI bit in PPUCTRL lda NmiSyncState cmp #NMI_SYNC_COMPLETE ; $01 bne @alternate ; if not complete, receive instead jsr TransmitRuntimeDataCode ; $C6C8 — serial transmit runtime data jmp @finish @alternate: jsr ReceiveRuntimeDataCode ; $C739 — serial receive runtime data @finish: lda #NMI_SYNC_PENDING ; $FF sta NmiSyncState ; $48 — reset sync flag for next frame jsr WaitForVblankEndAndEnableNmi ; wait for VBlank, set NMI bit jmp @main ; loop forever ``` The main loop is **NMI-synchronized**: it blocks until `NmiSyncState` ($48) becomes $01 (set by NMI each frame), then performs serial I/O (transmit or receive runtime data), resets the sync flag to $FF, waits for the next VBlank, enables NMI, and repeats. ### `ShiftRuntimeState` ($E8BC) Shifts runtime state bytes $18–$1F right by one bit. The carry-in is derived from controller bit 1 ($19 bit 2), enabling serial/data shifting tied to controller input. --- ## 5. NMI / Frame Heartbeat — `NMI` ($C23B) The NMI fires once per frame and is the central real-time driver: 1. **`jsr DisableNmi`** ($C362) — clear bit 7 of `PpuControlShadow` ($10), write to PPUCTRL ($2000). NMI is disabled during the handler to prevent re-entry. 2. **PPU mask**: if $45 == 0, `PPUMASK = PpuMaskShadow | $1A` (enable background + sprites); otherwise skip. 3. **OAM DMA**: write $00 to OAMADDR ($2003), then $02 to OAMDMA ($4014) — copy $0200–$02FF to sprite RAM. 4. **`jsr NmiPpuUpdate`** ($C27E) — process one PPU update command stream (indexed by $45), then reset $0300/$0301/$45 and PPU address to $3F00. 5. **Optional rider/lane update**: if $47 != 0, `jsr UpdateRiderLanePositionStates` and `jsr NmiRuntimeUpdate`. 6. **Runtime counters**: if $03B1 | $03B2 == 0, increment $3F and `jsr UpdateRuntimeCounters`. 7. **`jsr ReadControllers`** ($E8E5) — read both controller ports ($4016/$4017), update `Controller1Held` ($14), `Controller1Pressed` ($15), `Controller2Held` ($16), `Controller2Pressed` ($17). 8. **`jsr UpdateAudio`** ($F8DE) — update all four APU channels (Pulse 1, Pulse 2, Noise, Sound Effect) plus sound effects and DMA. 9. **`jsr DispatchNmiState`** ($C2A9) — dispatch to the current NMI state handler (see below). 10. **Read PPUSTATUS** ($2002) to reset the address latch. 11. **`jsr WaitForVblankEndAndEnableNmi`** — wait for VBlank end, set NMI bit in PPUCTRL. 12. **`rti`** — return from interrupt. ### `NmiPpuUpdate` ($C27E) Processes a single PPU update command: - Load pointer from `PpuUpdatePointerLow/High` ($C0F6/$C111 tables) indexed by $45. - Call `ProcessPpuUpdateStream` ($D3C1) which reads the command stream and writes bytes to $2007. - Zero out $0300, $0301, and $45. - Reset PPU address to $3F00 (four writes to $2006). --- ## 6. NMI State Machine — `DispatchNmiState` ($C2A9) The NMI dispatcher supports two modes based on `NmiDispatchMode` ($40): ### Mode 0 — Direct state dispatch (`DispatchNmiState` @ $C2A9) If `NmiDispatchMode` == 0: - Load `NmiState` ($41), double it, use as index into `NmiStateHandlerTable` ($C000–$C01B). - Jump indirect through zero-page $00/$01 (high byte left in A, stored to $01 by code at $CB36). **NMI State Handler Table** ($C000–$C01B) — 14 entries for states $00–$0D: | State | Handler Address | Purpose | |-------|----------------|---------| | $00 | `NmiState00` | Menu/title — selection handling | | $01 | $C395 | Menu horizontal input | | $02 | `UpdateGameplayRuntime` | Main gameplay update | | $03 | `DispatchNmiSequenceStep` | Sequence step dispatch | | $04 | `NmiState04_07` | Shared state $04/$07 handler | | $05 | `UpdateGameplayRuntime` | Main gameplay update | | $06 | `DispatchNmiSequenceStep` | Sequence step dispatch | | $07 | `NmiState04_07` | Shared state $04/$07 handler | | $08 | $C3BD | Menu vertical input | | $09 | `UpdateRaceRuntime` | Race gameplay update | | $0A | `UpdateGameplayRuntime` | Main gameplay update | | $0B | `DispatchNmiSequenceStep` | Sequence step dispatch | | $0C | $C43D | Serial handshake state $0C | | $0D | $C41C | Serial handshake state $0D | ### Mode 1 — Sequence dispatch If `NmiDispatchMode` != 0: - Disable PPU rendering. - Wait until $30 == 0 (not busy). - Look up sequence pointer from `NmiStateToSequenceIndex` + `NmiSequencePointerLow/High` tables. - Call the sequence handler at the current `NmiSequenceStep` ($44). - If step < 0 (after increment/decrement), go to `InitializeRuntimePpuState`. **NMI Sequence Pointers** ($C03C–$C07F) — 9 sequences: | Index | Address | Handlers | |-------|---------|----------| | 0 | $C03C | $C2BD, $C514 | | 1 | $C040 | `SetSerialUpdateSelector06`, $C3F3, $C514 | | 2 | $C046 | `SerialUpdateSelectorCluster`, `ClearRuntimeZeroPage`, `InitializeCourseRuntime`, `SetSerialUpdateSelector17`, `SetSerialUpdateSelector0E`, `InitializeGameRuntime` | | 3 | $C052 | `SetSerialUpdateSelector06`, `SetSerialUpdateSelector04`, `SelectNmiUpdateForState`, `CallC92CWithZero`, `CompareAndUpdateRecord` | | 4 | $C05C | `SetSerialUpdateSelector06`, `SetSerialUpdateSelector09`, `Prepare0300Structure`, $C514 | | 5 | $C064 | $C551, `ClearRuntimeZeroPage`, `InitializeCourseRuntime`, `SetSerialUpdateSelector17`, $C562, $C5C2 | | 6 | $C070 | `SetSerialUpdateSelector06`, $C575, $C514 | | 7 | $C07A | `ClearRuntimeZeroPage`, `RestoreWorkingRecordFields`, $C518 | | 8 | $C076 | $C582, $C518 | --- ## 7. Key NMI State Handlers ### `NmiState00` ($C346) Title/menu state: - If $31 == 0: initialize $43, $4A = 0, $03F7 = $0F, `NmiState` = 2 (gameplay). - Else: `jsr UpdateMenuSelection` with count 3; if no input change, set sprite 0 Y from `MenuInitialYTable`; otherwise handle selection done and advance state. ### `NmiState01` ($C397) Menu horizontal input: - Filter controller buttons, `jsr UpdateMenuSelection` (count 5). - If input changed, move horizontal marker; else store `MenuSelectionIndex` to $43 and advance to next NMI state, entering sequence mode. ### `NmiState08` ($C3BD) Menu vertical input: - `jsr UpdateMenuSelection` (count 6). - Based on selection: advance state, jump to sequence entry, or handle choice 5 (reset runtime, preserve selection). ### `NmiState04_07` ($C465) Shared handler for states $04/$07: - If $34 == 0: advance NMI state and enter sequence mode; clamp state to max 7. - Else: `jsr $C92C`, check bit 4 of $3F, either return or `jmp StateHelperCAFA`. ### `NmiState0C` / `NmiState0D` ($C41C / $C43D) Serial handshake states: - State $0C: if sequence step == 0, set sync complete; else if sync pending, copy record tails and continue handshake. - State $0D: similar handshake with persistent record synchronization. --- ## 8. Gameplay Runtime ### `UpdateGameplayRuntime` ($C99A) Called from NMI states $02, $05, $0A: - `jsr $CA08` (update four rider slots). - If $03B1 | $03B2 != 0: jump to PPU update tail. - Otherwise: `jsr $D918`, `jsr $DD8D`, `jsr UpdateAllRiderScriptStateE733`, `jsr $E927`. - If $52 != 0: `jsr StateHelperCA9B`. - If $03F2 != 0: optional updates ($DA9F, $DBC4, $DEBB). - `jsr UpdateGlobalCounters`, `jsr BuildGlobalDisplayEAC5`, `jsr CompareRuntimeSlots`, `jsr CheckSlotState`. - `jsr UpdateTriggeredSlots`, `jsr UpdateFourSlots`, `jsr UpdateFourSlotPositionsE96C`, `jsr UpdateDisplayState`, `jsr UpdateFourSlot8CE836`, `jsr BuildFourSlotSpritesEA44`, `jsr ProcessCourseDefinition`, `jsr UpdateSlotPairState`, `jsr UpdateGlobalFC`, `jsr UpdateGlobalDisplay`, `jsr ProcessRuntimeEntries`, `jsr PrepareSortedSlots`, `jsr UpdateRuntimeSlots`, `jsr UpdateRiderCourseStateE70B`, `jsr UpdatePpuState`, `jsr ProcessRiderSlots`, `jmp ProcessSortedSlots`. ### `UpdateRaceRuntime` ($C8D6) Race-specific update: - `jsr $CA1B`, `jsr UpdateGlobalRuntimeEB17`, `jsr UpdateAccumulator`, `jsr UpdateGlobalPosition`, `jsr UpdateRiderCourseStateE70B`, `jsr ProcessCourseDefinition`, `jsr $F68D`, `jsr $F755`, `jsr $C229`, then `jmp UpdatePpuState`. --- ## 9. Audio System **`UpdateAudio`** ($F8DE) — called from NMI: 1. `APUFRAMECNT` ($4017) = $C0. 2. `jsr UpdatePulse1Channel`, `jsr UpdatePulse2Channel`, `jsr UpdateNoiseChannel`, `jsr UpdateSoundEffect`. 3. Zero out $FF, $FE, $FD, $FB. 4. If $07FF != 0: `sta APUDMALOAD` ($4011), decrement $07FF. Audio channel configuration uses tables at $FF00+ (period tables) and $FF3E+ (mask tables). Audio sequence/data lives at $FF64–$FFE9. --- ## 10. Serial I/O System Excitebike uses the controller port $4016 for serial communication (likely the Famicom 3D System or a similar serial peripheral — peripheral identity unknown): **Transmit path** (`TransmitRuntimeDataCode` at $C6C8): - Configure a 64-byte block from $0060, transmit it. - Configure a 20-byte block from $05E0 (working buffer), transmit it. - Uses pulse-length encoding: long pulse = 1, short pulse = 0, MSB first. - Preamble: short pulses, then long pulses, then short pulses, then one long pulse. **Receive path** (`ReceiveRuntimeDataCode` at $C739): - Disable NMI, configure receive blocks, receive data. - Validate 5-byte header against `ReceiveHeaderTableBaseC17F` ($C17F: $E8). - If valid: set state variables ($A0–$A3), set $45 = $1A, wait for NMI update. - Receive second block (working buffer), validate checksum, return success/failure. Serial helpers: `TransmitByte` ($C66E), `ReceiveByte` ($C6A5), `TransmitPulseShort` ($C64E), `TransmitPulseLong` ($C653), `ReceiveSerialBit` ($C691), `SerialWaitPulse` ($C639). --- ## 11. Zero-Page / RAM Symbols | Address | Symbol | Use | |---------|--------|-----| | $00–$01 | TMP00/TMP01 | General scratch / indirect pointer | | $08–$09 | SerialBitCountLo/Hi | Serial bit counter | | $0E | SerialBlockPtr | Serial block pointer | | $0D | SerialBlockLength | Serial block length | | $10 | PpuControlShadow | $2000 mirror | | $11 | PpuMaskShadow | $2001 mirror | | $12–$1F | — | Runtime state bytes (shifted each frame by `ShiftRuntimeState`) | | $14 | Controller1Held | Controller 1 held buttons | | $15 | Controller1Pressed | Controller 1 newly pressed | | $16 | Controller2Held | Controller 2 held buttons | | $17 | Controller2Pressed | Controller 2 newly pressed | | $31 | — | Menu/timing state | | $34 | — | Menu selection state | | $3F | — | Runtime counter | | $40 | NmiDispatchMode | NMI dispatch mode (0=direct, 1=sequence) | | $41 | NmiState | Current NMI state ($00–$0D) | | $42 | MenuSelectionIndex | Current menu selection | | $43–$4A | — | State/temp variables | | $44 | NmiSequenceStep | Current NMI sequence step | | $45 | — | PPU update index | | $47 | — | Optional update flag | | $48 | NmiSyncState | $FF=PENDING, $01=COMPLETE | | $49 | InputActionLatch | Menu input latch | | $A4 | RiderMotionState | Rider motion state ($01=disabled, $02=active, $03=transition, $04=complete) | | $0100 | — | DMA/APU scratch | | $0200 | OamBuffer | OAM shadow / sprite DMA source | | $0300–$0301 | — | PPU update command structure pointer | | $03B1–$03B2 | — | Timed update flag | | $0580 | PersistentRecord0 | First persistent record (31 bytes + tails) | | $05A3 | PersistentRecord1 | Second persistent record | | $05CE–$05CF | ResetMagic1/2 | $A5/$5A warm-start signature | | $05E0 | WorkingBuffer | Working buffer for serial I/O | | $06E0 | PersistentBuffer | Persistent buffer (256 bytes) | | $07FF | — | Audio DMA trigger | --- ## 12. PPU / Rendering - **OAM DMA**: Every NMI, DMA from $0200–$02FF via `OAMDMA` ($4014). - **PPU update streams**: Indexed pointer tables at $C0F6 (low) / $C111 (high), 27 entries. Processed by `ProcessPpuUpdateStream` ($D3C1) during NMI. - **Scroll**: `PpuControlShadow` ($10) and `PpuMaskShadow` ($11) are software mirrors, updated during NMI and written to $2000/$2001. - **OAM shadow**: Filled with $F8 (offscreen) by `FillOamShadowWithF8` during runtime PPU init. --- ## 13. Summary of Execution Flow ``` Power On / Reset │ ▼ Reset ($C184) ────────────────────────────────────────┐ │ sei, cld, disable PPU/NMI, stack=$FF │ │ wait 2× VBlank (PPU warm-up) │ │ check reset magic ($A5,$5A at $05CE/$05CF) │ │ valid → warm start (preserve more RAM) │ │ invalid → cold start (clear more RAM) │ │ clear RAM ($0000–$00FF or $0000–$06FF) │ │ if valid: InitializePersistentRam │ │ (copy template to both persistent records) │ │ disable DMC, PPUMASK=$06, PPUCTRL=$90 (NMI on) │ │ InitializeRuntimePpuState │ │ │ ▼ │ @main ($C1A4) ────────────────────────────────────────┤ │ │ ├─ jsr ShiftRuntimeState ($E8BC) ──────────────┐ │ shift $18-$1F right, carry from controller │ │ │ ├─ lda NmiSyncState ($48) │ ├─ bmi @main (if $FF = PENDING, loop) │ ├─ beq @main (if $00, loop) │ │ (only proceed when NMI set it to $01) │ │ │ ├─ jsr DisableNmi ($C362) │ ├─ if NmiSyncState == $01: │ │ jsr TransmitRuntimeDataCode ($C6C8) │ │ else: │ │ jsr ReceiveRuntimeDataCode ($C739) │ │ │ ├─ lda #NMI_SYNC_PENDING ($FF) │ │ sta NmiSyncState │ ├─ jsr WaitForVblankEndAndEnableNmi │ └─ jmp @main ◄──────────────────────────────────┘ │ NMI ($C23B) [once per frame] │ │ │ ├─ jsr DisableNmi (PPUCTRL bit7 = 0) │ ├─ PPUMASK = PpuMaskShadow \| $1A (if $45==0) │ ├─ OAM DMA ($0200-$02FF → $4014) │ ├─ jsr NmiPpuUpdate ($C27E) [PPU update stream] │ ├─ if $47 != 0: │ │ jsr UpdateRiderLanePositionStates │ │ jsr NmiRuntimeUpdate ($D14E) │ ├─ if $03B1|$03B2 == 0: inc $3F, jsr UpdateRuntimeCounters │ ├─ jsr ReadControllers ($E8E5) │ ├─ jsr UpdateAudio ($F8DE) [4 channels + SFX] │ ├─ jsr DispatchNmiState ($C2A9) │ │ ├─ NmiDispatchMode==0: direct state table │ │ │ $00=NmiState00 (menu/title) │ │ │ $01=$C395 (menu horiz) │ │ │ $02=$C99A (UpdateGameplayRuntime) │ │ │ $03=DispatchNmiSequenceStep │ │ │ $04/$07=NmiState04_07 │ │ │ $05=$C99A (UpdateGameplayRuntime) │ │ │ $06=DispatchNmiSequenceStep │ │ │ $07=NmiState04_07 │ │ │ $08=$C3BD (menu vert) │ │ │ $09=$C8D6 (UpdateRaceRuntime) │ │ │ $0A=$C99A (UpdateGameplayRuntime) │ │ │ $0B=DispatchNmiSequenceStep │ │ │ $0C=$C43D (serial handshake) │ │ │ $0D=$C41C (serial handshake) │ │ └─ NmiDispatchMode!=0: sequence dispatch │ │ (9 sequences at $C03C-$C07F) │ │ │ ├─ read PPUSTATUS │ ├─ WaitForVblankEndAndEnableNmi │ │ (set NmiSyncState = $01 implicitly via │ │ main loop's view of completion) │ └─ rti │ Serial I/O (from main loop, once per frame) ├─ TransmitRuntimeDataCode ($C6C8) [when sync complete] │ ├─ 64-byte block from $0060 │ │ └─ 20-byte block from $05E0 (working buffer)│ └─ ReceiveRuntimeDataCode ($C739) [alternate] ├─ validate 5-byte header ($C17F) │ ├─ receive second block │ └─ checksum verify │ Audio (from NMI via UpdateAudio at $F8DE) ├─ Pulse 1 channel ($4000-$4003) │ ├─ Pulse 2 channel ($4004-$4007) │ ├─ Noise channel ($400C-$400F) │ ├─ Sound effect │ └─ DMC load if $07FF != 0 │ ``` --- ## 14. Notes - **Build verification:** `make` in `copilot/` produces `reconstructed_prg.bin` that is **byte-identical** to `prg.bin` and to the PRG bank of `rom.nes`. - Vectors confirm NMI at $C23B, RESET at $C184, IRQ at $FFF0 (points into audio data region — likely a placeholder or unused). - The main loop (`@main` at $C1A4) is strictly NMI-synchronized via `NmiSyncState` ($48): it loops until the NMI sets the state to $01 (COMPLETE), performs serial I/O, resets to $FF (PENDING), waits for VBlank, and repeats. - Serial I/O uses controller port $4016 for pulse-length encoded data transmission/reception — likely for the Famicom 3D System or a similar serial peripheral. - The NMI state machine supports both direct state dispatch (14 states via table at $C000) and sequence dispatch (9 sequences via tables at $C03C–$C07F). - Many routines carry address-based labels (`NmiState04_07`, `UpdateSideScrollingGameplay_C073`, etc.) reflecting the automated first-pass disassembly; semantic naming is provisional. - `symbols.inc` (embedded at the top of `prg.s`) provides NES hardware register names and key zero-page symbols. - One build fix was required: an undefined branch target `LookupTableEntry_FAF5` was replaced with a PC-relative `bne *-$1B` to produce the original `d0 e3` bytes.