Debugging Arduino Servo Issues

Post date: May 11, 2013 12:00:06 AM

Corrections/comments are very welcome, especially if I get the technical details wrong. Please email flippy.qa76 AT gmail.com.

If you are using servos with Arduino, sometimes the results can be...not what you expect. My example is that I was using continuous servos as the drive motors for a mini-sumo 'bot. The (greatly simplified) control loop for the motors looked like this:

loop()
{
    take distance measurement from ultrasonic sensor
    adjust servos' speeds accordingly (using Servo.write())
}

And I'd get all kinds of wonky behavior, mostly in that it didn't appear the servos were getting any kind of speed updates at all. It was *supposed* to move straight forward when the sensor detected another object in front, but in most cases it would act as if the opponent wasn't visible. (And yes, I checked and double-checked that the sensors were working...)

It turns out this is a timing problem. Before I cut to the punchline, let's look at the issues in play here.

Arduino loop() function timing

The first thing to look at is how often the void loop() function is called. For sake of simplicity and to get a ballpark figure, we'll assume loop() is empty, i.e., it is running about as fast as possible. I'm too lazy to run my own tests on a modern Arduino, but this thread post gives us a starting point of roughly 260 KHz for an empty loop. That means loop() gets called roughly 260,000 times a second or about 3 times every millisecond (ms). I'm going to make a wild guess and say that loaded with some actual executable code that loop() runs on the order of once every millisecond or so. Keep this in mind.

Servo timing

This page provides an excellent overview of the timing signals used to control servos. The TL;DR version is that a servo expects to receive a command to move (or in the case of continuous servos, to change speed) every 20 ms.

Now compare this to the loop() function timing. The ratio is 20:1, meaning loop() executes about 20 times for every time a servo expects a command. Obviously the actual frequency of loop() depends on the code in it, but it's fair to say it can send commands to a servo faster than a servo can react to them.

Hardware-level effects

Next, consider what actually happens when a Servo::write() method is called. As way of background, the Servo library uses one of the AtMega's built-in timers to create the pulse signals sent to a servo connected to an Arduino. This timer is a piece of hardware that periodically generates an interrupt -- a signal that pauses the normal Arduino program to run a special interrupt service routine or ISR. When the ISR is finished, the Arduino program picks back up wherever it left off. When you use the Servo library, it uses the ISR to send the appropriate control pulses to all the attached servos.

The thing to understand here is that Servo::write() does not immediately affect the attached servo. Instead, it simply records the position (or speed) the servo should have when the ISR runs again. In other words, you can write() a value to a servo and have other things in your program happen before the servo actually responds. Worse, loop() could run a bunch of times before this happens, so you could actually write() to a servo a bunch of times, with different values, before it responds (taking only the most-recent).

One more thing to check -- how does the timer's period hold up to our assumption that we're sending servo commands every 20 ms? The answer is in Servo.h, the header file for the Servo library, where you will see the line:

#define REFRESH_INTERVAL    20000     // minumim time to refresh servos in microseconds

Notice REFRESH_INTERVAL is in microseconds (1/1,000,000ths of a second). Doing the math, 20,000 microseconds equals...20 ms. Digging into the ISR code for the timer, it turns out that the ISR (which runs a MUCH more often than once every 20 milliseconds) checks to see if 20 ms have elapsed since the last "refresh" of the servos. If it has, then and only then will it send a new control signal to the attached servos.

I don't care, just tell me what I need to do to get my Arduino sketch working

Fine, fine. In the end, you just need to make sure that you only call write() on a Servo object once every 20 ms or so. Maybe a little longer to give the servo's hardware some time to do it's thing -- remember, the servo itself has some control logic that takes a little while to work.

From a code standpoint, this could be as easy as putting delay(20) at the end of loop(). This will work if there isn't really anything else other than servo control going on. On the other hand, if you're trying to interleave several independent tasks, you may need to set a "timer" of your own. The outline of that code looks something like this:

// "timer" variable, in milliseconds.  remembers the last time an update happened
// MUST be of type unsigned long!
unsigned long lastUpdate = 0;
// time between updates (20 ms for servo control) 
unsigned long updatePeriod = 20; // in milliseconds
void loop()
{
    // millis() returns the number of milliseconds since the Arduino turned on
    unsigned int now = millis();
    // check to see if updatePeriod milliseconds have elapsed since lastUpdate
    // if so, update the new position and reset lastUpdate
    if( (now-lastUpdate) >= updatePeriod)
    {
        servo.write(newPosition);
        lastUpdate = millis(); // reset our countdown timer
    }
    // can do other stuff here without servo delays getting in the way
}

In practice, I found that performing Servo::write()'s works a little better with a longer period than 20 ms, simply because the servo hardware can't move that fast. Depending on the particulars of your application, anywhere from 50 ms to 200 ms could be more appropriate -- you'll just have to experiment to find out.