1. 项目概述

Bolder Flight Systems Message Framing 是一个轻量级、零依赖的嵌入式消息帧化(Message Framing)库,专为资源受限的实时系统设计。其核心目标并非实现完整通信协议栈,而是提供一种 可预测、可验证、抗干扰强 的二进制数据封装机制,使原始数据载荷(payload)能够安全、可靠地穿越不可靠信道——无论是 UART 串口线缆、RS-485 总线、CAN FD 的应用层扩展,还是 Flash 存储扇区中的结构化日志。

该库不处理物理层电气特性、波特率配置、中断服务程序(ISR)注册或 DMA 传输调度;它只关注“数据如何被包装”与“包装如何被解开”这两个确定性问题。所有逻辑均在单个头文件 framing.h 中完成,无 .cpp 文件、无全局状态、无动态内存分配( malloc/free ),完全符合 MISRA-C:2012 Rule 21.3 和 AUTOSAR C++14 对静态内存使用的强制要求。这种“header-only + template-driven + stack-allocated buffer”设计,使其天然适配于裸机(Bare-Metal)、FreeRTOS、Zephyr 等各类 RTOS 环境,亦可无缝集成至 STM32CubeIDE、IAR EWARM、Keil MDK 等主流工具链。

其技术定位清晰: 是 HAL 层之上的数据序列化基础设施,而非通信协议本身 。开发者仍需自行管理 UART 外设初始化、接收缓冲区填充、发送完成中断回调等底层事务;而本库则确保:只要传入的数据字节流中存在合法帧,就能被精准识别、校验、解包;只要待发送数据长度未超限,就能生成符合规范的帧并交付底层发送。


2. 帧格式规范与工程设计原理

2.1 标准帧结构

每一帧由严格定义的字段按序组成,总长度可变,但结构恒定:

字段 长度(字节) 值/说明
Header 1 固定值 0x7E ,作为帧起始同步字节(Sync Byte)
Payload N(可变) 原始用户数据,长度 0 ≤ N ≤ PAYLOAD_SIZE
Checksum 2 Fletcher-16 校验和(Big-Endian),覆盖 Header 后全部内容(含 Payload)
Footer 1 固定值 0x7E ,作为帧结束标识

关键设计意图 :Header 与 Footer 使用相同字节 0x7E ,极大简化了接收端帧边界检测逻辑——只需查找连续出现的 0x7E 即可定位潜在帧头/尾。但此设计引入新问题:若 Payload 中恰好出现 0x7E 或校验和字节为 0x7E ,将导致接收端误判帧边界。为此,库采用**字节转义(Byte Stuffing)**机制。

2.2 字节转义(Byte Stuffing)机制

为解决 0x7E 0x7D 在 Payload 或 Checksum 中的歧义,库定义如下转义规则:

原始字节 转义后序列 说明
0x7E 0x7D 0x5E 0x7D 为转义标记(Escape Byte), 0x5E 0x7E XOR 0x20
0x7D 0x7D 0x5D 0x7D 为转义标记, 0x5D 0x7D XOR 0x20

为什么选择 0x20 作为异或偏移?
0x20 (空格字符 ASCII 码)是 7-bit 可打印字符集中最低的控制字符,其高比特位为 0 。对任意字节 b 执行 b ^ 0x20 ,结果的最高位保持不变(即不产生 0x80 及以上字节),从而避免引入新的控制字符或不可见字节,降低在某些老旧终端或调试器中显示异常的风险。同时, 0x20 非零且非 0xFF ,保证了转义后字节 0x7D 0x5E / 0x7D 0x5D 绝不会与原始 0x7E 0x7D 冲突,形成无歧义的双字节编码。

2.3 Fletcher-16 校验和算法

校验和计算范围:从 Header 字节 0x7E 开始,至 Payload 最后一字节结束( 不包含 Footer 0x7E )。算法采用标准 Fletcher-16 实现:

// 伪代码:Fletcher-16 计算(Big-Endian 输出)
uint16_t fletcher16(const uint8_t *data, size_t len) {
  uint16_t sum1 = 0, sum2 = 0;
  for (size_t i = 0; i < len; i++) {
    sum1 = (sum1 + data[i]) % 255;
    sum2 = (sum2 + sum1) % 255;
  }
  return (sum2 << 8) | sum1; // Big-Endian: high byte first
}

工程考量 :Fletcher-16 相比 CRC-16 具有极低的计算开销(仅需加法与模运算),在 Cortex-M0/M0+ 等无硬件乘法器的 MCU 上性能优势显著。其检错能力虽弱于 CRC-16,但对于串口通信中常见的单比特翻转、突发错误(burst error ≤ 16 bits)已足够鲁棒。库中校验和参与转义,确保其自身亦被正确解码。

2.4 缓冲区容量设计

FrameEncoder FrameDecoder 均以模板参数 PAYLOAD_SIZE 指定最大有效载荷长度。内部缓冲区大小固定为 2 * PAYLOAD_SIZE + 5 字节,推导依据如下:

  • 最坏情况:Payload 全为 0x7E 0x7D ,每个字节需扩展为 2 字节 → 2 * PAYLOAD_SIZE
  • Header ( 0x7E ):1 字节
  • Footer ( 0x7E ):1 字节
  • Checksum:2 字节
  • 总计: 2 * PAYLOAD_SIZE + 4
  • 额外 +1 字节:为 Found() 方法内部状态机预留哨兵空间,避免边界溢出

此设计保证:无论 Payload 内容如何,只要其原始长度 ≤ PAYLOAD_SIZE ,编码后帧必能容纳于缓冲区,无需运行时长度检查或重试。


3. 核心 API 接口详解

3.1 FrameEncoder<PAYLOAD_SIZE>

构造函数
template<size_t PAYLOAD_SIZE>
class FrameEncoder {
public:
  FrameEncoder();
  // ...
};
  • 作用 :构造编码器实例,静态分配 2*PAYLOAD_SIZE+5 字节栈缓冲区。
  • 工程实践 :应在 main() 或任务函数栈上声明,或作为全局/静态对象。禁止在堆上 new 分配(违反 header-only 设计哲学)。
  • 示例
    // 支持最大 128 字节 payload 的编码器
    bfs::FrameEncoder<128> uart_tx_encoder;
    
size_t Write(const uint8_t* data, size_t len)
  • 参数
    • data : 指向原始 payload 数据的指针(非 const,因内部需遍历)
    • len : payload 长度(字节),必须 ≤ PAYLOAD_SIZE
  • 返回值 :成功写入的 payload 字节数(即 len ,若 len > PAYLOAD_SIZE 则行为未定义)
  • 关键行为
    1. 0x7E 写入缓冲区首字节;
    2. data[0..len-1] 逐字节执行转义,写入缓冲区;
    3. 计算 Fletcher-16 校验和(输入: 0x7E + 转义后 payload);
    4. 将校验和(Big-Endian)写入缓冲区;
    5. 0x7E 写入缓冲区末字节。
  • 线程安全 :非线程安全。若在 ISR 与主循环间共享,需加临界区保护(如 __disable_irq() / __enable_irq() )。
size_t size() const
  • 返回值 :当前已编码帧的总字节数(含 Header、转义 Payload、Checksum、Footer)。
  • 用途 :获取 data() 返回缓冲区的有效长度,用于 HAL_UART_Transmit() write() 系统调用。
const uint8_t* data() const
  • 返回值 :指向内部缓冲区首地址的常量指针。
  • 注意 :此指针生命周期与 FrameEncoder 对象一致。切勿在对象析构后使用。

3.2 FrameDecoder<PAYLOAD_SIZE>

构造函数
template<size_t PAYLOAD_SIZE>
class FrameDecoder {
public:
  FrameDecoder();
  // ...
};
  • 作用 :构造解码器实例,静态分配 2*PAYLOAD_SIZE+5 字节栈缓冲区,用于暂存接收到的原始字节流及解包后 payload。
  • 示例
    bfs::FrameDecoder<128> uart_rx_decoder;
    
bool Found(uint8_t byte)
  • 参数 :单字节输入,通常来自 UART RX ISR 或环形缓冲区 pop()
  • 返回值
    • true : 成功识别并验证一帧,解包后的 payload 已就绪;
    • false : 当前字节未触发帧完成,或校验失败,或帧格式错误。
  • 内部状态机
    • IDLE : 等待首个 0x7E
    • IN_FRAME : 接收 payload 及 checksum 字节,遇 0x7D 进入转义模式;
    • ESCAPE : 下一字节需异或 0x20 后存入缓冲区;
    • CHECKSUM : 接收完 2 字节 checksum 后,立即校验;成功则置 found_ = true ,否则清空缓冲区回 IDLE
  • 关键特性 :支持 流式解析 。可将 UART 接收 ISR 中逐字节调用 Found() ,无需等待整帧到达,极大降低 RAM 占用与延迟。
size_t available() const
  • 返回值 :解包后 payload 的剩余可读字节数(即 size() 减去已 Read() 字节数)。
uint8_t Read()
  • 返回值 :返回 payload 中下一个字节,并将内部读取索引 ++
  • 适用场景 :逐字节处理传感器数据、协议状态机驱动。
size_t Read(uint8_t* data, size_t len)
  • 参数
    • data : 目标缓冲区指针;
    • len : 最大读取字节数。
  • 返回值 :实际复制的字节数(≤ len 且 ≤ available() )。
  • 典型用法 :将解包数据批量拷贝至应用层结构体。
const uint8_t* data() const
  • 返回值 :指向解包后 payload 起始地址的常量指针(不含 Header/Footer/Checksum)。
size_t size() const
  • 返回值 :解包后 payload 的总长度(等价于 available() 初始值)。

4. 典型应用场景与代码示例

4.1 STM32 HAL + FreeRTOS 串口透传(带心跳帧)

需求 :MCU 通过 UART 向上位机发送传感器数据,要求每 100ms 发送一帧,帧内含温度、湿度、时间戳,并支持上位机下发指令。

实现要点

  • 使用 FrameEncoder<32> 编码传感器数据;
  • 在 FreeRTOS Timer Callback 中构建 payload 并编码;
  • 通过 HAL_UART_Transmit_IT() 异步发送编码后帧;
  • UART RX ISR 中逐字节调用 FrameDecoder::Found() ,解包后通过队列通知处理任务。
// 全局对象(位于 .c 文件顶部)
static bfs::FrameEncoder<32> tx_encoder;
static bfs::FrameDecoder<32> rx_decoder;
static QueueHandle_t cmd_queue;

// 定时器回调:构建并发送传感器帧
void sensor_timer_callback(TimerHandle_t xTimer) {
  uint8_t payload[32];
  // 构建 payload: [temp_H][temp_L][humi_H][humi_L][ts_sec...]
  int16_t temp = read_temperature();
  uint16_t humi = read_humidity();
  uint32_t ts = HAL_GetTick();
  
  payload[0] = (temp >> 8) & 0xFF;
  payload[1] = temp & 0xFF;
  payload[2] = (humi >> 8) & 0xFF;
  payload[3] = humi & 0xFF;
  memcpy(&payload[4], &ts, 4);

  // 编码
  tx_encoder.Write(payload, 8);
  
  // 异步发送
  HAL_UART_Transmit_IT(&huart2, 
                      const_cast<uint8_t*>(tx_encoder.data()), 
                      tx_encoder.size());
}

// UART2 RX ISR(精简版)
void USART2_IRQHandler(void) {
  HAL_UART_IRQHandler(&huart2);
}

// UART Rx Complete Callback(由 HAL 调用)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
  if (huart == &huart2) {
    uint8_t rx_byte;
    HAL_UART_Receive(&huart2, &rx_byte, 1, HAL_MAX_DELAY);
    
    if (rx_decoder.Found(rx_byte)) {
      // 帧接收完成,投递到队列
      BaseType_t xHigherPriorityTaskWoken = pdFALSE;
      xQueueSendFromISR(cmd_queue, rx_decoder.data(), &xHigherPriorityTaskWoken);
      portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
    
    // 重新启动接收(单字节模式)
    HAL_UART_Receive_IT(&huart2, &rx_byte, 1);
  }
}

4.2 裸机环境下的 Flash 日志记录

需求 :将飞行控制器关键事件(如电机启动、IMU 校准完成)以结构化帧写入外部 SPI Flash,供事后分析。

挑战 :Flash 写入需按页(Page)操作,且擦除耗时长;帧必须自描述、可独立解析。

方案 :利用 FrameEncoder 生成自包含帧,每帧写入 Flash 一个扇区(Sector),帧头 0x7E 作为扇区有效标志。

// 假设 flash_write_sector(uint32_t addr, const uint8_t* data, size_t len)
void log_event(const char* event_str) {
  static bfs::FrameEncoder<64> logger;
  uint8_t payload[64];
  size_t str_len = strlen(event_str);
  
  // 构建 payload: [timestamp][event_id][string...]
  uint32_t ts = get_system_time_ms();
  memcpy(payload, &ts, 4);
  payload[4] = EVENT_ID_SYSTEM; // 自定义事件 ID
  memcpy(&payload[5], event_str, str_len);
  
  // 编码
  logger.Write(payload, 5 + str_len);
  
  // 写入 Flash(假设扇区大小 4KB,此处仅示意)
  static uint32_t log_addr = FLASH_LOG_BASE;
  flash_write_sector(log_addr, logger.data(), logger.size());
  log_addr += logger.size(); // 更新下一次写入地址
}

4.3 与 Bolder Flight Systems Checksum 库协同

framing 库依赖 bfs::Checksum 提供 Fletcher-16 实现。其头文件 checksum.h 提供:

namespace bfs {
  uint16_t Fletcher16(const uint8_t* data, size_t len);
  // ... 其他校验和算法
}

FrameEncoder 内部即调用此函数。若需自定义校验(如升级为 CRC-32),可继承 FrameEncoder 并重写 ComputeChecksum() 方法(需修改源码),但需确保转义逻辑与校验范围严格一致。


5. 配置与集成指南

5.1 Arduino 环境集成

  1. 安装依赖

    • 打开 Arduino IDE → Sketch Include Library Manage Libraries...
    • 搜索 Bolder Flight Systems Checksum ,安装最新版;
    • 搜索 Bolder Flight Systems Framing ,安装最新版。
  2. 代码包含

    #include "framing.h"
    #include "checksum.h" // 显式包含(部分 IDE 需要)
    
  3. 编译约束 FrameEncoder<200> 在 Arduino Uno (ATmega328P) 上占用约 405 字节 RAM,需确保 PAYLOAD_SIZE 设置合理。

5.2 CMake 项目集成

CMakeLists.txt 中:

# 方式1:子模块方式(推荐)
add_subdirectory(external/bolder-flight-framing)
target_link_libraries(your_target PRIVATE framing)

# 方式2:find_package(需先安装)
find_package(bfs_framing REQUIRED)
target_link_libraries(your_target PRIVATE bfs_framing::framing)

头文件包含路径自动添加,直接 #include "framing.h" 即可。

5.3 关键编译选项

  • -DBFS_FRAMING_DISABLE_ESCAPE : 定义后禁用字节转义,仅适用于 Payload 确保不含 0x7E / 0x7D 的封闭环境(如片内 RAM 通信),可节省约 15% 代码体积。
  • -DBFS_FRAMING_NO_CHECKSUM : 禁用校验和计算与验证(仅保留 Header/Footer), 强烈不推荐 ,仅用于调试或极低端 MCU。

6. 故障排查与性能优化

6.1 常见问题诊断

现象 可能原因 解决方案
Found() 永远返回 false 接收字节流中无 0x7E 检查 UART 波特率、电平、线序;用逻辑分析仪抓波形
Found() 返回 true size() 为 0 校验和失败 确认发送端与接收端 PAYLOAD_SIZE 一致;检查转义逻辑是否被意外修改
Write() size() 异常大 len > PAYLOAD_SIZE 在调用前添加 assert(len <= PAYLOAD_SIZE)

6.2 性能优化建议

  • 减少拷贝 FrameEncoder::Write() 输入为 const uint8_t* ,避免将数据先复制到中间缓冲区再传入。
  • 预分配缓冲区 :若 payload 长度固定(如 16 字节传感器数据),设置 PAYLOAD_SIZE=16 ,而非 200 ,可显著减小栈占用。
  • ISR 优化 :在高频 UART 接收场景下,将 FrameDecoder::Found() 内联( inline ),并确保其汇编代码紧凑(GCC: -O2 -finline-functions )。

7. 与其他帧协议对比

特性 BFS Framing SLIP (RFC 1055) HDLC (ISO/IEC 3309) CAN FD Data Frame
Header/Footer 0x7E 0xC0 0x7E 无(隐含)
Escape Byte 0x7D 0xDB 0x7D 不适用
Escape XOR 0x20 0xDC / 0xDD 0x20
Checksum Fletcher-16 CRC-16/32 CRC-15
Overhead (worst) ~100% ~100% ~100% ~1.5% (64B)
MCU Friendly ★★★★★ ★★★★☆ ★★☆☆☆ ★★★★★
标准兼容性 自定义 IETF 标准 ISO 标准 ISO 11898-1

结论 :BFS Framing 在“轻量性”与“鲁棒性”间取得最佳平衡,是嵌入式设备点对点串行通信的理想选择,尤其适合飞行控制器、机器人、工业传感器等对可靠性与资源占用均有严苛要求的场景。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐