
一、引言如果你的设备已经在千里之外的客户现场发现了一个致命的 bug你会怎么做派人飞过去用 J-Link 烧录还是——通过网络远程升级固件这就是 OTAOver-The-Air固件升级的价值。而 OTA 的核心就是Bootloader。本文从零开始带你设计一个工业级的 Bootloader 系统Bootloader 的三种架构设计Flash 分区策略与链接脚本固件校验CRC32/SHA256升级流程的状态机设计安全启动与固件签名差分升级Delta OTA原理平台STM32F4071MB Flash ESP8266WiFi 适用所有 Cortex-M 系列 MCU二、Bootloader 架构设计2.1 三种核心架构┌────────────────────────────────────────────────────────┐ │ 架构 A双区模式A/B Partition │ │ │ │ ┌──────────┬─────────────┬─────────────┬────────────┐ │ │ │Bootloader│ APP_A │ APP_B │ Download │ │ │ │ (48KB) │ (448KB) │ (448KB) │ Area │ │ │ │0x08000000│ 0x0800C000 │ 0x0807C000 │ │ │ │ └──────────┴─────────────┴─────────────┴────────────┘ │ │ │ │ 升级流程 │ │ APP_A 运行时下载新固件到 APP_B │ │ → 校验通过 → Bootloader 切换启动到 APP_B │ │ → 下次升级从 APP_B 写回 APP_A │ │ │ │ 优点永远有一个可运行的版本回滚安全 │ │ 缺点Flash 利用率仅 50% │ ├────────────────────────────────────────────────────────┤ │ 架构 B单区 下载缓存模式 │ │ │ │ ┌──────────┬──────────────────┬──────────────┐ │ │ │Bootloader│ APP │ Download │ │ │ │ (48KB) │ (512KB) │ Buffer │ │ │ │0x08000000│ 0x0800C000 │ (448KB) │ │ │ └──────────┴──────────────────┴──────────────┘ │ │ │ │ 升级流程 │ │ 先下载新固件到 Download Buffer │ │ → 校验通过 → 重启到 Bootloader │ │ → Bootloader 将 Download Buffer 覆盖 APP │ │ │ │ 优点APP 空间更大 │ │ 缺点覆盖期间断电 → 变砖需要外置看门狗恢复机制 │ ├────────────────────────────────────────────────────────┤ │ 架构 C外部 Flash 模式 │ │ │ │ ┌──────────┬──────────────────────────┐ │ │ │Bootloader│ APP (整个内部 Flash) │ │ │ │ (48KB) │ (976KB) │ │ │ │0x08000000│ 0x0800C000 │ │ │ └──────────┴──────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────┐ │ │ │ 外部 SPI Flash (W25Q64, 8MB) │ │ │ │ → 存储新固件镜像 │ │ │ └──────────────────────────────────────┘ │ │ │ │ 优点APP 可用全部内部 Flash冗余在外置 Flash │ │ 缺点需要额外芯片BOM 成本增加 │ └────────────────────────────────────────────────────────┘推荐对于 STM32F4071MB Flash推荐架构 A双区模式。512KB 的 APP 空间对大多数应用足够双区带来的回滚安全性是最重要的工程保障。2.2 Flash 分区的链接脚本实现/* flash_app_a.ld —— APP_A 的链接脚本 */ MEMORY { FLASH (rx) : ORIGIN 0x0800C000, LENGTH 448K /* APP_A 起始 */ RAM (rwx) : ORIGIN 0x20000000, LENGTH 128K } _estack ORIGIN(RAM) LENGTH(RAM); /* 栈顶 */ SECTIONS { .isr_vector : { KEEP(*(.isr_vector)) } FLASH .text : { *(.text*) } FLASH .rodata : { *(.rodata*) } FLASH .data : { /* ... */ } RAM AT FLASH .bss : { /* ... */ } RAM }/* flash_app_b.ld —— APP_B 的链接脚本关键差异起始地址不同*/ MEMORY { FLASH (rx) : ORIGIN 0x0807C000, LENGTH 448K /* ★ APP_B 起始地址 */ RAM (rwx) : ORIGIN 0x20000000, LENGTH 128K }2.3 为什么 APP 要偏移向量表// 在 APP 的 system_stm32f4xx.c 中 // ★ 关键修改 VTOR向量表偏移寄存器 #define VECT_TAB_OFFSET 0x0000C000 // APP_A 的偏移 void SystemInit(void) { // ... // 对于 APP_AVTOR 0x08000000 0x0000C000 0x0800C000 // 对于 APP_BVTOR 0x08000000 0x0007C000 0x0807C000 SCB-VTOR FLASH_BASE | VECT_TAB_OFFSET; }三、Bootloader 的核心逻辑3.1 Bootloader 主流程// bootloader_main.c #define APP_A_ADDR 0x0800C000 #define APP_B_ADDR 0x0807C000 #define APP_FLAG_ADDR 0x0800FFE0 // 最后一个扇区的末尾存启动标志 typedef enum { APP_SLOT_A 0, APP_SLOT_B 1, } app_slot_t; typedef struct { uint32_t magic; // 魔数0xAA55A5A5 uint32_t active_slot; // 当前活动槽位 uint32_t app_size; // APP 大小 uint32_t app_crc; // APP 的 CRC32 } boot_info_t; void Bootloader_Main(void) { boot_info_t boot_info; // 1. 读取启动信息 ReadBootInfo(boot_info); // 2. 校验启动信息 if (boot_info.magic ! 0xAA55A5A5) { // 首次启动默认从 APP_A 启动 boot_info.active_slot APP_SLOT_A; boot_info.magic 0xAA55A5A5; WriteBootInfo(boot_info); } // 3. 确定要启动的 APP 地址 uint32_t app_addr (boot_info.active_slot APP_SLOT_A) ? APP_A_ADDR : APP_B_ADDR; // 4. 校验 APP 栈指针的合法性 uint32_t app_sp *(volatile uint32_t *)app_addr; if ((app_sp 0x20000000) || (app_sp 0x20020000)) { // 栈地址不合法 → 尝试另一个槽位 app_addr (boot_info.active_slot APP_SLOT_A) ? APP_B_ADDR : APP_A_ADDR; app_sp *(volatile uint32_t *)app_addr; if ((app_sp 0x20000000) || (app_sp 0x20020000)) { // 两个槽位都无效 → 进入下载模式 EnterDownloadMode(); return; } } // 5. CRC 校验 uint32_t app_reset_handler *(volatile uint32_t *)(app_addr 4); uint32_t app_size GetAppSize(app_addr); // 从固件头读取 uint32_t calc_crc CRC32_Calc((uint8_t *)app_addr, app_size); if (calc_crc ! boot_info.app_crc) { // CRC 不匹配 → 尝试另一个槽位 // ... } // 6. ★ 关键的跳转操作 JumpToApp(app_addr); }3.2 跳转到 APP 的汇编实现// 跳转到 APP 的关键恢复默认环境 设置正确的栈和 PC void JumpToApp(uint32_t app_addr) { typedef void (*app_entry_t)(void); app_entry_t app_entry; uint32_t app_stack; // 1. 关闭所有中断NVIC 清空 for (int i 0; i 8; i) { NVIC-ICER[i] 0xFFFFFFFF; // 清除使能 NVIC-ICPR[i] 0xFFFFFFFF; // 清除挂起 } // 2. 关闭 SysTick SysTick-CTRL 0; // 3. 设置 MSP主栈指针 app_stack *(volatile uint32_t *)app_addr; // 向量表第一项 初始 SP __set_MSP(app_stack); // 直接用内核寄存器操作 // 4. 设置 VTOR SCB-VTOR app_addr; // 5. 读复位向量向量表第二项 复位处理函数地址 app_entry (app_entry_t)(*(volatile uint32_t *)(app_addr 4)); // 6. 跳转不再返回 app_entry(); }; 等价的汇编实现更底层、更安全 JumpToApp: LDR SP, [R0] ; R0 app_addr第一项赋值给 SP LDR R1, [R0, #4] ; 第二项 复位处理函数 BX R1 ; 跳转永不再返回四、固件格式设计与校验4.1 自定义固件头// firmware_header.h #define FIRMWARE_MAGIC 0x46574D53 // FWMS typedef struct __attribute__((packed)) { uint32_t magic; // 魔数 uint32_t version; // 版本号如 0x00010002 v1.0.2 uint32_t size; // 固件体大小不含头部 uint32_t crc32; // 固件体的 CRC32 uint32_t timestamp; // 构建时间戳 uint32_t hw_version; // 硬件版本兼容性 uint8_t signature[64]; // ECDSA 签名可选 uint8_t reserved[36]; // 保留补齐 128 字节 } firmware_header_t; // 完整的固件镜像 firmware_header_t 固件体.bin4.2 CRC32 vs SHA256// CRC32快速但仅检测随机错误 uint32_t CRC32_Calc(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; } // 对比CRC32 vs SHA256 // ┌──────────┬──────────┬────────────────────┬──────────┐ // │ 算法 │ 输出长度 │ 防篡改 │ 计算速度 │ // ├──────────┼──────────┼────────────────────┼──────────┤ // │ CRC32 │ 4 字节 │ ❌ 可伪造线性运算│ ★★★★★ │ // │ SHA256 │ 32 字节 │ ✅ 抗碰撞 │ ★★☆☆☆ │ // └──────────┴──────────┴────────────────────┴──────────┘ // 工程实践传输校验用 CRC32完整性验证用 SHA256 // CRC32 放置在固件头中做快速完整性检查 // SHA256 用于固件签名验证安全启动4.3 固件打包工具# pack_firmware.py —— 固件打包脚本 import struct import zlib import time import sys MAGIC 0x46574D53 def pack_firmware(bin_path, version_str, output_path): with open(bin_path, rb) as f: fw_body f.read() # 解析版本号 major, minor, patch map(int, version_str.split(.)) version (major 16) | (minor 8) | patch # 计算 CRC32 crc zlib.crc32(fw_body) 0xFFFFFFFF # 构建头部 header struct.pack(IIIII, MAGIC, # magic version, # version len(fw_body), # size crc, # crc32 int(time.time()) # timestamp ) # 补齐到 128 字节 header b\x00 * (128 - len(header)) # 写入 with open(output_path, wb) as f: f.write(header fw_body) print(fPacked: {output_path}) print(f Version: v{major}.{minor}.{patch}) print(f Size: {len(fw_body)} bytes) print(f CRC32: 0x{crc:08X}) if __name__ __main__: pack_firmware(firmware.bin, 1.0.2, firmware.fw)五、OTA 升级状态机5.1 完整的升级流程┌───────────────┐ │ 正常运行 │ │ (APP 模式) │ └───┬───────┬───┘ │ │ 收到升级指令│ │ 下载失败 / 校验失败 ▼ │ ┌──────────────────┐│ │ 下载新固件 ││ │ (分段下载校验) ││ └───────┬──────────┘│ │ │ 下载完成 │ CRC 通过 │ ▼ │ ┌──────────────┐ │ │ 写入升级标志 │ │ │ 重启系统 │ │ └──────┬───────┘ │ │ │ ▼ │ ┌──────────────┐ │ │ Bootloader │ │ │ 阶段1: 校验 │ │ │ 新固件 CRC │───┘ CRC 失败 → 回退旧固件 └──────┬───────┘ │ CRC 通过 ▼ ┌──────────────┐ │ Bootloader │ │ 阶段2: 切换 │ │ 活动槽位 │ └──────┬───────┘ │ ▼ ┌──────────────┐ │ 启动新固件 │ └──────────────┘5.2 代码实现// ota_manager.c typedef enum { OTA_STATE_IDLE, OTA_STATE_DOWNLOADING, OTA_STATE_VERIFYING, OTA_STATE_READY_TO_SWITCH, OTA_STATE_FAILED, } ota_state_t; static ota_state_t ota_state OTA_STATE_IDLE; static uint32_t ota_bytes_received 0; static uint32_t ota_total_bytes 0; static FIL ota_file; // 收到 OTA 开始命令 int OTA_Begin(uint32_t total_size, uint32_t crc_expected) { // 1. 打开下载文件写入到非活动槽位对应的 Flash 区域 app_slot_t target_slot GetInactiveSlot(); // 2. 如果目标槽位在外部 Flash挂载文件系统 if (f_open(ota_file, firmware.fw, FA_WRITE | FA_CREATE_ALWAYS) ! FR_OK) { return -1; } ota_total_bytes total_size; ota_bytes_received 0; ota_state OTA_STATE_DOWNLOADING; return 0; } // 接收固件数据块 int OTA_ReceiveChunk(const uint8_t *data, uint16_t len) { if (ota_state ! OTA_STATE_DOWNLOADING) return -1; UINT bw; f_write(ota_file, data, len, bw); ota_bytes_received len; if (ota_bytes_received ota_total_bytes) { f_close(ota_file); ota_state OTA_STATE_VERIFYING; } // 返回进度百分比 return (ota_bytes_received * 100) / ota_total_bytes; } // 完成校验 int OTA_VerifyAndCommit(void) { if (ota_state ! OTA_STATE_VERIFYING) return -1; // 1. 读取并校验固件头 firmware_header_t header; // ... 读取文件头部 ... if (header.magic ! FIRMWARE_MAGIC) { ota_state OTA_STATE_FAILED; return -2; // 非法固件 } // 2. CRC 校验固件体 uint32_t calc_crc CRC32_File(ota_file, sizeof(header), header.size); if (calc_crc ! header.crc32) { ota_state OTA_STATE_FAILED; return -3; // CRC 校验失败 } // 3. 写入启动标志下次启动时由 Bootloader 识别 boot_info_t info; info.magic 0xAA55A5A5; info.active_slot GetInactiveSlot(); // ★ 切换到新槽位 info.app_size header.size; info.app_crc header.crc32; WriteBootInfo(info); ota_state OTA_STATE_READY_TO_SWITCH; // 4. 软件复位 NVIC_SystemReset(); // 不会执行到这里 return 0; }六、差分升级Delta OTA6.1 原理差分升级不传输完整固件只传输新旧版本的差异 旧固件 (v1.0) ──┐ ├──► bsdiff/hdiffpatch ──► 差异包 (patch) 新固件 (v1.1) ──┘ (通常只有固件的 5%~30%) MCU 端 旧固件 patch ──► apply_patch() ──► 新固件 节省的流量 1 - (patch_size / full_firmware_size) 典型节省70%~95%6.2 在 MCU 上的实现方案// 简易差分升级——基于块的比较适合资源有限的 MCU #define BLOCK_SIZE 1024 // 1KB 块 typedef enum { DELTA_CMD_COPY_FROM_OLD 0, // 从旧固件拷贝 DELTA_CMD_COPY_FROM_NEW 1, // 从差异包中新数据拷贝 } delta_cmd_t; typedef struct { delta_cmd_t cmd; uint32_t src_offset; // COPY_FROM_OLD 时的源偏移 uint16_t length; // 拷贝长度 // 如果是 COPY_FROM_NEW数据跟在命令后面 } delta_block_t; // 应用差异包最简实现 int Delta_Apply(const uint8_t *old_fw, const uint8_t *patch, uint8_t *new_fw, uint32_t new_size) { const uint8_t *p patch; uint32_t new_offset 0; while (new_offset new_size) { delta_block_t *block (delta_block_t *)p; p sizeof(delta_block_t); if (block-cmd DELTA_CMD_COPY_FROM_OLD) { memcpy(new_fw new_offset, old_fw block-src_offset, block-length); } else { memcpy(new_fw new_offset, p, block-length); p block-length; } new_offset block-length; } return 0; }七、安全启动Secure Boot7.1 信任链安全启动的信任链 ┌───────────────────────────────────────┐ │ ROM Bootloader芯片出厂固化 │ │ 验证 Bootloader 签名 → 启动 Bootloader│ ├───────────────────────────────────────┤ │ Bootloader你写的 │ │ 验证 APP 签名 → 启动 APP │ ← 本文的范围 ├───────────────────────────────────────┤ │ APP │ │ 运行用户业务逻辑 │ └───────────────────────────────────────┘ 签名验证算法ECDSA椭圆曲线数字签名算法ECC-P2567.2 固件签名验证实现// 使用 micro-ecc 库适合 MCU 的轻量 ECC 库 #include uECC.h // 编译时嵌入公钥私钥绝不在固件中 static const uint8_t pub_key[64] { 0x12, 0x34, /* ... 64 字节 ECC-P256 公钥 ... */ }; // 验证固件签名 int SecureBoot_Verify(const uint8_t *firmware, uint32_t fw_size, const uint8_t *signature, uint32_t sig_size) { // 1. 计算固件的 SHA256 哈希 uint8_t hash[32]; SHA256_Calc(firmware, fw_size, hash); // 2. 用公钥验证 ECDSA 签名 int ret uECC_verify(pub_key, hash, sizeof(hash), signature, uECC_secp256r1()); return ret; // 1 验证通过0 验证失败固件被篡改或未签名 } // 签名工具在 PC 上运行绝不在 MCU 上 // $ openssl ecparam -genkey -name prime256v1 -out private_key.pem // $ openssl ec -in private_key.pem -pubout -out public_key.pem // $ ./sign_firmware.py firmware.bin private_key.pem firmware.signed.fw八、工业级可靠性增强8.1 看门狗保护// Bootloader 中启用独立看门狗 void Bootloader_Init(void) { // IWDG: LSI ~40kHz, 预分频 64 → 640Hz // 重装载值 3200 → 超时约 5 秒 IWDG-KR 0x5555; // 解除写保护 IWDG-PR 0x04; // 预分频 64 IWDG-RLR 3200; // 5 秒超时 IWDG-KR 0xCCCC; // 启动看门狗 // 在 Flash 写入操作的关键步骤喂狗 // 如果写入超时Flash 损坏→ 看门狗复位 → 重试 }8.2 下载断点续传// 断点续传支持 typedef struct { uint32_t total_size; uint32_t received_size; uint32_t chunk_size; uint8_t resumed; // 是否续传 } ota_resume_info_t; int OTA_Resume(ota_resume_info_t *info) { // 从 Flash 中读取上次中断时的进度 ReadResumeInfo(info); if (info-resumed) { printf(Resuming from %lu / %lu bytes\r\n, info-received_size, info-total_size); // 向服务器请求从 received_size 偏移处继续传输 // HTTP Range: bytesreceived_size- } return 0; }九、总结设计要点推荐方案分区策略双区模式A/B——回滚最安全固件校验CRC32快速 SHA256防篡改掉电保护先写完整再切换标志双区确保至少一个可用安全启动ECDSA 签名验证micro-ecc 库带宽优化差分升级bsdiff典型节省 70%~95%可靠性IWDG 断点续传 下载超时重试最小 Bootloader 大小~12KB优化后可做到 8KB核心原则Bootloader 的第一要务是确保设备永远不会变砖。宁可升级失败回退旧版本也不能因为升级导致设备不可用。双区模式 看门狗 校验机制 工业级可靠性。下一篇预告嵌入式低功耗设计深度解析