
这段时间我在RK3568板子上调一块SPI接口的LCD屏走的是Linux的FrameBuffer框架前前后后踩了不少坑。RK3568在工业HMI、仪器仪表、边缘计算盒子里用得非常多平时大家要么接HDMI要么接MIPI DSI但总有一些项目为了控制成本只预留了SPI接口给一块小尺寸LCD这让很多人一开始不知道从哪里下手。这篇东西主要想把“RK3568平台 SPI LCD FrameBuffer”这套方案的整体链路讲清楚从方案选型、设备树配置到驱动probe流程、刷新机制再到最后面的调试方法和优化思路。适合手里正好有RK3568开发板、准备驱动一块SPI小屏或者已经翻过一些SPI屏资料但没想清楚怎么跟FrameBuffer对接的工程师。1. 方案定调为什么是FrameBuffer加SPI屏1.1 RK3568显示栈与SPI屏的定位RK3568是瑞芯微面向AIoT和工控场景的一颗四核Cortex-A55处理器主频最高2.0GHz带Mali-G52 GPU显示接口丰富HDMI、MIPI DSI、eDP、LVDS甚至RGB并口都有。但这就引出一个问题既然有这么多“正路”显示接口谁还会用SPI去点屏真实项目里这个需求其实不少。比如某些带小型参数面板的工控设备不需要高刷新率也不指望屏幕显示复杂动效只要求稳定地显示几个关键数值、状态图标或者简单波形这时候SPI LCD就是性价比很高的选择。SPI屏分辨率一般在128x160、240x320、240x240这个级别功耗低、接线少、驱动电路简单很多定制设备只留了一条FPC排线位置背后走的就是四根SPI信号线。FrameBuffer模式在Linux里年头很久了说白了就是内核维护一块连续内存作为显存应用层用mmap把它映射到用户空间然后直接往里填像素数据。它相比现代DRM/KMS显得“土”但胜在简单直接。对于一块320x240的SPI小屏我不需要向上层的HWComposer、DRM plane这些复杂概念妥协只需要做到三点驱动能初始化屏、内核把显存注册成fb设备、应用层写入的像素数据能稳定搬到SPI屏上。这正好是FrameBuffer的舒适区。1.2 三种可行方案对比fb、fbtft、自写驱动在RK3568上让一块SPI LCD跑起来至少有三条路可以选。我整理了一张对比表实际评估时一目了然方案内核依赖开发量可控性适合场景fbtft框架CONFIG_FB_TFT及对应驱动IC低中验证硬件、快速原型自写fb驱动CONFIG_FB SPI中高高正式项目、定制功能较多用户态spidev直推只需spidev驱动低低简单验证、不带GUI的场景fbtft在内核里维护了一批SPI小屏驱动像st7789v、ili9341、st7735r这些常见驱动IC都有对应实现。如果你是第一天拿到屏幕只想确认硬件有没有接对优先用fbtft去验证一条module参数就能把驱动挂起来比自写驱动快得多。但它的局限也很明显fbtft很多代码停留在老kernel时代设备树支持不算完整对DC引脚、复位引脚、初始化序列的定制能力比较弱。部分贴牌屏的初始化命令跟fbtft默认值不一致出来的效果就是颜色偏、花屏或者屏幕只有半截能亮必须回过来手动改。用户态spidev直推就是在应用层直接往/dev/spidevB.C写入帧数据。这种方式绕开了内核显示框架确实简单但也意味着你没法让Qt、LVGL、AWTK这些GUI库直接打开一个帧缓冲设备来绘制因为它们通常只认/dev/fbN。除非你的屏只做固定画面刷新否则长期维护成本很高。所以我的建议很明确如果是正式项目或者后续要接GUI老老实实按照“platform驱动 fb_info”的方式自写一个驱动。内核里的fbtft代码虽然老但可以作为参考核心的波形发送、像素格式转换逻辑都能直接抄思路真正需要自己写的主要是驱动框架和设备树对接这一层。1.3 硬件连接与驱动IC选择要点SPI LCD的驱动IC常见的有ST7789V、ILI9341、ST7735S、GC9A01它们基本都是TFT屏内部带GRAM外部通过SPI接收命令和数据。引脚上常见的就那么几根SCLK时钟、MOSI主出从入、CS片选、DC数据/命令选择也有叫RS的、RESET复位、BLK背光控制。有些屏没有单独的DC引脚而是使用SPI的9-bit模式每帧第一位表示命令还是数据这种情况驱动代码里要处理好。ST7789V和ILI9341算是最主流的两个选择分辨率规格也比较常用。ILI9341多见于320x240的2.4寸/2.8寸屏ST7789V则经常出现在240x320或者240x240的1.3寸/1.54寸屏上。选屏幕的时候我习惯先确认以下几点屏的工作电压是3.3V还是1.8V、是否自带电平转换、SPI接口是Mode 0还是Mode 3、GRAM的偏移量是多少。最后这个“偏移量”特别容易踩坑很多屏明明分辨率写对了但画面就是往右下方偏实际是驱动IC内部行列地址的起始偏移不对后面调试篇我会专门讲。硬件连接方面最常见的坑是信号线没有上拉电阻。SPI的MOSI、SCLK在Master模式下由控制器输出问题不大但CS和DC引脚在主控初始化完成之前可能是浮空状态导致屏幕收到随机数据。尤其是在板子启动早期、GPIO还没有被内核接管的时候屏幕可能会出现花屏、白屏的瞬间状态。解决办法是给CS和DC加上10kΩ左右的上拉电阻或者在硬件设计上把它们默认拉到确定电平。2. 环境准备与设备树配置2.1 交叉编译环境与内核源码准备RK3568跑到Linux开发环境一般是一台x86的Ubuntu机器用交叉编译链编内核和驱动。SDK里通常会带好工具链如果没带用Linaro的aarch64-linux-gnu-也可以。我用的是RK SDK内核一般在kernel目录下操作。交叉编译前需要先设置环境变量export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu- export PATH/opt/toolchains/aarch64-linux-gnu/bin:$PATH然后先编一遍SDK默认config确保内核能正常跑起来再开始加自己的改动。RK3568的SDK默认defconfig里FrameBuffer支持可能没有完全打开因为默认使用DRM显示框架。所以你在menuconfig里要重点关注三个部分Device Drivers - Graphics support下的Frame buffer devicesDevice Drivers - SPI下的Rockchip SPI controller以及Device Drivers - Staging或Misc下的fbtft如果只是想验证硬件。我把需要的内核配置列成一个最小集合配置项含义推荐值CONFIG_SPISPI子系统yCONFIG_SPI_ROCKCHIPRK平台SPI控制器yCONFIG_FB帧缓冲设备yCONFIG_FB_CMDLINE命令行参数支持fbyCONFIG_FB_xxx具体屏驱动选一个即可m/y驱动调试阶段我更喜欢把自写驱动编成模块obj-m这样改代码不用每次重新烧整个内核insmod/rmmod就能完成迭代速度快很多。等你确认稳定了再把它编进内核里开机自动加载。2.2 设备树节点编写SPI控制器与LCD子节点RK3568的SPI控制器有好几组比如SPI0、SPI1、SPI2、SPI3每组又可能有不同的引脚mux组合。设备树里要做两件事先打开对应的SPI控制器然后在它下面挂一个LCD子节点。一个典型的设备树配置大概是这样的以RK3568为例spi1 { status okay; pinctrl-names default; pinctrl-0 spi1m0_cs0 spi1m0_clk spi1m0_miso spi1m0_mosi; max-freq 20000000; lcd_spi: lcd-spi0 { compatible custom,rk-spi-lcd; reg 0; spi-max-frequency 20000000; spi-mode 0; reset-gpio gpio4 5 GPIO_ACTIVE_LOW; dc-gpio gpio4 4 GPIO_ACTIVE_HIGH; backlight-gpio gpio4 6 GPIO_ACTIVE_HIGH; panel-width 240; panel-height 320; pixel-format rgb565; offset-x 0; offset-y 0; }; };这里有几个细节要强调。pinctrl-0里引用的是一组SPI引脚的复用状态名不同开发板可能同名引脚在不同mux组必须先确认原理图上实际用的是哪一组。RK3568的引脚复用经常让人头疼同是SPI1的时钟可能在GPIO3的m0组也可能在GPIO4的m1组配置串了之后内核起来会报spi设备注册失败或者pinctrl冲突有些甚至不报错只是SCLK完全没有波形。compatible这个字符串完全自定义驱动里会用它来匹配当前设备。reg 0表示这是SPI控制器下的第0个片选。如果你的板子用GPIO模拟片选可以不在SPI节点里直接挂子设备而是另写一个GPIO控件的设备节点然后在驱动里手动控制CS电平但这属于“软件片选”的做法后面第4章我展开讲。2.3 GPIO与硬件片选的几个容易踩的坑在设备树里定义了reset-gpio、dc-gpio、backlight-gpio之后要注意RK3568的GPIO默认状态。GPIO4的这几个引脚上电初期是高阻状态如果屏幕对复位时序比较敏感最常见的现象就是开机时屏出现无规律条纹。驱动里probe的时候要尽早把reset引脚拉低再拉高给屏幕一个完整的硬件复位然后再发初始化命令。如果SPI控制器本身带有硬件片选那CS引脚由SPI控制器驱动不需要额外配置GPIO。但有些板子设计时为了省事把CS接到了一个普通GPIO上这种情况下你要么用软件片选让驱动在每次传输前后手动拉CS要么在dts里把该GPIO配成SPI控制器的CS功能。前者代码好写但在高频SPI场景下性能会差一些后者更规范但必须确认控制器支持该组引脚的复用。一个额外提醒不是所有SPI LCD都适合用硬件片选直接连。部分屏幕的CS引脚在片选拉低之后要求DC命令和数据的第一位时序必须严格按照SPI Mode 0来如果控制器配置成了Mode 3时钟空闲电平是高的屏可能完全不工作。所以dts里的spi-mode必须对着屏幕规格书确认一般ST7789V默认是Mode 0也有少数屏幕支持Mode 3选错了最常见的现象就是黑屏或者花屏。3. 驱动框架搭设与核心代码实现3.1 驱动整体结构与probe流程自写一个FrameBuffer模式的SPI屏驱动核心是把两个子系统粘在一起一边是SPI子系统用来传输数据另一边是FrameBuffer子系统用来注册一个fb设备。驱动本身注册成一个spi_driver在match到设备树里的节点后执行probe。驱动文件拆成几个文件比较清晰文件职责lcd_spi.cprobe/remove、fb_info注册、SPI传输lcd_panel.c各驱动IC的初始化序列、屏幕参数lcd_fb_ops.cfb_ops回调实现lcd_spi.h公共头文件probe流程大概是这样的1. 从设备树解析屏幕参数宽高、偏移、像素格式等 2. 申请GPIO资源reset、dc、backlight 3. 对屏幕做硬件复位并通过SPI发送初始化命令序列 4. 分配显存vmem采用devm_kzalloc或vzalloc 5. 调用framebuffer_alloc分配fb_info 6. 填充fb_var_screeninfo与fb_fix_screeninfo 7. 设置info-fbops、info-screen_base、info-screen_size 8. 调用register_framebuffer注册设备 9. 创建背光设备或注册backlight类这段流程听起来不难但每一步都有细节。设备树解析我习惯用device_property_read_u32这一组API它能同时兼容设备树和ACPI环境。GPIO申请建议用devm_gpiod_get它可以自动管理生命周期不用手写gpio_free。显存分配则要看屏幕分辨率240x320的RGB565就是大约150KB用vzalloc分配连续虚拟内存即可不一定非要物理连续。3.2 fb_info结构体填充var和fix不能拍脑袋fb_info是FrameBuffer驱动最核心的结构体里面两个子结构体非常关键fb_var_screeninfo描述可变参数比如分辨率、位深、时序fb_fix_screeninfo描述固定参数比如显存物理地址、行字节数、屏幕类型。以一块240x320、RGB565的屏为例正确的填充方式大致如下struct fb_var_screeninfo var {0}; var.xres 240; var.yres 320; var.xres_virtual 240; var.yres_virtual 320; var.bits_per_pixel 16; var.red.offset 11; var.red.length 5; var.green.offset 5; var.green.length 6; var.blue.offset 0; var.blue.length 5; var.transp.offset 0; var.transp.length 0; var.nonstd 0; var.activate FB_ACTIVATE_NOW;struct fb_fix_screeninfo fix {0}; strcpy(fix.id, spi_lcd); fix.smem_start virt_to_phys(lcd-vmem); fix.smem_len 240 * 320 * 2; fix.type FB_TYPE_PACKED_PIXELS; fix.visual FB_VISUAL_TRUECOLOR; fix.line_length 240 * 2; fix.accel FB_ACCEL_NONE;这里最容易写错的是color的offset和length。RGB565是R占高5位G占6位B占低5位如果offset写反屏幕上会出现红蓝颜色互换最典型的表现是看起来像“颜色不对”但画面内容是正常的。我第一次调的时候没仔细看红蓝互换折腾了半天最后才发现var里的rgb字段填错了。另外fix.smem_start我建议直接用virt_to_phys得到物理地址这样应用层mmap时不需要额外处理。如果屏幕本身有显存并且映射在IO内存空间也可以填对应的物理地址。vmem指针和smem_start必须对应否则用户空间mmap到的地址就是错的写入数据会出现奇怪的撕裂或乱码。3.3 SPI发送初始化序列与数据帧格式屏幕初始化是LCD驱动里最“玄学”的一步。每款驱动IC的初始化命令序列不同甚至同型号不同厂商的屏也略有差异。这里以ST7789V为例展示最基础的启动流程static const u8 st7789v_init_cmds[] { 0x01, 0x00, // SWRESET 0x11, 0x00, // SLPOUT 0x3A, 0x01, 0x55, // COLMOD: 16bit color 0x36, 0x01, 0x00, // MADCTL 0x21, 0x00, // INVON 0x13, 0x00, // NORON 0x29, 0x00, // DISPON };我习惯把命令序列定义成“命令字节 参数长度 参数”的结构而不是单纯一个字节数组因为后期维护方便不同屏差异一目了然。发送时要注意DC引脚状态发命令字节时DC拉低发参数或像素数据时DC拉高。具体在代码里就是static int lcd_write_cmd(struct lcd_spi_dev *lcd, u8 cmd) { gpiod_set_value(lcd-dc_gpio, 0); return spi_write(lcd-spi, cmd, 1); } static int lcd_write_data(struct lcd_spi_dev *lcd, const u8 *data, int len) { gpiod_set_value(lcd-dc_gpio, 1); return spi_write(lcd-spi, data, len); }这里有个容易忽视的性能点如果每条命令都做一次spi_write内核会为每次传输构造一个spi_message消耗不少时间。初始化阶段无所谓但刷屏阶段如果还这么干帧率会非常难看。批量发数据时最好把整行甚至整帧数据打包成一个spi_transfer或者用spi_write几次拼接减少SPI事务开销。MADCTL寄存器0x36控制扫描方向和RGB次序这是调整屏幕横竖显示、镜像问题的关键。bit7 MX是左右镜像bit6 MY是上下镜像bit5 MV是行列交换bit3 BGR是RGB/BGR顺序。你要是发现屏幕上下颠倒或者左右反了别去改屏幕硬件直接改这个寄存器的值就行。3.4 刷新机制从dmabuf到屏幕的方法FrameBuffer设备本身并不会自己把显存内容搬到SPI屏上这一步必须由驱动主动做。常见做法是驱动里维护一个刷新线程或者延迟工作队列周期性地把fb_info-screen_base里的数据通过SPI发送到屏幕的GRAM中。但是整屏发送代价很高。240x320的RGB565一帧是153600字节如果SPI时钟跑20MHz理论极限也要61ms左右再加上协议开销和传输间隙实际上30fps很难做到。所以工业场景里更常用的做法是局部刷新记录电压或屏幕内容变化的区域只把变化的矩形区域推送到屏幕。很多屏驱动IC支持用CASET列地址设置2Ah和RASET行地址设置2Bh命令限定GRAM写入区域这样局部刷新的效率可以提升好几倍。一个最简单可靠的实现是使用延迟工作队列static void lcd_refresh_work(struct work_struct *work) { struct lcd_spi_dev *lcd container_of(work, struct lcd_spi_dev, refresh_work.work); mutex_lock(lcd-lock); lcd_send_frame(lcd); mutex_unlock(lcd-lock); schedule_delayed_work(lcd-refresh_work, HZ / 30); }在probe里初始化并启动这个工作队列它会以30fps的节奏持续把最新显存内容刷到屏上。这个“总是刷整屏”的做法虽然简单但在显示静态内容时浪费很大还会让CPU占用率偏高。后续优化方向是自己维护脏矩形列表或者应用层通过自定义ioctl通知驱动“这块区域变了你只刷这块就行”。如果未来要接LVGL这类GUI库LVGL自身的flush回调也会先调用你的驱动接口直接按dirty area刷新反而更顺。3.5 背光控制与电源管理背光并不是FrameBuffer设备注册的必需流程但产品级驱动里必须有。最粗暴的方案是把背光引脚当普通GPIO在fb_blank回调里控制开关。比如FB_BLANK_UNBLANK时把背光GPIO拉高FB_BLANK_POWERDOWN时拉低这样用户在应用层调用blank ioctl时就能控制屏亮灭。如果想支持PWM调光推荐用Linux的pwm-backlight驱动设备树里挂一个backlight节点然后在LCD驱动里通过backlight_get_brightness之类的接口去读取亮度。FrameBuffer驱动本身不需要直接操作PWM这样代码更干净也方便用户层用标准sysfs接口调节亮度。电源管理方面驱动的remove回调要确保注销工作队列、释放背光资源、关闭屏幕显示。如果系统支持suspend/resume还要在suspend里关闭显示并取消定时刷新的工作队列在resume里重新初始化屏幕并重启刷新。这里特别提醒SPI屏在系统休眠后经常需要重新发送初始化序列不能指望它像RGB并口屏一样保持状态。否则进入休眠再唤醒屏幕会出现白屏或者花屏。4. 调试实战与问题排查4.1 常用调试手段从内核到用户态调试FrameBuffer SPI LCD我习惯按照“先设备树、再驱动、再波形、最后乱码”的顺序来排查。第一步看内核有没有识别到设备驱动有没有成功probe。启动日志里找关键字改为dmesg | grep lcd或者dmesg | grep spi。如果看到“failed to match”说明compatible不对如果是“No GPIO description”说明设备树里GPIO没解析成功。第二步看fb设备有没有生成。在板端执行cat /sys/class/graphics/fb0/name如果驱动注册成功这里会输出我们在fb_fix_screeninfo里设置的id比如“spi_lcd”。如果这个文件不存在说明register_framebuffer没有执行成功往回看是不是内存不足或者参数校验失败。第三步做最原始的显示测试。往fb0里写满数据或写一个纯色图案dd if/dev/urandom of/dev/fb0 bs1024 count150 # 或者用下面的方式写红色图案 busybox devmem /dev/fb0 32 0x00F800F8如果屏幕上出现杂点或红色块说明FrameBuffer链路基本通了接下来才轮到像素格式和初始化序列的调试。如果屏幕上什么都没有要回去检查SPI波形和初始化序列。4.2 SPI时序与硬件层面的检查当驱动看起来一切正常但屏就是不亮或者颜色完全不对时需要用逻辑分析仪或示波器抓SPI波形。重点看几个点SCLK频率是否和设备树里设置的一致、MOSI数据在SCLK上升沿是否稳定、CS片选时序是否正确、DC引脚是不是在命令阶段为低、在数据阶段为高。SPI Mode的选择也会影响波形判读。Mode 0是空闲时钟低电平数据在上升沿采样Mode 3是空闲时钟高电平数据在上升沿采样。如果你的示波器抓到波形看起来“反了”大概率是Mode配错了。还有一点就是SPI的MSB/LSB顺序。大多数屏幕控制器期望MSB优先但RK3568的SPI控制器可以通过SPI_MASTER_NO_TX或transfers的tx_nbits等参数设置。如果数据顺序反了屏幕会显示完全无法解读的乱码而且RGB565的字节顺序还会进一步加重花屏这类问题光看代码很难发现抓波形最直接。4.3 常见显示异常与根因速查表我整理了一份问题排查表这些都是实际项目中反复出现的问题现象可能原因排查方向白屏复位时序不对、初始化序列未发、SPI时钟不工作检查GPIO复位时序、逻辑分析仪抓初始化波形黑屏背光未亮、屏幕未开启、MADCTL扫描方向指向不可见区域检查背光GPIO、DISPON命令花屏像素格式不匹配、字节序反了、刷新区域错位确认RGB565、检查COLMOD、检查列/行地址命令颜色红蓝互换RGB字段偏移错误、MADCTL的BGR位不对修正fb_var_screeninfo、修改MADCTL上下/左右颠倒MADCTL的MX、MY、MV位配置不对按屏幕实际安装方向调整屏幕偏移未设置GRAM行列偏移配置offset-x/offset-y或初始化序列中的PORCH刷新有拖影或闪烁刷新频率过低、SPI带宽不足降低分辨率、使用局部刷新、提高SPI时钟白屏和黑屏是最常见的两个坑。白屏很多时候是屏幕初始化没完成尤其ST7789V要求SWRESET之后至少要等120ms如果驱动把命令一口气发完没加延时屏幕就可能不响应。黑屏则要先排除背光问题很多人把背光GPIO配置错了方向GPIO初始状态为低屏幕当然黑屏一片。花屏问题里我印象最深的是RGB565高低字节反了。ST7789V默认接收的RGB565是高位字节在前而有些RK平台的内核spidev可能默认按小端发送。结果就是屏幕显示的画面像打了很多马赛克但隐约能看到内容轮廓。遇到这种问题可以在驱动发送像素前做一次字节交换或者调整fb定义的RGB偏移让它跟屏幕实际期望的字节序一致。4.4 性能优化SPI时钟、DMA与局部刷新SPI小屏的帧率瓶颈几乎都在链路上。提升性能有几个方向按性价比排序第一个是提高SPI时钟频率。RK3568的SPI控制器通常能跑到50MHz以上但问题是SPI LCD屏本身未必能承受这么高的时钟。很多屏幕规格书标称最高15.36MHz或者20MHz超过之后信号完整性和芯片内部时序会出问题。我一般先保守用20MHz跑再用示波器看波形质量逐步往上试探最终以长时间压力测试不花屏为标准。第二个是用DMA传输。RK3568的SPI控制器支持DMA把刷屏数据从内存直接搬给SPI FIFOCPU不用每次中断处理字节。不过DMA模式下要特别注意cache一致性如果显存区域被CPU修改过而没有做dma_map_single或dma_sync操作屏幕显示的可能是cache里过期的数据表现为画面有一块块“残影”。一个比较简单的做法是每次刷屏前调用dma_sync_single_for_device完成后调用dma_sync_single_for_cpu确保数据一致性。第三个是局部刷新这个我在3.4节提过。实际调优时即使不做复杂的脏矩形跟踪也可以先做一个简化版把屏幕分成N个横向条带每次定时刷新只刷其中一个条带循环执行。这样一帧数据只传1/N看起来整体画面虽然刷新率被分摊了但静态内容的细碎闪烁会大幅减少。如果你在跑LVGL或Qt它们自带的刷新回调也能告诉你哪些区域变了直接把这部分数据传过去即可。5. 几个个人体会最后聊点屏幕之外的经验。做这种FrameBuffer模式的SPI LCD驱动本质上是在跟“简单和复杂”做权衡。FrameBuffer确实老旧但它在嵌入式Linux里依然被大量设备依赖因为很多场景不需要DRM那样完备的合成能力只需要一块能写像素的画布。对于RK3568这款芯片SPI LCD更像是“曲线救国”的方案它不完全符合大家对该平台中高端显示场景的预期但放在成本敏感的垂直行业里依然有很强的存在意义。我自己实际调完这套驱动的感受是时间大头不是花在写驱动代码上而是花在屏幕初始化序列和硬件信号这两个“看不见”的地方。设备树里差一个引脚配置SPI波形就对不上初始化序列少一条延时屏幕就永远白屏MADCTL写错一个bit整个画面方向就全反了。所以如果你正在调SPI LCD建议第一件事就是拿逻辑分析仪把屏的初始化过程抓下来对照驱动代码逐个字节确认这比在代码里盲调高效太多。这套方案后续还能沿着两个方向扩展一是往底层走在uboot阶段就把SPI LCD点亮实现开机能直接出logo不至于等内核跑起来才显示界面二是往上层走把frame buffer设备接到LVGL、AWTK或者Qt的linuxfb后端做一套完整的嵌入式UI。这两条路我都在折腾等哪天真跑顺了再单独写一篇。