diff --git a/Core/Inc/lv_conf.h b/Core/Inc/lv_conf.h index dcb4c31..096dd17 100644 --- a/Core/Inc/lv_conf.h +++ b/Core/Inc/lv_conf.h @@ -13,7 +13,9 @@ *====================*/ #define LV_COLOR_DEPTH 16 -#define LV_COLOR_16_SWAP 0 +/* =1:LVGL 直接产出 ILI9341 需要的大端字节序,SPI(8bit)+DMA 可直接搬运, + * 无需在 flush 里逐像素交换/拷贝。改动此项必须整体重编 LVGL。 */ +#define LV_COLOR_16_SWAP 1 #define LV_COLOR_SCREEN_TRANSP 0 #define LV_COLOR_MIX_ROUND_OFS 0 #define LV_COLOR_CHROMA_KEY lv_color_hex(0x00ff00) diff --git a/Core/Src/freertos.c b/Core/Src/freertos.c index 19c8ed3..d8eb641 100644 --- a/Core/Src/freertos.c +++ b/Core/Src/freertos.c @@ -284,14 +284,12 @@ for(;;) lv_timer_handler(); /* - * DMA 看门狗:兜底极端情况下 DMA 传输既没有触发完成中断 - * 也没有触发错误中断(硬件卡死/中断丢失)时,强制恢复 SPI - * 状态机,避免画面永久冻结。正常情况下 ILI9341_WriteArea_DMA - * 内部的信号量超时已经能处理绝大多数异常,这里只是双保险。 - * timeout_ms 要大于 ILI9341_WriteArea_DMA 内部的 100ms 超时, - * 避免两层保护互相打架。 + * LCD 看门狗:新驱动是全异步的,正常刷新由 SPI 完成/错误中断 + * 自动收尾,不阻塞本任务。此处只兜底极端情况——一次 DMA 既没 + * 触发完成中断也没触发错误中断(硬件卡死/中断丢失)时,强制 + * 中止并重建 SPI,恢复刷新管线,避免长时间白屏/冻结。 */ - ILI9341_DMA_Watchdog(180); + ILI9341_ServiceWatchdog(180); osDelay(5); } diff --git a/Core/Src/lv_port_disp.c b/Core/Src/lv_port_disp.c index 9f9485d..ae6b320 100644 --- a/Core/Src/lv_port_disp.c +++ b/Core/Src/lv_port_disp.c @@ -1,9 +1,18 @@ /** * @file lv_port_disp.c * + * ILI9341 + LVGL 显示接驳(重写版) + * ------------------------------------------------------------------ + * - 硬件旋转:面板由 MADCTL 直接设为横屏 320x240,这里不再做任何 + * 软件转置,LVGL 坐标与面板坐标 1:1,移动物体不再留残影。 + * - 双绘制缓冲 + 异步 DMA:flush_cb 启动 DMA 后立即返回,LVGL 可 + * 在另一块缓冲上继续渲染;DMA 完成的中断回调里调用 + * lv_disp_flush_ready(),刷新与传输天然并行、时序自同步。 + * - 两块绘制缓冲用 LCD_RAM_ATTR 放入 .lcd_ram 段(AXI SRAM), + * 保证 DMA 可访问;配合 lv_conf.h 的 LV_COLOR_16_SWAP=1, + * LVGL 产出的字节序即面板发送序,DMA 直接搬运,无需拷贝。 */ -/*Copy this file as "lv_port_disp.c" and set this value to "1" to enable content*/ #if 1 /********************* @@ -17,95 +26,52 @@ * DEFINES *********************/ #ifndef MY_DISP_HOR_RES - #define MY_DISP_HOR_RES 320 /* LVGL 逻辑横屏宽度 */ + #define MY_DISP_HOR_RES 320 /* 横屏宽度,与 MADCTL 一致 */ #endif #ifndef MY_DISP_VER_RES - #define MY_DISP_VER_RES 240 /* LVGL 逻辑横屏高度 */ + #define MY_DISP_VER_RES 240 /* 横屏高度 */ #endif -/* 面板原生(未旋转)物理分辨率,用于软件转置计算 */ -#define PANEL_NATIVE_W 240 -#define PANEL_NATIVE_H 320 - /* - * LVGL 局部刷新缓冲区行数。 - * MY_DISP_HOR_RES * DISP_BUF_LINES * 2 字节 = 单次 flush 的数据量, - * 越小单次 SPI/DMA 传输耗时越短,越不容易和面板固定的内部扫描 - * 周期(约 14~16ms 一帧)撞在一起造成撕裂。8 行是当前验证下来 - * 比较均衡的取值(320*8*2 = 5120 字节,约 4ms 传输时间)。 + * 绘制缓冲行数。单块 = MY_DISP_HOR_RES * DISP_BUF_LINES 像素。 + * 20 行:320*20*2 = 12800 字节/块,两块共 25600 字节,均位于 AXI SRAM。 + * DMA 单次字节数(12800) < DMA NDTR 上限(65535),安全。 */ -#define DISP_BUF_LINES 8 - -/* - * 为什么用单缓冲(Example 1)而不是模板里的双缓冲(Example 2): - * 双缓冲的价值在于 flush_cb 立刻返回、LVGL 边渲染下一块边等待 - * DMA 传输完成。但当前 ILI9341_WriteArea_DMA() 内部是"发起 DMA - * 后用信号量阻塞等待传完再返回"(详见 ILI9341.c 里的实现和之前 - * 的讨论),flush_cb 本身就是同步返回的,双缓冲不会带来任何并行 - * 收益,只会多占一份 RAM。如果以后把 ILI9341_WriteArea_DMA 改成 - * 真正的"发起后立即返回、由中断回调里调用 lv_disp_flush_ready", - * 再切回模板的双缓冲写法(Example 2)才有意义。 - */ - -/********************** - * TYPEDEFS - **********************/ +#define DISP_BUF_LINES 20 /********************** * STATIC PROTOTYPES **********************/ static void disp_init(void); - static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p); - static void disp_flush_done_cb(void * user_data); /********************** * STATIC VARIABLES **********************/ - -/* 90 度旋转后的临时缓冲:把 landscape 数据转置成面板原生 portrait 排列。 - * 大小和绘制缓冲一致,装得下一整条 flush 的像素。 */ -static uint16_t rot_buf[MY_DISP_HOR_RES * DISP_BUF_LINES]; - -/********************** - * MACROS - **********************/ +/* 双绘制缓冲,必须放在 DMA 可访问内存(.lcd_ram → AXI SRAM) */ +static LCD_RAM_ATTR lv_color_t buf_1[MY_DISP_HOR_RES * DISP_BUF_LINES]; +static LCD_RAM_ATTR lv_color_t buf_2[MY_DISP_HOR_RES * DISP_BUF_LINES]; /********************** * GLOBAL FUNCTIONS **********************/ - void lv_port_disp_init(void) { - /*------------------------- - * Initialize your display - * -----------------------*/ disp_init(); - /*----------------------------- - * Create a buffer for drawing - *----------------------------*/ - - /* Example for 1) 单缓冲:原因见上面 DISP_BUF_LINES 注释 */ - static lv_disp_draw_buf_t draw_buf_dsc_1; - static lv_color_t buf_1[MY_DISP_HOR_RES * DISP_BUF_LINES]; - lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * DISP_BUF_LINES); - - /*----------------------------------- - * Register the display in LVGL - *----------------------------------*/ + static lv_disp_draw_buf_t draw_buf_dsc; + lv_disp_draw_buf_init(&draw_buf_dsc, buf_1, buf_2, + MY_DISP_HOR_RES * DISP_BUF_LINES); static lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); - disp_drv.hor_res = MY_DISP_HOR_RES; - disp_drv.ver_res = MY_DISP_VER_RES; - + disp_drv.hor_res = MY_DISP_HOR_RES; + disp_drv.ver_res = MY_DISP_VER_RES; disp_drv.flush_cb = disp_flush; - - disp_drv.draw_buf = &draw_buf_dsc_1; + disp_drv.draw_buf = &draw_buf_dsc; lv_disp_drv_register(&disp_drv); } @@ -113,50 +79,19 @@ void lv_port_disp_init(void) /********************** * STATIC FUNCTIONS **********************/ - -/*Initialize your display and the required peripherals.*/ static void disp_init(void) { - /* - * 把 ILI9341 的硬件初始化收拢到这里(原来分散在 freertos.c 里)。 - * 注意:这里只调用 ILI9341_Init(),它内部末尾会自动执行 - * SetRotation(0)(面板原生方向)。不要再调用 - * ILI9341_SetRotation(1),横屏旋转已经改成在 disp_flush() - * 里用软件转置实现,如果再叠加 MADCTL 的 MV 位会导致画面 - * 完全错乱。 - * - * 对应地,freertos.c 里原来的: - * ILI9341_Init(); - * ILI9341_SetRotation(1); - * ILI9341_FillScreen(ILI9341_BLACK); - * 这三行请删掉,改成只调用: - * lv_init(); - * lv_port_disp_init(); - * (init/rotation/清屏都移到这里统一做) - */ + /* 硬件初始化 + 清屏都收拢在这里;ILI9341_Init() 末尾已设为横屏, + * 不要在别处再调用 ILI9341_SetRotation()。 */ ILI9341_Init(); ILI9341_FillScreen(ILI9341_BLACK); } volatile bool disp_flush_enabled = true; -/* Enable updating the screen (the flushing process) when disp_flush() is called by LVGL - */ -void disp_enable_update(void) -{ - disp_flush_enabled = true; -} +void disp_enable_update(void) { disp_flush_enabled = true; } +void disp_disable_update(void) { disp_flush_enabled = false; } -/* Disable updating the screen (the flushing process) when disp_flush() is called by LVGL - */ -void disp_disable_update(void) -{ - disp_flush_enabled = false; -} - -/*Flush the content of the internal buffer the specific area on the display - *You can use DMA or any hardware acceleration to do this operation in the background but - *'lv_disp_flush_ready()' has to be called when finished.*/ static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { if (!disp_flush_enabled) @@ -165,11 +100,10 @@ static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_colo return; } - int32_t x1 = area->x1; - int32_t y1 = area->y1; - int32_t x2 = area->x2; - int32_t y2 = area->y2; + int32_t x1 = area->x1, y1 = area->y1; + int32_t x2 = area->x2, y2 = area->y2; + /* 面板外的区域直接跳过 */ if (x2 < 0 || y2 < 0 || x1 >= MY_DISP_HOR_RES || y1 >= MY_DISP_VER_RES) { lv_disp_flush_ready(disp_drv); @@ -181,56 +115,21 @@ static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_colo if (x2 >= MY_DISP_HOR_RES) x2 = MY_DISP_HOR_RES - 1; if (y2 >= MY_DISP_VER_RES) y2 = MY_DISP_VER_RES - 1; - uint16_t lx0 = (uint16_t)x1; - uint16_t ly0 = (uint16_t)y1; - uint16_t lw = (uint16_t)(x2 - x1 + 1); - uint16_t lh = (uint16_t)(y2 - y1 + 1); - - /* - * 90 度顺时针旋转映射: - * 物理列 xp = (PANEL_NATIVE_W - 1) - 逻辑行 ly - * 物理行 yp = 逻辑列 lx - * - * !!! 如果烧录后发现镜像/上下颠倒/转向错误,只需要调整这 - * 几行的正负号,不用改架构。备选公式: - * a) xp = ly; yp = (MY_DISP_HOR_RES-1) - lx; - * b) xp = (PANEL_NATIVE_W-1) - ly; yp = (MY_DISP_HOR_RES-1) - lx; - * c) xp = ly; yp = lx; - */ - uint16_t px0 = (uint16_t)(PANEL_NATIVE_W - ly0 - lh); - uint16_t py0 = lx0; - uint16_t pw = lh; - uint16_t ph = lw; - - for (uint16_t ly = 0; ly < lh; ly++) - { - for (uint16_t lx = 0; lx < lw; lx++) - { - uint16_t rel_xp = (uint16_t)(lh - 1U - ly); - uint16_t rel_yp = lx; - - rot_buf[(uint32_t)rel_yp * pw + rel_xp] = - (uint16_t)color_p[(uint32_t)ly * lw + lx].full; - } - } - - ILI9341_WriteArea_DMA(px0, py0, pw, ph, - rot_buf, - disp_flush_done_cb, - disp_drv); + /* 硬件旋转:LVGL 坐标即面板坐标,直接把这块像素 DMA 出去。 + * LV_COLOR_16_SWAP=1 保证 color_p 的字节序即面板发送序。 */ + ILI9341_FlushArea_DMA((uint16_t)x1, (uint16_t)y1, + (uint16_t)(x2 - x1 + 1), (uint16_t)(y2 - y1 + 1), + (const uint16_t *)color_p, + disp_flush_done_cb, disp_drv); } -/* - * ILI9341_WriteArea_DMA 完成后的回调:通知 LVGL 这一块已经刷新完。 - */ +/* DMA 传输完成回调(SPI 中断上下文):通知 LVGL 这一块刷新完成。 + * lv_disp_flush_ready() 只是清标志,可安全从中断调用。 */ static void disp_flush_done_cb(void * user_data) { - lv_disp_drv_t * disp_drv = (lv_disp_drv_t *)user_data; - lv_disp_flush_ready(disp_drv); + lv_disp_flush_ready((lv_disp_drv_t *)user_data); } -#else /*Enable this file at the top*/ - -/*This dummy typedef exists purely to silence -Wpedantic.*/ +#else typedef int keep_pedantic_happy; -#endif \ No newline at end of file +#endif diff --git a/MDK-ARM/motor_ili9341.uvprojx b/MDK-ARM/motor_ili9341.uvprojx index 360120c..1b42618 100644 --- a/MDK-ARM/motor_ili9341.uvprojx +++ b/MDK-ARM/motor_ili9341.uvprojx @@ -362,7 +362,7 @@ - 1 + 0 0 0 0 @@ -371,7 +371,7 @@ - + .\motor_ili9341_lcd.sct diff --git a/MDK-ARM/motor_ili9341_lcd.sct b/MDK-ARM/motor_ili9341_lcd.sct new file mode 100644 index 0000000..9d4a9c7 --- /dev/null +++ b/MDK-ARM/motor_ili9341_lcd.sct @@ -0,0 +1,34 @@ +; ************************************************************* +; *** ILI9341 定制 Scatter 文件 *** +; ------------------------------------------------------------- +; 目的:把 LCD 的 DMA 源缓冲(.lcd_ram 段)强制放到 AXI SRAM +; (0x24000000)。原因: +; - DTCM(0x20000000) CPU 访问最快,但 DMA1/DMA2 无法访问, +; 把 DMA 缓冲放这里会导致 DMA 读到空/错数据 → 白屏/花屏。 +; - AXI SRAM(0x24000000) DMA1 可访问,适合放 LCD 缓冲。 +; 默认自动 scatter 会把缓冲随机分到两块 RAM,是之前偶发 +; 白屏的根因之一。本文件消除这种不确定性。 +; +; 若在 Keil 里修改了内存布局,请保持 .lcd_ram 始终落在 AXI SRAM。 +; ************************************************************* + +LR_IROM1 0x08000000 0x00020000 { ; 加载域 = Flash 128KB + ER_IROM1 0x08000000 0x00020000 { ; 执行域 = 加载域 + *.o (RESET, +First) + *(InRoot$$Sections) + .ANY (+RO) + .ANY (+XO) + } + + ; DTCM 128KB —— CPU 专用高速 RAM,DMA 不可访问。 + ; 普通全局变量、堆栈放这里。切勿放 DMA 缓冲。 + RW_IRAM1 0x20000000 0x00020000 { + .ANY (+RW +ZI) + } + + ; AXI SRAM 512KB —— DMA 可访问。LCD DMA 缓冲必须放这里。 + RW_IRAM2 0x24000000 0x00080000 { + *(.lcd_ram) ; LCD 绘制/DMA 缓冲,DMA 可达 + .ANY (+RW +ZI) + } +} diff --git a/SystemDrivers/ILI9341.c b/SystemDrivers/ILI9341.c index 4c2ce59..988312b 100644 --- a/SystemDrivers/ILI9341.c +++ b/SystemDrivers/ILI9341.c @@ -1,418 +1,189 @@ #include "ili9341.h" -#include "cmsis_os.h" -#include "uart4.h" -#include -#include /* - * ILI9341 TFT LCD 驱动 - * STM32H750 + SPI1 + DMA - * - * 本版本相对上一版的主要改动: - * 1. DMA buffer 不再用裸指针指向硬编码地址,改用链接器 section, - * 避免和其他默认分配(heap/栈/其他静态变量)物理重叠。 - * —— 需要在 .ld 文件里配合添加一个 section,见文件末尾说明。 - * 2. DMA 完成/错误的等待方式从"HAL_GetTick() 忙等"改成 - * "RTOS 信号量 + 超时",真正让出 CPU,而不是空转。 - * 3. ISR 回调只做最少的事(释放信号量/记录错误码),所有 - * 恢复逻辑、日志打印都放在任务上下文里做,符合 - * "中断里越短越好"的原则。 - * 4. 增加了 HAL_SPI_ErrorCallback,之前只处理了 TxCplt, - * 一旦发生 SPI 总线错误(比如 CRC/Overrun/ModeFault), - * 原来的代码只能傻等 100ms 超时,现在能立刻被唤醒并记录。 - * 5. 调试日志走 Print_DMA(非阻塞 UART DMA 发送),不占用 - * 本来就紧张的 SPI/DMA 时间片。 + * ILI9341 驱动 —— 全异步 DMA 重写版 + * ------------------------------------------------------------------ + * 相比旧版的根本性改动: + * [方向] 旧版在 disp_flush() 里做软件转置 + MADCTL 原生方向, + * 任何坐标/尺寸的细微不一致都会在移动物体处留下残影。 + * 新版用 MADCTL 直接把面板设为横屏,LVGL 坐标 == 面板坐标, + * 刷新区域与 LVGL 的失效区域严格 1:1,残留问题消失。 + * [同步] 旧版发起 DMA 后用信号量阻塞等待,出错就 Abort,一旦 + * SPI 状态机被打乱就会级联白屏。新版 FlushArea_DMA 启动 + * DMA 后立即返回,完成/出错都由中断回调收尾,无信号量、 + * 无阻塞、时序自同步。 + * [内存] DMA 源缓冲一律位于 AXI SRAM(.lcd_ram 段),DMA1 可访问; + * 发送前做 D-Cache clean,保证 DMA 读到的是最新数据。 + * [恢复] 出错回调 + 看门狗都会把 CS 拉高、状态复位、并触发完成 + * 回调,任何单次异常都不会让刷新管线永久停摆。 */ extern SPI_HandleTypeDef hspi1; +/* ================= GPIO 控制 ================= */ +#define LCD_CS_LOW() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET) +#define LCD_CS_HIGH() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_SET) +#define LCD_DC_CMD() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_RESET) +#define LCD_DC_DATA() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_SET) +#define LCD_RST_LOW() HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_RESET) +#define LCD_RST_HIGH() HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_SET) +#define LCD_BL_ON() HAL_GPIO_WritePin(LCD_BL_PORT, LCD_BL_PIN, GPIO_PIN_SET) +#define LCD_BL_OFF() HAL_GPIO_WritePin(LCD_BL_PORT, LCD_BL_PIN, GPIO_PIN_RESET) + static uint16_t lcd_width = ILI9341_WIDTH; static uint16_t lcd_height = ILI9341_HEIGHT; -/* LCD GPIO 控制 */ -#define LCD_CS_LOW() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET) -#define LCD_CS_HIGH() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_SET) +/* ================= 异步刷新状态(全部由 volatile 保护) ================= */ +typedef enum { LCD_IDLE = 0, LCD_BUSY = 1 } lcd_state_t; -#define LCD_DC_CMD() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_RESET) -#define LCD_DC_DATA() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_SET) +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; -#define LCD_RST_LOW() HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_RESET) -#define LCD_RST_HIGH() HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_SET) +/* ================= 阻塞式低层写命令/数据(仅用于初始化与设窗口) ================= */ -#define LCD_BL_ON() HAL_GPIO_WritePin(LCD_BL_PORT, LCD_BL_PIN, GPIO_PIN_SET) -#define LCD_BL_OFF() HAL_GPIO_WritePin(LCD_BL_PORT, LCD_BL_PIN, GPIO_PIN_RESET) - -/* - * LVGL buffer 使用 20 行: - * 320 * 20 * 2 = 12800 bytes - */ -#define LCD_DMA_MAX_PIXELS (320U * 20U) -#define LCD_DMA_BUF_SIZE (LCD_DMA_MAX_PIXELS * 2U) - -/* - * DMA buffer 放到链接器专属 section 里,而不是裸写绝对地址。 - * 需要在 .ld 文件的 MEMORY 区里确认 D2 SRAM(0x30000000 起) - * 已经声明为一个可用 region(很多 CubeIDE 模板里叫 RAM_D2), - * 并在 SECTIONS 里加: - * - * .dma_buffer (NOLOAD) : - * { - * . = ALIGN(32); - * *(.dma_buffer) - * . = ALIGN(32); - * } >RAM_D2 - * - * 这样链接器会知道这块内存已经被占用,不会再把 heap/栈/其他 - * 全局变量分配到这里,从根本上排除"覆盖冲突"的可能性。 - * 32 字节对齐是为了配合 D-Cache clean-by-address 的对齐要求。 - */ -#if defined(__GNUC__) -#define LCD_DMA_BUF_ATTR __attribute__((section(".dma_buffer"), aligned(32))) -#else -#define LCD_DMA_BUF_ATTR -#warning "非 GNU 工具链,请自行确认 lcd_dma_buf 被放置在 DMA 可访问、且不会被其他数据覆盖的内存区域" -#endif - -LCD_DMA_BUF_ATTR -static uint8_t lcd_dma_buf[LCD_DMA_BUF_SIZE]; - -/* ---- 状态与同步 ---- */ -static volatile uint8_t lcd_dma_busy = 0; -static volatile uint32_t lcd_dma_start_tick = 0; -static volatile uint32_t lcd_spi_last_error = 0; - -static osSemaphoreId_t lcd_dma_sem = NULL; - -/* ---- 非阻塞调试日志:走 Print_DMA,忙的时候直接丢弃这条日志 ---- */ -static char lcd_dbg_line[96]; - -static void LCD_DebugLogf(const char *fmt, ...) -{ - if (UART_IsBusy()) - { - return; /* 调试串口正忙,宁可丢日志也不要阻塞关键路径 */ - } - - va_list args; - va_start(args, fmt); - vsnprintf(lcd_dbg_line, sizeof(lcd_dbg_line), fmt, args); - va_end(args); - - Print_DMA(lcd_dbg_line); -} - -static void LCD_DMA_EnsureSemaphore(void) -{ - if (lcd_dma_sem == NULL) - { - lcd_dma_sem = osSemaphoreNew(1, 0, NULL); - } -} - -/* ========================= 基础 SPI 写命令/数据(阻塞,保持不变) ========================= */ - -static void ILI9341_WriteCommand(uint8_t cmd) +static void LCD_WriteCmd(uint8_t cmd) { LCD_CS_LOW(); LCD_DC_CMD(); - HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); - LCD_CS_HIGH(); } -static void ILI9341_WriteData(uint8_t *data, uint16_t size) +static void LCD_WriteData(const uint8_t *data, uint16_t size) { LCD_CS_LOW(); LCD_DC_DATA(); - - HAL_SPI_Transmit(&hspi1, data, size, HAL_MAX_DELAY); - + HAL_SPI_Transmit(&hspi1, (uint8_t *)data, size, HAL_MAX_DELAY); LCD_CS_HIGH(); } -static void ILI9341_WriteData8(uint8_t data) +static void LCD_WriteData8(uint8_t data) { - LCD_CS_LOW(); - LCD_DC_DATA(); - - HAL_SPI_Transmit(&hspi1, &data, 1, HAL_MAX_DELAY); - - LCD_CS_HIGH(); + LCD_WriteData(&data, 1); } -static void ILI9341_WriteData16(uint16_t data) +/* 设定地址窗口 [x0..x1] x [y0..y1],并进入 RAM 写(0x2C)。 + * 结束后 CS 处于高电平,DC 为命令态。 */ +static void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { - uint8_t buf[2]; + uint8_t buf[4]; - buf[0] = (uint8_t)(data >> 8); - buf[1] = (uint8_t)(data & 0xFF); + LCD_WriteCmd(0x2A); + buf[0] = (uint8_t)(x0 >> 8); buf[1] = (uint8_t)(x0 & 0xFF); + buf[2] = (uint8_t)(x1 >> 8); buf[3] = (uint8_t)(x1 & 0xFF); + LCD_WriteData(buf, 4); - LCD_CS_LOW(); - LCD_DC_DATA(); + LCD_WriteCmd(0x2B); + buf[0] = (uint8_t)(y0 >> 8); buf[1] = (uint8_t)(y0 & 0xFF); + buf[2] = (uint8_t)(y1 >> 8); buf[3] = (uint8_t)(y1 & 0xFF); + LCD_WriteData(buf, 4); - HAL_SPI_Transmit(&hspi1, buf, 2, HAL_MAX_DELAY); - - LCD_CS_HIGH(); + LCD_WriteCmd(0x2C); } -static void ILI9341_Reset(void) +static void LCD_Reset(void) { - LCD_RST_HIGH(); - HAL_Delay(20); - - LCD_RST_LOW(); - HAL_Delay(20); - - LCD_RST_HIGH(); - HAL_Delay(120); + LCD_RST_HIGH(); HAL_Delay(20); + LCD_RST_LOW(); HAL_Delay(20); + LCD_RST_HIGH(); HAL_Delay(120); } -static void ILI9341_SetAddressWindow(uint16_t x0, - uint16_t y0, - uint16_t x1, - uint16_t y1) -{ - uint8_t data[4]; - - ILI9341_WriteCommand(0x2A); - data[0] = (uint8_t)(x0 >> 8); - data[1] = (uint8_t)(x0 & 0xFF); - data[2] = (uint8_t)(x1 >> 8); - data[3] = (uint8_t)(x1 & 0xFF); - ILI9341_WriteData(data, 4); - - ILI9341_WriteCommand(0x2B); - data[0] = (uint8_t)(y0 >> 8); - data[1] = (uint8_t)(y0 & 0xFF); - data[2] = (uint8_t)(y1 >> 8); - data[3] = (uint8_t)(y1 & 0xFF); - ILI9341_WriteData(data, 4); - - ILI9341_WriteCommand(0x2C); -} - -/* ========================= 初始化 ========================= */ +/* ================= 初始化 ================= */ void ILI9341_Init(void) { LCD_CS_HIGH(); LCD_BL_ON(); - LCD_DMA_EnsureSemaphore(); + LCD_Reset(); - ILI9341_Reset(); - - ILI9341_WriteCommand(0x01); + LCD_WriteCmd(0x01); /* Software reset */ HAL_Delay(120); - ILI9341_WriteCommand(0xEF); - { - uint8_t data[] = {0x03, 0x80, 0x02}; - ILI9341_WriteData(data, sizeof(data)); - } + /* 厂商推荐上电时序 */ + LCD_WriteCmd(0xEF); { const uint8_t d[] = {0x03,0x80,0x02}; LCD_WriteData(d,sizeof d); } + LCD_WriteCmd(0xCF); { const uint8_t d[] = {0x00,0xC1,0x30}; LCD_WriteData(d,sizeof d); } + LCD_WriteCmd(0xED); { const uint8_t d[] = {0x64,0x03,0x12,0x81}; LCD_WriteData(d,sizeof d); } + LCD_WriteCmd(0xE8); { const uint8_t d[] = {0x85,0x00,0x78}; LCD_WriteData(d,sizeof d); } + LCD_WriteCmd(0xCB); { const uint8_t d[] = {0x39,0x2C,0x00,0x34,0x02}; LCD_WriteData(d,sizeof d); } + LCD_WriteCmd(0xF7); LCD_WriteData8(0x20); + LCD_WriteCmd(0xEA); { const uint8_t d[] = {0x00,0x00}; LCD_WriteData(d,sizeof d); } + LCD_WriteCmd(0xC0); LCD_WriteData8(0x23); /* Power control 1 */ + LCD_WriteCmd(0xC1); LCD_WriteData8(0x10); /* Power control 2 */ + LCD_WriteCmd(0xC5); { const uint8_t d[] = {0x3E,0x28}; LCD_WriteData(d,sizeof d); } + LCD_WriteCmd(0xC7); LCD_WriteData8(0x86); /* VCOM control 2 */ + LCD_WriteCmd(0x3A); LCD_WriteData8(0x55); /* Pixel format = 16bit/pixel */ + LCD_WriteCmd(0xB1); { const uint8_t d[] = {0x00,0x18}; LCD_WriteData(d,sizeof d); } + LCD_WriteCmd(0xB6); { const uint8_t d[] = {0x08,0x82,0x27}; LCD_WriteData(d,sizeof d); } + LCD_WriteCmd(0xF2); LCD_WriteData8(0x00); + LCD_WriteCmd(0x26); LCD_WriteData8(0x01); /* Gamma curve */ + LCD_WriteCmd(0xE0); { const uint8_t d[] = {0x0F,0x31,0x2B,0x0C,0x0E,0x08,0x4E,0xF1,0x37,0x07,0x10,0x03,0x0E,0x09,0x00}; LCD_WriteData(d,sizeof d); } + LCD_WriteCmd(0xE1); { const uint8_t d[] = {0x00,0x0E,0x14,0x03,0x11,0x07,0x31,0xC1,0x48,0x08,0x0F,0x0C,0x31,0x36,0x0F}; LCD_WriteData(d,sizeof d); } - ILI9341_WriteCommand(0xCF); - { - uint8_t data[] = {0x00, 0xC1, 0x30}; - ILI9341_WriteData(data, sizeof(data)); - } - - ILI9341_WriteCommand(0xED); - { - uint8_t data[] = {0x64, 0x03, 0x12, 0x81}; - ILI9341_WriteData(data, sizeof(data)); - } - - ILI9341_WriteCommand(0xE8); - { - uint8_t data[] = {0x85, 0x00, 0x78}; - ILI9341_WriteData(data, sizeof(data)); - } - - ILI9341_WriteCommand(0xCB); - { - uint8_t data[] = {0x39, 0x2C, 0x00, 0x34, 0x02}; - ILI9341_WriteData(data, sizeof(data)); - } - - ILI9341_WriteCommand(0xF7); - ILI9341_WriteData8(0x20); - - ILI9341_WriteCommand(0xEA); - { - uint8_t data[] = {0x00, 0x00}; - ILI9341_WriteData(data, sizeof(data)); - } - - ILI9341_WriteCommand(0xC0); - ILI9341_WriteData8(0x23); - - ILI9341_WriteCommand(0xC1); - ILI9341_WriteData8(0x10); - - ILI9341_WriteCommand(0xC5); - { - uint8_t data[] = {0x3E, 0x28}; - ILI9341_WriteData(data, sizeof(data)); - } - - ILI9341_WriteCommand(0xC7); - ILI9341_WriteData8(0x86); - - ILI9341_WriteCommand(0x36); - ILI9341_WriteData8(0x48); - - ILI9341_WriteCommand(0x3A); - ILI9341_WriteData8(0x55); - - ILI9341_WriteCommand(0xB1); - { - uint8_t data[] = {0x00, 0x18}; - ILI9341_WriteData(data, sizeof(data)); - } - - ILI9341_WriteCommand(0xB6); - { - uint8_t data[] = {0x08, 0x82, 0x27}; - ILI9341_WriteData(data, sizeof(data)); - } - - ILI9341_WriteCommand(0xF2); - ILI9341_WriteData8(0x00); - - ILI9341_WriteCommand(0x26); - ILI9341_WriteData8(0x01); - - ILI9341_WriteCommand(0xE0); - { - uint8_t data[] = { - 0x0F, 0x31, 0x2B, 0x0C, 0x0E, - 0x08, 0x4E, 0xF1, 0x37, 0x07, - 0x10, 0x03, 0x0E, 0x09, 0x00 - }; - ILI9341_WriteData(data, sizeof(data)); - } - - ILI9341_WriteCommand(0xE1); - { - uint8_t data[] = { - 0x00, 0x0E, 0x14, 0x03, 0x11, - 0x07, 0x31, 0xC1, 0x48, 0x08, - 0x0F, 0x0C, 0x31, 0x36, 0x0F - }; - ILI9341_WriteData(data, sizeof(data)); - } - - ILI9341_WriteCommand(0x11); + LCD_WriteCmd(0x11); /* Sleep out */ HAL_Delay(120); - - ILI9341_WriteCommand(0x29); + LCD_WriteCmd(0x29); /* Display on */ HAL_Delay(20); - ILI9341_SetRotation(0); + ILI9341_SetRotation(1); /* 默认横屏 320x240,硬件旋转 */ } -/* ========================= 方向设置 ========================= */ - +/* ================= 方向(硬件 MADCTL 旋转) ================= + * 若烧录后发现镜像/上下颠倒,只需换用同一行注释里给出的备选 MADCTL: + * 横屏可选 0x28 / 0xE8,竖屏可选 0x48 / 0x88。 + */ void ILI9341_SetRotation(uint8_t rotation) { uint8_t madctl; - rotation %= 4; - - switch (rotation) + switch (rotation & 0x03) { - case 0: - madctl = 0x48; - lcd_width = 240; - lcd_height = 320; - break; - - case 1: - madctl = 0x28; - lcd_width = 320; - lcd_height = 240; - break; - - case 2: - madctl = 0x88; - lcd_width = 240; - lcd_height = 320; - break; - - default: - madctl = 0xE8; - lcd_width = 320; - lcd_height = 240; - break; + case 0: madctl = 0x48; lcd_width = 240; lcd_height = 320; break; /* 竖屏 */ + case 1: madctl = 0x28; lcd_width = 320; lcd_height = 240; break; /* 横屏(默认) */ + case 2: madctl = 0x88; lcd_width = 240; lcd_height = 320; break; /* 竖屏翻转 */ + default: madctl = 0xE8; lcd_width = 320; lcd_height = 240; break; /* 横屏翻转 */ } - ILI9341_WriteCommand(0x36); - ILI9341_WriteData8(madctl); + LCD_WriteCmd(0x36); + LCD_WriteData8(madctl); } -/* ========================= 基础绘图(阻塞,保持不变) ========================= */ +/* ================= 阻塞式基础绘图(初始化清屏用) ================= */ -void ILI9341_DrawPixel(uint16_t x, uint16_t y, uint16_t color) +uint16_t ILI9341_Color565(uint8_t r, uint8_t g, uint8_t b) { - if (x >= lcd_width || y >= lcd_height) - { - return; - } - - ILI9341_SetAddressWindow(x, y, x, y); - ILI9341_WriteData16(color); + return (uint16_t)(((r & 0xF8U) << 8) | ((g & 0xFCU) << 3) | (b >> 3)); } -void ILI9341_FillRect(uint16_t x, - uint16_t y, - uint16_t w, - uint16_t h, - uint16_t color) +void ILI9341_FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { + uint8_t line[64 * 2]; /* 一次最多推 64 像素 */ uint32_t pixels; - uint8_t buffer[128]; - uint16_t chunk_pixels; - if (x >= lcd_width || y >= lcd_height) + if (x >= lcd_width || y >= lcd_height) return; + if ((uint32_t)x + w > lcd_width) w = lcd_width - x; + if ((uint32_t)y + h > lcd_height) h = lcd_height - y; + if (w == 0 || h == 0) return; + + for (uint16_t i = 0; i < sizeof(line); i += 2) { - return; + line[i] = (uint8_t)(color >> 8); + line[i + 1] = (uint8_t)(color & 0xFF); } - if ((x + w) > lcd_width) - { - w = lcd_width - x; - } - - if ((y + h) > lcd_height) - { - h = lcd_height - y; - } - - pixels = (uint32_t)w * h; - - for (uint16_t i = 0; i < sizeof(buffer); i += 2) - { - buffer[i] = (uint8_t)(color >> 8); - buffer[i + 1] = (uint8_t)(color & 0xFF); - } - - ILI9341_SetAddressWindow(x, y, x + w - 1, y + h - 1); + LCD_SetWindow(x, y, x + w - 1, y + h - 1); LCD_CS_LOW(); LCD_DC_DATA(); - while (pixels > 0) + pixels = (uint32_t)w * h; + while (pixels) { - chunk_pixels = pixels > 64U ? 64U : (uint16_t)pixels; - - HAL_SPI_Transmit(&hspi1, - buffer, - chunk_pixels * 2U, - HAL_MAX_DELAY); - - pixels -= chunk_pixels; + uint16_t chunk = pixels > 64U ? 64U : (uint16_t)pixels; + HAL_SPI_Transmit(&hspi1, line, (uint16_t)(chunk * 2U), HAL_MAX_DELAY); + pixels -= chunk; } LCD_CS_HIGH(); @@ -423,293 +194,154 @@ void ILI9341_FillScreen(uint16_t color) ILI9341_FillRect(0, 0, lcd_width, lcd_height, color); } -uint16_t ILI9341_Color565(uint8_t r, uint8_t g, uint8_t b) +/* ================= D-Cache 维护 ================= */ + +static void LCD_CleanDCache(const void *addr, uint32_t size) { - return ((r & 0xF8U) << 8) | - ((g & 0xFCU) << 3) | - (b >> 3); -} - -/* ========================= LVGL 阻塞区域刷新(保持不变) ========================= */ - -void ILI9341_WriteArea(uint16_t x, - uint16_t y, - uint16_t w, - uint16_t h, - const uint16_t *pixels) -{ - uint32_t total_pixels; - uint32_t sent_pixels = 0; - - static uint8_t tx_buf[512]; - - if (w == 0 || h == 0 || pixels == NULL) - { - return; - } - - total_pixels = (uint32_t)w * h; - - ILI9341_SetAddressWindow(x, y, x + w - 1, y + h - 1); - - LCD_CS_LOW(); - LCD_DC_DATA(); - - while (sent_pixels < total_pixels) - { - uint32_t remain = total_pixels - sent_pixels; - uint32_t chunk_pixels = remain > 256U ? 256U : remain; - - for (uint32_t i = 0; i < chunk_pixels; i++) - { - uint16_t color = pixels[sent_pixels + i]; - - tx_buf[i * 2U] = (uint8_t)(color >> 8); - tx_buf[i * 2U + 1] = (uint8_t)(color & 0xFF); - } - - HAL_SPI_Transmit(&hspi1, - tx_buf, - chunk_pixels * 2U, - HAL_MAX_DELAY); - - sent_pixels += chunk_pixels; - } - - LCD_CS_HIGH(); -} - -/* ========================= LVGL DMA 区域刷新:重写版 ========================= */ - -static void LCD_CleanDCache(void *addr, uint32_t size) -{ -#if (__DCACHE_PRESENT == 1U) +#if defined(__DCACHE_PRESENT) && (__DCACHE_PRESENT == 1U) if ((SCB->CCR & SCB_CCR_DC_Msk) != 0U) { - uint32_t start_addr = (uint32_t)addr & ~31U; - uint32_t end_addr = ((uint32_t)addr + size + 31U) & ~31U; - - SCB_CleanDCache_by_Addr((uint32_t *)start_addr, end_addr - start_addr); + uint32_t start = (uint32_t)addr & ~31U; + uint32_t end = ((uint32_t)addr + size + 31U) & ~31U; + SCB_CleanDCache_by_Addr((uint32_t *)start, (int32_t)(end - start)); } #else - (void)addr; - (void)size; + (void)addr; (void)size; #endif } -static void ILI9341_SPI_WaitIdle(void) +/* ================= 异步刷新完成收尾(供 ISR / 看门狗调用) ================= */ + +static void LCD_FinishTransfer(void) { - uint32_t tick_start; + ILI9341_FlushDoneCb cb; + void *arg; -#if defined(SPI_FLAG_TXC) - tick_start = HAL_GetTick(); + /* 关中断内做“取出并清空回调 + 复位状态”,保证只收尾一次, + * 杜绝 ISR 与看门狗竞争导致 done_cb 被调用两次。 */ + uint32_t primask = __get_PRIMASK(); + __disable_irq(); - while (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_TXC) == RESET) + if (lcd_state != LCD_BUSY) { - if ((HAL_GetTick() - tick_start) > 20U) - { - break; - } + __set_PRIMASK(primask); + return; } -#elif defined(SPI_FLAG_EOT) - tick_start = HAL_GetTick(); - while (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_EOT) == RESET) - { - if ((HAL_GetTick() - tick_start) > 20U) - { - break; - } - } -#else - for (volatile uint32_t i = 0; i < 1000U; i++) - { - __NOP(); - } -#endif + LCD_CS_HIGH(); + lcd_state = LCD_IDLE; + cb = lcd_done_cb; + arg = lcd_done_arg; + lcd_done_cb = NULL; - __DSB(); + __set_PRIMASK(primask); + + if (cb != NULL) + { + cb(arg); + } } -void ILI9341_WriteArea_DMA(uint16_t x, - uint16_t y, - uint16_t w, - uint16_t h, - const uint16_t *pixels, - ILI9341_DmaDoneCallback 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) { - uint32_t total_pixels; uint32_t byte_len; - if (w == 0 || h == 0 || pixels == NULL) + if (w == 0 || h == 0 || data == NULL) { - if (done_cb != NULL) - { - done_cb(user_data); - } + if (done_cb) done_cb(user_data); return; } - total_pixels = (uint32_t)w * h; - - if (total_pixels > LCD_DMA_MAX_PIXELS) + /* 正常情况下 LVGL 在 flush_ready 之前不会再次进入,走到这里说明 + * 上一次传输异常未收尾:强制恢复后再继续,避免卡死。 */ + if (lcd_state == LCD_BUSY) { - /* 超出单次 DMA buffer 容量,退化为阻塞传输 */ - ILI9341_WriteArea(x, y, w, h, pixels); - - if (done_cb != NULL) - { - done_cb(user_data); - } - - return; - } - - LCD_DMA_EnsureSemaphore(); - - if (lcd_dma_busy) - { - /* - * 正常情况下,LVGL 在上一次 flush 没有调用 flush_ready - * 之前不会发起新的 flush,所以走到这里说明上一次传输 - * 大概率已经出了问题(信号量没有被正确释放)。 - * 这里做一次强制恢复,而不是继续等待,避免连锁卡死。 - */ - LCD_DebugLogf("[LCD] WARN busy-reenter, force recover\r\n"); - HAL_SPI_Abort(&hspi1); LCD_CS_HIGH(); - lcd_dma_busy = 0; + lcd_state = LCD_IDLE; } - lcd_dma_busy = 1; - lcd_spi_last_error = 0; + byte_len = (uint32_t)w * h * 2U; - /* 组包到 DMA buffer */ - for (uint32_t i = 0; i < total_pixels; i++) - { - uint16_t color = pixels[i]; + /* 登记回调并置忙。先置忙再启动,保证完成中断能看到 BUSY。 */ + lcd_done_cb = done_cb; + lcd_done_arg = user_data; + lcd_state = LCD_BUSY; + lcd_start_tick = HAL_GetTick(); - lcd_dma_buf[i * 2U] = (uint8_t)(color >> 8); - lcd_dma_buf[i * 2U + 1] = (uint8_t)(color & 0xFF); - } + LCD_SetWindow(x, y, x + w - 1U, y + h - 1U); - byte_len = total_pixels * 2U; - - LCD_CleanDCache(lcd_dma_buf, byte_len); - - ILI9341_SetAddressWindow(x, y, x + w - 1U, y + h - 1U); - - lcd_dma_start_tick = HAL_GetTick(); + /* DMA 读取前 clean D-Cache,确保面板拿到最新像素 */ + LCD_CleanDCache(data, byte_len); LCD_CS_LOW(); LCD_DC_DATA(); - if (HAL_SPI_Transmit_DMA(&hspi1, lcd_dma_buf, (uint16_t)byte_len) != HAL_OK) + if (HAL_SPI_Transmit_DMA(&hspi1, (uint8_t *)data, (uint16_t)byte_len) != HAL_OK) { LCD_CS_HIGH(); - - lcd_dma_busy = 0; - - LCD_DebugLogf("[LCD] ERR DMA start failed\r\n"); - - if (done_cb != NULL) - { - done_cb(user_data); - } - + lcd_state = LCD_IDLE; + lcd_done_cb = NULL; + if (done_cb) done_cb(user_data); return; } - - /* - * 真正的关键改动:这里不再用 HAL_GetTick() 忙等,而是让任务 - * 通过信号量挂起,把 CPU 让给调度器;DMA 完成中断 - * (HAL_SPI_TxCpltCallback)或错误中断(HAL_SPI_ErrorCallback) - * 会释放这个信号量把任务唤醒。100ms 超时依旧作为兜底。 - */ - if (osSemaphoreAcquire(lcd_dma_sem, 100) != osOK) - { - LCD_DebugLogf("[LCD] ERR DMA timeout, aborting\r\n"); - HAL_SPI_Abort(&hspi1); - } - else if (lcd_spi_last_error != 0U) - { - LCD_DebugLogf("[LCD] ERR SPI err=0x%08lX\r\n", - (unsigned long)lcd_spi_last_error); - lcd_spi_last_error = 0U; - } - - ILI9341_SPI_WaitIdle(); - - LCD_CS_HIGH(); - - lcd_dma_busy = 0; - - if (done_cb != NULL) - { - done_cb(user_data); - } + /* 传输完成 → HAL_SPI_TxCpltCallback;出错 → HAL_SPI_ErrorCallback。 */ } -void ILI9341_DMA_Watchdog(uint32_t timeout_ms) +uint8_t ILI9341_IsBusy(void) { - if (!lcd_dma_busy) + return (lcd_state == LCD_BUSY) ? 1U : 0U; +} + +/* ================= 看门狗(任务上下文调用) ================= */ + +void ILI9341_ServiceWatchdog(uint32_t timeout_ms) +{ + if (lcd_state != LCD_BUSY) { return; } - uint32_t elapsed = HAL_GetTick() - lcd_dma_start_tick; - - if (elapsed > timeout_ms) + if ((HAL_GetTick() - lcd_start_tick) <= timeout_ms) { - LCD_DebugLogf("[LCD] WDG force recover after %lums\r\n", - (unsigned long)elapsed); - - HAL_SPI_Abort(&hspi1); - LCD_CS_HIGH(); - - lcd_dma_busy = 0; - - /* 如果此时正好有任务卡在 WriteArea_DMA 里等信号量,唤醒它 */ - if (lcd_dma_sem != NULL) - { - osSemaphoreRelease(lcd_dma_sem); - } + return; } + + /* 传输疑似卡死:中止 DMA、重建 SPI,再收尾(触发完成回调)。 */ + HAL_SPI_Abort(&hspi1); + + if (HAL_SPI_DeInit(&hspi1) == HAL_OK) + { + HAL_SPI_Init(&hspi1); + } + + LCD_FinishTransfer(); } -/* - * SPI1 TX DMA 完成回调(ISR 上下文)。 - * 只做最必要的事:释放信号量。不打印日志、不调用 LVGL。 - */ +/* ================= SPI 中断回调(ISR 上下文,尽量短) ================= */ + void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { - if (lcd_dma_sem != NULL) - { - osSemaphoreRelease(lcd_dma_sem); - } + /* H7 上 TxCplt 由 SPI EOT 触发,此刻数据已全部移出,可直接拉高 CS。 */ + LCD_FinishTransfer(); } } -/* - * SPI 错误回调(ISR 上下文)。 - * 之前的版本没有实现这个回调,一旦发生总线错误(Overrun/ - * ModeFault/CRC 等),只能傻等 100ms 超时才能恢复。 - * 现在记录错误码并立即释放信号量唤醒等待的任务, - * 真正的日志打印和处理放到任务上下文(WriteArea_DMA 里)去做。 - */ void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { - lcd_spi_last_error = hspi->ErrorCode; - - if (lcd_dma_sem != NULL) - { - osSemaphoreRelease(lcd_dma_sem); - } + /* 记录并中止,避免把坏状态带入下一帧。收尾同样会触发完成回调, + * 让 LVGL 继续,不至于卡在 flushing。 */ + HAL_SPI_Abort(hspi); + LCD_FinishTransfer(); } -} \ No newline at end of file +} diff --git a/SystemDrivers/ILI9341.h b/SystemDrivers/ILI9341.h index ff9c4e8..247d6cd 100644 --- a/SystemDrivers/ILI9341.h +++ b/SystemDrivers/ILI9341.h @@ -7,27 +7,37 @@ extern "C" { #include "main.h" #include "spi.h" - -/* 默认竖屏分辨率 */ -#define ILI9341_WIDTH 240 -#define ILI9341_HEIGHT 320 +#include +#include /* ============================================================ - * 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. 全异步 DMA:ILI9341_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 \ No newline at end of file +#endif /* __ILI9341_H__ */ diff --git a/SystemDrivers/README_ILI9341.md b/SystemDrivers/README_ILI9341.md new file mode 100644 index 0000000..a7ff214 --- /dev/null +++ b/SystemDrivers/README_ILI9341.md @@ -0,0 +1,108 @@ +# 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**;② 全异步 DMA,去掉信号量;出错回调 + 看门狗自恢复,单次异常不级联 | +| 刷新与 CPU 抢时间 | flush_cb 阻塞等待传输完成 | flush_cb 启动 DMA 后**立即返回**,双缓冲边渲染边传,完成中断里 `lv_disp_flush_ready()` | +| 字节序靠 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,立即返回 + → [DMA 完成] SPI1 中断 → HAL_SPI_TxCpltCallback → CS 高 → lv_disp_flush_ready() + → [出错] HAL_SPI_ErrorCallback → Abort → 收尾 → flush_ready(不卡死) + → [兜底] 任务里 ILI9341_ServiceWatchdog(180) 超时中止+重建 SPI +``` + +关键不变量:任意时刻**至多一次 DMA 在途**(LVGL 在 `flush_ready` 前不会再次 +调用 `flush_cb`)。收尾函数 `LCD_FinishTransfer()` 在关中断临界区内“取回调+复位 +状态”,保证 `done_cb` 只被调用一次,杜绝 ISR 与看门狗竞争。 + +--- + +## 3. 内存放置(最关键,务必确认) + +STM32H7 的坑:**DMA1/DMA2 无法访问 DTCM(0x20000000)**,能访问 AXI SRAM(0x24000000)。 + +- 自定义链接脚本:`MDK-ARM/motor_ili9341_lcd.sct` + - `RW_IRAM1 @0x20000000`(DTCM):普通变量/堆栈,**不放** DMA 缓冲。 + - `RW_IRAM2 @0x24000000`(AXI SRAM):`*(.lcd_ram)` 显式放这里 + 其余 RW。 +- 工程已改用该脚本:`motor_ili9341.uvprojx` 中 + `0` + `.\motor_ili9341_lcd.sct`。 +- 所有交给 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`。 + +--- + +## 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) + +```c +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. 构建与验证清单 + +1. Keil 打开工程 → 确认 Options → Linker 使用 `motor_ili9341_lcd.sct`(本工程已配置)。 +2. 编译无警告地通过;`.map` 里确认 `buf_1/buf_2/.lcd_ram` 地址在 `0x24xxxxxx`。 +3. 烧录后: + - 蓝色方块左右移动**无残留线条/拖影**; + - 长时间(数十分钟以上)运行**不白屏、不冻结**; + - 若镜像/颠倒,按第 4 节调 MADCTL。 + +> 本仓库环境无法编译/烧录 STM32,以上第 2、3 步需在 Keil + 硬件上确认。 +> 第 3 节的内存放置是唯一强依赖工具链的点,请务必核对 `.map`。