6
\$\begingroup\$

I'm trying to change the PWM output frequency roughly once a millisecond using a dsPIC33FJ256GP710, and I'm having mixed results. I first tried this:

 #include <p33fxxxx.h> 

 _FOSCSEL(FNOSC_PRIPLL); 
 _FOSC(FCKSM_CSDCMD & OSCIOFNC_OFF & POSCMD_XT); 
 _FWDT(FWDTEN_OFF); 

 static unsigned int PWM_TABLE[7][2] = 
 { 
     {132, 66}, {131, 66}, {130, 65}, {129, 65}, {128, 64}, {127, 64}, {126, 63} // Compare, 50% duty 
 }; 

 static int curFreq = 0; 

 int main(void) 
 { 
     int i; 

     PLLFBD = 0x009E;                // Set processor clock to 32 MHz (16 MIPS) 
     CLKDIV = 0x0048;  

     LATCbits.LATC1 = 0;             // Make RC1 an output for a debug pin 
     TRISCbits.TRISC1 = 0;     

     LATDbits.LATD6 = 0;             // Make RD6/OC7 an output (the PWM pin) 
     TRISDbits.TRISD6 = 0; 

     T2CONbits.TON = 0;              // Disable Timer 2                      
     OC7CONbits.OCM = 0b000;         // Turn PWM mode off 
     PR2 = PWM_TABLE[curFreq][0];    // Set PWM period 
     OC7RS = PWM_TABLE[curFreq][1];  // Set PWM duty cycle 
     OC7CONbits.OCM = 0b110;         // Turn PWM mode on 
     T2CONbits.TON = 1;              // Enable Timer 2 

     while (1) 
     {                 
         for (i = 0; i < 3200; i++) {}      // Delay roughly 1 ms         
         curFreq = (curFreq + 1) % 7;       // Bump to next frequency        
         PR2 = PWM_TABLE[curFreq][0];       // Set PWM period 
         OC7RS = PWM_TABLE[curFreq][1];     // Set PWM duty cycle 
         LATCbits.LATC1 = !LATCbits.LATC1;  // Toggle debug pin so we know what's happening         
     } 
 } 

The result is that PWM drops out for about 4 ms at what looks to be a repeatable interval, roughly aligned with my debug pin toggle (in other words, when the code is messing with the period and duty cycle registers). I'll attach a photo of my scope trace. Channel 1 is PWM and channel 2 is the debug pin that's toggled when the code attempts to adjust the frequency.

Anyway, I started thinking about timer rollovers, and I did some searching on a few forums. I came up with a few ideas based on a few posts I read. The best idea seemed to be to enable the Timer 2 interrupt, turn PWM mode off inside it, and only change the period and duty cycle registers inside the Timer 2 interrupt. So, I wrote this:

 #include <p33fxxxx.h> 

 _FOSCSEL(FNOSC_PRIPLL); 
 _FOSC(FCKSM_CSDCMD & OSCIOFNC_OFF & POSCMD_XT); 
 _FWDT(FWDTEN_OFF); 

 static int curFreq = 0; 

 static unsigned int PWM_TABLE[7][2] = 
 { 
     {132, 66}, {131, 66}, {130, 65}, {129, 65}, {128, 64}, {127, 64}, {126, 63} // Compare, duty 
 }; 

 int main(void) 
 { 
     int i, ipl; 

     PLLFBD = 0x009E;                // Set processor clock to 32 MHz (16 MIPS) 
     CLKDIV = 0x0048;  

     LATCbits.LATC1 = 0;             // Make RC1 an output for a debug pin 
     TRISCbits.TRISC1 = 0;     

     LATDbits.LATD6 = 0;             // Make RD6/OC7 an output (the PWM pin) 
     TRISDbits.TRISD6 = 0; 

     OC7CONbits.OCM = 0b000;         // Turn PWM mode off     
     OC7RS = PWM_TABLE[curFreq][1];  // Set PWM duty cycle 
     PR2 = PWM_TABLE[curFreq][0];    // Set PWM period 
     OC7CONbits.OCM = 0b110;         // Turn PWM mode on 

     T2CONbits.TON = 0;              // Disable Timer 2 
     TMR2 = 0;                       // Clear Timer 2 register     
     IPC1bits.T2IP = 1;              // Set the Timer 2 interrupt priority level 
     IFS0bits.T2IF = 0;              // Clear the Timer 2 interrupt flag 
     IEC0bits.T2IE = 1;              // Enable the Timer 2 interrupt 
     T2CONbits.TON = 1;              // Enable Timer 2 

     while (1) 
     {                 
         for (i = 0; i < 1600; i++) {}      // Delay roughly 1 ms 
         SET_AND_SAVE_CPU_IPL(ipl, 2);      // Lock out the Timer 2 interrupt 
         curFreq = (curFreq + 1) % 7;       // Bump to next frequency         
         RESTORE_CPU_IPL(ipl);              // Allow the Timer 2 interrupt 
         LATCbits.LATC1 = !LATCbits.LATC1;  // Toggle debug pin so we know what's happening 
     } 
 } 

 void __attribute__((__interrupt__)) _T2Interrupt(void) 
 {     
     T2CONbits.TON = 0;              // Disable Timer 2 
     TMR2 = 0;                       // Clear Timer 2 register 
     OC7CONbits.OCM = 0b000;         // Turn PWM mode off 
     OC7RS = PWM_TABLE[curFreq][1];  // Set the new PWM duty cycle 
     PR2 = PWM_TABLE[curFreq][0];    // Set the new PWM period     
     OC7CONbits.OCM = 0b110;         // Turn PWM mode on 
     IFS0bits.T2IF = 0;              // Clear the Timer 2 interrupt flag 
     T2CONbits.TON = 1;              // Enable Timer 2 
 }

This looks to be more stable as far as I can tell on my ancient scope, but now the waveform is no longer regularly-shaped (the duty cycle seems to be inexplicably inconsistent) and if I try hard enough I can convince myself that I still see a millisecond of PWM dropout when my scope is set to a 5 or 10 millsecond timebase.

It's certainly better than it was, and I can continue to mess with it in the hopes of fixing the irregular waveform produced by the second bit of code, but my question is:

Is there a "right" way to do this? Or at least a better way than the path I'm on?

Any help would be thoroughly, thoroughly appreciated.

Scope trace http://www.freeimagehosting.net/uploads/c132216a28.jpg

\$\endgroup\$
10
\$\begingroup\$

For anyone who's interested, here is the solution I arrived at today:

#include <p33fxxxx.h>

_FOSCSEL(FNOSC_PRIPLL);
_FOSC(FCKSM_CSDCMD & OSCIOFNC_OFF & POSCMD_XT);
_FWDT(FWDTEN_OFF);

static int curFreq = 0;
static int nextFreq = 0;

static unsigned int PWM_TABLE[7][2] =
{
    {132, 66}, {131, 66}, {130, 65}, {129, 65}, {128, 64}, {127, 64}, {126, 63} // Compare, duty
};

int main(void)
{
    int i, ipl;

    PLLFBD = 0x009E;                // Set processor clock to 32 MHz (16 MIPS)
    CLKDIV = 0x0048; 

    LATCbits.LATC1 = 0;             // Make RC1 an output for a debug pin
    TRISCbits.TRISC1 = 0;

    OC7CONbits.OCM = 0b000;         // Turn PWM mode off    
    OC7RS = PWM_TABLE[curFreq][1];  // Set PWM duty cycle
    PR2 = PWM_TABLE[curFreq][0];    // Set PWM period
    OC7CONbits.OCM = 0b110;         // Turn PWM mode on

    T2CONbits.TON = 0;              // Disable Timer 2
    TMR2 = 0;                       // Clear Timer 2 register    
    IPC1bits.T2IP = 1;              // Set the Timer 2 interrupt priority level
    IFS0bits.T2IF = 0;              // Clear the Timer 2 interrupt flag
    IEC0bits.T2IE = 1;              // Enable the Timer 2 interrupt
    T2CONbits.TON = 1;              // Enable Timer 2

    while (1)
    {                
        for (i = 0; i < 1600; i++) {}      // Delay roughly 1 ms
        SET_AND_SAVE_CPU_IPL(ipl, 2);      // Lock out the Timer 2 interrupt
        curFreq = (curFreq + 1) % 7;       // Bump to next frequency
        nextFreq = 1;                      // Signal frequency change to ISR
        RESTORE_CPU_IPL(ipl);              // Allow the Timer 2 interrupt        
    }
}

void __attribute__((__interrupt__)) _T2Interrupt(void)
{   
    IFS0bits.T2IF = 0;                  // Clear the Timer 2 interrupt flag     

    if (nextFreq)
    {        
        nextFreq = 0;                   // Clear the frequency hop flag
        OC7RS = PWM_TABLE[curFreq][1];  // Set the new PWM duty cycle
        PR2 = PWM_TABLE[curFreq][0];    // Set the new PWM period         
    }
}

I confirmed with the scope and a debug pin my suspicion: the original code was suffering from a race condition. The main loop did not bother to synchronize changes to PR2 with the actual state of the TMR2 counter, and so would occasionally set PR2 to a value LESS THAN (or maybe equal to) the current TMR2 value. This, in turn, would cause TMR2 to count up until it rolled over, then continue counting until it reached PR2 and generated a rising edge. During the time TMR2 was counting up to 65535 to roll over, no PWM output was being generated. At 16 MIPS, the rollover time for a 16-bit timer like TMR2 is roughly 4 ms, explaining my 4 ms PWM dropout. So, the code was doing exactly what I wrote it to do :)

In the second snippet, the code is correctly synchronizing changes to PR2 and the duty cycle register with the TMR2 rollover event, and so the 4 ms dropout had gone away. I mentioned a "weird" waveform associated with that example: it was due to the RD6/OC7 pin being configured as an output and having a low value set in the LATD register. The second snippet actually turns PWM mode off inside the Timer 2 ISR: this lets the GPIO functionality take over and pulls RD6/OC7 down for a few microseconds before reenabling PWM and generating a rising edge, leading to a "hiccup" waveform.

The second snippet also has a problem in that it reconfigures PR2 and the duty cycle register on every Timer 2 rollover, regardless of whether the main loop has commanded a frequency change or not. It seems to me from observation that the timer rolls over and generates a rising edge on the PWM pin and THEN the Timer 2 ISR gets control a few nanoseconds later (owing I'm sure to vector latency, etcetera). Turning PWM off and rejiggering the registers every time through doesn't get you quite the right frequency and duty cycle in the long run because the hardware has already generated a rising edge and started counting up to the next compare value.

What this means is that in the corrected snippet I posted today, the work done in the Timer 2 ISR needs to be minimized! Because I'm running PWM at such a high frequency, and because there is a small latency between the rising edge generated by the PWM hardware and the invocation of the Timer 2 ISR, by the time I get into the ISR TMR2 has already had time to count up to a fair number. My code needs to set PR2 and the duty cycle register immediately and directly (i.e. no function calls, and even the table lookup is pushing it), otherwise it runs the risk of missing the compare and causing the 4 ms rollover bug that was my original problem.

Anyway, I think this is an accurate description of things, and I'm running the code in my "real" application with encouraging results so far. If anything else changes I'll post here, and of course any corrections to the above would be massively appreciated.

Thanks for your help, pingswept.

\$\endgroup\$
  • \$\begingroup\$ Sounds like a plausible analysis to me. Well done, sir! \$\endgroup\$ – pingswept Jul 29 '10 at 3:37
  • \$\begingroup\$ Just a few more quick notes on this: first, it's not necessary keep and test the flag that indicates that the target frequency has changed within the ISR; PR2 and the duty cycle register can be unconditionally modified. This could save a few cycles in some circumstances. Second, you get more accurate initial frequency and duty generation if the line that turns PWM on is placed as close as possible to the line that enables timer 2. The further apart they are, the more time elapses with the PWM pin held high before the timer starts counting towards the compare values. \$\endgroup\$ – indelible Jul 30 '10 at 3:19
3
\$\begingroup\$

I would try slowing everything down by a factor of 10 so you can see in more detail when exactly the PWM is dying. I'd also try tweaking the values in your table of period and duty cycle. Maybe you're misconfiguring the PWM on 4 of your 7 cycles? I notice that there are 4 PWM period values that are above 128-- maybe that's causing problems?

If that didn't help, I would look for a bigger pattern. How often does the 4 ms gap repeat?

Very well-asked question, by the way.

\$\endgroup\$
  • \$\begingroup\$ Thank you for the reply! Very helpful ideas. I'll mess with it tomorrow and report back if I manage to get things solved. The period and duty cycle registers on the dsPIC33 are 16-bit registers, so I'm not sure the 128 thing is an issue, but it's a great catch nonetheless. I'll double-check that the values in the table are sane. My suspicion is that the code in the first snippet was periodically setting the period register to a value BELOW the TMR2 register, and thus it didn't generate a rising edge again until TMR2 overflowed. ~65000 cycles at 16 MHz is roughly 4 ms. We'll see :) \$\endgroup\$ – indelible Jul 26 '10 at 0:26
  • \$\begingroup\$ It would be interesting to check whether the rising edge at the end of the "4 ms" exactly corresponds with the rising edge on the other signal. From the scope shot above, it looks like they're about 100 us apart, which is a lot of clock cycles. \$\endgroup\$ – pingswept Jul 26 '10 at 3:04
  • \$\begingroup\$ By the way, just out of curiosity, why does it say "Microchip Tecnology Inc." [sic] on your screenshot? \$\endgroup\$ – pingswept Jul 26 '10 at 3:05
  • \$\begingroup\$ You have a keen eye! I agree, the delay between the rising debug pin edge and the restart of PWM is interesting. I wonder if it has to do with my theory that the while loop can jigger PR2 at any point in TMR2's countup and thus the next rising edge can appear at a largely random interval. I will definitely focus on it tomorrow. As for the Microchip watermark, I cross-posted my question to Microchip's forums (which so far have been less than helpful) and recycled the image here. They watermarked it and, apparently, misspelled their own name :) BTW I really appreciate your collaboration. \$\endgroup\$ – indelible Jul 26 '10 at 3:54
1
\$\begingroup\$

I'm not sure about the PWM in a dsPIC but on a PIC16F1508, the documentation says that when the TMR2 reaches a reload cycle, it reloads the TMR2 register and then loads the PWMxDC registers into the actual duty cycle registers in the PWM hardware. i.e. The writing of the PWM duty cycle is automatically synchronized to the TMR2 reload. So when you write the duty cycle, it doesn't have an effect until the next duty cycle. This means that your change in duty cycle could be delayed by as much as one TMR2 reload cycle, but that is the only change you should see. Look in your documentation.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy