Files
motor_ili9341/SystemDrivers/ILI9341.c
T
zhy200308 1273a14bf2 ILI9341: 刷新改为 DMA+任务内同步等待,消除移动残留竞态
- 中断只置完成标志,等待/收尾/回调全部移到任务上下文
- 去掉 ISR 内 lv_disp_flush_ready、ISR 内 Abort、双重收尾等竞态
- flush 返回时该块像素必已完整写入,LVGL 不会提前复用缓冲
- 拉高 CS 前等待 SPI 移位排空,避免尾部像素被截断
- 传输 50ms 超时后重建 SPI 自恢复
2026-07-08 11:16:57 +08:00

349 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "ili9341.h"
/*
* 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 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_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();
}
/* ================= LVGL 刷新入口(DMA 加速,任务内同步等待完成) ================= */
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;
uint32_t t0;
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);
/* 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();
if (done_cb) done_cb(user_data);
return;
}
/* 任务内同步等待:中断只把 lcd_xfer_done 置 1,这里轮询等待。
* 传输一小块通常 1~2ms 即完成;50ms 超时兜底防止硬件卡死。
* 这样每次 flush 返回时,这块像素一定已经完整写入面板——不会出现
* “LVGL 提前复用缓冲 / 擦除区没写完”导致的移动残留。 */
t0 = HAL_GetTick();
while (lcd_xfer_done == 0U)
{
if ((HAL_GetTick() - t0) > 50U)
{
HAL_SPI_Abort(&hspi1);
/* 传输异常,重建 SPI,避免坏状态带入后续帧 */
if (HAL_SPI_DeInit(&hspi1) == HAL_OK)
{
HAL_SPI_Init(&hspi1);
}
lcd_xfer_done = 1;
break;
}
}
LCD_SPI_WaitShiftEmpty();
LCD_CS_HIGH();
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; /* 唤醒等待循环,由任务侧决定如何恢复 */
}
}