fbpx

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

Najpierw  mam małą uwagę. Zauważyłem, że najnowszy CubeMX v5.4.0 zamienia kolejność inicjalizacji DMA i UART2 co skutkuje niepoprawnym działaniem DMA! Upewnij się, że przed pętlą while(1) masz tak, jak poniżej.

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();        // <- WAZNE ABY DMA BYLO PRZED USART2!!!
  MX_USART2_UART_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
    while (1)
    {

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

typedef struct
{
  UART_HandleTypeDef* huart; // UART handler

  uint8_t DMA_RX_Buffer[DMA_RX_BUFFER_SIZE]; // DMA direct buffer
  uint8_t UART_Buffer[UART_BUFFER_SIZE]; // UART working circular buffer

  uint16_t UartBufferHead;
  uint16_t UartBufferTail;
  uint8_t UartBufferLines;
}UARTDMA_HandleTypeDef;

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:

void UARTDMA_Init(UARTDMA_HandleTypeDef *huartdma, UART_HandleTypeDef *huart);

void UARTDMA_UartIrqHandler(UARTDMA_HandleTypeDef *huartdma);
void UARTDMA_DmaIrqHandler(UARTDMA_HandleTypeDef *huartdma);

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

void UARTDMA_UartIrqHandler(UARTDMA_HandleTypeDef *huartdma);

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.

void UARTDMA_DmaIrqHandler(UARTDMA_HandleTypeDef *huartdma);

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

void USART2_IRQHandler(void);

void DMA1_Stream5_IRQHandler(void)

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

/**
* @brief This function handles DMA1 stream5 global interrupt.
*/
void DMA1_Stream5_IRQHandler(void)
{
/* USER CODE BEGIN DMA1_Stream5_IRQn 0 */

/* USER CODE END DMA1_Stream5_IRQn 0 */
//HAL_DMA_IRQHandler(&hdma_usart2_rx);
/* USER CODE BEGIN DMA1_Stream5_IRQn 1 */
  UARTDMA_DmaIrqHandler(&huartdma);
/* USER CODE END DMA1_Stream5_IRQn 1 */
}

/**
* @brief This function handles USART2 global interrupt.
*/
void USART2_IRQHandler(void)
{
/* USER CODE BEGIN USART2_IRQn 0 */

/* USER CODE END USART2_IRQn 0 */
//HAL_UART_IRQHandler(&huart2);
/* USER CODE BEGIN USART2_IRQn 1 */
  UARTDMA_UartIrqHandler(&huartdma);
/* USER CODE END USART2_IRQn 1 */
}

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.

/* USER CODE BEGIN Includes */
#include "UART_DMA.h"
/* USER CODE END Includes */

/* USER CODE BEGIN EV */
extern UARTDMA_HandleTypeDef huartdma;
/* USER CODE END EV */

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

uint8_t UARTDMA_IsDataReady(UARTDMA_HandleTypeDef *huartdma);
int UARTDMA_GetLineFromBuffer(UARTDMA_HandleTypeDef *huartdma, char *OutBuffer);

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.

/* USER CODE BEGIN PD */
UARTDMA_HandleTypeDef huartdma;
/* USER CODE END PD */

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

/* USER CODE BEGIN PV */
char ParseBuffer[8];
/* USER CODE END PV */

Pamiętaj zainicjować UART z odbiorem DMA

/* USER CODE BEGIN 2 */
UARTDMA_Init(&huartdma, &huart2);
/* USER CODE END 2 */

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

/* USER CODE BEGIN WHILE */
while (1)
{
  if(UARTDMA_IsDataReady(&huartdma))
  {
    UARTDMA_GetLineFromBuffer(&huartdma, ParseBuffer);
    if(strcmp(ParseBuffer, "ON") == 0)
    {
      HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_SET);
    }
    else if(strcmp(ParseBuffer, "OFF") == 0)
    {
      HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET);
    }
  }
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
}
  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.

/**
* @brief This function handles DMA1 stream5 global interrupt.
*/
void DMA1_Stream5_IRQHandler(void)
{
/* USER CODE BEGIN DMA1_Stream5_IRQn 0 */

  UARTDMA_DmaIrqHandler(&huartdma);
  return;

/* USER CODE END DMA1_Stream5_IRQn 0 */
  HAL_DMA_IRQHandler(&hdma_usart2_rx);
/* USER CODE BEGIN DMA1_Stream5_IRQn 1 */

/* USER CODE END DMA1_Stream5_IRQn 1 */
}

/**
* @brief This function handles USART2 global interrupt.
*/
void USART2_IRQHandler(void)
{
/* USER CODE BEGIN USART2_IRQn 0 */

  UARTDMA_UartIrqHandler(&huartdma);
  return;

/* USER CODE END USART2_IRQn 0 */
  HAL_UART_IRQHandler(&huart2);
/* USER CODE BEGIN USART2_IRQn 1 */

/* USER CODE END USART2_IRQn 1 */
}

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.6/5 - (12 votes)

Podobne artykuły

.

8 komentarzy

GREGO · 27/03/2020 o 13:04

Fajnie, że są takie artykuły – dzięki.
Z tym returnem genialne:) a ja całe życie robiłem tak:
void DMA1_Stream5_IRQHandler(void)
{
/* USER CODE BEGIN DMA1_Stream5_IRQn 0 */

UARTDMA_DmaIrqHandler(&huartdma);
#if (0)
/* USER CODE END DMA1_Stream5_IRQn 0 */
HAL_DMA_IRQHandler(&hdma_usart2_rx);
/* USER CODE BEGIN DMA1_Stream5_IRQn 1 */
#endif
/* USER CODE END DMA1_Stream5_IRQn 1 */
}

    Mateusz Salamon · 27/03/2020 o 19:04

    W sumie to można i tak to usuwać. Może nawet lepiej… nie wiem co kompilator robi po moim returnie. Mam nadzieję, że ucina kod 🙂

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

      Maciek · 12/12/2019 o 20:05

      Pod tym linkiem (https://github.com/olikraus/u8g2/wiki) jest bardzo ciekawa i bogata biblioteka obsługująca co najmniej kilkanaście sterowników graficznych. Biblioteka oferuje szereg krojów czcionek i elementy grafiki w trybie buforowanym, choć to tylko czubek góry lodowej możliwości.
      Testowałem tę bibliotekę na wyświetlaczu 1.3″ z interfejsem SH1106 zakupionym w sklepie Mateusza i z ciekawości na BluePill z poziomu Arduino IDE. Działa świetnie, praktycznie zero opóźnień. A co gdyby przeportować ją na STM32/HAL?.( Ktoś już podjął próbę dla STM32F103 – https://github.com/nikola-v/u8g2_template_stm32f103c8t6).
      Mateusz może to jest również temat na artykuł :). Wyświetlacze graficzne, w tym OLED, to gorący temat.

      Mateusz Salamon · 12/12/2019 o 20:08

      Dzięki za komentarz. Noo przeportowanie tej biblioteki i dołączenie do tego projektu to trochę taki mój mokry sen 😀 Jest bardzo fajna, ale jeszcze z HALem nie próbowałem używać 🙁 Może bym się jej podjął próby odpalenia na HAL/DMA
      i przy okazji np. zrobił Live na YT? Byłoby ciekawie 😀

Fonak · 02/11/2019 o 08:31

Witam,. Bardzo dobry artykuł.

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

    Dzięki 🙂

Dodaj komentarz

Avatar placeholder

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