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:
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.
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.
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
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
// 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)
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.