Diesen Baum bitte nicht gießen

Vor etwa zwei Jahren verschenkte ich einen kleinen LED-Baum, ein sehr einfach gehaltenes Design aus 17 weißen 0603-LEDs und etwas Kupferlackdraht. Am unteren Ende der verdrillten Einzeldrähte sind alle Wurzeln parallelgeschaltet, und in Serie zu einem LDR mit der 4.5 V-Batteriespannung verbunden. Dadurch passt sich der LED-Bonsai auf sehr natürlich wirkende Weise seiner Umgebungshelligkeit an (und verbraucht im Mittel weniger Strom).

Und tatsächlich leuchtet er immer noch mit seinem ersten Satz Batterien. Eine lange Laufzeit ist zwar grundsätzlich nicht überraschend bei der analogen Helligkeitsanpassung und der hohen Effizienz weißer LEDs, aber eine derartige Lebensdauer hatte ich trotzdem nicht erwartet.

Dieser Blogeintrag dreht sich aber nicht um dieses kleine Kupferlackdrahtgewirr. Er dreht sich um ein anderes, größeres Durcheinander von Draht und LEDs.

Der Plan

Die Idee formte sich als ich auf unwahrscheinlich kleine RGB-LEDs stieß. Mit diesen sollte ich doch mein letztes LED-Bäumchen noch ein Stück übertreffen können, oder?

Mit RGB-LEDs muss ich mich zwar von der beeindruckenden Effizienz weißer LEDs verabschieden, dafür bieten sich wesentlich mehr Möglichkeiten was Animationen angeht. Vor allem war natürlich die Herausforderung, diese winzigen LEDs in einer elektronischen Skulptur zu verwenden ein guter Grund – es sollte zwar wieder ein Geschenk werden, aber das heißt ja nicht dass ich beim Bauen nicht etwas Spaß haben konnte. Es gab keinen vorbestimmten Anlass und ich konnte etwas experimentieren anstatt alles bis zu einer Frist fertig haben zu müssen (für ein kurzfristiges Geschenk ist so eine Idee denkbar ungeeignet).

Dieser Beitrag detailliert einige Designentscheidungen, die die technische Seite so interessant machten wie das Endresultat, zumindest meiner Meinung nach. Außerdem wird es um ein paar Tricks gehen, die das flechten der Äste und Wurzeln erträglich machten.

Hardware

In der Realität war mein erster Schritt zwar die Machbarkeit meiner Ideen mit einem Prototyp der Software zu überprüfen, aber dieser Artikel liest sich etwas logischer, wenn ich mit der Hardware beginne. Daher geht es hier zuerst um den handwerklichen Teil, bevor ich über die ansteuernde Elektronik zu Softwarethemen übergehe.

LEDs an Drähte löten

So einfach es klingen mag, aber der erste und mit wichtigste Schritt ist es, die Blüten an die Zweige zu kleben. Glücklicherweise halten die oben verlinkten LEDs den Löttemperaturen gut stand, sonst wäre dieses Projekt sehr schwierig bis unmöglich geworden.

Was mit weißen LEDs nicht allzu schwer ist, wird mit RGB-LEDs gleich ein Stück aufwendiger. Bei den zwei Pins einer einfarbigen LED kann nur so viel schiefgehen: Kurzschluss, schlechte Lötstelle und eine verpolte LED sind schon alle Möglichkeiten, die alle auch bei einem Test sofort auffallen. Mit vier Pins ist die Zahl der möglichen Fehler gleich deutlich höher – manuelles Testen mit einem Multimeter wird hier schnell anstrengend.

Stattdessen baute ich einen einfachen Testsignalgenerator für das andere Ende des Kabels. Die unten aufgezeichnete Schaltung stellt sicher, dass alle drei Farben einzeln entweder an- oder abgeschaltet werden wenn eine RGB-LED richtig an ihre vier Ausgänge angeschlossen wird – egal, wie genau. Damit sind alle möglichen Fehler direkt erkennbar: Wenn eine Farbe fehlt, ist eine kalte Lötstelle das Problem, wenn mehrere immer gleichzeitig leuchten, gibt es einen Kurzschluss.

Ein 555 arbeitet als Oszillator und steuert ein Schieberegister an, das die vier phasenversetzten Ausgangssignale erzeugt. Da dessen SCK- und RCK-Eingang verbunden sind, wird der dritte Ausgang anstatt des letzten zum seriellen Eingang zurückgeführt. Ein weiteres RC-Glied sorgt für einen sauberen Reset beim Einschalten und damit dafür, dass die Signale jedes mal so aussehen wie im Bild.

In der Praxis verdrillte ich ein Drahtbündel mit einem Akkuschrauber und verband ein Ende mit der Testschaltung. Am anderen Ende konnte ich dann die vier Leiter trennen, verzinnen und mit einer LED verlöten. Sobald alles richtig verbunden ist, wird der Zweig abgelängt und der Prozess wiederholt.

Zweige und Äste

Nachdem alle LEDs verlötet waren, fixierte ich sie durch Eintauchen in Epoxidharz, um die Lötstellen mechanisch während des Äste-Biegens zu schützen. Würde ich nochmal von vorn beginnen, würde ich diesen Schritt allerdings auslassen: Zu 100% kann man die Lötstellen nicht schützen und wenn eine doch ausfällt macht die Klebeschicht ein Nachlöten absolut unmöglich. Mit eng verdrillten Drähten scheinen die Lötstellen außerdem ausreichend stabil zu sein – eine Schutzschicht ganz am Ende aufzutragen ist wohl die bessere Idee.

Matrix-Konfiguration

Mit Blick auf den Baumstamm im letzten Bild wirkt die Aufgabe, alle 128 Einzeldrähte wieder zu trennen und richtig zu sortieren, nicht gerade ansprechend. Während ich mir die Sache etwas einfacher machen könnte und je alle roten, grünen und blauen LEDs parallelschalten könnte, wollte ich mir diesmal alle Möglichkeiten offenhalten – ich wollte einzelne LEDs ansteuern können. Also gibt es nicht wirklich ein Entkommen vor dem Vereinzeln und Zuordnen.

Aber ich habe die Zahl 32 nicht ganz ohne Grund gewählt. Mit 32 RGB-LEDs lässt sich eine Matrix mit acht Zeilen und zwölf Spalten bauen. Um die Verdrahtung etwas einfacher zu machen, druckte ich eine kleine Platine mit einem einfachen Muster für die Spalten:

Die Sektoren sind der Reihe nach für die R-, G- und B-Kathoden vorgesehen. Es gibt 24, und damit zwei für jede Matrixspalte, damit man nur vier Drähte an jeden löten muss und es etwas überschaubarer wird. Die Anoden werden frei hängend zusammengelötet.

Ich ging wie folgt vor: Beginnend mit den äußersten Zweigen, trennte ich jeweils die Einzeldrähte, bog die Anode nach oben und verlötete die Kathoden mit drei R-/G-/B-Sektoren, die nicht schon vier Drähte hatten. Dabei ist es erst mal nur wichtig, die drei Kathoden einer LED zusammen zu behalten, ansonsten können die Drähte so verbunden werden wie es gerade am praktischsten ist.

Nachdem alle Kathoden mit den Sektoren verbunden waren, dürfen jeweils zwei zusammen eine Spaltenleitung formen. Danach verlötete ich vier zufällige Anoden zu einer Zeile (weshalb ich sie vorher nach oben gebogen habe). Die Kathodendrähte sorgen für ausreichend mechanische Stabilität, die Anoden können also problemlos frei in der Luft hängen.

Die Zeilen und Spalten bekamen jeweils einen Stecker, damit ich die ansteuernde Platine später einfach abstecken konnte. So zusammengebaut sieht es etwas wild aus, aber der Prozess war durchaus erträglich.

Perfekt! Damit haben wir nun eine LED-Matrix, die wie ein Baum aussieht, der stabil auf einer Basisplatte steht. Die einzelnen Pixel sind zwar zufällig verteilt, aber das ist später in Software einfacher zu beheben als während des Baus auf jede einzelne LED und ihre Position zu achten.

LED-Treiberelektronik

Zur Ansteuerung verwendete ich einen kleinen ARM Cortex-M0 (womit die versprochene interessante technische Seite langsam ins Spiel kommt). Die Zeilen werden über ein Schieberegister getrieben, weil sonst die Pins nicht gereicht hätten.

Ein alter Freund taucht wieder auf: Der STM32F030F4P6 treibt die Spalten direkt und die Zeilen über einen 74HCT595 und acht Treibertransistoren. Die HC-Variante würde wahrscheinlich ebensogut funktionieren, nachdem die Schaltung maximal mit 4,4 V arbeitet. Die LED-Vorwiderstände wurden empirisch bestimmt und sorgen für ein angemessenes weiß über den Betriebsspannungsbereich.

Alles passt auf die Prototypenplatine aus dem letzten Blogeintrag. Ich nahm hier eine unbestückte Platine und verlötete nur die notwendigen Teile, etwa fehlt der microSD-Sockel, der dem Spaltenstecker im Weg gewesen wäre.

Nachdem der komplette Port A für die Spalten der Matrix verwendet wird, sind leider auch die SWD-Pins belegt. Damit ist ein Debuggen nicht möglich1, der Controller kann aber weiterhin programmiert werden, indem man unter Reset verbindet.

Stromversorgung

Im letzten Bild sind vielleicht die beiden Dioden und ein SO-8-Chip aufgefallen, die im Schaltbild noch fehlen. Das ist im Grunde auch schon alles was die Stromversorgung angeht: Der Baum wird entweder über USB oder eine LiPo-Zelle betrieben, wobei eine Ladeschaltung für letztere auch vorgesehen ist.

Mit einer ideal diode wären ein paar Joules mehr aus der Batterie zu holen, aber die Schottkydiode hier war da und gut genug.

Gehäuse

Das Gehäuse, ein einfacher Pyramidenstumpf, besteht aus Platinenmaterial. Die Matrix-Platine wurde dazu einfach mit den Seitenteilen verlötet und formt so die Oberseite.

Software

Die LEDs mit einem relativ kleinen Mikrocontroller direkt anzusteuern macht natürlich die Software interessanter, nachdem es nicht genug Hardware-Pulsweitenmodulationskanäle für alle zwölf Spalten gibt. Alle zeitkritischen Signale für die Helligkeitssteuerung müssen also in Software generiert werden, während der Controller noch andere Aufgaben, wie die Messung der Umgebungshelligkeit und Animation der Pixel, zu erledigen hat.

Das Ziel waren hier sanfte Übergänge mit einer globalen Helligkeitseinstellung. Mit einfacher Gamma-Korrektur2 wollte ich daher mindestens 12 Bit Auflösung, sodass die einzelnen Schritte nicht sichtbar sind.

Eine Art PWM

12 Bit sind nicht wenig, wenn man auch mit dem Zeilen-Scan noch eine hohe Bildwiederholrate erreichen will – ohne Hardware-PWM-Erzeugung jedenfalls. Vermutlich wäre das auch mit einer naiven Implementierung mit Zählern und Vergleichen für jeden Kanal möglich gewesen, indem alle anderen Berechnungen in ein kurzes Pausenintervall gelegt werden. Ich schlug dennoch einen anderen Weg ein.

Eine Periode des BCM-Signals, hier mit 5 Bit Auflösung.

Die Idee hinter binary code modulation (BCM) ist es, die Periode eines PWM-Signals in sinnvolle „Segmente“ aufzuteilen. Verwendet man Zweierpotenzen, lässt sich jede gesamte Einschaltdauer durch gezieltes Ein- und Ausschalten während ausgewählter Segmente erreichen.

Jedes Bit gehört dabei zu einem dieser Segmente. Beispielsweise, im 5-Bit-Fall wie oben, wäre für einen binären Wer 10011 (dezimal 19) das Signal für 16 Zeiteinheiten 1, für 8 + 4 wäre es 0 und für 2 + 1 wieder 1. Zusammengerechnet ist das Signal also für 19 Takte an, genau wie gewünscht.

Aus Implementierungssicht ist so ein Signal auch wesentlich einfacher zu erzeugen als ein konventionelles PWM-Signal: Anstatt dass der Wert der Ausgänge in jedem Taktzyklus mit einem Vergleich pro Kanal bestimmt werden muss, sind die Bits schon da, sie müssen nur zu den richtigen Zeitpunkten ausgegeben werden. Nachdem die Zeitslots für alle Kanäle synchron sind, können alle Werte einfach vorberechnet und parallel ausgegeben werden.

Eine effiziente BCM-Implementierung

Die Firmware verwendet an einigen Stellen den direct memory access (DMA) des Mikrocontrollers, der im Hintergrund Daten zwischen Speicher und/oder Peripherieregistern hin- und herschieben kann, ohne dass (abgesehen von einer kurzen Initialisierung) die CPU aktiv werden muss. Hier schiebt der DMA etwa 16-Bit-Worte aus einem Array im Speicher in das Ausgaberegister von Port A, der mit den LEDs verbunden ist, und setzt somit alle LEDs auf einmal auf ihre neuen Werte.

Dieser Array wird davor aus den Helligkeitswerten berechnet und enthält Bit 0 jeder LED, dann, als nächsten Eintrag, Bit 1 jedes Helligkeitswerts, danach Bit 2, und so fort. Damit ist die Ausgabe der Ein-Aus-Sequenz abgedeckt – im Hintergrund und für alle Ausgänge exakt gleichzeitig.

Bleibt die Frage nach dem Timing. Auf sich allein gestellt könnte der DMA den Array-Inhalt so schnell wie möglich übertragen, was aber nicht viel hilft, da dann alle Bits gleich lang wären anstatt kontrolliert gewichtet. Glücklicherweise kann man den DMA mit einem Timer in Gang setzen, wodurch jeweils ein Array-Element übertragen wird sobald der Timer einen festgesetzten Zählerstand erreicht, also nach einer genau bestimmbaren Zeit.

Um nun die verschiedenen Längen für alle Bits zu erreichen, braucht es einen zweiten DMA-Request: Ausgelöst genau nachdem das GPIO-Ausgaberegister seinen neuen Wert bekommen hat, überschreibt dieser die Konfiguration des Timers und ersetzt die Zeit bis zum nächsten DMA-Request mit der nächsten Zweierpotenz. Damit kann also auch dieser Teil komplett im Hintergrund ausgeführt werden.

Nach einem BCM-Zyklus, in dem zuletzt alle Ausgänge per DMA abgeschaltet werden, wird ein Interrupt ausgelöst. In dessen Serviceroutine wird das Schieberegister um eine Position weitergeschoben und die DMA-Signalerzeugung für die nächste Zeile wird gestartet. Das Schieberegister-Signal könnte man theoretisch auch per DMA erzeugen, aber jetzt ist die CPU-Last nicht mehr so kritisch.

Die ganzen Transaktionen werden natürlich nicht instantan ausgeführt. Es gibt eine leichte Verzögerung vom Auslösen durch den Timer bis zum Schreiben der neuen LED-Werte. Nachdem aber die LEDs per DMA sowohl an- als auch abgeschaltet werden und die Verzögerung jeweils gleich ist, taucht diese nicht weiter auf. Wenn der DMA allerdings noch für etwas anderes verwendet werden würde, etwa um Kommunikationsdaten hin- und herzuschieben, wäre die Verzögerung nicht so gut vorhersehbar. Die leichte, zufällige Latenz wenn der DMA zum Beispiel gerade ein Byte von einem seriellen Interface abholen muss während er die LED-Ausgänge setzen sollte, würde schnell zu sichtbarem Jitter führen. In anderen Worten: Dieses System funktioniert nur, weil der DMA für nichts anderes verwendet wird und alles deterministisch ist.

Als kleine Ausnahme werden die ersten, niedrigstwertigen, vier Bits mit etwas Assemblercode erzeugt und nicht über den DMA ausgegeben. Bit 0 ist lediglich zwei CPU-Taktzyklen lang und auch die 16 Takte von Bit 3 sind zu kurz, als dass Timer und DMA mitkommen könnten.

Helligkeitssteuerung

Wenn alle LEDs immer mit voller Helligkeit leuchten würden, würde der Akku nicht sonderlich lange halten; ein unnötig hell strahlender Baum in der Abenddämmerung wäre ohnehin nicht sehr hübsch. Also braucht es noch eine Anpassung an die Umgebungshelligkeit, wozu ich einen SFH320-Phototransistor an PB1 (den einzigen noch freien Pin) anschloss.

Zuerst wollte ich mit einem Widerstand zur Versorgungsspannung einen Spannungsteiler bilden und den Sensor mit dem ADC des Controllers auswerten (wer genau hinsieht, findet den 0603-Widerstand noch auf dem obigen Bild der Platinenrückseite). Stattdessen verwendete ich am Ende einen 10 nF-Kondensator parallel zum Phototransistor (und keinen Pullup-Widerstand). Die Helligkeitsmessung erfolgt über die Zeit, die der Phototransistor zum Entladen des Kondensators braucht. Diese Variante hat den Vorteil, dass ein größerer Bereich abgedeckt werden kann, weil die maximale Entladezeit in Software angepasst werden kann, ein externer Widerstand aber nicht.

Die Software benutzt damit die selbe Methode, die man auch für eine LED in Sperrrichtung als Helligkeitssensor verwenden würde (normalerweise ohne den externen Kondensator, nachdem der Photostrom der LED wesentlich geringer ist). Mit einer Anpassung der Helligkeitsgrenzen im Quelltext sollte eine LED daher den Phototransistor ersetzen können.

Ordnung ins Chaos

Wir kommen damit den letztendlichen Animationen auf der 3D-LED-Matrix näher. Ein Thema fehlt aber noch bis dahin: Für jede Animation außer einem zufälligen Funkeln, für alles bei dem die LED-Position eine Rolle spielt, muss noch bestimmt werden, wo jeder Punkt in der Matrix in der physischen Welt ist.

Ein paar Abschnitte weiter oben schrieb ich, dass es wesentlich einfacher wäre, die LEDs später in Software umzusortieren als die Matrix genau so zu verdrahten, dass die 3D-Position in einem logischen Verhältnis zu den Matrixkoordinaten steht. Während das so stimmt, ist es immer noch etwas Arbeit, die passende Zuordnung zu bestimmen. Ohne Debuggingmöglichkeiten konnte ich keine Pixelwerte ändern während die Software läuft und ohne freie Pins ist es auch nicht möglich, per Knopfdruck durch alle Positionen zu gehen.

Ich überlegte kurz, einen einfachen UART-Sender auf die Matrixansteuerung zu setzen: Jede LED würde damit ihren Index aussenden, den ich dann mit einem optischen Empfänger einlesen konnte. Ich setzte aber lieber auf ein einfaches Testmuster, das diese Information über sichtbares Blinken, mit der Farbe und Anzahl an Lichtblitzen als Merkmale, überträgt. Hier mein etwas unordentlicher Code dafür:

unsigned int counter = 0;
const unsigned int blink_period = 1000;
const unsigned int group_size = 6;
while(1)
{
    if(LED_FrameFlag)
    {
        memset(LED_PixelData, 0, sizeof(LED_PixelData));

        for(unsigned int i = 0; i < LED_COUNT; i++)
        {
            uint8_t brightness = 0;
            if((counter / blink_period) <= (i % group_size))
            {
                if(counter / (blink_period / 2) % 2)
                {
                    brightness = 30;
                }
            }

            switch(i / group_size)
            {
                case 0:
                    LED_PixelData[i].r = brightness;
                    break;
                case 1:
                    LED_PixelData[i].g = brightness;
                    break;
                case 2:
                    LED_PixelData[i].b = brightness;
                    break;
                case 3:
                    LED_PixelData[i].r = brightness;
                    LED_PixelData[i].g = brightness;
                    break;
                case 4:
                    LED_PixelData[i].g = brightness;
                    LED_PixelData[i].b = brightness;
                    break;
                case 5:
                    LED_PixelData[i].b = brightness;
                    LED_PixelData[i].r = brightness;
                    break;
            }

            counter++;
            if(counter > blink_period * (group_size + 2))
            {
                counter = 0;
            }
        }

        LED_FrameFlag = false;
        LED_Commit();
    }
}

Mit dem blinkenden Baum vor mir konnte ich dann den Index jeder LED (in der Reihenfolge, in der ich sie am Ende haben wollte), in einen neuen Array eintragen.

Für die geplante Animation reichte mir diese 1D-Zuordnung aus, aber die beschriebene Methode funktioniert in höheren Dimensionen ebenso. Man kann sich etwa vorstellen das Blinken von oben zu filmen und ein Koordinatengitter darüberzulegen, sodass man die 2D-Positionen der LEDs bestimmen kann.

Animation

Von Beginn an wollte ich die Animation sehr subtil halten. Sehr subtil. Nichts auffälliges oder grelles, keine sprunghaften Änderungen. Ein einfacher, sich langsam ändernder Farbverlauf von unten nach oben war genug.

Ein kleines Python-Skript erzeugt eine Zuordnungstabelle für den späteren Verlauf im HSV-Raum mit einem Farbton „oben“ und „unten“, bei konstanter Sättigung und Helligkeit (die später von der Umgebungshelligkeitsanpassung korrigiert wird):

#!/usr/bin/env python

from colorsys import hsv_to_rgb

LUT_SIZE = 256
SATURATION = 0.8
VALUE = 1
RESOLUTION = 12     # in bits

print('const LED_Colour_t Animation_ColourLUT[] =')
print('{')
for i in range(LUT_SIZE):
    h = i / LUT_SIZE
    rgb = hsv_to_rgb(h, SATURATION, VALUE)
    # Gamma-correct
    rgb = tuple(pow(x, 2.2) for x in rgb)
    # Scale to BCM resolution
    (r, g, b) = tuple(round(2 ** RESOLUTION * x) for x in rgb)
    print('    {{ .r = {}, .g = {}, .b = {} }},'.format(r, g, b))
print('};')

Der Programmspeicher ist hier nicht kritisch, also hätte die Tabelle auch größer ausfallen können. 256 Einträge erwiesen sich aber als absolut ausreichend – es sind keine Stufen sichtbar, auch ohne zusätzliche Interpolation zwischen Tabelleneinträgen.

Die Endpunkte des Verlaufs verändern sich langsam, aber mit unterschiedlichen Geschwindigkeiten, sodass sich die Animation nicht zu schnell wiederholt. Um plötzliche Sprünge oder einen „mehrfach gewundenen“ Verlauf zu verhindern, kehrt sich die Richtung um, sobald der Farbton das Ende der Skala erreicht. Das oberste Pixel umrundet den Farbtonkreis (in einer Richtung) in ungefähr zehn Tagen, während das unterste etwa eine Woche braucht3. Mit beiden zusammen hat die Animation also eine Gesamtdauer von etwa 39 Tagen.

Ja, das ist sehr langsam, und das ist Absicht. Es dient zum einen als netter Überraschungseffekt, da die LEDs nicht sofort als Vollfarb-LEDs erkenntlich sind. Zum anderen ist die langsame Änderung kaum merklich, wenn man in der Nähe ist, aber man sieht morgens dennoch eine andere Färbung als abends zuvor.

Stromsparmaßnahmen

Batteriebetrieb heißt dass jedes Milliampere konstanten Stromverbrauchs sich auf die Laufzeit auswirkt und damit auch auf den Spaß, dem langsamen Farbwechsel zuzusehen. Wenn der Baum nicht ständig am Ladegerät hängen müssen soll, ist ein wenig Aufwand zur Senkung des Stromverbrauchs es durchaus wert.

Als erste und einfachste Maßnahme testete ich, wie weit ich die Taktfrequenz reduzieren konnte ohne dass die Helligkeitsmodulation sichtbar wurde. Bis zu diesem Zeitpunkt lief der Controller mit seinem maximalen Takt von 48 MHz, womit der ganze Baum bei der niedrigsten noch sichtbaren LED-Helligkeit etwa 13 mA brauchte. Ich deaktivierte die PLL, was die Taktfrequenz auf ein sechstel senkte und den Stromverbrauch auf etwa 3 mA. Die Wiederholrate sank auf 230 Hz, was geradeso nicht mehr sichtbar war, weshalb ich die Taktfrequenz nicht noch weiter senken wollte4.

Ein weiterer großer Sprung war es, die Matrixansteuerung nachts zu deaktivieren. Sobald die LED-Helligkeit Null erreicht, wird die Ansteuerung der Zeilen und Spalten komplett deaktiviert, und so auch die internen DMA-Transfers. Dadurch sinkt die Stromaufnahme sofort gut unter 1 mA.

Die Lichtsensor- und Animationsroutinen werden allerdings immer noch in den gleichen Abständen aufgerufen. Ich hätte hier noch etwas mehr Strom sparen können, indem ich den CPU-Takt weiter gesenkt hätte, sobald die Matrix abgeschaltet ist. Allerdings ist die Optimierung im Mikroamperebereich den Aufwand wohl kaum wert für ein Gerät, das jeden Tag stundenlang leicht 50 mA verbraucht.

Mit seinem 2000 mAh-LiPo leuchtet der Baum nun für etwas mehr als drei Wochen, was meiner Meinung nach nicht ganz schlecht ist für etwas, das hauptsächlich aus ständig leuchtenden LEDs besteht.

Wo sind wir stehengeblieben?

Drei Wochen Batterielaufzeit und 39 Tage Animation stehen allerdings immer noch in deutlichem Widerspruch. Nachdem die gesamte Elektronik die Stromversorgung verliert, wenn die Batterie am Ende ist, bekommt man maximal die ersten zwei Wochen der Animation mit. Nachdem es keinerlei Anzeige für einen nahezu leeren Akku gibt (und ein blinkender Baum als Anzeige steht außer Frage), ist ein regelmäßiges Aufladeintervall als Voraussetzung unrealistisch. Weitere Stromsparmaßnahmen könnten noch ein paar Tage mehr Laufzeit bringen, würden den Baum aber wohl kaum bis zu den ganzen 39 Tagen tragen. Ich könnte natürlich die Animation schneller machen, aber ihr Tempo gefiel mir bisher ganz gut. Zudem wird das Problem ja nicht besser, wenn der Akku altert und seine Kapazität abnimmt.

Um dem aus dem Weg zu gehen möchte ich die Animation dort fortsetzen, wo sie war als der Baum zuletzt Strom hatte. Dafür kopierte ich eine EEPROM-Emulation von einem Beispielprojekt, das ich zuvor geschrieben hatte. Damit konnte ich den aktuellen Stand jede Stunde abspeichern und beim Einschalten (Laden des Baums mit leerer Batterie) wieder abrufen.

Nachdem der Flash nur Page-weise gelöscht werden kann, hat der Code grundlegendes Wear-Levelling: Der aktuelle Status wird an verschiedenen Stellen innerhalb einer 1 KiB-Page geschrieben, bis diese voll ist und komplett gelöscht werden muss.

Da die CPU während des Löschvorgangs keine Instruktionen aus dem Flash holen kann, wird die Ausführung der Firmware kurz pausiert. Nachdem alle LED-Signale in Software erzeugt werden, ist dies leider als kurzes Aufblitzen der LEDs sichtbar. Auch wenn das nur sehr selten passiert, wollte ich einen derartig offensichtlichen Glitch nicht in der Firmware lassen. Als einfachen Ausweg wird Flash nur an Tag/Nacht-Übergängen gelöscht, während alle LEDs sowieso aus sind.

Abschließende Worte

Das war jetzt doch eine Menge verschiedener Themen. Das ganze war kein Wochenendprojekt und ging über einen Zeitraum von eineinhalb Jahren immer wieder ein wenig voran. Jetzt, nach aller Erkundung und Optimierung, könnte es auch als Last-Minute-Geschenk machbar sein. Aber keine Garantie!

Der fertige Baum, im Zeitraffer. Ohne den Faktor 100000 säße man hier eine Weile.

Der Code zum Projekt findet sich auf GitHub.


  1. Jedenfalls nicht mit der ganzen Matrix. Wenn man die letzten drei Spalten (und somit acht RGB-LEDs) auslässt, ist Debuggen wieder möglich. ↩︎

  2. Zuerst hatte ich eine einfache Gamma-Korrektur in der LED-Ansteuerung (jeder Farbwert wurde quadriert und die obersten 12 Bits hergenommen). Nachdem mir auffiel, dass ich die globale Helligkeitsanpassung entweder nach der Gammakorrektur durchführen oder die Wurzel des Messwerts ziehen müsste, verschob ich die Gamma-Korrektur einfach in die Animations-Farbtabelle. ↩︎

  3. Die genaue Zeit unterscheidet sich etwas von der im Quelltext angegebenen, weil alles mit Ganzzahlarithmetik arbeitet (keine FPU in diesem Controller!) und die Schrittgrößen noch gerundet werden. ↩︎

  4. Was auch heißt dass 14 Bit Auflösung kein großes Thema wären, wenn es auf den Stromverbrauch nicht ankäme. ↩︎