从零构建MFC串口工具:CSerialPort实战指南
在工业控制、嵌入式开发和硬件调试领域,串口通信依然是设备与计算机对话的"普通话"。虽然市面上有各种现成的串口调试助手,但当我们需要将串口功能深度集成到自己的MFC应用程序中时,掌握自主开发能力就显得尤为重要。本文将带你使用CSerialPort这一轻量级开源库,在MFC框架下打造一个完全可控的串口通信工具。
1. 环境准备与项目搭建
1.1 开发环境配置
首先确保你的开发环境满足以下要求:
- 操作系统:Windows 7/10/11(64位或32位均可)
- 开发工具:Visual Studio 2015或更高版本(社区版即可)
- 必备组件:MFC支持(安装VS时需勾选)
提示:虽然VS2008也能使用,但推荐使用较新版本以获得更好的C++11支持
创建MFC项目的基本步骤:
- 打开Visual Studio,选择"创建新项目"
- 搜索并选择"MFC应用程序"
- 项目类型选择"基于对话框"
- 命名为"SerialTool"(或其他你喜欢的名称)
1.2 集成CSerialPort库
CSerialPort是一个跨平台的串口通信库,支持Windows、Linux和macOS。我们将通过Git将其集成到项目中:
# 在项目解决方案目录下执行 git clone https://github.com/itas109/CSerialPort.git目录结构应如下所示:
SerialTool/ ├── SerialTool.sln ├── SerialTool/ # MFC项目目录 └── CSerialPort/ # 克隆的库源码 ├── include/ │ └── CSerialPort/ │ ├── SerialPort.h │ └── SerialPortInfo.h └── src/ # 源文件目录2. 项目配置与库集成
2.1 配置头文件路径
为了让编译器能找到CSerialPort的头文件,需要设置附加包含目录:
- 右键项目 → 属性 → C/C++ → 常规
- 在"附加包含目录"中添加:
$(ProjectDir)\..\CSerialPort\include
2.2 添加源文件并设置编译选项
将CSerialPort的源文件添加到项目中:
- 在解决方案资源管理器中,右键项目 → 添加 → 新建筛选器,命名为"CSerialPort"
- 右键该筛选器 → 添加 → 现有项
- 选择以下源文件:
- SerialPort.cpp
- SerialPortBase.cpp
- SerialPortWinBase.cpp
- SerialPortInfo.cpp
- SerialPortInfoBase.cpp
- SerialPortInfoWinBase.cpp
关键设置:对于每个添加的.cpp文件,需要禁用预编译头:
右键文件 → 属性 → C/C++ → 预编译头 → 选择"不使用预编译头"2.3 链接必要库文件
Windows平台下需要链接setupapi.lib:
- 右键项目 → 属性 → 链接器 → 输入
- 在"附加依赖项"中添加:
setupapi.lib
3. 实现串口通信核心功能
3.1 设计对话框界面
在资源视图中设计主对话框,添加以下控件:
| 控件类型 | ID | 用途 |
|---|---|---|
| 组合框 | IDC_PORT_COMBO | 显示可用串口列表 |
| 按钮 | IDC_OPEN_BTN | 打开/关闭串口 |
| 编辑框 | IDC_SEND_EDIT | 输入要发送的数据 |
| 按钮 | IDC_SEND_BTN | 发送数据 |
| 列表框 | IDC_LOG_LIST | 显示通信日志 |
3.2 初始化串口功能
在对话框头文件(SerialToolDlg.h)中添加必要的包含和声明:
#include "CSerialPort/SerialPort.h" #include "CSerialPort/SerialPortInfo.h" using namespace itas109; class CSerialToolDlg : public CDialog, public CSerialPortListener { // ... 其他代码 private: CSerialPort m_serialPort; void onReadEvent(const char* portName, unsigned int readBufferLen) override; };在OnInitDialog()中初始化串口列表:
BOOL CSerialToolDlg::OnInitDialog() { CDialog::OnInitDialog(); // 获取可用串口列表 std::vector<SerialPortInfo> portList = CSerialPortInfo::availablePorts(); CComboBox* pPortCombo = (CComboBox*)GetDlgItem(IDC_PORT_COMBO); for (const auto& port : portList) { pPortCombo->AddString(CString(port.portName.c_str())); } if (pPortCombo->GetCount() > 0) { pPortCombo->SetCurSel(0); } // 连接读取事件 m_serialPort.connectReadEvent(this); return TRUE; }3.3 实现串口操作
添加打开/关闭串口的处理函数:
void CSerialToolDlg::OnBnClickedOpenBtn() { CString portName; GetDlgItemText(IDC_PORT_COMBO, portName); if (m_serialPort.isOpen()) { m_serialPort.close(); SetDlgItemText(IDC_OPEN_BTN, _T("打开")); AddLog(_T("串口已关闭")); } else { if (m_serialPort.init(CT2A(portName))) { if (m_serialPort.open()) { SetDlgItemText(IDC_OPEN_BTN, _T("关闭")); AddLog(_T("串口打开成功")); } else { AddLog(_T("串口打开失败")); } } else { AddLog(_T("串口初始化失败")); } } }实现数据发送功能:
void CSerialToolDlg::OnBnClickedSendBtn() { if (!m_serialPort.isOpen()) { AddLog(_T("请先打开串口")); return; } CString sendText; GetDlgItemText(IDC_SEND_EDIT, sendText); if (!sendText.IsEmpty()) { int len = m_serialPort.writeData(CT2A(sendText), sendText.GetLength()); AddLog(CString(_T("发送: ")) + sendText); } }3.4 处理接收数据
实现onReadEvent回调函数:
void CSerialToolDlg::onReadEvent(const char* portName, unsigned int readBufferLen) { if (readBufferLen > 0) { char* buffer = new char[readBufferLen + 1]; int recvLen = m_serialPort.readData(buffer, readBufferLen); if (recvLen > 0) { buffer[recvLen] = '\0'; CString recvText(buffer); AddLog(CString(_T("接收: ")) + recvText); } delete[] buffer; } }辅助函数AddLog用于在列表框中添加日志:
void CSerialToolDlg::AddLog(LPCTSTR text) { CListBox* pList = (CListBox*)GetDlgItem(IDC_LOG_LIST); CString str; CTime tm = CTime::GetCurrentTime(); str.Format(_T("[%02d:%02d:%02d] %s"), tm.GetHour(), tm.GetMinute(), tm.GetSecond(), text); pList->AddString(str); pList->SetCurSel(pList->GetCount() - 1); }4. 功能扩展与优化
4.1 添加串口参数配置
完整的串口工具需要支持各种参数配置。在对话框中添加以下控件:
- 波特率组合框(9600, 19200, 38400, 57600, 115200等)
- 数据位选择(5,6,7,8)
- 停止位选择(1,1.5,2)
- 校验位选择(None, Odd, Even, Mark, Space)
修改打开串口的代码:
// 获取用户选择的参数 CString baudRate, dataBits, stopBits, parity; m_baudCombo.GetWindowText(baudRate); m_dataCombo.GetWindowText(dataBits); m_stopCombo.GetWindowText(stopBits); m_parityCombo.GetWindowText(parity); // 设置串口参数 m_serialPort.init(CT2A(portName), _ttoi(baudRate), (itas109::DataBits)_ttoi(dataBits), (itas109::Parity)GetParityIndex(parity), (itas109::StopBits)_ttoi(stopBits));4.2 实现十六进制发送与显示
添加复选框控件IDC_HEX_MODE,修改发送和接收处理:
// 发送时处理十六进制模式 if (IsDlgButtonChecked(IDC_HEX_MODE)) { // 将字符串转换为十六进制字节数组 CStringA str = CT2A(sendText); vector<BYTE> hexData; // ... 转换逻辑 len = m_serialPort.writeData((const char*)hexData.data(), hexData.size()); } else { len = m_serialPort.writeData(CT2A(sendText), sendText.GetLength()); } // 接收时处理十六进制显示 if (IsDlgButtonChecked(IDC_HEX_MODE)) { CString hexStr; for (int i = 0; i < recvLen; i++) { CString byteStr; byteStr.Format(_T("%02X "), (BYTE)buffer[i]); hexStr += byteStr; } recvText = hexStr; }4.3 添加自动发送功能
实现定时自动发送可以方便压力测试:
- 添加定时器处理:
void CSerialToolDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == AUTO_SEND_TIMER && m_serialPort.isOpen()) { OnBnClickedSendBtn(); } CDialog::OnTimer(nIDEvent); }- 添加开始/停止自动发送按钮:
void CSerialToolDlg::OnBnClickedAutoSendBtn() { if (m_autoSending) { KillTimer(AUTO_SEND_TIMER); SetDlgItemText(IDC_AUTO_SEND_BTN, _T("开始自动发送")); m_autoSending = false; } else { CString intervalStr; GetDlgItemText(IDC_INTERVAL_EDIT, intervalStr); int interval = _ttoi(intervalStr); if (interval > 0) { SetTimer(AUTO_SEND_TIMER, interval, NULL); SetDlgItemText(IDC_AUTO_SEND_BTN, _T("停止自动发送")); m_autoSending = true; } } }5. 常见问题排查与性能优化
5.1 解决资源占用问题
在高频率通信时,可能会遇到界面卡顿。可以采用以下优化措施:
- 使用双缓冲技术:对于频繁更新的列表框,实现双缓冲减少闪烁
// 在OnInitDialog()中 m_logList.ModifyStyle(0, LBS_OWNERDRAWFIXED); // 添加WM_DRAWITEM消息处理 void CSerialToolDlg::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) { if (nIDCtl == IDC_LOG_LIST) { // 双缓冲绘制代码 } CDialog::OnDrawItem(nIDCtl, lpDrawItemStruct); }- 限制日志条目:当条目超过一定数量时自动清理旧的
void CSerialToolDlg::AddLog(LPCTSTR text) { // ... 原有代码 if (pList->GetCount() > MAX_LOG_ITEMS) { pList->DeleteString(0); } }5.2 提高通信可靠性
- 添加超时处理:设置读写超时避免阻塞
m_serialPort.setReadTimeout(500); // 500ms读超时 m_serialPort.setWriteTimeout(500); // 500ms写超时- 实现流量控制:根据硬件需求启用RTS/CTS或XON/XOFF
m_serialPort.setFlowControl(itas109::FlowControl::FLOW_CONTROL_RTS_CTS);5.3 跨线程UI更新
CSerialPort的回调可能在工作线程中触发,直接更新UI会导致问题。安全的方式是使用PostMessage:
- 定义自定义消息:
#define WM_UPDATE_LOG WM_USER + 1- 添加消息处理函数:
afx_msg LRESULT OnUpdateLog(WPARAM wParam, LPARAM lParam); BEGIN_MESSAGE_MAP(CSerialToolDlg, CDialog) ON_MESSAGE(WM_UPDATE_LOG, OnUpdateLog) END_MESSAGE_MAP()- 修改接收回调:
void CSerialToolDlg::onReadEvent(const char* portName, unsigned int readBufferLen) { // ... 读取数据 CString* pStr = new CString(_T("接收: ") + recvText); PostMessage(WM_UPDATE_LOG, (WPARAM)pStr); } LRESULT CSerialToolDlg::OnUpdateLog(WPARAM wParam, LPARAM lParam) { CString* pStr = (CString*)wParam; AddLog(*pStr); delete pStr; return 0; }6. 项目打包与部署
6.1 静态链接CSerialPort
为避免依赖动态库,可以将CSerialPort静态链接:
- 在CSerialPort源码目录中找到或创建静态库项目
- 编译生成.lib文件
- 在主项目中链接该.lib文件
6.2 减少运行时依赖
确保程序能在没有安装VC++运行库的机器上运行:
- 项目属性 → C/C++ → 代码生成 → 运行库 → 选择"/MT"
- 或者打包对应的VC++运行库合并模块
6.3 创建安装程序
使用Visual Studio的安装项目或其他工具(如Inno Setup)创建安装包,包含:
- 主程序文件
- 必要的运行时库
- 串口驱动检测工具
- 开始菜单快捷方式
- 桌面图标
7. 进阶功能展望
虽然我们已经实现了一个功能完整的串口工具,但仍有扩展空间:
- 协议解析:添加MODBUS、NMEA等常见协议解析功能
- 数据可视化:绘制接收数据的波形图或曲线图
- 脚本支持:集成Lua或Python脚本实现自动化测试
- 多串口支持:同时监控和操作多个串口设备
- 云端同步:将通信记录同步到云端进行分析
在实际项目中,我发现最实用的功能往往是那些能解决特定痛点的定制化功能。比如,曾经为一家传感器厂商开发时,我们添加了自动解析传感器数据包并生成报表的功能,这大大提高了他们的测试效率。