RobG

LaunchPad controlling up to 8 RC Servos

54 posts in this topic

Here's my take on controlling RC servos with LaunchPad.

Again, I am using my favorite shift register, 74HC595 (later I will be adding another example which will use 74HC164.)

Since the voltage may be different in your application and the default DCO is used, you will have to work out the timing.

 

If you need only 4ch, look at this post.

Here is another 4ch example with ADC

The simplest single channel ADC -> Servo example is here

And finally, 2ch version is here

 

 

Besides LP you will need 74HC595 and 8 10k pull down resistors.

post-197-135135493665_thumb.png

 

#include 

unsigned int counter = 5;                   // Servo counter, start with 5 (see below in timer interrupt section.)
unsigned int servoPosition[8] = {180, 180, 180, 180, 180, 180, 180, 180}; // Default servo position

// DEMO counter
unsigned int demoCounter = 0;

void main(void)
{
WDTCTL = WDTPW + WDTHOLD;                 // Stop WDT

P1OUT &= ~BIT0;                                                       // Port P1.0 will be used as serial out to set first bit
P1DIR |= BIT0 + BIT2;                                         // Port P1.0 & P1.2 out

P1SEL &= ~BIT2;                                                       // An ugly way to clear register
volatile int c;                                                       // This could be done by some sort of POR
for( c = 0; c < 8; c++ ) {
      P1OUT |= BIT2;
      P1OUT &= ~BIT2;
}

P1SEL |= BIT2;                            // P1.2 TA1 option select
CCTL0 = CCIE;                             // CCR0 interrupt enabled
CCR0 = 325;                                                           // ~2.5ms
CCTL1 = OUTMOD_3;                         // CCR1 set/reset
CCR1 = 180;                                   // CCR1 duty cycle ~1.5ms
TACTL = TASSEL_2 + MC_1 + ID_3;           // SMCLK/8, upmode

_bis_SR_register(LPM0_bits + GIE);        // Enter LPM0 w/ interrupt
}

// Timer A0 interrupt service routine
#pragma vector = TIMERA0_VECTOR
__interrupt void Timer_A (void)
{
      counter++;                                                      // Increase counter
      if(counter == 0x08) counter = 0;                // Counter range 0 to 7

      if(counter == 6) {                                              // Set serial in to 1, then reset
        P1OUT |= BIT0;                                                // Both clocks of 595 are connected together, serial in must be set earlier
      } else if(counter == 7) {
        P1OUT &= ~BIT0;
      }

      // DEMO 
      demoCounter++;
      if(demoCounter == 800) servoPosition[0] = 80;
      if(demoCounter == 1600) servoPosition[0] = 280;
      if(demoCounter == 2000) servoPosition[7] = 280;
      if(demoCounter == 2400) servoPosition[7] = 180;
      if(demoCounter == 2800) servoPosition[7] = 280;
      if(demoCounter == 3200) servoPosition[0] = 130;
      if(demoCounter == 3600) servoPosition[0] = 230;
      if(demoCounter == 4000) {
              servoPosition[0] = 180;
              servoPosition[7] = 180;
              demoCounter = 0;
      }
      // END DEMO

      CCR1 = servoPosition[counter];
}

VIPTech, GeekDoc, MarkoeZ and 3 others like this

Share this post


Link to post
Share on other sites

thanks RobG,

 

i had a question. i understand u are dividing 20ms/8 and take advantage of the fact that a full servo occupies about 2ms, it's kind of like multiplexing leds (w/o the flicker). i don't understand the part when we have to "inject" the starting bit at count 6 (7 off), can u explain a bit more on that? thanks.

Share this post


Link to post
Share on other sites

Sure,

we need 595 to shift just one 1, so before first shift, we have to set input to 1, after that it's all zeros.

Every clock, we move that 1 to the next out.

Output is active when clock is 0. When clock is 1, output is disabled (Hi-Z.)

We have to set 1 on the input one clock earlier, because shift registers are one ahead of the latches when both clocks are connected together.

Share this post


Link to post
Share on other sites
thanks. i thought the setting of the 1st bit would be at count7 on count0 off.

That would be the case if we fed latch (storage clock) with inverted shift clock (register clock) as per specs.

Share this post


Link to post
Share on other sites

I was thinking that not many of us will ever need to use 8 servos so I have updated my code and here it is.

The new version controls up to 4 servos and does not require additional parts.

Just connect your servo to pins P1.4-P1.7 and voila.

 

#include 

 

unsigned int counter = 0;

GeekDoc likes this

Share this post


Link to post
Share on other sites

Hi Rob,

 

May I ask how many servo steps between 1 and 2 ms, please? Is it 125 steps with 8-us resolution?

 

TIA, Mike...

tech_juggernaut likes this

Share this post


Link to post
Share on other sites

Thanks Rob. Are those servos capable of higher resolution? I've written a dozen or more high performance drivers for people over the years but I've never had a servo to play with so I don't really know what kind of feats they're capable of (LOL)...

 

Also, since you're using a constant 10-msecs to fill out the balance of the 20-msec servo period, does that imply you may not have an exact 20-msec servo period? If so, is that a concern or are servos reasonably "period" tolerant? I always assumed a more precise period was in order and so I would use an extra array element just to keep track of the end-of-period "off" time, something like this;

 

    unsigned char ndx = 0;          // servo array index, 0..4
   unsigned char select = BIT4;    // servo channel select pin mask
   unsigned int servo[5] = { 1500, 1500, 1500, 1500, 20000 };

   /******************************************************************
   *  four servos (P1.7 thru P1.4), 1-MHz clock, 1-us TACLK source  *
   *                                                                */
   #pragma vector = TIMERA0_VECTOR
   __interrupt void Timer_A (void)
   { P1OUT &= 0x0F;            // turn servo outputs off
     P1OUT |= select;          // turn next servo on
     CCR0 = servo[ndx]-1;      // servo on time, 600..2400 (usecs)
     servo[4] -= servo[ndx];   // adjust end-of-period off time
     select <<= 1;             // shift servo output select bit
     if(ndx++ = 4)             // if end of 20-ms period
     { ndx = 0;                // reset servo array index and
       select = BIT4;          // reset servo select mask and
       servo[4] = 20000;       // reset end-of-period off time
     }
   }

Share this post


Link to post
Share on other sites

You can get higher resolution by changing Timer's clock settings, just like you did in your code example.

I just felt that 8us is good enough and I was thinking of storing that value in char to save memory.

Depending on the servo, the fill period should be between 10-20ms. Newer servos will work if your total period is 10ms, but the older ones will not work properly and you have to make sure that the total period is ~20ms.

My servos were not working correctly when I had it set to <10ms, they were jerky and unreliable.

Share this post


Link to post
Share on other sites

Another iteration of servo control. 4 ADC inputs control 4 servos (thanks to aEx155 for motivating me.)

Pots are connected to Vcc and GND, center to P1.x.

P1.0 controls P1.7, P1.1 -> P1.6, P1.2 -> P1.5, P1.3 -> P1.4

 

 

 

    #include 

   #define SERVO_OUTPUTS BIT4 + BIT5 + BIT6 + BIT7

   unsigned int counter = 0;                   // Servo counter
   unsigned int servoPosition[4] = { 180, 180, 180, 180 }; // Default servo position
   unsigned int servoOn[4] = { BIT4, BIT5, BIT6, BIT7 };
   unsigned char valueIndex = 0;
   unsigned int adcValues[4] = {0,0,0,0};

   void main(void) {
       WDTCTL = WDTPW + WDTHOLD;                 // Stop WDT

       BCSCTL1 = CALBC1_1MHZ;
       DCOCTL = CALDCO_1MHZ;

       P1OUT &= ~(SERVO_OUTPUTS);          // We could replace those BITs with define or simply 0xF0, but I left it for clarity
       P1DIR |= (SERVO_OUTPUTS);                                         // Port P1.4-1.7 is out
       P1SEL &= ~(SERVO_OUTPUTS);

       ADC10CTL1 = INCH_3 + CONSEQ_1;
       ADC10CTL0 = ADC10SHT_2 + MSC + ADC10ON + ADC10IE;
       ADC10DTC1 = 0x04;
       ADC10AE0 |= 0x0F;

       CCTL0 = CCIE;                             // CCR0 interrupt enabled
       CCR0 = 180;                                                           // ~1.5ms
       TACTL = TASSEL_2 + MC_1 + ID_3;           // SMCLK/8, upmode

       _bis_SR_register(LPM0_bits + GIE);        // Enter LPM0 w/ interrupt
   }

   // Timer A0 interrupt service routine
   #pragma vector = TIMERA0_VECTOR
   __interrupt void Timer_A (void) {
       counter++;                                                      // Increase counter
       if(counter == 0x05)
           counter = 0;                // Counter range is 0-4, the last count is used to add 10ms delay, otherwise analog servos might act funny

       P1OUT &= ~(BIT4 + BIT5 + BIT6 + BIT7);                       // Clear ports
       if(counter == 0x04) {
               CCR0 = 2500 - (servoPosition[0] + servoPosition[1] + servoPosition[2] + servoPosition[3]); // 20ms delay
           ADC10CTL0 &= ~ENC;          // disable conversion
           while (ADC10CTL1 & BUSY)  // make sure ADC is done
               ;
           ADC10SA = (unsigned int)&adcValues[0];  // set data buffer start address
           ADC10CTL0 |= ENC + ADC10SC;  // start sampling and conversion
       } else {
           P1OUT |= servoOn[counter];              // Set port of the current servo
           CCR0 = servoPosition[counter];          // Set time for the current servo
       }
   }

   // ADC10 interrupt service routine
   #pragma vector=ADC10_VECTOR
   __interrupt void ADC10_ISR(void) {

       valueIndex = 0;
       while(valueIndex < 4) {
           servoPosition[valueIndex] = (adcValues[valueIndex] >> 3) + 0x7F;
           valueIndex++;
       }
   }








tech_juggernaut and Gyula84 like this

Share this post


Link to post
Share on other sites

Hi Rob,

 

Can you comment these lines from the TIMER_A() interrupt, please?

 

TIA. Regards, Mike

 

            ADC10CTL0 &= ~ENC;
           while (ADC10CTL1 & BUSY)
               ;
           ADC10SA = (unsigned int)&adcValues[0];
           ADC10CTL0 |= ENC + ADC10SC;

Share this post


Link to post
Share on other sites

Another example, this time the simplest single channel ADC -> servo.

It is possible to get 2 or more channels this way, but MCU with at least 3 CCRs must be used.

 

      Vcc
       ^                         Vcc GND  
       |     +---------------+    ^  ^    
      +++    | MSP430G2331   |    |  +--+ 
      | |    |               |    +-----+ Servo
  Pot | |<---+ P1.1     P1.2 +----------+
      | |    |               |              
      +++    |               |           
       |     |               |              
       v     |               |    
      GND    |               |                  
             +---------------+              

 

#include 

void main(void) {
   WDTCTL = WDTPW + WDTHOLD;                 // Stop WDT

   BCSCTL1 = CALBC1_1MHZ;
   DCOCTL = CALDCO_1MHZ;

   P1DIR |= BIT2;      					// Bit 2 is connected to enable and will be used for PWM
   P1SEL |= BIT2;      					// P1.2 TA1 option select

   ADC10CTL0 = ADC10SHT_2 + ADC10ON + ADC10IE;
   ADC10CTL1 = INCH_1;						// P1.1 analog input
   ADC10AE0 |= BIT1;

   CCTL0 = CCIE;                           // CCR0 interrupt enabled
   CCR0 = 2500;                            // 20ms cycle
   CCTL1 = OUTMOD_7;                   	// CCR1 set/reset
   CCR1 = 180;                   			// CCR1 default position
   TACTL = TASSEL_2 + MC_1 + ID_3;         // SMCLK/8, upmode

   _bis_SR_register(LPM0_bits + GIE);      // Enter LPM0 w/ interrupt
}

// ADC10 interrupt service routine
#pragma vector=ADC10_VECTOR
__interrupt void ADC10_ISR(void) {
   CCR1 = (ADC10MEM >> 3) + 0x7F;
}

// Timer A0 interrupt service routine
#pragma vector = TIMERA0_VECTOR
__interrupt void Timer_A (void) {
ADC10CTL0 |= ENC + ADC10SC;
}

Share this post


Link to post
Share on other sites

Someone has asked me for a 2ch version, here it is.

 

      Vcc
       ^                         Vcc GND  
       |     +---------------+    ^  ^    
      +++    | MSP430G2331   |    |  +--+ 
      | |    |               |    +-----+ Servo1
  Pot | |<---+ P1.0     P1.5 +----------+
      | |    |               |              
      +++    |               |           
       |     |               |              
 Vcc   v     |               |    
  ^   GND    |               |   Vcc GND
  |          |               |    ^  ^      
 +++         |               |    |  +--+   
 | |         |               |    +-----+ Servo2
 | |<--------+ P1.1     P1.4 +----------+   
 | |         |               |
 +++         |               |
  |          |               |
  v          |               |
 GND         |               |
             +---------------+

 

#include 

#define SERVO_1 BIT4
#define SERVO_2 BIT5

unsigned int counter = 0;     // Servo counter
unsigned int servo1pos = 180; // Default servo position
unsigned int servo2pos = 180;
unsigned int adcValues[2] = {0,0}; // ADC buffer

void main(void) {
   WDTCTL = WDTPW + WDTHOLD;                 // Stop WDT

   BCSCTL1 = CALBC1_1MHZ;
   DCOCTL = CALDCO_1MHZ;

   P1OUT &= ~(SERVO_1 + SERVO_2);
   P1DIR |= SERVO_1 + SERVO_2;             // Port P1.4-1.5 is out
   P1SEL &= ~(SERVO_1 + SERVO_2);

   ADC10CTL1 = INCH_1 + CONSEQ_1;
   ADC10CTL0 = ADC10SHT_2 + MSC + ADC10ON + ADC10IE;
   ADC10DTC1 = 0x02;
   ADC10AE0 |= 0x03;

   CCTL0 = CCIE;                             // CCR0 interrupt enabled
   CCR0 = 180;                                                           // ~1.5ms
   TACTL = TASSEL_2 + MC_1 + ID_3;           // SMCLK/8, upmode

   _bis_SR_register(LPM0_bits + GIE);        // Enter LPM0 w/ interrupt
}

// Timer A0 interrupt service routine
#pragma vector = TIMERA0_VECTOR
__interrupt void Timer_A (void) {
   counter++;                        // Increase counter
   if(counter == 0x03)
       counter = 0;                // Counter range is 0-2, the last count is used to complete 20ms delay, otherwise analog servos might act funny

   P1OUT &= ~(SERVO_1 + SERVO_2);                       // Clear ports

   if(counter == 0) {
       P1OUT |= SERVO_1;          // Set port of the servo 1
       CCR0 = servo1pos;          // Set time for the servo 1
   } else if(counter == 1) {
       P1OUT |= SERVO_2;          // Set port of the servo 2
       CCR0 = servo2pos;          // Set time for the servo 2
   } else {
       CCR0 = 2500 - (servo1pos + servo2pos); // complete 20ms cycle
       ADC10CTL0 &= ~ENC;
       while (ADC10CTL1 & BUSY)
           ;
       ADC10SA = (unsigned int)adcValues;
       ADC10CTL0 |= ENC + ADC10SC;
   }
}

// ADC10 interrupt service routine
#pragma vector=ADC10_VECTOR
__interrupt void ADC10_ISR(void) {

   servo1pos = (adcValues[0] >> 3) + 0x7F; // copy ADC buffer to servo position variables
   servo2pos = (adcValues[1] >> 3) + 0x7F;
}

Gyula84 and MarkoeZ like this

Share this post


Link to post
Share on other sites

Since we are using Vcc as a Vref, you can use full range from GND to Vcc. Range can be changed either in the code or by manipulating Vrefs. I would start by changing the code.

Share this post


Link to post
Share on other sites

I loaded the 1 servo code and it ran good so I then loaded the 2 servo code, it is a good program as well.

 

I am now running 2 servos on one pot with usb power through the emulator board! But be careful not to put the servos under a load with usb as this can create usb malfunction.

 

RobG Thanks for the code :thumbup: . My next addition i will attempt to code 1 pot , 2 servo out and push button for auto centering.

 

______________________________________________________________________________________________________________

 

 

 

"Reading c or c++ is like juggling 3 chain saws on full while blindfolded."

Share this post


Link to post
Share on other sites

They are needed only if you plan to put 595's outputs in hi-Z state or if your logic is powered down while servos are powered up (from a different source.)

Share this post


Link to post
Share on other sites

Hi Rob,

 

I have an MSP430G2553 and I'd like to control six RC servos independently, and simultaneously, with six pots using all six capture/compare registers of Timer_A0 and Timer_A1.

 

I'm aiming to control a robotic arm with pots and servos and, in order for it to keep its position, each servo has to always have a constantly updated PWM signal. Is this possible? If so, how would the code differ from the code that you posted for the G2331?

 

Thanks for any help!

Share this post


Link to post
Share on other sites

Since you are using 2553, you do not need shift register to control 6 servos.

Using one CCR per servo is not possible (20 pin chip) due to pin function arrangement, but you can still control up to 8 servos using 8 potentiometers.

You can use P1 port for all analog inputs and P2 port for servo output.

HS-645MG and HS-485HB servos are analog, 20ms cycle, looks like they do not need any special treatment.

You can use this 4ch code as an example, just move all servo outs to P2.

Let me know if you need help with that.

 

Here's (untested) code, you may have to adjust min and max position as some servos do not take out of range values too well.

P1.0 - P1.5 is A0 to A5, P2.0 - P2.5 is S0 to S5

 

#include 

#define SERVO_OUTPUTS BIT0 + BIT1 + BIT2 + BIT3 + BIT4 + BIT5 // P2

unsigned int counter = 0;                   // Servo counter
unsigned int servoPosition[6] = { 180, 180, 180, 180, 180, 180 }; // Default servo position
unsigned int servoOn[6] = { BIT0, BIT1, BIT2, BIT3, BIT4, BIT5 };
unsigned char valueIndex = 0;
unsigned int adcValues[6] = {0,0,0,0,0,0};

void main(void) {
   WDTCTL = WDTPW + WDTHOLD;                 // Stop WDT

   BCSCTL1 = CALBC1_1MHZ;
   DCOCTL = CALDCO_1MHZ;

   P2OUT &= ~(SERVO_OUTPUTS);
   P2DIR |= (SERVO_OUTPUTS);             // Port P2.0 - P2.5 servo outs
   P2SEL &= ~(SERVO_OUTPUTS);

   ADC10CTL1 = INCH_5 + CONSEQ_1;
   ADC10CTL0 = ADC10SHT_2 + MSC + ADC10ON + ADC10IE;
   ADC10DTC1 = 0x06;
   ADC10AE0 |= 0x3F;

   CCTL0 = CCIE;                             // CCR0 interrupt enabled
   CCR0 = 180;                               // ~1.5ms
   TACTL = TASSEL_2 + MC_1 + ID_3;           // SMCLK/8, upmode

   _bis_SR_register(LPM0_bits + GIE);        // Enter LPM0 w/ interrupt
}

// Timer A0 interrupt service routine
#pragma vector = TIMER0_A0_VECTOR
__interrupt void Timer_A0(void) {
   counter++;                               // Increase counter
   if(counter == 0x07)
       counter = 0;                // Counter range is 0-6, the last count is used to add 5ms delay

   P2OUT &= ~(SERVO_OUTPUTS);                       // Clear ports
   if(counter == 0x06) {
       CCR0 = 2500 - (servoPosition[0] + servoPosition[1] + servoPosition[2] + servoPosition[3] + servoPosition[4] + servoPosition[5]);
       ADC10CTL0 &= ~ENC;
       while (ADC10CTL1 & BUSY)
           ;
       ADC10SA = (unsigned int)&adcValues[0];
       ADC10CTL0 |= ENC + ADC10SC;
   } else {
       P2OUT |= servoOn[counter];              // Set port of the current servo
       CCR0 = servoPosition[counter];          // Set time for the current servo
   }
}

// ADC10 interrupt service routine
#pragma vector=ADC10_VECTOR
__interrupt void ADC10_ISR(void) {

   while (ADC10CTL1 & BUSY)
       ;
   valueIndex = 0;
   while(valueIndex < 6) {
       servoPosition[5 - valueIndex] = (adcValues[valueIndex] >> 3) + 0x7F;
       valueIndex++;
   }
}

tech_juggernaut likes this

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!


Register a new account

Sign in

Already have an account? Sign in here.


Sign In Now