4、Modbus协议详解:RTU与TCP模式、寄存器地址映射、数据帧结构

各位同学,今天我们来啃一块硬骨头——Modbus协议。说实话,这玩意儿在工业现场的地位,就跟普通话在中国的重要性一样。你只要跟PLC、变频器、传感器打交道,就绕不开它。

我最早接触Modbus是在一个水处理项目上。当时要读取30多台仪表的温度数据,厂家说支持Modbus RTU,我心想这还不简单?结果一上手就发现,寄存器地址映射表看得我头皮发麻。嗯,今天我就把当年踩过的坑,一个一个给你们讲清楚。

核心要点:Modbus本质上是一个主从通信协议。一个主站(通常是PLC或上位机)发起请求,多个从站(传感器、执行器)响应。主站问,从站答,就这么简单。

4.1 RTU模式 vs TCP模式:到底选哪个?

很多新手会纠结这个问题。我直接说结论:物理层不同,应用层几乎一样

对比项 Modbus RTU Modbus TCP
物理层 RS-232 / RS-485 以太网(RJ45)
传输距离 RS-485可达1200米 100米(交换机可扩展)
节点数量 最多247个 理论上无限制
数据帧结构 带CRC校验 带MBAP报文头
典型场景 工厂车间、户外设备 上位机、SCADA系统

我个人习惯:距离远、环境干扰大,用RTU;距离近、需要高速通信,用TCP。有一次我在一个钢铁厂调试,现场电机变频器离控制室有800米,用RS-485走RTU模式,稳得很。要是用TCP,光拉网线就得花掉预算的一半。

避坑指南:我曾经遇到过一个案例,客户坚持用TCP模式连接分布在三个车间的设备。结果因为交换机级联太多,延迟飙到了200ms,电机报警根本来不及响应。后来全部改成RTU+中继器,问题解决。记住:TCP不是万能的

4.2 寄存器地址映射:别被文档骗了

这是最容易出问题的地方。你想想看,一个变频器可能有几百个参数,每个参数对应一个寄存器地址。但不同厂家的地址映射规则,有时候真能把你绕晕。

Modbus定义了四种数据对象:

  • 线圈(Coil):位操作,可读可写。地址范围00001-09999
  • 离散输入(Discrete Input):位操作,只读。地址范围10001-19999
  • 输入寄存器(Input Register):16位,只读。地址范围30001-39999
  • 保持寄存器(Holding Register):16位,可读可写。地址范围40001-49999

说白了,我们最常用的是保持寄存器。电机的电流、电压、转速,一般都映射到这里。

但有个坑:地址偏移。比如文档说“电机电流在地址40001”,但实际发送报文时,地址字段填的是0x0000还是0x0001?

我告诉你标准做法:协议层地址从0开始。40001对应协议地址0x0000,40002对应0x0001,以此类推。有些厂家文档直接写“地址1”,你得自己换算。

注意:我曾经被一个国产温控器坑过。文档写“温度寄存器地址40003”,我发0x0002死活读不到数据。折腾了两小时,最后发现它家地址从1开始计数,得发0x0003才行。所以拿到新设备,先读一个已知值验证地址映射,这是铁律。

4.3 数据帧结构:拆开看看里面是什么

理解了地址映射,我们来看看报文到底长什么样。我画了一张图,帮你把结构理清楚。

Modbus RTU 数据帧结构 从站地址 1字节 功能码 1字节 数据区 N字节(寄存器地址+数据) CRC校验 2字节 示例:读取电机电流(保持寄存器40001) 0x01 从站1号 0x03 读保持寄存器 0x00 0x00 起始地址 0x00 0x01 读取数量 0x84 0x0A CRC校验 从站响应报文 0x01 从站地址 0x03 功能码回显 0x02 字节数 0x01 0x2C 电流值=300 0x?? 0x?? CRC校验

看到没?RTU的帧结构就这么简单。主站发请求,从站回响应。功能码0x03是读保持寄存器,0x06是写单个寄存器,0x10是写多个寄存器。常用的就这几个。

TCP模式呢?其实就是把RTU的CRC校验去掉,换成MBAP报文头。MBAP头包含事务标识符、协议标识符、长度等字段。说白了,TCP模式依赖以太网自身的校验机制,不需要CRC了。

实战经验:我建议你在调试时,先用串口助手抓一下报文。看看主站发了什么,从站回了什么。有一次我发现从站一直不回数据,抓包一看,原来是CRC算错了。很多现成的CRC计算库可以直接用,别自己手算,容易出错。

4.4 功能码速查表

这里我整理了一份常用功能码,建议你收藏。我在现场调试时,经常要翻这个表。

功能码 名称 操作对象 说明
0x01 读线圈 线圈 读取DO状态
0x02 读离散输入 离散输入 读取DI状态
0x03 读保持寄存器 保持寄存器 最常用,读模拟量
0x04 读输入寄存器 输入寄存器 只读模拟量
0x05 写单个线圈 线圈 控制DO
0x06 写单个寄存器 保持寄存器 设置参数
0x0F 写多个线圈 线圈 批量控制
0x10 写多个寄存器 保持寄存器 批量设置

小技巧:如果你要读取电机电流,用功能码0x03。如果要启动电机,用功能码0x05写线圈。记住:读用0x03/0x04,写用0x06/0x10,这八个字够你应付90%的场景。

4.5 实战:用Python读取电机电流

光说不练假把式。我写一段Python代码,用modbus_tk库读取电机电流。这个库我用了好几年,稳定可靠。

import modbus_tk.modbus_rtu as modbus_rtu
import modbus_tk.defines as cst
import serial

# 配置串口
ser = serial.Serial(
    port='COM3',        # 根据实际情况修改
    baudrate=9600,
    bytesize=8,
    parity='N',
    stopbits=1,
    timeout=1
)

# 创建Modbus RTU主站
master = modbus_rtu.RtuMaster(ser)
master.set_timeout(1.0)

try:
    # 读取从站1,保持寄存器起始地址0,读取1个寄存器
    # 对应文档中的40001地址
    data = master.execute(
        slave=1,
        function_code=cst.READ_HOLDING_REGISTERS,
        starting_address=0,
        quantity_of_x=1
    )
    
    # data返回的是元组,例如(300,)
    current_value = data[0]
    print(f"电机电流值: {current_value} A")
    
except Exception as e:
    print(f"读取失败: {e}")
finally:
    ser.close()

这段代码的核心就一行:master.execute()。你只需要告诉它从站地址、功能码、起始地址、读取数量,剩下的库帮你搞定。

注意:有些电机的电流值是浮点数,但Modbus寄存器只能传整数。这时候需要做数据缩放。比如实际电流300.5A,寄存器存的是3005,你得自己除以10。这个缩放比例,设备文档里一定会写,千万别漏看。

好了,Modbus协议的核心内容就这些。RTU和TCP怎么选、地址映射怎么避坑、报文结构怎么拆解,我都用自己的经验给你讲了一遍。下一节我们开始实战,用Python写一个完整的电机监测程序。

总结一句话:Modbus不难,难的是细心。地址别搞错,CRC别算错,波特率别配错。做到这三点,你就能在工业现场横着走。


专注资料整理