ARTICLE DETAIL

资讯详情

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

RK3568 SPI LCD驱动实战:FrameBuffer替代DRM的完整方案

RK3568 SPI LCD驱动实战:FrameBuffer替代DRM的完整方案 在RK3568平台上用SPI接口驱动一块LCD屏很多人第一反应是直接套DRM的panel驱动框架然后被各种KMS概念绕得头晕。实际上如果屏本身分辨率不高、对刷新率也没那么苛刻FrameBuffer模式反而是最简单直接、也最容易跑通的一条路。这篇文章我以RK3568 一块2.0寸320x240的ST7789 SPI屏为例把从硬件接线、设备树配置、驱动核心实现到性能优化和排障的完整思路拆开讲清楚。我最早接到这个需求时客户要的是一块小尺寸SPI LCD用于显示设备状态和简单参数不需要跑视频也没有复杂的合成需求。当时评估了几种方案最终选了FrameBuffer SPI的方式主要原因是这套组合在低分辨率、低刷新率场景下足够成熟Linux核心对fbdev的支持非常稳定驱动代码量小排查问题也更直观。下面整个过程就是我实际调试下来的完整记录希望能帮准备上这块方案的人少走几步弯路。1. SPI LCD方案与FrameBuffer架构的适配逻辑1.1 为什么选择FrameBuffer而不是DRMRK3568的显示链路默认走的是DRM框架MIPI DSI、RGB、eDP这些主流接口都有现成驱动。但SPI接口的小屏比较特殊它不依赖RK3568自带的显示控制器而是完全靠CPU侧通过SPI总线把像素数据写进LCD模组自带的GRAM里。屏本身自带显存只要CPU能把数据送进去剩下的刷新工作都由屏控制芯片完成。这种屏如果硬塞进DRM框架就得走panel-simple这类驱动但SPI屏的数据通道并不是标准panel接口驱动需要自己实现一整条数据通路。就算实现了DRM带来的plane、crtc、connector、encoder这套抽象对SPI小屏来说几乎是纯开销。FrameBuffer架构在这个场景里就是降维打击内核里直接注册一个fb_info用户态通过/dev/fb0打开、mmap、写数据驱动只要把数据经SPI搬到GRAM里就完事。选择FrameBuffer的另一个原因是生态成熟。老的BSP、开发板资料、甚至网上的开源项目大量SPI LCD方案都是基于fbdev写的踩坑资料多遇到问题也好搜。DRM虽然更现代但在低端小屏场景下它的优势完全发挥不出来反而把问题复杂化。1.2 SPI LCD在RK3568上的资源盘点RK3568本身有多个SPI控制器带FIFO也支持DMA这给SPI LCD留了不错的性能基础。接线方面标准的4线SPI只需要SCLK、MOSI、CSMISO在很多LCD模组上可以不接因为LCD是单方向写数据的设备。除了SPI三根线还必须引出D/C引脚区分命令还是数据和RESET引脚另外背光控制一般用PWM引脚这样方便调节亮度。RK3568的SPI控制器在设备树里有多个iomux组可以选择比如SPI2可以mux到不同引脚组这就是为了嵌入式系统里引脚资源紧张准备的。我在项目中把SPI2m1用到了LCD上CS、CLK、MOSI、MISO各占一个引脚再加一个GPIO管DC、一个GPIO管复位、一路PWM管背光总共占不到10个引脚对RK3568这种接口丰富的芯片来说非常宽裕。2. 硬件设计与接线注意事项2.1 最小系统接线方案这里以ST7789V3控制芯片的2.0寸屏为例它的驱动IC可以支持到320x240分辨率RGB565格式内置GRAM 240x320x16bit用SPI接口时最高可以跑几十MHz时钟。实际项目中不会真跑那么快因为GPIO控制的D/C引脚和SPI之间需要时间配合时钟太快反而容易采错。接线建议如下信号屏端引脚RK3568侧说明SCLKSCK/SCLSPI2_CLKSPI时钟MOSISDA/SDISPI2_MOSI主出从入CSCSSPI2_CS0或GPIO片选建议硬件CSD/CDC/RSGPIO3_C0数据/命令选择RESETRESETGPIO3_C1复位引脚低电平有效BLLEDAPWM4PWM背光频率建议1kHz以上VCCVDD3.3V逻辑电源多数SPI屏是3.3VGNDGNDGND共地有个细节很容易忽略SPI LCD的供电能力问题。小屏模组一般逻辑电压3.3V、背光LED串联后再加限流背光全亮时电流可能到40mA以上。如果直接从RK3568的GPIO或某个弱供电轨取电容易出现背光闪烁或者屏幕亮度不稳定的现象。我一般习惯把屏的VCC和背光分开供VCC直接接到3.3V电源轨背光用PWM控制MOS管驱动不要用GPIO直驱背光。2.2 D/C引脚与SPI时序配合SPI LCD和普通SPI设备最大的区别就是多了一个D/C引脚它决定了当前SPI总线上传输的字节是命令还是数据。有些屏控制芯片支持9-bit SPI模式把第9个bit当作命令/数据标志但大多数情况下都用最通用的方案D/C引脚拉低时发命令拉高时发数据。时序上D/C信号必须在SPI传输的字节期间保持稳定不能用软件反复翻转。实际上Linux SPI框架发送数据时整个spi_message是一次性传输出去的所以驱动里需要在发起传输前把D/C引脚设好传输过程中保持不动。这点用GPIO操作很方便调用spi_sync_transfer之前用gpiod_set_value设置DC引脚电平然后开始传输。这里我踩过一个大坑如果SPI频率比较高超过了20MHzGPIO翻转D/C引脚的速度可能跟不上导致帧首的字节被当成错误类型处理表现出来就是屏幕显示错乱。所以要么降低SPI频率要么利用SPI控制器支持的spi_transfer的bits_per_word 9来实现硬件级D/C切换但后者对驱动兼容性要求更高不如直接控制GPIO来得可靠。2.3 背光控制与供电策略背光这一路最简单也最容易出问题。用PWM控制背光亮度时PWM频率最好不要低于1kHz否则耳朵灵敏的人能听到背光驱动电感发出的啸叫。RK3568的PWM控制器在设备树里配置很方便周期和占空比通过pwm-backlight框架管理用户态通过sysfs接口就可以调亮度。供电方面SPI屏的driver IC内部会产生多路电压如VCOM、VGH、VGL这些电压都是内部DCDC或电荷泵产生的对外部电源的纹波比较敏感。如果板子上3.3V纹波太大屏幕容易出现水波纹或者刷新时亮度抖动。实际调试中我遇到过类似问题最后在屏的VCC输入处加了一颗10uF陶瓷电容和1uF电容并联问题就消失了。给正在做硬件的朋友一个建议LCD的电源输入端预留电容位置永远不要省。3. 软件框架搭建设备树、驱动核心与FrameBuffer注册3.1 设备树配置详解设备树的配置决定了内核能不能正确识别SPI设备和背光设备。以SPI2为例先要确认RK3568的pinctrl中SPI2的引脚mux设置。我这边用到的是SPI2的m1组也就是spi2m1_cs0、spi2m1_clk、spi2m1_miso、spi2m1_mosi这组引脚。设备树的完整节点如下/ { backlight: backlight { compatible pwm-backlight; pwms pwm4 0 1000000 0; brightness-levels 0 4 8 16 32 64 128 255; default-brightness-level 6; status okay; }; }; spi2 { status okay; pinctrl-names default; pinctrl-0 spi2m1_cs0 spi2m1_clk spi2m1_miso spi2m1_mosi; st7789: st77890 { compatible mycomp,st7789-fb; reg 0; spi-max-frequency 16000000; spi-cpol 0; spi-cpha 0; reset-gpios gpio3 RK_PC1 GPIO_ACTIVE_LOW; dc-gpios gpio3 RK_PC0 GPIO_ACTIVE_HIGH; backlight backlight; width 320; height 240; rotate 0; bpp 16; status okay; }; };有几个点要重点解释spi-max-frequency设多少合适我最后稳定在16MHz这个值在绝大多数线长和GPIO DC方案下都能稳定工作。如果你用的是硬件D/C引脚切换方式可以再往上拉但16MHz对320x240的屏来说已经够用了。reg 0表示挂在CS0上如果你的SPI控制器硬件CS不够用可以把CS也改成GPIO软片选通过cs-gpios属性指定后面会提到。spi-cpol和spi-cpha决定SPI模式。ST7789和大部分SPI LCD都支持模式0即空闲时时钟低电平、首个边沿采样。如果模式配错屏幕上会出现极规律的雪花噪点而且完全无法通过软件修正。3.2 SPI软件片选与硬件片选的选择RK3568的SPI控制器自带硬件CS输出设备树里用reg指定的索引会映射到对应CS引脚。但实际项目中有时候SPI控制器某组引脚的CS被其他外设复用或者想灵活控制片选时序就可以改用GPIO软片选。Linux SPI框架支持通过cs-gpios属性定义一组GPIO来控制CSspi2 { status okay; cs-gpios gpio3 RK_PB6 GPIO_ACTIVE_LOW; pinctrl-names default; pinctrl-0 spi2m1_clk spi2m1_miso spi2m1_mosi; };需要注意的是当cs-gpios存在时reg就不再直接映射到硬件CS了而是映射到cs-gpios数组的索引。比如reg 0就代表使用cs-gpios里的第0个GPIO作为片选。软件片选的好处是CS拉低和拉高的时机完全由SPI框架控制配合SPI传输消息时可以精确做到先拉低再传输再拉高。缺点则是当SPI频率较高时GPIO翻转速度可能不如硬件CS平滑。对于LCD这种多字节连续传输的场景20MHz以下频率用GPIO片选没什么问题如果追求更高频率还是用硬件CS更稳。3.3 驱动核心结构与fb_info注册流程驱动这边的核心工作就是创建fb_info结构体实现必要的fb_ops然后注册到内核。整个过程可以简化为几个步骤在probe函数里解析设备树获取DC、复位等GPIO初始化SPI设备。分配fb_info结构体framebuffer_alloc。填充fb_fix_screeninfo和fb_var_screeninfo包括分辨率、颜色位深、行字节数、显存地址等。实现fb_ops中的fb_set_par、fb_blank、fb_fillrect、fb_copyarea、fb_imageblit、fb_mmap等回调。分配显存推荐用dma_alloc_coherent分配这样可以保证SPI DMA读取显存时缓存是一致的。调用register_framebuffer注册设备。这里把fb_var_screeninfo的初始化的一个例子贴出来static int lcd_fb_probe(struct spi_device *spi) { struct fb_info *info; struct lcd_fbdev *fb; fb kzalloc(sizeof(*fb), GFP_KERNEL); info framebuffer_alloc(0, spi-dev); ... fb-info info; fb-spi spi; strcpy(info-fix.id, spi_lcd); info-fix.type FB_TYPE_PACKED_PIXELS; info-fix.visual FB_VISUAL_TRUECOLOR; info-fix.line_length 320 * 2; info-fix.smem_len 320 * 240 * 2; info-var.xres 320; info-var.yres 240; info-var.xres_virtual 320; info-var.yres_virtual 240; 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; ... }RGB565的位域定义这里绝对不能写错。如果RGB的offset写乱了屏幕上就会出现颜色莫名其妙的偏色比如红色和蓝色对调、绿色忽亮忽暗。这类问题还不报错最难排查。显存分配用dma_alloc_coherent把返回的虚拟地址赋给info-screen_base物理地址赋给info-fix.smem_start。用户态mmap时我们在fb_mmap回调里把这块DMA缓冲区映射给用户这样就保证了用户写显存、SPI DMA读到的是同一份数据不会因为CPU cache没回写而出现花屏。3.4 帧缓冲刷新与SPI数据搬运实现FrameBuffer模式下LCD屏幕不会自己从内存拉数据它只接收SPI送来的像素。所以驱动里要有一个“把fb显存内容刷到屏幕GRAM”的机制。最简单的实现是后台线程定时全屏刷新或者在有写操作触发的回调里局部刷新。考虑到SPI屏主要显示静态页面我用了定时刷新加脏标记的方式用户往显存里写了数据置脏标记刷新线程检测到标记后把整帧数据通过SPI发送给屏幕。核心发送函数大概是这样的static void lcd_fb_update_display(struct lcd_fbdev *fb) { struct fb_info *info fb-info; u8 *buf (u8 *)info-screen_base; int xres info-var.xres; int yres info-var.yres; struct spi_transfer xfer; struct spi_message msg; lcd_set_dc(fb, 0); lcd_spi_write_cmd(fb, 0x2A); // column address set lcd_set_dc(fb, 1); lcd_spi_write_data(fb, 0x00); lcd_spi_write_data(fb, 0x00); lcd_spi_write_data(fb, 0); lcd_spi_write_data(fb, 239); lcd_set_dc(fb, 0); lcd_spi_write_cmd(fb, 0x2B); // row address set lcd_set_dc(fb, 1); lcd_spi_write_data(fb, 0x00); lcd_spi_write_data(fb, 0x00); lcd_spi_write_data(fb, 0); lcd_spi_write_data(fb, 319); lcd_set_dc(fb, 0); lcd_spi_write_cmd(fb, 0x2C); // memory write lcd_set_dc(fb, 1); memset(xfer, 0, sizeof(xfer)); xfer.tx_buf buf; xfer.len xres * yres * 2; spi_message_init(msg); spi_message_add_tail(xfer, msg); spi_sync(fb-spi, msg); }画窗口地址时注意ST7789的列地址和行地址对应的是GRAM坐标而有些屏模组出厂时物理方向和FB坐标系不一致就需要通过设备树里的rotate和驱动里的GRAM offset来调整。这块在后面的问题排查里还会细说。4. 编译配置与启动验证4.1 内核菜单配置与驱动编译把驱动源码放到内核树的drivers/video/fbdev/目录下然后在Kconfig里加一个配置项config FB_SPI_ST7789 tristate SPI LCD ST7789 framebuffer support depends on FB SPI help Say Y here to enable SPI LCD framebuffer support.执行make menuconfig按以下路径打开配置Device Drivers --- Graphics support --- * Support for frame buffer devices --- * SPI LCD ST7789 framebuffer support同时确认SPI框架已经打开Device Drivers --- * SPI support --- * Rockchip SPI controller如果你的BSP默认把DRM显示开着且不打算用DRM输出任何内容那可以保留DRM但要注意fb设备节点的分配。fbdev子系统按注册顺序分配/dev/fb0、/dev/fb1。如果DRM那边注册了一个fbdev模拟设备SPI LCD的fb设备可能变成/dev/fb1这对应用层使用影响不大只要用户程序指定正确的设备节点就行。如果不想编进内核也可以用module方式编译加载后观察dmesg有没有正确probe。4.2 烧录启动后的三分钟验证流程系统起来后可以先快速验证驱动是否工作正常整个流程就是经典的Linux frame buffer调试三步走。第一步查看fb设备是否存在读取基本信息cat /proc/fb fbset -fb /dev/fb0如果fbset能正确显示320x240, 16bpp说明fb_info注册成功。第二步往fb设备里写随机数据看屏幕有没有动态雪花dd if/dev/urandom of/dev/fb0 bs1024 count800如果屏幕出现彩色噪点说明SPI通路和GRAM刷新都正常。如果屏幕没有任何变化优先查SPI通信和初始化序列。第三步把一个已知图片的raw数据写到fb查看显示效果./fb_write_img /dev/fb0 logo.rawfb_write_img可以用简单的C程序实现打开/dev/fb0mmap后直接把raw像素数据拷贝进去。如果图片方向不对或者颜色不对再回设备树调整rotate和驱动里的RGB顺序。另外还有一个非常实用的功能内核FrameBuffer console。如果想让启动日志直接显示在SPI小屏上内核配置里使能CONFIG_FRAMEBUFFER_CONSOLE然后在内核启动参数里加上fbconmap:0这样系统日志就能直接出现在LCD上调试时相当直观。不过有一点要提前说SPI屏的刷新率本来就不高滚动日志时会明显感到刷新慢这是正常现象毕竟带宽上限摆在那里。5. 性能瓶颈分析SPI带宽与刷新率优化5.1 SPI屏刷新率的数学账很多人对SPI LCD的刷新率没概念我直接算一笔账。以320x240 RGB565为例每像素2字节一帧数据量是320 x 240 x 2 153600 字节每字节在SPI总线上占8个bitSPI时钟16MHz时每bit耗时62.5ns一字节耗时0.5us一帧纯数据耗时 153600 x 0.5us 76.8ms再加上命令、地址设置、D/C翻转、传输间隙实际一帧刷新耗时可能在90ms左右。也就是说16MHz SPI下全屏刷新理论帧率只有11fps左右。如果换成240x240的1.3寸屏一帧数据量只有240x240x2115200字节纯传数时间57.6ms加上开销后大概能到15fps。这个帧率对显示状态、菜单、仪表盘这类静态内容完全够用但对视频播放就彻底没戏这点在方案选型时就要想清楚。5.2 提升刷新效率的几种实用手段既然带宽有限那就想尽办法减少无谓的数据搬运。第一能局部刷新就不要整屏刷新。实现一个脏矩形追踪机制用户写显存后驱动记录脏区域刷新线程只把脏区域的像素发送到GRAM。对于大部分应用场景只是某个数值变化或者图标闪烁局部刷新能把有效刷新率提升好几倍。第二使用struct spi_transfer批量传输避免逐字节调用spi_write。我在驱动里全屏一帧数据只发起一次spi_sync这样既减少了GPIO翻转和内核调用的开销也让SPI控制器有机会走DMA通道。第三调整SPI控制器DMA支持。RK3568的SPI控制器支持DMA传输当一个spi_transfer长度较大时框架会自动启用DMA。但从实际经验看小长度传输走DMA反而效益不高只有几KB以上的大块数据才值得用DMA搬运。全屏刷新这种场景天然适合DMA。第四降低LCD控制芯片的内部等待时间。ST7789在收到大量GRAM写入命令后内部处理速度跟不上时会自动拉低busy信号但SPI协议本身没有busy反馈只能延时等方式处理。可以尝试在初始化时把FRMCTR1、PORCTRL等寄存器调优减少内部blanking时间不过这需要对比屏厂规格书不建议盲目改。优化后的实测效果16MHz SPI下320x240全屏刷新从最初的11fps提升到约12fps提升不算大但如果改成局部刷新状态栏区域100x30刷新一次只需要5ms人眼看到的效果就是即时响应体感提升非常明显。6. 常见问题排查与调试心得6.1 白屏、黑屏与无背光这是SPI LCD最常见的三大症状但根因各不相同。如果是白屏且背光亮说明屏有供电、背光正常但GRAM没有任何数据显示。优先检查复位时序RESET引脚是否按规格书要求拉低再拉高复位后是否等了120ms以上再发初始化命令。ST7789这类控制芯片对复位时间很敏感复位脉宽不够会导致初始化序列全部无效。如果是黑屏且背光不亮先查背光供电和PWM控制。用echo 1 /sys/class/backlight/backlight/brightness测试一下如果没反应再看看设备树backlight节点和PWM引脚绑定的gpio是否冲突。如果屏幕完全不显示但背光亮且SPI时钟和数据都能在逻辑分析仪上看到波形那就要怀疑初始化序列是否正确。很多山寨屏的初始化代码是从别的驱动里抄过来的寄存器参数不适配表现就是上电后屏幕不亮或者颜色异常。拿到屏先跟供应商确认控制芯片型号再对照驱动IC规格书核对初始化命令。6.2 花屏、错位与随机噪点花屏的原因比白屏多得多我按照排查优先级列一个速查表现象可能原因排查方向规律性条纹或错位SPI模式不对CPOL/CPHA设备树里调整spi-cpol/spi-cpha画面随机雪花SPI时钟过高降低spi-max-frequency到8MHz验证左右或上下颠倒GRAM方向与物理方向不一致调整MADCTL寄存器或fb rotate参数显示偏移了半个屏幕窗口地址设置错误核对0x2A/0x2B发送的坐标范围色彩混乱但形状正常RGB位域配置错误核对red/green/blue偏移只在快速刷新时花屏DMA数据与cache不一致改用dma_alloc_coherent分配显存花屏问题里最容易误导人的是SPI模式。设备树里spi-cpol和spi-cpha看似只影响电气时序但对控制芯片来说采样沿错误会导致数据错位。STM32、单片机经验多的人到这里容易带偏节奏因为单片机SPI初始化时设置的模式和Linux设备树里不是同一套命名逻辑建议直接用逻辑分析仪抓波形然后对照控制芯片数据手册里的时序图去核对。6.3 屏幕闪烁与刷新残留刷新残留常见于整屏刷新过程中GRAM被分多次写入用户看到上半帧和下半帧内容不一致。解决办法是把整帧缓冲准备好后一次性连续写入GRAM中间不要穿插耗时操作。闪烁则有三种情况。第一种是背光PWM频率过低这种情况下看到的是整屏亮度周期性跳变把PWM频率调到1kHz以上就能解决。第二种是刷新线程的定时器间隔不稳定导致画面时快时慢尽量把刷新线程优先级提高并且使用hrtimer或者内核deferred_work代替基础定时器。第三种是LCD电压配置问题屏幕内部VCOM电压偏移导致轻微闪烁这个只能通过调寄存器参数解决一般可以尝试改PVGAMCTRL、NVGAMCTRL这些gamma寄存器或VCOM setting寄存器。6.4 初始化序列等待时间的血泪教训最后说一个容易复发的问题初始化序列里的延时砍不得。很多驱动为了加快启动速度会把ST7789初始化序列里的msleep改短甚至去掉结果屏幕能亮但显示一段时间后会出现颜色漂移、残影、甚至偶发黑屏。原因很简单控制芯片内部的DCDC和电荷泵需要足够时间稳定输出各路电压比如VGH、VGL这些用于TFT开关的电压如果没稳定就开启显示TFT特性就会异常。我在实际项目中初始化序列分了三段复位后等120msSLPOUT后等120ms最后一个命令后至少等20ms再开启显示。这些延时是控制芯片的正常工作条件不是可以随便优化的死代码。另一个相关经验是初始化命令和数据之间必须严格区分D/C状态。如果DC引脚电平没切换到位比如本该发数据时发成了命令GRAM写入可能变成未知状态表现就是某一整块区域的颜色不对。建议在SPI发送函数入口加一个调试开关通过dev_dbg打印当前DC状态和发送的命令值初期调试时全打开稳定后再关掉。如果你也正好在RK3568上接SPI小屏这套流程可以直接照着走。整个方案里最难的不是驱动本身而是Timing和初始化序列遇到问题多抓波形、多对照控制芯片手册比盲目改代码排查得快得多。
返回列表