2. Modbus协议基础:Modbus RTU与TCP帧结构、功能码详解(03/06/16)、地址映射规则

好,咱们正式开始啃Modbus这块硬骨头。说实话,做工业通信网关这么多年,Modbus是我打交道最多的协议,没有之一。你想想看,从PLC到变频器,从温控表到智能电表,几乎每个设备都挂着Modbus接口。今天我就把Modbus RTU和TCP的底裤扒开,让你看得明明白白。

2.1 Modbus RTU帧结构:串口上的“一问一答”

Modbus RTU跑在串口上,RS-232或者RS-485。它的帧结构其实很简单,我习惯把它拆成四段来看:

  • 地址域(1字节):从站地址,范围1-247。0是广播地址,248-255保留。
  • 功能码(1字节):告诉从站要干什么。读还是写?读线圈还是读寄存器?
  • 数据域(N字节):具体参数,比如起始地址、数据个数、要写入的值。
  • 校验域(2字节):CRC16校验,保证数据在传输过程中没被干扰。

举个例子,主站要读取从站地址为1的设备,从寄存器地址0x0000开始读2个寄存器:

请求帧:01 03 00 00 00 02 C4 0B
         |  |  |     |     CRC16
         地址 功能码 起始地址 寄存器数量

从站正常响应:

响应帧:01 03 04 00 0A 00 14 3A 0C
         |  |  |  |     |     CRC16
         地址 功能码 字节数 寄存器值

嗯,这里要注意:RTU帧里没有帧头帧尾标记,完全靠时间间隔来区分一帧数据。3.5个字符时间的静默表示帧开始,1.5个字符时间表示字节间隔。我在项目中遇到过,有些国产设备对时间要求不严格,导致帧粘连,调试时折腾了我一整天。

避坑指南:我曾经在某个项目中,因为串口波特率设置成19200,而设备实际跑的是9600,结果CRC校验一直报错。排查了整整半天才发现是波特率不匹配。所以,调试Modbus RTU时,第一步永远先确认波特率、数据位、校验位、停止位是否一致。

2.2 Modbus TCP帧结构:以太网上的“快车道”

Modbus TCP说白了就是把RTU的帧封装到TCP包里,去掉了CRC校验(因为TCP/IP协议栈自己会做校验),加了一个MBAP头。我个人觉得,TCP版本比RTU省心多了,至少不用操心串口参数。

MBAP头共7个字节:

字段 长度 说明
事务处理标识符 2字节 用于匹配请求和响应,一般从0开始递增
协议标识符 2字节 固定为0x0000,表示Modbus协议
长度 2字节 后续数据的字节数(从单元标识符开始算)
单元标识符 1字节 相当于RTU里的从站地址

同样的读寄存器操作,TCP帧长这样:

请求帧:00 01 00 00 00 06 01 03 00 00 00 02
         |     |     |     |  |  |     |
         事务ID 协议ID 长度  单元ID 功能码 起始地址 数量

你发现没有?TCP帧里没有CRC,长度字段代替了RTU里的时间间隔判断。说白了,TCP帧是“自描述”的,收到多少字节就是多少字节,不用猜。

2.3 功能码详解:03、06、16,这三个够用了

Modbus功能码有几十个,但实际项目中,我个人觉得掌握03、06、16这三个就能搞定90%的场景。为什么?因为大部分工业设备的数据都放在保持寄存器里,而这三个功能码就是专门操作保持寄存器的。

功能码03:读保持寄存器

这个最常用。读取从站中连续的多个保持寄存器,每个寄存器16位。请求时指定起始地址和数量,响应时返回对应的寄存器值。

举个例子,读取地址0x0100开始的3个寄存器:

请求:01 03 01 00 00 03 45 C6
响应:01 03 06 00 01 00 02 00 03 3A 0C
         |  |  |  |     |     |
         地址 功能码 字节数 寄存器1 寄存器2 寄存器3
小技巧:一次最多读取125个寄存器(0x7D),这是协议规定的上限。我习惯一次读64个,既不会超限,也不会因为数据太多导致响应超时。

功能码06:写单个寄存器

这个简单,就是往一个寄存器里写一个16位的值。请求帧里包含寄存器地址和要写入的值,从站响应时原样返回请求帧表示成功。

请求:01 06 00 01 00 0A 18 0A
         |  |  |     |     CRC
         地址 功能码 地址  值(10)
响应:01 06 00 01 00 0A 18 0A  (原样返回)

嗯,这里要注意:06功能码只能写一个寄存器。如果你想一次写多个,就得用16功能码。

功能码16:写多个寄存器

这个比06复杂一点,但效率高。请求帧里要指定起始地址、寄存器数量、字节数,然后跟上要写入的数据。

请求:01 10 00 01 00 02 04 00 0A 00 14 3A 0C
         |  |  |     |     |  |     |
         地址 功能码 起始地址 数量 字节数 值1(10) 值2(20)
响应:01 10 00 01 00 02 10 08
         |  |  |     |     CRC
         地址 功能码 起始地址 数量

响应帧只返回起始地址和数量,不返回具体值。这是协议规定的,别指望从站会回传写入的值。

避坑指南:我曾经在调试一个变频器时,用16功能码写参数,结果变频器一直报异常码03(非法数据值)。查了半天才发现,变频器的寄存器地址是0x2000,而我写的是0x2000,但变频器手册里写的是“地址偏移0x1000”。说白了,不同厂家的地址映射规则不一样,一定要看手册。

2.4 地址映射规则:别被“地址偏移”坑了

地址映射是Modbus协议里最容易出问题的地方。我见过太多工程师在这里翻车了。说白了,Modbus地址有四种表示方式:

  1. 物理地址:协议帧里实际传输的地址,从0x0000开始。
  2. 逻辑地址:PLC或HMI里看到的地址,比如40001、30001这种。
  3. 功能码关联地址:不同功能码对应不同的地址空间,比如03功能码对应4xxxx(保持寄存器),01功能码对应0xxxx(线圈)。
  4. 设备内部地址:设备手册里写的地址,可能和协议地址差一个偏移量。

举个例子,你看到HMI上显示“40001”,这个地址对应的是:

  • 功能码:03(读保持寄存器)或06/16(写保持寄存器)
  • 协议地址:0x0000(因为40001 - 40001 = 0)
  • 设备内部地址:可能是0x0000,也可能是0x1000,看设备厂家怎么定义

我习惯的做法是:先看设备手册,找到“Modbus地址映射表”,搞清楚协议地址和设备内部地址的对应关系。然后写代码时,统一用协议地址,不做任何偏移。这样最不容易出错。

核心要点:地址映射的黄金法则是——协议帧里传什么地址,代码里就用什么地址。不要自作聪明去加偏移,除非你100%确定设备手册要求这么做。

好了,Modbus协议基础就讲到这里。下一节我们会讲CANopen协议,到时候你会看到,这两种协议虽然都是工业通信,但设计思路完全不同。嗯,做好准备,CANopen比Modbus复杂多了。