2. 固件升级原理:典型流程、包结构与校验机制
做 BootROM 固件升级,你得先搞清楚一件事:升级到底在干什么?说白了,就是把一段新程序写进 Flash,然后让芯片能正确跳过去执行。听起来简单,但这里面的门道,我踩过的坑可不少。
先给你画个全景图,看看固件升级的核心逻辑长什么样。
2.1 典型升级流程
我习惯把升级流程拆成六个阶段。每个阶段都可能翻车,咱们一个一个看。
- 升级包准备:主机(PC/手机/云端)把固件打包,加上头部、校验值、签名。这一步通常在编译服务器上完成。
- 传输与接收:通过 UART、SPI、I2C、USB 或网络把包发给设备。BootROM 把数据暂存到 RAM 或临时分区。
- 完整性校验:收到包之后,先算 CRC 或 SHA,跟包里的校验值比对。不对?直接拒绝,不往下走。
- Flash 擦写:校验通过,开始擦除目标分区,然后逐块写入新固件。这一步最怕掉电。
- 写入后验证:写完了再读出来算一遍校验值,确认写入没出错。我见过不少“写成功但读出来是乱的”案例。
- 跳转执行:验证通过,设置向量表、栈指针,然后跳转到新固件入口。跳过去起不来?那就回滚。
我的经验: 第 4 步和第 5 步之间,我建议加一个“备份旧固件”的动作。哪怕只备份前 4KB 的向量表,关键时刻能救命。我曾经在一个 IoT 项目里就是因为没备份,升级到一半断电,板子直接变砖。
2.2 升级包的组成结构
一个标准的固件升级包,不是把二进制文件直接扔过去就完事。它得有头有尾,结构清晰。我常用的包结构长这样:
| 偏移 | 字段 | 大小 | 说明 |
|---|---|---|---|
| 0x00 | Magic Number | 4 字节 | 固定标识,比如 0xDEADBEEF,用于快速识别 |
| 0x04 | 固件版本 | 4 字节 | 主版本+次版本+修订号,用于版本比对 |
| 0x08 | 固件长度 | 4 字节 | 有效载荷的字节数 |
| 0x0C | 校验算法 | 1 字节 | 0x01=CRC32, 0x02=SHA256 |
| 0x0D | 校验值 | 32 字节 | CRC32 或 SHA256 的结果 |
| 0x2D | 目标分区 | 4 字节 | 要写入的 Flash 起始地址 |
| 0x31 | 签名 | 64/128 字节 | RSA/ECDSA 签名(可选) |
| 0x31+N | 有效载荷 | 变长 | 实际的固件二进制数据 |
你想想看,为什么要有 Magic Number?我遇到过一例,传输过程中线松了,收到一堆 0xFF。没有 Magic Number 做第一道过滤,BootROM 会把垃圾当成固件去解析,后果不堪设想。
2.3 校验机制:CRC vs SHA
校验是升级的命门。我见过太多人在这上面栽跟头。常用的校验手段就两种:CRC 和 SHA。
CRC(循环冗余校验)
CRC 算得快,硬件支持好。很多 MCU 内置 CRC 计算单元,几微秒就算完 1KB 数据。但它有个致命弱点:抗碰撞能力弱。恶意攻击者可以构造一个不同但 CRC 相同的数据块。所以 CRC 只适合检测传输错误,不适合做安全校验。
代码示例:CRC32 计算(C语言)
uint32_t 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;
}
SHA(安全哈希算法)
SHA256 算出来 32 字节,碰撞概率极低。更重要的是,它跟签名算法配合,能验证固件的来源是否可信。代价就是慢——软件算 1MB 数据可能要几百毫秒到几秒。
注意: 在 BootROM 里用 SHA256,你得考虑 RAM 和 ROM 的开销。SHA256 需要 64 字节的工作缓冲区,加上 32 字节的摘要。有些低端 MCU 的 BootROM 总共才 4KB,塞不下完整的 SHA256 实现。这时候我建议用硬件加速器,或者退而求其次用 SHA1(虽然不推荐)。
怎么选?
我个人的经验法则:
- 只防传输错误(比如 UART 干扰):CRC32 足够,快且省资源。
- 需要防篡改(比如 OTA 升级):必须用 SHA256 + 签名验证。
- BootROM 空间紧张:优先用硬件 CRC,配合外部签名服务器做二次校验。
避坑指南: 我曾经在一个项目里只用了 CRC16 做校验,结果升级包在传输过程中被篡改了一个字节,CRC 竟然没发现。后来查出来是 CRC 多项式选得太弱。从那以后,我只要涉及安全升级,一律上 SHA256。
2.4 校验流程的典型实现
给你看一段我在实际项目中用过的伪代码逻辑。它把校验和升级流程串起来了。
// 升级主流程
int firmware_upgrade(uint8_t *pkg_buf, uint32_t pkg_len) {
// 1. 解析头部
pkg_header_t *hdr = (pkg_header_t *)pkg_buf;
if (hdr->magic != MAGIC_NUMBER) {
return ERR_INVALID_PKG; // 不是合法包
}
// 2. 版本检查
if (hdr->version <= current_version) {
return ERR_VERSION_SKIP; // 版本不新,跳过
}
// 3. 校验完整性
uint8_t calc_hash[32];
sha256_calc(pkg_buf + hdr->payload_offset,
hdr->payload_len, calc_hash);
if (memcmp(calc_hash, hdr->hash, 32) != 0) {
return ERR_HASH_MISMATCH; // 校验失败
}
// 4. 擦写 Flash
flash_erase(hdr->target_addr, hdr->payload_len);
flash_write(hdr->target_addr,
pkg_buf + hdr->payload_offset,
hdr->payload_len);
// 5. 写入后验证
uint8_t verify_hash[32];
sha256_calc((uint8_t *)hdr->target_addr,
hdr->payload_len, verify_hash);
if (memcmp(verify_hash, hdr->hash, 32) != 0) {
// 回滚:恢复旧固件
restore_old_firmware();
return ERR_VERIFY_FAILED;
}
// 6. 跳转
jump_to_firmware(hdr->target_addr);
return SUCCESS;
}
重要提醒: 第 5 步的写入后验证,很多人偷懒不做。我告诉你,千万别省。Flash 在写入过程中可能因为电压波动、温度变化导致位翻转。我亲自修过一个案子,就是写 Flash 时电源纹波太大,写进去的数据跟预期不一样,但擦写 API 返回了成功。没有写入后验证,那批板子全得返厂。
2.5 升级包的版本管理
版本号不是随便写的。我建议用 32 位整数,高 16 位主版本,低 16 位次版本。BootROM 里做版本比对时,直接比较整数大小就行,省事。
还有一个细节:降级保护。有些场景不允许固件回退到旧版本(比如安全补丁修复后)。BootROM 里要加一个降级标志位,只有特定条件下才允许降级。
版本号编码示例:
#define MAKE_VERSION(major, minor) ((major << 16) | minor)
// 版本 v2.3 = 0x00020003
// 版本 v3.1 = 0x00030001