C++ MFC RS485串口通信Demo:半双工收发与线程模型实战

发布时间:2026/10/15 18:37:15
C++ MFC RS485串口通信Demo:半双工收发与线程模型实战
简介这份资源是面向工业自动化、物联网设备开发者的C MFC RS485串口通信完整示例工程适合具备一定Windows编程基础、希望快速掌握串口通信实现方法的开发者参考学习。资源包共22个文件包含4个h头文件、3个cpp源文件以及sln解决方案、vcxproj工程配置、rc资源脚本、ico图标等Visual Studio 2015项目文件压缩包约52KB导入后可直接编译运行。内容围绕CSerialPort类展开覆盖串口对象创建、波特率与数据位等参数配置、串口打开与关闭、数据读写及异步事件响应等关键环节并涉及错误处理与通信协议等实际项目细节。目前已有3199人学习下载可帮助读者理解MFC与RS485结合的实现思路为自身项目提供可复用的代码参考与排错借鉴。1. 工控现场调试串口总翻车这份 C MFC-RS485 通信 Demo 把链路跑通了在工控现场调 RS485 串口通信最让人头疼的不是协议本身而是上位机软件那一层。你手里可能已经有 USB 转 485 模块、有温湿度传感器、有 PLC但打开串口调试助手发几条指令要么收不到回码要么收到一堆乱码要么第一次能通、第二次就卡死。问题往往出在串口参数配置、收发时序、线程阻塞这几个环节上。这份基于 C MFC 的 RS485 串口通信 Demo 完整版代码就是针对这个场景做的它把串口打开、参数设置、数据发送、接收解析、界面刷新这一整条链路用 MFC 框架串了起来适合做上位机开发的工程师直接拿来改。如果你正在用 C 做 Windows 端工控软件或者刚接触 RS485 半双工通信这份代码能帮你省掉从零搭框架的时间。2. RS485 半双工通信在 MFC 里的实现逻辑先搞懂收发切换再动手2.1 RS485 和 RS232 的本质区别半双工与差分信号很多人第一次用 RS485 会踩一个坑以为它和 RS232 一样发完数据就能立刻收。实际上 RS485 是半双工差分信号同一时刻总线上只能有一个设备在发送。上位机通过 USB 转 485 模块连接时模块内部会自动处理收发方向切换但软件层面必须保证发送指令后给从站足够的响应时间再去读缓冲区。如果发完立刻读大概率读到空数据如果发完等太久界面又会卡顿。这份 Demo 在发送函数里做了一个关键处理发送完成后不立即读取而是通过一个短延时或者等待事件的方式让从站有时间把响应数据放上总线。这个延时不是随便设的常见做法是根据从站的响应时间手册来定一般 10ms 到 50ms 之间。代码里把这个值做成了可配置参数方便不同设备适配。另一个区别是差分信号抗干扰能力强传输距离远。RS485 在 1200 米以内都能稳定通信但前提是 A、B 线要双绞终端要接匹配电阻。这些硬件层面的东西软件管不了但软件层面如果收发时序不对再好的硬件也白搭。2.2 MFC 串口通信的三种方案选型为什么这份 Demo 用 MSComm 或 Win32 API在 MFC 里做串口通信常见有三条路一是用微软早期的 MSComm 控件二是直接调 Win32 APICreateFile、ReadFile、WriteFile三是用第三方串口库。这份 Demo 走的是前两种结合的路子具体用哪个取决于你拿到的代码版本。MSComm 控件的优点是上手快拖到对话框上就能用事件驱动模式对新手友好。缺点是它依赖一个 ocx 文件部署到干净系统上可能要注册而且微软早就停止维护了。Win32 API 的方式更底层用 CreateFile 打开 COM 口用 DCB 结构体配置波特率、数据位、停止位、校验位用 ReadFile 和 WriteFile 收发数据。这种方式没有额外依赖编译出来的 exe 直接能跑适合做产品级软件。Demo 里如果用的是 Win32 API你会看到类似这样的初始化流程// 打开串口COM3 根据实际设备管理器里的端口号改 HANDLE hCom CreateFile(_T(COM3), GENERIC_READ | GENERIC_WRITE, // 允许读写 0, // 独占方式打开 NULL, OPEN_EXISTING, // 打开已存在的串口 FILE_ATTRIBUTE_NORMAL, // 同步方式不用重叠结构 NULL); if (hCom INVALID_HANDLE_VALUE) { AfxMessageBox(_T(串口打开失败检查端口号是否被占用)); return; } // 配置串口参数波特率96008位数据1位停止无校验 DCB dcb; memset(dcb, 0, sizeof(dcb)); dcb.DCBlength sizeof(dcb); GetCommState(hCom, dcb); dcb.BaudRate 9600; // 波特率常见还有19200、38400、115200 dcb.ByteSize 8; // 数据位一般固定8位 dcb.Parity NOPARITY; // 校验位NOPARITY表示无校验 dcb.StopBits ONESTOPBIT; // 停止位ONESTOPBIT表示1位停止位 SetCommState(hCom, dcb); // 设置读写超时避免ReadFile永久阻塞 COMMTIMEOUTS timeouts; timeouts.ReadIntervalTimeout 50; // 读间隔超时50ms timeouts.ReadTotalTimeoutConstant 100; // 读总超时常量100ms timeouts.ReadTotalTimeoutMultiplier 10; // 每字节读超时10ms timeouts.WriteTotalTimeoutConstant 100; // 写总超时常量100ms timeouts.WriteTotalTimeoutMultiplier 10; // 每字节写超时10ms SetCommTimeouts(hCom, timeouts);这段代码的逻辑说明CreateFile 的第三个参数设为 0 表示独占访问防止其他程序同时打开同一个串口。DCB 结构体里 BaudRate 必须和从站设备一致这是最常见的通信失败原因。COMMTIMEOUTS 里的 ReadIntervalTimeout 设成 50ms 意味着如果两个字节之间间隔超过 50msReadFile 就返回不会一直等下去。这个参数对半双工通信特别重要因为从站可能只回几个字节就结束了没有超时设置的话 ReadFile 会卡住整个线程。2.3 发送与接收的线程模型别让界面卡成 PPTMFC 的界面线程和串口读写线程如果不分开发送一条指令等响应的时候整个窗口就会无响应。这份 Demo 在架构上做了区分主线程负责界面刷新和用户操作串口读写放在一个单独的工作线程里通过自定义消息或者事件对象把收到的数据传回主线程。工作线程的典型写法是用 AfxBeginThread 创建一个 UI 线程或者工作者线程。工作者线程里跑一个 while 循环不断检查串口缓冲区有没有数据。收到数据后用 PostMessage 发一个自定义消息给主窗口主窗口的消息处理函数里再更新编辑框或者列表控件。// 自定义消息用于工作线程通知主线程收到数据 #define WM_COMM_RECEIVE (WM_USER 100) // 工作线程函数 UINT CSerialDemoDlg::SerialThread(LPVOID pParam) { CSerialDemoDlg* pDlg (CSerialDemoDlg*)pParam; DWORD dwBytesRead 0; BYTE buffer[1024]; while (pDlg-m_bThreadRunning) { // 读串口超时时间由COMMTIMEOUTS控制 if (ReadFile(pDlg-m_hCom, buffer, sizeof(buffer), dwBytesRead, NULL)) { if (dwBytesRead 0) { // 把收到的数据拷贝到堆上通过消息传给主线程 BYTE* pData new BYTE[dwBytesRead]; memcpy(pData, buffer, dwBytesRead); pDlg-PostMessage(WM_COMM_RECEIVE, (WPARAM)pData, dwBytesRead); } } Sleep(10); // 避免CPU占用过高 } return 0; }参数说明m_bThreadRunning 是一个 bool 成员变量关闭串口时置为 false线程就会退出循环。Sleep(10) 是让线程每 10ms 查一次这个值太小会占 CPU太大响应会迟钝。PostMessage 传的 pData 是在堆上分配的主线程处理完消息后必须 delete[]否则内存泄漏。主线程的消息处理函数afx_msg LRESULT CSerialDemoDlg::OnCommReceive(WPARAM wParam, LPARAM lParam) { BYTE* pData (BYTE*)wParam; int nLen (int)lParam; // 把字节数组转成十六进制字符串显示 CString strShow; for (int i 0; i nLen; i) { CString strByte; strByte.Format(_T(%02X ), pData[i]); strShow strByte; } m_editRecv.SetSel(-1, -1); m_editRecv.ReplaceSel(strShow); delete[] pData; // 释放工作线程分配的内存 return 0; }这里用 ReplaceSel 而不是 SetWindowText是为了追加显示而不是覆盖。十六进制格式显示是工控调试的常见做法因为很多传感器的回码不是可读 ASCII直接看十六进制更直观。3. 从零跑通 Demo环境配置、编译和第一次收发测试3.1 开发环境准备与项目编译拿到这份代码后第一步是确认开发环境。Demo 是基于 MFC 的所以 Visual Studio 必须安装「使用 C 的桌面开发」工作负载并且勾选 MFC 组件。VS2015、VS2017、VS2019、VS2022 都能编译但要注意项目属性里的「平台工具集」和「MFC 的使用方式」。打开项目后先检查几个关键配置配置项推荐值说明字符集使用多字节字符集 或 Unicode看代码里用的是 CString 还是 char不一致会报错MFC 使用方式在共享 DLL 中使用 MFC编译出的 exe 小但目标机器要有 MFC 运行库平台工具集和你 VS 版本匹配不匹配会提示找不到 v143 之类的工具集目标平台x86 或 x64取决于你系统一般 x86 兼容性更好如果编译报错「无法打开包括文件 afxwin.h」说明 MFC 组件没装去 VS Installer 里补上。如果报错「找不到 mfc140.dll」把 MFC 使用方式改成「在静态库中使用 MFC」这样编译出的 exe 不依赖外部 dll但体积会大几 MB。3.2 串口参数配置与端口号确认编译通过后先别急着连设备。打开 Windows 设备管理器展开「端口 (COM 和 LPT)」找到你的 USB 转 485 模块对应的 COM 号。常见的是 COM3、COM4但插拔不同 USB 口会变。如果设备管理器里看到黄色感叹号说明驱动没装好先装驱动。Demo 界面上一般会有波特率下拉框、数据位、停止位、校验位选择以及打开/关闭串口按钮。第一次测试建议用虚拟串口软件或者两个 USB 转 485 模块对接一个发一个收确认软件本身没问题。配置参数时注意波特率必须和从站设备完全一致。9600 和 19200 看着差不多但差一位就全乱。数据位一般 8 位停止位 1 位校验位无。有些老设备用 7 位数据位、偶校验这种就要在 DCB 里改 Parity 为 EVENPARITYByteSize 为 7。3.3 发送第一条指令并解析回码假设你连的是一个 Modbus RTU 温湿度传感器从站地址 0x01读温度寄存器。发送的指令是 01 03 00 00 00 01 84 0A。在 Demo 的发送编辑框里输入这串十六进制点发送。如果一切正常接收区会显示类似 01 03 02 00 FA 38 4B 的回码。其中 01 是从站地址03 是功能码02 是字节数00 FA 是温度数据后面是 CRC 校验。如果收不到回码按这个顺序排查检查 A、B 线有没有接反。RS485 的 A 接 AB 接 B反了收不到数据。检查波特率是否一致。用串口调试助手单独测一下从站确认参数。检查发送的指令 CRC 是否正确。Modbus RTU 的 CRC 是低字节在前很多在线工具生成的 CRC 顺序可能不对。检查串口是否被其他程序占用。串口是独占资源打开一个软件后另一个打不开。Demo 里如果带了 CRC 校验计算函数可以直接调用生成正确指令。常见做法是写一个 CRC16 查表或者直接计算// Modbus RTU CRC16 计算返回低字节在前的结果 WORD CSerialDemoDlg::CalculateCRC16(BYTE* pData, int nLen) { WORD wCRC 0xFFFF; for (int i 0; i nLen; i) { wCRC ^ pData[i]; for (int j 0; j 8; j) { if (wCRC 0x0001) { wCRC 1; wCRC ^ 0xA001; // 多项式 0xA001 } else { wCRC 1; } } } return wCRC; // 低字节在前高字节在后 }这个函数算出来的 CRC 直接追加到指令末尾即可。注意返回的 WORD 在内存里是小端序x86 机器上直接 memcpy 到字节数组就是低字节在前符合 Modbus RTU 要求。4. 避坑指南RS485 串口通信调试中最容易翻车的五个点4.1 现象发送正常但接收全是乱码原因波特率不匹配或者数据位/停止位/校验位设置和从站不一致。还有一种可能是 USB 转 485 模块的芯片质量差在高波特率下误码率高。解决先用 9600 低波特率测试确认通了再往上调。换一个 FT232 或 CH340 芯片的模块对比。检查 DCB 结构体里 ByteSize、Parity、StopBits 是否和从站手册一致。4.2 现象第一次通信成功第二次就超时原因半双工收发切换没处理好。发送完成后没有给从站足够的响应时间或者接收缓冲区没清空上一次的残留数据干扰了下一次。解决在每次发送前调用 PurgeComm 清空缓冲区发送后加 20ms 到 50ms 延时再读。Demo 里如果用了事件驱动检查事件重置逻辑是否正确。// 发送前清空缓冲区避免残留数据干扰 PurgeComm(m_hCom, PURGE_RXCLEAR | PURGE_TXCLEAR);4.3 现象界面卡死点关闭按钮没反应原因串口读写放在了主线程里ReadFile 阻塞导致消息循环卡住。或者工作线程退出时没有正确等待线程结束。解决确保串口读写在工作线程里。关闭串口时先把 m_bThreadRunning 置 false然后 WaitForSingleObject 等线程句柄最后再 CloseHandle 关闭串口。4.4 现象编译报错「error C2664: 无法将参数从 const char* 转换为 LPCWSTR」原因项目字符集设成了 Unicode但代码里字符串用的是 char 类型。MFC 的 CString 在 Unicode 下是宽字符和 char 不兼容。解决要么把项目字符集改成「使用多字节字符集」要么在字符串前加 _T() 宏比如 _T(COM3)。推荐后者兼容性更好。4.5 现象换一台电脑就找不到串口或打不开原因目标电脑没装 USB 转 485 驱动或者 COM 口号变了但软件里写死了。解决软件里做端口号下拉框枚举系统可用串口。用 SetupAPI 或者简单点用注册表读 HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM 下的键值。部署时把驱动一起打包。5. 进阶技巧把 Demo 改成能用的工控上位机还差哪几步这份 Demo 跑通之后离真正能上产线的上位机还有距离。我一般会做这几件事第一把接收数据解析从「显示十六进制」升级成「按协议解析成物理量」。比如 Modbus 回码里的 00 FA 要转成 25.0 摄氏度需要根据传感器手册做线性变换。第二加一个定时发送功能用 SetTimer 或者线程里的 Sleep 控制轮询周期常见是 1 秒读一次。第三把数据存到 SQLite 或者 CSV 里方便追溯。第四加日志功能每次收发都记时间戳和原始字节出问题时能回看。还有一个容易忽略的点串口热插拔。USB 转 485 模块拔掉再插上COM 口号可能变原来的句柄就失效了。成熟的做法是监听 WM_DEVICECHANGE 消息检测到设备变化时自动关闭旧串口、重新枚举、打开新串口。这个逻辑不复杂但能省掉现场运维很多电话。// 在对话框消息映射里加设备变化处理 BEGIN_MESSAGE_MAP(CSerialDemoDlg, CDialogEx) ON_WM_DEVICECHANGE() END_MESSAGE_MAP() BOOL CSerialDemoDlg::OnDeviceChange(UINT nEventType, DWORD_PTR dwData) { if (nEventType DBT_DEVICEARRIVAL || nEventType DBT_DEVICEREMOVECOMPLETE) { // 设备插入或拔出重新枚举串口列表 RefreshSerialPortList(); } return CDialogEx::OnDeviceChange(nEventType, dwData); }从那以后我每次交付带串口通信的上位机都强制走一遍「拔插测试」运行中拔掉 USB 转 485等几秒再插上看软件能不能自动恢复。这个习惯帮我挡掉了至少三次现场返工。希望这份 Demo 和上面的踩坑记录能帮到你少走点弯路。本文还有配套的精品资源点击获取