This thread has been locked.

If you have a related question, please click the "Ask a related question" button in the top right corner. The newly created question will be automatically linked to this question.

Servo (PWM)+ ADC MSP430G2553

I know this is a recursive topic. I have been reading a lot on forums and actually tried the code proposed. With a lot of source code to base my project on I made the following code.

#include <msp430g2553.h>

#define MCU_CLOCK           1100000
#define PWM_FREQUENCY       46      // In Hertz, ideally 50Hz.
#define SERVO 				BIT2
#define SERVO_MAX 			3000
#define SERVO_MIN  			700

unsigned int PWM_PERIOD = (MCU_CLOCK / PWM_FREQUENCY);
unsigned int PWM_DUTY = 0;
unsigned int servo1pos = 180;	// Default servo position
unsigned int adcValue = 0; 		// ADC buffer (stores ADC value)

void main(void) {
    WDTCTL = WDTPW + WDTHOLD;     // Stop WDT
    // Initialize Clocks
    BCSCTL1 = CALBC1_1MHZ;    // Set range
    DCOCTL = CALDCO_1MHZ;     // SMCLK = DCO = 1MHz

    // Configure Pins
    P1DIR |= SERVO ;          // P1.4 & P1.5 are outputs
    P1SEL |= SERVO;
	

    // Configure ADC
    ADC10CTL1 = INCH_0 ;  // A1/A0, repeat multi channel
    ADC10CTL0 = ADC10SHT_2 + ADC10ON + ADC10IE;

    // Configure PWM with TIMERA
    CCTL0 = CCIE;                             // CCR0 interrupt enabled
    TACCR0 = PWM_PERIOD;  							  // Set limit
    TACCR1 = PWM_DUTY;
    TACTL = TASSEL_2 + MC_1 + ID_3;           // Timer_A is using 125kHz
    _bis_SR_register(LPM0_bits + GIE);        // Enter LPM0 w/interrupt
}

// CCR0 interrupt service routine
#pragma vector = TIMER0_A0_VECTOR
__interrupt void Timer0_A0 (void) {
    TACCR1 = servo1pos;          		// Set time for servo
    __delay_cycles(1000000);			// 1 second delay 
    ADC10CTL0 |= ENC + ADC10SC;     	// Start sampling
}

// ADC interrupt service routine
#pragma vector=ADC10_VECTOR
__interrupt void ADC10_ISR(void) {
    adcValue = ADC10MEM;					// Get sample and modify duty cycle
    servo1pos = (adcValue) + SERVO_MIN;		// Scale the value
}

I connected the servo to the TP1 pin on my Launchpad for VCC and the GND of the servo from the Launchpad. The signal goes directly from P1.2 to the servo. Fixed some things in the code, now got the servo to move, unpredictable, but it moves. Servo is fine so the problem now appears to be the scaling of the potentiometer read and the PWM is not varying its duty cycle. Will keep trying to fix it and update if I get positive results. 

  • Hi Werner,

    A handful of thoughts:

    • What is the voltage level for VCC on the servo?  If you are using the 3.3V Vcc on the launchpad, this could potentially cause issues.  Most servos (that I have experience with) like to have a Vcc in the 4V to 6V range, and will sometimes have erratic behavior at 3.3V Vcc.  When controlling a servo with a launchpad, I typically use a 5V source to power the servo, then let the 3.3V PWM control the position (3.3V is typically sufficient for the PWM, but there is the off chance that it may require a level shifter).
    • Why are you using 1.1MHz as your constant for MCLK?  You are using the calibrated DCO values for 1MHz, so just use 1000000.
    • Why are you using 46Hz for your PWM frequency constant?  Why not just use 50Hz (as expected by the servo)?
    • Why are you using Timer0_A0 interrupt?  This, combined with the 1 second delay is causing your ADC sample to not run during the "__delay _cycles()" instruction (since the 430 does not do nested interrupts by default).  You shouldn't need the timer ISR at all, the hardware will automatically create the PWM for you with no additional input once it is set up correctly.
    • Since you are using SMCLK/8 (125KHz) as the clock source for the timer, but your calculations for PWM period use 1.1MHz, you are not going to end up with the correct frequency.
    • Take a look at some of the example code for how to create a PWM using output mode 7 (Rest/Set), this will help you understand how to create the PWM.
    • When calculating the PWM duty cycle (from the ADC, you need to add some sort of scaling to make the ADC input range match to the usable PWM duty cycle range for a servo (typically 5 to 10% duty cycle).

    My suggestion is to redo the constants at the top of code to give better values (MCLK, PWM_PERIOD/DUTY, etc).  Then correctly set up the PWM from these values.  Use the ADC ISR to scale the possible ADC values to the usable range (5 to 10% duty cycle).  The G2553 has a second Timer_A, so you could use this to add delay between ADC sample intervals.  The best way would be to use the ADC10 with setting the Sample and Hold Source Select to use the timer as the trigger for the ADC conversion (see the code examples for an example on how to do this).  The final consideration would be that when you change the PWM duty cycle, you need to check for the case where you are setting CCR1 below the current TAR, which would cause the counter to loop through to CCR0 before changing the output.  To check for this, when you go to change the CCR1 value, first halt the timer, compare the new CCR1 to TAR, and if TAR is greater, reset the counter when you restart the timer module after updating CCR1.  This should help prevent weird glitches when changing the PWM duty cycle.

    Mike

  • Werner,

    this one here is an absolute NO-GO:

    // CCR0 interrupt service routine
    #pragma vector = TIMER0_A0_VECTOR
    __interrupt void Timer0_A0( void )
    {
      TACCR1 = servo1pos;          // Set time for servo
      __delay_cycles(1000000);     // 1 second delay     <----- NO WAY! You are in an interrupt!
      ADC10CTL0 |= ENC + ADC10SC;  // Start sampling
    }

    Try to get rid of all those delay_cycles(). There is absolutely no need for them. At least not during the main program flow. In the init before the main program starts it might be OK. I did almost the same as you want to do. Maybe my sorce code for that helps. I removed some parts of it because I had an additional multiplexed seven segment display connected. This was most of the code and it would have only been confusing. So sorry, if I maybe deleted a thing I thought it was belonging to the display but was necessary for the servo control.

    /*
     * main.c
     *
     *  Created on: 13.11.2014
     *      Author: Dennis Eichmann
     */
    
    
    
    // ------------------------------------------------- MSP430G2553 Microcontroller
    //
    //                           MSP430G2553
    //                          -------------
    //                   VCC  =| 1 o      20 |=  GND
    //      BUTTON LEFT P1.0  =| 2        19 |=  P2.6 PWM OUTPUT
    //     BUTTON RIGHT P1.1  =| 3        18 |=  
    //    BUTTON CENTER P1.2  =| 4        17 |=  TEST/SBWTCK
    //       BUTTON SET P1.3  =| 5        16 |=  RST/NMI/SBWTDIO
    //         VREF OUT P1.4  =| 6        15 |=  
    //                        =| 7        14 |=  P1.6 ANALOG IN
    //                        =| 8        13 |=  
    //                        =| 9        12 |=  
    //                        =| 10       11 |=  
    //                          -------------
    //
    // -----------------------------------------------------------------------------
    
    
    
    #include "msp430g2553.h"
    #include <stdint.h>
    
    
    
    #define ADC_VAL_CCR_FACTOR      97752
    #define ADC_VAL_CCR_DIVIDER     100000
    
    #define BUTTON_LEFT_PRESSED     !( P1IN & 0x01 )
    #define BUTTON_RIGHT_PRESSED    !( P1IN & 0x02 )
    #define BUTTON_CENTER_PRESSED   !( P1IN & 0x04 )
    #define BUTTON_SET_PRESSED      !( P1IN & 0x08 )
    
    
    
    volatile uint32_t adc10_value = 511;           // Preset ADC result with 1.5ms
             uint16_t timer_value = 1500;          // Preset timer value with 1.5ms
    
    
    
    void main( void )
    {
      WDTCTL     = ( WDTPW | WDTHOLD );            // Stop watchdog timer
    
      BCSCTL1    =  CALBC1_1MHZ;                   // Set DCO range
      DCOCTL     =  CALDCO_1MHZ;                   // Set DCO step and modulation
    
      P1REN      =  ( 0x01 | 0x02 | 0x04 | 0x08 ); // P1.0/1/2/3 input buttons resistor enable
      P1OUT      =  ( 0x01 | 0x02 | 0x04 | 0x08 ); // P1.0/1/2/3 input buttons pull-up resistor
    
      P2SEL      =  0x40;                          // P2.6 signal output PWM TA0.1
      P2DIR      =  0x40;                          // P2.6 set to output
    
      TA0CCR0    =  20000;                         // Period
      TA0CCR1    =  1500;                          // Duty-cycle for P1.6
      TA0CCTL1   =  OUTMOD_7;                      // Reset/set for P1.6
      TA0CTL     =  ( TASSEL_2 | MC_1 );           // SMCLK, up-mode
    
      // Vref+/Vss, 64 clocks S&H, ref output on, 2,5V ref, ref on, ADC10 on, interrupt enable
      ADC10CTL0  =  ( SREF_1 | ADC10SHT_3 | REFOUT | REF2_5V | REFON | ADC10ON | ADC10IE );
      ADC10CTL1  =  ( INCH_6 | ADC10DIV_7 );        // Input channel A6 on P1.6, clock divided by 8
      ADC10AE0   =  0x40;                           // Analog input A6 enabled
    
      __bis_SR_register( GIE ); // Enable global interrupts
    
      ADC10CTL0 |= ( ENC | ADC10SC ); // Enable and start conversion
    
      while( 1 ) // Endless-loop (main-program)
      {
        if( BUTTON_SET_PRESSED )
        {
          timer_value = (1000 + ((adc10_value * ADC_VAL_CCR_FACTOR) / ADC_VAL_CCR_DIVIDER)); // Set duty-cycle to pot-adjusted value
        }
        else if( BUTTON_LEFT_PRESSED )
        {
          timer_value = 1000; // Set duty-cycle to 1.0ms
        }
        else if( BUTTON_RIGHT_PRESSED )
        {
          timer_value = 2000; // Set duty-cycle to 2.0ms
        }
        else if( BUTTON_CENTER_PRESSED )
        {
          timer_value = 1500; // Set duty-cycle to 1.5ms
        }
    
        if( timer_value < 1000 )
        {
          timer_value = 1000;
        }
        else if( timer_value > 2000 )
        {
          timer_value = 2000;
        }
    
        TA0CCR1 = timer_value;
      }
    }
    
    
    
    #pragma vector = ADC10_VECTOR
    __interrupt void ADC10_ISR ( void )
    {
      adc10_value = ADC10MEM;
      ADC10CTL0 |= ( ENC | ADC10SC ); // Enable and start conversion
    }

    I had four buttons, servo to the left, servo to the right, servo center and reading the potentiometer position. All buttons between pin of the micro and GND. The pot was connected to the refence voltage of the micro. No timer ISR, this is done in hardware.

    Maybe that helps a little bit.

    Dennis

  • Additional to Mike,

    Change: “unsigned int PWM_PERIOD = (MCU_CLOCK / PWM_FREQUENCY);” to “unsigned int PWM_PERIOD = ((unsigned long)MCU_CLOCK / PWM_FREQUENCY);” to avoid casting error. Also you could make it a constant instead of a variable.

  • The idea of the TIMERA interrupt was to modify the value of CCR1 only when TAR the relationship with TAR would not make a loop. I guess it is not necessary anyways. Thanks for the suggestions.
  • Would the change of the CCR1 in the While provoke issues with the Timer? In reference to what Mike said about the TAR and CCR1 messing up the TIMER.
  • Well, if I understand Mike right, then it is another scenario than the one in my code. My timer is running in up mode and the value written to CCR0 always sets the output high. CCR1 resets it to low. Of course CCR1 can never be greater than CCR0 since CCR0 sets the period, which is fixed (50Hz/20ms in this case). For proper operation your code always has to ensure that CCR1 is smaller than CCR0. A change of CCR1 will

    • immediately take effect if TAR is smaller than the new CCR1
    • will take effect the next round when TAR starts to count up again if TAR has already passed CCR1 and the output is already low

    But this is no problem at all, I think. If I'm wrong with that, please correct me.

    Dennis

  • Maybe has time to clear things up for us.
  • Dennis,

    Thanks for the clarity.  I actually misspoke in my original post about the count value potentially going up to 0xFFFF.  This can only happen if you modify CCR0, which should never be done when creating PWMs for servo control.  Dennis is correct that in the situation where you set CCR1 to a new value that is less than the current count value in TAR, the impact is that the PWM will not change for the rest of that timer period and expected operation will begin when CCR0 is reached and the count resets to 0.  For servos, this shouldn't be an issue, as it won't move very far in the 20ms that you get the weird PWM output.  However, if you need the PWM to be absolutely perfect, there are some steps you can take.

    I'll update my previous post to correct my comment.

    Mike

  • I'm not sure whether I can help here., but there are a few things I noticed:
    1) SERVO is defined as BIT2 while the comments in the code speak about P1.4 and P1.5. Well, P1.2 is TA0.1 output, so it seems to fit.
    2) The ADC is configured for single channel A0, but the comment talks about A0/A1 multi channel, which would (if used) require a completely different code flow.
    3) Period is defined as CLOCK/FREQUENCY. This is indeed the number of required timer ticks per period, but since the count to 0 is a tick too, CCR0 needs to be set to one tick less.
    4) the DCO is configured for calibrated 1MHz, so MCU_CLK is far closer to 1MHz than to the defined 1.1MHz (10% of period)
    5) with 46Hz and 1.1MHz configured clock, you have 24000 timer ticks per cycle. Yet your duty cycle only varies between 700 and 1724 ticks, which is a duty cycle range from 2.9% to 7.2%. With 42Hz PWM frequency. Assuming the potentiometer value spans the full range from 0V to VCC

    Dennis already mentioned the delay. A delay inside an ISR is a no-go. ISRs are no parallel threads. ISRs are interruptions of the code flow. Doing delays there is a death sin. And a sign of an error in the basic concept.
    You can e.g. count a variable inside the timer ISR and start the ADC only every N'th interrupt. But never, never do a delay.

    The other problem has been pointed out by Mike: Writing a value to CCR0 that is lower than the current TAR value results in one 100+x% duty cycle, which may or may not be a big problem. Especially if you update your DC often.
    TimerB (unfortunately not available on your MSP) has a mode to latch the written value and update it on next overflow by hardware.

    Also, it is not possible to set the DC to 0% _and_ 100%. Depending on output polarity (OUTMOD), only one of the two is possible, as both indicate the same count value on the number circle. (after all, with 0% or 100% DC, you can't talk about a PWM frequency at all, as it is a flat line without any period)

**Attention** This is a public forum