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,那又是另一番天地了。