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 → Timers (module)
An Overview of Timers
C64 OS is not a pre-emptively multitasking operating system; it is an event-driven operating system. There is a single main event loop and a stack of screen layers. Multiple kinds of events are pulled from different queues and allowed to propagate through the screen layers.
If any single task, a piece of code that handles an event, goes into a loop that lasts for a long time without returning to the main event loop, the user interface becomes unresponsive. When the Interrupt Service Routine (ISR) notices that the main event loop has not been run for more than a couple of seconds, it begins animating the CPU Busy indicator. This is a small analog clock icon in the top left corner of the screen. The CPU Busy indicator tells you that the main event loop is not looping, and therefore events are still being queued but are waiting to be processed.
Timers are a special kind of event. Rather than being queued as the result of some direct user input, timers are queued programmatically, usually as the indirect result of user input. The timer queue may hold up to 10 timers. When the queue is full, a call to timeque fails; the timer is not queued and the carry is returned set.
A timer consists of a short structure: a 3-byte jiffy countdown, a byte of state flags, a pointer to a routine called a trigger, and a 3-byte countdown reset value. The timer struct and state values are defined by //os/s/:timers.s.
Timer structure:
Property | Offset | Size | Notes |
---|---|---|---|
ttime | 0 | 3 | Little endian, jiffy countdown. Max length: NTSC ~77 hours, PAL ~93 hours |
tstat | 3 | 1 | Set of state flags. See the following table for bit values. |
ttrig | 4 | 2 | Pointer to a routine to call when the countdown expires. |
tvalu | 6 | 3 | Little endian, ttime reset value. Interval timers restore ttime from tvalu after they've expired. |
Timer state bit values:
Property | Description | Value | Notes |
---|---|---|---|
tpause | Paused | %10000000 | A paused timer does not count down, even though it's queued. |
tintrvl | Interval | %01000000 | An interval timer remains queued after it expires, and has its ttime automatically reset from its tvalu. |
tcancel | Cancel | %00100000 | A timer flagged as canceled stops counting down and gets dequeued on the next cycle. |
treset | Reset | %00010000 | On the next cycle, the ttime gets restored from from tvalu, then the reset and expired flags are cleared. |
texprd | Expired | %00001000 | When the tvalu countdown hits zero the expired flag is set. |
tprecis | Precise | %00000100 | Allows an expired interval timer to continue counting down. |
trealtm | Realtime | %00000010 | A realtime timer has its trigger called from the ISR. |
All queued unpaused Timers count down as part of the interrupt service routine. When a countdown reaches zero the expired state bit is set. Expired timers typically have their trigger called during the processing of the main event loop.
There are two state flags that affect timer behavior:
PreciseThe state flag tprecis affects when a timer counts down. A timer is counted down during the interrupt service routine. When its countdown reaches zero its expired flag is set. An expired timer is triggered by the main event loop. However, if the CPU is busy, the main event loop is waiting. Eventually, when the main event loop runs, the expired timer's expired flag is cleared and then the trigger routine is called.
A regular interval timer's ttime does not count down while the expired flag is set. This means that if the timer is scheduled to trigger after 1 minute, the 1 minute starts counting down immediately before the previous expiration is triggered. If the CPU is busy and the trigger gets delayed by a few seconds, the next 1 minute count down doesn't start until after that delay. Over time this can cause a 1 minute interval timer to take longer than 1 minute between triggers. Due to the accumulation of such delays, after an hour you may find that the timer only triggered, say, 58 times instead of 60.
When the tprecis flag is set, the countdown occurs even while the timer is already flagged as expired. If the CPU is busy when the timer expires, that will still delay the triggering of that expiration, but the countdown for the next one has already begun. This may result in less than the countdown time between two adjacent triggers, but over time it keeps a series of triggers more precisely aligned with the intent. For example, over an hour, a 1 minute precise timer should fire 60 times.
A precise timer does not help in the situation where a countdown time is too short. If the countdown is very short, say, 20 jiffies, and a precise timer expires, it begins counting down again before the timer is triggered. However, because 20 jiffies is so short, the CPU may still be busy when the countdown reaches zero again before the previous expiration was triggered. This does not result in missed triggers backing up. There is only one expired flag, so the expiration that didn't have time to trigger before it expired again is lost and never called.
RealtimeA timer that is flagged as realtime is triggered from inside the Interrupt Service Routine. This is called realtime because the IRQ interrupts the CPU to count down the timers. If the timer expires, its trigger is called immediately. The timer triggers before returning to whatever the CPU is busy working on, instead of waiting for that to finish and then being triggered during the main event loop.
Realtime timers are powerful because they add a high degree of control, but they must be used judiciously. For example, the code that is run by a realtime trigger must be very careful about modifying zero page. If it changes anything, it must back it up and restore it before returning. A realtime timer must never change the state of a drive. These sorts of changes would lead to serious unintended consequences and likely crash or lock up the system. Realtime tigger routines should also be as short as possible.
Realtime or not, timers have a maximum precision of one jiffy. That's 1/60th of a second on NTSC and 1/50th of a second on PAL.
timeque
Purpose | Enqueue a timer structure. |
---|---|
Module offset | 0 |
Communication registers | X, Y |
Stack requirements | 6 bytes |
Zero page usage | $02, $d8, $d9 |
Registers affected | A, X, Y |
Input parameters | RegPtr → Pointer to a timer structure. |
Output parameters |
C ← SET if the timer queue is full. Timer not queued. C ← CLR if the timer successfully queued. |
Description: This routine is used to add a timer to the queue. The queue can hold up to 10 timers at the same time. If the queue is full, this routine returns with the carry set to indicate that the timer was not queued.
The initial value of the timer's tcancel state flag affects the first firing of the timer. If the tcancel flag is set when the timer is queued, the timer undergoes an initial reset, which copies the tvalu to ttime. If tcancel is clear when the timer is queued, no reset occurs. This allows the initial ttime value to be different than the reset tvalu. This allows interval timers to have one inital countdown that is either longer or shorter than all the subsequent ones.
When a timer is queued, the only thing queued is a pointer to the original structure. Therefore, changing the status flags of the original structure affects the timer that is queued. It is possible, for example, to change the trigger pointer after each time the timer triggers. This can be useful for alternating something back and forth, without needing to requeue the timer. Alternatively, the tvalu could be changed between triggers to vary the length of time between triggers, again without needing to requeue the timer. However, any changes to the timer's ttime or tvalu (or ttrig if it's a realtime timer) should only be done while IRQs are masked.
There is no routine to dequeue a timer. To dequeue a timer, set the tcancel bit in the timer's state byte. It will be dequeued automatically on the next cycle of the main event loop.
All timers are dequeued automatically when an Application is quit. Therefore, it is not necessary to cancel queued timers when your Application is quit. Timers queued by a Utility should be canceled prior to the Utility being quit.
timedwn
Purpose | Counts down all queued, unpaused timers. |
---|---|
Module offset | 3 |
Communication registers | X, Y |
Stack requirements | 6 bytes + |
Zero page usage | $02, $d8, $d9 |
Registers affected | A, X, Y |
Input parameters | None |
Output parameters | None |
Description: This routine is called automatically by C64 OS's standard interrupt service routine. It shouldn't be necessary to call this routine manually.
This routine is responsible for counting down timers, and triggering expired realtime timers. It also counts down delayed asynchronous messages.
timeevt
Purpose | Runs expired timer triggers. Sends asynchronous messages. |
---|---|
Module offset | 6 |
Communication registers | X, Y |
Stack requirements | 6 bytes + |
Zero page usage | $02, $d8, $d9, Custom |
Registers affected | A, X, Y |
Input parameters | None |
Output parameters | None |
Description: This routine is called automatically by the main event loop. It should not be necessary to call this routine manually.
This routine is responsible for triggering expired timers, and dequeuing canceled timers and expired non-interval timers. This routine also sends asynchronous messages to the Application and Utility.
Asynchronous Messaging
Applications and Utilities both have a vector in their initial vector table for receiving messages. A message to an Application could come from a Utility, or from the system, such as when the user interacts with the menus. A message to a Utility could come from the Application, or it could also come from the system, such as when the clipboard contents change.
When an Application is running, it can send a message to the Utility simply by jumping through the Utility's message vector. When this happens though, code in the Utility starts executing, nested inside the context of the Application executing code. For any messages that can be handled quickly and easily, this is not a problem. In response to a message, a Utility might mark part of its user interface, and its screen layer, as dirty, but it won't redraw itself inline with the handling of the message. Setting some dirty flags is short and easy. The redraw cycle can later handle the heavy job of redrawing.
There are situations, however, where it is inconvenient or undesirable for the Utility to be handling the message in the middle of the Application processing an event. Or vice versa, it can sometimes be undesirable for an Application to respond immediately to a message while in the context of a Utility executing some code. An alternative to jumping directly to the message vector is to send an asynchronous message.
An asynchronous message is structured exactly the same as a direct message. The message command in the accumulator, and the X and Y registers can be used for message data. If there is more to pass than just 2 bytes, the X and Y can be used as a pointer to a data structure. The asynchronous message captures those 3 bytes, but doesn't pass them to their destination right away. It allows the currently running code to return to the main event loop, and then in the timer events phase of the main event loop, if an asynchronous message is waiting, it passes it then.
The caveat is that there can only be one queued asynchronous message, per destination, at a time. So there can be one asynchronous message at a time being sent to the Utility, plus one asynchronous message at the same time being sent to the Application. If one message is already queued, a second message to the same destination will fail to be queued. Asynchronous messages have to be used carefully, but they can be useful in some special situations.
Message commands are defined by //os/app.s.
msgapp
Purpose | Enqueue an asynchronous message to the Application. |
---|---|
Module offset | 9 |
Communication registers | A, X, Y |
Stack requirements | 2 bytes |
Zero page usage | None |
Registers affected | A |
Input parameters |
A → Message Command X → Message Data (meaning is specific to the message code) Y → Message Data (meaning is specific to the message code) C → SET to configure a delay of 20 jiffies. |
Output parameters | None |
Description: This routine is used to enqueue an asynchronous message to the Application. Load the message command into the accumulator, and message data into X and Y, just as you would to send a direct message. Then call msgapp. There may only be one asynchronous message queued at a time. If a message is already queued, subsequent calls will fail to enqueue the new message.
If the carry is set when msgapp is called, the message will be assigned a delay of 20 jiffies. The asynchronous message delay is counted down by timedwn. The message is sent to the Application during the timeevt routine called by the main event loop, if the delay timer is zero. At that point, the asynchronous message queue is opened to accept another message.
msgutil
Purpose | Enqueue an asynchronous message to the Utility. |
---|---|
Module offset | 12 |
Communication registers | A, X, Y |
Stack requirements | 2 bytes |
Zero page usage | None |
Registers affected | A |
Input parameters |
A → Message Command X → Message Data (meaning is specific to the message code) Y → Message Data (meaning is specific to the message code) C → SET to configure a delay of 20 jiffies. |
Output parameters | None |
Description: This routine is used to enqueue an asynchronous message to the Utility. Load the message command into the accumulator, and message data into X and Y, just as you would to send a direct message. Then call msgutil. There may only be one asynchronous message queued at a time. If a message is already queued, subsequent calls will fail to enqueue the new message.
If the carry is set when msgutil is called, the message will be assigned a delay of 20 jiffies. The asynchronous message delay is counted down by timedwn. The message is sent to the Utility during the timeevt routine called by the main event loop, if the delay timer is zero. At that point, the asynchronous message queue is opened to accept another message.
It is safe to send an asynchronous message to a Utility, even if there is no Utility loaded. The message is enqueued, any delay is counted down, and when it is time to send the message the timeevt routine checks to see if a Utility is loaded. If no Utility is loaded at that time, the message is not sent, but is still dequeued.
KERNAL Modules in Alphabetical Order
KERNAL Modules in Module Lookup Table Order
Return to Using the KERNAL → KERNAL Modules
Table of Contents
This document is subject to revision updates.
Last modified: Apr 20, 2023