
Właściwie mamy już Święta i świąteczny konkurs w DIY. A w nim sporo konstrukcji opartych na programowalnych diodach WS2812B. Stąd też, aby ułatwić innym realizację świątecznych projektów oświetleniowych, postanowiłem podzielić się rozwiązaniem pozwalającym na w pełni sprzętowe sterowanie tego typu diodami.
Quote:Jednocześnie, jako jeden z moderatorów działu DIY oświadczam, że nie biorę udziału w konkursie m.in. ze względu na potencjalny konflikt interesów. Także prezentowane rozwiązanie zamieszczam wyłącznie, aby, jak mam nadzieję, ułatwić życie innym.
Diody WS2812B są fajne, ale mają pewną wadę – protokół transmisji danych nie pasuje do żadnego standardowego interfejsu spotykanego w mikrokontrolerach. W efekcie możemy spotkać się z różnymi rozwiązaniami transmisji danych, od prostych, polegających na manglowaniu pinem IO, np.: Link
do nieco bardziej skomplikowanych, wykorzystujących częściowo dostępny hardware, np. interfejs SPI lub UART, np.: Link
W tym ostatnim przykładzie, do komunikacji zaprzęgnięty został UART i dostępne w XMEGA DMA, co znacznie odciąża mikrokontroler, a przede wszystkim ułatwia tworzenie efektów – nie musimy przeplatać obliczeń z wysyłaniem danych. Wadą tego przykładu jest konieczność transkodowania danych – informacje RGB muszą zostać przekodowane tak, aby przy wysyłce przez UART do WS2812B utworzyć poprawny ciąg danych. Proces transkodowania nie jest skomplikowany ani jakoś szczególnie obciążający dla mikrokontrolera, ale czy da się pójść o krok dalej i zrealizować całkowicie sprzętowo wysyłanie danych?
Do kontynuowania tematu zainspirował mnie artykuł kolegi @piotr_go pod tytułem „W pełni sprzętowe sterowanie LEDów WS2812B na STM32F030 by piotr_go” (Link).
Autor użył mikrokontrolera STM32F030 i do realizacji transmisji danych wykorzystał DMA i timer. Pomyślałem więc, czy da się zrealizować w pełni sprzętową transmisję na innych mikrokontrolerach, w tym 8-bitowcach? Postanowiłem to sprawdzić, jednocześnie próbując nie powielać rozwiązania kolegi @piotr_go. Ponieważ ze względu na konieczność zapewnienia ciągłego strumienia danych dla WS2812B, a jednocześnie możliwości łatwego wykonywania obliczeń, przyda nam się DMA, postanowiłem wziąć oczywiście 8-bitową XMEGę. Pierwsze, w pełni sprzętowe rozwiązanie dla XMEGA zostało opublikowane jakiś czas temu:
Link
Problem w tym, że wykorzystuje ono układ logiki programowalnej, dostępny wyłącznie w XMEGA serii E5. Ponieważ są to małe procesorki, posiadające niewiele RAMu, więc automatycznie średnio nadają się do sterowania setkami, a może nawet tysiącami diod WS2812B jednocześnie. Niestety inne rodziny XMEGA nie posiadają modułu XCL, więc zaprezentowane rozwiązanie nie daje się na nie przenieść.
Ale nic straconego. Wyzwanie przyjęte – i uprzedzając fakty – rozwiązane.
Zacznijmy od założeń:
-nie chcę powielać rozwiązania kolegi piotr_go, raz, że chcę wymyślić coś oryginalnego, dwa, że w XMEGA mamy tylko 4 kanały DMA, więc wolę je zostawić do transmisji danych do WS2812B, a nie do samego generowania impulsów sterujących,
-w XMEGA mamy sporo timerów, więc tych mi nie szkoda,
-rozwiązanie powinno zapewnić całkowicie sprzętową transmisję danych, bez konieczności ich transkodowania.
Nie powiem, rozwiązanie problemu zajęło mi trochę czasu – mniej więcej 4 godziny, ale się udało. Zacznijmy więc od początku. Ponieważ dane do WS2812B transmitowane są szeregowo, więc oczywistym jest, że rozwiązanie sprzętowe musi wykorzystać jakiś serializer – z dostępnych w XMEGA mamy interfejsy SPI lub UART. SPI wykluczyłem a priori, gdyż nie współpracuje w trybie master z DMA i ma pojedyncze buforowanie nadajnika. Stąd też nawet jeśli SPI by się nadawało do realizacji sprzętowej transmisji, to byłoby to niezbyt wygodne. Na placu boju pozostał więc interfejs UART. W XMEGA jego wykorzystanie ma same zalety:
-ma dwupoziomowy bufor nadajnika, dzięki czemu nawet bez DMA możemy stosunkowo łatwo zapewnić wysyłanie ciągłego strumienia danych,
-da się go połączyć z DMA w celu automatyzacji przesyłu danych, co z kolei znacznie ułatwia nam pisanie aplikacji generującej efekty świetlne,
-może pracować w trybie master SPI, co z kolei potrzebne jest do serializacji danych.
Ok, interfejs wybrany, teraz musimy wyrzucane przez niego zera i jedynki zamienić na format zrozumiały dla WS2812B. Dla przypomnienia, 0 i 1 nadawane są w następujący sposób:

Czas trwania bitu możemy podzielić na trzy części po około 400 ns. Nadanie zera oznacza utrzymanie stanu wysokiego przez 1/3 czasu trwania bitu i stanu niskiego przez 2/3 czasu trwania bitu, z kolei nadanie jedynki wymaga działania dokładnie odwrotnego – przez 2/3 czasu utrzymujemy stan wysoki, a przez 1/3 stan niski. Co się nadaje, do generowania impulsów o zadanym czasie? Oczywiście timer. A skoro nadanie zera lub jedynki wiąże się ze zmianą wypełnienia, to oczywiście użyjemy timera w trybie PWM. I tu jest pewien problem. Wypełnienie PWM musi się zmieniać w zależności od tego, czy nadajemy jeden, czy zero. Dodatkowo, w XMEGA timer nie ma trybu one shot, czyli raz skonfigurowany będzie nadawał zera lub jedynki w nieskończoność. Te dwa problemy na chwilę mnie zatrzymały. Jak pisałem, założenie było takie, że nie używam DMA do modulacji szerokości impulsu. Z pomocą przyszedł znany użytkownikom XMEGA i nowszych ATTiny tzw. event system. Umożliwia on elastyczne połączenie nadajnika zdarzeń z odbiornikiem. W przypadku timera, event system może wywołać jedną z wybranych akcji – np. wyzerować timer (TC_EVACT_RESTART_gc). Dzięki temu można łatwo połączyć wybrane zdarzenie na pinie IO z np. zerowaniem timera. Skoro jednak muszę generować dwa impulsy o różnym wypełnieniu, to muszę użyć dwóch timerów, jeden będzie odpowiedzialny za generowanie bitu o wartości 0, a drugi bitu o wartości 1. W moim przypadku będą to odpowiednio timery TCC1 i TCC0. Ktoś może się oburzyć moją rozrzutnością, ale w użytym mikrokontrolerze mam do dyspozycji osiem 16-bitowych timerów, więc dwa timery na jedną gałąź sterowania WSami to nie problem. Jest tylko jeszcze jeden mały kłopot – wyjścia PWM timerów są na różnych pinach, więc impulsy przez nie generowane trzebaby zsumować. Teoretycznie, ponieważ wyjścia w tym procesorze mogą pracować w trybie wired-OR, nie powinno być problemu. Ale jednak mały problem jest – WS2812B wymaga transmisji z szybkością ok. 800 kbps, co raczej utrudnia wykorzystanie trybu wired-OR (zbyt wolny czas opadania zboczy, wymuszany pasywnie przez rezystory np. wbudowane w port MCU). A zastosowanie zewnętrznej bramki byłoby porażką – miał być użyty wyłącznie mikrokontroler. I tu pojawia się przyjemna cecha odróżniająca ARMy od XMEGA i generalnie AVR. W przeciwieństwie do ARM, dla danego pinu możemy włączyć więcej niż jedną funkcję alternatywną. Jeśli włączymy na danym pinie dwa wyjścia PWM, to uzyskany przebieg będzie sumą logiczną dwóch przebiegów generowanych przez każdy z timerów. Czyli jest pięknie, dokładnie o to nam chodziło.
Pozostaje jeszcze jedna rzecz – jak w zależności od tego czy UART nadaje jeden czy zero, wybierać jeden z timerów? Załóżmy, że timer TCC1 stale generuje przebieg odpowiadający nadawaniu zer. Aby nadać jedynkę, wystarczy więc w nadawanym przebiegu wydłużyć czas trwania stanu wysokiego – a więc nałożyć przebieg generowany przez TCC0. I tu sprawa jest prosta – przez event system, z wyjścia TxD UARTa, w przypadku, gdy stan tego wyjścia jest niski (nadajemy zero), wysyłamy sygnał zerujący timer TCC0, odpowiadający za generowanie przebiegu odpowiadającego nadawaniu bitu o wartości 1. Z kolei jeśli TxD jest w stanie wysokim, to timer TCC0 nie jest blokowany i generuje przebieg odpowiadający jedynce, nakładany na przebieg generowany przez TCC1. Bingo! W ten sposób mamy załatwione całkowicie sprzętowe nadawanie danych do WS2812B.
Na koniec jeszcze jedno – generowanie RESET. Jak wiemy, wysyłka danych do WS2812B musi być poprzedzona wygenerowaniem sygnału RESET o czasie trwania co najmniej 50 µs:

Czas minimalny jest podany w nocie, a czas maksymalny? Ten może być dowolnie długi – utrzymywanie magistrali w stanie resetu nie ma wpływu na samo funkcjonowanie naszej diody. Oczywiście, możemy reset wygenerować programowo, ale przecież miało być sprzętowo. Pogłówkujmy więc jeszcze trochę. Spokojnie możemy założyć, że jeśli nie transmitujemy żadnych danych, to magistralę możemy trzymać w stanie resetu. Ma to kilka zalet:
-przypadkowe śmieci nie zmienią nam stanu diod,
-diody są natychmiast gotowe do transmisji danych.
Tylko mały problem – jak odróżnić sytuację, kiedy transmitujemy dane, od bezczynności? Przypominam – chodzi o rozwiązanie całkowicie sprzętowe, a nie jakieś programowe wygibasy. Sprawa jest prosta – jeśli nadajemy do WS2812B dane, to linia SCK (XCK UART) interfejsu UART jest aktywna – będzie na niej przebieg prostokątny, o okresie odpowiadającym okresowi trwania transmitowanego bitu. Jeśli nic nie transmitujemy, to linia będzie w stanie niskim (oczywiście zależy to od wykorzystywanego trybu pracy SPI). To teraz już mamy z górki. Brak impulsów na SCK powinien wymusić niski stan magistrali sterującej WS2812B. Ponieważ przy nieaktywnym SCK (brak nadawania danych), niski stan panuje także na linii TxD, więc automatycznie timer TCC0 (odpowiedzialny za generowanie przebiegu odbieranego jako jeden) utrzymywany jest w stanie zerowania – na jego wyjściu PWM panuje stan niski. Problemem w tej sytuacji jest tylko TCC1, który nam stale generuje bity o wartości zero. No to co trzeba zrobić? Ano też go wyzerujmy – czyli kolejny kanał event system i zdarzenie zerowania timera. W efekcie niski stan na SCK nam wyzeruje timer TCC1, czyli na wyjściu PWM też będzie zero, a więc cała linia sterująca WS2812B będzie w stanie niskim. W momencie nadawania danych, SCK będzie okresowo w stanie wysokim (wypełnienie tego przebiegu wynosi 50%), co odblokuje TCC1, który będzie mógł generować przebieg odpowiadający nadawaniu zera.
A więc mamy całkowicie zrealizowany sprzętowo interfejs dla WS812B, niczym nie musimy sterować programowo, po prostu w chwili, gdy wyślemy informację o kolorze RGB do UART, wszystko magicznie przekoduje się samo.
O ile wiem, pokazana realizacja jest pierwszą całkowicie sprzętową implementacją protokołu WS2812B dla XMEGA innej niż E5.
Opisana zasada zadziała także dla wielu innych mikrokontrolerów, praktycznie wprost będzie można ją przenieść m.in. na mikrokontrolery ARM SAM Dxx i inne Atmela/Microchipa.
Koniec gadania, czas na praktykę. W przykładzie użyłem XMEGA256A3BU – bo była pod ręką – a to dzięki uprzejmości forumowego kolegi Leon Instruments. Kod bez zmian zadziała praktycznie na każdej XMEGA.
Procesor będzie taktowany z wewnętrznego generatora 32 MHz – przyda nam się wysoka częstotliwość taktowania przy generowaniu skomplikowanych efektów graficznych. Zacznijmy od konfiguracji SPI:
Code: c
Tu nie mamy nic nietypowego – wykorzystujemy USARTC0, na pin PORTC1 wyprowadzony będzie sygnał SCK, a na PORTC3 sygnał TxD. Z tych pinów zroutujemy sobie sygnały przez event system. Niestety ponieważ sygnał XCK (SCK) zanim jest transmitowany przez event system, nie przechodzi przez inwerter portu, musimy połączyć fizycznie piny PORTC1 z PORTC5 – jest to jedyna, drobna niedogodność prezentowanego rozwiązania.
Teraz konfiguracja timera (funkcja TimerInit) – zacznijmy od przemapowania wyjścia PWM timera TCC0 z domyślnego (PORTC0) na wyjście współdzielone z TCC1 (PORTC4):
Code: c
Teraz możemy skonfigurować timer I event system odpowiedzialny za generowanie przebiegu odpowiadającego jedynce:
Code: c
W kolejnym etapie konfigurujemy timer TCC1 odpowiadający za generowanie przebiegu rozpoznawanego jako zero:
Code: c
Na koniec jeszcze udostępniamy zsumowany przebieg na pinie PORTC4:
Code: c
I to wszystko. Jeśli wejście DIN diody WS2812B podłączymy do pinu PORTC4, będziemy mogli nią w prosty sposób sterować. Przyda nam się jeszcze prosta funkcja wysyłająca do niej dane w formacie RGB:
Code: c
Aby sprawdzić, czy wszystko działa, możemy wysłać ciąg testowy:
Code: c
Powyższy kod nie robi nic imponującego – ustawia kolory pierwszych sześciu diod. Kompletny przykład znajduje się w załączniku. Oczywiście możemy ten przykład łatwo rozbudować o przesyłanie danych z wykorzystaniem DMA (wystarczy zaprogramować kanał na przesyłanie danych do USARTC0), możemy także wykorzystać pozostałe dostępne UARTy i timery, i w ten sposób stworzyć nawet cztery niezależne kanały-interfejsy do łańcucha diod WS2812B. Dzięki temu, bez problemu możemy wysterować nawet kilkanaście tysięcy diod WS2812B naraz!
Warto jeszcze rzucić okiem na generowane przebiegi. Zacznijmy od ogólnego oscylogramu (na górze sygnał SCK, w środku nadawane dane - sekwencja 255, 0, 255, na dole, wygenerowany sprzętowo sygnał dla WS2812B:

Tak to wygląda w powiększeniu (widać wygenerowane bity o wartości 1 i 0):

I na koniec, powiększenie i pomiar czasu trwania, wskazujący, że mieścimy się w specyfikacji protokołu:

Warto zauważyć, że możemy bardzo precyzyjnie ustalać zarówno okres, jak i czasy trwania stanu wysokiego i niskiego, co zapewnia nam możliwość ścisłego dostosowania się do wymagań użytego protokołu.
Zapraszam do komentowania.
Cool! Ranking DIY