重构 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);
}
+40 -141
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.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
+2 -2
View File
@@ -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>
+34
View File
@@ -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 专用高速 RAMDMA 不可访问。
; 普通全局变量、堆栈放这里。切勿放 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
View File
@@ -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 SRAM0x30000000 起)
* 已经声明为一个可用 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
View File
@@ -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. 全异步 DMAILI9341_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__ */
+108
View File
@@ -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. 对外 APIILI9341.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`。