The C64 OS Programmer's Guide is being written

This guide is being written and released and few chapters at a time. If a chapter seems to be empty, or if you click a chapter in the table of contents but it loads up Chapter 1, that's mostly likely because the chapter you've clicked doesn't exist yet.

Discussion of development topics are on-going about what to put in this guide. The discusssions are happening in the C64 OS Community Support Discord server, available to licensed C64 OS users.

C64 OS PROGRAMMER'S GUIDE

Chapter 4: Using the KERNAL

This chapter begins with an overview of what the KERNAL is, how it's loaded and divided into modules. It then provides general information about how to link to the KERNAL in your Application, Utility, and library code. Following is a discussion about the calling conventions used by C64 OS and how they're expressed in the headers and in the rest of the documentation.

In the second half of this chapter each KERNAL module is explored with a detailed discussion of each of the 114 KERNAL calls, how they related to each other and how they can be used. Code examples are provided for anything more than the most trivial calls.


What is a KERNAL?

First off, it's not a typo. Going back to the early days of Commodore, they spelled their 8-bit KERNAL with an "a". GEOS copied this practice calling theirs the GEOS Kernal. It has become a tradition and C64 OS therefore calls its kernel a KERNAL too.

Compute's Book, Tookit: KERNAL. GEOS Desktop showing the GEOS KERNAL.

In the C64 the built-in operating system comes in two layers, more or less (though not precisely) split between two 8 KB ROMs; The BASIC ROM and the KERNAL ROM. The KERNAL ROM provides the low-level functionality that makes the C64 work; interrupt service routine, keyboard driver, RS-232 driver, the blue scrolling full screen editor with PETSCII interpretation, a concept of devices with numbers, the low-level IEC serial bus routines, and higher level routines for opening logical files to devices, and reading and writing single bytes at a time from those logical file connections. In all, the KERNAL ROM provides a jump table with 39 routines that can be used in your assembly language programs to make writing software a lot easier.

The BASIC ROM provides the BASIC programming language, which, when used in direct mode, also provides the computer with a command line interface. The implementation of the BASIC commands backends on calls to the KERNAL ROM. For more information about the KERNAL, its 39 routines and how they group together into a set of 7 modules, see the reference text, C64 KERNAL ROM: Making Sense.

What is the C64 OS KERNAL?

The C64 OS KERNAL is an extension of the C64's own KERNAL ROM. There are some routines in the KERNAL ROM that are no longer applicable to C64 OS. The bottom of the aforementioned reference text has a compatibility table with C64 OS. 16 of the 39 (41%) are fully compatible and used by C64 OS. 12 of the 39 (31%) are deprecated and they should either be avoided or used very carefully. And the remaining 11 (28%) are marked Do Not Use. These must be avoided as they will overwrite critical addresses in C64 OS's workspace memory and likely lead to a crash.

The C64 OS KERNAL is divided into 10 modules, each represented by one or more files in the kernal directory. These files are loaded into memory by the KERNAL booter, which references modules.t in the settings directory to know which files to load.

File Path Notes
booter //os/ KERNAL booter
modules.t //os/settings/ List of KERNAL modules to load
*.o //os/kernal/ KERNAL modules

The modules.t settings file lists one KERNAL module per line, as a relative path from the system directory. Additionally, each line begins with a 1-byte code providing lightweight support for loading different KERNAL modules depending on hardware that is available. In version 1.0 of C64 OS, there are only 3 codes, comments were added in v1.04.

Symbol Character Meaning
* asterisk Always load
+ plus sign Load if a 17xx REU is detected
- minus sign Skip if a 17xx REU is detected
; semi-colon Comment line, up to 32 characters
# hash symbol Comment line, up to 32 characters

This allows for different versions of some KERNAL modules to be swapped in depending on whether an REU is available or not.

For example, to specify the string module, modules.t uses:

*/kernal/:string.o

The * is used because there is only one version of the string module. But to specify the screen module, it uses two lines:

+/kernal/:screen.reu.o
-/kernal/:screen.cpu.o

When the REU is available it only loads screen.reu.o, and when the the REU is not available it only loads screen.cpu.o. In all cases, the path component is a relative path from the system directory.


The KERNAL modules load to fixed addresses that have been assembled together using the constants defined in //os/h/:modules.h to pack them based on their individual sizes. The last 20 bytes of $CFxx ($CFEC → $CFFF) are used as a module lookup table. The KERNAL booter is assembled at the same time as the KERNAL modules, and knows where each module begins. The KERNAL booter writes the lookup table into memory during boot up. Each entry in the lookup table has the start address of a module. Each KERNAL module begins with a jump table to its own routines.

The About C64 OS Utility can be used to view the KERNAL module lookup table. It lists the modules in alphabetical order, pulls the address for each module from the lookup table and shows them in the mods section. (The alphabetical order shown is not their order in the lookup table, nor is it their order in memory.)

About C64 OS showing KERNAL modules.
About C64 OS showing the KERNAL module lookup table.

In total, the C64 OS v1.0 KERNAL provides an additional 114 routines, on top of the compatible KERNAL ROM routines. Here is a table of the modules by name, their offset in the lookup table, and the number of routines provided by each. Note that the position of the modules in the lookup table does not correspond with their order in memory. They were rearranged during beta testing to accomodate limitations imposed by IDE64. However, the existence of the module lookup table makes this kind of rearranging inconsequential, and demonstrates the usefulness of the abstraction.

No. Module Routines
1 Memory 11
2 Input 11
3 String 10
4 Math 6
5 Screen 14
6 Menu 4
7 Service 16
8 File 13
9 Toolkit 24
10 Timers 5

Linking to the KERNAL

Each module begins with a jump table to its own routines. This helps to keep the routines within the KERNAL organized, but it also made it possible to develop the KERNAL natively. The KERNAL is too large to be assembled by the C64 in a single step. Breaking it down into modules makes it easy for the C64 to handle.

When an Application (or a Utility, or library, etc.) wants to call a KERNAL routine, how does it find it? It has to know the module number, which is fixed. This gives it the offset into the module lookup table. Then it has to read the address of the start of the module out of the lookup table. Finally, it has to know the offset into the module's jump table to the specific routine.

All of this is accomplished by building a KERNAL link table of the routines you want access to, using a series of #syscall macros. Then, using the initextern routine, the KERNAL link table is converted into a local jump table that is linked to where the modules actually are in memory in this particular build of C64 OS.

This is easier than it sounds.

Building a KERNAL link table

Although not strictly required, a convenient place to put your KERNAL link table is at the very end of your code. In most first-party C64 OS Applications and Utilities it is the last thing found at the end of their main binaries.

The .h header files define a set of labels for the routines in each KERNAL module, plus they define one constant that is the offset of the module in the lookup table. Both of these are required as the parameters of the #syscall macro. The jump table constants in the module header files always end with an underscore. This is done intentionally so that the label to the #syscall macro can be exactly the same, only without the underscore. This should be made clearer with some examples.

Here is an example of how to create a #syscall table with some routines from the file KERNAL module, and an example of how it gets initialized.

The label externs defines the start of the KERNAL link table and is passed to the initextern call. The $ff byte at the end of the table is required as a terminator so that initextern knows where the table ends.

Don't forget the terminator

An easy mistake is to forget to include the terminator byte ($ff) at the end of the link table. If you forget this byte, your code will probably crash as the initextern routine will continue on into memory past the end of your table and rewrite the memory it finds there.

The #inc_h "file" on line 3 includes //os/h/:file.h, which defines lfil and all of those labels ending with underscore. "lfil" means "lookup file" and is the KERNAL module lookup table's offset for the file module. This number will never change, but it's nice to use the constant in the code because the meaning is clearer than if it were just a number.

In this example, we have built a table that lets our code call four routines provided by the file module. Note that "fopen_" with the underscore comes from the header, and our own local label at the start of the line can parallel it, "fopen" without the underscore. Each line is quite short and clean.

How does the #syscall macro and initextern work?

Into the weeds warning

This section goes into the technical details of how #syscall and initextern work. You do not have to understand how these work in order to use them. If you're not interested in how they work, skip to the next section, Calling Conventions.

The #syscall macro is extraordinarily simple, as can be seen by its implementation in //os/h/:modules.h. Here's what it does:

It takes the module's lookup number and outputs it as a byte. And it takes the routine's jump table offset number and outputs it as a word, two bytes little endian.

Let's look at how the lxxx lookup number is calculated. Taking an example from the top of //os/h/:file.h lfil looks like this:

lfil    = $0100-(2*8)

The module lookup table starts at the end of a page and works backwards. Each vector is 2 bytes, so module 1 would be $0100 - (2*1) == $FE. $FE/$FF are the last two bytes of any page and are the first 2-byte vector. lfil, module 8, is $0100-(2*8) == $F0. The complete table is:

Module # formula result
Memory 1 $0100-(2*1) lmem = $FE
Input 2 $0100-(2*2) linp = $FC
String 3 $0100-(2*3) lstr = $FA
Math 4 $0100-(2*4) lmat = $F8
Screen 5 $0100-(2*5) lscr = $F6
Menu 6 $0100-(2*6) lmnu = $F4
Service 7 $0100-(2*7) lser = $F2
File 8 $0100-(2*8) lfil = $F0
Toolkit 9 $0100-(2*9) ltkt = $EE
Timers 10 $0100-(2*10) ltim = $EC

#syscall outputs the lookup byte first, then outputs the routine jump table offset as a word. Each routine jump table offset is an increment of 3. These are offsets from where the start of the module is. If we look in //os/h/:file.h we find, for example, that fopen_ is 6, fread_ is 9, fwrite_ is 12 and fclose_ is 15. These numbers alone are not enough to tell you where in memory the routine is found.

Our example #syscall table from the previous section then, outputs a series of bytes like this:

Initextern is passed a pointer to the label at the start of the table, "externs" in this example. It reads the first byte, $F0, and combines it with the known page of the look up table, $CF, to get an address of $CFF0. This holds the low byte of the file module which it adds to the low byte of the jump table address, 6, and overwrites the 6 with the result. Then it loads the high byte of the file module from $CFF1, adds it (with carry) to the high byte in the jump table, 0, and overwrites the 0 with the result. Lastly, it replaces the $F0 with a JMP instruction, $4C.

It repeats this for every set of three bytes, until it encounters $FF as the first of a set of three. It identifies this as the table terminator and stops there. Assuming, from the screenshot of About C64 OS's mods section above, that the file module starts at $B734, we should expect that the lookup table has, $CFF0 = $34, $CFF1 = $B7. This will convert our entire KERNAL link table to this:

And that is of course exactly what we want. $4C,$3A,$B7 → JMP $B73A, the location of the fopen routine.

What's neat about this is, A) how simple it is, and B) it doesn't even need to assume the location of the lookup table. It only knows that it counts down from the top of whatever page the lookup table is in. It's the initextern routine that supplies the page the lookup table is found in. The KERNAL booter builds the module lookup table and the booter also writes the JMP in workspace memory to the initextern implementation which is actually found in the service KERNAL module.

In other words, in C64 OS, not only can the modules change size and be rearranged, but the whole KERNAL can be moved to a completely different place in memory. Everything that uses a series of #syscall macros to build a KERNAL link table, and then uses initextern to convert that table, without needing to be reassembled, will link correctly and continue to run totally unaware that the KERNAL is not even in the same place!

I think that's pretty cool.


Calling Conventions

The way that a C64 OS KERNAL call is made is inspired by the way the C64's KERNAL ROM calls are made. The call convention is kept similar in order to make the C64 OS KERNAL feel like an extension of the KERNAL ROM, and to be its spiritual descendent.

There are a few minor exceptions to this, some KERNAL calls use inline variables. This technique was borrowed, in principle, from the GEOS KERNAL.

In summary, most KERNAL calls take their parameters in the 6502 registers, A, X and Y, plus the carry is also sometimes used as an input parameter. Output parameters are also returned in 6502 registers. The carry is often used to indicate a success or failure state, clear for success or set for failure. One conventional difference is that the C64's KERNAL ROM often uses A/X as the low-byte/high-byte pair of a pointer passed in a call, while C64 OS's KERNAL tends to use X/Y for passing pointers. In C64 OS's terminology and documentation, X/Y used together to pass a pointer is referred to as a RegPtr (a register pointer). When X/Y are used to pass a non-pointer 16-bit word, the documentation refers to this is a RegWrd (register word) instead.

Passing parameters in CPU registers is incredibly fast. Many KERNAL calls that are meant to work together are designed so that the output parameters from one are either exactly or very close to the input parameters of the next. The overhead of passing parameters from one call to the next sometimes gets reduced literally down to zero. These conveniences are pointed out in the description of individual KERNAL calls, below.

The downside to using 6502 registers as parameters is that there are so few registers. They allow for passing 3 bytes plus 1 bit (A, X, Y plus carry) to a KERNAL call. Just like the C64's KERNAL ROM, when this is not enough, preparatory calls are used to supply extra parameters. Thus sometimes a KERNAL call requires making a set of calls. An example from the KERNAL ROM is the combination of SETNAM (set's a filename and length,) SETLFS (sets a logical file number, device number and channel number,) and finally OPEN. Combinations similar to this also exist in the C64 OS KERNAL.

Input and output parameters

The KERNAL module header files, found in //os/h/, specify the input and output parameters using an arrow notation. Everything listed with a right-facing arrow (-> or →) must be set prior to making the call. Everything listed with a left-facing arrow (<- or ←) will be set when the routine returns.

The following is an example from //os/h/:string.t (recall from Chapter 3: Development Environment → Constants and Includes that *.t files, where they exist, contain the comments and documentation, while their *.h equivalents have been stripped of these to speed up assemble time.)

strlen takes a RegPtr to a c-string as input. RegPtr's can be created manually, but are most often created with the standard macros #ldxy or #rdxy. Sometimes a RegPtr to a c-string will be the return parameter from another routine. A c-string is a sequence of bytes in memory that is terminated with a null ($00) byte. This routine counts the number of characters in the string and returns the result, a 16-bit number as a RegWrd. X is the low-byte, Y is the high-byte. It performs this work immediately, no other set up is required.

memncpy requires some additional manual set up. X, Y and A are each used, but they pass the address of a pointer found in zero page. The pointers to the memory and the length to copy have to be set up in zero page pointers prior to calling this routine. It has no returned result, because the result is applied directly to the memory pointed at by the pointers.

asc2pet, pet2asc and pet2scr all work fundamentally the same way. You put a byte in the accumulator, call the routine, and the the value in the accumulator gets transformed. Notice that it is possible to chain these calls together with no intervening parameter passing overhead. To convert a byte of ASCII to a screencode: