☰
多串口并发测试工具CommTest:VC6多线程串口通信工程实践
2026/10/14 15:33:55 网站建设 项目流程

简介:CommTest是一款面向嵌入式开发、工业自动化与物联网设备调试人员的多串口通信测试工具,针对多路串口并行传输场景下的性能验证与故障排查需求而设计。资源包共41个文件,约7.21MB,以exe可执行程序为主,辅以h头文件、cpp源码、obj与pdb调试文件及rc资源脚本等,既可直接运行多个测试版本,也便于二次开发与参数定制。工具支持多串口同时通信与多线程独立运行,可设置波特率、停止位、校验方式等参数,并具备误码率计算、丢包检测等评估能力,帮助工程师在产品设计阶段验证通信稳定性、在维护阶段快速定位串口故障。目前已有557人学习下载,适合希望掌握串口通信与多线程测试技术的中高级开发者参考实践。

1. 多串口并发测试的工程价值:为什么我留着这套 VC6 老工程

去年帮一个做工业数据采集的朋友排查产线问题,现场 12 台设备通过 RS-232 转 USB 集线器接到一台工控机,上位机每隔 200ms 轮询一次。现象很怪:单台设备通信正常,一旦超过 6 台同时在线,就开始随机丢包,换线、换集线器、换驱动都试过,问题依旧。后来我把 CommTest 这套工具翻出来,用它同时开了 12 个串口做并发收发,10 分钟就定位到是上位机串口读取线程阻塞导致的缓冲区溢出——这就是多串口多线程串口测试工具存在的意义。

CommTest 是一套基于 MFC 和 MSComm 控件的串口测试工程,从工程文件结构看是典型的 VC6 时代产物(.dsp/.dsw 工程文件、mscomm.h/mscomm.cpp 封装类、cnComm.h 扩展头文件),但它解决的核心问题至今没过时:同时打开多个串口,每个串口跑独立线程,互不阻塞地做收发测试。它适合三类人:一是做嵌入式/工控上位机开发、需要验证多串口并发稳定性的工程师;二是维护老旧 VC6 项目、需要参考串口线程模型的开发者;三是做设备老化测试、需要长时间跑多串口吞吐的测试人员。这套工程的价值不在于界面多漂亮,而在于它把多串口多线程的骨架搭好了,你改改参数就能用。

2. 工程结构与线程模型:从 .dsp 到 mscomm 封装类

拿到一个 .rar 压缩包,别急着双击 exe。先看目录结构,这套工程的代码组织方式决定了你后面改代码时会不会翻车。

2.1 文件清单与模块划分

解压后大致分三层:工程配置层、核心代码层、编译产物层。工程配置层是 VC6 的标准三件套——.dsp(项目文件)、.dsw(工作区文件)、.opt(用户配置),这三个文件决定了编译选项和依赖关系。核心代码层是真正要读的部分,我按职责拆一下:

文件职责改动频率
CommTestDlg.cpp/.h主对话框逻辑,串口开关、参数设置、UI 刷新高
mscomm.cpp/.hMSComm 控件封装类,串口打开/关闭/读写中
cnComm.h串口配置扩展,波特率/校验/停止位定义低
CommTest.cpp/.h应用入口,MFC 初始化低
StdAfx.cpp/.h预编译头不动
resource.h / .rc资源定义,对话框控件 ID按需

编译产物层里有一堆 exe,CommTest16Bp400ms.exe、CommTest16Bp200ms.exe、CommTest16Bp800ms3s.exe、CommTest500.exe、CommTest1500.exe、CommTest961.exe、CommTestOC.exe、CommTestOPen.exe——从命名能看出这是不同测试场景的编译版本,Bp 大概率是 Baud/bit per 的缩写,后面的数字是波特率或超时参数。这些 exe 可以直接跑,但你要改逻辑就得回到源码。

2.2 多线程串口收发的实现骨架

MSComm 控件本身是 ActiveX,单线程里用没问题,但多个串口同时跑就会互相卡。这套工程的做法是每个串口实例配一个工作线程,线程里循环读缓冲区。核心逻辑在CommTestDlg.cpp的串口打开和线程启动部分,我按常见实现还原一下骨架:

// 串口配置结构,每个串口一份 struct SSerialPort { UINT nPortNo; // 串口号,1~N DWORD dwBaudRate; // 波特率,如 9600/115200 BYTE byParity; // 校验位,0-4 BYTE byStopBits; // 停止位 BYTE byDataBits; // 数据位 BOOL bRunning; // 线程运行标志 HANDLE hThread; // 工作线程句柄 CMSComm m_Com; // MSComm 封装对象 }; // 线程函数:每个串口独立跑 UINT SerialWorkerThread(LPVOID pParam) { SSerialPort* pPort = (SSerialPort*)pParam; while (pPort->bRunning) { // 读缓冲区事件,MSComm 的 GetInBufferCount if (pPort->m_Com.GetInBufferCount() > 0) { VARIANT vData = pPort->m_Com.GetInput(); // 处理收到的数据,做计数/校验/落盘 ProcessRxData(pPort->nPortNo, vData); } // 发送队列非空则发 if (HasTxData(pPort->nPortNo)) { pPort->m_Com.SetOutput(GetTxData(pPort->nPortNo)); } Sleep(1); // 让出 CPU,避免空转吃满 } return 0; }

这段代码的关键点有三个。第一,bRunning标志必须用volatile或加锁,否则主线程改了标志工作线程可能读不到,这是 VC6 多线程的经典坑。第二,Sleep(1)不是随便加的,MSComm 的GetInBufferCount是轮询式查询,不加延时 CPU 直接飙到 100%,但加多了又影响实时性,1ms 是常见折中。第三,GetInput()返回的是VARIANT类型,里面可能是字节数组也可能是字符串,取决于InputMode设置,处理时要先判断类型。

2.3 编译环境与依赖确认

这套工程是 VC6 的,用 VS2019/2022 直接打开会提示升级,升级后 MSComm 控件可能注册不上。我的建议是:如果只是跑测试,直接用现成的 exe;如果要改代码,装一个 VC6 或者用 VS2010 兼容模式。MSComm 控件(mscomm32.ocx)需要注册:

# 以管理员身份运行 cmd,注册 MSComm 控件 regsvr32 mscomm32.ocx # 如果提示找不到文件,先确认 ocx 在系统目录 copy mscomm32.ocx C:\Windows\SysWOW64\ regsvr32 C:\Windows\SysWOW64\mscomm32.ocx

注册成功后,在 VC6 里打开.dsw文件,检查 Project Settings 里的链接库是否包含mscomm32.lib。如果编译报CMSComm未定义,说明 mscomm.h 没被正确包含,检查StdAfx.h里有没有#include "mscomm.h"。

提示:VC6 在 Win10/Win11 上安装本身就有兼容性问题,如果只是临时改代码,用 VS2010 打开并升级工程更省事,但升级后要手动重新添加 MSComm 控件到对话框。

3. 多串口并发配置:参数设置与线程调度实操

工程能编译只是第一步,真正要让它跑出有意义的测试结果,得把串口参数和线程调度调对。这一章讲具体怎么配、怎么改、怎么验证。

3.1 串口参数配置与代码修改点

每个串口的参数在CommTestDlg.cpp的初始化函数里设置,常见做法是在OnInitDialog里遍历串口列表,逐个配置。MSComm 的参数设置通过SetSettings完成,格式是"波特率,校验,数据位,停止位":

// 在 OnInitDialog 或串口打开函数里配置 BOOL CCommTestDlg::OpenSerialPort(int nPort, int nBaud) { CString strSettings; // 格式:波特率,校验(N/O/E),数据位,停止位 strSettings.Format(_T("%d,n,8,1"), nBaud); m_Com.SetCommPort(nPort); // 串口号 m_Com.SetInBufferSize(4096); // 接收缓冲区,默认 1024 偏小 m_Com.SetOutBufferSize(4096); // 发送缓冲区 m_Com.SetInputMode(1); // 1=二进制,0=文本 m_Com.SetInputLen(0); // 0=一次读全部缓冲区 m_Com.SetRThreshold(1); // 收到 1 字节就触发事件 m_Com.SetSThreshold(0); // 发送不触发事件 m_Com.SetSettings(strSettings); // 应用参数 m_Com.SetPortOpen(TRUE); // 打开串口 return m_Com.GetPortOpen(); }

参数说明:SetInBufferSize默认 1024 字节,多串口高频收发时很容易溢出,改成 4096 或 8192 更稳。SetInputMode(1)是二进制模式,如果你测试的是文本协议(如 AT 指令),改成 0 更方便。SetRThreshold(1)表示收到 1 字节就触发OnComm事件,但注意——如果你用的是独立线程轮询,这个阈值设不设影响不大,因为线程是主动查GetInBufferCount。SetInputLen(0)表示一次读取全部缓冲区内容,设成固定值会导致数据被截断。

3.2 线程数量与调度策略

多串口多线程的核心矛盾是:线程开多了,CPU 上下文切换开销大;线程开少了,串口等待时间长。这套工程默认每个串口一个线程,12 个串口就是 12 个线程。在普通工控机上,12 个线程完全扛得住,但如果串口数超过 32,建议改成线程池模式。

线程优先级也值得调。默认Normal优先级下,如果某个串口数据量大,可能挤占其他串口的时间片。常见做法是把串口工作线程设为THREAD_PRIORITY_ABOVE_NORMAL:

// 创建线程时指定优先级 hThread = AfxBeginThread(SerialWorkerThread, pPort, THREAD_PRIORITY_ABOVE_NORMAL, // 略高于普通 0, CREATE_SUSPENDED); if (hThread) { hThread->m_bAutoDelete = FALSE; // 手动管理生命周期 hThread->ResumeThread(); }

CREATE_SUSPENDED标志让线程创建后先挂起,等你把SSerialPort结构体填完再ResumeThread,避免线程启动时读到未初始化的数据。m_bAutoDelete = FALSE是为了在线程退出时能手动WaitForSingleObject确认它真的结束了,否则可能出现线程还在跑、对象已经析构的崩溃。

3.3 收发测试与结果验证

配置好之后,怎么验证多串口并发是否正常?我的做法是:用两个 USB 转串口模块做回环测试,一个模块的 TX 接另一个模块的 RX,然后 CommTest 同时打开两个串口,一个发一个收,对比发送字节数和接收字节数是否一致。

// 发送端:定时发送递增序列 void CCommTestDlg::OnTimerSend() { static BYTE seq = 0; BYTE buf[256]; for (int i = 0; i < 256; i++) { buf[i] = seq++; // 递增序列,方便接收端校验 } for (int port = 0; port < m_nPortCount; port++) { if (m_ports[port].bRunning) { m_ports[port].m_Com.SetOutput(COleVariant(buf, 256)); } } } // 接收端:校验序列连续性 void ProcessRxData(int nPort, VARIANT& vData) { // 将 VARIANT 转为字节数组 BYTE* pData = (BYTE*)vData.parray->pvData; long nLen = vData.parray->rgsabound[0].cElements; for (long i = 0; i < nLen; i++) { if (pData[i] != m_expectedSeq[nPort]) { m_nErrorCount[nPort]++; // 序列不连续,计一次错 } m_expectedSeq[nPort] = pData[i] + 1; } m_nRxCount[nPort] += nLen; }

跑 30 分钟,如果m_nErrorCount为 0 且m_nRxCount等于m_nTxCount,说明并发收发稳定。如果有错,先看是不是波特率不匹配,再看缓冲区是否溢出(GetInBufferCount是否经常接近InBufferSize)。

注意:回环测试时两个串口的波特率必须完全一致,且停止位、校验位也要一致,否则收到的就是乱码,别问我怎么知道的。

4. 避坑与排查:多串口测试里那些翻车现场

这套工程我用了不下 20 次,踩过的坑能写满一页 A4 纸。挑 5 个最典型的,按「现象 → 原因 → 解决」列出来,你遇到问题时直接对号入座。

4.1 串口打开失败,提示“端口已被占用”

现象:调用SetPortOpen(TRUE)后GetPortOpen()返回 FALSE,或者直接弹异常。

原因:三种可能——串口被其他程序占用(如串口调试助手没关)、串口号不存在(USB 转串口没插好)、MSComm 控件没注册。

解决:先用设备管理器确认串口号,再用CreateFile测试串口是否可打开:

HANDLE hTest = CreateFile(_T("COM3"), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (hTest == INVALID_HANDLE_VALUE) { DWORD dwErr = GetLastError(); // ERROR_ACCESS_DENIED = 被占用 // ERROR_FILE_NOT_FOUND = 串口不存在 } CloseHandle(hTest);

如果CreateFile能打开但 MSComm 打不开,检查控件注册。

4.2 多串口同时收发时数据错乱

现象:A 串口收到的数据出现在 B 串口的显示框里,或者数据内容对不上。

原因:线程函数里用了全局变量或静态变量存串口数据,多个线程竞争同一块内存。

解决:每个串口的接收缓冲区必须是独立的,放在SSerialPort结构体里,不要用static或全局数组。如果确实需要共享计数器,用InterlockedIncrement做原子操作。

4.3 程序运行一段时间后无响应

现象:跑十几分钟后界面卡死,但串口灯还在闪。

原因:工作线程里做了 UI 操作(如直接SetWindowText),MFC 的 UI 更新必须在主线程做,跨线程直接调会死锁。

解决:工作线程只负责收数据、存缓冲区,通过PostMessage通知主线程刷新 UI:

// 工作线程里 ::PostMessage(pDlg->m_hWnd, WM_UPDATE_UI, nPort, 0); // 主线程消息处理 LRESULT CCommTestDlg::OnUpdateUI(WPARAM wParam, LPARAM lParam) { int nPort = (int)wParam; UpdatePortDisplay(nPort); // 安全地刷新界面 return 0; }

4.4 高波特率下丢包严重

现象:115200 波特率下,接收字节数明显少于发送字节数。

原因:Sleep(1)的轮询间隔在高波特率下不够快,115200 波特率约每 87μs 传 1 字节,1ms 的轮询间隔意味着缓冲区可能已经堆了几十个字节,如果InBufferSize不够大就会溢出。

解决:两个方向——把SetInBufferSize调到 8192 或更大;把Sleep(1)改成WaitForSingleObject等待串口事件,或者用OnComm事件驱动代替轮询。事件驱动模式下,SetRThreshold(1)才有意义。

4.5 编译报错“无法打开预编译头文件”

现象:换了一台机器编译,报fatal error C1083: Cannot open precompiled header file。

原因:CommTest.pch是预编译头产物,换机器后路径变了或者文件损坏。

解决:删除Debug目录下的.pch文件,在 VC6 里执行Rebuild All。如果还不行,检查StdAfx.cpp的编译选项里是不是设了Create Precompiled Header。

5. 从测试工具到产线工具:参数化改造与长时间跑测技巧

这套工程直接拿来跑 demo 没问题,但要放到产线上做老化测试,还得做几处改造。我一般会做三件事:参数外置、日志落盘、异常自恢复。

参数外置是把串口号、波特率、发送间隔这些从代码里挪到配置文件,改参数不用重新编译。常见做法是读一个config.ini:

// 读取配置文件 int nPortCount = GetPrivateProfileInt(_T("Global"), _T("PortCount"), 4, _T(".\\config.ini")); for (int i = 0; i < nPortCount; i++) { CString strKey; strKey.Format(_T("Port%d"), i); m_ports[i].nPortNo = GetPrivateProfileInt(strKey, _T("PortNo"), i+1, _T(".\\config.ini")); m_ports[i].dwBaudRate = GetPrivateProfileInt(strKey, _T("Baud"), 9600, _T(".\\config.ini")); }

日志落盘是长时间跑测的后悔药。每个串口的收发计数、错误计数、时间戳,每隔 10 秒写一次 CSV,跑 72 小时后直接看日志就能定位问题发生在哪个时间段。我习惯用CStdioFile追加写,注意多线程写同一个文件要加锁,或者每个串口写独立文件。

异常自恢复是产线工具和 demo 的分水岭。串口在长时间运行后可能因为驱动或硬件原因掉线,工具要能检测到并自动重连。我的做法是在工作线程里加一个心跳检测:如果连续 30 秒没有收到任何数据且发送队列非空,就关闭串口重新打开。

// 心跳检测,放在工作线程循环里 if (GetTickCount() - pPort->dwLastRxTime > 30000 && pPort->bTxPending) { pPort->m_Com.SetPortOpen(FALSE); // 关闭 Sleep(500); // 等驱动释放 pPort->m_Com.SetPortOpen(TRUE); // 重开 pPort->dwLastRxTime = GetTickCount(); }

最后说一个验证技巧:跑长时间测试时,用另一个串口工具(或者 CommTest 自己开一个串口)监听被测串口的 TX 线,对比两边计数。如果 CommTest 显示发了 100 万字节,监听端只收到 99.8 万,那 0.2 万就是真实丢包,不是工具统计错误。从那以后我每次做多串口老化测试,都强制在外部挂一个独立监听串口做交叉验证,再也没被“工具显示正常但设备实际丢包”坑过。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询