校验链路全景
固件升级的本质是”用一段新代码替换旧代码”。如果新代码在传输或写入过程中损坏,设备可能变砖。Bootloader 的校验体系就是为此设计的——从数据包到达的那一刻起,直到设备启动后,每一层都在验证”写入的内容是否正确”。
整个校验链路分为 7 层:
YMODEM 单包 CRC16 → 状态页 CRC16 → Flash 回读校验
→ 整包 CRC32 → 整包 SHA-256 → 向量表校验
→ DONE 状态上电复核
前 3 层在传输和写入过程中实时校验,中间 3 层在写入完成后集中校验,最后 1 层在每次启动时复核。
第一层:YMODEM 单包 CRC16-CCITT
校验发生在传输过程中,每收到一个数据包立即执行。
// YMODEM 协议层:每包 128 或 1024 字节 + 2 字节 CRC16
// 多项式:CRC16-CCITT/XMODEM (0x1021)
如果 CRC16 不匹配,Bootloader 发送 NAK,PC 端重传当前包。单包最多重试 5 次,超过后会话失败,状态进入 FAILED,错误码 BOOT_ERR_PACKET_CRC。
防护范围:传输过程中的比特翻转。
第二层:升级状态页 CRC16
每次 Flash 写入完成后,状态页都会被更新并重新计算 CRC16。
// 多项式:Modbus CRC16 (0xA001)
// 保护整个 UpgradeStateImage 结构体
state->crc16 = ModbusCrc16(state, offsetof(UpgradeStateImage, crc16));
如果在写入状态页的过程中掉电,重启后 Bootloader 检测到 CRC16 不匹配,标记为 BOOT_ERR_STATE_CRC,清除状态页并回退。
防护范围:状态页本身的完整性,防止状态机在掉电后进入不可预期的分支。
第三层:Flash 逐字节回读校验
每写入一个 Flash 半字(16-bit),立即回读验证。
// BootFlash_WriteImageData() 内部
// 写入后:if (*(volatile uint16_t*)addr != data) → BOOT_ERR_FLASH_PROGRAM
这层校验是 STM32 HAL 库 HAL_FLASH_Program() 的补充——HAL 只保证编程操作完成,不保证写入值和预期值一致。回读校验直接捕获 Flash 写入错误。
防护范围:Flash 硬件编程错误。
第四层:整包 CRC32
写入完成后,在 Flash 上直接流式计算整包 CRC32,与 YMODEM 头包中的期望值比对。
static uint16_t BootVerify_ValidateDigestsAndVector(...)
{
uint32_t calc_crc32 = BootVerify_ComputeCrc32(slot_addr, image_size);
if (calc_crc32 != image_crc32)
return BOOT_ERR_VERIFY_CRC32;
// ...
}
CRC32 计算直接在 Flash 上流式读取,不需要将整个镜像加载到 RAM。多项式使用标准 CRC32(0xEDB88320)。
防护范围:写入了正确数量的字节,但内容可能因 Flash 编程错误而改变。
第五层:整包 SHA-256
在 CRC32 通过后,继续计算 SHA-256 摘要。SHA-256 提供比 CRC32 更强的碰撞抵抗。
// 纯软件 SHA-256 实现,按 256 字节分块读取 Flash
// 如果头包中的 SHA-256 为全零(旧版本兼容),则跳过此校验
if (BootVerify_HasDigest(expected_digest) != 0u) {
uint8_t digest[32];
BootVerify_ComputeSha256(slot_addr, image_size, digest);
if (memcmp(digest, expected_digest, 32) != 0)
return BOOT_ERR_VERIFY_SHA256;
}
SHA-256 同样在 Flash 上直接计算,不占用额外 RAM。支持向后兼容:如果头包中的 SHA-256 为全零,跳过此校验。
防护范围:高级别的数据完整性校验,防止 CRC32 碰撞导致的漏检。
第六层:向量表合法性校验
这是最靠近”能否运行”的一层校验——检查固件的启动向量是否合法。
uint8_t Upgrade_IsAppVectorValid(uint32_t app_base_addr)
{
uint32_t app_stack = *(__IO uint32_t *)app_base_addr; // MSP 初始值
uint32_t app_reset = *(__IO uint32_t *)(app_base_addr + 4); // Reset Handler
// 栈指针必须在 SRAM 范围内(0x20000000 ~ 0x2000FFFF)
if ((app_stack & 0x2FFE0000u) != 0x20000000u)
return 0u;
// 复位向量必须指向 App 区域内
if (app_reset < app_base_addr ||
app_reset >= (app_base_addr + UPGRADE_APP_MAX_SIZE))
return 0u;
return 1u;
}
两个检查项:
- 栈指针(MSP):必须在 SRAM 地址空间内(STM32F103ZE 的 SRAM 为 0x20000000~0x2000FFFF)
- 复位向量:必须指向 App 区域内部,不能是 Bootloader 地址或无效地址
防护范围:防止”镜像写入到了错误的槽位”——例如 App_A.bin 被写入了 B 槽,复位向量会指向 A 槽地址,超出 B 槽范围,校验失败。
第七层:DONE 状态上电复核
前六层校验通过后,状态页标记为 DONE,设备复位跳转 App。但 Bootloader 的职责还没结束——每次上电启动,只要状态页是 DONE,Bootloader 会再次执行 CRC32 + SHA-256 + 向量表三重校验。
// boot_main.c 启动路径
if (state->state == UPGRADE_STATE_DONE) {
// 重新校验整包 CRC32 + SHA-256 + 向量表
uint16_t err = BootVerify_ValidateDigestsAndVector(...);
if (err != BOOT_ERR_NONE) {
// 校验失败,标记 FAILED,不回退到旧 App
state->state = UPGRADE_STATE_FAILED;
state->error_code = err;
}
}
这层设计的价值在于:Flash 数据可能在设备运行期间发生静默损坏(数据保留问题、高温环境等)。每次上电复核确保设备永远不会运行一个已损坏的固件。
校验失败后的行为
| 校验层 | 失败行为 |
|---|---|
| 单包 CRC16 | 重传当前包(最多 5 次),超限后会话失败 |
| 状态页 CRC16 | 清除状态页,回退到稳定槽位 |
| Flash 回读 | 立即停止编程,标记 FAILED |
| 整包 CRC32 | 标记 FAILED,设备停留 Bootloader |
| 整包 SHA-256 | 标记 FAILED,设备停留 Bootloader |
| 向量表 | 标记 FAILED,设备停留 Bootloader |
| DONE 上电复核 | 标记 FAILED,设备停留 Bootloader,不跳转 App |
校验算法实现
CRC32 流式计算
直接在 Flash 上读取,不需要 RAM 缓冲:
static uint32_t BootVerify_ComputeCrc32(uint32_t addr, uint32_t size)
{
uint32_t crc = 0xFFFFFFFFu;
for (uint32_t i = 0; i < size; i++) {
uint8_t byte = *(volatile uint8_t *)(addr + i);
crc = crc32_table[(crc ^ byte) & 0xFF] ^ (crc >> 8);
}
return crc ^ 0xFFFFFFFFu;
}
SHA-256 分块计算
按 256 字节分块读取 Flash,与流式 CRC32 同样的内存友好设计:
static void BootVerify_ComputeSha256(uint32_t addr, uint32_t size,
uint8_t digest[32])
{
Sha256Context ctx;
Sha256_Init(&ctx);
uint8_t block[256];
for (uint32_t offset = 0; offset < size; offset += 256) {
uint32_t chunk = (size - offset < 256) ? (size - offset) : 256;
memcpy(block, (void *)(addr + offset), chunk);
Sha256_Update(&ctx, block, chunk);
}
Sha256_Final(&ctx, digest);
}
设计总结
| 层级 | 校验内容 | 算法 | 触发时机 | 内存占用 |
|---|---|---|---|---|
| 1 | 传输包 | CRC16-CCITT | 每包到达 | 无需额外 RAM |
| 2 | 状态页 | CRC16-Modbus | 状态页写入后 | 无需额外 RAM |
| 3 | Flash 写入 | 逐字节回读 | 每次半字编程 | 无需额外 RAM |
| 4 | 整包 | CRC32 | 全部写入后 | 无需额外 RAM |
| 5 | 整包 | SHA-256 | 全部写入后 | 256 字节缓冲 |
| 6 | 启动向量 | 地址范围检查 | 全部写入后 | 无需额外 RAM |
| 7 | 全套复核 | CRC32+SHA256+向量 | 每次上电启动 | 256 字节缓冲 |
这 7 层校验从传输到存储、从写入到启动,层层递进,确保设备在任何阶段都不会运行损坏的固件。