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