[C++ & Qt] 从零开始的QCanBus开发(Qt CAN通信上位机之一)
因为公司有需求做一个能够用PCAN通信/刷写bootloader的上位机。一开始想要使用PCAN官方提供的库PCAN Basic API,但后来发现QCanBus可以被认为是内嵌了PCAN的接口,并且还支持其他几种工具的接口,以后如果有其他工具的扩展需要就能省去不少麻烦。我平时是一个python用户,对于C++和Qt都是新手。即便大学的启蒙编程语言就是C++,但多年不用已经忘光,现在看C++代码犹
因为公司有需求做一个能够用PCAN通信/刷写bootloader的上位机。一开始想要使用PCAN官方提供的库PCAN Basic API,但后来发现QCanBus可以被认为是内嵌了PCAN的接口,并且还支持其他几种工具的接口,以后如果有其他工具的扩展需要就能省去不少麻烦。
我平时是一个python用户,对于C++和Qt都是新手。即便大学的启蒙编程语言就是C++,但多年不用已经忘光,现在看C++代码犹如看天书……在自己学习和调试的过程中,我收获到了很多珍贵的经验,于是想把这个过程记录下来。
文章目录
在进行本文中的调试时,我只创建了一个空的Qt工程,里面工程的.pro文件,以及我自己新建的一个.cpp文件。
1. 显示可选的接口(哪些工具的plugin是已安装的)
在QCanBus这一套规则中,连接电脑和ECU的CAN适配器以plugin的形式存在(在Qt安装的过程中会安装,如果没有安装后续也可以操作单独安装插件)。那么首先就是来看看有哪些CAN适配器设备/设备厂家的插件是已经安装了的,可以使用以下代码。
#include <QCoreApplication>
#include <QCanBus>
#include <QDebug>
int main(int argc, char *argv[]) {
QCoreApplication a (argc, argv);
//List all available QCanBus plugins
QStringList plugins = QCanBus::instance()->plugins();
qDebug() << "Available CAN plugins:";
for (const QString &plugin : plugins) {
qDebug() << " -" << plugin;
}
return 0;
}
此时运行代码得到的结果是:
Available CAN plugins:
- "passthrucan"
- "peakcan"
- "systeccan"
- "tinycan"
- "vectorcan"
- "virtualcan"
不出意外这些应该就是QCanBus类支持的所有接口了,我要使用的"peakcan"也在其中。此时我添加了一段代码要求程序检测plugins中是否存在peakcan,因为后面过程中要使用到"peakcan"作为参数,如果不存在的话则以错误代码-1退出:
//Check if the "peakcan" plugin is avialable
if (!QCanBus::instance()->plugins().contains("peakcan")) {
qCritical() << "The 'peakcan' plugin is not available.";
return -1;
}
对于新手玩家如我,这时可能会出现第一个坑。忘记了具体的报错内容是什么,大概就是说引用的<QCanBus>不存在之类的。这时只要到.pro文件中添加serialbus即可:
QT += core \
serialbus
另外还有一种可能,发现自己根本就没有安装serialbus组件,这时可以找到安装Qt的文件夹中找到MaintenanceTool.exe文件,双击进入安装即可。比如我的文件路径是:"D:\Qt\MaintenanceTool.exe"。搜索"serial"之后就可以选中安装你的Qt版本对应的组件了:
2. 创建设备(createDevice)和配置连接参数(setConfigurationParameter)
从下面开始的代码均可以直接往之前的代码中添加,调试中大家灵活地使用注释屏蔽掉不需要的代码即可。
在这里需要新增引用QCanBusDevice类。在确定了我们可以使用peakcan插件后,使用createDevice()方法创建设备,于是我们就得到了一个QCanBusDevice类的实例:
#include <QCanBusDevice>
//Create the device
QCanBusDevice *device = QCanBus::instance()->createDevice("peakcan", "usb0");
if (!device) {
qCritical() << "Failed to create device.";
// qCritical() << "Failed to create device:" << QCanBus::instance()->lastError();
return -1;
}
这里出现了我遇到的第二个坑:识别设备名称。createDevice()方法中需要带两个参数,一个是我们要使用的plugin "peakcan",另一个就是你现在连接的设备名字(在这里我我的设备名字叫"usb0")。那么设备名字究竟是什么呢?我到处找也没找到,使用PCAN-View连接设备也没找到设备名字在哪里(由于我对于PCAN也是完全的新手,找不到设备名字很有可能是我的问题,如果有朋友知道在哪里看也可以留言指导我一下,感谢)。最终解决方案是添加了一段代码来识别当前可用的device name:
//Check available devices names
QList<QCanBusDeviceInfo> devices = QCanBus::instance()->availableDevices("peakcan");
qDebug() << "Available devices:";
for (const QCanBusDeviceInfo &deviceInfo : devices) {
qDebug() << "Device name:" << deviceInfo.name();
}
接下来就是配置连接参数,比如我的例子中,我们使用CAN FD通信,bit rate 500 kbps,data bit rate 2 Mbps。我在这里添加了一个报错机制,代码如下:
//Set the CAN FD option
device->setConfigurationParameter(QCanBusDevice::CanFdKey, QVariant(true));
//Set bit rates
device->setConfigurationParameter(QCanBusDevice::BitRateKey, QVariant(500000)); //500 kbps
device->setConfigurationParameter(QCanBusDevice::DataBitRateKey, QVariant(2000000)); //2 Mbps
//Connect to errorOcurred signal for debugging
QObject::connect(device, &QCanBusDevice::errorOccurred, [](QCanBusDevice::CanBusError error) {
qDebug() << "CAN Bus Error:" << error;
});
3. 连接设备和查看设备状态
在配置好了设备连接参数之后,我们使用QCanBusDevice类中的connectDevice()方法连接设备。由于我正处于调试阶段,所以代码中添加了大量报错机制:
//Connect to the device
if (!device->connectDevice()) {
qCritical() << "Failed to connect device: " << device->errorString();
delete device;
return -1;
}
连接后由于没有任何反馈,我想查看现在的连接状态,于是写了一个打印当前状态的函数printDeviceConfigurations,在main()函数中添加语句printDeviceConfigurations(device);调用这个函数即可。
void printDeviceConfigurations(QCanBusDevice *device) {
//Check the connection status of device
QCanBusDevice::CanBusDeviceState state = device->state();
switch (state) {
case QCanBusDevice::UnconnectedState:
qDebug() << "Device state: Unconnected.";
break;
case QCanBusDevice::ConnectingState:
qDebug() << "Device state: Connecting.";
break;
case QCanBusDevice::ConnectedState:
qDebug() << "Device state: Connected.";
break;
default:
qDebug() << "Unknown device status.";
}
//Display CAN FD configuration
QVariant canFd = device->configurationParameter(QCanBusDevice::CanFdKey);
qDebug() << "CAN FD Enabled:" << canFd.toBool();
//Display arbitration bit rate
QVariant bitRate = device->configurationParameter(QCanBusDevice::BitRateKey);
if (bitRate.isValid()) {
qDebug() << "Arbitration bit rate:" << bitRate.toInt() << "bps";
} else {
qDebug() << "Arbitration bit rate: Not configured.";
}
//Display arbitration data bit rate
QVariant dataBitRate = device->configurationParameter(QCanBusDevice::DataBitRateKey);
if (dataBitRate.isValid()) {
qDebug() << "Arbitration data bit rate:" << dataBitRate.toInt() << "bps";
} else {
qDebug() << "Arbitration data bit rate: Not configured.";
}
}
运行后输出的内容为:
Device state: Connected.
CAN FD Enabled: true
Arbitration bit rate: 500000 bps
Arbitration data bit rate: 2000000 bps
特别说明:对于上面的打印函数我还有一个疑虑,使用configurationParameter()方法获得参数,理论上讲获得的应该只是我前面配置的参数,而非现在连接中的真正参数。但是否还有其他方法能获得连接的真实配置,我还没有深入研究。
至此,我们已经成功地连接了设备,可以进行进一步的读写操作了。
4. 接收数据
首先让我们用一段简单的代码看看Client(我们的程序)是不是收到了数据,这里使用的是QCanBusDevice类的framesAvailable()方法,所输出的数字是现在buffer中所累加的frame数量。同时这里使用的QTimer需要现在程序中引用。
#include <QTimer>
//Periodically check framesAvailable
QTimer timer;
QObject::connect(&timer, &QTimer::timeout, [&]() {
qDebug() << "framsAvailable:" << device->framesAvailable();
});
timer.start(1000); //check every 1000 ms
输出的内容如下,每秒增加一条输出:
framsAvailable: 1113
framsAvailable: 1650
framsAvailable: 2183
framsAvailable: 2713
......
因为我这个ECU中的程序存在多个frame ID,但我不想每次接收数据时都是一大堆,只想要筛选出我感兴趣的frame ID打印出来,也就是大家常见的筛选器。然后我就遇到了坑之三:QCanBusDevice::RawFilterKey。起初我打算通过上面设置通信波特率一样的办法(setConfigurationParameter)做frame ID筛选,代码就不贴了,总之用了很多时间也没成功实现筛选功能。考虑到可能peakcan并不完全支持QCanBusDevice::RawFilterKey,也就是不能从驱动层面做筛选,最终决定在读通信数据的过程中采用土办法if frame.frameId() == 0x123...来实现筛选效果。
读数据的关键在于使用readFrame()方法。在读写数据这部分内容中,我们又新引入了类QCanBusFrame,需要在代码中引用。
起初我直接使用了下面代码中的while loop及其里面的内容,但run了程序之后发现始终没有读到的报文打印出来;加了一个qDebug() << device->framesAvailable() 在while loop前,来打印一下现在已经buffer了多少frames,结果竟然是0!这到底是为什么呢?
#include <QCanBusFrame>
#include <QTimer>
QTimer timer;
QObject::connect(&timer, &QTimer::timeout, [&]() {
while (device->framesAvailable()) {
QCanBusFrame frame = device->readFrame();
if (frame.frameId() != 0x215) {
continue;
}
//Print frame details
qDebug() << "Received frame:";
qDebug() << "- Frame ID:" << QString::number(frame.frameId(), 16).toUpper();
qDebug() << "- Frame type:" << frame.frameType();
qDebug() << "- payload:" << frame.payload().toHex();
qDebug() << "- Payload size:" << frame.payload().size();
//print timestamp
const QCanBusFrame::TimeStamp timeStamp = frame.timeStamp();
qDebug() << "- Timestamp:" << QString("%1.%2").arg(timeStamp.seconds()).arg(timeStamp.microSeconds(), 6, 10, QChar('0'));
}
});
timer.start(1000); //check every 1000ms
如果我们直接使用while loop, 当程序运行到while loop的时候,会直接停掉所有其它的events,当然也包括我们的QCanBusDevice下的所有events。也就是说,这个过程就是while loop 去call device->framesAvailable(),但其它event已经被block了,QCanBusDevice不能处理进来的frames,所以framesAvailable()保持为0。
那么使用QObject::connect和QTimer的方法为什么又可以呢?因为这种方法属于Qt的event loop,它允许QCanBusDevice异步地处理进来的报文并且buffer它们。程序运行的效果就是,每秒钟跳出这一秒存储的所有frames。
同时,如果不想要使用QTimer,或者说不想每一秒钟刷新一次,还有如下使用QCoreApplication::processEvents()的方法:
//Continuous loop with event processing
while (true) {
QCoreApplication::processEvents();
if (device->framesAvailable()) {
qDebug() << device->framesAvailable();
QCanBusFrame frame = device->readFrame();
//Print frame details
qDebug() << "Received frame:";
qDebug() << "- Frame ID:" << QString::number(frame.frameId(), 16).toUpper();
qDebug() << "- Frame type:" << frame.frameType();
qDebug() << "- payload:" << frame.payload().toHex();
qDebug() << "- Payload size:" << frame.payload().size();
const QCanBusFrame::TimeStamp timeStamp = frame.timeStamp();
qDebug() << "- Timestamp:" << QString("%1.%2").arg(timeStamp.seconds()).arg(timeStamp.microSeconds(), 6, 10, QChar('0'));
}
}
但是,如果你希望在读取CAN帧的同时还能做些其他的事,还是推荐用QTimer的方案。
5. 写数据
与读数据对应的,写数据使用的是writeFrame()方法。
//Create and send a frame
QCanBusFrame w_frame;
w_frame.setFrameId(0x123);
w_frame.setPayload(QByteArray::fromHex("1122334455667788"));
//For CAN FD, enable flexible data rate
w_frame.setFrameType(QCanBusFrame::DataFrame);
w_frame.setExtendedFrameFormat(false);
//Write the frame
if (!device->writeFrame(w_frame)) {
qCritical() << "Failed to write frame:" << device->errorString();
} else {
qDebug() << "Frame successfully written:";
qDebug() << "- Frame ID:" << QString::number(w_frame.frameId(), 16).toUpper();
qDebug() << "- payload:" << w_frame.payload().toHex();
}
至此,使用C++进行 CAN (FD) 通信的基本功能已经实现完成。
不过既然我们选择使用Qt,多数开发者还是希望能最终实现一个GUI的。下一篇文章将讲解,如何将我们已经调试好的代码与GUI设计结合实现一个用户界面。
更多推荐


所有评论(0)