
不管你是做小仪表、电机驱动还是工控板stm32f103的Bootloader IAP串口升级这套东西迟早都会遇到。很多项目才用F103这种小资源单片机外壳一盖上、设备往现场一装再想用ST-Link烧程序基本就是拆机的节奏。更现实的需求是客户发来一个固件要你远程或者现场串口升级这时候Bootloader IAP串口升级就是唯一可行方案。而串口协议这块YMODEM协议几乎是最好的选择——它比XMODEM多了文件名和文件大小支持1024大块传输还有CRC校验够稳也够快。这篇就是把我做这类IAP升级的完整经验整理出来从分区规划、Bootloader写法、YMODEM状态机、跳转隐患到实测配置一次讲透。适合正在做F103固件升级、或者手头有产品要加在线升级能力的人参考哪怕你没接触过IAP顺着思路也能把一个能用的方案搭起来。1. 为什么F103的升级方案里一定要有Bootloader从拆壳刷机说起1.1 IAP的基本分工Loader负责收App负责跑IAP的全称是In-Application Programming翻译过来就是“在应用运行中编程”。这句话听起来有点绕其实拆开看就很简单在单片机的Flash里放两个部分——开头放一个独立的小程序叫Bootloader后面的地址空间放真正的应用叫用户App。单片机一上电先跑的是Bootloader它检查一下“要不要升级”如果要就通过串口接收新固件写成App如果不要就直接跳转到App的入口让App接管。这个结构本质上就像是电脑的BIOS和操作系统的关系。BIOS先起来它决定是进系统还是进恢复模式刷机。单片机里的Bootloader就是精简版的BIOS。你可能会问“我直接把程序烧在0x08000000不就好了为什么要多跳这么一下”关键就在这里没有Bootloader你只能靠烧录器烧想从App自己给自己换血是不可能实现的。因为App运行的时候程序计数器也就是PC指针一直指向App所在的Flash区域App自己擦掉自己所在的Flash这事儿在硬件上执行得太勉强而且一旦中途错乱设备直接变砖。而Bootloader独立于App它只负责“收包、写Flash、跳转”这三件事。哪怕App区已经写得乱七八糟Bootloader依旧在正常跑你依旧有办法重新拉一个固件进去。这是IAP方案最核心的价值——它能给产品一条“后路”。1.2 ISP、SWD与IAP三种升级方式的取舍很多刚开始接触固件升级的人会混淆ISP和IAP。ISP是系统内编程靠的是STM32出厂时固化的ROM引导程序把BOOT0引脚拉高复位后芯片进入ISP模式然后用串口配合上位机软件把程序烧进去。IAP则是我们自己写一个引导程序主动权完全掌握在开发者手里不需要依赖芯片出厂固件也不需要拨BOOT0引脚。SWD或JTAG则是通过调试器直接烧写好用是好用但必须有实体调试接口而且设备一旦封装、安装到现场这路基本就断掉了。三种方式的对比我用一个表列出来升级方式是否需要拆外壳能否远程/现场升级能否定制流程适合场景SWD/JTAG调试器需要不能不能开发调试、量产烧录ISP串口需要BOOT0得拉高很难不能第一次烧少量应急BootloaderIAP不需要能完全可控产品量产后的固件维护我个人的建议是开发阶段用SWD量产烧录阶段可以让产线用SWD或者ISP都行但产品一旦装好外壳、有了实际用户就必须给Bootloader让路。这也是很多成熟产品出厂前就把Bootloader固化进去的原因——宁可前期多点一点功夫也别后期让工程师提着螺丝刀去现场拆壳子。1.3 Flash分区怎么划以F103C8的64KB为例Flash规划是一切IAP方案的地基地基没打对后面全是坑。STM32F103的主Flash从0x08000000开始。我用的比较多的F103C8T6Flash总容量是64KB也就是地址范围到0x0800FFFF。常见的分法是把Bootloader放在最前面的16KB也就是0x08000000到0x08003FFFApp从0x08004000开始占剩下的48KB。为什么Bootloader要放在最前面因为MCU上电后默认从Flash起始地址取向量表、取复位入口。向量表必须放在0x08000000而Bootloader的“重置向量”就在这个位置所以Bootloader只能放最前。那Bootloader给多大合适一个功能完整的串口YMODEM升级Bootloader包含串口驱动、Flash驱动、YMODEM协议解析、必要的打印信息用标准外设库编译下来通常在6KB~10KB左右。留16KB已经算很宽裕再大就挤占App空间了。如果是更大容量的F103比如256KB的F103RCT6或者512KB的F103ZET6分区就更从容了。可以划32KB给BootloaderApp直接用剩下来的。更激进的方案还可以划一个中间“暂存区”出来先下载到暂存区、校验通过后再搬到App区这样升级失败还能继续跑旧App。F103C8这种64KB容量的芯片就玩不了双区因为地方太小。这种情况下重点应该放在把YMODEM协议和强制的升级入口做扎实让Bootloader“永远有入口、随时能重刷”这也算一种务实的最小化容灾设计。对了Flash是按页管理的F103的页大小是1KB也就是一次擦除最少擦1KB。分区的时候尽量让App起始地址对齐到1KB后面擦写会顺很多。0x08004000这个地址天然就是1KB对齐的不用额外操心。2. Bootloader里的核心动作串口收发、YMODEM帧解析与Flash写入2.1 串口初始化和“要不要升级”的判定Bootloader的代码量不大但结构要比App更严密。一般我写Bootloader会分这么几个文件系统时钟初始化、串口驱动、Flash驱动、YMODEM协议解析、主流程。串口用的最多的就是USART1因为它的引脚PA9、PA10好外接很多USB转TTL模块默认也是这个脚位。波特率选多少我一般默认115200、8位数据位、1位停止位、无校验。这个组合兼容性最好也是上位机软件最常见的默认配置。如果你以后要用自制脚本批量升级115200是底线大多数USB转串口的模块在115200下都很稳定。波特率再往上比如给F103上256000甚至460800代码本身没问题但USB串口模块的质量、连接线长短都会影响稳定性不是特别必要就别追求太高。串口初始化本身没什么稀奇的关键是“升级判定”这部分——怎么知道上电后是升级还是直接跳App我首推的方案是“按键优先 标志位配合”。Bootloader上电后先检查一个按键比如PA0接地按键按住了就无条件进入升级模式。然后检查备份寄存器里有没有App写入的升级标志位如果有也进入升级模式。两个条件都没有就去检查App区是否已有合法程序有就跳转没有就停留在Bootloader等待串口升级。这种设计的好处是在生产第一次烧录时Flash里没有AppBootloader不会跳出只会等升级产品运行中App需要升级时可以自己写一个升级标志再软复位然后自动进入Bootloader升级万一App写崩了、标志位也没来得及写现场工程师按一下按键强制升级照样能救回来。三层路口全留好了基本不会把自己困死。2.2 YMODEM帧结构拆开来看YMODEM不是STM32的什么协议它是一个通用的串口文件传输协议脱胎于XMODEM。XMODEM因为只能传128字节一帧速度慢也没有文件名信息所以YMODEM做了改进支持1024字节数据帧加上一个文件信息首帧。作为接收端你首先要知道YMODEM发送方会发来这么几类字节字节名称含义0x01SOH头标志之一后面跟128字节数据块0x02STX头标志之二后面跟1024字节数据块0x04EOT发送方说“文件发完了”0x06ACK接收方回复“这帧没问题”0x15NAK接收方回复“这帧坏了重发”0x18CAN取消传输双方都不要继续了0x43C接收方就绪信号支持CRC校验一帧数据的完整结构是头字节 序号 序号反码 数据 两个CRC字节。比如一帧普通的数据帧用STX开头表示这帧带1024字节0x02、序号、0xFF减序号、1024字节数据、CRC高字节、CRC低字节。序号的长度只有一个字节从0开始按帧递增到255再回绕。首帧特殊一点它一定是用SOH发送的128字节帧数据内容是ASCII字符串格式类似firmware.bin 32768也就是“文件名 文件大小”中间用空格隔开文件大小是十进制数字。接收端可以从首帧里解析出文件大小用来判断固件是否放得下、以及可以给用户显示进度条。CRC校验这部分YMODEM用的是CRC-16/XMODEM算法多项式是0x1021初始值为0x0000计算得到的两个字节在发送时低字节在前。这个算法在各个编程语言里都有现成实现但如果你写Bootloader时打算自己实现一遍要注意一点入参是“数据域”不包含头和序号校验的范围就是纯数据。2.3 接收状态机怎么设计缓冲给多少YMODEM的接收端在嵌入式上最容易写成一团乱麻因为数据不是一个包一个包地整齐到来而是字节不断地往串口里塞。如果把“接收数据”“校验”“写Flash”“回ACK”全堆在一个地方逻辑会乱到没法维护。正确做法是搭一个简单的状态机状态切换就是靠收到的字节引导。我的做法是这样的定义一个枚举表示当前处于哪个解析阶段从“等待帧头”开始逐个字节接收。收到SOH或STX之后记录是128字节模式还是1024字节模式接着收序号、序号反码再收数据区最后收两个CRC字节。所有字节都收完后统一做一次序号校验和CRC校验校验通过就交给Flash写入流程写完以后回ACK校验失败就回NAK并让状态机复位继续等待下一帧。这个状态机的代码量不大核心就几十行。伪代码大致长这样static YmodemRxStatus_t RxYmodemByte(uint8_t ch) { static uint8_t frame_buffer[1024]; static uint32_t received 0; switch (rx_state) { case WAIT_HEADER: if (ch SOH) { current_len 128; rx_state WAIT_BLOCK_NO; } else if (ch STX) { current_len 1024; rx_state WAIT_BLOCK_NO; } else if (ch EOT) { return YMODEM_EOT; } else if (ch CAN) { return YMODEM_CANCEL; } break; case WAIT_BLOCK_NO: file_block ch; rx_state WAIT_BLOCK_NO_COMP; break; case WAIT_BLOCK_NO_COMP: if ((uint8_t)(file_block ch) ! 0xFF) { rx_state WAIT_HEADER; return YMODEM_ERROR; } received 0; rx_state WAIT_DATA; break; case WAIT_DATA: frame_buffer[received] ch; if (received current_len) { rx_state WAIT_CRC_HI; } break; case WAIT_CRC_HI: crc_hi ch; rx_state WAIT_CRC_LO; break; case WAIT_CRC_LO: crc_lo ch; if (CheckCRC16(frame_buffer, current_len, crc_hi, crc_lo)) { rx_state WAIT_HEADER; return YMODEM_OK; } else { rx_state WAIT_HEADER; return YMODEM_ERROR; } } } }缓冲区给多大一帧最大1024字节那RAM就准备一个1024字节的数组顶多加几个操作标志。F103C8的20KB RAM完全没问题不用像伺候小资源单片机一样抠到用几百字节。这套状态机虽然没有用到DMA和空闲中断但在115200波特率下一个字节间隔接近87微秒单片机的处理完全来得及。逐字节中断收的好处是代码直观、调试容易尤其第一次跑协议的时候任何一个中间步骤有问题串口打印一加就能定位。2.4 Flash擦写按页擦除、半字编程、写完要读回STM32的Flash写操作有严格的约束必须先擦除再写、每次写入最小单位是16位半字、对F103来说擦除的最小单位是1KB一页。这些语句每一个都对应一个实际的坑。Bootloader里写Flash用的标准外设库是这么几个函数顺序也很固定上行解锁、擦除对应的页、按半字编程、再次上锁。先解锁FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);擦除App对应的页时按1KB一页去算for (addr APP_START_ADDR; addr APP_START_ADDR app_size; addr 1024) { FLASH_ErasePage(addr); }写数据的时候把接收缓冲区里的每两个字节组合成一个半字调用FLASH_ProgramHalfWorduint16_t half_word (uint16_t)(buf[i] | (buf[i 1] 8)); FLASH_ProgramHalfWord(flash_addr, half_word);有一件很容易被忽略的事写完之后要立刻读回并和源数据比较确认写入成功才回ACK。这虽然不是YMODEM规范要求的但Flash写入偶发失败是存在的尤其芯片在恶劣供电环境下或者擦写次数已经很大的时候。写完不验证等App跑起来随机崩了那排查起来比多写几行校验代码痛苦一百倍。3. 把YMODEM跑在F103上最容易掉的三个坑3.1 起始C发多久、谁来先开口YMODEM和XMODEM的握手有个方向问题容易绕晕。串口升级时谁是发送方、谁是接收方电脑加终端软件是发送方单片机Bootloader是接收方。YMODEM的规矩是接收方先发C表示“我用CRC校验模式你准备好就可以发数据了”。发送方收到C之后才会开始发第一个包含文件信息的SOH首帧。我一开始做的时候犯过迷糊写完Bootloader以为先在电脑端点“发送文件”就会自动传。结果不是的如果单片机不加主动发送C的逻辑上位机的YMODEM发送会一直处于等待状态像是在问“喂你倒是发个信号啊”。两边都干瞪眼。正确做法就是Bootloader进入升级模式后周期性地向串口发送一个C字符比如每500毫秒发一次。上位机的YMODEM发送一旦收到这个C就会开始传输首帧。整个接受方状态机在“发C等待首帧”这个阶段循环收到非首帧的乱码数据就忽略收到首帧就进入正常接收流程。等待时间设多长也是个实用问题。我建议不要在Bootloader里无限等待否则一台没有升级需求的设备每次开机都卡在Bootloader等人来刷系统白白耽误启动时间。设定一个超时比如5到10秒超时了如果还没有进入升级流程就自动跳转App。这样既给上位机留够了操作时间又不影响产品正常启动速度。3.2 包序号、1024字节块和最后那个128字节短包YMODEM的数据帧序号是从1开始编号的首帧的文件信息包序号是0。如果你在代码里做Flash地址映射时直接用帧序号减1再乘1024大方向是对的但有两个特殊情况必须处理。第一不是所有数据帧都是1024字节。发送方在文件的最后如果剩余数据不足1024字节就会用SOH头来发一个128字节的短包。也就是说你判断“当前帧数据长度到底是多少字节”不能靠假设必须要看包头是SOH还是STX。收到STX就当1024字节处理收到SOH就当128字节处理然后Flash写入长度也要跟着变不然会把最后不足1024字节的数据写到错误的位置或者把一堆垃圾也写进去。第二帧序号是8位传大文件时序号超过255会回绕。一个固件往往也好几百KB到了后面序号归零再递增如果代码里判断序号递增用的是“必须比上一帧大一”这种逻辑回绕之后就永远接收不到了。正确写法是判断“当前序号减去上一帧序号结果对256取模等于1”或者干脆不依赖序号连续性直接按收到的数据帧数累加计算地址。这样既能容忍回绕也更简单。3.3 EOT之后的空帧别漏了文件数据全部传完之后YMODEM还有一段收尾发送方会发送一个EOT字节表示数据发完了。接收方按协议要求第一反应是回一个NAK表示“你再来一次我想确认你别突然掉线”发送方收到NAK后会再发一次EOT这一次接收方回ACK表示“确实结束了我收到了”。紧接着还有一个小尾巴发送方会再发一个SOH空帧块号为0数据区全为0。这个空帧的意思是“整个会话结束”。接收方要回一个ACK然后升级流程才算真正闭环。很多文章讲YMODEM只讲到EOT就完了空帧这部分不提但真写出来跑了之后就会碰到EOT收尾都处理完了结果上位机还报“transfer error”或者发送方一直在等最后那个ACK。原因很简单——空帧没回ACK。所以在状态机的设计里收完EOT、回到等待帧头时要专门加一个判断如果收到序号为0的SOH帧就把它当作会话结束标志来回ACK。3.4 为什么我推荐“收完一帧再擦写”而不是边收边写Flash擦写是一个比较重的操作一次擦除1KB的页根据芯片体质大概要20到40毫秒写入1024字节要执行512次半字写中间处理器虽然还能响应中断但总线的访问冲突会让代码执行变得很慢。如果一边收串口一边擦写Flash没有DMA的话串口中断极容易在这段时间丢字节。那有没有更稳妥的处理方式有就是“帧内串行”整个1032或133字节的一帧完整收完后校验也通过了再去做擦除和写入。这时候下一帧数据还没到或者还在上位机的发送缓冲里排队Flash操作有时间窗口可以安静地完成写完再回ACK。这样MCU处理压力最小也最容易保证每个包都完整。有人会用DMA加环形缓冲区做到边收边写性能确实好但工程复杂度会高一截。对F103这种芯片又不是传几百MB的文件一次几十KB到几百KB固件用串行帧处理加115200波特率完全够用。升级体验差不了多少代码却好调试得多。我很推荐作为第一次做IAP的默认选择。4. APP端要改的不只是地址向量表重映射、Keil配置与bin提取4.1 Keil工程里IROM1的位置和大小Bootloader那边接线、协议、分区都打通了下一步就是让App“从新家”正常跑起来。这些年我见过太多次“Bootloader下载没问题跳过去就是黑屏”的情况一问十有八九是App工程的启动地址没改。在Keil MDK里选择Project、Options for Target、Target标签页有一个IROM1配置项。默认情况下Start是0x08000000Size是0x40000或者根据芯片自动给的。如果Bootloader占了前面的16KB你就要把App的IROM1改成Start0x08004000Size0x8000000-0x40000xFFC000不对我就是不想用浮点。App的Size应该是芯片总Flash大小减去Bootloader占的偏移。比如F103C8总Flash是0x10000Bootloader占用前0x4000那App的IROM1 Size就写0xC000也就是48KB。如果是F103ZET6总Flash是0x80000Bootloader占0x8000那App的Size就写0x78000。注意这个Size不改错地方很容易出错只改StartSize不改Keil会提示地址溢出只把Size改小了一旦固件超过范围链接直接报错起码能发现。IRAM的设置一般不用动。有人会把IRAM的起始地址也偏移一下说是什么向量表需要放到RAM里其实F103用SCB-VTOR方案之后根本不需要把向量表复制到RAMIRAM老老实实从0x20000000、16KB或按芯片RAM大小填就行。4.2 向量表重映射写在哪一行App如果从0x08004000运行而向量表仍然指向0x08000000那么只要发生任何中断MCU就会从Flash最前面取中断处理函数地址。那里是Bootloader的向量表不是App的。结果就是App里点个串口、开个定时器一触发中断就直接跑飞或者进HardFault。Cortex-M3内核允许通过SCB-VTOR这个寄存器设置向量表偏移。F103这边只需要在App的main函数最开头任何外设初始化之前执行一行SCB-VTOR APP_START_ADDR;这里的APP_START_ADDR要跟你Keil里设置的IROM1 Start保持一致也就是0x08004000。有的老工程会改system_stm32f10x.c里的VECT_TAB_OFFSET宏把0x0改成0x8000。这个宏最终也会被用来自动设置VTOR。两种方法都可以但我更推荐直接在自己的main第一行写死地址理由很简单代码一眼能看见别人接手的时候不用去翻库文件里某个宏到底配成多少。还有个细节VTOR设置完之后后面所有中断的入口都从App的向量表取了包括SysTick、串口、定时器等等。所以这行代码必须在“任何可能开中断的代码”之前执行。如果放在初始化串口之后一开串口中断就崩了调试起来会怀疑人生。4.3 从App跳回Bootloader软复位加备份寄存器常见的升级场景是产品已经烧好了App某天需要升级。这时候App可以通过界面、串口命令或按键触发自己进入“准备升级”的状态。可是App并不能直接调用Bootloader里的函数两者是两个独立的程序同时App也不能直接跳转到Bootloader的中断向量去执行因为你不知道Bootloader内部的状态何况两个程序本来就该解耦。工业上最稳妥的做法是App先在一个掉电不丢失的存储区域写一个升级标志然后执行软复位。芯片复位后Bootloader先跑起来它发现升级标志就留在Bootloader里等待串口升级。升级完成后Bootloader清掉标志再跳转App。升级标志放哪F103上我推荐放在RTC的备份寄存器里使用BKP_DR1这样的寄存器。它由VBAT供电维持芯片软复位不会丢断电也不丢。App这边写入标志的代码很简单RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); NVIC_SystemReset();Bootloader那边就是上电读判断相等就进入升级模式if (BKP_ReadBackupRegister(BKP_DR1) 0xA5A5) { // 清除标志进入升级模式 BKP_WriteBackupRegister(BKP_DR1, 0x0000); }强调一点万一设备的VBAT电池没接备份寄存器的内容是不可靠的。所以不能只依赖这个标志外部按键强制升级入口一定得留一个双保险才是可靠设计。4.4 输出bin文件fromelf参数和一个小陷阱Keil编译默认产出的是hex文件但YMODEM升级建议用bin文件。bin是纯二进制数据没有地址信息大小和固件实际体积一致传输时简单hex是文本格式带记录和地址虽然也能传但解析起来没必要地麻烦。让Keil在编译后自动生成bin需要在Options for Target的User标签页、After Build/Rebuild那一栏填一行fromelf命令。先勾选“Run User Program #1”然后内容按你自己的工程名改fromelf.exe --bin --output..\Output\firmware.bin ..\Obj\project.axffromelf是Keil ARM编译器自带的工具。重要提醒命令行里的路径和文件名要跟你工程实际的输出axf位置对照好很多人的bin文件一直没生成就是路径写错了。另外不要加引号不要写绝对路径用相对当前工程文件所在目录的相对路径最不容易踩坑。配合上固件大小校验生成bin之后顺手看一眼大小比如编译器里显示的Program Size再和自己预期的固件体积对比一下。如果bin文件的体积比预料的大了不少多半是开了某些调试信息或者数组开太大了Flash容易爆掉。5. 跳转那一下机器重启瞬间最容易翻车5.1 跳转前把“外部世界”收拾干净Bootloader收完固件、写好Flash最后一步就是跳转到App。很多人觉得这一步不就是“把PC指针指过去”吗其实不是这中间隐藏着一个非常容易翻车的环节——外设状态。Bootloader跑的过程中时钟、串口、SysTick、可能还有GPIO和看门狗都已经初始化了。你直接跳转到AppApp的main函数从头初始化通常是不介意重新配一遍的。但问题是中断可能正在挂着比如Bootloader的串口接收中断可能刚刚触发又还没来得及响应SysTick的计数器还在运行DMA可能还有未完成的传输。这些残留状态到了App里就是定时炸弹。我跳转前一定会做这几件事按顺序执行__disable_irq(); // 关闭SysTick SysTick-CTRL 0; SysTick-LOAD 0; SysTick-VAL 0; // 清掉所有中断的pending状态只清保留寄存器即可 for (uint32_t i 0; i 8; i) { NVIC-ICER[i] 0xFFFFFFFF; NVIC-ICPR[i] 0xFFFFFFFF; } // 恢复系统时钟到默认状态 RCC_DeInit();__disable_irq()那一步是关掉全局中断。这个关中断的动作至少保证在跳转切换的这几条指令期间不会有任何中断进来捣乱。跳进App的main之后App自己会重新初始化RCC、外设和NVIC再自己开中断链路是干净的。看门狗这块要单独说如果App之前启用了独立看门狗IWDG复位之后IWDG仍然在跑。Bootloader如果停留了几秒才升级完不及时喂狗就会在升级途中被看门狗复位。稳妥办法是Bootloader里开一个定时器周期喂狗或者在升级状态下重新初始化IWDG把超时时间拉长更狠一点的做法是整个Bootloader升级期间不碰IWDGIWDG的窗口期要长于Bootloader整个最长停留时间。这个值要根据你自己产品的需求算好别拿着默认的几百毫秒就去升级。5.2 合法的栈顶和应用入口怎么判断Bootloader要跳转之前Flash里新烧的App是不是合理的程序不能“蒙着眼睛”直接跳。一个合法的Cortex-M程序它的Flash最开始4字节必须是启动时用的主栈指针紧接着4字节是复位中断处理函数的地址。Bootloader至少应该检查这两种地址看起来是不是合理的。栈顶指针有非常明显的特征它必须落在SRAM地址范围内。F103的SRAM一般在0x20000000开头C8有20KBZE有64KB。检查时不能只看大于等于0x20000000还要看它有没有超出SRAM上边界不然一个非法栈顶会让代码第一时间陷入更深的位置。复位函数地址也应该落在Flash范围内也就是App区段内部。简单校验逻辑大概这样#define APP_START_ADDR 0x08004000 uint32_t stack_addr *(volatile uint32_t *)APP_START_ADDR; uint32_t reset_addr *(volatile uint32_t *)(APP_START_ADDR 4); if ((stack_addr 0x2FFE0000) 0x20000000 // 栈顶在SRAM内 (reset_addr 0x2FF80000) 0x08000000) // 入口在Flash内 { // 跳转 } else { // 留在Bootloader打印错误 }校验不通过就停在Bootloader等待重新升级而不是硬着头皮跳过去。这一个判断能避免大量“升级后设备啥反应没有”的莫名其妙问题。跳转本身也就是改一下栈指针然后调用函数指针typedef void (*pFunction)(void); void JumpToApp(void) { uint32_t jump_addr *(volatile uint32_t *)(APP_START_ADDR 4); pFunction jump_to_app (pFunction)jump_addr; __set_MSP(*(volatile uint32_t *)APP_START_ADDR); jump_to_app(); }注意是__set_MSP设置主栈指针不是__set_PSP。中断服务函数、main函数用的都是MSP特权模式下默认用的也是MSP。很多人写跳转的时候这里一马虎栈指针指错了地方App一跑就崩。5.3 几个常见失败现象和排查对照把我在实际调试中碰到过的失败现象和对应原因列成一张表方便你排查。现象可能原因排查方向升级完成后无反应芯片像死了一样栈顶指针校验没通过停留在Bootloader了检查烧进去的bin文件首4字节是否符合SRAM地址一上电LED亮几下就重启跳转后App里开了中断但向量表没重映射检查SCB-VTOR是否设置位置是否在App_main开头升级过程超时卡住收完一帧后长时间擦写Flash串口丢失下一帧头把“帧内串行”逻辑调好让擦写在空闲窗口完成或加上超时重发机制能升级、能跳转但串口打印乱码波特率不符或时钟配置被打乱检查Bootloader跳转前的RCC_DeInit以及App自己的SystemInit是否正常升级完成后仍运行旧App跳转条件没判定到直接跳回旧入口检查App起始地址处内容是不是新固件Keil IROM1设置和bin烧写地址是否一致升级时设备反复复位看门狗没喂或超时时间不够检查IWDG配置在Bootloader升级循环里增加喂狗排查工具上Keil的调试器其实可以看得很细先在Bootloader里打上断点看升级完成后Flash对应地址上的数据是不是新固件再看跳转前栈顶和复位入口两个变量的值。这种方法比瞎猜快得多如果跳转目标本身错了断点一看就暴露了。5.4 升级中途断电能不能救可靠性的最低配置“升级到一半拔了电”是嵌入式升级绕不开的话题。IAP方案的薄弱点就是Fly升级阶段App区可能写到一半另一头旧App已经被擦掉新App还没写完设备处于既没程序又没人理的阶段。F103C8这种芯片没地方做双Bank备份现实可行的容灾思路是Bootloader要保证在任何情况下都能自己重新出现在升级模式。这意味着三点。第一Bootloader所在的16KB空间不能被App任何操作擦掉。App里千万不要对0x08000000到0x08003FFFF范围做Flash擦除和写操作这个红线要在App的Flash驱动函数里加Address范围判断从代码层面就杜绝误擦。第二保留强制进入升级模式的物理入口。那个升级按键不能省即使有备份寄存器标志位也得留一手。真正的现场救援最后基本靠这个按键按住、上电、Bootloader停在升级模式重新传一个稳定固件。第三升级标志要设计成“写完新固件并且校验通过后才清除”。如果Bootloader能区分“上电时App区域有效”和“升级失败App区域无效”这两种状态就可以做到升级中途断电后再次上电仍然停留Bootloader并提示“App无效请重新升级”。这比摔在那里强制跳转要安全得多。对于Flash容量更大的F103型号如果产品对可靠性要求很高那就上双Bank。Bootloader把新固件写到备用区全部校验完成后再一次性切换启动地址旧App一直留到最后一刻新固件启动验证成功后才舍得淘汰旧版。代价是Flash至少要多留出整个App体积的空间看具体产品是否划算。6. 实测记录从第一次跑通到连续升级不翻车6.1 需要准备的东西和第一次烧录路径硬件上的准备是这个方案里最省心的一部分。用F103C8T6的常用开发板带一个USB转TTL模块几根杜邦线再加一个ST-Link。第一次把Bootloader烧进Flash我建议用SWD之后升级全部走串口。接线很简单USB转TTL的TX接板子的PA10也就是USART1的RXRX接PA9GND必须共地。这里有个很低级但很容易犯的错误USB转TTL模块和板子之间如果没共地串口电平参考点不一致收据会随机错乱到怀疑人生。第一次烧录路径是这样的ST-Link接到板子SWD口Keil里设置好Debug、Flash Download把编译好的Bootloader hex下载到0x08000000。打开串口终端波特率115200。复位板子看到Bootloader打印提示、开始周期发C。打开终端软件的YMODEM发送功能选择App的bin文件发送。升级完成后板子自动跳转串口打印App的启动信息流程跑通。之后每次升级就不需要任何调试器了串口走YMODEM直接搞定。这也是整套IAP系统的意义所在。6.2 用终端软件发送YMODEM的操作流常见的终端软件里Tera Term这类工具就带YMODEM收发功能上手门槛很低。启动Tera Term连接串口后确认波特率和Bootloader一致。然后复位开发板等Bootloader进入升级模式看到终端窗口里出现周期性的C字符。然后在菜单里选择File、Transfer、YMODEM、Send弹出文件选择框选中编译生成的bin文件点OK。软件会自动回应Bootloader的C开始发送。屏幕上会看到块号在跳动传完自动关闭窗口。这一步就完成了。有两点经验第一不要在Bootloader还没打印出C的时候就点Send那样上位机发送模块会等一个接收端信号等半天发现没有就报错第二传输过程中不要碰USB线尤其不要拔串口线或者重新插拔USB转串口模块不然协议直接断掉又得从头再来。对SecureCRT也可以这么玩只是菜单从File、Transfer里进底层走的还是同一个YMODEM协议。关键是接收端也就是Bootloader要主动发C这是整个流程能跑通的前提。6.3 给自己写一个Python升级工具的思路终端软件好用归好用但在产线或者批量升级场景下几十台设备一台一台去鼠标点发送显然不现实。这时候用一个简单脚本去驱动整个升级流程效率会远超手动操作。Python方案最简单的就是pyserial加一个YMODEM协议发送函数。核心是模拟发送方的那几步等C、发文件首帧、按1024发数据帧、处理EOT。有一个简单的思路可以复用就是只发、不折腾接收端回包判断难点在于要在每个环节等ACK/NAK。一个能用但不过分复杂的脚本框架大概长这样import serial from xmodem import YMODEM ser serial.Serial(COM3, 115200, timeout1) def getc(size, timeout1): return ser.read(size) or None def putc(data, timeout1): return ser.write(data) ymodem YMODEM(getc, putc) with open(firmware.bin, rb) as f: ymodem.send(f)实际用的时候要加不少校验和重试逻辑比如先检查串口是不是正常打开了发送前复位设备、等待Bootloader发出C发送过程中给每个块加个超时重发最后要确认整包都发完。完整脚本大概一百多行。Python的好处是这种脚本可以很方便地跟产线系统集成局域网设备、或者加个界面控制都容易扩展。6.4 实测耗时和稳定性参考我用一个约48KB的App固件做过完整升级测试YMODEM每帧1024字节加上首帧和收尾整个流程跑下来大概是5到7秒。这个时间包含文件传输、Flash擦写、ACK回包以及上位机软件自身的处理停顿。几次实测里115200波特率的纯传输时间占大头约4.5秒剩下的1到2秒是擦写Flash和协议交互带来的开销。如果固件体积上到256KB115200波特率下就大概要半分钟到四十秒。这个时间对现场升级来说还可以接受但如果客户要求“快一点”有两个立竿见影的办法一是把串口波特率拉到256000甚至更高传输时间直接减半二是在Bootloader和上位机都做1024字节块不要退回128字节模式。稳定性这块在排除了硬件电平不稳、供电噪声这些硬伤之后YMODEM配合Flash读回校验的设计连续升级二三十次没见过一次失败。偶发的失败往往集中在USB转串口模块劣质、接收缓冲区溢出、或者中途拔线这些外部因素上。真正跑起来之后意料之外的问题几乎都出在上位机软件对YMODEM协议末端空帧的处理差异或者Bootloader在跳转前没有处理好外设残留状态这两类。把这两个地方盯住了整套系统就非常皮实了。最后分享一个实际工作中让我省了不少事的习惯拿一个最小系统板先把Bootloader烧进去然后用一根串口线连着从头到尾把“升级-断电-再升级”这个循环在开发桌上反复操练个十几次再去动真设备。这种类型的软件最怕的从来不是正常流程而是那些“升级一半停电了”“串口线松了”“客户多按了一下复位键”的意外路径。把这些意外路径都提前模拟过一遍心里才踏实设备发到现场才真的敢说“这升级不会把人坑了”。