3. Modbus协议帧结构:RTU与ASCII帧格式详解
好,咱们今天来聊聊Modbus协议里最基础、也最核心的东西——帧结构。说白了,就是数据在线上是怎么包装、怎么传输的。我刚开始接触Modbus时,总觉得帧结构就是一堆十六进制数,没什么好看的。直到有一次在现场调试,设备死活连不上,折腾了半天才发现是CRC算错了……嗯,从那以后我再也不敢小看帧结构了。
3.1 为什么要有帧结构?
你想想看,通信就像两个人打电话。你说一句,对方得知道哪句是开始、哪句是结束、中间说的是什么内容。Modbus也一样。它规定了:
- 谁先说话(主站发请求)
- 找谁说话(地址码)
- 说什么事(功能码)
- 具体内容(数据区)
- 说得对不对(校验码)
这就是一帧完整的报文。我个人习惯把帧结构想象成一个快递包裹:地址码是收件人,功能码是快递单上的服务类型,数据区是包裹里的东西,CRC校验就是封箱胶带——少了它,包裹可能就散架了。
3.2 RTU帧格式
RTU(Remote Terminal Unit)模式是Modbus最常用的传输模式。它的特点是紧凑、高效。每个字节用8位二进制表示,传输效率高,适合高速通信。
3.2.1 RTU帧结构组成
| 字段 | 长度 | 说明 |
|---|---|---|
| 地址码 | 1字节 | 从站地址,范围1-247 |
| 功能码 | 1字节 | 操作类型,如03读保持寄存器 |
| 数据区 | N字节 | 具体数据内容,长度可变 |
| CRC校验 | 2字节 | 循环冗余校验,低字节在前 |
这里有个细节要注意:RTU帧没有起始位和停止位。它靠总线空闲时间来判断帧的开始和结束。具体来说,如果总线上超过3.5个字符时间没有数据传输,就认为上一帧结束了,下一帧开始了。
关键点:RTU帧的字节之间不能有超过1.5个字符时间的间隔,否则从站会认为帧不完整,直接丢弃。
3.2.2 地址码
地址码占1个字节,取值范围是1到247。0是广播地址,所有从站都会接收,但不会回复。248到255是保留地址,一般不用。
我在项目中遇到过一个问题:有个客户把从站地址设成了0,结果主站发什么它都收,但就是不回数据。排查了半天才发现地址码设错了。所以我的建议是:地址码从1开始分配,别用0。
3.2.3 功能码
功能码告诉从站要做什么。常用的有:
- 01:读线圈状态
- 02:读离散输入
- 03:读保持寄存器
- 04:读输入寄存器
- 05:写单个线圈
- 06:写单个寄存器
- 15:写多个线圈
- 16:写多个寄存器
功能码如果最高位被置1(比如0x83),表示发生了异常。这时候数据区会包含一个异常码,告诉你哪里出错了。
3.2.4 数据区
数据区是帧里最灵活的部分。它包含:
- 起始地址:要操作的寄存器或线圈的起始编号
- 数量:要读或写的数量
- 数据内容:写操作时,要写入的具体值
举个例子,读保持寄存器的请求帧:
从站地址:01
功能码:03
起始地址高字节:00
起始地址低字节:00
寄存器数量高字节:00
寄存器数量低字节:0A
CRC低字节:C4
CRC高字节:0B
这条报文的意思是:让地址为01的从站,从地址0000开始,连续读10个保持寄存器。
3.2.5 CRC校验
CRC校验是RTU模式的重头戏。它用16位循环冗余校验算法,计算从地址码到数据区最后一个字节的所有数据。计算结果是2个字节,低字节在前,高字节在后。
我的经验:CRC计算最容易出错的地方就是字节顺序。很多初学者会把高低字节搞反。我曾经在调试一个灌溉控制器时,CRC算对了但顺序反了,结果从站一直回复异常码。后来用逻辑分析仪抓波形才发现问题。
CRC的计算步骤大致是:
- 初始化CRC寄存器为0xFFFF
- 对每个字节,与CRC寄存器的低8位异或
- 右移1位,如果移出的位是1,则与0xA001异或
- 重复8次,处理完一个字节
- 所有字节处理完后,取反得到最终CRC值
3.3 ASCII帧格式
ASCII模式是另一种传输方式。它把每个字节拆成两个ASCII字符来发送。比如0x3A,在ASCII模式下会发送字符'3'和'A'(即0x33和0x41)。
说实话,现在用ASCII模式的项目越来越少了。它传输效率低——同样的数据,ASCII模式比RTU模式多一倍的字节数。但它的优点是肉眼可读,调试方便。
3.3.1 ASCII帧结构
| 字段 | 长度 | 说明 |
|---|---|---|
| 起始符 | 1字节 | 固定为':' (0x3A) |
| 地址码 | 2字节 | ASCII字符表示 |
| 功能码 | 2字节 | ASCII字符表示 |
| 数据区 | N×2字节 | 每个字节转成2个ASCII字符 |
| LRC校验 | 2字节 | 纵向冗余校验,ASCII字符表示 |
| 结束符 | 2字节 | 回车换行 (0x0D 0x0A) |
ASCII模式用LRC校验代替了CRC校验。LRC的计算很简单:把所有字节(不包括起始符和结束符)相加,取补码。
注意:ASCII模式下,帧与帧之间可以有任意长度的空闲时间。它不像RTU那样依赖时间间隔来判断帧边界,而是靠起始符':'和结束符CR/LF来界定。
3.4 报文示例解析
光说不练假把式。咱们来看几个实际报文。
3.4.1 RTU模式:读保持寄存器
请求帧:
01 03 00 00 00 0A C4 0B
解析:
- 01:从站地址1
- 03:读保持寄存器
- 00 00:起始地址0
- 00 0A:读10个寄存器
- C4 0B:CRC校验
响应帧:
01 03 14 00 01 00 02 00 03 00 04 00 05 00 06 00 07 00 08 00 09 00 0A 84 3F
解析:
- 01:从站地址1
- 03:功能码
- 14:数据字节数(20个字节,即10个寄存器×2字节)
- 00 01 ... 00 0A:10个寄存器的值
- 84 3F:CRC校验
3.4.2 ASCII模式:读保持寄存器
请求帧:
: 01 03 00 00 00 0A F6 CR LF
注意,这里显示的是实际发送的ASCII字符。在线上传输的是:
3A 30 31 30 33 30 30 30 30 30 30 30 41 46 36 0D 0A
解析:
- 3A:起始符':'
- 30 31:地址码'01'
- 30 33:功能码'03'
- 30 30 30 30:起始地址'0000'
- 30 30 30 41:数量'000A'
- 46 36:LRC校验'F6'
- 0D 0A:结束符CR LF
3.5 RTU与ASCII的选择建议
我个人在实际项目中,90%以上都用RTU模式。原因很简单:
- 效率高:同样数据量,RTU比ASCII少一半的字节
- 速度快:适合高速通信场景
- 校验强:CRC比LRC更可靠
但ASCII模式也有它的用武之地:
- 调试阶段:用ASCII模式可以肉眼看到报文内容
- 低速链路:如果通信速率很低,ASCII模式更容易处理
- 兼容性:有些老设备只支持ASCII模式
我的建议:新项目直接用RTU模式。如果遇到调试困难,可以临时切换到ASCII模式看看报文内容,调通了再改回RTU。我在做灌溉控制器时就是这么干的——先用ASCII模式确认通信逻辑没问题,再切回RTU模式正式运行。
3.6 常见问题与避坑指南
最后,分享几个我踩过的坑:
- CRC计算错误:我曾经在STM32上实现CRC时,忘了对结果取反,导致校验一直不过。后来查了标准文档才发现问题。
- 字节顺序搞反:RTU模式下,多字节数据都是高字节在前。但有些设备厂商会搞反,遇到这种情况只能看设备手册。
- 帧间隔时间:RTU模式要求帧间间隔至少3.5个字符时间。如果主站发送太快,从站可能来不及处理,导致丢帧。
- ASCII模式大小写:ASCII模式的十六进制字符,大写和小写都可以。但为了统一,我建议全部用大写。
嗯,帧结构这部分就讲到这里。下一章咱们会深入讲解功能码的具体用法,包括读线圈、写寄存器这些实际操作。到时候我会结合灌溉控制器的实际案例,带大家一步步调试。