一、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 为例。
  • 定义扇区基地址和数量。
#define FLASH_BASE_ADDR  0x08000000
#define SECTOR_SIZE      1024
#define SECTOR_COUNT     4
#define DATA_ADDR        (FLASH_BASE_ADDR + 0x10000) // 避开程序区,假设程序占用 <64KB

2. Flash 底层操作函数

// 擦除指定扇区(按地址)
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. 磨损均衡管理结构

使用扇区头部存储元数据:状态(有效/无效)、序号(递增)。

#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. 核心函数实现

// 查找当前活动扇区(序号最大的有效扇区)
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. 使用示例

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)或使用外部存储。理解底层原理,才能写出健壮的存储代码。