4. ASCII模式详解:ASCII帧结构、LRC校验原理与计算、ASCII模式下的数据编码与解码
好,咱们接着聊。上一节我们把RTU和ASCII的“选型之争”讲清楚了。这一节,我带你深入看看ASCII模式到底是怎么工作的。
说实话,我早年做项目时,对ASCII模式有点“偏见”。总觉得它效率低,传输慢,不如RTU“高级”。直到有一次,我在一个强干扰的钢厂项目里,RTU怎么都调不通,数据全是乱码。最后换成ASCII模式,问题迎刃而解。从那以后,我就再也不敢小看它了。
ASCII模式,说白了,就是把每个字节拆成两个ASCII字符来发送。比如一个字节 0x1A,它不直接发 0x1A,而是先转成字符 '1' 和 'A',再发它们的ASCII码 0x31 和 0x41。你想想看,数据量直接翻倍了。但为什么还要用它?因为它“稳”。
4.1 ASCII帧结构:看得见的“信封”
ASCII模式的帧结构,比RTU要“啰嗦”一些。它用特殊的字符来当“信封”,一眼就能看出哪里是开头,哪里是结尾。
一个完整的ASCII帧长这样:
: 11 03 00 6B 00 03 7A CR LF
我来拆开给你看:
| 字段 | 长度 | 说明 |
|---|---|---|
| 起始字符 | 1字节 (0x3A) | 就是冒号 ':'。表示一帧的开始。 |
| 地址码 | 2个ASCII字符 | 比如 '11',代表从站地址1。 |
| 功能码 | 2个ASCII字符 | 比如 '03',读保持寄存器。 |
| 数据区 | N个ASCII字符 | 具体数据,长度可变。 |
| LRC校验 | 2个ASCII字符 | 纵向冗余校验,后面细讲。 |
| 结束符 | 2字节 (0x0D, 0x0A) | 回车换行 CR LF。表示一帧结束。 |
关键点:ASCII模式没有“空闲线”判断,它完全靠 ':' 和 CR LF 来定界。所以,哪怕线路上一堆噪声,只要没碰到冒号,设备就不会乱动。这就是它抗干扰强的根本原因。
嗯,这里要注意:地址码、功能码、数据、LRC,这些字段都是用ASCII字符表示的十六进制数。比如地址1,不是发 0x01,而是发字符 '1' 和 '1',也就是 0x31 0x31。
4.2 LRC校验原理与计算:简单但有效
LRC,全称Longitudinal Redundancy Check,纵向冗余校验。它比RTU的CRC简单得多,但防护能力也弱一些。不过,在ASCII模式下,它够用了。
原理是什么?
说白了,就是把从地址码开始,到数据区结束的所有字节,做一个“累加和”,然后取补码。就这么简单。
计算步骤:
- 把地址码、功能码、数据区的所有原始字节(注意,是字节,不是ASCII字符)加起来。
- 只取结果的最低8位(也就是
0xFF相与)。 - 用
0x100减去这个结果,得到LRC值。 - 把LRC值转成2个ASCII字符,放到帧尾。
举个例子,还是上面那个帧:11 03 00 6B 00 03。
第一步,把这些字节加起来:
0x11 + 0x03 + 0x00 + 0x6B + 0x00 + 0x03 = 0x82
第二步,取低8位:0x82 & 0xFF = 0x82
第三步,计算LRC:0x100 - 0x82 = 0x7E
所以,LRC就是 0x7E。在帧里,它要写成两个ASCII字符 '7' 和 'E',也就是 0x37 0x45。
我的小技巧: 我在写代码时,习惯用一个小函数来算LRC。C语言里,直接对累加和取反加一,效果一样:LRC = (~sum) + 1。但要注意,只取低8位。
4.3 ASCII模式下的数据编码与解码
这部分是实际编程中最容易出错的地方。我见过不少新手,直接把收到的ASCII字符当数据用,结果全乱套了。
编码(发送时):
你要发送一个字节 0x3F,不能直接发 0x3F。你得先把它拆成两个“半字节”:高4位 0x3,低4位 0xF。然后分别转成ASCII字符:0x3 变成 '3'(0x33),0xF 变成 'F'(0x46)。最后发送 0x33 0x46。
解码(接收时):
收到两个ASCII字符 '3' 和 'F',也就是 0x33 0x46。你要把它们还原成一个字节:
- 把
'3'转成数值3(减去0x30)。 - 把
'F'转成数值15(减去0x37,因为A-F比9大7)。 - 组合:
3 * 16 + 15 = 63 = 0x3F。
避坑指南: 我曾经在一个项目中,解码时忘了处理小写字母。对方发了个 'a',我直接按 'A' 的算法算,结果数据全错了。记住,ASCII模式里,十六进制字母必须用大写。但作为接收方,最好大小写都兼容。
一个完整的编码/解码示例(C语言风格):
// 编码:字节转2个ASCII字符
void byte_to_ascii(unsigned char byte, char *ascii) {
unsigned char high = (byte >> 4) & 0x0F;
unsigned char low = byte & 0x0F;
ascii[0] = (high > 9) ? (high + 'A' - 10) : (high + '0');
ascii[1] = (low > 9) ? (low + 'A' - 10) : (low + '0');
}
// 解码:2个ASCII字符转字节
unsigned char ascii_to_byte(char high, char low) {
unsigned char h, l;
if (high >= '0' && high <= '9') h = high - '0';
else if (high >= 'A' && high <= 'F') h = high - 'A' + 10;
else if (high >= 'a' && high <= 'f') h = high - 'a' + 10; // 兼容小写
else h = 0; // 非法字符,按0处理
if (low >= '0' && low <= '9') l = low - '0';
else if (low >= 'A' && low <= 'F') l = low - 'A' + 10;
else if (low >= 'a' && low <= 'f') l = low - 'a' + 10;
else l = 0;
return (h << 4) | l;
}
你看,代码逻辑很清晰。但实际项目中,我建议你把这些操作封装成函数,别到处写重复代码。否则,一旦有个地方忘了转换,排查起来能让你怀疑人生。
好了,ASCII模式的核心内容就这些。总结一下:
- 帧结构:用
':'开头,CR LF结尾,中间全是ASCII字符。 - LRC校验:简单累加取补码,计算快,但防护弱。
- 编解码:一个字节拆两个ASCII字符,记得大小写兼容。
下一节,我会讲RTU模式的细节,特别是那个让人又爱又恨的CRC校验。到时候咱们再细聊。