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正好是「换行」的意思。
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不一样,计算简单得多。
计算方法:
- 从地址码开始,到数据区结束,所有字节相加
- 取和的低8位
- 计算补码(256 - 低8位)
- 结果转成两个ASCII字符
说白了就是:所有字节加起来,取反加一。
我曾经在项目里犯过这个错——把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模式
4.7 编程实现要点
写代码的时候,有几个关键点要注意:
- 接收时:检测到冒号':'才开始组帧,收到CR+LF就结束
- 超时处理:两个字符之间超过1秒,认为帧出错
- 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吃透,基础打牢了,后面才稳。