23
More on doing several things at the same time
The other day I suggested cooperative multitasking as a very simple technique to use on small microcontrollers, by beginners and also not-so-beginners. Today I’m going to explain a problem with the simple technique and provide a solution - also simple. Got curious? Just keep on reading!…
The Problem
There’s in fact a small problem with the technique shown on the previous article. It may or may not be important to your application; depends on how precise you need your timing to be. Let’s get back to the main loop of the example from the previous article:
void loop ()
{
blinkLed1();
blinkLed2();
sendNumber();
delayMs(10); // wait 10 ms
}
What we want from this program is that it runs the 3 tasks once every 10ms. So, after, let’s say, 100 ms, they should have been run 10 times. But suppose that the two 1st tasks take 2 ms each to execute and the 3rd task takes 4 ms to execute; what will it happen to our time unit? After the 1st pass on the loop, 2 ms + 2ms + 3 ms + 10 ms = 15 ms will have passed, after the 2nd pass on the loop 30 ms will have passed, 45 ms gone by the end of the 4th pass and so on, such that by the 100th ms, our loop has been run only 6 times instead of 10. Bummer!…
The Solution
So how do we solve this time drift? Modifying the delay to wait only 5 ms doesn’t work, because I described a very simple and unrealistic scenario where the tasks always take an exact amount of time. Real life however isn’t that predictable, and each task run can take a different amount of time, depending exactly on what code it runs. A single “if” statement being executed or not is enough to create a time difference, which accumulates over time to very measurable values. Ok, so another approach is needed.
What we need is to have a watch and run the loop when it marks specific times. In our 10 ms period example program, the loop must run when the watch marks multiples of 10 ms, such as “at 10 ms”, “at 20 ms”, “at 30 ms” and so on, instead of running the loop and then waiting for a fixed 10 ms. Then each loop pass will have up to 10 ms to run. In the Arduino platform that watch is a function called millis()
which I’m going to use in my example, but every development platform has a similar function. The millis()
function returns the number of milliseconds that have passed since the Arduino was last reset, so it can serve as our time watch:
void loop ()
{
static unsigned long now = 0;
static unsigned long timeToRun = 1;
// wait until it’s time to run the tasks
while (now < timeToRun) {
now = millis();
}
timeToRun = now + 10; // next time to run is 10 ms from now
// run our tasks
blinkLed1();
blinkLed2();
sendNumber();
}
Can you see the picture now :)? The tasks can take any time they need, as long as it isn’t more than our time period. At the loop()
top, we wait for the watch to reach the next time to run; this while
loop only waits the time needed for the watch to reach the next 10 ms mark. This provides a very accurate time period.
On the 1st run, now
(0) is smaller than timeToRun
(1), so the while
will execute a few times until 1 ms or more passes since reset; then timeToRun
will be set to the current time plus our time period and from then on the tasks will run once every 10 ms.
The total time of running the tasks can’t obviously be more than our time period, otherwise they won’t run at 10 ms intervals as we want. We can add a simple check for “period overflow”, which may be very useful in development:
// ... // check if we have a period overflow if (timeToRun > 1 && millis() > timeToRun) { digitalWrite(LED, HIGH); // light up a warning LED } // wait until it's time to run the tasks while (now < timeToRun) { now = millis(); } timeToRun = now + 10; // next time to run is 10 ms from now // …
In the example, a LED is light up if the time to run has already passed before we even start waiting for that time to come. The 1st part of the condition prevents the LED from lighting up on the 1st run, since the current time may be bigger than the initial value of timeToRun
.
In case of an overflow everything will continue to run, but the timing is ruined so we need to be alert.
A Word of Warning
The millis()
function cannot return increasing numbers forever; at some point, it will wrap around, that is, get back to 0 and continue to increase from there. When this happens, the example code here can get stuck. This, however, only happens every ~49 days, if the code is running non-stop without resets or power cycles. If your project is going to stay up for more than 49 days at a time, you should check for millis()
overflow. The overflow happens when timeToRun
is smaller than now. In this case, you need to wait for millis()
to return a value less than timeToRun
, and only then wait for now < timeToRun
.
Final Words
This is it, the simple method of how to do several things at the same time is still simple but now highly improved. I hope you enjoyed these articles and make good use of them :). Any comments are appreciated. Cheers!
p.s. The code probably doesn’t compile, it’s mainly for illustration purposes.
Hi, nice article. I really like it!