ILI9341: 刷新改为 DMA+任务内同步等待,消除移动残留竞态

- 中断只置完成标志,等待/收尾/回调全部移到任务上下文
- 去掉 ISR 内 lv_disp_flush_ready、ISR 内 Abort、双重收尾等竞态
- flush 返回时该块像素必已完整写入,LVGL 不会提前复用缓冲
- 拉高 CS 前等待 SPI 移位排空,避免尾部像素被截断
- 传输 50ms 超时后重建 SPI 自恢复
This commit is contained in:
zhy200308
2026-07-08 11:16:57 +08:00
parent e4d61b19f0
commit 1273a14bf2
2 changed files with 76 additions and 73 deletions
+67 -66
View File
@@ -33,13 +33,14 @@ extern SPI_HandleTypeDef hspi1;
static uint16_t lcd_width = ILI9341_WIDTH; static uint16_t lcd_width = ILI9341_WIDTH;
static uint16_t lcd_height = ILI9341_HEIGHT; static uint16_t lcd_height = ILI9341_HEIGHT;
/* ================= 异步刷新状态(全部由 volatile 保护 ================= */ /* ================= 刷新状态(DMA 传输 + 任务内同步等待 =================
typedef enum { LCD_IDLE = 0, LCD_BUSY = 1 } lcd_state_t; * 设计取舍:DMA 只用来加速一块像素的搬运,等待/收尾/回调全部放在
* 任务上下文里做(同步)。中断只负责把 lcd_xfer_done 置 1,极短。
static volatile lcd_state_t lcd_state = LCD_IDLE; * 这样彻底消除“ISR 里调用 lv_disp_flush_ready / 在 ISR 里 Abort /
* 完成被收尾两次”等竞态——这些正是快速移动物体出现残留的隐患。 */
static volatile uint8_t lcd_xfer_done = 1; /* 1=空闲/已完成 */
static volatile uint32_t lcd_spi_err = 0; /* 最近一次 SPI 错误码 */
static volatile uint32_t lcd_start_tick = 0; static volatile uint32_t lcd_start_tick = 0;
static ILI9341_FlushDoneCb lcd_done_cb = NULL;
static void *lcd_done_arg = NULL;
/* ================= 阻塞式低层写命令/数据(仅用于初始化与设窗口) ================= */ /* ================= 阻塞式低层写命令/数据(仅用于初始化与设窗口) ================= */
@@ -210,39 +211,30 @@ static void LCD_CleanDCache(const void *addr, uint32_t size)
#endif #endif
} }
/* ================= 异步刷新完成收尾(供 ISR / 看门狗调用) ================= */ /* ================= 等待 SPI 移位彻底完成 ================= */
static void LCD_FinishTransfer(void) static void LCD_SPI_WaitShiftEmpty(void)
{ {
ILI9341_FlushDoneCb cb; /* HAL 在传输完成中断里已经把数据全部交给 SPI,但最后几个 bit 可能
void *arg; * 还在移位。拉高 CS 前确认 TXC/EOT,避免尾部像素被 CS 截断——尾部
* 截断在快速移动时会表现为“擦除不干净/残留一条边”。 */
uint32_t t0 = HAL_GetTick();
/* 关中断内做“取出并清空回调 + 复位状态”,保证只收尾一次, #if defined(SPI_FLAG_TXC)
* 杜绝 ISR 与看门狗竞争导致 done_cb 被调用两次。 */ while (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_TXC) == RESET)
uint32_t primask = __get_PRIMASK();
__disable_irq();
if (lcd_state != LCD_BUSY)
{ {
__set_PRIMASK(primask); if ((HAL_GetTick() - t0) > 5U) break;
return; }
#elif defined(SPI_FLAG_EOT)
while (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_EOT) == RESET)
{
if ((HAL_GetTick() - t0) > 5U) break;
}
#endif
__DSB();
} }
LCD_CS_HIGH(); /* ================= LVGL 刷新入口(DMA 加速,任务内同步等待完成) ================= */
lcd_state = LCD_IDLE;
cb = lcd_done_cb;
arg = lcd_done_arg;
lcd_done_cb = NULL;
__set_PRIMASK(primask);
if (cb != NULL)
{
cb(arg);
}
}
/* ================= LVGL 异步刷新入口 ================= */
void ILI9341_FlushArea_DMA(uint16_t x, uint16_t y, void ILI9341_FlushArea_DMA(uint16_t x, uint16_t y,
uint16_t w, uint16_t h, uint16_t w, uint16_t h,
@@ -251,6 +243,7 @@ void ILI9341_FlushArea_DMA(uint16_t x, uint16_t y,
void *user_data) void *user_data)
{ {
uint32_t byte_len; uint32_t byte_len;
uint32_t t0;
if (w == 0 || h == 0 || data == NULL) if (w == 0 || h == 0 || data == NULL)
{ {
@@ -258,52 +251,65 @@ void ILI9341_FlushArea_DMA(uint16_t x, uint16_t y,
return; return;
} }
/* 正常情况下 LVGL 在 flush_ready 之前不会再次进入,走到这里说明
* 上一次传输异常未收尾:强制恢复后再继续,避免卡死。 */
if (lcd_state == LCD_BUSY)
{
HAL_SPI_Abort(&hspi1);
LCD_CS_HIGH();
lcd_state = LCD_IDLE;
}
byte_len = (uint32_t)w * h * 2U; byte_len = (uint32_t)w * h * 2U;
/* 登记回调并置忙。先置忙再启动,保证完成中断能看到 BUSY。 */
lcd_done_cb = done_cb;
lcd_done_arg = user_data;
lcd_state = LCD_BUSY;
lcd_start_tick = HAL_GetTick();
LCD_SetWindow(x, y, x + w - 1U, y + h - 1U); LCD_SetWindow(x, y, x + w - 1U, y + h - 1U);
/* DMA 读取前 clean D-Cache,确保面板拿到最新像素 */ /* DMA 读取前 clean D-Cache,确保面板拿到最新像素 */
LCD_CleanDCache(data, byte_len); LCD_CleanDCache(data, byte_len);
lcd_xfer_done = 0;
lcd_spi_err = 0;
lcd_start_tick = HAL_GetTick();
LCD_CS_LOW(); LCD_CS_LOW();
LCD_DC_DATA(); LCD_DC_DATA();
if (HAL_SPI_Transmit_DMA(&hspi1, (uint8_t *)data, (uint16_t)byte_len) != HAL_OK) if (HAL_SPI_Transmit_DMA(&hspi1, (uint8_t *)data, (uint16_t)byte_len) != HAL_OK)
{ {
lcd_xfer_done = 1;
LCD_CS_HIGH(); LCD_CS_HIGH();
lcd_state = LCD_IDLE;
lcd_done_cb = NULL;
if (done_cb) done_cb(user_data); if (done_cb) done_cb(user_data);
return; return;
} }
/* 传输完成 → HAL_SPI_TxCpltCallback;出错 → HAL_SPI_ErrorCallback。 */
/* 任务内同步等待:中断只把 lcd_xfer_done 置 1,这里轮询等待。
* 传输一小块通常 1~2ms 即完成;50ms 超时兜底防止硬件卡死。
* 这样每次 flush 返回时,这块像素一定已经完整写入面板——不会出现
* “LVGL 提前复用缓冲 / 擦除区没写完”导致的移动残留。 */
t0 = HAL_GetTick();
while (lcd_xfer_done == 0U)
{
if ((HAL_GetTick() - t0) > 50U)
{
HAL_SPI_Abort(&hspi1);
/* 传输异常,重建 SPI,避免坏状态带入后续帧 */
if (HAL_SPI_DeInit(&hspi1) == HAL_OK)
{
HAL_SPI_Init(&hspi1);
}
lcd_xfer_done = 1;
break;
}
}
LCD_SPI_WaitShiftEmpty();
LCD_CS_HIGH();
if (done_cb) done_cb(user_data);
} }
uint8_t ILI9341_IsBusy(void) uint8_t ILI9341_IsBusy(void)
{ {
return (lcd_state == LCD_BUSY) ? 1U : 0U; return (lcd_xfer_done == 0U) ? 1U : 0U;
} }
/* ================= 看门狗(任务上下文调用) ================= */ /* ================= 看门狗(任务上下文调用) =================
* 同步刷新下,本函数返回时传输一定已完成,看门狗基本用不到;
* 保留作为极端兜底:若发现长时间处于“未完成”,强制恢复 SPI。 */
void ILI9341_ServiceWatchdog(uint32_t timeout_ms) void ILI9341_ServiceWatchdog(uint32_t timeout_ms)
{ {
if (lcd_state != LCD_BUSY) if (lcd_xfer_done != 0U)
{ {
return; return;
} }
@@ -313,25 +319,22 @@ void ILI9341_ServiceWatchdog(uint32_t timeout_ms)
return; return;
} }
/* 传输疑似卡死:中止 DMA、重建 SPI,再收尾(触发完成回调)。 */
HAL_SPI_Abort(&hspi1); HAL_SPI_Abort(&hspi1);
if (HAL_SPI_DeInit(&hspi1) == HAL_OK) if (HAL_SPI_DeInit(&hspi1) == HAL_OK)
{ {
HAL_SPI_Init(&hspi1); HAL_SPI_Init(&hspi1);
} }
LCD_CS_HIGH();
LCD_FinishTransfer(); lcd_xfer_done = 1;
} }
/* ================= SPI 中断回调(ISR 上下文,尽量短) ================= */ /* ================= SPI 中断回调(ISR 上下文,短) ================= */
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{ {
if (hspi->Instance == SPI1) if (hspi->Instance == SPI1)
{ {
/* H7 上 TxCplt 由 SPI EOT 触发,此刻数据已全部移出,可直接拉高 CS。 */ lcd_xfer_done = 1; /* 只置标志,收尾/回调全部在任务里做 */
LCD_FinishTransfer();
} }
} }
@@ -339,9 +342,7 @@ void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi)
{ {
if (hspi->Instance == SPI1) if (hspi->Instance == SPI1)
{ {
/* 记录并中止,避免把坏状态带入下一帧。收尾同样会触发完成回调, lcd_spi_err = hspi->ErrorCode;
* 让 LVGL 继续,不至于卡在 flushing。 */ lcd_xfer_done = 1; /* 唤醒等待循环,由任务侧决定如何恢复 */
HAL_SPI_Abort(hspi);
LCD_FinishTransfer();
} }
} }
+11 -9
View File
@@ -12,8 +12,8 @@ STM32H750 · SPI1 · DMA1_Stream2 · LVGL v8
| 旧版问题 | 根因 | 新版做法 | | 旧版问题 | 根因 | 新版做法 |
|---|---|---| |---|---|---|
| 移动方块留下残影/线条 | `disp_flush()` 里做软件 90° 转置,坐标/尺寸与 LVGL 失效区域不严格一致,且多一层 `rot_buf→dma_buf` 拷贝 | **硬件旋转**:面板 MADCTL 直接设横屏,LVGL 坐标 = 面板坐标,刷新区与失效区严格 1:1,转置整段删除 | | 移动方块留下残影/线条 | `disp_flush()` 里做软件 90° 转置,坐标/尺寸与 LVGL 失效区域不严格一致,且多一层 `rot_buf→dma_buf` 拷贝 | **硬件旋转**:面板 MADCTL 直接设横屏,LVGL 坐标 = 面板坐标,刷新区与失效区严格 1:1,转置整段删除 |
| 长时间运行白屏 | ① DMA 源缓冲可能被链接器放进 **DTCM**,而 DMA1 无法访问 DTCM;② 阻塞式信号量 + 出错 `Abort`,SPI 状态一乱就级联白屏 | ① 缓冲用 `.lcd_ram` 段**强制放入 AXI SRAM**;② 全异步 DMA,去掉信号量;出错回调 + 看门狗自恢复,单次异常不级联 | | 长时间运行白屏 | ① DMA 源缓冲可能被链接器放进 **DTCM**,而 DMA1 无法访问 DTCM;② 阻塞式信号量 + 出错 `Abort`,SPI 状态一乱就级联白屏 | ① 缓冲用 `.lcd_ram` 段**强制放入 AXI SRAM**;② 传输超时后重建 SPI 自恢复,单次异常不级联 |
| 刷新与 CPU 抢时间 | flush_cb 阻塞等待传输完成 | flush_cb 启动 DMA 后**立即返回**,双缓冲边渲染边传,完成中断里 `lv_disp_flush_ready()` | | 刷新时序/竞态 | 中断里 `lv_disp_flush_ready`、ISR 内 `Abort`、完成被收尾两次等竞态,会丢失“擦除”写入 → 移动物体残留 | **DMA 加速 + 任务内同步等待**:中断只置完成标志,等待/收尾/回调全在任务里做;flush 返回时该块像素一定已完整写入,杜绝提前复用缓冲/擦除未写完 |
| 字节序靠 CPU 逐像素交换 | `LV_COLOR_16_SWAP=0`,驱动里手工打包 | `LV_COLOR_16_SWAP=1`,LVGL 直接产出面板字节序,DMA 直接搬运 | | 字节序靠 CPU 逐像素交换 | `LV_COLOR_16_SWAP=0`,驱动里手工打包 | `LV_COLOR_16_SWAP=1`,LVGL 直接产出面板字节序,DMA 直接搬运 |
> 备注:旧 `ILI9341.h` 里关于 “CS=PB12 未配置为输出” 的警告是**过时的**—— > 备注:旧 `ILI9341.h` 里关于 “CS=PB12 未配置为输出” 的警告是**过时的**——
@@ -26,15 +26,17 @@ STM32H750 · SPI1 · DMA1_Stream2 · LVGL v8
``` ```
LVGL 渲染 → buf_1/buf_2 (.lcd_ram, AXI SRAM, 32B 对齐) LVGL 渲染 → buf_1/buf_2 (.lcd_ram, AXI SRAM, 32B 对齐)
→ disp_flush() : 裁剪坐标,直接调 ILI9341_FlushArea_DMA() → disp_flush() : 裁剪坐标,直接调 ILI9341_FlushArea_DMA()
→ FlushArea_DMA : 设窗口(2A/2B/2C) → clean D-Cache → CS 低 → 启动 SPI DMA,立即返回 → FlushArea_DMA : 设窗口(2A/2B/2C) → clean D-Cache → CS 低 → 启动 SPI DMA
→ [DMA 完成] SPI1 中断 → HAL_SPI_TxCpltCallback → CS 高 → lv_disp_flush_ready() → 任务内轮询等待 lcd_xfer_done1~2ms50ms 超时兜底)
→ [出错] HAL_SPI_ErrorCallback → Abort → 收尾 → flush_ready(不卡死) → 等 SPI 移位排空 → CS 高 → done_cb(=lv_disp_flush_ready)
→ [兜底] 任务里 ILI9341_ServiceWatchdog(180) 超时中止+重建 SPI → [DMA 完成] SPI1 中断 → HAL_SPI_TxCpltCallback → 仅置 lcd_xfer_done=1
→ [出错] HAL_SPI_ErrorCallback → 记录错误 + 置 lcd_xfer_done=1,任务侧恢复
→ [兜底] 超时(或 ServiceWatchdog) → Abort + 重建 SPI
``` ```
关键不变量:任意时刻**至多一次 DMA 在途**(LVGL 在 `flush_ready` 前不会再次 关键不变量:`flush_cb` **返回时该块像素一定已完整写入面板**LVGL 不会提前复用
调用 `flush_cb`)。收尾函数 `LCD_FinishTransfer()` 在关中断临界区内“取回调+复位 缓冲,“擦除”写入不会丢失。中断只做“置标志”一件事,不在中断里调用 LVGL、不在
状态”,保证 `done_cb` 只被调用一次,杜绝 ISR 与看门狗竞争 中断里 `Abort`,从根上消除竞态
--- ---