ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Modbus功能码详解:从01到23的报文结构与调试避坑指南

Modbus功能码详解:从01到23的报文结构与调试避坑指南

1. 搞懂Modbus功能码,先弄明白它到底在解决什么问题

干了十来年自动化,现场调试最怕遇到什么?不是PLC程序写得有多复杂,也不是上位机界面有多花哨,而是通讯不通。尤其是Modbus这种在工控圈里遍地都是的协议,几乎每个搞电气、搞自控的人都绕不开它。你可以不懂OPC UA,也可以不碰Profinet,但只要你在现场摸过仪表、变频器、电表、温控器,Modbus就一定会在某个时刻出现在你面前。

Modbus功能码,说白了就是主站告诉从站“我要干什么”的那串代码。就像你去饭店点菜,服务员问你“吃什么”,你说“来个宫保鸡丁”,这个“宫保鸡丁”就是功能码。主站发一帧报文,里面有个字节专门用来标识操作类型——是读线圈、读寄存器,还是写单个寄存器、写多个寄存器。从站收到之后,根据这个字节去执行对应动作,然后把结果返回。

很多人第一次接触Modbus,觉得它简单得不像话,不就是发几个字节收几个字节吗?但真到了现场,问题就来了:为什么读回来的数据是乱的?为什么写不进去?为什么有时候通有时候不通?这些问题的根源,十有八九都跟功能码的理解不到位有关。比如你拿读保持寄存器的功能码去读线圈,从站要么不响应,要么返回异常码,你还以为是接线问题,查了半天线路,最后发现是功能码用错了。

这篇文章我打算把Modbus功能码从头到尾捋一遍,不光是列个表告诉你01是读线圈、03是读保持寄存器,而是把每个功能码的使用场景、报文结构、常见坑点都讲清楚。适合刚入行的电气小白,也适合干了几年但一直没系统整理过的老手。你看完之后,至少能做到:拿到一个Modbus设备,知道该用哪个功能码去读写,报文发出去之后能看懂返回的数据对不对,出了问题知道往哪个方向排查。

2. Modbus功能码的整体分类与设计逻辑

2.1 为什么功能码要分这么多类

Modbus协议最早是Modicon公司搞出来的,用来在PLC和现场设备之间传数据。当时的设计思路很朴素:把设备里能读写的东西分成几类,每类给一个编号,主站想操作哪类就发对应的编号。这个思路一直沿用到现在,成了工控通讯的“普通话”。

功能码分四大类:位操作、寄存器操作、文件记录操作、诊断和其他。位操作就是读写开关量,一个位代表一个状态,比如线圈通断、输入点高低电平。寄存器操作就是读写16位数据,比如温度值、频率设定、电量参数。文件记录操作用得少,一般用于批量读写历史数据。诊断类功能码主要用来查通讯质量、读设备状态。

为什么这么分?因为现场设备的数据类型就这几种。开关量是布尔值,寄存器是数值,分开处理效率最高。你想想,如果一个功能码既能读位又能读寄存器,那报文解析就乱了,从站也不知道该返回什么格式。所以Modbus的设计者从一开始就把数据类型和功能码绑定,简单粗暴但有效。

2.2 常用功能码速查表

先上一张表,把最常用的功能码列出来,后面再逐个拆解。

功能码名称操作对象数据类型典型用途
01读线圈线圈位读继电器状态、阀门开关
02读离散输入离散输入位读限位开关、按钮状态
03读保持寄存器保持寄存器16位读温度、频率、电量
04读输入寄存器输入寄存器16位读传感器原始值
05写单个线圈线圈位控制继电器通断
06写单个寄存器保持寄存器16位设定频率、温度阈值
15写多个线圈线圈位批量控制开关量
16写多个寄存器保持寄存器16位批量下发参数
23读写多个寄存器保持寄存器16位一次完成读和写

这张表建议截图存手机里,现场调试的时候随时能翻出来看。尤其是01、03、05、06这四个,日常工作中出现频率最高,先把这四个吃透,基本能应付八成以上的场景。

2.3 功能码的报文结构长什么样

Modbus RTU模式下,一帧报文的基本结构是:从站地址 + 功能码 + 数据 + CRC校验。从站地址占1字节,功能码占1字节,数据长度根据功能码不同而变化,CRC占2字节。

以读保持寄存器为例,主站发送的报文是:从站地址 + 03 + 起始地址高字节 + 起始地址低字节 + 寄存器数量高字节 + 寄存器数量低字节 + CRC低字节 + CRC高字节。从站返回的报文是:从站地址 + 03 + 字节数 + 数据高字节 + 数据低字节 + ... + CRC。

这里有个细节要注意:寄存器地址在报文里是从0开始算的,但很多设备手册上写的是从1开始。比如手册上写“温度值在40001寄存器”,你发报文的时候起始地址要填0,不是填40001。这个坑我踩过不止一次,刚开始调的时候死活读不到数据,后来才发现是地址偏移的问题。一般来说,保持寄存器的地址范围是40001到49999,对应报文里的0到9998;输入寄存器是30001到39999,对应报文里的0到9998;线圈是00001到09999,对应报文里的0到9998;离散输入是10001到19999,对应报文里的0到9998。

注意:不同厂家对地址的定义可能不一样,有的直接用偏移量,有的用绝对地址。拿到设备第一件事就是翻通讯手册,确认地址映射关系,别凭经验瞎猜。

3. 常用功能码逐个拆解与实操要点

3.1 功能码01和02:读开关量

功能码01用来读线圈,也就是可读写的开关量。线圈在设备里通常对应继电器输出、电磁阀控制、指示灯状态这些。功能码02用来读离散输入,也就是只读的开关量,比如限位开关、按钮、接近开关。

这两个功能码的报文格式一模一样,区别只在于操作的对象不同。主站发送:从站地址 + 功能码 + 起始地址高 + 起始地址低 + 数量高 + 数量低 + CRC。从站返回:从站地址 + 功能码 + 字节数 + 数据字节1 + 数据字节2 + ... + CRC。

举个例子,你要读从站地址为1的设备,从线圈地址0开始的8个线圈状态。发送报文:01 01 00 00 00 08 CRC。假设返回:01 01 01 55 CRC。这里的55是十六进制,转成二进制是01010101,表示第0、2、4、6个线圈为ON,第1、3、5、7个线圈为OFF。注意,返回数据是从低位到高位对应起始地址的,第一个线圈对应字节的最低位。

实操中容易遇到的问题:读回来的字节数和预期不符。比如你读8个线圈,返回1个字节;读9个线圈,返回2个字节。因为Modbus是按字节打包的,不够8位也要占一个字节。所以计算字节数的公式是:(数量 + 7) / 8,取整数部分。

实操心得:读线圈的时候,如果只需要读一两个点,建议单独读,不要批量读一大片。因为批量读的时候,如果某个地址不存在,从站可能整个报文都不返回,你连哪个点有问题都不知道。分开读虽然多几帧报文,但排查问题方便得多。

3.2 功能码03和04:读寄存器

功能码03读保持寄存器,这是Modbus里用得最多的功能码,没有之一。保持寄存器是可读写的16位寄存器,用来存各种参数:温度设定值、频率给定、PID参数、电量数据等等。功能码04读输入寄存器,通常是只读的,用来存传感器原始采集值。

报文格式:主站发送从站地址 + 03 + 起始地址高 + 起始地址低 + 数量高 + 数量低 + CRC。从站返回从站地址 + 03 + 字节数 + 数据高 + 数据低 + ... + CRC。字节数等于寄存器数量乘以2,因为每个寄存器占2个字节。

这里重点讲一下数据解析。假设你读到一个寄存器的值是0x0064,转成十进制是100。但这个100代表什么?是100度还是10.0度还是1.00度?这取决于设备手册里定义的缩放因子。有的设备温度值直接存整数,单位是0.1度,那100就是10.0度;有的存的是实际值,100就是100度。还有的设备用两个寄存器拼成一个32位浮点数,这时候就要把两个16位寄存器合并再解析。

我见过一个现场,电表读回来的电压值是0x0898,转十进制是2200。一开始以为是2200V,吓了一跳,后来查手册才知道缩放因子是0.1,实际电压是220.0V。所以读寄存器一定要先确认数据格式:是整数还是浮点?有没有缩放?字节序是大端还是小端?

注意:32位数据在Modbus里跨两个寄存器,字节序有ABCD、CDAB、BADC、DCBA四种可能。不同厂家用的不一样,遇到浮点数读出来是乱码,先换字节序试试。

3.3 功能码05和06:写单个量

功能码05写单个线圈,用来控制一个开关量的通断。发送报文:从站地址 + 05 + 地址高 + 地址低 + FF00 + CRC表示置位,0000表示复位。注意这里的数据域是固定的两个值:FF00和0000,其他值都是非法的。从站收到之后,如果执行成功,会把原报文原样返回。

功能码06写单个寄存器,用来设定一个参数值。发送报文:从站地址 + 06 + 地址高 + 地址低 + 数据高 + 数据低 + CRC。从站执行成功后也是原样返回。

这两个功能码看起来简单,但实际用的时候有几个坑。第一个坑是写进去没反应。很多时候是因为设备处于某种保护状态,比如运行中不允许修改参数,或者需要先写使能位。第二个坑是写进去的值被限制了。比如你写频率给定5000(代表50.00Hz),但设备最大频率只有4000,那它可能自动截断到4000,返回的报文还是5000,但实际值变了。第三个坑是掉电不保存。很多设备的保持寄存器是RAM的,断电就丢,需要发特定的保存命令或者写EEPROM。

实操心得:写寄存器之前,先读一遍当前值,确认通讯正常、地址正确,然后再写。写完再读一遍,确认写入生效。这个“读-写-读”的流程看起来麻烦,但能避免很多低级错误。

3.4 功能码15和16:批量写

功能码15写多个线圈,功能码16写多个寄存器。这两个功能码的存在是为了提高效率。比如你要设定10个参数,用06要发10帧报文,用16一帧就够了。

功能码16的报文格式:从站地址 + 10 + 起始地址高 + 起始地址低 + 寄存器数量高 + 寄存器数量低 + 字节数 + 数据1高 + 数据1低 + ... + CRC。从站返回:从站地址 + 10 + 起始地址高 + 起始地址低 + 寄存器数量高 + 寄存器数量低 + CRC。

这里要注意字节数的计算:寄存器数量乘以2。比如写10个寄存器,字节数是20,十六进制是0x14。数据域按顺序排列,第一个寄存器的数据在前。

批量写的时候,如果其中一个寄存器地址非法或者值超范围,从站可能整个报文都拒绝执行,也可能部分执行。这取决于从站的实现。所以批量写之后,最好逐个读回来确认。

3.5 功能码23:读写一把梭

功能码23是读写多个寄存器,这个功能码在需要同时读写的场景下很有用。比如你要调整PID参数,同时想读回当前的实际值,用23就可以一帧报文完成。发送格式:从站地址 + 17 + 读起始地址高 + 读起始地址低 + 读数量高 + 读数量低 + 写起始地址高 + 写起始地址低 + 写数量高 + 写数量低 + 写字节数 + 写数据 + CRC。

这个功能码不是所有设备都支持,用之前要确认从站是否实现了23功能码。如果不支持,从站会返回异常码01(非法功能)。

4. 异常码与常见故障排查

4.1 从站返回异常码是什么意思

当主站发送的功能码从站不支持,或者地址、数据有问题时,从站会返回一个异常响应。异常响应的格式是:从站地址 + 功能码(最高位置1)+ 异常码 + CRC。比如你发03,从站返回83,83就是03的最高位置1,表示这是异常响应。

常见的异常码有:

异常码名称含义排查方向
01非法功能从站不支持该功能码确认设备手册支持的功能码列表
02非法数据地址地址超出范围或不存在检查地址映射,确认寄存器范围
03非法数据值数据超出允许范围检查写入值是否在设备允许区间内
04从站设备故障从站执行时出错检查设备状态,可能需要复位
05确认从站已接收,正在处理等待后重试
06从站设备忙从站暂时无法处理增加延时后重试

看到异常码不要慌,先看是哪个码。01和02最常见,基本都是地址或功能码用错了。03一般是写入值超范围。04和06是设备本身的问题,可能需要断电重启或者检查设备故障。

4.2 通讯不通的排查思路

通讯不通是现场最头疼的问题,我一般按这个顺序排查:

第一步,检查物理层。接线对不对?A接A、B接B,别接反了。终端电阻有没有?长距离通讯两端要加120欧姆终端电阻。屏蔽线接地了没有?屏蔽层单端接地,不要两端都接,否则会形成地环路。

第二步,检查通讯参数。波特率、数据位、停止位、校验位,主从必须完全一致。最常见的是波特率不对,比如主站9600,从站19200,那肯定通不了。校验位也容易忽略,有的设备默认无校验,有的默认偶校验。

第三步,检查从站地址。主站发的地址和从站实际地址要匹配。有的设备地址是通过拨码开关设置的,有的通过参数设置。如果总线上有多个从站,地址不能重复。

第四步,用工具抓包。如果以上都没问题,就用串口调试助手或者Modbus调试工具抓一下报文,看看主站发出去的是什么,从站有没有返回。如果主站发了但从站没返回,说明从站没收到或者收到了但不认识。如果从站返回了但主站不认,可能是CRC校验错了或者格式不对。

实操心得:现场调试的时候,我习惯先用一个已知能通的设备测试,确认主站参数和接线没问题,再接入目标设备。这样可以快速定位问题是在主站侧还是在从站侧。

4.3 数据读回来不对怎么查

数据读回来不对,分几种情况。第一种是数值完全不对,比如读温度读回来65535。这通常是地址错了,读到了空地址或者保留地址。第二种是数值差一个倍数,比如实际220V读回来2200。这是缩放因子的问题,查手册确认。第三种是数值跳变,一会儿对一会儿不对。这可能是通讯干扰,检查屏蔽和接地,或者降低波特率试试。第四种是32位数据解析错乱,这是字节序的问题,换一种字节序组合试试。

我一般会准备一个Excel表格,把设备手册里的寄存器地址、数据类型、缩放因子、字节序都整理进去,调试的时候对着表格逐个核对。这个习惯帮我省了很多时间,尤其是设备多、参数杂的项目。

5. 实际项目中的功能码应用案例

5.1 案例一:读电表数据

某项目要读一个三相电表的电压、电流、功率。电表手册上写:电压在40001-40003,电流在40004-40006,功率在40007-40009,都是32位浮点数,字节序是CDAB。

我的操作步骤:第一步,确认电表的从站地址和通讯参数。第二步,用03功能码读40001开始的9个寄存器。第三步,把返回的18个字节按CDAB的顺序解析成3个浮点数。第四步,和电表面板显示的值对比,确认解析正确。

这里的关键是字节序。CDAB的意思是:第一个寄存器的高字节是C,低字节是D;第二个寄存器的高字节是A,低字节是B。实际浮点数的字节顺序是ABCD。所以解析的时候要把两个寄存器的字节重新排列。

5.2 案例二:控制变频器启停和频率

某项目要控制一台变频器的启停和频率给定。变频器手册上写:控制字在40100,频率给定在40101,都是16位整数,频率缩放因子是0.01。

操作步骤:第一步,用06功能码写40100,值设为1表示启动。第二步,用06功能码写40101,值设为5000表示50.00Hz。第三步,用03功能码读40102(运行状态)和40103(实际频率),确认变频器已启动并且频率正确。

这里要注意的是,有些变频器的控制字不是简单写1就启动,可能需要按位操作。比如bit0是启动,bit1是方向,bit2是故障复位。这时候就要先读当前控制字,修改对应位,再写回去。直接写1可能会把其他位清零,导致意外停机。

5.3 案例三:批量读取温控器参数

某项目有8台温控器,要读取每台的当前温度、设定温度、报警状态。每台温控器的寄存器地址相同,但从站地址不同。

操作步骤:第一步,写一个循环,从站地址从1到8。第二步,对每个从站用03功能码读起始地址0开始的3个寄存器。第三步,解析数据,当前温度和设定温度是16位整数,缩放因子0.1;报警状态是位数据,需要单独用01功能码读。

这个案例的难点在于轮询速度。8台设备如果每台都读,一轮下来可能要几百毫秒。如果对实时性要求高,可以只读关键数据,或者提高波特率。但提高波特率会降低通讯距离,需要权衡。

6. 调试工具与实用技巧

6.1 常用调试工具推荐

调试Modbus,手里没几个工具是不行的。我常用的有:

串口调试助手,用来抓原始报文,看主站发了什么、从站回了什么。Modbus Poll和Modbus Slave,一个模拟主站一个模拟从站,可以快速验证通讯参数和功能码。还有各种PLC的编程软件自带的Modbus调试功能,比如某些品牌的PLC可以直接在线监控Modbus通讯状态。

如果是以太网Modbus TCP,Wireshark是必备的,可以抓包分析每一帧的细节。不过Wireshark用起来门槛稍高,需要懂TCP/IP和Modbus TCP的报文结构。

6.2 提高调试效率的几个习惯

第一个习惯:先离线测试再上线。用Modbus Slave模拟一个从站,把地址、数据都配好,主站先和模拟从站通讯,确认报文格式和解析逻辑没问题,再接到真实设备上。这样可以排除主站侧的问题。

第二个习惯:做好地址映射表。每个项目的设备寄存器地址都整理成表格,标注功能码、数据类型、缩放因子、读写权限。调试的时候对着表格操作,不容易出错。

第三个习惯:保存通讯日志。调试通了之后,把正常的收发报文保存下来,以后出问题可以对比。我一般会存一个“正常通讯报文.txt”,里面记录每个操作的发送和返回报文。

第四个习惯:分段排查。通讯不通的时候,不要一上来就怀疑设备坏了。先查物理层,再查参数,再查地址,最后查设备。按这个顺序,大部分问题都能快速定位。

6.3 常见问题速查表

现象可能原因解决方法
从站无响应接线错误、参数不匹配、地址错误检查A/B线、波特率校验位、从站地址
返回异常码01功能码不支持查手册确认支持的功能码
返回异常码02地址非法检查地址映射和偏移量
返回异常码03数据超范围检查写入值是否在允许区间
数据乱码字节序错误、缩放因子错误调整字节序、确认缩放因子
通讯时通时断干扰、终端电阻、线缆质量加终端电阻、检查屏蔽接地、换线
写入不生效设备保护、需要使能、掉电丢失查手册确认写条件、发保存命令

最后分享一个小技巧:如果现场没有调试工具,可以用PLC的在线监控功能,强制读写Modbus地址,观察从站反应。虽然不如专业工具方便,但应急够用了。

这个内容后续还可以这样扩展:Modbus TCP和RTU的报文差异、Modbus ASCII模式的特点、多主站冲突的处理、以及如何用脚本批量测试Modbus设备。每个方向都够写一篇独立的文章,后面有机会再慢慢聊。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进