Skip to content

The Kernal

The Kernal is the machine's API: 72 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 72 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-VDP.inc gives every slot a name:

asm
.include "6502-VDP.inc"

  lda #'!'
  jsr Chrout                    ; the slot at $A000, by name
What's at the end of the table

13 slots from $A0D8 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, with a link to the chapter that teaches each group.

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
Notes
Every reader comes through here — Chrin, BASIC's line input, INKEY, the break
check, a cartridge — so this is where RTS is lowered again once the buffer has
drained. A reader that skipped it would leave a terminal with RTS/CTS flow
control waiting for good.

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

Text-mode console: registers, the card's font, palette row 0

ClobbersFlags, A, X, Y
Notes
Writes the register table below with the display off, has the card reload its
font into the pattern table at $0800 (VdpLoadFont, a frame at most), restores
palette row 0, sets the border
from VID_PEN and turns the display on. It does not clear the screen: the
name and attribute tables are left as they were and shown unscrolled. A
program that switched modes, moved tables or overwrote the glyphs gets the
console back with this one call.
Skips silently if no video card is fitted

VideoClear $A018

Clear video screen

ClobbersFlags, A, X, Y
Notes
Fills the 960-byte name table at VRAM $0000 with $20 and the 960 attributes at
$0400 with VID_PEN, so COLOR fg,bg : CLS gives a screen of that color, and
takes the scroll origin back to row 0.
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), on the screen as shown
ClobbersFlags, A
Notes
VID_CURSOR_ADDR = ((Y + VID_TOP) mod 24) * 40 + X, the cell's place in the
name table once the scroll origin is folded in
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
Moves the display origin down a row (VID_TOP, and L0SCRY = VID_TOP * 8) and
blanks the row that was at the top, which is now the bottom line, in the
current pen. The name table does not move. The cursor keeps its screen
position, so its VRAM address is worked out again.
Skips silently if no video card is fitted

VideoSetColor $A027

Set the pen and the border (A = fg<<4 | bg)

In
A = fg<<4 | bg. VID_PEN = A, and register 7 = A, so the border follows
ClobbersFlags, A
Notes
the background. Characters already on screen keep their colors.
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
Notes
TIC 00 turns the transmitter off as well as raising RTS, so a character
written while the BIOS is holding RTS up would never leave: the loop below
would spin on a TDRE that cannot come. RTS is therefore dropped for the byte
and put back afterwards if the buffer is still full, and interrupts are held
off meanwhile so that Irq cannot raise it again mid-character.

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

NvStat $A09F

Slot status (X=slot) → A=status, Y=owner ID

In
X = slot (0-15)
Out
A = NV_EMPTY / NV_VALID / NV_BAD, Y = owner ID, C clear
C set on no RTC or bad slot (A, Y undefined)
ClobbersFlags (D and I preserved), A, Y

NvRead $A0A2

Copy a valid slot's payload (X=slot, A/Y=dest lo/hi)

In
X = slot (0-15), A/Y = destination lo/hi
Out
A = status, Y = owner ID; C clear and the buffer written only if A = NV_VALID
C set on no RTC or bad slot (A, Y undefined), or a slot that is not valid (A = its status, Y = its owner ID) — the buffer is untouched
ClobbersFlags (D and I preserved), A, Y, NV_PTR

NvWrite $A0A5

Write a slot (X=slot, A/Y=src lo/hi, NV_ID=owner ID)

In
X = slot (0-15), A/Y = source lo/hi, NV_ID = owner ID ($01-$FF)
Out
C clear; C set on no RTC, bad slot, or NV_ID = 0 (nothing written)
ClobbersFlags (D and I preserved), A, Y, NV_PTR

NvErase $A0A8

Zero all 16 bytes of a slot (X=slot)

In
X = slot (0-15)
Out
C clear; C set on no RTC or bad slot
ClobbersFlags (I preserved), A, Y

NvFind $A0AB

Lowest slot owned by A ($00 = first free) → X

In
A = owner ID
Out
X = slot, C clear; C set if no slot matched (X undefined)
ClobbersFlags (I preserved), A, X, RTC_TMP
Notes
Matches any non-free slot, valid or damaged; A = $00 finds the lowest free slot.

NvFormat $A0AE

Erase all 16 slots

Out
C clear; C set on no RTC
ClobbersFlags, A, X, Y

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); 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 print anything or start BASIC

KernalVersion $A07B

Get BIOS version (A=major, X=minor)

Out
A = major version, X = minor version
ClobbersA, X

The PICOVDP

The chapter →

VdpInfo $A0B1

Card found at boot → A=VDP_FW, X=VDP_CAPS, Y=$AC; carry set if none

Out
A = VDP_FW (STAT5), X = VDP_CAPS (STAT6), Y = VDP_ID ($AC); with no card A = X = Y = 0 and carry set
ClobbersFlags, A, X, Y

VdpWriteReg $A0B4

Write register (A=value, X=0-127), keeping VID_MODE and the LxCTRL shadows

In
A = value, X = register (0-127)
Out
carry set, and nothing written, if no card or X > 127
ClobbersFlags, A
Notes
A VMODE write sets VID_MODE: the mode, with b7 (disturbed) set when it takes
the card to Text or the legacy submode from anything else, so that only
InitVideo can say the Text console is intact. L0CTRL and L1CTRL, which read
back as nothing, are kept in VDP_L0CTRL_SHADOW and VDP_L1CTRL_SHADOW.
Preserves: X, Y

VdpSetMode $A0B7

Write VMODE (A=1-4); carry set if out of range

In
A = mode (1-4)
Out
carry set, and nothing written, if no card or A is out of range
ClobbersFlags, A, X
Notes
Only the register (and VID_MODE, as VdpWriteReg keeps it): the tables, layers
and sprites are the caller's to lay out. The Text console proper is
InitVideo.

VdpPoke $A0BA

Write VRAM byte A at X/Y = address lo/hi (all 64 KB)

In
A = value, X = address low, Y = address high ($0000-$FFFF)
Out
carry set, and nothing written, if no card
ClobbersFlags, A, X

VdpPeek $A0BD

Read VRAM byte at X/Y = address lo/hi → A

In
X = address low, Y = address high ($0000-$FFFF)
Out
A = the byte; carry set, and nothing read, if no card
ClobbersFlags, A, X

VdpSetPalette $A0C0

Palette entry X = 0-255 ← A = $0R, Y = $GB (at $FC00 + 2X)

In
X = entry (0-255), A = $0R, Y = $GB
Out
carry set, and nothing written, if no card
ClobbersFlags, A, X, Y
Notes
The card takes the write into its palette at once (SPEC §11). $FC00 is where
InitVideo puts PALBASE; a program that moves PALBASE writes its own.

WaitVBlank $A0C3

Return at the start of the next vertical blank (STAT0 untouched)

ClobbersFlags, A, X, Y
Notes
Polls STAT3 b0 on port A: waits while the display is in blanking, then until
it is, and puts STATSEL_A back to 0. Not STAT0's F: reading STAT0 clears
F, OVF and COL and the STAT1 latches a program or its interrupt handler may
be relying on, and this leaves them all as they were.
No card: waits 2 cs through SysDelay instead, so a loop paced by it still
runs at about the speed it would, and returns carry set.

VdpLoadFile $A0C6

Load named file (STR_PTR) into VRAM at FS_IO_ADDR, exactly FS_FILE_SIZE bytes

In
STR_PTR = filename, FS_IO_ADDR = VRAM address
Out
Carry clear = loaded, FS_FILE_SIZE = bytes written
Carry set = no video card (nothing read), or not found or read error
ClobbersFlags, A, X, Y, CF_LBA, CF_BUF_PTR, XFER_REMAIN, FS_SECTOR_BUF
Notes
Streams the file through port A to FS_IO_ADDR, anywhere in the 64 KB, and
writes exactly its FS_FILE_SIZE bytes: the rest of the last sector is read
and dropped, so a table loaded here cannot spill into the one after it.

VdpLoadFont $A0C9

Copy built-in font A into L0PAT and wait for it

In
A = font ID
Out
carry set, and nothing written, if no card or A is not $00
ClobbersFlags, A, X, Y
Notes
Writes the font ID to FONT (SPEC §7), which copies it to L0PAT x $800 as that
register stands at the write, and waits for the copy: a pending load lands at
the line start where vertical blank begins, before STAT3 b0 sets, so
WaitVBlank returning is the completion. STAT0 is not read. The only font
STAT6 b7 promises is $00 (VDP_FONT_CP437), so any other ID is refused rather
than written as a command that would do nothing.

VdpSprite $A0CC

Sprite X = 0-63 ← VDP_P0-P3 = Y, X lo, pattern, attributes (table at $2000)

In
X = sprite (0-63), VDP_P0 = Y, VDP_P1 = X bits 7:0, VDP_P2 = pattern,
VDP_P3 = attributes (b7 = X bit 8, b6 priority, b5:4 flips, b3:0 sub-palette; SPEC §10)
Out
carry set, and nothing written, if no card or X > 63
ClobbersFlags, A, Y
Notes
The attribute table is taken to be at VRAM $2000 (SPRATTR = $40), where
BASIC's SCREEN 1-3 puts it; a program that moves SPRATTR writes its sprites
with VdpPoke. Sets VID_MODE b7 (disturbed).
Preserves: X

VdpSetScroll $A0CF

Layer X = 0-1 scroll: A = x lo, Y = y, VDP_P0 = x bit 8

In
X = layer (0-1), A = x bits 7:0, Y = y, VDP_P0 = x bit 8 (0 or not)
Out
carry set, and nothing written, if no card or X > 1
ClobbersFlags, A, X
Notes
Writes LxSCRX, LxSCRY, and LxCTRL with b6 = X bit 8 and its other bits from
the shadow. Sets VID_MODE b7 (disturbed).

VdpLayer $A0D2

Layer X = 0-1 off (A = 0) or on

In
X = layer (0-1), A = 0 to hide, anything else to show
Out
carry set, and nothing written, if no card or X > 1
ClobbersFlags, A, X
Notes
Sets or clears LxCTRL b4, keeping its other bits from the shadow. Sets
VID_MODE b7 (disturbed).

VdpStatus $A0D5

Read status register X = 0-15 on port A → A

In
X = status register (0-15)
Out
A = its value; carry set, and nothing read, if no card or X > 15
ClobbersFlags, A
Notes
Selects STATn with STATSEL_A, reads it, and puts STATSEL_A back to 0 without
reading STAT0. Reading STAT0 itself clears its flags, and STAT1 its
latches, as SPEC §6 says.
Preserves: X, Y

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

Written for BIOS v2.0. Released under the MIT License.