重构 ILI9341+LVGL 驱动:异步 DMA + 硬件旋转,修复残影与白屏

- 硬件 MADCTL 旋转,删除软件转置,LVGL 坐标与面板 1:1,消除移动残影
- 全异步 DMA:flush 启动即返回,完成/出错由 SPI 中断收尾,去掉信号量阻塞
- DMA 缓冲经 .lcd_ram 段强制放入 AXI SRAM,规避 DTCM 不可被 DMA 访问导致的白屏
- LV_COLOR_16_SWAP=1,LVGL 直出面板字节序,省去逐像素交换/拷贝
- 看门狗超时重建 SPI 自恢复,单次异常不再级联长时间白屏
- 定制 scatter(motor_ili9341_lcd.sct) 并接入 uvprojx
- 新增 README_ILI9341.md 说明根因/设计/构建验证
This commit is contained in:
zhy200308
2026-07-08 11:02:42 +08:00
parent b4423fcfa2
commit e4d61b19f0
8 changed files with 460 additions and 773 deletions
+69 -55
View File
@@ -7,27 +7,37 @@ extern "C" {
#include "main.h"
#include "spi.h"
/* 默认竖屏分辨率 */
#define ILI9341_WIDTH 240
#define ILI9341_HEIGHT 320
#include <stdint.h>
#include <stddef.h>
/* ============================================================
* LCD 控制引脚
* ILI9341 TFT-LCD 驱动 (STM32H750 + SPI1 + DMA1_Stream2)
*
* !!! 重要 !!!
* 下面的 LCD_CS_PIN 目前是 GPIO_PIN_12,但 gpio.c 的
* MX_GPIO_Init() 里只把 GPIO_PIN_6 配置成了输出(Output_PP)。
* GPIO_PIN_12 复位后是 Analog 模式,不是输出,直接
* HAL_GPIO_WritePin() 不会对物理引脚产生任何效果。
*
* 请确认实际硬件接线:
* - 如果 CS 确实接在 PB12,必须在 MX_GPIO_Init() 里把
* GPIO_PIN_12 加入 output push-pull 初始化列表;
* - 如果实际接线仍是 PB6,请把下面的宏改回 GPIO_PIN_6
* 在确认并修复之前,CS 时序不可信,任何"偶发花屏/白屏"的
* 排查都应该先排除这个问题
* 设计要点(详见 SystemDrivers/README_ILI9341.md):
* 1. 硬件旋转:方向完全由 MADCTL 决定,软件不做转置,
* LVGL 坐标与面板坐标 1:1 对应,从根本上消除移动物体
* 残留/拖影问题。
* 2. 全异步 DMAILI9341_FlushArea_DMA() 启动 DMA 后立刻返回,
* 传输完成由 SPI 中断回调收尾并触发用户回调(=lv_disp_flush_ready)。
* 刷新任务不再阻塞,SPI/DMA 时序自同步,不依赖任何 RTOS 信号量。
* 3. DMA 源缓冲必须位于 DMA 可访问内存(AXI SRAM 0x24000000)
* 绝不能放在 DTCM(0x20000000)——DMA1 无法访问 DTCM。
* LVGL 绘制缓冲用 LCD_RAM_ATTR 强制放入 .lcd_ram 段
* 4. 看门狗 + 出错自恢复:任何一次传输异常都会被恢复,
* 不会级联导致长时间白屏
* ============================================================ */
/* 面板原生(未旋转)分辨率 */
#define ILI9341_NATIVE_WIDTH 240U
#define ILI9341_NATIVE_HEIGHT 320U
/* 当前工作方向下的分辨率(默认横屏 320x240,见 ILI9341_SetRotation */
#define ILI9341_WIDTH 320U
#define ILI9341_HEIGHT 240U
/* ------------------------------------------------------------
* LCD 控制引脚(与 main.h / gpio.c 保持一致,PB12=CS 已被配置为输出)
* ------------------------------------------------------------ */
#define LCD_CS_PORT GPIOB
#define LCD_CS_PIN GPIO_PIN_12
@@ -40,7 +50,7 @@ extern "C" {
#define LCD_BL_PORT GPIOB
#define LCD_BL_PIN GPIO_PIN_9
/* RGB565 常用颜色 */
/* RGB565 常用颜色(大端字节序由发送时处理) */
#define ILI9341_BLACK 0x0000
#define ILI9341_WHITE 0xFFFF
#define ILI9341_RED 0xF800
@@ -51,49 +61,53 @@ extern "C" {
#define ILI9341_MAGENTA 0xF81F
#define ILI9341_GRAY 0x8410
typedef void (*ILI9341_DmaDoneCallback)(void *user_data);
/* ------------------------------------------------------------
* DMA 可访问内存属性。
* 所有交给 ILI9341_FlushArea_DMA() 的缓冲区都必须用它修饰,
* 链接脚本(motor_ili9341_lcd.sct)会把 .lcd_ram 段放到 AXI SRAM。
* 32 字节对齐是为了配合 D-Cache 的 clean-by-address 粒度。
* ------------------------------------------------------------ */
#if defined(__GNUC__) || defined(__clang__)
#define LCD_RAM_ATTR __attribute__((section(".lcd_ram"), aligned(32)))
#else
#define LCD_RAM_ATTR
#warning "未识别的工具链:请自行确保 LCD DMA 缓冲位于 DMA 可访问内存并 32 字节对齐"
#endif
void ILI9341_Init(void);
void ILI9341_SetRotation(uint8_t rotation);
/* 传输完成回调(在 SPI 中断上下文里被调用,务必只做极短的事) */
typedef void (*ILI9341_FlushDoneCb)(void *user_data);
void ILI9341_FillScreen(uint16_t color);
void ILI9341_DrawPixel(uint16_t x, uint16_t y, uint16_t color);
void ILI9341_FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color);
void ILI9341_WriteArea(uint16_t x,
uint16_t y,
uint16_t w,
uint16_t h,
const uint16_t *pixels);
void ILI9341_WriteArea_DMA(uint16_t x,
uint16_t y,
uint16_t w,
uint16_t h,
const uint16_t *pixels,
ILI9341_DmaDoneCallback done_cb,
void *user_data);
/*
* DMA 看门狗:建议从任务(线程)上下文周期性调用,例如放在
* StartDefaultTask 的 for(;;) 循环里、紧跟在 lv_timer_handler()
* 之后。绝不能从任何中断服务程序里调用。
*
* 新实现里 ILI9341_WriteArea_DMA 本身已经通过信号量+超时
* 做了同步等待和自恢复,正常情况下不需要这个看门狗介入;
* 它只是兜底:万一某次调用路径绕过了 WriteArea_DMA 内部的
* 超时保护(例如未来新增的调用方式),仍然能强制恢复 SPI
* 状态机,避免 lcd_dma_busy 卡死导致后续所有刷新被永久跳过。
*
* timeout_ms 建议 150~200ms(要大于 WriteArea_DMA 内部
* 100ms 的超时阈值,避免互相打架)。
*/
void ILI9341_DMA_Watchdog(uint32_t timeout_ms);
/* ---- 初始化 / 方向 ---- */
void ILI9341_Init(void);
void ILI9341_SetRotation(uint8_t rotation); /* 0/1/2/3,默认 1=横屏 */
/* ---- 阻塞式基础绘图(仅用于初始化清屏/自检,非 LVGL 热路径) ---- */
void ILI9341_FillScreen(uint16_t color);
void ILI9341_FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color);
uint16_t ILI9341_Color565(uint8_t r, uint8_t g, uint8_t b);
/* ---- LVGL 异步刷新入口 ----
* data 必须已按面板发送顺序排列(配合 lv_conf.h 的 LV_COLOR_16_SWAP=1
* LVGL 直接产出正确字节序,无需再拷贝/交换)。
* 函数启动 DMA 后立即返回;传输完成后在中断里调用 done_cb(user_data)。
* 若参数非法或启动失败,会同步调用 done_cb 以避免 LVGL 卡死。 */
void ILI9341_FlushArea_DMA(uint16_t x, uint16_t y,
uint16_t w, uint16_t h,
const uint16_t *data,
ILI9341_FlushDoneCb done_cb,
void *user_data);
/* ---- 传输是否进行中(供上层判断) ---- */
uint8_t ILI9341_IsBusy(void);
/* ---- 看门狗:从任务上下文周期性调用(例如 lv_timer_handler 之后)。
* 绝不能从中断里调用。若一次 DMA 传输超过 timeout_ms 仍未完成,
* 强制中止、重建 SPI、恢复 CS 并触发完成回调,避免永久白屏。
* timeout_ms 建议 150~200ms。 */
void ILI9341_ServiceWatchdog(uint32_t timeout_ms);
#ifdef __cplusplus
}
#endif
#endif
#endif /* __ILI9341_H__ */