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.

MSP430G2432: Strange issue when changing duty cycle

Part Number: MSP430G2432

Hi All,

I have an application where I have two PWM outputs which are compliments of one another (i.e. when one is 25% the other is 75%).  I am using timer A to generate both signals (there is only one timer on this chip).  The frequency is 1kHz.

Here is what my code does:

1) PWM1 starts low / PWM2 starts high

2) Every 10 ms, PWM1 goes up by 0.1% while PWM2 goes down by 0.1% until they reach 100% and 0% respectively.   

3) The program waits five seconds.

4) Every 10 ms, PWM2 goes up by 0.1% while PWM1 goes down by 0.1% until they reach 100% and 0% respectively.  The opposite of #2.

5) The program waits five seconds.

6) Steps 2 through 5 are repeated indefinitely.

It works great with the exception of one strange issue.  When the duty cycle of PWM1 goes from ~6% to the next step, the signal stays high for one cycle, then goes back to where it should be.  Here is a screenshot of my scope.  This also happens during turn off from ~95% to the next step.  

Does anyone have any ideas on how to fix the issue?

One thing that I tried was to stop the timer before changing the value in TACCR1 and TACCR2 and starting it back up afterwards.  That did not help.

Thanks in advance,

John

  • In case it is helpful, here is my initialization code...

    void tmra_init(void)
    {
    	TA0CTL = TASSEL_2 | ID_3 | MC_1;  // SMCLK, 1 MHz, Up Mode
    	TACCR0 = 1000 - 1;			// 1 ms overflow -> 1 kHz Frequency
    	TACCTL0 = CCIE;				// Enable Interrupt (Used for 100 us overflow)
    
    	// PWM1 Setup
    	TA0CCTL1 = OUTMOD_6;		// Port 2.6 (pin 19) -> PWM toggle/set mode
    	TACCR1 = 0;					// Start at 0% duty cycle
    	P2SEL |= BIT6;				// Port 2.6 (pin 19) -> CCR1 Output
    
    	// PWM2 Setup
    	TA0CCTL2 = OUTMOD_6;		// Port 1.4 (pin 6) -> PWM toggle/set mode
    	TACCR2 = 0;					// Start at 0% duty cycle
    	P1SEL |= BIT4;				// Port 1.4 (pin 6) -> CCR2 Output
    	P1SEL2 |= BIT4;
    
    	P2DIR |= BIT6;
    	P2SEL |= BIT6;
    	P2SEL2 &= ~BIT6;
    
    	// Port 2.7 Setup
    	P2SEL &= ~BIT7;
    	P2SEL2 &= ~BIT7;
    
    	TA0CTL = TASSEL_2 | ID_3 | MC_1;  // SMCLK, 1 MHz, Up Mode
    }

    And here is where I change the value of the duty cycle.

    void set3000KdutyCycle(uint16_t duty)
    {
    	TACCR1 = duty;
    }
    
    
    
    void set5000KdutyCycle(uint16_t duty)
    {
    	TACCR2 = duty;
    }

  • My first guess is that you're missing a compare, i.e. updating the CCR to a value less than the current TAR value, so the OUTMOD action doesn't happen. This is a hazard with unbuffered CCRs. Updating the CCR immediately following the compare helps. Sometimes choosing a different OUTMOD can make the procedure more fail-safe.

    There's not really enough code here to say where the "6%" trigger comes from, but it wouldn't surprise me if a delay from your (higher priority) CCR0 interrupt had something to do with it.
  • Hi Bruce,

    Thank you for your reply.

    I understand your thought process behind missing a compare due to the CCR value being less than the TAR value.  I added this line of code to prevent that from happening.

    void set3000KdutyCycle(uint16_t duty)
    {
        if (duty <= TAR) {}
        else
        {
            TACCR1 = duty;
        }
    
    }

    Unfortunately, it did not fix it.  It did make it happen less often and at different duty cycles.

    I also tried changing to all the different output modes without success.  Changing it to mode 2 from mode 6 made the signal stay low during the messed up cycles instead of high.

    Do you have any other ideas to try?

    Thanks

  • Unfortunately, your TAR guard has a race in it (if TAR ticks between the if() and the CCR update) and you'll most likely lose the race at the very time you need it.

    I suspect you're updating the duty cycle in your CCR0 interrupt. You may want to try enabling the CCR1/2 interrupts (TIMER0_A1_VECTOR) and updating the relevant CCR there. By updating right after the CCR match, you have a full cycle to get the update in. This isn't a general solution, but in your case, where your increments are small, it should work.

    I think I see where the "6%" comes from. My guess is that your CCR0 interrupt takes roughly 60usec to run. For pulse widths below 60usec, the ISR always misses, but things work out since the CCR matches the next time around (when it misses again). For pulse widths above 60usec, the ISR always hits, and there's a CCR match in the same cycle. At exactly 60usec, you get two CCR matches (one "miss" from the previous round + the new "hit" just written), so it toggles twice. I expect that updating the duty cycle at the CCR match, rather than at CCR0, will eliminate this.
  • You can avoid update glitch of unbuffered CCR if you do it in timer/CCR0 overflow ISR - especially if MCLK is much faster than timer clock. Make sure you run CPU as fast aspossible, at 16MHz. Pseudocode showing idea:

    void set3000KdutyCycle(uint16_t duty)
    {
    temp_TACCR1 = duty;
    }

    __interrupt void CCR0_interrupt_ISR(void)
    {
    TACCR1 = temp_TACCR1;
    }
  • Hi Bruce,

    I think you could be on the right track here.  It makes a lot of sense.

    “I suspect you're updating the duty cycle in your CCR0 interrupt. You may want to try enabling the CCR1/2 interrupts (TIMER0_A1_VECTOR) and updating the relevant CCR there. By updating right after the CCR match, you have a full cycle to get the update in. This isn't a general solution, but in your case, where your increments are small, it should work.”

    I’m not actually updating the duty cycle in CCRO.  However, my code runs in a timed loop which starts when TAR matches TACCR0 (every 1 ms).  Therefore, the duty cycle is getting updated shortly after the CCR0 interrupt.

    I tried to change per your suggestion.  However, apparently I have something set up wrong because I set breakpoints up for each case and the program only stops when the CCR0 overflows and not CCR1 and CCR2 matches.  Do you see anything wrong with this code?

    Here is my initialization...

    void tmra_init(void)
    {
           TA0CTL = TASSEL_2 | ID_3 | MC_1;  // SMCLK, 1 MHz, Up Mode
           TACCR0 = 1000 - 1;                // 1 ms overflow -> 1 kHz Frequency
    
           // PWM1 Setup
           TA0CCTL1 = OUTMOD_6;       // Port 2.6 (pin 19) -> PWM toggle/set mode
           TACCR1 = 0;                              // Start at 0% duty cycle
           P2SEL |= BIT6;                           // Port 2.6 (pin 19) -> CCR1 Output
    
           // PWM2 Setup
           TA0CCTL2 = OUTMOD_6;       // Port 1.4 (pin 6) -> PWM toggle/set mode
           TACCR2 = 0;                              // Start at 0% duty cycle
           P1SEL |= BIT4;                           // Port 1.4 (pin 6) -> CCR2 Output
           P1SEL2 |= BIT4;
    
           P2DIR |= BIT6;
           P2SEL |= BIT6;
           P2SEL2 &= ~BIT6;
    
           // Port 2.7 Setup
           P2SEL &= ~BIT7;
           P2SEL2 &= ~BIT7;
    
           TA0CTL = TASSEL_2 + ID_3 + MC_1 + TAIE;  // SMCLK, 1 MHz, Up Mode, Enable TAIFG Interrupt
    }
    

    And here is the interrupt vector...

    #pragma vector=TIMER0_A1_VECTOR
    __interrupt void timer0_A1_interrupt(void)
    {
        switch (TAIV)
        {
        case TA0IV_TACCR1:
            if (prev3000KDuty != currDutyCycle3000K)
            {
                TACCR1 = currDutyCycle3000K;
                prev3000KDuty = currDutyCycle3000K;
            }
            else {}
            break;
        case TA0IV_TACCR2:
            if (prev5000KDuty != currDutyCycle5000K)
            {
                TACCR2 = currDutyCycle5000K;
                prev5000KDuty = currDutyCycle5000K;
            }
            else {}
            break;
        case TA0IV_TAIFG:
            one_ms_elapsed = TRUE;
            break;
        default:
            break;
        }
    
    }
    

    I have breakpoints set on lines 7, 15, and 23 of the interrupt vector.  It is only stopping on 23.

    Thank you very much for your help.

    John

  • Updating CCR1 in CCR1 match ISR will cause problems when CCR1 value is incremented - you will get double CCR match during single timer cycle which in case of toggle PWM mode obviously will cause glitch. 

    Better put CCR1/CCR2 update in timer overflow interrupt, not "in a timed loop which starts when TAR matches TACCR0 (every 1 ms).".

  • Hi llmars,

    Thanks for your reply.  I think I didn't explain what I'm doing well.  When the TAR matches TACCRO, I set a flag.  Then, my main loop only consists of checking and clearing the flag and running my routines.

     while (1u)
        {
    		if (one_ms_elapsed)
    		{
    			one_ms_elapsed = FALSE;
    
    			light_task();
    			timer_task();
    
    			CLR_WDT_M();
    
    		}
        	else {}
    
        }

    I set this flag in the TIMER0_A0_VECTOR at first, but I changed to the TIMER0_A1_VECTOR per Bruce's suggestion.   

  • Don't worry, I got what you are doing. Thing is that your polling cycle approach is much more CPU-intensive than what I offer. Timer clock is 1MHz, CPU max freq is 16MHz meaning just 16 CPU cycles per timer tick. If you want glitchless CCR updates at low CCR values, you shall not be wasting CPU time like you do. Even ISR approach could be too late when CCR1/CCR2 is equal 1 and going to be 2 after update, but I can't tell - you shall try (obviously if you choose to).

  • Hi John,

    After reviewing the code you posted where the CCRx interrupts weren't working, I see you haven't enabled the CCRx interrupt in the TA0CCTLx register. Try doing this and see if the interrupts start to work. Also, let me know if you're still experiencing the duty cycle issue.

    Best regards,
    Caleb Overbay
  • Hi Caleb,

    Thanks for your reply.  That fixed that issue.  Unfortunately, I still have the weird duty cycle issue after trying this suggestion.  Any other ideas?

    Thanks,

    John

  • I should probably be more specific about what I tried :)

    Bruce suggested updating the duty cycles when each of the compares happen.  So, I changed my code in such a way that instead of actually updating the TACCRx register, it sets a global variable for the duty cycle and a flag to change it during the next compare interrupt.  

    ....
    
    currDutyCycle5000K++;
    update5000KPWM = TRUE;
    
    ....

    #pragma vector=TIMER0_A1_VECTOR
    __interrupt void timer0_A1_interrupt(void)
    {
        switch (TAIV)
        {
        case TA0IV_TACCR1:
            if (update5000KPWM)
            {
                TACCR2 = currDutyCycle5000K;
                update5000KPWM = FALSE;
            }
            else {}
            break;
        case TA0IV_TACCR2:
            if (update3000KPWM)
            {
                TACCR1 = currDutyCycle3000K;
                update3000KPWM = FALSE;
            }
            else {}
            break;
        case TA0IV_TAIFG:
            one_ms_elapsed = TRUE;
            break;
        default:
            break;
        }
    
    }

  • Hi John,

    Thanks for the clarification. I think this approach is a little flawed as Ilmars pointed out:

    "Updating CCR1 in CCR1 match ISR will cause problems when CCR1 value is incremented - you will get double CCR match during single timer cycle which in case of toggle PWM mode obviously will cause glitch. 

    Better put CCR1/CCR2 update in timer overflow interrupt, not "in a timed loop which starts when TAR matches TACCR0 (every 1 ms)."

    Can you try a similar approach to what you've done above, but instead of changing the CCRx value in the CCRx interrupt, change it in the CCR0 interrupt when the respective updatexxxxKPWM flag has been set?

    Best regards, 
    Caleb Overbay

  • Hi Caleb,

    I changed my interrupt routine to this.

    #pragma vector=TIMER0_A1_VECTOR
    __interrupt void timer0_A1_interrupt(void)
    {
        switch (TAIV)
        {
        case TA0IV_TACCR1:
            break;
        case TA0IV_TACCR2:
            break;
        case TA0IV_TAIFG:
            one_ms_elapsed = TRUE;
    
            if (update5000KPWM)
            {
                TACCR2 = currDutyCycle5000K;
                update5000KPWM = FALSE;
            }
            else {}
    
            if (update3000KPWM)
            {
                TACCR1 = currDutyCycle3000K;
                update3000KPWM = FALSE;
            }
            else {}
    
            break;
        default:
            break;
        }
    
    }

    When I ran it that way, it did something really strange.  The output would go high for 2.5 us and back low.  

    Then, I remove the CCIE enable from each TA0CCTLx register to see if that would work and since they are irrelevant now anyway.

    // PWM1 Setup
    //TA0CCTL1 = OUTMOD_6 | CCIE;		// Port 2.6 (pin 19) -> PWM toggle/set mode
    TA0CCTL1 = OUTMOD_6;
    TACCR1 = 0;					// Start at 0% duty cycle
    P2SEL |= BIT6;				// Port 2.6 (pin 19) -> CCR1 Output
    
    // PWM2 Setup
    //TA0CCTL2 = OUTMOD_6 | CCIE;		// Port 1.4 (pin 6) -> PWM toggle/set mode
    TA0CCTL2 = OUTMOD_6;
    TACCR2 = 0;					// Start at 0% duty cycle
    P1SEL |= BIT4;				// Port 1.4 (pin 6) -> CCR2 Output
    P1SEL2 |= BIT4;

    It worked after that, but the duty cycle issue is still happening.

  • Hi John,

    What frequency are you running the CPU at? Have you tried increasing it so the interrupt will execute faster and give you more time to alter the CCRx registers?

    Also, I could try replicating the issue on my end if you're willing to provide a more complete code example.

    Best regards,
    Caleb Overbay
  • Hi Caleb,

    I've been running at 8 MHz. I just increased it to 16 MHz. No luck. That would be great if you could test it. How do you want me to get my code to you?

    Thanks,
    John
  • Hi John,

    I've just sent you a friend request. Once you accept it, you can send me a personal message containing your code.

    Best regards,
    Caleb Overbay
  • Hi John,

    I'll take a look at it and get back to you soon.

    Best regards,
    Caleb Overbay
  • Hi John,

    I've been able to test out your code and replicate the issues you've been experiencing. I've also tried all the workarounds we've suggested throughout this thread and reached the same results as you. The good thing here is that we can replicate it and we're both on the same page.

    Unfortunately, I still don't have an explanation for this issue. As you've described, even stopping the timer and then updating the CCRx values doesn't solve the issue. This is something that concerns me because it is the "correct" way to modify these values without seeing race conditions.

    I need to do some more testing and I'll work with our quality team to find out what is really going on here. I'll keep you updated throughout the process.

    Best regards,
    Caleb Overbay
  • Caleb Overbay said:
    I've been able to test out your code and replicate the issues you've been experiencing.

    Just curious - at which CCR1/CCR2 range update glitch problems occur? Is it low range <= 2?

    If it is so, then I would implement CCR1 update following way which wait for timer to run away "at safe distance" from CCR1, then update it:

    If (TACCR1 < 3)
      __delay_cycles(16*50); // assuming timerfreq == 1MHz and CPUfreq == 16MHz
    TACCR1 = currDutyCycle3000K;
    update3000KPWM = FALSE;
    

  • Caleb Overbay said:
    Hi John,

    I've been able to test out your code and replicate the issues you've been experiencing. I've also tried all the workarounds we've suggested throughout this thread and reached the same results as you. The good thing here is that we can replicate it and we're both on the same page.

    Unfortunately, I still don't have an explanation for this issue. As you've described, even stopping the timer and then updating the CCRx values doesn't solve the issue. This is something that concerns me because it is the "correct" way to modify these values without seeing race conditions.

    I need to do some more testing and I'll work with our quality team to find out what is really going on here. I'll keep you updated throughout the process.

    Best regards,
    Caleb Overbay

    Hi Caleb,

    Thanks for the update.  It's good that you can repeat the issue.  Let me know if you want me to try anything on my end.

    Thanks,

    John

  • Hi Ilmars, 

    After looking at where the glitch occurs, I've calculated that it's somewhere in the range of when CCR1 transitions from a value of 6 to a value of 7. There should be plenty of time for the CCR0 interrupt to update the register. Even when I stopped the timer and then updated the value in CCR1, the issue was still occurring.

    I'll test out your above theory and let you all know the results. 

    Best regards, 
    Caleb Overbay

  • Caleb Overbay said:
    After looking at where the glitch occurs, I've calculated that it's somewhere in the range of when CCR1 transitions from a value of 6 to a value of 7.

    CPU @8MHz have just 8 clock cycles to execute during tick of timer which is running at 1MHz. During 6 timer ticks CPU possibly could not even finish to handle whole ISR and is right at CCR update instruction while timer already reached value 6 & CCR match occurred, PWM output toggled. Then CPU changes CCR value to 7 and at next timer tick next match occur (during same PWM period), output toggle again, you see glitch on scope. In set/reset or reset/set mode you would not see the glitch.

    I would suggest to reconsider PWM mode, change it from toggle to set/reset and/or reset/set and close this thread of timer/CPU race condition fishing :)

  • Hi Ilmars,

    This was what I was expecting as well, but changing the output mode had no effect. I do agree that a toggle output mode shouldn't be used for this application.

    Some things I'm going to try today:

    • Changing the 10ms updated period to 20ms
    • Updating in the CCRx interrupt after some delay
    • Trying on another part with TimerA module

    Best regards, 
    Caleb Overbay

  • Hi John,

    I have some good news. It appears that there is a very unique race condition going on here, I still haven't found the exact root cause. However, I am able to alleviate the issue by using Ilmar's suggestion of adding a delay before updating CCR1 if it's value is less than 7:

            if (update3000KPWM)
            {
                if(TACCR1 < 7)
                  __delay_cycles(8*50);         //Assuming timerfreq == 1MHz and CPUfreq == 8MHz
                TA0CTL &= ~MC_1;                //Stop Timer
                TACCR1 = currDutyCycle3000K;
                TA0CTL |= MC_1;                 //Start Timer
                update3000KPWM = FALSE;
            }
            else {}

    By adding this delay we are ensuring the value in TAR is at least 50 before updating CCR1 to 7. This avoids the race condition that was occurring because of the update from 6 to 7.  Also, I highly recommend changing your OUTMOD to Reset/Set. This way we know that the PWM falling edge is always when CCRx is reached and the rising edge is always when CCR0 is reached.

    I'll do a little more digging into this to discover a root cause but can you try this out and let me know your results.

    Best regards, 

    Caleb Overbay

  • Great!  I'll test it out Monday morning and let you know the results.  Thanks for your input, .

  • Caleb Overbay said:
    By adding this delay we are ensuring the value in TAR is at least 50 before updating CCR1 to 7. This avoids the race condition that was occurring because of the update from 6 to 7.

    Glad you are making progress, working on it so hard. IMHO timer restart is superflous, it also does not help PWM frequency to be constant. BTW good idea in such cases as this is to use debug pin - set it before CCR update and reset after - to see when exactly CCR update respectively to PWM cycle state, occur.

  • Hi  and .

    That fixed the issue with the code that Caleb has.  Unfortunately, when I put the fix back in to my production code, the very last cycle on the TACCR1 line stays high (when the TACCR2 line is off).  

    Caleb,

    I will send you my code in a PM.

    Thanks,

    John

  • John Deaton said:
    I will send you my code in a PM.

    Did you try to understand why this happens? Or just "I will send you my code, fix it".

  • What gave you the impression that I don't want to understand the issue?

  • I hesitate to disagree with the heavyweights here, but I still recommend my original solution. For this application, where each change is a fixed delta of 8 CPU clocks, you will always win the race since you can't get through the ISR in 8 CPU clocks. (Probably not in 16,24 or 32 clocks either.)

    The cause of the original symptom was not mysterious (I think I explained it up there), and it resulted from doing the CCR update based on CCR0; since software can't update the CCR in zero time, there will always be a race with small CCRn values. (By contrast, the buffered CCRs in TimerB are updated in zero time, since there's hardware to do that.) By updating at the compare match, you extend the allowable latency to an entire cycle (minus a little).

    Your original A1 ISR was pretty much what I suggested, except that (1) you forgot the CCIE settings and (2) you were doing the CCR updates backwards (TAIV_CCR2 should update TA0CCR2).

    As I said, this is not a general solution, but it should accomplish what you're trying to do.
  • John Deaton said:
    the very last cycle on the TACCR1 line stays high (when the TACCR2 line is off).  

    Instead of trying to stop timer at right time and w/o glitch, just switch off PWM outputs, then disable timer: 1) set desired values for PWM out pins in PxOUT 2) change PW output pin function to GPIO output 3) disable timer.

  • Hi , , and ,

    I think that it may be working now!  I went back to Bruce's suggestion of putting the updates in the A1 vector.  This caused one problem... once I set the duty cycles to either zero or full on, the TA0IV_TACCRx interrupts would no longer be called.  To get around this, I kept up with the previous duty cycle and re-set the duty cycle in the TA0IV_TAIFG interrupt if it started at 0 or 1000.  Here is my interrupt routine which seems to be working so far.

    #pragma vector=TIMER0_A1_VECTOR
    __interrupt void timer0_A1_interrupt(void)
    {
    	switch (TAIV)
    	{
    	case TA0IV_TAIFG:
    	    one_ms_elapsed = TRUE;
    
    	    if (prevDutyCycle3000K == 0 && currDutyCycle3000K != 0)
    	    {
    	        TACCR1 = currDutyCycle3000K;
                prevDutyCycle3000K = currDutyCycle3000K;
    	    }
    	    else {}
    	    if (prevDutyCycle5000K == 0 && currDutyCycle5000K != 0)
            {
                TACCR2 = currDutyCycle5000K;
                prevDutyCycle5000K = currDutyCycle5000K;
            }
            else {}
    
    	    if (prevDutyCycle3000K == 1000 && currDutyCycle3000K != 1000)
            {
                TACCR1 = currDutyCycle3000K;
                prevDutyCycle3000K = currDutyCycle3000K;
            }
            else {}
            if (prevDutyCycle5000K == 1000 && currDutyCycle5000K != 1000)
            {
                TACCR2 = currDutyCycle5000K;
                prevDutyCycle5000K = currDutyCycle5000K;
            }
            else {}
    
    	    break;
    	case TA0IV_TACCR1:
    	    if (update3KDuty)
            {
                update3KDuty = FALSE;
                TACCR1 = currDutyCycle3000K;
                prevDutyCycle3000K = currDutyCycle3000K;
            }
            break;
    	case TA0IV_TACCR2:
    	    if (update5KDuty)
            {
                update5KDuty = FALSE;
                TACCR2 = currDutyCycle5000K;
                prevDutyCycle5000K = currDutyCycle5000K;
            }
            break;
    	default:
    	    break;
    	}
    
    }

    I'll let you guys know if I see another glitch.  

  • I also started the duty cycle at 1% (10 counts) to get around the other issue.
  • Hi John,

    Glad to hear you've made progress. Your application is an uncommon use of TimerA so hiccups like this can be expected. I think the solution you've outlined above may be the best possible solution with TimerA's limitations. Is this working for you or do you have any other questions?

    Best regards,
    Caleb Overbay
  • Hi Caleb,

    Thanks for checking in.  Yes, it seems to be working.  Do you mean for this application I should have chosen a MCU with Timer B?  Unfortunately, I had to pick the cheapest option that would work as often happens :). 

    Regards,

    John