Skip to content

P8X ROM Monitor — Command Reference

The ROM monitor is the firmware that runs at power-on. It lives at $0000 in EEPROM (firmware/p8xmon.asm), talks to you over the 6850 ACIA serial console (9600 8N1), and is the entry point to everything else — from here you can inspect/modify memory, drive the CompactFlash card, and boot P8X/OS.

Source of truth: firmware/p8xmon.asm (and its in-ROM H/? help, which this document mirrors). The companion programmer's guide is the instruction-set reference; this is the monitor reference.

Running it

On reset the CPU jumps to $0000, which vectors to the monitor body at $0160; it resets the ACIA and prints the P8X MONITOR banner and a * prompt. In the emulator:

./os/run.sh        # builds the monitor ROM (BASIC is no longer ROM-resident) and launches it
# or directly, against an EEPROM image you've assembled and a CF disk image:
emulator/p8xemu -c disk.img eeprom.bin

Type a command letter at the * prompt. Commands are single letters; those that take an address read 4 hex digits (AAAA) right after the letter.

Commands

Command Syntax What it does
E E AAAA Examine / modify memory from AAAA. Interactive (see below).
D D AAAA Dump 256 bytes from AAAA as hex + ASCII. Pages (see below).
I I Init CF: reset the card, set 8-bit mode, IDENTIFY, print the model string.
F F Format the CF card as P8XFS (writes the boot block + root directory). Asks Y/N.
B B Boot the OS image from the CF card into $2000 and run it.
G G AAAA Go: JSR AAAA. The called code returns to the monitor with RTS.
? / H ? or H Print the built-in command help.

BASIC is no longer ROM-resident (the old X command is gone). It ships as the disk program /BIN/BASIC.BIN (assembled from basic/p8xbasic.asm); run it from the OS like any other program, and its BYE returns to the OS.

E — examine / modify (interactive)

E AAAA shows one byte at a time: aaaa: vv and then waits for input:

  • two hex digits — write that value to the location, then advance;
  • Enter (CR) — leave the byte unchanged, advance to the next;
  • . — stop and return to the prompt.

So you can walk forward through memory, setting only the bytes you want.

D — dump with paging

D AAAA prints sixteen 16-byte rows (256 bytes) as hex with an ASCII column (bytes <$20 or ≥$7F shown as .). After each block it waits for a key:

  • Enter (CR) (or any other key) — dump the next 256-byte block; the address keeps walking forward, so repeated Enters page through memory;
  • . — return to the prompt.

(This mirrors the E command's CR=next / .=exit convention.)

Returning to the monitor

Anything the monitor launches can come back to it:

  • G target — returns on RTS.
  • P8X/OS (B) — type EXIT (or MON).

Each of those re-enters the monitor (BASIC/OS do a cold restart via JMP $0000).

BIOS jump table — the program ABI

The monitor publishes a small jump table at $0100 so RAM-resident programs (P8X/OS, your own code loaded via G) can call console + CF services without knowing the monitor's internal addresses. These entry points are stable:

Address Name Behaviour
$0100 CONIN wait for a key; char → A
$0103 CONOUT A → serial (expands a bare LF to CR LF — see below)
$0106 CONST A = RDRF bit; Z=1 when no key is waiting
$0109 CFINIT reset CF + set 8-bit mode; C=1 on error
$010C CFREAD read sector LBA → (P1); P1 += 512
$010F CFWRITE write SBUF → sector LBA
$0112 PUTS print (P1)+ until $00
$0115 PHEX8 print A as two hex digits
$0118 FFIND find root file FNAME → LBA+FLEN; C=1 if not found
$011B FCREATE create root file FNAME from FSRC/FLEN; C=1 on error
$011E FDELETE tombstone root file FNAME (flag → $FF); C=1 if not found
$0121 FCOMMIT register a streamed file: write a root entry for data already at the free pointer (FNAME, length FLEN, sectors =ceil(FLEN/512)) and bump the free pointer; C=1 if root full
$0124 FOPEN open root file FNAME for sequential reading; P1 = a caller-owned 512-byte sector buffer; C=1 if not found
$0127 FGETB next byte of the open read stream → A (C=0); C=1 at end of file (refills from disk as needed)
$012A FWOPEN open a sequential write stream (streams to disk at the free pointer; uses SBUF as its buffer)
$012D FPUTB append byte A to the write stream (flushes a full sector automatically)
$0130 FCLOSE flush the partial sector + register file FNAME (length = bytes written); C=1 if root full
$0133 FRESOLVE resolve path at P1 (/a/b) → set the directory extent + leaf FNAME; a following FFIND/FOPEN runs in that dir; C=1 on a bad path
$0136 FNORM format the string at P1 into FNAME (≤12 chars, upper-cased, space-padded; stops at NUL/space)
$0139 FOPENDIR begin iterating the directory at path P1 (""/"/" = root); C=1 if not a directory
$013C FNEXT next live entry → FNAME/FFLAG/LBA/FLEN; C=1 at end (skips deleted entries)
$013F FLOADAT bulk-read FLEN bytes from sector LBA into (P1), a whole sector at a time (the fast "slurp a file" primitive; EDIT + the OS loader use it)
$0142 FOPENDIRAT begin iterating the directory whose 4-sector extent starts at the 16-bit LBA A (low) + LBA1 ($1F48, high) — lets a caller iterate an extent it already resolved, e.g. the OS's CWD. Set LBA1=0 for LBA < 256
$0145 FSDIRBUF point the directory sector buffer at the page in A (high byte; 512-byte page-aligned buffer; defaults to SBUF=$1D at boot and is reset to SBUF by FOPENDIR/FOPENDIRAT). Used by both FNEXT iteration and FSCAN (the engine behind FRESOLVE/FFIND/FOPEN), so repointing it lets a program iterate and resolve paths while a write stream keeps SBUF — e.g. DIR redirected/piped, or CAT *.X >OUT (resolve+open each match without clobbering the open write stream's SBUF)

| $0148 | CFSEL | select the active CF drive for subsequent sector/FS I/O: A = drive (0/1) → DRVSEL. The OS's mount redirect (in FRESOLVE/RV_START) calls this to route a /D1 path to drive 1 — there is no drive-letter prefix. Both cards share the $FF10 task-file port; DRVSEL is ORed into CFHEAD as the ATA device bit | | $014B | CFCURDRV | current CF drive → A (0/1) | | $014E | GCLS | clear the on-screen text console (the "glass TTY") and home its cursor; a no-op when no GL card is fitted. Added for two-mode operation — the glass TTY that mirrors CONOUT onto a GL display; see p8x-two-mode-design.md |

Call them with JSR $0103 etc. (P8X/OS is built entirely on this table.)

FDELETE marks the directory entry deleted but leaves its data sectors in place; they are reclaimed by the next PACK. To overwrite a file, FDELETE then FCREATE.

Directory: the file calls operate on a current directory extent that defaults to the root (LBA 33) and reverts there after each call. Call FRESOLVE first with a path to aim the next call at a subdirectory (it walks the ./.. tree and leaves the leaf name in FNAME): FRESOLVE("/BIN/X") then FOPEN reads /BIN/X; FRESOLVE("/SUB/W") then FWOPEN/FPUTB/FCLOSE writes /SUB/W. FFIND/FOPEN/FCREATE/FDELETE/FCLOSE are all path-aware this way. Parameters use fixed RAM: FNAME ($1F4A, 12-byte space-padded name), FSRC ($1F56, FCREATE source address), FLEN ($1F58, 24-bit length, 3 bytes — FCREATE input, FFIND output; max file 16 MB, matching the 24-bit ROLBA); FFIND returns the start LBA in the shared LBA ($1F47). (Subdirectory LBAs are 16-bit: the directory-iteration/resolution path carries DIRLBA/DILBA plus their high bytes, so a directory whose extent starts at LBA ≥ 256 resolves and lists correctly.)

Memory map

Range Use
$0000–$17FF EEPROM (6 KB; shrunk from 8 KB 2026-09-14 to free the $1800-$1FFF RAM island) — monitor + BIOS at $0000 (~4.9 KB used). BASIC is no longer ROM-resident; it ships as /BIN/BASIC.BIN on disk.
$2000–$56FF RAM — P8X/OS image loads here ($2000, ~13.8 KB including the resident window-manager kernel, ending ~$55E6; the 16 KB cap = the on-disk LBA 1–32 boot region).
$1800–$1CFF RAM — OS buffers in the low RAM island (moved there 2026-09-14): the stdin read buffer IBUF $1800, the search PATH $1A00, OS scratch, the >> prepend buffer APBUF $1B00.
$5700–$58FF RAM — OS data: shell variables (input line $5700), FS/PACK/FSCK state, the CWD path text $5800.
$1D00–$1EFF RAM — SBUF sector buffer (in the $1800-$1FFF island; was $6100, then $5E00; monitor-owned, but not part of the command //#define ABI).
$1F00–$1FFF RAM — firmware/BIOS scratch (fixed ABI — commands reach it through //#use abi / //#use mem): monitor line buffer $1F00 (64 bytes, so an input line is capped at 63 characters), the parameter block + read/write/dir-iteration state $1F40 (CF LBA $1F47–$1F49, FNAME $1F4A, FSRC/FLEN, FFLAG $1F75, DIBUFH $1F7E), the console and glass-TTY state $1FA1–$1FAF. Moved from $6000–$60FF with the 2026-09-14 ROM shrink.
$5900–$F7FF RAM — TPA: user programs + data (RUN loads at $5900, ~39.8 KB; the C stack grows down from CSTACKTOP $F800). Above $F800 sit the fixed scratch pages: the shell's 8-line command-history ring at $F800–$F9FF, commands' glob/dir-iteration page (FSDIRBUF) at $FA00, and the file-read buffer (RDBUF) at $FC00.
$FE00–$FEFF RAM — stack (P3 grows down from $FEFF).
$FF00 switch input port (read)
$FF02 LED output port (write)
$FF04 / $FF05 6850 ACIA status / data (the console serial port)
$FF08 / $FF09 second 6850 ACIA status / data — the 2nd serial port added for two-mode operation (register-identical to the console ACIA: status bit0 RDRF, bit1 TDRE). Drives host file transfer via the kermit command while the console keeps $FF04. Modelled by p8xemu (-2i/-2o file-backed RX/TX).
$FF10–$FF17 CF-IDE task-file registers
$FF20–$FF2F retired — was the graphics display's DEVICE door (register pokes drew immediately). Closed by the single-interface migration: reads float $FF like any absent card, and the register file survives only as the GL walker's internal property. The GL/PGC port at $FF50 is the one graphics interface — see p8x-graphics-theory.md. Modelled by p8xemu — see emulator/README.md

Every data address above is single-sourced in generators/gen_memmap.py (which emits memmap.inc for the OS/monitor to .include, plus memmap.h, memmap.py, and the command-facing lib_mem). This table is a hand-maintained mirror — regenerate the map and update it together. (Generating the table itself from gen_memmap.py is a tracked BACKLOG item.)

Reset clears the PC to $0000; the stack pointer (P3) is initialised to the top of RAM.

Console newlines (CONOUT / $0103)

P8X emits a bare LF for a newline — p8cc's puts ends with LDA #10, and most /bin commands follow suit. A bare LF moves the cursor down but not back, so on a real serial terminal every line would step diagonally across the screen.

CONOUT (the monitor's PUTC) therefore expands a bare LF into CR LF. This is the same place Unix puts the translation — on the terminal device (ONLCR in the tty line discipline), not in the program and not in write().

Two consequences worth knowing:

  • Files and pipes are unaffected. The OS's OUTCH routes REDIRF >= 1 to a file or capture buffer, which never reaches CONOUT, so dir > out.txt and a | b carry clean single-byte LF. Nothing has to check where its output is going; the routing already answers that.
  • An existing CR LF is not doubled. TTYLST remembers the last byte sent, so a caller that already emits CR LF — the monitor's own CRLF, the OS's key echo — passes through unchanged rather than becoming CR CR LF.
Address Name Meaning
$1FA1 TTYRAW 0 = expand; nonzero = pass bytes through untouched
$1FA2 TTYLST last byte transmitted (the anti-doubling state)

The two-mode / glass-TTY state lives just above, in the same console page (generated in generators/gen_memmap.py; see p8x-two-mode-design.md):

Address Name Meaning
$1FA4 GFXPRES 1 = a GL card is fitted (the mode flag the monitor sets at wake); GL programs and the on-screen console gate on it
$1FA5–$1FAE glass-TTY state the on-screen console's own cursor and scratch: GTCOL/GTROW (text cursor), GTSUSP ($1FA7, 1 = a full-screen program owns the screen, so the console stops drawing), GTXL…GTYH (pixel position), GTCH/GTTMP/GTCNT
$1FAF GCONEN 1 = mirror CONOUT onto the GL screen (the glass TTY). On by default whenever a card is fitted — DISPINIT sets it at wake, installs the stroke font from /FONT.GL on the CF root, and blanks the screen, so the monitor itself is on the LCD pre-boot; screen off disables the mirror for a session

TTYRAW is the escape hatch for sending binary down the serial link, where a $0A is data rather than a newline — the equivalent of stty raw. Nothing in the tree needs it yet; set it, do the transfer, clear it.