3、Modbus RTU协议:帧结构、主从通信与功能码详解
各位同学,咱们今天来啃Modbus RTU这块硬骨头。说实话,这协议在工业现场太常见了,从PLC到变频器,从传感器到执行器,几乎人手一份。我当年刚入行时,第一个项目就是调试Modbus RTU通信,那会儿没少踩坑。今天我把这些经验揉碎了讲给你听。
3.1 帧结构:四个部分,一个都不能少
Modbus RTU的报文帧,说白了就是四个部分拼起来的:地址码、功能码、数据区、CRC校验。每个部分都有讲究,咱们一个一个说。
| 字段 | 长度 | 说明 |
|---|---|---|
| 地址码 | 1字节 | 从站地址,范围1~247,0为广播地址 |
| 功能码 | 1字节 | 指示操作类型,如读线圈、写寄存器 |
| 数据区 | N字节 | 具体数据,长度取决于功能码 |
| CRC校验 | 2字节 | 循环冗余校验,低字节在前 |
地址码:从站地址范围1~247,0是广播地址。我建议你从站地址别用1,为啥?很多设备出厂默认就是1,容易冲突。我在项目里习惯从10开始分配,省心。
功能码:这是协议的核心。01~06是常用功能,15和16是批量操作。后面我会详细讲每个码的用法。
数据区:这里放的是具体数据。比如读寄存器时,数据区包含起始地址和寄存器数量。写数据时,数据区包含要写入的值。
CRC校验:这个我得多说两句。CRC校验是Modbus RTU的命根子,算错了整个报文就废了。我见过不少新手,CRC算不对,通信死活不通。嗯,这里要注意:CRC是低字节在前,高字节在后,别搞反了。
重要提醒:Modbus RTU报文帧之间必须有至少3.5个字符时间的间隔,这是协议规定的帧间隔。如果间隔太短,从站会认为是一帧数据,导致解析错误。我曾经因为这个坑,调试了整整一个下午。
3.2 主从站通信流程:一问一答,规矩得很
Modbus RTU是典型的主从架构。说白了,就是主站问,从站答。主站只有一个,从站可以有多个。每个从站有自己的地址,主站通过地址码来区分。
通信流程其实很简单:
- 主站发送请求帧,包含目标从站地址、功能码、数据、CRC
- 所有从站都收到这个帧,但只有地址匹配的那个从站才会响应
- 从站处理请求,然后返回响应帧
- 主站收到响应,校验CRC,解析数据
你想想看,如果从站没响应怎么办?主站一般会设置超时时间,比如100ms。超时了就重发,重发3次还不行,就报错。我在项目中遇到过,有个从站偶尔会死机,后来发现是电源干扰导致的。加了个隔离模块,问题就解决了。
个人经验:主站发送请求后,一定要等从站响应完再发下一帧。别一股脑儿地发,从站处理不过来会丢帧。我习惯在每帧之间加10ms的延时,稳得很。
3.3 功能码详解:01/02/03/04/05/06/15/16
这8个功能码,覆盖了Modbus RTU 90%以上的应用场景。咱们一个一个过。
01功能码:读线圈状态
读线圈,说白了就是读DO(数字量输出)。线圈只有0和1两种状态。请求帧里要告诉从站:从哪个线圈开始读,读多少个。响应帧里,数据区是按位打包的,一个字节可以表示8个线圈的状态。
举个例子:读地址为0的从站,从线圈0开始读10个线圈。请求帧是这样的:
地址: 0x01
功能码: 0x01
起始地址高字节: 0x00
起始地址低字节: 0x00
线圈数量高字节: 0x00
线圈数量低字节: 0x0A
CRC低字节: 0xCD
CRC高字节: 0xCA
响应帧里,数据区第一个字节的低10位就是这10个线圈的状态。注意,如果线圈数量不是8的倍数,最后一个字节的高位补0。
02功能码:读离散输入
这个和01功能码很像,区别在于读的是DI(数字量输入)。DI是只读的,不能写。请求格式和01完全一样,只是功能码不同。
我记得有一次,客户说读到的DI状态不对,我排查了半天,发现是接线松了。嗯,有时候问题不在协议本身,而在物理层。
03功能码:读保持寄存器
保持寄存器是16位的,可以读写。这个功能码用得最多,比如读温度、压力、速度等模拟量。请求帧里要指定起始寄存器地址和寄存器数量。
响应帧里,每个寄存器占2个字节,高字节在前。比如读2个寄存器,响应数据区就是4个字节。
注意:寄存器地址是从0开始的,但有些设备手册里写的是从1开始。比如手册说"寄存器40001",实际上对应的是地址0。这个坑我踩过,差点把设备烧了。一定要看仔细!
04功能码:读输入寄存器
输入寄存器和保持寄存器一样是16位的,但它是只读的。比如读AD转换结果,就用04功能码。请求和响应格式和03完全一样。
05功能码:写单个线圈
写单个线圈,就是控制一个DO输出。数据区里,0xFF00表示置1,0x0000表示置0。其他值都是非法的。
请求帧示例:把地址为0的从站,线圈0置1。
地址: 0x01
功能码: 0x05
线圈地址高字节: 0x00
线圈地址低字节: 0x00
数据高字节: 0xFF
数据低字节: 0x00
CRC低字节: 0x8C
CRC高字节: 0x3A
06功能码:写单个寄存器
写单个寄存器,就是设置一个16位的值。数据区里直接放要写的值,高字节在前。
这个功能码我用的最多的是设置PID参数。有一次,我写了个脚本批量设置参数,结果发现有些寄存器写不进去。后来查手册才知道,有些寄存器是只读的。所以,写之前一定要确认寄存器属性。
15功能码:写多个线圈
这个和01功能码对应,一次可以写多个线圈。请求帧里要指定起始线圈地址、线圈数量、字节数,然后是按位打包的数据。
比如写8个线圈,数据区就是1个字节。写9个线圈,数据区就是2个字节,最后一个字节的高7位补0。
16功能码:写多个寄存器
这个和03功能码对应,一次可以写多个寄存器。请求帧里要指定起始寄存器地址、寄存器数量、字节数,然后是每个寄存器的值。
举个例子:写2个寄存器,值分别是0x1234和0x5678。数据区就是:0x04(字节数)、0x12、0x34、0x56、0x78。
核心总结:这8个功能码,01/02是读位,03/04是读字,05/06是写单个,15/16是写多个。掌握了这些,Modbus RTU通信你就拿下了大半。剩下的就是多练,多踩坑,多总结。
好了,这一章的内容就到这儿。下一章咱们聊聊Modbus ASCII协议,那个和RTU有啥区别?为什么现在用得少了?到时候我结合项目经验给你讲讲。
课后练习:自己写一个Modbus RTU的请求帧,读地址为5的从站,从寄存器10开始读3个寄存器。算一下CRC,看看对不对。不会算CRC的,网上有在线计算器,先练练手。