Seit der OHM2013 habe ich immer wieder auf Chaos Events LED-Fackeln gesehen, die von einem Mikrocontroller angetrieben wurden. Häufig basieren diese auf Arduino-Development-Boards, bzw. Chips wie sie bei dort zum Einsatz kommen.

Leider bin ich nie dazu gekommen, dahingehend Kontakte zu knüpfen und mich über die Funktionsweise zu informieren – bis zum 35C3 dieses Jahr.

Ermutigt durch den (erfolgreichen) Bau meines SmartMirrors, dachte ich mir, dass es nun endlich mal angegangen werden sollte.

Arduino LED Fackel

Vorbereitungen

Benötigt werden für ein derartiges Projekt LED Streifen mit WS2812(b) oder APA102 Chips, bzw. SK6813 in den LEDs. Diese ermöglichen die Ansteuerung der einzelnen LEDs mit Bitshift-Register über eine Datenleitung, bzw. 2 für SPI (für die APAs/SKs).

Ein vollkommen überdimensionierter Arduino MEGA 2560 (mit Ethernet-Shield) lag noch in einer Schublade. Dieser ist natürlich viel zu groß und leistungsstark für so ein primitives Projekt, zum Code und Hardware testen reicht es aber allemal. Für das fertige Produkt habe ich 3 Arduino nanos mit Atmega 328P bestellt. Diese sind hinreichend klein, dass er später in der Facker verbaut werden kann. LED Reste zum Testen würden sich bestimmt bei den warpzone-Mitgliedern abschnorcheln lassen.

Fehlt nun noch die Software. Ich halte mich wohl für fähig, Code zu lesen, habe aber wenig Routine im Programmieren und tatsächlich außer einfachen Skripten noch nie etwas vollständig selbst gemacht.

Daher ließ ich mich von diesen beiden Projekten inspirieren, welche beide auch die WS2812er verwenden:

Das war für mich Grund genug, nicht die teureren APA102s zu kaufen. Die werde ich dann möglicherweise in Zukunft™ für abgefahrenere Sachen verwenden, die schnellere Schaltung benötigen.

Entwicklungsumgebung einrichten

Sublime Text mit geöffneter Deviot Konsole

Zuallererst brauche ich eine vernünftige Entwicklungsumgebung. Ich mag die Arduino IDE nicht. Sie ist bloatige Kackscheisse und kann nix. Ich verwende Sublime Text seit einigen Jahren und etwas Vollintegriertes hätte ich schon ganz gerne.

Package Control liefert unter dem Stichwort „Arduino“ 2 sinnvolle Treffer: „Arduino-like IDE“, welche Teile der offiziellen IDE verwendet, aber seit 1,5+ Jahren nicht mehr aktualisiert wurde und Deviot.

Letzteres basiert auf Platform.io und zieht alle Abhängigkeiten vollautomatisch nach. Außerdem bietet es eine Paketverwaltung für Bibliotheken und Libraries. Nach der Installation war ich sofort in der Lage einen Testsketch zu kompilieren und auf den Mega hochzuladen. Außerdem gibt es – wie von Sublime gewohnt – vernünftige Auto-completion und Syntaxhighlighting.

Arduino Testsetup

Testsetup mit WS2812B Streifen - nur echt mit Spule for scale.
Testsetup mit WS2812B Streifen – nur echt mit Spule for scale.

Mit den zusammengeschnorrten LEDs kann ich nun die Software testen – soweit möglich mit 16 LEDs. Wie man sieht ist die Schaltung denkbar einfach. Die Spannungsversorgung für die kleine Anzahl an LEDs ziehe ich über den Arduino direkt aus dem USB-Port des Notebooks. Das wird mit deutlich mehr LEDs nicht mehr gehen, dann benötigt man eine externe Quelle.

Anfangs hatte ich Probleme, den Sketch von Simon zu bauen, weil der eine angepasste NeoPixel libary voraussetzte. Diese Anpassungen waren nötig, um Probleme auf den Atmega32u4 zu umschiffen. Leider waren diese nicht dokumentiert. Auf Nachfrage teilte er mir mit, dass er mittlerweile einen besseren Workaround habe, und pflegte diesen auch prompt im git-Repository ein.

Simons Sketch unterstützt auch einen Taster, der – gegen GND gezogen – die Animationspresets durchschaltet.

Im Sketch müssen mindestens die folgenden Parameter passend gesetzt werden:


1
2
3
4
5
6
7
#define PIN_BUTTON   3      // Input pin für Button
#define PIN_LED      6      // Output pin für Led-Strip
#define NUM_PIXELS (5 * 60) // Anzahl der LEDs

// Framebuffer-Dimensions. Depends on the tube radius
#define FLAME_WIDTH  5      // LEDS pro Zeile
#define FLAME_HEIGHT 27     // Anzahl der Zeilen

Migration auf Arduino nano

Die Arduino Klone kamen mit lose in der Packung liegenden Stiftleisten. Bei einem der 3 habe ich diese für die weiteren Tests und Anpassungen angelötet. Bei den 2 für die finalen Fackeln, werde ich die paar Kabel direkt am PCB fest löten.

Da ich den Sketch mit Simons Hilfe bereits auf dem Mega kompilierfähig gemacht hatte, war die Migration auf den Arduino nano mit seinem Atmega 328P sehr einfach. Ich musste lediglich ein neues Board als Zielplattform und den passenden Serial Port auswählen. Nun wird die Rekompilierung und der Upload mit „Deviot: Hochladen“ angestoßen.

Dann kann man den nano nach diesem Schema verkabeln (Pins passend zum Sketch).

Arduino nano Schaltung

Bau der Fackel

Als Träger habe ich etwas günstiges, wetterfestes im lokalen Baumarkt gesucht. Gefunden habe ich HT-Rohr mit 50mm Durchmesser. Das praktische dran ist, dass es auch passende wasserdichte Verschlusskappen gibt. Rohr und Abdeckung kosten jeweils weniger als 1€.

Die Fackel wird gebaut, indem man den LED-Streifen von oben herab auf das Rohr aufwickelt. Ich habe alle paar Windungen den Streifen auf dem Rohr mit Heißkleber fixiert. Damit der Kleber besser haftet, habe ich das Rohr zuvor mit feinem Schleifpapier angeraut. Der Eingang des Datensignals (der dunkle Teil der Fackel) muss dabei oben sein.

Der Spannungsabfall auf den 5m PCB ist so hoch, dass ich an beiden Enden einspeisen muss, damit die Fackel homogen leuchtet. Daher habe ich mich dazu entschieden, das Rohr ober- und unterhalb des aufgewickelten LED-Streifens seitlich anzubohren und die Kabel so nach innen zu führen. Die Löcher dichte ich später wieder mit Silikon oder Heißkleber ab.

An die offen liegenden Drähte (D3, GND) kann der Taster angeschlossen werden
An die offen liegenden Drähte (D3, GND) kann der Taster angeschlossen werden

Oben habe ich den Arduino im Rohr versteckt. Dort ist Vin und GND mit den dünnen Kabeln des LED Streifens (da, wo der Stecker aufgecript war) verlötet. Das Datenkabel ist ebenfalls direkt mit dem PIN_LED verbunden. Die dickeren Kabel an beiden Enden habe ich jeweils verlängert und nach unten geführt.

Hinweise zur Stromversorgung

Da der Voltage Drop auf mehrere Meter Zuleitung nicht unerheblich ist, ist es evtl. sinnvoll einen Step-Down Wandler unten in die Fackel zu setzen und die Zuleitung mit höherer Spannung zu realisieren. Beachten sollte man ebenfalls, dass je nach Farbe, Helligkeit und Anzahl der leuchtenden LEDs 30-40W Leistung benötigt werden. Der Wandler sollte also entsprechend dimensioniert sein. Die Fackelanimation selbst liegt bei ~10W. Dies könnte so gerade ein 2A Handyladegerät liefern. Sinkt die Spannung am Arduino aber auf unter ~3,3V, crasht dieser und die Animation stoppt.

Hat die Fackel die gewünschte Größe, kann man LED Streifen kürzen und / oder das Rohr absägen. Ich lasse das Rohr bewusst länger, da ich die Fackeln direkt am Balkon an der Brüstung befestigen möchte.

Quellcode

Auch wenn es Sinn macht, den aktuellen Code aus Simons Repo zu beziehen, dokumentiere ich hier den Stand zur Zeit der Artikelerstellung. Ich habe zwar quick-and-diry einige Animationen ergänzt, dennoch bin ich mir nicht sicher, ob ich Euch mit meinem Spaghetti-Code belasten möchte


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
/*
 * Flaming Torch  (c) 2013-2019 Simon Budig <simon@budig.de>
 */

#include <EEPROM.h>
#include <Adafruit_NeoPixel.h>

#define MIN(x, y) ((x) < (y) ? (x) : (y))
#define MAX(x, y) ((x) > (y) ? (x) : (y))

#define PIN_BUTTON   3  // Input pin für Button
#define PIN_LED      6  // Output pin für Led-Strip
#define NUM_PIXELS (5 * 60)

#define NUM_MODES 6

// Framebuffer-Dimensions. Depends on the tube radius
#define FLAME_WIDTH  5
#define FLAME_HEIGHT 27

// Intensity buffer for flames and sparks
static uint16_t flamebuffer[FLAME_HEIGHT][FLAME_WIDTH] = { { 0, }, };
static uint16_t sparkbuffer[FLAME_WIDTH] = { 0, };

// Gamma-Lookup-Table
static uint8_t glut[256];

// Neo-Pixel Framebuffer
Adafruit_NeoPixel pixels = Adafruit_NeoPixel (NUM_PIXELS, PIN_LED,
                                              NEO_GRB | NEO_KHZ800);

// Function to show the torch flame
void
render_flame ()
{
  uint16_t i, val;
  uint8_t x, y;

  // Random values at the bottom end, random seeded sparks
  for (x = 0; x < FLAME_WIDTH; x++)
    {
      val = rand() & 0xff;
      val = (val * val) >> 8;
      flamebuffer[FLAME_HEIGHT-1][x] = val;

      if (sparkbuffer[x] == 0)
        {
          if (rand() % 512 == 0)
            sparkbuffer[x] = FLAME_HEIGHT-1;
        }
      else
        {
          sparkbuffer[x] -= 1;
        }
    }

  // propagate enegy and blur. Damping is a fiddle factor.
  for (y = 0; y < FLAME_HEIGHT-1; y++)
    {
      for (x = 0; x < FLAME_WIDTH; x++)
        {
          val  = flamebuffer[(y+1) % FLAME_HEIGHT][x];
          val += flamebuffer[(y+1) % FLAME_HEIGHT][(x+1) % FLAME_WIDTH];
          val += flamebuffer[(y+1) % FLAME_HEIGHT][(x+FLAME_WIDTH-1) % FLAME_WIDTH];
          val += flamebuffer[(y+2) % FLAME_HEIGHT][x];
          val <<= 5;
          val /= 140;

          flamebuffer[y][x] = val;
          if (sparkbuffer[x] && sparkbuffer[x] == y)
            flamebuffer[y][x] = 255;
        }
    }

  for (i = 0; i < NUM_PIXELS; i++)
    {
      val = flamebuffer[i / FLAME_WIDTH][i % FLAME_WIDTH];
      val = MIN (255, val * 3);

      pixels.setPixelColor (i,
                            glut[val],
                            glut[val * 3 / 4],
                            glut[val * 3 / 8]);
    }
}


void
render_blueyellow (const uint16_t t)
{
  uint16_t i;
  uint8_t pos;

  for (i = 0; i < NUM_PIXELS; i++)
    {
      pos = (t + i) % 64;
      if (pos < 32)
        pixels.setPixelColor (i, 255, 200, 0);
      else
        pixels.setPixelColor (i, 0, 0, 255);
    }
}


void
render_rainbow (const uint16_t t)
{
  uint16_t i;
  uint8_t pos, pos2;

  for (i = 0; i < NUM_PIXELS; i++)
    {
      pos = (t + i) % 255;
      pos2 = (pos % 85) * 3;
      if (pos < 85)
        pixels.setPixelColor (i, glut[pos2], glut[0], glut[255 - pos2]);
      else if (pos < 170)
        pixels.setPixelColor (i, glut[255 - pos2], glut[pos2], glut[0]);
      else
        pixels.setPixelColor (i, glut[0], glut[255 - pos2], glut[pos2]);
    }
}


void
render_redblue (const uint16_t t)
{
  uint16_t i;
  uint8_t pos;

  for (i = 0; i < NUM_PIXELS; i++)
    {
      pos = (t + i) % 400;
      if (pos < 85)
        pixels.setPixelColor (i, 0, 0, glut[pos * 3]);
      else if (pos < 136)
        pixels.setPixelColor (i, glut[255 - ((pos - 85) * 5)], 0, 0);
      else
        pixels.setPixelColor (i, 0, 0, 0);
    }
}


void
render_kitt (const uint16_t t)
{
  static uint8_t basecolor = 0;
  uint16_t i;
  uint16_t pos, pos2, p;
  uint8_t *pixdata = pixels.getPixels ();

  pos = (t*2) % (NUM_PIXELS * 2 - 2);
 
  for (i = 0; i < NUM_PIXELS * 3; i++)
    {
      pixdata[i] = (((uint16_t) pixdata[i]) * 7) / 8;
    }

  if (pos >= NUM_PIXELS)
    p = 2 * NUM_PIXELS - 2 - pos;
  else
    p = pos;
   
  basecolor = basecolor + 1 + 0 * ((rand() % 12) + 249) & 0xff;
  pos = (t*1) % 256;
  pos = basecolor % (85*3);
  pos2 = (pos % 85) * 3;
  if (pos < 85)
    {
      pixels.setPixelColor (p,   glut[pos2], glut[0], glut[255 - pos2]);
      pixels.setPixelColor (p+1, glut[pos2], glut[0], glut[255 - pos2]);
    }
  else if (pos < 170)
    {
      pixels.setPixelColor (p,   glut[255 - pos2], glut[pos2], glut[0]);
      pixels.setPixelColor (p+1, glut[255 - pos2], glut[pos2], glut[0]);
    }
  else
    {
      pixels.setPixelColor (p,   glut[0], glut[255 - pos2], glut[pos2]);
      pixels.setPixelColor (p+1, glut[0], glut[255 - pos2], glut[pos2]);
    }
}


void
render_rgbsparks (const uint16_t t)
{
  uint8_t x, y;

  // Random values: factor 3 differentiates between R/G/B

  x = rand() % FLAME_WIDTH;
  y = rand() % FLAME_HEIGHT;
  flamebuffer[y][x] = rand() % (255 * 3);

  for (y = FLAME_HEIGHT; y > 0; )
    {
      y--;

      for (x = 0; x < FLAME_WIDTH; x++)
        {
          switch (flamebuffer[y][x] % 3)
            {
              case 0:
                pixels.setPixelColor (y * FLAME_WIDTH + x,
                                      glut[flamebuffer[y][x] / 3], 0, 0);
                break;
              case 1:
                pixels.setPixelColor (y * FLAME_WIDTH + x,
                                      0, glut[flamebuffer[y][x] / 3], 0);
                break;
              case 2:
                pixels.setPixelColor (y * FLAME_WIDTH + x,
                                      0, 0, glut[flamebuffer[y][x] / 3]);
                break;
            }

          // Deal with multiples of three, this ensures the same base color
          // the condition here is false always, if enabled this makes the
          // colorful sparks go up.
          if (t % 6 == 7)
            {
              if (y > 1)
                flamebuffer[y][x] = MAX (9, flamebuffer[y-1][x]) - 9;
              else
                flamebuffer[y][x] = 0;
            }
          else
            {
              flamebuffer[y][x] = MAX (9, flamebuffer[y][x]) - 9;
            }
        }
    }
}


// Arduino init.

void
setup ()
{
  uint16_t i;
  uint8_t state;
  float rf;

  // calculate Gamma-Table
  for (i = 0; i < 256; i++)
    {
      rf = i / 255.0;
      rf = pow (rf, 2.2);
      glut[i] = 255.0 * rf;
    }

  // Button Pin Input, internal Pullup
  pinMode (PIN_BUTTON, INPUT_PULLUP);

  // initial button test to make it possible
  // to skip modes taking too much power (--> reset

   if (!digitalRead (PIN_BUTTON))
     {
       state = EEPROM.read(0);
       state = (state + 1) % NUM_MODES;
       EEPROM.write (0, state);
     }

  // initialize Neopixel library
  pixels.begin();
}


// Arduino Loop function. Repeats continuously

void
loop ()
{
  uint16_t i;
  static uint16_t t = 0xffff;
  static uint8_t pressed = 0;
  static uint8_t state = 0xff;
  uint8_t delay_value = 0;

#ifdef MAGIC_KEY_POS
  // for atmega32u4 based Arduinos:
  //
  // check if the bootloader has been activated.
  // avoid doing any rendering to prevent the
  // MAGIC_KEY getting overridden which in turn
  // would prevent entering the bootloader properly.

  if (*((uint16_t *) MAGIC_KEY_POS) == MAGIC_KEY &&
      WDTCSR & (1 << WDE))
    {
      return;
    }
#endif
   
  if (state >= NUM_MODES)
    state = EEPROM.read(0);
  if (state >= NUM_MODES)
    state = 0;

#ifdef MAGIC_KEY_POS
  // Now, this is quite unfortunate:
  //
  // for the atmege32u4 based arduinos (Leonardo, pro micro etc.)
  // entering the bootloader is initiated in the USB interrupt
  // handler (i.e. can happen at any time).
  //
  // This does two things: writes MAGIC_KEY to MAGIC_KEY_POS and
  // enables the watchdog reset.
  //
  // If the watchdog fires the atmega32u4 resets and the bootloader
  // code checks for the MAGIC_KEY at MAGIC_KEY_POS. If it finds
  // the MAGIC_KEY it sticks in the bootloader mode.
  //
  // for larger LED strips it is quite likely that MAGIC_KEY_POS
  // resides in the middle of the framebuffer. And if the USB interrupt
  // happens while the code is rendering stuff to the framebuffer,
  // it then might happen that the MAGIC_KEY immediately gets overwritten
  // by the rendering code. This prevents that the bootloader gets
  // entered upon the watchdog reset. For some effects the AVR is mostly
  // rendering, making it basically impossible to enter the bootloader
  // via the IDE.
  //
  // As a workaround we disable all interrupts during the rendering code
  // which is quite a brute force method. This delays the writing of the
  // MAGIC_KEY to the point of the sei() (since this is now the point where
  // the USB interrupt gets handled), giving the MAGIC_KEY precedence over
  // the rendered effect.
  //
  // and since we basically avoid running loop() when the
  // bootloader-conditions are met (see above) the switch to the bootloader
  // now is more reliable again.

  cli ();
#endif

  switch (state)
    {
      case 0:
        render_flame ();
        break;

      case 1:
        render_blueyellow (t);
        delay_value = 10;
        break;

      case 2:
        render_rainbow (t);
        delay_value = 10;
        break;

      case 3:
        render_redblue (t);
        delay_value = 10;
        break;

      case 4:
        render_kitt (t);
        delay_value = 20;
        break;
       
      case 5:
        render_rgbsparks (t);
        delay_value = 10;
        break;

      default:
        render_flame ();
        break;
    }

#ifdef MAGIC_KEY_POS
  sei ();
#endif

  // the actual delay relies on interupts, hence
  // we have to do the per-frame-waiting after the sei();
 
  if (delay_value)
    {
      delay (delay_value);
    }

  // Time-Tick. Needed for moving stripes
  t--;

  // update Pixels
  pixels.show ();

  // Button-Handling  (inverted logic: 0 = button pressed)
  if (!digitalRead (PIN_BUTTON))
    {
      // for software-debouncing on a pressed button we count down to 0
      // for each frame.
      pressed = MAX (pressed, 1) - 1;

      // at 1 we clear the framebuffer and switch mode.
      if (pressed == 1)
        {
          memset (flamebuffer, 0x00, sizeof (flamebuffer));
          memset (sparkbuffer, 0x00, sizeof (sparkbuffer));
          state = (state + 1) % NUM_MODES;
          EEPROM.write (0, state);
          for (i = 0; i < NUM_PIXELS; i++)
            {
              pixels.setPixelColor (i, 0, 0, 0);
            }
        }
    }
  else
    {
      pressed = 5;
    }
}

Im warpzone gitlab habe ich den Code nun auch in ein Repo gekippt. Viel Spaß damit.