简介这份源码面向希望掌握 Windows 平台串口通信的 C 开发者基于 VS2013 与 MFC 框架借助 MSComm 控件实现串口的打开关闭、波特率与数据位等参数配置以及数据收发可作为串口调试工具或集成到更大项目中的通信模块。压缩包共 29 个文件、约 190KB以 h 头文件与 cpp 源文件为核心配合 vcxproj、sln 等工程配置以及 ico、bmp 界面图标资源和 rc 资源脚本另有少量 log 与 db 文件结构完整、便于直接编译运行。目前已有 686 人学习下载。读者可从中了解串口初始化、Input/Output 收发与 OnComm 事件驱动等关键实现并在此基础上扩展多线程收发、错误处理与日志记录适合作为串口编程与 MFC 界面开发的实践参考。1. 从一台老设备说起为什么我还在翻 VS2013 的串口助手源码上周去现场调一台工控设备笔记本上装的是 VS2013客户那边只认 MFC 界面串口调试工具又不让随便装第三方软件。翻出这份 VS2013 C 串口助手源码编译、改波特率、加个十六进制收发半小时就把现场问题定位了。这份资源就是一个基于 MFC 和 MSComm 控件的串口调试助手完整工程能收发数据、配置串口参数、做十六进制与 ASCII 切换适合还在维护老工控项目、需要快速改一个串口工具的开发者。它不是新框架玩具是能直接塞进 VS2013 打开就编的实战底子新手照着能跑通熟手能看清 MFC 消息循环和串口事件怎么咬合。2. 工程结构与技术选型MFC 对话框 MSComm 的组合拳2.1 为什么是 MFC 对话框而不是控制台这份源码的骨架是典型的 MFC 对话框程序不是控制台也不是 Win32 SDK 裸写。原因很实际串口助手需要一堆控件——波特率下拉框、打开/关闭按钮、接收区编辑框、发送区编辑框、十六进制勾选框用对话框资源拖拽布局最快消息映射ON_BN_CLICKED、ON_CBN_SELCHANGE也比手写窗口过程清晰。工程里CSerialAssistantApp负责应用启动CSerialAssistantDlg承载全部界面逻辑这是 MFC 对话框程序的标准分工。选 MFC 还有个隐性好处它和 VS2013 的向导深度绑定新建对话框类、添加成员变量、绑定控件 ID 都能自动生成代码改起来不用记一堆 API。代价是界面风格停留在 Win7 时代但对工控现场来说能跑、稳定、不依赖运行库版本比好看重要得多。2.2 MSComm 控件在工程里的位置MSComm 是微软的串口通信 ActiveX 控件工程通过Insert ActiveX Control把它拖进对话框资源VS 会自动生成CMSComm包装类。核心用法就三件事设置CommPort选串口号、设置Settings配波特率数据位停止位校验位、设置PortOpen开关串口。接收靠OnComm事件发送靠SetOutput写 Variant 类型数据。这里有个血泪经验MSComm 是 32 位控件工程平台必须配成 Win32配成 x64 会直接报「控件未注册」或「类未注册」。VS2013 默认新建工程可能是 Win32但如果你手动改过平台记得在配置管理器里切回来。2.3 目录与文件清单工程文件不多但每个都有明确职责先看清再动手文件作用SerialAssistant.slnVS2013 解决方案入口SerialAssistant.vcxproj工程配置含 MFC 使用方式、字符集设置SerialAssistantDlg.cpp/.h主对话框逻辑串口开关、收发、参数解析全在这SerialAssistant.cpp/.h应用类初始化与退出清理SerialAssistant.rc资源脚本对话框布局、控件 ID、MSComm 控件都在这resource.h控件 ID 宏定义改界面前先看它mscomm32.ocx系统注册串口控件本体需注册到系统提示.rc文件里 MSComm 控件的 CLSID 是固定的别手动改要加控件就在资源视图里拖让 VS 自动维护。2.4 从零跑起来的最小步骤先把环境对齐再谈改代码。步骤不复杂但顺序错了会卡在编译或注册上用 VS2013 打开.sln确认顶部平台是Win32不是x64。检查项目属性 → 配置属性 → 常规 → 「使用 MFC」设为「在共享 DLL 中使用 MFC」或「在静态库中使用 MFC」两者都能编静态库版拷到没装运行库的机器上更省心。确认mscomm32.ocx已注册。没注册的话对话框资源里 MSComm 控件会显示为叉号编译能过但运行创建失败。编译运行界面上选串口号如 COM3、波特率如 9600、数据位 8、停止位 1、无校验点「打开串口」。用另一台机器或虚拟串口对发数据验证接收区能刷出内容。注册控件的命令常见做法是管理员权限跑一条 regsvr32路径按你系统里 ocx 的实际位置来:: 以管理员身份运行注册 MSComm 控件32 位 regsvr32 C:\Windows\SysWOW64\mscomm32.ocx这条命令的含义是把 32 位 ActiveX 控件写入注册表SysWOW64是 64 位系统存放 32 位组件的位置。如果提示「模块加载失败」先确认文件确实在那再确认你是管理员权限如果提示「找不到入口点」多半是拿错了位数版本。注册成功后重新打开 VS资源视图里的控件就能正常实例化。3. 核心代码拆解串口打开、事件接收与数据发送3.1 串口参数读取与打开流程打开串口不是一句PortOpen TRUE就完事参数得从界面控件逐个读出来拼成 MSComm 认识的格式。下面这段是「打开串口」按钮的典型实现我按源码结构还原并加了注释void CSerialAssistantDlg::OnBnClickedBtnOpen() { if (m_ctrlComm.GetPortOpen()) // 已打开则先关闭避免重复打开报错 { m_ctrlComm.SetPortOpen(FALSE); m_btnOpen.SetWindowText(_T(打开串口)); return; } int nPort m_cmbPort.GetCurSel() 1; // 下拉框索引 0 对应 COM1 m_ctrlComm.SetCommPort(nPort); // 设置串口号 CString strSettings; CString strBaud; m_cmbBaud.GetWindowText(strBaud); // 波特率从下拉框文本取如 9600 strSettings.Format(_T(%s,n,8,1), strBaud); // 拼成 9600,n,8,1 m_ctrlComm.SetSettings(strSettings); // 数据位 8、停止位 1、无校验 m_ctrlComm.SetInputMode(1); // 1 二进制接收便于十六进制处理 m_ctrlComm.SetRThreshold(1); // 每收到 1 字节就触发 OnComm m_ctrlComm.SetInputLen(0); // 每次读取全部缓冲区 m_ctrlComm.SetPortOpen(TRUE); // 真正打开串口 m_btnOpen.SetWindowText(_T(关闭串口)); }逻辑上分四步先判断当前状态做开关切换再从控件取串口号和波特率接着拼Settings字符串并设置接收模式最后SetPortOpen(TRUE)。参数说明几个关键点SetInputMode(1)表示按二进制收这样十六进制显示时不用再做编码转换SetRThreshold(1)是接收阈值设成 1 意味着来一个字节就触发一次OnComm实时性好但高频数据下事件会密集SetInputLen(0)表示读缓冲区时一次读空配合GetInBufferCount使用。注意Settings字符串格式是「波特率,校验,数据位,停止位」校验位n是无校验、e偶校验、o奇校验。拼错顺序不会报错但通信会静默失败这是最常见的翻车点之一。3.2 OnComm 事件里的接收与十六进制转换MSComm 的接收全靠OnComm事件工程里通过事件映射把控件事件接到对话框成员函数。核心是判断GetCommEvent()的返回值2表示接收缓冲区有数据void CSerialAssistantDlg::OnCommMscomm() { if (m_ctrlComm.GetCommEvent() 2) // 2 接收事件 { VARIANT variant_inp m_ctrlComm.GetInput(); // 取回 Variant 数据 COleSafeArray safearray_inp variant_inp; // 转安全数组便于逐字节访问 LONG len safearray_inp.GetOneDimSize(); BYTE rxdata[2048] {0}; for (LONG i 0; i len i 2048; i) safearray_inp.GetElement(i, rxdata i); // 逐字节取出 CString strShow; if (m_chkHex.GetCheck()) // 十六进制显示分支 { for (LONG i 0; i len; i) { CString strByte; strByte.Format(_T(%02X ), rxdata[i]); strShow strByte; } } else // ASCII 显示分支 { strShow CString((char*)rxdata, len); } m_editRecv.SetSel(-1, -1); // 光标移到末尾 m_editRecv.ReplaceSel(strShow); // 追加显示不覆盖历史 } }这段代码的骨架是「Variant → COleSafeArray → BYTE 数组 → 按显示模式格式化」。GetInput()返回的是VARIANT因为 MSComm 是 ActiveX 控件数据跨 COM 边界只能用 Variant 传COleSafeArray是 MFC 对 SAFEARRAY 的封装GetOneDimSize拿长度、GetElement按索引取字节。十六进制分支用%02X保证每字节两位、不足补零后面跟空格便于肉眼对齐ASCII 分支直接按字节构造CString但要注意如果数据里含\0CString((char*)rxdata, len)这种带长度的构造才不会截断。ReplaceSel而不是SetWindowText是关键前者在光标处插入保留历史接收内容后者会整体覆盖调试时看不到上下文。这个细节很多新手会写错导致接收区永远只有最后一帧。3.3 发送逻辑与 Variant 构造发送比接收简单但 Variant 的构造容易踩坑。工程里发送按钮的典型写法void CSerialAssistantDlg::OnBnClickedBtnSend() { CString strSend; m_editSend.GetWindowText(strSend); // 取发送区文本 if (strSend.IsEmpty()) return; CByteArray arrSend; if (m_chkHexSend.GetCheck()) // 十六进制发送解析 01 02 AB { strSend.Remove( ); // 去掉空格 for (int i 0; i 1 strSend.GetLength(); i 2) { CString strByte strSend.Mid(i, 2); arrSend.Add((BYTE)_tcstoul(strByte, NULL, 16)); } } else // ASCII 发送逐字符转字节 { for (int i 0; i strSend.GetLength(); i) arrSend.Add((BYTE)strSend[i]); } VARIANT variant_out; variant_out.vt VT_ARRAY | VT_UI1; // 无符号字节数组 variant_out.parray SafeArrayCreateVector(VT_UI1, 0, arrSend.GetSize()); for (LONG i 0; i (LONG)arrSend.GetSize(); i) SafeArrayPutElement(variant_out.parray, i, arrSend[i]); m_ctrlComm.SetOutput(variant_out); // 写入串口 VariantClear(variant_out); // 释放防内存泄漏 }逻辑分两条路十六进制模式先把空格去掉每两个字符用_tcstoul(..., 16)转成一个字节ASCII 模式直接逐字符转字节。然后构造VT_ARRAY | VT_UI1类型的 Variant用SafeArrayCreateVector分配、SafeArrayPutElement填充最后SetOutput发出。VariantClear这行别省SafeArrayCreateVector分配的内存归 Variant 管不清会慢慢泄漏——长时间高频发送时这个泄漏会累积成肉眼可见的内存上涨。参数上要注意_tcstoul的第三个参数16表示按十六进制解析如果发送区里混了非十六进制字符比如用户手滑打了G解析结果会是 0 或截断值发出去的数据就错了。稳妥做法是解析前先校验字符范围工程里如果没做二次开发时补上。3.4 关闭串口与资源清理关闭串口不只是SetPortOpen(FALSE)还要考虑程序退出时的清理。工程里在OnDestroy或对话框关闭时做收尾void CSerialAssistantDlg::OnDestroy() { if (m_ctrlComm.GetPortOpen()) m_ctrlComm.SetPortOpen(FALSE); // 先关串口释放系统句柄 CDialogEx::OnDestroy(); }顺序很重要先关串口再走基类OnDestroy。如果反过来基类销毁时控件可能已经失效GetPortOpen访问悬空对象会崩。这个坑在调试版不一定复现发布版偶发崩溃查起来很费劲。4. 避坑与排查串口助手最容易翻车的五个地方4.1 现象编译报「无法打开包括文件 afxwin.h」原因通常是项目属性里「使用 MFC」没设对或者 VS2013 安装时没勾 MFC 组件。解决项目属性 → 配置属性 → 常规 → 使用 MFC 选「在共享 DLL 中使用 MFC」若仍报错打开 VS 安装程序补装「Microsoft 基础类」。别去手动改包含路径那是治标不治本。4.2 现象运行时报「未注册类」或对话框创建失败MSComm 控件没注册或注册的是 64 位版本而工程是 Win32。解决用管理员权限regsvr32注册SysWOW64下的mscomm32.ocx确认工程平台是 Win32。如果系统里同时有 32/64 位版本注册 32 位那个。4.3 现象串口能打开但收不到数据三个常见原因一是RThreshold设成了 0事件永远不触发二是InputMode设成 0文本模式却按二进制解析数据错乱三是串口号选错比如设备实际是 COM5 而下拉框只列到 COM4。解决先确认SetRThreshold(1)、SetInputMode(1)再用设备管理器核对串口号必要时把下拉框改成可编辑让用户手填。4.4 现象十六进制发送的数据和预期不符多半是解析时没去空格或字符校验缺失。比如输入01 02没Remove( )就会把空格当字符解析_tcstoul遇到空格返回 0发出去变成01 00 02之类。解决解析前统一去空格逐字符校验是否在0-9A-Fa-f范围内非法字符直接提示用户而不是静默发错。4.5 现象长时间运行内存持续上涨SetOutput构造的 Variant 没VariantClear或接收端每次GetInput后没释放。解决发送路径确保VariantClear(variant_out)接收路径COleSafeArray是栈对象会自动析构但VARIANT variant_inp如果手动VariantInit过要对应清理。用任务管理器盯几分钟内存曲线能快速判断是不是泄漏。5. 二次开发改造从能用到好用我的三条改造路线拿到这份源码直接能用但要用得顺手我一般会做三件事按投入产出比排序。第一条是接收区加时间戳和自动滚动开关。现场调试时光看数据不知道哪帧对应哪个操作加时间戳能对上日志。改法是在OnCommMscomm的ReplaceSel前拼一个GetCurrentTime()格式化的前缀再在界面上加个「自动滚动」勾选框勾选时SetSel(-1,-1)追加不勾时保持当前位置不动方便回看历史帧。这个改动半小时能完成收益最大。第二条是串口参数持久化。每次打开都要重选波特率和串口号烦。用WritePrivateProfileString把上次的参数写进 ini程序启动时GetPrivateProfileString读回来初始化下拉框。代码量小但体验提升明显。注意 ini 路径用GetModuleFileName拼程序目录别写死绝对路径否则换台机器就失效。第三条是发送支持定时循环和转义字符。工控调试经常要周期性发同一条指令加个「定时发送」勾选框和间隔输入框用SetTimer驱动。转义字符则是让发送区支持\r\n、\x01这类写法解析时先做转义替换再走十六进制/ASCII 分支。这块改动稍大建议单独抽一个ParseSendString函数别把逻辑堆在按钮响应里。改造后的验证方法很朴素用虚拟串口工具建一对 COM 口一端跑改后的助手一端跑原始版本或脚本对发已知数据比对接收区显示和发送字节是否一致。十六进制模式重点验00、FF、含空格的输入ASCII 模式重点验中文和\0。每改一处就跑一遍这个对照比肉眼盯屏幕靠谱。有个习惯我保持了几年任何串口相关的改动先在虚拟串口上跑通再上真设备。真设备烧一次固件、断一次连接的成本远高于虚拟串口点几下。从那以后我每次改串口收发逻辑都强制走一遍虚拟串口对照确认字节级一致才接硬件。希望这份源码和这些改造思路能帮你在下一个工控现场少熬一个晚上。本文还有配套的精品资源点击获取