Rdzleo eb96130fc9 feat(Rtc_AIavatar): 数字人透明 GIF 显示方案 PoC 完成(背景图+透明GIF叠加)
源代码变更:
- main/dzbj/bg_gif_demo.c/h: 方案 C 最终实现 - JPG 背景图(lv_img) + 透明 GIF(lv_gif) 叠加
- main/dzbj/dual_gif_demo.c/h: 方案 B 中间产物 - 双 GIF 循环切换
- main/dzbj/sprite_demo.c/h: 方案 A 已弃用 - DMA 直写 GRAM 与 LVGL 争抢 LCD IO 失败
- main/dzbj/ai_chat_ui.c: 集成 USE_BG_GIF_POC 开关,加载背景图+透明 GIF
- main/dzbj/lcd.c: panel_handle 移除 static,便于其他模块访问
- main/CMakeLists.txt: 新增 3 个 dzbj 模块编译

资源新增:
- spiffs_image/Background_360x360.jpg: 设备背景图(20KB)
- spiffs_image/hiyori_m05.gif: Cubism Editor 直接导出的透明 GIF(2.3MB)
- docs/Rtc_AIavatar/: Live2D 模型(Hiyori/Haru) + 32 段 Haru GIF + 方案文档第18章 PoC 实战记录
- tools/sprite_poc/: Python GIF→RGB565 转换脚本

踩坑要点(详见 docs/Rtc_AIavatar 第18章):
- PIL Image.quantize() 会破坏 RGBA 透明度,必须改用 gifsicle
- PIL 保存动画 GIF 仅第1帧有透明,后续帧不透明 - LVGL gifdec 按帧读取
- Cubism Editor 直接导出 GIF 才能逐帧保留透明信息(FREE 版限制部分模型)
- gifsicle --lossy 会严重锯齿化,去掉只保留 --colors 256 + -O3 即可
- 裁剪居中需用全帧 bbox 不能只看第1帧(Live2D 角色每帧位置有偏移)
- LVGL 默认不支持 PNG,背景图用 JPG + esp_jpeg 解码到 RGB565 buffer
- 透明 GIF 显示黑色背景: gifdec.c canvas 初始化 alpha 须改为 0x00
2026-05-12 17:14:49 +08:00

457 lines
17 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 "dzbj_gpio.h"
#include "esp_lvgl_port.h"
#include "esp_lcd_st77916.h"
#include "esp_err.h"
#include "esp_log.h"
#include "lcd.h"
#if DZBJ_ENABLE_TOUCH
#include "esp_lcd_touch_cst816s.h"
#endif
#include <string.h>
#include "esp_heap_caps.h"
// ST77916 初始化命令(从 dzbj 项目已验证的驱动版本提取)
// 主项目 managed_component 新版默认命令与本硬件面板不匹配,需手动注入
static const st77916_lcd_init_cmd_t lcd_init_cmds[] = {
{0xF0, (uint8_t[]){0x28}, 1, 0},
{0xF2, (uint8_t[]){0x28}, 1, 0},
{0x73, (uint8_t[]){0xF0}, 1, 0},
{0x7C, (uint8_t[]){0xD1}, 1, 0},
{0x83, (uint8_t[]){0xE0}, 1, 0},
{0x84, (uint8_t[]){0x61}, 1, 0},
{0xF2, (uint8_t[]){0x82}, 1, 0},
{0xF0, (uint8_t[]){0x00}, 1, 0},
{0xF0, (uint8_t[]){0x01}, 1, 0},
{0xF1, (uint8_t[]){0x01}, 1, 0},
{0xB0, (uint8_t[]){0x5E}, 1, 0},
{0xB1, (uint8_t[]){0x55}, 1, 0},
{0xB2, (uint8_t[]){0x24}, 1, 0},
{0xB3, (uint8_t[]){0x01}, 1, 0},
{0xB4, (uint8_t[]){0x87}, 1, 0},
{0xB5, (uint8_t[]){0x44}, 1, 0},
{0xB6, (uint8_t[]){0x8B}, 1, 0},
{0xB7, (uint8_t[]){0x40}, 1, 0},
{0xB8, (uint8_t[]){0x86}, 1, 0},
{0xB9, (uint8_t[]){0x15}, 1, 0},
{0xBA, (uint8_t[]){0x00}, 1, 0},
{0xBB, (uint8_t[]){0x08}, 1, 0},
{0xBC, (uint8_t[]){0x08}, 1, 0},
{0xBD, (uint8_t[]){0x00}, 1, 0},
{0xBE, (uint8_t[]){0x00}, 1, 0},
{0xBF, (uint8_t[]){0x07}, 1, 0},
{0xC0, (uint8_t[]){0x80}, 1, 0},
{0xC1, (uint8_t[]){0x10}, 1, 0},
{0xC2, (uint8_t[]){0x37}, 1, 0},
{0xC3, (uint8_t[]){0x80}, 1, 0},
{0xC4, (uint8_t[]){0x10}, 1, 0},
{0xC5, (uint8_t[]){0x37}, 1, 0},
{0xC6, (uint8_t[]){0xA9}, 1, 0},
{0xC7, (uint8_t[]){0x41}, 1, 0},
{0xC8, (uint8_t[]){0x01}, 1, 0},
{0xC9, (uint8_t[]){0xA9}, 1, 0},
{0xCA, (uint8_t[]){0x41}, 1, 0},
{0xCB, (uint8_t[]){0x01}, 1, 0},
{0xCC, (uint8_t[]){0x7F}, 1, 0},
{0xCD, (uint8_t[]){0x7F}, 1, 0},
{0xCE, (uint8_t[]){0xFF}, 1, 0},
{0xD0, (uint8_t[]){0x91}, 1, 0},
{0xD1, (uint8_t[]){0x68}, 1, 0},
{0xD2, (uint8_t[]){0x68}, 1, 0},
{0xF5, (uint8_t[]){0x00, 0xA5}, 2, 0},
{0xDD, (uint8_t[]){0x40}, 1, 0},
{0xDE, (uint8_t[]){0x40}, 1, 0},
{0xF1, (uint8_t[]){0x10}, 1, 0},
{0xF0, (uint8_t[]){0x00}, 1, 0},
{0xF0, (uint8_t[]){0x02}, 1, 0},
{0xE0, (uint8_t[]){0xF0, 0x10, 0x18, 0x0D, 0x0C, 0x38, 0x3E, 0x44, 0x51, 0x39, 0x15, 0x15, 0x30, 0x34}, 14, 0},
{0xE1, (uint8_t[]){0xF0, 0x0F, 0x17, 0x0D, 0x0B, 0x07, 0x3E, 0x33, 0x51, 0x39, 0x15, 0x15, 0x30, 0x34}, 14, 0},
{0xF0, (uint8_t[]){0x10}, 1, 0},
{0xF3, (uint8_t[]){0x10}, 1, 0},
{0xE0, (uint8_t[]){0x08}, 1, 0},
{0xE1, (uint8_t[]){0x00}, 1, 0},
{0xE2, (uint8_t[]){0x00}, 1, 0},
{0xE3, (uint8_t[]){0x00}, 1, 0},
{0xE4, (uint8_t[]){0xE0}, 1, 0},
{0xE5, (uint8_t[]){0x06}, 1, 0},
{0xE6, (uint8_t[]){0x21}, 1, 0},
{0xE7, (uint8_t[]){0x03}, 1, 0},
{0xE8, (uint8_t[]){0x05}, 1, 0},
{0xE9, (uint8_t[]){0x02}, 1, 0},
{0xEA, (uint8_t[]){0xE9}, 1, 0},
{0xEB, (uint8_t[]){0x00}, 1, 0},
{0xEC, (uint8_t[]){0x00}, 1, 0},
{0xED, (uint8_t[]){0x14}, 1, 0},
{0xEE, (uint8_t[]){0xFF}, 1, 0},
{0xEF, (uint8_t[]){0x00}, 1, 0},
{0xF8, (uint8_t[]){0xFF}, 1, 0},
{0xF9, (uint8_t[]){0x00}, 1, 0},
{0xFA, (uint8_t[]){0x00}, 1, 0},
{0xFB, (uint8_t[]){0x30}, 1, 0},
{0xFC, (uint8_t[]){0x00}, 1, 0},
{0xFD, (uint8_t[]){0x00}, 1, 0},
{0xFE, (uint8_t[]){0x00}, 1, 0},
{0xFF, (uint8_t[]){0x00}, 1, 0},
{0x60, (uint8_t[]){0x40}, 1, 0},
{0x61, (uint8_t[]){0x05}, 1, 0},
{0x62, (uint8_t[]){0x00}, 1, 0},
{0x63, (uint8_t[]){0x42}, 1, 0},
{0x64, (uint8_t[]){0xDA}, 1, 0},
{0x65, (uint8_t[]){0x00}, 1, 0},
{0x66, (uint8_t[]){0x00}, 1, 0},
{0x67, (uint8_t[]){0x00}, 1, 0},
{0x68, (uint8_t[]){0x00}, 1, 0},
{0x69, (uint8_t[]){0x00}, 1, 0},
{0x6A, (uint8_t[]){0x00}, 1, 0},
{0x6B, (uint8_t[]){0x00}, 1, 0},
{0x70, (uint8_t[]){0x40}, 1, 0},
{0x71, (uint8_t[]){0x04}, 1, 0},
{0x72, (uint8_t[]){0x00}, 1, 0},
{0x73, (uint8_t[]){0x42}, 1, 0},
{0x74, (uint8_t[]){0xD9}, 1, 0},
{0x75, (uint8_t[]){0x00}, 1, 0},
{0x76, (uint8_t[]){0x00}, 1, 0},
{0x77, (uint8_t[]){0x00}, 1, 0},
{0x78, (uint8_t[]){0x00}, 1, 0},
{0x79, (uint8_t[]){0x00}, 1, 0},
{0x7A, (uint8_t[]){0x00}, 1, 0},
{0x7B, (uint8_t[]){0x00}, 1, 0},
{0x80, (uint8_t[]){0x48}, 1, 0},
{0x81, (uint8_t[]){0x00}, 1, 0},
{0x82, (uint8_t[]){0x07}, 1, 0},
{0x83, (uint8_t[]){0x02}, 1, 0},
{0x84, (uint8_t[]){0xD7}, 1, 0},
{0x85, (uint8_t[]){0x04}, 1, 0},
{0x86, (uint8_t[]){0x00}, 1, 0},
{0x87, (uint8_t[]){0x00}, 1, 0},
{0x88, (uint8_t[]){0x48}, 1, 0},
{0x89, (uint8_t[]){0x00}, 1, 0},
{0x8A, (uint8_t[]){0x09}, 1, 0},
{0x8B, (uint8_t[]){0x02}, 1, 0},
{0x8C, (uint8_t[]){0xD9}, 1, 0},
{0x8D, (uint8_t[]){0x04}, 1, 0},
{0x8E, (uint8_t[]){0x00}, 1, 0},
{0x8F, (uint8_t[]){0x00}, 1, 0},
{0x90, (uint8_t[]){0x48}, 1, 0},
{0x91, (uint8_t[]){0x00}, 1, 0},
{0x92, (uint8_t[]){0x0B}, 1, 0},
{0x93, (uint8_t[]){0x02}, 1, 0},
{0x94, (uint8_t[]){0xDB}, 1, 0},
{0x95, (uint8_t[]){0x04}, 1, 0},
{0x96, (uint8_t[]){0x00}, 1, 0},
{0x97, (uint8_t[]){0x00}, 1, 0},
{0x98, (uint8_t[]){0x48}, 1, 0},
{0x99, (uint8_t[]){0x00}, 1, 0},
{0x9A, (uint8_t[]){0x0D}, 1, 0},
{0x9B, (uint8_t[]){0x02}, 1, 0},
{0x9C, (uint8_t[]){0xDD}, 1, 0},
{0x9D, (uint8_t[]){0x04}, 1, 0},
{0x9E, (uint8_t[]){0x00}, 1, 0},
{0x9F, (uint8_t[]){0x00}, 1, 0},
{0xA0, (uint8_t[]){0x48}, 1, 0},
{0xA1, (uint8_t[]){0x00}, 1, 0},
{0xA2, (uint8_t[]){0x06}, 1, 0},
{0xA3, (uint8_t[]){0x02}, 1, 0},
{0xA4, (uint8_t[]){0xD6}, 1, 0},
{0xA5, (uint8_t[]){0x04}, 1, 0},
{0xA6, (uint8_t[]){0x00}, 1, 0},
{0xA7, (uint8_t[]){0x00}, 1, 0},
{0xA8, (uint8_t[]){0x48}, 1, 0},
{0xA9, (uint8_t[]){0x00}, 1, 0},
{0xAA, (uint8_t[]){0x08}, 1, 0},
{0xAB, (uint8_t[]){0x02}, 1, 0},
{0xAC, (uint8_t[]){0xD8}, 1, 0},
{0xAD, (uint8_t[]){0x04}, 1, 0},
{0xAE, (uint8_t[]){0x00}, 1, 0},
{0xAF, (uint8_t[]){0x00}, 1, 0},
{0xB0, (uint8_t[]){0x48}, 1, 0},
{0xB1, (uint8_t[]){0x00}, 1, 0},
{0xB2, (uint8_t[]){0x0A}, 1, 0},
{0xB3, (uint8_t[]){0x02}, 1, 0},
{0xB4, (uint8_t[]){0xDA}, 1, 0},
{0xB5, (uint8_t[]){0x04}, 1, 0},
{0xB6, (uint8_t[]){0x00}, 1, 0},
{0xB7, (uint8_t[]){0x00}, 1, 0},
{0xB8, (uint8_t[]){0x48}, 1, 0},
{0xB9, (uint8_t[]){0x00}, 1, 0},
{0xBA, (uint8_t[]){0x0C}, 1, 0},
{0xBB, (uint8_t[]){0x02}, 1, 0},
{0xBC, (uint8_t[]){0xDC}, 1, 0},
{0xBD, (uint8_t[]){0x04}, 1, 0},
{0xBE, (uint8_t[]){0x00}, 1, 0},
{0xBF, (uint8_t[]){0x00}, 1, 0},
{0xC0, (uint8_t[]){0x10}, 1, 0},
{0xC1, (uint8_t[]){0x47}, 1, 0},
{0xC2, (uint8_t[]){0x56}, 1, 0},
{0xC3, (uint8_t[]){0x65}, 1, 0},
{0xC4, (uint8_t[]){0x74}, 1, 0},
{0xC5, (uint8_t[]){0x88}, 1, 0},
{0xC6, (uint8_t[]){0x99}, 1, 0},
{0xC7, (uint8_t[]){0x01}, 1, 0},
{0xC8, (uint8_t[]){0xBB}, 1, 0},
{0xC9, (uint8_t[]){0xAA}, 1, 0},
{0xD0, (uint8_t[]){0x10}, 1, 0},
{0xD1, (uint8_t[]){0x47}, 1, 0},
{0xD2, (uint8_t[]){0x56}, 1, 0},
{0xD3, (uint8_t[]){0x65}, 1, 0},
{0xD4, (uint8_t[]){0x74}, 1, 0},
{0xD5, (uint8_t[]){0x88}, 1, 0},
{0xD6, (uint8_t[]){0x99}, 1, 0},
{0xD7, (uint8_t[]){0x01}, 1, 0},
{0xD8, (uint8_t[]){0xBB}, 1, 0},
{0xD9, (uint8_t[]){0xAA}, 1, 0},
{0xF3, (uint8_t[]){0x01}, 1, 0},
{0xF0, (uint8_t[]){0x00}, 1, 0},
{0x3A, (uint8_t[]){0x55}, 1, 0},
{0x21, (uint8_t[]){0x00}, 1, 0},
{0x11, (uint8_t[]){0x00}, 1, 120},
{0x29, (uint8_t[]){0x00}, 1, 0},
};
static lv_disp_t * disp_handle = NULL;
esp_lcd_panel_handle_t panel_handle = NULL; // 暴露给 sprite_demo 等模块直接 DMA 写 LCD
static esp_lcd_panel_io_handle_t io_handle = NULL;
#if DZBJ_ENABLE_TOUCH
static esp_lcd_touch_handle_t touch_handle = NULL;
static esp_lcd_panel_io_handle_t tp_io_handle = NULL;
// 外部传入的 I2C 总线句柄(与主项目共享)
static i2c_master_bus_handle_t ext_i2c_bus = NULL;
#endif
void lcd_set_i2c_bus(i2c_master_bus_handle_t bus) {
#if DZBJ_ENABLE_TOUCH
ext_i2c_bus = bus;
#else
(void)bus;
#endif
}
void lcd_init(){
const spi_bus_config_t buscfg = ST77916_PANEL_BUS_QSPI_CONFIG(PIN_LCD_CLK,
PIN_LCD_D0,
PIN_LCD_D1,
PIN_LCD_D2,
PIN_LCD_D3,
LCD_HIGH * 80 * sizeof(uint16_t));
spi_bus_initialize(SPI_LCD_HOST, &buscfg, SPI_DMA_CH_AUTO);
// QSPI 80MHz
esp_lcd_panel_io_spi_config_t io_config = ST77916_PANEL_IO_QSPI_CONFIG(PIN_LCD_CS, NULL, NULL);
io_config.pclk_hz = 80 * 1000 * 1000;
io_config.trans_queue_depth = 64; // 默认 10 太小sprite 分条传输需要更大队列
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI_LCD_HOST, &io_config, &io_handle));
const st77916_vendor_config_t vendor_config = {
.init_cmds = lcd_init_cmds,
.init_cmds_size = sizeof(lcd_init_cmds) / sizeof(st77916_lcd_init_cmd_t),
.flags = {
.use_qspi_interface = 1,
},
};
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = PIN_LCD_RST,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
.bits_per_pixel = 16,
.vendor_config = &vendor_config,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_st77916(io_handle, &panel_config, &panel_handle));
esp_lcd_panel_reset(panel_handle);
esp_lcd_panel_init(panel_handle);
// 启用 TE 内部同步信号TEON 0x35, 参数 0x00 = V-Blank only
// 即使 TE 引脚未接 MCU启用后可通过 TESLRD(0x45) 读取扫描行位置
uint8_t te_param = 0x00;
esp_lcd_panel_io_tx_param(io_handle, 0x35, &te_param, 1);
ESP_LOGI(LCD_TAG, "TEON enabled (V-Blank mode)");
// 清空LCD GRAM避免显示上次关机时的残留画面
size_t clear_buffer_size = LCD_WID * 40;
uint16_t *clear_buffer = heap_caps_malloc(clear_buffer_size * sizeof(uint16_t), MALLOC_CAP_DMA);
if (clear_buffer) {
memset(clear_buffer, 0, clear_buffer_size * sizeof(uint16_t));
for (int y = 0; y < LCD_HIGH; y += 40) {
int lines = (y + 40 > LCD_HIGH) ? (LCD_HIGH - y) : 40;
esp_lcd_panel_draw_bitmap(panel_handle, 0, y, LCD_WID, y + lines, clear_buffer);
}
heap_caps_free(clear_buffer);
ESP_LOGI(LCD_TAG, "LCD GRAM cleared (black filled)");
} else {
ESP_LOGE(LCD_TAG, "Failed to allocate clear buffer");
}
esp_lcd_panel_disp_on_off(panel_handle, true);
}
#if DZBJ_ENABLE_TOUCH
// 初始化触摸控制器(使用外部传入的 I2C 总线)
void touch_init(){
const esp_lcd_touch_config_t tp_cfg = {
.x_max = LCD_WID,
.y_max = LCD_HIGH,
.rst_gpio_num = PIN_TP_RST,
.int_gpio_num = PIN_TP_INT,
.levels = {
.reset = 0,
.interrupt = 0,
},
.flags = {
.swap_xy = false,
.mirror_x = false,
.mirror_y = false,
},
};
const esp_lcd_panel_io_i2c_config_t tp_io_config = ESP_LCD_TOUCH_IO_I2C_CST816S_CONFIG();
// 使用外部传入的 I2C 总线句柄(与音频编解码器共享 I2C_NUM_1
i2c_master_bus_handle_t i2c_bus = ext_i2c_bus;
if (i2c_bus == NULL) {
ESP_LOGE(LCD_TAG, "I2C bus not set, call lcd_set_i2c_bus() first");
return;
}
// 直接传入 i2c_master_bus_handle_t_Generic 宏分派到 V2新驱动
// 不要强转为 esp_lcd_i2c_bus_handle_t(uint32_t),否则会触发 V1 legacy 驱动冲突
esp_err_t err = esp_lcd_new_panel_io_i2c(i2c_bus, &tp_io_config, &tp_io_handle);
if (err != ESP_OK) {
ESP_LOGE(LCD_TAG, "Failed to create I2C IO for touch: %s", esp_err_to_name(err));
return;
}
err = esp_lcd_touch_new_i2c_cst816s(tp_io_handle, &tp_cfg, &touch_handle);
if (err != ESP_OK) {
ESP_LOGE(LCD_TAG, "Failed to create touch handle: %s", esp_err_to_name(err));
return;
}
ESP_LOGI(LCD_TAG, "Touch controller initialized successfully");
}
#endif // DZBJ_ENABLE_TOUCH
// 初始化LVGL显示
void lvgl_lcd_init(){
const lvgl_port_cfg_t lvgl_cfg = {
.task_priority = 4,
.task_stack = 8192,
.task_affinity = -1,
.task_max_sleep_ms = 500,
.timer_period_ms = 5
};
lvgl_port_init(&lvgl_cfg);
// 使用内部 DMA 内存20 行单缓冲360×20×2 = 14400 字节)
// PSRAM 缓冲区与 SPI DMA 不兼容spi transmit queue 失败)
// 单缓冲 + 小行数,节省内部 SRAM 给 WiFi/BLE
#define LVGL_DRAW_BUF_LINES 20
size_t buffer_size = LCD_WID * LVGL_DRAW_BUF_LINES;
ESP_LOGI(LCD_TAG, "LVGL buffer: %d bytes (W:%d, Lines:%d, DMA, single)",
buffer_size * sizeof(uint16_t), LCD_WID, LVGL_DRAW_BUF_LINES);
const lvgl_port_display_cfg_t disp_cfg = {
.io_handle = io_handle,
.panel_handle = panel_handle,
.buffer_size = buffer_size,
.double_buffer = false,
.hres = LCD_WID,
.vres = LCD_HIGH,
.monochrome = false,
.rotation = {
.swap_xy = false,
.mirror_x = false,
.mirror_y = false,
},
.flags = {
.buff_dma = true,
.buff_spiram = false,
}
};
disp_handle = lvgl_port_add_disp(&disp_cfg);
#if DZBJ_ENABLE_TOUCH
if (touch_handle != NULL) {
lvgl_port_touch_cfg_t touch_cgf = {
.disp = disp_handle,
.handle = touch_handle,
};
lvgl_port_add_touch(&touch_cgf);
ESP_LOGI(LCD_TAG, "Touch controller added to LVGL");
}
#endif
}
#if DZBJ_ENABLE_TOUCH
void get_touch(uint16_t* touchx, uint16_t* touchy){
if (touch_handle == NULL) {
*touchx = 0;
*touchy = 0;
return;
}
*touchx = touch_handle->data.coords[0].x;
*touchy = touch_handle->data.coords[0].y;
}
#endif // DZBJ_ENABLE_TOUCH
// 直接 DMA 填充全屏纯色(绕过 LVGL使用调用者预分配的缓冲区
void lcd_fill_color_with_buf(uint16_t *buf, int strip_h) {
if (panel_handle == NULL || buf == NULL || strip_h <= 0) return;
for (int y = 0; y < LCD_HIGH; y += strip_h) {
int lines = (y + strip_h > LCD_HIGH) ? (LCD_HIGH - y) : strip_h;
esp_lcd_panel_draw_bitmap(panel_handle, 0, y, LCD_WID, y + lines, buf);
}
}
// 读取当前扫描行号TESLRD 0x45
// 返回 ESP_OK 表示读取成功scanline 存储扫描行号 N[11:0]
// 扫描行范围0 ~ (VSYNC+VBP+VACT+VFP-1),其中 0~359 为活跃显示区
esp_err_t lcd_read_scanline(uint16_t *scanline) {
if (!io_handle || !scanline) return ESP_ERR_INVALID_ARG;
uint8_t data[2] = {0};
esp_err_t ret = esp_lcd_panel_io_rx_param(io_handle, 0x45, data, 2);
if (ret == ESP_OK) {
// N[11:8] 在 data[0] 低4位N[7:0] 在 data[1]
*scanline = ((uint16_t)(data[0] & 0x0F) << 8) | data[1];
}
return ret;
}
// 等待 VBLANK 消隐期(带超时保护)
// 轮询扫描行号,当 scanline >= LCD_HIGH(360) 时表示进入消隐区
// timeout_us: 最大等待时间(微秒),建议 17000约1帧
// 返回 true = 成功等到 VBLANKfalse = 超时或读取失败
bool lcd_wait_vsync_timeout(uint32_t timeout_us) {
uint16_t scanline = 0;
int64_t start = esp_timer_get_time();
do {
if (lcd_read_scanline(&scanline) != ESP_OK) {
return false;
}
if (scanline >= LCD_HIGH) {
return true;
}
esp_rom_delay_us(50);
} while ((esp_timer_get_time() - start) < (int64_t)timeout_us);
return false;
}
// 清空LCD GRAM为黑色用于低功耗熄屏前避免残影
void lcd_clear_screen_black(void) {
if (panel_handle == NULL) {
ESP_LOGE(LCD_TAG, "Panel handle is NULL, cannot clear screen");
return;
}
size_t clear_buffer_size = LCD_WID * 40;
uint16_t *clear_buffer = heap_caps_malloc(clear_buffer_size * sizeof(uint16_t), MALLOC_CAP_DMA);
if (clear_buffer) {
memset(clear_buffer, 0, clear_buffer_size * sizeof(uint16_t));
for (int y = 0; y < LCD_HIGH; y += 40) {
int lines = (y + 40 > LCD_HIGH) ? (LCD_HIGH - y) : 40;
esp_lcd_panel_draw_bitmap(panel_handle, 0, y, LCD_WID, y + lines, clear_buffer);
}
heap_caps_free(clear_buffer);
ESP_LOGI(LCD_TAG, "LCD GRAM cleared to black (for low power mode)");
} else {
ESP_LOGE(LCD_TAG, "Failed to allocate clear buffer");
}
}