Adresowalne diody WS2812B w praktyce

Od niedawna w naszym asortymencie możecie znaleźć linijki ośmiu adresowalnych LEDów RGB. W dzisiejszym artykule nauczymy się nimi sterować :) Zaprezentuję Wam sposób wykonania kilku prostych animacji jakie można zrobić na jednej takiej linijce. Zaczynajmy więc!

Jak podłączyć?

Podłączenie takich linijek jest banalnie proste. Zasilamy je napięciem 4-7 V, Więc 5V np. z Arduino świetnie się sprawdzi. Do pinu VDC4-7V podłączamy pin 5V, GND do GND, a DIN do pinu 6 Arduino. Na potrzeby artykułu dodałem jeszcze 1 przycisk (pin 2) oraz potencjometr (pin A0).

Dioda najbliżej pinu cyfrowego Arduino zawsze będzie miała adres 0. Każda kolejna będzie miała adres o 1 większy od swojej poprzedniczki.

Kod programu

Cały szkic do Arduino można pobrać tutaj.

Zanim jednak wgramy gotowy szkic do naszego Arduino musimy pobrać specjalną bibliotekę do obsługi naszych diod. Można ją pobrać stąd. Wgrywamy ją do folderu Dokumenty/Arduino/libraries/.

Na początku ustawiamy kilka przydatnych wartości takich jak stałe z numerami pinów oraz konstruujemy obiekt klasy Adafruit_NeoPixel.

Adafruit_NeoPixel strip(pixelCount, pixelPin, NEO_GRB + NEO_KHZ800);

Do konstruktora przekazujemy ilość diod w pasku, pin cyfrowy Arduino do którego podłączony jest pasek oraz kilka zmiennych konfiguracyjnych, którymi nie musimy się specjalnie przejmować na chwilę obecną :)

Przejdźmy do funkcji setup(). Musimy w niej rozpocząć komunikację z naszym paskiem ledowym w następujący sposób:

  strip.begin();
  strip.show();

Teraz zobaczmy co robi funkcja loop():

void loop() {
  switch(mode) {
    case 0: modePoliceLights(); break;
    case 1: modeRainbow(); break;
    case 2: modeRainbowPot(analogRead(potPin)); break;
    case 3: modePot1(analogRead(potPin)); break;
    case 4: modePot2(analogRead(potPin)); break;
  }
  bool currentButtonState = digitalRead(btnPin);
  if(currentButtonState && !lastButtonState) {
    mode = (mode + 1) % 5;
  }
  lastButtonState = currentButtonState;
}

Tutaj nic nadzwyczajnego się nie dzieje. Przy wciśnięciu przycisku przełączamy tryby świecenia naszych lampek poprzez wywoływanie odpowiednich funkcji.

Teraz czas na najważniejsze, czyli omawiamy funkcje wykonujące animacje.

Na pierwszy ogień pójdzie najprostsza czyli światła policyjne:

void modePoliceLights() {
  int period = 160;
  unsigned long progress = millis() % period;
  
  for(int x = 0; x < strip.numPixels(); x++) {
    if(x < strip.numPixels() / 2) {
      strip.setPixelColor(x, 255 * (progress < period/2) , 0, 255 * (progress >= period/2));
    } else {
      strip.setPixelColor(x, 255 * (progress >= period/2) , 0, 255 * (progress < period/2));
    }
  }
  strip.show();
}

Ogólnie w naszych funkcjach, które wykonują animacje zależne od czasu będziemy definiowali sobie 2 zmienne na początku:

  • int period – czas jednego pełnego okresu animacji
  • unsigned long progress – ilość ms jaka upłynęła od początku aktualnego okresu

Teraz możemy ustawić połowę paska w kolorze niebieskim, a drugą połowę w kolorze czerwonym. O tym jaki kolor będzie miała dana połówka w danym czasie będzie decydował warunek progress < period/2 oraz przeciwny progress >= period/2. Zastosowaliśmy tutaj ciekawy trik, mianowicie z uwagi, że potrzebujemy tylko 2 kolorów z maksymalnym nasyceniem możemy wynik naszego warunku (który będzie jedynką lub zerem) pomnożyć przez 255 w efekcie czego dostaniemy maksymalne natężenie (255) lub brak danego koloru (0). Po ustaleniu koloru dla każdej diody możemy wywołać metodę show(), która zaktualizuje kolory na pasku.

Zajmijmy się teraz dużo ciekawszym i wymagającym trochę więcej matematyki efektem tęczy. Kod funkcji wygląda tak:

void modeRainbow() {
  int period = 5000;
  unsigned long progress = millis() % period;

  int dif = 360 / strip.numPixels();

  for(int x = 0; x < strip.numPixels(); x++) {
    setHue(x, dif * (x+1) + (360.0 * progress / period));
  }

  strip.show();
}

Pojawia się tu zmienna dif, która jest niczym innym jak przesunięciem fazowym dwóch sąsiednich diod. W tej animacji używamy jeszcze jednej funkcji: setHue(). Przyjmuje ona jako parametr adres diody oraz kolor (a raczej parametr Hue) z palety HSV przy założeniu, że S = 1 oraz V = 1. Dzięki temu możemy płynnie przechodzić między kolejnymi kolorami tęczy zwiększając H co 1 stopień. Rozłóżmy teraz na czynniki pierwsze równanie, które przekazujemy do funkcji jako drugi parametr.

dif * (x+1) + (360.0 * progress / period)

Mnożymy tutaj nasze przesunięcie fazowe przez numer diody (zaczynając od 1, kończąc na 8) oraz dodajemy do niego postęp animacji. Progress dzielone przez period będzie wartością z przedziału 0 do 1, a 360 to ilość stopni w palecie dla wartości HUE. Przesuwamy więc w ten sposób kolor na danej diodzie przez cały zakres.

Funkcja modeRainbowPot() różni się od modeRainbow() jedynie tym, że jest uzależniona nie od czasu a wartości na potencjometrze. Jedyna zmiana to usunięcie zmiennych period i progress, oraz drobna zmiana w równaniu ustalającym kolor na diodzie.

Mamy jeszcze funkcje modePot1() oraz modePot2(). Są one uzależnione od potencjometru i pokazują tym wyższy słupek im wyższa wartość na potencjometrze. Pierwsza z nich wypełnia pasek kolorem, który jest tym bardziej czerwony im wyższa wartość, a druga wypełnia pasek kolorem czerwonym, a zwiększanie wartości powoduje, że kolejny pojawiający się pixel zmienia stopniowo kolor od zielonego do czerwonego oraz intensywność od 0 do 1.

Przyjrzyjmy się pierwszej z tych funkcji:

void modePot1(int potValue) {
  byte red = (byte) (255 * (potValue / 1024.0));
  byte green = 255 - red;
  
  int progress = floor(strip.numPixels() * (potValue / 1024.0));
  for(int x = 0; x < strip.numPixels(); x++) {
    if(x <= progress) {
      strip.setPixelColor(x, red, green, 0);
    } else {
      strip.setPixelColor(x, 0, 0, 0);
    }
  }
  strip.show();
}

Na początku ustalamy sobie ile % koloru czerwonego będzie w pasku. Następnie obliczamy zawartość koloru zielonego odejmując od wartości maksymalnej wartość dla koloru czerwonego (stąd wzrost czerwonego automatycznie spowoduje spadek zielonego). Następnie ustalamy sobie zmienną progress, która informuje nas ile pixeli trzeba zapalić.

Zobaczmy teraz czym różni się druga z funkcji:

void modePot2(int potValue) {
  int singleSegment = 1024 / strip.numPixels();
  int progress = floor(strip.numPixels() * (potValue / 1024.0));
  
  for(int x = 0; x < progress; x++) {
    strip.setPixelColor(x, 255, 0, 0);
  }

  float intensity = (potValue - (progress * singleSegment * 1.0)) / singleSegment;


  byte red = (byte) (255.0 * intensity);
  byte green = 255 - red;
  
  strip.setPixelColor(progress, red * intensity, green * intensity, 0);

  for(int x = progress+1; x < strip.numPixels(); x++) {
    strip.setPixelColor(x, 0, 0, 0);
  }

  strip.show();
}

Tutaj jest już trochę trudniej. Na początku w zmiennej singleSegment ustalamy sobie jaka zmiana na potencjometrze będzie odpowiadała jednej diodzie. Następnie obliczamy ile diod trzeba zaświecić na czerwono, po czym pętlą for zapalamy je.

Teraz możemy policzyć sobie intensywność świecenia diody, która jeszcze nie jest w pełni czerwona. Od wartości na potencjometrze odejmujemy wartość progową, od której najwyższy pixel zaczyna się świecić. Następnie dzielimy to przez singleSegment aby uzyskać wynik z przedziału 0 do 1.  Wartości kolorów czerwonego i zielonego ustalamy analogicznie do funkcji modePot1(). Teraz ustawiamy kolor najwyższego aktywnego pixela mnożąc każdą składową przez intensywność świecenia tegoż pixela. Resztę pixeli gasimy ustawiając na nich same zera dla składowej czerwonej, zielonej i niebieskiej. Na końcu aktualizujemy pasek i voila!

Efekty

Efekty naszej pracy można zobaczyć na poniższym filmiku: