Kurs STM32 F1 HAL – #8 – bezpośredni dostęp do pamięci

Kurs STM32 F1 HAL – #8 – bezpośredni dostęp do pamięci

Podczas 6 części kursu poznaliśmy możliwości przetwornika ADC. Uruchamiane przykłady były jednak pod pewnym względem niedoskonałe.

Dziś 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 ż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ę.

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

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!

Zestaw elementów do kursu

Gwarancja pomocy na forum Błyskawiczna wysyłka

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!

Kup w Botland.com.pl

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

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

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.

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:

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

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.

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.

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:

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:

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:

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:

Właściwie wszystko już mamy gotowe wystarczy uruchomić przetwornika ADC w trybie DMA:

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

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!

Nawigacja kursu

Autor kursu: Piotr Bugalski
Redakcja: Damian Szymański

ADC, DMA, HAL, kursSTM32F1HAL, stm32

Komentarze

Trwa przerwa techniczna - komentarze do tego wpisu są dostępne na forum: