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 数据存储流程图

我把整个流程画成一张图,方便你理解:

校准数据存储与校验流程图 开始校准流程 采集触摸点坐标 计算校准参数 填充结构体 + 计算校验和 写入 EEPROM / Flash 读取 → 校验魔数 + 校验和 校验通过?→ 使用 / 重新校准 魔数 + 版本号 + 校验和 三者共同保证数据完整性

4.5 避坑指南

最后,分享几个我踩过的坑:

  • 掉电保护:写入过程中掉电,数据可能只写了一半。我建议先写到一个临时区域,写入成功后再更新标志位。这样即使掉电,下次启动也能发现数据无效,回退到默认值。
  • 不要频繁写入:EEPROM 虽然有 10 万次寿命,但如果你每秒钟都写一次,几天就报废了。我一般只在校准完成后写一次,或者加上写入次数限制。
  • 备份机制:如果存储空间允许,存两份校准数据。主数据校验失败时,尝试读取备份。我曾经在一个产品里用这个方案,把返修率从 3% 降到了 0.1%。
  • 初始化默认值:第一次上电时,EEPROM 里全是 0xFF 或 0x00。魔数检查会失败,这时候要有一套默认校准参数兜底,至少让屏幕能点。

嗯,数据存储这部分,说白了就是「存得进去、读得出来、坏了能发现」。把结构体设计好,校验和加上,读写流程走通,后面就稳了。