8个功能码,干完90%的Modbus活儿
8个功能码,干完90%的Modbus活儿

8个功能码,干完90%的Modbus活儿

我把 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 只能配 4xxxx
  • 04H 只能配 3xxxx
  • 01H / 05H / 0FH 只能配 0xxxx
  • 02H 只能配 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 F6E6 6642 F6 E6 66
CDAB字交换E6 6642 F6E6 66 42 F6
DCBA小端字序66 E6F6 4266 E6 F6 42
BADC字节交换F6 4266 E6F6 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凭什么在工厂里横着走