Qt之CAN设计(十三)

发布于:2024-12-21 ⋅ 阅读:(16) ⋅ 点赞:(0)

Qt开发 系列文章 - CAN(十三) 


目录

前言

一、CAN

二、实现方式

1.创建类

2.相关功能函数

3.用户使用

4.效果演示

5.拓展应用-实时刷新

总结


前言

Qt框架中并没有提供关于CAN接口的相关模块,需要用户自己根据CAN接口硬件模块,例如周立功CAN分析仪,编写底层接口功能函数,然后调用它。一般用户买的CAN分析仪工具都自带二次开发功能,即对于can接口的底层函数都已编写好,且封装完成,用户只需要根据头文件调用相关功能即可,完成对他的二次开发。


一、CAN

CAN(Controller Area Network)是一种用于连接微控制器和设备的串行总线标准,主要用于实时数据交换,它支持分布式控制或实时控制的串行通信系统,特别适用于那些对时间延迟敏感的应用。CAN通信基于广播模式,任何节点发送的数据可以被网络上所有其他节点接收,每个节点都可以通过发送具有唯一标识符的消息来与其他节点通信。与传统的点对点通信相比,CAN总线具有更高的可靠性和灵活性,它能够自动检测错误并采取相应措施,如重新发送数据包,确保数据的完整性和准确性。

‌CAN总线标准由ISO/IEC 11898和ISO/IEC 11519定义,广泛应用于汽车工业、工业自动化、楼宇自动化、医疗设备、机器人等领域。在工业自动化中,CAN总线用于连接传感器、执行器和控制单元,实现设备的互联和监控。

二、实现方式

要实现CAN上位机通讯,通过相关工具,一般是CAN分析仪板,市场都有卖的,连接到你的上位机电脑上面。然后根据卖家提供的CAN硬件底层接口源文件或库文件,导入到你的项目中,进行二次开发。

本文使用的是周立功的分析仪工具,首先将厂家提供的底层CAN接口库和头文件添加到项目中,然后使用开发!

574ba6ed91ff4967920570b3d2c0c010.png

1.创建类

在Qt项目上创建一个串口类,属于线程public QThread,具体实现如下。

#include <QThread>
#include "ControlCAN.h"

#define CANalyst_II  4  //CAN卡设备类型 USBCAN-2A或USBCAN-2C或CANalyst-II类型均为4
#define CAN_Channel  0  //CAN卡通道 0和1
#define CAN_Reserve  0  //保留参数
#define CAN_MSG_TYPE_FILTER 0x4
#define CAN_MSG_LEN sizeof(CAN_Message)	//13字节
#define ID_IGNORE_CAN_BUS(id) ((id) & 0x1FFFFCFFUL) // 扩展帧有效位29位标识位去掉bus位

class CANThread:public QThread
{
    Q_OBJECT
public:
    CANThread();
    ~CANThread();
    bool openCAN();
    void closeCAN();
    bool creatdbtable();
    void closedbtable(void);
    void ClearCANmsgsData();
    void PushSendData(CAN_Message);

    quint32 deviceType;  //设备类型
    quint32 deviceIndex; //设备索引
    quint32 baundRate;   //波特率
    quint32 devicCom;    //CAN通道
    QString isonline;
    bool stopped;
    bool savefile;
    QList<CAN_Message> CANmsgs;
signals:
    void getProtocolData(QString);
protected:
    void run();
    void SendCANMsgs(void);
    void RecvCANMsgs(void);
    //void sleep(unsigned int msec);
    void ProcessCANMsg(CAN_Message);
    QDateTime m_zerot;
    qint64 getSystemTimeGap(){
        QDateTime nowTime = QDateTime::currentDateTime();
        return m_zerot.msecsTo(nowTime);
    }
private:
    QSqlDatabase db;  //数据库对象
    QSqlQuery sql;
    mutable QMutex DataLock;
};

 

2.相关功能函数

该线程类的实现:首先在构造函数初始化相关变量;然后通过openCAN函数完成对CAN口的一系列设置;最后启动线程run后,在while循环中实现对CAN口的收(SendCANMsgs函数)、发(RecvCANMsgs函数)。代码如下(示例):

CANThread::CANThread()
{
    stopped = true;
    savefile = false;
    isonline = "";
    //sql = new QSqlQuery();
    qRegisterMetaType<VCI_CAN_OBJ>("VCI_CAN_OBJ");
    qRegisterMetaType<DWORD>("DWORD");
}
CANThread::~CANThread()
{
    if(isRunning()){
        exit();
        wait(100);
    }
    db.close();
}
void CANThread::closedbtable(void)
{
    // 提交事务并保存更改
    if (!db.commit())
        qDebug() << "Failed to commit transaction";
    db.close();
}
bool CANThread::openCAN()
{
    //**1**打开设备 注意一个设备只能打开一次
    if(1 != VCI_OpenDevice(deviceType, deviceIndex, CAN_Reserve)){
        emit getProtocolData("CAN device open fail.");
        return false;
    }
    //**2**清空指定CAN通道接收和发送缓冲区
    if(1 != VCI_ClearBuffer(deviceType, deviceIndex, devicCom)){
        emit getProtocolData("Clear the buffer of 1#CAN channel fail.");
    }
    if(1 != VCI_ClearBuffer(deviceType, deviceIndex, devicCom+1)){
        emit getProtocolData("Clear the buffer of 2#CAN channel fail.");
    }
    //**3**配置CAN数据类型
    VCI_INIT_CONFIG vic;
    vic.AccCode=0x80000000;
    vic.AccMask=0xFFFFFFFF;
    vic.Filter=1;
    vic.Mode=0;
    //**4**确定CAN波特率
    switch (baundRate) {
    case 10:
        vic.Timing0=0x31;
        vic.Timing1=0x1c;
        break;
    case 20:
        vic.Timing0=0x18;
        vic.Timing1=0x1c;
        break;
    case 40:
        vic.Timing0=0x87;
        vic.Timing1=0xff;
        break;
    case 50:
        vic.Timing0=0x09;
        vic.Timing1=0x1c;
        break;
    case 80:
        vic.Timing0=0x83;
        vic.Timing1=0xff;
        break;
    case 100:
        vic.Timing0=0x04;
        vic.Timing1=0x1c;
        break;
    case 125:
        vic.Timing0=0x03;
        vic.Timing1=0x1c;
        break;
    case 200:
        vic.Timing0=0x81;
        vic.Timing1=0xfa;
        break;
    case 250:
        vic.Timing0=0x01;
        vic.Timing1=0x1c;
        break;
    case 400:
        vic.Timing0=0x80;
        vic.Timing1=0xfa;
        break;
    case 500:
        vic.Timing0=0x00;
        vic.Timing1=0x1c;
        break;
    case 666:
        vic.Timing0=0x80;
        vic.Timing1=0xb6;
        break;
    case 800:
        vic.Timing0=0x00;
        vic.Timing1=0x16;
        break;
    case 1000:
        vic.Timing0=0x00;
        vic.Timing1=0x14;
        break;
    case 33:
        vic.Timing0=0x09;
        vic.Timing1=0x6f;
        break;
    case 66:
        vic.Timing0=0x04;
        vic.Timing1=0x6f;
        break;
    case 83:
        vic.Timing0=0x03;
        vic.Timing1=0x6f;
        break;
    default:
        break;
    }
    //**5**初始化指定CAN通道
    quint8 InitFailCnt = 0;
    if(1 != VCI_InitCAN(deviceType, deviceIndex, devicCom, &vic)){
        emit getProtocolData("CAN1 device init fail.");
        InitFailCnt += 1;
    }
    if(1 != VCI_InitCAN(deviceType, deviceIndex, devicCom+1, &vic)){
        emit getProtocolData("CAN2 device init fail.");
        InitFailCnt += 1;
    }
    if(2 == InitFailCnt)
        return false;
    //**6**获取设备信息
    VCI_BOARD_INFO vbi; //用来存储设备信息的结构指针
    if(1 != VCI_ReadBoardInfo(deviceType, deviceIndex, &vbi))
    {
        emit getProtocolData("Pick up device info fail.");
        return false;
    }
    else
        emit getProtocolData(QString("CAN通道数: %1\n硬件版本号: %2\n接口库版本号: %3")
                             .arg(vbi.can_Num).arg(vbi.hw_Version).arg(vbi.in_Version));
    //**7**启动CAN卡某个通道
    InitFailCnt = 0;
    if(1 != VCI_StartCAN(deviceType, deviceIndex, devicCom))
    {
        emit getProtocolData("Start CAN1 device fail.");
        InitFailCnt += 1;
    }
    if(VCI_StartCAN(deviceType, deviceIndex, devicCom+1) !=1)
    {
        emit getProtocolData("Start CAN2 device fail.");
        InitFailCnt += 1;
    }
    if(2 == InitFailCnt)
        return false;
    return true;
}
void CANThread::closeCAN()
{
    VCI_CloseDevice(deviceType, deviceIndex);
}
void CANThread::ClearCANmsgsData()
{
    QMutexLocker locker(&DataLock);
    CANmsgs.clear();
}
void CANThread::PushSendData(CAN_Message pdata)
{
    QMutexLocker locker(&DataLock);
    CANmsgs.push_back(pdata);
}

void CANThread::run()
{
    while(!stopped)
    {
        // 处理接受数据
        RecvCANMsgs();
        // 处理发送数据
        SendCANMsgs();
    }
}
void CANThread::SendCANMsgs(void)
{  
    if(!CANmsgs.isEmpty())
    {
        QMutexLocker locker(&DataLock);
        CAN_Message msg;
        memcpy(&msg, &CANmsgs.first(), sizeof(msg));
        DWORD dwRel;
        VCI_CAN_OBJ vco;
        vco.ID = msg.ID.id;
        vco.RemoteFlag = 0;
        vco.ExternFlag = 1;
        vco.DataLen = msg.Len;
        for(uchar j=0;j<msg.Len;j++)
            vco.Data[j] = msg.Data.b[j];
        dwRel = VCI_Transmit(deviceType, deviceIndex, devicCom, &vco,1);
        if(dwRel>0){
            qDebug()<<"成功发送帧数:" << dwRel;
            //CAN_Message dealmsg;
            //dealmsg = CANmsgs.takeFirst(); //移除队头元素并返回该元素,不释放该元素内存
            CANmsgs.removeFirst(); //删除队头元素不返回,释放该元素内存,更有效率
        }
        else
            qDebug()<<"发送错误:" << dwRel;
    }
}
void CANThread::RecvCANMsgs(void)
{
    static quint8 invalmsg=0,dismsg=0;
    int cancnt;
    VCI_CAN_OBJ vco[2500];
    CAN_Message pData;
    cancnt = VCI_Receive(deviceType, deviceIndex, devicCom+1, vco,2500,0); //返回CAN2实际读取的帧数
    if(NULL < cancnt){
        for(int i=0;i<cancnt;i++)
        {
            pData.ID.id = vco[i].ID;
            pData.Len = vco[i].DataLen;
            for(quint8 j = 0;j<8;j++)
                pData.Data.b[j] = vco[i].Data[j];
            ProcessCANMsg(pData);
            //memset(&pData, 0, CAN_MSG_LEN);
        }
        isonline = QString("lightgreen");
        invalmsg = 0;
        dismsg = 0;
    }
    else if(NULL == cancnt){
        invalmsg++;
        if(invalmsg > 70){
            isonline = QString("lightyellow");
            invalmsg = 0;
        }
        dismsg = 0;
    }
    else{
        //设备不存在或USB掉线 可以调用VCI_CloseDevice并重新VCI_OpenDevice
        //如此可以达到USB-CAN设备热插拔的效果
        emit getProtocolData("CAN2 Device not find or USB disconnect.");
        //VCI_CloseDevice(deviceType, devicIndex);
        //VCI_OpenDevice(deviceType, devicIndex, CAN_Reserve);
        dismsg++;
        if(dismsg > 70){
            isonline = QString("lightred");
            dismsg = 0;
        }
    }
    msleep(30);
    //同时每隔30ms调用一次VCI_Receive为宜.
    //在满足应用的时效性情况下 尽量降低调用VCI_Receive的频率 只要保证内部缓存不溢出 每次读取并处理更多帧 可以提高运行效率
}

3.用户使用

创建完上面的线程类后,用户需要调用/使用它,首先在构造函数初始化CAN口和CAN的参数设置变量、定时器相关变量,具体含义实现如下。

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
protected:
    void timerEvent(QTimerEvent *t);
    void InitTimerEvent(void);
private slots:

private:
    Ui::MainWindow *ui;
    CANParamSetting *CANsetting;
    CANThread *canthread;
    int m_pluseTimeid;
    QString StyleSheet;
};

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    /****定时器初始化******/
    InitTimerEvent();
    CANsetting = new CANParamSetting();
    /****CAN线程初始化****/
    canthread = new CANThread();
    /****建立信号槽联系****/
    connect(canthread,&CANThread::getProtocolData,this,&MainWindow::displayText);
    /****隐藏测试项窗口****/
    ui->tabWidget->hide();
    StyleSheet = ui->pushButton_2->styleSheet();
    //背景测试
//    QString ImagePath = qApp->applicationDirPath()+"/images/sss.gif";
//    QString ImageSheet = QString("QWidget#widget {border-image: url(%1);}").arg(ImagePath);
//    ui->widget->setStyleSheet(ImageSheet);
    /*
    首先说明一下background-image、border-image、image三种区别
    background-image:简单理解就是将图片从部件的左上角开始贴图,部件的大小限制了显示图片范围;好比是我们按照部件的大小来裁剪图片
    border-image:就是将贴图缩放进到部件里,部件能看到完整图片,但是此时图片会被压缩的变形
    iamge:部件会按照图片的原始大小进行填充
    */
}
MainWindow::~MainWindow()
{
    delete ui;
}
void MainWindow::on_pushButton_clicked()//参数设置
{
    CANsetting->show();
}
void MainWindow::on_pushButton_2_clicked()//启动CAN
{
    if(ui->pushButton_2->text() == tr("CAN设备启动"))
    {
        ui->pushButton->setEnabled(false);
        ui->pushButton_2->setText(tr("CAN设备关闭"));
        canthread->deviceType = CANalyst_II;
        canthread->deviceIndex = CANsetting->index;
        canthread->baundRate = CANsetting->baundRate;
        canthread->devicCom = CAN_Channel;
        if(canthread->openCAN())
        {
            displayText("Open CAN success.");
            canthread->ClearCANmsgsData();
            canthread->stopped = false;
            canthread->start();
        }
    }
    else if(ui->pushButton_2->text() == tr("CAN设备关闭"))
    {
        ui->pushButton->setEnabled(true);
        ui->pushButton_2->setText(tr("CAN设备启动"));
        canthread->stopped = true;
        canthread->closeCAN();
        canthread->isonline = StyleSheet;
    }
}
void MainWindow::on_pushButton_15_clicked()
{
    if(ui->pushButton_15->text() == tr("开始数据保存")){
        if(ui->pushButton_2->text() == tr("CAN关闭"))
            ui->pushButton_2->setEnabled(false);
        ui->pushButton_15->setText(tr("停止数据保存"));
        if(canthread->creatdbtable()){
            qInfo() << "Successfully create database! SQLite Type.";
            canthread->savefile = true;
            ui->pushButton_15->setStyleSheet("QPushButton { background-color: lightgreen; }");
        }
    }
    else if(ui->pushButton_15->text() == tr("停止数据保存")){
        ui->pushButton_2->setEnabled(true);
        ui->pushButton_15->setText(tr("开始数据保存"));
        canthread->savefile = false;
        canthread->closedbtable();
        ui->pushButton_15->setStyleSheet(StyleSheet);
    }
}

4.效果演示

上述理解清除后,编译运行,显示窗口如下。

f830140070d8439ea569d0258e26e95d.png

如果需要进行CAN参数设置,点击参数设置按钮,显示如下。

233bd076ab5f4a039995452c45ef9b1b.png

设置完CAN参数后,可以点击设备启动,会出现如下显示。(因为未接CAN设备,提示打开失败)

efda1d0c89194f92ad34c4a852615976.png

如果点击保存数据,会出现如下显示。

b89d9c192a3f414f86a623973885f5f9.png

停止保存数据后,会在项目路劲savefile文件夹下面看到保存的文件,如下所示。(会保存为db类型的数据库,关于其具体详细使用,会在后期更新相关使用方法教程。)

bf614017b5804c949112fe1570285afa.png

点击开放测试窗口,会出现CAN指令测试项,如下所示。

127db7db104a45c2b533f8e62d422376.png

软件上其它的功能指令,就不一一详细介绍了,具体可以摸索。

5.拓展应用-实时刷新

一般我们工业上使用CAN口,为了实现对数据实时采集、并通过曲线显示出来。这时我们一般采取的是,先在线程中将CAN口收到的数据进行解析处理ProData,然后将解析出来的结果值通过变量传递给主界面上,让其知晓,然后通过主窗口的定时器或者信号槽函数显示在UI界面上。

如果是要绘制曲线,可以参考博文Qt之第三方库QCustomPlot使用(二)-CSDN博客,了解曲线绘制的特性及使用方法。


总结

博文中相应的工程代码Qt-Case.zip 利用Qt开发软件进行编的例程,为博文提供案例-CSDN文库