niedziela, 13 marca 2011

XMEGA + DMA i diody WS2812B cz. III





Autor: tmf
Redakcja: Dondu

Artykuł jest częścią cyklu: Wstęp do mikrokontrolerów XMEGA Atmel'a.

Wykorzystujemy peryferia…

W poprzedniej części tego mini cyklu poznaliśmy jak wykorzystać UART do transmisji danych w formacie zrozumiałym dla WS2812B. Jednak pokazane rozwiązanie ciągle nie do końca jest dobre – co prawda możemy sprzętowo wysyłać dane, co znacznie odciąża procesor, ale ciągle musimy często uzupełniać bufor nadajnika UART. W efekcie instrukcje uzupełniania bufora przeplatają pozostałe instrukcje programu. Co więcej, jeśli bufor będzie pełny to procesor zostanie wstrzymany do czasu jego opróżnienia.




Co możemy z tym zrobić? Jak wiemy w każdym mikrokontrolerze istnieje możliwość zgłaszania przerwań w chwili zwolnienia miejsca w buforze. Stąd też możemy po prostu napisać funkcję obsługi przerwania, której zadaniem będzie uzupełnienie bufora kolejną porcją danych. Proste, prawda?

Proste, ale niewygodne i niezbyt optymalne. Dlaczego? Przyczyną jest duża szybkość transmisji UART – w poprzednim przykładzie wybraliśmy szybkość 2 Mbps (potencjalnie nawet 2,5 Mbps), w efekcie cały bajt danych, składający się z 9-ciu bitów (bit startu, 7 bitów danych i bit stopu) jest wysyłany w czasie 3,6-4,5 µs.

Czy to dużo? Dla mikrokontrolera taktowanego zegarem 32 MHz przekłada się to na czas potrzebny do wykonania 115-144 jednotaktowych instrukcji asemblera. Jeśli korzystany nie z XMEGA, lecz np. ATMega i maksymalne taktowanie wynosi 16 MHz, to pomiędzy kolejnymi uzupełnieniami bufora mija zaledwie 58-72 instrukcji. A to już naprawdę niewiele.

Musimy uwzględnić, że samo wejście w funkcję obsługi przerwania trwa 5-7 taktów, do tego musimy doliczyć czas wykonania tej funkcji, który wyniesie co najmniej kilkanaście taktów. W efekcie czego mikrokontroler będzie tracił sporo czasu na obsługę przerwań UART. Przy taktowaniu 16 MHz zakładając, że napiszemy optymalną funkcję przerwania i całość zamknie się zaledwie w 20 cyklach, obsługa przerwań pochłonie nam około 27-35% czasu procesora.

Wbrew pozorom nie jest źle – jeśli byśmy zrezygnowali z UART i zrobili wszystko za pomocą manglowania pinem IO, tak jak to robiliśmy w I części, to mikrokontroler zajęty byłby w 100%. W dodatku, jeśli naszą XMEGę będziemy taktować zegarem 32 MHz, to obciążenie procesora wnoszone przez przerwanie UART wyniesie tylko połowę wcześniej wyliczonego (mikrokontroler będzie dwukrotnie szybciej realizował instrukcje, w efekcie czas wykonania funkcji obsługi przerwania się skróci); wyniesie więc 14-18%, a to już naprawdę nieźle. To znaczy, że 80% czasu procesora mamy do własnej dyspozycji, co dla XMEGI przekłada się na około 25 MIPsów – więcej niż ma do dyspozycji nieobciążona żadnym zadaniem ATMega!

Wydaje się, że jest więc ok – dodatkowo wielopoziomowy system przerwań XMEGI powoduje, że ciągle możemy używać przerwań o wyższym priorytecie, lub o niższym (co będzie się zdarzać częściej). Warto zauważyć, że w mikrokontrolerach z jednopoziomowym systemem przerwań (ATTiny i ATMega) moglibyśmy napotkać na problem – w aplikacji intensywnie wykorzystującej przerwania, których czas wykonania byłby dłuższy niż kilkanaście mikrosekund mogłoby dojść do całkowitego opróżnienia bufora nadajnika UART, co mogłoby zostać odebrane przez diody jako sygnał RESET.

W XMEGA możemy się przed taką sytuacją łatwo zabezpieczyć nadając przerwaniu UART wyższy priorytet – dzięki czemu będzie ono mogło przerywać przerwania o niższym priorytecie. Uff, brzmi to zawile, prawda? Porzućmy więc przerwania. Jakie jest inne rozwiązanie? Całkiem proste – w XMEGA mamy 4-ro kanałowe DMA!

Większości osób DMA kojarzy się z układem, którego główną zaletą jest szybkie przesyłanie danych pomiędzy różnymi obszarami pamięci – w końcu nazwa Direct Memory Access (DMA) zobowiązuje.
Ale to nie jedyne, a nawet najważniejsze funkcje DMA. Układ ten umożliwia także transfer m.in. pomiędzy pamięcią a rejestrami układów IO (np. rejestrem nadajnika UART).

W dodatku największe zalety DMA uwidaczniają się właśnie w sytuacji, w której zachodzi potrzeba transmisji danych pomiędzy pamięcią, a układem peryferyjnym działającym nieznacznie wolniej niż sam mikrokontroler.

Dlaczego? Jeśli układ peryferyjny jest naprawdę szybki to będziemy do niego transmitować dane z szybkością limitowaną tylko przez rdzeń CPU, użycie DMA w takiej sytuacji znacząco tego transferu nie przyśpieszy (ale ciągle może mieć pewne zalety, szczególnie jeśli użyty mikrokontroler ma oddzielne magistrale danych), z kolei jeśli układ peryferyjny jest ekstremalnie wolny (w stosunku do szybkości działania mikrokontrolera), np. I2C, to dane będziemy wysyłać do niego tak rzadko, że robienie tego w przerwaniach praktycznie pozostanie bez wpływu na obciążenie rdzenia mikrokontrolera.

A co jeśli układ peryferyjny spodziewa się kolejnych danych np. co 10 cykli zegarowych/instrukcji asemblera? Wtedy właśnie DMA pokazuje pazurki! Układ ten umożliwia automatyczną transmisję danych dokładnie wtedy, kiedy oczekuje tego nasz układ peryferyjny. Bez DMA bylibyśmy w kropce – dane musielibyśmy przesyłać na tyle szybko, że wykorzystanie przerwań nie miałoby sensu, gdyż mikrokontroler by się nie wyrabiał z ich obsługą, z drugiej strony, stosowanie strategii sprawdź czy mamy miejsce w buforze i prześlij dane wiąże się ze 100% obciążeniem mikrokontrolera.

Wiemy już, że DMA to to, co nam pomoże, zaprzęgnijmy więc je do pracy.

Zabawy z DMA

W XMEGA mamy do dyspozycji 4 kanały DMA, nam potrzebny będzie tylko jeden, transmitujący dane pomiędzy pamięcią, w której umieścimy dane dla WS2812B, a rejestrem nadajnika UART.

Większość potrzebnych funkcji – inicjalizacji UART oraz odpowiedzialnych za transkodowanie danych już mamy. Jedyne, co musimy dodać to funkcję inicjalizującą DMA. Będzie ona odpowiedzialna za transfer danych z bufora w RAM do UART. Czynnikiem wyzwalającym transfer będzie ustawienie flagi sygnalizującej wolne miejsce w buforze nadajnika (DREIF). Nasza funkcja będzie przyjmować dwa argumenty:

void DMA_init(void *from, uint16_t size)

Pierwszy to adres bufora w RAM, w którym mieszczą się dane do wysłania, a drugi to liczba danych do wysłania – od 1 do 65536.

Tu mała dygresja – w typie uint16_t możemy zapisać liczby z zakresu <0..65535>, jak więc przesłać 65536 bajtów? Otóż prosto – przesyłanie 0 bajtów nie ma sensu, stąd też twórcy XMEGA przyjęli, że jeśli określimy długość transferu na 0, to przesłanych zostanie 65536 bajtów!

Jest to niezwykle wygodne, gdyż 65536 w zapisie szesnastkowym to 0x10000, stąd też po obcięciu do 16-bitów mamy właśnie 0 – dzięki temu, aby przetransferować 65536 bajtów nie musimy wykonywać żadnych specjalnych operacji.

Przejdźmy do inicjalizacji DMA – wykorzystamy kanał CH3, chociaż oczywiście możemy wykorzystać dowolny z czterech dostępnych. Na początku odblokujemy kontroler DMA (jeśli wcześniej tego nie zrobiliśmy) i ustawimy priorytety kanałów na round robin:

DMA.CTRL=DMA_ENABLE_bm | DMA_DBUFMODE_DISABLED_gc | DMA_PRIMODE_RR0123_gc; //Włącamy DMA, round robin, bez podwójnego buforowania

Teraz musimy zainicjować parametry transferu dla użytego kanału DMA – określamy adres początku transferu (bufor z danymi), dokąd je będziemy przesyłać (rejestr nadajnika USARTC0), ile ich przesyłamy (size), a także zachowanie rejestrów adresowych (rejestr źródła po każdym transferze będzie inkrementowany, adres rejestru docelowego nie będzie się zmieniał – wszystkie dane trafią do rejestru USARTC0_DATA):

DMA.CH3.ADDRCTRL=DMA_CH_SRCRELOAD_BLOCK_gc | DMA_CH_SRCDIR_INC_gc | DMA_CH_DESTRELOAD_NONE_gc | DMA_CH_DESTDIR_FIXED_gc; //Nie przeładowujemy adresu docelowego
 DMA.CH3.SRCADDR0=(uint16_t)from;
 DMA.CH3.SRCADDR1=((uint16_t)from) >> 8;
 DMA.CH3.SRCADDR2=0;            //Adres skąd bierzemy dane
 DMA.CH3.DESTADDR0=(uint16_t)&USARTC0_DATA & 0xff;
 DMA.CH3.DESTADDR1=((uint16_t)&USARTC0_DATA) >> 8;
 DMA.CH3.DESTADDR2=0;           //Dane transmitujemy do rejestru nadajnika USART
 DMA.CH3.TRIGSRC=DMA_CH_TRIGSRC_USARTC0_DRE_gc;  //Transfer będzie wyzwalany przez wolne miejsce w buforze
 DMA.CH3.TRFCNT=size;             //Ile transmitujemy danych
 DMA.CH3.CTRLB=DMA_CH_TRNIF_bm;   //Skasuj flagę zakończenia transferu
 DMA.CH3.CTRLA=DMA_CH_ENABLE_bm | DMA_CH_SINGLE_bm | DMA_CH_BURSTLEN_1BYTE_gc;  //Odpal transfer - po jednym bajcie na żądanie 

Powyższa inicjalizacja może wydawać się zawiła, chociaż w istocie jest banalnie prosta – jeśli masz z powyższym kodem kłopot, to odsyłam do naszego kursu XMEGA, noty katalogowej Atmela lub mojej książki „AVR. Praktyczne projekty”, gdzie znajdziesz wiele przykładów użycia DMA.

I to wszystko – jak widzisz krótka funkcja załatwia wszystko, cała reszta funkcji jest taka sama jak w przykładzie z części II naszego minikursu.

Jak powyższy kod wykorzystać w praktyce? Najpierw musimy zamienić informacje o kolorach dla poszczególnych diod na postać nadającą się do wysłania przez UART:

uint8_t bufor[30*8];
for(uint8_t i=0; i<30; i++) WS2812B_transcodeGRB(i*256/30, 0, 0, &bufor[i*8]); 

W tym celu wykorzystana została znana nam już funkcja WS2812B_transcodeGRB. Tablica bufor zawiera teraz dane w postaci nadającej się do wysłania przez UART, pozostaje więc skonfigurowanie DMA:

DMA_init(bufor, sizeof(bufor));

I to wszystko! Od tego momentu to DMA dba o przesył danych do UART, w taki sposób, aby nadajnik UART miał zawsze co wysyłać – oczywiście do czasu aż prześle wszystkie dane. Dzięki temu rdzeń mikrokontrolera nie musi się już zajmować tymi czynnościami i mamy go w 100% do swojej dyspozycji.

Musimy tylko zwrócić uwagę na mały szczegół – DMA pracuje niezależnie od CPU, w efekcie po inicjalizacji tego układu sterowanie wraca do naszego programu, który działa całkowicie równolegle z przesyłem danych. Jeśli będziemy przesłać kolejną porcję danych, zajdzie potrzeba sprawdzenia, czy poprzedni transfer się zakończył.


Jak to zrobić? W tym celu kanał DMA dysponuje bitem TRNIF (Channel n Transaction Complete Interrupt Flag), który zostanie ustawiony po zakończeniu transferu danych. Stąd też jeśli nie mamy nic do zrobienia i chcemy po prostu zaczekać na koniec transferu danych to możemy posłużyć się następującą konstrukcją:

while (!(DMA.CH3.CTRLB & DMA_CH_TRNIF_bm));

Jak widzimy bit ten tak naprawdę jest flagą przerwania zakończenia transferu DMA. Stąd też zamiast czekać na zakończenie transferu, np. w celu inicjalizacji kolejnego możemy po prostu napisać funkcję obsługi przerwania końca transferu DMA. Dzięki temu mikrokontroler nie będzie tracił czasu na czekanie.

Ponieważ bit ten nie jest automatycznie kasowany, musimy to zrobić programowo, co odbywa się w naszej funkcji DMA_init.


Pliki do pobrania

Do pobrania pliki programu w środowisku Atmel Studio: WS2812B-DMA.ZIP (kopia)



Skalowanie

Warto się zastanowić, jaką liczbą diod możemy sterować? Producent układu WS2812B nie nakłada żadnych ograniczeń co do ilości diod jakie możemy połączyć w łańcuszek.

Ograniczenie wynika wyłącznie z wymaganej częstotliwości odświeżania. Jeśli wyświetlamy tylko statyczne obrazy to możemy w szereg połączyć praktycznie dowolną liczbę diod. Lecz jeśli chcemy wyświetlać obrazy dynamiczne… to sytuacja wygląda inaczej.

Transfer danych dla jednej diody trwa 24 bity razy 1200 ns na bit, czyli 28,8 µs. Jeśli będziemy chcieli wyświetlać obraz z częstotliwością odświeżania wynoszącą np. 20 Hz, to znaczy, że całą kompletną ramkę musimy przesłać w czasie 1/20 s, czyli 50 ms. Dzieląc ten czas przez czas potrzebny na transmisję danych dla jednej diody otrzymamy liczbę diod, jakie możemy umieścić w łańcuszku.

W naszym przykładzie będzie to 1736 diod. Pozornie dużo, ale jeśli chcemy zbudować duże matryce to ta liczba może okazać się zbyt mała. Co wtedy? Ano nic, użyliśmy XMEGI, do dyspozycji mamy 4 kanały DMA i co najmniej tyle samo interfejsów UART. Nic prostszego jak po prostu zrobić cztery niezależne łańcuszki po 1736 diod i ten sam sposób sterowania powielić.

W efekcie bez najmniejszych problemów będziemy mogli sterować prawie 7000 diod! W warunkach amatorskich, ze względu na cenę takiej ilości diod raczej więcej ich nie będziemy stosować :)

Podsumowanie

Po lekturze III części artykułu o XMEGA i WS2812B czas na pewne podsumowanie i tzw. words of wisdom :)

Zaczęliśmy od prostego programu, który co prawda poprawnie sterował diodami, ale zajmował 100% czasu procesora, co gorsze było to 100% czasu każdego użytego procesora, nawet jeśli byłby nim super mega, hiper wypaśny ARM.

Trochę kombinując, bez większego wysiłku udało się zaprząc do pracy układ peryferyjny jakim jest UART, dzięki czemu obniżyliśmy obciążenie CPU do ok. 15-20%, co więcej obciążenie będzie tym niższe im szybszy mikrokontroler zastosujemy.

Dzięki wykorzystaniu DMA udało nam się zejść z obciążeniem CPU praktycznie do 0%! Nieźle, prawda?

Powyższy przykład pokazuje, że warto poświęcić trochę czasu na przemyślenie problemu – zazwyczaj, oczywiste i najprostsze rozwiązanie nie jest najlepszym.

Niestety jak pokazuje Internet i znajdujące się w nim liczne przykłady użycia WS2812B większość osób zatrzymuje się na pierwszym etapie… szkoda.

Ale z powyższego przykładu możemy wysunąć jeszcze jeden, ważniejszy, wniosek. Właściwe i maksymalne użycie dostępnych układów peryferyjnych jest koniecznością w systemach embedded. A co znaczy dostępnych układów peryferyjnych?

Warto zauważyć, że to co mamy do dyspozycji zależy od nas, czyli ważne jest wybranie właściwego mikrokontrolera do realizacji problemu. Często o wiele ważniejsze niż szybkość taktowania, czy typ rdzenia, są odpowiednio elastyczne układy peryferyjne. Dzięki nim w wielu przypadkach to, co musielibyśmy realizować programowo i w efekcie obciążać rdzeń, co z kolei pociągałoby konieczność użycia odpowiednio szybkiego CPU, możemy zrobić całkowicie sprzętowo.

W swoich książkach o XMEGA pokazałem kilkadziesiąt różnych przykładów, w których poprawne użycie dostępnych peryferiów powoduje, że prosty, 8-bitowy mikrokontroler bez wysiłku generuje kolorowy obraz VGA lub TV, potrafi odtwarzać i nagrywać skompresowane pliki dźwiękowe, czy sterować matrycami składającymi się z tysięcy diod LED. Z tego punktu widzenia rdzeń mikrokontrolera jest po prostu kolejnym układem peryferyjnym – znaczy to, że także musimy go dobrać do realizowanego zadania.

Ktoś mógłby powiedzieć, chwila, łatwo mi mówić, jeśli użyłem XMEGA taktowanej zegarem 32 MHz. Warto jednak zastanowić się dlaczego XMEGA jest tak wysoko taktowana w pokazanych przykładach. Tylko w pierwszym z nich (przykładzie jak nie należy czegoś robić) wysokie taktowanie ułatwia nam realizację zadania. Ale w kolejnych – taktowanie na poziomie 16-32 MHz potrzebne jest nie ze względu na szybkość rdzenia, a na możliwość wygenerowania impulsów o odpowiednich reżimach czasowych.

Stąd też tak szybko taktowany musi być wyłącznie układ UART, taktowanie rdzenia możemy obniżyć, a w ostatnim przykładzie możemy wręcz go uśpić.

Warto to potestować obniżając taktowanie rdzenia za pomocą preskalerów. Są oczywiście inne możliwości realizacji pokazanej komunikacji.

Na koniec zasieję pewien niepokój – uciążliwością pokazanego rozwiązania jest konieczność wywołania funkcji transkodującej (przy okazji, pamiętacie o konkursie na jej ulepszenie?), a czy dałoby się zrobić tak, żeby ona w ogóle była niepotrzebna? Może wykorzystywane przez was mikrokontrolery mają coś co spowoduje, że jej nie będziemy potrzebować? A może jakiś prosty kawałek hardware rozwiąże problem?

Czekam na Wasze pomysły :-)

22 komentarze:

  1. DMA sprzężony z UART to wielki potencjał!
    Gdzie kupiłeś te diody? Jaka cena?

    OdpowiedzUsuń
    Odpowiedzi
    1. Kupiłem u Chińczyków na AliExpress. Nawet jeśli doliczą cło + VAT to i tak wychodzi prawie 2xtaniej niż w Polsce.

      Usuń
  2. Na jakiej wersji xmega testowałeś? Czy możesz załączyć cały projekt w Atmel Studio? Z góry dziękuję. Arek.

    OdpowiedzUsuń
    Odpowiedzi
    1. Na XMEGA256A3BU, ale będzie działać na każdej. Spakowany projekt w Atmel Studio dołączymy, nie ma go przez przeoczenie.

      Usuń
    2. Mea culpa :-)
      Pliki do pobrania dodałem pod koniec artykułu.

      Usuń
  3. while (!(DMA.CH3.CTRLB & DMA_CH_TRNIF_bm));

    Takie aktywne czekanie będzie skutkować 100% obciążeniem CPU. Niby nie ma nic do roboty w tym czasie, ale tak go zamęczać? Może usypiać choć na 50 ms :-)

    OdpowiedzUsuń
    Odpowiedzi
    1. Napisałem o tym w artykule. Czekamy, bo to tylko przykład i nie bardzo jest co w międzyczasie robić, można jak sugerujesz uśpić CPU.

      Usuń
  4. Witam serdecznie.
    Jak takie taśmy się podłącza do mikrokontrolera? Lutuje się, czy jakoś inaczej?

    OdpowiedzUsuń
    Odpowiedzi
    1. Jest to pokazane w I części - w skrócie wejście DI WS2812B łączysz z wyjściem TxD XMEGA.

      Usuń
  5. witam,

    coś nie mogę tego rozgryźć, więc proszę o małą pomoc :)
    Jaki powinien być wynik załączonego projektu po wgraniu na xmegę ?
    U mnie dzieje się coś takiego, że wyskakuje kilka zapalonych diód a po chwili wszystkie robią się pomarańczowe i tak już zostaje (choć widać efekt "drgania" światła). Losowe diody są zawsze inne, a etap "pomarańczowy" zawsze się powtarza.

    Wystarczy, że zmienię pin sterujący z xmegi na raspberry pi i tam odpalę jakiś program testowy to wszystko działa poprawnie, jednak na xmedze nie mogę sobie poradzić choć kombinowałem z tym kodem cały dzisiejszy dzień.
    Co powinienem sprawdzać / gdzie szukać błędu, kod, podłączenie diód ?
    używam ATxmega128A3U na płytce z leon-instruments.pl, diody to neopixel ring z adafruit - może to ma znaczenie ?

    OdpowiedzUsuń
    Odpowiedzi
    1. Powinien zmienić kolory 30-tu pierwszych diod, tak, że składowa R koloru jest w każdej inna. Wcześniejsze przykłady uruchomiłeś? Zrób wszystko po kolei, a znajdziesz błąd.

      Usuń
  6. witam
    bardzo dziękuję za fajny artykuł, a jednocześnie chciał bym się dowiedzieć jak wygenerować bardziej zaawansowane rzeczy do wyświetlania, jako że moje nieudolne próby wrzucenia pętli while nic nie dają (znaczy dają jakieś randomy)

    OdpowiedzUsuń
  7. a jak radzicie sobie z zasilaniem takiej ilości diód - pasek 5m to około 10A maksymalnie..

    OdpowiedzUsuń
    Odpowiedzi
    1. Zasilacz impulsowy, np. z PCta i wszystko ładnie działa. Przetestowane na 2 tys. diod.

      Usuń
  8. Kiedy można się spodziewać artykułu o 1-wire/USART ?


    Pozdrawiam,
    Tomek



    OdpowiedzUsuń
    Odpowiedzi
    1. Owszem. Czekamy aż Dondu upora się z problemami serwerowymi i pewnie w końcu to opublikuje :)

      Usuń
  9. Nie bawiłem się AtMegą. Czy DMA nie obciąża wcale CPU czy czasem kradnie cykle?

    OdpowiedzUsuń
    Odpowiedzi
    1. Oczywiście czasami kradnie cykle - w końcu magistrala dla CPU i DMA jest wspólna. Natomiast zaletą jest automatyzm działania, w efekcie więcej zyskuje się, bo nie trzeba aktywnie czekać na jakieś zdarzenie, np. koniec transferu bajtu przez wolny interfejs szeregowy.

      Usuń
  10. "uciążliwością pokazanego rozwiązania jest konieczność wywołania funkcji transkodującej, a czy dałoby się zrobić tak, żeby ona w ogóle była niepotrzebna?"
    Doczekamy się odpowiedzi?:]

    OdpowiedzUsuń
    Odpowiedzi
    1. Pewnie, opublikowałem ją tu:
      https://www.elektroda.pl/rtvforum/topic3411874.html

      Usuń
    2. Coś w tym wątku na elektrodzie jest nie tak - zaczyna się od postu #1 z filmem, ale następny ma nr #31, a gdzie najważniejsze???

      Usuń
  11. DMA_init(bufor, sizeof(bufor));
    nie działa, trzeba zmienić na:
    DMA_init(&bufor, sizeof(bufor));

    Jeszcze muszę ustalić czemu program zatrzymuje się na:
    while (!(DMA.CH3.CTRLB & DMA_CH_TRNIF_bm));
    i nie chce dalej ruszyć ;(

    OdpowiedzUsuń