Files
disasm/NES PowerPad Test Card (U)/OVERVIEW.md
T
tmerritt 4c644d2a20 Pass 3 for NES PowerPad Test Card (U): name all data tables and VramWrite streams
- Decoded the VramWrite ($E1CC) stream format: <addr hi> <addr lo> <count> <bytes>, $00 terminator
- Named all 9 payload streams ($E4CC-$E7C2, no gaps/overlaps) plus VramWriteSourcePointerTable at $F000
- Split PadBitTableLives into Player1TileTable ($E4B4) and Player2TileTable ($E4C0), 12 entries each (Y=0..11)
- Decoded CHR font (digits $30-$39, letters $3A-$52, space $5F; 'W' missing from the A-Z run)
  and recovered on-screen text including 'PRESS A TO TEST SIDE-A' / 'SIDE-B'
- Documented that the 16 KiB PRG is two byte-identical 8 KiB halves; $C000 copy is dead
- Documented 7 unreached subroutines inside the live code region (kept as .byte per Pass-1 policy)
- Fixed a stray duplicate sentence and stale _wip paths in root OVERVIEW.md

Byte-identical ROM verified.
2026-10-16 20:24:23 -04:00

11 KiB
Raw Blame History

NES PowerPad Test Card (U) — Game Flow from Power-On

This document describes the execution flow of the disassembled NES PowerPad Test Card (U) 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
PRG 1 × 16 KB (mapped at CPU $C000–$FFFF)
CHR 1 × 8 KB
Mirroring Horizontal
PRG bank start CPU $C000
Source file copilot/prg.s (~1,650 lines with headers)
Output reconstructed_prg.bin (16,384 bytes)

Interrupt vectors (CPU $FFFA–$FFFF)

Vector Address Handler
NMI $E06E NmiHandler
RESET $E000 Reset
IRQ / BRK $E000 Reuses Reset

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 (all 16,384 bytes), and prg.bin matches the PRG bank of NES PowerPad Test Cart (U) [!].nes.


3. Pass Status

Pass Status Description
Pass 1 Complete Code/data split — entire PRG bank classified as traced code (~7.2%) or explicit .byte data (~92.8%) with ; CPU:$xxxx PRG:$xxxx annotations
Pass 2 Complete Routine headers added for all labeled positions; key routines semantically named (Reset, NmiHandler, VramWrite, ReadPad, ProcessPads, etc.); zero-page aliases in locals.inc
Pass 3 Complete All data tables named: VramWriteSourcePointerTable ($F000, 9 streams), Player1TileTable/Player2TileTable ($E4B4/$E4C0), PaletteData ($E121), and all nine VramWrite payload streams (PowerPadSideAButtonGraphics, PanelFrameTiles, StatusMessageText, SideATestPromptText, …). Screen text decoded from CHR. Remaining 46 Label_Exxxx are internal loop/branch targets.

4. Reset — CPU $FFFC → $E000

Reset ($E000)

On power-on or reset:

  1. Calls a chain of initialization subroutines in sequence:
    • ResetInit ($E09B): sei/cld, disable PPU rendering, wait for two vblanks
    • ClearRAM ($E0C9): clear 8 pages of zero-page RAM
    • ClearVRAM ($E0EB): clear nametable memory
    • LoadPalette ($E107): load 32 bytes of palette data into PPU palette RAM
    • ClearVRAM_Fill23 ($E142): fill VRAM with $23 (blank tile attribute)
    • InitAPU ($E16B): initialize APU pulse/triangle channels
    • ClearVRAM_FillFF ($E187): fill VRAM with $FF
    • Five LoadPatternTable routines ($E19A–$E1CB): load pattern tables from $F000 region
    • SetPPUCTRL_NMI ($E219): enable NMI bit in PPUCTRL
  2. Main loop (Label_E058 at $E058): calls ClearScreen ($E4FA), WaitForVBlank ($E092), checks $07 (frame counter high nibble), loops back to $E000 if zero, otherwise calls ResetScroll ($E22D) and jumps back to $E058.

Key point: The main loop is driven by vblank waits and frame counter checks. All per-frame work (controller reading, pad processing, screen drawing) happens inside the NMI handler.


5. NMI / Frame Heartbeat — NmiHandler ($E06E)

The NMI fires once per frame:

  1. Save registers (PHA / TXA / PHA / TYA / PHA).
  2. Calls DisableNMI ($E1FB) — clears NMI bit in PPUCTRL shadow.
  3. Increments Zp03 (frame counter).
  4. Reads PPUSTATUS ($2002), stores to Zp02 (vblank flag).
  5. Calls ReadPad ($E338) — reads controller pad data.
  6. Calls ShiftPads ($E362) — shifts pad data into state variables.
  7. Calls ProcessPads ($E398) — processes pad transitions, updates scores.
  8. Calls DrawScore ($E3E1) — draws score digits.
  9. Calls DrawLives ($E407) — draws lives indicators.
  10. Restore registers (PLA / TAX / PLA / TAY / PLA / RTI).

6. Key Subroutines

Label Address Description
Reset $E000 Cold-start initialization and main loop entry
NmiHandler $E06E Per-frame NMI: pad read, process, score/lives draw
WaitForVBlank $E092 Waits for vblank flag, clears it
ResetInit $E09B Cold-start hardware init (PPU, APU, vblank waits)
ClearRAM $E0C9 Clears 8 pages of zero-page RAM
ClearVRAM $E0EB Clears nametable memory with zeros
LoadPalette $E107 Loads 32-byte palette into PPU palette RAM
ClearVRAM_Fill23 $E142 Fills VRAM with blank tile value $23
InitAPU $E16B Initializes APU pulse/triangle channels
ClearVRAM_FillFF $E187 Fills VRAM with $FF
LoadPatternTable1–5 $E19A–$E1CB Load pattern tables from $F000 region
VramWrite $E1CC Generic VRAM write via indirect pointer
EnableNMI $E1F1 Enables NMI bit in PPUCTRL shadow
DisableNMI $E1FB Disables NMI bit in PPUCTRL shadow
SetPPUCTRL_NMI $E219 Sets NMI bit ($80) in PPUCTRL shadow
ResetScroll $E22D Resets PPU scroll X/Y to zero
InitRoundParams $E236 Initializes round parameters
DrawPlayer1Pad $E270 Draws player 1 pad indicators
DrawPlayer2Pad $E2B0 Draws player 2 pad indicators
ClearRows $E2F0 Clears screen rows with blank tiles
ClearGameVars $E32D Clears game variables $06–$09
ReadPad $E338 Reads controller pad via shift register
ShiftPads $E362 Shifts pad data into state variables
ProcessPads $E398 Processes pad transitions, updates scores
DrawScore $E3E1 Draws score digits on screen
DrawLives $E407 Draws lives indicators on screen
ShiftVar1–4 $E433–$E47E Shift pad bits into score accumulators
UpdatePads $E47F Updates pad state, checks button transitions
LoadPatternTable6–7 $E4A0–$E4B3 Load additional pattern tables
ProcessPadBits $E4FA Process pad bit data for transitions
SetAPUNoise $E52E Sets APU noise channel
ClearAPUNoise $E534 Clears APU noise channel
ClearScreen $E53A Clears screen with blank tile $5F

7. Zero-Page Aliases (locals.inc)

Alias Address Role
Zp00 $00 PPUCTRL shadow register
Zp01 $01 PPUMASK shadow register
Zp02 $02 VBlank flag
Zp03 $03 Frame counter / NMI tick
Zp05 $05 Score/digit temp
Zp06 $06 Pad 1 state byte 1
Zp07 $07 Pad 1 state byte 2
Zp08 $08 Pad 1 state byte 3
Zp09 $09 Pad 1 state byte 4
Zp0A $0A Pad 2 state byte 1
Zp0B $0B Pad 2 state byte 2
Zp0C $0C Pad temp / swap variable
Zp0D $0D Pad mode / draw flag
Zp0E $0E Shift counter / temp
Zp0F $0F Delay counter
Zp10 $10 VRAM pointer low byte
Zp11 $11 VRAM pointer high byte
Zp12 $12 Score digit 1 accumulator
Zp13 $13 Score digit 2 accumulator
Zp14 $14 Score digit 3 accumulator
Zp15 $15 Score digit 4 accumulator

Full list in copilot/locals.inc.


8. Data Tables

8.1 Direct tables

Table Address Size Description
PaletteData $E121 32 Palette loaded into PPU $3F00-$3F1F by LoadPalette
Player1TileTable $E4B4 12 PPUADDR low bytes for player 1, indexed Y=0..11
Player2TileTable $E4C0 12 Same for player 2; $00 = "skip this button"
VramWriteSourcePointerTable $F000 18 9 little-endian stream pointers, one per LoadPatternTable* / draw call site

8.2 VramWrite payload streams

VramWrite ($E1CC) reads records of <addr hi> <addr lo> <count> <count bytes>, terminated by a $00 at a record boundary. All nine streams tile $E4CC-$E7C2 with no gaps and no overlaps:

Slot Ptr Stream label Address Records Writes to
0 $F000 PowerPadSideAButtonGraphics $E54F 2 $2083, $20A3 — 52 tile graphics in pattern table 0
1 $F002 PowerPadButtonLabelTiles $E58A 4 $2136, $2156, $2236, $2256 — button captions
2 $F004 PanelFrameTiles $E5B3 8 $2124…$22C4 — panel borders for six indicator boxes
3 $F006 SideAPressedButtonRow $E64C 6 $2184…$22A4 — side-A button caps
4 $F008 SideAPanelButtonRow $E6BF 6 same rows, alternate layout
5 $F00A StatusMessageText $E732 3 $2121, $2163, $2183 — status messages
6 $F00C Player1ButtonIconStream $E4CC 2 $2196, $21B6
7 $F00E Player2ButtonIconStream $E4E3 2 $2296, $22B6
8 $F010 SideATestPromptText $E790 2 $2304, $2344 — the prompt rows

8.3 Screen text (decoded from CHR)

Font mapping read directly out of chr.bin pattern table 1: digits $30-$39 = '0'-'9', letters $3A-$52 = 'A'-'Z' (note W is absent from the A-Z run), $57 = -, $58 = /, $59 = \, $5F = space.

Address Text
$2121 CONNECT POWER PAD <$5D> CONTROLLER
$2163 <$60>IF POWER PAD IS CONNECTED<$62>
$2183 <$61> IC MAY BE BAD <$53> <$63>
$2304 PRESS A TO TEST SIDE-A
$2344 PRESS B TO TEST SIDE-B

Tiles $53-$5E are truncated glyph fragments in CHR ROM — including the W of POWER, which sits at $5E and renders incomplete. The original cartridge therefore displays partial marks at those positions; this is a ROM authoring artifact, reproduced faithfully.


9. Build

cd "NES PowerPad Test Card (U)/copilot"
make        # → ca65 + ld65 + cmp reconstructed.nes rom.nes → "Byte-identical ROM verified."

10. Notes

  • The 16 KiB PRG is two byte-identical 8 KiB halves. prg[0x0000:0x2000] == prg[0x2000:0x4000] is exactly true. Both halves hold the same program, pointer table, and vector set. The vectors point at $E06E/$E000/$E000, so the CPU always enters the $E000 copy; nothing references $C000-$DFFF, so the first copy is dead and is kept as classified .byte data rather than disassembled.
  • Unreached gaps inside the live code region decode as valid subroutines (PPUMASK show/hide background, PPUCTRL NMI-disable, frame-counter digit tiles, ldx #$80 / txs). Per Pass-1 policy they stay .byte data; each is decoded in the prg.s header comment block.
  • The PRG is approximately 7.2% code / 92.8% data. Most data is $FF padding from $E7C3-$EFFF and $F012-$FFF9.
  • The NMI handler drives per-frame updates; the main loop spins waiting for vblank and frame-counter changes.
  • IRQ vector points to $E000 (Reset) and is effectively unused.

11. Next Steps

  • Optional: refine the Zp05–Zp0F aliases now that ProcessPads/DrawScore/DrawLives semantics are known (pad-state bytes vs. shift counters).
  • Optional: $E4CC-$E4F9 holds two near-identical icon streams differing only in two bytes; could be collapsed to a parameterized table in a future reconstruction, but the byte form is preserved as-is.