4、Modbus协议基础:Modbus RTU与Modbus TCP的帧结构、功能码分类、地址映射规则
各位同行,咱们今天聊聊Modbus协议。说实话,这玩意儿在消防联动控制器和BMS通讯里,几乎是绕不开的“通用语言”。我做了这么多年消防系统,见过太多因为协议理解不透彻导致的通讯故障。今天我就把Modbus RTU和Modbus TCP这两个最常用的变种,掰开揉碎了讲清楚。
4.1 为什么是Modbus?
Modbus诞生于1979年,比我都大几岁。但它至今活跃在工业现场,为什么?说白了就三个字:简单、开放、可靠。你想想看,消防系统需要的是稳定,不是花哨。Modbus这种主从架构,一问一答,逻辑清晰,非常适合消防控制器和BMS之间的数据交换。
我个人习惯把Modbus比作“工业界的普通话”。不管你是西门子的PLC,还是国产的消防主机,只要支持Modbus,就能对话。嗯,这里要注意,Modbus有两个主流版本——RTU和TCP,它们的帧结构完全不同,但核心逻辑是一样的。
4.2 Modbus RTU帧结构
RTU模式用在串行通讯上,RS-232或RS-485。我在项目里用得最多的是RS-485,因为传输距离远,抗干扰能力强。RTU的帧结构是这样的:
| 字段 | 长度 | 说明 |
|---|---|---|
| 地址码 | 1字节 | 从站地址,范围1-247 |
| 功能码 | 1字节 | 指示操作类型(读/写) |
| 数据区 | N字节 | 寄存器地址+数据内容 |
| CRC校验 | 2字节 | 循环冗余校验,低字节在前 |
举个例子,读取从站地址为1的消防主机状态寄存器(地址0x0000),请求帧就是:01 03 00 00 00 01 84 0A。其中01是地址,03是功能码(读保持寄存器),00 00是起始地址,00 01是读取数量,84 0A是CRC校验。
关键点:RTU帧没有起始位和停止位标识,靠的是帧间空闲时间来判断一帧的开始和结束。如果两个字节之间的间隔超过3.5个字符时间,接收方就认为上一帧结束了。这个时间参数在消防系统中特别重要,我曾经遇到过因为波特率设置不一致,导致帧间隔判断错误,整个系统通讯瘫痪的情况。
4.3 Modbus TCP帧结构
TCP版本就简单多了。它去掉了CRC校验,因为TCP/IP协议栈自己会处理。取而代之的是MBAP报文头。帧结构如下:
| 字段 | 长度 | 说明 |
|---|---|---|
| 事务标识符 | 2字节 | 用于匹配请求和响应 |
| 协议标识符 | 2字节 | 固定为0x0000 |
| 长度字段 | 2字节 | 后续字节数 |
| 单元标识符 | 1字节 | 相当于RTU的地址码 |
| 功能码 | 1字节 | 同RTU |
| 数据区 | N字节 | 寄存器地址+数据 |
同样的读取操作,TCP帧就是:00 01 00 00 00 06 01 03 00 00 00 01。你看,没有CRC了,长度字段直接告诉接收方“后面还有6个字节”。
我的经验:在消防联动控制器和BMS通讯时,如果走局域网,我建议用TCP。因为TCP有重传机制,丢包了会自动补发。但如果是走长距离的串行总线,RTU更可靠。我曾经在一个大型商业综合体项目中,因为用了TCP走无线网桥,结果网络抖动导致消防信号延迟,差点误了大事。后来全部改成RS-485走RTU,稳如老狗。
4.4 功能码分类
功能码是Modbus的灵魂。常用的就那么几个,我列个表:
| 功能码 | 名称 | 用途 | 消防场景 |
|---|---|---|---|
| 01 (0x01) | 读线圈状态 | 读取DO输出 | 读取声光报警器状态 |
| 02 (0x02) | 读离散输入 | 读取DI输入 | 读取手动报警按钮状态 |
| 03 (0x03) | 读保持寄存器 | 读取16位数据 | 读取烟感浓度值、温度值 |
| 04 (0x04) | 读输入寄存器 | 读取只读数据 | 读取系统运行参数 |
| 05 (0x05) | 写单个线圈 | 控制DO输出 | 启动/停止消防泵 |
| 06 (0x06) | 写单个寄存器 | 写入16位数据 | 设置报警阈值 |
| 15 (0x0F) | 写多个线圈 | 批量控制DO | 批量复位所有报警 |
| 16 (0x10) | 写多个寄存器 | 批量写入数据 | 批量配置参数 |
避坑指南:我曾经遇到过一个问题——BMS用功能码03去读消防主机的状态,但消防主机只支持功能码04。结果通讯一直失败,查了两天才发现是功能码不匹配。所以,对接前一定要确认双方支持的功能码列表。另外,功能码80-FF是异常响应码,如果收到0x83(功能码03+0x80),说明读操作失败了,后面跟着的异常码会告诉你原因。
4.5 地址映射规则
地址映射是Modbus协议里最容易搞混的地方。我直接说结论:Modbus协议里的地址是0-based(从0开始),但很多设备手册里写的是1-based(从1开始)。
举个例子,手册上说“烟感浓度值在地址40001”,这是PLC的地址表示法。40001对应的是Modbus协议里的地址0x0000。为什么?因为40001是“4”开头表示保持寄存器,后面“0001”是1-based地址,转换成0-based就是0。
我画了一张图,帮你理清这个关系:
你看,不管手册上写的是40001还是30001,到了协议层,地址都是从0x0000开始的。区别在于功能码不同——读保持寄存器用03,读输入寄存器用04。
实战技巧:我在做消防主机和BMS对接时,习惯先做一个地址映射表。比如:
// 消防主机地址映射表
// 手册地址 -> 协议地址 -> 功能码 -> 数据类型
// 40001 -> 0x0000 -> 03 -> UINT16 (烟感1浓度)
// 40002 -> 0x0001 -> 03 -> UINT16 (烟感2浓度)
// 30001 -> 0x0000 -> 04 -> UINT16 (系统运行状态)
// 10001 -> 0x0000 -> 02 -> BIT (手动报警按钮状态)
这个表做好后,调试的时候对着看,一目了然。我曾经因为地址偏移搞错,把烟感浓度读成了温度值,还好测试阶段发现了,不然真到验收时就要出洋相了。
4.6 数据格式与字节序
Modbus传输16位数据时,默认是大端模式(Big-Endian),也就是高字节在前,低字节在后。比如数值0x1234,先发0x12,再发0x34。
但32位数据(比如浮点数)就有点麻烦了。Modbus协议本身没有规定32位数据的字节序,这就导致不同厂家的设备可能不一样。有的先发高16位,有的先发低16位。
我曾经踩过的坑:有一次对接某品牌的消防主机,读取烟感浓度值时,BMS那边显示的数据完全不对。查了半天,发现是32位浮点数的字节序问题。消防主机用的是“高字在前”,而BMS默认是“低字在前”。最后在BMS的配置里改了一个参数,数据就正常了。所以,对接前一定要确认双方的字节序约定,特别是32位数据。
4.7 异常处理机制
Modbus的异常处理很简单。如果从站收到一个无法处理的请求,它会返回一个异常响应帧。异常帧的功能码是请求功能码加上0x80,后面跟一个异常码。
常见的异常码有:
- 01 (非法功能码):从站不支持这个功能码。比如你发03去读线圈,从站就会返回这个。
- 02 (非法数据地址):你读的地址超出了从站的范围。我遇到过有人读地址0xFFFF,结果从站直接报错。
- 03 (非法数据值):数据值不合法。比如你写寄存器时写了一个超出范围的值。
- 04 (从站设备故障):从站内部出问题了。这个在消防系统中比较常见,比如某个传感器坏了,从站就会返回这个。
嗯,这里要注意,异常响应不代表通讯断了。它只是告诉你“这个请求我处理不了”。真正的通讯故障,比如超时、CRC错误,那是收不到任何响应的。
我的调试习惯:在调试Modbus通讯时,我会先用一个Modbus调试工具(比如ModScan或Modbus Poll)单独测试每个功能码。先读一个已知地址,确认能正常响应。然后再逐步增加读取范围。这样能快速定位是地址问题、功能码问题,还是数据格式问题。别一上来就写一大段代码,然后抓瞎。
好了,Modbus协议的基础就讲到这里。RTU和TCP的帧结构、功能码分类、地址映射规则,这些都是硬知识。你只要把今天的内容吃透了,后面讲消防联动控制器和BMS的具体对接时,就会轻松很多。
公众号:蓝海资料掘金营,微信deep3321