modbus-rtu帧拆解需缓存+crc校验+3.5t静默超时双重判定;crc16用0xa001查表法,输入不含crc;解析0x03/0x04响应须先判异常码(功能码&0x80),再依byte_count/2得寄存器数。

Modbus-RTU帧结构怎么拆解才不丢字节
Modbus-RTU不是“发一串字节就完事”,它依赖严格的帧边界判断:起始静默时间 ≥3.5T(T为1字符传输时间),结尾静默时间同样 ≥3.5T。但实际串口接收时,read() 或 serial_port::read_some() 往往分多次返回数据,导致一个完整帧被切成两段——比如先收到前6字节,后收到剩下5字节。直接按固定长度(如最小帧长8字节)截取会出错。
正确做法是缓存未完成帧,并基于 CRC 校验 + 静默超时双重判定:
- 维护一个
std::vector<uint8_t></uint8_t>缓冲区,持续追加新读入字节 - 每次追加后,从缓冲区头部开始扫描:找合法地址域(1–247)、功能码(0x01/0x03/0x04/0x06/0x10等)、再检查末尾2字节是否为该帧的 CRC16(小端排列)
- 同时用系统时钟记录最后收到字节的时间,若当前时间 - 最后接收时间 > 3.5T(例如9600bps下约3.5ms),则认为上一帧已结束,剩余缓冲区内容可清空或告警
如何用C++跨平台计算Modbus CRC16(0xA001多项式)
Modbus RTU 使用 CRC-16-IBM(多项式 0xA001,初始值 0xFFFF,无反转、无异或输出),和常见 Linux cksum 或 Qt 的 QCryptographicHash 不兼容。手写错误率高,尤其字节序和异或时机容易搞反。
推荐直接实现标准查表法(快且确定):
static constexpr uint16_t modbus_crc16(const uint8_t* data, size_t len) {
static const uint16_t table[256] = {
0x0000, 0xC0C1, 0xC181, 0x0140, /* ... 省略,共256项,可生成或复制权威表 */
};
uint16_t crc = 0xFFFF;
for (size_t i = 0; i > 8) ^ table[idx];
}
return crc;
}
注意:data 指针必须指向不含 CRC 的原始帧(即地址+功能码+数据域),长度 len 也不含末尾2字节。校验时,把整帧(含CRC)传入,结果应为 0x0000 才合法。
解析0x03/0x04读寄存器响应时,数据长度字段怎么用
功能码 0x03(读保持寄存器)和 0x04(读输入寄存器)响应帧格式为:[addr][0x03][byte_count][data...][crc_lo][crc_hi]。其中 byte_count 是后续数据字节数(不是寄存器个数),而每个寄存器占2字节、大端存储。
常见误判:看到 byte_count == 4 就以为是2个寄存器,却忽略它可能来自异常响应(此时 byte_count 实际是功能码 | 0x80,比如 0x83)。所以必须先确认功能码是否带异常标志:
- 收到响应首字节为设备地址,第二字节为功能码 → 若该字节 & 0x80,则为异常响应,
byte_count实际是异常码,后面只有1字节,无需继续解析数据 - 否则才是正常响应,
byte_count有效,且必须为偶数;寄存器数量 =byte_count / 2 - 解析数据时,用
uint16_t reg = (data[i] 提取每个寄存器值,不能直接 memcpy 到 <code>uint16_t[](大小端风险)
为什么用 std::vector 而不用 char[] 处理 Modbus 帧
Modbus 帧本质是二进制流,含大量 0x00 字节。char 在某些平台默认有符号,当某字节为 0xFF 时,若被当 signed char 读取再转 int,会扩展成 0xFFFFFFFF 而非 0x00FF,导致 CRC 计算或地址比对失败。
更关键的是内存管理:串口接收节奏不可控,帧长动态变化(最短 8 字节,最长可达 256+ 字节)。用定长 char buf[256] 容易溢出,而 std::vector<uint8_t></uint8_t> 可安全 resize,且 uint8_t 明确无符号语义,所有位运算、比较、CRC 输入都稳。
另外,C++20 起可配合 std::span<const uint8_t></const> 传递子帧视图,避免拷贝,但前提是确保底层 vector 生命周期足够长。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











