Delays, timeouts and timing requirements in programming practice

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.

To view or add a comment, sign in

More articles by Maurizio Pirazzini

  • Il codice ASCII

    ASCII è l'acronimo di American Standard Code for Information Interchange. Letteralmente: codice standard americano per…

  • L'andamento del coefficiente di temperatura nei termistori

    I termistori sono componenti elettrici nei quali la resistenza cambia notevolmente al variare della temperatura. In…

  • L'inverter trifase, ovvero come ti ricostruisco la sinusoide

    Lo stadio finale di un inverter trifase viene realizzato tipicamente con 6 transistor di potenza disposti a ponte…

  • I convertitori digitale/analogico

    Dopo che l'uso di convertitori analogico/digitale e digitale/analogico è divenuto comune, le case produttrici di…

  • Linee di trasmissione: formule e relazioni

    Posto: Otteniamo i seguenti parametri: Il loro range è: Il coefficiente di riflessione è un numero complesso: La sua…

  • Gli operatori Modulo e Remainder

    Modulo e Remainder sono operatori utilizzati in molti linguaggi di programmazione. Consideriamo una divisione fra…

  • FPGA, circuiti logici e dintorni

    Il mondo dell'elettronica comprende talmente svariati settori che non tutti riscuotono fortuna nel medesimo momento…

  • La tassa sulle vendite americana - 3

    In articoli precedenti si disse che l'istituzione di una tassa sulle vendite fu deliberata in diversi Stati della…

  • La tassa sulle vendite americana - 2

    La tassa sulle vendite istituita in diversi Stati della federazione statunitense, si applica alla "proprietà tangibile"…

  • La tassa sulle vendite americana

    Sebbene la Cina stia avanzando a grandi passi e si appresti a diventare la prima economia mondiale, gli Stati Uniti…

Insights from the community

Others also viewed

Explore topics