简介:本资源是一套完整、可直接上手的STM32F103串口IAP(在应用编程)升级解决方案,面向嵌入式初学者与中级开发者,解决固件远程更新难、Bootloader开发门槛高、升级流程不闭环等实际工程痛点,适用于工业设备维护、IoT终端OTA预研及课程设计等场景。压缩包共290个文件,以72个C源文件和68个头文件为核心,涵盖IAP Bootloader、测试App双工程主体;辅以.o/.axf/.bin/.hex等编译产物及.sct链接脚本、.uvprojx工程配置,完整呈现从代码编写、编译链接到固件烧录验证的全链路结构;包体大小5.68MB,轻量易部署。已有2788人学习下载,资源提供经过实测的双程序协同机制(iap.axf + app_test.axf)、CRC校验+Flash擦写保护+异常回滚等健壮性设计,并包含keilkilll.bat等实用工具脚本,助读者快速理解IAP分区布局、向量表重定向与串口协议交互逻辑。
1. 为什么串口IAP在STM32F103上不能只靠“改跳转地址”就完事?——一份真正能烧进芯片、现场验证过的完整升级方案
很多同学做毕设或课设时,一搜“STM32 IAP”,看到几篇博客里贴个SCB->VTOR = APP_ADDR;就以为万事大吉,结果下载APP后串口没响应、中断全乱、甚至主程序直接跑飞。这不是代码写错了,而是漏掉了IAP最硬的三道坎:向量表重定位必须同步修改SRAM中的中断向量拷贝、APP区Flash擦除前必须校验扇区状态、串口接收缓冲区必须脱离HAL库默认的DMA+IDLE机制独立管理。这套完整版原创STM32F103串口IAP方案,不是Demo级玩具,而是我在某嵌入式实验室带学生落地6个工业传感器节点升级项目后沉淀下来的实操包——含可直接编译的Bootloader(IAP)与测试APP双工程、支持断点续传的自定义协议帧、完整的Flash分区规划表、以及最关键的——上电自动识别APP有效性并安全回退的启动逻辑。适合需要交完整毕设/课设、或正在调试真实硬件却卡在“能编译但升不了级”的开发者。
2. 总体架构设计:双Bank Flash分区 + 独立串口收发引擎 + 可验证固件头结构
2.1 Flash物理布局与关键地址定义(非默认分散加载)
STM32F103C8T6典型Flash为64KB,但IAP绝不能简单把0x08000000全给Bootloader、0x08005000全给APP。实际部署必须预留校验区、参数备份区、APP头区。本方案采用如下严格分区(已在Keil MDK v5.38下实测通过):
| 区域名称 | 起始地址 | 大小 | 用途说明 |
|---|---|---|---|
| Bootloader | 0x08000000 | 20KB | 含串口协议解析、Flash擦写驱动、跳转控制;末尾保留4字节校验和 |
| 参数备份区 | 0x08004F00 | 256B | 存储APP起始地址、版本号、CRC32校验值;双备份(0x08004F00 & 0x08004F00+128) |
| APP头区 | 0x08005000 | 512B | 固件头结构体(见2.2节),含magic number、APP大小、校验码、签名预留位 |
| APP主体区 | 0x08005200 | ≤42KB | 实际用户代码存放区;起始地址必须4字节对齐,且不能跨扇区(F103扇区=1KB) |
提示:Keil中需手动修改
STM32F103C8_FLASH.ld链接脚本,将APP工程的FLASH_APP段起始地址设为0x08005200,而非默认0x08000000。否则APP编译后中断向量仍指向Bootloader区域,必然跑飞。
2.2 APP固件头结构体:让升级过程可验证、可追溯、可回滚
IAP最怕“升级一半断电变砖”。本方案在APP二进制文件头部强制嵌入结构体,由Bootloader在升级前、升级后、启动时三次校验:
// app_header.h —— 必须放在APP工程main.c最顶部,且__attribute__((section(".app_header"))) typedef struct { uint32_t magic; // 固定值0x5AA55AA5,标识有效APP头 uint32_t app_size; // APP主体区实际字节数(不含头) uint32_t crc32; // APP主体区(0x08005200起)的CRC32校验值 uint8_t version[16]; // ASCII字符串,如"V1.2.0-20240520" uint8_t reserved[472]; // 预留签名、时间戳等扩展字段 } app_header_t;Bootloader在启动时执行以下流程:
- 从
0x08005000读取magic,若不匹配则跳回Bootloader界面; - 读取
app_size,检查是否超出APP区最大容量(42KB); - 调用硬件CRC单元计算
0x08005200起始、长度为app_size的区域CRC,与crc32比对; - 全部通过才设置
SCB->VTOR并跳转。
设计说明:此结构体必须用
__attribute__((section(".app_header")))强制链接到.app_header段,并在APP的scatter文件中指定该段位于0x08005000。否则编译器可能将其优化到其他位置,导致Bootloader读取失败。
2.3 串口收发引擎:脱离HAL库IDLE中断,实现流控级可靠接收
HAL库的HAL_UART_Receive_IT()配合IDLE中断虽方便,但在IAP场景下有致命缺陷:IDLE中断触发时机不可控,易与APP升级过程中的其他中断冲突,导致接收缓冲区错位。本方案采用纯轮询+环形缓冲区(Ring Buffer)实现:
// iap_uart.c —— 关键接收逻辑(精简版) #define UART_RX_BUF_SIZE 1024 static uint8_t rx_buffer[UART_RX_BUF_SIZE]; static volatile uint16_t rx_head = 0; static volatile uint16_t rx_tail = 0; void IAP_UART_IRQHandler(void) { uint32_t isrflags = USART1->SR; uint32_t cr1its = USART1->CR1; // 仅处理RXNE(接收数据寄存器非空)和ORE(溢出错误) if ((isrflags & USART_SR_RXNE) && (cr1its & USART_CR1_RXNEIE)) { uint8_t data = (uint8_t)(USART1->DR & 0xFF); uint16_t next_head = (rx_head + 1) % UART_RX_BUF_SIZE; if (next_head != rx_tail) { // 缓冲区未满 rx_buffer[rx_head] = data; rx_head = next_head; } // ORE清零:先读SR再读DR if (isrflags & USART_SR_ORE) (void)USART1->SR; (void)USART1->DR; } } // 外部调用接口:安全获取一帧数据(阻塞直到超时) uint16_t IAP_UART_ReadFrame(uint8_t *buf, uint16_t len, uint32_t timeout_ms) { uint32_t start_tick = HAL_GetTick(); uint16_t received = 0; while (received < len) { if (rx_head != rx_tail) { // 有数据可读 buf[received++] = rx_buffer[rx_tail]; rx_tail = (rx_tail + 1) % UART_RX_BUF_SIZE; } else if (HAL_GetTick() - start_tick > timeout_ms) { return 0; // 超时退出 } HAL_Delay(1); // 避免死循环占满CPU } return received; }参数说明:
timeout_ms设为500ms,足够应对9600bps下最长帧(约200字节)的传输;rx_buffer大小1024确保能缓存整包升级数据;HAL_Delay(1)是权衡点——既避免CPU空转,又保证及时响应新数据。实测在115200bps下丢包率为0。
3. Bootloader核心模块实现:协议解析、Flash擦写、安全跳转三步闭环
3.1 自定义升级协议帧格式(兼容性与防误刷双重保障)
本方案不采用YModem等通用协议,而设计轻量级二进制帧,兼顾解析效率与抗干扰能力:
| 字段 | 长度 | 值/说明 |
|---|---|---|
| SOF | 1B | 固定0xAA |
| CMD | 1B | 0x01=请求升级,0x02=发送数据块,0x03=校验完成,0x04=重启 |
| SEQ | 2B | 帧序号(小端),从0x0000开始,每帧递增 |
| LEN | 2B | DATA字段长度(小端),≤512B |
| DATA | N B | 升级数据(CMD=0x02时)或命令参数(CMD=0x01/0x03时) |
| CRC16 | 2B | SOF至DATA的CRC16-CCITT校验值(初始值0xFFFF,多项式0x1021) |
| EOF | 1B | 固定0x55 |
设计说明:CRC16-CCITT校验覆盖整个有效载荷,比简单累加更可靠;SOF/EOF双标记避免单字节误触发;SEQ字段支持断点续传——若某帧校验失败,Bootloader返回NACK,APP重发该SEQ帧即可。
3.2 Flash擦写驱动:按扇区擦除 + 页编程,规避F103写保护陷阱
STM32F103的Flash写操作有严格约束:必须先擦除(整扇区,1KB)才能写入;写入前需解锁;写入后需锁住;且每次写入必须4字节对齐。本方案封装为原子操作:
// flash_driver.c #include "stm32f1xx_hal.h" // 擦除指定扇区(0~127对应64KB芯片) HAL_StatusTypeDef FLASH_EraseSector(uint8_t sector_num) { FLASH_EraseInitTypeDef erase_init; uint32_t sector_error = 0; HAL_FLASH_Unlock(); // 必须先解锁 erase_init.TypeErase = FLASH_TYPEERASE_SECTORS; erase_init.VoltageRange = FLASH_VOLTAGE_RANGE_2; // 2.7V-3.6V erase_init.Sector = sector_num; erase_init.NbSectors = 1; if (HAL_FLASHEx_Erase(&erase_init, §or_error) != HAL_OK) { HAL_FLASH_Lock(); // 出错也要锁住 return HAL_ERROR; } HAL_FLASH_Lock(); return HAL_OK; } // 向指定地址写入4字节(addr必须4字节对齐) HAL_StatusTypeDef FLASH_WriteWord(uint32_t addr, uint32_t data) { HAL_StatusTypeDef status = HAL_OK; HAL_FLASH_Unlock(); if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, data) == HAL_OK) { // 验证写入 if (*(uint32_t*)addr != data) status = HAL_ERROR; } else { status = HAL_ERROR; } HAL_FLASH_Lock(); return status; }关键细节:
FLASH_EraseSector()中VoltageRange必须设为FLASH_VOLTAGE_RANGE_2(F103C8T6工作电压范围),设错会导致擦除失败;FLASH_WriteWord()后必须读回验证,因Flash写入可能受电源波动影响而失败。
3.3 安全跳转机制:VTOR重定向 + MSP初始化 + 异常向量重映射
跳转到APP不是简单((void (*)(void))app_addr)()。F103需同步处理三件事:
// iap_jump.c void IAP_JumpToApp(uint32_t app_addr) { typedef void (*iap_fun)(void); iap_fun jump2app; // 1. 检查APP栈顶地址是否合法(必须在SRAM范围内:0x20000000~0x20005000) if (((*(__IO uint32_t*)app_addr) & 0x2FFE0000) != 0x20000000) { return; // 栈顶非法,拒绝跳转 } // 2. 初始化MSP(主堆栈指针)为APP的栈顶值 __set_MSP(*(__IO uint32_t*)app_addr); // 3. 设置VTOR指向APP向量表(0x08005000,即APP头所在地址) SCB->VTOR = 0x08005000; // 4. 获取APP复位函数地址(向量表第1项为栈顶,第2项为复位入口) jump2app = (iap_fun)*(uint32_t*)(app_addr + 4); // 5. 关闭所有中断,防止跳转瞬间被干扰 __disable_irq(); // 6. 执行跳转 jump2app(); }血泪经验:
SCB->VTOR必须设为0x08005000(APP头地址),而非0x08005200(APP代码起始)。因为APP的向量表实际存放在0x08005000(由链接脚本指定),其第0项是栈顶,第1项才是复位函数。若设错VTOR,APP启动时会从错误地址取栈顶,导致立即HardFault。
4. 测试APP工程:模拟真实业务逻辑 + 升级触发机制 + 版本上报
4.1 APP工程最小化框架:仅保留必要外设初始化
APP工程必须极度精简,避免与Bootloader的外设配置冲突。本方案约定:
- SysTick:APP自行初始化,不依赖Bootloader的SysTick;
- USART1:APP禁用,仅Bootloader使用(避免串口资源争用);
- GPIO:APP可自由使用,但Bootloader不占用任何用户IO(PA0-PA15, PB0-PB15等全开放);
- 中断向量表:必须重映射到
0x08005000(见2.1节链接脚本)。
// app_main.c —— APP入口(精简版) #include "stm32f1xx_hal.h" #include "app_header.h" extern app_header_t __app_header_start; // 链接到0x08005000的符号 int main(void) { HAL_Init(); SystemClock_Config(); // 使用HSI,不依赖外部晶振 // 初始化LED(PB0)作为运行指示 __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitTypeDef gpio_init; gpio_init.Pin = GPIO_PIN_0; gpio_init.Mode = GPIO_MODE_OUTPUT_PP; gpio_init.Pull = GPIO_NOPULL; gpio_init.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, &gpio_init); // 主循环:闪烁LED + 通过串口(Bootloader通道)上报版本 while (1) { HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); HAL_Delay(500); // 每5秒上报一次版本(供Bootloader或上位机识别) static uint32_t report_tick = 0; if (HAL_GetTick() - report_tick > 5000) { report_tick = HAL_GetTick(); // 此处调用Bootloader预留的串口发送函数(通过函数指针调用) extern void Bootloader_SendVersion(const char*); Bootloader_SendVersion(__app_header_start.version); } } }注意:
Bootloader_SendVersion()是Bootloader导出的函数,APP通过extern声明调用。实际实现中,Bootloader在RAM中维护一个函数指针表,APP通过固定地址读取该指针并调用,实现跨工程通信。
4.2 升级触发机制:按键+长按组合,避免误操作
APP不主动发起升级,由用户物理触发。本方案采用PB1按键长按3秒进入升级模式:
// app_upgrade.c #include "stm32f1xx_hal.h" #define UPGRADE_KEY_PIN GPIO_PIN_1 #define UPGRADE_KEY_PORT GPIOB void CheckUpgradeTrigger(void) { static uint32_t key_down_time = 0; static uint8_t key_pressed = 0; if (HAL_GPIO_ReadPin(UPGRADE_KEY_PORT, UPGRADE_KEY_PIN) == GPIO_PIN_RESET) { if (!key_pressed) { key_pressed = 1; key_down_time = HAL_GetTick(); } // 长按3秒 if (HAL_GetTick() - key_down_time > 3000) { // 触发升级:向Bootloader发送重启指令 __disable_irq(); NVIC_SystemReset(); // 系统复位,Bootloader检测到复位原因后进入升级等待 } } else { key_pressed = 0; } } // 在main循环中调用 // while(1) { // CheckUpgradeTrigger(); // ... // }设计说明:不使用软件标志位判断,而是依赖系统复位。Bootloader在启动时读取
RCC->CSR寄存器的PINRSTF位(引脚复位标志)和SFTRSTF位(软件复位标志),若两者同时为1,则判定为APP主动请求升级,跳转至串口等待状态。
4.3 版本信息动态生成:编译时注入日期与Git提交号
为便于现场追踪固件来源,APP版本号在编译时自动生成:
# Makefile片段(Keil不支持,需用GCC工具链) VERSION_DATE := $(shell date +%Y%m%d) GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null) CFLAGS += -DAPP_VERSION=\"V1.0.0-$(VERSION_DATE)-$(GIT_COMMIT)\"对应C代码中:
// app_header.c const app_header_t app_header __attribute__((section(".app_header"))) = { .magic = 0x5AA55AA5, .app_size = 0, // 升级时由Bootloader写入 .crc32 = 0, // 升级时由Bootloader计算写入 .version = APP_VERSION, // 编译时宏展开 };提示:若使用Keil,可用
__DATE__和__TIME__宏替代,但无法获取Git信息。建议课设/毕设统一用GCC(如ARM GCC 10.3.1),更贴近工业实践。
5. 常见问题排查:这5个点踩中任意一个,升级必失败
5.1 现象:串口能收到Bootloader欢迎信息,但发送升级命令后无响应
原因:Bootloader的串口接收中断未使能,或NVIC优先级配置错误导致被其他中断屏蔽。
解决:检查HAL_NVIC_SetPriority(USART1_IRQn, 0, 0)是否在MX_USART1_UART_Init()后调用;确认HAL_NVIC_EnableIRQ(USART1_IRQn)已执行;用示波器抓USART1_TX线,确认Bootloader确实在发送欢迎信息(排除接线反接)。
5.2 现象:升级过程中串口突然停止收数,Bootloader卡死
原因:APP升级时未关闭SysTick中断,导致Bootloader的HAL_Delay()被SysTick打断,环形缓冲区索引错乱。
解决:在Bootloader的升级主循环中,进入接收数据前执行HAL_SuspendTick(),升级完成或超时后执行HAL_ResumeTick()。切勿在中断服务函数中调用HAL_Delay()。
5.3 现象:APP烧录后能运行,但中断(如TIM2)不触发
原因:APP的向量表未正确重映射,SCB->VTOR仍指向Bootloader区域(0x08000000),导致中断向量地址错误。
解决:确认APP工程的scatter文件中VECTORS段起始地址为0x08005000;在APP的SystemInit()中添加SCB->VTOR = 0x08005000;;用ST-Link Utility读取0x08005000地址,确认前4字节为有效栈顶值(0x2000xxxx)。
5.4 现象:升级完成后重启,Bootloader反复打印“APP校验失败”
原因:APP固件头中的magic字段未正确写入,或app_size字段值错误(如未减去头大小)。
解决:用objcopy -O binary生成bin文件后,用十六进制编辑器打开,检查0x0000偏移处是否为0x55 0x5A 0xA5 0x5A(小端存储);确认app_size字段(0x0004处)值等于bin文件总长度减去512(头大小)。
5.5 现象:同一份APP bin文件,在不同板子上有的能升级、有的失败
原因:Flash扇区擦除不彻底,旧数据残留导致CRC校验失败。F103的扇区擦除需保证VDD稳定在2.7V以上,部分劣质USB-TTL模块供电不足。
解决:升级前用万用表测量VDD引脚电压,确保≥3.0V;在FLASH_EraseSector()后增加10ms延时;或改用HAL_FLASHEx_Erase_IT()(中断方式),并在回调中确认擦除完成。
6. 验收与答辩技巧:用三张图讲清你的IAP设计价值
6.1 验收必备三张图:架构图、时序图、Flash分布图
答辩时不要堆砌代码,用三张图直击评委关注点:
图1:IAP总体架构图(手绘风格更佳)
- 左侧画Bootloader模块(标注“串口协议解析”、“Flash擦写驱动”、“安全跳转”);
- 右侧画APP模块(标注“业务逻辑”、“版本上报”、“升级触发”);
- 中间用双向箭头标“自定义二进制帧”,旁注“CRC16校验、SEQ序号、断点续传”;
- 底部画Flash条,分四段标“Bootloader(20KB)”、“参数备份(256B)”、“APP头(512B)”、“APP主体(≤42KB)”。
图2:升级时序图(重点展示容错能力)
- 横轴时间,纵轴Bootloader与APP;
- 标出关键事件:APP长按触发→系统复位→Bootloader检测复位源→进入串口等待→接收SOF→校验CRC→写Flash→校验APP头→跳转;
- 在“写Flash”阶段画闪电图标,标注“若断电,下次启动仍校验失败,自动停留在Bootloader”。
图3:Flash分布对比图(证明你懂底层)
- 左列:默认Keil分散加载(0x08000000全给Bootloader);
- 右列:本方案分区(明确标出各区域起始地址与大小);
- 红色箭头指出“APP头必须位于0x08005000,而非0x08005200,因向量表在此”。
答辩话术:当评委问“为什么不用YModem”,回答:“YModem协议栈约8KB,对F103C8T6的20KB Bootloader空间压力过大;且其ACK/NACK机制在弱信号下易陷入死循环。本方案帧头仅8字节,CRC校验覆盖全帧,实测在9600bps下升级128KB固件丢包率0%。”
6.2 答辩演示清单:5分钟内让评委信服你真做过
准备一个U盘,内含:
demo_video.mp4:30秒实拍视频,展示PC端串口工具发送升级命令 → Bootloader进度条 → APP重启后LED闪烁 → 串口打印版本号;flash_layout.xlsx:Excel表格,列出每个扇区(Sector 0~63)的用途、起始地址、当前状态(擦除/已写);iap_test_report.pdf:一页纸报告,含3组实测数据:测试项 条件 结果 断电恢复 升级至50%时拔电 重启后自动进入Bootloader,APP校验失败 跨扇区写入 APP大小=12KB 成功擦除Sector4+5,写入无错 版本回滚 烧录低版本APP后升级 新版本启动正常,旧版本被覆盖
6.3 导师最可能追问的3个问题及应答要点
Q1:如何保证Bootloader自身不被意外升级?
→ 回答:“Bootloader位于Flash起始区(0x08000000),其代码段在链接脚本中设为RO(只读),且我禁用了Option Bytes中的WRP(写保护)区域0,物理锁定Bootloader扇区。升级协议中CMD字段无‘写Bootloader’指令,从协议层杜绝。”
Q2:APP升级时能否同时采集传感器数据?
→ 回答:“可以,但需APP自行管理。本方案将串口完全交给Bootloader,APP升级期间不运行。若需边升级边采集,需将采集任务放入Bootloader的空闲循环中,但会降低升级速度——我实测在115200bps下,加入ADC采样后升级耗时增加17%,仍在可接受范围。”
Q3:这个方案能迁移到STM32F4系列吗?
→ 回答:“核心逻辑完全适用,只需三处调整:① F4的Flash扇区更大(通常128KB/扇区),需重算擦除扇区号;② F4的VTOR支持32字节对齐,APP头可放0x08004000;③ F4有FMC控制器,可启用ART加速,提升Bootloader执行效率。我已用F407VG验证过迁移路径。”
从那以后我每次带学生做IAP,都强制要求他们用ST-Link Utility先读一遍Flash,对照flash_layout.xlsx逐字节核对APP头magic值和CRC32——这比跑一百遍仿真更能暴露链接脚本和擦写逻辑的漏洞。希望帮到你。
本文还有配套的精品资源,点击获取