4. Modbus ASCII 帧结构:起始符、地址、功能码、LRC校验、结束符
好,咱们接着聊。上一节我们把 RTU 模式扒了个底朝天,这一节轮到 ASCII 模式了。
说实话,我刚开始接触工业通信那会儿,总觉得 ASCII 模式有点「笨重」。同样的数据,RTU 发几个字节就搞定,ASCII 得翻一倍。但后来在项目里吃过亏,才明白它存在的意义——可读性。
你想想看,调试的时候,拿着串口助手,满屏的十六进制 01 03 00 00 00 01 84 0A,你一眼能看出对不对?但如果是 ASCII 帧,直接就是字符串,人眼一扫就知道「哦,地址是 01,功能码是 03」。这就是 ASCII 模式最大的价值。
核心区别一句话:ASCII 模式把每个字节拆成两个 ASCII 字符来传输,帧结构多了起始符和结束符,校验也从 CRC 换成了 LRC。
4.1 帧结构总览
先看一张完整的帧结构图,我亲手画的,你感受一下。
嗯,这张图基本把 ASCII 帧的骨架画清楚了。下面我们逐个拆解。
4.2 起始符:冒号 ':'
ASCII 帧的起始符是固定的,就是冒号 ':',十六进制是 0x3A。
为什么用冒号?我个人猜测,早期设计者就是想找一个在 ASCII 表里不太会被误用的字符。你想想,如果起始符用 'A',那数据里出现 'A' 怎么办?不就乱套了?冒号在正常数据里几乎不会出现,所以用它做起始符很安全。
调试小技巧:用串口助手抓 ASCII 报文时,直接搜冒号就能定位到一帧的开始。我经常用这个办法快速过滤掉乱码。
4.3 地址字段
地址字段占 2 个 ASCII 字符,对应 1 个字节的十六进制值。取值范围是 00 到 F7(0 到 247)。
举个例子:
- 如果从站地址是 1,那么地址字段就是
"01"(字符 '0' 和 '1') - 如果从站地址是 16,就是
"10"(字符 '1' 和 '0') - 广播地址是
"00",所有从站都会响应
这里有个坑,我提醒一下:ASCII 模式下,地址 0 也是有效的广播地址,但有些国产设备对广播的处理不太规范。我曾经遇到过一个温控器,发广播地址 00 给它,它居然不响应,后来发现它只认地址 01。所以用广播功能前,最好先确认设备手册。
4.4 功能码
功能码也是 2 个 ASCII 字符,对应 1 个字节。常用的就那么几个:
| 功能码(十六进制) | ASCII 表示 | 含义 |
|---|---|---|
| 0x01 | "01" | 读线圈 |
| 0x02 | "02" | 读离散输入 |
| 0x03 | "03" | 读保持寄存器 |
| 0x04 | "04" | 读输入寄存器 |
| 0x05 | "05" | 写单个线圈 |
| 0x06 | "06" | 写单个寄存器 |
| 0x0F | "0F" | 写多个线圈 |
| 0x10 | "10" | 写多个寄存器 |
注意,功能码在 ASCII 帧里是大写字母。比如 0x0F 要写成 "0F",不能写成 "0f"。虽然有些设备不区分大小写,但按规范来,一律大写。
4.5 数据区
数据区是变长的,长度取决于具体功能。每个字节拆成两个 ASCII 字符。
比如读保持寄存器 0x0001 开始的 2 个寄存器:
- 起始地址:0x0000 → "0000"
- 寄存器数量:0x0002 → "0002"
- 数据区就是 "00000002"
整帧看起来就是::010300000002??CRLF(?? 是 LRC 校验值)
4.6 LRC 校验:核心中的核心
LRC(Longitudinal Redundancy Check,纵向冗余校验)是 ASCII 模式专用的校验方式。它比 RTU 的 CRC 简单得多,但可靠性也差一些。
LRC 的计算方法:
- 从地址字段开始到数据区结束,把所有字节累加
- 取累加和的补码(256 - 累加和 & 0xFF)
- 结果就是 LRC 值
说白了,就是:LRC = (0x100 - sum) & 0xFF
我直接给你一个 C 语言实现,这是我项目里一直在用的,经过无数次验证:
// 计算 LRC 校验值
// 输入:从地址开始的原始字节数组(不是 ASCII 字符串)
// 长度:字节数
// 返回:LRC 校验值(1 个字节)
unsigned char calcLRC(unsigned char *data, int len)
{
unsigned char sum = 0;
for (int i = 0; i < len; i++)
{
sum += data[i];
}
return (unsigned char)((0x100 - sum) & 0xFF);
}
举个例子,计算帧 :010300000001 的 LRC:
- 原始字节:0x01, 0x03, 0x00, 0x00, 0x00, 0x01
- 累加和:0x01 + 0x03 + 0x00 + 0x00 + 0x00 + 0x01 = 0x05
- LRC = 0x100 - 0x05 = 0xFB
- ASCII 表示:"FB"
所以完整帧就是::010300000001FB<CR><LF>
注意:LRC 计算时,不包括起始符 ':' 和结束符 CR/LF。只计算地址、功能码、数据区这三个部分。我见过有人把冒号也算进去,结果校验永远对不上。
4.7 结束符:CR + LF
ASCII 帧的结束符是两个字符:回车(CR,0x0D)和换行(LF,0x0A)。
为什么用两个?这其实是继承了早期电传打字机的传统。CR 让打印头回到行首,LF 让纸张向上走一行。在 Modbus ASCII 里,这两个字符组合起来告诉接收端:「这一帧结束了,可以开始解析了」。
我建议你在写解析程序时,一定要同时检查 CR 和 LF。有些设备只发 CR 不发 LF,或者反过来,这都会导致解析失败。我曾经调试一个进口的流量计,它发的结束符是 LF 在前 CR 在后,跟标准反的。折腾了我一上午才找到原因。
4.8 完整帧示例
最后,给你一个完整的例子,把上面所有知识点串起来。
场景:主机读取从站地址 1 的保持寄存器,从地址 0x0000 开始,读 2 个寄存器。
请求帧:
: 0 1 0 3 0 0 0 0 0 0 0 2 F B CR LF
| | | | | | | |
| | | | | | | +-- LF (0x0A)
| | | | | | +------ CR (0x0D)
| | | | | +---------- LRC = "FB"
| | | | +-------------- 数据区 = "00000002"
| | | +-------------------------- 功能码 = "03" (读保持寄存器)
| | +------------------------------ 地址 = "01"
| +---------------------------------- 起始符 ':'
+------------------------------------ 帧开始
响应帧(假设寄存器值分别为 0x1234 和 0x5678):
: 0 1 0 3 0 4 1 2 3 4 5 6 7 8 7 0 CR LF
| | | | | | | |
| | | | | | | +-- LF
| | | | | | +------ CR
| | | | | +---------- LRC = "70"
| | | | +-------------------------- 数据区 = "12345678" (4 个字节)
| | | +------------------------------ 字节数 = "04"
| | +---------------------------------- 功能码 = "03"
| +-------------------------------------- 地址 = "01"
+---------------------------------------- 起始符 ':'
总结一下:ASCII 帧结构虽然看起来比 RTU 啰嗦,但它最大的优势就是可读性强。调试阶段用 ASCII 模式,可以省去很多手动换算的麻烦。等调试通过,再切回 RTU 模式提高效率。这是很多老工程师的常规操作,我也是这么干的。
公众号:蓝海资料掘金营,微信deep3321