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位地址)。但实际项目中,我们通常只用到一部分。

我习惯这样分配:

  1. 线圈地址:从0x0000开始,按功能模块分配。比如:0x0000-0x000F给主控继电器,0x0010-0x001F给报警输出。
  2. 保持寄存器地址:从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的基础就像盖房子的砖头,看起来简单,但用不好真会砸脚。下一章,咱们就正式进入地址自动分配的设计思路了。到时候你会发现,今天回顾的这些基础,全是关键。