4、固件打包:固件镜像格式、版本号管理、CRC/哈希校验、签名机制

好,咱们接着聊固件打包这件事。

说实话,很多做美容仪开发的团队,往往把精力都花在硬件设计和APP交互上。固件打包?随便搞个bin文件就完事了。嗯,我以前也这么干过,直到有一次OTA升级把一批设备刷成了砖头……那场面,至今难忘。

固件打包不是简单的「把代码编译出来,扔给用户下载」。它背后涉及四个关键环节:镜像格式、版本号、校验、签名。一个都不能少。

4.1 固件镜像格式:别只扔一个裸bin

裸的bin文件,说白了就是纯二进制数据。它没有头信息,没有结构,MCU拿到后只能从固定地址开始执行。这在量产和OTA场景下,其实挺危险的。

我个人习惯,会在固件前面加一个「镜像头」。这个头里包含关键元数据。比如这样:

typedef struct {
    uint32_t magic;          // 魔数,比如 0xBE4F5A01
    uint32_t image_size;     // 固件总长度(不含头)
    uint32_t hw_version;     // 硬件版本,防止刷错平台
    uint32_t fw_version;     // 固件版本号
    uint32_t crc32;          // 整个固件的CRC校验值
    uint32_t reserved[4];    // 保留字段,以后扩展用
    uint8_t  signature[64];  // 签名数据(后面讲)
} firmware_header_t;

有了这个头,MCU在烧录前就能做很多检查。比如魔数不对?直接拒绝。硬件版本不匹配?也拒绝。我在项目中遇到过,有客户把A型号的固件刷到了B型号上,结果屏幕不亮。加了硬件版本检查后,这种问题再也没出现过。

小技巧:魔数建议用有意义的ASCII字符,比如"FWUP"。调试时用十六进制查看器一眼就能认出来。

4.2 版本号管理:语义化版本是底线

版本号怎么管?很多团队用「V1.0」「V2.1」这种,但OTA升级时问题就来了——你怎么判断V2.1能不能直接升到V3.0?中间有没有必须经过的版本?

我建议用语义化版本号:主版本.次版本.修订号

字段 含义 OTA升级策略
主版本 不兼容的API或协议变更 必须全量升级,不能差分
次版本 向下兼容的功能新增 可以差分升级,但建议全量
修订号 向下兼容的问题修复 差分升级最安全

举个例子。你现在的固件是2.3.1,服务器上有2.3.2和3.0.0。那OTA逻辑应该是:先升到2.3.2,再升到3.0.0。不能跳过中间版本。为什么?因为2.3.1到3.0.0之间可能改了Flash分区表,直接跳过去数据就全乱了。

注意:版本号要存储在Flash的固定位置,最好和镜像头放在一起。不要只写在代码里,否则OTA升级后你都不知道当前跑的是哪个版本。

4.3 CRC/哈希校验:保证数据没被篡改

固件在传输过程中,可能因为网络丢包、存储介质错误等原因,出现比特翻转。这时候如果没有校验,MCU刷了一个坏固件,后果就是死机、重启、甚至变砖。

常用的校验方式有两种:

  • CRC32:速度快,适合嵌入式实时校验。但安全性一般,容易被碰撞。
  • SHA256:哈希算法,安全性高。但计算量大,低端MCU可能吃不消。

我个人习惯是:OTA下载时用CRC32做快速校验,下载完成后在升级前再用SHA256做一次完整校验。这样既保证了速度,又保证了安全。

代码实现其实不复杂。比如CRC32校验:

uint32_t calculate_crc32(const uint8_t *data, uint32_t len) {
    uint32_t crc = 0xFFFFFFFF;
    for (uint32_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (int j = 0; j < 8; j++) {
            if (crc & 1) {
                crc = (crc >> 1) ^ 0xEDB88320;
            } else {
                crc >>= 1;
            }
        }
    }
    return crc ^ 0xFFFFFFFF;
}

嗯,这段代码我用了好多年,从来没出过问题。你直接拿去用就行。

核心原则:校验必须在MCU端做,不能依赖APP或服务器。我曾经见过一个方案,CRC在APP端算好了传给MCU,MCU直接比对——这跟没校验一样,因为攻击者可以同时篡改固件和CRC值。

4.4 签名机制:防止固件被恶意替换

校验只能保证数据没损坏,但不能保证数据是合法的。如果有人伪造了一个固件,CRC也是对的,那MCU照样会刷进去。这就是签名要解决的问题。

签名机制的核心是非对称加密。简单说:

  • 你在服务器上用私钥对固件哈希值加密,生成签名。
  • MCU里预置公钥,用公钥解密签名,得到哈希值。
  • 再对比本地计算的哈希值,一致就说明固件是合法的。

流程大概是这样的:

// 服务器端(用私钥签名)
hash = SHA256(firmware_data);
signature = RSA_Encrypt(hash, private_key);

// MCU端(用公钥验签)
hash_from_sig = RSA_Decrypt(signature, public_key);
hash_local = SHA256(firmware_data);
if (hash_from_sig == hash_local) {
    // 固件合法,允许升级
} else {
    // 固件被篡改,拒绝升级
}

这里有个坑,我曾经踩过——公钥怎么存?如果公钥也放在固件里,那攻击者替换了固件,公钥也跟着换了,签名就形同虚设。正确的做法是:公钥写在Bootloader里,而且Bootloader要写保护,不允许OTA更新。

重要提醒:签名算法不要用MD5,已经被破解了。建议用RSA-2048或ECDSA。低端MCU如果算力不够,可以考虑硬件加密引擎,或者用Ed25519这种轻量级算法。

4.5 打包流程总结

好了,把上面这些串起来,一个完整的固件打包流程应该是这样的:

  1. 编译生成固件二进制文件(.bin或.hex)
  2. 计算固件的SHA256哈希值
  3. 用私钥对哈希值签名,生成签名数据
  4. 组装镜像头(魔数、版本、硬件版本、CRC、签名等)
  5. 将镜像头 + 固件数据 + 签名打包成一个文件
  6. 上传到OTA服务器,等待设备下载

你想想看,如果少了其中任何一步,OTA升级都可能出大问题。我见过最惨的一次,就是因为没加签名,有人写了个脚本批量给美容仪刷恶意固件,导致几十台设备无法开机。最后只能返厂维修,成本高得吓人。

所以,别嫌麻烦。固件打包这件事,做得越严谨,后面省的事就越多。