我把 Modbus 的功能码梳理了一遍,常用的就 8 个。把这 8 个搞透,日常调试基本不用查文档了。这篇就把它们一次性讲清楚。
一、先分两类:读 和 写
Modbus 功能码看着多,其实就两个动作——读或者写,操作对象就两种——开关量或者数值量。
2×2 = 4 种组合,再加上”单个”和”多个”的区别,凑齐了这 8 个最常用的。
读取类(主站要数据)
- 01H 读线圈 —— 读开关量输出状态。继电器吸没吸合、电机转没转,用这个。对应
00001-09999。一次最多读 2000 个位(实际受设备能力限制,建议一次别超过 500)。 - 02H 读离散输入 —— 读开关量输入状态。按钮按没按、限位开关触没触发,用这个。对应
10001-19999。同样一次最多 2000 个位。 - 03H 读保持寄存器 —— 最常用的一个。读设定值、参数、配置数据。对应
40001-49999。一次最多读 125 个寄存器(这是协议硬限制,因为响应帧数据长度字段才 1 字节,最大只能表达 250 字节,每个寄存器 2 字节)。 - 04H 读输入寄存器 —— 读实时采集的模拟量。温度、压力、电流、电压。对应
30001-39999。同样一次最多 125 个。
记住一条:03 配 4 区,04 配 3 区。把 03 用到了 3 区,所以读不出来。这种错犯了第一次就别犯第二次。
🔧 小贴士:返回的位数据是按字节打包的。比如你用 01H 读了 8 个线圈,返回 1 个字节,bit0 对应第一个线圈地址。如果你只读 3 个线圈,返回还是 1 个字节(不足 8 位补齐 0),你得自己把低 3 位取出来——剩余的位不准用。
写入类(主站下指令)
- 05H 写单个线圈 —— 控制一个开关。启动一台电机、点亮一个灯。报文里数据字段填
0xFF00表示 ON,0x0000表示 OFF(别填别的值,协议就认这两个)。 - 06H 写单个寄存器 —— 改一个参数。设目标转速、调报警阈值。
- 0FH 写多个线圈 —— 批量控制开关。同时启停多台设备。数据按字节打包(和 01H 读回来一个套路,按位解析)。
- 10H 写多个寄存器 —— 写入里最常用的。一次下发一组参数,比如同时设 P/I/D 三个值。一次最多写 123 个寄存器(协议硬限制,和 03H 的上限逻辑一样)。
为什么 10H 比 06H 更常用?因为现实中很少只改一个参数。下配置的时候往往是一组一组地改,一个一个写太慢,而且每个写请求之间还有间隔,10H 一把梭,省事得多。
二、速查表(贴墙上那种)
| 功能码 | 操作 | 数据类型 | 权限 | 地址范围 | 典型场景 |
|---|---|---|---|---|---|
| 01H | 读线圈 | 1位开关 | 读/写 | 00001-09999 | 读继电器状态、电机运行标志 |
| 02H | 读离散输入 | 1位开关 | 只读 | 10001-19999 | 读按钮、限位开关、传感器状态 |
| 03H | 读保持寄存器 | 16位数值 | 读/写 | 40001-49999 | 读设备参数、设定值、配置 |
| 04H | 读输入寄存器 | 16位数值 | 只读 | 30001-39999 | 读实时温度、压力、电压、电流 |
| 05H | 写单个线圈 | 1位开关 | 写 | 00001-09999 | 控制继电器、启停电机 |
| 06H | 写单个寄存器 | 16位数值 | 写 | 40001-49999 | 改单个参数、设定值 |
| 0FH | 写多个线圈 | 1位开关 | 写 | 00001-09999 | 批量控制开关量输出 |
| 10H | 写多个寄存器 | 16位数值 | 写 | 40001-49999 | 批量改参数、下发配置 |
三、四个真正会绊倒你的坑
坑1:功能码和地址不匹配
这是最容易犯的错。规则就一条——功能码的数据类型必须和地址区一致:
03H只能配4xxxx04H只能配3xxxx01H/05H/0FH只能配0xxxx02H只能配1xxxx
还有,04H 是只读的,你拿它去”写”——协议层根本不支持,会直接报异常。
坑2:异常响应——功能码最高位变 1
正常情况下,从站回你的功能码和请求里的一样。但如果出错了,从站会把功能码最高位置 1,再跟一个异常码。
比如你发 03H 请求,从站回 83H——这就是异常响应。后面那个字节是异常码:
01H—— 非法功能码(设备不支持这个操作)02H—— 非法数据地址(你要的地址它没有)03H—— 非法数据值(值超范围或格式不对)04H—— 从站设备故障(设备自己挂了,比如传感器损坏、EEPROM 写失败)05H—— 确认(不是错误,是从站告诉你”收到了,在处理”,一般写操作耗时较长时返回)06H—— 从站忙(老设备、慢设备常见,让它喘口气再试)
看到 83 02,第一反应去查地址表,别瞎试。04H——多半是那种带 EEPROM 存储的老设备,写寄存器时报故障,断电重启一下可能就好。
坑3:32位浮点数的字节序
这个坑深一点,能让你怀疑人生。
很多模拟量(温度、压力)是 32 位浮点数,一个值要占两个连续的寄存器。比如 40001 和 40002 拼起来才是一个完整的 float。
问题来了——这 4 个字节怎么拼?Modbus 没规定,全看厂家。常见的有 4 种字节序。
举个实际的例子,浮点数 123.45 在内存里的十六进制是 42 F6 E6 66(IEEE 754)。下面看它在报文里可能长成什么样:
| 字节序 | 名称 | 寄存器1 (40001) | 寄存器2 (40002) | 报文里看起来 |
|---|---|---|---|---|
| ABCD | 大端字序 | 42 F6 | E6 66 | 42 F6 E6 66 |
| CDAB | 字交换 | E6 66 | 42 F6 | E6 66 42 F6 |
| DCBA | 小端字序 | 66 E6 | F6 42 | 66 E6 F6 42 |
| BADC | 字节交换 | F6 42 | 66 E6 | F6 42 66 E6 |
你如果预期是 ABCD(大端,最常见的默认),结果设备发的是 CDAB,那解出来就是一个天文数字或者 NaN。调试的时候经常是:”我这温度怎么读出来是 3.8e+38??”——字节序反了,正常。
调试软件(比如 Modbus Poll)右键 → Display 就能切换字节序,挨个试一遍,哪个值看着合理就是哪个。代码里就麻烦点,得自己写转换函数:
csharp
// C# 里切换字节序的一个土办法
byte[] raw = new byte[4];
raw[0] = (byte)(reg2 >> 8); // 寄存器2高字节
raw[1] = (byte)(reg2 & 0xFF); // 寄存器2低字节
raw[2] = (byte)(reg1 >> 8); // 寄存器1高字节
raw[3] = (byte)(reg1 & 0xFF); // 寄存器1低字节
float val = BitConverter.ToSingle(raw, 0); // CDAB 模式
习惯是:拿到新设备先抓一帧报文,用不同的字节序解一遍,确认了再写代码。不然等项目跑起来才发现数据不对,排查起来更恶心。
坑4(附赠):USB 转 485 模块的收发切换
这个不属于 Modbus 协议本身,但调试时它出现的频率实在太高了,不能不提。
RS485 是半双工的——同一对线上既要发又要收。USB 转 485 模块(比如 CH340 + MAX485 那种)靠一个 DE/RE 引脚来切换收发方向。问题是:什么时候切?切快了对方还没收完,切慢了你已经该收数据了。
- 好一点的模块(比如 FT232 方案的)有硬件自动流控,自动管理收发切换,基本不出问题。
- 便宜的 CH340 方案靠软件控制 DE/RE,驱动层做不好就会出现”自己发的报文尾字节丢了”或者”收的时候前半截数据被自己抢发了”。
排查方法:如果每次都是报文最后两个字节(CRC)不对,或者收不到响应,先怀疑这个模块的收发切换有问题。换一个带硬件自动流控的模块——FT232 + SP485/SP3485 方案比 CH340 稳不少,贵十几块,值。
四、收尾
8 个功能码,记住”03 配 4、04 配 3″,再会看异常响应,剩下的就是字节序这种 case by case 的活儿了。
说真的,Modbus 的功能码体系设计得挺巧——一个字节既表达了操作类型,又表达了数据类型,紧凑又没歧义。1979 年的设计,到现在还能打,是有道理的。
下一篇聊聊串口那点事,从 RS232 到 RS485,为什么工厂里清一色都是 485。
上一篇:Modbus地址的那个”5位数地址” 下一篇:RS485凭什么在工厂里横着走
