DMA – Bezpośredni dostęp do pamięci

Czas czytania: 7 min.

Spora część elektroników pamięta czasy, w których na rynku królowały proste, 8-bitowe mikrokontrolery z rodzin 8051 oraz AVR. Niesłabnąca popularność „małych” mikrokontrolerów (zwłaszcza drugiej z wymienionych grup) daje się stosunkowo łatwo wyjaśnić – wiele urządzeń elektronicznych nie wymaga dużej mocy obliczeniowej, zdolności szybkiego wykonywania skomplikowanych obliczeń zmiennoprzecinkowych czy bardzo zaawansowanych układów peryferyjnych. Jeżeli projektowany układ elektroniczny stawia konstruktorowi wyższe wymagania wydajnościowe, najczęściej sięga on po jeden z licznych, 32-bitowych (a nawet 64-bitowych) procesorów z rdzeniem ARM. Jednak wysoka częstotliwość taktowania, spora pamięć RAM czy Flash to nadal nie wszystko – w bardziej złożonych aplikacjach konieczne jest wsparcie rdzenia mikrokontrolera poprzez „zwolnienie go” z wykonywania operacji, które wiążą się z przesyłaniem dużych ilości danych – czy to w obszernych, jednolitych blokach, czy też w niewielkich porcjach, transferowanych z ogromną częstością. W zdecydowanej większości wymienionych przypadków z odsieczą przychodzi układ DMA – bezpośredni dostęp do pamięci. Z tego artykułu dowiesz się, jakie jest jego zadanie, jakie możliwości oferuje programiście oraz w jakich sytuacjach jest najczęściej wykorzystywany.

Praca bez DMA, czyli jak skutecznie stracić czas na najprostsze operacje

Nie od dziś wiadomo, że podział zadań pomiędzy poszczególnych pracowników firmy lub członków zespołu pozwala na efektywne wykorzystanie czasu i zasobów, dostępnych na potrzeby realizacji danego przedsięwzięcia. Nie inaczej jest w świecie mikrokontrolerów – wiele operacji, które mógłby realizować rdzeń procesora (a dokładniej jednostka artymetyczno-logiczna – ALU – będąca „mózgiem” całego układu), można niejako „zrzucić” na bloki peryferyjne. Załóżmy, że mikrokontroler ma za zadanie odbierać dane z jakiegoś urządzenia zewnętrznego (np. kamery) i zapisywać je w buforze, czyli przygotowanym do tego celu obszarze pamięci operacyjnej (RAM) – w celu dalszej obróbki, wyświetlenia czy też przesłania do innego podsystemu. Ciąg danych jest odbierany przez układ peryferyjny (np. kontroler SPI), a ponieważ każdy kolejny bajt trafia do tego samego rejestru pamięci, natychmiast po przesłaniu ostatniego bitu trzeba zapisać odebraną wartość w odpowiednim miejscu bufora, zwalniając interfejs w celu umożliwienia odbioru kolejnego bajtu. Każdy bajt jest zapisywany w kolejnej komórce bufora, stąd pojawia się konieczność pilnowania, aby żadna część danych nie została przypadkowo nadpisana nową wartością – dlatego każdorazowo po zapisaniu nowego bajtu trzeba inkrementować (zwiększać) zmienną, przechowującą adres (wskaźnik) do kolejnej wolnej komórki bufora. Po tym należy rozpocząć oczekiwanie na odebranie kolejnego bajtu danych i powtórzyć całą procedurę. Jeżeli dane przychodzą z dużą częstotliwością (co w przypadku kamery jest oczywiste), może zdarzyć się sytuacja, w której czas procesora będzie wykorzystany w tak dużej części, że nie wystarczy go już na wykonywanie innych operacji (np. przetwarzanie obrazu lub przesłanie go do wyświetlacza). Identyczna sytuacja pojawia się w aplikacjach realizujących odbiór, odtwarzanie czy obróbkę dźwięku, danych pomiarowych, a nawet transmisji plików.

Dla jeszcze lepszego zobrazowania sytuacji wyobraź sobie przypadek prostego generatora sygnałów, zrealizowanego na bazie mikrokontrolera. Przypuśćmy, że chcesz generować sygnał sinusoidalny, trójkątny lub dowolny inny przebieg z użyciem (wbudowanego lub zewnętrznego) przetwornika cyfrowo-analogowego (DAC). Próbki są zapisane w pamięci RAM lub Flash, a w celu wygenerowania odpowiedniego przebiegu napięciowego konieczne jest kolejne ich przesyłanie do przetwornika DAC; po dojściu do końca tablicy odtwarzanie powinno rozpocząć się od początku. Załóżmy w tym miejscu, że nie korzystamy z bardziej zaawansowanych metod wytwarzania sygnałów (tj. cyfrowej syntezy DDS), ale w zamian chcemy obciążyć procesor kilkoma dodatkowymi zadaniami – w tym obsługą wyświetlacza, pokazującego rodzaj i częstotliwość sygnału oraz klawiatury, służącej do ustawiania parametrów pracy urządzenia. Ponieważ dane muszą być generowane w sposób ciągły, procesor ma bardzo mało czasu (jeżeli w ogóle znalazłby go w natłoku zadań związanych z obsługą przetwornika DAC) na odczytywanie stanu klawiszy czy przesyłanie komend do wyświetlacza. W tym przypadku mamy do czynienia z sytuacją niejako odwrotną do przedstawionej w poprzednim przykładzie: dane są przesyłane z pamięci do układu peryferyjnego.

Kontroler DMA – silne wsparcie w wymagających aplikacjach

Najprościej rzecz ujmując:
Zadaniem kontrolera DMA jest przesyłanie danych ze wskazanej lokalizacji źródłowej (rejestru peryferyjnego lub miejsca w pamięci) do określonego miejsca docelowego (które także może być rejestrem lub obszarem pamięci) w taki sposób, aby informacje były przekazywane bezbłędnie i niejako automatycznie, bez konieczności angażowania mocy obliczeniowej procesora. Zadaniem programu jest zatem właściwa konfiguracja i uruchomienie bloku DMA.

Dodatkowo, jeżeli transfer angażuje także inne bloki peryferyjne (np. interfejs SPI, I2C, UART czy I2S, przetworniki analogowo-cyfrowe czy cyfrowo-analogowe, a nawet timery), konieczne jest „podpięcie” ich do kontrolera DMA, tj. właściwe ustawienie „ścieżek”, jakimi będą przesyłane dane oraz sygnały synchronizacji i wyzwalania transferów. Warto bowiem wiedzieć, że DMA (w zależności od sytuacji oraz wyboru określonych bloków) może pracować w trybie pojedynczego transferu lub transferów cyklicznych. W tym pierwszym przypadku, kontroler – po przesłaniu określonej ilości danych z miejsca źródłowego do lokalizacji docelowej – przerywa swoje działanie i pozostaje w gotowości do kolejnej operacji, wywołanej programowo lub przez określone zdarzenie sprzętowe (np. sygnał z timera). W trybie cyklicznym DMA kontynuuje transfer danych po przesłaniu zadanej liczby bajtów lub słów, rozpoczynając kolejną operację od początku bufora. Dzięki temu użytkownik nie musi martwić się o ponowne rozpoczęcie transferu, a tryb ten jest szczególnie chętnie wykorzystywany w sytuacjach wymagających ciągłej transmisji danych – np. w przetwarzaniu dźwięku cyfrowego.

Budowa i połączenia bloku DMA

Aby lepiej zobrazować budowę kontrolera DMA i wskazać praktyczne implikacje, jakie z niej wypływają, posłużymy się jako przykładem implementacją DMA, zastosowaną w mikrokontrolerach STM32 z rodziny F0. Są to stosunkowo niewielkie, ale wydajne i uniwersalne mikrokontrolery 32-bitowe z rdzeniem ARM Cortex M0. Warto zapoznać się tutaj ze schematem przedstawiający uproszczoną architekturę mikrokontrolera oraz DMA. Zarówno sam rdzeń Cortex-M0, jak i DMA, są podłączone do tej samej macierzy (Bus matrix), niejako „spinającej” wszystkie bloki główne oraz peryferyjne – w tym pamięci Flash i RAM, szyny do obsługi portów GPIO oraz most (Bridge), obsługujący wszystkie pozostałe peryferia.

Z opisanych połączeń wynika pierwsza, bardzo ważna cecha DMA – kontroler może uzyskiwać dostęp do pamięci oraz peryferiów niezależnie od pracy rdzenia (czyli – niejako „równolegle” do aktualnie wykonywanych instrukcji, zawartych w programie). Oczywiście, nie ma nic za darmo – w momencie, gdy DMA uzyskuje dostęp np. do pamięci Flash lub kontrolera UART, rdzeń procesora musi poczekać na moment, gdy dany zasób będzie wolny. Dopiero wtedy rdzeń może uzyskać dostęp i wykonać zadane operacje. Taka forma działania, w której dostęp do zasobów mikrokontrolera jest przełączany pomiędzy DMA oraz rdzeniem, pozwala na uzyskanie znacznych oszczędności czasowych – procesor nie musi samodzielnie realizować wszystkich procedur przesyłania danych, gdyż – za cenę chwilowego „oddania” kontrolerowi DMA dostępu do pamięci i rejestrów peryferyjnych – zostaje zwolniony z konieczności samodzielnego realizowania każdej, elementarnej operacji odczytu i zapisu nawet na sporych blokach danych.

Zwróć teraz uwagę na schemat samego bloku DMA – zawiera on określoną liczbę kanałów (tutaj jest ich pięć, natomiast w dużych mikrokontrolerach wielokrotnie więcej), które poprzez wspólny „przełącznik” są doprowadzone do szyny, łączącej DMA z macierzą. Ponadto kontroler zawiera w swojej strukturze blok, określany jako arbiter – jego zadaniem jest decydowanie, który z kanałów DMA ma w danym momencie dostęp do zasobów mikrokontrolera. Ale czym dokładnie są kanały DMA i jakie dają możliwości programiście?

Kanały, priorytety i multipleksing, czyli jak uniknąć zderzenia dwóch pociągów na tym samym torze?

Powiedzieliśmy już, że kontroler DMA umożliwia dostęp do różnych zasobów procesora bez konieczności angażowania rdzenia w najprostsze operacje zapisu i odczytu. Każdy kanał DMA, który ma być zaprzęgnięty do pracy w projektowanej aplikacji, musi być uprzednio właściwie skonfigurowany. Zapewne intuicyjnie domyślasz się już, że każdy z nich może pracować niezależnie od pozostałych, realizując nawet zupełnie różne funkcje. Użycie kanałów (zamiast jednego, „wielkiego” kontrolera) jest pierwszym sposobem na uniknięcie konfliktu podczas dostępu do poszczególnych zasobów procesora. Możesz wyobrazić sobie w dużym uproszczeniu, że kanały DMA stanowią osobne, równoległe torowiska na dużym dworcu kolejowym. Po każdym torze, w tym samym momencie, może przejeżdżać inny pociąg – w dowolną stronę i z dowolną prędkością (oczywiście, w granicach swoich możliwości technicznych).

Co jednak zrobić, gdy dwa pociągi muszą skorzystać z tego samego peronu, a nawet toru? Oczywistym (i jedynym możliwym) wyjściem jest rzecz jasna zatrzymanie drugiego ze składów na czas przejazdu pierwszego, a – po zwolnieniu danego toru – wprowadzenie nań pierwszego pojazdu. W ten sposób, kosztem nieuniknionego (choć możliwie najkrótszego) przestoju, obydwa składy mogą bezpiecznie przejechać określony odcinek. Ruchem na stalowych szynach kolejowych odpowiada dróżnik, którego zadaniem jest ustalenie pierwszeństwa danego pociągu, albo – mówiąc szerzej – kolejności przejazdu wszystkich pociągów oczekujących na dostęp do zawiadywanego przezeń obszaru. W świecie mikrokontrolerów też używamy pojęcia szyn – np. szyna adresowa i szyna danych – a za przydzielanie dostępu do nich odpowiednim peryferiom odpowiada tzw. arbiter. Jest to wyspecjalizowana część kontrolera DMA, która na podstawie ustalonych przez programistę priorytetów „dopuszcza” do danego zasobu wykorzystujące go bloki peryferyjne lub pamięć.

Konfiguracja DMA

Masz już podstawowe informacje na temat sposobu działania oraz najczęściej stosowanych scenariuszy wykorzystania kontrolera DMA. Zbierzmy zatem informacje na temat podstawowych parametrów, które trzeba skonfigurować (poprzez zapis odpowiednich rejestrów), aby móc skorzystać z dobrodziejstw naszego kontrolera. Dla lepszego zobrazowania przedstawianego tematu, posłużymy się znów przykładem bloku DMA, oferowanego przez mikrokontrolery z rodziny STM32F0 (pełen opis znajdziesz w dokumentacji firmy ST – w tym przypadku jest to dokument nr RM0360).

Kontroler DMA musi „wiedzieć”, skąd należy pobierać dane oraz do jakiej lokalizacji docelowej je przesyłać. W tym celu konieczne jest ustawienie adresów – w językach C lub C++ do rejestrów adresowych DMA należy wpisać odpowiednie wskaźniki. Ponieważ każdy kanał może pracować na innych zasobach, dla każdego z nich jest przewidziana osobna para rejestrów adresowych: DMA_CPARx (gdzie x- numer kanału) oraz DMA_CMARx. Niezbędne jest określenie, czy każdy kolejny transfer danych ma odbywać się z użyciem tego samego rejestru (lub elementu bufora w pamięci), czy też do kolejnego – w tym drugim przypadku należy w rejestrze konfiguracyjnym DMA_CCRx ustawić bit MINC (jeżeli adres bufora wpisano do rejestru DMA_CMARx) lub PINC (jeżeli transfery mają się odbywać do/z kolejnych rejestrów lub komórek, począwszy od adresu wpisanego do DMA_CPARx).

Ponieważ jednak dane mogą mieć różną długość (jednego bajtu – 8 bitów, słowa 16-bitowego lub 32-bitowego), konieczne jest także ustalenie rozmiaru pojedynczej danej w obu lokalizacjach osobno – do tego służą bity MSIZE i PSIZE, również należące do rejestru DMA_CCRx. Ten sam rejestr zawiera także bity określające priorytet (bity PL0 oraz PL1), tryb pracy – pojedynczy lub cykliczny (bit CIRC), a także kierunek transferu pomiędzy obydwiema lokalizacjami (DIR). Liczbę transferów do wykonania po uruchomieniu DMA (lub w jednym cyklu, jeżeli włączony jest tryb cykliczny) określa zawartość rejestru DMA_CNDTRx. Ponadto, wspomniany wcześniej rejestr DMA_CCRx zawiera bity pozwalające na włączenie (zezwolenie na wywoływanie) przerwań, informujących procesor o końcu transmisji (bit TCIE), osiągnięciu połowy liczby zadanych transferów (HTIE) czy też o wystąpieniu błędu (TEIE). Uruchomienie DMA musi oczywiście także być wykonane przez program – w tym celu należy ustawić bit EN. Warto dodać, że – niejako wbrew nazwom – DMA w mikrokontrolerach STM32 (i nie tylko) pozwala także na wygodną realizację przesyłu danych pomiędzy dwiema różnymi lokalizacjami pamięci – w tym celu należy ustawić bit MEM2MEM (również w rejestrze DMA_CCRx) – wtedy obydwa rejestry adresowe DMA_CMAR i DMA_CPAR powinny oczywiście zawierać wskaźniki na zmienne lub tablice, znajdujące się w którejś z dostępnych pamięci.

Niezwykle ważne jest – oprócz ustawienia opisanych powyżej rejestrów – także odpowiednie skonfigurowanie bloków peryferyjnych, które mają współpracować z DMA. Przykładowo, jeżeli dane odbierane przez UART mają być zapisywane do bufora w pamięci RAM, to w trzecim rejestrze konfiguracyjnym kontrolera interfejsu szeregowego (USART_CR3) należy ustawić bit DMAR. Jeżeli transfer ma się odbywać z pamięci do interfejsu UART, należy ustawić bit DMAT – przy czym warto dodać, że dzięki rozdzieleniu kanałów możliwe jest jednoczesne wykorzystywanie DMA zarówno do nadawania, jak i odbioru danych.

DMA w dużych mikrokontrolerach

Dla uproszczenia opisu posłużyliśmy się przykładem jednego z relatywnie prostszych mikrokontrolerów, należących do ogromnej rodziny STM32. Warto wiedzieć, że w przypadku naprawdę sporych układów z rdzeniem ARM kontroler DMA może umożliwiać znacznie większą liczbę kombinacji adresowych dzięki zastosowaniu – oprócz kanałów – także tzw. strumieni (ang. stream). Niektóre mikrokontrolery mają w swojej strukturze dwa osobne kontrolery DMA, zaś najwięksi członkowie rodziny STM32 oferują ponadto specjalizowany kontroler Chrom-Art Accelerator(TM), oznaczany skrótem DMA2D. Przyrostek „2D” jest nieprzypadkowy – blok ten służy bowiem do obsługi ramek obrazu, a jego przeznaczeniem jest uproszczenie i przyspieszenie powtarzalnych, prostych, ale jednocześnie czasochłonnych operacji, związanych z wyświetlaniem grafiki – np. wypełniania obrazu określonym kolorem czy obsługę przenikania i przezroczystości.

Podsumowanie

W artykule przedstawiliśmy, do czego służy kontroler DMA, jakie pełni funkcje w praktycznych aplikacjach oraz jakie kroki należy wykonać w celu zaprzęgnięcia go do pracy. Warto o tym pamiętać podczas pisania programów na bardziej zaawansowanych mikrokontrolerach, gdyż wykorzystanie kontrolera bezpośredniego dostępu do pamięci pozwala nie tylko znacznie uprościć pewne powtarzalne operacje na, niejednokrotnie obszernych, zbiorach danych, ale także umożliwia znaczne odciążenie procesora. W wielu zastosowaniach odzyskana w ten sposób moc obliczeniowa umożliwia sprawniejsze i bardziej niezawodne realizowanie wielu ważnych zadań, niejednokrotnie krytycznych pod względem czasu wykonywania programu.

Dodaj komentarz