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.
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.
|modules.t||//os/settings/||List of KERNAL modules to load|
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.
|+||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:
The * is used because there is only one version of the string module. But to specify the screen module, it uses two lines:
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 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.
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:
|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.
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:
The output of asc2pet is exactly the same register as the input of pet2scr.
The following is a slightly more complex example of making KERNAL calls. This is taken from the file module, whose header is found at //os/h/:file.t (and //os/h/:file.h).
finit takes a RegPtr to a file reference. Thus, there must already be a valid file reference configured somewhere in memory. The RegPtr merely points to this structured memory. The carry is used to indicate success or failure. Only if the carry is set does the accumulator hold the error code. Otherwise, the accumulator's value upon return is undefined.
Another result of this routine is that it sets the workspace address currentdv. Currentdv is defined by //os/s/:file.s, and it is exactly the same workspace variable that is used by the KERNAL ROM. The C64 OS file KERNAL module is a thin abstraction layer on top of the KERNAL ROM.
In addition, finit returns the current device's command channel's logical file number in the X register. Why the X register? Because the KERNAL ROM's chkout routine takes the logical file number in the X register. This allows you to call finit and then immediately call chkout, with no parameter passing overhead, to begin sending a command to the device's command channel. This is a common pattern and makes using these routines together feel smooth and connected.
fopen takes a RegPtr to a file reference, plus it takes a set of file open flags in the accumulator. If an error occurred, the carry is returned set with the error code in the accumulator. Regardless of the error condition, RegPtr is still set to the same file reference upon return.
fread can thus be chained immediately following fopen. fread is, however, one of those that takes inline parameters. The first is a pointer to a memory buffer, and the second is a 16-bit word for the maximum length of data to read. The RegPtr is maintained upon returning from this routine, so that it can be chained directly into a call to fclose.
Let's take a quick look at an example of how these might appear in code.
This has given you a brief overview of how the documentation expresses the input and output parameters required for making C64 OS KERNAL calls. In the sections below, each KERNAL modules and all of their routines are outlined, with code examples and related usage commentary.
C64 OS KERNAL Modules
The C64 OS KERNAL is quite large, it provides 114 routines split across 10 modules. To make it easier to navigate to the section of interest, this chapter has been divided into separate pages for each KERNAL module.
The links to the module subpages are available in the left side navigation, and at a bottom of each subpage.
KERNAL Modules in Alphabetical Order
KERNAL Modules in Module Lookup Table Order
Chapter 4: Using the KERNAL, Summary
This chapter began with an over of what the KERNAL is, and how to link your Application, Utility, library or driver to the KERNAL using the initextern call. This chapter then discussed how to make calls to the KERNAL with an explanation of how the documentation works.
The second part, making up by far the bulk of this chapter, contains a detailed discussion of every one of the 114 calls in the C64 OS KERNAL. The calls are divided into the KERNAL's 10 modules: memory, input, string, math, screen, menu, service, file, toolkit and timers. Each module has a sub-table of contents linking to each routine in the module. Most modules have some explanatory introductory paragraphs. And some modules have explanatory content found above a group of a related routines. Many KERNAL calls include sample code showing how to use them, and including relevant surrounding context for setting them up.
This document is subject to revision updates.
Last modified: Apr 20, 2023