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 praktycznego przykładu. Z góry zaznaczam, ż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ę. Całość mogłaby wyglądać w taki sposób:
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ę. Mogłoby to wyglądać tak:
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 »
DMA - Kopiowanie pamięci
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
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
Następnie deklarujemy zmienną z konfiguracją, ustawiamy pola i inicjalizujemy moduł:
C
1
2
3
4
5
6
7
8
9
10
DMA_InitTypeDef dma;
DMA_StructInit(&dma);
dma.DMA_PeripheralBaseAddr=(uint32_t)src_buffer;
dma.DMA_PeripheralInc=DMA_PeripheralInc_Enable;
dma.DMA_MemoryBaseAddr=(uint32_t)dst_buffer;
dma.DMA_MemoryInc=DMA_MemoryInc_Enable;
dma.DMA_BufferSize=BUFFER_SIZE;
dma.DMA_M2M=DMA_M2M_Enable;
DMA_Init(DMA1_Channel1,&dma);
Funkcja DMA_StructInit wypełnia pola domyślnymi wartościami. Musimy ustawić:
adres źródłowy DMA_PeripeheralBaseAddr na bufor src_buffer
adres docelowy DMA_MemoryBaseAddr na dst_buffer (adresy są wskaźnikami, ale traktujemy je jako liczby całkowite bez znaku, czyli uint32_t).
Nazwy DMA_PeripheralBaseAddr i DMA_MemoryBaseAddr biorą się stąd, że najczęściej DMA wykorzystywane jest do kopiowania między modułem peryferyjnym, a pamięcią. Po każdym kopiowaniu wskaźniki powinny być zwiększane, ustawiamy więc dwie flagi:
DMA_PeripheralInc_Enable oraz DMA_MemoryInc_Enable.
Następnie podajemy liczbę bajtów do skopiowania, wpisujemy ją do pola DMA_BufferSize. Musimy też poinformować DMA, że kopiowanie jest z pamięci do pamięci (DMA_M2M_Enable). 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ć:
C
1
DMA_Cmd(DMA1_Channel1,ENABLE);
Gdy dane są kopiowane, program mógłby robić coś innego, ale w tym przykładzie po prostu poczekamy na zakończenie transmisji:
C
1
while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
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 RS-232. 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, więc przed rozpoczęciem kopiowania musimy wyzerować flagę zakończenia pracy DMA_ISR_TCIF1 oraz ustawić od nowa licznik danych do przesłania funkcją DMA_SetCurrDataCounter.
Program dodatkowo zawiera pomiar czasu działania oraz wysyłanie wyników przez złącze RS-232. Wykorzystamy timer SysTick, ale tym razem zamiast zmniejszać wartość timer_ms, będziemy ją zwiększać co 1 ms. Ułatwi to pomiar czasu działania procedury.
Po uruchomieniu programu możemy obejrzeć wyniki: DMA działa znacznie szybciej – około 5 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.
DMA - Odczyt danych z ADC
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.
Jako adres źródłowy podajemy adres rejestru DR przetwornika ADC1. Jest to rejestr, w którym znajduje się wynik ostatniej konwersji. Jak pamiętamy z części 6, do odczytu używaliśmy funkcji ADC_GetConversionValue. Jej treść jest bardzo prosta i sprowadza się od odczytu z tego rejestru:
C
1
2
3
4
5
6
7
uint16_t ADC_GetConversionValue(ADC_TypeDef*ADCx)
{
/* Check the parameters */
assert_param(IS_ADC_ALL_PERIPH(ADCx));
/* Return the selected ADC conversion value */
return(uint16_t)ADCx->DR;
}
Podajemy adres rejestru jako adres źródłowy danych. Ponieważ ten adres jest stały, wyłączamy inkrementację adresu źródłowego:
DMA_PeripheralInc_Disable.
W poprzednich przykładach kopiowaliśmy dane bajt-po-bajcie. Teraz chcemy odczytać wynik konwersji, który jak widać jest 16-bitowy. Zmieniamy więc rozmiar danych na pół-słowo:
DMA_PeripheralDataSize_HalfWord.
Docelowym adresem jest nasz bufor. Ustawienia są podobne, ale tym razem chcemy zwiększać adres po każdym zapisie. Ustawiamy więc DMA_Memory_Inc_Enable. Gdybyśmy tego nie zrobili, moduł DMA za każdym razem zapisywałby do tego samego miejsca w pamięci, a my chcemy w kolejnych pozycjach bufora mieć odczyty z różnych wejść.
Następnym krokiem jest ustalenie liczby danych do przesłania. Ustawiamy wartość pola DMA_BufferSize na równą liczbie kanałów, z których odczytujemy dane.
Wybieramy tryb DMA_Mode_Circular, dzięki temu po zapełnieniu bufora, transmisja zacznie się od początku. Gdy kanał DMA jest już przygotowany, trzeba go jeszcze uruchomić:
C
1
DMA_Cmd(DMA1_Channel1,ENABLE);
Kolejny krok, to skonfigurowanie modułu ADC. W poprzedniej części kursu już widzieliśmy większość kodu, jednak teraz konfiguracja jest nieco inna:
Będziemy odczytywać wiele kanałów, więc ustawiamy ADC_ScanConvMode na ENABLE.
Włączamy też tryb ciągły (ADC_ContinuousConvMode), ponieważ chcemy, żeby przetwornik pracował cały czas w tle. Po raz kolejny podajemy liczbę kanałów, tym razem wstawiamy ją do pola ADC_NbrOfChannel. Podobnie jak w pierwszym przykładzie w części 6 uruchomimy konwersję programowo, możemy więc wyłączyć trigger ADC_ExternalTrigConv_None.
Wywołujemy ADC_Init(), a następnie ustawiamy parametry konwersji dla każdego kanału:
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.
Rezultat działania programu:
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.
Uwaga! Ten kurs został zarchiwizowany. Sprawdź najnowszy kurs STM32 »
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! Jeśli nie chcesz przeoczyć kolejnego odcinka, to skorzystaj z poniższego formularza i zapisz się na powiadomienia o nowych publikacjach!
Autor kursu: Piotr (Elvis) Bugalski Redakcja: Damian (Treker) Szymański
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...