4. Modbus ASCII帧结构:起始字符、地址码、功能码、LRC校验、结束字符

好,咱们接着聊。上一章我们把RTU模式扒了个底朝天,这一章轮到ASCII模式了。说实话,我个人觉得ASCII模式在电力系统里用得不如RTU多,但你不能不会。为什么?因为有些老设备、某些特定通信链路,就认ASCII。

说白了,ASCII模式就是把每个字节拆成两个ASCII字符来发送。你想想看,一个字节0x1A,在ASCII模式下就变成了字符'1'和字符'A'。效率确实低了点,但好处是——肉眼可读,调试方便。

4.1 帧结构概览

Modbus ASCII的帧结构长这样:

起始字符 | 地址码 | 功能码 | 数据区 | LRC校验 | 结束字符
   ':'    |  2字符 |  2字符 |  N字符 |  2字符  | CR+LF

嗯,这里要注意:所有字段都是ASCII字符。不是二进制,是字符。比如地址码0x11,在帧里就是两个字符——'1'和'1'。

字段 长度 说明
起始字符 1字节 固定为':' (0x3A)
地址码 2字符 从站地址,如'01'表示地址1
功能码 2字符 如'03'表示读保持寄存器
数据区 可变 每个字节拆成两个ASCII字符
LRC校验 2字符 纵向冗余校验,ASCII表示
结束字符 2字节 CR (0x0D) + LF (0x0A)

4.2 起始字符与结束字符

起始字符是冒号':',十六进制0x3A。这个字符告诉从站:「嘿,新的一帧来了!」

结束字符是回车换行——CR (0x0D) 和 LF (0x0A)。为什么用这个?因为ASCII模式最早就是给字符终端用的,CR+LF正好是「换行」的意思。

我的经验: 有一次在现场调试,发现从站老是丢帧。查了半天,原来是主站发的结束符只有CR没有LF。有些从站比较严格,缺一个就不认。所以,CR+LF一个都不能少

4.3 地址码与功能码

这两个字段和RTU模式本质上一样,只是表示方式变了。

  • 地址码:从站地址,范围0x00~0xF7。在ASCII帧里用两个字符表示,比如地址0x11显示为'11'。
  • 功能码:操作类型,比如0x03显示为'03'。

举个例子:你要读取地址为0x11的从站的保持寄存器,功能码0x03。那么在ASCII帧里,地址码就是字符'1'和'1',功能码就是字符'0'和'3'。

4.4 LRC校验——这个坑我踩过

LRC,全称Longitudinal Redundancy Check,纵向冗余校验。它和RTU的CRC不一样,计算简单得多。

计算方法:

  1. 从地址码开始,到数据区结束,所有字节相加
  2. 取和的低8位
  3. 计算补码(256 - 低8位)
  4. 结果转成两个ASCII字符

说白了就是:所有字节加起来,取反加一

注意: LRC计算的是原始字节,不是ASCII字符。比如地址0x11,计算时用的是0x11这个字节,不是字符'1'和'1'。

我曾经在项目里犯过这个错——把ASCII字符拿去算LRC了,结果怎么都对不上。后来查了三天资料才发现,原来算的是原始值。嗯,这个坑你们别踩。

4.5 完整帧示例

咱们来个实际的。假设你要读取地址0x11的从站,从寄存器地址0x006B开始读2个寄存器。

原始数据:

地址: 0x11
功能码: 0x03
起始地址: 0x006B
寄存器数量: 0x0002

计算LRC:

0x11 + 0x03 + 0x00 + 0x6B + 0x00 + 0x02 = 0x81
LRC = 256 - 0x81 = 0x7F

最终ASCII帧:

: 11 03 00 6B 00 02 7F CR LF

注意看,每个字节都变成了两个字符。比如0x11变成了'1'和'1',0x03变成了'0'和'3'。整个帧就是一堆可读的ASCII字符。

4.6 ASCII vs RTU:怎么选?

我个人习惯是:能用RTU就用RTU。为什么?效率高啊!同样一条指令,RTU可能只要8个字节,ASCII要17个字节。在9600波特率下,RTU快了一倍不止。

但有些场景必须用ASCII:

  • 通信链路有字符转换设备(比如某些电台、光端机)
  • 调试阶段,需要肉眼观察报文
  • 某些老设备只支持ASCII模式
避坑指南: 我曾经在一个水利项目中,用了电台通信。RTU模式死活通不上,换成ASCII就好了。后来发现是电台内部做了字符转换,把二进制数据给破坏了。所以,如果通信链路中间有「智能」设备,优先考虑ASCII

4.7 编程实现要点

写代码的时候,有几个关键点要注意:

  1. 接收时:检测到冒号':'才开始组帧,收到CR+LF就结束
  2. 超时处理:两个字符之间超过1秒,认为帧出错
  3. LRC验证:收到后重新计算LRC,和收到的比对

给你看一段伪代码思路:

// 发送ASCII帧
function sendAsciiFrame(slaveAddr, funcCode, data) {
    let frame = ':'
    frame += byteToAscii(slaveAddr)
    frame += byteToAscii(funcCode)
    for (let b of data) {
        frame += byteToAscii(b)
    }
    let lrc = calculateLRC(slaveAddr, funcCode, data)
    frame += byteToAscii(lrc)
    frame += '\r\n'
    uart.send(frame)
}

// 计算LRC
function calculateLRC(addr, func, data) {
    let sum = addr + func
    for (let b of data) {
        sum += b
    }
    return (256 - (sum & 0xFF)) & 0xFF
}

嗯,代码不复杂。但要注意,byteToAscii这个函数,是把一个字节转成两个十六进制字符。比如0x1A转成'1'和'A'。别搞反了。

4.8 小结

ASCII模式说白了就是RTU的「文本版」。结构清晰,调试方便,但效率低。在电力系统里,我建议你:

  • 新项目优先用RTU
  • 老设备或特殊链路用ASCII
  • 调试阶段可以临时切ASCII,调完再切回去

下一章我们讲Modbus TCP,那个更简单,因为TCP/IP协议栈已经帮我们处理了校验和组帧。但别急,先把ASCII吃透,基础打牢了,后面才稳。