LED-Fading

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Das Problem

Eine LED soll mittels PWM in ihrer Helligkeit gesteuert werden. Zudem möchte man sie eventuell weich aufleuchten lassen, d.h. langsam heller und dunkler machen, was (engl.) fading genannt wurde. Es zeigt sich aber recht schnell ein Problem: Wenn man eine 8-Bit-PWM linear zwischen 0..255 hochlaufen lässt, wird die LED nicht optisch linear gedimmt. Sie wird oberhalb eines bestimmten Tastverhältnisses vergleichsweise schnell hell und bleibt auch lange hell. Dieses Verhalten hat einerseits mit dem Durchbruchverhalten der Diode und andererseits mit der nichtlinearen Lichtausbeute zu tun. D.h. weder ist der Strom linear zur Spannung, noch ist das Licht linear zum Strom.

Die Theorie

Des Rätsels Lösung liegt in der Kennline des menschlichen Auges. Diese ist nichtlinear, genauer gesagt: sie ist nahezu logarithmisch. Das ermöglicht die Wahrnehmung eines sehr großen Helligkeitsbereichs, angefangen von Vollmond mit ~1/4 Lux über eine normale Schreibtischbeleuchtung mit ca. 750 Lux bis zu einem hellen Sommertag mit bis zu 100.000 Lux. Solche hochdynamischen Signale sind nur mit einer logarithmischen Kennlinie in den Griff zu kriegen, auch von Mutter Natur und Erfinder Papa.

Die Kennlinie des Auges genau betrachtet

Ein weiteres Problem ist das Verhalten des menschlichen Auges: Dies ist annähernd logarithmisch, was bereits früh durch das Weber-Fechner-Gesetz beschrieben wurde. Genauere Untersuchungen zur Gammakorrektur führten später zur Stevenschen Potenzfunktion, die die Empfindlichkeit des Auges noch etwas besser beschreibt. (s. auch Diskussionsseite). Die Unterschiede sind jedoch marginal.

Praktisch heißt das, dass wir unserem Auge vergleichsweise große physikalische Helligkeitsunterschiede präsentieren müssen, damit es diese als linearen Helligkeitsverlauf erkennt. Etwas wissenschaftlicher formuliert heißt das, wir müssen durch Verkettung der logarithmischen Kennlinie des Auges mit einer exponentiellen Kennlinie eine physiologisch lineare Helligkeitsverteilung erzielen. Des weiteren muss unter Beachtung der Diodenkennlinien die richtige Spannung eingeprägt werden.

Eine einfache Möglichkeit der praktischen Umsetzung besteht im Nutzen einer Tabelle, in welcher die Funktionen abgebildet sind. Berechnet werden kann eine passende Tabelle beispielsweise mit z.B. folgender Funktion:

[math]\displaystyle{ y=\frac{b^{\,x/r_x}-1}{b-1}\cdot r_y }[/math]

Dabei sind x und y die Ein-, bzw. Ausgabewerte der Funktion, jeweils im Bereich von 0 bis r–1. b ist die Basis der Exponentialfunktion und bestimmt, wann und wie stark die Kurve ansteigen soll. Hier ist etwas Ausprobieren erforderlich, gute Ergebnisse liefern Werte im Bereich 10–100.

Das Demoprogramm

Das folgende Beispielprogramm demonstriert die Wirkung verschiedener PWM-Auflösungen. Eine 8-Bit PWM wird mit 4/8/16 und 32 nichtlinearen Stufen betrieben, welche über eine Exponentialfunktion berechnet wurden. Dazu dient die Exceltabelle[1] - überarbeitet, inkl. neuer Varianten der Berechnung als LibreOffice Calc Sheet / PDF Vorschau[2]. Die einzelnen, benachbarten Werte haben zueinander ein konstantes Verhältnis, das in der Exceltabelle als Factor berechnet wird. Ausserdem werden eine 10-Bit PWM mit 64 Stufen sowie eine 16-Bit PWM mit 256 Stufen betrieben.

Eine neue Excel 2000 Tabelle ist ebenfalls verfügbar. Die Schaltflächen kopieren die Arraywerte in die Zwischenablage.

Das Programm ist ursprünglich auf einem AVR vom Typ ATmega32 entwickelt und getestet worden. Aber es ist leicht auf jeden AVR portierbar, welcher eine PWM zur Verfügung hat. Der AVR muss mit etwa 8 MHz getaktet werden, egal ob mit internem Oszillator oder von aussen mit Quarz. Man muss nur noch eine LED mittels Vorwiderstand von ca. 1 kΩ an Pin D5 anschliessen und los gehts. Es sollte hier noch erwähnt werden, dass das Programm mit eingeschalteter Optimierung compiliert werden muss, sonst stimmen die Zeiten der Warteschleifen aus util/delay.h nicht.

Bei Verwendung der LEDs auf dem STK500 bzw. bei der Verwendung von invertierenden Treiberstufen ist das

#define STK500 0

durch

#define STK500 1

zu ersetzen.

Das Programm durchläuft alle 6 PWMs und lässt dabei die LED jeweils 3 mal glimmen. Mit 4 Schritten Auflösung ist das natürlich ruckelig, mit 8 schon wesentlich besser. Mit 16 Stufen sieht man bei langsamen Änderungen noch Stufen, dreht man die Ein- und Ausblendzeiten runter, ist der Übergang schon recht flüssig. Die 8-Bit PWM mit 32 Stufen unterscheidet sich praktisch nicht von der 10-Bit PWM mit 64 Stufen, es sei denn, man macht extrem langsame Einblendungen. Hier schlägt die Stunde der 16-Bit PWM. Diese wird bewußt sehr langsam ausgeführt um zu demonstrieren, daß hiermit praktisch keine Stufen mehr sichtbar sind, egal wie langsam gedimmt wird. Wie man auch sieht sind die drei höherauflösenden PWMs im unteren Bereich an ihrer Auflösungsgrenze, da einige PWM-Werte mehrfach vorkommen. Da heißt gleichzeitig, daß eine Steigerung der Stufenanzahl relativ sinnlos ist.

//**************************************************************************
//*
//*  LED fading test
//*  uses exponential PWM settings to achive visual linear brightness
//*
//*  ATmega32 @ 8 MHz
//*                  
//**************************************************************************
 
#include <avr/io.h>
#include <avr/pgmspace.h>

#define F_CPU 8000000L
#include <util/delay.h>

#define STK500 0

#if STK500
// inverted PWM on OC1A for STK500
#define INVERT_PWM (1 << COM1A0)
#else
// non-inverted PWM on OC1A
#define INVERT_PWM 0
#endif // STK500

const uint16_t pwmtable_8A[4]  PROGMEM = { 0, 16, 64, 255 };
const uint16_t pwmtable_8B[8]  PROGMEM =
{
    0, 4, 8, 16, 32, 64, 128, 255
};

const uint16_t pwmtable_8C[16] PROGMEM =
{
    0, 2, 3, 4, 6, 8, 11, 16, 23, 32, 45, 64, 90, 128, 181, 255
};

const uint16_t pwmtable_8D[32] PROGMEM =
{
    0, 1, 2, 2, 2, 3, 3, 4, 5, 6, 7, 8, 10, 11, 13, 16, 19, 23,
    27, 32, 38, 45, 54, 64, 76, 91, 108, 128, 152, 181, 215, 255
};
 
const uint16_t pwmtable_10[64] PROGMEM =
{
    0, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 6, 6, 7, 8, 9, 10,
    11, 12, 13, 15, 17, 19, 21, 23, 26, 29, 32, 36, 40, 44, 49, 55,
    61, 68, 76, 85, 94, 105, 117, 131, 146, 162, 181, 202, 225, 250,
    279, 311, 346, 386, 430, 479, 534, 595, 663, 739, 824, 918, 1023
};
 
const uint16_t pwmtable_16[256] PROGMEM =
{
    0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3,
    3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 7,
    7, 7, 8, 8, 8, 9, 9, 10, 10, 10, 11, 11, 12, 12, 13, 13, 14, 15,
    15, 16, 17, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
    31, 32, 33, 35, 36, 38, 40, 41, 43, 45, 47, 49, 52, 54, 56, 59,
    61, 64, 67, 70, 73, 76, 79, 83, 87, 91, 95, 99, 103, 108, 112,
    117, 123, 128, 134, 140, 146, 152, 159, 166, 173, 181, 189, 197,
    206, 215, 225, 235, 245, 256, 267, 279, 292, 304, 318, 332, 347,
    362, 378, 395, 412, 431, 450, 470, 490, 512, 535, 558, 583, 609,
    636, 664, 693, 724, 756, 790, 825, 861, 899, 939, 981, 1024, 1069,
    1117, 1166, 1218, 1272, 1328, 1387, 1448, 1512, 1579, 1649, 1722,
    1798, 1878, 1961, 2048, 2139, 2233, 2332, 2435, 2543, 2656, 2773,
    2896, 3025, 3158, 3298, 3444, 3597, 3756, 3922, 4096, 4277, 4467,
    4664, 4871, 5087, 5312, 5547, 5793, 6049, 6317, 6596, 6889, 7194,
    7512, 7845, 8192, 8555, 8933, 9329, 9742, 10173, 10624, 11094,
    11585, 12098, 12634, 13193, 13777, 14387, 15024, 15689, 16384,
    17109, 17867, 18658, 19484, 20346, 21247, 22188, 23170, 24196,
    25267, 26386, 27554, 28774, 30048, 31378, 32768, 34218, 35733,
    37315, 38967, 40693, 42494, 44376, 46340, 48392, 50534, 52772,
    55108, 57548, 60096, 62757, 65535
};

/*

  Diese Tabellen sind nicht nach der Theorie (s. oben) berechnet, sondern wie folgt:
  a = Anzahl an Schritte (4, 8, 16, 32, 64, 256)
  b = Auflösung des PWM's (256, 1024, 65536)
  y = Errechneter Wert an einer stelle x
      y = 0 wenn x = 0
      y = pow(2, log2(b-1) * (x+1) / a) wenn x > 0

  Gerne wird auch diese alternative Formel genutzt:
      y = pow(2, log2(b) * (x+1) / a) - 1

  Obige Funktionen wurden in den LibreOffice Calc Sheet eingearbeitet.
*/
 
// long, variable delays
 
void my_delay (uint16_t milliseconds)
{
    for (; milliseconds > 0; milliseconds--)
        _delay_ms (1);
}


void pwm_up_down (const uint16_t pwm_table[], int16_t size, uint16_t delay)
{
    int16_t tmp;

    for (tmp = 0; tmp < size; tmp++)
    {
        OCR1A = pgm_read_word (& pwm_table[tmp]);
        my_delay (delay);
    }
 
    for (tmp = size-1; tmp >= 0; tmp--)
    {
        OCR1A = pgm_read_word (& pwm_table[tmp]);
        my_delay (delay);
    }
}
 
// 8-Bit PWM with only 4 different settings
 
void pwm_8_4 (uint16_t delay)
{
    // 8 Bit Fast PWM
    TCCR1A = (1 << COM1A1) | (1 << COM1B0) | INVERT_PWM;
    // prescaler 256 -> ~122 Hz PWM frequency
    TCCR1B = (1 << WGM12) | (1 << CS12) | (0 << CS11) | (0 << CS10);
    
    pwm_up_down (pwmtable_8A, 4, delay);
}
 
// 8-Bit PWM with 8 different settings
 
void pwm_8_8 (uint16_t delay)
{
    // 8 Bit Fast PWM
    TCCR1A = (1 << COM1A1) | (1 << COM1B0) | INVERT_PWM;
    // prescaler 256 -> ~122 Hz PWM frequency
    TCCR1B = (1 << WGM12) | (1 << CS12) | (0 << CS11) | (0 << CS10);
 
    pwm_up_down (pwmtable_8B, 8, delay);
}
 
// 8-Bit PWM with 16 different settings
 
void pwm_8_16 (uint16_t delay)
{
    // 8 Bit Fast PWM
    TCCR1A = (1 << COM1A1) | (1 << COM1B0) | INVERT_PWM;
    // prescaler 256 -> ~122 Hz PWM frequency
    TCCR1B = (1 << WGM12) | (1 << CS12) | (0 << CS11) | (0 << CS10);
 
    pwm_up_down (pwmtable_8C, 16, delay);
}
 
// 8-Bit PWM with 32 different settings
 
void pwm_8_32 (uint16_t delay)
{
    //  8 Bit Fast PWM
    TCCR1A = (1 << COM1A1) | (1 << COM1B0) | INVERT_PWM;
    // prescaler 256 -> ~122 Hz PWM frequency
    TCCR1B = (1 << WGM12) | (1 << CS12) | (0 << CS11) | (0 << CS10);
 
    pwm_up_down (pwmtable_8D, 32, delay);
}
 
// 10-Bit PWM with 64 different settings
 
void pwm_10_64 (uint16_t delay)
{ 
    // 10 Bit Fast PWM
    TCCR1A = (1 << COM1A1) | (1 << COM1B1) | (1 << COM1B0) | INVERT_PWM;
    // prescaler 64 -> ~122 Hz PWM frequency
    TCCR1B = (1 << WGM12) | (0 << CS12) | (1 << CS11) | (1 << CS10);
 
    pwm_up_down (pwmtable_10, 64, delay);
}
 
// 16-Bit PWM with 256 different settings
 
void pwm_16_256 (uint16_t delay)
{
    // 16 Bit Fast PWM
    TCCR1A = (1 << COM1A1) | (1 << COM1B1) | INVERT_PWM;
    // stop timer
    TCCR1B = 0;
    // TOP for PWM, full 16 Bit
    ICR1 = 0xFFFF;
    // prescaler 1 -> ~122 Hz PWM frequency
    TCCR1B = (1 << WGM12) | (1 << WGM13) | (0 << CS12) | (0 << CS11) | (1 << CS10);
 
    pwm_up_down (pwmtable_16, 256, delay);
}
 
int main (void)
{
    int8_t i;
    // delay in milliseconds for one fading step
    int16_t step_time = 400;
 
    // LED uses OC1A
    DDRD |= 1 << PD5;
 
    // test all fading routines
 
    while (1)
    {
        for (i=0; i<3; i++) pwm_8_4 (step_time);
        my_delay (1000);
        
        for (i=0; i<3; i++) pwm_8_8 (step_time/2);
        my_delay (1000);    
        
        for (i=0; i<3; i++) pwm_8_16 (step_time/4);
        my_delay (1000);
        
        for (i=0; i<3; i++) pwm_8_32 (step_time/8);
        my_delay (1000);
        
        for (i=0; i<3; i++) pwm_10_64 (step_time/16);
        my_delay (1000);
        
        for (i=0; i<3; i++) pwm_16_256 (step_time/16);
        my_delay (1000);
    }
 
    return 0;
}

FAQ

Wieso geht die LED nie ganz aus?
Es ist normal, dass die LED selbst bei OCR1A = 0 immer noch ganz schwach leuchtet. Die Hardware-PWM funktioniert so, dass bei einem Timerwert von 0 auf jeden Fall der Ausgang eingeschaltet wird. Danach kommt der Compare Match bei 0 und schaltet gleich wieder aus. Daher ist der Ausgang für einen PWM-Takt eingeschaltet. Um das zu ändern, muss man entweder invertierte PWM nutzen, dann ist allerdings der Ausgang nie zu 100% High, sondern hat immer einen Takt Low beim maximalem PWM-Wert. Oder man schaltet bei 0 einfach die PWM-Funktion ab und setzt den Ausgang normal auf Low. [1].
Wieso dimmt man eine LED nicht besser mit einer variablen Stromquelle?
Nur so ist es möglich, die LEDs von nahezu 0 bis 100% zu dimmen, ohne dass es zu Farbänderungen kommt, was besonders bei RGB-Anwendungen wichtig ist.
Wie stimmen die Werte im LibreOffice Calc Sheet mit denen im Programm überein?
Basis = (Max/Factor)^(1/(Steps-1))
pwmtable_xx[0] = 0
pwmtable_xx[Index] = Round(Factor * Basis^Index); for Index=1..Steps-1

pwmtable_8A - Max=255,   Steps=4,   Factor=4
pwmtable_8B - Max=255,   Steps=8,   Factor=2
pwmtable_8C - Max=255,   Steps=16,   Factor=1.435
pwmtable_8D - Max=255,   Steps=32,  Factor=1.21
pwmtable_10 - Max=1023,  Steps=64,  Factor=1.118
pwmtable_16 - Max=65535, Steps=256, Factor=1.04427

Anmerkung 1: Dies ist nur ein Reverse-Engineering, es ist derzeit nicht bekannt, warum der jeweilige Faktor so gewählt wurde wie oben angeführt.
Anmerkung 1.1: Mit der gefitteten Funktion Faktor(Steps) = 0,983118 + 18,061952 / ( 1+ (LOG10(steps)/0,3515028)^2,985948 ) lassen sich die Faktor-Werte für beliebige Schrittzahlen ausreichend genau berechnen, auch wenn sie keinen zusätzlichen Erklärungsgehalt bietet.
Anmerkung 2: Für das LibreOffice Calc Sheet siehe oben Das Demoprogramm.
Anmerkung 3: Der für die Tabelle benötigte EEPROM-Speicher entspricht log_2(MAX+1)*steps bit.

Fußnoten

  1. Anmerkung: Bitte die Exceltabelle nochmal erklären, die Werte in der Tabelle stimmen nicht mit denen im Programm überein
  2. Download alternativ von ftp://ftp.fl.priv.at/pub/mikrocontroller.net/led-fading/

Siehe auch