LED Beleuchtung mit dem WS2811

Die Zeiten des einzelnen Birnchens in einem Modellbahnhäuschen, das nicht nur durch alle Fenster, sondern meist auch durch Wände und Dach scheint, sind glücklicherweise vorbei, auch wenn diese Beleuchtungssockel immer noch angeboten werden.
Mittlerweile werden einzelne Fenster mit Lichtkästen oder sogar vollständig eingerichtete Räume mit einer oder mehreren, von der Farbtemperatur abgestimmten, LEDs beleuchtet. Mitunter werden auch ganze Szenarien abgespielt, in denen verschiedene Fenster nach zeitlichen Abläufen an- und ausgeschaltet werden.

Allerdings erfordern n LEDs auch entsprechend viele Anschlussleitungen, mindestens n+1.

Vor dem gleichen Problem stehen auch die Hersteller von LED-Ketten, deren Lichteffekte, z.B. ein farbiges Lauflicht, die separate Ansteuerung von einigen -zig LEDs erfordern.
Daher gibt es dafür auch eine Lösung: Die LEDs (üblicherweise jeweils rot, grün und blau) werden über einen seriellen Treiber angesteuert und diese Treiber hintereinandergeschaltet, so dass nur noch drei Leitungen (+, - und Data) erforderlich sind.

Einer dieser Bausteine ist der WS2811 von WorldSemi, der sowohl als Einzel-IC als auch schon in eine RGB-LED integriert erhältlich ist, und der sowohl mit 5V als auch über einen entsprechenden Vorwiderstand mit 12V betrieben werden kann.
Er besitzt drei LED-Ausgänge, die über Pulsweitenmodulation die Helligkeit jeder der LEDs unabhängig voneinander in 255 Stufen steuern. Der Maximalstrom liegt bei 18,5 mA, womit alle für die Modellbahn sinnvoll einsetzbaren LEDs betrieben werden können, meist allerdings mit deutlich geringerem Strom.

Im Internet finden sich reichlich Anwendungshinweise für diesen Baustein, außerdem ist eine Programmbibliothek für den Arduino verfügbar, so dass dem Einsatz für Modellbahnbeleuchtung nichts im Wege steht.

Hardware

Der WS2811 besitzt 3 Ausgänge. An jeden kann bei 5V eine, bei 12V bis zu 3 LEDs in Reihe angeschlossen werden (bei 12V Vorwiderstand nicht vergessen!). Jeder dieser Ausgänge kann getrennt angesteuert werden. Für mehr Ausgänge werden einfach mehrere Bausteine hintereinandergehängt, wobei zur Vermeidung störender Leitungsreflexionen ein Widerstand von min. 33Ω zwischen Aus- und nächsten Eingang geschaltet werden sollte.
Zusammen mit dem bei 12V erforderlichen Vorwiderstand ergibt sich dann nebenstehende Schaltung:

Über R6 kommt das serielle Steuersignal vom vorigen Baustein nach DIN. Die LEDs werden unten zwischen +12V und den jeweiligen Ausgang geklemmt. DOUT leitet das Signal weiter über R7 zum nächsten Baustein. Wird dieser nicht benötigt, kann das Ausgangssignal über die Lötbrücke SJ2 direkt auf den Leiterplattenausgang gegeben werden, so dass auch Teilbestückungen möglich sind.

Damit das Ganze in ein Spur-N-Gebäude passt, wurde eine Platine entwickelt, auf die maximal 4 Bausteine passen, womit 12 unabhängige Lichter geschaltet werden können. Braucht man nur einen Treiber (max. 3 LEDs), kann man das Modul von Fichtelbahn verwenden, dies ist allerdings nur für 5V ausgelegt, kann aber nach Austausch eines Widerstandes gegen einen mit 2,7kΩ auch mit 12V benutzt werden.
Für 2 oder 3 ICs kann man die Platine wie auf dem Bild teilbestücken (Vorwiderstände und Lötbrücke auf der Rückseite) und bei wenig Platz auch hinter dem letzten IC abtrennen.

Alle Ein- und Ausgänge können optional über JST-Stecker angeschlossen werden, so dass z.B. das beleuchtete Dach abgenommen werden kann.

Auf dem hier mit einem Stecker versehenen Eingang liegen neben +12V, GND und DIN auch der Datenausgang DOUT, so dass ein weiteres Gebäude anhehängt werden kann.

Software

Da ein Arduino in C++ programmiert wird, kann man die Funktionalität in getrennte Klassen auslagern und damit aufteilen und (hoffentlich) übersichtlicher machen. Außerdem kann man so andere Funktionalität von bestehender ableiten und muss nicht jedesmal alles neu codieren. Machen wir also etwas OOP.

Das einfachste Element einer Beleuchtung wäre eine LED:

// declare an observer interface which is updated from the LED
class LEDObserver
{
  public:
    virtual void updateFromLED(bool now)  = 0;
    virtual ~LEDObserver();
};

// declare the states of the LED to be in one of
enum  class LEDState: uint8_t  {
  OFF,					// off
  TRANSIENT_ON,			// playing switch-on animation, e.g. increasing
  ON,					// on, may be playing continuous animation, e.g. flicker
  TRANSIENT_OFF			// playing switch-off animation, e.g. fading out
};

class WS2811LED
{
  private:
    uint16_t		myidx;				// index within the chain
    uint8_t			nominalbrightness;  // the nominal/maximum LED brightness (0..255)
    uint8_t			currentbrightness;  // the current LED brightness (0..255)
    uint8_t			minimumBrightness;	// the minimum brightness to start with
    LEDState		theState;
    LEDObserver*	theObserver;        // the observer to notify of changes (only one!)
    virtual void	animateOn();        // perform the transition when the LED is switched on
    virtual void	animateOff();       // perform the transition when the LED is switched off
    virtual void	animateLoop();      // perform something while the LED is on
  public:
    WS2811LED();
    void			loop(void);
    void			setIdx(uint8_t achn, uint8_t aidx);
    void			setState(LEDState astate);
    LEDState		getState();
    boolean			isOn();
    void			setObserver(LEDObserver* aobserver);
    uint8_t			getCurrentBrightness();
    void			setCurrentBrightness(uint8_t abrite);
    uint8_t			getNominalBrightness();
    void			setNominalBrightness(uint8_t abrite);
    uint8_t			getMinimumBrightness();
    void			setMinimumBrightness(uint8_t abrite);
    void			setOn(bool animate);
    void			setOff(bool animate);
    virtual 		~WS2811LED();
};

Sieht aufwendig aus, macht es aber einfacher, z.B. eine LED zu definieren, die wie eine defekte Leuchtstoffröhre beim Einschalten flackert. Man muss dazu nur die Methode animateOn() überschreiben.

Die nächste Stufe ist dann die Kette aus mehreren LEDs. Ändert sich die Helligkeit einer LED, wird ihr Eintrag in der Kette aktualisiert und ein Flag gesetzt, dass die Hardware neue Daten bekommen muss.
Außerdem kann die Kette in den Lernmodus gehen, um nacheinander die Helligkeiten einzustellen. Diese Werte werden dann im EEPROM des Arduino gespeichert und beim nächsten Booten wieder eingelesen.

//  max number of daisy-chained controllers in one chain
#define CONTROLLER_COUNT 4 
//  maximum number of LEDs for the given number of controllers (max 255!)
const   uint8_t     maxleds = CONTROLLER_COUNT * 3;

class WS2811LEDChain : public LEDObserver
{
  private:
    uint8_t       chainIdx;       		// the index of the chain for debugging
    uint8_t       adrEEPROM;      		// EEPROM start address to store the nominal brightnesses
    uint8_t       currentLEDIdx;		// the index of the currently selected LED
    bool          updaterequired;		// if true, update all the next time
    bool          isTeaching;			// true if in teaching mode
    uint8_t       numLEDs;				// number of LEDs to control (<= maxleds)
    WS2811LED*    theLEDs[maxleds]; 	// array of the LEDs
    void          updateCRGB();
    uint8_t       getLEDCRGB(uint8_t aled);
    void          setLEDCRGB(uint8_t aled, uint8_t abrite);
  public:
    WS2811LEDChain(uint8_t achainidx, uint8_t anumleds, uint8_t aadr);
    CRGB          controllers[CONTROLLER_COUNT];
    void          addLEDObject(WS2811LED* aled, uint8_t aidx);
    bool          firstLED();			// make the first LED the current LED
    WS2811LED*    getCurrentLED();
    WS2811LED*    getLED(uint8_t aled);
    bool          hasnext();			// select the next LED and return true if it exists
    void          loop(void);
    uint8_t       getLedBrightness();  				// return the brightness value (0..255) of the current led
    void          setNominalBrightness(uint8_t abrite);
    uint8_t       getLedBrightness(uint8_t aled);	// return the brightness value (0..255) of the given led
    void          setNominalBrightness(uint8_t aled, uint8_t abrite);
    virtual void  updateFromLED(bool now) override;	// collect LED data
    void          update();							// send data to the serial bus
    void          allOff(bool animate);				// switch all LEDs off
    void          setTeaching(bool ateach);			// enter or leave teach mode
    void          blinkLed(uint8_t acount);			// blink the current LED
    void          blinkLed(uint8_t aled, uint8_t acount);
    void          load();							// load nominal brightnesses from EEPROM
    void          store();							// store nominal brightnesses to EEPROM
    virtual 	  ~WS2811LEDChain();				// destructor
};

/*******************************************************************
 *  factory method to create a new WS2811LEDChain instance and
 *  connect it to the hardware at the given pin
 *  Must be defined here to be available at compile time
 */
template <uint8_t PIN_DOUT>
WS2811LEDChain* createChain(uint8_t achainidx, uint8_t anumleds, uint8_t aadr) {
  WS2811LEDChain* newChain = new WS2811LEDChain(achainidx, anumleds, aadr);
  FastLED.addLeds<WS2811, PIN_DOUT, RGB>(newChain->controllers, CONTROLLER_COUNT);
  debug("Chain %i at pin %i for %i LEDs\n", achainidx, PIN_DOUT, anumleds);
  return newChain;
};

Die FastLED-Library ist sehr universell und daher etwas komplexer als für diesen Anwendungsfall nötig. Daher ist für das Erzeugen der Kette die Template-Methode am Ende erforderlich.

Einstellen der Helligkeit

LEDs gibt es in sehr vielen Varianten und unterschiedlichen Nennströmen. Die meisten der hier verwendeten SMD-LEDs sind bei ihrem Nennstrom von 20mA viel zu hell. Es muss ja nicht sein, dass eine Straßenlaterne am anderen Ende der Anlage immer noch Schatten wirft...
Üblicherweise wird der Strom mit einem Vorwiderstand begrenzt, der jedoch aus den genannten Gründen meist deutlich von dem berechneten/empfohlenen/mitgelieferten Wert abweicht - ein Faktor 5 ist da durchaus möglich. Und mehrfaches Umlöten, bis der richtige Wert gefunden ist, nervt irgendwann, vor allem, wenn die Beleuchtung schon eingebaut ist. Jede LED an ein eigenes Trimmpotentiometer anzuschließen, ist auch nicht optimal.
Daher nutze ich den Vorteil des WS2811, die Helligkeit über PWM vorgeben zu können.

An den Arduino werden dazu ein Taster und ein Potentiometer angeschlossen. Wird der Taster lange gedrückt, geht das Programm in den Teach-Modus und blinkt zur Bestätigung mit der ersten LED in der Kette. Deren Helligkeit kann jetzt mit dem Poti eingestellt werden. Danach wird der Taster kurz gedrückt, was den eingestellten Wert im EEPROM des Arduino speichert und zur nächsten LED springt.
Erneutes langes Drücken beendet den Einstellmodus.

Damit beim Weiterschalten die nächste LED nicht sofort auf die noch eingestellte Helligkeit der Vorgängerin springt, muss das Poti erst um einen gewissen Wert verdreht werden, bevor eine Änderung erkannt wird. So kann zur nächsten weitergeschaltet werden, ohne die Helligkeit zu ändern.

Steuerung der Raumbeleuchtung

Wenn die Beleuchtung auf der Anlage schon vom Arduino gesteuert wird, wäre es doch wünschenswert, auch die Beleuchtung der Anlage bzw. die Raumbeleuchtung einzuschließen, damit es nachts auch dunkel wird.

Wird diese über eine Infrarot-Fernbedienung gesteuert, kann man deren Signale auch vom Arduino aus erzeugen. Ein gutes Tutorial dafür findet sich z.B. in Wolles Elektronikkiste
Dazu gibt es eine umfangreiche Bibliothek, die nicht nur die Signale generiert, sondern über einen geeigneten Empfänger auch zunächst einmal erfassen und decodieren kann, damit man weiss, auf welche Codes die Beleuchtung reagiert.

Die meisten (China-)Fernbedienungen arbeiten mit 38kHz, auf die z.B. ein TSOP 31238 abgestimmt ist. Schließt man diesen an Pin D2 des Arduino, lassen sich die Codes der Fernbedienung mit dem kleinen Programm

#include <IRLibAll.h>

IRrecvPCI 	myReceiver(2);	//	create receiver and pass pin number
IRdecode 	myDecoder;   	//	create decoder

void setup() {
	Serial.begin(9600);
	delay(2000); while (!Serial);		// delay for Leonardo
	myReceiver.enableIRIn();			// Start the receiver
	Serial.println(F("Ready to receive and send IR signals"));
}

void loop() {
	//Continue looping until you get a complete signal received
	if (myReceiver.getResults()) {
		myDecoder.decode();				// Decode it
		myDecoder.dumpResults(true);	// Now print results. Use false for less detail
		myReceiver.enableIRIn();		// Restart receiver
	}
}

auslesen und anzeigen.

Die so gefundenen Codes lassen sich dann über eine verstärkte (!, siehe Bild) IR-LED an Pin D3 des Arduino an die Beleuchtung senden.
Mein Paulmann MaxLED-Dimmer lässt sich z.B. über

mySender.send(NEC, 0xFF00FF, 32);

ein- und ausschalten.