ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RK3568 SPI LCD驱动实战:FrameBuffer模式与调试全解析

RK3568 SPI LCD驱动实战:FrameBuffer模式与调试全解析 从RK3568上接一块SPI小屏这件事网上资料很零散大多是拿fbtft改一改、能亮就算完。但真到产品化阶段你会遇到一堆绕不开的问题初始化时序不对导致花屏、片选策略影响命令发送、刷新帧率上不去、竖屏改横屏不知道改哪里。这篇文章把我自己在RK3568上从零驱动一块ST7789V SPI LCD的完整过程写出来重点讲FrameBuffer模式下的驱动架构、设备树配置、刷新机制和实际调试中踩过的坑给正在做类似事情的嵌入式驱动开发工程师一个可以直接参考的路径。1. 为什么在RK3568上驱动SPI LCD要选择FrameBuffer模式1.1 RK3568显示链路里没有SPI LCD的位置RK3568的显示资源其实很丰富VOPVideo Output Processor后面挂了MIPI DSI、LVDS、RGB/eDP这些标准显示接口系统正常显示链路走的是DRM/KMS框架。但你拿到的SPI小屏完全不属于这一套体系它没有标准视频时序没有pixel clock也不需要HSYNC/VSYNC它就是一个挂在SPI总线上的从设备需要主机持续往它内部的GRAM里写像素数据。VOP根本不知道这种屏的存在所以不能像处理RGB屏那样直接接上就完事。这时候摆在面前的路有三条一是用内核自带的fbtft框架二是基于FrameBuffer自己写一个spi_driver三是强行用DRM的panel-bridge去包一层虚拟显示接口。第三条路工作量最大而且对DRM内部机制不熟的人很容易被绕晕除非团队里有人专门搞过否则不建议碰。1.2 FrameBuffer模式与DRM/KMS方案的选择逻辑很多新入行的驱动工程师一听说Linux显示就应该用DRM看到FrameBuffer就觉得是老古董这种判断在标准RGB/LVDS屏场景下是正确的但放在SPI LCD上就不太合适。我对比一下这两种思路以及fbtft方案的实际体验用一个表格看更清楚方案实现复杂度可控性适用场景自写FrameBuffer驱动中高所有刷新逻辑自己掌控产品定制、低分辨率SPI屏、LVGL界面fbtft低但调试成本高低很多行为被框架锁死快速验证、个人DIY、不追求深度定制DRM panel-bridge高中团队有DRM基础必须统一走DRM框架的场景我自己最终选的是自写FrameBuffer驱动。原因很简单fbtft在一定程度上绑定老内核的初始化路径和一些历史包袱在RK3568这种比较新的内核上要打补丁而且我想实现局部刷新、亮度调节、MADCTL旋转这些产品化功能在fbtft上改远不如自己掌控来得直接。FrameBuffer模式的核心思想是把显存看作一块普通内存用户程序往/dev/fb0里写数据等同于往这块内存写像素驱动后台用一个刷新机制把内存里的内容搬运到SPI LCD里。LCD显示的是一个静态画面和VOP定时扫描是两回事。1.3 这种模式能跑什么应用不能跑什么应用搞清楚定位才能选对方案。FrameBuffer模式的SPI LCD适合做这些事低分辨率人机界面比如240x240、320x240的小屏配合LVGL等GUI库显示菜单、仪表盘、状态信息调试面板、参数显示、简单的串口屏替代品不适合做这些事视频播放刷新率上限受SPI总线带宽限制一般到不了流畅视频的帧率复杂动画场景CPU往SPI上持续搬运数据会占用不少算力需要GPU合成加速的界面FrameBuffer模式绕开了VOP合成链路GPU画的画面无法直接命中这块显存所以做方案选型时先算清楚这个产品要不要跑动效、视频再决定是不是用SPI LCD。只是显示静态文字、温湿度、电压电流这类数据SPI屏方案成本优势非常明显。2. 先把SPI协议和RK3568 SPI控制器的脾气摸清2.1 SPI四根线的语义与LCD只收不发的特点不管接的是ST7789V、ILI9341还是ST7735SSPI LCD的通信都围绕四根线展开CLK时钟、MOSI主出从入、MISO主入从出、CS片选。这里可以类比成对讲机通话主机按下CS按键表示“我要开始通话了”CLK相当于节拍器每打一个拍子双方约定传输一个bitMOSI是主机说话的内容MISO是从机回话的内容。LCD屏只需要接收命令和数据绝大多数情况下不返回数据所以MISO这根线可以不接或者留着以后做屏幕ID读取用。驱动开发中的速率上限由CLK频率决定。RK3568的SPI控制器支持的频率范围很宽但并不是设备树里写多高就一定能跑多高还要看PCB走线长度、杜邦线质量、屏幕本身的规格上限。SPI小屏驱动IC通常能跑到30MHz到60MHz但实际产品里为了信号完整性我一般控制在20MHz到40MHz之间。2.2 时钟极性和相位配置错了黑屏是最轻的后果SPI协议有四种模式由CPOL时钟极性和CPHA时钟相位组合而成。CPOL决定空闲时CLK是高还是低CPHA决定数据是在时钟上升沿还是下降沿被采样。大部分SPI TFT LCD驱动IC支持Mode 0CPOL0, CPHA0或Mode 3CPOL1, CPHA1具体看数据手册。配置错了最直观的表现就是整个屏幕无显示或者显示乱码而且这种问题用眼睛看代码很难发现必须用逻辑分析仪抓波形和手册里的时序图对照。ST7789V这颗IC我在RK3568上用的是Mode 0设备树里不需要显式写模式驱动代码中通过spi_device的mode成员设置lcd_spi-mode SPI_MODE_0;如果是自己创建spi_device或者使用SPI_DEV_NAME方式匹配记得在probe里确认这个值否则默认可能是Mode 0但如果你的板级配置里被bootloader改过就会出问题。2.3 硬件片选和软件片选之争直接决定你的代码怎么写这是SPI LCD驱动里最隐蔽的一个坑值得单独拿出来讲。RK3568的SPI控制器支持硬件片选也就是控制器根据spi_message的状态自动拉低或拉高CS引脚。看起来省事但对LCD驱动来说有一个致命问题硬件片选在两个spi_transfer之间可能会把CS抬起来一下即使它们属于同一个spi_message。对多数SPI外设这不是问题因为它们按字节或按块解析数据但对LCD来说CS被认为是“一次事务”的边界CS抬升了屏幕驱动IC会认为当前接收过程结束了这会直接导致命令后面的数据被误解。举个例子你想发一个RGB颜色设置命令0x2A后面跟4字节的窗口坐标数据。如果用硬件片选并且cmd和数据是两次spi_sync调用CS在两次调用之间必然抬升。屏幕内部状态机很奇怪有些IC能容忍有些IC直接废掉整条命令。解决思路有两个用软件片选把CS引脚配成普通GPIO自己控制拉低和拉高这样可以在连续多次spi_sync之间保持CS始终为低。把命令和数据放在同一个spi_message的多个spi_transfer里但前提是你的SPI控制器在同一个message内部不会抬CS。我自己实践下来的结论是用软件片选最稳而且调试时还能用GPIO手动拉CS来验证时序对排查问题特别有帮助。设备树里设置cs-gpios属性即可驱动里的片选操作完全由GPIO子系统接管。2.4 DC线和RESET、背光引脚的配合时序SPI LCD除了SPI四根线通常还有DC数据/命令选择、RESET复位、BLK背光三根控制线。DC线的作用是告诉屏幕驱动IC当前SPI总线上传输的字节是命令还是数据。对于ST7789V这类ICDC为低表示命令为高表示数据。这个引脚切换的时机非常重要必须在CS拉低之后、SPI时钟启动之前完成切换否则第一个字节的采样就错了。RESET引脚处理也很有讲究。我见过不少人在probe里拉一下RESET就去初始化屏幕结果屏幕没反应。正确流程一般是VCC上电稳定后等待至少10msRESET拉低保持10ms以上RESET拉高等待120ms以上然后发送初始化命令序列背光引脚最理想是和RESET分开控制并且不要在上电初始化完成之前点亮背光否则你会看到屏幕先闪一下白屏非常影响观感。正确的顺序是初始化屏幕、清屏、再开背光。3. 设备树配置驱动能不能跑起来第一关在这里3.1 在RK3568设备树里打开SPI控制器并挂自定义设备RK3568设备树里SPI控制器节点外设默认状态很可能是disabled需要手动打开。以SPI2为例典型配置如下spi2 { status okay; pinctrl-names default; pinctrl-0 spi2m1_pins; cs-gpios gpio3 RK_PB0 GPIO_ACTIVE_LOW; spilcd: spilcd0 { compatible vendor,spi-lcd; reg 0; spi-max-frequency 40000000; rotate 90; dc-gpio gpio3 RK_PB1 GPIO_ACTIVE_HIGH; reset-gpio gpio3 RK_PB2 GPIO_ACTIVE_LOW; backlight-gpio gpio3 RK_PB3 GPIO_ACTIVE_HIGH; }; };解释几个关键字段pinctrl-0指定SPI引脚的复用功能RK3568同一个SPI控制器可能有多个引脚组比如spi2m0和spi2m1要根据原理图选对。cs-gpios指定CS用软件片选GPIO_ACTIVE_LOW表示片选有效电平是低。compatible是驱动匹配的关键必须和驱动里的of_match_table一致。spi-max-frequency是SPI总线的最大频率不是固定频率驱动里还可以在每次transfer单独设置speed_hz但不能超过这个值。自定义的rotate、dc-gpio、reset-gpio、backlight-gpio都是给驱动用的命名可以自己定驱动里通过of_property_read_u32和devm_gpiod_get解析。3.2 pinctrl引脚复用与调试串口冲突的排查RK3568的引脚复用功能强大但也最容易出错。很多引脚默认被UART占用比如调试串口通常是UART2而某些开发板的SPI2引脚恰好和UART2属于同一个引脚组。设备树里如果不把UART2节点disable就算你在SPI节点里配好了pinctrlSPI时钟也可能出不来因为引脚功能已经被占用。我遇到的一个真实案例板子启动时内核日志还在正常打印但SPI引脚就是没有波形。查了半天发现UART2和SPI2共用了部分引脚UART2设备树节点启用了pinctrl子系统把引脚占用关系锁死SPI2的pinctrl申请冲突被忽略。解决方法是在设备树里把冲突的UART节点改成disableduart2 { status disabled; };做这一步的前提是你能接受SPI的功能优先级更高。如果调试串口和SPI冲突但你的板子还需要串口看日志那就得重新选SPI控制器或者换引脚组。3.3 自定义属性旋转角度、最大SPI频率、亮度等级设备树里定义私有属性是驱动开发常用的做法好处是硬件改版时不需要重新编译驱动改dts就行。旋转角度我建议直接用度数表示比如rotate0、90、180、270驱动probe里根据这个值设置ST7789V的MADCTL寄存器。比驱动里写死成宏要灵活得多。最大SPI频率用标准的spi-max-frequency即可不需要自定义。亮度等级如果不是用PWM调光而是用屏幕内部对比度寄存器或者简单的GPIO占空比可以在设备树里定义brightness-levels数组驱动里解析后暴露给用户态。3.4 验证设备树是否生效的快速方法配置改完后怎么确认设备树里生成的spi_device被成功创建了开机后执行ls /sys/bus/spi/devices/正常会看到spi2.0这样的目录。然后在/sys/bus/spi/devices/spi2.0/下能看到modalias、of_node等属性。如果没有生成设备节点大概率是compatible没匹配上或者父节点status没设为okay。另外可以加一处调试输出在驱动的probe函数里打一条printkdev_info(spi-dev, spi lcd probed, max_speed_hz%u, mode0x%x\n, spi-max_speed_hz, spi-mode);内核日志里看到这行说明设备树匹配没问题驱动已经进入probe接下来才是真正调屏幕的阶段。4. 驱动代码拆解从spi_driver注册到像素真正跑到屏上4.1 显存分配为什么必须用DMA一致内存FrameBuffer驱动的核心资源是显存用户写的像素数据会先落在这块内存里然后由刷新机制搬运到SPI LCD。显存分配不能用普通kmalloc因为SPI控制器发起DMA传输时要求buffer是物理连续的而且必须保证CPU缓存和DMA看到的数据一致。用dma_alloc_coherent一举两得static void *lcd_fb_mem; static dma_addr_t lcd_fb_dma; lcd_fb_mem dma_alloc_coherent(spi-dev, xres * yres * 2, lcd_fb_dma, GFP_KERNEL);这里申请的是240x240x2字节约112KB对应RGB565格式的一整帧。用DMA一致内存后CPU写入数据通过缓存会被硬件自动处理不需要每次刷新前手动flush cache。如果图省事用kmalloc刷新时遇到花屏、残影很大概率就是cache一致性问题。4.2 初始化序列屏幕驱动IC的上电流程图ST7789V这类IC开机后不会自动进入可显示状态必须由主机发送一串初始化命令。不同型号的屏初始化序列不一样但都遵循类似流程软件复位0x01退出睡眠模式0x11设置像素格式0x3ARGB565时设为0x05设置扫描方向0x36也就是MADCTL打开显示0x29驱动里我会把初始化命令写成一个表方便按型号调整struct lcd_init_cmd { u8 cmd; u8 len; u8 data[8]; }; static const struct lcd_init_cmd st7789v_init[] { { 0x01, 0, {} }, // SWRESET { 0x11, 0, {} }, // SLPOUT退出睡眠 { 0x3A, 1, { 0x05 } }, // COLMODRGB565 { 0x36, 1, { 0x00 } }, // MADCTL方向由rotate决定 { 0x20, 0, {} }, // INVOFF { 0x29, 0, {} }, // DISPON };命令发送时我封装了两个基础函数后面所有操作都基于它们。4.3 维护命令/数据模式跨过片选细缝这是整个驱动编码里最需要注意的地方。由于我用了软件片选所以命令和数据可以分开发送中间安全地切换DC线static void lcd_write_cmd(struct spi_device *spi, u8 cmd) { gpiod_set_value_cansleep(dc_gpio, 0); // DC低命令模式 gpiod_set_value_cansleep(cs_gpio, 0); // 拉低CS spi_write(spi, cmd, 1); gpiod_set_value_cansleep(cs_gpio, 1); // 拉高CS } static void lcd_write_data(struct spi_device *spi, const u8 *data, size_t len) { gpiod_set_value_cansleep(dc_gpio, 1); // DC高数据模式 gpiod_set_value_cansleep(cs_gpio, 0); spi_write(spi, data, len); gpiod_set_value_cansleep(cs_gpio, 1); }看起来很简单但要注意几个细节必须在spi_write之前把DC切换到位不能在写操作过程中切换。CS拉低期间不能被打断所以如果系统里有并发写屏要给整个CSDCSPI发送过程加锁我用的mutex因为在工作队列和用户write回调里都可以睡眠。大块数据传输时spi_write内部可能被拆成多个transfer理论上有被并发抢占的风险加锁可以规避。如果你的硬件没有把CS配成软件片选而是硬件CS那么cmd和data必须放在同一个spi_message里并且中间不能有让CS抬升的机会。那样代码复杂不少这也是我坚持软件片选的原因。4.4 刷新机制定时整刷与局部刷新两个思路像素从显存到屏幕的搬运驱动里可以有多种方式。最简单的是定时整屏刷新起一个delayed_work每50ms把整个显存内容通过SPI写到屏幕。static void lcd_refresh_worker(struct work_struct *work) { /* 把fb显存整体刷新到LCD */ lcd_write_data(spi, lcd_fb_mem, xres * yres * 2); schedule_delayed_work(refresh_work, msecs_to_jiffies(50)); }但这种做法在SPI上很浪费因为240x240x2115200字节就算40MHz时钟也要传一阵子。更聪明的做法是局部刷新检测显存的脏区域只把变化的那一小块刷过去。ST7789V支持窗口地址设置先圈定一个矩形区域然后只向该区域写像素数据static void lcd_set_window(u16 x0, u16 y0, u16 x1, u16 y1) { u8 buf[4]; lcd_write_cmd(0x2A); // CASET列地址 buf[0] x0 8; buf[1] x0 0xff; buf[2] x1 8; buf[3] x1 0xff; lcd_write_data(spi, buf, 4); lcd_write_cmd(0x2B); // RASET行地址 buf[0] y0 8; buf[1] y0 0xff; buf[2] y1 8; buf[3] y1 0xff; lcd_write_data(spi, buf, 4); lcd_write_cmd(0x2C); // RAMWR开始写GRAM }有了这个函数每次刷新只传变化的矩形处理纯数字界面时效率提升非常明显。比如一个时钟界面只有中间数字区域变化刷新数据量可以从115KB降到几KB。内核的fb_deferred_io机制可以自动帮你检测脏页但它是基于页面粒度的对SPI LCD这种按矩形高效刷新的场景颗粒度不够细。我自己在实际项目中更倾向于在fb_ops的fb_write、fb_imageblit、fb_fillrect回调里做脏矩形标记然后由后台worker在下一个刷新周期集中处理。这样脏矩形是精确的效率最高。5. 实战调试花屏、黑屏、低帧率问题的完整排查链路5.1 从我第一次上电就看到随机雪花说起第一次在RK3568上点亮ST7789V时屏幕显示的是随机雪花点没有任何规律。我当时的排查链路是这样的第一反应是SPI模式配置问题于是用逻辑分析仪抓CLK、MOSI、CS、DC四路信号。抓下来发现一个问题MOSI上确实有数据但DC线切换时机比我预期的晚第一个数据字节已经被当成命令发出去了。原因是我在初始化时先调用了spi_write发送命令字节然后才去切换DC线这中间有一段裸奔时间。修复方式就是前面说的先切DC再拉CS再发数据顺序不能反。第二件事是检查MADCTL寄存器发现屏幕内容虽然是正常的但方向错了。这个不是bug是旋转参数没生效后面在设备树里加了rotate属性后解决。5.2 用逻辑分析仪盯住CLK、MOSI、CS和DC调试SPI LCD逻辑分析仪是必备工具比示波器更直观能一次性看清多路信号的关系。我的排查顺序是这样的看CS有没有按预期拉低并保持软件片选状态下连续多次spi_write之间CS有没有意外抬起。看CLK频率是否接近设定值如果设了40MHz但实际只有几MHz说明时钟源或分频配置可能有问题。看DC命令字节前后的DC电平变化是否与数据手册一致CMD时低、DATA时高。看MOSI数据对照屏幕驱动IC手册里的命令格式确认字节顺序和格式正确。如果抓到波形并确认命令格式和手册一致屏幕还是不亮基本可以排除驱动IC通信问题转向检查复位时序、电源、背光这几路。5.3 帧率到底卡在哪算一笔账SPI屏的刷新率瓶颈在SPI总线的带宽这个问题可以提前算清楚。以240x240分辨率、RGB565格式为例一帧数据量 240 x 240 x 2 115200字节如果SPI时钟跑40MHz传输一帧的时间 115200 x 8 / 40000000 ≈ 23ms对应理论帧率约43fps但这是纯数据搬运时间没有算命令开销、CS切换、GPIO操作和调度延迟。实际跑下来40MHz时钟下定时整刷能稳定在20到25fps已经不错了CPU占用还不低。如果你发现实际帧率远低于这个值优先检查这几件事SPI实际速率是不是被降频了比如信号质量差导致内核自动降低速度每次刷新是不是把整屏数据都搬了一遍没有利用窗口裁剪刷新线程是否被高优先级任务抢占有没有不必要的msleep或忙等5.4 画面撕裂与闪屏的几个隐藏原因画面撕裂在SPI屏上比RGB屏更隐蔽因为SPI屏不像VOP那样有固定的VSYNC机制。我自己遇到过一次非常诡异的“上部正常下部乱掉”的问题。排查到最后发现是刷新worker在SPI传输数据的过程中用户程序正在往显存写入新帧导致SPI搬运的数据一半是旧帧、一半是新帧。解决方案是加一个简单的刷帧锁刷新开始时把显存数据快照到一个临时dma buffer或者干脆接受单缓冲并用一个自旋锁保护关键写入口。闪屏的原因往往和背光时序有关。如果初始化ST7789V之前就把背光打开上电瞬间屏幕内部状态不确定会闪一下白屏。另外屏幕内部电源稳定需要时间背光开启太早会让用户看到电源纹波带来的闪烁。我的经验是复位完成后延时200ms初始化命令发完清屏最后开背光。6. 显示效果与产品化细节旋转、背光、叠加LVGL6.1 竖屏改横屏一条MADCTL命令还是绕不开坐标旋转SPI LCD做产品经常遇到竖屏改横屏的需求。ST7789V这类IC通过MADCTL寄存器0x36可以控制像素扫描方向本质上就是控制行/列地址的增长方向。MADCTL寄存器里有一个MY位、一个MX位、一个MV位组合起来可以实现四种旋转方向。我不建议记住每个组合的二进制值而是建议在设备树里定义rotate度数驱动里查表设置static u8 lcd_rotate_to_madctl(int rotate) { switch (rotate) { case 90: return MADCTL_MV | MADCTL_MX; case 180: return MADCTL_MY | MADCTL_MX; case 270: return MADCTL_MV | MADCTL_MY; default: return 0; } }这样产品的机械方向确定后改一个设备树属性就行。但有一点要注意MADCTL只改变扫描方向不改变FrameBuffer的逻辑分辨率。如果屏幕是240x240正方形旋转无影响如果是320x240这类矩形屏旋转后xres和yres要交换驱动里分配显存时就要考虑这个因素。6.2 背光控制从“能亮”到“能调”简单的SPI屏背光是一个GPIO只能开和关。产品上如果要做亮度调节建议用PWM背光。RK3568上有PWM控制器设备树里可以这样声明backlight: pwm-backlight { compatible pwm-backlight; pwms pwm3 0 50000 0; brightness-levels 0 20 40 80 160 255; default-brightness-level 3; status okay; };内核的pwm-backlight驱动会生成/sys/class/backlight/pwm-backlight/brightness节点用户态echo数值进去就能调亮度。要注意PWM引脚和SPI引脚可能又有复用冲突配设备树时先查pinctrl。如果驱动里想自己控制PWM可以在probe里获取pwm句柄按亮度和占空比映射表操作但绝大多数场景用内核现成的pwm-backlight就够。6.3 用LVGL驱动/dev/fb0把UI跑起来FrameBuffer模式的SPI LCD在现代嵌入式产品里最经典的搭配就是LVGL。LVGL官方的linux framebuffer驱动会直接打开/dev/fb0然后当成一块普通显示画布使用。我实际项目中的操作路径是确认/dev/fb0存在并且文件权限正确用户态程序打开/dev/fb0LVGL初始化时调用lv_linux_fbdev_create()传入fb0的fdLVGL内部通过mmap映射显存然后调用lv_refr_now()刷新配合局部刷新区LVGL只需要重绘变化的矩形区域然后由驱动把脏矩形通过SPI刷到屏幕。这样设计下一个240x240的界面刷新率完全够用CPU占用也低。6.4 最后的调试建议把fbcon挂到屏上所有驱动代码都跑通之后我强烈建议你在内核启动参数里加上fbconmap:0把内核console输出重定向到FrameBuffer上。这样做的好处是以后驱动调试不需要再接串口看日志内核启动信息和应用程序的printf信息都会显示在SPI小屏上非常直观。我自己的RK3568项目中就是靠这个方式在屏上看到了内核崩溃栈快速定位了一个GPIO申请失败导致probe退出的问题。另外再分享一个实用技巧调试阶段把/sys/class/graphics/fb0/rotate写一下看看FB方向切换对你当前的MADCTL配置有没有叠加影响这样产品改旋转方向时不会在驱动和用户态之间两头踩坑。这块屏的方案做完之后后续如果再接到不同型号的SPI LCD基本上就是换一个初始化命令表、改一下分辨率宏定义的事。剩下的FrameBuffer架构、刷新机制、DMA缓冲、设备树框架都可以复用这也是为什么值得在这条路上花时间把驱动吃透的原因。
返回列表