如果你的项目恰好要在RK3568上用SPI接口点亮一块小尺寸LCD,同时又不希望引入复杂的DRM显示链路,那这篇文章值得你花十分钟看完。我在实际项目里用FrameBuffer模式给RK3568写过SPI LCD驱动,整个过程踩了不少坑——从设备树节点怎么写、SPI时序参数怎么定,到预初始化指令序列的排列组合,每一步都有讲究。这篇文章就把我调通的完整思路、驱动框架对接经验和排障过程整理出来,给准备在RK3568上做SPI LCD驱动开发的工程师一个可以复现的起点。
先说清楚这篇文章讲的是什么:RK3568平台下,用Linux内核的FrameBuffer机制驱动一块SPI接口的TFT LCD屏幕。它解决的是“低成本小屏显示”这一类需求,适合做HMI面板、工控显示模块、仪表副屏,也适合刚接触嵌入式Linux显示驱动的同学用来理解FrameBuffer和SPI设备驱动的协同工作方式。文中的代码和配置基于Linux内核5.10版本的常见框架,fbtft和自研驱动的两套思路我都会讲到,你可以按自己的项目情况选。
1. 整体技术路线与FrameBuffer方案选型
1.1 为什么在RK3568上还要用SPI LCD
RK3568是瑞芯微一颗四核Cortex-A55处理器,资源不算差,通常搭配eDP、MIPI DSI、LVDS这类高速显示接口。但实际项目中,SPI LCD依旧有它不可替代的存在价值。
第一是成本敏感场景。小尺寸屏(1.3寸、1.54寸、2.4寸TFT)走SPI接口,屏幕本身单价低,PCB走线也简单,四层板甚至两层板都能把信号拉通。SPI一共就CLK、MOSI、MISO、CS几条线,相比MIPI DSI的差分对和RGB并口的二十多根线,布局压力小一个量级。
第二是低功耗场景。很多SPI LCD在进入Sleep In模式后可以做到极低功耗,配合屏幕自带的内部RAM刷新,适合常显设备。比如一个温控器面板,需要做的只是每隔几秒更新一组数字,SPI屏的功耗优势就很明显。
第三是布线受限场景。处理器的MIPI DSI或RGB并口被主屏占用时,用SPI引出一条副屏是成本最低的扩展方案。我在一个双屏项目里就是这么干的,主屏走eDP显示主界面,副屏用SPI接口显示状态图标和日志信息,两套显示链路互不干扰。
选FrameBuffer而不是DRM,核心原因是开发效率。fbtft、pwm-backlight这些老牌框架在FrameBuffer模式下已经非常成熟,社区资料多,踩坑成本低。而RK3568如果完全走DRM,SPI LCD要么写一个drm_panel驱动挂到虚拟显示接口上,要么走MIPI DSI的command mode,配置复杂度会高出一个数量级。对只需要做简单状态显示的设备来说,FrameBuffer的mmap、ioctl接口完全够用,用户态直接往显存里写像素数据就行。
1.2 FrameBuffer、fbtft和DRM的关系梳理
来理一下这几个概念,它们在嵌入式显示开发里经常被混着说。
FrameBuffer是Linux内核里最早期的显示抽象层,它把显示设备表达成一个简单的内存区,应用程序通过mmap把显存映射到用户空间,直接往里写RGB数据,然后由驱动负责把显存内容推到屏幕上。它的优势是简单直白,劣势是没有做图形合成、多图层叠加这些现代显示功能。
DRM是后来取代FrameBuffer的现代显示框架,它把显示链路拆成了CRTC、Encoder、Connector、Plane这些对象,功能强很多,但上手成本也高很多。RK3568主线内核的eDP、HDMI、MIPI DSI基本都是走DRM框架。
fbtft是Linux内核里专门为小尺寸TFT屏幕写的一套驱动库,它在FrameBuffer框架下把“显存管理、SPI传输、初始化序列下发”这些公共逻辑全部封装好了。ST7789、ILI9341、GC9A01这些常见屏幕都可以直接套用。fbtft的核心思想是:你只需要告诉它屏幕的分辨率、像素格式、初始化命令序列,它就能帮你把FrameBuffer设备注册好,把SPI传输管起来。
我在这篇文章里会以fbtft为主线讲,因为对绝大多数SPI LCD项目来说,这是最短路径。但自研驱动的思路也会单独讲,因为总有fbtft覆盖不到的场景。
2. 硬件链路与SPI总线参数设计
2.1 RK3568 SPI控制器能力与引脚分配
RK3568内部有多组SPI控制器,一般命名为SPI0到SPI3。每组控制器都支持master和slave模式,支持8/16/32位字宽,时钟由PLL分频得到。实际项目中我用的是SPI3,挂在某个GPIO组上,具体引脚以你自己的原理图为准。
引脚分配上有一个关键决策:使用硬件CS还是用软件GPIO模拟CS。
RK3568的SPI控制器自带硬件片选输出,在设备树里通过cs-gpios或默认cs引脚配置。但硬件CS有几个让我头疼的问题:片选释放时机不好控制,某些屏幕在CS拉高瞬间需要额外的保持时间,硬件CS不一定满足;硬件CS的电平极性如果和屏幕控制器的期望不匹配,会出现首字节丢失或偶发误触发。所以我建议直接用普通GPIO手动拉CS,尤其在系统里GPIO有复杂mux配置的时候。软件拉CS的代价是每次传输前多几十纳秒的GPIO操作,对10MHz以下的SPI速率来说完全不是瓶颈。
除了CLK、MOSI、CS三根线,SPI LCD一般还需要两个控制引脚:DC(数据/命令选择)和RST(复位)。部分屏幕还有BL(背光)或TE(撕裂同步)引脚。DC这根线非常关键,后面驱动设计部分会详细展开。
2.2 SPI模式与初始化序列的匹配
SPI有四种模式,由CPOL(时钟极性)和CPHA(时钟相位)组合决定。SPI LCD几乎只用到mode 0和mode 3:
SPI mode 0:CPOL=0,CPHA=0,SCLK空闲为低电平,数据在第一个时钟沿采样。 SPI mode 3:CPOL=1,CPHA=1,SCLK空闲为高电平,数据同样在第一个时钟沿采样。
ST7789、ILI9341、GC9A01这些主流屏幕控制器的规格书里都会写明支持的SPI模式。但我踩过的坑是:规格书写的“支持mode 0/3”不代表实际点亮效果一样,个别屏在mode 3下会因为时钟沿采样时序的微小差异出现颜色偏移或首像素异常。
我的调试习惯是:先把逻辑分析仪挂在CLK和MOSI线上,按照规格书推荐的模式发一条读ID指令,观察返回的8位数据是否和屏厂手册一致。如果一致,说明模式正确;不一致,换另一个模式再试。这个方法比盲调快得多。
2.3 SPI速率的选择逻辑
SPI时钟不是越快越好。你需要先算一笔带宽账。
一块240x320分辨率的RGB565屏,每像素16bit,每帧数据量是240 x 320 x 16 = 1,228,800 bit。SPI速率10MHz下,理论帧率约8fps,实际加上命令传输、CS切换和协议开销,约4到6fps。如果屏幕只显示静态内容或低频更新,这个帧率完全够用。
如果把SPI速率提到40MHz,理论帧率到32fps,但这时候问题就来了。第一是信号完整性,SCLK走线稍长就会出现振铃,导致数据采样错误,具体表现就是花屏或者随机噪点。第二是屏幕控制器的内部FIFO处理能力,很多低端屏控制器在超过20MHz后写显存指令会出现丢字节现象。我用过的一块屏数据手册标称最高支持60MHz,实际20MHz以上就偶发花屏。
所以我的建议是:优先保证稳定,先按10MHz调试通,再根据实际波形和花屏情况慢慢往上升。SPI LCD的传输速率从来不是瓶颈,屏幕控制器的响应能力和初始化序列才是决定显示品质的关键。
3. 设备树配置与驱动框架对接
3.1 设备树spi节点编写要点
RK3568的设备树是标准的DTS格式,SPI LCD的节点一般挂在某个SPI控制器下面。下面是一个我在项目中实际使用的设备树节点示例,屏幕是ST7789控制器的240x320 IPS屏:
&spi3 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&spi3_pins>; st7789v@0 { compatible = "fbtft,st7789v"; reg = <0>; spi-max-frequency = <10000000>; spi-mode = <0>; dc-gpios = <&gpio1 RK_PA0 GPIO_ACTIVE_HIGH>; reset-gpios = <&gpio1 RK_PA1 GPIO_ACTIVE_HIGH>; backlight-gpios = <&gpio1 RK_PA2 GPIO_ACTIVE_HIGH>; rotate = <0>; bgr = <1>; fps = <30>; }; };逐个解释这些属性的作用。
compatible字段决定了驱动怎么找到这个节点。fbtft框架下,compatible写"fbtft,st7789v"就能被fbtft的内置驱动匹配上。如果你决定自研驱动,可以自定义一个compatible字符串,比如"mycomp,spilcd"。
reg = <0>表示这是这个SPI控制器上的第0个片选设备,对应CS0。
spi-max-frequency就是前面说的SPI传输时钟上限,单位Hz。我写的是10MHz,你可以根据实际屏幕支持调整。
dc-gpios和reset-gpios是SPI LCD的两个命门。DC引脚用来区分当前SPI总线上的字节是命令还是数据,fbtft驱动会在这个引脚上做精确的时序控制。reset引脚则负责上电时序,后面会细说。
rotate和bgr是fbtft特有的显示参数。rotate控制屏幕旋转方向,0表示不旋转,90/180/270对应不同方向;bgr字段控制像素RGB通道是否交换,这在同型号屏幕不同批次之间经常需要调整。
3.2 fbtft接入还是自研驱动
fbtft在主线内核里维护已久,对标准ST7789、ILI9341、GC9A01这类屏幕支持得很完整。如果你的屏幕控制器是这些常见型号之一,直接用fbtft是最省力的。
但fbtft不是万能的。我在项目中遇到不适合fbtft的场景包括:
屏幕初始化序列特别长且需要精确的延时控制,比如初始化过程中需要等待屏幕内部稳压器稳定,fbtft的init序列框架对“写命令后等待特定时间”的支持不够灵活。
设备树里不好表达复杂的偏置电压或Gamma设置,这些参数在某些屏厂的非标控制器上需要一次性写入几十个连续寄存器,fbtft的初始化数组写起来非常费劲。
项目要求内核裁剪到极致,不希望引入fbtft的公共代码和依赖。
这种情况下,自研一个FrameBuffer驱动的确更合适。自研的工作量其实不大,核心就是注册一个platform_driver,在probe里完成spi_device挂载、fb_info分配、显存映射和FB ops定义。下面这个章节我会把自研驱动的关键代码骨架写出来。
3.3 FrameBuffer注册过程与显存映射思路
无论用fbtft还是自研,FrameBuffer的注册逻辑都是一样的。核心是填充fb_info结构体,然后调用register_framebuffer把它注册到内核显示子系统。
fb_info有几个字段必须正确设置:var结构体描述显示参数,包括分辨率、像素格式、行间距;fix结构体描述固定属性,包括显存起始地址、长度、类型;还有fb_ops函数集,里面至少要实现fb_setcolreg、fb_blank、fb_fillrect、fb_copyarea、fb_imageblit这几个基本函数,不然用户态的Qt和fbcon用起来会出问题。
显存分配这一块有个关键点。SPI LCD本质上是一个慢速输出设备,显存只是内核里的一块普通RAM,不需要也不应该关联复杂的DMA引擎。我的做法是用dma_alloc_coherent分配一块一致性的内存区域,即使SPI控制器不走DMA,这样也是安全的。它比kmalloc的好处是内存地址物理连续,如果后续你决定让SPI控制器走DMA传输,可以无缝切换;同时它的cache属性配置比较干净,用户态mmap和内核写显存之间不容易出现缓存一致性带来的花屏问题。
4. 驱动代码实现与刷新机制剖析
4.1 probe函数流程与初始化序列下发
我先给一个自研驱动的probe流程骨架,然后逐个环节展开解释。
static int spilcd_probe(struct spi_device *spi) { struct fb_info *info; struct spilcd_par *par; int ret; // 1. 解析设备树参数 par = kzalloc(sizeof(*par), GFP_KERNEL); par->dc = devm_gpiod_get(&spi->dev, "dc", GPIOD_OUT_LOW); par->reset = devm_gpiod_get(&spi->dev, "reset", GPIOD_OUT_HIGH); // 2. 设置屏幕初始化序列 spilcd_reset(par); spilcd_init_sequence(par); // 3. 分配fb_info和显存 info = framebuffer_alloc(sizeof(*par), &spi->dev); info->screen_base = dma_alloc_coherent(&spi->dev, SZ_2M, &par->dma_addr, GFP_KERNEL); info->fbops = &spilcd_ops; info->fix = spilcd_fix; info->var = spilcd_var; // 4. 注册FrameBuffer ret = register_framebuffer(info); if (ret) { dev_err(&spi->dev, "register framebuffer failed\n"); return ret; } return 0; }probe里最容易被忽略的是reset时序。ST7789的规格书要求reset引脚拉低至少10ms,拉高后再等120ms才能下发初始化命令。这个时序不满足,屏幕可能表现为白屏、花屏或随机显示异常。更麻烦的是,这种问题在逻辑分析仪上不一定看得出来,因为reset脚通常没接逻辑分析仪,你只会看到初始化命令发出去了但屏幕没反应。
我建议在reset信号上加一个固定延时函数,不要依赖gpiod_set_value的快速返回。实际操作中我会这样写:
static void spilcd_reset(struct spilcd_par *par) { gpiod_set_value(par->reset, 1); usleep_range(10000, 12000); gpiod_set_value(par->reset, 0); usleep_range(10000, 12000); gpiod_set_value(par->reset, 1); usleep_range(120000, 130000); }这里先拉高再拉低再拉高,是为了确保上电后reset有一个完整的下降沿和上升沿,让屏幕控制器的内部复位逻辑可靠触发。后面120ms是屏幕初始化等待时间,这个值我一般按最大规格来,提前个10ms都可能在某些批次上出问题。
4.2 屏初始化序列的构造技巧
初始化序列是指屏幕控制器的寄存器配置指令,它决定了显示方向、颜色格式、Gamma曲线、电源时序等。这个序列通常由屏厂提供,每个屏幕型号都不一样。
在驱动里,初始化序列一般组织成一个命令数组,格式为“命令长度、命令码、参数1、参数2...”:
static const u8 st7789v_init_seq[] = { 0x01, 0x36, 0x00, // MADCTL 0x01, 0x3A, 0x05, // COLMOD 16bit 0x02, 0xC8, 0x13, 0x04, // Power Control 1 ... 0x00, 0x11, // 无参数,Sleep Out 0x80, 0x29, // 延时后 Display On };fbtft的init序列机制有个约定:命令长度用0xFF表示无参数命令,用0x80开头表示命令执行后需要额外延时。自研驱动时我也沿用了这种协议,写了一个简单的解析函数,遇到0x80标记就在下发完当前命令后msleep。
这里有个经验之谈:初始化序列的顺序千万不能改动,尤其Power Control相关的命令必须在Sleep Out之前执行,Display On之前的延时必须留足。我调试时曾经把Sleep Out提到前面,结果屏幕亮度明显偏低,查了半天才发现是Power Control的命令被覆盖了。
4.3 SPI传输与DC引脚的控制逻辑
SPI LCD的传输和普通SPI设备有个核心区别:DC引脚决定了总线上的字节是命令还是数据。发送命令时DC拉低,发送像素数据时DC拉高。
fbtft和自研驱动都采用spi_message和spi_transfer结构来组织传输。一个常见的优化是:把命令阶段的DC切换和数据阶段的DC切换放到同一个spi_message里,用两个spi_transfer分别对应命令字段和数据字段,这样整个帧数据只需要一次spi_sync调用。
struct spi_transfer tr[2]; struct spi_message msg; tr[0].tx_buf = cmd; tr[0].len = cmd_len; tr[1].tx_buf = data; tr[1].len = data_len; spi_message_init(&msg); spi_message_add_tail(&tr[0], &msg); spi_message_add_tail(&tr[1], &msg); spi_sync(spi, &msg);注意,在使用spi_transfer时,DC引脚的电平控制需要你自己在transfer之前通过gpiod_set_value设置好。这里有一个容易犯的错误:不要在每次transfer之间释放CS,而是要在整个命令+数据序列结束后才拉高CS。很多屏幕控制器对CS的连续性是敏感的,如果CS在命令和数据之间出现一个高电平毛刺,控制器可能把第一个数据字节误判为命令。
另一个常见问题是spi_transfer的delay_usecs字段。某些屏幕要求在命令和相应数据之间保持至少几微秒的间隔,可以在tr[0]和tr[1]之间设置delay。但尽量不要用这个字段做长延时,它依赖SPI控制器的实际精度,在RK3568上可能不准。
4.4 FrameBuffer的读写接口与mmap支持
注册了FrameBuffer之后,用户态程序通过mmap打开/dev/fbX设备节点,直接映射显存。
static int spilcd_mmap(struct fb_info *info, struct vm_area_struct *vma) { return dma_mmap_coherent(info->dev, vma, info->screen_base, info->fix.smem_start, info->fix.smem_len); }dma_alloc_coherent分配的内存必须用dma_mmap_coherent来映射,不能直接用remap_pfn_range,否则cache一致性策略会出问题。
fb_ops里的fb_write、fb_fillrect、fb_copyarea这些函数,如果用户态用的是标准Qt over FrameBuffer或者directfb,系统会调用这些函数。fbtft内部提供了一套默认的绘制实现,效率还不错。自研驱动的话,我建议直接复用内核里的cfb_fillrect、cfb_copyarea、cfb_imageblit,它们是通用的软件绘制函数,代码量小且经过充分验证。
4.5 刷新时机与延迟控制
SPI LCD是一个内存型显示设备,数据写入屏幕控制器的GRAM后,屏幕会持续刷新显示,不需要额外动作。所以驱动的职责就是把用户态的显存内容推送到GRAM。
这里有两套刷新策略。第一套是全屏刷新,在fb_write或者其他写操作完成后,把整块显存通过SPI写入GRAM。这套逻辑简单,但带宽开销大。第二套是局部刷新,维护一个脏矩形区域,只把变化的部分推送到GRAM。fbtft的deferred IO机制实现的就是局部刷新,它利用page fault或delayed work来检测脏区域。
自制驱动里,我用过最简单有效的局部刷新方案是在fb_write里记录写入的起始位置和长度,然后合并相邻的写操作,在schedule_delayed_work里延迟10毫秒统一刷新。这个延迟对显示静态内容的工控界面完全无感,但能把SPI流量降低一个数量级以上。
5. 调试实战:常见问题与排查清单
这部分我完全是拿实际调试的血泪史换来的,照着这个顺序查,能省下大半天的排查时间。
5.1 白屏问题排查
白屏是SPI LCD调试遇到最多的问题。白屏有两种情况:整个屏幕完全点亮但无任何内容,或者背光亮了但屏幕显示全白。
完全点亮的白屏,问题几乎都出在初始化序列没有生效。按这个顺序排查:
先检查reset时序,用示波器量reset引脚,看拉低宽度是否大于10ms,拉高后是否延时了120ms。我在项目里就遇到过屏厂FAE给的示例代码是正确的,但驱动里因为GPIO申请失败导致reset脚根本没动作。
再检查DC引脚的初始电平,DC在发送命令时要为低,如果驱动加载时DC默认被拉高,第一条命令就会被当成数据写入GRAM,整个初始化就乱了。
最后检查初始化序列本身,找屏厂FAE要一份新的初始化代码,很多非标控制器的初始化序列会随批次更新。
背光亮了但屏幕全白,通常是显存内容没有正确推送到GRAM。重点检查fb_info的虚拟分辨率(xres_virtual和yres_virtual)是否设置正确,如果虚拟分辨率大于实际分辨率,用户态写入的显存位置可能超出屏幕控制器的GRAM范围,导致显示区域全是零值。
5.2 花屏、噪点和颜色错乱的排查
花屏的成因比白屏复杂,我把优先级排一下:
最高优先级是SPI模式。mode 0和mode 3在逻辑分析仪上都能看到数据,但如果采样沿不对,屏幕控制器采到的就是错位的数据。表现是颜色分布完全随机,像一个万花筒。用逻辑分析仪对比CLK的采样沿和MOSI数据变化时刻,确保数据在采样沿之前至少保持半个时钟周期的稳定时间。
第二优先级是SCLK速率。前面讲过,速率过高时信号完整性变差,表现是随机噪点、横向条纹、颜色闪烁,而且无规律。这个最好排查,把spi-max-frequency改成5MHz看是否消失。
第三优先级是字节序和RGB通道顺序。ST7789用RGB565格式时,发送到GRAM的数据是大端序还是小端序取决于COLMOD寄存器配置。设备树里的bgr字段如果配反,红蓝通道会互换,整体色调偏色。还有一个隐蔽的坑:某些屏幕在rotate=90或270时,扫描起始位置和方向会改变,颜色顺序也可能受影响。
第四优先级是显存与GRAM的映射关系。FrameBuffer里每行的字节数必须是偶数对齐,否则后续行的起始地址会错位,表现为每隔一定行数出现一条斜向的错位条纹。
5.3 刷新率上不去的瓶颈定位
同样一块屏和驱动,别人刷新率能到30fps,你的只有8fps,差距一般出在这几个地方。
用perf top或tracepoint看CPU占用,如果spi_sync的调用栈占了很大比例,说明SPI传输本身耗时。这时检查SPI时钟是否真的到了配的10MHz,有些内核SPI驱动会根据设备树时钟频率自动分频,分频系数不对会把实际速率拉低。
看传输的组织方式。如果每次传输都单独调用spi_sync,CS会反复拉高拉低,每帧的CS切换开销就很大。正确做法是一个spi_message里尽量包含整帧数据,我实测下来10MHz SPI单次传输1.2Mbit和分成100次传输,时间差距可以达到两倍以上。
检查是否开启了SPI的DMA传输。RK3568的SPI控制器本身支持DMA,但设备树里要正确配置dmas属性。如果SPI传输走PIO模式,CPU需要逐个字节或者逐个FIFO搬运数据,占用率会很高。SPI DMA需要两个通道,一个TX一个RX,即使你只发不收,也要配置RX通道,很多芯片的SPI控制器要求TX/RX成对使能。
5.4 排查速查表
| 现象 | 优先排查项 | 验证手段 |
|---|---|---|
| 全灭无背光 | 背光GPIO、背光电源 | 万用表量背光电压 |
| 白屏无内容 | reset时序、DC电平、初始化序列 | 示波器量reset波形 |
| 全白屏 | 显存映射、虚拟分辨率 | dmesg看fb_info参数 |
| 花屏 | SPI mode、SCLK速率 | 逻辑分析仪对比时序 |
| 颜色偏色 | RGB顺序、字节序 | 显示纯红/纯绿/纯蓝测试图 |
| 横向错位 | 行字节数对齐、MADCTL | 显示渐变色测试图 |
| 随机噪点 | SCLK速率、电平转换 | 降低速率确认 |
| 刷新率低 | SPI message组织、DMA配置 | tracepoint统计耗时 |
6. 性能优化与工程落地的几个建议
6.1 局部刷新与脏矩形机制
如果屏幕需要以30fps以上显示动态内容,全屏刷新这条路在10MHz SPI速率下走不通。唯一的出路是局部刷新。
以ST7789为例,屏幕控制器支持设置GRAM的窗口区域,通过CASET(列地址设置)和RASET(行地址设置)指令,你可以只更新屏幕的某个矩形区域。脏矩形机制的实现思路是:在fb_write或deferred IO的回调中,维护一个位图记录哪些行被修改过,然后每隔一段时间把连续修改的区域合并成几个大矩形,只推送这些矩形到GRAM。
实际效果非常可观。我对一块240x320的屏幕做过测试,刷新一个240x10的进度条区域,SPI数据量只有全屏刷新的1/32,帧率提升空间巨大。但要注意窗口切换指令本身也有开销,如果脏矩形过小,反而会因为频繁切换窗口导致效率下降。工程上我建议脏矩形合并的最小宽度设置为16像素。
6.2 背光调节与PWM参数
SPI LCD的背光一般走PWM调光。设备树里用pwm-backlight节点,或者直接在lcd节点下加backlight属性。PWM频率建议在10kHz到30kHz之间,低于5kHz时人眼能感知闪烁,高于30kHz时部分屏幕驱动芯片的调光线性度变差。
有一个容易被忽略的点:PWM归零和屏幕Sleep Out的时序配合。如果PWM关闭的同时屏幕进入睡眠模式,再次唤醒时可能出现背光时序紊乱。我习惯在驱动suspend回调里先关PWM,再发Sleep In命令,resume时顺序相反。
6.3 FrameBuffer模式下跑Qt的体验
很多项目需要在这个FrameBuffer上跑Qt界面。Qt有linuxfb和eglfs两个插件可以跑FrameBuffer,前者只做软件渲染,后者会尝试用GPU。
RK3568自带GPU,在纯FrameBuffer模式下,eglfs是可以直接对接fb设备的,但由于没有DRM的plane合成能力,GPU渲染的结果需要额外拷贝到FrameBuffer的显存里,性能比主流的DRM/KMS路径差一些。实测下来,用eglfs在FrameBuffer上跑简单控件界面问题不大,但如果涉及视频播放或复杂动画,建议还是走DRM流程。
Qt界面在这个模式下要特别注意字体和控件布局对性能的影响。开启Qt的QT_ANIMATION_DURATION=0,关闭不必要的过渡动画,能明显提升流畅度。
6.4 从FrameBuffer平移到DRM的路径
如果你的产品后续要升级到复杂UI或需要和摄像头、GPU做深度联动,终归还是要走DRM。好消息是SPI LCD的核心驱动代码不用全部推翻。
DRM框架下有一个drm_panel机制,你可以把SPI LCD的初始化序列和液晶面板控制逻辑独立成drm_panel驱动,然后把SPI传输和命令控制封装在panel里,最后接一个虚拟的connector和encoder。这样工作量比直接编写一个完整DRM驱动小很多。
另一个更省事的方案是继续保留FrameBuffer,用DRM的drm_fbdev_emulation做兼容层,这样既能享受DRM的现代管理方式,又保留了/dev/fbX的接口,用户态程序改动最小。不过这个方案的SPI LCD真实性还是依赖于你自己实现的drm_panel,绕不开。
收尾:一点个人经验
最后分享一个小经验:设备树里的spi-max-frequency,我一开始按屏幕控制器的标称值60MHz来写,结果屏花到完全没法看,降到10MHz后稳定运行一个多月没出过问题。后来查了波形才发现是PCB布局上SCLK走线太长,振铃严重。所以调SPI LCD时,永远先把速率降下来确认链路通畅,再慢慢提上去,别一上来就追求标称值。
另外一个让我印象深刻的坑是——某批次的屏幕初始化序列里,有一个控制内部稳压器的命令,屏厂更新的代码里改了一个参数,但同一个供应商给的另外一个型号屏幕还在用旧序列。结果就是同一条产线两种屏,一种正常一种色彩发暗,最后靠分型号加载不同初始化序列解决。遇到SPI LCD项目,永远把屏幕型号和对应的初始化序列版本管理起来,这比驱动代码本身更容易出问题。