十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

QT上位机通过周立功USBCAN实现CAN总线数据实时监控的实战方法

QT上位机通过周立功USBCAN实现CAN总线数据实时监控的实战方法 简介本资源是一套基于Qt框架实现周立功CAN总线通信的完整可运行工程面向嵌入式开发、汽车电子及工业自动化领域的中高级Qt开发者解决CAN设备接入、实时数据自动接收与解析等核心问题。压缩包共67个文件含34个XML配置文件定义设备参数与通信协议、15个DLL动态库含USBCAN、CANFD、CANET等驱动支持、7个头文件如zlgcan.h、canframe.h与4个CPP源码含mainwindow.cpp、candatabase.cpp等核心逻辑辅以INI配置、UI界面、PRO项目文件及ZLG官方PDF手册整体3.46MB结构清晰、即开即用。已有2348人学习下载提供从库集成、句柄初始化、ID过滤设置到循环接收解析的全链路实现含多线程安全处理与Qt信号槽绑定示例可直接部署于Windows平台对接周立功USB-CAN系列硬件大幅降低CAN通信模块开发门槛。 做嵌入式或者工业上位机开发的朋友肯定绕不过CAN通信。最近因为一个设备调试项目我需要在上位机里实时监控CAN总线上的数据并且要保证长时间运行不卡顿、不丢帧。我手上的工具是周立功的USBCAN设备上位机框架用的是QT。整个项目从驱动接入到数据稳定自动接收踩了一些坑也积累了一些经验整理出来给同样要做QT和周立功CAN通信的朋友做个参考。这个项目本质上解决的是两个问题第一怎么在QT的界面程序里高效调用周立功的CAN接口库把硬件收发通道打通第二如何实现数据的自动接收不能让界面卡死、不能丢数据还要把收到的报文实时显示出来。如果你正在做类似的上位机开发或者准备用QT配合周立功USBCAN做测试工具这篇文章应该能帮你节省不少时间。1. 项目背景与方案选型1.1 为什么选QT加周立功这套组合先说设备端的选择。周立功的USBCAN系列在工控和汽车电子领域用得非常广它最大的优势是把复杂的CAN控制器操作封装成了统一的动态库接口我们不用直接操作USB驱动和寄存器只要调用VCI开头的几个函数就能完成打开设备、初始化、收发数据这些操作。而且周立功官方给的DEMO基本都是C写的和QT是天然契合的。上层框架选QT原因也很直接。QT的跨平台能力、信号槽机制和丰富的控件库让开发上位机界面的效率比用MFC高出不少。特别是涉及到后续可能要扩展数据曲线绘制、日志记录、协议解析这些功能时QT的工具链和社区资源都比较成熟。我用的版本是QT 5.15.2搭配MSVC2015编译环境这套组合在Windows下工作很稳定。1.2 整体架构设计整个项目的架构我是这样设计的底层是一层封装好的CAN通信管理类负责和周立功动态库交互中间层是线程管理和数据分发模块最上层就是QT的界面显示层。架构分层的核心原因是为了把硬件通信逻辑和界面逻辑解耦。如果直接在界面按钮里调用CAN接收函数在接收数据量大或者总线繁忙的时候界面线程会被阻塞表现出来就是窗口无响应、拖拽卡顿。所以我的设计里接收数据的动作被放到一个独立的工作线程中收到数据后通过QT的信号槽机制通知界面刷新这样界面永远只做显示这一个任务。1.3 项目最终效果实测下来最终的程序运行起来以后能够稳定自动接收CAN总线上的数据帧界面显示流畅连续运行几个小时没有出现卡死或者内存持续增长的问题。我的测试环境是周立功USBCAN-II设备波特率配置为500Kbps总线上有另一个节点周期性发送报文发送间隔为10ms。程序每秒能收到约100帧数据CPU占用率维持在很低的水平。2. 环境准备与驱动接入2.1 开发环境搭建要点QT安装这块我不展开太多只说几个容易出问题的细节。如果你是在Windows下用MSVC编译套件一定要把QT版本对应的编译器版本和调试器配对上否则编译的时候会报一堆莫名其妙的头文件错误。我自己用的是QT 5.15.2加VS2015编译环境这两个版本搭配在周立功动态库调用上没有任何问题。周立功的驱动安装也是一步关键操作。到官网下载对应的USBCAN驱动包插上设备后系统会自动识别并安装驱动也可以在设备管理器里手动更新驱动路径指向驱动包目录。这里有个非常容易踩的坑即使驱动装好了如果你使用官方库文件的方式调用一定要确认你是从正确的路径加载了ControlCAN.dll以及头文件和库文件是否匹配。我遇到过几次运行时报“无法定位程序输入点”的情况最后排查都是因为动态库和头文件版本不一致导致的。2.2 周立功驱动接入方式周立功USBCAN的接入方式有两种一种是直接安装官方驱动把ControlCAN.dll放到程序目录下进行动态调用另一种是使用他们提供的库文件在编译时链接。我选择的是隐式链接方式也就是把ControlCAN.lib放在项目里进行静态链接运行时只需要把ControlCAN.dll复制到exe所在目录即可。注意这里要强烈建议你在工程文件中显式指定库路径并且保证Debug和Release两种模式下的库路径分开避免模式切换时链接到错误的库。我在项目里用的配置片段如下CONFIG(release, debug|release) { LIBS -L$$PWD/lib/ -lControlCAN } else { LIBS -L$$PWD/lib_debug/ -lControlCAN }使用相对路径的好处是换电脑编译的时候不用重新配置环境。2.3 核心接口说明周立功的ControlCAN.dll提供了一整套VCI接口我们最常用到的有以下几个接口函数功能说明VCI_OpenDevice打开USBCAN设备VCI_CloseDevice关闭设备VCI_InitCAN初始化指定的CAN通道VCI_StartCAN启动CAN通道VCI_Transmit发送CAN报文VCI_Receive接收CAN报文VCI_GetReceiveNum获取接收缓冲区报文数量VCI_ClearBuffer清空指定缓冲区这些接口的调用顺序是有讲究的。设备打开后必须先初始化对应的CAN通道然后再启动通道最后才能收发数据。顺序错了会导致收发失败或者行为异常。我在第一次迭代时就犯了这个错误直接调用了发送接口结果设备一点响应都没有后来逐行检查才发现是少了Init这一步。3. CAN通信参数配置与核心原理3.1 CAN帧结构基础CAN协议里的报文帧结构有几个关键字段帧ID、帧类型标准帧还是扩展帧、数据帧还是远程帧、数据长度DLC、数据内容。对于做上位机的开发人员来说帧ID和DLC是最常用的信息因为在解析报文时主要关注这两个字段。标准帧的ID长度是11位扩展帧的ID长度是29位。在周立功的接口中通过VCI_CAN_OBJ结构体的字段来区分比如ID字段填入帧IDSendType字段设置发送类型。我测试时用的节点ID是0x123这个ID在标准帧范围内在后续的滤波配置中也是基于这个ID来设计的。3.2 波特率参数计算CAN通信的波特率配置最终要填到一个十六进制的配置字里。周立功的初始化结构体VCI_INIT_CONFIG里有一个字段叫Timing0和Timing1这两个值组合决定了通信波特率。很多新手在这里卡住不知道这两个数值怎么来的。不同波特率对应不同的参数组合我整理了一份常用配置表方便大家直接查用波特率Timing0 (十六进制)Timing1 (十六进制)1Mbps0x000x14800Kbps0x000x16500Kbps0x000x1C250Kbps0x010x1C125Kbps0x030x1C100Kbps0x040x1C这里要特别提醒一个常识CAN总线上的所有节点波特率必须一致否则整个总线都会通信失败。排查的时候可以先检查总线上是否有其他设备在占用然后用万用表测量CAN_H和CAN_L之间的电压正常工作时会有约2V左右的差分电压。我测的是500Kbps对应的Timing0为0x00Timing1为0x1C实测通信很稳定。3.3 滤波掩码设置滤波掩码的设计是整个CAN通信中比较底层也比较容易绕晕的部分。周立功的滤波机制依赖两个参数验收码ACC和掩码AMR。简单理解掩码位为1表示这一位不关心为0表示这一位必须和验收码完全匹配。举个例子如果我只想接收ID为0x123这个标准帧验收码可以设置为0x00000123掩码设置为0xFFFFFF00表示高24位必须匹配后8位不关心。如果我想接收所有报文那就把掩码全部设为1即可。注意滤波配置影响的是硬件层面直接过滤。如果你的程序需要接收不同ID的报文一个简单的做法是把滤波关闭掩码全FF在软件层通过if判断ID来做过滤。这个方案灵活度更高也是我在这个项目中采用的方式。因为测试时总线上节点报文ID是固定的直接全收再在代码里判断即可省心。相关代码段如下VCI_INIT_CONFIG config; config.AccCode 0x00000000; config.AccMask 0xFFFFFFFF;// 关闭滤波 config.Filter 1; config.Timing0 0x00; config.Timing1 0x1C; config.Mode 0; // 0为正常模式4. 自动接收数据机制的实现4.1 为什么不能直接在界面线程里收数据这是很多初学者最容易踩的坑。有人图省事直接在定时器或者按钮事件里调VCI_Receive来收数据数据量小的时候看起来没问题但一旦总线上报文频率高界面就会出现两个典型问题一是界面卡顿因为大量的数据接收和处理占用了主线程时间二是数据丢失如果接收函数处理不及时硬件的接收缓冲区会被覆盖。我测试时总线上的报文频率是10ms一帧一秒钟100帧这种情况下如果在主线程里轮询接收偶尔会丢帧或者出现界面响应变慢。把接收放到独立线程后这两个问题都解决了。4.2 接收线程加信号槽方案我的设计方案是创建一个继承自QThread的CAN接收线程类在它的run函数里写一个while循环持续调用VCI_Receive读取数据。每当读到一个完整的报文就通过信号发送给主界面进行处理和显示。线程内的核心循环结构大概是这样void CANReceiveThread::run() { VCI_CAN_OBJ canObj[100]; while (!isInterruptionRequested()) { int count VCI_Receive(m_devType, m_devIndex, m_canIndex, canObj, 100, 50); if (count 0) { for (int i 0; i count; i) { emit receiveFrame(canObj[i]); } } QThread::msleep(5); } }这里有个经验VCI_Receive的最后一个参数是超时时间单位是毫秒我设置为50ms。这样在总线上没有数据时线程不会被完全阻塞死还能及时响应停止命令。同时在线程内做了5ms的休眠防止CPU占用率过高。4.3 数据缓冲与界面刷新平衡自动接收架构中最容易忽略的是界面刷新频率。如果每收一帧数据就触发一次信号在高速率报文场景下界面可能每秒刷新几百次这既浪费资源又可能造成界面闪烁。我采用了一种批量刷新的策略接收线程收完一批数据后把这一批数据通过一个自定义结构体发送出去界面在槽函数中一次性把多行数据显示到表格控件中。具体做法是累计一定数量的帧或者达到一定的时间间隔后再刷新界面。我的代码做法是维护一个临时列表每收满30帧或者超过200ms就发射一次批量信号QListVCI_CAN_OBJ batchList; if (batchList.size() 30 || time.elapsed() 200) { emit receiveBatch(batchList); batchList.clear(); time.restart(); }这个方案的直接收益就是UI响应快、不卡顿同时数据帧也不会因为刷新频率过高而丢失。5. 核心代码实战演示5.1 通信设备管理类封装为了方便界面层调用我把所有CAN设备操作封装成了一个单例类CANManager对外只暴露简洁的接口初始化设备、打开通道、发送数据、接收回调注册、关闭设备。类的核心结构如下class CANManager : public QObject { Q_OBJECT public: static CANManager* instance(); bool initDevice(int devType, int devIndex, int canIndex); bool start(); void stop(); void sendFrame(int id, QByteArray data); signals: void frameReceived(VCI_CAN_OBJ frame); private: CANReceiveThread* m_recvThread; };用单例模式的好处是全局只保留一份设备状态避免多个界面组件各自打开设备导致的资源冲突。发送功能也封装在这里外部只要填入ID和字节数组就行。5.2 打开设备与启动收发初始化设备这一段代码是标准流程必须要逐行看懂bool CANManager::initDevice(int devType, int devIndex, int canIndex) { if (VCI_OpenDevice(devType, devIndex, 0) ! STATUS_OK) { return false; } VCI_INIT_CONFIG config; memset(config, 0, sizeof(VCI_INIT_CONFIG)); config.AccCode 0x00000000; config.AccMask 0xFFFFFFFF; config.Filter 1; config.Timing0 0x00; config.Timing1 0x1C; config.Mode 0; if (VCI_InitCAN(devType, devIndex, canIndex, config) ! STATUS_OK) { return false; } if (VCI_StartCAN(devType, devIndex, canIndex) ! STATUS_OK) { return false; } m_recvThread new CANReceiveThread(devType, devIndex, canIndex); connect(m_recvThread, CANReceiveThread::receiveBatch, this, CANManager::frameReceived); m_recvThread-start(); return true; }这里面有一个值得注意的点VCI_OpenDevice第三个参数是保留参数填0即可。VCI_InitCAN的返回值一定要检查如果返回-1往往是波特率参数配置错误或者设备被占用。5.3 数据解析与界面展示收到CAN帧之后核心工作就是把帧ID和数据内容解析出来然后格式化显示到界面的表格控件中。我在槽函数里做如下处理void MainWindow::onFrameReceived(QListVCI_CAN_OBJ frames) { ui-tableWidget-setUpdatesEnabled(false); for (int i 0; i frames.size(); i) { VCI_CAN_OBJ obj frames.at(i); QString idStr QString(0x%1).arg(obj.ID, 0, 16).toUpper(); QString dataStr; for (int j 0; j obj.DataLen; j) { dataStr QString(%1 ).arg(obj.Data[j], 2, 16, QLatin1Char(0)).toUpper(); } int row ui-tableWidget-rowCount(); ui-tableWidget-insertRow(row); ui-tableWidget-setItem(row, 0, new QTableWidgetItem(idStr)); ui-tableWidget-setItem(row, 1, new QTableWidgetItem(dataStr)); } ui-tableWidget-setUpdatesEnabled(true); ui-tableWidget-scrollToBottom(); }这里有个小技巧在批量插入数据之前调用setUpdatesEnabled(false)插入完成后再恢复能大幅减少表格更新的渲染开销。我实测在插入大量行时这个设置能把界面刷新时间减少一半以上。另外一个容易被忽视的细节是信号槽连接类型。由于接收线程发出的信号会跨线程传递连接方式需要设置为Qt::QueuedConnection确保槽函数在界面线程中执行。我一般在connect时显式指定这个连接类型防止默认的AutoConnection在某些特殊场景下判断错误。6. 常见问题与排查技巧实录6.1 高频问题排查表下面这些问题是实际开发中反复遇见的我整理成了一个排查速查表异常现象可能原因解决方案VCI_OpenDevice返回-1设备没有正确插入或驱动未安装成功检查设备管理器中的USB设备是否正常识别重新拔插设备VCI_InitCAN返回-1Timing0/Timing1参数错误或CAN通道已经被占用对照波特率参数表修改配置确认程序没有重复打开能发送但收不到数据滤波掩码配置错误硬件层面就把数据过滤掉了把AccMask设为全FF关闭滤波先确认能收全量数据数据时有时无偶尔丢帧接收线程内轮询间隔过长或批量刷新策略过于激进调整VCI_Receive超时时间优化批量信号发送阈值程序关闭时报内存错误关闭设备前没有停止接收线程在主窗口关闭事件中先请求线程终止再执行VCI_CloseDevice界面无响应在界面线程中直接进行大量CAN数据的处理和显示把接收和数据处理全部移到线程中主线程只负责显示6.2 我踩过的几个坑第一个坑是设备初始化明明成功了但程序一退出再重新打开就会报设备被占用。后来排查发现是因为退出时没有按顺序关闭要先停止接收线程再调用VCI_CloseDevice不能反着来。如果先关闭设备接收线程还在跑就会产生不确定的访问异常。第二个坑是刷新效率和CPU占用之间的平衡。最初我设计成每收到一帧就刷新一次界面在高速场景下CPU占用率直接飙到30%多。后来加入批量刷新机制后CPU占用率降到了5%以下而且界面滚动更平滑。如果你做的是高报文率的项目这一步优化是必须的。第三个坑隐藏得比较深当总线上同时存在多路报文时如果接收线程中的结构体数组对象长度设置得太小比如一次只接收1帧DLL内部缓冲区会不断累积数据当数据量超过硬件FIFO容量时就会发生覆盖丢帧。所以我的代码中数组设置为100实测在高速收数据时没有丢帧。6.3 接收线程的停止方法当你需要关闭程序或者重新配置设备时线程的停止操作必须做到优雅。不能直接调用terminate强行结束线程那样很容易造成资源泄漏。正确做法是调用requestInterruption请求中断然后在线程的循环条件中判断这个标志void CANManager::closeDevice() { if (m_recvThread) { m_recvThread-requestInterruption(); m_recvThread-quit(); m_recvThread-wait(2000); delete m_recvThread; m_recvThread nullptr; } VCI_CloseDevice(m_devType, m_devIndex); }wait函数设置超时时间的好处是不会在极端情况下永久卡住主线程。如果2秒内线程还没结束说明循环逻辑里有阻塞调用需要检查是不是某处没有正确响应中断请求。7. 测试结果与运行优化7.1 长时间稳定性测试整个程序准备完成后我做了一个持续6小时的稳定性测试。测试环境为周立功USBCAN-II配置为500Kbps另一块CAN卡作为发送节点以10ms间隔循环发送ID为0x123、数据长度为8字节的报文。最终结果程序运行期间共接收约216万帧数据界面无卡顿无内存持续增长现象接收到的数据帧数量与发送端统计基本一致。这验证了线程加批量刷新的架构在长时间运行场景下是可靠的。7.2 波形数据查看与显示优化如果你需要在接收数据的同时绘制波形可以在批量刷新信号中增加一个分支把数据同时送入绘图控件。基于QT的QPainter或者QCustomPlot都可以直接对接这批数据。需要注意绘图操作本身也比较耗时建议通过定时器控制绘图频率比如每秒刷新30次波形图就足够了不需要跟数据接收保持完全同步。7.3 后续扩展思路这套架构还可以继续扩展比如增加DBC协议解析模块把解析出来的物理量直接显示在界面上或者增加日志记录功能把接收到的报文按时间戳写入数据库再比如增加曲线绘制控件实现对某个信号量的实时趋势监控。因为这些功能的接入点都已经通过信号槽机制预留好了后续扩展的成本很低。8. 最后的实操心得做了这个项目之后我对QT做上位机通信类工具的信心强了很多。整个过程下来最核心的体会就是分层一定要清晰界面层就是界面层只管显示和交互通信层就是通信层只负责收发数据线程调度也要明确边界接收线程不要掺和界面操作。把这三者理清楚后续不管接什么硬件代码都不用伤筋动骨。再分享一个小细节在开发和调试阶段别急着做花哨的界面。先把设备初始化和收发这两个基础流程跑通用命令行窗口打印出收到的每一帧数据确认硬件链路和数据正确性再开始做界面美化。我见过不少同学一上来就折腾布局和样式结果底层通信还没通排查问题的时候界面反而成了干扰项。还有一个习惯值得养成接收线程里读到的原始数据尽量不要在槽函数之外的地方直接访问。因为跨线程的变量访问如果没有加锁很容易出现数据竞争。我的做法是在信号槽传递的时候用值传递QT的信号槽机制会帮我做好数据拷贝虽然效率略低换来的却是稳定和安全。最后忍不住提醒一句把周立功官方文档里的示例代码吃透再把这里提到的线程和信号槽机制理解清楚了整个项目基本就稳了。如果调试中遇到什么问题欢迎一起交流。本文还有配套的精品资源点击获取
返回列表