恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
STM32串口IAP Bootloader完全指南:分区、协议与安全升级
首页
资讯中心
/
STM32串口IAP Bootloader完全指南:分区、协议与安全升级
STM32串口IAP Bootloader完全指南:分区、协议与安全升级
发布时间:2026/10/20 15:26:47
简介STM32 在应用编程IAP配合串口 Bootloader是嵌入式产品实现串口固件升级的常见方案该资源以用户模式 Bootloader 为核心面向需要为 STM32 增加在线升级能力的中级嵌入式开发者覆盖启动、UART 通信、固件校验、Flash 写入与跳转运行全流程。压缩包共 108 个文件约 1.16MB主要包含 36 个 h 头文件、31 个 c 源文件、6 个 hex 与 6 个 bin 固件以及 bat 转换脚本、Keil 工程文件、链接脚本和调试配置便于打开工程直接查看 IAP 实现并烧录验证。已有 648 人学习下载适合用于学习 STM32 IAP 原理、移植 Bootloader 或作为量产升级模块的基础代码。资源针对不同容量 STM32 系列提供了对应固件并附有 hextobin、axftobin 等转换工具可辅助理解编译产物转换与烧写流程目录结构清晰能快速定位串口驱动、Flash 写入与跳转代码配合 README 等说明可让读者更高效地在现有工程中复用这套升级机制。1. STM32串口IAP Bootloader升级程序的最后一公里保障串口IAP这四个字看着技术感很强本质却朴素让主控在不清除当前程序的前提下通过UART把新固件写进Flash再跳过去运行。不少团队看到stm32-iap-uart-boot这个组合以为就是用串口下载bin文件真写完才发现难的是Flash分区、中断向量和掉电保护。本文按完整落地路径拆先讲Bootloader与App如何在Flash共处再给出可复现的跳转函数和串口协议状态机接着落到上位机脚本与升级流程最后把现场常见的坑和防变砖措施讲明白。新手能照着把第一版跑通熟手可以直接拿来做量产升级方案。2. Flash分区与中断向量偏移Bootloader的立足之本写IAP的第一件事不是写串口而是决定Flash怎么分。很多Demo把Bootloader放在0x08000000App放在0x08008000甚至0x08010000但说不清为什么。这里的原则只有一条Boot区和App区都要页对齐且App的链接地址必须和Bootloader里跳转用的地址完全一致。2.1 把Flash分成三块Boot区、App区、参数区以比较常见的STM32F103RCT6256KB Flash、48KB RAM高密度型号每页2KB为例我一般这么分| 区域 | 起始地址 | 大小 | 存放内容 | | Boot区 | 0x08000000 | 16KB | IAP引导程序 | | App区 | 0x08004000 | 112KB | 用户功能固件 | | 参数区 | 0x0801C000 | 16KB | 升级标志、版本号、回滚备份 |为什么要留独立的参数区升级标志可以放这里避免每次升级都去擦App区头部版本号和回滚信息也最好放在独立区域不要在App区里反复擦写。参数区分页对齐以后擦写它不影响Boot区和App区。分区时一定按芯片实际的页/扇区大小做整数对齐。F103高密度型号一页2KB16KB就是8个页F407这种F4系列单扇区最小16KB。分区表不对齐擦除时就会波及邻居区域轻则标志丢失重则把Bootloader自己擦掉。2.2 跳转App的C函数栈顶校验不能省Bootloader收到跳转命令后真正让控制权交到App手里靠的是一个函数指针。前面分区分得好这一步就稳。核心代码#define APP_START_ADDR 0x08004000u #define STM32_RAM_START 0x20000000u #define STM32_RAM_SIZE 48u * 1024u /* F103RCT6 的 48KB RAM */ typedef void (*pFunction)(void); void jump_to_app(void) { pFunction app_entry; /* App起始地址的前4字节是栈顶指针(MSP)紧接着4字节是Reset_Handler地址 */ uint32_t stack_addr *(volatile uint32_t *)APP_START_ADDR; uint32_t reset_addr *(volatile uint32_t *)(APP_START_ADDR 4); /* 栈顶指针必须落在RAM范围内否则说明App区根本没有有效固件 */ if ((stack_addr STM32_RAM_START) || (stack_addr (STM32_RAM_START STM32_RAM_SIZE))) { return; /* 不跳转留在IAP等待升级 */ } app_entry (pFunction)reset_addr; __disable_irq(); /* 跳转瞬间不允许任何中断介入 */ SysTick-CTRL 0; /* 停掉系统节拍定时器 */ HAL_UART_DeInit(huart1); /* 反初始化串口避免App初始化串口时冲突 */ HAL_RCC_DeInit(); /* 恢复时钟到复位默认状态 */ __set_MSP(stack_addr); /* 换成App的栈顶指针 */ app_entry(); /* 跳转永不返回 */ }逻辑说明STM32的Cortex-M上电后CPU从0x08000000取栈顶指针从0x08000004取复位入口。App链接到0x08004000后它头部的前8字节同样是这两个信息所以跳转时先读出来验证再走。注释里App区没有固件的场景很常见擦除后还没写数据Flash内容是全0xFF读出来的栈顶指针大概率落在RAM范围外直接被拦住。参数说明范围检查用了STM32_RAM_START和STM32_RAM_SIZE两个宏比位掩码更直白换芯片时只改宏就行。调试时如果怀疑跳转失败可以在return前加打印把读到的stack_addr打出来一眼就能看出是不是RAM地址。注意如果App启用了外部SDRAM并把栈放过去这个检查范围要按实际内存映射调整。另一个细节__disable_irq()只是屏蔽中断不清pending。如果跳转前正好有串口中断pending跳到App后一旦开中断会用App的中断向量偏移后的入口执行表现很随机。所以跳转前要把用过的外设逐一反初始化这一步不能省。用寄存器标准库的项目对应的函数是RCC_DeInit()和USART_DeInit()作用一样。2.3 App里的中断向量偏移没有这一行跳转后一进中断就死跳转函数写完了如果App工程里不做两件事升级后多半会翻车App的链接起始地址必须是APP_START_ADDRApp的main()里必须设置SCB-VTOR。MDK工程对应修改Target选项卡中的IROM1起始地址为0x08004000Size设置为0x1C000112KB。GCC工程则改链接脚本里的FLASH起始地址。编译出的bin文件烧进App区后第一行就是栈顶指针、第二行是复位入口和Bootloader里读的数据完全对上。在App的main函数第一行加上/* App必须在所有外设初始化之前重定位中断向量表 */ SCB-VTOR APP_START_ADDR;为什么强调第一行如果先初始化了SysTick或串口再改VTOR中断随时可能发生一发生就去查老向量表结果跳到Bootloader里的中断处理函数现场表现为随机死机非常难查。Cortex-M0/M0系列没有VTOR寄存器得用向量表拷到SRAM再重映射的方案M3/M4直接用VTOR最省事。提示Cortex-M0/M0 没有 VTOR 寄存器App 侧的中断向量重定位要用向量表搬运方案。选型时如果计划做 IAP优先考虑带 VTOR 的 M3/M4 内核能省不少事。到这里Bootloader最根基的两件事已经跑通。下一章把协议和程序状态机补上。3. UART协议与Bootloader程序设计从复位到跳转分区和跳转解决的是App放哪、怎么过去这章解决Bootloader怎么拿到数据。串口协议是整个IAP可靠性的关键协议没设计好后面不管上位机怎么写都不稳定。我建议先把协议定死在纸上再写状态机。3.1 一个够用的帧格式帧头、命令、长度、数据、CRC16我用过几版协议最后留下来的是这个升级包出错率最低| 字段 | 长度 | 取值/含义 | | 帧头 | 1字节 | 固定0xA5用于字节同步 | | 命令字 | 1字节 | 0x01擦除 0x02写数据 0x03校验 0x04跳转 0x05查询版本 | | 数据长度 | 2字节 | 大端序payload字节数 | | payload | N字节 | 地址(4字节) 数据 | | CRC16 | 2字节 | 从命令字到payload末尾的CRC-16/MODBUS |CRC覆盖范围不含帧头方便Bootloader先定位帧头再做校验定位错了CRC也过不了。长度用大端序主流工具链和调试器都习惯大端长度MCU端解析也不会搞错字节序。命令字设计成一张表放在头文件里上位机和Bootloader共用同一份定义加新命令时两边同步改避免上位机发了0x05、Bootloader还以为是0x05的旧含义这种低级错误。3.2 串口接收状态机逐字节喂别等整帧一个常见错误是串口收到一帧再丢给解析函数。串口是流式数据什么时刻帧边界不可预知一旦错位整包全废。正确的做法是状态机每进一个字节走一步typedef enum { WAIT_HEADER, /* 等待0xA5 */ WAIT_CMD, /* 等待命令字 */ WAIT_LEN_H, /* 等待长度高字节 */ WAIT_LEN_L, /* 等待长度低字节 */ WAIT_DATA, /* 接收数据 */ WAIT_CRC_H, /* 等待CRC高字节 */ WAIT_CRC_L /* 等待CRC低字节校验并分发 */ } rx_state_t; typedef struct { rx_state_t state; uint8_t cmd; uint16_t payload_len; uint16_t recv_cnt; uint8_t buf[1152]; /* 1024数据 4地址 余量 */ uint16_t crc; uint16_t crc_recv; } rx_ctx_t; void iap_rx_byte(uint8_t byte, rx_ctx_t *ctx) { switch (ctx-state) { case WAIT_HEADER: if (byte 0xA5) ctx-state WAIT_CMD; break; case WAIT_CMD: ctx-cmd byte; ctx-state WAIT_LEN_H; break; case WAIT_LEN_H: ctx-payload_len byte 8; ctx-state WAIT_LEN_L; break; case WAIT_LEN_L: ctx-payload_len | byte; ctx-recv_cnt 0; ctx-state WAIT_DATA; break; case WAIT_DATA: if (ctx-recv_cnt ctx-payload_len) { ctx-crc crc16_step(ctx-crc, byte); ctx-buf[ctx-recv_cnt] byte; } if (ctx-recv_cnt ctx-payload_len) ctx-state WAIT_CRC_H; break; case WAIT_CRC_H: ctx-crc_recv byte 8; ctx-state WAIT_CRC_L; break; case WAIT_CRC_L: ctx-crc_recv | byte; if (ctx-crc_recv ctx-crc) iap_dispatch(ctx); /* 校验通过按命令分发 */ /* 无论是否通过都回到帧头状态等待下一帧 */ ctx-state WAIT_HEADER; break; default: ctx-state WAIT_HEADER; break; } }逻辑说明状态机放在串口中断或DMA回调里每收到一个字节调用一次。WAIT_LEN_H先把长度高字节暂存等到WAIT_LEN_L再合并避免帧被拆成两个部分时中间状态被干扰。WAIT_DATA里边收边算CRC收到指定长度后转去等CRC最后统一比对。参数说明ctx-buf大小决定单包最大长度。上位机一包1024字节payload还有4字节写入地址所以buf给1152字节够用同时要在WAIT_DATA里对payload_len做上限判断超过buf容量就丢弃防止写穿缓冲区。超时重置也不能省用HAL_GetTick记录每字节到达时间超过500ms没收到下一字节就强制把state拉回WAIT_HEADER。没有这个机制拔线或半包会把Bootloader卡死在等待状态后续命令永远进不来。3.3 Flash擦写细节擦除粒度与半字编程数据校验通过后真正写入Flash还要注意几件事。写入函数uint32_t flash_write_block(uint32_t addr, uint8_t *src, uint16_t len) { HAL_FLASH_Unlock(); /* F103只能半字编程len需为偶数循环内按2字节合并写入 */ for (uint16_t i 0; i 1 len; i 2) { uint32_t word src[i] | (src[i 1] 8); if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr i, word) ! HAL_OK) { HAL_FLASH_Lock(); return HAL_FLASH_GetError(); } } /* 奇数长度时最后一字节单独补0处理 */ if (len 1) { uint32_t word src[len - 1]; if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr len - 1, word) ! HAL_OK) { HAL_FLASH_Lock(); return HAL_FLASH_GetError(); } } HAL_FLASH_Lock(); return 0; }逻辑说明HAL_FLASH_Program只负责编程不负责擦除。Flash写1变0不需要擦除写0变1必须先擦除所以写数据之前要先对整个App区执行擦除命令再分块写。F103高密度型号一页2KBF4系列最小扇区16KB擦除粒度影响上位机的分片策略工程里移植时最容易踩坑。参数说明F103只能半字编程len是奇数时最后一个字节要单独处理这里用补0的方式避免越界。FLASH_TYPEPROGRAM_HALFWORD在F4上支持更宽的类型但如果要在F103和F4之间移植这个参数改错了会编译通过但运行跑飞临时改类型一定查对应芯片参考手册。注意F103的一页是2KB高密度型号F4系列最小扇区16KB页对齐在不同系列里粒度不同分区表和擦除代码一定要跟具体芯片手册对齐。4. 上位机工具与升级流程把bin文件送进FlashMCU端Bootloader跑起来以后升级能不能顺畅取决于上位机怎么配合。这章讲两件事怎么选传输协议、怎么生成bin以及一条完整的升级流程长什么样。4.1 Ymodem与自研协议通用方案和量产方案的取舍| 对比项 | Ymodem | 自研协议 | | 上位机 | SecureCRT/Tera Term直接用 | 需要自己写脚本或上位机 | | Bootloader复杂度 | 需完整实现Ymodem握手与块传输 | 按自己帧格式写逻辑更轻 | | 错误可追溯性 | 只有协议状态码 | 自定义应答码和错误码产线好排查 | | 学习成本 | 有现成实现和参考文档 | 协议边界自己定容易改但也容易踩坑 |结论很直接如果只是给三五块板子打个固件Ymodem最快PC端不用写东西Bootloader找官方参考实现就行如果要做产线工具或者远程运维我建议自研协议因为每一帧的应答、错误码和重传逻辑都能量化出问题知道卡在哪一帧。4.2 让App工程直接产出binfromelf一行命令从MDK工程里拿bin不用二次用工具软件转换。在Options for Target的User选项卡After Build/Rebuild里加一行fromelf --bin --output./Objects/app.bin ./Objects/app.axfGCC工具链对应的是arm-none-eabi-objcopy -O binary app.elf app.bin逻辑说明fromelf由ARM编译工具链提供路径在工具链bin目录确认环境变量能找到。--bin输出纯二进制不带地址信息Bootloader收到什么就往App区写什么。产物路径建议放到工程输出目录别跟源码混在一起上位机从固定路径读取避免每次复制错文件。hex文件不适合IAP。hex是带绝对地址的记录格式直接把hex发给Bootloader还要解析记录既增加协议复杂度又引入地址和实际分区不符的风险没有任何好处。4.3 一条完整的升级流程脚本擦除、写入、校验、跳转实际项目里我用Python脚本配合产线工位软件做验证核心流程如下import serial, struct, time FRAME_HEADER 0xA5 CMD_ERASE 0x01 CMD_WRITE 0x02 CMD_VERIFY 0x03 CMD_JUMP 0x04 APP_START 0x08004000 PACK_SIZE 1024 def crc16(data: bytes) - int: crc 0xFFFF for b in data: crc ^ b for _ in range(8): crc (crc 1) ^ 0xA001 if crc 1 else crc 1 return crc def build_frame(cmd: int, payload: bytes b) - bytes: body bytes([cmd]) struct.pack(H, len(payload)) payload crc crc16(body) return bytes([FRAME_HEADER]) body struct.pack(H, crc) dev serial.Serial(COM3, 115200, timeout1) def send_cmd(cmd, payloadb): dev.write(build_frame(cmd, payload)) resp dev.read(4) # 应答固定4字节生产环境要按帧解析 time.sleep(0.1) # 兜底等待实际以MCU应答为准 return resp # 1) 擦除App区。擦除命令的payload携带起始地址MCU端按页整片擦 send_cmd(CMD_ERASE, struct.pack(I, APP_START)) # 2) 分片写入。每包1024字节payload前4字节是写入地址 with open(app.bin, rb) as f: firmware f.read() for offset in range(0, len(firmware), PACK_SIZE): chunk firmware[offset:offset PACK_SIZE] payload struct.pack(I, APP_START offset) chunk send_cmd(CMD_WRITE, payload) time.sleep(0.05) # 3) 整片校验。MCU端对App区重新算CRC与上位机结果对比 calc crc16(firmware) resp send_cmd(CMD_VERIFY, struct.pack(I, calc)) print(verify resp:, resp.hex()) # 4) 校验通过才跳转 send_cmd(CMD_JUMP, struct.pack(I, APP_START))逻辑说明擦除、写入、校验、跳转四步严格串行每一步都等MCU应答后再发下一步。写入时每包携带绝对地址这样即使中间断线重来也能从任意偏移继续相当于天然支持断点续传。参数说明PACK_SIZE取1024是经验值。包太大容易在MCU端Flash写入期间丢字节包太小握手次数翻倍、升级变慢115200波特率下1024字节最均衡。两个time.sleep是粗粒度兜底正式上位机应该改为应答定超时重传把MCU端擦写112KB可能需要的几百毫秒考虑进去。最后一步整片校验不能省单包CRC通过只能证明每包没传错无法发现丢了一整包这种结构性错误只有MCU端重新计算整个App区的CRC才能确认固件真正完整可用。5. IAP避坑指南四个最容易翻车的现场问题写完Bootloader只是开始接入产品才是真正的考验。下面四个坑来自实际项目按现象、原因、解决的顺序给排查路径。5.1 跳转后App直接跑飞卡死在HardFault_Handler现象Bootloader串口打印Jump to App随后板子无响应调试器挂上发现进了HardFault或者Bootloader干脆不跳一直停在等待升级。原因一是App工程的链接地址不是0x08004000编译出的bin带了错误的栈顶指针二是栈顶指针检查通过了但Reset_Handler地址指向非法位置三是App区里的内容是空的或写了一半前8字节本来就是0xFFFFFFFF。排查与解决先在上位机对整片App做CRC校验再核对App工程的IROM1地址和bin文件大小。调试时在jump_to_app里读一次*(uint32_t *)APP_START_ADDR打印出来看是否落在RAM区域。如果是0xFFFFFFFF说明擦除后没写进去问题在串口协议先查帧CRC和写入流程不要一上来就怀疑跳转代码。5.2 升级中途断电板子完全变砖现象写数据写到一半电源断开重新上电后板子没有App也没有打印只有Bootloader还能串口握手。最坏情况下变砖的设备只能开壳用烧录器救。原因升级流程先擦除整个App区再逐包写入一旦中途断电App区是残缺的没有可执行代码。Bootloader里如果只靠App区有效标志判断标注为有效但实际残缺的固件也会被跳转执行。解决两条路至少做一条。一是双区方案把Flash分成Boot、App_Active、App_Backup三个区新固件完整写入备份区整片CRC通过后再一次性切换有效标志下次启动跳新的App区。二是有效标志超时兜底App区写完才置有效标志置了之后Bootloader不再修改该标志断线重连时从断点偏移继续把断电窗口压缩到最小。量产设备建议直接上双区投入成本换来的是一次断电都不怕的升级体验。5.3 跳转后串口乱码printf输出一堆乱字符现象Bootloader阶段串口助手收发正常跳转后App的串口打印乱码或者串口完全不动。原因Bootloader初始化过的USART在跳转时没有反初始化。App代码编译时假设自己从复位开始寄存器里却留着Bootloader的USART配置波特率分频器、中断使能位甚至DMA流都不一样。另一种情况是App里配置了系统时钟但Bootloader跳转前没把时钟恢复默认USART外设在错误时钟下工作。解决跳转函数里在__disable_irq()之后依次调用HAL_UART_DeInit、HAL_RCC_DeInit和HAL_GPIO_DeInit把MCU还原到接近复位的状态。用寄存器标准库就调对应的USART_DeInit和RCC_DeInit。这步在跳转前做比在App里反复初始化可靠得多。5.4 波特率看着对数据就是频繁CRC错误现象上位机波特率115200MCU端配的也是115200但升级过程中CRC错误率很高帧头经常对不齐重试多次才能成功一次。原因F103用内部RC或外部晶振配置出的波特率有偏差。常见是外部晶振频率不是标准的8MHz倍频后串口分频产生误差或者上位机和MCU两端的容差叠加长数据帧越到尾部越容易错位。解决首选确认晶振实际频率按精确值重新计算USART分频不要照抄Demo里的8MHz配置。协议里的帧头加长度字段能起到字节对齐作用就算一帧错位下一个0xA5会重新定位。更彻底的做法是波特率自适应上位机先发连续两个字节0x55、0xAAMCU测量这位宽反推出波特率再初始化USART。量产设备我一般默认开启这个机制能省掉很多产线通讯不上的排查时间。6. 进阶验证给IAP加自检与回滚升级成功率拉满Bootloader调通之后日常升级还有两块短板要补怎么证明固件真的有效写坏了怎么办。先说有效标志。在参数区第2章里的0x0801C000区域专门放4字节App区写完并且整片CRC通过后Bootloader把这个标志写成0xA5A5A5A5。每次上电Bootloader先检查两个条件App起始地址的栈顶指针是否合法、有效标志是否为0xA5A5A5A5两个都通过才跳转否则留在IAP等待升级。这能挡住只写了一半和整片校验失败两种状态比单纯检查栈顶指针更可靠。回滚的逻辑我一般这样设计上一版固件存在备份区新固件整片校验失败时Bootloader从备份区恢复App区内容再跳转。启动流程的骨架如下if (app_valid()) { jump_to_app(); } else if (backup_valid()) { restore_backup(); /* 从备份区复制到App区 */ jump_to_app(); } else { wait_for_upgrade(); /* 停IAP等上位机重新升级 */ }这套逻辑的代价是备份区要占一份App大小256KB的F103在App压缩到112KB时刚好够用Flash更大或App更小的型号值得上。说一个亲历的教训某批产线节点升级折腾到凌晨查到最后是上位机脚本CRC计算范围多算两个字节单包校验全过整片校验没过。当时Bootloader里只做了单包校验没有把整片CRC加有效标志加备份区这套加上结果几十片全是半成品固件。后来我写任何IAP都默认至少带整片校验和有效标志两层保险备份区看Flash余量再加。升级这个功能代码量看着不多每一层保险都在关键时刻替你把损失挡回去。希望帮到你。本文还有配套的精品资源点击获取