源代码变更: - main/dzbj/bg_gif_demo.c/h: 方案 C 最终实现 - JPG 背景图(lv_img) + 透明 GIF(lv_gif) 叠加 - main/dzbj/dual_gif_demo.c/h: 方案 B 中间产物 - 双 GIF 循环切换 - main/dzbj/sprite_demo.c/h: 方案 A 已弃用 - DMA 直写 GRAM 与 LVGL 争抢 LCD IO 失败 - main/dzbj/ai_chat_ui.c: 集成 USE_BG_GIF_POC 开关,加载背景图+透明 GIF - main/dzbj/lcd.c: panel_handle 移除 static,便于其他模块访问 - main/CMakeLists.txt: 新增 3 个 dzbj 模块编译 资源新增: - spiffs_image/Background_360x360.jpg: 设备背景图(20KB) - spiffs_image/hiyori_m05.gif: Cubism Editor 直接导出的透明 GIF(2.3MB) - docs/Rtc_AIavatar/: Live2D 模型(Hiyori/Haru) + 32 段 Haru GIF + 方案文档第18章 PoC 实战记录 - tools/sprite_poc/: Python GIF→RGB565 转换脚本 踩坑要点(详见 docs/Rtc_AIavatar 第18章): - PIL Image.quantize() 会破坏 RGBA 透明度,必须改用 gifsicle - PIL 保存动画 GIF 仅第1帧有透明,后续帧不透明 - LVGL gifdec 按帧读取 - Cubism Editor 直接导出 GIF 才能逐帧保留透明信息(FREE 版限制部分模型) - gifsicle --lossy 会严重锯齿化,去掉只保留 --colors 256 + -O3 即可 - 裁剪居中需用全帧 bbox 不能只看第1帧(Live2D 角色每帧位置有偏移) - LVGL 默认不支持 PNG,背景图用 JPG + esp_jpeg 解码到 RGB565 buffer - 透明 GIF 显示黑色背景: gifdec.c canvas 初始化 alpha 须改为 0x00
Sprite Sheet PoC 操作步骤
目标:把一张 GIF 转换为 RGB565 raw → 烧到 SPIFFS → AI 对话模式开机播放 不涉及 BLE/APP/云端,最快验证整条数据链路
前置准备
- Python 3 + Pillow:
pip install Pillow - ESP-IDF v5.4.2 环境就绪
Step 1:准备 GIF 文件
把你的 GIF 放到本目录:
cp <你的GIF路径> /Users/rdzleo/Desktop/Baji_Rtc_Toy/tools/sprite_poc/input.gif
Step 2:转换为 RGB565 raw .bin
cd /Users/rdzleo/Desktop/Baji_Rtc_Toy/tools/sprite_poc
# 默认 200x200
python3 gif_to_rgb565.py input.gif sprite_test.bin
# 或自定义尺寸(与 AI 对话默认 emotion 一致)
python3 gif_to_rgb565.py input.gif sprite_test.bin 200 89
输出示例:
原始 GIF: (240, 240), 帧数估计 6
帧 0: duration=100ms
帧 1: duration=100ms
...
转换信息:
目标尺寸: 200x200
帧数: 6
平均 FPS: 10
单帧大小: 80.0 KB
帧数据总大小: 480.0 KB
✓ 输出: sprite_test.bin
总大小: 480.1 KB (491600 bytes)
注意:
- 单帧不能超过 LCD 分辨率(360×360 = 253KB)
- 总大小不要超过 SPIFFS 剩余空间(约 3MB - 已用图片)
Step 3:放入 SPIFFS 资源目录
cp sprite_test.bin /Users/rdzleo/Desktop/Baji_Rtc_Toy/spiffs_image/
编译时会自动烧到 storage 分区(看 CMakeLists.txt:21):
spiffs_create_partition_image(storage spiffs_image FLASH_IN_PROJECT)
Step 4:把 sprite_demo.c 加入构建
编辑 main/CMakeLists.txt,在 SRCS 列表中加入:
"dzbj/sprite_demo.c"
(如果项目用 GLOB 自动收集 dzbj/*.c 则跳过此步)
Step 5:在 AI 对话模式集成 PoC 调用
最简单的方式:在 ai_chat_screen_init() 末尾添加调用。
打开 main/dzbj/ai_chat_ui.c,在函数末尾(return 前)添加:
#include "sprite_demo.h"
void ai_chat_screen_init(void) {
// ... 原有 LVGL UI 创建代码 ...
// === PoC:替换默认 GIF 显示为 sprite sheet ===
// 隐藏原有 GIF 对象
if (gif_emotion) {
lv_obj_add_flag(gif_emotion, LV_OBJ_FLAG_HIDDEN);
}
// 启动 sprite demo(异步,加载完自动播放)
sprite_demo_start("/spiflash/sprite_test.bin");
}
注意:sprite_demo 直接调用 esp_lcd_panel_draw_bitmap 写 LCD GRAM, 会覆盖 LVGL 渲染的内容。所以要先把原 LVGL GIF 对象隐藏。
Step 6:编译烧录
cd /Users/rdzleo/Desktop/Baji_Rtc_Toy
source ~/esp/esp-idf/v5.4.2/esp-idf/export.sh
# 完整编译(含 SPIFFS 镜像生成)
idf.py build
# 烧录(包含 storage 分区,确保新 .bin 被写入)
idf.py -p /dev/tty.usbmodem834401 flash monitor
Step 7:观察效果
串口日志应该看到:
I (3500) SPRITE_DEMO: Sprite Pack: 200x200, 总帧数=6, 大小=491520 B
I (3501) SPRITE_DEMO: Entry: "test", 6 帧 @ 10 FPS
I (3520) SPRITE_DEMO: 已加载 480.0 KB 到 PSRAM
I (3521) SPRITE_DEMO: ✓ 开始播放 @ 10 FPS (interval=100000 us)
LCD 屏幕:进入 AI 对话模式后,原 GIF 表情位置显示你的 sprite 动画循环播放。
验证清单
进入 AI 对话模式后,请重点观察:
- 画面正确:sprite 颜色是否正常(如果偏色 → 字节序问题,调整 Python 的
'>H'大端为'<H'小端) - 动画流畅:帧间过渡是否平滑(如果卡顿 → 检查 esp_timer 是否被阻塞)
- CPU 占用:用
esp_get_free_heap_size或 idle hook 测量。预期 sprite 显示侧 <5% CPU(vs GIF 方案 ~30%) - 音频共存:与 RTC 对话同时跑,音频是否流畅(这是关键验证点)
- PSRAM 占用:日志中应该看到 ~500KB 分配,esp_get_free_heap_size 应有 7MB+ 剩余
故障排查
| 现象 | 可能原因 | 解决 |
|---|---|---|
magic 不匹配 |
.bin 没烧到 SPIFFS | 重新烧录,确认 spiffs_create_partition_image 生效 |
PSRAM 分配失败 |
帧太大 | 缩小尺寸到 200×200 或更小 |
| 画面偏色 | RGB565 字节序错 | Python 脚本改为 '<H' 小端 |
| 画面错位 | 字节序混合 | 检查 LCD 是否需要 swap_color_bytes |
| 闪烁 | LVGL 同时在刷该区域 | 永久隐藏原 GIF 对象 |
| 音频卡顿 | sprite 任务优先级太高 | 把 esp_timer 优先级降低或检查 PSRAM 争抢 |
下一步
PoC 通过后:
- 扩展到多情绪:Python 脚本支持多目录输入,生成多情绪 sprite pack
- 接入 RTC 信令:在
application.cc的 OnBotMessage 中根据 emotion 切换帧索引 - 方案 D 升级:把 SPIFFS 改为专用 Flash 分区 + mmap(释放 PSRAM)
- BLE 接收:让 APP 通过 BLE 下发新 sprite pack(替换烧录方式)
详见 docs/数字人表情渲染方案_云端预渲染+BLE+OTA.md 第十三章。