news 2026/10/21 1:57:00

STM32串口IAP升级实战:Bootloader与App分区设计及避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32串口IAP升级实战:Bootloader与App分区设计及避坑指南

简介:STM32F103串口IAP升级程序是一套完整且可直接落地的固件在线升级方案,面向嵌入式开发工程师及学习IAP原理的爱好者,解决设备运行中无需断电、不使用编程器即可更新固件的需求。该工程整合了开发者原创的核心逻辑与部分参考代码,包含完整的Bootloader、串口通信、固件接收与校验、Flash擦写、错误恢复及固件合法性校验机制,并附带一个测试应用程序,用于验证升级流程的稳定性;代码结构清晰,关键流程带有注释,便于理解跳转与Flash操作细节。压缩包共290个文件,以C源码和头文件为主(h/c文件合计140个),另有编译生成的axf/bin/hex可直接烧录文件、Keil工程文件(uvprojx/uvoptx)及map/lst/sct等辅助文档,整体体积约5.68MB,目录结构清晰,便于直接查阅与二次开发。目前已有2786人学习下载,适合希望深入理解IAP实现细节的STM32开发者,可据此快速搭建自己的串口升级框架。

1. 串口IAP升级:为什么你的产品不能每次升级都靠J-Link

做过量产设备的人都有过这种经历:产品已经装进机箱、封好外壳,结果客户说“逻辑要改”,你只能拆机、飞线、接上烧录器,战战兢兢地刷一遍固件。要是产品在异地,那更是一场灾难。这份原创的STM32F103串口IAP升级程序,解决的就是“不用拆机、不用J-Link、通过串口就能升级固件”这件事。它自带一个完整的Bootloader工程和一个测试App工程,Bootloader负责接收串口数据并写入Flash,App负责跑业务逻辑并在启动时跳转到Bootloader进入升级模式。两个工程互相配合,你拿来改一改就能用在自己的板子上。

项目适合三类人:正在做量产设备的嵌入式工程师,想搞懂IAP跳转和Flash读写细节的STM32学习者,以及手头有项目但一直用烧录器升级、想改成串口升级的开发者。整体技术栈就是STM32F103标准外设库,没有依赖复杂的RTOS或第三方组件,逻辑清晰,移植成本低。下面我从架构开始拆,然后给出具体跑通步骤和能直接抄的代码段。

2. 架构设计:两个工程的协作方式与Flash分区规划

2.1 Bootloader与App的分区设计

IAP的核心思想是把Flash分成两个区域:Bootloader区运行在起始地址,App区运行在高地址段。设备上电先跑Bootloader,Bootloader决定是进入App还是等待新的固件包。这份资源里的分区规划用的是最常见也是最稳妥的方案:Bootloader放在0x08000000起始的32KB空间,App放在0x08008000起始的剩余空间。

// Bootloader工程中的Flash起始地址定义 #define BOOTLOADER_START_ADDR 0x08000000 // Bootloader起始地址 #define APP_START_ADDR 0x08008000 // App起始地址,偏移32KB #define APP_MAX_SIZE 0x0000F000 // App最大空间60KB,留出最后的标志位区域

分区参数说明:Bootloader占32KB,足够放下串口接收、Flash擦写、协议解析这些逻辑。App最大60KB,对于大多数中小型应用够了,不够的话可以调整偏移量,但要注意Bootloader和App的编译链接地址必须对应上。最后的0x1000空间我用来放升级标志,避免每次上电都进Bootloader等待超时,后面会详细讲。

提示:分区前先看自己的芯片型号,F103C8T6是64KB Flash,F103RBT6是128KB,别把App大小设得超过实际容量。

2.2 跳转实现:从Bootloader到App的关键代码

Bootloader判断条件满足后,需要跳转到App的复位向量地址。这里有一个最容易踩的坑:不能直接调用函数指针跳转,必须先关闭全局中断、设置好主栈指针,再从App的复位向量取出地址跳过去。以下是标准做法。

// IAP_JumpToApp函数:完成从Bootloader到App的跳转 typedef void (*pFunction)(void); pFunction JumpToApplication; void IAP_JumpToApp(uint32_t app_addr) { uint32_t app_reset_addr; // App复位向量地址,存放在App起始地址的前4字节 uint32_t app_stack_addr; // App初始栈顶地址,存放在App起始地址的前4字节的地址里 // 先关闭全局中断,避免跳转过程中被打断 __disable_irq(); // 取App起始地址处的栈顶值(前4字节) app_stack_addr = *(volatile uint32_t *)app_addr; // 取App复位中断向量(前4字节之后的4字节) app_reset_addr = *(volatile uint32_t *)(app_addr + 4); // 校验栈顶地址是否在RAM范围内,防止跳到无效地址 if ((app_stack_addr & 0x2FFF0000) != 0x20000000) { return; // 栈顶异常,不执行跳转 } // 设置主栈指针为App的栈顶值 __set_MSP(app_stack_addr); // 重新使能中断,App主程序里会再次初始化中断向量表 __enable_irq(); JumpToApplication = (pFunction)app_reset_addr; JumpToApplication(); // 跳转到App的Reset_Handler }

这段代码的逻辑说明:前4字节是SP值,紧接着4字节是Reset_Handler地址,这是Cortex-M3内核的规定。校验栈顶地址落在0x20000000到0x2000FFFF之间,是为了防止Flash数据错误时程序飞到莫名其妙的地方。我在实际项目里还加过CRC校验,但这份例程没有,你自己加也很简单,后面会提。跳转之前务必关闭中断,否则中断响应时PC已经跳到App,NVIC却还挂着Bootloader的向量表,会直接HardFault。

2.3 App工程的编译地址设置与中断向量重映射

App工程不能直接用默认的0x08000000编译,必须把链接地址改到0x08008000,否则Bootloader跳过去会跑飞。同时,App内部的中断服务函数比如USART1_IRQHandler、定时器中断,它们的地址是基于编译时指定的Flash地址算出来的,所以需要在App启动的最早阶段重新设置中断向量表。

// App工程main.c的启动部分 int main(void) { // 关键:重映射中断向量表到App所在Flash区域 // 第一个参数是向量表基地址,这里设为App起始地址 // 第二个参数是NVIC向量表重映射偏移量寄存器 NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x08008000); // 剩下的就是正常的时钟、外设初始化 SystemInit(); GPIO_Configuration(); USART_Configuration(); // ... 你的业务代码 }

参数说明:NVIC_SetVectorTable的第一个参数NVIC_VectTab_FLASH表示向量表在Flash里,第二个参数是偏移量。如果用标准外设库,这一步必须在任何可能触发中断的操作之前完成,否则一旦使能外设中断,CPU去旧地址找向量表就会异常。很多新手把这一行放到SystemInit()之后,结果串口中断一进来就死机,就是因为向量表还没有偏移过去。

2.4 升级标志位:上电时决定进Bootloader还是App

如果Bootloader每次上电都要等待几秒看有没有升级指令,那用户体验很差。常规做法是用Flash的最后一个页专门存升级标志。Bootloader启动时读这个标志:为特定值就进入升级等待模式,为另一个值就直接跳到App。

// 升级标志地址定义:使用Flash最后一页的起始地址 #define UPGRADE_FLAG_ADDR 0x0800FC00 // 以64KB Flash为例,最后一页起始地址 #define UPGRADE_FLAG_VALUE 0xA5A5A5A5 // 需要升级时写入的固定值 // Bootloader启动流程 int main(void) { uint32_t flag_value; // 读取升级标志 flag_value = *(volatile uint32_t *)UPGRADE_FLAG_ADDR; if (flag_value == UPGRADE_FLAG_VALUE) { // 需要升级:清除标志,进入等待接收固件的模式 IAP_EraseFlashPage(UPGRADE_FLAG_ADDR); IAP_WaitForUpgrade(); } else { // 无升级需求,直接跳转到App IAP_JumpToApp(APP_START_ADDR); } }

这个设计的巧妙之处在于:App端收到升级指令后,先写升级标志,然后软复位,这样Bootloader上电后自然就走升级流程。App正常工作时,这个标志位是无效值,所以每次上电直接跳App,几乎不增加启动延迟。这里注意,擦除标志页之前要把标志读出来保存,或者先擦除再写,顺序反了会导致上电后标志还残留着,一直进升级模式。

3. 串口通信协议与固件接收:一帧数据怎么被完整写入Flash

3.1 协议帧格式设计

IAP的数据链路用的是串口UART,没有用现成的Ymodem协议,而是自定义了一套简单的帧格式。这么做的好处是逻辑透明、容易二次修改,坏处是你要自己保证校验和粘包处理。这份例程的帧格式定义如下:

字段长度说明
帧头2字节固定为0xAA 0x55,用于帧同步
指令1字节0x00表示写入数据,0x01表示结束升级,0x02表示读取版本
数据长度2字节本帧携带的有效数据字节数,小端序
数据区N字节固件二进制数据,最大不超过256字节
累加校验1字节前面所有字节的累加和,取低8位
// 协议解析中的帧缓冲区结构体 typedef struct { uint8_t buf[260]; // 接收缓冲区:2帧头 + 1指令 + 2长度 + 256数据 + 1校验 uint8_t state; // 当前状态机的状态 uint8_t data_len; // 本次帧的有效数据长度 uint8_t received_len; // 当前已接收的字节数 } IAP_Frame_t;

这种协议比裸发数据包要稳得多,因为帧头、长度、校验都在,接收方可以根据长度字段决定收多少字节再判断校验,而不是靠固定长度的包来处理。实际产品里我还建议在数据区里加入地址偏移字段,让上位机可以跳着写Flash,这样即使传输中断也可以断点续传。

3.2 串口状态机接收:逐字节解析不丢数据

串口接收如果采用简单的“收满一包再处理”,可能因为与帧边界不齐导致丢帧。我在这份例程里用的是状态机方式,每个字节进来都走一次状态迁移,这样不管上位机从什么位置开始发,都能同步上帧头。

// 串口中断里逐字节调用IAP_ParseByte uint8_t IAP_ParseByte(IAP_Frame_t *frame, uint8_t byte) { switch (frame->state) { case 0: // 等待第一个帧头0xAA if (byte == 0xAA) { frame->state = 1; } break; case 1: // 等待第二个帧头0x55 if (byte == 0x55) { frame->state = 2; } else if (byte != 0xAA) { frame->state = 0; // 不是0x55也不是0xAA,重新等帧头 } else { // 仍然是0xAA,保持状态1继续等0x55 } break; case 2: // 接收指令字节 frame->buf[2] = byte; frame->state = 3; break; case 3: // 接收数据长度低字节 frame->buf[3] = byte; frame->state = 4; break; case 4: // 接收数据长度高字节 frame->buf[4] = byte; frame->data_len = frame->buf[3] | (frame->buf[4] << 8); if (frame->data_len > 256) { frame->state = 0; // 长度异常,重新同步 } else { frame->received_len = 0; frame->state = 5; } break; case 5: // 接收数据区 if (frame->received_len < frame->data_len) { frame->buf[5 + frame->received_len] = byte; frame->received_len++; if (frame->received_len == frame->data_len) { frame->state = 6; // 数据收满,进入校验等待 } } break; case 6: // 接收校验字节 frame->buf[5 + frame->data_len] = byte; if (IAP_CheckSum(frame->buf, 5 + frame->data_len) == byte) { frame->state = 0; return 1; // 校验通过,通知主循环处理这帧数据 } frame->state = 0; break; default: frame->state = 0; break; } return 0; }

逻辑说明:状态0和1负责消抖帧头,比如收到0xAA后收到0xAA,不能把它当作第二个帧头,得继续等0x55。状态3、4拼出长度后立刻判断上限,防止缓冲区溢出。最后校验通过才返回1,主循环拿到这个返回值再去执行Flash写入。这样串口中断里只有字节搬移和状态迁移,没有耗时操作,115200波特率下不会丢数据。

参数说明:IAP_CheckSum函数就是把所有字节累加取低8位,上位机也要用同样的算法。如果你用的是带DMA的串口,也可以把这一套逻辑放到DMA接收完成回调里,但状态机本身不需要改。

3.3 闪存写入:页擦除与半字写入

STM32F103的Flash写入规则是:写入前必须先擦除整个页(1KB),然后按16位半字为单位写入,不能直接写单字节。这也是为什么数据帧长度最好不要超过256字节的原因——一页1KB,四帧数据正好填满一页,方便边收边写。下面是处理固件数据帧的核心代码。

// 处理一帧固件数据 void IAP_ProcessDataFrame(uint32_t app_base_addr, uint8_t *data, uint16_t len) { uint32_t target_addr; uint16_t i; static uint16_t offset = 0; // 当前已写入的偏移量 uint16_t word_count; // 算出本次写入的目标Flash地址 target_addr = app_base_addr + offset; // 如果目标地址跨页,需要先擦除新页 if ((target_addr % 1024) == 0) { IAP_EraseFlashPage(target_addr); } // 把数据按半字(16位)写入Flash word_count = len / 2; for (i = 0; i < word_count; i++) { uint16_t half_word = data[2 * i] | (data[2 * i + 1] << 8); FLASH_ProgramHalfWord(target_addr + 2 * i, half_word); } // 处理奇数长度的情况(最后一个字节单独补0写) if (len % 2) { uint16_t last_word = data[len - 1]; // 高字节填0 FLASH_ProgramHalfWord(target_addr + len - 1, last_word); } offset += len; }

关键点说明:FLASH_ProgramHalfWord是标准外设库的Flash写函数,必须确保调用前Flash已经解锁。擦除页的操作会阻塞CPU,一般在擦除前关中断,擦除完再开中断,避免串口接收缓冲区溢出。这里的static uint16_t offset是个隐患,如果中途升级失败重传,需要先复位offset,否则地址会错乱。更稳妥的做法是把offset作为参数传入,由主循环管理。

// Flash解锁与操作示例 void IAP_EraseFlashPage(uint32_t page_addr) { FLASH_Unlock(); // 解锁Flash控制寄存器 FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); // 擦除指定页,F103每次擦1KB FLASH_ErasePage(page_addr); FLASH_Lock(); // 擦除完成后重新锁定 }

注释说明:FLASH_Unlock必须写在每次擦除和写入之前,因为复位后Flash默认是锁定状态,不解锁直接操作会返回写保护错误。擦除很快,大约几十毫秒,但对串口接收来说足够丢十来帧数据了,所以上位机发送固件包的节奏一般是一帧一应答,等应答后再发下一帧。

3.4 完整升级流程:上位机与下位机的握手过程

整个串口IAP升级的时序可以分为四个阶段。阶段一是下位机进入升级等待状态后,上位机发送0x02指令查询版本,用于确认通信正常。阶段二是上位机发送擦除指令,让下位机擦除整个App区域。阶段三是逐帧发送固件数据,每帧下位机写入Flash后返回一个应答字节。阶段四是发送结束指令,下位机校验整体长度和CRC(可选),然后跳转到App。

// Bootloader主循环中的升级处理逻辑 void IAP_WaitForUpgrade(void) { IAP_Frame_t frame; uint32_t flash_write_addr = APP_START_ADDR; while (1) { // 调用串口接收解析,只有校验通过的帧才会返回1 if (IAP_ParseByte(&frame, USART_ReceiveData(USART1))) { if (frame.buf[2] == 0x00) // 写入固件数据 { IAP_ProcessDataFrame(flash_write_addr, &frame.buf[5], frame.data_len); flash_write_addr += frame.data_len; USART_SendByte(USART1, 0x01); // 应答成功 } else if (frame.buf[2] == 0x01) // 结束升级 { // 清除升级标志位,下次上电直接进App IAP_EraseFlashPage(UPGRADE_FLAG_ADDR); // 跳转到App IAP_JumpToApp(APP_START_ADDR); } } } }

这段流程是简化过的,实际工程里还要加超时判断:如果3秒内没有新帧进来,就自动复位跳转App,防止升级过程中设备断电后卡死在Bootloader。另外,每次写帧之前都要检查flash_write_addr是否超出APP_MAX_SIZE,超出就要停止接收并上报错误。

4. 测试App工程:为什么需要一个假的业务程序来验证升级

4.1 测试App的结构与LED控制逻辑

这套资源里附带的App工程,本质上是一个“用来证明升级成功”的试金石。它编译出的固件通过Bootloader烧进Flash后,会控制开发板上的LED以某个频率闪烁,并且串口会输出版本信息字符串。当你升级了一份新版App,LED闪烁频率发生变化,就说明固件确实被更新了。

// App工程中用于验证升级版本的主循环 int main(void) { NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x08008000); SystemInit(); LED_Init(); USART1_Init(115200); printf("App Version 2.0\r\n"); // 这个版本号会在升级后改变 while (1) { LED_Toggle(); // 翻转LED DelayMs(500); // 旧版延时500ms,新版改成300ms } }

这个App工程的价值不只是跑起来,它还是一个教学模板:工程里已经把启动文件、链接脚本的Flash地址改到了0x08008000,编译之前不用再手动改一堆配置。你把自己的业务代码填充到这份App骨架里,就能直接生成可被Bootloader接收的固件。注意printf要重定向到串口,标准外设库例程里一般会重写fputc函数。

4.2 生成可烧录固件:从Keil工程到bin文件

Bootloader接收的数据是纯二进制文件,也就是.bin,不是Keil生成的.hex。因为.hex是文本格式,每行有地址和校验,不适合直接按帧刷到固定偏移。在Keil里生成bin文件的方法是在After Build的User选项卡里添加一条命令。

fromelf --bin --output ..\output\app.bin ..\obj\app.axf

命令说明:fromelf是Keil自带的格式转换工具,--bin表示输出二进制格式,--output指定输出路径,最后的参数是AXF文件名。你也可以用自己写的上位机直接解析HEX文件提取数据,但那样上位机逻辑复杂很多,不建议。

4.3 测试App的修改方式:调整版本号和闪灯频率

验证升级效果不需要改业务逻辑,只需要改两个地方:把printf里的版本号从1.0改成2.0,把LED翻转延时从500ms改成300ms。编译生成新的bin,通过串口工具发送给Bootloader,完成后看到LED闪烁变快,升级就成功了。这个做法简单直观,我每次给客户演示IAP功能时,都靠这个手段当场证明新旧固件确实被切换了。

5. 避坑与常见问题排查:串口升级翻车记录

5.1 跳转App后程序跑飞,HardFault

现象:Bootloader通过串口完成升级,复位后LED不亮,串口无输出,调试器看到程序停在HardFault_Handler里。

原因:最常见的是App工程的链接地址没有改成0x08008000。Bootloader跳到了0x08008000,但App代码实际编译在0x08000000,CPU取到的第一条指令就是错的。另一个原因是没有在App里调用NVIC_SetVectorTable,虽然跳转时SP和PC都对,但一旦App里发生任何中断,CPU去0x08000000找中断向量表,找到的是Bootloader的服务函数,两个工程共用同一份向量表地址,直接冲突。

解决:打开Keil的“Options for Target”,把IROM1的Start地址改为0x08008000,Size对应减小。同时在App的main函数第一行调用NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x08008000),并且保证这行代码在初始化任何中断之前在。

5.2 升级过程中途断电,设备变砖

现象:擦除Flash时突然断电,再上电设备没有任何反应,Bootloader也不进、App也跑不了。

原因:擦除动作发生在Flash里,如果此时正好擦除了Bootloader所在的扇区(比如把App起始地址设错了,或者擦除函数参数传了0x08000000),Bootloader自身就没了,自然无法启动。还有一种情况是App区域擦到一半,标志位还没清除,上电后Bootloader认为需要升级,一直等待串口数据,但App已经不存在,看起来也是“变砖”。

解决:把升级标志位区域和Bootloader区域分开,不要放在同一个扇区。擦除App之前先检查目标地址是否大于等于0x08008000,小于就报错。更保险的方案是“先接收后擦除”:把完整固件先暂存到外部Flash或RAM(如果RAM够大),校验通过后再擦除App区并写入,这样断电不会造成中间状态。

5.3 串口能收到数据,但写入Flash失败

现象:上位机发送固件帧,下位机总返回错误应答,或者明明写成功了,跳转后运行结果不对。

原因:Flash操作前没有解锁,或者一次写入的字节数不是偶数。F103的Flash编程必须按16位半字操作,如果你发送的帧长度是奇数,最后一个字节需要补0凑成半字。另外,如果芯片有读保护(RDP),Flash写入会直接报错。

解决:在每次FLASH_ProgramHalfWord前调用FLASH_Unlock,写完后FLASH_Lock。处理奇数长度帧时,在数据末尾补一个0x00。在调试器里查看FLASH->CR寄存器的PG位和LOCK位,确认解锁是否成功。读保护可以用烧录器解除,但它会先全片擦除,所以量产设备升级时一定要关掉读保护,或者用支持在保护状态下写入的专用方案。

5.4 上位机发送正常,Bootloader却收不到完整帧

现象:串口助手里能看到数据在发,但下位机一直没有应答,或者偶尔能收到一次,经常卡住。

原因:Bootloader的串口接收缓冲区溢出。Bootloader主循环一边擦Flash一边写,擦除过程中CPU被阻塞,串口中断没法及时把新到的字节搬走,硬件FIFO溢出后后续字节直接丢弃。

解决:一是降低波特率到9600或19200,延长单字节传输时间;二是改为“边收边写”策略,每收到一帧就立即擦写,擦写前关串口中断,擦写完再打开,同时上位机等待应答再发下一帧;三是在主循环里用查询方式轮询串口状态寄存器,避免中断被屏蔽时丢字节。

5.5 App跑起来后,第一次串口中断就死机

现象:Bootloader跳转成功,App的LED亮起来了,但只要串口收发一个字节,程序就跳到HardFault。

原因:这是典型的向量表没有重映射。App在main开始就初始化串口并使能中断,但此时NVIC还在用Bootloader的向量表偏移量,中断信号来了CPU去0x08000000查向量表,找到的地址是Bootloader的USART1_IRQHandler,但这个函数在App工程里并没有真正定义(或者地址不对),跳转过去就是非法地址。

解决:在App的main第一行加NVIC_SetVectorTable,并且在跳转前通过Bootloader禁用全局中断,App初始化完后再打开。如果Bootloader的跳转函数里没有关闭中断,建议从Jump代码到App启动这段路径上全部屏蔽中断,最稳妥的办法是在跳转前调用__disable_irq(),App的SystemInit后调用__enable_irq()。

6. 二次开发:把这份IAP改造成你的量产升级方案

拿到这份代码后,不要直接拿来做产品,先做你自己的适配。第一步是调整Flash分区:查看你的芯片型号,如果Flash总容量是64KB,Bootloader还是建议32KB起,App区域剩下32KB;如果是128KB,Bootloader可以压缩到16KB,给App留下更多空间。分区调整后,需要同步修改Bootloader里的APP_START_ADDR和App工程的链接脚本,两边的值必须一字不差。

第二步是升级传输协议。这套源码用的是串口直连,但实际产品往往通过WiFi模块或者CAN总线转发升级数据。我的做法是把协议解析层和传输层完全分离:串口接收直接映射到一个环形队列接口,WiFi接收的数据写入同一个队列,这样IAP解析函数不用改动。你只需要实现一个UART_SendByte的替代函数,把数据送到你的WiFi透传通道即可。

第三步是加校验。现在只有累加和,建议升级为CRC32,哪怕是个简化版。在结束升级指令里带上整个固件的CRC值,Bootloader写完最后一段后重新读Flash计算CRC,比对不一致就回滚到旧版本,重新进入等待升级状态。具体做法是在App区末尾保留两个向量:一个是“最新有效固件CRC”,一个是“回滚地址”,写失败时跳回上一个版本。

// 二次开发时加入的整体校验伪代码 uint32_t crc = 0; uint32_t flash_size = IAP_GetAppSize(); // 从固件头部读取App实际长度 // 结束后校验 for (uint32_t addr = APP_START_ADDR; addr < APP_START_ADDR + flash_size; addr += 4) { crc = IAP_CRC32(crc, *(volatile uint32_t *)addr); } if (crc != expect_crc) { // 校验失败,重新擦除并等待接收,或者回滚到备份区 IAP_EraseFlashPage(APP_START_ADDR); IAP_WaitForUpgrade(); }

这段逻辑适合放在结束指令的处理分支里,此时所有数据都写完了,重新读Flash的耗时虽然多,但能够发现“写入中途Flash异常”这类数据区不可见的问题。我一般还会加一个“最大重试次数”,上位机连续发送三次都校验失败就停止升级,防止一直擦写把Flash寿命耗尽。

第四步是处理掉电恢复。上一节提到的现象已经证明,中间态断电是量产升级最怕的事。我习惯的改进方案是A/B面互备:把App区分成A区和B区,Bootloader先向B区写新固件,校验成功后再把跳转地址切到B区。这样升级过程中A区始终是完整可跑的,即使B区写坏了,Bootloader自动回退到A区。代价是App可用空间减半,但对于Flash容量充足的芯片来说,这是最稳的路子。

验证改造效果时,不要只用LED闪灯,我建议做一轮“异常注入测试”:在升级过程中手动拔掉串口线、直接断电、发送损坏的帧。每一次异常后重新上电,设备都应该要么回到旧版本继续工作,要么停留在Bootloader等待重新升级,绝不应该出现“卡死、无响应、无法再次升级”的状态。这个测试通过后,你的IAP才算能上产线。

我最初做IAP时也因为跳转没关中断、向量表没重映射这类问题翻过车,从那以后我每次改完工程都会先检查三处:App链接地址、NVIC重映射、跳转前的全局中断状态。这三个点任何一处错了,表面症状都是“程序跑飞”,但根因完全不同。这套检查流程我建议你也固化下来,能省下大量调试时间。希望你能拿这份源码顺利跑通,然后把它改造成真正属于你自己的可靠升级方案。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 1:55:46

OpenAI API 身份验证上手指南:从 API Key 到 OAuth 2.0 的正确选择

OpenAI API 身份验证上手指南&#xff1a;从 API Key 到 OAuth 2.0 的正确选择 【免费下载链接】openai-openapi OpenAPI specification for the OpenAI API 项目地址: https://gitcode.com/GitHub_Trending/op/openai-openapi 第一次执行 curl 请求&#xff0c;返回了 …

作者头像 李华
网站建设 2026/10/21 1:54:51

STM32F103串口IAP升级:Bootloader与APP完整实现

简介&#xff1a;基于意法半导体STM32F103微控制器的串口应用内编程升级程序完整原创开源包&#xff0c;面向嵌入式开发者&#xff0c;解决设备运行状态下经串口更新固件、避免拆机烧录与依赖专用编程器的问题。工程实现了从引导程序、串口通信到闪存擦写、循环冗余校验、非法固…

作者头像 李华
网站建设 2026/10/21 1:54:26

Linux安全提权:普通用户执行root脚本的sudo与setuid全解析

我是一个在服务器上折腾了七八年的老运维&#xff0c;今天想好好聊聊 Linux 权限管理里一个非常实用、但也经常被搞出安全问题的操作&#xff1a;如何让普通用户安全执行 root 权限的特定脚本。很多人一遇到“普通用户要执行管理员操作”的需求&#xff0c;第一反应就是给账号加…

作者头像 李华
网站建设 2026/10/21 1:52:38

微信小游戏跑Unity WebGL游戏:完整适配方案与快速上手指南

微信小游戏跑Unity WebGL游戏&#xff1a;完整适配方案与快速上手指南 【免费下载链接】minigame-unity-webgl-transform 微信小游戏Unity引擎适配器文档。 项目地址: https://gitcode.com/GitHub_Trending/mi/minigame-unity-webgl-transform 手里有个 Unity 游戏项目&…

作者头像 李华