Brak produktów
Mikrokontroler ATxmega128A3U wyposażony jest w cztery kanały DMA, pracujące niezależnie od siebie. 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.
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. Działanie programu 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, 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:
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, 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:
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:
To już wszystko! Dalej musi być tylko pusta pętla while(1), a cała transmisja zostanie zrealizowana sprzętowo.
Pliki do pobrania:
#include <avr/io.h> uint8_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.
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.
Debugowaniem sterują przyciski z górnego paska narzędzi. Warto nauczyć się ich skrótów klawiaturowych.
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:
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.