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
-
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.
-
Length-prefixed (binary packets)
-
Message starts with a header that includes
LEN
, then exactlyLEN
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).
-
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.
-
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 time ≈ .
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):
1) Delimiter-terminated parsing (e.g., \n
)
2) Length-prefixed state machine (header 0xAA 0x55, then LEN, PAYLOAD, CRC)
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.
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 advancelast_wr
.
STM32 (HAL) sketch:
(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.
评论
发表评论