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 启动时自动加载机制
自动加载的逻辑其实不复杂,但顺序很重要。我一般按这个流程走:
- 硬件初始化完成——I2C/SPI总线就绪,NVRAM可访问
- 尝试加载参数——调用Calib_Load()
- 校验结果判断:
- 加载成功 → 直接应用参数
- 魔数错误 → 可能是首次开机,触发校准流程
- 版本不匹配 → 清除旧参数,重新校准
- 校验和错误 → 数据损坏,重新校准
- 应用参数——将偏移量和缩放因子写入触摸控制器寄存器
下面这张图是我项目里实际用的启动加载流程:
4.6 避坑指南——我踩过的几个坑
坑1:NVRAM写入次数限制
EEPROM一般只有10万次擦写寿命。我曾经有个项目每秒钟存一次参数,结果一个月后EEPROM就挂了。后来改成只在参数变化时才写入,并且加了个防抖延时。
坑2:断电瞬间写入不完整
参数写到一半断电了,数据就坏了。我的做法是:先写一份备份,校验通过后再覆盖主数据。或者用双缓冲区,保证总有一份完整数据可用。
坑3:文件系统缓存没刷
Linux下用fwrite写文件,数据还在内核缓存里,没真正落盘。一定要调fsync()或fdatasync(),否则断电就丢数据。我因为这个被测试部门提过bug,印象深刻。
4.7 小结
校准参数存储这件事,说白了就是三个选择:格式选二进制还是文本、介质选NVRAM还是文件系统、加载时机要卡准。我个人建议量产项目无脑上二进制+NVRAM,稳定可靠。调试阶段可以用JSON+文件系统,方便排查问题。
嗯,这一节就聊到这儿。记住一点:存储代码写完后,一定要做断电测试——反复断电100次,看看参数还能不能正确加载。这个测试能帮你发现绝大多数隐藏问题。