3. 串口通信协议设计
任务目标
为Bootloader与上位机之间的通信设计一个可靠的帧协议,解决数据粘包、丢包和校验问题。协议帧格式包含帧头、长度、数据和校验字段,本案例实现帧的封装与解析函数,并验证其正确性。
协议定义
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 帧头 | 2 | 固定值 0xAA 0x55 |
| 长度 | 1 | 数据域长度(不含帧头、长度、校验) |
| 数据 | 0~255 | 实际载荷,如命令字、地址、固件数据等 |
| 校验 | 1 | 累加和校验(帧头+长度+数据所有字节求和取低8位) |
完整代码(C语言实现)
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#define FRAME_HEADER0 0xAA
#define FRAME_HEADER1 0x55
#define MAX_DATA_LEN 255
#define FRAME_OVERHEAD 4 // 帧头2 + 长度1 + 校验1
typedef enum {
PARSE_OK = 0,
PARSE_ERR_HEADER,
PARSE_ERR_LEN,
PARSE_ERR_CHECKSUM
} ParseResult;
// 计算校验:帧头 + 长度 + 数据 逐字节累加取低8位
static uint8_t calc_checksum(const uint8_t *data, uint16_t len) {
uint8_t sum = 0;
for (uint16_t i = 0; i < len; i++) {
sum += data[i];
}
return sum;
}
// 封装帧:将payload封装为完整帧写入out_buf,返回帧总长度
uint16_t frame_pack(const uint8_t *payload, uint16_t payload_len,
uint8_t *out_buf, uint16_t out_buf_size) {
if (!payload || !out_buf || payload_len > MAX_DATA_LEN) return 0;
if (out_buf_size < payload_len + FRAME_OVERHEAD) return 0;
uint16_t idx = 0;
out_buf[idx++] = FRAME_HEADER0;
out_buf[idx++] = FRAME_HEADER1;
out_buf[idx++] = (uint8_t)payload_len;
memcpy(&out_buf[idx], payload, payload_len);
idx += payload_len;
// 校验范围:帧头2字节 + 长度1字节 + 数据
uint8_t checksum = calc_checksum(out_buf, idx);
out_buf[idx++] = checksum;
return idx;
}
// 解析帧:从rx_buf中提取一帧,成功时payload_out存放数据,返回PARSE_OK
ParseResult frame_parse(const uint8_t *rx_buf, uint16_t rx_len,
uint8_t *payload_out, uint16_t *payload_len_out) {
if (!rx_buf || !payload_out || !payload_len_out || rx_len < FRAME_OVERHEAD)
return PARSE_ERR_HEADER;
// 检查帧头
if (rx_buf[0] != FRAME_HEADER0 || rx_buf[1] != FRAME_HEADER1)
return PARSE_ERR_HEADER;
uint8_t data_len = rx_buf[2];
if (data_len + FRAME_OVERHEAD > rx_len)
return PARSE_ERR_LEN;
// 校验
uint8_t calc = calc_checksum(rx_buf, 3 + data_len); // 帧头+长度+数据
if (calc != rx_buf[3 + data_len])
return PARSE_ERR_CHECKSUM;
memcpy(payload_out, &rx_buf[3], data_len);
*payload_len_out = data_len;
return PARSE_OK;
}
// 测试验证
int main(void) {
// 构造一帧:命令0x01 + 地址0x1000(小端)+ 数据0xDE 0xAD
uint8_t payload[] = {0x01, 0x00, 0x10, 0xDE, 0xAD};
uint8_t frame[64];
uint16_t frame_len = frame_pack(payload, sizeof(payload), frame, sizeof(frame));
printf("封装帧 (%d字节): ", frame_len);
for (int i = 0; i < frame_len; i++) printf("%02X ", frame[i]);
printf("\n");
// 模拟接收并解析
uint8_t payload_out[64];
uint16_t payload_len_out = 0;
ParseResult ret = frame_parse(frame, frame_len, payload_out, &payload_len_out);
if (ret == PARSE_OK) {
printf("解析成功,数据长度=%d,内容: ", payload_len_out);
for (int i = 0; i < payload_len_out; i++) printf("%02X ", payload_out[i]);
printf("\n");
} else {
printf("解析失败,错误码=%d\n", ret);
}
// 错误注入测试:篡改一个数据字节
frame[4] ^= 0xFF;
ret = frame_parse(frame, frame_len, payload_out, &payload_len_out);
printf("篡改后解析结果: %s (错误码=%d)\n",
ret == PARSE_OK ? "成功" : "失败", ret);
return 0;
}
编译与运行
gcc -o frame_test frame_test.c
./frame_test
验证方法与通过条件
- 正常路径:程序输出封装后的完整帧字节序列,随后解析成功并打印与原始payload一致的数据内容。
- 错误检测:篡改数据字节后,解析返回
PARSE_ERR_CHECKSUM,证明校验能捕获数据损坏。 - 判定标准:若输出中“解析成功”且数据内容与输入一致,同时篡改测试返回失败,则协议封装/解析逻辑正确。
Bootloader中的使用要点
在实际Bootloader中,串口接收通常采用中断+环形缓冲区。每收到一个字节即存入缓冲区,然后调用frame_parse尝试解析;返回PARSE_OK即表示收到完整一帧,可进入命令处理流程。若返回PARSE_ERR_HEADER,说明帧头不匹配,应丢弃缓冲区首字节并继续向后搜索帧头;若返回PARSE_ERR_LEN,说明数据尚未收完,等待后续字节;若返回PARSE_ERR_CHECKSUM,则丢弃当前帧并重新同步。