
简介面向C与MFC开发者的串口通信类库修正版由itas109维护主打轻量、可裁剪的CSerialPort串口类适用于需要在MFC或普通Win32程序中快速集成串口收发功能的项目。整个zip压缩包共45个文件以头文件13个h、源文件9个cpp和说明文档3个txt为主体同时附带2个sln解决方案、2个工程文件、2个exe演示程序及3个ico图标整体仅316KB结构清晰便于移植。本次修正重点包含新增_AFX宏定义以适配MFC必要函数Hkey2ComboBox进一步去除MFC依赖并修改AfxMessageBox调用增加Win32示例程序验证非MFC环境的可用性。读者可以拿到可直接编译的串口类源码、MFC与Win32双环境Demo、使用说明以及作者在去MFC依赖过程中的改动思路适合希望深入理解串口封装或需要跨环境复用串口代码的开发者。目前已有909人学习下载。 串口这玩意在嵌入式、仪器仪表、工业控制这些场景里存在了几十年但地位一直没被真正替代。很多做上位机开发的同行早年间练手就是靠 CSerialPort 这类串口类入的门后来有人转到 C# 的 SerialPort有人用 Qt 的 QSerialPort可真到接手老项目、处理老设备厂家给的协议 Demo或者需要精细控制 DCB 参数、自己处理事件驱动时最后还是得绕回这个 C 类。前段时间我整理工作电脑上那一堆历代工程代码发现 2017-03-12 这版 CSerialPort 串口类修正版居然还被好几个老项目引用所以决定借这个机会把这玩意儿的来龙去脉、修正内容以及把它接到新工程里会踩的坑完整梳理一遍。这也算给正在被“老代码 新编译器”折磨的人一个参考。串口通信不像普通文件读写关闭时机、线程回调、字符集处理一个不对就是偶发崩溃或者收不到数据。下面这些内容都是我在实际项目里验证过的东西摊开讲。1. CSerialPort 的前世今生为什么 2017 年还在改1.1 一个封装了 Win32 串口 API 的经典轮子早年在 Windows 上写串口程序没有现成控件可用标准做法是直接用 CreateFile 打开 COM 口然后通过 GetCommState、SetCommState 操作 DCB 结构体再用 ReadFile、WriteFile 收发数据。听起来不复杂但一旦涉及数据到达通知、超时控制、线程安全关闭代码很容易变成一团乱麻。CSerialPort 这种类的核心价值就是把“打开-配置-监视-收发-关闭”这个过程收敛到一个对象里对外暴露 InitPort、StartMonitoring、WriteToPort、ClosePort 这些接口底层逻辑则交给一个后台工作线程去跑。这个类最初流传最广的是 Remon Spekreijse 的版本后来经过大量开发者修补国内外不少技术社区和个人站点上都有变种。大家搜 CSerialPort 时能看到好几个长得差不多的版本其中一条比较有代表性的维护线就是 naughter 那边整理的版本文件命名、函数签名都非常接近。2017-03-12 这个修正版很多地方能看到 naughter 维护路线的影子改动重点集中在编译兼容性和生命周期管理上。1.2 为什么 2017-03-12 这个日期值得关注2017 年前后老一批 VC6、VS2008 项目开始大规模往 VS2015、VS2017 迁移。CSerialPort 这类老代码在迁移过程中暴露出一堆问题不是 C4996 安全函数告警就是 wchar_t 类型不匹配或者干脆在 Win10 上打开串口偶尔失败。2017-03-12 这版修正基本上就是针对这些问题做的累积更新。这个时间点的修改也很有代表性它不搞大改动不重构成现代 C 风格而是保持旧接口不动只修内部实现。好处是项目升级成本极低把 CSerialPort.h 和 CSerialPort.cpp 换掉上层代码几乎不用动。对于讲究稳定、不能随便回归的工控项目来说这是最稳妥的维护方式。我现在接手老项目第一件事就是看它用的串口类是哪个版本如果是 2017 年之前的老版本会优先考虑替换成这版再继续改业务。2. 这次修正版到底修了哪些东西2.1 新编译器下的警告和错误清理我在 VS2015 和 VS2017 下都编译过这版最直观的感受是告警少了很多。老版本里常见的 strcpy、sprintf 等函数在新编译器下会触发 C4996 安全警告修正版把内部这些调用改成了带长度限制的版本或者通过中间变量避免越界。不要小看这些告警在大型工程里如果开启了“将警告视为错误”老代码根本编不过去。还有一类是默认参数重复声明的问题。C 规定默认参数只能在声明或定义处二选一老代码经常在头文件和 cpp 文件里各写一份老编译器睁一只眼闭一只眼VS2017 会直接报错。2017-03-12 这版把这些重复默认参数都清理了编译过程变得清爽很多。如果你在升级工程时遇到莫名其妙的 C2535 错误多半就是这种老毛病。2.2 打开、拔出、关闭过程中的稳定性修正串口通信最怕的不是慢而是偶发崩溃。修正版在几个关键生命周期节点上做了加固打开端口失败时资源清理不会重复调用 CloseHandle设备被拔出或端口被占用时工作线程能正确退出而不是在等待事件时死等析构函数里增加了线程状态判断避免对象已经销毁、线程还在调用回调的情况。这里多说一句串口类的难点从来不在怎么发一个字节而在于“什么时候能安全关闭”。修正版把 StartMonitoring 启动的线程句柄、事件句柄、端口句柄的释放顺序理了一遍明显比早期版本更能扛得住异常场景。实际在 USB 转串口设备上做插拔测试稳定度提升是能感知到的设备突然断开时不会让整个上位机进程一起挂掉。2.3 Unicode/ANSI 字符集下的细节修正老代码很多默认是 ANSI 编译但 VS2015 之后的新工程向导经常默认 Unicode。CSerialPort 内部有部分代码直接用 char也有部分用 TCHAR 宏混用时最容易出问题。修正版把构造函数的端口名参数、日志输出、调试字符串都统一了处理方式在 Unicode 工程下不会出现端口名乱码也不会因为字符串长度计算错误导致数据发不完整。这一条对很多人来说可能觉得不起眼但真遇到“Debug 正常、Release 乱码”这种问题时才知道字符集处理有多折腾。当初我自己排查一个扫码枪乱码问题最后定位到是 ANSI 和 Unicode 混用导致的换掉那版串口类之后就再没出现过。3. 核心设计拆解从 CreateFile 到消息通知3.1 工作线程和事件掩码怎么配合CSerialPort 底层的原理其实很清晰打开串口时用 CreateFile 拿到句柄然后设置 DCB 和超时StartMonitoring 之后内部起一个监视线程线程里执行 WaitCommEvent 等待串口事件。比如初始化时传入 EV_RXCHAR表示“收到一个字符”就触发事件。线程被唤醒后通过 ReadFile 把数据读到缓冲区再通过 PostMessage 抛给上层窗口。为什么用 PostMessage 而不是 SendMessage因为 PostMessage 是异步的不会阻塞工作线程这样即便上层界面卡住串口接收线程也能继续收数据不会造成缓冲堆积和数据丢失。这一点在设计上位机软件时非常关键很多自己封装串口的开发者容易忽略一旦用 SendMessage 处理不当就会把接收线程卡死最后表现为“设备一收数据程序就没响应”。3.2 向上层通知的两种姿势传统 CSerialPort 是窗口消息通知制你指定一个 HWND串口线程收到数据后就往这个窗口发 WM_COMM_RXCHAR 之类的自定义消息。这种方式的优点是代码简单、天然和 MFC 消息映射契合缺点是不方便用在纯控制台或非窗口线程里。后来很多变种版本增加了回调函数接口比如给 InitPort 传一个函数指针收到数据后直接回调。两者各有适用场景我的建议是如果项目本身有界面用消息通知就够了如果是服务程序或者数据中转程序可以改用回调版避免为了收串口数据硬造一个隐藏窗口。至于这版 CSerialPort默认还是消息通知为主和 MFC 工程配合最顺。4. 把 CSerialPort 接到自己项目里的实操记录4.1 文件拷贝和工程配置我自己的习惯是新建一个底层模块目录把 CSerialPort.h 和 CSerialPort.cpp 放进去然后通过“现有项”添加到工程。这里有个容易踩坑的点这个类并不是纯 Win32 对象部分实现里依赖窗口消息和 MFC 的头文件所以工程要么是 MFC 工程要么在项目设置里把“使用 MFC”从“使用标准 Windows 库”改成“在共享 DLL 中使用 MFC”。如果不小心漏掉这一步编译时会看到一大串和 afxwin.h、CWinApp 有关的错误新手容易懵其实就是 MFC 支持没开。如果是 VS2017 之后的版本还要注意把工程的字符集、平台工具集确认好工具集选 v141 或 v140 都可选成 v142 一般也能编但个别老版本 SDL 检查会有告警建议在“配置属性-常规-SDL 检查”里关掉。4.2 一个完整可用的收发示例假设要在对话框程序里打开 COM3波特率 115200初始化代码是这样// 头文件中声明 CSerialPort m_serial; // 对话框 OnInitDialog 中 if (!m_serial.InitPort(this, _T(COM3), 115200, N, 8, 1, EV_RXCHAR, 512)) { AfxMessageBox(_T(串口打开失败请检查设备)); return FALSE; } if (!m_serial.StartMonitoring()) { AfxMessageBox(_T(启动串口监视失败)); return FALSE; }消息映射按 CSerialPort 惯例接收事件消息名是 WM_COMM_RXCHAR。头文件里声明处理函数cpp 里建立映射和实现afx_msg LRESULT OnCommReceive(WPARAM wParam, LPARAM lParam); BEGIN_MESSAGE_MAP(CComDemoDlg, CDialogEx) ON_MESSAGE(WM_COMM_RXCHAR, OnCommReceive) END_MESSAGE_MAP() LRESULT CComDemoDlg::OnCommReceive(WPARAM wParam, LPARAM lParam) { char ch static_castchar(wParam); m_strReceive ch; // 把字符追加到接收缓冲区 return 0; }发送数据时调用 WriteToPortchar szSend[] AT\r\n; m_serial.WriteToPort(szSend, sizeof(szSend) - 1);这段代码在实际项目里已经够用了。需要注意的是如果上位机界面上需要实时显示接收内容不要在 OnCommReceive 里直接 UpdateData 或操作控件因为消息虽然是在 UI 线程收到的高频刷新也会把界面拖得很卡。更合理的做法是把字符追加到一个缓存然后用定时器或者累计到一定长度再刷新界面。4.3 关闭和析构的顺序不能乱我见过不少崩溃案例都是直接在对话框 OnClose 里 delete 串口对象结果串口工作线程还没来得及退出窗口已经销毁消息没地方投递或者句柄被提前释放线程继续访问非法内存。正确的关闭顺序是先调用 StopMonitoring让工作线程退出再调用 ClosePort释放串口句柄最后才销毁串口对象。如果担心 StopMonitoring 阻塞 UI可以在退出标志上下点功夫或者干脆在收到关闭通知后先隐藏窗口再用 PostMessage 触发真正的清理逻辑。这样用户看到界面关了程序还能在后台安全地完成线程收尾不会弹“应用程序错误正在关闭”这种框。5. 排查实录和避坑清单5.1 编译报错排查速查表报错特征常见原因处理办法找不到 afxwin.hMFC 支持未开启项目属性里改为“在共享 DLL 中使用 MFC”C4996 安全告警老接口被标记废弃换成带 _s 版本或暂时关闭 SDL 检查C2535 默认参数重复头文件和 cpp 都写了默认值保留声明处删除定义处的默认参数Unicode 下端口名乱码TCHAR/char 混用统一用宽字符或确保内部都走 TCHAR 宏这张表基本覆盖了迁移到新编译器时 90% 的编译问题。剩下 10% 多半是工程的预编译头、运行时库不匹配造成的优先检查“代码生成”里的运行库设置是否一致Debug 和 Release 的配置分开看。5.2 收不到数据的几个常见原因运行期收不到数据最先查三样东西串口号对不对、波特率和其他设备端是否一致、串口是否被别的程序占用。确认这些都正常后再看事件掩码。如果初始化时没有传入 EV_RXCHAR或者写成 EV_ERR工作线程就永远等不到接收事件看起来就像设备没反应。还有一个隐蔽点超时设置。CSerialPort 内部会把 COMMTIMEOUTS 设置为一种“立即返回”的模式如果上层代码修改了超时或者重新设置过 DCB读操作可能阻塞或返回异常。建议把超时相关配置收敛到串口类内部不要在业务代码里到处改否则排查起来非常费劲。5.3 长时间运行和高频收发的优化经验CSerialPort 本身能跑但要在长时间运行中保持稳定必须解决两个问题一是接收缓冲区不能被撑爆二是 UI 不能跟着串口频率一起抖。我的做法是把接收到的原始数据先累积到一个独立的环形缓冲区由专门的解析线程按协议帧切包只有完整解析出一帧数据时才通过消息通知界面刷新。这种做法改起来不大但收益非常明显。早年我做过一个条码数据采集程序波特率 115200数据两三百毫秒来一次如果用最原始的逐个字符 PostMessage 刷新CPU 占用长期 10% 以上改成帧缓冲通知之后CPU 占用降到 1% 左右界面也不卡了。核心思路其实就一句话不要让 UI 工作在串口频率上要让它工作在业务频率上。6. 使用这个类几年后的一点个人体会从最早接手的老工控机到后来 Win10 下用 USB 转串口的小盒子CSerialPort 这个类在我的项目里反复出现过很多次。说句实话它不是一个现代设计意义上的漂亮类没有完整的 RAII、没有智能指针、也没有模板化的事件处理但它胜在一个“稳”字。大多数用它的项目不是追求代码美感而是要一个经过千锤百炼、行为可预期、出问题能在网上搜到解决方案的轮子。我个人给后来者的建议是如果新项目完全从零开始可以选 C#、Qt 或者更现代的 C 串口库但如果老项目已经基于 CSerialPort 跑了好几年完全没必要推倒重来。先把这版 2017-03-12 修正版替换进去解决新编译器兼容问题再在业务层做好收发缓冲和线程生命周期管理它的生命周期还能再续很久。最后分享一个小技巧如果你在公司内部维护多个上位机项目最好把 CSerialPort 这类公共组件抽出来单独作为一个模块用统一版本管理并且保留一份变更记录。这样“同一个串口类在不同项目里演化出三个互相不兼容的版本”这种破事就不会再发生了。我用这个办法收拾过好几台历史遗留电脑亲测有效。本文还有配套的精品资源点击获取