Abstract
In this tutorial I will describe how to:
[+] use the U(S)ART of an STM32F103C8 MPU
[+] with a circular DMA buffer for Rx,
[+] a normal DMA buffer forTx,
[+] and how to use the Idle Line Interrupt to receive messages of unknown length
Hardware Requirements
I am using a “Bluepill” prototyping board, with an STM32F103C8T6 MPU, CubeMX for Hardware Configuration, CubeIDE for editing and the STM32 HAL Hardware Abstraction Layer along with the following hardware:
[+] a breadboard and a few jumper wires
[+] a ST-Link V2 Clone
[+] a simple UART to USB Converter, you can get e.g. from ebay
Usefull Links
Cube MX: Basic Setup
Please use CubeMX and follow the instructions in this tutorial for the configuration of RCC, Clock Settings, SWD and the project setup. The MCU Main clock is 72 MHz in this example.
Cube MX: Preripherals
In this tutorial we need PC13 as GPIO Output and USART1 with DMA-Channels for RX and TX and global Interrupts enabled:
Please make sure the RX DMA Channel is set to „circular“ and the TX DMA Channel is set to „normal“ and dont forget to enable the global interrupt for USART1.
When you have finished the configuration in CubeMX the MCU pinpout should look like this:
Code: main.c
All functionality is implemented in the main.c file, except the call for the idle interrupt, which needs to be in the stm32f1xx_it.c
You can download both files (zipped) at the end of this article, if you don’t want to type it by your own.
We need and for sprintf() and strlen(). The right place for the two include statements is in the „Includes section“ of the main.c-file, between the USER CODE Includes tags.
We need two defines, one for the RX Buffer Size and another one for the TX Buffer size. The RX Buffer is intentionally very small, only 16 Bytes. This might be way to small for a real world application, but for this tutorial it’s just right, as I want to demonstrate the buffer rollovers and if the buffer is small I don’t have to send long strings to show the effect. In a real application you might set the buffer size to a value, that reflects your expectations of the size of the incoming commands.

We need a couple of global variables. Place them in the Private variables (user code) section. I have made all the necessary variables global variables for this example, because it’s easier to track them with the debugger. In a real world application you would most probably not do that this way.
- RxRollover is a counter that counts the DMA RxComplete Interrupts
- RxCounter counts the incoming transmissions
- RxBfrPos is used to store the Buffer Position of the last transmission
- TxCounter counts the outgoing transmissions
- TxBuffer is the char buffer for outgoing transmissions
- RxBuffer is the U(S)ART receiving buffer
We need the protptypes for the UART RX Complete Callback function and the UART TX Complete callback functions. You can copy and paste them from the stm32f1xx_hal_uart.h heder file. You can find this file, in the Drivers –> STM32F1xx_HAL_Driver –> Inc folder of your CubeIDE project. Place them in the „USER CODE 0“ section of the main.c-file.
Both functions are declared as „__weak“ in the stm32f1xx_hal_uart.c file and will be overwritten by your implementation in the main.c file.
Both functions are called by the DMA handler (automatically). So you don’t have to care about where to call them and why …
In the USER CODE 2 section (within the int main(void) function) we have to:
- enable the UART Idle Interrupt
- start the UART in DMA mode to receive incoming transmissions
- and turn the LED1 off by setting the GPIO in a high impedance state
The LED1 is toggled with each outgoing transmission.
There is no need to type all the code by yourself. At the end of this tutorial you can download the source code or by following this link
stm32f1xx_it.c Code
Only a small change to the stm32f1xx_it.c is neccessary:

When the general UART IRQHandler is called, we need to check if it is the Idle Interrupt, and if yes, we call the RxComplete Callback Function.
How does it work?
This code in general works as follows:
There are 3 interrupts: the Rx Complete Interrupt (created by DMA), the Tx Complete Interrupt (also created by DMA) and the „Idle Line“ Interrupt, coded manually in the stm32f1xx_it.c.
The Rx Complete Interrupt always occurs, when the Rx Buffer rolls over. To demonstrate this, the Rx buffer size was intentionally made very low (16 bytes) for this example. We use this interrupt to count the „Buffer rollovers“. A Buffer may only roll over once before an Idle Interrupt occurs for a valid transmission. If it rolls over two or more times, we can assume, that some data have been overwritten and the transmission is corrupted.
The incoming data is checked when the idle interrupt occurs. The start position of the new incoming data stream is the last end position, which can be calculated from querying the DMA “CNDTR” register (which tells us, how many bytes are left until the buffer is full / rolls over) and subtracting that value from the (known) buffer size. This information needs to be consistent over the whole runtime of the program, so you have to store it “in a safe place” 😉 – don’t lose it …
With the information of the last known buffer position and the current buffer position we can calculate the length of the received data. We have to check, if a buffer rollover occurred during the transmission and have to take that into account. See the source code for details …
The Tx Complete Interrupt is used here only to toggle the LED of the Bluepill.
Output
I am using RealTerm to test the output.
In the first screenshot you can see the output of sending 3 times the string „01234“ (always with Carriage Return and Linefeed at the end), which makes the buffer to roll over during the 3rd transmission.
Sending 16 bytes of test +CR +LF produces an „Error 1“, as the first 3 bytes of the transmission were overwritten in the buffer.
Sending more then 32 bytes (reminder: buffer size = 16 bytes …) makes the buffer roll over 2 times and produces „Error 2“ as result.
Sending exactly 16 bytes again works fine.
Hallo, habe das mal ausprobiert. Läuft ganz gut. Allerdings wenn die vom Terminalprogramm gesendete Stringlänge > 218 ist, dann hängt das Programm beim zweiten Mal senden und der String wird nicht mehr zurück gesendet. Ich habe USART2 genommen bei 9600 bd. Die Buffer habe ich beide auf 1024 gesetzt.
great article! i learn a lot by reading it. thank you.
Thanks for a perfect tutorial. I was struggling with UART communications on an STM32429 board, and could not find any working example except for yours. After porting it to this board, it works great!
Great tutorial, it helped me solve a problem i was having with UART1 TX using DMA on my STM32F446RE Nucleo board. I have one question: is it always required to enable UART Global Interrupt when using DMA for Memory to Peripheral data transfer?
Thank-you in advance for your time.
nope, the global interrupt is only needed, if you want to react on the „idle interrupt“ (which I use in this tutorial). Without the idle interrupt you need to know the length of the incoming transmission, or you have to poll. The DMA transfer itself works without the global interrupt – but DMA interrupts have to be enabled (obviously …)
If your DMA works catching the last byte of RX transmission, probably you have to enable Memory Increment option ONE MORE TIME:
https://community.st.com/s/question/0D50X0000BmoycOSQQ/stm32f030-hal-problem-with-uart-dma-receive-memory-increment-not-set-properly-with-workaround
many thanks. Was having trouble figuring out how this worked and this did the trick. Finding out the ST docs and support are not so good. Perfect example and explanation.
HAL_UARTEx_ReceiveToIdle_DMA
Thanks.