1. 项目背景与方案选型:为什么用RK3568驱动一块SPI小屏
做嵌入式Linux开发这些年,接触过不少显示方案。去年接了个工控HMI面板的小项目,主控选了瑞芯微RK3568,屏幕却是一块几英寸的SPI接口LCD。很多人一听就皱眉:RK3568好歹是四核A55,带GPU带VPU,跑个QT界面都轻轻松松,怎么反而去点一块SPI小屏?这个疑问恰恰是本文想聊清楚的核心。
先说结论:SPI LCD在这个项目里不是“性能妥协”,而是成本、功耗、结构空间多方权衡后的理性选择。RK3568这颗芯片定位在AIoT和边缘计算,板卡上通常预留了丰富的GPIO和低速接口。对于只显示温度、湿度、运行状态、IP地址这类文本信息的设备,一块1.3寸到4寸的SPI屏完全够用,整块屏幕成本可能只有同尺寸RGB或MIPI屏的几分之一;再加上SPI屏引脚少、布线简单、驱动不依赖专门的显示时钟,对小批量产品来说,打样和生产都很省事。
从软件实现角度看,Linux下驱动这类屏幕主要有两条路:一是用标准的DRM/KMS框架,二是用传统的FrameBuffer框架。DRM是现在桌面和主流嵌入式显示的大方向,但对付SPI这种低速、低分辨率屏,反而显得“杀鸡用牛刀”——DRM的atomic校验、plane管理、vblank机制这些重型设计,在单图层、无硬件光标的小屏上完全用不上,配置复杂还容易踩各种符号链接和设备节点的坑。FrameBuffer则简单直接,用户态直接用mmap把显存映射出来,往里面写像素字节,屏幕就刷新了,非常契合SPI屏“整帧刷新、无撕裂要求”的使用场景。
所以这个项目的最终方案就是:RK3568 + SPI接口LCD,内核里启用FrameBuffer驱动,通过设备树描述硬件连接,实现屏幕点亮和内容刷新。这篇文章会把整个开发过程拆开讲清楚,包括设备树怎么配、驱动框架怎么选、调试中会遇到哪些坑,以及最关键的几个“为什么”。如果你也在做RK3568或者其他瑞芯微平台的SPI屏项目,这篇文章应该能帮你少走不少弯路。
需要说明的是,本文所有操作基于常见实践和我个人的工程经验,具体路径和参数建议以你自己手上板卡的实际BSP版本为准。
2. 硬件连接与SPI通信基础:点亮屏幕前的必修课
2.1 SPI协议要点回顾:时钟相位、速率、片选
SPI这种总线在嵌入式开发里太常见了,但真正动手驱动LCD时,有几个细节需要重新审视。SPI本质是主从结构,四根线:SCLK、MOSI、MISO、CS。LCD这类只写不读的外设,MISO基本用不上,驱动里甚至可以不接。剩下的核心是时钟极性和相位,也就是CPOL和CPHA。
很多SPI屏驱动IC默认支持SPI Mode 0,即CPOL=0、CPHA=0,时钟空闲为低电平,数据在上升沿采样。但也有屏幕的初始化序列或者控制器要求Mode 1、Mode 2,配置错了最典型的症状是屏幕完全无反应,或者颜色错乱、花屏。RK3568的设备树里,SPI子节点通过spi-max-frequency和spi-cpol、spi-cpha这两个布尔属性来控制时序。我的建议是,动手前先看屏幕数据手册里的时序图,确认它在哪个mode下工作,不要想当然认为是Mode 0。
另一个容易踩坑的地方是速率。普通SPI LCD的驱动IC对时钟上限是有要求的,比如ST7789这类常见IC,datasheet上写的最大SCLK频率一般在几十MHz,但实际走线、电平转换芯片、杜邦线都会让信号劣化。我习惯从1MHz开始往上调,先在4MHz左右验证基本显示,再逐步提到8MHz、16MHz,通过观察屏幕是否有噪点来判断信号质量。RK3568的SPI控制器跑到20MHz以上通常没问题,但如果你用的屏线和接口很随意,高频下字符边缘出现毛刺、整行偏移,那多半是速率过高导致的信号完整性问题。
2.2 SPI硬件片选与软件片选的取舍
RK3568的SPI控制器可以工作在硬件片选模式,也可以把片选引脚配成普通GPIO由驱动手动拉低拉高。两者在实际项目里都要用到,这里直接说结论。
如果只是驱动一块屏幕,且SPI总线没有被其他设备共享,建议优先用硬件片选。设备树里SPI子节点只要有一个reg = <0>这样的属性,控制器就会自动管理CS引脚,驱动程序完全不需要关心片选时序。可一旦总线挂了多个设备,或者你发现某个外设老是和LCD抢总线,就要考虑软件片选了。
软件片选的核心问题是时序。Linux的SPI框架在每次transfer之前会调用cs_control之类的回调来拉低片选,传输结束后再拉高。这个过程如果中间有别的线程插队,就会产生片选毛刺。更麻烦的是,很多SPI LCD的控制器对片选时序有要求——比如从CS拉低到第一个SCLK上升沿,需要几个时钟周期的建立时间。硬件片选模式下控制器会自动处理;软件片选模式下,如果GPIO操作太快,LCD控制器可能根本没识别到“片选有效”,导致后续数据全部丢失。我之前调试一块屏时遇到过诡异的现象:屏幕偶尔能显示,偶尔白屏,查了半天才发现是软件片选建立时间不够。解决办法是在设备树或驱动里给cs-gpios加上gpio-hog之外的延时控制,或者在每次传输前手动加一个udelay(1)。
2.3 RK3568平台上的SPI控制器资源与引脚复用
RK3568芯片内部有多个SPI控制器,具体到某个板子上能用到哪几个,要看硬件原理图和设备树的引脚复用。常见的是SPI0、SPI1、SPI2、SPI3,每个控制器默认有几组引脚可以选择。设备树里通过pinctrl-0来指定引脚功能,比如:
&spi1 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&spi1m0_cs0 &spi1m0_pins>; ... };这里的spi1m0_cs0表示SPI1控制器使用m0这组引脚复用,CS0作为片选。如果引脚复用配置错误,最常见的结果是SPI总线完全无波形,用示波器量SCLK引脚上是高阻或者被拉死的电平,而不是正常的时钟脉冲。遇到这种问题,第一反应不是怀疑驱动,而是先查pinctrl配置是否和原理图一致。
我见过有人把SPI控制器配到m0组,但实际硬件焊在m1组引脚上,导致怎么调都不通。排查方法也很简单:看RK3568的TRM(Technical Reference Manual)里SPI引脚复用表,对照原理图确认使用的是哪一组,然后检查内核dts里对应的节点是否有status = "okay",以及引脚是否被其他外设节点抢占。
除了SPI本身的引脚,LCD还需要几根控制线:RESET复位引脚、DC/RS数据命令选择引脚、背光使能和PWM调光引脚、有时候还有TE tearing effect同步引脚。这些线在SPI LCD上通常不用SPI控制器管,而是独立接到GPIO上。设备树里要把这些GPIO完整描述出来,驱动才能正确完成初始化时序。
3. FrameBuffer驱动框架选择:fbtft、自研还是直接上DRM
3.1 三种驱动方案的对比与适用边界
驱动一个RK3568的SPI LCD,摆在面前的具体实现方案有三条路,先对比一下再决定走哪条。
第一,内核自带的fbtft框架。这是一个专门针对低速LCD的FrameBuffer驱动框架,最早由Noralf Trønnes维护,后来进入了内核主线。它把“初始化显示屏控制器”“操作显存”“刷新到屏幕”这些公共逻辑抽象出来,开发者只需要针对具体的屏幕IC写一小段初始化序列和像素格式转换代码。目前内核里已经支持大量常见IC,比如ili9341、st7789v、st7735r等,很多屏可以直接复用,不用写一行代码。
第二,基于内核SPI框架自己写一个独立驱动。这种方式最灵活,也更“驱动开发”,适合fbtft覆盖不到的怪芯片,或者有特殊刷新时序要求的情况。缺点是工作量大,还得自己处理FrameBuffer的注册、mmap、panic_blink等细节。
第三,直接使用DRM框架。前面提到过不推荐,但对于某些必须使用标准weston或wayland合成器的项目来说,DRM反而是绕不开的。fbtft本身不算一个正式驱动框架,它更像是一个半官方的辅助层,因此它不能直接对接DRM。如果你的最终目标是跑完整的图形栈,那从一开始就得考虑DRM方案。但纯文本、简单图形显示场景,FrameBuffer足够了,没必要给自己找麻烦。
我这次项目选的就是fbtft,理由很直接:屏幕IC是常见的ST7789,fbtft里有现成驱动;应用层只需要显示状态信息和简单图形,mmap一块内存往上面画就行。内核配置时把那几个fbtft相关的选项全打开,设备树里把屏幕节点配好,启动后/dev/fb0就出来了。
3.2 fbtft框架的工作流程:从初始化到刷新屏幕
fbtft的整体流程并不神秘。驱动加载时,首先读取设备树中LCD节点的属性,比如宽度、高度、像素格式、旋转角度、刷新率等。然后调用屏幕IC的初始化函数,通常是一段长长的命令序列,通过SPI逐个发送寄存器配置,把显示控制器设置成目标分辨率、颜色深度和扫描方向。
初始化完成后,fbtft会分配一块内核内存作为显存,并注册一个FrameBuffer设备。用户态的应用程序通过open("/dev/fb0")、ioctl(fb_fix_screeninfo)、mmap等标准接口操作这块显存。屏幕上显示的每一个像素,都对应显存中的若干字节。比如RGB565格式,一个像素2字节,一块320x240的屏幕,显存大小就是320x240x2=153600字节。
关键的刷新机制是fbtft里一个叫做fbtft_deferred_io的线程。它周期性地检查显存中哪些区域被修改了,然后把脏区域对应的像素数据通过SPI发送给LCD控制器。这种“局部刷新”机制比每次全屏刷新省了很多SPI带宽,对低速总线来说极其重要。这里也解释了为什么FrameBuffer的pan_display和fb_blank这类操作在fbtft里只是简单处理:屏幕没有硬件光标和图层概念,一切靠软件刷新。
3.3 决定选型之前需要确认的几个问题
在开始配置设备树之前,有几个问题必须想清楚,否则后面会反复返工。
第一个问题是屏幕控制器是什么型号。同一块裸屏,可能是ST7789、ILI9341、GC9A01等等,不同IC的初始化命令不同,甚至像素格式和RGB顺序都有差异。拿到屏的第一件事就是找到数据手册或者驱动源码,确认IC型号和关键参数。
第二个问题是颜色格式。绝大多数SPI LCD支持RGB565和RGB666,少量还支持RGB444。fbtft默认使用RGB565,因为在16位总线上效率最高。如果你的应用层需要真彩色,可以考虑RGB666,但数据打包和传输字节数都会增加,刷新率会下降。我建议无脑用RGB565,除非有特殊颜色精度需求。
第三个问题是屏幕的扫描方向和行列偏移。同样一块屏,安装方向横放和竖放,在初始化命令里需要设置不同的扫描方向;有些屏的显存坐标和物理像素坐标还有偏移,典型的是ST7789常见的madctl参数和行列偏移,不校准的话,显示内容会出现边框错位或者画面镜像。
第四是背光控制方式。SPI屏通常带一个LED背光,可以用固定GPIO拉高常亮,也可以通过PWM调光。RK3568的PWM控制器很好配置,设备树里可以直接引用pwm-backlight节点。如果只是做产品原型,直接拉高常亮最省事,但做正式产品还是建议上PWM,方便夜间模式下降低亮度。
4. 设备树配置与屏幕参数解析:把硬件信息告诉内核
4.1 RK3568设备树中SPI LCD节点的完整写法
设备树在整个驱动开发中扮演“硬件描述”的角色。内核启动时解析设备树,把SPI控制器、GPIO、背光这些硬件资源分配给对应的驱动程序。这里的配置直接影响fbtft能否“看到”屏幕。
一个最基本的设备树节点长这样:
/ { compatible = "rockchip,rk3568"; backlight: backlight { compatible = "pwm-backlight"; pwms = <&pwm0 0 1000000 0>; brightness-levels = <0 10 20 30 40 50 60 70 80 90 100>; default-brightness-level = <8>; }; lcd_panel: lcd-panel { compatible = "merrii,spi-lcd"; reg = <0>; spi-max-frequency = <8000000>; rotate = <0>; fps = <25>; buswidth = <8>; bgr = <0>; reset-gpios = <&gpio4 RK_PB1 GPIO_ACTIVE_LOW>; dc-gpios = <&gpio4 RK_PB2 GPIO_ACTIVE_HIGH>; backlight = <&backlight>; }; }; &spi1 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&spi1m0_cs0 &spi1m0_pins>; max-freq = <8000000>; panel: spi-lcd@0 { compatible = "merrii,spi-lcd"; reg = <0>; spi-max-frequency = <8000000>; ... }; };注意这里我用的是compatible = "merrii,spi-lcd",这只是一个示例。如果你用的是内核里fbtft自带的st7789v驱动,compatible应该写成"fbtft,st7789v",驱动会根据compatible字符串进行匹配。也可以用"sitronix,st7789v"这类厂商命名,具体看BSP内核里fbtft目录下源码的of_match_table。
设备树配置中最容易出错的有几点。首先,spi-max-frequency在控制器节点和面板节点里都要配置,两者的关系是最小值生效,我想给一个比较高的工作频率时,两个地方都要调高。其次,GPIO属性里的GPIO_ACTIVE_LOW和GPIO_ACTIVE_HIGH要仔细核对,复位引脚一般是低电平有效,DC引脚高电平代表数据、低电平代表命令,弄反了会导致屏幕初始化序列完全乱掉。最后,backlight = <&backlight>这个属性不是通用约定,具体要看驱动源码里使用哪个属性名来获取背光设备,有的是backlight,有的是backlight-gpios。
4.2 行列偏移、扫描方向与bgr标志的调试思路
每一个SPI LCD屏都像一个有“怪癖”的设备。同一个IC,不同厂商封装出来的屏幕,物理走线和显存映射可能不同。最常见的表现是,初始化完显示正常,但屏幕四周有彩条边框,说明显存尺寸和分辨率不匹配,或者初始化参数里没有正确设置偏移量。
以ST7789为例,它内部显存是240x320,如果你的屏物理分辨率是240x280,那么上下各有一部分显存是“看不见”的,如果初始化时没有设置恰当的垂直偏移,显示内容会整体往上或往下偏。这类偏移一般写在屏幕初始化序列里,不同的屏幕模组厂商会给一个推荐值。比如0x36命令设置madctl(Memory Access Control),里面包含行/列交换、行/列反转等信息;0x2A和0x2B命令设置列地址和行地址范围,起始坐标可以直接写在里面。
设备树里还有两个属性跟这个密切相关:rotate和bgr。rotate表示屏幕旋转角度,0、90、180、270分别对应正常、右转90度、翻转、左转90度。fbtft内部会通过调整扫描方向和坐标映射来实现旋转,而不是单纯地旋转FrameBuffer内容。这导致旋转后,屏幕上的文字或者图形可能会拉伸变形或者位置偏移,需要重新校准。bgr则控制RGB顺序是否交换。如果你的屏显示出来的颜色里红色和蓝色互换,那多半是BGR和RGB顺序配置反了,把设备树里bgr的值取反即可。
我调试时通常分两步走。第一步,先用一个纯色测试画面,比如全红、全绿、全蓝,确认RGB顺序是否正确。如果显示出来变成蓝绿红,那就不是驱动问题,而是bgr标志问题。第二步,画一个带方向性的图案,比如左上角画一个实心圆,右下角画一个三角,看旋转和镜像是否符合预期。这两个验证步骤做完,屏幕坐标系就基本确定了。
4.3 背光节点与PWM调光的参数配置
背光这块看似简单,但设备树配置参数一多也会出问题。RK3568的PWM输出通常接到背光驱动芯片或者直接驱动LED。设备树里使用pwm-backlight这个通用节点比较推荐,因为内核已经帮你处理了亮度映射,应用层只需要通过sysfs接口写brightness节点就能调节亮度。
pwms = <&pwm0 0 1000000 0>这行的意思是,使用pwm0控制器,通道0,PWM周期为1000000纳秒即1kHz,极性为0。这里的1000000不是随意的,1kHz是LED背光PWM的常用频率,太高了驱动芯片可能响应不过来,太低了肉眼能看出闪烁。brightness-levels是亮度阶梯表,内核会把它映射到0到255的亮度值范围。default-brightness-level是开机默认亮度等级,索引对应亮度表里第几档。
实际调试中,如果屏幕亮但背光不亮,先看PWM节点有没有被正确启用,用示波器量PWM输出脚有没有波形,同时检查brightness节点的当前值:
cat /sys/class/backlight/backlight/brightness echo 50 > /sys/class/backlight/backlight/brightness如果写入brightness没有反应,很可能是背光节点没有和面板驱动关联上,或者PWM控制器时钟没有使能。RK3568的pwm控制器本身也需要在设备树里设置status = "okay"和时钟源,漏了这一步,PWM信号就是死的。
5. 内核配置、编译与系统移植:让FrameBuffer设备真正出现
5.1 内核配置中fbtft相关选项的正确打开方式
RK3568上的Linux内核一般基于Rockchip官方BSP或者Buildroot维护的内核分支。准备编译内核前,先把fbtft相关功能确认打开。由于fbtft在内核中的组织比较分散,我一般通过menuconfig直接搜索:
make ARCH=arm64 menuconfig按下/搜索FBTFT,会出来几个相关选项。需要打开的核心有:
CONFIG_FB_TFT:fbtft主框架CONFIG_FB_TFT_ST7789V:针对st7789v控制器的驱动CONFIG_FB_TFT_ILI9341:针对ili9341控制器的驱动,如果用到就打开CONFIG_FB_TFT_FBTFT_DEVICE:用于在设备树里配置面板组合CONFIG_FB_DEFERRED_IO:fbtft依赖的deferred io支持,必须打开
如果打算把驱动编成模块,就在menuconfig里选M,之后insmod加载;如果直接编进内核镜像,选*。我个人的习惯是编成模块,调试的时候方便替换,不用每次都重烧整个boot分区。但要注意,编成模块的时候,设备树里的compatible匹配并不会有问题,关键是把模块放到rootfs里,或者通过initramfs加载。还有一种做法是直接在menuconfig里把fbtft编进内核,省掉模块管理这一层麻烦,适合产品发布前的固化阶段。
5.2 设备树编译与烧录:修改dts后的完整操作路径
RK3568的设备树编译不是简单的dtc一把梭。Rockchip的BSP里通常有专门编译脚本,把多个dts源文件、dtsi包含文件以及头文件宏定义一起编译成最终的dtb文件。比较典型的是内核源码目录下直接执行:
make ARCH=arm64 rockchip/rk3568-evb.dtb这里rk3568-evb.dtb是示例,具体名字看你板卡对应的dts文件。如果在Buildroot或者SDK环境下,又有单独的命令。关键是,修改完dts后,要确保编译出来的dtb和你实际烧录的分区匹配。RK3568的dtb一般在boot分区或者单独的资源分区里,具体看板卡的分区表。
烧录方式常见的有两种:一是通过瑞芯微的RKDevTool工具,在Loader模式下把生成的dtb或者带dtb的boot.img烧进对应分区;二是通过fastboot命令直接刷入。如果板卡已经跑起来了,也可以用以下方式把新dtb刷进去再重启:
adb root && adb remount adb push rk3568-evb.dtb /boot/ adb reboot这种情况下,u-boot会从boot分区加载dtb,覆盖原有配置。这个方法调试阶段效率最高,不用反复插拔USB进入loader模式。但要注意,dtb修改后必须和内核版本匹配,不然内核启动时会报FDT_ERR_BADMAGIC之类错误。
5.3 从启动日志到/dev/fb0:点亮过程全记录
完成内核配置和设备树修改后,重启系统。启动过程中,首先应该关注内核日志里SPI控制器和LCD驱动的初始化信息。可以通过串口控制台看到类似下面的输出:
[ 2.345678] fbtft: module is from the staging directory, the quality is unknown, you have been warned. [ 2.345879] fbtft_device: SPI display (spi1.0) configured [ 2.346001] st7789v spi1.0: fbtft_probe_common: width=240, height=320, rotate=0, bgr=0 [ 2.346114] st7789v spi1.0: Display initialized如果驱动加载失败,比如GPIO申请失败或者SPI通信超时,日志里会有明确报错。启动完成后,检查设备节点:
ls /dev/fb* fbset -i/dev/fb0存在,说明FrameBuffer设备注册成功。接着可以用系统自带的工具画测试画面:
cat /dev/urandom > /dev/fb0 echo -e '\x00\xf8\x00\x00' > /dev/fb0第一条命令随机填充,屏幕上应该出现雪花点;第二条把左上角几个像素设成亮绿色。如果雪花点能正常显示,整个FrameBuffer链路就是通的,接下来可以进入应用层开发。
5.4 应用层显示的常见做法:直接画还是借助图形库
屏幕点亮只是第一步,真正用起来还要解决“显示什么”的问题。纯FrameBuffer模式下,应用层有两个主流选择。
最直接的是自己写程序mmap显存,按坐标计算像素地址,逐行填充。比如要在屏幕上显示一行文字,就得自己做字模提取和排列。这种方式的优势是零依赖,适合显示固定画面或者简单叠加图形。缺点是显示中文、抗锯齿、多字体这些都要自己搞。之前有个需求是屏幕显示中文字段,我在应用层直接用点阵字库,把GB2312编码转换成16x16点阵数组,然后往FrameBuffer对应区域写字节,实测效果很不错,刷新也很稳。
另一种是移植MiniGUI、QT或者LVGL这类图形库。QT在FrameBuffer模式下有linuxfb插件,LVGL本身也支持FrameBuffer作为底层接口。这类库为我们解决了窗口管理、字体渲染、控件绘制的大部分工作,代价是占用更多内存和CPU。在本项目里,因为只需要显示固定几个页面,我选择自己写一个简单的绘制函数,配合背景图和少量矢量图形,资源占用非常低,CPU占用率几乎可以忽略。
6. 调试实录与避坑技巧:从白屏到稳定刷屏的实战记录
6.1 白屏问题排查:从电源到初始化序列的层层剥离
白屏是最常见的现象,也是最容易让人摸不着头脑的问题。白屏说明LCD的背光和面板本身是正常的,但没有收到有效的显示数据,导致整个屏幕显示默认的白色背景。排查顺序一般如下。
第一步,确认SPI通信是否真的发生。不接屏幕,用示波器或者逻辑分析仪量SPI主控的输出引脚。如果SCLK没有波形,问题在SPI控制器配置或者引脚复用;如果有波形但MOSI上没有数据,问题在fbtft驱动没有正确写命令序列。
第二步,确认复位时序。很多SPI屏要求在初始化前拉低RESET一段时间,然后拉高,这个过程由GPIO控制。如果复位时序不对,屏幕IC会一直处于复位状态,任何数据都不响应。用示波器量一下RESET引脚的波形,看是否出现明显的高低电平跳变。
第三步,怀疑初始化序列本身。fbtft驱动一般会把初始化代码放在init_display回调里。不同IC的初始化命令不一样,如果多了一条非法命令或者参数错误,IC会忽略后续命令,屏幕自然不亮。这种问题比较难查,我的建议是直接从屏幕厂商或者IC的官方例程里找初始化序列,逐条对照驱动源码,看是否有遗漏或多余的命令。
我在这个项目里碰到过一次白屏,查了大半天,最后发现是dc-gpios配置反了。因为DC引脚电平状态决定了当前字节是命令还是数据,配置反了等于所有命令都被当成像素数据发送,屏幕当然不会工作。把设备树里dc-gpios的GPIO_ACTIVE_HIGH改成GPIO_ACTIVE_LOW后,屏幕立刻正常。
6.2 花屏、偏色与镜像:坐标系和颜色格式该怎么校准
花屏比白屏更“有意思”,因为它说明通信链路和大部分初始化都是通的,只是某个细节没对上。
偏色是最容易处理的。全屏显示红色,如果实际显示为蓝色,说明RGB和BGR顺序反了,改设备树里bgr属性即可。如果颜色发暗或者偏色比较诡异,先怀疑RGB565的字节序问题。FrameBuffer里的像素格式跟屏幕IC要求的不一致是常事,检查驱动源码里pixel_format的配置,确保fb_var_screeninfo.bits_per_pixel是16且red、green、blue各通道的偏移是典型RGB565排列。
镜像问题通常是扫描方向设置错误。屏幕显示内容左右镜像,意味着初始化命令里的行扫描方向反了。ST7789通过0x36命令的MX和MY位来控制扫描方向。设备树里可以整体设置rotate,但有时候rotate调整会同时影响行列偏移,需要配合行列偏移参数一起改,才能把画面完整摆正。这个阶段要有耐心,建议一次只改一个参数,结合屏幕显示结果逐步逼近正确配置。
花屏还有一种隐蔽情况,是SPI接收数据时字节错位。比如驱动配置的buswidth是8,实际屏IC可能要求按9bit格式传送,或者在某些初始化阶段需要9bit模式。大部分LCD用的都是8bit SPI,但有些屏需要先用9bit模式发送一条特殊命令,然后切回8bit模式。如果遇到花屏且排除其他原因,可以检查屏幕IC的datasheet是否有类似的“扩展命令模式”。
6.3 刷新率与闪烁:如何平衡SPI带宽和显示效果
SPI屏的刷新率是个无法回避的瓶颈。假设屏幕分辨率是240x320,RGB565格式,一帧数据量是240x320x2=153600字节。SPI时钟8MHz,理论带宽1MB/s,实际扣掉协议开销、片选切换、命令间隙,每秒能刷新的帧数大约是6到7帧。这个刷新率做静态显示没问题,但显示动态文字或者视频就明显卡顿。
fbtft的fps参数可以调节刷新目标,但不要以为把fps调到60就能实现60帧。这个参数是目标值,实际刷新率还受限于SPI带宽和deferred io的工作方式。如果显示内容变化频繁,fbtft会尝试全屏刷新,带宽不够时刷新率自然上不去。
改善刷新率有几个策略。第一,降低SPI时钟的上升沿裕量,在信号质量允许的情况下提到12MHz甚至16MHz,能明显提升带宽。第二,把刷新区域限制在屏幕的一部分,不要全屏更新,deferred io是自动检测脏区域的,这个特性要利用好。第三,减少像素格式字节数,比如用RGB444或者65K色,一帧数据量更小,但颜色精度会有损失。
闪烁问题通常是刷新和面板扫描不同步导致的。SPI屏没有TE信号同步机制,靠软件刷新,如果刷新过程中屏幕控制器正在扫描,画面就可能撕裂或者闪烁。解决方案有:降低刷新率以减少刷新频率,或者开启fbtft的fps稳定机制,把每次刷新间隔控制在一个固定节奏。如果屏幕IC支持,也可以在初始化序列里开启tearing effect输出,然后通过GPIO中断来实现帧同步刷屏,但SPI屏上一般很少这么干,优先级不高。
6.4 常见问题速查表
做一次完整的SPI LCD驱动开发,反复出现的问题其实就那几类,我整理了一张速查表,调试时对照着看非常方便。
| 现象 | 可能原因 | 快速排查方法 | 解决参考 |
|---|---|---|---|
| 白屏无任何显示 | DC引脚配置反、复位时序不对、初始化序列错误 | 示波器量DC和RESET引脚波形 | 检查设备树dc-gpios的高低电平定义,核对初始化命令 |
| 花屏 | SPI时钟太快、RGB顺序错、坐标映射错误 | 降低SPI速率,显示纯色测试画面 | 调整spi-max-frequency或修改bgr、rotate |
| 颜色偏色 | RGB565字节序不对 | 显示纯红/纯绿/纯蓝 | 检查驱动中red/green/blue偏移配置 |
| 画面左右或上下镜像 | 扫描方向设置错误 | 绘制带方向性的图案 | 修改madctl初始化参数,调rotate |
| 屏幕有边框/显示偏移 | 行列偏移设置错误 | 用全屏颜色测试定位偏移方向 | 修改初始化命令中的行列起始地址 |
| 背光不亮 | PWM未使能、亮度为0 | 检查brightness节点,量PWM输出 | 确认设备树pwm节点status = "okay" |
| 刷新率低 | SPI带宽不足 | 测量SPI实际吞吐 | 提高SPI时钟,减小刷新区域 |
| 启动时不生成/dev/fb0 | 驱动未加载、设备树匹配失败 | 看内核日志,dmesg | 检查compatible字符串和模块是否加载 |
7. 项目收尾时的经验总结与可扩展方向
把这块SPI屏调稳定之后,项目里后续还做了一些扩展,这里一起分享。首先是触摸功能。很多SPI LCD模组会带上触摸芯片,常见的有XPT2046这类SPI接口触摸IC。在RK3568上额外接一个SPI设备,设备树里再配一个ads7846或者ti,tsc2046节点,内核就能注册成input设备,应用层通过/dev/input/eventX读取触摸坐标。要注意的是,触摸芯片和LCD共用一条SPI总线时,片选分配要精确,两个设备节点在设备树里用不同reg值区分。
其次是系统裁剪优化。FrameBuffer模式下,不需要启动weston或者其他合成器服务,可以把大量桌面组件裁剪掉,只保留最小rootfs。实测下来,系统内存占用从500多MB降到不到200MB,对资源受限的盒子产品来说非常实用。内核里不需要的Touchscreen、GPU相关的驱动也可以关掉,但如果后续要跑界面动画,GPU还是建议保留。
还有一个值得提起的是双屏方案。RK3568自带HDMI和LVDS/RGB输出,如果把SPI小屏作为辅助状态屏,与主屏同时工作,应用层可以通过多个FrameBuffer设备节点分别操作。这在实际产品里很常见,比如主机界面通过HDMI输出,小屏显示运行状态。这种情况下,系统初始化时给不同的/dev/fb设备分配不同分辨率和像素格式即可。
就我个人这次项目的整体体会而言,FrameBuffer方案虽然听着“复古”,但在特定场景下依旧是效率和成本的最佳平衡点。它的调试思路其实比DRM更直白:设备树描述硬件,驱动注册显存,应用层直接画。顺着这条链路逐个环节查,问题总会水落石出。芯片性能再强,也别忘了架构简单往往就是最大的可靠。SPI LCD项目让我重新认识了“合适的技术”和“先进的技术”之间的区别,以后再做类似小屏显示需求,我大概率还是会在FrameBuffer和fbtft这条路上继续深挖。