RP6502-RIA¶
RP6502 - RP6502 Interface Adapter
Introduction¶
The RP6502 Interface Adapter (RIA) is a Raspberry Pi Pico 2 running RP6502-RIA firmware. It provides every essential service a WDC W65C02S microprocessor needs to run.
The RIA must live at $FFE0-$FFFF and must control RESB and PHI2. Those are the only hard requirements — everything else about your Picocomputer is yours to customize. Even the RP6502-VGA is optional.
A fresh RIA boots into the RP6502 monitor. The easiest way to get started
is the standard setup: a RP6502-VGA module for the display and a USB
keyboard plugged into the RIA. The monitor runs on the console, which
isn’t tied to any single device — other terminals fan in through the
console manifold, including USB serial,
telnet, and the RIA’s bare UART pins (115200 8N1). The monitor itself is
documented here only by a few common commands — its built-in help is
extensive and always current. Type help to get started, then dig into
deep help like help set phi2.
The RP6502 monitor is not an operating-system shell; think of it more like a UEFI shell. Its main job is loading ROMs, with just enough hardware and locale configuration to get going — kept deliberately minimal.
Use the load command to load ROMs in .rp6502 format. These
aren’t ROMs in the traditional (obsolete) sense: a ROM here is a file
holding a memory image that’s loaded into RAM before the 6502 starts.
The RIA has 1 MB of flash you can install ROMs into. Once installed,
a ROM can be run directly, or you can set boot to load it whenever
the RIA boots.
A few monitor commands, such as upload and binary, exist for
developer tools. The rp6502.py script that ships with the examples
and templates automates ROM packaging and execution.
Reset¶
Think of reset as two states rather than a pulse on RESB. While reset is low, the 6502 is stopped and the console talks to the RP6502 monitor. While reset is high, the 6502 runs and the console manifold connects to both the RP6502-OS and the UART TX/RX registers described below.
To bring reset from low to high, either load a ROM that has a reset
vector, or use the reset command if you’ve prepared RAM some other
way.
To drop reset from high to low and return to the monitor — even from a crashed or halted 6502 — use any terminal on the console manifold:
Press Alt-F4 or Ctrl-Alt-Del from a keyboard.
Send a break from a serial terminal.
Send a break from a telnet terminal.
Caution
Don’t wire a physical button to RESB — the RIA must stay in control
of it. What you probably want is the reset driven by the RIA RUN pin,
which we call a reboot. The reference hardware’s reboot button is
wired to the RIA RUN pin, and rebooting this way loads any configured
boot ROM, just like at power-on. Resetting the 6502 from a terminal
only returns you to the RP6502 monitor.
Registers¶
Address |
Name |
Description |
|---|---|---|
$FFE0 |
READY |
Flow control for UART FIFO.
|
$FFE1 |
TX |
Write bytes to the UART. |
$FFE2 |
RX |
Read bytes from the UART. |
$FFE3 |
VSYNC |
Increments every 1/60 second when PIX VGA device 1 is connected. |
$FFE4 |
RW0 |
Read or write the Extended RAM (XRAM) referenced by ADDR0. |
$FFE5 |
STEP0 |
Signed byte added to ADDR0 after every access to RW0. |
$FFE6 -
$FFE7
|
ADDR0 |
Address of Extended RAM (XRAM) for RW0. |
$FFE8 |
RW1 |
Read or write the Extended RAM (XRAM) referenced by ADDR1. |
$FFE9 |
STEP1 |
Signed byte added to ADDR1 after every access to RW1. |
$FFEA -
$FFEB
|
ADDR1 |
Address of Extended RAM (XRAM) for RW1. |
$FFEC |
XSTACK |
512 bytes for Extended Stack (XSTACK). |
$FFED |
ERRNO_LO |
Low byte of errno. All errors fit in this byte. |
$FFEE |
ERRNO_HI |
Ensures errno is optionally a 16-bit int. |
$FFEF |
OP |
Write the OS operation id here to begin an OS call. |
$FFF0 |
IRQ |
Interrupt enable mask. Reading returns the triggered signals as bits and clears them. Writing sets the enable mask and also clears any triggered signals.
|
$FFF1 |
SPIN |
Always $80 (the BRA opcode). JSR here ( |
$FFF2 |
BUSY |
Bit 7 high while OS operation is running. |
$FFF3 |
LDA |
Always $A9 (the LDA immediate opcode). Part of the spin-loop return sequence. |
$FFF4 |
A |
OS call register A. |
$FFF5 |
LDX |
Always $A2 (the LDX immediate opcode). Part of the spin-loop return sequence. |
$FFF6 |
X |
OS call register X. |
$FFF7 |
RTS |
Always $60 (the RTS opcode). Ends the spin-loop return sequence, returning to the caller with A and X loaded. |
$FFF8 -
$FFF9
|
SREG |
32-bit extension to AX - AXSREG. |
$FFFA -
$FFFB
|
NMIB |
6502 vector. |
$FFFC -
$FFFD
|
RESB |
6502 vector. |
$FFFE -
$FFFF
|
BRK/IRQB |
6502 vector. |
UART¶
The RIA’s UART RX/TX pins are directly accessible at $FFE0-$FFE2. The ready flags on bits 6-7 let you test with the BIT operator. Use these or the RP6502-OS stdio — but not both at once: driving the UART directly while a stdio OS function is in progress is undefined behavior. The UART runs at 115200 bps, 8-bit words, no parity, 1 stop bit.
Extended RAM (XRAM)¶
RW0 and RW1 are two portals into the same 64 KB of XRAM. A single portal would make moving XRAM slow, since data would have to buffer through 6502 RAM. Ideally you won’t move XRAM at all and can use the pair for smarter optimizations.
STEP0 and STEP1 default to 1 after reset. Both are signed, so negative values walk XRAM in reverse. These auto-increment adders make sequential access very fast — more than enough to offset the slightly slower random access compared to 6502 system RAM.
Extended Stack (XSTACK)¶
This is a 512-byte, top-down, last-in-first-out stack used by the fastcall mechanism described in the RP6502-OS. Reading past the end is guaranteed to return zeros. Write to push, read to pull.
Extended Registers (XREG)¶
The RIA is both the host of the PIX bus (documented below) and device 0 on it.
Address |
Name |
Description |
|---|---|---|
$0:0:00 |
KEYBOARD |
See Keyboard section |
$0:0:01 |
MOUSE |
See Mouse section |
$0:0:02 |
GAMEPADS |
See Gamepads section |
$0:1:00 |
PSG |
See Programmable Sound Generator section |
$0:1:01 |
OPL |
See Yamaha OPL2 FM Sound Generator section |
Pico Information Exchange (PIX)¶
The Raspberry Pi Pico has only so many GPIO pins, so high-bandwidth devices like video systems needed a bus of their own. PIX is that bus: an addressable broadcast system that any number of devices can listen to.
Physical layer¶
The Pico’s PIO decodes the physical layer easily, since PIO is essentially a shift register. The signals are PHI2 and PIX0-3. This is a double-data-rate bus: it shifts PIX0-3 left on both transitions of PHI2, so a 32-bit frame travels in just 4 PHI2 cycles.
Bit 28 (0x10000000) is the framing bit, set in every message. When the bus is idle, an all-zero payload repeats on device ID 7. A receiver synchronizes by checking that PIX0 is high on a falling transition of PHI2; if it isn’t, stall until the next clock cycle.
Bits 31-29 (0xE0000000) carry the device ID for a message:
Device 0 — the RIA. It’s also overloaded to broadcast XRAM.
Device 1 — the RP6502-VGA.
Devices 2-6 — open for user expansion.
Device 7 — synchronization. (0xF0000000 is hard to miss on test equipment.)
The remaining bits address a register within a device:
Bits 27-24 (0x0F000000) — the channel ID; each device can have 16 channels.
Bits 23-16 (0x00FF0000) — the register address within that channel.
Bits 15-0 (0x0000FFFF) — the value to store in the register.
PIX Extended RAM (XRAM)¶
The RIA broadcasts every change to its 64 KB of XRAM on PIX device 0. Bits 15-0 carry the XRAM address; bits 23-16 carry the XRAM data.
Each PIX device keeps a local replica of the XRAM it uses. Typically all 64 KB is replicated, and an XREG set by a 6502 application installs virtual hardware at some location in XRAM.
PIX Extended Registers (XREG)¶
PIX devices may use bits 27-0 however they like. The suggested split is:
Bits 27-24 — a channel. The RIA, for example, has separate channels for audio, keyboard, mice, and so on.
Bits 23-16 — an extended register address.
Bits 15-0 — the value to store.
That gives seven PIX devices, each with 16 channels of 256 16-bit registers. The idea is to use these extended registers to configure virtual hardware and map it into extended memory.
Keyboard¶
The RIA can hand applications direct access to keyboard data, which is what you want when you need key-up and key-down events or the modifier keys. If you don’t need that, the UART or stdin works just as well.
Enable and disable direct keyboard access by mapping it to an address in XRAM.
xreg(0, 0, 0x00, xaddr); // enable
xreg(0, 0, 0x00, 0xFFFF); // disable
xreg_ria_keyboard(xaddr); // macro shortcut
The RIA continuously updates XRAM with a bit array of USB HID keyboard keycodes — note these are HID keycodes, not PS/2 scancodes. Each keycode is one bit in the array: bit N is 1 while the key with HID keycode N is pressed. The first four keycodes are special:
0 - No key pressed
1 - Num Lock on
2 - Caps Lock on
3 - Scroll Lock on
uint8_t keyboard[32];
#define key(code) (keyboard[code >> 3] & \
(1 << (code & 7)))
Mouse¶
The RIA can give applications direct access to mouse data. Enable and disable it by mapping it to an address in XRAM.
xreg(0, 0, 0x01, xaddr); // enable
xreg(0, 0, 0x01, 0xFFFF); // disable
xreg_ria_mouse(xaddr); // macro shortcut
This sets the XRAM address of a structure holding the live mouse input.
struct {
uint8_t buttons;
uint8_t x;
uint8_t y;
uint8_t wheel;
uint8_t pan;
} mouse;
Compute movement by subtracting the previous value from the current one. VSYNC timing (60 Hz) is period-correct but slow by modern standards. For precise mouse input, poll from an ISR at 8 ms or faster (125 Hz).
Account for canvas resolution when interpreting movement. At 640x480 and 640x360, one unit equals one pixel; at 320x240 and 320x180, two units equal one pixel.
int8_t delta_x = current_x - prev_x;
int8_t delta_y = current_y - prev_y;
Mouse buttons are a bitfield:
0 - LEFT
1 - RIGHT
2 - MIDDLE
3 - BACKWARD
4 - FORWARD
Gamepads¶
The RIA supports up to four gamepads, with drivers for Generic HID, XInput, and PlayStation controllers.
Modern gamepads have all converged on the same layout: four face buttons, a d-pad, dual analog sticks, select, start, and four shoulders. The face buttons vary only in labeling — XY/AB, YX/BA, or Square/Triangle/Cross/Circle. That rarely matters to an application unless the buttons stand in for directions, in which case the Square/Triangle/Cross/Circle and XY/AB arrangements are “the official” RP6502 layout. You’re free to do your own thing, of course — ask players to use a specific gamepad, or offer an “AB or BA” option.
Note
The RP6502 expects modern gamepads.
The RP6502 is not an emulation platform. Sega, NES, SNES, TG16, Atari, and other retro-style gamepads are not supported.
Retro-style gamepads are wired with button mappings meant for emulators, and emulators in turn expect the layout of a modern gamepad. The two don’t cancel out — you just end up with wonky mappings that don’t follow the de facto modern standard.
Enable and disable the RIA gamepad data by setting its extended register. The register value is the XRAM start address of the gamepad data; any invalid address disables the gamepads.
xreg(0, 0, 2, xaddr); // enable
xreg(0, 0, 2, 0xFFFF); // disable
xreg_ria_gamepad(xaddr); // macro shortcut
The RIA continuously updates extended memory with gamepad state. The 10-byte structure below repeats four times — 40 bytes total, one block per gamepad.
The upper bits of the DPAD register report readiness and type. The connected bit is high when a gamepad occupies that player slot. The Sony bit indicates a PlayStation-style gamepad with Circle/Cross/Square/Triangle faces.
Both digital and analog values are available for the sticks and the L2/R2 triggers, so applications can ignore the analog values entirely if they like.
Some gamepads report only digital data; in that case, code that uses L2 and R2 should expect analog values of just 0 or 255.
Applications taking the simple “one stick and buttons” approach should merge the d-pad and left stick into a single input.
Offset |
Name |
Description |
|---|---|---|
0 |
DPAD |
|
1 |
STICKS |
|
2 |
BTN0 |
|
3 |
BTN1 |
|
4 |
LX |
Left analog stick X position. -128=left, 0=center, 127=right |
5 |
LY |
Left analog stick Y position. -128=up, 0=center, 127=down |
6 |
RX |
Right analog stick X position. -128=left, 0=center, 127=right |
7 |
RY |
Right analog stick Y position. -128=up, 0=center, 127=down |
8 |
L2 |
Left analog trigger position. 0-255 |
9 |
R2 |
Right analog trigger position. 0-255 |
Programmable Sound Generator¶
The RIA includes a Programmable Sound Generator (PSG), configured through extended register device 0, channel 1, address 0x00.
Eight 24 kHz 8-bit oscillator channels.
Five waveforms: Sine, Square, Sawtooth, Triangle, Noise.
ADSR envelope: Attack, Decay, Sustain, Release.
Stereo panning.
PWM for all waveforms.
Each of the eight oscillators uses eight bytes of XRAM for configuration. The structure size is a power of two, so indexing into the oscillator array is a bit shift rather than a multiply.
typedef struct
{
unsigned int freq;
unsigned char duty;
unsigned char vol_attack;
unsigned char vol_decay;
unsigned char wave_release;
unsigned char pan_gate;
unsigned char unused;
} ria_psg_t;
Enable and disable the PSG by setting its extended register. The value is the XRAM start address for the 64 bytes of config; it must be int-aligned and must not cross a page boundary. Any invalid address disables the PSG.
xreg(0, 1, 0x00, xaddr); // enable
xreg(0, 1, 0x00, 0xFFFF); // disable
Configuration changes take effect immediately, which opens the door to panning, slide instruments, and other CPU-driven shenanigans.
Name |
Description |
|---|---|
freq |
0-65535 Oscillator frequency as Hertz * 3. This results in a resolution of 1/3 Hz. |
duty |
0-255 (0-100%) Duty cycle of oscillator. This affects all waveforms. |
vol_attack |
Attack phase volume and rate.
|
vol_decay |
Decay phase volume and rate.
|
wave_release |
Waveform and release rate.
|
pan_gate |
Stereo pan and gate.
|
Value table. ADR rates are the time it takes for a full volume change. Volume attenuation is logarithmic.
Value |
Attack |
Decay/Release |
Attenuation Multiplier |
|---|---|---|---|
0 |
2ms |
6ms |
256/256 (loud) |
1 |
8ms |
24ms |
204/256 |
2 |
16ms |
48ms |
168/256 |
3 |
24ms |
72ms |
142/256 |
4 |
38ms |
114ms |
120/256 |
5 |
56ms |
168ms |
102/256 |
6 |
68ms |
204ms |
86/256 |
7 |
80ms |
240ms |
73/256 |
8 |
100ms |
300ms |
61/256 |
9 |
250ms |
750ms |
50/256 |
10 |
500ms |
1.5s |
40/256 |
11 |
800ms |
2.4s |
31/256 |
12 |
1s |
3s |
22/256 |
13 |
3s |
9s |
14/256 |
14 |
5s |
15s |
7/256 |
15 |
8s |
24s |
0/256 (silent) |
Yamaha OPL2 FM Sound Generator¶
The RIA includes a YM3812 FM Sound Generator (OPL2), configured through extended register device 0, channel 1, address 0x01.
Enable and disable the OPL2 by setting its extended register. The value is the XRAM start address for the 256 OPL2 registers, which must begin on a page boundary.
xreg(0, 1, 0x01, xaddr); // enable
xreg(0, 1, 0x01, 0xFFFF); // disable
So if xaddr is 0x4200, the 256 OPL2 registers map into XRAM from 0x4200 to 0x42FF.
Timers, interrupts, and the status register are not supported. Those features existed mainly to cost-reduce consumer devices; computers of the era had their own timers and rarely used the chip’s.
Console Port¶
The RIA’s main serial port is the system console. Modern operating systems layer canonical input and translated output over something configurable like termios. A full termios is too heavy for an 8-bit system, but raw and non-blocking I/O still need to be on the table.
The familiar stdin blocks for canonical input: the console user edits a line, and once they press Enter the stdin read unblocks and returns the line up to a linefeed.
The familiar stdout and stderr block too, inserting a carriage return before any newline that lacks one. All of the data is always sent, and writes block until it has fully drained into the hardware FIFOs.
These interfaces are exactly what a C programmer expects, but they’re a
poor fit for a multitasking 6502 program. For that, a non-blocking
interface is available: open the special filename "CON:". Reads can
return 0 bytes, and writes may send less than you asked for.
Going one step further, the special filename "TTY:" gives a
non-blocking, raw connection to the console port — no canonical input,
no newline translation. It’s exactly what the RIA_TX and RIA_RX
registers provide, just packaged as stdio for convenience.
"CON:" and "TTY:" are each locked to their own file descriptor,
which cannot be closed. A second open returns the same file descriptor
as the first, and a close succeeds as a no-op.
Virtual COM Port¶
If you need serial ports beyond the console UART, USB adapters are available for CMOS/TTL, RS-232, RS-422, and RS-485. The RIA includes drivers for FTDI, CP210X, CH34X, PL2303, and CDC ACM, and each one appears as a Virtual COM Port (VCP).
The status command lists any connected VCP devices. Open one like a
file, using a special name. By default "VCP0:" opens at 115200 bps
with 8 data bits, no parity, and 1 stop bit. Set the baud rate with
"VCP0:115200", or the full bit configuration with
"VCP0:115200,8N1". The file won’t open if your hardware can’t
support the requested configuration, and the open flags are ignored.
open("VCP0:1200,7E2", 0);
// then read and write
Generous FIFO buffers serve both directions, and both reads and writes are non-blocking. Reads can return 0 bytes, and writes may send less than you asked for — resubmit any remaining bytes on a later call.
MIDI¶
USB MIDI instruments plug right in, and the status command lists
them. Each virtual cable is its own device — "MIDI0:" onward,
assigned in the order cables appear, up to four at a time. A simple
keyboard is one cable (1X1); a multi-port interface is several. Open one
like a file.
A cable opens in one of two modes, chosen by the open name. Bare
"MIDI0:" is raw — reads and writes are plain wire MIDI, the same
bytes a 5-pin DIN cable carries, with no timing and nothing added or
removed. Give a division instead, "MIDI0:480", and the cable is
timed: the RIA handles timing for you using the event format from
Standard MIDI Files, prefixing every message with a variable length
quantity delta time measured in ticks. The rest of this section is the
timed format; raw mode is just the wire bytes.
In timed mode, time starts at the open — the first byte in either direction is a delta measuring from the open itself, and a delta of zero means right now. Writes are scheduled — the RIA holds each message and sends it to the instrument exactly on time, so your program only needs to keep the buffer fed. Reads are a recording — incoming messages arrive with delta times measuring when they actually happened, ready to store in a file or play back later.
The division — ticks per quarter note, what an SMF carries in its header — accepts 1 to 32767 and is fixed while open; reopen between songs to change it. The open flags are ignored. A cable can be input, output, or both; reading an output-only cable or writing an input-only one returns an error.
Tempo changes on the fly with the standard SMF Set Tempo meta event,
which the RIA consumes locally and never forwards to the instrument —
FF, a type, a length, then that many data bytes:
Control event |
Effect |
|---|---|
|
Set tempo in microseconds per quarter note — the standard SMF event. The tick rate becomes tempo × 1000 ÷ division. |
|
A wire System Reset. The doubled escape is the whole event — no length byte — and unlike the others it is sent to the instrument. |
Tempo defaults to 500000 µs per quarter note — 120 BPM, a 1041667 ns
tick at 480 PPQN. Every other FF event, including the rest of the
SMF meta set, is swallowed without effect, so a Standard MIDI File track
plays through nearly verbatim — division from the file header, tempo
events straight from the track:
open("MIDI0:96", 0); // division from the MThd header
// FF 51 03 07 A1 20 tempo = 500000 (120 BPM at 96 PPQN)
// then delta-timed events; the RIA paces them and tracks tempo changes
The RIA echoes every tempo event onto the read stream at the moment it
takes effect, so a recording is self-describing. A rejected event —
malformed, or a value of zero or out of range — is echoed with its value
zeroed and the tempo unchanged; zero is never a valid tempo, so it
unambiguously marks an event that didn’t apply. Your read parser must
handle FF: a second FF is a System Reset, and anything else is
a meta type and length to skip.
The stream carries raw wire MIDI messages after each delta time: channel
voice messages (running status accepted on writes), system common, and
single-byte real-time messages F8-FE. System Reset travels as the
FF FF escape in both directions: write a delta then FF FF to
send one, and a reset from the instrument is recorded the same way. The
undefined bytes F4 and F5 are quietly dropped.
System Exclusive — sysex — is how instruments move the big stuff, like
patch banks and sample dumps, in one long message: F0, any number of
data bytes, then F7 to finish. Only the opening F0 takes a delta
time; the data bytes flow without timing until the F7, on writes and
recordings alike. Real MIDI lets real-time messages like clock barge
into the middle of a sysex — the RIA passes them through in place, so
expect the occasional F8-FE byte inside a recorded dump. And if a tempo
echo comes due during a dump, the recording closes the sysex early and
reopens it after — everything arrives, just split into two
F0 … F7 fragments.
Delta times measure from the previous event, so timing stays exact over any song length: events are anchored to an absolute tick count, and ticks are kept internally in nanoseconds, holding arithmetic rounding below one part per million. What remains is the hardware — the microsecond timer’s crystal drifts single-digit milliseconds over a several-minute song, and USB full-speed framing sets the moment-to-moment jitter near one millisecond, the same pace as the classic MIDI wire itself.
If your program stops feeding the buffer and resumes, messages already past due play immediately and the timeline continues from there. If you stop reading, the recording drops whole messages rather than backing up, and the timing of everything that survives stays exact. Reads and writes are non-blocking with the same short read/write rules as other non-blocking devices.
Closing a timed output cable blocks until its buffered tail has played
out on schedule, so the final notes — and the note-offs that end them —
reach the instrument before close returns, and nothing is left ringing.
sync does the same without closing: a way to wait for the schedule to
catch up between songs. Both follow the timeline, so a far-future delta
still in the buffer makes them wait that long. If a sysex is still open
when a timed cable closes, the RIA sends its F7 so the instrument is
not left waiting mid-dump. A raw cable has no schedule, so close and
sync simply flush what is buffered, and they inject no F7.
Near Field Communications (NFC)¶
NFC cards have become a popular media replacement in the retro community, and they map neatly onto the RP6502’s use of “ROM files” in place of “ROM cartridges”. In 1983 you might have grabbed a cartridge with colorful stickers to home in on the exact dopamine hit you were after. NFC cards are cheap and just as easy to decorate, whether with stickers or direct printing. Grab a card, tap it on the reader, and the ROM you want loads instantly. Here’s how it works.
You’ll need a PN532 card reader with a USB interface. It’s the only reader supported, and it’s cheap — around $10 USD. You’ll also want a card (or fob, or sticker) for each ROM you plan to support. New to NFC? Buy a pack of NTAG215 cards and a sharpie.
Do not buy a kit of separate USB-to-UART and PN532 boards unless you want an unsupported project on your hands. Buy a single board with everything already engineered and ready to use.
With the reader plugged in, run the monitor command SET NFC 2 to
start USB detection. It may probe your other VCP devices with PN532 data
along the way; that’s normal. You’ll hear an error buzz, or two beeps for
success. You can also run status to see whether (NFC) is listed
next to one of your VCP devices.
SET NFC 1 turns the reader on and SET NFC 0 turns it off; the
choice persists across reboots (a successful SET NFC 2 also leaves it
on). SET NFC 86 forgets the paired reader so a later SET NFC 2 can
pair a different one.
From now on, scanning a card produces one of three sounds: an error buzz if something went wrong, two beeps for success, or a single beep for a partial success.
Program each card with the filename and arguments of the ROM to launch.
If you’d load the ROM with LOAD /jigsaw.rp6502, put an NDEF TEXT
record on the card holding just /jigsaw.rp6502 — no load command. A
leading / is implied if you leave it off, and the current working
directory is ignored.
Paths with spaces need quotes, and you can include arguments:
"/My Games/jigsaw.rp6502" cat.bmp
When a card is read, every mounted drive is scanned for the ROM file. On a match, you get two beeps, the 6502 stops, the current drive and directory switch to the ROM’s location, and the new ROM starts loading. If that ROM is already running, you get a single beep and nothing else happens.
To search just one drive, name it in the text record:
MSC0:/encabulator.rp6502
NFC Device API¶
Applications can take over the NFC reader for advanced uses, or to help
program NFC tags. While the "NFC:" device is open, automatic ROM
launching is suppressed.
int fd = open("NFC:", O_RDWR);
The PN532 reader runs autonomously on the RIA. The 6502 arms operations
with write() and polls results with read(): NFC_CMD_READ
returns the current tag data immediately, NFC_CMD_WRITE arms a write,
and NFC_CMD_CANCEL disarms a pending one. State changes and write
completions are posted to read() automatically.
write() – Commands¶
write() is non-blocking and streaming. A call may consume less than
you passed; resubmit the remaining bytes on a later call.
Byte |
Command |
|---|---|
|
Arm a write |
|
Disarm pending write |
|
Return current tag data |
|
Play success tone 1 |
|
Play success tone 2 |
|
Play error tone |
The NFC_CMD_WRITE payload starts with the start page, a two-byte
length, then the tag data. page is the NTAG page to begin writing at
(page 4 is the start of user data). Data is written in 4-byte pages, and
the final page is zero-padded if the payload isn’t a multiple of 4. The
write arms once the full payload arrives and runs on the current card or
the next one presented. A second NFC_CMD_WRITE overwrites the first —
last write wins.
The payload may be at most 888 bytes; a longer length is rejected with the error tone and never armed. A write also fails (error tone) if it would run past the card’s NDEF data area or target a page below 4, and a zero-length payload completes immediately as a no-op.
NFC_CMD_READ always returns NFC_RESP_READ on the next read();
if no card data is available, the length is zero.
read() – Responses¶
read() is non-blocking and streaming. It returns 0 bytes when there’s
nothing new. Responses may be split across multiple calls, so callers
must buffer and reassemble them. State changes are sent once per change,
including once right after open(). Only the latest state is tracked, so
a rapid transition (such as NFC_RESP_CARD_INSERTED immediately followed
by NFC_RESP_CARD_READY) may be coalesced to the later state if you don’t
read() in between.
Byte |
Meaning |
|---|---|
|
Read result |
|
Armed write complete |
|
State: no reader attached |
|
State: no card present |
|
State: card present, tag data not ready |
|
State: card present, tag data ready |
The NFC_RESP_READ payload is a two-byte length followed by raw tag
data starting at page 0, and it may span multiple read() calls. The
page layout is: pages 0-2 are UID/lock bytes, page 3 is the Capability
Container (CC[2] * 8 = max NDEF bytes), and pages 4+ are user data
(TLV-wrapped NDEF records terminated with 0xFE).
After NFC_RESP_READ or NFC_RESP_WRITE, send one or more tone
commands or play your own sounds. Typically you request reads on
NFC_RESP_CARD_READY and arm writes on NFC_RESP_NO_CARD, but you
can also arm a write after reading and verifying a card. The state
changes give you flexibility in how you sequence operations. The cached
tag image is not refreshed by a write, so re-present the card before the
next NFC_CMD_READ if you want to read back what you wrote.
ROM File Format¶
A ROM file begins with a shebang line, followed by any number of assets.
Text lines end with \r, \n, or both, and numbers may be written
in decimal (255), C-style hex (0xFF), or MOS-style hex ($FF).
Shebang — first line of every ROM file:
#!RP6502
Null-named asset — a group of memory chunks loaded directly into RAM:
#>len crc
Followed by one or more memory chunks, each a header line plus len
bytes of raw binary data:
addr len crc
Field |
Description |
|---|---|
|
Destination address in 6502 RAM (0x0000-0xFEFF) or XRAM (0x10000-0x1FFFF). |
|
Number of raw binary bytes that immediately follow this line. |
|
CRC of the binary payload (checked). |
Named asset — a raw binary blob identified by name:
#>len crc name
Followed immediately by len bytes of raw binary data. Assets repeat
until end of file.
Field |
Description |
|---|---|
|
Number of raw binary bytes that immediately follow this line. |
|
CRC of the binary payload (ignored by RIA). |
|
Asset identifier string. |
The rp6502.py tool that comes with the new-project templates handles
these details and integrates with CMake, so adding assets is
straightforward. In this example the image data is packed into the ROM
as memory chunks that load into RAM or XRAM when the ROM loads:
rp6502_asset(your_project 0x10000 img/intro.bin)
A ROM can also hold named assets of raw data, and some names are
special — the help asset is shown by the HELP and INFO monitor
commands.
rp6502_asset(your_project help src/help.txt)
While a ROM runs, its assets become part of the filesystem. Prefix the asset name with “ROM:” and open it like any other file. ROM assets are read-only, but you can have several open at once.
open("ROM:help", O_RDONLY)
There’s no enforced limit on the number or size of named assets. Opening a file is a linear search; it skips over the data, but how many seeks and string compares your application can tolerate is up to you.