STM32 F1 Odbiór UART za pomocą DMA.
Istniejący artykuł.
Nie będę ściemniał, nie jestem w pełni autorem tego pomysłu.
Główna idea pochodzi ze znanego bloga msalamon.pl: https://msalamon.pl/odbieranie-uart-z-dma-na-f103-to-rowniez-jest-proste/
Natomiast do swoich potrzeb ideę tę zmodyfikowałem i chciałbym się tą modyfikacją podzielić.
Różnica
No dobra, to zacznijmy od tego na czym polega owa modyfikacja.
Oryginalne rozwiązanie wykorzystuje podwójne buforowanie i DMA uruchamiane za pomocą przerwania IDLE.
W swoim rozwiązaniu, ze względu na duże baud-rate’y potrzebowałem maksymalnie szybkiego rozwiązania z minimalną ingerencją procesora. Dlatego zrezygnowałem z podwójnego buforowania na rzecz pojedynczego bufora. Do tego wykorzystałem pewien tryb DMA, który pozwolił mi zapomnieć o ciągłym ponownym uruchamianiu DMA w przerwaniach.
Mianowicie DMA posiada tryb zwany Circular. Jak się można domyślić z nazwy, kopiowanie jest realizowane w kółko – jeżeli DMA dojdzie do końca bufora do którego zapisuje dane, zaczyna od początku bez potrzeby jego ponownego uruchamiania.
Co nam to daje? A to, że gdy przychodzi znak do naszego interfejsu UART, DMA natychmiast go przepisuje do naszego bufora kołowego, dlatego nie musimy martwić się w żaden sposób o odczyt danych z UART i ich zapis do pamięci. Robi się samo – magia 🙂
Rozwiązanie
Dość krótkiej teorii, przejdźmy do praktyki.
Na dzień dobry musimy poustawiać nasze peryferia – w Cube ustawiamy zegary, uruchamiamy nasz interfejs UART i dodajemy do niego DMA:
Należy zwrócić uwagę na tryb DMA ustawiony na Cuircular. Baud Rate na screenie ma dość niską wartość bo pisząc ten artykuł wykorzystałem to rozwiązanie w innym projekcie, gdzie odczytuje dane z GPS.
Po wygenerowaniu projektu mamy skonfigurowany UART oraz odbiorcze DMA. Teraz pozostaje nam tego użyć. Tak na prawdę nie potrzebujemy żadnych przerwań, chyba że chcemy obsłużyć błędy samego UART’a. Ale zacznijmy od samego obioru danych i ich obsługi.
Na początku przydałaby się nam struktura zawierająca trzy rzeczy: wskaźnik na naszego UART’a, bufor i informację o położeniu ogona bufora. Ogonem będę nazywał pozycję gdzie mamy najstarsze dane, głową miejsce gdzie dane są najświeższe.
#define UARTDMA_RX_BUFFER_SIZE 512 //Macro z pojemnością bufora.
typedef struct
{
UART_HandleTypeDef* huart; // UART handler
uint8_t DMA_RX_Buffer[UARTDMA_RX_BUFFER_SIZE]; // DMA buffer
uint16_t UartBufferTail; //DMA buffer tail
} UARTDMA_HandleTypeDef;
Na pierwszy rzut oka można zauważyć, że brakuje nam głowy naszego bufora kołowego. O tym za chwilę. Na tą chwilę musicie mi uwierzyć, że to wystarczy.
Do podstawowej obsługi przychodzących danych potrzebujemy także kilku funkcji:
void UARTDMA_Init(UARTDMA_HandleTypeDef *huartdma, UART_HandleTypeDef *huart); //Inicjalizacja naszego bufora i uruchomienie DMA
uint16_t UARTDMA_GetDataCount(UARTDMA_HandleTypeDef *huartdma); //Pobranie ilości znaków znajdujących się w buforze.
int UARTDMA_GetCharFromBuffer(UARTDMA_HandleTypeDef *huartdma); //Pobranie kolejnego znaku z bufora.
Zacznijmy więc od inicjalizacji:
void UARTDMA_Init(UARTDMA_HandleTypeDef *huartdma,
UART_HandleTypeDef *huart) {
huartdma->huart = huart; // Wpisanie wkaźnika na UART z którym pracujemy.
huartdma->UartBufferTail = 0; //Wstępne ustawienie ogona, zaczynamy od 0.
HAL_UART_Receive_DMA(huartdma->huart, (uint8_t*) huartdma->DMA_RX_Buffer,
UARTDMA_RX_BUFFER_SIZE); // Uruchomienie DMA na naszym buforze
}
Po uruchomieniu funkcji UARTDMA_Init DMA zacznie odbierać dane przychodzące z portu szeregowego i przepisywać je w kółko do bufora. Od tej pory każdy odebrany znak ląduje w kolejnej komórce pamięci naszego bufora, bez żadnej ingerencji z naszej strony. W przypadku gdy DMA dojedzie do końca bufora, zaczyna od początku nadpisując poprzednie znaki. Tak karuzela się kręci, napędzają ją jedynie znaki przychodzące na port szeregowy mikrokontrolera.
Dobrze, dane nam się zapisują, przejdźmy więc do odczytu. Tutaj też pojawi się nam „głowa” naszego bufora. Zacznijmy od policzenia naszych znaków w buforze, wprowadzę tutaj funkcję pomocniczą UARTDMA_GetHead, która zwróci aktualną pozycję DMA w buforze. DMA posiada rejestr CNDTR, który przechowuje informację o ilości pozostałych znaków do skopiowania. Odjęcie wartości tego rejestru od całkowitej wielkości bufora da nam aktualną pozycję początku bufora (tam gdzie znajduje się najnowszy odebrany znak) – tak znajdujemy naszą głowę. W dodatku gdy wartość rejestru CNDTR osiągnie wartość 0 zostanie on zresetowany do wartości początkowej a DMA zacznie kopiowanie od początku.
uint16_t UARTDMA_GetHead(UARTDMA_HandleTypeDef *huartdma) {
uint32_t cndtr = __HAL_DMA_GET_COUNTER(huartdma->huart->hdmarx);
return (UARTDMA_RX_BUFFER_SIZE - cndtr);
}
uint16_t UARTDMA_GetDataCount(UARTDMA_HandleTypeDef *huartdma) {
uint16_t currentHead = UARTDMA_GetHead(huartdma);
if (currentHead >= huartdma->UartBufferTail) {
return (currentHead - huartdma->UartBufferTail);
} else {
return (UARTDMA_RX_BUFFER_SIZE - huartdma->UartBufferTail + currentHead);
}
}
Funkcja UARTDMA_GetDataCount uwzględnia dwa przypadki wzajemnego położenia głowy i ogona w buforze i w zależności od tego, czy głowa ma indeks większy od ogona czy też mniejszy, zwraca wynik odpowiedniego działania.
Mamy już policzone znaki w buforze, teraz czas je przeczytać. Do tego służyć będzie ostatnia funkcja:
int UARTDMA_GetCharFromBuffer(UARTDMA_HandleTypeDef *huartdma) {
uint16_t currentHead = UARTDMA_GetHead(huartdma);
if (currentHead == huartdma->UartBufferTail) {
return -1; // błąd, bufor pusty :(
}
uint8_t return_value = huartdma->DMA_RX_Buffer[huartdma->UartBufferTail];
huartdma->UartBufferTail =
(huartdma->UartBufferTail + 1) % UARTDMA_RX_BUFFER_SIZE;
return return_value;
}
Należy zwrócić uwagę na typ zwracany przez funkcję UARTDMA_GetCharFromBuffer. Jest on taki nieprzypadkowo, ponieważ chcielibyśmy się dowiedzieć kiedy nasz bufor jest pusty. W takim wypadku zostanie zwrócona wartość -1. W każdym innym przypadku dostaniemy wartość odczytaną z naszego bufora. Dodatkowo funkcja automatycznie przesunie nasz ogon o jeden znak do przodu => odczytane-zapomniane.
Na koniec nasuwa się ważne pytanie: Co z przepełnieniem bufora?! 😮
A no nic. Jeżeli chcemy bazować na najświeższych danych prawdopodobnie będziemy chcieli wyzerować bufor. Otóż jak się przyjrzymy to w momencie gdy głowa bufora dogoni ogon, to bufor zresetuje się sam (head == tail) i zaczną pojawiać się w nim nowe znaki od początku.
Podsumowując, jest to bardzo uproszczone rozwiązanie w dodatku nie wymagające większego zachodu z obsługą przerwań, kopiowaniem czy też przenoszeniem danych. Wszystko robi się samo, a my możemy się skupić na przyjemnym czytaniu danych. 🙂 W dodatku nasz procesor sobie w tym czasie wypocznie, chyba że zaprzęgniemy go do innych zadań…
0 komentarzy