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.
Der Quelltext ist für avrasm2 von Atmel, nicht für avr-gcc. Die selbst erstellte Makrosammlung „makros.i90“ erspart Tipparbeit..nolist .include "m8def.inc".include "makros.i90".list
Hier sind die 3 Leuchtdioden am ATmega8 (DIL, Durchsteck) anzuschließen. Mit Vorwiderstand nach Masse — oder über einen Strom verstärkenden Transistor.// Port B // PB0 (14) SLEEP-Anzeige // PB1 OC1A (15) // PB2 OC1B (16) PWM-Ausgänge für LEDs // PB3 OC2 (17)
Das 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.// Register R8..R15 sowie R23 für ISR erforderlich .def ZERO =r8 // immer0x00 .def ONES =r9 // immer0xFF .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 sein
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)..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=3out TCCR2,isrt0 out OCR2,ONES // startet erst wenn TCNT2H == 1 t2a : cp OCR2H,TCNT2Hbrne t2b out OCR2,OCR2L // LOW-Zeit setzen t2b : mov isrt0,OCR2Hinc 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=0out TCCR2,isrt0 // (COM2 bleibt 2! Löscht OC2 später) t2c : out SREG,isrt1reti // max. 26 Takte (inkl. Aufruf + RETI)
Diese kleine Warteroutine wird fĂĽr die Hauptschleife (Animationsprogramm) gebraucht und wartet eine volle Umrundung von 16-bit-Timer1.WaitT1 : // Wartet auf Timer1-Ăśberlauf (bei F_CPU == 8 MHz ca. alle 8 ms)w1 : sbi PORTB,0 // SLEEP-Kontrollausgabe an Pin 14sleep // Strom sparen cbi PORTB, 0 in r16,TIFR jbrc r16, 2 ,w1outi TIFR, 0b00000100 // EOIret
Initialisiert wird mit massivem Einsatz der „makros.i90“.Init : clr ZEROclr ONES dec ONES outihl SP,RAMEND // Stackpointer initialisieren outi TIMSK, 0b01000000 // Timer2: Interrupt aktivierenouti TCCR2, 0b01101001 // Timer2: VT1, schnelle nichtinv. PWMouti TCCR1A, 0b10100010 // Timer1: schnelle nichtinv. PWMouti TCCR1B, 0b00011001 // Timer1: VT1outhl 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 // Ausgabeportsouti MCUCR, 0b10000000 // SLEEP aktivieren, nur IDLEsei
Die Grafik erklärt die Funktion viel besser als jeder Text:-)MainLoop :p1 : // Farbübergang Blau ⇒ Rotrcall WaitT1 dec b rcall SetPwmB inc r rcall SetPwmR cp r,ONES brne p1 p2 : // Farbübergang Rot ⇒ Grünrcall WaitT1 dec r rcall SetPwmR inc g rcall SetPwmG cp g,ONES brne p2 p3 : // Farbübergang Grün ⇒ Blaurcall WaitT1 dec g rcall SetPwmG inc b rcall SetPwmB cp b,ONES brne p3 rjmp MainLoop
So werden bei Änderung von r, g und b die PWM-Ausgänge nachgeführt.SetPwmR :mov ZL,r rcall getexp outhl OCR1A, r17,r16 in r16,TCCR1A tst r breq ppr1 sbr r16, 1 <<COM1A1rjmp ppr2 ppr1 : cbr r16,1 <<COM1A1ppr2 : out TCCR1A,r16ret SetPwmG :mov ZL,g rcall getexp outhl OCR1B, r17,r16 in r16,TCCR1A tst g breq ppg1 sbr r16, 1 <<COM1B1rjmp ppg2 ppg1 : cbr r16,1 <<COM1B1ppg2 : out TCCR1A,r16ret SetPwmB :mov ZL,b rcall getexp movw LEN2H :LEN2L,r17 :r16in r16,TCCR2 tst b breq ppb1 sbr r16, 1 <<COM21rjmp ppb2 ppb1 : cbr r16,1 <<COM21ppb2 : out TCCR2,r16ret
Um 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: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,65535
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.