ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

RK3568 SPI LCD的FrameBuffer驱动实战:从设备树配置到性能优化

RK3568 SPI LCD的FrameBuffer驱动实战:从设备树配置到性能优化 1. 项目概述与FrameBuffer模式选型1.1 这个项目到底在做什么手头有一块RK3568的开发板需要外接一块SPI接口的LCD屏用来做系统状态显示和简单的交互界面。这应该是很多做嵌入式产品的工程师都遇到过的场景主控性能足够强但产品形态不复杂不需要高分辨率屏幕也不需要复杂的图形渲染一块几寸的SPI屏就能满足需求。RK3568是瑞芯微的一款四核A55处理器平时大家拿它跑Linux、跑Qt、做边缘计算网关视频编解码和AI算力是它的强项。但它的原生显示接口是RGB、LVDS、MIPI DSI、eDP这类并行或高速串行接口并没有原生的SPI LCD控制器。这就带来一个问题同样一块屏在单片机上可以用硬件SPI轻松刷起来但在Linux系统下要让它作为一个标准显示设备工作路径就复杂了很多。我选择FrameBuffer模式核心考量是它足够直接。在内核层面FrameBuffer是一个抽象出来的显存设备用户态可以通过/dev/fb0直接操作显存写入像素数据就能在屏幕上显示内容。与DRM/KMS相比FrameBuffer在SPI LCD这种低刷新率、低分辨率的场景下反而更合适不需要复杂的modeset流程也不需要Atomic Commit那套机制驱动路径更短出问题更好排查。当然这不是说DRM/KMS不好。如果屏幕是RGB接口或者MIPI DSI接口走DRM框架是正路。但SPI LCD本身就是一种软性显示设备数据量小、刷新慢FrameBuffer这种朴素的模型反而能把这些特点都照顾到。很多低成本Linux产品比如带屏的充电桩、门禁面板、工业仪表都是用这种方式在做显示稳定性和开发效率都经过验证。1.2 适合谁来参考这个方案这篇文章适合几类读者正在RK3568或其他瑞芯微平台RK3288、RK3399、RV1126等上折腾SPI LCD的嵌入式工程师想在Linux系统下快速让一块SPI屏亮起来、用于产品原型验证的朋友对Linux驱动开发有一定基础但还没完整做过一个显示类驱动想用SPI LCD作为入门项目的开发者如果你完全不了解Linux驱动开发建议先补充一下设备树、平台驱动、字符设备这几块基础否则下文某些描述可能会觉得跳跃。如果你已经写过I2C或SPI的外设驱动那其实驱动的骨架你已经掌握了SPI LCD只是多了一个要往显存里放数据的维度。2. SPI LCD硬件连接与驱动方案整体设计2.1 硬件连接与原理图解读SPI LCD最常见的接口是4线SPI加一组控制引脚。所谓4线就是SCLK时钟、MOSI主出从入、CS片选、DC数据/命令选择。有些屏不带DC引脚那就只能用9位SPI模式地址位表示数据还是命令来区分指令和数据但有DC引脚的屏绝大多数情况下都更好处理。在RK3568上SPI控制器从SPI0到SPI3都有引出各个开发板的引出情况不一样。我用的这块板子把SPI0的引脚功能复用出来了连接方式大概如下LCD引脚RK3568引脚复用功能SCLKGPIO3_29SPI0_CLKMOSIGPIO3_30SPI0_MOSICSGPIO3_28SPI0_CS0DCGPIO3_31普通GPIORSTGPIO4_5普通GPIOBLGPIO4_6普通GPIO背光这里有一个非常关键的细节DC引脚和RST引脚不要接在SPI控制器的其他CS上。有人为了省事把DC接到SPI0_CS1上复用结果发现驱动代码里控制DC时总是会连带影响SPI片选状态调试起来非常痛苦。DC就是纯粹的数据/命令标志用普通GPIO操作是最省心的。另外背光引脚如果支持PWM建议接到带有PWM功能的引脚上这样后续做亮度调节会方便很多。我一开始只把背光接到了普通GPIO上只能做开和关后来发现产品要调亮度只能重新飞线教训比较深刻。2.2 常见SPI LCD面板驱动IC与特性市面上小尺寸SPI LCD屏的驱动IC种类很多常见的有ST7735、ILI9341、ST7789V等。这三类IC的驱动方式大同小异核心差异在于分辨率、初始化序列和部分指令集。这款屏用的是ST7789V支持240x320分辨率RGB565颜色格式是它的默认配置。ST7789V相比老的ILI9341画质更细腻功耗也低一些在小尺寸屏幕里可以说是占据了大半江山。需要注意的是同一型号的模组不同的厂商做出来的初始化序列可能会有微小差异所以不要直接copy网上的初始化数组最好向屏厂要一份针对你这一批次模组的初始化代码。初始化序列这一块有经验的工程师会建议把整个初始化数组放进一个static const数组里一次性通过SPI发送。在SPI LCD驱动里初始化代码的发送时机在fb_probe阶段也就是驱动探测的时候。如果初始化序列不完整屏幕大概率不亮或者显示异常这是最常见的坑之一。2.3 驱动方案对比自己写还是用fbtft在Linux社区里SPI LCD FrameBuffer驱动一般有两个选择。一个是自己从零写一个平台驱动注册fb_info并实现fb_ops另一个是使用内核里的fbtft子系统。fbtft是一个专门为SPI/并行小尺寸LCD编写的FrameBuffer驱动框架它把fb_ops、SPI传输、DMA缓冲区这些公共逻辑都封装好了你要做的事情就是提供init_display函数和set_var设置分辨率/颜色格式函数。对于绝大多数人来说fbtft是性价比极高的选择。不过要注意fbtft没有进mainline内核主线它是一个第三方维护的补丁集。在瑞芯微的SDK内核里有时已经帮忙集成好了有时需要你自己合入。我这边的情况是SDK里已经有fbtft相关的源码路径省了不少事情。如果选择自己写驱动工作量主要在三块fb_ops里的fb_fillrect、fb_copyarea、fb_imageblit三个函数的实现或者直接交给内核默认的cfb_fillrect等通用函数fb_var_screeninfo和fb_fix_screeninfo的填充以及最关键的数据刷新路径。这个路径通常是用户态写显存 -fb_write或fb_ioctl触发刷新 - 驱动把显存里的数据打包成SPI事务发送到LCD面板。自己写的优劣势非常清晰灵活性极高想怎么优化SPI时序都行但工作量也确实不小。fbtft则是稳框架帮你处理了很多边界情况而且网上资料多遇到问题容易搜到答案。3. 设备树配置与驱动实现3.1 设备树节点配置详解设备树是RK3568平台上所有外设的户口本SPI LCD的节点配置是否合理直接决定了驱动能不能正常工作。下面给出一个我实际用过的设备树片段重点部分逐一解释。spi0 { status okay; pinctrl-names default; pinctrl-0 spi0_clk spi0_miso spi0_mosi spi0_cs0; spilcd: st7789v0 { compatible sitronix,st7789v; reg 0; spi-max-frequency 20000000; rotate 90; bgr 0; fps 30; buswidth 8; dc-gpios gpio3 RK_PB7 GPIO_ACTIVE_HIGH; reset-gpios gpio4 RK_PB1 GPIO_ACTIVE_LOW; backlight lcd_backlight; status okay; }; };首先说spi-max-frequency。这个参数很多新手不知道该怎么定它的意思是SPI SCLK的最高时钟频率。ST7789V的数据手册上写的是最大支持到大概100MHz左右但这是理论极限。实际调试时建议从低往高调先用1MHz把屏幕点亮确认初始化流程没问题再逐步往上提。我最终稳定在20MHz再高就会出现花屏或边缘发虚的情况这与PCB走线、杜邦线长度、电源质量都有关系。rotate参数是fbtft框架定义的表示屏幕显示方向。ST7789V模组本身有物理方向通常是排线在下方如果产品安装需要旋转90度可以在这里设置。注意这个rotate在fbtft框架里不是简单改一个标志位它会重新计算xres和yres所以调的时候要确保和面板自身的MADCTL寄存器设置匹配。dc-gpios和reset-gpios是必须在设备树里明确指定的。fbtft驱动会读取dc-gpios作为数据/命令选择引脚读取reset-gpios做硬件复位。上电时序上RK3568 GPIO输出默认电平需要特别注意如果reset引脚默认输出低电平面板会一直处于复位状态屏幕必然是黑的。我的做法是在驱动probe阶段把reset拉高延时50ms再拉低延时100ms再拉高确保面板完成上电复位。3.2 内核配置与fbtft注册流程设备树只是描述硬件真正让屏幕亮起来还需要内核配置和驱动代码联动。在RK3568的SDK内核源码中底下的路径一般长这样drivers/staging/fbtft/ ├── fbtft-core.c ├── fbtft-io.c ├── fbtft-bus.c ├── fb_st7789v.c └── ...如果你的SDK里没有fbtft目录你需要从网上找到fbtft补丁合入内核。合入后开启以下内核配置项CONFIG_FBy CONFIG_FB_TFTy CONFIG_FB_TFT_ST7789Vy然后重新编译内核。这个过程中最容易踩的坑是CONFIG_FB没有打开。RK3568的SDK默认倾向于把DRM/KMS打开FrameBuffer子系统甚至可能没被编译进去。CONFIG_FB没打开的情况下fbtft驱动即使编译成功也无法注册fb_info表现为设备树节点都正常解析了但/dev/fb0就是不出现。fbtft的probe流程大致如下解析设备树节点获取dc-gpios、reset-gpios、rotation等属性调用fbtft_framebuffer_alloc分配fb_info结构体设置fb_var_screeninfo分辨率、颜色格式、行像素数调用驱动定制的init_display发送初始化序列注册fb_info/dev/fb0出现在系统中这里有一个细节值得留意fbtft在init_display阶段会通过SPI发送一堆初始化命令这个过程是在probe阶段同步完成的。如果SPI时钟设置太高、或者DC引脚控制时序不对初始化序列发出去但面板不响应整个probe也不会报错只有等你往/dev/fb0里写数据时才发现屏幕没反应。3.3 自研驱动的关键代码路径解析如果你的场景比较特殊fbtft不好使比如要同时驱动多个SPI LCD、或者要在极低内存环境下运行那自研驱动就是唯一选择。这里我把自己写过的一个简化版驱动核心代码分享出来这个框架也适用于其他SPI接口设备。先看fb_ops中最重要的刷新函数static ssize_t fb_write(struct fb_info *info, const char __user *buf, size_t count, loff_t *ppos) { unsigned long p *ppos; u32 screen_size info-screen_size; u32 xres info-var.xres; u32 yres info-var.yres; int ret; ret fb_sys_write(info, buf, count, ppos); if (ret 0) return ret; lcd_update_screen(info, 0, 0, xres, yres); return ret; }fb_sys_write是内核提供的通用函数它会把用户态的数据拷贝到info-screen_base这个内存区域。之后必须自己触发一次刷新把screen_base里的数据发送给LCD。这里最容易犯的错误是只拷贝了数据忘了触发刷新结果屏幕上什么都不变。lcd_update_screen的实现大致是把显存中的RGB565像素逐行打包然后通过SPI发送给面板static void lcd_update_screen(struct fb_info *info, int x, int y, int w, int h) { u16 *pixel (u16 *)info-screen_base; u8 buf[320 * 2 4]; int i, j; lcd_set_addr_window(info, x, y, w, h); lcd_set_dc(info, 1); for (i 0; i h; i) { int offset (y i) * info-var.xres x; for (j 0; j w * 2; j) { buf[j 4] ((u8 *)pixel[offset])[j]; } buf[0] 0x00; buf[1] w * 2 8; buf[2] w * 2 0xff; buf[3] 0x00; spi_write(lcd_spi_device, buf, w * 2 4); } }lcd_set_addr_window是往面板发送设置窗口的命令告诉它接下来要往哪个区域写数据。这块如果不设置面板会按顺序刷新整屏相当于每帧数据量固定为320x240x2字节浪费一半以上带宽。我用SPI写操作的带宽大约在20Mbps刷一屏全彩数据大概需要70多毫秒设置窗口后只更新变化区域同样的时间可以刷更多局部内容。4. 驱动调试过程中的常见问题4.1 屏幕完全不亮怎么办这是我调试过程中最纠结的一个阶段代码写完了设备树配好了/dev/fb0也出现了但屏幕就是不亮。排查思路我总结成了下面这张速查表现象可能原因排查方式屏幕背光亮无任何显示初始化序列未正确发送用示波器抓DC引脚和MOSI波形屏幕完全黑屏背光也不亮背光GPIO未配置检查GPIO复用是否正确直接echo 1测试屏幕亮起但全白颜色格式不匹配或MADCTL设置错误检查RGB565的字节序调整bgr参数屏幕亮起但花屏SPI时钟过高或电源波动降低spi-max-frequency闪一下就灭了reset引脚时序不对检查复位时序背光不亮这个问题尤其常见。RK3568的GPIO引脚复用非常灵活但默认状态下可能是复用成其他功能了。比如我用的背光引脚默认复用成了I2C功能设备树里如果没有用pinctrl明确改回GPIO模式用户态怎么操作都不起作用。我后来是在设备树中显式添加了如下节点gpio4 { lcd-backlight-pin { rockchip,pins 4 RK_PB2 RK_FUNC_GPIO pcfg_pull_none; }; };4.2 显示方向不对或者颜色不对ST7789V的显示方向和颜色排列非常容易搞混。rotate参数控制的是驱动层的数据排列方向而面板内部还需要通过MADCTL寄存器来设置扫描方向。如果两者不一致会出现屏幕显示内容是正确的但整体转了90度这种奇怪现象。颜色不对的问题更多常见的是红蓝互换。RGB565的字节序有两种一种是高字节在前big endian一种是低字节在前little endian。ST7789V默认的是高字节在前也就是一个像素0xRRRRGGGGBBBB在总线上先发高字节。如果你的显存数据是按little endian组织的发送出去就需要做个字节交换。这就是fbtft驱动里bgr参数的作用。我自己踩过这个坑fbtft驱动按照默认方式把RGB565数据直接发给面板结果屏幕上的蓝色都变成了红色红色都变成了蓝色。调整设备树中的bgr 1后恢复正常。如果你的屏显示的颜色整体偏色优先排查这个参数。4.3 SPI时序问题如何定位SPI时序是SPI设备驱动中最折磨人的一个环节。RK3568的SPI控制器具有完善的硬件FIFO和DMA支持但正因如此时序问题反而是最难查的。用示波器抓取SPI波形的时候有几个关键观察点时钟空闲电平。SPI mode 0要求SCLK空闲为低电平Mode 3要求空闲为高电平。ST7789V通常工作在Mode 0或Mode 3具体看模组规格。如果mode配置错误数据线和时钟线的采样点完全错位屏幕上就会是雪花一样的乱码。CS信号的建立时间。CS拉低之后第一个SCLK沿之前需要一定的建立时间ST7789V一般要求在50ns左右。RK3568的SPI控制器这个参数可以通过设备树或驱动调整。DC引脚与数据的先后。DC必须在前一个字节的最后一个时钟沿之后、下个字节的第一个时钟沿之前切换状态否则面板会把数据误判成命令。实际调试时我习惯先用逻辑分析仪SPI解码器看几帧波形确认命令和数据确实按照预期分布在总线上再用示波器看模拟电平是否合格。如果逻辑分析仪显示数据正确但屏幕就是不亮多半是电平方面的时序问题这就需要示波器出马了。5. 性能优化与使用经验5.1 SPI时钟频率的选择策略SPI LCD的刷新率瓶颈不在CPU算力而在SPI总线带宽。20MHz的SPI时钟每秒钟可以传输约2.5MB数据。240x320的RGB565整屏数据是150KB理论上每秒可以刷16帧左右。但这是在纯数据搬运的情况下实际的刷新率远低于这个值因为还要考虑命令发送、地址窗口设置、寄存器配置等开销。我的经验是优先保证显示稳定性再考虑性能提升。先从10MHz开始确认屏幕工作正常然后每5MHz一档往上加每档都要做高温拷机测试。我曾经把SPI调到33MHz当时刷新效果很好但设备在高温环境下连续跑两个小时开始花屏最后降到20MHz才稳定下来。这个教训说明SPI时钟不是越高越好要综合散热、电源、PCB布线等多方面因素。5.2 局部刷新让刷新率翻倍的关键如果整屏刷新只有十几帧对于显示时间、数字这类信息是够用的但要做简单的动画效果就会觉得卡顿。这个问题可以通过局部刷新来解决。ST7789V支持通过CASET列地址设置和RASET行地址设置命令来指定刷新窗口。只要修改了屏幕的某个区域就只发送这块区域的数据其他区域保持不变。比如一个数字时钟界面只有中间那块数字在变化使用局部刷新后实际刷新率可以从10fps直接提升到40fps而这个提升不需要任何硬件改动只是驱动层面的优化。要注意的是局部刷新对显存同步有要求。如果用户态在一次write操作中更新了多个不相邻的区域驱动层面要么分别刷新要么合并成一个大的bounding box。分别刷新开销小但代码复杂合并成bounding box简单但可能有额外数据量。我实际产品中采用的是脏矩形合并策略先记录所有被修改的小区域再计算它们的外接矩形一次刷新这个外接矩形。5.3 用mmap直接操作显存/dev/fb0的write操作有一个性能问题每次写入都会经过文件系统和驱动层的拷贝对于高频刷新的场景是很不划算的。比较高效的方案是使用mmap把显存直接映射到用户空间。int fb_fd open(/dev/fb0, O_RDWR); void *fb_mem mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);之后就可以直接对fb_mem数组进行像素操作不需要拷贝。但注意mmap映射只完成了写显存这部分要触发实际刷新还要调用FBIO_WAITFORVSYNC或FBIOPAN_DISPLAY之类的ioctl或者在驱动的刷新策略中配合定时器周期性刷新。在RK3568平台上我建议的做法是驱动里加一个内核定时器每隔20ms检查一下显存是否有内容更新有的话就触发刷新没有就跳过。这种轮询脏检查的方式实现起来最简单也最容易满足大多数场景。真正高性能的做法是用户态显存写入时主动调用一个自定义ioctl通知驱动去刷新避免轮询开销但这需要同时修改驱动的用户态接口。5.4 背光调节与省电策略SPI LCD在嵌入式产品中通常不会一直满亮度运行背光调节也算一个不大不小的需求点。如果背光引脚支持PWM建议把它接到RK3568的PWM控制器上。设备树里可以用backlight节点关联一个PWM背光然后在用户态通过/sys/class/backlight/xxx/brightness来调节。这个方案的好处是标准Qt和LVGL等图形框架都原生支持。如果不支持PWM只能用GPIO模拟背光调节那体验会差很多只有开和关两个状态。这种方案建议在FPGA项目里用Linux产品还是优先选PWM。省电方面有一个技巧ST7789V支持进入Sleep模式进入后电流消耗几乎为零。当系统检测到长时间没有显示更新时可以通过ioctl让驱动发送Sleep命令唤醒时再发Wake命令。这个休眠唤醒过程大约需要120ms左右的时间对于低频操作的产品完全可以接受。6. 与屏幕无关但影响很大的周边问题6.1 RK3568设备树里的pinctrl复用冲突RK3568的引脚复用特别灵活但这也是它最容易出问题的地方。一个引脚可以复用到I2C、UART、SPI、GPIO等多种功能如果驱动初始化顺序不对之前某个驱动占用了引脚功能后面再想用GPIO操作就会失败。排查方式很简单检查内核日志中是否有pin conflict相关的报错。我调试时遇到过一个问题显示驱动已经探测成功了但DC引脚始终拉不高最后发现是内核中某个I2C驱动先把这片引脚复用成了I2C功能。解决办法是在设备树中给LCD驱动增加pinctrl-0属性显式声明这些引脚为GPIO功能高优先级覆盖其他驱动的复用请求。6.2 显示内容中文与字库的设计思路SPI LCD的分辨率不高通常也不需要很复杂的字体渲染。嵌入式Linux下显示中文有几种路径在用户态用freetype渲染适用于Qt/LVGL这类GUI框架在驱动层集成中文字库适用于最简单的字符显示方案预先生成点阵字模烧录到flash里适合极其精简的场景由于我们用的是FrameBuffer最灵活的做法是用户态渲染后往/dev/fb0写数据。把需要的汉字点阵拔出来生成一个数组再通过绘图函数把点阵放到显存里。这样的好处是不需要额外依赖库缺点是字库管理要自己维护。如果显示内容不大几十个汉字就够用了这种做法很干净。6.3 与触摸屏配合时的坐标映射很多SPI LCD模组会搭配电阻触摸屏或电容触摸屏。注意触摸屏通常有自己的I2C接口控制芯片如GT911、FT6236不要试图把触摸信号从SPI里读出来它们是两套完全独立的系统。触摸坐标和屏幕显示坐标映射问题在旋转显示方向后尤其突出。比如屏幕rotate 90后显示坐标系随之旋转但触摸芯片仍然报告物理坐标这时用户态就需要自己做坐标变换。这个映射关系在设备树中一般没有统一的标准不同触摸芯片实现方式各异。我的做法是在应用层完成变换这样不用改驱动产品适配不同屏时也方便调整。7. 写在最后的经验和建议这个项目做下来最大的感受是SPI LCD驱动本身不难难的是周边一系列细节的配合。设备树pinctrl要去查SPI时钟要根据实际硬件反复调DC/RST的上电时序要严格参照面板规格书显存格式和面板内部的寄存器设置必须完全一致。任何一个环节忽略都会以屏幕不亮或显示异常这种粗粒度现象反馈出来排查起来相当消耗耐心。给大家一个非常实用的建议在开始写驱动之前先用一个裸机例程或单片机的SPI代码把屏点亮。这个步骤看起来多此一举实际上能帮你把初始化序列是否正确硬件连接是否有误这两类核心问题提前排除掉。如果单片机上都点不亮就别指望Linux驱动能帮你解决了。另外调试驱动时要养成一个习惯每次改动只动一个变量。比如优化刷新率时只调spi-max-frequency其他都不变调显示方向时只改rotate或MADCTL字段其他都不改。这样出问题时你可以快速定位是哪一步引入的。我之前有一段时间同时调整了SPI时钟和初始化序列结果屏幕花屏后花了两天才找到真正原因效率极低。如果你想在这个方案上继续扩展可以考虑的方向包括把FrameBuffer替换成DRM/KMS以支持更多现代图形特性、增加双缓冲以消除撕裂感、或者通过fbdev接口让OpenGL/EGL直接渲染到这块小屏上。这些方向都建立在基础驱动能正常工作之上先把基础打扎实后面的路会顺很多。
返回列表