磨损均衡核心算法(二):基于桶的算法、哈希映射与滑动窗口
好,咱们接着聊磨损均衡。上一章讲了基本思想,这一章我直接上干货——三种实战中常用的算法。说实话,这三种算法我都在项目里亲手调过,各有各的脾气。
一、基于桶(Bucket)的算法
先说说桶算法。这玩意儿思路特别朴素,你想想看,就是把EEPROM的存储空间分成若干个“桶”。每个桶里放数据,同时记录这个桶被擦写了多少次。
核心思想:每次写数据时,挑那个擦写次数最少的桶来写。这样所有桶的磨损程度会慢慢趋近于平均。
我在一个车身控制模块的项目里用过这个方案。当时要存故障码,数据量不大但写得很频繁。我画了个草图给团队看——其实就是把64字节的EEPROM分成8个桶,每个桶8字节。
桶算法的基本流程:
- 初始化时,把所有桶的擦写计数清零
- 每次写入前,遍历所有桶的计数
- 选计数最小的桶来写
- 写完后更新这个桶的计数
- 如果某个桶的计数超过阈值,触发全局磨损均衡
// 桶算法伪代码示例
#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;
}
}
三种算法对比:
| 算法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 桶算法 | 低频写入,小数据量 | 实现简单,开销小 | 热点问题,扩展性差 |
| 哈希映射 | 中高频写入,数据量大 | 分布均匀,灵活性高 | 计算开销大,有冲突风险 |
| 滑动窗口 | 高频写入,固定数据量 | 磨损最均匀,实现简单 | 地址空间利用率低 |
你可能会问,这三种算法到底选哪个?我的建议是——先看你的写入频率。如果一天写不了几次,桶算法就够了。如果每秒写好几次,滑动窗口更靠谱。哈希映射嘛,适合那些对灵活性要求高的场景。
我的个人习惯:在项目初期,我会先用桶算法快速验证功能。等系统稳定了,再根据实际写入频率换成更合适的算法。别一开始就上复杂的算法,容易把自己绕进去。
最后说一句,这三种算法不是互斥的。我在一个项目里就同时用了桶算法和滑动窗口——低频数据用桶,高频数据用窗口。嗯,灵活搭配才是王道。