Ostatnio na mojej grupie mailowe zadałem pytanie dotyczące tego, jaki temat najbardziej Was w tej chwili interesuje z STM32. Otrzymałem mnóstwo różnych odpowiedzi, ale jeden temat powtórzył się kilkukrotnie. Było to odbieranie dowolnej długości wiadomości z UART po DMA. Skoro czytelnik chce – to piszę!

Wokół tego tematu narosło wiele mitów. Jedni mówią, że się nie da. Inni z kolei twierdzą, że to ogromny wysiłek. Nie mam pojęcia skąd to się wzięło. Być może po części spowodowane jest to tym, że biblioteki HAL w ogóle nie biorą pod uwagę bardzo ważnego przerwania UART IDLE, które jest tutaj niezbędne. Może też dlatego, że jest to możliwe na kilka sposobów i każdy ma swoje wady i zalety.

Jak odbierać UART po DMA?

Jak wiesz lub właśnie się dowiadujesz – w trakcie uruchamiania odbioru DMA musimy zadeklarować ilość znaków po odebraniu których zostanie wygenerowane przerwanie od zakończenia transferu DMA. W połowie też wyskoczy przerwanie od odebrania połowy zadeklawowanej ilości danych.

Komunikacja UART ma to do siebie, że komunikaty z urządzeń często mają różną długość. Weźmy na przykład taki GPS, który opisywałem niedawno tutaj. Jeżeli nie ma fix’a, komunikaty będą bardzo krótkie bez danych między rozdzielnikami w postaci przecinka. Po złapaniu sygnału GPS, komunikaty mogą mieć kilkadziesiąt znaków. W dodatku różne typy wiadomości też mają różną długość. Skąd masz wiedzieć jakiej długości będzie kolejna wiadomość?

Można po prostu czekać na przepełnienie podanej sztywno ilości danych. Wtedy wpadnie jedna, dwie lub więcej wiadomości, zanim wyzwoli się przerwanie DMA. A co jeśli już nie nadejdzie żadna wiadomość, a ta ostatnia była krytyczna? No nie jest to najwygodniejsza sytuacja, co nie?

Przerwanie UART IDLE LINE

Mikrokontrolery STM32 posiadają mnóstwo ciekawych przerwań. Jednym z takich ciekawszych jest UART IDLE LINE. Może ono służyć do komunikacji multi-master, ale również może nam posłużyć do detekcji końca wiadomości.

Jak ono działa? Otóż po wykryciu pierwszego nadchodzącego znaku na UART, uruchamiane jest badanie aktywności linii odbiorczej. Jeżeli na tej linii nie będzie żadnej aktywności przez czas równy długości jednego znaku, zostanie podniesione przerwanie. Czy już widzisz zastosowanie?

Dajmy na to, że idzie do mikrokontrolera po UART równe 20 znaków. Po czasie 21-go znaku dostaniemy przerwanie IDLE. Koniec wiadomości wykryty. Co dalej?

Koniec wiadomości a sprawa z DMA

No dobra, ale co ma do tego DMA? Podczas researchu widziałem kilka sposobów na implementację. Jedna ciekawa polegała na cyklicznym startowaniu DMA dajmy na to po 10 znaków. Moim zdaniem generuje to sporo niepotrzebnej roboty z restartowaniem DMA. Z drugiej strony potrzebuje niewiele RAMu i jest kompletnie niezależne od długości komunikatów UART.

Moją propozycją jest bufor DMA, który jest w stanie pomieścić najdłuższą możliwą wiadomość, której możemy się spodziewać. Skąd w takim razie weźmiemy przerwanie DMA? Otóż jest taka fajna zależność, że zatrzymując “pędzące” DMA generujesz przerwanie od zakończenia transmisji. I to jest klucz do sukcesu!

Wystarczy w przerwaniu IDLE zatrzymać DMA, aby wejść do obsługi przerwania zakończenia transferu DMA. Magia!

Obsługa przerwania DMA

Są dwa “niestety”. Jedno polega na tym, że biblioteki HAL nie obsługują przerwania IDLE. Należy napisać sobie własne i podmienić standardową obsługę przerwania w HALu. Drugie niestety polega na tym, że Cube zawsze będzie próbował przywrócić domyślną obsługę przerwania przy regenerowaniu projektu i należy o tym pamiętać.

Jest też spory plus w HALu. Generuje on osobne handlery dla każdego z UART oraz każdego z kanałów DMA. Podmiana przerwania jest więc nieszkodliwa dla innych przerwań.

Okej wystarczy teoretyzowania. Zapraszam Cię do zapoznania się z realizacją.

Projekt odbioru UART po DMA

Kod napisałem na Nucleo F411RE z użyciem STM32CubeIDE 1.0.2 oraz bibliotekami HAL w wersji 1.24.1. Schematu tym razem nie będzie. Nic dodatkowego nie podłączyłem do płytki. 

Projekt wygeneruj z zainicjowanymi standardowymi peryferiami. Łatwo dokonasz tego wybierając przy tworzeniu projektu płytkę Nucleo zamiast samego mikrokontrolera.

Skoro wybrałeś domyślne peryferia, to masz aktywowany już UART oraz wbudowaną diodę. Użyjemy dzisiaj to i to.

Jednak UART musisz dokonfigurować. Skonfiguruj żądanie DMA USART2_RX w trybie Normal. Do tego dorzuć w zakładce NVIC globalne przerwanie USART2. Poniżej zobacz jak to wygląda na screenach.

I to tyle po stronie Cube’a. Przejdźmy do kodu. Przygotowałem bibliotekę do obsługi całości. Pozwala ona na używanie więcej niż jednego UARTu w trybie odbioru DMA. Wymaga jednak  kilku kroków i modyfikacji wygenerowanego kodu, aby mogła działać.

Kod

Zacznę od omówienia struktury, która jest rdzeniem każdego z UARTa którego chcesz użyć do odbioru DMA.

Zawiera ona w sobie uchwyt do UARTa, dwa bufory:

  • Dedykowany DMA do odbioru aktualnej wiadomości
  • Bufor cykliczny UART, do którego trafiają kolejne wiadomości z bufora DMA (po zakończeniu transferu)

a także kilka zmiennych pomocniczych do obsługi bufora cyklicznego oraz wskaźnik ilości pełnych linii (ilość wiadomości zakończonych znakiem ‘\n’).

Z punktu mechanizmu detekcji nadchodzących wiadomości UART są 3 funkcje:

W inicjalizacji operuję przerwaniami:

  • Włączam przerwanie UART IDLE
  • Włączam przerwanie DMA TC (Transfer Complete)
  • Wyłączam przerwanie DMA HT (Half Transfer) – nie będzie potrzebne, a nawet może przeszkadzać
  • Uruchamiam odbiór UART DMA na dedykowany bufor

Do tej funkcji musisz przekazać wskaźnik na utworzoną wcześniej strukturę (na przykład w main’ie).

Ta funkcja jest obsługą przerwań od UART. Sprawdzam w niej czy przerwanie zostało wywołane przez IDLE. Jeżeli tak to odczytuję odpowiednie rejestry w celu skasowania flagi i zatrzymuję DMA. Zostaje automatycznie podniesione przerwanie TC DMA.

Teraz najważniejsza funkcja z punktu widzenia całego kodu. Jesteśmy w stanie, kiedy na UART przestały być nadawane znaki. Jest to sygnał, że prawdopodobnie otrzymaliśmy cały komunikat. Prawdopodobnie, bo możemy mieć do czynienia z programowym UARTem, który nie trzyma czasów. Niestety przerwanie IDLE jest bezwzględne i bierze pod uwagę bezczynność tylko podczas trwania jednego znaku.

Co się w nim takiego dzieje? Po pierwsze odczytuję rejestry przerwań i sprawdzam, czy mam do czynienia z przerwaniem Transfer Complete. Jeżeli tak to:

  1. Czyszczę flagę przerwania (HAL domyślnie w swoich obsługach robi to za nas, ale to jest NASZA obsługa)
  2. Sprawdzam ile znaków przyszło po DMA przy pomocy rejestru NDTR.
  3. Kopiuję z bufora DMA w kolejne wolne miejsca bufora cyklicznego UART. Jest to bufor kołowy, więc wpisywane dane będą się zawijały. Przy okazji sprawdzam, czy aktualnie kopiowany znak nie jest przypadkiem końcem linii ‘\n’. Jeśli tak, to inkrementuję odpowiednią zmienną w strukturze.
  4. Czyszczę przerwania
  5. Ustawiam rejestry DMA na początek bufora DMA i wstawiam ponownie ilość znaków do odbioru, która jest równa rozmiarowi bufora DMA.
  6. Startuję ponownie DMA

I tak kręci się w kółko. Kolejne komunikaty lądują w wolnych miejscach bufora UART.

Jednak aby to się kręciło musimy podmienić przerwania! Jak tego dokonać? Na celownik weźmiemy plik stm32fxx_it.c

Dla naszej konfiguracji szukamy dwóch funkcji związanych z USART2 i DMA1 kanał 5. Są to

Zauważ, że jest tam domyślnie HALowa obsługa przerwań HAL_DMA_IRQHandler i HAL_UART_IRQHandler. Nie chcemy tego, więc zakomentuj je. Przypominam, że po regeneracji projektu one wrócą! Edit – poniżej dopisałem jak to rozwiązać bardziej elegancko. Z nowym sposobem, przerwania domyślne nie będą przeszkadzać nawet po powrocie 😉

Dodaj teraz nasze funkcje w odpowiednie miejsca. U mnie wygląda to tak

Jest jeszcze jedno. Plik ten nie będzie znał tych funkcji, a tym bardziej naszej struktury. Musisz mu je podsunąć w sekcji użytkownika na samej górze pliku.

I tyle. Będzie działać 🙂 Teraz przydałoby się coś w mainie dorzucić, aby reagować na to, co przychodzi na UART. Nie będę tutaj nic wielkiego wymyślał. Będę odbierał dwa komunikaty “ON” i “OFF” które odpowiednio włączą i wyłączą mi wbudowaną w Nucleo diodę.

Do sprawdzania, czy jest coś w kolejce do parsowania oraz do pobrania linii danych posłużą mi dwie funkcje

Ich nazwy chyba mówią same za siebie. Jeszcze tylko kawałek kodu w mainie. Pamiętaj, że potrzebujesz utworzyć zmienną strukturalną dla całej machiny.

Przydałby się tez jakiś mały buforek do parsowania pobieranych linii.

Pamiętaj zainicjować UART z odbiorem DMA

No i na koniec pozostało nam pobranie danych i parsowanie

  1. Sprawdzam, czy jest jakaś linia do przeparsowania
  2. Jeśli tak, to pobieram ją w całości
  3. Prostym porównaniem strcmp sprawdzam co to za komunikat i odpowiednio reaguję
  4. Cieszę się z działania

Trudne? Oczywiście, że nie 🙂 Zerknijmy jeszcze na czasy, bo to zawsze jest ciekawe. Odbiór dwóch znaków:

Obsługa przerwania UART IDLE to 1,437 µs, a DMA TC 4,125 µs. Piekielnie szybko biorąc pod uwagę, że te czasy zawierają w sobie czas obsługi GPIO. Swoją drogą muszę porównać obsługę niektórych peryferiów z HAL i “na rejestrach”. Co Ty na to?

A co jak puszczę 50 znaków?

Obsługa UART IDLE nie uległa zmianie – nie zależy od długości wiadomości. Zmienił się czas potrzebny na obsługę przerwania DMA TC i jest to teraz 33,975 µs. Wzrost bierze się z konieczności przepisania większej ilości znaków do bufora cyklicznego UART.

Myślę, że te czasy są bardzo dobre.

EDIT – obsługa przerwań

Dostałem podpowiedź na Facebooku jak lepiej rozwiązać problem domyślnej obsługi przerwań HALowskich. Wystarczy wsadzić “nasze” przed fabryczne i od razu wyjść returnem 🙂 Takie proste, a nie wpadłem na to pisząc kod. Nie będę usuwał mojego pierwotnego pomysłu mimo, że nie jest zbyt elegancki.

Podsumowanie

W Internecie znajdziesz mnóstwo pomysłów na wykonanie UART over DMA. Myślę, że to, co zrobiłem jest jedną z prostszych realizacji. Wymaga ona trochę RAMu na dwa bufory, ale w STM32 mamy go pod dostatkiem 🙂 Zresztą jak spodziewasz się krótkich wiadomości to nie potrzebujesz sporych buforów.

Mam nadzieję, że artykuł Ci się spodobał i zrozumiałeś jak to działa. Gdybyś miał jakieś wątpliwości i pytania to sekcja komentarzy jest Twoja. Będę wdzięczny, jeśli podzielisz się tym artykułem ze znajomymi. Niech dobre treści się niosą jak najszerzej.

Temat UART i DMA będzie również poruszony w moim kursie STM32 dla początkujących. Tam dzięki wideo będę miał lepsze narzędzia, aby o tym opowiedzieć szerzej. Oczywiście nie zabraknie bardziej skomplikowanego zastosowania w przyjemnej formie. Jeżeli jeszcze nie zapisałeś się do mojego newslettera, to zapraszam. Kliknij w baner, aby dołączyć do listy oczekujących na kurs.

To w mailach otrzymasz bieżące informacje oraz masz realny wpływ na to, co dzieje się nie tylko w kursie, ale i na blogu.

kurs stm32

Pełny projekt wraz z biblioteką znajdziesz jak zwykle na moim GitHubie: link

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.


4 Komentarze

Optimex · 04/11/2019 o 23:21

Zgadzam się z przedmówcą, przystępna forma i jak zwykle jakiś “magic trick” w tle dzięki czemu częściej wiadomo “jak żyć” w świecie stm32 😀 Przydałby się artykuł dot. jakiejś biblioteki graficznej i budowy prostych GUI w oparciu o np. freeRTOS i będzie, w zestawieniu z poprzednimi wpisami, wszystko co na początek potrzebne.
Pozdrawiam serdecznie.

    Mateusz Salamon · 07/11/2019 o 10:19

    Cześć. Ciekwwych bibliotek graficznych jest kilka i nie wiem którą wybrać 🙁 Będę robił ankietę na liście mailowej

Fonak · 02/11/2019 o 08:31

Witam,. Bardzo dobry artykuł.

    Mateusz Salamon · 02/11/2019 o 08:33

    Dzięki 🙂

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *