磨损均衡核心算法(二):基于桶的算法、哈希映射与滑动窗口

好,咱们接着聊磨损均衡。上一章讲了基本思想,这一章我直接上干货——三种实战中常用的算法。说实话,这三种算法我都在项目里亲手调过,各有各的脾气。

一、基于桶(Bucket)的算法

先说说桶算法。这玩意儿思路特别朴素,你想想看,就是把EEPROM的存储空间分成若干个“桶”。每个桶里放数据,同时记录这个桶被擦写了多少次。

核心思想:每次写数据时,挑那个擦写次数最少的桶来写。这样所有桶的磨损程度会慢慢趋近于平均。

我在一个车身控制模块的项目里用过这个方案。当时要存故障码,数据量不大但写得很频繁。我画了个草图给团队看——其实就是把64字节的EEPROM分成8个桶,每个桶8字节。

桶算法的基本流程:

  1. 初始化时,把所有桶的擦写计数清零
  2. 每次写入前,遍历所有桶的计数
  3. 选计数最小的桶来写
  4. 写完后更新这个桶的计数
  5. 如果某个桶的计数超过阈值,触发全局磨损均衡
// 桶算法伪代码示例
#define BUCKET_NUM  8
#define BUCKET_SIZE 8

typedef struct {
    uint8_t data[BUCKET_SIZE];
    uint16_t erase_count;
} Bucket_t;

Bucket_t buckets[BUCKET_NUM];

uint8_t select_bucket(void) {
    uint8_t min_idx = 0;
    uint16_t min_count = buckets[0].erase_count;
    
    for (int i = 1; i < BUCKET_NUM; i++) {
        if (buckets[i].erase_count < min_count) {
            min_count = buckets[i].erase_count;
            min_idx = i;
        }
    }
    return min_idx;
}

我的经验:桶的数量别设太多。我曾经在一个项目里设了32个桶,结果每次写入前遍历开销太大,影响了实时性。后来改成8个桶,效果反而更好。嗯,这里要注意——桶算法适合数据量小、写入频率不高的场景。

二、基于哈希映射的算法

哈希映射算法就高级一些了。说白了,就是用哈希函数把逻辑地址映射到物理地址上。每次映射关系都变一下,这样数据不会老往同一个物理地址上写。

为什么需要哈希?因为桶算法有个问题——如果某个桶的数据特别重要,老是被读,那它的磨损还是比其他桶快。哈希映射能打散这种“热点”。

我记得在做一个OBD诊断模块时,DTC故障码的存储就用了哈希映射。当时有个坑——哈希函数选得不好,导致映射不均匀。我调试了整整两天才找到原因。

// 哈希映射算法示例
#define EEPROM_SIZE  1024
#define PAGE_SIZE    16
#define PAGE_NUM     (EEPROM_SIZE / PAGE_SIZE)

uint16_t hash_function(uint16_t logical_addr, uint16_t seed) {
    // 简单的线性同余哈希
    return (logical_addr * 2654435761 + seed) % PAGE_NUM;
}

uint16_t logical_to_physical(uint16_t logical_addr, uint16_t write_count) {
    uint16_t seed = write_count % PAGE_NUM;
    return hash_function(logical_addr, seed);
}

避坑指南:我曾经遇到过哈希冲突导致数据覆盖的问题。解决方案是加一个冲突链表,或者用二次探测法。千万别图省事直接用线性探测,在EEPROM上效率太低了。

哈希映射的好处是灵活性高,坏处是每次读写都要算一次哈希,对MCU性能有要求。我个人习惯在Cortex-M3以上的核上用这个算法,8位机还是老老实实用桶算法吧。

三、滑动窗口算法

滑动窗口算法,这个名字听起来挺唬人,其实原理很简单。你想象一个窗口在EEPROM的地址空间上滑动,每次写数据就写在窗口当前的位置,然后窗口往前挪一点。

核心机制:

  • 窗口大小固定,比如64字节
  • 窗口起始地址随着写入次数递增
  • 当窗口滑到末尾时,回到开头(环形缓冲区)
  • 每个地址被写入的次数自然趋于平均

我在一个雨量传感器的项目中用过这个算法。那个传感器每100毫秒就要存一次数据,写入频率很高。桶算法扛不住,哈希映射又太费CPU。最后选了滑动窗口,效果出奇的好。

// 滑动窗口算法示例
#define WINDOW_SIZE  64
#define MEMORY_SIZE  1024

typedef struct {
    uint16_t window_start;
    uint16_t write_count;
    uint8_t buffer[WINDOW_SIZE];
} SlidingWindow_t;

void write_to_window(SlidingWindow_t *sw, uint8_t *data) {
    // 计算当前写入位置
    uint16_t offset = sw->write_count % WINDOW_SIZE;
    uint16_t addr = sw->window_start + offset;
    
    // 写入数据
    eeprom_write(addr, data, sizeof(data));
    
    // 更新计数
    sw->write_count++;
    
    // 窗口滑动
    if (sw->write_count % WINDOW_SIZE == 0) {
        sw->window_start = (sw->window_start + WINDOW_SIZE) % MEMORY_SIZE;
    }
}

三种算法对比:

算法 适用场景 优点 缺点
桶算法 低频写入,小数据量 实现简单,开销小 热点问题,扩展性差
哈希映射 中高频写入,数据量大 分布均匀,灵活性高 计算开销大,有冲突风险
滑动窗口 高频写入,固定数据量 磨损最均匀,实现简单 地址空间利用率低

你可能会问,这三种算法到底选哪个?我的建议是——先看你的写入频率。如果一天写不了几次,桶算法就够了。如果每秒写好几次,滑动窗口更靠谱。哈希映射嘛,适合那些对灵活性要求高的场景。

我的个人习惯:在项目初期,我会先用桶算法快速验证功能。等系统稳定了,再根据实际写入频率换成更合适的算法。别一开始就上复杂的算法,容易把自己绕进去。

最后说一句,这三种算法不是互斥的。我在一个项目里就同时用了桶算法和滑动窗口——低频数据用桶,高频数据用窗口。嗯,灵活搭配才是王道。