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