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,则丢弃当前帧并重新同步。

帧头0 帧头1 长度 数据域 (0~255字节) 校验 0xAA 0x55 N payload[0..N-1] sum&0xFF 累加和校验范围(帧头+长度+数据) 接收解析状态 等待帧头0 (0xAA) 等待帧头1 (0x55) 接收长度+数据+校验