简介:这是一套基于 Arm 与 Qt 的智能车载系统完整源码,面向嵌入式开发者和 Qt 应用工程师,可用于学习车载端功能集成与界面开发。资源共 115 个文件,压缩包约 26.54MB,涵盖 C++/C 源文件、Qt 的 .ui 界面文件、qrc 资源文件、多语言翻译用的 ts/qm 文件,以及 png/jpg 图片、音频素材和驱动相关代码,整体目录模块化,便于按功能查阅和二次开发。功能方面已覆盖天气预报、音乐播放器、视频播放器、倒车雷达、行车记录仪、多语言切换等常见车载场景,底层涉及 v4l2 摄像头采集、超声波雷达驱动、按键驱动等接口,适合在嵌入式 Linux 环境配合 Arm 板卡运行。已有 468 人学习下载,代码中包含汽车主控、音乐播放、背光调节、语言切换等模块的完整实现,能帮助读者快速理解车载系统的任务划分与 Qt 界面逻辑,也可作为毕业设计或车载项目基础框架直接使用。
1. 用C++在Arm上组装一套Qt智能车载系统,先迈过哪道坎
在Arm架构上落地一套Qt智能车载系统,工程量比“写个界面”大得多。C++负责把总线协议、状态机和内存边界约束好,Qt用信号槽与事件循环把硬件事件翻译成界面刷新,Arm板卡则以车规级的功耗和成本兜住整台设备的算力。很多团队从拿到源码压缩包到第一个画面点亮,最常卡住的并非业务逻辑,而是交叉编译链、QPA插件路径和Qt模块裁剪这些环境问题。这篇博文按开发顺序展开:先搭Arm上的Qt交叉编译环境,再谈C++服务层与Qt信号槽的协作方式,接着对比QWidget与QML在车机HMI里的取舍,最后落到性能调优和板级验收。内容面向嵌入式C++开发者,也适合准备这类岗位面试时梳理技术主干。
2. 搭建Arm目标板的Qt交叉编译工具链:架构、源码与QPA插件的匹配
2.1 先确认Arm架构与ABI,再选交叉工具链
拿到一块Arm板卡,不要急着下载Qt源码。第一步是确认目标平台的CPU架构和根文件系统的C库版本,这两个信息决定后续所有编译参数。命令很简单:
uname -m # 输出 aarch64 或 armv7l ldd --version | head -1 # 查看glibc版本当前主流车规级SoC,例如Cortex-A53、Cortex-A55、Cortex-A72,都运行在aarch64模式下。与之对应的交叉编译器前缀是aarch64-linux-gnu-,Qt源码里对应的平台描述文件是linux-aarch64-gnu-g++。如果是较老的armv7 32位平台,则用arm-linux-gnueabihf-前缀和linux-arm-gnueabi-g++这一套。两套平台文件的差别集中在编译器前缀和浮点ABI配置上,硬浮点工具链编译出的程序不能与软浮点库混用,否则运行时会直接报非法指令。
第二件要核对的事是glibc版本。交叉工具链内部链接的glibc版本如果高于目标板根文件系统的版本,程序运行时会报出形如GLIBC_2.34 not found的错误。最稳妥的方式是直接使用厂商BSP自带的工具链,因为它与根文件系统是同源构建的;如果自己安装,尽量选择与板卡所载Linux发行版发布时间接近的工具链。
| 目标架构 | 编译器前缀 | Qt平台文件 | 说明 |
|---|---|---|---|
| armv7 32位 | arm-linux-gnueabihf- | linux-arm-gnueabi-g++ | 硬浮点ABI,适合Cortex-A7/A9 |
| aarch64 64位 | aarch64-linux-gnu- | linux-aarch64-gnu-g++ | 主流车机平台,Cortex-A53/A72 |
提示:即使目标板是64位CPU,也先查一下根文件系统到底是32位还是64位。部分低成本方案会用64位芯片配32位用户态,这种情况下工具链选择反而要回到armv7方案。
2.2 Qt源码交叉编译的最小命令序列
以Qt 5.15.2为例,先安装交叉编译工具链和构建工具。Qt 5.15在嵌入式项目里使用非常广泛,6.x新项目可参考同一套流程,但configure选项和依赖检查会有所不同。
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu \ make cmake ninja-build pkg-config随后准备sysroot目录。所谓sysroot,就是目标板根文件系统的一份拷贝,Qt的configure阶段需要读取它里面的头文件和库文件来判断平台特性。常见做法是通过rsync把运行中的板卡根文件系统同步到开发机:
rsync -avz root@192.168.1.100:/ \ /home/dev/sysroot/ \ --exclude /proc --exclude /sys --exclude /dev --exclude /tmp下载Qt源码并配置。这一条配置命令值得逐项说明:
wget https://download.qt.io/archive/qt/5.15/5.15.2/single/qt-everywhere-opensource-src-5.15.2.tar.xz tar xf qt-everywhere-opensource-src-5.15.2.tar.xz cd qt-everywhere-src-5.15.2 ./configure -release -opensource -confirm-license \ -xplatform linux-aarch64-gnu-g++ \ -sysroot /home/dev/sysroot \ -prefix /usr/local/Qt-5.15.2 \ -nomake examples -nomake tests \ -skip qtwebengine -skip qtwebview -skip qtlocation \ -no-opengl -no-gtk参数含义分四类理解。-xplatform指定Qt使用哪个交叉平台文件,这里选的linux-aarch64-gnu-g++会读入aarch64交叉编译器前缀,并在内部设置目标CPU指令集。-sysroot指向刚才同步的板卡根文件系统,configure阶段的所有头文件和库文件探测都在这个目录里进行,不污染开发机自身环境。-prefix是将来部署到板卡后的安装路径,程序运行时通过这个路径查找Qt库和插件。-skip与-no-opengl则是裁剪动作,车机项目几乎不会用到webengine和webview,去掉之后依赖链和最终产物体积都大幅下降。
配置完成后执行编译和安装:
make -j8 make install产物默认安装到/home/dev/sysroot/usr/local/Qt-5.15.2。裁剪带来的差别非常大:带webengine的Qt全量安装轻松超过1GB,裁剪后核心运行库加plugins通常不到200MB,对eMMC存储受限的板卡影响显著。
2.3 QPA插件路径与触摸事件:板载运行最会卡住的坑
交叉编译产物拷贝到板卡后,第一屏不一定起得来。最常见的报错是:
qt.qpa.plugin: Could not find the Qt platform plugin "linuxfb" in "" This application failed to start because no Qt platform plugin could be initialized.这个报错源于Qt的QPA机制,即Qt Platform Abstraction。Qt把窗口系统差异封装成运行时插件,程序启动时去plugins/platforms目录下寻找对应的库文件。报错原因通常是插件搜索路径不对。对应的环境变量是QT_QPA_PLATFORM_PLUGIN_PATH,这是排查时可最先检查的入口:
export QT_QPA_PLATFORM=linuxfb export QT_QPA_PLATFORM_PLUGIN_PATH=/usr/local/Qt-5.15.2/plugins/platforms ./vehicle_applinuxfb是嵌入式场景最朴素的Qt显示插件,它直接向/dev/fb0这个framebuffer设备写入像素,对GPU没有要求。板卡如果支持DRM/KMS,可以改用eglfs插件获得更好的刷新与合成性能。触摸屏方面,车机常用电容屏通过/dev/input/eventX上报事件,Qt侧需要启用evdevtouch插件:
export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event1这里的event1是触摸屏对应的输入设备节点。问题在于该编号在加装外设后可能变化,稳妥做法是用udev规则按设备属性做软链接,让Qt始终指向同一个稳定路径。这一整套排查逻辑,对所有自行交叉编译的Qt版本都适用,桌面Qt开发经验在这里帮不上太多忙。
3. 用C++和Qt信号槽把SocketCAN数据桥接进车机UI
3.1 解耦硬件与UI:C++接口设计的第一原则
车机软件在结构上一般分成硬件访问层、服务抽象层和界面表现层。C++在中间这层最有价值的地方,是利用抽象基类把硬件差异隐藏起来。仪表系统要读取车速、转速、油量信号,这些信号可能来自CAN总线,也可能来自硬线模拟输入,但UI层不应关心信号来源。
实践中的常见做法是让服务层接口尽量不依赖Qt类型。因为Qt类耦合了事件循环和元对象系统,接口层一旦引入QString和QByteArray,单元测试就不得不同时拉起QCoreApplication。用标准C++类型定义数据结构和边界,再用Qt对象做具体实现,这样测试业务逻辑时可以完全脱离GUI环境。这一点在C++岗位面试里也常被追问,本质考察的是接口与实现分离是否真的落在工程里。
3.2 用SocketCAN和QSocketNotifier把总线帧接进Qt事件循环
SocketCAN是Linux内核自带的CAN协议实现,通过socket接口访问。车机上采集总线数据的一个典型写法是创建CanService类,内部持有CAN套接字文件描述符,并用QSocketNotifier把可读事件挂到Qt事件循环上。头文件定义如下:
#ifndef CAN_SERVICE_H #define CAN_SERVICE_H #include <QObject> #include <QSocketNotifier> #include <QByteArray> class CanService : public QObject { Q_OBJECT public: explicit CanService(QObject *parent = nullptr); ~CanService() override; bool open(const QString &interfaceName); void close(); signals: void frameReceived(quint32 canId, QByteArray payload); void busError(int errorCode); private: int socketFd_ = -1; QSocketNotifier *notifier_ = nullptr; }; #endif实现文件里最关键的是notifier的用法:
#include "can_service.h" #include <cstring> #include <sys/socket.h> #include <sys/ioctl.h> #include <linux/can.h> #include <linux/if.h> #include <unistd.h> CanService::CanService(QObject *parent) : QObject(parent) { } CanService::~CanService() { close(); } bool CanService::open(const QString &interfaceName) { socketFd_ = ::socket(PF_CAN, SOCK_RAW, CAN_RAW); if (socketFd_ < 0) return false; struct ifreq ifr {}; std::strncpy(ifr.ifr_name, interfaceName.toLatin1().data(), IFNAMSIZ - 1); if (::ioctl(socketFd_, SIOCGIFINDEX, &ifr) < 0) { ::close(socketFd_); return false; } struct sockaddr_can addr {}; addr.can_family = AF_CAN; addr.can_ifindex = ifr.ifr_ifindex; if (::bind(socketFd_, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)) < 0) { ::close(socketFd_); return false; } // 关键:把CAN套接字的可读事件交给Qt事件循环 notifier_ = new QSocketNotifier(socketFd_, QSocketNotifier::Read, this); connect(notifier_, &QSocketNotifier::activated, this, [this](int) { struct can_frame frame; int n = ::read(socketFd_, &frame, sizeof(frame)); if (n < 0) { emit busError(errno); return; } if (n != static_cast<int>(sizeof(can_frame))) return; QByteArray payload(reinterpret_cast<const char *>(frame.data), frame.can_dlc); emit frameReceived(frame.can_id, payload); }); return true; } void CanService::close() { if (notifier_) { delete notifier_; notifier_ = nullptr; } if (socketFd_ >= 0) { ::close(socketFd_); socketFd_ = -1; } }这段实现规避了一个常见问题:没有必要为CAN开启一个阻塞读线程。把套接字fd交给QSocketNotifier后,帧到达时会在Qt主事件循环里触发读取,避免了自己维护线程同步和锁的麻烦。注意can_frame是传统CAN的帧结构,CAN FD需要改用canfd_frame并确认驱动支持,两种帧的read长度校验逻辑不能混用。can_id过滤也应该在socket层通过setsockopt配置,而不是在Qt层逐帧丢弃,否则总线负载高时会白白占用主循环时间。
3.3 跨线程信号槽:Direct与Queued连接的车机场景
Qt界面更新的硬约束是:所有widget操作必须在GUI线程完成。CanService如果通过moveToThread移到工作线程,信号与槽的连接方式就需要显式思考。
CanService canSvc; DashboardWidget dashboard; connect(&canSvc, &CanService::frameReceived, &dashboard, &DashboardWidget::onFrame, Qt::QueuedConnection);Qt::QueuedConnection保证槽函数在接收者所属线程的事件循环中执行,而不是在发送者线程里即时调用。Qt默认的AutoConnection会根据两个对象实际所属线程自动做出选择,在多数场景可用,但涉及跨线程时显式写出QueuedConnection能让代码意图清晰得多。下面这个表格是车机项目里最常见的三种线程布局:
| 线程模型 | 实现方式 | 适合场景 |
|---|---|---|
| 单线程+事件驱动 | QSocketNotifier监听CAN/串口fd | 总线帧率不高的仪表项目 |
| 对象迁移 | moveToThread + QueuedConnection | 音视频解码、持久化存储 |
| 独立进程 | QProcess或D-Bus通信 | 功能域隔离、故障隔离要求高 |
在使用原生的std::thread时有一个容易踩的坑:工作线程里直接发信号给GUI对象,若连接方式是Direct,槽函数会在线程上下文中操作widget,轻则刷新闪烁,重则崩溃。记住Qt的信号槽机制本身是线程安全的,但搭配何种连接类型需要按线程亲和性显式选择,这是车机系统里C++与Qt协作的核心问题。
4. HMI实现:在Arm上选QWidget还是QML,以及仪表盘落地
4.1 QWidget还是Qt Quick:按Arm算力和动画复杂度选型
车机的HMI层选型,本质上是一次算力与开发效率的权衡。QWidget走CPU直接绘制,控件体系成熟,内存开销可控,适合设置页、空调控制这类简单交互。Qt Quick/QML走场景图渲染,动画描述能力强,但对GPU或软件渲染性能的要求更高。历史项目中还有一个中间选项QGraphicsView,它把图元组织成场景,适合转速表、油表这类需要自定义绘制的仪表,但新增动画效果时的开发成本不低。
| 对比维度 | QWidget | Qt Quick/QML | QGraphicsView |
|---|---|---|---|
| 渲染模型 | CPU直接绘制 | 场景图+GPU合成 | 场景图+CPU缓存 |
| 硬件要求 | 极低 | 中高,依赖OpenGL ES | 低 |
| 动画能力 | 一般 | 强 | 中等 |
| 开发迭代 | 纯C++ | QML+JS快速迭代 | 纯C++ |
| 典型位置 | 设置页/简单弹窗 | 中控娱乐/复杂仪表动效 | 传统仪表项目 |
经验上,主频低于1GHz且没有GPU的板卡,QML的复杂动画反而比QGraphicsView更吃力,因为场景图本身有额外的合成开销。反过来,带GPU的Cortex-A53以上平台,QML能获得明显流畅的动效,这也是目前新项目中控方案的主流。
4.2 用QML画一个实时刷新的转速表表盘
车速表适合用QML的Canvas元素实现。Canvas在嵌入式Qt Quick里既有软件渲染也有OpenGL路径,对Arm平台的适配较好。下面是一个建立基础表盘的QML代码:
import QtQuick 2.15 import QtQuick.Controls 2.15 Item { id: speedGauge width: 320 height: 320 property real speedValue: 0.0 Canvas { anchors.fill: parent onPaint: { var ctx = getContext("2d"); ctx.reset(); // 表盘背景圆环 ctx.beginPath(); ctx.arc(160, 160, 150, 0, Math.PI * 2); ctx.lineWidth = 6; ctx.strokeStyle = "#222222"; ctx.stroke(); // 刻度线,每30度画一条 for (var i = 0; i < 12; i++) { var angle = i * Math.PI / 6; ctx.beginPath(); ctx.moveTo(160 + Math.sin(angle) * 130, 160 - Math.cos(angle) * 130); ctx.lineTo(160 + Math.sin(angle) * 150, 160 - Math.cos(angle) * 150); ctx.stroke(); } // 速度指针,由 speedValue 驱动 var pointerAngle = (speedValue / 240.0) * Math.PI * 2; ctx.save(); ctx.translate(160, 160); ctx.rotate(pointerAngle); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, -120); ctx.lineWidth = 4; ctx.strokeStyle = "#E53935"; ctx.stroke(); ctx.restore(); } // 值变化时触发重绘 onSpeedValueChanged: requestPaint() } Text { anchors.centerIn: parent text: parseInt(speedGauge.speedValue) + " km/h" font.pixelSize: 42 color: "#FFFFFF" } }onSpeedValueChanged: requestPaint()是这段代码的核心关联逻辑。QML属性一改变,Canvas立即重绘指针,不需要C++侧主动调用任何刷新接口。C++侧通过上下文属性把实时数据注入QML,是车机项目里最常见的桥接方式:
#include <QGuiApplication> #include <QQmlApplicationEngine> #include <QQmlContext> #include "vehicledatamodel.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine engine; VehicleDataModel model; engine.rootContext()->setContextProperty("vehicle", &model); engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); return app.exec(); }上下文属性的含义是:把C++对象model暴露给QML侧的全局名字vehicle。QML里可以直接绑定vehicle.speedValue,数据更新时界面自动刷新。数据量大时,更推荐把C++对象注册为QML类型而非上下文属性,性能更好,但需要额外处理类型注册表的初始化。
4.3 中文字体与Qt国际化的双保险
车载系统几乎都要求多语言切换,Qt国际化(qt国际化)的标准流程是:源码里所有用户可见字符串包在tr()内,lupdate工具扫描生成ts文本,翻译完成后lrelease编译为qm二进制文件。程序启动时加载对应语言的qm:
QTranslator translator; if (translator.load(QLocale::system(), "vehicle", "_", ":/i18n")) app.installTranslator(&translator);这里用QLocale::system()读取系统语言区域,再拼接出类似vehicle_zh_CN.qm的文件名。这个机制本身很成熟,真正的坑却在字体上。简体中文、繁体中文、日文所需的字体文件体积动辄几十MB,直接在Arm板卡上加载全部字库会显著抬高常驻内存。常见做法是用pyftsubset按产品实际使用到的字符生成子集字体,再在main函数里动态注册:
const int id = QFontDatabase::addApplicationFont( QStringLiteral("/usr/share/fonts/NotoSansCJKsc-Regular.otf")); if (id >= 0) { const QString family = QFontDatabase::applicationFontFamilies(id).at(0); QApplication::setFont(QFont(family, 12)); }addApplicationFont从外部路径加载字体文件并返回字体族ID,setFont把它设为全局默认。需要注意字体文件的路径必须提前通过文件系统验证,否则Qt会静默回退到无字库状态,界面出现方框而不是报错,这种问题在板级测试时很难一眼发现。
5. C++内存与启动性能:把车载Qt应用调到稳定跑满帧
5.1 生命周期与内存泄漏:父子机制容易被误用的两个场景
QObject的父子机制能省去大量手工释放,但有两个场景会悄悄泄漏内存。第一种是使用new创建临时对象却不指定parent,局部作用域结束后对象没有任何管理者;第二种是在循环里反复创建网络reply、定时器等短生命周期对象,虽然设置了父对象,但父对象生命周期极长,子对象持续累积,内存只升不降。这台设备连续运行72小时以上的车机,第二种问题尤其致命。
典型的修复方式是deleteLater:
QNetworkReply *reply = manager->get(request); connect(reply, &QNetworkReply::finished, this, [reply]() { QByteArray body = reply->readAll(); // 处理响应数据 reply->deleteLater(); });deleteLater()并不是立刻释放内存,而是把删除动作排进所依附对象的当前事件循环。等到控制点回到事件循环后,对象才会被析构。之所以不直接delete,是为了防止在网络栈内部还在处理信号时把对象从底层夺走。跨线程场景下delete还会造成悬空指针。小块内存多得足以说明:车机里尽量不要手动delete一个还在事件队列里的对象。
在板卡上验证内存是否泄漏,一条简单的采样循环就够了:
while true; do date; cat /proc/$(pidof vehicle_app)/status | grep VmRSS; sleep 10; doneVmRSS是当前常驻物理内存,若它每隔一段固定时间稳定上升而不回落,基本可以断定存在泄漏。进一步的定位可以用valgrind的massif工具,但它在Arm上的运行速度非常慢,更实际的做法是审查connect配对和QObject parent。
5.2 启动流程优化:模块裁剪、预加载与延迟初始化
车载系统的启动时间一般要求从上电到首页首帧在5秒内,其中Linux内核和init占掉一半以上,留给Qt程序的预算并不多。Qt程序启动阶段拆开来看,依次是动态库加载、QPA平台初始化、业务初始化三个环节。
| 启动阶段 | 常见耗时点 | 优化手段 |
|---|---|---|
| 动态库加载 | 依赖模块过多、未裁剪Qt | 编译期裁剪,预加载常用so |
| QPA初始化 | framebuffer/DRM设备打开缓慢 | 固定设备节点,避免轮询 |
| 业务初始化 | 同步等待数据库/网络超时 | 延迟初始化,异步加载 |
应用层最容易见效的是延迟初始化。不要在main函数里一口气建立数据库连接、扫描外设和预加载媒体库,把非核心工作放到窗口首次绘制完成后:
void MainWindow::showEvent(QShowEvent *event) { QMainWindow::showEvent(event); if (!lazyInitialized_) { lazyInitialized_ = true; QTimer::singleShot(0, this, [this]() { // 主界面已经可见,再做耗时初始化 audioManager_->init(); networkService_->connectToServer(); }); } }QTimer::singleShot(0, ...)把任务排到当前事件循环末尾,不会阻塞首帧渲染。更彻底的做法是把这些初始化任务放入独立的Qt工作线程,用moveToThread加QueuedConnection与主线程通信。系统层面还可以用strace -c -f ./vehicle_app查看每个系统调用的耗时分布,进一步定位启动路径上的瓶颈。
5.3 性能监控与帧率测量:Arm板上的实用手段
在开发板上做性能剖析,工具的可获得性比理论指标更重要。perf工具在部分精简内核上不可用,pidstat则相对通用。以帧率为核心指标的做法是:在Qt绘制路径的关键位置用QElapsedTimer记录耗时时长:
QElapsedTimer frameTimer; frameTimer.start(); // 每帧渲染完成后的位置 const qreal elapsedMs = frameTimer.nsecsElapsed() / 1e6; const qreal fps = 1000.0 / elapsedMs; frameTimer.restart(); if (fps < 30.0) { qWarning() << "落帧:" << fps << "fps"; }这段代码有几个细节值得注意。计时器必须在帧的末尾调用restart,而不能在循环开头,否则把所有非渲染时间也统计进去。QML场景下可以在QQuickItem的afterSynchronizing信号里采样,QWidget场景则是在paintEvent末尾记录两次的时间间隔。低于30fps时先检查QPA插件的渲染路径:若板卡无GPU却走了OpenGL软件模拟,性能会明显劣于直接使用linuxfb或eglfs。
6. 板级验收:Qt运行库部署、依赖核验与崩溃定位技巧
6.1 部署运行库的最小目录布局
将Qt编译产物部署到目标板,不需要把整个Qt目录都拷贝过去。常见做法是只拷贝运行所需的lib、plugins和qml目录,最终目录结构如下:
/opt/vehicle/ ├── lib/ # 依赖的.so文件 ├── plugins/ │ ├── platforms/ # libqlinuxfb.so 或 libqeglfs.so │ ├── imageformats/ # libqjpeg.so、libqpng.so │ └── iconengines/ ├── qml/ # QML模块的二进制和qml文件 └── bin/ └── vehicle_app部署时建议保持Qt安装目录的内部相对结构,因为Qt在运行时按相对路径查找插件,破坏目录关系会导致插件加载失败。
6.2 依赖核验与缺失补漏
到达板端的第一件事是在目标板上执行ldd检查动态库依赖:
cd /opt/vehicle/bin LD_LIBRARY_PATH=/opt/vehicle/lib:/usr/local/Qt-5.15.2/lib \ ldd ./vehicle_app | grep "not found"grep结果为空代表依赖齐全。若出现not found,优先检查LD_LIBRARY_PATH是否覆盖了对应so所在的目录,其次检查目标板根文件系统是否缺少基础库。板端执行ldconfig -p可以查看当前动态库缓存,确认哪些路径已经被系统收录。
6.3 验收冒烟测试清单与崩溃定位
车载系统验收阶段需要一个固定场景的回归清单,按经验整理如下:
| 序号 | 测试项 | 预期结果 |
|---|---|---|
| 1 | 上电到首页首帧 | 5秒内 |
| 2 | 连续运行72小时 | 无崩溃,VmRSS增幅小于5% |
| 3 | CAN模拟器持续发送100帧/s | 界面无冻结、无持续掉帧 |
| 4 | 中英日三语切换 | 无方框、无文字重叠 |
| 5 | 待机唤醒循环200次 | 每次都能正常恢复显示 |
最后一类技巧是启用core dump来定位崩溃。板卡Linux默认可能关闭了core文件,需要临时打开:
ulimit -c unlimited gdb ./vehicle_app core进入gdb后执行bt查看调用栈,能直接看到崩溃发生时的函数调用链。相比在代码里到处插日志,这种方法在Arm板上的定位效率高得多,这也是从开发环境走向板级验证时最值得养成的调试习惯。
本文还有配套的精品资源,点击获取