KursyPoradnikiInspirujące DIYForum

Kurs STM32 – #8 – DMA, czyli bezpośredni dostęp do pamięci

Kurs STM32 – #8 – DMA, czyli bezpośredni dostęp do pamięci

W części 6 kursu STM32 poznaliśmy możliwości przetwornika ADC. Uruchamiane przykłady były jednak niedoskonałe.

Teraz poznamy nową, efektywną metodę. Zamiast aktywnie czekać na odczyt, wykorzystamy moduł DMA, który będzie robił to w tle.

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).

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.

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:

animacjaDMA_bezDMA

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:

animacjaDMA_zDMA

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!

Zamów w Botland.com.pl »

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ę:

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.

Pozostaje przygotować kanał DMA do pracy. Jak zwykle pierwszym krokiem jest uruchomienie zegara modułu peryferyjnego:

Następnie deklarujemy zmienną z konfiguracją, ustawiamy pola i inicjalizujemy moduł:

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.

Teraz, gdy moduł jest gotowy do pracy spróbujmy go uruchomić:

Gdy dane są kopiowane, program mógłby robić coś innego, ale w tym przykładzie po prostu poczekamy na zakończenie transmisji:

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:

Zadanie 8.1

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.

Cały kod programu:

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_01

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.

STM32_6_6

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.

Na początek podłączmy, tak jak poprzednio, dwa potencjometry:

adc-pot2-b_bb

Zadeklarujmy w programie liczbę czujników jako stałą oraz bufor na odebrane wyniki:

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:

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ć:

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:

W tym momencie całość jest już prawie gotowa. Czas uruchomić przetwornik oraz kanał DMA do niego przypisany:

Podobnie jak wcześniej wykonamy jeszcze kalibrację i uruchomienie przetwornika. Jeśli ktoś się pogubił, to zachęcam do sprawdzenia całego programu:

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.

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.

Nawigacja kursu

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

ADC, DMA, kursSTM32, stm32

Trwa ładowanie komentarzy...