Receiving Framed Data on STM32 Using the USART Idle Line Interrupt (No DMA)
This guide shows how to receive variable-length serial frames on STM32 microcontrollers using the USART idle line interrupt, without DMA. The pattern is simple: collect bytes via RXNE in the ISR and treat an IDLE event as the frame delimiter.
Background: UART in brief
- UART sends data asynchronously as frames: start bit, data bits, optional parity, and stop bit(s).
- Both ends must agree on baud rate, data bits, parity, and stop bits.
- An idle line occurs when the RX line stays high for at least one frame duration after the last received byte.
Why the idle line interrupt
- IDLE triggers when the receiver detects a frame-time gap on the RX line after at least one byte has been received since the last IDLE clear.
- It is ideal to delimit packets of unknown length without scanning for timeouts in software.
- After clearing the IDLE condition, the next receptino sequence can generate IDLE again.
Minimal setup (HAL)
Assume the USART/LPUART is already initialized by CubeMX or equivalent. Enable RXNE and IDLE interrupts and configure the NVIC.
#include "stm32xxxx_hal.h"
#include <string.h>
extern UART_HandleTypeDef hlpuart1; // Created by CubeMX or user code
static uint8_t s_rx_buf[512];
static volatile uint16_t s_rx_len = 0;
static void on_uart_frame(const uint8_t* data, uint16_t len);
static void USART_EnableIdleLine(UART_HandleTypeDef* huart)
{
// Enable data-ready and idle interrupts
__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);
__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE);
// Clear any pending idle flag before starting
__HAL_UART_CLEAR_IDLEFLAG(huart);
}
static void USART_ConfigNVIC(void)
{
HAL_NVIC_SetPriority(LPUART1_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(LPUART1_IRQn);
}
void App_SerialInit(void)
{
// hlpuart1 is already initialized with baud/parity/word length/stop bits
USART_EnableIdleLine(&hlpuart1);
USART_ConfigNVIC();
}
Notes:
- If your HAL/MCU family differentiates DR vs RDR, use the register available on your part. The RDR read clears RXNE on STM32 series with the USART ISR/RDR interface.
Interrupt handler
Read as many bytes as available on RXNE. When IDLE fires, clear the flag and treat the data collected so far as one frame.
void LPUART1_IRQHandler(void)
{
UART_HandleTypeDef* huart = &hlpuart1;
// RXNE: byte ready
if (__HAL_UART_GET_FLAG(huart, UART_FLAG_RXNE))
{
uint8_t byte = (uint8_t)(huart->Instance->RDR & 0xFFu);
if (s_rx_len < sizeof(s_rx_buf))
{
s_rx_buf[s_rx_len++] = byte;
}
else
{
// Overflow: drop or handle error
(void)byte; // prevent unused warning
}
}
// IDLE: gap detected, frame ended
if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE))
{
__HAL_UART_CLEAR_IDLEFLAG(huart); // Clears by the proper HAL sequence
if (s_rx_len > 0)
{
on_uart_frame(s_rx_buf, s_rx_len);
s_rx_len = 0; // ready for next frame
}
}
}
Optional guards:
- If you want to avoid growing latency in the ISR, copy the pointer/length to a queue or set a flag and process the frame in the main loop/RTOS task.
Frame callback
Only dispatch for plausible frames and keep ISR work minimal.
static void on_uart_frame(const uint8_t* data, uint16_t len)
{
// Example: only process when length looks valid
if (len > 5)
{
// Application-specific parsing; keep it fast or offload
parse_debug_frame(data, len);
}
}
Example parser: HEAD(1B) + CMD(1B) + LEN(2B) + DATA + CS(1B)
The example below validates a simple framed portocol:
- HEAD is a fixed byte (e.g., 0xA5)
- LEN is big-endian payload length
- CS is a one-byte sum over all previous bytes (modulo 256)
#include <stdbool.h>
#define FRAME_HEAD 0xA5
#define RX_COPY_MAX 512
static volatile bool g_frame_ready = false;
static uint8_t g_frame_buf[RX_COPY_MAX];
static volatile uint16_t g_frame_len = 0;
static uint8_t checksum_sum8(const uint8_t* buf, uint16_t len)
{
uint32_t s = 0;
for (uint16_t i = 0; i < len; ++i) s += buf[i];
return (uint8_t)(s & 0xFF);
}
bool parse_debug_frame(const uint8_t* buf, uint16_t len)
{
// Minimum: HEAD + CMD + LEN(2) + CS
if (len < 1 + 1 + 2 + 1)
return false;
if (buf[0] != FRAME_HEAD)
return false;
uint8_t cmd = buf[1];
uint16_t payload_len = ((uint16_t)buf[2] << 8) | buf[3];
// Expected total length
uint16_t expected = 1 + 1 + 2 + payload_len + 1;
if (len != expected)
return false;
uint8_t cs_calc = checksum_sum8(buf, len - 1);
uint8_t cs_recv = buf[len - 1];
if (cs_calc != cs_recv)
return false;
// At this point the frame is valid; store/copy for later processing
if (len <= RX_COPY_MAX)
{
memcpy(g_frame_buf, buf, len);
g_frame_len = len;
g_frame_ready = true;
}
// Use cmd and data (buf + 4, length payload_len) as needed
(void)cmd;
return true;
}
Alternative: scatter-gather buffering
If you expect large bursts or want to avoid copying in the ISR, collect bytes into a ring buffer on RXNE and, on IDLE, record the current write index as a snapshot for processing outside the ISR. This reduces latency and jitter when frames are frequent or large.