第4章 显示驱动移植实战(ILI9341):SPI/LTDC接口配置、DMA优化、双缓冲机制

显示驱动移植,说白了就是让GUI框架能跟屏幕“说上话”。ILI9341这颗驱动芯片,在嵌入式圈子里太常见了。我这些年经手的项目,少说有一半用的都是它。今天咱们就把它掰开揉碎了讲清楚。

你可能会问:为什么拿ILI9341举例?因为它同时支持SPI和LTDC两种接口,正好能对比LVGL和TouchGFX在驱动适配上的差异。嗯,选它就对了。

核心要点:显示驱动移植不是简单的“填寄存器”,而是要考虑接口带宽、刷新策略、内存布局。LVGL和TouchGFX在这三个维度上的处理方式完全不同。

4.1 SPI接口配置:从零开始点亮屏幕

SPI接口,说白了就是串行通信。优点是引脚少,缺点是速度慢。我刚开始做GUI时,总觉得SPI够用,直到有一次项目要求30fps刷新率……嗯,那之后我就老老实实研究LTDC了。

先看SPI的初始化流程。ILI9341的初始化序列其实挺固定的,但有几个坑要注意:

/* ILI9341 SPI初始化序列(关键部分) */
static void ili9341_init(void)
{
    /* 硬件复位 */
    HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_RESET);
    HAL_Delay(10);
    HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_SET);
    HAL_Delay(120);

    /* 发送初始化命令 */
    ili9341_write_cmd(0x01);  /* 软件复位 */
    HAL_Delay(120);

    ili9341_write_cmd(0x11);  /* 退出睡眠 */
    HAL_Delay(120);

    /* 像素格式:16位色 */
    ili9341_write_cmd(0x3A);
    ili9341_write_data(0x55);

    /* 内存访问控制 */
    ili9341_write_cmd(0x36);
    ili9341_write_data(0x48);  /* BGR顺序 + 行列交换 */

    /* 开启显示 */
    ili9341_write_cmd(0x29);
}

个人经验:初始化序列里的延时不能省。我曾经为了省那几十毫秒,把延时砍了一半,结果屏幕花屏。后来查手册才发现,ILI9341退出睡眠后需要120ms等待内部振荡器稳定。这个坑我踩过,你别踩。

4.2 LTDC接口配置:并行传输才是王道

LTDC接口,说白了就是并行RGB总线。它直接跟MCU的LTDC外设对接,数据是一拍一拍刷过去的。速度比SPI快一个数量级。

配置LTDC时,最关键的是时序参数。ILI9341的时序要求其实挺宽松的,但不同MCU的LTDC外设实现有差异。我习惯先把时序图打印出来,对着寄存器一个一个填:

参数 ILI9341要求 典型配置值 说明
HBP (Horizontal Back Porch) ≥ 10 20 行同步后的消隐区
HFP (Horizontal Front Porch) ≥ 10 10 行同步前的消隐区
VBP (Vertical Back Porch) ≥ 2 2 帧同步后的消隐区
VFP (Vertical Front Porch) ≥ 2 2 帧同步前的消隐区
Hsync Width ≥ 1 1 行同步脉冲宽度
Vsync Width ≥ 1 1 帧同步脉冲宽度

配置时有个小技巧:HBP和HFP加起来最好凑成4的倍数,这样DMA传输时对齐更好。我踩过这个坑,当时没对齐,DMA传输总是丢数据。

4.3 DMA优化:别让CPU干苦力活

DMA优化,说白了就是把刷屏这件事从CPU手里解放出来。CPU只管告诉DMA“把这块内存搬到屏幕”,然后就可以去干别的事了。

LVGL和TouchGFX在DMA使用上的思路不太一样:

  • LVGL:默认使用CPU刷屏,但提供了disp_flush回调接口,你可以在里面启动DMA传输。LVGL会等待DMA完成后再继续。
  • TouchGFX:原生支持DMA2D(Chrom-ART),刷屏操作自动走硬件加速。你只需要配置好DMA2D的源地址和目标地址。

看代码更直观:

/* LVGL的DMA刷屏回调 */
static void disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p)
{
    /* 启动DMA传输 */
    HAL_DMA_Start(&hdma_mem2mem, (uint32_t)color_p, 
                  (uint32_t)<dc_framebuf, 
                  (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1) * 2);

    /* 等待DMA完成(这里可以用中断优化) */
    HAL_DMA_PollForTransfer(&hdma_mem2mem, HAL_DMA_FULL_TRANSFER, 100);

    /* 通知LVGL刷新完成 */
    lv_disp_flush_ready(disp_drv);
}

/* TouchGFX的DMA2D配置(HAL层) */
void TouchGFXHAL::flushFrameBuffer(const touchgfx::Rect& rect)
{
    /* DMA2D自动处理,只需设置区域 */
    HAL_DMA2D_Start(&hdma2d, 
                    (uint32_t)frame_buffer, 
                    (uint32_t)ltdc_framebuf, 
                    rect.width, rect.height);
}

注意:LVGL的disp_flush_ready必须在DMA传输完成后调用。如果你在DMA还没完成时就调用了,LVGL会认为缓冲区已经可用,然后开始往里面写新数据——结果就是画面撕裂。我见过好几个项目因为这个bug折腾了一周。

4.4 双缓冲机制:告别画面撕裂

双缓冲,说白了就是准备两个缓冲区。一个给屏幕显示用(前台缓冲),一个给GUI绘制用(后台缓冲)。绘制完成后,交换指针,屏幕显示新画面。

为什么需要双缓冲?因为单缓冲时,你正在往缓冲区写数据,屏幕也在读同一个缓冲区——读和写打架了,画面就撕裂了。

LVGL和TouchGFX的双缓冲实现方式不同:

特性 LVGL TouchGFX
缓冲区数量 1~3个(可配置) 固定2个
切换方式 软件指针交换 硬件LTDC地址切换
撕裂预防 依赖disp_flush_ready时序 硬件VSync同步
内存开销 可控(最小1个缓冲区也能跑) 固定2个缓冲区

我个人习惯在LVGL里用3个缓冲区。为什么?因为2个缓冲区时,如果绘制速度跟不上刷新速度,还是会卡。3个缓冲区相当于多了一个“蓄水池”,能平滑掉绘制波动。

TouchGFX的双缓冲是硬件级别的。LTDC外设直接支持双缓冲地址切换,你只需要在VSync中断里更新LTDC的帧缓冲地址就行。这种方式更可靠,但灵活性差一些。

4.5 LVGL与TouchGFX的驱动适配差异

这部分是本章的重点。两种框架在驱动适配上的思路完全不同,我画了张图帮你理解:

LVGL vs TouchGFX 驱动适配架构对比 LVGL 驱动适配 disp_drv 结构体注册 (flush_cb / rounder_cb / monitor_cb) disp_flush 回调实现 (手动管理DMA / 等待完成) 缓冲区管理 (1~3个缓冲区 / 软件切换) 刷新完成通知 (disp_flush_ready 手动调用) TouchGFX 驱动适配 HAL 类继承 (TouchGFXHAL / 重写虚函数) flushFrameBuffer 实现 (DMA2D自动处理 / 硬件加速) 双缓冲硬件切换 (LTDC地址切换 / VSync同步) 帧同步回调 (HAL_FrameBufferSync 自动触发) 核心差异:LVGL把驱动控制权交给开发者,TouchGFX封装了底层细节 LVGL灵活但需要更多手动配置,TouchGFX自动化但定制空间小

从图上能看出来,两种框架的适配思路完全是反的:

  • LVGL:给你一堆回调接口,你自己实现刷屏逻辑。好处是灵活,坏处是你要自己处理DMA、缓冲区切换、同步等细节。
  • TouchGFX:封装了底层驱动,你只需要继承HAL类,重写几个关键函数。好处是上手快,坏处是遇到问题不好排查。

我的建议:如果你的团队有底层驱动经验,选LVGL,定制空间大。如果团队偏应用开发,选TouchGFX,开发效率高。没有绝对的好坏,只有合不合适。

4.6 避坑指南:我踩过的那些坑

做显示驱动移植,坑是真的多。我挑几个典型的说说:

  1. SPI时钟极性搞反:ILI9341要求SPI模式0(CPOL=0, CPHA=0),但有些MCU默认是模式3。我第一次移植时没注意,屏幕死活不亮,查了两天才发现是SPI模式不对。
  2. LTDC像素时钟频率超限:ILI9341最高支持20MHz像素时钟,但有些MCU的LTDC输出能到30MHz。我试过超频到25MHz,屏幕显示正常,但偶尔闪屏。后来老老实实降到18MHz,稳如老狗。
  3. DMA传输地址没对齐:DMA2D要求源地址和目标地址都是4字节对齐。我踩过这个坑,当时用了一个未对齐的缓冲区,DMA传输总是丢数据。后来加了个__ALIGN_BEGIN宏就解决了。
  4. 双缓冲切换时序不对:TouchGFX的双缓冲切换是在VSync中断里做的。如果你在VSync中断里做了太多事情,会导致切换延迟,画面出现撕裂。我建议VSync中断里只做地址切换,其他事情放到任务里做。

重要提醒:ILI9341的数据手册里有个坑——它的像素时钟极性(DOTCLK)默认是上升沿采样,但有些MCU的LTDC外设默认是下降沿输出。如果你发现屏幕显示错位,先检查这个极性配置。我因为这个坑浪费了整整一个下午。

4.7 性能对比:SPI vs LTDC

最后给个性能数据,让你心里有数:

接口类型 理论带宽 实际帧率(320x240, 16位色) CPU占用率 适用场景
SPI (40MHz) 5 MB/s 15~20 fps 30%~50% 静态界面、低刷新率
SPI + DMA 5 MB/s 20~25 fps 5%~10% 简单动画、菜单
LTDC (18MHz) 36 MB/s 50~60 fps < 5% 复杂动画、视频播放
LTDC + DMA2D 36 MB/s 60 fps (稳定) < 1% 高性能GUI、游戏

看到这个数据,你应该明白为什么我建议用LTDC了吧?SPI加DMA虽然能省CPU,但带宽瓶颈摆在那里,帧率上不去。LTDC加DMA2D才是高性能GUI的正确打开方式。

嗯,这一章的内容就到这。ILI9341的驱动移植,说白了就是搞清楚接口配置、DMA优化、双缓冲这三个点。LVGL和TouchGFX的差异,本质上是对“控制权”和“自动化”的不同取舍。你根据项目需求选就行。