## 一、STM32 Flash 硬件特性与擦写原理 STM32 系列(如 F1、F4、H7)内置 Flash 采用 NOR 型工艺,具有以下关键特性: - **最小擦除单位**:扇区(Sector)或块(Block),不同型号大小不同(如 F103 的 1KB/扇区,F407 的 16KB/扇区)。 - **写入单位**:半字(16位)或字(32位),需先擦除后写入。 - **擦除操作**:将扇区所有位写为 0xFF,耗时约 20-40ms(取决于型号)。 - **编程操作**:将某些位从 1 改为 0,不可逆,需按字/半字写入。 - **寿命限制**:典型擦写次数 10,000 次(工业级可达 100,000 次),超过后可能产生坏块。 **关键时序**:擦除和编程操作需要解锁 Flash(写入 KEY1/KEY2 到 FLASH_KEYR),操作完成后重新锁定。若违反时序(如未等待 BSY 位清零),会导致总线错误或数据损坏。 ## 二、磨损均衡的必要性与设计思路 若固定使用同一扇区存储参数,写入 10,000 次后扇区即失效。磨损均衡(Wear Leveling)的核心思想是**将写入操作分散到多个扇区**,避免热点扇区过早老化。 ### 常见策略 - **静态均衡**:将数据均匀分配到所有扇区,每次写入选择下一个扇区(轮询)。 - **动态均衡**:记录每个扇区的擦写次数,优先选择次数最少的扇区。 - **日志结构**:将数据以日志形式追加到当前活动扇区,写满后擦除最旧扇区。 ### 设计要点 - 扇区数量至少 2 个,推荐 4-8 个以降低擦写频率。 - 需要元数据(如扇区状态、序号)来管理有效性。 - 掉电安全:需考虑写入中断时的恢复机制(如双备份)。 ## 三、基于 STM32F103 的磨损均衡实现 以下示例使用 STM32F103 的 4 个 1KB 扇区(如扇区 0-3,地址 0x08000000 起),实现简单的轮询磨损均衡。 ### 1. 硬件配置 - 使用标准外设库(SPL)或 HAL 库,本文以 SPL 为例。 - 定义扇区基地址和数量。 ```c #define FLASH_BASE_ADDR 0x08000000 #define SECTOR_SIZE 1024 #define SECTOR_COUNT 4 #define DATA_ADDR (FLASH_BASE_ADDR + 0x10000) // 避开程序区,假设程序占用 <64KB ``` ### 2. Flash 底层操作函数 ```c // 擦除指定扇区(按地址) void Flash_EraseSector(uint32_t addr) { FLASH_Unlock(); while (FLASH_GetFlagStatus(FLASH_FLAG_BSY) == SET); if (FLASH_ErasePage(addr) != FLASH_COMPLETE) { // 错误处理 } FLASH_Lock(); } // 写入一个字(32位) void Flash_WriteWord(uint32_t addr, uint32_t data) { FLASH_Unlock(); while (FLASH_GetFlagStatus(FLASH_FLAG_BSY) == SET); if (FLASH_ProgramWord(addr, data) != FLASH_COMPLETE) { // 错误处理 } FLASH_Lock(); } // 读取一个字 uint32_t Flash_ReadWord(uint32_t addr) { return *(volatile uint32_t*)addr; } ``` ### 3. 磨损均衡管理结构 使用扇区头部存储元数据:状态(有效/无效)、序号(递增)。 ```c #define SECTOR_STATUS_VALID 0xA5A5A5A5 #define SECTOR_STATUS_INVALID 0x00000000 typedef struct { uint32_t status; uint32_t seq; // 写入序号,越大越新 uint32_t data[250]; // 实际数据,1KB-8字节头部 } SectorHeader; ``` ### 4. 核心函数实现 ```c // 查找当前活动扇区(序号最大的有效扇区) uint32_t FindActiveSector(void) { uint32_t max_seq = 0; uint32_t active_addr = 0; for (int i = 0; i < SECTOR_COUNT; i++) { uint32_t addr = DATA_ADDR + i * SECTOR_SIZE; SectorHeader *hdr = (SectorHeader*)addr; if (hdr->status == SECTOR_STATUS_VALID && hdr->seq >= max_seq) { max_seq = hdr->seq; active_addr = addr; } } return active_addr; } // 写入数据(执行磨损均衡) void Flash_WriteData(uint32_t *data, uint32_t len) { uint32_t active = FindActiveSector(); uint32_t new_seq = 0; if (active != 0) { SectorHeader *hdr = (SectorHeader*)active; new_seq = hdr->seq + 1; } // 选择下一个扇区(轮询) int idx = (active == 0) ? 0 : ((active - DATA_ADDR) / SECTOR_SIZE + 1) % SECTOR_COUNT; uint32_t new_addr = DATA_ADDR + idx * SECTOR_SIZE; // 擦除新扇区 Flash_EraseSector(new_addr); // 写入头部和数据 SectorHeader new_hdr; new_hdr.status = SECTOR_STATUS_VALID; new_hdr.seq = new_seq; Flash_WriteWord(new_addr, new_hdr.status); Flash_WriteWord(new_addr + 4, new_hdr.seq); for (uint32_t i = 0; i < len; i++) { Flash_WriteWord(new_addr + 8 + i*4, data[i]); } // 将旧扇区标记为无效(擦除后写入状态字) if (active != 0) { Flash_WriteWord(active, SECTOR_STATUS_INVALID); } } // 读取最新数据 void Flash_ReadData(uint32_t *buf, uint32_t len) { uint32_t active = FindActiveSector(); if (active == 0) return; // 无数据 SectorHeader *hdr = (SectorHeader*)active; for (uint32_t i = 0; i < len; i++) { buf[i] = hdr->data[i]; } } ``` ### 5. 使用示例 ```c int main(void) { uint32_t write_data[4] = {0x12345678, 0x9ABCDEF0, 0xDEADBEEF, 0xCAFEBABE}; uint32_t read_data[4] = {0}; // 写入数据(每次调用会切换扇区) Flash_WriteData(write_data, 4); // 读取数据 Flash_ReadData(read_data, 4); while (1); } ``` ## 四、注意事项与优化建议 - **掉电保护**:在写入新扇区前,先标记旧扇区无效,再擦写新扇区,可避免数据丢失。但若在擦除新扇区时掉电,旧数据仍可恢复。更可靠的做法是使用双缓冲(A/B区)。 - **擦写次数统计**:可在每个扇区头部记录擦写次数,动态选择最少使用的扇区,但会增加管理开销。 - **对齐与缓存**:写入时尽量按字对齐,避免跨扇区操作。 - **中断处理**:Flash 操作期间应关闭中断或确保中断不访问 Flash,否则可能引发硬件错误。 - **使用 HAL 库**:若使用 HAL,函数名类似 `HAL_FLASH_Program`,但底层原理相同。 - **性能考量**:磨损均衡会增加写入延迟(擦除+写入),对于高频写入场景,可考虑外部 EEPROM 或 SPI Flash。 ## 五、总结 STM32 内置 Flash 的扇区擦写与磨损均衡是嵌入式存储管理的基础。通过合理设计扇区管理策略,可以显著延长 Flash 寿命,保证数据可靠性。本文提供的轮询算法简单有效,适用于大多数参数存储场景。对于更复杂的系统,可结合文件系统(如 LittleFS)或使用外部存储。理解底层原理,才能写出健壮的存储代码。