# LittleFS 资源构建、烧录与挂载流程 本文说明项目中的 LCD 图片和提示音资源如何从源码目录进入设备 Flash,以及设备运行时如何通过 LittleFS 访问这些资源。同时给出新增、替换和排查资源问题的完整操作步骤。 ## 1. 先建立整体认识 项目不会把每一张图片分别写入 Flash,也不会在设备启动时从电脑复制资源。 实际流程是:构建系统先将需要的资源整理成一个临时目录,再将整个目录制作成 LittleFS 文件系统镜像 `gifpack.bin`,最后把这个镜像作为一个独立分区烧录到设备。设备启动后,将该分区挂载到 `/littlefs`,应用程序便可以像读取普通文件一样读取图片和音频。 ```text 源码资源 components/platform/assets/ | | CMake 选择并复制资源 v 构建临时目录 build/esp-idf/platform/asset_image/ | | 制作 LittleFS 文件系统镜像 v build/gifpack.bin | | idf.py flash v 设备 Flash 中的 gifpack 分区 | | esp_vfs_littlefs_register() v 设备文件路径 /littlefs/ ``` 需要区分以下三个概念: | 名称 | 所在位置 | 作用 | | --- | --- | --- | | 源码资源目录 | `components/platform/assets/` | Git 管理的原始待烧录资源 | | 构建临时目录 | `build/esp-idf/platform/asset_image/` | 生成镜像前整理出来的目录树 | | LittleFS 镜像 | `build/gifpack.bin` | 最终烧入设备 `gifpack` 分区的文件系统镜像 | 临时目录只存在于开发电脑上。设备真正挂载的是 Flash 中的 `gifpack.bin` 内容,不是电脑上的 `build` 目录。 ## 2. 相关文件在哪里 | 文件或目录 | 作用 | | --- | --- | | `components/platform/assets/lcd/` | LCD 的 `.rle` 图片资源 | | `components/platform/assets/audio_prompts/` | `.opus` 提示音资源 | | `components/platform/CMakeLists.txt` | 选择资源、整理临时目录、生成 LittleFS 镜像 | | `components/platform/idf_component.yml` | 声明 `joltwallet/littlefs` 组件依赖 | | `managed_components/joltwallet__littlefs/project_include.cmake` | 定义 `littlefs_create_partition_image()` | | `16m.csv` | 定义 `gifpack` 分区的类型、大小和位置 | | `components/platform/src/platform_behavior_stubs.c` | 运行时挂载 `gifpack` 分区 | | `components/platform/src/lcd_direct_assets.c` | 将 LCD 资源名映射到 `/littlefs/lcd/*.rle` | | `components/platform/src/lcd_direct.c` | 打开、读取、解码并绘制 RLE 文件 | | `components/domain/src/runtime/domain_runtime_adapters.c` | 将业务表达式映射为 LCD 资源播放请求 | ## 3. CMake 如何选中资源 ### 3.1 定义资源目录 `components/platform/CMakeLists.txt` 中定义: ```cmake set(platform_audio_prompt_src_dir "${CMAKE_CURRENT_SOURCE_DIR}/assets/audio_prompts") set(platform_lcd_asset_src_dir "${CMAKE_CURRENT_SOURCE_DIR}/assets/lcd") set(platform_asset_image_dir "${CMAKE_CURRENT_BINARY_DIR}/asset_image") ``` 在当前组件中,这些变量最终大致对应: ```text platform_audio_prompt_src_dir -> components/platform/assets/audio_prompts platform_lcd_asset_src_dir -> components/platform/assets/lcd platform_asset_image_dir -> build/esp-idf/platform/asset_image ``` ### 3.2 LCD 使用显式白名单 LCD 资源由 `platform_lcd_flash_assets` 明确列出: ```cmake set(platform_lcd_flash_assets buyunqian1 jichubiaoqing0 jichubiaoqing1 jichubiaoqing2 muyu0 muyu1 muyu2 # ... ) ``` 列表中不写 `.rle` 后缀。后续循环会自动拼出完整文件名: ```cmake "${platform_lcd_asset_src_dir}/${lcd_rle_asset}.rle" ``` 例如白名单中的: ```text jichubiaoqing0 ``` 最终对应: ```text components/platform/assets/lcd/jichubiaoqing0.rle ``` 因此,把新 `.rle` 文件放进 `assets/lcd/` 并不代表它会自动进入 Flash。只有加入 `platform_lcd_flash_assets` 的 LCD 文件才会进入 `gifpack.bin`。 ### 3.3 音频使用自动扫描 音频资源采用另一种策略: ```cmake file(GLOB_RECURSE platform_audio_prompt_files CONFIGURE_DEPENDS "${platform_audio_prompt_src_dir}/*.opus" ) ``` 这会递归选中 `assets/audio_prompts/` 下的全部 `.opus` 文件。因此: - 新增 LCD 文件时,需要手动加入 CMake 白名单; - 新增 `.opus` 文件时,一般不需要修改资源白名单。 ## 4. CMake 如何形成临时目录 构建时,CMake 会先清理旧的临时目录: ```cmake COMMAND ${CMAKE_COMMAND} -E rm -rf "${platform_asset_image_dir}" COMMAND ${CMAKE_COMMAND} -E make_directory "${platform_asset_image_dir}" ``` 这样可以避免已经从白名单删除的旧资源继续残留在镜像中。 随后,LCD 文件被复制到临时目录的 `lcd/` 子目录: ```cmake COMMAND ${CMAKE_COMMAND} -E copy_if_different "${platform_lcd_asset_src_dir}/${lcd_rle_asset}.rle" "${platform_asset_image_dir}/lcd/${lcd_rle_asset}.rle" ``` 音频文件被复制到 `audio_prompts/` 子目录。完成后,临时目录大致如下: ```text build/esp-idf/platform/asset_image/ ├── audio_prompts/ │ ├── boot.opus │ ├── network_connect_success.opus │ └── ... └── lcd/ ├── jichubiaoqing0.rle ├── jichubiaoqing1.rle ├── muyu0.rle └── ... ``` 这个目录结构会原样成为 LittleFS 镜像内部的目录结构。 资源文件还被加入 `DEPENDS`: ```cmake DEPENDS ${platform_audio_prompt_files} ${platform_lcd_flash_asset_files} ``` 因此,白名单内的资源发生变化时,构建系统能够重新整理目录并生成镜像。 ## 5. `littlefs_create_partition_image()` 从哪里来 项目在 `components/platform/idf_component.yml` 中声明: ```yaml dependencies: joltwallet/littlefs: 1.20.4 ``` ESP-IDF Component Manager 因此引入: ```text managed_components/joltwallet__littlefs/ ``` 这个组件包含特殊文件: ```text managed_components/joltwallet__littlefs/project_include.cmake ``` ESP-IDF 构建系统会自动加载各组件的 `project_include.cmake`。该文件定义了: ```cmake function(littlefs_create_partition_image partition base_dir) # ... endfunction() ``` 所以 `littlefs_create_partition_image()`: - 不是标准 CMake 内置函数; - 也不是本项目自行定义的普通函数; - 它是 `joltwallet/littlefs` 组件提供并由 ESP-IDF 自动导入的构建扩展。 ## 6. 如何生成 `gifpack.bin` 项目调用: ```cmake littlefs_create_partition_image( gifpack "${platform_asset_image_dir}" FLASH_IN_PROJECT DEPENDS "${platform_asset_stage_stamp}" ) ``` 参数含义如下: | 参数 | 含义 | | --- | --- | | `gifpack` | 要匹配的分区名称 | | `platform_asset_image_dir` | 要制作成文件系统镜像的目录 | | `FLASH_IN_PROJECT` | 将镜像加入项目的完整 `flash` 目标 | | `DEPENDS` | 确保资源整理完成后再生成镜像 | 该函数会根据分区名查询分区表: ```cmake partition_table_get_partition_info(size "--partition-name ${partition}" "size") partition_table_get_partition_info(offset "--partition-name ${partition}" "offset") ``` 随后调用 `littlefs-python`,大致执行: ```text littlefs-python create \ build/esp-idf/platform/asset_image \ build/gifpack.bin \ --fs-size= \ --block-size=4096 ``` 得到: ```text build/gifpack.bin ``` `gifpack.bin` 不是简单压缩包,而是一个包含目录、文件名、文件内容和文件系统元数据的完整 LittleFS 镜像。 ## 7. 分区表如何决定烧录位置 项目通过 `sdkconfig.defaults` 选择自定义分区表: ```text CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="16m.csv" ``` `16m.csv` 中定义: ```csv gifpack, data, littlefs, , 0xa80000, ``` 字段含义: | 字段 | 当前值 | 含义 | | --- | --- | --- | | Name | `gifpack` | 分区标签,构建和挂载都依赖它 | | Type | `data` | 数据分区 | | SubType | `littlefs` | LittleFS 文件系统分区 | | Offset | 空 | 由 ESP-IDF 根据前面的分区自动计算 | | Size | `0xa80000` | 分区大小,约 10.5 MiB | 当前生成结果中,`gifpack` 的起始地址是 `0x10000`。由于分区 Offset 没有写死,如果前面的分区布局发生变化,实际地址也可能变化;应以当前构建生成的分区表和 `build/flasher_args.json` 为准。 下面三处名称必须一致: ```text CMake: littlefs_create_partition_image(gifpack ...) 分区表: gifpack, data, littlefs, ... 运行时: .partition_label = "gifpack" ``` 任何一处名称不一致,都可能导致镜像生成失败、烧错位置或运行时找不到分区。 ## 8. `FLASH_IN_PROJECT` 如何让镜像随项目烧录 `FLASH_IN_PROJECT` 会让 LittleFS 组件执行类似以下注册: ```cmake esptool_py_flash_target_image( flash "${partition}" "${offset}" "${image_file}" ) ``` 它的含义是:把 `gifpack.bin` 加入 ESP-IDF 的完整 `flash` 目标。 当前 `build/flasher_args.json` 中可看到类似内容: ```json "0x10000": "gifpack.bin" ``` 所以执行: ```bash idf.py flash ``` 时,ESP-IDF 会分别烧录 bootloader、分区表、应用固件和 `gifpack.bin`。`gifpack.bin` 并没有编入应用程序 `.bin`,它始终是一个独立的分区镜像。 需要特别区分: | 命令 | 对资源分区的影响 | | --- | --- | | `idf.py build` | 生成或更新 `gifpack.bin`,不写设备 | | `idf.py flash` | 按完整烧录清单写入 `gifpack.bin` | | `idf.py app-flash` | 通常只更新应用分区,不更新 `gifpack` | | 普通应用 OTA | 通常只更新 OTA 应用分区,不更新 `gifpack` | 因此,修改资源后只执行 `app-flash` 或普通应用 OTA,设备中的资源可能仍然是旧版本。 ## 9. 设备运行时如何挂载 挂载函数位于 `components/platform/src/platform_behavior_stubs.c`: ```c #define VOICE_PROMPT_LITTLEFS_BASE_PATH "/littlefs" #define VOICE_PROMPT_LITTLEFS_PARTITION "gifpack" esp_vfs_littlefs_conf_t conf = { .base_path = VOICE_PROMPT_LITTLEFS_BASE_PATH, .partition_label = VOICE_PROMPT_LITTLEFS_PARTITION, .partition = NULL, .format_if_mount_failed = false, .dont_mount = false, }; esp_err_t err = esp_vfs_littlefs_register(&conf); ``` ### 9.1 `partition_label = "gifpack"` LittleFS 驱动根据标签在运行时分区表中查找 `gifpack`,得到该分区的实际 Flash 地址和大小。 ### 9.2 `partition = NULL` 调用方没有直接传入 `esp_partition_t`,由 LittleFS 驱动根据 `partition_label` 自动查找分区。 ### 9.3 `base_path = "/littlefs"` 将 LittleFS 注册到 ESP-IDF VFS 的 `/littlefs` 路径。挂载成功后,可以使用标准文件 API: ```c open("/littlefs/lcd/jichubiaoqing0.rle", O_RDONLY); fopen("/littlefs/audio_prompts/boot.opus", "rb"); ``` ### 9.4 `format_if_mount_failed = false` 挂载失败时不格式化分区。这对预置资源分区非常重要,因为格式化会清空已经烧录的图片和音频。 ### 9.5 `dont_mount = false` 注册后立即执行挂载,而不是只注册驱动。 ### 9.6 VFS 路径如何对应到 LittleFS 内部路径 临时目录中的: ```text asset_image/lcd/jichubiaoqing0.rle ``` 进入 LittleFS 后对应: ```text /lcd/jichubiaoqing0.rle ``` 挂载到 `/littlefs` 后,应用程序看到: ```text /littlefs/lcd/jichubiaoqing0.rle ``` 应用调用 `open()` 时,ESP-IDF VFS 识别 `/littlefs` 前缀,并将剩余的 `/lcd/jichubiaoqing0.rle` 交给 LittleFS 驱动读取。 ## 10. 当前启动时机和隐式依赖 当前 `platform_mount_littlefs()` 不是由 `platform_init()` 直接调用,而是在 `voice_prompt_play_file()` 播放提示音之前调用: ```c esp_err_t mount_err = platform_mount_littlefs(); ``` 当前启动配置启用了 `boot.opus`,启动顺序大致是: ```text 准备运行时存储 -> 初始化音频 -> 播放 boot.opus -> platform_mount_littlefs() -> 挂载 gifpack 到 /littlefs -> 初始化 LCD -> LCD 渲染线程读取 /littlefs/lcd/*.rle ``` 这意味着当前 LCD 成功读取资源,隐式依赖“开机音流程已经触发 LittleFS 挂载”。如果以后禁用开机音,或者在调用 `voice_prompt_play_file()` 之前就失败,LCD 可能在尚未挂载时访问 `/littlefs/lcd/`。 更稳妥的长期方案是把 `platform_mount_littlefs()` 作为独立启动阶段,在音频和 LCD 使用资源之前显式调用。修改启动流程前,应保持现有分层规则,并由 `app_composition` 编排启动顺序。 ## 11. LCD RLE 文件格式 当前 `lcd_direct` 驱动读取的 `.rle` 是原始二元组序列: ```c typedef struct { uint16_t color; uint16_t count; } lcd_direct_rle_run_t; ``` 每个 run 占 4 字节: ```text 2 字节 RGB565 color,little-endian 2 字节重复像素数 count,little-endian ``` 全屏图片解压后的像素总数应与屏幕绘制区域匹配。当前横屏画布通常为: ```text 320 x 240 = 76800 像素 ``` 图标资源则应与 `lcd_direct_icon_t` 中配置的宽高匹配。 ### 11.1 `run_count` 必须与文件一致 `components/platform/src/lcd_direct_assets.c` 不仅保存文件路径,还保存 `run_count`: ```c { .path = LCD_ASSET_BASE_PATH "jichubiaoqing0.rle", .run_count = 5411U, }, ``` 对于当前原始格式: ```text run_count = 文件字节数 / 4 ``` 替换资源后,即使文件名没有变化,只要编码后的 run 数发生变化,就必须同步更新 `run_count`。否则驱动可能少读、读到文件末尾失败或显示异常。 可以用下面的方法检查: ```bash python3 -c 'from pathlib import Path; p=Path("components/platform/assets/lcd/jichubiaoqing0.rle"); print(p.stat().st_size // 4)' ``` 还应确认文件大小能被 4 整除。 ### 11.2 不要误用 `tools/png_to_rle.py` 当前仓库的 `tools/png_to_rle.py` 使用 Pillow 生成标准 TGA RLE 文件;而 `lcd_direct.c` 读取的是上面描述的原始 `color + count` 二元组格式。 两种格式不兼容。不要直接用 `tools/png_to_rle.py` 的输出替换 `components/platform/assets/lcd/*.rle`,除非先统一转换工具和驱动格式。 `tools/extract_lcd_assets.py` 能写出当前原始二元组格式,但其中包含旧工程的固定 Windows 路径和固定资源映射,使用前必须检查和调整,不能假设它在所有开发环境中可直接运行。 ## 12. 替换现有 LCD 资源 适用场景:资源名、帧数和业务行为不变,只替换画面内容。 以替换 `jichubiaoqing0.rle` 为例: 1. 使用与当前驱动兼容的原始 RLE 转换流程生成新文件。 2. 确认解压像素总数与目标画布一致。 3. 用新文件替换: ```text components/platform/assets/lcd/jichubiaoqing0.rle ``` 4. 计算新文件的 `run_count`。 5. 更新 `components/platform/src/lcd_direct_assets.c` 中对应条目的 `run_count`。 6. 因为文件名没有变化且已经在 CMake 白名单中,一般不需要修改 `platform_lcd_flash_assets`。 7. 执行完整构建和烧录: ```bash eim run "idf.py build" v5.5.4 eim run "idf.py flash" v5.5.4 ``` 8. 在串口日志中确认 LittleFS 挂载成功,并检查 LCD 是否有 `open` 或 `read` 失败日志。 如果当前环境没有 `eim`,按照项目 `AGENTS.md` 中规定的 ESP-IDF v5.5.4 激活流程执行相同命令。 ## 13. 给已有动画新增一帧 适用场景:例如已有 `muyu0.rle`、`muyu1.rle`、`muyu2.rle`,现在增加 `muyu3.rle`。 ### 第一步:添加文件 放到: ```text components/platform/assets/lcd/muyu3.rle ``` ### 第二步:加入 CMake 白名单 在 `components/platform/CMakeLists.txt` 的 `platform_lcd_flash_assets` 中加入: ```cmake muyu3 ``` 否则文件不会进入 `gifpack.bin`。 ### 第三步:加入平台资源映射 在 `components/platform/src/lcd_direct_assets.c` 的对应数组中增加: ```c { .path = LCD_ASSET_BASE_PATH "muyu3.rle", .run_count = <实际 run 数>, }, ``` 平台通过数组下标解释 `frame_index`,所以新条目在数组中的位置就是它的帧索引。 ### 第四步:确认业务播放顺序 如果希望业务流程实际播放新帧,还需要检查 `components/domain/src/runtime/domain_runtime_adapters.c` 中的帧顺序,例如: ```c static const uint8_t s_muyu_frame_order[] = {0, 1, 2, 3, 2, 1}; ``` 只增加平台资源映射但不修改播放顺序,新帧可能已经烧入设备,却永远不会被请求。 ### 第五步:完整构建并烧录 ```bash eim run "idf.py build" v5.5.4 eim run "idf.py flash" v5.5.4 ``` ## 14. 新增一组 LCD 资源 适用场景:增加一个新的动画组,例如: ```text happy0.rle happy1.rle happy2.rle ``` 需要同时完成以下工作: 1. 将所有文件放入 `components/platform/assets/lcd/`。 2. 将 `happy0`、`happy1`、`happy2` 加入 `platform_lcd_flash_assets`。 3. 在 `components/platform/src/lcd_direct_assets.c` 中建立资源数组,填写每帧路径和 `run_count`。 4. 在 `lcd_direct_asset_frame()` 中增加资源名到数组的查找映射,使 `asset_dir = "happy"` 能找到对应帧。 5. 如果这是新的业务表达式,按照 `docs/lcd-expressions.md` 的分层要求: - 在 `domain` 公共契约中增加语义表达式 ID; - 在 `domain_runtime_adapters.c` 中映射到 `asset_dir = "happy"`; - 由 `control_plane` 决定何时请求该表达式; - 不要让 `control_plane` 直接调用 `platform_lcd_*`。 6. 构建并完整烧录。 7. 在硬件上验证帧序、帧间隔、中断优先级和播放结束后的返回行为。 这里涉及两个彼此独立的问题: ```text 资源是否进入 Flash -> 由 CMake 白名单决定 资源是否会被业务播放 -> 由 platform 资源映射和 domain 表达式映射决定 ``` 文件成功烧入 Flash,不代表业务代码一定能找到或播放它。 ## 15. 新增或替换音频资源 ### 15.1 替换已有音频 如果文件名和调用路径不变: 1. 替换 `components/platform/assets/audio_prompts/` 下对应的 `.opus` 文件。 2. 执行 `idf.py build` 重新生成 `gifpack.bin`。 3. 执行完整 `idf.py flash` 更新资源分区。 一般不需要修改 CMake,因为 `.opus` 文件会被自动扫描。 ### 15.2 新增音频 1. 将 `.opus` 文件加入 `components/platform/assets/audio_prompts/`。 2. 确认播放代码引用的路径正确: ```text /littlefs/audio_prompts/<文件名>.opus ``` 3. 如果传给 `voice_prompt_play_file()` 的是相对文件名,它会自动补上 `/littlefs/audio_prompts/`。 4. 完整构建并烧录。 ## 16. 推荐验证流程 资源修改后,建议按以下顺序验证。 ### 16.1 检查源码资源 确认: - 文件名和大小写正确; - LCD 文件已加入白名单; - LCD `run_count` 与文件一致; - 资源映射路径与文件名一致。 ### 16.2 构建镜像 ```bash eim run "idf.py build" v5.5.4 ``` 检查是否生成: ```text build/gifpack.bin ``` 也可检查临时目录中是否存在目标文件: ```text build/esp-idf/platform/asset_image/lcd/<资源名>.rle build/esp-idf/platform/asset_image/audio_prompts/<资源名>.opus ``` ### 16.3 检查烧录清单 检查 `build/flasher_args.json` 中是否包含: ```json "gifpack": { "offset": "...", "file": "gifpack.bin" } ``` 不要把 `0x10000` 永久当作固定值,应以当前分区表生成结果为准。 ### 16.4 完整烧录 ```bash eim run "idf.py flash" v5.5.4 ``` 不要只执行 `app-flash`。 ### 16.5 检查运行日志 挂载成功时应出现类似日志: ```text LittleFS mounted at /littlefs, used=... total=... ``` LCD 资源异常时重点关注: ```text open LCD asset failed read LCD asset failed LCD presentation draw failed ``` 音频异常时重点关注: ```text Mount LittleFS failed Open prompt failed ``` ## 17. 常见问题排查 ### 17.1 源码目录里有文件,但设备提示找不到 按顺序检查: 1. LCD 文件是否加入 `platform_lcd_flash_assets`; 2. 是否重新执行了 `idf.py build`; 3. 是否执行了完整 `idf.py flash`; 4. `lcd_direct_assets.c` 中路径是否完全一致; 5. `/littlefs` 是否已经挂载。 ### 17.2 替换图片后还是显示旧图 最常见原因是只执行了 `app-flash` 或 OTA,只更新了应用分区,没有更新 `gifpack`。 重新执行完整烧录: ```bash eim run "idf.py flash" v5.5.4 ``` ### 17.3 能打开文件,但读取失败 检查: - `run_count` 是否与新文件一致; - 文件大小是否能被 4 整除; - 文件是否为当前驱动要求的原始二元组 RLE; - 是否错误使用了 TGA RLE 文件; - RLE 解压后的像素数量是否匹配目标画布。 ### 17.4 构建时报分区空间不足 说明临时目录中的总资源超过 `gifpack` 容量。处理方式包括: - 删除未使用资源并从白名单移除; - 优化资源编码; - 在确认整个 Flash 布局允许后调整 `16m.csv`; - 调整分区表时同时检查 OTA、模型和日志分区是否仍有足够空间。 不要只增大 `gifpack` 而忽略后续分区的地址和总 Flash 容量。 ### 17.5 挂载失败 检查: 1. 是否已经完整烧录 `gifpack.bin`; 2. CMake、分区表、运行时使用的分区名是否都是 `gifpack`; 3. 分区表是否和当前固件一起烧录; 4. 镜像大小是否与当前分区表一致; 5. 是否使用旧构建目录生成的镜像; 6. 必要时执行 `idf.py fullclean` 后重新构建和完整烧录。 由于资源分区设置了 `format_if_mount_failed = false`,挂载失败不会自动格式化,这可以避免资源被意外清空。 ## 18. 修改资源时的最终检查清单 ### 只替换现有 LCD 文件 - [ ] 替换 `components/platform/assets/lcd/.rle` - [ ] 确认文件格式为原始 `uint16_t color + uint16_t count` - [ ] 更新 `lcd_direct_assets.c` 中的 `run_count` - [ ] 执行 `idf.py build` - [ ] 执行完整 `idf.py flash` - [ ] 检查挂载和 LCD 读取日志 ### 新增 LCD 帧或资源组 - [ ] 添加 `.rle` 文件 - [ ] 加入 `platform_lcd_flash_assets` - [ ] 在 `lcd_direct_assets.c` 中增加路径和 `run_count` - [ ] 在 `lcd_direct_asset_frame()` 中增加或确认名称映射 - [ ] 在 domain 层配置播放顺序或表达式映射 - [ ] 执行完整构建和烧录 - [ ] 在硬件上验证实际播放 ### 替换或新增音频 - [ ] 添加或替换 `.opus` 文件 - [ ] 确认调用路径正确 - [ ] 执行 `idf.py build` - [ ] 执行完整 `idf.py flash` - [ ] 检查提示音打开和播放日志 ## 19. 一句话总结 资源交付链路可以概括为: ```text 源码资源 -> CMake 选择并整理目录 -> LittleFS 组件生成 gifpack.bin -> ESP-IDF 根据分区表烧入 gifpack 分区 -> 设备把 gifpack 挂载为 /littlefs -> LCD 和音频通过标准文件 API 读取资源 ``` 新增资源时要同时确认“是否进入 `gifpack.bin`”和“运行时代码是否会找到并使用它”;替换资源时则要特别注意更新 LCD `run_count`,并执行完整 `idf.py flash` 更新独立的资源分区。