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.

评论

此博客中的热门博文

How To Connect Stm32 To PC?

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

What is a Look-Up Table (LUT) in an FPGA, and how does it work?