VC++ 6.0与MFC串口通信开发实战:从MSComm控件到稳定工具
1. 项目概述与核心思路
在嵌入式、工控、仪器仪表等硬件开发领域,串口通信是工程师与设备“对话”最基础、最可靠的方式之一。无论是调试MCU、配置FPGA,还是与传感器、PLC进行数据交互,一个稳定、直观的上位机软件都是不可或缺的“瑞士军刀”。虽然如今Python、C#、QT等现代工具大行其道,但在许多遗留系统、教学环境或对运行环境有严格要求的工业场景中,基于VC++ 6.0和MFC开发的串口工具依然有其独特的生命力。它生成的可执行文件体积小巧,不依赖庞大的运行时库,在Windows XP乃至更早的工控机上都能稳定运行,这种“复古”的技术栈恰恰解决了一些现实中的痛点。
这个项目,就是带你从零开始,在VC++ 6.0环境下,使用经典的MSComm串口控件,打造一个具备基本收发功能的对话框应用程序。整个过程就像组装一台老式收音机,步骤清晰,逻辑直接,但每一步都蕴含着对Windows消息机制、ActiveX控件以及串口通信原理的理解。我将不仅复现操作步骤,更会深入解释每个设置背后的“为什么”,并分享我在多年硬件调试中积累的、关于串口通信稳定性和健壮性的实战经验。无论你是刚接触Windows编程的嵌入式工程师,还是需要维护老旧代码的开发者,这篇详尽的指南都能让你获得一个立即可用、并可深度定制的串口工具原型。
2. 开发环境搭建与项目初始化
2.1 VC++ 6.0环境确认与MSComm控件准备
首先,确保你的VC++ 6.0安装完整。很多精简版或绿色版可能会缺失ActiveX控件支持,而这恰恰是使用MSComm控件的关键。一个快速的验证方法是:打开VC++ 6.0,尝试新建一个基于对话框的MFC应用,然后查看“Project” -> “Add to Project” -> “Components and Controls...”菜单是否可用。如果不可用,或者后续在控件列表中找不到MSComm,那么你可能需要重新运行安装程序,在自定义安装中务必勾选“ActiveX Controls”和“MFC”相关组件。
注意:在Windows 10或Windows 11等高版本系统上安装和运行VC++ 6.0可能会遇到兼容性问题。一个常见的解决方案是以兼容模式(如Windows XP SP3)运行安装程序,并以管理员身份启动开发环境。有时,直接使用已经配置好的绿色便携版反而更省事。
MSComm控件,即Microsoft Communications Control,是一个封装了串口底层API(如 CreateFile , ReadFile , WriteFile )的ActiveX控件。它通过事件驱动的方式,让开发者可以像处理按钮点击一样处理串口数据的到达,极大地简化了编程模型。其核心原理是:控件内部维护着接收和发送缓冲区,并创建一个后台线程监控串口状态。当指定事件(如接收到指定字符数)发生时,它会向主程序发送一个Windows消息,触发我们编写的事件处理函数。
2.2 创建MFC对话框应用程序框架
启动VC++ 6.0,点击“File” -> “New”。在“Projects”选项卡中,选择“MFC AppWizard (exe)”。在“Project name”中输入“SCommTest”,并选择合适的保存位置。点击“OK”进入向导。
在Step 1中,选择“Dialog based”,即基于对话框的应用程序。这种类型最适合制作工具软件,它没有复杂的文档视图结构,启动后就是一个主界面。点击“Next”。
在Step 2中,可以保持默认设置。这里需要注意几个选项:“About box”建议保留,这是一个关于对话框;“Context-sensitive Help”如果不需要可以取消,以简化项目。点击“Next”直到Step 4。
在Step 4中,取消“Docking toolbar”、“Initial status bar”和“Printing and print preview”的勾选,因为我们这个简单的串口工具不需要这些功能。确保“ActiveX Controls”复选框是被勾选的,这是支持MSComm控件的基础。点击“Finish”,然后“OK”,项目框架就创建完毕了。
此时,IDE会生成 CSCommTestApp (应用类)、 CSCommTestDlg (主对话框类)等核心文件,并自动打开对话框资源编辑器,显示一个带有“OK”和“Cancel”按钮的空白对话框。这就是我们后续操作的主舞台。
3. 引入并配置MSComm串口控件
3.1 插入MSComm控件到项目
这是关键一步。在资源编辑器中,确保对话框(IDD_SCOMMTEST_DIALOG)处于打开状态。点击菜单栏的“Project” -> “Add to Project” -> “Components and Controls...”。
这时会弹出一个文件对话框,定位到VC++ 6.0安装目录下的“Common\MSDev98\Gallery”目录可能更快。在弹出的“Components and Controls Gallery”对话框中,双击“Registered ActiveX Controls”文件夹。系统会花一点时间枚举所有已注册的ActiveX控件,请耐心等待。
在长长的控件列表中,找到“Microsoft Communications Control, version 6.0”。如果找不到,原因如前所述,是VC++安装不完整。选中它,点击“Insert”按钮。会弹出一个确认对话框,询问是否插入该组件,点击“OK”。接着会提示你为这个控件生成的包装类命名,通常保持默认的“CMSComm”即可,点击“OK”完成插入。
插入成功后,你会发现在工作区的“ClassView”标签页中,多了一个 CMSComm 类。同时,在控件工具箱(通常位于IDE左侧或右侧)的最后,会出现一个电话听筒模样的新图标,这就是MSComm控件的UI标识。
3.2 将控件放置到对话框并关联变量
用鼠标将工具箱中的电话图标拖拽到对话框资源上。放置后,它显示为一个小的电话图标,但这个图标在程序运行时是不可见的,它只是一个设计时的占位符。你可以右键点击它,选择“Properties”,将其ID修改为更易识别的“IDC_MSCOMM1”。
接下来,我们需要为这个控件在对话框类中定义一个控制变量,以便在代码中操作它。按下快捷键“Ctrl+W”打开“MFC ClassWizard”(如果菜单失效,有时需要先关闭所有打开的文件再试)。切换到“Member Variables”选项卡。
在“Class name”下拉框中,确保选中了“CSCommTestDlg”。在下面的控件ID列表中,找到“IDC_MSCOMM1”,选中它,点击“Add Variable...”按钮。在弹出的对话框中,“Member variable name”填写为“m_ctrlComm”,“Category”选择“Control”,“Variable type”会自动变为“CMSComm”。点击“OK”完成添加。
这个操作做了两件事:一是在 CSCommTestDlg 类中声明了一个 CMSComm 类型的成员变量 m_ctrlComm ;二是在对话框的头文件( SCommTestDlg.h )中自动添加了 #include "mscomm.h" 语句。你现在可以打开 SCommTestDlg.h 文件,在 //{{AFX_INCLUDES() 和 //}}AFX_INCLUDES() 注释对之间确认这一点。
4. 设计用户界面与关联变量
4.1 添加必要的控件
回到对话框资源编辑器,我们需要添加用户交互的控件。
- 接收数据显示框 :从工具箱拖拽一个“Edit Control”(编辑框)到对话框。右键点击它,选择“Properties”。在“General”页,将ID改为“IDC_EDIT_RXDATA”。切换到“Styles”页, 务必勾选“Multiline”(多行)和“Vertical scroll”(垂直滚动条) ,这样它才能显示多行文本并支持滚动。你可以拉大这个编辑框,以便显示更多内容。
- 发送数据输入框 :再拖拽一个编辑框。将其ID改为“IDC_EDIT_TXDATA”。同样,如果需要输入多行发送数据(例如发送一帧包含换行符的指令),也勾选“Multiline”。
- 手动发送按钮 :拖拽一个“Button”控件。将其ID改为“IDC_BUTTON_MANUALSEND”,标题(Caption)改为“手动发送”。
- 静态文本 (可选但推荐):为了界面友好,可以在两个编辑框旁边添加“Static Text”控件,分别标注“接收区:”和“发送区:”。 调整这些控件的位置和大小,形成一个清晰的布局,例如接收框在上方,发送框在中间,按钮在下方。
4.2 为编辑框关联数据变量
我们需要将编辑框的内容与程序中的字符串变量绑定起来,以便读写。再次打开“MFC ClassWizard”(Ctrl+W),确保仍在“Member Variables”选项卡和“CSCommTestDlg”类。
- 在控件ID列表中找到“IDC_EDIT_RXDATA”,点击“Add Variable...”。“Member variable name”填写为“m_strRXData”,“Category”选择“Value”,“Variable type”选择“CString”。点击“OK”。这个
m_strRXData变量将用于存储和显示接收到的数据。 - 同样,为“IDC_EDIT_TXDATA”添加一个“Value”类型的“CString”变量,命名为“m_strTXData”。它将用于存储待发送的文本。
至此,界面与数据的桥梁已经搭建好。当调用 UpdateData(TRUE) 时,对话框上编辑框的内容会自动更新到 m_strTXData 变量中;调用 UpdateData(FALSE) 时, m_strRXData 变量的内容会自动显示到对应的编辑框中。这是MFC对话框数据交换(DDX)机制的核心。
5. 实现串口数据接收功能
5.1 添加串口事件消息处理函数
MSComm控件采用事件驱动模型。当串口发生特定事件(如接收到数据、发送缓冲区空、检测到线路信号变化等)时,会触发一个 OnComm 事件。我们需要为这个事件编写处理函数。
打开“MFC ClassWizard”,切换到“Message Maps”选项卡。在“Class name”中选择“CSCommTestDlg”,在“Object IDs”列表中选择“IDC_MSCOMM1”。右边的“Messages”列表框中会列出该控件支持的消息,找到“OnComm”并双击它。在弹出的对话框中,可以接受默认的函数名“OnComm”,点击“OK”和“Edit Code”按钮,VC++会自动在 CSCommTestDlg.cpp 中生成该函数的框架,并将光标定位到那里。
5.2 编写数据接收与处理代码
在生成的 OnComm 函数框架内,我们需要判断事件类型,并处理接收数据。以下是详细的代码实现与解析:
void CSCommTestDlg::OnComm()
{
// TODO: Add your control notification handler code here
VARIANT variant_inp;
COleSafeArray safearray_inp;
LONG len, k;
BYTE rxdata[2048]; // 定义一个足够大的缓冲区
CString strtemp;
// 获取通信事件代码
switch(m_ctrlComm.GetCommEvent())
{
case 2: // comEvReceive, 表示接收缓冲区有字符
variant_inp = m_ctrlComm.GetInput(); // 读取缓冲区数据,返回VARIANT类型
safearray_inp = variant_inp; // 将VARIANT转换为COleSafeArray,便于操作
len = safearray_inp.GetOneDimSize(); // 获取实际接收到的数据长度
if(len > 2048) len = 2048; // 防止缓冲区溢出,这是一个重要的安全措施
// 将安全数组中的数据逐字节拷贝到BYTE数组中
for(k = 0; k < len; k++)
{
safearray_inp.GetElement(&k, rxdata+k);
}
// 将BYTE数组中的数据转换为字符串并显示
// 注意:这里假设接收的是ASCII字符。如果是二进制数据,需要做十六进制显示等处理。
for(k = 0; k < len; k++)
{
strtemp.Format(_T("%c"), rxdata[k]); // 每个字节按字符格式化
m_strRXData += strtemp; // 追加到接收字符串变量
}
// 可选:限制接收框显示长度,防止长时间运行内存暴涨
// 例如,只保留最近5000个字符
// if(m_strRXData.GetLength() > 5000)
// {
// m_strRXData = m_strRXData.Right(5000);
// }
UpdateData(FALSE); // 更新界面,将m_strRXData的内容显示到IDC_EDIT_RXDATA编辑框
break;
// 可以处理其他事件,例如 comEvSend (1), comEvCTS (3), comEvDSR (4) 等
// case 1:
// AfxMessageBox(_T("发送缓冲区空"));
// break;
// case 3:
// // CTS线状态变化,可用于硬件流控制
// break;
default:
break;
}
}
代码解析与注意事项:
-
GetCommEvent():这是核心,它返回触发本次OnComm调用的事件代码。2代表comEvReceive,即接收事件。在实际项目中,你应该使用预定义的常量(如MSCOMM_EV_RECEIVE,其值就是2),或者自己定义,以提高代码可读性。 -
GetInput()方法 :它从控件的接收缓冲区中取出数据,并清空缓冲区。返回的是VARIANT类型,这是ActiveX控件间传递数据的通用容器。 -
COleSafeArray:是MFC对OLE安全数组的包装类,用于安全地处理VARIANT中的数组数据。GetOneDimSize()获取数组大小(即接收的字节数)。 - 缓冲区大小 :
rxdata[2048]定义了静态缓冲区。这里存在一个潜在风险:如果一次接收的数据超过2048字节,会导致缓冲区溢出。因此代码中加入了if(len > 2048) len = 2048;进行保护。更健壮的做法是使用动态数组(如std::vector<BYTE>)或者根据len动态分配内存。 - 数据格式处理 :
strtemp.Format(_T("%c"), rxdata[k]);这行代码将每个字节当作ASCII字符处理。 这是一个关键点 :如果下位机(MCU/嵌入式设备)发送的是二进制数据(例如0x00, 0xFF),那么用%c格式化可能会得到乱码或不可显示字符。在工业通信中,更常见的做法是将二进制数据以十六进制字符串的形式显示,例如strtemp.Format(_T("%02X "), rxdata[k]);,这样每个字节显示为两个十六进制数字加一个空格。 -
UpdateData(FALSE):这个调用触发MFC的对话框数据交换,将成员变量m_strRXData的最新内容更新到ID为IDC_EDIT_RXDATA的编辑框控件上,从而在界面上显示出来。
6. 串口初始化、参数配置与打开
6.1 在对话框初始化时打开串口
我们选择在对话框初始化函数 OnInitDialog() 中完成串口的初始配置和打开操作。找到 CSCommTestDlg::OnInitDialog() 函数,在 // TODO: Add extra initialization here 注释后面添加代码。
BOOL CSCommTestDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// ... 其他自动生成的代码(如设置图标等)...
// TODO: Add extra initialization here
// 初始化串口控件
if(m_ctrlComm.GetPortOpen())
m_ctrlComm.SetPortOpen(FALSE); // 如果已经打开,先关闭
m_ctrlComm.SetCommPort(1); // 选择COM1口。实际应根据需要选择,可通过下拉框让用户选择
// 配置串口参数:波特率9600,无校验,8数据位,1停止位
m_ctrlComm.SetSettings(_T("9600,n,8,1"));
// 设置输入模式:1-以二进制方式检取数据
m_ctrlComm.SetInputMode(1);
// 设置RThreshold:接收缓冲区有>=1个字符时触发OnComm事件
// 设为1表示每收到一个字符就触发一次,实时性高但频繁。可设为更大值(如10)以减少事件频率。
m_ctrlComm.SetRThreshold(1);
// 设置SThreshold:发送缓冲区空时触发OnComm事件,设为0表示不触发发送事件
m_ctrlComm.SetSThreshold(0);
// 设置InputLen:每次读取的字符数,0表示读取全部缓冲区内容
m_ctrlComm.SetInputLen(0);
// 尝试打开串口
if(!m_ctrlComm.GetPortOpen())
{
if(!m_ctrlComm.SetPortOpen(TRUE))
{
AfxMessageBox(_T("无法打开串口!可能原因:\n1. 串口不存在或被占用。\n2. 没有访问权限。"));
// 这里可以做一些错误恢复操作,比如禁用发送按钮等
}
else
{
// 打开成功,可以预读一次以清除可能存在的残留数据
m_ctrlComm.GetInput();
AfxMessageBox(_T("串口COM1已成功打开。"));
}
}
return TRUE; // return TRUE unless you set the focus to a control
}
6.2 关键参数详解与选型思考
-
SetCommPort(int nPort):指定物理串口号。COM1对应1,COM2对应2,以此类推。在实际工具中,这个参数应该做成可配置的(如下拉列表),因为用户设备的串口号可能不同。 -
SetSettings(LPCTSTR strSettings):参数字符串格式为“波特率,奇偶校验,数据位,停止位”。- 波特率 :必须与下位机严格一致,常见的有9600, 19200, 38400, 115200等。波特率越高,传输越快,但误码率可能增加,线缆要求也更高。
- 奇偶校验 :
n-无校验,o-奇校验,e-偶校验。用于简单的错误检测,在要求不高的场合常用n。 - 数据位 :通常是
8,代表一个字节。 - 停止位 :通常是
1。1.5和2较少见。
-
SetInputMode(long nMode):0:以文本模式检取数据。控件会将接收到的数据视为文本,并可能根据EOF字符(默认为\x1A)来停止读取。 在嵌入式通信中,强烈不建议使用此模式 ,因为二进制数据中的0x1A会被错误解释。1:以二进制模式检取数据。这是最常用的模式,原样接收所有字节。
-
SetRThreshold(long nChars):这是 性能与实时性的权衡点 。设为1,每收到一个字节就触发一次OnComm事件,实时性最好,但频繁的事件处理会消耗较多CPU资源,在高波特率大量数据传输时可能成为瓶颈。设为10或更大值,可以等缓冲区积累一定数据后再处理,减少事件触发次数,提高效率,但实时性略有下降。应根据实际通信的数据包大小和频率来调整。 -
SetInputLen(long nChars):设为0,表示GetInput()方法读取接收缓冲区中的所有内容。如果设为n,则只读取n个字符。通常保持为0即可。 -
SetPortOpen(TRUE):这是实际打开串口的操作。它内部会调用Windows的API创建串口文件句柄、配置超时、设置通信参数等。如果失败(返回FALSE),常见原因有:串口号不存在、串口已被其他程序(如超级终端、另一个串口工具)占用、或没有足够的权限(在Windows Vista之后,对COM10及以上的串口可能需要管理员权限)。
7. 实现数据发送功能
7.1 为发送按钮添加事件处理
回到资源编辑器,双击我们之前创建的“手动发送”按钮(IDC_BUTTON_MANUALSEND)。VC++会自动为其在 CSCommTestDlg 类中生成一个单击事件处理函数 OnButtonManualsend() ,并跳转到该函数体。
7.2 编写发送数据代码
在 OnButtonManualsend() 函数中,我们需要完成两件事:从发送编辑框获取文本,然后通过串口发送出去。
void CSCommTestDlg::OnButtonManualsend()
{
// TODO: Add your control notification handler code here
UpdateData(TRUE); // 将界面数据(IDC_EDIT_TXDATA编辑框的内容)读入成员变量m_strTXData
// 检查串口是否已打开
if(!m_ctrlComm.GetPortOpen())
{
AfxMessageBox(_T("请先打开串口!"));
return;
}
// 检查是否有数据要发送
if(m_strTXData.IsEmpty())
{
AfxMessageBox(_T("发送内容不能为空!"));
return;
}
// 核心发送语句
// SetOutput参数要求是VARIANT类型。COleVariant是MFC对VARIANT的包装类,能方便地将CString转换过去。
// 注意:这里发送的是字符串的二进制形式(包含结束符\0吗?不,CString的GetBuffer返回的是字符数组,不含\0)。
// 对于需要发送二进制数据的场景,需要构造包含BYTE数组的COleSafeArray,再转换为VARIANT。
m_ctrlComm.SetOutput(COleVariant(m_strTXData));
// 可选:发送后清空发送框,方便下次输入
// m_strTXData.Empty();
// UpdateData(FALSE);
}
代码解析与进阶讨论:
-
UpdateData(TRUE):这是MFC的机制,将对话框中所有关联了变量的控件内容更新到对应的成员变量中。这里就是把用户在“发送区”编辑框里输入的内容,保存到m_strTXData这个CString对象里。 -
SetOutput(const VARIANT& newValue):这是MSComm控件发送数据的方法。它接受一个VARIANT类型的参数。COleVariant类构造函数能接受CString,并自动将其转换为包含BSTR(宽字符串)的VARIANT。控件会把这个字符串的字节序列(对于ASCII是单字节,对于Unicode是双字节)发送出去。 - 二进制数据发送 :上述代码只能发送文本字符串。但在嵌入式通信中,协议帧常常是二进制的,例如要发送
0xAA, 0x55, 0x01, 0x02这样的指令。这时就不能直接用CString了。你需要构造一个COleSafeArray来存放这些字节,示例如下:
然后调用void CSCommTestDlg::SendBinaryData(BYTE* pData, int nLength) { COleSafeArray safearray; safearray.CreateOneDim(VT_UI1, nLength); // 创建一维字节数组 for(long i=0; i<nLength; i++) { safearray.PutElement(&i, pData+i); } m_ctrlComm.SetOutput(COleVariant(safearray)); }SendBinaryData(dataArray, sizeof(dataArray));即可。 - 发送效率与UI响应 :
SetOutput是同步调用,在数据完全写入操作系统底层发送缓冲区之前,函数不会返回。如果发送大量数据(比如几MB的文件),会导致界面“假死”。对于需要发送大量数据的场景,应该将发送操作放在一个单独的线程中,或者采用分批发送并在每次OnComm事件中检查发送缓冲区状态(comEvSend事件,当发送缓冲区空时触发)的方式。
8. 项目编译、运行与部署实战
8.1 编译与本地测试
完成以上所有代码后,按F7键编译整个项目。确保没有语法错误。编译成功后,按Ctrl+F5运行程序(不调试)。
程序启动后,首先会尝试打开COM1口。如果COM1口空闲且存在,你会看到“串口COM1已成功打开”的提示。此时,你需要准备一个实际的串口通信环境进行测试。
经典测试方法——自发自收(Loopback Test):
- 硬件环回 :找一根串口线(通常是DB9接头),将第2脚(RXD,接收数据)和第3脚(TXD,发送数据)短接。这样,从本机TXD发送出去的数据,会立刻被RXD接收回来。
- 运行程序 :在你的SCommTest程序中,在发送框输入“Hello, World!”,点击“手动发送”。
- 观察接收框 :如果一切正常,接收框中会立刻显示出“Hello, World!”。这表明发送和接收通路都是正确的。
8.2 与真实设备或虚拟串口调试
- 与下位机(如单片机)通信 :将你的电脑通过USB转串口线(如CH340、CP2102、FT232等芯片的线)连接到开发板。在设备管理器中查看该线对应的COM口号(例如COM5)。你需要修改代码中的
SetCommPort(5),并重新编译运行。确保波特率、校验位等参数与单片机程序中的设置完全一致。 - 使用虚拟串口工具 :在没有物理串口或设备时,可以使用虚拟串口软件(如VSPD, Virtual Serial Port Driver)创建一对虚拟的互联串口,例如COM2和COM3。将你的程序设置为打开COM2,然后用另一个串口调试助手(如AccessPort、串口猎人)打开COM3。两者之间就可以像真实串口一样互相收发数据,非常适合协议调试和逻辑验证。
8.3 程序打包与在没有VC环境的电脑上运行
这是使用MFC和ActiveX控件开发程序必须面对的一个问题。你的程序在开发机上运行正常,但拷贝到一台没有安装VC++ 6.0的电脑上,很可能会因为缺少运行时库或控件而无法启动。
部署所需文件及注册方法:
-
必需的文件 :
- 你的可执行文件 :
SCommTest.exe(位于项目目录的Release或Debug文件夹下)。 - MFC动态库 :
mfc42.dll(Release版) 或mfc42d.dll(Debug版)。通常可以在C:\Windows\System32(或SysWOW64)下找到,但为了兼容性,建议从你的VC6安装目录(如C:\Program Files (x86)\Microsoft Visual Studio\VC98\MFC\Lib)或系统目录拷贝对应的Release版本。 - C运行时库 :
msvcrt.dll(通常系统自带,但旧系统可能需要)。 - MSComm控件 :
mscomm32.ocx。这个文件通常在C:\Windows\System32(或SysWOW64)下。 重要:必须注册此控件 。
- 你的可执行文件 :
-
部署步骤 :
- 在一个干净的文件夹(例如
SCommTest_Release)中,放入SCommTest.exe、mfc42.dll和mscomm32.ocx。 - 在目标计算机上,以 管理员身份 打开命令提示符(CMD)。
- 使用
cd命令切换到上述文件夹。 - 执行注册命令:
regsvr32 mscomm32.ocx。如果成功,会弹出提示框。 - 现在,直接双击
SCommTest.exe应该就可以运行了。
- 在一个干净的文件夹(例如
实操心得 :对于正式发布的小工具,更专业的做法是制作一个安装包(可以使用Inno Setup、NSIS等免费工具)。安装包会自动将OCX控件复制到系统目录并注册,将MFC DLL放到程序目录或系统目录,并创建开始菜单快捷方式和桌面图标,用户体验会好很多。另外,尽量使用Release版本进行发布,它比Debug版本更小、更快,且不依赖调试库。
9. 常见问题排查与高级技巧实录
9.1 编译与运行时的典型错误
-
编译错误:
CMSComm未声明的标识符- 原因 :没有成功插入MSComm控件,或者插入后
mscomm.h头文件没有被正确包含。 - 解决 :检查
SCommTestDlg.h文件中是否有#include "mscomm.h"。如果没有,手动添加。并确保在“FileView”的“Header Files”里能看到mscomm.h和mscomm.cpp。
- 原因 :没有成功插入MSComm控件,或者插入后
-
运行错误:打开串口失败
- 原因1:串口不存在 。检查设备管理器中的端口号。注意COM10及以上的端口,在代码中需要用
\\.\COM10这样的格式,但MSComm控件可能不支持或需要特殊处理。建议只使用COM1-COM9。 - 原因2:串口被占用 。关闭可能占用该串口的其他所有软件(如串口调试助手、Putty、甚至某些编程软件的串口监视功能)。
- 原因3:权限不足 。在Windows 7及以上系统,对某些串口的操作可能需要管理员权限。尝试以管理员身份运行你的程序。
- 原因4:硬件问题 。USB转串口线驱动未正确安装,或硬件损坏。
- 原因1:串口不存在 。检查设备管理器中的端口号。注意COM10及以上的端口,在代码中需要用
-
程序运行后接收不到数据,但发送似乎正常
- 排查步骤 :
- 确认连接 :检查TX、RX线是否接反?与下位机通信时,原则是 A设备的TX接B设备的RX,A设备的RX接B设备的TX ,以及地线(GND)相连。
- 确认参数 :波特率、数据位、停止位、校验位是否与通信对方 完全一致 ?一个比特的差异都会导致乱码或收不到。
- 确认控件设置 :
SetInputMode是否设为1(二进制)?SetRThreshold是否大于0?如果为0,则永远不会触发接收事件。 - 使用环回测试 :用短接TX/RX的方式,排除程序以外的硬件和对方设备的问题。
- 添加调试输出 :在
OnComm函数开头用OutputDebugString或写日志文件的方式,输出事件代码,确认事件是否被触发。
- 排查步骤 :
9.2 通信稳定性与数据处理进阶
-
接收数据不完整或粘包
- 现象 :发送方分两次发送“ABCD”和“EFGH”,接收方可能一次收到“ABCDEFGH”。
- 原因 :串口是流式设备,没有消息边界。
OnComm事件触发取决于RThreshold和操作系统调度,可能两次数据到达间隔极短,被一次读取。 - 解决方案 :在应用层定义 通信协议 。例如:
- 定长协议 :每条消息固定长度,接收方凑够固定长度才视为一条完整消息。
- 特殊字符分隔 :用特定的字符(如换行符
\n、回车符\r)作为消息结束符。在OnComm中,将收到的数据追加到一个缓冲区,然后不断检查缓冲区中是否有结束符,有则提取出一条完整消息。 - 包头+长度+数据+校验 :这是最可靠的方式。包头(如0xAA,0x55)标识开始,接着是数据长度字段,然后是实际数据,最后是校验和(如CRC16)。接收方先匹配包头,再根据长度字段读取指定字节数,最后校验,通过后才算一条有效消息。
-
高波特率下数据丢失或界面卡顿
- 原因 :
RThreshold=1时,每个字节触发一次事件,CPU忙于处理消息,UI线程被阻塞。 - 优化方案 :
- 增大
RThreshold:例如设为10或50,让缓冲区积累一些数据再处理,减少事件频率。 - 在
OnComm中快速读取,离线处理 :OnComm函数里只做最必要的工作——将数据从控件缓冲区读出来,追加到一个线程安全的队列或环形缓冲区中。然后设置一个定时器(SetTimer)或在工作线程中,从这个队列里取出数据进行解析和显示。这样避免在事件处理函数中做耗时操作。 - 使用多线程 :创建一个专门的串口读写线程,使用Windows原生API(
CreateFile,ReadFile,WriteFile)进行通信,通过消息或线程安全队列与主UI线程交换数据。这能获得最大的灵活性和性能,但复杂度也最高。
- 增大
- 原因 :
-
中文或非ASCII字符乱码
- 原因 :VC++ 6.0默认使用多字节字符集(MBCS),而现代Windows和许多设备通信使用UTF-8或Unicode。直接发送中文字符串可能会因编码不一致导致乱码。
- 处理建议 :对于简单的英数字符通信,可以忽略。如果需要处理中文,一个相对简单的方法是,在发送前将
CString(MBCS)转换为UTF-8编码的字节流再发送;接收时,将收到的UTF-8字节流转换回MBCS编码的CString再显示。可以使用WideCharToMultiByte和MultiByteToWideCharAPI函数进行转换。
9.3 功能扩展建议
一个基础的串口工具已经完成,但一个实用的工具还需要更多功能:
- 串口参数动态配置 :将波特率、数据位等做成下拉列表(Combo Box),让用户可以在运行时选择,而不是写死在代码里。
- 自动发送 :添加一个复选框(Check Box)和编辑框(用于设置间隔时间),实现定时自动发送。
- 数据格式显示 :添加单选按钮(Radio Button),让用户选择接收数据显示为“ASCII字符串”还是“十六进制”。
- 发送数据格式 :同样,发送区也可以支持输入十六进制字符串(如
AA BB 0D 0A),程序将其转换为二进制数据发送。 - 日志记录 :添加一个“保存”按钮,将接收框的内容保存到文本文件中,便于后续分析。
- 清空功能 :添加“清空接收区”和“清空发送区”按钮。
- 状态栏显示 :在对话框底部添加状态栏,实时显示串口开关状态、发送/接收字节数等信息。
通过这个项目,你不仅学会了如何使用一个古老的工具链完成一个实用的功能,更重要的是,你深入理解了串口通信的基本原理、事件驱动编程模型、以及如何构建一个健壮的桌面应用程序的骨架。这些知识,在你未来使用更现代的工具(如C#的SerialPort类、Python的pyserial)时,依然具有重要的参考价值。
更多推荐

所有评论(0)