Timers, Encoders, Rollover, and the Magic of Two’s Complement (pt 1 of 2)

In the Arduino world, delay functions are convenient.

void loop()
{
    doSomeCoolStuff();
    delay(1000);
}

Unfortunately, they’re also impractical.

Let’s say we tried to doSomeCoolStuff once per second. The code snippet above doesn’t actually behave that way because it ignores how much time the doSomeCoolStuff function call takes to return. If doSomeCoolStuff takes 15 ms, then the loop actually runs at a rate of 1.015 ms, not 1.000 ms.

One solution is to use a timer. Arduino’s hardware-abstraction layer actually makes using timers strikingly easy. milis and micros return the respective milliseconds and microseconds since the program first elapsed. Now, provided that doSomeCoolStuff() takes less than one second to complete, we might try writing the code like this:

const int LOOP_TIME = 1000;
unsigned long lastTime = 0;

void loop()
{
    unsigned long elapsedTime = millis() - lastTime;
    if (elapsedTime == LOOP_TIME)
    {
        lastTime = millis();
        doSomeCoolstuff();
    }
}

There are two subtleties here, though, but first let’s see what the code does.  Since loop repeats itself as long as the Arduino is powered, elapsedTime continues to get reassigned and checked against LOOP_TIME. If they’re equal, a new lastTime gets assigned and doSomeCoolStuff happens.

The first problem here is that code is rarely this simple. Let’s say we call some other function in the loop:

const int LOOP_TIME = 1000;
unsigned long lastTime = 0;

void loop()
{
    unsigned long elapsedTime = millis() - lastTime;
    if (elapsedTime == LOOP_TIME)
    {
        lastTime = millis();
        doSomeCoolstuff();
    }
    doSomeOtherStuff();
}

In this case, we’re also calling doSomeOtherStuff at every single loop iteration. I haven’t timed how long doSomeOtherStuff will take, but it might fall just outside the millisecond range such that 1001 or more milliseconds have elapsed before the if case is queried. If such a case happens, then doSomeCoolStuff will miss its chance to run and elapsedTime will increase until it rolls over, a very long time from now.

The fix? Check against an inequality:

    if (elapsedTime >= LOOP_TIME)

Now, even if 1001 seconds elapse, doSomeOtherStuff will indeed trigger.

Ok, now for the last point worth mentioning: can we guarantee that this line works in all edge cases? i.e: will elapsedTime always return a number between 0 and LOOP_TIME, or will it return a huge number in some weird overflow-error case?

    unsigned long elapsedTime = millis() - lastTime;

The short answer: actually yes! The longer answer is in that the binary number two’s complement representation actually handles this very gracefully.

More on that in part 2….

 

Leave a Reply

Your email address will not be published. Required fields are marked *