Kurs STM32 LL cz. 9. Kontroler DMA, komunikacja USART w trybie DMA

Przerwania pozwalają w przyzwoity sposób zarządzać komunikacją USART. Mimo wszystko przy każdej przychodzącej danej musimy ją przekopiować do bufora ręcznie. W przypadku dużej ilości danych możemy ten proces zautomatyzować. Do tego celu służy kontroler DMA.

Kontroler DMA

Direct Memory Access (DMA), czyli mechanizm bezpośredniego dostępu do pamięci to technika przesyłania danych z pominięciem jednostki CPU.

DMA pozwala na przesyłanie danych:

  • z pamięci do pamięci
  • z pamięci do układu peryferyjnego
  • z układu peryferyjnego do pamięci
  • z układu peryferyjnego do układu peryferyjnego

W mikrokontrolerach STM32 mamy do dyspozycji jeden lub dwa kontrolery DMA. Każdy kontroler ma kilka kanałów, a każdy kanał może obsługiwać różne rodzaje transferów. W STM32G071 dostępny jest jeden kontroler DMA1 z 7 kanałami. To, jak zbudowany jest przepływ danych w STM32, przedstawia poniższa grafika.

Kontroler DMA podłączony jest do szyny AHB (Advanced High-Performance Bus), która komunikuje go z pamięcią SRAM  i Flash oraz za pośrednictwem szyny APB (Advanced Peripheral Bus) z układami peryferyjnymi. Układem nadzorującym współpracę między szynami jest Bus Matrix.

W przypadku, gdy dwa kanały DMA chcą wykonywać transfer jednocześnie, Arbiter rozdziela czas dostępu do Bus Matrix dla każdego kanału. Gdy dostępne są dwa oddzielne kontrolery DMA, mogą one korzystać z Bux Matrix jednocześnie, dopóki nie korzystają z tych samych układów peryferyjnych.

Szyna AHB implementuje algorytm karuzelowy (round robin), według którego przydziela dla każdego z układów przedział czasowy na wykonanie operacji, nie uwzględniając żadnych priorytetów. Kontroler DMA może więc spowolnić pracę CPU, jeżeli oba elementy potrzebują dostępu do tego samego obszaru pamięci lub układu peryferyjnego (zdarza się to bardzo rzadko). Nie może jednak zablokować CPU, ponieważ arbiter rozdziela po równo cykle pracy pomiędzy poszczególne elementy mikrokontrolera.

W starszych mikrokontrolerach (seria F i L) kontroler DMA podzielony jest na kanały. Do każdego z kanałów dołączonych jest kilka układów peryferyjnych i tylko jeden z nich może być jednocześnie obsługiwany jest dany kanał DMA. W nowszych układach (seria G) do każdego z kanałów można podpiąć dowolny układ peryferyjny z dostępnych. Za zarządzanie odpowiada DMAMUX. DMAMUX pozwala także na użycie generatora DMA do układów peryferyjnych, które nie mają funkcji DMA.

DMA generuje trzy rodzaje przerwań:

  • po wykonaniu połowy transferu
  • po wykonaniu całego transferu
  • w przypadku błędu

Może przesyłać dane po 8, 16 lub 32 bity (odpowiednio Byte, Half Word i Word). Szerokość danych zależna jest od rodzaju wysyłanych przez nas danych (po stronie pamięci) oraz od rozmiaru rejestru. Dodatkowo przed rozpoczęciem transferu DMA ustalany jest rozmiar, czyli ilość danych do przesłania.

Każdy transfer DMA może mieć przypisany jeden z 4 priorytetów, według których kontroler wykonuje transfery w przypadku, gdy jednocześnie wystąpi potrzeba obsługi dwóch lub więcej kanałów.

  • very high
  • high
  • medium
  • low

Poza tym kontroler DMA ma możliwość automatycznej inkrementacji adresów po stronie układu peryferyjnego i pamięci oraz możliwość cyklicznego transferu (po zakończeniu jednego rozpoczyna się kolejny od początku skonfigurowanego adresu – na zasadzie bufora kołowego). Pozwala to dobrać sposób przesyłania danych według potrzeb.

Rejestry DMA

DMA_ISR – rejestr z flagami przerwań dla każdego z 7 kanałów

DMA_IFCR – czyszczenie flag przerwań

DMA_CCRx – rejestr konfiguracyjny dla kanału x

Bity MEM2MEM – włączenie transferu pamięć-pamięć

Bit PL[1:0] – wybór priorytetu

Bity MSIZE[1:0] – rozmiar danych po stronie pamięci

Bity PSIZE[1:0] – rozmiar danych po stronie układu peryferyjnego

Bit MINC – włączenie zwiększania adresu po stronie pamięci

Bit PINC – włączenie zwiększania adresu po stronie układu peryferyjnego

Bit CIRC – tryb kołowy

Bit DIR – kierunek transferu

Bit TEIE – włączenie przerwania od błędu

Bit HTIE – włączenie przerwania od połowy transferu

Bit TCIE – włączenie przerwania od pełnego transferu

Bit EN – włączenie kanału

Rejestr DMA_CNDTRx – rozmiar transferu dla kanału x

DMA_CPARx – adres układu peryferyjnego dla kanału x

DMA_CMARx – adres układu pamięci dla kanału x

Multiplekser DMA

W układach z serii G do każdego z kanałów można podpiąć dowolny układ peryferyjny z puli dostępnych. Za zarządzanie przepływem danych odpowiada multiplekser, czyli DMAMUX.

DMAMUX ma za zadanie przekierować na wybrany kanał sygnał z układu peryferyjnego, którego chcemy użyć. Dzięki temu mamy dużą elastyczność pod względem używania DMA. Nie pojawi się problem często występujący w poprzednich seriach STM32 (np. F1), gdzie nie mogliśmy użyć dwóch peryferiów w trybie DMA, bo były podłączone do tego samego kanału.

W STM32G071 mamy do dyspozycji 77 wejść mogących korzystać z kontrolera DMA. Pełną listę przedstawia tabela.

Rodzaj wejścia dla kanału x konfigurujemy przy pomocy rejestru DMAMUX_CxCR i bitów DMAREQ_ID[6:0].

DMAMUX ma możliwość także konfiguracji wyzwalaczy i synchronizacji. Nie będziemy jednak korzystali z tych funkcjonalności, dlatego pominąłem ich opis.

Wszystkie projekty z kursu dostępne są w moim repozytorium GitHub.

[PROGRAM] Wysyłanie danych w trybie DMA

Poznaliśmy podstawy działania kontrolera DMA. Czas przejść do przykładu. Dzisiaj zajmiemy się wysyłaniem danych. Konfiguracja pinu TX oraz magistrali USART jest identyczna jak dla przykładu transmisji danych w trybie Polling. Przejdźmy do konfiguracji kontrolera DMA.

Do transmisji danych przez DMA wykorzystamy kanał 1. Jak już wiemy, w STM32G0 nie mamy ściśle przypisanego kanału do układu peryferyjnego, dlatego tutaj mamy pełną dowolność.

Uruchamiamy zegar dla DMA1 (mikrokontroler STM32G071 ma tylko jeden kontroler DMA oznaczony jako DMA1).

LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);

Następnie konfigurujemy parametry pracy DMA. Ustawiamy źródło dla kanału 1 w multiplekserze DMAMUX.

LL_DMA_SetPeriphRequest(DMA1, LL_DMA_CHANNEL_1, LL_DMAMUX_REQ_USART2_TX);

Teraz kolejno ustawiamy kierunek transmisji, priorytet i tryb pracy.

LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PRIORITY_HIGH);
LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MODE_NORMAL);

Na koniec konfigurujemy, czy adresy mają być inkrementowane oraz rozmiar pojedynczego elementu w transmisji po stronie układu peryferyjnego i pamięci.

LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PERIPH_NOINCREMENT);
LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MEMORY_INCREMENT);
LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PDATAALIGN_BYTE);
LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MDATAALIGN_BYTE);

Pozostało jeszcze skonfigurowanie i włączenie przerwania od DMA1 na kanale 1.

NVIC_SetPriority(DMA1_Channel1_IRQn, 0);
NVIC_EnableIRQ(DMA1_Channel1_IRQn);
LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_1);

Teraz, żeby wysłać dane przez DMA, wywołujemy funkcję ustawiającą adresy pamięci (naszego bufora TX), układu peryferyjnego (czyli rejestru USART_TDR) oraz typ transmisji (pamięć-pamięć, peryferium-pamięć lub pamięć-peryferium). 

LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_1, (uint32_t)tx_buffer, LL_USART_DMA_GetRegAddr(USART2, LL_USART_DMA_REG_DATA_TRANSMIT), LL_DMA_GetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1));

Musimy jeszcze poinformować kontroler, ile danych chcemy przesłać.Po zakończeniu transmisji licznik ten spadnie do 0, dlatego przed kolejną transmisją musimy go ustawić ponownie.

LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, sizeof(tx_buffer));

Teraz, gdy mamy już wszystko skonfigurowane, włączamy przerwania od DMA TX i włączamy kanał 1 DMA1.

LL_USART_EnableDMAReq_TX(USART2);
LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1);

W obsłudze przerwania od zakończenia transmisji sprawdzamy flagę, czyścimy ją (warto wyczyścić wszystkie flagi od transferu DMA, nawet, jak z nich nie korzystamy) i wyłączamy kanał DMA.

void DMA_Channel1_IRQHandler(void)
{
    if (LL_DMA_IsActiveFlag_TC1(DMA1))
    {
         LL_DMA_ClearFlag_TC1(DMA1);
	 LL_DMA_ClearFlag_HT1(DMA1);

         LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_1);
    }
}

[PROGRAM] Odbieranie danych w trybie DMA

W poprzedniej części nauczyliśmy się wysyłać dane przez DMA. Dzisiaj zajmiemy się ich odbieraniem.

Konfiguracja pinu RX oraz magistrali USART jest identyczna jak dla przykładu transmisji danych w trybie Polling. Przejdźmy do konfiguracji kontrolera DMA. Do odbierania danych przez DMA wykorzystamy również kanał 1. Jak już wiemy, w STM32G0 nie mamy ściśle przypisanego kanału do układu peryferyjnego, dlatego tutaj mamy pełną dowolność.

Uruchamiamy zegar dla DMA1 (mikrokontroler STM32G071 ma tylko jeden kontroler DMA oznaczony jako DMA1).

LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);

Następnie konfigurujemy parametry pracy DMA. Ustawiamy źródło dla kanału 1 w multiplekserze DMAMUX.

LL_DMA_SetPeriphRequest(DMA1, LL_DMA_CHANNEL_1, LL_DMAMUX_REQ_USART2_RX);

Teraz kolejno ustawiamy kierunek transmisji, priorytet i tryb pracy.

LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1, LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PRIORITY_HIGH);
LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MODE_NORMAL);

Na koniec konfigurujemy, czy adresy mają być inkrementowane oraz rozmiar pojedynczego elementu w transmisji po stronie układu peryferyjnego i pamięci.

LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PERIPH_NOINCREMENT);
LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MEMORY_INCREMENT);
LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PDATAALIGN_BYTE);
LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MDATAALIGN_BYTE);

Pozostało jeszcze skonfigurowanie i włączenie przerwania od DMA1 na kanale 1.

NVIC_SetPriority(DMA1_Channel1_IRQn, 0);
NVIC_EnableIRQ(DMA1_Channel1_IRQn);
LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_1);

Teraz, żeby wysłać dane przez DMA, wywołujemy funkcję ustawiającą adresy pamięci (naszego bufora RX), układu peryferyjnego (czyli rejestru USART_TDR) oraz typ transmisji (pamięć-pamięć, peryferium-pamięć lub pamięć-peryferium). 

LL_DMA_ConfigAddresses(DMA1, LL_DMA_CHANNEL_1, LL_USART_DMA_GetRegAddr(USART2, LL_USART_DMA_REG_DATA_RECEIVE), (uint32_t)rx_buffer, LL_DMA_GetDataTransferDirection(DMA1, LL_DMA_CHANNEL_1));

Musimy jeszcze poinformować kontroler, ile danych chcemy przesłać. Po zakończeniu transmisji licznik ten spadnie do 0, dlatego przed kolejną transmisją musimy go ustawić ponownie.

LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, sizeof(rx_buffer));

Teraz, gdy mamy już wszystko skonfigurowane, włączamy przerwania od DMA RX i włączamy kanał 1 DMA1.

LL_USART_EnableDMAReq_RX(USART2);
LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1);

W obsłudze przerwania od zakończenia transmisji sprawdzamy flagę, czyścimy ją i wyłączamy kanał DMA, aby ponownie ustawić ilość danych do odbioru (licznik w rejestrze DMA->CNDTR nie może być zmieniony, gdy kanał DMA jest włączony).

void DMA_Channel1_IRQHandler(void)
{
    if (LL_DMA_IsActiveFlag_TC1(DMA1))
    {
        LL_DMA_ClearFlag_TC1(DMA1);
        LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_1);
        LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, sizeof(rx_buffer));
        LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1);
    }
}

Chciałbyś otrzymywać na bieżąco informacje o nowych artykułach z kursu? Zapisz się do newslettera!

TO NIE TYLKO MAIL Z INFORMACJĄ O NOWEJ LEKCJI, ALE TAKŻE DODATKOWE MATERIAŁY. NIE PRZEGAP NOWEJ TREŚCI I DODATKOWYCH BONUSÓW. PRZEJDŹ DO STRONY KURSU I PODAJ SWÓJ ADRES E-MAIL. NIE ZAPOMNIJ POTWIERDZIĆ CHĘCI DOŁĄCZENIA W PIERWSZEJ WIADOMOŚCI!

Repozytorium GitHub

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *