摘要:本文复盘了V3智控面板双MCU间自定义串口协议的固定长度、扩展性差等不足,并规划了V4的帧头+长度+类型码+载荷+校验+帧尾的可扩展协议框架
- 往期连接:【双MCU项目复盘与优化】01 - 总体架构与调度逻辑 - 临祁 - 博客园
1. V3 串口协议复盘
1.1 协议内容
- 以下是具体的协议内容
| 帧头 | 类型码 | 数据 1 | 数据 2 | 数据 3 | 数据 4 | 数据 5 | 数据 6 | 校验 | 帧尾 |
|---|---|---|---|---|---|---|---|---|---|
| CD | 系统状态:01 | 系统空闲:00 系统工作:01 |
00 | 00 | 00 | 00 | 00 | —— | FA |
| CD | 继电器 1:03 | 关闭:00 开启:01 人体:02 |
超时时间:0~60 分钟 | 00 | 00 | 00 | 00 | —— | FA |
| CD | 继电器 2:04 | 关闭:00 开启:01 人体:02 |
超时时间:0~60 分钟 | 00 | 00 | 00 | 00 | —— | FA |
| CD | 灯光:05 | 关闭:00 开启:01 人体:02 |
超时时间: 0~60 分钟 |
亮度值:0~100 | 00 | 00 | 00 | —— | FA |
| CD | 传感器:02 | 温度: 低八位 |
温度: 高八位 |
湿度: 低八位 |
湿度: 高八位 |
流明: 低八位 |
流明: 高八位 |
—— | FA |
- 可见,V3 采用了固定字长的帧格式,格式为“帧头+数据+校验+帧尾”
- 校验字节在构建数据帧时使用 XOR 实时计算,此处用 “——” 占位
- 修改配置时,STM32F103 会接收数据帧,然后解析并修改配置,然后把最新的配置打包成同样的数据帧回传给 ESP32-S3
1.2 分析不足
- V3 协议在设计上满足了基本的数据交换需求,但随着项目迭代,以下问题逐渐暴露:
- 扩展性差:协议采用固定位置、固定含义的字段,添加新功能(如 NFC 卡号列表)需要重新定义帧结构,无法平滑升级。
- 带宽利用率低:每一帧固定为 7 个数据字节(数据1~数据7),但实际有效载荷往往只有 2~3 字节。例如配置继电器时,只有类型码、模式、超时三个有效字节,其余 4 字节填 0x00,浪费率接近 60%
- 这些问题共同指向一个结论:V3 协议只能用于当前有限功能,V4 必须彻底重构
2. V4 串口协议规划
- 参考资料:【第33期】协议设计:帧头、帧尾与转义(Stuffing)
基本结构:帧头+长度+类型码+载荷+校验值+帧尾
- 串口协议的解析顺序:接收方先查找帧头,然后读取长度 Len,接着读取 Len 字节载荷,再读 1 字节 XOR 校验和,最后读 1 字节帧尾用于可选验证。帧尾不参与定位,仅作完整性检查。
2.1 确定帧头帧尾
- 在开始规划具体内容之前,可以先把帧头和帧尾确定下来,这里采用固定值,并且 ESP32-S3 发送的帧头帧尾和 STM32 发送的不一样
- 我这里确定为以下这四个值。帧头帧尾可以随意分配(因为长度字段已存在,接收方按长度解析,不依赖帧尾定位,故不需要转义):
| 发送方 | 接收方 | 帧头 | 帧尾 |
|---|---|---|---|
| ESP32-S3 | STM32 | 0xBC | 0x3A |
| STM32 | ESP32-S3 | 0xCE | 0x9D |
2.2 结合场景规划类型码、载荷
- 在开始之前,可以先约定一下类型码的分配规则,这样可以便于调试和扩展
- 类型码高半字节
0xE表示 ESP32 发出的指令,0xF表示 STM32 发出的响应或数据,
- 类型码高半字节
2.2.1 配置场景
① 灯光、继电器 1/2 配置
- ESP32-S3 会对 STM32 发送关于灯光、继电器 1/2 的配置参数,而 STM32 会根据接收到的配置参数修改硬件状态。在 STM32 修改完状态之后,最好给 ESP32 报告一下。
- 所以可以这样设计(长度指的是载荷的字节数)
| 发送方 | 帧头 Head | 长度 Len | 类型码 Type | 载荷 Payload | XOR | 帧尾 Tail | 含义 |
|---|---|---|---|---|---|---|---|
| ESP32-S3 | 0xBC | 3 | 0xE1 | uint8_t mode = 0 关闭 / 1 开启 / 2 人体检测; uint8_t tout = 人体检测模式的超时时间(分钟); uint8_t value = 亮度值(百分比); |
—— | 0x3A | 配置 Light |
| 0xBC | 2 | 0xE2 | uint8_t mode = 0 关闭 / 1 开启 / 2 人体检测; uint8_t tout = 人体检测模式的超时时间(分钟); |
—— | 0x3A | 配置 Relay1 | |
| 0xBC | 2 | 0xE3 | uint8_t mode = 0 关闭 / 1 开启 / 2 人体检测; uint8_t tout = 人体检测模式的超时时间(分钟); |
—— | 0x3A | 配置 Relay2 | |
| STM32 | 0xCE | 0 | 0xF1 | 无 | —— | 0x9D | Light 配置成功 |
| 0xCE | 0 | 0xF2 | 无 | —— | 0x9D | Relay1 配置成功 | |
| 0xCE | 0 | 0xF3 | 无 | —— | 0x9D | Relay2 配置成功 | |
| 0xCE | 0 | 0xF4 | 无 | —— | 0x9D | Light 配置失败 | |
| 0xCE | 0 | 0xF5 | 无 | —— | 0x9D | Relay1 配置失败 | |
| 0xCE | 0 | 0xF6 | 无 | —— | 0x9D | Relay2 配置失败 |
- 其中,XOR暂时留空,只需要知道是校验值即可
- 另外,如果要更保险一定,可以选择在 STM32 发送“配置成功”消息时,可以在载荷里面把配置参数也一起回传,这样 ESP32 可以检验是否真的配置成功了。目前这个项目我觉得数据量不大,就不使用回传了。
② NFC 卡配置
- 由于我们终端有需要有增加卡、删除卡的功能。
- 我们需要定义一个增卡的配置消息,而 STM32 收到后就会读取当前识别到的卡号并且添加到已有的卡号列表,然后我们可以返回一个增卡成功的消息,并且把拿到的卡号也一并回传给 ESP32-S3 方便保存。当然,对应的增卡失败要有。
- 同时,删卡的配置消息也需要有,STM32 收到后就会读取当前识别到的卡号,然后在已经有的卡号列表里面把它删除掉,然后我们可以返回一个删卡成功的消息,并且把删除的卡号也一并回传给 ESP32-S3 方便删除。对应的删卡失败也必不可少。
- 由于需要在 ESP32-S3 保存卡号列表,而 STM32 内部不做掉电保存,于是必须在开机的时候,给 STM32 发送具体的卡号列表,所以还需要一个初始化卡号列表消息,以及对应的回复消息
| 发送方 | 帧头 Head | 长度 Len | 类型码 Type | 载荷 Payload | XOR | 帧尾 Tail | 含义 |
|---|---|---|---|---|---|---|---|
| ESP32-S3 | 0xBC | 2 + num*4 | 0xE4 | uint8_t capacity:卡号列表最大容量 uint8_t num:当前卡 ID 数量 uint32_t nfc_card_id[num] |
—— | 0x3A | 初始化 STM32 内部的卡号列表 |
| 0xBC | 0 | 0xE5 | 无 | —— | 0x3A | 增加卡 | |
| 0xBC | 0 | 0xE6 | 无 | —— | 0x3A | 删除卡 | |
| STM32 | 0xCE | 0 | 0xF7 | 无 | —— | 0x9D | 初始化卡号列表失败 |
| 0xCE | 0 | 0xF8 | 无 | —— | 0x9D | 初始化卡号列表成功 | |
| 0xCE | 0 | 0xF9 | 无 | —— | 0x9D | 增加卡失败 | |
| 0xCE | 1 | 0xFA | 增加的卡号 | —— | 0x9D | 增加卡成功 | |
| 0xCE | 0 | 0xFB | 无 | —— | 0x9D | 删除卡失败 | |
| 0xCE | 1 | 0xFC | 删除的卡号 | —— | 0x9D | 删除卡成功 |
2.2.2 传感器数据场景
- 由于终端需要显示室内的温湿度数据,以及屏幕自适应亮度需要知道环境光照,而传感器这种硬件基本由 STM32 进行驱动,所以,我们还需要设计关于传感器数据的协议部分。
- 温度(Temp):
- 室内温度通常只需要显示 3 位数即可,即两位整数和一位小数,范围大概在 5.0~32.0 之间,而串口通信一般数据是整数的,所以我们最好对温度数据进行定点化再通过串口发送。
- 这里选择 10 为定点化倍数,如 25.6 定点化之后就是 256,可以用 uint16_t 来存储并发送,接收之后再进行去定点化即可获得 25.6
- 湿度(Humi):
- 室内湿度是百分比,范围是 0~100,一般显示无小数即可
- 所以可以用 uint8_t 来存储并发送,无需定点化
- 光照(Lux):
- 光照的数据大小一般与环境光传感器的增益、积分时间等参数有关,目前不确定,可以先决定使用 uint16_t 来存储并发送,如果后续知道光照的值在 0~255 内,可以改为使用 uint8_t
- 所以内容可以设置如下:
| 发送方 | 帧头 Head | 长度 Len | 类型码 Type | 载荷 Payload | XOR | 帧尾 Tail | 含义 |
|---|---|---|---|---|---|---|---|
| STM32 | 0xCE | 5 | 0xFD | uint16_t temp:温度 uint8_t humi:湿度 uint16_t lux:光照 |
—— | 0x9D | 发送温度、湿度、光照 |
- 由于传感器数据仅需最新值,所以为了减少冗余,无需应答设计,STM32 周期性发送、ESP32 保留最后有效值即可,而不是等待确认。
2.3 选择校验和算法
- 除了硬件自带的奇偶检验之外,我们还应该给自己协议设计选择一个专属于协议的校验和算法
- 可以选择异或校验(XOR)、累加和校验(Checksum)、CRC 校验(CRC-8/16/32)
- 这里,我选择最简单的 XOR 检验,因为数据量小、通信环境良好(同一块板子内部或短距离),且后续会配合串口本身的硬件校验,所以够用
- XOR 会对帧头、长度、类型码、载荷所有字节进行异或,结果放在 CRC 字段
2.4 大小端问题
- 在之前的协议内容设计中,载荷里面出现了变量类型:uint16_t 和 uint32_t
- 而这就涉及到大小端的问题,例如:uint32_t val = 0x12345678,在存储中的情况就是:
- 小端:78 56 34 12
- 大端:12 34 56 78
- 所以,我们还需要约定好在协议里面 uint16_t 和 uint32_t 是使用大端还是小端
- 如果是小端,例如 uint16_t 的变量, 串口则会先发送低八位再发送高八位,大端则相反
- STM32 和 ESP32-S3,两者均为小端模式,但协议约定通常使用大端传输,于是本协议同样使用大端模式
- 发送多字节整数时,先发送高位字节(MSB),后发送低位字节(LSB);接收方需按相同顺序重组。
2.5 收尾
- 协议内容汇总
| 发送方 | 帧头 Head | 长度 Len(载荷字节数) | 类型码 Type | 载荷 Payload | XOR | 帧尾 Tail | 含义 |
|---|---|---|---|---|---|---|---|
| ESP32-S3 | 0xBC | 3 | 0xE1 | uint8_t mode = 0 关闭 / 1 开启 / 2 人体检测; uint8_t tout = 人体检测模式的超时时间(分钟); uint8_t value = 亮度值(百分比); |
—— | 0x3A | 配置 Light |
| 0xBC | 2 | 0xE2 | uint8_t mode = 0 关闭 / 1 开启 / 2 人体检测; uint8_t tout = 人体检测模式的超时时间(分钟); |
—— | 0x3A | 配置 Relay1 | |
| 0xBC | 2 | 0xE3 | uint8_t mode = 0 关闭 / 1 开启 / 2 人体检测; uint8_t tout = 人体检测模式的超时时间(分钟); |
—— | 0x3A | 配置 Relay2 | |
| 0xBC | 2 + num*4 | 0xE4 | uint8_t capacity:卡号列表最大容量 uint8_t num:当前卡 ID 数量 uint32_t nfc_card_id[num] |
—— | 0x3A | 初始化 STM32 内部的卡号列表 | |
| 0xBC | 0 | 0xE5 | 无 | —— | 0x3A | 增加卡 | |
| 0xBC | 0 | 0xE6 | 无 | —— | 0x3A | 删除卡 | |
| STM32 | 0xCE | 0 | 0xF1 | 无 | —— | 0x9D | Light 配置成功 |
| 0xCE | 0 | 0xF2 | 无 | —— | 0x9D | Relay1 配置成功 | |
| 0xCE | 0 | 0xF3 | 无 | —— | 0x9D | Relay2 配置成功 | |
| 0xCE | 0 | 0xF4 | 无 | —— | 0x9D | Light 配置失败 | |
| 0xCE | 0 | 0xF5 | 无 | —— | 0x9D | Relay1 配置失败 | |
| 0xCE | 0 | 0xF6 | 无 | —— | 0x9D | Relay2 配置失败 | |
| 0xCE | 0 | 0xF7 | 无 | —— | 0x9D | 初始化卡号列表失败 | |
| 0xCE | 0 | 0xF8 | 无 | —— | 0x9D | 初始化卡号列表成功 | |
| 0xCE | 0 | 0xF9 | 无 | —— | 0x9D | 增加卡失败 | |
| 0xCE | 1 | 0xFA | 增加的卡号 | —— | 0x9D | 增加卡成功 | |
| 0xCE | 0 | 0xFB | 无 | —— | 0x9D | 删除卡失败 | |
| 0xCE | 1 | 0xFC | 删除的卡号 | —— | 0x9D | 删除卡成功 | |
| 0xCE | 5 | 0xFD | uint16_t temp:温度 uint8_t humi:湿度 uint16_t lux:光照 |
—— | 0x9D | 发送温度、湿度、光照 |
另外,由于本项目使用电路板,MCU 间串口线路短、电磁环境良好,误码率极低,所以本协议的帧头、帧尾、类型码的分配随意,不是某些约定俗成的固定值。如果希望协议健壮些,可以考虑是否是载荷里面的高概率数字,并引入转义。如果还希望更健壮,还进一步结合通信原理来进行编码。
