4. ASCII模式详解:ASCII帧结构、LRC校验原理与计算、ASCII模式下的数据编码与解码

好,咱们接着聊。上一节我们把RTU和ASCII的“选型之争”讲清楚了。这一节,我带你深入看看ASCII模式到底是怎么工作的。

说实话,我早年做项目时,对ASCII模式有点“偏见”。总觉得它效率低,传输慢,不如RTU“高级”。直到有一次,我在一个强干扰的钢厂项目里,RTU怎么都调不通,数据全是乱码。最后换成ASCII模式,问题迎刃而解。从那以后,我就再也不敢小看它了。

ASCII模式,说白了,就是把每个字节拆成两个ASCII字符来发送。比如一个字节 0x1A,它不直接发 0x1A,而是先转成字符 '1''A',再发它们的ASCII码 0x310x41。你想想看,数据量直接翻倍了。但为什么还要用它?因为它“稳”。

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模式下,它够用了。

原理是什么?

说白了,就是把从地址码开始,到数据区结束的所有字节,做一个“累加和”,然后取补码。就这么简单。

计算步骤:

  1. 把地址码、功能码、数据区的所有原始字节(注意,是字节,不是ASCII字符)加起来。
  2. 只取结果的最低8位(也就是 0xFF 相与)。
  3. 0x100 减去这个结果,得到LRC值。
  4. 把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。你要把它们还原成一个字节:

  1. '3' 转成数值 3(减去0x30)。
  2. 'F' 转成数值 15(减去0x37,因为A-F比9大7)。
  3. 组合: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校验。到时候咱们再细聊。