Serial Communication Mistakes to Avoid in Embedded Projects

Common UART and RS232 mistakes in embedded projects, with practical fixes for framing, buffering, parser state, CRC checks, timeouts, backpressure, and bench debugging.

Serial communication often looks like the easy part of an embedded project, right up to the moment the first real board starts dropping bytes at exactly the wrong time. A command works from a terminal but fails from the production GUI, a packet survives at 9600 baud and breaks at 115200, or a USB to serial adapter behaves perfectly on the bench while the installed RS232 cable behaves differently in the cabinet.

A lot of that pain starts with one bad assumption: treating a serial byte stream as if it already contains clean messages. UART gives you bytes, and RS232 gives you an electrical interface. The protocol, buffering, recovery rules, and diagnostic evidence are still part of the product design.

Contents
  1. Design the frame before writing the parser
  2. Keep the receive path short and measurable
  3. Parse with explicit states and limits
  4. Do not ignore UART hardware error flags
  5. Check the baud rate from the real clock
  6. Treat transmit as a queue with backpressure
  7. Keep the PC or GUI side from blocking the device
  8. Remember that RS232 is not just UART with a connector
  9. Add diagnostics before the link fails in production
  10. Practical takeaways

Design the frame before writing the parser

A receiver can only recover from noise, dropped bytes, or a reset in the middle of a transfer when the frame gives it enough structure to find its way back. A useful frame normally has a start condition, a length or delimiter, a message type, a payload rule, an integrity check, and clear timeout behavior.

Serial Frame Path A receiver should move from raw bytes to checked messages. Bytesinterrupt/DMAFramingsync and lengthCheckCRC/statusMessageapplication
A serial receiver should separate byte collection, frame detection, integrity checking, and application handling.

The exact frame format depends on the product. A bootloader, a sensor link, and a lab instrument command protocol do not need the same packet shape, but they all need a receiver that can find the next valid frame after something goes wrong.

The following constants make those protocol rules visible. 0x7E is the start byte, the length byte is limited to a small payload, and the timeout is treated as part of the protocol rather than as an accidental delay hidden in the main loop.

Named protocol facts make reviews much easier. A raw 0x7E in the middle of a parser is just a number, while FRAME_START_BYTE tells the next engineer why that value is there.

Keep the receive path short and measurable

The receive interrupt or byte callback should do the smallest useful amount of work. Store the byte, update a small amount of state, and leave parsing or command execution to a foreground task. Once the callback starts waiting for a response, formatting a string, writing to flash, or updating a GUI, dropped bytes are usually only a matter of time.

The ring buffer below assumes one ISR writer and one foreground reader. On small 8 bit MCUs, check whether index reads and writes are atomic; if they are not, protect the reader side with the same critical section style used elsewhere in the firmware.

That overflow counter is not just bookkeeping. During bring-up, it tells you whether the parser is too slow, the baud rate is too high for the current scheduling, or the sender is allowed to transmit faster than the receiver can drain.

Parse with explicit states and limits

A parser should not be a long chain of assumptions buried inside nested if statements. Give it a state, keep the length bounded, count bad frames, and reset back to a known state when the frame is impossible.

This example uses CRC-8 to keep the snippet compact. Many product protocols use CRC-16 or CRC-32 instead, but the important habit is the same: every byte that belongs to the checked part of the frame should pass through one named CRC update path.

The parser state object is deliberately boring, which is exactly what you want here. A debugger watch window should be able to show where the frame is, how many bytes remain, and which error counter is growing.

The next part shows the parser consuming bytes. The helper time_reached() uses wrap-safe unsigned time arithmetic, which is safer than comparing raw millisecond counters directly.

In a real project, the good-frame path would call a message dispatcher or put the frame into another queue. It should not execute a motor command, block on flash, or update a user interface from inside the byte receive path.

Do not ignore UART hardware error flags

Most UART peripherals can tell you about overrun, framing, parity, and noise errors. Those flags are not decoration; they are the evidence that helps separate a parser bug from an electrical problem, a baud-rate mismatch, or firmware that is simply not servicing receive fast enough.

The names in this snippet are intentionally generic because every MCU vendor names the registers differently. The shape is the important part: read status, read the byte according to the reference manual requirements, count the errors, and only pass clean bytes to the receive buffer.

A rising framing error count often points toward the clock, baud rate, polarity, or electrical interface. A rising overrun count usually points toward firmware that cannot service receive fast enough. Those are different fixes, and the counters keep you from chasing the wrong one.

Check the baud rate from the real clock

Serial links are sensitive to timing error, and the value typed into the code is only part of the story. The real baud rate depends on the oscillator, PLL configuration, peripheral clock divider, UART oversampling mode, and any clock changes made during low power states.

During bring-up, I like to check the link from the clock tree all the way to the actual waveform, because each layer catches a different class of mistake.

Check What it catches
Verify the peripheral clock used by the UART Wrong clock tree or stale board-support setting
Calculate baud divider error Marginal links that pass at one temperature or cable length
Capture TX with a logic analyzer Actual bit time, polarity, idle level, and frame format
Test at the final burst rate Buffers that pass single-byte tests but fail real traffic
Test after low power transitions UART clock not restored or receiver enabled too late

When the link is close to the timing limit, random delays can make the bench test look better without fixing the product. Measure the bit time, compare it with the configured clock path, and fix the cause instead of tuning around the symptom.

Treat transmit as a queue with backpressure

Receive bugs get most of the attention, but transmit bugs can make a system feel just as unstable. A firmware module that prints too much debug text, a GUI that sends commands faster than the device can process, or a driver that blocks while waiting for TX space can all turn a simple serial link into a system problem.

The transmit side needs an explicit policy. Queue the frame, reject it cleanly, or block only in a context where blocking is allowed. Silent truncation is usually the worst choice because the receiver sees a valid start byte followed by a damaged frame.

This pattern gives the caller a real decision point. A telemetry frame may be dropped with a counter, a firmware update frame may need retry logic, and a user command may need a busy response. Those policies belong above the byte queue rather than hidden inside the UART driver.

Keep the PC or GUI side from blocking the device

Many embedded serial problems are half firmware and half host software. A desktop GUI that reads serial data on the UI thread can freeze while waiting for a response. A Python script that writes a command and immediately sleeps can accidentally create timing that never happens in the real system. A test tool that never drains receive data can make the device block when hardware flow control is enabled.

For host tools and GUIs, keep serial I/O away from the UI thread, set read timeouts deliberately, and log raw frames when debugging protocol issues. If a GUI sends commands, it needs the same kind of state machine as the firmware: request sent, waiting for response, timeout, retry, failed, and done.

Remember that RS232 is not just UART with a connector

UART is the logic-level serial peripheral inside the microcontroller. RS232 is an electrical interface with different voltage levels and inverted signaling, so a microcontroller UART pin should not be connected directly to a real RS232 port.

Before blaming the parser, check the physical link carefully. Small wiring and interface mistakes often look like software bugs until you put a probe on the line.

  • Use a proper RS232 transceiver between MCU logic levels and the RS232 connector.
  • Confirm TX, RX, and ground at the actual connector, not only on the schematic symbol.
  • Check whether the cable is straight-through or null-modem.
  • Confirm whether hardware flow control lines are required, ignored, or tied to a safe state.
  • Measure idle polarity at both sides of the transceiver.
  • Check the ground reference and cable length in the installed system, not only on the bench.

Electrical mistakes often appear as framing errors, random bytes, or a link that works with one adapter and fails with another. That is why hardware error counters and a logic analyzer save so much time.

Serial failures are much easier to fix when the firmware already records the reason. At minimum, keep counters for overflow, frame timeout, CRC error, length error, UART overrun, framing error, parity error, TX queue full, and valid frames received.

The counters do not need to be fancy. They only need to be readable from a debug shell, status command, test pin trace, or diagnostic frame. During bring-up, having the pattern visible matters more than the exact interface used to read it.

Symptom Evidence to collect Likely direction
Dropped bytes during bursts RX overflow count, max queue depth Increase buffer, improve scheduling, reduce burst rate
Parser gets stuck Frame timeout count, parser state trace Reset partial frames and check sender gaps
Random invalid commands CRC and length error counts Improve framing, inspect noise, check resynchronization
Works at low baud only Framing errors and measured bit time Fix clock, divider, polarity, or transceiver
GUI freezes TX queue depth and host thread logs Move serial I/O off UI thread and add command timeouts
Works on bench, fails installed RS232 levels, grounding, cable type Check transceiver, pinout, ground, and flow control

Good diagnostics change the conversation from “serial is flaky” to “we lose frames after a 70 byte burst when the UI requests history while telemetry is enabled.” That second version is a problem an engineer can reproduce and fix.

Practical takeaways

Start with the frame format before writing the parser. Keep receive work short, bounded, and measurable. Use explicit parser states, named protocol constants, length limits, CRC checks, and timeouts. Record UART hardware errors instead of throwing them away. Treat transmit as a queue with backpressure, and keep UART protocol debugging separate from RS232 electrical debugging.

The main habit is to make failure visible. Once the firmware can tell you whether it saw overflow, timeout, CRC failure, framing error, or TX backpressure, serial communication stops feeling random and starts behaving like an engineering problem.

Saeid Yazdani working at an electronics workbench
Saeid Yazdani

Embedded Systems Engineer with 20+ years of professional experience developing firmware, electronics, measurement systems, and hardware-software solutions. I have been programming for more than two decades and write about Embedded C/C++, STM32, AURIX, PCB design, debugging, and practical engineering lessons from real-world projects.

Articles: 40

Leave a Reply

Your email address will not be published. Required fields are marked *