CAN通讯实战:Qt基于周立功 USBCAN 的 CAN 总线通信开发全攻略

前言

在工业控制、汽车电子、新能源设备等领域,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 年推出的串行通信协议,核心设计目标是高可靠性、实时性、多主结构,无需主机调度,任意节点均可主动发送数据。

  • 核心特性:
  1. 多主控制:所有节点地位平等,均可主动发送数据,无需主机控制;
  2. 非破坏性总线仲裁:通过 ID 优先级解决总线冲突,优先级高的帧优先发送;
  3. 数据帧短帧结构:最大数据长度 8 字节,传输速度快、实时性高;
  4. 错误检测与重发:硬件自动检测错误、重发损坏帧,可靠性极高;
  5. 节点自动离线:严重错误时节点自动断开总线,不影响其他设备通信。
  • 应用场景:
  1. 汽车电子:发动机控制、车身控制、电池管理(BMS);
  2. 工业控制:PLC、传感器、执行器通信;
  3. 新能源:充电桩、储能设备、逆变器通信;
  4. 医疗设备、机器人控制。

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),结构如下:

  1. 标准数据帧(11 位 ID)
    结构:帧起始 → 仲裁段(11 位 ID+RTR) → 控制段 → 数据段(0-8 字节) → CRC 段 → ACK 段 → 帧结束
  2. 扩展数据帧(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 总线采用非破坏性位仲裁,解决多节点同时发送数据的冲突:

  1. 所有节点同步发送帧 ID;
  2. 显性电平(0)覆盖隐性电平(1);
  3. ID 数值越小,优先级越高;
  4. 仲裁失败的节点自动转为接收模式,等待总线空闲后重发。

优势:无冲突、无延迟、实时性极高。

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 驱动安装与库文件准备

  1. 驱动安装
    周立功官网下载对应型号驱动;
    安装驱动后,插入 USBCAN 模块,设备管理器显示Zhouligong USBCAN即安装成功。
  2. 开发文件准备
    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

核心分为宏定义、结构体、函数声明三部分:

  1. 跨平台类型定义
    解决 Windows 与 Linux 平台类型不兼容问题,避免windows.h冲突:
cpp 复制代码
// Windows平台使用原生类型,Linux使用Qt类型
typedef quint32 DWORD;
typedef quint8 BYTE;
typedef void* PVOID;
#define STDCALL __stdcall  // Windows调用约定
  1. 设备与错误码宏定义
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溢出
  1. 核心结构体(必掌握)
    周立功驱动通过结构体传递 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调用约定:

  1. 设备操作函数
  2. 数据收发函数
  3. 信息读取函数

三、Qt 环境下调用周立功 USBCAN 驱动

3.1 Qt 工程配置

  1. 新建 Qt Widgets 工程
  2. 添加驱动文件
    将ControlCAN_qt.h加入工程,将ControlCAN.dll放入编译输出目录。
  3. 跨平台库加载配置
    在.pro文件中添加动态库引用:
cpp 复制代码
# 引入USBCAN库
INCLUDEPATH += $$PWD/ControlCAN
# 链接库
LIBS += -L$$PWD/ControlCAN -lControlCAN

3.2 核心设计思路

为了保证代码模块化、线程安全、可复用,我们设计三层架构:

USBCANManager:单例模式,管理所有 USBCAN 设备、CAN 通道,封装驱动 API;

DeviceCANAdapter:CAN 适配器,封装协议解析、数据收发、同步等待;

业务层:调用适配器实现设备控制、数据采集。

3.3 USBCANManager 单例类实现(设备管理核心)

核心功能:

探测 USBCAN 设备、匹配序列号;

打开 / 关闭 CAN 通道、初始化波特率;

线程安全的数据收发;

多通道管理。

关键代码解析:

  1. 单例模式(线程安全懒汉式)
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;
}

优势:全局唯一实例,避免重复打开设备、线程安全。

  1. 探测 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;
}
  1. 初始化 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;
}
  1. 发送 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;
}
  1. 接收 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 协议封装代码实现

  1. 封装 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;
}
  1. 封装写命令帧
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;
}
  1. 帧转换为 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 多线程核心代码

  1. 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); // 数据接收信号
};
  1. 线程轮询接收逻辑
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占用
    }
}
  1. 线程启动与绑定
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 总线上位机软件,满足各类工业设备的通信需求。

相关推荐
大迪deblog1 小时前
软件工程-④测试
系统架构·软件工程
xiaoxiaoxiaolll1 小时前
《Nature Communications》论文解读:皮秒级单光子偏振测量如何绘制多模光纤中的模态动态图谱
网络·人工智能
大迪deblog1 小时前
软件工程-③结构化分析与设计
系统架构·软件工程
Inhand陈工1 小时前
城投公司地面与停车场监控改造实战:映翰通IR302 + GRE隧道实现RFID与视频数据远程汇聚
网络·人工智能·物联网·网络安全·智能路由器·信息与通信
其实防守也摸鱼2 小时前
DVWA--Brute Force (暴力破解)通关指南
服务器·网络·安全·靶场·教程·工具·dvwa
_君莫笑2 小时前
Qt+Qml前后端分离上位机软件技术方案
c++·qt·用户界面·qml
源远流长jerry2 小时前
TCP 三次握手深度解析:从内核源码到生产实践
linux·运维·网络·网络协议·tcp/ip
加号32 小时前
【Python】 实现 HTTP 网络请求功能入门指南
网络·python·http
数据门徒2 小时前
神经网络原理 第五章:径向基函数网络
网络·人工智能·神经网络