- ILI9341_USE_DMA 开关:1=DMA+同步等待,0=纯阻塞SPI(不用DMA),同一入口 - ILI9341_LOG_EN/LOG_EVERY:每次(或每N次)flush 打印 方式/区域/字节/耗时/ 错误码/累计错误/超时/缓冲地址(判断是否落在AXI SRAM) - DWT 微秒计时;纯阻塞路径无 D-Cache/DMA 一致性问题,用于对照 - README 增加两种模式对照判读表
8.1 KiB
ILI9341 + LVGL 驱动说明(重写版)
STM32H750 · SPI1 · DMA1_Stream2 · LVGL v8
本次重写的目标:彻底解决移动物体残留/拖影 与 长时间运行白屏, 并让 SPI/DMA 时序自同步、可稳定构建运行。
1. 问题根因与对应改动
| 旧版问题 | 根因 | 新版做法 |
|---|---|---|
| 移动方块留下残影/线条 | disp_flush() 里做软件 90° 转置,坐标/尺寸与 LVGL 失效区域不严格一致,且多一层 rot_buf→dma_buf 拷贝 |
硬件旋转:面板 MADCTL 直接设横屏,LVGL 坐标 = 面板坐标,刷新区与失效区严格 1:1,转置整段删除 |
| 长时间运行白屏 | ① DMA 源缓冲可能被链接器放进 DTCM,而 DMA1 无法访问 DTCM;② 阻塞式信号量 + 出错 Abort,SPI 状态一乱就级联白屏 |
① 缓冲用 .lcd_ram 段强制放入 AXI SRAM;② 传输超时后重建 SPI 自恢复,单次异常不级联 |
| 刷新时序/竞态 | 中断里 lv_disp_flush_ready、ISR 内 Abort、完成被收尾两次等竞态,会丢失“擦除”写入 → 移动物体残留 |
DMA 加速 + 任务内同步等待:中断只置完成标志,等待/收尾/回调全在任务里做;flush 返回时该块像素一定已完整写入,杜绝提前复用缓冲/擦除未写完 |
| 字节序靠 CPU 逐像素交换 | LV_COLOR_16_SWAP=0,驱动里手工打包 |
LV_COLOR_16_SWAP=1,LVGL 直接产出面板字节序,DMA 直接搬运 |
备注:旧
ILI9341.h里关于 “CS=PB12 未配置为输出” 的警告是过时的——main.h/gpio.c已把 PB12 配为推挽输出,CS 时序正常,无需改动。
2. 数据通路
LVGL 渲染 → buf_1/buf_2 (.lcd_ram, AXI SRAM, 32B 对齐)
→ disp_flush() : 裁剪坐标,直接调 ILI9341_FlushArea_DMA()
→ FlushArea_DMA : 设窗口(2A/2B/2C) → clean D-Cache → CS 低 → 启动 SPI DMA
→ 任务内轮询等待 lcd_xfer_done(1~2ms,50ms 超时兜底)
→ 等 SPI 移位排空 → CS 高 → done_cb(=lv_disp_flush_ready)
→ [DMA 完成] SPI1 中断 → HAL_SPI_TxCpltCallback → 仅置 lcd_xfer_done=1
→ [出错] HAL_SPI_ErrorCallback → 记录错误 + 置 lcd_xfer_done=1,任务侧恢复
→ [兜底] 超时(或 ServiceWatchdog) → Abort + 重建 SPI
关键不变量:flush_cb 返回时该块像素一定已完整写入面板,LVGL 不会提前复用
缓冲,“擦除”写入不会丢失。中断只做“置标志”一件事,不在中断里调用 LVGL、不在
中断里 Abort,从根上消除竞态。
3. 内存放置(最关键,务必确认)
STM32H7 的坑:DMA1/DMA2 无法访问 DTCM(0x20000000),能访问 AXI SRAM(0x24000000)。
- 自定义链接脚本:
MDK-ARM/motor_ili9341_lcd.sctRW_IRAM1 @0x20000000(DTCM):普通变量/堆栈,不放 DMA 缓冲。RW_IRAM2 @0x24000000(AXI SRAM):*(.lcd_ram)显式放这里 + 其余 RW。
- 工程已改用该脚本:
motor_ili9341.uvprojx中<umfTarg>0</umfTarg>+<ScatterFile>.\motor_ili9341_lcd.sct</ScatterFile>。 - 所有交给 DMA 的缓冲都用
LCD_RAM_ATTR(=.lcd_ram段 + 32B 对齐)修饰, 见lv_port_disp.c的buf_1/buf_2。
在 Keil 里若手改内存布局,请始终保证
.lcd_ram落在 AXI SRAM。
4. 方向/镜像调节
方向完全由 ILI9341_SetRotation() 的 MADCTL 决定(默认 1 = 横屏 320×240,
MADCTL=0x28)。若烧录后镜像/上下颠倒,只需换 MADCTL:
- 横屏:
0x28↔0xE8 - 竖屏:
0x48↔0x88
换方向后如果分辨率变了,同步修改 lv_port_disp.c 的
MY_DISP_HOR_RES / MY_DISP_VER_RES。
4.5 诊断:DMA vs 纯阻塞 + 日志(排查错位/残留)
在 ILI9341.h 顶部有三个开关:
#define ILI9341_USE_DMA 1 // 1=DMA搬运(+任务内同步等待) 0=纯阻塞SPI(不用DMA)
#define ILI9341_LOG_EN 1 // 1=每次flush打印一行日志(UART4)
#define ILI9341_LOG_EVERY 1U // 每N次flush打印一次(串口太卡就调大,如 8/16)
两种传输走同一个入口 ILI9341_FlushArea_DMA(),只是内部实现不同,
所以切换 ILI9341_USE_DMA 后其它代码无需改动,直接对比现象与日志即可判断
“错位/残留”到底和 DMA / D-Cache 有没有关系。
日志格式(UART4,115200 或工程设定波特率):
[LCD] DMA #1234 (20,115 46x40) len=3680 t=210us fail=0 err=0x0 eC=0 toC=0 buf=0x24000000
│ │ │ │ │ │ │ │ │ │ └ 数据缓冲地址:必须是 0x24xxxxxx(AXI SRAM);
│ │ │ │ │ │ │ │ │ └ 累计超时次数 若是 0x2000xxxx(DTCM) 说明 scatter 没生效
│ │ │ │ │ │ │ │ └ 累计错误次数
│ │ │ │ │ │ │ └ 本次 SPI 错误码(HAL ErrorCode)
│ │ │ │ │ │ └ 本次是否失败(1=超时/出错)
│ │ │ │ │ └ 本次传输耗时(微秒, DWT 计时)
│ │ │ │ └ 本次字节数
│ │ │ └ 区域宽x高
│ │ └ 区域左上角(x,y)
│ └ flush 序号
└ 传输方式: DMA / POLL
怎么判断:
| 现象 | 结论 |
|---|---|
POLL 模式画面干净、DMA 模式错位 |
问题在 DMA / D-Cache 一致性(缓冲没 clean 干净、或缓冲不在 AXI) |
| 两种模式都错位 | 与 DMA 无关,是面板刷新撕裂(需 TE 同步)或上层逻辑 |
日志里 buf=0x2000xxxx |
scatter 没生效,缓冲落在 DTCM,DMA 读到错/旧数据 → 先修内存放置 |
err= 非 0 或 eC 持续增长 |
SPI 总线错误(接线/速率/干扰),先降 BaudRatePrescaler 验证 |
toC 增长 |
传输超时,DMA/中断未触发,查 NVIC/时钟 |
t= 远大于预期(如 >20ms) |
SPI 太慢/被阻塞,撕裂概率大增 |
串口日志是阻塞发送,会显著拖慢刷新、改变时序。判断完请把
ILI9341_LOG_EN关回 0(或把ILI9341_LOG_EVERY调大),否则时序失真、也会掩盖真实表现。
5. 参数与调优
DISP_BUF_LINES(lv_port_disp.c,默认 20):单块缓冲行数。越大单次 DMA 越长、 刷新越连续;越小越省 RAM。两块共320*行数*2*2字节。DMA 单次字节数须 < 65535。ILI9341_ServiceWatchdog(180)(freertos.c 任务循环):超时(ms)兜底恢复,建议 150~200ms。正常刷新根本走不到看门狗,它只处理硬件级卡死。- SPI 速度:
spi.c的BaudRatePrescaler。残影/花屏排查时可先降速验证接线。
6. 对外 API(ILI9341.h)
void ILI9341_Init(void); // 初始化 + 默认横屏
void ILI9341_SetRotation(uint8_t r); // 0/1/2/3
void ILI9341_FillScreen(uint16_t color); // 阻塞,初始化清屏用
void ILI9341_FillRect(...); // 阻塞
uint16_t ILI9341_Color565(r,g,b);
void ILI9341_FlushArea_DMA(x,y,w,h,data,done_cb,user); // 异步,LVGL 用
uint8_t ILI9341_IsBusy(void);
void ILI9341_ServiceWatchdog(uint32_t timeout_ms); // 任务里周期调用
FlushArea_DMA 的 data 必须已是面板字节序(由 LV_COLOR_16_SWAP=1 保证),
且位于 DMA 可访问内存。
7. 构建与验证清单
- Keil 打开工程 → 确认 Options → Linker 使用
motor_ili9341_lcd.sct(本工程已配置)。 - 编译无警告地通过;
.map里确认buf_1/buf_2/.lcd_ram地址在0x24xxxxxx。 - 烧录后:
- 蓝色方块左右移动无残留线条/拖影;
- 长时间(数十分钟以上)运行不白屏、不冻结;
- 若镜像/颠倒,按第 4 节调 MADCTL。
本仓库环境无法编译/烧录 STM32,以上第 2、3 步需在 Keil + 硬件上确认。 第 3 节的内存放置是唯一强依赖工具链的点,请务必核对
.map。