前言
前段时间老板给了个 广成科技 的USB Can盒子,说是要做can总线通信,以前也没弄过这东西,上网一搜,有现成的QT代码,这不分分钟搞定嘛。
#include <QCoreApplication>
#include <QCanBus>
#include <QCanBusDevice>
#include <QCanBusFrame>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 查看可用的 CAN 接口
qDebug() << "Available CAN plugins:" << QCanBus::instance()->plugins();
// 创建 CAN 设备
QCanBusDevice *device = QCanBus::instance()->createDevice("socketcan", "can0");
if (!device) {
qDebug() << "Failed to create CAN device";
return -1;
}
// 连接到 CAN 设备
if (!device->connectDevice()) {
qDebug() << "Failed to connect to CAN device";
delete device;
return -1;
}
// 创建 CAN 帧并发送
QCanBusFrame frame;
frame.setFrameId(0x123);
frame.setPayload(QByteArray("Hello, CAN!"));
if (device->writeFrame(frame)) {
qDebug() << "Frame sent successfully";
} else {
qDebug() << "Failed to send frame";
}
// 读取接收到的 CAN 帧
while (device->framesAvailable()) {
QCanBusFrame receivedFrame = device->readFrame();
qDebug() << "Received frame ID:" << receivedFrame.frameId()
<< "Payload:" << receivedFrame.payload();
}
// 断开连接并释放资源
device->disconnectDevice();
delete device;
return a.exec();
}
问题来了,socketcan是linux才支持的东西,windows并不支持。咨询了下业内人士,windows下都要基于usb can盒子的厂商的sdk做二次开发,于是乎有找了下QT有框架,只要把二次开发的东西做成qt canbus plugin上面的代码就可以复用,QT默认支持的canbus硬件厂商有"passthrucan", "peakcan", "systeccan", "tinycan", "vectorcan"。
在github上也找到一个第3方厂商的plugin实现
https://github.com/viteo/QtIXXATCANBus-plugin
Can设备要通信需要2个以上的设备挂在can总线上,我现在手头上只有一个 广成 的can,还要大几百块,有点小贵。那就上淘宝花30大洋买个支持peakcan,一不小心买到了一个刷kvaser固件的。那就只能含泪再给它写个plugin了。。。
QT官方文档
Qt CAN Bus | Qt Serial Bus 6.6.1
从官方文档得知,实现一个自定义的canbus plugin需要完成以下东西:
1. 添加一个 plugin.json 在load plugin的时候会使用到
2. 实现某些接口
3. 生成的dll文件要部署到QT的安装目录 $QTDIR/plugins/canbus
前期准备
在kvaser官网把二次开发的sdk包下载下来,是个exe文件,canlib.exe
安装后到安装目录取出我们需要的库文件和头文件
C:\Program Files (x86)\Kvaser\Canlib\Lib\x64\canlib32.lib
C:\Program Files (x86)\Kvaser\Canlib\INC
创建Plugin工程
QMake文件如下
#! [0]
TEMPLATE = lib
CONFIG += plugin
QT += serialbus core
HEADERS = kvaserplugin.h \
canevt.h \
canlib.h \
canstat.h \
kvaserbackend.h \
kvaserbackend_p.h \
obsolete.h \
predef.h
SOURCES = kvaserbackend.cpp
TARGET = $$qtLibraryTarget(kvaserplugin)
DISTFILES += \
kvaserplugin.json
win32 {
LIBS += -L$$PWD/KvaserSDKLib -lcanlib32
}
x64 {
LIBS += -L$$PWD/KvaserSDKLib -lcanlib64
}
LIBS += -lSetupapi
DESTDIR = $$[QT_INSTALL_PLUGINS]/canbus
message( can plugin install dir is $$DESTDIR)
#! [0]
下面2行搞定了QT文档中要求的json文件
DISTFILES += \
kvaserplugin.json
下面1行搞定了QT文档中要求的部署目录
DESTDIR = $$[QT_INSTALL_PLUGINS]/canbus
至此所有的工作已经完成2/3
实现Backend
新建kvaserplugin.h
填充availableDevices 和 createDevice
class KvaserPlugin : public QObject, QCanBusFactoryV2
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QCanBusFactory" FILE "kvaserplugin.json")
Q_INTERFACES(QCanBusFactoryV2)
public:
QList<QCanBusDeviceInfo> availableDevices(QString* errorMessage) const override
{
Q_UNUSED(errorMessage);
return KvaserBackend::interfaces();
}
// Name passed from availableDevices() list
QCanBusDevice* createDevice(const QString& interfaceName, QString* errorMessage) const override
{
Q_UNUSED(errorMessage);
qDebug() << "createDevice for kvaser";
auto device = new KvaserBackend(interfaceName);
return device;
}
};
新建kvaserbackend.h
这部分为QT文档中提到了需要实现的纯虚函数,里面的代码实现直接拷贝其他canbus plugin的源码。
class KvaserBackendPrivate;
class KvaserBackend : public QCanBusDevice
{
Q_OBJECT
Q_DECLARE_PRIVATE(KvaserBackend)
Q_DISABLE_COPY(KvaserBackend)
public:
explicit KvaserBackend(const QString &name, QObject *parent = nullptr);
~KvaserBackend();
bool open() override;
void close() override;
void setConfigurationParameter(int key, const QVariant &value) override;
bool writeFrame(const QCanBusFrame& newData) override;
QString interpretErrorFrame(const QCanBusFrame& errorFrame) override;
static QList<QCanBusDeviceInfo> interfaces();
private:
void resetController();
KvaserBackendPrivate * const d_ptr;
};
以close为例,真正的实现为d->close(); 即KvaserBackendPrivate里面需要真正的实现调用kvaser sdk对can盒子进行close
void KvaserBackend::close()
{
Q_D(KvaserBackend);
d->close();
setState(QCanBusDevice::UnconnectedState);
}
新建kvaserbackend_p.h
class QTimer;
class KvaserBackendPrivate
{
Q_DECLARE_PUBLIC(KvaserBackend)
public:
KvaserBackendPrivate(KvaserBackend *q);
~KvaserBackendPrivate();
bool open();
void close();
bool setConfigurationParameter(int key, const QVariant &value);
void startWrite();
void startRead();
void resetController();
bool setBitRate(int bitrate);
void checkDeviceStatus();
KvaserBackend * const q_ptr;
bool m_isOpen = false;
int m_channelIndex = 0;
canHandle m_hnd;
int m_baudrate = 200*1000;
QTimer *writeNotifier = nullptr;
QThread *m_readThread;
QTimer *m_checkDeviceTimer;
};
如上d->close(); 调用的是KvaserBackendPrivate::close,在这里调用kvaser sdk中的canBusOff(m_hnd); canClose(m_hnd); 关闭总线
void KvaserBackendPrivate::close()
{
if (m_hnd >= 0) {
canBusOff(m_hnd);
canClose(m_hnd);
}
m_readThread->exit();
delete writeNotifier;
writeNotifier = nullptr;
qDebug()<<"closed";
m_isOpen = false;
}
setConfigurationParameter实现设置波特率
bool KvaserBackendPrivate::setConfigurationParameter(int key, const QVariant &value)
{
Q_Q(KvaserBackend);
switch (key) {
case QCanBusDevice::BitRateKey:
m_baudrate = value.toInt();
return setBitRate(m_baudrate);
default:
q->setError(KvaserBackend::tr("Unsupported configuration key: %1").arg(key),
QCanBusDevice::ConfigurationError);
return false;
}
return true;
}
以下为往canbus写数据的过程
1. 将数据入队到发送队列
2. 启动writeNotifier
3.writeNotifier继承于QTimer,这里用这个有两个好处,
一、不会阻塞主线程
二、当发送队列已经发送完时调用stop把自己停掉
bool KvaserBackend::writeFrame(const QCanBusFrame &newData)
{
Q_D(KvaserBackend);
... ...
enqueueOutgoingFrame(newData);
if (!d->writeNotifier->isActive())
d->writeNotifier->start();
return true;
}
class KvaserWriteNotifier : public QTimer
{
// no Q_OBJECT macro!
public:
KvaserWriteNotifier(KvaserBackendPrivate *d, QObject *parent)
: QTimer(parent)
, dptr(d)
{
}
protected:
void timerEvent(QTimerEvent *e) override
{
if (e->timerId() == timerId()) {
dptr->startWrite();
return;
}
QTimer::timerEvent(e);
}
private:
KvaserBackendPrivate * const dptr;
};
void KvaserBackendPrivate::startWrite()
{
Q_Q(KvaserBackend);
if (!q->hasOutgoingFrames()) {
writeNotifier->stop();
return;
}
const QCanBusFrame frame = q->dequeueOutgoingFrame();
const QByteArray payload = frame.payload();
if (Q_UNLIKELY(payload.size() > 8)) {
return;
}
long id = frame.frameId();
unsigned int flag = 0;
if (frame.frameType() == QCanBusFrame::RemoteRequestFrame) {
flag |= canMSG_RTR;
}
if (frame.hasExtendedFrameFormat())
{
flag |= canMSG_EXT;
}
canWrite(m_hnd, id, (void *)payload.data(), payload.size(), flag);
if (q->hasOutgoingFrames() && !writeNotifier->isActive())
writeNotifier->start();
}
读取数据过程
二次开发sdk中canReadWait是一个同步函数,这里另外起一个读线程读取数据。
数据读取到后 q->enqueueReceivedFrames(newFrames); 入队接收队列。
KvaserBackendPrivate::KvaserBackendPrivate(KvaserBackend *q)
: q_ptr(q)
{
canInitializeLibrary();
m_readThread = new QThread;
QObject::connect(m_readThread, &QThread::started, [=]() {
while (true) {
startRead();
}
});
}
void KvaserBackendPrivate::startRead()
{
Q_Q(KvaserBackend);
QVector<QCanBusFrame> newFrames;
long id ;
uint8_t msg[16];
unsigned int dlc;
unsigned int flag;
unsigned long time;
canStatus ret = canReadWait(m_hnd, &id, (void *)msg, &dlc, &flag, &time, 0xFFFFFFFF);
if (ret != canOK) {
return;
}
QCanBusFrame frame(id, QByteArray(reinterpret_cast<char *>(msg),
int(dlc)));
frame.setTimeStamp(QCanBusFrame::TimeStamp(time));
if (flag & canMSG_EXT) {
frame.setExtendedFrameFormat(true);
}
if (flag & canMSG_RTR)
frame.setFrameType(QCanBusFrame::RemoteRequestFrame);
else
frame.setFrameType(QCanBusFrame::DataFrame);
newFrames.append(std::move(frame));
qDebug() << "enqueueReceivedFrames:";
q->enqueueReceivedFrames(newFrames);
}
查看QT源码qcanbusdevice.cpp看到数据接收到的信号发送出去了。
void QCanBusDevice::enqueueReceivedFrames(const QVector<QCanBusFrame> &newFrames)
{
Q_D(QCanBusDevice);
if (Q_UNLIKELY(newFrames.isEmpty()))
return;
d->incomingFramesGuard.lock();
d->incomingFrames.append(newFrames);
d->incomingFramesGuard.unlock();
emit framesReceived();
}
测试
在QT欢迎界面搜索can bus example,用这个工程进行测试