Rdzleo 31982ba7b9 feat(ui): Phase 10 - 数字人模式 LVGL → esp_emote_gfx 完整切换
 验证完成:
- 音频卡顿明显改善(用户实测)
- 数字人 hiyori 动画正常显示
- nm 验证:固件中 0 个 lv_*/lvgl_* 函数符号
- kapi.bin: 4.7MB → 2.75MB(-42%)

关键改动:
- main/dzbj/ai_chat_ui_eaf.c (404 行新增):
  完全替代 LVGL 版 ai_chat_ui.c,提供同名 C API(ai_chat_screen_init
  / set_status / set_emotion / set_chat_message / resume_animation)。
  AiChatDisplay C++ 桥接层无需改动。
  内部用 gfx_emote_init + gfx_disp_add + gfx_anim + mmap_assets。
- main/CMakeLists.txt:双轨编译
  CONFIG_BAJI_BADGE_MODE=y → ai_chat_ui.c (LVGL) + bg_gif_demo.c
  CONFIG_BAJI_BADGE_MODE=n → ai_chat_ui_eaf.c (esp_emote_gfx)
- main/dzbj/dzbj_init.c:EAF 模式跳过 lvgl_lcd_init() 调用
- main/dzbj/lcd.c/h:暴露 lcd_io_handle 给 EAF 注册 IO 完成回调

踩坑修复(commit message 留档供后续参考):
1. esp_mmap_assets v2.0.0 在 use_fs=true 模式下 mmap_assets_get_mem()
   返回的是文件内偏移量而非 RAM 指针(fseek bug + offset 没加
   data_section_start),导致 LoadProhibited panic。
   解决:完全绕过 mmap_assets,自己 fopen + 解析 MMAP bin 头
   (layout: 头 16B + 每 entry 28B + data 段每文件 2B magic + 数据)。
2. esp_emote_gfx 期望 esp_lcd_touch v2.x 新 API,项目用 v1.1.2 旧 API。
   在 managed_components 内 gfx_touch.c 加 shim 桥接(local patch,
   reconfigure 后需 reapply)。
3. EAF format magic 是 0x89 'EAF'(gfx_eaf_dec.h),不是 0x5A5A
   (那是 esp_mmap_assets 内部文件分隔符)。
4. SPIFFS 需要在 ai_chat_screen_init 入口自动挂载(不能依赖
   bg_gif_demo 的惰性挂载,那个已被 CONFIG 排除)。

依赖增量:
- espressif2022/esp_emote_gfx: ~3.0.5
- espressif/esp_mmap_assets: * (仅用于声明依赖,运行时被绕过)

数字人模式核心 UI 范围:
- 显示数字人动画  (hiyori_m06/m07, 居中循环)
- 情绪 → GIF 映射  (23 情绪 → 2 EAF,sad/angry 暂用 m07,m03 待补)
- 字幕/状态文字: stub (字体接驳留待后续,需打包 .bin 字体到资源)
- 触摸: 不支持(PoC 阶段不需要)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:53:21 +08:00

459 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; // 仅文件内使用
esp_lcd_panel_io_handle_t lcd_io_handle = NULL; // Phase 10: 暴露给 EAF UI 注册 IO 完成回调lcd_init 后赋值)
#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));
lcd_io_handle = io_handle; // Phase 10: 同步给 EAF UI 使用
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 = 16 // 卡顿优化 4: 5ms→16ms (60Hz) 减少 LVGL CPU 占用 60%
};
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");
}
}