简介:这份源码面向希望掌握 Windows 平台串口通信的 C++ 开发者与在校学生,基于 VS2013 与 MFC 框架,借助 MSComm 控件完成串口的打开关闭、波特率与数据位等参数设置以及数据收发,可作为串口调试工具使用,也可作为理解事件驱动编程的入门范例。压缩包共 29 个文件,约 190KB,以 h 头文件与 cpp 源文件为主体承载界面与通信逻辑,另含 ico、bmp 图标位图资源及 vcxproj、sln 等工程配置,导入 VS2013 即可编译运行。目前已有 686 人学习下载。读者可从中梳理串口初始化、OnComm 事件响应与数据读写的完整链路,并在此基础上扩展多线程收发、错误处理与日志记录,适合作为课程设计或硬件联调的参考起点。
1. 一份能直接编译的 VS2013 C++ 串口助手源码,到底值不值得拆
手上有个工控小项目,需要在上位机里做串口收发、十六进制显示、定时发送,还要能保存日志。网上找了一圈,要么是只有 exe 没有源码,要么是源码缺文件、依赖一堆第三方库,编译报错能排到天亮。这次拿到的是一份 VS2013 + C++ + MFC 的串口助手源码,走的是 MSComm 控件路线,工程文件齐全,打开 sln 就能编译。它解决的不是“串口通信原理”这种教科书问题,而是“我明天就要交一个能跑的上位机 Demo”这种现实诉求。适合两类人:一是刚接触 MFC 串口编程、想拿一份完整工程对照着改的新手;二是需要快速搭上位机原型、不想从零写消息循环的老手。下面按“工程怎么落地 → 参数怎么设 → 坑在哪”的顺序拆。
2. 工程结构与 MSComm 控件接入:先让窗口跑起来
2.1 这份源码的工程骨架长什么样
拿到源码包,第一件事不是急着编译,而是先看目录结构,判断它是不是一个“完整可编译”的工程,而不是别人随手打包的半成品。这份源码是典型的 VS2013 MFC 对话框工程,核心文件大致是这几类:
| 文件类型 | 典型文件 | 作用 |
|---|---|---|
| 解决方案/工程 | .sln、.vcxproj | VS2013 打开入口,决定编译配置 |
| 对话框资源 | .rc、resource.h | 界面布局、控件 ID 定义 |
| 主对话框类 | 主 Dlg 的 .h/.cpp | 串口打开关闭、收发逻辑主战场 |
| MSComm 包装类 | 通常是一个 CMSComm 类 | 对 ActiveX 控件的 C++ 封装 |
| 预编译头 | stdafx.h/.cpp | MFC 工程标配,别删 |
判断工程是否完整,有个很土但有效的办法:用 VS2013 打开 .sln,看“解决方案资源管理器”里有没有带红色感叹号或黄色三角的文件。缺文件会在编译前就暴露,比编译到一半报 LNK 错误好排查得多。另外注意工程属性里的“字符集”设置,MFC 工程分 Unicode 和多字节两套,这份源码如果界面里有中文,多半是 Unicode,改错字符集会冒出一堆 C2664 类型转换错误。
2.2 MSComm 控件的注册与插入
MSComm 是微软早年提供的串口 ActiveX 控件,本质是把 Win32 的串口 API 包了一层,用消息驱动的方式回调数据。VS2013 里它不在默认工具箱里,需要手动注册和插入。常见做法是先把控件注册到系统,再在对话框上插入:
# 以管理员身份运行 cmd,注册 MSComm 控件(32 位系统或 32 位工程) regsvr32 mscomm32.ocx # 如果工程是 32 位而系统是 64 位,注意用 32 位 regsvr32 C:\Windows\SysWOW64\regsvr32.exe mscomm32.ocx注册成功后,在 VS2013 里右键工具箱 →“选择项”→ 浏览到 mscomm32.ocx,勾选后工具箱会多出一个电话图标。把它拖到对话框上,控件 ID 默认是 IDC_MSCOMM1。这一步的坑在于:如果 regsvr32 报“模块加载失败”,多半是 ocx 文件位数和 regsvr32 位数不匹配,或者当前用户没有管理员权限。注册完还拖不出来,就检查 VS 是不是以管理员身份运行。
插入控件后,VS 会自动生成一个 CMSComm 包装类的头文件和 cpp,工程里会多出 mscomm 相关的文件。这时候别急着写业务代码,先编译一次空工程,确认控件包装类能过编译,再往下走。
2.3 串口打开与参数配置的代码骨架
串口助手的核心就三件事:打开串口、配置参数、收发数据。MSComm 把这些浓缩成几个属性和一个事件。下面是一段典型的打开串口逻辑,参数按实际设备改:
// 打开串口并配置基本参数 void CSerialAssistantDlg::OpenPort(int nPort, int nBaud) { if (m_ctrlComm.GetPortOpen()) { m_ctrlComm.SetPortOpen(FALSE); // 先关,避免重复打开 } m_ctrlComm.SetCommPort(nPort); // 串口号,1 对应 COM1 m_ctrlComm.SetInBufferSize(1024); // 接收缓冲区,按数据量调 m_ctrlComm.SetOutBufferSize(1024); // 发送缓冲区 m_ctrlComm.SetSettings(_T("9600,n,8,1")); // 波特率,校验,数据位,停止位 m_ctrlComm.SetInputMode(1); // 1 = 二进制模式,收十六进制必须 m_ctrlComm.SetInputLen(0); // 0 = 一次读空整个缓冲区 m_ctrlComm.SetRThreshold(1); // 收到 1 字节就触发 OnComm m_ctrlComm.SetPortOpen(TRUE); // 真正打开 }这段代码里几个参数值得单独说。SetInputMode(1)是二进制模式,收十六进制数据必须设成 1,设成 0 是文本模式,遇到 0x00 会截断,这是新手最容易翻车的地方。SetInputLen(0)配合SetRThreshold(1),意思是“来一个字节就通知我,通知时把缓冲区读空”,适合实时性要求高的场景;如果数据包很大、触发太频繁,可以把 RThreshold 调大,比如 10,攒够 10 字节再回调。SetSettings的字符串格式是“波特率,校验位,数据位,停止位”,校验位 n 是无校验、e 偶校验、o 奇校验,写错一个字母串口就打不开,而且 MSComm 不会给你明确报错,只会让 PortOpen 返回失败。
打开失败时,先查串口号是否被占用(别的软件开着同一个 COM),再查波特率是否和设备匹配,最后查 USB 转串口驱动是否装好。这三步能解决八成“打不开串口”的问题。
3. 收发逻辑与十六进制处理:数据对不对,全在这几行
3.1 OnComm 事件里的接收分支
MSComm 的数据到达是通过 OnComm 事件通知的,工程里会有一个OnCommMscomm1函数,所有串口状态变化都从这里进来。接收逻辑的关键是先判断事件类型,再读数据:
void CSerialAssistantDlg::OnCommMscomm1() { if (m_ctrlComm.GetCommEvent() == 2) { // 2 = 接收事件 VARIANT variant_inp = m_ctrlComm.GetInput(); COleSafeArray safearray_inp = variant_inp; LONG len = safearray_inp.GetOneDimSize(); CString strRecv; for (LONG i = 0; i < len; i++) { BYTE bt; safearray_inp.GetElement(&i, &bt); CString tmp; tmp.Format(_T("%02X "), bt); // 十六进制显示 strRecv += tmp; } // 追加到接收框,注意跨线程/重入问题 m_editRecv.SetSel(-1, -1); m_editRecv.ReplaceSel(strRecv); } }GetCommEvent() == 2是接收事件的固定值,其他常见值还有 1(发送完成)、3(CTS 变化)、4(DSR 变化)。GetInput()返回的是 VARIANT,必须转成 COleSafeArray 才能逐字节取。这里有个血泪经验:如果接收框是 CEdit 且数据量大,每来一包就 ReplaceSel 一次,界面会卡;正确做法是攒一批再刷,或者用SetRedraw(FALSE)包一下。另外 OnComm 是消息回调,不要在里面对控件做耗时操作,否则会丢后续事件。
3.2 十六进制发送的转换与校验
发送侧比接收侧更容易出错,因为用户输入的是“AA BB 01”这种带空格的字符串,得先解析成字节数组再发。下面是一个稳妥的转换函数:
// 把 "AA BB 01" 转成字节数组,返回实际字节数 int HexStrToBytes(const CString& strHex, BYTE* pOut, int nMax) { int nCount = 0; CString str = strHex; str.Remove(_T(' ')); // 去掉空格 str.MakeUpper(); if (str.GetLength() % 2 != 0) return -1; // 奇数长度非法 for (int i = 0; i < str.GetLength() && nCount < nMax; i += 2) { CString byteStr = str.Mid(i, 2); // 校验每个字符都是合法十六进制 for (int k = 0; k < 2; k++) { TCHAR c = byteStr[k]; if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'))) return -1; } pOut[nCount++] = (BYTE)_tcstoul(byteStr, NULL, 16); } return nCount; }这个函数做了三件事:去空格、校验长度奇偶、逐字符校验合法性。很多现成源码省掉了合法性校验,用户输入“GG”就直接崩或者发出错误数据。发送时用SetOutput传 COleVariant,注意传的是字节数组而不是字符串:
BYTE buf[256]; int n = HexStrToBytes(m_strSend, buf, sizeof(buf)); if (n > 0) { CByteArray arr; arr.SetSize(n); for (int i = 0; i < n; i++) arr[i] = buf[i]; m_ctrlComm.SetOutput(COleVariant(arr)); // 二进制发送 }SetOutput传 CByteArray 走的是二进制通道,传 CString 走的是文本通道,两者在接收端表现不同。发十六进制命令必须用 CByteArray,否则 0x0D 会被当成回车处理,设备收到的数据就变了。
3.3 定时发送与日志落盘
定时发送用 MFC 的SetTimer就够,不需要另起线程。在对话框初始化时SetTimer(1, 1000, NULL),在OnTimer里判断复选框状态再调发送函数。注意定时器 ID 别和控件冲突,发送前要判断串口是否已打开,否则 SetOutput 会抛异常。
日志落盘常见做法是在接收回调里把原始字节追加写文件,用CStdioFile以追加模式打开,写完即关,避免程序崩溃时日志丢失。文件路径建议放在 exe 同目录的 log 子目录,文件名带日期,方便按天排查。写文件不要在 OnComm 里同步做,数据量大时会拖慢接收,可以丢到一个队列里由定时器批量写。
4. 避坑与排查:这几个坑我替你踩过了
4.1 编译报“无法打开 mscomm32.ocx”或控件类未定义
现象:工程能打开,但编译时报 CMSComm 未定义,或者运行时对话框创建失败。原因:MSComm 控件没注册,或者工程位数和控件位数不匹配。解决:先用 SysWOW64 下的 regsvr32 注册 32 位控件,确认工具箱能拖出控件;再检查工程属性里的“平台”是 Win32 还是 x64,MSComm 只有 32 位版本,x64 工程用不了,必须切回 Win32 重新生成。
4.2 收十六进制数据被截断,0x00 之后全丢
现象:设备发01 00 02,接收框只显示01。原因:SetInputMode设成了 0(文本模式),文本模式遇到 0x00 视为字符串结束。解决:打开串口前强制SetInputMode(1),并且接收时按字节数组处理,不要用 CString 直接接 GetInput 的返回值。
4.3 串口打开成功但收不到数据
现象:PortOpen 返回 TRUE,设备也在发,但 OnComm 不触发。原因:RThreshold 设成了 0,表示不触发接收事件;或者 InputLen 设成了非 0 且数据长度不够。解决:把 RThreshold 设为 1,InputLen 设为 0,确保“来一个字节就通知、通知时读空”。另外检查硬件流控,如果设备要求 RTS/CTS 而代码没开,数据会被挡在门外。
4.4 界面卡死或接收丢包
现象:数据量大时界面无响应,或者接收框内容缺段。原因:在 OnComm 回调里直接操作 CEdit 且数据量大,消息队列被刷屏。解决:接收回调里只做数据拷贝进缓冲区,界面刷新交给定时器批量做;或者用SetRedraw(FALSE)包住批量追加,刷完再SetRedraw(TRUE)和Invalidate。
4.5 换台电脑就编译不过
现象:在自己机器上好好的,拷到同事电脑上编译报一堆错。原因:VS2013 的 MFC 组件没装全,或者平台工具集版本不一致。解决:让对方在 VS 安装程序里勾选“MFC 和 ATL 支持”,工程属性里把“平台工具集”统一成 Visual Studio 2013 (v120),别混用 v140/v141。
5. 进阶:把这份源码改成能长期用的上位机底座
拿到一份能跑的串口助手源码,价值不在“能收发”,而在“能当底座改”。我一般会做三件事让它从 Demo 变成能交付的东西。
第一件是参数持久化。把串口号、波特率、校验位、上次发送内容写进 ini 或注册表,下次打开自动恢复。用WritePrivateProfileString写 ini 最省事,路径放 exe 同目录,换机器拷走也不丢配置。第二件是收发分离。接收回调只负责把字节塞进环形缓冲区,界面刷新和日志写盘各用一个定时器消费,这样即使界面卡一下也不丢数据。第三件是加一层协议解析。原始字节流直接显示只能调试用,真正对接设备时要在接收侧加帧头帧尾判断和校验,把“收字节”升级成“收帧”。
验证改造成果有个笨办法但很有效:拿一个 USB 转串口模块,把 TX 和 RX 短接,做自发自收测试。发AA 55 01 02,看接收框是否原样回来,再故意发奇数长度的十六进制串,看是否被正确拒绝。这个自环测试能同时验证发送转换、接收解析和缓冲区处理三条链路,比对着设备猜问题快得多。
从那以后我每次拿到这类串口源码,都先做自环测试再碰真实设备,省下来的排查时间比改代码还多。希望这份拆解帮到你,少走几个我走过的弯路。
本文还有配套的精品资源,点击获取