An approach to timing Arduino sketches.
The mentality of Arduino is really to be able to throw some code in there and make it work. Coming from the nitty-gritty world of industrial design, I found the Arduino IDE was a fun toy to play with on weekends. I didn't have to care about being professional and could shoehorn my ideas into the loop, delay and repeat. I thought the Arduino IDE was unprofessional and there's no point to trying to write good code with it, because really, why would you?
however...
After I started working at SparkFun I came up with a challenge for myself. I saw the forty seven effects midi library, looked inside and said, "It's not the Arduino IDE that is wrong. It's my way of thinking that's wrong!" I challenged myself to use the constraints of the IDE to create code that works and is maintainable.
The deal breaker from the beginning was the use of the delay statement. One of the first lessons in microcontrollers is that software-driven delays are not a good idea.
A few problems with delays:
The LED blink application states:
Well what if I want another LED to blink every half second?
It's do-able. But what if I want to flash one LED at one second and the other at 34/92 of a second? Well then I'm in trouble. Or if I want to do any number of services, all at different rates? It becomes a nightmare.
The timer interrupt is a perfect example of something else you can use. As soon as I realize a sketch needs consistent time tracking, a free interrupt gets used. I consulted the Hardware Resource Map from the Google code archive, found a timer I could take hold of, and made the interrupt increment some counter somewhere. Then, I can look at this counter and compare what time it is to some time I've previously recorded. Loop() can race along as fast as possible while the counter regularly increments, and I can do things on intervals.
Using a timer interrupt to unload work from the processor isn't exactly a new concept. I noticed when I code that I always make it the same way, so it was time to pull it out and make it a module. TimerModule is born.
I want a thing that keeps time without drifting, and operates user code chunks at arbitrary intervals. That's the spec. I made, tested, and incremented until I was happy with it. More importantly, until I could trust it. Here's how it works.
From the highest level, the sketch, the system is drawn like this:
Every time the loop executes, it first updates the current time to all the timers that are in use, then checks if any of the timers have exceeded the defined period. If so, each timer has a place the user code can be executed from. All the while, the interrupt increments the time counter.
Here's how the loop is coded. Objects msTimerA, B, etc. are all TimerModule classes, with msTimerx as the name for copies of the class.
void loop()
{
//Update the timers, but only once per interrupt
if( msTicksLocked == 0 )
{
msTimerA.update(msTicks);
msTimerB.update(msTicks);
msTimerC.update(msTicks);
//Done? Lock it back up
msTicksLocked = 1;
} //The ISR will unlock.
if(msTimerA.flagStatus() == PENDING)
{
digitalWrite( LEDPIN, digitalRead( LEDPIN ) ^ 0x01 );
}
if(msTimerB.flagStatus() == PENDING)
{
digitalWrite( LEDPIN, digitalRead( LEDPIN ) ^ 0x01 );
}
if(msTimerC.flagStatus() == PENDING)
{
digitalWrite( LEDPIN, digitalRead( LEDPIN ) ^ 0x01 );
}
delay(1);
}
There are a few assumptions for this to work.
If these assumptions can be accounted for in code, the goal will have been met.
The first hurdle is that the size of the variable that counts how many times the interrupt has to be triggered. It can't be infinite and must rollover somehow.
The simplest solution is to use a known data type and just let it roll over. If I do that though, I have no way of knowing (ok, we're not looking at low level cpu register bits here). So I add more code. I check during each interrupt if it's above some limit, and reset it to zero.
Graphing the value of the time counter as a function of real time, the rolling action can be seen.
The ISR is written like this:
void serviceMS(void)
{
uint32_t returnVar = 0;
if( msTicks >= ( MAXTIMER + MAXINTERVAL ) )
{
returnVar = msTicks - MAXTIMER;
}
else
{
returnVar = msTicks + 1;
}
msTicks = returnVar;
msTicksLocked = 0; //unlock
}
Again, there's a subtle extra part that has been added. Rather than resetting to zero, a known quantity is subtracted from the counter and it's given an overhead. This is actually a product of how the arbitrary timers operate.
To create a sketch with any number of timers (to the extent of the memory available):
TimerModule myNewTimer
myTimer.update(msTicks);
if(msTimerA.flagStatus() == PENDING){do something}
- code goes within!That's all there is to use it. If you want to try it out, drop this GitHub library in your 'libraries' directory, restart Arduino, and select the example "timerModuleExample." This will run on the Teensy 3.1 or Uno/Mega, and will xor the LED at three different rates, giving a pseudo-random like effect.
If you want to see what goes on under the hood, keep reading!
The TimerModule is a small state machine that is controlled by the call to update, and the check of the flag. Here's the state diagram of the messaging action (Need a refresher? State Machines Enginursday Post):
The last piece of the puzzle is to determine how the timer module handles rollover. Also, what's with the counter overhead in the ISR?
Take a step back and look at the loop. The loop begins by sending the current time (in msTicks) to each timerModule. Then, it checks to see if any of the updates caused a timer to expire.
Imagine three irregular but closely spaced updates of the timerModule. The loop takes different amounts of time, depending on which timers were executed, so they are not spaced evenly.
Each time they are updated, they check if the current threshold (or next service time) is reached. If so, they activate the state machine and calculate a new threshold for comparison.
The basic operation is defined, but the case of rollover is still not understood. To investigate, look at what happens as the timer counters get close to the rollover. As Neal Stephenson said in Snow Crash, "Interesting things happen along borders."
The ISR automaticaly rolls the msTicks timer over. If the system presents a timerModule with a new time that is less than the previous time, it knows the ISR must have rolled the msTicks counter. Then, the threshold and the previous threshold time get reduced by MAXTIMER. Now, the counter is at the bottom of the cycle and the threshold is still the same distance away. One of the thresholds is negative though, and the "greater than" check doesn't work.
A solution is to add a padding of the longest used timer interval to the bottom of the ticks coming in from the system, or, let the ISR do it automatically.
Now, the ISR and the time keeping qualities of the TimerModule all ratchet the target numbers automatically. The logic of the TimerModule's state machine is then executed without any knowledge that rollover ever occured. Take a look at the update code of the TimerModule.
void TimerClass::update( uint16_t msTicksInput )
{
//Check if overflow has occurred
if( msTicksInput < lastService ) //overflow has occurred
{
//Adjust as if nothing ever happened
lastService = lastService - MAXTIMER;
}
//Now process knowing an overflow has been dealt with if present
if( msTicksInput >= (lastService + interval) )
{
//Timer has expired
//Save the last service time
lastService = lastService + interval;
//Ready the flag
if( flag == WAITING )
{
flag = PENDING;
}
else
{
flag = UNSERVICED;
}
}
}
The library is compatible with Arduino. Put the library in your libraries folder and try running the example "timerModuleExample" on an Uno/Mega or Teensy 3.1.
Direct link: https://github.com/marshalltaylorSFE/uCModules
This post talks about just one module I commonly use. If you download the library and try it out, you'll find sketches for a few others I didn't want to talk about in the post (check /extras for more pictures!)
Also in the library you'll find:
And as development continues, these modules will be added:
I wanted to present how this engineer thinks, but I also wanted to give out something that may be useful to someone. If you gave it a whirl, let me know what your reaction was.
Keep on thinkin'!