4、校准参数存储与加载:参数存储格式(JSON/INI/二进制)、NVRAM与文件系统存储、启动时自动加载机制

校准参数存好了,下次开机还得重新校准?那肯定不行。我见过不少项目,校准算法写得漂亮,结果参数存丢了,用户一重启就回到解放前。说白了,存储这块要是没做好,前面所有校准工作都白费。

这一节咱们就聊聊:参数该用什么格式存?存到哪儿?怎么在启动时自动加载?

4.1 三种存储格式的选型对比

我个人习惯,先看项目资源再选格式。MCU资源紧张,就别硬上JSON;Linux平台资源充裕,用JSON反而方便调试。下面这张表是我多年总结的对比:

格式 可读性 解析开销 存储效率 适用场景
JSON 高 中(需解析器) 低(文本冗余) Linux/Android,调试阶段
INI 中 低(逐行解析) 中 轻量嵌入式,RTOS
二进制 无 极低(memcpy) 高 资源受限MCU,批量生产

我的建议:量产阶段一律用二进制格式。JSON和INI只适合开发调试阶段,方便你拿串口工具直接看内容。

4.2 二进制格式——我最常用的方案

先看一个实际例子。我在一个Cortex-M3项目里,校准参数结构体是这样定义的:

// 校准参数结构体
typedef struct {
    uint32_t magic;          // 魔数,用于校验数据有效性
    uint16_t version;        // 版本号,方便后续升级
    int16_t  offset_x;       // X轴偏移量
    int16_t  offset_y;       // Y轴偏移量
    float    scale_x;        // X轴缩放因子
    float    scale_y;        // Y轴缩放因子
    uint16_t checksum;       // 校验和
} CalibParams_t;

为什么要加魔数和版本号?我曾经踩过一个坑:产品升级后,旧参数结构体长度变了,加载时直接内存越界。从那以后,我每个参数块都带版本号,加载时先比对版本,不匹配就清空重新校准。

存储和加载的代码也很简洁:

// 存储到NVRAM
void Calib_Save(CalibParams_t *params) {
    params->magic    = 0xCA11B001;
    params->checksum = Calib_CalcChecksum(params);
    NVRAM_Write(CALIB_ADDR, (uint8_t*)params, sizeof(CalibParams_t));
}

// 从NVRAM加载
int Calib_Load(CalibParams_t *params) {
    NVRAM_Read(CALIB_ADDR, (uint8_t*)params, sizeof(CalibParams_t));
    
    // 校验魔数
    if (params->magic != 0xCA11B001) return -1;
    
    // 校验版本
    if (params->version != CALIB_VERSION) return -2;
    
    // 校验和检查
    uint16_t calc = Calib_CalcChecksum(params);
    if (calc != params->checksum) return -3;
    
    return 0; // 加载成功
}

小技巧:校验和算法别用太复杂的。我一般用简单的累加和或CRC-16,够用且速度快。别在启动阶段搞什么SHA256,那纯粹是给自己找麻烦。

4.3 JSON格式——调试阶段的利器

在Linux平台上,我习惯用JSON做调试。原因很简单:用cat命令就能看到参数内容,改起来也方便。

{
    "magic": "0xCA11B001",
    "version": 2,
    "offset": {
        "x": -12,
        "y": 8
    },
    "scale": {
        "x": 1.0245,
        "y": 0.9876
    },
    "timestamp": 1698123456
}

用cJSON库解析也就几行代码:

cJSON *root = cJSON_Parse(json_str);
params->offset_x = cJSON_GetObjectItem(root, "offset")->child->valueint;
params->scale_x  = cJSON_GetObjectItem(root, "scale")->child->valuedouble;
cJSON_Delete(root);

但注意,JSON解析器会占用不少RAM。我有个项目STM32F103只有64KB RAM,用cJSON解析一次堆栈就炸了。后来老老实实换回二进制。

4.4 存储介质的选择:NVRAM vs 文件系统

存储介质这块,我分两种情况处理:

  • MCU裸机/RTOS:直接操作NVRAM地址。EEPROM、FRAM、后备电池SRAM都行。优点是访问快、代码简单;缺点是不好管理多个参数块。
  • Linux/Android:走文件系统。存成 /etc/calib.conf 或 /data/misc/calib.json。优点是方便备份、权限管理;缺点是启动时文件系统可能还没挂载。

注意:文件系统挂载时机是个坑。我遇到过好几次:应用层启动太早,文件系统还没ready,读出来全是0。解决方案是在init脚本里加依赖,或者让校准服务延迟启动。

4.5 启动时自动加载机制

自动加载的逻辑其实不复杂,但顺序很重要。我一般按这个流程走:

  1. 硬件初始化完成——I2C/SPI总线就绪,NVRAM可访问
  2. 尝试加载参数——调用Calib_Load()
  3. 校验结果判断:
    • 加载成功 → 直接应用参数
    • 魔数错误 → 可能是首次开机,触发校准流程
    • 版本不匹配 → 清除旧参数,重新校准
    • 校验和错误 → 数据损坏,重新校准
  4. 应用参数——将偏移量和缩放因子写入触摸控制器寄存器

下面这张图是我项目里实际用的启动加载流程:

校准参数启动加载流程图 系统启动 从NVRAM加载参数 校验魔数 Magic 魔数正确? 是 否 校验版本号 版本匹配? 是 校验 Checksum 校验通过? 是 应用校准参数 触发重新校准 任何校验失败都走重新校准路径,确保参数可靠性

4.6 避坑指南——我踩过的几个坑

坑1:NVRAM写入次数限制

EEPROM一般只有10万次擦写寿命。我曾经有个项目每秒钟存一次参数,结果一个月后EEPROM就挂了。后来改成只在参数变化时才写入,并且加了个防抖延时。

坑2:断电瞬间写入不完整

参数写到一半断电了,数据就坏了。我的做法是:先写一份备份,校验通过后再覆盖主数据。或者用双缓冲区,保证总有一份完整数据可用。

坑3:文件系统缓存没刷

Linux下用fwrite写文件,数据还在内核缓存里,没真正落盘。一定要调fsync()或fdatasync(),否则断电就丢数据。我因为这个被测试部门提过bug,印象深刻。

4.7 小结

校准参数存储这件事,说白了就是三个选择:格式选二进制还是文本、介质选NVRAM还是文件系统、加载时机要卡准。我个人建议量产项目无脑上二进制+NVRAM,稳定可靠。调试阶段可以用JSON+文件系统,方便排查问题。

嗯,这一节就聊到这儿。记住一点:存储代码写完后,一定要做断电测试——反复断电100次,看看参数还能不能正确加载。这个测试能帮你发现绝大多数隐藏问题。