尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

QT调用周立功USBCAN实现CAN报文自动接收的完整指南

QT调用周立功USBCAN实现CAN报文自动接收的完整指南 简介本资源是一套基于Qt框架实现周立功CAN总线通信的完整可运行工程面向嵌入式开发、汽车电子及工业自动化领域的中初级Qt开发者解决CAN设备接入、实时数据自动接收与基础解析等典型工程问题。压缩包共67个文件含34个XML设备配置与协议描述文件、15个动态链接库如USBCAN.dll、zlgcan.dll等、7个核心头文件zlgcan.h、canframe.h等、4个CPP源码mainwindow.cpp、candatabase.cpp等及UI、PRO工程文件和PDF手册整体3.46MB结构清晰开箱即用。已有2348人学习下载资源已预集成周立功CAN驱动库与多型号设备支持USBCAN、CANET、CANFD系列提供带过滤机制的循环接收逻辑、线程安全的数据处理骨架及Qt信号槽绑定示例助开发者快速验证硬件通信、调试ID/DLC解析逻辑并为后续UI可视化或协议解析扩展奠定坚实基础。 做上位机开发这些年CAN通信是绕不开的一块。最近有个项目要在QT环境里快速验证车载ECU的报文手里正好有一块周立功USBCAN-II于是把QT调用周立功CAN接口库、自动接收数据这套流程完整跑了一遍。整个项目从搭环境到调通前后花了一天时间今天把能直接用的代码、参数计算方法和踩过的坑整理出来给准备做QT 周立功CAN通信的朋友一个参考。无论你是刚接触CAN的小白还是已经在写上位机的老手这套流程都能帮你少走很多弯路。1. 需求拆解与整体方案设计1.1 这个项目到底在解决什么项目表面上是“在QT里实现CAN通信并自动接收数据”但实际拆开看底层需求其实是三件事。第一件是要在一台Windows电脑上通过USB连接周立功USBCAN分析仪把CAN总线上的报文实时读进上位机。第二件是不能让上位机界面卡死接收报文这种高频操作必须放后台线程。第三件是要能灵活处理不同波特率、不同ID过滤规则不能每次换设备都重新写一遍底层代码。很多新手把“能用串口助手看数据”当成目标其实做上位机软件的话还要考虑数据怎么流转、界面怎么刷新、关闭程序的时候怎么安全释放设备。这些才是项目真正要花时间的地方。1.2 为什么选QT 周立功USBCAN这套组合选型这件事我是权衡过的。市面上能接CAN总线的方案不少比如自己做个STM32转串口再发数据或者买其他厂家的USB-CAN适配器但最终还是选了周立功。原因很直接。周立功USBCAN驱动稳定官方SDK文档齐全网上资料多遇到问题能很快找到答案这对开发效率非常重要。QT这边就更不用说了跨平台、信号槽机制适合做界面和后台线程通信QTimer、QThread这些模块天生就是为这类实时采集场景准备的。说句实话如果用串口转CAN自己还得处理帧拆包、校验容易出bug用QT直接调周立功的ControlCAN库API都封装好了只要能看懂VCI开头的函数基本半天就能跑通。1.3 整体框架与数据流整个工程的思路其实非常清晰GUI主线程负责显示和用户交互一个继承QObject的工作线程负责持续从CAN设备缓冲区读数据读到的数据封装成自定义结构体通过信号槽发到界面。数据流大概是这样的CAN总线上的报文进入USBCAN-II设备周立功驱动把报文缓存到电脑端QT后台线程以10毫秒左右的间隔调用VCI_Receive去取数据取到后通过emit发送到主线程主线程再把数据显示到QTableWidget或QTextEdit上。这个架构的好处是解耦界面和采集互不干扰。即使把接收间隔缩短到5毫秒UI也不会掉帧。后续如果要增加日志保存、曲线绘制只需要在信号槽里加处理逻辑就行。2. 开发环境准备与CAN通信基础2.1 软硬件清单先说硬件。我手里这块是周立功USBCAN-II支持2路CAN通道标准的CAN2.0A/B波特率最高能到1Mbps做常规的CAN数据采集完全够用。如果你只需要单通道USBCAN-I也可以但代码上要注意设备号改成1。还需要准备一个能产生CAN报文的工具比如另一个CAN分析仪、PCAN或者周立功自家CANTest虚拟仪器。如果没有外部设备可以直接把USBCAN-II的CAN1和CAN2用双绞线连起来做回环CAN1发CAN2收这样测试最方便。注意两端或一端要并联120欧姆终端电阻否则高速传输会反射导致接收错误。软件方面我用的开发环境是Windows 10 QT 5.15.2 MSVC2017 64位编译器周立功驱动版本是V2.0以上。用QT 6系列也行核心代码不变只要注意头文件和库的路径别配错。有个重要的坑要先提醒QT的编译套件一定要和周立功SDK的DLL匹配。官方库是标准C接口的DLLMSVC编译的QT调用没问题如果你用MinGW编译器虽然也能通过但某些环境可能会出现加载异常建议直接用MSVC套件省心。软硬件清单汇总类型名称说明硬件周立功USBCAN-II2路CANUSB2.0硬件CAN分析仪/CANTest用于对测或模拟发送硬件120Ω终端电阻接在CAN_H/CAN_L之间软件Windows 10/11直接以从业者口吻开始软件QT 5.15.2用MSVC2017_64套件软件周立功USBCAN驱动安装后设备管理器能识别开发包ControlCAN SDK包含ControlCAN.h、DLL和库文件2.2 CAN 2.0 帧结构速览在写代码之前一定要把CAN帧结构搞明白。CAN2.0标准帧大概长这样起始位SOF、仲裁段ID RTR、控制段IDE、DLC、数据段最多8字节、CRC、ACK和EOF。作为上位机开发者最关心的就三个字段ID、DLC、Data。ID就是报文的身份证用于仲裁和过滤DLC表示后面跟着几个数据字节范围0到8Data就是实际的有效载荷。标准帧ID是11位范围0x000到0x7FF扩展帧ID是29位范围更大。应用层在设计协议时通常会把ID分成几个位段比如高三位代表报文类型低八位代表目标地址做过滤的时候就能按位处理。还要注意CAN是异步差分信号用CAN_H和CAN_L两根线传输。软件层面看不到波形所以收到“乱码”大多是波特率不对或物理链路有问题。2.3 滤波掩码计算真的很简单周立功的CAN控制器支持硬件滤波核心寄存器是验收码AccCode和验收掩码AccMask。掩码这个知识点网上很多人讲复杂了其实就一句话掩码位为1时收到报文的ID对应位必须和AccCode的对应位相同掩码位为0时对应位不关心。举个例子你想只接收ID为0x123的标准帧那AccCode就填0x123AccMask填0x7FF。如果你想同时接收0x123和0x129可以先算两个ID有哪些共同的“必须匹配”的位。把0x123和0x129写成二进制0x123 001 0010 00110x129 001 0010 1001逐位比较结果相同的位就是需要关心的位不相同的位就可以掩掉。这里差异只在第0位和第3位其他位都一致所以AccMask 1111 1111 0110 0x7F6AccCode可以填0x123也可以填0x129。用这个组合硬件就只会放行0x123和0x129这两帧大幅减少无效中断。注意这是我调通过的经验上位机软件调试初期建议AccMask填0xFFFFFFFF也就是接收所有报文等协议稳定了再在业务层做软件过滤。因为硬件滤波配错了会完全收不到任何数据增加排查难度。3. QT调用周立功CAN接口库的详细实现3.1 周立功SDK里到底有哪些核心接口周立功的ControlCAN库是老牌API了核心函数几个就够用。最重要的有这几个函数作用VCI_OpenDevice打开设备VCI_InitCAN初始化CAN通道VCI_StartCAN启动CAN通道VCI_Transmit发送一帧或多帧数据VCI_GetReceiveNum获取接收缓冲区中待读取的帧数VCI_Receive读取接收缓冲区中的数据VCI_CloseDevice关闭设备VCI_ReadBoardInfo读取设备信息这些函数的头文件是ControlCAN.h调用约定是标准C。QT里直接用extern C引入或者直接包含头文件都行不需要额外封装成C类。不过为了工程整洁我建议你还是自己包一层CANDevice类把打开、初始化、收发、关闭都封装起来界面代码只跟自己的类打交道。这里要特别注意很多函数返回0表示设备不存在或操作失败返回1表示成功这类返回值判断的时候别写反。VCI_Receive的返回值语义也容易搞混它返回的是实际读取到的帧数不是错误码。3.2 初始化设备打开通道和设置波特率初始化流程通常分四步打开设备、初始化通道、启动通道、配置滤波。别觉得简单顺序错一个都会出问题。首先调用VCI_OpenDevice参数有设备类型、设备索引、保留参数。设备类型要填对USBCAN-II对应VCI_USBCAN2也就是4。设备索引默认0如果你只插了一个设备就写0。打开成功后再初始化通道。初始化时要填VCI_INIT_CONFIG结构体重点是AccCode、AccMask、Filter和Timing0、Timing1。这里直接给出一份我验证过的初始化代码#include ControlCAN.h #include QDebug #define DEV_TYPE 4 // VCI_USBCAN2 #define DEV_INDEX 0 #define CAN_INDEX 0 // 通道0 bool initCanDevice() { // 1. 打开设备 if (VCI_OpenDevice(DEV_TYPE, DEV_INDEX, 0) ! 1) { qWarning() VCI_OpenDevice failed; return false; } VCI_INIT_CONFIG config; memset(config, 0, sizeof(config)); // 接收所有帧后期在业务层过滤 config.AccCode 0x00000000; config.AccMask 0xFFFFFFFF; config.Filter 0; // 接收模式0接收所有类型帧 config.Timing0 0x00; // 500Kbps config.Timing1 0x1C; config.Mode 0; // 正常模式 // 2. 初始化通道 if (VCI_InitCAN(DEV_TYPE, DEV_INDEX, CAN_INDEX, config) ! 1) { qWarning() VCI_InitCAN failed; VCI_CloseDevice(DEV_TYPE, DEV_INDEX); return false; } // 3. 启动通道 if (VCI_StartCAN(DEV_TYPE, DEV_INDEX, CAN_INDEX) ! 1) { qWarning() VCI_StartCAN failed; VCI_CloseDevice(DEV_TYPE, DEV_INDEX); return false; } return true; }关于波特率我直接把我验证过的一组常用值列出来方便大家直接抄波特率Timing0Timing11000K0x000x14800K0x000x16500K0x000x1C250K0x010x1C125K0x030x1C如果用的是250K就把初始化里的Timing0改成0x01、Timing1改成0x1C。这个值是周立功官方给的比自己算采样点可靠。前提是硬件上所有CAN设备的波特率必须一致否则数据全是错的。3.3 收发函数的封装与自动接收线程初始化完成后就可以开始写收发逻辑了。这里重点说一下发送和接收的封装以及自动接收线程的设计。发送相对简单构造一个VCI_CAN_OBJ结构体填好ID、SendType、DataLen、Data然后调VCI_Transmit。比如发一个ID为0x123的标准帧数据是0x01 0x02 0x03bool sendCanFrame(uint32_t id, const QByteArray data) { VCI_CAN_OBJ sendFrame; memset(sendFrame, 0, sizeof(sendFrame)); sendFrame.ID id; sendFrame.SendType 0; // 0正常发送1单次发送 sendFrame.RemoteFlag 0; // 数据帧 sendFrame.ExternFlag 0; // 标准帧 sendFrame.DataLen data.size(); memcpy(sendFrame.Data, data.data(), data.size()); int ret VCI_Transmit(DEV_TYPE, DEV_INDEX, CAN_INDEX, sendFrame, 1); if (ret ! 1) { qWarning() VCI_Transmit failed, ret ret; return false; } return true; }自动接收才是重头戏。我一开始图省事直接在界面里放一个QTimer每隔50毫秒去读结果一卡一卡的后来改成QThread线程轮询就稳了。接收线程的思路是一个继承QObject的worker类内部用一个volatile bool或者QAtomicInteger控制循环。在run里调用VCI_GetReceiveNum查询缓冲区帧数如果有帧再调VCI_Receive批量取出来每取一帧就emit一个signal让主线程去更新界面。代码大概是这样class CanWorker : public QObject { Q_OBJECT public: explicit CanWorker(QObject *parent nullptr) : QObject(parent), m_stop(false) {} public slots: void start() { m_stop false; while (!m_stop) { // 获取缓冲区待读帧数 DWORD count VCI_GetReceiveNum(DEV_TYPE, DEV_INDEX, CAN_INDEX); if (count 0) { const int maxBuf 200; // 一次最多读200帧 VCI_CAN_OBJ frames[maxBuf]; int readNum VCI_Receive(DEV_TYPE, DEV_INDEX, CAN_INDEX, frames, maxBuf, 10); for (int i 0; i readNum; i) { CanFrameInfo info; info.id frames[i].ID; info.isExt frames[i].ExternFlag; info.dlc frames[i].DataLen; memcpy(info.data, frames[i].Data, 8); emit frameReceived(info); } } QThread::msleep(10); } } void stop() { m_stop true; } signals: void frameReceived(const CanFrameInfo frame); private: volatile bool m_stop; };这里有几个细节必须注意。VCI_Receive最后一个参数是等待超时时间单位是毫秒。如果你在循环里已经用VCI_GetReceiveNum判断过数量了这个参数其实无所谓但别填0否则缓冲区为空时会立刻返回导致空转。还有VCI_CAN_OBJ.Data是UCHAR[8]最多8字节这是标准CAN2.0的硬限制。如果碰到CAN FD报文得用支持FD的设备才能读到更多数据。3.4 把数据显示到QT界面后台线程发信号出来之后主界面直接连接信号槽就行。我一般用QTableWidget来显示因为它简单直接还能按列排序。连接方式如下CanWorker *worker new CanWorker; QThread *thread new QThread; worker-moveToThread(thread); connect(thread, QThread::started, worker, CanWorker::start); connect(worker, CanWorker::frameReceived, this, [](const CanFrameInfo frame) { // 在主线程刷新表格 int row ui-tableWidget-rowCount(); ui-tableWidget-insertRow(row); ui-tableWidget-setItem(row, 0, new QTableWidgetItem(QString::number(m_frameCount))); ui-tableWidget-setItem(row, 1, new QTableWidgetItem( QString(%1).arg(frame.id, 3, 16, QLatin1Char(0)).toUpper())); ui-tableWidget-setItem(row, 2, new QTableWidgetItem( frame.isExt ? 扩展帧 : 标准帧)); ui-tableWidget-setItem(row, 3, new QTableWidgetItem(QString::number(frame.dlc))); QString dataStr; for (int i 0; i frame.dlc; i) { dataStr QString(%1 ).arg(frame.data[i], 2, 16, QLatin1Char(0)).toUpper(); } ui-tableWidget-setItem(row, 4, new QTableWidgetItem(dataStr.trimmed())); ui-tableWidget-scrollToBottom(); }); thread-start();注意QTableWidget的行数不能无限涨否则运行久了直接卡死。我一般会在行数超过5000时自动清空或者用QTableView 自定义Model管理。在测试阶段最简单的方案是每加1000行就removeRow(0)保证表格行数稳定。线程停止的地方也要细心。程序关闭时先调worker的stop让循环退出再thread-quit()和thread-wait()最后释放内存。如果没退出循环就强行关闭程序大概率会崩溃。4. 实测过程中的调试记录与排查技巧4.1 先用CANTest验证物理链路好多朋友直接上手写QT代码结果发现收不到数据第一反应就是代码写错了其实大概率是物理链路或者配置问题。我的习惯是先把设备接好打开周立功自带的CANTest工具当场测一遍。具体做法是设备插上电脑打开CANTest选择USBCAN-II和对应通道波特率设成500K启动接收。如果没有外部发送设备就把CAN1和CAN2用两根线连起来CAN1作为一个模拟发送端周期发送0x123帧看CAN2能不能收到。CANTest能收到说明设备、线缆、波特率都没问题这时再回到QT程序里排查代码就有的放矢了。这一步非常关键。我在实际项目里至少遇到过三次“QT收不到数据”的情况最后发现都是CANTest里忘了点启动或者波特率没选对根本不是QT代码的问题。4.2 常见问题排查速查表调试过程中我整理了一张排查表基本覆盖了新人会遇到的坑现象可能原因排查办法VCI_OpenDevice返回0驱动没装好/设备被占用检查设备管理器关闭CANTestVCI_InitCAN返回0波特率参数不对/通道号错误查Timing0/Timing1表确认通道索引一直收不到数据滤波掩码把帧滤掉了暂时把AccMask设成0xFFFFFFFF收到数据但内容乱跳波特率不匹配/线路接触不良两端统一波特率检查接线程序界面卡死在主线程里轮询接收改用QThread后台轮询发送返回1但总线上没包发送ID冲突或线路故障用CANTest抓包确认关闭程序时崩溃线程没停止就释放了资源先stop再quit/wait这里重点说一下“发送返回1但总线上没包”。这个坑比较隐蔽USBCAN-II在正常模式下发送如果总线上同时有多个节点在发且ID仲裁被别的报文压住了发送函数也可能返回成功因为数据已经交给了控制器但没真正发出去。用CANTest抓一下就能看到。4.3 几点实际操作经验最后分享几个实操层面的体会。第一自动接收的轮询间隔不是越小越好。我之前试过1毫秒结果USB负载高CPU占用也偏高后来改成5到10毫秒效果已经很理想了。500K波特率下总线最密大概每隔2毫秒一帧10毫秒轮询完全来得及因为周立功设备内部有缓冲区不会丢帧。第二滤波只在报文量很大的场景下才有意义。如果一秒钟只有几十帧就算全部收进来UI也扛得住完全可以在信号槽里做软件过滤这样更灵活。硬件滤波留给那些一秒钟几千帧的高速场景。第三记得处理数据总线的错误状态。USBCAN-II有错误计数和状态寄存器如果你的上位机需要长时间稳定运行最好也读一下错误帧标志一发现总线错误就提示用户检查物理连接而不是傻等下去。第四关于代码工程组织我建议把CAN设备相关代码单独放到一个can_device.h/cpp目录里界面代码完全不碰VCI函数。这样以后换成别的CAN卡只要改一个类就够了界面一行都不用动。这个设计习惯帮我节省过不少时间。总的来说QT 周立功USBCAN这套组合做CAN通信上位机确实非常顺手。我在这套代码基础上已经接过了车身控制器、BMS模拟器好几个项目每次都是改改业务逻辑就能用。如果你也正在做类似的事情按着这个框架走一遍应该能少踩很多坑。本文还有配套的精品资源点击获取
返回列表