fd6c18c4c7
- ILI9341_USE_DMA 开关:1=DMA+同步等待,0=纯阻塞SPI(不用DMA),同一入口 - ILI9341_LOG_EN/LOG_EVERY:每次(或每N次)flush 打印 方式/区域/字节/耗时/ 错误码/累计错误/超时/缓冲地址(判断是否落在AXI SRAM) - DWT 微秒计时;纯阻塞路径无 D-Cache/DMA 一致性问题,用于对照 - README 增加两种模式对照判读表
457 lines
15 KiB
C
457 lines
15 KiB
C
#include "ili9341.h"
|
||
#include "uart4.h" /* Debug_Printf:刷新日志 */
|
||
|
||
/*
|
||
* ILI9341 驱动 —— 全异步 DMA 重写版
|
||
* ------------------------------------------------------------------
|
||
* 相比旧版的根本性改动:
|
||
* [方向] 旧版在 disp_flush() 里做软件转置 + MADCTL 原生方向,
|
||
* 任何坐标/尺寸的细微不一致都会在移动物体处留下残影。
|
||
* 新版用 MADCTL 直接把面板设为横屏,LVGL 坐标 == 面板坐标,
|
||
* 刷新区域与 LVGL 的失效区域严格 1:1,残留问题消失。
|
||
* [同步] 旧版发起 DMA 后用信号量阻塞等待,出错就 Abort,一旦
|
||
* SPI 状态机被打乱就会级联白屏。新版 FlushArea_DMA 启动
|
||
* DMA 后立即返回,完成/出错都由中断回调收尾,无信号量、
|
||
* 无阻塞、时序自同步。
|
||
* [内存] DMA 源缓冲一律位于 AXI SRAM(.lcd_ram 段),DMA1 可访问;
|
||
* 发送前做 D-Cache clean,保证 DMA 读到的是最新数据。
|
||
* [恢复] 出错回调 + 看门狗都会把 CS 拉高、状态复位、并触发完成
|
||
* 回调,任何单次异常都不会让刷新管线永久停摆。
|
||
*/
|
||
|
||
extern SPI_HandleTypeDef hspi1;
|
||
|
||
/* ================= GPIO 控制 ================= */
|
||
#define LCD_CS_LOW() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET)
|
||
#define LCD_CS_HIGH() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_SET)
|
||
#define LCD_DC_CMD() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_RESET)
|
||
#define LCD_DC_DATA() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_SET)
|
||
#define LCD_RST_LOW() HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_RESET)
|
||
#define LCD_RST_HIGH() HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_SET)
|
||
#define LCD_BL_ON() HAL_GPIO_WritePin(LCD_BL_PORT, LCD_BL_PIN, GPIO_PIN_SET)
|
||
#define LCD_BL_OFF() HAL_GPIO_WritePin(LCD_BL_PORT, LCD_BL_PIN, GPIO_PIN_RESET)
|
||
|
||
static uint16_t lcd_width = ILI9341_WIDTH;
|
||
static uint16_t lcd_height = ILI9341_HEIGHT;
|
||
|
||
/* ================= 刷新状态(DMA 传输 + 任务内同步等待) =================
|
||
* 设计取舍:DMA 只用来加速一块像素的搬运,等待/收尾/回调全部放在
|
||
* 任务上下文里做(同步)。中断只负责把 lcd_xfer_done 置 1,极短。
|
||
* 这样彻底消除“ISR 里调用 lv_disp_flush_ready / 在 ISR 里 Abort /
|
||
* 完成被收尾两次”等竞态——这些正是快速移动物体出现残留的隐患。 */
|
||
static volatile uint8_t lcd_xfer_done = 1; /* 1=空闲/已完成 */
|
||
static volatile uint32_t lcd_spi_err = 0; /* 最近一次 SPI 错误码 */
|
||
static volatile uint32_t lcd_start_tick = 0;
|
||
|
||
/* ---- 诊断统计 ---- */
|
||
static uint32_t lcd_flush_cnt = 0; /* flush 总次数 */
|
||
static uint32_t lcd_err_cnt = 0; /* SPI 错误次数 */
|
||
static uint32_t lcd_timeout_cnt = 0; /* 传输超时次数 */
|
||
|
||
/* ================= DWT 微秒计时(用于日志里的传输耗时) ================= */
|
||
static void LCD_DWT_Init(void)
|
||
{
|
||
#if defined(DWT) && defined(CoreDebug)
|
||
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
||
DWT->CYCCNT = 0;
|
||
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
||
#endif
|
||
}
|
||
|
||
static uint32_t LCD_Micros(uint32_t start_cyc)
|
||
{
|
||
#if defined(DWT)
|
||
uint32_t now = DWT->CYCCNT;
|
||
uint32_t diff = now - start_cyc; /* 32 位自然回绕 */
|
||
uint32_t cyc_per_us = SystemCoreClock / 1000000U;
|
||
return (cyc_per_us != 0U) ? (diff / cyc_per_us) : diff;
|
||
#else
|
||
(void)start_cyc;
|
||
return 0U;
|
||
#endif
|
||
}
|
||
|
||
/* ================= 阻塞式低层写命令/数据(仅用于初始化与设窗口) ================= */
|
||
|
||
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 LCD_WriteData(const uint8_t *data, uint16_t size)
|
||
{
|
||
LCD_CS_LOW();
|
||
LCD_DC_DATA();
|
||
HAL_SPI_Transmit(&hspi1, (uint8_t *)data, size, HAL_MAX_DELAY);
|
||
LCD_CS_HIGH();
|
||
}
|
||
|
||
static void LCD_WriteData8(uint8_t data)
|
||
{
|
||
LCD_WriteData(&data, 1);
|
||
}
|
||
|
||
/* 设定地址窗口 [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[4];
|
||
|
||
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_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);
|
||
|
||
LCD_WriteCmd(0x2C);
|
||
}
|
||
|
||
static void LCD_Reset(void)
|
||
{
|
||
LCD_RST_HIGH(); HAL_Delay(20);
|
||
LCD_RST_LOW(); HAL_Delay(20);
|
||
LCD_RST_HIGH(); HAL_Delay(120);
|
||
}
|
||
|
||
/* ================= 初始化 ================= */
|
||
|
||
void ILI9341_Init(void)
|
||
{
|
||
LCD_CS_HIGH();
|
||
LCD_BL_ON();
|
||
|
||
LCD_DWT_Init();
|
||
|
||
LCD_Reset();
|
||
|
||
LCD_WriteCmd(0x01); /* Software reset */
|
||
HAL_Delay(120);
|
||
|
||
/* 厂商推荐上电时序 */
|
||
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); }
|
||
|
||
LCD_WriteCmd(0x11); /* Sleep out */
|
||
HAL_Delay(120);
|
||
LCD_WriteCmd(0x29); /* Display on */
|
||
HAL_Delay(20);
|
||
|
||
ILI9341_SetRotation(1); /* 默认横屏 320x240,硬件旋转 */
|
||
}
|
||
|
||
/* ================= 方向(硬件 MADCTL 旋转) =================
|
||
* 若烧录后发现镜像/上下颠倒,只需换用同一行注释里给出的备选 MADCTL:
|
||
* 横屏可选 0x28 / 0xE8,竖屏可选 0x48 / 0x88。
|
||
*/
|
||
void ILI9341_SetRotation(uint8_t rotation)
|
||
{
|
||
uint8_t madctl;
|
||
|
||
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; /* 横屏翻转 */
|
||
}
|
||
|
||
LCD_WriteCmd(0x36);
|
||
LCD_WriteData8(madctl);
|
||
}
|
||
|
||
/* ================= 阻塞式基础绘图(初始化清屏用) ================= */
|
||
|
||
uint16_t ILI9341_Color565(uint8_t r, uint8_t g, uint8_t b)
|
||
{
|
||
return (uint16_t)(((r & 0xF8U) << 8) | ((g & 0xFCU) << 3) | (b >> 3));
|
||
}
|
||
|
||
void ILI9341_FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color)
|
||
{
|
||
uint8_t line[64 * 2]; /* 一次最多推 64 像素 */
|
||
uint32_t pixels;
|
||
|
||
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)
|
||
{
|
||
line[i] = (uint8_t)(color >> 8);
|
||
line[i + 1] = (uint8_t)(color & 0xFF);
|
||
}
|
||
|
||
LCD_SetWindow(x, y, x + w - 1, y + h - 1);
|
||
|
||
LCD_CS_LOW();
|
||
LCD_DC_DATA();
|
||
|
||
pixels = (uint32_t)w * h;
|
||
while (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();
|
||
}
|
||
|
||
void ILI9341_FillScreen(uint16_t color)
|
||
{
|
||
ILI9341_FillRect(0, 0, lcd_width, lcd_height, color);
|
||
}
|
||
|
||
/* ================= D-Cache 维护 ================= */
|
||
|
||
static void LCD_CleanDCache(const void *addr, uint32_t size)
|
||
{
|
||
#if defined(__DCACHE_PRESENT) && (__DCACHE_PRESENT == 1U)
|
||
if ((SCB->CCR & SCB_CCR_DC_Msk) != 0U)
|
||
{
|
||
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;
|
||
#endif
|
||
}
|
||
|
||
/* ================= 等待 SPI 移位彻底完成 ================= */
|
||
|
||
static void LCD_SPI_WaitShiftEmpty(void)
|
||
{
|
||
/* HAL 在传输完成中断里已经把数据全部交给 SPI,但最后几个 bit 可能
|
||
* 还在移位。拉高 CS 前确认 TXC/EOT,避免尾部像素被 CS 截断——尾部
|
||
* 截断在快速移动时会表现为“擦除不干净/残留一条边”。 */
|
||
uint32_t t0 = HAL_GetTick();
|
||
|
||
#if defined(SPI_FLAG_TXC)
|
||
while (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_TXC) == RESET)
|
||
{
|
||
if ((HAL_GetTick() - t0) > 5U) break;
|
||
}
|
||
#elif defined(SPI_FLAG_EOT)
|
||
while (__HAL_SPI_GET_FLAG(&hspi1, SPI_FLAG_EOT) == RESET)
|
||
{
|
||
if ((HAL_GetTick() - t0) > 5U) break;
|
||
}
|
||
#endif
|
||
__DSB();
|
||
}
|
||
|
||
/* ================= 两种传输实现 =================
|
||
* 均为“同步返回”:函数返回时该块像素一定已完整写入面板。
|
||
* 返回值:0=成功,1=超时/出错(已尽力恢复)。 */
|
||
|
||
/* (A) DMA 传输 + 任务内同步等待 */
|
||
static uint8_t LCD_Xfer_DMA(const uint16_t *data, uint32_t byte_len)
|
||
{
|
||
uint32_t t0;
|
||
uint8_t fail = 0;
|
||
|
||
/* DMA 读取前 clean D-Cache,确保面板拿到最新像素 */
|
||
LCD_CleanDCache(data, byte_len);
|
||
|
||
lcd_xfer_done = 0;
|
||
lcd_spi_err = 0;
|
||
lcd_start_tick = HAL_GetTick();
|
||
|
||
LCD_CS_LOW();
|
||
LCD_DC_DATA();
|
||
|
||
if (HAL_SPI_Transmit_DMA(&hspi1, (uint8_t *)data, (uint16_t)byte_len) != HAL_OK)
|
||
{
|
||
lcd_xfer_done = 1;
|
||
LCD_CS_HIGH();
|
||
return 1;
|
||
}
|
||
|
||
/* 中断只把 lcd_xfer_done 置 1,这里轮询等待;50ms 超时兜底。 */
|
||
t0 = HAL_GetTick();
|
||
while (lcd_xfer_done == 0U)
|
||
{
|
||
if ((HAL_GetTick() - t0) > 50U)
|
||
{
|
||
HAL_SPI_Abort(&hspi1);
|
||
if (HAL_SPI_DeInit(&hspi1) == HAL_OK)
|
||
{
|
||
HAL_SPI_Init(&hspi1);
|
||
}
|
||
lcd_xfer_done = 1;
|
||
lcd_timeout_cnt++;
|
||
fail = 1;
|
||
break;
|
||
}
|
||
}
|
||
|
||
LCD_SPI_WaitShiftEmpty();
|
||
LCD_CS_HIGH();
|
||
return fail;
|
||
}
|
||
|
||
/* (B) 纯阻塞 SPI(完全不用 DMA,CPU 直推)
|
||
* CPU 经缓存读数据,无 DMA/D-Cache 一致性问题;用于对照排查。 */
|
||
static uint8_t LCD_Xfer_Poll(const uint16_t *data, uint32_t byte_len)
|
||
{
|
||
HAL_StatusTypeDef st;
|
||
|
||
lcd_xfer_done = 1; /* 阻塞方式无“在途”状态 */
|
||
lcd_spi_err = 0;
|
||
lcd_start_tick = HAL_GetTick();
|
||
|
||
LCD_CS_LOW();
|
||
LCD_DC_DATA();
|
||
|
||
/* byte_len 上限 = 单块缓冲字节数 < 65535,可一次发完 */
|
||
st = HAL_SPI_Transmit(&hspi1, (uint8_t *)data, (uint16_t)byte_len, 100);
|
||
|
||
LCD_SPI_WaitShiftEmpty();
|
||
LCD_CS_HIGH();
|
||
|
||
return (st == HAL_OK) ? 0U : 1U;
|
||
}
|
||
|
||
/* ================= 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 byte_len;
|
||
uint8_t fail;
|
||
uint32_t cyc0;
|
||
uint32_t us;
|
||
|
||
if (w == 0 || h == 0 || data == NULL)
|
||
{
|
||
if (done_cb) done_cb(user_data);
|
||
return;
|
||
}
|
||
|
||
byte_len = (uint32_t)w * h * 2U;
|
||
|
||
LCD_SetWindow(x, y, x + w - 1U, y + h - 1U);
|
||
|
||
#if defined(DWT)
|
||
cyc0 = DWT->CYCCNT;
|
||
#else
|
||
cyc0 = 0U;
|
||
#endif
|
||
|
||
#if (ILI9341_USE_DMA)
|
||
fail = LCD_Xfer_DMA(data, byte_len);
|
||
#else
|
||
fail = LCD_Xfer_Poll(data, byte_len);
|
||
#endif
|
||
|
||
us = LCD_Micros(cyc0);
|
||
|
||
lcd_flush_cnt++;
|
||
if (fail || lcd_spi_err) lcd_err_cnt++;
|
||
|
||
#if (ILI9341_LOG_EN)
|
||
if ((lcd_flush_cnt % ILI9341_LOG_EVERY) == 0U)
|
||
{
|
||
/* 一行一条,便于两种方式对比:
|
||
* 方式 / 序号 / 区域 / 字节数 / 传输耗时us / 是否失败 / SPI错误码 /
|
||
* 累计错误 / 累计超时 / 数据缓冲地址(判断是否落在 0x24xxxxxx=AXI SRAM) */
|
||
Debug_Printf("[LCD] %s #%lu (%u,%u %ux%u) len=%lu t=%luus fail=%u err=0x%lX "
|
||
"eC=%lu toC=%lu buf=0x%08lX\r\n",
|
||
#if (ILI9341_USE_DMA)
|
||
"DMA ",
|
||
#else
|
||
"POLL",
|
||
#endif
|
||
(unsigned long)lcd_flush_cnt,
|
||
(unsigned)x, (unsigned)y, (unsigned)w, (unsigned)h,
|
||
(unsigned long)byte_len,
|
||
(unsigned long)us,
|
||
(unsigned)fail,
|
||
(unsigned long)lcd_spi_err,
|
||
(unsigned long)lcd_err_cnt,
|
||
(unsigned long)lcd_timeout_cnt,
|
||
(unsigned long)(uint32_t)data);
|
||
}
|
||
#else
|
||
(void)us;
|
||
#endif
|
||
|
||
if (done_cb) done_cb(user_data);
|
||
}
|
||
|
||
uint8_t ILI9341_IsBusy(void)
|
||
{
|
||
return (lcd_xfer_done == 0U) ? 1U : 0U;
|
||
}
|
||
|
||
/* ================= 看门狗(任务上下文调用) =================
|
||
* 同步刷新下,本函数返回时传输一定已完成,看门狗基本用不到;
|
||
* 保留作为极端兜底:若发现长时间处于“未完成”,强制恢复 SPI。 */
|
||
void ILI9341_ServiceWatchdog(uint32_t timeout_ms)
|
||
{
|
||
if (lcd_xfer_done != 0U)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if ((HAL_GetTick() - lcd_start_tick) <= timeout_ms)
|
||
{
|
||
return;
|
||
}
|
||
|
||
HAL_SPI_Abort(&hspi1);
|
||
if (HAL_SPI_DeInit(&hspi1) == HAL_OK)
|
||
{
|
||
HAL_SPI_Init(&hspi1);
|
||
}
|
||
LCD_CS_HIGH();
|
||
lcd_xfer_done = 1;
|
||
}
|
||
|
||
/* ================= SPI 中断回调(ISR 上下文,极短) ================= */
|
||
|
||
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
|
||
{
|
||
if (hspi->Instance == SPI1)
|
||
{
|
||
lcd_xfer_done = 1; /* 只置标志,收尾/回调全部在任务里做 */
|
||
}
|
||
}
|
||
|
||
void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi)
|
||
{
|
||
if (hspi->Instance == SPI1)
|
||
{
|
||
lcd_spi_err = hspi->ErrorCode;
|
||
lcd_xfer_done = 1; /* 唤醒等待循环,由任务侧决定如何恢复 */
|
||
}
|
||
}
|