告别串口调试助手!用CSerialPort和MFC打造你的专属串口测试工具(附完整源码)
基于CSerialPort与MFC的工业级串口调试工具开发实战在嵌入式开发和工业控制领域串口通信作为最基础也最可靠的通信方式之一其调试工作占据了开发者大量时间。虽然市面上有各种通用串口调试助手但当面对特定项目需求时这些工具往往显得力不从心——要么功能冗余导致操作复杂要么缺少项目所需的特定功能。本文将带你从零开始利用CSerialPort开源库和MFC框架打造一个完全贴合自身项目需求的定制化串口调试工具。与简单调用现成工具不同自主开发串口工具可以带来三个显著优势功能定制化完全按照项目需求设计、流程集成化与现有开发环境无缝衔接和数据可视化根据项目特点优化显示方式。我们选择的CSerialPort库不仅支持Windows平台还能轻松移植到Linux系统为未来可能的跨平台需求预留了扩展空间。而MFC框架则提供了快速构建图形界面的能力特别适合需要频繁交互的调试工具开发。1. 开发环境配置与项目初始化1.1 开发环境准备工欲善其事必先利其器。在开始编码前我们需要准备以下开发环境Visual Studio 2019社区版即可建议版本15.9以上Windows 10 SDK版本10.0.17763.0或更高MFC支持安装VS时需勾选MFC和ATL支持Git客户端用于获取CSerialPort源码提示虽然VS 2008也能完成开发但使用较新版本可以获得更好的C标准支持和更现代的调试体验。1.2 创建MFC对话框项目启动Visual Studio按照以下步骤创建项目选择文件→新建→项目在模板中选择MFC应用程序命名项目为SerialTool解决方案名称自动同步在应用程序类型中选择基于对话框取消勾选使用Unicode库CSerialPort当前版本对ANSI支持更好完成向导保持其他选项为默认值创建完成后解决方案目录结构应如下所示SerialTool/ ├── SerialTool.sln ├── SerialTool/ │ ├── SerialTool.aps │ ├── SerialTool.cpp │ ├── SerialTool.h │ ├── SerialTool.rc │ ├── SerialToolDlg.cpp │ ├── SerialToolDlg.h │ ├── res/ │ └── ...其他MFC标准文件1.3 集成CSerialPort库CSerialPort作为开源库我们可以直接从代码仓库获取最新版本cd SerialTool git clone https://github.com/itas109/CSerialPort.git克隆完成后项目目录中将新增CSerialPort文件夹包含所有必要的头文件和源文件。接下来需要配置项目属性使主项目能够正确引用CSerialPort右键项目→属性→C/C→常规在附加包含目录中添加$(ProjectDir)\CSerialPort\include切换到链接器→输入在附加依赖项中添加setupapi.lib2. 核心功能模块实现2.1 串口基础功能封装在SerialToolDlg.h中我们需要添加CSerialPort的引用并设置事件监听// SerialToolDlg.h #pragma once #include CSerialPort/SerialPort.h #include CSerialPort/SerialPortInfo.h class CSerialToolDlg : public CDialog, public itas109::CSerialPortListener { // ...其他代码 private: itas109::CSerialPort m_serialPort; void onReadEvent(const char* portName, unsigned int readBufferLen) override; // ...其他成员变量和函数 };在SerialToolDlg.cpp中实现基本的串口操作// SerialToolDlg.cpp BOOL CSerialToolDlg::OnInitDialog() { CDialog::OnInitDialog(); // 初始化串口事件监听 m_serialPort.connectReadEvent(this); // ...其他初始化代码 } void CSerialToolDlg::onReadEvent(const char* portName, unsigned int readBufferLen) { if(readBufferLen 0) { char data[4096]; // 适当增大缓冲区 int recLen m_serialPort.readData(data, min(readBufferLen, 4095)); if (recLen 0) { data[recLen] \0; // 将数据传递给UI线程显示 CString strData(data); PostMessage(WM_UPDATE_RECV_CTRL, (WPARAM)strData.AllocSysString(), recLen); } } }2.2 串口参数配置界面设计一个专业的串口工具需要提供完整的参数配置选项。在对话框资源编辑器中添加以下控件控件类型ID用途Combo BoxIDC_PORT_COMBO选择串口号Combo BoxIDC_BAUD_COMBO波特率选择(9600-115200)Combo BoxIDC_DATABITS_COMBO数据位(5-8)Combo BoxIDC_PARITY_COMBO校验位(None,Odd,Even)Combo BoxIDC_STOPBITS_COMBO停止位(1,1.5,2)ButtonIDC_OPEN_BTN打开/关闭串口在OnInitDialog()中初始化这些控件的默认值// 初始化串口号 CComboBox* pPortCombo (CComboBox*)GetDlgItem(IDC_PORT_COMBO); std::vectoritas109::SerialPortInfo portList itas109::CSerialPortInfo::availablePorts(); for (const auto port : portList) { pPortCombo-AddString(CString(port.portName.c_str())); } // 初始化波特率 CComboBox* pBaudCombo (CComboBox*)GetDlgItem(IDC_BAUD_COMBO); int baudRates[] {9600, 19200, 38400, 57600, 115200}; for (int rate : baudRates) { CString str; str.Format(_T(%d), rate); pBaudCombo-AddString(str); } pBaudCombo-SetCurSel(4); // 默认1152002.3 数据收发功能实现发送功能的实现相对直接在发送按钮的事件处理函数中添加void CSerialToolDlg::OnBnClickedSendBtn() { if (!m_serialPort.isOpen()) { MessageBox(_T(请先打开串口), _T(警告), MB_ICONWARNING); return; } CString strData; GetDlgItemText(IDC_SEND_EDIT, strData); if (strData.IsEmpty()) { MessageBox(_T(发送内容不能为空), _T(警告), MB_ICONWARNING); return; } // 根据是否需要十六进制发送进行处理 if (((CButton*)GetDlgItem(IDC_HEX_SEND))-GetCheck() BST_CHECKED) { // 十六进制发送处理 CStringA strDataA(strData); // ... 十六进制转换逻辑 } else { // 普通文本发送 CStringA strDataA(strData); m_serialPort.writeData(strDataA.GetBuffer(), strDataA.GetLength()); } }接收数据的显示需要考虑性能问题特别是高速数据接收时。我们采用以下优化策略使用双缓冲机制减少UI刷新次数限制最大显示行数避免内存耗尽提供暂停显示功能方便分析数据// WM_UPDATE_RECV_CTRL消息处理函数 LRESULT CSerialToolDlg::OnUpdateRecvCtrl(WPARAM wParam, LPARAM lParam) { if (m_bPauseDisplay) return 0; CString strData((LPCTSTR)wParam); int dataLen (int)lParam; CEdit* pRecvEdit (CEdit*)GetDlgItem(IDC_RECV_EDIT); CString strCurrent; pRecvEdit-GetWindowText(strCurrent); // 限制最大行数 int nLineCount pRecvEdit-GetLineCount(); if (nLineCount MAX_RECV_LINES) { // ... 截断旧数据 } // 添加新数据 if (((CButton*)GetDlgItem(IDC_HEX_DISPLAY))-GetCheck() BST_CHECKED) { // 十六进制显示处理 // ... } else { strCurrent strData; } pRecvEdit-SetWindowText(strCurrent); pRecvEdit-LineScroll(pRecvEdit-GetLineCount()); ::SysFreeString((BSTR)wParam); return 0; }3. 高级功能扩展3.1 数据日志记录调试过程中记录通信数据对于问题分析至关重要。我们扩展日志记录功能支持以下特性自动按日期创建日志文件可选记录发送数据、接收数据或两者时间戳精确到毫秒文件大小限制和自动分割void CSerialToolDlg::WriteToLog(const CString strData, LogType type) { if (!m_bEnableLogging) return; CTime time CTime::GetCurrentTime(); CString strLogFile; strLogFile.Format(_T(Logs\\SerialLog_%04d%02d%02d.log), time.GetYear(), time.GetMonth(), time.GetDay()); // 确保Logs目录存在 CreateDirectory(_T(Logs), NULL); CStdioFile file; if (file.Open(strLogFile, CFile::modeCreate | CFile::modeNoTruncate | CFile::modeWrite)) { file.SeekToEnd(); CString strLogLine; strLogLine.Format(_T([%02d:%02d:%02d.%03d] %s\r\n), time.GetHour(), time.GetMinute(), time.GetSecond(), GetTickCount() % 1000, strData); file.WriteString(strLogLine); file.Close(); } }3.2 数据协议解析针对特定协议的数据我们可以添加解析功能。以Modbus RTU协议为例void CSerialToolDlg::ParseModbusRTU(const BYTE* data, int length) { if (length 4) return; // Modbus RTU最小帧长 // 计算CRC校验 WORD crc CalculateCRC(data, length - 2); WORD frameCrc MAKEWORD(data[length-1], data[length-2]); if (crc ! frameCrc) { AddToProtocolDisplay(_T(CRC校验失败)); return; } BYTE slaveAddr data[0]; BYTE functionCode data[1]; CString strInfo; strInfo.Format(_T(从站地址: %d, 功能码: %02X), slaveAddr, functionCode); AddToProtocolDisplay(strInfo); // 根据功能码进一步解析 switch (functionCode) { case 0x01: // 读线圈 case 0x02: // 读离散输入 // ... 解析逻辑 break; case 0x03: // 读保持寄存器 case 0x04: // 读输入寄存器 // ... 解析逻辑 break; // ... 其他功能码处理 } }3.3 自动化测试脚本为方便重复测试我们可以实现简单的脚本功能void CSerialToolDlg::RunTestScript(const CString strScript) { CStringArray lines; SplitString(strScript, _T(\n), lines); for (int i 0; i lines.GetSize(); i) { CString line lines[i].Trim(); if (line.IsEmpty() || line[0] _T(#)) continue; // 解析延时命令 if (line.Left(5).CompareNoCase(_T(DELAY)) 0) { int nDelay _ttoi(line.Mid(5)); Sleep(nDelay); continue; } // 解析发送命令 if (line.Left(4).CompareNoCase(_T(SEND)) 0) { CString strData line.Mid(4).Trim(); if (!strData.IsEmpty()) { CStringA strDataA(strData); m_serialPort.writeData(strDataA.GetBuffer(), strDataA.GetLength()); } continue; } // 其他命令... } }4. 项目构建与部署4.1 发布版本配置在项目开发完成后我们需要配置发布版本切换到Release配置项目属性→C/C→优化→选择最大优化(优选速度)链接器→优化→选择优化引用链接器→清单文件→生成清单选择否4.2 静态链接MFC库为使程序可以独立运行我们需要静态链接MFC库项目属性→常规→MFC的使用→选择在静态库中使用MFC项目属性→C/C→代码生成→运行库→选择多线程(/MT)4.3 打包依赖项使用Windows Installer或第三方打包工具如Inno Setup创建安装包时需要包含生成的EXE文件必要的DLL如果使用动态链接示例配置文件使用说明文档可以创建一个批处理文件自动完成这些步骤echo off set BUILD_DIRRelease set OUTPUT_DIRPackage mkdir %OUTPUT_DIR% copy %BUILD_DIR%\SerialTool.exe %OUTPUT_DIR% copy Readme.txt %OUTPUT_DIR% copy Config.ini %OUTPUT_DIR% echo 打包完成输出目录: %OUTPUT_DIR% pause5. 实际应用中的性能优化在工业现场应用中串口工具的性能和稳定性至关重要。以下是几个关键优化点5.1 接收数据缓冲优化#define RECV_BUFFER_SIZE 8192 // 8KB缓冲区 class CSerialToolDlg : public CDialog { // ... private: CRITICAL_SECTION m_csRecvBuffer; // 缓冲区访问临界区 std::vectorchar m_recvBuffer; // 接收数据缓冲区 // ... }; // 在构造函数中初始化 CSerialToolDlg::CSerialToolDlg(CWnd* pParent /*nullptr*/) : CDialog(IDD_SERIALTOOL_DIALOG, pParent) { InitializeCriticalSection(m_csRecvBuffer); m_recvBuffer.reserve(RECV_BUFFER_SIZE * 2); // ... } // 接收数据处理 void CSerialToolDlg::onReadEvent(const char* portName, unsigned int readBufferLen) { EnterCriticalSection(m_csRecvBuffer); // 检查缓冲区剩余空间 if (m_recvBuffer.size() readBufferLen m_recvBuffer.capacity()) { // 处理缓冲区溢出... } else { m_recvBuffer.insert(m_recvBuffer.end(), portName, portName readBufferLen); } LeaveCriticalSection(m_csRecvBuffer); // 通知UI线程更新显示 PostMessage(WM_UPDATE_RECV_CTRL, 0, 0); }5.2 发送数据流量控制void CSerialToolDlg::OnBnClickedSendBtn() { static DWORD lastSendTime 0; DWORD currentTime GetTickCount(); // 发送间隔限制(100ms) if (currentTime - lastSendTime 100) { MessageBox(_T(发送频率过高请稍后再试), _T(警告), MB_ICONWARNING); return; } lastSendTime currentTime; // ...原有发送逻辑 }5.3 多线程处理模型对于高负载场景建议采用生产者-消费者模型// 工作线程处理函数 UINT CSerialToolDlg::SerialThreadProc(LPVOID pParam) { CSerialToolDlg* pThis (CSerialToolDlg*)pParam; while (pThis-m_bThreadRunning) { // 检查并处理接收数据 pThis-ProcessReceivedData(); // 检查发送队列 pThis-ProcessSendQueue(); // 适当休眠避免CPU占用过高 Sleep(10); } return 0; } void CSerialToolDlg::ProcessReceivedData() { EnterCriticalSection(m_csRecvBuffer); if (!m_recvBuffer.empty()) { // 复制数据到临时缓冲区 std::vectorchar tempBuffer(m_recvBuffer.begin(), m_recvBuffer.end()); m_recvBuffer.clear(); LeaveCriticalSection(m_csRecvBuffer); // 处理数据(可耗时操作) ParseProtocolData(tempBuffer.data(), tempBuffer.size()); } else { LeaveCriticalSection(m_csRecvBuffer); } }