[037] Prawie wszystko o diodach świecących cz. 5
W poprzednim artykule stworzyliśmy choinkowe lampki: skromne, bo składające się tylko z pięciu diod świecących, jednak chyba każdy potrafiłby taki program rozbudować. Miganie jednak na dłuższą metę było uciążliwe dla oka, dlatego w tym artykule będziemy je łagodzić i upiękniać.
#include <Adafruit_NeoPixel.h> // Dołącz bibliotekę sterującą diodami WS2812
Adafruit_NeoPixel dioda = Adafruit_NeoPixel(5, 12, NEO_GRB + NEO_KHZ800); // Ustaw parametry zestawu: pięć diod na dwunastym porcie.
unsigned long czasObecny = 0; // Aktualnie odczytana wartość czasu od włączenia Arduino, wyrażona w milisekundach.
unsigned long czasPoprzedni0 = 0; // Poprzednio odczytana wartość czasu od włączenia Arduino dla diody zerowej.
unsigned long czasPoprzedni1 = 0;
unsigned long czasPoprzedni2 = 0;
unsigned long czasPoprzedni3 = 0;
unsigned long czasPoprzedni4 = 0;
bool led0 = true; // Stan aktywności diody zerowej.
bool led1 = true;
bool led2 = true;
bool led3 = true;
bool led4 = true;
void setup() {
dioda.begin(); // Przygotuj sterowniki znajdujące się w diodach do inicjalizacji.
dioda.show(); // Wykonaj powyższe polecenie.
}
void loop() {
czasObecny = millis(); // Zapisz wartość czasu od włączenia Arduino.
if (czasObecny - czasPoprzedni0 > 400) { // Jeśli różnica między pomiarami przekracza 400 ms, to...
czasPoprzedni0 = czasObecny - random(200); // Przenieść wartość aktualną do poprzedniej, pomniejszając ją o wartość losową.
led0 = !led0; // Zaprzecz aktywności mocy diody świecącej.
dioda.setPixelColor(0, dioda.Color(128 + 127 * led0, 0, 0)); // Wyślij komunikat włączający czerwoną strukturę diody świecącej.
dioda.show(); // Wykonaj powyższe polecenie.
}
if (czasObecny - czasPoprzedni1 > 400) {
czasPoprzedni1 = czasObecny - random(200);
led1 = !led1;
dioda.setPixelColor(1, dioda.Color(0, 128 + 127 * led1, 0));
dioda.show();
}
if (czasObecny - czasPoprzedni2 > 400) {
czasPoprzedni2 = czasObecny - random(200);
led2 = !led2;
dioda.setPixelColor(2, dioda.Color(0, 0, 128 + 127 * led2));
dioda.show();
}
if (czasObecny - czasPoprzedni3 > 400) {
czasPoprzedni3 = czasObecny - random(200);
led3 = !led3;
dioda.setPixelColor(3, dioda.Color(128 + 127 * led3, 128 + 127 * led3, 0));
dioda.show();
}
if (czasObecny - czasPoprzedni4 > 400) {
czasPoprzedni4 = czasObecny - random(200);
led4 = !led4;
dioda.setPixelColor(4, dioda.Color(128 + 127 * led4, 0, 128 + 127 * led4));
dioda.show();
}
}
Powyższy szkic ideą pozostaje identyczny wobec tego, którym zakończyliśmy poprzedni artykuł. Różni się natomiast w szczegółach. Po pierwsze, użyjemy mojej ulubionej instrukcji random. Będzie ona mieć wpływ na miganie, już nie periodyczne, jak poprzednio, a w pewnym zakresie losowe. Ponieważ losowość obejmie wszystkie diody, tym razem każda ma wstępnie ustawioną wartość opóźnienia na 400 ms, od której odejmujemy wartość losową z przedziału 0-200. Zatem miganie będzie się zmieniać z opóźnieniem z przedziału 200 do 400 ms
To jest jedna innowacja. Drugą zawarłem w linii formującej komunikat dla diod. Jak widać, tym razem poziomy jasności będą inne: 128 lub 256. Dzięki temu diody nie będą gasnąć zupełnie, tylko przyjmą dwa poziomy jasności: pełny bądź połówkowy. Taka choinka będzie mniej chaotyczna. Dobierając zarówno poziomy jak i współczynniki opóźnień, możemy zmieniać dynamikę mrugania i zachęcam tutaj do eksperymentów.
Do tej pory każda dioda świeciła jedną barwą. Można i tak, ale mając takie diody RGB, byłoby to marnotrawstwo. Niechaj choinka będzie wesoła.
#include <Adafruit_NeoPixel.h> // Dołącz bibliotekę sterującą diodami WS2812
Adafruit_NeoPixel dioda = Adafruit_NeoPixel(5, 12, NEO_GRB + NEO_KHZ800); // Ustaw parametry zestawu: pięć diod na dwunastym porcie.
unsigned long czasObecny = 0; // Aktualnie odczytana wartość czasu od włączenia Arduino, wyrażona w milisekundach.
unsigned long czasPoprzedni0 = 0; // Poprzednio odczytana wartość czasu od włączenia Arduino dla diody zerowej.
unsigned long czasPoprzedni1 = 0;
unsigned long czasPoprzedni2 = 0;
unsigned long czasPoprzedni3 = 0;
unsigned long czasPoprzedni4 = 0;
void setup() {
dioda.begin(); // Przygotuj sterowniki znajdujące się w diodach do inicjalizacji.
dioda.show(); // Wykonaj powyższe polecenie.
}
void loop() {
czasObecny = millis(); // Zapisz wartość czasu od włączenia Arduino.
if (czasObecny - czasPoprzedni0 >= 400) { // Jeśli różnica między pomiarami przekracza 400 ms...
czasPoprzedni0 = czasObecny - random(200); // Przenieść wartość aktualną do poprzedniej, pomniejszając ją o wartość losową.
dioda.setPixelColor(0, dioda.ColorHSV(random(65535), 255, 255)); // Wyślij komunikat włączający losową barwę.
dioda.show(); // Wykonaj powyższe polecenie.
}
if (czasObecny - czasPoprzedni1 >= 400) {
czasPoprzedni1 = czasObecny - random(200);
dioda.setPixelColor(1, dioda.ColorHSV(random(65535), 255, 255));
dioda.show();
}
if (czasObecny - czasPoprzedni2 >= 400) {
czasPoprzedni2 = czasObecny - random(200);
dioda.setPixelColor(2, dioda.ColorHSV(random(65535), 255, 255));
dioda.show();
}
if (czasObecny - czasPoprzedni3 >= 400) {
czasPoprzedni3 = czasObecny - random(200);
dioda.setPixelColor(3, dioda.ColorHSV(random(65535), 255, 255));
dioda.show();
}
if (czasObecny - czasPoprzedni4 >= 400) {
czasPoprzedni4 = czasObecny - random(200);
dioda.setPixelColor(4, dioda.ColorHSV(random(65535), 255, 255));
dioda.show();
}
}
Nadal zostaniemy przy starej filozofii i wprowadzimy niewielką, acz istotną modyfikację w linii formującej dane wysyłane na diody. Przede wszystkim przejdziemy z modelu RGB na HSV, gdzie to za barwę odpowiedzialna jest tylko jedna, szesnastobitowa zmienna. A skoro jest jedna, po prostu losujmy sobie ją za każdym razem. O tym modelu pisałem w tym artykule i zapraszam tam po szczegóły.
Zrezygnowałem z przygaszania diod, zmiana koloru da wystarczająco dużo dynamiki, zatem wycofałem wszystkie przełączniki led. Teraz jasność będzie deklarowana jako maksymalna w każdym wypadku. Choinka stała się, że tak powiem, cukierkowa, ale taka ma być, wszak to choinka.
Czas na zupełnie inną filozofię. Popracujemy z przerwaniami. O tym już kiedyś mówiłem, więc przypomnę krótko: przerwania polegają na tym, że na skutek pewnych zdarzeń – u nas będzie to upłynięcie pewnego czasu – program rzuci wszystko i wyląduje w z góry określonym miejscu. Gdy wykona to, co tam znajdzie, wróci do porzuconego wątku i będzie kontynuował pracę do kolejnego przerwania.
#include <Adafruit_NeoPixel.h> // Dołącz bibliotekę sterującą diodami WS2812
Adafruit_NeoPixel dioda = Adafruit_NeoPixel(5, 12, NEO_GRB + NEO_KHZ800); // Ustaw parametry zestawu: pięć diod na dwunastym porcie.
#include <TimerOne.h> // Dołącz bibliotekę obsługi przerwań.
byte licznik0 = 0; // Licznik wyznaczający podstawę czasu animacji diody.
byte jasnosc0 = 0; // Wartość jasności diody.
bool przelacznik0 = true; // Przełącznik: rozjaśnianie lub ściemnianie diody.
unsigned int los0 = 0; // Losowy kolor diody w bieżącej animacji.
byte czas0 = 0; // Losowy czas bieżącej animacji.
byte licznik1 = 0;
byte jasnosc1 = 0;
bool przelacznik1 = true;
unsigned int los1 = 0;
byte czas1 = 0;
byte licznik2 = 0;
byte jasnosc2 = 0;
bool przelacznik2 = true;
unsigned int los2 = 0;
byte czas2 = 0;
byte licznik3 = 0;
byte jasnosc3 = 0;
bool przelacznik3 = true;
unsigned int los3 = 0;
byte czas3 = 0;
byte licznik4 = 0;
byte jasnosc4 = 0;
bool przelacznik4 = true;
unsigned int los4 = 0;
byte czas4 = 0;
void setup() {
dioda.begin(); // Przygotuj sterowniki znajdujące się w diodach do inicjalizacji.
dioda.show(); // Wykonaj powyższe polecenie.
Timer1.initialize(100); // Ustaw licznik na sto mikrosekund.
Timer1.attachInterrupt(przerwanie); // Włącz przerwanie z określeniem miejsca lądowania co ćwierć miliona mikrosekund.
}
void loop() {
dioda.setPixelColor(0, dioda.ColorHSV(los0, 255, jasnosc0)); // Wyślij komunikat włączający losową barwę i zmieniającą się jasność.
dioda.show(); // Wykonaj powyższe polecenie.
dioda.setPixelColor(1, dioda.ColorHSV(los1, 255, jasnosc1));
dioda.show();
dioda.setPixelColor(2, dioda.ColorHSV(los2, 255, jasnosc2));
dioda.show();
dioda.setPixelColor(3, dioda.ColorHSV(los3, 255, jasnosc3));
dioda.show();
dioda.setPixelColor(4, dioda.ColorHSV(los4, 255, jasnosc4));
dioda.show();
}
void przerwanie() { // Tutaj lądujemy za każdym razem, gdy wewnętrzny timer odliczy sto mikrosekund.
licznik0++; // Licznik wyznaczający podstawę czasu animacji diody.
licznik1++;
licznik2++;
licznik3++;
licznik4++;
if (licznik0 > 100 + czas0) { // Jeśli licznik wyznaczający podstawę czasu animacji diody, powiększony o wartość losową przekroczy 100, to...
licznik0 = 0; // Zeruj go.
if (przelacznik0 == true) { // Jeśli przełącznik jest ustawiony w pozycji: rozjaśnianie, to...
jasnosc0++; // Zwiększ bieżącą jasność o jeden.
if (jasnosc0 == 255) { // Jeśli osiągnęła maksimum, to...
przelacznik0 = false; // Przestw przełącznik w pozycji: ściemnianie.
}
} else { // Jeśli przełącznik jest ustawiony w pozycji: ściemnianie, to...
jasnosc0 = jasnosc0 - 5; // Zmniejsz bieżącą jasność o pięć.
if (jasnosc0 == 0) { // Jeśli osiągnęła minimum, to...
przelacznik0 = true; // Przestw przełącznik w pozycji: rozjaśnianie.
los0 = random(65535); // Losuj wartość koloru dla kolejnej animacji.
czas0 = random(155); // Losuj wartość losową dla podstwy czasu kolejnej animacji.
}
}
}
if (licznik1 > 100 + czas1) {
licznik1 = 0;
if (przelacznik1 == true) {
jasnosc1++;
if (jasnosc1 == 255) {
przelacznik1 = false;
}
} else {
jasnosc1 = jasnosc1 - 5;
if (jasnosc1 == 0) {
przelacznik1 = true;
los1 = random(65535);
czas1 = random(155);
}
}
}
if (licznik2 > 100 + czas2) {
licznik2 = 0;
if (przelacznik2 == true) {
jasnosc2++;
if (jasnosc2 == 255) {
przelacznik2 = false;
}
} else {
jasnosc2 = jasnosc2 - 5;
if (jasnosc2 == 0) {
przelacznik2 = true;
los2 = random(65535);
czas2 = random(155);
}
}
}
if (licznik3 > 100 + czas3) {
licznik3 = 0;
if (przelacznik3 == true) {
jasnosc3++;
if (jasnosc3 == 255) {
przelacznik3 = false;
}
} else {
jasnosc3 = jasnosc3 - 5;
if (jasnosc3 == 0) {
przelacznik3 = true;
los3 = random(65535);
czas3 = random(155);
}
}
}
if (licznik4 > 100 + czas4) {
licznik4 = 0;
if (przelacznik4 == true) {
jasnosc4++;
if (jasnosc4 == 255) {
przelacznik4 = false;
}
} else {
jasnosc4 = jasnosc4 - 5;
if (jasnosc4 == 0) {
przelacznik4 = true;
los4 = random(65535);
czas4 = random(155);
}
}
}
}
By użyć tego dobra, potrzeba biblioteki TimerOne. Musimy także określić częstotliwość procedury [Timer1.initialize(100)] – tutaj będzie to dość często, bo odstęp między wejściami w przerwania wyniesie tylko 100 mikrosekund. Nazwiemy sobie jeszcze podprogram [Timer1.attachInterrupt(przerwanie)], w którym ląduje się podczas występowania przerwania. O przerwaniach nieco więcej napiszę wkrótce, więc kto poczuje się zagubiony, niech chwilkę zaczeka.
Główna pętla jest zdecydowanie prostsza i przekazuje wyłącznie zestaw zmiennych do diod świecących. Zmienne te formuje się w podprogramie przerwań, dokąd właśnie się udamy. Zaczynamy od zwiększenia wartości pięciu liczników, po jednym dla każdej diody. Ich wartości rosną co 100 mikrosekund. Teraz będziemy mieć pięć bliźniaczych bloków, różniących się tylko odwołaniem do kolejnych zmiennych. Rozpatrzymy sobie zatem tylko jeden z nich.
Zaczynamy od analizy licznika przerwań. Wszystkie późniejsze instrukcje wykonają się wyłącznie wtedy, gdy licznik ten osiągnie wartość większą od 100 plus czas-n, a jak zaraz zobaczymy, czas-n jest wartością losową o maksimum równym 155. Dlaczego taka dziwna liczba? Otóż dla opóźnień przyjąłem zakres ośmiobitowy, a jak wiadomo 155 plus 100 daje maksymalną wartość mieszczącą się w tym zakresie.
Jak często zatem będziemy wykonywali fragment programu po tej linii? Policzmy: najczęściej po 100 plus zero razy 100 mikrosekund, czyli co 10 milisekund. Najrzadziej po 100 plus 155 razy 100 mikrosekund, co daje 25 i pół milisekundy.
Pierwszą rzeczą, którą trzeba będzie zrobić po spełnieniu powyższego warunku, to wyzerowanie licznika. Następnie będziemy mieć dwie alternatywne sytuacje: dioda będzie się rozjaśniać albo ściemniać. O tym zadecyduje przełącznik o nazwie przelacznik. Wartość prawdy, czyli jedynka – dioda się rozjaśnia, fałszu, czyli zera – ściemnia się. Więc jest logiczne, że przy rozjaśnianiu wartość zmiennej jasność będzie rosnąć. Przy ściemnianiu – maleć. Ale zrobiłem tutaj taką małą modyfikację: rozjaśnianie przebiega powoli, o jedną jednostkę. Ściemnianie natomiast dużo szybciej, o pięć jednostek. Po co tak? Bo tak jest po prostu ładniej.
Z chwilą osiągnięcia maksimum jasności – albo minimum, należy przelacznik przestawić, by w następnym wejściu w pętlę jasność odpowiednio gasła bądź rosła. Nazwa przelacznik bardzo dobrze oddaje sens tej zmiennej.
I już na końcu, ale tylko po osiągnięciu zerowej jasności, losujemy wspomniany czas-n, odpowiedzialny za szybkość animacji oraz kolor diody [los-n]. Z tego wynika, że kolor zmieniany jest tylko wtedy, gdy dioda wygasi się zupełnie. Po kompilacji zobaczymy dokładnie to, o czym wspominałem: a więc diody rozjaśniają się powoli, osiągają maksimum, po czym ściemniają się pięć razy szybciej i znowu się rozjaśniają, tym razem w przypadkowo innym kolorze. Wygląda to przyjemnie i właściwie moglibyśmy zakończyć na dziś, gdyby nie pewna obserwacja.
Jeśli dłużej popatrzymy na naszą choinkę, zauważymy, że czerwona barwa jak i pomarańczowa trafiają się rzadko, a najczęściej widać turkusy. Z czego to wynika? Matematyka działa dobrze, ale nasze oko nie domaga. Okazuje się, że barwom ciepłym przysługuje mały fragment koła HSV, natomiast zieleniom, turkusom i, nazwijmy to, mało niebieskim odcieniom niebieskiego, przysługuje nieproporcjonalnie więcej. Wprowadźmy zatem ostatnią już dziś modyfikację.
#include <Adafruit_NeoPixel.h> // Dołącz bibliotekę sterującą diodami WS2812
Adafruit_NeoPixel dioda = Adafruit_NeoPixel(5, 12, NEO_GRB + NEO_KHZ800); // Ustaw parametry zestawu: pięć diod na dwunastym porcie.
#include <TimerOne.h> // Dołącz bibliotekę obsługi przerwań.
byte licznik0 = 0; // Licznik wyznaczający podstawę czasu animacji diody.
byte jasnosc0 = 0; // Wartość jasności diody.
bool przelacznik0 = true; // Przełącznik: rozjaśnianie lub ściemnianie diody.
unsigned int los0 = 0; // Losowy kolor diody w bieżącej animacji.
byte czas0 = 0; // Losowy czas bieżącej animacji.
byte licznik1 = 0;
byte jasnosc1 = 0;
bool przelacznik1 = true;
unsigned int los1 = 0;
byte czas1 = 0;
byte licznik2 = 0;
byte jasnosc2 = 0;
bool przelacznik2 = true;
unsigned int los2 = 0;
byte czas2 = 0;
byte licznik3 = 0;
byte jasnosc3 = 0;
bool przelacznik3 = true;
unsigned int los3 = 0;
byte czas3 = 0;
byte licznik4 = 0;
byte jasnosc4 = 0;
bool przelacznik4 = true;
unsigned int los4 = 0;
byte czas4 = 0;
const unsigned int kolory[] = {
0, // czerwony
5461, // pomarańczowy
10922, // zółty
21845, // zielony
43691, // niebieski
54613 // fioletowy
};
void setup() {
dioda.begin(); // Przygotuj sterowniki znajdujące się w diodach do inicjalizacji.
dioda.show(); // Wykonaj powyższe polecenie.
Timer1.initialize(100); // Ustaw licznik na sto mikrosekund.
Timer1.attachInterrupt(przerwanie); // Włącz przerwanie z określeniem miejsca lądowania co ćwierć miliona mikrosekund.
}
void loop() {
dioda.setPixelColor(0, dioda.ColorHSV(kolory[los0], 255, jasnosc0)); // Wyślij komunikat włączający losową barwę i zmieniającą się jasność.
dioda.show(); // Wykonaj powyższe polecenie.
dioda.setPixelColor(1, dioda.ColorHSV(kolory[los1], 255, jasnosc1));
dioda.show();
dioda.setPixelColor(2, dioda.ColorHSV(kolory[los2], 255, jasnosc2));
dioda.show();
dioda.setPixelColor(3, dioda.ColorHSV(kolory[los3], 255, jasnosc3));
dioda.show();
dioda.setPixelColor(4, dioda.ColorHSV(kolory[los4], 255, jasnosc4));
dioda.show();
}
void przerwanie() { // Tutaj lądujemy za każdym razem, gdy wewnętrzny timer odliczy sto mikrosekund.
licznik0++; // Licznik wyznaczający podstawę czasu animacji diody.
licznik1++;
licznik2++;
licznik3++;
licznik4++;
if (licznik0 > 100 + czas0) { // Jeśli licznik wyznaczający podstawę czasu animacji diody, powiększony o wartość losową przekroczy 100, to...
licznik0 = 0; // Zeruj go.
if (przelacznik0 == true) { // Jeśli przełącznik jest ustawiony w pozycji: rozjaśnianie, to...
jasnosc0++; // Zwiększ bieżącą jasność o jeden.
if (jasnosc0 == 255) { // Jeśli osiągnęła maksimum, to...
przelacznik0 = false; // Przestw przełącznik w pozycji: ściemnianie.
}
} else { // Jeśli przełącznik jest ustawiony w pozycji: ściemnianie, to...
jasnosc0 = jasnosc0 - 5; // Zmniejsz bieżącą jasność o pięć.
if (jasnosc0 == 0) { // Jeśli osiągnęła minimum, to...
przelacznik0 = true; // Przestw przełącznik w pozycji: rozjaśnianie.
los0 = random(6); // Losuj adres koloru z tablicy dla kolejnej animacji.
czas0 = random(155); // Losuj wartość losową dla podstwy czasu kolejnej animacji.
}
}
}
if (licznik1 > 100 + czas1) {
licznik1 = 0;
if (przelacznik1 == true) {
jasnosc1++;
if (jasnosc1 == 255) {
przelacznik1 = false;
}
} else {
jasnosc1 = jasnosc1 - 5;
if (jasnosc1 == 0) {
przelacznik1 = true;
los1 = random(6);
czas1 = random(155);
}
}
}
if (licznik2 > 100 + czas2) {
licznik2 = 0;
if (przelacznik2 == true) {
jasnosc2++;
if (jasnosc2 == 255) {
przelacznik2 = false;
}
} else {
jasnosc2 = jasnosc2 - 5;
if (jasnosc2 == 0) {
przelacznik2 = true;
los2 = random(6);
czas2 = random(155);
}
}
}
if (licznik3 > 100 + czas3) {
licznik3 = 0;
if (przelacznik3 == true) {
jasnosc3++;
if (jasnosc3 == 255) {
przelacznik3 = false;
}
} else {
jasnosc3 = jasnosc3 - 5;
if (jasnosc3 == 0) {
przelacznik3 = true;
los3 = random(6);
czas3 = random(155);
}
}
}
if (licznik4 > 100 + czas4) {
licznik4 = 0;
if (przelacznik4 == true) {
jasnosc4++;
if (jasnosc4 == 255) {
przelacznik4 = false;
}
} else {
jasnosc4 = jasnosc4 - 5;
if (jasnosc4 == 0) {
przelacznik4 = true;
los4 = random(6);
czas4 = random(155);
}
}
}
}
Zadeklarujemy sobie tablicę kolorów kolory z sześcioma barwami. Nie musi być ich sześć, może być więcej albo mniej. Wartości kolorów pobrałem z Photoshopa, w którym można pobierać dane w systemie HSV dla konkretnych kolorów.
Trzeba jeszcze zamienić współczynniki funkcji random w miejscu, gdzie wcześniej losowaliśmy jeden z 65 tysięcy kolorów (los-n). Obecnie mamy ich tylko sześć.
Ostatnią poprawką będzie zmiana już w głównej pętli, gdzie to będziemy się odwoływać do indeksu tablicy z osadzoną barwą. Po kompilacji choinka zaświeci w bardziej zdecydowanych i jednoznacznych barwach.
Zapraszam do eksperymentów. Animacje światła to ogrom możliwości i czasem wystarczy zmienić jeden tylko współczynnik, by otrzymać zdecydowanie inny efekt.