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.
Lista lekcji „Kurs STM32 Low Layer”
- Kurs STM32 LL cz. 1. Biblioteki Low Layer, Nucleo-G071RB, STM32CubeIDE
- Kurs STM32 LL cz. 2. Przygotowanie projektu
- Kurs STM32 LL cz. 3. Wewnętrzne i zewnętrzne źródła zegara
- Kurs STM32 LL cz. 4. Pętla PLL i taktowanie układów peryferyjnych
- Kurs STM32 LL cz. 5. Budowa GPIO i sterowanie wyjściem
- Kurs STM32 LL cz. 6. Wyjście GPIO i przerwania EXTI
- Kurs STM32 LL cz. 7. Interfejs USART, transmisja danych w trybie polling
- Kurs STM32 LL cz. 8. Komunikacja USART w trybie przerwań
- Kurs STM32 LL cz. 9. Kontroler DMA, komunikacja USART w trybie DMA
- Kurs STM32 LL cz. 10. Rodzaje i budowa Timerów, Timer w funkcji licznika
- Kurs STM32 LL cz. 11. Timer w trybie Input Capture
- Kurs STM32 LL cz. 12. Timer w trybie Output Compare i PWM
- Kurs STM32 LL cz. 13. Wstęp do konwertera ADC
- Kurs STM32 LL cz. 14. Konwersja ADC Single Channel i Multi Channel w trybie Polling
- Kurs STM32 LL cz. 15. Konwersja ADC Single Channel i Multi Channel w trybie przerwań
- Kurs STM32 LL cz. 16. Konwersja ADC Single Channel i Multi Channel w trybie DMA
- Kurs STM32 LL cz. 17. Wstęp do magistrali I2C
- Kurs STM32 LL cz. 18. Komunikacja I2C w trybie polling
- Kurs STM32 LL cz. 19. Komunikacja I2C w trybie przerwań
- Kurs STM32 LL cz. 20. Komunikacja I2C w trybie DMA
- Kurs STM32 LL cz. 21. Wprowadzenie do interfejsu SPI
- Kurs STM32 LL cz. 22. Komunikacja SPI w trybie polling
- Kurs STM32 LL cz. 23. Komunikacja SPI w trybie przerwań
- Kurs STM32 LL cz. 24. Komunikacja SPI w trybie 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!