Introduction: Use a PIC Microcontroller to Control a Hobby Servo

This instructable describes how to integrate hobby servos (the kind used in RC planes, cars, etc.) into your microcontroller projects.

Step 1: How Servos Are Different From Regular Motors

In a regular DC motor, the amount of torque the motor exerts on the shaft is proportional to the amount of current flowing through the motor. A simple way to control it is by varying the voltage across the motor; more voltage means more current which means the motor pushes harder against its load which means the shaft turns faster.

When using a servo, however, you don't control torque or velocity. Instead, you specify what angle you want the shaft at. In other words, you have positional control of the motor.

Inside a servo is a traditional DC motor, a potentiometer (variable resistor), and control circuitry. The potentiometer is connected to the motor such that when the motor shaft turns it also turns the potentiometer. The controller can then measure the voltage at the center pin of the potentiometer and get an indication of the shaft's position. The controller receives a signal (see next step) from the user that sets a desired position. The controller compares the desired position to the current position of the motor and uses that information to turn the motor in a direction that minimizes the error.

The way this works in practice is you specify the angle you want the shaft at using your PIC, the shaft turns to that position, and then holds there. The further it gets pushed away from that position, the harder it tries to turn back. Hobby servos are usually geared way down, so even a wimpy $15 or $20 one can hold its position reasonably well.

Step 2: The Control Signal

In addition to power (red wire, 5 volts works fine) and ground (black wire) connections, the servo accepts a control signal on the third wire (usually white or yellow). The signal is almost pulse width modulated, except that it doesn't have a fixed period.

It is composed of pulses of voltage, the duration of which determine the angle of the output shaft. The pulses can be from 0.9 ms to 2.1 ms long, 1.5 ms being the center position (in other words, pulse duration varies linearly with shaft angle). I don't remember whether 0.9 ms means all the way clockwise or all the way counter clockwise, but it's not a big deal to test our your project, see that the servo is turning the wrong way, and reverse it in your code. A new pulse must be sent to the servo once every 10 to 20 ms (50 to 100 Hz).

These timing/voltage values are for a typical servo. You should be able to find documentation for you specific servo from the manufacturer.

Step 3: The Circuit

This schematic is actually from something else, but it's ridiculously similar to the one I'll use for a quick demonstration.

The only change that needs to be made for this project is to replace the LED and its resistor with the wire for the control signal. So instead of connecting pin 3 to a resistor and then to an LED and then to ground, just connect pin 3 to the white/yellow wire from the servo. The ICD2 block represents the PIC programmer I used, substitute your own if you want. Also, don't forget to hook up the servo's power and ground connections.

The potentiometer (wiper connected to pin 2) is used here as a voltage divider. A voltage divider is simply two resistors in series with a contact in the middle at which to measure voltage. In this case, the potentiometer is both of the resistors and we adjust their relative proportions by adjusting the potentiometer. It will be used in this project as an analogue input to let us vary the duration of the control signal pulses.

The second image is a bigger version of the schematic. The smaller one just displays better at the size shown on the page.

Step 4: The Firmware

Attached is the firmware I used for this little demo. I used the free MPLAB IDE with the C18 compiler (free to students). Both are available to download from the Microchip website. The logic goes like this:
1. Delay for somewhere between 10 and 20 ms (in this case, 12)
2. If the button (pin 4) is pushed, measure the voltage of the potentiometer (pin 2)
3. Set the control signal high (5 v)
4. Delay for a length of time proportional to the voltage measured in step 2
5. Set the control signal low (ground)
6. Loop back to step 1

Maybe a little more information on the analog-to-digital conversion is in order. So we already know that the voltage at pin 2 is going to be somewhere between 0 and 5 volts because of the voltage divider. The PIC's A/D conversion works (conceptually) sort of like a reverse voltage divider. It reports the voltage as a proportion of its supply voltage. However, the proportion is scaled to be between 0 and 255 (which is 8 bits of information, the size of an unsigned character). In other words, the following equality is set up:
(pin 2 voltage) / 5v = POT_VALUE / 255
POT_VALUE = 255 * ((pin 2 voltage) / 5v)
The PIC is actually doing a 10-bit A/D conversion, but 8 bits is plenty granular enough for our purposes, so the two least significant bits of the A/D conversion aren’t used in this code. Besides, the last couple of digits are probably measuring noise instead of the signal itself, so we're not missing all that much by ignoring them.

The delays are calculated based on the time it takes to complete one instruction on the PIC with a 4 MHz clock. I don't remember why, but somehow this works out to 1/6 us per instruction cycle. So if we delay for 5400 cycles, that's 5400 cycles * 1/6 us/cycle = 900 us = 0.9 ms. Another important piece is finding the constant of proportionality that scales POT_VALUE (on a range of 255 - 0 = 255) to our pulse width (on a range of 2.1 ms - 0.9 ms = 1200 us); or, to frame the question a different way: How many cycles should we delay for each count of POT_VALUE?
(1200 us * 6 cycles/us) / 255 counts = 28 cycles/count
Those of you following along with the code will notice that I actually delay 20 cycles per count, not 28. This is an experimentally determined value (I looked at the signal with an oscilloscope) that adjusts for all the factors that effect the timing, mainly the overhead from the for-loop on line 68.


Step 5: Improvements

Which brings me to my next point: this code sucks. You can do hardly any processing other than signaling to the servo, which is ridiculous because the vast majority of that time is spent locked up in a delay function. Also, it's hard to predict just how much overhead you have from other parts of the code that could be throwing the timing off; that constant of proportionality shouldn't have required an experiment to get it working correctly.

What this code should use instead of delays is the PIC's built in timers. Code for this might look something like:
OpenTimer0( TIMER_INT_OFF &
T0_PS_1_2 ); // i think this makes a timer that raises a flag every 21.8 ms

while (1) {
while(!INTCONbits.TMR0IF){ // wait for timer to set flag
// do useful things
INTCONbits.TMR0IF=0; // reset timer flag

// send pulse to servo

I know I said there was supposed to be a pulse every 10 to 20 ms, but 21.8 ms is pretty close and hobby servos are generally very forgiving.

Step 6: Aside: Power Issues

I've run two or three small servos and a PIC off of a line-powered USB connection before, so there shouldn't be any issues using the same power supply for the servo and the logic. However, if your servo isn't as powerful as you know it should be or your PIC keeps randomly resetting or even breaking, these are indications that you have power issues. It might mean that your power supply can't source enough current or can't deal with the rate at which the current through the servo changes or that the servo is polluting the power lines with back EMF noise. Possible solutions include using a separate power supply for the PIC and for the servo (they'll have to have a common ground because that's the reference for the control signal), getting a beefier power supply, or making the decoupling capacitor (between pins 19 and 20) a little bigger.

I honestly don't know what best practice is in terms of decoupling and avoiding ground loops and such, but it's something I'm interested in. Can some one help me out here?