From 9223fd5a7d3350c28b8b57b98005aab3437548b9 Mon Sep 17 00:00:00 2001 From: Rdzleo Date: Fri, 27 Feb 2026 09:43:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20BLE=E4=BC=A0=E8=BE=93=E4=BC=98=E5=8C=96?= =?UTF-8?q?=20+=20GIF=E6=9D=A1=E4=BB=B6=E7=BC=96=E8=AF=91=20+=20=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89GIF=E6=92=AD=E6=94=BE=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一、BLE 蓝牙优化 - 设备名称改为动态名称 Airhub_MAC(基于BLE MAC地址) - 广播数据拆分为 ADV + Scan Response 两包 - 图片接收完成后数据直通显示(跳过SPIFFS重读,减少200-500ms延迟) - BLE耗时操作(NVS写入+导航显示)转移到独立FreeRTOS任务,避免BTC_TASK栈溢出 - 缩短BLE连接间隔(min=7.5ms, max=20ms),提升传输吞吐量 - 减少传输日志输出(每100包打印一次),提升传输速度 二、显示性能优化 - LVGL绘制缓冲区从DMA 30行改为PSRAM 120行大缓冲,减少flush次数 - CPU最大频率从160MHz提升到240MHz,提升解码性能 三、GIF动图支持(条件编译,当前默认关闭) - 实现自定义GIF播放器:Palette LUT查表 + TRUE_COLOR无Alpha + 后台线程解码流水线 - 使用 #if LV_USE_GIF 条件编译包裹所有GIF代码,sdkconfig中CONFIG_LV_USE_GIF=n时零开销 - 启用GIF时需设置 CONFIG_LV_USE_GIF=y 即可 四、图片管理优化 - BLE接收新图片后直接追加到列表(避免重扫SPIFFS目录) - SPIFFS图片扫描支持.gif扩展名(条件编译控制) 五、文档更新 - 设备运行日志:GIF性能瓶颈分析与优化方案 Co-Authored-By: Claude Opus 4.6 --- main/ble/ble.c | 110 +++++-- main/lcd/lcd.c | 7 +- main/main.c | 4 +- main/pages/include/pages.h | 5 + main/pages/pages.c | 505 ++++++++++++++++++++++++++++++--- main/ui/screens/ui_ScreenImg.c | 13 +- sdkconfig | 44 ++- 设备运行日志.md | 266 +++++++++++++++++ 8 files changed, 868 insertions(+), 86 deletions(-) diff --git a/main/ble/ble.c b/main/ble/ble.c index 716c2a0..858f342 100644 --- a/main/ble/ble.c +++ b/main/ble/ble.c @@ -1,6 +1,8 @@ #include #include #include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" #include "esp_log.h" #include "esp_bt.h" #include "esp_gap_ble_api.h" @@ -29,7 +31,8 @@ static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_ static const char *CONN_TAG = "CONN_BLE"; -static const char device_name[] = "MY-BLE"; +static char ble_device_name[32]; +static uint8_t adv_raw_len = 0; static uint16_t conn_id; @@ -53,6 +56,22 @@ MegStatus SendStatus = {false,0}; uint8_t *img_data = 0; FILE *file_img; +// BLE 图片处理任务(NVS 写入 + 导航显示在独立任务中执行,避免 BTC_TASK 栈溢出) +static TaskHandle_t ble_process_task_handle = NULL; +static char ble_pending_filename[24]; +static uint8_t *ble_pending_data = NULL; // 传输完成的图片数据(直通显示,跳过 SPIFFS 重读) +static size_t ble_pending_data_size = 0; + +static void ble_process_task(void *arg) { + while (1) { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + nvs_change_img(ble_pending_filename); + ble_image_navigate_with_data(ble_pending_filename, ble_pending_data, ble_pending_data_size); + ble_pending_data = NULL; // 所有权已转移,不再释放 + ble_pending_data_size = 0; + } +} + static uint8_t attr_value_write[512] = {0}; static uint8_t attr_value_edit[20] = {0}; @@ -101,12 +120,12 @@ static esp_ble_adv_params_t adv_params = { }; -static uint8_t adv_raw_data[] = { - 0x02, ESP_BLE_AD_TYPE_FLAG, 0x06, - 0x07, ESP_BLE_AD_TYPE_NAME_CMPL, 'M','Y','-','B','L','E', - 0x02, ESP_BLE_AD_TYPE_TX_PWR, 0x09, - 0x03, ESP_BLE_AD_TYPE_16SRV_CMPL, 0xB0, 0x00, - 0x07, ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE,0x4C,0x44,0x64,0x7A,0x62,0x6A +static uint8_t adv_raw_data[31]; + +// Scan Response 数据:厂商标识 + 服务UUID +static uint8_t scan_rsp_data[] = { + 0x07, ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE, 0x4C, 0x44, 0x64, 0x7A, 0x62, 0x6A, // "LDdzbj" + 0x03, ESP_BLE_AD_TYPE_16SRV_CMPL, 0x00, 0x0B, // 服务UUID 0x0B00 }; @@ -156,15 +175,54 @@ void ble_init(void) ESP_LOGE(CONN_TAG, "set local MTU failed, error code = %x", ret); return; } - ret = esp_ble_gap_set_device_name(device_name); + + // 获取 BLE MAC 地址并构建设备名称: Airhub_xx:xx:xx:xx:xx:xx + const uint8_t *ble_addr = esp_bt_dev_get_address(); + if (ble_addr) { + snprintf(ble_device_name, sizeof(ble_device_name), + "Airhub_%02x:%02x:%02x:%02x:%02x:%02x", + ble_addr[0], ble_addr[1], ble_addr[2], + ble_addr[3], ble_addr[4], ble_addr[5]); + ESP_LOGI(CONN_TAG, "BLE MAC: %02x:%02x:%02x:%02x:%02x:%02x", + ble_addr[0], ble_addr[1], ble_addr[2], + ble_addr[3], ble_addr[4], ble_addr[5]); + } else { + strcpy(ble_device_name, "Airhub_BLE"); + ESP_LOGW(CONN_TAG, "获取BLE MAC失败,使用默认名称: %s", ble_device_name); + } + + ret = esp_ble_gap_set_device_name(ble_device_name); if (ret) { ESP_LOGE(CONN_TAG, "set device name failed, error code = %x", ret); return; } - ret = esp_ble_gap_config_adv_data_raw(adv_raw_data, sizeof(adv_raw_data)); + ESP_LOGI(CONN_TAG, "蓝牙设备名称: %s", ble_device_name); + + // 构建广播数据: Flags + Complete Local Name + uint8_t name_len = strlen(ble_device_name); + int offset = 0; + adv_raw_data[offset++] = 0x02; + adv_raw_data[offset++] = ESP_BLE_AD_TYPE_FLAG; + adv_raw_data[offset++] = 0x06; + adv_raw_data[offset++] = name_len + 1; + adv_raw_data[offset++] = ESP_BLE_AD_TYPE_NAME_CMPL; + memcpy(&adv_raw_data[offset], ble_device_name, name_len); + offset += name_len; + adv_raw_len = offset; + + ret = esp_ble_gap_config_adv_data_raw(adv_raw_data, adv_raw_len); if (ret) { ESP_LOGE(CONN_TAG, "config adv data failed, error code = %x", ret); } + + // 配置 Scan Response 数据(厂商标识 "dzbj" + 服务UUID) + ret = esp_ble_gap_config_scan_rsp_data_raw(scan_rsp_data, sizeof(scan_rsp_data)); + if (ret) { + ESP_LOGE(CONN_TAG, "config scan response data failed, error code = %x", ret); + } + + // 创建图片处理任务(8KB 栈,足够 SPIFFS 扫描 + LVGL + GIF 解码) + xTaskCreate(ble_process_task, "ble_img", 8192, NULL, 5, &ble_process_task_handle); } @@ -174,6 +232,10 @@ static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *par switch (event) { case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: ESP_LOGI(CONN_TAG, "Advertising data set, status %d", param->adv_data_raw_cmpl.status); + // ADV 数据设置完成,等待 Scan Response 也设置完成后再开始广播 + break; + case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT: + ESP_LOGI(CONN_TAG, "Scan response data set, status %d", param->scan_rsp_data_raw_cmpl.status); esp_ble_gap_start_advertising(&adv_params); break; case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: @@ -268,9 +330,12 @@ static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_ ESP_LOGI(CONN_TAG,"传输通道建立成功,数据指针:%p,文件名称:%s,文件大小:%d",img_data,firstMeg.filename,(int)firstMeg.len); } }else if(SendStatus.isSend){ - ESP_LOGI(CONN_TAG, "获取到数据:第:%d包,长度:%d,是否结束:%d",*value+1,(int)param->write.len,*(value+1)); + uint8_t pkt_no = *value; uint8_t isEnd = *(value + 1); - uint8_t port = *(value); + // 每 100 包或最后一包打印日志(减少串口输出提升传输速度) + if (pkt_no % 100 == 0 || isEnd) { + ESP_LOGI(CONN_TAG, "获取到数据:第:%d包,长度:%d,是否结束:%d", pkt_no+1, (int)param->write.len, isEnd); + } uint8_t *data = value + 2; memcpy(img_data + SendStatus.port,data,(int)param->write.len-2); SendStatus.port += param->write.len-2; @@ -281,12 +346,15 @@ static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_ fclose(file_img); SendStatus.isSend = false; SendStatus.port = 0; - ESP_LOGI(CONN_TAG,"图片接收成功"); - nvs_change_img(firstMeg.filename); - // 导航到ScreenImg显示新图片(内部刷新列表+设置索引+切换界面) - ble_image_navigate(firstMeg.filename); - free(img_data); + // img_data 不释放,传给显示任务直通显示(跳过 SPIFFS 重读) + ble_pending_data = img_data; + ble_pending_data_size = firstMeg.len; + img_data = NULL; // 转移所有权 free(filepath); + ESP_LOGI(CONN_TAG,"图片接收成功,数据直通显示(%d字节)", (int)ble_pending_data_size); + strncpy(ble_pending_filename, firstMeg.filename, sizeof(ble_pending_filename) - 1); + ble_pending_filename[sizeof(ble_pending_filename) - 1] = '\0'; + xTaskNotifyGive(ble_process_task_handle); } } }// 图片编辑特征写入事件 @@ -296,8 +364,10 @@ static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_ uint8_t type = *(value + param->write.len - 1); memcpy(imgName, value, 23); if(type == 0xff){ - nvs_change_img(imgName); - ble_image_navigate(imgName); + // 耗时操作转移到独立任务执行 + strncpy(ble_pending_filename, imgName, sizeof(ble_pending_filename) - 1); + ble_pending_filename[sizeof(ble_pending_filename) - 1] = '\0'; + xTaskNotifyGive(ble_process_task_handle); }else if(type == 0xF1){ remove(filepath); SendStatus.isSend = false; @@ -311,8 +381,8 @@ static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_ esp_ble_conn_update_params_t conn_params = {0}; memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t)); conn_params.latency = 0; - conn_params.max_int = 32; - conn_params.min_int = 16; + conn_params.max_int = 16; // 16 × 1.25ms = 20ms(缩短连接间隔提升传输吞吐量) + conn_params.min_int = 6; // 6 × 1.25ms = 7.5ms conn_params.timeout = 400; conn_id = param->connect.conn_id; ESP_LOGI(CONN_TAG, "Connected, conn_id %u, remote "ESP_BD_ADDR_STR"", diff --git a/main/lcd/lcd.c b/main/lcd/lcd.c index d001227..159e229 100644 --- a/main/lcd/lcd.c +++ b/main/lcd/lcd.c @@ -109,12 +109,12 @@ void lvgl_lcd_init(){ }; lvgl_port_init(&lvgl_cfg); - #define LVGL_DRAW_BUF_LINES 30 + #define LVGL_DRAW_BUF_LINES 120 size_t buffer_size = LCD_WID * LVGL_DRAW_BUF_LINES; ESP_LOGI(LCD_TAG, "LVGL buffer size: %d bytes (W: %d, Lines: %d)", buffer_size * 2, LCD_WID, LVGL_DRAW_BUF_LINES); - + const lvgl_port_display_cfg_t disp_cfg = { .io_handle = io_handle, .panel_handle = panel_handle, @@ -129,7 +129,8 @@ void lvgl_lcd_init(){ .mirror_y = false,// 垂直镜像 }, .flags = { - .buff_dma = true,// 使用DMA传输显示缓冲区 + .buff_dma = false, + .buff_spiram = true,// PSRAM 120行大缓冲,每帧仅3次flush,GIF最流畅 } }; disp_handle = lvgl_port_add_disp(&disp_cfg); diff --git a/main/main.c b/main/main.c index 76b3b17..206c7d1 100644 --- a/main/main.c +++ b/main/main.c @@ -213,13 +213,13 @@ void app_main(void) // 配置 Power Management(低功耗管理) esp_pm_config_t pm_config = { - .max_freq_mhz = 160, // 最大频率 160MHz(与当前CPU频率一致) + .max_freq_mhz = 240, // 最大频率 240MHz(GIF解码需要高算力) .min_freq_mhz = 40, // 最小频率 40MHz(保证LVGL正常刷新) .light_sleep_enable = true // 启用自动 Light Sleep }; esp_err_t pm_err = esp_pm_configure(&pm_config); if (pm_err == ESP_OK) { - ESP_LOGI("MAIN", "2.1 Power Management已启用:40-160MHz动态频率 + 自动Light Sleep"); + ESP_LOGI("MAIN", "2.1 Power Management已启用:40-240MHz动态频率 + 自动Light Sleep"); } else { ESP_LOGW("MAIN", "2.1 Power Management启用失败:%s", esp_err_to_name(pm_err)); } diff --git a/main/pages/include/pages.h b/main/pages/include/pages.h index 6d457b6..a747d29 100644 --- a/main/pages/include/pages.h +++ b/main/pages/include/pages.h @@ -1,6 +1,7 @@ #include "esp_err.h" #include #include +#include "lvgl.h" void app_test_display(); // 测试显示 void app_img_display(); // 显示图片 @@ -19,3 +20,7 @@ void init_spiffs_image_list(void); // 初始化/扫描SPIFFS图片列表 void free_spiffs_image_list(void); // 重置图片列表 bool set_image_index_by_name(const char *name); // 根据文件名设置当前图片索引 void ble_image_navigate(const char *filename); // BLE接收后导航到ScreenImg显示 +void ble_image_navigate_with_data(const char *filename, uint8_t *data, size_t data_size); // BLE接收后直通显示(跳过SPIFFS重读) +#if LV_USE_GIF +void pages_cleanup_gif(void); // 清理 GIF 控件资源 +#endif diff --git a/main/pages/pages.c b/main/pages/pages.c index e0c0669..121eb38 100644 --- a/main/pages/pages.c +++ b/main/pages/pages.c @@ -1,4 +1,11 @@ #include "lvgl.h" +#if LV_USE_GIF +#include "extra/libs/gif/lv_gif.h" +#include "extra/libs/gif/gifdec.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#endif +#include "esp_heap_caps.h" #include "fatfs.h" #include "driver/ledc.h" #include "gpio.h" @@ -29,6 +36,29 @@ static int spiffs_image_count = 0; static int current_image_index = 0; static bool image_list_initialized = false; +#if LV_USE_GIF +// === 自定义 GIF 播放器(替代 lv_gif,性能优化) === +// 优化: 1.Palette LUT查表 2.TRUE_COLOR无Alpha 3.后台线程解码流水线 +static gd_GIF *gif_decoder = NULL; // gifdec 解码句柄 +static lv_obj_t *gif_img_obj = NULL; // 普通 lv_img 控件(替代 lv_gif) +static uint16_t *gif_rgb565_buf[2] = {NULL, NULL}; // 双缓冲 RGB565 帧 (PSRAM) +static lv_img_dsc_t gif_frame_dsc; // LVGL 图片描述符(TRUE_COLOR,无Alpha) +static uint16_t gif_palette_lut[256]; // 调色板 RGB565 查找表 +static volatile uint8_t gif_front_idx = 0; // 当前显示的缓冲区索引 +static lv_timer_t *gif_play_timer = NULL; // LVGL 播放定时器 +static TaskHandle_t gif_decode_task_handle = NULL; // 后台解码任务句柄 +static volatile bool gif_playing = false; // 播放状态标志 +static volatile bool gif_new_frame_ready = false; // 新帧就绪标志 +static uint32_t gif_last_frame_ms = 0; // 上一帧显示时间戳 +static uint8_t *gif_psram_buf = NULL; // GIF 文件数据(PSRAM) +static bool current_is_gif = false; // 当前是否为 GIF 模式 + +// 前向声明 +static bool is_gif_file(const char *filename); +static void gif_player_start(void); +static void gif_player_stop(void); +#endif // LV_USE_GIF + // 当前亮度值(用于休眠恢复) static uint8_t current_brightness = 50; @@ -600,7 +630,11 @@ void init_spiffs_image_list(void) { if(len > 4 && len < MAX_FILENAME_LEN) { const char *ext = name + len - 4; if(strcasecmp(ext, ".jpg") == 0 || strcasecmp(ext, ".jpeg") == 0 || - strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".bmp") == 0) { + strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".bmp") == 0 +#if LV_USE_GIF + || strcasecmp(ext, ".gif") == 0 +#endif + ) { // 存储图片文件名到静态缓冲区 strncpy(spiffs_image_files[spiffs_image_count], name, MAX_FILENAME_LEN - 1); spiffs_image_files[spiffs_image_count][MAX_FILENAME_LEN - 1] = '\0'; @@ -686,12 +720,25 @@ bool set_image_index_by_name(const char *name) { // BLE接收图片后导航到ScreenImg显示 void ble_image_navigate(const char *filename) { - // 刷新图片列表 - free_spiffs_image_list(); - init_spiffs_image_list(); - - // 设置当前索引为新接收的图片 - set_image_index_by_name(filename); + // 将新文件直接追加到列表(避免重扫 SPIFFS 目录,节省 ~200ms) + if (!image_list_initialized) { + init_spiffs_image_list(); + } + // 检查文件是否已在列表中(避免重复) + bool found = false; + for (int i = 0; i < spiffs_image_count; i++) { + if (strcmp(spiffs_image_files[i], filename) == 0) { + current_image_index = i; + found = true; + break; + } + } + if (!found && spiffs_image_count < MAX_IMAGE_FILES) { + strncpy(spiffs_image_files[spiffs_image_count], filename, MAX_FILENAME_LEN - 1); + spiffs_image_files[spiffs_image_count][MAX_FILENAME_LEN - 1] = '\0'; + current_image_index = spiffs_image_count; + spiffs_image_count++; + } // 检查是否已在ScreenImg界面 lvgl_port_lock(0); @@ -710,6 +757,82 @@ void ble_image_navigate(const char *filename) { ESP_LOGI("IMG_LIST", "BLE导航到ScreenImg显示: %s", filename); } +// BLE接收图片后导航显示(携带预加载数据,跳过 SPIFFS 重读) +void ble_image_navigate_with_data(const char *filename, uint8_t *data, size_t data_size) { + // 将新文件追加到列表 + if (!image_list_initialized) { + init_spiffs_image_list(); + } + bool found = false; + for (int i = 0; i < spiffs_image_count; i++) { + if (strcmp(spiffs_image_files[i], filename) == 0) { + current_image_index = i; + found = true; + break; + } + } + if (!found && spiffs_image_count < MAX_IMAGE_FILES) { + strncpy(spiffs_image_files[spiffs_image_count], filename, MAX_FILENAME_LEN - 1); + spiffs_image_files[spiffs_image_count][MAX_FILENAME_LEN - 1] = '\0'; + current_image_index = spiffs_image_count; + spiffs_image_count++; + } + +#if LV_USE_GIF + // 如果有预加载数据且是 GIF,直接用内存数据显示(跳过 SPIFFS 重读) + if (data && data_size > 0 && is_gif_file(filename)) { + // 停止旧 GIF 播放器 + gif_player_stop(); + if (gif_psram_buf) { + free(gif_psram_buf); + } + // BLE 数据直接作为 GIF 源(所有权转移) + gif_psram_buf = data; + + // 打开 gifdec 解码器(从 PSRAM 内存源) + gif_decoder = gd_open_gif_data(gif_psram_buf); + if (!gif_decoder) { + ESP_LOGE("GIF", "gifdec 打开失败"); + free(gif_psram_buf); + gif_psram_buf = NULL; + return; + } + + // 确保在 ScreenImg 界面 + lvgl_port_lock(0); + bool already_on_screen = (lv_scr_act() == ui_ScreenImg); + if (!already_on_screen) { + _ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init); + } + lvgl_port_unlock(); + + // 启动自定义 GIF 播放器 + gif_player_start(); + + ESP_LOGI("IMG_LIST", "BLE GIF直通显示(优化): %s", filename); + return; + } +#endif // LV_USE_GIF + + // 非 GIF 或无预加载数据,释放 BLE 数据,走常规 SPIFFS 路径 + if (data) { + free(data); + } + + lvgl_port_lock(0); + bool already_on_screen = (lv_scr_act() == ui_ScreenImg); + if (!already_on_screen) { + _ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init); + } + lvgl_port_unlock(); + + if (already_on_screen) { + update_ui_ImgBle(filename); + } + + ESP_LOGI("IMG_LIST", "BLE导航到ScreenImg显示: %s", filename); +} + // 获取当前图片文件名 const char* get_current_image(void) { if(!image_list_initialized || spiffs_image_count == 0) { @@ -770,32 +893,255 @@ bool delete_current_image(void) { return true; } -// 更新ui_ImgBle控件的图片 +#if LV_USE_GIF +// 判断文件是否为 GIF 格式 +static bool is_gif_file(const char *filename) { + int len = strlen(filename); + if (len < 4) return false; + return strcasecmp(filename + len - 4, ".gif") == 0; +} + +// === GIF 播放器内部函数 === + +// 构建调色板 RGB565 查找表(每次帧解码前调用,处理局部调色板) +static void gif_build_palette_lut(gd_Palette *palette) { + for (int i = 0; i < palette->size; i++) { + uint8_t r = palette->colors[i * 3 + 0]; + uint8_t g = palette->colors[i * 3 + 1]; + uint8_t b = palette->colors[i * 3 + 2]; + lv_color_t c = lv_color_make(r, g, b); // 自动处理 LV_COLOR_16_SWAP + gif_palette_lut[i] = c.full; + } +} + +// 快速渲染帧到 canvas(palette LUT 替代逐像素 lv_color_make) +static void gif_render_frame_fast(gd_GIF *gif) { + int i = gif->fy * gif->width + gif->fx; + for (int j = 0; j < gif->fh; j++) { + for (int k = 0; k < gif->fw; k++) { + uint8_t index = gif->frame[(gif->fy + j) * gif->width + gif->fx + k]; + if (!gif->gce.transparency || index != gif->gce.tindex) { + uint16_t c = gif_palette_lut[index]; + gif->canvas[(i + k) * 3 + 0] = c & 0xff; + gif->canvas[(i + k) * 3 + 1] = (c >> 8) & 0xff; + gif->canvas[(i + k) * 3 + 2] = 0xff; + } + } + i += gif->width; + } +} + +// canvas (RGB565+Alpha 3字节/像素) → RGB565 (2字节/像素) 快速拷贝 +static void gif_canvas_to_rgb565(gd_GIF *gif, uint16_t *out) { + uint8_t *canvas = gif->canvas; + int total = gif->width * gif->height; + for (int i = 0; i < total; i++) { + out[i] = (uint16_t)canvas[i * 3] | ((uint16_t)canvas[i * 3 + 1] << 8); + } +} + +// 后台解码任务(FreeRTOS,与 LVGL 显示并行) +static void gif_decode_task(void *pvParameters) { + while (1) { + // 等待解码请求 + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + if (!gif_playing || !gif_decoder) break; + + // LZW 解码(含 dispose 处理) + int ret = gd_get_frame(gif_decoder); + if (ret == 0) { + // GIF 循环播放 + gd_rewind(gif_decoder); + ret = gd_get_frame(gif_decoder); + } + if (ret < 0 || !gif_playing) break; + + // 更新调色板 LUT(可能有局部调色板变化) + gif_build_palette_lut(gif_decoder->palette); + + // 快速渲染到 canvas + 拷贝到后台 RGB565 缓冲 + gif_render_frame_fast(gif_decoder); + uint8_t back = 1 - gif_front_idx; + gif_canvas_to_rgb565(gif_decoder, gif_rgb565_buf[back]); + + // 标记新帧就绪 + gif_new_frame_ready = true; + } + + gif_decode_task_handle = NULL; + vTaskDelete(NULL); +} + +// LVGL 定时器回调(检查新帧并切换显示) +static void gif_play_timer_cb(lv_timer_t *t) { + if (!gif_playing || !gif_decoder || !gif_img_obj) return; + + // 检查 GIF 帧延时 + uint32_t delay_ms = gif_decoder->gce.delay * 10; + if (delay_ms < 20) delay_ms = 20; + + uint32_t elapsed = lv_tick_elaps(gif_last_frame_ms); + if (elapsed < delay_ms) return; + + if (!gif_new_frame_ready) return; + + gif_last_frame_ms = lv_tick_get(); + gif_new_frame_ready = false; + + // 切换前后缓冲 + uint8_t back = 1 - gif_front_idx; + gif_front_idx = back; + + // 更新 LVGL 图片源(TRUE_COLOR,无 Alpha 混合) + gif_frame_dsc.data = (uint8_t *)gif_rgb565_buf[gif_front_idx]; + lv_img_cache_invalidate_src(&gif_frame_dsc); + lv_obj_invalidate(gif_img_obj); + + // 通知后台线程解码下一帧 + if (gif_decode_task_handle) { + xTaskNotifyGive(gif_decode_task_handle); + } +} + +// 启动自定义 GIF 播放器 +static void gif_player_start(void) { + if (!gif_decoder || !gif_psram_buf) return; + + uint16_t w = gif_decoder->width; + uint16_t h = gif_decoder->height; + size_t buf_size = w * h * sizeof(uint16_t); + + // 分配双缓冲 RGB565 帧 (PSRAM) + for (int i = 0; i < 2; i++) { + if (!gif_rgb565_buf[i]) { + gif_rgb565_buf[i] = heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM); + if (!gif_rgb565_buf[i]) { + ESP_LOGE("GIF", "RGB565 缓冲分配失败: %d", i); + return; + } + } + memset(gif_rgb565_buf[i], 0, buf_size); + } + + // 初始化调色板 LUT + gif_build_palette_lut(gif_decoder->palette); + + // 同步解码第一帧(确保立即显示) + int ret = gd_get_frame(gif_decoder); + if (ret <= 0) { + ESP_LOGE("GIF", "首帧解码失败"); + return; + } + gif_render_frame_fast(gif_decoder); + gif_canvas_to_rgb565(gif_decoder, gif_rgb565_buf[0]); + gif_front_idx = 0; + gif_new_frame_ready = false; + gif_last_frame_ms = lv_tick_get(); + + // 配置 LVGL 图片描述符(TRUE_COLOR,无 Alpha) + gif_frame_dsc.header.cf = LV_IMG_CF_TRUE_COLOR; + gif_frame_dsc.header.always_zero = 0; + gif_frame_dsc.header.reserved = 0; + gif_frame_dsc.header.w = w; + gif_frame_dsc.header.h = h; + gif_frame_dsc.data_size = buf_size; + gif_frame_dsc.data = (uint8_t *)gif_rgb565_buf[0]; + + // 创建 lv_img 控件 + LVGL 播放定时器 + lvgl_port_lock(0); + lv_obj_add_flag(ui_ImgBle, LV_OBJ_FLAG_HIDDEN); + gif_img_obj = lv_img_create(ui_ScreenImg); + lv_obj_set_size(gif_img_obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_align(gif_img_obj, LV_ALIGN_CENTER); + lv_obj_clear_flag(gif_img_obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_move_background(gif_img_obj); + lv_img_set_src(gif_img_obj, &gif_frame_dsc); + gif_play_timer = lv_timer_create(gif_play_timer_cb, 10, NULL); + lvgl_port_unlock(); + + // 启动后台解码任务 + gif_playing = true; + current_is_gif = true; + xTaskCreatePinnedToCore(gif_decode_task, "gif_dec", 4096, NULL, 5, &gif_decode_task_handle, 1); + + // 触发后台解码第2帧 + xTaskNotifyGive(gif_decode_task_handle); + + ESP_LOGI("GIF", "播放器启动: %dx%d, 双缓冲 %dKB×2", w, h, buf_size / 1024); +} + +// 停止自定义 GIF 播放器 +static void gif_player_stop(void) { + // 停止后台解码任务 + gif_playing = false; + if (gif_decode_task_handle) { + xTaskNotifyGive(gif_decode_task_handle); + // 等待任务退出(最多 500ms) + for (int i = 0; i < 50 && gif_decode_task_handle; i++) { + vTaskDelay(pdMS_TO_TICKS(10)); + } + } + + // 删除 LVGL 定时器和控件 + lvgl_port_lock(0); + if (gif_play_timer) { + lv_timer_del(gif_play_timer); + gif_play_timer = NULL; + } + if (gif_img_obj) { + lv_obj_del(gif_img_obj); + gif_img_obj = NULL; + } + lvgl_port_unlock(); + + // 关闭 gifdec 解码器 + if (gif_decoder) { + gd_close_gif(gif_decoder); + gif_decoder = NULL; + } + + // 释放 RGB565 双缓冲 + for (int i = 0; i < 2; i++) { + if (gif_rgb565_buf[i]) { + free(gif_rgb565_buf[i]); + gif_rgb565_buf[i] = NULL; + } + } + + current_is_gif = false; + gif_new_frame_ready = false; +} + +// 清理 GIF 资源(公开接口,供界面切换时调用) +void pages_cleanup_gif(void) { + gif_player_stop(); + // 释放 GIF 源文件缓冲区 + if (gif_psram_buf) { + free(gif_psram_buf); + gif_psram_buf = NULL; + } +} +#endif // LV_USE_GIF + +// 更新ui_ImgBle控件的图片(支持 JPEG) void update_ui_ImgBle(const char *img_name) { if(!img_name) { ESP_LOGE("IMG_UI", "图片名为空"); return; } - + if(!ui_ImgBle) { ESP_LOGE("IMG_UI", "ui_ImgBle控件不存在"); return; } - + static uint8_t *ui_img_data = NULL; static lv_img_dsc_t ui_image; - - // 释放之前的图片数据 - if(ui_img_data) { - free(ui_img_data); - ui_img_data = NULL; - ESP_LOGI("IMG_UI", "释放之前的图片数据"); - } - + // 构建图片路径 snprintf(img_path, sizeof(img_path), "/spiflash/%s", img_name); ESP_LOGI("IMG_UI", "准备显示图片: %s, 路径: %s", img_name, img_path); - + // 检查文件是否存在 struct stat file_stat; if(stat(img_path, &file_stat) != 0) { @@ -803,37 +1149,104 @@ void update_ui_ImgBle(const char *img_name) { return; } ESP_LOGI("IMG_UI", "文件大小: %ld 字节", file_stat.st_size); - - // 解码图片 - esp_jpeg_image_output_t ui_outdata; - esp_err_t ret = DecodeImg(img_path, &ui_img_data, &ui_outdata); - if(ret == ESP_OK) { - ESP_LOGI("IMG_UI", "图片解码成功,宽度: %d, 高度: %d", ui_outdata.width, ui_outdata.height); - - // 检查解码后的数据 - if(ui_img_data == NULL) { - ESP_LOGE("IMG_UI", "解码数据为空"); + +#if LV_USE_GIF + if (is_gif_file(img_name)) { + // === GIF 显示路径(自定义播放器:Palette LUT + 无Alpha + 后台解码) === + // 释放之前的 JPEG 数据 + if(ui_img_data) { + free(ui_img_data); + ui_img_data = NULL; + } + + // 停止旧 GIF 播放器 + 释放旧 GIF 数据 + gif_player_stop(); + if (gif_psram_buf) { + free(gif_psram_buf); + gif_psram_buf = NULL; + } + + // 将 GIF 文件整体读入 PSRAM + FILE *gif_file = fopen(img_path, "rb"); + if (!gif_file) { + ESP_LOGE("IMG_UI", "GIF文件打开失败: %s", img_path); return; } - - // 配置图片数据 - ui_image.header.cf = LV_IMG_CF_TRUE_COLOR; - ui_image.header.always_zero = 0; - ui_image.header.reserved = 0; - ui_image.header.w = ui_outdata.width; - ui_image.header.h = ui_outdata.height; - ui_image.data_size = ui_outdata.output_len; - ui_image.data = ui_img_data; - - // 更新ui_ImgBle控件的图片 + gif_psram_buf = heap_caps_malloc(file_stat.st_size, MALLOC_CAP_SPIRAM); + if (!gif_psram_buf) { + ESP_LOGE("IMG_UI", "PSRAM分配失败: %ld 字节", file_stat.st_size); + fclose(gif_file); + return; + } + fread(gif_psram_buf, 1, file_stat.st_size, gif_file); + fclose(gif_file); + + // 打开 gifdec 解码器(从 PSRAM 内存源) + gif_decoder = gd_open_gif_data(gif_psram_buf); + if (!gif_decoder) { + ESP_LOGE("IMG_UI", "gifdec 打开失败: %s", img_name); + free(gif_psram_buf); + gif_psram_buf = NULL; + return; + } + + // 启动自定义 GIF 播放器(Palette LUT + 双缓冲流水线) + gif_player_start(); + + ESP_LOGI("IMG_UI", "GIF显示启动(优化): %s", img_name); + } else +#endif // LV_USE_GIF + { + // === JPEG 显示路径 === +#if LV_USE_GIF + // 停止 GIF 播放器并释放资源 + gif_player_stop(); + if (gif_psram_buf) { + free(gif_psram_buf); + gif_psram_buf = NULL; + } + // 恢复 JPEG 控件显示 lvgl_port_lock(0); - lv_img_set_src(ui_ImgBle, &ui_image); + lv_obj_clear_flag(ui_ImgBle, LV_OBJ_FLAG_HIDDEN); lvgl_port_unlock(); - - ESP_LOGI("IMG_UI", "ui_ImgBle图片更新成功: %s", img_name); - } else { - ESP_LOGE("IMG_UI", "图片解码失败,错误码: %d", ret); - ui_img_data = NULL; +#endif + + // 释放之前的图片数据 + if(ui_img_data) { + free(ui_img_data); + ui_img_data = NULL; + ESP_LOGI("IMG_UI", "释放之前的图片数据"); + } + + // 解码图片 + esp_jpeg_image_output_t ui_outdata; + esp_err_t ret = DecodeImg(img_path, &ui_img_data, &ui_outdata); + if(ret == ESP_OK) { + ESP_LOGI("IMG_UI", "图片解码成功,宽度: %d, 高度: %d", ui_outdata.width, ui_outdata.height); + + if(ui_img_data == NULL) { + ESP_LOGE("IMG_UI", "解码数据为空"); + return; + } + + // 配置图片数据 + ui_image.header.cf = LV_IMG_CF_TRUE_COLOR; + ui_image.header.always_zero = 0; + ui_image.header.reserved = 0; + ui_image.header.w = ui_outdata.width; + ui_image.header.h = ui_outdata.height; + ui_image.data_size = ui_outdata.output_len; + ui_image.data = ui_img_data; + + lvgl_port_lock(0); + lv_img_set_src(ui_ImgBle, &ui_image); + lvgl_port_unlock(); + + ESP_LOGI("IMG_UI", "JPEG图片更新成功: %s", img_name); + } else { + ESP_LOGE("IMG_UI", "图片解码失败,错误码: %d", ret); + ui_img_data = NULL; + } } } diff --git a/main/ui/screens/ui_ScreenImg.c b/main/ui/screens/ui_ScreenImg.c index 9622149..bf0b872 100644 --- a/main/ui/screens/ui_ScreenImg.c +++ b/main/ui/screens/ui_ScreenImg.c @@ -104,13 +104,19 @@ if ( event_code == LV_EVENT_SCREEN_LOADED ) { if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_TOP ) { lv_indev_wait_release(lv_indev_get_act()); - // 离开界面前隐藏容器 + // 离开界面前清理资源和隐藏容器 +#if LV_USE_GIF + pages_cleanup_gif(); +#endif ui_ScreenImg_hide_delete_container(); _ui_screen_change( &ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init); } if ( event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_BOTTOM ) { lv_indev_wait_release(lv_indev_get_act()); - // 离开界面前隐藏容器 + // 离开界面前清理资源和隐藏容器 +#if LV_USE_GIF + pages_cleanup_gif(); +#endif ui_ScreenImg_hide_delete_container(); // 设置返回到Img界面 ui_ScreenSet_set_previous(&ui_ScreenImg, &ui_ScreenImg_screen_init); @@ -196,6 +202,9 @@ lv_obj_add_event_cb(ui_ScreenImg, ui_event_ScreenImg, LV_EVENT_ALL, NULL); void ui_ScreenImg_screen_destroy(void) { +#if LV_USE_GIF + pages_cleanup_gif(); +#endif if (ui_ScreenImg) lv_obj_del(ui_ScreenImg); // NULL screen variables diff --git a/sdkconfig b/sdkconfig index 023258b..bdfc89f 100644 --- a/sdkconfig +++ b/sdkconfig @@ -14,6 +14,7 @@ CONFIG_SOC_GDMA_SUPPORTED=y CONFIG_SOC_AHB_GDMA_SUPPORTED=y CONFIG_SOC_GPTIMER_SUPPORTED=y CONFIG_SOC_LCDCAM_SUPPORTED=y +CONFIG_SOC_LCDCAM_CAM_SUPPORTED=y CONFIG_SOC_LCDCAM_I80_LCD_SUPPORTED=y CONFIG_SOC_LCDCAM_RGB_LCD_SUPPORTED=y CONFIG_SOC_MCPWM_SUPPORTED=y @@ -101,7 +102,7 @@ CONFIG_SOC_CPU_HAS_FPU=y CONFIG_SOC_HP_CPU_HAS_MULTIPLE_CORES=y CONFIG_SOC_CPU_BREAKPOINTS_NUM=2 CONFIG_SOC_CPU_WATCHPOINTS_NUM=2 -CONFIG_SOC_CPU_WATCHPOINT_MAX_REGION_SIZE=64 +CONFIG_SOC_CPU_WATCHPOINT_MAX_REGION_SIZE=0x40 CONFIG_SOC_SIMD_PREFERRED_DATA_ALIGNMENT=16 CONFIG_SOC_DS_SIGNATURE_MAX_BIT_LEN=4096 CONFIG_SOC_DS_KEY_PARAM_MD_IV_LENGTH=16 @@ -208,7 +209,7 @@ CONFIG_SOC_RTCIO_INPUT_OUTPUT_SUPPORTED=y CONFIG_SOC_RTCIO_HOLD_SUPPORTED=y CONFIG_SOC_RTCIO_WAKE_SUPPORTED=y CONFIG_SOC_LP_IO_CLOCK_IS_INDEPENDENT=y -CONFIG_SOC_SDM_GROUPS=y +CONFIG_SOC_SDM_GROUPS=1 CONFIG_SOC_SDM_CHANNELS_PER_GROUP=8 CONFIG_SOC_SDM_CLK_SUPPORT_APB=y CONFIG_SOC_SPI_PERIPH_NUM=3 @@ -369,6 +370,9 @@ CONFIG_SOC_BLE_DEVICE_PRIVACY_SUPPORTED=y CONFIG_SOC_BLUFI_SUPPORTED=y CONFIG_SOC_ULP_HAS_ADC=y CONFIG_SOC_PHY_COMBO_MODULE=y +CONFIG_SOC_LCDCAM_CAM_SUPPORT_RGB_YUV_CONV=y +CONFIG_SOC_LCDCAM_CAM_PERIPH_NUM=1 +CONFIG_SOC_LCDCAM_CAM_DATA_WIDTH_MAX=16 CONFIG_IDF_CMAKE=y CONFIG_IDF_TOOLCHAIN="gcc" CONFIG_IDF_TOOLCHAIN_GCC=y @@ -836,6 +840,7 @@ CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y CONFIG_BT_BLE_42_DTM_TEST_EN=y CONFIG_BT_BLE_42_ADV_EN=y CONFIG_BT_BLE_42_SCAN_EN=y +CONFIG_BT_BLE_VENDOR_HCI_EN=y # CONFIG_BT_BLE_HIGH_DUTY_ADV_INTERVAL is not set # CONFIG_BT_ABORT_WHEN_ALLOCATION_FAILS is not set # end of Bluedroid Options @@ -1057,6 +1062,7 @@ CONFIG_ESP_TLS_USE_DS_PERIPHERAL=y # CONFIG_ESP_TLS_SERVER_MIN_AUTH_MODE_OPTIONAL is not set # CONFIG_ESP_TLS_PSK_VERIFICATION is not set # CONFIG_ESP_TLS_INSECURE is not set +CONFIG_ESP_TLS_DYN_BUF_STRATEGY_SUPPORTED=y # end of ESP-TLS # @@ -1083,6 +1089,12 @@ CONFIG_ESP_ERR_TO_NAME_LOOKUP=y CONFIG_ESP_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y # end of Common ESP-related +# +# ESP-Driver:Camera Controller Configurations +# +# CONFIG_CAM_CTLR_DVP_CAM_ISR_CACHE_SAFE is not set +# end of ESP-Driver:Camera Controller Configurations + # # ESP-Driver:GPIO Configurations # @@ -1403,8 +1415,11 @@ CONFIG_ESP_PHY_RF_CAL_PARTIAL=y # CONFIG_ESP_PHY_RF_CAL_NONE is not set # CONFIG_ESP_PHY_RF_CAL_FULL is not set CONFIG_ESP_PHY_CALIBRATION_MODE=0 +CONFIG_ESP_PHY_PLL_TRACK_PERIOD_MS=1000 # CONFIG_ESP_PHY_PLL_TRACK_DEBUG is not set # CONFIG_ESP_PHY_RECORD_USED_TIME is not set +CONFIG_ESP_PHY_IRAM_OPT=y +# CONFIG_ESP_PHY_DEBUG is not set # end of PHY # @@ -1473,9 +1488,9 @@ CONFIG_SPIRAM_ALLOW_NOINIT_SEG_EXTERNAL_MEMORY=y # ESP System Settings # # CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80 is not set -CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=y -# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240 is not set -CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160 +# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160 is not set +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240 # # Cache config @@ -2084,6 +2099,7 @@ CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 # CONFIG_MBEDTLS_X509_TRUSTED_CERT_CALLBACK is not set # CONFIG_MBEDTLS_SSL_CONTEXT_SERIALIZATION is not set CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE=y +# CONFIG_MBEDTLS_SSL_KEYING_MATERIAL_EXPORT is not set CONFIG_MBEDTLS_PKCS7_C=y # end of mbedTLS v3.x related @@ -2564,9 +2580,8 @@ CONFIG_LV_COLOR_CHROMA_KEY_HEX=0x00FF00 # # Memory settings # -# CONFIG_LV_MEM_CUSTOM is not set -CONFIG_LV_MEM_SIZE_KILOBYTES=64 -CONFIG_LV_MEM_ADDR=0x0 +CONFIG_LV_MEM_CUSTOM=y +CONFIG_LV_MEM_CUSTOM_INCLUDE="stdlib.h" CONFIG_LV_MEM_BUF_MAX_NUM=16 # CONFIG_LV_MEMCPY_MEMSET_STD is not set # end of Memory settings @@ -2631,7 +2646,6 @@ CONFIG_LV_ASSERT_HANDLER_INCLUDE="assert.h" # Others # # CONFIG_LV_USE_PERF_MONITOR is not set -# CONFIG_LV_USE_MEM_MONITOR is not set # CONFIG_LV_USE_REFR_DEBUG is not set # CONFIG_LV_SPRINTF_CUSTOM is not set # CONFIG_LV_SPRINTF_USE_FLOAT is not set @@ -2802,7 +2816,10 @@ CONFIG_LV_USE_GRID=y # 3rd Party Libraries # # CONFIG_LV_USE_FS_STDIO is not set -# CONFIG_LV_USE_FS_POSIX is not set +CONFIG_LV_USE_FS_POSIX=y +CONFIG_LV_FS_POSIX_LETTER=83 +CONFIG_LV_FS_POSIX_PATH="/spiflash/" +CONFIG_LV_FS_POSIX_CACHE_SIZE=0 # CONFIG_LV_USE_FS_WIN32 is not set # CONFIG_LV_USE_FS_FATFS is not set # CONFIG_LV_USE_FS_LITTLEFS is not set @@ -3067,6 +3084,7 @@ CONFIG_BT_NIMBLE_COEX_PHY_CODED_TX_RX_TLIM_DIS=y CONFIG_SW_COEXIST_ENABLE=y CONFIG_ESP32_WIFI_SW_COEXIST_ENABLE=y CONFIG_ESP_WIFI_SW_COEXIST_ENABLE=y +# CONFIG_CAM_CTLR_DVP_CAM_ISR_IRAM_SAFE is not set # CONFIG_MCPWM_ISR_IN_IRAM is not set # CONFIG_EVENT_LOOP_PROFILING is not set CONFIG_POST_EVENTS_FROM_ISR=y @@ -3095,9 +3113,9 @@ CONFIG_ESP32S3_SPIRAM_SUPPORT=y CONFIG_DEFAULT_PSRAM_CLK_IO=30 CONFIG_DEFAULT_PSRAM_CS_IO=26 # CONFIG_ESP32S3_DEFAULT_CPU_FREQ_80 is not set -CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160=y -# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240 is not set -CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=160 +# CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160 is not set +CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y +CONFIG_ESP32S3_DEFAULT_CPU_FREQ_MHZ=240 CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32 CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304 CONFIG_MAIN_TASK_STACK_SIZE=3584 diff --git a/设备运行日志.md b/设备运行日志.md index e69de29..2f2ea09 100644 --- a/设备运行日志.md +++ b/设备运行日志.md @@ -0,0 +1,266 @@ +一、GIF卡顿原因深度分析 +1. 核心瓶颈定位 +瓶颈点 + +具体问题 + +对360×360屏幕的影响 + + +CPU开销​ + +图片中列出LZW+调色板+缩放+RGBA→565转换,每帧需多次转换 + +每帧约5-10ms计算时间,15fps需6.7ms/帧,已接近极限 + + +循环重播​ + +dispose+rewind+重解码首帧,每次循环额外开销 + +每次循环增加10-20ms延迟,导致循环衔接卡顿 + + +闪烁问题​ + +Canvas重置时的可见空白帧 + +用户感知明显的"闪一下" + + +BLE延迟​ + +SPIFFS写→SPIFFS读→解码,I/O操作过多 + +传输到显示延迟达200-500ms + + +内存占用​ + +Canvas约504KB + 解码缓冲区 + 文件缓冲 + +总内存需求约1.5-2MB,挤占其他功能 +2. 您当前配置的极限分析 +ESP32-S3-WROOM-1-N16R8(8MB PSRAM)的资源分配: +火山RTC SDK:2-2.5MB(音频处理+网络缓冲) +LVGL基础:1-1.5MB(框架+字体+UI缓存) +GIF显示: +Canvas缓冲区:504KB +解码工作区:300-500KB +文件缓冲:100-200KB +小计:约1-1.2MB +系统基础:1-1.5MB +合计:5.2-6.7MB → 剩余仅1.3-2.8MB缓冲 +关键发现:剩余PSRAM不足,导致: +频繁GC:内存碎片整理导致卡顿 +无预解码空间:无法缓存下一帧 +I/O阻塞:频繁读写SPIFFS +二、市面上电子吧唧配置对比 +1. 主流产品硬件方案 +产品/方案 + +主控配置 + +内存规格 + +显示方案 + +GIF优化技术 + + +EchoEar喵伴​ + +ESP32-S3-WROOM-1-N16R16V + +16MB PSRAM + +360×360 QSPI + +硬件缩放+直接帧传输 + + +四博智联方案​ + +ESP32-S3-WROOM-1-N16R16VA + +16MB PSRAM + +240×240 SPI + +预解码+多级缓存 + + +利尔达AI豆​ + +ESP32-S3-WROOM-2-N32R16V + +16MB PSRAM+硬件加速 + +320×320 RGB接口 + +专用DMA通道 + + +NN家族徽章​ + +定制芯片+外置DDR + +32-64MB + +电子墨水屏 + +云端预渲染+本地流式更新 +2. 与您配置的关键差异 +您的配置 vs 市场主流配置 +┌─────────────────┬─────────────────────────┬─────────────────────────┐ +│ 维度 │ 您的配置 (N16R8) │ 市场主流 (N16R16V+) │ +├─────────────────┼─────────────────────────┼─────────────────────────┤ +│ PSRAM │ 8MB (紧张) │ 16MB (充裕) │ +│ GIF处理策略 │ 实时解码+文件中转 │ 预解码+内存直出 │ +│ 显示接口 │ 可能为SPI │ 多为QSPI/RGB并行 │ +│ 传输延迟 │ 200-500ms (SPIFFS中转) │ 50-100ms (内存直传) │ +│ 可缓存帧数 │ 0-1帧 (无预缓冲) │ 3-5帧 (流畅预缓冲) │ +│ 循环优化 │ 每次重解码 │ 首帧缓存+增量更新 │ +└─────────────────┴─────────────────────────┴─────────────────────────┘ +三、他们为何不卡顿?关键技术优化 +1. 内存优化策略 +// 市场方案典型的内存分配(16MB PSRAM) +#define GIF_POOL_SIZE (3 * 1024 * 1024) // 3MB GIF专用池 +#define FRAME_BUFFER_SIZE (2 * 1024 * 1024) // 2MB双缓冲 +#define DECODE_BUFFER_SIZE (1 * 1024 * 1024) // 1MB解码工作区 + +// 您的配置对比(8MB PSRAM) +// 难以分配专用池,需与其他功能共享 +2. 显示流水线优化 +市场方案的四级流水线: +接收级:BLE直接写入PSRAM,绕过文件系统 +预解码级:后台线程预解码3-5帧 +显示级:专用DMA从PSRAM直传屏幕 +清理级:显示完成后异步释放 +您的现状:单线程串行处理,每步都阻塞。 +3. 解码算法优化 +市场方案:使用NEON指令集加速LZW,硬件RGBA→565转换 +您的现状:软件解码,CPU占用高 +四、具体优化建议(基于8MB PSRAM) +1. 内存管理重构 +// 创建专用内存池,避免碎片 +static uint8_t gif_pool[2 * 1024 * 1024] __attribute__((section(".spiram"))); + +// 替换malloc/free +void* gif_malloc(size_t size) { + static size_t offset = 0; + if(offset + size > sizeof(gif_pool)) return NULL; + void* ptr = &gif_pool[offset]; + offset += size; + return ptr; +} +2. 绕过文件系统中转 +// 原流程:BLE→SPIFFS→读文件→解码→显示 +// 新流程:BLE→PSRAM→解码→显示 + +// BLE接收直接写入PSRAM缓冲区 +void ble_on_receive(uint8_t* data, size_t len) { + static uint8_t* gif_buffer = heap_caps_malloc(MAX_GIF_SIZE, MALLOC_CAP_SPIRAM); + memcpy(gif_buffer + offset, data, len); + offset += len; + + if(offset == total_size) { + // 直接解码PSRAM中的GIF + decode_gif_from_ram(gif_buffer, total_size); + } +} +3. 降低GIF规格 +必须妥协的参数: +分辨率:240×240(在360×360屏幕上居中显示) +帧率:10-12fps(人眼可接受下限) +颜色:64色(从256色降低) +循环:单次播放,避免重解码开销 +效果:内存需求从1.2MB降至约400KB。 +4. 显示技术优化 +// 使用LVGL的部分刷新 +lv_disp_set_draw_buffers(disp, buf1, buf2, 360 * 120, LV_DISP_RENDER_MODE_PARTIAL); + +// 启用脏区域标记 +lv_obj_add_flag(gif_obj, LV_OBJ_FLAG_DIRTY_UPDATE); +五、终极解决方案:模组升级 +升级到N16R16V(16MB PSRAM)的收益 +性能指标 + +升级前 + +升级后 + +改善幅度 + + +可用PSRAM​ + +1.3-2.8MB + +8-10MB + ++300% + + +GIF缓存帧​ + +0-1帧 + +3-5帧 + +流畅循环 + + +传输延迟​ + +200-500ms + +50-100ms + +-80% + + +解码并发​ + +串行 + +流水线并行 + +帧率提升2-3倍 + + +系统稳定性​ + +频繁GC卡顿 + +稳定运行 + +用户体验质变 +成本分析: +N16R8模组:约25-30元 +N16R16V模组:约30-35元 +增量成本:5元左右,但体验提升显著 +六、实施路线图 +短期(1-2周):软件优化 +实现BLE直存PSRAM,绕过SPIFFS +降低GIF规格(240×240,10fps,64色) +使用LVGL部分刷新减少绘制区域 +中期(2-4周):架构优化 +实现双缓冲显示流水线 +添加后台预解码线程 +优化内存管理,减少碎片 +长期(如有必要):硬件升级 +更换为ESP32-S3-WROOM-1-N16R16V +重新设计PCB优化布局 +考虑QSPI屏幕接口提升传输速度 +总结 +卡顿的根本原因是8MB PSRAM在多任务(RTC+LVGL+BLE+GIF)下资源不足,导致必须通过文件系统中转、无预解码缓冲、频繁内存回收。 +市场主流产品通过以下方式避免卡顿: +硬件升级:16MB+ PSRAM提供充足缓冲 +架构优化:内存直传+预解码流水线 +显示优化:QSPI/RGB接口+硬件加速 +对您的建议: +立即实施:软件优化方案,特别是绕过SPIFFS +评估升级:如果优化后仍不满足需求,强烈建议升级到N16R16V +平衡妥协:适当降低GIF规格,在现有硬件上获得最佳体验 +5元左右的模组升级成本,可换来用户体验的质变,在产品化阶段是值得的投资。 \ No newline at end of file