Wie moduliere ich die PWM-Frequenz in Echtzeit mit einem Microchip dsPIC?

Ich versuche, die PWM-Ausgangsfrequenz mit einem dsPIC33FJ256GP710 ungefähr einmal pro Millisekunde zu ändern, und ich habe gemischte Ergebnisse. Ich habe das zuerst probiert:

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

Das Ergebnis ist, dass PWM für etwa 4 ms in einem scheinbar wiederholbaren Intervall ausfällt, das grob auf meinen Debug-Pin-Umschalter ausgerichtet ist (mit anderen Worten, wenn der Code mit den Perioden- und Arbeitszyklusregistern herumspielt). Ich füge ein Foto meiner Zielfernrohrspur bei. Kanal 1 ist PWM und Kanal 2 ist der Debug-Pin, der umgeschaltet wird, wenn der Code versucht, die Frequenz anzupassen.

Wie auch immer, ich fing an, über Timer-Rollovers nachzudenken, und ich habe in einigen Foren gesucht. Ich bin auf ein paar Ideen gekommen, basierend auf ein paar Beiträgen, die ich gelesen habe. Die beste Idee schien zu sein, den Timer 2-Interrupt zu aktivieren, den PWM-Modus darin auszuschalten und nur die Perioden- und Arbeitszyklusregister innerhalb des Timer 2-Interrupts zu ändern. Also, ich habe das geschrieben:

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

Dies scheint stabiler zu sein, soweit ich das auf meinem alten Oszilloskop beurteilen kann, aber jetzt ist die Wellenform nicht mehr regelmäßig geformt (der Arbeitszyklus scheint unerklärlicherweise inkonsistent zu sein), und wenn ich mich nur genug anstrenge, kann ich mich davon überzeugen, dass ich es immer noch sehen Sie eine Millisekunde PWM-Ausfall, wenn mein Oszilloskop auf eine Zeitbasis von 5 oder 10 Millisekunden eingestellt ist.

Es ist sicherlich besser als es war, und ich kann weiter damit herumspielen, in der Hoffnung, die unregelmäßige Wellenform zu beheben, die durch das zweite Codebit erzeugt wird, aber meine Frage ist:

Gibt es einen "richtigen" Weg, dies zu tun? Oder zumindest einen besseren Weg als den Weg, auf dem ich mich befinde?

Jede Hilfe wäre gründlich, gründlich geschätzt.

Scope-Trace http://www.freeimagehosting.net/uploads/c132216a28.jpg

Antworten (3)

Für alle, die es interessiert, hier ist die Lösung, zu der ich heute gekommen bin:

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

Mit dem Scope und einem Debug-Pin bestätigte ich meine Vermutung: Der ursprüngliche Code litt unter einer Race Condition. Die Hauptschleife hat sich nicht die Mühe gemacht, Änderungen an PR2 mit dem tatsächlichen Zustand des TMR2-Zählers zu synchronisieren, und würde daher gelegentlich PR2 auf einen Wert setzen, der KLEINER (oder vielleicht gleich) dem aktuellen TMR2-Wert ist. Dies wiederum würde dazu führen, dass TMR2 aufwärts zählt, bis es überrollt, und dann weiterzählt, bis es PR2 erreicht und eine steigende Flanke erzeugt. Während der Zeit, in der TMR2 bis 65535 hochzählte, um zu überrollen, wurde kein PWM-Ausgang erzeugt. Bei 16 MIPS beträgt die Rollover-Zeit für einen 16-Bit-Timer wie TMR2 ungefähr 4 ms, was meinen PWM-Ausfall von 4 ms erklärt. Der Code tat also genau das, wofür ich ihn geschrieben hatte :)

Im zweiten Ausschnitt synchronisiert der Code Änderungen an PR2 und dem Arbeitszyklusregister korrekt mit dem TMR2-Rollover-Ereignis, sodass der 4-ms-Ausfall verschwunden war. Ich erwähnte eine "seltsame" Wellenform, die mit diesem Beispiel verbunden ist: Es lag daran, dass der RD6/OC7-Pin als Ausgang konfiguriert war und einen niedrigen Wert im LATD-Register hatte. Das zweite Snippet schaltet den PWM-Modus innerhalb des Timer 2 ISR tatsächlich aus: Dadurch übernimmt die GPIO-Funktionalität und zieht RD6/OC7 für einige Mikrosekunden nach unten, bevor PWM wieder aktiviert und eine steigende Flanke erzeugt wird, was zu einer „Schluckauf“-Wellenform führt.

Das zweite Snippet hat auch ein Problem, da es PR2 und das Arbeitszyklusregister bei jedem Rollover von Timer 2 neu konfiguriert, unabhängig davon, ob die Hauptschleife eine Frequenzänderung befohlen hat oder nicht. Aus der Beobachtung scheint mir, dass der Timer überläuft und eine steigende Flanke am PWM-Pin erzeugt und DANN der Timer 2 ISR einige Nanosekunden später die Kontrolle erhält (aufgrund von Vektorlatenz usw. bin ich mir sicher). Wenn Sie PWM ausschalten und die Register jedes Mal neu justieren, erhalten Sie auf lange Sicht nicht die richtige Frequenz und das richtige Tastverhältnis, da die Hardware bereits eine steigende Flanke erzeugt und damit begonnen hat, bis zum nächsten Vergleichswert zu zählen.

Das bedeutet, dass in dem korrigierten Snippet, den ich heute gepostet habe, die in der Timer 2 ISR geleistete Arbeit minimiert werden muss! Da ich PWM mit einer so hohen Frequenz betreibe und zwischen der von der PWM-Hardware erzeugten steigenden Flanke und dem Aufruf des Timer 2 ISR eine kleine Latenz besteht, hatte TMR2 bereits Zeit, als ich in den ISR einstieg bis zu einer fairen Zahl zählen. Mein Code muss PR2 und das Duty-Cycle-Register sofort und direkt setzen (dh keine Funktionsaufrufe, und sogar die Tabellensuche drückt es), sonst läuft er Gefahr, den Vergleich zu verpassen und den 4-ms-Rollover-Fehler zu verursachen, der mein Original war Problem.

Jedenfalls denke ich, dass dies eine genaue Beschreibung der Dinge ist, und ich führe den Code in meiner "echten" Anwendung mit bisher ermutigenden Ergebnissen aus. Wenn sich sonst noch etwas ändert, werde ich es hier posten, und natürlich wären Korrekturen an den oben genannten Punkten sehr willkommen.

Danke für deine Hilfe, pingswept.

Klingt für mich nach einer plausiblen Analyse. Gut gemacht, Herr!
Nur noch ein paar kurze Anmerkungen dazu: Erstens ist es nicht notwendig, das Flag zu behalten und zu testen, das anzeigt, dass sich die Zielfrequenz innerhalb der ISR geändert hat; PR2 und das Duty-Cycle-Register können bedingungslos modifiziert werden. Dies könnte unter Umständen einige Zyklen einsparen. Zweitens erhalten Sie eine genauere Anfangsfrequenz und Tastverhältniserzeugung, wenn die Leitung, die PWM einschaltet, so nah wie möglich an der Leitung platziert wird, die Timer 2 aktiviert. Je weiter sie voneinander entfernt sind, desto mehr Zeit vergeht mit dem hoch gehaltenen PWM-Pin vor dem Der Timer beginnt mit dem Zählen der Vergleichswerte.

Ich würde versuchen, alles um den Faktor 10 zu verlangsamen, damit Sie genauer sehen können, wann genau die PWM stirbt. Ich würde auch versuchen, die Werte in Ihrer Tabelle für Zeitraum und Arbeitszyklus zu optimieren. Vielleicht konfigurieren Sie die PWM in 4 Ihrer 7 Zyklen falsch? Ich bemerke, dass es 4 PWM-Periodenwerte gibt, die über 128 liegen - vielleicht verursacht das Probleme?

Wenn das nicht hilft, würde ich nach einem größeren Muster suchen. Wie oft wiederholt sich die Lücke von 4 ms?

Sehr gut gestellte Frage übrigens.

Danke für Ihre Antwort! Sehr hilfreiche Ideen. Ich werde mich morgen damit beschäftigen und mich wieder melden, wenn ich es schaffe, die Dinge zu lösen. Die Perioden- und Arbeitszyklusregister auf dem dsPIC33 sind 16-Bit-Register, daher bin ich mir nicht sicher, ob das 128-Ding ein Problem ist, aber es ist trotzdem ein toller Fang. Ich werde überprüfen, ob die Werte in der Tabelle vernünftig sind. Mein Verdacht ist, dass der Code im ersten Ausschnitt das Periodenregister regelmäßig auf einen Wert UNTER dem TMR2-Register gesetzt hat und daher keine steigende Flanke erneut erzeugt hat, bis TMR2 übergelaufen ist. ~65000 Zyklen bei 16 MHz sind ungefähr 4 ms. Wir werden sehen :)
Interessant wäre zu prüfen, ob die steigende Flanke am Ende der „4 ms“ genau mit der steigenden Flanke am anderen Signal übereinstimmt. Aus dem oben aufgenommenen Oszilloskop sieht es so aus, als ob sie etwa 100 us voneinander entfernt sind, was viele Taktzyklen sind.
Übrigens, nur aus Neugier, warum steht "Microchip Tecnology Inc." [sic] auf deinem Screenshot?
Du hast ein scharfes Auge! Ich stimme zu, die Verzögerung zwischen der steigenden Debug-Pin-Flanke und dem Neustart von PWM ist interessant. Ich frage mich, ob es mit meiner Theorie zu tun hat, dass die While-Schleife PR2 an jedem Punkt im Countup von TMR2 erschüttern kann und daher die nächste steigende Flanke in einem weitgehend zufälligen Intervall erscheinen kann. Ich werde mich morgen auf jeden Fall darauf konzentrieren. Was das Microchip-Wasserzeichen betrifft, so habe ich meine Frage in den Foren von Microchip gepostet (die bisher weniger als hilfreich waren) und das Bild hier recycelt. Sie haben es mit einem Wasserzeichen versehen und anscheinend ihren eigenen Namen falsch geschrieben :) Übrigens, ich schätze Ihre Zusammenarbeit wirklich.

Ich bin mir bei der PWM in einem dsPIC nicht sicher, aber bei einem PIC16F1508 sagt die Dokumentation, dass der TMR2, wenn er einen Neuladezyklus erreicht, das TMR2-Register neu lädt und dann die PWMxDC-Register in die tatsächlichen Tastverhältnisregister in der PWM-Hardware lädt. dh das Schreiben des PWM-Tastverhältnisses wird automatisch mit dem TMR2-Reload synchronisiert. Wenn Sie also das Tastverhältnis schreiben, wirkt es sich erst beim nächsten Tastverhältnis aus. Dies bedeutet, dass Ihre Änderung des Arbeitszyklus um bis zu einen TMR2-Nachladezyklus verzögert werden könnte, aber das ist die einzige Änderung, die Sie sehen sollten. Schauen Sie in Ihre Dokumentation.