A drop-in solution for STM32F4 + HAL that receives variable-length UART data reliably

Here’s a drop-in solution for STM32F4 + HAL that receives variable-length UART data reliably. I’m giving you two production-proven patterns:

  1. Delimiter framing (e.g., \n) using an ISR ring buffer (simple, great for text/AT commands)

  2. DMA circular + IDLE line (best for bursts/binary streams; minimal CPU)

Both work on F4 (F401/F405/F407/F411, etc.) with CubeMX/HAL.




A) Delimiter framing (\n) with ISR ring buffer

1) CubeMX setup

  • Enable USARTx (e.g., USART1 @ 115200 8N1)

  • Enable RXNE interrupt

  • Configure GPIO pins (Tx/Rx AF)

  • NVIC: set a sensible priority (e.g., preempt 5, sub 0 if using FreeRTOS)

2) Ring buffer + IRQ handler

// uart_rx_isr_rb.c #include "main.h" #include <string.h> #include <stdarg.h> #define RB_SIZE 512 typedef struct { volatile uint8_t buf[RB_SIZE]; volatile uint16_t head, tail; // modulo RB_SIZE } ringbuf_t; extern UART_HandleTypeDef huart1; // adjust to your instance static ringbuf_t rxrb; static uint8_t rxbyte; // temp byte for HAL_IT static inline void rb_put(volatile ringbuf_t *rb, uint8_t b){ uint16_t n = (rb->head + 1) & (RB_SIZE - 1); if (n != rb->tail) { rb->buf[rb->head] = b; rb->head = n; } else { /* overflow: drop or advance tail = n to discard oldest */ } } int rb_get(volatile ringbuf_t *rb){ // -1 if empty if (rb->head == rb->tail) return -1; uint8_t b = rb->buf[rb->tail]; rb->tail = (rb->tail + 1) & (RB_SIZE - 1); return b; } void UART_RingBufferStartIT(void){ // Prime a 1-byte interrupt-driven receive HAL_UART_Receive_IT(&huart1, &rxbyte, 1); } // Called by HAL when 1 byte received void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){ if (huart->Instance == USART1){ rb_put(&rxrb, rxbyte); HAL_UART_Receive_IT(&huart1, &rxbyte, 1); // re-arm } } // Optional: also capture errors void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart){ if (huart->Instance == USART1){ // Clear flags as needed; HAL usually handles them. // You could count errors/log here. HAL_UART_Receive_IT(&huart1, &rxbyte, 1); // ensure re-armed } }

3) Delimiter parser (non-blocking)

// Returns length when a full line is ready; 0 otherwise. // Lines end with '\n'. '\r' is stripped if present before '\n'. int uart_readline(char *out, int maxlen){ static char acc[RB_SIZE]; static int n = 0; int ch; while ((ch = rb_get(&rxrb)) != -1){ if (ch == '\n'){ // Trim optional '\r' int len = n; if (len > 0 && acc[len-1] == '\r') len--; if (len >= maxlen) len = maxlen - 1; memcpy(out, acc, len); out[len] = '\0'; n = 0; return len; } if (n < (int)sizeof(acc)) acc[n++] = (char)ch; else n = 0; // overflow -> reset (or keep last N policy) } return 0; }

4) Usage (in main.c)

// In main() MX_USART1_UART_Init(); UART_RingBufferStartIT(); char line[128]; for (;;){ int n = uart_readline(line, sizeof(line)); if (n > 0){ // Process complete message here // Example: echo back with prefix char resp[160]; int m = snprintf(resp, sizeof(resp), "RX:%s\r\n", line); HAL_UART_Transmit(&huart1, (uint8_t*)resp, m, HAL_MAX_DELAY); } // do other work... }

Pros: dead simple; ideal for ASCII/AT commands.
Cons: payload can’t contain the delimiter; use escaping if needed.


B) DMA circular + IDLE line (variable length without delimiter)

This is the go-to for binary packets or bursts. The UART sets IDLE when the line is idle for ~1 frame; we then consume the newly arrived DMA bytes as a “frame”. Layer a length/CRC on top if needed.

1) CubeMX setup

  • USARTx enabled

  • DMA for RX:

    • Mode: Circular

    • Memory increment: Enabled

    • Peripheral increment: Disabled

    • Data alignment: Byte

  • Enable UART global interrupt and IDLE interrupt:

    • In user code after MX_USARTx_UART_Init(): __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

  • NVIC priority set appropriately

2) Code

// uart_dma_idle.c #include "main.h" #include <string.h> extern UART_HandleTypeDef huart1; #define RX_DMA_BUF_SZ 512 static uint8_t rx_dma_buf[RX_DMA_BUF_SZ]; static volatile uint16_t dma_last = 0; static void UART_DMA_Start(void){ HAL_UART_Receive_DMA(&huart1, rx_dma_buf, RX_DMA_BUF_SZ); __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // enable IDLE detection } // Called when IDLE fires or in other events to consume new data static void uart_dma_consume(void (*on_chunk)(const uint8_t*, uint16_t)){ // DMA in circular mode: NDTR counts down remaining uint16_t remaining = __HAL_DMA_GET_COUNTER(huart1.hdmarx); uint16_t dma_write = RX_DMA_BUF_SZ - remaining; if (dma_write == dma_last) return; // nothing new if (dma_write > dma_last){ // Linear region: [dma_last, dma_write) on_chunk(&rx_dma_buf[dma_last], dma_write - dma_last); } else { // Wrapped: [dma_last, end) + [0, dma_write) on_chunk(&rx_dma_buf[dma_last], RX_DMA_BUF_SZ - dma_last); if (dma_write > 0) on_chunk(&rx_dma_buf[0], dma_write); } dma_last = dma_write; } // Example “framer” using idle gaps as frame boundaries. // Accumulate until IDLE; hand over a complete frame in callback. #define FRAME_MAX 512 static uint8_t frame_acc[FRAME_MAX]; static uint16_t frame_len = 0; static void on_chunk_idle_frame(const uint8_t *data, uint16_t len){ while (len--){ if (frame_len < FRAME_MAX) frame_acc[frame_len++] = *data++; else { /* overflow policy: drop/flush */ data++; } } } static void on_idle_event(void){ // Called on IDLE: treat accumulated as 1 frame if (frame_len){ // >>> Process completed frame here <<< // (Optional) validate length/CRC, then handle: HAL_UART_Transmit(&huart1, frame_acc, frame_len, 50); uint8_t crlf[] = "\r\n"; HAL_UART_Transmit(&huart1, crlf, sizeof(crlf)-1, 50); frame_len = 0; } } void USART1_IRQHandler(void){ // Standard HAL IRQ handler HAL_UART_IRQHandler(&huart1); // Check IDLE if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET){ __HAL_UART_CLEAR_IDLEFLAG(&huart1); // Consume any bytes DMA wrote since last time uart_dma_consume(on_chunk_idle_frame); // IDLE marks end of burst -> deliver frame on_idle_event(); } } // Optional: call periodically (e.g., in a 1ms tick) to handle // late arrivals even without IDLE (or to set an explicit timeout) void UART_DMA_Poll(void){ uart_dma_consume(on_chunk_idle_frame); } void UART_DMAIdle_Init(void){ UART_DMA_Start(); dma_last = 0; }

3) Usage

// In main() MX_USART1_UART_Init(); UART_DMAIdle_Init(); for (;;){ // Optionally poll if you want time-based flush as well // UART_DMA_Poll(); // Your app work... }

Pros: handles arbitrary lengths and binary safely, minimal CPU, no byte-by-byte ISRs.
Cons: you still need a packet format for integrity (length + CRC) if noise is possible.


Optional: length-prefixed + CRC on top (robust binary)

If your sender uses [SYNC=0xAA][LEN][PAYLOAD...][CRC8], replace on_idle_event() with a small state machine that parses the accumulated frame_acc[0..frame_len-1] into valid packets (resync on errors). This gives you exact message boundaries even if the line doesn’t idle cleanly.


Practical tips (STM32F4)

  • Clock accuracy: Use HSE (or good HSI trimming) for higher baud rates.

  • Buffers: Size DMA/RB for worst bursts (baud × burst_time / 10 bits).

  • Flow control: If the peer can flood you, enable RTS/CTS in CubeMX.

  • Priorities: If using FreeRTOS, keep UART/DMAs at higher priority than tasks.

  • Latency: With DMA+IDLE, your latency ≈ 1 frame time (e.g., ~1 char).

  • Testing: Fuzz with random lengths/delimiters; inject pauses to ensure IDLE framing works as expected.

评论

此博客中的热门博文

Detailed Explanation of STM32 HAL Library Clock System

How To Connect Stm32 To PC?

How do you set up ADC (Analog-to-Digital Converter) in STM32?