Kurs STM32L4 – #15 – diody RGB WS2812B (liczniki), quiz

Kurs STM32L4 – #15 – diody RGB WS2812B (liczniki), quiz

Sterowane cyfrowo diody RGB to elementy, które spotyka się w coraz większej liczbie urządzeń. Są one wygodne dla konstruktorów, bo wymagają tylko jednego pinu mikrokontrolera.

Komunikacja z diodami WS2812B wymaga jednak precyzji. Idealnie sprawdzą się tutaj liczniki, dzięki którym wygenerujemy odpowiednie sygnały.

Czego dowiesz się z tej części kursu STM32L4?

Podczas wykonywania eksperymentów z tej części kursu dowiesz się, jak sterować diodami WS2812B. Nie będziemy jednak korzystać z gotowych rozwiązań – napiszemy własną, niskopoziomową bibliotekę, która będzie sterowała diodami za pomocą liczników sprzętowych. Oprócz tego omówimy w skrócie problematykę konwersji poziomów logicznych, a na koniec rozwikłamy zagadkę błędnego generowania sygnału PWM.

WS2812 – cyfrowe diody RGB

W trakcie omawiania PWM na STM32L4 pokazywaliśmy, jak sterować zwykłą diodą RGB. Element ten wymagał aż trzech wyprowadzeń mikrokontrolera STM32L4 i zaangażowania licznika sprzętowego (do generowania PWM). Nasz układ posiada mnóstwo timerów, ale ich liczba jest jednak ograniczona. Jeśli chcielibyśmy sterować 1–3 diodami RGB, to pewnie nie byłoby żadnego problemu. Gorzej, jeśli nasz projekt wymaga np. podłączenia 20 diod RGB – wtedy tradycyjne sterowanie jest już problematyczne.

Niektóre projekty wymagają sterowania setkami diod RGB – tradycyjne podejście byłoby tutaj niepraktyczne

Niektóre projekty wymagają sterowania setkami diod RGB – tradycyjne podejście byłoby tutaj niepraktyczne

Na szczęście z pomocą przychodzą nam diody RGB z wbudowanym sterownikiem (nazywane inaczej „cyfrowymi diodami RGB” lub „programowalnymi diodami RGB”). W zdecydowanej większości przypadków mowa o popularnych diodach WS2812B, które sprzedawane są w przeróżnej formie – od pojedynczych diod, przez różne moduły, aż po długie paski, zawierające dziesiątki diod na metrze.

Podczas dalszych ćwiczeń wykorzystamy moduł z 7 diodami RGB, które zostały wlutowane na gotowy, okrągły moduł. Od razu widać tutaj zaletę diod WS2812B – 7 diod RGB na jednej płytce i tylko trzy wyprowadzenia (zasilanie i jeden sygnał sterujący).

Pierścień z 7 diodami WS2812B

Pierścień z 7 diodami WS2812B

W dokumentacji samego modułu raczej nie znajdziemy niczego ciekawego. Zdecydowanie ważniejsza jest dla nas dokumentacja diod WS2812B. Zanim przejdziemy do analizy parametrów oraz tego, jak należy komunikować się z tymi diodami, warto zwrócić uwagę na opis wyprowadzeń.

Opis wyprowadzeń pojedynczej diody (fragment dokumentacji)

Opis wyprowadzeń pojedynczej diody (fragment dokumentacji)

Każda z diod ma fizycznie cztery wyprowadzenia. Jednak po odliczeniu zasilania VDD i VSS zostają raptem dwa piny: DIN oraz DOUT. Pierwszy służy do komunikacji z konkretną diodą, a drugi pozwala na podłączenie kolejnej. Dzięki temu można łączyć diody w długie łańcuchy i sterować nimi za pomocą jednej linii danych. W związku z tym, że mamy jedną linię danych, a do zasilania potrzebne są dwa piny, dość łatwo domyślić się, jakie znaczenie mają piny dostępne w naszym module:

  • IN – linia danych
  • VCC – zasilanie
  • GND – masa

Zasilanie diod WS2812B

W dokumentacji znajdziemy informację, że zasilanie diod musi mieścić się w przedziale od 4,5 do 5,5 V. W naszym przypadku będzie więc to 5 V wprost z płytki Nucleo. Niestety dalej nie będzie już tak łatwo, bo napotkamy pewne problemy „napięciowe” na linii danych.

Informacja o poziomach logicznych na linii danych (fragment dokumentacji)

Informacja o poziomach logicznych na linii danych (fragment dokumentacji)

Powyższy zapis mówi nam o tym, jakie napięcie będzie traktowane przez diody (a właściwie przez ich sterownik) jako stan niski, a jakie jako stan wysoki. W tym przypadku za logiczne zero uznawane będzie napięcie w granicach od 0 V do 0,3 * VDD. W związku z tym, że moduł będzie zasilany z 5 V, górną granicą będzie 0,3 * 5 = 1,5 V.

Problemem jest jednak parametr informujący o tym, co będzie interpretowane jako wysoki stan logiczny. Tutaj jest to 0,7 * VDD, czyli 0,7 * 5 = 3,5 V – za logiczną jedynkę uznawane będzie zatem napięcie na linii danych od 3,5 V do 5 V. Nasz mikrokontroler, czyli STM32L476, zasilany jest z napięcia 3,3 V i właśnie takie napięcie pojawia się na jego wyprowadzeniach w stanie wysokim.

Metody konwersji poziomów logicznych

Mamy więc problem: nasza logiczna jedynka to 3,3 V, a sterownik w diodach będzie oczekiwał minimum 3,5 V. Problem ten jest powszechny (i dotyczy nie tylko tych diod), można go rozwiązać na wiele sposobów, opisywanych najczęściej jako metody konwersji poziomów logicznych. Oto przykłady kilku najpopularniejszych rozwiązań, które moglibyśmy tutaj wykorzystać.

Podniesienie napięcia zasilania

Na początku kursu wspominaliśmy, że nasz mikrokontroler może być zasilany napięciem od około 1,8 V do 3,6 V. Moglibyśmy więc podnieść napięcie zasilania i uzyskać wymagane 3,5 V na wyjściu. Takie rozwiązanie jest jednak trudne z co najmniej  kilku powodów – napięcie 3,5 V jest dość nietypowe, więc zwyczajnie ciężko byłoby uzyskać stabilne 3,5 V. Wymagałoby to przeróbki płytki Nucleo.

Obniżenie napięcia zasilania

Można też obniżyć napięcie zasilania WS2812B (przynajmniej dla pierwszej diody w szeregu). Skoro mogą one pracować w zakresie od 4,5 V do 5,5 V, to zasilanie ich z 4,5 V obniżyłoby próg dla stanu wysokiego do  0,7 * 4,5 = 3,15 V. Tego typu rozwiązanie jest nieco prostsze w realizacji niż poprzednie – można spotkać układy, gdzie zwykła dioda prostownicza jest używana do obniżenia napięcia z 5 V do takiego w okolicy 4,5 V.

Metoda na „u mnie działa”

Najczęstsza, prowizoryczna metoda, którą można spotkać w wielu poradnikach internetowych. Nikt się nie przejmuje błędnymi poziomami logicznymi, bo różnica między napięciem wyjściowym naszego mikrokontrolera a minimalnym oczekiwanym przez diody jest tak mała, że diody WS2812 działają po prostu sterowane z 3,3 V.

Prowizoryczne rozwiązania przy masowej produkcji mogą wymagać kosztownych akcji serwisowych

Prowizoryczne rozwiązania przy masowej produkcji mogą wymagać kosztownych akcji serwisowych

Problem w tym, że to nie najlepsze rozwiązanie, bo wiele może zależeć od konkretnych egzemplarzy diody. Dlatego u jednych osób to zadziała, u innych nie. Co więcej, takie rozwiązanie może psuć się w losowych momentach (np. zależnie od temperatury).

Otwarty dren i rezystor podciągający

Podczas omawiania I2C na STM32L4 wspominaliśmy, że wyjścia mikrokontrolera mogą działać w trybie, w którym logiczne zero zwiera wyjście do masy, a logiczna jedynka zostawia pin niepodłączony. Jeśli dodamy do układu rezystor podciągający do 5 V, to będziemy mieli wówczas właśnie takie napięcie na wyjściu. STM32L476 nie może sam wymusić na wyjściu 5 V, ale większość pinów toleruje takie napięcie, więc to rozwiązanie jest jak najbardziej poprawne.

Użycie konwertera napięć

Problem kompatybilności układów zasilanych różnymi napięciami nie jest niczym nowym, więc znajdziemy wiele gotowych rozwiązań pozwalających na łączenie takich układów ze sobą. Przykładowy układ scalony pozwalający na łączenie modułów pracujących z napięciami 3,3 V oraz 5 V to TXB0104. Zapewnia on odpowiednie czasy narastania, wymagane napięcia itd.

Użycie bufora cyfrowego

Przygotowując zestawy elementów do kursu STM32L4, zdecydowaliśmy się na użycie jeszcze innego rozwiązania. Komunikacja z diodami przebiega jednokierunkowo (wysyłamy dane z mikrokontrolera do sterowników diod), nie potrzebujemy więc dwukierunkowego konwertera napięć.

W takim celu zamiast drogiego konwertera można wykorzystać jeden ze stosunkowo tanich układów serii 74HCT. Są to układy logiczne, które mogą być zasilane z 5 V, ale – co ciekawe – napięcie na ich wejściach zaczynające się od 2 V traktują już jako stan wysoki. W naszym przypadku zdecydowaliśmy się na wykorzystanie układu 74HCT125, który jest wyposażony w cztery bufory cyfrowe.

Wyprowadzenia 74HCT125 (fragment dokumentacji)

Wyprowadzenia 74HCT125 (fragment dokumentacji)

Bufor działa w taki sposób, że po ustawieniu na zanegowanych liniach OE stanu niskiego przekazuje on stan wejścia na wyjście. W naszym układzie połączymy wejście OE z masą na stałe i wówczas na wyjściu będziemy mieli taki sam stan jak na wejściu. Logicznie bufor nie robi właściwie nic ciekawego, ale jeśli popatrzymy na jego parametry elektryczne, znajdziemy tam to, co nas interesuje.

Interpretacja napięć przez układ 74HCT125

Interpretacja napięć przez układ 74HCT125

Jak widać, parametr VIH nie zależy od napięcia zasilania i wynosi 2 V, czyli wartość, jaką bez problemu nasz STM32L476 jest w stanie zapewnić. Natomiast na wyjściu układu 74HCT125 pojawią się sygnały zgodne z napięciem zasilania, czyli w tym przypadku będzie to 5 V. Co więcej, wyjścia tego bufora pracują w trybie push-pull, więc czasy narastania nie stanowią problemu.

Podłączenie diod WS2812B do STM32L4

Za nami krótki wstęp teoretyczny na temat „napięć”, więc możemy już spokojnie przejść do budowy układu. Od strony mikrokontrolera wykorzystamy tylko jeden pin – PA6. Zarówno bufor 74HCT125, jak i moduł z diodami WS2812B są zasilane z 5 V; warto pamiętać o podłączeniu kondensatorów 100 nF oraz 220 µF. Dzięki temu unikniemy problemów, które mogłyby się pojawić przy większym poborze prądu (generowanym np. podczas szybkiego migania wszystkimi diodami).

Schemat ideowy i montażowy dla przykładów z tej części kursu STM32L4

Schemat ideowy i montażowy dla przykładów z tej części kursu STM32L4

Komunikacja z diodami WS2812B

Protokół komunikacji z diodami WS2812B jest prosty, ale nie korzystają one z żadnego interfejsu, który już znamy. Kolor każdej diody zapisany jest jako 24-bitowa wartość (po 8 bitów na każdą składową koloru RGB). Kolejność przesyłania jest nietypowa, bo najpierw przesyłana jest składowa zielona następnie czerwona, a na końcu niebieska. Wszystkie w kolejności od najwyższego bitu do najniższego:

Sposób formatowania informacji na temat kolorów dla diod WS2812B

Sposób formatowania informacji na temat kolorów dla diod WS2812B

Jeśli więcej diod jest połączonych szeregowo (czyli prawie zawsze), to najpierw przesyłane są dane dla pierwszej diody, następnie drugiej, trzeciej itd. Na najniższym poziomie przesyłane są trzy rodzaje informacji: RESET, bit reprezentujący logiczne zero i bit reprezentujący logiczną jedynkę.

Informacja na temat sposobu transmisji bitów (fragmenty dokumentacji)

Informacja na temat sposobu transmisji bitów (fragmenty dokumentacji)

Jak widać, poza symbolem resetu, który trwa powyżej 50 µs, czasy przesyłania bitów są dość krótkie – jeden bit „trwa” 1,25 µs, co nawet dla mikrokontrolera pracującego z częstotliwością 80 MHz jest krótkim czasem (transmisja jednego bitu trwa dokładnie 100 cykli zegara naszego mikrokontrolera). 

Wśród modułów peryferyjnych STM32L476 nie znajdziemy układu obsługującego protokół WS2812B (i nie znajdziemy go raczej w żadnych innych mikrokontrolerach). Teoretycznie można byłoby próbować napisać program, który sterowałby GPIO zgodnie z wymaganymi czasami, ale byłoby to bardzo trudne i wymagało wyłączenia obsługi przerwań.

Można jednak wykorzystać dostępne moduły sprzętowe do komunikacji z diodami WS2812B – istnieją przykłady wykorzystania modułów SPI oraz UART (chociaż, jak wiemy, domyślnie są używane w zupełnie innych celach). Podczas tworzenia tego kursu zdecydowaliśmy się jednak na opisanie metody z użyciem liczników – schemat przesyłania bitów bardzo przypomina przebieg PWM, będzie to zatem okazja do poznania dodatkowych możliwości tych modułów.

Konfiguracja projektu

Możemy nareszcie przejść do programowania. Zaczynamy od projektu z STM32L476RG, który pracuje z częstotliwością 80 MHz (koniecznie), standardowo uruchamiamy też debugger. Zaznaczamy również w opcjach projektu, że CubeMX ma wygenerować osobne pliki dla wszystkich modułów

Od razu idziemy też dalej i przechodzimy do konfiguracji modułu TIM3:

  • Clock Source: Internal Clock
  • Channel 1: PWM Generation CH1
  • Counter Period: 99

Dla formalności: ustawienie licznika na 99 oznacza podział częstotliwości 80 MHz przez 100, co daje 800 kHz, czyli 1,25 µs, co odpowiada czasowi transmisji jednego bitu dla diod WS2812B.

Konfiguracja licznika TIM3 do obsługi diod WS2812B

Konfiguracja licznika TIM3 do obsługi diod WS2812B

Tryb PWM, który poznaliśmy wcześniej, ustawiał stałe wypełnienie sygnału, teraz chcielibyśmy jednak zmieniać wypełnienie dla każdego cyklu, odpowiednio do przesyłanej informacji. Posłuży nam do tego mechanizm DMA, który będzie w locie aktualizował ustawienia PWM.

W dolnej części okna konfiguracji TIM3 przechodzimy więc do zakładki DMA Settings i klikamy przycisk Add. Następnie wybieramy kanał TIM3_CH1/TRIG oraz ustawienia:

  • Direction: Memory To Peripheral
  • Data Width:
    • Peripheral: Half Word
    • Memory: Byte
Konfiguracja DMA dla licznika TIM3

Konfiguracja DMA dla licznika TIM3

Wybraliśmy kierunek przesyłania danych z pamięci do modułu peryferyjnego, ponieważ będziemy wysyłać dane. Ciekawsze jest ustawienie szerokości danych. Większość liczników jest 16-bitowych, zatem licznik naszego TIM3 oczekuje właśnie takiego formatu danych i dlatego po stronie Peripheral wybraliśmy wielkość danych Half Word. Okres timera jest równy 100, więc przechowywanie w pamięci aż 16-bitowych wartości nie jest konieczne, wystarczy nam 8 bitów – ustawiamy po stronie Memory rozmiar danych na Byte, a mechanizm DMA uzupełni wyższe bity zerami.

Pierwszy program obsługujący diody WS2812B

Możemy przejść już do pisania właściwego kodu. Sterowanie diodami WS2812 jest zaskakująco łatwe, ale najpierw musimy nieco lepiej poznać działanie trybu PWM w połączeniu z DMA. Przyda nam się do tego analizator stanów logicznych – oczywiście zamieściliśmy tutaj zrzuty z takiego narzędzia.

Zacznijmy od bardzo prostego kodu:

Najpierw uruchamiamy moduł timera, wywołując funkcję HAL_TIM_Base_Start. Następnie tworzymy tablicę z przykładowymi danymi – jak pamiętamy, okres sygnału PWM wynosi 100, więc podane liczby można interpretować jako procent wypełnienia. Na koniec wywołujemy funkcję HAL_TIM_Start_DMA. Jej nagłówek wygląda następująco:

Pierwszy parametr to wskaźnik do naszego licznika, czyli zmiennej htim3, drugim jest numer kanału – używamy kanału 1, więc przekazujemy stałą TIM_CHANNEL_1. Następnie przekazujemy wskaźnik do bufora z danymi oraz liczbę danych do wysłania. Ustawiliśmy wcześniej rozmiar danych w pamięci na Byte, dlatego nasza tablica ma elementy typu uint8_t.

Efekt działania powyższego programu (podgląd z analizatora stanów logicznych)

Efekt działania powyższego programu (podgląd z analizatora stanów logicznych)

Jak widać po lewej stronie, domyślnie pin był wysterowany stanem niskim. Wartość 10 spowodowała wygenerowanie impulsu o wypełnienie 10%, kolejna wartość to 20, czyli 20% wypełnienia itd. Tym, co może być zaskoczeniem, jest natomiast powtarzanie ostatniej wartości

Wolelibyśmy, żeby po przesłaniu danych stan linii był ustawiony jako wysoki, dlatego możemy na końcu dodać wartość 100, czyli pełne wypełnienie. Dodajmy również 100 na początku, aby lepiej było widać, gdzie zaczyna się nasz sygnał.

Zmieniamy więc program na następujący:

Teraz sygnał wygląda zgodnie z oczekiwaniami:

Efekt działania drugiej wersji programu (podgląd z analizatora stanów logicznych)

Efekt działania drugiej wersji programu (podgląd z analizatora stanów logicznych)

Okres naszego sygnału to 1,25 µs. Aby przesłać bit oznaczający 0, musimy wygenerować impuls o szerokości około 400 ns. Jeden cykl zegara ma 12,5 ns, więc 32 cykle powinny dać oczekiwany efekt:

W efekcie uzyskamy przebieg zbliżony do poniższego – wszystko działa więc poprawnie:

Efekt działania trzeciej wersji programu (podgląd z analizatora stanów logicznych)

Efekt działania trzeciej wersji programu (podgląd z analizatora stanów logicznych)

Podobnie możemy policzyć i przetestować, że aby wysłać bit oznaczający 1, potrzebujemy impulsu trwającego 800 ns, co odpowiada wartości 64.

Na koniec zostaje nam wygenerowanie sygnału RESET. Z dokumentacji dowiemy się, że jest to po prostu stan niski trwający ponad 50 µs. Jeśli ustawimy wypełnienie o wartości 0, to na wyjściu pojawi się stan niski przez 1,25 µs. Wystarczy więc, że wyślemy 40 razy wartość 0, i osiągniemy oczekiwany rezultat (w rzeczywistości lepiej użyć nieco dłuższego czasu).

Wiemy już wystarczająco dużo o działaniu PWM, możemy zostawić analizator stanów logicznych i wrócić do sterowania diodami. Stwórzmy pierwszy, jeszcze bardzo brzydki, ale prosty w zrozumieniu program. Na początek deklarujemy tablicę wypełnioną zerami:

Wielkość tego buforu wynika z tego, że potrzebujemy miejsca na następujące elementy:

  • 40 cykli PWM zerowych, aby wysłać sygnał RESET,
  • następnie dane dla 7 diod WS2812B (każda oczekuje 24 bitów koloru),
  • 1 cykl z wypełnieniem 100%, który będzie powtarzany po zakończeniu transmisji.

Tablica jest wypełniona zerami, ale diody powinny otrzymać impuls o szerokości 400 ns, aby odczytać te dane jako 0. Wpisujemy zatem w odpowiednie miejsce tablicy wartość 32, bo takie wypełnienie da właśnie impulsy o szerokości 400 ns.

Pozostało nam jeszcze dodać na końcu wypełnienie równe 100% i wysłać dane za pomocą poniższej linijki – kompilator zgłosi później do niej ostrzeżenie na temat typu danych (podczas testów może je jednak zignorować, można też dodać tutaj jawne rzutowanie na uint32_t):

Jeśli uruchomimy taki program, to nic się nie wydarzy – ewentualnie diody zgasną (jeśli z jakiegoś powodu wcześniej się świeciły). Dodajmy więc drobną zmianę, mianowicie bit reprezentujący 1 dla pierwszej diody (czyli musimy ustawić PWM o wypełnieniu 64):

Cały kod powinien wyglądać teraz następująco:

Po uruchomieniu powinniśmy zobaczyć, że jedna dioda została włączona na kolor zielony. Wiemy już, jak można komunikować się z diodami WS2812B. Czas napisać o wiele czytelniejszą wersję programu.

Efekt działania powyższego programu – pierwsza dioda w szeregu świeci na zielono

Efekt działania powyższego programu – pierwsza dioda w szeregu świeci na zielono

Optymalizacja kodu do obsługi WS2812B

Tym razem przeniesiemy całość od razu do osobnej biblioteki. Zaczynamy od pliku nagłówkowego ws2812b.h, w którym umieszczamy informacje na temat czterech funkcji:

Od razu tworzymy też plik ws2812b.c, do którego dodajemy odpowiednie pliki nagłówkowe oraz stałe:

Definicje BIT_0_TIME i BIT_1_TIME określają liczbę cykli zegara dla poszczególnych stanów, aby były one poprawnie rozpoznawane przez sterowniki wbudowane do naszych diod. Z kolei RESET_LEN to liczba cykli potrzebnych, aby diody wykryły tzw. stan RESET. Na koniec LED_N definiuje liczbę diod, których będziemy używać.

Teraz, wewnątrz pliku ws2812b.c, możemy zdefiniować bufor na dane oraz funkcję inicjalizującą:

Kod jest właściwie identyczny z tym, czego używaliśmy wcześniej, jednak użycie stałych zamiast „magicznych liczb” trochę poprawia czytelność. Na końcu dodaliśmy również wywołanie nowej funkcji, czyli ws2812b_update, od razu dodajemy więc odpowiednią definicję – zadaniem tej funkcji jest „wysłanie nowych danych” przez PWM za pomocą DMA.

Przesyłanie danych za pomocą DMA odbywa się w tle, ale gdybyśmy jednak z jakiegoś powodu chcieli poczekać na zakończenie transmisji, możemy od razu dodać do biblioteki funkcję ws2812b_wait:

Teraz napiszmy funkcję pomocniczą, która zakoduje 8-bitową liczbę w postaci 8 bajtów, tak aby po wysłaniu ich do timera otrzymać sterowanie zgodne z oczekiwaniami diod WS2812B. Jest to funkcja statyczna, więc powinna znaleźć się w pliku ws2812b.c, nad wcześniej dodanymi funkcjami.

Bity wysyłane są w kolejności od najbardziej znaczącego – sprawdzamy wartość bitu przez porównanie z wartością 0x80. Jeśli najwyższy bit jest ustawiony na 1, to wstawiamy do tablicy wartość BIT_1_TIME, a w przeciwnym przypadku BIT_0_TIME.

Teraz to, co najważniejsze, czyli funkcja ws2818b_set_color, dzięki której ustawimy kolor diod:

Jak pamiętamy, na początku mamy RESET_LEN bajtów zajętych przez wysyłanie stanu RESET – każda dioda zajmuje 24 bajty, a kolory są ustawiane w kolejności GRB. Oprócz tego za pomocą prostego warunku sprawdzamy tylko dla bezpieczeństwa, czy nie próbujemy ustawić koloru dla „diody”, która wykracza poza miejsce zarezerwowane w buforze.

Teraz biblioteka jest gotowa, możemy wrócić do programu głównego i przetestować, czy całość działa tak, jak chcieliśmy. Zaczynamy oczywiście od dodania plików nagłówkowych:

Teraz możemy napisać program główny:

Losujemy wartości dla trzech zmiennych 8-bitowych, które wykorzystujemy później jako losowe kolory. W pętli uruchamiamy kolejne diody na wybrany kolor i wywołujemy funkcję ws2812b_update, aby wyświetlić nowy wzór itd. Po uruchomieniu kodu powinniśmy zobaczyć efekt zbliżony do poniższego.

Przykładowy efekt wygenerowany przez najnowsza wersję programu

Przykładowy efekt wygenerowany przez najnowsza wersję programu

Korekcja gamma

Efekt poprzedniego programu, chociaż ładny, ma jednak pewną wadę. Diody świecą bardzo jasno i większość losowych kolorów jest bardzo zbliżona do białego. Okazuje się, że diody WS2812B mają tę samą wadę co zwykłe diody RGB – ich jasność nie zależy liniowo od poziomu wypełnienia.

Moglibyśmy użyć matematycznego wzoru z części 8 kursu, ale lepiej sprawdzi się rozwiązanie, które na swoich stronach opisuje firma Adafruit. Umieszczony tam kod dotyczy co prawda Arduino, ale możemy skopiować sobie tablicę z tzw. korekcją gamma i wstawić ją dla testu do pliku main:

Teraz możemy zmodyfikować nasze demo, tak aby kolory były poddawane odpowiedniej korekcie. Wystarczy, że zamiast wylosowanej wartości jako kolor ustawimy odpowiednią wartość z tablicy. Trzeba więc (oprócz dodania tablicy ze współczynnikami) podmienić tylko trzy linijki na poniższe (dzięki dzieleniu modulo 256 otrzymujemy liczbę z zakresu od 0 do 255):

Teraz kolory powinny być o wiele ładniejsze niż w pierwszej wersji programu. Nie jest to kluczowe dla tematu tej części kursu, ale po prostu warto pamiętać o korekcie gamma podczas wykorzystywania tego typu diod, bo wyniki będą wtedy znacznie bardziej atrakcyjne.

Dla zainteresowanych używaniem PWM oraz DMA

Nasze sterowanie diodami WS2812B działa bardzo ładnie i właściwie moglibyśmy na tym poprzestać. Jednak spostrzegawczy czytelnicy mogą zauważyć nieco zbyt szerokie impulsy na diagramach z wypełnieniem 100%. Nie musieliśmy się tym zbytnio przejmować, bo nasze sterowanie nie przekraczało 64%, ale warto byłoby zobaczyć, skąd tak dziwne zachowanie modułu oraz jak poprawić jego działanie (dla spokoju własnego sumienia).

Nie chcemy zamiatać niczego pod dywan i udawać, że tak miało być. Zaraz to poprawimy, ale w tym konkretnym przypadku nie przełoży się to na żaden wizualny efekt – dlatego podeprzemy się tutaj zrzutami z analizatora stanów logicznych, aby każdy mógł dostrzec problem.

Zacznijmy od prostego testu. Wygenerujemy trzy „cykle” PWM z wypełnieniem kolejno 90% oraz 0%. Wracamy więc do pierwszej wersji kodu i testujemy następujący fragment:

Po uruchomieniu możemy sprawdzić, że wszystko działa dokładnie tak, jak tego oczekujemy:

Podgląd wygenerowanych sygnałów za pomocą analizatora stanów logicznych

Podgląd wygenerowanych sygnałów za pomocą analizatora stanów logicznych

Spróbujmy więc zmienić wypełnienie z 90% na 100% – wystarczy podmiana tablicy:

Zmieniliśmy tylko wypełnienie, ale program zachowuje się inaczej, niż tego oczekujemy – wypełnienie wynosi co prawda 100%, ale szerokość impulsu wzrosła i odpowiada dwóm cyklom (2 × 1,25 µs).

Zaskakujące działanie programu – wzrost szerokości impulsu

Zaskakujące działanie programu – wzrost szerokości impulsu

Co ciekawe, jeszcze gorzej będzie, jeśli użyjemy wartości pośredniej, przykładowo 94%:

Teraz nasz przebieg wygląda zupełnie inaczej, niż byśmy chcieli:

Jeszcze dziwniejszy efekt działania pozornie prostego programu

Jeszcze dziwniejszy efekt działania pozornie prostego programu

Aby zrozumieć przyczynę takiego zachowania naszego PWM, musimy nieco dokładniej zagłębić się w działanie układu czasowo-licznikowego. W części 8 kursu wspominaliśmy, że sercem timera jest rejestr nazwany CNT (TIM counter register), czyli 16-bitowy rejestr, który jest zwiększany w każdym cyklu zegara. Jego wartość jest porównywana z kolejnym rejestrem, tym razem o nazwie ARR (Auto-reload register), którego wartość ustawiliśmy na 99 podczas konfiguracji za pomocą CubeMX.

Fragment ustawień z CubeMX

Fragment ustawień z CubeMX

Wartość CNT zmienia się od 0 do 99, dlatego okres naszego timera wynosi 100. Po upływie tego czasu generowane jest zdarzenie o nazwie UEV (Update Event), wartość rejestru CNT jest zerowana, a to już rozpoczyna kolejny okres PWM, na wyjściu jest więc ustawiany stan wysoki.

Każdy kanał posiada własny rejestr pamiętająco-porównujący, a w związku z tym, że używamy kanału numer 1, interesujący nas rejestr to CCR1 (TIM capture/compare register 1). Kiedy wartość rejestru CNT jest równa CCR1, generowane jest zdarzenie CCEV (Capture/Compare Event), a stan na wyjściu zmieniany jest na niski.

Wartość rejestru CCR1 jest buforowana w tzw. rejestrze cieni (shadow register). Czyli zapis do rejestru CCR1 nie daje efektu natychmiast, ale dopiero po zakończeniu cyklu PWM. Buforowanie to bardzo przydatny mechanizm, więc zostawiliśmy je włączone podczas konfiguracji timera.

Aktywacja buforowania w CubeMX

Aktywacja buforowania w CubeMX

Teraz wiemy już właściwie wszystko, co jest potrzebne do zrozumienia przyczyny naszych problemów. Mechanizm DMA działa w ten sposób, że po wystąpieniu zdarzenia CCEV zapisuje nową wartość z pamięci RAM, czyli naszej tablicy test do rejestru CCR1.

Niestety, nawet zapis za pomocą DMA nie następuje natychmiast, lecz wymaga nieco czasu. Jeśli wypełnienie jest bliskie 100%, to zdarzenie CCEV wystąpi za późno i kolejny cykl PWM rozpocznie się z użyciem poprzedniej wartości CCR1!

Znacznie lepszym rozwiązaniem byłoby kopiowanie danych w odpowiedzi na zdarzenie UEV zamiast CCEV, jednak funkcja HAL_TIM_PWM_Start_DMA tego nie umożliwia. Aby używać zdarzenia UEV, wracamy do CubeMX, przechodzimy do konfiguracji TIM3 i wybieramy zakładkę DMA Settings.

Jeśli mamy skonfigurowany kanał TIM3_CH1/TRIG, kasujemy go przyciskiem Delete, a następnie dodajemy nowy, o nazwie TIM3_CH4/UP. Parametry kanału ustawiamy podobnie jak poprzednio, czyli kierunek na Memory To Peripheral, a jako wielkość danych w pamięci ustawiamy Byte.

Nowa konfiguracja dla DMA w CubeMX

Nowa konfiguracja dla DMA w CubeMX

Teraz możemy zapisać zmiany i wrócić do edycji kodu – podobnie jak poprzednio najpierw musimy uruchomić działanie licznika bazowego:

Następnie zamiast funkcji HAL_TIM_PWM_Start_DMA, która – jak wiemy – nie spełnia naszych oczekiwań, wywołamy HAL_TIM_PWM_Start, której już używaliśmy wcześniej (gdy nie korzystaliśmy z DMA):

Teraz najtrudniejsza część, czyli wywołanie funkcji HAL_TIM_DMABurst_MultiWriteStart, dzięki której wykonamy transfer danych przy użyciu DMA:

Parametry tej funkcji to kolejno:

  • wskaźnik na nasz moduł licznikowy (htmi3),
  • rejestr, do którego będziemy zapisywać (CCR1),
  • zdarzenie, które będzie wyzwalać zapis (UPDATE / UEV),
  • bufor danych do wysłania,
  • ilość danych przesyłanych w jednym transferze DMA,
  • wielkość bufora.

Teraz nasz program będzie działał dokładnie tak, jak oczekujemy, niezależnie od wypełnienia sygnału:

Efekt działania poprawionej wersji programu

Efekt działania poprawionej wersji programu

Użycie funkcji HAL_TIM_DMABurst_MultiWriteStart jest nieco trudniejsze niż HAL_TIM_PWM_Start, ale może być niezbędne, jeśli chcielibyśmy używać wypełnienia bliskiego 100%. W przypadku diod RGB na szczęście wystarczy łatwiejsza opcja – temat ten opisaliśmy jednak w ramach ciekawostki.

Quiz – sprawdź, ile już wiesz!

Przygotowaliśmy aż cztery quizy, dzięki którym sprawdzisz, jak dużo zapamiętałeś z tego kursu. Masz za sobą już piętnaście części kursu, więc możesz zabrać się za kolejny quiz – składa się on z 15 pytań testowych (jednokrotnego wyboru), a limit czasu to 15 min. W rankingu liczy się pierwszy wynik, ale w quizie będziesz mógł później wziąć udział wielokrotnie (w ramach treningu).

Przejdź do quizu nr 3 z 4 »

Bez stresu! Postaraj się odpowiedzieć na pytania zgodnie z tym, co wiesz, a w przypadku ewentualnych problemów skorzystaj ze swoich notatek. To nie są wyścigi – ten quiz ma pomóc w utrwaleniu zdobytej już wiedzy i wyłapaniu tematów, które warto jeszcze powtórzyć. Powodzenia!

Quiz - najnowsze wyniki

Oto wyniki 10 osób, które niedawno wzięły udział w quizie. Teraz pora na Ciebie! Uwaga: wpisy w tej tabeli mogą pojawiać się z opóźnieniem, pełne wyniki są dostępne „na żywo” na stronie tego quizu.

# Użytkownik Data Wynik
1szymon81203.09.2021, 16:1093%, w 311 sek.
2Pennsatucky07.07.2021, 14:0793%, w 317 sek.
3ignisyo03.07.2021, 21:5980%, w 260 sek.
4Andrzej822704.08.2021, 12:2880%, w 269 sek.
5padus20.07.2021, 09:5980%, w 549 sek.
6Gieneq01.07.2021, 13:5573%, w 425 sek.
7omarsalloum03.09.2021, 13:1166%, w 484 sek.
8marville30.07.2021, 00:2940%, w 115 sek.
9malynia03.09.2021, 09:3526%, w 28 sek.

Zadanie domowe

  1. Rozwiąż powyższy quiz.
  2. Stwórz lampkę RGB, którą będzie można obsługiwać za pomocą przycisku USER_BUTTON (na płytce Nucleo). Każde naciśnięcie przycisku powinno powodować przejście do następnego stanu lampki zgodnie ze schematem: wszystkie czerwone > efekt 1 > wszystkie zielone > efekt 2 > wszystkie niebieskie > efekt 3 > diody wyłączone, a w miejscu stanów opisanych jako efekty diody powinny migać lub zmieniać kolory zgodnie z napisanymi przez Ciebie funkcjami.
  3. Wykorzystaj moduł diod RGB do wizualizowania odczytów z czujnika ultradźwiękowego. Gdy przeszkoda jest dalej niż 100 cm od czujnika, wszystkie diody powinny świecić się na zielono. W zakresie od 99 do 50 cm kolejne diody (zgodnie z kierunkiem wskazówek zegara) powinny zmieniać swój kolor na żółty. Analogicznie w zakresie od 49 do 10 cm diody powinny zmieniać kolor na czerwony, a przy odległości poniżej 10 cm powinny migać na czerwono.

Podsumowanie – co warto zapamiętać?

Cyfrowe diody RGB to niezwykle użyteczne elementy, dzięki którym można zaoszczędzić wiele GPIO. Ich obsługa jest stosunkowo łatwa – trzeba tylko wiedzieć, jak podejść do tego tematu. Czasami nie warto korzystać z gotowych programów, które operują bezpośrednio na GPIO, bo znacznie lepsze efekty można uzyskać za pomocą sprzętowych peryferii.

Czy wpis był pomocny? Oceń go:

Średnia ocena 5 / 5. Głosów łącznie: 11

Nikt jeszcze nie głosował, bądź pierwszy!

Artykuł nie był pomocny? Jak możemy go poprawić? Wpisz swoje sugestie poniżej. Jeśli masz pytanie to zadaj je w komentarzu - ten formularz jest anonimowy, nie będziemy mogli Ci odpowiedzieć!

Do cyfrowych diod RGB jeszcze wrócimy, i to w kolejnej części, w której zajmiemy się komunikacją bezprzewodową za pomocą IR. Dzięki temu będziemy mogli zbudować zdalnie sterowaną lampkę RGB. Oczywiście przy okazji poznamy również kolejne zastosowanie liczników – wykorzystamy je właśnie do dekodowania sygnałów z odbiornika IR.

Nawigacja kursu

Główny autor kursu: Piotr Bugalski
Współautor: Damian Szymański, ilustracje: Piotr Adamczyk
Oficjalnym partnerem tego kursu jest firma STMicroelectronics
Zakaz kopiowania treści kursów oraz grafik bez zgody FORBOT.pl

diody, kurs, kursSTM32L4, rgb, stm32l4, WS2812B

Trwa ładowanie komentarzy...