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 |
4 cards × 128 bytes |
|
SGU-1 (sound) |
64-byte channel-switched window |
|
CGIA (graphics) |
128 registers |
|
GPIO expander / joystick |
PCAL6416A, 2× DE-9 (not decoded by firmware yet) |
|
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 |
Math, TOD, DMA, files, UART, IRQ, fastcall API, CPU vectors |
Everything outside the top half-page ($FExx–$FFxx) and the expansion window at $FCxx–$FDxx is plain PSRAM and available to software; chunks of the expansion window can be reclaimed as PSRAM through EXTIOCTL (see below). Bank crossing happens on the fly at $800000.
$FC00–$FDFF — Expansion Slots¶
The expansion window is 512 bytes, divided into four 128-byte slots — one per card. The expansion port routes four IO_EN signals and four IO_INT signals, and a card claims its slot through its own IO_EN line:
Range |
Slot |
Enable |
|---|---|---|
|
0 |
|
|
1 |
|
|
2 |
|
|
3 |
|
The whole window belongs to the expansion bus by default. Few peripherals have any use for 128 registers, though, so the RIA can hand parts of it back: EXTIOCTL ($FFF6) is a bitmap of eight 64-byte chunks, two per slot, and setting a bit maps that chunk back to RAM. A card that only needs 64 bytes therefore need not cost the program the other 64.
Bit |
Chunk range |
Slot |
|---|---|---|
0 |
|
0 |
1 |
|
0 |
2 |
|
1 |
3 |
|
1 |
4 |
|
2 |
5 |
|
2 |
6 |
|
3 |
7 |
|
3 |
EXTIOCTL powers up as $00, so a freshly started machine has the entire $FC00–$FDFF window on the expansion bus and none of it as memory. Reading a chunk that is on the bus with no card answering returns $FF; writes to it go nowhere.
No register layout is imposed by the core system — each expansion board defines its own map within its slot, and a board is free to use only the first few bytes of it. 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 bank by writing a selector to the last byte. Values $00–$08 select the nine synthesis channels. $FF selects the service bank, which reaches past the synthesis engine: PCM sample data is uploaded through it, and the master volume there is a proxy for the CODEC/DSP downstream of the waveform generator. Everything in between ($09–$FE) is reserved: writes are ignored and reads return $FF. The selector is stored verbatim; out-of-range values are not wrapped onto a channel.
SGU-1 comes up muted. The service bank’s master volume gates the entire mix and resets to 0, so the chip is silent until software raises it. This is deliberate: a reset must never blast the user with whatever the channel register file happened to power up holding. Put the channels into a known state first, then unmute. Under OS/816 that is the system’s job; a bare-metal program has to do it itself, or it will hear nothing.
The selector:
Offset |
Register |
R/W |
Notes |
|---|---|---|---|
|
|
R/W |
Write: select bank — |
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) |
|
|
|
|
|
Phase reset, filter reset, PCM loop, per-sweep enables, |
|
|
Filter cutoff, low byte |
|
|
Filter cutoff, high byte |
|
|
Pulse width, signed ( |
|
|
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 |
|
|
LFO shapes: |
|
|
Bank 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 SGU-1 is a self-contained module hosted on the board and 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 control block occupies the first 64; the upper 64 are four per-plane banks.
Address |
Register |
R/W |
Notes |
|---|---|---|---|
|
|
R/W |
|
|
|
R/W |
Bank number for background-plane fetches |
|
|
R/W |
Bank number for sprite fetches |
|
|
R |
Current raster line (16-bit) |
|
|
R |
Raster status bits |
|
|
R/W |
Line at which to fire a raster interrupt (16-bit) |
|
|
R/W |
|
|
|
R/W |
Same layout; write to acknowledge |
|
|
R/W |
|
|
|
R/W |
Encodes one of 24 Z-order permutations of the four planes (Steinhaus-Johnson-Trotter) |
|
|
R/W |
Backdrop / border colour |
|
|
R/W |
Plane 0 display-list or sprite-descriptor table start (16-bit) |
|
|
R/W |
Plane 1 table start |
|
|
R/W |
Plane 2 table start |
|
|
R/W |
Plane 3 table start |
|
|
R/W |
Plane 0 registers — interpretation depends on plane type and active mode |
|
|
R/W |
Plane 1 registers |
|
|
R/W |
Plane 2 registers |
|
|
R/W |
Plane 3 registers |
Addresses not listed inside $FF00–$FF3F are reserved. For the per-plane register layouts 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¶
This 24-byte window maps the on-board PCAL6416A I²C GPIO expander behind the two DE-9 ports — bi-directional, 5 V-TTL general-purpose I/O that conventionally carries Atari-style joysticks (see Chapter 6). Expander port 0 is the first connector, port 1 the second; port bits 0–7 map to DE-9 pins 1–7 and 9 in order (pin 8 is ground). Under the joystick convention that is Up/Down/Left/Right on bits 0–3, fire on bit 5 (pin 6), and buttons 2/3/4 on bits 7/4/6 (pins 9/5/7), all active-low. Bit 6 can instead be driven high as an output to recreate the classic +5 V supply on pin 7 for old-machine accessories — optional, at the cost of button 4 (see Chapter 6). Registers pair up as xx0 for port 0 and xx1 for port 1:
Address |
Register |
R/W |
Reset |
Notes |
|---|---|---|---|---|
|
|
R |
live |
Input port — actual pin levels regardless of direction; reading clears that port’s pending interrupt |
|
|
R/W |
|
Output port — level driven on output-configured pins; reads return the written value, not the pin |
|
|
R/W |
|
Polarity inversion — 1 inverts that input’s sense in |
|
|
R/W |
|
Direction — 1 = high-impedance input, 0 = output |
|
|
R/W |
|
Output drive strength, two bits per pin ( |
|
|
R/W |
|
Input latch — 1 = a level change is held in |
|
|
R/W |
|
Pull resistor enable — 1 connects the internal 100 kΩ resistor |
|
|
R/W |
|
Pull direction — 1 = pull-up, 0 = pull-down (needs |
|
|
R/W |
|
Interrupt mask — clear a bit to request an IRQ on that pin change; all masked at power-on |
|
|
R |
|
Interrupt status — 1 = this pin is the interrupt source |
|
— |
— |
— |
Reserved |
|
|
R/W |
|
Output stage per port: bit 0 = port 0, bit 1 = port 1; 0 = push-pull, 1 = open-drain |
The expander’s INT output feeds bit 1 of the RIA interrupt controller ($FFEC/$FFED). The interrupt-mask registers are its defining feature for the X65: software asks for IRQs only on the pin transitions it cares about, instead of re-reading every pin on every change.
Note
The NORTH firmware does not decode this window yet — reads currently 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 RIA decodes four direct LED registers and the firmware drives all four (RGB_LED_COUNT 4), but the X65 DEV-board populates only the first three WS2812B-style RGB LEDs; $FFA3 addresses a fourth position you can populate yourself on the strip header. A chain of up to 256 LEDs is supported 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 (position unpopulated on the DEV-board) |
|
|
R/W |
LED index in the chain (0–255); writing here commits the chain update |
|
|
R/W |
Red byte (0–255); latched |
|
|
R/W |
Green byte (0–255); latched |
|
|
R/W |
Blue byte (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, latch the red, green and blue bytes into $FFA5, $FFA6 and $FFA7, then write the LED index to $FFA4. The 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) |
|
|
R/W |
Reserved; currently unused |
Frequency encoding. The 16-bit value FREQ = BUZZ_FREQ_HI:BUZZ_FREQ_LO 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. The 65C816 reserves two vector tables up here — native at $FFE4–$FFEF and emulation at $FFF4–$FFFF — and the RIA fills the gaps the CPU leaves in and around them with system services: hardware multiply/divide, a time-of-day counter, DMA, file descriptors, the UART, an RNG, the interrupt controller and the fastcall API.
Hardware Multiply and Divide¶
Address |
Register |
R/W |
Notes |
|---|---|---|---|
|
|
R/W |
Operand A (16-bit) |
|
|
R/W |
Operand B (16-bit) |
|
|
R |
|
|
|
R |
Signed |
Both results are computed combinatorially from the current operands — write the operands, then read the result; there is no “start” or “busy” handshake. Division by zero yields $FFFF.
Time of Day¶
Address |
Register |
R/W |
Notes |
|---|---|---|---|
|
|
R |
Monotonic microseconds since boot (48-bit, little-endian) |
DMA¶
Address |
Register |
R/W |
Notes |
|---|---|---|---|
|
|
R/W |
Source address (24-bit) |
|
|
R/W |
Source step |
|
|
R/W |
Destination address (24-bit) |
|
|
R/W |
Destination step |
|
|
R/W |
Transfer count |
|
|
R |
Transfer |
File Descriptors¶
Address |
Register |
R/W |
Notes |
|---|---|---|---|
|
|
R/W |
File-descriptor A number (obtained from the |
|
|
R/W |
File-descriptor B number |
|
|
R/W |
Read a byte from FDA / write a byte to FDA |
|
|
R/W |
Read a byte from FDB / write a byte to FDB |
|
|
R |
File-descriptor A status |
|
|
R |
File-descriptor B status |
Streaming a file is a tight loop on FDARW — no API round-trip per byte.
UART, RNG and Interrupt Controller¶
Address |
Register |
R/W |
Notes |
|---|---|---|---|
|
|
R |
UART FIFO flow control — TX-ready and RX-available flags |
|
|
R/W |
Write to transmit, read to receive |
|
|
R |
Random number generator; two bytes so 16-bit values can be read at once |
|
|
R/W |
RIA interrupt enable mask |
|
|
R |
Interrupt controller status — which source raised |
IRQ_ENABLE and IRQ_STATUS sit in the two bytes the 65C816 leaves reserved inside the native vector table, and EXTIOCTL / EXTMEM (below) occupy the matching reserved pair in the emulation table.
Fastcall API¶
Address |
Register |
R/W |
Notes |
|---|---|---|---|
|
|
R/W |
Write the API operation id to begin a kernel call; read the return value |
|
|
R |
High byte of a 16-bit return value, otherwise |
|
|
R/W |
XSTACK port — 512 bytes for passing call parameters |
|
|
R |
|
Extension Control¶
Address |
Register |
R/W |
Notes |
|---|---|---|---|
|
|
R/W |
Bitmap of the eight 64-byte chunks of |
|
|
R/W |
Reserved for future use (extended-memory MMU) |
CPU Vectors¶
The 65C816 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.