# 外部 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` 中: ```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 卡顿) 1. **分离锁与文件读取**: - 先 `stat()` 检查文件是否存在(不持 lvgl 锁)。 - 在持锁前,将文件内容读入内存缓冲区(或使用 LVGL 支持的内存源)。 - 持锁期间只做 `lv_lottie_set_src_buffer` 或等效 API,不进行 NAND 读。 2. **缩短持锁时间**: - 若无法改为内存源,至少将 `lv_lottie_set_src_file` 移出锁外,仅把 LVGL 对象创建/修改等必须持锁的部分放在锁内。 ### 6.2 中期(统一访问顺序) 1. **增加 extflash 访问互斥锁**: - 所有对 `/extflash` 的 fopen/fread/fwrite/stat/opendir 等操作统一通过一个 `extflash_mutex` 保护。 - 避免长时间写阻塞短时读,可考虑读写锁(如支持)或优先级策略。 2. **解压时暂停动画切换**: - 解压开始前设置标志,`change_task` 收到切换请求时若发现解压中则排队或延后。 - 解压完成后再处理排队请求。 ### 6.3 长期(架构优化) 1. **预加载/缓存**: - 常用 Lottie 预先读入内存或临时文件,切换时减少 NAND 访问。 2. **异步加载**: - 先显示占位或上一帧,后台异步读 Lottie,完成后切换。 3. **NAND 性能调优**: - 检查 SPI 时钟、DMA、队列等配置,在稳定前提下提升吞吐。 --- ## 7. 结论 - **并发访问**:不会出现硬件级并发,但多任务会排队访问 NAND,存在相互阻塞。 - **动画切换**:在持 `lvgl_api_lock` 期间进行 NAND 读,会导致 UI 卡顿。 - **UI 操作**:手势本身不访问 extflash,但会因等待 `lvgl_api_lock` 而受动画切换影响。 - **建议**:优先将持锁与 NAND 读分离,并考虑引入 extflash 访问互斥与解压/切换协调逻辑。