Electronic – How to control a servo motor with the PWM output of a microcontroller

avrmotor controllerpwm

I wanted to control the direction of servo motor with a switch. I am using an Atmega32. Outputs are PIND4 and PIND5 and the input is PINA3, but the two servo motors turn only in 1 direction.

 #ifndef F_CPU
 #define F_CPU 1000000UL 
 #endif

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>


int main(void)
{
DDRD= 0xFF ;
DDRA=0x00;
PORTA =0x00;
// FAST PWM
TCCR1A |= 1<<COM1A0 | 1<<COM1A1 | 1<<WGM11 |1<<COM1B0|1<<COM1B1 ;
TCCR1B= 1<<WGM12|1<<WGM13 ;
ICR1 = 19999 ;

TCCR1B |=1<<CS10 ;
while (1)
{
   if(PORTA==0x08) //if pina3 is 1
   {
    OCR1A = ICR1 -2000 ;   //turn servo 1
    _delay_ms(1000);
    
    OCR1B=ICR1-2000;    //turn servo 2
    _delay_ms(1000);
    }
    if(PORTA==0x00)// if pina3 is 0
    {
      OCR1A = ICR1 -900 ; //turn servo 1 in the other direction
      _delay_ms(1000); 
      
      OCR1B=ICR1-900;     //turn servo 2 in the other direction
      _delay_ms(1000);

    }
}

    
return 0 ;

}

Best Answer

You need to clarify. Are these ...

  1. Hobbyist RC servo motors that turn to a position (0 ~ 180 degrees)
  2. Modified hobbyist RC servo motors that can turn full revolutions
  3. Industrial servo motors with precision feedback

Servo motors due not work the same as using PWM to control a common motor speed or LED brightness.

Hobbyist Servo Motor

This type of servo motor has an internal potentiometer that provides feedback to internal circuitry as the motor shaft turns, thus allowing it to turn to a know position. You control the motor by sending it very specifically timed pulses. Servo motors I've worked with want a 50Hz signal, so a pulse every 20 milliseconds. This pulse is used to create an analog value in the servo driver that is compared to the value coming from the potentiometer. That's how the servo knows to stop at the correct position.

The pulses themselves control the rotational position of the motor. The working range is typically 1000 to 2000 microseconds (1 - 2 ms) with the center position (90 degress) right in the middle at 1500 us. Some motors have a bit of an extended range [0.5, 2.5us]. See this image from Jameco:

Servo motor pulses

These motors have an internal close-loop system, but are open-loop over all. Unless you are monitoring the rotation externally, you have no way of knowing if the motor actually turned to the correct position. It could be stopped by an external force, drawing excessive current and damaging the driver.

There are also "digital servos" that work differently internally and sometimes require a more complicated driver.

Modified Servo Motor

Common hobbyist servo motors can be modified internally to allow full rotation and fairly accurate speed control (under little load). They work the same way as before, only now a 1500us pulse width is a speed of 0 rpm. Larger pulse widths provide positive rotation with increasing speed, and lower pulse widths provide negative rotation with increasing speeds.

Industrial Servo Motor

These require special drivers and more knowledge. I will not talk about these unless it's actually what you're trying to use (which I doubt).

Driving them with a microcontroller (MCU)

You cannot directly power a motor from the MCU, it must have an external power supply. Also note that when the motor starts to turn, it can draw enough current to brown out the MCU (under-voltage) if they share a power supply. Use bulk capacitance on the supply lines, bypass capacitors on the VCC pins, and/or separate power supplies/regulators.

You should be driving the third wire of a common servo motor with the pulse train mentioned above. The best way to do this allowing for independent control of multiple servos is to configure a timer with a TOP value set for 20ms. Then, set an output compare value to trigger at the desired pulse width time.

You could connect the output pins directly to the timers on compare match, or you could enable the interrupt service routines (ISR) for the TOP (overflow) and the output compare. Set the servo pulse pin(s) at the top, and clear them at the output compare.

You can use separate timers for each motor, separate output compare registers on a single timer, or an algorithm in your compare ISR to set itself to the next lowest compare value in an array of servo control value.

Code Issues

There are numerous issues with your code. For starters, you check an input using PINx, not PORTx. With inputs, PORTx typically controls the pullup resistors.

if(PORTA==0x08) //if pina3 is 1

should be

if(PINA & _BV(PA3)) //if pina3 is 1

BTW, I prefer the "_BV(bit)" macro. It is defined as (1 << (bit)), so this is equivalent to:

if(PINA & 0x08) //if pina3 is 1

I have to assume this isn't all of your code, as you are missing other important includes, definitions, and configuration code. Also, you don't need a return in main. It's an embedded system. To where would it return?

Next, and this is the kicker, you can't control servos this way. I have no idea what your CPU frequency is. The default for AVR is 1MHz (8Mhz / 8) and Arduino was 16MHz (external xtal). I'm not familliar with the mega32, so I don't know if you're setting the timer up correctly for PWM, but assuming you are, and assuming your PWM is operating at the necessary 50Hz frequency (which I highly doubt), You are setting a pulse width of 18ms. That's a 90% duty cycle: (19999 - 2000) / 19999 * 100% = 90%. (for servo 1). Then you let it run for 1 second before turning on servo 2. That's just... no.

Also, you never STOP the servos from running. You'd need to stop the timer or set OCR1A/B to 0.

If PA3 is low, you set the servo pulse widths to 19ms (again, assuming a correct 50Hz frequency). That's a 95% duty cycle. If the servos were ever turning, they would continue to turn to the same position, in the same direction, at the same speed, because the max pulse width they'd care about is 2.5ms, a 12.5% duty cycle.

Relevant Example

It's actually easy to drive 8 servos with a single timer knowing that the maximum pulse to any one of them will be 2500us, and that 8 * 2500us = 20ms - the total pulse period. Conceptually split that period into 8 chunks, and enable a servo at the start of each 2.5ms chunk if desired, disabling at the desired pulse width.

Here's some bits of code I wrote a long time ago for a TWI enabled ATtiny24 servo driver. I used an external 16MHz crystal with clock divide disabled. Without a reliable clock source, the servos will be very jittery. It shouldn't be hard to port this to any other AVR.

// Set up Timer 1 for 2.5ms counter (20ms period shared by 8 servos)
TCCR1A = 0x00;      // Outputs Disabled
TCCR1B = 
    _BV(WGM13) |        // TOP - ICR1
    _BV(WGM12) |        // CTC Mode
    _BV(CS11);          // Prescaler = 8
ICR1 = 4999;            // Top = (16Mhz * 2.5ms / 8) -1
OCR1A = 5000;           // Ensure Timer 1 Compare Match A does not happen first
TIMSK1 = 
    _BV(ICIE1) |        // Enable Input Capture Interrupt
    _BV(OCIE1A);        // Enable Output Compare Match A Interrupt

Now here's the meat... I've declared global arrays of current and next pulse widths, and a count of the current servo:

static volatile uint8_t servo_cnt = 8;      // Signifies current servo [0,7]
static volatile uint16_t    currPos[8];     // Current Position - Updated after movement
static volatile uint16_t    nextPos[8];     // Next Position - Updated by BUS Master

The first ISR triggers on the timer overflow every 2.5ms. It should turn on servo[x], where x is [0,7]. How you do this depends on your circuitry (set a pin, shift byte to a register, etc). At this time, you also need to set the value of the output compare register to end the pulse.

ISR(TIM1_CAPT_vect)
{
  // Set servo[x] control pin HI
  // Shift values out to register
  // etc

    /*** UPDATE OCR1A ***
    The "nextPos" value is first multipled by 2 because when the timer counts to N, 
    N/2 us have actually passed. This is a factor of the TIMER1 prescale value.     */
  currPos[--servo_cnt] = nextPos[servo_cnt];            // Update Current Position
  OCR1A = currPos[servo_cnt] << 1;  // Tell counter when to turn outputs OFF
  if(0 == servo_cnt) servo_cnt = 8; // Shift control to the next of 8 outputs (compare to 0 is more efficient)
}

The last piece is the compare ISR which triggers when it's time to turn a servo off.

ISR(TIM1_COMPA_vect)
{
  // Shut OFF current servo output (if applicable) - set pin LO
}

I then updated the values for nextPos using TWI. The reason for two position arrays is to calculate more complicated pulses for controlling how fast the servo actually rotates to the next position.