简介
从 Qt5.8 开始,提供了 CAN Bus 类,假设您的 Qt 版本没有 CAN Bus,可以参考 Linux 应用编程来操控开发板的 CAN,目前我们主要讲解 Qt 相关的 CAN编程。其实 Qt 也提供了相关的 Qt CAN 的例子,我们也可以直接参考来编程。读者手上需要有测试 CAN 的仪器!否则写好程序,却无法测试。
STM32板子简介
主要是讲解如何在 Qt 里对 CAN 编程。
项目示例
项目简介:本例适用于STM32开发板。不适用于 Windows。因为 Windows没有 CAN 设备。虽然 Windows 可以外接 USB CAN 模块,但是这些模块都是某些厂商开发的,需要有相应的固件才能驱动 CAN 设备。所以编写的例子不一定适用于 Windows 下的 CAN。笔者写的例子已经在正点原子 STM32开发板上验证了,确保正常使用!
在STM32板上,需要使用 CAN 必须初始化 CAN。它的开启与关闭都是由系统完成。STM32为 FD CAN,最大比特率为 5000kBit/s,本次只测试普通 CAN,非 FD模式。最大比特率为 1000 kBit/s。
在系统执行要在 100 毫秒后自动从"总线关闭"错误中恢复,并以比特率 1000000,可以使用以下命令开启 CAN。
ip link set up can0 type can bitrate 1000000 restart-ms 100
创建一个项目:QTCan.pro
c++
Qt += serialbus
QT += core gui serialbus
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
mainwindow.cpp
HEADERS += \
mainwindow.h
FORMS += \
mainwindow.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
QCanBusFrame
QCanBusFrame是一种用于控制器区域网络(CAN)通信的数据帧格式。CAN是一种广泛应用于汽车和工业领域的串行通信协议,用于在不同的电子控制单元(ECU)之间进行数据交换。
QCanBusFrame的主要作用是在CAN总线上传输数据和信息。它包含了CAN通信所需的各种字段,如标识符、数据、远程传输请求等。标识符用于唯一标识消息的发送者和接收者,数据字段用于传输实际的数据内容,远程传输请求字段用于请求其他节点发送特定的数据。
通过使用QCanBusFrame,开发人员可以方便地构建和解析CAN通信中的数据帧。QCanBusFrame是Qt框架中提供的一个类,它封装了CAN数据帧的各个字段,并提供了相应的方法来设置和获取这些字段的值。
QCanBusDevice
QCanBusDevice
是Qt中用于CAN总线通信的类。CAN(Controller Area Network)是一种常用于实时控制系统中的通信协议,常用于汽车、工业自动化等领域。
QCanBusDevice
类提供了访问CAN总线设备的功能,可以用于发送和接收CAN消息。它是Qt的CAN总线API的一部分,用于与CAN总线进行交互。
使用QCanBusDevice
,你可以执行以下操作:
-
打开和关闭CAN总线设备:使用
open()
方法打开CAN总线设备,使用close()
方法关闭设备。 -
发送CAN消息:使用
writeFrame()
方法发送CAN消息。你可以创建QCanBusFrame
对象来表示CAN消息,并设置消息的ID、数据和其他属性。 -
接收CAN消息:使用
readFrame()
方法从CAN总线设备接收CAN消息。该方法将返回一个
使用系统的 CAN 硬件,必须初始化系统的 CAN。在项目里添加相应的开启CAN 的指令。第一个指令是先关闭本地的 CAN,因为只有关闭 CAN,才能以新的速率再开启。构造函数里界面初始化,以及 QComboBox 里的项初始化。格式化帧处理函数。
发送消息,将 lineEdit 的文本进行处理后,第一个作为 CAN 的帧 ID,后面 8个数据作为需要要发送的数据。每帧只能发送 8 个数据。接收消息,读取帧并格式化处理,显示到 textBrowser 里。界面布局初始化设置,在嵌入式里,根据实际的屏的大小,设置全屏显示。可用插件初始化,检查系统 QCanBus 提供的插件。在 Linux 里使用的插件
类型是 SocketCAN, SocketCAN 插件支持 Linux 内核和用于所用 CAN 硬件的 SocketCAN 设备驱动程序。下面程序遍历可用的 CAN 插件,并设置 socketcan 为当前插件。注意,只能使用SocketCAN 访问本地硬件 CAN,其他插件是不同类型的 CAN 驱动程序所使用的。请自行测试。 当插件类型改变时,我们需要更新可用接口。
常用的比特率初始化。连接/断开 CAN,很遗憾 Qt 的 QCanBusDevice::BitRateKey 不能设置比特率,因为系统的 CAN 需要使用 ip 指令以一个比特率才能进行初始化,Qt 需要系统 CAN 起来才能进行操作。所以需要使用系统指令设置 CAN。错误处理,CAN 设备可能遇到错误,打印错误的信息。
头文件:
c++
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include<QCanBus> // 总线
#include<QCanBusDevice> //总线设备
#include<QPushButton>
#include <QTextBrowser>
#include <QLineEdit>
#include <QVBoxLayout>
#include <QLabel>
#include <QComboBox>
#include <QGridLayout>
#include <QMessageBox>
#include <QDebug>
#include <QTextCodec>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
/* CAN 设备 */
QCanBusDevice *canDevice;
/* 用作接收数据 */
QTextBrowser *textBrowser;
/* 用作发送数据 */
QLineEdit *lineEdit;
/* 按钮 */
QPushButton *pushButton[2];
/* 下拉选择盒子 */
QComboBox *comboBox[3];
/* 标签 */
QLabel *label[4];
/* 垂直布局 */
QVBoxLayout *vboxLayout;
/* 网络布局 */
QGridLayout *gridLayout;
/* 主布局 */
QWidget *mainWidget;
/* 设置功能区域 */
QWidget *funcWidget;
// 设置控件的字符编码
QTextCodec *codec;
private:
/* 布局初始化 */
void layoutInit();
/* 插件类型项初始化 */
void pluginItemInit();
/* 比特率项初始化 */
void bitrateItemInit();
private slots:
/* 发送消息 */
void sendFrame();
/* 接收消息 */
void receivedFrames();
/* 插件发生改变 */
void pluginChanged(int);
/* 处理 can 错误 */
void canDeviceErrors(QCanBusDevice::CanBusError) const;
/* 连接或者断开 can */
void connectDevice();
};
#endif // MAINWINDOW_H
源文件.cpp:
c++
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include<QScreen> // 屏幕
#include<QGuiApplication> // 纯代码实现界面需要添加
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
codec = QTextCodec::codecForName("UTF-8");
/* 使用系统指令比特率初始化 CAN,默认为 1000000bits/s */
::system("ifconfig can0 down");
::system("ip link set up can0 type can bitrate 1000000 restart-ms 100");
/* 布局初始化 */
layoutInit();
/* 可用插件初始化 */
pluginItemInit();
/* 可用接口项初始化 */
pluginChanged(comboBox[0]->currentIndex());
/* 比特率项初始化 */
bitrateItemInit();
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::layoutInit()
{
/* 获取屏幕的分辨率,Qt 官方建议使用这
99 * 种方法获取屏幕分辨率,防上多屏设备导致对应不上
100 * 注意,这是获取整个桌面系统的分辨率
101 */
QList <QScreen *> list_screen = QGuiApplication::screens();
/* 如果是 ARM 平台,直接设置大小为屏幕的大小 */
#if __arm__
/* 重设大小 */
this->resize(list_screen.at(0)->geometry().width(),list_screen.at(0)->geometry().height());
#else
/* 否则则设置主窗体大小为 800x480 */
this->resize(800, 480);
#endif
/* 对象初始化 */
textBrowser = new QTextBrowser();
lineEdit = new QLineEdit();
vboxLayout = new QVBoxLayout();
funcWidget = new QWidget();
mainWidget = new QWidget();
gridLayout = new QGridLayout();
/* QList 链表,字符串类型 */
QList <QString> list1;
list1<<"Plugin Type:"<<"Available interfaces:"<<"bitrate bits/sec:";
for (int i = 0; i < 3; i++) {
label[i] = new QLabel(list1[i]);
/* 设置最小宽度与高度 */
label[i]->setMinimumSize(120, 30);
label[i]->setMaximumHeight(50);
/* 自动调整 label 的大小 */
label[i]->setSizePolicy(QSizePolicy::Expanding,
QSizePolicy::Expanding);
/* 将 label[i]添加至网格的坐标(0, i) */
gridLayout->addWidget(label[i], 0, i);
}
label[3] = new QLabel();
label[3]->setMaximumHeight(30);
for (int i = 0; i < 3; i++) {
comboBox[i] = new QComboBox();
comboBox[i]->setMinimumSize(120, 30);
comboBox[i]->setMaximumHeight(50);
/* 自动调整 label 的大小 */
comboBox[i]->setSizePolicy(QSizePolicy::Expanding,
QSizePolicy::Expanding);
/* 将 comboBox[i]添加至网格的坐标(1, i) */
gridLayout->addWidget(comboBox[i], 1, i);
}
/* QList 链表,字符串类型 */
QList <QString> list2;
list2<<"send"<<"connect CAN";
for (int i = 0; i < 2; i++) {
pushButton[i] = new QPushButton(list2[i]);
pushButton[i]->setMinimumSize(120, 30);
pushButton[i]->setMaximumHeight(50);
/* 自动调整 label 的大小 */
pushButton[i]->setSizePolicy(QSizePolicy::Expanding,
QSizePolicy::Expanding);
/* 将 pushButton[0]添加至网格的坐标(i, 3) */
gridLayout->addWidget(pushButton[i], i, 3);
}
pushButton[0]->setEnabled(false);
/* 垂直布局 */
vboxLayout->addWidget(textBrowser);
vboxLayout->addWidget(lineEdit);
funcWidget->setLayout(gridLayout);
vboxLayout->addWidget(funcWidget);
vboxLayout->addWidget(label[3]);
mainWidget->setLayout(vboxLayout);
this->setCentralWidget(mainWidget);
/* 设置文本 */
textBrowser->setPlaceholderText("System time frame ID length data");
lineEdit->setText("123 aa 77 66 55 44 33 22 11");
label[3]->setText(tr("unconnected"));
connect(pushButton[1], SIGNAL(clicked()),this, SLOT(connectDevice()));
connect(pushButton[0], SIGNAL(clicked()),this, SLOT(sendFrame()));
}
/* 从系统中读取可用的插件,并显示到 comboBox[0] */
void MainWindow::pluginItemInit()
{
comboBox[0]->addItems(QCanBus::instance()->plugins());
for (int i = 0; i < QCanBus::instance()->plugins().count(); i++) {
if (QCanBus::instance()->plugins().at(i) == "socketcan")
comboBox[0]->setCurrentIndex(i);
}
connect(comboBox[0], SIGNAL(currentIndexChanged(int)),this, SLOT(pluginChanged(int)));
}
void MainWindow::bitrateItemInit()
{
const QList<int> rates = {
10000, 20000, 50000, 100000, 125000,
250000, 500000, 800000, 1000000
};
for (int rate : rates)
comboBox[2]->addItem(QString::number(rate), rate);
/* 默认初始化以 1000000 比特率 */
comboBox[2]->setCurrentIndex(8);
}
// 静态函数可以不在头文件声明 只在当前源文件使用(QCanBusFrame是一个容器类,表示单个CAN帧)
static QString frameFlags(const QCanBusFrame &frame)
{
/* 格式化接收到的消息 */
QString result = QLatin1String(" --- ");
// 如果CAN使用带比特率开关的灵活数据速率以更高的数据比特率传输有效负载数据,则返回true。
if (frame.hasBitrateSwitch())
result[1] = QLatin1Char('B');
// 如果CAN使用设置有错误状态指示器的灵活数据速率,则返回true。
// 此标志由变送器的CAN FD硬件设置,用于指示变送器的错误状态。
if (frame.hasErrorStateIndicator())
result[2] = QLatin1Char('E');
// 如果帧是本地回波帧,即当具有相同内容的帧成功发送到CAN总线时,作为回波接收的帧,则返回true。
if (frame.hasLocalEcho())
result[3] = QLatin1Char('L');
return result;
}
// 槽函数
void MainWindow::sendFrame()
{
if (!canDevice)
return;
/* 读取 QLineEdit 的文件 */
QString str = lineEdit->text();
QByteArray data = 0;
QString strTemp = nullptr;
/* 以空格分隔 lineEdit 的内容,并存储到字符串链表中 */
QStringList strlist = str.split(' ');
for (int i = 1; i < strlist.count(); i++) {
strTemp = strTemp + strlist[i];
}
/* 将字符串的内容转为 QByteArray 类型 */
data = QByteArray::fromHex(strTemp.toLatin1());
bool ok;
/* 以 16 进制读取要发送的帧内容里第一个数据,并作为帧 ID */
int framId = strlist[0].toInt(&ok, 16);
QCanBusFrame frame = QCanBusFrame(framId, data);
/* 写入帧 */
canDevice->writeFrame(frame);
}
void MainWindow::receivedFrames()
{
if (!canDevice)
return;
/* 读取帧 */
while (canDevice->framesAvailable()) {
const QCanBusFrame frame = canDevice->readFrame();
QString view;
if (frame.frameType() == QCanBusFrame::ErrorFrame)
view = canDevice->interpretErrorFrame(frame);
else
view = frame.toString();
const QString time = QString::fromLatin1("%1.%2 ")
.arg(frame.timeStamp()
.seconds(), 10, 10, QLatin1Char(' '))
.arg(frame.timeStamp()
.microSeconds() / 100, 4, 10, QLatin1Char('0'));
const QString flags = frameFlags(frame);
/* 接收消息框追加接收到的消息 */
textBrowser->insertPlainText(time + flags + view + "\n");
}
}
/* 插件类型改变 */
void MainWindow::pluginChanged(int)
{
QList<QCanBusDeviceInfo> interfaces;
comboBox[1]->clear();
/* 当我们改变插件时,我们同时需要将可用接口,从插件类型中读取出来 */
interfaces = QCanBus::instance()->availableDevices(comboBox[0]->currentText());
for (const QCanBusDeviceInfo &info : qAsConst(interfaces)) {
comboBox[1]->addItem(info.name());
}
}
/* 初始化一些常用的比特率,can 的比特率不是随便设置的,有相应的计算公式 */
void MainWindow::canDeviceErrors(QCanBusDevice::CanBusError error) const
{
/* 错误处理 */
switch (error) {
case QCanBusDevice::ReadError:
case QCanBusDevice::WriteError:
case QCanBusDevice::ConnectionError:
case QCanBusDevice::ConfigurationError:
case QCanBusDevice::UnknownError:
label[3]->setText(canDevice->errorString());
break;
default:
break;
}
}
/* 连接或断开 CAN */
void MainWindow::connectDevice()
{
if (pushButton[1]->text() == "connect CAN")
{
/* Qt 中的 QCanBusDevice::BitRateKey 不能设置比特率 */
QString cmd1 = tr("ifconfig %1 down").arg(comboBox[1]->currentText());
QString cmd2 = tr("ip link set up %1 type can bitrate %2 restart-ms 100").arg(comboBox[1]->currentText()).arg(comboBox[2]->currentText());
/* 使用系统指令以设置的比特率初始化 CAN */
system(cmd1.toStdString().c_str());
system(cmd2.toStdString().c_str());
QString errorString;
/* 以设置的插件名与接口实例化 canDevice */
canDevice = QCanBus::instance()->createDevice(comboBox[0]->currentText(),comboBox[1]->currentText(),&errorString);
if (!canDevice) {
label[3]->setText(
tr("Error creating device '%1', reason: '%2'")
.arg(comboBox[0]->currentText())
.arg(errorString));
return;
}
/* 连接 CAN */
if (!canDevice->connectDevice()) {
label[3]->setText(tr("Connection error: %1")
.arg(canDevice->errorString()));
delete canDevice;
canDevice = nullptr;
return;
}
connect(canDevice, SIGNAL(framesReceived()),this, SLOT(receivedFrames()));
connect(canDevice,
SIGNAL(errorOccurred(QCanBusDevice::CanBusError)),this,SLOT(canDeviceErrors(QCanBusDevice::CanBusError)));
/* 将连接信息插入到 label */
label[3]->setText(
tr("The plugin type is: %1, Connected to %2, The bit rate is %3 kBit/s")
.arg(comboBox[0]->currentText())
.arg(comboBox[1]->currentText())
.arg(comboBox[2]->currentText().toInt() / 1000));
pushButton[1]->setText("disconnect CAN");
/* 使能/失能 */
pushButton[0]->setEnabled(true);
comboBox[0]->setEnabled(false);
comboBox[1]->setEnabled(false);
comboBox[2]->setEnabled(false);
}
else {
if (!canDevice)
return;
/* 断开连接 */
canDevice->disconnectDevice();
delete canDevice;
canDevice = nullptr;
pushButton[1]->setText("connect CAN");
pushButton[0]->setEnabled(false);
label[3]->setText(tr("unconnect"));
comboBox[0]->setEnabled(true);
comboBox[1]->setEnabled(true);
comboBox[2]->setEnabled(true);
}
}
程序运行效果
条件不允许我展示给你们看了,sorry
在 Ubuntu 上运行 界面效果如下,因为 Ubutnu 没有 CAN 设备,所以在可用接口处是不可选的。请把程序交叉编译到开发板上运行。与 CAN 仪器以相同的比特率通信,插件类型默认是(必须是)socketcan,可用接口为 can0,即可发送消息与接收消息。
下图最上面的是接收消息框,"123 aa 77 66 55 44 33 22 11"这个是需要发送的帧,"123"为帧 ID,后面的为 8 个字节数据,每个字节需要以空格隔开。点击连接后,发送按钮才能使用。
==END==