✅ 验证完成: - 音频卡顿明显改善(用户实测) - 数字人 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>
459 lines
17 KiB
C
459 lines
17 KiB
C
#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 = 成功等到 VBLANK,false = 超时或读取失败
|
||
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");
|
||
}
|
||
}
|