A MILLISECOND TIMER
BY ROBERT H. OSNESS
Bob Osness, who has been programming his ST for 2½ years, works as an electrical engineer for Boeing Aerospace in Kent, Washington. He and his wife, Georgia, spoil grandchildren as a hobby.
Being able to measure time accurately in a software program can be helpful and sometimes essential. For real-time applications dealing with multiple events occurring outside the computer, it can be critical to know the exact time when those events occur. One example is in MIDI applications dealing with the time of occurrence of note-on or note-off commands. Unfortunately, it isn't always obvious just how to go about measuring time, especially if one isn't familiar with the details of the ST's hardware.
Real-time programs aren't the only ones that can make use of time-measurement techniques, though. Consider how nice it would be to have an easy way to program precise time delays or to measure the time required to execute a critical path in the new program you've just completed. Another useful software development tool might allow you to scatter markers through a troublesome program, giving a readout of the elapsed execution time between markers. In this article, we will develop these tools and provide the basis for others.
With one exception, the program functions are written in C, but they are well commented, which will allow you to transfer the ideas to the language of your choice. The millisecond timer's interrupt-service routine is the exception. It is written in 68000 assembly code because it is required in order to run in supervisor mode and must return using the privileged RTE instruction. All code was written using the Alcyon C Compiler included with the Atari Developer's Package, but should be adaptable to other C compilers with few if any changes.
What time is it?
The ST contains a real-time clock, but the clock's time values are accumulated in increments of two seconds—not a very useful resolution for events happening at electronic speeds! Unbeknownst to many of us, however, the designers of the ST cleverly included a 68901 multi-function peripheral (MFP) chip, which has four (count 'em) programmable timers. Most of these are allocated to various system functions, but one, Timer A, has been left for applications—that's us, folks!
When enabled, Timer A will generate an interrupt after a programmable number of cycles of its 2.4576 MHz input clock have elapsed, then reload itself for another timing cycle. Our program uses the ST's XBIOS functions Xbtimer, jenbint and Jdisint to set up the timer and to enable or disable the associated interrupt. We program the timer to generate an interrupt every 1.0 millisecond, and use the interrupt to increment a memory word used as a counter. Reading the counter allows us to measure time directly in milliseconds.
It's possible to set up the timer to interrupt more frequently than once every millisecond, but it's not very desirable, because that could slow the ST in performing its other tasks. If greater timing resolution is necessary, there are better ways to get it as a future article will show.
The timer ISR
Where there are interrupts, there must be ISR's—that is, interrupt service routines. These are just like subroutines or function calls, except that a branch to the code they contain is executed when the corresponding interrupt occurs. The ISR then restores control to the normal program, wherever it was when the interrupt struck.
Listing 1 contains the assembly code for __intr, the millisecond timer's ISR. It is intentionally very short, to minimize system delays. All that happens is that the global variable__timcnt is incremented, and then the interrupt in-service bit is cleared before returning to the main program. The variable__timcnt contains the number of elapsed milliseconds since it was last initialized or cleared. It is declared as a long integer to allow time values greater than 65 seconds to be accumulated, which would be the limit for a regular 16-bit integer. Note that no register contents need to be saved in this ISR, because none are used.
The underscore must appear in front of the variables __timcnt and __intr for compatibility with the C compiler, which adds underscores to external variables. The variables __timcnt and __intr are declared as globals because the ISR is to be linked with the C code as a separately compiled module. The underscores are not used in the C source code.
Listing 2 contains the tools that expand the horizons of the millisecond timer into the real world. Let's skip over the main program segment for a moment and look at the subroutine functions.
Let's get started
The function init__tmr() is used to set up the timer. Jdisint is an XBIOS function that disables Timer A's interrupt. Xbtimer is then called to set up the timer's prescale divider ratio, countdown value and ISR vector—that is, the ISR's address.
Jenabint is called to enable the timer's interrupt, and the millisecond timer is off and running! For those who want to know more of the hardware details, there are specifications for the timer in the 68901 data section of the Atari Developer's Package, beginning on page 984.
Next, let's look at the function wait__ms(ms). By passing a long-integer millisecond value to the function, we cause it to enter a timing loop, where it remains until the specified number of milliseconds have elapsed. Simple, isn't it?
There is a similar function for longer delays, wait__sec(sec). It is the same, except that a long integer value for seconds of delay is passed.
Now for a little more fun. To measure the execution time between two points in a program, we can use the function celc__ms(). At the first of the two measurement points, we simply insert the statement tl = timcnt, where tl is a long integer. Then, at the second measurement point, we insert a call to calc__ms(), passing the tl and the string "since tl." The subroutine will measure the elapsed time, and will print the result to the screen as "Elapsed time = xxx milliseconds since tl," where the last two words are the string passed by the calling routine.
Successive calls can use additional declared variables t2, t3, etc. The time difference is also returned as an 16-bit integer, which may be used or ignored. Note that the variable t2 used inside this subroutine is local and is not the same as the variable t2 used in the main program.
The main thing
Now let's look at how main() tests the timing subroutines. First we initialize the timer with a call to init__timer(). Next comes a while loop, where we will remain until typing a "q" to quit. The first printf statement prints the counter value, which should be 0 because the timer has just been started. Next is a 100-millisecond delay, then another Printf statement to show how much time has elapsed to this point.
Now comes a combined test of the millisecond and second delay subroutines. The time is sampled in t1, then delays of ten seconds and 250 milliseconds are called, and finally calc__ms() is called to measure the elapsed time since t1.
Next is a test of a the execution time for a dummy for loop, which executes 9,000 times. The value of t2 is used to indicate this result.
Last, another call to calc__ms() is used, with a zero in place of the identification string. This is just a variation in how the subroutine can be used if no id is needed.
The end of the loop has now been reached, and the user can enter a "q" to quit or repeat the sequence as many times as desired.
Let's build the program
Disk subscribers may now gloat, get out their program disks and skip the remainder of this section. Everyone else, please follow along!
Begin by typing in the timer ISR in Listing 1. Using the assembler disk in Drive A and your working disk in Drive B, assemble it using the batch file of Listing 3, BI2.BAT.
Now type in the C program TIMERS.C from Listing 2, and compile and link it using the batch file from Listing 4, BINT.BAT. Note the inclusion of "b:intr" in the link statement, to link in the object file INT.O generated in the preceding step. Both of the batch files can be adapted to RAMdisk with minor modifications in order to speed assembly if desired.
Time to fly!
All that remains is to watch time fly. Click on TIMERS.PRG and we're off. In about 10.5 seconds, the first pass through the main loop will be complete, and we can look at the results.
At first glance, our 100-millisecond delay seems too long. Actually, the additional delay indicated on the second line of output is caused by the execution time of the printf function. This is demonstrated when we look at the next line. Exactly 10,250 milliseconds (that's 10.250 seconds) are indicated for the execution time of the ten-second and 250-millisecond delays.
The fourth line of output indicates that our dummy for loop takes 75 or 76 milliseconds to execute: about 8.3 microseconds for each pass through its loop.
The fifth line shows that the use of calc__ms() with a zero string parameter works as desired. It also indicates a delay of about 24 to 56 milliseconds due to the Printf statement contained in the preceding call to calc__ms().
Repeating the main program loop shows that the use of printf inserts a variable delay of 18 to 56 milliseconds. Though this is really not a long time in the human world, it can be of some importance in code segments that have to be fast.
In a future article, we will take a look at some ways to get even more timing accuracy. Until then, you may want to expand the program and try some code timing of your own.