The Kernal
The Kernal is the machine's API: 53 routines that already know how to talk to every chip on the board. Printing a character, reading a joystick, saving a file, setting the clock — all of it is written, tested, and sitting in ROM.
It is also what BASIC is built on. PRINT ends up in the same routine your program will call.
How it works
The first 256 bytes of the Kernal are nothing but jumps:
$A000 JMP ChroutDispatch
$A003 JMP ChrinImpl
$A006 JMP WriteBufferImpl
...Three bytes each, in a fixed order that has not changed and will not. So jsr $A000 prints a character this year and next year, even though ChroutDispatch itself will have shuffled up or down the ROM in between.
Call the slot, never the implementation. That is the whole contract.
You will not type $A000 either, because 6502.inc gives every slot a name:
.include "6502.inc"
lda #'!'
jsr Chrout ; the slot at $A000, by nameWhat's at the end of the table
32 slots from $A09F to $A0FE are reserved. Each is a real jump to a routine that does nothing but return, so calling one is harmless today and will do something useful in a later ROM. Do not put your own code there — that is what the 30 KB of program RAM is for.
Calling one
Everything is passed in registers, and the pattern is always the same shape: put the arguments in A, X and Y, jsr, read the answer back out of A, X, Y or the carry flag.
lda #<Message ; low byte of the address
ldy #>Message ; high byte
jsr PrintStr ; print until the zero byte
jsr RtcReadTime ; A = hours, X = minutes, Y = seconds
jsr FsLoadFileAddr
bcs Failed ; carry set means it didn't workThree conventions cover nearly all of it:
- Pointers go in A and Y — low byte in A, high byte in Y.
- The carry flag reports success — clear means it worked. Anything that touches the memory card or the serial port answers this way.
- A routine clobbers what it says it clobbers, and nothing else. The tables below list it per routine; when in doubt, push what you care about.
Version numbers are cheap; check them
KernalVersion hands back the major version in A and the minor in X. If your program depends on something a particular ROM added, check it and say something polite rather than crashing on an older machine.
Every routine
Grouped by what it is for. Each chapter listed goes into its group properly — this is the index, not the tutorial.
Console
Chrout $A000
Output char (dispatched by IO_MODE)
| In | A = character to output |
|---|---|
| Clobbers | Flags |
Chrin $A003
Input char from buffer
| Clobbers | Flags, A |
|---|---|
| Notes | On return, carry flag indicates whether a character was available If character available the character will be in the A register |
PrintStr $A090
Print NUL-terminated string (A=lo, Y=hi); clobbers A,Y,STR_PTR
| In | A = string address low, Y = string address high |
|---|---|
| Out | A, Y clobbered; X preserved; clobbers STR_PTR (Chrout preserves it) |
| Notes | through Chrout, so it works for video OR serial). General-purpose; used by BASIC (via the BasPrintStr alias) and available to cartridges. |
PrintCRLF $A093
Print CR+LF
PrintDecU16 $A096
Print unsigned 16-bit decimal (A=lo, X=hi), no leading zeros
| In | A = value low, X = value high |
|---|---|
| Clobbers | Flags, A, X, Y, FS_FILE_SIZE (consumed), FS_DIR_IDX |
| Notes | General-purpose console output; used by BASIC (line numbers) and available to cartridges. Shares the FsPrintSize core below. |
WriteBuffer $A006
Write byte to input buffer
| Clobbers | Flags, X |
|---|
ReadBuffer $A009
Read byte from input buffer
| Clobbers | Flags, X, A |
|---|
BufferSize $A00C
Get buffer count
| Clobbers | Flags, A |
|---|
SetIOMode $A00F
Set IO_MODE
| In | A = mode (bit 0: 0=video, 1=serial) |
|---|
GetIOMode $A012
Get IO_MODE
| Out | A = current IO_MODE |
|---|
The screen
InitVideo $A015
Initialize TMS9918 (mode registers + character set)
| Clobbers | Flags, A, X, Y |
|---|---|
| Notes | Writes the eight mode registers (text mode, 40x24) and reloads the character set into the pattern table at $0800, so a program that overwrote the glyphs can fully restore text mode with a single call. |
VideoClear $A018
Clear video screen
| Clobbers | Flags, A, X, Y |
|---|---|
| Notes | Skips silently if no video card is fitted |
VideoPutChar $A01B
Write char at cursor
| In | A = character to write |
|---|---|
| Clobbers | Flags |
VideoChroutRaw $A02A
Output char to video (raw, no control-code handling)
| In | A = character code (0-255) |
|---|---|
| Clobbers | Flags |
| Notes | Always writes the character glyph at the cursor position and advances. Preserves: A, X, Y |
VideoSetCursor $A01E
Set cursor (X=col, Y=row)
| In | X = column (0-39), Y = row (0-23) |
|---|---|
| Clobbers | Flags, A |
| Notes | Calculates VRAM address = Y * 40 + X and stores in VID_CURSOR_ADDR Skips silently if no video card is fitted |
VideoGetCursor $A021
Get cursor position
| Out | X = column (0-39), Y = row (0-23) |
|---|---|
| Clobbers | Flags |
VideoScroll $A024
Scroll screen up one line
| Clobbers | Flags, A, X, Y |
|---|---|
| Notes | Copies VRAM rows 1-23 to rows 0-22 (920 bytes), clears row 23 with spaces Uses SCROLL_BUF ($0320, 40 bytes) as temporary storage |
VideoSetColor $A027
Set TMS9918 text color (A=reg7 byte: hi=fg, lo=bg)
| In | A = color byte (high nibble = fg color, low nibble = bg color) |
|---|---|
| Clobbers | Flags, A |
| Notes | Skips silently if no video card is fitted |
Sound
InitSID $A02D
Initialize SID
| Clobbers | Flags, A, X |
|---|
Beep $A030
Play beep tone
| Clobbers | Flags, A, X, Y |
|---|---|
| Notes | Uses SidPlayNote on voice 0 with ~475 Hz tone, then silences Skips silently if SID is absent |
SidPlayNote $A033
Play note (A=voice, X=freqLo, Y=freqHi)
| In | A = voice (0-2), X = frequency low byte, Y = frequency high byte |
|---|---|
| Clobbers | Flags, A |
| Notes | Uses triangle waveform with standard ADSR (Attack=0, Decay=9, Sustain=A, Release=2) Skips silently if no SID is fitted |
SidSilence $A036
Silence all voices
| Clobbers | Flags, A |
|---|---|
| Notes | Gates off all voices, letting the release phase of the envelope ring out. The frequency registers are deliberately left alone. Zeroing them stops the oscillator dead, which freezes the waveform at whatever level it had reached and leaves the envelope to decay a DC offset instead of a tone — an audible thump at the end of every note. Gate off is all the SID needs; the envelope takes the voice to zero on its own. Skips silently if no SID is fitted |
SidSetVolume $A039
Set SID master volume (A=0-15)
| In | A = volume (0-15); upper nibble of SID_MODE_VOL is cleared (no filter) |
|---|---|
| Clobbers | Flags, A |
| Notes | Skips silently if no SID is fitted |
Keyboard and sticks
InitKB $A045
Initialize GPIO/VIA keyboard
| Clobbers | Flags, A |
|---|---|
| Notes | Configures Port B (matrix) and Port A (PS/2) as inputs CB2 low (enable matrix encoder), CA2 low (enable PS/2 encoder) CB1 and CA1 falling-edge IRQs enabled |
ReadJoystick1 $A048
Read joystick 1
| Out | A = joystick bitmask (active-low bits: R-L-D-U-Y-X-B-A) |
|---|---|
| Clobbers | Flags, A |
| Notes | Disables both encoders, waits for release, then reads the raw port directly — the same way a C64 reads a CIA port. No sei/PCR save-restore is needed: the port is static while the encoders are off and no interrupt handler touches these ports. |
ReadJoystick2 $A04B
Read joystick 2
| Out | A = joystick bitmask (active-low bits: R-L-D-U-Y-X-B-A) |
|---|---|
| Clobbers | Flags, A |
KBDisable $A099
Release both encoders and settle; ports free for raw read
| Clobbers | Flags, A |
|---|---|
| Notes | Sets CB2/CA2 high, then busy-waits so the encoder firmware has time to let go of both ports before the caller reads them. Self-contained cycle loop — deliberately not SysDelay or the VIA T1 path, so it is safe to call while the caller owns the timers. |
KBEnable $A09C
Re-enable both encoders
| Clobbers | Flags, A |
|---|---|
| Notes | Sets CB2 low (enable matrix encoder) and CA2 low (enable PS/2 encoder) |
Files
FsLoadFileAddr $A07E
Load named file to FS_IO_ADDR
| In | STR_PTR = filename, FS_IO_ADDR = destination address |
|---|
FsSaveFileAddr $A081
Save FS_FILE_SIZE bytes from FS_IO_ADDR to named file
| In | STR_PTR = filename, FS_IO_ADDR = source address, FS_FILE_SIZE = byte count |
|---|
FsLoadFile $A03C
Load file from CF
| In | STR_PTR ($02-$03) points to null-terminated filename |
|---|---|
| Out | Carry clear = success, FS_FILE_SIZE = bytes loaded Carry set = file not found or read error |
| Clobbers | Flags, A, X, Y, CF_LBA, CF_BUF_PTR |
FsSaveFile $A03F
Save file to CF
| In | STR_PTR ($02-$03) points to null-terminated filename FS_FILE_SIZE ($034A-$034B) = number of bytes to save |
|---|---|
| Out | Carry clear = success, Carry set = error (directory full or write error) |
| Clobbers | Flags, A, X, Y, CF_LBA, CF_BUF_PTR |
FsDeleteFile $A042
Delete file from CF
| In | STR_PTR ($02-$03) points to null-terminated filename |
|---|---|
| Out | Carry clear = success, Carry set = file not found or error |
| Clobbers | Flags, A, X, Y, CF_LBA, CF_BUF_PTR |
FsFormatDisk $A084
Zero the current disk's directory sector
| Out | Carry clear = success, Carry set = write error |
|---|---|
| Clobbers | Flags, A, X, Y, CF_LBA, CF_BUF_PTR |
FsSetDisk $A087
Select current CF disk (A=0-255)
| In | A = disk number (0-255) |
|---|---|
| Clobbers | Flags |
FsGetDisk $A08A
Get current CF disk (A=disk)
| Out | A = current disk number |
|---|---|
| Clobbers | Flags, A |
FsPrintDisk $A08D
Print "DISK n" + CRLF via Chrout
| Clobbers | Flags, A, X, Y, FS_FILE_SIZE, FS_DIR_IDX |
|---|
The card itself
StReadSector $A06C
Read CF sector
| In | CF_LBA ($26-$29) = LBA address, CF_BUF_PTR ($24-$25) = destination pointer |
|---|---|
| Out | Carry clear = success, Carry set = error CF_BUF_PTR advanced by 512 bytes on success |
| Clobbers | Flags, A, X, Y |
StWriteSector $A06F
Write CF sector
| In | CF_LBA ($26-$29) = LBA address, CF_BUF_PTR ($24-$25) = source pointer |
|---|---|
| Out | Carry clear = success, Carry set = error CF_BUF_PTR advanced by 512 bytes on success |
| Clobbers | Flags, A, X, Y |
StWaitReady $A072
Wait CF ready
| Out | Carry clear = ready, Carry set = error or timeout |
|---|---|
| Clobbers | Flags, A, X, Y |
| Notes | Polls ST_STATUS until BSY=0 and RDY=1, with X/Y timeout (~65536 iterations) |
Serial
InitSC $A04E
Initialize serial 6551
| Clobbers | Flags, A |
|---|
SerialChrout $A051
Direct serial output (bypass IO_MODE)
| Clobbers | Flags |
|---|
XModemLoad $A054
Receive binary via XModem
| In | XFER_PTR = destination address (set by caller) |
|---|---|
| Out | Carry clear = success, XFER_PTR past last byte written XFER_REMAIN = total bytes received Carry set = transfer failed |
| Clobbers | Flags, A, X, Y |
XModemSave $A057
Send binary via XModem
| In | XFER_PTR = source address, XFER_REMAIN = byte count (set by caller) |
|---|---|
| Out | Carry clear = success, Carry set = transfer failed |
| Clobbers | Flags, A, X, Y |
Clock and lasting memory
RtcReadTime $A05A
Read RTC time
| Out | A = hours (binary), X = minutes (binary), Y = seconds (binary) |
|---|---|
| Clobbers | Flags |
RtcReadDate $A05D
Read RTC date
| Out | A = day of month (binary), X = month (binary), Y = year (binary) RTC_BUF_CENT = century (binary) |
|---|---|
| Clobbers | Flags |
RtcWriteTime $A060
Set RTC time
| In | A = hours (binary), X = minutes (binary), Y = seconds (binary) |
|---|---|
| Clobbers | Flags, A, X |
RtcWriteDate $A063
Set RTC date
| In | A = day of month (binary), X = month (binary), Y = year (binary) RTC_BUF_CENT = century (binary) |
|---|---|
| Clobbers | Flags, A, X |
RtcReadNVRAM $A066
Read NVRAM byte
| In | X = NVRAM address ($00-$FF) |
|---|---|
| Out | A = data byte |
| Clobbers | Flags |
RtcWriteNVRAM $A069
Write NVRAM byte
| In | X = NVRAM address ($00-$FF), A = data byte |
|---|---|
| Clobbers | Flags |
The machine
SysDelay $A075
Delay A=cnt_lo, X=cnt_hi centiseconds
| In | A = count low byte, X = count high byte |
|---|---|
| Clobbers | Flags, A, X, Y (X/Y clobbered only in software-fallback path) |
| Notes | Uses VIA T1 in one-shot mode. 9999 cycles @ 1MHz = ~10ms per tick. |
KernalInit $A078
Initialize all hardware (caller must reset SP; no cli, no splash); rts when done
| Clobbers | All registers, flags |
|---|---|
| Notes | Sets HW_PRESENT, IO_MODE, IRQ/BRK/NMI pointers, BOOT_VECTOR=0 Does NOT enable interrupts (caller must cli) Does NOT reset the stack pointer (caller should do this before JSR) Does NOT display splash or enter boot menu |
KernalVersion $A07B
Get BIOS version (A=major, X=minor)
| Out | A = major version, X = minor version |
|---|---|
| Clobbers | A, X |
📄 Kernal Jump Table card — every slot with its registers, grouped the same way, on two printable pages.
Next: hello world — the smallest program that uses any of it.

