This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

ULS-DeCoNet UART Transport

Production

1 - ULS-DeCoNet UART Transport Specification

Protocol Specification ULSDC-PBDS031125, version 1.0.9

ULSDC-PBDS031125 · version 1.0.9 · issued November 13, 2025
The numbered PDF is the controlled copy: download it from the portal.

Introduction

Purpose

This document specifies the UART framing layer of the ULS-DeCoNet protocol, providing detailed information about packet delimiting, byte stuffing, and error detection mechanisms.

Scope

The UART protocol layer operates between the Physical Layer (UART hardware) and the Network Layer (IF protocol), providing:

  • Frame delimiting
  • Error detection (CRC/checksum)
  • Transparent data transmission
  • Multiple operating modes

Physical Layer

UART Configuration

Standard Settings:

Data bits:    8
Parity:       None
Stop bits:    1
Flow control: None (optional RTS/CTS)

Common Baud Rates:

  • 115200 bps - Standard rate
  • 230400 bps - High-speed
  • 460800 bps - Very high-speed
  • 921600 bps - Maximum speed

Hardware Requirements

Minimum:

  • UART peripheral with TX/RX
  • FIFO buffers (recommended ≥ 64 bytes)
  • Interrupt or DMA support

Recommended:

  • FIFO buffers ≥ 256 bytes
  • DMA for high-speed operation
  • Hardware flow control

Framing Modes

Mode Selection

typedef enum {
    SERIAL_MODE_RAW = 0,            // No framing
    SERIAL_MODE_ESC = 1,            // Escape framing with CRC-16
    SERIAL_MODE_ESC_SIMPLE_CRC = 2, // Escape framing with 8-bit checksum
    SERIAL_MODE_COBS = 3            // COBS framing with CRC-16
} _serial_mode;

Mode Selection Criteria:

ModeBest ForOverheadError Detection
RAWCustom protocols0%None
ESCGeneral purpose~15-25%Strong (CRC-16)
ESC_SIMPLELow-power/simple~12-20%Basic (8-bit sum)
COBSBinary data~0.4%Strong (CRC-16)

Frame Format Details

RAW Mode

Format: Direct byte stream, no framing

[BYTE_0][BYTE_1][BYTE_2]...[BYTE_N]

Characteristics:

  • No delimiters
  • No error detection at this layer
  • Minimum latency
  • Maximum throughput
  • Higher layers must handle framing

Usage:

serial.mode(SERIAL_MODE_RAW);
serial.write(data, length);
// Bytes transmitted directly

ESC Mode (CRC-16)

Frame Structure

---
config:
    packet:
        showBits: false
---
packet-beta
title "ESC Mode Frame Structure (CRC-16)"
0-7: "SOF 0x55 (1 byte)"
8-15: "START_TYPE 0x01 (1 byte)"
16-79: "PAYLOAD (variable, ~8 bytes)"
80-87: "CRC_LOW (1 byte)"
88-95: "CRC_HIGH (1 byte)"
96-103: "EOF 0x55 (1 byte)"
104-111: "END_TYPE 0x03 (1 byte)"

Frame Components:

The ESC mode frame follows this sequence:

FieldSizeValueDescription
SOF1 byte0x55Start of Frame delimiter
START_TYPE1 byte0x01Frame start marker
PAYLOADVariableAnyUser data (byte-stuffed: 0x55 becomes 0x55 0x02)
CRC_LOW1 byteCalculatedLow byte of CRC-16 (byte-stuffed)
CRC_HIGH1 byteCalculatedHigh byte of CRC-16 (byte-stuffed)
EOF1 byte0x55End of Frame delimiter
END_TYPE1 byte0x03Frame end marker

Byte Stuffing Algorithm

Rule: If data byte equals 0x55, insert 0x02 immediately after it

Encoding:

Original byte:  0x55
Transmitted:    0x55 0x02

Example Frame:

Original payload: [0x12, 0x55, 0x34, 0x55]
CRC-16:          0x1234

Transmitted:
[0x55][0x01][0x12][0x55][0x02][0x34][0x55][0x02][0x34][0x12][0x55][0x03]
 SOF   TYPE  data  ←stuffed→    data  ←stuffed→   CRC_L CRC_H  EOF TYPE

CRC-16 Calculation

Algorithm: CRC-16-IBM/ANSI
Polynomial: 0x8005
Initial Value: 0xFFFF
Method: Table lookup

Calculation:

uint16_t crc = 0xFFFF;
for (int i = 0; i < payload_length; i++) {
    crc = (crc >> 8) ^ crc16_table[(crc ^ payload[i]) & 0xFF];
}

Table (partial, see full table in crc16.h):

uint16_t const crc16_table[256] = {
    0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
    0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
    // ... 248 more entries
};

Transmission Pseudocode

def transmit_esc_frame(payload):
    # Start frame
    send_byte(0x55)
    send_byte(0x01)
    
    # Calculate CRC
    crc = calculate_crc16(payload)
    
    # Send payload with byte stuffing
    for byte in payload:
        send_byte(byte)
        if byte == 0x55:
            send_byte(0x02)  # Stuff byte
    
    # Send CRC with byte stuffing
    crc_low = crc & 0xFF
    send_byte(crc_low)
    if crc_low == 0x55:
        send_byte(0x02)
    
    crc_high = (crc >> 8) & 0xFF
    send_byte(crc_high)
    if crc_high == 0x55:
        send_byte(0x02)
    
    # End frame
    send_byte(0x55)
    send_byte(0x03)

Reception State Machine

stateDiagram-v2
    [*] --> IDLE
    IDLE --> CHECK_START: 0x55 received
    CHECK_START --> RECEIVING: 0x01 start marker
    CHECK_START --> IDLE: 0x03 or other byte

    RECEIVING --> CHECK_END: 0x55 received
    RECEIVING --> RECEIVING: data byte; update CRC

    CHECK_END --> RECEIVING: 0x02 escaped 0x55
    CHECK_END --> VALIDATE: 0x03 end marker
    CHECK_END --> IDLE: 0x01 or other byte error

    VALIDATE --> IDLE: CRC OK process frame
    VALIDATE --> IDLE: CRC error discard frame and increment error counter

Reception Pseudocode

def receive_esc_frame():
    stage = IDLE
    buffer = []
    crc = 0xFFFF
    last_byte = 0
    
    while True:
        byte = read_byte()
        
        if stage == IDLE:
            if byte == 0x55:
                stage = CHECK_START
        
        elif stage == CHECK_START:
            if byte == 0x01:
                stage = RECEIVING
                buffer = []
                crc = 0xFFFF
            else:
                stage = IDLE
        
        elif stage == RECEIVING:
            if byte == 0x55:
                stage = CHECK_END
                last_byte = 0x55
            else:
                buffer.append(byte)
                crc = update_crc(crc, byte)
        
        elif stage == CHECK_END:
            if byte == 0x02:
                # Escaped 0x55
                buffer.append(0x55)
                crc = update_crc(crc, 0x55)
                stage = RECEIVING
            elif byte == 0x03:
                # End of frame
                if validate_crc(buffer, crc):
                    return buffer[:-2]  # Remove CRC bytes
                else:
                    increment_crc_error()
                    stage = IDLE
            else:
                # Error
                stage = IDLE

ESC Simple CRC Mode

Frame Structure

packet-beta
title "ESC Simple Mode Frame Structure (8-bit Checksum)"
0-7: "SOF 0x55 (1 byte)"
8-15: "START_TYPE 0x01 (1 byte)"
16-79: "PAYLOAD (variable, ~8 bytes)"
80-87: "CHECKSUM (1 byte)"
88-95: "EOF 0x55 (1 byte)"
96-103: "END_TYPE 0x03 (1 byte)"

Differences from ESC Mode:

  • Only 1 byte checksum (instead of 2 bytes CRC)
  • Simpler calculation

Checksum Algorithm

Simple 8-bit summation:

uint8_t checksum = 0;
for (int i = 0; i < length; i++) {
    checksum += payload[i];
}
checksum = checksum & 0xFF;

Properties:

  • Fast computation
  • Low error detection capability
  • Catches single bit errors
  • May miss multi-bit errors or byte swaps

Use Case: Low-power devices or low-error-rate channels

COBS Mode

Introduction to COBS

COBS = Consistent Overhead Byte Stuffing

Advantages:

  • Predictable overhead
  • No data expansion for most patterns
  • Very efficient for binary data
  • Zero byte (0x00) always available as delimiter

Frame Structure

packet-beta
title "COBS Mode Frame Structure (with CRC-16)"
0-7: "SOF 0x00 (1 byte)"
8-15: "COBS CODE (1 byte)"
16-79: "ENCODED DATA (variable, ~8 bytes)"
80-87: "CRC_LOW (1 byte)"
88-95: "CRC_HIGH (1 byte)"
96-103: "EOF 0x00 (1 byte)"

COBS Encoding Algorithm

Principle: Replace zeros with code bytes indicating distance to next zero

Code Byte Values:

  • 0x01 to 0xFE: Number of non-zero bytes following + 1
  • 0xFF: Special case - 254 non-zero bytes follow

Encoding Process:

  1. Scan data for zero bytes
  2. For each segment between zeros:
    • Count non-zero bytes (max 254)
    • Insert code byte before segment
    • Code byte = count + 1
  3. Replace zero positions with code bytes

Example 1 - Simple Data:

Original: [0x45, 0x00, 0x00, 0x2C, 0x4C, 0x79]

Segments:
  [0x45]          → 1 byte
  []              → 0 bytes
  [0x2C, 0x4C, 0x79] → 3 bytes

Encoded:
  [0x02, 0x45]       // code=2, 1 byte follows
  [0x01]             // code=1, 0 bytes follow (zero position)
  [0x04, 0x2C, 0x4C, 0x79] // code=4, 3 bytes follow

Result: [0x02, 0x45, 0x01, 0x04, 0x2C, 0x4C, 0x79]

Example 2 - No Zeros:

Original: [0x11, 0x22, 0x33, 0x44, 0x55]

Segments:
  [0x11, 0x22, 0x33, 0x44, 0x55] → 5 bytes

Encoded:
  [0x06, 0x11, 0x22, 0x33, 0x44, 0x55]
  // code=6 means 5 bytes follow, no zero at end

Result: [0x06, 0x11, 0x22, 0x33, 0x44, 0x55]
Overhead: 1 byte for 5 bytes of data = 20%

Example 3 - Long Segment (>254 bytes):

Original: [0x01, 0x02, ..., 0xFF, 0xFE] (255 bytes, no zeros)

Segments:
  First 254 bytes
  Last 1 byte

Encoded:
  [0xFF, 0x01, 0x02, ..., (254 bytes)]
  [0x02, 0xFE]

Overhead: 2 bytes for 255 bytes = 0.8%

COBS Decoding Algorithm

Process:

  1. Read code byte
  2. If code == 0x00: Frame delimiter (start/end)
  3. If code == 0xFF: Copy next 254 bytes as-is
  4. If code < 0xFF:
    • Copy next (code-1) bytes
    • Insert 0x00 after them (unless end of data)
  5. Repeat until frame delimiter

Pseudocode:

def decode_cobs(encoded_data):
    decoded = []
    pos = 0
    
    while pos < len(encoded_data):
        code = encoded_data[pos]
        pos += 1
        
        if code == 0x00:
            break  # End of frame
        
        # Copy (code-1) bytes
        for i in range(code - 1):
            if pos < len(encoded_data):
                decoded.append(encoded_data[pos])
                pos += 1
        
        # Insert zero (if code != 0xFF and not end of data)
        if code < 0xFF and pos < len(encoded_data):
            decoded.append(0x00)
    
    return decoded

COBS Frame Transmission

def transmit_cobs_frame(payload):
    # Calculate CRC on original data
    crc = calculate_crc16(payload)
    
    # Append CRC to payload
    full_data = payload + [crc & 0xFF, (crc >> 8) & 0xFF]
    
    # COBS encode
    encoded = cobs_encode(full_data)
    
    # Transmit frame
    send_byte(0x00)  # Start delimiter
    for byte in encoded:
        send_byte(byte)
    send_byte(0x00)  # End delimiter

COBS Frame Reception

State Machine:

stateDiagram-v2
    [*] --> IDLE
    IDLE --> RECEIVING: 0x00 start delimiter
    RECEIVING --> RECEIVING: code byte process block
    RECEIVING --> VALIDATE: 0x00 end delimiter
    VALIDATE --> IDLE: CRC OK return decoded data
    VALIDATE --> IDLE: CRC error discard and increment counter

Reception with Validation:

bool readCobsCheck(uint32_t sizelimit) {
    uint8_t v = 0;
    while (fifo->seek(&v)) {
        switch(stage) {
            case 0:  // Start of packet
                cobs_code = cobs_counter = v;
                stage = 1;
                len = 0;
                crc = 0xFFFF;
                break;
            
            case 1:  // Analyzing packet
                cobs_counter--;
                if (cobs_counter != 0) {
                    len++;
                    crc = GetCrcByte(crc, v);
                } else {
                    if (v == 0) {  // Packet complete
                        stage = 0;
                        if (crc != 0 || len < 2 || len > sizelimit + 2) {
                            fifo->flush_to_seeker();
                            return false;  // Error
                        }
                        len -= 2;  // Remove CRC
                        return true;  // Valid packet
                    }
                    // Next block
                    if (cobs_code != 0xFF) {
                        len++;
                        crc = GetCrcByte(crc, 0);  // Implicit zero
                    }
                    cobs_counter = cobs_code = v;
                }
                break;
        }
    }
    return false;
}

COBS Overhead Analysis

Best Case (no zeros in data):

Data length: 254 bytes
Overhead: 1 code byte
Percentage: 0.39%

Worst Case (alternating data and zeros):

Data: [0x01, 0x00, 0x02, 0x00, 0x03, 0x00]
Encoded: [0x02, 0x01, 0x02, 0x02, 0x02, 0x03, 0x01]
Overhead: 3 code bytes for 6 bytes = 50%

Typical Case (random binary data):

Average overhead: ~1-2%

Fixed Overhead:

  • Start delimiter: 1 byte
  • End delimiter: 1 byte
  • CRC: 2 bytes
  • Total fixed: 4 bytes

Implementation Details

ULSSerial Class

class ULSSerial {
public:
    ULSSerial(_io_fifo_u8 *rxBuf, _io_fifo_u8 *txBuf);
    
    // Mode selection
    void mode(_serial_mode mode);
    _serial_mode mode();
    
    // Transmission
    uint32_t write(uint8_t *buf, uint32_t size);
    uint32_t writeEsc(uint8_t *buf, uint32_t size);
    uint32_t writeCobs(uint8_t *buf, uint32_t size);
    
    // Reception
    uint32_t read(uint8_t *buf, uint32_t sizelimit);
    uint32_t readEsc(uint8_t *buf, uint32_t sizelimit);
    uint32_t readCobs(uint8_t *buf, uint32_t sizelimit);
    
    // Statistics
    uint32_t _crcerrors;
    uint32_t _packeterrors;
    
private:
    _io_fifo_u8 *_rxFifo;
    _io_fifo_u8 *_txFifo;
    _serial_mode _mode;
};

FIFO Buffers

Required Buffer Sizes:

ApplicationRX BufferTX Buffer
Low-speed256 bytes256 bytes
Standard512 bytes512 bytes
High-speed1024 bytes1024 bytes

Buffer Management:

  • Circular FIFO implementation
  • Interrupt-safe operations
  • DMA support for high throughput

Error Counters

uint32_t _crcerrors;      // CRC validation failures
uint32_t _packeterrors;   // Framing errors

Usage:

  • Monitor link quality
  • Detect noisy channels
  • Trigger error recovery

Performance Analysis

Throughput Comparison

Test Conditions: 1000 bytes payload, 115200 bps

ModeFrame SizeTransmission TimeThroughput
RAW100086.8 ms11.52 KB/s
ESC (worst case)2006174.2 ms5.74 KB/s
ESC (typical)1200104.2 ms9.59 KB/s
ESC_SIMPLE (typical)115099.8 ms10.02 KB/s
COBS (best)100787.4 ms11.44 KB/s
COBS (typical)102088.5 ms11.30 KB/s

Latency Analysis

Frame Detection Latency:

ModeStart DetectionEnd DetectionTotal
RAWN/AN/A0
ESC2 bytes2 bytes~0.17 ms @ 115200
COBS1 byte1 byte~0.09 ms @ 115200

CPU Usage

Relative Processing Cost:

ModeEncodingDecodingCRC
RAWNoneNoneNone
ESCLowMediumHigh
ESC_SIMPLELowMediumLow
COBSMediumMediumHigh

Mode Selection Guide

Choose RAW when:

  • Custom framing at higher layer
  • Maximum throughput required
  • No error detection needed at this layer

Choose ESC when:

  • General-purpose applications
  • Strong error detection required
  • Variable data content

Choose ESC_SIMPLE when:

  • Low-power requirements
  • Simple error detection sufficient
  • Low CPU overhead critical

Choose COBS when:

  • Binary data transmission
  • Predictable overhead required
  • Maximum efficiency needed
  • Zero byte available for framing

Error Handling

Error Types

CRC Errors:

  • Bit flips during transmission
  • Electrical noise
  • Buffer overruns

Framing Errors:

  • Lost delimiters
  • Synchronization loss
  • Incomplete packets

Error Recovery

Receiver Behavior:

1. Detect error (CRC mismatch or framing error)
2. Increment error counter
3. Discard corrupted frame
4. Flush FIFO to next frame delimiter
5. Resume normal operation

Higher Layer Responsibility:

  • Implement timeouts
  • Request retransmission
  • Handle missing data

Error Statistics

Monitoring:

void check_link_quality() {
    float error_rate = (float)serial._crcerrors / total_packets;
    
    if (error_rate > 0.01) {
        // More than 1% error rate - poor link
        reduce_baud_rate();
    }
}

Troubleshooting

SymptomPossible CauseSolution
High CRC errorsNoisy channelLower baud rate, add shielding
Framing errorsTiming issuesCheck baud rate configuration
No receptionWrong modeVerify sender/receiver mode match
Intermittent lossBuffer overflowIncrease buffer size, use DMA

Code Examples

Basic Usage

// Initialize
_io_fifo_u8 rxFifo, txFifo;
ULSSerial serial(&rxFifo, &txFifo);

// Configure mode
serial.mode(SERIAL_MODE_COBS);

// Transmit
uint8_t txData[] = {0x01, 0x02, 0x03, 0x04};
serial.write(txData, sizeof(txData));

// Receive
uint8_t rxData[256];
uint32_t len = serial.read(rxData, sizeof(rxData));
if (len > 0) {
    process_data(rxData, len);
}

// Check errors
if (serial._crcerrors > 0) {
    printf("CRC errors: %d\n", serial._crcerrors);
}

Mode Switching

// Start with simple mode
serial.mode(SERIAL_MODE_ESC_SIMPLE);

// Negotiate with remote device
send_mode_negotiation();

// Switch to COBS for binary data transfer
serial.mode(SERIAL_MODE_COBS);
transfer_binary_data();

// Switch back
serial.mode(SERIAL_MODE_ESC_SIMPLE);

Appendices

Appendix A: CRC-16 Table

See crc16.h for complete 256-entry table.

Appendix B: Test Vectors

ESC Mode Test Vector:

Payload: [0x01, 0x02, 0x03]
CRC-16: 0x3D8C

Transmitted frame:
0x55 0x01 0x01 0x02 0x03 0x8C 0x3D 0x55 0x03

COBS Mode Test Vector:

Payload: [0x00, 0x01, 0x00]
CRC-16: 0x628F

COBS encoded (with CRC):
0x01 0x02 0x01 0x01 0x02 0x8F 0x02 0x62

Full frame:
0x00 0x01 0x02 0x01 0x01 0x02 0x8F 0x02 0x62 0x00

Copyright © 2020-2026 UAVLAS <www.uavlas.com>
License: GNU Lesser General Public License v3.0+