校验链路全景

固件升级的本质是”用一段新代码替换旧代码”。如果新代码在传输或写入过程中损坏,设备可能变砖。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;
}

两个检查项:

  1. 栈指针(MSP):必须在 SRAM 地址空间内(STM32F103ZE 的 SRAM 为 0x20000000~0x2000FFFF)
  2. 复位向量:必须指向 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
3Flash 写入逐字节回读每次半字编程无需额外 RAM
4整包CRC32全部写入后无需额外 RAM
5整包SHA-256全部写入后256 字节缓冲
6启动向量地址范围检查全部写入后无需额外 RAM
7全套复核CRC32+SHA256+向量每次上电启动256 字节缓冲

这 7 层校验从传输到存储、从写入到启动,层层递进,确保设备在任何阶段都不会运行损坏的固件。