3、Modbus功能码详解:01-06功能码(读/写位与寄存器)的报文结构与实战
各位工程师朋友,咱们今天来啃一块硬骨头——Modbus最核心的01到06号功能码。说实话,我见过不少同行,协议栈调了几年,对这六个码的理解还停留在“会发会收”的层面。但真正出问题的时候,往往就是这些基础功能码的细节没吃透。
我个人习惯把这六个码分成两组:位操作组(01、02、05、15)和寄存器组(03、04、06、16)。今天咱们先聚焦01-06,这是最常用的六个,也是我当年入行时手撸的第一个协议栈。
3.1 功能码01:读线圈状态
功能码01,说白了就是读数字量输出。你想想看,PLC里那些继电器输出、指示灯,都是线圈。这个命令就是问设备:“嘿,你的线圈是通还是断?”
请求报文结构:
从站地址 | 功能码01 | 起始地址高8位 | 起始地址低8位 | 数量高8位 | 数量低8位 | CRC低8位 | CRC高8位
1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节
响应报文结构:
从站地址 | 功能码01 | 字节数 | 数据1 | 数据2 | ... | 数据N | CRC低8位 | CRC高8位
1字节 | 1字节 | 1字节 | 1字节 | 1字节 | | 1字节 | 1字节 | 1字节
关键点:起始地址从0开始。比如你要读第1个线圈,地址填0x0000。我见过有人填1,结果读出来的数据全对不上,排查了半天才发现是地址偏移问题。
实战示例:读取从站地址为1的设备,从线圈地址10(0x000A)开始,连续读16个线圈。
请求:01 01 00 0A 00 10 29 C6
响应:01 01 02 CD 01 3A 7F
响应中数据字节数为2(因为16个线圈=2字节)。0xCD = 1100 1101,对应线圈10-17的状态;0x01 = 0000 0001,对应线圈18-25的状态。注意:线圈10对应数据字节的最低位。
我的小技巧:调试时如果读回来的数据全是0xFF或0x00,先别急着怀疑硬件。检查一下起始地址是不是写错了,或者数量是不是超过了设备实际支持的线圈数。我曾经在一个项目中,因为数量字段多写了1,导致设备直接返回异常码02(非法数据地址)。
3.2 功能码02:读离散输入
功能码02和01几乎一模一样,唯一的区别是:01读的是线圈(可读可写),02读的是离散输入(只读)。离散输入对应的是物理按钮、传感器信号这类输入点。
报文结构:和01完全一致,只是功能码换成0x02。
请求:01 02 00 00 00 08 79 C6
响应:01 02 01 A5 89 3F
这个例子读了8个离散输入,返回1字节数据0xA5 = 1010 0101。嗯,这里要注意:如果读取的数量不是8的倍数,最后一个字节的高位会补0。
我曾经踩过的坑:有一次客户说读回来的离散输入数据总是不对,我远程抓包一看,发现他请求了12个输入点,但响应里只返回了1个字节。问题出在哪?他用的库函数自动把数量向上取整了,但解析时没处理高位补零的情况。所以,永远不要假设响应数据的字节数等于你请求的数量/8,一定要根据响应中的“字节数”字段来解析。
3.3 功能码03:读保持寄存器
功能码03,这是Modbus里最常用的功能码,没有之一。保持寄存器是16位的,可读可写,一般用来存配置参数、设定值、运行数据等。
请求报文结构:
从站地址 | 功能码03 | 起始地址高8位 | 起始地址低8位 | 数量高8位 | 数量低8位 | CRC低8位 | CRC高8位
1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节
响应报文结构:
从站地址 | 功能码03 | 字节数 | 数据高8位 | 数据低8位 | ... | CRC低8位 | CRC高8位
1字节 | 1字节 | 1字节 | 1字节 | 1字节 | | 1字节 | 1字节
实战示例:读取从站地址1的设备,从寄存器地址100(0x0064)开始,连续读3个寄存器。
请求:01 03 00 64 00 03 04 0E
响应:01 03 06 00 0A 00 14 00 1E 3A 7F
响应中字节数为6(3个寄存器×2字节)。数据依次为:0x000A(10)、0x0014(20)、0x001E(30)。
字节序问题:Modbus标准规定是大端序(高字节在前)。但很多国产设备用的是小端序。我建议你在做驱动开发时,一定要在配置里留一个字节序切换的选项。别问我怎么知道的——当年有个项目,就因为字节序问题,现场调试了整整两天。
3.4 功能码04:读输入寄存器
功能码04和03的区别,类似于02和01的区别。04读的是输入寄存器(只读),一般用来读取模拟量输入、温度值、压力值等实时数据。
报文结构:和03完全一致,功能码换成0x04。
请求:01 04 00 00 00 02 71 CB
响应:01 04 04 01 2C 00 64 3A 7F
这个例子读了2个输入寄存器,返回0x012C(300)和0x0064(100)。
我的建议:如果你要读的数据是实时变化的(比如电机转速、温度),用功能码04。如果是配置参数(比如PID系数、报警阈值),用功能码03。这样从报文上就能一眼看出数据的性质,方便调试。
3.5 功能码05:写单个线圈
功能码05用来控制单个数字量输出。说白了就是让一个继电器吸合或者断开。
请求报文结构:
从站地址 | 功能码05 | 线圈地址高8位 | 线圈地址低8位 | 数据高8位 | 数据低8位 | CRC低8位 | CRC高8位
1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节
数据字段的特殊规定:
- 0xFF00 = 线圈接通(ON)
- 0x0000 = 线圈断开(OFF)
- 其他值 = 无效,设备应返回异常码
实战示例:将从站地址1的设备,线圈地址5(0x0005)置为ON。
请求:01 05 00 05 FF 00 9C 3A
响应:01 05 00 05 FF 00 9C 3A
注意:响应报文是请求报文的回显。这是Modbus的一个设计特点——写操作成功后,从站会把请求原封不动地返回给主站。
我曾经犯过的错:刚开始做项目时,我以为写线圈的数据字段可以填0x0001或0x00FF。结果设备死活没反应,抓包一看,返回了异常码03(非法数据值)。后来查手册才发现,Modbus规定得死死的:只有0xFF00和0x0000是合法的。所以,别自作聪明,老老实实按标准来。
3.6 功能码06:写单个寄存器
功能码06用来写一个16位的保持寄存器。这是最常用的写操作功能码。
请求报文结构:
从站地址 | 功能码06 | 寄存器地址高8位 | 寄存器地址低8位 | 数据高8位 | 数据低8位 | CRC低8位 | CRC高8位
1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节 | 1字节
实战示例:将从站地址1的设备,寄存器地址200(0x00C8)写入值0x03E8(1000)。
请求:01 06 00 C8 03 E8 09 3A
响应:01 06 00 C8 03 E8 09 3A
同样,响应是请求的回显。
重要提醒:写寄存器时,一定要确认该寄存器是否支持写操作。有些寄存器是只读的(比如设备型号、固件版本),你写进去它也不会理你,只会返回异常码02(非法数据地址)。我建议你在设计设备时,把寄存器地址空间规划好:0x0000-0x0FFF放只读参数,0x1000-0x1FFF放可读写参数,这样一眼就能看出来。
3.7 实战经验总结
好了,六个功能码讲完了。最后分享几点我这些年积累的经验:
- 调试工具要趁手:我个人习惯用Modbus Poll(主站模拟)和Modbus Slave(从站模拟)。抓包用Wireshark加串口转USB的监听器。没有趁手的工具,调试就像瞎子摸象。
- 异常码要背熟:01(非法功能码)、02(非法数据地址)、03(非法数据值)。看到这三个码,基本能定位90%的问题。
- 超时处理要合理:一般串口通信超时设500ms,以太网设1000ms。别设太短,也别设太长。太短容易误判,太长影响系统响应。
- 批量操作优先:如果需要连续读写多个位或寄存器,尽量用功能码15(写多个线圈)和16(写多个寄存器),而不是循环调用05和06。效率能差10倍以上。
嗯,今天就先聊到这儿。下一节咱们讲功能码15和16,以及一些高级应用技巧。各位回去可以拿手头的设备练练手,抓几个报文看看,比光看书强多了。