Summary
This postmortem analyzes a design failure encountered when attempting to bridge multiple SERCOM/UART interfaces on a Microchip PIC32CZ CA80 board to independent TCP/IP sockets. The issue stemmed from architectural assumptions about Harmony’s driver model, concurrency handling, and socket lifecycle management. The result was unstable UART‑to‑TCP bridging, dropped connections, and unpredictable data flow.
Root Cause
The failure originated from treating each UART↔TCP path as an isolated pipeline without accounting for how Harmony’s TCP/IP stack and SERCOM drivers actually behave under load.
Key root causes included:
- Single-threaded execution model in Harmony tasks causing starvation between UART and TCP tasks
- Blocking UART reads stalling TCP socket writes
- Improper socket state handling (CONNECTING, CONNECTED, CLOSING)
- Lack of ring buffers between UART and TCP layers
- Assuming Microchip’s old UART‑TCP bridge examples still apply to the PIC32CZ architecture
- No concurrency abstraction (e.g., RTOS tasks per SERCOM)
Why This Happens in Real Systems
Systems like this fail because embedded networking is not symmetric with UART communication.
Common systemic reasons:
- TCP is bursty, UART is continuous
- TCP backpressure can stall UART unless buffered
- UART overruns occur when TCP cannot drain fast enough
- Harmony’s TCP/IP stack requires periodic servicing, unlike UART interrupts
- Developers underestimate the complexity of bridging two asynchronous transports
Real-World Impact
When deployed, these issues manifest as:
- Dropped TCP connections under moderate UART throughput
- Corrupted or interleaved UART data
- SERCOM FIFO overruns
- High CPU usage from busy‑polling sockets
- Unrecoverable socket states requiring system reset
- Unpredictable latency that breaks protocols riding over UART
Example or Code (if necessary and relevant)
Below is a minimal example of how senior engineers structure a non-blocking UART↔TCP bridge task using ring buffers and state machines.
void BridgeTask(BRIDGE_CTX *ctx)
{
// Pump UART → TCP
size_t uartBytes = UART_Read(ctx->uart, ctx->uartBuf, sizeof(ctx->uartBuf));
if (uartBytes > 0 && TCPIP_TCP_IsConnected(ctx->sock)) {
TCPIP_TCP_ArrayPut(ctx->sock, ctx->uartBuf, uartBytes);
}
// Pump TCP → UART
int tcpBytes = TCPIP_TCP_GetIsReady(ctx->sock);
if (tcpBytes > 0) {
tcpBytes = TCPIP_TCP_ArrayGet(ctx->sock, ctx->tcpBuf, sizeof(ctx->tcpBuf));
UART_Write(ctx->uart, ctx->tcpBuf, tcpBytes);
}
}
How Senior Engineers Fix It
Experienced engineers solve this by architecting the system, not by patching symptoms.
They typically:
- Use one RTOS task per SERCOM↔TCP bridge
- Implement ring buffers on both UART and TCP sides
- Use non-blocking drivers only
- Add connection watchdogs to auto‑reconnect sockets
- Ensure TCPIP_Task() is serviced frequently
- Use interrupt-driven UART with DMA when possible
- Add flow control (RTS/CTS) if the UART protocol supports it
- Build a state machine for each socket (INIT → CONNECT → RUN → ERROR → RETRY)
Why Juniors Miss It
Less experienced engineers often overlook:
- TCP is not a byte stream like UART
- Harmony drivers are cooperative, not preemptive
- Blocking calls silently break real-time behavior
- UART and TCP run at different speeds and patterns
- Examples from older PIC32 families do not apply
- Bridging transports requires buffering and scheduling, not just forwarding bytes
They tend to write “straight-line” code that works in a lab but collapses under real traffic.