前两章我们打通了"物理连接"和"数据搬运(链路层)",现在的状态是:我们能从总线上收到一堆字节,但不知道这些字节代表什么,也不知道是不是发给我的。
这一章我们将设计协议层(Protocol Layer),解决三个核心问题:
-
数据怎么打包?(帧结构设计)
-
发给谁?(多机寻址策略)
-
G0 怎么偷懒?(利用 STM32 硬件静默模式实现地址唤醒)。
1. 为什么不能只发"裸数据"?
在单线多机(One-to-Many)网络中,如果 Master 只是简单地发送 0x01(代表开灯),所有 Slave 都会收到。
-
Slave A 以为是开灯。
-
Slave B 以为是电机转速设为 1。
-
Slave C 可能把它当成了校验和的一部分。
因此,我们需要给数据穿上"衣服",这就是协议帧(Frame) 。考虑到单线带宽通常有限(如 9600 或 115200),我们设计一个比 Modbus 更轻量的Mini-Frame。
2. Mini-Frame 帧结构设计
我们要追求的是:最小的开销(Overhead) + 足够的可靠性。
推荐结构 : [Header] [Target_ID] [Len] [Cmd] [Payload...] [CRC]
-
Header (1 Byte) : 帧头,如
0xAA或0x55。用于在数据流中快速定位一帧的开始。 -
Target_ID (1 Byte): 目标设备地址(0x00-0xFF)。
-
Len (1 Byte): 后续数据长度,防止解析越界。
-
Cmd (1 Byte): 功能码(如 0x01=读状态, 0x02=写配置)。
-
Payload (N Bytes): 具体的参数数据。
-
CRC (1/2 Bytes) : 校验和。单线抗干扰差,必须有校验。
3. 多机寻址策略 (Addressing Strategy)
3.1 单播 (Unicast) ------ "点名提问"
-
逻辑 :Master 发出的
Target_ID等于 Slave 的本地 ID。 -
行为 :Slave 收到后,执行指令,并必须回复(ACK 或 数据)。
-
超时:Master 发送后启动定时器(如 50ms)。若超时未收到回复,判定 Slave 离线或重发。
3.2 广播 (Broadcast) ------ "全员通告"
-
逻辑 :Master 发出的
Target_ID为广播地址(通常定义为0x00或0xFF)。 -
行为:所有 Slave 收到后执行指令。
-
红线警告(Critical) :Slave 收到广播包后,绝对禁止回复!
- 原因:如果 10 个 Slave 同时拉低总线回复 "OK",总线电平会打架,甚至烧毁 IO 口(在推挽模式下),数据也会完全乱码。
3.3 组播 (Multicast) ------ "分组行动" (可选)
-
逻辑:利用 ID 的高 4 位作为 Group ID,低 4 位作为 Device ID。
-
场景:比如让"所有空调"(Group 1)关机,而"所有灯光"(Group 2)保持不变。
4. STM32G0 杀手锏:9-bit 模式与硬件地址唤醒
在传统的 8-bit 模式下,Slave 的 CPU 非常辛苦。总线上每一字节数据(不管是发给谁的),CPU 都要进中断,醒来看看:"是我的地址吗?" -> "不是" -> 继续睡。这在 RTOS 或低功耗场景下极其浪费资源。
STM32G0 提供了 Address Mark Wakeup (9-bit mode),可以实现**"不是叫我,我就装聋"**。
4.1 工作原理
利用串口的第 9 个数据位(MSB):
-
第 9 位 = 1 :表示这个字节是地址。
-
第 9 位 = 0 :表示这个字节是数据。
STM32G0 硬件逻辑:
-
配置从机进入 Mute Mode (静默模式)。此时除了"地址字节",其他所有数据都不会触发 RXNE 中断。
-
当收到一个 9th bit = 1 的字节时,硬件自动比对该字节与
USART_CR2中的ADD(节点地址)。 -
匹配 :硬件自动清除 Mute 状态,产生中断。后续收到的 9th bit = 0 的数据(Payload)也会正常触发中断。
-
不匹配:硬件保持 Mute,CPU 继续睡觉,完全不被打扰。
4.2 STM32G0 代码实战 (HAL库)
Master 发送端配置: 需要将 Word Length 改为 9 Bits。
// 发送地址字节 (标记位=1)
// 0x1xx -> 第9位为1
uint16_t address_byte = 0x100 | Target_ID;
HAL_UART_Transmit(&huart1, (uint8_t*)&address_byte, 1, 10);
// 发送数据字节 (标记位=0)
uint8_t payload[] = {0x01, 0x02};
HAL_UART_Transmit(&huart1, payload, 2, 10);
Slave 接收端配置 (初始化):
void MX_USART1_UART_Init(void)
{
// ... 基础配置 Baudrate 等 ...
huart1.Init.WordLength = UART_WORDLENGTH_9B; // 必须是9位
huart1.Init.StopBits = UART_STOPBITS_1;
// 关键配置:多处理器通讯
if (HAL_UART_Init(&huart1) != HAL_OK) Error_Handler();
// 1. 设置本机地址 (例如 0x05)
// G0支持4位或7位地址检测,这里用7位
HAL_MultiProcessor_Init(&huart1, 0x05, UART_WAKEUPMETHOD_ADDRESSMARK);
// 2. 立刻进入静默模式
HAL_MultiProcessor_EnterMuteMode(&huart1);
// 3. 开启中断 (RXNE)
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
}
Slave 中断处理 : 在中断中,我们不需要手动判断地址了。只要中断进来了,说明这就是发给我的(或者是广播)! 处理完一帧后,记得再次调用 HAL_MultiProcessor_EnterMuteMode 让硬件重新静默,等待下一次呼唤。
5. 数据完整性:CRC 校验
单线总线极易受干扰(尤其是长距离 Mode B/C)。简单的"异或校验"或"累加和"往往不够。 STM32G0 全系内置了 硬件 CRC 计算单元(AHB 总线外设),速度极快。
建议:
-
在发送前,用硬件 CRC 计算 Payload 的校验值,附在帧尾。
-
接收端收到后,再次计算 CRC,比对一致才执行 Cmd。
/* G0 硬件 CRC 使用示例 */
__HAL_RCC_CRC_CLK_ENABLE(); // 别忘了开时钟
uint32_t Calculate_CRC(uint8_t *data, uint32_t len)
{
// 复位 CRC 计算单元
__HAL_CRC_DR_RESET(&hcrc);
// 累积计算
return HAL_CRC_Accumulate(&hcrc, (uint32_t*)data, len);
}
6. 本章小结
我们定义了单线通讯的"语言规则":
-
帧结构:定义了 Header, Cmd, Len, CRC 等字段。
-
寻址规则:明确了单播必须回、广播绝不回的铁律。
-
硬件加速 :利用 STM32G0 的 9-bit Address Mark Wakeup 功能,极大降低了从机 CPU 负载,这是相比普通 GPIO 模拟串口最大的优势。
下一章预告 : 协议定好了,如何用代码把它跑起来? 如果是简单的温湿度采集,用 裸机 (Bare Metal) 怎么写状态机才不会卡死? 在第四章《架构实现 I:裸机下的事件驱动与状态机设计》中,我们将构建一个基于 SysTick 的非阻塞收发框架。
/*******************************************
* Description:
* 本文为作者《嵌入式开发基础与工程实践》系列文之一。
* 关注我即可订阅后续内容更新,采用异步推送机制。
* 转发本文可视为广播分发,有助于信息传播至更多节点。
*******************************************/