一、EEPROM 的物理特性与挑战

EEPROM(Electrically Erasable Programmable Read-Only Memory)支持字节级擦写,但每次写入前需先擦除(通常按页或整片),且擦写次数有限(典型 10 万次)。在频繁保存数据的场景(如设备计数器、用户设置),若不加以管理,很快会耗尽寿命。此外,掉电瞬间可能发生写入中断,导致数据损坏或丢失。因此,设计需兼顾数据完整性磨损均衡

二、掉电保存设计:从硬件到软件

2.1 硬件掉电检测

  • 使用 MCU 内置的 BOD(Brown-Out Detector)或外部电压监控芯片(如 TPS3839),在电源跌落至阈值时触发中断。
  • 在中断服务函数中,立即将关键数据写入 EEPROM,并禁止其他中断,确保写入过程不被干扰。
  • 注意:EEPROM 写入时间约 3-5ms,需保证掉电后电源能维持足够时间(通常通过大电容储能)。

2.2 软件数据缓冲策略

  • 双备份(Double Buffer):将数据存储在两个固定区域,写入时先写备份区,再写主区,读取时校验主区,若损坏则从备份区恢复。
  • 校验和(Checksum):在数据末尾附加 CRC 或累加和,读取时校验,防止写入中断导致的数据不完整。

三、磨损均衡算法详解

磨损均衡的核心是避免反复擦写同一地址,将写入操作分散到整个存储空间。常用方法有:

3.1 轮询写入(Round-Robin)

将存储区划分为 N 个槽位,每次写入使用下一个槽位,并记录当前槽位索引。读取时需扫描所有槽位,找到最新有效数据。

优点:实现简单,均衡效果好。 缺点:读取需遍历,速度较慢;需额外存储索引。

3.2 日志结构(Log-Structured)

类似日志文件系统,每次写入追加到存储区末尾,当写满时进行垃圾回收(擦除旧数据)。

优点:写入效率高,适合频繁更新。 缺点:需要管理日志头,回收机制复杂。

3.3 动态磨损均衡(结合索引表)

在 RAM 中维护一个逻辑地址到物理地址的映射表,每次写入选择擦写次数最少的物理页。适用于较大容量 EEPROM。

四、完整代码示例(基于 STM32 HAL 库)

以下示例实现轮询写入 + 双备份 + 校验和,适用于保存 16 字节的用户数据。

// eeprom_manage.h
#ifndef EEPROM_MANAGE_H
#define EEPROM_MANAGE_H

#include "stdint.h"
#include "stdbool.h"

#define DATA_SIZE       16          // 数据长度(字节)
#define SLOT_COUNT      8           // 槽位数量(需为偶数,便于双备份)
#define SLOT_SIZE       (DATA_SIZE + 2) // 数据 + 校验和(2字节)
#define EEPROM_START_ADDR 0x08080000 // 假设外部 EEPROM 起始地址,内部需调整

typedef struct {
    uint8_t data[DATA_SIZE];
    uint16_t checksum;
} Slot_t;

bool EEPROM_Save(uint8_t *data);
bool EEPROM_Load(uint8_t *data);
void EEPROM_Init(void);

#endif
// eeprom_manage.c
#include "eeprom_manage.h"
#include "i2c.h" // 假设使用 I2C 接口的外部 EEPROM

static uint8_t current_slot = 0; // 当前写入槽位索引

// 计算校验和(简单累加)
static uint16_t calc_checksum(uint8_t *data) {
    uint16_t sum = 0;
    for (int i = 0; i < DATA_SIZE; i++) {
        sum += data[i];
    }
    return sum;
}

// 写入一个槽位
static void write_slot(uint8_t slot, uint8_t *data) {
    uint16_t addr = EEPROM_START_ADDR + slot * SLOT_SIZE;
    uint16_t checksum = calc_checksum(data);
    // 先写数据,再写校验和(分两次写,防止中断导致校验和不匹配)
    HAL_I2C_Mem_Write(&hi2c1, 0xA0, addr, 2, data, DATA_SIZE, 100);
    HAL_I2C_Mem_Write(&hi2c1, 0xA0, addr + DATA_SIZE, 2, (uint8_t*)&checksum, 2, 100);
}

// 读取并校验槽位,返回是否有效
static bool read_slot(uint8_t slot, uint8_t *data) {
    uint16_t addr = EEPROM_START_ADDR + slot * SLOT_SIZE;
    uint8_t buf[SLOT_SIZE];
    HAL_I2C_Mem_Read(&hi2c1, 0xA0, addr, 2, buf, SLOT_SIZE, 100);
    uint16_t checksum = (buf[DATA_SIZE] << 8) | buf[DATA_SIZE+1];
    if (calc_checksum(buf) == checksum) {
        memcpy(data, buf, DATA_SIZE);
        return true;
    }
    return false;
}

// 初始化:扫描所有槽位,找到最新有效数据(槽位索引最大且校验通过)
void EEPROM_Init(void) {
    uint8_t temp[DATA_SIZE];
    current_slot = 0;
    for (int i = SLOT_COUNT-1; i >= 0; i--) {
        if (read_slot(i, temp)) {
            current_slot = i;
            break;
        }
    }
}

// 保存数据:写入下一个槽位(循环),并更新当前索引
bool EEPROM_Save(uint8_t *data) {
    uint8_t next_slot = (current_slot + 1) % SLOT_COUNT;
    write_slot(next_slot, data);
    // 写入后立即验证
    uint8_t verify[DATA_SIZE];
    if (read_slot(next_slot, verify)) {
        if (memcmp(verify, data, DATA_SIZE) == 0) {
            current_slot = next_slot;
            return true;
        }
    }
    return false; // 写入失败,保持原槽位
}

// 读取数据:从当前槽位读取,若失败则尝试前一个槽位(双备份)
bool EEPROM_Load(uint8_t *data) {
    if (read_slot(current_slot, data)) {
        return true;
    }
    // 尝试前一个槽位(备份)
    uint8_t prev_slot = (current_slot == 0) ? (SLOT_COUNT-1) : (current_slot-1);
    if (read_slot(prev_slot, data)) {
        return true;
    }
    return false;
}

使用说明

  • 初始化时调用 EEPROM_Init(),上电后调用 EEPROM_Load() 读取数据。
  • 需要保存时调用 EEPROM_Save(),内部自动轮询槽位。
  • 掉电中断中调用 EEPROM_Save() 时,需确保 I2C 中断优先级最高,或使用轮询方式。

五、注意事项与优化建议

  • 写入时间:EEPROM 写入较慢,频繁保存会阻塞主循环,建议采用“脏数据标记 + 定时批量保存”策略。
  • 磨损均衡粒度:槽位数量越多,均衡效果越好,但占用空间增大,需权衡。
  • 掉电保护:在写入过程中掉电,可能导致校验和不匹配,双备份可有效恢复。
  • 外部 EEPROM vs 内部:内部 EEPROM 擦写次数更少(通常 10 万次),外部如 AT24Cxx 可达 100 万次,选型时注意。
  • 测试:使用循环写入测试寿命,并模拟掉电(随机中断写入)验证数据完整性。

六、总结

EEPROM 数据管理是嵌入式开发中的关键细节,通过掉电检测、双备份和磨损均衡,可以显著提升系统可靠性。本文提供的轮询写入算法简单高效,适用于大多数场景。对于更高频率的写入,可考虑日志结构或外部 FRAM(铁电存储器)替代。希望本文能帮助你在实际项目中设计出健壮的存储方案。