公司动态
OTA 实战二:STM32 从零写 Bootloader,实现串口升级
适用人群跑通过上篇 ESP32 OTA、想搞懂升级到底怎么跳过去的同学读完你能得到① 明白为什么 STM32 没有 ESP-IDF 那种免费 OTA② 亲手写出一个能收固件、写 Flash、安全跳转到 app 的 Bootloader③ 避开 5 个必踩的 HardFault 坑。一、为什么 STM32 要自己写 Bootloader上篇我们用 ESP32esp_ota_ops.h把 bootloader、分区表、校验全包了——你一行底层代码没写。但 STM32裸机或只跑 FreeRTOS没有这套东西。ESP32 的 OTA 是乐鑫在 SDK 里给你做好的特例STM32 出厂只有一个最简 Bootloader藏在系统存储器里只支持串口/USB 升级且不能和你业务共存。所以现实场景是你做了个STM32 4G 模块的远程采集器装在野外铁塔里。某天发现 bug你不可能爬塔拆壳。你希望设备通过 4G本质是串口透传到 MCU自己收新固件、自己刷、自己重启。这活儿得你自己写 Bootloader。二、前置概念这 4 个不懂代码必懵名词白话解释中断向量表一块放在 Flash 最前面、按固定顺序排列的函数地址清单。第 0 项是初始栈顶 MSP第 1 项是复位处理函数地址后面依次是每个中断的入口。CPU 上电/进中断时按这张表找代码VTOR一个寄存器SCB-VTOR告诉 CPU向量表在哪。默认在0x08000000。app 不在 0 地址时必须改它否则中断全跑飞跳转Jump本质就是把栈指针指向 app 的栈顶再把 PC 指到 app 的复位向量等于手动触发一次 app 的启动Flash 擦写单位STM32 的 Flash不能按字节改要先整扇区擦除擦完全 0xFF才能写。F1 一次写半字(2B)F4 写字(4B)H7 写双字(8B)——写前务必查参考手册 ESP32 有分区表自动管理地址STM32 没有地址全靠链接脚本写死 代码里常量约定。bootloader 和 app 的地址必须严丝合缝对上。三、STM32 的 Flash 布局没有分区表只有地址约定以STM32F4071MB Flash为例我们约定0x08000000 ┌──────────────────────────┐ │ Bootloader (64KB) │ ← 复位入口永远在这 │ sector 0~3 (16KB×4) │ 0x08010000 ├──────────────────────────┤ │ Application (剩余 ~960KB)│ ← APP 起始地址 APP_ADDR │ 向量表(最前面 0x188 字节) │ │ 你的业务代码/数据 │ └──────────────────────────┘ 0x080FFFFF约定常量bootloader、app、PC 工具三方共用#defineBOOT_ADDR0x08000000// Bootloader 起点复位向量在这#defineAPP_ADDR0x08010000// Application 起点 sector 4#defineAPP_SIZE0x000F0000// 留给 app 的空间四、Bootloader 核心代码4.1 先校验 app 合不合法防跳进砖里CPU 上电后不能无脑跳 app。先读 app 向量表前两项判断是不是一份真固件#includestm32f4xx.hintapp_is_valid(uint32_tapp_addr){uint32_tsp*(volatileuint32_t*)app_addr;// 向量表[0] 初始 MSPuint32_tpc*(volatileuint32_t*)(app_addr4);// 向量表[1] 复位向量// ① 栈顶必须在 SRAM 范围F407 SRAM: 0x20000000 ~ 0x20030000, 192KBif((sp0xFF000000)!0x20000000)return0;if(sp0x20030000)return0;// ② 复位向量必须在 Flash 范围且最低位为 1Thumb 状态if((pc0xFF000000)!0x08000000)return0;if(!(pc0x1))return0;return1;// 看起来像一份合法的 app}4.2 跳转函数OTA 的切换启动本质就这十几行typedefvoid(*app_entry_t)(void);voidjump_to_app(uint32_tapp_addr){uint32_tsp*(volatileuint32_t*)app_addr;uint32_tpc*(volatileuint32_t*)(app_addr4);__disable_irq();// 先关全局中断避免切换瞬间用旧向量表误触发// 清掉 bootloader 侧所有 NVIC 使能与挂起中断for(inti0;i8;i){NVIC-ICER[i]0xFFFFFFFF;NVIC-ICPR[i]0xFFFFFFFF;}SysTick-CTRL0;// 关 SysTick否则 app 接管前可能进滴答中断SCB-VTORapp_addr;// 向量表切到 app__DSB();__ISB();// 确保新向量表对取指可见后再跳__set_MSP(sp);// 主栈指针设成 app 的栈顶__enable_irq();// 关键恢复全局中断。app 默认期望 PRIMASK0// Cortex-M 上电/复位即 PRIMASK0此处不恢复// app 的中断将永远被屏蔽裸机工程会假死app_entry_tentry(app_entry_t)(pc|0x1);// 最低位1 表示 Thumbentry();// 跳相当于手动复位进 app}⚠️ 顺序要点关中断 → 清 NVIC/SysTick → 改VTOR紧跟__DSB();__ISB()让新向量表生效→ 设MSP→__enable_irq()恢复全局中断→ 跳。最容易踩的坑就是忘了最后一步在 bootloader 里__disable_irq()后PRIMASK不会自动恢复而 app 的 Reset_Handler 通常不主动开中断于是 app 跑起来中断全被屏蔽。app 的复位处理函数会重新初始化.data/.bss、再设一次VTOR然后进main。4.3 主流程等触发 → 校验 → 升级 / 跳转教学版用串口等待触发字符最简单直观量产可改成读备份寄存器/RTC 标志不依赖手速intmain(void){// 注uart_wait_byte_timeout 为示意函数需你用串口驱动实现等待 N 毫秒收 1 字节HAL_Init();SystemClock_Config();uart_init();// 用于收固件的串口led_init();// 上电先闪 LED 表示我在 bootloaderfor(inti0;i6;i){LED_TOGGLE();HAL_Delay(200);}// 等待 2 秒若收到升级触发字节 U进入升级if(uart_wait_byte_timeout(U,2000)0){do_upgrade();// 收固件 写 Flash见 4.4}// 没触发、且 app 合法 → 跳过去不合法 → 留在 bootloader 等重传if(app_is_valid(APP_ADDR)){jump_to_app(APP_ADDR);}while(1){LED_TOGGLE();HAL_Delay(500);}// 停在 bootloader}4.4 收固件 写 Flash简化帧原理版下面用自定义帧演示边收边写的原理生产建议直接用成熟的Ymodem协议自带序号/重传/CRC不要自己造轮子// 帧格式: [0xA5][长度高][长度低][序号][数据...][CRC16低][CRC16高]voiddo_upgrade(void){uint32_taddrAPP_ADDR;uint32_toffset0;flash_erase_app(APP_ADDR,APP_SIZE);// 整片擦 app 区while(1){frame_tfuart_recv_frame();// 收一帧教学简化if(f.head0xEE)break;// 0xEE 结束帧// 按 4 字节对齐写入 FlashF4 一次写 1 个字// 用 memcpy 而非 *(uint32_t*) 强转避免非对齐访问M0 会 HardFault且属 C 未定义行为for(inti0;i4f.len;i4){uint32_tword;memcpy(word,f.data[i],4);flash_write_word(addroffset,word);offset4;}uart_send_ack(f.seq);// 回 ACK发送方才发下一帧}if(app_is_valid(APP_ADDR)){jump_to_app(APP_ADDR);// 校验通过跳 app}// 失败则自然落到 main 末尾的停机循环等 PC 重传}Flash 擦写函数F4 示例注意写前解锁、写后锁voidflash_erase_app(uint32_taddr,uint32_tsize){HAL_FLASH_Unlock();__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP|FLASH_FLAG_OPERR|FLASH_FLAG_WRPERR|FLASH_FLAG_PGSERR);FLASH_EraseInitTypeDef ei{0};ei.TypeEraseFLASH_TYPEERASE_SECTORS;ei.SectorFLASH_SECTOR_4;// app 从 sector 4 (0x08010000) 开始ei.NbSectors8;// F407 sector 4~11ei.VoltageRangeFLASH_VOLTAGE_RANGE_3;// 3.3V 供电uint32_tbad_sector0;HAL_FLASHEx_Erase(ei,bad_sector);HAL_FLASH_Lock();}voidflash_write_word(uint32_taddr,uint32_tdata){HAL_FLASH_Unlock();HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,addr,data);// F4 写字(4B)HAL_FLASH_Lock();}⚠️型号差异F1 是半字(2B)、F7/H7 是双字(8B)VoltageRange也不同。移植时务必查你芯片的参考手册RM0090 对应 F4别照抄。文中的 Flash 编程、选项字节和跳转细节都来自 STM32F4 参考手册 RM0090、ST 应用笔记 AN4657 和 ARM Cortex-M4 技术参考手册。五、Application 侧必须改的 3 处新手最易漏Bootloader 写好了app 不改成住在 0x08010000两者会打架。三处① 改链接脚本起始地址CubeIDE 的.ld/ Keil 的 Target 对话框/* 旧: FLASH (rx) : ORIGIN 0x08000000, LENGTH 1M */ /* 新: */ FLASH (rx) : ORIGIN 0x08010000, LENGTH 0xF0000② 重定向向量表让 app 用自己的向量表CubeMX 工程改system_stm32f4xx.c#defineVECT_TAB_OFFSET0x10000// 原来 0x00改成 app 偏移或在main()开头直接写SCB-VTORFLASH_BASE0x10000;③ 其余业务代码不用动——app 完全不知道自己被 bootloader 托管正常写main即可。为什么必须改这两处改 ① 是告诉链接器代码请放到 0x08010000 起改 ② 是告诉 CPU中断来时去 0x08010000 找向量表。只改一个必 HardFault。六、校验与回滚让升级升坏了也不死本篇是单 app bootloader结构没上篇的 A/B 双分区回滚思路不同升级前bootloader 先擦 app 区。若收固件中途断线app 已被擦成 0xFF →app_is_valid必然失败 → 重启后 bootloader 发现 app 不合法自动停在升级模式等重传。设备不会变砖只是暂时进不了业务。升级后app_is_valid通过才跳。这就是收到完整且合法的固件才接管。想要真正 A/B 回滚把 Flash 再切出一份APP_BACKUP升级前把旧 app 备份过去新 app 跑起来自检失败比如连续重启 3 次bootloader 把 backup 拷回执行。这个留作进阶练习。安全原则不变底线是升坏了也不死机。单 app 方案靠校验失败就不跳 停留可重传实现A/B 方案靠备份分区实现。七、新手必踩的 5 个 HardFault 坑#坑现象正确做法1VTOR 没设/设错app 一开中断就 HardFaultSCB-VTOR APP_ADDR地址须按向量表大小对齐F4 至少 0x200用 0x08010000 这种 64KB 对齐地址必没问题2app 链接地址没改bootloader 和 app 重叠互烧互覆盖.ld里 FLASH ORIGIN 改成0x08010000LENGTH 减 0x100003跳之前没校验栈顶向量表损坏跳过去直接死循环跳前必做app_is_valid栈顶须在 SRAM 范围4Flash 没解锁/没清标志HAL_FLASH_Program返回写保护/Busy 错误HAL_FLASH_Unlock()__HAL_FLASH_CLEAR_FLAG(...)再写5跳转前没关中断旧 bootloader 的 ISR 在新向量表生效前被触发跑飞jump_to_app第一行__disable_irq()八、动手练一练照着做你就能讲给面试官听在 CubeIDE 建两个工程bootloaderROM 0x08000000和appROM 0x08010000app 里让 LED 以 1Hz 闪。bootloader 上电先以 5Hz 快闪 LED 表示我在 boot 区2 秒内串口收到U则升级否则跳 appapp 慢闪靠频率区分。用串口助手手动发几帧结束帧用0xEE让 bootloader 收数据写 Flash 再跳 app验证 app 能跑。故意把 app 的链接地址保持 0x08000000 编译烧录观察为什么冲突——这步让你彻底理解链接地址是什么。把 app 里SCB-VTOR那行注释掉看一开串口中断就死机——这步让你彻底理解向量表重映射为什么不能省。5 步跑通你就拥有了一个可量产、可远程的 STM32 升级方案骨架这比只会调库值钱得多。小结ESP32 的 OTA 是别人把底层写好你调用STM32 的自写 Bootloader 才是升级到底怎么发生的真相收固件写 Flash → 校验栈顶和复位向量 → 改 VTOR → 设 MSP → 跳。三处地址约定bootloader 起点、app 起点、VTOR 偏移对不齐就 HardFault。把本篇 5 个坑和 5 步练习吃透STM32 OTA 你就算真正入门了。下一篇预告《差分升级Delta OTA—— 4G 流量太贵怎么办》