Appendix A: Memory Map¶
The X65 exposes a flat 24-bit address space to the 65C816. Most of that space is plain PSRAM; a 512-byte window at the top of bank 0 is carved up into memory-mapped-I/O regions owned by the custom chips, and a small expansion window sits slightly below it. This appendix documents every byte of that MMIO region.
The canonical live spreadsheet is at https://tinyurl.com/x65-memory-map:
The tables below mirror that spreadsheet for offline and search-engine-friendly reference.
Top-Level MMIO Layout¶
Bank 0, pages $FC–$FF:
Range |
Owner |
Notes |
|---|---|---|
|
Expansion slots |
16 slots × 16 bytes |
|
SGU-1 (sound) |
64-byte channel-switched window |
|
CGIA (graphics) |
128 registers |
|
GPIO expander / joystick |
Reserved (currently stubbed, reads return |
|
System timers |
Two CIA-compatible 16-bit counters, 1 µs resolution |
|
RGB LED chain |
4 direct RGB332 + 4-byte chain protocol |
|
System buzzer |
16-bit log frequency + 8-bit duty |
|
Reserved |
Reads return |
|
USB HID (keyboard / mouse / gamepad) |
Device-selector at |
|
RIA |
Fastcall API window at |
Everything outside the top half-page ($FExx–$FFxx) and the expansion slots at $FCxx is plain PSRAM and available to software. Bank crossing happens on the fly at $800000.
$FC00–$FCFF — Expansion Slots¶
Sixteen 16-byte slots selected by address bits $FC?0–$FC?F. The expansion port routes four IO_EN signals and four IO_INT signals; which slot a given board responds to is board-specific. A typical OPL-3 card, for example, sits at $FC00–$FC1F.
No register layout is imposed by the core system — each expansion board defines its own map. See Chapter 6: Input and Output Interfaces for the expansion port pinout.
$FEC0–$FEFF — SGU-1¶
SGU-1 presents a single 64-byte window that is re-bound to a specific channel by writing a channel index to the last byte. Channels are numbered 0–8; writing any value wraps modulo-9, so there is no special “service” channel index. The 65816 never sees a “global” SGU register file — everything is per-channel.
The selector:
Offset |
Register |
R/W |
Notes |
|---|---|---|---|
|
|
R/W |
Write: select channel ( |
Once a channel is selected, the first 32 bytes ($00–$1F) are four operators of 8 bytes each; the next 32 bytes ($20–$3F) hold channel-wide controls.
Operators (4 × 8 bytes at $00–$1F)¶
Each operator occupies eight bytes. Operator n starts at offset 0x08*n.
Offset |
Register |
Bit layout |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The 5-bit envelope rates AR and DR are split across R2 and R7. TL (Total Level) is 7 bits, split as R1[5:0] plus R6[0]. SL is 4 bits, RR 4 bits, SR 5 bits. WAVE selects one of eight per-operator waveforms (0 SINE, 1 TRIANGLE, 2 SAWTOOTH, 3 PULSE, 4 NOISE, 5 PERIODIC_NOISE, 6 reserved, 7 SAMPLE). WPAR shapes SINE/TRIANGLE/SAWTOOTH, picks a tap configuration for PERIODIC_NOISE, or sets a fixed pulse width for PULSE.
Channel Controls (32 bytes at $20–$3F)¶
Offset |
Register |
Notes |
|---|---|---|
|
|
Channel base frequency, low byte |
|
|
Channel base frequency, high byte |
|
|
Channel volume (signed) |
|
|
Stereo pan (signed; negative = left, positive = right) |
|
|
Gate, PCM-enable (bit 3), filter mode, ring modulation |
|
|
Phase reset, filter reset, PCM loop, per-sweep enables |
|
|
Filter cutoff, low byte |
|
|
Filter cutoff, high byte |
|
|
Pulse width for PULSE waveform (0–127) |
|
|
Filter resonance (0–255; feedback is 256 − RESON) |
|
|
Current PCM sample position, low byte |
|
|
Current PCM sample position, high byte |
|
|
PCM end boundary, low byte |
|
|
PCM end boundary, high byte |
|
|
PCM loop restart, low byte; also the 1024-sample wavetable base for |
|
|
PCM loop restart, high byte |
|
|
Frequency-sweep speed, low byte |
|
|
Frequency-sweep speed, high byte |
|
|
Frequency-sweep amount + direction/mode |
|
|
Frequency-sweep boundary |
|
|
Volume-sweep speed, low byte |
|
|
Volume-sweep speed, high byte |
|
|
Volume-sweep amount + mode |
|
|
Volume-sweep boundary |
|
|
Cutoff-sweep speed, low byte |
|
|
Cutoff-sweep speed, high byte |
|
|
Cutoff-sweep amount + mode |
|
|
Cutoff-sweep boundary |
|
|
Phase-reset timer, low byte |
|
|
Phase-reset timer, high byte |
|
reserved |
|
|
|
Channel selector (see above) |
PCM sample data itself lives in 64 KB of RAM internal to the audio chip, addressed via the PCM_POS / PCM_END / PCM_RST pointers; it is not visible in the 65816’s address space.
Writes to this window are intercepted by NORTH and forwarded to the audio chip via the SPU device on the PIX bus (see Chapter 2: System Architecture Overview). The audio chip is a dedicated RP2350 bridged from SOUTH over SPI; the SOUTH-side driver caches register values so repeat reads avoid a round-trip.
$FF00–$FF7F — CGIA¶
CGIA exposes 128 registers. The most relevant top-level ones:
Offset |
Register |
Notes |
|---|---|---|
|
|
|
|
|
Encodes one of 24 Z-order permutations of the four planes (Steinhaus-Johnson-Trotter) |
The remaining registers fall into four per-plane banks of sixteen registers each; see Chapter 4: Graphics and Display and Chapter 11: Graphics Programming for the plane-register map, display-list instruction encoding, and sprite-descriptor format.
$FF80–$FF97 — GPIO Expander (reserved)¶
This 24-byte window is reserved for the on-board PCAL6416A GPIO expander that routes the DE-9 joystick inputs. In the current firmware the region is stubbed: all reads return $FF and writes are ignored. Until it is activated, use USB HID gamepads via $FFB0–$FFBF.
$FF98–$FF9F — System Timers (CIA-Compatible)¶
Two 16-bit countdown timers with 1 µs resolution, modelled on the MOS 6526 CIA.
Offset |
Register |
R/W |
Notes |
|---|---|---|---|
|
|
R/W |
Timer A counter, low byte |
|
|
R/W |
Timer A counter, high byte |
|
|
R/W |
Timer B counter, low byte |
|
|
R/W |
Timer B counter, high byte |
|
— |
— |
Reserved, reads |
|
|
R/W |
Interrupt control / flags |
|
|
R/W |
Timer A control |
|
|
R/W |
Timer B control |
Counter semantics. Reading a counter returns the current remaining count (in µs). Writing the low byte latches it; writing the high byte loads the latched 16-bit pair into the counter when the timer is stopped, or sets the reload value used on underflow when running.
ICR bits. [0] Timer A underflow, [1] Timer B underflow, [7] any-interrupt summary. Reading ICR clears all pending flags. To set or clear interrupt enables: write with [7]=1 to set the bits listed in [1:0], [7]=0 to clear them.
Control registers (CRA/CRB). [0] START, [3] RUN_MODE (0 continuous, 1 one-shot), [4] FORCE_LOAD. CRB additionally has [6:5] INPUT_MODE (0 counts PHI2, 2 counts Timer A underflows, useful for compounding to a 32-bit period).
$FFA0–$FFA7 — RGB LED Chain¶
The X65 DEV-board carries four on-board WS2812B-style RGB LEDs and supports a chain of up to 256 LEDs via the expansion port’s WS2812 data line.
Offset |
Register |
R/W |
Notes |
|---|---|---|---|
|
|
R/W |
Direct RGB332 colour for LED 0; write commits immediately |
|
|
R/W |
Direct RGB332 colour for LED 1 |
|
|
R/W |
Direct RGB332 colour for LED 2 |
|
|
R/W |
Direct RGB332 colour for LED 3 |
|
|
R/W |
Red byte (0–255); writing here commits the chain update |
|
|
R/W |
Green byte (0–255); latched |
|
|
R/W |
Blue byte (0–255); latched |
|
|
R/W |
LED index in the chain (0–255); latched |
RGB332 byte ($FFA0–$FFA3): [7:5] R · [4:2] G · [1:0] B. A single STA $FFA0 sets LED 0 — the LED 0–3 interface is one instruction per LED.
Chain protocol ($FFA4–$FFA7): to set LED n to 24-bit colour, write the LED index to $FFA7, the green byte to $FFA5, the blue byte to $FFA6, and finally the red byte to $FFA4. The final write to $FFA4 is what dispatches the update to the hardware; the other three bytes are simply latched. (Order of the latch writes is free; only the write to $FFA4 must be last.)
$FFA8–$FFAB — System Buzzer¶
A PWM-driven piezo buzzer. Two commands are exposed to the CPU; each write forwards a PIX command to the south-side driver.
Offset |
Register |
R/W |
Notes |
|---|---|---|---|
|
|
R/W |
Frequency, low byte (see encoding) |
|
|
R/W |
Frequency, high byte |
|
|
R/W |
Duty cycle, 0 (silent) – 255 (50 % square peak) |
|
reserved |
R/W |
Currently unused |
Frequency encoding. The 16-bit value FREQ = FREQ_H:FREQ_L is mapped logarithmically to audio Hz:
$$ f(\text{FREQ}) = 20,\text{Hz} \cdot 2^{10,\text{FREQ}/65535} $$
This covers roughly 20 Hz to 20 kHz across the 16-bit range. Writing either byte commits the new frequency. Writing $FFAA commits a new duty cycle independently.
$FFB0–$FFBF — USB HID¶
USB keyboards, mice, and gamepads attached to the NORTH chip’s USB host stack are exposed as a 16-byte window whose contents depend on which device is currently selected.
Offset |
Register |
R/W |
Notes |
|---|---|---|---|
|
|
W |
Device selector: |
|
device data |
R |
Depends on selected device |
Device-type codes (HID_SEL[3:0]):
Code |
Device |
High-nibble meaning |
|---|---|---|
|
Keyboard |
|
|
Mouse |
Ignored |
|
Gamepad |
|
Writing to offset $FFB0 commits the selector; the other fifteen bytes in the window are read-only.
Keyboard (selector low nibble 0)¶
The full keyboard state is 256 bits (32 bytes) — one bit per HID keycode. Bit n is set iff the key with HID keycode n is currently pressed. Because only 16 bytes are visible at a time, the window is split into two pages; set HID_SEL to $00 to read bytes 0–15 of the state at $FFB0–$FFBF, and $10 to read bytes 16–31.
The first byte ($FFB0 when page 0 is selected) carries device status: [0] connected, [1] NUMLOCK LED, [2] CAPSLOCK LED, [3] SCROLLLOCK LED.
Mouse (selector low nibble 1)¶
Offset |
Field |
Notes |
|---|---|---|
|
Buttons |
|
|
X delta (8-bit) |
Signed |
|
Y delta (8-bit) |
Signed |
|
Wheel |
Signed |
|
Pan |
Signed horizontal wheel |
|
X / Y counters |
16-bit absolute X and Y counters |
Gamepad (selector low nibble 2)¶
Ten-byte snapshot for the selected pad (or for pad 0, the OR of all connected pads):
Offset |
Field |
Notes |
|---|---|---|
|
D-pad + features |
|
|
Stick digitals |
|
|
Buttons 0 |
Bits 0–7 of the button bitmap |
|
Buttons 1 |
Bits 8–15, including the Home button at bit 4 |
|
Left stick X |
Signed 8-bit |
|
Left stick Y |
Signed 8-bit |
|
Right stick X |
Signed 8-bit |
|
Right stick Y |
Signed 8-bit |
|
Left trigger |
Unsigned 8-bit |
|
Right trigger |
Unsigned 8-bit |
The merged-pad 0 view is the bitwise OR of all connected pads across every field. It is the right endpoint for single-player code that should accept input from any controller; multiplayer code should loop across pads 1–4.
$FFC0–$FFFF — RIA¶
The RIA registers live at the very top of bank 0. They cover firmware status, the fastcall API at $FFF0–$FFF3, the native-mode 65816 vector table at $FFE4–$FFFF, and a handful of system services.
The 65816 native-mode vectors (reserved by the CPU) live at fixed offsets:
Offset |
Vector |
|---|---|
|
COP (native) |
|
BRK (native) |
|
ABORTB (native) |
|
NMIB (native) |
|
IRQB (native) |
|
COP (emulation) |
|
ABORTB (emulation) |
|
NMIB (emulation) |
|
RESB |
|
IRQB / BRK (emulation) |
Because the X65 boots and operates exclusively in native mode, the emulation-mode vectors exist for completeness but are not used by X65 firmware or applications.
The fastcall window at $FFF0–$FFF3 is the primary entry point for system calls. Arguments are passed through the 512-byte XSTACK maintained by the RIA. The full RIA register list is documented in the spreadsheet linked at the top of this appendix.