在RGBLCD上使用LVGL时踩到的一些坑
最近入手了一块RGBLCD并口屏幕,打算替换开发板上原有的SPI串口屏。新屏幕不仅尺寸更大、分辨率更高,还支持多点触控,可玩性无疑会提升不少。不过在适配使用的过程中,我还是遇到了不少问题,这里做个简单记录,供有相似需求的开发者参考。
1. RGBLCD驱动适配
我选用STM32H750作为主控芯片,通过LTDC接口驱动该屏幕。LTDC的使用难度不高,按照屏幕厂家提供的参数配置相关寄存器,就能快速点亮屏幕。其中可调整的关键参数是屏幕刷新率——屏幕的并口时钟由PLL3的R分频提供,我暂将其配置为32MHz,对应屏幕刷新率约为83Hz。
该屏幕分辨率为800x480,采用RGB565颜色格式,需要分配一块750KB的图像帧内存。由于单片机内置SRAM空间不足,我将这块帧内存分配到了外部SDRAM中,但由此产生了新的顾虑:外部RAM的带宽可能不足,容易出现MCU与屏幕抢占内存带宽的情况。此外,对屏幕显存进行操作时,必须格外注意D-Cache的处理,若未执行D-Cache的Clean操作,屏幕极易出现花屏问题。
2. 将RGBLCD集成到LVGL的优化过程
这一环节的核心问题集中在性能优化上,主要围绕”提升帧率(目标50fps)、降低处理器负载、避免画面撕裂、防止花屏”四个方向展开。
初期我采用LVGL双缓存局部刷新模式,将两块256KB的缓存(合计为屏幕内存的三分之一)同样存放在外部SDRAM中。这种方案能实现基本的显示效果,但在全屏页面滑动时,偶尔会出现画面撕裂问题,运行基准测试(benchmark)的帧率仅为37fps,未达预期目标。
为彻底解决画面撕裂,我转而采用显存直通模式,分配两块750KB的内存作为显存,交替完成渲染与刷新操作。该模式的关键是配合屏幕垂直同步信号——需在屏幕刷新至最后一个像素时切换显存指针,同时依赖相关中断信号实现双显存的同步:一块用于LVGL渲染,另一块用于屏幕刷新。
这里曾踩过一个频闪坑:在直通模式下实现flush_cb函数时,未判断画面是否需要刷新。后续通过调用lv_display_flush_is_last函数做判断,成功解决了不完整画面的刷新问题。
后续发现LVGL内置了LTDC驱动,其实现更简洁高效,支持直通模式与局部刷新模式(局部刷新模式还可搭配DMA2D使用)。最终我选择”直通模式+双显存”的组合,将两块显存通过MPU配置为透写模式避免花屏。优化后效果显著:画面撕裂与花屏问题彻底解决,运行基准测试的帧率也略高于此前的局部刷新模式。
3. DMA2D的合理使用
DMA2D可用于画面层叠与显存拷贝,而要发挥其作用,需先明确LVGL的核心流程:渲染是指LVGL将代码描述的页面组件计算为显存数据的过程,刷新则是将显存数据传输至屏幕的过程。DMA2D可参与这两个环节以降低CPU负载,但需注意一个关键限制:DMA2D无法同时服务于渲染和刷新。由于LVGL双缓存机制支持渲染与刷新并行,因此DMA2D只能二选一参与其中。
通过配置宏定义可实现DMA2D的功能切换:开启LV_USE_DRAW_DMA2D宏可将DMA2D用于渲染,配置LV_ST_LTDC_USE_DMA2D_FLUSH宏可将其用于LTDC局部刷新,但这两个宏不可同时开启。结合前文采用的显存直通模式(刷新过程无需DMA2D参与),我将DMA2D优先分配给渲染过程,以最大化降低CPU负担。
4. DMA2D渲染中的隐藏风险
DMA2D渲染的核心代码位于/src/draw/dma2d/目录下,其中lv_draw_dma2d.c文件的缓存处理逻辑存在重大隐患。凡是涉及DMA的操作,都绕不开Cache一致性问题——通常在DMA搬运数据前,需对源地址执行Clean操作,对目标地址执行Invalid操作。但在LVGL v9.4版本中,这部分逻辑被修改为全局缓存操作,代码如下:
1 |
|
这种全局D-Cache操作完全忽视了其他外设的DMA需求。例如,若某SPI外设正通过DMA读取数据,此时若因多线程调度执行SCB_CleanDCache(),会导致SPI读取的数据出错;而SCB_InvalidateDCache()的风险更高——直接丢弃缓存数据可能破坏栈内有效信息,进而导致程序跑飞。
该问题源于2024年5月的一次兼容性优化提交(feat(dma2d): improve DMA2D Compatibility by apedersen00)。作者将原本的局部缓存处理改为全局清理,认为单片机缓存空间小,两种方式的性能开销相近且代码更简洁,相关基准测试也显示性能波动不大。但这种修改显然对D-Cache的工作机制考虑不足,引入了新的稳定性风险。
5. LVGL的资源开销控制
LVGL需周期性调用lv_timer_handler()处理内部周期性事件,我的实现方式是创建独立线程专门负责调度,代码如下:
1 | void lvgl_handler_task(void *p){ |
该任务初始栈大小设为1024字,运行Demo时却出现栈溢出问题;将栈大小提升至2048字后,问题得以解决。有趣的是,使用GCC静态栈分析工具时,并未检测到超过1024字的调用链——这大概率是LVGL内部大量函数指针与回调机制导致的,这类动态调用链难以通过静态分析覆盖,因此使用LVGL时必须通过实际测试评估栈空间需求。
LVGL提供RTOS支持,开启LV_USE_OS宏后,部分异步操作可释放处理器资源,避免阻塞等待。此外,LVGL还会创建一个或多个独立线程负责渲染,将渲染过程从lv_timer_handler()中剥离。但我的实际测试显示,在单核处理器上,独立渲染线程并未提升性能,反而因线程调度与同步的额外开销导致帧率略有下降;推测在多核处理器中,通过多渲染线程并行处理,可能会实现性能突破。