2、Modbus协议基础回顾:RTU与TCP模式、功能码、数据模型(线圈、寄存器)
好,咱们正式开始第二讲。在动手做地址分配之前,我建议咱们先把Modbus协议的老底儿翻出来晒一晒。你可能会说:“这玩意儿我天天用,还用回顾?” 嗯,我理解。但说实话,我在项目里见过太多因为基础概念模糊导致的“低级错误”。比如有人把线圈地址当寄存器地址用,结果设备死活不响应。所以,咱们花点时间,把地基夯实了。
2.1 两种传输模式:RTU vs TCP
Modbus协议最常用的两种“马甲”,就是RTU和TCP。说白了,它们俩干的活一样——都是把数据打包发送。区别在于穿的衣服不同。
- Modbus RTU:跑在串口上(RS-232/RS-485)。数据是二进制格式,紧凑高效。一个报文里没有多余的“头”,全靠时间间隔来区分一帧的开始和结束。我个人习惯在工厂现场用RTU,因为抗干扰能力强,布线成本低。
- Modbus TCP:跑在以太网上。它把RTU的报文直接塞进TCP/IP的数据包里,前面加了个MBAP报文头。说白了,就是给RTU报文套了个“网络马甲”。
核心区别一句话:RTU靠时间判断帧结束,TCP靠报文头里的长度字段判断帧结束。
我在项目中遇到过一件事:有个同事用TCP的库去解析RTU的数据,结果死活收不到完整报文。为什么?因为TCP的库会等数据,而RTU的帧之间是有静默时间的。嗯,这里要注意:千万别混用解析方式。
2.2 功能码:Modbus的“指令集”
功能码就是Modbus设备能听懂的命令。你想想看,你发一个“01 03 00 00 00 01”,设备就知道你要读什么。这就是功能码的功劳。
常用的功能码就那么几个,我列个表,你一眼就能看明白:
| 功能码 | 名称 | 作用 | 我常用的场景 |
|---|---|---|---|
| 01 (0x01) | 读线圈 | 读取DO(数字量输出) | 读取继电器状态 |
| 02 (0x02) | 读离散输入 | 读取DI(数字量输入) | 读取按钮、限位开关 |
| 03 (0x03) | 读保持寄存器 | 读取AO或参数(可读写) | 读取变频器频率、温度值 |
| 04 (0x04) | 读输入寄存器 | 读取AI(只读) | 读取模拟量传感器数据 |
| 05 (0x05) | 写单个线圈 | 控制单个DO | 启动/停止电机 |
| 06 (0x06) | 写单个寄存器 | 设置单个参数 | 设定PID目标值 |
| 15 (0x0F) | 写多个线圈 | 批量控制DO | 批量复位报警 |
| 16 (0x10) | 写多个寄存器 | 批量设置参数 | 下载配方数据 |
我的小技巧:调试时,先用功能码03读一遍设备的所有寄存器,看看返回的数据是不是你预期的。这能快速判断设备地址和波特率是否设置正确。
2.3 数据模型:线圈、寄存器,别再搞混了
Modbus定义了四种数据对象。很多新手容易把线圈和寄存器搞混。我刚开始做项目时也犯过这个错——把线圈地址当寄存器地址发,结果设备返回异常码02(非法数据地址)。
咱们一个一个说清楚:
- 线圈(Coil):就是1个bit。要么是0,要么是1。对应物理世界就是继电器的通断、灯的亮灭。用功能码01、05、15操作。
- 离散输入(Discrete Input):也是1个bit。但它是只读的。对应物理世界就是按钮、限位开关的状态。用功能码02操作。
- 输入寄存器(Input Register):16个bit(2个字节)。只读。一般用来存模拟量输入值,比如4-20mA信号转换后的数字量。用功能码04操作。
- 保持寄存器(Holding Register):16个bit。可读可写。这是最常用的数据对象。变频器的频率、PID的设定值、累计流量,都放这里。用功能码03、06、16操作。
避坑指南:我曾经在一个项目里,把保持寄存器的地址从40001开始编号(PLC的惯用表示法),但实际发送报文时,地址是从0开始的。结果上位机软件和PLC对不上。记住:协议报文里的地址永远从0开始,PLC显示的地址(如40001)是加了偏移量的。
2.4 地址分配的核心逻辑
好了,有了上面的基础,咱们才能谈地址分配。你想想看,一个设备里有那么多线圈和寄存器,怎么给它们编号?
Modbus协议规定:每个数据对象都有一个唯一的地址。地址范围是0到65535(16位地址)。但实际项目中,我们通常只用到一部分。
我习惯这样分配:
- 线圈地址:从0x0000开始,按功能模块分配。比如:0x0000-0x000F给主控继电器,0x0010-0x001F给报警输出。
- 保持寄存器地址:从0x0000开始,按数据类型分配。比如:0x0000-0x000F存运行参数,0x0010-0x001F存配置参数。
这里有个关键点:线圈和寄存器的地址空间是独立的。也就是说,你可以有一个线圈地址0x0001,同时有一个寄存器地址0x0001,它们互不干扰。因为功能码不同,设备知道你要操作哪个空间。
实战建议:在设计地址表时,我建议你留出一些“空洞”。比如寄存器地址从0x0000到0x000F用了,下一个功能块从0x0020开始。这样以后扩展功能时,不用大改地址表。我曾经因为没留空洞,后来加一个参数就得把整个地址表往后挪,那叫一个痛苦。
2.5 报文示例:看看数据怎么“飞”
光说不练假把式。咱们看一个实际的RTU报文例子。假设我要读一个从站设备(地址0x01)的保持寄存器,从地址0x0000开始,读2个寄存器。
主站发送:
01 03 00 00 00 02 C4 0B
解释一下:
01:从站地址03:功能码(读保持寄存器)00 00:起始地址(高字节在前)00 02:读取数量(2个寄存器)C4 0B:CRC校验(循环冗余校验)
从站正常响应:
01 03 04 00 0A 00 14 7A 3E
01:从站地址03:功能码04:数据字节数(4个字节,因为2个寄存器×2字节)00 0A:第一个寄存器的值(10)00 14:第二个寄存器的值(20)7A 3E:CRC校验
我的调试习惯:用串口助手抓报文时,先看CRC对不对。如果CRC错了,后面的数据全是垃圾。我曾经花了一下午查一个“数据不对”的问题,最后发现是串口线的地线没接好,导致CRC频繁出错。
好了,这一章的内容就到这里。Modbus的基础就像盖房子的砖头,看起来简单,但用不好真会砸脚。下一章,咱们就正式进入地址自动分配的设计思路了。到时候你会发现,今天回顾的这些基础,全是关键。