/** * sprite_demo.c — Sprite Sheet PoC(LVGL lv_img 方案) * * 数据流:SPIFFS .bin → PSRAM → 构造 lv_img_dsc_t 数组 → LVGL lv_img 控件 → LVGL 自动 flush * * 设计要点: * - sprite 帧数据预先转好 RGB565,加载到 PSRAM 后零拷贝 * - 用 LVGL lv_img 控件显示,完全在 LVGL 控制下,不绕过 LVGL * - lv_timer 定时切换 lv_img_set_src,LVGL 自动 invalidate 和 flush * - 与 LVGL 其他控件(字幕/电池等)共存,无 SPI 队列冲突 */ #include "sprite_demo.h" #include "esp_log.h" #include "esp_heap_caps.h" #include "esp_lvgl_port.h" #include "esp_spiffs.h" #include "lvgl.h" #include #include #include #include static const char *TAG = "SPRITE_DEMO"; // 文件格式定义(与 Python 脚本一致,总计 64 字节) typedef struct __attribute__((packed)) { char magic[8]; uint32_t version; uint16_t frame_width; uint16_t frame_height; uint8_t pixel_format; uint8_t emotion_count; uint8_t motion_count; uint8_t mouth_levels; uint32_t emotion_table_offset; uint32_t motion_table_offset; uint32_t frame_data_offset; uint32_t total_frames; uint32_t total_size; uint8_t checksum[4]; uint8_t reserved[20]; } sprite_header_t; _Static_assert(sizeof(sprite_header_t) == 64, "sprite_header_t must be 64 bytes"); typedef struct __attribute__((packed)) { char name[8]; uint16_t frame_count; uint16_t fps; uint32_t first_frame_idx; uint32_t flags; } sprite_entry_t; // 运行时状态 static sprite_header_t g_header; static sprite_entry_t g_entry; static uint8_t *g_frames = NULL; static lv_img_dsc_t *g_frame_dscs = NULL; // 每帧一个 lv_img_dsc_t static uint16_t g_current_frame = 0; static lv_obj_t *g_sprite_img = NULL; // LVGL 图像控件 static lv_timer_t *g_play_timer = NULL; static bool g_playing = false; // LVGL 定时器回调(在 LVGL 任务中执行) static void play_timer_cb(lv_timer_t *timer) { if (!g_playing || !g_frame_dscs || !g_sprite_img) return; g_current_frame = (g_current_frame + 1) % g_entry.frame_count; lv_img_set_src(g_sprite_img, &g_frame_dscs[g_current_frame]); } esp_err_t sprite_demo_start(const char *path) { if (g_playing) { ESP_LOGW(TAG, "已在播放,先 stop 再 start"); return ESP_ERR_INVALID_STATE; } // 检查 SPIFFS 是否已挂载,未挂载则自动挂载 size_t total = 0, used = 0; if (esp_spiffs_info("storage", &total, &used) != ESP_OK) { ESP_LOGI(TAG, "SPIFFS 未挂载,自动挂载 storage 分区..."); esp_vfs_spiffs_conf_t conf = { .base_path = "/spiflash", .partition_label = "storage", .max_files = 5, .format_if_mount_failed = false, }; esp_err_t err = esp_vfs_spiffs_register(&conf); if (err != ESP_OK) { ESP_LOGE(TAG, "SPIFFS 挂载失败: %s", esp_err_to_name(err)); return err; } } FILE *f = fopen(path, "rb"); if (!f) { ESP_LOGE(TAG, "打开文件失败: %s", path); return ESP_FAIL; } // 1. 读文件头 if (fread(&g_header, 1, sizeof(sprite_header_t), f) != sizeof(sprite_header_t)) { ESP_LOGE(TAG, "读文件头失败"); fclose(f); return ESP_FAIL; } if (memcmp(g_header.magic, "SPRITES", 7) != 0) { ESP_LOGE(TAG, "magic 不匹配"); fclose(f); return ESP_ERR_INVALID_ARG; } ESP_LOGI(TAG, "Sprite Pack: %dx%d, 总帧数=%lu", g_header.frame_width, g_header.frame_height, g_header.total_frames); // 2. 读情绪索引 fseek(f, g_header.emotion_table_offset, SEEK_SET); fread(&g_entry, 1, sizeof(sprite_entry_t), f); char name[9] = {0}; memcpy(name, g_entry.name, 8); ESP_LOGI(TAG, "Entry: \"%s\", %d 帧 @ %d FPS", name, g_entry.frame_count, g_entry.fps); // 3. 加载帧数据到 PSRAM uint32_t frame_size = g_header.frame_width * g_header.frame_height * 2; size_t total_data = frame_size * g_entry.frame_count; g_frames = heap_caps_malloc(total_data, MALLOC_CAP_SPIRAM); if (!g_frames) { ESP_LOGE(TAG, "PSRAM 分配失败: %d 字节", (int)total_data); fclose(f); return ESP_ERR_NO_MEM; } fseek(f, g_header.frame_data_offset, SEEK_SET); fread(g_frames, 1, total_data, f); fclose(f); ESP_LOGI(TAG, "已加载 %.1f KB 到 PSRAM", total_data / 1024.0); // 4. 为每帧构造 lv_img_dsc_t(共享 PSRAM 数据,零拷贝) g_frame_dscs = calloc(g_entry.frame_count, sizeof(lv_img_dsc_t)); if (!g_frame_dscs) { ESP_LOGE(TAG, "frame_dscs 分配失败"); heap_caps_free(g_frames); g_frames = NULL; return ESP_ERR_NO_MEM; } for (int i = 0; i < g_entry.frame_count; i++) { g_frame_dscs[i].header.cf = LV_IMG_CF_TRUE_COLOR; g_frame_dscs[i].header.always_zero = 0; g_frame_dscs[i].header.reserved = 0; g_frame_dscs[i].header.w = g_header.frame_width; g_frame_dscs[i].header.h = g_header.frame_height; g_frame_dscs[i].data_size = frame_size; g_frame_dscs[i].data = g_frames + i * frame_size; } // 5. 在当前活动屏幕上创建 lv_img 控件并启动定时器 if (!lvgl_port_lock(200)) { ESP_LOGE(TAG, "lvgl_port_lock 失败"); free(g_frame_dscs); g_frame_dscs = NULL; heap_caps_free(g_frames); g_frames = NULL; return ESP_FAIL; } g_sprite_img = lv_img_create(lv_scr_act()); lv_img_set_src(g_sprite_img, &g_frame_dscs[0]); lv_obj_align(g_sprite_img, LV_ALIGN_CENTER, 0, 0); g_current_frame = 0; g_playing = true; uint32_t interval_ms = 1000 / g_entry.fps; g_play_timer = lv_timer_create(play_timer_cb, interval_ms, NULL); lvgl_port_unlock(); ESP_LOGI(TAG, "✓ 开始播放 @ %d FPS (interval=%lu ms, %dx%d)", g_entry.fps, (unsigned long)interval_ms, g_header.frame_width, g_header.frame_height); return ESP_OK; } void sprite_demo_pause(void) { if (g_play_timer && g_playing) { if (lvgl_port_lock(100)) { lv_timer_pause(g_play_timer); lvgl_port_unlock(); } g_playing = false; ESP_LOGI(TAG, "暂停播放"); } } void sprite_demo_resume(void) { if (g_play_timer && !g_playing && g_frames) { if (lvgl_port_lock(100)) { lv_timer_resume(g_play_timer); lvgl_port_unlock(); } g_playing = true; ESP_LOGI(TAG, "恢复播放"); } } void sprite_demo_stop(void) { if (lvgl_port_lock(200)) { if (g_play_timer) { lv_timer_del(g_play_timer); g_play_timer = NULL; } if (g_sprite_img) { lv_obj_del(g_sprite_img); g_sprite_img = NULL; } lvgl_port_unlock(); } if (g_frame_dscs) { free(g_frame_dscs); g_frame_dscs = NULL; } if (g_frames) { heap_caps_free(g_frames); g_frames = NULL; } g_playing = false; ESP_LOGI(TAG, "停止播放并释放 PSRAM"); } bool sprite_demo_is_playing(void) { return g_playing; }