前言
在工业控制、汽车电子、新能源设备等领域,CAN(Controller Area Network)总线凭借其高可靠性、抗干扰能力强、多主节点架构等优势,成为设备间通信的主流方案。周立功 USBCAN 模块是工业级 CAN 转 USB 接口设备,广泛用于上位机与下位机的 CAN 通信调试、数据采集与控制。
本文将从零开始,系统讲解 CAN 协议核心原理、周立功 USBCAN 驱动接口详解、Qt 环境下驱动调用方法、自定义 CAN 协议封装、多线程数据收发实现,结合完整可运行的工程代码,让你快速掌握 Qt + 周立功 USBCAN 的上位机开发技术,适用于新能源、工控、车载等实际项目开发。
本文配套代码基于Qt 5.14.2、Windows11 系统、周立功 USBCAN2 型号开发,所有代码均可直接移植使用。
一、CAN 总线协议深度解析
1.1 CAN 总线概述
CAN 总线是德国博世公司在 1986 年推出的串行通信协议,核心设计目标是高可靠性、实时性、多主结构,无需主机调度,任意节点均可主动发送数据。
- 核心特性:
- 多主控制:所有节点地位平等,均可主动发送数据,无需主机控制;
- 非破坏性总线仲裁:通过 ID 优先级解决总线冲突,优先级高的帧优先发送;
- 数据帧短帧结构:最大数据长度 8 字节,传输速度快、实时性高;
- 错误检测与重发:硬件自动检测错误、重发损坏帧,可靠性极高;
- 节点自动离线:严重错误时节点自动断开总线,不影响其他设备通信。
- 应用场景:
- 汽车电子:发动机控制、车身控制、电池管理(BMS);
- 工业控制:PLC、传感器、执行器通信;
- 新能源:充电桩、储能设备、逆变器通信;
- 医疗设备、机器人控制。
1.2 CAN 总线物理层
物理层定义了电气特性、传输介质、接口标准,决定了硬件连接方式:
- 传输介质:双绞线(抗干扰能力最强,工业首选);
- 终端电阻:总线两端必须接120Ω 终端电阻,匹配阻抗、防止信号反射;
- 电平标准:
隐性电平:CAN_H=2.5V,CAN_L=2.5V,差分电压 0V,表示逻辑1;
显性电平:CAN_H=3.5V,CAN_L=1.5V,差分电压 2V,表示逻辑0; - 通信速率:速率与总线长度成反比,常用速率:5Kbps、10Kbps、20Kbps、50Kbps、125Kbps、250Kbps、500Kbps、1Mbps。
1.3 CAN 协议帧类型
CAN 总线定义了4 种帧类型,完成数据传输、总线控制、错误处理:
| 帧类型 | 核心作用 |
|---|---|
| 数据帧 | 节点向总线发送有效数据,最常用 |
| 远程帧 | 节点请求其他节点发送数据(无数据域) |
| 错误帧 | 节点检测到总线错误时发送,通知其他节点 |
| 过载帧 | 节点接收能力不足时发送,延迟下一次传输 |
1.4 CAN 数据帧详解(核心)
数据帧是实际开发中最常用的帧,分为标准帧(11 位 ID) 和扩展帧(29 位 ID),结构如下:
- 标准数据帧(11 位 ID)
结构:帧起始 → 仲裁段(11 位 ID+RTR) → 控制段 → 数据段(0-8 字节) → CRC 段 → ACK 段 → 帧结束 - 扩展数据帧(29 位 ID)
结构:帧起始 → 仲裁段(11 位基本 ID+IDE+18 位扩展 ID+RTR) → 控制段 → 数据段 → CRC 段 → ACK 段 → 帧结束
关键字段解析:
- ID(标识符):
标准帧:11 位,支持 2048 个节点;
扩展帧:29 位,支持 5 亿 + 节点,工业 / 新能源项目首选扩展帧; - RTR(远程发送请求位):
显性电平(0):数据帧(包含数据);
隐性电平(1):远程帧(无数据); - IDE 位:扩展帧标识位,隐性电平表示扩展帧;
- DLC(数据长度码):4 位,表示数据段长度(0-8);
- 数据段:0-8 字节,实际传输的业务数据;
CRC 段:循环冗余校验,保证数据完整性;
ACK 段:接收节点应答,确认数据接收成功。
1.5 CAN 总线仲裁机制
CAN 总线采用非破坏性位仲裁,解决多节点同时发送数据的冲突:
- 所有节点同步发送帧 ID;
- 显性电平(0)覆盖隐性电平(1);
- ID 数值越小,优先级越高;
- 仲裁失败的节点自动转为接收模式,等待总线空闲后重发。
优势:无冲突、无延迟、实时性极高。
1.6 CAN 波特率计算(周立功模块专用)
周立功 USBCAN 模块通过Timing0 和 Timing1两个寄存器配置波特率,公式:
波特率 = 系统时钟 / (分频系数 × (同步段 + 时间段1 + 时间段2))
周立功 USBCAN 默认系统时钟为8MHz,常用波特率配置表:
| 波特率 | Timing0 | Timing1 |
|---|---|---|
| 5Kbps | 0xBF | 0xFF |
| 10Kbps | 0x31 | 0x1C |
| 20Kbps | 0x18 | 0x1C |
| 50Kbps | 0x09 | 0x1C |
| 125Kbps | 0x03 | 0x1C |
| 250Kbps | 0x01 | 0x1C |
| 500Kbps | 0x00 | 0x1C |
| 1Mbps | 0x00 | 0x14 |
本文配套代码默认使用125Kbps(Timing0=0x03,Timing1=0x1C)。

二、周立功 USBCAN 驱动与接口全解析
2.1 周立功 USBCAN 模块简介
周立功 USBCAN 是 USB 转 CAN 接口卡,支持 USB2.0、多通道 CAN、即插即用,适配 Windows/Linux 系统,提供标准动态库(ControlCAN.dll/libControlCAN.so)供上位机调用。
常用型号:
VCI_USBCAN1:单通道 CAN;
VCI_USBCAN2:双通道 CAN(本文使用型号);
VCI_USBCAN_2E_U:工业级双通道 CAN。
2.2 驱动安装与库文件准备
- 驱动安装
周立功官网下载对应型号驱动;
安装驱动后,插入 USBCAN 模块,设备管理器显示Zhouligong USBCAN即安装成功。 - 开发文件准备
Qt 开发需要 3 个核心文件:
ControlCAN.h:驱动接口头文件(定义结构体、函数声明、宏定义);
ControlCAN.dll(Windows)/libControlCAN.so(Linux):动态库文件;
ControlCAN.lib:链接库文件;
库文件放入 Qt 工程编译输出目录(debug/release 文件夹)。
2.3 ControlCAN.h 核心内容解析
周立功提供的头文件一般要使用Qt跨平台,最好稍微改造下,比如我下面的头文件是适配 Qt 的跨平台头文件。
cpp
#ifndef CONTROLCAN_QT_H
#define CONTROLCAN_QT_H
// 类型定义:兼容Windows SDK类型,避免包含windows.h导致宏冲突
// 如果Windows类型已定义(通过windows.h),则不重复定义
#ifndef _WINDEF_
// Windows类型未定义,提供兼容定义
#if defined(_WIN32) || defined(_WIN64)
// Windows平台:使用与Windows SDK相同的底层类型
typedef unsigned long DWORD;
typedef unsigned long ULONG;
typedef unsigned int UINT;
typedef unsigned short USHORT;
typedef unsigned char UCHAR;
typedef unsigned char BYTE;
typedef int INT;
typedef void* PVOID;
#else
// 非Windows平台:使用Qt类型(与Windows类型大小相同)
#include <QtGlobal>
typedef quint32 DWORD;
typedef quint32 ULONG;
typedef quint32 UINT;
typedef quint16 USHORT;
typedef quint8 UCHAR;
typedef quint8 BYTE;
typedef qint32 INT;
typedef void* PVOID;
#endif
#endif
// 调用约定宏(Windows下使用stdcall,其他平台使用默认)
#if defined(_WIN32) || defined(_WIN64)
#define STDCALL __stdcall
#else
#define STDCALL
#endif
#define CONTROLCAN_API
// 接口卡类型定义
#define VCI_USBCAN1 3
#define VCI_USBCAN2 4
#define VCI_USBCAN_2E_U 21
//CAN错误码
#define ERR_CAN_OVERFLOW 0x0001 //CAN控制器内部FIFO溢出
#define ERR_CAN_ERRALARM 0x0002 //CAN控制器错误报警
#define ERR_CAN_PASSIVE 0x0004 //CAN控制器消极错误
#define ERR_CAN_LOSE 0x0008 //CAN控制器仲裁丢失
#define ERR_CAN_BUSERR 0x0010 //CAN控制器总线错误
#define ERR_CAN_BUSOFF 0x0020 //总线关闭错误
#define ERR_CAN_BUFFER_OVERFLOW 0x0040 //CAN控制器内部BUFFER溢出
//通用错误码
#define ERR_DEVICEOPENED 0x0100 //设备已经打开
#define ERR_DEVICEOPEN 0x0200 //打开设备错误
#define ERR_DEVICENOTOPEN 0x0400 //设备没有打开
#define ERR_BUFFEROVERFLOW 0x0800 //缓冲区溢出
#define ERR_DEVICENOTEXIST 0x1000 //此设备不存在
#define ERR_LOADKERNELDLL 0x2000 //装载动态库失败
#define ERR_CMDFAILED 0x4000 //执行命令失败错误码
#define ERR_BUFFERCREATE 0x8000 //内存不足
//CANET错误码
#define ERR_CANETE_PORTOPENED 0x00010000 //端口已经被打开
#define ERR_CANETE_INDEXUSED 0x00020000 //设备索引号已经被占用
#define ERR_REF_TYPE_ID 0x00030000 //SetReference或GetReference传递的RefType不存在
#define ERR_CREATE_SOCKET 0x00030002 //创建Socket失败
#define ERR_OPEN_CONNECT 0x00030003 //打开Socket的连接时失败,可能设备连接已经存在
#define ERR_NO_STARTUP 0x00030004 //设备没启动
#define ERR_NO_CONNECTED 0x00030005 //设备无连接
#define ERR_SEND_PARTIAL 0x00030006 //只发送了部分的CAN帧
#define ERR_SEND_TOO_FAST 0x00030007 //数据发得太快,Socket缓冲区满了
//函数调用返回状态值
#define STATUS_OK 1
#define STATUS_ERR 0
#define CMD_DESIP 0
#define CMD_DESPORT 1
#define CMD_CHGDESIPANDPORT 2
#define CMD_SRCPORT 2
#define CMD_TCP_TYPE 4 //tcp 工作方式,服务器:1 或是客户端:0
#define TCP_CLIENT 0
#define TCP_SERVER 1
//服务器方式下有效
#define CMD_CLIENT_COUNT 5 //连接上的客户端计数
#define CMD_CLIENT 6 //连接上的客户端
#define CMD_DISCONN_CLINET 7 //断开一个连接
#define CMD_SET_RECONNECT_TIME 8 //使能自动重连
//CANDTU_NET支持GPS
#define CMD_GET_GPS 9
#define CMD_GET_GPS_NUM 10 //获取GPS信息的数目
// 板卡信息结构体
typedef struct _VCI_BOARD_INFO {
USHORT hw_Version; //硬件版本号
USHORT fw_Version; //固件版本号
USHORT dr_Version; //驱动程序版本号
USHORT in_Version; //接口库版本号
USHORT irq_Num; //板卡所使用的中断号
BYTE can_Num; //表示有几路 CAN 通道
char str_Serial_Num[20]; //此板卡的序列号
char str_hw_Type[40]; //硬件类型
USHORT Reserved[4]; //保留
} VCI_BOARD_INFO, *PVCI_BOARD_INFO;
//定义CAN信息帧结构体
typedef struct _VCI_CAN_OBJ {
UINT ID; //帧 ID
UINT TimeStamp; //时间标识
BYTE TimeFlag; //是否使用时间标识
BYTE SendType; //发送帧类型,0正常发送,1单次发送,2自发自收,3单次自发自收
BYTE RemoteFlag; // 是否是远程帧
BYTE ExternFlag; // 是否是扩展帧
BYTE DataLen; //数据长度
BYTE Data[8]; //数据
BYTE Reserved[3]; //预留
} VCI_CAN_OBJ, *PVCI_CAN_OBJ;
//CAN控制器状态状态信息结构体
typedef struct _VCI_CAN_STATUS {
UCHAR ErrInterrupt; //中断记录
UCHAR regMode; //模式寄存器值
UCHAR regStatus; //状态寄存器值
UCHAR regALCapture; //仲裁丢失寄存器值
UCHAR regECCapture; //错误寄存器值
UCHAR regEWLimit; //错误警告限制寄存器值
UCHAR regRECounter; //接收错误寄存器值
UCHAR regTECounter; //发送错误寄存器值
DWORD Reserved; //预留
} VCI_CAN_STATUS, *PVCI_CAN_STATUS;
// 错误信息的结构体
typedef struct _VCI_ERR_INFO {
UINT ErrCode; //错误码
BYTE Passive_ErrData[3]; //消极错误的错误标识数据
BYTE ArLost_ErrData; //仲裁丢失错误的错误标识数据
} VCI_ERR_INFO, *PVCI_ERR_INFO;
// 初始化CAN的结构体
typedef struct _VCI_INIT_CONFIG {
DWORD AccCode; //验收码
DWORD AccMask; //屏蔽码
DWORD Reserved; //保留
UCHAR Filter; //滤波方式。1单滤波,0双滤波
UCHAR Timing0; //波特率定时器0
UCHAR Timing1; //波特率定时器1
UCHAR Mode; //模式;0正常模式,1只听模式
} VCI_INIT_CONFIG, *PVCI_INIT_CONFIG;
typedef struct _tagChgDesIPAndPort {
char szpwd[10]; //更改目标IP和端口所需要的密码
char szdesip[20]; //所要更改的目标IP
int desport; //所要更改的目标端口
BYTE blistenonly; //所要更改的工作模式;0正常模式,1只听模式
} CHGDESIPANDPORT;
// 过滤器记录
typedef struct _VCI_FILTER_RECORD {
DWORD ExtFrame; //过滤的帧类型标志;1为扩展帧,0为标准帧
DWORD Start; //滤波范围的起始帧ID
DWORD End; //滤波范围的结束帧ID
} VCI_FILTER_RECORD, *PVCI_FILTER_RECORD;
typedef struct _DEVICE_CONFIG {
VCI_INIT_CONFIG InitConfig[2];
DWORD DeviceType;
} DEVICE_CONFIG, *PDEVICE_CONFIG;
//定时自动发送帧结构
typedef struct _VCI_AUTO_SEND_OBJ{
BYTE Enable; //使能本条报文 0:禁能 1:使能
BYTE Index; //报文编号 最大支持32条报文
DWORD Interval; //定时发送时间 1ms为单位
VCI_CAN_OBJ obj; //报文
}VCI_AUTO_SEND_OBJ,*PVCI_AUTO_SEND_OBJ;
//设置指示灯状态结构
typedef struct _VCI_INDICATE_LIGHT{
BYTE Indicate; //指示灯编号
BYTE AttribRedMode:2; //Red LED灭/亮/闪烁/自控
BYTE AttribGreenMode:2; //Green LED灭/亮/闪烁/自控
BYTE AttribReserved:4; //保留暂时不用
BYTE FrequenceRed:2; //Red LED闪烁频率
BYTE FrequenceGreen:2; //Green LED闪烁频率
BYTE FrequenceReserved:4; //保留暂时不用
} VCI_INDICATE_LIGHT,*PVCI_INDICATE_LIGHT;
//设置转发结构
typedef struct _VCI_CAN_OBJ_REDIRECT{
BYTE Action; //标识开启或停止转发
BYTE DestCanIndex; //CAN目标通道
} VCI_CAN_OBJ_REDIRECT,*PVCI_CAN_OBJ_REDIRECT;
#ifdef __cplusplus
extern "C" {
#endif
// 设备操作函数
DWORD STDCALL VCI_OpenDevice(DWORD DeviceType, DWORD DeviceInd, DWORD Reserved);
DWORD STDCALL VCI_CloseDevice(DWORD DeviceType, DWORD DeviceInd);
DWORD STDCALL VCI_InitCAN(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, PVCI_INIT_CONFIG pInitConfig);
// 信息读取函数
DWORD STDCALL VCI_ReadBoardInfo(DWORD DeviceType, DWORD DeviceInd, PVCI_BOARD_INFO pInfo);
DWORD STDCALL VCI_ReadErrInfo(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, PVCI_ERR_INFO pErrInfo);
DWORD STDCALL VCI_ReadCANStatus(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, PVCI_CAN_STATUS pCANStatus);
// 参数设置函数
DWORD STDCALL VCI_GetReference(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, DWORD RefType, PVOID pData);
DWORD STDCALL VCI_SetReference(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, DWORD RefType, PVOID pData);
// 缓冲区操作函数
ULONG STDCALL VCI_GetReceiveNum(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd);
DWORD STDCALL VCI_ClearBuffer(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd);
// CAN控制函数
DWORD STDCALL VCI_StartCAN(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd);
DWORD STDCALL VCI_ResetCAN(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd);
// 数据收发函数
ULONG STDCALL VCI_Transmit(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, PVCI_CAN_OBJ pSend, ULONG Len);
ULONG STDCALL VCI_Receive(DWORD DeviceType, DWORD DeviceInd, DWORD CANInd, PVCI_CAN_OBJ pReceive, ULONG Len, INT WaitTime);
#ifdef __cplusplus
}
#endif
#endif // CONTROLCAN_QT_H
核心分为宏定义、结构体、函数声明三部分:
- 跨平台类型定义
解决 Windows 与 Linux 平台类型不兼容问题,避免windows.h冲突:
cpp
// Windows平台使用原生类型,Linux使用Qt类型
typedef quint32 DWORD;
typedef quint8 BYTE;
typedef void* PVOID;
#define STDCALL __stdcall // Windows调用约定
- 设备与错误码宏定义
cpp
// 设备类型
#define VCI_USBCAN2 4 // 双通道USBCAN
// 函数返回值
#define STATUS_OK 1 // 调用成功
#define STATUS_ERR 0 // 调用失败
// CAN错误码
#define ERR_CAN_BUSOFF 0x0020 // 总线关闭
#define ERR_CAN_OVERFLOW 0x0001 // FIFO溢出
- 核心结构体(必掌握)
周立功驱动通过结构体传递 CAN 数据、配置参数,是开发核心:
(1)VCI_BOARD_INFO:设备硬件信息
cpp
typedef struct _VCI_BOARD_INFO {
USHORT hw_Version; // 硬件版本
USHORT fw_Version; // 固件版本
char str_Serial_Num[20];// 设备序列号(唯一标识)
BYTE can_Num; // CAN通道数
} VCI_BOARD_INFO;
作用:读取设备序列号、版本信息,用于设备识别与匹配。
(2)VCI_CAN_OBJ:CAN 数据帧结构体
cpp
typedef struct _VCI_CAN_OBJ {
UINT ID; // 帧ID(标准帧/扩展帧)
BYTE ExternFlag; // 扩展帧标识:1=扩展帧,0=标准帧
BYTE RemoteFlag; // 远程帧标识:1=远程帧,0=数据帧
BYTE DataLen; // 数据长度(0-8)
BYTE Data[8]; // 数据域
BYTE SendType; // 发送类型:0=正常发送
} VCI_CAN_OBJ;
作用:配置波特率、滤波规则、工作模式。
验收码 + 屏蔽码:用于 CAN 帧滤波,AccMask=0xFFFFFFFF表示接收所有帧;
Mode=0:正常模式(可收发数据);Mode=1:只听模式(仅接收,不发送应答)。
(4)VCI_CAN_STATUS:CAN 控制器状态
cpp
typedef struct _VCI_CAN_STATUS {
UCHAR regTECounter; // 发送错误计数器
UCHAR regRECounter; // 接收错误计数器
} VCI_CAN_STATUS;
作用:读取总线状态,排查总线错误。
2.4 周立功驱动核心 API 函数详解
驱动提供 12 个核心 API,完成设备打开、初始化、收发、关闭全流程,函数均为STDCALL调用约定:
- 设备操作函数

- 数据收发函数

- 信息读取函数

三、Qt 环境下调用周立功 USBCAN 驱动
3.1 Qt 工程配置
- 新建 Qt Widgets 工程
- 添加驱动文件
将ControlCAN_qt.h加入工程,将ControlCAN.dll放入编译输出目录。 - 跨平台库加载配置
在.pro文件中添加动态库引用:
cpp
# 引入USBCAN库
INCLUDEPATH += $$PWD/ControlCAN
# 链接库
LIBS += -L$$PWD/ControlCAN -lControlCAN
3.2 核心设计思路
为了保证代码模块化、线程安全、可复用,我们设计三层架构:
USBCANManager:单例模式,管理所有 USBCAN 设备、CAN 通道,封装驱动 API;
DeviceCANAdapter:CAN 适配器,封装协议解析、数据收发、同步等待;
业务层:调用适配器实现设备控制、数据采集。
3.3 USBCANManager 单例类实现(设备管理核心)
核心功能:
探测 USBCAN 设备、匹配序列号;
打开 / 关闭 CAN 通道、初始化波特率;
线程安全的数据收发;
多通道管理。
关键代码解析:
- 单例模式(线程安全懒汉式)
cpp
// 头文件
class USBCANManager : public QObject
{
Q_OBJECT
public:
static USBCANManager* getInstance();
private:
static USBCANManager* m_instance;
static QMutex m_instanceMutex;
USBCANManager(QObject* parent = nullptr);
};
// 源文件
USBCANManager* USBCANManager::m_instance = nullptr;
QMutex USBCANManager::m_instanceMutex;
USBCANManager* USBCANManager::getInstance()
{
if (m_instance == nullptr) {
QMutexLocker locker(&m_instanceMutex);
if (m_instance == nullptr) {
m_instance = new USBCANManager();
}
}
return m_instance;
}
优势:全局唯一实例,避免重复打开设备、线程安全。
- 探测 USBCAN 设备
遍历设备索引,打开设备、读取序列号,匹配配置文件中的设备 SN:
cpp
bool USBCANManager::enumUsbCanDevices()
{
QMutexLocker locker(&m_detectionMutex);
m_lstDeviceInfo.clear();
// 遍历设备索引0-1
for (int devIndex = 0; devIndex < 2; ++devIndex)
{
// 打开设备
if (VCI_OpenDevice(VCI_USBCAN2, devIndex, 0) == STATUS_OK)
{
VCI_BOARD_INFO boardInfo;
memset(&boardInfo, 0, sizeof(boardInfo));
// 读取设备序列号
if (VCI_ReadBoardInfo(VCI_USBCAN2, devIndex, &boardInfo) == STATUS_OK)
{
QString deviceSn = QString::fromLatin(boardInfo.str_Serial_Num).trimmed();
// 匹配预设序列号
if (deviceSn == 配置的SN) {
DeviceInfo info;
info.deviceIndex = devIndex;
info.strSn = deviceSn;
m_lstDeviceInfo.append(info);
}
}
}
}
m_devicesProbed = true;
return true;
}
- 初始化 CAN 通道
配置波特率、验收码、启动 CAN:
cpp
bool USBCANManager::initializeCAN(int channelId)
{
ChannelData* ch = m_channels[channelId];
CANChannelConfig cfg = ch->config;
// 初始化配置:125Kbps波特率
VCI_INIT_CONFIG initConfig;
memset(&initConfig, 0, sizeof(initConfig));
initConfig.AccCode = 0x00000000; // 验收码
initConfig.AccMask = 0xFFFFFFFF; // 屏蔽码(接收所有帧)
initConfig.Filter = 0; // 双滤波
initConfig.Mode = 0; // 正常模式
initConfig.Timing0 = 0x03; // 125Kbps
initConfig.Timing1 = 0x1C;
// 初始化CAN
VCI_InitCAN(VCI_USBCAN2, cfg.deviceIndex, cfg.canIndex, &initConfig);
// 启动CAN
VCI_StartCAN(VCI_USBCAN2, cfg.deviceIndex, cfg.canIndex);
// 清空缓冲区
VCI_ClearBuffer(VCI_USBCAN2, cfg.deviceIndex, cfg.canIndex);
return true;
}
- 发送 CAN 数据
cpp
bool USBCANManager::sendMessage(int channelId, const CANMessage& msg)
{
QMutexLocker locker(&m_channelsMutex);
ChannelData* channelData = m_channels[channelId];
// 构造周立功CAN帧结构体
VCI_CAN_OBJ canObj;
memset(&canObj, 0, sizeof(VCI_CAN_OBJ));
canObj.ID = msg.id;
canObj.ExternFlag = 1; // 扩展帧
canObj.RemoteFlag = 0; // 数据帧
canObj.DataLen = msg.dataLen;
memcpy(canObj.Data, msg.data.constData(), msg.dataLen);
// 调用驱动发送
ULONG result = VCI_Transmit(VCI_USBCAN2, channelData->config.deviceIndex,
channelData->config.canIndex, &canObj, 1);
return result == STATUS_OK;
}
- 接收 CAN 数据
cpp
QList<CANMessage> USBCANManager::receiveMessages(int channelId, int maxCount)
{
QList<CANMessage> messages;
ChannelData* channelData = m_channels[channelId];
// 获取接收缓冲区帧数
ULONG num = VCI_GetReceiveNum(VCI_USBCAN2, channelData->config.deviceIndex,
channelData->config.canIndex);
if (num == 0) return messages;
// 非阻塞读取数据
VCI_CAN_OBJ objs[100];
ULONG len = VCI_Receive(VCI_USBCAN2, channelData->config.deviceIndex,
channelData->config.canIndex, objs, maxCount, 0);
// 转换为自定义CANMessage
for (ULONG i = 0; i < len; i++) {
CANMessage msg;
msg.id = objs[i].ID;
msg.dataLen = objs[i].DataLen;
msg.data = QByteArray((char*)objs[i].Data, objs[i].DataLen);
messages.append(msg);
}
return messages;
}
3.4 DeviceCANAdapter 适配器类实现(协议封装核心)
核心功能:
- 自定义 CAN 协议封装 / 解析;
- 同步收发(发送请求→等待响应);
- 广播 / 点对点通信;
- 帧校验、超时处理。
四、自定义 CAN 协议封装与解析实战
4.1 协议设计原则
在实际项目中,必须自定义应用层 CAN 协议,规范 ID 定义、数据域格式,保证设备间通信兼容。本文采用29 位扩展帧,协议设计如下:
4.2 扩展帧的29 位 CAN ID 定义(核心)
lua
29位ID分配:
[28-20] PROTNO :协议编号(9位)
[19] PTP :通信类型(1位:0=广播,1=点对点)
[18-11] DSTADDR :目标地址(8位)
[10-3] SRCADDR :源地址(8位)
[2-0] GROUP :组号(3位,固定0)
PROTNO:区分不同设备协议(如充电桩、逆变器);
PTP:0 = 广播(所有节点接收),1 = 点对点(仅目标节点接收);
DSTADDR:下位机地址(1-255);
SRCADDR:上位机地址(固定 0x01)。
4.3 CAN 数据域定义
数据域固定 8 字节,格式:
lua
字节0:功能码
字节1:错误码(比如:0xF0=成功,其他=失败)
字节2-3:寄存器地址
字节4-7:数据值(32位,根据情况可拆分或组合使用)
4.4 协议封装代码实现
- 封装 29 位 CAN ID
cpp
QByteArray DeviceCANAdapter::pack29BitCanId(quint16 protNo, quint8 ptp, quint8 dstAddr,
quint8 srcAddr, quint8 group)
{
quint32 canId = 0;
canId |= ((protNo & 0x1FF) << 20); // 协议号
canId |= ((ptp & 0x01) << 19); // 点对点标识
canId |= ((dstAddr & 0xFF) << 11); // 目标地址
canId |= ((srcAddr & 0xFF) << 3); // 源地址
canId |= (group & 0x07); // 组号
// 转为4字节大端数组
QByteArray data(4, 0);
data[0] = (canId >> 24) & 0xFF;
data[1] = (canId >> 16) & 0xFF;
data[2] = (canId >> 8) & 0xFF;
data[3] = canId & 0xFF;
return data;
}
- 封装写命令帧
cpp
QByteArray DeviceCANAdapter::packSetFrame(quint16 protNo, quint8 ptp, quint8 dstAddr,
quint8 srcAddr, quint8 group,
quint16 regAddr, quint32 setData)
{
QByteArray frame;
frame.append(pack29BitCanId(protNo, ptp, dstAddr, srcAddr, group));
// 数据域:功能码+保留+寄存器地址+数据
QByteArray data(8, 0);
data[0] = 0x03; // 写功能码
data[2] = (regAddr >> 8) & 0xFF;
data[3] = regAddr & 0xFF;
data[4] = (setData >> 24) & 0xFF;
data[5] = (setData >> 16) & 0xFF;
data[6] = (setData >> 8) & 0xFF;
data[7] = setData & 0xFF;
frame.append(data);
return frame;
}
- 帧转换为 CANMessage
这里很重要,如何将你的字节帧数据转换成周立功驱动里的格式。
cpp
CANMessage DeviceCANAdapter::buildCANMessage(const QByteArray& frame)
{
CANMessage msg;
// 解析ID
msg.id = ((quint8)frame[0] << 24) | ((quint8)frame[1] << 16)
| ((quint8)frame[2] << 8) | ((quint8)frame[3]);
msg.isExtFrame = 1; // 扩展帧
msg.data = frame.mid(4);// 数据域
msg.dataLen = msg.data.size();
return msg;
}
4.5 协议解析与校验
cpp
bool DeviceCANAdapter::checkRespFrame(const QByteArray& resp, quint16 regAddr)
{
QByteArray data = resp.size() >=12 ? resp.mid(4,8) : resp;
// 校验功能码
if (data[0] != 0x41 && data[0] != 0x42) return false;
// 校验错误码
if (data[1] != 0xF0) return false;
// 校验寄存器地址
quint16 respReg = (quint16)data[2] << 8 | data[3];
return respReg == regAddr;
}
五、Qt 多线程实现 CAN 数据收发
5.1 为什么需要多线程?
- 主线程不阻塞:CAN 接收需要轮询,同步收发有超时等待,若在主线程执行,界面会卡死;
- 多通道并发:USBCAN2 有两个通道,需独立线程收发数据;
- 实时性:独立线程保证数据实时接收,不被界面操作影响。
5.2 Qt 多线程实现方案
采用QObject+moveToThread方案(Qt 推荐,安全稳定):
- 创建CANWorker工作类,继承 QObject,实现接收轮询逻辑;
- 将 worker 移入子线程;
- 信号与槽通信,线程安全传递数据;
- 启动 / 停止线程控制收发。
5.3 多线程核心代码
- CAN 工作线程类
cpp
class CANWorker : public QObject
{
Q_OBJECT
public:
explicit CANWorker(int channelId);
public slots:
void startWork(); // 启动接收
void stopWork(); // 停止接收
private:
int m_channelId;
bool m_isRunning;
USBCANManager* m_canManager;
signals:
void receivedData(QList<CANMessage> msgs); // 数据接收信号
};
- 线程轮询接收逻辑
cpp
void CANWorker::startWork()
{
m_isRunning = true;
while (m_isRunning)
{
// 非阻塞读取数据
QList<CANMessage> msgs = m_canManager->receiveMessages(m_channelId, 10);
if (!msgs.isEmpty()) {
emit receivedData(msgs); // 发送数据到主线程
}
QThread::msleep(5); // 轮询间隔5ms,降低CPU占用
}
}
- 线程启动与绑定
cpp
// 主线程中创建线程
QThread* m_receiveThread = new QThread(this);
CANWorker* m_worker = new CANWorker(ChannelId_Inverter);
m_worker->moveToThread(m_receiveThread);
// 信号槽连接
connect(m_receiveThread, &QThread::started, m_worker, &CANWorker::startWork);
connect(m_worker, &CANWorker::receivedData, this, &MainWindow::onReceiveData);
connect(m_receiveThread, &QThread::finished, m_worker, &QObject::deleteLater);
// 启动线程
m_receiveThread->start();
5.4 同步收发实现(发送→等待响应)
上位机发送读 / 写命令后,需要等待下位机响应,实现同步通信:
cpp
CANMessage DeviceCANAdapter::sendAndWaitResponse(const CANMessage& request, int timeoutMs)
{
// 清空旧数据
m_canManager->receiveMessages(m_channelId, 100);
// 发送请求
m_canManager->sendMessage(m_channelId, request);
// 计算预期响应ID
quint32 expectId = 交换源地址和目标地址后的ID;
// 轮询等待响应
qint64 startTime = QDateTime::currentMSecsSinceEpoch();
while (QDateTime::currentMSecsSinceEpoch() - startTime < timeoutMs)
{
QList<CANMessage> msgs = m_canManager->receiveMessages(m_channelId, 10);
for (const auto& msg : msgs) {
if (msg.id == expectId) return msg;
}
QThread::msleep(5);
}
return CANMessage(); // 超时
}
六、完整工程使用与测试
6.1 硬件连接
- USBCAN 模块插入电脑 USB 口;
- CAN_H、CAN_L 连接下位机 CAN 总线;
- 总线两端接 120Ω 终端电阻。
6.2 软件运行流程
- 打开工程,配置设备序列号;
- 编译运行,自动探测设备;
- 打开 CAN 通道,启动接收线程;
- 发送读 / 写命令,接收下位机数据;
- 关闭通道,关闭设备。
6.3 常见问题排查
- VCI_OpenDevice 失败:驱动未安装、设备未插入、索引错误;
- 无法接收数据:波特率不匹配、终端电阻未接、ID 滤波错误;
- 发送失败:总线关闭、通道未启动、设备离线;
- 线程卡死:轮询间隔过短、未加 msleep、死循环。
七、总结
本文完整讲解了CAN 协议核心原理、周立功 USBCAN 驱动接口、Qt 驱动调用、自定义协议封装、多线程收发五大核心内容。周立功 USBCAN+Qt 是上位机 CAN 通信开发的黄金组合,经常在实际工业中大量使用,熟悉CAN协议可以快速开发出稳定、高效的 CAN 总线上位机软件,满足各类工业设备的通信需求。