搞工控调试这几年,我有个特别深的感受:现场一半以上的通讯问题,最后查来查去,要么是功能码用错了,要么是地址没有对齐。而Modbus作为自动化行业里最常见的通讯协议,无论你是拿PLC去读仪表、控变频器、接温控器,还是和组态软件、上位机做对接,最终都要落到那几个功能码上。很多新手一开始就啃协议文档,啃完还是不会用;反而是那些在现场泡过几年的老师傅,不一定能把协议条文背全,但翻来覆去就用那么几个功能码,却什么问题都能搞定。这篇就把Modbus常用功能码和PLC应用这件事一次说透,适合刚入门PLC通讯的新人,也适合在现场被通讯问题反复折磨的老兵参考。
1. 先把Modbus的底子摸清
1.1 为什么工控现场永远绕不开Modbus
Modbus的江湖地位,用一句话就能概括:它是工业通讯里的“普通话”。不管是西门子、三菱、欧姆龙这类外资PLC,还是信捷、汇川这些国产品牌,也不管是变频器、温控表、流量计还是智能电表,几乎所有设备出厂时都会标配Modbus-RTU或者Modbus-TCP接口。原因很简单:这个协议公开、免费、实现成本低,而且报文结构足够简单,一个单片机就能跑起来。
从现场维护的角度看,Modbus最大的价值在于“可预测”。它只有一张数据表,主站发什么命令、从站回什么数据,格式是固定的。那怕你手上没有任何资料,用一个串口助手把报文抓出来,也能反推出设备的寄存器分布。这一点在工程调试里特别实用。很多时候设备厂家给的说明书语焉不详,最后就是靠功能码扫描加寄存器试读,把通讯“盲调”通的。
1.2 选RTU还是TCP,别拍脑袋
Modbus协议从物理层上分两大流派:串口走RTU,网口走TCP。RTU模式用485总线,两线制,半双工,波特率常见的有9600、19200、38400等,特点是走线简单、抗干扰能力强,适合几十米到几百米的现场布线。TCP模式直接用以太网,全双工,速度比RTU快几个数量级,适合数据量大、实时性要求高的场合。
选型时我的习惯是:只要能走以太网,优先考虑Modbus-TCP。理由是它省掉了串口参数配置这一堆麻烦事——波特率、校验位、停止位不对就通不上,这类坑在RTU里太常见了。但如果是老设备改造、现场只有485线,那就老老实实做RTU。需要注意的是,RTU的从站地址范围是1到247,地址0是广播地址,实际工程中很少用。而TCP模式没有从站地址的概念,取而代之的是IP加端口号,默认端口502。
提示:如果你在做方案阶段还有选择余地,别因为“以前都这么干的”就死守着485不放。用TCP省下来的调试时间,够你多测两轮设备。
2. Modbus常用功能码,逐条拆给你看
2.1 功能码01、02:读线圈和离散输入,别把“位”搞混
功能码01是读线圈状态,02是读离散输入状态。这两个都是按“位”操作的,一次读的是若干个开关量,返回的数据要按位拆解。
先看功能码01的请求报文结构。假设你有一个Modbus从站,地址为1,想从线圈起始地址0x0000开始读8个线圈,请求帧是这样的:
01 03 00 00 00 08 CRC等等,这里我要纠正一个很多人犯的错。功能码01的请求,第二个字节应该是01,不是03。写全了是这样:
01 01 00 00 00 08 CRC对照着解释一下:第一个字节01是从站地址,第二个字节01是功能码,第三第四字节00 00是起始地址,第五第六字节00 08表示读8个线圈,最后两个字节是CRC16校验。
响应帧就有意思了。从站会先回自己的地址和功能码,然后是数据字节数,接着是数据。比如从站返回8个线圈状态时,一个字节就装下了,各位的含义是:bit0对应起始线圈,bit1对应该起始线圈的下一个,以此类推。如果线圈个数不是8的整数倍,最后多余的位补0。
这里最容易踩的坑是“位顺序”。比如你读回来的字节是0x05,二进制就是00000101,这意味着起始线圈是ON,第三个线圈是ON,而不是第一个和第六个。很多人在上位机(比如组态软件、LabVIEW)里解析数据时把顺序搞反,最终画面上开关状态全是反的,而且这种问题极难发现,因为看起来通讯又是正常的。
功能码02和01的区别就一个:01读的是线圈,线圈是可读可写的开关量,对应PLC里的Q区、M区、中间继电器等;02读的是离散输入,是只读的开关量输入,对应PLC里的I区、数字量输入模块。在工程语义上,02读的是外部按钮、限位开关、传感器反馈这类信号,本身只允许读,不允许写。
2.2 功能码03、04:读寄存器,绝大多数数据交换都靠它们
接下来是Modbus最核心的两个功能码:03读保持寄存器,04读输入寄存器。PLC和仪表、变频器之间的数据交换,比如读温度、读转速、读电流,或者下发设定值,基本都离不开03和04。
功能码03和04的报文结构几乎一致。比如从站地址1,读保持寄存器起始地址0x0000,读2个寄存器:
01 03 00 00 00 02 CRC从站正常回包是:
01 03 04 数据1高字节 数据1低字节 数据2高字节 数据2低字节 CRC第三个字节04表示后面跟着4个字节数据,也就是两个16位寄存器。
功能码03和04的本质区别在于寄存器类型。保持寄存器(03)是可读可写的,你要给设备下参数、写给定值,必须用它;输入寄存器(04)是只读的,用来存设备实时测量值,比如温度采集模块的通道值、电能表的电压电流,通常只能用04去读。
但在实际工程里,设备厂家对寄存器类型的定义并不完全统一。我见过有的仪表把测量值放到保持寄存器里,也见过有的PLC从站把输入寄存器映射到了模拟量输入通道。所以选功能码之前,一定要确认设备的手册里是怎么定义的。很多通讯调不通,不是因为协议不会写,而是你把03当成04用,或者反过来,设备根本不响应。
2.3 功能码05、06、15、16:写操作,PLC控制输出的关键
写完再讲写。功能码05是写单个线圈,06是写单个寄存器,15是写多个线圈,16是写多个寄存器。这四个是PLC作为主站去控制从站设备时最常用的功能码。
功能码05写单线圈,报文里有个非常经典的细节:线圈状态用0xFF00表示ON,0x0000表示OFF。注意,ON不是0x0001,而是0xFF00。如果按习惯写0x0001,有些从站设备会直接返回非法数据值错误(异常码03)。报文长这样:
01 05 00 00 FF 00 CRC这串的意思是:给地址1的从站,从线圈0x0000开始,写一个线圈,状态为ON。
功能码06写单寄存器,报文类似:
01 06 00 00 00 01 CRC表示给从站1的保持寄存器0x0000写入值1。这个常用于单点设置,比如修改变频器的运行频率、修改仪表的报警值。
功能码15和16是批量写。当你要一次下发一组参数,或者同时控制多路输出时,用批量写能大幅度减少通讯次数。批量写寄存器的报文结构比单点写复杂一点,因为要带上字节数:
01 10 00 00 00 02 04 00 01 00 02 CRC第一个字节01是从站地址,第二个字节10是功能码16的十六进制表示,00 00是起始地址,00 02是寄存器个数,04是数据字节数(2个寄存器就是4个字节),后面四个字节是两个寄存器的值。
写操作最需要注意的是“一次写多少”。Modbus RTU的单帧数据通常限制在256字节以内,实际项目中我建议一次写寄存器的个数不要超过120个,线圈不要超过960个。超过这个量,有些从站会直接拒绝响应,有些则会悄悄丢数据,很难排查。
2.4 不常用但能救场的功能码
除了上面八个功能码,还有几个平时用得少、但关键时刻能救场的。
功能码07是读异常状态,目前支持它的设备不多,可以忽略。功能码08是诊断,最常用的是子功能码00(回环测试)——发什么就回什么,用来测试链路是否通畅。排查通讯问题时我经常先发一个08 00 00 00 00 00,如果从站能原样返回,说明物理链路和从站通讯模块都是好的,问题大概率出在功能码或地址上。
功能码17是读从站ID,可以拿到设备的厂家代码、型号、版本号等信息。有些杂牌设备说明书写得不清不楚,设备地址也可能和铭牌不一致,这时用17功能码探一下,能省不少事。不过同样要面对“设备支持不支持”的问题,不支持的话就只能老老实实拨码或看软件配置了。
下面这张表建议收藏,调试时对照着用:
| 功能码 | 中文名 | 作用对象 | 典型使用场景 |
|---|---|---|---|
| 0x01 | 读线圈 | 线圈(可读写位) | 读取PLC Q/M区输出状态 |
| 0x02 | 读离散输入 | 离散输入(只读位) | 读取PLC I区输入、传感器反馈 |
| 0x03 | 读保持寄存器 | 保持寄存器(可读写16位) | 读写设备参数、设定值 |
| 0x04 | 读输入寄存器 | 输入寄存器(只读16位) | 读模拟量测量值、电参量 |
| 0x05 | 写单个线圈 | 线圈 | 控制单路DO输出 |
| 0x06 | 写单个寄存器 | 保持寄存器 | 设置单个参数 |
| 0x0F | 写多个线圈 | 线圈 | 批量控制DO输出 |
| 0x10 | 写多个寄存器 | 保持寄存器 | 批量下发参数或配方 |
| 0x08 | 诊断(回环) | 通讯链路 | 排查物理链路和从站状态 |
| 0x11 | 读从站ID | 从站信息 | 识别设备型号与版本 |
3. PLC地址映射与数据处理才是真正翻车高发区
3.1 PLC地址到Modbus地址怎么换算
很多人把功能码报文写对了,也用调试软件测通了,但一接上PLC程序就出问题。出问题的地方十有八九是地址映射。PLC里的寄存器地址和Modbus协议里的寄存器地址不是一回事,中间需要换算。
拿最常见的保持寄存器举例子。Modbus协议里的寄存器地址从0x0000开始,而在设备手册里,你看到的往往是40001、40002这种OC地址。这里的4是功能码区标识,40001对应的就是协议地址0x0000,40002对应0x0001,以此类推。同理,30001对应输入寄存器的0x0000,00001对应线圈的0x0000,10001对应离散输入的0x0000。
为什么会出现这种差异?因为Modbus发明的时候,用的是“数据区加偏移量”的编址方式,4xxxx表示保持寄存器区,后面是OC序号;而协议报文里为了节省空间,用的是从0开始的十六进制偏移地址。两者在工程上必须做转换。转换公式很简单:
协议地址 = PLC/设备地址 - 数据区起始编号 - 1举几个常用的例子:
- 设备手册说“运行频率在地址40005”,那么协议地址就是40005 - 40001 = 4,十六进制就是0x0004。
- 设备手册说“当前温度在地址30010”,那么协议地址就是30010 - 30001 = 9,十六进制0x0009。
- 设备手册说“启动按钮对应线圈00008”,那么协议地址就是00008 - 00001 = 7,十六进制0x0007。
这些看似简单的加减,真到了现场写程序时,特别容易在“到底减不减1”上犯迷糊。我自己的习惯是:不管从手册里读到什么地址,第一件事先换算成协议地址写进程序注释,再对照报文抓包确认,宁可多花两分钟验算,也别凭感觉发。
3.2 字节序、字序与浮点数转换,95%的工程师都在这翻过车
地址对了,数据也不一定对;数据对上了,数值还有可能读出来是个天文数字。这就涉及Modbus数据在字节层面的排列问题。
Modbus协议规定,16位寄存器在报文里是高位字节在前、低位字节在后。比如寄存器值是0x1234,报文里先发0x12,再发0x34。这种叫大端模式,绝大多数设备都遵守。但32位数据(比如浮点数)就麻烦了,因为32位数据占两个寄存器,两个寄存器之间的先后顺序,不同厂家并没有统一。
常见的有两种顺序:
- 大端字序(CDAB):先发高16位寄存器,再发低16位寄存器。比如浮点数123.45的32位十六进制是0x42F6E666,寄存器1放0x42F6,寄存器2放0x6666。
- 小端字序(BADC):先发低16位寄存器,再发高16位寄存器。同样一个数,寄存器1放0x6666,寄存器2放0x42F6。
这就是为什么同一块仪表,你按说明书地址读出来了1.623e-34这种数据,而厂家工程师一口咬定设备没问题。数据本身是正常的,只是字序解析反了。解决这个问题有两种路径:一是部分PLC的Modbus库指令支持“字节交换字序交换”的配置项,直接在配置里调;二是上位机(组态、LabVIEW、.NET程序)里把两个寄存器Swap一下再转浮点数。
我自己写数据处理时,固定用一套方法:先用Modbus调试软件读一段已知的、能确认数值的寄存器(比如把变频器频率设定为50.00Hz),强行分析它的字节排列,确定设备到底是哪种字序,然后再写PLC程序。不要看说明书想当然,说明书里很多厂家根本不会写清楚字序细节。
顺带说一句IEEE 754浮点数转换。如果你拿到两个16位寄存器想合成一个32位浮点数,在C语言里可以直接用指针强转,在PLC里通常用两个字拼接指令或者字交换指令处理。上面说的字序决定你拼接时谁前谁后,只处理字节序不处理字序,结果一样不对。
3.3 不同品牌PLC的Modbus指令怎么用
不同品牌的PLC对Modbus的支持方式差别很大,这里挑几个常见的说。
西门子S7-200 SMART走Modbus,官方库里有MBUS_CTRL、MBUS_MSG这些指令,其中MBUS_MSG可以同时支持读写。S7-1200/1500则用MB_COMM_LOAD加载通讯参数,再用MB_CLIENT(作为主站)或MB_SERVER(作为从站)处理请求。用西门子的库函数时,特别是S7-200,要注意V区地址的分配和保持寄存器映射关系,比如MBUS_MSG指令里的DataPtr参数往往要求指向V区。
三菱FX系列比较特殊,走内置485口时通常用ADPRW指令直接构造Modbus请求,也可以用RS指令自己拼报文,然后通过CRC校验程序处理。FX5U和Q系列对Modbus-TCP的支持就更完善,可以配置成服务器或客户端。
国产PLC里,信捷、汇川这些品牌基本都内置了Modbus主从站配置,不需要自己拼CRC。以汇川为例,调用MODBUS通讯功能块,填从站地址、功能码、起始地址、数量、数据缓存区就行,功能码选好了基本不用操心报文细节。但这里有个通病:越方便的功能块,越容易让人忽视地址和寄存器类型的对应,建议配置完先用调试软件验证一轮再下载到现场。
老规矩,不管什么品牌,我都要强调:PLC程序里的人机界面地址,最好设计成“配置表”的形式,把从站地址、寄存器地址、数据类型、采样周期全部列在表格里,集中管理。否则项目后期甲方往上位机里加一堆点位,你光查地址就能查到怀疑人生。
4. 调试与故障排查实战记录
4.1 先用调试软件把协议跑通,再动PLC程序
我的调试顺序从来都是:电脑装Modbus调试软件,先把设备测通,再写PLC程序和上位机。用Modbus Poll这类的工具,可以稳定地发任意功能码、任意地址的请求,并直观看到返回的数据。如果你是做从站(比如把PLC配成Modbus从站给上位机读),可以用Modbus Slave来模拟设备侧返回数据,先验证上位机和你的通讯参数对不对。
调试软件还有一个好用的功能:持续循环发报文。我在测试一批温控仪表时,就是把某个寄存器设成周期1秒地读,然后拿万用表测仪表实际输出,对比数值是否一致。数据对不上,先怀疑字序和系数;数据完全读不到,先怀疑地址和功能码;连请求都发不出去,先怀疑串口参数和接口转换器。
注意:Modbus Poll、Modbus Slave这类调试工具在工控圈很普及,但正版授权是收费的。工程上建议买授权,个人学习可以用官方提供的试用模式来熟悉协议流程,别去网上找破解。调试工具本身不复杂,但被带伤工具坑掉一个下午的调试时间,太不划算。
4.2 现场连不上的排查顺序,按下述步骤来
现场通讯故障五花八门,但核心规律就那么几条。遇到“主站和从站分别测都正常,一接在一起就不通”的情况,绝大多数不是协议的问题,而是物理层和参数层的问题。我的排查顺序如下,建议直接抄作业:
第一步,看接线。485通讯是A和B两根线,但不同厂家对A/B的标法不统一,有的标D+、D-,有的标485+、485-,先确认A对A、B对B。两个设备间的A/B接反,是“单独测都行,连着不通”最常见的元凶。
第二步,查终端电阻。485总线两端要各接一个120欧姆终端电阻。短距离、两台设备的临时测试可以不管,但现场几十米以上、节点多的时候不接终端电阻,通讯大概率会出现偶发超时或者乱码。
第三步,核对通讯参数。波特率、数据位、校验位、停止位四项必须完全一致。这里特别强调校验位,很多国产设备默认无校验(8N1),但仪表出厂可能是偶校验(8E1),两边不一致时会出现“能收到数据但CRC永远不对”的怪现象。
第四步,核对从站地址。主机发出的请求里带从站地址,如果从站设备实际地址是2,你发的却是1,从站是不会回应的。现场多台设备组网时,别忘了检查重复地址——两台设备都设成1,总线会异常混乱。
第五步,检查地电位。485本质上是差分信号,理论上可以不共地,但现场强电干扰严重、距离又长时,建议在主机侧把485的GND和设备的信号地连接起来,避免共模电压过高导致通讯时好时坏。
这五步走完,绝大多数通讯都Wall能通。如果五步都没问题,再用功能码08回环测试去判断是不是从站通讯模块本身的问题。
4.3 几个典型踩坑案例,都是真金白银换来的
案例一:读温度变成-1000度。某项目用PLC读一个温度采集模块,按说明书功能码04读取,数据能读到,但数值明显不对,显示类似65530这种数据。排查发现模块的测量值实际应该用功能码03读,说明书里“只读”说的是寄存器属性,但厂家实现时把值放在保持寄存器里了。最后改成03,数据正常。教训是:功能码的“读”和“写”属性看厂家实现,不要光看协议标准定义。
案例二:批量写参数只成功一半。现场要给一块带128路参数的仪表下发预设值,我用功能码16一次写了100个寄存器,仪表只更新了前60个。后来发现仪表内部处理单帧数据的上限就是64个寄存器,超出后整帧丢弃,只是仪表响应帧会带异常码,但PLC程序没做异常码判断,就一直显示“通讯成功”。从那以后,我写批量写逻辑时固定按每帧50个寄存器切分,并在PLC程序里做响应异常码判断,宁可多拆几帧,绝不赌设备处理能力。
案例三:PLC做从站,上位机读M区数据总差一位。信捷PLC里有%MB2000这种M区字节地址,上位机工程师用Modbus读保持寄存器时,总是发现地址对不上。原因是PLC的M区地址映射到Modbus寄存器时,有个起始偏移规则,通常PLC的MB/ MW地址会对应到寄存器地址加一个固定偏移。这类问题最有效的办法是:在PLC里给一个已知的M区地址写个固定值,然后在上位机用调试工具去扫,确定对应关系,不要靠猜。
4.4 排查问题时的通病速查表
| 故障现象 | 优先排查项 | 次要排查项 |
|---|---|---|
| 完全无响应 | 通讯参数不一致、从站地址错误 | A/B接反、接口转换器故障 |
| 响应时好时坏 | 终端电阻缺失、线路过长 | 共模干扰、地线问题 |
| 能通讯但数据为0/65535 | 功能码选错、寄存器地址错 | 数据字节序处理错误 |
| 数据数值异常巨大或极小 | 字序、字节序不匹配 | 浮点数转换方法错误 |
| PLC程序发请求但上位机收不到 | 从站配置未生效 | 地址映射表未对齐 |
5. 我的一点个人心得
做Modbus调试这些年,我自己的习惯是:不管现场多着急,先把主站、从站的通讯参数表、地址映射表整理好,然后用电脑端的调试软件在实验室把协议层跑通,再写进PLC程序里。通讯这种东西,绝大多数故障都不是协议本身高深没搞懂,而是参数、地址、字节序这类细节没有对齐。遇到问题不要慌,从头到尾按“物理层、参数层、数据层、应用层”的顺序捋一遍,八成的坑都能自己填上。
最后再分享一个小技巧:无论你用的是哪家PLC,都建议在程序里把从站返回的异常码给解析出来,状态字里留两个字节专门报通讯状态。这样现场设备一旦通讯异常,上位机画面上能直接看到“非法功能码”“非法数据地址”“从站忙”这类具体原因,而不是只看到一个干巴巴的“通讯故障”。对甲方来说,这能省掉无数来回沟通的时间;对你自己来说,这是半夜被电话叫起来时最值钱的东西。