
简介本资源面向使用STM32F103C8T6进行产品开发与固件维护的嵌入式工程师提供一套可直接参考的串口IAP在线升级固件源码解决设备出厂后无法通过串口远程更新程序的问题适合具备一定STM32与C语言基础的开发者学习移植。压缩包共430个文件约2.23MB以86个h头文件与86个c源文件为核心配合48个s汇编、18个asm启动代码及24个lsl、18个ld链接脚本另有uvproj、uvopt等工程配置与bin、hex固件文件覆盖IAP引导与应用程序两部分的完整工程结构。目前已有5135人学习下载说明该方案在同类需求中具有较高参考价值。读者可从中获取Bootloader跳转逻辑、串口接收与固件写入流程、Flash分区规划及中断向量重映射等关键实现并借助配套工程文件快速搭建验证环境为自研IAP升级功能提供可复用的代码基础与排错思路。1. 串口IAP升级到底解决了什么问题去年帮一个做工业控制的朋友处理售后现场设备装在配电柜里每次改一行参数都要派工程师带着笔记本和ST-Link跑一趟差旅成本比固件本身还贵。后来换成串口IAP方案产线工人拿个USB转TTL模块插上三根线用上位机发个bin文件就完事。STM32F103C8T6这颗芯片虽然只有64KB Flash、20KB RAM但跑串口IAP绰绰有余关键是方案成熟、源码好找、移植成本低。这份源码包针对的就是这个场景不需要拆机、不需要仿真器通过UART把新固件写进Flash并跳转执行。适合做嵌入式产品固件迭代的开发者尤其是设备已经部署到现场、物理接触困难的场合。下面从原理到代码逐步拆解把Bootloader和App的分区、跳转、中断向量表这些容易翻车的地方讲透。2. Bootloader与App的分区设计Flash怎么切、向量表怎么挪2.1 为什么必须做分区STM32F103C8T6的Flash起始地址是0x08000000上电后CPU从这儿取第一条指令。如果只有一个App升级时就得自己擦自己擦到一半断电设备直接变砖。所以标准做法是切两块Bootloader占前若干KBApp从后面某个扇区开始。Bootloader永远不动负责接收新固件、写入App区、校验、跳转。App收到升级指令后往备份区写新固件或者直接由Bootloader接管串口两种模式各有取舍。常见分区方案有两种。第一种是Bootloader App 备份区App运行时收到升级命令先把新固件存到备份区再重启让Bootloader搬运。这种方案App区可以做得大但需要额外Flash空间。第二种是Bootloader AppApp收到命令后直接跳回Bootloader由Bootloader通过串口接收新固件覆盖App区。第二种省Flash但升级过程中断电就麻烦得靠Bootloader里的重试机制兜底。STM32F103C8T6只有64KB我一般推荐第二种Bootloader给12KBApp给52KB够大多数控制逻辑用了。具体到地址划分区域起始地址大小用途Bootloader0x0800000012KB串口接收、Flash擦写、跳转App0x0800300052KB用户应用代码配置参数区0x0800FC001KB升级标志、版本号、校验值0x08003000这个偏移不是随便定的。STM32F103C8T6的Flash页大小是1KB12KB正好是12页对齐到页边界擦除时不会误伤Bootloader。配置参数区放在最后一页存升级请求标志和固件CRCBootloader启动时先读这一页判断要不要进入升级模式。2.2 中断向量表的偏移App从0x08003000开始执行但中断向量表默认在0x08000000。如果不做重映射App里所有中断都会跳到Bootloader的向量表直接跑飞。解决办法是在App的main函数开头调用NVIC_SetVectorTable把向量表偏移到App的起始地址。// App的main函数开头必须在使能任何中断之前调用 int main(void) { // 向量表偏移 App起始地址 - Flash基地址 0x3000 NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x3000); // 之后才初始化时钟、外设、中断 SystemInit(); // ... 其他初始化 }NVIC_SetVectorTable的第二个参数是相对于0x08000000的偏移量单位是字节。0x08003000减去0x08000000等于0x3000所以传0x3000。这个值必须和Bootloader里定义的App起始地址一致改一处就得改另一处否则中断响应错乱。我见过有人把偏移写成0x3000但App实际烧在0x08004000结果串口接收中断死活进不去查了一下午才发现是地址对不上。2.3 Bootloader的跳转逻辑Bootloader完成固件接收和写入后需要跳转到App。跳转前要关中断、关外设、设置主堆栈指针然后取App的复位向量地址执行。// 跳转到App的完整流程 typedef void (*pFunction)(void); void JumpToApp(uint32_t appAddr) { pFunction jumpToApp; uint32_t stackPointer; // 检查App区第一个字栈顶地址是否合法 stackPointer *(volatile uint32_t*)appAddr; if ((stackPointer 0x2FFE0000) ! 0x20000000) { // 栈顶不在SRAM范围内说明App区没有有效固件 return; } // 关闭所有中断 __disable_irq(); // 关闭外设时钟串口、定时器等 RCC-APB1ENR 0; RCC-APB2ENR 0; // 设置主堆栈指针 __set_MSP(stackPointer); // 取复位向量地址App起始地址 4 jumpToApp (pFunction)(*(volatile uint32_t*)(appAddr 4)); // 跳转 jumpToApp(); }这段代码有几个关键点。第一检查栈顶地址是否在SRAM范围0x20000000起始这是判断App区是否有有效固件的最快方法。第二__disable_irq()之后要清掉所有外设时钟否则App初始化时可能因为外设状态残留而出问题。第三__set_MSP必须在取复位向量之前执行因为复位向量里的代码会用到栈。第四跳转前最好把串口、定时器这些外设的寄存器恢复到复位值不然App里重新初始化时可能遇到玄学问题。3. 串口接收与Flash写入YMODEM协议和页擦除的配合3.1 为什么选YMODEM而不是自定义协议串口传固件最怕丢包和误码。自定义协议要自己处理分包、重传、校验写起来麻烦还容易漏边界条件。YMODEM是现成的上位机工具多SecureCRT、Xshell、自己写的Qt程序都支持带CRC校验和重传机制128字节一包适合STM32的RAM和Flash页大小。这份源码包用的就是YMODEMBootloader里实现接收端上位机发bin文件。YMODEM的交互流程上位机先发文件名和大小Bootloader回ACK然后上位机发数据包每包128字节Bootloader校验后回ACK或NAK最后发EOT结束。Bootloader收到完整文件后计算CRC和上位机发来的比对一致才写入Flash。3.2 Flash擦除的页对齐问题STM32F103C8T6的Flash擦除最小单位是1KB页。YMODEM每包128字节8包才凑够1KB。如果每收一包就写Flash会出现跨页写的问题——写到页边界时下一页还没擦直接写会失败。正确做法是攒够1KB再擦一页、写一页。// Flash写入缓冲区1KB对齐 #define FLASH_PAGE_SIZE 1024 static uint8_t flashBuf[FLASH_PAGE_SIZE]; static uint32_t bufIndex 0; static uint32_t writeAddr APP_START_ADDR; // YMODEM每收到一包数据后调用 void Flash_WritePacket(uint8_t *data, uint16_t len) { uint16_t i; for (i 0; i len; i) { flashBuf[bufIndex] data[i]; // 攒够一页擦除并写入 if (bufIndex FLASH_PAGE_SIZE) { FLASH_ErasePage(writeAddr); FLASH_WritePage(writeAddr, flashBuf, FLASH_PAGE_SIZE); writeAddr FLASH_PAGE_SIZE; bufIndex 0; } } } // 文件接收完成后处理不足一页的剩余数据 void Flash_FlushRemain(void) { if (bufIndex 0) { FLASH_ErasePage(writeAddr); FLASH_WritePage(writeAddr, flashBuf, bufIndex); bufIndex 0; } }FLASH_ErasePage和FLASH_WritePage需要自己封装核心是操作FLASH-CR、FLASH-KEYR这些寄存器。擦除前要解锁Flash擦除后要等待BSY位清零。写入时按半字16位写因为STM32F103的Flash编程宽度是16位。如果传进来的数据长度是奇数最后一个字节要补0xFF凑成半字。3.3 升级标志与断电保护升级过程中断电是必须考虑的场景。如果App区擦了一半断电下次上电Bootloader直接跳转App跑不起来设备就变砖了。解决办法是在配置参数区存一个升级标志Bootloader启动时先读这个标志。// 配置参数区结构 typedef struct { uint32_t upgradeFlag; // 0x5A5A5A5A表示需要升级 uint32_t appSize; // App固件大小 uint32_t appCrc; // App固件CRC32 uint32_t version; // 版本号 } ConfigParam; // Bootloader启动时检查 void Bootloader_CheckUpgrade(void) { ConfigParam *cfg (ConfigParam*)CONFIG_ADDR; if (cfg-upgradeFlag 0x5A5A5A5A) { // 进入升级模式等待串口接收新固件 YMODEM_Receive(); // 接收完成后校验CRC if (CheckAppCrc(cfg-appCrc) 0) { // 校验通过清除升级标志跳转App cfg-upgradeFlag 0; FLASH_ErasePage(CONFIG_ADDR); FLASH_WritePage(CONFIG_ADDR, (uint8_t*)cfg, sizeof(ConfigParam)); JumpToApp(APP_START_ADDR); } else { // 校验失败保持升级标志等待重新发送 while (1); } } else { // 正常启动直接跳转App JumpToApp(APP_START_ADDR); } }升级标志的写入时机很关键。App收到升级命令后先把标志写成0x5A5A5A5A再软复位。Bootloader读到标志后进入升级模式接收完新固件、校验通过才清除标志。如果接收过程中断电标志还在下次上电继续进入升级模式不会跳转到已经损坏的App。这个机制相当于给升级过程上了保险代价是多用一页Flash。注意配置参数区的擦写次数有限STM32F103C8T6的Flash擦写寿命约1万次。升级标志不要频繁写只在真正需要升级时写一次升级完成后清一次。4. 避坑排查串口IAP最容易翻车的五个地方4.1 跳转后串口无输出但仿真器单步能跑现象Bootloader跳转到App后App的串口初始化代码执行了但PC收不到任何数据。用仿真器挂上去单步调试又能正常发送。原因跳转前没有关闭Bootloader里已经使能的外设时钟尤其是USART。App重新初始化USART时某些寄存器位处于非复位状态导致波特率分频计算错误或者发送使能位异常。解决在JumpToApp函数里跳转前执行RCC-APB1ENR 0; RCC-APB2ENR 0;关闭所有外设时钟。更彻底的做法是调用RCC_DeInit()但注意这个函数会重置时钟树如果Bootloader里改了时钟配置App里要重新配置。4.2 YMODEM传输到一半卡死上位机一直重发现象传输几十包后上位机显示超时重传Bootloader没有任何响应。原因Flash擦除或写入时没有等待BSY位清零或者擦除地址超出了App区范围触发了Flash错误标志。STM32F103的Flash操作是阻塞的擦一页约20ms写一页约10ms。如果在这期间串口来了新数据没有及时响应上位机就超时了。解决在Flash擦写函数里加超时检测操作完成后清除所有Flash错误标志FLASH-SR FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR。另外YMODEM接收时把串口中断优先级设高一点确保数据不丢。4.3 App能跳转但中断不响应现象App的main函数能跑串口轮询发送正常但定时器中断、串口接收中断进不去。原因中断向量表偏移没设对或者设了偏移但App的链接脚本里没有把向量表放到正确位置。Keil默认把向量表放在0x08000000如果App工程没有修改IROM1起始地址编译出来的向量表还是在最前面。解决在Keil的Target设置里把IROM1的Start改成0x08003000Size改成0xD00052KB。同时在App的main函数开头调用NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x3000)。两个地方必须一致。4.4 升级后App跑不起来但读Flash内容是对的现象用ST-Link读FlashApp区的数据和bin文件完全一致CRC校验也通过但跳转后就是跑不起来。原因跳转前没有设置主堆栈指针或者设置的栈顶地址不对。App的栈顶地址存在App起始地址的第一个字如果这个值被意外修改或者跳转时没有执行__set_MSPApp的栈就乱了。解决跳转前先读*(volatile uint32_t*)APP_START_ADDR检查是否在0x20000000~0x20005000范围内STM32F103C8T6的SRAM是20KB。不在范围内说明App区没有有效固件不要跳转。在范围内则执行__set_MSP再取复位向量。4.5 升级过程中断电重新上电后设备无响应现象升级到一半拔了电源重新上电后串口没反应LED也不亮。原因升级标志写入了但Bootloader的YMODEM接收没有超时退出机制一直死等串口数据。或者升级标志没写入Bootloader直接跳转到已经擦除的App区取到的栈顶地址非法跳转后跑飞。解决Bootloader里加超时机制等待YMODEM起始字符超过30秒就自动跳转App如果App区有有效固件或者进入死循环闪灯提示。升级标志的写入和清除要配对App发升级命令时写标志Bootloader校验通过后清标志。如果标志存在但App区CRC校验失败保持升级模式等待重新发送。5. 进阶技巧用CRC32校验和双备份提高升级可靠性5.1 CRC32校验的软件实现YMODEM自带的CRC16只能校验单包数据整包固件的完整性需要CRC32。STM32F103没有硬件CRC32外设F4以上才有得用软件查表法。这份源码包里带了一个CRC32表256个32位条目占1KB Flash计算64KB固件的CRC大概几十毫秒。// CRC32查表法多项式0xEDB88320 static const uint32_t crc32_table[256] { /* 省略具体数值 */ }; uint32_t CRC32_Calculate(uint8_t *data, uint32_t len) { uint32_t crc 0xFFFFFFFF; uint32_t i; for (i 0; i len; i) { crc crc32_table[(crc ^ data[i]) 0xFF] ^ (crc 8); } return crc ^ 0xFFFFFFFF; }上位机发完固件后把整包CRC32值通过YMODEM的结束帧传过来Bootloader接收完所有数据后计算App区的CRC32比对一致才清除升级标志。这样即使传输过程中有极低概率的未检出错误也能在最后一步拦住。5.2 双备份区的取舍如果Flash够用可以在App区后面再划一块同样大小的备份区。升级时先把新固件写到备份区校验通过后再搬运到App区。搬运过程中断电App区还是旧的下次上电重新搬运。这种方案最安全但STM32F103C8T6只有64KBApp区52KB备份区根本放不下。折中方案是只备份关键参数和版本号固件本身不备份。升级前把当前App的版本号和CRC存到配置区升级失败后Bootloader检测到App区CRC不匹配可以通过串口请求上位机重新发送。这个方案不需要额外Flash代价是升级失败后设备暂时不可用得等上位机重发。我一般会在Bootloader里加一个“安全模式”如果连续三次上电都检测到App区CRC错误就永久停留在Bootloader的YMODEM接收模式LED慢闪提示。这样现场人员看到闪灯就知道要重新升级不用拆机。5.3 上位机脚本的配合Bootloader写好后上位机发送端也得配套。用Python写一个基于pyserial的YMODEM发送脚本比SecureCRT灵活可以集成到产线测试工装里。import serial import time from ymodem import YMODEM def send_firmware(port, baudrate, filepath): ser serial.Serial(port, baudrate, timeout1) ymodem YMODEM(ser) # 发送文件名和大小 filesize os.path.getsize(filepath) ymodem.send_filename(os.path.basename(filepath), filesize) # 发送数据 with open(filepath, rb) as f: while True: data f.read(128) if not data: break ymodem.send_packet(data) # 发送结束帧和CRC32 crc32 calculate_crc32(filepath) ymodem.send_eot(crc32) ser.close()这个脚本的关键是send_eot要把CRC32值带过去Bootloader收到后做最终校验。产线工装里可以把这个脚本包一层GUI操作员选bin文件、点发送就行。从那以后我每次做IAP方案都会先在Bootloader里把超时机制和CRC校验加好再联调上位机。升级功能不怕慢就怕断电和误码这两道防线守住了现场基本不会出问题。希望帮到你。本文还有配套的精品资源点击获取