feat: BLE传输优化 + GIF条件编译 + 自定义GIF播放器
一、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 <noreply@anthropic.com>
This commit is contained in:
parent
811559be49
commit
9223fd5a7d
110
main/ble/ble.c
110
main/ble/ble.c
@ -1,6 +1,8 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#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"",
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#include "esp_err.h"
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
44
sdkconfig
44
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
|
||||
|
||||
266
设备运行日志.md
266
设备运行日志.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元左右的模组升级成本,可换来用户体验的质变,在产品化阶段是值得的投资。
|
||||
Loading…
x
Reference in New Issue
Block a user