REAL-TIME/EMBEDDED SYSTEMS


A Polled Timer Class

J. David Wendel

"Real time" means "soon enough." Sometimes even MS-DOS qualifies as a real-time system if it can react soon enough.


Polling is generally not as robust as interrupt handling in detecting real-time events, but where timing requirements are somewhat relaxed, polling has some advantages: the code is conceptually simple; there are no interrupt masks or event handlers to set (which typically requires a bothersome excursion into assembly language); and the execution path of the program is more predictable.

We have recently completed a project that puts polling to good use. PCs make up the target for this prototype system, and they have plenty of processing power for what we want to do. We have plenty of buffers on all our I/O ports, so servicing them, while important, is not time critical. As long as we get to the ports within 10-20 ms we are okay. In fact, everything that happens can tolerate innaccuracies on the order of tens of milleseconds. Given this generous time allotment, we have opted for simplicity above all else.

On this prototype system we run a DOS program with a single thread of execution. It contains a main executive loop that scans all the different events possible, and just services them as they happen. This architecture is similar to a Macintosh program's "Main Event Loop," the difference being that in our loop, any response to any event returns to the executive within about 5ms. Actually, only the display routines take that long, everything else returns to the executive in less than a millisecond. So with some extra logic on the display code to make sure it doesn't use all our time, the rest of the system runs with no timing problems.

One of the key parts of this polled executive loop is a timer that is suited to a polled environment. The simplest interface we can conceive of is that of a single timer object. Anyone who needs a timer can "open" one through this timer object, use the timer, and "close" it. Only one timer object is created at the beginning of the run, but many timers can be opened and closed through this object as needed. We chose this approach over using one object per timer because we did not want to be creating and destroying objects in real time. This article shows how we implemented this multi-timer object.

Polled Timing Requirements

Because we are running in DOS, memory is limited and all the timer routines must be as small as possible. Also, to keep the code simple and easy to debug, we want to keep all our code in high-level C or C++. We would normally use hardware-level timers for our required watchdog functions, coded in small assembly-language routines. But since our host compiler (Borland 4.02) provides a BIOS call to the system clock that fits our purpose perfectly, we don't need any assembly-language routines at all for our timer.

Our system requirements are rather straightforward. Our system has three serial ports, an ARINC-429 port, and some TTL output lines to be driven. Two of the serial ports receive messages that must be handled within a certain time after some other event has occurred — hence our need for multiple timers. We use off-the-shelf boards for the different interfaces and use the DOS drivers supplied. Fortunately, these drivers have functions that provide immediate Ready/Not-Ready status, precluding the need for interrupts or the wait routines common to multiprocessing systems.

Our system runs in one-second cycles (epochs) and must receive specific messages within certain parts of the epoch. We must set a timer to raise an alarm if we fail to receive a message in a certain amount of time. Since it is difficult to predict when a message will arrive at a serial port, we allow a window of about 0.3 second to receive a message.

Since all our timing is on the order of tenths of seconds, the resolution of the PC system tick (18.2 Hz = 54ms) is sufficient for our timer. Note that there are ways to alter the system tick frequency up to 60 Hz (or higher). Changing the system tick frequency to 60 Hz would give a 16ms resolution. The changes to the timer code are easy to make, but then the time-of-day clock on the PC speeds up too.

The Timer Implementation

The header file for the timer appears in Listing 1. The Open procedure returns a handle subsequently used to refer to that timer. The Close procedure releases the handle for later reuse. The global constant max_timers specifies how many timers the TIMER class can manage. It is currently set to 10. This number can be set higher, but be aware that the higher it is, the longer it takes Open to find an unused handle.

While the timer is open, calling the expired function tells if the timer has expired, and calling time_left returns the number of seconds (as a floating-point value) left before the timer expires.

The executive_tic procedure is probably the hardest one to understand. This procedure updates the timer counters and makes the timers function. executive_tic is the routine that uses the BIOS call supplied by Borland. This BIOS call gets the system tick count, which changes every 54ms. So just comparing it to the previous value will determine if a tick has occurred. When a tick has occurred, executive_tic increments the counter value.

Since ours is a totally polled timer system, executive_tic must be called at least once every system time tic. On a normal PC, the system time ticks at 18.2 Hz. We simply place executive_tic at the top of our main executive loop. Since the rest of the system is polled as well, executive_tic is usually called hundreds of times more than needed.

Listing 2 shows the TIMER class implementation. The constructor sets count_mod to 4,096. count_mod is the maximum value a timer can attain before it rolls over. It is necessary to reduce the timer counter modulo some value, since in an embedded system you don't want any counter marching off to infinity — bad things happen when registers overflow. 4,096 is a nice power of two, but 4,096 / 18.2Hz = 225.05 seconds = 3.75 minutes. So the longest timer we can make runs for about 3.75 minutes. If you need longer running timers you can increase this number.

The instantiated TIMER object uses a single counter, time_count, to track the passage of time. As new timers are opened, the TIMER object retrieves this time count, computes the opened timer's expiration time, and stores it for that timer.

The expired procedure determines when one of the opened timers has expired. Conceptually, an opened timer expires when the TIMER object's time count exceeds the opened timer's expiration value. Actually, it's a little more complicated than that, due to the time count's wrap-around behavior. The difference

time_count - expire_time[handle]

is positive (indicating expiration) only periodically. Even if a timer has expired, as soon as the time count wraps to zero this difference will become negative and give a false indication. The interval during which this difference will correctly indicate expiration depends upon the value stored in expire_time[handle], and this interval can conceivably be zero. To correct this problem, we've modified the above formula. Function expired uses the conditional expression:

(time_count - expire_time[handle] + count_mod) % count_mod > 36

This expression gives user code a two-second window after a timer expires in which to call expired and correctly detect expiration. The expression still works on a periodic basis, but now the valid detection window is a guaranteed interval.

The open_timer procedure is very simple. It takes seconds as a parameter, indicating how long before the timer expires. open_timer searches for a free timer, flags the timer as used, and computes the expiration count for the timer. You can also open an expired timer (yes, there are times you'll want to do that) passing it a parameter of 0.0 seconds. Each timer has a resolution of 1/18.2 = 0.055 seconds.

Timers can be opened for several seconds or just fractions of seconds. Note the variable search_next_handle_start, declared private in the TIMER class (Listing 1) . We added this variable after discovering how frequently in our coding we needed to close a timer and then immediately reopen it. Rather than always searching for a new handle from scratch, open_timer first uses the handle stored in search_next_handle_start to open the timer that was just closed. Of course, if more than one timer must be opened, the most recently closed timer is used first, then subsequent timer opens do a search from scratch.

The close_timer procedure just frees up the timer handle, to be used elsewhere.

Using The Timer

To use the timer, create a single TIMER object, then open as many timers (up to max_timers) as you need. Timers may be opened and closed in any order or combination.

Listing 3 shows a stripped-down version of the executive used on our project. It shows how we used the timers in combination with the executive loop and function executive_tic. This stripped-down version shows just one serial port being serviced, but the other serial ports are handled similarly.

The example code calls the request_ephemeris routine every two minutes and uses a one-second timer to send a "ping" message to another machine every second. The rest of the executive handles the message sequence for type1 messages.

A message sequence starts by checking for occurrence of a top of second event in a GPS receiver. The top_of_second function returns true if a top of second has passed; the function returns false on subsequent calls. All routines that check different ports are latched in this manner. When a top of second occurs we expect to receive a preliminary message at the port within 0.3 second. Therefore, we set a timer for 0.3 seconds. When the preliminary message is received we expect the final message to come within the next 0.3 second, so we reset the timer to 0.3 to wait for the final message. If the timer expires before we receive a message then we have "missed" the message and an error message is reported.

We could have added a reset function to our timer object This function would just reset the timer when it expired to go again for the same interval. In this example, we accomplish a reset by closing the timer and immediately reopening it. This technique crops up quite a bit in our example. A true reset would be easy enough to do, but we leave it as an exercise.

Also, at the end of Listing 3, we implement a delay function. This function just repeatedly calls executive_tic until the delay timer expires. The timer interface provided by our TIMER object makes this function very clean and easy to follow. We use this function quite a bit at startup. One of our devices must receive several initialization strings upon startup, and it needs about half a second to digest each string before it is ready for the next. The delay function fills that need just fine.

Polled Timers in Practice

The system we constructed was a prototype GPS Category III aircraft landing system. As it was a proof-of-concept model, we wanted to build it as quickly and cheaply as possible. We chose PCs as the target machines and Borland 4.02, since we already had it in house. We had some initial concerns about using PCs, since they are not usually considered embedded machines. Also, DOS isn't considered a real-time operating system and we were concerned that DOS itself would sabotage any attempt to produce a real-time system. But we found that by altering AUTOEXEC.BAT and eliminating TSRs, we could use DOS as a program loader and use its disk access routines for storing our data.

Our system consisted of three PCs, two of them talking to each other over a RS-232 line, and the third (the airborne unit) receiving transmissions over a datalink from the first two machines on the ground. All three of the machines were receiving measurement data from their own GPS receivers over another RS-232 port, and the interface to the transmitter and receiver of the datalink were over yet another RS-232 port. After we had all three machines using the timer and executive routines described here, our integration and testing went rather smoothly (as smoothly as an embedded system can go).

The airborne unit had some more rigorous timing constraints. We ended up using an external 15Hz clock synced to GPS time. We were still able to use our timer we had an interrupt handler respond to the 15Hz pulse and just increment the TIMER counter. The interrupt handler merely replaced the executive_tic routine, and the rest of it worked as is (after changing all instances of 18.2s to 15.0s).

The finished system was able to do a lot of crunch work and update the displays, and it lost no messages due to inattention of the processors. Since then I've worked on other projects that use genuine multiprocessing operating systems. While their kernels are nice and their task priorities are proper, I still find myself wishing we could just do it on PCs with this little executive and timer. o

David Wendel has a B.S. in math from Brigham Young University and an M.S. in math from the University of Utah. He has spent seven years as an embedded systems programmer. Four of those years were at Eyring Corporation, coding in Ada for the Navy's SH-2g helicopter simulator. The last three years have been at Raytheon Aircraft Montek Company, coding GPS aircraft landing systems for the FAA and the Navy in C/C++. He can be reached at dwendel@montek.com.