2、Modbus协议基础:RTU与ASCII模式、功能码详解、报文结构解析
好,咱们直接进入正题。
Modbus协议,说白了就是工业设备之间互相喊话的「黑话」。你想想看,PLC要读一个变频器的频率,或者要写一个温控仪的设定值,总得有个双方都懂的规矩吧?Modbus就是这个规矩。
我个人习惯把Modbus理解成「主从对话」——主站问,从站答。主站不发问,从站绝不主动开口。这个规矩很死,但也正是它稳定可靠的原因。
2.1 两种传输模式:RTU vs ASCII
Modbus协议在串行链路上有两种「说话方式」:RTU模式和ASCII模式。我当年刚入行时,就被这两个模式坑过一次,后面细说。
RTU模式(远程终端单元)
这是工业现场最常用的模式。数据用二进制传输,紧凑、高效。一个字节就是8位,直接发。
- 优点:同样的波特率下,RTU能传更多数据。说白了就是快。
- 缺点:报文没有明显的起始和结束标记,靠「静默时间」来区分。如果两个报文间隔太短,从站可能把它们当成一个报文来处理。
关键点:RTU模式要求报文之间至少有3.5个字符时间的静默间隔。这个时间怎么算?比如9600波特率,1个字符约1ms,3.5个字符就是3.5ms。我曾经见过一个项目,上位机发得太快,间隔只有2ms,结果从站直接乱码——嗯,这就是典型的「粘包」问题。
ASCII模式(美国标准信息交换码)
每个字节拆成两个ASCII字符发送。比如0x1A,RTU直接发1A,ASCII要发字符'1'和'A'(十六进制0x31和0x41)。
- 优点:报文以冒号':'开头,回车换行结尾,边界清晰。调试时用串口助手看,一目了然。
- 缺点:效率低,同样的数据量,ASCII报文长度是RTU的两倍。
我个人建议:除非你的设备对时间不敏感,或者你正在调试阶段需要肉眼观察报文,否则一律用RTU。我在一个老旧的水处理项目里见过用ASCII的,那速度慢得让人抓狂。
| 对比项 | RTU模式 | ASCII模式 |
|---|---|---|
| 数据编码 | 二进制,8位 | ASCII字符,16进制表示 |
| 报文长度 | 短,高效 | 长,约为RTU的2倍 |
| 帧间隔 | 3.5字符时间静默 | 冒号':'和回车换行 |
| 错误校验 | CRC-16 | LRC(纵向冗余校验) |
| 适用场景 | 高速、实时控制 | 低速、调试、人机交互 |
2.2 报文结构解析
不管RTU还是ASCII,报文结构其实差不多。咱们拿RTU来拆解,因为最常用。
一个完整的Modbus RTU报文,由四部分组成:
- 地址域(1字节):从站地址,范围1-247。0是广播地址,所有从站都要响应,但不回复。
- 功能码(1字节):告诉从站要干什么。读线圈?读寄存器?写单个?写多个?
- 数据域(N字节):具体参数。比如从哪个地址开始读,读多少个,或者要写入的值。
- 校验域(2字节):CRC-16校验,保证数据没被干扰。
举个例子,我要读地址为01的从站,从寄存器地址0x0000开始读2个寄存器:
请求报文:01 03 00 00 00 02 C4 0B
| | | | |
| | | | +-- CRC校验(低字节0C4,高字节0B)
| | | +-------- 数据长度(读2个寄存器)
| | +-------------- 起始寄存器地址(0x0000)
| +----------------- 功能码(03:读保持寄存器)
+-------------------- 从站地址(01)
从站如果正常响应,会回复:
响应报文:01 03 04 00 0A 00 14 3A 9B
| | | | |
| | | | +-- CRC校验
| | | +----------- 数据内容(寄存器1=0x000A,寄存器2=0x0014)
| | +-------------- 数据字节数(4个字节)
| +----------------- 功能码(03)
+-------------------- 从站地址(01)
小技巧:我调试时习惯先用串口助手抓一下报文,看看地址和功能码对不对。有一次我写了个上位机,怎么都读不到数据,抓包一看——地址写成了0x10,但设备地址是0x01。这种低级错误,抓包一眼就能看出来。
2.3 功能码详解
功能码是Modbus协议的核心。说白了,就是主站给从站下的「指令」。常用的功能码其实没几个,我列个表:
| 功能码 | 名称 | 作用 | 数据对象 |
|---|---|---|---|
| 0x01 | 读线圈 | 读取DO(数字量输出)状态 | 位 |
| 0x02 | 读离散输入 | 读取DI(数字量输入)状态 | 位 |
| 0x03 | 读保持寄存器 | 读取AO(模拟量输出)或参数 | 16位字 |
| 0x04 | 读输入寄存器 | 读取AI(模拟量输入)值 | 16位字 |
| 0x05 | 写单个线圈 | 控制单个DO | 位 |
| 0x06 | 写单个寄存器 | 设置单个AO或参数 | 16位字 |
| 0x0F | 写多个线圈 | 批量控制DO | 位 |
| 0x10 | 写多个寄存器 | 批量设置AO或参数 | 16位字 |
这里面,0x03和0x06是我用得最多的。为什么?因为大多数设备参数都映射到保持寄存器里。比如变频器的频率、温控仪的设定值、电表的读数,全在保持寄存器里。
避坑指南:我曾经在一个项目中,用0x03读寄存器,结果从站返回了异常码0x02(非法数据地址)。查了半天,发现设备手册里寄存器地址是从1开始的,而Modbus协议里地址是从0开始的。也就是说,手册上写「寄存器1」,实际报文里要填0x0000。这种「1-based vs 0-based」的坑,我至少踩过三次。
2.4 异常响应
从站不是永远都乖乖听话的。如果它发现报文有问题,会返回一个异常响应。异常响应的格式很简单:
异常响应:地址 + (功能码 | 0x80) + 异常码 + CRC
比如,你发0x03读寄存器,从站返回0x83,说明它拒绝了你的请求。后面的异常码告诉你原因:
| 异常码 | 含义 | 常见原因 |
|---|---|---|
| 0x01 | 非法功能码 | 从站不支持这个功能码 |
| 0x02 | 非法数据地址 | 寄存器地址超出范围 |
| 0x03 | 非法数据值 | 要写入的值超出范围 |
| 0x04 | 从站设备故障 | 硬件故障或正在忙 |
我个人习惯,在写上位机程序时,一定要把异常码解析出来打印到日志里。这样现场出问题了,不用猜,直接看日志就知道是地址错了还是值超范围了。
2.5 实战中的几个要点
最后,分享几个我在项目中积累的经验:
- CRC校验一定要做:别偷懒。工业现场干扰多,一个位翻转就可能让设备误动作。我见过有人为了省CPU资源,把CRC校验跳过了,结果设备偶尔乱动,查了三天才找到原因。
- 超时处理要合理:从站响应慢是常态。一般设500ms超时,如果从站是老旧设备,可以放宽到1秒。但别设太长,否则主站会卡死。
- 重试机制:一次通信失败,别急着报错。重试3次,每次间隔100ms。很多干扰是瞬时的,重试一次就好了。
- 广播地址0:所有从站都要接收,但不回复。适合批量设置参数,但别用来读数据——你想想看,所有从站同时回复,总线不就乱套了吗?
嗯,Modbus协议基础就讲到这里。下一章咱们聊聊固件升级时,怎么用这些功能码来传输数据。说白了,就是把固件文件拆成小块,用0x10写多个寄存器一块一块地塞进去。到时候我会详细讲怎么设计这个流程。