Delays, timeouts and timing requirements in programming practice
When developing firmware for microcontrollers, a very common necessity is a time delay. Delays are often required because a certain action cannot take place too close to another, to debounce switches, to generate a timeout, etc.
The most common way to implement a delay is by just a routine that is inserted in some place inside the code. The disavantage of such implementation is that routine is blocking. When the program flow reaches the point where the delay is inserted, the program flow will stop waiting for some time to be elapsed. It could be microseconds, or milliseconds. Some delays may require hundreds of milliseconds. Sometimes it could be necessary the delay last for a couple of seconds. The program flow is just blocked, doesn't perform anything else, doesn't process any part of the entire software that could need attention. The delay routine can be arranged to be more complex; more than one delay routine can be implemented. For example, a macro, or a function, can be created where the argument is the number of instructions. Then the main code can invoke it. In the C programming language we would write:
delay(50); // this delay is 50 instructions long
Or a function can be created where the argument is the time expressed in microseconds or in milliseconds. In this case the system clock frequency has to be taken into consideration. The oscillator clock frequency needs to be defined by the user application (and must be that really used by the hardware; each project is required to specify its own clock frequency).
#define F_OSC 8000000 // system clock is 8 MHz
The delay function will perform a calculation with that frequency define and will generate a delay based on the argument passed by the caller (the calling function):
delay_ms(20); // perform a delay 20 ms long
However, everything discussed so far will always be a blocking implementation. Have you ever seen some machine with a display that works fine but, when a button is pressed or some other action is undertaken, the display doesn't perform well anymore? It stops executing (stick around a screen), or it seems to be late, or behaves in some not optimal way. If you are a programming engineer, you may have thought: "I know why that machine behaves like so!". Maybe it is because of an interrupt; maybe it is because a delay is inserted in the flow of the program and the program stops execution for a little (or not so little) while.
For many microcontroller tasks it would be desirable that the program flow never stops. It would be desirable that the flow doesn't stick around a delay, but always continues with an uninterrupted processing of the main code. Delay routines are useful and a necessity in some cases, however there is another feature that can be implemented as a handy solution.
A timer is required. The timer will be configured (prescaler, etc.) so as to increment its count every a precise period of time. In the following example we will consider 1 ms. Every millisecond the hardware increments the timer count by one. All functions must never write to the timer register. Any function can read the timer register at any time. Then, each task having a timing requirement shall have a count variable. The task count is compared with the millisecond count and a decision is made. If the required time has elapsed, the function executes the task, and reloads the task counter. If the time has not elapsed, the function is exited. The program doesn't wait for a delay and doesn't get blocked there. It just continues with another task. In this way the program is always kept running and doesn't stick around any particular task. Let us have an example of the whole thing.
The application has 2 switches and we want to debounce them for a period of 50 ms: once a switch has been pressed, the program will not consider any switch action within 50 ms. Then the application drives a device that must not be operated more frequently than once every 600 ms. Furthermore, another task needs a delay of at least 280 ms.
#define DEB_SW 50
#define DEVICE_A_DELAY 600
#define TASK_B_DELAY 280
// a 16-bit timer is considered here, so let us declare some 16-bit variables:
uint16_t sw1_count, sw2_count, device_A_count, task_B_count;
//--- INIT --------------------------------------------------------------
// configure a timer for incrementing its count every 1 ms (clock
// frequency, prescaler, etc. must be taken into account)
// start the timer (suppose the timer register is defined TIMER)
//--- MAIN CODE ---------------------------------------------------------
//--- SW_1 TASK ----------------------------------------------
if ((TIMER - sw1_count) >= DEB_SW) { // if time not elapsed, continue!!!
if (condition for sw1) { // if condition not met, nothing happens
... do something
sw1_count = TIMER; // reload the sw_1 counter
}
}
//--- SW_2 TASK ----------------------------------------------
if ((TIMER - sw2_count) >= DEB_SW) {
if (condition for sw2) { // both conditions must be true for doing something
... do something
sw2_count = TIMER; // reload sw_2 counter; next time the program
// encounters this task, it will not be executed,
} // if 50 ms have not elapsed
}
//--- DEVICE_A TASK ------------------------------------------
if ((TIMER - device_A_count) >= DEVICE_A_DELAY) {
if (condition for device A) {
... do something
device_A_count = TIMER; // reload device_A counter; the action will
} // not take place again within 600 ms
}
//--- TASK_B -------------------------------------------------
if ((TIMER - task_B_count) >= TASK_B_DELAY) {
if (condition for task B) {
... do something
task_B_count = TIMER; // reload task_B counter; in the next 280 ms
} // task_B will not be executed
}
Note that just one timer will serve for whatever number of tasks.
What about when the timer overflows? Will the computation be corrupted? It will not, as long as all the involved variables are unsigned integers of the same length. Example: timeout is 7 ms, and task count has been reloaded with 65530:
--------------------- Timer count Task count TIMER minus task_count
---------------------- 0xFFFA 65530 65523 7 task executed
---------------------- 0xFFFB 65531 65530 1
---------------------- .... .... ....
---------------------- 0xFFFF 65535 65530 5
timer overflow -----> 0x0000 00000 65530 6 <--- correct!!!!
---------------------- 0x0001 00001 65530 7 task executed
---------------------- 0x0002 00002 00001 1
At the beginning we talked about declaring a count variable for each task. Should those variables be initialized to a known value? It is not necessary. In general, we don't know the relationship between the timer count and the first instance of a task action. The first time a timing is processed, we don't know if the task count is less than the timer count. It can happen that the first instance will not be executed immediately. In the example of the 600 ms device, the first execution can be delayed up to 600 ms. The starting value of the task count can be disregarded and the starting value of the timer can be disregarded as well. There is a risk, though. The main program might process a piece of code always, or almost always, when a task count is less than timer count. In that case the task would never be executed, or be executed following a broadly incorrect pattern. With a 16-bit timer incrementing once per ms, the timer overflows in about 65 s. For a task period up to a few seconds, it is unlike for the task to remain in a stall condition. General rule: an application will work fine as long as the required timing is a small portion of the timer overflow period.
Sometimes an application may need to perform a very important action, and blocking is not a concern. The aforementioned implementation will resolve this case very well. Suppose the application wants to communicate with a peripheral. It needs to know if the peripheral is available, and this is done through a dedicated function. The peripheral can take up to 2 seconds before starting a communication.
uint8_t per_action (void) { // return type could be bool
uint16_t time_out;
time_out = TIMER;
while ((TIMER - time_out) < 2000) { // keep on trying for 2 seconds
if (! got the anwser) {
... wait for an answer
}
else {
return GOOD;
}
}
return BAD; // inform the application that the peripheral is not available
}
There is another type of timeout, that works in the opposite way. Suppose there is a task requiring to be executed every 100 ms. That goal can be easily achieved with an interrupt, however there may be applications where it is not convenient or not possible to use an interrupt.
//--- RECURRENT TASK -----------------------------------------
uint16_t my_timeout; // static variable
if ((TIMER - my_timeout) >= 100) {
... do something // no condition here, task is always executed
my_timeout = TIMER; // reload my_timeout counter
} // note: main code should poll often enough
The microcontroller resource utilized, for satisfying timing requirements as discussed in these pages, is a timer. The benefit of this implementation resides in the fact that the timing count is taken directly from the timer, and no other resource is requested. Because many microcontrollers have at least 3 timers, the use of a timer is worthwhile.
FURTHER CONSIDERATIONS
The choice of a practical clock frequency has not yet been discussed. With frequencies in the range of several megahertz, a high prescaler ratio is required, for example 256. And some timer modules also have the option for a 1024 prescaler ratio. Although some applications can be implemented with an 8-bit timer, to achieve good results most projects will require a 16-bit timer.
In the phase of designing the schematics and providing for components selection, the difficulty which very likely can arise is that the divider stages, prescaler included, are not enough for scaling down the oscillator frequency. A solution can be to use a processor with a 16-bit timer split in two one-byte registers. Suppose to call them TMR_H and TMR_L. TMR_H and TMR_L are 8-bit registers that can be accessed separately, both in writing and in reading. Only the value read in TMR_H is utilized for our purpose, and TMR_L functions as a divider by 256. Using the prescaler too, the overall dividing ratio can become as high as 256 x 256 = 65536, which will be enough for whatever project. Some microcontrollers require special attention when a 16-bit timer is accessed byte by byte. The datasheet has to be consulted carefully.
Choosing a quartz with a frequency value multiple of megahertz (4, 12, 16 MHz, for example) may not bring to the generation of the desired timing. Other values, perhaps less common, have to be chosen. Example given: with a 32 prescaler ratio, TMR_L utilized as a 256 divider, and a clock frequency of 8.192 MHz, we can generate the millisecond timing:
8,192,000 / 32 / 256 = 1,000 Hz T = 1 / f = 1 / 1,000 = 0.001 s = 1 ms
Maximum task count will be limited to 255 ms. The general rule applies.
When the required timing cannot be achieved through the clock-dividers-prescaler-timer chain, still there is another programming tip to speed up the development of code lines. We will place a define that adjusts the timer count to the desired timing. Let us keep our beloved millisecond:
#define MILLISEC_K (F_OSC / 256000UL) // MILLISEC_K is a coefficient
F_OSC is the system clock
256,000 ---> 256 * 1000, where 256 is the prescaler ratio and 1000 is the frequency corresponding to 1 ms
Example:
clock frequency = 8 MHz
prescaler ratio = 256
timer count period: 0.001 s 1 / 0.001 = 1,000
256 * 1,000 = 256,000
8,000,000 / 256,000 = 31.25 at compile time, the compiler will truncate this value to 31
31 is a coefficient. Every value, intended as milliseconds, has to be multiplied by 31, so the correct timing will be generated. A small difference due to rounding exists. For most applications that will not be a problem at all. Now the code can be written expressing milliseconds in an immediate understandable form:
//--- TASK B -------------------------------------------------
if ((TIMER - B_count) >= (12 * MILLISEC_K)) { // 12 ms are needed
if (condition for task B) {
... do something
B_count = TIMER;
}
}
//--- TASK C -------------------------------------------------
if ((TIMER - C_count) >= (150 * MILLISEC_K)) { // 150 ms are needed
if (condition for task C) {
... do something
C_count = TIMER;
}
}
//--- TASK D -------------------------------------------------
uint16_t var; // the value in var cannot be too large
....
if ((TIMER - D_count) >= (var * MILLISEC_K)) { // <-- run-time multiplication
if (condition for task D) {
... do something
D_count = TIMER;
}
}
In Task B and C, (VALUE * MILLISEC_K) is solved at compile time. A constant value will be placed in code. Task D is different. Because one of the two operands is a variable, a run-time multiplication has to be performed. The code can be slightly larger. Again, that should not represent a problem in most cases. The var value cannot be too large, not just to avoid a 16-bit overflow in the multiplication, rather for the general rule stated above.
Some modern microcontrollers have 16-bit timers that can be paired to function as a 32-bit timer. When working at high clock frequency, a demanding project can take advantage of a 32-bit timer. Task count variables must be declared as unsigned 32-bit integers.
ANOTHER SOLUTION
The coefficient tip should work fine for many applications. When none of the above solutions is feasible, a millisecond count can be generated in interrupt with the compare feature of a timer.
The timer count is automatically compared, by hardware, to that of a Compare register. When a match is found, an interrupt is issued and the timer count is reset. In this way, the desired time period can be generated by selecting an appropriate value to store in the Compare register. For many microcontrollers the formula for calculating the output frequency is:
Tosc / [PS * (Comp + 1)]
Tosc = timer clock input
PS = prescaler ratio
Comp = Compare register value
The timer input frequency may not be the oscillator frequency. Perhaps the timer gets the oscillator frequency divided by 2 or by 4. Example:
Fosc = 16 MHz Tosc is Fosc / 2
Tosc = 8 MHz
Prescaler ratio: 64
the Compare register is loaded with the value 124
Tosc / [PS * (Comp + 1)] = 8,000,000 / [64 * (124 + 1)] = 1,000 Hz
T = 1 / f = 1 / 1000 = 1 ms every millisecond the Compare match will trigger an interrupt
C code would look like so:
uint16_t ms_count; // global variable; volatile qualifier not strictly required
//--- INIT ---------------------------------------------------
COMP_REG = 124; // load the compare register with the correct value
// configure a timer with a prescaler ratio of 64; start the timer
// enable Timer Compare interrupt
// enable global interrupt
//--- INTERRUPT SERVICE ROUTINE ------------------------------
void interrupt_Comp (void) { // the exact spelling is compiler dependent
ms_count++; // every millisecond the variable is incremented by one
}
No main code function is allowed to write to ms_count. Any function can read ms_count at any time.
//--- TASK A -------------------------------------------------
if ((ms_count - A_count) >= 40)) { // 40 ms are required
if (condition for task A) {
... do something
A_count = ms_count;
}
}
Tasks are implemented in the same way as above.