嵌入式串口通信高阶实战(2/3)——协议设计(简化版+专业版)
🔥 嵌入式串口通信高阶实战(2/3)— 协议设计📌带你掌握工业级协议开发精髓!从简化版帧结构入门,学习帧头、命令ID、数据长度等核心字段设计,到专业版的多设备寻址、CRC校验、序列号管理等高级特性。
"大家好,这节我们来讲协议设计。本节讲两种协议桢格式设计:简化版和专业版
简化版适合学生去做些小项目开发用,可以更好的规范自己开发框架和代码习惯;
专业版就更适合做企业级产品开发用。
一、简化版
1. 协议设计

解释说明:
-
为什么很多协议的帧头都喜欢用0x55、0xAA?- 调试定位。把0x55转为二进制 = 01010101,把0xAA转为二进制 = 10101010,发现这个两个值都是0101或1010翻转的顺序,如果你用示波器去调试信号,可以很好的定位到数据帧的帧头位置,因为它是一个很标准的方波。
- 自动波特率识别。有些产品它支持自动识别波特率,这个原理就是监测每个比特周期的跳变,也就是通过测量对方发过来的连续跳变间隔来自动校准,像0x55、0xAA就是很标准的上下沿跳变,假设检测到3次跳变时间点是t1,t2,t3,那么实测波特率 = 1 / ((t3 - t1)/2 )
-
命令ID是什么意思,怎么用?
比如说现在有两个设备:一个发送端一个接收端,发送端向接收端发送几帧数据,其中这几帧数据的命令ID字段分别是0x01、0x02、0x03,那么接收端就会去解析这几帧数据,如果读出命令ID是0x01后,就点亮LED1,如果是0x02,那么就点亮LED2。。。简而言之,不同的命令ID对应不同的处理动作。 -
“数据长度”和“数据”字段 能干嘛、怎么用?
同样现在有两个设备,设备1和设备2,那么我现在需要通过设备1去控制或者配置设备2的功能参数(假定配置5个参数,分别配置为1、2、3、4、5),那么在这帧数据中具体表现为:“数据长度”字段就等于5,而“数据字段”就是5个字节,分别是1、2、3、4、5。 -
什么是校验码,怎么用,有什么用?
你可以理解为校验码就是一种验算机制,可以确保双方数据通信的完整正确性。
怎么计算呢?好比我们现在的数据帧格式是“帧头+命令+数据长度+数据+校验码+帧尾”,那么你就可以选择校验码只对重要数据进行验算(命令+数据长度+数据),常用的验算方法有和校验,和校验就是对数据进行求和计算,在这里就是把“命令ID、数据长度、数据”这三者的值加起来就等于校验码的值,然后在接收端同样也对接收到的“命令ID、数据长度、数据”进行验算,如果发送端的验算值等于接收端的验算值,那么我们就认为这帧数据完整且正确。
2. 举例说明

示例一: 让单片机一去控制单片机二打开LED灯,无需应答。(双方约定0x01为打开LED的指令,且无需其他参数配置)
解: 那么就表示0x01为命令ID,无需参数配置就表示不需要携带有效数据,即不需要数据段,且数据长度为0。
所以完整数据帧为:帧头 + 命令ID + 数据长度 + 校验码 + 帧尾
(单片机一发送) 0x55 0x01 0x00 0x00 0x01 0xFF
示例二: 让单片机一设置单片机二的两个参数为0x0A和0x0B,需要应答。(双方约定0x02为设置参数的指令)
解: 那么就表示0x02为命令ID,配置两个参数为0x0A和0x0B就表示数据长度为2,并携带2个字节的有效数据,分别是0x0A和0x0B。
所以完整数据帧为:帧头 + 命令ID + 数据长度 + 数据 + 校验码 + 帧尾
(单片机一发送) 0x55 0x02 0x02 0x00 0x0A 0x0B 0x19 0xFF
单片机二接收到单片机一发来的数据后进行参数配置并立即应答:
所以应答数据帧为:帧头 + 命令ID + 数据长度 + 数据 + 校验码 + 帧尾
(单片机二应答) 0x55 0x02 0x01 0x00 0x01 0x04 0xFF
补充说明:
什么是小端,什么是大端?
大小端就是指字节数据在内存当中的存储格式。
小端:就是指低字节存放在低位,高字节存放在高位;
大端:就是指低字节存放在高位,高字节存放在低位;
对于大多数Cortex-M系列MCU默认采用小端模式,但也可以用实际代码去验证;
举个代码例子就明白了:uint32_t num = 0x12345678; 那么0x12是高字节,0x78是低字节,由于uint32_t占四个字节,那么就假设变量(num)存放的地址是0x00到0x03。
如果是小端模式下,则0x78存放在地址0x00上,0x12存放在0x03上;
如果是大端模式下,则0x78存放在地址0x03上,0x12存放在0x00上;
3. 协议封包(代码设计)
①定义协议帧结构体
typedef struct
{
uint8_t head; // 帧头
uint8_t cmd; // 命令ID
uint16_t len; // 数据长度(小端模式)
uint8_t *data; // 数据内容
uint8_t checksum; // 校验码(和校验)
uint8_t tail; // 帧尾
} SimpleProtocolFrame;
②计算校验和(命令ID + 数据长度 + 数据内容)
uint8_t calculate_checksum(uint8_t cmd, uint16_t len, const uint8_t *data)
{
uint8_t sum = cmd;
sum += (len & 0xFF); // 添加数据长度(小端模式:先低字节后高字节)
sum += ((len >> 8) & 0xFF);
for (uint16_t i = 0; i < len; i++) // 添加数据内容
{
sum += data[i];
}
return sum;
}
③协议封包
//参数:frame - 协议帧结构体 buffer - 发送缓冲区
//返回:封包后的数据长度
uint16_t simple_protocol_pack(const SimpleProtocolFrame *frame, uint8_t *buffer)
{
uint16_t idx = 0;
buffer[idx ++] = frame->head;
buffer[idx ++] = frame->cmd;
buffer[idx ++] = frame->len & 0xFF; // 低字节
buffer[idx ++] = (frame->len >> 8) & 0xFF; // 高字节
if (frame->len > 0 && frame->data != NULL) {
memcpy(&buffer[idx ], frame->data, frame->len);
idx += frame->len;
}
buffer[idx ++] = frame->checksum;
buffer[idx ++] = frame->tail;
return idx ;
}
④协议发送
void send_protocol_frame(uint8_t cmd, const uint8_t *data, uint16_t data_len)
{
SimpleProtocolFrame frame;
uint8_t buffer[256]; // 发送缓冲区
// 填充帧结构
frame.head = 0x55;
frame.cmd = cmd;
frame.len = data_len;
frame.data = (uint8_t*)data; // 去除const限定
frame.checksum = calculate_checksum(cmd, data_len, data);
frame.tail = 0xFF;
// 封包
uint16_t frame_len = simple_protocol_pack(&frame, buffer);
// 发送
uart_send_buffer(buffer, frame_len);
}
⑤应用示例
// 示例1:发送无数据帧(打开LED)
send_protocol_frame(0x01, NULL, 0);
// 示例2:发送带参数帧
uint8_t params[] = {0x0A, 0x0B};
send_protocol_frame(0x02, params, sizeof(params));
二、专业版
1.协议设计

(1)配置位结构说明:
(2)CRC校验说明
CRC(Cyclic Redundancy Check,循环冗余校验)是一种通过位移和异或实现的复杂数学运算。
问:见下,为什么是“CRC-8(多项式0x07)”和“CRC-16(多项式0x1021)”?
解:这些多项式是由国际标准组织定义的,也经过相关数学验证,可以提供最优的错误检测能力。
①CRC-8(多项式0x07)
uint8_t crc8(const uint8_t *data, uint16_t len) {
uint8_t crc = 0x00; // 初始值
while(len--) {
crc ^= *data++;
for(uint8_t i=0; i<8; i++)
crc = (crc & 0x80) ? (crc << 1) ^ 0x07 : (crc << 1);
}
return crc;
}
②CRC-16(多项式0x1021)
uint16_t crc16(const uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF; // 初始值
while(len--) {
crc ^= (uint16_t)(*data++) << 8;
for(uint8_t i=0; i<8; i++)
crc = (crc & 0x8000) ? (crc << 1) ^ 0x1021 : (crc << 1);
}
return crc;
}
(3)为什么要有源地址和目的地址?
因为在实际产品开发中,并不是单纯的设备A发送给设备B,然后设备B就去直接处理了,因为一个产品里面会涉及到多条通信线路,而其他设备无法直接与设备A通信,但它可以与设备B通信,所以设备B就起到了数据转发的作用,相应的就引入了源地址和目的地址去辨别处理。
2.举例说明
3.协议封包(代码设计)
那么专业版的协议设计我就先讲到这里,其他举例说明和协议封包(代码设计)部分也不多赘述了,大家可以参考简化版讲的部分进行设计,有什么疑问可以留言评论区或后台私信,我们下节课再见!
三、视频讲解
嵌入式串口通信高阶实战(2/3)——协议设计
四、技术交流
感兴趣同学可联系主页wx(Lntt-xbc)入交流群
更多推荐



所有评论(0)