Necessary knowledge: [HW] Controller module, [AVR] Counters/Timers, [LIB] Pins, [LIB] Delay, [LIB] Timers
There is often a need to create delays in programs of microcontrollers, it is necessary to time the actions or wait them to end. By its concept one of the easiest methods for creating a break in the work of a microcontroller is overloading its processor with some alternative action – for example order it to count big numbers. From the stroke frequency of the processor can be calculated to which number it should count in order to get a certain time delay. Counting a number from zero up to the value of the stroke frequency of the processor in hertz-s, should theoretically create a delay for one second. For number of reasons this is not as simple in practice.
If the processor of the microcontroller calculates using numbers which’s binary form is as wide as its inner bus (AVR has 8 bits), then it takes one firing stroke of the processor to perform one arithmetical operation for example adding 1 to any value. In order to operate with thousands or millions the number has to be 16 or 32 bit and for an 8 bit processor it takes more than one firing stroke to calculate them. Hence when dealing with large numbers, one has to be familiar with the inside of the processor – exactly its commands.
When programming in advanced languages (C-language for example), the programs are not written directly on the basis of the command, in order to create a software delay, one needs to know also its compiler which converts the program to the machine code. From this depends how many instructions (and phases therefore) it takes to perform one arithmetical operation. Complexity is added by compilers ability to convert the program into the machine code in several ways – for example, by making the machine code as memory saving as possible or easily executable. Such compiler’s actions are called optimization. With different modes of optimization the software delay’s machine codes and their duration come out different.
The following is an example of generating software delay with AVR microcontroller. A part of a program in C-language is written, which counts the variable x of for-cycle from 0 to 100. Inside each cycle a no-action empty instruction is completed. It is needed, because if the content of the cycle is empty, the compiler optimizes it out of the program as it considers this to be useless.
unsigned char x; // Cycle until x is 100 for (x = 0; x < 100; x++) { // With empty instruction nop asm volatile ("nop"); }
This is the same part of a program after compiling. 2 hexadecimal numbers on the left is machine code and on the right is the command with operand(s) in assembler language. The machine code and the assembler language are conformal; the assembler is just for presenting the machine code for humans in a readable form. In compiling, the optimization of the length of the program is used (compiler’s parameter –Os).
80 e0 ldi r24, 0x00 ; r24 loading number 0 to the index 00 00 nop ; Empty operation 8f 5f subi r24, 0xFF ; subtracting 255 form the r24 index, that means adding +1 84 36 cpi r24, 0x64 ; comparing r24 index with number 100 e1 f7 brne .-8 ; If the comparison was wrong, then transfer 8-baits back
In the compiled form can be seen, what is actually happening with the cycle of the C-language and it can be used to calculate how many clock sycles is needed to complete a cycle of one period. The information about the effect of the instructions and operating time can be found from AVR’s instructions datasheet. In the given example, it takes 4 clock cycles to complete 4 instructions in one cycle period, because all instructions demand one clock rate. In addition, one clock rate is used before the cycle for loading instruction and afterwards one extra clock rate for exiting the cycle. By assuming, that the working clock rate of the controller is 14,7456 MHz, the whole delay produced by the program can be calculated.
(1 + 100 ⋅ 4 + 1) / 14745600 = 27,26 μs
The delay produced in the example, is in microseconds and the variable used is 8-bit so the machine code is fairly simple. In order to produce a break in millisecond, we need to count much larger numbers and thus the machine code gets longer. Cycles working inside each other may also be used, but with this method the delay is not in linear relation with the number of the cycles because with each level of cycle a small extra delay occurs.
The goal of this exercise is not creating precise software delay on the level of machine code, because it is quite an accurate work and we already have the functions necessary to produce delays in the avr-libc and in the library of the HomeLab. Those are used also in the following examples.
When dealing with the software delay it is important to know, that regardless its basic simplicity; it is extremely inefficient method from the power consumption point of view. During all of these clock rates when the microcontroller is counting the useless, energy is consumed. So if using applications operating on batteries, it is not wise to write long software delays. Wiser is to use hardware timers, which are working independently and wake the processor from hibernating when it is time to continue the work.
The software delay is not the only method for creating breaks. The same can be done using timer. Timer is a hardware which counts up or down with a certain frequency. The clock frequency of the timer can be generated from microcontroller’s frequency or from some kind of other outside pace. In general the clock frequency can be divided with a multiplier to reach a smaller frequency - this is done with prescaler. Important fact is that the fixed clock frequency timer’s value is linearly related to the time. The time can be calculated by multiplying the period of the clock frequency of the timer with the value of the timer.
AVR counters can be made to inform about overflow of the counter or achieving compare mach. Overflow happens when the counter has the maximal possible value and the cycle starts all over again form 0. With reaching a pre set value during the moment of growth of the counter’s value it is compared to the value given by the user. On the occurrence of the event, the bits in the status indexes of the AVR are automatically set as high.
For generating a delay using a timer, it is only necessary to set the timer and waiting for the status bit to go high. Different from the software delay, the work of the timer is not depending on the compiler, which makes them more reliable. At the same time the diversity (or complexity) of the set-up of the AVR counter can be considered fairly troublesome. Depending on the microcontroller’s timing signal, may happen that it will not divide exactly with the desired delay period and the delay will not be accurate.
The following code of a program is about software delay function sw_delay_ms , which makes a given delay in milliseconds using the parameter count. The function uses avr-libc library’s function _delay_ms which is partly written in assembler language. The reason why the _delay_ms is not used in this exercise immediately is that when using the _delay_ms problems may occur when the delays are long. The sw_delay_ms enables creating 65535 ms long delays without any complications.
// Software delay in milliseconds void sw_delay_ms(unsigned short count) { // Counting the variable of the delay to 0 while (count-- > 0) { // 1ms delay with a special function _delay_ms(1); } }
The following program is for using the given function, this creates two delays in the endless loop: 100 ms and 900 ms. During the shorter delay LED is lit and during the longer it is switched off, the result – the LED is blinking periodically
// The demonstration program of the software delay of the HomeLab // The program is blinking a LED for a moment after ~1 second #include <homelab/pin.h> #include <homelab/delay.h> // Main program int main(void) { // Setting the pin of the LED as output pin_setup_output(led_debug); // Endless loop while (true) { // Lighting the LED pin_clear(led_debug); // Software delay for 100 ms sw_delay_ms(100); // Switching off the LED pin_set(led_debug); // Software delay for 900 milliseconds sw_delay_ms(900); } }
Although it seems that the LED blinks in every 1 second, the time is actually a little bit longer, because the callouts of LED’s and delay functions are taking a couple of clock rates of the microcontroller
The program code below is a delay function based on a timer, which is simplified a little bit. The principle of counting is the same as it is at software delay function – a desired amount of 1 ms long delays are produced. The delay is produced with an 8-bit counter 0. It is calculated previously that at clock frequency 14,7456 Mhz the timing signal has to be divided at least 64 times, so that the counter would not reach to overflow in 1 ms. The value which the counter must have so that the overflow occurs after 1 ms is presented in the form of an expression and the variable is timer_start. F_CPU which is a constant in macro-language, that shows clock frequency in Hz. The clock frequency should be 25,6 at the moment but since fractions can not be used, the initial value will be set 26. Unfortunately here arises a mistake in delay time, however it is fairly small (-1,7 μs).
In the cycle takes place initialing of the counter and zeroing the flag of the overflow (by writing 1 into that). Then is waited until the counter counts to 256 from the initial value, i.e. to the overflow. At the moment of the overflow the flag goes high and the delay of 1 ms has taken place. In the end of the function the timer is stopped.
// Hardware delay in milliseconds void hw_delay_ms(unsigned short count) { // Calculating the initial value of the timer register unsigned char timer_start = 256 - F_CPU / 1000 / 64; // Starting the timer timer0_init_normal(TIMER0_PRESCALE_64); // Counting the variable of the delay to the 0 while (count-- > 0) { // Initializing the timer timer0_set_value(timer_start); // Zeroing the overflow flag timer0_overflow_flag_clear(); // Waiting for overflow while (!timer0_overflow_flag_is_set()) { asm volatile ("nop"); } } // Zeroing the overflow flag timer0_overflow_flag_clear(); // Stoping the timer timer0_stop(); }
Referenced delay function uses a timer library whose source code for ATmega controller looks like the following:
// Timer 0 prescaler selection type typedef enum { TIMER0_NO_PRESCALE = 0x01, TIMER0_PRESCALE_8 = 0x02, TIMER0_PRESCALE_32 = 0x03, TIMER0_PRESCALE_64 = 0x04, TIMER0_PRESCALE_128 = 0x05, TIMER0_PRESCALE_256 = 0x06, TIMER0_PRESCALE_1024 = 0x07 } timer0_prescale; // Setting Timer 0 to a normal mode inline void timer0_init_normal(timer0_prescale prescale) { TCCR0 = prescale & 0x07; } // Stopping the Taimer 0 inline void timer0_stop() { TCCR0 = 0x00; } // Taimer 0 value set inline void timer0_set_value(unsigned char value) { TCNT0 = value; } // Timer 0 overflow flag clear inline void timer0_overflow_flag_clear(void) { bit_set(TIFR, TOV0); } // Timer 0 overflow flag state reading inline bool timer0_overflow_flag_is_set(void) { return (bit_is_set(TIFR, TOV0) ? true : false); }
The following is a similar program to the example of the software delay. In the shorter 100 ms half-period the LED is lit and on the longer 900 ms half-period it is switched off. As the result the LED is blinking after every second. Unfortunately, in this example the period isn't precisely 1 second either, because executing other functions of the program takes also time. For exact timing a 16-bit timer with interruptions must be used.
// Demonstration program of hardware delay of the HomeLab // The Program blinks LED for a moment after every ~1 second #include <homelab/pin.h> #include <homelab/delay.h> // Main program int main(void) { // Setting the pin of the LED as output pin_setup_output(led_debug); // Endless loop while (true) { // Lighting the LED pin_clear(led_debug); // Hardware delay for 100 milliseconds hw_delay_ms(100); // Switch off of the LED pin_set(led_debug); // Hardware delay for 900 milliseconds hw_delay_ms(900); } }