Autor: tmf
Redakcja: Dondu
Artykuł jest częścią cyklu: Wstęp do mikrokontrolerów XMEGA Atmel'a.
Kolejne artykuły dot. XMEGA pojawiają się co jakiś czas na blogu, a w niniejszym chciałbym Wam pokazać coś fajnego – sterowanie diodą WS2812B.
Każdy z nas lubi jak coś się świeci lub kręci. O silnikach może będzie później, dzisiaj zajmiemy się przykładem tworzenia biblioteki do obsługi programowalnych diod WS2812B.
Co w nich jest fajnego? Moim zdaniem jest to bardzo ciekawy pomysł, na rozwiązanie wielu problemów, z którymi borykali się miłośnicy diod RGB. W przypadku klasycznej diody RGB różne kolory uzyskujemy sterując intensywnością trzech kolorów podstawowych: czerwonego, zielonego i niebieskiego. Zmianę intensywności koloru podstawowego uzyskuje się doprowadzając przebieg PWM o zmiennym wypełnieniu – im będzie ono większe tym jaśniejszy kolor podstawowy uzyskamy.
Wynika z tego, że żeby sterować diodą RGB potrzebujemy 3 kanały PWM. Niewiele, tym bardziej, że w XMEGA dysponujemy nawet 24 16-bitowymi kanałami PWM, a jeśli zadowala nas 8-bitowa rozdzielczość (w przypadku diody zasadniczo to wystarczy) mamy do dyspozycji nawet 32 kanały PWM.
Dioda WS2812B na taśmie Źródło: www.instructables.com |
Z jednej strony to dużo, z drugiej, daje to możliwość bezpośredniego podłączenia zaledwie dziesięciu diod RGB, w dodatku wymaga to użycia 30 pinów IO. A czy da się inaczej?
Oczywiście możemy zastosować multipleksowanie, w swojej książce „AVR. Praktyczne projekty” (recenzja) pokazałem nawet przykład sterowania matrycą RGB, gdzie właśnie dzięki użyciu multipleksowania możemy podłączyć nawet kilkaset diod z możliwością indywidualnego sterowania kolorem każdej z nich.
Ale od jakiegoś czasu mamy do dyspozycji inne rozwiązanie – wykorzystanie „inteligentnej” diody WS2812B. W stosunku do zwykłej diody RGB różni się ona wbudowanym sterownikiem, który zapewnia indywidualne sterowanie jasnością każdego koloru podstawowego z 8-bitową rozdzielczością.
W efekcie dioda może wyświetlić jeden z 2^24 kolorów, teoretycznie umożliwia ona uzyskanie przestrzeni barw odpowiadającej TrueColor. Piszę teoretycznie, bo ze względu na nieliniową charakterystykę diody, aby móc rzeczywiście wyświetlić wszystkie barwy przestrzeni TrueColor musielibyśmy mieć możliwość ustalania jasności diody ze znacznie większą precyzją. Ale bez obaw – możliwości jakie daje ta dioda i tak są zachwycające i zadowolą nawet bardzo wybredne osoby.
Tyle tytułem wstępu, spróbujmy wysterować te diody za pomocą mikrokontrolera XMEGA.
Użyty protokół komunikacyjny
Zacznij jak zwykle od lektury dokumentacji: WS2812B-datasheet.pdf (kopia)Sterowanie indywidualną diodą wymaga przekazania jej 24 bitów zawierających informacje o składowych RGB koloru. W tym celu sterownik diody posługuje się prostym protokołem szeregowym, w którym bity o wartości 0 i 1 kodowane są poprzez zmianę szerokości impulsu dodatniego. Popatrzmy do noty jak to wygląda:
Czasy i protokół |
Jak widzimy, czas trwania całego bitu wynosi 1250ns ±300 ns, bit o wartości 1 nadawany jest jako impuls dodatni o szerokości 800ns ±150 ns, po którym następuje impuls ujemny o szerokości 450ns ±150ns, natomiast bit o wartości 0 kodowany jest przez impuls dodatni o szerokości 400ns ±150ns, po którym następuje impuls ujemny o szerokości 850ns ±150ns, co ilustruje poniższy rysunek:
Czasy trwania bitów |
Sygnałem do zatrzaśnięcia danych jest sygnał RESET, który trwa, co najmniej 50 µs:
Czas trwania sygnału RESET WS2812B |
Co prawda RESET kojarzy nam się z zerowaniem układu, lecz w przypadku diod WS2812B jego nazwa jest nieco niefortunna i nie odzwierciedla do końca jego działania. Powoduje on zatrzaśnięcie przesłanych danych, w efekcie, czego dioda zmienia kolor, a także jest sygnałem rozpoczynającym przesył nowych danych.
Same dane przesyłane są w dosyć ciekawy sposób. Otóż każda dioda ma wejście danych (DIN) i wyjście danych (DOUT), które łączymy z wejściem DIN kolejnej diody, tworząc w ten sposób łańcuszek:
Kaskadowe łączenie diod WS2812B |
Dzięki temu każda dioda odbiera 24 bity danych (po 8 bitów na każdą składową koloru), po czym kolejne pojawiające się dane po prostu retransmituje z wejścia DIN na wyjście DOT, skąd pobiera je kolejna dioda itd.
Widzimy więc, że pierwsze wysłane 24 bity trafią do pierwszej diody w łańcuszku, kolejne 24 bity do drugiej diody itd. Taki sposób przekazywania danych ma dużą zaletę – każda dioda nie tylko je retransmituje, ale także odtwarza przesyłany sygnał, dzięki temu unikamy jego zniekształcenia. Ponieważ każda dioda pobiera spory prąd, a zmieniając swój stan (kolor) pobierany prąd zmienia się skokowo, przy każdej diodzie powinniśmy umieścić kondensator o wartości 10-100nF – zwykle producenci taśm zawierających te diody montują go, dzięki czemu taśmy możemy użyć bezpośrednio w budowanym układzie.
Jeśli kupiliśmy po prostu „gołe” diody pamiętajmy o kondensatorach.
Schemat łączenia diod WS2812B |
Przy okazji warto wspomnieć, że zasilanie diody nie może przekraczać przedziału 3,5-5,3 V.
Wiemy już prawie wszystko, jeszcze tylko warto wspomnieć o formacie kodowania kolorów:
Format kodowania kolorów |
Jak widzimy dane przesyłane są dosyć nietypowo w formacie GRB, a nie RGB, przy czym transmisja rozpoczyna się od najbardziej znaczącego bitu (MSB) określającego kolor zielony. Trudno powiedzieć, czemu akurat tak producent sobie wymyślił, na szczęście nie jest to jakimś większym problemem.
Komunikacja z diodą
Przystąpmy więc do próby realizacji komunikacji z diodą. Jak widzieliśmy, diody WS2812B wykazują się dużą tolerancją czasów, w efekcie wygenerowanie odpowiedniego przebiegu powinno być proste. Ale czy na pewno?Dla procesora taktowanego zegarem 4 MHz jeden cykl trwa aż 250ns, stąd też, czas równy około 1200ns, czyli czas trwania jednego bitu to zaledwie 5 instrukcji asemblera (zakładając, że każda jest wykonywana w jednym takcie). Dla wyższych częstotliwości taktowania sytuacja wygląda nieco lepiej, lecz zachowanie katalogowej tolerancji czasów na poziomie ±150ns wymaga taktowania mikrokontrolera wynoszącego co najmniej 8 MHz i… prawdopodobnie użycia asemblera.
Na szczęście nie jest tak źle, czas trwania bitu możemy trochę wydłużyć (ale nie należy go skracać poniżej 1100ns), także czas trwania stanów wysokich możemy nieco zmienić – w szczególności możemy znacznie skrócić czas trwania dodatniego impulsu przy przesyłaniu bitu o wartości 0. Dzięki temu z łatwością wygenerujemy odpowiednie czasy przy pomocy instrukcji opóźniających w C.
W naszym programie zdefiniujemy sobie pin przy pomocy którego będzie realizowana transmisja:
#define WS2812B_PORT PORTB //Port na który wysyłane są dane #define WS2812B_PIN (1<<0) //Nr wykorzystywanego pinu
Zacznijmy od napisania funkcji wysyłających bit o wartości 1:
void WS2812B_sendOne() { WS2812B_PORT.OUTSET=WS2812B_PIN; //Ustaw pin sterujący _delay_loop_2(8*F_CPU/10000000UL/3); WS2812B_PORT.OUTCLR=WS2812B_PIN; //Wyzeruj pin sterujący _delay_loop_1(1); }
Powyższa funkcja najpierw ustawia pin sterujący wejściem DIN diody – do generowania krótkiego opóźnienia wykorzystywana jest funkcja biblioteczna _delay_loop_2, której każdy krok iteracji trwa 4 takty CPU. Dzięki jej wykorzystaniu generujemy impuls dodatni o czasie trwania charakterystycznym dla bitu o wartości 1, po nim na czas równy reszcie czasu trwania bitu stan pinu jest niski.
Podobnie wygląda funkcja wysyłająca bit o wartości 0:
void WS2812B_sendZero() { WS2812B_PORT.OUTSET=WS2812B_PIN; //Ustaw pin sterujący asm volatile ("nop"); asm volatile ("nop"); asm volatile ("nop"); asm volatile ("nop"); WS2812B_PORT.OUTCLR=WS2812B_PIN; //Wyzeruj pin sterujący _delay_loop_2(8*F_CPU/10000000UL/3); }
Jedyna różnica jest taka, że tym razem czas trwania impulsu dodatniego jest bardzo krótki (ale wystarczający do prawidłowej identyfikacji przez diodę), przez całą resztę czasu trwania bitu pin sterujący ma stan niski.
Mając powyższe dwie funkcje z łatwością napiszemy funkcję wysyłającą bajty:
void WS2812B_send(uint8_t byte) { uint8_t cnt=8; while(cnt--) { if(byte & 0x80) WS2812B_sendOne(); else WS2812B_sendZero(); byte<<=1; } }
Jak widzimy, w zależności od stanu najstarszego bitu (pamiętamy, że WS2812B oczekuje na bity począwszy od najbardziej znaczącego) wywoływana jest funkcja wysyłająca bit o wartości 0 lub 1 po czym przesuwane są wszystkie bity o jedną pozycję w prawo.
Potrzebujemy jeszcze jednej funkcji – generującą sygnał zatrzaśnięcia danych i zresetowania komunikacji:
void WS2812B_reset() { WS2812B_PORT.OUTCLR=WS2812B_PIN; //Wyzeruj pin sterujący _delay_us(50); }
Powyższa funkcja chyba nie wymaga wyjaśnień.
Zapomnieliśmy o jednej, bardzo ważnej sprawie – inicjalizacji pinu, który wykorzystamy do komunikacji:
void WS2812B_init() { WS2812B_PORT.OUTCLR=WS2812B_PIN; //Wyzeruj pin sterujący WS2812B_PORT.DIRSET=WS2812B_PIN; //I zmień go na wyjście }
Jak wiemy, w XMEGA mamy do dyspozycji specjalne rejestry OUTCLR – ustawienie w nim jednego z 8 bitów zeruje odpowiadające im piny IO, podobnie jak w rejestrze DIRSET – ustawienie bitu powoduje, że odpowiadający mu bit rejestru DIR jest ustawiany, w efekcie czego odpowiadający mu pin IO staje się wyjściem.
W ten sposób, możemy zmieniać stan pinu IO i jego kierunek, bez wpływu na pozostałe piny portu. Warto zauważyć, że inne modele AVR w tym celu wymagają zazwyczaj wykonania operacji read-modify-write, która jest dłuższa, a co gorsze – nie jest wykonywana w sposób atomowy.
Mamy już wszystko co potrzebne – napiszmy więc małe demko. Zaczniemy od konfiguracji zegara taktującego XMEGĘ, w tym celu przy pomocy PLL pomnożymy zegar podstawowy o częstotliwości 2MHz razy 16, co da nam zegar o częstotliwości 32 MHz, a następnie zmusimy mikrokontroler, aby z niego korzystał:
SelectPLL(OSC_PLLSRC_RC2M_gc, 16); //Taktowanie CPU - 32 MHz CPU_CCP=CCP_IOREG_gc; //Odblokuj zmianę konfiguracji CLK.CTRL=CLK_SCLKSEL_PLL_gc; //Wybierz PLL
Teraz zainicjujemy sobie pin IO wykorzystywany do komunikacji i zresetujemy magistralę:
WS2812B_init(); WS2812B_reset();
Na koniec proste demko:
uint8_t cnt=LEDNO; uint8_t offset=0; uint8_t delta=256/LEDNO; while(1) { WS2812B_reset(); while(cnt--) { WS2812B_send(cnt*delta+offset); //Wyślij składową G WS2812B_send(0); //Wyślij składową R WS2812B_send(0); //Wyślij składową B } offset+=delta; _delay_ms(10); }
Cały kod projektu w formacie Atmel Studio 6.x możesz pobrać poniżej – jest on dostosowany do mikrokontrolera XMEGA256A3BU, lecz jeśli posiadasz inną XMEGĘ to wystarczy tylko zmienić typ MCU i zrekompilować projekt.
Do pobrania
Spakowany projekt: WS2812B-bb.zip (kopia)
Problemy
Mamy prosty kod, który doskonale działa, więc pozornie wszystko jest ok, prawda? Niestety powyższy kod jest przykładem tego jak zasadniczo nie należy programować! Dlaczego?Z kilku powodów, z których najważniejszy to marnotrawstwo czasu procesora. Zauważmy, że czas trwania bitu wynosi ok. 1200ns, a dzięki zastosowaniu opóźnień, mikrokontroler w tym czasie nie robi nic innego, poza generowaniem przebiegu sterującego diodą. Tyle, że jak widzimy to samo może zrobić MCU taktowany zegarem zaledwie 4 MHz, a my pędzimy nasz MCU zegarem 32 MHz. Lecz nic to nie zmienia – co prawda w tym samym czasie nasz MCU wykonuje o wiele więcej instrukcji, lecz wszystkie one robią tylko jedno – generują potrzebne opóźnienia.
Jak widzisz, powyższy kod jest tym gorszy im szybszy mikrokontroler wykorzystujemy – powyższy kod sprowadza nawet wypasionego ARMa funkcjonalnie do prostego mikrokontrolera o niewielkiej mocy obliczeniowej.
Pokazany kod ma jeszcze jedną wadę – aby generowane opóźnienia były dokładne, czego wymaga protokół transmisji z WS2812B, na czas wysyłania danych musimy zablokować przerwania. W naszym, prostym przykładzie, tego nie robimy, gdyż po prostu nie korzystamy z przerwań.
Normalnie, jakiekolwiek przerwanie, które wystąpi w czasie nadawania danych kompletnie zrujnuje nam timingi! W efekcie powyższy kod może być polecany jeśli:
- jesteśmy dopiero w przedszkolu programowania MCU,
- chcemy sobie na szybko odpalić komunikację z taśmą, bo nie możemy się doczekać pierwszych efektów,
- piszemy naprawdę prostą aplikacyjkę, która wygeneruje nam kilka prostych efektów wizualnych – tak jak powyższy przykład.
Warto też pamiętać, że transmitując dane do układów WS2812B nie możemy tak po prostu przerwać transmisji. Przedłużenie stanu niskiego ponad 50µs zresetuje nam transmisję (tak naprawdę reset może już wystąpić przy czasach rzędu 10-20µs), z kolei przetrzymanie magistrali w stanie wysokim spowoduje nieprawidłową interpretację kolejnego bitu.
Stąd też wszystkie dane musimy wysyłać jednym ciągiem, a odstęp pomiędzy kolejnymi bitami nie może być dłuższy niż czas wymagany do zresetowania magistrali. W efekcie niezwykle trudno jest wykorzystać pokazany kod w bardziej rozbudowanych aplikacjach.
Jak więc powyższy kod poprawić? O tym piszę w drugiej części tego mini cyklu.
Gdzie tak tanio można kupić te taśmy?
OdpowiedzUsuńCeny na ebay-u dużo wyższe z tego co widzę :(
OdpowiedzUsuńAch ciapa ze mnie - nie doczytałem oferty pomino, że nie była w języku mandaryńskim :-)
UsuńNiemniej jednak sprowadzenie ich z Chin jest zdecydowanie tańsze niż kupowanie w Polsce - nota bene także chińskich. Kupowanie mikrokontrolerów i innych układów scalonych w Chinach, to znaczne ryzyko kupienia podróbek przez co nie trzymania parametrów. Diody LED to inna para kaloszy - tutaj sprowadzanie z Chin jest po prostu opłacalne, a różnice w parametrach mniej istotne.
Dodałem plik z projektem w Atmel Studio.
OdpowiedzUsuńWartościowy artykuł.
OdpowiedzUsuńSame konkrety podane w formie "kawa na ławę".
Ale TMF już nas po trosze przyzwyczaił do wysokiego poziomu merytorycznego ...
Moim zdaniem " ELITA" więc szacun :)
Dodam jeszcze że w tym wypadku naprawdę warto pamiętać o kondensatorach 100nF - sam właśnie spędziłem dobre 20 minut na szukaniu problemów z przykładem. Po dołączeniu "lizaka" problem rozwiązany.
OdpowiedzUsuńKiedy można liczyć na kolejną część?
OdpowiedzUsuńDruga część już jest, a III czeka na publikację.
UsuńSterownik ten ma jeszcze jedną ciekawą właściwość, długość stanu niskiego można wydłużać całkiem sporo, bo aż do ok 9us ( czas jest praktycznym minimalnym czasem resetu dla sterownika oryginalnego) i nawet do ok 30 us (dla sterowników - klonów). Dzięki temu blokowanie przerwań należałoby robić głównie przy wysterowaniu sygnału wysokiego, bo dla stanu niskiego to już więcej czasu na obsługę przerwań, aczkolwiek i tak nie aż tak dużo.
OdpowiedzUsuń