Modbus RTU / TCP 入门:功能码、寄存器、从站地址、CRC
- 搞清 Modbus 的四种数据对象:线圈、离散输入、保持寄存器、输入寄存器,知道各自能读写哪些
- 认识常用功能码 01/02/03/04/05/06/15/16 的含义和请求/应答帧格式
- 理解 RTU(RS485 上)和 TCP(网络上)的帧格式差异:CRC vs MBAP 头
- 会用真实帧示例逐字节拆解,知道地址偏移问题是怎么来的
- 拿到一张 Modbus 故障排查表
你翻开科星某款网络继电器的手册,第一页就写"支持 Modbus RTU / Modbus TCP 协议"。翻到协议说明页,看到的是一张寄存器表、一堆十六进制数字,还有"功能码 01"、"功能码 03"、"线圈"、"保持寄存器"这些词,脑袋就开始转圈。
Modbus 是展厅里使用最广的工业控制协议,没有之一。科星的网络继电器、IO 控制器、采集器,有人串口服务器透传过来的设备,绝大多数都讲 Modbus。看懂这套协议,你就能控制展厅里绝大部分工业级设备。
好消息是:Modbus 其实很简单。它只有两件事:主控发请求,从站返应答。请求帧和应答帧都是定长或近定长的字节串,格式固定,每个字节的含义都在官方规范里写得清楚。这一节我们从数据模型开始,把帧格式、功能码、CRC 一次性说透。
涉及工业协议配置(R2)。帧格式、功能码、CRC 算法以 Modbus 官方规范(Modbus Application Protocol V1.1b3)为准(见文末来源);具体设备的寄存器地址以该设备手册的地址映射表为准,不可照此篇例子硬套——不同厂商对地址偏移的处理有所不同。
Modbus 数据模型:四张表
Modbus 把设备的所有数据组织成四张表,每张表都有不同的数据类型和读写权限:
| 数据对象 | 编号区间 | 数据类型 | 读写 | 典型用途 |
|---|---|---|---|---|
| 线圈(Coil) | 00001 ~ 09999 | 1 位(bit),可读写 | 读写 | 继电器开关状态(0=OFF,1=ON),可直接写 |
| 离散输入(Discrete Input) | 10001 ~ 19999 | 1 位(bit),只读 | 只读 | 干接点输入状态(传感器/按钮),只能读回来 |
| 保持寄存器(Holding Register) | 40001 ~ 49999 | 16 位(word),可读写 | 读写 | 设置值(亮度/温度阈值/运行参数) |
| 输入寄存器(Input Register) | 30001 ~ 39999 | 16 位(word),只读 | 只读 | 测量值(温度/电压/电流)只能读 |
"编号区间"是 Modbus 的数据地址(Data Address)表示法,是用来帮人理解数据归属的命名约定;实际帧里传输的是协议地址(Protocol Address),规则是:协议地址 = 数据地址编号 - 该区的起始号 - 1。
举例(以 Modbus Application Protocol 规范为准):
- 线圈编号 1(第一个线圈)→ 协议地址
0x0000 - 线圈编号 8(第八个线圈)→ 协议地址
0x0007 - 保持寄存器编号 40001(第一个保持寄存器)→ 协议地址
0x0000 - 保持寄存器编号 40010(第十个保持寄存器)→ 协议地址
0x0009
地址偏移是 Modbus 实战第一坑。 有些厂商手册把地址写成"线圈地址 1"对应 0x0001(即数据地址编号 = 协议地址),而不是规范的 0x0000。这种"偏移 +1"的写法在部分厂商文档里存在。遇到发帧应答都正常但实际控的是错误的线圈时,先试一下把地址 -1 或 +1 看看,或者直接以该设备手册的"协议地址"列为准——前提是手册里要明确标注出来是协议地址还是数据编号。
常用功能码:8 个够用
Modbus 定义了几十个功能码,但展厅里常用的就 8 个:
| 功能码 | 十六进制 | 操作 | 操作对象 |
|---|---|---|---|
| 01 | 0x01 | 读多个线圈 | Coil(1位可写) |
| 02 | 0x02 | 读多个离散输入 | Discrete Input(1位只读) |
| 03 | 0x03 | 读多个保持寄存器 | Holding Register(16位可写) |
| 04 | 0x04 | 读多个输入寄存器 | Input Register(16位只读) |
| 05 | 0x05 | 写单个线圈 | Coil |
| 06 | 0x06 | 写单个保持寄存器 | Holding Register |
| 15 | 0x0F | 写多个线圈 | Coil(批量写) |
| 16 | 0x10 | 写多个保持寄存器 | Holding Register(批量写) |
记忆方式:读 01~04(先读线圈和离散,再读两种寄存器);写 05/06(单写)、15/16(批量写)。奇数功能码写线圈(位),偶数功能码写保持寄存器(字)。
从站地址:每台设备的"名字"
Modbus 网络里,主控叫"主站(Master)",被控设备叫"从站(Slave)"。每台从站有一个地址(Slave ID),取值 1~247。
在 RTU 帧里,第一个字节就是从站地址。主控发出的每条请求帧带上目标从站地址,只有地址匹配的从站才处理并应答,其他从站保持沉默(地址 0 是广播,通常不要求应答)。
从站地址怎么配:大部分设备通过拨码开关或配置软件设置,详细在RS485 总线组网里讲过,这里不重复。
RTU 帧格式:逐字节拆解
Modbus RTU 是走 RS485(串口)时用的帧格式。一个完整的 RTU 请求帧长这样:
[从站地址 1B] [功能码 1B] [数据字段 nB] [CRC 2B]
例子:FC03 读保持寄存器(读 2 个寄存器,从地址 0x0000 开始)
请求帧(主控→从站,目标从站地址 0x01):
01 03 00 00 00 02 C4 0B
│ │ │────│ │────│ │──────
│ │ │ │ │ │ └──────── CRC16 低字节(0xC4),高字节(0x0B)
│ │ │ │ └──────────────── 读取数量:0x0002(读 2 个寄存器)
│ │ └────────────────────── 起始地址:0x0000(第 1 个保持寄存器)
│ └────────────────────────── 功能码:03 = 读保持寄存器
└────────────────────────────── 从站地址:01
正常应答帧(从站→主控,地址 0x01 回应):
01 03 04 xx xx xx xx CRC_L CRC_H
│ │ │ │────────────
│ │ │ └── 寄存器数据:两个 16 位寄存器共 4 字节,具体值以设备实际返回为准
│ │ └────── 字节数:0x04(2 个寄存器 × 2 字节)
│ └────────── 功能码:03(同请求,表示正常应答)
└────────────── 从站地址:01
CRC 是什么、怎么算:CRC16 是一种 16 位循环冗余校验算法,作用是让接收方检查帧是否在传输中出错。具体算法、手算方式和代码,在下一节校验算法专题里单独讲,这里先记住"RTU 帧最后 2 字节是 CRC,低字节在前高字节在后"。
例子:FC05 写单个线圈(线圈 1 置 ON)
请求帧(目标从站地址 0x01,线圈协议地址 0x0000,ON = 0xFF00):
01 05 00 00 FF 00 8C 3A
│ │ │──── │──── │────
│ │ │ │ └──── CRC16 校验(低字节 0x3A,高字节 0x8C)
│ │ │ └─────────── 输出值:FF 00 = ON;0000 = OFF(Modbus 规范定义,不是其他值)
│ │ └──────────────────── 线圈地址:0x0000(第 1 个线圈,协议地址)
│ └──────────────────────── 功能码:05 = 写单线圈
└──────────────────────────── 从站地址:01
正常应答帧:FC05 的正常应答帧是把整个请求帧原样回送(共 8 字节不含 CRC 头),你收到的应答和发出的请求一模一样——这是 Modbus 规范对 FC05 的定义。
例子:FC06 写单个保持寄存器
请求帧(目标从站地址 0x01,写寄存器 0x0001,写入值 0x0064 = 100):
01 06 00 01 00 64 D9 E1
│ │ │──── │──── │────
│ │ │ │ └──── CRC16 校验(0xD9 低字节,0xE1 高字节)
│ │ │ └─────────── 写入值:0x0064 = 十进制 100
│ │ └──────────────────── 寄存器地址:0x0001(第 2 个保持寄存器,协议地址)
│ └──────────────────────── 功能码:06 = 写单保持寄存器
└──────────────────────────── 从站地址:01
正常应答帧:与 FC05 类似,FC06 正常应答也是原样回送请求帧。
TCP 帧格式:多了 MBAP 头,少了 CRC
网络控制 TCP/UDP 里已经讲过 MBAP 头的结构,这里对比汇总:
| 字段 | RTU 帧 | TCP 帧 |
|---|---|---|
| 从站地址 | 帧第 1 字节 | 单元标识符(MBAP 第 7 字节) |
| 功能码 | 帧第 2 字节 | MBAP 后第 1 字节(帧第 8 字节) |
| 数据字段 | 功能码后 | MBAP 后(帧第 9 字节起) |
| 帧完整性校验 | CRC16(帧末 2 字节) | 无 CRC(TCP 层自带可靠性) |
| 额外头部 | 无 | MBAP 头(事务标识符 2B + 协议标识符 2B + 长度 2B)共 6 字节 |
Modbus TCP 帧 = MBAP 头(6 字节)+ 从站地址(1 字节)+ 功能码(1 字节)+ 数据
MBAP 头结构:
事务标识符(2B): 每次请求递增,主控用来匹配应答,通常从 0x0001 开始
协议标识符(2B): 固定 0x0000(表示 Modbus TCP)
长度(2B): 后续字节数(单元标识符+功能码+数据字段的总字节数)
以 FC05 写单线圈(从站地址 0x01,线圈 0x0000 置 ON)的 TCP 帧为例:
00 01 00 00 00 06 01 05 00 00 FF 00
│──── │──── │──── │ │ │──── │────
│ │ │ │ │ │ └── 输出值:FF 00 = ON
│ │ │ │ │ └──────── 线圈地址:0x0000
│ │ │ │ └──────────── 功能码:05
│ │ │ └──────────────── 单元标识符:01(即从站地址)
│ │ └───────────────────── 长度:0x0006(后续 6 字节)
│ └──────────────────────────── 协议标识符:0x0000(固定)
└─────────────────────────────────── 事务标识符:0x0001(第 1 次请求)
注意:TCP 帧里没有 CRC,不要加。这是很多人第一次写 Modbus TCP 帧时的失误——把 RTU 的 CRC 字节也加进去了,结果多出 2 个字节,设备无法解析。
Modbus 异常应答:设备告诉你哪里错了
从站出错时,不会静默,而是返回一个异常应答帧,格式是:
从站地址(1B) + (功能码 | 0x80)(1B) + 异常码(1B) + CRC(2B,RTU 时有)
功能码最高位置 1(即 OR 0x80)是异常应答的标志。异常码含义(以 Modbus Application Protocol V1.1b3 为准):
| 异常码 | 十六进制 | 含义 |
|---|---|---|
| 1 | 0x01 | 功能码不支持(设备不认识这个功能码) |
| 2 | 0x02 | 数据地址无效(地址超出设备范围) |
| 3 | 0x03 | 数据值无效(写入的值超出允许范围) |
| 4 | 0x04 | 从站设备故障(设备内部错误) |
例如,对从站 0x01 发了 FC03 请求(功能码 0x03),如果设备不支持,会回:
01 83 01 (CRC)
│ │ └──── 异常码 0x01 = 功能码不支持
│ └──────── 0x83 = 0x03 | 0x80,表示这是 FC03 的异常应答
└──────────── 从站地址 01
遇到异常应答,先看功能码(80 以上的那个字节 & 0x7F = 原功能码),再看异常码:
- 0x01:确认设备手册,这台设备是否支持你用的功能码(有些只支持 03/06,不支持 01/05)
- 0x02:地址超范围——要么你发的地址比设备最大地址还大,要么地址偏移算错了
- 0x03:写入值不在允许范围——比如写入寄存器时超出了设备允许的最大值 这三条异常码在实战调试中是"设备在帮你缩小排查范围",比完全静默要好处理得多。
展厅常见场景:科星设备的 Modbus 使用
科星(corxnet.com)的网络继电器、IO 控制器普遍支持 Modbus RTU(RS485 口)和 Modbus TCP(网口),两种方式的功能码和地址一致,只是传输载体不同。
以控制继电器开关为例(具体地址以该型号官方文档为准):
- 读继电器状态:用 FC01(读线圈)或 FC03(读保持寄存器,如果状态放在寄存器里),读回 1 表示闭合、0 表示断开
- 写继电器状态:用 FC05(写单线圈)控单路,ON=0xFF00,OFF=0x0000;多路批量操作用 FC15
- 读采集量(如电压/温度):用 FC04(读输入寄存器)或 FC03(读保持寄存器),返回的是原始值,再乘以手册里的换算系数
科星某些型号的 Modbus 地址表里,线圈地址从 0x0000 开始(符合规范),但也有型号从 0x0001 开始(地址偏移 +1)。接到新设备时先发一个 FC01 读从 0x0000 开始的 8 个线圈,如果应答的数据符合预期(继电器状态对得上),地址就是对的;如果全部偏移一位,就把地址改 +1 或 -1。这个偏移问题不是只有科星有,几乎所有 Modbus 设备都可能遇到,以手册为准是唯一可靠的方式。
故障排查表
| 现象 | 最可能的原因 | 怎么查 / 怎么办 |
|---|---|---|
| 发请求帧,完全无应答 | 从站地址不对 / 串口参数不对(RTU) | 先单台、用广播地址 0x00 试(广播通常不要求应答,但确认设备能收到);再单独对一台确认地址 |
| 收到异常应答,异常码 0x01 | 功能码设备不支持 | 查手册,确认该型号支持的功能码列表,改用支持的功能码 |
| 收到异常应答,异常码 0x02 | 数据地址超范围 / 地址偏移算错 | 先读地址 0x0000 开始的少量(如 1 个),确认在范围内;核对手册地址偏移 |
| RTU 帧发出无应答(工具显示已发) | CRC 计算错误,从站丢弃 | 重新核算 CRC16(见下一节校验算法专题);或用自动计算 CRC 的工具 |
| TCP 帧发出无应答 | 把 RTU 的 CRC 带进 TCP 帧了 | TCP 帧不含 CRC,去掉末尾 2 字节 |
| TCP 帧 MBAP 头长度字段算错 | 长度字段值不对,设备无法解析帧边界 | 长度 = 从单元标识符起的字节数 = 1(地址)+1(功能码)+数据字节数 |
| 多台设备时某台偶发不响应 | RS485 总线问题(终端电阻/地址冲突) | 参考RS485 总线组网故障排查 |
| 读回的值比预期大/小 1 | 地址偏移(±1 偏移) | 从 0x0000 和 0x0001 都试一次,对照手册实际地址确认 |
| 写了 FC05 ON,继电器动了但状态读回还是 0 | 状态寄存器和控制寄存器地址不同 | 分别查手册"状态读取地址"和"控制写入地址",读写用不同地址 |
小结 · 你现在掌握了什么
- Modbus 数据模型四张表:线圈(位/可写)、离散输入(位/只读)、保持寄存器(字/可写)、输入寄存器(字/只读)。
- 8 个常用功能码:读四种(01/02/03/04)、单写两种(05/06)、批量写两种(0F/10)。
- RTU 帧 = 从站地址 + 功能码 + 数据 + CRC(2B);TCP 帧 = MBAP 头(6B)+ 从站地址 + 功能码 + 数据,无 CRC。
- 地址偏移是第一坑,协议地址 ≠ 数据编号,以设备手册的"协议地址"列为准。
- 异常应答是设备告诉你错在哪,0x01/0x02/0x03 三个异常码对应不支持/地址超范围/数据非法。
下一步:RTU 帧末尾的 CRC16 怎么算?和校验/异或/BCC 又是什么?展厅里还有很多设备用私有协议带校验,去看校验算法专题:Sum / XOR / CRC16 / BCC 手算与代码,把各种校验搞透,再也不怕对不上校验字节。