拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于C++ MFC的RS485串口通信上位机Demo完整实现

简介基于C MFC实现的RS485串口通信完整Demo面向工业自动化、物联网设备及串口通信入门开发者可在Visual Studio 2015环境下直接编译运行。资源包共22个文件压缩后仅52KB包含4个h头文件、3个cpp源文件、2个txt说明文档以及sln/suo/vcxproj工程配置文件、rc资源文件和图标等其中头文件负责接口声明cpp源文件实现具体通信逻辑txt文件可用于查阅使用说明整体工程结构完整方便查看与二次修改。目前已有3197人学习下载。代码演示了CSerialPort类的创建、串口参数配置波特率/数据位/停止位/校验位、打开/读写/关闭串口的完整流程并带有接收事件处理框架同时展示了RS485半双工通信的基本用法。通过该Demo读者可快速掌握MFC下RS485串口通信的实现思路为实际项目中的错误处理、数据校验与通信协议制定提供可参考的代码基础。 做设备联调这些年我一直觉得手头得有个趁手的串口调试工具。最近帮现场同事整理一套上位机示例正好把基于C、MFC框架的RS485串口通信demo完整代码捋了一遍。这套demo不大但是串口通信该有的环节全都有打开/关闭串口、参数配置、接收线程、界面实时显示装完就能跑。非常适合刚接触工控上位机的朋友入门也适合那些要快速给设备写临时候测工具、又不想从头啃Win32 API的工程师直接抄作业。如果你接手的老项目里还有大量MFC界面代码这篇就更有参考价值了。1. 这个demo到底做了什么需求拆解与整体设计思路1.1 RS485到底是什么工业现场为什么至今离不开它RS485是工业现场最常见的物理层总线标准之一。它只用两根线A和B通过差分电压传输信号逻辑“1”和“0”由两根线之间的电压差决定所以抗共模干扰能力强传输距离能达到上千米还能挂载几十个节点一起组网。相比RS232的单端传输方式RS485在噪声大的车间、电力柜旁边要稳定得多。但要注意一个关键特性RS485是半双工通信。也就是说同一时刻只能有一个方向的数据在链路上传输要么发、要么收。这跟网线的全双工不一样所以在上位机软件层面有时候需要控制收发方向的切换。很多USB转485模块内部已经做了自动收发转换但工业场合的自制485板卡很多还要求程序手动控制方向这是后面代码部分的一个隐藏坑。为什么这么多年还是离不开RS485因为现场存量设备实在太多了。变频器、智能电表、温控器、PLC、气象站、各种传感器绝大多数都保留RS485接口走的还是Modbus-RTU这类老协议。上位机只要能通过串口把数据读上来整个系统就能跑通。这也是我始终建议工控上位机开发者把串口通信吃透的原因——它是和硬件打交道的基本功。1.2 选型思考为什么用C/MFC而不是别的框架串口通信上位机可以选C#、Qt、Python为什么我这次还是用C/MFC第一是存量兼容。很多设备厂商的SDK、DLL示例工程还是老式MFC写的现场工程师拿到的参考代码就是CString、CWnd这一套如果不会MFC连厂商的例程都看不懂。第二是部署环境。有些工控机是老旧Windows系统装个轻量MFC程序比部署.NET环境省事得多。第三是调试手感。MFC对话框工程对Win32 API几乎零封装成本串口编程用到的CreateFile、WaitCommEvent这些底层函数在MFC里随便调没有托管代码那层隔阂。当然MFC的界面老旧、控件不够美观也是事实。但作为工具型demo界面实用性优先用标准按钮、编辑框、下拉框足够了。之前也有同事用Qt重写过一版效果确实好看但工程体积和依赖项复杂不少。对于工控现场“快速实现、稳定运行”的需求MFC依然是一个很务实的选择。再说个细节传统教材里喜欢用MSComm控件做串口但我自己在实际工程里几乎不碰它。MSComm依赖OCX注册打包分发麻烦64位系统兼容性还不稳定事件回调模型也绕。直接用Windows API操作串口函数就那么几个写一次到处能用代码完全可控。这个demo采用的就是纯API方案。1.3 demo功能清单与架构分工先把这个demo能干什么列清楚后续看代码就有的放矢。串口参数配置串口号、波特率、数据位、停止位、校验位全部下拉可选。串口打开/关闭带状态指示打开失败有错误提示。数据发送支持ASCII文本和十六进制两种输入方式。数据接收独立线程监听实时显示不乱码、不卡界面。收发日志接收区自动加时间戳方便现场核对数据帧顺序。清空与计数发送字节数、接收字节数实时统计。线程安全退出关闭串口时能干净地停掉接收线程不崩溃。整个架构分成三块界面层负责控件交互业务层负责数据格式转换和显示串口层只做最底层的收发操作。分层的目的很直接——以后想加协议解析比如解析Modbus帧只需要在业务层加函数不用动串口底层排查问题也方便。2. 串口通信核心原理与关键代码解析2.1 打开串口CreateFile这一步的细节Windows里操作串口本质上就是操作一个文件设备所以打开串口的函数是CreateFile。这个函数人人会写但细节极多。下面这段是我在demo里用的完整打开逻辑m_hCom CreateFile( _T(\\\\.\\COM3), // 设备路径COM10以后必须加\\.\前缀 GENERIC_READ | GENERIC_WRITE, // 读写权限 0, // 串口独占不能共享 NULL, OPEN_EXISTING, // 串口必须用OPEN_EXISTING FILE_ATTRIBUTE_NORMAL, // 同步方式线程里做独立接收 NULL ); if (m_hCom INVALID_HANDLE_VALUE) { CString strErr; strErr.Format(_T(打开串口失败错误码%d), GetLastError()); AfxMessageBox(strErr); return; }先说设备路径COM1到COM9可以直接写COM1但COM10以上必须写成\\\\.\\COM10否则打开失败。我见过不少项目在笔记本电脑上中招一台机器串口号一多就诡异打不开其实就是这个反斜杠转义的问题。为了省事demo里统一都用\\\\.\\COM%d格式。再说打开方式串口是独占设备dwShareMode参数必须填0。如果你发现程序在串口助手里测试正常但自己写代码打开失败大概率就是那个软件还没退出或者上次进程异常结束没释放句柄。GetLastError返回5就是“拒绝访问”这个最典型。2.2 参数配置DCB结构体与超时设置打开串口后第一件事就是把通信参数配好。核心是DCBDevice Control Block结构体。给一份配置代码DCB dcb { 0 }; dcb.DCBlength sizeof(DCB); GetCommState(m_hCom, dcb); // 先取当前参数再改要改的项 dcb.BaudRate 9600; // 波特率和设备保持一致 dcb.ByteSize 8; // 数据位工业设备基本都8位 dcb.Parity NOPARITY; // 无校验 dcb.StopBits ONESTOPBIT; // 1位停止位 // 关闭流控RS485一般不启用硬件流控 dcb.fOutxCtsFlow FALSE; dcb.fOutxDsrFlow FALSE; dcb.fDtrControl DTR_CONTROL_DISABLE; dcb.fOutX FALSE; dcb.fInX FALSE; SetCommState(m_hCom, dcb);波特率选9600还是115200取决于设备。旧设备往往只支持9600新设备大多能跑115200或更高。注意一点波特率越高同样线长下信号质量越差现场长距离布线宁可用9600也别贸然上高速。校验位、停止位必须和设备侧完全一致否则能收到数据但解析出来全是乱的。超时设置是很多人忽略的重点。串口数据不像TCP有明确的消息边界设备可能任何时候发来一串字节。如果不设置超时ReadFile会一直阻塞等待程序就卡死了。所以要用COMMTIMEOUTS设置读超时COMMTIMEOUTS timeouts { 0 }; timeouts.ReadIntervalTimeout 50; // 字符间隔超过50ms认为帧结束 timeouts.ReadTotalTimeoutMultiplier 10; timeouts.ReadTotalTimeoutConstant 100; // 总体读超时100ms SetCommTimeouts(m_hCom, timeouts);ReadIntervalTimeout是这几个参数里最有用的通信总是一个字节一个字节地到它规定两个字节之间最大间隔一旦超过就立刻把缓冲区里已有的数据交出来。这样即使设备一帧数据不连续也不至于卡死等待。2.3 接收线程与事件驱动为什么不能直接轮询串口数据什么时候来上位机完全控制不了。如果你在界面里放一个定时器不停去ReadFile界面会卡、数据还可能丢。正统做法是开一个接收线程用事件驱动方式等数据。UINT ReceiveThreadProc(LPVOID pParam) { CMy485Dlg* pDlg (CMy485Dlg*)pParam; BYTE buf[1024] { 0 }; DWORD dwEvent 0; // 只监听收到字符这个事件 SetCommMask(pDlg-m_hCom, EV_RXCHAR); while (pDlg-m_bThreadRun) { if (!WaitCommEvent(pDlg-m_hCom, dwEvent, NULL)) { // 返回FALSE说明出错或句柄被关闭直接退出 break; } DWORD dwLen 0; ClearCommError(pDlg-m_hCom, NULL, dwLen); if (dwLen 0 dwLen sizeof(buf)) { DWORD dwRead 0; if (ReadFile(pDlg-m_hCom, buf, dwLen, dwRead, NULL)) { // 关键不能在线程里直接更新UI必须扔给主窗口 pDlg-PostMessage(WM_DATA_RECEIVED, dwRead, (LPARAM)new CByteArray(buf, dwRead)); } } } return 0; }线程模型里的核心难点不是收数据而是“线程不能操作UI”。MFC的窗口、控件都属于主线程工作线程里直接改编辑框内容会闪退或者显示异常。所以这里用PostMessage把接收到的字节数组发给主窗口主窗口在消息处理函数里更新接收区显示。这个设计几乎是所有串口上位机必须遵守的规矩。另外我特意把数据包装成CByteArray用指针传过去接收方记得delete。如果不走堆内存也可以定义一个自定义结构体把长度和字节数组一起发过去。核心思路就是跨线程传数据必须复制一份不能在线程退出后访问已释放的内存。3. 从建工程到跑通收发完整实操过程3.1 工程创建与界面布局打开Visual Studio我用的是VS20192015/2013也能跑选择“MFC应用”应用程序类型选“基于对话框”其余默认即可。注意字符集选“使用Unicode字符集”现在的设备厂商库和新代码基本都兼容Unicode少给自己找麻烦。界面布局建议从上到下分四块。第一块是参数区放两个下拉框串口号、波特率再加一个“打开串口”按钮。第二块是发送区一个多行编辑框用来输入要发的数据一个“发送”按钮再放两个单选按钮切换“ASCII/Hex”输入格式。第三块是接收区一个多行编辑框属性里设置“只读”为True垂直滚动条打开。第四块是辅助功能清空显示、发送计数、接收计数外加一个状态文本提示当前串口状态。控件建好后给主要控件添加关联变量m_comCombo绑定串口下拉框m_baudCombo绑定波特率下拉框m_editRecv绑定接收编辑框m_editSend绑定发送编辑框。MFC里可以在类向导里快速添加按下CtrlW呼出类向导选中对话框类就能看到成员变量页签。我建议在OnInitDialog里把串口号和波特率先填充好波特率就是那几个常用档位2400、4800、9600、19200、38400、115200。串口号如果嫌枚举麻烦可以先放一个默认COM1再让用户手动修改。更省事一点的写法是读取注册表HARDWARE\DEVICEMAP\SERIALCOMM把当前机器所有的真实串口号枚举出来这个后续可自行扩展。3.2 核心收发逻辑实现发送数据的封装很简单就是把编辑框内容按ASCII或Hex方式转成字节然后WriteFile写出去。Hex转换是工控调试的刚需因为设备协议里大量字段都是16进制表示的比如发01 03 00 00 00 0A这个请求帧写成ASCII设备根本看不懂。void CMy485Dlg::OnBnClickedBtnSend() { CString strData; m_editSend.GetWindowText(strData); if (strData.IsEmpty()) return; BYTE buf[512] { 0 }; DWORD len 0; if (m_bHexSend) { len HexStringToBytes(strData, buf, sizeof(buf)); } else { len WideCharToMultiByte(CP_ACP, 0, strData, -1, (char*)buf, sizeof(buf)-1, NULL, NULL); if (len 0) len--; // 去掉结尾\0 } if (len 0 m_hCom ! INVALID_HANDLE_VALUE) { DWORD written 0; if (!WriteFile(m_hCom, buf, len, written, NULL)) { AfxMessageBox(_T(发送失败请检查串口状态)); } else { m_sendCount written; } } }接收消息处理里做的第一件事是取出字节数组然后判断显示格式。ASCII模式直接转成CStringHex模式一个字节拼两个字符输出。为了现场排查方便我在每条数据前加了时间戳格式是[HH:mm:ss.fff]这样看到某条数据是几点几分几百毫秒收到的对判断设备响应超时很有帮助。接收区显示还有一个细节数据量大了之后接收编辑框会越来越卡。建议在每次添加显示内容前先看一下文本长度超过比如2万字符就自动清掉一半保证界面流畅。这个优化加在OnDataReceived里几行代码的事但体验差很多。3.3 硬件自测与demo演示流程没有真实设备怎么验证demo好不好用最经典的自测方法是用两个USB转RS485模块把A接A、B接B然后分别插到电脑上。程序打开第一个模块对应的串口发送另一个串口就能收到反之亦然。没有两个模块的话USB转RS232模块也可以直接把TX和RX短接实现自发自收的回环测试。我用的是两个USB转485模块加一根短双绞线连接好后的实操流程如下。先打开demo串口号分别对应两个模块波特率都选9600打开第一个串口在发送区输入Hello RS485点发送打开第二个串口的demo实例或同一个demo换串口重开如果能看到地址栏自动刷新并收到数据说明整个链路收发正常。此时观察USB转485模块的TX、RX指示灯会交替闪烁对应数据流方向。如果不想开两个程序也可以把同一个串口打开后把发送区连接到接收区做一个本机自测模式。更严格的做法是用逻辑分析仪去抓A/B两线的差分波形但现场调试大多数情况靠接收区显示和指示灯就能判断八九不离十。固定的演示顺序我建议走“参数设置→打开串口→输入数据→发送→接收区确认”每一步都能通过控件状态判断是否成功新手照着走一遍就心里有底了。4. 实战踩坑记录常见问题与排查技巧4.1 串口打不开、被占用如何处理这个问题在工控现场出现频率极高。现象就是点“打开串口”按钮弹窗提示“打开串口失败”错误码要么是5拒绝访问要么是2文件不存在。先说错误码599%的情况是串口被其他程序独占。常见元凶包括设备厂商的配置软件、上一个没正常关闭的调试程序、操作系统自带的“电话和调制解调器”诊断工具。处理方式很笨但管用挨个关掉可疑程序或者重启电脑再试。错误码2则大概率是串口号根本不存在最常见的原因是USB转串口适配器的驱动没装好或者插了USB口但系统没识别。这时候去设备管理器看端口COM和LPT列表确认实际串口号再看看设备前有没有黄色感叹号。有感叹号就重装驱动没有感叹号就用实际串口号替换程序里的默认值。还有一类隐蔽问题程序崩溃过串口句柄没关闭。Windows在进程结束时会自动回收句柄理论上系统重启后肯定能打开。如果某个串口一直被占用用工具查一下进程是哪个或者直接重启系统比花时间排查快得多。4.2 数据乱码、丢帧、线程卡死乱码是所有串口调试里最容易让人抓狂的问题。我总结过排查顺序。第一步确认波特率、数据位、停止位、校验位两端完全一致尤其是校验位设备是偶校验、上位机配成无校验就会时不时冒出一个错字符。第二步检查RS485的A/B线有没有接反接反通常表现为完全收不到数据不会乱码但也要排除。第三步检查接地。485虽然是差分传输但在雷击或大功率设备启动时如果两端地电位差太大照样出现零星乱码这时候可以在A/B线间接120欧终端电阻试试。至于丢帧多半不是串口丢的而是接收线程处理不及时。比如你在接收线程里做了太多耗时的协议解析操作处理不过来底层驱动缓冲区又太小数据就被覆盖了。解决思路就是接收线程只管收和转发所有解析、显示、存储全部放到主线程或者单独的解析线程去处理。这个demo里的分层设计就是为了这个。线程卡死最经典的发生点是关闭串口时先CloseHandle了句柄而接收线程还阻塞在WaitCommEvent里面。这里有两个办法正常退出时先把m_bThreadRun置为FALSE然后WaitForSingleObject等线程退出在等之前再发出一个“取消阻塞”的信号。实操中我常用更粗暴的方法关闭串口前先调用CancelIo取消所有IO操作让WaitCommEvent立即返回错误从而退出循环这招对同步句柄尤其好用。4.3 常见问题速查表问题现象可能原因排查与处理办法CreateFile返回INVALID_HANDLE_VALUE错误码5串口被其他程序占用关闭占用程序必要时重启电脑打开串口报错误码2串口号不存在或驱动未装查看设备管理器实际COM号重装驱动能收到数据但全是乱码波特率/校验位等参数不一致或A/B接反统一两端参数配置核对接线数据丢帧或粘包接收线程处理不及时、超时时间不合理接收线程只负责收发加大缓冲区调整超时关闭串口时程序卡死接收线程阻塞在WaitCommEvent使用CancelIo取消阻塞等线程完全退出后再关句柄发送失败WriteFile返回FALSE串口未打开或已被移除先判断句柄状态重插USB转485设备接收区刷新卡顿文本无限增长定期清理文本超过阈值自动截断这张表看起来简单但每一条背后都是实实在在趟过的坑。尤其是“接收线程必须安全退出”这条多少个同事的程序都在关窗口的时候崩过一次。我自己在后面所有串口项目里都固化成一套模板创建线程、线程循环、取消阻塞、等待退出、释放句柄顺序一步都不能错。最后再分享一个小技巧。现场联调时如果发现设备只能回复一帧、第二帧开始就没响应先不要怀疑程序逻辑先检查RS485的方向控制。有些设备要求上位机发送结束后必须保持一小段延时再切回接收状态否则设备刚回的第一帧会被上位机自己的发送波形盖住。解决办法也简单在发送函数最后加一个Sleep(20)或者根据波特率计算1字节的传输时间保证总线真正空闲后再开启接收。这类时序问题在高速率长线缆下特别明显搞工控的应该都懂。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门