重构 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
+3 -1
View File
@@ -13,7 +13,9 @@
*====================*/
#define LV_COLOR_DEPTH 16
#define LV_COLOR_16_SWAP 0
/* =1LVGL 直接产出 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)
+5 -7
View File
@@ -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);
}
+43 -144
View File
@@ -1,9 +1,18 @@
/**
* @file lv_port_disp.c
*
* ILI9341 + LVGL 显示接驳(重写版)
* ------------------------------------------------------------------
* - 硬件旋转:面板由 MADCTL 直接设为横屏 320x240,这里不再做任何
* 软件转置,LVGL 坐标与面板坐标 1:1,移动物体不再留残影。
* - 双绘制缓冲 + 异步 DMAflush_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
#endif