4. Modbus功能码详解

在Modbus协议中,功能码(Function Code)是决定“这次通信到底要做什么”的核心字段。无论是串行链路上的RTU/ASCII,还是以太网上的TCP,请求PDU中的第一个字节就是功能码,它直接决定了后续数据区的组织方式、地址范围、数量限制以及异常响应的判断逻辑。对于设备测试与故障诊断而言,如果只记住“03是读保持寄存器”而说不清请求与响应的字节结构,就很难从抓包数据中定位问题。本节围绕最常用的八个功能码——01、02、03、04、05、06、15、16,逐一拆解其请求与响应格式,并给出测试中可直接对照的字节级说明。

一、核心原理:功能码决定数据区的“语法”

Modbus的PDU由“功能码 + 数据”组成。功能码告诉从站两件事:第一,操作哪一类对象(线圈、离散输入、保持寄存器、输入寄存器);第二,是读还是写。八个常用功能码可以按对象和方向分为四组:

  • 位对象读取:01读线圈、02读离散输入;
  • 寄存器读取:03读保持寄存器、04读输入寄存器;
  • 位对象写入:05写单个线圈、15写多个线圈;
  • 寄存器写入:06写单个保持寄存器、16写多个保持寄存器。

读操作请求的核心参数是“起始地址 + 数量”,响应则返回“字节数 + 数据”;写操作请求的核心参数是“地址 + 值”或“地址 + 数量 + 字节数 + 数据”,响应通常回显地址和数量/值,用于确认写入已被接受。理解这一对称关系,就能在抓包时快速判断报文是否完整。

常用Modbus功能码请求/响应结构总览 读取类:01 / 02 / 03 / 04 请求:功能码 | 起始地址Hi Lo | 数量Hi Lo 01/02数量≤2000;03/04数量≤125 响应:功能码 | 字节数 | 数据… 位对象按位打包,寄存器按2字节/个 异常:功能码+0x80 | 异常码(01/02/03/04) 写入类:05 / 06 / 15 / 16 05/06请求:功能码 | 地址Hi Lo | 值Hi Lo 05值:FF00置位 / 0000复位;06为寄存器值 15/16请求:功能码 | 地址 | 数量 | 字节数 | 数据 15数量≤1968;16数量≤123 响应:回显地址与数量/值,确认写入 功能码与数据对象映射 线圈 Coil 01读 / 05写单个 / 15写多个 离散输入 DI 02读(只读) 保持寄存器 HR 03读 / 06写单个 / 16写多个 输入寄存器 IR 04读(只读) 测试要点:地址从0还是1开始?数量是否超限?字节序是否一致? 请求中的“起始地址”是协议地址,设备手册常标1-based,需减1换算 响应字节数必须与请求数量匹配,否则判为异常或截断

二、方法流程:逐字节拆解请求与响应

下面以RTU帧为例,给出各功能码的典型报文结构。RTU帧由“从站地址 + PDU + CRC”组成,以下只展示PDU部分,便于对照功能码本身。

01 读线圈(请求):01 | 00 13 | 00 25
含义:功能码01,起始地址0x0013,读取0x0025=37个线圈

01 读线圈(响应):01 | 05 | CD 6B B2 0E 1B
含义:功能码01,字节数5,5字节位数据(37位按位打包)

02 读离散输入(请求):02 | 00 C4 | 00 16
含义:功能码02,起始地址0x00C4,读取22个离散输入

03 读保持寄存器(请求):03 | 00 6B | 00 03
含义:功能码03,起始地址0x006B,读取3个保持寄存器

03 读保持寄存器(响应):03 | 06 | 02 2B 00 00 00 64
含义:功能码03,字节数6,3个寄存器值:0x022B、0x0000、0x0064

04 读输入寄存器(请求):04 | 00 08 | 00 01
含义:功能码04,起始地址0x0008,读取1个输入寄存器

05 写单个线圈(请求):05 | 00 AC | FF 00
含义:功能码05,地址0x00AC,写入FF00表示置位ON

05 写单个线圈(响应):05 | 00 AC | FF 00
含义:响应回显地址和值,确认写入成功

06 写单个保持寄存器(请求):06 | 00 01 | 00 03
含义:功能码06,地址0x0001,写入值0x0003

15 写多个线圈(请求):0F | 00 13 | 00 0A | 02 | CD 01
含义:功能码15,起始地址0x0013,写10个线圈,字节数2,数据CD 01

16 写多个保持寄存器(请求):10 | 00 01 | 00 02 | 04 | 00 0A 01 02
含义:功能码16,起始地址0x0001,写2个寄存器,字节数4,数据00 0A 01 02

16 写多个保持寄存器(响应):10 | 00 01 | 00 02
含义:响应回显起始地址和寄存器数量,确认写入

从上述报文可以看出,读请求的“数量”字段在响应中并不直接回显,而是通过“字节数”间接体现;写多个请求的“字节数”必须等于“数量×2”(寄存器)或“ceil(数量/8)”(线圈)。测试时若发现字节数与数量不匹配,从站通常会返回异常码03(非法数据值)。

三、案例/场景:一次03功能码读取失败的排查

某现场使用Modbus RTU读取电表保持寄存器,主站发送:01 03 00 00 00 02 C4 0B,期望读取地址0x0000开始的2个寄存器。从站无响应。排查过程如下:

  1. 确认串口参数:波特率9600、8数据位、无校验、1停止位,与电表手册一致;
  2. 用串口监听工具抓取原始字节,发现主站实际发出的是01 03 00 00 00 02 C4 0B,CRC正确;
  3. 检查电表手册,发现其保持寄存器地址从1开始编号,协议地址应为0x0000对应手册的40001,但该电表要求读取“电压”时起始地址为0x0000、数量为2,理论上正确;
  4. 进一步检查发现电表从站地址实际为0x02,而非0x01,修改从站地址后通信恢复。

这个案例说明:功能码格式正确并不等于通信成功,从站地址、串口参数、地址基准、数量限制都是测试中必须同步核对的要素。尤其在多设备总线上,从站地址冲突或错位是高频故障。

四、误区/复盘:功能码使用中的常见陷阱

误区一:把“起始地址”直接当作设备手册上的1-based地址。Modbus协议地址从0开始,手册常标40001、30001等。若直接发送40001,会因地址越界返回异常码02(非法数据地址)。正确做法是:协议地址 = 手册地址 - 40001(保持寄存器)或 - 30001(输入寄存器)。

误区二:忽略数量上限。01/02最多读2000个位,03/04最多读125个寄存器,15最多写1968个线圈,16最多写123个寄存器。超限时从站可能返回异常码03,也可能直接截断,导致数据不完整。

误区三:写多个线圈时字节数计算错误。15功能码的字节数 = ceil(数量/8)。例如写10个线圈,字节数为2;写9个线圈,字节数也为2,但最后一个字节的高位补0。若字节数写成1,从站会判为非法数据值。

复盘建议:测试时先用手工构造的报文验证单个功能码,再逐步增加数量;抓包时重点核对“功能码、起始地址、数量、字节数、CRC”五个字段。对于写操作,务必检查响应是否回显了正确的地址和数量/值,不能只看“有响应”就判定成功。

五、常用功能码速查清单

功能码 名称 对象 请求关键字段 响应关键字段 数量上限
01 (0x01)读线圈线圈起始地址、数量字节数、位数据2000
02 (0x02)读离散输入离散输入起始地址、数量字节数、位数据2000
03 (0x03)读保持寄存器保持寄存器起始地址、数量字节数、寄存器值125
04 (0x04)读输入寄存器输入寄存器起始地址、数量字节数、寄存器值125
05 (0x05)写单个线圈线圈地址、值(FF00/0000)回显地址和值1
06 (0x06)写单个保持寄存器保持寄存器地址、值回显地址和值1
15 (0x0F)写多个线圈线圈起始地址、数量、字节数、数据回显起始地址和数量1968
16 (0x10)写多个保持寄存器保持寄存器起始地址、数量、字节数、数据回显起始地址和数量123

掌握这八个功能码的请求与响应格式,是Modbus设备测试的基本功。在实际诊断中,建议将抓包报文与上表逐字段对照,先确认功能码和地址范围,再核对数量与字节数,最后检查CRC或MBAP头,绝大多数通信问题都能快速定位。