ULS-DeCoNet UART Transport Specification
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:
| Mode | Best For | Overhead | Error Detection |
|---|---|---|---|
| RAW | Custom protocols | 0% | None |
| ESC | General purpose | ~15-25% | Strong (CRC-16) |
| ESC_SIMPLE | Low-power/simple | ~12-20% | Basic (8-bit sum) |
| COBS | Binary 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:
| Field | Size | Value | Description |
|---|---|---|---|
| SOF | 1 byte | 0x55 | Start of Frame delimiter |
| START_TYPE | 1 byte | 0x01 | Frame start marker |
| PAYLOAD | Variable | Any | User data (byte-stuffed: 0x55 becomes 0x55 0x02) |
| CRC_LOW | 1 byte | Calculated | Low byte of CRC-16 (byte-stuffed) |
| CRC_HIGH | 1 byte | Calculated | High byte of CRC-16 (byte-stuffed) |
| EOF | 1 byte | 0x55 | End of Frame delimiter |
| END_TYPE | 1 byte | 0x03 | Frame 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 counterReception 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:
- Scan data for zero bytes
- For each segment between zeros:
- Count non-zero bytes (max 254)
- Insert code byte before segment
- Code byte = count + 1
- 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:
- Read code byte
- If code == 0x00: Frame delimiter (start/end)
- If code == 0xFF: Copy next 254 bytes as-is
- If code < 0xFF:
- Copy next (code-1) bytes
- Insert 0x00 after them (unless end of data)
- 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 counterReception 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:
| Application | RX Buffer | TX Buffer |
|---|---|---|
| Low-speed | 256 bytes | 256 bytes |
| Standard | 512 bytes | 512 bytes |
| High-speed | 1024 bytes | 1024 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
| Mode | Frame Size | Transmission Time | Throughput |
|---|---|---|---|
| RAW | 1000 | 86.8 ms | 11.52 KB/s |
| ESC (worst case) | 2006 | 174.2 ms | 5.74 KB/s |
| ESC (typical) | 1200 | 104.2 ms | 9.59 KB/s |
| ESC_SIMPLE (typical) | 1150 | 99.8 ms | 10.02 KB/s |
| COBS (best) | 1007 | 87.4 ms | 11.44 KB/s |
| COBS (typical) | 1020 | 88.5 ms | 11.30 KB/s |
Latency Analysis
Frame Detection Latency:
| Mode | Start Detection | End Detection | Total |
|---|---|---|---|
| RAW | N/A | N/A | 0 |
| ESC | 2 bytes | 2 bytes | ~0.17 ms @ 115200 |
| COBS | 1 byte | 1 byte | ~0.09 ms @ 115200 |
CPU Usage
Relative Processing Cost:
| Mode | Encoding | Decoding | CRC |
|---|---|---|---|
| RAW | None | None | None |
| ESC | Low | Medium | High |
| ESC_SIMPLE | Low | Medium | Low |
| COBS | Medium | Medium | High |
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
| Symptom | Possible Cause | Solution |
|---|---|---|
| High CRC errors | Noisy channel | Lower baud rate, add shielding |
| Framing errors | Timing issues | Check baud rate configuration |
| No reception | Wrong mode | Verify sender/receiver mode match |
| Intermittent loss | Buffer overflow | Increase 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+
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve this page !
Sorry to hear that. Please tell us how we can improve it !