Normative specification · v0.1

re8 console — high-level technical specification

Document purpose. This is the normative specification of the re8 console: a boutique 2D games machine built from currently-manufactured parts, comprising a W65C02S CPU and a single custom ASIC (oito) that integrates the video display processor, a 16-voice audio unit, the cartridge mapper and all system glue.

It defines everything an implementer needs to build the hardware or a Tier 1 (compatibility) reference emulator (§14.5): machine-visible behaviour, electrical interfaces and bill of materials, timing, file and tool ABI, and the conformance contract.

Version 0.1 · 2026-07-26 · architecture draft. Revision history and the versioning rules are in §18.

Document map. This specification is normative; rationale and history live in re8-design-history.md.

Concern Sections
1. Machine-visible behaviour §4 CPU · §5 memory map & registers · §6 oito (VDP, arbitration, formats, raster, text) · §8 APU · §9 input · §10 interrupts
2. Electrical interfaces & BOM §2 BOM & power tree · §6.2–6.3 video bus & pins · §7 video outputs · §11 cartridge · §12 expansion header · §13 debug probe
3. Timing §3 clocks · §6.5 VRAM arbitration · §6.7 raster · §11.1.1 handoff state
4. File & tool ABI §11.0 ROM image · §11.2 header · §11.4 saves · §14 SDK & toolchain
5. Conformance & limits §15 restrictions · §16 conformance & validation
Revision §17 design history (separate document) · §18 version history

Diagrams accompanying this document are in diagrams/ and are referenced from the sections they illustrate.

Requirement keywords. MUST, MUST NOT, SHOULD and MAY are used in the RFC 2119 sense and only for requirements; all other prose is descriptive and carries no obligation. Performance figures carry either a stated workload or the label estimate; no cross-machine comparisons are made.

Scope.

Status (2026-07-26): architecture draft. The memory map, register file, subsystem behaviour and BOM are specified to a level intended to support emulator implementation and schematic capture. The design has been through two adversarial reviews, both resolved in full; the decision log and review registers live in re8-design-history.md.

Explicitly outstanding before compatibility freeze or tape-out (full criteria in §16.2):

Item
CH7035B qualification (custom 240p input, nearest-neighbour, latency, EDID-free operation)
AD725 240p validation (colour lock, burst, vertical lock)
prog8 ROM-ability validation (%option romable + build gate + fixture)
Typed-symbol source against the pinned compiler
Debug-info pipeline proof fixtures, per host
W65C02S instruction-jamming validation on silicon
Full 144-pin package table (pad types, per-state directions)
HDMI/DVI/S-PDIF licensing review by counsel
Closure package (register CSV, golden vectors, conformance ROMs)

1. System overview

re8 — SYSTEM BLOCK DIAGRAM W65C02S 7.159 MHz (master÷3) 3.3 V System SRAM IS61C256AL · 16 KB decoded 3.3 V Boot flash SST39VF010 · 128 KB 3.3 V Cartridge NOR 512 KB / 1 MB + FM18W08 FRAM (save) 3.3 V · 36-pin CPU BUS · A0–15 · D0–7 · R/W̄ · PHI2 oito SkyWater 130 nm · LQFP-144 1.8 V core / 3.3 V I/O ▸ VDP — 2 BG planes, 128 sprites ▸ mini-blitter, collision, text layer ▸ hardware cursor + hit-test ▸ 16-voice APU (PSG/wave/PCM) ▸ cartridge mapper + address decode ▸ PS/2 host + joypad acquisition ▸ PLL: CPU, dot, MCLK ▸ 2 KB boot stub (mask ROM) bus master during PCM fetch RDY / BE — wired-AND, open-drain XIN/XOUT 21.47727 MHz JTAG + DBḠ → expansion header 116 signal pins private bus VRAM IS61C1024AL · 128 KB 3.3 V · 8-bit · 21.5 MHz 12-bit RGB + syncs + DE R-2R ladder + THS7374 buffers 5 V CH7035B scaler → DVI-D 1080p60 1.8 V + 3.3 V AD725 NTSC composite + Y/C 5 V Outputs HDMI-A (DVI-D) SCART RGB DE-15 15 kHz RGBHV Composite (RCA) S-Video TOSLINK S/PDIF Line-out 3.5 mm 2× DB9 controller (PS/2 kbd/mouse via passive adaptor) DIT4192 + NJM4556 S/PDIF + line driver 3.3 V / 5 V I²S + MCLK · 2× PWM 74LVC244A · AHCT125 · BSS138 port front-end (5 V ↔ 3.3 V) 5 V / 3.3 V Expansion header (40-pin) → RP2350B debug probe accessory, ≤300 mA @3.3 V Power tree 9 V DC in → buck 5 V → buck 3.3 V → LDO 1.8 V (+ ferrite-filtered 1.8 V analog for oito and CH7035B PLLs) Voltage supervisor holds CPU RES̄, oito RES̄ and CH7035B RESETB until all rails are stable; re-asserts on brownout. 5 V · 3.3 V · 1.8 V ~3–5 W, passively cooled
re8 system block diagram

re8 is a boutique 2D game console built exclusively from currently-manufactured ("factory-fresh") components — no New Old Stock. Its architecture deliberately sits between the 8-bit era (NES, Master System, PC Engine) and early-90s arcade hardware (Capcom CPS-1):

Core philosophy.

  1. Zero-emulation: real silicon executes game code; no soft-cores or interpretation layers.
  2. No NOS: every part must be orderable new (this single rule drove the CPU, sound, video-encoder and flash choices).
  3. First-time-right silicon: oito is 100% digital; every analog function (video encoding, DACs, audio filtering) lives on the motherboard in proven off-the-shelf parts, because analog blocks on a low-volume ASIC are hard to verify and risky.
  4. ROM is cheap, RAM is precious: the SDK trades ROM space freely to preserve the 16KB RAM budget.

2. Bill of materials — active components

# Component Part Function Voltage Bus/interfaces
1 CPU WDC W65C02S6TPG-14 8-bit host processor 3.3V (datasheet AC table: 14MHz @5V, 8MHz @3.3V) 16-bit address, 8-bit data, PHI2, R/W̄, SYNC, RDY, BE, RES̄, IRQ̄
2 Custom ASIC oito (re8-vdp-8001, SkyWater 130nm, custom pad ring, LQFP-144, ~134–138 pins used (signal budget, not a sign-off pinout —)) VDP (tiles, sprites, blitter, collision, scroll) + 16-voice APU + cartridge mapper + address decode + joypad read 1.8V core / 3.3V I/O CPU bus slave; private VRAM bus master; 12-bit digital RGB + syncs out; PWM audio out; I²S out; 2× controller ports; JTAG
3 System RAM IS61C256AL (32K×8, 10 ns, 3.3V) with A14 tied low — only 16KB is decoded (32KB parts are far more available than 16KB). Alternative: 2× AS6C6264 (8K×8). A 55 ns part is an acceptable substitute (see timing note) CPU work RAM 3.3V CPU bus
4 VRAM IS61C1024AL (128K×8, 10 ns, 3.3V) — exact fit, 17 address lines matching VA0–VA16 Video memory 3.3V Private bus to oito only
5 Cartridge flash SST39VF040 (512KB, TSOP-32/PLCC-32) or SST39VF080 (1MB = 8Mbit×8, TSOP-40 — 20 address lines do not fit a 32-pin package). Both 3.0–3.6V, 70 ns, JEDEC program/erase. Same SST39 family as the boot flash, so one JEDEC driver serves cartridge, boot flash and updater. "Macronix" was a vendor name, not a selectable item — Game ROM (in cartridge) 3.3V Cartridge slot (D0–D7, A0–A13, BANK0–BANK5)
6 Boot ROM internal oito stub (~2KB on-die mask ROM, unbrickable) + external boot flash SST39VF010 (128KB parallel NOR, 3.3V, PLCC-32/TSOP-32) Two-stage boot firmware — stub → updateable firmware → cartridge (§11.1) on-die / 3.3V internal / CPU bus
7 Video encoder Analog Devices AD725 (provisionally selected — 240p operation requires bench validation) Analog RGB → NTSC composite + S-Video (Y/C) (the part's PAL mode is unused) 5V (§2.1) Analog RGB in (from R-2R DACs), CSYNC, subcarrier clock (14.318MHz NTSC / 17.734MHz PAL)
8 DVI transmitter Chrontel CH7035B 240p digital RGB → scaled 1080p60 DVI-D (video only, no audio) over HDMI-shaped connector (§7.1) 1.8V DVDD/AVDD_PLL + 3.3V VDDH/AVDD/VDDMQ/VDDMS/VDDIO 24-bit RGB-888 parallel in, fed by oito's 12 lines with bit replication + HSYNC/VSYNC + pixel clock; I²C slave (SPC/SPD); I²C master (SPCM/SPDM); TMDS out
8a Digital-audio transmitter TI DIT4192 (24-bit/192kHz I²S→S/PDIF/AES3 transmitter, active production) — replaces the WM8805 example, which Cirrus marks end-of-life and which therefore violated the console's founding no-NOS rule. Alternative kept on file: encode S/PDIF in a CPLD or in oito (biphase-mark is simple, but costs an ASIC pin and risk) APU I²S → S/PDIF (§8.4); royalty-free 3.3V I²S in; S/PDIF out
8b Optical transmitter current-production 3.3V TOSLINK transmitter module (Everlight PLT133 / Sharp GP1FAV31TK0F class), with its drive current and series resistor specified by the module datasheet electrical S/PDIF → optical out 3.3V S/PDIF in; TOSLINK out
9 Config ROM Chrontel CH9904 — an 8KB (8192×8) I²C boot ROM (not a sub-1KB device). Substitute: a generic 8-pin I²C EEPROM (24C64-class, 8KB) only if it matches address 0x57, the CH7035B master's byte/page-read protocol, and is readable before RESETB releases holds the CH7035B power-on register image; the configuration binary is a versioned release artefact 3.3V I²C at address 0x57 (to CH7035B master port)
9a CH7035B reference crystal 27.000 MHz crystal (+ load caps) scaler timebase on XI/XO CH7035B XI/XO
10 Controller port front-end per port: 1× 74LVC244A octal buffer (receive) + a bidirectional path on 2 lines (~4.7kΩ pull-ups to 5V + open-drain pull-down); shared: 1× SN74AHCT125 (5V-powered) driving both ports' SELECT. PS/2 lines: 4.7kΩ pull-up to 5V + 1kΩ series into the LVC receiver + BSS138 gate-driven pull-down (drain on the 5V line, source GND, gate from oito) + ≤10pF TVS — 244 buffers the 4 joystick-only data lines inbound (5V→3.3V, LVC inputs 5V-tolerant at 3.3V). SELECT is driven outbound by an SN74AHCT125 powered at 5V: a 3.3V LVC output cannot guarantee the ~3.5V CMOS high a 5V-powered 74HC157 requires, whereas AHCT has TTL-level inputs (V_IH 2.0V) and a ~4.9V CMOS output. The 2 PS/2-capable lines need bidirectional drive for the mouse-enable command and keyboard LEDs (§9.1/§9.2), which a unidirectional 244 cannot provide 3.3V rail, 5V-tolerant inputs DB9 data (6/port) + SELECT → oito; 2 lines also driven low by oito
11 Debug probe MCU (accessory, not in console) Raspberry Pi RP2350B (QFN-80, 48 GPIO, 3× PIO @150MHz) — RP2040 has only 30 GPIO and cannot reach the header's 36 signals Bus sniffing, breakpoints, instruction jamming, JTAG, USB bridge 3.3V (powered from header) 40-pin header; USB-C to host PC
11a Analog video buffers THS7374 (4-channel video amplifier, integrated reconstruction filters, 2× gain, single-supply) — one for SCART, one for VGA (RGB + CSYNC each) buffers the shared R-2R ladder so no destination double-loads it; 2× gain into a 75Ω series resistor gives standard 0.7 Vₚₚ into 75Ω 5V analog RGB + CSYNC in/out
11b AD725 input buffer unity-gain video buffer (triple) the AD725 needs ~0.714 V full-scale, AC-coupled with its own bias — a different requirement from driving a 75Ω line, so it cannot share the THS7374 outputs 5V ladder → AD725
11d Analog audio line driver NJM4556-class dual audio op-amp + 3rd-order active reconstruction filter (−3 dB @20 kHz), output coupling and mute control turns the APU's 2× delta-sigma PWM into a real line output: 1 Vᵣₘₛ, ≤100 Ω, separately series-fed to the 3.5 mm jack and SCART audio, muted until rails/APU are up 5V PWM in; line-out + SCART audio
11c SCART control drivers divider for pin 16 (fast-switch, 1–3V) + small transistor stage for pin 8 (status, 9.5–12V from the 9V input rail, signalling 4:3) forces RGB mode and reports aspect 5V / 9V SCART pins 8, 16
12 Power management 2× buck (5V, 3.3V) + 1× LDO (1.8V) + PLL filters + voltage-supervisor/reset IC (or a PMIC) — the former ~1.2V LDO is deleted; the CH7035B has no such mandatory core rail Rail generation, power-on reset, brownout (§2.1) 9V in

Notes:

2.1 Power tree, reset & protection

A single 3.3V rail was never feasible — the design has always needed 5V (AD725, controllers) and 1.8V (oito core); the CH7035B needs 1.8V and 3.3V domains. So the board is multi-rail. What survives is the digital core: CPU + oito + all SRAM/flash + glue share one 3.3V rail (no level shifters in the CPU/memory domain — the original benefit). The extra rails serve only the analog/video/core-voltage parts, which is normal.

Input: 9V DC barrel jack, ≥1.5A recommended (typical board draw ~3–5W, to be confirmed by measurement/synthesis). Protection: series reverse-polarity FET (or Schottky), input fuse/PTC, TVS on the jack.

Rails:

Rail Source Regulator Feeds
5V 9V buck AD725 + controller ports (DB9 pin 5). Accessory allowance: 250mA per port, 350mA total across both (not 2× the per-port figure — simultaneous maximum draw is not a design case). Protection: 500mA polyfuse per port as the trip target, distinct from the budget
3.3V (main) 5V (or 9V) buck oito I/O, W65C02S, system + VRAM SRAM, cartridge + boot flash, 74LVC244A, CH7035B VDDH/AVDD/VDDMQ/VDDMS/VDDIO (VDDIO = 3.3V because oito drives 3.3V RGB), glue, plus ≤300 mA for an attached debug probe
1.8V 3.3V LDO oito core; CH7035B DVDD. Sharing this rail with oito is permitted only after noise/current/sequencing analysis — the scaler's SDRAM interface is a switching load
1.8V analog (CH7035B AVDD_PLL) 1.8V via ferrite + caps filter CH7035B PLL — a clean supply, mirroring oito's PLL treatment
1.8V analog (oito VDDA_PLL) 1.8V via ferrite + caps filter oito PLL (clean supply for the pixel/core-clock PLL)

(A single multi-output PMIC could consolidate the bucks/LDOs to save board area — an implementation option.)

Sequencing & reset. A dedicated voltage-supervisor / reset controller holds CPU RES̄, oito RES̄ and CH7035B RESETB asserted until every rail is up and stable (plus a settle delay), and re-asserts on brownout.

Controller-port power policy. Typical draws: 3-button pad <5 mA, 6-button pad <10 mA, PS/2 mouse 20–50 mA, PS/2 keyboard 100–150 mA.

Thermal & protection: ~3–5W is passively cooled (no fan); the buck regulators and CH7035B want small copper pours / thermal vias. Connector ESD protection on the controller, cartridge and AV ports; cartridge-insertion current-limit.

Emulator note: power/reset is board-level and does not affect emulation; captured here for BOM correctness and to retire the "all-3.3V" misconception.


3. Clocks and frame timing

Clock Value Notes
Master clock (oito) 21.47727 MHz (= 6 × NTSC colorburst 3.579545 MHz) PINNED. NTSC-locked so the frame rate lands cleanly near 60Hz
CPU PHI2 master ÷ 3 = 7.15909 MHz PINNED. Synchronous to oito, fixed 3:1 ratio; fastest integer divisor under the W65C02S 8MHz@3.3V rating (~12% margin)
Dot clock, lo-res ~5.365MHz (341 dots/line, PLL) 256×224 active — §6.7
Dot clock, hi-res ~6.702MHz (426 dots/line, PLL) 320×224 active — §6.7
Frame rate 60.055Hz (fixed) progressive 240p, never 480i; 60Hz-only worldwide — no 50Hz PAL; HDMI re-timed to 1080p60 by CH7035B — §6.7
HSYNC rate 15.734kHz (both H-modes share it) standard NTSC 240p line rate
Lines per frame 262 total, 224 active, 38 VBLANK (lines 224–261)
Line duration 1,365 master ticks = 455 CPU cycles = 63.56µs 1365 ÷ 3 = 455 exactly
Frame duration 357,630 master ticks = 119,210 CPU cycles 262 × 1,365; VBLANK ≈ 51,870 ticks = 17,290 CPU cycles
CH7035B reference 27.000 MHz (dedicated crystal on XI/XO) the conventional reference for this class of converter; divides cleanly to standard pixel/TMDS rates incl. 148.5MHz for 1080p60. Deliberately independent of the 21.477MHz master — the scaler frame-buffers and re-times anyway (§6.7), so coupling them would buy nothing and risk noise
AD725 subcarrier 14.318MHz (NTSC) dedicated crystal to AD725 (regenerates color independent of master). The part also supports a 17.734MHz PAL subcarrier — an unused capability of the chip, not an re8 feature (60Hz-only).

Emulator-relevant derived facts: CPU runs at exactly master/3, so 455 CPU cycles per scanline and 119,210 per frame. VBLANK ≈ 17,290 CPU cycles (~2.4ms).

Determinism caveat. Those figures are an exact bus budget, not a guaranteed instruction budget: three mechanisms stall the CPU with RDY and reduce the cycles actually available to code — the CPU VRAM port under contention (§6.5), OAM DMA (§5.1), and PCM sample fetch (§8.3, ~3–6% when four voices stream). The line/frame timing remains exact and deterministic; the CPU's work per line varies with those stalls. Cycle-exact raster code should account for them or suspend PCM (§14.2). The exact line-total/porch/sync timing and the 60.00-vs-59.94 selection are a separate video-timing item needed only for a cycle-accurate emulator; the baseline above is sufficient to run software.


4. CPU subsystem (W65C02S)

Choice rationale. By 2026 the W65C02S and W65C816S are the only classic-bus CPUs still in production: Z80 survives only as the bus-incompatible eZ80; 68000 only as ColdFire. The 65C816 was evaluated and rejected: its 16-bit data bus + 24-bit address bus would have pushed oito past the (then-assumed) LQFP-64 pin budget. Note: the move to LQFP-144 (§6.3) weakens that pin-count argument — an 816's extra host lines would still fit — so the W65C02S choice now rests on the remaining reasons: a simpler 8-bit bus matching the SDK's memory model, and the fully static core that is the foundation of the debug strategy (clock to 0Hz with full state retention). Revisiting the CPU is not proposed here. (Marketing copy calling the W65C02S "the last standalone classic-bus CPU" is inaccurate — the W65C816S is also in production; see.)

Electrical/operating decisions:

Signals used by the system (pin mapping):

Signal Direction Used for
A0–A15 out 64KB address space; sniffed by probe (header pins 1–16)
D0–D7 bidir data bus; the only lines the probe drives during instruction jamming (BE stays high); header pins 17–24
PHI2 in system clock; probe can take over and single-cycle it
R/W̄ out (tri-stated by BE) write detect (probe uses it for ghost-port sniffing). oito drives it read-high while bus-mastering
SYNC out high during opcode fetch; probe breakpoint qualifier
RDY bidirectional, open-drain pulled low by the probe (breakpoints) and by oito (VRAM stalls, OAM DMA, PCM steals); the CPU itself drives it low during WAI — hence wired-AND with an external pull-up
BE in (open-drain drivers) pulled low by the probe or oito to tri-state the CPU off the bus; pull-up keeps buses enabled by default
RES̄ in system reset (also on debug header)
IRQ̄ in single interrupt line, driven by oito
NMĪ not used (never mentioned in any conversation)

Standard 6502 conventions apply: zero page $0000–$00FF, hardware stack $0100–$01FF, vectors at $FFFA–$FFFF (reset $FFFC/D, IRQ/BRK $FFFE/F).

Control-line termination. The modern W65C02S has no internal pull-up on RDY, and RDY is bidirectional because WAI drives it low. re8 has three parties on that line (CPU via WAI, oito for VRAM/OAM-DMA/PCM stalls, probe for breakpoints), so termination must be specified rather than assumed. "Unused" is a design statement, not an electrical connection — every unused input MUST be terminated.

Signal Termination
RDY open-drain, wired-AND, external ~10 kΩ pull-up to 3.3 V. oito and the probe may only pull low, never drive high — this is what lets the CPU's own WAI pull-low coexist with them (§10 recommends WAI, so this is not hypothetical)
BE external ~10 kΩ pull-up (buses enabled by default); oito and probe pull low. Fail-safe: an absent or unpowered probe cannot tri-state the CPU
IRQ̄ driven by oito; pull-up so a floating input cannot storm interrupts while oito is held in reset
NMĪ tied high through a pull-up — a floating NMI is a classic intermittent-crash source
SOB tied high (unused, must not float)
RES̄ driven by the supervisor (§2.1) with a pull-up, so the line is defined before the supervisor is alive

Implementation constraint: oito's RDY and BE pads are open-drain outputs, not push-pull (recorded for the pad-ring table, §6.3).

Note: an early section of the debug-probe design referenced a "HALT̄" pin; the later, corrected design uses RDY/BE/SYNC (the W65C02S has no HALT pin). The RDY/BE/SYNC version is normative.


5. Memory map

re8 — CPU ADDRESS SPACE (64 KB) Zero page$00–$1F runtime · $20–$7F prog8 · $80–$FF free $0000 Hardware stack $0100 General RAM ≈14.4 KB available to game code (after SDK: shadow OAM 1 KB, FIFOs, tables) 16 KB SRAM total — A14 tied low $0200 oito register file $4000–$40FF decoded (see right) $4FFF Ghost Debug Port all other addresses read $00, writes ignored $4000 Cartridge — switchable bank = BANK_SELECT masked to bank_count−1 also the save window when SAVE_CTRL.0 = 1 bank 0 begins with the 64-byte header $8000 Cartridge — fixed always bank_count − 1 (last physical bank) engine code + vectors $FFFA–$FFFF $C000 $FFFF Boot overlay — $E000–$FFFF BOOT_SRC 0 = internal 2 KB stub ×4 mirror BOOT_SRC 1 = boot-flash page (BOOT_BANK) BOOT_SRC 2 = disabled (one-way until RES̄) REGISTER FILE $4000–$40FF $4000 System / video / IRQ / boot $4010 Background scroll $4020 Blitter $4030 Collision $4040 VRAM port (auto-increment) $4050 Palette port (64 × 12-bit) $4060 Sprite / OAM + DMA $4070 Input · keyboard · port mode $4080 Mapper · save · cart config $4090 Text overlay + font port $40A0 APU indirect port $40B0 Mouse + hardware cursor $40C0 Hit-test / pick VRAM 128 KB (private) Plane A name-table4 KB · 64×32 cells Plane B name-table4 KB OAM 128×8 B1 KB (shadowed on-die) Text char-map4,480 B · 80×28 Tile / pattern space≈118 KB · up to ~3,700 tilesBG + sprite patterns +blitter dynamic tile pool All address arithmetic wraps mod 128 KB. Palette RAM is on-die, not in VRAM. Reset defaults: BOOT_SRC = 0 (stub mapped) · BOOT_FLASH_WE = 0 · SAVE_CTRL.1 = 1 (save write-protected) · all else zero. Cartridge geometry (bank count, save type/size) is programmed by boot firmware into CART_CONFIG_* and locked before handoff.
CPU address space, register blocks and VRAM layout

Canonical CPU address space (final iteration):

Range Size Region Notes
$0000–$00FF 256B Zero page $0000–$001F re8 runtime reservation (§5.0); $0020–$007F prog8 compiler variables; $0080–$00FF free
$0100–$01FF 256B Hardware stack
$0200–$3FFF ~15.5KB General RAM flattened globals, game state. (Address-range size; ≈14.4 KB (14,704 B) is actually available to game code after SDK allocations — budget in.)
$4000–$7FFF 16KB window oito register I/O space canonical register file at $4000–$40FF (§5.1); all unmapped space reads $00, writes ignored; $4FFF Ghost Debug Port
$8000–$BFFF 16KB Cartridge window 1 — bank-switchable driven by BANK0–BANK5 via oito mapper. Also the save window: while SAVE_CTRL.0 ($4081) is set this range addresses the cart's FRAM instead of ROM
$C000–$FFFF 16KB Cartridge window 2 — fixed cartridge code + vectors at $FFFA–$FFFF (after boot handoff). At power-on $E000–$FFFF is overlaid per BOOT_SRC (internal stub → external firmware → disabled) — §11.1

5.0 Zero-page allocation

Nothing in oito or the CPU decodes zero page$0000–$00FF is ordinary system SRAM. The source re8.properties comment calling $0000–$001F "reserved for internal chip logic" was a misnomer; it is a software convention: the re8 runtime and prog8 boot stub reserve these 32 bytes for fast (single-byte-address) variables, and prog8's allocator is told to start user/compiler variables at $0020 (zeropage_start=0020). For an emulator this needs no special handling — it is plain RAM.

Runtime reservation $0000–$001F (illustrative; finalized when the library is written):

Addr Use
$00–$01 IRQ dispatcher scratch
$02 event/VBLANK flags (bit0 = VBLANK since last poll)
$03–$04 frame counter (16-bit)
$05–$06 joypad-1 / joypad-2 previous-state shadow (software edge detection)
$07–$09 VRAM auto-increment pointer shadow (mirrors $4040–$4042)
$0A cartridge bank shadow (mirrors $4080, write-mostly register)
$0B–$0F runtime indirect-addressing pointers (ptr0, ptr1 — 2 bytes each) + RNG seed
$10–$13 debug/REPL mailbox [command_id, arg0, arg1, arg2]
$14–$15 IRQ handler RAM vector — the syslib IRQ trampoline calls through this cell (pushed return + JMP ($14)); sys.set_irq() updates it
$16–$1F reserved for future runtime use

$0020–$007F = prog8 compiler zero-page variables (96 bytes). $0080–$00FF = free zero page (128 bytes) for hand-assigned fast variables. A pure bare-metal program that does not link the re8 runtime may reclaim $0000–$001F.

System RAM budget. The compile-time overflow check (§14.1) is only meaningful if every consumer of the 16 KB is declared. Several were added after that promise was written — notably the 1 KB shadow-OAM buffer that the shadow-OAM design makes mandatory — so the real figure is published here:

Consumer Size Owner
Zero page — runtime reservation ($00–$1F) 32 B SDK
Zero page — prog8 compiler variables ($20–$7F) 96 B compiler
Zero page — free for hand-assignment ($80–$FF) 128 B user
Hardware stack ($0100–$01FF) 256 B CPU
Shadow-OAM staging buffer 1,024 B SDK (only when re8.sprites is linked)
Keyboard FIFO + layout state ~64 B SDK
Callback tables (input / collision / mouse) ~64 B SDK
Bank stack (8-deep) + IRQ state ~16 B SDK
ROM→RAM initialised data variable compiler
Available to game globals/BSS 14,704 B ≈ 14.4 KB user

5.1 oito register map

The window $4000–$40FF is the register file (low 8 bits decode the register); $4FFF is the deliberately-isolated Ghost Debug Port. Everything unmapped follows the single rule below. All multi-byte fields are little-endian. R = read-only, W = write-only, R/W = both. Unlisted addresses read $00 and ignore writes — see the unmapped-access table below.

Base-register width. Every "2KB units" base register (BG0/BG1_MAP_BASE, BG0/BG1_PAT_BASE, OAM_BASE, SPR_PAT_BASE, TEXT_MAP_BASE) is 8-bit but VRAM is 128KB = 64 × 2KB, so only bits 0–5 are significant; bits 6–7 are not wired — ignored on write, read back as 0. Bases therefore always land inside VRAM (no undefined aliasing), and hardware and emulator agree by construction. CURSOR_TILE_LO/HI is a 12-bit tile index (matching the name-table/OAM index width, §6.6); its VRAM byte address is index × 32.

Design conventions. The CPU never touches VRAM or palette RAM directly — both are reached through auto-incrementing address/data ports. OAM (sprite attributes) and name-tables live in VRAM; a small dedicated palette RAM (64×12-bit) lives inside oito and has its own port. IRQ status bits are write-1-to-clear.

Block layout ($40xx):

Range Block
$4000–$400F System / video control & status / IRQ
$4010–$401F Background scroll
$4020–$402F Blitter
$4030–$403F Collision
$4040–$404F VRAM access port
$4050–$405F Palette access port
$4060–$406F Sprite / OAM control
$4070–$407F Input (joypads)
$4080–$408F Cartridge mapper
$4090–$409F Text overlay layer (§6.8)
$40A0–$40AF APU audio port (16-voice APU — §8)
$40B0–$40BF Mouse & hardware cursor (§9.2)
$40C0–$40CF Hit-test / pick (§9.2)
$4FFF Ghost Debug Port

System / video / IRQ ($4000–$400F):

Addr Name R/W Bits
$4000 VIDEO_CTRL W 0=screen enable · 1=resolution (0=256×224, 1=320×224) · 2=Plane A (BG0) enable · 3=sprite enable · 4 = reserved (was "render mode"; deleted — the blitter writes tiles into VRAM and is not a display layer, so the bit had no defined visible effect) · 5=Plane B (BG1) enable · 6–7 reserved
$4001 IRQ_ENABLE W 0=VBLANK · 1=collision · 2=joypad-change · 3=raster-compare · 4=blitter-done · 5–7 reserved
$4002 IRQ_STATUS R / W1C read: pending bits (same bit order as IRQ_ENABLE). Write-1-to-clear each bit (acknowledge). IRQ̄ = NOT(any(IRQ_STATUS & IRQ_ENABLE)), combinational; masked events still set status; same-cycle set beats W1C-clear (full state machine in §10)
$4003 STATUS R / bit3 W1C 0=in VBLANK · 1=in HBLANK · 2=blitter busy · 3=sprite overflow — reports the completed frame; set on any line where a 33rd sprite was dropped; cleared at the start of the next active frame (line 0) or by writing 1 · 4–7 reserved
$4004 RASTER_CMP W scanline number (0–261) that raises the raster-compare IRQ (low 8 bits; bit 8 in $4005)
$4005 RASTER_CMP_HI / CUR_LINE_HI R/W write: raster-compare bit 8. read: current scanline bit 8
$4006 CUR_LINE R current scanline, low 8 bits
$4007 BOOT_CTRL R/W bits0–1 = BOOT_SRC — what occupies the $E000–$FFFF overlay: 0 = internal 2KB stub, mirrored 4× (reset default; $FFFC/D is the stub's reset vector) · 1 = external boot-flash page selected by BOOT_BANK · 2 = overlay disabled, cartridge fixed bank visible · 3 = ignored. 0↔1 may be switched freely during boot (firmware validation and recovery require it); writing 2 is one-way until RES̄. bit2 = BOOT_FLASH_WE — boot-flash write enable; cleared at reset, and while 0 oito never asserts BOOT_W̄Ē, so no stray write can program the firmware. The updater sets it immediately before erase/program and clears it after (same idiom as SAVE_CTRL's write-protect). bits3–7 reserved
$4008 BOOT_BANK R/W bits0–3 = which 8KB page of the external boot flash appears at $E000–$FFFF while the overlay is enabled (16 pages → 128KB; reset = 0 = resident page). Applies only while BOOT_SRC = 1; inert otherwise
$4009–$400F reserved

Background scroll ($4010–$401F):

Addr Name R/W Notes
$4010 BG0_SCROLL_X_LO W Plane A X scroll bits 0–7
$4011 BG0_SCROLL_X_HI W Plane A X scroll bit 8 (9-bit, wraps over the 512px-wide map)
$4012 BG0_SCROLL_Y W Plane A Y scroll, 8-bit (0–255, wraps over the 256px-tall map)
$4013 reserved Y scroll is 8-bit — there is no bit 8 (the map is 256px tall). Writes ignored, reads 0
$4014 BG0_MAP_BASE W Plane A name-table base, 2KB units (§6.6)
$4015 BG0_PAT_BASE W Plane A tile pattern base, 2KB units
$4016–$4019 BG1_SCROLL_* W Plane B X (9-bit) / Y (8-bit) scroll — same layout as $4010–$4013, with $4019 reserved
$401A BG1_MAP_BASE W Plane B name-table base, 2KB units
$401B BG1_PAT_BASE W Plane B tile pattern base, 2KB units
$401C–$401F reserved

Blitter ($4020–$402F) — operates VRAM→VRAM on the private bus (never touches the CPU bus):

Addr Name R/W Notes
$4020 BLIT_SRC0/1/2 W source VRAM address, 17-bit ($4020 lo, $4021 mid, $4022 bit0=A16)
$4023 BLIT_DST0/1/2 W destination VRAM address, 17-bit ($4023/$4024/$4025)
$4026 BLIT_W W width (tiles or pixels — unit set by BLIT_MODE)
$4027 BLIT_H W height
$4028 BLIT_MODE W bits0–1 = operation enum (0 = copy · 1 = transparent copy, per-nibble skip of index 0 · 2 = fill · 3 = masked) · bit2 = unit (0 = tiles, 1 = pixels) · bit3 = interleave during active display (else blanking-only, §6.5) · bits4–7 reserved. (Was an ambiguous mix of enum values and flags.)
$4029 BLIT_FILL W fill colour index in the low nibble; high nibble ignored; the byte written is the nibble duplicated
$402A BLIT_CTRL R/W write 1 → start (ignored while busy); read bit0 = busy (mirrors STATUS.2)
$402B BLIT_SRC_PITCH W bytes added to the source address per row (0 = contiguous)
$402C BLIT_DST_PITCH W bytes added to the destination address per row (0 = contiguous)
$402D–$402F BLIT_MASK_SRC W 17-bit VRAM address of the 1bpp mask used by masked mode (1 bit/pixel, MSB = leftmost)

Collision ($4030–$403F):

Addr Name R/W Notes
$4030 COLLIDE_STATUS R 0=collision latched · 1=overflow (≥2 pairs since last clear)
$4031 COLLIDE_A R first colliding sprite ID (0–127)
$4032 COLLIDE_B R second colliding sprite ID
$4033 COLLIDE_CLEAR W write 1 → clear latch and re-arm

VRAM access port ($4040–$404F) interface:

Addr Name R/W Notes
$4040/$4041/$4042 VRAM_ADDR0/1/2 W 17-bit pointer ($4042 bit0 = A16)
$4043 VRAM_CTRL W 0=auto-increment enable · 1=direction (0=+, 1=−) · 2–4=stride select, all eight values defined: 0→1, 1→2, 2→4, 3→8, 4→32, 5→64, 6→128, 7→256 · 5–7 reserved
$4044 VRAM_DATA R/W read or write the byte at the pointer; pointer post-increments per VRAM_CTRL. Full semantics in the note below.

VRAM port semantics. This is the CPU's only path to VRAM, so its behaviour is pinned rather than sketched:

Palette access port ($4050–$405F) — dedicated internal 64×12-bit palette RAM:

Addr Name R/W Notes
$4050 PAL_INDEX W entry 0–63 (4 sub-palettes × 16). Masked to 6 bits; bits 6–7 ignored, read back 0
$4051 PAL_DATA_LO R/W color bits: 7–4 = G[3:0], 3–0 = B[3:0]. Writing LO does not increment
$4052 PAL_DATA_HI R/W 3–0 = R[3:0]; writing HI post-increments PAL_INDEX (wrapping 63 → 0); reading HI does not increment. Color = R<<8 | G<<4 | B (12-bit 4:4:4)

Palette port semantics.

Sprite / OAM control ($4060–$406F) — OAM lives in VRAM; fast-path DMA from CPU RAM:

Addr Name R/W Notes
$4060 OAM_BASE W OAM base in VRAM, 2KB units (§6.6)
$4061 OAM_DMA_SRC W 1KB block number in bits 0–3 (0–15): source address = block << 10. Bits 4–7 ignored, read 0. All 16 values are valid, since 16KB of RAM is exactly sixteen 1KB blocks. Writing this register triggers the OAM DMA burst CPU-RAM → VRAM OAM
$4062 OAM_DMA_CTRL R/W read bit0 = DMA busy
$4063 SPR_PAT_BASE W sprite tile pattern base, 2KB units
$4064 OAM_DMA_LEN W transfer length in 8-byte OAM entries: 0 = all 128; 1–128 = that many; 129–255 clamp to 128
$4065 OAM_DMA_OFS W starting OAM entry index (0–127) for the transfer; source and destination advance together, so the burst copies LEN entries from src_block + OFS×8 into OAM + OFS×8. OFS+LEN past entry 127 wraps within OAM
$4066–$406F reserved

Input ($4070–$407F) — pad state latched inside oito (§9); active-high (1 = pressed):

Addr Name R/W Notes
$4070 JOYPAD_1 R 0=Up 1=Down 2=Left 3=Right 4=A 5=B 6=C 7=Start
$4071 JOYPAD_1_EXT R 0=X 1=Y 2=Z 3=Mode (6-button pads) · 4–7 reserved
$4072 JOYPAD_2 R as JOYPAD_1, port 2
$4073 JOYPAD_2_EXT R as JOYPAD_1_EXT, port 2
$4074 INPUT_STATUS R/W1C 0=port1 changed · 1=port2 changed · 2=keyboard scan-code available (§9.1) · 3=mouse event (move, button change or wheel — read MOUSE_STATUS $40B0 to distinguish; §9.2) · 4–7 reserved. Write-1-to-clear. All four are always latched here (so any source can be polled); which of them actually raise the input-change IRQ (IRQ_STATUS bit 2) is selected by INPUT_IRQ_MASK
$407C PORT_MODE R/W bits0–1 = port 1 mode, bits2–3 = port 2 mode: 0 = joystick (reset default), 1 = PS/2, 2–3 reserved. oito's open-drain PS/2 transmitters are released only in PS/2 mode — the non-circular form of the joystick-safety invariant. Set by boot firmware (persisted in boot flash) or by the SDK
$407B INPUT_IRQ_MASK W per-source IRQ enable, same bit order as INPUT_STATUS (0=port1 · 1=port2 · 2=keyboard · 3=mouse). Reset 0 = poll-only. Lets a game take joypad IRQs but poll the mouse each frame instead of being interrupted at its 40–200Hz report rate — without hiding the cursor, since display enable (CURSOR_CTRL.0) and interrupt enable are deliberately separate concerns
$4075 KBD_STATUS R 0=keyboard present · 1=scan-code available (FIFO non-empty) · 2=FIFO overflow (sticky, write 1 to clear) · 3=keyboard on port 2 (else port 1) · 4=PORT_HINT port 1, 5=PORT_HINT port 2 — a valid unsolicited PS/2 frame (e.g. BAT $AA) was received while the port is still in joystick mode, i.e. a PS/2 device may be attached; receive-only, nothing is ever driven · 6–7 reserved
$4076 KBD_SCAN R pop next raw Set-2 make/break byte from the 16-byte FIFO (0 if empty); reading advances it. On overflow new bytes are dropped (never overwriting a partially-consumed sequence) and KBD_STATUS.2 sets
$4077 KBD_MODS R live modifiers/locks: 0=Shift · 1=Ctrl · 2=Alt · 3=CapsLock · 4=NumLock
$4078 KBD_CTRL W 0=keyboard enable · 1=key→joypad passthrough enable · 2=passthrough target (0=player 1, 1=player 2) · 3–7 reserved
$4079 KBD_MAP_IDX W key→joypad map slot: bits0–6 = scan code, bit7 = E0 extended prefix256 slots covering plain and extended codes. Without the flag, E0 4B (right arrow) and 4B would collide — and the arrow keys are precisely what the injection maps. Firmware loads defaults at boot
$407A KBD_MAP_VAL R/W joypad assignment for the selected slot: bits0–3 = bit index (0–7), bit4 = target _EXT register (so X/Y/Z/Mode are reachable); $00 = unmapped

Cartridge mapper ($4080–$408F):

Addr Name R/W Notes
$4080 BANK_SELECT R/W 6-bit bank (0–63) for the $8000–$BFFF window. (Moved from gen-C's $4020; the SDK hides the address, and extsub codegen must emit STA $4080.)
$4082 CART_CONFIG_BANKS R/W bits0–2 = bank-count log2 (0–6 → 1…64 × 16KB); bits3–6 reserved; bit7 = lock status (read-only mirror of CART_LOCK). Programmed by boot firmware from header $16. Reset 0 = a 1-bank/16KB cartridge
$4083 CART_CONFIG_SAVE R/W bit0 = has-save · bits1–2 = save type · bits3–5 = save-size code — the header $17 values, loaded by boot firmware. SAVE_CTRL is honoured only when bit0 is set
$4084 CART_LOCK W write $A5 once → CART_CONFIG_BANKS/CART_CONFIG_SAVE become read-only until RES̄. Boot firmware locks them before the cartridge handoff, so a game cannot restate its own geometry
$4081 SAVE_CTRL R/W 0 = save-window enable (accesses to $8000–$BFFF assert SAVE_CĒ instead of ROM_CĒ — §11.4) · 1 = write-protect (reset = 1) · 2–3 = size code (mirrors header $17) · 4–7 reserved. Honoured only when CART_CONFIG_SAVE.0 (has-save) is set — oito learns that bit from firmware, not from the header

Text overlay ($4090–$409F) — the top-priority character layer (§6.8); the char map lives in VRAM, glyphs in on-die font RAM reached through an indirect font port:

Addr Name R/W Notes
$4090 TEXT_CTRL W 0=layer enable · 1=narrow (0=8×8/40-col, 1=4×8/80-col) · 2=font_sel (0=default bank, 1=cart bank) · 3=blink enable · 4=caret enable · 5–6=sub-palette select (0–3) · 7 reserved
$4091 TEXT_MAP_BASE W char-map base in VRAM, 2KB units; the map is a fixed 80×28 grid, 160-byte row stride, 4,480 B total in every mode
$4092 TEXT_SCROLL_X W whole-plane fine X offset (pixels)
$4093 TEXT_SCROLL_Y W whole-plane fine Y offset (pixels)
$4094 TEXT_CARET_X W hardware caret cell column (text layer; distinct from the mouse CURSOR_* block)
$4095 TEXT_CARET_Y W hardware caret cell row
$4096 FONT_ADDR_LO W font-RAM address bits 0–7
$4097 FONT_ADDR_HI W bits0–3 = font-RAM address bits 8–11 (3KB bank: $000–$7FF = 8×8 set, $800–$BFF = 4×8 set) · bits4–6 reserved · bit7 = write-bank select: 0 = default bank 0, 1 = cart bank 1 — independent of TEXT_CTRL.font_sel, which selects the display bank. Bank-0 writes are honoured only while the boot overlay is active (BOOT_SRC ≠ 2); after handoff they are silently ignored
$4098 FONT_DATA W write glyph byte at [FONT_ADDR]; auto-increments (upload cart font from cart ROM)

Mouse & hardware cursor ($40B0–$40BF) — §9.2; oito accumulates position from PS/2 deltas and renders the cursor; the game reads state + sets the cursor graphic:

Addr Name R/W Notes
$40B0 MOUSE_STATUS R 0=mouse present · 1=moved · 2=button changed · 3=wheel moved — bits 1–3 clear on a read of this register, not on X/Y/button reads · 4–7 reserved
$40B1 MOUSE_X_LO R cursor X (at hotspot), bits 0–7. Reading this latches X_HI and Y into a shadow, so LO→HI→Y always returns one coherent position
$40B2 MOUSE_X_HI R cursor X bit 8 (0–319)
$40B3 MOUSE_Y R cursor Y (0–223)
$40B4 MOUSE_BUTTONS R 0=Left · 1=Right · 2=Middle · 3=btn4 · 4=btn5
$40B5 MOUSE_WHEEL R signed wheel delta accumulator, saturating at ±127; clears on read
$40B6 CURSOR_CTRL W 0=cursor enable · 1=size (0=8×8, 1=16×16) · 2–3=sub-palette · 4–5=pointer source (0=auto: mouse-if-present else D-pad fallback · 1=mouse only · 2=D-pad fallback only · 3=off) · 6=bounds enable · 7=fallback player (0=P1, 1=P2)
$40B7 CURSOR_TILE_LO W cursor pattern tile index bits 0–7 (VRAM byte address = index × 32)
$40B8 CURSOR_TILE_HI W tile index bits 8–11 (12-bit index, as name-table/OAM); bits 4–7 ignored. In 16×16 mode the index addresses the first of four consecutive tiles and its low 2 bits are ignored (quad must be 4-aligned)
$40B9 CURSOR_HOTSPOT_X W hotspot pixel within tile (0–15)
$40BA CURSOR_HOTSPOT_Y W hotspot pixel within tile (0–15)
$40BB CURSOR_SCALE W velocity/sensitivity, 4.4 fixed-point (1.0 = $10; 0 is treated as 1.0); bit 7 = acceleration enable (
$40BC CURSOR_BOUND_SEL W bits0–2 select the bounds field: 0=X0_LO · 1=X0_HI (bit 8) · 2=Y0 · 3=X1_LO · 4=X1_HI (bit 8) · 5=Y1; 6–7 unused. Six fields give real 9-bit X without new addresses
$40BD CURSOR_BOUND_VAL W value written to the field selected by CURSOR_BOUND_SEL. Bounds are inclusive; an invalid rectangle (X1 < X0 or Y1 < Y0) disables clamping rather than trapping the cursor

Hit-test / pick ($40C0–$40CF) — §9.2; oito samples the compositor at the cursor hotspot (or PICK_X/Y) once per frame and latches what's under the pointer (cursor/text layers excluded):

Addr Name R/W Notes
$40C0 HIT_STATUS R 0=sprite under point · 1=plane A opaque · 2=plane B opaque · 3–4 = visible winner (0=backdrop, 1=plane B, 2=plane A, 3=sprite) · 5=sample valid — set when the raster passes the pick point, cleared at the start of each frame · 6–7 reserved
$40C1 HIT_SPRITE R topmost opaque sprite index under point (0–127; 0xFF = none)
$40C2 HIT_A_COL R plane A name-table column under point (0–63; scroll/wrap applied)
$40C3 HIT_A_ROW R plane A name-table row (0–31)
$40C4 HIT_A_TILE R plane A tile ID bits 0–7 (high nibble in HIT_TILE_HI)
$40C5 HIT_B_COL R plane B column under point
$40C6 HIT_B_ROW R plane B row
$40C7 HIT_B_TILE R plane B tile ID bits 0–7 (high nibble in HIT_TILE_HI)
$40C8 PICK_CTRL W 0=pick source (0=follow cursor hotspot, 1=use PICK_X/Y) · 1–7 reserved
$40C9 PICK_X_LO W probe X bits 0–7 (when PICK_CTRL.0=1)
$40CA PICK_X_HI W probe X bit 8
$40CB PICK_Y W probe Y (0–223)
$40CC HIT_TILE_HI R high nibbles of both hit tile IDs — bits0–3 = plane A tile[11:8], bits4–7 = plane B tile[11:8]. With HIT_A_TILE/HIT_B_TILE this reports the full 12-bit index

APU — audio port ($40A0–$40AF) — the 16-voice APU (§8) is reached through an indirect port into an internal 16-bit-addressed register file + wave RAM; the CPU-visible footprint is tiny:

Addr Name R/W Notes
$40A0 AUDIO_ADDR_LO W internal APU address bits 0–7
$40A1 AUDIO_ADDR_HI W internal APU address bits 8–15
$40A2 AUDIO_DATA R/W read/write internal register or wave RAM at [AUDIO_ADDR]; auto-increments per AUDIO_CTRL
$40A3 AUDIO_CTRL W 0 = auto-increment enable · 1 = APU master enable · 2–7 reserved
$40A4 AUDIO_STATUS R 0 = any PCM DMA busy · 1–7 reserved

Ghost Debug Port:

Addr Name R/W Notes
$4FFF GHOST_LOG W write-only; no storage; 4 CPU cycles per write; snooped by probe/emulator; inert otherwise (reads $00; a write with no probe/emulator attached has no effect)

Unmapped reads & writes. One rule, no open bus, no mirroring:

Range / case Read returns Write
Unlisted register inside $4000–$40FF $00 ignored
Read of a write-only register $00
$4100–$4FFE $00 ignored
$4FFF (Ghost Debug Port) $00 consumed by probe/emulator (§13)
$5000–$7FFF $00 ignored

Rationale. Open bus (returning the last value on the data bus) is period-authentic but timing-dependent, and is historically the largest single source of emulator divergence on comparable systems; oito is synchronous and can simply drive $00, which is free to implement, deterministic, and trivially conformance-testable. Mirroring the register file across $5000–$7FFF was also rejected: it would invite games to depend on aliases and permanently constrain register-space expansion.

Consequence. A write-only register cannot be read back, so software must shadow any value it needs to read-modify-write. The runtime already keeps ZP shadows for BANK_SELECT and the VRAM pointer (§5.0), and the SDK does the same for VIDEO_CTRL.

Reset state. All registers clear to 0 at RES̄ with one non-zero exception: SAVE_CTRL.1 = 1 (save write-protect engaged — §11.4). Two zero values are load-bearing rather than incidental and are called out for clarity: BOOT_CTRL.BOOT_SRC = 0 selects the internal boot stub and BOOT_CTRL.BOOT_FLASH_WE = 0 disables boot-flash writes. (CURSOR_CTRL source = 0 was previously listed as an "exception" although 0 is the general reset value — corrected.) Everything else is zero: screen off, all IRQs disabled and clear, blitter idle, scroll 0, bank 0, BOOT_BANK 0, cartridge geometry unprogrammed (CART_CONFIG_* = 0, unlocked), save window closed, boot-flash writes disabled (BOOT_FLASH_WE=0), PSG muted, text and cursor layers off. The boot code must enable video, program the palette, load VRAM, and enable IRQs before anything is displayed.


6. oito (custom ASIC)

6.1 Functions

oito integrates, on one 130nm die:

  1. Video Display Processor — hybrid architecture: - Tile-based line renderer (NES/Genesis lineage): 8×8 tiles, tile grids 32×28 (lo-res) / 40×28 (hi-res); two independent background planes (Plane A / Plane B), each a 64×32-cell name-table (512×256px) with pixel-perfect scrolling (9-bit X, 8-bit Y). Full data formats, plane priority and VRAM layout are in §6.6. - Mini-blitter: writes raw pixels into a "Dynamic Tile Pool" region of VRAM, sharing the single private VRAM bus at priority 2 (behind display fetch — §6.5; it is not a separate bus); throughput ≥1,000 tiles/frame interleaved (≥350 blanking-only) under the §6.5 benchmark workload; a full 320×224 background is 1,120 tiles, so a whole-screen redraw per frame is possible only under the §6.5 benchmark workload, and sits just above the ≥1,000 floor.
  2. Sprites — 128 hardware-tracked sprites, up to 32 per scanline, evaluated from an on-die shadow OAM with a line-buffer cache. Within that per-line budget rendering is flicker-free; the 33rd and beyond are dropped for that line — "zero flicker" is a property of staying inside the budget, not an unconditional guarantee.
  3. Hardware collision — compares opaque sprite pixels during line rendering (index 0 ignored); on overlap latches both sprite IDs into COLLIDE_A/B and asserts IRQ. Latch/overflow/priority semantics are defined in §6.6.
  4. Color — 4-bit indexed pixels; 12-bit master palette (4:4:4 = 4,096 colors); 4 sub-palettes × 16 colors = 64 simultaneous; palettes 0–1 backgrounds, 2–3 sprites/blitter; color 0 transparent.
  5. APU — a 16-voice audio unit (4 enhanced PSG + 8 wavetable + 4 PCM), full detail in §8; outputs (a) dual high-speed PWM lines (stereo) for the analog path and (b) I²S digital audio (+MCLK) to the S/PDIF transmitter and an optional on-board DAC — not to the CH7035B, whose audio inputs are unconnected.
  6. Cartridge mapper — a console-side fixed-plus-switchable 16KB mapper: the bank register drives BANK0–BANK5 (64 × 16KB = 1MB addressable), swapping the $8000–$BFFF window in under a clock cycle while $C000–$FFFF stays fixed. Banking model resembles UxROM (fixed + switchable PRG window); it implements none of MMC3's PRG modes, CHR banking, scanline IRQ or serial write protocol — raster interrupts come from the VDP's RASTER_CMP instead (§5.1). The distinctive choice is that banking lives in the console, not the cartridge, which is why carts are one chip.
  7. Address decoding / glue — decodes the $4000+ I/O window; drives the DB9 SELECT lines and reads the (always-enabled 74LVC244A-translated) controller data directly (§9).
  8. JTAG — boundary scan into VDP internals (palette state, line-buffer flags, blitter status), independent of the CPU-side debug path. (Standard IEEE 1149.1 boundary scan only exposes I/O-cell state; reading internal registers requires custom JTAG instructions/data registers — a debug TAP — that were never specified. See.)

6.2 Video output bus (all-digital rule)

oito never produces analog video. It outputs:

Rationale: NTSC/PAL subcarrier generation on unproven low-volume silicon was judged too risky (an off-by-fraction error yields rolling B&W pictures); analog encoding is delegated to the AD725 (a proven, in-production encoder).

6.3 Pin budget — LQFP-144

The original LQFP-64 tally was unbuildable. The 64-pin package is abandoned; oito targets LQFP-144 (0.5mm pitch, 20×20mm, standard OSAT part) on a custom pad ring (no Efabless Caravel harness — see §6.4). The complete signal budget, counting every interface the design actually requires:

Interface Signals Pins
6502 host bus A0–A15 (16, bidirectional — oito drives them when bus-mastering, §8.3), D0–D7 (8, bidir), PHI2, R/W̄ (bidirectional — BE tri-states the CPU's RWB too, so oito must drive read-high during steals), IRQ̄, RDY, BE 29
Private VRAM bus (128KB, 8-bit data) VA0–VA16 (17), VD0–VD7 (8), /CE, /OE, /WE 28
CPU-bus memory control (oito is the address decoder, §6.1) RAM_CĒ, RAM_ŌĒ, RAM_W̄Ē (16KB system SRAM), BOOT_CĒ, BOOT_ŌĒ, BOOT_W̄Ē (boot flash program/erase) 6
Video output RGB 4:4:4 (12), HSYNC, VSYNC, CSYNC, DE, PCLK 17
Audio output PWM L/R (2), I²S BCLK/LRCLK/SDATA + MCLK (4) 6
Cartridge mapper BANK0–BANK5 (6), ROM_CĒ, SAVE_CĒ (§11.4) 8
Controllers (data into oito) 2 ports × (6 data + 1 SELECT) 14
Clock + reset XIN, XOUT, RES̄ 3
JTAG + debug TCK, TMS, TDI, TDO, DBḠ (probe debug-request; §13) 5
Signal subtotal 116
Power / ground (derivation below) I/O VDD/VSS pairs (~8), core VDD/VSS (~2 pairs), PLL VDDA/VSSA (1 isolated pair) ~18–22
Total ~134–138 (of 144)

6–10 pins spare — adequate for ECO headroom, not room for another subsystem.

This is a signal budget, not a sign-off pinout. It answers one question — does the design fit LQFP-144? — and the answer is yes. It is not a pad-ring specification. Power/ground is derived, not measured: ≥1 VDD/VSS pair per ~8 simultaneously-switching outputs (~60 outputs → ~8 I/O pairs), plus separate core pairs, plus an isolated PLL VDDA/VSSA pair behind its own ferrite. The true count depends on the foundry I/O cell library, ESD clamp strategy, SSO current, bond-wire inductance and lead assignment — none of which are available until the fab route of §6.4 is engaged.

Owed before tapeout (deliverable, not an estimate): a complete 144-pin table giving, per pin — number, name, direction in every bus-ownership state (normal / oito bus-master / probe bus-master / debug), pad type (open-drain for RDY and BE), drive class, I/O bank supply, corner cells, no-connects, and the exposed-pad decision. (This is the reconciled count.)

Key resolutions captured here:

6.4 Production strategy (context)

RTL synthesized with open-source OpenLane against the open SkyWater 130nm PDK. The Efabless Caravel harness / chipIgnite base flow is abandoned (decision 2026-07): its ~38 user I/O cannot carry oito's 116 signal pins (§6.3), its base shuttle delivers ~100 QFN prototypes rather than a low-thousands LQFP-144 supply, and its RISC-V management core is dead weight in this console. oito therefore needs a custom pad ring and its own LQFP-144 package, taped out on SKY130 as a full-custom (bring-your-own-pad-ring) project — through a commercial MPW/fab engagement rather than the stock Caravel wrapper.

Open: which specific full-custom SKY130 route/foundry/shuttle, and its real NRE, per-unit cost and deliverable quantity. The earlier Caravel-based figures (~$115K NRE, ~$4.50/unit at 5K, 1,000–5,000 parts/run) are now stale — a custom pad ring + LQFP-144 raises NRE and per-unit cost, to be re-quoted (economics is out of scope for this spec).

6.5 VRAM bus arbitration & bandwidth

re8 — PIXEL PRIORITY LADDER & VRAM BUS ARBITRATION Per-pixel priority — topmost opaque pixel wins 1 · Hardware mouse cursorabsolute top 2 · Text overlay layer1bpp, transparent by default 3 · Sprite, priority 1 4 · Plane A, priority 1 5 · Plane B, priority 1 6 · Sprite, priority 0 7 · Plane A, priority 0 8 · Plane B, priority 0 9 · Backdrop — sub-palette 0, entry 0 Colour index 0 is transparent in every sub-palette. Palette RAM is sampled per pixel, so mid-scanline palette writes take effect on the very next pixel. Per line: OAM is scanned 0→127 from the on-die shadow; the first 32 covering sprites are kept, the 33rd onward dropped (lower index wins and draws on top). VRAM bus — one 8-bit bus at 21.477 MHz, fixed priority 1 · Display fetch always wins — the picture is never corrupted 2 · Blitter blanking-only, or interleaved into spare cycles 3 · CPU VRAM port / OAM DMA legal any time — RDY-stalled until a slot opens Per-frame budget — 357,630 accesses BG planes A+B ≈110 K sprite patterns ≈29–57 K text layer ≈36 K sprite evaluation — 0 (on-die shadow OAM) free for blitter + CPU ≈157–182 K Display total ≈175–200 K (50–56 %). Margin is adequate, not large: a line of 32 full-width sprites plus both planes can approach saturation. Three sources stall the CPU via RDY VRAM port contention · OAM DMA (≈683 cycles) · PCM sample fetch (≈2.7–6 %) 455 cycles/line is therefore an exact bus budget, not a guaranteed instruction budget.
Pixel priority ladder and VRAM bus arbitration

The single 8-bit VRAM bus (VA0–16, VD0–7, 10 ns SRAM) has three masters. This section defines who wins, when the CPU can touch VRAM, and the resulting bandwidth — the last emulator-blocking unknown in the graphics path.

Bus clock & budget. The VRAM bus runs at the oito master clock (21.47727 MHz, 1 byte-access per cycle). Per frame that is 357,630 accesses (262 lines × 1,365 cycles); per scanline 1,365. At lo-res there are ~4 master cycles per output pixel (~3.2 at hi-res), and display fetch needs well under one access per pixel on average — so active display leaves spare VRAM cycles, and blanking lines are entirely free. (Note the CPU runs at master ÷ 3, so 1 CPU cycle = 3 VRAM-bus cycles.)

Masters & fixed priority (highest first):

  1. Display fetch — the line renderer reading name-tables, tile rows, sprite patterns and OAM to fill the line buffer. Always wins; the picture is never corrupted by contention.
  2. Blitter — VRAM→VRAM. Two modes, selected by BLIT_MODE bit 4: - Blanking-only (default): runs during HBLANK + VBLANK only → ≥350 tiles/frame guaranteed (benchmark below). - Interleaved: additionally steals the spare VRAM cycles during active display → ≥1,000 tiles/frame guaranteed (benchmark below).
  3. CPU VRAM port (VRAM_DATA $4044, and the OAM_DMA burst) — lowest priority.

CPU access rule. The CPU may read/write VRAM_DATA at any time, but oito grants it only on a free VRAM cycle. If the bus is busy, oito stalls the CPU by pulling RDY low until a slot opens (the W65C02S honors RDY on both reads and writes). During HBLANK/VBLANK the grant is immediate; during active display the stall is bounded by the display fetch in progress (exact counts are a Tier 2 refinement, §14.5). This replaces the NES-style "VRAM only in VBLANK" limitation — access is always legal, just occasionally slowed. (RDY is shared with the debug probe and with the CPU's own WAI; all three are wired-AND open-drain with an external pull-up.) The OAM_DMA ($4061) is a CPU-stalling burst (128 entries copied CPU-RAM→VRAM) best issued in VBLANK.

Blitter operational definition. The register list alone left eleven externally-visible behaviours undefined; this is the reference implementation.

blit: # all VRAM addresses are 17-bit, wrapping mod 128KB
 if BLIT_W == 0 or BLIT_H == 0: return # zero means NO-OP (not 256)
 unit_bytes = 32 if unit==tiles else 1 # tile = 32B; pixel unit = one nibble
 src = BLIT_SRC ; dst = BLIT_DST ; msk = BLIT_MASK_SRC
 for row in 0.. BLIT_H-1: # rows ascend; bytes within a row ascend
 s = src ; d = dst ; m = msk
 for i in 0.. (BLIT_W * unit_bytes) - 1:
 match op:
 copy: write(d, read(s))
 transparent: b = read(s) # per-NIBBLE test
 hi = (b>>4) ; lo = b & 15
 o = read(d)
 if hi != 0: o = (o & 0x0F) | (hi<<4)
 if lo != 0: o = (o & 0xF0) | lo
 write(d, o)
 fill: write(d, (BLIT_FILL & 15) * 0x11) # nibble duplicated
 masked: apply per-pixel using 1bpp mask bits from m, MSB = leftmost
 s += 1 ; d += 1
 src += BLIT_W*unit_bytes + BLIT_SRC_PITCH
 dst += BLIT_W*unit_bytes + BLIT_DST_PITCH
 msk +=... # mask advances 1 bit per pixel
 STATUS.2 = 0 ; raise blitter-done IRQ # latches on the final byte write

Shadow OAM. oito keeps a 1 KB on-die copy of OAM. Per-line sprite evaluation (§6.6) scans that internal copy, so sprite evaluation costs zero VRAM accesses; only the pattern rows of the ≤32 winning sprites are fetched from VRAM. This is standard VDP practice (the NES copies OAM internally; the Genesis caches the sprite list) and removes the largest, most variable term in the budget — a naive per-line scan of 128 VRAM entries would cost 57 K–229 K accesses/frame by itself.

Bandwidth. Per-frame budget at 320×224, two planes, text layer on, against 357,630 accesses:

Consumer Accesses/frame
BG planes A + B (name-table + tile rows) ~110 K
Sprite patterns (typical 8–16 px sprites) ~29–57 K
Text overlay (≤80 codes + attributes per line) ~36 K
Sprite evaluation / OAM scan 0 (on-die shadow)
Display total (typical) ~175–200 K (~50–56 %)
Free for blitter + CPU port ~157–182 K

Blitter throughput — a benchmark, not a slogan. Throughput is quoted only against a defined workload, with guaranteed minimums:

The correct characterisation of the margin remains adequate, not large. A worst-case line (both planes plus 32 full-width 32 px sprites) can approach saturation; the renderer's contract in that case is unchanged — display always wins, and the blitter/CPU simply get fewer spare cycles that line.

Emulator guidance (Tier 1 — §14.5). Model per scanline: run the CPU for the line's cycle budget; service VRAM_DATA/OAM-DMA writes immediately (optionally add a small stall model for accuracy); process queued blitter work against the per-frame or per-line bandwidth budget; render the scanline from VRAM state at that line. Cycle-exact fetch patterns and stall counts are a later cycle-accurate refinement, not needed to boot software.

Residual (cycle-accurate pass only): the exact per-pixel/per-tile display fetch schedule, the precise RDY stall-cycle counts, and OAM-DMA duration. These refine timing but do not block a working emulator.

6.6 Graphics data formats & rendering

Everything the display-fetch model (§6.5) reads from VRAM, plus the sprite overflow/priority/collision contract — the complete renderer specification.

VRAM address arithmetic — one rule. Every VRAM address computation wraps modulo 128 KB (17 bits). This is not an edge case: a 12-bit tile index alone spans the whole 128 KB (4096 × 32 B = 131,072 B), so any non-zero pattern base can carry a high index past 17 bits — a game using tile 4000 with a 2 KB base is already out of range.

Pixel & tile format. 4bpp indexed. One 8×8 tile = 32 bytes = 8 rows × 4 bytes; each byte holds two pixels, high nibble = left pixel, low nibble = right pixel; row 0 = top. Color index 0 = transparent. Tiles are addressed by a 12-bit index relative to a pattern base: VRAM address = (pattern_base + index × 32) mod 128 KB (base registers are in 2KB units → 64 tiles/step; wrap rule).

Palette (recap §5.1). Internal 64×12-bit RAM = 4 sub-palettes × 16 colors. Background planes use sub-palette 0 or 1; sprites use sub-palette 2 or 3. Index 0 of any sub-palette is transparent. The backdrop (shown where every layer is transparent) is sub-palette 0, entry 0.

Background: two planes. Plane A (BG0) and Plane B (BG1), independent, each:

Name-table cell (16-bit, little-endian):

Bits Field
0–11 tile index (relative to that plane's pattern base)
12 horizontal flip
13 vertical flip
14 sub-palette select (0 or 1)
15 priority (0 = low, 1 = high)

OAM DMA. OAM is 128 × 8 = 1024 bytes, so the DMA source is a 1KB-aligned block of CPU RAM (OAM_DMA_SRC $4061 holds the block number 0–15; address = block << 10) — not a 256-byte page. The SDK reserves one 1KB shadow-OAM buffer and always passes it, so games never compute the address. Entry size stays 8 bytes: it is fully spent on 9-bit X/Y, a 12-bit tile index, independent W/H, flip, palette and priority, and 1KB of the 16KB RAM budget is proportionally less than the NES spends (256B of 2KB).

Sprites — OAM (in VRAM at OAM_BASE, 128 entries × 8 bytes = 1KB):

Byte Contents
0 Y[7:0]
1 X[7:0]
2 tile index [7:0]
3 [0–3] tile index [11:8] · [4] H-flip · [5] V-flip · [6] sub-palette (2 or 3) · [7] priority
4 [0] Y[8] · [1] X[8] · [2–3] width (0=8, 1=16, 2=32 px) · [4–5] height (same encoding) · [6] enable · [7] reserved
5–7 reserved (future: link/scale)

Recommended VRAM layout (all bases are register-relocatable):

Region Size Notes
Plane A name-table 4KB
Plane B name-table 4KB
OAM 1KB
Tile/pattern space ~119KB BG + sprite patterns + the blitter's dynamic tile pool (all just tiles); up to ~3,700 distinct tiles

Palette RAM is internal to oito, not in VRAM.

Per-pixel priority ladder (front → back). The topmost opaque pixel wins, tested in this fixed order:

Level Layer
1 hardware mouse cursor (§9.2) — absolute top when enabled
2 text overlay layer (§6.8) — above all graphics and sprites
3 sprite, priority 1
4 Plane A, priority 1
5 Plane B, priority 1
6 sprite, priority 0
7 Plane A, priority 0
8 Plane B, priority 0
9 backdrop colour (sub-palette 0, entry 0)

Sprite per-line evaluation & overflow.

Collision.

Emulator note. A standard scanline renderer suffices: per visible line, evaluate the ≤32 sprites, then compose each pixel through the priority ladder (backdrop → BG/sprite levels → text overlay §6.8 → mouse cursor §9.2 on absolute top) from VRAM + palette state, registering collisions among opaque sprite overlaps. Correct output needs no cycle accuracy; exact fetch scheduling is a Tier 2 refinement (§14.5).

6.7 Raster timing

re8 — RASTER TIMING (60.055 Hz, 262 lines, 455 CPU cycles/line) Frame — 262 lines × 1,365 master ticks = 357,630 ticks = 119,210 CPU cycles ACTIVE DISPLAY — lines 0–223 224 lines · sprite evaluation from shadow OAM · display fetch has bus priority 1 VBLANK — lines 224–261 (38 lines ≈ 17,290 CPU cycles ≈ 2.4 ms) 0223261 line 224 — shadow-OAM snapshot · VBLANK IRQ line 0 — pick coords latched · STATUS.3 cleared OAM DMA and bulk VRAM work belong here Line — 1,365 master ticks = 455 CPU cycles = 63.56 µs · HSYNC 15.734 kHz 320-mode · dot clock 6.703 MHz · 426 dots 320 active — 47.7 µs 14 sync 32 back porch 60 256-mode · dot clock 5.365 MHz · 341 dots 256 active — 47.7 µs 9 sync 25 back porch 51 63.56 µs — identical line and frame timing in both modes Both modes fill the same 47.7 µs of active video, so neither is pillarboxed on analog outputs. DE marks the active window explicitly for the scaler. Vertical: 224 active + 3 front porch + 3 VSYNC + 32 back porch = 262. CSYNC = NOT(HSYNC XOR VSYNC), active-low, no equalisation or serration. Digital output is re-timed by the CH7035B to exactly 1080p60; analog outputs carry the native 60.055 Hz ±0.01 %.
Frame and line timing for both H-modes

Both H-modes share identical line and frame timing — only the dot clock (hence horizontal resolution) changes — so switching resolution never re-syncs the display or the CH7035B. The CPU clock and the dot clocks are independent PLL outputs from the 21.47727 MHz master, so the dot count per line is exact even though the dot clock isn't a whole divisor of the master.

Common line & frame (both modes):

Horizontal — lo-res (256): dot clock ≈ 5.365 MHz, 341 dots/line = 256 active + 9 front porch + 25 HSYNC + 51 back porch.

Horizontal — hi-res (320): dot clock ≈ 6.702 MHz, 426 dots/line = 320 active + 14 front porch + 32 HSYNC + 60 back porch.

(Porch/sync splits are oito's canonical values; they may be fine-tuned within the fixed blanking budget without changing the line or frame rate.)

Refresh, tolerance & compatibility. 60.055 Hz is the native rate — an NTSC-locked 240p line count does not land on an exact 60.00/59.94 with the ÷3 CPU line. The previous claim that "every 60 Hz display tolerates it" was an untestable universal and is replaced by a specification:

Emulator note. Model 262 lines × 455 CPU cycles/line; VBLANK on lines 224–261 (drives the VBLANK IRQ). Exact porch values matter only for mid-scanline raster tricks and CH7035B input modeling — the active/blank split is what the renderer needs.

6.8 Text overlay layer

An optional monochrome character layer composited above both BG planes and all spriteslevel 2 of the §6.6 priority ladder, beneath only the hardware mouse cursor (§9.2). It reuses the proven tile-plane and BG-scroll machinery; it has its own font, does not consume a BG plane, and its characters do not count against the 128-sprite / 32-per-line budgets. Characters cannot move independently or collide (§9 collision ignores this layer).

Transparent by default. The layer is a true overlay: each cell is transparent unless a glyph pixel is lit or the cell opts into an opaque background (textbox, below). When disabled (TEXT_CTRL.enable=0) the layer costs nothing.

Glyphs & font RAM. Characters are 8 pixels tall, 1bpp (glyph pixel = on/off). oito carries two 3 KB font-RAM banks (6 KB total; bank 0 = system default, bank 1 = cart font). Each bank holds both densities:

Glyph width — two column densities (no new video timing):

Placement — grid + fine scroll (not a second sprite engine):

Attribute byte (per cell)) was not a real encoding: three bits cannot hold a 4-bit background entry plus inverse plus blink. Actual allocation, all colours drawn from the layer sub-palette (TEXT_CTRL bits 5–6):

Bits Field
0–3 foreground colour index (any of the 16 sub-palette entries)
4–5 background colour index — restricted to entries 0–3 of that sub-palette
6 opaque background (0 = cell transparent, 1 = fill with the background entry first)
7 blink this cell (gated by the TEXT_CTRL.3 global enable)

Per-cell inverse is deliberately dropped: it is exactly "swap the two colours", which an author encodes directly by choosing fg and bg in the same byte — spending a bit on a redundant operation would have cost background range. Four background entries is sufficient in practice (a panel colour plus a highlight for the selected row), and those entries can be chosen per sub-palette; foreground keeps all 16. The 2-byte cell stride is preserved — a 3-byte cell would have grown an 80×28 map from 4,480 to 6,720 bytes and broken easy addressing.

Blink & caret timing.

Cost & impact. Grid mode's per-line fetch ≈ one BG plane's worth (≤80 char codes + attributes; font rows come from on-die font RAM, not VRAM) — counted in the §6.5 budget, where text joins display priority. Silicon: +1 compositor level, +6 KB font RAM (2 banks × 3 KB), +char-map fetch, ~9 registers. VRAM: char map is a fixed 4,480 B (80×28) of 128 KB. No pins, no external parts, no video-timing change. Boot flash: +3 KB per regional charset (8×8 + 4×8).

Emulator note. A deterministic top-layer composite: per visible line, for each of the ≤80 cells on that text row, fetch the char code + attribute, index the selected font bank, and overlay the 1bpp row (honouring transparent/opaque, blink phase, and the SCROLL_X/Y pixel offset) after the sprite/BG composite. No cycle accuracy required.


7. Video output subsystem

Region: 60Hz-only, single global timing. No 50Hz PAL mode — games run full-speed worldwide and the HDMI output is region-agnostic 1080p60. Composite/S-Video use NTSC color (AD725 + 14.318MHz crystal); RGB SCART, DE-15 RGBHV and HDMI are region-agnostic (raw RGB / digital, no subcarrier) and work on any 60Hz-capable display. The only unsupported case is pure composite/S-Video into a 50Hz-only vintage PAL CRT.

One digital pipeline feeds all outputs simultaneously (no switching). (The former claim that "digital fan-out costs almost zero current" is removed: it contradicted this section's own analog-buffer requirement — one source cannot drive five outputs, which is why §7's analog stage specifies three buffer paths — and was an unmeasured performance claim without a workload.) Analog output stage. A single R-2R ladder cannot drive five outputs, so the buffering is specified rather than deferred (it consists of active parts and is therefore in scope — see the corrected scope rule in §1):

Output Connector Path Notes
Composite (CVBS) RCA R-2R DACs → analog RGB + oito CSYNC on the AD725's composite-sync input (its other sync input tied to the static level per datasheet; the encoder's internal H/V combination is deliberately unused) → AD725 → CVBS vintage TVs — AD725 is provisionally selected pending 240p validation
S-Video (Y/C) 4-pin mini-DIN same AD725 produces Y/C simultaneously
RGB SCART Euro-SCART R-2R DACs + CSYNC on pin 20; pin 16 held at 1–3V to force RGB mode PVMs/EU CRTs
15 kHz analog RGBHV (DE-15) DE-15 buffered ladder (THS7374, §7 analog stage) → RGB 0.7 Vₚₚ into 75Ω, source-terminated 75Ω; separate H and V sync on pins 13/14, active-low, 3.3V logic. CSYNC is available on the sync pin as a build option, since oito generates it anyway — many PVMs/upscalers prefer it Not VGA timing (VGA is 31.5 kHz): this is 15.734 kHz for CRTs, PVMs and upscalers, and standard PC monitors will not sync
Digital video 19-pin HDMI-Type-A receptacle 12-bit RGB (replicated to RGB-888) + syncs + pixel clock → CH7035B → TMDS strict DVI-D, video only (no audio, no HDCP) → genuinely royalty-free; audio is separate (§8.4)

AD725 240p qualification. The AD725's application material specifies interlaced RGB at TV-standard rates and discusses equalisation/serration in composite sync, whereas re8 feeds it 262-line progressive 240p with a plain 3-line VSYNC and no equalisation pulses — the classic "240p is not a broadcast standard" situation. CRTs are tolerant; the encoder still has to lock subcarrier and vertical timing to a waveform its datasheet never promised. Two specific risks: colour-burst insertion and subcarrier phase across a 262-line frame (NTSC's 4-field colour sequence assumes 525-line interlace), and vertical lock without equalisation.

7.1 CH7035B path (final digital-video design)


8. Audio subsystem — the oito APU

The original 4-channel PSG was 8-bit-tier and badly mismatched to the VDP. It is replaced by a 16-voice hybrid APU integrated in oito — enhanced PSG + wavetable + PCM — sized to the graphics system. It needs no extra pins (output is still 2× PWM + I²S) and no external RAM (small on-die wave RAM; PCM streams from cartridge ROM).

8.1 Voice complement (16 total)

Block Voices Synthesis
PSG 4 (3 pulse + 1 noise) variable-duty pulse; 15-bit LFSR noise; per-voice ADSR envelope, frequency sweep, stereo volume
Wavetable 8 32-sample × 8-bit user waveforms from on-die wave RAM; per-voice ADSR, stereo volume, pitch
PCM 4 8-bit signed PCM or 4-bit IMA-ADPCM, streamed from cartridge ROM; per-voice pitch, loop, stereo volume

All 16 voices feed an internal digital mixer → stereo → I²S (clean multi-bit, to the DIT4192 S/PDIF transmitter and an optional motherboard audio DAC — not to the CH7035B) and a 2nd-order delta-sigma PWM stereo pair at master ÷ 8 (feeding the active analog stage). Nominal output/mix rate ≈ 48 kHz (master ÷ 448 = 47.94 kHz).

8.2 Internal register file (via the $40A0 port, 16-bit internal address)

Range Contents
$0000–$001F PSG voices 0–3 (8 bytes each)
$0020–$005F Wavetable voices 0–7 (8 bytes each)
$0060–$009F PCM voices 0–3 (16 bytes each)
$00A0–$00AF globals — enumerated in §8.5 (16 voice-active flags need two bytes, not one)
$0100–$02FF Wave RAM: 16 slots × 32 bytes (512 B)

PSG voice (8 bytes): +0 period[7:0] · +1 pulse: [0–3] period[11:8] (12-bit period), [6–7] duty (12.5/25/50/75%) / noise: [0–2] noise rate, [3] LFSR mode (0=15-bit long, taps 0⊕1; 1=short/periodic, taps 0⊕6) · +2 L volume (5-bit) · +3 R volume (5-bit) · +4 ADSR attack[7:4]/decay[3:0] · +5 sustain-level[7:4]/release[3:0] · +6 sweep shift[2:0]/dir[3]/rate[6:4]/enable[7] · +7 key-gate[0]/enable[1]/retrigger[2].

Wavetable voice (8 bytes): +0 period[7:0] · +1 [0–3] period[11:8] (12-bit, same ÷12 tone clock) · +2 L volume · +3 R volume · +4 wave slot[3:0] (0–15 into wave RAM) · +5 ADSR attack/decay · +6 sustain/release · +7 key-gate[0]/enable[1]/loop[2].

PCM voice (16 bytes): +0..2 start address (20-bit linear cartridge-ROM byte address = the full 1MB cart space; stored in 3 bytes, top 4 bits reserved/must-be-0) · +3..5 loop address · +6..8 end address · +9..10 pitch (16-bit playback-rate multiplier, 1.0 = source rate) · +11 L volume · +12 R volume · +13 format[0] (0=8-bit PCM, 1=4-bit IMA-ADPCM)/loop-enable[1]/key-gate[2]/enable[3] · +14..15 (now +13..15) current-position readback, 20-bit.

8.3 Behavior details (emulator-grade)

Bus-master timing (normative). WDC specifies that BE low tri-states Address, Data and R/W̄, so the cartridge would otherwise see a floating strobe while ROM_CĒ is asserted — a potential write into flash. oito's R/W̄ pad is therefore bidirectional and is driven high (read) for the whole steal. Sequence:

  1. assert RDY low; let the in-flight CPU cycle finish;
  2. assert BE low → the CPU releases A, D and R/W̄;
  3. oito drives A0–A15, BANK0–BANK5 and R/W̄ = high, then asserts ROM_CĒ;
  4. sample D0–D7 after the flash access time (≥70 ns ⇒ one full CPU cycle per byte);
  5. deassert ROM_CĒ; repeat from 3 for the remainder of the burst;
  6. restore the CPU's bank value, then MUST release BE before releasing RDY — the CPU must regain its buses before it resumes clocking, or it would execute a cycle with its outputs still tri-stated. A steal always asserts ROM_CĒ explicitly, so sample fetch is unaffected by SAVE_CTRL (§11.4) — that register redirects CPU accesses to $8000–$BFFF only, never oito's own bus-master cycles. PCM therefore keeps streaming correctly while a game has the save window open. A NOR read (~70 ns) does not fit one 46.6 ns master cycle, so each stolen access occupies a full CPU cycle (139.7 ns); bursting amortises the arbitration. 8-bit = signed raw; 4-bit = IMA-ADPCM (standard predictor + step-index table). Pitch resamples via the phase accumulator; loop between loop-address and end-address when loop-enabled. Because oito drives the bank lines during a steal, a sample may exceed 16KB and cross bank boundaries freely — unlike anything reachable through the CPU's window.

8.4 Output paths

re8 — AUDIO SIGNAL CHAIN (16-voice APU) oito APU 4 × PSG — 3 pulse + 1 noise 12-bit period @ master÷12 · 27.3 Hz–112 kHz 8 × wavetable 32-sample slots · 512 B on-die wave RAM 4 × PCM / IMA-ADPCM streamed from cart ROM by bus-master steal mixer — 20-bit accumulator ×volume → hard clamp to 16-bit 47,940.3 Hz I²S BCLK 3.068 MHz MCLK 6.136 MHz DIT4192 I²S → S/PDIF transmitter 3.3 V TOSLINK module optical output 3.3 V S/PDIF out 2× PWM Δ-Σ 2nd order master÷8 NJM4556 stage 3rd-order LPF @20 kHz 5 V · 1 Vrms · ≤100 Ω 3.5 mm line-out SCART audio pins Channel status declares 48 kHz with variable accuracy — S/PDIF has no code for 47,940 Hz; receivers recover the true clock from the biphase stream. The I²S pins are physically unconnected to the CH7035B: the digital video link is strict DVI-D and carries no audio. Output is muted until the supervisor releases reset and the APU is enabled, and re-mutes on brownout.
Audio signal chain from APU to outputs

Audio does not ride the digital-video cable: the link is strict DVI-D (video only — §7.1), so audio has its own royalty-free outputs.

Digital audio format. Every clock divides from the master, so the whole chain is synchronous by construction:

Signal Value Derivation
LRCLK (fs) 47,940.3 Hz master ÷ 448
BCLK 3.0682 MHz master ÷ 7 = 64 × fs (32 bits per channel)
MCLK 6.1364 MHz 128 × fs = 2 × BCLK, PLL-generated (as the dot clocks are, §6.7)

Emulator model. Per output sample (~48 kHz): advance each active voice's phase accumulator and envelope, fetch its sample (pulse/noise/wave-RAM/PCM-from-ROM), apply envelope × L/R volume, sum all 16, apply master volume, emit stereo. Fully deterministic; no cycle accuracy required. The exact integer model is §8.5.

8.5 APU reference model

Voice layouts alone cannot produce identical audio across implementations; this section is the arithmetic contract. All values are integers; the mix rate is master ÷ 448 = 47,940.3 Hz and every "per sample" step below occurs once per mix tick.

Global registers ($00A0–$00AF).

Addr Contents
$A0 / $A1 master volume L / R (0–255, unity = 128)
$A2 output enable (bit0 = APU on)
$A3 / $A4 voice-active flags, voices 0–7 and 8–15 (16 flags do not fit in one byte)
$A5 PCM FIFO status (bit n = voice n underran since last read; read-clears)
$A6–$AF reserved, read $00

Envelope (ADSR, all voices). Attack/decay/release are 4-bit rate indices into a 16-entry table giving mix ticks per step; the envelope level is 8-bit. Attack adds one step per interval to $FF; decay falls to sustain<<4; release falls to 0. The table is geometric, ≈ ticks[r] = round(2 ^ (r/2)) * 4 (r = 0…15 → 4…512 ticks/step), i.e. a lookup table, not a runtime exponential. Key-gate 1 starts attack; 0 starts release.

Pulse. freq = 1,789,772.5 / (16 × (period+1)) (§8.3); duty 12.5/25/50/75 %. Output ±1 scaled by envelope.

Noise. 15-bit LFSR, shifted right, feedback into bit 14 = bit0 XOR bit1 (long) or bit0 XOR bit6 (short). Reset seed = $0001; the all-zero state is unreachable by construction. Clocked by the noise-rate divider off the same ÷12 tone clock.

Sweep (PSG). Clocked at 239.70 Hz = mix rate ÷ 200 (a fixed integer frame-sequencer divider — not "frames"). An exact 240 Hz would require ÷199.75, which no divider can produce; nothing musical depends on the 0.13 % difference. Each tick period ± (period >> shift); rate 0 disables; a result below 8 or above $FFF mutes the voice until re-keyed.

Wavetable. Phase accumulator indexes the 32-sample slot; no interpolation (nearest sample — matches the aesthetic and is cheaper); phase resets to 0 on key-on; samples are 8-bit signed.

PCM.

Mixer. Each voice yields a 16-bit signed sample = waveform × envelope × side-volume (right-shifted to 16-bit). All 16 sum into a 20-bit accumulator; master volume is applied; the result is hard-clamped to signed 16-bit — clamping is saturation to $7FFF/$8000, replacing the untestable phrase "soft-clamp". That stream feeds I²S directly and the delta-sigma PWM pair.

Reset. All voice registers, envelopes and phases zero; $A0/$A1 = 128; output disabled. Wave RAM is undefined at power-on and is zeroed by boot firmware (§11.1).

Conformance. A golden test hashes the PCM output of a fixed register-write script plus a fixed sample ROM; any implementation must reproduce the hash bit-exactly.


9. Controller input

Pin Signal SELECT high SELECT low
1 D0 Up Up
2 D1 Down Down
3 D2 Left logic 0
4 D3 Right logic 0
5 VCC +5V +5V
6 D4 Button B Button A
7 SEL console output console output
8 GND ground ground
9 D5 Button C Start
# TH D0 D1 D2 D3 D4 D5
1 1 Up Down Left Right B C
2 0 Up Down 0 0 A Start
3 1 Up Down Left Right B C
4 0 Up Down 0 0 A Start
5 1 Up Down Left Right B C
6 0 Up Down 0 0 A Start
7 1 Z Y X Mode B C
8 0 1 1 1 1 A Start

Device identification:

Timeouts: the pad counter self-resets after ~1.5 ms of TH-high, so the mandated >2 ms idle guarantees each acquisition begins at counter state 0; an acquisition aborted mid-sequence simply retries after the idle. Game code never sees the protocol.

9.1 Keyboard input — PS/2, via the controller ports

re8 supports a PS/2 keyboard with no dedicated connector and no new oito pins — it reuses a controller port. This unlocks keyboard-native genres (strategy, simulation, text/RPG, dev tools) while keeping all existing joystick games playable on a keyboard.

Bidirectional PS/2 host in oito. oito contains a small PS/2 host state machine on two of a port's data lines. On receive, the device drives the clock (~10–16 kHz) and DATA; oito samples DATA on each CLK falling edge, assembles the 11-bit frame (start + 8 data + parity + stop), checks parity, and pushes the byte into a small FIFO (KBD_SCAN, §5.1). On transmit (host→device — the mouse enable command §9.2, keyboard LEDs/config, and the identification sequence below), oito pulls CLK then DATA low via open-drain drive and clocks out its command frame per the state machine below. The two lines therefore carry ~4.7 kΩ pull-ups to 5V and an open-drain (pull-low-only) driver — a small front-end change on just those two of the six data lines (the other four data lines + SELECT keep the plain 74LVC244A input path). oito is only ever in the 3.3 V domain; it drives low through the open-drain device and reads through the 5V-tolerant path. (This supersedes an earlier receive-only sketch — bidirectional is required by the mouse and, as a bonus, restores keyboard LEDs and host configuration.)

PS/2 line circuit. Per line (CLK, DATA), per port — a specified circuit, not a menu of alternatives:

Element Specification
Pull-up 4.7 kΩ to +5 V, on the console PCB (not in the adaptor, so a bare adaptor cannot leave lines floating)
Receive line → 1 kΩ series74LVC244A input (5V-tolerant at a 3.3V rail); the series resistor limits fault current into the clamp
Transmit BSS138 N-channel MOSFET — drain = the 5V line, source = GND, gate = oito's 3.3V open-drain output. Gate high pulls the line low; gate released lets the 5V pull-up restore the high
ESD ≤10 pF/line TVS to GND at the connector
Power-off oito unpowered ⇒ gate low ⇒ FET off ⇒ line idles high through the pull-up; no path from 5V into oito in any power state

Why gate-driven rather than the classic BSS138 bidirectional shifter: in this topology oito's pin never sees 5V — only the FET drain does, while the gate stays in the 3.3V domain and the source is grounded. The familiar bidirectional level-shifter arrangement ties both rails through the FET body diode, which is acceptable for 3.3V↔5V I²C but would give 5V a route toward the ASIC. Rise-time budget: PS/2 runs at 10–16.7 kHz (≥30 µs half-period); 4.7 kΩ with a realistic 50 pF gives τ ≈ 235 ns and a full rise ≈1 µs (~2 % of a half-period), and even a pessimistic 150 pF gives ≈3 µs.

Port mode & joystick-safety invariant. The earlier rule was circular: transmitters were to stay high-Z until a port was "positively identified as PS/2", yet identification was specified as sending PS/2 Reset — which requires driving the lines. It also assumed a plug-detect event the DB9 does not provide. Replaced by explicit mode selection:

PS/2 protocol & identification state machine. Runs only once PORT_MODE = PS/2. The earlier text conflated two distinct operations — Reset returns $FA (ACK) then $AA (BAT pass); it is $F2 Identify that returns ID bytes.

Host→device frame: pull CLK low ≥100 µs (inhibit) → pull DATA low (request-to-send) → release CLK → the device clocks out 11 bits (start, 8 data, odd parity, stop) → device returns an ACK bit → host releases both lines.

Step Host sends Expected reply
1 $FF Reset $FA, then $AA (BAT pass) within ~750 ms
2 $F2 Identify $FA, then $AB $83 = keyboard · $00 = mouse · $03 = wheel mouse
3a (mouse) $F3 $C8, $F3 $64, $F3 $50, then $F2 $03 ⇒ IntelliMouse wheel present (the "knock")
3b (mouse) $F4 Enable Data Reporting $FA; device begins streaming packets
3c (keyboard) $F0 $02 Set Scan Code Set 2 $FA

Error and recovery states:

Passive DB9↔PS/2 adaptor (no MCU). The DB9 already carries +5V and GND, and PS/2 keyboards are 5V devices, so the adaptor is pure wiring: DB9 pin 5→PS/2 +5V, DB9 GND→PS/2 GND, and two DB9 data pins→PS/2 CLK and DATA (mini-DIN-6). When a keyboard is on a port, oito routes those two data pins to the PS/2 host instead of the joypad mux (mode set explicitly via PORT_MODE; KBD_STATUS.present flags a device once identified); the port's other lines and SELECT are unused. The same passive adaptor serves a mouse (§9.2) — identical wiring, since a PS/2 mouse is the same 4-wire interface.

Keyboard resource rules.

Key→joypad injection (compatibility). When a port is a keyboard, oito can map a fixed key set (arrows→D-pad, and e.g. Z/X→A/B, Enter→Start) straight into a chosen player's JOYPAD registers (KBD_CTRL bits 1–2; map in KBD_MAP_IDX/VAL, firmware-loaded defaults). The mapping happens below the game, so every existing joystick title becomes keyboard-playable with zero game or SDK cooperation — a raw $4070 read just sees buttons. Keyboard-aware games disable passthrough (joypad_passthrough(false)) while taking text, so typing keys don't also fire buttons.

SDK & firmware. Keyboard-aware games use the re8.keyboard module (§14.2) for cooked characters, a line-input field widget (echoes into the §6.8 text overlay + hardware caret), raw make/break key events, and modifier state. Scan-code→character translation uses a region keyboard-layout table in the boot flash, selected to match — and shipped alongside — the §6.8 region charset (US/UK/DE/…). Cooked characters are Latin-1 codes, identical to the §6.8 glyph indices, so echoing typed text is a direct write.

Impact.

Emulator note. Host keystrokes → the KBD_SCAN FIFO (Set-2 make/break) + the same key→joypad injection; layout translation and the field widget are SDK-side. Fully deterministic; the PS/2 bit-timing is internal to oito and need not be simulated.

9.2 Mouse input & hardware cursor

re8 supports a PS/2 mouse on a controller port (same passive adaptor, §9.1) with a fully hardware-rendered cursor: the game sets a cursor graphic once and thereafter only receives move/click events — oito owns all cursor rendering and all mouse-hardware interaction.

Why the §9.1 host is bidirectional. Unlike a keyboard, a PS/2 mouse reports nothing until the host sends Enable Data Reporting (0xF4) (plus optional sample-rate/resolution and the Microsoft wheel "knock" sequence). That host→device path is exactly the bidirectional PS/2 upgrade folded into §9.1; the mouse is the reason for it. After the $F4 enable (§9.1 state machine), the mouse streams 3-byte packets (button byte + signed X delta + signed Y delta), or 4-byte once the IntelliMouse wheel is enabled by the 200/100/80 sample-rate knock and confirmed by an Identify returning $03.

oito maintains the cursor; the game does nothing to draw it.

Cursor arithmetic. Cursor feel is immediately noticeable to players, so none of this is left to implementers:

Pointer ownership — mouse-first, single source. Several devices can potentially move the cursor, so oito arbitrates to exactly one authoritative source at a time, defaulting to the physically-connected pointing device:

  1. A detected PS/2 mouse owns the pointer — D-pad and keyboard cannot move it (two mice: lower port wins).
  2. With no mouse present, the pointer follows the game-designated fallback = a chosen player's D-pad (CURSOR_CTRL bit 7). Keyboard arrow keys reach that D-pad through the §9.1 key→joypad injection, so "keyboard moves the pointer" needs no separate path.
  3. Hot-plug takes over: attaching a mouse seizes the pointer; removing it reverts to the fallback. Never additive.

System pointer without a mouse. Because oito already turns deltas into a rendered cursor, menus and point-and-click UIs get a working pointer via the D-pad fallback even with no mouse attached, at zero extra game cost. The game may override the default arbitration with CURSOR_CTRL.source (auto / mouse-only / D-pad-only / off) for accessibility or a deliberate control scheme.

Hit-test sampling timing. "Once per frame" did not say when, and the answer determines which scroll and sprite state the result reflects:

This deliberately mirrors the shadow-OAM decision: one named event, inputs latched at frame start, results read in VBLANK — two features sharing a single timing idiom.

Hardware hit-testing ("pick"). oito already computes, per pixel, which sprite and which BG cell win the composite — so it reports what is under the pointer at the cost of one coordinate comparator and a few latches. Once per frame — at the moment the raster reaches the cursor hotspot or the game-set pick point (timing above) — oito latches into the pick registers ($40C0–$40CF, §5.1): the topmost opaque sprite index under the point (HIT_SPRITE, 0xFF = none → maps to a game object); for each BG plane, the name-table cell (col,row) and the tile ID under the point, with scroll/wrap already applied (→ indexes a tile-grid map array directly); and HIT_STATUS flags (sprite present / plane-A opaque / plane-B opaque / a 2-bit winner field naming the layer that actually shows). On a click the game reads these directly — no pixel→cell math, no scroll compensation, no per-object bounding-box tests on the CPU — turning "what did the player click?" into a register read. The pick point defaults to the hotspot; a game may set an arbitrary PICK_X/Y to probe any location (e.g. what lies ahead of a projectile), making it a general hit-test facility. The cursor and text-overlay layers are excluded (you get what's under the pointer). Silicon cost: one coordinate comparator plus the latches listed in §5.1, tapping the existing compositor.

SDK. re8.mouse (§14.2): present(), set_cursor(TILE, hotx, hoty), set_speed(n), set_bounds(...), show()/hide(), on_move(&fn(x,y)), on_button(&fn(buttons)), on_wheel(&fn(delta)), and pollable x()/y()/buttons().

Impact: oito +mouse-packet parser + position accumulator/scaler/clamp + the top-layer cursor compositor (1 tile in 8×8, 4 in 16×16) + the MOUSE/CURSOR registers ($40B0–$40BF, §5.1); the bidirectional PS/2 host is already counted in §9.1. No new connector; no new pins (the two PS/2 lines are the same bidirectional port I/O). Board: shares §9.1's pull-ups + open-drain drive; a PS/2 mouse draws only 20–50 mA, well within the §2.1 per-port allowance. VRAM: the cursor is one existing tile (8×8) or four (16×16). Restrictions: PS/2 only; a mouse occupies one of the two ports (so joystick + mouse, or keyboard + mouse — not all three).

Emulator note. Host mouse deltas → the scaled position accumulator → top-tile cursor composite; buttons/wheel → events. Fully deterministic; the PS/2 packet timing is internal to oito.


10. Interrupts


11. Cartridge subsystem

11.0 ROM image construction & banked calls

prog8 compiles into one flat 64KB address space; nothing in the compiler places code into 64 overlapping $8000–$BFFF banks or produces a 1MB image. re8 therefore defines a build model rather than depending on compiler capability:

11.1 Boot ROM & power-on handoff

re8 — BOOT SEQUENCE & OVERLAY STATES ($E000–$FFFF) RES̄ asserted supervisor holds CPU, oito and CH7035B until rails stable BOOT_SRC = 0 Internal stub runs 2 KB mask ROM, mirrored ×4 owns $FFFC/D reset vector copies loader → system RAM unbrickable — cannot be erased jumps to RAM RAM loader validates BOOT_SRC = 1 walks pages via BOOT_BANK verifies firmware checksum valid blank / corrupt Recovery BOOT_SRC = 0 reads image via cart window reflashes; probe = alt. path retry after reflash Boot firmware logo + jingle · hardware init charset → font bank 0 · self-test header validate → CART_CONFIG_* CART_LOCK ← $A5 Handoff copy stub → RAM BOOT_SRC = 2 one-way until RES̄ JMP ($FFFC) Cartridge runs BOOT_SRC 0internal stub mirrored ×4 1boot-flash page selected by BOOT_BANK 2overlay off cartridge owns $C000–$FFFF 0↔1 free during boot; →2 is one-way. Fast-boot emulators may skip to the §11.1.1 handoff-state table, which must be byte-identical to what the canonical image produces.
Boot sequence and overlay states

re8 boots in two stages: an unbrickable internal stub in oito hands off to an updateable external boot ROM, which initializes the system and launches the cartridge. (This replaces the earlier "boot straight into the cartridge, no firmware" model.)

Storage — hybrid, brick-safe:

Boot-flash paging. The overlay window is only 8KB but the flash is 64–128KB, so the firmware pages itself through that window using the same fixed+banked idiom as the cartridge:

Stage 0 — internal stub and firmware validation. The overlay's contents are selected by BOOT_SRC (BOOT_CTRL bits 0–1), not a single enable bit:

  1. At RES̄ BOOT_SRC = 0: the internal 2KB stub is mapped at $E000–$FFFF, mirrored 4× (so every read in the window is defined and $FFFC/D is the stub's reset vector). External flash is not visible. BOOT_BANK = 0.
  2. The stub brings up minimal clock/bus state and copies a small loader into system RAM, then jumps to it — the same RAM-trampoline technique used for the cartridge handoff below, because the code must survive remapping its own window.
  3. From RAM the loader sets BOOT_SRC = 1 (external flash page visible), walks every page via BOOT_BANK, and verifies the firmware checksum.
  4. Valid → it leaves BOOT_SRC = 1 and jumps to the external firmware's entry point; the firmware continues at step 1 of the handoff below.
  5. Blank/corrupt → it restores BOOT_SRC = 0 and enters recovery: the stub reads a firmware image through the cartridge window ($8000–$BFFF, BANK_SELECT operating normally — the plain-update, SD and dev cartridges all present as ROM there) and reflashes the boot flash. The debug probe remains an alternative recovery path, since it can halt the CPU and drive system RAM directly (§13). The internal stub is mask ROM and cannot be erased, so recovery always exists.

Reset overlay & handoff (Game-Boy-style):

  1. With BOOT_SRC = 1 the boot firmware owns $E000–$FFFF, so reset/IRQ vectors are the firmware's.
  2. The firmware runs: hardware init, logo + APU jingle, controller poll (self-test combo), read + validate the cartridge header via the switchable window.
  3. To launch the game it copies a tiny stub to system RAM and jumps to it; the stub writes BOOT_SRC = 2 (one-way until next reset) — un-mapping the boot ROM and restoring the cartridge fixed bank at $C000–$FFFF — then JMP ($FFFC) into the cartridge. Running the handoff from RAM avoids pulling the code out from under itself.
  4. After handoff the memory map is exactly §5 (cartridge owns $C000–$FFFF + vectors); the boot ROM is invisible.

Boot ROM responsibilities.

11.1.1 Handoff state

The canonical boot image is the compatibility contract. Machine state at cartridge launch is defined as "whatever boot firmware version X produces". Fast-boot (skipping the firmware, as GB emulators skip the DMG ROM) remains supported, but only against the table below, which is generated from the canonical image and must match it byte-for-byte — it is not hand-written. Without this, two emulators could fast-boot the same cartridge into different states and a game relying on firmware leftovers would diverge.

Domain State at handoff
oito registers all $00 except: BOOT_CTRL BOOT_SRC = 2 (overlay off), CART_CONFIG_BANKS/CART_CONFIG_SAVE = programmed from the header and locked, VIDEO_CTRL = screen enabled + resolution per firmware default, SAVE_CTRL = window closed + write-protected
System RAM $0000–$3FFF cleared to $00, including the ZP runtime area — no leftover garbage
VRAM cleared to $00
Palette RAM default 64-colour set loaded (published as data in the SDK)
Font RAM bank 0 = region charset (both densities); bank 1 undefined
Mapper BANK_SELECT = 0; save window closed
APU all voices off, wave RAM zeroed, master volume 128
CPU SP = $FF; I set, D clear; A/X/Y undefined (explicitly so); PC = cartridge reset vector

Versioning consequence. Because the table is derived from the firmware, any firmware revision that changes it is a compatibility-relevant change and MUST bump a documented handoff-state version, which the SDK and emulator both record. This is the price of permitting fast-boot, and it is stated rather than hidden.

Emulator note: execute the canonical boot image, or fast-boot directly to the table above and jump to the cartridge entry. Both paths must be indistinguishable to a conforming cartridge.

11.2 Cartridge header

64 bytes at ROM image offset $0000 (= $8000 in bank 0), read by the boot ROM via the switchable window:

Offset Size Field
$00 4 magic "re8\x1a"
$04 1 header version
$05 16 title (ASCII, space-padded)
$15 1 region tag — advisory metadata (title origin); the console is 60Hz-only, so this does not switch a hardware mode
$16 1 bank count (log2 of 16KB banks, 0–6 → up to 1MB)
$17 1 save flags (§11.4): bit0 has-save; bits1–2 save type (0 = none, 1 = FRAM, 2–3 reserved — EEPROM removed); bits3–5 save-size code (0 = none, 3 = 16KB; 1–2 reserved); bits6–7 reserved
$18 2 entry point (CPU address) — advisory/tooling only; launch uses the reset vector JMP ($FFFC)
$1A 2 header checksum
$1C 4 ROM CRC32
$20 32 reserved

Checksum & version definitions. Both are format ABI: a packer and a boot ROM that disagree reject valid cartridges, so they are pinned exactly.

Invalid magic/checksum → the boot ROM shows the bad-cartridge screen.

Validation scope. At boot the firmware verifies only the 2-byte header checksum ($1A) — cheap and instant. The ROM CRC32 ($1C) is not scanned at boot (1MB at 7.16MHz would cost seconds); it exists for the SDK/tooling, as the emulator's .sav key (§11.4), and for optional self-test. Entry is JMP ($FFFC) — the cartridge reset vector is normative; the header's entry-point field ($18) is advisory metadata for tooling and is not used to launch (same treatment as the region byte).

11.3 Smart-cartridge family (accessories)

Because the connector passes reads and writes (above), one interface serves four cartridge types with no console-side modes:

One updater, three deliveries. There is a single boot-flash updater routine: it copies itself to RAM, sets BOOT_CTRL.BOOT_FLASH_WE, runs the JEDEC erase/program sequences (§11.1) against the external boot flash from an image in the cartridge window, verifies, clears BOOT_FLASH_WE, and reboots. It is delivered by the plain update cartridge, the dev cartridge (loaded via probe), or the SD cartridge — the console's own CPU always does the flashing; the internal stub guarantees no brick.

Security note (out of scope): side-loading unsigned games via SD is a piracy/authenticity consideration; optional cartridge/firmware signing could be added later but conflicts with the open "copy bits to SD" flow — deferred.

11.4 Save / persistent storage

Normative save medium: on-cart FRAM, battery-free — and the only save device. A saving cartridge carries a parallel FRAM (FM18W08, §11.4 below) alongside its NOR. FRAM is nonvolatile, SRAM-fast, ~10¹³-write endurance, and needs no battery — deliberately rejecting battery-backed SRAM, whose coin cell dies in 10–20 years and takes the save with it (the classic retro failure re8 refuses to ship).

EEPROM is removed as a supported type. Parallel EEPROM has ~5 ms page-program delays, page-write rules, ~10⁵ endurance and busy polling, so it cannot share FRAM's software contract: it would force two save protocols on every game, the SDK and the emulator (which would need a commit-timing model, since software can observe the busy state — making EEPROM part of the compatibility contract, not just the BOM). It also undercuts the battery-free-durability rationale: at 10⁵ writes a game autosaving every 30 s exhausts an EEPROM in about a month, whereas FRAM effectively never wears out. The BOM saving is small beside shell, PCB, flash and assembly. Result: re8.save.write has one contract — immediate, no polling, no busy state.

How it's wired — the save chip maps over the switchable window. The CPU map (§5) is fully allocated, so saves get no new address space; instead they temporarily replace the ROM bank, the standard cartridge-RAM idiom (Genesis/Mega Drive SRAM-over-ROM, GB MBC):

Save device & timing. The normative part is Infineon FM18W08 (32 K×8, 3.3 V, parallel, active production). A real FRAM is not an always-selected SRAM, so the access model is specified:

The FRAM sits directly on those lines with no glue logic; non-saving carts leave SAVE_CĒ unconnected and remain a single chip. So the connector is 36-pin (was 35): a saving cart is NOR + FRAM, a plain cart is NOR only. Save size is 16 KB (FM18W08 with A14 low); smaller codes are reserved. Declared in the header and mirrored into CART_CONFIG_SAVE.

Digital (SD) games don't use cart FRAM — the SD cartridge (§11.3) writes each game's save as a .sav file on the SD card, keyed to the game, using its own MCU + storage. So retail carts save to on-cart FRAM; digital titles save to SD files. No console-side shared NVRAM — it would tie saves to the console (not portable), need an allocation/management layer, and risk mixing with the boot flash; rejected.

Virgin contents. FRAM is not flash: it has no erased state, and a factory-fresh FM18W08 powers up holding whatever the die settled at (commonly $00, not guaranteed). The spec therefore declares virgin save contents unspecified, with the consequence stated plainly: a game MUST validate its own save using its own magic/checksum rather than assuming a blank pattern. This is a property of the hardware, not a modelling convenience. Separately — and not to be confused with it — an emulator initialises a newly created .sav to $00 for determinism.

Emulator persistence contract. The save is a flat image of the 16KB FRAM region (one contract — immediate writes, no busy state).


12. Expansion header (40-pin)

2×20, 2.54mm pitch, unpopulated on retail units. Taps existing signals (costs oito only its 4 JTAG pins; costs the CPU nothing):

Pins Group Signals
1–16 Address bus A0–A15
17–24 Data bus D0–D7
25–28 oito JTAG TCK, TMS, TDI, TDO
29–36 CPU control PHI2, R/W̄, RES̄, RDY, BE, SYNC, IRQ̄, DBḠ (probe debug-request to oito)
37–40 Power 2× 3.3V, 2× GND — ≤300 mA budget for an attached probe

Pin assignment is final: 8 control pins (29–36) carry every signal the probe needs — RDY/SYNC/BE for breakpoints and jamming, PHI2 + DBḠ for oito-mediated single-step (§13), RES̄, R/W̄, and IRQ̄ (so the probe can observe/inject interrupts). NMĪ is unused system-wide and not brought out. The header still costs oito only its 4 JTAG pins; the CPU-bus and control signals are already exposed.


13. Debug probe

Hardware: external accessory; RP2350B MCU — 48 GPIO in QFN-80, so all 36 non-power header signals connect simultaneously (16 address + 8 data + 4 JTAG + 8 control) with ~12 pins left for transceiver direction, status and spare; no multiplexing, so address-match-on-SYNC, RDY assertion, data jamming and JTAG all operate concurrently. RP2040 was insufficient — 30 GPIO cannot reach 36 signals. 3× PIO at 150 MHz gives extra margin for the breakpoint timing budget. oito JTAG also usable with J-Link/FT2232H + OpenOCD; ribbon cable to the 40-pin header; USB-C to PC; powered from header 3.3V. Sniffs the bus passively at full bus speed.

Probe electrical design. An MCU must not drive a 24-line CPU bus directly through a ribbon cable, and an accessory that can be unpowered or USB-connected while the console runs needs defined behaviour in every state:

Capabilities (all exploiting the fully-static W65C02S):

  1. Hardware breakpoints (timing budget): PC address sent from IDE; the probe state machine matches the address bus with SYNC high (opcode fetch, not data access) and, for addresses in the switchable window, the shadowed BANK_SELECT value — the probe tracks writes to $4080 off the bus, so a breakpoint in bank 3 does not fire in the other 63 and pulls RDY low; zero ROM/software overhead; the game runs at full speed until the breakpoint. The former claim of asserting RDY "within nanoseconds" is replaced by an actual budget at 7.159 MHz (cycle = 139.7 ns):
Element Budget
Address/SYNC valid after PHI2↑ (CPU tADS) ~30 ns
Inbound: transceiver + ~30 cm ribbon ~15 ns
PIO detect + assert (RP2350B @150 MHz, ~2 cycles) ~14 ns
Outbound: open-drain pull-down + RC on RDY ~20 ns
Total ~79 ns
Available before PHI2↓ within the same cycle ~70 ns

~79 ns does not fit in ~70 ns, so same-cycle stopping is not claimed. The probe instead asserts RDY for the following cycle, giving the full 139.7 ns against the ≈79 ns path — ≈61 ns of margin. Consequence: the CPU halts having fetched the breakpoint opcode but before the instruction completes; the probe reports the breakpoint PC from the latched fetch address and resumes by releasing RDY, after which the instruction executes normally. This is ordinary bus-level-breakpoint behaviour and is invisible to the developer. RDY pull-up/RC values are chosen to keep the outbound edge inside the budget, and the whole path MUST be validated on a logic analyser against real silicon before probe-firmware freeze.

  1. Single-stepping: oito is the clock master (§3) and gates its own PHI2 — no board-level clock mux, and PHI2 remains an oito output at all times, so the probe never drives it and there is no contention. The one DBḠ wire carries both the mode and the step requests: - DBḠ low = debug mode: oito stops free-running PHI2 and holds it in the low phase. - Each rising edge on DBḠ = "advance one cycle": oito emits exactly one complete PHI2 cycle, then halts again. - DBḠ held high exits debug mode and resumes free-running. - Acknowledgement is implicit — the probe observes the PHI2 pulse it requested; no extra pin is needed (the 40-pin header is fixed, §12). - Entry and exit are sampled at a PHI2 cycle boundary, so a cycle is never truncated mid-phase and CPU timing is never violated. The static core preserves all state between pulses.

  2. Register capture — "instruction jamming": freeze (RDY) → probe raises DBḠ → oito suppresses every external chip select (RAM, boot flash, cartridge, its own register decode) so nothing else drives the data bus → the probe drives only D0–D7, jamming PHA ($48)/PHX ($DA)/PHY ($5A)/PHP ($08) in place of fetches to capture A/X/Y/P as they are pushed, or PLA/PLX/PLY/PLP with probe-supplied bytes to modify; restore PC and release. - BE stays HIGH throughout. WDC specifies BE low makes Address, Data and R/W̄ high-impedance; since the data buffer is bidirectional, tri-stating it plausibly disables the CPU's data input path, in which case the CPU could never consume an injected opcode. Holding BE high also preserves A0–A15 and SYNC, so the probe can still see the fetch address and qualify injection on SYNC — information the earlier BE-based scheme discarded. - Clean separation of mechanisms: chip-select suppression is what makes jamming contention-free; BE is reserved for true bus mastering — when the probe, or oito during a PCM steal, must own the address bus. - Validation required: this rests on CPU-internal data-path behaviour that no datasheet states explicitly. It MUST be validated on real W65C02S silicon with published cycle waveforms before RTL freeze.

  3. Function injection (REPL): jam context-save, jam JMP $target (e.g. game.skipLevel), run to RTS, jam context-restore, jam JMP back. Parameters pass through a 4-byte RAM mailbox at $0010–$0013 (in the runtime ZP reservation §5.0): [command_id, arg0, arg1, arg2] — e.g. spawnEnemy(type,x,y) → [cmd, type, x, y].
  4. Live memory access: while the CPU is halted (RDY) and the probe owns the bus via BE, it reads/writes system RAM and drives VRAM/palette through their ports directly; VRAM/tile dumps can also be streamed. No "bus idle" guessing — halting via RDY + oito CS-control gives the probe deterministic bus ownership.
  5. Autonomous crash detection (best-effort): configurable heuristics, each disable-able: (a) hang = the same ≤3-address cycle repeating for > a threshold (default ~250ms) with no I/O writes; (b) out-of-bounds execution = a SYNC-high fetch from an address the IDE's symbol map marks non-code; (c) stack overflow = a push after SP wraps below a watermark. These cannot be provably correct for every program (a legitimate idle-loop can look like a hang), so they are advisory: on trigger, freeze + snapshot PC/SP/regs + report; the developer can tune or disable each.
  6. Execution trace: a rolling ring buffer of the last 1,024 executed opcode-fetch addresses, rendered as a call-stack-style timeline in VS Code.
  7. Ghost Debug Port: probe watches for writes to $4FFF and streams the data byte to the IDE console. 4 CPU cycles per character; zero RAM; inert without a probe attached. Same contract implemented by the emulator.
  8. oito debug TAP (JTAG): beyond IEEE 1149.1 boundary scan, oito implements a custom debug TAP: JTAG instructions select an internal debug address space that mirrors the register file, wave RAM, and a VRAM read window, so palette/line-buffer/blitter/APU state can be read (and injected) over JTAG independent of the CPU. Usable via the probe or off-the-shelf J-Link/FT2232H + OpenOCD.
  9. Dev bootstrap (§11.3): with a writable dev cartridge inserted, the probe halts the CPU and writes a streamed game image (or the firmware updater) into the cart's SRAM over the expansion-header CPU bus — cartridge-less development and firmware flashing with no console USB port and no cable. Small test programs can instead be loaded straight into the 16KB system RAM.

Shared control lines. Both the probe and oito can assert RDY and BE: oito for VRAM-port stalls, OAM DMA and PCM sample steals (§6.5/§8.3); the probe for breakpoints and jamming. Both are therefore wired-AND / open-drain, asserted-low, either master may pull. During probe jam cycles oito's debug mode already suppresses chip-selects and MUST likewise hold off its own bus-master steals, so a jammed instruction is never interleaved with a PCM fetch. (Practically: entering debug mode pauses PCM streaming; long halts may underrun the audio FIFOs, which is expected while stopped at a breakpoint and self-corrects on resume.)

Host protocol: the probe (RP2350B) exposes a USB CDC/vendor interface speaking a compact binary command set — halt/run, single-step, read/write memory, set/clear breakpoint, read trace, jam-registers, ghost-port stream, JTAG passthrough. The VS Code extension's debug adapter translates DAP ↔ this protocol, so the IDE side is standard Debug Adapter Protocol. Probe firmware uses the MCU's PIO to meet the bus timing (a 7.16MHz bus cycle is ~140ns; PIO at 150MHz has ample margin to sample SYNC/address and assert RDY within a cycle).


14. SDK and tooling

14.1 prog8 toolchain (normative language choice)

Illustrative snippet only: cpu=65c02, pc_start=8040 (header occupies $8000–$803F), launcher=none, ram_start=0000, ram_end=3FFF, zeropage_start=0020, zeropage_end=007F, symbols=true, plus romable. Custom boot stub emits vectors; custom syslib.prg replaces the C64 KERNAL layer (plus suggested vdp.prg, audio.prg).

14.2 re8 library (re8 / re8_engine modules)

Design rule: game code never contains a hex address — all registers are private const uword inside SDK modules; hardware revisions only update the SDK.

Documented API surface (final, event-driven style):

14.3 VS Code extension

14.4 Runtime IRQ vectoring & debug-info model

IRQ install on bare metal. The cartridge's fixed-bank vector at $FFFE/F is set at build time to a small syslib stub, __irq_entry, not to the user handler directly. The stub calls the RAM-vectored handler by pushing a synthetic return address before an indirect jump — a plain JMP ($0014) would have been a jump, leaving the handler's RTS to pop the interrupted code's return address :

__irq_entry: ; $FFFE/F → here, in the fixed bank
 PHA
 TXA : PHA
 TYA : PHA
 CLD ; 6502 IRQs do NOT clear decimal mode; prog8 assumes binary
 LDA #>(__irq_ret-1) ; push return address − 1, high byte first
 PHA
 LDA #<(__irq_ret-1)
 PHA
 JMP ($0014) ; handler's RTS returns here
__irq_ret:
 CMP #0 ; A = handler result: 0 = consumed, 1 = chain
 BEQ +
 JSR __sdk_default_irq
+ PLA : TAY
 PLA : TAX
 PLA
 RTI

IRQ-safe subset. This is the root cause behind the re-entrancy caveats elsewhere in this section. prog8 subroutines are non-reentrant because parameters live in fixed cells, so a callback that calls an SDK routine the main program is already inside overwrites that routine's parameters mid-call. Since the SDK's headline feature is event-driven callbacks, "event-driven" was doing rhetorical work that "safe" had not earned.

Bank safety across interrupts. BANK_SELECT ($4080) is global mutable state with three parties: main-line code (far_call, use_data_bank()), IRQ handlers, and oito itself during PCM steals — only the PCM path already restores the bank explicitly (§8.3). The dangerous case is concrete: main code selects bank 7 to read level data, an IRQ fires and switches to bank 3, and on return the main code keeps reading bank 3 believing it is bank 7 — silent corruption.

ABI (normative). The wrapper saves and restores A, X, Y (the CPU stacks P and PC); handlers need not preserve them. CLD is issued by the wrapper — the 6502 does not clear decimal mode on interrupt, and prog8 assumes binary arithmetic. The consume/chain result is sampled from A immediately on return, before any restore. Cost ≈ 13 bytes, ≈ 20 cycles of prologue. The NMI vector points at the address of a real RTI instruction in the fixed bank (not "conceptually a bare RTI"). Re-entrancy caveat: because the handler is a called routine, it may call only @irqsafe SDK routines — see the IRQ-safe subset in §14.4. The RAM vector is initialized to a default handler (VBLANK frame-counter tick, else RTI). sys.set_irq(&fn) is then simply an atomic write of that RAM cell — wrapped by sys.set_irqd()/sys.clear_irqd() (SEI/CLI) so an interrupt can't fire mid-update. This is the classic C64 CINV pattern retargeted to a fixed RAM cell instead of a KERNAL vector; no OS is required. (NMI is unused system-wide; its $FFFA/B vector points at the address of a real RTI instruction.) The handler's ubyte return (§14.1: 0 = consumed, 1 = fall through) lets the stub optionally chain to the SDK default before RTI.

Variable watch/hover. prog8 uses static allocation with no runtime stack frames — every variable and every subroutine parameter lives at a fixed address. A flat, typed symbol table therefore describes all program state; there are no stack-relative locals needing DWARF frame-base computation. So the debug adapter does not synthesize .debug_info for the prog8 path. Instead it builds its own symbol model from prog8's -dumpvars output (name → address → prog8 type: ubyte/uword/byte/word/bool/arrays/structs) and serves watch/hover by reading memory (live USB read or emulator) and formatting per that type. Typed-symbol source — an open dependency with a defined resolution path. The typed-symbol model is sound, but its data source is unconfirmed: -dumpvars may carry only names and addresses rather than types. This is closed by choosing, not hoping:

  1. Capture reality first — run the pinned prog8c, record the actual -dumpvars/-dumpsymbols formats, and archive samples as versioned fixtures.
  2. Select one source, in preference order: (a) -dumpvars if it genuinely carries types; (b) otherwise the SDK build emits its own type table alongside the symbol dump — mechanical and entirely under our control; (c) a small upstream patch only if neither suffices.
  3. Version the schema. Whatever the source, the debug adapter consumes a versioned typed-symbol schema, so a compiler change fails the build visibly instead of silently mis-rendering watch values.
  4. Required test coverage: arrays, structs, aliases, private/nested scope names, and banked symbols — the last matters because per-bank compilation means the same CPU address exists in several banks, so symbol identity is (bank, address), not address alone. Breakpoint identity has the same requirement and shares the fix.

Until this is settled against the pinned binary it remains a dependency, not an established fact. The two symbol inputs each have exactly one job — the 64tass listing gives line↔address (for stepping/breakpoints via the synthesized ELF), -dumpvars gives typed symbols (for inspection) — so they're complementary, not competing (this retires the old "two symbol paths" concern). With the TypeScript/LLVM-MOS front-end dropped, this typed-symbol model is the single debug-info path.

14.5 Reference emulator

Accuracy tiers. The front matter has always said "non-cycle-perfect emulator" while this section asked for "cycle-accurate" line rendering — a contradiction that survived every revision. Two named tiers replace it, and Tier 1 is what "the reference emulator" means:

Tier 1 — compatibility (the conformance target). Runs any conforming cartridge correctly:

Tier 2 — cycle-exact (optional refinement). Adds exact display-fetch scheduling and arbitration, precise RDY stall counts, mid-scanline raster effects at cycle resolution, and exact placement of DMA and PCM steals.

Golden conformance ROMs validate Tier 1. Tier 2 is explicitly not required for compatibility.

C++ or Rust core compiled to WASM, hosted in a VS Code webview; Tier 1 line-rendering/blitting behaviour (Tier 2 cycle-exactness is a later, optional refinement — see the tiers above); host keyboard/USB-gamepad input mapped through the timestamped input-event model, not sampled once per frame; ghost-port hook (if (addr == 0x4FFF) send_to_console(byte)); shares the VRAM-inspector bridge with the probe. Internal architecture (scheduler, timing granularity, PSG model, save states, accuracy validation) entirely unspecified — that is precisely what the emulator-planning stage must define.


15. Known restrictions (hard limits)

  1. 16,384 bytes system RAM total — ≈14.4 KB (14,704 B) available to game code after ZP, hardware stack, the 1 KB shadow-OAM buffer and SDK structures (full budget in §5.0; the former "≈15.5 KB" predated shadow OAM). Compile-time enforced against the real remainder.
  2. No dynamic memory. Ordinary subroutines are non-reentrant (static parameter cells): no automatic recursion. Manual recursion is possible with programmer-managed state, bounded in practice by the 256-byte hardware stack (restriction 10).
  3. 64KB CPU address space; cartridge access only through one switchable + one fixed 16KB window.
  4. 64 simultaneous colors (4 × 16 sub-palettes) from 4,096; color 0 of each sprite palette transparent; 16 colors max within one tile.
  5. 128 hardware sprites; 32 per scanline.
  6. 240p progressive only (256×224 / 320×224 @60Hz); no interlace, no 480i; 60Hz-only worldwide, no 50Hz PAL mode.
  7. 16-voice APU: 4 PSG (3 pulse + 1 noise) + 8 wavetable + 4 PCM/ADPCM (§8); no FM operators (deliberate — flexibility + emulability over Genesis-style FM).
  8. Blitter throughput: ≥1,000 tiles/frame interleaved / ≥350 blanking-only, guaranteed under the §6.5 benchmark workload (opaque tile copy, pitch 0, both BG planes, no sprites, text off). Other modes and loads are lower — see §6.5.
  9. CPU reaches VRAM only through the $4044 port and is RDY-stalled during bus contention (§6.5); bulk VRAM work is fastest in the 38-line VBLANK window but is legal any time. 9a. PCM streaming steals CPU cycles: oito bus-masters the CPU bus to fetch samples from cart ROM, costing ≈2.7–6 % of CPU cycles under the §8.3 workload (4 voices, 8-bit, 48 kHz). PCM sample addresses are 20-bit (1MB, the cart limit). The nominal 455 cycles/line is a bus budget; RDY stalls from PCM, the VRAM port and OAM DMA reduce instruction throughput (§3).

  10. Single IRQ, no NMI; 256-byte hardware stack (callback nesting is the documented "interrupt hell" risk).

  11. Digital video is DVI-D 1080p60, video-only (no HDMI/HDCP/TERC4-audio/CEC), fixed via CH7035B Strategy-A self-boot — no runtime mode changes. Digital audio is a separate royalty-free optical S/PDIF output (§8.4); the design carries no HDMI-only signalling; the licensing conclusion itself requires counsel.
  12. Saves use on-cart FRAM only (FM18W08, battery-free, 16KB), oito-decoded via a per-access-pulsed SAVE_CĒ (§11.4); no EEPROM, no battery-backed SRAM, no console-side shared NVRAM. Digital/SD titles save to .sav files on the SD card.
  13. Keyboard & mouse are PS/2 only (no USB-HID), reached through a controller port via a passive adaptor (§9.1/§9.2); the PS/2 host is bidirectional, so keyboard LEDs are supported. With two ports total, a keyboard or mouse occupies one — the realistic combinations are joystick+keyboard, joystick+mouse, or keyboard+mouse, never all three. Simultaneous-key limits come from the keyboard's own matrix ghosting rather than the interface, which is NKRO-capable. Each port supplies ≤250 mA at 5V (§2.1).
  14. Mouse cursor is hardware-rendered by oito as an absolute-top layer (§9.2) — one VRAM tile in 8×8 mode, four consecutive tiles in 16×16 mode, not a sprite; the game supplies only the tile, hotspot, and velocity and receives move/click/wheel events (no software cursor drawing).
  15. The DE-15 output carries 15 kHz analog RGBHV, not VGA timing — standard PC monitors will not sync (it targets CRTs/PVMs/upscalers) — and carries no audio.
  16. Cartridge max 1MB (64 × 16KB banks) as designed.

16. Conformance & validation

This section is the acceptance contract: what an implementation must reproduce, and what must still be proven on real parts.

16.1 Emulator conformance (Tier 1)

An implementation claiming Tier 1 (§14.5) MUST reproduce:

Artefact Definition
Register model every register in §5.1 with its reset value, mask, read/write side effects and unmapped behaviour ($00, §5.1)
Golden framebuffer hashes per-frame hashes for conformance ROMs exercising both BG planes, all sprite sizes, priority ladder, collision, blitter modes, text overlay and cursor/pick
Golden audio hash PCM output of a fixed register-write script plus sample ROM, bit-exact (§8.5)
IRQ trace ordered (cycle, source) list for a fixed program — VBLANK, collision, input, raster-compare, blitter-done
Bank/save behaviour BANK_SELECT masking, fixed-bank derivation, save-window swap, .sav image and CRC keying (§11)
Reset & handoff state the §11.1.1 handoff table, byte-identical between canonical-boot and fast-boot paths

Tier 2 (cycle-exact) is OPTIONAL and is not required for compatibility.

16.2 Hardware validation still outstanding

These are unproven on real parts and gate schematic freeze or tape-out:

Item Criterion Ref
CH7035B qualification accepts both native geometries via DE; true nearest-neighbour; 1080p60 out; latency ≤1 frame; register image archived §7.1
AD725 240p operation colour lock, burst insertion, vertical lock on CRTs and capture devices/upscalers §7
W65C02S instruction jamming opcode injection with BE high and chip-selects suppressed, on silicon, with published waveforms §13
Breakpoint timing RDY asserted within the following cycle, measured on a logic analyser §13
prog8 ROM-ability %option romable output with no write target outside $0000–$3FFF, proven by the build gate and a published fixture §14.1
Typed-symbol source actual -dumpvars format captured; typed-symbol schema selected and versioned §14.4
Debug-info pipeline line tables, stepping and banked duplicate addresses proven per host §14.3
Package pinout full 144-pin table: per-state directions, pad types, drive classes, bank supplies §6.3
Licensing position HDMI/DVI/S-PDIF review by counsel before commercialisation §7.1

16.3 Rules that keep this document honest


17. Design history

The architectural decision log, the gaps & inconsistencies registers from both adversarial reviews, and the source provenance live in re8-design-history.md.

That document records why each choice was made and what was rejected. It is not normative: where it and this specification disagree, this specification wins and the disagreement is a defect.


18. Version history

This specification is versioned so that a cartridge, an emulator or a board revision can state precisely which contract it was built against.

18.1 Versioning rules

Change Effect
Machine-visible behaviour — any register, bit field, reset value, timing, data format, memory-map or ABI change MUST bump the version. Before 1.0 this is a minor bump; after 1.0 it is a major bump and MUST carry a compatibility note naming what breaks
New capability that does not alter existing behaviour minor bump
Clarification, correction of an error, or editorial work that leaves behaviour unchanged recorded in the table below; a patch bump (0.1.1) is used when a released version needed correcting
Rationale, alternatives, review history no bump — these live in re8-design-history.md, which is not normative

Milestones. 0.x are pre-freeze drafts: machine-visible behaviour may still change between them. 1.0 is the compatibility freeze — it is issued only once every item in §16.2 has been validated on real parts, and from that point the register map, timing and file formats are fixed for the life of the platform.

Related versions. Two artefacts carry their own version numbers because software can observe them independently: the handoff-state version (§11.1.1), bumped whenever boot firmware changes the state a cartridge starts in, and the cartridge header version ($04, §11.2). Neither is implied by this document's version.

18.2 Revisions

Version Date Summary
0.1 2026-07-26 First tagged version. Establishes the complete baseline: W65C02S at 7.159 MHz with the oito ASIC (LQFP-144); the canonical $4000–$40FF register file; 128 KB VRAM with defined arbitration; two BG planes, 128 sprites, blitter, collision and the priority ladder; the text overlay and hardware cursor with hit-testing; the 16-voice APU with its reference model; five simultaneous video outputs plus optical S/PDIF; PS/2 keyboard and mouse over the controller ports; the two-stage boot ROM, cartridge format and FRAM saves; the prog8 toolchain and debug probe; and the conformance contract of §16. Incorporates the resolution of two adversarial reviews (see re8-design-history.md).

Add each subsequent revision as a new row at the top of this table, newest first.