引言

STM32F4 系列微控制器(如 STM32F407)内部集成了大容量 Flash,但缺少真正的 EEPROM。Flash 的擦写寿命(通常 10,000 次)远低于 EEPROM(100,000 次以上),且必须以扇区为单位擦除。直接使用 Flash 存储频繁更新的参数会导致寿命迅速耗尽。因此,需要设计一种软件层,模拟 EEPROM 的按字节读写功能,同时通过磨损均衡(Wear Leveling)延长 Flash 寿命,并通过掉电保护确保数据一致性。

原理讲解

Flash 硬件特性

  • 扇区擦除:STM32F4 的 Flash 扇区大小不等(如 16KB、64KB),擦除操作会将整个扇区置为 0xFF。
  • 写入限制:只能将 1 写为 0,不能反向操作。写入最小单位通常为 16 位(半字)或 32 位(字)。
  • 寿命限制:每个扇区擦写次数有限,超过后可能损坏。

磨损均衡算法

磨损均衡的核心思想是将写入操作分散到多个扇区,避免固定扇区被频繁擦写。常见方法有:

  • 日志结构(Log-structured):将数据以记录形式追加写入当前活动扇区,当扇区写满时,擦除下一个扇区并继续。
  • 双扇区轮换:使用两个扇区,交替写入,每次写入前先检查当前扇区剩余空间,若不足则切换到另一个扇区并擦除旧扇区。

本文采用多扇区日志式方案,每个扇区内部维护记录头,包含有效标志、数据长度和 CRC 校验。

掉电保护策略

掉电可能发生在写入过程中,导致数据半写或记录不完整。保护策略包括:

  • 原子写入:先写入数据到临时位置,再更新有效标志。
  • CRC 校验:每条记录附带 CRC,读取时校验,无效记录视为垃圾。
  • 启动扫描:上电时扫描所有扇区,找到最新有效记录,并清理无效记录。

配置步骤

1. 硬件资源

  • 使用 STM32F407 开发板,Flash 扇区 11(地址 0x080E0000,大小 16KB)和扇区 12(0x080E4000,16KB)作为模拟 EEPROM 区域。
  • 注意:避免使用存放程序代码的扇区。

2. 软件设计

定义数据结构:

#define EEPROM_SECTOR_SIZE   (16 * 1024)
#define EEPROM_SECTOR_COUNT  2
#define EEPROM_START_ADDR    0x080E0000

// 记录头(8字节)
typedef struct {
    uint16_t crc;       // CRC16 校验
    uint16_t len;       // 数据长度(字节)
    uint32_t valid;     // 有效标志(0xA5A5A5A5 表示有效)
} RecordHeader;

3. 核心函数实现

3.1 擦除扇区

void eeprom_erase_sector(uint32_t addr) {
    FLASH_EraseInitTypeDef erase;
    uint32_t page_error = 0;
    erase.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase.Sector = get_sector_num(addr);
    erase.NbSectors = 1;
    erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
    HAL_FLASH_Unlock();
    HAL_FLASHEx_Erase(&erase, &page_error);
    HAL_FLASH_Lock();
}

3.2 写入记录

int eeprom_write(uint32_t key, uint8_t *data, uint16_t len) {
    // 1. 查找当前活动扇区(最后一个有效记录所在扇区)
    uint32_t active_sector = find_active_sector();
    uint32_t write_addr = find_free_addr(active_sector);
    
    // 2. 若剩余空间不足,切换到下一个扇区并擦除
    if (write_addr + sizeof(RecordHeader) + len > active_sector + EEPROM_SECTOR_SIZE) {
        uint32_t next_sector = get_next_sector(active_sector);
        eeprom_erase_sector(next_sector);
        write_addr = next_sector;
    }
    
    // 3. 写入数据(先写数据,再写头部)
    HAL_FLASH_Unlock();
    // 写入数据内容(按半字)
    for (uint16_t i = 0; i < len; i += 2) {
        uint16_t half = (i+1 < len) ? (data[i] | (data[i+1] << 8)) : data[i];
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, write_addr + sizeof(RecordHeader) + i, half);
    }
    // 写入头部(先写 crc 和 len,最后写 valid)
    RecordHeader hdr;
    hdr.crc = crc16(data, len);
    hdr.len = len;
    hdr.valid = 0xA5A5A5A5;
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, write_addr, *(uint32_t*)&hdr);
    HAL_FLASH_Lock();
    return 0;
}

3.3 读取最新记录

int eeprom_read(uint32_t key, uint8_t *buf, uint16_t *len) {
    // 从最后一个扇区向前扫描,找到匹配 key 的最新有效记录
    for (int s = EEPROM_SECTOR_COUNT-1; s >= 0; s--) {
        uint32_t sector_addr = EEPROM_START_ADDR + s * EEPROM_SECTOR_SIZE;
        uint32_t addr = sector_addr;
        while (addr < sector_addr + EEPROM_SECTOR_SIZE) {
            RecordHeader *hdr = (RecordHeader*)addr;
            if (hdr->valid == 0xA5A5A5A5) {
                // 校验 CRC
                if (crc16((uint8_t*)(addr + sizeof(RecordHeader)), hdr->len) == hdr->crc) {
                    memcpy(buf, (uint8_t*)(addr + sizeof(RecordHeader)), hdr->len);
                    *len = hdr->len;
                    return 0;
                }
            }
            // 跳到下一条记录(若头部无效,则结束)
            if (hdr->valid != 0xA5A5A5A5 && hdr->valid != 0xFFFFFFFF) break;
            addr += sizeof(RecordHeader) + hdr->len;
        }
    }
    return -1; // 未找到
}

3.4 启动扫描与垃圾回收

void eeprom_init(void) {
    // 扫描所有扇区,如果发现有效记录,则记录最新位置;否则初始化第一个扇区
    // 若某个扇区无效记录过多,可执行压缩:将有效记录复制到新扇区,然后擦除旧扇区
}

完整代码示例

以下为简化版完整代码(省略 CRC 实现和部分辅助函数):

// eeprom_emul.h
#ifndef __EEPROM_EMUL_H
#define __EEPROM_EMUL_H

#include "stm32f4xx_hal.h"

#define EEPROM_OK       0
#define EEPROM_ERROR   -1

int eeprom_write(uint32_t key, uint8_t *data, uint16_t len);
int eeprom_read(uint32_t key, uint8_t *buf, uint16_t *len);
void eeprom_init(void);

#endif
// eeprom_emul.c
#include "eeprom_emul.h"

// ... 实现上述函数

注意事项

  • 扇区选择:务必确认所选扇区未被程序代码占用,否则会覆盖程序。
  • 写入对齐:Flash 写入必须按半字或字对齐,数据长度最好为偶数。
  • 掉电处理:在写入过程中掉电,可能导致头部 valid 未写入,此时该记录视为无效,但数据可能残留,不影响正确性。
  • 磨损均衡效果:本算法将写入分散到多个扇区,但若数据量小且频繁,仍可能集中在某个扇区,可增加扇区数量或采用更复杂的均衡策略。
  • 性能考虑:每次写入需扫描扇区,耗时较长,可缓存当前写入位置以加速。

总结

通过日志式磨损均衡和 CRC 校验,STM32F4 的片上 Flash 可以可靠地模拟 EEPROM,满足大多数嵌入式应用需求。本文提供的代码框架可直接移植到实际项目中,开发者可根据需求调整扇区大小和数量。掉电保护策略确保了数据一致性,是工业级应用的关键。希望本文能帮助你更好地利用 STM32F4 的 Flash 资源。