© Eberhard Haug 2003-2025
Optimale Darstellung dieser Website
bei aktiviertem
"Active Scripting"
(LED-Menü-Buttons)
Hinweis:
Bei Ansicht auf Smartphones werden Zeilen umgebrochen, so dass die Strukturierung der s’AVR-Programme teilweise verloren geht.
Der kurze Weg geht hier entlang:
LED-Crossfader mit Luminanz-Korrektur (27.4.2016)
LED-Crossfader phasenverschoben für Tunable White (5.5.2019)
LED-Crossfader mit Luminanz-Korrektur (27.4.2016)
Nach LED-Lauflicht und PWM-Poti-Dimmer gibt es nun noch einen µC-basierenden LED-Crossfader als s’AVR-Programm, wenn auch hier aufgrund des einfachen Programms mit nur zwei s’AVR-Strukturen.
Zum Umblenden zwischen zwei LED-Treibern dient wiederum ein Poti, wobei z.B. Links-Anschlag LED-Treiber #1 100% AUS und Rechts-Anschlag LED-Treiber #1 100% EIN und LED-Treiber #2 umgekehrt.
Beide LED-Treiber benötigen einen PWM-Dimm-Eingang (meist ein Enable-Eingang), der jeweils durch einen der beiden PWM-Ausgänge des AVR-µC angesteuert wird.
Beide PWM-Kanäle werden in diesem Beispiel mit Timer 1 des AVR-µC und für superweiches Umblenden mit 10 Bit Auflösung realisiert. Die PWM-Frequenz beträgt 225 Hz (bedingt durch 3,6864 MHz CPU-Takt).
Und so geradlinig schaut das s’AVR-Programm einschließlich der großen Tabelle für die 10-bit-Luminanz-Korrektur aus (wiederum eine Kopie aus der Atmel-Studio-Quelldatei, Assembler-Befehle sind blau markiert):
.def rmp = r16 ; rmp = temporary register (multi purpose)
.def ADCresultL = r20 ; ADC result
.def ADCresultH = r21 ; ADC result
.def PWMvalueL = r22 ; value for PWM
.def PWMvalueH = r23 ; value for PWM
clr rmp
out DDRC,rmp ; all Port C pins are inputs (default)
; ADC (using pin ADC5 at PORTC,5):
ldi rmp,(1<<REFS0 | 1<< MUX2 | 1<< MUX0) ; 0b0100_0101
sts ADMUX,rmp ; select VCC ref, ADC5
clr rmp
sts ADCSRB,rmp ; ADC free running mode (default setting)
ldi rmp,1<<ADC5D ; 0b0010_0000
sts DIDR0,rmp ; disable digital in for ADC5
ldi rmp,(1<<ADEN | 1<<ADSC | 1<<ADPS2 | 1<<ADPS0) ; 0b1100_0101
sts ADCSRA,rmp ; enable ADC, 114 kHz sampling rate
; for free running mode ADSC must be set as well!
; PWM using TC1, OC1A at PORTB,1 and OC1B at PortB,2
; (pins 15 and 16 for ATmega328P)
; 10 bit PWM, non-inverted mode:
ldi rmp,(1<<COM1A1 | 1<<COM1B1| 1<<WGM11 | 1<<WGM10)
sts TCCR1A,rmp ; WGM12/13 = 0 --> Phase Correct PWM mode
ldi rmp,(1<<CS11) ; prescaler /8 --> 225 Hz PWM @3,6864 MHz clock
sts TCCR1B,rmp
sbi DDRB,1 ; enable OC1A as output (pin 15 for ATmega328P)
sbi DDRB,2 ; enable OC1B as output (pin 16 for ATmega328P)
cbi PORTB,1 ; prepare Port B1 for LOW if set as input
cbi PORTB,2 ; prepare Port B2 for LOW if set as input
LOOP ; main loop
lds rmp,ADCSRA ; read ADCSRA
sbr rmp,1<<ADSC ; set start ADC bit
sts ADCSRA,rmp ; write back ADCSRA
REPEAT
lds rmp,ADCSRA ; check ADCSRA
UNTIL !rmp,ADSC ; ADSC cleared after ADC ready
lds ADCresultL,ADCL ; get ADC low byte
lds ADCresultH,ADCH ; get ADC high byte
ldi zl,low(2*LumiTable)
ldi zh,high(2*LumiTable)
mov xl,ADCresultL ; keep original ADC copy
mov xh,ADCresultH ; keep original ADC copy
; movw xl,ADCResultL
; alternately MOVW instead of 2x MOV
lsl xl ; *2 due to word data
rol xh ; *2 due to word data
add zl,xl
adc zh,xh
lpm PWMvalueL,z+ ; get corrected value for PWM ...
lpm PWMvalueH,z ; ... from LumiTable
sts OCR1AH,PWMvalueH ; initiate PWM, high byte first
sts OCR1AL,PWMvalueL
ldi rmp,0xff ; now handle complementary PWM channel
sub rmp,ADCresultL ; get 0xffff - ADCResult for channel B
mov ADCResultL,rmp
ldi rmp,0x03 ; take only 10 bits into account
sbc rmp,ADCresultH
mov ADCresultH,rmp ; now complementary value available
ldi zl,low(2*LumiTable)
ldi zh,high(2*LumiTable)
lsl ADCresultL ; *2 due to word data
rol ADCresultH ; *2 due to word data
add zl,ADCresultL
adc zh,ADCresultH
lpm PWMvalueL,z+ ; get corrected value for PWM ...
lpm PWMvalueH,z ; ... from LumiTable
sts OCR1BH,PWMvalueH ; initiate PWM, high byte first
sts OCR1BL,PWMvalueL
ENDL ; main loop (forever)
LumiTable:
.dw 0,0,0,0,0,1,1,1,1,1,1,1,1,1,2,2
.dw 2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3
.dw 4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5
.dw 5,5,6,6,6,6,6,6,6,6,6,7,7,7,7,7
.dw 7,7,7,7,8,8,8,8,8,8,8,8,8,9,9,9
.dw 9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,11
.dw 11,11,11,11,11,11,12,12,12,12,12,12,12,12,13,13
.dw 13,13,13,13,13,14,14,14,14,14,14,14,15,15,15,15
.dw 15,15,16,16,16,16,16,16,17,17,17,17,17,17,18,18
.dw 18,18,18,18,19,19,19,19,19,19,20,20,20,20,20,21
.dw 21,21,21,21,22,22,22,22,22,23,23,23,23,23,24,24
.dw 24,24,24,25,25,25,25,26,26,26,26,26,27,27,27,27
.dw 28,28,28,28,29,29,29,29,29,30,30,30,30,31,31,31
.dw 31,32,32,32,32,33,33,33,34,34,34,34,35,35,35,35
.dw 36,36,36,37,37,37,37,38,38,38,39,39,39,39,40,40
.dw 40,41,41,41,42,42,42,42,43,43,43,44,44,44,45,45
.dw 45,46,46,46,47,47,47,48,48,48,49,49,49,50,50,50
.dw 51,51,51,52,52,52,53,53,54,54,54,55,55,55,56,56
.dw 56,57,57,58,58,58,59,59,60,60,60,61,61,61,62,62
.dw 63,63,63,64,64,65,65,66,66,66,67,67,68,68,68,69
.dw 69,70,70,71,71,72,72,72,73,73,74,74,75,75,76,76
.dw 76,77,77,78,78,79,79,80,80,81,81,82,82,83,83,84
.dw 84,85,85,86,86,86,87,87,88,89,89,90,90,91,91,92
.dw 92,93,93,94,94,95,95,96,96,97,97,98,98,99,100,100
.dw 101,101,102,102,103,103,104,105,105,106,106,107,107,108,109,109
.dw 110,110,111,111,112,113,113,114,114,115,116,116,117,118,118,119
.dw 119,120,121,121,122,122,123,124,124,125,126,126,127,128,128,129
.dw 129,130,131,131,132,133,133,134,135,135,136,137,137,138,139,140
.dw 140,141,142,142,143,144,144,145,146,146,147,148,149,149,150,151
.dw 152,152,153,154,154,155,156,157,157,158,159,160,160,161,162,163
.dw 163,164,165,166,166,167,168,169,170,170,171,172,173,173,174,175
.dw 176,177,177,178,179,180,181,182,182,183,184,185,186,186,187,188
.dw 189,190,191,192,192,193,194,195,196,197,197,198,199,200,201,202
.dw 203,204,204,205,206,207,208,209,210,211,212,213,213,214,215,216
.dw 217,218,219,220,221,222,223,224,225,225,226,227,228,229,230,231
.dw 232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247
.dw 248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263
.dw 264,265,266,268,269,270,271,272,273,274,275,276,277,278,279,280
.dw 281,283,284,285,286,287,288,289,290,291,293,294,295,296,297,298
.dw 299,300,302,303,304,305,306,307,309,310,311,312,313,314,316,317
.dw 318,319,320,322,323,324,325,326,328,329,330,331,332,334,335,336
.dw 337,339,340,341,342,344,345,346,347,349,350,351,352,354,355,356
.dw 357,359,360,361,363,364,365,367,368,369,370,372,373,374,376,377
.dw 378,380,381,382,384,385,386,388,389,391,392,393,395,396,397,399
.dw 400,402,403,404,406,407,408,410,411,413,414,416,417,418,420,421
.dw 423,424,426,427,428,430,431,433,434,436,437,439,440,442,443,445
.dw 446,448,449,451,452,454,455,457,458,460,461,463,464,466,467,469
.dw 470,472,473,475,476,478,480,481,483,484,486,487,489,491,492,494
.dw 495,497,499,500,502,503,505,507,508,510,511,513,515,516,518,520
.dw 521,523,525,526,528,530,531,533,535,536,538,540,541,543,545,546
.dw 548,550,552,553,555,557,558,560,562,564,565,567,569,571,572,574
.dw 576,578,579,581,583,585,587,588,590,592,594,596,597,599,601,603
.dw 605,606,608,610,612,614,616,617,619,621,623,625,627,629,630,632
.dw 634,636,638,640,642,644,646,647,649,651,653,655,657,659,661,663
.dw 665,667,669,671,673,674,676,678,680,682,684,686,688,690,692,694
.dw 696,698,700,702,704,706,708,710,712,714,716,719,721,723,725,727
.dw 729,731,733,735,737,739,741,743,745,748,750,752,754,756,758,760
.dw 762,764,767,769,771,773,775,777,779,782,784,786,788,790,792,795
.dw 797,799,801,803,806,808,810,812,814,817,819,821,823,826,828,830
.dw 832,835,837,839,841,844,846,848,851,853,855,857,860,862,864,867
.dw 869,871,874,876,878,881,883,885,888,890,892,895,897,899,902,904
.dw 907,909,911,914,916,919,921,923,926,928,931,933,936,938,940,943
.dw 945,948,950,953,955,958,960,963,965,968,970,973,975,978,980,983
.dw 985,988,990,993,995,998,1000,1003
.dw 1005,1008,1011,1013,1016,1018,1021,1023
Einzig die Include-Zeile für den jeweiligen AVR-µC (hier wiederum ein ATmega328P), die Interrupt-Sprungtabelle und die Stack-Initialisierung sind nicht im Quellprogramm dargestellt und müssen je nach verwendetem AVR-µC entsprechend ergänzt werden.
Komplement
Im vorliegenden Beispiel wird die Helligkeit zwischen links und rechts linear aufgeteilt und zwar so, dass die Summe immer 100% ist. D.h. wenn beim linken Kanal 0% eingestellt ist (also komplett dunkel), ergeben sich für den rechten Kanal 100% (also maximal hell).
Bei 10-bit-Auflösung des A/D-Wandlers ergibt sich folgende Formel:
Helligkeit_rechts = 0x3ff - Helligkeit_links
Aus den beiden Werten Helligkeit_links und Helligkeit_rechts wird das PWM-Tastverhältnis (also der LED-Strom) für beide Kanäle getrennt gemäß Luminanz-Kennlinie per Software bestimmt.
D.h., dass bei 50% Helligkeit beide Kanäle für einigermaßen richtiges Helligkeitsempfinden nur ca. 18,35% PWM-Tastverhältnis bzw. LED-Strom haben.
Screen-Shots
Hier ein paar Oszillogramme, zunächst beide Kanäle gleich hell eingestellt (Poti in Mittenstellung, Kanal #1 = 0x1ff, Kanal #2 = 0x200), wodurch sich ca. 18,35% PWM-Tastverhältnis ergeben:
Hier Kanal #1 mit einem Helligkeitswert von 0xff (bzw. 4,4% LED-Strom) und Kanal #2 = 0x300:
Und hier noch die entgegengesetzte Einstellung, nämlich Kanal # 1 = 0x2ff (bzw. 48,3% LED-Strom) und Kanal #2 = 0x100:
Bei Poti-Anschlag links oder rechts ist einer der beiden LED-Treiber 100% ein und der andere 100% aus, so wie es sein soll.
Enable oder Shutdown
Falls die beiden mittels PWM angesteuerten LED-Treiber statt einem Enable-Eingang einen Shutdown-Eingang haben, muss man die PWM des µC per TCCR1A eben statt "non-inverting" als "inverting" konfigurieren. Der Rest des Programms bleibt unverändert.
Statt Poti
Dank µC ist man bezüglich Bedienelement natürlich sehr flexibel.
So könnte man zum Umblenden statt einem Potenziometer auch zwei entprellte Taster "Umblenden nach rechts" bzw. "Umblenden nach links" nehmen, oder gleich einen Mini-Joy-Stick oder Dreh-Encoder, mit deren Taster man den jeweils eingestellten Wert bei Bedarf in das EEPROM des µC abspeichert, damit die gewünschte Einstellung beim nächsten Einschalten erhalten bleibt (es sei denn, man wünscht eine beliebige andere Voreinstellung beim Einschalten).
Das Abspeichern der aktuellen Einstellung kann man sich bei Verwendung eines Potis natürlich ersparen.
Tunable White
Anstatt zwischen zwei gleichfarbigen LEDs zu faden, dürfen es natürlich auch LEDs mit unterschiedlichen Farben sein - jeweils per eigenem LED-Treiber mit PWM- bzw. Enable-Eingang versorgt, so dass man die beiden LED-Fader-Ausgänge dort anschließen kann.
Und sind es weiße LEDs mit unterschiedlicher Farbtemperatur (also warmweiß und kaltweiß), hat man auch schon das nahezu perfekte "Tunable White".
LED-Crossfader phasenverschoben für Tunable White (5.5.2019)
Wie in obigen Screen-Shots für den LED-Crossfader ersichtlich, liegen die Mitten der beiden PWM-Ausgänge aufgrund des verwendeten Phase-Correct-Modes prinzipbedingt exakt übereinander, was im Normalfall bei der Ansteuerung zweier unabhängigen LED-Treiber nicht relevant ist.
Bei "Tunable White" gibt es vereinzelt antiparallel geschaltete weiße LEDs, die per zwei Adern und abwechselnder Polarität versorgt werden.
Das bedeutet, dass man dann zur Ansteuerung einen LED-Treiber benötigt, der entweder als Vollbrücke oder aus zwei Halbbrücken aufgebaut ist. Wird damit schlicht die Versorgungsspannung geschaltet bzw. umgepolt, benötigen die angeschlossenen LEDs bzw. der LED-Strang natürlich einen Vorwiderstand.
Und genau für eine solche Ansteuerung werden zwei um 180° gegeneinander phasenverschobene[1] PWM-Signale benötigt.
Für den zweiten PWM-Port lässt sich dies in obigem Programm mit Luminanz-Korrektur einfach durch zwei kleine Eingriffe erreichen, nämlich zum einen durch eine Negierung des zweiten PWM-Ausgangs (Teil der PWM-Initialisierung) und zum anderen einer Differenzbildung des über LumiTable gefundenen PWM-Tastverhältnisses zum 100%-Wert (sprich zu 0x3FF bei 10 Bit Auflösung) des zweiten PWM-Ausgangs.
Die beiden PWM-Signale schauen für unterschiedliche Tastverhältnisse dann wie folgt aus, zunächst bei Poti in Mittenstellung, Kanal #1 = 0x1ff, Kanal #2 = 0x200:
Dann Kanal #1 mit einem Helligkeitswert von 0xff (bzw. Poti bei 25% mit 4,4% LED-Strom) und Kanal #2 = 0x300:
Und schließlich Kanal # 1 = 0x2ff (bzw. Poti bei 75% und 48,3% LED-Strom) und Kanal #2 = 0x100:
Bei Links- bzw. Rechtsanschlag des Potis ist natürlich jeweils der eine Kanal vollständig AUS und der andere vollständig EIN.
Dem aufmerksamen Betrachter fällt auf, dass die LOW-Pegel der beiden Signale immer dann geringfügig angehoben sind (und das Rauschen etwas geringer wird), wenn das jeweils andere Signal HIGH-Pegel hat.
Das liegt daran, dass bei meinem Testaufbau keine separate H-Brücke verwendet wurde, sondern die antiparallelen LEDs nebst Vorwiderstand einfach zwischen den beiden Port-Pins angeschlossen sind, was ja auch einer Brückenschaltung entspricht.
Der kleine Spannungssprung ist durch den Spannungsabfall in den Ausgangstransistoren des ATmega328P auf der GND-Seite durch den (relativ kleinen) LED-Strom bedingt.
Da ich für diese Messungen den ATmega328P mit 16 MHz getaktet habe, wurden auch die Einstellungen für den ADC- und PWM-Vorteiler angepasst, wodurch sich eine PWM-Frequenz von 122 Hz ergibt.
Deshalb hier das vollständige s’AVR-Quellprogramm (wiederum ohne Interrupt-Sprungtabelle und Stack-Initialisierung, die wesentlichen Unterschiede sind rot markiert):
.def rmp = r16 ; multi purpose register
.def ADCresultL = r20 ; ADC result
.def ADCresultH = r21 ; ADC result
.def PWMvalueL = r22 ; value for PWM
.def PWMvalueH = r23 ; value for PWM
clr rmp
out DDRC,rmp ; all Port C pins are inputs (default)
ldi rmp,(1<<REFS0 | 1<< MUX2 | 1<< MUX0) ; ADC (using pin ADC5 at PORTC,5)
sts ADMUX,rmp ; select VCC ref, ADC5
clr rmp
sts ADCSRB,rmp ; ADC free running mode (default setting)
ldi rmp,1<<ADC5D ; 0b0010_0000
sts DIDR0,rmp ; disable digital in for ADC5
ldi rmp,(1<<ADEN | 1<<ADSC | 1<<ADPS2 | 1<<ADPS1 | 1<<ADPS0) ; 0b1100_0111
; enable ADC, prescaler /128 for 125 kHz sampling rate @ 16 MHz,
; for free running mode ADSC must be set as well!
sts ADCSRA,rmp
; PWM using TC1 at pin 15 and pin 16 (OC1A at PORTB,1 and OC1B at PORTB,2):
; non-inverted mode port A, inverting mode port B, 10 bit PWM
ldi rmp,(1<<COM1A1 | 1<<COM1B1 | 1<<COM1B0 | 1<<WGM11 | 1<<WGM10)
sts TCCR1A,rmp
; WGM12/13 = 0 should be Phase Correct PWM mode,
; prescaler /64 results in 244 Hz/2 = 122 Hz PWM @ 16 MHz
ldi rmp,(1<<CS11 | 1<<CS10)
sts TCCR1B,rmp
sbi DDRB,1 ; enable OC1A as output (Port B1, pin 15)
sbi DDRB,2 ; enable OC1B as output (Port B2, pin 16)
cbi PORTB,1 ; prepare Port B1 for LOW if set as input
cbi PORTB,2 ; prepare Port B2 for LOW if set as input
LOOP ; main loop (LOOP is not an address!)
lds rmp,ADCSRA ; read ADCSRA
sbr rmp,1<<ADSC ; set start ADC bit
sts ADCSRA,rmp ; write back ADCSRA
REPEAT
lds rmp,ADCSRA ; check ADCSRA
UNTIL !rmp,ADSC ; ADSC cleared after ADC ready
lds ADCResultL,ADCL ; get ADC low byte
lds ADCResultH,ADCH ; get ADC high byte
ldi zl,low(2*LumiTable)
ldi zh,high(2*LumiTable)
movw xl,ADCResultL ; MOVW instead of 2x MOV
lsl xl ; *2 due to word data
rol xh ; *2 due to word data
add zl,xl
adc zh,xh
lpm PWMvalueL,z+ ; get corrected value for PWM ...
lpm PWMvalueH,z ; ... from LumiTable
sts OCR1AH,PWMvalueH ; initiate PWM, high byte first
sts OCR1AL,PWMvalueL
ldi rmp,0xff ; now handle complementary PWM channel
sub rmp,ADCResultL ; get 0xffff - ADCResult for channel B
mov ADCResultL,rmp
ldi rmp,0x03 ; take only 10 bits into account
sbc rmp,ADCResultH
mov ADCResultH,rmp ; now complementary value available
ldi zl,low(2*LumiTable)
ldi zh,high(2*LumiTable)
lsl ADCResultL ; *2 due to word data
rol ADCResultH ; *2 due to word data
add zl,ADCResultL
adc zh,ADCResultH
lpm PWMvalueL,z+ ; get corrected value for PWM ...
lpm PWMvalueH,z ; ... from LumiTable
; Make the 10-bit complement of calculated Port B PWM value
; to get 180° phase shift against Port A:
ldi rmp,0xff ; now handle complementary PWM channel
sub rmp,PWMvalueL ; get 0xffff - ADCResult for channel B
mov PWMvalueL,rmp
ldi rmp,0x03 ; take only 10 bits into account
sbc rmp,PWMvalueH
mov PWMvalueH,rmp ; now complementary value available
; finally store PWM values for Port B:
sts OCR1BH,PWMvalueH ; initiate PWM, high byte first
sts OCR1BL,PWMvalueL
rcall Delay10ms ; insert a short delay if no additional code follows
ENDL ; main loop (forever)
Falls jemand das Programm in reinem AVR-Assembler schreiben will, hier ein Ausschnitt der Hauptschleife (LOOP bis ENDL) mit dem von s’AVR erzeugten flachen ASM-Code (rot markiert):
;01// LOOP ; main loop (LOOP is not an address!)
_L1:
lds rmp,ADCSRA ; read ADCSRA
sbr rmp,1<<ADSC ; set start ADC bit
sts ADCSRA,rmp ; write back ADCSRA
;02// REPEAT
_L4:
lds rmp,ADCSRA ; check ADCSRA
;02// UNTIL !rmp,ADSC ; ADSC cleared after ADC ready
SBRC rmp,ADSC
RJMP _L4
lds ADCResultL,ADCL ; get ADC low byte
lds ADCResultH,ADCH ; get ADC high byte
.
.
.
rcall Delay10ms
;01// ENDL ; main loop (forever)
RJMP _L1
Der Rest ist identisch mit dem s’AVR-Quellprogramm, einschließlich der großen Tabelle für die Luminanz-Korrektur (LumiTable) wie im Programm ganz oben.
Weniger LED-Spitzenstrom
Ein schöner Nebeneffekt der phasenverschobenen PWM-Signale ist der halbe LED-Spitzenstrom im Vergleich zu den gleichphasigen PWM-Signalen. Der mittlere LED-Strom ist natürlich in beiden Fällen derselbe.
Und selbstverständlich kann man mit den phasenverschobenen PWM-Signalen genau so gut zwei einzelne LED-Treiber ansteuern.
[1] Zumindest dürfen die beiden PWM-Signale nicht gleichzeitig aktiv sein.