新闻详情

新闻详情

首页 / 资讯中心 / 详情

RK3568平台FrameBuffer模式驱动SPI LCD:从设备树到刷屏优化实战

发布时间:2026/9/16 16:58:26来源:尧图网络
RK3568平台FrameBuffer模式驱动SPI LCD:从设备树到刷屏优化实战
手里这块RK3568板卡的显示接口已经被HDMI和MIPI DSI占满了外设接口只剩下SPI、I2C和一堆GPIO但又必须挂一块小尺寸LCD做状态显示。翻遍方案最终决定走FrameBuffer模式直接用SPI驱动一颗240x320的ST7789屏幕。这个选择当时被团队里做上层应用的同事质疑过因为在RK3568这种级别的处理器上大家默认都会走DRM/KMS、上VOP2很少有人愿意回到fbdev的老路上。但实际做完之后我对FrameBuffer模式在低速小屏场景下的价值有了完全不一样的认识。这篇文章就围绕RK3568平台、FrameBuffer框架、SPI总线和LCD驱动开发这四个关键词展开。我会把我从设备树写法、fb_ops实现、初始化序列下发到带宽计算、白屏花屏调试的完整过程梳理出来其中既有能直接抄走的配置和代码也有我踩过之后觉得必须写下来的坑。1. 为什么选FrameBuffer而不是DRM小屏方案的第一个岔路口1.1 RK3568显示链路与SPI小屏的错位RK3568的显示链路设计非常典型VOP2负责图形合成然后通过eDP、HDMI、MIPI DSI、RGB并口这些高速接口把画面送给大屏。VOP2本身是一套非常成熟的DRM/KMS实现对主流显示接口的支持很完善但唯独对SPI这种低速串行接口没有多少兴趣。这不是瑞芯微的问题整个Linux社区都没有把SPI LCD纳入DRM作为一等公民SPI屏幕更多被视为一个“慢速外设”而不是“显示设备”。这个定位差异直接决定了技术路线的选择。如果我强行在DRM框架下挂这颗SPI屏我需要自己写一个drm_panel驱动实现panel_prepare、panel_enable、get_modes、prepare、enable等一整套回调还要处理drm_connector的注册最后画面数据流还是要落到SPI上。这就相当于给一个自行车装上航空发动机的仪表盘流程复杂、效率不高而且DRM框架里所有关于vblank、crtc、encoder的抽象对SPI小屏来说都没有实际意义。反观FrameBuffer模式也就是Linux传统的fbdev框架它本身就是一个非常朴素的“显存映射 刷新回调”模型。驱动只需要给上层一个连续的内存区域上层往这片内存写像素驱动再把变化同步到屏幕。对于SPI LCD这种整屏刷新或者局部刷新都不复杂的设备fbdev这种简单的抽象反而是最贴合的。1.2 fbdev在RK3568 BSP中的实际处境很多做RK平台开发的人有一个误解觉得RK3568的BSP内核默认只开DRM不带fbdev。实际上瑞芯微的SDK内核里DRM和FBDEV是可以共存的前者通过CONFIG_DRM配置后者通过CONFIG_FB配置。在公众号、博客上看到的各种“如何把/dev/fb0映射出来”的教程在RK3568上不是不可用而是需要确认内核里这几个配置项处于开启状态CONFIG_FBy CONFIG_FB_CMDLINEy CONFIG_FB_NOTIFYy CONFIG_FB_CFB_FILLRECTy CONFIG_FB_CFB_COPYAREAy CONFIG_FB_CFB_IMAGEBLITy我用的这块板子内核版本是5.10SDK默认配置里CONFIG_FB_CFB_*这几个软件加速回调默认是打开的但CONFIG_FB本身在某些config文件里可能没被显式打开需要通过menuconfig确认。在dts层面还要注意如果内核里DRM的VOP2驱动把某个显示接口注册成了主显示设备它可能会占用掉/dev/fb0这个设备号。如果我发现自己的SPI屏驱动注册不到fb0不要慌可以在驱动里使用这个函数手动指定一个更高的设备号info-node -1; /* 让框架自动分配 */或者在加载驱动时使用内核参数fbdev1来指定设备编号虽然我没在实际项目中依赖这个参数但排错时会用来区分设备节点。1.3 选型结论什么情况下走FrameBuffer更划算做了这轮调研之后我给这类需求定了一个简单的判断标准屏幕分辨率低于480x320刷新率要求不超过30fps显示内容以文本、简单图形、状态图标为主直接走FrameBuffer。原因有三个。第一SPI总线的物理带宽就摆在那里就算上到60MHz时钟理论最高也就7.5MB/s扣掉协议开销和命令字头240x320 RGB565全屏刷新一次要20毫秒级别的时间这决定了它撑不起高分辨率高刷场景。第二fbdev驱动模型简单代码量远小于一个DRM panel驱动出错时排查链路短。第三上层应用哪怕只写普通文件操作通过open(/dev/fb0)、mmap()、write()就能把中文点阵、波形曲线画上去省掉了和DRM property、plane、crtc打交道的复杂度。至于有人说fbdev是过时技术这个观点在普通应用场景下成立但在“用SPI挂小屏”这个特定夹缝里它就是最省事、最稳定的选择。2. SPI LCD驱动的地基从显存模型到刷新链路2.1 屏幕控制器的显存模型ST7789为例我们常用的SPI小屏像ST7789、ILI9341、ILI9488它们的内部都带一块GRAM也就是显存。ST7789的分辨率最高是240x320内部GRAM的大小就是240x320x18bit这是RGB666格式的总位宽。控制器支持RGB565、RGB666等不同输入格式但显存本身固定是每像素18bit物理存放。驱动要做的事情就是把上层写过来的RGB565像素先转换或直接打包成控制器要求的格式再通过SPI发进GRAM。这里有个容易搞混的地方SPI LCD驱动和GPU驱动、VOP驱动的本质区别在于前者没有DMA2D、没有硬件光标、没有图层混合GRAM的每一个字节都是CPU或者DMA通过SPI总线手动推进去的。所以“显示驱动”在这里实际上退化成了“像素搬运工”。ST7789的显存更新流程是标准的通过命令0x2A设置列地址范围。通过命令0x2B设置行地址范围。通过命令0x2C开启内存写之后所有SPI数据都会被写入GRAM地址自动递增。写入完成后控制器根据扫描方向把GRAM内容刷新到液晶面板。这给了我们一个很关键的启发驱动里做局部刷新很简单只需要把列地址和行地址设置到变化区域然后只发送那一块区域的像素数据。这个特性会在后面带宽优化部分派上大用场。2.2 framebuffer_alloc到fb_ops驱动骨架怎么搭一个标准的SPI LCD fbdev驱动核心结构可以简化为三个部分fb_info的分配与初始化、SPI传输逻辑、以及与屏幕控制器的命令交互。分配fb_info的经典代码路径是static int spi_lcd_probe(struct spi_device *spi) { struct fb_info *info; struct spi_lcd_priv *priv; int ret; info framebuffer_alloc(sizeof(struct spi_lcd_priv), spi-dev); if (!info) return -ENOMEM; priv info-par; priv-spi spi; dev_set_drvdata(spi-dev, info); /* 申请显存注意不要用普通的kmalloc显存一般用vzalloc或dma_alloc_coherent */ priv-vmem_size LCD_WIDTH * LCD_HEIGHT * sizeof(u16); priv-vmem vzalloc(priv-vmem_size); if (!priv-vmem) { ret -ENOMEM; goto err_free_info; } /* 设置屏幕固定参数和可变参数 */ strcpy(info-fix.id, spi_lcd); info-fix.type FB_TYPE_PACKED_PIXELS; info-fix.visual FB_VISUAL_TRUECOLOR; info-fix.line_length LCD_WIDTH * sizeof(u16); info-fix.smem_len priv-vmem_size; info-fix.smem_start virt_to_phys(priv-vmem); info-screen_base priv-vmem; info-screen_size priv-vmem_size; info-var.xres LCD_WIDTH; info-var.yres LCD_HEIGHT; info-var.xres_virtual LCD_WIDTH; info-var.yres_virtual LCD_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-var.activate FB_ACTIVATE_NOW; info-fbops spi_lcd_fb_ops; info-flags FBINFO_FLAG_DEFAULT; ret register_framebuffer(info); if (ret) goto err_free_vmem; spi_lcd_init_panel(spi); /* 发送初始化序列 */ spi_lcd_refresh(info); /* 把脏显存刷新到屏 */ return 0; }注意fix.smem_start在支持DMA的平台上如果后续打算用dma_alloc_coherent申请显存这里就要用其返回的DMA地址并且传输时也要用对应的DMA地址这直接影响到能否走SPI控制器的DMA通道。如果不要求高性能用vzalloc配普通PIO模式CPU逐字节喂数据寄存器也是可行的帧率会低一些。2.3 刷新一帧数据到底经过哪几条路很多人一开始不理解fbdev下的“刷新”到底指什么。在没有硬件光标、没有自动刷新引擎的情况下fbdev驱动只是把用户写入screen_base的那段内存视为“当前帧”。屏幕不会自动去读它必须由驱动主动把数据搬到SPI控制器。我的实现里用了两条刷新路径主动刷新驱动注册一个高精度定时器周期比如50ms到100ms每次tick检查fb_info里由fb_deferred_io维护的脏页或者简单粗暴地全屏重发。被动刷新上层调用write()、ioctl(FBIOPAN_DISPLAY)或fb_blank()时触发相应回调。实际上在fbdev框架里fb_deferred_io是个非常好用的机制它的原理是注册一个struct fb_deferred_io对象利用页面故障机制来跟踪哪些页面被写过然后延迟到后台工作队列里做脏区域刷新。对小分辨率SPI屏特别适合既能做局部刷新又不用自己维护脏矩形算法。static struct fb_deferred_io spi_lcd_defio { .delay HZ / 20, /* 50ms 合并脏页 */ .deferred_io spi_lcd_dirty_update, }; info-fbdefio spi_lcd_defio; info-fbops-fb_mmap fb_deferred_io_mmap;spi_lcd_dirty_update回调里会得到一个fb_deferred_io对象的pagelist把页范围换算成屏幕坐标区域然后只对这个区域发起SPI传输。这块内容会在后面第5章结合帧率一起分析代码怎么组织也很关键。3. 设备树配置时序、片选与GPIO的排列组合3.1 SPI节点、时序参数与硬件/软件片选的权衡RK3568有多个SPI控制器我在板子上用的是SPI3。设备树节点首先要确保SPI控制器本身是使能的并且引脚复用正确spi3 { status okay; pinctrl-names default; pinctrl-0 spi3_pins; assigned-clocks cru SCLK_SPI3; assigned-clock-rates 60000000; spilcd: spi-lcd0 { compatible example,spi-lcd; reg 0; spi-max-frequency 60000000; spi-cpol 0; spi-cpha 0; rotate 0; bgr 0; fps 30; reset-gpios gpio3 RK_PA6 GPIO_ACTIVE_LOW; dc-gpios gpio3 RK_PA5 GPIO_ACTIVE_HIGH; backlight-gpios gpio3 RK_PA4 GPIO_ACTIVE_HIGH; }; };这里有几个关键点。第一spi-max-frequency不能只看屏幕控制器手册标称的“最大支持XX MHz”还要把PCB走线长度、排线质量、转接板接触电阻都算进去。我经历过把频率提到80MHz后图像出现横纹噪声降回60MHz就完全正常的事情所以如果PCB设计不那么讲究留20%~30%余量比较稳妥。第二spi-cpol和spi-cpha决定了SPI传输模式。绝大多数SPI接口的LCD控制器ST7789、ILI9341都支持在Mode 0和Mode 3下工作也就是CPOL0、CPHA0或CPOL1、CPHA1。在屏幕数据手册里通常叫“SPI Mode 0”或“SPI Mode 3”。上电初始化前一定要确认这两个引脚的上拉状态否则可能出现时钟极性和屏幕内部锁存边沿对不上的问题。第三硬件片选和软件片选的问题值得多说两句。硬件片选由SPI控制器在每次传输开始和结束时自动拉低拉高对驱动来说最省事但坑在于SPI控制器可能在同一个message的不同transfer之间插入了片选释放再拉起的动作。对绝大多数LCD控制器来说每一次片选下降沿都可能被识别成一次新的命令/数据序列开始如果屏幕对CS的连续保持有苛刻要求尤其是某些带状态机锁存的控制IC连续的少量数据传输会被切碎。软件片选就是用一个普通GPIO手动控制CS引脚由驱动在spi_message发起前手动拉低、结束后再拉高这样能精确控制CS保持时间。RK3568的SPI控制器用硬件片选完全没问题前提是驱动把所有要发送的数据组织成一个完整的spi_message而不是拆成多次spi_sync。这个细节在fbtft框架里有个use_gpio_cs选项就是这个原因。如果后面遇到屏幕上电后偶发花屏、或者首帧刷新不完整优先检查是不是CS被反复拉高拉低导致的状态机复位。3.2 DC、RESET、背光三个GPIO的归属SPI LCD除了SCLK、MOSI、MISO、CS四根线一般还需要三个控制引脚DC数据/命令选择、RESET复位、BL背光。这三个引脚如果设计成固定的IO口那没问题但做驱动时最好都放到设备树里方便不同版本硬件兼容。DC引脚的用法很死板发送命令字节之前拉低发送数据字节之前拉高。在SPI协议里没有专用的DC信号线所以它只能是一个GPIO。我在驱动里封装了两个发送函数static void spi_lcd_write_cmd(struct spi_lcd_priv *priv, u8 cmd) { gpiod_set_value(priv-dc, 0); /* DC0命令 */ spi_write(priv-spi, cmd, 1); gpiod_set_value(priv-dc, 1); /* DC1恢复数据模式 */ } static void spi_lcd_write_data(struct spi_lcd_priv *priv, const u8 *buf, size_t len) { gpiod_set_value(priv-dc, 1); /* DC1数据 */ spi_write(priv-spi, buf, len); }这里有一个值得注意的时序细节DC电平必须在SPI的第一个时钟沿之前建立并且在最后一个时钟沿之后保持一段时间。GPIO操作如果走标准gpiod_set_value往往会有几十纳秒的延迟这个延迟对60MHz SPI周期约16.7ns来说可能不太够因为DC信号需要提前至少几十纳秒稳定下来。所以如果发现偶尔第一个字节被屏幕识别成错误命令可以考虑使用gpiod_set_value_cansleep配合老一点的GPIO控制器实现或者在发送命令字节前先插入一个微小的忙等待。我这里用的GPIO控制器是CPU直连的本身响应够快没有遇到问题但如果你用I2C转GPIO的扩展芯片来控DC就绝对不行了。RESET引脚的上电时序也不能马虎。ST7789数据手册要求RESET拉低至少10us然后拉高拉高后需要等待120ms才能开始发送初始化命令。实测很多国产兼容屏要求更长我直接把等待时间加到200ms避免因为某个批次屏的电源纹波导致初始化失败。背光引脚可以做两件事一个是普通的GPIO拉高拉低另一个是接到PWM控制器实现亮度调节。如果只需要开关设备树里配GPIO就行如果需要无级调光就拿一个PWM节点然后在驱动里通过pwm_backlight框架管理。3.3 设备树合入外部驱动的一种通用套路在RK3568的SDK上除了这颗SPI LCD我还顺带合入过YT6801这种 PCIe转以太网芯片以及一些WIFI模组。这些外设的dts合入方式与SPI LCD是类似的先确认内核里对应驱动是否被编译然后在设备树里新建节点、配好引脚和时钟再匹配compatible字符串。这个流程走一遍之后你会发现设备树本质上就是在干“板级描述”这一件事与具体芯片是什么类型的设备关系不大。做RK3568开发时拿一个已知可用的外设驱动作为模板比从零开始读内核源码要快得多。4. 驱动实现细节初始化序列、刷屏路径与颜色问题4.1 初始化序列发送spi_message的批量传输屏幕控制器上电后不会直接进入可用的显示状态需要发送一串初始化配置。ST7789典型初始化序列包括退出睡眠0x11、设置像素格式0x3A、设置扫描方向0x36、设置伽马曲线0xE0、0xE1等。这些命令对应的数据一般以数组形式内嵌在驱动里。在发送初始化序列时常见做法是组装一个spi_transfer数组把命令阶段和数据阶段分开。由于DC引脚的存在不能用单个transfer同时发命令和数据必须把“命令数据”组织成多段transfer并保证整个message传输过程中CS保持低。这里正是spi_message相对于spi_write的优势spi_write每次都是一个独立messageCS会重新拉低拉高而一个spi_message内的所有transferCS默认保持连续选中除非控制器设置了cs_change 1。static int spi_lcd_send_buf(struct spi_lcd_priv *priv, u8 cmd, const u8 *data, size_t len) { struct spi_transfer xfer[2]; struct spi_message msg; int ret; memset(xfer, 0, sizeof(xfer)); xfer[0].tx_buf cmd; xfer[0].len 1; xfer[0].cs_change 0; xfer[1].tx_buf data; xfer[1].len len; gpiod_set_value(priv-dc, 0); /* 命令阶段 */ spi_message_init(msg); spi_message_add_tail(xfer[0], msg); spi_message_add_tail(xfer[1], msg); ret spi_sync(priv-spi, msg); gpiod_set_value(priv-dc, 1); /* 恢复数据模式 */ return ret; }这个函数的使用前提是整个spi_message在spi_sync执行期间CS一直保持有效。RK3568的SPI控制器在同一个message内部默认是保持CS有效的。如果你发现一个消息内的两个transfer之间CS出现了抖动可以在设备树里给子节点加cs-gpios改软件片选手动控制。4.2 fb_ops实现与刷屏定时器struct fb_ops里必须实现的内容比想象中的少最简单的可用驱动只需要四个字段static struct fb_ops spi_lcd_fb_ops { .owner THIS_MODULE, .fb_setcolreg spi_lcd_setcolreg, .fb_fillrect cfb_fillrect, .fb_copyarea cfb_copyarea, .fb_imageblit cfb_imageblit, .fb_blank spi_lcd_blank, };cfb_fillrect、cfb_copyarea、cfb_imageblit这三个是内核自带的软件绘制函数它们操作的是fb_info-screen_base对应的内存不需要和硬件有交互。这给了我们一个很好的用户态体验上层通过mmap往/dev/fb0里填充颜色底层软件绘制函数负责把矩形、拷贝、位图这些常见图形操作变成显存里的像素更新然后再有刷新线程把显存里的内容同步到屏上。刷屏定时器我建议不要用fb_deferred_io之外另起一个内核线程因为两套机制同时工作会造成重复刷屏。在低分辨率场景下用内核提供的fb_deferred_io是最优解。它通过struct vm_area_struct的页面故障处理把被写过的页记录下来所以你只需要实现一个sched_delayed_work的工作队列函数static void spi_lcd_dirty_update(struct fb_info *info) { struct spi_lcd_priv *priv info-par; unsigned long *pagelist info-fbdefio-pagelist; int num_pages info-fbdefio-pagelist_len; int y_top -1, y_bottom 0; int i; for (i 0; i num_pages; i) { int pg pagelist[i] - info-fix.smem_start; int y pg / (info-fix.line_length); if (y_top -1) y_top y; y_bottom y; } if (y_top 0) spi_lcd_update_rect(priv, 0, y_top, LCD_WIDTH, y_bottom - y_top 1); }注意上面这个算法只计算了页包络的矩形区域是一个“粗粒度的局部刷新”。如果页面尺寸是4K而屏幕一行是480字节240像素RGB565 480B一页就覆盖约8.5行。虽然局部刷新的精度比不上逐像素脏矩形但在240x320这种分辨率下已经足够而且算法简单可靠。如果要做到更细的脏矩形得自己记录每个像素点的修改操作性价比不高尤其当上层应用经常全屏重绘时粗粒度页刷新和全屏刷新的差异几乎可以忽略。4.3 RBG/RGB颠倒和颜色深度调整颜色不对是SPI LCD调试中遇到最多的问题之一。现象通常是红色和蓝色对调或者颜色整体发暗、发绿。我在ST7789上遇到的是典型的RBG颠倒。ST7789的0x36命令是用来设置扫描方向的其中bit3是RGB/BGR顺序控制位。置1表示BGR顺序置0表示RGB顺序。这个位不对RGB565的红色和蓝色就会被交换。很多屏幕模组厂商为了布线方便内部把RGB巴线顺序做了旋转导致同一颗IC在不同模组上表现出来的顺序不一样。所以需要在设备树里给驱动预留一个bgr属性让硬件工程师可以根据实际模组调整。代码里在初始化序列中根据bgr属性设置这个位static void spi_lcd_set_addr_mode(struct spi_lcd_priv *priv) { u8 mode 0x00; /* bit7: MY, bit6: MX, bit5: MV, bit3: BGR */ mode | (priv-rotate 0x01) 6; /* 根据旋转方向设置MX */ mode | (priv-rotate 0x02) 5; /* 根据旋转方向设置MY */ if (priv-bgr) mode | 0x08; spi_lcd_write_cmd(priv, 0x36); spi_lcd_write_data(priv, mode, 1); }此外还要确认上层传给fbdev的颜色格式。我这边强制用RGB565bits_per_pixel16所以fb_var_screeninfo里red、green、blue的偏移和长度必须正确设置红色偏移11、长度5绿色偏移5、长度6蓝色偏移0、长度5。如果这里填错层看到的颜色就会一团糟。5. 带宽预算与帧率实测SPI这条管道能跑多快5.1 60MHz SPI时钟下的理论帧率计算在做任何SPI LCD驱动之前我建议先花两分钟算一下带宽预算这能避免后期对帧率抱有不切实际的期望。以240x320、RGB565为例每帧数据量 240 * 320 * 2 153600 字节在四线标准SPI模式下每字节需要8个时钟周期所以60MHz 时钟下传输速率 60M / 8 7.5 MB/s 理论最短传输时间 153600 / 7.5MB/s 20.48ms 理论最大帧率 1000 / 20.48 48.8fps但要注意这还只是纯像素数据没有算初始化命令、设置窗口命令、以及两次传输之间的CS、DC电平切换时间。再加上fb_deferred_io的合并延迟、内核调度延迟、SPI控制器FIFO操作开销实测能达到35fps已经是相当不错的成绩30fps左右比较常见。如果觉得帧率不够有几种挖潜方式提高SPI时钟频率但要考虑PCB质量、改用Dual SPI或Quad SPI模式ST7789支持2线/4线SPI但需要把数据引脚全接上很多模组没引出这些引脚、开启控制器自带的Tear Effect引脚同步、使用DMA而不是PIO。其中DMA是最直接有效的RK3568的SPI控制器支持DMA传输但要求buffer是DMA可访问地址这也是为什么我在probe里特意提了dma_alloc_coherent。用DMA之后CPU不再参与逐字节搬运同样的60MHz时钟下帧率可以稳定提升10%~20%因为省掉了大量的寄存器读写等待时间。5.2 局部刷新小屏的生存之道全屏刷新的上限就摆在那所以对SPI小屏来说局部刷新不是优化项而是必须项。大部分状态显示场景里实际变化的区域只有一小块比如一个温度数字、一个进度条、一条曲线。用fb_deferred_io配合页级脏跟踪可以非常方便地实现局部刷新。实测一个典型的场景上层每秒刷新一次时钟显示区域假设是200x40像素全屏刷新需要20ms左右而局部刷新只需要计算变化页的包络矩形也就是大概200x48像素的区域传输数据量约为全屏的1/15耗时不到2ms。但这里有两个隐藏的坑。第一个坑是fb_deferred_io的页跟踪粒度是系统页面大小通常4K这对大分辨率屏幕来说意味着“被写一个点整页都要重发”但在240x320屏幕上一页最多覆盖约8.5行总共最多约38个页重发范围其实可以接受。第二个坑是上层如果使用mmap直接操作像素写内存不会触发write系统调用必须依靠deferred_io的页面故障机制才能知道哪些页被写了。如果上层替换为驱动提供的FBIOPAN_DISPLAY之类的方式刷新全屏那页面跟踪就没用了。实际开发中我个人更喜欢在驱动里维护一个“脏矩形”由用户态的write或ioctl调用传递变化区域这样比页级跟踪更精确也不依赖mmap的页错误触发。不过对大多数人的使用方式来说fb_deferred_io现成、稳定可以直接用。5.3 实测帧率与CPU占用数据我在实际项目中测过一组数据环境是RK3568 A55四核1.8GHzSPI360MHz时钟240x320 ST7789RGB565使用fb_deferred_io50ms合并延迟。刷新模式帧率(fps)CPU占用(单核)说明全屏刷新PIO26-2865%能明显看到刷屏闪烁全屏刷新DMA30-3215%CPU占用大幅下降帧率接近理论值局部刷新PIO取决于变化区域很低时钟/温度这类小面积场景基本无感局部刷新DMA取决于变化区域极低推荐方案从这个表能看出DMA是最值得投入的优化手段。它的原理是把SPI传输描述符和DMA缓冲交给控制器CPU只需要在spi_sync期间等待完成中断。在使用DMA时要注意spi_transfer里的tx_buf需要指向dma_alloc_coherent申请的缓冲区或者在spi_message初始化时设置spi_transfer_dma。我在STM32上习惯用HAL库直接填DMA描述符到了Linux内核里反而简单因为SPI核心层已经把这些封装好了。还有个容易被忽略的细节即使使用DMAfb_info-screen_base依然是CPU可见的虚拟地址用户mmap得到的是这个地址的映射。发送到SPI时要保证DMA传输的起始地址对齐到SPI控制器要求的边界。我自己遇到过一次未对齐导致DMA传输前几个字节错乱的情况排查半天后发现是dma_alloc_coherent返回的地址没问题但我在计算偏移时没有做缓存行对齐。解决方案是使用IS_ALIGNED宏检查或者在申请显存时直接按64字节对齐。6. 调试三板斧白屏、花屏、颜色不对的排查路径6.1 白屏先背光后复位再查初始化序列接上一块新屏最常撞见的问题就是白屏。我的调试顺序是固定的先背光再复位再查初始化序列。第一步确认背光电路正常。如果屏幕有背光但不亮检查背光GPIO复用是否正确用gpioinfo或者简单的设备树读取命令确认引脚电平。如果背光亮了但整个屏幕是全白说明LCD面板本身通电正常只是没有显存内容或者TFT驱动异常。第二步确认复位时序。很多新手直接把RESET引脚悬空或者接到一个固定高电平这会导致控制器上电后处于未知状态。用示波器量一下RESET引脚的波形拉低时间要大于10us拉高后要有足够的稳定时间。我遇到过一批屏对RESET后等待时间特别敏感别的屏120ms就能初始化完成这批屏必须等到200ms否则首次初始化就失败白屏。第三步也是最复杂的检查初始化序列是否有遗漏或顺序错误。ST7789如果漏发了0x11Sleep Out屏幕会一直停留在睡眠模式白屏且没有任何响应。漏发0x3A设置像素格式则可能出现显示内容颜色错乱但不至于全白。建议在初始化序列每个关键命令后加一个回读操作如果支持或者直接对照官方驱动源码逐条核对。在嵌入式Linux里驱动加载后立刻主动刷新一帧纯色测试图像也能很快判断是初始化问题还是后续刷屏问题。6.2 花屏模式极性、时序余量与DMA缓存花屏是一个比白屏更令人抓狂的问题因为它的原因分布很广。我在实际调试中归纳出三类主要原因。第一类是SPI模式极性错误。屏幕工作模式与设备树里配置的spi-cpol、spi-cpha不匹配时数据会在错误的时钟沿被锁存出现间隔性乱码或整个画面打成雪花点。解决方法是把模式0和模式3都试一遍观察哪边显示稳定。注意SDO和MISO引脚的连线不接也看不出来问题但SDI也就是MOSI到屏幕的引脚的数据必须在正确的时钟沿采样。第二类是时序余量不足。这包括SPI时钟频率过高、DC引脚电平建立时间不够、CS释放时间不合规等。当把spi-max-frequency从60MHz提升到80MHz之后SPI时钟高电平时间缩短如果屏幕模组的输入建立时间不能满足要求就会出现偶发花屏。这种情况在PCB走线短且板级设计优秀时可能不明显但在飞线连接、排线较长的原型上非常容易出现。解决方式是降低频率同时在SPI时钟输出串一个小电阻或者改短飞线。第三类是DMA缓存一致性问题。如果显存使用vzalloc分配而SPI控制器设置了DMA传输CPU写入显存的数据可能还在Cache里DMA控制器读到的物理内存已经是脏数据。这会导致屏幕上的内容要么延迟更新、要么显示出一部分旧数据。在内核里使用dma_alloc_coherent或者传输前调用dma_map_single做Cache一致性处理可以解决。更稳妥的做法是确保显存本身是DMA一致性的避免在每次刷新前手动flush cache。还有一个小坑是DMA缓冲区未对齐。SPI控制器如果要求DMA描述符对齐到32字节或64字节而驱动把DMA偏移算错了一位传输就会出现首尾错乱。可以用dma_get_cache_alignment()查看平台要求申请缓存时多留一些padding。6.3 颜色偏色字节序、BGR位与灰度映射颜色偏色问题相对好定位但坑也不少。最经典的是红蓝对调就是之前提到的BGR/RGB位问题。在0x36命令里调整BGR位就能解决。如果驱动和设备树都预留了bgr属性直接翻转属性值重新加载驱动即可完成对比测试。另一种颜色不对是整体偏绿或偏暗这种情况大概率是RGB565的字节序和屏幕控制器的输入格式不一致。一些屏幕模组内部把数据总线的高低位做了翻转驱动发送的每个像素都是“低位在前”但控制器期望“高位在前”。此时所有颜色都会变化但不像红蓝对调那么明显。可以打出单色纯色测试分别填充纯红、纯绿、纯蓝然后观察屏幕显示。纯红如果变成了纯蓝说明RGB顺序纯红如果变成了暗红或者紫色说明某个通道的位深或位偏移有问题。再有一种情况是图形边界出现锯齿状彩色条纹这通常是内容颜色深度与帧缓冲不匹配比如上层用了ARGB8888而fbdev设置为RGB565。由于内核软件绘制函数cfb_fillrect处理16bpp时正常工作但上层不做转换直接丢32bpp数据过来就会出错。解决办法是严格按fb_var_screeninfo里的信息初始化上层绘图库或者在驱动里实现fb_set_par根据var-bits_per_pixel动态调整屏幕控制器的像素格式。最后想提一个边界情况有次屏幕出现整屏偏灰看起来像半透明蒙了一层。排查半天发现不是显示问题而是背光PWM的频率太低导致以相机快门速度观察时看到明显的亮度波动。把PWM频率从1kHz提到10kHz后彻底消失。这个案例提醒我显示驱动的问题不一定都在驱动里背光电路和供电也会以奇怪的方式出现在屏幕上。做个简单收尾。我在RK3568上用FrameBuffer模式驱动SPI LCD的整体感受是这条路虽然不如DRM那么“现代”但在小屏、低刷新率、快速出效果的场景下非常实用。框架简单调试路径清晰社区也有fbtft这类现成代码可以参考。只要把设备树时序、DC/CS/GPIO控制、DMA一致性和脏区域刷新这几个关键点处理好一块SPI小屏在RK3568上稳定跑起来并不难。如果你也在做类似的外接副屏、状态面板、工控显示项目希望这篇文章能帮你少走一段弯路。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

GeoLibre 开源技术栈全景:从 MapLibre 渲染到 Python 侧车的依赖生态解析 2026/9/16 18:19:39

GeoLibre 开源技术栈全景:从 MapLibre 渲染到 Python 侧车的依赖生态解析

GeoLibre 开源技术栈全景:从 MapLibre 渲染到 Python 侧车的依赖生态解析 【免费下载链接】GeoLibre A lightweight, cloud-native GIS platform for visualizing, exploring, and analyzing geospatial data. It runs in the web browser, on the desktop, on mobi…

阅读更多 →
JSON omitempty vs omitzero 终极指南:go-modern-guidelines 帮你写出更正确的 Go JSON 2026/9/16 18:19:39

JSON omitempty vs omitzero 终极指南:go-modern-guidelines 帮你写出更正确的 Go JSON

JSON omitempty vs omitzero 终极指南:go-modern-guidelines 帮你写出更正确的 Go JSON 【免费下载链接】go-modern-guidelines Help AI coding agents write modern Go 项目地址: https://gitcode.com/GitHub_Trending/go/go-modern-guidelines go-modern-g…

阅读更多 →
全栈开发技术栈:TypeScript+React+Next.js+MongoDB+Docker实践 2026/9/16 18:19:39

全栈开发技术栈:TypeScript+React+Next.js+MongoDB+Docker实践

1. 全栈开发的技术栈选择逻辑现代全栈开发已经形成了相对固定的技术组合模式,这套TypeScriptReactNext.jsMongoDBDocker的技术栈之所以能够成为主流选择,背后有着清晰的演进逻辑和技术合理性。TypeScript作为JavaScript的超集,在全栈开发中扮…

阅读更多 →
基于51单片机的智能断路器保护系统设计与Proteus仿真实现 2026/9/16 18:19:39

基于51单片机的智能断路器保护系统设计与Proteus仿真实现

简介:一套基于51单片机Proteus仿真的多功能断路器开发设计资料,面向单片机课程设计、电子竞赛及嵌入式入门者,解决过压、欠压、过流、温度异常时的实时监测与自动断电保护问题。系统以51单片机为核心,配合LCD1602液晶显示和按键操…

阅读更多 →
NocoBase 添加子记录操作(Add Child)实战指南:在树表区块中快速创建层级数据 2026/9/16 18:19:39

NocoBase 添加子记录操作(Add Child)实战指南:在树表区块中快速创建层级数据

NocoBase 添加子记录操作(Add Child)实战指南:在树表区块中快速创建层级数据 【免费下载链接】nocobase NocoBase is an open-source AI no-code platform for building business systems fast. Instead of generating everything from scra…

阅读更多 →
51单片机投票器设计:按键消抖、倒计时与数码管动态扫描 2026/9/16 18:16:39

51单片机投票器设计:按键消抖、倒计时与数码管动态扫描

简介:本资源是一套完整的基于51单片机的8人投票器设计与实现方案,面向电子类专业学生、单片机初学者及课程设计实践者,解决课堂实验、毕业设计或竞赛中对人机交互、定时控制与数据统计功能的典型开发需求。压缩包共36个文件,约837…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞