LED Dimmer

Aus Trimension Wiki
Wechseln zu:Navigation, Suche
Pwm dimmer aufbau.jpg

LED Dimmer mit natürlichem Steuerungsverhalten

Die Helligkeit von LED’s läßt sich im Gegensatz zu Glühlampen nicht durch die Reduzierung der Spannung steuern. Lediglich in begrenztem Umfang ist eine Helligkeitssteuerung über den Strom machbar. Für die Steuerung der Helligkeit von LED’s wird daher das Pulsweiten-Modulationsverfahren (PWM) [1] eingesetzt, welches eine Regulierung der Helligkeit von 0-100% ermöglicht.

In diesem Projekt werden zwei Ansätze gezeigt, eine solche Dimmung zu realisieren. Zuerst wird ein diskreter Aufbau mit Hilfe eines Timerbausteins gezeigt. Dann wird eine deutlich verbesserte Lösung mit einem AVR-Mikrocontroller entwickelt, die zudem einige extra Features aufweist.

Dimmen von Gleichspannungslasten

LED’s erlöschen vollständig, wenn Ihre Schwellspannung unterschritten wird. Diese Schwellspannung muß also mindestens vorhanden sein, damit die LED leuchtet. Weiterhin bevorzugen LED’s zum Betrieb einen konstanten Strom, was sie mit einer langen Lebensdauer belohnen.

Abbildung 1 - Prinzip der Pulsweitenmodulation

Bei der Pulsweitenmodulation werden die LED’s mit einem konstanten Arbeitsstrom versorgt, der typischerweise zwischen 20 und 25 mA liegt. Um die Helligkeit steuern zu können, wird der Strom in schneller Folge ein- und ausgeschaltet, was unser träges Auge als Dimmung wahrnimmt

Die Frequenz muß also ausreichend hoch sein, damit wir das Flackern der LED’s nicht mehr wahrrnehmen können. Die Helligkeit kann nun durch die Änderung des Tastverhältnisses bei konstanter Frequenz gesteuert werden, was zu längeren oder kürzeren Einschaltzeiten der LED’s führt, wie Abbildung 1 verdeutlicht. Weitere Informationen zu diesem Thema sind in [1] [2] [3] zu finden.

PWM mit Timerbaustein NE555

Mit diesem universellen Baustein [4] lassen sich sehr viele Probleme lösen, da er je nach Beschaltung die unterschiedlichsten Funktionen erfüllen kann. Zur Erzeugung eines Rechteck-Signals verschalten wir den Timer als astabilen Multivibrator (Oszillator). Dazu finden sich in der Literatur zahlreiche Beispiele. Ich verwende hier eine Variante, wie Sie in [5] beschrieben ist. Abbildung 2 zeigt den Schaltplan des PWM-Dimmers. Wenn der Ausgang (Pin 3) auf 1 schaltet, wird der Kondensator C1 über das Poti P1 [SA] und die Diode D2 geladen. Beträgt die Ladung etwa 2/3 der Betriebsspannung, kippt der Threshold-Eingang (Pin 6) und schaltet den Ausgang auf Low. Das führt zum Entladen des Kondensators C1 über die andere Potihälfte [SE] und die Diode D1. Wenn die Ladung unter ca. 1/3 der Betriebsspannung gefallen ist, schaltet Pin 6 wieder um und der Vorgang startet von Neuem. Die Frequenz des Oszillators wird über das RC-Glied aus P1/C1 bestimmt und kann nach der Formel

f = 1,44 / (P1 * C1)

näherungsweise berechnet werden. In der Schaltung Abbildung 2 liegt sie bei ca. 150 Hz. Das Tastverhältnis wird durch das Poti P1 und die beiden Dioden D1/D2 eingestellt. Durch Verändern der Potistellung ändert sich das Widerstandsverhältnis für den Lade-/Entladezweig des Kondensators C1, was zu einem schnelleren oder langsameren Laden/Entladen des Kondensators und damit zu einer Veränderung des Tastverhältnisses (Duty Cycle) des Ausgangssignals führt, welches am Discharge-Ausgang (Pin 7) des 555 abgegriffen wird.

Abbildung 2 - Schaltplan PWM Dimmer mit NE555

Um größere Lasten schalten zu können, wird hier ein Treiber nachgeschaltet. Der Ausgang 3 verfügt im Gegensatz zum Ausgang an Pin 7 über eine integrierte Gegentakt-Endstufe, die Ströme bis 200 mA treiben kann. Da Pin 3 und Pin 7 immer den gleichen Pegel führen und wir für den Treiber keinen nennenswerten Strom benötigen, wird der Ausgang hier am Pin 7 abgegriffen. Als Treiber kommt ein Logic-Level HEXFET zum Einsatz, der neben einem hohen Eingangswiderstand auch einen geringen DS-Widerstand aufweist und bereits bei Spannungen von 2-4V durch schaltet. Der hier verwendete Typ kann bis zu 50A schalten. Die Gate-Source-Kapazität kann bei der geringen Frequenz von 150 Hz vernachlässigt werden. Wird die Frequenz jedoch erhöht, dann steigt auf Grund der geringeren Eingangsimpedanz auch der Gate-Strom, was in dieser Schaltung zu Problemen führen könnte.

Da der Ausgangspegel etwas unter der Betriebsspannung liegt, müssen wir die Schaltung mit mindestens 6V betreiben, um ein sicheres Schalten des FETs zu gewährleisten. Leider steigt durch die Erhöhung der Betriebsspannung auch der Stromverbrauch der Schaltung. Dieser beträgt bei 12V etwa 8 mA. Die Höhe der Betriebsspannung ist in der hier gezeigten Schaltung abhängig vom Verbraucher und kann zwischen 5V und max. 16V liegen. Mehr verkraftet der Timer nicht.

Die hier gezeigte Lösung funktioniert sehr stabil und ist nach dem Anlegen der Versorgungs-spannung sofort betriebsbereit. Die Einstellung wird auch nach Abschalten des Dimmers beibehalten. Allerdings ist diese Schaltung auf Grund ihres diskreten Aufbaus wenig flexibel und die Helligkeit ist über den gesamten Bereich lediglich mit dem geringen Drehwinkel von etwa 270° eines Potentiometers einstellbar. Die Steuerung erfolgt je nach eingesetztem Potentiometer linear oder logarithmisch.

PWM mit Mikrocontroller (MCU)

Die zweite „zeitgemäße“ Lösung verwendet einen Mikrocontroller und besteht somit aus einem Hardware- und einem Softwareteil. Sie weist gegenüber der ersten Lösung eine Reihe von Vorteilen auf, die auf die Verwendung einer MCU (Micro-Controller-Unit) zurückzuführen sind:

Abbildung 3 - Pinbelegung des ATtiny13 (PDIP)
  • Funktionalität wird in Software abgebildet (erweiterbar, änderbar)
  • Geringer Stromverbrauch
  • Einsatz eines Drehgebers (Encoders) möglich
  • Steuerungsverhalten (Funktion) ist beliebig programmierbar

Der Schaltungsaufwand ist gegenüber der diskreten Variante etwa gleich groß. Zusätzlich muß aber der Controller mit einem Programm bestückt werden, was zu erhöhtem Entwicklungsaufwand führt. Die o.g. Vorteile dieser Lösung liegen jedoch auf der Hand und rechtfertigen diesen Aufwand allemal. Als MCU kommt hier ein ATtiny13, der Fa. Atmel zum Einsatz [6]. Dieses kleine Kraftpaket verfügt über einen sehr leistungsfähigen 8-Bit Timer mit eingebautem PWM-Generator einen 10-Bit ADC-Wandler sowie einen Analog-Comparator. Der Chip ist sehr kompakt und energieeffizient, da er im Betrieb weniger als 1mA verbraucht und nur wenige Bauteile für die äußere Beschaltung benötigt.

Abbildung 4 - Schaltplan der MCU Variante des LED-Dimmers

Obwohl die folgende Schaltung (Abbildung 4) ziemlich kompakt ist, kann man sie in unterschiedliche Funktionsblöcke unterteilen, die im Folgenden besprochen werden:

  • Stromversorgung
  • Lasttreiber
  • Drehgeber
  • Speicherlogik
  • Controller (PWM Generator)

Stromversorgung

Die Spannungsversorgung hat die Aufgabe, die Betriebsspannung des Controllers von der Lastspannung zu entkoppeln und zu stabilisieren. Dadurch werden Lastspannungen möglich, die von der Betriebsspannung des Controllers abweichen. Da unsere Schaltung weniger als 1mA Strom zieht, genügt für die Stabilisierung ein 78L05.
Der ATTiny13 läßt sich auch mit Spannungen, kleiner als 5V betreiben, wodurch er noch sparsamer wird, allerdings muß man dabei bedenken, daß die Ausgangsspannung an den Pin's nicht größer als die Betriebsspannung sein kann. Um den Treiber-FET voll durchsteuern zu können, gehen wir also sicher und verwenden 5V. Da wir einen Spannungsregler einsetzen, gibt es auch hier bezüglich der Eingangsspannung Grenzen. Diese liegen etwa zwischen 7V und 30V.

Lasttreiber

Der Lasttreiber ist schon beim NE555 erklärt worden. Die zum Durchsteuern des HEXFETs benötigte Spannung von min. 2-4 Volt können wir direkt an einem Port abgreifen. Um mehr Reserve zu haben, läßt sich auch ein IRLZ34N einsetzen, der schon bei 2V durchschaltet. Da wir die Stromversorgung für die Logik von der Lastspannung getrennt haben, können wir hier auch größere Spannungen schalten. Aber Achtung, der 78L05 verkraftet an seinem Eingang nicht mehr als 35V!

Drehgeber

Drehgeber (Encoder oder Inkrementaldrehgeber) sind Drehschalter, die keinen Anschlag haben, also endlos in beide Richtungen drehbar sind und dabei zwei eingebaute versetzte Schalter betätigen. Sie erzeugen daher zwei Impulse, die um 90° phasenverschoben sind (Abbildung 5). Eine detaillierte Beschreibung des Prinzips ist in [7] zu finden.

Abbildung 5 - Drehgeber Funktion

Abbildung 5 zeigt die Funktion eines Drehgebers. Je nach Drehrichtung wird dabei eine Abfolge sogenannter „GrayCodes“ erzeugt. Diese sind ebenfalls in der Abbildung dargestellt und ergeben sich aus dem diskreten Zustand beider Signale. Aus der Sequenz können wir damit die Drehrichtung ableiten, wobei jeder Flankenwechsel einen Takt erzeugt. Aus der Abbildung erkennen wir auch, daß zwischen zwei Raststellungen jeweils zwei Flanken, also zwei Impulse entstehen (auf jedem Signal eins).

Rechtsdrehung 00-01-11-10 (0-1-3-2)
Linksdrehung 00-10-11-01 (0-2-3-1)

Die Auswahl an verfügbaren Drehgebern ist groß. Häufig verwendet, da leicht erhältlich, sind preiswerte Drehgeber, die nicht so eine optimale Charakteristik aufweisen, wie dies in Abbildung 5 dargestellt ist. Dazu gehört z.B. die sehr verbreitete EC11 Serie von ALPS (Ich hab leider auch so ein Ding). Das Datenblatt dieser Encoder beschreibt daß die Raststellungen direkt auf der Flanke des B-Signals liegen. Der Zustand ist damit am Rastpunkt nicht genau definiert, was die Auswertung deutlich erschwert und die Auflösung einschränkt. In [7] wird dieses Problem diskutiert und zeigt entsprechende Lösungen auf, die hier ebenfalls Verwendung finden.

Es gibt grundsätzlich zwei gängige Verfahren, den Code eines Drehgebers auszuwerten. Bei einem hochwertigen Modell kann man alle Flanken beider Signale zum triggern verwenden und hat somit die maximale Auflösung. Ein Drehgeber mit 30 Raststellungen hat demzufolge pro Umdrehung 60 Flankenwechsel. Beim EC11 (siehe oben), ist der Flankenwechsel B nicht sicher und kann daher nicht verwendet werden. Die Auflösung halbiert sich, da wir nur das Signal A als Takt verwerten können.

Flankentrigger
Bei mechanischen Encodern muß mit Prellen der Kontakte gerechnet werden. Wenn man also die Flanken zum triggern verwendet, muß man dafür sorgen, daß die Flankenwechsel entprellt werden. Wenn nur ein Signal zum triggern verwendet wird, muß auch nur dieses entprellt werden, da sich immer nur ein Signal zu einem Zeitpunkt ändern kann.
Die Logik ist einfach, denn wenn man Signal A zum Triggern verwendet, bekommt man bei der Abfrage von Signal B direkt die Drehrichtung. Leider ist die Gesamtauflösung dabei auf ein Viertel reduziert, wenn wir z.B. nur die steigende Flanke des A-Signals verwenden. Werden beide Flanken des A-Signals verwendet, wird die Auflösung nur halbiert, der Verarbeitungsaufwand wird dann aber höher. Der Flankentrigger ist mit einigen Nachteilen behaftet, da die Genauigkeit durch das Prellen der Taster und durch mögliches Pendeln der Zustände, eingeschränkt ist und Fehlimpulse ausgelöst werden können.
Polling
Beim Pollen werden die GrayCodes des Drehgebers in regelmäßigen Abständen abgefragt und jeweils mit dem Vorgänger verglichen. Dieses Verfahren ist sehr viel genauer, liefert bei genügend hoher Frequenz eine gute Auflösung und ist sehr robust (geringe Fehlerquote). Auch muß man sich hier nicht um das Prellen der Tasten kümmern, da man ungültige Werte in der Sequenz (siehe oben) einfach ignorieren kann.
In [7] werden zwei Implementierungen für das Polling beschrieben, die beide gut geeignet sind. Bei der zweiten Version ist die Auflösung halbiert, was aber eine stabile Auswertung von Encodern erlaubt, die einen Flankenwechsel auf der Raststellung haben (wie mein EC11).

Speicherlogik

Um Daten persistent zu speichern, enthalten die meisten Mikrocontroller ein integriertes EEPROM. Dieses ist i.d.R. nur etwa 100000 mal beschreibbar [6]. Das erfordert eine effiziente Speicher-Strategie. Ein praktikabler Weg ist es, nur beim Abschalten der Betriebsspannung zu speichern. Da gehen zwar die Daten bei einem Controller-Reset verloren, aber damit läßt sich leben. Siehe auch Artikel Mikrocontroller Status speichern [8].

Dazu muß ein Betriebsspannungsausfall frühzeitig erkannt werden um das Speichern auszulösen. Die Spannung für den Controller muß dann entsprechend lange aufrecht erhalten werden, bis die Daten weggeschrieben sind.

Die Betriebsspannung wird vor dem Regler abgegriffen und über einen Spannungsteiler dem integrierten Komparator des Controllers zugeführt. Als Referenzspannung dient die interne Bandgap-Referenz. Der Kondensator C1 glättet die Eingangsspannung für den Regler.

Der Pufferkondensator C2 hält die Spannung nach der Abschaltung noch für etwa 10ms aufrecht (Abbildung 6). Das genügt dem Controller zum Speichern der Daten. Die Kapazität vor dem Regler muß größer bemessen sein als dahinter um Spannungsrückfluß über den Regler zu vermeiden. Alternativ kann der Regler mit einer Rückkopplungs-Diode geschützt werden.

Der Spannungsteilers R1/R2 muß so eingestellt werden, daß dem Controller eine Spannung zugeführt wird, die während des Betriebes immer über der Bandgap-Referenzspannung von ca. 1,1V liegt. Fällt die Spannung unter diesen Wert, schaltet der Komparator um und löst einen Interrupt aus, der zum Wegschreiben der Daten genutzt wird. Die Schaltschwelle sollte aber so gewählt werden, daß der Regler nach der Abschaltung noch sicher arbeitet und der Komparator auf eine stabile Referenzspannung zugreifen kann, bis der Interrupt ausgelöst ist.

Im Schaltplan ist der Spannungsteiler so bemessen, daß der Komparator bei einer Betriebsspannung von ca. 8V kippt und einen Interrupt auslöst. Je nach Welligkeit der Meßspannung ist zu beachten, daß auch die negative Halbwelle nicht unter den Schwellwert sinkt (Abbildung 6).

Das Oszilloskop zeigt in Abbildung 6, wie bei Unterschreitung der Referenzspannung die Versorgungsspannung noch für einige Millisekunden gehalten wird. In Abbildung 7 ist zu erkennen, wie beim Erreichen der internen Referenzspannung das Flag ACO (Kanal 2) umschaltet und einen Interrupt auslöst.

Abbildung 6 - Spannungsabfall
Abbildung 7 - Interrupt Auslösung

Controller-Logik

Der Controller benötigt kaum externe Beschaltung. Als Taktquelle wird der interne RC-Oszillator verwendet, da es auf Frequenzgenauigkeit nicht ankommt.

Das PWM-Signal wird durch den integrierten Timer des Controllers bereitgestellt. Die Software wertet das Encoder-Signal aus und steuert damit das Tastverhältnis des internen PWM-Generators.

Software

Doch nun zum interessanten Teil, der Programmierung der MCU. Ein sehr guter Einstieg in die Programmierung von AVR-Controllern u.a. mit Beispielen zur PWM findet sich in [9]. Im ersten Schritt werden wir das PWM-Signal einmal „zu Fuß“ erzeugen, um zu verstehen, wie das funktioniert [10]. Dazu aber zunächst noch ein paar wichtige Vorüberlegungen:

Abbildung 8 - PWM mit Timer / Counter

Unsere Last liegt am Pin5 (PB0) der MCU. Wir müssen also in regelmäßigen Abständen diesen Port-Pin ein- und wieder ausschalten. Für sich wiederholende Aktionen benötigt man einen Timer (Scheduler), der zyklisch einen Interrupt auslöst, den wir dann zum Umschalten des Ausgangs verwenden können. Im einfachsten Fall müssen wir den Ausgang nur toggeln, also umschalten. Aber dann haben wir ein fest eingestelltes Tastverhältnis von 50%, da ja die Interrupts in regelmäßigen Abständen ausgelöst werden und damit die EIN-Phasen genauso lang sind wie die AUS-Phasen. Das wollen wir aber nicht !

Wir benötigen einen Rechteckimpuls mit steuerbarem Tastverhältnis. Der Timer zählt mit jedem Takt seinen Zähler um eins weiter (in unserem Fall von 0 bis 255) (siehe Abbildung 8). Dann löst er einen Überlauf-Interrupt (TOV) aus, setzt sich zurück und fängt wieder von vorn an. Da die Frequenz des Ausgangssignals ja konstant sein soll, setzen wir eine vollständige Zählsequenz genau einer Periode des Ausgangssignals gleich. Wir müssen jetzt nur noch irgendwo zwischendrin das Signal umschalten.

Der Timer bietet uns dafür zwei Vergleichsregister an, die er während des Hochzählens ständig mit seinem Zähler vergleicht. Bei Übereinstimmung wird ein CompareMatch-Interrupt (OCF) ausgelöst. Abbildung 8 zeigt das Prinzip. Wenn wir diesen Interrupt zur Umschaltung unseres Signalpegels nutzen, dann bestimmt der Inhalt des CompareMatch-Register das Tastverhältnis am Ausgang.

Wir müssen also nur im OCF-Interrupt das Signal zurück setzen und beim Überlauf wieder setzen. Da das CompareMatch-Register gepuffert ist, kann es jederzeit neu beschrieben werden. Ein Update führt der Controller dann beim Timerüberlauf selbsttätig aus.

/************************************************************************/
/*              PWM Dimmer (Soft PWM)                                   */
/*                                                                      */
/*              Autor: Juergen Werner                                   */
/*              Datum: 22.01.2012                                       */
/*              Version: 1.0                                            */
/*              Copyright (c) by Trimension, Cologne                    */
/************************************************************************/
#include <avr/io.h>
#include <avr/interrupt.h>

uint8_t duty_cycle = 192;

// timer overflow
ISR( TIM0_OVF_vect ) 
{
	PORTB |= (1<<PB0);		// set output
}

// compare match
ISR( TIM0_COMPA_vect )
{
	PORTB &= ~(1<<PB0);		// reset output
}

int main(void)
{
	cli();
	
	// port configuration
	DDRB = (1<<PB0);		// set output port direction

	// interrupt configuration
	TIMSK0 = (1<<TOIE0) | (1<<OCIE0A);	// enable timer overflow and
 				// compare match A interrupt

	// timer configuration
	// CPU-Clock		=> 4.8 MHz	(Fuse CKSEL1..0)
	// CKDIV8 /8		=> 600 KHz	(Fuse CKDIV8 set per default)
	// Timer Counter /256	=> 2.34 KHz	(Timer no prescaling)
	TCCR0B = (1<<CS00);             // precaler (no prescaling)
	OCR0A = duty_cycle;		// initial Value of Duty Cycle 30%

	sei();
	while(1);
}
Listing 1 - Software PWM

Die CPU-Taktfrequenz in Listing 1 wird durch den internen RC-Oszillator des Controllers vorgegeben und kann per Fuse-Bits auf 4,8 MHz oder 9,6 MHz (default) eingestellt werden. Zusätzlich gibt es das FuseBit CKDIV8, das standardmäßig gesetzt ist und den CPU-Takt durch 8 teilt. Beim Setzen dieser Bits sollte beachtet werden, daß der Chip bei höheren Frequenzen mehr Strom verbraucht. In Batterieschaltungen kann das durchaus relevant sein.

Die Frequenz des Ausgangssignals bestimmt sich dann aus der Taktrate und dem Timer-Prescaler. Im Listing 1 verwenden wir einen CPU-Takt von 600 KHz, und deaktivieren den Prescaler des Timers und kommen so mit unserem 8-Bit Timer auf eine Ausgangsfrequenz von ca. 2300 Hz.

Um den CPU-Takt auf 600 KHz einzustellen setzen wir die folgenden Fuse-Bits:

CKSEL1 - aus

CKSEL0 - an
CKDIV8 – an (default)

Achtung! Die Fuse Bits arbeiten invers (an bedeutet also 0 und aus gleich 1)

Dieses kleine Beispiel zeigt sehr deutlich, wie einfach es ist, ein PWM-Signal zu erzeugen. Aber es geht noch einfacher und die Lösung ist ja auch noch nicht vollständig, da wir das Tastverhältnis nicht einstellen können.

/************************************************************************/
/*              PWM Dimmer						*/
/*                                                                      */
/*              Autor: Juergen Werner                                   */
/*              Datum: 22.01.2012                                       */
/*              Version: 1.0                                            */
/*              Copyright (c) by Trimension, Cologne                    */
/************************************************************************/
#include <avr/io.h>
#include <avr/interrupt.h>

uint8_t duty_cycle = 192;

int main(void)
{
	cli();
	
	// port configuration
	DDRB = (1<<PB0);		// set output port direction

	// timer configuration
	// CPU-Clock		=> 4.8 MHz	(Fuse CKSEL1..0)
	// CKDIV8 /8		=> 600 KHz	(Fuse CKDIV8 set per default)
	// Timer Counter /256	=> 2.34 KHz	(Timer no prescaling)
	// select Fast PWM on OC0A (non inverting mode)
	TCCR0A = (1<<WGM01) | (1<<WGM00) | (1<<COM0A1) | (0<<COM0A0
	TCCR0B = (1<<CS00);        	// precaler (no prescaling)
	OCR0A = duty_cycle;		// initial Value of Duty Cycle 30%

	sei();
	while(1);
}
Listing 2 - PWM mit WaveformGenerator

Der eingebaute WaveformGenerator arbeitet nach dem gleichen Prinzip, nimmt uns jedoch einiges an Arbeit ab, arbeitet schneller und sparsamer, als unser „HandMade“ Code. Wie Listing 2 zeigt, wird der Code deutlich kürzer, weil wir keine Interrupts mehr benötigen.

Nach entsprechender Konfiguration der Timer-Register können wir das fertige PWM Signal am Ausgang PB0 abgreifen. Das Tastverhältnis wird wie bei unserer ersten Lösung im Compare-Match Register abgelegt. Um das Tastverhältnis des Ausgangssignals einstellen zu können, müssen wir also nur den Inhalt des OCR0x Registers ändern. Dazu setzen wir einen Drehgeber ein, für den wir zwei Eingangs-Pins benötigen.

Wie im Abschnitt Drehgeber bereits beschrieben, gibt es hier unterschiedliche Ansätze. Auf die Flankentriggerung werde ich nicht weiter eingehen, da sie unbefriedigende Ergebnisse liefert und einen hohen Aufwand für das Entprellen der Signale erfordert.

/* =====================================================================
 *	PWM Dimmer Headerfile
 *
 *	pwmdimmer.h - contains program's global definitions
 *
 *	Copyright (c) by Juergen Werner, Koeln 2012
 * ====================================================================*/

// === MCU Speed Definition ===
#ifndef F_CPU
#define F_CPU           4800000            // processor clock frequency
#warning kein F_CPU definiert
#endif

#define PHASE_A	(PINB & (1<<ENC_A))
#define PHASE_B	(PINB & (1<<ENC_B))

#define ENC_A	PB4	// IN  Encoder Input Phase A
#define ENC_B	PB3	// IN  Encoder Input Phase B
#define PWM_OUT	PB0	// OUT PWM Output Signal
Listing 3 - Headerfile für Encoderversion
/************************************************************************/
/*              PWM Dimmer (Encoder)				 	*/
/*                                                                      */
/*              Autor: Juergen Werner                                   */
/*              Datum: 22.01.2012                                       */
/*              Version: 1.0                                            */
/*              Copyright (c) by Trimension, Cologne                    */
/************************************************************************/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include "pwmdimmer.h"

volatile uint8_t duty_cycle = 192;

// reinit Compare Match (Duty Cycle)
ISR( TIM0_OVF_vect ) 
{
	static uint8_t enc_last = 0;
	int8_t enc_value = 0;

	if( PHASE_A ) enc_value = 1;	// phase A (clock)
	if( PHASE_B ) enc_value ^= 3;	// phase B (direction)

	enc_value -= enc_last;		// difference new - last

	if( enc_value & 1 ) {			// bit 0 = value (1)
		enc_last += enc_value;		// store new as next last
		if(enc_value & 2) {
			if(duty_cycle < 255) duty_cycle++;
		} else {
			if(duty_cycle > 0)  duty_cycle--;
		}
		OCR0A = duty_cycle;
	}
}

int main(void)
{
	cli();
	
	// port configuration
	DDRB = (1<<PWM_OUT);		// set output port direction
	PORTB = (1<<ENC_A) | (1<<ENC_B);// activate pullup's for encoder input

	// mcu and interrupt configuration
	MCUCR = (1<<SE);		// activate sleep enable (idle)
	TIMSK0 = (1<<TOIE0);		// enable timer overflow

	// timer configuration
	// CPU-Clock		=> 4.8 MHz	(Fuse CKSEL1..0)
	// CKDIV8 /8		=> 600 KHz	(Fuse CKDIV8 set per default)
	// Timer Counter /256	=> 2.34 KHz	(Timer no prescaling)

	// select Fast PWM in non inverting Mode on OC0A (PB0)
	TCCR0A = (1<<WGM01) | (1<<WGM00) | (1<<COM0A1) | (0<<COM0A0);	
	TCCR0B = (1<<CS00);       	// precaler (no prescaling)
	OCR0A = duty_cycle;		// initial Value of Duty Cycle

	sei();

	sleep_cpu();		// we are going to idle mode...
	sleep_disable();
	
 	while(1);
}
Listing 4 - Dimmer mit einstellbarem Tastverhältnis

Für das Polling sind regelmäßige Abfragen der beiden Encoder-Eingänge notwendig. Dafür setzen wir wieder unseren Timer ein, der mit einer Frequenz von ca. 2,3 KHz, also alle 0,43 ms einen Interrupt auslöst.

Der TOV-Interrupt muß also wieder aktiviert werden, diesmal jedoch nicht um das Ausgangssignal zu generieren, sondern um den Encoder abzufragen und das CompareMatch Register neu zu setzen. Ich verwende hier die Lösung von Peter Dannegger, wie sie in [7] beschrieben ist. Die Abfrage der Pins ist im Header definiert und macht den Code in Listing 4 übersichtlicher. Zu beachten ist noch, daß die internen Pullup-Widerstände für die Encoder-Pins aktiviert werden müssen, da der Encoder gegen Masse schaltet und um die externe Beschaltung zu minimieren.
In der Interrupt-Routine werden die beiden Encoder-Pins abgefragt und erzeugen daraus den Graycode. Davon ziehen wir den zuletzt gelesenen Code ab und ermitteln so, ob der Encoder betätigt wurde (Bit 0) und die Drehrichtung (Bit 1). Die genaue Funktion ist in [7] beschrieben. Abhängig von diesen Informationen, wird dann das Vergleichsregister incrementiert oder decrementiert, wobei der Über- bzw. Unterlauf des Registers berücksichtigt wird.

Diese Auswertung nutzt die volle Encoderauflösung, was aber zu Fehlern führen kann, wenn die Abtastfreuquenz, zu gering ist oder wenn, wie oben beschrieben, die Flanke des B-Signals nicht klar definiert ist. Die Verarbeitung des Graycodes ist jedoch fehlertolerant, weshalb sich Fehlauswertungen nicht negativ auswirken.

Eine stabilere Lösung, ist ebenfalls in [7] beschrieben und in Listing 5 gezeigt. Dabei wird eine Lookup-Tabelle genutzt, die mit dem Graycode indiziert wird und direkt das Ergebnis der Auswertung liefert. Da die B-Flanke nicht stabil ist, wird nur die A-Flanke zur Auswertung genutzt und dadurch leider auch die Encoderauflösung halbiert.

const int8_t enc_table[16] PROGMEM = 
         {0, 0, -1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, -1, 0, 0};

// reinitialization Compare Match (Duty Cycle)
ISR( TIM0_OVF_vect ) 
{
	static uint8_t enc_last = 0;
	enc_last = (enc_last << 2) & 0x0F;
	if( PHASE_A ) enc_last |= 2;		// phase A (clock)
	if( PHASE_B ) enc_last |= 1;		// phase B (direction)

	int8_t counter = pgm_read_byte(&enc_table[enc_last]);

	if( ((counter > 0) && (duty_cycle < 255))
	 || ((counter < 0) && (duty_cycle > 0)) ) {
		duty_cycle += counter;
		OCR0A = duty_cycle;
	}
}
Listing 5 - Stabile Auswertung bei halber Auflösung

Sinneswahrnehmung

Der vorgestellte PWM-Dimmer ist bereits eine praktikable, robuste und einfach zu realisierende Lösung. Der Controller ist damit aber noch nicht ausgelastet und wir haben somit noch jede Menge Spielraum für Verbesserungen.

Das Tastverhältnis wird über den Drehgeber über den gesamten Bereich von 0% bis 100% linear eingestellt. Unsere natürlichen Sinne sind jedoch nicht linear. So empfinden wir eine konstante Helligkeits- oder Lautstärkeänderung im oberen Bereich anders als im unteren Bereich. In [11] und [12] sind diese Zusammenhänge erklärt. Die menschliche Sinneswahrnehmung folgt, nach dem Weber-Fechner Gesetz einem logarithmischen Verlauf.

Aus diesem Grund hat man früher (teilweise auch noch heute) in den Radio- und Fernsehgeräten logarithmische Potentiometer für die Lautstärkeregelung eingesetzt, da so die Lautstärke für uns sehr viel natürlicher zu regeln war.

Diese Erkenntnis wollen wir nun auf unseren LED-Dimmer anwenden. Um den nichtlinearen Verlauf unserer Regelung zu beschreiben, benötigen wir eine Funktion. Für unser natürliches Helligkeitsempfinden muß also eine nichtlineare Steuerung implementiert werden, die der nichtlinearen Funktion unserer Sinne entspricht. Abbildung 9 zeigt den exponentiellen Verlauf dieser Funktion.

Abbildung 9 - Exponentialfunktion für die Wertetabelle

Um diese Funktion in unseren Dimmer zu integrieren, muß für jede Stellung des Drehgebers ein Funktionswert berechnet werden. Auf der x-Achse des Diagramms in Abbildung 9 finden wir die „alten“ Werte unseres Compare-Registers. Diese müssen nun in den Wert aus dem Diagramm umgerechnet werden. Da der Controller mit einer Exponential-Funktion hoffnungslos überfordert wäre, nutzen wir wieder eine Lookup-Tabelle mit bereits errechneten Werten.

Ähnlich der Digitalen Signalerzeugung (DDS), legen wir die vorab berechneten Funktionswerte in einer Tabelle ab und laufen mit unserem Zähler nur jeweils vorwärts oder rückwärts durch diese Tabelle.

Da der ATTiny13 nur 64 Bytes EEProm-Speicher mitbringt, legen wir die Tabelle im Programm-Speicher ab. Die Software benötigt etwa 400 Bytes, sodaß wir reichlich Luft haben und ohne Begrenzung der Auflösung eine Tabelle mit 256 Funktionswerten im Programmspeicher ablegen können.

Listing 6 zeigt die Implementierung der Tabelle und den Zugriff darauf.

Auf diese Weise können wir jede erdenkliche Funktion in Form einer Werte-Tabelle im Speicher ablegen und damit das Verhalten der Steuerung beeinflussen. Zum leichteren Austauschen wäre es zwar besser, die Tabelle im EEProm abzulegen, aber da haben wir nur 64 Bytes Platz, was die Auflösung entsprechend reduzieren würde. Auch ist es auf Grund der relativ langen Zugriffszeiten nicht empfehlenswert, da Interrupt-Routinen möglichst kurze Laufzeiten haben sollten.

Status abspeichern

Als letztes Feature soll der aktuelle Index persistent gespeichert werden. Denn es wäre schon ziemlich lästig, wenn eine LED-Leuchte bei jedem Einschalten immer wieder bei einem vorgegebenen Initialwert starten würde.

Wie im Abschnitt Speicherlogik bereits beschrieben wurde, verwenden wir den integrierten Komparator zur externen Spannungsausfall-Erkennung um die Daten in das EEPROM zu schreiben.

Die Meßspannung liegt an Pin 6 des Controllers (PB1/AIN1). Per Konfiguration wählen wir als Referenzspannung die interne Bandgap-Referenz. Diese wird intern an den nicht invertierenden Eingang des Komparators gelegt. Die Meßspannung muß also größer sein, als die Referenzspannung. Der Ausgang des Komparators wird durch das ACO-Flag im Register ACSR repräsentiert. Im Betrieb ist es nicht gesetzt. Erst wenn die Meßspannung unter die Komparator-Schwelle fällt, wird ACO gesetzt. Diese Flanke löst den komparatoreigenen Interrupt aus.

Im Listing 6 ist zu erkennen, wie der Komparator konfiguriert wird. Die Bandgap Referenz benötigt für den Start einige Zeit und wird erst hochgefahren, wenn Sie benutzt wird, also wenn wir den Komparator initialisieren. Diese Zeit muß gewartet werden, bevor die Interrupts freigegeben werden.

Da die Lookup-Table konstant ist, genügt es, den aktuellen Index zu speichern um beim nächsten Einschalten den aktuellen PWM-Wert wieder herstellen zu können. Also müssen wir nur ein Byte speichern, was beim AVR etwa 3ms benötigt. Die Schaltung hält die Spannung noch für etwa 10 ms, was genug Reserven bietet. Das Speichern wird in der ISR des Komparators erledigt.

Nach dem Speichern sollte die CPU nichts mehr tun. Daher ist es sinnvoll, den Controller schlafen zu legen, damit ggf. weitere Impulse die ISR nicht noch einmal ausführen können.

Schließlich soll noch darauf hingewiesen werden, daß der Controller die meiste Zeit nichts zu tun hat. Nur bei Betätigung des Drehgebers, gibt es Arbeit. Daher wird der Controller nach der Initialisierung in den „Idle“ Modus versetzt. Das reduziert den Stromverbrauch erheblich. In diesem Modus sind die Interrupts und die PWM-Generierung weiterhin aktiv.

Listing 6 zeigt das vollständige Listing der finalen Version des PWM-Dimmers mit allen beschriebenen Features.

/************************************************************************/
/*              PWM Dimmer						*/
/*                                                                      */
/*              Autor: Juergen Werner                                   */
/*              Datum: 22.01.2012                                       */
/*              Version: 1.0                                            */
/*              Copyright (c) by Trimension, Cologne                    */
/************************************************************************/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/eeprom.h>
#include <avr/sleep.h>
#include <avr/delay.h>
#include "pwmdimmer.h"

uint8_t dc_idx = 128;
uint8_t ee_index EEMEM = 128;
const int8_t enc_table[16] PROGMEM = 
		{0, 0, -1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, -1, 0, 0};
const uint8_t pwm_value[256] PROGMEM = 
		{   0,   0,   0,   0,   0,   1,   1,   1,   
		    1,   2,   2,   2,   2,   2,   2,   2,
		    2,   2,   2,   2,   2,   2,   2,   2,   
		    2,   3,   3,   3,   3,   3,   3,   3,
		    3,   3,   3,   3,   3,   3,   3,   3,   
		    3,   3,   4,   4,   4,   4,   4,   4,
		    4,   4,   4,   4,   4,   4,   4,   5,   
		    5,   5,   5,   5,   5,   5,   5,   5,
		    5,   6,   6,   6,   6,   6,   6,   6,   
		    6,   7,   7,   7,   7,   7,   7,   7,
		    8,   8,   8,   8,   8,   8,   9,   9,   
		    9,   9,   9,   9,  10,  10,  10,  10,
		   10,  11,  11,  11,  11,  11,  12,  12,  
		   12,  12,  13,  13,  13,  13,  14,  14,
		   14,  15,  15,  15,  16,  16,  16,  16,  
		   17,  17,  17,  18,  18,  19,  19,  19,
		   20,  20,  21,  21,  21,  22,  22,  23,  
		   23,  24,  24,  25,  25,  26,  26,  27,
		   27,  28,  28,  29,  29,  30,  31,  31,  
		   32,  33,  33,  34,  35,  35,  36,  37,
		   37,  38,  39,  40,  40,  41,  42,  43,  
		   44,  45,  46,  47,  48,  48,  49,  50,
		   51,  53,  54,  55,  56,  57,  58,  59,  
		   60,  62,  63,  64,  65,  67,  68,  69,
		   71,  72,  74,  75,  77,  78,  80,  82,  
		   83,  85,  87,  88,  90,  92,  94,  96,
		   98, 100, 102, 104, 106, 108, 110, 112, 
		  115, 117, 119, 122, 124, 127, 129, 132,
		  134, 137, 140, 143, 146, 149, 152, 155, 
		  158, 161, 164, 168, 171, 174, 178, 182,
		  185, 189, 193, 197, 201, 205, 209, 213, 
		  217, 222, 226, 231, 235, 240, 245, 255 };

// reinitialization Compare Match (Duty Cycle)
ISR( TIM0_OVF_vect ) 
{
	static uint8_t enc_last = 0;
	enc_last = (enc_last << 2) & 0x0F;
	if( PHASE_A ) enc_last |= 2;	// phase A (clock)
	if( PHASE_B ) enc_last |= 1;	// phase B (direction)

	int8_t offset = pgm_read_byte(&enc_table[enc_last]);

	if(((offset > 0) && (dc_idx < 255))
		|| ((offset < 0) && (dc_idx > 0))) {
		dc_idx += offset;
		OCR0A = pgm_read_byte(&pwm_value[dc_idx]);
	}
}

ISR( ANA_COMP_vect ) {
	eeprom_busy_wait();
	eeprom_write_byte(&ee_index, dc_idx);

	GIMSK = 0x00;
	MCUCR |= (1<<SM1) | (0<<SM0) | (1<<SE);
	sleep_cpu();		// power down...
	sleep_disable();
}

int main(void)
{
	// port configuration
	DDRB = (1<<PWM_OUT);			// set output port direction
	PORTB = (1<<ENC_A) | (1<<ENC_B);	// set pullup's for encoder

	// mcu and interrupts configuration
	MCUCR = (1<<SE);			// activate sleep enable (idle)
	TIMSK0 = (1<<TOIE0);			// enable timer overflow
	
	// adc / analog comparator config
	ADCSRB = (0<<ACME);		// disable mux
	ACSR = (1<<ACBG) | (1<<ACIE) | (1<<ACIS1) | (1<<ACIS0); // init comp
	DIDR0 = (1<<AIN1D);		// disable digital input buffer
	_delay_loop_2(10000);		// wait for bandgap reference
	ACSR |= (1<<ACI);		// clear wainting interrupts

	// timer configuration
	// CPU-Clock		=> 4.8 MHz	(Fuse CKSEL1..0)
	// CKDIV8 /8		=> 600 KHz	(Fuse CKDIV8 set per default)
	// Timer Counter  /256	=> 2.34 KHz	(Timer no prescaling)

	// select Fast PWM in non Inverting Mode on OC0A (PB0)
	TCCR0A = (1<<WGM01) | (1<<WGM00) | (1<<COM0A1) | (0<<COM0A0);
	TCCR0B = (1<<CS00);           	// precaler (no prescaling)
	
	// read last state
	eeprom_busy_wait();
	dc_idx = eeprom_read_byte(&ee_index);
	OCR0A = pgm_read_byte(&pwm_value[dc_idx]);	// get initial value

	sei();

	sleep_cpu();	// we are going to idle mode...
	sleep_disable();
	
 	while(1);
}
Listing 6 - Vollständiges Programm (finale Version)

Fazit

Im Folgenden werden die beschriebenen Varianten der PWM Dimmer kurz zusammenfassend gegenüber gestellt. Wie aus der Tabelle leicht ersichtlich ist, nehmen die Nachteile von oben nach unten ab, während die Vorteile zunehmen.

Lösung Vorteile Nachteile
PWM mit Timer
  • keine Programmierung notwendig
  • Einstellung überlebt Stromausfall
  • Potiregelung (geringe Auflösung)
  • Nur lineare oder logarithm. Regelung
  • Hoher Stromverbrauch der Schaltung
  • Unflexibel, Funktionalität kann nach Aufbau nicht mehr verändert werden
PWM mit MCU
  • Einsatz eines Drehgebers (hohe Auflösung)
  • Beliebige Regelfunktionen abbildbar
  • Geringer Stromverbrauch
  • Sehr flexibel, da Funktionalität durch Programmierung änderbar ist
  • Einstellung wird automatisch gespeichert
  • muß programmiert werden

Literaturverzeichnis

  1. 1,0 1,1 Wikipedia - Pulsweitenmodulation Beschreibung Pulsweitenmodulation
  2. Mikrocontroller.net - AVR PWM Online Artikel über PWM mit dem AVR
  3. Mikrocontroller.net - AVR Tutorial PWM AVR Tutorial
  4. Texas Instruments - Datenblatt NE555 Datenblatt Timerbaustein NE555 und NE556
  5. R. Bickle - A Simple PWM Circuit Based on the 555 Timer Online Artikel von November 2005
  6. 6,0 6,1 Atmel Corporation - Datenblatt ATtiny13 Datenblatt Atmel ATtiny13
  7. 7,0 7,1 7,2 7,3 7,4 7,5 Mikrocontroller.net - Drehgeber Online Artikel Drehgeber
  8. Trimension - Mikrocontroller Zustand speichern
  9. F. Schäffer, AVR, Hardware und C-Programmierung in der Praxis, Elektor Verlag - Grundlagen und Hintergrundinformationen über die Anwendung der AVR-Mikrocontroller
  10. Mikrocontroller.net - Soft PWM - Online Artikel über Softwareseitiges PWM
  11. Wikipedia - Weber-Fechner Gesetz
  12. Wikipedia - Stevenssche Potenzfunktion