Nieuchronnie zbliża się ten czas, gdy z telewizora będzie wyjeżdzała nam czerwona ciężarówka,  a na każdym kroku będziemy słyszeć piękną piosenkę o złamanym sercu, którą społeczność uznaje za kreacje swiątecznego klimatu. Tak, niedługo Boże narodzenie i oczywiście nie ma świąt bez światełek! Każdy elektronik już kilka miesięcy wcześniej głowi się jak by tu zaskoczyć sąsiada. Zwykłe diody RGB niestety już dawno stały się powszechne, ale pomocą przychodzą diody tzw. “adresowalne” WS2812B które od kilku lat królują jeśli chodzi o efekty świetlne.

Czym są te diody?

Są to diody RGB w obudowie 5050 z wbudowanym kontrolerem PWM o rozdzielczości 8-bit na każdy kolor. Wartości wypełnienia PWM dostarczane są w postaci cyfrowej poprzez tylko jedną linię danych. Z diodami WS2812B rozmawiamy za pomocą jednoprzewodowej komunikacji NZR podobnie jak 1-Wire przy czym komunikacja w przypadku diod jest jednokierunkowa. Oznacza to, że możemy wysłać do diody informacje o tym jak ma się ustawić, ale nie dowiemy się od niej w jakim znajduje się stanie. Na szczęście nie jest to jakiś szczególny problem. Jak to wygląda? Oczywiście najlepiej wszystko tłumaczy dokumentacja diod ( WS2812B Datasheet). Mimo że jest ona bardzo krótka to skrócę tutaj najważniejsze informacje.

Przesyłane sygnały podobnie tak jak w przypadku 1-Wire mają z góry określone stałe czasowe. Do pełni szczęścia potrzebne nam są jedynie 3 sygnały: logiczne 0, logiczne 1 oraz RESET.

 

Z powyższego obrazu wynika, że częstotliwość komunikacji znajduje się w okolicach 800 kHz.

Ważnym feature’m diod jest możliwość łączenia ich kaskadowo. Diody posiadają 4 nóżki. Dwie oczywiście do zasilania (w zakresie 3,5÷5,3 V)  oraz dwie dla danych – wejście i wyjście. Dioda po dostarczeniu niej wszystkich potrzebnych jej bajtów na temat kolorów, automatycznie przerzuca sygnał wejściowy na swoje wyjście. Dzięki temu można łączyć je teoretycznie w nieskończone łańcuchy. 

Producent zaznacza również, aby przy każdej diodzie w kaskadzie zajdował się kondensator 100 nF. Dostępne w zakupie taśmy z Państwa Środka posiadają takie kondensatory.

Sekwencja wpisywania danych do diod rozpoczyna się od sygnału RESET trwającego co najmniej 50 µs. Następnie idą dane dla diod w kolejności GRB od pierwszej do ostatniej diody. Ważne jest to, że dane nie mogą mieć absolutnie żadnej przerwy podczas przesyłania. Od momentu w którym wystąpi anomalia kolory zaczną być inne niż planowane.

Kod STM32

Dzisiaj będę bazował na STM32F103C8T6 który znajduje się w tanich płytkach z Chin szerzej znanych jako BluePill. Te chińskie moduły są bardzo popularne i tanie. Sam mam ich kilka, więc czemu by ich nie używać.

Mikrokontrolery nie mają sprzętowej obsługi interfejsu wymaganego przez diody WS2812B. Trzeba poradzić sobie w inny sposób. Pierwsze do głowy przychodzi banglowanie GPIO. W AVR programiści radzą sobie poprzez asemblerową instrukcję ‘nop’ dzięki której można w mniej lub bardziej kontrolowany sposób odczekać wymagany czas do zmiany stanu. Leczy używając nopów w STM32 nie sprawię, że napiszę uniwersalną i przenośną bibliotekę – mamy mnóstwo różnych konfiguracji MCU i używamy mnóstwo różnych taktowań. Potrzebuję czegoś lepszego. Można pilnować GPIO Timerem i zmieniać jego stan po określonych czasach. Może zdałoby to egzamin, ale nie próbowałem nawet tego sposobu. Natknąłem się w internecie na pomysł użycia sygnału MOSI z interfejsu SPI. Wydało mi się to ciekawe więc zacząłem drążyć temat. Schemat połączenia paska diod jest banalnie prosty. Wykorzystam SPI numer 1. Uwaga! Nie zasilaj bezpośrednio z modułu STM32. Diody WS2812B potrzebują przy maksymalnych jasnościach ~50 mA prądu na każdą sztukę. Łańcuch 60 diod/metr będzie w takim wypadku wymagał ~3 A/metr przy pełnym, jasnym białym świetle! Zasilanie dostarczaj z zewnątrznego wydajnego zasilacza.

Pin PA5 służy jako SCK interfejsu SPI1. Nie będzie on potrzebny lecz w tym wypadku nie można go użyć do inego celu niż SPI.

Aby kontrolować szerokość impulsu sygnału NRZ musiałem wykorzystać cały bajt SPI jako jeden bit dla diody. Także czas trwania jednego bajtu powinien wynosić ok 1,25 µs więc jeden bit SPI powinien trwać ok 0,156 µs. Daje to taktowanie SPI na poziomie 6,4 MHz. W F103C8T6 wartość prescalera SPI do wyboru mam 2, 4, 8, 16, 32, 64, 128 i 256 co odpowiednio daje taktowanie MCU:

  1. 2 * 6,4 = 12,8 MHz
  2. 4 * 6,4 = 25,6 MHz
  3. 8 * 6,4 = 51,2 MHz
  4. 16 * 6,4 = 102,4 MHz…

Wartości szczerze mówiąc nieciekawe. Ciężko jest ustawić takie w Cube. Spotkałem się na pewnym popularnym forum ze stwierdzeniem, że MUSI BYĆ 6,4 MHz I KROPKA! No niestety, ale nie musi. Na mamy coś takiego jak tolerancja(w końcu tyle się o nią wszędzie walczy) i dla WS2812B tolerancja sygnałów wejściowych może być w wysokości ±0,15 µs, czyli ±0,66 MHz. Dużo łatwiej będzie uzyskać 6 MHz na SPI np za pomocą 48 MHz taktowania MCU i prescalera SPI ustawinego na wartość 8. Cube dodatkowo podpowiada baudrate SPI po wybraniu prescalera. Jeśli chcesz wyższego/niższego takowania MCU, szukaj takiego, na którym prescaler da ~6 MHz na SPI.

6 MHz oznacza czasy trwania bitów:

  1. 1 bit – 0,166 µs
  2. 2 bit – 0,333 µs
  3. 3 bit – 0,499 µs
  4. 4 bit – 0,666 µs
  5. 5 bit – 0,833 µs
  6. 6 bit – 0,999 µs
  7. 7 bit – 1,166 µs
  8. 8 bit – 1,333 µs

Czy coś z powyższych czasów pasuje do tabelki stałyczh czasowych WS2812B?

Do T0H podpasują 2 bity (0,35 µs ±0,15) a do T1H 5 bitów (0,9 µs ±0,15). Reszta naturalnie też podpasuje 😉

Jako, że SPI transferowane jest od LSB, definicje stanów logicznych wyglądać będą następująco:

1
2
#define zero 0b00000011
#define one 0b00011111

Dla ułatwienia powołałem strukturę która zawiera dane dla kolorów jednej diody.

1
2
3
typedef struct ws2812b_color {
uint8_t red, green, blue;
} ws2812b_color;

Podstawowa biblioteka zawiera tylko trzy samokomentujące się funkcje.

1
2
3
void WS2812B_Init(SPI_HandleTypeDef * spi_handler);
void WS2812B_SetDiodeColor(int16_t diode_id, ws2812b_color color);
void WS2812B_Refresh();

Inizjalizacja polega jedynie na przypisaniu wskaźnika na SPI do biblitoteki.

Ustawienie diody dokonuje się przez numer diody w kaskadzie oraz podanie zmiennej strukturalnej z kolorami tej diody.

Odświeżenie ustawia bajty bufora SPI według kolorów diod. Tworzony jest w niej wielki bufor, który zawiera wszystkie dane dla wszystkich diod. Niestety zjada to ogromne ilości RAMu, ale jest na to rada o czym będzie za chwilę.

Przesłanie danych

Myślę, że przesyłanie danych do diod jest warte omówienia. Jest to bowiem ciekawe wyzwanie, aby dane dotarły do diod jednym ciągiem oraz niewielkim nakładem MCU.

Przygotowanie danych w buforze według kolorów zawiera sporo przesunięć bitowych które na szczęście są lekkie dla MCU.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
for(uint8_t i = 0; i < 72; i++)
buffer[i] = 0x00;

for(uint16_t i=0, j=72; i<WS2812B_LEDS; i++)
{
//GREEN
for(int8_t k=7; k>=0; k--)
{
if((ws2812b_array[i].green & (1<<k)) == 0)
buffer[j] = zero;
else
buffer[j] = one;
j++;
}

//RED
for(int8_t k=7; k>=0; k--)
{
if((ws2812b_array[i].red & (1<<k)) == 0)
buffer[j] = zero;
else
buffer[j] = one;
j++;
}

//BLUE
for(int8_t k=7; k>=0; k--)
{
if((ws2812b_array[i].blue & (1<<k)) == 0)
buffer[j] = zero;
else
buffer[j] = one;
j++;
}
}

HAL_SPI_Transmit(hspi_ws2812b, buffer, (WS2812B_LEDS+3) * 24, 1000);

I to działa, ale nie do końca poprawnie. Niestety końcowe najczęściej 3-4 diody przy pasku 100 sztuk mają losowe kolory. Bierze się to pewnie stąd, że podczas transferu SPI wpada jakieś przerwanie. Prawdopodobnie jest to przerwanie SysTick Timera, ale nie sprawdzałem – nie ma sensu. Można je wyłączyć, ale nie polecam. Co teraz? Każdy STM32 ma coś takiego jak DMA. Tłumacząc na polski jest to takie peryferium o bezpośrednim dostępie do pamięci. Można mu oddelegować jakąś operację związaną z pamięcią lub innym peryferium. Dzięki temu MCU ma czas na wykonywanie innych zadań jak np. przerwanie SysTick które burzyło transfer danych do diod.

Konfiguracja DMA w Cube jest prosta. W zakładce Configuration i tabeli System jest przycisk konfiguracji DMA. Dodaj konfiguracje DMA dla SPI1_TX, które zostało wcześniej skonfigurowane. Priorytet ustaw jako Very High ponieważ jest to kluczowa operacja. W ustawieniach w dolnej części tryb Normal oraz inkrementację danych dla pamięci, w tym wypadku będzie to bufor RAM.

Jak teraz ma działać? Zmienia się tylko wywołanie transferu danych po SPI w funkcji odświeżąjącej. Teraz wygłąda to tak:

1
2
HAL_SPI_Transmit_DMA(hspi_ws2812b, buffer, (WS2812B_LEDS+3) * 24);
while(HAL_DMA_STATE_READY != HAL_DMA_GetState(hspi_ws2812b->hdmatx));

Druga linijka jest to oczekiwanie na zakończenie transferu. Można z niej zrezygnować, ale należy robić to świadomie i uważać na to, aby nie spowodować “kolizji” na DMA bo się wszystko popsuje.

Teraz diody działają miodzio. Każda ma dokładnie ten kolor który przewidziałem.

Przesłanie danych dla 35 diod(tyle mam podłączone do testów) to nieco ponad 1 ms, a dokładniej 1,22 ms. Czas potrzebny na przygotowanie bufora do wysłania to 0,27 ms z życia MCU. Do prostych zastosowań można czas transferu odczekać tak jak to zrobiłem. Warto jednak pokusić się o wykorzystanie tego czasu na inne zadania. Jest to prawie milisekunda przy zaledwie 35 diodach. Sterując ilością 100 diod ten zaoszczędzony czas będzie wynosił około 2,8 ms, a tysiąc diod to już 28 ms. Oszczędność spora bo można w tym czasie odświeżyć jakiś TFT zamiast bezczynnie czekać.

Niestety ogromną wadą tego rozwiązania jest bufor danych przekazywany do transferu SPI. Każda dioda pożera 24 bajty z RAMu. Dla STM32F103C8T6 kompilator zgłasza problemy z pamięcią już przy około 300 diodach. Trochę smutno 🙁

Ograniczenie zużycia RAM

Jest na to sposób! Każdy STM32 nie dość, że ma DMA to jeszcze kilka ciekawych przerwań które może ono wywołać w trakcie swojej pracy.

  • Half-transfer
  • Transfer complete
  • Transfer Error

Pierwsze dwa będą idealne. Przecież można stworzyć mały bufor danych, puścić cykliczne DMA i w momencie wystąpienia przerwania od połowy wykonanego transferu, podmienić pierwszą połowę bufora. Genialne! Zauważ też , że przygotowanie kompletnego bufora trwa zdecydowanie mniej czasu niż przesłanie połowy tego bufora. MCU powinien zdążyć z palcem w… GND 😉

Ważna rzecz: Funkcja startująca transfer SPI przez DMA przyjmuje rozmiar bufora trzymającego dane, a nie ilości wszystkich danych które chcesz przesłać! Wystartowanie DMA w trybie bufora cyklicznego będzie wysyłało dane dopóki tego ręcznie nie przerwiesz. Dlatego w przerwaniach half-transfer i full-transfer trzeba liczyć dla ilu diod już wysłano danych i po ostatniej paczce zatrzymać DMA.

Bufor danych skrócę do 48 bajtów, czyli zmieszczą się w nim dane dla dwóch diod. Ładnie będzie się to paczkowało. Schemat działania jest następujący:

  1. Załaduj 2*24 bajty sygnału reset i wystartuj cykliczną transmisję DMA.
  2. Half-transfered trigger – załaduj kolejne 24 bajty do pierwszej połowy bufora.
  3. Full-transfered trigger – dane pierwszej diody do drugiej połowy bufora
  4. Half-transfered trigger – dane drugiej(parzystej) diody do pierwszej połowy bufora
  5. Full-transfered trigger – dane trzeciej(nieparzystej) diody do drugiej połowy bufora
  6. Powtarzaj 4 i 5 aż wszystkie diody zostaną przesłane
  7. Ciesz się efektem

Ciekawostka. Biblioteka HAL jest napisana w taki sposób, że callbacki poszczególnych  przerwań DMA są w niej zadeklarowane z symbolem weak. Oznacza to, że można je nadpisać w swoich plikach źródłowych jednak muszą mieć tą samą nazwę, argumenty oraz zwracany typ. ST napisało HALa tak, aby nie martwić się o włączanie odpowiednich przerwań DMA za pomocą bitów i rejestrów. Jeżeli zadeklarujemy własne funkcje adekwatne do wymaganego przez nas przerwania, wewnątrz funckji HAL_SPI_Transmit_DM zostanie wykryty ten fakt i biblioteka sama za nas aktywuje odpowiednie przerwania. Nie trzeba się o nic martwić. Fajnie, nie? Jedyne o czym należy pamiętać to aktywowanie w Cube globalnego przerwania od DMA i nadanie mu priorytetu. Przechodząc do mojego kodu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
void WS2812B_Refresh()
{
CurrentLed = 0;
ResetSignal = 0;

for(uint8_t i = 0; i < 48; i++)
buffer[i] = 0x00;

HAL_SPI_Transmit_DMA(hspi_ws2812b, buffer, 48); // Additional 3 for reset signal
while(HAL_DMA_STATE_READY != HAL_DMA_GetState(hspi_ws2812b->hdmatx));
}

void HAL_SPI_TxHalfCpltCallback(SPI_HandleTypeDef *hspi)
{
if(hspi == hspi_ws2812b)
{
if(!ResetSignal)
{
for(uint8_t k = 0; k < 24; k++) // To 72 impulses of reset
{
buffer[k] = 0x00;
}
ResetSignal = 1; // End reset signal
}
else // LEDs Odd 1,3,5,7...
{
if(CurrentLed > WS2812B_LEDS)
{
HAL_SPI_DMAStop(hspi_ws2812b);
}
else
{
uint8_t j = 0;
//GREEN
for(int8_t k=7; k>=0; k--)
{
if((ws2812b_array[CurrentLed].green & (1<<k)) == 0)
buffer[j] = zero;
else
buffer[j] = one;
j++;
}

//RED
for(int8_t k=7; k>=0; k--)
{
if((ws2812b_array[CurrentLed].red & (1<<k)) == 0)
buffer[j] = zero;
else
buffer[j] = one;
j++;
}

//BLUE
for(int8_t k=7; k>=0; k--)
{
if((ws2812b_array[CurrentLed].blue & (1<<k)) == 0)
buffer[j] = zero;
else
buffer[j] = one;
j++;
}
CurrentLed++;
}
}
}
}

void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
if(hspi == hspi_ws2812b)
{
if(CurrentLed > WS2812B_LEDS)
{
HAL_SPI_DMAStop(hspi_ws2812b);
}
else
{
// Even LEDs 0,2,0
uint8_t j = 24;
//GREEN
for(int8_t k=7; k>=0; k--)
{
if((ws2812b_array[CurrentLed].green & (1<<k)) == 0)
buffer[j] = zero;
else
buffer[j] = one;
j++;
}

//RED
for(int8_t k=7; k>=0; k--)
{
if((ws2812b_array[CurrentLed].red & (1<<k)) == 0)
buffer[j] = zero;
else
buffer[j] = one;
j++;
}

//BLUE
for(int8_t k=7; k>=0; k--)
{
if((ws2812b_array[CurrentLed].blue & (1<<k)) == 0)
buffer[j] = zero;
else
buffer[j] = one;
j++;
}
CurrentLed++;
}
}

}

Wyniki zmniejszonego bufora

Jak wygląda teraz przebieg i poszczególne etapy przygotowywania i wysyłania danych z bufora?

Czas transferu całej ramki nie uległ zmianie znaczącej zmianie. Całościowo transfer wzrósł nieznacznie do 1,23 ms. Czas wymagany na przesłanie połowy bufora(24 bajty) przez DMA to 31 µs. Bardzo mało. Przygotowanie kolejnej porcji 24 bajtów danych zajmuje MCU jedynie 7 µs. Daje to 26 µs oszczędności czasu na CPU dla jednej diody, który może zajmować się czymś innym. Mało? Przy 100 diodach będzie to 2,6 ms natomiast przy 1000 mamy 26 ms czasu dla CPU. Różnice czasu transferu w porównaniu do przesłania jednego, dużego bufora są niewielkie. Za to jaka oszczędność RAMu! 912 bajtów dla pełnego bufora vs 48 bajtów dla porcjowania i to tylko przy 35 diodach. Zwiększając liczbę świecących punktów w łańcuchu  oraz trzymając się koncepcji z jednym, wielkim buforem wielkość bufora drastycznie rośnie. 100 diod to już ~2,4 kB danych. Natomiast korzystając z małego bufora i połówkowych przerwań DMA bufor pozostaje niezmienny! Pięknie.

Podsumowanie

Diody WS2812B są świetne. Przy na prawdę minimalnej ilości połączeń jesteśmy w stanie ustawić KAŻDĄ diodę z osobna. Co prawda nie ma tu faktycznego adresowania diod, ale w łatwy sposób można się po nich poruszać w buforze. Efekt ten uzyskujemy bez multipleksowania i bez rozdzielania kanałów dla kolorów. Sterowanie może wydawać się kłopotliwe, ale umiejętne wykorzystanie standardowych interfejsów w STM32 pozwala na bezproblemową kontrolę naprawdę długich łańcuchów. Przedstawione w tym wpisie wykorzystanie SPI ma jedną, podstawową wadę. Pin odpowiedzialny za SCK interfejsu szeregowego jest nieużywany i nie można nic z nim zrobić(bynajmniej ja o tym nic nie wiem). Tworząc upchany i skomplikowany projekt ten pin mógłby się przydać. Na szczęście większość znanych mi projektów wykorzystujących efekciarskie kombinacje na diodach nie zawiera zbyt dużo układów połączonych do MCU. Bierz i twórz swoje! Za dwa tygodnie zaprezentuję kilka gotowych efektów świetlnych, które z pewnością przydadzą się na choince.

Dziękuję Ci za przeczytanie tego wpisu. Jeśli taka tematyka Ci odpowiada, daj mi znać w komentarzu. Będę też wdzięczny za propozycję tematów które chciałbyś abym poruszył.

Kod standardowo dostępny jest na moim GitHubie: link

Jeśli zauważyłeś jakiś błąd, nie zgadzasz się z czymś, chciałbyś coś dodać istotnego lub po prostu uważasz, że chciałbyś podystkutować w tym temacie, napisz komentarz. Pamiętaj, że dyskusja ma być kulturalna i zgodna z zasadami języka polskiego


2 Komentarze

Konari · 02/11/2018 o 21:05

Czy próbowałeś generować przebiegi przez GPIO ? Czy od razu założyłeś że zrobisz przez SPI ?

    Mateusz · 02/11/2018 o 22:09

    Cześć! Od razu założyłem, że użyję SPI. Chcę spróbować jeszcze generować sygnał przy pomocy Timera, ale czy to będzie GPIO to nie mogę obiecać. Na pewno da się to pożenić z PWM i DMA i efekt będzie podobny to tego na SPI z tym, że będzie do wyboru któryś z kanałów PWM Timera. Na pewno napiszę o tym, czy mi się udało czy nie. Jak się uda to będzie opisane całe rozwiązanie wraz z kodem 🙂

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Serwis wykorzystuje pliki cookies. Korzystając ze strony wyrażasz zgodę na wykorzystywanie plików cookies. więcej informacji

Wrażenie zgody na pliki Cookies jest konieczne, aby uzyskać najlepsze wrażenia z przeglądania strony. Jeżeli nadal nie wyraziłeś zgody na używanie plików Cookies, zaakceptuj poniżej klikając w przycisk "Akceptuj" na banerze.

Close