4. 校准数据存储:EEPROM/Flash读写操作、校准参数结构体设计、校验和机制
数据存不住,校准就白做。这是我在好几个项目里摔过的跟头。
触摸屏校准参数,说白了就是几个坐标值。但你别小看这几个数——它们要是丢了、错了、被篡改了,用户一开机就得重新点十字,体验直接归零。所以这一章,我们重点解决三个问题:存什么、怎么存、怎么保证存对了。
4.1 校准参数结构体设计
先想清楚我们要存哪些东西。我个人习惯,把校准参数打包成一个结构体,方便读写,也方便扩展。
typedef struct {
uint16_t magic; // 魔数,用于识别数据有效性
uint8_t version; // 结构体版本号
uint8_t reserved; // 保留字节,对齐用
int32_t offset_x; // X轴偏移量
int32_t offset_y; // Y轴偏移量
float scale_x; // X轴缩放系数
float scale_y; // Y轴缩放系数
uint16_t touch_width; // 触摸屏物理宽度
uint16_t touch_height; // 触摸屏物理高度
uint16_t checksum; // 校验和
} CalibrationData_t;
这里我解释几个关键字段:
- magic:我习惯用
0xCA11("CAL" 的谐音)。读数据时先检查魔数,不对就直接用默认值。 - version:结构体版本号。万一以后要加字段,版本号能帮你做兼容处理。
- checksum:校验和,放在最后。计算时把整个结构体(除了 checksum 自身)累加,结果存到这里。
小技巧:结构体最好按 4 字节对齐。有些 MCU 对非对齐访问会触发 HardFault,或者性能骤降。加一个 reserved 字段就是为了凑对齐。
4.2 EEPROM/Flash 读写操作
存储介质不同,读写方式也不同。我列个对比表,你一看就明白:
| 特性 | EEPROM | 内部 Flash | 外部 SPI Flash |
|---|---|---|---|
| 擦写次数 | 10万~100万次 | 1万~10万次 | 10万次左右 |
| 写入方式 | 按字节写 | 必须先擦除扇区 | 必须先擦除扇区 |
| 擦除粒度 | 无(直接写) | 扇区(通常 1KB~128KB) | 扇区(通常 4KB~64KB) |
| 读取速度 | 慢(I2C/SPI 总线限制) | 快(CPU 直接寻址) | 中等(SPI 时钟限制) |
| 典型场景 | 小数据、频繁更新 | 固件参数、不常改 | 大容量、文件系统 |
我建议:如果 MCU 内部有 EEPROM,优先用它。校准数据一般就几十个字节,EEPROM 按字节写,不用擦除,省心很多。
下面给一个 EEPROM 读写示例(以 STM32 内部 EEPROM 为例):
#define CALIBRATION_ADDR 0x08080000 // EEPROM 起始地址(视具体 MCU 而定)
void Calibration_Save(CalibrationData_t *data) {
uint16_t *p = (uint16_t *)data;
uint16_t sum = 0;
uint8_t i;
// 计算校验和(跳过最后一个 checksum 字段)
for (i = 0; i < sizeof(CalibrationData_t) / 2 - 1; i++) {
sum += p[i];
}
data->checksum = sum;
// 写入 EEPROM
HAL_FLASHEx_DATAEEPROM_Unlock();
for (i = 0; i < sizeof(CalibrationData_t); i += 2) {
HAL_FLASHEx_DATAEEPROM_Program(FLASH_TYPEPROGRAMDATA_HALFWORD,
CALIBRATION_ADDR + i,
*(uint16_t *)((uint8_t *)data + i));
}
HAL_FLASHEx_DATAEEPROM_Lock();
}
uint8_t Calibration_Load(CalibrationData_t *data) {
uint16_t *p = (uint16_t *)data;
uint16_t sum = 0;
uint8_t i;
// 从 EEPROM 读取
for (i = 0; i < sizeof(CalibrationData_t); i += 2) {
*(uint16_t *)((uint8_t *)data + i) = *(uint16_t *)(CALIBRATION_ADDR + i);
}
// 验证魔数
if (data->magic != 0xCA11) {
return 0; // 数据无效
}
// 验证校验和
for (i = 0; i < sizeof(CalibrationData_t) / 2 - 1; i++) {
sum += p[i];
}
if (sum != data->checksum) {
return 0; // 校验失败
}
return 1; // 数据有效
}
注意:写 EEPROM 或 Flash 时,一定要关中断!否则写入过程中被中断打断,轻则数据写错,重则 MCU 死锁。我遇到过一例,就是因为中断里也调用了 EEPROM 写入,两个操作冲突,导致整个系统卡死。
4.3 校验和机制
为什么要加校验和?
你想想看,EEPROM 和 Flash 虽然是非易失的,但数据在存储过程中可能因为掉电、电磁干扰、老化等原因发生位翻转。尤其是工业现场,电机启停产生的干扰,足以让一个 bit 从 1 变成 0。
校验和的作用就是:读出来之后,算一遍,跟存进去的值对比,不一样就说明数据坏了。
常用的校验方式有几种:
| 校验方式 | 计算量 | 检错能力 | 适用场景 |
|---|---|---|---|
| 累加和(Sum) | 极低 | 弱(无法检测字节顺序错误) | 简单参数、数据量小 |
| 异或和(XOR) | 极低 | 中等 | 小数据、实时性要求高 |
| CRC-16 | 低 | 强(可检测 2 位错误) | 推荐用于校准数据 |
| CRC-32 | 中等 | 极强 | 大容量数据、固件校验 |
我个人推荐用 CRC-16。校准数据就几十个字节,CRC-16 的计算量可以忽略不计,但检错能力比累加和强得多。如果你 MCU 有硬件 CRC 外设(比如 STM32 的 CRC 模块),那就更好了——几微秒就算完。
下面是一个简单的 CRC-16 实现(多项式 0x8005):
uint16_t Cal_CRC16(uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
uint16_t i, j;
for (i = 0; i < len; i++) {
crc ^= data[i];
for (j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0x8005;
} else {
crc >>= 1;
}
}
}
return crc;
}
核心原则:写入时计算校验和并存储,读取时重新计算并比对。不一致则使用默认校准参数,并提示用户重新校准。
4.4 数据存储流程图
我把整个流程画成一张图,方便你理解:
4.5 避坑指南
最后,分享几个我踩过的坑:
- 掉电保护:写入过程中掉电,数据可能只写了一半。我建议先写到一个临时区域,写入成功后再更新标志位。这样即使掉电,下次启动也能发现数据无效,回退到默认值。
- 不要频繁写入:EEPROM 虽然有 10 万次寿命,但如果你每秒钟都写一次,几天就报废了。我一般只在校准完成后写一次,或者加上写入次数限制。
- 备份机制:如果存储空间允许,存两份校准数据。主数据校验失败时,尝试读取备份。我曾经在一个产品里用这个方案,把返修率从 3% 降到了 0.1%。
- 初始化默认值:第一次上电时,EEPROM 里全是 0xFF 或 0x00。魔数检查会失败,这时候要有一套默认校准参数兜底,至少让屏幕能点。
嗯,数据存储这部分,说白了就是「存得进去、读得出来、坏了能发现」。把结构体设计好,校验和加上,读写流程走通,后面就稳了。