Skip to main content
Associate
August 12, 2026
Question

STM32L451 UART circular DMA receptions not generating a full buffer (TC) interrrupt

  • August 12, 2026
  • 16 replies
  • 159 views

Hi.

I’m using CubeIDE, CubeMX and HAL to create an application for an STM32L451.

I’ve setup USART1 to receive via DMA in circular mode and detect line idle, starting receptions with HAL_UARTEx_ReceiveToIdle_DMA(), and processing receptions in callback HAL_UARTEx_RxEventCallback().

All seems to work fine except that the TC (transfer complete, or full buffer) interrupt never happens.HT (half transfer, or half of the buffer reached) and line idle do interrupt.

I’m providing HAL_UARTEx_ReceiveToIdle_DMA() a buffer of size 128 bytes. I’m logging the value of the size argument to HAL_UARTEx_RxEventCallback(). When I manually send a message of 20 bytes repeatedly, but slowly enough to ensure line idle triggers, the callback is called with the following size values: 20, 40, 60, 64, 80, 100, 120, 120, 12, 32, 52, 64, 72, 92, 112, 112, 4, 24, (etc).

You can see that line idle works, and also that HT is issued in the middle of a reception (size == 64), but TC isn’t, there’s no size equal to 128.

What’s also odd is that the callback is always called twice with the same last size value before the buffer wraps back to 0: in the above example at size==120, and in the next loop at size==112. If I use different buffer or message sizes, or keep on iterating, etc, I always get a double call at some point before the buffer is full, but never at exactly the buffer’s size.

Is my expectation of getting an interrupt at full buffer wrong? Why is always a second interruption with no further data just before a new message makes the buffer wrap around to 0, but not at the buffer’s full size?

Thanks for any insight.

16 replies

TDK
August 12, 2026

Ensure you’re on the latest version of the code as this was fixed within the past year.

Blaming stm32l4xx-hal-driver/Src/stm32l4xx_hal_uart.c at 90cd6033feb291fcaeb4221715cf848d231a762f · STMicroelectronics/stm32l4xx-hal-driver

 

If that’s not it, show your code, particularly the HAL_UARTEx_ReceiveToIdle_DMA call and HAL_UARTEx_RxEventCallback and how you’re getting the size values out. Seems like the number of interrupts are correct.

"If you feel a post has answered your question, please click ""Accept as Solution""."
Karl Yamashita
Principal
August 12, 2026

For 128 bytes, you’ll get a HT interrupt when you get 64 bytes. At the same time, you’ll get a Idle interrupt, because it well, idled. So 2 interrupts is expected when you get at least 64 bytes.

At some point you should get a HAL_UART_RXEVENT_TC interrupt when the DMA circular buffer reaches 128 bytes, plus a idle interrupt. Show your relevant code as there maybe something that you’re doing that may cause it not to get a TC interrupt.

 

 

If a reply has proven helpful, click on Accept as Solution so that it'll show at top of the post.CAN Jammer an open source CAN bus hacking toolCANableV3 Open Source
CTX1Author
Associate
August 13, 2026

Thanks for the replies.

I’m in the latest versions of both CubeIDE and CubeMX. I’ve checked that the line pointed by TDK is indeed in my project’s version of stm32l4xx_hal_uart.c.

Kari Yamashita, unless I’m mistaken, I would expect double HT and TC interrupts only if idle happens exactly at those points in the buffer, which for the former it does happen in some iterations. However, if half or full buffer is reached while line is still busy receiving, I would expect only a single interrupt. Nevertheless, double interrupts with the same size is not a concern in my code, it’s the lack of TC interrupt.

The original program is quite involved, specially what it does with the serial port. Thus, to simplify the code to share, I’ve created a new simple project that does nothing in the callback but to store the value of “size” argument, for the first 32 calls. Data received is not even touched. The main loop is just a nop to stop the the program running in debug mode to check the values of size (which for the record is not how I checked my original program, it relies on serial communications to transmit information back).

This is the main.c

/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2026 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

#define SERIAL_BUFLEN 128u
#define DEBUG_BUFLEN 32u
static uint8_t serial_buf[SERIAL_BUFLEN];
static volatile uint16_t debug_sizes[DEBUG_BUFLEN];
static volatile size_t debug_index = 0;

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef* const huart, const uint16_t size)
{
if (huart->Instance == huart1.Instance) {
if (debug_index < DEBUG_BUFLEN) {
debug_sizes[debug_index++] = size;
}
}
}

void HAL_UART_ErrorCallback(UART_HandleTypeDef* const huart)
{
if (huart->Instance == huart1.Instance) {
HAL_UART_Abort(huart);
HAL_UARTEx_ReceiveToIdle_DMA(huart, serial_buf, SERIAL_BUFLEN);
}
}


/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{

/* USER CODE BEGIN 1 */

/* USER CODE END 1 */

/* MCU Configuration--------------------------------------------------------*/

/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();

/* USER CODE BEGIN Init */

/* USER CODE END Init */

/* Configure the system clock */
SystemClock_Config();

/* USER CODE BEGIN SysInit */

/* USER CODE END SysInit */

/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */

/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, serial_buf, SERIAL_BUFLEN);

while (1)
{
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
__NOP();
}
/* USER CODE END 3 */
}

/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

/** Configure the main internal regulator output voltage
*/
if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
{
Error_Handler();
}

/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 1;
RCC_OscInitStruct.PLL.PLLN = 10;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;
RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;
RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}

/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK)
{
Error_Handler();
}
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */


And this is usart.c as created by MX to configure USART1:

/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file usart.c
* @brief This file provides code for the configuration
* of the USART instances.
******************************************************************************
* @attention
*
* Copyright (c) 2026 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "usart.h"

/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
DMA_HandleTypeDef hdma_usart1_tx;

/* USART1 init function */

void MX_USART1_UART_Init(void)
{

/* USER CODE BEGIN USART1_Init 0 */

/* USER CODE END USART1_Init 0 */

/* USER CODE BEGIN USART1_Init 1 */

/* USER CODE END USART1_Init 1 */
huart1.Instance = USART1;
huart1.Init.BaudRate = 230400;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
huart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART1_Init 2 */

/* USER CODE END USART1_Init 2 */

}

void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{

GPIO_InitTypeDef GPIO_InitStruct = {0};
RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
if(uartHandle->Instance==USART1)
{
/* USER CODE BEGIN USART1_MspInit 0 */

/* USER CODE END USART1_MspInit 0 */

/** Initializes the peripherals clock
*/
PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1;
PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;
if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
{
Error_Handler();
}

/* USART1 clock enable */
__HAL_RCC_USART1_CLK_ENABLE();

__HAL_RCC_GPIOA_CLK_ENABLE();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

/* USART1 DMA Init */
/* USART1_RX Init */
hdma_usart1_rx.Instance = DMA2_Channel7;
hdma_usart1_rx.Init.Request = DMA_REQUEST_2;
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;
if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK)
{
Error_Handler();
}

__HAL_LINKDMA(uartHandle,hdmarx,hdma_usart1_rx);

/* USART1_TX Init */
hdma_usart1_tx.Instance = DMA2_Channel6;
hdma_usart1_tx.Init.Request = DMA_REQUEST_2;
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_tx.Init.Mode = DMA_NORMAL;
hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)
{
Error_Handler();
}

__HAL_LINKDMA(uartHandle,hdmatx,hdma_usart1_tx);

/* USART1 interrupt Init */
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
/* USER CODE BEGIN USART1_MspInit 1 */

/* USER CODE END USART1_MspInit 1 */
}
}

void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle)
{

if(uartHandle->Instance==USART1)
{
/* USER CODE BEGIN USART1_MspDeInit 0 */

/* USER CODE END USART1_MspDeInit 0 */
/* Peripheral clock disable */
__HAL_RCC_USART1_CLK_DISABLE();

/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
HAL_GPIO_DeInit(GPIOA, GPIO_PIN_9|GPIO_PIN_10);

/* USART1 DMA DeInit */
HAL_DMA_DeInit(uartHandle->hdmarx);
HAL_DMA_DeInit(uartHandle->hdmatx);

/* USART1 interrupt Deinit */
HAL_NVIC_DisableIRQ(USART1_IRQn);
/* USER CODE BEGIN USART1_MspDeInit 1 */

/* USER CODE END USART1_MspDeInit 1 */
}
}

/* USER CODE BEGIN 1 */

/* USER CODE END 1 */


After manually sending a message with 20 bytes, this is the list of sizes as reported by the debugger. Notice how the first value is actually 128, which is the size of the buffer. I don’t know why, it should be 20, and it doesn’t appear again.
128, 20, 40, 60, 64, 80, 100, 120, 120, 12, 32, 52, 64, 72, 92, 112, 112, 4, 24, 44, 64, 64, 84, 104, 124, 124, 16, 36, 56, 64, 76, 96
 

Other than the initial 128, this pet program behaves the same than my actual project: interrupts when line idle and at HT (64). And again, double interrupt when the value is about to wrap back to the beginning of the buffer but not at 128.

TDK
August 13, 2026

Yeah, sure seems like a bug. Last time I tested this it worked fine, but that was over a year ago. Wonder if the “fixes” actually did the reverse.

In case it’s not obvious, the TC interrupt is clearly happening, but the Size parameter is not correctly reflected when it happens. The “double” interrupt is an IDLE plus a TC.

"If you feel a post has answered your question, please click ""Accept as Solution""."
CTX1Author
Associate
August 13, 2026

Yeah, that was I was fearing, that TC was being generated too early :(

 

Pavel A.
August 14, 2026

> except that the TC (transfer complete, or full buffer) interrupt never happens.

In the HAL source see: https://github.com/STMicroelectronics/stm32l4xx-hal-driver/blob/90cd6033feb291fcaeb4221715cf848d231a762f/Src/stm32l4xx_hal_uart.c#L2482

“ If all expected data are received, do nothing,          (DMA cplt callback will be called).”

This is conditioned “If Reception till IDLE event has been selected” && “DMA mode is enabled”.

 

CTX1Author
Associate
August 14, 2026

Wouldn’t that apply if Idle happened at the same time than TC? My expectation according to documentation is that when line is not idle but the end of the buffer is reached TC interrupt is issued, but it isn’t. The same can be said of HT, whose interrupt is indeed issued.

Pavel A.
August 15, 2026

This is a delicate question. The Idle condition interrupt belongs to the U(S)ART, but TC and HT are interrupts of the DMA channel. Which of them is seen earlier by the CPU depends on priorities of these interrupts.

The HAL driver is quite complicated; some users do like ​@Karl Yamashita and write their own code that is tailored for their exact case, is easier to understand and is not affected by changes in the “HAL library”.

 

Karl Yamashita
Principal
August 14, 2026

Ok so after testing on a Nucleo-L433RC, i got the exact same values you did. But i took it further and checked the interrupt type.

 

So, TC does interrupt. But what the value Size that the callback passes, is not the size of the data received. I had brain fart and finally remembered that Size is actually a pointer to the DMA. How to grab the data from the DMA is the tricky part. For awhile the word Size was messing me up when i was writing a ring buffer driver. Then i finally figured out after days of debugging, that it is a pointer to the DMA. I had my driver for years so i put all that Size/Pointer behind me. 

ST used Size which is misleading. It should have been worded something like ptr.

 

You can check out my project that shows how i use the pointer to get the actual size of bytes that i need to copy from the DMA and put that data into a larger circular buffer for processing outside of the callback.

https://github.com/karlyamashita/Nucleo-G071RB_UART_DMA_Idle_Circular_Multi_Instances 

And this is a better explanation https://github.com/karlyamashita/Nucleo-G071RB_UART_DMA_Idle_Circular/wiki

 

 

 

If a reply has proven helpful, click on Accept as Solution so that it'll show at top of the post.CAN Jammer an open source CAN bus hacking toolCANableV3 Open Source
CTX1Author
Associate
August 14, 2026

Yes, the name Size is a bit odd and it also got me initially a bit off guard. Technically, it’s a 1-based index to last written element into the buffer, and one must keep record of the previous Size value to know how many new elements have arrived since the callback was last invoked.

Hence, why with a 128-bytes buffer I expect the the Size argument to have a value of whenever Idle happens, and then 64 and 128 for HT and TC respectively.

EDIT: just to clarify that my actual application uses Size correctly, or so I hope. The simple code I provided above is to illustrate how the callback’s argument is not what I understand it should be, without actually doing anything with the receive data.