3、通讯协议基础(上):Modbus RTU协议原理、寄存器地址映射、功能码详解(03、06、16)

好,咱们进入通讯协议这一块。说实话,很多做电力监控的工程师,最后栽跟头都栽在通讯上。设备选对了,接线没问题,但数据就是读不上来。为什么?多半是协议没搞透。

Modbus RTU,这玩意儿在工业现场太常见了。我做了十几年项目,从老式的RS485到现在的以太网网关,底层几乎都离不开它。今天咱们就把它的底裤扒干净。

3.1 Modbus RTU协议原理

Modbus RTU是一种串行通讯协议。说白了,就是主站问,从站答。主站一般是你的上位机或者网关,从站就是那些电表、保护装置、智能断路器。

它的报文结构很简单,就三部分:地址码、功能码、数据区,再加一个校验。我习惯把报文想象成一个信封——地址码是收件人,功能码是你要干什么事,数据区是具体内容,校验是防伪标签。

举个例子,一个完整的读数据请求帧长这样:

从站地址:0x01
功能码:0x03
起始地址高字节:0x00
起始地址低字节:0x6B
寄存器数量高字节:0x00
寄存器数量低字节:0x03
CRC校验低字节:0x76
CRC校验高字节:0x87

嗯,这里要注意,Modbus RTU的数据是高位在前,低位在后。也就是大端模式。我刚开始做项目时,有一次把高低位搞反了,读出来的电压值直接翻了好几倍。排查了一下午才发现是字节顺序的问题。

核心要点:Modbus RTU是半双工通讯,同一时刻只能有一方发送数据。主站发完请求后,必须等待从站回复,不能连续发。

3.2 寄存器地址映射

寄存器地址映射,这是新手最容易懵的地方。你想想看,一个电表里有几百个数据,电压、电流、功率、电能...怎么区分?靠的就是寄存器地址。

Modbus定义了四种数据对象:线圈、离散输入、输入寄存器、保持寄存器。咱们做电力监控,最常用的是保持寄存器和输入寄存器。

数据对象 读写属性 PLC地址范围 Modbus协议地址 实际用途
线圈 可读可写 00001-09999 0x0000-0x270E 控制开关、继电器
离散输入 只读 10001-19999 0x0000-0x270E 读取开关状态
输入寄存器 只读 30001-39999 0x0000-0x270E 读取电压、电流等测量值
保持寄存器 可读可写 40001-49999 0x0000-0x270E 读取/设置参数、电能等

这里有个坑,我必须要说。很多设备厂家给的地址是PLC地址,比如40001。但你在报文里填的地址是协议地址,要从0开始算。40001对应协议地址0x0000,40002对应0x0001。我曾经见过一个工程师,直接把40001填进报文里,结果怎么读都是错。

我的习惯:拿到设备手册后,先把所有地址换算成协议地址,列个表贴在机柜上。省得每次都要算一遍。

3.3 功能码03:读保持寄存器

功能码03,这是咱们用得最多的一个。读电压、读电流、读功率,全用它。

请求报文格式:

从站地址 + 0x03 + 起始地址(2字节) + 寄存器数量(2字节) + CRC(2字节)

响应报文格式:

从站地址 + 0x03 + 数据字节数 + 数据(N字节) + CRC(2字节)

举个例子,我要读地址0x006B开始的3个寄存器:

请求:01 03 00 6B 00 03 76 87
响应:01 03 06 02 2B 00 00 00 64 79 86

响应里的06表示后面有6个字节数据。02 2B是第一个寄存器的值,换算成十进制是555。00 00是第二个,00 64是第三个,也就是100。

我记得有一次在现场调试,读回来的电压值总是跳变。排查了半天,发现是波特率设置不对。从站设的是9600,主站设的是19200,数据全乱了。所以通讯参数一定要核对清楚。

避坑指南:我曾经遇到过从站返回异常码的情况。如果功能码最高位变成1,比如返回0x83,说明出错了。后面跟的字节是错误码,01表示功能码不支持,02表示地址越界,03表示数据值非法。

3.4 功能码06:写单个寄存器

功能码06,用来写一个寄存器。比如设置电表的变比、修改通讯参数、控制继电器。

请求报文格式:

从站地址 + 0x06 + 寄存器地址(2字节) + 数据值(2字节) + CRC(2字节)

响应报文就是原样返回请求报文。如果返回的和发出去的一样,说明写成功了。

举个例子,把地址0x0001的寄存器写成0x00C8(也就是200):

请求:01 06 00 01 00 C8 48 0A
响应:01 06 00 01 00 C8 48 0A

嗯,这里要注意,写寄存器是有风险的。你想想看,万一写错了参数,电表的计量系数变了,那整个月的电费统计就全废了。所以我建议,写操作之前一定要先读一遍当前值,确认无误再写。

我的习惯:写参数前,先用功能码03读一遍当前值。写完后,再读一遍确认。这叫"读-改-写"三步法,能避免很多低级错误。

3.5 功能码16:写多个寄存器

功能码16,也叫0x10。当你需要一次写多个寄存器时,用它比用06一个个写效率高得多。

请求报文格式:

从站地址 + 0x10 + 起始地址(2字节) + 寄存器数量(2字节) + 字节数 + 数据(N字节) + CRC(2字节)

响应报文格式:

从站地址 + 0x10 + 起始地址(2字节) + 寄存器数量(2字节) + CRC(2字节)

举个例子,从地址0x0002开始写3个寄存器,值分别是0x0010、0x0020、0x0030:

请求:01 10 00 02 00 03 06 00 10 00 20 00 30 48 0A
响应:01 10 00 02 00 03 41 0A

请求里的06表示后面有6个字节数据。响应里只返回起始地址和数量,不返回具体数据。

为什么要有16功能码?我举个例子你就明白了。假设你要设置电表的时段费率,有8个时段,每个时段2个字节。用06功能码要发8次请求,用16功能码一次搞定。在485总线上,减少通讯次数就是减少出错概率。

实战经验:用16功能码时,一次写的寄存器数量不要太多。我一般控制在20个以内。有些老旧的从站设备,一次写太多会超时或者丢包。稳妥起见,分批次写更靠谱。

3.6 三个功能码的对比

功能码 名称 读写方向 一次操作数量 典型应用
0x03 读保持寄存器 主站→从站 1-125个 读取电压、电流、功率
0x06 写单个寄存器 主站→从站 1个 设置参数、控制开关
0x10 写多个寄存器 主站→从站 1-123个 批量设置参数、下发配置

最后说一句,Modbus RTU虽然老,但稳定可靠。我见过不少项目,用了十几年都没出过问题。关键是把原理搞懂,把细节抠到位。下一节咱们讲Modbus TCP,那又是另一番天地了。