Programming tutorial: Part 14–Timers

Atari Lynx programming tutorial series:

In the last part of the tutorial we looked at how the Lynx console uses UART and how the hardware behaves. Before we dig deeper into ComLynx and programming for it, we need to take a little detour to investigate timers. In this part we will cover the basics of timers, the hardware, how they work and get you started programming the timers.

Lynx and timers

The Atari Lynx has a customized 65SC02 processor called Mikey. One of the customizations is the addition of a set of timers. The Lynx has 12 timers inside of Mikey: 8 of these are “normal” timers and the other four are audio channels, which behave like timers but generate audio. We will look at the 8 regular timers first and are going to cover audio and channels in a later tutorial part.

First, what are the timers in the Lynx? There are a number of possible meanings to the word timer. The Lynx has countdown-timers, meaning they count down to zero. They have some characteristics and specific behavior.

The short story on timers

An activated timer counts down from a start value to zero at a specific pace. Once it reaches zero it will underflow and optionally cause an interrupt (IRQ) with the timer’s flag set in the interrupt status byte (available through INTSET or INTRST). Also, it might reload to counter to a backup value and continue counting down again.

And the long story with pretty pictures

A timer ticks down to zero at a certain frequency. It does so by reducing its counter value at the end of every time interval. That interval is called the source period. The timer keeps its current value for the length of the interval, before dropping by –1 (minus 1) at the end.


When the timer has reached zero, it is said to “expire” or “timeout”. It will expire at the end of the period. This means that when a timer starts counting down from 5, it will expire after 6 (not 5) periods of time. An expiring timer might trigger an IRQ and might reload. Both of these depend on the settings of the timer.

If a timer has reloading enabled, the value of the timer will change to the backup value (aka reload value) of the timer after it expires. The behavior of a reloading timer would look like this:


Note that the start value of a timer does not have to be the same as the reload value, as depictured above. I intentionally had it start at 3, instead of the reload value 5.

The timers all behave the same, with only a very small number of exceptions and special purposes for some of them. They share the following properties:

  • Count enable
    A timer can be turned on (enabled) or off (disabled). Only when it is enabled will it count down.
  • Source period
    The timer counts down one tick at a time. One tick takes an amount of time that is called the “source period”. The source period ranges from 1, 2, 4, 8, 16, 32 to 64 µs (microseconds).
  • Current count
    The timer has a current value or count that indicates how many periods are left for the timer to reach zero.
  • Reload enable
    When the reloading is enabled, the timer will reload once it reaches zero. Reloading means it will get a new current value higher than zero.
  • Backup (or reload) value
    A timer that reached zero it will reload its counter to the backup value provided reloading is enabled. The backup value must be higher than zero for the timer to count at all.
  • Timer done flag
    Once a timer has reached zero, it is done. The timer will remember that it is done, even when it is set to reload, by flagging a bit called Timer Done. It is possible to clear this flag.
    Important: an active timer that has the Timer Done flag set will not count down, unless it has reloading enabled. 
  • Interrupt enable
    By enabling interrupts, the timer will cause an IRQ when it underflows. Otherwise, the timer will simply expire, flag it is done and reload (if reloading is enabled), then continue counting.

More than one timer

The 8 timers of the Lynx are numbered from 0 to 7: timer 0, timer 1, all the way up to timer 7. Timers 0, 2 and 4 are special. Timer 0 and 2 are related to video and correspond to the dimensions and refresh rate of the LCD screen. Timer 4 is the baud rate generator of the UART, like we discussed in a previous part. The other timers are yours to use.

The timers can be used stand-alone, or linked together. The first speaks for itself. A standalone timer is a timer with its own properties and completely self-contained in its behavior. However, a linked timer will not have a source period defined in microseconds, but depends on the timer to which it is linked to count it down.


The picture shows how timer 3 is linked to timer 1. Whenever timer 1 expires it “ticks” the linked timer, number 3 in this case. It’s kind of like a countdown stopwatch. Imagine that timer 1 corresponds to seconds and timer 3 to minutes. Whenever the seconds timer 1 reaches zero it will cause the minutes timer 3 to reduce by 1.


Multiple timers can be linked in a chain according to the linking order. The linking order of the timers is:image
This order is fixed, so timer 3 can be linked to 1 (ie. timer 1 ticks timer 3), but to none of the other timers. Timer 7 ticks audio channel 0, which is a special kind of timer. Audio channel 1 links to 2, and 2 to 3. Audio channel 3 ticks timer 1. The other chain is timer 0 to 2 to 4.

It is important to remember that each of these timers can be linked, but don’t have to be and usually are not. Except for timer 0 and 2 as you will see next.

Video timers

The special timers 0 and 2 deserve a bit of extra explanation. These video timers should not be touched by you. They are initialized by the boot rom code and set to specific values. They are set up to give some additional help during the drawing of the screen and timing your code. The timers both have interrupts and reloading enabled. Again, do not change their settings! You have been warned.

Timer 2 corresponds to the frequency of screen refreshes. Once every screen refresh it will expire and generates an interrupt that usually goes by the name of the vertical blank (VBL) interrupt. It has a backup value 104 for 102 horizontal LCD lines plus 3 for vertical blank time (also referred to as the overscan on some other consoles). Timer 2 is set up to link to timer 0.

Now here comes some math. Take the regular refresh rate of 60 Hz. That’s 1/60 * 1000000 = 0,016667 seconds or 16667 us  per screen or also the “time” that timer 2 should take from reload to expired zero. For a screen that has 102 real + 3 virtual display lines, it means that the time per tick should be 16667/105 = 158.7 microseconds. That’s the time that timer 0 needs to expire. Given a source period of 1 µs (this is how it is set by the boot rom code) we can deduce that the reload value of timer 0 should be 158. That’s indeed what it is set to.

There’s another Magic “P” value that is somehow related to this. The Epyx specification mentions a formula that takes the time a line needs to expire and computes this P value. It is important in the electronics of the hardware somewhere.


For 60Hz the P value is known to be 41 (0x29, again from the Epyx documentation). With the inverse of the function


linetime turns out to be (41+1)/4*15+0.5 = 158 µs. That brought us right back to the expiry time of timer 0. Sounds reasonable.

Hardware registers for timers

The properties of a timer are influenced by 4 hardware registers:

    The backup (reload) value of the timer. Whether this is used depends on the reloading setting of the timer (see CTLA).
    The static control byte of the timer, which I’ll refer to as CTLA from here on. This enables or disables the timer, reloading and interrupt, plus it has the source period selector.
  3. TIMxCNT
    The current value of the counter of the clock.
    This is the dynamic control byte. It has 4 bits that indicate the state of the timer. The most important one is the Timer Done bit.

In this list the x denotes each of the timers. Each timer has these for bytes. E.g., timer 3 has TIM3BKUP, TIM3CTLA, TIM3CNT and TIM3CTLB.

The location of the hardware registers starts at $FD00 and continues to $FD1F, in groups of four consecutive bytes (BKUP, CTLA, CNT and CTLB) per timer. So, $FD00 to $FD03 for timer 0’s backup, static control, current count and dynamic control, then $FD04 to $FD07 for timer 1 all the way to $FD1C – $FD1F for timer 7’s bytes.

The backup value and the current value are full 8-bit values. They range from 0 to 255 as an unsigned byte and do not really deserve much explanation. Both can be written to and read from. By writing to the backup value you set the counter for the timer upon reload. It usually does not have an immediate effect. Writing a byte to the count byte will immediately change the current value. It might be a good idea to disable a timer first before writing a new value into count.

Static and dynamic control

The other two bytes are more complicated. Both are composed of individual bits that have a specific meaning.

The static control has three Enable bits: for the timer itself, the reloading and the interrupt. When the value of the particular bit is a 1 (one) it is enabled. For 0 (zero) the specific function or behavior is disabled.

One bit is used to indicate that the Timer Done bit should be reset to zero. It is a write-only bit and when written to will clear the Timer Done bit in the dynamic control byte (more on the dynamic control bits below).


The bits 0-2 are used to select the source period of the timer. This table will help you find the right bits for your needs:

Bits Value Description
000 0 1 µs (microsecond)
001 1 2 µs
010 2 4 µs
011 3 8 µs
100 4 16 µs
101 5 32 µs
110 6 64 µs
111 7 Linking (linked to previous timer in link order)

Here’s an example of a particular static control value: writing 0x98 to TIM1CTLA. That is 0%10011000 in binary. You can see bits 7, 4 and 3 are set. Looking at the meaning of the bits, this means timer 1 is enabled, it reloads and fires interrupts. The source period bits are 000, so that’s a 1 µs interval time for timer 1.

Another one: writing 0x4A (or 0%01001010 binary) to TIM5CTLA. This means that the Timer Done bit will be reset for the timer 5, and it is started at a 2 microsecond source period. It will not reload or fire an interrupt when it expires. For a count value of 199 the timer will expire after 400 microseconds.

Then there is the dynamic control. It has the four lower bits that reflect the state of the timer dynamically. You typically do not write to dynamic control, but read from it. There’s one important bit in dynamic control that has a known function. It is the fourth bit (bit 3) that tells whether the timer has ever timed out (expired). You can inspect the individual bits with code like this:

MIKEY.timer5.control2 & 0x08) == 0x08

The other three bits are Last Clock, Borrow-in and Borrow-out. The function of these bits are unknown to me. I do know that it is not emulated correctly in Handy or any of its derived emulators. Last Clock has frequently changing values at a rate comparable to the source period of the timer. The two borrow bits have a function that I couldn’t figure out yet. If anyone knows, feel free to comment. The bottom line is you probably only need the Timer Done bit anyway.

Yooh, Mikey! Program the timers already

Alright, we know enough now to do some programming. The first thing will be a little piece of code that creates a timer that will count down from 100 to zero. The include file _mikey.h has various handy definitions related to the Mikey hardware registers. These have been captured in a struct that reflects the layout of the Mikey address space (see the tutorial part on memory mapping) and its hardware registers. It also holds the structs for the timers:

/* Mikey structure definition */
typedef struct
_mikey_timer {
  unsigned char reload;
  unsigned char control;
  unsigned char count;
  unsigned char control2;
} _mikey_timer;

This has the exact layout of the hardware registers per timer we discussed a moment ago. The only difference  is that CTLA and CTLB are named control and control2.

The struct for Mikey has the 8 timers starting from $FD00 like so:

struct __mikey {
  struct _mikey_timer timer0;       // 0xFD00
  struct _mikey_timer timer1;       // 0xFD04
  struct _mikey_timer timer2;       // 0xFD08
  struct _mikey_timer timer3;       // 0xFD0C
  struct _mikey_timer timer4;       // 0xFD10
  struct _mikey_timer timer5;       // 0xFD14
  struct _mikey_timer timer6;       // 0xFD18
  struct _mikey_timer timer7;       // 0xFD1C

And finally, the include file lynx.h has this defined:

/* Define Hardware */
#include <_mikey.h>
#define MIKEY (*(struct __mikey *)0xFD00)

Essentially this creates an overlay of a struct over the hardware memory addresses, so they get convenient names and an entry point called MIKEY. We can refer to the timer registers by using MIKEY.timerx and naming the property of the timer.

MIKEY.timer1.count = 100;
MIKEY.timer1.control = 0x0E;

That gives you a timer that will go from 100 to 0 and expires. Since reloading and interrupts are not enabled nothing will happen except that the Timer Done bit gets set in the dynamic control byte.

When you use a single timer in this way, you will find out that even at the slowest setting (64 µs) and the highest reload (255), the expiry time of a reloading timer is still fast (64 * 256 = 16384 µs = 0.016 seconds). To get a more realistic timer you will have to link timers or use the VBL (and its interrupt). We are going to investigate the latter method in another part of the series. Linking is something we can do right now.

MIKEY.timer1.control = 0x1E;
MIKEY.timer1.reload = 255;
MIKEY.timer1.count = 255;

MIKEY.timer3.control = 0x1F;
MIKEY.timer3.reload = 255;
MIKEY.timer3.count = 255;

With the setup above you have enabled timers 1 and 3 where timer 1 has a 64 µs source period and timer 3 is linked to timer 1. Both will count from 255 to zero, then reload to 255 again. In the draw routine of your program you can use the current count value:

char text[20];
itoa(MIKEY.timer1.control2, text, 16);
tgi_outtextxy(10, 0, text);
itoa(MIKEY.timer3.control2, text, 16);
tgi_outtextxy(20, 0, text);
itoa(MIKEY.timer5.control2, text, 16);
tgi_outtextxy(30, 0, text);

You can also read and dump the other control bytes to the screen and see how they behave. Here’s a screenshot of what is included in the sample code for this part.


You can look at the code at what this does and change it around to do some experiments. The line that says Timer5 done uses this piece of code:

tgi_outtextxy(95,70, (MIKEY.timer5.control2 & 0x08) > 0 ? “Yes” : “No”)

to mask out the Timer Done bit with the 0x08 (bit 3 of CTLB) .

A short remark on interrupts

In the Lynx interrupts are always (always) caused by timers. Keyboard and IO never generate them. The video related interrupt (HBL and VBL), plus the ComLynx interrupts for TX buffer ready and received char are generated by timers 0, 2 and 4. Each of these interrupts is enabled by setting bit 7 of the respective timer’s static control byte CTLA.

When we get to interrupts we will revisit timers and look how the interrupts are generated by them. It is probably the most relevant function of a timer, as timers keep ticking regardless of what code is executing. Interrupts fit nicely into the picture and give the timers a purpose and good use. Without interrupts timer might not be as useful.

Right now you can enable the interrupts, but cannot handle them without knowing how to program an interrupt in CC65 (and assembler code). We will get there, don’t worry.

Next time

The next tutorial part returns us to the ComLynx functionality. We will dive into the ComLynx driver and how it can be used to transfer data across Lynx consoles, from Lynx to PC and vice versa. We needed this detour to timers to understand how timer 4 is used and can be configured. Till next time.

This entry was posted in Tutorial. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s