RK3568 SPI小屏FrameBuffer驱动实战:从设备树到性能优化
前阵子手头有个RK3568的小项目需要带一块1.54寸、240x240分辨率的SPI接口LCD用来做状态显示同时不想占用主显示资源。我选了SPI屏驱动直接走Linux标准的FrameBuffer框架。整个过程从硬件接线、设备树配置到驱动代码、性能优化和排障踩了不少坑。这篇就把完整路径整理出来给后面做同类屏幕的人留个参考。先说明一下背景RK3568本身主显示链路都是DRM框架RGB、MIPI DSI、LVDS这些屏幕都有现成驱动模型但SPI小屏属于低带宽、小分辨率的应用场景用DRM那套反而费劲。FrameBuffer模式简单直观应用层直接/dev/fb0读写就能显示内容而且内核fbtft框架已经cover了一大批SPI屏幕驱动。对嵌入式快速落地来说这是省事且稳定的方案。1. 项目背景与方案选型拆解1.1 为什么是RK3568加SPI小屏RK3568这颗SoC的算力做UI交互其实绰绰有余但不代表所有场景都要上复杂显示方案。实际项目里有一类需求特别典型设备只需要一个状态窗口显示几个数字、跑马灯、二维码、当前工作模式屏幕尺寸在1寸到2寸之间分辨率最高也就是240x320这类级别。这类屏幕用RGB并口或MIPI DSI有点杀鸡用牛刀因为引脚数量多RGB并口至少要16根数据线加控制线对小项目布线不友好MIPI DSI在低分辨率屏上成本高屏幕器件本身也贵这类屏幕的刷新率要求其实不高状态显示场景每秒10帧以内都能接受。SPI接口LCD只需要SCK、MOSI、CS、DC、RESET、BLK这么几根线四线或六线就能驱动任何一个GPIO口多的ARM平台都能带。RK3568的SPI控制器数量充足频率也能跑到几十MHz跑240x240这种小屏完全够用。1.2 FrameBuffer方案为什么比DRM省心既然屏幕是SPI小屏驱动模型的选择有两条路一条是接入DRM/KMS一条是走传统FrameBuffer。DRM框架理论上更现代化但要做的东西就多了connector、encoder、crtc、plane四个对象要串起来还要和RK3568本身的VOP显示链路匹配调度。一个SPI屏本身没有VSYNC中断没有自刷新机制硬塞进DRM的atomic提交流程里调试成本非常高。FrameBuffer方案简单直接内核分配一块显存缓冲区应用层通过mmap直接操作驱动只要实现基本的fb_ops把显存内容通过SPI搬到屏幕GRAM里不需要管DRM里那一堆对象模型和状态管理。而且RK3568的另一个显示控制器VOP只用来看主屏幕或HDMI输出SPI屏的FrameBuffer驱动完全可以用独立的spi master fbdev子系统两者互不干扰。这在嵌入式Linux里是老牌做法文档多、社区有人踩过坑、工具链成熟。从实际进度来算DRM方案光打通链路可能就要两三天FrameBuffer方案一天内就能点亮屏幕并显示出彩条测试图案。1.3 整体数据传输链路从应用层到屏幕硬件数据流大概是这样的应用层写/dev/fb0实际是写进FrameBuffer驱动分配的内存缓冲区驱动通过某种机制感知到内容变化后把这块内存里的像素数据按屏幕要求打包通过SPI接口发送给LCD控制器控制器把数据写进自己的GRAM再定时刷新到液晶面板上。在Linux系统中这个过程被抽象为用户程序 - write/mmap - fb_ops - SPI传输 - LCD控制器GRAM -面板其中用户程序可以只往一个固定的内存地址里填颜色值对屏幕刷新过程完全无感。这也是FrameBuffer最方便的地方上层的Qt、LVGL、直接画点程序都能跑。2. 硬件设计要点与屏体参数确认2.1 常见SPI LCD控制器和引脚定义市面上1寸到2寸的SPI屏常见控制芯号有ST7789V、ST7735S、ILI9341、GC9A01这几类。ST7789V在240x240和240x320屏上非常常见ILI9341一般出现在2.4寸320x240屏上GC9A01则是圆形屏常用。这类屏幕接口大同小异引脚一般包括SCL/SCKSPI时钟SDA/MOSI命令和数据输入DC也叫RS、A0区分命令还是数据高电平为数据低电平为命令CS片选RST复位低电平有效BLK/BL背光控制VCC、GND电源和地。注意有一部分屏幕没有独立的DC引脚而是通过SPI 9-bit模式传输用第一个bit来区分命令数据。这种屏驱动方式稍有不同初期调试建议优先选有DC引脚的屏兼容性更好。2.2 电平匹配、背光复位这些细节不能省RK3568的GPIO和SPI引脚都是3.3V逻辑绝大多数SPI屏模块也是3.3V逻辑可以直接怼。但有些便宜屏模块上带了5V背光供电或者全模块5V兼容设计这种情况我一般会在供电和信号线上加电平转换或至少串电阻防止模数转换接口电平不确定导致屏幕花屏或者芯片发热。背光控制最省事的做法是直接用GPIO拉高如果要调节亮度最好接到RK3568的PWM引脚上通过pwm-backlight驱动控制。如果屏幕走的是模块排针BLK引脚串一个小电阻再接3.3V或背光电源不要直接悬空。复位脚的处理也要注意。很多屏模块复位脚有上拉但芯片复位时序要求电源稳定后至少拉低10us以上再释放否则初始化序列容易在错误状态下执行。我会把RST接到GPIO上由驱动控制而不是简单拉高这样上电时序可控排查问题也方便。2.3 确认屏体参数和初始化序列来源写驱动之前最关键的是找对屏幕数据手册和初始化序列。同一个控制芯片不同模组厂给出的初始化序列可能不一样屏厂提供的初始化代码一般是一串SPI写寄存器操作比如ST7789V常见的0x01软复位之后延时150ms0x36设置扫描方向和RGB/BGR顺序0x3A设置像素格式为16bit/18bit0x2A、0x2B设置列地址、行地址范围0x2C开始写显存数据RAMWR0x21反色显示INVON。很多人屏幕点不亮问题往往出在初始化序列不对或者时序延时不够。这里我的习惯是先用逻辑分析仪抓一遍手头屏幕在厂家例程里跑出来的初始化波形确认每个寄存器值真的发了出去再在Linux驱动里对照着写。没有逻辑分析仪的情况下也可以先跑通用初始化序列点亮后再逐个精简。3. 设备树配置与SPI控制器的联动3.1 RK3568 SPI节点的设备树写法RK3568有多个SPI控制器在设备树里的节点路径一般是spi0、spi1……具体用哪一路要看你的原理图。我手头这块底板把SPI小屏接在了SPI1的CS0上设备树大致长这样spi1 { status okay; pinctrl-names default; pinctrl-0 spi1m1_cs0 spi1m1_pins; spi-max-frequency 24000000; lcd_st7789: st77890 { compatible vendor,spi-st7789; reg 0; spi-max-frequency 24000000; dc-gpios gpio3 RK_PB3 GPIO_ACTIVE_HIGH; reset-gpios gpio3 RK_PB4 GPIO_ACTIVE_LOW; backlight-gpios gpio3 RK_PB5 GPIO_ACTIVE_HIGH; rotation 0; bpp 16; }; };这里的pinctrl引用了两个spi1m1_cs0和spi1m1_pins这是RK3568里SPI1第二组复用的引脚定义。数据手册上每个SPI控制器都有m0、m1等多组引脚复用必须和实际原理图对应否则SPI总线不工作内核挂载spi设备时表现为总线无响应或者设备不存在。3.2 硬件片选与软件片选怎么选SPI片选有两种方式硬件片选hardware CS和软件片选software CS。硬件片选由SPI控制器自动控制CS引脚传输开始时拉低结束后拉高CPU不需要干预时序稳定。设备树里直接用默认的pinctrl片选即可。软件片选是把CS做成GPIO在驱动里绕过了控制器的硬件片选逻辑自己通过gpiod_set_value控制。这种方式适合CS引脚不在SPI控制器标准片选复用上、或者一个SPI总线上挂了多个器件需要灵活切换的场景。对驱动开发来说能用硬件片选尽量用硬件片选。我自己刚开始为了省事想用普通GPIO模拟整个SPI结果发现时钟和片选时序很难保证稳定刷屏稍快就出现命令漏发或错发。换成SPI控制器硬件片选后问题立刻消失。软件片选只推荐给那些读时序特别宽松、速率要求不高的屏。在设备树里如果想用软件片选可以配置cs-gpios属性spi1 { status okay; cs-gpios gpio3 RK_PB6 GPIO_ACTIVE_LOW; ... };但要留意启用cs-gpios后内核会让SPI控制器放弃硬件CS控制如果你的时序敏感还需要自己在驱动里确认CS拉低时机。3.3 背光、电源GPIO和pinctrl复用冲突排查设备树里除了SPI节点背光控制也很重要。如果直接用GPIO拉背光可以在LCD节点里配backlight-gpios驱动probe时获取这个GPIO并拉高。如果要用PWM调节亮度则单独配一个pwm-backlight节点backlight: pwm-backlight { compatible pwm-backlight; pwms pwm5 0 20000 0; brightness-levels 0 10 30 60 100 150 200 255; default-brightness-level 5; status okay; };然后把pwm5的复用改为pwm而不是gpio。RK3568的引脚复用很容易踩坑。比如我一开始背光GPIO选了GPIO3_B5但这个引脚默认复用功能是UART2_TX结果GPIO控制完全无效。查了半天才发现同一个引脚既被pinctrl配置成了uart功能又被GPIO子系统请求虽然GPIO请求成功但引脚实际由外设功能接管。遇到这种情况要么换引脚要么在设备树里把对应pinctrl-0设置为gpio。举例来说如果背光引脚复用冲突可以在节点里显式声明pinctrl为gpiobacklight-gpio { compatible gpio-leds; pinctrl-names default; pinctrl-0 backlight_pin; status okay; };再用pinctrl把它设成gpio功能pinctrl: pinctrl { backlight_pin: backlight-pin { rockchip,pins 3 RK_PB5 0 pcfg_pull_none; }; };注意这里的rockchip,pins里的function选0表示GPIO复用否则又会被其他外设吃掉。4. FrameBuffer驱动的核心实现4.1 自研驱动还是基于fbtft改造内核staging目录下有个fbtft框架封装好了ST7735S、ST7789V、ILI9341等一批常见SPI屏驱动理论上我们可以直接在menuconfig里打开CONFIG_FB_TFT_ST7789在设备树里加个fbtft节点系统起来后就会自动创建fb设备。但实际用下来fbtft适合快速验证真正量产或做复杂功能时我更倾向基于它改造或者干脆自研一个精简驱动。原因有几个fbtft的刷屏方式相对简单整屏刷新效率不算高平台相关的背光、复位、电源管理自定义不够灵活有些屏幕的初始化序列和fbtft内置模板不一致需要自己改。这次的项目里我选择自研驱动但参考了fbtft的框架思路。核心结构就是spi_driver加fb_info代码量不大可控性强。4.2 驱动框架从spi_driver到fb_info自研驱动的主体结构分成三块SPI驱动注册用spi_driver匹配设备树里的compatibleprobe回调解析GPIO、分配fb_info、申请显存、下发屏幕初始化序列fb_ops实现提供fillrect、copyarea、imageblit、blank等操作。probe里最核心的代码是分配并注册fb_infostatic int st7789_probe(struct spi_device *spi) { struct fb_info *info; struct st7789_par *par; int ret; par devm_kzalloc(spi-dev, sizeof(*par), GFP_KERNEL); if (!par) return -ENOMEM; par-spi spi; spi_set_drvdata(spi, par); par-dc devm_gpiod_get(spi-dev, dc, GPIOD_OUT_LOW); par-reset devm_gpiod_get(spi-dev, reset, GPIOD_OUT_HIGH); par-backlight devm_gpiod_get(spi-dev, backlight, GPIOD_OUT_HIGH); /* 复位时序 */ gpiod_set_value(par-reset, 1); msleep(50); gpiod_set_value(par-reset, 0); msleep(50); gpiod_set_value(par-reset, 1); msleep(120); info framebuffer_alloc(sizeof(*par), spi-dev); if (!info) return -ENOMEM; info-par par; par-info info; /* 设置固定参数和可变参数 */ strcpy(info-fix.id, st7789); info-fix.type FB_TYPE_PACKED_PIXELS; info-fix.visual FB_VISUAL_TRUECOLOR; info-fix.line_length ST7789_WIDTH * 2; info-fix.smem_len ST7789_WIDTH * ST7789_HEIGHT * 2; info-var.xres ST7789_WIDTH; info-var.yres ST7789_HEIGHT; info-var.xres_virtual ST7789_WIDTH; info-var.yres_virtual ST7789_HEIGHT; 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-fbops st7789_fbops; info-screen_buffer dma_alloc_coherent(spi-dev, info-fix.smem_len, par-dma_addr, GFP_KERNEL); if (!info-screen_buffer) { framebuffer_release(info); return -ENOMEM; } st7789_init_sequence(par); st7789_write_display_on(par); ret register_framebuffer(info); if (ret 0) { dma_free_coherent(spi-dev, info-fix.smem_len, info-screen_buffer, par-dma_addr); framebuffer_release(info); return ret; } return 0; }有一点容易忽视screen_buffer是给应用层mmap和写操作直接使用的缓冲区建议用dma_alloc_coherent分配这样如果你后面想用SPI DMA传输显存物理地址是连续的可以直接丢给DMA引擎。如果只是用PIO模式跑SPI用kmalloc分配也能工作但大数据量传输时性能明显不如DMA。4.3 fb_ops里必须实现的几个关键APIfb_ops是FrameBuffer驱动和应用层交互的核心。对SPI屏来说最值得关注的是这几个操作static struct fb_ops st7789_fbops { .owner THIS_MODULE, .fb_setcolreg st7789_setcolreg, .fb_blank st7789_blank, .fb_fillrect sys_fillrect, .fb_copyarea sys_copyarea, .fb_imageblit sys_imageblit, .fb_mmap st7789_mmap, .fb_deferred_io st7789_defio, };这里使用sys_fillrect、sys_copyarea、sys_imageblit而不是cfb_*系列原因是fb_deferred_io模式下系统默认会用shmem管理的可换页内存这些操作直接作用在用户内存映射上配合后续dirty_page机制做延迟刷新。fb_mmap的实现也要特殊处理。如果使用fb_deferred_io驱动需要把显存区域映射成带faulthandler的VMA每次页面被写入时记录dirty状态static int st7789_mmap(struct fb_info *info, struct vm_area_struct *vma) { return fb_deferred_io_mmap(info, vma); }如果不使用deferred_io就直接把分配的dma buffer映射给用户vma-vm_page_prot pgprot_writecombine(vma-vm_page_prot); return dma_mmap_coherent(info-dev, vma, info-screen_buffer, par-dma_addr, info-fix.smem_len);从开发效率看我推荐先用deferred_io省心后面优化性能时再改成自定义刷新策略。4.4 SPI传输函数命令与数据的封装SPI屏通信的关键就是DC引脚区分命令和数据。驱动里我会封装两个函数static int st7789_write_cmd(struct st7789_par *par, u8 cmd) { gpiod_set_value(par-dc, 0); return spi_write(par-spi, cmd, 1); } static int st7789_write_data(struct st7789_par *par, const u8 *data, size_t len) { gpiod_set_value(par-dc, 1); return spi_write(par-spi, data, len); }注意写命令时DC要先拉低再触发SPI传输写数据前要把DC拉高。这里有一个隐藏问题如果连续写多条命令每条之间都要切换DCGPIO翻转很快会有一定的CPU开销。更好的做法是最终驱动里用SPI单独bit或特殊模式但简单场景下这个封装完全够用。屏幕初始化序列其实就是一堆命令加数据。以ST7789V 240x240屏为例关键初始化片段如下static void st7789_init_sequence(struct st7789_par *par) { st7789_write_cmd(par, 0x01); /* SWRESET */ msleep(150); st7789_write_cmd(par, 0x11); /* SLPOUT */ msleep(120); st7789_write_cmd(par, 0x36); /* MADCTL */ st7789_write_data(par, (u8[]){0x00}, 1); st7789_write_cmd(par, 0x3A); /* COLMOD */ st7789_write_data(par, (u8[]){0x55}, 1); /* 16bit/pixel */ st7789_write_cmd(par, 0x2A); /* CASET */ st7789_write_data(par, (u8[]){0x00, 0x00, 0x00, 0xEF}, 4); st7789_write_cmd(par, 0x2B); /* RASET */ st7789_write_data(par, (u8[]){0x00, 0x00, 0x00, 0xEF}, 4); st7789_write_cmd(par, 0x21); /* INVON */ st7789_write_cmd(par, 0x13); /* NORON */ st7789_write_cmd(par, 0x29); /* DISPON */ msleep(20); }这里的0x00到0xEF就是屏体240x240分辨率对应的行/列地址范围。如果换了320x240的ILI9341屏这个范围就要改成0x00到0x13F和0x00到0x1DF。初始化序列错了最常见的表现就是花屏或部分屏幕不显示。4.5 刷屏实现从整屏刷新到deferred_io刷屏是FrameBuffer驱动里最影响体验的部分。最粗暴的做法是只要有人写显存就把整个屏幕的240x240x2字节全部刷一遍。在小屏上这样做没问题但要考虑效率。我采用fb_deferred_io的懒刷新机制核心思想是应用层写入显存时先不急着刷屏等一小段时间比如20ms或者等到驱动的刷新回调被触发再把脏页数据统一搬给屏幕。内核提供了fb_deferred_io_init和fb_deferred_io_cleanup接口配合一个定时器进行聚合。刷屏回调如下static void st7789_fb_dirty(struct fb_info *info, u32 x, u32 y, u32 width, u32 height) { struct st7789_par *par info-par; u32 line_len info-fix.line_length; u32 offset y * line_len x * 2; u8 *buf info-screen_buffer offset; size_t len width * height * 2; st7789_set_window(par, x, y, x width - 1, y height - 1); st7789_write_cmd(par, 0x2C); st7789_write_data(par, buf, len); }注意st7789_set_window就是发CASET和RASET命令。这部分做得好后面局部刷新就顺理成章。如果不使用deferred_io也可以跑一个内核线程或者用mod_timer定时全屏刷但那样刷新范围和时机都不够精细屏幕显示内容变化频繁时会出现撕裂感。我强烈建议新写的SPI屏驱动都从deferred_io起步。4.6 与开机Logo和内核启动显示的衔接FrameBuffer驱动注册成功后内核里的fbcon可以在启动阶段直接把printk信息输出到SPI小屏上。这个功能本身不用额外配置只要fbcon模块加载时检测到fb0就会尝试切过去。如果不想让内核信息干扰应用显示可以改内核启动参数fbconmap:1或者fbconnodefer这里的小技巧是如果你希望SPI屏显示开机动画同时又不想让内核日志疯狂刷屏拖慢启动配合quiet参数使用显示效果会干净很多。如果是uboot阶段就想显示logo那工作量要更早介入uboot里也要有对应的SPI LCD驱动和fb框架并且保证uboot和内核的初始化序列一致否则会出现uboot显示正常、跳转到内核后黑屏几秒再亮的现象。这个衔接问题属于bootloader侧的事本文不展开但如果你发现开机logo阶段和内核阶段屏幕显示风格不一致大概率是两者初始化参数没对齐。5. 性能优化思路与实际效果5.1 SPI速率、DMA传输和刷屏帧率实测很多人以为SPI屏只能跑到几帧每秒其实主要看SPI时钟和刷屏策略。240x240x16bit一帧数据量是115200字节加上命令开销假设SPI时钟跑20MHz理论传输时间约46ms帧率上限大约21fps如果能稳定跑到40MHz理论传输时间约23ms帧率上限可以到43fps。但这只是纯理论值实际还要算上GPIO翻转DC的开销、CPU搬运数据的开销、以及deferred_io定时器等系统调度损耗。我在这块板子上实测20MHz PIO模式整屏刷新大约60ms一帧约16fps40MHz PIO模式整屏刷新大约45ms一帧约22fps40MHz DMA模式整屏刷新大约35ms一帧约28fps。PIO模式到40MHz时CPU占用已经很高因为每次spi_write都要反复读写SPI FIFO。这个时候最有效的优化就是让SPI控制器走DMA把CPU释放出来。RK3568控制器自带的DMA传输并不难配置。如果驱动里用的是spi_sync_transfer并且传输缓冲区是dma_alloc_coherent分配的我们可以实现can_dma回调让spi核心自动选择DMA路径static bool st7789_spi_can_dma(struct spi_controller *ctlr, struct spi_device *spi, struct spi_transfer *xfer) { return xfer-len 16; }然后spi_register_driver前给spi控制器或者spi设备设置相应的能力位。DMA不是银弹短命令传输用DMA反而因为准备开销导致性能下降所以我只在数据量大于16字节时启用DMA。5.2 局部刷新、双缓冲与数据的坑比起盲目整屏刷新应用层场景里更实用的是局部刷新。状态显示界面通常只有某个区域在变化比如温度数字、进度条。驱动实现了set_window后应用层只要把脏矩形坐标通过自定义ioctl传给驱动或者在deferred_io基础上自己维护回调和脏区域标记就可以只刷新变化区域。局部刷新的性能提升非常可观刷新一个64x64的区域数据量8192字节20MHz下传输时间只有3.2ms刷新一个128x64的区域数据量16384字节传输时间约6.5ms整屏240x24020MHz下传输时间约46ms。对UI交互来说局部刷新能轻松达到30fps以上的更新速率肉眼几乎无延迟。双缓冲主要是处理撕裂问题。SPI屏没有VSYNC同步信号应用层画到一半被驱动刷走画面就会出现撕裂。使用fb_deferred_io后如果应用层连续写显存刷新回调可能把半成品画面发出去。解决思路有两个一是让应用层做双缓冲画完再memcpy到fb0二是驱动里维护一个内部shadow buffer配合定时器只在空闲时刻刷屏。颜色顺序是另一个大坑。ST7789V的GRAM默认有RGB和BGR两种排列模式MADCTL寄存器里的RGB位决定颜色顺序。设备树里我配了16bit RGB565像素格式但如果MADCTL没配好屏幕显示颜色就会偏色比如红色和蓝色互换。排查时在应用层画一个纯红块然后用示波器或逻辑分析仪看发送数据是最快的确认方法。5.3 内核配置里不能少的几个选项编译内核时FrameBuffer相关配置项要特别注意。如果少了CONFIG_FB_DEVICE/dev/fb0节点可能不会自动创建如果少了CONFIG_FB_SYS_FILLRECT之类的辅助函数驱动里使用sys_fillrect会链接失败。我一般会打开以下配置项CONFIG_FByCONFIG_FB_DEVICEyCONFIG_FB_SYS_FILLRECTyCONFIG_FB_SYS_COPYAREAyCONFIG_FB_SYS_IMAGEBLITyCONFIG_FB_DEFERRED_IOyCONFIG_FB_BACKLIGHTy然后根据屏幕控制芯片选择是否有专门驱动。如果自研驱动就在drivers/video/fbdev/Kconfig里加一个配置项避免每次手动insmod。6. 调试方法与常见问题排查实录6.1 屏幕全白或全黑的定位步骤屏幕全白初始化序列执行了但显示数据没有正确写进GRAM或者GRAM被清成白色。这种情况优先查初始化序列是否完整特别是SLPOUT后有没有给足延时检查DC引脚极性是否反了。如果命令和数据反了屏幕大概率会呈现花白状态用spidev工具单独发几个命令确认SPI总线本身没问题。屏幕全黑可能初始化没完成或者背光没打开。先量背光引脚电压再确认DISPON命令有没有下发最后检查RST复位时序。我在调试时常用一个最土但很有效的方法在驱动probe完成后往fb0写一个纯色全屏图案dd if/dev/urandom of/dev/fb0 bs1024 count100如果屏幕出现随机雪花或彩条说明驱动链路已经通了问题仅在细节如果还是黑屏说明初始化或者SPI传输根本没走通。6.2 花屏、偏色和显示区域不对的根源花屏和偏色是SPI屏的家常便饭常见原因按可能性排序MADCTL扫描方向设置错误屏幕上下或左右颠倒或者x/y方向互换GRAM行列地址偏移设置错误比如屏幕实际从0列开始但驱动从52列开始显示内容就会整体偏移像素格式不匹配驱动发8bit数据屏体期待18bit模式SPI时钟相位极性不对导致第一个bit被吞掉画面整体左移一个像素。要确认SPI_CPOL和SPI_CPHA参数是否正确可以看屏幕数据手册里SPI时序波形图。ST7789V通常工作在mode 0CPOL0CPHA0但个别屏模块对时钟极性的定义有差别我遇到过必须设成mode 3才能稳定显示的情况。这个只能逐个试或者用逻辑分析仪对比厂家例程波形。6.3 fb设备节点异常的操作排查如果系统起来后没有/dev/fb0先看内核日志dmesg | grep -i fb dmesg | grep -i st7789常见情况有设备树compatible不匹配spi_driver的of_match_table没对应上SPI控制器节点status不是okayspi-max-frequency配置过高控制器枚举失败驱动probe中某个GPIO请求失败中断了注册流程。如果/dev/fb0存在但open失败大概率是权限问题或者fb设备没有正确注册。确认权限ls -l /dev/fb0 chmod 666 /dev/fb0有时候内核里fbcon会自动接管fb0这会导致用户程序write时只更新屏幕但看不到内容。解决方法是启动参数里加fbconmap:0或者干脆不注册fbcon。6.4 常见问题排查速查表现象可能原因排查手段屏幕全黑、背光亮初始化序列未完成、DISPON未执行检查上电时序加宽延时用spidev裸发DISPON屏幕全黑、背光灭背光GPIO配置错误或PWM未启动检查GPIO电压、pwm节点状态花屏、乱码SPI速率过高、DC极性反、初始化序列错降频到1MHz测试调DC极性对照datasheet查序列色彩偏色BGR/RGB位设置错修改MADCTL的RGB位测试显示位置偏移CASET/RASET范围错误、帧偏移参数不对读屏手册确认分辨率对应行列范围刷屏慢、卡顿PIO模式CPU占用高、整屏刷新过度开启DMA、改局部刷新屏幕闪烁、撕裂刷新时机不当、应用层绘制与刷新冲突使用deferred_io聚合、双缓冲/dev/fb0缺失设备树没配好、驱动probe失败查dmesg确认bus总线和compatible6.5 调试中我养成的三个习惯第一手边常备一根逻辑分析仪。SPI屏其实只有几根线抓一次完整初始化序列就能看到命令是否正确、时序是否满足。很多看起来玄学的问题抓波形后一眼就明白了。第二先用spidev把裸SPI读写跑通再写FrameBuffer驱动。spidev用户态工具比如spidev_test可以手工发送任意字节配合GPIO导出DC和RST能在不加载屏驱动的情况下把屏幕点亮。这个步骤排错了再进入驱动开发会顺畅得多。第三把初始化序列放在驱动里一个独立的数组里通过sysfs或ioctl动态下发。调试阶段可以随时在应用层修改寄存器值不用每次改动都重新编译内核模块。7. 扩展玩法与个人体会7.1 FrameBuffer上跑LVGL的轻量组合SPI小屏应用层最常见的UI方案是LVGL。LVGL官方支持fbdev作为刷新后端刷新回调里把脏矩形区域传给驱动只更新局部。这里有个关键点LVGL的flush_cb会告诉你要刷新的区域不能简单直接整屏提交。如果驱动已经实现了set_window局部刷新LVGL的flush回调大致可以这样写static void lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { struct fb_info *info open_fb(); u32 x area-x1; u32 y area-y1; u32 w area-x2 - area-x1 1; u32 h area-y2 - area-y1 1; /* 把颜色数据按RGB565写入fb的对应区域 */ fb_write_region(info, x, y, w, h, color_p); lv_disp_flush_ready(drv); }注意LVGL内部颜色格式需要和fb设置为一致一般是LV_COLOR_DEPTH16。如果两者字节序不一致颜色会偏。7.2 背光调节和屏保能力的扩展驱动里实现fb_blank回调后可以通过ioctl或sysfs控制面板开关和背光。内核的原生接口是FBIOBLANK应用层发FB_BLANK_POWERDOWN可以熄屏FB_BLANK_UNBLANK可以亮屏。但我实际使用中更多是把背光单独做成一个LED或者PWM背光设备因为fb_blank很多时候会连带关闭显示内容状态显示类应用不一定希望这样。在驱动里加一个简单的sysfs入口也很方便static ssize_t backlight_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { unsigned int brightness; sscanf(buf, %u, brightness); pwm_config(par-pwm, brightness, par-period); pwm_enable(par-pwm); return count; } static DEVICE_ATTR_WO(backlight);这样用户层echo亮度值就能调节不依赖额外的背光框架适合轻量场景。7.3 这套方案能平移到哪些平台SPI小屏加FrameBuffer驱动的思路不只适用于RK3568RK3288、RV1126、全志H3、NXP i.MX6ULL这些Linux平台都能用同样套路。只要SoC有SPI控制器内核支持fbdev子系统就能把这套驱动代码的核心逻辑搬过去改的主要是设备树引脚和时钟频率。做嵌入式开发久了就会发现屏幕驱动本质上是在处理“数据格式转换”和“时序控制”两件事。FrameBuffer提供标准数据接口SPI提供传输通道中间的屏幕初始化序列只是屏厂定义好的一套寄存器流程。理解了这三层任何SPI屏在Linux下都能很快点亮。最后再分享一个经验这类小屏驱动项目排错的大头往往不是内核代码而是硬件细节。刚拿到一款屏先用裸SPI点亮、确认初始化序列和引脚定义再开始写驱动流程会顺很多。我也在这块RK3568的板子上经历过“一上电就白屏—折腾半天发现是复位引脚悬空”这种低级失误。做驱动开发耐心和系统化排错比堆代码更重要。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →