第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:给你一堆回调接口,你自己实现刷屏逻辑。好处是灵活,坏处是你要自己处理DMA、缓冲区切换、同步等细节。
- TouchGFX:封装了底层驱动,你只需要继承HAL类,重写几个关键函数。好处是上手快,坏处是遇到问题不好排查。
我的建议:如果你的团队有底层驱动经验,选LVGL,定制空间大。如果团队偏应用开发,选TouchGFX,开发效率高。没有绝对的好坏,只有合不合适。
4.6 避坑指南:我踩过的那些坑
做显示驱动移植,坑是真的多。我挑几个典型的说说:
- SPI时钟极性搞反:ILI9341要求SPI模式0(CPOL=0, CPHA=0),但有些MCU默认是模式3。我第一次移植时没注意,屏幕死活不亮,查了两天才发现是SPI模式不对。
- LTDC像素时钟频率超限:ILI9341最高支持20MHz像素时钟,但有些MCU的LTDC输出能到30MHz。我试过超频到25MHz,屏幕显示正常,但偶尔闪屏。后来老老实实降到18MHz,稳如老狗。
- DMA传输地址没对齐:DMA2D要求源地址和目标地址都是4字节对齐。我踩过这个坑,当时用了一个未对齐的缓冲区,DMA传输总是丢数据。后来加了个
__ALIGN_BEGIN宏就解决了。 - 双缓冲切换时序不对: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的差异,本质上是对“控制权”和“自动化”的不同取舍。你根据项目需求选就行。