资讯动态

ESP-IDF 磨损均衡组件深度解析:Flash_Access 架构、内存组织与 Dummy Page 轮换原理

发布时间:2026/9/14 19:48:42 来源:尧图企业网站定制
ESP-IDF 磨损均衡组件深度解析Flash_Access 架构、内存组织与 Dummy Page 轮换原理【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf本文以 ESP-IDF 仓库中磨损均衡Wear Levelling组件的软件结构文档为核心系统讲解 WLC 的组件构成、Flash_Access 访问接口、启动配置结构 wl_config_t、内部内存的三区域划分以及虚拟/物理地址映射与 dummy page 轮换的核心算法并结合 WL_Flash.cpp、wear_levelling.cpp 等源码还原了从状态恢复、地址换算到磨损计数更新的完整调用链帮助读者既能按文档理解设计意图也能在源码层面验证每一处机制。组件定位与版本兼容性磨损均衡组件Wear Levelling Component下称 WLC是一个用于延长存储介质使用寿命的软件组件。SPI Flash 这类扇区级组织的存储介质每个扇区都有有限的擦写次数如果所有写操作都落在固定位置该位置会先于其他扇区磨损失效。WLC 接管对整个分区所有扇区的访问将擦写负担均匀地摊到全部扇区上从而在不要求用户做任何额外操作的前提下延缓整体失效。两个关键设计约束来自 组件结构文档无内部缓存WLC 不在 RAM 中缓存数据。wl_write/wl_erase_range返回时数据已经真实落盘Flash 内容与主机内存状态始终一致。依赖介质访问驱动WLC 通过 Flash_Access 接口参数化地操作目标扇区本身不绑定具体的 Flash 驱动实现。在版本管理方面文档给出两条规则新版 WLC 可以接受旧版本留下的数据格式并在首次挂载时自动升级到当前格式当前实现的版本号已经是 2且 V2 数据格式与 V1 不向后兼容旧版组件无法读取 V2 格式。源码印证了这一点wear_levelling.cpp 中定义了WL_CURRENT_VERSION 2而 WL_Flash.cpp 中的updateV1_V2()正是 V1→V2 的升级路径——它先用 V1 时代的校验方式CRC 存放在wl_device_id字段位置验证两份状态记录确认为 V1 后重算 CRC、写入随机生成的wl_device_id并把 dummy 位置迁移记录重建到两份状态扇区中。组件构成从 Flash_Access 到 wear_levelling 的类族文档列出了 WLC 由若干组件构成分别实现在不同文件中组件职责对应源码Flash_AccessSPI Flash 内存访问的抽象接口Flash_Access.hSPI_Flash实现 Flash_Access直接访问整片 Flash 内存SPI_Flash.cppPartition实现 Flash_Access访问指定的一个分区Partition.cppWL_Flash核心类实现磨损均衡功能WL_Flash.h、WL_Flash.cppWL_StateWLC 状态结构WL_State.hWL_ConfigWLC 启动配置结构WL_Config.hwear_levelling包装 API提供 C 接口访问 WLCwear_levelling.cpp、wear_levelling.h从源码结构看这是一个清晰的继承链WL_Flash派生自Flash_Access其内部又持有一个Flash_Access *partition指向底层Partition实例。也就是说 WLC 自身就是一个 Flash 访问对象——上层文件系统、块设备把WL_Flash当作一块逻辑 Flash来读写它再把它接收到的逻辑地址翻译成底层分区的物理地址。512 字节扇区模式下还有两个派生类WL_Ext_Perf/WL_Ext_Safe见 wear_levelling.cpp由 Kconfig 选项在wl_mount时选择实例化哪一个。Flash_Access 虚拟接口文档描述 Flash_Access 定义了 read、write、erase、erase_range、flash_size、sector_size、flush 等基本函数。当前仓库中的抽象基类 Flash_Access.h 给出了确切签名get_flash_size()—— 返回可用 Flash 字节数对应文档的 flash_sizeget_sector_size()—— 返回扇区大小对应文档的 sector_sizeread(src_addr, dest, size)/write(dest_addr, src, size)erase_sector(sector)—— 擦除单个扇区erase_range(start_address, size)—— 擦除一段扇区范围范围长度需为扇区大小的整数倍flush()—— 必要时把当前状态存回 Flash默认实现直接返回ESP_OK由子类如WL_Flash::flush覆盖另有is_readonly()用于标识只读设备。这套接口是整个 WLC 的解耦点WLC 只面向Flash_Access编程具体走SPI_Flash还是Partition对上层完全透明。启动配置wl_config_t 结构文档中的启动配置对应 WL_Config.h 中的wl_config_t结构体强制 16 字节对齐并带有crc32校验字段注释说明这是为了兼容 Flash 加密的对齐要求字段含义wl_partition_start_addr磨损均衡分区在 Flash 中的起始地址wl_partition_size分区大小字节flash_sector_sizeFlash 扇区大小即一次擦除/存储的单位wl_page_sizeWLC 页大小满足wl_page_size N * flash_sector_sizeN 0页可以大于扇区wl_update_rate触发物理扇区与 dummy 扇区交换地址的擦除操作计数阈值wl_pos_update_record_size每次wl_update_rate到期后附加在状态扇区数据之后用于记录位置更新的字节数versionWLC 组件版本号要擦除并重分配全部内存该 ID 必须与之前不同wl_temp_buff_size在 Flash 两个区域间拷贝数据的临时缓冲区大小最好等于扇区大小需要注意配置校验逻辑WL_Flash::config()见 WL_Flash.cpp要求flash_sector_size % wl_temp_buff_size 0且wl_page_size flash_sector_size否则返回ESP_ERR_INVALID_ARG。在当前的公共 C API 中用户并不直接填写wl_config_t。wl_mount(const esp_partition_t *partition, wl_handle_t *out_handle)见 wear_levelling.h 与 wear_levelling.cpp在内部用分区参数加上编译期默认值组装配置cfg.wl_partition_start_addr WL_DEFAULT_START_ADDR; // 0 cfg.wl_partition_size partition-size; cfg.wl_page_size partition-erase_size; cfg.flash_sector_size partition-erase_size; cfg.wl_update_rate WL_DEFAULT_UPDATERATE; // 16 cfg.wl_pos_update_record_size WL_DEFAULT_WRITE_SIZE; // 16 cfg.version WL_CURRENT_VERSION; // 2 cfg.wl_temp_buff_size WL_DEFAULT_TEMP_BUFF_SIZE; // 32 cfg.fat_sector_size CONFIG_WL_SECTOR_SIZE;其中WL_DEFAULT_UPDATERATE 16意味着默认每 16 次扇区擦除就推进一次 dummy pageMAX_WL_HANDLES 8限制了可同时挂载的 WLC 实例数。挂载流程还会经过config()与init()两步并受全局锁与每实例锁的双重保护_lock_t保证多任务并发访问安全。除经典分区 API 外wear_levelling.h 还提供wl_get_blockdev()可把 WLC 构建在任意块设备BDL之上此时引擎配置从底层设备几何推导。内部内存组织Configuration / States / Data文档指出WLC 把wl_partition_start_addr与wl_partition_size给出的内存划分为三个区域Configuration—— 存放 WLC 配置信息用户可用它从内存 dump 中恢复 WL 分区States—— 存放 WLC 内部状态且保存两份状态记录副本用于在断电时安全地恢复最后一次已知状态Data—— 用户数据区。WL_Flash::config()中的地址计算见 WL_Flash.cpp给出了具体的布局规则三个区域都锚定在分区尾部。this-cfg_size ceil(sizeof(wl_config_t) / sector_size) * sector_size; this-addr_cfg start size - cfg_size; // 最尾部 this-addr_state1 start size - state_size*2 - cfg_size; this-addr_state2 start size - state_size*1 - cfg_size; this-flash_size ((size - state_size*2 - cfg_size) / wl_page_size - 1) * wl_page_size;状态区大小state_size至少为一个扇区且要容纳sizeof(wl_state_t)加上每个数据扇区一条位置更新记录的空间即sizeof(wl_state_t) (partition_size / sector_size) * wl_pos_update_record_size向上取整到扇区倍数。最后一行计算中- 1正是文档所说的扣除一个 dummy page——用户可用空间比原始 Data 区少一个 WLC 页。wl_state_t见 WL_State.h字段与文档的两个内部计数器一一对应wl_sec_erase_cycle_count—— 擦除计数器access_count每次扇区擦除 1wl_dummy_sec_move_count—— 移动计数器move_countdummy 页走完一整圈后 1wl_dummy_sec_pos—— 当前 dummy 扇区位置wl_part_max_sec_pos—— 参与轮换的扇区总数wl_max_sec_erase_cycle_count—— 擦除计数阈值即wl_update_ratewl_block_size—— WLC 页大小version、wl_device_id首次初始化时随机生成、reserved[7]、crc32。initSections()见 WL_Flash.cpp首次初始化时会把两份状态副本和配置同时写入上述地址并写入esp_random()生成的wl_device_id。核心算法虚拟地址、物理地址与 dummy page 轮换基本思想文档Main Idea一节是 WLC 算法的灵魂WLC 使用两类地址——用户使用的虚拟逻辑地址和 WLC 内部访问真实内存块所用的真实物理地址。Data 区被划分为 N 页页大小可以等于扇区大小见配置其中一页被定义为dummy 页用户实际只能使用 N-1 页存数据。WLC 用两个计数器驱动地址映射erase counteraccess_count每次擦除操作 1。达到wl_update_rate后dummy 页后一页的内容被复制到 dummy 页然后后一页本身成为新的 dummy 页走完一圈时分区最后一页与第一页互换位置。这样 dummy 页会扫过整个分区磨损被均匀分摊。move countermove_countdummy 页完成一个完整循环后 1。每当 move counter 递增WLC 状态记录就被写入 Flash 的状态扇区。文档给出的 4 页示例X 为 dummy 页- X 0 1 2 - 起始位置 - 0 X 1 2 - 第一次移动页 0 与 Dummy 页交换位置 - 0 1 X 2 - 第二次移动页 1 与 Dummy 页交换位置 - 0 1 2 X - - X 1 2 0 - 状态写入内存dummy 回到页 0move_count 1 - 1 X 2 0 - - 1 2 X 0 - - 1 2 0 X - - X 2 0 1 - 状态写入内存 - 2 X 0 1 - - 2 0 X 1 - - 2 0 1 X - - X 0 1 2 - 状态写入内存内存完成一整圈可以看出扇区的擦除周期被分散到整个 Flash 上代价只是 WLC 占用了少量状态 配置 一个 dummy 页内存这部分对用户不可用。虚拟→物理地址映射calcAddr映射公式在 WL_Flash.cpp 中实现非常紧凑size_t WL_Flash::calcAddr(size_t addr) { size_t result (flash_size - state.wl_dummy_sec_move_count * wl_page_size addr) % flash_size; size_t dummy_addr state.wl_dummy_sec_pos * wl_page_size; if (result dummy_addr) { // 保持 } else { result wl_page_size; // 撞上 dummy 页向后顺延一页 } return result; }其中flash_size恰好等于(N-1) * page_sizeconfig 阶段已扣除 dummy 页addr是虚拟偏移。第一步用(flash_size - move_count * page_size addr) % flash_size实现整个逻辑空间随 move_count 的旋转第二步处理逻辑空间与物理空间各有一个洞dummy 页的对齐——若映射落点落在物理 dummy 页上或之后则跳过 dummy 页顺延一页。read()/write()按wl_page_size切块对每一页分别调用calcAddr见 WL_Flash.cpperase_sector()则在擦除前先调用updateWL()推进磨损计数再对换算后的物理扇区执行真实擦除见 WL_Flash.cpp。磨损推进updateWLupdateWL()见 WL_Flash.cpp与文档描述逐步对应wl_sec_erase_cycle_count未达wl_max_sec_erase_cycle_count时直接返回计数器归零确定源页data_addr (dummy_sec_pos 1) % wl_part_max_sec_pos文档中dummy 后一页绕回时最后一页换到第一页擦除 dummy 页用wl_temp_buff_size大小的缓冲分块把源页内容拷入 dummy 页——这正是wl_temp_buff_size用途等于扇区大小最好的原因在两份状态记录后追加位置更新标记fillOkBuff(n)以device_id n*4 i为种子做 crc32 填入 16 字节记录OkBuffSet(n)反向验证。这个位置更新记录就是文档中的wl_pos_update_record_size用途——每个数据扇区一条用于断电后恢复 dummy 页位置dummy_sec_pos绕回 0 时move_count并把整份wl_state_t重新擦写进 state1/state2 两个扇区——即文档所说On the increment of the move counter, the WLC status record is stored to the State sector任何一步 Flash 操作失败时计数器被回拨到阈值 -1wl_sec_erase_cycle_count wl_max_sec_erase_cycle_count - 1下次擦除立即重试迁移保证迁移不会丢失。断电恢复init 与 recoverPos两份状态副本的校验与恢复逻辑在WL_Flash::init()见 WL_Flash.cpp中覆盖文档承诺的安全恢复最后一次已知状态两份 CRC 都正确若两副本不一致一次更新断电中断用完整副本补齐另一份若版本不符配置变化走initSections()全量重建。两份 CRC 都不正确判定为新 Flash 或版本升级先尝试updateV1_V2()失败则initSections()从零初始化。只有一份正确以好的那份重建坏的那份含位置更新记录recoverPos()扫描 state1 中的位置记录逐条OkBuffSet校验找到最后一个有效标记确定dummy_sec_pos见 WL_Flash.cpp。由于状态、位置标记和配置都带 CRC 且有双副本WLC 在任意时刻断电后重启都能回到一致的映射状态这正是无内部缓存 每次写操作即落盘设计的安全基础。配置选项与实测入口Kconfig见 Kconfig暴露两级选择WL_SECTOR_SIZE库使用的扇区大小512 或 4096 字节。4096 效率更高但叠加 FAT 文件系统时每个挂载实例与每个打开的文件需要 4096 字节临时内存512 则 Flash 操作更多但 RAM 占用小。当前 Kconfig 中默认为 4096WL_SECTOR_SIZE_4096。WL_SECTOR_MODE仅 512 模式下可选Performance模式把待擦扇区数据暂存 RAM擦除后写回速度快但擦除过程中断电会丢失整个扇区数据Safety模式先把数据读出、擦除后再写回慢一些但断电可恢复。该选项对应wl_mount中实例化WL_Ext_Perf还是WL_Ext_Safe。组件级说明参见 README.rst其中亦强调 WLC 与 FAT FS 组合使用、写/擦除函数直接修改 Flash 且返回时内容一致。验证方面仓库自带两套测试目标板测试应用 test_apps/test_wl.c配合 partitions.csv 定义的 WL 分区和 pytest_wear_levelling.py 驱动并准备了 512perf / 512safe / 4k 等 sdkconfig 变体分别覆盖上述模式主机端测试 host_test/test_wl.cppLinux 平台见 host_test/README.md便于在无硬件环境下复现读写与状态恢复路径。小结回到 软件结构文档 的骨架WLC 以 Flash_Access 接口解耦介质访问用wl_config_t参数化、用配置区 双状态副本 数据区组织分区内存再以 erase counter / move counter 双计数器驱动 dummy page 扫描实现虚拟地址到物理地址的旋转映射calcAddr()的模运算、updateWL()的迁移流程、init()的 CRC 双副本恢复分别对应文档中Main Idea示例的每一格状态迁移。理解了这条从文档示例到源码实现的对应关系就能既按 APIwl_mount/wl_read/wl_write/wl_erase_range使用 WLC也能在排查磨损分区数据异常时定位到具体的状态扇区与位置更新记录。【免费下载链接】esp-idfEspressif IoT Development Framework. Official development framework for Espressif SoCs.项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价