
RK3568 FrameBuffer模式SPI LCD驱动开发这周终于把手上这块RK3568核心板上的SPI小屏点亮了前前后后折腾了差不多五天中间踩了不少坑包括白屏、颜色错乱、系统启动阶段刷新率太低、SPI时序总在不经意间出问题。趁着印象还热乎把从方案选型、设备树配置到驱动代码实现、调试优化的整个过程整理出来给正在做RK3568或类似平台SPI LCD驱动开发的朋友一个参考。先交代一下背景板子是RK3568配的屏幕是常见的ST7789V控制器的1.3寸圆屏分辨率240x240RGB565颜色格式。这种小屏在HMI面板、桌面摆件、侧屏仪表这类场景里非常多成本低接线简单不需要MIPI也不需要RGB888并口只要四根SPI线就能出画面。而所谓FrameBuffer模式就是让这块LCD在Linux下以/dev/fb0的形式暴露出来上层不管是Qt还是纯C语言的自绘程序往这块内存区域里写像素驱动负责把内容刷到物理屏幕。这个方案比走DRM/KMS整套流程要轻量得多在小分辨率屏幕上完全够用。1. 方案选型为什么选FrameBuffer而不是DRM/KMS1.1 先搞明白FrameBuffer模式能解决什么问题RK3568是一颗带GPU、带VPU的处理器官方SDK默认的显示链路是DRM/KMS正常接HDMI、eDP、MIPI DSI、RGB接口都没问题。但是当你想接一颗SPI总线的TFT小屏时情况就变了。SPI屏本身带宽有限如果硬塞到DRM框架里你得写一个drm_panel驱动的同时还要处理drm_connector、drm_encoder、drm_display_mode这一堆对象搞一圈下来发现大部分属性对这个240x240的小屏幕根本没有意义。FrameBuffer模式的核心思路是绕过DRM的复杂抽象直接用Linux内核里最经典的fbdev子系统。驱动只需要注册一个platform_driver在probe里填充fb_info结构体实现fb_ops里面的几个关键回调比如fb_fillrect、fb_imageblit、fb_copyarea然后把显存地址暴露给用户态。上层调用write()或者直接mmap这块显存驱动在刷新时把这些像素数据通过SPI控制器发出去。打个比方DRM像是给大型企业做ERP系统各种权限、审批、跨部门协作都给你安排得明明白白FrameBuffer就是个体户的记账本虽然功能少但胜在简单直接一眼看到底。实际选型时要考虑的是如果产品形态确定只需要一块SPI小屏并且不打算在多个显示接口之间做热插拔切换那FrameBuffer模式是性价比最高的方案。反过来如果这颗RK3568以后可能要同时接HDMI和LVDS那就别用FrameBuffer了老老实实走DRM把SPI屏作为panel挂到新的dimension框架里去。1.2 几个备选方案的对比在确定用FrameBuffer之前我梳理了三条路线方案工作量系统集成度适用场景官方DRM panel驱动较大高与HDMI/MIPI统一管理多屏并存、需要modetest等工具fbtft框架加载已支持屏小中依赖内核配置快速出prototype、验证屏幕自研fbdev驱动中等较高独立模块不干扰其他链路商用产品、需要深度定制刷新逻辑fbtft在早期内核里是很火的一套框架它自带了许多常见屏的初始化序列比如fb_ili9341、fb_st7789v、fb_ili9488等。但是在RK3568的SDK上我发现一个问题SDK默认的内核配置里可能不会保留/drivers/video/fbdev路径下的fbtft相关编译选项而且fbtft在内核里长期处于staging状态接口变动比较频繁。如果你用的是Rockchip官方BSP内核与其去把fbtft的Kconfig路径找出来加上不如直接写一个自包含的fbdev驱动依赖面最小出问题也好排查。最后我选择了自研fbdev驱动Kconfig里只需依赖CONFIG_FB和CONFIG_SPI_ROCKCHIP不再牵扯任何DRM相关的东西。2. SPI LCD硬件的连接与信号工作原理2.1 引脚定义与连接方式ST7789V这一类SPI屏模组对外引出的核心引脚一般就是GND、VCC、SCK、SDAMOSI、RES、DC、CS、BLK这八个。部分型号还有一个MISO脚用于读寄存器不过刷屏场景基本用不到读操作不接也没事。RK3568的SPI控制器数量很足6路SPI够用。我这里用的是SPI3设备树里的节点是spi3。硬件上我做了这样的连接SCK - GPIO3_B1 / SPI3_CLKSDA - GPIO3_B0 / SPI3_MOSICS - GPIO3_C0 / SPI3_CS0DC - GPIO0_C5普通GPIORES - GPIO0_C6普通GPIOBLK - GPIO0_C7可PWM调光这里有一个非常容易被忽略的点DC脚一定要选一个在设备树里没有被复用成其他功能的GPIO并且确认这个引脚没有被RK3568的IOMUX默认占用。我一开始选了GPIO3_C2结果那个脚被复用成UART5的TX导致DC信号一直拉不起来屏幕怎么初始化都不出画面。后来改用GPIO0_C5干净利落。2.2 像素数据是怎么通过SPI传进屏幕的ST7789V这类控制器内部维护一整块GRAM尺寸和分辨率一一对应240x240的屏每个像素是16bitRGB565那一整帧数据就是2402402115200字节。你通过SPI写像素数据的本质是先把地址指针定位到GRAM里的某个坐标然后连续写入颜色数据控制器会自动把数据填进GRAM。这里必须说清楚DC引脚的作用。SPI通信只分数据位没有天然区分“这是命令”还是“这是数据”。屏幕使用DC这根线的电平来判断当前SPI总线上传输的是命令还是数据DC为低时字节被解释为控制命令DC为高时字节则写入GRAM或者作为命令参数。驱动里每一次操作前要先拉高或拉低DC再拉起CS完成一个事务后释放CS。用240x240屏举例如果你要刷一整帧RGB565数据SPI上实际要发的比特数是115200*8921600个bit。这里还没算命令前缀因为直接连续写GRAM的话只需要开头发一条RAMWR命令之后全是数据。2.3 带宽算一算别对刷新率有不切实际的期待RK3568的SPI控制器最高可以跑到大概50MHz但实际要看PCB走线和屏模组支持的极限。为了稳妥我把SPI时钟先配置成20MHz也就是每秒能传20Mbit。按照上面的计算一帧数据921600bit理论上每秒最多能刷21帧左右。这个数字很有参考价值如果你用这个屏做视频播放那帧率铁定不够但如果只是显示仪表盘、状态图标、文字刷新这种低频场景完全够用。我在实际测试中纯CPU搬运刷新一帧静态图耗时大约50ms逐行局部刷新小区域则基本无感。3. RK3568设备树配置与内核编译3.1 设备树节点编写设备树里需要做的核心事情有三件配置SPI控制器节点、配置GPIO作为DC/RES/BL、设置背光节点。下面是精简后的设备树片段我直接把相关节点写在根节点下然后通过spi3和pinctrl将它们关联起来。spi3 { status okay; pinctrl-names default; pinctrl-0 spi3_clk spi3_miso spi3_mosi spi3_cs0; max-freq 20000000; st7789v0 { compatible myvendor,st7789v; reg 0; spi-max-frequency 20000000; dc-gpios gpio0 RK_PC5 GPIO_ACTIVE_HIGH; reset-gpios gpio0 RK_PC6 GPIO_ACTIVE_HIGH; backlight-gpios gpio0 RK_PC7 GPIO_ACTIVE_HIGH; rotation 0; buswidth 8; bgr 0; }; };有两点需要特别注意。第一spi3_miso这个pinctrl必须是存在的即使你的屏没有MISO线RK3568的SPI控制器在某些传输模式下会检测接收FIFO缺失该引脚配置可能导致DMA传输挂死。我实测如果不配miso引脚开启SPI DMA传输时偶尔会卡死。第二st7789v0这个子节点的reg必须和CS片选号对应SPI3的CS0对应reg0如果接的是CS1则写成reg1。3.2 背光控制方式的选择有些屏模组的BLK引脚可以直接接3.3V常亮但产品上一般都需要调节亮度。RK3568提供了PWM控制器最简单的方式是在设备树里加一个pwm-backlight节点。我用的是内核pwm-backlight的通用方案backlight: backlight { compatible pwm-backlight; pwms pwm5 0 1000000 0; brightness-levels 0 32 64 128 192 255; default-brightness-level 5; status okay; };注意pwm的频率设置。上面例子里period为1000000ns即1kHz这个频率对大多数LED背光来说都够用肉眼看不到频闪。如果你把频率设到几十Hz亮度和调节会有明显跳动感。3.3 内核配置选项设备树准备好了接着就是内核编译选项。在RK3568 SDK内核里需要确认以下配置CONFIG_FB必须开启这是fbdev的核心CONFIG_FB_ROCKCHIP如果SDK存在这个选项要根据情况关闭因为它可能和自研驱动争用fb注册接口CONFIG_SPI_ROCKCHIPSPI控制器驱动CONFIG_BACKLIGHT_PWM如果用了pwm-backlightCONFIG_FB_SYS_FILLRECT、CONFIG_FB_SYS_COPYAREA、CONFIG_FB_SYS_IMAGEBLIT、CONFIG_FB_SYS_FOPSfbdev的软件加速和文件操作接口这些一定要开否则编译时会发现缺少依赖还有一个容易被坑的地方是CONFIG_DRM_FBDEV_EMULATION。RK3568 SDK里DRM默认开启它会创建一个虚拟的fbdev模拟层如果你同时加载自研SPI LCD的fbdev驱动两者会争抢/dev/fb0这个编号。我的做法是把DRM相关的fbdev emulation关闭然后让自研驱动独占fb0。如果产品上还需要HDMI那这个方案就行不通了需要给自研驱动分配不同的fb编号或者在DRM框架下注册为第二个connector。4. Framebuffer驱动开发实战4.1 驱动框架自研驱动的核心结构先给出这个驱动最主要的几个组成部分platform_driver、fb_info、backlight操作、SPI写屏函数。整个驱动的生命周期是这样的probe阶段从设备树读GPIO、SPI配置申请GPIO初始化屏幕控制器注册fb_infoopen/release阶段处理用户的打开与关闭管理背光开关fb_ops阶段实现fillrect、copyarea、imageblit以及最关键的fb_fbdev_write操作remove阶段注销fb_info释放GPIO和内存关键代码如下框所示我做了删减但保留了核心逻辑。static int st7789v_probe(struct spi_device *spi) { struct fb_info *info; struct st7789v_par *par; int ret; par kzalloc(sizeof(*par), GFP_KERNEL); ... par-dc_gpio devm_gpiod_get(spi-dev, dc, GPIOD_OUT_LOW); par-reset_gpio devm_gpiod_get(spi-dev, reset, GPIOD_OUT_HIGH); par-bl_gpio devm_gpiod_get_optional(spi-dev, backlight, GPIOD_OUT_HIGH); spi-mode SPI_MODE_0; spi-bits_per_word 8; spi_setup(spi); /* 初始化控制器序列 */ st7789v_init_lcd(spi, par); info framebuffer_alloc(sizeof(struct st7789v_par), spi-dev); ... info-fbops st7789v_fbops; info-screen_base (char *)par-vmem; info-screen_size ST7789V_WIDTH * ST7789V_HEIGHT * 2; ret register_framebuffer(info); ... }这个结构看起来简单但有几个粗糙的地方要注意。screen_base指向的是一块内核通过kmalloc申请的连续物理内存对于240x240 RGB565的屏幕只需要115200字节kmalloc完全够用。如果你的屏分辨率更高比如800x480那就要用dma_alloc_coherent分配连续且适合DMA传输的内存否则SPI DMA刷屏时会出cache一致性或者物理不连续的问题。4.2 fb_ops的实现要点fb_ops是fbdev驱动的灵魂。系统底层和图形库调用的所有画点、画线、填充操作最终都会分发到这几个函数里。对于驱动开发最土但最稳妥的写法是完全依赖内核的软加速函数也就是将fb_sys_fillrect、fb_sys_copyarea、fb_sys_imageblit和fb_sys_read、fb_sys_write直接填入fb_ops结构体。实际写屏动作发生在哪里这取决于你选择的刷新策略。最简单的实现是每次任何图形操作完成后都整屏发送一次但这种做法在240x240分辨率下也能接受只是帧率低。稍微优化一点的思路是维护一个dirty区域只有在目标区域发生变化时才把对应矩形发送到屏幕。这个阶段我使用了fbtft里刷屏的思路实现了一个st7789v_update_display(start, end)函数它只刷新从start到end之间的行数据。关键点在这里用户在fb上通过mmap直接修改显存时fbdev本身并不知道内容变了。这就是为什么很多早期fbdev驱动在画面更新时会有残影因为没有任何机制去通知驱动刷新。一个可行的补救是周期性用内核定时器触发全刷或者脏矩形扫描。但在Linux fbdev框架下更常见的做法是让上层应用写入后主动调用ioctl的FBIOPAN_DISPLAY或者通过c2p/fb_ops的pan_display回调来触发刷新。我最终在驱动里保留了三种刷新路径触发方式实现策略适用场景fb_fillrect/fb_imageblit等操作后自动刷新直接将脏矩形发送到屏幕Qt或DMA-BUF方式绘图mmap直接写显存后调用ioctl通过FBIOPAN_DISPLAY触发刷新自研绘图程序内核定时器定时全刷每200ms检查一次脏标记调试期兜底4.3 初始化序列是最大变量SPI LCD驱动最难的部分不是框架而是屏幕控制器的初始化序列。不同厂家、不同批次甚至同型号不同版本的屏初始化寄存器配置都可能不一样。ST7789V是一个相对规范的产品官方手册有余辉抑制、伽马校正、显示方向、颜色深度等一堆寄存器。一段能用的初始化序列一般包含软件复位、退出睡眠模式、设置像素格式为RGB565、设置显存访问控制字控制扫描方向和RGB/BGR顺序、设置伽马曲线、开启显示。这里举一个我实际验证过的关键片段static const uint8_t st7789v_init_cmds[] { 0x01, /* SWRESET */ 0x11, /* SLPOUT */ 0x3A, 0x05, /* COLMOD, 16bit RGB565 */ 0x36, 0x00, /* MADCTL, 正常方向RGB 顺序取决于屏模组 */ 0xB2, 0x0C, 0x0C, 0x00, 0x33, 0x33, /* PORCH 设置 */ 0xB7, 0x35, 0xBB, 0x19, /* VCOM */ 0xC0, 0x2C, /* LCMCTRL */ 0xC2, 0x01, 0xC3, 0x12, 0xC4, 0x20, 0xC6, 0x0F, 0xD0, 0xA4, 0xA1, 0xE0, 0xD0, 0x04, 0x0D, 0x11, 0x13, 0x2B, 0x3F, 0x54, 0x4C, 0x18, 0x0D, 0x0B, 0x1F, 0x23, /* 正极性伽马 */ 0xE1, 0xD0, 0x04, 0x0C, 0x11, 0x13, 0x2C, 0x3F, 0x44, 0x51, 0x2F, 0x1F, 0x1F, 0x20, 0x23, /* 负极性伽马 */ 0x21, /* INVON 反色显示 */ 0x29, /* DISPON */ };排错的时候我发现一个现象同一个初始化序列在别人家STM32上正常放到RK3568上就花屏。原因多半是SPI时序不同。STM32驱动往往在每个字节之间穿插一些延时而RK3568的CPU主频高SPI传输连续屏幕控制器某些寄存器在连续写入时反而会吃不到参数。我的处理方式是对于涉及多字节参数的寄存器在命令后加入1ms左右的延时尤其是sleepout之后必须至少等待120ms否则后续设置全部无效。4.4 SPI传输方式的选型PIO还是DMARK3568的SPI支持两种数据传输路径CPU手动读写FIFO的PIO模式以及利用DMA引擎自动搬运的DMA模式。PIO模式代码简单对系统依赖少缺点是在高速传输时CPU占用很高。刷一张115200字节的图PIO模式下CPU几乎全程参与这在做HMI时会干扰其他任务的实时性。DMA模式下驱动程序只需要把显存地址告诉DMA控制器然后等待传输完成中断。这样CPU在刷屏的大部分时间里是空闲的。代价是代码复杂需要处理dma_map_single或提前用dma_alloc_coherent分配内存还要写complete回调来唤醒等待队列。我实测的对比数字可以给你参考传输方式刷一帧耗时 (20MHz SPI)CPU占用PIO模式约55ms接近100%DMA模式约52ms不到10%传输耗时差异不大的原因在于SPI速率本身是瓶颈。但CPU占用差异非常显著所以产品化驱动必须上DMA。如果选择DMA需要在内核里确认CONFIG_DMA_ENGINE和CONFIG_SPI_ROCKCHIP_DMA这两个配置项是打开的。Rockchip SPI的DMA支持有对应的device tree属性通常只要在SPI控制器节点里配置dmas和dma-names即可dmas dmac0 3, dmac0 4; dma-names tx, rx;5. 调试、性能优化与常见问题5.1 白屏问题定位的一线经验白屏是SPI LCD驱动里最高发的问题。白屏意味着背光亮了但控制器没有进入显示状态或者GRAM里的数据全是0xFF。定位方法按顺序做第一步用示波器抓DC和CS的波形。如果DC信号在发送命令时没有正确拉低后续所有寄存器写操作都会被控制器当成显示数据初始化必然失败。抓DC波形能一眼看出问题。第二步检查复位时序。屏幕的RES引脚要先拉低至少10us然后再拉高拉高后等待120ms再发送初始化命令。有些驱动里为了省事没有在probe路径里保证这个时序导致控制器还处于异常状态。第三步怀疑初始化序列不匹配。最直接的办法是找一个已知能亮的STM32工程把它的初始化数组抄过来对比。我前面给出的序列不一定适配所有模组特别是某些厂家把偏压电压、VCOM值改了直接用原厂序列会更稳。第四步查看内核打印。在st7789v_init_lcd里加dev_info把每个命令和参数打印出来比对数据手册上的要求排查是否某条命令的长度写错。5.2 颜色错乱是RGB/BGR顺序问题屏幕显示出现红蓝互换九成是MADCTL寄存器里的RGB位没配对。ST7789V的0x36寄存器第3位控制RGB/BGR顺序。0x00是RGB0x08是BGR。有些模组的排线把R和B交换了驱动里必须设置成BGR才能正确显示。我的处理是在设备树里增加一个bgr属性驱动解析这个属性后决定往0x36寄存器写入0x00还是0x08。这样硬件改版时不需要改代码只动设备树就行。另外颜色错乱也可能来自fb_info里设置的红绿蓝偏移量。如果你用fbdev自带的真彩格式必须在probe里正确设置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;这段代码看着简单漏掉任何一个offset或length颜色就会全部错位。5.3 刷新闪烁与撕裂的规避SPI LCD没有硬件VSync信号所以不存在严格意义上的tearing同步。出现闪烁或者撕裂根本原因是上层在写显存的同时驱动正在通过SPI搬运同一块数据。解决撕裂的方法只有一个加锁。我在驱动里给显存加了一个spinlock但实战后发现spinlock粒度太大容易卡刷新流程。更好的做法是用双缓冲也就是在fb_info里注册两个逻辑屏一个给用户写一个给驱动刷写和刷之间通过ioctl切换。不过双缓冲对内存要求更高240x240的双缓冲也就230KB完全可以接受。如果你不想改上层程序那就退而求其次确保刷屏期间用户态不能写这个用semaphore就能实现。代价是上层如果高频更新会偶尔等待。实际效果在交互界面上基本无感。5.4 性能优化把脏矩形优化到底最影响刷新体验的其实不是刷屏速率快慢而是要不要把所有内容都重新传输。我在这个项目里做了行列级的脏矩形跟踪。每次fb_fillrect或者通过write操作更新时记录更新的矩形范围。DMA传输时只需要把这部分行数据发出去。240x240分辨率下假如每次只更新一个20x20的小图标SPI上实际传输的数据量只有原来的几十分之一刷新感觉自然流畅得多。这里要小心坐标换算。fb的坐标原点和屏幕的扫描方向可能不一样尤其是把屏转成竖屏或者增加rotation之后脏矩形的行列和GRAM的地址关系就会变化。虚线实现时最好先在驱动里做一个通用的坐标变换把任何rotation下用户画的矩形都换算成GRAM里的一段连续地址区间这样才能安全地做局部刷新。5.5 驱动加载时序与开机动画的配合如果不想让系统启动过程停留在黑屏阶段需要让这个fbdev驱动尽早初始化然后配合内核的fbcon把console输出也显示在屏上。做法是在内核命令行参数里加上fbconmap:0或直接设置consoletty1。这样内核启动时日志会直接打到屏上连续刷屏的感觉和单片机开发很像调试特别方便。不过fbcon显示在内核启动早期会拖慢启动速度每打印一行日志就得触发一次SPI刷新。我的建议是调试阶段开启fbcon产品阶段把console重定向到串口SPI屏只留给业务应用使用。5.6 常见问题速查表现象直接原因解决路径白屏复位时序不对/初始化失败抓RES波形确保复位后延时120ms花屏SPI速率过高/传输宽度错降低max-freq检查buswidth颜色红蓝互换RGB/BGR位不对修改0x36寄存器值屏幕亮但无内容DC信号未配置确认DC GPIO被正确拉低/拉高刷新时画面撕裂显存读写并发加锁或双缓冲DMA传输卡死SPI控制器DMA配置不完整配置rx dma通道或回退PIOfb0被DRM占用内核开启了fbdev emulation关闭CONFIG_DRM_FBDEV_EMULATION或分配差异化fb编号6. 个人心得这类驱动开发的核心收获这个项目做下来我个人最大的体会是SPI LCD驱动本身不难难的是对硬件时序和设备树的理解。如果你已经玩过单片机SPI驱动屏幕那在Linux下做fbdev驱动你缺的主要不是C语言能力而是对Linux显示子系统分工的认识。驱动只是把数据搬运到屏幕真正复杂的是上层图形库如何与fb设备协作。把fb_info结构体里的字段、fb_ops回调、SPI DMA路径理清楚整个链路就通了。最后再分享一个小技巧如果遇到屏幕初始化后某些区域的颜色不正常先别急着怀疑驱动和寄存器试着把屏幕翻转180度再看有时候只是MADCTL里的扫描方向反了导致GRAM地址顶到了显示区的非对齐区域。这种事我踩过两次每次排查都花了半天才醒悟过来。