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

Estoy tratando de cambiar la frecuencia de salida de PWM aproximadamente una vez por milisegundo usando un dsPIC33FJ256GP710 y tengo resultados mixtos. Primero probé 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 cae durante aproximadamente 4 ms en lo que parece ser un intervalo repetible, aproximadamente alineado con mi pin de depuración (en otras palabras, cuando el código interfiere con los registros de período y ciclo de trabajo). Adjuntaré una foto de mi rastro de alcance. El canal 1 es PWM y el canal 2 es el pin de depuración que se activa cuando el código intenta ajustar la frecuencia.

De todos modos, comencé a pensar en los cambios de tiempo 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 ver en mi alcance antiguo, pero ahora la forma de onda ya no tiene una forma regular (el ciclo de trabajo parece ser inexplicablemente inconsistente) y si me esfuerzo lo suficiente puedo convencerme de que todavía ver un milisegundo de caída de PWM cuando mi alcance está configurado en una base de tiempo de 5 o 10 milisegundos.

Ciertamente es mejor de lo que era, y puedo seguir jugando con él con la esperanza de arreglar 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 mejor manera que el camino en el que estoy?

Cualquier ayuda sería muy, muy apreciada.

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

Respuestas (3)

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 pin de depuración mi sospecha: el código original sufría 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 MENOR QUE (o tal vez igual) al valor TMR2 actual. Esto, a su vez, haría que TMR2 contara hasta que volviera, luego continuaría contando hasta que llegara a PR2 y generara un flanco ascendente. Durante el tiempo que TMR2 estuvo contando hasta 65535 para reinvertir, no se generó ninguna salida PWM. A 16 MIPS, el tiempo de reinicio para un temporizador de 16 bits como TMR2 es de aproximadamente 4 ms, lo que explica mi pérdida de PWM de 4 ms. Entonces, el código estaba haciendo exactamente lo que escribí para hacer :)

En el segundo fragmento, el código sincroniza correctamente los cambios en PR2 y el registro del ciclo de trabajo con el evento de transferencia de TMR2, por lo que la caída de 4 ms desapareció. Mencioné una forma de onda "extraña" asociada con ese ejemplo: se debió a que el pin RD6/OC7 estaba configurado como salida y tenía un valor bajo establecido en el registro LATD. El segundo fragmento en realidad desactiva el modo PWM dentro del ISR del temporizador 2: esto permite que la funcionalidad GPIO se haga cargo y reduce RD6/OC7 durante unos microsegundos antes de volver a habilitar PWM y generar un flanco ascendente, lo que lleva a una forma de onda de "hipo".

El segundo fragmento también tiene el problema de que reconfigura PR2 y el registro del ciclo de trabajo en cada reinicio del temporizador 2, independientemente de si el bucle principal ordenó un cambio de frecuencia o no. A partir de la observación, me parece que el temporizador da la vuelta y genera un flanco ascendente en el pin PWM y ENTONCES el ISR del temporizador 2 obtiene el control unos nanosegundos más tarde (debido a la latencia del vector, etcétera). Desactivar PWM y reajustar los registros cada vez no le da 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 Timer 2 ISR debe minimizarse! Debido a que estoy ejecutando PWM a una frecuencia tan alta, y debido a que hay una pequeña latencia entre el flanco ascendente generado por el hardware PWM y la invocación del Timer 2 ISR, cuando entro en el ISR TMR2 ya ha tenido tiempo contar hasta un número justo. Mi código necesita 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á presionando), de lo contrario, corre el riesgo de perder la comparación y causar el error de reinversión de 4 ms que era mi original problema.

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

Gracias por tu ayuda, pingswept.

Me parece un análisis plausible. ¡Bien hecho, señor!
Solo algunas notas rápidas más sobre esto: primero, no es necesario mantener y probar la bandera que indica que la frecuencia objetivo ha cambiado dentro del ISR; PR2 y el registro del ciclo de trabajo se pueden modificar incondicionalmente. Esto podría ahorrar algunos ciclos en algunas circunstancias. En segundo lugar, obtendrá una frecuencia inicial y una generación de trabajo más precisas si la línea que activa PWM se coloca lo más cerca posible de la línea que habilita el temporizador 2. Cuanto más separados estén, más tiempo transcurrirá con el pin PWM en alto antes de que se active el temporizador. el temporizador comienza a contar hacia los valores de comparación.

Intentaría ralentizar todo por un factor de 10 para que pueda ver con más detalle cuándo se 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ás configurando mal el PWM en 4 de tus 7 ciclos? Noté que hay 4 valores de período PWM que están por encima de 128, ¿tal vez eso esté causando problemas?

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

Muy bien hecha la pregunta, por cierto.

¡Gracias por la respuesta! Ideas muy útiles. Lo arreglaré mañana e informaré si logro resolver las cosas. Los registros de período y ciclo de trabajo en el dsPIC33 son registros de 16 bits, por lo que no estoy seguro de que la cosa 128 sea un problema, pero de todos modos es una gran captura. Verificaré dos veces que los valores en la tabla sean cuerdos. Mi sospecha es que el código en el primer fragmento establecía periódicamente el registro de período en un valor POR DEBAJO del registro TMR2 y, por lo tanto, no generó un borde ascendente nuevamente hasta que TMR2 se desbordó. ~65000 ciclos a 16 MHz es aproximadamente 4 ms. Ya veremos :)
Sería interesante comprobar si el flanco ascendente al final de los "4 ms" se corresponde exactamente con el flanco ascendente de la otra señal. De la toma de alcance de arriba, parece que están a unos 100 nosotros de distancia, que son muchos ciclos de reloj.
Por cierto, solo por curiosidad, ¿por qué dice "Microchip Tecnology Inc." [sic] en tu captura de pantalla?
¡Tienes buen ojo! Estoy de acuerdo, la demora entre el borde ascendente del pin de depuración y el reinicio de PWM es interesante. Me pregunto si tiene que ver con mi teoría de que el ciclo while puede alterar PR2 en cualquier punto de la cuenta ascendente de TMR2 y, por lo tanto, el siguiente flanco ascendente puede aparecer en un intervalo mayormente aleatorio. Definitivamente me enfocaré en eso mañana. En cuanto a la marca de agua de Microchip, publiqué mi pregunta en los foros de Microchip (que hasta ahora han sido menos que útiles) y reciclé la imagen aquí. Le pusieron una marca de agua y, aparentemente, escribieron mal su propio nombre :) Por cierto, realmente aprecio tu colaboración.

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 de ciclo de trabajo reales en el hardware PWM. es decir, la escritura del ciclo de trabajo PWM se sincroniza automáticamente con la recarga del TMR2. Entonces, cuando escribe el ciclo de trabajo, no tiene 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. Mira en tu documentación.