Pulsweitenmodulation "Fast PWM"

NEU: Die Seite wurde durch einen "Nachtrag zu Pulsweite setzen" ergänzt!!

Standardmäßig bietet der Arduino Uno mit dem Befehl "analogWrite(...)" die Möglichkeit, PWM-Signale mit einer Auflösung von 8 Bit und einer PWM-Frequenz von 490 Hz (Pin D3, D9, D10 und D11) bzw. 980 Hz (Pin D5 und D6) auszugeben. Wer für seine Anwendung eine höhere Auflösung und/oder eine höhere oder tiefere PWM-Frequenz haben möchte, muss zwangsläufig selbst diverse Bits in den entsprechenden Timer-/Counter-und Port-Registern setzen.

Die PWM-Pins des Arduino Uno werden von, wie nachfolgend dargestellt, von den Timern 0, 1 und 2 kontrolliert:

  • Pins D5 und D6:   Timer0 (8 Bit)
  • Pins D9 und D10: Timer1 (16 Bit)
  • Pins D3 und D11: Timer2 (8 Bit)


Unter Verwendung des 16 Bit Timer1 möchte ich nachfolgend zeigen, welche Schritte erforderlich sind, um ein PWM-Signal mit anderer Frequenz und/oder Auflösung als standardmäßig vorgegeben, programmieren zu können.

Mit Timer1 können nur die Pins D9 und D10 als PWM-Pins verwendet werden!


  • Verwendete Register:

    • Timer Counter Control Register A: TCCR1A
    • Timer Counter Control Register B: TCCR1B
    • Output Compare Register A: OCR1A (für PWM-Pin D9)
    • Output Compare Register B: OCR1B (für PWM-Pin D10)
    • Port B Data Direction Register: DDRB


    Vorgangsweise:

    1. Modus wählen
    2. PWM-Frequenz festlegen
    3. Ausgangs-Aktion festlegen
    4. Pin als Ausgang definieren
    5. Setzen des Wertes der Pulsweite


    1. Modus wählen:

    Timer 1 erlaubt drei Moden: 8 Bit-, 9 Bit- und 10 Bit-PWM. Die Auswahl erfolgt über die Bits "Waveform Generation Mode" WGM10, WGM11 und WGM12 der "Timer/Counter Controll Register" A und B (TCCR1A und TCCR1B).

    Die Bits WGM10 und 11 befinden sich im Timer/Counter Control Register A (TCCR1A), das Bit WGM12 im Timer/Counter Control Register B (TCCR1B).

    TCCR1A - Timer/Counter1 Control Register A:

    TCCR1B - Timer/Counter1 Control Register B:

  • In der nachfolgenden Tabelle sind die Einstellungen der Bits für den gewünschten Modus ersichtlich:

    2. PWM-Frequenz festlegen

    Die PWM-Frequenz ist abhängig von der Taktfrequenz des Prozessors, der Auflösung und der Einstellung des Vorteilers (Prescaler).

    Vorgaben:

    • PWM-Auflösung 8, 9 oder 10 Bit
    • CPU-Frequenz Arduino Uno: 16.000.000 Hz
    • Mögliche Vorteiler: 1, 8, 64, 256 oder 1024


    In Abhängigkeit der Auflösung wird die PWM-Frequenz [Hz] nach folgender Formel ermittelt:


    Berechnungsbeispiel:

    Z.B. Auflösung = 10 Bit, Vorteiler = 256:

    PWM Frequenz = 16.000.000 / (256 * 1024) = 61 Hz

    oder z.B. Auflösung = 9 Bit, Vorteiler = 1:

    PWM Frequenz = 16.000.000 / (1 * 512) = 31.250 Hz

    Nachfolgende Tabelle zeigt alle möglichen PWM-Frequenzen in Abhängigkeit der Auflösung und des Vorteilers bei einer Taktfrequenz des Uno von 16 MHz. Wie man sieht, liegt die höchste PWM-Frequenzen bei 62,5 kHz, die tiefste Frequenz bei 15 Hz:


    Der gewünschte Vorteiler muss im Timer/Counter Control Register B (TCCR1B) mit den Bits "Clock Select" CS10, 11 und 12 gesetzt werden:

    Die Bitkombination für den gewünschten Vorteiler ist in nachfolgender Tabelle ersichtlich:

    3. Ausgangsaktion festlegen

    Hier legt man mit den "Compare Output Mode" - Bits des Timer/Counter1 Control Register A (TCCR1A) fest, ob das PWM-Signal nicht invertiert oder invertiert ausgegeben wird, oder ob das Signal abgeschaltet wird:

    • Für PWM-Pin D9  : Bit COM1A0 und COM1A1
    • Für PWM-Pin D10: Bit COM1B0 und COM1B1


    Die Einstellung der Bits für ein invertiertes oder nicht invertiertes PWM Signal ist in nachfolgender Tabelle ersichtlich:

    4. PWM-Pin als Ausgang definieren:

    Die PWM-Pins D9 und D10 sind Pins des Port B und können durch direktes Setzen der entsprechenden Bits DDB1 oder DDB2 im Data Direction Register Port B (DDRB) als Binärausgang definiert werden.

    5. Pulsweite setzen:

    Die gewünschte Pulsweite kann nun, je nach verwendetem PWM-Pin, in das 16-Bit Output Compare Register A (OCR1A) für PWM-Pin D9 oder Output Compare Register B (OCR1B) für PWM-Pin D10 geschrieben werden. In Abhängigkeit der eingestellten Auflösung von 8, 9 oder 10 Bit kann der Wertebereich der Pulsweite 0 bis 255, 0 bis 511 oder 0 bis 1023 betragen. Bei einer Auflösung von z.B. 9 Bit und einer Pulsweite von 255 beträgt das Impuls-Pausenverhältnis somit 50:50.


    Siehe "Nachtrag zu Pulsweite setzen" nach dem Programmbeispiel!


    Programmbeispiel:

    Im folgenden Beispielprogramm wird die Helligkeit einer LED durch ein Potentiometer mit Pulsweitenmodulation (PWM-Frequenz = ca. 2 kHz, Auflösung 10 Bit = 1024 Stufen) gesteuert.


    Verwendete Bauteile:

    • 1 Arduino Uno
    • 1 LED
    • 1 Widerstand 220 Ohm
    • 1 Poti 10 kOhm



    //PWM-Testprogramm
    //Code fuer Arduino
    //Author Retian
    //Version 1.0


    #define potiPin 2 //Eingang Poti ist A2

    int potiWert;


    void setup() {
      Serial.begin(115200);


      //Löschen der Timer/Counter Control Register A und B

      TCCR1A = 0;
      TCCR1B = 0;


      //Modus Fast PWM-Mode 10 Bit einstellen
      TCCR1A |= (1 << WGM10) | (1 << WGM11);
      TCCR1B |= (1 << WGM12);


      //Vorteiler auf 8 setzen

      TCCR1B |= (1 << CS11);


      //Nichtinvertiertes PWM-Signal setzen
      TCCR1A |= (1 << COM1A1);
     

       //PWM-Pin 9 als Ausgang definieren
      DDRB |= (1 << DDB1);
    }


    void loop() {
      potiWert = analogRead(potiPin); //Potiwert einlesen (Auflösung Analogeingang = 10 Bit)


      //Ist die Auflösung des PWM-Signals z.B 8 Bit, muss der potiWert angepasst werden:

      //potiWert = map(potiWert, 0, 1023, 0, 255);


      OCR1A = potiWert; // Setzen des Impuls-Pausenverhältnis


      Serial.println(potiWert);

      delay(15);
    }


    Oszillogramm des PWM-Signals mit einem Impuls-Pausenverhältnis von 10:90:

    Horizontal: 0,2 ms/Div. => Periodendauer = 0,5 ms => PWM-Frequenz = 2 kHz

    Vertikal    : 2 V/Div.


    Nachtrag zu Pulsweite setzen :

    Von einem Leser wurde ich aufmerksam gemacht, dass beim Ausführen meines Testprogrammes auch bei einer angegebenen Pulsweite von 0 die angesteuerte LED ganz gering leuchtet (nur bei Dunkelheit oder Dämmerlicht sichtbar). Was ich zunächst nicht für möglich hielt, konnte ich dann doch auch mit dem Oszilloskop beobachten.


    Oszillogramm des PWM-Signals bei OCR1A = 0, welches es gar nicht geben sollte:

    Horizontal: 0,2 ms/Div. => Periodendauer = 0,5 ms => PWM-Frequenz = 2 kHz

    Vertikal    : 2 V/Div.

    Auch bei OCR1A = 0 werden ganz kurze PWM-Impulse erzeugt. Die Dauer der PWM-Impulse ist kürzer als bei OCR1A = 1, ca. 50 - 75% der Dauer. Für die meisten Anwendungen werden diese Impulse keine Rolle spielen, man wird es wahrscheinlich kaum oder gar nicht bemerken. Wenn es doch störend ist, so kann ich 2 Varianten zur Behebung anbieten:


    Variante 1:

    Man ersetzt den die Zeile im Testprogramm

    OCR1A = potiWert; // Setzen des Impuls-Pausenverhältnis

    durch den PWM-Standardbefehl

    analogWrite(9, potiWert); //PWM-Ausgang ist Pin D9

    Durch das vorherige Setzen der PWM-Register werden nun auch mit dem Standardbefehl die gewünschten PWM-Parameter (PWM-Frequenz und Ausgangsaktion) durchgeführt. Und die kurzen PWM-Impulse, wenn potiWert 0 ist, sind nicht mehr vorhanden!

    Warum?

    Durch Auslesen des PWM-Register Timer/Counter1 Control Register A (TCCR1A) habe herausgefunden, dass im Falle von

    analogWrite(9, 0);

    das von mir im Testprogramm im setup() gesetzte Bit COM1A1 zur Einstellung der Ausgangsfunktion "PWM-Ausgang nicht Invertiert" vom System auf 0 gesetzt wird. Somit sind dann in meinem Beispiel beide Bits COM1A1 und COM1A0 rückgesetzt und die PWM-Funktion somit ausgeschaltet (siehe Tabelle unter Punkt 3. Ausgangsfunkfion festlegen). Wird wieder eine Zahl größer 0 übergeben, wird automatisch der alte Zustand wieder hergestellt.

    Achtung: Bei

    analogWrite(9, 255);

    wird ein Impuls-Pausenverhältnis von 100:0 (Pulseweite 100%), anstatt 25:75 (bei 10 Bit-Auflösung) ausgegeben! Warum das so ist, hängt offensichtlich mit der analogWrite-Funktion selbst zusammen.

    Abhilfe kann z.B. durch Ändern der Pulsweite auf 254 geschaffen werden, wenn die Pulsweite 255 beträgt:

    if (potiWert != 255) analogWrite(9, potiWert);

    else analogWrite(9, 254);

    In den meisten Fällen wird es wohl nicht stören, dass es keinen Pulsweitewert von 255 gibt, wenn doch, dann sollte man die nachfolgende Variante 2 verwenden.

    (Danke an Matthias H. und David S., die mich auf dieses Verhalten aufmerksam gemacht haben!!!)


    Variante 2:

    In Variante 2 verwende ich weiterhin die OCR1A-Zuweisung, nutze aber die Erkenntnis von Variante 1 und schalte die PWM-Funktion selbst in Abhängigkeit von potiWert ein und aus:

    if (potiWert == 0) TCCR1A &= ~(1 << COM1A1);

    else TCCR1A |= (1 << COM1A1);

    OCR1A = potiWert; // Setzen des Impuls-Pausenverhältnis

    Allerdings würde hier das COM1A1-Bit bei jedem Durchlauf mit 0 oder 1 beschrieben werden, obwohl das Bit in den meisten Fällen schon richtig gesetzt ist. Beim nachfolgenden "Testprogramm Variante 2" wird daher nur im Falle der Änderung von potiWert von N auf 0 (N = potiWert > 0) oder von 0 auf N das Bit COM1A1 gelöscht oder gesetzt.


    Testprogramm Variante 1:

    Hier nun das PWM-Testprogramm mit Änderung laut Variante 1:


    //PWM-Testprogramm Variante 1
    //Code fuer Arduino
    //Author Retian
    //Version 1.1


    #define potiPin 2 //Eingang Poti ist A2


    int potiWert;


    void setup() {
      Serial.begin(115200);


      //Löschen der Timer/Counter Control Register A und B

      TCCR1A = 0;
      TCCR1B = 0;


      //Modus Fast PWM-Mode 10 Bit einstellen
      TCCR1A |= (1 << WGM10) | (1 << WGM11);
      TCCR1B |= (1 << WGM12);


      //Vorteiler auf 8 setzen
      TCCR1B |= (1 << CS11);


      //Nichtinvertiertes PWM-Signal setzen
      TCCR1A |= (1 << COM1A1);


      //PWM-Pin 9 als Ausgang definieren
      DDRB |= (1 << DDB1);
    }


    void loop() {
      potiWert = analogRead(potiPin); //Potiwert einlesen (Auflösung Analogeingang = 10 Bit)


      //Ist die Auflösung des PWM-Signals z.B 8 Bit, muss der potiWert angepasst werden:
      //potiWert = map(potiWert, 0, 1023, 0, 255);

      

      //Wenn potiWert = 255 dann 254 ausgeben, ansonsten potiWert

      if (potiWert != 255) analogWrite(9, potiWert);

      else analogWrite(9, 254);


      Serial.println(potiWert);
      delay(15);
    }


    Testprogramm Variante 2:

    Hier nun das PWM-Testprogramm mit Erweiterung laut Variante 2, wobei mit Hilfe die bool'sche Variable "pwmStatus" das Bit COM1A1 nur jeweils bei einer Änderung von potiWert von N auf 0 (N = potiWert > 0) oder von 0 auf N gelöscht oder gesetzt wird.

    Hinweis: Gegenüber Variante 1 benötigt Variante 2 um fast 400 Bytes Programmspeicher weniger!


    //PWM-Testprogramm Variante 2
    //Code fuer Arduino
    //Author Retian
    //Version 1.0


    #define potiPin 2 //Eingang Poti ist A2


    int potiWert;
    bool pwmStatus = true;


    void setup() {
      Serial.begin(115200);


      //Löschen der Timer/Counter Control Register A und B

      TCCR1A = 0;
      TCCR1B = 0;


      //Modus Fast PWM-Mode 10 Bit einstellen
      TCCR1A |= (1 << WGM10) | (1 << WGM11);
      TCCR1B |= (1 << WGM12);


      //Vorteiler auf 8 setzen
      TCCR1B |= (1 << CS11);


      //Nichtinvertiertes PWM-Signal setzen
      TCCR1A |= (1 << COM1A1);


      //PWM-Pin 9 als Ausgang definieren
      DDRB |= (1 << DDB1);
    }


    void loop() {
      potiWert = analogRead(potiPin); //Potiwert einlesen (Auflösung Analogeingang = 10 Bit)


      //Ist die Auflösung des PWM-Signals z.B 8 Bit, muss der potiWert angepasst werden:
      //potiWert = map(potiWert, 0, 1023, 0, 255);

      
      if (potiWert == 0 && pwmStatus)
      {
        TCCR1A &= ~(1 << COM1A1); //COM1A1-Bit loeschen
        pwmStatus = false;
      }
      else if (potiWert > 0 && !pwmStatus)
      {
        TCCR1A |= (1 << COM1A1); //COM1A1-Bit setzen
        pwmStatus = true;
      }
     
      OCR1A = potiWert; // Setzen des Impuls-Pausenverhältnis


      Serial.println(potiWert);
      delay(15);
    }