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帧内部不允许出现 :CRLF 这些特殊字符。如果数据中恰好有这些值,必须做转义处理。不过好在Modbus ASCII的数据都是十六进制字符,天然避开了这个问题。

4.3 LRC校验:简单但有效

RTU模式用的是CRC校验,计算复杂但检错能力强。ASCII模式用的是LRC校验,说白了就是“纵向冗余校验”。

LRC的计算方法非常简单:

  1. 把从站地址到数据区的所有字节,按二进制相加
  2. 丢掉进位,只保留低8位
  3. 对结果取补码(也就是用0xFF减去结果,再加1)
  4. 最终得到的就是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的转换关系

这两种模式本质上是在传输层做了不同的“编码”。它们的数据内容是完全一样的。

转换关系其实很简单:

  1. RTU转ASCII:把RTU帧的每个字节拆成两个ASCII字符,加上 : 开头和 CR LF 结尾,把CRC换成LRC。
  2. 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的异常响应,看看设备出问题时,它会怎么告诉你。