纳入固件源码、屏幕组件、nand_xfer 工具和文档。忽略本机构建产物、sdkconfig,以及含本机路径的依赖锁。 Co-authored-by: Cursor <cursoragent@cursor.com>
5.8 KiB
5.8 KiB
外部 Flash (NAND) 访问时序与并发分析报告
1. 概述
本报告分析 /extflash 挂载的 NAND Flash 在动画切换、UI 操作、下载解压等场景下的访问时序与潜在并发问题。
2. extflash 访问点汇总
2.1 读访问(Lottie JSON、stat、opendir)
| 调用位置 | 任务/上下文 | 操作 | 触发时机 |
|---|---|---|---|
switch_lottie_animation_cross_path_internal |
change_task (core 0) | stat() + lv_lottie_set_src_file() (fopen/fread) |
BLE 指令、动画完成回调、定时器、下载成功 |
main_task |
main_task (core 1) | stat(path_buf) |
处理 DOWNLOAD_SUCCESS_BIT 时 |
extract_task |
extract_task (core 1) | list_files_in_directory() (opendir/readdir), stat() |
解压完成后 |
emoji_storage_sync_downloaded_packs_from_fs |
调用者上下文 | opendir/readdir/stat | emoji_storage 初始化 |
zip_extractor |
extract_task 内 | stat(zip_file_path) |
解压前检查 ZIP 存在 |
change_task 路径解析 |
change_task | 多处 stat() |
解析表情包路径时 |
2.2 写访问
| 调用位置 | 任务/上下文 | 操作 | 触发时机 |
|---|---|---|---|
download_file |
MQTT 回调上下文 | fopen/fwrite (ZIP 下载) | MQTT 收到下载 URL 后 |
extract_zip_file |
extract_task (core 1) | fopen/fwrite (解压 JSON) | 下载完成后 |
check_flash_integrity |
flash_main 初始化 | fopen/fwrite | 启动时 |
OTA extract_running_app_to_storage |
OTA 流程 | 写入 /extflash/flash.bin |
OTA 升级时 |
3. 并发场景分析
3.1 典型并发组合
| 场景 | 任务 A | 任务 B | 是否可能并发 |
|---|---|---|---|
| 1 | change_task 读 Lottie | extract_task 写解压文件 | 是 |
| 2 | change_task 读 Lottie | download_file 写 ZIP | 是 |
| 3 | main_task stat | extract_task 写 | 是 |
| 4 | 动画切换 | 用户手势/UI 操作 | 是(逻辑上并发) |
3.2 底层串行化
- VFS/FAT 层:ESP-IDF 的 fatfs 挂载使用
ff_mutex,同一时刻只允许一个文件操作。 - NAND 硬件:单 SPI 总线,单设备,物理上只能串行传输。
因此,不会出现真正的硬件级并发读写,但会出现多个任务排队等待同一 NAND 访问。
4. 关键问题
4.1 持锁期间进行 NAND 读(UI 卡顿)
lvgl_init.c 中:
// 约 1185-1188 行
_lock_acquire(&lvgl_api_lock);
lv_lottie_set_src_file(current_lottie, file_path); // 内部会 fopen/fread 从 NAND 读
// ...
_lock_release(&lvgl_api_lock);
lv_lottie_set_src_file会从 NAND 读取整个 Lottie JSON,耗时较长。- 整个读取过程在持有
lvgl_api_lock期间执行。 - LVGL 任务需要该锁才能刷新界面,因此动画切换期间 UI 会冻结。
- 若用户在切换过程中滑动手势,UI 回调会卡在等待锁释放。
4.2 写操作阻塞读
extract_task解压大量 JSON 时,会长时间占用 FAT 锁。- 此时
change_task若执行stat()或lv_lottie_set_src_file(),会阻塞在 FAT 层。 - 导致动画切换延迟或卡顿。
4.3 读操作阻塞写
change_task正在读取 Lottie 时,若extract_task或download_file开始写,会阻塞在 FAT 层。- 下载/解压时间被拉长。
4.4 UI 手势与 extflash 的关系
ui_event_gesture_cb不直接访问 extflash,只做ui_change_page(lv_screen_load_anim)。- 但若此时
change_task正在执行switch_lottie_animation_cross_path_internal并持有lvgl_api_lock,LVGL 任务会阻塞,手势处理会延迟。
5. 任务与核心分布
| 任务 | 核心 | 优先级 | 主要 extflash 操作 |
|---|---|---|---|
| change_task | 0 | CHANGE_TASK_PRIORITY | 读 Lottie、stat |
| 主 LVGL 任务 | 0 | EXAMPLE_LVGL_TASK_PRIORITY | 无直接访问 |
| main_task | 1 | - | stat |
| extract_task | 1 | configMAX_PRIORITIES - 1 | 写解压、list_files、stat |
| download_file | 调用者上下文 (MQTT) | - | 写 ZIP |
6. 建议措施
6.1 短期(缓解 UI 卡顿)
-
分离锁与文件读取:
- 先
stat()检查文件是否存在(不持 lvgl 锁)。 - 在持锁前,将文件内容读入内存缓冲区(或使用 LVGL 支持的内存源)。
- 持锁期间只做
lv_lottie_set_src_buffer或等效 API,不进行 NAND 读。
- 先
-
缩短持锁时间:
- 若无法改为内存源,至少将
lv_lottie_set_src_file移出锁外,仅把 LVGL 对象创建/修改等必须持锁的部分放在锁内。
- 若无法改为内存源,至少将
6.2 中期(统一访问顺序)
-
增加 extflash 访问互斥锁:
- 所有对
/extflash的 fopen/fread/fwrite/stat/opendir 等操作统一通过一个extflash_mutex保护。 - 避免长时间写阻塞短时读,可考虑读写锁(如支持)或优先级策略。
- 所有对
-
解压时暂停动画切换:
- 解压开始前设置标志,
change_task收到切换请求时若发现解压中则排队或延后。 - 解压完成后再处理排队请求。
- 解压开始前设置标志,
6.3 长期(架构优化)
-
预加载/缓存:
- 常用 Lottie 预先读入内存或临时文件,切换时减少 NAND 访问。
-
异步加载:
- 先显示占位或上一帧,后台异步读 Lottie,完成后切换。
-
NAND 性能调优:
- 检查 SPI 时钟、DMA、队列等配置,在稳定前提下提升吞吐。
7. 结论
- 并发访问:不会出现硬件级并发,但多任务会排队访问 NAND,存在相互阻塞。
- 动画切换:在持
lvgl_api_lock期间进行 NAND 读,会导致 UI 卡顿。 - UI 操作:手势本身不访问 extflash,但会因等待
lvgl_api_lock而受动画切换影响。 - 建议:优先将持锁与 NAND 读分离,并考虑引入 extflash 访问互斥与解压/切换协调逻辑。