Remember that up to now we didn’t much care what numbers we had to feed our software delay function, as long as we could get a human-perceivable LED blink. Now we have to do better. To run our LCD display, we have to produce a delay of around 1us, and a delay of around 50us, and delays in the ms range. The easier task is to write a ms_delay function, one that takes an argument for the number of milliseconds to delay. The reason it is easier is that there are a lot of clock cycles to work with in one millisecond. Anyway, here is the function, followed by the explanation:
void ms_delay(u16 d)
{
while (d-- != 0)
{
volatile u16 i = MS_COUNT;
while (i-- != 0)
;
}
}
What we have done is to break the software delay loop into two loops. If we choose a value of MS_COUNT that causes the inner loop to execute in exactly 1ms, then the entire function will execute in “d” ms, as desired. So all that is required is to come up with a number for MS_COUNT. As is always the case with software delays, that number will depend on the uC clock rate – a higher clock rate means a larger number. We could figure a number by looking at the generated ASM code and counting the cycles for each instruction, but there’s an easier way (for me, at least): fire up your oscilloscope (or beg or borrow one), and write a loop that calls ms_delay with some convenient number like 100. Then pick a number for MS_COUNT – 1000 is a good start. Toggle an LED just as before, using “ms_delay(100)” for the delay, and look at the scope. You will see a square wave where the signal toggles every so often. What we want is to see that toggle exactly every 100ms, but we will almost certainly not see that at first. We will have a longer or shorter interval, and from that we can make MS_COUNT shorter or longer until we get exactly (well, very close, which is good enough) to 100ms. For my AVR board, now running at a clock rate of 14.7456MHz, that number turns out to be 1053.
Now that we’ve done that work, we can actually come up with a formula that relates MS_COUNT to F_CPU. 14,745,600 / 1053 = 14,003.42. Now we have a simple definition of MS_COUNT:
#define MS_COUNT (F_CPU/14003)
Remember, this is for AVR, and is only valid for the compiler I used (avr-gcc in Atmel Studio 6, which is AVRGCC 3.4.1.95) and the optimization settings I used. That’s one of the two curses of software delays – any change to clock rate or toolchain or settings can break them (the other curse is that they waste valuable cycles that could be used for doing the real work of your system).
There’s another trick you can use to figure out MS_COUNT if you don’t have access to a scope – become a scope yourself. To do this we’ll extend the delay time way out past 100ms, to values a human can measure with some accuracy. Since the argument to ms_delay is a u16, we can time up to 65,535ms. 60,000ms is exactly one minute – convenient! So if we run our LED blinky program using ms_delay(60000), one entire blink cycle (ON-OFF) will take exactly 2 minutes. We can time 2 minutes pretty easily by eye and by clock. Then adjust your initial_count as follows:
MS_COUNT = initial_count*120/measured_time
So if the initial count was 1000 and one complete ON-OFF blink took 105 seconds, MS_COUNT would be 1000*120/105 = 1143.
Tangent: Software Delays – Microseconds
Unfortunately we can’t take the same approach with microsecond delays, because there just aren’t enough CPU clocks to work with in a microsecond. That’s not a terrible problem because we only need two delays, >1us and >50us. The simplest solution is to just use a function like our earlier “delay” function, and use a scope to come up with the correct loop counts. The only change would be to modify the delay function argument from a uint32_t to a uint8_t, to get better time resolution (faster looping on an 8-bit uC). Using this approach you will be able to come up with a delay count that will generate a delay very close to 50us, and a very small delay count (1 is what I use) that will generate a delay that is rather longer than 1us, but that’s not a problem, so just go with it. For my AVR, clock rate, tool chain and settings, those delay counts are 1 and 90. You will see both of these delay values in the listing to follow.
BUT WHAT ABOUT THE INCLUDED DELAY FUNCTIONS?
Users of avr-gcc know that there are some accurate built-in delay functions, _us_delay() and _ms_delay(), so why not use them? There are no doubt similar built-in functions included in some other compilers. The answer is, there is no reason at all not to use them, if you have them available. But not every compiler will have such functions, and it’s a good exercise in learning how to write something equivalent, even if not as accurate.