"大家好,这节我们来讲协议设计。本节讲两种协议桢格式设计:简化版和专业版

简化版适合学生去做些小项目开发用,可以更好的规范自己开发框架和代码习惯;
专业版就更适合做企业级产品开发用。

一、简化版

1. 协议设计

在这里插入图片描述
解释说明:

  1. 为什么很多协议的帧头都喜欢用0x55、0xAA?

    • 调试定位。把0x55转为二进制 = 01010101,把0xAA转为二进制 = 10101010,发现这个两个值都是0101或1010翻转的顺序,如果你用示波器去调试信号,可以很好的定位到数据帧的帧头位置,因为它是一个很标准的方波。
    • 自动波特率识别。有些产品它支持自动识别波特率,这个原理就是监测每个比特周期的跳变,也就是通过测量对方发过来的连续跳变间隔来自动校准,像0x55、0xAA就是很标准的上下沿跳变,假设检测到3次跳变时间点是t1,t2,t3,那么实测波特率 = 1 / ((t3 - t1)/2 )
  2. 命令ID是什么意思,怎么用?
    比如说现在有两个设备:一个发送端一个接收端,发送端向接收端发送几帧数据,其中这几帧数据的命令ID字段分别是0x01、0x02、0x03,那么接收端就会去解析这几帧数据,如果读出命令ID是0x01后,就点亮LED1,如果是0x02,那么就点亮LED2。。。简而言之,不同的命令ID对应不同的处理动作。

  3. “数据长度”和“数据”字段 能干嘛、怎么用?
    同样现在有两个设备,设备1和设备2,那么我现在需要通过设备1去控制或者配置设备2的功能参数(假定配置5个参数,分别配置为1、2、3、4、5),那么在这帧数据中具体表现为:“数据长度”字段就等于5,而“数据字段”就是5个字节,分别是1、2、3、4、5。

  4. 什么是校验码,怎么用,有什么用?
    你可以理解为校验码就是一种验算机制,可以确保双方数据通信的完整正确性。
    怎么计算呢?好比我们现在的数据帧格式是“帧头+命令+数据长度+数据+校验码+帧尾”,那么你就可以选择校验码只对重要数据进行验算(命令+数据长度+数据),常用的验算方法有和校验,和校验就是对数据进行求和计算,在这里就是把“命令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

补充说明:

  1. 什么是小端,什么是大端?
    大小端就是指字节数据在内存当中的存储格式。
    小端:就是指低字节存放在低位,高字节存放在高位;
    大端:就是指低字节存放在高位,高字节存放在低位;
    对于大多数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)——协议设计

嵌入式串口通信高阶实战(2/3)——协议设计

四、技术交流

感兴趣同学可联系主页wx(Lntt-xbc)入交流群

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐