How does a microcontroller serial port receive data of variable length?

 A UART/serial port only gives you a stream of bytes—no built-in “message length.” To receive variable-length data you add a framing rule in software (and sometimes use hardware features to help). Here are the proven patterns and how to implement them on MCUs.




The Four Common Framing Strategies

  1. Delimiter-terminated (e.g., newline \n)

  • Sender ends each message with a unique byte/sequence (\n, \r\n, 0x00, etc.).

  • Receiver collects bytes until it sees the delimiter.

  • Pros: simple; works with text protocols.

  • Cons: delimiter cannot appear in payload unless you escape it.

  1. Length-prefixed (binary packets)

  • Message starts with a header that includes LEN, then exactly LEN bytes of payload (often with a checksum/CRC).

  • Pros: fast, no searching for delimiters, great for binary.

  • Cons: you must trust the header (add CRC + bounds checks).

  1. Start/End markers with escaping (SLIP/COBS-style)

  • Use a start (and/or end) byte (e.g., 0x7E), and escape that value if it appears in data.

  • Pros: robust framing without length field.

  • Cons: encoder/decoder logic slightly more complex.

  1. Idle-gap (timeout) framed

  • Treat a silent gap on the line as “end of frame.” Common in Modbus RTU (≥3.5 char times).

  • Pros: no special bytes needed.

  • Cons: depends on timing; noisy lines can confuse it.

Char timestart+data+parity?+stopbaud\frac{\text{start} + \text{data} + \text{parity?} + \text{stop}}{\text{baud}}.
Typical 8-N-1 → ~10 bit-times/byte. So at 115200 bps, 1 char ≈ 86.8 µs; an idle frame gap might be ~300–500 µs.


How to Implement on an MCU

A. Interrupt + Ring Buffer (works everywhere)

  • Enable RX interrupt. On each received byte: push to a ring buffer (non-blocking, O(1)).

  • In the main loop (or a parser task), pop bytes and run a small state machine that applies your framing rule.

Ring buffer ISR & parser (C-like pseudocode):

#define RB_SIZE 256 volatile uint8_t rb[RB_SIZE]; volatile uint16_t w = 0, r = 0; // write/read indices // UART RX ISR: void USARTx_IRQHandler(void){ uint8_t b = UARTx->RDR; // read received byte (clears RX flag) uint16_t next = (w + 1) & (RB_SIZE-1); if(next != r) { rb[w] = b; w = next; } // drop byte if buffer full (or set an overflow flag) } // Non-blocking read: int rb_get(uint8_t *out){ if(r == w) return 0; *out = rb[r]; r = (r + 1) & (RB_SIZE-1); return 1; }

1) Delimiter-terminated parsing (e.g., \n)

#define MAX_MSG 128 uint8_t msg[MAX_MSG]; int len = 0; void parse_loop(void){ uint8_t b; while(rb_get(&b)){ if(b == '\n'){ // end of message msg[len] = 0; // optional NUL handle_message(msg, len); len = 0; }else if(len < MAX_MSG){ msg[len++] = b; }else{ // oversize line -> reset or drop until delimiter len = 0; } } }

2) Length-prefixed state machine (header 0xAA 0x55, then LEN, PAYLOAD, CRC)

enum { ST_SYNC1, ST_SYNC2, ST_LEN, ST_PAYLOAD, ST_CRC } st = ST_SYNC1; uint8_t buf[256], crc=0, idx=0, need=0; void parse_loop(void){ uint8_t b; while(rb_get(&b)){ switch(st){ case ST_SYNC1: if(b==0xAA) st=ST_SYNC2; break; case ST_SYNC2: if(b==0x55) st=ST_LEN; else st=ST_SYNC1; break; case ST_LEN: need=b; idx=0; crc=0; st=(need?ST_PAYLOAD:ST_CRC); break; case ST_PAYLOAD: buf[idx++]=b; crc ^= b; if(idx==need) st=ST_CRC; break; case ST_CRC: if(b==crc) handle_packet(buf, need); st=ST_SYNC1; // accept/reject; resync break; } } }

3) Idle-gap framing with a timer

  • Start/restart a one-shot timer on each received byte.

  • If the timer expires (no new byte for, say, 3–5 char times), treat collected bytes as one frame.

// In RX ISR: push b to rb[]; restart gap_timer to (3*char_time) // In gap_timer ISR: finalize current frame (copy out what arrived since last frame)

Sizing the buffer:
min_ring_size ≥ line_rate_bytes_per_sec × worst_case_service_latency.
Example: 115200 bps ≈ 11.5 kB/s; if your parser may be busy for 10 ms → buffer ≥ 115 bytes (round up, use power of two like 256).


B. DMA + Circular Buffer (high throughput, STM32/ESP32/etc.)

  • Configure UART RX → DMA circular into rxbuf[N].

  • Use IDLE line detection or DMA half/complete interrupts to know “new data arrived.”

  • Compute the “write index” as wr = N - DMA_Remaining(). Parse the new span [last_wr … wr) and advance last_wr.

STM32 (HAL) sketch:

#define N 512 uint8_t rxbuf[N]; size_t last_wr = 0; HAL_UARTEx_ReceiveToIdle_DMA(&huart, rxbuf, N); // enables IDLE callback void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t rx_len){ // rx_len = bytes now in buffer since last event process_span(rxbuf + last_wr, rx_len); last_wr = (last_wr + rx_len) % N; // (HAL restarts DMA automatically in ReceiveToIdle) }

(With LL/LLDMA: on IDLE interrupt, read NDTR to get wr = N - NDTR, parse the delta, handle wraparound.)

Why this is great: minimal CPU, no per-byte ISR overhead, easy to combine with delimiter/length/idle parsing.


Robustness & Gotchas

  • Bounds & timeouts: Always guard against malformed headers, excessive LEN, or missing delimiter. Use a max message size and reset state if exceeded.

  • Checksums/CRC: Add at least a simple CRC-8/16 to reject corrupted frames.

  • Flow control: If bursts can exceed your processing rate, enable RTS/CTS (hardware) or XON/XOFF (software).

  • Error flags: Read/clear UART status (framing/parity/overrun). Drop the current frame if errors occur.

  • Concurrency: If ISR and main code share indices/buffers, keep indices volatile, and update them atomically.

  • Throughput math: 1 char ≈ 10/baud seconds. Make your idle timeout ≈ 3–5 char times for gap-framed protocols.


Quick Recommendations

  • Human-readable (CLI, logs): Delimiter (\n) + ring buffer ISR.

  • Binary control/data: Length-prefixed + CRC + small state machine.

  • Very high rate / tight CPU: UART DMA circular + IDLE detection + delimiter or length.

  • Legacy timing-based (e.g., Modbus RTU): Idle-gap + timer.

评论

此博客中的热门博文

How To Connect Stm32 To PC?

What are the common HDL languages used in FPGA design?

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