因为公司有需求做一个能够用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::connectQTimer的方法为什么又可以呢?因为这种方法属于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设计结合实现一个用户界面。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐