C64 OS PROGRAMMER'S GUIDE
C64 OS consists of several layers of code. It relies on the C64's KERNAL ROM at the lowest layer. Above this layer is the C64 OS KERNAL. And above the KERNAL sit the C64 OS applications and utilities.
The C64 OS KERNAL is entirely custom, it bears no resemblance to the KERNAL of any other operating system. In spirit it is an extension of the KERNAL ROM. The KERNAL is divided into 10 modules followed by an address lookup table.
C64 OS KERNAL Modules
C64 OS divides its code up into modules. Each module consists of one source code file (*.a), one or more include files for constants and macros (*.s), and one header file (*.h) for module exports. You can read more about this here.
There are currently 10 modules:
|Module||Access Offset||Lookup Offset|
C64 OS also makes use of the C64's KERNAL ROM, and provides header files which declare its exported routines and also divides them into modules. You can read more about this here. The documentation names the C64 KERNAL ROM's modules with the prefix KER_ to distinguish them from the C64 OS KERNAL modules.
The C64 KERNAL ROM has 7 modules:
The source code uses a handful of simple macros to shorten the code, reduce bugs and improve clarity of intent.
|LDXY||16-bit constant||Load's X and Y with constant, X = Low Byte, Y = High Byte|
|RDXY||address||Reads a 16-bit value from address to X and Y, X = Low Byte, Y = High Byte|
|STXY||address||Stores X and Y to address, X = Low Address, Y = High Address|
|ARG8||index,address||Copies an 8-bit inline argument starting at index to address|
|ARG16||index,address||Copies a 16-bit inline argument starting at index to address|
|PUSHXY||Pushes X and Y to the stack, X first then Y|
|PULLXY||Pulls X and Y from the stack, Y first then X|
|PUSH16||16-bit constant||Pushes 16-bit constant to the stack, Low Byte first then High Byte|
|PUSHPTR||address||Pushes 16-bit value at address to the stack, Low Byte first then High Byte|
|PULL16||address||Pulls 16-bit value from stack and puts at address, High Byte first then Low Byte|
|COPY16||16-bit constant,dst addr||Copies a 16-bit constant to dst address|
|COPYPTR||src addr,dst addr||Copies a 16-bit value from src address to dst address|
|CARG16||16-bit constant||Pushes constant to stack, Low Byte first then High Byte|
|CARG8||8-bit constant||Pushes constant to stack.|
|CCALL||address,size_of_args||Calls address, pulls size_of_args bytes off stack upon return.|
The documentation uses a set of symbols for input and output.
|A||The Accumulator Register|
|X||The X Index Register|
|Y||The Y Index Register|
|C||The Carry CPU Flag|
|Z||The Zero CPU Flag|
|N||The Negative CPU Flag|
|RegPtr||A 16-bit word, which contains a pointer to a memory address, X = Low Byte, Y = High Byte|
|RegWrd||A 16-bit word, which contains a non-pointer value, X = Low Byte, Y = High Byte|
|Ax → .B||An 8-bit inline input parameter. Inline parameters immediately follow the JSR $xxxx.*|
|Ax → .W||A 16-bit inline input parameter. Inline parameters immediately follow the JSR $xxxx.*|
The documentation is divided by KERNAL module. Each module's section begins with a standard structure describing the names of the KERNAL calls, their input and output format and a brief description of what the routine does.
The right arrow (→) indicates an input parameter. Before the arrow is the register, flag, or workspace variable and after the arrow is what that parameter must contain.
The left arrow (←) indicates an output parameter. Before the arrow is the register, flag, or workspace variable and after the arrow is what that parameter will contain when the KERNAL routine returns.
Inline parameters require some explanation. An example of their documentation looks like this:
This means there are two inline arguments, A1 and A2, both are 16-bit words, and must come in that order and that size following the call. To call this routine the code looks like this:
The topmost section of the documentation for each KERNAL module lists the names of the KERNAL calls, along with their input and output parameters and a description of what each routine does. There are certain steps which must be taken in order to call these routines, and these steps vary depending on who is making the call.
KERNAL Module Calling KERNAL Module
All KERNAL Modules are assembled against each other, in a given version and build of C64 OS. As well as a few central services that are not KERNAL modules proper; the Booter and the Loader, for example. In order for these modules to call the routines in other modules they need to do three things.
- Include "/h/:modules.h"
- Include "/h/:xxx.h" Where xxx = name of the module
- JSR to the module's exported call + the access offset of the module.
The exported module calls are just as they are listed in the documentation but with a trailing underscore. Here is an example of the Service module calling the Input module's polldevices routine.
modules.h statically computes the access offsets of each module, and defines the access offset constants. These can then be used by other modules by adding them to the exported routine offsets declared by the module's own header file.
The problem is that anything assembled using this call mechanism will only be compatible with all of the other modules and services that have been assembled against the same original modules.h. It is insufficient to change modules.h down the road, because it is only used at assemble time. Therefore, although this call mechanism is fast and lightweight, it is only suitable for use by parts of the core OS to call other parts of the core OS.
Application Calling KERNAL Module
In order for applications to remain forwards-compatible with future versions of C64 OS, they need to make use of the KERNAL's Module Lookup Table. Applications implement one or more private jump tables containing only those KERNAL calls they require. However, the jump tables are only partially static, and, during the application's initialization, use a small fixed-location routine to remap the entries according to records found in the Module Lookup Table. This is not as complicated as it sounds.
Here's an example of how an application should implement forward-compatible C64 OS KERNAL calls.
This example does not show everything required to build an application, but it shows the essentials necessary to make KERNAL calls.
The application has to include "/s/:app.s" which contains numerous constants and some macros that are essential for every application. One of those constants defines the location of the (very tiny) initexterns routine, which is at a fixed location in workspace memory.
Next, the application declares the start of its private jump table using a label. I've used the label "externs" here, but its name is irrelevant. The location of the jump table also is not important, although I usually put it at the end of a source code file, by convention.
Each module, for which you will make a jump table entry, must have its header file included. Again, where the header gets included is not important. But by convention I usually put the include statements within the jump table itself, and use them as separators and titles such that all of the entries that follow come from the same module. In the example above, the include statement brings in the input module's header. Below which are two jump table entries for exports from the input module.
App.s defines the syscall macro which makes it very easy to create the jump table entries. Recall that the routine names in the header files have a trailing underscore. This allows the jumptable entry to use the same name, but without the underscore, as the label that this application will use. Then simply use the #syscall macro with two arguments: The Lookup Offset for the module (see the table above for the lookup offsets of every module,) and the exported routine name from the header file.
The syscall macro creates a three byte entry in the table that is only partially resolved. In order to use the table it needs to be initialized at runtime. Every application has an initialization routine, in this routine you load a RegPtr (#ldxy) to the start of the externs table and call initextern. Initextern uses the data in the externs table and combines it with the lookup table from the current version of C64 OS to resolve the address of the KERNAL CALL. Initextern transforms the externs table in your application, in place, into a valid jump table, making your application compatible with whatever version of C64 OS the application is running on.
The externs table must end with a terminating byte, $ff.
Utility Calling KERNAL Module
It is mostly the same process for a Utility to make C64 OS KERNAL calls. However, the situation is complicated somewhat by the fact that utilities are assembled to, and run from, the memory space shared by the KERNAL ROM. Many but not all C64 OS KERNAL calls require the C64's KERNAL ROM.
The problem is that a Utility's own code cannot simply patch in the KERNAL ROM and then continue execution to call a KERNAL routine. The moment the KERNAL ROM is patched in, the CPU ceases to see the RAM that holds the Utility's code, and starts seeing code in the KERNAL ROM. This will virtually always lead to an instant crash. Thus, it is somewhat more problematic for a Utility to make C64 OS KERNAL calls.
There are a number of ways to deal with this:
- Some KERNAL calls don't require the KERNAL ROM.
- Some KERNAL calls, by their nature, are not the kind that will be called by a Utility.
- Some KERNAL calls explicitly handle mapping the KERNAL ROM in and restoring the previous mapping before returning control to the Utility.
- A Utility can store its data in main memory.
- A Utility can make use of the redirect vector.
In the documentation, dependence on the KERNAL ROM is specified for each routine. Any routine not marked as requiring the KERNAL ROM may be called directly by a Utility without any further effort. Examples include fetching the latest events from an event queue: readmouse, readkcmd, readkprnt. Other examples include allocating and deallocating memory: pgalloc, pgfree, malloc and free. These and others can be called freely.
Some KERNAL routines do require the KERNAL ROM, but are not the sort of routines that are generally called by Applications or Utilities. An example includes: polldevices. Usually this routine is called only by the system's interrupt service routine.
The most commonly used routines that require the use of the KERNAL ROM have built into them the ability to automatically map in the KERNAL ROM, make use of it, and then automatically restore the mapping before returning to the caller. Examples of these include the file access routines: finit, fopen, fread, fwrite, fclose, and the clipboard routines: copen, cread, cwrite and cclose.
However, there are caveats to using these file routines. Often, an application or Utility will have some statically allocated memory. Unfortunately, because a Utility's executable code is loaded to behind the KERNAL ROM, its statically allocated memory is also behind the KERNAL ROM. It is impossible to have the KERNAL load data directly from disk into the RAM behind itself. In this case, it is necessary for the Utility to allocate some memory dynamically, have the file routines load into that memory, and if necessary copy that data from the dynamically allocated space into static memory. This is in fact what the utilities do to save and restore their state.
There is a final option. If a KERNAL routine requires the KERNAL ROM, the Utility may make use of the system's redirect vector. Simply load the desired KERNAL routine into the system redirect vector (as defined by the service module), the load the registers with required input parameters and JSR to the system redirect. The redirect patches in the KERNAL ROM and I/O, jumps through the configured vector, and upon return patches the KERNAL ROM and I/O back out before returning to your code. Registers and flags are unmodified by the redirection process.
This document is subject to revision updates.
Last modified: Sep 20, 2022