Chapter 6: Input and Output Interfaces¶
The X65 surfaces its I/O through a small, disciplined register window at the top of bank 0 and a handful of physical connectors on the board. This chapter walks the hardware side of each subsystem — what it is, where it lives in memory, and how the pieces fit together. Assembly-level programming sequences for the same subsystems are covered in Chapter 13: Input/Output Handling.
UART and the Monitor Console¶
The NORTH chip exposes a simple UART at $FFE0–$FFE1 — two registers used to talk to the on-board monitor and, through it, to the OS and to applications.
Address |
Register |
R/W |
Notes |
|---|---|---|---|
|
|
R |
|
|
|
R |
Receive a byte; clears the RX-ready flag |
|
|
W |
Send a byte; dropped if |
Polling is idiomatic: read $FFE0, test [7] before writing, test [6] before reading. A byte-per-call discipline keeps the firmware UART buffer happy.
The same UART register pair also carries the USB-CDC serial path. When the X65 is powered over USB-C, the connection doubles as a virtual serial port — plugging the DEV-board into a host PC brings up a CDC device that the monitor can be reached through directly, with no extra wiring.
A few neighbouring addresses share the $FFE0 page:
Address |
Function |
|---|---|
|
Hardware random-number generator (read two bytes) |
|
IRQ mask — enable/disable interrupt sources per bit |
|
IRQ status — which sources are currently asserting |
Monitor Commands¶
The monitor is reached through the same UART ($FFE0–$FFE1), whether attached physically or via the USB-CDC path. Two ways in:
From a host terminal over USB-CDC: open the virtual serial port; the monitor prompt appears once a full system boot has happened.
From an attached screen + keyboard: press Ctrl-Alt-Delete. The combination halts the 65C816 — the CPU is held in RESET — and switches the display / keyboard to the monitor console. Resetting or
reboot-ing from the monitor releases the CPU back into normal execution.
The currently-implemented commands are:
System¶
Command |
Purpose |
|---|---|
|
List available commands |
|
Print system info: firmware build, CPU + core clocks, DVI mode, RAM size/topology |
|
Get or set a firmware configuration value |
|
Restart the whole system |
|
Reset only the 65C816 |
Filesystem (USB storage)¶
Command |
Purpose |
|---|---|
|
List directory contents on the mounted USB volume |
|
Change the current working directory |
|
Create a directory |
|
Receive a file from the host over the serial link |
|
Delete a file |
|
Switch the console into a binary transfer mode |
Boot ROM catalogue¶
These commands manage the .xex images that the NORTH firmware loads at power-on — the machine’s built-in-ROM layer. On a stock X65, the shipped boot ROM is OS/816; normal user applications live under OS/816 and are not deployed here. See Chapter 9 for the two deployment paths.
Command |
Purpose |
|---|---|
|
Load a |
|
Show the INFO-segment metadata of a |
|
Install a |
|
Remove a |
Diagnostics¶
Command |
Purpose |
|---|---|
|
Benchmark and stress-test both PSRAM banks; report copy speeds and block-test results |
The combination of upload + install + info + load is the normal development loop: upload a freshly-built .xex from the host, inspect its metadata, load to run, install to persist.
USB HID¶
USB keyboards, mice, and gamepads plugged into the X65 are handled by the NORTH chip’s USB host stack and surfaced to the 65C816 as a 16-byte memory-mapped window at $FFB0–$FFBF.
The window is device-switched: writing to $FFB0 picks which device appears in the other fifteen bytes. The selector byte packs two fields:
Bits |
Meaning |
|---|---|
|
Device type — |
|
For keyboard: page index (0 or 1). For gamepad: pad number (0–4). |
Keyboards expose a 32-byte state bitmap — one bit per HID keycode — split across two 16-byte pages. Page 0 also carries a small status byte at offset 0 (connected, NUMLOCK, CAPSLOCK, SCROLLLOCK).
Gamepads present a 10-byte report: D-pad and feature flags, digital stick directions, two button bytes, analog stick coordinates, and triggers. A writable selector value of 2 (high nibble zero) targets pad 0 — the merged view, whose bytes are the bitwise OR of every physically-connected gamepad. Single-player applications can read pad 0 unconditionally; multiplayer applications loop across pad indices 1–4.
Full register offsets and bit layouts live in Appendix A: Memory Map; polling patterns are in Chapter 13: Input/Output Handling.
DE-9 GPIO Ports¶
The board carries two DE-9 connectors in the classic double-joystick-port arrangement — but read the label carefully: these are general-purpose, bi-directional GPIO ports first and joystick ports second. Each of the sixteen lines can be individually configured as an input or an output, and the port side runs at 5 V TTL levels. In the spirit of the C64’s control and user ports, the same connectors that accept an Atari-style stick can just as well read a sensor, switch a relay driver, or bit-bang a home-built peripheral — the retro joystick pinout is simply the default convention for how the pins are used, not what the hardware is.
Behind the connectors sits an NXP PCAL6416A, a 16-bit I²C GPIO expander with built-in voltage level translation: its I²C side talks to the NORTH chip at 3.3 V while its port side runs from the 5 V rail. Expander port 0 is the first DE-9 connector and port 1 the second, eight lines each. The expander’s other defining property for the X65 is its interrupt-mask registers — software requests IRQs only for the pin transitions it cares about, avoiding the paging-on-every-pin-change behaviour of simpler expanders.
Connector pinout¶
Port bits map to DE-9 pins in order; pin 8 is ground, so the eight GPIO lines occupy pins 1–7 and 9:
DE-9 pin |
Port bit |
Joystick convention |
As GPIO |
|---|---|---|---|
1 |
0 |
Up |
GPIO line 0 |
2 |
1 |
Down |
GPIO line 1 |
3 |
2 |
Left |
GPIO line 2 |
4 |
3 |
Right |
GPIO line 3 |
5 |
4 |
Button 3 |
GPIO line 4 |
6 |
5 |
Fire (button 1) |
GPIO line 5 |
7 |
6 |
Button 4 |
GPIO line 6 |
8 |
— |
Ground |
Ground |
9 |
7 |
Button 2 |
GPIO line 7 |
Joystick inputs are active-low: a closed switch shorts the pin to ground (pin 8), so with the pull-ups enabled a pressed direction or button reads 0. Note that on classic machines pin 7 carried +5 V; on the X65 it is a full GPIO line, used as button 4 under the joystick convention. Pins 5 and 9 — the analog paddle inputs of the old ports — are plain digital lines here, used as buttons 3 and 2.
Because the port side runs at 5 V, classic-machine accessories that expect power on pin 7 are still an option: configure bit 6 as an output and drive it permanently high, and the pin becomes the +5 V supply the old ports provided. That is enough for the light electronics of the era — autofire circuits, adapter dongles — within the expander’s drive limits (see below). The trade-off is losing button 4 on that connector, which classic devices never had anyway.
Register file¶
The expander’s complete register file is memory-mapped at $FF80–$FF97; the 65C816 sees plain bank-0 addresses while the firmware relays accesses over I²C. Registers come in pairs — one per port / connector, suffix 0 and 1:
Address |
Register |
Reset |
Function |
|---|---|---|---|
|
|
live |
Input port (read-only). The actual pin levels, regardless of direction. Reading clears a pending interrupt for that port. |
|
|
|
Output port. Level driven on pins configured as outputs; bits for input pins are ignored. Reads return what was written, not the pin. |
|
|
|
Polarity inversion. |
|
|
|
Direction. |
|
|
|
Output drive strength, two bits per pin ( |
|
|
|
Input latch. |
|
|
|
Pull resistor enable. |
|
|
|
Pull direction. |
|
|
|
Interrupt mask. |
|
|
|
Interrupt status (read-only). |
|
— |
— |
Reserved. |
|
|
|
Output stage configuration, per port: bit 0 = port 0, bit 1 = port 1; |
The reset state is deliberately benign: every pin an input, pulls disconnected, every interrupt masked. Reading a joystick therefore takes two setup writes — enable the pull-ups (PLS already defaults to “up”, so just set PLE to $FF) — after which IN0/IN1 deliver the stick state directly. Driving external hardware is the mirror image: write the level to OUT, then clear the pin’s CFG bit to turn the driver on.
Electrical limits¶
For the “control external devices” use case, the numbers that matter (from the PCAL6416A datasheet):
Inputs are high-impedance and tolerate up to 5.5 V regardless of the port supply.
Internal pull resistors are nominally 100 kΩ (50–150 kΩ across conditions).
Outputs sink up to 25 mA per pin — enough to drive an LED directly — but keep each 8-pin port under 100 mA total, and mind the per-pin drive-strength setting.
Open-drain mode (
OUTCF) disconnects the internal pulls on that port; provide an external pull-up when wire-OR-ing.
Interrupts¶
The expander’s INT line feeds bit 1 of the RIA interrupt controller ($FFEC mask / $FFED status — see the UART section above). Unmasking a pin in INTE makes any change on it assert the IRQ; the handler reads INST0/INST1 to find which pin fired and then reads IN to clear the condition. Combined with the input latch this gives fully edge-driven joystick handling — no per-frame polling required.
Note
The NORTH firmware does not decode this window yet — reads currently return $FF and writes are ignored. Until the I²C path to the expander is brought up, use USB HID gamepads via $FFB0–$FFBF for controller input.
RGB LED Chain¶
The DEV-board has three on-board RGB LEDs (intended for keyboard state on the full machine) plus a WS2812 data line on the expansion port, which together support a chain of up to 256 addressable LEDs. The RIA decodes and the firmware drives four direct LED registers; the fourth position is simply not populated on the DEV-board, so $FFA3 addresses an LED you can solder onto the strip header yourself. The CPU reaches them through two parallel interfaces at $FFA0–$FFA7:
Direct RGB332 for LEDs 0–3¶
Address |
LED |
Byte format |
|---|---|---|
|
0 |
|
|
1 |
same |
|
2 |
same |
|
3 |
same |
A single STA lights up one of the first four LEDs — no sequencing required. LED 3 is unpopulated on the DEV-board; the write still reaches the chain.
Chain protocol (any LED 0–255, full 24-bit colour)¶
Address |
Role |
|---|---|
|
LED index in the chain — writing this commits the update |
|
Red byte (latched) |
|
Green byte (latched) |
|
Blue byte (latched) |
Writing to $FFA5, $FFA6, or $FFA7 only latches a value — no output is sent to the LED chain. Writing to $FFA4 dispatches the PIX command to the SOUTH-side WS2812 driver with the current latched G / B / index and the freshly-written R.
Internally, both paths go out over the PIX bus as messages on the MISC device; the SOUTH chip drives the WS2812 line at the board’s timing spec.
System Buzzer¶
A small PWM-driven piezo buzzer on the board handles the kinds of tones an OS or application wants without needing to spin up an SGU-1 channel. Its registers live at $FFA8–$FFAB:
Address |
Register |
Notes |
|---|---|---|
|
FREQ_L |
Low byte of the 16-bit frequency code |
|
FREQ_H |
High byte of the 16-bit frequency code |
|
DUTY |
Duty cycle, 0 (silent) – 255 (peak square wave) |
|
— |
Reserved |
The 16-bit FREQ value is mapped logarithmically to audio Hz, covering roughly 20 Hz to 20 kHz across the full range:
$$ f(\text{FREQ}) = 20,\text{Hz} \cdot 2^{10,\text{FREQ}/65535} $$
Writing either FREQ_L or FREQ_H commits the new frequency via a PIX command to the SOUTH buzzer driver. Writing DUTY commits the new duty cycle independently — setting it to zero is the canonical way to silence a running tone without changing its pitch.
Expansion Port¶
The X65’s main expansion connector is physically a PCIe x4 slot (chosen for cheap, ubiquitous mechanical availability) repurposed as a CPU-bus expansion. The signal list exposes enough of the system bus for a wide range of add-on boards — memory, custom chips, external I/O:
Group |
Signals |
|---|---|
Data bus |
|
Address bus |
|
CPU control |
|
I/O enables |
|
I/O interrupts |
|
Power |
|
Other buses |
|
Audio |
|
LEDs |
|
Each of the four I/O slots on the connector gets its own enable and interrupt line, meaning up to four cards can be installed at once with independent address decoding and IRQ routing. CPU-bus expansion boards see the same 8-bit data and low-address signalling as the on-board chips, so a custom peripheral can map itself into the $FC00–$FDFF expansion window using its own IO_EN line — one 128-byte slot per card. See Appendix A for the slot ranges and the RIA chunk bitmap that decides which parts of the window reach the bus.
For a working reference: the board KiCad project and schematic PDF are published in the X65 schematic repository.
On-Board I²C Header (Clockport)¶
Inspired by the Amiga Clockport, the DEV-board carries a small on-board I²C header intended for simple add-ons — real-time clocks, temperature sensors, EEPROMs — that do not warrant a full expansion-port slot. The header shares the same I²C bus that runs over the expansion port, so device addresses must be allocated to avoid collision across both surfaces.
Software reaches I²C peripherals via the RIA fastcall API rather than bit-banging the bus from the 65C816.
Networking (Raspberry Pi Radio Module 2)¶
The board’s wireless connectivity is provided by the Raspberry Pi Radio Module 2 — a small ready-to-use surface-mount module carrying CYW43-series silicon, the same wireless core used by the Raspberry Pi Pico W. It is driven directly by the NORTH chip over a PIO-bit-banged SPI link (GPIOs 34–37).
Capabilities:
Wi-Fi: TCP / UDP networking, IP configuration, DNS.
Bluetooth: BLE and classic Bluetooth links.
AT command interface — a text protocol for configuring the networking stack, parsed by NORTH. Modern extensions take the form
AT+XXX?(query) andAT+XXX=YYY(set); traditional Hayes-like commands (D,E,H,O,S,Z,&…) remain available for modem-style dial-out paths.
Implemented AT+ extensions today include AT+RF and AT+RFCC (radio country code), AT+SSID, and AT+PASS.
For a program to reach the network layer, it issues AT commands via the UART ($FFE0–$FFE1) the same way it reaches the monitor; the firmware routes the bytes to the correct back-end based on whether the current console context is “monitor” or “modem”.
Summary¶
The X65 keeps its I/O surface shallow and direct: a dozen named MMIO windows at the top of bank 0, a monitor console reachable from the UART / USB-CDC path, a generous expansion connector exposing the raw 65C816 bus with I/O enables and interrupts, and a small I²C header for the kinds of add-ons a modern retro-style machine expects. Chapter 13: Input/Output Handling shows how to actually talk to each of these from 65C816 assembly.