FujiNet Programming Guide for the Atari 7800 - FujiNetWIFI/fujinet-firmware GitHub Wiki
FujiNet Programming Guide for the Atari 7800
The FujiNet programming manual for the Atari 7800 ProSystem: the memory map and
the mailbox, the text display three ways, living in 4K, the 6502 library, C with
fujinet-lib and 7800basic on the mailbox, eight worked programs in all three
languages, the command reference, the tile engine, and walkthroughs of
Battleship and CONFIG. This is the wiki condensation; the full illustrated
edition is the PDF in atari-7800/programmers-handbook/ of the manuals
repository, styled after the 1986 ATARI 7800 ProSystem Owner's Manual.
See also the FujiNet-Network-Protocol-Handbook
for everything the N: device can say once you can reach it.
Contents
- Your FujiNet cartridge
- Installing the tools
- The memory map
- How the mailbox works
- The text display
- Living in 4K
- The three libraries
- The programs
- Banks and SuperGame
- Command reference
- Battleship
- CONFIG
- Things that cost real time
- Troubleshooting
- Sources of truth
Your FujiNet cartridge
An RP2354B microcontroller and 512K of static RAM sit on the cartridge edge.
The console reads the RAM itself β its address and data lines go straight to
the chip β and the RP2354B only decides which 8K page each slot of the address
space shows, so MARIA can read the cartridge at full speed. Behind it, over
USB, an ESP32-S3 runs the FujiNet firmware (fujiversal-atari7800).
The console sees a signed boot block at power-on, a 512-byte loader at $0600,
a mailbox from $0800 to $0FFF, a POKEY at $0450, a High Score Cart when
one is installed, and up to 448K of program laid out the way any of ten real
7800 boards would lay it out.
The board is a Rev0 design that has not yet been built. Everything here ran
in host tests and the MAME cartridge model. The cartridge edge has no reset
line; the console's RESET switch is a bit in SWCHB.
Installing the tools
- cc65 2.19:
ca65,ld65,cc65,cl65. - fujinet-lib with the
atari7800target:fujinet-lib-experimental, branchadd-atari7800(PR #152) until it merges;make atari7800. - 7800basic, built natively:
make LEX=flex 7800preprocess, thenmake LEX=flex 7800optimize, thenmake LEX=flex(the two lexers sharelex.yy.c; never build them in parallel). dasm 2.20.16 on the PATH. - MAME with the cartridge:
fujinet-firmware/pico/atari-7800/emu/apply.sh mame, thenmake SUBTARGET=a7800 SOURCES=src/mame/atari/a7800.cpp REGENIE=1; run with-cartslot fujinet. MAME must run from its own tree for-autoboot_script. - fujinet-pc:
./build.sh -p RS232in the firmware; its[BOIP]port is where the cartridge connects (FUJINET_TCP, default127.0.0.1:9995). - Or FujiNet Go Atari 7800, which has all of it inside: Open Cartridgeβ¦
runs any
.a78, and a claimed program keeps the mailbox.
The memory map
| Range | What |
|---|---|
$0000β$003F |
TIA (INPTCTRL $01), MARIA |
$0040β$00FF, $0140β$01FF |
zero page and stack: console RAM |
$0280 |
RIOT: joysticks, switches |
$0450β$045F |
the cartridge's POKEY |
$0600β$07FF |
the loader |
$0800β$0BFF |
reply window (read) |
$0C00β$0CFB |
status page (read) |
$0D00β$0DFF |
registers and one-shots (write) |
$0F00β$0FFF |
TX stream (write) |
$1000β$17FF, $3000β$3FFF |
High Score Cart RAM and ROM, when on |
$1800β$27FF |
console RAM, 4K |
$4000β$7FFF |
cartridge RAM (16K) if the claim asks |
$8000β$FFFF |
the program; the claim at $FF70 |
The cartridge's mode is at $0C12: 0 BOOT (the boot block), 1 LOAD (the
loader filling the RAM), 2 GAME (an image, mailbox off), 3 APP (an image that
keeps the mailbox). What makes an APP is the claim: ten bytes at $FF70,
"FUJI", version 1, kind+1 (the board layout; 0 lets the cartridge decide),
flags (bit 0: 16K of RAM at $4000), three zeros.
Rules for every program:
- Never store to
$00β$1Funtil you mean to lock INPTCTRL; a lock stops games you boot from going through the console's BIOS. Sound through the POKEY at$0450; MARIA'sWSYNCat$24. - Never use a read-modify-write instruction on
$0D00β$0FFF. - Never
JMP ($xxFF). - Set your own stack: the loader leaves S at
$16.
How the mailbox works
A register is one store: V to $0D00+n. A transaction:
- DEVICE (
$0D00), CMD ($0D01), NPARAM ($0D02); DATA_RST ($0D05). - Parameters into
$0F00, one store a byte: size (1, 2, 4) then the value, low byte first; then the payload. 320 bytes, eight parameters at most. - SEQ (
$0D10) = ACKSEQ ($0C00) + 1; 0 becomes 1. Never keep your own counter: the cartridge outlives a power cycle of the console. - Wait for ACKSEQ = SEQ; the cartridge paints it last.
- ERR (
$0C02) 0? REPLY_CMD ($0C03)$06? The reply is at$0800, RXLEN ($0C04) bytes, at most 1024.
lda #$70 ; device, command, no parameters
sta FN_REGSEL+FN_REG_DEVICE
lda #$C4 ; GET_ADAPTERCONFIG_EXTENDED
sta FN_REGSEL+FN_REG_CMD
lda #0
sta FN_REGSEL+FN_REG_NPARAM
sta FN_REGSEL+FN_REG_DATA_RST
lda FN_ACKSEQ ; the next sequence number
clc
adc #1
bne @seq
lda #1 ; 0 is reserved
@seq: sta FN_REGSEL+FN_REG_SEQ ; launch
@wait: cmp FN_ACKSEQ ; painted last
bne @wait
The cartridge allows 5 seconds a transaction, 60 for MOUNT_IMAGE and COPY_FILE, 90 for the network device's OPEN, STATUS, READ and CLOSE; a program's own wait must be longer. In MAME a transaction completes inside the store to SEQ.
The text display
MARIA draws from display lists; a text row is an indirect object pointing at character codes. MARIA cannot read the mailbox: copy reply text to RAM first.
| Assembly | C | 7800basic | |
|---|---|---|---|
| Library | fujidisp.s |
maria.s (CONFIG's tile engine) |
built in |
| Mode | 320A, 40 Γ 24 | 320B indirect, 32 Γ 24 tiles | 320A, plotchars, plotmap |
| Text lives | $4000 (cart RAM) |
$4000 (cart RAM) |
$2200β$27FF |
Living in 4K
| Assembly | C | 7800basic | |
|---|---|---|---|
| Zero page | $40β$DF; library $E0β$EB |
cc65 and ZEROPAGE |
temps $40β$4A; aβz $E6β$FF |
| Stack page | stack | stack | var0βvar99 $140β$1A3, stack from $1C2 |
$1800β$27FF |
free | CONSOLE |
DLL $1800, lists $1880β$1FFF; free $2200β$27FF |
$4000β$7FFF |
screen, BSS |
data, heap, tile map, C stack | (with 32kRAM) |
The reply window is free memory: read replies in place.
The three libraries
Assembly β the bring-up's fujilib.s: fn_chk (magic), fn_beg
(FN_DEV, FN_CMD, FN_NPR; timeout FN_TMO = 16, ~11 s), fn_pb, fn_pw,
fn_txb, fn_path (padded to 256), fn_go (A = ERR, $FF no answer),
fn_ack ($EE on NAK), fn_blk, fn_boot. The handbook adds devdefs.inc,
netdefs.inc and the FNBEGIN dev, cmd, nparam macro.
C β fujinet-lib's atari7800 target: its crt0.s (never touches
INPTCTRL), atari7800-fujinet.cfg (32K ROM at $8000, 16K RAM at $4000),
atari7800-romstamp.py. Use the ordinary fuji_*, network_*, clock_*
calls; fujinet-atari7800.h adds fuji_a7800_present(), _mode(),
_boot_state(), _boot(), _config(), _tv(), _hsc() and more. The bus
layer (fujinet-bus-atari7800.h): fn_regwr(), fn_tx(), fn_commit(),
the FN_* cells, and fuji_bus_call() for any command. No conio, no
clock(), no TIA.
7800basic β incbasic fujinet.bas first (the mailbox's names: in BASIC a
register's name is its address, FN_CMD = $C4 is one STA $0D01), then
incbasic fujilib.bas after the main loop (fn_chk, fn_beg, fn_pb,
fn_pw, fn_pad, fn_go, fn_hex; arguments in fn_dev, fn_cmd,
fn_npr, fn_val, fn_valhi, fn_tmo; result in fn_res).
tools/basfix.py stamps the claim; 7800header writes the .a78. Rules:
- No one-letter variable names (
aβzare 7800basic's). - No arithmetic on a mailbox name.
- No expression in an array index:
FN_REPLY[ix + 2]compiles silently into nonsense.dima name at the field's address. - No expression in a
forbound:to n - 1silently drops the- 1. - 7800basic locks INPTCTRL at start-up;
const NOTIALOCK = 1andconst NOTIALOCKMUTE = 1keep it unlocked (use the POKEY for sound).
The programs
Eight programs, each in all three languages, verified in MAME against
fujinet-pc by emu/verify.sh (which reads the screen through MARIA's
display lists):
| Program | Shows |
|---|---|
| hello | text, and that a FujiNet cartridge answers |
| first contact | GET_ADAPTERCONFIG_EXTENDED: SSID, IP, firmware |
| netget | an HTTP GET through N:, a line at a time |
| directory | MOUNT_HOST, OPEN_DIRECTORY, READ_DIR_ENTRY until $7F,$7F |
| boot | MOUNT_HOST, SET_DEVICE_FULLPATH, MOUNT_IMAGE, watch BOOT_STATE, BOOTLOCK, JMP $0600 |
| appkey | the shared name (1:1:0), then a key written and read back |
| clock | GET_ISO_LOCAL and GET_ISO_UTC |
| banks | a transaction from each of three SuperGame banks |
Banks and SuperGame
16K banks; the last is fixed at $C000; a store anywhere in $8000β$BFFF
picks the bank shown there. Switch from the fixed bank; keep everything MARIA
reads (font, tiles) and the mailbox library there. A FujiNet app names the
scheme in its claim's kind byte: 3 SuperGame, 5 SuperGame with RAM. In C:
a linker config with BANK0βBANK2 at $8000, a crt0 with the claim's kind,
#pragma code-name, and tools/checkbanks.py (fujinet-lib's romstamp takes
48K at most). In 7800basic: set romsize 128k, bank N, gosub x bankN,
return otherbank; font and library in bank 8.
Command reference
The cartridge's ESP32 runs the firmware's RS232 build. Conventions: byte = size 1 + value; word = size 2 + two bytes, low first.
FUJI device $70
| Command | Code | nparam | Notes |
|---|---|---|---|
| GET_ADAPTERCONFIG_EXTENDED / _CONFIG | $C4 / $E8 |
0 | 240 / 140 bytes |
| SCAN_NETWORKS | $FD |
0 | reply: count |
| GET_SCAN_RESULT | $FC |
1 | byte index; 33-byte SSID + signed RSSI |
| SET_SSID | $FB |
1 | byte (ignored, required); payload exactly 97 bytes |
| GET_SSID | $FE |
0 | 97 bytes |
| GET_WIFISTATUS / GET_WIFI_ENABLED | $FA / $EA |
0 | 3 = connected |
| RESET | $FF |
0 | restarts the ESP32 |
| STATUS / DEVICE_READY | $53 / $00 |
1 / 0 | |
| READ_HOST_SLOTS / WRITE_HOST_SLOTS | $F4 / $F3 |
0 | 8 Γ 32 bytes |
| MOUNT_HOST | $F9 |
1 | host slot |
| READ_DEVICE_SLOTS / WRITE_DEVICE_SLOTS | $F2 / $F1 |
0 | |
| SET_DEVICE_FULLPATH | $E2 |
3 | device, host, mode; 256-byte path |
| MOUNT_IMAGE / UNMOUNT_IMAGE | $F8 / $E9 |
2 / 1 | the push into the cartridge; 60 s |
| MOUNT_ALL / SET_BOOT_MODE / CONFIG_BOOT | $D7 / $D6 / $D9 |
0 / 1 / 1 | |
| OPEN_DIRECTORY | $F7 |
1 | host; 256 bytes: path, NUL, filter |
| READ_DIR_ENTRY | $F6 |
2 | maxlen (never 31), flags; $7F,$7F ends |
| SET / GET_DIRECTORY_POSITION | $E4 / $E5 |
1 / 0 | one word |
| CLOSE_DIRECTORY | $F5 |
0 | |
| COPY_FILE | $D8 |
2 | hosts from 1; payload src|dst |
| SET / GET_HOST_PREFIX | $E1 / $E0 |
1 | |
| GET_DEVICE_FULLPATH | $DA |
1 | |
| OPEN_APPKEY | $DC |
0 | 6 bytes: creator (word), app, key, mode, reserved 0 |
| READ / WRITE / CLOSE_APPKEY | $DD / $DE / $DB |
0 / 1 / 0 | READ: word length + value |
| GENERATE_GUID | $BB |
0 | 36 characters |
| HASH INPUT / COMPUTE / LENGTH / OUTPUT | $C8β$C5 |
1 | also $C3, $C2 |
| BASE64 encode / decode | $D0β$CD / $CCβ$C9 |
||
| QR code | $BCβ$BF |
NAK on this build: $F0, $EB, $E7 NEW_DISK, $E6 UNMOUNT_HOST, $E3,
$DF, $D5, $D4, $D3 RANDOM_NUMBER, $D2 GET_TIME, $D1, $C1, $BA,
$A0β$A9, $90, $3F.
Network devices $71β$78
| Command | Code | nparam | Notes |
|---|---|---|---|
| OPEN | $4F |
2 | mode, translation; payload the devicespec |
| STATUS | $53 |
2 | two zeros; 4 bytes: waiting (word), connected, status |
| READ | $52 |
1 | one word, β€ waiting and β€ 1024 |
| WRITE | $57 |
1 | one word; payload |
| CLOSE | $43 |
0 | |
| SET_PARSER / PARSE / QUERY | $FC / $50 / $51 |
2 / 0 / 0 | parser 0 none, 1 JSON, 2 HTML, 3 XML |
| SET_PARAMETER | $FB |
2 | 0 query flags, 1 line ending |
| SET_HTTP_MODE | $4D |
2 | 0 body, 1 collect, 2 get, 3 set headers, 4 POST data |
| SEEK / TELL | $25 / $26 |
1 / 0 | four-byte offset |
| TRANSLATION / SET_EOL / SET_INT_RATE | $54 / $4C / $5A |
||
| RENAME, DELETE, LOCK, UNLOCK, MKDIR, RMDIR | $20β$2B |
2 | payload the devicespec |
| CHDIR / GETCWD | $2C / $30 |
0 | |
| ACCEPT / CLOSE_CLIENT | $41 / $63 |
0 | TCP server |
| GET_REMOTE / SET_DESTINATION / USERNAME / PASSWORD | $72 / $44 / $FD / $FE |
0 |
Disk $31β$38, printer $40, clock $45, modem $50
Clock replies, measured: GETTIME $93 6 bytes (D, M, Y, H, M, S); $54 7
bytes; GET_ISO_LOCAL $49 / GET_ISO_UTC $5A 25 bytes with the NUL; ProDOS
$50 4; SOS $53 19; $4D 8; zone name $47, its length $4C; $9A
GETTIME in the alternate zone. $74 sets the adapter's zone, $99 the
alternate. Disk READ/WRITE/PUT $52/$57/$50 (one sector-number
parameter), STATUS $53; printer WRITE $57; modem STATUS/WRITE/STREAM
$53/$57/$58 and seven configuration commands.
The cartridge itself
Registers $14 SLICE_ACK, $15 TV, $16 HSC (0 off, 1 on, 2 forget, 3
install). One-shots (the loader's): $0DFE SWAP, $0DFD GO, $0DFC CONFIG,
$0DFB GO_BIOS. Leave with JMP $0600 (boot the armed image) or JMP $0603
(CONFIG), after SEI and $7F to CTRL. POKEY at $0450: RANDOM is the
only live read; no interrupts. Board layouts at $0C14: 0 ROM, 1 ROM+POKEY,
2 SuperGame, 3 SG+POKEY, 4 SG+RAM, 5 SG 9-bank, 6 RAM, 7 Absolute, 8
Activision, 9 HSC.
Battleship
The merged 7800 port (fujinet-battleship 87b8cc7): a shared game, and a
7800 layer of graphics.c, input.c, osk.c, sound.c (POKEY) and one
fujinet.c through which every call reaches the cartridge
(CUSTOM_FUJINET_CALLS, because the 7800's fuji_* are macros). Each server
request is network_open/network_read/network_close with bin=1: the
509-byte game state in binary, one READ. The art is the MS-DOS client's, fitted
into the tile engine's 128 tiles by mkchr.py. A local server
(fujinet-game-system/battleship, go run .) needs MAME in real time:
THROTTLE=1.
CONFIG
fujinet-config atari7800/: hosts, files, copy, WiFi, info (TV, INPTCTRL,
HSC), boot, lobby, on the tile engine. Draws straight out of the reply window;
fujiraw.c streams the payloads assembled from pieces. Baked into the
firmware with build-cart.sh --bake.
Things that cost real time
- MARIA's DMA windows (280β420 ns) are too short for a microcontroller: a real SRAM serves the game, and nothing MARIA reads may live in the mailbox.
- Only the first INPTCTRL lock counts.
- MAME hides the BIOS's reads; mode changes are write-triggered.
- 7800basic's silent miscompiles (index and
for-bound expressions). - ITC Eras's cap height is wrong in the font files (
tools/fixeras.py). - Game servers count real seconds:
THROTTLE=1. - The clock's command names mislead; measured replies above.
- A font in a switched bank breaks the picture while the program waits.
Troubleshooting
| Symptom | Cause |
|---|---|
fn_go returns $FD |
No magic at $0C09: not a FujiNet cartridge, or no claim (GAME mode) |
| Every transaction after the first: no answer | Your own sequence counter; use ACKSEQ + 1 |
| ERR 2 on network | Wait longer (90 s budget); check the server |
REPLY_CMD $15 |
Command not on this build, or wrong parameter shape |
| OPEN_APPKEY times out | Missing the sixth, reserved byte |
| Reply text shows as garbage | MARIA reading the mailbox; copy to RAM |
| BASIC copies wrong bytes | Expression in an array index |
BOOT_STATE $80 |
BOOT_ERR: 1 too big, 2 truncated, 3 no layout, 4 store busy |
| Yellow screen after boot | The loader's stores below $1000 don't reach the cartridge |
| Banked app runs as garbage | Claim kind 0 and no .a78 type: name the layout |
Sources of truth
| Source | Revision |
|---|---|
fujinet-firmware pico/atari-7800, pico/common, device dispatch |
07e9bbaa9 |
fujinet-lib atari7800 (PR #152) |
1ebf9d0 |
fujinet-config atari7800/ |
f38ed1d |
fujinet-battleship |
87b8cc7 |
| 7800basic | e7c18d2 (v0.41) |
| cc65 | 2.19 |