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.
You will not type $A000 either, because 6502-VDP.inc gives every slot a name:
.include "6502-VDP.inc"
lda #'!'
jsr Chrout ; the slot at $A000, by nameWhat'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.
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, with a link to the chapter that teaches each group.
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 |
|---|---|
| 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
| 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
Text-mode console: registers, the card's font, palette row 0
| Clobbers | Flags, 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
| Clobbers | Flags, 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 |
|---|---|
| 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), on the screen as shown |
|---|---|
| Clobbers | Flags, 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) |
|---|---|
| Clobbers | Flags |
VideoScroll $A024
Scroll screen up one line
| Clobbers | Flags, 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 |
|---|---|
| Clobbers | Flags, A |
| Notes | the background. Characters already on screen keep their colors. 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 |
|---|---|
| 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 |
| 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 |
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) |
| Clobbers | Flags (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 |
| Clobbers | Flags (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) |
| Clobbers | Flags (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 |
| Clobbers | Flags (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) |
| Clobbers | Flags (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 |
|---|---|
| Clobbers | Flags, A, X, Y |
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); 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 print anything or start BASIC |
KernalVersion $A07B
Get BIOS version (A=major, X=minor)
| Out | A = major version, X = minor version |
|---|---|
| Clobbers | A, X |
The PICOVDP
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 |
|---|---|
| Clobbers | Flags, 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 |
| Clobbers | Flags, 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 |
| Clobbers | Flags, 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 |
| Clobbers | Flags, 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 |
| Clobbers | Flags, 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 |
| Clobbers | Flags, 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)
| Clobbers | Flags, 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 |
| Clobbers | Flags, 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 |
| Clobbers | Flags, 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 |
| Clobbers | Flags, 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 |
| Clobbers | Flags, 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 |
| Clobbers | Flags, 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 |
| Clobbers | Flags, 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 |
📄 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.

