Neues aus der Kreativitätsabteilung

In den letzten Folgen hatten wir uns technisch mit Lichtspielen auf Basis von WS2812B-Bausteinen beschäftigt. Zeit, die neuen Erkenntnisse auch mal für kreative Designstudien zu verwenden.

In Pocket speichern vorlesen Druckansicht 1 Kommentar lesen
Lesezeit: 14 Min.
Von
  • Dr. Michael Stal
Inhaltsverzeichnis

In den letzten Folgen hatten wir uns technisch mit Lichtspielen auf Basis von WS2812B-Bausteinen beschäftigt. Zeit, die neuen Erkenntnisse auch mal für kreative Designstudien zu verwenden.

Im vorliegenden Post stehen zwei Kreativprojekte auf dem Programm, zum einen eine Designeruhr als Wohnaccessoire, zum anderen eine Kombination aus einem RGB-Farbsensor und einem LED-Streifen.

Unsere Designeruhr kommt mit den Bausteinen aus, die wir bereits in den letzten beiden Episoden verwendet haben.

  • Ein Ring aus 60 WS2812B-LEDs wie der Adafruit Giant Ring, oder alternativ, ein 1m-Streifen mit 60 LEDs.
  • Ein Netzteil, um die LEDs zum Leuchten zu bringen.
  • Ein Arduino-Board nach eigener Wahl!
  • Einen Elektrolyt-Kondensator mit 1000 µF, zu platzieren zwischen Anode und Kathode der Stromversorgung des LED-Streifens/-Rings.
  • Einen 470 Ω Widerstand zwischen Digitalausgang 6 des Arduino und dem Dateneingang der Pixel.

Der bereits bewährte Schaltkreis

Die 60 LEDs nutzen wir wie bei unsere Uhr natürlich zur Anzeige von Stunde, Minute, Sekunde sowie der Markierungen für 12 Uhr (Nord), 3 Uhr (Ost), 6 Uhr (Süd), 9 Uhr (West). Stunden-, Minuten- und Sekundenzeiger bestehen aus wandernden Pixeln mit unterschiedlicher Farbe. Auch die erwähnten Markierungen visualisieren wir als Leucht-Pixel mit vorkonfigurierter Farbe.

Ein Problem besteht darin, dass sich mehrere Positionen überschneiden können, etwa die von Stunden- und Minutenanzeiger. Hier können wir nur mit "zusammengesetzten" Farben arbeiten. Betroffene Pixel erhalten eine entsprechend berechnete Färbung. Tritt etwa Rosé auf, befinden sich an der entsprechenden Position gleichzeitig Sekunden- und Stundenzeiger.

Das Programm selbst ist nicht gerade weltbewegend, sondern eher leichte Kost, und enthält auch einiges Verbesserungs- beziehungsweise Erweiterungspotenzial:

  • Es empfiehlt sich, der Schaltung eine Echtzeituhr hinzufügen, um die Anfangszeit nicht programmatisch eingeben zu müssen. Siehe rot markierte Stelle im Listing.
  • Mit einem ESP-01 WiFi-Board könnten wir die Zeit per NTP-Server synchronisieren.
  • Die Leuchtintensität ließe sich abhängig von der Tageszeit einstellen.
  • Wir könnten die Farbselektion über eine externe Konfiguration variabel gestalten.
  • Wir könnten eine IR-Fernbedienung nutzen, um bequem Einstellungen vornehmen zu können.
  • Man könnte die Uhr aus konzentrischen Ringen zusammensetzen, auf denen sich die verschiedenen Zeiger unabhängig voneinander bewegen.

Das Programmieren ist der einfachste Teil des Projekts, weshalb es sich empfiehlt, den nachfolgenden Sketch zu durchforsten. Programmiertechnische Stolperfallen gibt es keine.


//////////////////////////////////////////////////////////////////
//
// Programmierung einer Designeruhr als Wandschmuck
// Grundlage ist ein LED-Streifen (WS2812B) oder ein Ring mit
// 60 LEDs
//
//////////////////////////////////////////////////////////////////



// Import der Neopixel-Bibliothek von Adafruit:
#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
#include <avr/power.h>
#endif


// Anschluss der LEDs an Digitalausgang 6:
#define PIN 6


// Streifen/Ring initialisieren:
Adafruit_NeoPixel strip = Adafruit_NeoPixel(60, PIN, NEO_GRB + NEO_KHZ800);


// Position des Stundenzeigers:
uint8_t currentHour;
// Position des Minutenzeigers:
uint8_t currentMin;
// Position des Sekundenzeigers:
uint8_t currentSec;


// Initialisierung:
void setup() {
strip.begin(); // Streifen initialisieren
strip.show(); // Alle Pixel erst einmal aus

// Initiale Uhrzeit manuell einstellen
// Initialer Zustand der Uhrenanzeige bei Programmstart:
currentHour = 19; // Position wird modulo 12 berechnet
currentMin = 59;
currentSec = 40;
}
//////////////////////////////////////////////////////////////////
//
// Methode: colorPicker(Pixelnummer)
// Ergebnis: zusammengesetzte Farbe
// Zweck: Farbanteile einer LED berechnen
//
//////////////////////////////////////////////////////////////////

uint32_t colorPicker(uint8_t pos) {
const uint32_t hourColor = 0xD00000; // Stundenzeiger mit Rot
const uint32_t minColor = 0x00D000; // Minutenzeiger mit Grün
const uint32_t secColor = 0x0000D0; // Sekundenzeiger mit Blau
uint32_t result = 0;
// Bei Position in N, O, S, W Extra-Farbanteil für R, G, B hinzufügen:
if (pos % 15 == 0) result += 0x2F2F2F;
// Ist Minutenzeiger auf dieser Position,
// Grünanteil hinzufügen:

if (pos == currentMin) result += minColor;
// Ist Sekundenzeiger auf dieser Position,
// Blauanteil hinzufügen:

if (pos == currentSec) result += secColor;
// Ist Stundenzeiger auf dieser Position,
// Rotanteil hinzufügen:

if (pos == currentHour) result += hourColor;
// Resultierende Farbe zurück melden
return result;
}


//////////////////////////////////////////////////////////////////
//
// Methode: displayClock()
// Zweck: Für alle 60 Pixel Farbe errechnen,
// bei der LED entsprechend setzen,
// und darstellen
//
//////////////////////////////////////////////////////////////////

void displayClock() {

for (uint8_t pixno = 0; pixno < 60; pixno++)
strip.setPixelColor(pixno, colorPicker(pixno));
strip.show();
}


//////////////////////////////////////////////////////////////////
//
// Methode: incrementTimeBySecond
// Zweck: Berechnet neue Zeit nach einer Sekunde
//
//////////////////////////////////////////////////////////////////

void incrementTimeBySecond() {
currentSec ++;
if (currentSec == 60) { // Minutengrenze erreicht?
currentSec = 0; // Ja => Sekundenzeiger auf 0
currentMin++; // und Minuten erhöhen
}
if (currentMin == 60) { // Stundengrenze erreicht?
currentMin = 0; // Ja => Minutenzeiger auf 0
currentHour++; // und Stunden erhöhen
}
if (currentHour == 12) // Nordpol erreicht/12h-Stellung?
currentHour = 0; // Ja => wieder auf 0 zurück
}


//////////////////////////////////////////////////////////////////
//
// Methode: loop()
// Zweck: Uhr implementieren
//
//////////////////////////////////////////////////////////////////

void loop() {
// Uhr anzeigen:
displayClock();
// 1 Sekunde Pause:
delay(1000);
// Neuberechnen der Tageszeit:
incrementTimeBySecond();
}

Das Uhrenprojekt demonstriert, mit wie wenigen Mitteln sich interessante Ideen umsetzen lassen, wenn die entsprechende Hardware vorliegt.

Da das erste Projekt eher als Appetitanreger fungiert, können wir dem ersten Streich gleich einen zweiten folgen lassen.

Mit Hilfe von Farbsensoren bzw. Colorimetern lassen sich Farben messen. Prinzipiell funktioniert das so ähnlich wie beim Fotosensor in einem Fotoapparat. Auch in Kalibrierungssystemen für Fernseher, Computerbildschirme oder Drucker kommt der Mechanismus zum Einsatz. Nicht zu vergessen Philips Ambilight, das Farben aus dem Fernsehbild über LEDs in den Raum transportiert.

Im zweiten Projekt dieser Ausgabe setzen wir einen Sensor zur Farberkennung ein, ein sogenanntes Colorimeter. Die erkannte Farbe wandeln wir in RGB-Kommandos an einen LED-Streifen mit WS2812B-Pixeln um. Zum Beispiel unter Verwendung von Adafruit's Neopixel-Produkten.

Wir haben es also mit einem Chamäleon als Projektziel zu tun.

Für diese zweite künstlerische Installation nutzen wir den konkret Baustein TCS230 für die Farberkennung.

Der Farbsensor TCS320

(Bild: seeedstudio.com)

Die vier weißen LEDs des Sensors dienen zur Ausleuchtung des Messobjekts. In der Mitte erkennen Sie einen IC mit 4x16 Fotodioden, von denen je sechzehn Rot-, Blau-, Grün- oder Weißlicht erkennen. Diese Fotodioden-Reihen sind über die Eingänge S2 und S3 individuell filterbar. Die Dioden haben eine Fläche von je 120 μm x 120 μm. Nur, damit Sie sich ein Bild machen können.

(S2, S3) Filter

  • (LOW , LOW) Rot
  • (LOW , HIGH) Blau
  • (HIGH, LOW) Ungefiltert
  • (HIGH, HIGH) Grün

Um den Messbereich zu verändern bzw. die Empfindlichkeit zu verstärken, können Entwickler über die Eingänge S0 und S1 wählen, welche Frequenzbereiche moduliert werden sollen.

(S0, S1) Teilung der Ausgabefrequenz

  • (LOW , LOW) Aus
  • (LOW , HIGH) 2 %
  • (HIGH, LOW) 20 %
  • (HIGH, HIGH) 100%

Die Funktionsweise des TCS230-Farbsensors ist relativ einfach zu erläutern:

  • Licht gelangt an die Fotodioden und löst dort einen Strom aus. Das besagt schon Einsteins Arbeit zum fotoelektrischen Effekt. Wie gesagt, verwenden wir hierfür Fotodioden, die gewisse Frequenzbereiche filtern.
  • Diesen Strom wandelt ein separater Teil des Sensors in Rechtecksimpulse um, die der Sensor am Ausgang OUT ausgibt. Diese Rechtecksimpulse haben einen Duty Cycle von 50% – Ausgabe zu 50% low und zu 50% high. Das Ausgangssignal ist bezüglich seiner Frequenz proportional zum gemessenen Lichtsignal.

Seine Energie bezieht der TCS230 über die Eingänge VCC und GND. Mit dem Active LOW Eingang ¬OE lässt sich die Signalausgabe einschalten. Daher legen wir diesen Eingang in der Regel dauerhaft auf GND.

In der vorliegenden Schaltung für unsere Art-Installation haben wir folgende Verbindungen zwischen Arduino Uno und TCS230.

Fritzing-Diagramm der Schaltung mit TCS230

TC230 Arduino Uno

  • S0 Digitaler Ausgang 6
  • S1 Digitaler Ausgang 7
  • S2 Digitaler Ausgang 4
  • S3 Digitaler Ausgang 5
  • OUT Digitaler Eingang 2 (externer Interrupt 0 zur Beobachtung von Signalen)
  • GND GND
  • Vcc 5V
  • OE GND

Den WS2812B-basierten LED-Streifen schließen wir an den Arduino Digitalausgang 13 an.

Im Programm benutzen wir die relativ einfache Metro-Bibliothek. Sie dient zum Umgang mit regelmäßig wiederkehrenden Éreignissen. Achtung: Sobald Sie die ZIP-Datei der Bibliothek aus GitHub heruntergeladen haben, wählen Sie in der Arduino IDE nicht das Masterverzeichnis der Bibliothek in der Arduino-IDE, wenn Sie die Bibliothek importieren wollen. Ansonsten erhalten Sie eine Fehlermeldung. Stattdessen navigieren Sie zum Unterverzeichnis Metro, um die Bibliothek zu importtieren.

Mehr Infos

Metro Library

In diesem Beispielsprogramm aus der Metro-Bibliothek findet sich die Variable ledMetro.

Diese ist auf 250 Millisekunden voreingestellt. In der Endlosschleife überprüft der Sketch mittels

if (ledMetro.check() == 1) { ... }

ob diese Zeit schon erreicht ist. Wenn ja, lässt er die LED blinken.

#include <Metro.h> //Include Metro library
#define LED 13 // Define the led's pin

//Create a variable to hold theled's current state
int state = HIGH;

// Instanciate a metro object and set the interval to 250 milliseconds (0.25 seconds).
Metro ledMetro = Metro(250);

void setup()
{
pinMode(LED,OUTPUT);
digitalWrite(LED,state);
}

void loop()
{
if (ledMetro.check() == 1) { // check if the metro has passed its interval .
if (state==HIGH) state=LOW;
else state=HIGH;
digitalWrite(LED,state);
}
}

Sie können stattdessen auch jederzeit das Wissen aus der Folge über Timer benutzen, was übrigens auch eine sehr schöne Aufgabe ist.

Auch das zweite Programm dieser Folge ist relativ einfach zu durchdringen, weshalb ich mir an dieser Stelle unnötige Wiederholungen erspare.

Die Kommentare sollten Sie gut durch den Code führen.

///////////////////////////////////////////////////////
//
// Ansteuerung des Farberkennungssensors TCS230
//
///////////////////////////////////////////////////////


// Import der Neopixel Bibliothek:
#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
#include <avr/power.h>
#endif

// Import der Metro-Bibliothek:
#include <Metro.h> // library
#include <math.h>


const int S0 = 6; // S2 und S3 definieren die Modulation
const int S1 = 7; // der Ausgabefrequenz
const int S2 = 4; // S0 und S1 selektieren die Photodioden
const int S3 = 5; // bzw. Filter
const int RGB = 13; // Anschluss der RGB LEDs
const int OUT = 2; // Hier liegt das Ausgabesignal des Sensors an

// Zähler vereinbaren:
unsigned int cntr = 0; // globaler Zähler
unsigned int cntrR = 0; // roter Zähler
unsigned int cntrG = 0; // grüner Zähler
unsigned int cntrB = 0; // blauer Zähler
unsigned int cntrRavg = 0; // durchschnittlicher Rotwert
unsigned int cntrGavg = 0; // durchschnittlicher Grünwert
unsigned int cntrBavg = 0; // durchschnittlicher Blauwert

// Metroobjekte für Zeitmanagement:
Metro illuminate = Metro(60); // leuchten
Metro getSensorData = Metro(50); // verarbeiten
Metro triggerSensor = Metro(10); // messen

// Initialisieren eines Neopixel-Zugriffsobjekts mit 64 LEDs:

Adafruit_NeoPixel strip = Adafruit_NeoPixel(64, RGB, NEO_GRB + NEO_KHZ800);

// Diverse Farbfilter des TCS230:
enum {
NoFilter = 0,
RedFilter = 1,
GreenFilter = 2,
BlueFilter = 3
};

uint8_t sensorFlag = NoFilter; // initial kein Filter

///////////////////////////////////////////////////////
// Methode zum Durchlaufen der Farbfilter-Codes:
///////////////////////////////////////////////////////

void nextSensorFlag () {
sensorFlag = (sensorFlag + 1) % 4;
}

///////////////////////////////////////////////////////
//
// Methode: display
// Parameter: color = Farbe
// Zweck: lässt alle Pixel in color erstrahlen
//
///////////////////////////////////////////////////////

void display(uint8_t red, uint8_t green, uint8_t blue) {
uint32_t color; // Komponierter RGB Farbwert
uint8_t * rgb = (uint8_t *)&color;
rgb[3] = 0; // Weißanteil
rgb[2] = red; // rote Komponente
rgb[1] = green; // grüne Komponente
rgb[0] = blue; // blaue Komponente

// Gewünschte Farbe am LED-Streifen darstellen:
for(uint16_t pixno=0; pixno<strip.numPixels(); pixno++) {
strip.setPixelColor(pixno, color);
strip.show();
}
}

///////////////////////////////////////////////////////
// Bei jedem Interrupt an Eingang 2 => Aufruf ISR
///////////////////////////////////////////////////////

void ISR_Counter() {
cntr++;
}

///////////////////////////////////////////////////////
//
// Methode: setup
// Zweck: Initialisierung
//
///////////////////////////////////////////////////////

void setup()
{
// Ausgabe am seriellen Monitor einschalten:
Serial.begin(9600);

pinMode(S0, OUTPUT); // S0, S1, S2, S3 sind Eingänge des Sensors
pinMode(S1, OUTPUT); // S0 und S1 definieren Ausgabefrequenz
pinMode(S2, OUTPUT); // S2 und S3 definieren Farbfilterauswahl
pinMode(S3, OUTPUT);
pinMode(OUT, INPUT); // OUT ist ein Ausgang des Sensors
pinMode(RGB,OUTPUT); // LED-Streifen oder -Matrix


// Leuchtstreifen initialisieren und alle LEDs anfangs ausschalten:
strip.begin();
strip.show();
}


////////////////////////////////////////////////////////////////////
//
// Methode: loop()
//
// Zweck:
// Wir selektieren schrittweise die Photodioden für Rot, Grün, Blau,
// messen die entsprechenden Anteile im Frequenzband,
// und geben das Ergebnis aus
//
////////////////////////////////////////////////////////////////////


void loop()
{
// Ausgabefrequenz auf 100% einstellen:
digitalWrite(S0,HIGH);
digitalWrite(S1,HIGH);

// Wir melden eine Interruptroutine für Pin 2 (= Interrupt 0) an.
// Auslösung erfolgt bei Signalwechsel am externen Pin 2:

attachInterrupt(0, ISR_Counter, CHANGE);

// zunächst Messung auslösen:
if (triggerSensor.check()) {
nextSensorFlag();
switch(sensorFlag) {
case RedFilter:
// Rotfilter einschalten:
digitalWrite(S2,LOW);
digitalWrite(S3,LOW);
cntrR=cntr;
// Grünfilter einschalten:
digitalWrite(S2,HIGH);
digitalWrite(S3,HIGH);
break;
case GreenFilter:
cntrG=cntr;
// Blaufilter einschalten:
digitalWrite(S2,LOW);
digitalWrite(S3,HIGH);
break;
case BlueFilter:
cntrB=cntr;
// Rotfilter einschalten:
digitalWrite(S2,LOW);
digitalWrite(S3,LOW);
nextSensorFlag();
break;
}
cntr = 0; // Zähler auf 0 zurücksetzen
}

// danach Durchschnitte berechnen, um Fehler zu kompensieren:
if (getSensorData.check()) {
const int PROBES = 8; // Zahl der Messungen
static int reds [PROBES] = { 0 }; // Alles mit 0 vorbelegen
static int blues [PROBES] = { 0 };
static int greens[PROBES] = { 0 };

// alle Messungen durchlaufen:
for(int measurement = PROBES-1; measurement > 0; measurement--) {
reds [measurement] = reds [measurement-1];
blues [measurement] = blues [measurement-1];
greens[measurement] = greens[measurement-1];
}

if(cntrR < 2500) // realistischer Wert?
reds [0] = cntrR;
else // kompensieren
reds [0] = reds [1];

if(cntrB < 2500) // realistischer Wert?
blues [0] = cntrB;
else // kompensieren
blues [0] = blues [1];

if(cntrG < 2500) // realistischer Wert?
greens[0] = cntrG;
else // kompensieren
greens[0] = greens[1];

cntrRavg = 0; // Durchschnitte mit 0 initialisieren
    cntrGavg  = 0;
cntrBavg = 0;

// alle Ergebnisse der Messungen aufaddieren:
for(int probe = 0; probe <= PROBES - 1; probe++){
cntrRavg += reds [probe];
cntrGavg += greens[probe];
cntrBavg += blues [probe];
}

// Durchschnitt der Messungen berechnen:
cntrRavg /= PROBES;
cntrGavg /= PROBES;
cntrBavg /= PROBES;
}

// Ergebnisse in RGB-Werte transformieren und erkannte Farbe
// ausgeben:

if (illuminate.check()) {
uint8_t red = map(constrain(cntrRavg, 350,1700), 350, 1700, 0, 255);
uint8_t green = map(constrain(cntrGavg, 350,1650), 350, 1650, 0, 255);
uint8_t blue = map(constrain(cntrBavg, 350,1500), 350, 1500, 0, 255);

// Gemessene Farbe am Neopixel ausgeben:
display(red, green, blue);
}
}

Wann immer Sie den Farbsensor in die Nähe eines farbigen Objekts bringen, sollten Sie eine entsprechende Verfärbung der Pixel beobachten. Die Genauigkeit der Farberkennung hängt auch von der Distanz zwischen Sensor und Zielobjekt ab. Durch intensives Experimentieren erkennen Sie noch weitere Zusammenhänge. Außerdem lernen Sie dabei die Stärken und Schwächen der Selbstbauslösung kennen.

In dieser sehr ausführlichen Folge haben Sie zwei eher künstlerische Projekte kennengelernt. Das Thema Farbspiele ist für kreative Ideen ohnehin sehr dankbar. Vielleicht können Sie die ein oder andere coole Idee umsetzen und auch andere daran partizipieren lassen.

Das Thema Licht haben wir damit aber noch lange nicht abgeschlossen. Das Thema der Begierde wechselt in der nächsten Folge allerdings erst einmal zu Projekten mit Lasern.

Mehr Infos

()