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字节):从站地址,范围1-247。0是广播地址,所有从站都要响应,但不回复。
  2. 功能码(1字节):告诉从站要干什么。读线圈?读寄存器?写单个?写多个?
  3. 数据域(N字节):具体参数。比如从哪个地址开始读,读多少个,或者要写入的值。
  4. 校验域(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写多个寄存器一块一块地塞进去。到时候我会详细讲怎么设计这个流程。