AVR-Tutorial: PWM: Unterschied zwischen den Versionen

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche
K (Syntaxhighlight (asm), R/G, Kleinigkeiten)
 
(31 dazwischenliegende Versionen von 13 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
PWM - Dieses Kürzel steht für '''P'''uls '''W'''eiten '''M'''odulation.
'''PWM''' – Dieses Kürzel steht für <u>P</u>uls-<u>W</u>eiten-<u>M</u>odulation (oder englisch ''Pulse Width Modulation'').


==Was bedeutet PWM?==
==Was bedeutet PWM?==
Viele elektrische Verbraucher können in ihrer Leistung reguliert werden, indem die Versorgungsspannung in weiten Bereichen verändert wird. Ein normaler Gleichstrommotor wird z.B. langsamer laufen, wenn er mit einer geringeren Spannung versorgt wird, bzw. schneller laufen, wenn er mit einer höheren Spannung versorgt wird. LEDs werden zwar nicht mit einer Spannung gedimmt, sondern mit dem Versorgungsstrom. Da dieser Stromfluss aber im Normalfall mit einem Vorwiderstand eingestellt wird, ist durch das Ohmsche Gesetz dieser Stromfluss bei konstantem Widerstand wieder direkt proportional zur Höhe der Versorgungsspannung.
Viele elektrische Verbraucher können in ihrer Leistung reguliert werden, indem die Versorgungsspannung in weiten Bereichen verändert wird. Ein normaler Gleichstrommotor wird z.&nbsp;B. langsamer laufen, wenn er mit einer geringeren Spannung versorgt wird, bzw. schneller laufen, wenn er mit einer höheren Spannung versorgt wird. LEDs werden zwar nicht mit einer Spannung gedimmt, sondern mit dem Versorgungsstrom. Da dieser Stromfluss aber im Normalfall mit einem Vorwiderstand eingestellt wird, ist durch das Ohmsche Gesetz dieser Stromfluss bei konstantem Widerstand wieder (näherungsweise) direkt proportional zur Höhe der Versorgungsspannung.


Im wesentlichen geht es also immer um diese Kennlinie, trägt man die Versorgungsspannung entlang der Zeitachse auf:
Im wesentlichen geht es also immer um diese Kennlinie, trägt man die Versorgungsspannung entlang der Zeitachse auf:


<center>
[[Bild:PWM_1.gif]]
[[Bild:PWM_1.gif]]
</center>


Die Fläche unter der Kurve ist dabei ein direktes Mass für die Energie die dem System zugeführt wird. Bei geringerer Energie ist die Helligkeit geringer, bei höherer Energie entsprechend heller.
Die Fläche unter der Kurve ist dabei ein direktes Maß für die Energie, die dem System zugeführt wird. Bei geringerer Energie ist die Helligkeit geringer, bei höherer Energie entsprechend heller.


Jedoch gibt es noch einen zweiten Weg, die dem System zugeführte Energie zu verringern. Anstatt die Spannung abzusenken, ist es auch möglich die volle Versorgungsspannung über einen geringeren Zeitraum anzulegen. Man muß nur dafür Sorge tragen, dass im Endeffekt die einzelnen Pulse nicht mehr wahrnehmbar sind.
Jedoch gibt es noch einen zweiten Weg, die dem System zugeführte Energie zu verringern. Anstatt die Spannung abzusenken, ist es auch möglich, die volle Versorgungsspannung über einen geringeren Zeitraum anzulegen. Man muß nur dafür Sorge tragen, dass im Endeffekt die einzelnen Pulse nicht mehr wahrnehmbar sind.


<center>
[[Bild:PWM_Theorie_1.gif]]
[[Bild:PWM_Theorie_1.gif]]
</center>


Die Fläche unter den Rechtecken hat in diesem Fall dieselbe Größe wie die Fläche unter der Spannung V=, glättet man die Spannung also mit einem Kondensator, ergibt sich eine niedrigere konstante Spannung. Die Rechtecke sind zwar höher, aber dafür schmäler. Die Flächen sind aber dieselben. Diese Lösung hat den Vorteil, dass keine Spannung geregelt werden muss, sondern der Verbraucher immer mit derselben Spannung versorgt wird.
Die Fläche unter den Rechtecken hat in diesem Fall dieselbe Größe wie die Fläche unter der Spannung V=, glättet man die Spannung also mit einem Kondensator, ergibt sich eine niedrigere konstante Spannung. Die Rechtecke sind zwar höher, aber dafür schmaler. Die Flächen sind aber dieselben. Diese Lösung hat den Vorteil, dass keine Spannung geregelt werden muss, sondern der Verbraucher immer mit derselben Spannung versorgt wird.


Und genau das ist das Prinzip einer PWM. Durch die Abgabe von Pulsen wird die abgegebene Energiemenge gesteuert. Es ist auf einem µC wesentlich einfacher Pulse mit einem definiertem Puls/Pausen Verhältnis zu erzeugen als eine Spannung zu variieren.
Und genau das ist das Prinzip einer PWM. Durch die Abgabe von Pulsen wird die abgegebene Energiemenge gesteuert. Es ist auf einem µC wesentlich einfacher, Pulse mit einem definierten Puls/Pausen-Verhältnis zu erzeugen, als eine Spannung zu variieren.


==PWM und der Timer==
==PWM und der Timer==
Der Timer1 des Mega8 unterstützt direkt das Erzeugen von PWM. Beginnt der Timer beispielsweise bei 0 zu zählen, so schaltet er gleichzeitig einen Ausgangspin ein. Erreicht der Zähler einen bestimmten Wert X, so schaltet er den Ausgangspin wieder aus und zählt weiter bis zu seiner Obergrenze. Danach wiederholt sich das Spielchen, der Timer beginnt wieder bei 0 und schaltet gleichzeitig den Ausgangspin ein, etc., etc. Durch verändern von X kann man daher steuern, wie lange der Ausgangspin, im Verhältnis zur kompletten Zeit die der Timer benötigt um seine Obergrenze zu erreichen, eingeschaltet ist.
Der Timer&nbsp;1 des ATmega8 unterstützt direkt das Erzeugen von PWM. Beginnt der Timer beispielsweise bei 0 zu zählen, so schaltet er gleichzeitig einen Ausgangspin ein. Erreicht der Zähler einen bestimmten Wert X, so schaltet er den Ausgangspin wieder aus und zählt weiter bis zu seiner Obergrenze. Danach wiederholt sich das Spielchen. Der Timer beginnt wieder bei 0 und schaltet gleichzeitig den Ausgangspin ein usw. Durch Verändern von X kann man daher steuern, wie lange der Ausgangspin im Verhältnis zur kompletten Zeit, die der Timer benötigt, um seine Obergrenze zu erreichen, eingeschaltet ist.


Dabei gibt es aber verwirrenderweise verschiedene Arten der PWM:
Dabei gibt es aber verwirrenderweise verschiedene Arten der PWM:
* Fast PWM
* ''Fast PWM'' (= schnelle PWM)
* Phasen-korrekte PWM
* Phasenkorrekte PWM
* Phasen- und Frequenzkorrekte PWM
* Phasen- und frequenzkorrekte PWM
Für die Details zu jedem PWM-Modus sei auf das Datenblatt verwiesen.
Für die Details zu jedem PWM-Modus sei auf das Datenblatt verwiesen.


===Fast PWM===
===Fast PWM===


Die Fast PWM gibt es beim Mega8 mit mehreren unterschiedlichen Bit-Zahlen. Bei den Bit-Zahlen geht es immer darum, wie weit der Timer zählt, bevor ein Rücksetzen des Timers auf 0 erfolgt
Die ''Fast PWM'' gibt es beim ATmega8 mit mehreren unterschiedlichen Bit-Zahlen. Bei den Bit-Zahlen geht es immer darum, wie weit der Timer zählt, bevor ein Rücksetzen des Timers auf 0 erfolgt:


* Modus 5: 8 Bit Fast PWM - Der Timer zählt bis 255
* Modus 5: 8-Bit Fast PWM Der Timer zählt bis 255
* Modus 6: 9 Bit Fast PWM - Der Timer zählt bis 511
* Modus 6: 9-Bit Fast PWM Der Timer zählt bis 511
* Modus 7: 10 Bit Fast PWM - Der Timer zählt bis 1023
* Modus 7: 10-Bit Fast PWM Der Timer zählt bis 1023
* Modus 14: Fast PWM mit beliebiger Schrittzahl (festgelegt durch '''ICR1''')
* Modus 14: Fast PWM mit beliebiger Schrittzahl (festgelegt durch '''ICR1''')
* Modus 15: Fast PWM mit beliebiger Schrittzahl (festgelegt durch '''OCR1A''')
* Modus 15: Fast PWM mit beliebiger Schrittzahl (festgelegt durch '''OCR1A''')


Grundsätzlich funktioniert der Fast-PWM Modus so, dass der Timer bei 0 anfängt zu zählen, wobei natürlich der eingestellte Vorteiler des Timers berücksichtigt wird. Erreicht der Timer einen bestimmten Zählerstand (festgelegt durch die Register '''OCR1A''' und '''OCR1B''') wird eine Aktion ausgelöst. Je nach Festlegung kann der entsprechende µC Pin (OC1A und OC1B) entweder  
Grundsätzlich funktioniert der Fast-PWM-Modus so, dass der Timer bei 0 anfängt zu zählen, wobei natürlich der eingestellte Vorteiler des Timers berücksichtigt wird. Erreicht der Timer einen bestimmten Zählerstand (festgelegt durch die Register '''OCR1A''' und '''OCR1B''') wird eine Aktion ausgelöst. Je nach Festlegung kann der entsprechende µC-Pin (OC1A und OC1B) entweder
* umgeschaltet
* umgeschaltet
* auf 1 gesetzt
* auf 1 gesetzt
* auf 0 gesetzt
* auf 0 gesetzt
werden. Wird der OC1A/OC1B Pin so konfiguriert, dass er auf 1 oder 0 gesetzt wird, so wird automatisch der entsprechende Pin beim Timerstand 0 auf den jeweils gegenteiligen Wert gesetzt.
werden. Wird der OC1A/OC1B-Pin so konfiguriert, dass er auf 1 oder 0 gesetzt wird, so wird automatisch der entsprechende Pin beim Timerstand 0 auf den jeweils gegenteiligen Wert gesetzt.


Der OC1A Pin befindet sich beim Mega8 am Port B, konkret am Pin '''PB1'''. Dieser Pin muss über das zugehörige Datenrichtungsregister '''DDRB''' auf Ausgang gestellt werden. Anders als beim UART geschieht dies nicht automatisch.
Der OC1A-Pin befindet sich beim ATmega8 am Port B, konkret am Pin '''PB1'''. Dieser Pin muss über das zugehörige Datenrichtungsregister '''DDRB''' auf Ausgang gestellt werden. Anders als beim UART geschieht dies nicht automatisch.


Das Beispiel zeigt den Modus 14. Dabei wird der Timer-Endstand durch das Register '''ICR1''' festgelegt. Weiters wird die Funktion des OC1A Pins so festgelegt, dass der Pin bei einem Timer Wert von 0 auf 1 gesetzt wird und bei Erreichen des im '''OCR1A''' Registers festgelegten Wertes auf 0 gesetzt wird. Der Vorteiler des Timers, bzw. der ICR-Wert wird zunächst so eingestellt, dass eine an '''PB1''' angeschlossene LED noch blinkt, die Auswirkungen unterschiedlicher Register Werte gut beobachtet werden können. Den Vorteiler zu verringern ist kein Problem, hier geht es aber darum, zu demonstrieren wie PWM funktioniert.
Das Beispiel zeigt den Modus 14. Dabei wird der Timer-Endstand durch das Register ICR1 festgelegt. Des Weiteren wird die Funktion des OC1A-Pins so festgelegt, dass der Pin bei einem Timer-Wert von 0 auf 1 gesetzt wird und bei Erreichen des im OCR1A-Register festgelegten Wertes auf 0 gesetzt wird. Der Vorteiler des Timers bzw. der ICR-Wert wird zunächst so eingestellt, dass eine an PB1 angeschlossene LED noch sichtbar blinkt, damit die Auswirkungen unterschiedlicher Registerwerte gut beobachtet werden können. Den Vorteiler zu verringern ist kein Problem, hier geht es aber darum, zu demonstrieren, wie PWM funktioniert.


<font color="FF0000">Hinweis:</font> Wie überall im ATMega8 ist darauf zu achten, dass beim Beschreiben eines 16-Bit Registers zuerst das High-Byte und dann das Low-Byte geschrieben wird.
'''Hinweis:''' Wie überall im ATmega8 ist darauf zu achten, dass beim Beschreiben eines 16-Bit-Registers '''zuerst das High-Byte''' und '''dann das Low-Byte''' geschrieben wird.


<avrasm>
<syntaxhighlight lang="asm">
.include "m8def.inc"
.include "m8def.inc"
 
.def temp1         = r17
.def temp1 = r17
 
.equ XTAL = 4000000
.equ XTAL = 4000000
 
     rjmp    init
     rjmp    init


Zeile 62: Zeile 65:
;
;
;
;
 
init:
init:
     ldi      temp1, LOW(RAMEND)    ; Stackpointer initialisieren
     ldi      temp1, HIGH(RAMEND)    ; Stackpointer initialisieren
    out      SPH, temp1
    ldi      temp1, LOW(RAMEND)
     out      SPL, temp1
     out      SPL, temp1
    ldi      temp1, HIGH(RAMEND)
 
    out      SPH, temp1
 
     ;
     ;
     ; Timer 1 einstellen
     ; Timer 1 einstellen
Zeile 113: Zeile 116:
     ldi      temp1, 0xFF
     ldi      temp1, 0xFF
     out      OCR1AL, temp1
     out      OCR1AL, temp1
 
     ; Den Pin OC1A zu guter letzt noch auf Ausgang schalten
     ; Den Pin OC1A zu guter letzt noch auf Ausgang schalten
     ldi      temp1, 0x02
     ldi      temp1, 0x02
Zeile 120: Zeile 123:
main:
main:
     rjmp    main
     rjmp    main
</avrasm>
</syntaxhighlight>


Wird dieses Programm laufen gelassen, dann ergibt sich eine blinkende LED. Die LED ist die Hälfte der Blinkzeit an und in der anderen Hälfte des Blinkzyklus aus. Wird der Compare Wert in '''OCR1A''' verändert, so lässt sich das Verhältnis von LED Einzeit zu Auszeit verändern. Ist die LED wie im I/O Kapitel angeschlossen, so führen höhere '''OCR1A''' Werte dazu, dass die LED nur kurz aufblitzt und in der restlichen Zeit dunkel bleibt.
Wird dieses Programm laufen gelassen, dann ergibt sich eine blinkende LED. Die LED ist die Hälfte der Blinkzeit an und in der anderen Hälfte des Blinkzyklus aus. Wird der Compare-Wert in '''OCR1A''' verändert, so lässt sich das Verhältnis von LED-Einzeit zu -Auszeit verändern. Ist die LED wie im [[AVR-Tutorial: IO-Grundlagen|I/O-Kapitel]] angeschlossen, so führen höhere OCR1A-Werte dazu, dass die LED nur kurz aufblitzt und in der restlichen Zeit dunkel bleibt.


<avrasm>
<syntaxhighlight lang="asm">
     ldi      temp1, 0x6D
     ldi      temp1, 0x6D
     out      OCR1AH, temp1
     out      OCR1AH, temp1
     ldi      temp1, 0xFF
     ldi      temp1, 0xFF
     out      OCR1AL, temp1
     out      OCR1AL, temp1
</avrasm>
</syntaxhighlight>


Sinngemäß führen kleinere '''OCR1A''' Werte dazu, daß die LED länger leuchtet und die Dunkelphasen kürzer werden.
Sinngemäß führen kleinere OCR1A-Werte dazu, dass die LED länger leuchtet und die Dunkelphasen kürzer werden.


<avrasm>
<syntaxhighlight lang="asm">
     ldi      temp1, 0x10
     ldi      temp1, 0x10
     out      OCR1AH, temp1
     out      OCR1AH, temp1
     ldi      temp1, 0xFF
     ldi      temp1, 0xFF
     out      OCR1AL, temp1
     out      OCR1AL, temp1
</avrasm>
</syntaxhighlight>


Nachdem die Funktion und das Zusammenspiel der einzelnen Register jetzt klar ist, ist es Zeit aus dem Blinken ein echtes Dimmen zu machen. Dazu genügt es den Vorteiler des Timers auf 1 zu setzen:
Nachdem die Funktion und das Zusammenspiel der einzelnen Register jetzt klar sind, ist es Zeit, aus dem Blinken ein echtes Dimmen zu machen. Dazu genügt es, den Vorteiler des Timers auf 1 zu setzen:


<avrasm>
<syntaxhighlight lang="asm">
     ldi      temp1, 1<<WGM13 | 1<<WGM12 | 1<<CS10
     ldi      temp1, 1<<WGM13 | 1<<WGM12 | 1<<CS10
     out      TCCR1B, temp1
     out      TCCR1B, temp1
</avrasm>
</syntaxhighlight>


Werden wieder die beiden '''OCR1A''' Werte 0x6DFF und 0x10FF ausprobiert, so ist deutlich zu sehen, dass die LED scheinbar unterschiedlich hell leuchtet. Dies ist allerdings eine optische Täuschung. Die LED blinkt nach wie vor, nur blinkt sie so schnell, daß dies für uns nicht mehr wahrnehmbar ist. Durch Variation der Einschalt- zu Ausschaltzeit kann die LED auf viele verschiedene Helligkeitswerte eingestellt werden.
Werden wieder die beiden OCR1A-Werte 0x6DFF und 0x10FF ausprobiert, so ist deutlich zu sehen, dass die LED scheinbar unterschiedlich hell leuchtet. Dies ist allerdings eine optische Täuschung. Die LED blinkt nach wie vor, nur blinkt sie so schnell, dass dies für uns nicht mehr wahrnehmbar ist. Durch Variation des Verhältnisses von Einschalt- zu Ausschaltzeit kann die LED auf viele verschiedene Helligkeitswerte eingestellt werden.


Theoretisch wäre es möglich die LED auf 0x6FFF verschiedene Helligkeitswerte einzustellen. Dies deshalb, weil in '''ICR1''' genau dieser Wert als Endwert für den Timer festgelegt worden ist. Dieser Wert könnte genauso gut kleiner oder größer eingestellt werden. Um eine LED zu dimmen ist der Maximalwert aber hoffnungslos zu hoch. Für diese Aufgabe reicht eine Abstufung von 256 oder 512 Stufen normalerweise völlig aus. Genau für diese Fälle gibt es die anderen Modi. Anstatt den Timer Endstand mittels '''ICR1''' festzulegen, genügt es den Timer einfach nur in den 8, 9 oder 10 Bit Modus zu konfigurieren und damit eine PWM mit 256 (8 Bit), 512 (9 Bit) oder 1024 (10 Bit) Stufen zu erzeugen.
Theoretisch wäre es möglich, die LED auf 0x6FFF (=&nbsp;28.671) verschiedene Helligkeitswerte einzustellen. Dies deshalb, weil in '''ICR1''' genau dieser Wert als Endwert für den Timer festgelegt worden ist. Dieser Wert könnte genauso gut kleiner oder größer eingestellt werden. Um eine LED zu dimmen ist der Maximalwert aber hoffnungslos zu hoch. Für diese Aufgabe reicht eine Abstufung von 256 oder 512 Stufen normalerweise völlig aus. Genau für diese Fälle gibt es die anderen Modi. Anstatt den Timer-Endstand mittels ICR1 festzulegen, genügt es, den Timer einfach nur in den 8-, 9- oder 10-Bit-Modus zu konfigurieren und damit eine PWM mit 256 (8&nbsp;Bit), 512 (9&nbsp;Bit) oder 1024 (10&nbsp;Bit) Stufen zu erzeugen.


<!--
<!--
===PWM mit Timer 2 (OCR2)===
===PWM mit Timer 2 (OCR2)===


===Phasen-korrekte PWM===
===Phasenkorrekte PWM===


===Phasen- und Frequenz-korrekte PWM===
===Phasen- und frequenzkorrekte PWM===
-->
-->
==PWM in Software==
==PWM in Software==
Die Realisierung einer PWM mit einem Timer, wobei der Timer die ganze Arbeit macht, ist zwar einfach, hat aber einen Nachteil. Für jede einzelne PWM ist ein eigener Timer notwendig. Und davon gibt es in einem Mega8 nicht all zu viele.
Die Realisierung einer PWM mit einem Timer, wobei der Timer die ganze Arbeit macht, ist zwar einfach, hat aber einen Nachteil. Für jede einzelne PWM ist ein eigener Timer notwendig (Ausnahme: Der Timer&nbsp;1 besitzt zwei Compare-Register und kann damit zwei PWM-Stufen erzeugen). Und davon gibt es in einem ATmega8 nicht allzu viele.


Es geht auch anders: Es ist durchaus möglich viele PWM Stufen mit nur einem Timer zu realisieren. Der Timer wird nur noch dazu benötigt, eine stabile und konstante Zeitbasis zu erhalten. Von dieser Zeitbasis wird alles weitere abgeleitet.
Es geht auch anders: Es ist durchaus möglich, viele PWM-Stufen mit nur einem Timer zu realisieren. Der Timer wird nur noch dazu benötigt, eine stabile und konstante Zeitbasis zu erhalten. Von dieser Zeitbasis wird alles weitere abgeleitet.


===Prinzip===
===Prinzip===
Das Grundprinzip ist dabei sehr einfach: Eine PWM ist ja im Grunde nichts anderes als eine Blinkschleife, bei der das Verhältnis von Ein- zu Auszeit variabel eingestellt werden kann. Die Blinkfrequenz selbst ist konstant und ist so schnell, dass das eigentliche Blinken nicht mehr wahrgenommen werden kann. Das lässt sich aber auch alles in einer ISR realisieren:
Das Grundprinzip ist dabei sehr einfach: Eine PWM ist ja im Grunde nichts anderes als eine Blinkschleife, bei der das Verhältnis von Ein- zu Auszeit variabel eingestellt werden kann. Die Blinkfrequenz selbst ist konstant und ist so schnell, dass das eigentliche Blinken nicht mehr wahrgenommen werden kann. Das lässt sich aber auch alles in einer [[Interrupt|ISR]] realisieren:
* Ein Timer (Timer0) wird so aufgesetzt, dass er eine Overflow-Interruptfunktion (ISR) mit dem 256-fache der gewünschten Blinkfrequenz aufruft.
* Ein Timer (Timer&nbsp;0) wird so aufgesetzt, dass er eine Overflow-Interruptfunktion (ISR) mit dem 256-fachen der gewünschten Blinkfrequenz aufruft.
* In der ISR wird ein weiterer Zähler betrieben (<i>PWMCounter</i>), der ständig von 0 bis 255 zählt.
* In der ISR wird ein weiterer Zähler betrieben (''PWMCounter''), der ständig von 0 bis 255 zählt.
* Für jede zu realisierende PWM Stufe gibt es einen Grenzwert. Liegt der Wert des PWMCounters unter diesem Wert, so wird der entsprechende Port Pin eingeschaltet. Liegt er darüber, so wird der entsprechende Port Pin ausgeschaltet
* Für jede zu realisierende PWM-Stufe gibt es einen Grenzwert. Liegt der Wert des ''PWMCounter''s unter diesem Wert, so wird der entsprechende Port-Pin eingeschaltet. Liegt er darüber, so wird der entsprechende Port-Pin ausgeschaltet.


Damit wird im Grunde nichts anderes gemacht, als die Funktionalität der Fast-PWM in Software nachzubilden. Da man dabei aber nicht auf ein einziges OCR Register angewiesen ist, sondern in gewissen Umfang beliebig viele davon implementieren kann, kann man auch beliebig viele PWM Stufen erzeugen.
Damit wird im Grunde nichts anderes gemacht, als die Funktionalität der Fast-PWM in Software nachzubilden. Da man dabei aber nicht auf ein einziges OCR-Register angewiesen ist, sondern in gewissem Umfang beliebig viele davon implementieren kann, kann man auch beliebig viele PWM-Stufen erzeugen.


===Programm===
===Programm===
Am '''Port B''' werden an den Pins '''PB0''' bis '''PB5''' insgesamt 6 LEDs gemäß der Verschaltung aus dem [[AVR-Tutorial: IO-Grundlagen|I/O Artikel]] angeschlossen. Jede einzelne LED kann durch setzen eines Wertes von 0 bis 127 in die zugehörigen Register <i>ocr_1</i> bis <i>ocr_6</i> auf einen anderen Helligkeitswert eingestellt werden. Die PWM-Frequenz (Blinkfrequenz) jeder LED beträgt: ( 4000000 / 256 ) / 127 = 123Hz. Dies reicht aus um das Blinken unter die Wahrnehmungsschwelle zu drücken und die LEDs gleichmässig erleuchtet erscheinen zu lassen.
Am '''Port B''' werden an den Pins '''PB0''' bis '''PB5''' insgesamt 6 LEDs gemäß der Verschaltung aus dem [[AVR-Tutorial: IO-Grundlagen|I/O-Kapitel]] angeschlossen. Jede einzelne LED kann durch Setzen eines Wertes von 0 bis 127 in die zugehörigen Register <code>ocr_1</code> bis <code>ocr_6</code> auf einen anderen Helligkeitswert eingestellt werden. Die PWM-Frequenz (Blinkfrequenz) jeder LED beträgt: (4.000.000 / 256) / 127 = 123&nbsp;Hz. Dies reicht aus, um das Blinken unter die Wahrnehmungsschwelle zu drücken und die LEDs gleichmäßig erleuchtet erscheinen zu lassen.


<avrasm>
<syntaxhighlight lang="asm">
.include "m8def.inc"
.include "m8def.inc"
 
.def temp  = r16
.def temp  = r16


Zeile 187: Zeile 191:
.def ocr_5 = r22                      ; Helligkeitswert Led5: 0 .. 127
.def ocr_5 = r22                      ; Helligkeitswert Led5: 0 .. 127
.def ocr_6 = r23                      ; Helligkeitswert Led6: 0 .. 127
.def ocr_6 = r23                      ; Helligkeitswert Led6: 0 .. 127
 
.org 0x0000
.org 0x0000
         rjmp    main                  ; Reset Handler
         rjmp    main                  ; Reset Handler
.org OVF0addr
.org OVF0addr
         rjmp    timer0_overflow      ; Timer Overflow Handler
         rjmp    timer0_overflow      ; Timer Overflow Handler
 
main:
main:
         ldi    temp, LOW(RAMEND)    ; Stackpointer initialisieren
         ldi    temp, LOW(RAMEND)    ; Stackpointer initialisieren
Zeile 198: Zeile 202:
         ldi    temp, HIGH(RAMEND)
         ldi    temp, HIGH(RAMEND)
         out    SPH, temp
         out    SPH, temp
 
         ldi    temp, 0xFF            ; Port B auf Ausgang
         ldi    temp, 0xFF            ; Port B auf Ausgang
         out    DDRB, temp
         out    DDRB, temp
Zeile 208: Zeile 212:
         ldi    ocr_5, 80
         ldi    ocr_5, 80
         ldi    ocr_6, 127
         ldi    ocr_6, 127
 
         ldi    temp, 0b00000001      ; CS00 setzen: Teiler 1
         ldi    temp, 1<<CS00        ; CS00 setzen: Teiler 1
         out    TCCR0, temp
         out    TCCR0, temp
 
         ldi    temp, 0b00000001      ; TOIE0: Interrupt bei Timer Overflow
         ldi    temp, 1<<TOIE0        ; TOIE0: Interrupt bei Timer Overflow
         out    TIMSK, temp
         out    TIMSK, temp
 
         sei
         sei
 
loop:  rjmp    loop
loop:  rjmp    loop
 
timer0_overflow:                      ; Timer 0 Overflow Handler
timer0_overflow:                      ; Timer 0 Overflow Handler
         inc    PWMCount              ; den PWM Zähler von 0 bis
         inc    PWMCount              ; den PWM Zähler von 0 bis
Zeile 229: Zeile 233:


         cp      PWMCount, ocr_1      ; Ist der Grenzwert für Led 1 erreicht
         cp      PWMCount, ocr_1      ; Ist der Grenzwert für Led 1 erreicht
         brlt   OneOn
         brlo   OneOn
         ori    temp, $01
         ori    temp, $01


OneOn:  cp      PWMCount, ocr_2      ; Ist der Grenzwert für Led 2 erreicht
OneOn:  cp      PWMCount, ocr_2      ; Ist der Grenzwert für Led 2 erreicht
         brlt   TwoOn
         brlo   TwoOn
         ori    temp, $02
         ori    temp, $02


TwoOn:  cp      PWMCount, ocr_3      ; Ist der Grenzwert für Led 3 erreicht
TwoOn:  cp      PWMCount, ocr_3      ; Ist der Grenzwert für Led 3 erreicht
         brlt   ThreeOn
         brlo   ThreeOn
         ori    temp, $04
         ori    temp, $04


ThreeOn:cp      PWMCount, ocr_4      ; Ist der Grenzwert für Led 4 erreicht
ThreeOn:cp      PWMCount, ocr_4      ; Ist der Grenzwert für Led 4 erreicht
         brlt   FourOn
         brlo   FourOn
         ori    temp, $08
         ori    temp, $08


FourOn: cp      PWMCount, ocr_5      ; Ist der Grenzwert für Led 5 erreicht
FourOn: cp      PWMCount, ocr_5      ; Ist der Grenzwert für Led 5 erreicht
         brlt   FiveOn
         brlo   FiveOn
         ori    temp, $10
         ori    temp, $10


FiveOn: cp      PWMCount, ocr_6      ; Ist der Grenzwert für Led 6 erreicht
FiveOn: cp      PWMCount, ocr_6      ; Ist der Grenzwert für Led 6 erreicht
         brlt   SetBits
         brlo   SetBits
         ori    temp, $20
         ori    temp, $20


Zeile 256: Zeile 260:


         reti
         reti
</avrasm>
</syntaxhighlight>


Würde man die LEDs anstatt direkt an ein Port anzuschliessen, über ein oder mehrere [[AVR-Tutorial: Schieberegister|Schieberegister]] anschließen, so kann auf diese Art eine relativ große Anzahl an LEDs gedimmt werden. Natürlich müsste man die softwareseitige LED Ansteuerung gegenüber der hier gezeigten verändern, aber das PWM Prinzip könnte so übernommen werden.
Würde man die LEDs nicht direkt an einen Port, sondern über ein oder mehrere [[AVR-Tutorial: Schieberegister|Schieberegister]] anschließen, so könnte auf diese Art eine relativ große Anzahl an LEDs gedimmt werden. Natürlich müsste man die softwareseitige LED-Ansteuerung gegenüber der hier gezeigten verändern, aber das PWM-Prinzip könnte so übernommen werden.


== Siehe auch ==
== Siehe auch ==
* [[PWM]]
* [[PWM]]
* [[AVR-GCC-Tutorial#PWM (Pulsweitenmodulation)|AVR-GCC-Tutorial: PWM]]
* [[AVR_PWM]]
* [[Soft-PWM]] - optimierte Software-PWM in C
* [[Soft-PWM]] optimierte Software-PWM in C
* [[LED-Fading]] - LED dimmen mit PWM
* [[LED-Fading]] LED dimmen mit PWM
 
----


{{Navigation_zurückhochvor|
{{Navigation_zurückhochvor|
Zeile 274: Zeile 280:
vorlink=AVR-Tutorial: Schieberegister}}
vorlink=AVR-Tutorial: Schieberegister}}


[[Category:AVR]][[Category:AVR-Tutorial]]
[[Category:AVR-Tutorial|PWM]]

Aktuelle Version vom 21. November 2022, 09:42 Uhr

PWM – Dieses Kürzel steht für Puls-Weiten-Modulation (oder englisch Pulse Width Modulation).

Was bedeutet PWM?

Viele elektrische Verbraucher können in ihrer Leistung reguliert werden, indem die Versorgungsspannung in weiten Bereichen verändert wird. Ein normaler Gleichstrommotor wird z. B. langsamer laufen, wenn er mit einer geringeren Spannung versorgt wird, bzw. schneller laufen, wenn er mit einer höheren Spannung versorgt wird. LEDs werden zwar nicht mit einer Spannung gedimmt, sondern mit dem Versorgungsstrom. Da dieser Stromfluss aber im Normalfall mit einem Vorwiderstand eingestellt wird, ist durch das Ohmsche Gesetz dieser Stromfluss bei konstantem Widerstand wieder (näherungsweise) direkt proportional zur Höhe der Versorgungsspannung.

Im wesentlichen geht es also immer um diese Kennlinie, trägt man die Versorgungsspannung entlang der Zeitachse auf:

PWM 1.gif

Die Fläche unter der Kurve ist dabei ein direktes Maß für die Energie, die dem System zugeführt wird. Bei geringerer Energie ist die Helligkeit geringer, bei höherer Energie entsprechend heller.

Jedoch gibt es noch einen zweiten Weg, die dem System zugeführte Energie zu verringern. Anstatt die Spannung abzusenken, ist es auch möglich, die volle Versorgungsspannung über einen geringeren Zeitraum anzulegen. Man muß nur dafür Sorge tragen, dass im Endeffekt die einzelnen Pulse nicht mehr wahrnehmbar sind.

PWM Theorie 1.gif

Die Fläche unter den Rechtecken hat in diesem Fall dieselbe Größe wie die Fläche unter der Spannung V=, glättet man die Spannung also mit einem Kondensator, ergibt sich eine niedrigere konstante Spannung. Die Rechtecke sind zwar höher, aber dafür schmaler. Die Flächen sind aber dieselben. Diese Lösung hat den Vorteil, dass keine Spannung geregelt werden muss, sondern der Verbraucher immer mit derselben Spannung versorgt wird.

Und genau das ist das Prinzip einer PWM. Durch die Abgabe von Pulsen wird die abgegebene Energiemenge gesteuert. Es ist auf einem µC wesentlich einfacher, Pulse mit einem definierten Puls/Pausen-Verhältnis zu erzeugen, als eine Spannung zu variieren.

PWM und der Timer

Der Timer 1 des ATmega8 unterstützt direkt das Erzeugen von PWM. Beginnt der Timer beispielsweise bei 0 zu zählen, so schaltet er gleichzeitig einen Ausgangspin ein. Erreicht der Zähler einen bestimmten Wert X, so schaltet er den Ausgangspin wieder aus und zählt weiter bis zu seiner Obergrenze. Danach wiederholt sich das Spielchen. Der Timer beginnt wieder bei 0 und schaltet gleichzeitig den Ausgangspin ein usw. Durch Verändern von X kann man daher steuern, wie lange der Ausgangspin im Verhältnis zur kompletten Zeit, die der Timer benötigt, um seine Obergrenze zu erreichen, eingeschaltet ist.

Dabei gibt es aber verwirrenderweise verschiedene Arten der PWM:

  • Fast PWM (= schnelle PWM)
  • Phasenkorrekte PWM
  • Phasen- und frequenzkorrekte PWM

Für die Details zu jedem PWM-Modus sei auf das Datenblatt verwiesen.

Fast PWM

Die Fast PWM gibt es beim ATmega8 mit mehreren unterschiedlichen Bit-Zahlen. Bei den Bit-Zahlen geht es immer darum, wie weit der Timer zählt, bevor ein Rücksetzen des Timers auf 0 erfolgt:

  • Modus 5: 8-Bit Fast PWM – Der Timer zählt bis 255
  • Modus 6: 9-Bit Fast PWM – Der Timer zählt bis 511
  • Modus 7: 10-Bit Fast PWM – Der Timer zählt bis 1023
  • Modus 14: Fast PWM mit beliebiger Schrittzahl (festgelegt durch ICR1)
  • Modus 15: Fast PWM mit beliebiger Schrittzahl (festgelegt durch OCR1A)

Grundsätzlich funktioniert der Fast-PWM-Modus so, dass der Timer bei 0 anfängt zu zählen, wobei natürlich der eingestellte Vorteiler des Timers berücksichtigt wird. Erreicht der Timer einen bestimmten Zählerstand (festgelegt durch die Register OCR1A und OCR1B) wird eine Aktion ausgelöst. Je nach Festlegung kann der entsprechende µC-Pin (OC1A und OC1B) entweder

  • umgeschaltet
  • auf 1 gesetzt
  • auf 0 gesetzt

werden. Wird der OC1A/OC1B-Pin so konfiguriert, dass er auf 1 oder 0 gesetzt wird, so wird automatisch der entsprechende Pin beim Timerstand 0 auf den jeweils gegenteiligen Wert gesetzt.

Der OC1A-Pin befindet sich beim ATmega8 am Port B, konkret am Pin PB1. Dieser Pin muss über das zugehörige Datenrichtungsregister DDRB auf Ausgang gestellt werden. Anders als beim UART geschieht dies nicht automatisch.

Das Beispiel zeigt den Modus 14. Dabei wird der Timer-Endstand durch das Register ICR1 festgelegt. Des Weiteren wird die Funktion des OC1A-Pins so festgelegt, dass der Pin bei einem Timer-Wert von 0 auf 1 gesetzt wird und bei Erreichen des im OCR1A-Register festgelegten Wertes auf 0 gesetzt wird. Der Vorteiler des Timers bzw. der ICR-Wert wird zunächst so eingestellt, dass eine an PB1 angeschlossene LED noch sichtbar blinkt, damit die Auswirkungen unterschiedlicher Registerwerte gut beobachtet werden können. Den Vorteiler zu verringern ist kein Problem, hier geht es aber darum, zu demonstrieren, wie PWM funktioniert.

Hinweis: Wie überall im ATmega8 ist darauf zu achten, dass beim Beschreiben eines 16-Bit-Registers zuerst das High-Byte und dann das Low-Byte geschrieben wird.

.include "m8def.inc"

.def temp1 = r17

.equ XTAL = 4000000

    rjmp    init

;.include "keys.asm" 
;
;

init:
    ldi      temp1, HIGH(RAMEND)     ; Stackpointer initialisieren
    out      SPH, temp1
    ldi      temp1, LOW(RAMEND)
    out      SPL, temp1

    ;
    ; Timer 1 einstellen
    ;
    ; Modus 14:
    ;    Fast PWM, Top von ICR1
    ;
    ;     WGM13    WGM12   WGM11    WGM10
    ;      1        1       1        0
    ;
    ;    Timer Vorteiler: 256
    ;     CS12     CS11    CS10
    ;      1        0       0
    ;
    ; Steuerung des Ausgangsport: Set at BOTTOM, Clear at match
    ;     COM1A1   COM1A0
    ;      1        0
    ;

    ldi      temp1, 1<<COM1A1 | 1<<WGM11
    out      TCCR1A, temp1

    ldi      temp1, 1<<WGM13 | 1<<WGM12 | 1<<CS12
    out      TCCR1B, temp1

    ;
    ; den Endwert (TOP) für den Zähler setzen
    ; der Zähler zählt bis zu diesem Wert
    ;
    ldi      temp1, 0x6F
    out      ICR1H, temp1
    ldi      temp1, 0xFF
    out      ICR1L, temp1

    ;
    ; der Compare Wert
    ; Wenn der Zähler diesen Wert erreicht, wird mit
    ; obiger Konfiguration der OC1A Ausgang abgeschaltet
    ; Sobald der Zähler wieder bei 0 startet, wird der
    ; Ausgang wieder auf 1 gesetzt
    ;
    ldi      temp1, 0x3F
    out      OCR1AH, temp1
    ldi      temp1, 0xFF
    out      OCR1AL, temp1

    ; Den Pin OC1A zu guter letzt noch auf Ausgang schalten
    ldi      temp1, 0x02
    out      DDRB, temp1

main:
    rjmp     main

Wird dieses Programm laufen gelassen, dann ergibt sich eine blinkende LED. Die LED ist die Hälfte der Blinkzeit an und in der anderen Hälfte des Blinkzyklus aus. Wird der Compare-Wert in OCR1A verändert, so lässt sich das Verhältnis von LED-Einzeit zu -Auszeit verändern. Ist die LED wie im I/O-Kapitel angeschlossen, so führen höhere OCR1A-Werte dazu, dass die LED nur kurz aufblitzt und in der restlichen Zeit dunkel bleibt.

    ldi      temp1, 0x6D
    out      OCR1AH, temp1
    ldi      temp1, 0xFF
    out      OCR1AL, temp1

Sinngemäß führen kleinere OCR1A-Werte dazu, dass die LED länger leuchtet und die Dunkelphasen kürzer werden.

    ldi      temp1, 0x10
    out      OCR1AH, temp1
    ldi      temp1, 0xFF
    out      OCR1AL, temp1

Nachdem die Funktion und das Zusammenspiel der einzelnen Register jetzt klar sind, ist es Zeit, aus dem Blinken ein echtes Dimmen zu machen. Dazu genügt es, den Vorteiler des Timers auf 1 zu setzen:

    ldi      temp1, 1<<WGM13 | 1<<WGM12 | 1<<CS10
    out      TCCR1B, temp1

Werden wieder die beiden OCR1A-Werte 0x6DFF und 0x10FF ausprobiert, so ist deutlich zu sehen, dass die LED scheinbar unterschiedlich hell leuchtet. Dies ist allerdings eine optische Täuschung. Die LED blinkt nach wie vor, nur blinkt sie so schnell, dass dies für uns nicht mehr wahrnehmbar ist. Durch Variation des Verhältnisses von Einschalt- zu Ausschaltzeit kann die LED auf viele verschiedene Helligkeitswerte eingestellt werden.

Theoretisch wäre es möglich, die LED auf 0x6FFF (= 28.671) verschiedene Helligkeitswerte einzustellen. Dies deshalb, weil in ICR1 genau dieser Wert als Endwert für den Timer festgelegt worden ist. Dieser Wert könnte genauso gut kleiner oder größer eingestellt werden. Um eine LED zu dimmen ist der Maximalwert aber hoffnungslos zu hoch. Für diese Aufgabe reicht eine Abstufung von 256 oder 512 Stufen normalerweise völlig aus. Genau für diese Fälle gibt es die anderen Modi. Anstatt den Timer-Endstand mittels ICR1 festzulegen, genügt es, den Timer einfach nur in den 8-, 9- oder 10-Bit-Modus zu konfigurieren und damit eine PWM mit 256 (8 Bit), 512 (9 Bit) oder 1024 (10 Bit) Stufen zu erzeugen.


PWM in Software

Die Realisierung einer PWM mit einem Timer, wobei der Timer die ganze Arbeit macht, ist zwar einfach, hat aber einen Nachteil. Für jede einzelne PWM ist ein eigener Timer notwendig (Ausnahme: Der Timer 1 besitzt zwei Compare-Register und kann damit zwei PWM-Stufen erzeugen). Und davon gibt es in einem ATmega8 nicht allzu viele.

Es geht auch anders: Es ist durchaus möglich, viele PWM-Stufen mit nur einem Timer zu realisieren. Der Timer wird nur noch dazu benötigt, eine stabile und konstante Zeitbasis zu erhalten. Von dieser Zeitbasis wird alles weitere abgeleitet.

Prinzip

Das Grundprinzip ist dabei sehr einfach: Eine PWM ist ja im Grunde nichts anderes als eine Blinkschleife, bei der das Verhältnis von Ein- zu Auszeit variabel eingestellt werden kann. Die Blinkfrequenz selbst ist konstant und ist so schnell, dass das eigentliche Blinken nicht mehr wahrgenommen werden kann. Das lässt sich aber auch alles in einer ISR realisieren:

  • Ein Timer (Timer 0) wird so aufgesetzt, dass er eine Overflow-Interruptfunktion (ISR) mit dem 256-fachen der gewünschten Blinkfrequenz aufruft.
  • In der ISR wird ein weiterer Zähler betrieben (PWMCounter), der ständig von 0 bis 255 zählt.
  • Für jede zu realisierende PWM-Stufe gibt es einen Grenzwert. Liegt der Wert des PWMCounters unter diesem Wert, so wird der entsprechende Port-Pin eingeschaltet. Liegt er darüber, so wird der entsprechende Port-Pin ausgeschaltet.

Damit wird im Grunde nichts anderes gemacht, als die Funktionalität der Fast-PWM in Software nachzubilden. Da man dabei aber nicht auf ein einziges OCR-Register angewiesen ist, sondern in gewissem Umfang beliebig viele davon implementieren kann, kann man auch beliebig viele PWM-Stufen erzeugen.

Programm

Am Port B werden an den Pins PB0 bis PB5 insgesamt 6 LEDs gemäß der Verschaltung aus dem I/O-Kapitel angeschlossen. Jede einzelne LED kann durch Setzen eines Wertes von 0 bis 127 in die zugehörigen Register ocr_1 bis ocr_6 auf einen anderen Helligkeitswert eingestellt werden. Die PWM-Frequenz (Blinkfrequenz) jeder LED beträgt: (4.000.000 / 256) / 127 = 123 Hz. Dies reicht aus, um das Blinken unter die Wahrnehmungsschwelle zu drücken und die LEDs gleichmäßig erleuchtet erscheinen zu lassen.

.include "m8def.inc"

.def temp  = r16

.def PWMCount = r17

.def ocr_1 = r18                      ; Helligkeitswert Led1: 0 .. 127
.def ocr_2 = r19                      ; Helligkeitswert Led2: 0 .. 127
.def ocr_3 = r20                      ; Helligkeitswert Led3: 0 .. 127
.def ocr_4 = r21                      ; Helligkeitswert Led4: 0 .. 127
.def ocr_5 = r22                      ; Helligkeitswert Led5: 0 .. 127
.def ocr_6 = r23                      ; Helligkeitswert Led6: 0 .. 127

.org 0x0000
        rjmp    main                  ; Reset Handler
.org OVF0addr
        rjmp    timer0_overflow       ; Timer Overflow Handler

main:
        ldi     temp, LOW(RAMEND)     ; Stackpointer initialisieren
        out     SPL, temp
        ldi     temp, HIGH(RAMEND)
        out     SPH, temp

        ldi     temp, 0xFF            ; Port B auf Ausgang
        out     DDRB, temp

        ldi     ocr_1, 0
        ldi     ocr_2, 1
        ldi     ocr_3, 10
        ldi     ocr_4, 20
        ldi     ocr_5, 80
        ldi     ocr_6, 127

        ldi     temp, 1<<CS00         ; CS00 setzen: Teiler 1
        out     TCCR0, temp

        ldi     temp, 1<<TOIE0        ; TOIE0: Interrupt bei Timer Overflow
        out     TIMSK, temp

        sei

loop:   rjmp    loop

timer0_overflow:                      ; Timer 0 Overflow Handler
        inc     PWMCount              ; den PWM Zähler von 0 bis
        cpi     PWMCount, 128         ; 127 zählen lassen
        brne    WorkPWM
        clr     PWMCount

WorkPWM:
        ldi     temp, 0b11000000      ; 0 .. Led an, 1 .. Led aus

        cp      PWMCount, ocr_1       ; Ist der Grenzwert für Led 1 erreicht
        brlo    OneOn
        ori     temp, $01

OneOn:  cp      PWMCount, ocr_2       ; Ist der Grenzwert für Led 2 erreicht
        brlo    TwoOn
        ori     temp, $02

TwoOn:  cp      PWMCount, ocr_3       ; Ist der Grenzwert für Led 3 erreicht
        brlo    ThreeOn
        ori     temp, $04

ThreeOn:cp      PWMCount, ocr_4       ; Ist der Grenzwert für Led 4 erreicht
        brlo    FourOn
        ori     temp, $08

FourOn: cp      PWMCount, ocr_5       ; Ist der Grenzwert für Led 5 erreicht
        brlo    FiveOn
        ori     temp, $10

FiveOn: cp      PWMCount, ocr_6       ; Ist der Grenzwert für Led 6 erreicht
        brlo    SetBits
        ori     temp, $20

SetBits:                              ; Die neue Bitbelegung am Port ausgeben
        out     PORTB, temp

        reti

Würde man die LEDs nicht direkt an einen Port, sondern über ein oder mehrere Schieberegister anschließen, so könnte auf diese Art eine relativ große Anzahl an LEDs gedimmt werden. Natürlich müsste man die softwareseitige LED-Ansteuerung gegenüber der hier gezeigten verändern, aber das PWM-Prinzip könnte so übernommen werden.

Siehe auch