Mikrokontrolery AVR XMEGA w praktyce, część 24. DMA i tablice
Konfiguracja DMA
W jednej chwili może pracować tylko jeden kanał o najwyższym priorytecie, a praca pozostałych jest zawieszana. Każda operacja kopiowania danych przez DMA w dokumentacji procesora nazywana jest transakcją. Transakcję można podzielić na poszczególne bloki, a te dzielą się na transfery burst. Jest to spowodowane koniecznością przerwania pracy układu DMA, kiedy do pamięci dostęp chce uzyskać procesor.
Burst jest najbardziej elementarną częścią transferu i może mieć długość 1, 2, 4 lub 8 bajtów. Jest to fragment transmisji, której nie można przerwać – jeśli procesor będzie chciał uzyskać dostęp do pamięci podczas transmisji burst, będzie mógł to zrobić dopiero po jej zakończeniu. Nie ma sensu ustalać długości burst większej niż rzeczywiście potrzebna, ponieważ może to niepotrzebnie blokować procesor i obniżać jego wydajność, zamiast ją podwyższać. Powinniśmy wybrać taką długość transmisji burst, jaką maja typ kopiowanych danych. Tzn. dla danych tekstowych ASCII, przechowywanych w zmiennych 8-bitowych typu char lub uint8_t, powinniśmy wybrać burst o długości 1 bajta. W przypadku kopiowania danych z przetwornika cyfrowo-analogowego, przechowywanych w dwóch rejestrach 8-bitowych, powinniśmy wybrać burst 2-bajtowy.
Blok jest jednym burstem lub określoną ilością transmisji burst następujących po sobie. Transmisja bloku może zostać zainicjalizowana automatycznie po wystąpieniu odpowiedniego wyzwalacza, pochodzącego z układu USART, SPI lub przetwornika analogowego. W obrębie bloku można przesłać maksymalnie 65536 bajtów.
Transakcja jest zbiorem bloków, obojętnie czy przesyłanych bezpośrednio po sobie, czy z jakimiś przerwami. Istotne jest to, że ustawienia układu DMA podczas wykonywania transakcji są stałe i nie mogą się zmieniać. Transakcja może składać się z 256 bloków, co przy maksymalnym rozmiarze bloku oznacza, że DMA może przesłać w jednej transakcji nawet 16MB.
Może się to wydawać trochę zagmatwane, jednak odrobina ćwiczeń i praktyki rozwieje wątpliwości. Praktyczne przykłady, które omówimy w tym kursie, zilustrują zastosowanie poszczególnych ustawień układu DMA.
Przesyłanie tablicy
W pierwszym przykładzie napiszemy bardzo prosty program, w którym będą dwie tablice, a korzystając z DMA, skopiujemy zawartość jednej tablicy do drugiej. Tablica źródłowa source[] będzie miała 10 elementów, a tablica docelowa dest[] będzie składać się z 15 elementów. Żeby przykład nie był zbyt trywialny, podczas kopiowania odwrócimy kolejność danych. Kod programu przedstawia listing 1, a jego działanie będziemy mogli zaobserwować przy pomocy debugera JTAG albo symulatora. Przy okazji poznamy kilka ciekawych opcji Atmel Studio, pozwalających na zaglądanie do wnętrza procesora podczas pracy, dzięki czemu można na własne oczy zobaczyć pracę programu, co pozwala łatwo i szybko znaleźć błędy. Jeśli nie masz JTAG – to jeszcze nie problem! Program możesz przetestować w symulatorze Atmel Studio. Mimo to, polecam zaopatrzenie są w programator JTAG, np. AVR Dragon (link do sklepu), który jest warty swojej ceny. Czas zaoszczędzony na szukaniu błędów bardzo szybko rekompensuje cenę programatora JTAG.
Aby skorzystać z dobrodziejstw DMA, musimy najpierw ustawić jego kontroler, a dopiero potem poszczególne kanały. Ustawienie kontrolera jest bardzo proste i polega na wpisaniu do rejestru DMA.CTRL odpowiednich wartości:
- DMA_ENABLE_bm – włączenie kontrolera DMA
- DMA_DBUFMODE_xxx_gc – grupa konfiguracyjna odpowiadająca za konfigurację podwójnego buforowania. Za xxx można wpisać DISABLED, CH01, CH23 lub CH01CH23. W naszych przykładach z podwójnego buforowania korzystać nie będziemy, więc wybieramy opcję DISABLED
- DMA_PRIMODE_xxx_gc – grupa konfiguracyjna ustalająca priorytety kanałów. Domyślnie wszystkie kanały mają równy priorytet, a który z nich ma mieć pierwszeństwo w przypadku jednoczesnej pracy, określa algorytm Round Robin. Oznacza to, że ten, który ostatnio był używany, trafia na koniec kolejki i musi czekać aż pozostałe kanały zakończą pracę, po czym cykl się powtarza. Tryb taki uzyskujemy po wpisaniu RR0123 w miejsce xxx. Możliwe jest, by wybrane kanały o najniższych numerach miały wyższy priorytet. Po wpisaniu CH0RR123 kanał 0 będzie miał priorytet najwyższy, natomiast 1, 2 i 3 będą działać na zasadzie Round Robin. Możliwe jest też ustawienie CH01RR23 lub CH0123, gdzie nie ma Round Robin, a priorytety kanałów ustawione są na sztywno.
Następnie przechodzimy do konfiguracji kanału 0 w rejestrach DMA.CH0, gdzie musimy ustalić adresy tablicy źródłowej i docelowej. Magistrala adresowa w XMEGA ma szerokość 24 bitów, zatem adresy musimy wpisywać do trzech rejestrów, przechowujących adres źródła danych: SRCADDR0, 1, 2, oraz do trzech rejestrów przechowujących adres docelowy: DESTADDR0, 1, 2. Sposób wpisywania adresów jest dość fikuśny. Jeśli chcemy podać adres początku tablicy, wystarczy wpisać jej nazwę bez nawiasów klamrowych. W przypadku zwykłej zmiennej, rejestru lub innego elementu tablicy niż pierwszy, musimy posłużyć się operatorem pobrania adresu &. W obu przypadkach adres musimy przerzutować na zmienną 16-bitową i na końcu wyciągnąć z niej młodszy i starszy bajt. Najlepiej będzie spojrzeć na kod programu na listingu 1, gdzie zostało to przedstawione. Dobrze jest po prostu zapamiętać pewien szablon kodu, w którym podajemy adresy celu i źródła dla DMA.
DMA.CH0.SRCADDR0 = (uint16_t)source & 0xFF; // adres źródła DMA.CH0.SRCADDR1 = (uint16_t)source >> 8; DMA.CH0.SRCADDR2 = 0; DMA.CH0.DESTADDR0 = (uint16_t)&dest[12] & 0xFF; // adres celu DMA.CH0.DESTADDR1 = (uint16_t)&dest[12] >> 8; DMA.CH0.DESTADDR2 = 0;
Kiedy chcemy podać początek tablicy, wystarczy wpisać jej nazwę bez żadnych nawiasów ani innych ozdobników. Chcemy jednak, by kolejność danych została odwrócona, tak więc musimy zapisywać tablicę od tyłu – w naszym przykładzie będzie to od 12 elementu, dlatego w kodzie programu do rejestrów DESTADDR wpisujemy adres &dest [12] (jest to de facto trzynasty element, ponieważ w C elementy numeruje się od zera, zatem nasza 15-elementowa tablica ma elementy o numerach 0-14).
Kolejnym krokiem jest określenie, ile bajtów zamierzamy przesłać i wpisać tę wartość do rejestru DMA.CH0.TRFCNT. Warto tutaj się posłużyć operatorem sizeof() i jako argument podać nazwę tablicy (uwaga – choć sizeof() wygląda jak funkcja, w rzeczywistości jest to operator działający na etapie kompilacji programu; użycie sizeof() nie jest możliwe w przypadku tablic o zmiennym rozmiarze z dynamiczną alokacją pamięci).
W rejestrze DMA.CH0.ADDRCTRL musimy ustalić, w jakim kierunku będą kopiowane dane. Możliwe są trzy opcje:
- Dane kopiowane są zawsze z/do tej samej komórki pamięci. Ma to zastosowanie, kiedy DMA współpracuje z jakimś układem peryferyjnym – wpisujemy DMA_CH_SRCDIR_FIXED_gc.
- Adres źródłowy/docelowy zwiększa się po każdym przesłanym bajcie. W ten sposób kopiujemy dane od początku do końca – DMA_CH_SRCDIR_INC_gc.
- Adres źródłowy/dolecowy zmniejsza się po każdym bajcie. Przez to możemy tablicę zapisywać od końca do początku – DMA_CH_SRCDIR_DEC_gc.
Nic nie stoi na przeszkodzie, by tablicę źródłową odczytywać od początku, a docelową zapisywać od końca. W tym samym rejestrze określamy też, kiedy ma nastąpić przeładowanie rejestrów adresowych, tzn. przywrócenie wartości początkowej. Ponieważ w tym przykładzie interesuje nas pojedyncza transakcja, nie będziemy korzystać z możliwości przeładowania.
Ostatnim rejestrem jest DMA.CH0.CTRLA do którego wpisujemy odpowiednio:
- DMA_CH_ENABLE_bm – uruchomienie kanału,
- DMA_CH_TRFREQ_bm – ustawienie tego bitu powoduje uaktywnienie transmisji, jeśli nie wybraliśmy wcześniej automatycznego wyzwalacza. W ten sposób można programowo sterować układem DMA, kiedy ma się rozpocząć kopiowanie,
- DMA_CH_BURSTLEN_xBYTE_gc – ustalamy długość transmisji burst, na 1, 2, 4 lub 8 bajtów. W przypadku, kiedy nasze tablice przechowują zmienne 8-bitowe, więc wybieramy burst o wielkości 1 bajtu.
To już wszystko! Dalej musi być tylko pusta pętla while(1), a cała transmisja zostanie zrealizowana sprzętowo.
List. 1. Kod programu do pierwszego ćwiczenia
#includeuint8_t source[10] = {10,11,12,13,14,15,16,17,18,19}; uint8_t dest[15]; int main(void) { // konfiguracja kontrolera DMA DMA.CTRL = DMA_ENABLE_bm| // włączenie kontrolera DMA_DBUFMODE_DISABLED_gc| // bez podwójnego buforowania DMA_PRIMODE_RR0123_gc; // wszystkie kanały równy priorytet // konfiguracja kanału DMA DMA.CH0.SRCADDR0 = (uint16_t)source & 0xFF; // adres źródła DMA.CH0.SRCADDR1 = (uint16_t)source >> 8; DMA.CH0.SRCADDR2 = 0; DMA.CH0.DESTADDR0 = (uint16_t)&dest[12] & 0xFF; // adres celu DMA.CH0.DESTADDR1 = (uint16_t)&dest[12] >> 8; DMA.CH0.DESTADDR2 = 0; DMA.CH0.TRFCNT = sizeof(source); // rozmiar tablicy DMA.CH0.ADDRCTRL = DMA_CH_SRCRELOAD_NONE_gc| // przeładowanie adresu źródła po zakończeniu bloku DMA_CH_SRCDIR_INC_gc| // zwiększanie adresu źródła po każdym bajcie DMA_CH_DESTRELOAD_NONE_gc| // przeładowanie adresu celu po zakończeniu bloku DMA_CH_DESTDIR_DEC_gc; // zmniejszenie adresu celu po każdym bajcie DMA.CH0.CTRLA = DMA_CH_ENABLE_bm| // włączenie kanału DMA_CH_TRFREQ_bm| // uruchomienie transmisji DMA_CH_BURSTLEN_1BYTE_gc; // burst = 1 bajt // pusta pętla główna while(1) {} }
Przetestujemy działanie programu przy pomocy programatora JTAG lub poprzez symulator wbudowany w Atmel Studio. Przy próbie uruchomienia debugowania, po wciśnięciu klawisza F5, powinien pojawić się komunikat, że nie wybrano programatora. Jeśli takie okienko się nie pojawiło, wybieramy z menu Project > Properties i w zakładce Tools wybieramy Simulator lub posiadany przez Ciebie programator JTAG (ja wybrałem AVR Dragon). W przypadku programatorów, trzeba jeszcze wybrać, poprzez który interfejs procesor ma być połączony. Wybieramy oczywiście JTAG. Przedstawiono to na rysunku 1.
Rys. 1. Ustawienia programatora JTAG
Choć w przypadku tak prostego programu nie ma to znaczenia, to warto wyrobić sobie zwyczaj dostosowywania optymalizacji kodu do naszych wymagań. W tym samym oknie, otwórz zakładkę Toolchain, następnie z drzewka wybierz AVR/GNU C Compiler, a potem Optimalization. Domyślnie włączona jest opcja –O1, stanowiąca kompromis pomiędzy wielkością kodu wynikowego a szybkością działania programu. Kiedy zależy nam na oszczędzaniu miejsca warto wybrać opcję –Os. Optymalizator zastosuje różne sztuczki, aby kod wynikowy był jak najbardziej zwarty. Do debugowania przez JTAG warto jednak wyłączyć wszelkie optymalizacje, gdyż optymalizator potrafi pozmieniać kolejność wykonywania linii, co może nas niepotrzebnie mylić. Wybieramy więc opcję –O0. Kod programu będzie wtedy relatywnie duży. Oczywiście kiedy zakończymy debugowanie i będziemy chcieli uzyskać końcową wersję programu, możemy wtedy zmienić poziom optymalizacji na inny. Właściwe ustawienia pokazano na rysunku 2.
Rys. 2. Wybór poziomu optymalizacji kodu
Debugowaniem sterują przyciski z górnego paska narzędzi, opisane na rysunku 3. Warto nauczyć się ich skrótów klawiaturowych.
Rys. 3. Przyciski sterujące pracą krokową programu
Aby zobaczyć na żywo, jak DMA kopiuje poszczególne komórki, wystartujmy program z natychmiastowym zatrzymaniem go w pierwszej linijce. Aby to uczynić, kliknij przycisk Rozpocznij i zatrzymaj lub naciśnij Alt-F5. Aktualnie wykonywana linijka zostanie podświetlona na żółto, a po prawej stronie pojawią się dodatkowe okna:
- Processor – stan najważniejszych rejestrów procesora, wskaźnik stosu, licznik programu, rejestr statusowy i rejestry robocze R0-R31,
- IO View – pozwala podglądać i modyfikować rejestry wszystkich układów peryferyjnych,
- Call stack – podgląd stosu i wywołania poszczególnych funkcji, wywołujących kolejne funkcje,
- Memory – podgląd pamięci RAM, Flash, EEPROM,
- Watch – podgląd wybranych zmiennych.
Oprócz tego, dostępnych jest jeszcze całe mnóstwo narzędzi ułatwiających debugowanie i monitorowanie pracy procesora – nie będę ich tu opisywał, ponieważ jest to temat na osobny odcinek (albo i dwa).
Aby widzieć zawartość tablic source[] oraz dest[], musimy kliknąć je prawym przyciskiem myszy, a następnie wybrać opcję Add to watch. Po prawej stronie pokażą nam się tabele source[], wypełnioną liczbami od 10 do 19 oraz dest[], która jest wypełniona zerami.
Wciskaj klawisz F11, aby przejść przez kolejne linie programu, aż do pustej pętli głównej. Możesz wtedy poćwiczyć korzystanie z IO View – obserwuj jak ustawiają się poszczególne bity w rejestrach kontrolera DMA.
Kiedy dojdziesz do pętli głównej, wróć do Watch i obserwuj tablicę dest[], wciskając klawisz F11. Kontroler DMA zaczyna działać i rozpoczyna kopiowanie od elementu zerowego tablicy źródłowej, który trafia do elementu dwunastego. W ten sposób zapełnia się tabela aż do elementu trzeciego, kiedy to kopiowanie zostaje zakończone. Wynik programu przedstawiono na rysunku 4.
Rys 4. Wynik działania pierwszego programu demonstrującego pracę DMA
Dystrybutorem zestawu X3-DIL64 jest KAMAMI.pl. |
Dominik Leon Bieczyński
http://leon-instruments.blogspot.com