SerialSage
FlagshipDeterministic UART communication stack, FreeRTOS priority scheduler, and dual watchdog framework
Marks the beginning of an incoming packet. When received, the state machine resets the frame buffer and begins payload accumulation.
UART Physical Layer
Configurable asynchronous serial driver with hardware flow control and termios integration.
- ▸Supported baud rates: 9600 to 115200 bps with 8N1 framing.
- ▸Linux POSIX non-canonical mode with epoll event notification.
- ▸Hardware flow control (RTS/CTS) preventing microcontroller buffer overruns.
Problem and motivation
Serial communication between embedded microcontrollers and Linux host computers is susceptible to frame misalignment, electrical noise, buffer overruns, and process stalls.
In industrial sensor nodes and robotics, a single dropped byte or blocked read call can stall an entire control loop. Standard raw serial reads lack framing boundaries, priority handling, and automated error recovery.
SerialSage was designed to provide deterministic, framed message transport between a Cypress PSoC 5LP (ARM Cortex-M3) microcontroller and a Linux host service.
Protocol framing specification
The communication layer implements a deterministic packet framing structure:
| Field | Size | Value / Range | Description |
|---|---|---|---|
| START | 1 Byte | 0x7E |
Frame synchronization delimiter |
| LENGTH | 1 Byte | 0x00 - 0xFF |
Length of unescaped payload in bytes (0 - 255) |
| CMD_ID | 1 Byte | 0x01 - 0xFE |
Target command identifier or response code |
| PAYLOAD | Variable | 0 - 255 Bytes | Byte-stuffed application data |
| CRC-16 | 2 Bytes | 0x0000 - 0xFFFF |
CRC-CCITT polynomial 0x1021, init 0xFFFF |
| END | 1 Byte | 0x7D |
Frame termination delimiter |
Byte stuffing and synchronization
Packets begin with 0x7E and terminate with 0x7D. If these control bytes appear inside the payload or CRC fields, the transmitter escapes them using 0x7C, XORing the original byte with 0x20.
View C implementation: parse_serial_byte framing state machine
// Protocol byte-stuffing framing decoder state machine (C)
typedef enum {
STATE_WAIT_START,
STATE_READ_LEN,
STATE_READ_CMD,
STATE_READ_PAYLOAD,
STATE_READ_CRC_HIGH,
STATE_READ_CRC_LOW,
STATE_WAIT_END
} FrameState;
void parse_serial_byte(uint8_t byte, FrameBuffer *buf) {
static FrameState state = STATE_WAIT_START;
static bool escaped = false;
if (byte == 0x7E) { // START delimiter resets frame state
state = STATE_READ_LEN;
buf->len = 0;
escaped = false;
return;
}
if (byte == 0x7C) { // ESCAPE control character
escaped = true;
return;
}
if (escaped) {
byte ^= 0x20;
escaped = false;
}
switch (state) {
case STATE_READ_LEN:
buf->expected_len = byte;
state = STATE_READ_CMD;
break;
case STATE_READ_CMD:
buf->cmd_id = byte;
state = (buf->expected_len > 0) ? STATE_READ_PAYLOAD : STATE_READ_CRC_HIGH;
break;
case STATE_READ_PAYLOAD:
buf->payload[buf->len++] = byte;
if (buf->len >= buf->expected_len) {
state = STATE_READ_CRC_HIGH;
}
break;
case STATE_READ_CRC_HIGH:
buf->received_crc = (uint16_t)(byte << 8);
state = STATE_READ_CRC_LOW;
break;
case STATE_READ_CRC_LOW:
buf->received_crc |= byte;
state = STATE_WAIT_END;
break;
case STATE_WAIT_END:
if (byte == 0x7D) { // END delimiter validates frame
validate_and_dispatch_frame(buf);
}
state = STATE_WAIT_START;
break;
default:
state = STATE_WAIT_START;
break;
}
}
Data integrity verification (CRC-16-CCITT)
Every packet includes a 16-bit CRC-CCITT checksum calculated over length, command ID, and raw payload. Table-driven lookups on Linux minimize CPU usage, while bit-shift operations execute on the PSoC microcontroller. If the receiver computes a mismatched CRC, it discards the frame and transmits a negative acknowledgment (NACK).
View C implementation: calculate_crc16_ccitt
uint16_t calculate_crc16_ccitt(const uint8_t *data, size_t length) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < length; i++) {
crc ^= (uint16_t)(data[i] << 8);
for (uint8_t bit = 0; bit < 8; bit++) {
if (crc & 0x8000) {
crc = (uint16_t)((crc << 1) ^ 0x1021);
} else {
crc <<= 1;
}
}
}
return crc;
}
Host and firmware architecture
Multi-priority queue system
Commands route through a thread-safe four-tier message queue using FreeRTOS task notifications on the MCU and POSIX mutexes with condition variables on Linux:
- Critical: Emergency stops, watchdog heartbeats, safety triggers (preemptive dispatch).
- High: Real-time sensor readings and motor position feedback.
- Normal: Configuration updates and status polls.
- Low: Debug logs and background telemetry.
Dual-layer watchdog framework
System availability is guaranteed through two independent watchdogs:
- Hardware Watchdog (PSoC5): The MCU firmware configures an internal timer that triggers a full hardware reset if the main RTOS control task fails to kick the timer within 500ms.
- Software Watchdog (Linux): The host daemon expects periodic heartbeat frames every 250ms. If the microcontroller misses three consecutive heartbeat intervals (750ms), the host issues a hardware reset via GPIO and re-establishes serial synchronization.
Linux POSIX termios and epoll driver
The Linux daemon configures non-canonical raw serial mode via termios (B115200, CS8, CLOCAL | CREAD, VMIN=1, VTIME=0) and registers file descriptors with epoll for event-driven asynchronous I/O with zero busy-waiting loops. An interactive ncurses terminal displays live packet streams, error rates, and throughput in real time.