Skocz do zawartości

Diody ws2812b i Arduino UNO


Elvis

Pomocna odpowiedź

Skoro zasilanie pasków ws2812b nie stanowi już problemu, czas poświęcić kilka chwil na sterowanie nimi. Jak już wcześniej wspominałem, użycie Arduino oraz biblioteka Adafruit_Neopixel (https://github.com/adafruit/Adafruit_NeoPixel) niesamowicie ułatwia zadanie - właściwie wystarczy podłączyć przewody, wgrać przykładowy program ewentualnie dostosowując liczbę sterowanych diod i gotowe.

Oczywiście nie wszystko jest takie idealne, więc może warto dokładniej przyjrzeć się takiemu rozwiązaniu, poznać jego zalety i wady, a przede wszystkim poznać lepiej sposób sterowania ws2812b.

Ma początek trochę dokumentacji. Okazuje się że do WS2812B znajdziemy kilka różnych datasheet-ów. Przykładowo:

Na pierwszy rzut oka różnice nie są duże, ale w pewnych momentach istotne.

Każdy moduł ws2812b składa się z układu sterującego oraz trzech diod świecących (czerwonej, zielonej i niebieskiej).

ws2812b_14a.thumb.png.13108d7414c13911e61ae7a743d29f36.png

Moduł nie dość że jest mały to posiada raptem 4 wyprowadzenia. Dwa służą do zasilania, a pozostałe to wejście i wyjście danych.

Dzięki posiadaniu również wyjścia danych możliwe jest łatwe łączenie ws2812b w długie łańcuchy - wyjścia modułów są po prostu łączone z wejściami kolejnych.

Oznacza to, że cała komunikacja z mikrokontrolerem ogranicza się do jednego sygnału, czyli wejścia pierwszego modułu w całym łańcuchu.

W dokumentacji znajdziemy opis sterowania ws2812b, która opiera się na przesyłaniu trzech możliwych kodów: bitu o wartości 0, 1 oraz sygnału reset.

ws2812b_14.thumb.png.8de4194f1fded434f909427f7b45ebbe.png

Jak widać diagramy są raczej proste, pozostaje rozszyfrować ile wynoszą zaznaczone na nich czasy, czyli T0H, T0L, T1H, T1L oraz Treset.

Okazuje się że każda dokumentacja podaje trochę inne wartości:

ws2812b_15.thumb.png.05cfc638b44ae0be31ede5633351f710.pngws2812b_15b.thumb.png.ecd0311fb9c1b759c4ad70fc98f82cee.pngws2812b_15c.thumb.png.d5c207d6579b681f1b8a0c25256a8050.png

Na szczęście różnice nie są ogromne, widzimy więc że dla bitu 0 mamy mieć przez ok. 0,4us stan wysoki, a następnie przez 0,8us stan niski. Dla bitu o wartości 1, najpierw przez 0,8us stan wysoki, a następnie przez 0,4us stan niski. W przypadku resetu, stan niski ma trwać co najmniej 50us - ale może być dowolnie długo.

Teraz możemy zobaczyć jak wygląda sterowanie 24 diodami za pomocą Arduino oraz biblioteki Adafruit_NeoPixel. Kod programu jest banalnie prosty:

#include <Adafruit_NeoPixel.h>

#define NUMPIXELS   24
#define PIN         6 

Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

void setup()
{
}

void loop()
{
  pixels.begin(); 
  pixels.clear(); 
  for (int i = 0; i < 24; i++)
    pixels.setPixelColor(i, pixels.Color(0x00, 0x01, 0x00));
  pixels.show();   
}

Jego działanie polega na bardzo słabym wysterowaniu wszystkich 24 diod - wartość 0x01 oznacza 1/256 wypełnienia PWM dla diody zielonej. Okazuje się że nawet tak małe wysterowanie zapewnia całkiem widoczne świecenie diody. Wartość wybrałem jednak z innego powodu, chodziło o łatwiejsze przeanalizowanie wyników odczytanych analizatorem.

Na początek cała transmisja:

ws2812b_16.thumb.png.79ade23957db8b21a884fd2551e72f16.png

Jak widzimy po prawej stronie, zajmuje ona ok. 720us. Policzmy, mamy 24 diody, każda ma 3 składowe po 8 bitów dla każdej. Czyli 24 x 3 x 8 = 576 bitów danych. Transmisja każdego bitu ma według dokumentacji trwać 1,25us (+-0,6us), więc wychodzi nam idealnie to co zmierzyliśmy. Między transmisjami jest dużo wolnego czasu, kiedy na linii danych jest stan niski - dzięki temu układy ws2812b wykrywają reset transmisji i wiedzą kiedy rozpocząć przesyłanie nowych danych.

Teraz możemy przyjrzeć się transmisji bitów

ws2812b_17.thumb.png.09842882dd0fd8fbf1caf1f72d209f0a.png

Ogólnie większość danych testowych to zera, przy pierwszym przesyłanym zerze widzimy czasy: stan wysoki, czyli T0H ma 312us, a stan niski to pozostałe 9,938us. Wartości jak widzimy odpowiadają dokumentacji.

Teraz dwa słowa o kolejności danych: najpierw przesyłane jest 8 bitów dla diody zielonej pierwszego modułu ws2812b, następnie 8 bitów dla czerwonej, a na koniec dla niebieskiej. Jeśli mamy więcej niż jeden ws2812b w łańcuchu to bezpośrednio po danych dla pierwszego, przesyłane są dane dla drugiego i kolejnych. Kolejność barw jest więc trochę nietypowa to GRB.

Dane dla każdej diody przesyłane są zaczynając od najbardziej znaczącego bitu.

W ramach testów używany był kod:

pixels.setPixelColor(i, pixels.Color(0x00, 0x01, 0x00));

Biblioteka Neopixel używa zanacznie popularniejszej kolejności kolorów, czyli RGB, więc nasz przykład oznacza R=0, G=1, B=0. Skoro najpierw przesyłana jest składowa dla zielonej diody, powinniśmy zobaczyć pierwszy transmitowany bajt o wartości 1:

ws2812b_17b.thumb.png.69530b8d004b18d9aa54057ca954a43e.png

Zgodnie z oczekiwaniami, transmisja zaczyna się od najwyższego bitu, a my wysyłamy 7 bitów zerowych. Ósmy bit ma wartość "1" i widzimy jego czasy podświetlone na obrazku.

Tym razem stan wysoki T1H trwał 0,814 us, a niski 0,432 us - czyli zgodnie z oczekiwaniami.

Mając takie wyniki nie powinniśmy być zaskoczeni, że wszystko pięknie działa. Testowałem bibliotekę Neopixel używając do 432 diod ws2812b i nie było najmniejszych problemów. Pewnie można byłoby użyć więcej, ale pojawiają się pierwsze ograniczenia Arduino UNO - mała ilość pamięci RAM. Biblioteka Adafruit_Neopixel przechowuje kolory wszystkich diod w pamięci, więc dla każdej diody konieczne są 3 bajty. Oznacza to że 432 diody wymagały 1296 z dostępnych 2048. Trochę pamięci trzeba zostawić dla programu, więc przy tak prostym sterowaniu raczej nie można liczyć na więcej niż 500 diod - co i tak jest chyba niezłym wynikiem.

Czy wszystko jest tak idealnie jak się wydaje? Okazuje się że niestety nie do końca. Po pierwsze nasz mikrokontroler ma mnóstwo pracy przy sterowaniu diodami ws2812b - wszystko wykonywane jest programowo, więc wymaga mnóstwo pracy. To nie jest jakaś ogromna wada, bo i tak nasze Arduino nie ma nic lepszego do roboty w Święta.

Druga wada jest poważniejsza - dla 24 diodek transmisja trwała 720us, ale przy 432 będzie to już 12960 us, czyli prawie 13 ms. Okazuje się że biblioteka Adafruit_Neopixel aby zapewnić niezbędne przebiegi czasowe na cały czas transmisji wyłącza obsługę przerwań. Oznacza to że większość modułów używających przerwań jak np. port szeregowy, a nawet funkcja millis() może nie działać prawidłowo. Możemy sobie z tym poradzić, ale takie proste rozwiązanie nie będzie niestety działało z każdą biblioteką i kodem.

Jeśli więc chcemy przygotować pięknie migający łańcuch diod na choinkę, możemy po prostu użyć Arduino i biblioteki Neopixel. Natomiast jeśli chcemy zrobić coś więcej, trzeba będzie poznać diody ws2812b nieco lepiej.

 

  • Lubię! 2
Link do komentarza
Share on other sites

Żeby lepiej zrozumieć sterowanie ws2812b postanowiłem spróbować napisać własną obsługę tych diod. Od razu chciałbym wyjaśnić, że nie mam aspiracji do napisania lepszej biblioteki niż Adafruit_Neopixel - kod tej biblioteki został wielokrotnie przetestowany, napisany w asemblerze i świetnie zoptymalizowany. Jest więc pod prawie każdym względem lepszy - tym czego mu brakuje to czytelność, ale nie można mieć o to chyba pretensji. Ja chciałbym tylko napisać prosty kod prototypowy, który pozwoli mi zrozumieć sterowanie ws2812b.

Na początek samo sterowanie liniami GPIO - ws2812b wymaga raczej szybkiego sterowania, więc zanim zabiorę się do pracy trzeba upewnić się, że jest to wykonalne.

Każdy, kto miał do czynienia z Arduino wie jak proste jest używanie funkcji digitalWrite() i sterowanie pinami. Zacznę więc od wręcz banalnego programu:

#define LED_PIN         6 

void setup()
{
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_PIN, HIGH);
  digitalWrite(LED_PIN, LOW);
}

Podłaczam analizator i mogę zobaczyć co udało mi się uzyskać:

ws2812b_18.thumb.png.d2621004697185c50e3fbd144c5a6f63.png

Szerokość stanu wysokiego to 3,8 us, a do sterowania ws2812b konieczne są ~0,4us - więc digitalWrite() jest miłe i sympatyczne, ale zupełnie się nie nadaje.

Na szczęście pod Arduino są dostępne też inne biblioteki. Spróbuję użyć FastGPIO (https://github.com/pololu/fastgpio-arduino)

Nie znam tej biblioteki za bardzo, więc jeśli program jest niepoprawny to proszę śmiało pisać. W każdym razie udało mi się przygotować coś takiego:


#include <FastGPIO.h>

#define LED_PIN         6 

void setup()
{
  FastGPIO::Pin<LED_PIN>::setOutput(1);
}

void loop() {
  FastGPIO::Pin<LED_PIN>::setOutput(1);
  FastGPIO::Pin<LED_PIN>::setOutput(0);
  FastGPIO::Pin<LED_PIN>::setOutput(1);
  FastGPIO::Pin<LED_PIN>::setOutput(0);
}

 

Efekt działania programu wygląda następująco:

ws2812b_18b.thumb.png.54b71f8194ac377d37b1a055725b93ef.png

Jak widać impuls ma teraz szerokość 0,252us - to 15 razy lepiej niż przy użyciu digitalWrite !

Poprzednio sterowanie wyjściem było tak powolne, że czas spędzony poza pętlą loop() nie miał praktycznie znaczenia. Używając szybszej biblioteki możemy jednak zobaczyć, że dwa impulsy są generowane z odstępem równym ich szerokości, ale kolejna przerwa jest trochę większa (ma 0,5us). Wywołanie funkcji loop() oraz pętli głównej trochę zajmuje, więc mierząc czasy trzeba o tym pamiętać.

Na koniec spróbujmy zupełnie pominąć biblioteki Arduino i bezpośrednio odwołać się do rejestrów mikrokontrolera

#define LED_PIN         6 

void setup()
{
  DDRD |= _BV(6);
}

void loop()
{
  PORTD |= _BV(6);
  PORTD &= ~_BV(6);
  PORTD |= _BV(6);
  PORTD &= ~_BV(6);
}

Teraz wyniki są już prawie takie jak powinny:

ws2812b_18c.thumb.png.5dbfa3c767294763b7cd026c9bc11ee4.png

Arduino UNO jest taktowane zegarem o częstotliwości 16MHz, co oznacza że czas wykonywania instrukcji to 62,5ns (albo więcej). Skoro uzyskaliśmy szerokość impulsu 126ns to znaczy że zmiana stanu wyjścia wymaga dwóch cykli maszynowych. Nie jest źle, ale w asemblerze biblioteki Adafruit_Neopixel pewnie mogą osiągnąć więcej.

W każdym razie ja postanowiłem zostać przy tej wersji - nadal jest to język C, ale jak widać wydajność starej szkoły bezpośredniego dostępu do rejestrów jest trudna do pobicia nawet przez C++.

Przy okazji mała dygresja odnośnie przerwań - jak wspominałem wcześniej biblioteka Neopixel wyłącza obsługę przerwań na czas całej komunikacji z ws2812b. Mając prosty program testowy można ładnie zobaczyć dlaczego:

ws2812b_19.thumb.png.ff935ba0d10ac30175f98ec776752639.png

Jak widzimy obsługa przerwania zajmuje prawie 6,5us i wówczas program nie może "machać" pinem. Dlatego obsługując ws2812b autorzy biblioteki wyłączają przerwania - żeby w trakcie transmisji nie pojawiały się przerwy trwające kilka mikrosekund.

  • Lubię! 1
Link do komentarza
Share on other sites

(edytowany)

Pisanie w języku C lub C++ programów, które mają działać z dokładnością do cykli procesora jest w sumie bez sensu, ale postanowiłem chociaż spróbować. Używając analizatora oraz dobierając mocno empirycznie wartości udało mi się przygotować taki kod na wysyłanie bitu zerowego:

static inline void send_bit_zero(void)
{ 
  PORTD |= _BV(6);
  _NOP();
  PORTD &= ~_BV(6);
  for (uint8_t i = 0; i < 4; i++)
    _NOP();
}

Najważniejsze jest małe opóźnienie między ustawieniem stanu wysokiego na wyjściu, a powrotem do stanu niskiego - to raptem jeden _NOP().

Aby wysłać bit o wartości "1" trzeba dodać większe opóźnienie:

static inline void send_bit_one(void)
{ 
  PORTD |= _BV(6);
  for (uint8_t i = 0; i < 8;i++)
    _NOP();
  PORTD &= ~_BV(6);
}

Teraz już można napisać cały program. Jak wspominałem bity są wysyłane zaczynając od najbardziej znaczącego, kolejność kolorów to GRB

#define LED_PIN         6 

void setup()
{
  DDRD |= _BV(6);
}

static inline void send_bit_zero(void)
{ 
  PORTD |= _BV(6);
  _NOP();
  PORTD &= ~_BV(6);
  for (uint8_t i = 0; i < 4; i++)
    _NOP();
}

static inline void send_bit_one(void)
{ 
  PORTD |= _BV(6);
  for (uint8_t i = 0; i < 8;i++)
    _NOP();
  PORTD &= ~_BV(6);
}

static inline void send(uint8_t value)
{
  for (uint8_t i = 0; i < 8; i++ ) {
    if (value & 0x80)
      send_bit_one();
    else
      send_bit_zero();
    value <<= 1;
  }
}

void loop()
{
  cli();
  for (int i = 0; i < 24; i++) {
    send(0x01);
    send(0);
    send(0);
  }
  sei();

  delay(10);
}

Gdy podłączymy analizator zobaczymy, że efekt jest dużo gorszy niż biblioteka Adafruit_Neopixel, ale co ważne działa poprawnie - testowałem nawet z 4 x 144 diodami.

Tutaj wynik dla 24 diod:

ws2812b_20.thumb.png.25788b15a9a8f5328e6c14f0392c4ad1.png

Problemy pojawiają się po transmisji bajtu - powroty z funkcji, obsługa pętli itd zajmuje czas 😞 To niestety widać podczas transmisji:

ws2812b_20b.thumb.png.307a42b79aada1c7ced35f0adab07471.pngws2812b_20c.thumb.png.6bfdc8bf75f2558f98ef0ca5832ab87b.png

Jak widać napisanie dobrego programu do sterowania ws2812b nie jest łatwe - ten który przygotowałem działa, ale nie jestem z niego dumny. Chciałem natomiast przetestować moje rozumienie komunikacji z ws2812 i pokazać że to nie czarna magia. Po prostu trzeba dbać o dokładne czasy generowanych przebiegów. Co więcej sprzęt potrafi nam wybaczyć nawet więcej niż gwarantuje dokumentacja.

Edytowano przez Elvis
  • Lubię! 2
Link do komentarza
Share on other sites

Zarejestruj się lub zaloguj, aby ukryć tę reklamę.
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.

jlcpcb.jpg

jlcpcb.jpg

Produkcja i montaż PCB - wybierz sprawdzone PCBWay!
   • Darmowe płytki dla studentów i projektów non-profit
   • Tylko 5$ za 10 prototypów PCB w 24 godziny
   • Usługa projektowania PCB na zlecenie
   • Montaż PCB od 30$ + bezpłatna dostawa i szablony
   • Darmowe narzędzie do podglądu plików Gerber
Zobacz również » Film z fabryki PCBWay

Ale ta choinka to już świeci, tak? Bo wiesz, eksperymenty eksperymentami a tu Wigilia za pasem. Napisz czy spadają sople białego światła albo może wirują płatki śniegu? Bo oprócz schowanych pod pokrywką technikaliów najważniejsze w tym przypadku są "wrażenia artystyczne", prawda? Zrobiłeś krótkie, osobne paski z własnymi zasilaniami czy jeden długi łańcuch? Może jakiś film?

I Wesołych Świąt dla wszystkich ❄️🎄🌟🎅🎁🧦🙂

Link do komentarza
Share on other sites

(edytowany)

Choinki jeszcze nie kupiłem, więc są ostatnie chwile na przygotowania 😉 Natomiast czy wrażenia artystyczne są takie ważne nie jestem pewien - w każdym razie oświetlenie choinki to dla mnie dobry pretekst do zajęcia się ws2812b, których trochę nazbierałem a jakoś ciągle brakowało czasu.

Edytowano przez Elvis
Link do komentarza
Share on other sites

Znalazłem na Ali pasek tych diod, w atrakcyjnej cenie i gęstości upakowania 60LED/metr i korci mnie podmienić moje stare RGB w listwie na to. Ale malutki sterownik bym musiał zbudować, na jakim miniaturowym ATTiny85, lub ESp8266. Jest też pasek o upakowaniu 144LEd/metr, ale to już cenowo i gabarytowo, jest nieopłacalne.

Ledy_ali.jpg

Link do komentarza
Share on other sites

Dnia 22.12.2019 o 21:12, Elvis napisał:

Natomiast jeśli chcemy mieć poprawne sterowanie ws2812b oraz możliwość obsługi przerwań musimy zmienić platformę sprzętową. Arduino UNO nie ma niestety odpowiednich modułów peryferyjnych

Arduino UNO ma SPI i można sterować diodami na przerwaniach.

Link do komentarza
Share on other sites

Widzę, że pisząc cokolwiek w internecie trzeba niesamowicie uważać na słówka - faktycznie źle się wyraziłem, na pewno z Arduino i atmegii 328p da się jeszcze niejedno wycisnąć. Co do obsługi przerwań to nie byłbym pewien, czy to dobry kierunek - standardowe przerwanie od timera zajmuje ponad 6us, a transfer jednego bitu - 1,25us. Więc nawet mając 3 bity do przesłania (np. używając UART), zaczyna brakować czasu. Tak jak napisałem wcześniej - ws2812b są o wiele bardziej tolerancyjne na przerwy w komunikacji niż podaje specyfikacja, więc użycie przerwań i SPI, PWM lub UART pewnie zadziała, tyle że to nie będzie sterowanie zgodne z dokumentacją i to miałem na myśli pisząc o zmianie platformy.

W tym co napisałem chciałem pokazać że za pomocą Arduino UNO można sterować nawet dość długim łańcuchem diod ws2812b. Nie jest to jedyne możliwe rozwiązanie i postaram się jeszcze opisać inne możliwości - dlatego wspomniałem o innych platformach, chociaż może niezbyt dobrze dobrałem słowa.

Link do komentarza
Share on other sites

Dnia 25.12.2019 o 11:43, Elvis napisał:

standardowe przerwanie od timera zajmuje ponad 6us, a transfer jednego bitu - 1,25us. Więc nawet mając 3 bity do przesłania (np. używając UART), zaczyna brakować czasu.

W Internecie widziałem projekty razem z kodami źródłowymi obsługujące LED na przerwaniach od UART więc się da. Kody były w C ale była też opcja z wstawką asemblerową, która szybciej wykonywała przerwanie.

Link do komentarza
Share on other sites

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.