Skip to content

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.

How a Kernal call reaches the routine What jsr Chrout actually does 53 slots, 3 bytes each, from $A000 Your programjsr $A000 The jump tablejmp ChroutDispatch The routinesomewhere in ROM $A000 Chrout $A003 Chrin $A006 WriteBuffer $A009 ReadBuffer The routine moves whenever the ROM is rebuilt. The slot does not — which is the whole point of it. Call the slot, never the address you found the code at.
Two jumps instead of one, and the second one is free of your program. That is the price of never having to look an address up again.

You will not type $A000 either, because 6502.inc gives every slot a name:

asm
.include "6502.inc"

  lda #'!'
  jsr Chrout                    ; the slot at $A000, by name
What'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.

asm
  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 work

Three 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

The chapter →

Chrout $A000

Output char (dispatched by IO_MODE)

In
A = character to output
ClobbersFlags

Chrin $A003

Input char from buffer

ClobbersFlags, 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
ClobbersFlags, 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

ClobbersFlags, X

ReadBuffer $A009

Read byte from input buffer

ClobbersFlags, X, A

BufferSize $A00C

Get buffer count

ClobbersFlags, 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

The chapter →

InitVideo $A015

Initialize TMS9918 (mode registers + character set)

ClobbersFlags, 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

ClobbersFlags, A, X, Y
Notes
Skips silently if no video card is fitted

VideoPutChar $A01B

Write char at cursor

In
A = character to write
ClobbersFlags

VideoChroutRaw $A02A

Output char to video (raw, no control-code handling)

In
A = character code (0-255)
ClobbersFlags
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)
ClobbersFlags, 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)
ClobbersFlags

VideoScroll $A024

Scroll screen up one line

ClobbersFlags, 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)
ClobbersFlags, A
Notes
Skips silently if no video card is fitted

Sound

The chapter →

InitSID $A02D

Initialize SID

ClobbersFlags, A, X

Beep $A030

Play beep tone

ClobbersFlags, 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
ClobbersFlags, 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

ClobbersFlags, 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)
ClobbersFlags, A
Notes
Skips silently if no SID is fitted

Keyboard and sticks

The chapter →

InitKB $A045

Initialize GPIO/VIA keyboard

ClobbersFlags, 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)
ClobbersFlags, 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)
ClobbersFlags, A

KBDisable $A099

Release both encoders and settle; ports free for raw read

ClobbersFlags, 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

ClobbersFlags, A
Notes
Sets CB2 low (enable matrix encoder) and CA2 low (enable PS/2 encoder)

Files

The chapter →

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
ClobbersFlags, 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)
ClobbersFlags, 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
ClobbersFlags, 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
ClobbersFlags, A, X, Y, CF_LBA, CF_BUF_PTR

FsSetDisk $A087

Select current CF disk (A=0-255)

In
A = disk number (0-255)
ClobbersFlags

FsGetDisk $A08A

Get current CF disk (A=disk)

Out
A = current disk number
ClobbersFlags, A

FsPrintDisk $A08D

Print "DISK n" + CRLF via Chrout

ClobbersFlags, A, X, Y, FS_FILE_SIZE, FS_DIR_IDX

The card itself

The chapter →

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
ClobbersFlags, 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
ClobbersFlags, A, X, Y

StWaitReady $A072

Wait CF ready

Out
Carry clear = ready, Carry set = error or timeout
ClobbersFlags, A, X, Y
Notes
Polls ST_STATUS until BSY=0 and RDY=1, with X/Y timeout (~65536 iterations)

Serial

The chapter →

InitSC $A04E

Initialize serial 6551

ClobbersFlags, A

SerialChrout $A051

Direct serial output (bypass IO_MODE)

ClobbersFlags

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
ClobbersFlags, 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
ClobbersFlags, A, X, Y

Clock and lasting memory

The chapter →

RtcReadTime $A05A

Read RTC time

Out
A = hours (binary), X = minutes (binary), Y = seconds (binary)
ClobbersFlags

RtcReadDate $A05D

Read RTC date

Out
A = day of month (binary), X = month (binary), Y = year (binary)
RTC_BUF_CENT = century (binary)
ClobbersFlags

RtcWriteTime $A060

Set RTC time

In
A = hours (binary), X = minutes (binary), Y = seconds (binary)
ClobbersFlags, A, X

RtcWriteDate $A063

Set RTC date

In
A = day of month (binary), X = month (binary), Y = year (binary)
RTC_BUF_CENT = century (binary)
ClobbersFlags, A, X

RtcReadNVRAM $A066

Read NVRAM byte

In
X = NVRAM address ($00-$FF)
Out
A = data byte
ClobbersFlags

RtcWriteNVRAM $A069

Write NVRAM byte

In
X = NVRAM address ($00-$FF), A = data byte
ClobbersFlags

The machine

The chapter →

SysDelay $A075

Delay A=cnt_lo, X=cnt_hi centiseconds

In
A = count low byte, X = count high byte
ClobbersFlags, 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

ClobbersAll 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
ClobbersA, X

Next: hello world — the smallest program that uses any of it.

Written for BIOS v1.5. Released under the MIT License.