Skip to content

Repository files navigation

6502-BIOS

CI

📖 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.

Overview

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.

Boot Sequence

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:

  1. Clear HW_PRESENT — all bits start at zero
  2. 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)
  3. Conditionally initialise — each subsystem is only initialised if its probe succeeded
  4. Console auto-detection — if video is present, IO_MODE is set to video; if only serial is present, output is routed to the serial port; if neither is found, IO_MODE is left unchanged (no halt — allows cartridges with their own display hardware to boot)
  5. Beep — a short tone on the SID (skipped silently if SID absent; provides audible feedback that the system is alive)
  6. 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
  7. 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)
  8. 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.

  1. 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

Hardware Presence Flags

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)

Graceful Degradation

All hardware-dependent operations are guarded at every level — Kernal, BASIC, and Monitor:

  • CompactFlash absentLOAD, SAVE, DIR, DEL, BLOAD, BSAVE, FORMAT in BASIC print NO DEVICE; Monitor L, S, @ print I/O ERROR; StWaitReady returns 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; Chrin flow control writes are suppressed; XModem LOAD/SAVE return an error
  • GPIO/VIA absentSysDelay falls 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 absentBeep, SOUND, VOL, SidPlayNote, SidSilence, SidSetVolume silently return
  • Video absentCLS, LOCATE, COLOR silently skip (arguments are still consumed); VideoClear, VideoSetCursor and VideoSetColor skip 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 absentTIME, DATE, SETTIME, SETDATE, and NVRAM (write) in BASIC print NO DEVICE; NVRAM() (read) returns 0

BASIC

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. COUNT and COURSE are the same variable (CO), and PRINT COUNT after COUNT = 7 : COURSE = 9 prints 9. A name may not contain a keyword: SCORE will not parse, because it contains OR.

Numeric range: ~±1.7 × 10³⁸, nine significant digits (PRINT 1 / 3 gives .333333333, PRINT SQR(2) gives 1.41421356). Numbers print with a leading-space sign convention (positive numbers prefixed by a space, negative by -). Boolean expressions evaluate to -1 (true) or 0 (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

Program Files (.bas and .prg)

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, RUN and SAVE are all fine. (The C64 works the same way.)
  • Load them with LOAD. The Monitor's L also works, as long as you take its default $0800 address and then X back 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, because BasCmdGosub checks the stack pointer against GOSUB_STACK_MIN before it pushes.

FOR has no such guard, and fails differently. A FOR frame is 18 bytes (TXTPTR, CURLIN, the 5-byte limit, the step sign, the 5-byte step, the variable address, and the $81 tag), so 14 of them fill page 1. BasCmdFor pushes 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 matching NEXT, as ?NEXT WITHOUT FOR ERROR. Fourteen levels of nesting is the working ceiling at the top level of a program, and less inside a GOSUB, 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. MEM and the cold-boot banner report MEMSIZ - VARTAB (free bytes for variables, arrays, and strings combined).

Machine Code Monitor

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 BRK statement
  • Hardware BRK — any BRK opcode 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 with G FF00 or from BASIC with SYS $FF00.

Video

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.

Keyboard

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.

Joystick

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:

  1. KBDisable ($A099) raises CB2/CA2 to tell both encoders to release the ports, then busy-waits ~200 µs so they have time to go high-impedance.
  2. The 6502 reads the raw port directly — GPIO_PORTB for joystick 1, GPIO_PORTA for joystick 2.
  3. KBEnable ($A09C) lowers CB2/CA2 to 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.

CompactFlash Storage

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).

Serial I/O & XModem Transfer

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.

Real-Time Clock

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.

Sound

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).


Memory Map

ROM ($8000–$FFFF, 32KB)

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)

RAM ($0000–$7FFF, 32KB)

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–$07FF is free memory, despite BIOS.inc naming $0400 USER_VARS — a legacy name kept only because Wozmon builds its input buffer on it. The free RAM is $003A–$00FF in 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.


Kernal Jump Table ($A000)

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_MODEA
$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: ACF_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

Cartridge Support

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.

Cart Usage Patterns

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 entry

This 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 entry

This 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: KernalInit zeroes BOOT_VECTOR during init. A cartridge can write to BOOT_VECTOR after calling KernalInit if 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.

What Cartridges Can Rely On

  • Kernal jump table ($A000–$A0FF) — all entries remain stable across BIOS versions
  • HW_PRESENT ($030D) — read after KernalInit to discover installed hardware
  • KernalVersion ($A07B) — check BIOS compatibility (A=major, X=minor)
  • RAM vectorsIRQ_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 via SetIOMode ($A00F) to route console output
  • No-console safeKernalInit does not halt if neither video nor serial is detected, allowing cartridges with their own display hardware to boot normally

Chaining an IRQ Handler

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 IRQ

That $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.


Prerequisites

Install cc65 Toolchain

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 cc65

Linux, 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/local

bin 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

Optional: Install minipro (for EEPROM burning)

Only required if you plan to program an AT28C256 EEPROM chip:

brew install minipro

Building

Build the ROM image:

make

This generates:

  • BIOS.bin - 32KB ROM image ($8000-$FFFF)
  • BIOS.lst - Assembly listing file for debugging

Verification

View the generated binary as hex dump:

make view

Testing

The 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.

Programming EEPROM

To burn the ROM to an AT28C256 EEPROM chip using a TL866 programmer:

make eeprom

Note: This requires a TL866 (or compatible) programmer and the minipro software.

Cleaning Build Artifacts

Remove generated files:

make clean

Related

  • 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.inc that 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 .prg images
  • 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

License

MIT License — see LICENSE.

About

BIOS ROM for the A.C. Wright 6502 project

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages