☰
STM32串口IAP升级实战:Bootloader分区、YMODEM传输与避坑指南
2026/10/15 17:31:15 网站建设 项目流程

简介:本资源面向使用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占前若干KB,App从后面某个扇区开始。Bootloader永远不动,负责接收新固件、写入App区、校验、跳转。App收到升级指令后,往备份区写新固件或者直接由Bootloader接管串口,两种模式各有取舍。

常见分区方案有两种。第一种是Bootloader + App + 备份区,App运行时收到升级命令,先把新固件存到备份区,再重启让Bootloader搬运。这种方案App区可以做得大,但需要额外Flash空间。第二种是Bootloader + App,App收到命令后直接跳回Bootloader,由Bootloader通过串口接收新固件覆盖App区。第二种省Flash,但升级过程中断电就麻烦,得靠Bootloader里的重试机制兜底。STM32F103C8T6只有64KB,我一般推荐第二种,Bootloader给12KB,App给52KB,够大多数控制逻辑用了。

具体到地址划分:

区域起始地址大小用途
Bootloader0x0800000012KB串口接收、Flash擦写、跳转
App0x0800300052KB用户应用代码
配置参数区0x0800FC001KB升级标志、版本号、校验值

0x08003000这个偏移不是随便定的。STM32F103C8T6的Flash页大小是1KB,12KB正好是12页,对齐到页边界,擦除时不会误伤Bootloader。配置参数区放在最后一页,存升级请求标志和固件CRC,Bootloader启动时先读这一页判断要不要进入升级模式。

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页大小。这份源码包用的就是YMODEM,Bootloader里实现接收端,上位机发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改成0x08003000,Size改成0xD000(52KB)。同时在App的main函数开头调用NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x3000)。两个地方必须一致。

4.4 升级后App跑不起来,但读Flash内容是对的

现象:用ST-Link读Flash,App区的数据和bin文件完全一致,CRC校验也通过,但跳转后就是跑不起来。

原因:跳转前没有设置主堆栈指针,或者设置的栈顶地址不对。App的栈顶地址存在App起始地址的第一个字,如果这个值被意外修改,或者跳转时没有执行__set_MSP,App的栈就乱了。

解决:跳转前先读*(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只有64KB,App区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, timeout=1) 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校验加好,再联调上位机。升级功能不怕慢,就怕断电和误码,这两道防线守住了,现场基本不会出问题。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询