☰
IMX6ULL外接SPI LCD跑QT:从设备树到framebuffer完整指南
2026/10/14 3:30:51 网站建设 项目流程

简介:本资源是面向嵌入式Linux初学者与Qt移植实践者的IMX6ULL平台SPI LCD图形界面开发套件,聚焦解决ARM Cortex-A7平台下ILI9341驱动的TFT屏(MSP2402模组)与Qt应用协同显示的核心问题。压缩包共含多个关键文件,涵盖适配IMX6ULL的设备树源码(.dts)、基于Qt 5.x的轻量级界面源码(.cpp/.h/.pro)、SPI初始化及LCD背光控制脚本,以及编译部署说明文档,整体大小为9.57MB,结构清晰、模块职责明确,便于理解硬件抽象层与GUI框架的衔接逻辑。已有278人学习下载,适合开展嵌入式Qt入门实验、设备树定制训练及SPI外设驱动调试。读者可直接部署运行示例界面,掌握从内核设备树修改、SPI总线配置到Qt渲染后端适配的完整链路,获得可复用的移植方法论与排错参考。 手头这块IMX6ULL开发板吃灰有一段时间了,最近翻出来想给它配一块小尺寸屏幕跑一个简单的QT界面。本来觉得SPI LCD接上去、内核驱动一开、Qt往fb上一指就完事了,结果真正动手才发现链条挺长——SPI设备树、fbtft驱动、framebuffer设备、Qt的linuxfb插件,哪一个环节没对上,屏幕都只能白屏或者黑屏。这篇文章就是完整过程的梳理,包括MSP2402这块ILI9341屏的接线、内核侧点亮到QT界面源码的整个链路,最后还会把打包好的源码工程里有什么、怎么复用讲清楚。

标题里的“源码.rar”我之前传过一份,是给朋友参考用的,里面不只包含QT工程,还有设备树、内核配置片段和部署脚本。如果你正好也是IMX6ULL的板子,想低成本外接一块SPI小屏显示中文状态页,那这份记录应该能帮你省下至少一整个周末的排查时间。

1. 为什么用SPI小屏跑QT:这个需求是怎么被我折腾出来的

1.1 一块吃灰开发板引发的场景选择

IMX6ULL这颗Cortex-A7内核的处理器,在嵌入式Linux圈子里算是非常大众的选择。它的原生产品定位是工业控制、车载终端和物联网网关,性能不算强,但胜在稳定、外设齐全、资料多,而且它内部带了多个ECSPI控制器,数量充足,其中一个拿来接SPI屏幕完全不影响主功能使用。

我手上这块板子主芯片是IMX6ULL,底板已经带了百兆网口、串口和USB,但唯独没有配屏。之前接过的都是RGB并口LCD或者LVDS屏,体积大、线多,而且价格也不便宜。手头正好有一块2.4英寸的SPI接口小屏,型号MSP2402,驱动IC是ILI9341,分辨率240x320,16位色,SPI接口只需要几根信号线就能驱动,非常适合做那种“只显示几个状态数据”的小界面。

有些朋友可能会问:IMX6ULL不是自带LCD控制器吗?为什么不用RGB并口屏?这个问题的答案是“成本和使用场景”。并口RGB屏的像素数据总线至少16根,占用的引脚非常多,而且需要额外的背光控制、行场同步信号,PCB布局和排线都更复杂。SPI屏只需要SCLK、MOSI、CS、DC、RST五根核心信号线,接线简单,软件上通过帧缓冲设备驱动,Qt的linuxfb插件能直接识别,开发效率很高。缺点也很明显:刷新带宽不高,不适合做复杂动画,但做静态菜单、仪表盘、状态页绰绰有余。

1.2 源码包里最终交付了哪些东西

网上下载的很多“IMX6ULL + SPI LCD + QT界面源码”要么只有Qt工程,要么只有设备树,很少有把整条链路串起来的。我整理这份源码的时候,目标就是让一个不太熟悉内核移植的人,拿到手也能按步骤跑起来。

压缩包里主要包含这几块:

  • 设备树DTS源码,包含ECSPI引脚复用、ILI9341节点、背光节点;
  • 内核配置片段,把fbtft和fb_ili9341相关选项列出来;
  • Qt工程源码,一个可运行的简单界面,含中文显示字体配置;
  • 交叉编译后的部署脚本,拷贝二进制、设置环境变量、挂载调试信息;
  • README文档,记录每一步操作和常见问题排查方法。

这个工程不仅仅能在MSP2402上跑,其他基于ILI9341的SPI LCD模块也基本通用,最多调整一下引脚映射和初始化参数。

2. 硬件连接和引脚级确认:MSP2402这块屏不能只看排针丝印

2.1 MSP2402和ILI9341的关系

MSP2402是模块厂商给的型号,本质上是一块2.4英寸TFT屏幕模组,核心是主控ILI9341。这个IC是市场占有率非常高的TFT LCD控制器,240x320分辨率,支持SPI、并行等接口,内置GRAM、电源电路和伽马校正。因为用得人太多,Linux内核里已经有现成的驱动,不管是老的fbtft还是新的DRM tinydrm都能直接支持。

大部分MSP2402模块会引出8个或6个引脚。8针版本一般是“VCC、GND、CS、RESET、DC、MOSI、SCK、LED”,6针版本把CS和DC合到固定电平或者直接省略,剩下“GND、VCC、SCK、SDA、RES、DC”。如果模块上没有单独的CS引脚,通常是把CS接地,表示这个SPI总线上只挂一个设备;如果有CS引脚,我建议一定要接到控制器上,方便做片选管理,后面排查问题也容易定位。

拿到屏幕先不要急着接线,用万用表量一下VCC和GND之间的电阻,再确认模块上有没有板载稳压器。有些模块的VCC脚可以接5V,因为板上带了AMS1117-3.3;有些则只能接3.3V,接5V会烧。MSP2402这个模块我自己测的是可以接3.3V供电,背光灯串一个限流电阻后也能直接用3.3V,但为了保险起见,电源部分我还是走的PMIC的3.3V输出,没有碰5V。

2.2 和IMX6ULL的ECSPI接线

IMX6ULL的SPI控制器叫ECSPI,每个控制器有4个片选信号。我们平常用的功能引脚都通过IOMUX复用,具体选哪个引脚要看你的底板设计。我的板子上ECSPI1引到了扩展排针,引脚定义如下,你可以对照你的板子原理图调整:

LCD模块引脚IMX6ULL侧信号说明
VCC3.3V电源为LCD模块供电
GNDGND必须共地
SCLKECSPI1_SCLKSPI时钟,由主控发出
MOSI / SDAECSPI1_MOSI主机输出,数据线
MISOECSPI1_MISO部分模块有,可用于读寄存器
CSECSPI1_SS0 / GPIO片选信号
RESETGPIO任意复位信号,低电平有效
DC / RSGPIO任意数据/命令选择
LED / BLGPIO或PWM背光使能/亮度控制

如果你的是6针屏,排针上可能没有CS和MISO,这时候CS直接接GND,MISO空着不接。DC和RESET一定不能省,ILI9341通过DC引脚分辨SPI上传的是命令还是像素数据,RESET则用来上电复位。背光引脚最好单独用一个GPIO控制,我默认在设备树里配置成“上电默认点亮”,方便调试。

2.3 电气和信号完整性:串阻、上拉、供电

SPI屏的调试中,有一个经常被忽略的问题:信号线上要不要串电阻。很多人问“SPI时钟线串电阻一般多大”,这个问题没有标准答案,取决于驱动端输出阻抗、PCB走线长度和负载电容。以IMX6ULL的ECSPI引脚为例,输出的边缘速率挺快,如果连接线超过5厘米,且又是杜邦线飞线,就容易产生过冲和回铃,导致SPI时序抖动,表现出来就是屏幕花屏或者偶发白屏。

我测试下来,SCLK和MOSI上各串一个33Ω电阻能明显改善波形,如果线材特别长,可以提高到47Ω。这不是必须的,但如果你用杜邦线飞线而且频率上不去,可以先试试串电阻。CS和DC这两个信号因为是电平控制,不需要串电阻,但建议靠近主控引脚加一个10kΩ上拉,避免上电瞬间悬空导致误操作。

另外要注意“共地”。SPI是同步通信,主从设备之间如果地电位差太大,抗干扰能力会很差,尤其屏幕背光瞬间启动时电流变化很快,地弹会导致SPI数据错误。所以电源线和地线尽量短,避免和背光驱动共用一根细长地线。

2.4 关于片选:硬件片选还是GPIO软件片选

IMX6ULL的ECSPI控制器支持硬件自动片选,也可以在设备树里把CS引脚配置成GPIO,用软件控制片选。两种方式都能用,但效果有区别。硬件片选由控制器在每次传输时自动拉低拉高,时序精准,适合高速传输。软件片选的好处是引脚选择灵活,尤其当你需要把CS放到一个原本不是SPI片选功能的GPIO上时,就会非常方便。

我的源码里用的就是GPIO软件片选,因为板子引出的ECSPI1_SS0同时被其他功能占用了,直接换GPIO最省事。设备树里配置cs-gpios属性就能实现。你可能要问:软件片选会不会慢?实际测试在20MHz SPI频率下,CE电平切换的延迟并不会明显影响帧率,对静态界面来说完全够用。但如果你的屏幕要跑视频或者高频动态刷新,建议还是用硬件片选,时序余量更大。

3. 设备树和内核驱动:从SPI总线上“长出”一个/dev/fb0

3.1 驱动选型:fbtft还是内核DRM

早期Linux内核中,ILI9341这类SPI屏的驱动都收在drivers/staging/fbtft/里,提供的是传统帧缓冲接口,也就是生成/dev/fb0。Qt的老牌插件linuxfb可以直接映射这个帧缓冲,所以fbtft是STM32MP1、IMX6ULL这类入门板子上最常用的方案。新内核里有个更潮的tinydrm驱动,基于DRM框架,设备节点是/dev/dri/card0,适合跑Wayland或者EGLFS。但在IMX6ULL这种性能有限的平台上,跑Qt Widgets界面用fbtft足够了,配置简单,文档也多,我最终选择了fbtft。

如果你用的内核版本比较新,比如5.10以上,fbtft可能还在staging里,打开对应Kconfig就能用。如果你的BSP是NXP官方老内核,比如4.1.15,也已经自带fbtft,直接在menuconfig里勾上就行。

启用fbtft相关的内核配置项,我一般是这样勾选:

Device Drivers ---> [*] Staging drivers ---> [*] Support for small TFT LCD display modules ---> <*> FB driver for the ILI9341 LCD Controller <*> Generic FB driver for TFT LCD displays [*] SPI support ---> <*> SPI driver skeleton

如果内核里找不到FB_TFT_ILI9341,说明没开Staging驱动或者Kconfig里没启用fbtft目录。确认方法就是grep FB_TFT /boot/config-$(uname -r),如果为空,需要先把CONFIG_STAGING打开。

3.2 设备树节点完整示例

拿我的板子举例,ECSPI1接的是MSP2402,设备树里需要两个部分:一个是IOMUX引脚复用配置,一个是SPI设备子节点。

IOMUX配置放在&iomuxc节点下:

&iomuxc { pinctrl_ecspi1_lcd: ecspi1_lcdgrp { fsl,pins = < MX6UL_PAD_GPIO1_IO00__ECSPI1_SCLK 0x80000000 MX6UL_PAD_GPIO1_IO01__ECSPI1_MOSI 0x80000000 MX6UL_PAD_GPIO1_IO02__ECSPI1_MISO 0x80000000 MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x80000000 MX6UL_PAD_UART3_CTS_B__GPIO1_IO26 0x80000000 MX6UL_PAD_UART3_RTS_B__GPIO1_IO27 0x80000000 MX6UL_PAD_UART2_CTS_B__GPIO1_IO22 0x80000000 >; }; };

这里我把SCLK、MOSI、MISO配成了ECSPI功能,CS配成GPIO1_IO03,DC用GPIO1_IO26,RST用GPIO1_IO27,背光用GPIO1_IO22。每个板子的引脚选择不一样,你只需要把实际用到的引脚按这个模式写进去就行。

然后配置ECSPI1节点和屏幕子节点:

&ecspi1 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_ecspi1_lcd>; fsl,spi-num-chipselects = <1>; cs-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>; status = "okay"; display: ili9341@0 { compatible = "fbtft,ili9341"; reg = <0>; spi-max-frequency = <16000000>; reset-gpios = <&gpio1 27 GPIO_ACTIVE_LOW>; dc-gpios = <&gpio1 26 GPIO_ACTIVE_LOW>; backlight = <&lcd_backlight>; rotate = <0>; bgr = <1>; fps = <30>; buswidth = <8>; debug = <0>; }; };

背光节点单独定义在根节点下:

/ { lcd_backlight: lcd-backlight { compatible = "gpio-backlight"; gpios = <&gpio1 22 GPIO_ACTIVE_HIGH>; default-on; }; };

这里有几个属性要注意。compatible固定填"fbtft,ili9341",驱动匹配就靠它。reg对应片选索引,因为我们只有一个设备,填0。spi-max-frequency不要一上来就拉满,ILI9341标称虽然支持到几十MHz,但飞线布线情况下,我建议先从8MHz试,稳定后再往上加。rotate控制屏幕旋转方向,0/90/180/270分别对应四个方向,如果画面内容上下颠倒或者左右镜像,改这个值就行。bgr用于控制颜色顺序,如果发现红蓝色对调,改成0或者1各试一次。fps是帧缓冲刷新率,不要设太高,30左右比较合适,高了反而因为带宽不足导致闪烁。

3.3 启用内核模块和最基本的点亮验证

设备树改好后,重新编译设备树并烧写进启动分区。内核模块如果编译成<M>,上电后需要手动modprobe fb_ili9341,或者提前把模块放进rootfs开机自动加载。我习惯直接编成<*>内置,这样设备树匹配到节点后,驱动会自动加载并注册framebuffer。

开机后在终端执行:

ls /dev/fb* cat /sys/class/graphics/fb0/name

如果能看到fb0,名字显示fbtft-ili9341之类,那么驱动层面已经通了。接着做最直观的点亮测试:

dd if=/dev/urandom of=/dev/fb0 bs=1024 count=100

如果屏幕出现随机色点,说明像素数据链路没问题。如果屏幕全白但屏幕背光亮,大概率是DC、RESET、CS引脚配置错了,或者SPI根本没有传输数据。这时候去查内核日志:

dmesg | grep -i fbtft dmesg | grep -i ili9341

很多问题在日志里就能看到,比如spi-master spi0: failed to transfer one message这类错误,说明SPI传输失败,优先检查引脚复用和电气连接。

4. Qt交叉编译与linuxfb平台插件:让Qt认识这块SPI帧缓冲

4.1 为什么用linuxfb而不是eglfs

IMX6ULL有2D加速和PXP,但没有GPU。如果接的是RGB并口屏,本来可以用IMX6ULL自带的LCD控制器配合eglfs或者wayland来做硬件合成,会流畅一点。但外接SPI屏的情况下,屏幕本质是一个通过fbtft模拟出来的帧缓冲设备,没有DRM主节点,也没有KMS,最合适的方式就是Qt的linuxfb平台插件。

linuxfb插件是Qt最早的一套嵌入式后端,它不依赖X11、Wayland,直接mmap/dev/fb0,然后通过QPainter等绘制到帧缓冲中。虽然性能不算高,但结构非常清晰,很少出幺蛾子。你在项目里通过配置环境变量来告诉Qt用哪个平台插件,完全不用改动业务代码。

有些朋友问能不能用QML/Quick?QML本身跟平台无关,但QML渲染依赖OpenGL或者软件渲染,SPI屏上跑Quick的软件渲染也不是不行,只是性能会比较感人。如果你的界面只是简单的控件布局,老老实实用Qt Widgets最省心。我这份源码里的示例就是Widgets工程。

4.2 交叉编译Qt时的configure参数

IMX6ULL跑Linux,一般用arm-linux-gnueabihf交叉编译器。树莓派、BeagleBone这类平台很多人直接装发行版的Qt包,但嵌入式板子往往需要自己裁剪。如果你不想从头编译Qt,也可以考虑直接用Yocto或Buildroot生成的Qt SDK,那样省很多时间,但前提是你对这两个构建系统足够熟悉。

我自己用Qt 5.15.2源码交叉编译的,configure参数供参考:

./configure \ -prefix /opt/qt5.15.2-arm \ -opensource -confirm-license \ -xplatform linux-arm-gnueabihf-g++ \ -release -no-opengl \ -no-eglfs -no-xcb -no-wayland \ -linuxfb \ -no-tslib \ -qt-freetype \ -nomake examples -nomake tests

说明一下几个关键选项:-no-opengl和-no-eglfs都是为了去掉无用的图形后端,减小体积。-linuxfb一定要保留,这是让Qt有linuxfb平台插件的开关。-qt-freetype是把字体渲染引擎编进Qt,这样后续在板子上加载TTF字体时会很方便。-nomake examples和-nomake tests纯粹是为了编译速度。

编译完成后,安装到本地目录:

make -j4 make install

安装完成后,交叉编译你的Qt工程时,需要把qmake指向这个新生成的qmake:

export PATH=/opt/qt5.15.2-arm/bin:$PATH qmake your_project.pro make

出来的二进制就是ARM架构的可执行文件,直接拷贝到板子上。

4.3 运行环境变量和中文显示字体

Qt在板子上运行时,首先需要一组环境变量:

export QT_QPA_PLATFORM=linuxfb export QT_QPA_FB_DEVICE=/dev/fb0 export QT_QPA_FB_NO_LIBINPUT=1 export QT_QPA_FB_TSLIB=0

QT_QPA_PLATFORM=linuxfb指定平台插件。QT_QPA_FB_DEVICE指定帧缓冲设备,如果你系统里还有HDMI或者LVDS等其他fb,可能需要改成/dev/fb0、/dev/fb1,可以在应用启动前用cat /proc/fb确认。

中文显示是另一个大坑。linuxfb插件默认只编译了很小的字体集,如果代码里直接写中文,屏幕上大概率是一堆方块。解决办法是把中文字体文件放到板子上,然后在代码里用QFontDatabase注册字体。推荐用文泉驿正黑或者思源黑体的TTF精简版,体积控制在几MB以内,IMX6ULL上加载不会太慢。

我源码里的做法是,在main函数最前面加载字体:

#include <QFontDatabase> int main(int argc, char *argv[]) { QApplication app(argc, argv); QFontDatabase::addApplicationFont("/usr/share/fonts/wqy-zenhei.ttc"); QFont font("WenQuanYi Zen Hei", 16); app.setFont(font); MainWindow w; w.showFullScreen(); return app.exec(); }

如果你不想在代码里写字体名,也可以直接用环境变量QT_QPA_FONTDIR指定字体目录,Qt启动时会扫描该目录下的字体文件。

5. 写一个可以演示的QT界面工程:结构、代码和部署

5.1 工程文件组织

我打包的QT工程结构很简单,没有用复杂的模块化设计,原因就是让它更容易看懂:

imx6ull_lcd_demo/ ├── imx6ull_lcd_demo.pro ├── main.cpp ├── mainwindow.h ├── mainwindow.cpp ├── resources.qrc └── fonts/ └── wqy-zenhei.ttc

.pro文件内容:

QT += core gui widgets TARGET = imx6ull_lcd_demo TEMPLATE = app CONFIG += c++11 SOURCES += main.cpp mainwindow.cpp HEADERS += mainwindow.h RESOURCES += resources.qrc

resources.qrc把字体文件编进可执行文件,这只是为了演示方便。实际产品中,字体文件放rootfs里独立管理更好,避免每次改字体都要重新编译。

5.2 main.cpp和主窗口实现

main函数我已经贴过一段,核心就是注册字体、设置全局字体、全屏显示主窗口。

主窗口实现一个很简单但能体现“LCD屏幕刷新能力”的界面:首页显示一块状态信息,包含一个标签、一个进度条、两个按钮,还有一个自绘区域,用来测试局部刷新是否正常。

mainwindow.h:

#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> class QLabel; class QProgressBar; class QPushButton; class QTimer; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); protected: void paintEvent(QPaintEvent *event) override; private slots: void updateProgress(); private: QLabel *infoLabel; QLabel *stateLabel; QProgressBar *progressBar; QPushButton *btnRefresh; QPushButton *btnQuit; QTimer *timer; int progressValue; }; #endif

mainwindow.cpp里用QVBoxLayout把控件纵向排列。这个布局在240x320的竖屏上很合适,所有控件都能装得下。

#include "mainwindow.h" #include <QVBoxLayout> #include <QLabel> #include <QProgressBar> #include <QPushButton> #include <QTimer> #include <QDateTime> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), progressValue(0) { setWindowTitle("IMX6ULL LCD Demo"); infoLabel = new QLabel("IMX6ULL + MSP2402", this); infoLabel->setAlignment(Qt::AlignCenter); stateLabel = new QLabel("状态: 运行中", this); stateLabel->setAlignment(Qt::AlignCenter); progressBar = new QProgressBar(this); progressBar->setRange(0, 100); progressBar->setValue(0); btnRefresh = new QPushButton("刷新数据", this); btnQuit = new QPushButton("退出", this); QWidget *central = new QWidget(this); QVBoxLayout *layout = new QVBoxLayout(central); layout->addWidget(infoLabel); layout->addWidget(stateLabel); layout->addWidget(progressBar); layout->addWidget(btnRefresh); layout->addWidget(btnQuit); setCentralWidget(central); timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &MainWindow::updateProgress); timer->start(500); connect(btnRefresh, &QPushButton::clicked, [this]() { stateLabel->setText("状态: 手动刷新 " + QDateTime::currentDateTime().toString("HH:mm:ss")); }); connect(btnQuit, &QPushButton::clicked, qApp, &QCoreApplication::quit); } void MainWindow::updateProgress() { progressValue = (progressValue + 5) % 101; progressBar->setValue(progressValue); stateLabel->setText(QString("状态: 进度 %1%").arg(progressValue)); update(); } void MainWindow::paintEvent(QPaintEvent *event) { Q_UNUSED(event); QPainter painter(this); painter.setPen(QPen(Qt::cyan, 2)); painter.drawLine(0, height() / 2, width(), height() / 2); }

paintEvent里画了一条水平线,用来直观确认Qt的绘制事件能否正确触发LCD局部刷新。如果你把界面最大化或者全屏显示,这条线每次进度更新时重绘,就能看出SPI屏的刷新能力有多强。

注意要在cpp里包含#include <QPainter>。这段代码只是为了演示,实际项目里可以把自绘区域独立成一个QWidget子类。

5.3 在板子上构建和部署

交叉编译时,先确认qt的qmake路径在PATH里,然后在工程目录下执行:

qmake make

生成的二进制是ARM格式,可以直接file imx6ull_lcd_demo验证架构。部署到板子上,假设rootfs目录是/opt/rootfs:

sudo cp imx6ull_lcd_demo /opt/rootfs/home/root/ sudo cp -r fonts /opt/rootfs/usr/share/fonts/

然后在板子上运行:

export QT_QPA_PLATFORM=linuxfb export QT_QPA_FB_DEVICE=/dev/fb0 export QT_QPA_FB_NO_LIBINPUT=1 ./imx6ull_lcd_demo

如果一切正常,屏幕上应该出现标签、进度条和按钮。按钮本身在没触摸屏的情况下没什么用,但可以通过鼠标事件或者外部按键来控制退出和刷新。如果你接入了tslib触摸屏,环境变量里加上QT_QPA_FB_TSLIB=1即可。

6. 调优和踩坑:白屏、花屏、刷新慢、方块字,我一个个排查过

6.1 白屏和花屏:先从最基础的信号查起

用这套方案的点亮调试,我遇到最多的就是白屏。白屏说明背光亮了,但液晶没有收到有效数据或者初始化没成功。排插顺序,我一般按下面这个链路查:

  1. 确认设备树节点有没有被内核解析:ls /sys/bus/spi/devices/,如果有类似spi0.0的设备节点,说明SPI上发现了设备。如果节点都没有,检查compatible属性是否匹配、ECSPI控制器status是否为okay。
  2. 确认fbtft驱动是否加载:ls /sys/class/graphics/fb0/name,如果不存在,dmesg里会有fbtft相关报错。
  3. 手动给RESET引脚一个低电平脉冲,观察模块是否有初始化反应。可以在shell里操作GPIO,也可以用示波器抓波形。
  4. 如果以上都正常,很可能是DC引脚接反了。DC引脚高电平时传输的是数据,低电平时传输的是命令。如果设置反了,初始化序列全乱,屏幕也就白屏。

花屏通常比白屏相对好定位。花屏说明数据已经传到了屏幕,但数据内容或时序不对。优先把spi-max-frequency从16MHz降到8MHz再试,如果正常了,就去检查信号线上的串阻、飞线长度、共地质量。还有一种情况是bgr属性设置反了,红蓝互换会产生“彩色噪点”的视觉效果。

ILI9341初始化时还需要注意一个细节:模块的rotate设置会改变扫描方向和RGB/BGR顺序,MSP2402这个模组默认有些出厂就是BGR顺序,Linux里的fbtft驱动如果没做处理,显示出来的红色和蓝色会颠倒。直接在设备树里把bgr从0改成1测试即可,不用改代码。

6.2 刷新速度和QT性能:SPI屏不能啥都往上堆

用SPI屏跑Qt,帧率天然有限。ILI9341在16MHz SPI时钟下,满屏240x320像素,16位色,一帧裸数据大概是153600字节,理论刷新时间至少76ms,加上控制开销,实际跑满30fps都吃力。所以如果界面上有大面积动画、复杂渐变或者频繁整屏重绘,刷新就会显得很卡。

应对办法有几个。一是降级刷新需求,界面尽量做成静态样式,用标签和简单图形,不要用半透明、阴影、复杂QSS。二是把屏幕的rotate设置为0或者90度,减少数据量不会变,但扫描方向差异在某些模块上会影响观感流畅度。三是合理使用update()的局部刷性,不要调用repaint()强制整屏重绘。

我在源码里用QTimer每隔500ms更新一次进度条,这个频率在SPI屏上看是流畅的,如果缩短到100ms,肉眼就能看到进度条一顿一顿地跳,这就是带宽瓶颈。如果你需要显示实时曲线或者仪表,建议降低采样频率,或者只在小区域局部重绘。

还有一点容易踩:Qt的linuxfb插件默认会在每次事件循环空闲时刷新整个窗口,如果你的主窗口是showFullScreen(),它可能把整个屏幕都重绘一遍。这时候可以在应用启动前设置QT_QPA_FB_NO_FULLSCENARIO_UPDATE=1来限制整屏刷新,不过这个变量在不同Qt版本里行为有所差异,最好还是通过控制代码里的重绘范围解决。

6.3 中文显示成方块:字体文件路径没生效

在linuxfb这类嵌入式平台,Qt不像PC端有系统字体管理服务,它默认只带少量ASCII字体。中文如果没加载自定义字体,就会显示成方块或者问号。

我在实际调试中最常见的失误是字体文件路径写错,或者文件名大小写不对。Linux文件名区分大小写,比如wqy-zenhei.ttc和WQY-ZenHei.ttc完全不同。调试时可以先用一个最简单的测试工程,只显示一个QLabel的中文字符串,如果正常,再往主界面里加内容。

另一个小技巧:如果使用QFontDatabase::addApplicationFont()加载字体,返回值是字体的ID,你可以通过QFontDatabase::applicationFontFamilies(id)获取实际的字体族名称,再设置QFont时用这个名字。很多情况下,文件名和字体族名不完全一致,忽略这一点容易设错。

6.4 背光控制和后续扩展

设备树里配置的gpio-backlight,提供了标准的sysfs背光映射。系统起来后,通过以下命令控制亮度:

echo 255 > /sys/class/backlight/lcd-backlight/brightness echo 0 > /sys/class/backlight/lcd-backlight/brightness

如果你的屏幕支持PWM调光,可以改用pwm-backlight,配合IMX6ULL的PWM模块,实现无级调光。不过对于SPI屏,使用GPIO开关背光已经能满足大多数需求,PWM方案会增加设备树和驱动复杂度。

关于扩展,这套方案后续可以走两条路。一条是把MSP2402换成带XPT2046触摸的模块,硬件上增加几个引脚,驱动上使用ads7846或tsc2007,Qt的QPA层通过tslib接收触摸事件,就能实现全触摸交互。另一条是直接用IMX6ULL的PXP加速,但那种方案通常需要改到DRM/KMS框架,不是这篇SPI小屏能承载的范围。如果你只是需要一块小屏显示状态和简单菜单,现在这套链路已经是性价比最高的路线了。

最后再分享一个打包部署的小技巧:把环境变量和启动命令写成一个shell脚本,放到板子的/etc/init.d/或者/home/root/下,开机后一条命令启动:

#!/bin/sh export QT_QPA_PLATFORM=linuxfb export QT_QPA_FB_DEVICE=/dev/fb0 export QT_QPA_FB_NO_LIBINPUT=1 /home/root/imx6ull_lcd_demo &

这样每次调试节省几秒钟输环境变量的时间。这个脚本我也放在源码包里了,拿走就能用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询