SerialSage

Flagship

Deterministic UART communication stack, FreeRTOS priority scheduler, and dual watchdog framework

CCC++C++PSoC 5LPPSoC 5LPLinux termiosLinux termiosepollepollCRC-16CRC-16FreeRTOSFreeRTOSUnity TestUnity Test
Baud Rate Range
9600 - 115200 bps
Host Latency
< 1ms Protocol Parsing
Memory Footprint
~4KB MCU • ~50MB Host
UART Frame Structure (Interactive)
CLICK BYTES TO INSPECT
START (0x7E) Frame Synchronization Delimiter
1 Byte

Marks the beginning of an incoming packet. When received, the state machine resets the frame buffer and begins payload accumulation.

Byte-Stuffing Rule: If 0x7E appears in payload or CRC, it is transmitted as 0x7C 0x5E.
SerialSage Core Layers
CLICK TO INSPECT PILLAR

UART Physical Layer

Configurable asynchronous serial driver with hardware flow control and termios integration.

Hardware I/O
Implementation Details
  • ▸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.