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。

我画了一张图,帮你理清这个关系:

Modbus地址映射关系图 设备手册地址(1-based) 例如:40001、30001、10001 减去偏移量 Modbus协议地址(0-based) 例如:0x0000、0x0001 常见地址类型偏移量 • 0x (线圈/离散输入):手册地址 00001 → 协议地址 0x0000 • 1x (离散输入):手册地址 10001 → 协议地址 0x0000 • 3x (输入寄存器):手册地址 30001 → 协议地址 0x0000 • 4x (保持寄存器):手册地址 40001 → 协议地址 0x0000

你看,不管手册上写的是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