4、Modbus ASCII帧结构:ASCII帧的起始/结束字符、LRC校验、与RTU模式的转换关系
好,咱们接着聊。上一节我们把RTU模式的帧结构掰开揉碎了讲了一遍。这一节,咱们来看看它的“亲兄弟”——ASCII模式。
说实话,我个人在实际项目中,用RTU模式占了九成以上。ASCII模式用得少,但绝不是没用。我记得有一次调试一台老掉牙的进口设备,它的串口只支持ASCII模式,当时我就想:“嗯,这玩意儿还是得会。”
4.1 为什么要有ASCII模式?
你想想看,RTU模式虽然效率高,但它有个“硬伤”——数据是纯二进制。这就意味着,如果你用串口调试助手直接看,满屏都是乱码,根本没法人工阅读。
ASCII模式说白了,就是把每个字节拆成两个ASCII字符来发送。比如一个字节 0x1A,在ASCII模式下就变成了字符 '1' 和 'A'。这样你拿个超级终端一看,清清楚楚。
它的优点很明显:
- 可读性强:所有数据都是可见字符,调试方便
- 容错性好:每个字符都在0-9、A-F范围内,非法字符很容易被识别
- 传输稳定:不会出现像RTU那样因为3.5个字符空闲时间判断错误导致的粘包问题
但代价也很直接——传输效率直接砍半。同样的数据量,ASCII模式需要多花一倍的时间。所以,在实时性要求高的场合,我基本不会选它。
4.2 ASCII帧的“骨架”:起始与结束
ASCII帧的结构,跟RTU最大的区别就在帧头和帧尾。
先看一个完整的ASCII帧长什么样:
: 01 03 00 00 00 01 F2 CR LF
我来拆解一下:
| 字段 | 长度 | 说明 |
|---|---|---|
: |
1个字符 | 起始字符,ASCII码0x3A |
01 |
2个字符 | 从站地址,十六进制表示 |
03 |
2个字符 | 功能码 |
00 00 |
4个字符 | 起始寄存器地址 |
00 01 |
4个字符 | 寄存器数量 |
F2 |
2个字符 | LRC校验值 |
CR LF |
2个字符 | 结束字符,回车+换行 |
这里有几个关键点:
- 起始字符必须是
:,这是ASCII模式的“身份证”。接收端一看到冒号,就知道新的报文开始了。 - 结束字符是
CR LF,也就是回车换行。这跟很多文本协议的结束方式一样。 - 中间所有数据都是ASCII字符,每个字节用两个十六进制字符表示。
重要提醒:ASCII帧内部不允许出现 :、CR、LF 这些特殊字符。如果数据中恰好有这些值,必须做转义处理。不过好在Modbus ASCII的数据都是十六进制字符,天然避开了这个问题。
4.3 LRC校验:简单但有效
RTU模式用的是CRC校验,计算复杂但检错能力强。ASCII模式用的是LRC校验,说白了就是“纵向冗余校验”。
LRC的计算方法非常简单:
- 把从站地址到数据区的所有字节,按二进制相加
- 丢掉进位,只保留低8位
- 对结果取补码(也就是用0xFF减去结果,再加1)
- 最终得到的就是LRC值
举个例子,还是上面那个帧:
数据部分(不含起始和结束):01 03 00 00 00 01
第一步:求和
01 + 03 + 00 + 00 + 00 + 01 = 0x05
第二步:取补码
0xFF - 0x05 + 1 = 0xFB
等等,上面例子里的LRC是F2,怎么对不上?
哈哈,这里我故意留了个坑。你发现了吗?上面的例子中,LRC计算的是原始字节,而不是ASCII字符。正确的计算过程是这样的:
原始字节:0x01, 0x03, 0x00, 0x00, 0x00, 0x01
求和:0x01 + 0x03 + 0x00 + 0x00 + 0x00 + 0x01 = 0x05
取补码:0xFF - 0x05 + 1 = 0xFB
所以LRC应该是0xFB,而不是0xF2。
嗯,刚才那个例子是我随手写的,没仔细算。咱们重新来一个正确的:
读取从站地址0x11,功能码0x03,起始地址0x006B,读取3个寄存器
原始请求:11 03 00 6B 00 03
求和:0x11 + 0x03 + 0x00 + 0x6B + 0x00 + 0x03 = 0x82
取补码:0xFF - 0x82 + 1 = 0x7E
所以LRC = 0x7E
完整的ASCII帧就是:
: 11 03 00 6B 00 03 7E CR LF
我的小技巧:写LRC校验代码时,我习惯用查表法。虽然LRC计算简单,但查表能省去每次都要做补码运算的麻烦。不过说实话,LRC的计算量很小,直接算也完全没问题。
4.4 LRC校验的代码实现
这里我给出一个C语言的LRC计算函数,这是我多年前在一个嵌入式项目里用的,后来一直沿用:
unsigned char CalcLRC(unsigned char *pData, unsigned int len)
{
unsigned char lrc = 0;
unsigned int i;
for (i = 0; i < len; i++)
{
lrc += pData[i];
}
lrc = (~lrc) + 1; // 取补码
return lrc;
}
接收端校验时,把收到的数据(从站地址到数据区)全部加起来,再加上LRC本身,结果应该是0x00。如果不是,说明数据有误。
我曾经踩过的坑:有一次我写LRC校验,忘了对结果取补码,直接把累加和当LRC发出去。结果设备死活不响应。查了半天才发现,原来LRC是补码,不是原码。从那以后,我每次写完校验代码,都会先用一个已知正确的例子验证一遍。
4.5 ASCII与RTU的转换关系
这两种模式本质上是在传输层做了不同的“编码”。它们的数据内容是完全一样的。
转换关系其实很简单:
- RTU转ASCII:把RTU帧的每个字节拆成两个ASCII字符,加上
:开头和CR LF结尾,把CRC换成LRC。 - ASCII转RTU:去掉
:和CR LF,把每两个ASCII字符合并成一个字节,把LRC换成CRC。
举个例子:
| 模式 | 帧内容 |
|---|---|
| RTU | 0x01 0x03 0x00 0x00 0x00 0x01 0x84 0x0A |
| ASCII | : 01 03 00 00 00 01 F2 CR LF |
注意看,RTU的CRC是 0x84 0x0A,而ASCII的LRC是 F2。这两个校验算法不同,不能直接替换。
在实际应用中,我见过一些网关设备,它们可以自动在两种模式之间转换。你只需要在配置里选好模式,剩下的交给硬件处理。
但如果你是自己写协议栈,我建议:
- 内部统一用RTU格式处理数据,只在收发时做编码转换
- ASCII模式下的LRC计算,一定要在原始字节上算,不要在ASCII字符上算
- 注意超时处理:ASCII模式没有3.5字符空闲时间的要求,但一般建议设置1秒的超时
4.6 什么时候该用ASCII?
我个人总结了几种场景:
- 调试阶段:用ASCII模式可以直观地看到收发数据,方便排查问题
- 无线传输:有些无线模块对二进制数据不太友好,ASCII模式更稳定
- 老旧设备兼容:很多90年代的PLC和仪表只支持ASCII模式
- 数据记录:如果要把通信数据存到文本文件里,ASCII模式直接就能看
但如果你追求性能,或者数据量比较大,还是老老实实用RTU吧。ASCII模式那50%的效率损失,在高速采集场景下是致命的。
好了,ASCII帧结构就讲到这里。下一节咱们聊聊Modbus的异常响应,看看设备出问题时,它会怎么告诉你。