单片机通信协议拆解:从 0 到 1 搞定 CRC 校验与帧解析,新手也能懂!单片机串口协议解析(含完整代码 + 流程图)

一、零基础视角拆解核心知识点(通俗版)

先把上文中的核心概念、结构体、功能拆解成 “人话”,确保初学者能看懂每一个部分的作用和意义:

1. 先理解核心目标:单片机通信协议解析到底是干啥?

你可以把单片机之间 / 单片机和电脑之间的通信,想象成两个人打电话:

  • 两个人说同一种语言(比如都讲中文)= 双方约定好通信协议(就是我们定义的帧头、长度、CRC、帧尾规则);

  • 打电话时先喊 “喂,是小明吗?”(确认对方)= 协议里的帧头(0xAA+0x55)

  • 接着说 “我要讲 3 句话”(告诉对方要听多少内容)= 协议里的数据长度

  • 然后说具体内容(“今天下雨、记得带伞、晚上吃火锅”)= 协议里的数据负载

  • 说完加一句 “我说的没错吧”(验证内容没传错)= 协议里的CRC 校验

  • 最后说 “拜拜”(表示说完了)= 协议里的帧尾(0x0D)

我们写的代码,本质就是让单片机 “听懂” 这套 “通话规则”,能准确接收对方说的话,还能检查有没有听错(CRC 校验),听错了就重新听,没听错就处理内容。

2. 拆解核心代码模块(初学者能懂的解释)

代码模块

通俗解释

CRC16 计算函数(crc16_calc)

给一段数据 “算个验证码”。发送方算好验证码一起发,接收方重新算一遍,对比验证码是否一致,一致就说明数据没传错。

解析器状态枚举(parse_state_t)

给单片机的 “接收过程” 分阶段:比如 “先等对方喊喂→再等确认身份→再听要讲多少内容→再听具体内容→再验证验证码→最后等拜拜”,每个阶段就是一个 “状态”。

解析器结构体(proto_parser_t)

给单片机准备一个 “小本子”,用来记:1. 当前听到哪个阶段了(state);2. 对方说的具体内容记下来(data_buf);3. 对方说要讲多少内容(data_len);4. 对方发的验证码是多少(recv_crc);5. 已经记了多少内容(buf_idx)。

初始化函数(proto_parser_init)

通话前把 “小本子” 清空,让单片机从 “等待对方喊喂” 这个阶段开始准备。

单字节解析函数(proto_parse_byte)

单片机每次只接 1 个字节(比如只听到 “喂”),这个函数就是判断 “当前该做什么”:比如听到 “喂” 就等下一句,听到 “拜拜” 就验证验证码,错了就清空本子重新来。

打包函数(proto_pack_frame)

单片机要说话时,按 “喂→说长度→说内容→算验证码→说拜拜” 的顺序,把要讲的话打包成一串,方便发送。

3. 为什么要用这些结构(初学者最容易问的问题)
  • 为什么用枚举(enum)?

    不用记数字(比如用 1 代表等帧头、2 代表等长度),而是用 “STATE_WAIT_HEADER1” 这种一看就懂的名字,代码读起来像 “说人话”,不容易写错,也方便改。

  • 为什么用结构体(struct)?

    把和 “接收数据” 相关的所有信息(状态、缓存、长度等)放在一起,像一个 “打包文件”,不用零散记很多变量,比如要传这些信息时,直接传结构体就行,不用传 10 个单独的变量。

  • 为什么用状态机(分阶段接收)?

    单片机接收数据是 “一个字节一个字节来” 的,不可能一次性收到完整的一帧。状态机就像 “按步骤办事”,比如没听到 “喂” 之前,就算收到其他内容也不理,避免接收到错误数据。

  • 为什么要 CRC 校验?

    单片机通信(比如串口)容易受干扰(比如电线、电磁影响),数据可能传错(比如把 “3” 传成 “8”)。CRC 就是给数据做个 “指纹”,指纹对不上就说明数据错了,直接扔了重新接,保证数据准确。

二、初学者需要学习的核心知识(按优先级排序)

要能看懂、写得出上面的代码,初学者需要按以下顺序学习,每一步都打牢基础:

1. 必学基础(核心中的核心)
  • C 语言核心语法(优先级最高):

    • 基本类型:uint8_t/uint16_t(单片机常用的无符号 8 位 / 16 位整数,比 int 更精准);

    • 数组:理解data_buf[MAX_DATA_LEN]是 “存数据的格子”,知道数组索引(buf_idx)怎么用;

    • 结构体(struct):理解 “把相关变量打包” 的思想,会定义、初始化、访问结构体成员(比如parser->state);

    • 枚举(enum):理解 “给数字起名字” 的作用,会定义和使用;

    • 函数:会写带参数、带返回值的函数(比如crc16_calc),理解函数的输入输出;

    • 条件判断(if/else)、循环(for/while):状态机的核心逻辑全靠这两个,必须熟练。

  • 单片机基础

    • 串口通信原理:知道单片机怎么通过串口发 / 收字节(比如 UART 的基本工作方式);

    • 中断概念:理解 “串口接收中断”—— 数据来了单片机自动触发处理,而不是一直等(这是实时解析的关键);

    • 内存常识:知道单片机的 RAM 有限,不能定义太大的数组(比如MAX_DATA_LEN不能设成 10000),避免内存溢出。

2. 进阶知识(理解协议设计思想)
  • 通信协议基本概念

    • 帧结构设计:理解 “帧头 + 长度 + 数据 + 校验 + 帧尾” 的设计逻辑(为什么要有帧头?防止误触发;为什么要有长度?知道该收多少数据);

    • 校验算法(CRC16):不用死记 CRC 的计算细节,但要理解 “校验的目的是验证数据完整性”,会调用现成的 CRC 函数即可;

    • 状态机思想:理解 “分阶段处理事件” 的逻辑(不只是通信,单片机的按键处理、传感器读取都能用)。

  • 单片机编程规范

    • 宏定义(#define):用宏定义代替魔法数字(比如FRAME_HEADER_1代替 0xAA),方便修改和阅读;

    • 错误处理:理解 “解析失败就重置解析器” 的思想,避免单片机卡死在错误状态;

    • 内存安全:知道 “缓冲区溢出” 的危害(比如数据长度超过数组大小),会做长度校验(if (len > MAX_DATA_LEN) return 0)。

3. 实践类知识(把代码落地)
  • 硬件实操

    • 会用串口助手(比如 SSCOM)模拟发送数据,测试单片机的解析功能(比如发AA 55 03 11 22 33 XX XX 0D,看单片机是否能正确解析);

    • 会调试单片机代码:比如用 printf 打印解析状态(当前是等待帧头 / 读取数据),看哪里出了问题。

  • 代码优化思路

    • 精简代码:比如数据长度为 0 时,直接跳过数据读取阶段,减少无用操作;

    • 适配不同单片机:比如把代码从 STM32 移植到 Arduino,只需修改串口接收的部分,核心解析逻辑不用改。

三、总结

  1. 核心概念

    :单片机通信协议解析的本质是 “约定一套数据格式 + 分阶段接收验证”,结构体是 “存储接收状态的小本子”,状态机是 “分阶段接收的规则”,CRC 是 “数据验证码”;

  2. 学习顺序

    :先吃透 C 语言的结构体 / 枚举 / 数组 → 理解单片机串口和中断 → 掌握状态机和校验的思想 → 动手调试代码;

  3. 关键思维

    :初学者要从 “能跑通” 到 “能理解”,重点是搞懂 “为什么要这么设计”(比如为什么要帧头、为什么要 CRC),而不是死记代码。

初学者不用一开始就追求写完美的协议,先实现 “能接收数据 + 简单校验”,再逐步优化(比如加帧头帧尾、加 CRC),慢慢就能理解通信协议的核心逻辑了。

一、协议核心设计(帧结构 + 代码功能)

1. 帧结构定义(固定格式,便于单片机解析)

字段

字节数

说明

帧头 1

1

固定值0xAA,帧起始标识 1

帧头 2

1

固定值0x55,帧起始标识 2(双重校验避免误触发)

数据长度

1

后续数据负载的字节数(范围 0~64,可通过MAX_DATA_LEN调整)

数据负载

N

实际传输的数据(N = 数据长度字段值,最大 64 字节)

CRC-16 校验

2

多项式0x8005,计算范围:数据长度字段 + 数据负载(确保数据完整性)

帧尾

1

固定值0x0D,帧结束标识

2. 核心代码功能
  • 状态机解析

    :按 “帧头→长度→数据→CRC→帧尾” 分步处理字节流,适配单片机串口中断接收场景。

  • CRC 校验

    :发送端计算校验值封装到帧中,接收端重新计算并比对,排除数据传输错误。

  • 错误处理

    :帧头不匹配、长度超限、CRC 校验失败、帧尾错误时,自动重置解析器重新同步。

  • 资源优化

    :使用静态缓冲区,不依赖动态内存分配,适合 STM32、Arduino 等资源受限的 MCU。

二、完整代码实现(单片机适配版)


#include <stdint.h>
#include <string.h>

// 协议配置参数(可根据实际需求修改)
#define FRAME_HEADER_1 0xAA    // 帧头第1字节
#define FRAME_HEADER_2 0x55    // 帧头第2字节
#define FRAME_TAIL     0x0D    // 帧尾字节
#define MAX_DATA_LEN   64      // 最大数据负载长度
#define CRC16_POLY     0x8005  // CRC-16多项式

// CRC-16计算函数(输入:数据地址+长度,输出:16位校验值)
uint16_t crc16_calc(const uint8_t *data, uint16_t len) {
    uint16_t crc =0xFFFF;// 初始值
for(uint16_t i =0; i < len; i++){
        crc =(uint16_t)data[i]<<8;// 数据左移8位后与CRC异或
for(uint8_t j =0; j <8; j++){// 逐位处理
            crc =(crc &0x8000)?(crc <<1CRC16_POLY):(crc <<1);
}
}
return crc;
}

// 解析器状态枚举(状态机核心)
typedef enum {
STATE_WAIT_HEADER1,// 等待帧头第1字节
STATE_WAIT_HEADER2,// 等待帧头第2字节
STATE_READ_LEN,// 读取数据长度
STATE_READ_DATA,// 读取数据负载
STATE_READ_CRC_H,// 读取CRC高字节
STATE_READ_CRC_L,// 读取CRC低字节
STATE_READ_TAIL// 读取帧尾
} parse_state_t;

// 解析器结构体(存储解析状态和缓存数据)
typedef struct {
    parse_state_t state;// 当前解析状态
    uint8_t data_buf[MAX_DATA_LEN];// 数据负载缓存
    uint8_t data_len;// 接收的数据长度
    uint16_t recv_crc;// 接收的CRC值
    uint8_t buf_idx;// 数据缓存索引
} proto_parser_t;

// 初始化解析器(调用一次即可)
void proto_parser_init(proto_parser_t *parser) {
    parser->state =STATE_WAIT_HEADER1;
    parser->data_len =0;
    parser->recv_crc =0;
    parser->buf_idx =0;
memset(parser->data_buf,0,MAX_DATA_LEN);
}

// 单字节解析函数(串口中断中调用,每接收1字节执行1次)
// 返回值:0=解析中,1=解析成功,-1=解析失败
int8_t proto_parse_byte(proto_parser_t *parser, uint8_t byte) {
switch(parser->state){
// 状态1:等待帧头第1字节(0xAA)
caseSTATE_WAIT_HEADER1:
if(byte ==FRAME_HEADER_1){
                parser->state =STATE_WAIT_HEADER2;// 找到后进入下一状态
}
break;

// 状态2:等待帧头第2字节(0x55)
caseSTATE_WAIT_HEADER2:
if(byte ==FRAME_HEADER_2){
                parser->state =STATE_READ_LEN;// 帧头完整,进入读取长度状态
}else{
                parser->state =STATE_WAIT_HEADER1;// 帧头错误,重置
}
break;

// 状态3:读取数据长度
caseSTATE_READ_LEN:
if(byte <=MAX_DATA_LEN){// 长度合法(不超过最大限制)
                parser->data_len = byte;
                parser->state =(byte ==0)?STATE_READ_CRC_H:STATE_READ_DATA;
// 数据长度为0时,直接跳过数据读取,进入CRC读取
}else{
proto_parser_init(parser);// 长度非法,重置解析器
return-1;
}
break;

// 状态4:读取数据负载
caseSTATE_READ_DATA:
            parser->data_buf[parser->buf_idx++]= byte;// 存入缓存
// 数据读取完成(索引达到长度)
if(parser->buf_idx >= parser->data_len){
                parser->buf_idx =0;// 重置索引
                parser->state =STATE_READ_CRC_H;// 进入CRC读取状态
}
break;

// 状态5:读取CRC高字节
caseSTATE_READ_CRC_H:
            parser->recv_crc =(uint16_t)byte <<8;// 高字节左移8位
            parser->state =STATE_READ_CRC_L;
break;

// 状态6:读取CRC低字节
caseSTATE_READ_CRC_L:
            parser->recv_crc |= byte;// 拼接低字节
            parser->state =STATE_READ_TAIL;
break;

// 状态7:读取帧尾(0x0D)
caseSTATE_READ_TAIL:
if(byte ==FRAME_TAIL){// 帧尾正确,验证CRC
// 计算CRC:数据长度字段 + 数据负载
                uint8_t crc_input[MAX_DATA_LEN+1];
                crc_input[0]= parser->data_len;
memcpy(&crc_input[1], parser->data_buf, parser->data_len);
                uint16_t calc_crc =crc16_calc(crc_input, parser->data_len +1);

if(calc_crc == parser->recv_crc){// CRC校验通过
proto_parser_init(parser);// 重置解析器,准备下一包
return1;// 解析成功
}else{
proto_parser_init(parser);
return-1;// CRC错误
}
}else{
proto_parser_init(parser);
return-1;// 帧尾错误
}
break;

default:
proto_parser_init(parser);// 异常状态重置
return-1;
}
return0;// 解析中
}

// 示例:发送帧封装函数(将数据打包为协议帧)
uint8_t proto_pack_frame(uint8_t *data, uint8_t len, uint8_t *frame_buf) {
if(len >MAX_DATA_LEN)return0;// 数据长度超限

// 帧头
    frame_buf[0]=FRAME_HEADER_1;
    frame_buf[1]=FRAME_HEADER_2;
// 数据长度
    frame_buf[2]= len;
// 数据负载
memcpy(&frame_buf[3], data, len);
// CRC校验(计算范围:长度+数据)
    uint16_t crc =crc16_calc(&frame_buf[2], len +1);
    frame_buf[3+ len]=(crc >>8)&0xFF;// CRC高字节
    frame_buf[4+ len]= crc &0xFF;// CRC低字节
// 帧尾
    frame_buf[5+ len]=FRAME_TAIL;

return6+ len;// 返回帧总长度(6=头2+长1+CRC2+尾1)
}

// 主函数示例(模拟单片机运行逻辑)
int main(void) {
    proto_parser_t parser;
proto_parser_init(&parser);// 初始化解析器

// 模拟:发送端打包数据
    uint8_t send_data[]={0x11,0x22,0x33};// 待发送数据
    uint8_t send_frame[100];
    uint8_t frame_len =proto_pack_frame(send_data,sizeof(send_data), send_frame);

// 模拟:接收端逐字节解析(实际中在串口中断中调用)
for(uint8_t i =0; i < frame_len; i++){
        int8_t ret =proto_parse_byte(&parser, send_frame[i]);
if(ret  1){
// 解析成功,处理数据(parser.data_buf中存储有效数据)
// 此处可添加数据处理逻辑(如控制GPIO、上报数据等)
}elseif(ret  -1){
// 解析失败,可记录错误日志
}
}

while(1){
// 单片机主循环
}
}

三、解析流程流程图(初学者友好版)

四、流程图关键步骤解释(初学者必看)

  1. 初始化阶段

    :解析器默认处于 “等待帧头 1” 状态,所有缓存和索引清零,确保初始状态干净。

  2. 帧头校验

    :必须连续收到0xAA0x55才认为帧开始,避免单个错误字节触发解析(比如数据中恰好有0xAA)。

  3. 长度校验

    :限制数据长度不超过MAX_DATA_LEN,防止缓冲区溢出(单片机内存有限,需避免越界)。

  4. 数据读取

    :按长度字段指定的字节数读取数据,读完后自动进入 CRC 校验阶段,无需手动控制。

  5. CRC 校验

    :核心是 “发送端和接收端计算范围一致”(均包含长度 + 数据),确保数据传输中无篡改、无丢失。

  6. 错误重置

    :任何环节出错(帧头不匹配、长度超限、CRC 错误、帧尾错误),都会立即重置解析器,重新等待下一个帧头,保证解析鲁棒性。

五、实际使用场景说明

  • 串口中断集成

    :在单片机串口接收中断服务函数中,调用proto_parse_byte,每接收 1 字节触发 1 次解析,实时性高。

  • 数据处理

    :当proto_parse_byte返回 1 时,parser.data_buf中存储的就是有效数据,可直接用于控制(如点亮 LED、驱动电机)或上报(如通过 WiFi 发送到服务器)。

  • 灵活调整

    :如果需要修改协议,只需修改FRAME_HEADER_1CRC16_POLY等宏定义,无需改动核心逻辑(比如将帧尾改为0x0A,或 CRC 改为 8 位)。

为了让你彻底理解,我会把代码按 “模块 + 功能 + 逐行解释” 的方式拆解,重点标注结构体、核心函数的设计思路和作用:

模块 1:头文件与协议配置(代码最开头)


#include <stdint.h>
#include <string.h>

// 协议配置参数(可根据实际需求修改)
#define FRAME_HEADER_1 0xAA    // 帧头第1字节
#define FRAME_HEADER_2 0x55    // 帧头第2字节
#define FRAME_TAIL     0x0D    // 帧尾字节
#define MAX_DATA_LEN   64      // 最大数据负载长度
#define CRC16_POLY     0x8005  // CRC-16多项式

功能解释

  • #include <stdint.h>

    :引入单片机常用的 “固定位数整数类型”(比如uint8_t=8 位无符号整数,uint16_t=16 位),避免不同单片机对int的位数定义不同导致错误;

  • #include <string.h>

    :引入内存操作函数(比如memset清空数组、memcpy复制数组);

  • #define

     宏定义:把协议里的 “固定值” 用 “见名知意” 的名字代替(比如FRAME_HEADER_1代替 0xAA),初学者改参数时不用翻代码,直接改宏定义就行,还能避免写错数字。


// CRC-16计算函数(输入:数据地址+长度,输出:16位校验值)
uint16_t crc16_calc(const uint8_t *data, uint16_t len){
    uint16_t crc =0xFFFF;// 初始值(行业通用,不用改)
for(uint16_t i =0; i < len; i++){// 遍历每一个字节
        crc =(uint16_t)data[i]<<8;// 把8位数据左移8位,和CRC异或(对齐位数)
for(uint8_t j =0; j <8; j++){// 对每个字节的8个位逐位处理
// 核心逻辑:如果CRC最高位是1,左移后异或多项式;否则只左移
            crc =(crc &0x8000)?(crc <<1CRC16_POLY):(crc <<1);
}
}
return crc;// 返回最终计算的CRC值
}

功能解释

  • 作用:给一段数据生成唯一的 “校验码”(比如数据{0x11,0x22}算出的 CRC 是固定值),发送方算好一起发,接收方重新算一遍,对比校验码就知道数据有没有传错;

  • 初学者重点:不用死记计算逻辑(这是行业标准的 CRC16 算法),只需知道:

    1. 输入:要校验的数据(data)+ 数据长度(len);

    2. 输出:16 位的 CRC 值;

    3. 调用示例:crc16_calc(send_data, 3) 就能算出send_data数组前 3 个字节的 CRC。


// 解析器状态枚举(状态机核心)
typedef enum{
STATE_WAIT_HEADER1,// 等待帧头第1字节(0xAA)
STATE_WAIT_HEADER2,// 等待帧头第2字节(0x55)
STATE_READ_LEN,// 读取数据长度
STATE_READ_DATA,// 读取数据负载
STATE_READ_CRC_H,// 读取CRC高字节
STATE_READ_CRC_L,// 读取CRC低字节
STATE_READ_TAIL// 读取帧尾(0x0D)
} parse_state_t;

功能解释

  • typedef enum

    :枚举类型,把 “数字” 和 “状态名字” 绑定(比如STATE_WAIT_HEADER1对应 0,STATE_WAIT_HEADER2对应 1);

  • 初学者理解:把单片机接收数据的过程拆成 7 个 “阶段”,就像玩游戏的 “关卡”,必须过了第一关(收到 0xAA)才能进第二关(等 0x55),错了就回到第一关,避免混乱。

模块 4:解析器结构体(“存储接收数据的小本子”)

// 解析器结构体(存储解析状态和缓存数据)
typedef struct {
    parse_state_t state;// 当前解析到哪个阶段(比如在等帧头、读数据)
    uint8_t data_buf[MAX_DATA_LEN];// 存接收的实际数据(比如传感器值、控制指令)
    uint8_t data_len;// 记录数据长度(比如对方说要发3个字节,这里就存3)
    uint16_t recv_crc;// 存接收的CRC值(用来和计算的CRC对比)
    uint8_t buf_idx;// 记录已经存了几个字节到data_buf(比如存了2个,idx=2)
} proto_parser_t;

功能解释(初学者必懂)

  • typedef struct

    :结构体,把 “和接收数据相关的所有变量” 打包成一个整体,就像给单片机准备一个 “专用笔记本”,所有接收相关的信息都记在这本子里,不用零散记 5 个变量;

  • 各成员作用:

    成员名

    通俗解释

    state

    记当前 “关卡”(比如现在在等帧头,还是在读数据)

    data_buf

    记对方发的实际内容(比如对方发 “11 22 33”,这里就存这 3 个字节)

    data_len

    记对方说要发多少字节(比如对方说发 3 个,这里就存 3,防止存多了溢出)

    recv_crc

    记对方发的 CRC 校验码(比如对方发的是 0x1234,这里就存 0x1234)

    buf_idx

    记已经存了几个字节到 data_buf(比如存了 2 个,idx=2,再存 1 个就凑够 3 个)

模块 5:解析器初始化函数(清空 “小本子”)

// 初始化解析器(调用一次即可)
voidproto_parser_init(proto_parser_t *parser){
    parser->state =STATE_WAIT_HEADER1;// 初始关卡:等帧头1(0xAA)
    parser->data_len =0;// 数据长度清零
    parser->recv_crc =0;// CRC值清零
    parser->buf_idx =0;// 缓存索引清零
memset(parser->data_buf,0,MAX_DATA_LEN);// 清空数据缓存(填0)
}

功能解释

  • 作用:每次开始接收数据前,把 “小本子” 清空,让单片机从 “第一关(等帧头)” 开始;

  • parser->state

    :访问结构体成员的方式(->用于结构体指针,.用于结构体变量),初学者记住:定义结构体指针(proto_parser_t *parser)就用->,定义普通结构体(proto_parser_t parser)就用.

  • memset(parser->data_buf, 0, MAX_DATA_LEN)

    :把data_buf数组的所有元素设为 0,相当于清空笔记本。

模块 6:核心解析函数(状态机处理,逐字节解析)

// 单字节解析函数(串口中断中调用,每接收1字节执行1次)
// 返回值:0=解析中,1=解析成功,-1=解析失败
int8_t proto_parse_byte(proto_parser_t *parser, uint8_t byte){
switch(parser->state){// 根据当前“关卡”处理字节
// 关卡1:等帧头1(0xAA)
caseSTATE_WAIT_HEADER1:
if(byte ==FRAME_HEADER_1){
                parser->state =STATE_WAIT_HEADER2;// 收到0xAA,进下一关
}
break;

// 关卡2:等帧头2(0x55)
caseSTATE_WAIT_HEADER2:
if(byte ==FRAME_HEADER_2){
                parser->state =STATE_READ_LEN;// 收到0x55,进“读长度”关卡
}else{
                parser->state =STATE_WAIT_HEADER1;// 没收到0x55,回第一关
}
break;

// 关卡3:读数据长度
caseSTATE_READ_LEN:
if(byte <=MAX_DATA_LEN){// 长度合法(不超过64)
                parser->data_len = byte;// 记录数据长度
// 如果长度是0(没数据),直接跳去读CRC;否则读数据
                parser->state =(byte ==0)?STATE_READ_CRC_H:STATE_READ_DATA;
}else{
proto_parser_init(parser);// 长度非法,清空本子
return-1;
}
break;

// 关卡4:读数据负载
caseSTATE_READ_DATA:
            parser->data_buf[parser->buf_idx++]= byte;// 把字节存到缓存,索引+1
if(parser->buf_idx >= parser->data_len){// 存够了指定长度
                parser->buf_idx =0;// 索引清零
                parser->state =STATE_READ_CRC_H;// 进“读CRC高字节”关卡
}
break;

// 关卡5:读CRC高字节
caseSTATE_READ_CRC_H:
            parser->recv_crc =(uint16_t)byte <<8;// 高字节左移8位(比如0x12→0x1200)
            parser->state =STATE_READ_CRC_L;// 进下一关
break;

// 关卡6:读CRC低字节
caseSTATE_READ_CRC_L:
            parser->recv_crc |= byte;// 拼接低字节(0x1200 | 0x34 = 0x1234)
            parser->state =STATE_READ_TAIL;// 进“读帧尾”关卡
break;

// 关卡7:读帧尾(0x0D)
caseSTATE_READ_TAIL:
if(byte ==FRAME_TAIL){// 收到帧尾
// 准备计算CRC:把“长度+数据”打包成数组
                uint8_t crc_input[MAX_DATA_LEN+1];
                crc_input[0]= parser->data_len;// 第0位存长度
memcpy(&crc_input[1], parser->data_buf, parser->data_len);// 后面存数据
                uint16_t calc_crc =crc16_calc(crc_input, parser->data_len +1);// 算CRC

if(calc_crc == parser->recv_crc){// CRC对比一致
proto_parser_init(parser);// 清空本子,准备下一次接收
return1;// 解析成功
}else{
proto_parser_init(parser);// CRC错,清空
return-1;
}
}else{
proto_parser_init(parser);// 帧尾错,清空
return-1;
}
break;

default:// 异常状态(比如state变成了不存在的数字)
proto_parser_init(parser);
return-1;
}
return0;// 还在解析中,没完成
}

核心解释(初学者重点)

  1. 调用时机:单片机串口每收到 1 个字节,就调用一次这个函数(比如串口中断里调用);

  2. switch (parser->state)

    :根据当前 “关卡” 处理字节,比如在 “等帧头 1” 时,只关心字节是不是 0xAA,其他字节都忽略;

  3. 错误处理:任何一步出错(比如帧头错、长度超了、CRC 错),都会调用proto_parser_init清空本子,回到第一关,避免单片机卡死;

  4. 返回值:

    • 0:还在解析(比如刚收到 0xAA,还在等 0x55);

    • 1:解析成功(可以去处理data_buf里的数据了);

    • -1:解析失败(数据错了,重新等)。

模块 7:发送帧封装函数(打包数据)

// 示例:发送帧封装函数(将数据打包为协议帧)
uint8_t proto_pack_frame(uint8_t *data, uint8_t len, uint8_t *frame_buf){
if(len >MAX_DATA_LEN)return0;// 数据太长,打包失败

// 步骤1:填帧头
    frame_buf[0]=FRAME_HEADER_1;
    frame_buf[1]=FRAME_HEADER_2;
// 步骤2:填数据长度
    frame_buf[2]= len;
// 步骤3:填数据负载
memcpy(&frame_buf[3], data, len);
// 步骤4:算CRC并填进去
    uint16_t crc =crc16_calc(&frame_buf[2], len +1);// 计算“长度+数据”的CRC
    frame_buf[3+ len]=(crc >>8)&0xFF;// CRC高字节
    frame_buf[4+ len]= crc &0xFF;// CRC低字节
// 步骤5:填帧尾
    frame_buf[5+ len]=FRAME_TAIL;

return6+ len;// 返回帧总长度(2帧头+1长度+len数据+2CRC+1帧尾=6+len)
}

功能解释

  • 作用:把要发送的 “原始数据”(比如{0x11,0x22,0x33})打包成符合协议的 “完整帧”(比如AA 55 03 11 22 33 XX XX 0D);

  • 输入:data(要发送的原始数据)、len(数据长度)、frame_buf(用来存打包后的完整帧);

  • 输出:返回打包后的帧总长度(方便串口发送时知道要发多少字节)。

模块 8:主函数示例(模拟单片机运行)
// 主函数示例(模拟单片机运行逻辑)
int main(void){
    proto_parser_t parser;// 定义一个解析器结构体(普通变量,用.访问)
proto_parser_init(&parser);// 初始化解析器(传地址,用&)

// 模拟:发送端打包数据
    uint8_t send_data[]={0x11,0x22,0x33};// 要发送的原始数据
    uint8_t send_frame[100];// 存打包后的帧
    uint8_t frame_len =proto_pack_frame(send_data,sizeof(send_data), send_frame);

// 模拟:接收端逐字节解析(实际中在串口中断中调用)
for(uint8_t i =0; i < frame_len; i++){
        int8_t ret =proto_parse_byte(&parser, send_frame[i]);
if(ret  1){
// 解析成功,处理数据(比如把data_buf里的数发给传感器/点亮LED)
}elseif(ret  -1){
// 解析失败,记录错误(比如闪灯提示)
}
}

while(1){// 单片机主循环(一直运行)
// 这里可以加其他逻辑(比如定时发送数据、处理按键)
}
}

功能解释

  • proto_parser_t parser;

    :定义一个解析器结构体变量(普通变量,不是指针),初学者注意:定义指针用proto_parser_t *parser,定义普通变量用proto_parser_t parser

  • proto_parser_init(&parser);

    :初始化解析器,&parser是取结构体的地址(因为初始化函数的参数是指针);

  • 模拟发送:打包send_datasend_frame,模拟实际项目中 “单片机发送数据” 的场景;

  • 模拟接收:逐字节解析send_frame,模拟 “串口中断接收字节并解析” 的场景;

  • while (1)

    :单片机的主循环,一旦进入就永远运行(单片机不能像电脑一样运行完就退出)。

二、核心知识点总结(初学者必记)

  1. 结构体(struct)

    :把 “相关变量打包”,比如解析器的状态、缓存、长度等放在一起,方便管理和传递,访问方式:指针用->,普通变量用.

  2. 状态机(enum+switch)

    :把 “分步接收数据” 拆成多个阶段,每个阶段只处理对应字节,避免混乱,是单片机通信解析的核心思想;

  3. CRC 校验

    :不用记计算逻辑,只需知道 “发送方算 CRC 一起发,接收方重算对比,一致则数据正确”;

  4. 逐字节解析

    :单片机串口接收是 “一个字节一个字节来”,所以解析函数要按字节处理,不能一次性处理整帧;

  5. 错误处理

    :任何环节出错都要 “重置解析器”,回到初始状态,避免单片机卡死。

初学者先把 “结构体成员作用”“状态机的 7 个阶段” 记熟,再动手把代码烧到单片机里,用串口助手发数据测试(比如发AA 55 03 11 22 33 78 9A 0D),看解析是否成功,就能快速理解每段代码的实际作用了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小范好好学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值