1. 一个让我记忆深刻的现场:地址明明填对了,数据却全是错的
几年前我第一次独立调试一台变频器,从站手册上写得清清楚楚:频率设定值地址是40001,频率反馈地址是40003。我兴冲冲地打开Modbus Poll,在寄存器地址栏填了40001,功能码选03,结果弹了个让我懵掉的东西——响应超时,错误码9003。我又试了40003,还是9003。把波特率、校验位、从站号全查了一遍,没问题。折腾了半个下午,最后问了老工程师一句,他瞟了一眼说:"你填0试试。"我填了个0,数据哗啦一下就出来了。
从那以后我才真正意识到,Modbus地址规则这件事,看起来是工控入门第一课,实际上是最容易被低估的坑。协议本身不复杂,但不同厂商、不同软件、不同文档对"地址"的表述方式五花八门,同一块寄存器能给你写出三种完全不同的编号。这篇文章我就把这些规则掰开揉碎讲一遍,把协议层面的地址、PLC风格的地址、软件界面上填的地址之间的关系彻底理清楚。刚入行的电气工程师、自控工程师,以及做上位机集成的朋友,应该都能从这里省下不少排查通信故障的时间。
2. 先把四种数据对象搞清楚:线圈、离散输入、输入寄存器、保持寄存器
Modbus协议把设备内部的数据划分成四种对象,地址规则的所有混乱,根源都在这个分类上。很多人一上来就背"40001是保持寄存器",但不知道40001这个数是怎么来的,也不知道它和协议地址0是什么关系。所以先从数据对象本身说起。
| 对象名称 | 英文 | 读写属性 | 位/字 | 协议地址范围 | PLC风格地址 |
|---|---|---|---|---|---|
| 线圈 | Coil | 可读写 | 位 | 0x0000~0xFFFF | 00001~09999 |
| 离散输入 | Discrete Input | 只读 | 位 | 0x0000~0xFFFF | 10001~19999 |
| 输入寄存器 | Input Register | 只读 | 16位 | 0x0000~0xFFFF | 30001~39999 |
| 保持寄存器 | Holding Register | 可读写 | 16位 | 0x0000~0xFFFF | 40001~49999 |
这四种对象用最直白的话理解:
- 线圈就是设备里的DO,说白了就是一个可读可写的开关量。你既能读它当前是通还是断,也能通过写操作让它通或断。变频器里的"运行指令""故障复位"通常就是线圈。
- 离散输入是设备里的DI,只读的开关量。它反映的是外部接进来的干接点信号,比如门禁状态、限位开关、急停回路,你只能读,不能写。
- 输入寄存器是只读的16位数据区,一般对应模拟量输入,比如温度、压力、电流经过AD转换后的数值。像仪表采集到的实时温度,往往放在输入寄存器里。
- 保持寄存器是应用最广的一类,可读可写的16位数据区。设备的参数设定值、运行状态字、故障代码、PID给定值等,基本都在保持寄存器里。你在工控项目里碰到的大部分通信需求,都是在跟保持寄存器打交道。
这四种对象的存在,解释了功能码为什么需要分开设计:01、02、03、04分别是读线圈、读离散输入、读保持寄存器、读输入寄存器;05、06分别写单线圈、写单寄存器;15、16批量写线圈和寄存器。功能码本身就是在告诉从站"我要操作哪种对象",而地址才是告诉它"操作对象里的哪一位/哪一字"。
注意:协议规范里,这四类对象的地址空间都是独立的0x0000~0xFFFF,也就是每一类最多65536个。同一个数字"0",在保持寄存器里和在线圈里,指向的是完全不同的两个数据点。所以不要觉得"地址0"就是一个固定的东西,它必须和功能码搭配起来看才有意义。
3. 同一块寄存器,地址有三种写法:协议地址、PLC风格地址、软件显示地址
四种对象分清楚之后,来到今天全文最核心的部分:为什么一个寄存器会有好几种地址写法,它们之间怎么换算。我见过太多人把0写成40000、把19写成40019,其实问题都出在没理解"编号起点"和"类型前缀"这两件事。
3.1 协议报文里根本没有40001这个数字
Modbus的报文,比如RTU模式读保持寄存器,数据域里携带的起始地址是一个16位的数值。你抓包看,地址0就是0x0000,地址19就是0x0013,地址65535就是0xFFFF。协议本身并不认识40001、30001这种东西,它只知道0到65535的纯数字。
也就是说,协议层面的地址是从0开始编号的。第一个保持寄存器协议地址是0,第二个是1,以此类推。这是换算的基准,所有花里胡哨的表示法最后都要回归到这个0起点上来。
3.2 PLC风格的4xxxx、3xxxx又是怎么来的
这个要追溯到PLC和组态软件发展的早期。为了让工程师不用背十六进制、不用去数偏移,各个厂商发明了一套"带前缀的十进制地址":前缀数字表示数据类型,后四位表示从1开始的序号。
- 4代表保持寄存器,4开头后面跟位数,所以40001就是"保持寄存器第1个",40002是第2个。
- 3代表输入寄存器,30001是"输入寄存器第1个"。
- 1代表离散输入,10001是"离散输入第1个"。
- 0代表线圈,00001是"线圈第1个"。
注意这里的关键:**PLC地址的编号从1开始,而协议地址从0开始。**所以换算规则其实就一条:
PLC风格地址 = 类型前缀 + 协议地址 + 1
举例:
| 数据对象 | 协议地址 | PLC风格地址 |
|---|---|---|
| 保持寄存器 | 0 | 40001 |
| 保持寄存器 | 1 | 40002 |
| 保持寄存器 | 18 | 40019 |
| 保持寄存器 | 65535 | 465536(超5位,很多软件不显示) |
| 输入寄存器 | 2 | 30003 |
| 线圈 | 5 | 00006 |
| 离散输入 | 7 | 10008 |
这就是为什么手册里写"频率设定值40001",实际上协议地址是0;手册里写"运行状态字40003",协议地址是2。如果你在Modbus Poll这类软件里直接填40001,软件会把这个数字当成协议地址去换算,通常会得到错误的结果。
Modbus Poll这类上位机调试软件一般做得比较聪明,它提供了两种显示模式:一种你填原始协议地址(有些版本直接填0开始的地址),一种你可以切换到底层地址模式,软件帮你换算成3xxxx/4xxxx显示。所以在工具里填地址之前,先看清楚当前界面用的是哪种模式。
提示:从站设备的手册如果写"地址0~65535",它用的是协议地址;如果写"40001~465535",它用的是PLC风格地址。这两种说法在切换时千万不要忘了加1或者减1,这是无数通信故障的根源。
3.3 超过9999时5位表示法会露馅
PLC风格的5位地址有一个先天缺陷:后四位最多到9999。如果一个保持寄存器设备的协议地址超过9998,对应PLC地址就超过49999,5位表示法直接崩了。所以新的智能仪表、网关、嵌入式从站设备,手册上越来越倾向于直接写协议地址(比如0x0000~0xFFFF,或者十进制0~65535),而不是4xxxx。
这也是很多老工程师会觉得"新设备不讲武德"的原因。其实不是设备不讲武德,是协议地址本来就是0起点的,4xxxx反而是历史遗留包装。在项目图纸和技术协议里,如果甲方用的是老式PLC风格,乙方用的是协议地址,双方对点位表时经常会出现"+1"之争。我建议做点位表时同时标注两种写法,一列写协议地址,一列写4xxxx,这样谁看都不会错。
4. 功能码决定读还是写,地址决定读写哪里
搞清楚地址怎么写之后,再说说地址和功能码的关系。前面提到四种对象都有独立的地址空间,那么功能码就是访问空间的"钥匙"。同一个数字地址,配上不同的功能码,指向的可能完全是不同物理点。
4.1 功能码速查表
常见的功能码就这么几个,建议直接背下来:
| 功能码 | 作用 | 操作对象 |
|---|---|---|
| 01 | 读线圈 | 线圈 |
| 02 | 读离散输入 | 离散输入 |
| 03 | 读保持寄存器 | 保持寄存器 |
| 04 | 读输入寄存器 | 输入寄存器 |
| 05 | 写单个线圈 | 线圈 |
| 06 | 写单个寄存器 | 保持寄存器 |
| 15 | 写多个线圈 | 线圈 |
| 16 | 写多个寄存器 | 保持寄存器 |
场景化理解一下:假设某个温控表把"当前温度"放在输入寄存器地址0,把"温度设定值"放在保持寄存器地址0。你读温度要用功能码04加地址0,读设定值要用功能码03加地址0。功能码不同,即使地址数字相同,拿到的也是完全不同的数据。
我在现场调试仪表时碰到过一种情况:从站手册上的寄存器表里,有些设备把参数放在保持寄存器区,有些放在输入寄存器区,整整两页表看得眼花。这时候最忌讳的就是看到"地址100"就直接用03去读——你得先确认这个地址属于哪一类数据对象。**如果功能码和对象不匹配,从站会回异常码02(非法数据地址)或者干脆不响应。**这比地址写错更隐蔽,因为你要是不了解四种对象的区别,可能根本没想到是功能码的问题。
4.2 从站地址和数据地址是两个维度
和寄存器地址一起容易混淆的,还有从站地址(也叫站号、Unit ID)。一条Modbus总线上可以挂多个从站设备,每个从站有自己唯一的站号。报文里站号的作用就是告诉总线"我这条命令是发给谁"的。寄存器地址则是在这个站内部找数据位置。
举个例子:一条RS485总线上挂了3台温控表,站号分别是1、2、3。你要读2号表的温度,报文里的站号就是0x02,寄存器地址是温度所在的协议地址。如果你站号填了1,即使寄存器地址完全正确,读到的也可能是1号表的温度,或者干脆超时。
错误码9003很多场合就是这么来的。在Modbus Poll里看到9003,大多数情况下意味着通信这一层有问题,而不是数据地址有毛病。排查的顺序我建议是这样:
- 物理链路:串口有没有选对、线有没有接反、波特率校验位和从站是否一致。
- 站号:从站地址填的是不是目标设备。总线有多台设备时,这一步特别容易错。
- 寄存器地址和功能码:确认对象类型和起点。
- 超时时间:从站响应慢,而软件超时设得太短,也会报9003。
很多人一看到9003就猛抠寄存器地址,这是不对的。地址不对通常从站会回异常码02,或者返回一堆乱数据;根本不回包、报超时,问题大多数在链路、站号和参数配置上。
5. 地址规划里不显眼的三个陷阱:数量、类型、字节序
就算你地址规则滚瓜烂熟,现场调试还是会遇到一种非常气人的情况:地址填对了、功能码对了、站号对了,读出来的数值却是天文数字,或者一个正常的整数变成了负数。这时候要往更深一层想——寄存器地址只是第一步,你还需要搞清楚读多少个寄存器、每个寄存器里装的是什么类型的数据、多字节数据在寄存器里是怎么排列的。
5.1 寄存器数量:填地址别忘了长度
读保持寄存器的功能码03,请求报文里不光有起始地址,还有寄存器数量。比如起始地址0,数量2,表示从0号开始连续读2个寄存器(即0号和1号)。Modbus Poll这类工具里一般都会有一个"Quantity"或者"长度"输入框,很多人只盯着地址框,把这个忽略了。
这个概念在批量读取时特别有用。如果设备的运行参数是一段连续的寄存器表,比如"当前温度"在地址0、"目标温度"在地址1、"运行状态"在地址2,你可以一次性发起一个起始地址0、数量3的读请求,一次把三个数据全拿回来。这样比一条一条读快很多,总线负载也小很多。**在实际项目里,我习惯把连续的点位整合成批量读取,比如每200ms读一次10个寄存器,而不是一个周期内读10个单点请求。**总线利用率能明显改善,从站的压力也小。
但批量读有一个前提:寄存器必须是连续的。如果从站的寄存器表中间有保留位、空洞、或者某些地址不支持,你一次性跨过去读,从站可能会回异常码02或者返回错误数据。这时候就得人为把地址拆成几段,把空洞单独绕过去。
5.2 32位数据和四字序那个大坑
Modbus寄存器是16位一格的,这是协议本身的单位。可是现场的数据不全是16位整数,还有很多32位浮点数、32位有符号整数、甚至是64位的数据。遇到这种数据,一个数就要占用两个寄存器。比如一个32位浮点数,占据保持寄存器地址0和地址1;下一个浮点数从地址2开始。
数据本身跨了两个寄存器之后,**字节序和字序怎么排,Modbus协议没有规定统一的顺序。**这就非常要命。同样是地址0~1里的两个寄存器,不同厂家的设备可能给出四种完全不同的组合:
| 顺序 | 高16位来源 | 低16位来源 | 常见称呼 |
|---|---|---|---|
| 模式1 | 寄存器0 | 寄存器1 | ABCD(大端) |
| 模式2 | 寄存器1 | 寄存器0 | CDAB(字交换或中端) |
| 模式3 | 寄存器0高低字节交换 | 寄存器1高低字节交换 | BADC(字节交换) |
| 模式4 | 寄存器1高低字节交换 | 寄存器0高低字节交换 | DCBA(小端) |
怎么理解?打个比方,把数据想成一句话:"AB CD EF GH"。大端模式按顺序存放,看着就是原来的顺序;小端模式要把整句话倒过来,变成"GH EF CD AB";还有一些设备会在中间层面交换字或字节,就产生了另外两种组合。你配置错了字节序,读出来的浮点数往往是一个巨大而离谱的数,比如1.7e38、0.0000x这种,一眼看起来就不对。
我在项目里调试进口仪表的时候,就吃过字节序的亏。明明面板上显示温度25.5度,上位机读出来却是3.8e-35,我一度怀疑是寄存器地址抄错了。后来把设备说明书的"数据格式"一节翻出来仔细看,才发现它用的是小端模式,而组态软件里默认是大端。在软件的数据格式设置里改了一下,数值立刻正常。
**验证方法其实很简单:先把设备面板上显示的一个已知数值记下来,然后用上位机读,读完之后依次尝试四种顺序,看哪种对得上。**不需要猜,试一次就知道。
5.3 数据类型决定你怎么解释16位格子
寄存器数量确定了,还有个基础但容易被新手忽略的问题:同一段寄存器,拿到手之后要用什么数据类型来解释。
| 数据类型 | 占用寄存器数 | 典型值域 | 用途 |
|---|---|---|---|
| uint16 | 1 | 0~65535 | 状态字、计数器 |
| int16 | 1 | -32768~32767 | 温度、压力值 |
| uint32 | 2 | 0~4294967295 | 累计流量、电度 |
| int32 | 2 | -2147483648~2147483647 | 位置、流量累积 |
| float32 | 2 | ±3.4e38 | 浮点测量值 |
比如一个16位寄存器里存的是有符号整数,你却按无符号整数去解析,本来应该是-5,结果读出来成了65531;本来是温度负值,却被当成很大的正数。这种问题在老的PLC系统和仪表之间特别常见。只要在现场看到"数值大得离谱但地址确定没问题",优先检查数据类型和字节序。
还有一个容易忽视的点,**有些设备的32位数据要求起始地址是偶数位对齐,也就是从0、2、4这种地址开始。**如果你从奇数地址去读一个32位数据,有的从站会拒绝响应。分组态软件里配置时必须对齐,否则报错或者读出来是乱的。
6. 我在调试Modbus地址时的三个习惯
地址规则的理论说到底就那么几条,真正值钱的是在现场怎么快速定位问题。这些年我调试串口设备、网关、PLC和仪表,逐渐养成了几个习惯,分享出来供参考。
6.1 第一件事永远是对"基址起点"
不管手册上写的是4xxxx还是协议地址,我拿到一台新设备的第一件事,就是找它的最前面几个寄存器,然后读一个已知值来验证起点。举个例子:手册上写明"设备型号存于保持寄存器地址0,值为0x0103",那我就读地址0,看读回来是不是0x0103。如果读出来的是别的值,或者读出来的是地址1的内容,说明我对“起点”的理解和手册有偏差,那就去做加1减1的换算。
这种方法比凭空猜测可靠得多。因为很多设备手册可能是从别的型号抄过来的,也可能翻译时把"0x0000"和"1"搞混了。不要相信手册里的序号,要以实际抓到的报文和返回值为准。
6.2 用Modbus Poll的两种显示模式交叉验证
Modbus Poll里可以在地址显示上做模式切换。当你拿到一个寄存器地址不确定它应该填多少时,把两种模式都试一遍。比如我想访问保持寄存器的协议地址19,软件如果处于4x显示模式,它可能显示成40020;如果处于hex模式,直接显示0x0013。看到两种模式下的对应关系,就能立刻判断自己有没有加错偏移。
另外,我调试时习惯同时开着串口监视或者抓包工具。看到请求报文里实际发出的起始地址,就能知道软件到底把你填的数字原样发出去,还是又做了一次转换。这招在排除"软件自动加偏移"问题时特别好用。
一个标准的Modbus RTU读保持寄存器请求报文长这样:
01 03 00 13 00 02 75 89拆开看:01是从站号,03是功能码,0013是协议起始地址(十进制19),0002是寄存器数量,7589是CRC校验。如果你填了40020而报文里变成0x0013,说明软件帮你减了1;如果报文里直接是0x0014,说明你填什么它就发什么。这两种情况,现场调试思路会完全不同。
6.3 先读后写,用固定值验证映射关系
凡是涉及写操作(比如写设定值、写控制字),我绝对不会一开始就写真实工艺参数。我的习惯是先写一个绝对安全的测试值,比如设定值写0或者写一个明确的整数,然后读回来确认数值完全一致,再切到真实值。这样做是为了排除地址映射错误导致的"写错地方"风险。
写错地址在工控现场是非常危险的事情。假设你把频率设定值写到了命令字的位上面,可能设备直接动作,造成安全事故。所以凡是写操作,必做回读验证;凡是批量写,先单点测。这不是胆小,是现场的基本修养。
写到这里,我发现调试Modbus的很多坑其实跟协议复杂度没有关系。协议本身很简单,难的是不同设备厂商对地址、字节序、数据类型这些边界规则的"自由发挥"。理解了本文这些规则之后,你再看任何设备的Modbus寄存器手册,就不会被4xxxx、0x0000、9003这些东西吓住了。地址换算、功能码选择、数据类型解析这三板斧掌握住,现场通信问题解决起来会顺手很多。