Adresowane diody WS2813, czyli PWM i DMA w akcji

Zbliża się czas dekorowania domów przed świętami. A jeśli mowa o oświetleniu, które wprowadzi nas w świąteczny nastrój, to każdy elektronik/programista embedded ma w głowie tylko jedno – paski LED z diodami adresowanymi. Przecież sterowanie każdą diodą z osobna daje dużo więcej frajdy i możliwości, a przy ich pomocy możemy stworzyć iluminacje, które wyróżnią nasze pomieszczenie.

Interfejs sterujący diodami WS2813

Diody WS2813 to ulepszona wersja popularnych, adresowanych diod RGB WS2812, z którymi pewnie każdy elektronik miał kiedyś w życiu styczność. WS281x swoją popularność zyskały dzięki dużym możliwościom, przystępnej cenie oraz dobrej dostępności. Mają wygodny interfejs sterujący, który wymaga tylko jednej linii danych, dzięki czemu jesteśmy w stanie sterować dużą ilością elementów nie zajmując przy tym wielu wyjść mikrokontrolera.

Czym różnią się diody WS2813 od poprzedniej wersji? Przede wszystkim odpornością na uszkodzenia. I nie mam tutaj na myśli samej podatności na uszkodzenia mechaniczne, ale wdrożony system dwóch linii sterujących, który powoduje, że (w przeciwieństwie do WS2812) uszkodzenie jednej diody nie powoduje, że wszystkie, które są w linii za nią również przestają działać. Dopiero uszkodzenie dwóch diod (i to dwóch jedna za drugą) przerywa nam łańcuch oświetlenia. Ma to ogromne znaczenie w przypadku systemów, gdzie działanie każdej diody nie jest warunkiem krytycznym, np. w przypadku dekoracji świątecznych. Uszkodzenie jednej diody czy nawet kilku w przypadku WS2813 nie zmusza nas do wymiary/naprawy taśmy. Sposób działania zabezpieczenia przedstawia poniższa grafika (źródło).

Sposób podłączenia diod z dwuprzewodową wersją interfejsu przedstawia poniższa grafika. Jak możemy zauważyć, dodanie linii sterującej nie zwiększa zużycia wyjść po stronie mikrokontrolera, a jedynie dubluje sygnał sterujący podawany na linie.

Drugą, dość istotną zmianą, jest częstotliwość odświeżania PWM, która w porównaniu do WS2812 wzrosła z 400 Hz do 2 kHz. Pozwala to uniknąć efektu migotania, widocznego w szczególności w przypadku nagrań w rozdzielczości HD. Należy też dodać, że diody WS2813 są dostępne w różnych obudowach (wersje A, B, B-Mini, C-2020 oraz E), które mogą różnić się parametrami pracy, jak jasność i pobór prądu. Ja będę pracować z wersją WS2813B dostępną na pasku LED od firmy Seeed Technology.

Ostatnio na rynku pojawiła się kolejna wersja adresowanych diod, czyli WS2815. Od opisywanej w artykule wersji WS2813 różni się w zasadzie napięciem zasilania – w przypadku WS2812 i WS2813 nominalnym napięciem było 5 V, natomiast dla WS2815 przewidziano zasilanie napięciem 12 V. Pozwala to zredukować prąd pobierany przez diody przy zachowaniu, a nawet zwiększeniu ich jasności.

Interfejs sterujący w miarę wprowadzania kolejnych wersji w zasadzie się nie zmieniał. Jest to jednoprzewodowy sygnał, w którym przy zachowaniu odpowiednich czasów trwania stanów wysokich i niskich przesyłamy kolejne bity sterujące jasnością i kolorem każdej z diod. Interfejs zakłada pracę z częstotliwością przełączania bitów danych ok. 800 kHz. Czasy trwania stanu wysokiego i niskiego przy przesyłaniu logicznej '1′ i '0′ przedstawione są w poniższej tabeli. Wartości te dla poszczególnych wersji diod mogą się nieznacznie różnić, jednak proporcje i sam sposób sterowania jest zachowany.

SymbolOpisCzas
T0HKod '0′, czas trwania stanu wysokiego220 ns – 380 ns
T1HKod '1′, czas trwania stanu wysokiego580 ns – 1000 ns
T0LKod '0′, czas trwania stanu niskiego580 ns – 1000 ns
T1LKod '1′, czas trwania stanu niskiego220 ns – 420 ns
RESReset (stan niski)> 280 us

W momencie odebrania danych przez pierwszą diodę pobiera ona pierwsze 24 bity, a resztę retransmituje dalej. Każda kolejna dioda postępuje analogicznie, dzięki czemu przesyłając w odpowiedniej kolejności dane jesteśmy w stanie sterować każdą z diod indywidualnie. Bardziej obrazowo metoda transmisji przedstawiona została na poniższych wykresach.

W przypadku sterowania diodami WS2813 należy zwrócić uwagę na jeszcze jedną rzecz – niekompatybilne poziomy napięć. Nasze diody zasilane są napięciem 5 V, a sygnały sterujące STM32 pracują w zakresie 0 – 3,3 V. Zaglądając głębiej w dokumentację możemy poznać szczegóły, które pozwolą nam na zastosowanie jednego z kilku rozwiązań.

Po pierwsze napięcie zasilania diod nie musi wynosić koniecznie 5 V – WS2813 ma zakres napięcia wynoszący od 3,7 V do 5,3 V. Po drugie stan wysoki powinien przyjmować min. 0,7*VDD, czyli przy 5 V co najmniej 3,5 V. W przypadku projektów hobbystycznych możemy pójść na kompromis i zmniejszyć napięcie zasilania np. do 4,5 V, co zmniejszy maksymalną jasność, ale spowoduje spadek wymaganego napięcia stanu wysokiego do 3,15 V. Możemy również „zaryzykować” w skrajnych przypadkach niestabilne działanie i sterować przy napięciu zasilania 5 V sygnałem 3,3 V z STM32. Trzecim rozwiązaniem jest zastosowanie dodatkowego układu, który dokona konwersji poziomów napięcia np. tranzystora, czy popularnych modułów z konwerterem poziomów napięć (również opartych na tranzystorach) np. Logic Level Converter.

Konfiguracja mikrokontrolera

Znając zasadę pracy interfejsu stosowanego w WS2813, możemy przejść do konfiguracji mikrokontrolera. Program zaimplementuję dla płytki Nucleo-L476RG.

Pozostaje nam wybór sposobu generowania sygnału czasowego wymaganego przed diody. Nie ma w uC dostępnego konkretnego układu peryferyjnego, które byłoby dedykowany pod sterowanie WS2813, dlatego wykorzystamy jeden ze standardowych interfejsów. W sieci można znaleźć różne rozwiązania na sterowanie tego typu układami – od użycia interfejsów komunikacyjnych (UART, SPI) z dobraną odpowiednią prędkością transmisji danych, po zastosowanie sygnału generowanego za pomocą timera i GPIO lub PWM. Która metoda jest poprawna? Właściwie każda, jeśli prowadzi do zamierzonego celu. Zastosowanie UART czy SPI daje nam lepszą portowalność między różnymi układami, ale może wymagać większej konfiguracji niż np. PWM. Ja chciałbym przedstawić sposób sterowania przy użyciu sygnału PWM i zastosowaniu kontrolera DMA, który odciąży nam procesor w trakcie transmisji danych.

Tworzymy zatem nowy projekt dla Nucleo-L476RG. Wykorzystamy TIMER 2 i kanał 1 jako wyjście PWM.

Aby otrzymać sygnał o częstotliwości ok. 800 kHz, który wymagany jest do sterowania WS2813, musimy odpowiednio dobrać wartość Prascalera i Period. Ze względu na to, że STM32L476RG pracuje z maksymalną częstotliwością 80 MHz, musimy go podzielić przez 100. Przykładowo możemy więc zastosować Preskaler o wartości 0 oraz Period o wartości 100. Kanał 1 będzie pracował w trycie PWM1 oraz polaryzacją High.

Jak już wspomniałem wcześniej, chcemy wykorzystać kontroler DMA do przesyłania danych. Aby go skonfigurować przechodzimy do zakładki DMA Settings. Wybieramy opcję Add i wybieramy kierunek przepływu danych Memory to Peripheral. Zaznaczamy opcję inkrementacji adresów przy Memory oraz szerokość danych jako Word przy Peripheral (rejestr CCR1 w TIMER 2 jest 32 bitowy) oraz Byte przy Memory (będziemy dane przesyłać bajtami).

Wyjście TIM2_CH1 będzie dostępne na pinie PA0. Konfiguracja układu będzie więc wyglądała jak na grafice poniżej.

Mając skonfigurowane peryferia mikrokontrolera, możemy przejść do napisania kodu programu.

Implementacja

Do obsługi diod WS2813 napiszemy prostą bibliotekę składającą się z plików „led_ws2813b.c” oraz „led_ws2813b.h”. Od razu chciałbym zaznaczyć, że będzie ona miała za zadanie jedynie wysterować diody LED z możliwością ustawienia koloru i jasności każdej z nich. Nie będę przedstawiał sposobu tworzenia efektów świetlnych.

W pliku „led_ws2813b.h” dodajemy stałe określające rozmiary bufora, ilość sterowanych przez nas diod oraz strukturę i unię, dzięki której będziemy w przejrzysty sposób przechowywać i zmieniać dane dotyczące kolorów. Ze względu na to, że jeden okres sygnału PWM będzie odpowiadał za jeden bit danych, do przesłania danych dla jednej diody będzie potrzebna tablica o rozmiarze 24 bajtów (3 kolory x 8 bitów na każdy kolor). W strukturze rgb_led będziemy zatem przechowywać informację dla jednej diody, zaś w unii rgb_led_buffer dane o wszystkich diodach. Ja będę używał do testów paska LED z 30 diodami. Dodatkowo w unii umieściłem bajt stop, który będzie przesyłany na koniec transferu. Pozwoli on wywołać stan niski odpowiadający funkcji reset.

#define RGB_LED_BUFFER_SIZE		24
#define	NUMBER_OF_LEDS			30
#define HEAD_LEDS_BUFFER_SIZE	(NUMBER_OF_LEDS*RGB_LED_BUFFER_SIZE + 1)

struct __attribute__((packed)) rgb_led
{
	uint8_t green[8];
	uint8_t red[8];
	uint8_t blue[8];
};

union rgb_led_buffer
{
    uint8_t bytes[HEAD_LEDS_BUFFER_SIZE];

    struct __attribute__((packed))
    {
		struct rgb_led leds[NUMBER_OF_LEDS];
		uint8_t stop;
    };
};

Zastosowanie unii powoduje, ze dane zawarte w strukturze będą jednocześnie przechowywane i automatycznie konwertowane do postaci tablicy bajtów. Dzięki temu bez przepisywania danych ze struktury do tablicy będziemy mieli gotowe dane do wysłania przez DMA.

W pliku „led”ws2813b.c” dodajemy kilka dodatkowych definicji stałych, które określają wypełnienie PWM dla poszczególnych stanów w protokole komunikacji. Jak zostały one obliczone? Ponieważ w polu Period wpisaliśmy wartość 100, mamy zakres sterowania wypełnieniem w zakresie od 0 do 100. Wartość 100 oznacza, że przez całe 1250 ns będzie stan wysoki. Zgodnie z opisem sygnału sterującego diodami WS2813, bit o wartości '1′ jest reprezentowany przez sygnał, w którym stan wysoki trwa przez czas od 580 ns do 1000 ns. Wybrałem wartość pośrednią, czyli ok. 800 ns. W takim wypadku z prostej proporcji możemy obliczyć, że aby mieć stan wysoki na wyjściu przez 800 ns, należy ustawić wypełnienie 64. Analogicznie bit '0′ powinien trwać ok. 300 ns (taką wartość przyjąłem z dostępnego zakresu podanego w dokumentacji), co odpowiada wypełnieniu 24.

#define LED_COMM_HIGH_STATE		64
#define LED_COMM_LOW_STATE		24
#define LED_COMM_RESET_STATE	        0


#define BIT_IN_BYTES			8

Najważniejszą funkcją potrzebną do realizacji komunikacji z WS2813 jest konwersja danych z postaci wygodnej dla nas do ustawiania w kodzie (czyli wartości określających jasność w zakresie od 0 do 255) na postać potrzebną do wysłania, czyli tablicę 3 x 8 bajtów. Aby taką konwersję wykonać, musimy każdy z bitów przekształcić na bajt. Zgodnie z opisanymi powyżej stałymi, gdy bit będzie miał wartość '1′, do tablicy bajtów w odpowiednie miejsce wpisujemy LED_COMM_HIGH_STATE, zaś dla bitu '0′ stałą LED_COMM_LOW_STATE.

void led_convert_color_data_to_ws2812b_format(uint8_t color_data, uint8_t *buffer)
{
	for(uint8_t i=0; i<BIT_IN_BYTES; i++)
	{
		if(((color_data >> i) & 0x01) == 1)
		{
			*(buffer+(BIT_IN_BYTES-i-1)) = LED_COMM_HIGH_STATE;
		}
		else
		{
			*(buffer+(BIT_IN_BYTES-i-1)) = LED_COMM_LOW_STATE;
		}
	}
}

Aby ułatwić sobie operacje na poszczególnych diodach dodamy jeszcze funkcję do ustawiania danych w konkretnej diodzie w pasku, podając jako argument adres do elementu unii lub numer diody na pasku. Poza tym dodałem możliwość ustawienia takiego samego koloru dla wszystkich diod, co znajdzie zastosowanie np. do zgaszenia wszystkich ledów.

void led_set_all_led_colors(uint8_t green, uint8_t red, uint8_t blue)
{
	uint32_t i;

	for(i=0; i < NUMBER_OF_LEDS; i++)
	{
		led_set_one_led_colors(i, green, red, blue);
	}
}

void led_set_one_led_colors(uint32_t led_number, uint8_t green, uint8_t red, uint8_t blue)
{
	led_set_colors_data(&(leds_buffer.leds[led_number]), green, red, blue);
}

void led_set_colors_data(struct rgb_led *led, uint8_t green, uint8_t red, uint8_t blue)
{
	led_convert_color_data_to_ws2812b_format(gamma_correction[green], led->green);
	led_convert_color_data_to_ws2812b_format(gamma_correction[red], led->red);
	led_convert_color_data_to_ws2812b_format(gamma_correction[blue], led->blue);
}

Przed rozpoczęciem pracy z diodami warto też wyczyścić kolor na każdej z nich. Dodatkowo ustawimy wartość wypełnienia LED_COMM_RESET_STATE dla bajtu kończącego transmisję.

void led_init(void)
{
	uint32_t i;

	for(i=0; i < NUMBER_OF_LEDS; i++)
	{
		led_set_one_led_colors(i, 0, 0, 0);
	}

	leds_buffer.stop = LED_COMM_RESET_STATE;
}

Na koniec dodamy jeszcze dwie funkcje. Pierwsza będzie odpowiedzialna za rozpoczęcie transferu danych przez DMA. Druga natomiast wyłączy sygnał PWM po zakończeniu transferu danych.

void led_send_led_colors_to_head(void)
{
	HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_1, (uint32_t *)leds_buffer.bytes, HEAD_LEDS_BUFFER_SIZE);
}
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
	if(TIM2 == htim->Instance)
	{
		HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1);
	}
}

W moim rozwiązaniu zakładam, że dane do wszystkich diod LED przesyłane są w jednym buforze, ponieważ chciałem skupić się na sposobie sterowania diodami bez zagłębiania się w kwestie algorytmu. Jest to wygodne rozwiązanie w przypadku, gdy mamy niedużo diod LED do obsługi lub mamy do dyspozycji dużo pamięci RAM i nie zależy nam na jej oszczędzaniu. Do sterowania jedną diodą LED potrzebujemy 24 B danych, zatem przy 96 kB dostępnych w STM32L476RG jesteśmy w stanie sterować ponad 4000 diodami. Chcąc jednak zoptymalizować ilość wykorzystywanego RAM-u możemy dane wysyłać fragmentami tzn. np. po 128 diod i podmieniać bufor w trakcie ich wysyłania (po wysyłaniu połowy zamienić dane w pierwszej połowie bufora itd.). Należy też pamiętać, że przez DMA możemy zlecić wysyłanie do 65535 bajtów, co oznacza wysłanie maksymalnie 2730 diod.

W funkcji main() wywołujemy inicjalizację struktury danych. Następnie dodałem krótki kod do zaprezentowania działania programu, który będzie zapalał kolejno każdą diodę, gasząc pozostałe.

if((HAL_GetTick() - time) > time_max_ms)
{
	time = HAL_GetTick();

	if(0 == state)
	{
		g = 255;
		r = 0;
		b = 0;
	}
	else if (1 == state)
	{
		g = 0;
		r = 255;
		b = 0;
	}
	else if (2 == state)
	{
		g = 0;
		r = 0;
		b = 255;
	}

	led_set_all_led_colors(0, 0, 0);
	led_set_one_led_colors(led, g, r ,b);
	led_send_led_colors_to_head();

	led++;

	if(led >= NUMBER_OF_LEDS)
	{
		led = 0;
		state++;
	}

	if(state >= 3)
	{
		state = 0;
	}
}

Pasek LED z diodami należy podłączyć do płytki Nucleo zgodnie ze schematem poniżej. Warto pamięć o dobrej jakości połączeniach i niezbyt długich przewodach, aby zakłócenia nie powodowały nam błędów w przesyłanych danych.

Efekt działania przedstawiony został na filmiku.

Podsumowanie

Adresowane diody LED RGB WS2813 to nowsze zamienniki dla popularnych i powszechnie znanych diod WS2812. Mają wbudowane zabezpieczenie w postaci podwójnej linii danych oraz większą częstotliwość odświeżania PWM, co przy nieznacznie większej cenie powoduje, że coraz bardziej wypierają z rynku starszą wersję. Interfejs zastosowany w diodach pozwala nam na indywidualne sterowanie kolorem i jasnością każdej diody, dzięki czemu możemy tworzyć niezwykłe efekty świetlne przy niewielkim zużyciu peryferiów mikrokontrolera. W materiale przedstawiłem komunikację opartą o PWM i kontroler DMA, dzięki czemu proces sterowania jest przejrzysty i nie obciąża mocno układu. Mam nadzieję, że pozwoli Ci stworzyć niezapomniane świąteczne (i nie tylko) iluminacje.

Do pobrania

Repozytorium GitHub

  1. Cześć, dlaczego sygnał reset zajmuje 1 bajt (czyli trwa 1.25us), gdzie w dokumentacji napisane jes,t aby trwał minimum 280us?

    1. Dodałem tylko jeden bajt, dlatego żeby mieć pewność, że po wysłaniu danych będzie tam stan niski. Potem ten stan niski jest utrzymywany przez długi czas, aż do wysłania następnych danych, czyli na pewno będzie dłuższy niż te 280us. Ale gdybyś chciał bardzo często wysyłać dane, to rzeczywiście warto dla pewności uwzględnić ten sygnał reset w ramce.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *