Uwaga! Ten kurs został zarchiwizowany. Sprawdź najnowszy kurs STM32 »
Nazwa DMA jest skrótem od angielskiego Direct Memory Access. Mówiąc najprościej DMA jest modułem którym ma bezpośredni dostęp do pamięci RAM. Dzięki temu potrafi wyręczyć procesor w przesyłaniu danych między obszarami pamięci lub pamięcią, a układami peryferyjnymi.
Dlaczego jest to takie ważne? Dotychczas poznaliśmy kilka modułów peryferyjnych (np. ADC). Gdy chcieliśmy odczytać dane z czujników mikrokontroler musiał odpytać moduł peryferyjny (odczytać zawartość odpowiedniego rejestru), a następnie wynik skopiować do zmiennej (czyli pamięci RAM).
Okazuje się, że takie działanie jest dość czasochłonne!
Moduły peryferyjne działają często znacznie wolniej niż sam procesor, więc dużo czasu jest marnowane na oczekiwanie. Można wykorzystywać przerwania do odczytu danych, jednak takie rozwiązanie jest również czasochłonne – na każdy odebrany bajt procesor musi wykonać procedurę obsługi przerwania.
Wymyślono więc układ, który potrafi odciążyć procesor. Procesor konfiguruje moduł DMA, podaje skąd dane mają być odczytane oraz gdzie zapisane, a później może zająć się zupełnie innymi zadaniami.
Gdy wszystko będzie gotowe DMA powiadomi procesor o zakończeniu pracy za pomocą odpowiedniej flagi lub przerwania.
Analogia DMA w życiu codziennym
Nie każdy musi od razu rozumieć, jak działa DMA i w czym tkwi jego przewaga. Stąd pora na analogię do bardziej życiowego przykładu. Z góry zaznaczamy, że nie musi ona przemówić do wszystkich czytelników.
Wyobraźmy sobie okienko pocztowe z długą kolejką klientów. Urzędnik obsługuje po kolei każdego klienta. Jednak między jednym, a drugim musi udać się do innego pomieszczenia i przenieść paczkę z samochodu na regał. Dopiero wtedy może wrócić do biurka i zająć się sprawami innej osoby. Rozwiązania działa, ale jest powolne, bo tracimy czas wykonując zbędną pracę.
Natomiast w wersji z DMA moglibyśmy zatrudnić drugiego pracownika, który będzie działał w tle (innym pomieszczeniu). To on będzie za nas przenosił paczki. Dzięki temu pracownik przy okienku zyskuje czas, może przeznaczyć go na coś innego - przykładowo na szybszą obsługę reszty osób. Chociaż akurat w naszym, analogiczym przykładzie urzędnik stwierdził, że zaoszczędzony czas przeznaczy na krótką drzemkę.
Na tym koniec naszej dalekiej od elektroniki analogii. Mam nadzieję, że niektórym z Was pomoże ona zrozumieć zalety stosowania DMA (odciążenie procesora). Pora na testy w praktyce!
Gotowe zestawy do kursów Forbota
Komplet elementów Gwarancja pomocy Wysyłka w 24h
Zestaw ponad 120 elementów do przeprowadzenia wszystkich ćwiczeń z kursu można nabyć u naszych dystrybutorów! Dostępne są wersje z płytką Nucleo lub bez niej!
Masz już zestaw? Zarejestruj go wykorzystując dołączony do niego kod. Szczegóły »
Kopiowanie bloków pamięci dzięki DMA
Na początek zapoznajmy się z nieco prostszą funkcją DMA jaką jest kopiowanie bloków pamięci. W wielu programach konieczne jest czasami kopiowanie dużych bloków danych, np. buforów ekranu, czy odebranych informacji.
Możemy do tego celu wykorzystać funkcję memcpy() lub napisać prostą pętlę:
C
1
2
for(i=0;i<BUFFER_SIZE;i++)
dst_buffer[i]=src_buffer[i];
Ponieważ mikrokontrolery mają relatywnie mało pamięci wykorzystywanie DMA jest tutaj trochę na wyrost, jednak spróbujmy porównać czasy wykonywania powyższej pętli oraz kopiowania przy użyciu DMA. Dzięki temu poznamy jak mechanizm ten sprawdza się w praktyce.
Na początek zadeklarujmy 2 bufory – źródłowy i docelowy:
C
1
2
3
4
#define BUFFER_SIZE 32
uint8_t src_buffer[BUFFER_SIZE];
uint8_t dst_buffer[BUFFER_SIZE];
Pozostaje przygotować kanał DMA do pracy. Jak zwykle pierwszym krokiem jest uruchomienie zegara modułu peryferyjnego:
C
1
__HAL_RCC_DMA1_CLK_ENABLE();
Następnie deklarujemy zmienną z konfiguracją, ustawiamy pola i inicjalizujemy moduł:
C
1
2
3
4
5
6
7
8
9
10
11
DMA_HandleTypeDef dma;
dma.Instance=DMA1_Channel1;
dma.Init.Direction=DMA_MEMORY_TO_MEMORY;
dma.Init.PeriphInc=DMA_PINC_ENABLE;
dma.Init.MemInc=DMA_MINC_ENABLE;
dma.Init.PeriphDataAlignment=DMA_PDATAALIGN_BYTE;
dma.Init.MemDataAlignment=DMA_MDATAALIGN_BYTE;
dma.Init.Mode=DMA_NORMAL;
dma.Init.Priority=DMA_PRIORITY_HIGH;
HAL_DMA_Init(&dma);
W przypadku biblioteki HAL musimy ustawić wszystkie pola:
wybrany kanał DMA w polu DMA1_Channel1
kierunek przesyłu danych - za pamięci do pamięci DMA_MEMORY_TO_MEMORY
adres danych źródłowych ma być zwiększany po każdym transferze DMA_PINC_ENABLE
podobnie adres danych docelowych DMA_MINC_ENABLE
kopiujemy bajty, wybieramy więc wyrówanie DMA_PDATAALIGN_BYTE
tryb normalny wykona pojedynczy transfer danych DMA_NORMAL
wybieramy wysoki priorytet DMA_PRIORITY_HIGH
Nazwy PeriphInc i MemInc biorą się stąd, że najczęściej DMA wykorzystywane jest do kopiowania między modułem peryferyjnym, a pamięcią. Jako przykład wykorzystujemy kanał 1 w module 1 DMA.
Mikrokontroler STM32 może obsługiwać do 12 kanałów DMA jednocześnie!
Teraz, gdy moduł jest gotowy do pracy spróbujmy go uruchomić. W tym celu wywołujemy funkcję HAL_DMA_Start, której parametrami jest adres bufora źródłowego, docelowego oraz liczba bajtów do przesłania:
To właściwie wszystko, co potrzebowaliśmy do skopiowania danych – oczywiście wykorzystanie DMA dla 32 bajtów, to pewna przesada. Jednak teraz robimy to w celach edukacyjnych.
Cały kod realizujący powyższe zadanie wygląda następująco:
Sprawdź, czy funkcja copy_dma() działa poprawnie. Po jej wykonaniu w buforze dst_buffer powinniśmy mieć takie same wartości jak w src_buffer. Napisz program, który to sprawdzi i wyśle wynik przez UART. W podobny sposób sprawdź, czy poprawnie działa copy_cpu().
Porównanie wydajności
Po co więc stosować DMA? Spróbujmy nieco skomplikować nasz przykład i odpowiedzieć na to pytanie. Po pierwsze zamiast 32 bajtów, będziemy kopiować bufory o wielkości 4KB. Po drugie, żeby lepiej było widać różnicę w prędkości działania, wykonajmy kopiowanie 100 razy. Wtedy będziemy mogli porównać czas wykonywania programu z wykorzystaniem prostej pętli oraz DMA.
Konfiguracja kanału pozostaje identyczna jak poprzednio. Musimy tylko napisać procedurę kopiowania z użyciem DMA – będziemy ją wywoływać 100 razy w pętli. Gdy używaliśmy biblioteki StdPeriph, ponowne uruchamianie transferu DMA wymagało dodatkowej konfiguracji. Biblioteka HAL jest pod tym względem znacznie wygodniejsza, funkcja HAL_DMA_Start wyręcza nas w tym zadaniu.
Program dodatkowo zawiera pomiar czasu działania oraz wysyłanie wyników przez UART. Wykorzystamy timer SysTick uruchamiany przez bibliotekę HAL. Biblioteka udostępnia również funkcję HAL_GetTick zwracającą liczbę milisekund, które upłynęły od uruchomienia systemu. Wykorzystamy ją do pomiaru czasu pracy naszej procedury.
Po uruchomieniu programu możemy obejrzeć wyniki: DMA działa znacznie szybciej – około 4 razy.
Co więcej, w czasie przesyłania danych, mikrokontroler może zajmować się czymś pożytecznym np. sterowaniem robotem. Można również wykorzystać wiele kanałów DMA jednocześnie.
Porównanie wydajności kopiowania.
Odczyt ADC z użyciem DMA
Poprzednie przykłady miały na celu zapoznanie z możliwościami jakie daje DMA. Teraz napiszemy program, który można zastosować budując np.: robota.
W szóstej części kursu poznaliśmy przetwornik ADC. Sprawdziliśmy, jak oczytać wartości z jednego oraz kilku czujników. Teraz napiszemy program, który będzie odczytywał dane z wielu czujników i automatycznie zapisywał wyniki w pamięci RAM.
Dzięki temu przetwornik będzie mógł pracować w trybie ciągłym, a najnowsze dane będą dostępne w zwykłych zmiennych.
Na początek podłączmy, tak jak poprzednio, dwa potencjometry:
Zadeklarujmy w programie liczbę czujników jako stałą oraz bufor na odebrane wyniki:
C
1
2
3
#define ADC_CHANNELS 2
uint16_t adc_value[ADC_CHANNELS];
Konfiguracja DMA jest teraz trochę bardziej skomplikowana:
Chcemy odczytywać dane z przetwornika, więc wybieramy kopiowanie z układu peryferyjnego do pamięci DMA_PERIPH_TO_MEMORY. Adres rejestru danych przetwornika jest stały, więc wyłączamy zwiększanie adresu DMA_PINC_DISABLE. Natomiast wyniki chcemy zapisywać w kolejnych zmiennych, włączamy więc zwiększanie adresu w pamięci DMA_MINC_ENABLE (inaczej dane uległyby nadpisaniu).
Przetwornik w naszym mikrokontrolerze zwraca wyniki jako liczby 12-bitowe. Jest to raczej nietypowy rozmiar, więc użyjemy 16-bitowych wartości do przechowywania wyników:
DMA_PDATAALIGN_HALFWORD,
DMA_MDATAALIGN_HALFWORD.
Wcześniej używaliśmy trybu normalnego, w którym DMA wykonuje transfer danych i kończy pracę. Teraz chcemy żeby odczyt następował ciągle, wybieramy więc tryb DMA_CIRCULAR, który pozwala na pobieranie kolejnych danych do tego samego bufora. Na koniec ustawiamy wysoki priorytet dla naszych danych DMA_PRIORITY_HIGH.
Tak jak poprzednio inicjalizujemy moduł DMA wywołując funkcję HAL_DMA_Init, teraz jednak chcemy powiązać nasz kanał DMA z modułem ADC, czyli przetwornikiem analogowo-cyfrowym. W tym celu wywołujemy funkcję (albo raczej makro) __HAL_LINKDMA.
Kolejny krok, to skonfigurowanie modułu ADC. W poprzedniej części kursu już widzieliśmy większość kodu, jednak teraz konfiguracja jest nieco inna:
C
1
2
3
4
5
6
7
8
9
10
11
ADC_HandleTypeDef adc;
adc.Instance=ADC1;
adc.Init.ContinuousConvMode=ENABLE;
adc.Init.ExternalTrigConv=ADC_SOFTWARE_START;
adc.Init.DataAlign=ADC_DATAALIGN_RIGHT;
adc.Init.ScanConvMode=ADC_SCAN_ENABLE;
adc.Init.NbrOfConversion=ADC_CHANNELS;
adc.Init.DiscontinuousConvMode=DISABLE;
adc.Init.NbrOfDiscConversion=1;
HAL_ADC_Init(&adc);
Włączamy tryb ciągły (ContinuousConvMode), ponieważ chcemy, żeby przetwornik pracował cały czas w tle. Będziemy odczytywać wiele kanałów, więc ustawiamy ScanConvMode na ENABLE. Liczbę kanałów ustawiamy wypełniając pole NbrOfConversion. Na koniec wywołujemy HAL_ADC_Init, aby skonfigurować moduł przetwornika.
Chcemy pobierać dane z dwóch wejść, musimy więc skonfigurować odpowiednie kanały przetwornika:
C
1
2
3
4
5
6
7
8
9
ADC_ChannelConfTypeDef adc_ch;
adc_ch.Channel=ADC_CHANNEL_0;
adc_ch.Rank=ADC_REGULAR_RANK_1;
adc_ch.SamplingTime=ADC_SAMPLETIME_239CYCLES_5;
HAL_ADC_ConfigChannel(&adc, &adc_ch);
adc_ch.Channel=ADC_CHANNEL_1;
adc_ch.Rank=ADC_REGULAR_RANK_2;
HAL_ADC_ConfigChannel(&adc, &adc_ch);
Wybraliśmy kanał 0 oraz 1, pole Rank ustala kolejność wykonywania pomiarów, SamplingTime ustala czas wykonywania konwersji. Podobnie jak wcześniej wykonamy jeszcze kalibrację przetwornika:
C
1
HAL_ADCEx_Calibration_Start(&adc);
Właściwie wszystko już mamy gotowe wystarczy uruchomić przetwornika ADC w trybie DMA:
Wyniki są na bieżąco kopiowane i przepisywane do tablicy adc_value[]. Mikrokontroler może właściwie nie zajmować się więcej konwersją, w pełni poświęcając czas na inne zadania, np. sterowanie silnikami, realizację algorytmu PID itd. W naszym przykładzie czas ten zostanie przeznaczony na miganie diodą znajdującą się na zestawie Nucleo (GPIOA5).
Rezultat działania programu wyświetlany w terminalu widoczny jest poniżej:
Odczyt ADC przez DMA na STM32.
Zadanie 8.2
Podłącz dwa fotorezystory i dwa potencjometry. Napisz program, który będzie wyświetlał jednocześnie pomiary z 4 wejść (wykorzystaj do tego DMA). Wyniki sformatuj korzystając z możliwości funkcji printf.
Zadanie 8.3
Podłącz dwa fotorezystory oraz dwie diody LED sterowane przez PWM. Napisz program, który będzie jaśniej świecił diodą, przy której znajduje się mniej oświetlony fotorezystor.
Podsumowanie DMA w STM32
Zachęcam do dalszych testów z DMA. Mechanizm ten jest szczególnie przydatny np.: w robotach typu LineFollower. Nie musimy wtedy przejmować się odczytami z licznych czujników obiciowych. Wszystko dzieje się w tle, a wartości pobieramy tylko z wcześniej zdefiniowanej tablicy.
W kolejnym odcinku naszego kursu programowania STM32 omówimy cześć zagadnień związanych z SPI. Dzięki temu obsłużymy ekspander portów, a docelowo wyświetlacz graficzny!
Dołącz do 20 tysięcy osób, które otrzymują powiadomienia o nowych artykułach! Zapisz się, a otrzymasz PDF-y ze ściągami (m.in. na temat mocy, tranzystorów, diod i schematów) oraz listę inspirujących DIY na bazie Arduino i Raspberry Pi.
Dołącz do 20 tysięcy osób, które otrzymują powiadomienia o nowych artykułach! Zapisz się, a otrzymasz PDF-y ze ściągami (m.in. na temat mocy, tranzystorów, diod i schematów) oraz listę inspirujących DIY z Arduino i RPi.
Trwa ładowanie komentarzy...