📖 Guide: AC6502 Documentation — the user's and programmer's guide for the whole family. The tutorial half of what this README specifies lives there: BASIC, assembly and the Kernal API, and the Monitor.
BIOS is the firmware ROM for the A.C. Wright 6502 family of computer systems. It occupies the upper 32KB of the address space ($8000–$FFFF) and provides everything the machine needs to go from power-on to a usable computing environment.
The CPU is a WDC 65C02S. That is the Rockwell instruction set — including the bit-addressed RMB/SMB/BBR/BBS — plus WDC's WAI and STP. Building the ROM therefore needs cc65's W65C02 setting, not its narrower 65C02; BIOS.asm asserts this and fails the build with a reason if it is narrowed.
The A.C. Wright 6502 family of computer systems is a modular design where every I/O card is optional. On reset, the Kernal probes each I/O slot to discover which hardware is installed and records the results in a single bitmask byte at HW_PRESENT ($030D). Only detected hardware is initialised — missing cards are silently skipped and never cause a hang.
The probe-and-boot sequence is:
- Clear
HW_PRESENT— all bits start at zero - Probe each I/O slot — RAM (read-back), RTC (NVRAM read-back), CompactFlash (BSY/RDY with timeout), Serial (TDRE after reset), GPIO/VIA (DDR read-back), SID (active oscillator), Video (VRAM read-back)
- Conditionally initialise — each subsystem is only initialised if its probe succeeded
- Console auto-detection — if video is present,
IO_MODEis set to video; if only serial is present, output is routed to the serial port; if neither is found,IO_MODEis left unchanged (no halt — allows cartridges with their own display hardware to boot) - Beep — a short tone on the SID (skipped silently if SID absent; provides audible feedback that the system is alive)
- Boot vector check — if
BOOT_VECTOR($035B) is non-zero, jump to the address stored there (cartridge or external program takes over). Otherwise continue to normal boot - Console check — verify that at least video or serial is present. If neither is found and no boot vector was set, the CPU halts (interactive boot requires a console)
- Splash screen — displayed on the active console:
-- 6502 BIOS v1.5 --
ENTER=BASIC ESC=MONITOR
On video the two lines are centred on the 40-column screen, as above. On a serial console they are printed as plain text, left aligned — a terminal's width is the user's to choose, so there is no column to centre on.
- Boot menu with timeout — waits ~5 seconds for a keypress, then auto-boots BASIC
- ENTER (or timeout) — launches the BASIC interpreter
- ESC — drops into the machine-code monitor
The HW_PRESENT byte at $030D can be read from user code or inspected in the monitor. Each bit corresponds to an I/O slot:
| Bit | Mask | Card |
|---|---|---|
| 0 | $01 |
RAM card low (IO 1) |
| 1 | $02 |
RAM card high (IO 2) |
| 2 | $04 |
RTC DS1511Y (IO 3) |
| 3 | $08 |
CompactFlash (IO 4) |
| 4 | $10 |
Serial R65C51 (IO 5) |
| 5 | $20 |
GPIO/VIA 65C22 (IO 6) |
| 6 | $40 |
SID/ARMSID (IO 7) |
| 7 | $80 |
Video TMS9918 (IO 8) |
All hardware-dependent operations are guarded at every level — Kernal, BASIC, and Monitor:
- CompactFlash absent —
LOAD,SAVE,DIR,DEL,BLOAD,BSAVE,FORMATin BASIC printNO DEVICE; MonitorL,S,@printI/O ERROR;StWaitReadyreturns an error at once when the boot probe found no card, and times out instead of hanging on a card that stops answering - Serial absent — IRQ handler skips serial status polling;
Chrinflow control writes are suppressed; XModemLOAD/SAVEreturn an error - GPIO/VIA absent —
SysDelayfalls back to a calibrated software busy-loop;JOY()returns$FF(every line reads released, as an untouched stick does); keyboard IRQ check is skipped - SID absent —
Beep,SOUND,VOL,SidPlayNote,SidSilence,SidSetVolumesilently return - Video absent —
CLS,LOCATE,COLORsilently skip (arguments are still consumed);VideoClear,VideoSetCursorandVideoSetColorskip with them, so a cartridge calling the slot gets the same treatment; console auto-switches to serial
The two silent rows are silent because a screen and a speaker have nothing to report back — the statement had no answer to return, so there is nothing an error could say. The rows that move data (CompactFlash, RTC) raise NO DEVICE instead, because there the program asked for something it did not get. Either way the arguments are parsed and range-checked first: LOCATE 24,0 and VOL 16 are ILLEGAL QUANTITY on a machine with no screen and no sound card, so a program is wrong or right everywhere rather than only where it was written.
- RTC absent —
TIME,DATE,SETTIME,SETDATE, andNVRAM(write) in BASIC printNO DEVICE;NVRAM()(read) returns 0
A full interactive floating-point BASIC interpreter is included, with a feature surface comparable to Microsoft 6502 BASIC. Programs are typed line-numbered and executed with RUN. Numeric variables hold 5-byte (40-bit) floating-point values; a $ suffix makes the name a string variable. Each name can additionally be dimensioned as a 1-D array via DIM. Multiple statements per line are separated by :.
Variable names: any length, letters and digits, but only the first two characters are significant — the usual Microsoft BASIC rule.
COUNTandCOURSEare the same variable (CO), andPRINT COUNTafterCOUNT = 7 : COURSE = 9prints9. A name may not contain a keyword:SCOREwill not parse, because it containsOR.
Numeric range: ~±1.7 × 10³⁸, nine significant digits (
PRINT 1 / 3gives.333333333,PRINT SQR(2)gives1.41421356). Numbers print with a leading-space sign convention (positive numbers prefixed by a space, negative by-). Boolean expressions evaluate to-1(true) or0(false).
Core Statements
| Command | Syntax | Effect |
|---|---|---|
PRINT |
PRINT [item [sep item ...]] |
Output items to console. Items may be string or numeric expressions. ; = no separator (trailing ; suppresses CRLF); , = advance to next 14-column print zone. Bare PRINT prints only CRLF |
INPUT |
INPUT ["prompt"{;|,}] var [, var ...] |
Read value(s) from the user. Numeric or string vars supported. Re-prompts with ?REDO FROM START on bad numeric input; ?EXTRA IGNORED if too many comma-separated values |
LET |
[LET] var = expr |
Assign expression to variable. LET keyword is optional |
GOTO |
GOTO linenum |
Jump unconditionally to line linenum |
GOSUB |
GOSUB linenum |
Push current position and jump. Nesting is bounded by the 6502 stack, which the frames share with the interpreter's own working space — at least 20 levels are available whatever the subroutine does, and about 27 for a simple one. Exceeding the space raises OUT OF MEMORY |
RETURN |
RETURN |
Pop the GOSUB stack and resume after the calling GOSUB |
IF |
IF expr THEN stmt [ELSE stmt] |
Execute THEN branch if expr non-zero, else (if present) the ELSE branch. THEN linenum is shorthand for THEN GOTO linenum, and ELSE linenum for ELSE GOTO linenum |
FOR |
FOR var = init TO limit [STEP step] |
Counted loop. Default step is 1. Nests 14 deep — see Stack limits below. The limit is tested at NEXT, as in Microsoft 6502 BASIC, so the body always runs at least once — FOR I = 5 TO 1 runs once and leaves I at 6 |
NEXT |
NEXT [var] |
Increment loop variable and branch back to matching FOR if condition holds. One variable only — the comma form NEXT J, I is not accepted and fails at runtime with ?NEXT WITHOUT FOR ERROR at the comma |
REM |
REM [text] |
Comment — rest of line is ignored |
END |
END |
Stop execution and return to OK. Variables preserved |
STOP |
STOP |
Stop and print BREAK IN nnnn. Resume with CONT |
CONT |
CONT |
Continue after STOP, END or a Ctrl+C break (immediate mode only), including from inside a FOR loop or a subroutine — the loop and GOSUB frames survive the break, so NEXT and RETURN still find them. CAN'T CONTINUE after an error, a NEW, a CLR or a RUN, all of which reset the stack those frames live on |
ON |
ON expr GOTO l1,l2,... / ON expr GOSUB l1,l2,... |
Evaluate expr, branch to nth target. Out-of-range index silently continues |
DATA |
DATA v1,v2,... |
Inline data for READ (numeric or string literals). Skipped during normal execution |
READ |
READ var [,var ...] |
Read next value(s) from DATA into variables. OUT OF DATA if exhausted |
RESTORE |
RESTORE |
Reset DATA pointer to start of program |
LIST |
LIST |
Print the program in detokenized form. Ctrl+C interrupts |
RUN |
RUN [linenum] |
Clear variables and run the program (optionally from linenum) |
NEW |
NEW |
Erase the program and clear variables |
CLR |
CLR |
Clear variables and arrays; reset GOSUB/FOR stacks. Program is kept |
DIM |
DIM var(size) [, var(size) ...] |
Dimension a 1-D array (numeric or string), valid indices 0..size. REDIM'D ARRAY if already dimensioned. Only one dimension is supported |
DEF FN |
DEF FN A(X) = expr |
Define a single-argument numeric user function. Call with FN A(value) |
POKE |
POKE addr, value |
Write byte value to memory address addr |
BRK |
BRK |
Drop into the machine-code monitor. Return to BASIC with X |
Storage & System
| Command | Effect |
|---|---|
SYS <addr> |
Call a machine-code routine; RTS returns to BASIC |
LOAD "name" |
Load a named file from CompactFlash to $0800 |
SAVE "name" |
Save the current program to CompactFlash |
LOAD (no arg) |
Receive a program via XModem on the serial port |
SAVE (no arg) |
Transmit the current program via XModem |
DIR |
List current disk's directory (prints DISK n header) |
DEL "name" |
Delete a named file from the current disk |
DISK <n> |
Select CF disk bank n (0–255); resets to 0 on boot. Each disk is 1 MB (2048 sectors) — 256 disks = 256 MB total |
BLOAD <addr>,"name" |
Load a file's raw bytes from the current disk to address addr |
BSAVE <addr>,<len>,"name" |
Save len bytes from address addr to a named file on the current disk |
FORMAT |
Erase the current disk's file directory (prompts ERASE DISK n? (Y/N)) |
BANK <n> |
Select 1KB RAM bank n at $8000–$83FE |
MEM |
Print free bytes, HW=$xx, and DISK n |
Both extensions name the same thing: the bytes of a program, ready to sit at
$0800. A .prg is one of these whose BASIC part is a single 10 SYS 2060 line
with machine code attached behind it. The extension is a label for your benefit —
nothing in the BIOS reads it, so name files whatever helps you.
What decides the behaviour is the command, not the filename:
| Command | Use it for |
|---|---|
LOAD "name" / SAVE "name" |
Programs. Loads at $0800 and readies BASIC to RUN it |
BLOAD <addr>,"name" / BSAVE <addr>,<len>,"name" |
Raw bytes at an address you choose — data, graphics, code |
Both accept any filename, so BLOAD 32768,"GUESS.BAS" will happily drop a BASIC
program at $8000 as raw data.
LOAD and SAVE round-trip a .prg intact, machine code included, and MEM
accounts for the whole thing.
Two rules for .prg files:
- Don't edit the BASIC line. Inserting or deleting a line shifts the attached
machine code, whose addresses were fixed when it was built.
LIST,RUNandSAVEare all fine. (The C64 works the same way.) - Load them with
LOAD. The Monitor'sLalso works, as long as you take its default$0800address and thenXback to BASIC. A Wozmon upload does not — it has no way to tell BASIC how long the image is, so the machine code is lost as soon as you assign a variable.
Video & Display
| Command | Effect |
|---|---|
CLS |
Clear the screen and reset cursor to (0, 0) |
LOCATE <row>, <col> |
Move cursor to row 0–23, column 0–39 |
COLOR <fg>, <bg> |
Set TMS9918 text foreground/background colours (0–15 each) |
Sound
| Command | Effect |
|---|---|
SOUND <voice>, <freq>, <dur> |
Play a tone on voice 1–3 at freq Hz for dur centiseconds, then silence. Numbered from 1 as in Commodore BASIC V3.5; the SidPlayNote Kernal slot indexes the same three voices from 0 |
VOL <n> |
Set SID master volume (0–15) |
Timing & I/O
| Command | Effect |
|---|---|
PAUSE <n> |
Pause for n centiseconds (~10 ms each) |
WAIT <addr>, <mask> |
Spin until (addr) AND mask is non-zero; Ctrl+C aborts |
Time & Date
| Command | Effect |
|---|---|
TIME |
Print current RTC time as HH:MM:SS |
DATE |
Print current RTC date as CCYY-MM-DD |
SETTIME <hh>, <mm>, <ss> |
Set the RTC time |
SETDATE <cc>, <yy>, <mm>, <dd> |
Set the RTC date |
NVRAM <addr>, <value> |
Write a byte to RTC NVRAM at address 0–255 |
Functions & Expressions
| Function | Returns |
|---|---|
ABS(x) |
Absolute value of x |
SGN(x) |
Sign of x: 1, 0, or -1 |
INT(x) |
Largest integer ≤ x (floor) |
SQR(x) |
Square root of x (error if negative) |
EXP(x) |
e raised to x |
LOG(x) |
Natural logarithm (error if x ≤ 0) |
SIN(x) / COS(x) / TAN(x) |
Trig functions, radians |
ATN(x) |
Arctangent, radians |
RND(x) |
Pseudo-random float in [0, 1) for x > 0; repeats last value for x = 0; reseeds for x < 0 |
PEEK(addr) |
Byte value at memory address addr |
FRE(x) |
Free bytes between top of variable space and bottom of string heap (argument ignored) |
POS(x) |
Current print column (argument ignored) |
LEN(s$) |
String length |
VAL(s$) |
Parse s$ as a number; returns 0 if not numeric |
ASC(s$) |
ASCII code of first character of s$ |
CHR$(n) |
One-character string with ASCII code n |
STR$(n) |
Numeric value n formatted as a string |
LEFT$(s$,n) / RIGHT$(s$,n) |
First / last n chars of s$ |
MID$(s$,start[,len]) |
Substring of s$ starting at 1-based index start |
TAB(n) |
In PRINT, advance cursor to column n (no-op if already past) |
SPC(n) |
In PRINT, emit n spaces |
INKEY |
Non-blocking key read: ASCII code or 0. No parentheses |
JOY(1) / JOY(2) |
Joystick port 1 or 2 bitmask (R-L-D-U-Y-X-B-A). The port is active low — each line is pulled up and grounded by its switch — and the value is the port read raw, so a held button is a 0 bit and an untouched stick reads $FF. Test a direction with IF (JOY(1) AND 16) = 0 |
NVRAM(addr) |
Read byte from RTC NVRAM (returns 0 if RTC absent) |
HEX(n) |
In PRINT, output n as $xxxx hex; in expressions, returns n unchanged |
MIN(a,b) / MAX(a,b) |
Smaller / larger of a and b |
var(index) |
Array element access. Array must be DIM-med first |
Operators
+ - * / — standard arithmetic. ^ — exponentiation. + between strings — concatenation. Comparisons = <> < > <= >= work on numbers and strings. Logical AND, OR, NOT operate bitwise on the integer parts of operands; relational comparisons return -1 (true) or 0 (false).
Operator Precedence (high to low)
| Level | Operators |
|---|---|
| Power | ^ |
| Unary | - (negate), + |
| Multiplicative | *, / |
| Additive | +, - |
| Relational | =, <>, <, >, <=, >= |
| Logical NOT | NOT |
| Logical AND | AND |
| Logical OR | OR |
Stack limits: GOSUB and FOR/NEXT frames both live on the 6502 stack, which they share with the interpreter's own working space. At least 20 GOSUB levels are available whatever the subroutine does, and about 27 for a simple one; exceeding that raises
OUT OF MEMORY, becauseBasCmdGosubchecks the stack pointer againstGOSUB_STACK_MINbefore it pushes.
FORhas no such guard, and fails differently. AFORframe is 18 bytes (TXTPTR,CURLIN, the 5-byte limit, the step sign, the 5-byte step, the variable address, and the$81tag), so 14 of them fill page 1.BasCmdForpushes without testing the stack pointer, so the 15th frame overwrites the bottom of the stack instead of raising an error — and the failure surfaces later, at the matchingNEXT, as?NEXT WITHOUT FOR ERROR. Fourteen levels of nesting is the working ceiling at the top level of a program, and less inside aGOSUB, which shares the same 256 bytes.
Memory layout: Programs grow up from
$0800. Numeric/string scalar variables follow the program, then arrays, then the string heap which grows down from$8000.MEMand the cold-boot banner reportMEMSIZ - VARTAB(free bytes for variables, arrays, and strings combined).
A full-featured Supermon-style machine-code monitor occupies the $EE00–$FEFF segment. It supports memory inspection, 65C02 disassembly, register manipulation, code execution, CompactFlash and serial file I/O, and number base conversion. The monitor prompt is ..
The monitor is entered in three ways, and all three arrive through BRK, so all three print the banner, BRK AT $xxxx, and the register display:
- ESC at boot — cold entry from the boot menu
- BRK from BASIC — the
BRKstatement - Hardware BRK — any
BRKopcode in user code
Memory Inspection
| Command | Syntax | Description |
|---|---|---|
M |
M [addr] [addr] |
Hex + ASCII memory dump (8 bytes/line); bare M continues from last address |
D |
D [addr] [addr] |
Disassemble 65C02 instructions (20 lines default); supports the full WDC 65C02 + Rockwell instruction set |
R |
R |
Display saved CPU registers: PC=xxxx A=xx X=xx Y=xx SP=xx NV-BDIZC |
Memory Manipulation
| Command | Syntax | Description |
|---|---|---|
> |
> addr byte [byte...] |
Deposit (write) bytes starting at address |
F |
F addr addr byte |
Fill memory range with a byte value |
T |
T addr addr dest |
Transfer (copy) a memory block; handles overlapping regions |
H |
H addr addr byte [byte...] |
Hunt (search) for a byte pattern in a range |
C |
C addr addr addr |
Compare two memory regions; prints differing addresses |
Execution Control
| Command | Syntax | Description |
|---|---|---|
G |
G [addr] |
Go — JMP to address (or saved PC); restores all registers via RTI |
J |
J [addr] |
JSR — call a subroutine; RTS returns to the monitor with register display |
; |
; PC xxxx A xx X xx ... |
Modify saved registers (any subset, any order) |
File I/O & Utilities
| Command | Syntax | Description |
|---|---|---|
L |
L "file" [addr] |
Load from CompactFlash (with filename) or XModem (without) to address (default $0800). Loading at the default address lets X hand the program straight to BASIC, ready to RUN |
S |
S "file" addr addr |
Save to CompactFlash (with filename) or XModem (without) |
@ |
@ |
List current disk's directory (prints DISK n header) |
# |
# NN |
Select CF disk bank NN (hex 00–FF); # alone reports the current disk |
N |
N value |
Number conversion — hex ($xx), decimal (+ddd), or binary (%bbbb) input shown in all three bases |
X |
X |
Exit to BASIC |
Wozmon easter egg: The original Apple II Wozmon remains at
$FF00. Enter it from the monitor withG FF00or from BASIC withSYS $FF00.
Output is displayed on a TMS9918 video chip in 40×24 text mode. The screen scrolls upward automatically when the cursor reaches the bottom. The Kernal tracks cursor position and exposes routines for direct character and cursor manipulation.
Both a PS/2 keyboard (via CA1 interrupt) and a matrix keyboard (via CB1 interrupt) are supported simultaneously. Key presses are queued in a 256-byte ring buffer at $0200–$02FF and read via Chrin.
Two joystick ports are supported. ReadJoystick1 and ReadJoystick2 each return a bitmask byte in A:
Bit: 7 6 5 4 3 2 1 0
R L D U Y X B A
The joysticks share the VIA's two ports with the keyboard encoders, so a read cannot happen until the encoders let go of the lines. Each read therefore runs a fixed sequence:
KBDisable($A099) raisesCB2/CA2to tell both encoders to release the ports, then busy-waits ~200 µs so they have time to go high-impedance.- The 6502 reads the raw port directly —
GPIO_PORTBfor joystick 1,GPIO_PORTAfor joystick 2. KBEnable($A09C) lowersCB2/CA2to hand the ports back to the encoders.
The keyboard is briefly offline for the duration — roughly 200 µs per read — but the encoders buffer PS/2 and matrix keystrokes across the gap and resume their scan where they left off, so nothing is dropped. Because both ports are released together, both sticks can be read in a single disable/enable window: a caller that wants both at once should bracket two raw reads with its own KBDisable/KBEnable rather than paying the settle twice. This is exactly what JOY() in BASIC does, one stick at a time.
A simple flat filesystem is stored on a CompactFlash card (true 8-bit IDE). The card is divided into up to 256 disk banks of 1 MB each (2048 sectors × 512 bytes), giving a maximum usable capacity of 256 MB. The current disk bank is selected with DISK n in BASIC or #NN in the Monitor, and resets to 0 (disk 0) on power-on or reset.
Within each disk, the directory lives at the first sector (LBA n×2048) and holds up to 16 entries (8.3 filenames). Data sectors follow contiguously. The filesystem prevents a file on one disk from spilling into the next disk's region.
LOAD/SAVE/DIR/DEL in BASIC, and L/S/@ in the Monitor, all operate on the currently selected disk bank. BLOAD and BSAVE load/save raw binary data to/from any memory address, making it straightforward to load game maps, graphics, or data files while a program is running.
Assembly programs can access disk storage directly through the Kernal jump table (see FsLoadFileAddr, FsSaveFileAddr, FsSetDisk, and FsFormatDisk below).
A 6551 ACIA provides a serial port at 19200 baud (8-N-1). The IO_MODE Kernal variable selects whether Chrout routes to video or serial. LOAD/SAVE without a filename use the standard XModem protocol (128-byte blocks with checksum) to transfer programs over serial. The receiver initiates the transfer by sending NAK; the sender responds with data blocks; each block is acknowledged before the next is sent. The last block is padded with SUB ($1A). Compatible with any terminal program that supports XModem (checksum mode).
When an XModem transfer is initiated, the system prints XMODEM RX READY (receive) or XMODEM TX READY (send) and waits up to ~60 seconds for the terminal program to start the transfer, giving ample time to configure and begin the transfer in your terminal program.
A DS1511Y RTC provides time and date. RtcReadTime returns hours/minutes/seconds in A/X/Y (binary). RtcReadDate returns date/month/year. 256 bytes of battery-backed NVRAM are accessible via RtcReadNVRAM / RtcWriteNVRAM.
A SID chip provides audio output. The Beep Kernal routine plays a ~475 Hz tone on voice 1. Use SidPlayNote to play any frequency on any of the three voices, SidSilence to stop all voices, and SidSetVolume to set the master volume (0–15).
| Range | Size | Contents |
|---|---|---|
$8000–$9FFF |
8KB | I/O space (hardware registers) |
$A000–$A0FF |
256B | Kernal jump table (public API) |
$A100–$B7FF |
~6KB | Kernal routines |
$B800–$BFFF |
2KB | IBM CP437 character set (VRAM init data) |
$C000–$EDFF |
~11.5KB | BASIC interpreter (5-byte floating-point) |
$EE00–$FEFF |
~4KB | Machine-code monitor |
$FF00–$FFF9 |
250B | Wozmon (Apple I machine-code monitor) |
$FFFA–$FFFF |
6B | CPU vectors (NMI / RESET / IRQ) |
| Range | Size | Purpose |
|---|---|---|
$0000–$00FF |
256B | Zero page (Kernal + BASIC workspace) |
$0100–$01FF |
256B | CPU stack — and therefore BASIC's GOSUB and FOR frames, which are pushed onto it |
$0200–$02FF |
256B | Keyboard input ring buffer |
$0300–$03FF |
256B | Kernal variables (vectors, cursor, HW_PRESENT, CF_DISK, BOOT_VECTOR, RTC, FS state including FS_IO_ADDR, BASIC runtime) |
$0400–$04FF |
256B | BAS_LINBUF — the raw input line, as typed |
$0500–$05FF |
256B | BAS_TOKBUF — tokenizing scratch |
$0600–$07FF |
512B | FS_SECTOR_BUF — CompactFlash sector buffer, overwritten by any filesystem call (LOAD, SAVE, DIR, DEL, BLOAD, BSAVE, FORMAT) |
$0800–$7FFF |
~31KB | Program text grows up from $0800; numeric/string variables follow; arrays then string heap grow down from $8000 |
None of
$0400–$07FFis free memory, despiteBIOS.incnaming$0400USER_VARS— a legacy name kept only because Wozmon builds its input buffer on it. The free RAM is$003A–$00FFin zero page (for a machine-code program that has taken the machine over; not underneath a running BASIC, which uses that space as it interprets) and everything above your program up to$8000.
All public Kernal entry points are accessed through stable 3-byte jmp slots. Call these addresses from your own code — the implementation behind each slot can change without breaking your program.
The table is a fixed 256 bytes: the 51 published slots below, then 34 reserved slots ($A099–$A0FE) that return immediately, then one pad byte. New entry points are appended into the reserved space, so no existing address ever moves. Calling a reserved slot on a BIOS that has not filled it in yet returns cleanly rather than crashing.
| Address | Label | Description |
|---|---|---|
$A000 |
Chrout |
Output one character (routed by IO_MODE) |
$A003 |
Chrin |
Read one character from the input buffer |
$A006 |
WriteBuffer |
Push byte into the input buffer |
$A009 |
ReadBuffer |
Pop byte from the input buffer |
$A00C |
BufferSize |
Return number of bytes waiting in buffer |
$A00F |
SetIOMode |
Set IO_MODE: A=0 (video) or 1 (serial) |
$A012 |
GetIOMode |
Get IO_MODE → A |
$A015 |
InitVideo |
Initialise TMS9918 video chip — writes the mode registers and reloads the character set into the pattern table at $0800 |
$A018 |
VideoClear |
Clear the screen |
$A01B |
VideoPutChar |
Write character at current cursor position |
$A01E |
VideoSetCursor |
Set cursor: X=column (0–39), Y=row (0–23) |
$A021 |
VideoGetCursor |
Get cursor: returns column in X, row in Y |
$A024 |
VideoScroll |
Scroll screen up one line |
$A027 |
VideoSetColor |
Set TMS9918 text colour register: A=(fg<<4)|bg |
$A02A |
VideoChroutRaw |
Output character glyph at cursor (raw, no control-code handling): A=char code |
$A02D |
InitSID |
Initialise SID sound chip |
$A030 |
Beep |
Play a short beep tone |
$A033 |
SidPlayNote |
Play note: A=voice (0–2), X=freqLo, Y=freqHi |
$A036 |
SidSilence |
Silence all SID voices |
$A039 |
SidSetVolume |
Set SID master volume: A=0–15 |
$A03C |
FsLoadFile |
Load a file from CompactFlash by name |
$A03F |
FsSaveFile |
Save a file to CompactFlash by name |
$A042 |
FsDeleteFile |
Delete a file from CompactFlash by name |
$A045 |
InitKB |
Initialise VIA keyboard / joystick ports |
$A048 |
ReadJoystick1 |
Read joystick 1 → bitmask in A |
$A04B |
ReadJoystick2 |
Read joystick 2 → bitmask in A |
$A04E |
InitSC |
Initialise 6551 serial card (19200 8-N-1) |
$A051 |
SerialChrout |
Output character directly to serial (bypass IO_MODE) |
$A054 |
XModemLoad |
Receive data via XModem into memory at XFER_PTR; returns total bytes in XFER_REMAIN |
$A057 |
XModemSave |
Send data via XModem from XFER_PTR, XFER_REMAIN bytes |
$A05A |
RtcReadTime |
Read time → A=hours, X=minutes, Y=seconds |
$A05D |
RtcReadDate |
Read date → A=date, X=month, Y=year |
$A060 |
RtcWriteTime |
Write time ← A=hours, X=minutes, Y=seconds |
$A063 |
RtcWriteDate |
Write date ← A=date, X=month, Y=year |
$A066 |
RtcReadNVRAM |
Read NVRAM byte: X=address → A=data |
$A069 |
RtcWriteNVRAM |
Write NVRAM byte: X=address, A=data |
$A06C |
StReadSector |
Read one 512-byte CF sector |
$A06F |
StWriteSector |
Write one 512-byte CF sector |
$A072 |
StWaitReady |
Wait for CF ready; carry set on error |
$A075 |
SysDelay |
Delay A=count_lo, X=count_hi centiseconds (~10 ms each) using VIA T1 |
$A078 |
KernalInit |
Initialise all hardware (caller must reset stack pointer first; no cli, no splash). Returns via RTS |
$A07B |
KernalVersion |
Get BIOS version → A=major, X=minor |
$A07E |
FsLoadFileAddr |
Load named file from current disk to FS_IO_ADDR ($037F); returns size in FS_FILE_SIZE |
$A081 |
FsSaveFileAddr |
Save FS_FILE_SIZE bytes from FS_IO_ADDR to a named file on the current disk |
$A084 |
FsFormatDisk |
Zero the current disk's directory sector (no confirmation — caller decides) |
$A087 |
FsSetDisk |
Select current CF disk bank: A = 0–255 → CF_DISK ($030F) |
$A08A |
FsGetDisk |
Get current CF disk bank: A ← CF_DISK |
$A08D |
FsPrintDisk |
Print DISK n + CRLF via Chrout (uses current CF_DISK) |
$A090 |
PrintStr |
Print a NUL-terminated string via Chrout: A=lo, Y=hi (address) |
$A093 |
PrintCRLF |
Print CR+LF via Chrout |
$A096 |
PrintDecU16 |
Print an unsigned 16-bit value as decimal, no leading zeros: A=lo, X=hi |
$A099 |
KBDisable |
Disable both keyboard encoders and wait for them to release the ports (~200 µs). Modifies A, flags |
$A09C |
KBEnable |
Re-enable both keyboard encoders. Modifies A, flags |
Cartridges for this system overlay the ROM area from $C000–$FFFF. When inserted, the cartridge replaces the Monitor, BASIC, Wozmon, and CPU vectors (NMI/RESET/IRQ) with its own code. The Kernal ($A000–$B7FF) and character set ($B800–$BFFF) remain accessible.
Two Kernal facilities support cartridge development:
KernalInit ($A078) — A callable subroutine that performs the complete hardware initialisation sequence (IRQ/BRK/NMI pointers, hardware probing, peripheral init, console auto-detection) and returns via RTS. It clears decimal mode and disables interrupts, but does not reset the stack pointer (the caller must do ldx #$ff / txs before the JSR), enable interrupts (cli), play the beep, display the splash screen, or enter the boot menu. This gives the cartridge full control over what happens after hardware init.
BOOT_VECTOR ($035B–$035C) — A 2-byte RAM address that, if non-zero after KernalInit, causes the normal Reset flow to jump to the specified address instead of continuing to the splash screen and boot menu. KernalInit zeroes this variable, so a cartridge must write to it after calling KernalInit but before Reset checks it — or use Pattern B below.
Pattern A — Direct KernalInit call (cart handles everything after init):
; Cart reset vector points here
CartReset:
ldx #$ff
txs ; Reset stack pointer
jsr $A078 ; KernalInit — all hardware ready, interrupts off
; Override IRQ_PTR ($0300) / NMI_PTR ($0304) if needed
cli
jmp CartMain ; Cart's own program entryThis is the simplest approach. The cartridge gets fully initialised hardware and takes complete control. No beep, no splash — the cart decides what the user sees and hears.
Pattern B — KernalInit + Beep (get the audible startup feedback, then take control):
; Cart reset vector points here
CartReset:
ldx #$ff
txs ; Reset stack pointer
jsr $A078 ; KernalInit — all hardware ready, interrupts off
jsr $A030 ; Beep — audible "system alive" feedback
; Override IRQ_PTR ($0300) / NMI_PTR ($0304) if needed
cli
jmp CartMain ; Cart's own program entryThis is Pattern A with the addition of the startup beep. The beep provides audible confirmation that hardware initialised successfully, which is useful when the cartridge has its own display that may take time to set up.
Note on
BOOT_VECTOR:KernalInitzeroesBOOT_VECTORduring init. A cartridge can write toBOOT_VECTORafter callingKernalInitif it needs to redirect a later soft-reset back to the cart. However, for the initial boot, Patterns A and B above are the recommended approaches.
In practice, Pattern A is recommended for most cartridges.
- Kernal jump table (
$A000–$A0FF) — all entries remain stable across BIOS versions HW_PRESENT($030D) — read afterKernalInitto discover installed hardwareKernalVersion($A07B) — check BIOS compatibility (A=major,X=minor)- RAM vectors —
IRQ_PTR($0300),BRK_PTR($0302),NMI_PTR($0304) can be overwritten to install custom interrupt handlers. A handler that chains to the Kernal's must not leave anything on the stack — see below IO_MODE($0306) — set viaSetIOMode($A00F) to route console output- No-console safe —
KernalInitdoes not halt if neither video nor serial is detected, allowing cartridges with their own display hardware to boot normally
IRQ_PTR can be pointed at your own handler, which then chains to the Kernal's by jumping to the address it replaced. That works, but it carries a constraint nothing else in this README implies:
A chained handler must leave the stack exactly as the CPU left it.
The Kernal's Irq pushes A, Y and X, then decides whether it was entered by BRK or by hardware:
Irq:
pha
phy
phx
tsx
lda $104,x ; the saved P, at a fixed depth past the three pushes
and #$10 ; B flag — set by BRK, clear by a hardware IRQThat $104,x is an absolute offset, not a search. A handler in front of the Kernal's that pushes anything — even one byte it means to pull back after the chain — shifts the read onto the wrong byte, and the Kernal then services a hardware interrupt as a BRK or the reverse.
So a chained handler either touches no register at all (inc, dec and stz on absolute addresses do useful work without one), or saves and restores everything it used before the jmp. The alternative is to replace the vector outright and end in rti, taking on the keyboard and serial servicing yourself.
A template project for creating cartridges for the A.C. Wright 6502 system is available here: https://github.com/acwright/6502-CRT.
The cc65 toolchain provides the assembler and linker needed to build 6502 assembly code.
It must be newer than the 2.19 release, which is still what every package manager ships. The ROM sets .setcpu "W65C02", and cc65 did not gain that CPU until July 2025 — five and a half years after 2.19 — so the packaged toolchain stops on the first directive in BIOS.asm rather than producing a subtly wrong ROM.
macOS (using Homebrew):
brew install --HEAD cc65Linux, and anywhere else the package is 2.19 — from source, which takes about a minute:
git clone https://github.com/cc65/cc65.git
make -C cc65 -j"$(nproc)" bin PREFIX=/usr/local
mkdir -p cc65/lib
make -C cc65 -j"$(nproc)" none PREFIX=/usr/local
sudo make -C cc65/src install PREFIX=/usr/local
sudo make -C cc65/libsrc install PREFIX=/usr/localbin builds the tools; none builds the single target library that cl65 -t none hands to the linker. cc65 has thirty-odd other targets and this ROM is not any of them, so skipping them is what keeps that to about a minute — the mkdir is only there because asking cc65 for one target by name skips the pass that would have created that directory. Both halves get installed because the tools alone are not enough: BASIC.asm uses .macpack longbranch, which is read from cc65's asminc. .github/workflows/ci.yml pins the exact commit CI builds against.
Other platforms: See cc65 documentation
Only required if you plan to program an AT28C256 EEPROM chip:
brew install miniproBuild the ROM image:
makeThis generates:
BIOS.bin- 32KB ROM image ($8000-$FFFF)BIOS.lst- Assembly listing file for debugging
View the generated binary as hex dump:
make viewThe ROM has a regression suite that runs it headless on the A.C. Wright 6502 emulator, covering every Monitor command and every BASIC keyword:
make test # build the ROM and run everything
make test-one T=gosub # just the cases matching /gosub/It needs Node 22 or newer and the emulator's CLI as 6502 on PATH; SIXTY502 points it at a checkout instead, which is how CI runs it. tests/README.md covers writing a case, and every fix to this ROM is expected to arrive with one that fails without it.
To burn the ROM to an AT28C256 EEPROM chip using a TL866 programmer:
make eepromNote: This requires a TL866 (or compatible) programmer and the minipro software.
Remove generated files:
make clean- 6502-ACE — the hardware, and the index of the whole family
- 6502-EMULATOR — runs this ROM on desktop, in a browser, or headless; the regression suite above drives it
- 6502-PRG / 6502-CRT — templates for programs and cartridges, each shipping a
6502.incthat tracks the Kernal API documented above - 6502-ASM / 6502-BAS — example programs and BASIC listings
- cffs — builds CompactFlash images for the filesystem described above
- bastok — tokenizes BASIC listings into
.prgimages - bin2woz — converts a binary into a paste-able upload for the Wozmon at
$FF00 - 6502-DOCS — the documentation site: the guide, the printable reference cards, and the memory-map and character-set references
MIT License — see LICENSE.