尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

RK3568 SPI LCD FrameBuffer驱动开发实战:从设备树到性能优化

RK3568 SPI LCD FrameBuffer驱动开发实战:从设备树到性能优化 最近帮一个做工业HMI的客户调了一块1.54寸的SPI LCD用的是RK3568平台驱动框架没走常规的DRM/KMS而是落了最直接的FrameBuffer方案。客户需求很简单低成本小屏显示数据不追求复杂图形加速系统跑Linux底层需要快速点亮并稳定刷新。这个场景在实际项目里其实非常常见尤其是做仪表盘、充电桩、小家电面板这类产品主控跑RK3568这个级别显示端却用一颗SPI接口的LCD——听起来有点“大马拉小车”但成本、供应链和研发周期的账算下来这么选的人还真不少。这篇文章就完整梳理一遍RK3568上FrameBuffer模式SPI LCD驱动开发的思路和方法从硬件准备、设备树配置、内核驱动适配到常见问题排查把能直接用在项目里的东西都整理出来。1. 整体方案设计与技术选型1.1 为什么在RK3568上还用FrameBuffer老框架先说个很多人会问的问题RK3568明明自带GPU和Display Controller跑的是DRM/KMS这套现代显示框架为什么还要用FrameBuffer这种“老古董”答案其实很现实成本和需求匹配度。SPI LCD本身是低速设备刷新率低、分辨率小一般就是240x320、320x480这类级别它根本吃不满RK3568的显示带宽。如果为了这类小屏去走DRM框架你得设计一个panel驱动、一个bridge驱动还要对接到RK3568的VOPVideo Output Processor显示通路中间涉及VOP2/DPHY/MIPI DSI等一大串链路开发周期和调试成本直接翻倍。而FrameBuffer框架在内核里存在了几十年机制简单直接你给它一块内存它把内存内容同步到屏上中间几乎没有额外封装非常适合SPI这种低速、简单、无复杂时序要求的显示设备。再讲讲实际选型时的另外两条路fbtft设备驱动这是社区的一套SPI LCD FrameBuffer驱动方案支持一批常见驱动芯片ST7789V、ILI9341、SSD1306等写起来很轻量适合快速验证。DRM/KMS panel-simple正规、功能全但对SPI这类低速小屏来说性能和收益都不划算代码量还大。我在这个项目里实际选了fbtft的思路但没直接用fbtft的现成代码而是基于FrameBuffer框架自己写了个精简驱动。原因有两点一是fbtft在内核主线里已经被移除很久了打补丁麻烦二是客户对初始化序列有定制需求自己写的驱动灵活性更高也便于针对Flash读写性能做优化。1.2 RK3568 SPI控制器能力评估RK3568芯片内部集成8个SPI控制器编号从SPI0到SPI7每个控制器支持Master/Slave模式最高时钟频率可以跑到50MHz。这几个控制器在硬件上支持标准的4线模式SCK、MOSI、MISO、CS#部分还支持Quad模式IO0~IO3但FrameBuffer的LCD驱动场景一般只用标准4线就够除非你的LCD驱动芯片支持RGB565格式的SPI传输比如ST7789V就支持3线9bit模式但实际用得少。SPI控制器的工作模式分为两种硬件片选和软件片选。驱动FrameBuffer LCD时推荐优先使用硬件片选让SPI控制器自动管理CS引脚时序可以减少CPU中断次数提升刷新效率。硬件片选也有个坑某些平台的片选信号在SPI传输结束后会立刻拉高这对某些LCD驱动芯片的“D/CX数据/命令选择”脚不友好因为D/CX通常要求与片选同步配合。如果你的LCD模块把D/CX接到了GPIO那么硬件片选就够用如果D/CX也靠SPI控制器的另一种模式去控制比如有些模块没有D/CX靠SPI的byte stream里的命令位来区分这就要注意了——SPI控制器无法在硬件层面区分“命令”和“数据”必须靠驱动软件去控制。我调试用的板子是飞凌的OK3568-C默认SPI2引出了一组4线接口电平是3.3V正好匹配大部分SPI LCD模块的供电范围。具体引脚分配见下表信号RK3568引脚功能说明SPI2_CLKGPIO2_B2SPI时钟建议初始化为9MHzSPI2_MOSIGPIO2_B3主发从收LCD数据输入SPI2_MISOGPIO2_B4主收从发LCD一般不用可悬空或复用GPIOSPI2_CS0GPIO2_B6硬件片选输出有效低电平LCD_DCGPIO3_A1数据/命令选择0命令1数据LCD_RSTGPIO3_A2复位信号低有效LCD_BLGPIO3_A3背光控制可用PWM2. 设备树配置与内核启用细节2.1 设备树节点设计设备树在Linux驱动开发中起到“硬件描述”的关键作用RK3568平台对SPI控制器节点和GPIO控制节点都有固定的描述方式。首先看SPI控制器本身的配置。在rk3568.dtsi文件里SPI2节点默认是disabled状态需要在板级设备树文件比如rk3568-evb.dts或自定义的ok3568-c.dts里把它打开并且声明挂在它下面的SPI从设备——这里就是我们的LCD屏。spi2 { status okay; pinctrl-names default, sleep; pinctrl-0 spi2m0_clk spi2m0_cs0 spi2m0_miso spi2m0_mosi; pinctrl-1 spi2m0_clk_gpio; spi-max-frequency 9000000; st7789v: st7789v0 { compatible jianda,st7789v; reg 0; spi-max-frequency 9000000; rotate 90; rgb 565; fps 30; buswidth 8; dc-gpios gpio3 RK_PA1 GPIO_ACTIVE_HIGH; reset-gpios gpio3 RK_PA2 GPIO_ACTIVE_LOW; backlight lcd_backlight; status okay; }; };有几个关键参数需要解释spi-max-frequency 9000000SPI时钟频率上限。这个值不要一上来就拉满先用9MHz点亮后再慢慢往上调试。SPI LCD毕竟是低速外设频率太高容易出干扰尤其是走线长的板子。rotate 90屏幕默认方向。这里的数值由驱动解析0/90/180/270分别对应四个旋转方向。rgb 565像素格式表示RGB565。ST7789V这类驱动芯片默认支持RGB565和RGB666两种格式RGB565每个像素占16bit刷屏速度快开发中优先选用。buswidth 8一次SPI传输多少位。这里是8位因为ST7789V的命令和参数都是字节流。2.2 背光节点的配置背光控制有两种常见方式高低电平开关和PWM调光。如果只要求“亮”和“灭”用普通GPIO控制就行如果需要调节亮度就得走PWM通道。下面这个节点是典型的GPIO背光控制方式lcd_backlight: lcd-backlight { compatible gpio-backlight; gpios gpio3 RK_PA3 GPIO_ACTIVE_HIGH; default-on; status okay; };注意default-on;这个属性它确保驱动在加载的时候立即点亮背光。如果你希望在LCD初始化完成后再上电可以减少这个属性在驱动probe完成后用gpiod_set_value手动拉高。如果改用PWM背光RK3568设备树的写法类似下面lcd_pwm_backlight: lcd-pwm-backlight { compatible pwm-backlight; pwms pwm7 0 50000 0; brightness-levels 0 10 20 30 40 50 60 70 80 90 100; default-brightness-level 8; status okay; };这里的pwms属性包含三个值对应PWM控制器、PWM通道号、周期纳秒。50000纳秒就是20kHz的频率LCD背光的PWM频率最好在15kHz~25kHz之间太低会有频闪人眼虽然不易察觉但长时间看容易累额定功率下也影响LED寿命。2.3 pinctrl引脚复用配置RK3568是典型的引脚复用型SoC同一颗引脚可能被I2C、SPI、UART、GPIO等不同功能复用。设备树里通过pinctrl节点来定义某种功能模式下引脚的状态。SPI2的引脚复用在rk3568-pinctrl.dtsi里已经定义好了默认选项板级文件里通常只需要引用它们pinctrl-0 spi2m0_clk spi2m0_cs0 spi2m0_miso spi2m0_mosi;这里spi2m0表示SPI2的“m0组”引脚。RK3568的每个控制器可能有多个引脚组m0/m1选哪组看板级硬件连接。如果板子设计时引出的SPI2引脚和m0不吻合就需要查原理图确认后切换成m1组否则SPI通信完全不通。调试阶段如果发现SPI总线完全无波形80%以上的可能性不是驱动问题而是引脚复用配置错了或者节点没打开。先用IO命令拉高某个测试引脚看反应或者在驱动里临时加一条dev_info打印of_property_read_string去确认实际生效的引脚组都能快速定位这类问题。2.4 内核menuconfig配置设备树写好后在内核编译阶段必须确保FrameBuffer相关模块被编译进内核。在kernel源码目录执行make menuconfig按以下路径打开选项Device Drivers - Graphics support - Frame buffer devices这里需要确保以下几个选项是开启的CONFIG_FBFrameBuffer核心支持必须。CONFIG_FB_CMDLINE允许在bootargs里设置分辨率参数可选但推荐。CONFIG_FB_SIMPLE简单的Memory Framebuffer支持除非你的平台用simplefb一般可以不选。CONFIG_FB_ST7789V我们写的驱动模块编译成模块M或编入内核Y皆可调试期建议M。编译好内核后bootloaderU-Boot启动参数里需要追加video...参数FrameBuffer模式下最常见的设置是videofb0:240x320-1660这个参数指定了fb0设备的分辨率是240x320每像素16位色深RGB565刷新率目标60Hz。这里要强调一下60Hz只是目标值SPI LCD实际能否跑到取决于SPI总线和LCD控制器能力后面性能部分会详述。3. 驱动核心实现与工作原理拆解3.1 Framework层面FrameBuffer核心如何工作在写具体驱动之前有必要把FrameBuffer的框架理一下。Linux内核的FrameBuffer系统由三层组成最上层fbdev接口暴露/dev/fb0设备节点用户态程序可以用open、mmap、ioctl直接操作。中间层fbmem.cfb_defio管理显示缓冲区显存的分配、映射和刷新。最底层驱动开发者实现的struct fb_info注册逻辑填充fb_ops结构体并实现最关键的两种刷屏机制——整屏刷新和局部刷新。图省事的话很多SPI LCD驱动在初始化后直接映射一整块内存作为显存每次用户程序写入驱动通过fb_deferred_io机制检测脏区域把变化的部分打包成SPI数据发送给LCD。fb_deferred_io是省流量的关键它借助内核的延迟工作队列定时把标记为脏的页面重新发送到屏幕而不用每帧都把整块显存全量刷一遍。3.2 驱动的probe流程设计我们驱动的probe函数是整个初始化的核心主要完成以下几件事从设备树解析GPIO、分辨率、旋转方向等参数。发起LCD复位时序。发送初始化命令序列。分配FrameBuffer显存并注册fb_info。创建defio相关的延迟工作队列。开启背光。看一下核心代码框架static int st7789v_probe(struct spi_device *spi) { struct fb_info *info; struct st7789v_par *par; int ret; par kzalloc(sizeof(*par), GFP_KERNEL); if (!par) return -ENOMEM; par-spi spi; par-reset_gpio devm_gpiod_get(spi-dev, reset, GPIOD_OUT_HIGH); par-dc_gpio devm_gpiod_get(spi-dev, dc, GPIOD_OUT_LOW); par-bl_gpio devm_gpiod_get(spi-dev, backlight, GPIOD_OUT_HIGH); // 复位时序 gpiod_set_value(par-reset_gpio, 0); msleep(120); gpiod_set_value(par-reset_gpio, 1); msleep(120); // 初始化LCD控制器 st7789v_init_sequence(par); // 注册FrameBuffer info framebuffer_alloc(sizeof(struct st7789v_par), spi-dev); if (!info) { ret -ENOMEM; goto err_free; } info-par par; info-var.xres 240; info-var.yres 320; info-var.bits_per_pixel 16; info-var.red.offset 11; info-var.red.length 5; info-var.green.offset 5; info-var.green.length 6; info-var.blue.offset 0; info-var.blue.length 5; info-fix.type FB_TYPE_PACKED_PIXELS; info-fix.visual FB_VISUAL_TRUECOLOR; info-fix.line_length 240 * 2; info-fbops st7789v_ops; info-screen_size 240 * 320 * 2; // 分配显存 info-screen_buffer dma_alloc_coherent(spi-dev, info-screen_size, info-fix.smem_start, GFP_KERNEL); memset(info-screen_buffer, 0, info-screen_size); // 注册framebuffer设备 ret register_framebuffer(info); if (ret) goto err_alloc; spi_set_drvdata(spi, info); // 开启背光 gpiod_set_value(par-bl_gpio, 1); dev_info(spi-dev, ST7789V framebuffer registered, %dx%d\n, info-var.xres, info-var.yres); return 0; err_alloc: dma_free_coherent(spi-dev, info-screen_size, info-screen_buffer, info-fix.smem_start); err_free: kfree(par); return ret; }复位时序这里有个细节值得单独说LCD驱动芯片的复位时间要求芯片手册里一般会写“至少10us的低电平脉冲”但实际做产品时120ms的低电平时间更稳妥。因为系统上电瞬间电源纹波可能很大如果复位脉冲太短芯片内部状态机可能没有完全复位成功后续发初始化命令就乱套了。我曾经在一批物料上遇到过复位时间不够导致花屏的问题把复位拉低时间从10ms改到100ms后问题消失。3.3 fb_ops操作函数实现fb_ops是fbdev框架里驱动必须实现的回调集合。SPI LCD驱动里最重要的三个回调是fb_setcoloreg、fb_imageblit、fb_fillrect以及可选的fb_deferred_io相关回调。对于真彩模式RGB565fb_setcoloreg基本可以留空。但fb_fillrect和fb_imageblit值得优化——它们负责“填充矩形”和“绘制图形”在GUI刷新时被高频调用。如果直接用框架默认的实现每个像素都做一次运算和写显存操作一来效率不高二来显存变更后还得等延迟刷新体验差。一个简单的优化方式是重写fb_fillrect直接对screen_buffer对应的内存区域做memset同时标记该区域为脏。类似地fb_imageblit可以调用通用的fb_sys_imageblit再手动触发一次刷新。static void st7789v_fillrect(struct fb_info *info, const struct fb_fillrect *rect) { struct st7789v_par *par info-par; u32 x, y, w, h; unsigned long flags; x rect-dx; y rect-dy; w rect-width; h rect-height; // 直接操作显存填充 spin_lock_irqsave(par-lock, flags); if (info-fbops-fb_sys_fillrect) info-fbops-fb_sys_fillrect(info, rect); else sys_fillrect(info, rect); spin_unlock_irqrestore(par-lock, flags); // 触发脏区域刷新 fb_deferred_io_mark_dirty(info, rect-dx, rect-dy, rect-width, rect-height); }这段代码里的fb_deferred_io_mark_dirty是延刷机制的核心调用它告诉内核“这片区域有变化等延迟周期后刷新”。这里的锁定方式也需要注意显存操作可能被多个进程同时触发必须保证互斥不然会出现刷屏撕裂。3.4 屏幕刷新任务deferred IO机制详解SPI LCD帧率低如果每一次用户写显存都立刻刷到屏上会产生大量SPI中断和DMA请求CPU开销高到不可接受。fb_deferred_io机制就是为了解决这个问题而存在的。利用fb_deferred_io时驱动需要做到以下几点在fb_info里挂载fb_deferred_io结构体。利用fb_sys_read和fb_sys_write作为默认读写回调。注册一个延迟工作队列周期性地调用fb_deferred_io_work。在fb_deferred_io_work里调用我们的刷屏函数st7789v_update_display。延迟周期是可配置的一般设置在20ms~50ms之间。设20ms意味着任何显存变化最多40ms后就会反映到屏上因为deferred_io的定时器是周期性的对应约25~50Hz的有效刷新率。st7789v_update_display的核心工作是计算出脏区域然后通过SPI把这块区域的像素数据发送给LCD。实际操作中为了让实现简单通常直接整屏刷新不做脏矩形优化。因为SPI低位宽传输本身就是瓶颈脏矩形算法节省的时间远不足以抵消代码复杂度。static void st7789v_update_display(struct fb_info *info) { struct st7789v_par *par info-par; u16 *buf (u16 *)info-screen_buffer; int x, y; // 设置窗口为全屏 st7789v_set_window(par, 0, 0, 240, 320); // 发送像素数据 for (y 0; y 320; y) { for (x 0; x 240; x) { u16 pixel buf[y * 240 x]; // RGB565需要转换成LCD控制器期望的字节序 pixel (pixel 8) | (pixel 8); spi_write(par-spi, pixel, 2); } } }别被我这里的“简化写法”误导实际驱动绝对不会逐像素调用spi_write——那样每个像素都有一次SPI事务开销CPU会被中断淹没。正确做法是准备一个DMA缓冲区把一行甚至半屏的数据凑好再一次性发送static void st7789v_update_display_dma(struct fb_info *info) { struct st7789v_par *par info-par; int y; u16 *src (u16 *)info-screen_buffer; // 逐行发送 for (y 0; y 320; y) { int x; for (x 0; x 240; x) { u16 pixel src[y * 240 x]; par-dma_buf[x] (pixel 8) | (pixel 8); } st7789v_set_window(par, 0, y, 239, y); spi_write(par-spi, par-dma_buf, 240 * 2); } }这里先把每一行的像素从显存拷贝到dma_buf顺便完成字节序转换然后一次性SPI发送整行240x2480字节。这样320行只需要320次SPI事务效率大幅提升。实测9MHz SPI时钟下这种方案整屏刷新耗时约 240×320×2×8 / 9000000 ≈ 136ms对应约7fps的帧率。注意这只是理论值实测加通信开销在130~160ms浮动。如果要用到流畅的GUI动画这个帧率很吃力但对数据展示型应用完全够用。4. 问题排查与性能优化实录4.1 白屏问题排查初始化序列的“隐性”要求白屏是SPI LCD调试中出现频率最高的问题没有之一。如果你上电后屏幕一片白优先排查顺序是背光是否亮白屏说明背光亮了但LCD没有收到有效信号。如果连背光都不亮先查背光GPIO、供电。复位是否正常确认复位时序满足芯片手册要求。有些LCD模块的复位脚上拉了电容拉低时间太短芯片没复位成功。初始化序列是否正确这是最大的坑。网上GitHub上能找到的ST7789V初始化序列大多来自Arduino生态用于3.3V/5V单片机的直接搬到Linux下不一定管用。初始化序列的每个命令都必须对照芯片手册确认尤其注意Sleep Out命令后必须延时120ms以上再发Display On否则白屏概率极高。D/CX脚极性是否匹配很多屏的D/CX定义是“低电平发命令高电平发数据”但有些模块设计成反的。确认你的驱动里对dc_gpio的设置极性是否和模块硬件一致。SPI模式是否匹配ST7789V支持SPI Mode 0CPOL0, CPHA0和Mode 3CPOL1, CPHA1。RK3568 SPI控制器默认Mode 0但有些LCD模块在Mode 0下不稳定尝试改Mode 3。排查白屏时最有效的技巧是拿逻辑分析仪抓SPI波形对比命令字节和手册里的初始化序列。没有逻辑分析仪的话可以临时在驱动里加打印把每次发送命令的寄存器和值打出来和参考代码比对。4.2 花屏与横竖屏方向问题花屏现象分为两类一类是“彩色噪点”通常是因为SPI时钟时序不匹配或供电纹波太大导致的另一类是“图像错位、颜色不对”这通常是显存布局和LCD扫描方向不一致导致的。方向问题本质上是坐标系映射。FrameBuffer显存的坐标永远是“从左到右、从上到下”排列的但LCD控制器的GRAM扫描顺序是可以通过Memory Data Access Control寄存器配置的。以ST7789V为例寄存器36h控制扫描方向值0x00从左到右从上到下横屏值0x60从上到下从右到左90度旋转后竖屏值0xC0从下到上从右到左180度旋转值0xA0从下到上从左到右270度旋转如果你在设备树里配置了rotate 90驱动会在发送像素数据前把显存的坐标映射关系转换成LCD要求的扫描方向。具体实现有两种改初始化寄存器初始化时就把扫描方向固化显存内容不需要额外变换。缺点是设备树改方向要重新发命令。显存坐标重映射在st7789v_update_display里计算坐标变换关系把显存中的[(x, y)]对应到LCD的GRAM坐标。工作量稍大但灵活性高。我在驱动里选的是第二种方式因为客户有横竖屏切换需求运行时动态调整设备树rotate属性需要reboot而显存重映射只需要在驱动里改一个映射函数的四个分支即可。还要特别提醒一句像素格式字节序问题。RK3568的FrameBuffer默认是RGB565内存里小端序存储的0x07E0表示绿色这个16位数值发送到LCD控制器时不同控制器要求的高低位顺序不同。ST7789V是高位在前MSB First所以驱动里要做一次字节交换。如果你发现颜色整体错乱红色变蓝色、绿色固定不对先查这个字节序基本十有八九。4.3 刷新率不足与性能瓶颈分析SPI LCD的刷新率瓶颈非常明确数据是串行传输的总线宽度只有1bit标准SPI是1条MOSI线。以240x320分辨率RGB565计算每帧需要的原始数据量为240 × 320 × 2字节 153600字节 1228800 bit如果在9MHz SPI时钟下工作理论最短传输时间为1228800 / 9000000 0.1365秒 ≈ 136ms也就是说满打满算SPI LCD在这种配置下最高也就7fps左右。如果你还想刷新率更高只有几个方向提高SPI时钟从9MHz改为18MHz甚至28MHz理论上刷屏时间能缩短一半。但要注意SPI走线不能太长接口电平3.3V时高频信号完整性会变差可能需要降低摆率或加端接电阻。改用DMA方式传输前面代码示例就是DMA方式比中断方式省掉大量CPU等待时间能真正跑满SPI时钟。开启Quad SPI模式如果LCD驱动芯片支持QSPI比如ST7789V的IO0~IO3、ILI9341的部分型号支持同时RK3568的SPI控制器也支持Quad模式理论上4线同时传输可以到28fps左右。代价是硬件走线多两根线驱动复杂度翻倍。降低帧率要求对数据展示型应用7fps其实已经流畅。如果只是偶尔刷新数据可以进一步降到2~3fpsSPI用很低频率就行抗干扰能力还更强。4.4 SPI时钟频率不稳定导致的花屏噪声调试过程中遇到过一个典型问题屏幕偶尔出横向彩色条纹看起来像是信号被干扰了。用示波器看SPI_CLK波形发现时钟频率不是稳定的9MHz而是间歇性跳变到13MHz甚至17MHz各种毛刺。排查后定位到两个原因RK3568的SPI控制器时钟源是PPLLPLL调整频率时会短暂停振如果驱动在持续性刷屏正好撞上内核调频调压DVFSSPI时钟就会抖动。SPI总线走线过长且没有串阻信号反射导致采样错误。解决方案也有两个层面软件上固定SPI控制器的时钟源为GPLL显卡PLL或NPLL不让内核DVFS机制调节它。设备树里SPI节点添加assigned-clocks和assigned-clock-parents即可。硬件上在SPI_CLK和MOSI线上串33欧姆电阻降低信号过冲和反射。改版后来不及也可以在驱动里把SPI时钟从9MHz降到6MHz问题也会缓解。4.5 常见问题速查表把几个高频问题做个速查表后续做同类项目可以直接对表排查现象可能原因排查手段解决方案白屏背光亮初始化序列错误 / D/CX极性不对逻辑分析仪对比初始化序列逐条对比芯片手册修正延时和命令屏幕显示倒置或左右镜像GRAM扫描方向配置错误查看寄存器36h的值修改初始化序列或坐标映射函数颜色偏色或红蓝对调RGB565字节序不对显示一个已知纯色画面测试驱动里做高低字节交换显示汉化后文字模糊分辨率配置不对 / 旋转后坐标错位检查设备树rotate和分辨率对照显存映射函数逐项检查刷新时CPU占用率过高未使用DMA传输查看CPU占用率改用DMA deferred_io 整行传输偶发光栅条纹SPI时钟抖动 / 信号完整性问题示波器看时钟波形固定时钟源PLL串阻33欧姆驱动加载失败注册fb失败SPI节点没有正确匹配dmesg打印检查设备树compatible字段和status开机后屏闪一下然后黑屏LCD供电不稳 / 复位不完整检查电源波形和复位时序增加复位延时检查电源滤波电容4.6 性能优化从8fps到14fps的实际调优过程最后分享一个实际调优的案例。客户板卡上用的SPI总线走线大概8厘米经过一个FPC排线在初始9MHz配置下整屏刷新稳定在7fps左右。第一阶段优化把SPI时钟从9MHz提高到18MHz整屏刷新时间从136ms下降到68ms帧率提升到14fps。但出现了偶发性花屏原因是FPC排线带来的信号反射。解决方式是给驱动里的SPI传输增加一个“空闲时钟周期”降低时钟沿附近数据的建立时间压力。这个方案初始发送每个字节之间多等一个时钟周期的操作实际效果等同把有效传输速率降到16MHz花屏问题消失。最终稳定在13fps。第二阶段优化使用fb_deferred_io的脏矩形模式。客户应用场景是表盘界面秒针区域每秒钟刷新一次其他区域基本不变。脏矩形模式下每次只刷新秒针附近的200x20像素区域SPI数据量只有全屏的1/24功耗降低不少。第三阶段优化压缩像素格式到RGB565后把SPI的DMA描述符环形缓冲区调整到8个默认4个确保刷屏时SPI FIFO不空等。这步让帧率又提升了约5%。这三个阶段做完后最终效果是全屏刷新13fps局部刷新30fps以上秒针场景完全流畅CPU占用率低于5%。这组数据对后续做同类项目的朋友有很好的参考意义——SPI LCD帧率低不代表体验差关键是知道瓶颈在哪以及怎么绕开它。5. 底层原理补充FrameBuffer与SPI的数据通路这部分内容虽然属于理论知识但对后续排查问题极有帮助尤其是遇到帧率不对、画面撕裂这类问题时理解数据通路可以帮你快速定位瓶颈。一条显示数据从用户态到屏上大致经过这么几步用户态程序使用mmap把/dev/fb0映射到进程地址空间。程序对映射区做写操作比如画一个点、写一段文字这些数据直接落到内核为其分配的系统内存即screen_buffer。内核缺页异常捕捉到写访问把对应的物理页面标记为脏页。fb_deferred_io定时器到期调用驱动的刷新回调。驱动按一定策略把脏页的数据打包成SPI命令和数据流。SPI控制器通过DMA把数据搬移到内部FIFO并按照配置的时钟和相位输出到总线。LCD控制芯片收到数据更新内部GRAM屏幕内容改变。几个关键环节的瓶颈分析第2步到第3步显存写入速度取决于内存带宽RK3568的内存带宽远大于SPI刷新能力这里不会是瓶颈。第5步到第6步驱动准备数据和DMA搬运存在竞争如果你看到SPI总线在大部分时间处于空闲状态说明驱动处理速度跟不上如果SPI总线一直很忙则是时钟到了极限。第6步到第7步LCD控制器内部有个GRAMSPI数据进来后先写GRAM再自动刷屏这个写入速度往往比SPI传输本身快很多但注意有些低端LCD控制器的GRAM写入速度只有几MHz如果你的SPI时钟超过它可能丢数据。这时候只能降SPI时钟或改用QSPI。还有一个容易忽略的点FrameBuffer显存必须配置为连续物理内存。RK3568默认CMA区域能分配大块连续内存240x320x2字节约150KBCMA肯定足够。但如果分辨率更高、或者系统内存碎片严重dma_alloc_coherent可能失败要在内核参数里加大CMA大小。# 在U-Boot环境变量bootargs中添加 cma256M注意RK3568的CMA默认是128M对于只是给SPI LCD做显存是完全够用的不用动。但如果同一个系统还跑ISP、GPU相关应用就要统一规划CMA大小避免互相抢内存。6. 驱动调试经验与避坑心得6.1 调试工具链的组合打法做驱动开发工具选对了能少走很多弯路。我在这个项目里使用的工具组合是逻辑分析仪抓SPI波形确认时钟、数据、片选时序是否正常。不用买很贵的24MHz采样率的就够用便宜好上手。示波器排查信号完整性问题时必备重点看SCLK信号沿有没有过冲、振铃。串口打印 dmesg驱动开发的基本功但注意在内核panic后串口还能不能输出建议用可靠的串口工具并配置earlycon。调试内核驱动时dev_info打印一定要学会善用。不要觉得打印多会影响性能——调试阶段性能是次要的能看清流程更重要。我习惯在关键节点probe入口、复位完成、初始化完成、刷屏前、刷屏后各放一条打印加上dev_dbg级别的细节打印这样一旦出现问题dmesg就能定位到是哪一步挂了或没执行。还有一个技巧先把屏点亮再优化驱动。“点亮”指的是能显示纯色画面即可不需要完整实现文字、图像、鼠标等功能。用最简单的方式把屏点亮就是先跑通最小闭环后续再扩展功能。我在这个项目里第一步是发一条0x01Software Reset延时后再发一条0x11Sleep Out再看屏幕是否从白色变为黑色——只验证这两步就确认了SPI通路和LCD控制器的基本功能。6.2 设备树改动后的验证方法设备树的坑隐蔽性极强经常是“看起来没问题但就是不起作用”。改完设备树重新编译内核后推荐用以下方法验证# 在板子上查看实际生效的设备树 ls /proc/device-tree/ cat /proc/device-tree/spi2fe610000/status cat /proc/device-tree/spi2fe610000/spi-max-frequency # 查看SPI控制器是否被正确注册 cat /sys/bus/spi/devices/spi2.0/device/uevent cat /sys/bus/spi/devices/spi2.0/of_node如果/sys/bus/spi/devices/下没有spi2.0说明设备树中的spi2节点没被正确解析或status不是okay。这时候检查compatible字段是否和驱动匹配以及reg 0是否和片选通道对应如果是CS1就要reg 1。/proc/device-tree这个路径信息量很大它直接展示了解析后的设备树二进制结构。我之前遇到过status写成了ok而不是okay结果平台就是不认后来就是靠这个路径下排查出来的。6.3 常见Kernel Panic场景与规避裸写FrameBuffer驱动遇到内核panic属于家常便饭最常见的场景是注册fb_info之前没有初始化screen_buffer的物理地址register_framebuffer要求fix.smem_start有效否则用户态mmap会失败或崩溃。分配方式统一用dma_alloc_coherent千万别用kmalloc。用kmalloc分配的缓冲区物理地址不连续用户态mmap后表现为随机花屏或段错误。fb_deferred_io的刷新函数里访问了空指针比如spi设备在remove时还未把screen_buffer释放干净刷新队列还在跑。规范做法是先unregister_framebuffer然后flush_work确保刷新任务退出再释放缓冲区。旋转映射函数越界旋转90度后显存按240宽x320高分配但映射后访问坐标写错就会越界踩内存。这一类的排查方式是打开内核的CONFIG_DEBUG_PAGEALLOC在测试时能及时发现非法内存访问。另外补充一个救命技巧把CONFIG_FRAMEBUFFER_CONSOLE打开。这个选项允许内核终端输出重定向到FrameBuffer设备你在调试阶段即使没有串口也能通过屏幕看到内核启动日志。它对驱动开发调试的作用非常大。注意CONFIG_FRAMEBUFFER_CONSOLE打开后如果FrameBuffer驱动未初始化完成期间内核日志可能丢失而且如果同时使用了串口console两个console会互相干扰输出。调试时建议串口和FrameBuffer console只留一个。7. 写在最后的工程建议7.1 第二个SPI LCD驱动开发项目可以怎么做做完这个项目后我对后续类似的SPI LCD驱动开发有了个清晰的SOP标准作业程序分享出来供参考先确认硬件通路用GPIO点灯的方式验证SPI引脚的IO能力再拿逻辑分析仪抓波形确认SPI主机正常输出最后再上屏。三步循序渐进避免硬件和软件问题混在一起。驱动最小闭环先行只实现“复位初始化单色填充”三个动作半天内能跑通后面再迭代功能。初始化序列从厂商或屏厂获取网上现成的初始化序列和你的具体型号可能不一致务必核对数据手册。设备树改动一次到位把需要用的GPIODC、RST、BL全部在设备树里配好写成”gpio“结尾的属性方便驱动里统一用devm_gpiod_get获取。性能优化按需进行数据显示型应用不需要极速刷新非必要不碰DMA、非必要不碰QSPI复杂度是渐进引入的。7.2 RK3568平台上的额外注意事项最后针对RK3568平台本身再补充几点我在项目中学到的经验电源域问题RK3568外设的IO电源域有多个SPI控制器供电的VCCIO3和VCCIO4要配置正确否则SPI引脚的电平无法匹配LCD模块的工作电压通常是3.3V。这个要看具体开发板的原理图RK3568开发板一般默认3.3V但定制板一定要确认。睡眠唤醒支持如果产品要做低功耗模式驱动需要实现spi_driver的shutdown和remove回调在系统休眠时关背光、发Display Off命令唤醒时重新初始化LCD。这里有个容易踩的坑spi_driver的remove回调要和probe的清理逻辑严格对称否则反复休眠唤醒后系统会内存泄漏甚至panic。U-Boot阶段显示FrameBuffer模式的SPI LCD在U-Boot阶段也可以点亮U-Boot有独立的SPI LCD驱动这样可以实现开机logo从U-Boot平滑过渡到内核避免黑屏间隙。RK3568平台的U-Boot配置起来比内核稍微复杂但体验差异明显如果产品对外观有要求值得投入时间去做。触摸屏联动如果产品带触摸常见的做法是触摸用独立I2C接口与LCD通过设备树固定位置关系。注意触摸屏的坐标系和LCD本身的扫描方向可能是镜像的需要在触摸驱动里做坐标变换才能保证“点到哪显示到哪”。整体来说RK3568上FrameBuffer模式SPI LCD驱动开发并不复杂关键难点集中在硬件信号确认和设备树配置上。只要这两个环节不出问题从零到点亮一块屏两天的时间足够了。希望这篇文章能帮准备做类似项目的同学少踩几个坑尤其是白屏排查和性能优化这两块真的是我拿实际项目时间换来的经验。
返回列表