CokeOS v3

A 68HC11-based operating system for slave-type embedded systems

By Bret Victor

CokeOS v3 is somewhat generic, in that it knows nothing about the Coke machine or any physical devices. Instead, it allows code and data to be uploaded to the board, provides user code with timer and interrupt access, and allows user code to communicate in a standardized manner with the host and with each other.

The kernel spends most of its time waiting for incoming commands over the serial port. While waiting, it checks for timer functions that need to be executed. The resource manager handles memory. Everything that is uploaded to the board is called a resource and is assigned a resource identifier (rezID) that can be used as a handle to refer to the uploaded data. Resources can be used for all kinds of things: executable code, fonts, graphics bitmaps, graphics scrips, command scripts, doodad patterns, text strings, audio clips, and whatever else a particular module might want to use that can be uploaded to the board.

How to write a user module

User modules run from RAM, so they have their data segment in with the code (and can also use self-modifying code tricks). The assembly file should contain a standard comment header (see the existing modules) and should be assembled with cokedasm's -r option to produce a relocatable .rel file. (You can use the shell script reldasm for this.)

There are four ways of getting the functions in your code called: IPC, commands, timers, and hooks.

IPC

IPC is what the kernel and other modules use to call functions in your module. The very first code in the module is the IPC entry point, which is called at certain times by the kernel. Register a indicates why the entry point is being called:
IPC_Coke_Hello the module has just been relocated and initialized
IPC_Coke_Goodbye the code resource is being freed
IPC_Coke_Execute the Coke_ExecCodeResource command has been called

If you want your module to receive more IPC messages than just that, you need to register your module ID using Coke_RegisterIPC. Then, other modules can call the IPC entry point using the ID, and the entry point will also be called when an IPC message is broadcast to all modules.

The entry point is called with the IPC message code in a, and a pointer to a parameter table in x. The pointer in x may be used to pass data in either direction (or both) or it may be ignored. The IPC function should return with the carry clear if it handled the message, or carry set if it didn't. The IPC function should preserve the registers.

Command Handlers

Commands are how the host system tells your module what it wants it to do. In its initialization routine, a module usually uses Coke_InstallCommandTable to register a list of its command handlers with the kernel. Then, when the kernel receives a command for your module, it will call the appropriate command handler.

The only differences between a command handler and a normal function are that the command handler calls Coke_GetChar for each parameter that the command requires, and a handler has standardized return values. Upon return, register a must be one of the following:
COKE_NAK the command is not implemented
COKE_ACK the command was executed successfully
COKE_FAIL the command failed. Register b must contain the error code.
COKE_QUERY the command was executed successfully and is returning data to the host. Register b contains the length of the query data to return, and register x holds a pointer to the data in memory.
A command handler does not have to preserve the registers.

Timer Functions

Timer functions are called periodically by the kernel while it is waiting for serial port data. Timers are installed with Coke_SetTimer and are removed with Coke_RemoveTimer. Timers are just like normal functions, but they should preserve the registers. A timer function is allowed to call Coke_RemoveTimer to remove itself when it is no longer needed. (A timer function that always removes itself can be used for "one-shot" delays.)

Interrupt Hooks

Hook functions are installed into "interrupt hooks", meaning that they are called when a particular interrupt occurs. Hooks are installed with Coke_InstallHook and are removed with Coke_RemoveHook. Hook functions do not need to preserve the registers, but they must end with the line

_endhook StartOfHookFunction

The macro _endhook is used to implement a linked list of hook functions, so any number of functions can be hooked to a single interrupt, and hooks can be installed and removed in any order. When writing a hook, keep in mind that it is being called from within an interrupt, and take appropriate precautions to not call non-reentrant functions or otherwise disturb the main code.

Kernel functions

These are all called by name with a jsr, with the registers set as indicated. Any registers which aren't being used to return a value will be left unmodified.

Coke_GetChar: returns the next parameter of a command in a. The parameter may come from the serial port or from a script.

Coke_SendMessage: takes a message code in a. Adds the message code to the outgoing message queue, and sends it out to the host the next time through the idle loop. It is safe to call SendMessage from both main code and interrupt routines.

Coke_InstallCommand: takes a command number in a, function pointer in x. Installs the command so that invoking that command will call the given function.

Coke_InstallCommandTable: takes a pointer to a command table in x. Each entry of the table is of the form:
dc.b commandnumber
dc.w functionpointer
The table ends with a zero for commandnumber.

Coke_RemoveCommand: takes a command number in a. Removes the command so that subsequent invokations of the command return COKE_NAK.

Coke_RemoveCommandTable: takes a pointer to a command table in x. Removes all the commands in a table. The table has the same format as with Coke_InstallCommandTable above.

Coke_SetTimer: takes a timer ID in a, param in b, function pointer in x, timeout value in y. Installs a timer function, or updates a timer function if the given timer ID is already installed. The function will be called approximately every (8 * timeoutvalue) milliseconds, during idle time. If the timeout value is zero, the function is called every time through the idle loop. Param is passed to the timer function in a when it is called, and can be used for whatever the function wants. Timer functions should leave all registers unmodified.

Coke_RemoveTimer: takes a timer ID in a. Removes the given timer function from the timer list, if it is installed.

Coke_InstallHook: takes a hook code in a, pointer to the end of a hook routine in x. Installs a hook function into an interrupt hook. Any number of functions can be installed into an interrupt hook. The end of the hook function (which x points to) should consist of the _endhook macro. Hook functions do not need to leave the registers unmodified. Hook code is one of the following:
HOOK_USER 8 ms real-time interrupt
HOOK_AUDIO interrupt whose rate is controlled by the system variable audiodelay and can be toggled with the system call sys_audio.
HOOK_GFX interrupt whose rate is controlled by the system variable gfxdelay.
HOOK_IRQ external /IRQ line
HOOK_SWI software interrupt, invoked with the SWI command.

Coke_RemoveHook: takes a hook code in a, pointer to the end of a hook routine x. Removes the given hook function from the given hook. Hook functions can be removed in any order.

Coke_RegisterIPC: takes a module ID in a and a pointer to the IPC entry point in x. Registers the entry point with the kernel so it can receive broadcast messages and other modules can call it with Coke_CallIPC.

Coke_UnregisterIPC: takes a module ID in a. Unregisters the module's IPC entry point.

Coke_CallIPC: takes an IPC message code in a, flags or a module ID in b, and an optional pointer to a parameter table in x. Calls an IPC function or broadcasts a message. Register b can either contain a module ID or the logical OR of the following flags:
IPCFLAG_Broadcast all registered IPC entry points are called with the given message. Carry is always clear on return.
IPCFLAG_StopAfterOne if IPCFLAG_Broadcast is set, all registered IPC entry points are called until one handles the message (returns with carry clear), at which point the broadcast is stopped. Carry is returned clear if someone handled the message, set if no one did.

If b contains a module ID, only that module is called. Carry is returned set if the module did not handle the message or isn't installed, clear if the module handled it. To check whether a module is installed, call Coke_CallIPC with a set to IPC_AreYouThere. Carry will be clear on return if the module is installed and registered.

Coke_InvokeCommand: takes a command number in a, and parameters on the stack. Allows a routine to call a command handler by pushing parameters on the stack. The parameters required by the command must be pushed in REVERSE ORDER. (This also means that words must be pushed explicitly as hibyte, then lobyte.) InvokeCommand cleans up the stack itself, so the caller doesn't need to pull off what it pushed. However, REGISTERS ARE NOT PRESERVED! The command's response is returned in a -- all other registers are indeterminate.

Coke_InvokeCommandMem: takes a command number in a, pointer to a parameter list in x. Allows a routine to call a command handler by providing a pointer to a parameter list in memory. Note that words must be stored in memory with lobyte first. Registers are preserved, except for a, which returns the command's response.

Resource functions

Resource_GetFreeSpace: returns in d the number of bytes of available memory.

Resource_CreateResource: takes resource size (in bytes) in d. Allocates a chunk of memory for whatever a module wants. Attributes are initially set to zero, but can be changed with Resource_SetAttributes. If there isn't enough room to allocate the memory, carry is returned set. Otherwise, carry is returned clear and the new resource ID is returned in b.

Resource_GetResource: takes a resource ID in b. If the resource does not exist, carry is returned set. Otherwise, carry is returned clear and a pointer to the start of the resource data is returned in x.

Resource_GetLength: takes a resource ID in b. If the resource does not exist, carry is returned set. Otherwise, carry is returned clear and the size in bytes of the given resource is returned in d.

Resource_GetAttributes: takes a resource ID in b. If the resource does not exist, carry is returned set. Otherwise, carry is returned clear and the attributes byte of the resource is returned in a.

Resource_SetAttributes: takes a resource ID in b and an attributes byte in a. If the resource does not exist, carry is returned set. Otherwise, carry is returned clear and the attributes byte of the resource is set to the given value.

Resource_FreeResources: takes a resource ID in b. Frees all resources after and including the given ID.

Assembing CokeOS v3

One of the cool things about CokeOS v3 is that it can be assembled as a user module and uploaded to the board as a code resource. It will then "take over" as the operating system, running out of RAM. This gives you an opportunity to test and debug changes to the OS without having to program the EEPROM. To assemble as a module, type something like this:

cokedasm coke.asm -r -ocoke.rel -D__module

To assemble normally, use this:

cokedasm coke.asm -f3 -ocoke.out -c

You can then upload coke.out as a resource and use the Program module to write it into the external EEPROM.

Source code

Here's the assembly source for CokeOS v3. Nobody outside of Caltech can transfer or use this code (in part or in whole) without writing to me: bret@ugcs.caltech.edu.

Here are the include files: