4. 数据存储规划:按摩仪参数分类与地址映射表设计

好,咱们接着聊。上一节我们把 EEPROM 的硬件接口和驱动搞定了,但说实话,那只是万里长征第一步。你想想看,驱动写好了,数据往哪儿存?怎么存?存错了怎么办?

我刚开始做嵌入式那会儿,就吃过这个亏。有一次项目赶得急,我直接把参数按顺序往 EEPROM 里一塞,觉得“反正能读写就行”。结果产品上市后,客户反馈说“按摩仪用着用着,力度突然变成最大了”。查了半天,原来是地址重叠了,数据被覆盖。嗯,从那以后,我再也不敢轻视数据存储规划了。

4.1 按摩仪参数分类:先分清楚再动手

说白了,数据规划的第一步就是分类。你得知道你的按摩仪里到底要存哪些东西。我个人习惯,先把参数分成四大类:

  • 用户模式:比如“轻柔模式”、“强力模式”、“助眠模式”等。每个模式对应一套预设参数。
  • 力度参数:用户自定义的力度值,通常是 1-10 档。这个值用户会频繁调整。
  • 时长参数:单次按摩的持续时间,比如 15 分钟、30 分钟。有些用户喜欢短时间,有些喜欢长一点。
  • 历史记录:最近几次的使用记录,比如“2025-03-15 使用了强力模式 20 分钟”。这个功能虽然不起眼,但用户反馈很好。

为什么要分类?因为不同类型的参数,写入频率重要性完全不同。力度参数用户天天调,写入次数多;用户模式可能一个月才改一次。如果你把高频写入的数据和低频写入的数据混在一起,很容易把 EEPROM 的寿命耗尽。EEPROM 的擦写次数一般是 10 万次左右,看着挺多,但如果你每秒写一次,几天就挂了。

我的经验: 高频写入的参数(比如力度、时长)尽量放在 EEPROM 的前半段地址,并且考虑磨损均衡。低频参数(比如用户模式、历史记录)放在后半段。这样能最大化 EEPROM 的寿命。

4.2 地址映射表设计:一张表管好所有数据

分类完了,接下来就是画地址映射表。这张表就是你的“数据地图”,每个参数在 EEPROM 里住哪个房间,门牌号是多少,都得清清楚楚。

我建议用表格来规划,一目了然。下面是我给按摩仪设计的一个典型地址映射表:

参数名称 起始地址 占用字节 数据类型 默认值 说明
魔数(Magic Number) 0x0000 2 uint16_t 0xA5A5 用于判断 EEPROM 是否已初始化
版本号 0x0002 1 uint8_t 0x01 数据结构版本,方便后续升级
当前用户模式 0x0003 1 uint8_t 0x00 0=轻柔, 1=强力, 2=助眠
力度等级 0x0004 1 uint8_t 5 1-10 档,用户频繁修改
按摩时长 0x0005 1 uint8_t 15 单位:分钟,范围 5-60
历史记录索引 0x0006 1 uint8_t 0 循环写入的当前索引
历史记录区 0x0010 240 结构体数组 全0 存储最近 10 条记录,每条 24 字节
预留区 0x0100 256 0xFF 为未来功能预留

看到这个表,你可能会问:“为什么要在最前面放一个魔数?”

这个问题问得好。魔数的作用,说白了就是判断 EEPROM 是不是“处女地”。新出厂的芯片,EEPROM 里全是 0xFF。如果程序第一次运行时读到魔数不是 0xA5A5,就说明 EEPROM 还没初始化过,需要写入默认值。否则,直接读取已有数据。这个技巧我在多个项目里都用过,非常实用。

注意: 地址映射表一旦确定,尽量不要随意改动。尤其是产品已经量产之后,改地址意味着旧设备的数据全部作废。我曾经在一个项目中,因为临时加了一个参数,把历史记录区的地址往后挪了 10 个字节,结果老设备升级后,用户的历史记录全乱了。那叫一个惨。

4.3 历史记录区的设计:循环缓冲区是个好办法

历史记录这块,我单独拿出来说说。因为它的存储方式和普通参数不太一样。

普通参数是“原地更新”,比如用户把力度从 5 改成 6,直接往地址 0x0004 写 6 就行。但历史记录是“追加写入”,每次使用完都要新增一条。如果每次都往同一个地址写,那只能存一条记录,显然不行。

我的做法是:用循环缓冲区。在地址 0x0010 到 0x00FF 之间,划分出 240 个字节,每条记录固定 24 字节,总共可以存 10 条。再配合一个索引变量(地址 0x0006),记录当前写到哪一条了。

伪代码大概是这样:

// 定义历史记录结构体
typedef struct {
    uint8_t  year;      // 年份偏移,比如 25 表示 2025 年
    uint8_t  month;
    uint8_t  day;
    uint8_t  hour;
    uint8_t  minute;
    uint8_t  mode;      // 使用的模式
    uint8_t  intensity; // 使用的力度
    uint8_t  duration;  // 使用的时长
    uint16_t crc;       // 校验和,防止数据错误
} HistoryRecord_t;

// 写入历史记录
void History_Write(HistoryRecord_t *record) {
    uint8_t index;
    uint16_t addr;

    // 读取当前索引
    EEPROM_Read(0x0006, &index, 1);

    // 计算写入地址:0x0010 + index * 24
    addr = 0x0010 + (index * sizeof(HistoryRecord_t));

    // 写入记录
    EEPROM_Write(addr, (uint8_t *)record, sizeof(HistoryRecord_t));

    // 更新索引(循环)
    index = (index + 1) % 10;
    EEPROM_Write(0x0006, &index, 1);
}

你看,这样每次写入都会覆盖最旧的那条记录,永远保留最新的 10 条。而且因为索引是单独存的,即使写入过程中断电,最多丢一条记录,不会把整个历史区搞乱。

核心要点: 数据存储规划的核心,就是“分类 + 映射 + 容错”。分类解决写入频率问题,映射解决地址管理问题,容错解决数据可靠性问题。这三件事做好了,你的 EEPROM 方案就稳了。

4.4 避坑指南:我踩过的几个坑

最后,分享几个我实际项目中踩过的坑,希望能帮你省点时间:

  • 坑一:地址对齐问题。 有些 MCU 的 EEPROM 要求 2 字节或 4 字节对齐,否则读写会出错。我建议所有结构体都按 4 字节对齐,省心。
  • 坑二:默认值写错。 有一次我把力度默认值写成了 0,结果用户开机发现没力度,以为机器坏了。后来改成 5,问题解决。
  • 坑三:忘记预留空间。 产品上市后,市场部突然说要加一个“加热功能”,需要存温度参数。结果地址全占满了,只能硬着头皮改地址映射表,导致老设备不兼容。

嗯,关于数据存储规划,今天就聊到这儿。下一节我们讲讲如何实现参数的读写接口,以及怎么保证数据的一致性。到时候见。