都说 Modbus 简单,但真到了现场,我见过太多人卡在地址上:设备手册写着 40001,软件里填 0,报错;照着说明书填 30001,读出来全是乱码;好不容易读数对了,写设定值又把功能码搞错。这些问题的根源,基本都是没把 Modbus 地址规则彻底吃透。
这篇就聚焦一个话题——Modbus 地址规则。从协议里的四张数据表,到 5 位逻辑编号和 6 位协议偏移的换算,再到寄存器字节序和站号规划,最后用一台温控仪做完整换算演示,把从手册到报文的全过程一步步拆开。内容不挑设备,不管是 PLC、触摸屏、组态软件、DCS,还是各种仪表传感器,只要你跟 Modbus 通信打交道,这套规则都通用。
1. 地址规则背后的设计逻辑
1.1 为什么 Modbus 要把数据分成四张表
Modbus 协议是 1979 年由 Modicon(也就是后来的施耐德电气)提出的,初衷特别朴素:PLC 和现场设备之间要传数据,但现场设备的输入信号(开关量、传感器读数)和输出信号(阀门开度、电机转速)以及内部计算数据(累计值、状态字)性质完全不同。如果全部混在一个大地址池里,主站没法判断某个地址的数据到底是"设备的真实状态"还是"主站下发的结果",通信安全根本没有保障。
所以协议把数据分成了四个区,也就是大家常说的四张表:
| 数据区 | 类型 | 读写特性 | 位宽 | 编号范围 |
|---|---|---|---|---|
| 线圈(Coil) | 开关量输出 | 可读可写 | 1 bit | 000001 - 099999 |
| 离散输入(Discrete Input) | 开关量输入 | 只读 | 1 bit | 100001 - 199999 |
| 输入寄存器(Input Register) | 模拟量输入 | 只读 | 16 bit | 300001 - 399999 |
| 保持寄存器(Holding Register) | 模拟量输出/参数 | 可读可写 | 16 bit | 400001 - 499999 |
这个分区的逻辑在工控现场特别重要。你去看一台温控仪的说明书,它会把"PV 当前温度"放在 30001 或 40001 区,"SV 设定温度"放在 40001 区,绝对不会随便乱放。就是因为协议规定了读写权限和数据类型,设备厂商只能在框架内安排映射。
1.2 5 位数字编号和 6 位数字编号的坑
很多新手对着说明书看地址,最容易懵的就是:为什么有的资料写"40001",有的写"4x0001",还有的写"0x0000"?这其实只是编号体系不同,本质是同一个寄存器。
先看传统编号。Modbus 协议标准的寄存器编号是 5 位十进制数,第一位代表数据区,后四位代表偏移。比如 40001,4 开头代表保持寄存器区,0001 表示该区内的第 1 个寄存器;30001 就是输入寄存器区第 1 个。这种表示法历史悠久,老工程师的习惯就是从设备手册上看 40001 就直接填。
但是到了现代,很多上位机组态软件、PLC 的 Modbus 指令块、网关配置页面,用的却是 6 位数字或纯 16 进制偏移地址。典型的有两种写法:
- 6 位数字:400001,其实就是把 40001 扩成 6 位,保持寄存器区编号 400001 开始,这样整个地址空间可以容纳更多寄存器。
- 偏移地址:0x0000、0x0001 这种。这里有个极易踩坑的点——无论数据区是哪张表,偏移都是从 0 开始计数的。
也就是说,40001 这个"逻辑地址"对应的偏移量是 0x0000,40002 对应 0x0001,以此类推。一套公式可以直接换算:
偏移地址 = 逻辑地址 - 区起始地址
- 保持寄存器区起始地址 = 40001,偏移 = 40001 - 40001 = 0x0000
- 输入寄存器区起始地址 = 30001,偏移 = 30001 - 30001 = 0x0000
- 离散输入区起始地址 = 10001,偏移 = 10001 - 10001 = 0x0000
- 线圈区起始地址 = 00001,偏移 = 00001 - 00001 = 0x0000
这个换算逻辑其实也说明了为什么我们看到很多组态软件里地址填写界面会同时提供"寄存器地址"和"偏移地址"两个输入框——因为它们负责不同层的寻址。
1.3 数据模型与功能码的对应关系
地址分区和功能码是绑定的。Modbus 主站去读写某个地址,实际上是通过指定功能码来区分"你要操作哪张表"的。比如:
| 功能码 | 名称 | 操作对象 |
|---|---|---|
| 01H | 读线圈状态 | 线圈区 |
| 02H | 读离散输入状态 | 离散输入区 |
| 03H | 读保持寄存器 | 保持寄存器区 |
| 04H | 读输入寄存器 | 输入寄存器区 |
| 05H | 写单个线圈 | 线圈区 |
| 06H | 写单个寄存器 | 保持寄存器区 |
| 0FH | 写多个线圈 | 线圈区 |
| 10H | 写多个寄存器 | 保持寄存器区 |
这个表对照着看就能明白:地址的前缀(0、1、3、4)不只是习惯,它实际上决定了主站会用哪个功能码去访问。如果你在软件里填地址 30001,工具自动会用 04H 功能码;填 40001,就用 03H。如果在写 40001 的时候手滑填成了 30001,主站会按 04H 去读输入寄存器,设备返回的数据完全不对。
注意:有些设备厂商(尤其是国产仪表)为了简化手册,会把"输入寄存器"和"保持寄存器"都写成 4xxxx 区,然后用功能码来区分。这种情况下你先看手册里的功能码说明,再决定地址怎么填,不要看到 4 开头就默认是保持寄存器。
2. 寄存器地址与比特地址的映射细节
2.1 线圈地址和离散输入地址的比特寻址
Modbus 里线圈(Coil)和离散输入(Discrete Input)都是 1 bit 的数据,主站一次读多个线圈时,返回的数据会自动打包成字节。比如主站读起始地址 00001、数量 9 个线圈,设备会返回 2 个字节,第一个字节的 bit0 到 bit7 分别对应线圈 1 到 8,第二个字节的 bit0 对应线圈 9,其余补 0。
这里有个容易搞混的点:线圈号 1 对应的是数据字节中的 bit 0,不是 bit 1。我之前调试一个环网柜的遥控回路,主站下发合闸指令到线圈地址 00001,现场设备一直没动作,排查老半天,发现是上位机把 bit 位置搞反了——以为线圈 1 应该放在 bit1,结果把整个字节移位了一位,指令虽然发出去了,但设备收到的是另一个线圈的状态。从那以后我习惯在抓包工具里先看原始报文,确认 bit 对应关系,再谈逻辑。常见做法是下载 Modbus Poll 这样的调试工具,读一段线圈数据,对照返回的十六进制字节和线圈编号,很快就摸清设备的位映射规则。
2.2 16 位寄存器与 32 位数据的大端小端
保持寄存器和输入寄存器都是 16 位(1 Word)为单位,但温度、流量、压力这些模拟量经常用 32 位浮点数或 32 位整数表示,那就需要占两个寄存器。两个寄存器怎么拼,就成了现场调试里最经典的坑之一。
Modbus 协议规定,多寄存器数据在报文里按大端字节序传输(高字节在前),但两个寄存器之间的先后顺序,协议本身没有强制规定,于是出现了两种常见排列:
- 大端模式(Big Endian / ABCD):高字在前。比如浮点数 1.0 在 IEEE754 里是 3F800000,存到寄存器对时,第一个寄存器放 3F80,第二个放 0000。
- 小端模式(Little Endian / CDAB):低字在前。同样 3F800000,第一个寄存器放 0000,第二个放 3F80。
很多仪表在参数配置里会有"字节序"选项,常见的有 ABCD、CDAB、BADC、DCBA 四种系列,分别对应字序和字节序的组合。实际调试中,如果你读了两个寄存器,组合出来的值像天文数字,基本就是字序选反了,把 DABC 改成 BADC 通常就对了。我写过一个小经验:先用固定值测试设备,比如给设备设一个已知的温度 25.5(对应 0x41CC0000),再把读到的两个寄存器原始值记录下来,自己用手算一遍,确认是大端还是小端。这种做法比乱试快得多。注意这里说的字节序只是组合顺序,Modbus RTU 报文本身的 CRC 校验和帧结构跟字节序没关系,别混淆。
2.3 地址偏置 1 与 0 的区别
还有一个老生常谈的问题:为什么有的软件里填 40001,有的填 40000?
这其实是"从 1 开始编号"和"从 0 开始编号"两种习惯的碰撞。Modbus 协议规范里寄存器地址(协议层)是从 0 开始计数的,即第一个保持寄存器的协议地址是 0x0000;但传统设备手册为了方便人阅读,用 1 起始的逻辑编号,即第一个保持寄存器叫 40001。上位机软件在处理时,内部往往用 0 起始的协议地址,而界面展示给用户时用 1 起始的逻辑地址。
所以很多组态软件里会有两个地址字段:一个是"寄存器地址"(逻辑编号,如 40001),一个是"内部偏移"(协议地址,如 0)。如果你用 Modbus Poll 这类工具,它的"Address"字段默认填的是协议偏移地址——读第一个保持寄存器就填 0,而不是 40001。填 40001 会直接指向第二个寄存器,数据全部错位。
实操技巧:不要在同一个项目里混用"逻辑编号"和"协议偏移"。要么全部按手册逻辑编号换算,要么全部按协议偏移处理。我见过不少项目,工程师在触摸屏和 PLC 之间写地址,一会儿用 40001 一会儿用 0,最后查故障查了整整两天,就因为这个偏差。
3. 不同串口/网络形态下的地址填写规则
3.1 Modbus RTU 从站地址与寄存器地址的区分
很多人一听到"Modbus 地址"就以为是寄存器编号,其实 Modbus RTU 报文里有两层地址概念:从站地址(Slave ID / 站号)和寄存器地址(Data Address)。
从站地址是链路层的设备标识,一根 RS485 总线上最多挂 247 个从站(1 到 247),每个从站的站号必须唯一,否则总线上两从站同时应答,报文直接冲突。实际项目里有个不成文的习惯:站号从 1 开始分,不要从 0,因为 0 是广播地址,所有从站都会接收但不应答。
寄存器地址则是应用层的数据标识,主站发起请求时,报文结构大致是:[从站地址] [功能码] [寄存器起始地址高字节] [寄存器起始地址低字节] [寄存器数量高字节] [寄存器数量低字节] [CRC 校验]。看到没,报文里的寄存器地址是 16 位的,而且先高后低。所以你在工具里看到的寄存器地址最大能到 65535,即 0xFFFF。
这里要注意 RTU 报文地址的单位,跟前面说的逻辑编号又不一样:报文里的地址就是协议偏移地址,从 0 开始。比如读保持寄存器第 1 个,报文里起始地址填 0x0000,数量填 0x0001,功能码 03。这些细节在抓包时直接可见,不抓几次包,光靠看手册很难建立起这个对应关系。
3.2 Modbus TCP 的地址映射与单元标识符
Modbus TCP 的报文结构比 RTU 多了个 MBAP 头部(7 个字节),其中包含一个单元标识符(Unit ID)。TCP 里这个 Unit ID 的用途是当你通过网关或串口服务器透传时,用来标识背后的 RTU 从站地址。如果设备直接支持 TCP 且只有一个从站,Unit ID 一般填 0 或 255 也可以,但很多网关要求填实际的从站号才能转发到正确的串口设备。
寄存器地址本身在 TCP 和 RTU 中是完全一致的,功能码也一样。差别在于 RTU 有 CRC 校验,TCP 则用 IP 层的校验和来保证传输完整,协议内部不再做额外帧校验。这也就是说,TCP 报文里你不会看到 CRC 字段,抓包工具显示的 Modbus TCP 帧比 RTU 干净很多,更容易直接阅读地址和值。实际调试时,我通常先用 Modbus Poll 配 TCP 连接,把地址和长度先测通,再转去改 RTU 设备,这样排除链路干扰,问题定位快。
3.3 一主多从轮询中的地址规划
现场最常见的就是 PLC 或触摸屏做 Modbus 主站,总线上挂一堆从站:变频器、温控表、电表、流量计,数量多的时候三十几个。这种场景下地址规划的核心原则:从站号与寄存器地址一起考虑,不要只想着把寄存器地址排满。
我的处理顺序是这样的:
- 先把每个从站分配固定站号,写在机柜端子图和程序注释里。
- 然后对每个从站,单独建一张"寄存器地址映射表",列清楚功能码、逻辑地址、协议偏移、数据类型(16位/32位)、字节序、读写权限。
- 最后把整个系统的映射表汇成一张总表,按从站号分组,方便排查。
这样做的好处是当现场出现数据错乱时,你可以先看站号有没有冲突,再看寄存器地址有没有越界,不用重新摸一遍设备。很多老工程师在图纸上留白处手写这些表,但我觉得最好用 Excel 或者直接在程序里建结构体数组,把映射表和程序逻辑绑定,维护起来省事得多。
4. 实操演示:从手册到报文的完整地址换算
4.1 某温控仪表手册的地址解读
我们拿一台典型的国产温控仪来做例子。设备手册里一般会有一张某寄存器地址表,类似这样:
| 参数 | 寄存器 | 功能码 | 数据类型 | 范围 |
|---|---|---|---|---|
| 当前温度 PV | 40001 | 03H | 16位无符号 | 0-4000 |
| 设定温度 SV | 40002 | 03H/06H | 16位无符号 | 0-4000 |
| 报警上限 | 40003 | 03H/06H | 16位无符号 | 0-4000 |
| 当前报警状态 | 30001 | 04H | 16位无符号 | 0-1 |
假设我们想用上位机读取当前温度,同时把设定温度写入 80.0℃。比如这台仪表数据格式规定:温度 80.0℃ 对应的寄存器数值为 800(分辨率 0.1℃)。那我该填什么地址?如果用 Modbus Poll 读取,保持寄存器第一个就是协议偏移 0,我填地址 0、功能码 03、长度 1,读出来的原始值如果是 800,那就对应手册的 40001。写入时填地址 1(对应 40002)、功能码 06、值 800,就能把设定温度写成 80.0℃。
如果你用的是 DCS/PLC 的 Modbus 指令块,那就要看指令块里地址参数是"逻辑地址"还是"偏移地址"。比如有的 PLC 指令块里"Address"填 40001 会被自动转换成偏移 0,有的则需要你自己填 0。这个没有统一标准,只能看具体平台的手册。踩过坑之后,我的原则是:先把设备手册的寄存器编号换算成协议偏移,再用抓包或调试工具验证主站实际发出的报文起始地址,两者对上才放心。
4.2 RTU 请求报文逐字节拆解
再具体一点,我们算一下读上面这台仪表保持寄存器 40001 到 40002 的 RTU 报文。假设从站地址是 1,功能码 03,起始寄存器协议地址 0x0000,数量 0x0002,CRC 我们先用常见工具自动算出,不手算(手算 CRC16 太容易出错,实际工作中没人手算,都是工具代劳)。
报文帧结构可以逐字节列出来:
| 字段 | 值 | 说明 |
|---|---|---|
| 从站地址 | 01 | 站号 1 |
| 功能码 | 03 | 读保持寄存器 |
| 起始地址高字节 | 00 | 地址 0x0000 的高字节 |
| 起始地址低字节 | 00 | 地址 0x0000 的低字节 |
| 寄存器数量高字节 | 00 | 数量 0x0002 的高字节 |
| 寄存器数量低字节 | 02 | 数量 0x0002 的低字节 |
| CRC 低字节 | 校验值 | 由 CRC16/MODBUS 算法计算 |
| CRC 高字节 | 校验值 | 低字节在前 |
如果你填错地址,比如把起始寄存器填成 40001 当成协议偏移 40001(也就是十六进制 0x9C41),报文就会变成起始地址 9C41,设备大概率返回异常码 02(非法数据地址)。这就是很多国产仪表返回 9003 之类错误码或者干脆不响应的一个常见原因——地址超范围。
注意:Modbus 的异常响应码里,02 表示非法数据地址,03 表示非法数据值,01 表示非法功能码。看到这些码先对照报文地址和数量是否越界,90% 的情况是这两个字段填错了。
4.3 抓包验证地址是否正确的实操步骤
调试时最靠谱的办法就是把报文抓出来看。推荐用 Modbus Poll 配合一个串口抓包工具(或者直接用支持 Modbus TCP 的虚拟串口软件),这里我分享一套我常用的步骤:
- 先用 Modbus Poll 建立一个连接,协议选 RTU 或 TCP,从站地址填设备站号,功能码按手册选 03 或 04 或 01 或 02。
- 地址先填 0,数量填 1,读取一次,观察返回的数据值。如果返回的不是 0 且数值范围合理,再读相邻地址,逐渐构建区域图。
- 一旦发现某些地址读出来的值明显不合常理(比如温度读到 65535),尝试把字节序改为 CDAB、BADC 等再看。
- 记录所有地址对应的数据,跟手册比对,确认设备厂商是否存在偏移+1 或字节序反转的特殊实现。
- 最后用同样的配置去检查 PLC 或 DCS 的指令块参数,两边一致后再投入运行。整个调试过程控制在半小时内,比盲猜快得多。
5. 常见问题速查与避坑心得
5.1 地址错误导致的典型现象
工控现场,地址错位引发的现象很有迷惑性,几乎不会被当作"地址问题"处理,因为现象往往表现为数据不正常而不是通信中断。典型的有这几种:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 所有数据都读到但值不对 | 偏移地址错位(1 和 0 混用) | 检查偏移计算 |
| 有的地址能读,有的读不到 | 寄存器数量超范围 | 检查协议偏移+数量是否越界 |
| 32 位数值天文数字 | 大端小端选反 | 切换字节序 |
| 从站号冲突,时而通信时而不通 | 总线站号重复 | 抓帧看应答来源,或逐台断开排查 |
| 读浮点数读成整数 | 数据类型定义错误 | 确认寄存器数量和数据格式 |
我印象最深的一次是某项目里电表数据每隔一会儿就跳变一次,看起来像干扰,查了好久才发现其实是主站程序里把 30001 和 40001 搞混了,读取的是输入寄存器区,里面的值随负载波动才是正常的,根本不是干扰。学会区分数据区的读写属性和功能码,是避免这种误判的基础。
5.2 不同设备厂商的差异化实现
国产仪表和进口设备在 Modbus 实现上有个显著差异:进口设备(比如一些欧洲品牌仪表)通常严格遵循协议规范,地址从协议偏移 0 开始,功能码变化不多;国产仪表则更多考虑用户习惯,手册直接给逻辑编号,寄存器地址和字节序的灵活性更大,甚至同一个型号不同固件版本的地址都不一样。这就意味着:
- 拿到新设备别迷信"上一台设备是这个地址"这种经验,先翻手册。
- 用抓包工具把新设备的真实映射记录下来,再迁移到代码里。
- 如果设备支持修改从站号和波特率,先把这些参数固定下来,减小变量。
另外有些设备厂商在文档里把线圈和寄存器混叫,比如把"继电器输出"标为"线圈输出",但实际上是用寄存器写的。碰到这种文档含糊的情况,直接抓包看功能码是最快的方式,文档只能作参考。
5.3 地址规划的经验清单
最后把我在多个项目里沉淀的地址规划清单分享出来,照着做能少踩很多坑:
- 地址表用统一的 Excel 模板登记,列包括:设备名称、站号、功能码、逻辑地址、协议偏移、数据类型、字节序、读写属性、实际值换算公式。
- 程序里的 Modbus 地址参数全部以协议偏移为基准,逻辑编号只出现在注释和手册里。
- 每个站号的分配要固化,不要在项目中间改站号,改一次就要同步修改所有上位机画面和 PLC 程序。
- 对 32 位数据,严格登记字节序,并且用固定值测试确认后才接线投运。
- 现场新增设备时,先离线用 Modbus 调试工具把地址映射测一遍,再进入正式总线。
从站号、寄存器地址、功能码、数据类型到字节序,这些不是一个一个孤立的参数,而是一套环环相扣的规则。搞懂这套规则,Modbus 调试就从"玄学"变成了"照表施工"。我自己早期做项目时也在这上面栽过不少跟头,后来总结出一招最实用的:先抓包,再对表,最后才动手改程序。这个顺序能帮你省下大把现场调试时间。希望这篇小知识对正在跟 Modbus 打交道的朋友有所帮助,也算是给后来者铺个路。
最后再补充一个实用细节:调试 Modbus 时,养成保存抓包文件和地址映射表的习惯。现场问题复现往往不容易,有一份完整的报文记录和地址记录,返工成本能低一半以上。我自己踩过几次坑之后,现在的项目文件里一定会放一份"MODBUS_ADDR_MAP.xlsx",这比什么调试技巧都管用。