STM32F103 Bootloader实战:串口升级与Flash跳转 简介这份资源面向嵌入式开发初学者与需要深入理解STM32F103启动机制的工程师围绕ARM Cortex-M3内核的Bootloader设计展开帮助解决上电初始化、应用程序加载与固件更新等核心问题。压缩包共450个文件约5.12MB以C源码、头文件与编译中间产物为主包含76个c、83个h、80个o文件另有汇编启动文件、链接脚本、HEX与BIN固件、DFU升级相关文件及工程配置文档覆盖从源码到可烧录镜像的完整链路。资源内容涉及BOOT引脚启动模式选择、Flash与SRAM启动流程、时钟与GPIO初始化、串口/USB固件更新协议、Flash擦写校验以及JTAG/SWD调试思路并包含STM3210B-EVAL、STM3210E-EVAL等评估板工程示例。目前已有1196人学习下载适合希望掌握Bootloader框架搭建、加载协议编写与安全校验机制的读者参考实践。1. 从 bootloader.zip 说起STM32F103 为什么需要自己写一段启动代码你手上有一块 STM32F103 最小系统板焊好了 5V 转 3.3V 电路Keil MDK 工程能编译能下载程序跑起来了。但某天产品要出货发现固件升级得把板子拆下来接 ST-Link产线工人操作一次要两分钟还容易插反烧掉引脚。这时候你搜到了「stm32F103单片机启动代码bootloader.zip」——它要解决的就是这件事让单片机自己能接收新固件、自己擦写自己、自己跳转不用仿真器。Bootloader 本质是一段先于用户程序运行的代码负责判断升级条件、接收数据、写入 Flash 指定区域最后把控制权交给应用程序。STM32F103 的 Flash 从 0x08000000 开始Cortex-M3 内核上电后从 0x08000000 取栈顶地址、从 0x08000004 取复位向量所以只要把 bootloader 放在起始地址应用程序偏移到后面某个扇区就能实现「一段代码引导另一段代码」。这套方案适合做单片机小车测速、毕业设计、工业采集板这类需要现场升级的场景也适合想搞懂启动流程的初学者——毕竟 STM32F103 系列参考手册RM0008里关于 Flash 和中断向量表的章节光看理论很难记住自己写一遍 bootloader 就全通了。2. 启动代码的底层逻辑从复位向量到 Flash 扇区划分2.1 Cortex-M3 的启动序列与中断向量表重映射STM32F103 上电或复位后内核做三件事从 0x08000000 读取第一个字作为主栈指针 MSP 的初始值从 0x08000004 读取第二个字作为复位向量跳转执行。这两个地址属于 bootloader 区域所以 bootloader 必须先运行。当 bootloader 决定跳转到应用程序时应用程序的向量表默认还在 0x08000000但它的代码实际在 0x08004000 或更后面中断发生时内核仍然去 0x08000000 找中断服务函数结果跳进 bootloader 的向量表程序跑飞。解决办法是重映射向量表。Cortex-M3 提供了一个寄存器叫 SCB-VTORVector Table Offset Register地址 0xE000ED08。应用程序在 main 函数开头把 VTOR 设成自己的起始地址中断就能正确响应。常见做法是在 system_stm32f10x.c 里改 VECT_TAB_OFFSET 宏或者在 main 里直接写 SCB-VTOR 0x08004000。我一般会在 bootloader 跳转前关掉所有中断、清除挂起位跳转后再由应用程序重新开中断避免跳转瞬间有中断 pending 导致硬件错误。2.2 Flash 扇区划分与空间分配策略STM32F103 的 Flash 按页擦除中容量产品如 C8T6、R8T6每页 1KB大容量产品如 ZET6每页 2KB。bootloader 本身占用的空间取决于功能复杂度只做串口升级10KB 以内够用带 CRC 校验、双备份、YModem 协议可能到 20KB。我一般把 bootloader 放在 0x08000000 ~ 0x08003FFF16KB应用程序从 0x08004000 开始剩余空间给应用程序和参数存储。参数区用来存升级标志、固件长度、CRC 值。常见做法是在 Flash 最后一页划出 1KB 做参数区比如 0x0801FC00C8T6 的 64KB 末尾。升级标志可以是一个魔数比如 0x5A5A5A5A 表示「需要升级」0x00000000 表示「正常运行」。bootloader 启动后先读这个标志决定是进升级模式还是直接跳转。这里有个细节擦除参数页会连带擦掉整页所以写参数前要先读出整页内容到 RAM修改后再整页写回不能只写单个字。2.3 跳转前的现场清理与 MSP 设置跳转函数看起来简单但翻车点不少。下面是我常用的跳转代码typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { pFunction jump; uint32_t stack_top; // 检查栈顶地址是否合法必须在 SRAM 范围内 stack_top *(volatile uint32_t *)app_addr; if ((stack_top 0x2FFE0000) ! 0x20000000) { return; // 栈顶不合法不跳转 } // 关闭全局中断清除所有挂起位 __disable_irq(); for (int i 0; i 8; i) { NVIC-ICER[i] 0xFFFFFFFF; NVIC-ICPR[i] 0xFFFFFFFF; } // 设置主栈指针 __set_MSP(stack_top); // 取复位向量并跳转 jump (pFunction)(*(volatile uint32_t *)(app_addr 4)); jump(); }逻辑说明先读应用程序起始地址的第一个字判断它是否落在 SRAM 的 0x20000000 区域。STM32F103 的 SRAM 从 0x20000000 开始C8T6 有 20KB所以栈顶一般在 0x20005000 附近。如果读到 0xFFFFFFFF 或 0x00000000说明应用程序区是空的跳过去必死。关闭中断和清除挂起位是为了防止跳转后 bootloader 的中断还挂着。设置 MSP 是因为应用程序有自己的栈空间不设置的话用的还是 bootloader 的栈可能越界。最后从 app_addr 4 取复位向量执行。参数说明app_addr 是应用程序起始地址必须与 Keil 工程里 IROM1 的起始地址一致。栈顶检查的掩码 0x2FFE0000 是常见做法能覆盖 0x20000000 ~ 0x2001FFFF 范围对于 20KB SRAM 的 C8T6 够用。如果用的是 R8T664KB SRAM掩码要相应放宽。3. 动手实现串口升级协议与 Flash 写入流程3.1 最小可用 bootloader 的工程配置在 Keil MDK 里新建工程选 STM32F103C8不加载标准外设库的启动文件那个启动文件是给应用程序用的bootloader 自己写向量表。我一般用寄存器操作减少依赖。工程设置里 IROM1 起始 0x08000000大小 0x400016KBIRAM1 起始 0x20000000大小 0x500020KB。输出勾选「Create HEX File」方便用 ST-Link Utility 烧录。时钟配置不用太复杂bootloader 阶段用内部 8MHz RC 振荡器就够了串口波特率 115200 能跑。等跳转到应用程序后应用程序再重新配置时钟树。这样做的原因是 bootloader 越简单越可靠少一个外部晶振就少一个故障点。串口用 USART1PA9 发 PA10 收接 CH340 模块就能和电脑通信。3.2 YModem 协议接收固件并写入 FlashYModem 是串口升级里最常用的协议支持文件名和文件长度传输带 CRC16 校验。STM32 端实现 YModem 接收核心是状态机等待起始帧 C接收数据包校验写 Flash回 ACK直到收到 EOT。下面是一个简化的接收循环#define SOH 0x01 #define STX 0x02 #define EOT 0x04 #define ACK 0x06 #define NAK 0x15 #define CAN 0x18 uint8_t rx_buf[1024]; void ymodem_receive(uint32_t flash_addr) { uint8_t packet_num 0; uint32_t offset 0; uint8_t *data; uint16_t len; // 发送 C 请求发送方开始 uart_send_byte(C); while (1) { uint8_t header uart_recv_byte(); if (header EOT) { uart_send_byte(ACK); break; } if (header ! SOH header ! STX) { uart_send_byte(NAK); continue; } len (header SOH) ? 128 : 1024; uart_recv_buf(rx_buf, len 4); // 数据 包号 包号反码 CRC16 // 校验包号 if (rx_buf[0] ! packet_num || rx_buf[1] ! (uint8_t)(~packet_num)) { uart_send_byte(NAK); continue; } // 校验 CRC16这里省略具体计算实际要算 if (!crc16_check(rx_buf[2], len, rx_buf[len 2] 8 | rx_buf[len 3])) { uart_send_byte(NAK); continue; } // 写 Flash data rx_buf[2]; flash_write(flash_addr offset, data, len); offset len; packet_num; uart_send_byte(ACK); } }逻辑说明YModem 每包数据前面有 3 字节头SOH/STX 包号 包号反码后面跟 2 字节 CRC16。SOH 表示 128 字节包STX 表示 1024 字节包。收到 EOT 表示传输结束回 ACK 后发送方可能还会发一个空包表示文件名和长度这里简化处理直接退出。写 Flash 前要确保目标扇区已擦除STM32F103 的 Flash 写入必须先擦后写擦除单位是页。参数说明flash_addr 是应用程序起始地址比如 0x08004000。offset 累加写入位置。crc16_check 用标准 CRC16-CCITT 多项式 0x1021初始值 0x0000。实际项目中我会在接收完所有数据后再做一次整包 CRC 校验和发送方比对防止传输过程中有包被漏掉但 ACK 丢了导致重复写。3.3 Flash 擦除与写入的时序要求STM32F103 的 Flash 操作有严格的时序擦除一页典型时间 20~40ms写入一个字 40~70us。操作期间 CPU 会暂停取指如果代码存在 Flash 里执行擦写函数时不能从同一块 Flash 取指令。标准做法是把擦写函数放到 RAM 里执行或者用「页擦除 快速编程」模式。我一般用固件库的 FLASH_ErasePage 和 FLASH_ProgramWord它们内部已经处理了等待周期。关键是要在擦写前解锁 FlashFLASH_Unlock操作完再上锁FLASH_Lock否则会触发硬件错误。另一个坑是擦除时不能跨页。比如应用程序从 0x08004000 开始长度 30KB那要擦除 0x08004000 ~ 0x0800B7FF 共 30 页。如果只擦了一部分写入时遇到未擦除的区域会失败FLASH_SR 寄存器的 PGERR 位会置 1。我习惯在擦除后读回校验全 0xFF 才继续写。4. 避坑与排查串口升级翻车的五个真实场景4.1 跳转后程序不跑示波器看 PA9 无输出现象bootloader 打印「Jumping to app」后应用程序没有任何反应串口无数据LED 不亮。原因通常是应用程序的向量表没重映射中断触发后跳回 bootloader 的向量表而 bootloader 的中断服务函数是空的程序卡死。解决在应用程序 main 函数第一行加SCB-VTOR 0x08004000;或者在 system_stm32f10x.c 里把 VECT_TAB_OFFSET 改成 0x4000。注意改完要重新编译应用程序只改 bootloader 没用。4.2 YModem 传输到一半卡死发送方超时现象SecureCRT 或 Xshell 发送固件进度条走到 30% 停住然后超时断开。原因可能是 Flash 擦除时间过长导致串口接收溢出。STM32F103 擦一页 1KB 要 20~40ms这期间如果发送方还在发数据串口硬件缓冲区只有 1 字节必然丢包。解决在擦除前先发 NAK 或延迟 ACK让发送方等待或者用双缓冲接收一包的同时擦下一包的区域。我一般会在 YModem 实现里每收 10 包就多发一个 C让发送方降速。4.3 升级后应用程序能跑但串口中断不响应现象跳转后主循环正常LED 闪烁但串口接收中断进不去。原因通常是 bootloader 里开了串口中断跳转前没有关闭NVIC 的挂起位还置着。解决跳转函数里加NVIC-ICER[0] 0xFFFFFFFF; NVIC-ICPR[0] 0xFFFFFFFF;把 USART1 的中断使能和挂起都清掉。应用程序初始化时再重新配置 NVIC。这个坑很隐蔽因为主循环不依赖中断看起来程序是好的。4.4 写入 Flash 返回成功但读出来是 0xFF现象FLASH_ProgramWord 返回 FLASH_COMPLETE但用 ST-Link Utility 读回发现目标地址还是 0xFF。原因是没有擦除就写入。STM32F103 的 Flash 只能把 1 写成 0不能把 0 写成 1未擦除的区域是 0xFF如果直接写 0x00硬件会拒绝。解决每次写入前先擦除对应页擦除后读回确认全 0xFF。另外注意擦除操作会整页擦掉如果参数区和应用程序在同一页擦应用程序会把参数也擦掉所以参数区要单独放一页。4.5 用 CubeProgrammer 通过 CH340 下载失败现象用 STM32CubeProgrammer 选 UART 模式接 CH340 模块点连接提示「No response」。原因可能是 BOOT0 引脚没拉高或者串口线 TX/RX 接反。STM32F103 进系统存储器启动模式需要 BOOT01、BOOT10上电复位后才走内置 bootloader。解决检查 BOOT0 跳线确认 CH340 的 TX 接 PA10、RX 接 PA9波特率选 115200 甚至 9600 试。如果还是不行用示波器看 PA9 有没有波形没有就是 CH340 驱动或线的问题。5. 进阶技巧双备份升级与 CRC 校验的工程化落地5.1 双备份升级的状态机设计单备份升级有个致命问题升级过程中断电应用程序区被擦了一半设备变砖。双备份的做法是在 Flash 里划两个应用程序区App_A 从 0x08004000 开始App_B 从 0x08010000 开始。bootloader 维护一个「当前运行区」标志和「升级目标区」标志。升级时把新固件写到非运行区写完后校验 CRC通过则改标志跳转到新区旧区保留作为回滚备份。如果新区启动失败比如看门狗复位三次bootloader 自动切回旧区。状态机用参数区的几个字节实现active_flag0 表示 A 区1 表示 B 区、upgrade_flag0x5A 表示需要升级、fw_size、fw_crc。bootloader 启动流程读参数区 → 如果 upgrade_flag 有效校验目标区 CRC → 通过则更新 active_flag、清除 upgrade_flag → 跳转到 active 区。这个方案多占一倍的 Flash 空间C8T6 的 64KB 只够放两个 24KB 的应用程序适合功能不复杂的场景。5.2 CRC32 校验的快速实现与比对策略CRC 校验是升级可靠性的最后一道防线。STM32F103 没有硬件 CRC 外设F4 以上才有只能用软件算。我一般用查表法算 CRC32多项式 0xEDB88320初始值 0xFFFFFFFF结果取反。下面是一个精简实现static const uint32_t crc32_table[16] { 0x00000000, 0x1DB71064, 0x3B6E20C8, 0x26D930AC, 0x76DC4190, 0x6B6B51F4, 0x4DB26158, 0x5005713C, 0xEDB88320, 0xF00F9344, 0xD6D6A3E8, 0xCB61B38C, 0x9B64C2B0, 0x86D3D2D4, 0xA00AE278, 0xBDBDF21C }; uint32_t crc32_calc(uint8_t *data, uint32_t len) { uint32_t crc 0xFFFFFFFF; for (uint32_t i 0; i len; i) { crc ^ data[i]; crc (crc 4) ^ crc32_table[crc 0x0F]; crc (crc 4) ^ crc32_table[crc 0x0F]; } return ~crc; }逻辑说明每次取一个字节异或到 CRC 低 8 位然后分两次查 4 位表每次右移 4 位。这个实现比逐位算法快 8 倍左右在 72MHz 的 STM32F103 上算 30KB 数据大约 10ms。参数说明crc32_table 是 16 项 4 位表占用 64 字节 Flash。算完后取反是标准 CRC32 的输出。校验时发送方把 CRC 值放在固件末尾或单独发一包接收方算完比对不一致就拒绝跳转并置错误标志。5.3 用 RTC 备份寄存器存升级标志的可靠性对比参数区放 Flash 里有个问题擦写次数有限1 万次左右频繁升级会磨损。STM32F103 有 10 个 16 位备份寄存器BKP_DR1 ~ BKP_DR10由 VBAT 供电主电源掉电后数据不丢。我一般用 BKP_DR1 存升级标志BKP_DR2 存固件长度BKP_DR3 存 CRC 低 16 位BKP_DR4 存 CRC 高 16 位。写备份寄存器前要先开 PWR 和 BKP 时钟解除备份域写保护。这个方案的好处是不占 Flash 页擦写次数几乎无限缺点是 VBAT 没接电池的话主电源掉电数据就丢了所以适合升级过程中不断电的场景。对比下来Flash 参数区适合存长期配置备份寄存器适合存临时升级标志。我现在的习惯是两者都用升级标志放 BKP固件信息放 Flash 参数区互为备份。最后说一个血泪教训每次改完 bootloader 的 Flash 划分一定要同步改应用程序的 Keil 工程 IROM1 起始地址和 VECT_TAB_OFFSET这三个地方不一致程序必翻车。希望帮到你。本文还有配套的精品资源点击获取