Qt开发 系列文章 - CAN(十三)
目录
前言
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接口库和头文件添加到项目中,然后使用开发!
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.效果演示
上述理解清除后,编译运行,显示窗口如下。
如果需要进行CAN参数设置,点击参数设置按钮,显示如下。
设置完CAN参数后,可以点击设备启动,会出现如下显示。(因为未接CAN设备,提示打开失败)
如果点击保存数据,会出现如下显示。
停止保存数据后,会在项目路劲savefile文件夹下面看到保存的文件,如下所示。(会保存为db类型的数据库,关于其具体详细使用,会在后期更新相关使用方法教程。)
点击开放测试窗口,会出现CAN指令测试项,如下所示。
软件上其它的功能指令,就不一一详细介绍了,具体可以摸索。
5.拓展应用-实时刷新
一般我们工业上使用CAN口,为了实现对数据实时采集、并通过曲线显示出来。这时我们一般采取的是,先在线程中将CAN口收到的数据进行解析处理ProData,然后将解析出来的结果值通过变量传递给主界面上,让其知晓,然后通过主窗口的定时器或者信号槽函数显示在UI界面上。
如果是要绘制曲线,可以参考博文Qt之第三方库QCustomPlot使用(二)-CSDN博客,了解曲线绘制的特性及使用方法。
总结
博文中相应的工程代码Qt-Case.zip 利用Qt开发软件进行编的例程,为博文提供案例-CSDN文库。