Efficient UART Reception Programming Example with DMA and IDLE Based on STM32
The goal of this article: A FreeRTOS programming example based on STM32_H5
According to the description in this article, experiments should be able to be conducted on the corresponding hardware, and similar applications can be derived.
Prerequisites: Basic knowledge of C language, and a development environment with compilation and integration, such as: Keil uVision5
Used peripherals: USART1, USART2, GPIO, SysTick
HAL library version: STM32H5xx HAL Driver version number 1.1.0
STMCubeMX version: 6.10.0
Keil uVision5 version: V5.38.0.0
Experiment Objective
Document project learning, learn UART programming in the project, experience efficient serial reception, and design an experiment to implement serial reception.
Circuit Diagram for Application Scenario
In my application scenario, the content of the circuit diagram is as follows:

I will connect the J4 interface, thus designing an experiment with one serial port for sending and another for receiving.
Three Programming Methods for UART
Based on the UART hardware structure, there are 3 programming methods:
① Polling method:
To send data, first write the data into the TDR register, then check if TDR is empty before returning. Alternatively, you can check if TDR is empty first, then write. To read data, first check if RDR is not empty, then read RDR to get the data.
② Interrupt method:
Using the interrupt method is more efficient and can avoid data loss during reception. To send data, enable the “TXE” interrupt (transmit register empty interrupt). In the TXE interrupt handler, take one data from the program’s send buffer and write it into TDR. When the TXE interrupt occurs again, take the next data from the program’s send buffer and write it into TDR.
For receiving data, enable the “RXNE” interrupt (receive register not empty) from the start. This way, when the UART receives one data, it will trigger an interrupt, and in the interrupt program, read RDR to get the data and store it in the program’s receive buffer. When the program wants to read serial data, it can directly read from the receive buffer. The “send buffer” and “receive buffer” involved here are particularly suitable for using a “circular buffer”.
③ DMA method:
When using the interrupt method, interrupts will occur during data transmission and reception, requiring the CPU to execute the interrupt handler. There is another method: DMA (Direct Memory Access), which can transfer data directly between two devices without CPU involvement.

After setting up DMA (source, destination, address increment/decrement direction, data length for each read, number of reads), DMA will automatically transfer data between SRAM and UART:
① When sending: DMA gets data from SRAM and writes it into the UART’s TDR register
② When receiving: DMA gets data from the UART’s RDR register and writes it to SRAM
③ After the specified data transfer is complete, a DMA interrupt is triggered; during the data transfer process, there are no interrupts, and the CPU does not need to handle anything.
The HAL library APIs used are as follows:
// Polling method: // Send: HAL_UART_Transmit // Receive: HAL_UART_Receive // Interrupt method: // Send: HAL_UART_Transmit_IT HAL_UART_TxCpltCallback // Receive: HAL_UART_Receive_IT HAL_UART_RxCpltCallback // DMA method: // Send: HAL_UART_Transmit_DMA HAL_UART_TxHalfCpltCallback HAL_UART_TxCpltCallback // Receive: HAL_UART_Receive_DMA HAL_UART_RxHalfCpltCallback HAL_UART_RxCpltCallback // Error HAL_UART_ErrorCallback HAL_UART_ErrorCallback
IDLE
IDLE is defined as: no data is received on the bus within the time of one byte. When does the UART IDLE interrupt occur? The RxD pin is initially idle, so does the IDLE interrupt keep occurring? No. When we enable the IDLE interrupt, it does not occur immediately; rather, it occurs after at least one data is received and it is found that no new data has been received within the time of one byte. When we use DMA to receive data, it indeed improves CPU efficiency, but we cannot predict how much data will be received, and we want to process the received data as soon as possible. What should we do? For example, if I want to read 100 bytes of data, but after receiving 60 bytes, the other party stops sending data, what should we do? How do we determine that the data transmission has stopped? We can use the IDLE interrupt. In this case, there are 3 conditions for DMA transfer to end:
① The specified amount of data has been received, for example, 100 bytes of data, and HAL_UART_RxCpltCallback is called
② The bus is idle: HAL_UARTEx_RxEventCallback is called
③ An error occurred: HAL_UART_ErrorCallback is called
The functions that use the IDLE state for reception are:
// Polling method: // Receive: HAL_UARTEx_ReceiveToIdle // Callback function: // Determine if reception is complete based on the returned parameter RxLen, or if it returned due to idleness // Interrupt method: // Receive: HAL_UARTEx_ReceiveToIdle_IT // Callback function: Complete: HAL_UART_RxCpltCallback Aborted due to idleness: HAL_UARTEx_RxEventCallback // DMA method: // Receive: HAL_UARTEx_ReceiveToIdle_DMA // Callback function: Half transfer: HAL_UART_RxHalfCpltCallback Complete: HAL_UART_RxCpltCallback Aborted due to idleness: HAL_UARTEx_RxEventCallback // Error HAL_UART_ErrorCallback
Program Design
① Use the DMA + IDLE interrupt method to receive data, which will store the data in a temporary buffer;
② In the callback function: write the data from the temporary buffer into the queue, then re-enable DMA
③ APP reads the queue: if there is no data in the queue, it blocks.

Serial Port Configuration
Open the configuration tool to configure the serial port


Configure Interrupts

Configure DMA


Code Snippet
After the low-level configuration as described above, we write some code as follows:
xTaskCreate( CH1_UART2_TxTaskFunction, // Function pointer, task function "ch1_uart2_tx_task", // Task name 200, // Stack size, in words, 200 means 800 bytes NULL, // Parameters passed to the task function osPriorityNormal, // Priority NULL); // Task handle, used later to operate this task xTaskCreate( CH2_UART4_RxTaskFunction, // Function pointer, task function "ch2_uart4_rx_task", // Task name 200, // Stack size, in words, 200 means 800 bytes NULL, // Parameters passed to the task function osPriorityNormal, // Priority NULL); // Task handle, used later to operate this task
The code snippets for the two created tasks are as follows:
static void CH1_UART2_TxTaskFunction( void *pvParameters ) { uint8_t c = 0; while (1) { // send data HAL_UART_Transmit_DMA (&huart2, &c, 1); Wait_UART2_TxComplete(100); vTaskDelay(500); c++; }} static void CH2_UART4_RxTaskFunction( void *pvParameters ) { uint8_t c = 0; int cnt = 0; char buf[100]; HAL_StatusTypeDef err; UART4_Rx_Start(); while (1) { // receive data err = UART4_GetData(&c); if(err == 0) { sprintf(buf, "Recv Data : 0x%02x, Cnt : %d", c, cnt++); Draw_String(0, 0, buf, 0x0000ff00, 0); } else { HAL_UART_DMAStop(&huart4); } }}
The code snippets for starting sending and receiving are as follows:
int UART4_GetData(uint8_t *pData){ xQueueReceive(g_xUART4_RX_Queue, pData, portMAX_DELAY); return 0;} void UART4_Rx_Start(void){ g_xUART4_RX_Queue = xQueueCreate(200, 1); HAL_UARTEx_ReceiveToIdle_DMA(&huart4, g_uart4_rx_buf, 100);}
Compile, flash, and run, and you can see the LED on the development board flashing continuously, and data coming in on the LCD, indicating the experiment design was successful.

The project experiment was successful, and I will continue to document experiments in the project. Thank you for your attention.
Test Project Used in This Article
https://download.csdn.net/download/weixin_44317448/89195002