
简介本资源是一套面向嵌入式开发初学者与进阶工程师的STM32F103C8T6专用Bootloader完整实现方案聚焦固件安全启动、在线升级IAP及多通信接口烧录等核心需求。项目覆盖启动模式配置、Flash编程算法、UART/USB协议交互、CRC校验机制、双Bank升级流程等关键知识点适用于智能硬件OTA升级、工业设备远程维护等实际场景。压缩包共196个文件含32个C源码如stm32f10x_flash.c、stm32f10x_usart.c、33个头文件h、34个编译中间文件o/d及可执行镜像axf、hex、bin结构清晰便于理解Bootloader内存布局与链接脚本设计包体大小为4.82MB兼顾完整性与轻量性。目前已有4098人学习下载提供可直接编译运行的Keil工程含uvprojx、uvoptx、调试配置dbgconf、映射文件map及汇编列表lst助读者快速掌握从向量表重定向、中断接管到应用跳转的全流程实现细节。1. 为什么 STM32F103C8T6 的 Bootloader 不是“烧完就能用”的功能模块而是必须亲手重写的关键枢纽很多刚接触 STM32F103C8T6 最小系统板的开发者在完成第一个 LED 闪烁例程后会自然地想既然芯片支持 IAPIn-Application Programming那直接调用STM32F10x_StdPeriph_Driver里的FLASH_Unlock()和FLASH_ProgramWord()就能升级固件了——事实恰恰相反原厂标准库不提供 Bootloader 框架所有跳转逻辑、校验机制、通信协议、分区管理都必须从零实现。你手上的 Blue Pill 板STM32F103C8T6出厂时 Flash 0x08000000 地址存放的是用户 APP而 Bootloader 必须被强制烧录到 0x08000000 之前的固定起始地址如 0x08000000 → 0x08003000并接管复位向量表重映射Vector Table Remap。这决定了它不是“附加功能”而是整个固件生命周期的入口守门人它要验证 APP 校验和、响应串口/USB 升级指令、安全擦除旧区、写入新区、校验 CRC、最后跳转到 APP 入口。尤其在工业现场或远程设备中一次错误跳转或未校验的固件写入直接导致设备变砖。本文聚焦于基于标准外设库非 HAL的纯手工 Bootloader 实现覆盖启动流程、Flash 操作边界、RCC 时钟重配置、TIM 定时器超时控制、USART 协议解析等真实开发链路所有代码可直接用于 Keil MDK 5.37 或 IAR EWARM 8.50 环境。2. 从复位向量重映射到 APP 跳转Bootloader 启动流程的四层硬件级控制2.1 复位后执行路径与向量表重映射的物理约束STM32F103C8T6 上电复位后CPU 总是从 Flash 起始地址 0x08000000 取第一条指令即复位向量该地址必须存放 Bootloader 的 Reset_Handler。但用户 APP 默认也链接在 0x08000000因此必须将 Bootloader 固定放置在 Flash 前段例如 0x08000000–0x08002FFFAPP 放置在后续区域例如 0x08003000–0x0800FFFF。关键在于Bootloader 运行期间APP 的中断向量表仍位于其自身基址0x08003000而 CPU 默认从中断向量表偏移 0x00 处读取 MSP 初始值、0x04 处读取 Reset_Handler 地址。若不重映射APP 中断将无法响应。标准库提供NVIC_SetVectorTable()函数但其本质是操作SCB-VTOR寄存器Cortex-M3 内核寄存器而 F103 系列需配合SYSCFG-MEMRMP寄存器启用内存重映射Memory Remap才能使 VTOR 生效。常见误操作是仅调用NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x3000)却忽略RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_SYSCFG, ENABLE)和SYSCFG_MemoryRemapConfig(SYSCFG_MemoryRemap_Flash)导致 APP 启动后中断全失效。// Bootloader 启动后立即执行的向量表重映射必须在任何中断使能前完成 void Bootloader_VectTableRemap(void) { RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_SYSCFG, ENABLE); // 使能 SYSCFG 时钟 SYSCFG_MemoryRemapConfig(SYSCFG_MemoryRemap_Flash); // 启用 Flash 重映射模式 NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x3000); // 设置 APP 向量表偏移为 0x3000 字节12KB }提示0x3000是 APP 在 Flash 中的起始偏移单位字节不是地址。若 APP 链接地址为0x08003000则偏移 0x08003000 - 0x08000000 0x3000。此值必须与 Keil 工程中 Target 页的 IROM1 起始地址严格一致。2.2 RCC 时钟重配置Bootloader 与 APP 的独立时钟域管理Bootloader 通常使用内部 HSI8MHz快速启动以缩短升级等待时间而 APP 可能依赖外部 HSE8MHz 晶振配置为 72MHz 主频。若 Bootloader 不主动关闭 HSE 并重置 RCC 寄存器APP 启动时SystemInit()会因 HSE 已就绪而跳过初始化导致主频异常。正确做法是Bootloader 在跳转前强制将 RCC 寄存器恢复至复位默认值并关闭所有外设时钟。标准库无现成函数需手动操作// 清除 RCC 寄存器模拟复位状态 void RCC_DeInit(void) { RCC-CR 0x00000001; // HSI ON, 其他 OFF RCC-CFGR 0x00000000; // 默认配置 RCC-CIR 0x00000000; // 关闭所有中断 RCC-APB2RSTR 0x00000000; // 复位 APB2 外设 RCC-APB1RSTR 0x00000000; // 复位 APB1 外设 RCC-AHBENR 0x00000014; // 仅保留 SRAM/FLITF/CKL_EN必要基础 RCC-APB2ENR 0x00000000; // 关闭 APB2 时钟 RCC-APB1ENR 0x00000000; // 关闭 APB1 时钟 RCC-CR ~(RCC_CR_HSEON | RCC_CR_PLLON); // 强制关闭 HSE/PLL RCC-CFGR ~(RCC_CFGR_SW); // 清除系统时钟源选择 }2.2.1 时钟切换失败的典型现象与定位方法当 APP 启动后 LED 不闪烁、串口无输出首先检查RCC_GetSYSCLKSource()返回值是否为0x08HSI而非0x0CHSE。若返回0x08说明 APP 的SystemInit()未成功切换至 HSE根源往往是 Bootloader 未执行RCC_DeInit()或RCC-CR中 HSE 使能位残留。使用 J-Link Commander 连接后执行mem32 0x40021000RCC_BASE可直接读取 CR 寄存器值确认 bit16HSEON是否为 0。2.3 TIM 定时器作为 Bootloader 超时判断的核心机制Bootloader 必须在上电后等待一段可配置时间如 1.5 秒在此期间监听串口是否有升级指令超时则无条件跳转至 APP。使用SysTick不可靠因 SysTick 依赖系统时钟而时钟可能未稳定应选用独立时钟源的TIM2或TIM3。F103C8T6 的TIM2由 APB1 总线驱动默认预分频系数为 72-172MHz/721MHz计数周期设为 1.5 秒需装载值1500000// 初始化 TIM2 作为 Bootloader 超时定时器1.5s void Bootloader_TIM2_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1PERIPH_TIM2, ENABLE); TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 1499999; // 自动重装载值ARR TIM_TimeBaseStructure.TIM_Prescaler 71; // PSC71 → 72MHz/(711)1MHz TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 使能更新中断 TIM_Cmd(TIM2, ENABLE); } // TIM2 中断服务程序超时标志置位 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { g_BootloaderTimeoutFlag 1; // 全局超时标志 TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } }注意TIM_Period设为1499999是因为计数器从 0 开始满 1500000 次溢出才触发中断即 1.5 秒。若使用HAL库__HAL_TIM_SET_AUTORELOAD()参数需减 1但标准库TIM_TimeBaseStructure.TIM_Period直接填入目标值。3. 串口协议解析与 Flash 编程Bootloader 的核心数据通路与存储操作3.1 USART 接收缓冲区与命令帧解析的防错设计Bootloader 通过 USART1PA9/PA10接收升级指令常见协议为0xAA 0x55 CMD LEN DATA... CRC8。但实际部署中串口线干扰、MCU 供电波动会导致帧头丢失或数据错位。不能依赖单次USART_ReceiveData()读取必须实现环形缓冲区 状态机解析。标准库无环形缓冲区支持需自行定义#define RX_BUFFER_SIZE 256 typedef struct { uint8_t buffer[RX_BUFFER_SIZE]; uint16_t head; uint16_t tail; } RingBuffer; RingBuffer rx_buffer {0}; // USART1 中断接收必须开启 RXNE 中断 void USART1_IRQHandler(void) { uint8_t data; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { data USART_ReceiveData(USART1); // 环形缓冲区写入忽略溢出 uint16_t next_head (rx_buffer.head 1) % RX_BUFFER_SIZE; if (next_head ! rx_buffer.tail) { // 未满 rx_buffer.buffer[rx_buffer.head] data; rx_buffer.head next_head; } } } // 帧解析状态机在主循环中调用 uint8_t ParseFrame(uint8_t* frame, uint16_t* len) { static uint8_t state 0; static uint8_t expect_len 0; static uint8_t crc_calc 0; uint8_t byte; while (rx_buffer.head ! rx_buffer.tail) { byte rx_buffer.buffer[rx_buffer.tail]; rx_buffer.tail (rx_buffer.tail 1) % RX_BUFFER_SIZE; switch (state) { case 0: // 等待 0xAA if (byte 0xAA) state 1; break; case 1: // 等待 0x55 if (byte 0x55) state 2; else state 0; break; case 2: // 读取 CMD frame[0] byte; state 3; break; case 3: // 读取 LEN expect_len byte; *len expect_len; state 4; crc_calc 0xAA ^ 0x55 ^ byte; break; case 4: // 读取 DATA frame[state-3] byte; crc_calc ^ byte; if (--expect_len 0) state 5; break; case 5: // 读取 CRC if (crc_calc byte) { return 1; // 帧正确 } else { state 0; // 丢弃错误帧 } break; } } return 0; }3.1.1 串口波特率自适应的工程实践不同调试工具ST-Link Virtual COM、CH340、CP2102可能设置不同波特率。Bootloader 应支持 9600/115200 双速检测先以 115200 接收 50ms若无有效帧则切换至 9600 重试。实现时需在USART_Init()前配置USARTDIV并利用USART_GetFlagStatus(USART1, USART_FLAG_RXNE)轮询检测首字节到达时间。3.2 STM32F10x_FLASH 操作的三重校验与扇区擦除边界F103C8T6 的 Flash 分为 128 个 1KB 扇区0–127但APP 区域必须按扇区对齐擦除且每次擦除前必须验证目标地址是否属于合法扇区范围。标准库FLASH_ErasePage()仅接受 Page Address即扇区起始地址若传入0x08003000APP 起始地址实际擦除的是扇区 60x08001800–0x08001FFF而非 APP 所在扇区 120x08003000–0x08003FFF。正确做法是计算目标地址所属扇区号再转换为扇区起始地址// 根据地址获取对应扇区起始地址F103C8T6每扇区 1KB uint32_t GetSectorStartAddr(uint32_t addr) { uint32_t sector_num addr / 0x400; // 0x400 1024 return 0x08000000 (sector_num * 0x400); } // 安全擦除 APP 区域0x08003000–0x0800FFFF void SafeEraseAppArea(void) { uint32_t start_addr 0x08003000; uint32_t end_addr 0x0800FFFF; FLASH_Unlock(); // 必须先解锁 FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); for (uint32_t addr start_addr; addr end_addr; addr 0x400) { uint32_t sector_addr GetSectorStartAddr(addr); // 额外校验确保 sector_addr 在合法范围内0x08000000–0x0801FFFF if (sector_addr 0x08000000 || sector_addr 0x0801F000) continue; FLASH_Status status FLASH_ErasePage(sector_addr); if (status ! FLASH_COMPLETE) { // 记录错误日志停止擦除 while(1); } } FLASH_Lock(); }提示FLASH_ErasePage()返回FLASH_COMPLETE表示擦除成功但不保证扇区内容全为 0xFF。实际应用中需在擦除后逐字节读取验证尤其在高温或老化 Flash 上部分扇区可能擦除不彻底。3.3 APP 校验和计算与跳转前的最终确认跳转前必须验证 APP 的完整性否则可能执行损坏代码。常用方法是计算 APP 区域0x08003000–0x0800FFFF的 CRC32 或累加和。标准库无 CRC32 硬件加速F103 无 CRC 外设故采用查表法软件 CRC32// CRC32 查表法多项式 0xEDB88320 const uint32_t crc32_table[256] { 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, /* ... 256 项此处省略 */ }; uint32_t CalculateCRC32(uint32_t* data, uint32_t len) { uint32_t crc 0xFFFFFFFF; uint8_t* p (uint8_t*)data; for (uint32_t i 0; i len; i) { crc (crc 8) ^ crc32_table[(crc ^ p[i]) 0xFF]; } return crc ^ 0xFFFFFFFF; } // 跳转前校验 uint32_t app_crc CalculateCRC32((uint32_t*)0x08003000, 0xD000); // APP 区域长度 53248 字节 if (app_crc ! *(uint32_t*)0x08002FFC) { // 假设 CRC 存储在 APP 区末尾 4 字节 // 校验失败保持 Bootloader 循环 while(1); }4. Keil 工程配置与链接脚本让 Bootloader 和 APP 各自安放、互不越界4.1 启动文件修改与向量表重定位的汇编级控制Keil 默认使用startup_stm32f10x_md.s其中向量表定义在.isr_vector段。Bootloader 的向量表必须位于0x08000000而 APP 的向量表需重定位至0x08003000。不能仅靠NVIC_SetVectorTable()必须修改启动文件中的__Vectors符号地址。在 Bootloader 工程的startup_stm32f10x_md.s中将__Vectors DCD __initial_sp ; Top of Stack DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMI Handler ; ... 其余中断向量替换为; Bootloader 向量表固定在 0x08000000 AREA RESET, DATA, READONLY __Vectors DCD __initial_sp ; Top of Stack DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMI Handler ; ... 其余中断向量 ALIGN并在Options for Target → Linker → Use Memory Layout from Target Dialog取消勾选手动指定 Scatter 文件。4.2 Scatter 文件定义 Bootloader 与 APP 的 Flash 分区创建bootloader_scatter.sctLR_IROM1 0x08000000 0x00003000 { ; load region size 12KB ER_IROM1 0x08000000 0x00003000 { ; load address execution address *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00001000 { ; 4KB RAM .ANY (RW ZI) } } LR_IROM2 0x08003000 0x0000D000 { ; APP 区域12KB–64KB ER_IROM2 0x08003000 0x0000D000 { *(RO) } RW_IRAM2 0x20001000 0x00001000 { ; APP 使用剩余 RAM .ANY (RW ZI) } }注意0x0000D000 53248 字节即 APP 最大容量。若 APP 编译后超过此值Keil 链接器报错L6218E: Undefined symbol此时需增大该值或优化代码。4.3 APP 工程的启动代码修正跳过 SystemInit 中的 Flash 等待周期配置APP 的SystemInit()默认调用SetSysClockTo72()其中包含FLASH_SetLatency(FLASH_Latency_2)。但 Bootloader 已配置好 Flash 等待周期APP 若重复配置可能导致总线错误。解决方案在 APP 的system_stm32f10x.c中注释掉FLASH_SetLatency()调用或添加运行时判断// APP 的 SystemInit() 中修改 void SystemInit (void) { // ... 其他初始化 // FLASH_SetLatency(FLASH_Latency_2); // 注释掉此行 // ... 继续其他配置 }5. 实战排错J-Link 无法连接、APP 跳转后死机、Flash 擦除失败的三大高频问题5.1 J-Link 连接失败Bootloader 占用 SWDIO/SWCLK 引脚的硬件冲突F103C8T6 的 SWD 调试接口引脚为 PA13SWDIO和 PA14SWCLK。若 Bootloader 初始化了 GPIOA 时钟并重置了 PA13/PA14 为普通推挽输出则 J-Link 无法建立连接。根本解决方法是在 Bootloader 初始化代码最前端禁用 PA13/PA14 的复用功能// Bootloader main() 开头立即执行 void DisableSWDPins(void) { RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_13 | GPIO_Pin_14; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; // 浮空输入释放 SWD GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); }提示若已因引脚占用导致 J-Link 失联需短接 BOOT0 引脚至 3.3V上电后强制进入系统存储器启动模式System Memory再用 STM32 ST-LINK Utility 重新烧录 Bootloader。5.2 APP 跳转后死机MSP 初始值未从 APP 向量表正确加载跳转代码常见错误是直接((void (*)(void))app_jump_address)();但 Cortex-M3 要求先从 APP 向量表首地址0x08003000读取 MSP 初始值再设置__set_MSP()最后跳转。缺失 MSP 设置会导致堆栈指针指向非法地址执行第一条指令即 HardFault// 正确的 APP 跳转序列 typedef void (*pFunction)(void); pFunction Jump_To_Application; uint32_t JumpAddress; // 1. 从 APP 向量表读取 MSP 初始值 if (((*(__IO uint32_t*)0x08003000) 0x2FFE0000) 0x20000000) { // 校验 MSP 是否在 SRAM 范围内 JumpAddress *(__IO uint32_t*)(0x08003000 4); // 获取 Reset_Handler 地址 Jump_To_Application (pFunction)JumpAddress; __set_MSP(*(__IO uint32_t*)0x08003000); // 设置主堆栈指针 Jump_To_Application(); // 执行 APP }5.2.1 HardFault 定位技巧通过 SCB-CFSR 寄存器解码错误类型若跳转后进入 HardFault可在HardFault_Handler中读取SCB-CFSRvoid HardFault_Handler(void) { uint32_t cfsr SCB-CFSR; if (cfsr 0x00000080) { // IACCVIOL: 指令访问违规 // 可能原因JumpAddress 无效如 0x00000000 } if (cfsr 0x00000001) { // INSTR_BUS_ERR: 指令总线错误 // 可能原因APP 未正确烧录或 Flash 擦除不完整 } while(1); }5.3 Flash 擦除失败写保护位未清除或电压不足的底层信号FLASH_ErasePage()返回FLASH_TIMEOUT时常见原因是① Flash 写保护位WRP未清除② 电源电压低于 2.0VF103 最小工作电压。必须在擦除前调用FLASH_WRPPageConfig()解锁所有扇区// 解锁全部扇区F103C8T6扇区 0–127 void UnlockAllFlashSectors(void) { FLASH_WRPPageConfig(FLASH_WRPPage_0, DISABLE); FLASH_WRPPageConfig(FLASH_WRPPage_1, DISABLE); // ... 依次禁用扇区 0–127标准库宏定义到 FLASH_WRPPage_127 // 或使用循环for (uint16_t i 0; i 127; i) FLASH_WRPPageConfig(i, DISABLE); }同时用万用表测量 VDDA 引脚电压确保 ≥2.5V。若使用 LDO 供电注意其压差特性——输入 3.3V 时某些 LDO 输出可能仅 2.8V在高温下进一步跌落。6. 串口升级协议优化支持断点续传与双备份分区的轻量级增强方案6.1 断点续传机制通过 Flash 末尾存储升级进度标记标准 Bootloader 在升级中断后需重传全部固件耗时且不可靠。可在 Flash 最后一个扇区0x0801F000–0x0801FFFF预留 16 字节存储当前已接收字节数与校验和#define UPGRADE_STATUS_ADDR 0x0801F000 #pragma pack(1) typedef struct { uint32_t received_bytes; // 已接收字节数 uint32_t expected_crc; // 预期 CRC32 uint32_t timestamp; // 升级开始时间戳秒 uint8_t status_flag; // 0x01进行中0x02完成0x00空闲 } UpgradeStatus; UpgradeStatus* status_ptr (UpgradeStatus*)UPGRADE_STATUS_ADDR;升级指令0xAA 0x55 0x01 LEN后增加0x01表示“续传模式”Bootloader 读取status_ptr-received_bytes后跳过已接收部分从该偏移继续接收。此方案无需额外 EEPROM完全利用片上 Flash。6.2 双备份分区A/B的地址映射与切换逻辑为实现无缝升级可将 Flash 划分为 A 区0x08003000–0x08009FFF和 B 区0x0800A000–0x0800FFFF各 28KB。Bootloader 维护一个标志位存于 0x08002FF8指示下次启动加载 A 或 B#define ACTIVE_FLAG_ADDR 0x08002FF8 #define ACTIVE_A 0x55AA55AA #define ACTIVE_B 0xABBAABBA uint32_t GetActivePartition(void) { uint32_t flag *(uint32_t*)ACTIVE_FLAG_ADDR; if (flag ACTIVE_A) return 0x08003000; else if (flag ACTIVE_B) return 0x0800A000; else return 0x08003000; // 默认 A 区 } // 升级 B 区完成后写入标志并重启 *(uint32_t*)ACTIVE_FLAG_ADDR ACTIVE_B; FLASH_ProgramWord(ACTIVE_FLAG_ADDR, ACTIVE_B); NVIC_SystemReset();注意双分区需在 Scatter 文件中定义两个独立加载区域并确保 Bootloader 能识别当前激活分区的向量表位置。此方案将升级风险降至最低——即使新固件崩溃重启后仍可回退至旧版本。6.3 串口升级速率瓶颈突破启用 DMA 接收与校验并行化标准轮询或中断接收在 115200 波特率下CPU 占用率超 70%。启用 USART1 的 RX DMA通道 5可释放 CPUvoid USART1_DMA_Init(void) { RCC_AHBPeriphClockCmd(RCC_AHBPERIPH_DMA1, ENABLE); DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)rx_dma_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize RX_DMA_BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; // 循环模式避免溢出 DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel5, DMA_InitStructure); DMA_Cmd(DMA1_Channel5, ENABLE); USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); }DMA 接收期间CPU 可并发计算已接收数据的 CRC32实现“接收-校验”流水线将升级吞吐量提升至理论极限115200/10 ≈ 11.5KB/s。本文还有配套的精品资源点击获取