I wrote this library some time ago, after repeatedly running out of hardware timers even for simple and small projects. I noticed that I often needed something like "Get back again after 200 milliseconds.". Another class of timing events is like "Watch out for this area each 250 milliseconds.".
Based on my FreeBSD background, I decided that my project(s) needed some simple timer queue management for medium to low resolution timing purposes. (Microsecond timing is explicitly not covered by this. It can be done by either one of the remaining hardware timer channels, or perhaps also by simple delay loops, see <avr/delay.h>.) The main idea for such a timer management is that one of the timers is used as a "hardware tick", triggering the timer queue manager on each tick, which will in turn walk the timer queue, and see which events to fire that time. Any triggered event will then call a callback function that has been registered when starting the timer. In case a periodic event is desired, it's then the responsibility of the callback function to re-trigger the event. This is a simplified event management as can be found on any modern operating system, which includes those tiny real-time OSes for the AVR as well. The timer library discussed here is merely intended for projects where a full RTOS is not being considered.
Note that the hardware timer used to trigger the timer queue management not necessarily needs to be dedicated to the queue management. In particular, if the application already requires a PWM channel anyway, the very same timer could perhaps be used for this timer library (using the overflow interrupt).
There are two basic options to run such a timer queue:
Option #2 above is the preferable way, as it is less prone to interrupt congestion in case some of the callback functions consumes too much time. If one timer tick gets missed, it will simply ignored, but the remainder of the application will at least continue to work. The basic issue with this approach is that the main loop must run quick enough to ensure that the interrupt flag bit is (normally) recognized quick enough.
Option #1, while it seems to be the more obvious solution at first, is much more problematic. It contradicts the common principle to keep interrupt service routines as short as possible. If at all, the timer queue manager should at least be run with interrupts already re-enabled by that time, so other interrupts get a chance to fire while a callback function is being processed. Obviously, this risks endless nesting of timer interrupt handling in case one of the callback functions hogs CPU time.
This implementation now supports both of the options above. If option #1 (called directly from the timer ISR) needs to be chosen, the library ought to be compiled with the macro TMR_INTPROTECT being set (e.g. using the compiler's command-line option -DTMR_INTPROTECT). This will compile the library in a way where interrupts are being disabled when entering critical regions. For option #2 above, this measure is not necessary (saving code and execution time as well) as the queue management would never happen from inside an interrupt routine anyway.
The interface to all functions described below, and the type definitions can be found in the header file timer.h. The implementation itself is performed in timer.c which needs to be linked to the application.
union timeoutarg { uint16_t i; void *p; };
This is the type of the argument that can passed to the callback function. It needs to be given when calling timeout(), and will be passed verbatim to the callback routine. A union has been chosen so simple arguments can be passed as integers, while not limiting arbitrarily complex argument passing, by using a pointer to whatever information needs to be passed down.
typedef uint32_t time_t;
This is our basic unit of time, measured in (hardware) timer ticks. Note that for a tick interval of 10 ms, the clock will overrun after 49 days. (Using 64-bit data types would have avoided this problem, but caused much more bloat.) All timeouts given to timeout() are of type time_t. Calculation of "real" times is usually done by C preprocessing then.
typedef void (*timeout_t)(union timeoutarg);
The data type of the callback function. Any callback function takes a single union timeoutarg argument as discussed above, and cannot return anything as there is no environment to return something to. If data needs to be passed back, it must either be done using global variables, or by means of the union timeoutarg argument.
void *timeout(time_t tm, timeout_t fun, union timeoutarg arg);
Register a one-shot timer, to trigger after tm timer ticks, calling callback function fun with argument arg.
This function will return a handle that can be remembered in case there is desire to possibly unregister this event later.
If malloc() failed when allocating the new queue entry, a NULL pointer will be returned.
int untimeout(void *handle);
Unregister a timer event, removing it from the queue. Argument is the pointer as returned by timeout() before. Unregistering can be done anytime after the event has registered, and before it has been triggered. Attempts to unregister an event that has already been triggered are benign, but will waste CPU time (to find out about there's actually nothing to do).
On success (entry for handle found and removed from the queue), 0 will be returned. On failure, -1 will be returned.
void timertick(void);
This is the housekeeping function that manages the timer queue, to be called from the timer's interrupt service, or from the application's main loop after the hardware timer has triggered (see options above).
This is a small part from a real project that is using this library. It contains examples for the preprocessor calculations, as well as for re-triggering timer events and argument passing.
volatile struct { /* hardware interrupts */ uint8_t tmr2_int: 1; /* ... */ } intbits; #if SYSCLK == 14745600L /* * 14.7456 MHz clk / (TMCON1 * TMCON2) = 200 Hz soft interrupt rate */ #define TMCON1 144 #define TMCON2 512 #define TICKRATE 200L SIGNAL(SIG_OUTPUT_COMPARE2) { #if TMCON2 >= 256 static uint16_t ticks; #else static uint8_t ticks; #endif if (++ticks == TMCON2) { intbits.tmr2_int = 1; ticks = 0; } } /* ... */ void calibrate(union timeoutarg arg) { /* ... */ if (arg.i == 0) { /* ... */ arg.i = 1; } /* else ... */ timeout(TICKRATE / 10, calibrate, arg); } void startcalib(union timeoutarg arg) { void **argp = arg.p; /* * If a timeout handle has been passed, clear it to indicate the * timeout has hit. */ if (argp) *argp = 0; /* ... */ timeout(TICKRATE / 100, calibrate, arg); } int main(void) { union timeoutarg u; /* ... */ /* get things going... */ u.i = 0; timeout(TICKRATE, startcalib, u); for (;;) { if (intbits.tmr2_int) { intbits.tmr2_int = 0; timertick(); } /* ... */ } /* NOTREACHED */ return 0; }
This only outlines the main difference against the previous example: timertick() is called directly from within the ISR, after re-enabling interrupts. Nothing is required in the main loop.
SIGNAL(SIG_OUTPUT_COMPARE2) { #if TMCON2 >= 256 static uint16_t ticks; #else static uint8_t ticks; #endif if (++ticks == TMCON2) { ticks = 0; sei(); timertick(); } }
sent the following remark:
One change that I would make is rather than a bit flag in the IRQ, is to use a counter. I've found this works better over the years.
The IRQ counts up. The main loop then does a
while (--counter > 0)operation. This prevents ticks from being lost. Of course you need proper protections for decrementing the counter outside of the IRQ etc.