4. 多机通信协议设计:主从模式与多主模式
多机通信,说白了就是让多个设备在同一条总线上“说话”。但问题来了——谁先说?谁听谁的?万一两个人同时开口怎么办?
我刚开始做多机通信时,以为只要把几个单片机的 TX、RX 连在一起就能聊起来。结果一上电,数据全乱套。嗯,从那以后我才明白:没有协议的总线,就是菜市场。
4.1 主从模式:最简单的“一问一答”
主从模式是最常用的多机通信方式。总线上只有一个主机(Master),其他都是从机(Slave)。主机掌握“话语权”,从机只能被点名后才能发言。
核心规则:
- 主机主动发送数据帧,从机被动响应
- 从机之间不直接通信,必须通过主机中转
- 总线冲突概率极低,实现简单
我在一个温湿度采集项目中用过这种模式。一个 STM32 做主控,挂了 8 个传感器节点。主机轮询每个节点,节点收到自己的地址就回复数据。简单、稳定、好调试。
主从模式的典型流程:
- 主机发送地址字节,选中某个从机
- 从机识别到自己的地址,准备接收数据
- 主机发送功能码 + 数据
- 从机执行操作并回复响应
- 主机收到响应,继续下一轮轮询
4.2 多主模式:谁都能说话,但得守规矩
多主模式就复杂多了。总线上每个节点都能主动发起通信,就像会议室里谁都能发言,但得等别人说完。
我做过一个工业控制项目,要求多个 PLC 之间实时交换状态信息。用主从模式?不行,每个 PLC 都可能需要主动上报紧急事件。最后选了多主模式,配合 CSMA/CD 机制。
注意:多主模式必须解决总线仲裁问题。否则两个节点同时发送数据,总线就乱套了。我曾经因为没做仲裁,导致整个产线停机 20 分钟——教训深刻。
4.3 地址字节与数据字节的区分
多机通信中,接收端怎么知道当前收到的是“叫谁”还是“说什么”?
常用的方法有两种:
| 区分方式 | 原理 | 典型应用 |
|---|---|---|
| 第9位地址位 | UART 数据帧的第9位为1表示地址,为0表示数据 | 8051 多机通信、STM32 地址位模式 |
| 特殊字节标记 | 用特定字节(如 0xAA)作为地址前缀 | Modbus RTU、自定义协议 |
我个人习惯用第9位地址位的方式。为什么?因为硬件自动识别,不占用数据字节,效率高。STM32 的 USART 支持地址位模式,配置好之后,收到地址字节会自动触发中断,数据字节则不会打扰 CPU。
经验之谈:用第9位地址位时,注意所有节点的数据格式必须一致。我遇到过有人把 8 位数据 + 1 位地址配成了 9 位数据 + 无地址——结果地址和数据全混了,查了三天才找到原因。
4.4 总线仲裁机制
多主模式下,两个节点同时发送怎么办?总线仲裁就是解决这个问题的。
常见的仲裁方式:
- CSMA/CD(载波监听多点接入/冲突检测):先听总线是否空闲,空闲才发送。发送时继续监听,发现冲突就退避重试。
- 位仲裁(Bit Arbitration):每个节点发送自己的地址位,谁先发送 0 谁就赢得总线。CAN 总线用的就是这种方式。
- 令牌传递(Token Passing):总线上只有一个令牌,拿到令牌的节点才能说话。谁用完就传给下一个。
UART 多机通信中,最常用的是 CSMA/CD。为什么?因为 UART 是异步通信,没有时钟同步,位仲裁很难实现。
下面这张图展示了 CSMA/CD 的工作流程:
4.5 冲突检测与重试机制
冲突检测是 CSMA/CD 的核心。怎么检测?
发送节点在发送数据的同时,持续监听总线上的电平。如果自己发送的是 1,但总线上读到的是 0,说明有别的节点在拉低总线——冲突了!
重试策略:
- 立即重试:冲突后马上再发一次。简单但容易再次冲突。
- 随机退避:每个节点随机等待一段时间再重试。时间 = 随机数 × 基本退避时间。
- 指数退避:每次冲突后,退避时间范围翻倍。比如第一次 1-2 个时隙,第二次 1-4 个时隙,第三次 1-8 个时隙。
我的建议:嵌入式系统中,指数退避最实用。为什么?因为节点数量少,冲突概率低,但一旦冲突,指数退避能快速让总线恢复稳定。我在一个 12 节点的多主系统中用过,效果很好。
4.6 典型协议栈:Modbus RTU
说到多机通信协议,Modbus RTU 是绕不开的。它是工业自动化领域的事实标准,几乎所有 PLC、变频器、传感器都支持。
Modbus RTU 的特点:
| 特性 | 说明 |
|---|---|
| 物理层 | RS-232 / RS-485,半双工 |
| 通信模式 | 主从模式,一主多从 |
| 地址范围 | 1-247(0 为广播地址) |
| 帧格式 | 地址 + 功能码 + 数据 + CRC16 |
| 波特率 | 9600 / 19200 / 115200 等 |
Modbus RTU 数据帧结构:
| 地址字节 (1B) | 功能码 (1B) | 数据区 (N B) | CRC16 (2B) |
| 0x01 | 0x03 | 0x00 0x00 0x00 0x01 | 0x84 0x0A |
举个例子:主机读取 1 号从机的寄存器。地址 0x01,功能码 0x03(读保持寄存器),起始地址 0x0000,读取数量 0x0001。CRC16 校验码 0x840A。
从机收到后,如果地址匹配,就执行操作并回复:
| 地址字节 (1B) | 功能码 (1B) | 数据长度 (1B) | 数据区 (N B) | CRC16 (2B) |
| 0x01 | 0x03 | 0x02 | 0x00 0x64 | 0xB8 0x1D |
回复中数据长度 0x02,表示后面有 2 个字节的数据。0x0064 就是 100——寄存器的当前值。
踩坑提醒:Modbus RTU 的帧间隔要求很严格。两帧之间必须有至少 3.5 个字符时间的空闲。我见过有人把帧间隔设得太短,结果从机把两帧当成一帧处理,CRC 校验永远过不了。查了整整一天才发现是时序问题。
Modbus RTU 的地址字节与数据字节怎么区分?很简单——地址字节是帧的第一个字节,接收端先判断地址是否匹配,匹配才继续解析后面的数据。不匹配就直接丢弃,不浪费 CPU 资源。
我个人习惯在项目中用 Modbus RTU 做设备级通信。为什么?因为生态好,调试工具多,随便一个串口助手都能抓包分析。自己写协议?除非有特殊需求,否则别折腾。
4.7 本章小结
多机通信协议设计,核心就三件事:
- 谁说话:主从还是多主?决定了总线控制权的分配方式。
- 说什么:地址和数据怎么区分?决定了接收端能不能正确解析。
- 冲突了怎么办:仲裁和重试机制决定了总线在异常情况下能不能恢复。
Modbus RTU 是一个很好的参考模板。它把主从模式、地址区分、CRC 校验都封装好了,拿来就能用。但理解它背后的设计思路,比直接调库更重要。