Oct 4, 2026 · Updated Oct 11, 2026 · @Craig Taylor
The MEGA65's ROM holds the KERNAL, the screen editor, BASIC 65, the DOS, the monitor, the character sets, and a complete set of C64 ROMs for C64 mode, in a 128KB image. Unlike the C64's, it is still changing: new releases appear regularly, fixing bugs and adding features.
That makes the question "which parts can I call?" more important than
it was on the C64. On a C64, programmers called almost any ROM routine
they found, because the ROM never changed. On the MEGA65, a routine that
is not part of an official interface may move in the next release. The
MEGA65 team treats the KERNAL jump table as officially supported, along
with certain documented facts about the memory layout such as
$2001 being the start of BASIC (Dan
Sanderson, MEGA65 Digest). Almost everything else is internal.
This chapter therefore concentrates on the stable interfaces:
There is no complete map of BASIC 65's internal routines here, of the kind West gave for the C64's BASIC. Such a map would be out of date within months, and code built on it would break with it. Where earlier chapters needed BASIC internals, such as the USR function's conversion routines, the safest course is to check the documentation for the ROM version in use.
The ROM is a single 128KB image, loaded from the SD card into chip
RAM at $20000-$3FFFF and write-protected, as Chapter 5
described. Because it is an ordinary file, the installed version is
simply whichever image the SD card holds, and different machines may run
different versions.
The ROM is a 128KB image that the MEGA65 holds at
$20000-$3FFFF, in banks 2 and 3. Its arrangement comes from
the C65's. The C65 brings its system ROM into the CPU's window using
both banking mechanisms: $D030 for some regions, and MAP
for the rest (MEGA65 ROM
issue #62). ROM 920422 is laid out as follows, according to the
ROM's build files:
| ROM address | Contents |
|---|---|
$20000-$23FFF |
DOS |
$24000-$28FFF |
BASIC 65 (first part) |
$29000-$29FFF |
Character set A |
$2A000-$2BFFF |
C64-mode BASIC |
$2C000-$2CFFF |
DOS and KERNAL code |
$2D000-$2DFFF |
Character set C |
$2E000-$2FFFF |
End of C64-mode BASIC, then the C64-mode KERNAL |
$30000-$31FFF |
Machine-language monitor |
$32000-$3BFFF |
BASIC 65 |
$3C000-$3CFFF |
Unused |
$3D000-$3DFFF |
Character set B |
$3E000-$3EFFF |
Screen editor |
$3F000-$3FFFF |
KERNAL, including the jump table and hardware vectors |
Treat this table as orientation only. The ROM's internal arrangement is not an interface, MEGA65 releases have reorganised it, and code should reach the ROM only through the jump table and vectors described in the following sections.
MEGA65 ROM versions are six-digit numbers such as 920422, shown in the start-up banner. Release and beta ROMs are numbered 92xxxx and are meant to stay compatible with existing software. Development ROMs, numbered 99xxxx, are built separately and may include experimental features that break existing software. The two series run side by side; most owners should run a 92xxxx ROM (MEGA65 ROM source, README; owners can request access through the MEGA65 ROM issue repository).
The ROM version matters most to programs that depend on behaviour added in a particular release, such as the BASIC 65 features dated in Chapter 6. Such programs should say which version they need. Programs that keep to the interfaces in this chapter rarely need to check at all.
As on every Commodore machine since the C64, the byte at
$FF80 is set aside for a KERNAL version number (pagetable.com). On the MEGA65
it is always $FF, the C65's "not released" value, so it
says nothing about the ROM version (MEGA65 ROM source, jump table).
From ROM 920417, the KERNAL's VERSIONQ routine at $FF2F
returns the six-digit ROM version shown in the banner (see Extended entries). It loads the version
into Q, the 32-bit register made up of A, X, Y and Z, as a little-endian
binary integer: 920422 is $000E0B66. A single STQ stores it
in four bytes:
jsr $ff2f ; VERSIONQ
stq version ; four bytes, low byte first
On a ROM older than 920417, $FF2F is not VERSIONQ, and
calling it is likely to crash the program. At the time of writing,
920417 and later are beta releases; the latest stable ROM is 920413
(MEGA65 ROM change log).
The version is also stored as text. The ROM image, which the MEGA65
holds at $20000, has the letter V followed by the six ASCII
digits at $20016-$2001C, for example
V920422. Reading this string is discouraged: its address is
not part of the ROM's supported interface, and it may not work with
future ROM versions. A program that must also run on ROMs older than
920417 can fall back on it, accepting that risk.
The jump table is a list of JMP instructions at the top of the KERNAL, each at a fixed address. Programs call the fixed address, and the JMP there leads to wherever the routine currently lives. When a new ROM moves a routine, only the JMP changes.
The MEGA65-mode KERNAL derives from the C65's, which in turn derives from the C128's. Its table has two parts: the standard entries shared with the C64, and an extended block below them inherited from the C128 by way of the C65, to which the MEGA65 has added entries of its own.
These 39 entries occupy the same addresses as on the C64, and most do the same jobs (pagetable.com). Four addresses hold different routines on the MEGA65: KEY, MONEXIT, ScanStopKey and SCRORG take the places of the C64's SCNKEY, SETTMO, UDTIM and SCREEN. MEMTOP, MEMBOT and IOBASE are reserved. Some routines that keep their C64 names take different arguments: the clock routines work in BCD hours, minutes, seconds and tenths. PLOT keeps the C64's registers, with the line in X and the column in Y, but measures them from the top-left corner of the current window rather than of the screen. The inputs and outputs in the table come from the MEGA65 Book's KERNAL Jump Table appendix, checked against the ROM source. Where they disagree, as for PLOT, the table follows the ROM.
X/Y means a 16-bit value with the low byte in X and the high byte
in Y. C is the carry flag: routines that can fail return with C=1 and an
error code in A. A dash means the routine takes no input or returns no
result. Unless the table says otherwise, assume a KERNAL routine changes
every register and flag. All of them expect the KERNAL's normal memory
map, with the base page register B set to $00.
| Address | Name | Purpose | Input | Output |
|---|---|---|---|---|
$FF81 |
CINT | Initialise screen editor and video | — (IRQs disabled) | — |
$FF84 |
IOINIT | Initialise I/O devices and the timer interrupt | — (IRQs disabled) | — |
$FF87 |
RAMTAS | Test and initialise RAM | — | — |
$FF8A |
RESTOR | Restore the default KERNAL vectors | — (IRQs disabled) | — |
$FF8D |
VECTOR | Read or set the KERNAL vectors | C=1 to read, with X/Y = destination; C=0 to set, with X/Y = source. The table is 56 bytes (32 before ROM 920400) | — |
$FF90 |
SETMSG | Enable or disable KERNAL messages | A = message flags (bit 7 = control messages, bit 6 = error messages) | — |
$FF93 |
SECOND | Send a secondary address after LISTEN | A = secondary address | — |
$FF96 |
TKSA | Send a secondary address after TALK | A = secondary address | — |
$FF99 |
MEMTOP | Reserved, do not use | — | — |
$FF9C |
MEMBOT | Reserved, do not use | — | — |
$FF9F |
KEY | Handle special keys (Mega+Shift, No Scroll, Ctrl-S); called by the IRQ handler | — | — |
$FFA2 |
MONEXIT | The monitor’s exit to BASIC; not for use by programs | — | — |
$FFA5 |
ACPTR | Read a byte from the serial bus | — | A = byte received |
$FFA8 |
CIOUT | Send a byte to the serial bus | A = byte to send | — |
$FFAB |
UNTLK | Send UNTALK | — | — |
$FFAE |
UNLSN | Send UNLISTEN | — | — |
$FFB1 |
LISTEN | Send LISTEN | A = device number (0–31) | — |
$FFB4 |
TALK | Send TALK | A = device number (0–31) | — |
$FFB7 |
READST | Read the I/O status byte | — | A = status byte |
$FFBA |
SETLFS | Set logical file, device and secondary address | A = logical file, X = device, Y = secondary address ($FF for none) | — |
$FFBD |
SETNAM | Set the file name | A = name length (0 for none), X/Y = address of name (bank set by SETBNK) | — |
$FFC0 |
OPEN | Open a logical file | — (call SETBNK, SETLFS and SETNAM first) | C=1 on error, A = error code |
$FFC3 |
CLOSE | Close a logical file | A = logical file; C=1 to close without sending a close command | C=1 on error, A = error code |
$FFC6 |
CHKIN | Set the input channel | X = logical file | C=1 on error, A = error code |
$FFC9 |
CHKOUT | Set the output channel | X = logical file | C=1 on error, A = error code |
$FFCC |
CLRCHN | Restore default channels | — | — |
$FFCF |
BASIN | Input a character (CHRIN) | — | C=0: A = byte read; C=1: A = error code |
$FFD2 |
BSOUT | Output a character (CHROUT) | A = byte to write | C=1 on error, A = error code |
$FFD5 |
LOAD | Load or verify a file | A = 0 to load, 1 to verify (add $40 for raw mode); X/Y = load address (bank set by SETBNK) | C=1 on error, A = error code; X/Y = end address |
$FFD8 |
SAVE | Save memory to a file | A = base-page address of a pointer to the start; X/Y = end address + 1 | C=1 on error, A = error code |
$FFDB |
SETTIM | Set the system clock | Y = hours (0–23), X = minutes, A = seconds, all in BCD; Z = tenths (0–9) | — |
$FFDE |
RDTIM | Read the system clock | — | Y = hours (0–23), X = minutes, A = seconds, all in BCD; Z = tenths (0–9) |
$FFE1 |
STOP | Test the STOP key | — | Z=1 if STOP is pressed |
$FFE4 |
GETIN | Get a character from the keyboard buffer | — | A = character; C=1 on error |
$FFE7 |
CLALL | Close all files | — | — |
$FFEA |
ScanStopKey | Test the STOP key without closing channels or clearing the keyboard buffer | — | A = $7F and N=0 if STOP is pressed; A = $FF and N=1 if not |
$FFED |
SCRORG | Return the size and position of the screen window | — | X = window width, Y = window height; A/Z = screen-memory address of the window’s top-left corner (low/high); C=0 for 80 columns, C=1 for 40 |
$FFF0 |
PLOT | Read or set the cursor position | C=1 to read; C=0 to set, with X = line, Y = column, relative to the window origin | X = line, Y = column, relative to the window origin (when read); C=1 on error (when set) |
$FFF3 |
IOBASE | Reserved, do not use; on the C65 it always returns $D000 | — | — |
SCRORG is worth using on the MEGA65, where the text screen may be 40 or 80 columns wide and 25 or 50 rows high, and the WINDOW command can shrink the area the editor uses. A program that asks SCRORG for the window's size and position works in every mode.
The C65 kept most of the C128's additions, dropped three that made
sense only on a C128 (DMA_CALL, DLCHR and GETCFG), and added
MONITOR_CALL. The removals moved many entries to new addresses, so they
do not match the C128 (pagetable.com). The MEGA65
has extended the table downwards: the entries from $FF2F to
$FF4A are its own additions and are not on the C65. They
arrived between ROM 920398 and 920417, as the table notes, so a program
that uses one should check the ROM version first; KEYLOCKS was renamed
SYSFLAGS in ROM 920418 (MEGA65 ROM change log).
| Address | Name | Purpose | Input | Output |
|---|---|---|---|---|
$FF2F |
VERSIONQ | Load the ROM version number into Q (ROM 920417 and later) | — | Q (A, X, Y, Z) = ROM version, as a 32-bit little-endian integer |
$FF32 |
RESET_RUN | Reset the computer, optionally running the program in memory (ROM 920416 and later) | A = 0 for a KERNAL warm boot, 1 for a Hypervisor reset, 2 for a warm boot that then RUNs the BASIC program at $2001 | Does not return |
$FF35 |
CURSOR | Enable or disable the blinking cursor (ROM 920415 and later) | C=0 to enable, C=1 to disable | — |
$FF38 |
— | Reserved, do not use | — | — |
$FF3B |
SAVEFL | Save to a file, with flags (ROM 920412 and later) | As SAVE, plus Z = flags (bit 6 = raw mode: omit the two-byte address header) | C=1 on error, A = error code |
$FF3E |
— | Reserved, do not use | — | — |
$FF41 |
GETIO | Read the current input and output devices (ROM 920398 and later) | — | X = input device (0 = keyboard), Y = output device (3 = screen) |
$FF44 |
GETLFS | Read the current logical file, device and secondary address (ROM 920398 and later) | — | A = logical file, X = device, Y = secondary address ($FF for none) |
$FF47 |
SYSFLAGS | Read or set the system flags (keyboard and mode locks) (ROM 920398 and later) | C=1 to read; C=0 to set, with A = flags (bit 4 = GO64 locked, bit 5 = raw function keys, bit 6 = No Scroll locked, bit 7 = Mega+Shift locked; preserve bits 0–3) | A = flags (when read) |
$FF4A |
ADDKEY | Add a character to the keyboard buffer (ROM 920398 and later) | A = PETSCII code | C=1 if the buffer is full |
$FF4D |
SPIN_SPOUT | Set up fast serial for I/O | C=0 to set up for input (SPINP), C=1 for output (SPOUT) | — |
$FF50 |
CLOSE_ALL | Close all files on a device | A = device number (0–31) | — |
$FF53 |
C64MODE | Switch to C64 mode | — (call with JMP) | Does not return |
$FF56 |
MONITOR_CALL | Enter the machine-language monitor | — (call with JMP) | Does not return |
$FF59 |
BOOT_CALL | Boot a program from disk | — (call with JMP) | Does not return |
$FF5C |
PHOENIX | Initialise function cartridges | — (call with JMP) | Does not return |
$FF5F |
LKUPLA | Find a free logical file number | A = logical file | C=0 if found: A = logical file, X = device, Y = secondary address ($FF for none); C=1 if not found |
$FF62 |
LKUPSA | Find a free secondary address | Y = secondary address | C=0 if found: A = logical file, X = device, Y = secondary address ($FF for none); C=1 if not found |
$FF65 |
SWAPPER | Switch screen width | — | — |
$FF68 |
PFKEY | Program a function key | A = base-page address of a pointer to the string (low, high, bank); X = key number (1–16); Y = string length | C=1 if the update failed |
$FF6B |
SETBNK | Set the banks for LOAD and SAVE data and the file name | Banks 0–5: A = data bank, X = file name bank. 28-bit addresses: A = $80 + address bits 24–27, Y = bits 16–23; X = $80 + file name bits 24–27, Z = bits 16–23 | — |
$FF6E |
JSRFAR | Call a routine in another bank | $02 = bank (0–5), $03 = address high, $04 = address low; $05–$09 = status, A, X, Y and Z to pass to the routine. In banks 1–5 the target must lie in $2000–$7FFF | Status, A, X, Y and Z as the routine left them |
$FF71 |
JMPFAR | Jump to a routine in another bank | As JSRFAR (call with JMP) | Does not return |
$FF74 |
LDA_FAR | Load a byte from another bank | X = base-page address of a pointer, Y = index, Z = bank | A = byte read |
$FF77 |
STA_FAR | Store a byte in another bank | A = byte to store; X = base-page address of a pointer, Y = index, Z = bank | — |
$FF7A |
CMP_FAR | Compare A with a byte in another bank | A = byte to compare; X = base-page address of a pointer, Y = index, Z = bank | Flags set as by CMP |
$FF7D |
PRIMM | Print the zero-terminated string that follows the JSR | A zero-terminated string of up to 255 bytes, including the zero, after the JSR | All registers preserved |
The last six are not among the entries the C65 moved, so they keep
their C128 addresses. PRIMM at $FF7D is confirmed on the
MEGA65 (MEGA65
Digest). The far-memory calls do not take the same arguments as on
the C128: LDA_FAR, STA_FAR and CMP_FAR take the bank in the Z register,
and JSRFAR and JMPFAR take their target and register values from base
page locations $02-$09. The inputs and outputs
come from the MEGA65 Book's KERNAL Jump Table appendix.
The far-call routines don't run from ROM. They exist to change the
memory map, and code running in the KERNAL at $E000-$FFFF
could map itself out halfway through. So the KERNAL's start-up routine
copies them into bank 0 RAM from $0380, below
$2000, where they stay visible whatever the mapping, and
the jump-table entries at $FF6E (JSRFAR) and
$FF71 (JMPFAR) simply jump to those copies. Disassembling
$FF6E therefore shows a JMP into page 3 (MEGA65 ROM source,
install and the RAM-copied far-call block).
Always call them through the jump table, never at their RAM
addresses. The jump-table addresses are the ROM's promise; where the
copies land is an internal detail, decided by the size of the code ahead
of them, and it changes when that code changes. It has already happened
between two released ROMs. For SYS TO, ROM 920414 added a MAP-offset
version of JSRFAR, which BASIC reaches through the reserved entry
$FF38, and four bytes of code to support it. On the stable
ROM 920413, JMPFAR's copy starts at $03BB; on 920414 and
later it starts at $03BF, and the MAP-offset entry at
$0397 does not exist at all (MEGA65 ROM change log; MEGA65 ROM source). A
program that called $03BB directly would work on the stable
ROM and jump into the middle of an instruction on every beta since, and
the reverse holds for a program built against a beta. Calling
$FF71 works on both. As with every jump-table call, the
KERNAL ROM must be mapped at $E000-$FFFF to reach the
entry.
PRIMM reads the bytes after the JSR as a string, prints them, and returns to the instruction after the terminating zero:
jsr $ff7d ; PRIMM
.text "HELLO, MEGA65", 13, 0
rts ; execution continues here
It is the shortest way to print a fixed message from machine language, and it needs no pointer setup.
The jump table lets programs call the KERNAL. The RAM vectors let programs change what the KERNAL does. Each is a two-byte address in RAM that the KERNAL jumps through at a key moment, such as handling an interrupt or outputting a character. Pointing a vector at your own routine inserts that routine into the KERNAL's work.
The C65's vector table at $0314-$0333 follows the C64
and C128 layout, with the vector the C64 left unused now serving the
monitor (pagetable.com).
The MEGA65 KERNAL inherits it from the C65.
| Address | Name | Jumped through for |
|---|---|---|
$0314 |
CINV | IRQ handler |
$0316 |
CBINV | BRK handler |
$0318 |
NMINV | NMI handler |
$031A |
IOPEN | OPEN |
$031C |
ICLOSE | CLOSE |
$031E |
ICHKIN | CHKIN |
$0320 |
ICKOUT | CHKOUT |
$0322 |
ICLRCH | CLRCHN |
$0324 |
IBASIN | BASIN |
$0326 |
IBSOUT | BSOUT |
$0328 |
ISTOP | STOP |
$032A |
IGETIN | GETIN |
$032C |
ICLALL | CLALL |
$032E |
EXMON | Monitor command input |
$0330 |
ILOAD | LOAD |
$0332 |
ISAVE | SAVE |
The C65 followed this table with two more groups of two-byte
vectors. From $0334 to $0343 are the low-level
serial bus routines, in the order TALK, LISTEN, TKSA, SECOND, ACPTR,
CIOUT, UNTLK and UNLSN. From $0344 to $034B
are four screen editor vectors: CTLVEC, SHFVEC and ESCVEC, through which
the editor handles control, shifted and escape characters, and KEYSCAN,
which is called after a key is read and before it is buffered.
The VECTOR call at $FF8D copies the vectors to or from
memory, and RESTOR at $FF8A resets the KERNAL's own vectors
to the defaults. From ROM 920400, VECTOR copies all three groups, a
56-byte table from $0314 to $034B; earlier
ROMs copied only the first 32 bytes. Give VECTOR room for 56 bytes
(MEGA65
ROM change log). Using VECTOR to save the table before changing it,
and to put it back afterwards, is safer than editing individual
entries.
A cooperative program that wants code to run on every system interrupt, such as a background music player, points CINV at its own routine and ends that routine by jumping to the old handler, so that keyboard scanning and the clock keep working:
install sei
lda $0314
sta oldirq
lda $0315
sta oldirq+1
lda #<myirq
sta $0314
lda #>myirq
sta $0315
cli
rts
myirq inc $d020 ; the routine's own work
jmp (oldirq) ; continue with the KERNAL handler
oldirq .word 0
Before jumping through CINV, the KERNAL saves A, X, Y and Z and the
base page register B, then sets B to $00. A handler that
ends by jumping on to the KERNAL's handler, as above, can therefore use
all of them freely: the KERNAL restores them before its RTI. Changing the vector inside SEI and CLI
prevents an interrupt from arriving when only one byte has been
written.
Separate from the RAM vectors are the CPU's own vectors at the top of
the KERNAL ROM: NMI at $FFFA, RESET at $FFFC
and IRQ/BRK at $FFFE. They point into the KERNAL; for NMI
and IRQ, it then jumps through the RAM vectors. A takeover program that switches the
KERNAL out replaces them with its own, as shown in Chapter 8.
BASIC 65 has its own vectors, which belong to BASIC rather than the
KERNAL. The one programs most often need is the USR vector, two bytes at
$02F8, low byte first, read in the system bank:
BANK 128 : WPOKE $02F8, $1800 points USR at a routine at
$1800 (MEGA65 Book, p. B-285). Chapter 9 describes writing
USR routines. Other BASIC vectors and work areas are internal and should
not be assumed from the C64 or C128.
The Hypervisor's services, documented by the MEGA65 Book for Hyppo
version 1.2, are reached through trap registers at
$D640-$D67F. The service number goes in A, other inputs in
X, Y and Z, and the write is followed by CLV, because the CPU may or may
not execute the byte after the trap when the service returns. On return,
carry set means success; carry clear means failure, with an error code
in A. Registers not used for results are preserved, and the trap
registers exist only in the MEGA65 I/O personalities, the normal one
and the Ethernet one (MEGA65 Book, pp. N-4, N-5; mega65-core, gs4510.vhdl).
Most services use $D640. A few use other trap registers:
$D641 for ROM write-protection, $D642 for
configuration and freeze-slot services, $D643 for serial
monitor output, and $D67F for a program to freeze itself
(MEGA65 Book, Appendix N).
$D640The table lists the services the Book documents. "Not in 1.2" marks
services the Book defines but describes as not implemented in Hyppo 1.2;
they return error $82 there.
The Input column lists the registers a service reads besides A, and
anything that must be set up first. A page is the high byte of a
page-aligned address between $0000 and $7E00.
The Output column lists results besides the carry flag. "Not documented"
means the Book gives no details for a service it does not
implement.
| A | Service | Status in 1.2 | Purpose | Input | Output |
|---|---|---|---|---|---|
$00 |
getversion | Yes | Hyppo version in A.X, HDOS version in Y.Z | — | A.X = Hyppo version, Y.Z = HDOS version (major.minor) |
$02 |
getdefaultdrive | Yes | SD card partition selected at boot | — | A = drive number |
$04 |
getcurrentdrive | Yes | Current drive | — | A = drive number |
$06 |
selectdrive | Yes | Select a drive (SD card partition) | X = drive number | — |
$08 |
getdrivesize | Not in 1.2 | Size of a drive | Not documented | Not documented |
$0A |
getcwd | Not in 1.2 | Current directory | Not documented | Not documented |
$0C |
chdir | Yes | Change directory | — (find the directory’s entry first with findfile, findfirst, findnext or readdir) | — |
$0E |
mkdir | Not in 1.2 | Create a directory | Not documented | Not documented |
$10 |
rmdir | Not in 1.2 | Remove a directory | Not documented | Not documented |
$12 |
opendir | Yes | Open a directory for reading | — (select the drive and directory first) | A = file descriptor |
$14 |
readdir | Yes | Read the next directory entry | X = file descriptor; Y = page of an 87-byte destination | Directory entry at $YY00: long name, 8.3 name, start cluster, length and attributes |
$16 |
closedir | Yes | Close a directory | X = file descriptor | — |
$18 |
openfile | Yes | Open a file | — (find the file first) | A = file descriptor |
$1A |
readfile | Yes | Read a sector of the open file | — (reads the current open file) | X/Y = number of bytes read |
$1C |
writefile | Yes | Write a sector to the open file | — (writes the current open file; the Book’s description is incomplete) | — |
$1E |
mkfile | Partly implemented | Create a file in the current directory, as one contiguous block, so a
D81 image made this way can be mounted. Limited: 8.3 names only, and a
name without an extension may cause problems; normal files only; space
is allocated in 512KB steps, and a nearly full card is not handled
properly (mega65-core,
Hyppo dos.asm, release 0.97 and development) |
Name set first with setname; Z:Y:X = size in bytes, up to 16MB | C=0 and A = $8D if the file exists, or $8E
if the directory is full |
$20 |
closefile | Yes | Close a file | X = file descriptor | — |
$22 |
closeall | Yes | Close all files and directories | — | — |
$24 |
seekfile | Not in 1.2 | Seek within a file | Not documented | Not documented |
$26 |
rmfile | Yes | Delete a file | — (find the file first) | — |
$28 |
fstat | Not in 1.2 | File information | Not documented | Not documented |
$2A |
rename | Not in 1.2 | Rename a file | Not documented | Not documented |
$2C |
filedate | Not in 1.2 | File date | Not documented | Not documented |
$2E |
setname | Yes | Set the current file name | Y = page of the name | — |
$30 |
findfirst | Yes | Find the first file matching the name | — (set the name with setname first) | A = file descriptor for the directory, which may need closing with closedir |
$32 |
findnext | Yes | Find the next matching file | — (call findfirst first) | — |
$34 |
findfile | Yes | Find a file by name | — (set the name with setname first) | — |
$36 |
loadfile | Yes | Load the named file to $00ZZYYXX |
X/Y/Z = load address, low/middle/high; set the name with setname first | — |
$38 |
geterrorcode | Yes | Error code of the last failed service | — | A = error code of the last failed service |
$3A |
setup_transfer_area | Yes | Set the page used to pass data | Y = page of the transfer area | — |
$3C |
cdrootdir | Yes | Change to the root directory | X = drive number | — |
$3E |
loadfile_attic | Yes | Load the named file to $08ZZYYXX |
X/Y/Z = load address in attic RAM, low/middle/high; set the name with setname first | — |
$40 |
d81attach0 | Yes, deprecated from 1.3 (use attach) | Attach a D81 image as drive 0 | — (set the image name with setname first) | — |
$42 |
d81detach | Yes | Detach disk images | — | — |
$44 |
d81write_en | Yes | Allow writing to attached images | — | — |
$46 |
d81attach1 | Yes, deprecated from 1.3 (use attach) | Attach a D81 image as drive 1 | — (set the image name with setname first) | — |
$48 |
get_proc_desc | Yes | Read the process descriptor | Y = page of a 256-byte destination | Process descriptor at $YY00: task ID and name, attached disk images, file descriptors |
$4A |
attach | Yes | Attach a disk image | X bit 0 = drive (0 or 1); bit 7 = 0 to attach, 1 to detach; when detaching, bit 1 = both drives and bit 6 = do not reattach real drives. Set the image name with setname before attaching | — |
$50-$6E |
task and message services | Not in 1.2 | Create, switch and exit tasks; send and receive messages | Not documented | Not documented |
$70 |
toggle_rom_writeprotect | Yes | Toggle write-protection of the ROM in banks 2 and 3 | — | A bit 2 set if the ROM area is write-protected |
$72 |
toggle_force_4502 | Yes | Toggle the CPU's 6502 personality | — | A bit 5 set for the 45GS02 personality, clear for 6502 |
$74 |
get_mapping | Yes | Copy the MAP state to six bytes at page Y | Y = page of a 6-byte destination | At $YY00: MAPLO and MAPHI (big-endian words), then their megabyte offsets |
$76 |
set_mapping | Yes | Set the MAP state from memory | Y = page of a 6-byte source, laid out as for get_mapping | — (execution continues with the new mapping) |
$7C |
serial_monitor_write | Yes | Write to the serial monitor | Y = ASCII character | — |
$7E |
reset | Yes | Warm boot the MEGA65 | — | Does not return |
Several services have extra requirements. setname needs the name to
start on a page boundary between $0000 and
$7E00, be shorter than 63 characters and end with a zero
byte; it is ASCII, and Hyppo does not convert from PETSCII. Services
that pass data use the transfer area, which setname also sets to the
name's page. The file services work on the FAT32 file system of the SD
cards, not on floppy disks or disk images (MEGA65 Book, pp. N-10,
N-38).
Hyppo is updated with the MEGA65 core, independently of the ROM. In
August 2026, for example, a pull request was opened against mega65-core
to fill in getdisksize (the Book's getdrivesize), getcwd, mkdir, rmdir,
seekfile and rename, finish fstat, and add long file name support to
mkfile; it had not been merged at the time of writing (mega65-core PR #946). A program should therefore call getversion first and treat
error $82, not implemented, as an expected result for newer
services rather than as a failure of the program.
The Book lists these error codes (MEGA65 Book, pp. N-7, N-8). Its
table also has lower-numbered codes for partition, argument and
read/write errors, such as $10 for an invalid address,
which setname returns when Y is above $7E, and it notes that
the list may not be exhaustive.
| Code | Meaning |
|---|---|
$80 |
No such drive |
$81 |
Name too long |
$82 |
Service not implemented |
$83 |
File larger than 16MB |
$84 |
Too many open files |
$85 |
Invalid cluster |
$86 |
Is a directory |
$87 |
Not a directory |
$88 |
File not found |
$89 |
Invalid or closed file descriptor |
$8A |
Disk image has the wrong length |
$8B |
Disk image is fragmented on the SD card |
$8C |
No space on the SD card |
$8D |
File already exists |
$8E |
Directory full |
$8F |
Image already attached to the other drive |
$FF |
End of file or directory, or no such trap |
A MEGA65 program may be run on a ROM newer, or older, than the one it was written on. The habits below keep it working across versions.
| Practice | Why |
|---|---|
| Call the KERNAL only through the jump table | Routine addresses move between releases; jump table addresses do not |
| Change KERNAL behaviour through RAM vectors, using VECTOR and RESTOR | The vector table is an interface; patching ROM code is not |
| Avoid BASIC 65 internals | BASIC's routines, work areas and token handling change from release to release |
| Use the Hypervisor for SD card files | Its services are documented and its version can be checked |
| Check versions only where a feature demands it | Version checks for features you do not use only add ways to fail |
| Use a private base page, as in Chapter 9 | BASIC 65 and KERNAL zero-page usage is private to the ROM |
| Prefer flat addressing and DMA to MAP in cooperative code | The ROM's map settings are internal, and the CPU cannot read MAP back directly; only the Hypervisor's get_mapping can |
The C64's zero page was so well documented that programmers routinely borrowed its locations. On the MEGA65 that is risky. The MEGA65 team keeps what it calls a private API contract over the KERNAL's zero page, so its use can change (MEGA65 ROM issue #80). The same issue records a ROM release that began using a zero-page location in C64 mode that third-party software also used, breaking that software.
A routine that needs base page storage should therefore bring its own, by pointing B at a page it owns. A routine that must share zero page should restrict itself to locations the ROM documentation for its target version describes as free.
Sometimes a program genuinely needs an internal ROM routine, such as BASIC's floating-point conversion for USR. In that case:
This turns a silent crash after a ROM update into a clear message, which is the best that can be done for code outside the official interfaces.
$20000-$3FFFF. Versions are six-digit numbers: 92xxxx for
release and beta ROMs, 99xxxx for development ROMs built alongside them.
VERSIONQ returns the version from ROM 920417; $FF80 is
always $FF.$FF81-$FFF3 sit at the C64's addresses, though a few hold
different routines or take different arguments. The extended block runs
from VERSIONQ at $FF2F to PRIMM at $FF7D: the
MEGA65's own additions, then the C65-derived block from SPIN_SPOUT at
$FF4D, with C128-style calls at C65 addresses.$0314-$0319 and the I/O vectors up to $0333
follow the C64 layout; VECTOR and RESTOR manage them safely. The
KERNAL saves A, X, Y, Z and B before jumping through CINV, so a routine
that chains on to the old handler needn't preserve them.$D640, CLV, and carry for success. Hyppo 1.2 leaves
directory creation, renaming, seeking and the services for creating and
switching tasks unimplemented; check the version before relying on newer services, and
treat error $82 as "not available".Chapter 12 turns to the VIC-IV and graphics.