CAN Bus
从 Qt5.8 开始,提供了 CAN Bus 类,很庆幸,正点原子的 I.MX6U 出厂系统里 Qt 版本是
QT5.12.9。我们可以直接使用Qt的提供的CAN相关类编程即可。假设您的Qt版本没有CAN Bus,
可以参考 Linux 应用编程来操控开发板的 CAN,目前我们主要讲解 Qt 相关的 CAN 编程。其实
Qt 也提供了相关的 Qt CAN 的例子,我们也可以直接参考来编程。笔者根据实际情况,化繁为
易,直接写了个简单的例子给大家参考。最重要的一点,读者应会使用 CAN 相关测试工具,
我们应该提前去熟悉,并且读者手上需要有测试 CAN 的仪器!否则写好程序,却无法测试,
这就有些尴尬了。
资源简介
正点原子 I.MX6U 开发板底板上预留了一路 CAN 接口(6U 芯片最大支持两路)。如下图。
在正点原子【正点原子】I.MX6U 用户快速体验 V1.x.pdf 里也有相关的 CAN 测试方法。这里就
不多介绍 CAN 了,笔者默认读者是会使用 CAN 的。同时不对 CAN 总线协议进行讲解,主要
是讲解如何在 Qt 里对 CAN 编程。
应用实例
项目简介:本例适用于正点原子 I.MX6U 开发板。不适用于 Windows。因为 Windows 没有
CAN 设备。虽然 Windows 可以外接 USB CAN 模块,但是这些模块都是某些厂商开发的,需要
有相应的固件才能驱动 CAN 设备。所以编写的例子不一定适用于 Windows 下的 CAN。笔者写
的例子已经在正点原子 I.MX6U 开发板上验证了,确保正常使用!
在正点原子 I.MX6U 板上,需要使用 CAN 必须初始化 CAN。它的开启与关闭都是由系统
完成。I.MX6U 为普通 CAN,非 FD CAN,最大比特率为 1000kBit/s。
在系统执行要在 100 毫秒后自动从“总线关闭”错误中恢复,并以比特率 1000000,可以
使用以下命令开启 CAN。
ip link set up can0 type can bitrate 1000000 restart-ms 100
例 04_socketcan,Qt CAN 编程(难度:较难)。项目路径为 Qt/3/04_ socketcan。
04_socketcan.pro 要想使用 Qt 的 QCanBus,需要在 pro 项目文件里添加相应的模块支持。
同时还需要添加对应的头文件,详细请看项目里的代码。
1 QT += core gui serialbus
2
3 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
4
5 CONFIG += c++11
6
7 # The following define makes your compiler emit warnings if you use
8 # any Qt feature that has been marked deprecated (the exact warnings
9 # depend on your compiler). Please consult the documentation of the
10 # deprecated API in order to know how to port your code away from it.
11 DEFINES += QT_DEPRECATED_WARNINGS
13 # You can also make your code fail to compile if it uses deprecated APIs.
14 # In order to do so, uncomment the following line.
15 # You can also select to disable deprecated APIs only up to a certain
version of Qt.
16 #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the
APIs deprecated before Qt 6.0.0
17
18 SOURCES += \
19 main.cpp \
20 mainwindow.cpp
21
22 HEADERS += \
23 mainwindow.h
24
25 # Default rules for deployment.
26 qnx: target.path = /tmp/$${TARGET}/bin
27 else: unix:!android: target.path = /opt/$${TARGET}/bin
28 !isEmpty(target.path): INSTALLS += target
第 1 行,添加的 serialbus 就是添加串行总线模块的支持。
在头文件“mainwindow.h”的代码如下。一些声明。
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 04_socketcan
* @brief mainwindow.h
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-03-15
*******************************************************************/
1 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include <QMainWindow>
5 #include <QCanBusDevice>
6 #include <QCanBus>
7 #include <QPushButton>
8 #include <QTextBrowser>
9 #include <QLineEdit>
10 #include <QVBoxLayout>
11 #include <QLabel>
12 #include <QComboBox>
13 #include <QGridLayout>
14 #include <QMessageBox>
15 #include <QDebug>
16
17 class MainWindow : public QMainWindow
18 {
19 Q_OBJECT
20
21 public:
22 MainWindow(QWidget *parent = nullptr);
23 ~MainWindow();
24
25 private:
26 /* CAN 设备 */
27 QCanBusDevice *canDevice;
28
29 /* 用作接收数据 */
30 QTextBrowser *textBrowser;
31
32 /* 用作发送数据 */
33 QLineEdit *lineEdit;
34
35 /* 按钮 */
36 QPushButton *pushButton[2];
37
38 /* 下拉选择盒子 */
39 QComboBox *comboBox[3];
40
41 /* 标签 */
42 QLabel *label[4];
43
44 /* 垂直布局 */
45 QVBoxLayout *vboxLayout;
46
47 /* 网络布局 */
48 QGridLayout *gridLayout;
49
50 /* 主布局 */
51 QWidget *mainWidget;
52
53 /* 设置功能区域 */
54 QWidget *funcWidget;
55
56 /* 布局初始化 */
57 void layoutInit();
58
59 /* 插件类型项初始化 */
60 void pluginItemInit();
61
62 /* 比特率项初始化 */
63 void bitrateItemInit();
64
65 private slots:
66 /* 发送消息 */
67 void sendFrame();
68
69 /* 接收消息 */
70 void receivedFrames();
71
72 /* 插件发生改变 */
73 void pluginChanged(int);
74
75 /* 处理 can 错误 */
76 void canDeviceErrors(QCanBusDevice::CanBusError) const;
77
78 /* 连接或者断开 can */
79 void connectDevice();
80 };
81 #endif // MAINWINDOW_H
82
上面代码是在 mianwindow.h 里声明需要用到的变量,方法及槽函数。
mainwindow.cpp 的代码如下。
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 04_socketcan
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-03-15
*******************************************************************/
1 #include "mainwindow.h"
2 #include <QGuiApplication>
3 #include <QScreen>
4
5 MainWindow::MainWindow(QWidget *parent)
6 : QMainWindow(parent)
7 {
8 /* 使用系统指令比特率初始化 CAN,默认为 1000000bits/s */
9 system("ifconfig can0 down");
10 system("ip link set up can0 type can bitrate 1000000 restart-ms 100");
11
12 /* 布局初始化 */
13 layoutInit();
14
15 /* 可用插件初始化 */
16 pluginItemInit();
17
18 /* 可用接口项初始化 */
19 pluginChanged(comboBox[0]->currentIndex());
20
21 /* 比特率项初始化 */
22 bitrateItemInit();
23 }
24
25 MainWindow::~MainWindow()
26 {
27 }
28
29 static QString frameFlags(const QCanBusFrame &frame)
30 {
31 /* 格式化接收到的消息 */
32 QString result = QLatin1String(" --- ");
33
34 if (frame.hasBitrateSwitch())
35 result[1] = QLatin1Char('B');
36 if (frame.hasErrorStateIndicator())
37 result[2] = QLatin1Char('E');
38 if (frame.hasLocalEcho())
39 result[3] = QLatin1Char('L');
40
41 return result;
42 }
43
44 /* 发送消息 */
45 void MainWindow::sendFrame()
46 {
47 if (!canDevice)
48 return;
49 /* 读取 QLineEdit 的文件 */
50 QString str = lineEdit->text();
51 QByteArray data = 0;
52 QString strTemp = nullptr;
53 /* 以空格分隔 lineEdit 的内容,并存储到字符串链表中 */
54 QStringList strlist = str.split(' ');
55 for (int i = 1; i < strlist.count(); i++) {
56 strTemp = strTemp + strlist[i];
57 }
58 /* 将字符串的内容转为 QByteArray 类型 */
59 data = QByteArray::fromHex(strTemp.toLatin1());
60
61 bool ok;
62 /* 以 16 进制读取要发送的帧内容里第一个数据,并作为帧 ID */
63 int framId = strlist[0].toInt(&ok, 16);
64 QCanBusFrame frame = QCanBusFrame(framId, data);
65 /* 写入帧 */
66 canDevice->writeFrame(frame);
67 }
68
69 /* 接收消息 */
70 void MainWindow::receivedFrames()
71 {
72 if (!canDevice)
73 return;
74
75 /* 读取帧 */
76 while (canDevice->framesAvailable()) {
77 const QCanBusFrame frame = canDevice->readFrame();
78 QString view;
79 if (frame.frameType() == QCanBusFrame::ErrorFrame)
80 view = canDevice->interpretErrorFrame(frame);
81 else
82 view = frame.toString();
83
84 const QString time = QString::fromLatin1("%1.%2 ")
85 .arg(frame.timeStamp()
86 .seconds(), 10, 10, QLatin1Char(' '))
87 .arg(frame.timeStamp()
88 .microSeconds() / 100, 4, 10, QLatin1Char('0'));
89
90 const QString flags = frameFlags(frame);
91 /* 接收消息框追加接收到的消息 */
92 textBrowser->insertPlainText(time + flags + view + "\n");
93 }
94 }
95
96 void MainWindow::layoutInit()
97 {
98 /* 获取屏幕的分辨率,Qt 官方建议使用这
99 * 种方法获取屏幕分辨率,防上多屏设备导致对应不上
100 * 注意,这是获取整个桌面系统的分辨率
101 */
102 QList <QScreen *> list_screen = QGuiApplication::screens();
103
104 /* 如果是 ARM 平台,直接设置大小为屏幕的大小 */
105 #if __arm__
106 /* 重设大小 */
107 this->resize(list_screen.at(0)->geometry().width(),
108 list_screen.at(0)->geometry().height());
109 #else
110 /* 否则则设置主窗体大小为 800x480 */
111 this->resize(800, 480);
112 #endif
113 /* 对象初始化 */
114 textBrowser = new QTextBrowser();
115 lineEdit = new QLineEdit();
116 vboxLayout = new QVBoxLayout();
117 funcWidget = new QWidget();
118 mainWidget = new QWidget();
119 gridLayout = new QGridLayout();
120
121 /* QList 链表,字符串类型 */
122 QList <QString> list1;
123 list1<<"插件类型:"<<"可用接口:"<<"比特率 bits/sec:";
124
125 for (int i = 0; i < 3; i++) {
126 label[i] = new QLabel(list1[i]);
127 /* 设置最小宽度与高度 */
128 label[i]->setMinimumSize(120, 30);
129 label[i]->setMaximumHeight(50);
130 /* 自动调整 label 的大小 */
131 label[i]->setSizePolicy(QSizePolicy::Expanding,
132 QSizePolicy::Expanding);
133 /* 将 label[i]添加至网格的坐标(0, i) */
134 gridLayout->addWidget(label[i], 0, i);
135 }
136 label[3] = new QLabel();
137 label[3]->setMaximumHeight(30);
138
139 for (int i = 0; i < 3; i++) {
140 comboBox[i] = new QComboBox();
141 comboBox[i]->setMinimumSize(120, 30);
142 comboBox[i]->setMaximumHeight(50);
143 /* 自动调整 label 的大小 */
144 comboBox[i]->setSizePolicy(QSizePolicy::Expanding,
145 QSizePolicy::Expanding);
146 /* 将 comboBox[i]添加至网格的坐标(1, i) */
147 gridLayout->addWidget(comboBox[i], 1, i);
148 }
149
150 /* QList 链表,字符串类型 */
151 QList <QString> list2;
152 list2<<"发送"<<"连接 CAN";
153
154 for (int i = 0; i < 2; i++) {
155 pushButton[i] = new QPushButton(list2[i]);
156 pushButton[i]->setMinimumSize(120, 30);
157 pushButton[i]->setMaximumHeight(50);
158 /* 自动调整 label 的大小 */
159 pushButton[i]->setSizePolicy(QSizePolicy::Expanding,
160 QSizePolicy::Expanding);
161 /* 将 pushButton[0]添加至网格的坐标(i, 3) */
162 gridLayout->addWidget(pushButton[i], i, 3);
163 }
164 pushButton[0]->setEnabled(false);
165
166 /* 布局 */
167 vboxLayout->addWidget(textBrowser);
168 vboxLayout->addWidget(lineEdit);
169 funcWidget->setLayout(gridLayout);
170 vboxLayout->addWidget(funcWidget);
171 vboxLayout->addWidget(label[3]);
172 mainWidget->setLayout(vboxLayout);
173 this->setCentralWidget(mainWidget);
174
175 /* 设置文本 */
176 textBrowser->setPlaceholderText("系统时间 帧 ID 长度 数据");
177 lineEdit->setText("123 aa 77 66 55 44 33 22 11");
178 label[3]->setText(tr("未连接!"));
179
180 connect(pushButton[1], SIGNAL(clicked()),
181 this, SLOT(connectDevice()));
182 connect(pushButton[0], SIGNAL(clicked()),
183 this, SLOT(sendFrame()));
184 }
185
186 /* 从系统中读取可用的插件,并显示到 comboBox[0] */
187 void MainWindow::pluginItemInit()
188 {
189 comboBox[0]->addItems(QCanBus::instance()->plugins());
190 for (int i = 0; i < QCanBus::instance()->plugins().count(); i++) {
191 if (QCanBus::instance()->plugins().at(i) == "socketcan")
192 comboBox[0]->setCurrentIndex(i);
193 }
194 connect(comboBox[0], SIGNAL(currentIndexChanged(int)),
195 this, SLOT(pluginChanged(int)));
196 }
197
198 /* 插件类型改变 */
199 void MainWindow::pluginChanged(int)
200 {
201 QList<QCanBusDeviceInfo> interfaces;
202 comboBox[1]->clear();
203 /* 当我们改变插件时,我们同时需要将可用接口,从插件类型中读取出来 */
204 interfaces = QCanBus::instance()
205 ->availableDevices(comboBox[0]->currentText());
206 for (const QCanBusDeviceInfo &info : qAsConst(interfaces)) {
207 comboBox[1]->addItem(info.name());
208 }
209 }
210
211 /* 初始化一些常用的比特率,can 的比特率不是随便设置的,有相应的计算公式 */
212 void MainWindow::bitrateItemInit()
213 {
214 const QList<int> rates = {
215 10000, 20000, 50000, 100000, 125000,
216 250000, 500000, 800000, 1000000
217 };
218
219 for (int rate : rates)
220 comboBox[2]->addItem(QString::number(rate), rate);
221
222 /* 默认初始化以 1000000 比特率 */
223 comboBox[2]->setCurrentIndex(8);
224 }
225
226 /* 连接或断开 CAN */
227 void MainWindow::connectDevice()
228 {
229 if (pushButton[1]->text() == "连接 CAN") {
230 /* Qt 中的 QCanBusDevice::BitRateKey 不能设置比特率 */
231 QString cmd1 = tr("ifconfig %1 down")
232 .arg(comboBox[1]->currentText());
233 QString cmd2 =
234 tr("ip link set up %1 type can bitrate %2 restart-ms 100")
235 .arg(comboBox[1]->currentText())
236 .arg(comboBox[2]->currentText());
237 /* 使用系统指令以设置的比特率初始化 CAN */
238 system(cmd1.toStdString().c_str());
239 system(cmd2.toStdString().c_str());
240
241 QString errorString;
242 /* 以设置的插件名与接口实例化 canDevice */
243 canDevice = QCanBus::instance()->
244 createDevice(comboBox[0]->currentText(),
245 comboBox[1]->currentText(),
246 &errorString);
247
248 if (!canDevice) {
249 label[3]->setText(
250 tr("Error creating device '%1', reason: '%2'")
251 .arg(comboBox[0]->currentText())
252 .arg(errorString));
253 return;
254 }
255
256 /* 连接 CAN */
257 if (!canDevice->connectDevice()) {
258 label[3]->setText(tr("Connection error: %1")
259 .arg(canDevice->errorString()));
260 delete canDevice;
261 canDevice = nullptr;
262
263 return;
264 }
265
266 connect(canDevice, SIGNAL(framesReceived()),
267 this, SLOT(receivedFrames()));
268 connect(canDevice,
269 SIGNAL(errorOccurred(QCanBusDevice::CanBusError)),
270 this,
271 SLOT(canDeviceErrors(QCanBusDevice::CanBusError)));
272 /* 将连接信息插入到 label */
273 label[3]->setText(
274 tr("插件类型为: %1, 已连接到 %2, 比特率为 %3 kBit/s")
275 .arg(comboBox[0]->currentText())
276 .arg(comboBox[1]->currentText())
277 .arg(comboBox[2]->currentText().toInt() / 1000));
278 pushButton[1]->setText("断开 CAN");
279 /* 使能/失能 */
280 pushButton[0]->setEnabled(true);
281 comboBox[0]->setEnabled(false);
282 comboBox[1]->setEnabled(false);
283 comboBox[2]->setEnabled(false);
284 } else {
285 if (!canDevice)
286 return;
287
288 /* 断开连接 */
289 canDevice->disconnectDevice();
290 delete canDevice;
291 canDevice = nullptr;
292 pushButton[1]->setText("连接 CAN");
293 pushButton[0]->setEnabled(false);
294 label[3]->setText(tr("未连接!"));
295 comboBox[0]->setEnabled(true);
296 comboBox[1]->setEnabled(true);
297 comboBox[2]->setEnabled(true);
298 }
299 }
300
301 void MainWindow::canDeviceErrors(QCanBusDevice::CanBusError error)
const
302 {
303 /* 错误处理 */
304 switch (error) {
305 case QCanBusDevice::ReadError:
306 case QCanBusDevice::WriteError:
307 case QCanBusDevice::ConnectionError:
308 case QCanBusDevice::ConfigurationError:
309 case QCanBusDevice::UnknownError:
310 label[3]->setText(canDevice->errorString());
311 break;
312 default:
313 break;
314 }
315 }
316
第 9~10 行,使用系统的 CAN 硬件,必须初始化系统的 CAN。在项目里添加相应的开启
CAN 的指令。第一个指令是先关闭本地的 CAN,因为只有关闭 CAN,才能以新的速率再开启。
第 12~22 行,构造函数里界面初始化,以及 QComboBox 里的项初始化。
第 29~42 行,格式化帧处理函数。
第 45~67 行,发送消息,将 lineEdit 的文本进行处理后,第一个作为 CAN 的帧 ID,后面 8
个数据作为需要要发送的数据。每帧只能发送 8 个数据。
第 70~94 行,接收消息,读取帧并格式化处理,显示到 textBrowser 里。
第 96~184 行,界面布局初始化设置,在嵌入式里,根据实际的屏的大小,设置全屏显示。
其中我们用到垂直布局和网格布局,如果布局这方面内容理解不了,请回到第七章 7.5 小节学
习布局内容。
第 187~196 行,可用插件初始化,检查系统 QCanBus 提供的插件。在 Linux 里使用的插件
类型是 SocketCAN, SocketCAN 插件支持 Linux 内核和用于所用 CAN 硬件的 SocketCAN 设备驱
动程序。下面程序遍历可用的 CAN 插件,并设置 socketcan 为当前插件。注意,只能使用
SocketCAN 访问本地硬件 CAN,其他插件是不同类型的 CAN 驱动程序所使用的。请自行测试。
第 199~209 行,当插件类型改变时,我们需要更新可用接口。
第 212~224 行,常用的比特率初始化。
第 227~299 行,连接/断开 CAN,很遗憾 Qt 的 QCanBusDevice::BitRateKey 不能设置比特
率,因为系统的 CAN 需要使用 ip 指令以一个比特率才能进行初始化,Qt 需要系统 CAN 起来
才能进行操作。所以需要使用系统指令设置 CAN。
第 301~315 行,错误处理,CAN 设备可能遇到错误,打印错误的信息。
程序运行效果
在 Ubuntu 上运行 界面效果如下,因为 Ubutnu 没有 CAN 设备,所以在可用接口处是不可
选的。请把程序交叉编译到开发板上运行。与 CAN 仪器以相同的比特率通信,插件类型默认
是(必须是)socketcan,可用接口为 can0,即可发送消息与接收消息。
下图最上面的是接收消息框,“123 aa 77 66 55 44 33 22 11”这个是需要发送的帧,“123”
为帧 ID,后面的为 8 个字节数据,每个字节需要以空格隔开。点击连接后,发送按钮才能使用。