There are multiple ways to setup the STM32 UART for data reception, including polling, interrupts and using DMA. Polling is simple, you just keep checking the UART for data. However, this is a “blocking” function and ties-up the CPU from doing anything else. Using interrupts to check for data reception is a step-up from polling, but still relies on making many interrupt calls to the CPU. One way this is implemented is to setup an interrupt to fire when a new byte is received on the UART, then process it. In the case of GNSS data (GPS, Galileo, etc.), this could mean firing several hundred times per second. Doable, but there seems to be a better way; DMA.
Direct Memory Access, or DMA, appears to be the least CPU intensive option as the CPU isn’t involved in the data transfer itself. Using the DMA we can basically setup this memory buffer off to the side, let it catch data, then process it later.
The DMA will place data into a buffer that needs to be created, such as:
uint8_t buffer[200];
The DMA can be setup to write to the buffer in “normal” mode or “circular” mode. In normal mode, DMA writes to the end of the buffer and stops. In contrast, when in “circular” mode, the DMA will continue writing to the buffer in a loop. Once it reaches the end of the buffer, it begins again, overwriting any data from the beginning of the buffer. This works well for receiving and processing data sentences of varying lengths.
Configuration:
- In STM32CubeIDE, setup a USART in asynchronous mode (USART1 used here)
- Under DMA Settings, Add a new DMA for receiving, use “Circular” mode
- Under NVIC Settings, ensure USART and DMA interrupts are enabled
To initiate the DMA transfer of UART data, we’ll use the following function placed before the while(1) loop.
// receive until idle, then trigger interrupt HAL_UARTEx_ReceiveToIdle_DMA(&huart1, buffer, sizeof(buffer)); while (1)
Outside of the “main” function we need to place the interrupt handler:
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { // put code here // don't need to call this if using DMA circular mode // HAL_UARTEx_ReceiveToIdle_DMA(&huart1, buffer, sizeof(buffer)); }
Calling HAL_UARTEx_ReceiveToIdle_DMA function will enable three interrupts:
- DMA Transfer Complete (TC) – fires when buffer is full
- DMA Half Transfer (HT) – fires when buffer is 50% full
- UART Idle Line (IDLE) – fires when the UART is idle for at least one byte worth of time
- The IDLE interrupt indicates that the UART line is no longer transmitting data.
In my case, I’m only interested in the IDLE interrupt. Since the other two interrupts will still fire when appropriate, those need to be disabled. We can add two lines below the HAL_UARTEx_ReceiveToIdle_DMA function call to disable the TC and HT interrupts:
// receive until idle, then trigger interrupt HAL_UARTEx_ReceiveToIdle_DMA(&huart1, buffer, sizeof(buffer)); // receive until idle, then trigger interrupt __HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_HT); // Disables "Half Transfer" interrupt __HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_TC); // Disables "Transfer Complete" interrupt while (1)
Overrun Error Handling
One issue I’ve run into is if there is data present on the UART RX line during startup (like if the GNSS is powered-on at the same time and immediately begins sending data), it will trigger an error flag, specifically ORE or Overrun Error, which will prevent DMA from working. This appears to happen prior to the HAL_UARTEx_ReceiveToIdle_DMA function being called. The fix thus far for this is to clear the error flag just before calling HAL_UARTEx_ReceiveToIdle_DMA. The updated code:
// Check if ORE flag is set, which can happen if data is present on UART RX line if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_ORE)) { __HAL_UART_CLEAR_FLAG(&huart1, UART_CLEAR_OREF); } // receive until idle, then trigger interrupt HAL_UARTEx_ReceiveToIdle_DMA(&huart1, buffer, sizeof(buffer)); // receive until idle, then trigger interrupt __HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_HT); // Disables "Half Transfer" interrupt __HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_TC); // Disables "Transfer Complete" interrupt while (1)
The IDLE interrupt is a great option for receiving the GNSS data sentences, which can vary in length. As long as the GNSS module is setup to provide at least 1 byte worth of space, I shouldn’t have any problem receiving all data within the circular buffer and keeping them separated.
Now, data being received from the UART will be directly written into our buffer. After each data sentence is completed, the IDLE interrupt will fire and call the interrupt handler created earlier. The handler has a variable called Size, which tells us the position of the last byte received within our buffer. This will be helpful for keeping track of sentence lengths and start/stop positions for processing purposes. Here is an example for visualization:
The head and tail can be set based on personal preference to some extent. In my example, while the actual head of the data is located at BYTE[4], because Size is returned as 5, it’s quite convenient to set Head equal to this value. Alternatively I could use Head = Size – 1, but then on the next pass I couldn’t just set Tail = Head, I’d have to do Tail = Head + 1. Simply using Size to mark the Head of the current sentence and the Tail of the next sentence seems straight forward enough for me.
Let’s read some GNSS data.
I had created some variables to keep track of the head and tail positions for the circular buffer. The head position will be updated within the callback function for our IDLE event. In the main loop I’ll watch for the case when Head != Tail, then process the data between the current tail and head.
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) // Size=position of last byte received { uart1BufferHead = Size; // store position of last byte received, this is our "head" }
For processing the data, another function will be called in the main loop. Within that function I’ll update the tail position based on the amount of buffer bytes processed. The tail update will look something like:
// Update tail uart1BufferTail = (uart1BufferTail + (6 + payloadLength + 2)) % size;
In my case, I’m processing a message from the U-Blox GNSS module. It starts with 6 bytes of header information, followed by a various number of “payload” bytes, ending with 2 checksum bytes. I’ve formatted the code to make that easily readable and identifiable for myself. The modulo (%) operation compares the size of the newly calculated tail against the buffer “size”. This will automatically “wrap” the tail value to the beginning of the buffer. For example, if the buffer size is 200 bytes (array positions [0] to [199]), but the above calculation resulted in a tail position of [205], the modulo would change the final value to [5]. Slick!
Eventually, the tail position should equal the head position once all the “new” buffer data has been read. At that point the program should stop processing data.
Changing UART baud rate after startup (on the fly)
In my application, I must initialize the UART at baud rate 9600 due to the default baud rate of the U-Blox GNSS module. Once UART is started, I can send an update message to the module to change it’s baud rate to 115200. After that, I need to change the STM32’s baud rate to match. This is easily accomplished by this code:
// Reinitialize UART at 115200 baud HAL_UART_DeInit(&huart1); // Deinitialize before reinitializing huart1.Init.BaudRate = 115200; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } HAL_Delay(100);
Note, when changing the baud rate, I found the DMA had to be re-started as well (call the HAL_UARTEx_ReceiveToIdle_DMA function again and disable the desired interrupts as done before).
Sites I found helpful on this topic:
https://www.steppeschool.com/pages/blog/stm32-uart-polling-dma
https://stm32world.com/wiki/STM32_UART_DMA_Idle_Detection
https://deepbluembedded.com/stm32-uart-receive-unknown-length-idle-line-detection-examples/
