微控制器 EEPROM 上的磨损均衡

电器工程 微控制器 eeprom 算法
2022-01-26 01:49:23

例如: ATtiny2313 的数据表(与大多数 Atmel AVR 数据表一样)指出:

128 字节系统内可编程 EEPROM 耐用性:100,000 次写入/擦除周期

想象一个程序只需要两个字节来存储一些配置,其他 126 个字节实际上被浪费了。我担心的是,定期更新两个配置字节可能会磨损设备的 EEPROM 并使其无用。整个设备将变得不可靠,因为在某个时刻您无法跟踪 EEPROM 中的哪些字节不可靠。

当您有效地仅使用可用的 128 个字节中的一个或两个字节时,是否有一种智能方法可以在微控制器的 EEPROM 上进行磨损均衡?

4个回答

我通常使用的技术是在数据前面加上一个 4 字节的滚动序列号,其中最大的数字代表最新/当前值。在存储 2 个字节的实际数据的情况下,总共 6 个字节,然后我形成一个循环队列排列,因此对于 128 个字节的 EEPROM,它将包含 21 个条目并增加 21 倍的耐用性。

然后在启动时可以使用最大的序列号来确定下一个要使用的序列号和队列的当前尾部。以下 C 伪代码演示,这假设在初始编程时 EEPROM 区域已被擦除为 0xFF 的值,因此我忽略了 0xFFFF 的序列号:

struct
{
  uint32_t sequence_no;
  uint16_t my_data;
} QUEUE_ENTRY;

#define EEPROM_SIZE 128
#define QUEUE_ENTRIES (EEPROM_SIZE / sizeof(QUEUE_ENTRY))

uint32_t last_sequence_no;
uint8_t queue_tail;
uint16_t current_value;

// Called at startup
void load_queue()
{
  int i;

  last_sequence_no = 0;
  queue_tail = 0;
  current_value = 0;
  for (i=0; i < QUEUE_ENTRIES; i++)
  {
    // Following assumes you've written a function where the parameters
    // are address, pointer to data, bytes to read
    read_EEPROM(i * sizeof(QUEUE_ENTRY), &QUEUE_ENTRY, sizeof(QUEUE_ENTRY));
    if ((QUEUE_ENTRY.sequence_no > last_sequence_no) && (QUEUE_ENTRY.sequence_no != 0xFFFF))
    {
      queue_tail = i;
      last_sequence_no = QUEUE_ENTRY.sequence_no;
      current_value = QUEUE_ENTRY.my_data;
    }
  }
}

void write_value(uint16_t v)
{
  queue_tail++;
  if (queue_tail >= QUEUE_ENTRIES)
    queue_tail = 0;
  last_sequence_no++;
  QUEUE_ENTRY.sequence_no = last_sequence_no;
  QUEUE_ENTRY.my_data = v;
  // Following assumes you've written a function where the parameters
  // are address, pointer to data, bytes to write
  write_EEPROM(queue_tail * sizeof(QUEUE_ENTRY), &QUEUE_ENTRY, sizeof(QUEUE_ENTRY));
  current_value = v;
}

对于较小的 EEPROM,3 字节序列会更有效,尽管需要一些位切片而不是使用标准数据类型。

以下是一种使用桶和每个桶大约一个开销字节的方法。桶字节和开销字节的磨损量大致相同。在手头的示例中,给定 128 个 EEPROM 字节,此方法分配 42 个 2 字节存储桶和 44 个状态字节,将磨损能力提高约 42 倍。

方法:

将 EEPROM 地址空间划分为k个桶,其中k =⌊ E /( n +1)⌋,其中n = setup-data-array size = 桶大小,E = EEPROM 大小(或者更一般地说,EEPROM 的数量专门用于此数据结构的单元格)。

初始化一个目录,一个 m 字节数组,全部设置为k,其中m = En·k当您的设备启动时,它会读取目录,直到找到当前条目,这是一个不等于k​​的字节。[如果所有目录条目都等于k ​​,则将第一个目录条目初始化为 0,然后从那里继续。]

当前目录条目包含j时,桶j包含当前数据。当需要编写新的 setup-data 条目时,将j +1 存储到当前目录条目中;如果这使它等于k ​​,则将下一个目录条目初始化为 0,然后从那里继续。

请注意,目录字节与桶字节的磨损量大致相同,因为 2· k > mk

(我根据我对 Arduino SE 问题 34189 的回答改编了上述内容如何增加 EEPROM 的寿命?。)

根据您拥有的 EEPROM 类型和数据大小,有几个选项。

  1. 如果您的 EEPROM 具有可单独擦除的页并且您使用 1 页(或更多页),则只需擦除除正在使用的页之外的所有页,并以循环方式重复使用页。

  2. 如果您只使用必须一次擦除的页面的一部分,则将该页面划分为数据条目。每次写作时使用干净的条目,一旦用完干净的条目,请擦除。

如有必要,使用“脏”位来区分干净条目和脏条目(通常,您至少有一个字节可以保证与 0xFF 不同,可用于跟踪脏条目)。

如果您的 EEPROM 库没有公开擦除功能(如 Arduino),这里有一个算法#2 的巧妙技巧:由于始终使用您的第一个 EEPROM 条目,您可以通过读取它来确定“脏”位的值。然后,一旦您用完干净的条目,您只需从第一个条目重新开始,反转“脏”位,其余条目会自动标记为“干净”。

除非您希望能够跟踪坏页或独立更新 EEPROM 数据的不同部分,否则序列号和目录会浪费空间。

我为此使用了滚动序列号(类似于彼得的回答)。序列号实际上可以少到 1 位,前提是提示中的元素数量是奇数。然后头部和尾部由 2 个连续的 1 或 0 标记

例如,如果想要滚动 5 个元素,则序列号将是:

{01010}(写入 0) {11010}(写入 1) {10010}(写入 2) {10110}(写入 3) {10100}(写入 4) {10101}(写入 5)