1. 项目概述:基于MSComm控件的串口通信实现
在工业控制和嵌入式系统开发中,串口通信是最基础也是最可靠的数据传输方式之一。我最近完成了一个使用MSComm控件实现PC端串口通信的项目,采用中断方式接收数据,支持数据的发送、接收、显示和存储功能。这个方案特别适合需要实时性较高的工业控制场景,比如PLC通信、传感器数据采集等。
MSComm(Microsoft Communications Control)是微软提供的一个ActiveX控件,它封装了串口通信的底层细节,让开发者能够快速实现串口通信功能。相比直接调用Windows API,使用MSComm控件可以节省大量开发时间,而且稳定性更有保障。在我的实际项目中,这个方案成功应用在了温度监控系统中,PC端通过串口与多个温度传感器节点通信,实时采集并记录温度数据。
2. MSComm控件核心特性解析
2.1 事件驱动机制
MSComm控件的核心优势在于它的事件驱动机制。通过OnComm事件,我们可以实时响应串口上发生的各种通信事件,而不需要不断轮询串口状态。这种机制大大提高了程序的效率,也简化了代码结构。
在实际应用中,OnComm事件可以响应多种通信状态:
- 数据到达(CommEvent=2):这是最常用的事件,表示有数据到达串口缓冲区
- 发送完成(CommEvent=4):当发送缓冲区中的数据全部发送完毕时触发
- 通信错误(CommEvent=5):当发生通信错误时触发,如帧错误、溢出等
2.2 两种数据接收模式对比
MSComm控件支持两种数据接收方式,各有适用场景:
-
查询方式:
- 通过定期检查InBufferCount属性判断是否有数据到达
- 实现简单,适合数据量小、实时性要求不高的场景
- 缺点是需要占用CPU资源进行轮询
-
中断方式(事件驱动):
- 通过设置RThreshold属性(建议值为1-10)
- 当接收缓冲区中的字符数达到RThreshold时触发OnComm事件
- 实时性高,CPU占用率低,适合工业控制等实时应用
- 需要处理事件回调函数,代码结构稍复杂
在我的项目中,由于需要实时响应传感器数据,所以选择了中断接收方式。通过设置RThreshold=1,确保每个到达的字节都能立即触发事件处理。
2.3 关键属性详解
MSComm控件提供了丰富的属性来配置串口通信参数,以下是最关键的几个:
cpp复制// 串口号(1对应COM1,2对应COM2,依此类推)
m_mscom.SetCommPort(1);
// 通信参数设置:波特率,校验位,数据位,停止位
m_mscom.SetSettings(_T("9600,n,8,1"));
// 输入模式:0-文本模式,1-二进制模式
m_mscom.SetInputMode(1);
// 接收阈值:触发OnComm事件的字节数
m_mscom.SetRThreshold(1);
// 发送阈值:当发送缓冲区低于此值时触发发送事件
m_mscom.SetSThreshold(0);
// 硬件流控制设置
m_mscom.SetHandshaking(2); // 2表示启用RTS/CTS硬件握手
3. 完整实现方案解析
3.1 开发环境准备
在开始编码前,需要确保开发环境正确配置:
-
注册MSComm控件:
- 将mscomm32.ocx复制到系统目录(如C:\Windows\System32)
- 以管理员身份运行命令提示符,执行:
bash复制
regsvr32 mscomm32.ocx
-
VC++项目配置:
- 在stdafx.h中添加头文件引用和库链接:
cpp复制#include <mscomm.h> #pragma comment(lib, "mscomm.lib") - 在资源视图中右键对话框,选择"Insert ActiveX Control",找到"Microsoft Communications Control"
- 在stdafx.h中添加头文件引用和库链接:
-
界面设计要点:
- 串口选择组合框(IDC_COMBO_COM):列出COM1-COM16
- 波特率选择组合框(IDC_COMBO_BAUD):常用波特率选项
- 发送编辑框(IDC_EDIT_SEND):输入要发送的数据
- 接收编辑框(IDC_EDIT_RECEIVE):显示接收到的数据
- 操作按钮:打开/关闭串口、发送、清空、保存等
3.2 核心代码实现
3.2.1 串口初始化和打开
cpp复制BOOL CMainDlg::OnInitDialog() {
CDialog::OnInitDialog();
// 初始化串口列表
for (int i = 1; i <= 16; i++) {
CString str;
str.Format(_T("COM%d"), i);
m_comboCom.AddString(str);
}
m_comboCom.SetCurSel(0);
// 初始化波特率列表
CString baudRates[] = {_T("1200"), _T("2400"), _T("4800"),
_T("9600"), _T("19200"), _T("38400"),
_T("57600"), _T("115200")};
for (int i = 0; i < 8; i++) {
m_comboBaud.AddString(baudRates[i]);
}
m_comboBaud.SetCurSel(3); // 默认9600
// 初始化MSComm控件
if (!m_mscom.Create(NULL, WS_VISIBLE | WS_CHILD, CRect(0,0,0,0), this, IDC_MSCOMM1)) {
AfxMessageBox(_T("无法创建MSComm控件!"));
return FALSE;
}
// 设置默认参数
m_mscom.SetCommPort(1); // 默认COM1
m_mscom.SetSettings(_T("9600,n,8,1")); // 默认参数
m_mscom.SetInputMode(1); // 二进制模式
m_mscom.SetRThreshold(1); // 每接收1个字符触发事件
m_mscom.SetSThreshold(0); // 不触发发送事件
m_mscom.SetPortOpen(FALSE); // 初始关闭
return TRUE;
}
void CMainDlg::OnButtonOpen() {
CString strCom, strBaud;
m_comboCom.GetWindowText(strCom);
m_comboBaud.GetWindowText(strBaud);
int nCom = _ttoi(strCom.Mid(3));
CString strSettings = strBaud + _T(",n,8,1");
// 关闭已打开的串口
if (m_mscom.GetPortOpen()) {
m_mscom.SetPortOpen(FALSE);
}
m_mscom.SetCommPort(nCom);
m_mscom.SetSettings(strSettings);
// 尝试打开串口
if (!m_mscom.GetPortOpen()) {
if (m_mscom.SetPortOpen(TRUE)) {
UpdateStatusBar(_T("串口已打开: ") + strCom + _T(" ") + strBaud);
GetDlgItem(IDC_BUTTON_OPEN)->EnableWindow(FALSE);
GetDlgItem(IDC_BUTTON_CLOSE)->EnableWindow(TRUE);
} else {
AfxMessageBox(_T("无法打开串口!"));
}
}
}
3.2.2 中断接收数据处理
cpp复制// 事件映射
BEGIN_EVENTSINK_MAP(CMainDlg, CDialog)
ON_EVENT(CMainDlg, IDC_MSCOMM1, 1, OnCommMscomm, VTS_NONE)
END_EVENTSINK_MAP()
// 串口事件处理
void CMainDlg::OnCommMscomm() {
VARIANT variant_inp;
COleSafeArray safearray_inp;
LONG len, k;
BYTE rxdata[2048]; // 接收缓冲区
switch (m_mscom.GetCommEvent()) {
case 2: // 接收事件
variant_inp = m_mscom.GetInput();
safearray_inp = variant_inp;
len = safearray_inp.GetOneDimSize();
// 数据复制到缓冲区
for (k = 0; k < len; k++) {
safearray_inp.GetElement(&k, rxdata + k);
}
// 处理接收到的数据
ProcessReceivedData(rxdata, len);
break;
case 4: // 发送事件
UpdateStatusBar(_T("数据发送完成"));
break;
case 5: // 错误事件
UpdateStatusBar(_T("通信错误!"));
break;
default:
break;
}
}
// 处理接收数据
void CMainDlg::ProcessReceivedData(BYTE* data, int len) {
CString strDisplay;
// 十六进制显示
if (m_checkHexReceive.GetCheck()) {
strDisplay = FormatHexData(data, len);
}
// 文本显示
else {
// 转换为CString
data[len] = '\0';
strDisplay = CString(data);
}
// 追加到接收区
CString strOld;
m_editReceive.GetWindowText(strOld);
if (!strOld.IsEmpty()) {
strOld += _T("\r\n");
}
m_editReceive.SetWindowText(strOld + strDisplay);
// 滚动到最后一行
int nLength = m_editReceive.GetWindowTextLength();
m_editReceive.SetSel(nLength, nLength);
m_editReceive.ReplaceSel(_T(""));
// 更新状态栏
CString statusMsg;
statusMsg.Format(_T("收到 %d 字节数据"), len);
UpdateStatusBar(statusMsg);
}
3.2.3 数据发送实现
cpp复制void CMainDlg::OnButtonSend() {
if (!m_mscom.GetPortOpen()) {
AfxMessageBox(_T("串口未打开!"));
return;
}
CString strSend;
m_editSend.GetWindowText(strSend);
if (strSend.IsEmpty()) {
return;
}
// 十六进制发送处理
if (m_checkHexSend.GetCheck()) {
CString strHex = strSend;
strHex.Remove(' ');
strHex.Remove('-');
if (strHex.GetLength() % 2 != 0) {
AfxMessageBox(_T("HEX数据长度必须为偶数!"));
return;
}
int len = strHex.GetLength() / 2;
BYTE* data = new BYTE[len];
for (int i = 0; i < len; i++) {
CString byteStr = strHex.Mid(i*2, 2);
data[i] = (BYTE)strtol(byteStr, NULL, 16);
}
m_mscom.SetOutput(COleVariant((BYTE*)data, len));
delete[] data;
}
// 文本发送
else {
m_mscom.SetOutput(COleVariant(strSend));
}
// 更新状态栏
CString statusMsg;
statusMsg.Format(_T("已发送: %s"), strSend);
UpdateStatusBar(statusMsg);
}
4. 工业应用中的通信协议设计
4.1 典型通信协议示例
在实际工业应用中,通常需要设计一套完整的通信协议来确保数据传输的可靠性。以下是一个与51单片机通信的温度采集协议示例:
| 方向 | 数据格式 | 说明 |
|---|---|---|
| PC→MCU | 0x01 | 请求温度数据 |
| MCU→PC | [温度高8位][温度低8位] | 16位温度数据(0.1℃精度) |
| PC→MCU | 0x02 [参数1][参数2] | 设置参数(如采样间隔) |
| MCU→PC | [状态码] | 操作结果(0=成功,1=失败) |
4.2 数据解析实现
在OnComm事件处理函数中,我们可以根据协议解析接收到的数据:
cpp复制case 2: // 接收事件
// ... 接收数据到rxdata
// 解析温度数据
if (len == 2) {
short temp_raw = (rxdata[0] << 8) | rxdata[1];
float temperature = temp_raw / 10.0f;
CString strTemp;
strTemp.Format(_T("温度: %.1f°C"), temperature);
m_editReceive.SetWindowText(strTemp);
}
// 解析状态码
else if (len == 1) {
if (rxdata[0] == 0) {
UpdateStatusBar(_T("操作成功"));
} else {
UpdateStatusBar(_T("操作失败"));
}
}
break;
4.3 数据校验机制
为了提高通信可靠性,可以添加校验机制,如校验和或CRC校验:
cpp复制// 计算校验和(异或校验)
BYTE CalculateChecksum(BYTE* data, int len) {
BYTE checksum = 0;
for (int i = 0; i < len; i++) {
checksum ^= data[i];
}
return checksum;
}
// 发送带校验的数据
void CMainDlg::SendWithChecksum(BYTE* data, int len) {
BYTE* buffer = new BYTE[len + 1];
memcpy(buffer, data, len);
buffer[len] = CalculateChecksum(data, len);
m_mscom.SetOutput(COleVariant(buffer, len + 1));
delete[] buffer;
}
// 接收时验证校验和
bool CMainDlg::VerifyChecksum(BYTE* data, int len) {
if (len < 1) return false;
BYTE receivedChecksum = data[len - 1];
BYTE calculatedChecksum = CalculateChecksum(data, len - 1);
return receivedChecksum == calculatedChecksum;
}
5. 常见问题与解决方案
5.1 串口无法打开
可能原因及解决方案:
-
串口被其他程序占用
- 关闭可能占用串口的其他软件
- 重启计算机释放被占用的串口资源
-
串口号选择错误
- 确认设备管理器中显示的实际串口号
- 对于USB转串口设备,可能需要重新安装驱动
-
权限不足
- 以管理员身份运行程序
- 修改注册表权限(谨慎操作)
5.2 数据接收不完整
调试步骤:
-
检查波特率设置
- 确保两端波特率一致
- 尝试降低波特率(如从115200降到9600)
-
调整接收缓冲区
cpp复制m_mscom.SetInBufferSize(4096); // 增大接收缓冲区 m_mscom.SetRThreshold(1); // 每个字节都触发事件 -
检查硬件连接
- 确认串口线完好
- 检查RTS/CTS流控制设置是否匹配
5.3 高频数据丢失问题
在高速数据传输时,可能会因为处理不及时导致数据丢失。解决方案:
-
优化数据处理代码
- 在OnComm事件中只做最小必要的处理
- 将数据快速存入队列,由其他线程处理
-
使用双缓冲机制
cpp复制// 定义全局缓冲区 CCriticalSection g_cs; // 用于线程同步 CArray<BYTE> g_receiveBuffer; // 在OnComm中快速存入数据 void CMainDlg::OnCommMscomm() { // ... 接收数据 CSingleLock lock(&g_cs, TRUE); for (k = 0; k < len; k++) { g_receiveBuffer.Add(rxdata[k]); } } // 在定时器或线程中处理数据 void CMainDlg::OnTimer(UINT_PTR nIDEvent) { CSingleLock lock(&g_cs, TRUE); if (!g_receiveBuffer.IsEmpty()) { ProcessReceivedData(g_receiveBuffer.GetData(), g_receiveBuffer.GetSize()); g_receiveBuffer.RemoveAll(); } } -
提高线程优先级
cpp复制SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
6. 项目扩展与优化
6.1 自动发送功能
对于需要定期查询设备状态的场景,可以添加定时自动发送功能:
cpp复制// 在OnInitDialog中添加定时器
SetTimer(1, 1000, NULL); // 1秒定时器
// 定时器处理函数
void CMainDlg::OnTimer(UINT_PTR nIDEvent) {
if (nIDEvent == 1 && m_mscom.GetPortOpen()) {
// 自动发送查询命令
BYTE cmd = 0x01; // 温度查询命令
m_mscom.SetOutput(COleVariant(cmd));
}
CDialog::OnTimer(nIDEvent);
}
6.2 数据可视化
添加图表控件可以直观显示数据变化趋势。以下是使用第三方ChartCtrl的示例:
cpp复制// 添加图表控件
#include "ChartCtrl.h"
// 在对话框类中添加成员变量
CChartCtrl m_chart;
// 初始化图表
BOOL CMainDlg::OnInitDialog() {
// ... 其他初始化代码
// 创建图表控件
m_chart.Create(WS_CHILD | WS_VISIBLE, CRect(10, 300, 400, 500), this, IDC_CHART);
m_chart.SetTitle(_T("温度变化曲线"));
m_chart.GetBottomAxis()->SetTitle(_T("时间"));
m_chart.GetLeftAxis()->SetTitle(_T("温度(°C)"));
// 添加曲线
CChartLineSerie* pSeries = m_chart.CreateLineSerie();
pSeries->SetSeriesOrdering(poNoOrdering);
pSeries->SetColor(RGB(255,0,0));
return TRUE;
}
// 在接收数据时更新图表
void CMainDlg::ProcessReceivedData(BYTE* data, int len) {
// ... 解析温度数据
// 添加到图表
static double x = 0;
CChartLineSerie* pSeries = (CChartLineSerie*)m_chart.GetSerie(0);
pSeries->AddPoint(x++, temperature);
// 限制显示点数
if (pSeries->GetPointsCount() > 100) {
pSeries->RemoveFirstPoint();
}
m_chart.RefreshCtrl();
}
6.3 多线程处理
对于数据量大或处理复杂的场景,可以使用多线程避免界面卡顿:
cpp复制// 工作线程函数
UINT CommThreadProc(LPVOID pParam) {
CMainDlg* pDlg = (CMainDlg*)pParam;
while (pDlg->m_bThreadRunning) {
// 检查并处理接收队列中的数据
CSingleLock lock(&pDlg->m_csComm, TRUE);
if (!pDlg->m_arrReceiveData.IsEmpty()) {
// 处理数据...
pDlg->m_arrReceiveData.RemoveAll();
}
lock.Unlock();
Sleep(10); // 短暂释放CPU
}
return 0;
}
// 在对话框类中启动线程
void CMainDlg::StartCommThread() {
m_bThreadRunning = TRUE;
m_pThread = AfxBeginThread(CommThreadProc, this);
}
// 关闭线程
void CMainDlg::StopCommThread() {
m_bThreadRunning = FALSE;
if (m_pThread) {
WaitForSingleObject(m_pThread->m_hThread, INFINITE);
}
}
7. 实际应用中的经验分享
7.1 调试技巧
-
使用虚拟串口工具:
- 在开发阶段,可以使用虚拟串口工具(如VSPD)创建一对虚拟串口
- 一个用于程序,另一个用于串口调试助手,方便测试
-
日志记录:
cpp复制void CMainDlg::LogMessage(LPCTSTR msg) { CStdioFile file; if (file.Open(_T("comm_log.txt"), CFile::modeCreate | CFile::modeNoTruncate | CFile::modeWrite)) { file.SeekToEnd(); CString strLog; strLog.Format(_T("[%s] %s\r\n"), CTime::GetCurrentTime().Format("%Y-%m-%d %H:%M:%S"), msg); file.WriteString(strLog); file.Close(); } } -
十六进制显示:
- 在调试通信协议时,始终开启十六进制显示模式
- 可以快速识别数据帧结构、校验和等问题
7.2 性能优化
-
缓冲区设置:
cpp复制m_mscom.SetInBufferSize(8192); // 接收缓冲区 m_mscom.SetOutBufferSize(2048); // 发送缓冲区 -
事件处理优化:
- 避免在OnComm事件中执行耗时操作
- 对于批量数据处理,考虑使用RThreshold=10-20,减少事件触发频率
-
界面更新优化:
cpp复制// 使用定时器批量更新界面,而不是每次接收都更新 void CMainDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == 2) { // 界面更新定时器 CSingleLock lock(&m_csData, TRUE); if (!m_strPendingDisplay.IsEmpty()) { m_editReceive.SetWindowText(m_strPendingDisplay); m_strPendingDisplay.Empty(); } } }
7.3 兼容性考虑
-
MSComm控件版本:
- 不同Windows版本可能自带不同版本的MSComm控件
- 建议将mscomm32.ocx打包到安装程序中
-
64位系统支持:
- 32位程序在64位系统上需要注册32位版本的mscomm32.ocx
- 注册路径应为:C:\Windows\SysWOW64\regsvr32.exe mscomm32.ocx
-
替代方案:
- 对于新项目,可以考虑使用Windows API(CreateFile, ReadFile, WriteFile)实现串口通信
- 或者使用开源的串口库,如Boost.Asio