Eine beliebte Anwendung der Atmel-Mikrocontroller ATmega bzw. ATtiny ist das Ansteuern und Dimmen von Leuchtdioden. Aufkommende „Vollfarb-LEDs“ (ich würde sie eher RGB-LEDs nennen) ermöglichen das Erstellen von beliebigen Mischfarben (für's Auge, Brillianz nicht immer perfekt) und einstellbarer Helligkeit.
8-Bit-PWM-Kanäle sind für ruckelfreie Animationsprogramme zu grob gestuft, und die meisten Atmel-Controller haben nur zwei 16-Bit-PWM-Kanäle, leider. Klar, dass für diese Anwendung D/A-Wandler Overkill und Energieverschwendung sind, daher die Pulsweitenmodulation (=PWM). Der Artikel zeigt, wie man einen 8-Bit-PWM-Kanal für eine echte 16-Bit-PWM verwenden kann, und das bei nur moderaten Anforderungen an Rechenleistung und Interruptlatenz.
Im folgenden beziehe ich mich auf ATmega8.
Die Ausführungen sind auch auf die Typen ATmega48, ATmega88, ATmega168, ATmega16 und größer
sowie ATtiny24, ATtiny44, ATtiny84 und ATtiny2313 anwendbar.
Diesen Mikrocontrollern gemeinsam ist ein 16-bit-Timer1 mit zwei
echten 16-bit-PWM-Kanälen
und (mindestens) 1 weiterer 8-bit-Timer (Timer0 oder Timer2) mit (mindestens)
einem 8-bit-PWM-Kanal.
Mikrocontroller | Gesamtanzahl realisierbarer 16-bit-PWM-Kanäle |
---|---|
ATtiny24, ATtiny44, ATtiny84, ATtiny2313 | 4 (2+2) (lies: 2 Hardware plus 2 Software) |
ATtiny25, ATtiny45, ATtiny85 | 4 (0+2+2) |
ATtiny13, ATtiny26 | 2 (0+2) |
ATtiny261, ATtiny461, ATtiny861 | 4 (0+4) |
ATtiny11, ATtiny12 | 0 – kein PWM-Ausgang |
ATtiny10, ATtiny4/5/9 | 2 (2+0) – kein 8-Bit-Timer |
ATmega48, ATmega88, ATmega168 | 6 (2+2+2) |
ATmega8 | 3 (2+1) |
ATmega16, ATmega32 | 4 (2+1+1) |
ATmega64 | 8 (6+2) |
8051 | PIC | AVR | C166 |
---|---|---|---|
- | - | LED-PWM.zip | - |
Die Implementierung von 16-Bit-PWM auf einem 8-Bit-Timer erfordert:
Vom Timer wird nur der Überlauf-Interrupt benötigt. Der Output-Compare-Interrupt wird nicht benötigt. Ein 8-Bit-Timer darf mehrere PWM-Ausgänge bedienen, sofern vorhanden. Etwa bei den ATtiny sowie ATmegaX8.
Das Programm fährt die Helligkeit der 3 LEDs augenscheinlich linear (also exponentiell!) in insgesamt 3 Phasen rauf und runter.
.nolist .include "m8def.inc" .include "makros.i90" .listDer Quelltext ist für avrasm2 von Atmel, nicht für avr-gcc. Die selbst erstellte Makrosammlung „makros.i90“ erspart Tipparbeit.
// Port B // PB0 (14) SLEEP-Anzeige // PB1 OC1A (15) // PB2 OC1B (16) PWM-Ausgänge für LEDs // PB3 OC2 (17)Hier sind die 3 Leuchtdioden am ATmega8 (DIL, Durchsteck) anzuschließen. Mit Vorwiderstand nach Masse — oder über einen Strom verstärkenden Transistor.
// Register R8..R15 sowie R23 für ISR erforderlich .def ZERO =r8 // immer 0x00 .def ONES =r9 // immer 0xFF .def LEN2L =r10 // gepuffert (Pulslänge, von außen zu setzen) .def LEN2H =r11 .def isrt1 =r12 // ISR-Register (SREG-Backup) .def TCNT2H =r13 // High-Teil Zählerstand .def OCR2L =r14 // ungepuffert .def OCR2H =r15 .def r =r4 // Rote LED-Helligkeit, 0=AUS, 255=voll, logarithmisch skaliert .def g =r5 // Grün .def b =r6 // Blau .def isrt0 =r23 // ISR-Register, muss ≥ R16 seinDas Beispielprogramm verwendet eine Menge Register für die ISRs. Das ist wegen der Geschwindigkeit auch erforderlich! Der ATmega8 läuft mit 8 MHz. Für die Animation als solche werden nur 8-bit-Zahlen r, g und b gezählt.
.org 0 rjmp Init .org OVF2addr // Timer2-Überlauf-ISR (darf sich [255 Takte minus CLI-Zeit] Zeit gönnen) // Erzeugt CPU-Last, da Aufruf alle 256 Takte (32 µs @ F_CPU == 8 MHz)! in isrt1,SREG inc TCNT2H // High-Teil in Software brne t2a movw OCR2H:OCR2L,LEN2H:LEN2L // Bei Null Wert übernehmen (Doppel-Puffer) in isrt0,TCCR2 sbr isrt0,1<<WGM21|1<<WGM20 // PWM EIN: WGM=3 out TCCR2,isrt0 out OCR2,ONES // startet erst wenn TCNT2H == 1 t2a: cp OCR2H,TCNT2H brne t2b out OCR2,OCR2L // LOW-Zeit setzen t2b: mov isrt0,OCR2H inc isrt0 // Sonderfall OCR2H == 0xFF? breq t2c cp isrt0,TCNT2H // Wenn TCNT2H == OCR2H+1 ... brne t2c in isrt0,TCCR2 cbr isrt0,1<<WGM21|1<<WGM20 // PWM AUS: WGM=0 out TCCR2,isrt0 // (COM2 bleibt 2! Löscht OC2 später) t2c: out SREG,isrt1 reti // max. 26 Takte (inkl. Aufruf + RETI)Die Interruptserviceroutine für den 8-bit-Timer2 realisiert die 16-bit-PWM als 8-bit-PWM in einer von 256 „Zählrunden“. Davor (von HIGH-Zählerstand Null ab) ist das Ausgangssignal permanent HIGH (durch Setzen von 255 in das Register OCR2), danach permanent LOW (durch Ausschalten der PWM).
WaitT1: // Wartet auf Timer1-Überlauf (bei F_CPU == 8 MHz ca. alle 8 ms) w1: sbi PORTB,0 // SLEEP-Kontrollausgabe an Pin 14 sleep // Strom sparen cbi PORTB,0 in r16,TIFR jbrc r16,2,w1 outi TIFR, 0b00000100 // EOI retDiese kleine Warteroutine wird für die Hauptschleife (Animationsprogramm) gebraucht und wartet eine volle Umrundung von 16-bit-Timer1.
Init: clr ZERO clr ONES dec ONES outihl SP,RAMEND // Stackpointer initialisieren outi TIMSK, 0b01000000 // Timer2: Interrupt aktivieren outi TCCR2, 0b01101001 // Timer2: VT1, schnelle nichtinv. PWM outi TCCR1A, 0b10100010 // Timer1: schnelle nichtinv. PWM outi TCCR1B, 0b00011001 // Timer1: VT1 outhl ICR1,ONES,ONES // Timer1: Volle 16 bit Zählumfang mov r,ZERO mov g,ZERO mov b,ONES // Start mit 100 % Blau outi DDRB, 0b00011111 // Ausgabeports outi MCUCR, 0b10000000 // SLEEP aktivieren, nur IDLE seiInitialisiert wird mit massivem Einsatz der „makros.i90“.
MainLoop: p1: // Farbübergang Blau ⇒ Rot rcall WaitT1 dec b rcall SetPwmB inc r rcall SetPwmR cp r,ONES brne p1 p2: // Farbübergang Rot ⇒ Grün rcall WaitT1 dec r rcall SetPwmR inc g rcall SetPwmG cp g,ONES brne p2 p3: // Farbübergang Grün ⇒ Blau rcall WaitT1 dec g rcall SetPwmG inc b rcall SetPwmB cp b,ONES brne p3 rjmp MainLoopDie Grafik erklärt die Funktion viel besser als jeder Text:-)
SetPwmR: mov ZL,r rcall getexp outhl OCR1A, r17,r16 in r16,TCCR1A tst r breq ppr1 sbr r16,1<<COM1A1 rjmp ppr2 ppr1: cbr r16,1<<COM1A1 ppr2: out TCCR1A,r16 ret SetPwmG: mov ZL,g rcall getexp outhl OCR1B, r17,r16 in r16,TCCR1A tst g breq ppg1 sbr r16,1<<COM1B1 rjmp ppg2 ppg1: cbr r16,1<<COM1B1 ppg2: out TCCR1A,r16 ret SetPwmB: mov ZL,b rcall getexp movw LEN2H:LEN2L,r17:r16 in r16,TCCR2 tst b breq ppb1 sbr r16,1<<COM21 rjmp ppb2 ppb1: cbr r16,1<<COM21 ppb2: out TCCR2,r16 retSo werden bei Änderung von r, g und b die PWM-Ausgänge nachgeführt.
getexp: // PE: ZL = Index 0..255 // PA: R17:R16 = logarithmischer OCR-Wert mit der Eigenschaft: // Y[ZL=0] = egal (PWM-Länge 0, ganz AUS) // Y[ZL=1] = 0 (PWM-Länge 1, minimal) // Y[ZL=255] = 65535 (PWM-Länge 65536, ganz EIN) // Die PWM des ATtiny/ATmega hat die innewohnende Eigenschaft der // Impulsausgabe: 1 Takt länger als der OCR-Wert clr ZH lsl ZL // WORD-Adresse generieren (0..510) rol ZH subi ZL,LOW(-exptab*2) // Anfangsadresse der Tabelle "addieren" sbci ZH,HIGH(-exptab*2) lpm r16,Z+ // Tabellenwert lesen lpm r17,Z+ ret exptab: .nolist .dw 0,0,1,2,3,4,5,7,8,9,10,12,13,15,16,18 .dw 19,21,22,24,26,28,30,32,34,36,38,40,42,44,47,49 .dw 52,54,57,60,63,65,68,72,75,78,81,85,89,92,96,100 .dw 104,108,113,117,122,126,131,136,141,146,152,158,163,169,175,182 .dw 188,195,202,209,216,224,232,240,248,257,265,275,284,293,303,314 .dw 324,335,346,358,369,382,394,407,420,434,448,463,478,493,509,526 .dw 543,560,578,596,616,635,655,676,698,720,743,766,790,815,841,867 .dw 894,923,951,981,1012,1044,1076,1110,1144,1180,1216,1254,1293,1333,1375,1417 .dw 1461,1506,1553,1601,1650,1701,1753,1807,1863,1920,1980,2040,2103,2168,2234,2303 .dw 2373,2446,2521,2598,2678,2760,2844,2931,3021,3113,3208,3306,3407,3511,3618,3729 .dw 3842,3959,4080,4204,4332,4464,4600,4740,4885,5033,5186,5344,5507,5674,5847,6024 .dw 6207,6396,6590,6790,6997,7209,7428,7654,7886,8125,8372,8626,8887,9157,9435,9721 .dw 10016,10319,10632,10954,11287,11629,11981,12344,12718,13104,13501,13910,14331,14765,15212,15673 .dw 16148,16637,17140,17659,18194,18745,19312,19897,20499,21120,21759,22417,23096,23795,24515,25257 .dw 26021,26808,27619,28455,29316,30203,31117,32058,33028,34027,35056,36116,37209,38334,39494,40688 .dw 41919,43186,44492,45838,47224,48652,50123,51639,53201,54809,56467,58174,59933,61745,63612,65535Um aus einem „augenscheinlich linearen“ Helligkeitswert die zugehörige PWM-Einschaltzeit zu ermitteln, ist eine Exponentialtabelle hilfreich. Diese wurde mit einem awk-Programm erstellt, und zwar derart, dass:
Der Linearisierungseffekt für's Auge ist von den verwendeten LEDs sowie der Umgebungshelligkeit abhängig. Jedenfalls sind hier kleine Helligkeiten so fein wie technisch möglich gequantelt.
Für das angegebene Beispielprogramm (je 2 Grundfarben wechseln sich ab) erscheinen die Wendungen augenscheinlich zu spitz. Ist genau so eine Anwendung geplant, ist es günstiger, die Tabelle als eine „exponentierte Sinusfunktion“ zu erstellen.
Ein Bild sagt mehr als 1000 Worte, daher hier noch eine grafische Verdeutlichung des Funktionsprinzips und der Randprobleme. Wer das Bild kopieren möchte findet die zugehörige Vektorgrafik im .ZIP-Archiv.
Die MSP430 haben viele PWM-Ausgänge (4..20) standardmäßig, allesamt mit 16 Bit.
Für LEDs genügen durchaus auch 13 Bit. Wie man es für hunderte Ausgänge bewerkstelligt ist dort (von mir) beschrieben.
Man kann beim gewohnten ATmega bleiben, und dieser ist trotz dieser Fülle noch nicht mal ganz ausgelastet.