¿Cómo puedo modular la frecuencia PWM en tiempo real con un Microchip dsPIC?

6

Estoy intentando cambiar la frecuencia de salida de PWM aproximadamente una vez por milisegundo utilizando un dsPIC33FJ256GP710, y tengo resultados mixtos. Primero intenté esto:

 #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         
     } 
 } 

El resultado es que PWM se retira durante aproximadamente 4 ms en lo que parece ser un intervalo repetible, aproximadamente alineado con mi conmutador de pin de depuración (en otras palabras, cuando el código está jugando con el período y los registros del ciclo de trabajo). Adjuntaré una foto de mi traza de alcance. El canal 1 es PWM y el canal 2 es el pin de depuración que se alterna cuando el código intenta ajustar la frecuencia.

De todos modos, comencé a pensar en los reinversores del temporizador e hice algunas búsquedas en algunos foros. Se me ocurrieron algunas ideas basadas en algunas publicaciones que leí. La mejor idea parecía ser habilitar la interrupción del temporizador 2, desactivar el modo PWM en su interior y solo cambiar los registros de período y ciclo de trabajo dentro de la interrupción del temporizador 2. Entonces, escribí esto:

 #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 
 }

Esto parece ser más estable por lo que puedo decir sobre mi alcance antiguo, pero ahora la forma de onda ya no tiene forma regular (el ciclo de trabajo parece ser inexplicablemente inconsistente) y si me esfuerzo lo suficiente, puedo convencerme que todavía veo un milisegundo de abandono de PWM cuando mi alcance se establece en una base de tiempo de 5 o 10 milisegundos.

Es ciertamente mejor de lo que era, y puedo seguir jugando con la esperanza de corregir la forma de onda irregular producida por el segundo bit de código, pero mi pregunta es:

¿Hay una forma "correcta" de hacer esto? ¿O al menos una manera mejor que el camino en el que estoy?

Cualquier ayuda sería completamente apreciada.

Seguimiento del alcance http://www.freeimagehosting.net/uploads/c132216a28.jpg

    
pregunta Trygve Laugstøl

3 respuestas

10

Para cualquiera que esté interesado, aquí está la solución a la que llegué hoy:

#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         
    }
}

Confirmé con el alcance y un error de depuración mi sospecha: el código original sufría de una condición de carrera. El bucle principal no se molestó en sincronizar los cambios en PR2 con el estado real del contador TMR2, por lo que ocasionalmente establecería PR2 en un valor MENOS QUE (o quizás igual a) el valor actual de TMR2. Esto, a su vez, haría que TMR2 contara hasta que se volcó, luego continuaría contando hasta que alcanzó PR2 y generó un flanco ascendente. Durante el tiempo que TMR2 contó hasta 65535 para reinvertir, no se generó ninguna salida de PWM. En 16 MIPS, el tiempo de reinversión para un temporizador de 16 bits como TMR2 es de aproximadamente 4 ms, lo que explica mi abandono de PWM de 4 ms. Entonces, el código estaba haciendo exactamente lo que yo escribí para hacer :)

En el segundo fragmento, el código sincroniza correctamente los cambios en PR2 y el registro de ciclo de trabajo con el evento de reinversión de TMR2, por lo que el abandono de 4 ms había desaparecido. Mencioné una forma de onda "extraña" asociada con ese ejemplo: se debió a que el pin RD6 / OC7 se configuró como una salida y tenía un valor bajo establecido en el registro LATD. El segundo fragmento realmente desactiva el modo PWM dentro del Timer 2 ISR: esto permite que la funcionalidad GPIO tome el control de RD6 / OC7 durante unos pocos microsegundos antes de volver a habilitar PWM y generar un flanco ascendente, lo que lleva a una forma de onda "hipo".

El segundo fragmento de código también tiene un problema porque reconfigura PR2 y el registro del ciclo de trabajo en cada reinversión del Temporizador 2, independientemente de si el bucle principal ha ordenado un cambio de frecuencia o no. Desde la observación, me parece que el temporizador gira y genera un flanco ascendente en el pin PWM y ENTONCES el ISR del temporizador 2 toma el control unos nanosegundos más tarde (debido a que estoy seguro de la latencia vectorial, etc.). La desactivación de la PWM y el reajuste de los registros cada vez que se producen no le proporciona la frecuencia y el ciclo de trabajo correctos a largo plazo porque el hardware ya ha generado un flanco ascendente y comenzó a contar hasta el siguiente valor de comparación.

Lo que esto significa es que en el fragmento corregido que publiqué hoy, el trabajo realizado en el ISR del temporizador 2 debe minimizarse. Porque ejecuto PWM a una frecuencia tan alta y porque hay una pequeña latencia entre el flanco ascendente generado por el hardware de PWM y la invocación del ISR del temporizador 2, en el momento en que entro al ISR TMR2 ya ha tenido tiempo para contar hasta un número justo. Mi código debe configurar PR2 y el registro del ciclo de trabajo de forma inmediata y directa (es decir, no hay llamadas de función, e incluso la búsqueda en la tabla lo está empujando), de lo contrario, corre el riesgo de perder la comparación y causar el error de transferencia de 4 ms que era mi original problema.

De todos modos, creo esta es una descripción precisa de las cosas, y estoy ejecutando el código en mi aplicación "real" con resultados alentadores hasta el momento. Si hay algo más que cambiaré, publicaré aquí, y por supuesto, cualquier corrección a lo anterior se agradecerá enormemente.

Gracias por tu ayuda, pingswept.

    
respondido por el indelible
3

Intentaría ralentizar todo en un factor de 10 para que pueda ver con más detalle cuándo está muriendo exactamente el PWM. También intentaría ajustar los valores en su tabla de período y ciclo de trabajo. ¿Quizás esté configurando incorrectamente el PWM en 4 de sus 7 ciclos? Me doy cuenta de que hay 4 valores de período PWM que están por encima de 128 ... ¿quizás eso esté causando problemas?

Si eso no ayudara, buscaría un patrón más grande. ¿Con qué frecuencia se repite la brecha de 4 ms?

Una pregunta muy bien formulada, por cierto.

    
respondido por el pingswept
1

No estoy seguro acerca del PWM en un dsPIC pero en un PIC16F1508, la documentación dice que cuando el TMR2 alcanza un ciclo de recarga, vuelve a cargar el registro TMR2 y luego carga los registros PWMxDC en los registros del ciclo de trabajo real en el Hardware PWM. es decir, la escritura del ciclo de trabajo PWM se sincroniza automáticamente con la recarga TMR2. Entonces, cuando escribe el ciclo de trabajo, no tendrá efecto hasta el siguiente ciclo de trabajo. Esto significa que su cambio en el ciclo de trabajo podría retrasarse tanto como un ciclo de recarga de TMR2, pero ese es el único cambio que debería ver. Busque en su documentación.

    
respondido por el Donald Murray

Lea otras preguntas en las etiquetas