重构 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:
+3
-1
@@ -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)
|
||||
|
||||
+5
-7
@@ -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);
|
||||
}
|
||||
|
||||
+40
-141
@@ -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.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;
|
||||
}
|
||||
/* 硬件旋转: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(px0, py0, pw, ph,
|
||||
rot_buf,
|
||||
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
|
||||
@@ -362,7 +362,7 @@
|
||||
</VariousControls>
|
||||
</Aads>
|
||||
<LDads>
|
||||
<umfTarg>1</umfTarg>
|
||||
<umfTarg>0</umfTarg>
|
||||
<Ropi>0</Ropi>
|
||||
<Rwpi>0</Rwpi>
|
||||
<noStLib>0</noStLib>
|
||||
@@ -371,7 +371,7 @@
|
||||
<TextAddressRange></TextAddressRange>
|
||||
<DataAddressRange></DataAddressRange>
|
||||
<pXoBase></pXoBase>
|
||||
<ScatterFile></ScatterFile>
|
||||
<ScatterFile>.\motor_ili9341_lcd.sct</ScatterFile>
|
||||
<IncludeLibs></IncludeLibs>
|
||||
<IncludeLibsPath></IncludeLibsPath>
|
||||
<Misc></Misc>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+185
-553
@@ -1,418 +1,189 @@
|
||||
#include "ili9341.h"
|
||||
#include "cmsis_os.h"
|
||||
#include "uart4.h"
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/*
|
||||
* 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;
|
||||
|
||||
static uint16_t lcd_width = ILI9341_WIDTH;
|
||||
static uint16_t lcd_height = ILI9341_HEIGHT;
|
||||
|
||||
/* LCD GPIO 控制 */
|
||||
/* ================= 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)
|
||||
|
||||
/*
|
||||
* 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)
|
||||
static uint16_t lcd_width = ILI9341_WIDTH;
|
||||
static uint16_t lcd_height = ILI9341_HEIGHT;
|
||||
|
||||
/*
|
||||
* 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
|
||||
/* ================= 异步刷新状态(全部由 volatile 保护) ================= */
|
||||
typedef enum { LCD_IDLE = 0, LCD_BUSY = 1 } lcd_state_t;
|
||||
|
||||
LCD_DMA_BUF_ATTR
|
||||
static uint8_t lcd_dma_buf[LCD_DMA_BUF_SIZE];
|
||||
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;
|
||||
|
||||
/* ---- 状态与同步 ---- */
|
||||
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;
|
||||
return (uint16_t)(((r & 0xF8U) << 8) | ((g & 0xFCU) << 3) | (b >> 3));
|
||||
}
|
||||
|
||||
ILI9341_SetAddressWindow(x, y, x, y);
|
||||
ILI9341_WriteData16(color);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
uint32_t tick_start;
|
||||
/* ================= 异步刷新完成收尾(供 ISR / 看门狗调用) ================= */
|
||||
|
||||
#if defined(SPI_FLAG_TXC)
|
||||
tick_start = HAL_GetTick();
|
||||
static void LCD_FinishTransfer(void)
|
||||
{
|
||||
ILI9341_FlushDoneCb cb;
|
||||
void *arg;
|
||||
|
||||
while (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_TXC) == RESET)
|
||||
{
|
||||
if ((HAL_GetTick() - tick_start) > 20U)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
#elif defined(SPI_FLAG_EOT)
|
||||
tick_start = HAL_GetTick();
|
||||
/* 关中断内做“取出并清空回调 + 复位状态”,保证只收尾一次,
|
||||
* 杜绝 ISR 与看门狗竞争导致 done_cb 被调用两次。 */
|
||||
uint32_t primask = __get_PRIMASK();
|
||||
__disable_irq();
|
||||
|
||||
while (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_EOT) == RESET)
|
||||
if (lcd_state != LCD_BUSY)
|
||||
{
|
||||
if ((HAL_GetTick() - tick_start) > 20U)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
#else
|
||||
for (volatile uint32_t i = 0; i < 1000U; i++)
|
||||
{
|
||||
__NOP();
|
||||
}
|
||||
#endif
|
||||
|
||||
__DSB();
|
||||
__set_PRIMASK(primask);
|
||||
return;
|
||||
}
|
||||
|
||||
void ILI9341_WriteArea_DMA(uint16_t x,
|
||||
uint16_t y,
|
||||
uint16_t w,
|
||||
uint16_t h,
|
||||
const uint16_t *pixels,
|
||||
ILI9341_DmaDoneCallback done_cb,
|
||||
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)
|
||||
{
|
||||
cb(arg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 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;
|
||||
/* 传输完成 → HAL_SPI_TxCpltCallback;出错 → HAL_SPI_ErrorCallback。 */
|
||||
}
|
||||
|
||||
ILI9341_SPI_WaitIdle();
|
||||
|
||||
LCD_CS_HIGH();
|
||||
|
||||
lcd_dma_busy = 0;
|
||||
|
||||
if (done_cb != NULL)
|
||||
uint8_t ILI9341_IsBusy(void)
|
||||
{
|
||||
done_cb(user_data);
|
||||
}
|
||||
return (lcd_state == LCD_BUSY) ? 1U : 0U;
|
||||
}
|
||||
|
||||
void ILI9341_DMA_Watchdog(uint32_t timeout_ms)
|
||||
/* ================= 看门狗(任务上下文调用) ================= */
|
||||
|
||||
void ILI9341_ServiceWatchdog(uint32_t timeout_ms)
|
||||
{
|
||||
if (!lcd_dma_busy)
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
/* 传输疑似卡死:中止 DMA、重建 SPI,再收尾(触发完成回调)。 */
|
||||
HAL_SPI_Abort(&hspi1);
|
||||
LCD_CS_HIGH();
|
||||
|
||||
lcd_dma_busy = 0;
|
||||
|
||||
/* 如果此时正好有任务卡在 WriteArea_DMA 里等信号量,唤醒它 */
|
||||
if (lcd_dma_sem != NULL)
|
||||
if (HAL_SPI_DeInit(&hspi1) == HAL_OK)
|
||||
{
|
||||
osSemaphoreRelease(lcd_dma_sem);
|
||||
}
|
||||
}
|
||||
HAL_SPI_Init(&hspi1);
|
||||
}
|
||||
|
||||
/*
|
||||
* SPI1 TX DMA 完成回调(ISR 上下文)。
|
||||
* 只做最必要的事:释放信号量。不打印日志、不调用 LVGL。
|
||||
*/
|
||||
LCD_FinishTransfer();
|
||||
}
|
||||
|
||||
/* ================= 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();
|
||||
}
|
||||
}
|
||||
+64
-50
@@ -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. 全异步 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
|
||||
|
||||
/* 传输完成回调(在 SPI 中断上下文里被调用,务必只做极短的事) */
|
||||
typedef void (*ILI9341_FlushDoneCb)(void *user_data);
|
||||
|
||||
/* ---- 初始化 / 方向 ---- */
|
||||
void ILI9341_Init(void);
|
||||
void ILI9341_SetRotation(uint8_t rotation);
|
||||
void ILI9341_SetRotation(uint8_t rotation); /* 0/1/2/3,默认 1=横屏 */
|
||||
|
||||
/* ---- 阻塞式基础绘图(仅用于初始化清屏/自检,非 LVGL 热路径) ---- */
|
||||
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);
|
||||
uint16_t ILI9341_Color565(uint8_t r, uint8_t g, uint8_t b);
|
||||
|
||||
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,
|
||||
/* ---- 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);
|
||||
|
||||
/*
|
||||
* 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);
|
||||
/* ---- 传输是否进行中(供上层判断) ---- */
|
||||
uint8_t ILI9341_IsBusy(void);
|
||||
|
||||
uint16_t ILI9341_Color565(uint8_t r, uint8_t g, uint8_t b);
|
||||
/* ---- 看门狗:从任务上下文周期性调用(例如 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__ */
|
||||
|
||||
@@ -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` 中
|
||||
`<umfTarg>0</umfTarg>` + `<ScatterFile>.\motor_ili9341_lcd.sct</ScatterFile>`。
|
||||
- 所有交给 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`。
|
||||
Reference in New Issue
Block a user