Die nahezu perfekte Poti-PWM per AVR (12.4.2016)
PWM-Poti-Dimmer mit Luminanz-Korrektur (13.4.2016)
Erfahrungsbericht 10-bit-Poti-PWM-Dimmer (15.4.2016)
Bei unserem ersten - ausgesprochen einfachen - Versuch eines µC-basierenden PWM-Dimmers soll ein Potenziometer zum Einstellen des PWM-Tastverhältnisses dienen.
Hierzu wird der Abgriff des von der µC-Versorgungsspannung gespeisten Potis an den Eingang eines A/D-Wandlers gelegt.
Damit erhält man - je nach Poti-Stellung - Digitalwerte von 0 bis 255 (bzw. 0xff), die direkt zum Speisen einer der beiden 8-bit-PWMs des AVR verwendet werden.
D.h., außer einem Poti (bei meinem Aufbau ein lineares mit einem Wert von 47 kΩ) und dem AVR-µC benötigen wir nichts, um eine solide und dennoch einfache PWM zu realisieren, mit der man dann einen geeigneten LED-Treiber dimmen kann.Einfacher geht es kaum!
Im konkreten Fall mit einem ATmega328P (getaktet mit 3,6864 MHz) habe ich die Fast[1]-PWM des Timers 2 mit Ausgang OC2A an Pin 17 (PORTB,3) und den A/D-Wandler-Eingang ADC5 an Pin 28 (PORTC,5) verwendet. Statt ADC5 kann man natürlich auch einen der anderen ADC0...4 verwenden, je nachdem, welches Pin man frei hat.
Und so schaut das s'AVR-Programm dazu aus (wie immer ohne den Include-Befehl für den jeweils verwendeten AVR, die Interrupt-Vektoren und die Initialisierung des Stack):
.def rmp = r16 ; temporary register (multi purpose)
.def ADCresult = r17 ; ADC result
; ADC (using ADC5 at PORTC,5):
ldi rmp,(1<<REFS0 | 1<<ADLAR | 1<< MUX2 | 1<< MUX0)
sts ADMUX,rmp ; select VCC ref, ADC left adjusted, ADC5
clr rmp
sts ADCSRB,rmp ; ADC free running mode (default setting)
ldi rmp,1<<ADC5D
sts DIDR0,rmp ; disable digital in for ADC5
ldi rmp,(1<<ADEN | 1<<ADSC | 1<<ADPS2 | 1<<ADPS0)
sts ADCSRA,rmp ; enable ADC, ; prescaler /32 for 114 kHz sampling rate
; PWM using TC2 at Pin 17 (OC2A at PORTB,3):
ldi rmp,(1<<COM2A1 | 1<<WGM21 | 1<<WGM20)
sts TCCR2A,rmp ; non-inverted mode, Fast PWM
ldi rmp,1<<CS22
sts TCCR2B,rmp ; prescaler /64, results in 225 Hz PWM
sbi DDRB,3 ; enable OC2A as output
cbi PORTB,3 ; prepare output for LOW
LOOP ; main loop
lds rmp,ADCSRA ; read ADCSRA
sbr rmp,1<<ADSC ; set start ADC bit
sts ADCSRA,rmp ; write back ADCSRA
lds rmp,ADCSRA
UNTIL !rmp,ADSC ; cleared after ADC ready
lds ADCResult,ADCH ; High byte only (ADC left adjusted)
sts OCR2A,ADCresult ; for PWM
ENDL ; main loop (forever) Das Hauptprogramm zwischen LOOP und ENDL ist kleiner als die Initialisierung von ADC und PWM. Wenn erst einmal die Poti-Stellung per A/D-Wandler-Ergebnis verfügbar ist, ist für die PWM nur noch ein Store-Befehl nötig. Fertig ist die PWM-Laube!
Nicht so perfekt
Zunächst sehr gut passt die per TCCR2B mit Teilerfaktor 64 eingestellte PWM-Frequenz von 225 Hz.
Auch dass das Poti bei Rechtsanschlag einen PWM-Wert von 0xff für saubere 100% Tastverhältnis (also dauerhaft EIN) liefert, fällt positiv auf.
Weniger gut gefällt eine Eigenschaft der PWM (die auch im Datenblatt erwähnt ist), dass diese immer noch einen (wenn auch sehr kurzen) Impuls am Ausgang OC2A liefert, wenn OCR2A der PWM mit dem Wert 0x00 (also Poti in Linksanschlag) gefüttert wird. Dieser Impuls ergibt ein (festes) Tastverhältnis von 0,39%, das eine LED immer noch sichtlich leuchten lässt.
Nun, das liegt daran, dass für das PWM-Tastverhältnis der Fast[1]-PWM mit Timer 2 des AVR folgende Formel gilt (steht nicht im Datenblatt):
PWM-Tastverhältnis = (OCR2A-Wert +1 )/256
Mit den 256 Werten 0 bis 255 für OCR2A erhält man also rechnerisch die genannten 0,39% bis 100%, die auch per DMM nachgemessen exakt stimmen.
Wenn diese 0,39% Resthelligkeit beim Nullwert nicht stört (oder gar erwünscht ist), kann man mit Pin OC2A des AVR auch schon direkt einen LED-Treiber mit PWM- oder Enable-Eingang ansteuern.
Falls der LED-Treiber einen negierten Enable- (bzw. einen Shutdown-) Eingang hat, kann man das PWM-Signal einfach beim Initialisieren von TCCR2A invertieren (man benötigt also keinen externen Inverter):
ldi rmp,(1<<COM2A1 | 1<<COM2A0 | 1<<WGM21 | 1<<WGM20)
sts TCCR2A,rmp ; inverted mode, Fast PWM Statt den an Hochsprachen angelehnten Links-Schiebe-Anweisungen für den AVR-Assembler mittels "1<<" zum Definieren der einzelnen Konfigurations-Bits kann man natürlich auch direkt Binär- (0b...._....) oder Hex-Werte (0x..) angeben.Das ist möglicherweise einleuchtender (und ggf. praktischer beim Debuggen), aber weniger flexibel, wenn man auf einen anderen AVR-µC umsteigt.
Operand out of range[2]
Ein anderes (wichtiges) Detail sei bei dieser Gelegenheit auch noch erwähnt: Beim verwendeten ATmega328 liegen die Timer und der A/D-Wandler nicht im I/O-Adressbereich bis 0x3f (63), der per IN- und OUT-Befehlen bedient wird, sondern oberhalb.Deshalb sind an dieser Stelle LDS- und STS-Befehle nötig (die im Programmspeicher zwei Befehlsworte benötigen statt nur einem).
Ohne Blitzer
Falls die Resthelligkeit beim Nullwert aber stört, lässt sich dieser Blitzer auch einfach beseitigen, indem man ihn per IF/ELSE/ENDI-Struktur vor dem Laden von OCR2A (sts OCR2A,ADCresult) ausblendet, wodurch man das PWM-Ausgangs-Pin abschaltet:
IF ADCResult == #0
sts TCCR2A,rmp ; force OC2A to GND
sts TCCR2A,rmp ; re-enable non-inverted mode, Fast PWM
ENDI
... Wichtig ist, auch die ELSE-Struktur zu bedienen, sonst wird das LED-Licht mit dem ersten Null-Drehen dauerhaft erlöschen ...
Ein kleiner Nachteil dabei ist, dass der erste Hellwert (nämlich 1) dann bereits 0,78% Tastverhältnis hat (siehe obige Formel) und der ausgeblendete Helligkeitwert mit 0,39% Tastverhältnis fehlt. Andererseits benötigt die Einstellung von einzelnen Bits mit einem "normalen" Poti und einem kleinen Drehknopf bei 8 Bit Auflösung schon ziemlich Fingerspitzengefühl (aber es geht).
Andere AVR-µC
Bei den kleineren AVRs mit weniger Resourcen müsste man für dasselbe Verhalten statt Timer 2 den Timer 0 (ebenfalls im Fast-PWM-Mode) verwenden. Statt den LDS/STS-Befehlen im obigen Programm könnte man die IN/OUT-Befehle nehmen (muss aber nicht), da die Peripherie im I/O-Bereich bis 0x3f liegt.
Auch nicht ganz perfekt
Dass die oben gezeigte Formel für das Tastverhältnis einen exakt linearen Verlauf hat (auch wenn dieser leider keine Ursprungsgerade darstellt), ist unbestritten. Aber zum Dimmen eines LED-Treibers ist das aufgrund der nichtlinearen Empfindlichkeit des Auges nicht gerade vorteilhaft.
Diesen Sachverhalt kann man auch mit unserem einfachen Versuchsaufbau sehr schön beobachten, indem man vom Rechtsanschlag des Potis mit voller Helligkeit gleichmäßig nach links dreht: In der ersten Hälfte nimmt die Helligkeit sehr wenig ab (anfangs so gut wie gar nicht), um danach um so schneller abzunehmen[3].
Um die lineare Kennlinie der PWM an das Helligkeitsempfinden des Auges anzupassen, kann man die linearen Poti-Werte des A/D-Wandlers entweder per Software (am einfachsten mit einer Tabelle) geeignet für den jeweils passenden PWM-Wert umrechnen oder ein logarithmisches Poti nehmen.
Mehr Auflösung (15.4.2016)
Nimmt man statt dem 8-bit-Timer 2 (oder 0) des ATmega328 den Timer 1 für die PWM, kann man z.B. eine 4-fach höhere Auflösung bekommen.
Timer 1 erlaubt PWM-Modi mit 10 Bit Auflösung (sprich 1024 Helligkeitswerten), die beim Dimmen von LEDs normalerweise immer ausreichend sind.
Für die 10-bit-Poti-PWM habe ich dann natürlich die volle 10-bit-Auflösung des integrierten A/D-Wandlers des ATmega328 genommen, wobei das Ändern einer Bitstelle[5] mit einem "normalen" Poti damit schon reichlich Fingerspitzengefühl verlangt.
Das Programm ist dann nur unwesentlich komplexer, auch mit einer 10-bit-Luminanz-Korrektur. Die Korrekturtabelle belegt dann zwangsläufig 1024 Worte (2048 Byte) im Programmspeicher.
Soft gedimmt
Das Dimmen einer Einzel-LED ist mit 10 Bit Dimm-Auflösung ausgesprochen weich. Man sieht über den gesamten Dimm-Bereich keine Helligkeitssprünge und der 10-bit-Poti-PWM-Dimmer lässt somit keine Wünsche mehr offen.
Doppelt gedimmt 27.4.2016
Sowohl für das Dimmen mit 10 Bit Auflösung als auch für das PWM-Dimmen mit Luminanz-Korrektur gibt es hier ein Beispiel eines Crossfaders, der zwei PWM-Dimm-Kanäle mit einem einzigen AVR-Timer realisiert. In diesem speziellen Fall sind die beiden Kanäle jeweils komplementär zueinander.
Genau so gut könnten es auch zwei individuelle PWM-Kanäle sein, die dann aber jeweils ein eigenes Poti an einem der diversen A/D-Wandler-Eingänge des AVR-µC benötigen würden.
Am 15.4.2016 hatte ich ein paar überraschende Feststellungen mit der 10-bit-PWM und Timer 1 des ATmega328P gemacht, die dadurch bedingt waren, dass ich das Bit WGM12 versehendlich nicht im Register TCCR1B sondern im Register TCCR1A gesetzt hatte (reserviert, direkt neben Bit WGM11), wodurch sich statt Fast-PWM der Phase-Correct-PWM-Mode eingestellt hat (das echte WGM12-Bit ist dann auf 0).
Das ist eine kleine Tücke, wenn man ein Register z.B. mittels ldi rmp,(1<<COM1A1 | 1<<WGM11 | 1<<WGM10) lädt und bei dieser Gelegenheit auch noch das WGM12 dazu packt.
Die Ursache für den geschilderten Sachverhalt habe ich beim Experimentieren für einen Crossfader mit den Ausgängen OC1A und OC1B auf dem Scope festgestellt, deren Flanken jeweils symmetrisch zu den Pulsmitten waren und die PWM-Frequenz nur halb so groß ist, woraus man auf ein Dual-Slope-Verfahren schließen kann, das aber nicht im angedachten Fast-PWM-Mode verwendet wird.
Damit ergeben sich für den Phase-Correct-PWM-Mode bei fest eingestellter 10-bit-Periode schließlich diese Tatsachen:
Das PWM-Tastverhältnis beträgt: OCR1-Wert/1023
Bei OCR1-Werten von 0 bis 0x3ff ergibt sich gemäß dieser Formel über den gesamten Dimm-Bereich ein korrektes Tastverhältnis von 0% bis 100% (bei 10 Bit also insgesamt 1024 Helligkeitsstufen).
Bei einem OCR1-Wert von 0 werden die Pins OC1A/OC1B dauerhaft auf LOW geschaltet, so dass dieser Zustand nicht (wie bei Timer 2) per Software ausgeblendet werden muss.
Ein OCR-Wert von TOP (0xff, 0x1ff oder 0x3ff, je nach 8/9/10-bit-Mode) setzt die Pins OC1A/OC1B dauerhaft auf HIGH.
Die PWM-Frequenz ist im 10-bit-Mode mit Teilerfaktor 8 weiterhin 225 Hz (wie bei der PWM-Dimmer-Version mit Timer 2).
Für die PWM-Frequenz gilt allgemein die Datenblattformel:
fOCnxPWM = fclk_I/O/( 2 * N * (1+TOP))
Bei 10 Bit ist TOP = 1023. Als Teilerfaktor N sind leider nur die Werte N = 1, 8, 64, 256 oder 1024 möglich (per CS12|11|10).
Bei 225 Hz PWM-Frequenz beträgt die zeitliche Auflösung (1/225 Hz)/1024 = 4,34 µsec. D.h. bei einem OCR1-Wert von 1 wird ein kurzer Impuls von 4,34 µsec am Pin OC1A geliefert.
Damit habe ich einen LED-Treiber mit einem schnellen (!) Dimm-Pin angesteuert. Dieser kurze Impuls von 4,34 µsec bzw. 0,09% Tastverhältnis lässt die 6 LED-Chips einer mit 400mA betriebenen (etwas älteren) OStar-HEX bereits so kräftig leuchten, dass man die sechs Segmente beim Fotografieren wegen Überbelichtung auf dem Foto nicht mehr einzeln sieht (mit dem bloßen Auge direkt betrachtet aber schon), sondern nur noch einen hellen Fleck.
Bestimmt habe ich bei meinen ursprünglichen Versuchen nur zu lange in die helle LED geschaut und das falsch gesetzte WGM12-Bit dabei übersehen ...
Weitere Versuche mit AVR Timer 1 im Phase-Correct-PWM-Mode (19.4.2016)
Mit einem Teilerfaktor 64 statt 8 (16 und 32 gibt es leider nicht) sind es im 8-bit-Phase-Correct-PWM-Mode nur 112,5 Hz PWM-Frequenz.
Falls das für einen LED-PWM-Dimmer zu wenig ist und man trotzdem nur mit 8 Bit für die PWM arbeiten möchte, bleibt man besser im 10-Bit-Mode (Mode 3) mit Teilerfaktor 8 (und somit 225 Hz PWM-Frequenz) und schreibt dann eben den 4-fachen 8-bit-Wert in das OCR1A-Register (H und L).
Ein kleiner Nachteil ist dabei, dass man bei 100% Helligkeit den 10-bit-Wert 4*0xff = 0x3fc (also 1020 statt 1023) schreibt und somit keine echten 100% PWM bekommt sondern nur 1020/1023 = 99,7%.
Diese Kleinigkeit lässt sich aber wiederum per Software korrigieren, indem man genau für diesen (und nur für diesen) Fall die beiden niederwertigsten Bits auch noch setzt. Der Unterschied zwischen 0x3fc und 0x3ff ist mit dem Auge nicht sichtbar, sondern nur mit dem Scope oder DMM messbar.
Im 9-bit-Phase-Correct-PWM-Mode (Mode 2) hätte man mit Teilerfaktor 8 schließlich 450 Hz PWM-Frequenz.
Auch hier müsste man bei 100% Helligkeit den PWM-Eintrag für echte 100% (statt 510/511 = 99,8%) Tastverhältnis korrigieren, indem man per Software genau für diesen Fall zusätzlich das niederwertigste Bit auf 1 setzt (sonst ist es immer 0).
Mit diesen Tweaks unterscheidet sich die 8-bit-PWM mit Timer 1 nicht von der 8-bit-PWM mit Timer 2 (oder 0) des AVR. Aber die Qualität der 10-Bit-PWM erreicht sie bei Weitem nicht (beide mit Luminanz-Korrektur).
Es hat mir keine Ruhe gelassen und ich habe nun auch noch eine Luminanz-Korrektur gemäß CIE[4] per Tabelle in das obige Poti-PWM-Programm mit eingebunden.
Außer der Umrechnungstabelle (weiterhin nur mit 8-bit-Werten) wurde lediglich ein zusätzliches Register PWMvalue definiert:
.def PWMvalue = r18 ; corrected value for PWM Der Rest (insbesondere die Initialisierung von ADC und PWM) ist wie oben.
Und das ist das um die Luminanz-Korrektur erweiterte Hauptprogramm des AVR-Poti-Dimmers für LED-Treiber:
ldi zl,low(2*LumiTable)
ldi zh,high(2*LumiTable)
add zl,ADCResult
adc zh,rmp
lpm PWMvalue,z ; corrected value from Luminance table
IF ADCResult < #2
sts OCR2A,PWMvalue ; corrected value for PWM ENDL ; main loop (forever) Und hier die im Code-Bereich liegende Korrekturtabelle:
LumiTable:.db 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1.db 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4.db 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7.db 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 10, 10, 11, 11.db 11, 12, 12, 12, 13, 13, 13, 14, 14, 15, 15, 15, 16, 16, 17, 17.db 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 24, 25.db 25, 26, 26, 27, 28, 28, 29, 29, 30, 31, 31, 32, 32, 33, 34, 34.db 35, 36, 37, 37, 38, 39, 39, 40, 41, 42, 43, 43, 44, 45, 46, 47.db 47, 48, 49, 50, 51, 52, 53, 54, 54, 55, 56, 57, 58, 59, 60, 61.db 62, 63, 64, 65, 66, 67, 68, 70, 71, 72, 73, 74, 75, 76, 77, 79.db 80, 81, 82, 83, 85, 86, 87, 88, 90, 91, 92, 94, 95, 96, 98, 99.db 100, 102, 103, 105, 106, 108, 109, 110.db 112, 113, 115, 116, 118, 120, 121, 123.db 124, 126, 128, 129, 131, 132, 134, 136.db 138, 139, 141, 143, 145, 146, 148, 150.db 152, 154, 155, 157, 159, 161, 163, 165.db 167, 169, 171, 173, 175, 177, 179, 181.db 183, 185, 187, 189, 191, 193, 196, 198.db 200, 202, 204, 207, 209, 211, 214, 216.db 218, 220, 223, 225, 228, 230, 232, 235.db 237, 240, 242, 245, 247, 250, 252, 255 Wie man aus der Tabelle erkennen kann, gibt es im Nullbereich nun eine ganze Menge Positionen mit dem Wert 0, die ich anfangs alle per Software ausgeblendet hatte.
Mit einer geänderten Abfrage werden in der aktuellen Version jedoch nur die beiden ersten Nullwerte (rot markiert) dunkel getastet (Poti im Linksanschlag).
Damit erreicht man, dass die folgenden Nullwerte mit den bisher unterdrückten 0,39% Tastverhältnis nun auch angezeigt werden, gefolgt von einer ganzen Reihe "1"-Werten mit 0,78% Tastverhältnis (bisher der erste leuchtende Helligkeitswert).
Die Tabellenwerte mit 1, 2 und 3 wurden gegenüber der ursprünglichen Berechnung geringfügig neu verteilt.
Mit dieser Luminanz-Korrektur ist das LED-Dimm-Gefühl per Poti um Klassen besser als mit der linearen PWM!
Man kann die ersten Helligkeitswerte etwa bis 10 (bzw. 4,3% Tastverhältnis) noch mit dem bloßen Auge unterscheiden.
[1] "Fast" für schnell, nicht für beinahe.
[2] Fehlermeldung des AVR-Assemblers bei Verwendung von OUT/IN statt STS/LDS.
[3] Mit dem analogen Poti-Dimmer ist das aufgrund dessen umgekehrter Exponential-Kennlinie nicht der Fall.
[4] Im WWW gibt es eine ganze Reihe von entsprechenden Vorschlägen und Tabellen. Meine selbst berechnete Tabelle weicht in einigen Werten geringfügig davon ab. Ich habe aber nicht weiter recherchiert, woher die Unterschiede kommen.
Es gibt darüberhinaus weitere Verfahren zur Anpassung bezüglich Helligkeitsempfinden, die dann aber bereits stark von der CIE-Tabelle abweichen.
Stichworte hierzu sind Gamma-Korrektur und Weber-Fechner, was aber genau genommen nicht dasselbe ist.
Da mir eine Bezeichnung "Korrektur bezüglich Helligkeitsempfinden des menschlichen Auges" (oder so ähnlich) etwas zu lang ist, verwende ich die kürzere Bezeichnung "Luminanz-Korrektur" (vom englischen Luminance für Leuchtdichte, die maßgebend ist für den vom Auge wahrgenommenen Helligkeitseindruck).
[5] Bei meinem Versuchsaufbau habe ich am ATmega328P derzeit ein 2x8-Digit-LCD angeschlossen, auf dem u.a. der ADC-Wert und der (korrigierte) PWM-Wert (aus Platzmangel jeweils in HEX) angezeigt wird.