Modbus通信数据切片问题实战:MFC OnComm响应函数导致的CRC校验错误解决方案
最近在调试一个基于MFC的工业数据采集上位机时,遇到了一个颇为棘手的通信问题。设备通过串口以Modbus RTU协议上报数据,大部分时间运行良好,但只要稍微提高一下发送频率,上位机解析到的数据帧就会时不时地出现CRC校验失败。起初怀疑是线路干扰或者设备端的问题,但用串口助手监听发现,设备发出的原始报文完全正确。问题显然出在接收端——我们自己的MFC程序上。经过一番抽丝剥茧,最终定位到MFC的MSComm控件及其OnComm事件响应机制,一个典型的“数据切片”问题浮出水面。如果你也在用MFC做串口通信,特别是处理像Modbus这类对帧完整性要求严格的协议时,这篇文章或许能帮你避开这个坑。
1. 问题现象与根源剖析:为什么完整的数据帧会被“切碎”?
在串口通信中,尤其是在Windows环境下使用MFC的MSComm控件,开发者常常会有一个美好的假设:当一帧完整的数据到达串口缓冲区时,OnComm事件会被触发一次,我们就能在响应函数里一次性读取到整个数据帧。然而,现实往往比理想骨感。
典型的问题场景是这样的: 你向设备发送了一条Modbus查询指令,设备正确回复了一帧包含8个字节的数据(例如:地址码、功能码、数据长度、数据域、CRC校验码)。在你的MFC程序中,SetRThreshold(1)意味着接收缓冲区每收到1个字节就会触发一次OnComm事件。逻辑上,当8个字节全部到达后,你应该能一次性读取它们。但实际调试中,你可能会发现OnComm事件被触发了两次,第一次读到了5个字节,第二次读到了3个字节。当你将这“两片”数据拼凑起来进行CRC校验时,校验失败。因为CRC校验是针对完整原始帧计算的,任何物理上的拆分都会破坏校验的完整性。
// 一个典型的、有问题的OnComm响应函数示例
void CYourDlg::OnOnCommMscomm1()
{
// 假设RThreshold设置为1
VARIANT variant_inp;
COleSafeArray safearray_inp;
LONG len, k;
BYTE rxdata[2048]; // 接收缓冲区
// ... 获取接收缓冲区数据长度的代码 ...
// 问题所在:这里触发时,缓冲区里的数据可能不完整
for(k=0; k<len; k++)
{
safearray_inp.GetElement(&k, rxdata+k);
}
// 立即对rxdata进行CRC校验和解包 -> 很可能失败
ProcessModbusFrame(rxdata, len);
}
注意:这里的关键不在于
SetRThreshold的值设为1还是8。即使你设置为期望的帧长度(比如8),OnComm事件的触发时机也不保证与物理数据帧的边界对齐。它只与Windows系统底层驱动将数据从硬件缓冲区搬运到应用层缓冲区的时机有关,这个时机可能受到系统调度、CPU负载等多种因素影响。
那么,根因究竟是什么?问题核心在于事件触发与数据到达的异步性。MSComm控件的OnComm事件是消息驱动机制的一部分。当有通信事件(如接收到指定阈值字符)发生时,控件会向应用程序发送一个WM_COMMNOTIFY消息,最终调用你的OnComm响应函数。然而,从物理层一个字节一个字节地接收数据,到系统驱动将其组合并通知应用层,这个过程存在微小但不可忽视的时间差。在高频率通信或系统繁忙时,一帧数据在物理传输上虽然是连续的,但在被应用程序“感知”和读取时,可能已经被操作系统的底层机制分成了多个“数据块”放入缓冲区,从而导致OnComm被多次触发。
2. 解决方案对比:从“包头包尾”到“延迟接收”
面对数据切片问题,常见的解决思路主要有两种,它们各有优劣,适用于不同的场景。
2.1 方法一:添加帧头帧尾与缓存拼接
这是一种非常经典和通用的通信协议处理方式,不仅适用于MFC,在任何异步串口通信场景下都有效。
核心思想:在原始数据帧(如Modbus PDU)的前后加上独特的、不会在数据域中出现的标识字节,例如0xAA作为帧头(SOF),0x55作为帧尾(EOF)。接收方不再假设一次读取就是完整帧,而是维护一个缓存区。每次OnComm触发读取数据后,都将数据追加到缓存区,然后从头开始扫描缓存区:
- 寻找帧头
0xAA。 - 找到帧头后,继续寻找帧尾
0x55。 - 如果同时找到了帧头和帧尾,并且它们之间的数据长度符合预期,则提取出中间部分作为一帧有效数据,进行后续处理(如CRC校验)。
- 将处理过的数据从缓存区中移除。
// 伪代码展示缓存拼接逻辑
CByteArray m_SerialBuffer; // 全局或成员变量,用作接收缓存
void CYourDlg::OnOnCommMscomm1()
{
// ... 读取本次触发得到的数据到 tempBuf ...
m_SerialBuffer.Append(tempBuf); // 追加到缓存
// 循环处理缓存中所有完整帧
while(m_SerialBuffer.GetSize() > 0)
{
int sofIndex = FindByte(m_SerialBuffer, 0xAA);
if(sofIndex == -1) // 没找到帧头,清空无效数据
310



被折叠的 条评论
为什么被折叠?



