Temat UART w połączeniu z DMA co chwilę wraca jak bumerang. Zwłaszcza odbieranie danych na mikrokontrolerze sprawia najwięcej problemów. Dlaczego? Bo nie wiemy ile tych danych przyjdzie. Stąd odbiór po DMA jest nieco kiepskim pomysłem, bo musimy z góry określić ilość odbieranych danych. Ale nie zawsze…
UART w połączeniu z DMA
Jakiś czas temu pisałem o świetnym wykorzystaniu odbioru na DMA w połączeniu z przerwaniem UART od stanu bezczynności, czyli IDLE. Znajdziesz na moim blogu dwa artykuły jak taki mechanizm napisać pod mikrokontrolery F4 (o tutaj), oraz F1 (o tutaj). Przypomnę w skrócie, na czym taka konstrukcja polega.
- Konfigurujemy UART do pracy z DMA po to, aby był on w stanie wysyłąc żądania transferu do DMA.
- Włączamy przerwanie od bezczynności UART, czyli IDLE.
- DMA konfigurujemy tak, aby czytał z rejestru UART DR i wpisywał gdzieś do przygotowanej tablicy w pamięci RAM (z inkrementacją).
- Włączamy odbiór z UARTa poprzez DMA.
- Zakończenie odbioru może nastąpić w dwóch momentach
- Odebraliśmy określoną z góry ilość znaków – dostajemy przerwanie DMA Transfer Complete
- Odebraliśmy mniej znaków, ale UART wszedł w stan bezczynności na linii RX – dostajemy przerwanie UART IDLE
Finalnie dostajemy przerwanie po zakończonym odbiorze niezależnie ile tych znaków odebraliśmy. Znamy więc moment, kiedy należałoby się tymi danymi zająć. Super, co nie?
Implementacja na rejestrach
W poprzednich wpisach dotyczących STM32F4 i STM32F1 robiłem to na pieszo z wykorzystaniem rejestrów. Dlaczego? Otóż biblioteki HAL ni w ząb nie wspierały przerwania IDLE. Trzeba było się posiłkować własną implementacją i dorzucać obsługę przerwania IDLE na UARcie.
Takie działanie niosło ze sobą mnóstwo wad i niedoskonałości.
Po pierwsze nie jest to rozwiązanie uniwersalne, o czym przekonali się moi kursanci. Osoby działające na STM32L4, czy STM32F7 zderzyły się z problemem nieco inaczej wyglądających rejestrów. Dla każdego mikrokontrolera tak naprawdę trzeba robić osobne podejście i osobną implementację, bo miały drobne niuanse w działaniu DMA czy UARTa. To może być uciążliwe.
Po drugie moja implementacja nie była odporna na to, że IDLE występował również po odebraniu równej ilości zleconych znaków na DMA. My się zajmowaliśmy już odebranymi danymi w przerwaniu od DMA, a tutaj weszło IDLE i de facto robiłem wtedy drugi raz to samo…
Przydałoby się coś bardziej uniwersalnego i działającego, prawda? Coś, co będzie działało dla każdej rodziny STM32. Już jest!!!
UART IDLE w bibliotekach HAL
Pod koniec 2020 roku ST Microelectronics dodało w końcu obsługę przerwania UART IDLE! Dla wszystkich rodzin. Oznacza to dla nas – programistów – jednolite użycie tego przerwania, oraz jego obsługi niezależnie od rodziny mikrokontrolera.
To wszystko standardowo jak to dla HALa – robi się “za nas”. W takim sensie, że zlecamy odbiór specjalną funkcją i mamy do dyspozycji specjalny Callback. Jest on wywoływany w dwóch sytuacjach, czyli przy DMA Transfer Complete i przy UART Idle.
Zlecenie odbioru jest bardzo podobne do zwykłego zlecenia odbioru po DMA. Po prostu po ustawieniu odbierania na DMA włączane jest przerwanie IDLE. Po drodze jest ustawiana też informacja, że to był tryb odbioru z IDLEm, aby HAL w obsłudze przerwania wiedział co robić.
Jak wygląda kod z przerwania? Po pierwsze HAL sprawdza w strukturze UARTa, czy odbiór był zlecany z użyciem IDLE. Jeśli tak, to będzie sprawdzana i kasowana również ta flaga. Później sprawdza, czy to był odbiór z użyciem DMA, bo to również ma znaczenie i kasuje odpowiednie flagi. Na końcu wywołuje specjalny Callback.
Ustawmy odbiór na DMA z użyciem IDLE
Po pierwsze trzeba pobawić się w CubeMX. Od dawna korzystam z STM32CubeIDE, więc CubeMX jest w nim zaszyty.
Na czym dzisiaj oprę swoje działania? Mam pod ręką Nucleo-F411RE, które używam między innymi w swoim Kursie STM32 dla Początkujących. STM32CubeIDE w wersji 1.6.0 i biblioteki HAL F4 v1.26.1.
Po pierwsze musimy mieć ustawiony UART. Jeśli tworzysz projekt domyślnie z wyboru Nucleo, to masz już skonfigurowany UART2 na 11520. Trzeba mu dorzucić dwie rzeczy.
Oczywiście trzba ustawić DMA na linię odbiorczą USART2_RX. Zrób dokładnie jak ja, czyli z autoinkrementacją po stronie pamięci.
Numer Streamu/Kanału nie ma większego znaczenia. Chcemy tylko odbierać coś na UART, więc nic innego nam nie przeszkodzi.
Dalej trzeba włączyć przerwania od UARTa. Przecież chcemy korzystać z przerwania UART IDLE. Przerwanie od DMA jest z domysłu włączone.
Na koniec przerzucenie włączenia przerwań po wszystkich inicjalizacjach peryferiów. Nie jest to konieczne, ale poprawia czytelność kodu i niekiedy działanie. Wyznaję zasadę, że zawsze inicjalizacja peryferiów jest w pierwszej kolejności i dopiero później mogę myśleć o włączaniu przerwań.
Mamy wszystko, czego potrzebujemy. Przejdźmy do kodu.
Jak używać DMA z przerwaniem UART IDLE
Nie pozostało nam nic innego jak po prostu użyć DMA i IDLE do odebrania jakiegoś komunikatu. Zróbmy to.
HAL ułatwia nam odbieranie na maksa. Potrzebujemy tablicy, do której będziemy odbierać znaki. Zazwyczaj tworzę ją w obszarze zmiennych globalnych w wyznaczonym komentarzami odpowiednim miejscu pliku main.c.
/* USER CODE BEGIN PV */ uint8_t ReceiveBuffer[32]; // Receive from UART Buffer /* USER CODE END PV */
Teraz musimy ustawić odbiór po DMA. Samo odbieranie z wykorzystaniem DMA załatwia nam funkcja HAL_UART_Receive_DMA(…), jednak ona się nam tutaj nie przyda. Ona nastawia sztywną ilość znaków do odbioru i tyle. Potrzebujemy przecież dodatkowo włączyć przerwanie IDLE.
Dlatego musimy użyć zupełnie innej funkcji. Jest ona z dopiskiem “Ex”, czyli funkcji rozszerzonych z API HALa. Będzie to funkcja HAL_UARTEx_ReceiveToIdle_DMA(…), która przyjmuje dokładnie te same argumenty, co ta zwykła od odbioru na DMA.
Użyjmy więc tej specjalnej funkcji w mainie jeszcze przed startem głównej pętli. Odpowiednim miejscem na to jest sekcja użytkownika numer 2.
/* USER CODE BEGIN 2 */ // Start to listening on UART with DMA + Idle interrupt detection // the Callback is in USER CODE 4 section HAL_UARTEx_ReceiveToIdle_DMA(&huart2, ReceiveBuffer, 32); /* USER CODE END 2 */
Pamiętaj, że my ustawiamy “nasłuch” i nie musimy czekać na zakończenie odbioru. Dostaniemy przerwanie w swoim czasie. Właśnie – przerwanie. W HALu pamiętamy, że obsługą przerwań zajmuje się biblioteka. Nam wystawiane są callbacki do reagowania na odpowiednie przerwania.
Dla odbioru UART na DMA z wykorzystaniem przerwania IDLE jest jeden wspólny callback – HAL_UARTEx_RxEventCallback(…). W nim dostajemy oczywiście informację, który UART wywołał nam callback, ale nie tylko. W drugim argumencie dostajemy liczbę bajtów, które udało się odebrać na UART.
Jest to niesamowicie cenna informacja, bo przerwanie mogło wystąpić po różnej ilości znaków! Po to właśnie korzystamy z IDLE. Mają tę informację mamy już komplet tego, co potrzebujemy do parsowania.
Jak napisać naszą obsługę callbacku? Wystarczy sprawdzić, czy to nasz UART wywołał, później wykonać swoją robotę. Na przykład parsowanie lub wsadzenie do jakiegoś bufora (np. kołowego) do późniejszej obróbki poza przerwaniem.
Na samym końcu koniecznie musisz włączyć ponownie nasłuch. W taki sam sposób jak w funkcji main. Musisz od nowa ustawić DMA i UART w odbiór z wykorzystaniem IDLE. Cały callback będzie wyglądał więc tak.
/* USER CODE BEGIN 4 */ void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { // Check if UART2 trigger the Callback if(huart->Instance == USART2) { // // Do something // // Start to listening again - IMPORTANT! HAL_UARTEx_ReceiveToIdle_DMA(&huart2, ReceiveBuffer, 32); } } /* USER CODE END 4 */
I to tyle. Sprawdźmy, czy to działa.
Działanie odbioru
Mamy ustawione 32 bajty do odebrania na DMA. Wyślijmy mniej. Na przykład string “Ukulele”. Ma on zdecydowanie mniej znaków niż zaplanowana ilość dla DMA. Powinno nam w takim razie wejść przerwanie od UART IDLE.
Postawiłem pułapkę w naszym callbacku, puściłem program i wysłałem napis.
I… włala! Program stanął na breakpoincie. Z prawej strony wrzuciłem do Live Expression to, co nas interesuje. Rozmiar odebranego stringa – 7 bajtów. To przekazuje nam callback i należy wykorzystać później.
Dorzuciłem też samą tablicę, do której chciałem odbierać na UART i jest tam moje Ukulele 🙂
Działa jak należy!
Parsowanie stringów
W dalszej kolejności należałoby to, co otrzymaliśmy przeparsować, czyli zareagować na to, co przyszło. Jest to operacja dużo bardziej złożona i można podzielić ją na parsowanie proste i złożone.
Temat odbierania i parsowania Stringów na UART bardzo dokładnie opracowałem w moim Kursie STM32 dla Początkujących.
Właśnie są otwarte zapisy do trzeciej edycji. Kurs obejmuje bardzo szeroki zakres programowania STM32. Poznajesz nie tylko sam mikrokontroler, ale właśnie różne techniki programowania i radzenia sobie w embedded. Przykładowo właśnie odbieranie znaków na UART, buforowanie ich w buforze kołowym i późniejszą obróbkę stringów.
Sprawdź cały program na https://kursstm32.pl/. Dołączyć do trzeciej edycji można do piątku 04.06.2021 do godziny 20:00. Później nie będzie możliwości dołączenia do kursu!
Podsumowanie
W końcu doczekaliśmy się obsługi UART z DMA i przerwaniem IDLE. ST wykonało to w taki sposób, że użycie tej metody odbioru jest bajecznie proste.
Jedyne co musisz zrobić to ustawić DMA i przerwania na UART, oraz skorzystać ze startu odbioru i callbacku.
Metoda ta jest taka sama dla wszystkich rodzin STM32, więc śmiało wypróbuj to na swoim mikrokontrolerze!
Jeśli zauważyłeś jakiś błąd, nie zgadzasz się z czymś, chciałbyś coś dodać istotnego lub po prostu uważasz, że chciałbyś podyskutować na ten temat, napisz komentarz. Pamiętaj, że dyskusja ma być kulturalna i zgodna z zasadami języka polskiego.
25 komentarzy
Paweł · 25/05/2022 o 08:00
Z tego co zauważyłem, dla f103cb trzeba wywołać __HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT); tuż po uruchomieniu HAL_UARTEx_ReceiveToIdle_DMA. Inaczej domyślnie będziemy dostawali przerwanie przy HT.
Mateusz Salamon · 06/06/2022 o 19:30
Tak jest! HT warto wyłączyć jeśli z niego nie korzystamy.
domints · 16/07/2023 o 16:07
Mateuszu, fajnie by było, gdybyś w artykule wspomniał o tym HT – nie przeczytałem komentarzy tylko wzorowałem się na Twoim komentarzu i sporo krwi napsułem, zanim doszedłem do tego czemu nie zgadzają mi się bajty w buforze 🙂
Paweł, to samo jest dla serii F0xx, ja walczyłem z tym w F042 🙂
Michał · 26/11/2021 o 14:38
A czy da się ustawić to przerwanie od stanu IDLE ale gdy przerwa w transmisji jest dłuższa niż 1 znak? np. 10 znaków?
Mateusz Salamon · 26/11/2021 o 14:54
Z tego co mi wiadomo to nie. To jest realizowane sprzętowo. W sumie fajna byłaby taka opcja.
Michał · 18/11/2021 o 12:11
Witam 🙂
A dlaczego tablica ReceiveBuffer nie jest volatile skoro jej zawartość jest modyfikowana w przerwaniu?
Mateusz Salamon · 18/11/2021 o 14:01
Ups 😀 powinna być. Co ciekawe samo ST nie wskazuje, że argument dla obsługi przerwania ma być volatile. Myślisz, że to błąd czy zamierzone działanie?
Michał · 18/11/2021 o 15:23
hm… działa bez volatile. ST by się nie machnął chyba aż tak mocno. Jak zauważę jakieś nieprawidłowości dam znać 🙂
Blublub · 24/11/2021 o 22:13
To powinien byc ten volatile, czy nie? Jesli mozna to z uzasadnieniem
domints · 16/07/2023 o 16:09
Jak ustawiałem ten bufor jako volatile to mi się kompilator burzył, że tak nie powinno być, więc chyba jednak nie ma być volatile 🙂
Michał · 17/11/2021 o 15:29
Witam.
Super poradnik :)!
W momencie w którym przychodzi pierwsze przerwanie odbieram sobie taki ciąg znaków dzięki buforowi:
KURSPROGRAMOWANIA
W momencie w którym dostaje kolejne przerwanie spodziewam się dostać taki ciąg znaków: JAVA
ale dostaje:
JAVAPROGRAMOWANIA
czyli nowe dane nadpisują stare, zostawiając część starych. Proszę o wskazówkę jak się z tym uporać.
Mateusz Salamon · 17/11/2021 o 15:33
Wyciągaj z tego bufora to, co potrzebujesz. Dostajesz informacje ile znaków przyszło. To, że nie czyści bufora to dobrze, bo nie traci na to czasu 🙂 To Ty musisz zadbać o poprawne wyciągnięcie i użycie danych.
Michał · 17/11/2021 o 15:54
Dziękuję za błyskawiczną odpowiedź.
Jak dokopać się do zmiennej która informuje ile znaków zostało właśnie nadpisanych?
Spośród tych:
uint8_t *pTxBuffPtr; /*!< Pointer to UART Tx transfer Buffer */
uint16_t TxXferSize; /*!< UART Tx Transfer size */
__IO uint16_t TxXferCount; /*!< UART Tx Transfer Counter */
uint8_t *pRxBuffPtr; /*!< Pointer to UART Rx transfer Buffer */
uint16_t RxXferSize; /*!< UART Rx Transfer size */
__IO uint16_t RxXferCount; /*!< UART Rx Transfer Counter */
uint16_t Mask; /*!< UART Rx RDR register mask
sprawdzilem tą: RxXferSize
ale ona pokazuje jaki rozmiar ma bufor, a nie o to chodzi 🙂
Mateusz Salamon · 17/11/2021 o 15:58
Z tego co przeczytałem w moim artykule powyżej to masz tę informację w callbacku: void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) <- argument Size w callbacku mówi ile znaków odebrano.
MMichał · 17/11/2021 o 16:05
Tak jest. O to chodziło. Serdeczne dzięki 🙂 Pozdrawiam.
Krzychu1995 · 25/10/2021 o 10:17
O, a ja już chciałem pisać z zapytaniem co zrobić jak nie działa mi odbiór po DMA i parsowanie komunikatów MIDI. USART gubi mi bajty!! A mój parser MIDI musi dostać komplet bajtów żeby odpalić procedurę która wykona komunikat (czyli odpowiednio przydzieli jeden z sześciu głosów i zagra odpowiednią nutę po I2S). Komunikaty mają też różne długości (Note on, note off, control change mają po 3 bajty, ale już program change ma 2 bajty, z kolei sysex może ich mieć dowolnie dużo), co wpisuje się w problem odbioru komunikatów o różnej długości.
Ale na razie nie będę mailował, najpierw wypróbuje powyższą metodę 🙂
Mateusz Salamon · 17/11/2021 o 15:37
Gubi, bo coś gdzieś za długo trwa. Jak przyjdą Ci dane po DMA to musisz je NATYCHMIAST wyciągnąć, aby zrobić miejsce dla kolejnych.
remzibi · 25/09/2021 o 13:27
Sprawdzilem na f103 i dziala b.dobrze, a na np. f042 nie dziala, po przerwaniu mamy poprawny size a w buforze tylko jeden ostatni znak na pierwszej, czyli gdzies biblioteka na M0 nie incrementuje pointera bufora 🙁 ,a wMX na 100% zaznaczony incr. przy memory
macjan · 06/10/2021 o 08:55
Potwierdzam, u mnie na Nucleo F103RB dokładnie ten sam efekt, tylko ostatni znak na pierwszej pozycji.
domints · 16/07/2023 o 16:03
U mnie działa 🙂 Dokładnie w F042. Pułapką tu jest HT – gdy odbywa się transfer równy co najmniej połowie długości bufora to dostajesz dwa wywołania callbacka – najpierw na pół bufora, a potem na cały pakiet. Możesz albo spróbować wyłączać ten HT (jak w pierwszym komentarzu), albo go obsłużyć.
A biblioteka nie incrementuje, bo zawsze zapisuje od pierwszego elementu 🙂
Nowy · 17/09/2021 o 12:11
A kiedy coć o CAN/ FDCAN
Olek · 09/06/2021 o 10:56
Cześć,
Z tego co widzę można nawet to uruchomić w trybie circular. Dzięki temu nie będziemy musieli uruchamiać w callbacku odbioru. Jedyna różnica, że Size nie podaje ilości odebranych danych od ostatniego przerwania, tylko pozycję w buforze, więc musimy zapamiętać poprzednią pozycję i odpowiednio kopiować dane.
Pozdrawiam,
Mateusz Salamon · 28/06/2021 o 12:08
O, tego nie zbadałem jeszcze. Dzięki za wskazówkę z Size 🙂
Kamil · 25/08/2021 o 20:28
Też to zauważyłem :). Size pokazuje pozycję w buforze.
domints · 16/07/2023 o 16:04
Podpowiedziałbyś jak to zrobić? Dokumentacja do HAL w tej materii jest bardzo mocno średnia. W tym momencie sam kopiuję do swojego bufora kołowego, ale jeżeli mógłbym uniknąć tego kroku to mogłoby zaoszczędzić sporo cykli procesora 🙂