资讯详情

资讯详情

RK3568平台SPI LCD驱动开发:FrameBuffer模式实战

搞RK3568调试的时候除了RGB屏、LVDS屏和HDMI这些大路货很多人还会遇到一个非常实在的需求给设备加一块低成本、低功耗的小尺寸状态面板。这个需求最划算的做法就是选一块SPI接口的LCD用FrameBuffer模式挂进Linux系统应用层直接往/dev/fbX写数据就能显示内容。这篇文章我就拿一块1.8寸、240x320、ST7789V控制芯片的SPI屏为例把RK3568平台下从设备树配置、驱动代码编写到实际点亮、刷新调优的完整过程从头过一遍。适合正在做RK3568/RK3566方案、需要接小屏做状态显示、开机画面或者轻量级界面的朋友参考。先说结论:这套链路本身不复杂真正的复杂度来自设备树节点怎么写、初始化序列怎么发、以及GPIO时序怎么保证。踩过一次坑之后你会发现SPI屏驱动其实是一通百通的工程并不是什么玄学。1. 方案选型为什么是SPI为什么还用FrameBuffer1.1 先看清SPI屏适合放在哪个位置RK3568作为一款带GPU、支持4K解码的Soc很多人习惯性认为接屏幕至少得RGB或者MIPI DSI。但实际上对一块1.8寸或者2.4寸的小屏来讲MIPI DSI接口屏不仅贵量也少RGB接口又需要占用大量引脚16位色至少16根数据线加若干控制线为了一个小状态面板去消耗这么多引脚资源并不划算。SPI接口的优势很明显TFT LCD通过SPI只需要SCK、MOSI两根信号线很多屏不接MISO再加上CS、DC、RST、BLK满打满算六根线就能点亮。引脚占用少layout好走线成本也低市面上一块ST7789V芯片的1.8寸屏几块钱就能拿到。缺点同样明显SPI是串行协议刷新带宽有限不适合高分、高刷场景但显示静态状态、CPU负载、网络信息、调试日志这种低频内容绰绰有余。选不选SPI屏本质上是“成本、引脚、刷新率”之间的取舍。如果你只是做一个数据面板SPI屏是性价比最高的答案如果要做动画、做拖动交互回头老老实实上RGB屏、LVDS屏或MIPI DSI别在SPI这条路上死磕。1.2 从DRM/KMS回退到fbdev的思考现在Linux内核在显示架构上主推DRM/KMSRK3568的官方BSP里HDMI、eDP、LVDS这些都走DRM路子。但FrameBufferfbdev这套老框架在内核里依然保留而且对SPI小屏来说用到今天依然很顺手。DRM为了管理现代显示设备的多样性和合成需求抽象出了plane、crtc、encoder、connector这一整套对象。这套抽象对复杂显示管线是必要的对一个只有一个固定分辨率、固定像素格式、没有硬件叠加层的SPI小屏来说反而显得规格过大。fbdev模型就简单直白得多你有一块显存把它映射到用户空间应用往显存里写像素驱动负责把这块像素数据刷到屏幕。写屏幕就是写文件学习和调试成本都很低。另外一个客观因素是历史积累。Linux内核里以drivers/video/fbdev/下躺着大量小屏fb驱动范例部分来自早期的fbtft框架社区踩坑经验也集中。做产品时与其在DRM的KMS驱动里去对接SPI panel不如直接在fb框架下把显示通道打通稳定可靠也方便维护。1.3 从/dev/fb0到像素一条完整链路怎么走整个显示调用链可以这样理解用户在应用层打开/dev/fb0通过mmap把内核里一块连续内存映射到用户地址空间然后往这块内存写入RGB565颜色数据。这部分写入会触发fb_defio的缺页处理把脏页收集起来由驱动内部的回调通过SPI控制器发送到LCD控制芯片。LCD控制芯片再把收到的数据写入它的GRAM显存由TFT面板持续刷新显示。这里有一个很多人第一次接触会犯迷糊的点SPI屏的控制芯片自身是有显存的比如ST7789V内部就带240x320x18bit的GRAM驱动需要把数据从SoC内存搬到芯片GRAM里。由于SPI是串行口这个搬运过程不可能像RGB接口那样整屏并行刷新只能一字节一字节发。所以驱动设计的核心就是两个问题显存缓存怎么管理、脏区域怎么高效发给屏幕。FrameBuffer框架正好把第一件事解决了剩下第二件事就是写SPI发送功能。2. 动手前的准备内核配置、设备树与硬件接线2.1 内核里需要打开哪些配置项在RK3568的BSP内核里默认可能已经打开了FrameBuffer框架但小屏驱动相关的一些配置需要确认。我这里的做法是使用menuconfig手动检查一遍。需要重点确认的配置项有这些CONFIG_FB老框架总开关必须为y。CONFIG_FB_CMDLINE、CONFIG_FB_NOTIFY辅助选项一般默认开启。CONFIG_FB_SYS_FILLRECT、CONFIG_FB_SYS_COPYAREA、CONFIG_FB_SYS_IMAGEBLIT这三个提供系统内存级绘图操作一般fb_ops里直接挂这三个函数建议开启。CONFIG_FB_DEFERRED_IO如果要用fb_defio实现脏页延迟刷新必须开启。这个对SPI屏来说很重要开启后能避免每次写入都整屏刷新。CONFIG_SPI_ROCKCHIPRK3568的SPI控制器驱动必须为y或m否则设备树注册了SPI设备也无法工作。CONFIG_GPIO_CDEV方便调试GPIO用建议开启。提示RK3568的BSP内核版本不同配置项名称可能有差异。如果menuconfig里搜索不到先确认内核源码是否完整、是否已经先make ARCHarm64 defconfig过。配置好之后重新编译内核和设备树烧录再去调驱动避免后面写驱动才发现框架没开。2.2 设备树节点逐行拆解RK3568有多个SPI控制器我接的是SPI1。设备树里需要同时配置SPI控制器节点和面板设备节点。这里给出一份实测可用的参考片段spi1 { status okay; pinctrl-names default; pinctrl-0 spi1_pins; spi-max-frequency 20000000; cs-gpios gpio3 RK_PB3 GPIO_ACTIVE_LOW; spi_lcd: spi-lcd0 { compatible my,spi-tft; reg 0; spi-max-frequency 20000000; rotate 90; bgr 1; reset-gpios gpio3 RK_PB4 GPIO_ACTIVE_LOW; dc-gpios gpio3 RK_PB5 GPIO_ACTIVE_LOW; backlight-gpios gpio3 RK_PB6 GPIO_ACTIVE_HIGH; status okay; }; };重点解释几个容易被忽略的地方cs-gpios这里采用的是软件片选方式把CS控制权从SPI控制器手里拿到了GPIO子系统。好处是极性和拉低时机可以自由控制调试时哪里不对可以直接拿逻辑分析仪量。切换成软件片选后SPI控制器只负责SCK和MOSICS由gpiolib控制。内核里pinctrl还要确保这组GPIO没被其他外设复用。spi-max-frequency我初始设成20MHz。RK3568的SPI控制器本身能跑到更高频率但屏模组端、杜邦线连接、上拉电阻都会限制实际稳定频率。板内走线可以尝试40MHz板外用杜邦线连接时不要超过20MHz否则容易出现花屏和偶发数据错位。compatible这里写成my,spi-tft对应驱动里的of_match_table。注意不要和内核已有的ilitek,ili9341这类标准compatible冲突自定义compatible最省事。reset-gpios和dc-gpios分别对应复位引脚和数据/命令选择引脚。很多教程直接把DC叫RS或者A0本质是一样的。DC引脚是SPI屏能不能正确显示的控制命门后面驱动里会重点讲。设备树配置完成之后先别急着写驱动在u-boot里可以利用i2c或spi的命令简单量一下引脚电平是否正常或者直接在Linux起来后用gpioinfo查看这几个GPIO对应的chip和line确认申请成功。2.3 硬件接线和上拉电阻经验我这次接线的引脚分配是这样的LCD引脚功能连接到RK3568VCC电源3.3VVCC3V3_SYSGND地GNDSCL/SCKSPI时钟SPI1_CLKSDA/MOSISPI数据SPI1_MOSICS片选GPIO3_B3DC命令/数据选择GPIO3_B5RST复位GPIO3_B4BLK背光GPIO3_B6这里最常见的一个坑是电平匹配问题。RK3568的GPIO一般工作在1.8V或3.3V逻辑电平如果你买的是5V供电版本液晶模组或者中间经过了一级电平转换芯片务必确认逻辑电平一致。直接3.3V的SoC接5V的屏逻辑引脚长期运行存在损坏风险。上拉电阻这件事也值得单独提。SPI总线在空闲时SCK和MOSI的电平应该保持稳定。实测下来如果屏和主机相距超过10cm的排线SCK和MOSI上加10k到47k欧姆的上拉到3.3V能明显降低花屏概率。背光引脚BLK一般接一个NPN三极管或MOS管控制不建议用GPIO直接驱动大电流背光电流不够时屏亮度低且不稳定。硬件连接完成后上电前用万用表量一遍VCC和GND之间有没有短路这几个GPIO有没有和附近引脚搭锡。很多时候驱动写完了屏不亮回头查硬件结果就是一根杜邦线接触不良。3. 驱动代码框架与关键参数推导3.1 spi_driver骨架与probe全流程驱动基于内核的spi_driver机制写。核心结构体长这样static const struct of_device_id spi_tft_of_match[] { { .compatible my,spi-tft }, { } }; MODULE_DEVICE_TABLE(of, spi_tft_of_match); static struct spi_driver spi_tft_driver { .driver { .name spi_tft, .of_match_table spi_tft_of_match, }, .probe spi_tft_probe, .remove spi_tft_remove, }; module_spi_driver(spi_tft_driver);probe函数里需要完成的步骤按我的习惯是这样排列获取设备树里配置的GPIO句柄包括reset、dc、backlight。设置SPI传输模式一般是SPI_MODE_0CPOL0、CPHA0具体看LCD数据手册时序图。申请并配置背光GPIO先把背光拉低等初始化完成后再点亮避免上电瞬间出现花屏噪点。执行LCD硬件复位拉低RST保持10ms再拉高保持20ms以上等待内部DCDC稳定。发送初始化命令序列。分配framebuffer缓存填充fb_info结构体注册到系统。整个probe流程里我强烈建议每一步都加dev_dbg或printk打印尤其是GPIO申请失败和SPI传输失败的地方。SPI屏驱动调试时最怕的就是屏不亮又没有任何日志完全是盲人摸象。3.2 fb_info结构体到底在配什么FrameBuffer驱动里最核心的数据结构是fb_info它包含两个子结构fb_var_screeninfo描述可变参数fb_fix_screeninfo描述固定参数。对于我的240x320、RGB565屏初始化代码如下static int spi_tft_fb_config(struct fb_info *info) { struct fb_var_screeninfo *var info-var; struct fb_fix_screeninfo *fix info-fix; 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-activate FB_ACTIVATE_NOW; var-nonstd 0; fix-smem_start (unsigned long)info-screen_buffer; fix-smem_len 240 * 320 * 2; fix-type FB_TYPE_PACKED_PIXELS; fix-visual FB_VISUAL_TRUECOLOR; fix-line_length 240 * 2; return 0; }这里最容易忽略的是xres_virtual和yres_virtual。SPI屏没有滚动缓冲的需求虚拟分辨率直接跟物理分辨率一致即可避免应用层fbmem计算映射地址时出现偏差。bits_per_pixel设置成16后一帧大小是240 * 320 * 2 153600字节约150KB这个数值后面在计算刷新耗时和选择SPI频率时非常关键。fix-smem_start指向的info-screen_buffer是通过framebuffer_alloc和fb_deferred_io_init分配的内存。注意这块内存不是LCD内部GRAM而是我们在系统内存里建的缓存。应用层mmap映射的是这块内存刷屏时把这块缓存的数据通过SPI搬运到LCD的GRAM中。理解了这一点就不会把smem_start误认为是什么寄存器地址了。fb_ops里填充三个通用绘图函数即可static struct fb_ops spi_tft_fb_ops { .owner THIS_MODULE, .fb_read fb_sys_read, .fb_write fb_sys_write, .fb_fillrect sys_fillrect, .fb_copyarea sys_copyarea, .fb_imageblit sys_imageblit, .fb_blank spi_tft_blank, };fb_blank回调需要自己实现作用是开关显示和背光。这里有个细节blank时不要直接关背光应该先发SLPOUT或者DISPON/OFF命令再延时几个毫秒最后拉背光。顺序反了容易出现关屏瞬间的白色残影。3.3 DC引脚SPI屏驱动的命门SPI屏驱动和普通SPI设备驱动最大的不同是必须有命令/数据切换机制。ST7789V这类控制芯片接收到的第一个字节如果是命令则进入命令解析模式如果是数据则写入当前命令对应的寄存器或GRAM。这个“命令/数据”选择由DC引脚完成。驱动内部需要封装两个最基本的函数static void lcd_send_cmd(struct spi_device *spi, u8 cmd) { gpiod_set_value(dc_gpio, 0); // DC拉低发送命令 spi_write(spi, cmd, 1); udelay(1); } static void lcd_send_data(struct spi_device *spi, u8 data) { gpiod_set_value(dc_gpio, 1); // DC拉高发送数据 spi_write(spi, data, 1); }这个逻辑看似简单实际调试中很多问题都出在这里。比如DC引脚设置的是GPIO_ACTIVE_LOW那gpiod_set_value(...,0)对应的是高电平还是低电平取决于设备树里flag的定义。实际用GPIO_ACTIVE_LOW时gpiod_set_value(1)才是硬件上的低电平。搞反了整个屏幕的初始化序列都会错乱表现就是白屏或者显示内容一片随机噪点。批量发送数据时不要一字节一字节地调用spi_write那样每次都要重新配置spi_transfer开销很大。更好的做法是构建一个spi_transfer数组一次把所有数据发给控制器static void lcd_send_data_buf(struct spi_device *spi, u8 *buf, size_t len) { struct spi_transfer xfer { .tx_buf buf, .len len, .speed_hz 20000000, }; gpiod_set_value(dc_gpio, 1); spi_sync_transfer(spi, xfer, 1); }这里还需要注意字节序。RGB565数据在Buffer里是低字节在前、高字节在后小端而ST7789V的GRAM默认接收的是高字节在前大端。因此很多驱动里刷屏时会做一个字节交换或者用fb_set_par里通过COLMOD寄存器的颜色顺序位来做适配。实际调试时如果发现颜色偏色、红蓝互换先查这个字节序问题不要急着改Gamma。3.4 初始化序列一屏能否点亮的关键ST7789V这类屏上电后并不会自动进入可显示状态需要按照数据手册里的初始化序列依次发送多个寄存器配置命令。不同厂家的模组虽然都用ST7789V芯片但引脚接线和面板参数可能不同所以官方例程里的伽马值、偏压设置不一定直接适用。我实际使用中验证过的一套初始化简化序列如下static const struct lcd_init_cmd init_cmds[] { { 0x01, 0 }, // SWRESET软复位 { 0x11, 0 }, // SLPOUT退出睡眠模式 { 0x36, 1, 0x60 }, // MADCTL设置扫描方向和RGB/BGR顺序 { 0x3A, 1, 0x05 }, // COLMOD16bit RGB565 { 0x20, 0 }, // INVOFF关闭反转显示 { 0x13, 0 }, // NORMALON正常工作模式 { 0x29, 0 }, // DISPON打开显示 };其中最重要的两个寄存器是MADCTL0x36和COLMOD0x3A。MADCTL控制屏幕显示方向0x00是横屏左上到右下0x60代表竖屏并且使能BGR颜色顺序。调试时屏幕方向不对或者红蓝颜色反了都是改这个寄存器。COLMOD的0x05对应RGB565格式如果设成0x06则变成RGB666而很多应用层默认按双字节一个像素写数据格式不对就会出现拉伸和花屏。初始化序列执行完之后建议等待120ms再继续操作。有些屏芯片内部DCDC需要时间稳定过早发DISPON会出现亮屏但显示内容未就绪的现象表现就是背景白色但没有任何图像。3.5 刷屏回调与FbDefio的取舍SPI屏的刷屏方式有两种常见实现。第一种是接到fb_info的fb_blank或者自定义的ioctl里整屏刷新这种方式代码最简单在需要刷新时手动调用一次全量刷新函数把160KB数据通过SPI全发一遍。缺点也直接任何局部修改都会整屏重发刷新慢应用写屏会明显感觉到闪烁。第二种是使用内核的fb_defio机制。原理是应用通过mmap映射显存后内核通过缺页异常跟踪哪些页面被写入了在系统调度到某个延迟工作队列时把所有脏页找出来只把这些页对应的屏幕区域发给LCD。对我的240x320屏来说页大小4KB28个页面就能覆盖全屏局部刷新时往往只有几个页需要传输传输量能下降80%。实际使用fb_defio时需要注意它的触发时延。fb_deferred_io有一个delay参数单位是ms默认值在几十毫秒量级调太小会导致刷新频繁、CPU占用高调太大则操作有延迟感。我做状态显示时设置成30ms既保证更新及时也不会让SPI总线持续满载。配置fb_defio的关键步骤static struct fb_deferred_io spi_tft_defio { .delay HZ / 30, .deferred_io spi_tft_damage_handler, }; info-fbdefio spi_tft_defio; fb_deferred_io_init(info);spi_tft_damage_handler在整个刷新周期里做两件事计算需要刷新的矩形区域然后调用lcd_update_rect把这块区域的像素通过SPI发出去。很多新手会忽略的是deferred_io的回调运行在workqueue上下文不能在回调里调用spi_sync这种睡在进程上下文等待传输的函数实际上spi_sync在可以睡眠的上下文是允许的回调函数在进程上下文也确实能调用。但如果要做得更精细可以在回调里只收集脏页区域然后额外开内核线程执行SPI传输避免阻塞系统其它工作队列。实测下来简单屏直接调用spi_sync问题不大只要传输总时间控制在几十毫秒内。4. 调试实录从白屏到完美显示4.1 点屏第一件事先打成纯色驱动写完后我建议不要上来就指望它显示完整桌面先在串口控制台下做最基础的验证。先确认驱动有没有正常probedmesg | grep spi_tft ls /dev/fb*如果看到/dev/fb0已经生成再写纯色测试dd if/dev/zero of/dev/fb0 bs1024 count150这条命令把fb显存全部清零屏幕理论上应该显示全黑。如果显示全黑说明命令通道、数据通道、初始化流程大概率是通的。然后再写全白printf \xFF\xFF | dd of/dev/fb0 bs1 seek0 count153600 convnotrunc如果全白正常显示说明GRAM写入路径和像素格式没问题。之后再尝试写一个红色块、绿色块、蓝色块判断颜色顺序是否正确。这套验证逻辑比直接去跑Qt界面要高效得多能在五分钟内定位出是驱动问题还是上层问题。4.2 白屏、花屏、方向颠倒高频问题排查表实际调试中我归纳出了下面几个高频问题基本覆盖了SPI LCD驱动80%的异常情况现象可能原因解决思路背光亮、屏幕一直白/黑屏初始化序列没执行复位时序不对DC引脚极性和配置不符确认probe日志有没有打印初始化完成用示波器量RST引脚低电平时间是否足够检查DC引脚gpiod_get和硬件实际连接是否一致花屏、乱码SPI模式不对频率太高供电不稳确认屏的数据手册里时序图是SPI MODE 0还是MODE 3把spi-max-frequency降到10MHz试检查电源纹波和线材红蓝颜色互换MADCTL的BGR位没设置字节序反转改设备树里bgr参数或初始化序列的MADCTL值检查RGB565高低字节是否需要交换屏幕上下左右颠倒MADCTL扫描方向不对修改0x36命令的参数用0x00/0x60/0xC0/0xA0组合试出正确方向显示有残影、刷新不完整fb_defio脏页回收覆盖不全未正确计算刷新矩形在damage回调里打印脏页坐标范围检查fb_ops里fillrect和imageblit是否都注册正确应用层写屏后不刷新忘记初始化fb_defio或delay设置太长确认probe里调用了fb_deferred_io_init把delay从HZ/30调小到HZ/60观察注意表格里的“黑白屏”现象排查顺序永远是先确认背光再确认初始化命令有没有发出再确认DC引脚极性最后才是怀疑SPI时钟和模式。很多人都是一上来就改频率结果浪费两个小时发现是GPIO配置反了。4.3 SPI屏的性能极限与帧率计算最后聊聊SPI屏到底能跑多快。以240x320 RGB565为例一帧数据量是153600字节。SPI时钟20MHz时理论上每字节需要8个时钟周期纯数据发送时间是153600 * 8 / 20000000 ≈ 61.44ms也就是理论帧率约16fps。但实际上还有命令开销、GPIO切换延时、deferred_io的调度延迟真正稳定刷新率在10到12fps左右。如果还想提高刷新率可以从这几个方向优化提高SPI时钟频率。板内走线良好时可以提升到40MHz甚至更高但需要反复做长时间压力测试防止偶发数据错位。启用DMA传输。RK3568的SPI控制器支持DMA一次传输大块数据时能显著降低CPU占用。配置时使用spi_sync配合SPI_MASTER_MUST_TX标志实测能减少约30%的传输等待时间。减小刷新区域。利用fb_defio按脏页刷新避免整屏拷贝。对显示数字、图标这种内容实际刷新数据量会远小于一帧153600字节。使用双缓冲方案。分配两个fb缓存一个给应用层写入一个给驱动后台发送发送完成后再交换避免应用写屏和SPI发送之间争抢同一块缓存导致撕裂。这个优化在应用层频繁动态刷新时尤其有效。性能优化的优先级我建议是先开DMA再调高SPI频率最后再上双缓冲。每一步改动都验证一次显示稳定性和CPU占用率不要一次全改不然出了问题都不知道是哪一步引起的。还有一个经验是SPI屏调好后应用层写屏不要高频全屏重绘尽量采用局部更新方式。比如显示时间时只更新数字所在的区域而不是清掉整个屏幕再重画。配合fb_defio的局部刷新机制这样即使SPI屏本身带宽有限联动操作的手感也不会太差。写在最后的一点体会如果把这次SPI LCD驱动调试过程压缩成一句话那就是“设备树决定能不能注册成功初始化序列决定能不能点亮DC时序决定能不能正常显示”。回想起来这个项目真正花时间的不是写驱动代码本身而是一个一个排查GPIO极性问题、SPI频率稳定性问题。RK3568平台资料已经算比较全的了但网上很多文档都是抄来抄去关于DC引脚和设备树dp、pinctrl冲突的深坑很少有人说透。建议新手在做类似方案时先拿一个逻辑分析仪或者示波器把SPI时序抓出来看能看到波形就等于成功了一大半。只要能坚持到把一帧纯色数据发进GRAM后面所有调试都会突然顺畅起来。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →