新闻详情

新闻详情

首页 / 资讯中心 / 详情

IAP 升级入门:从原理到实现,手把手搞懂单片机在线升级(入门篇)

发布时间:2026/9/29 20:13:03来源:尧图网络
IAP 升级入门:从原理到实现,手把手搞懂单片机在线升级(入门篇)
做嵌入式的同学一定听过 IAP但很多人一开始搞不明白程序不是用烧录器下载的吗怎么自己给自己升级本文从最基础的原理讲起用最直白的方式讲清楚 IAP 到底是什么、为什么能实现、怎么实现。读完你就能自己写一个最简单的 Bootloader。 什么是 IAPIAP In-Application Programming在应用中编程通俗说就是单片机自己给自己烧程序。正常情况下我们用 ST-Link、J-Link 这种烧录器把程序下载到单片机里。但产品卖出去之后总不能让客户把壳拆了、接个烧录器升级吧IAP 就是解决这个问题的产品已经在用户手里了也能通过串口/网络/蓝牙等方式升级固件。 一、IAP 的核心原理两个程序分工IAP 能实现的核心秘密是Flash 里同时放着两个程序。Bootloader引导程序上电第一个运行的小程序负责判断「我是直接跳去 App还是先升级固件」App应用程序用户的主程序也就是正常功能的固件一句话理解Bootloader 是「烧录工具」App 是「被烧的程序」。只不过这个烧录工具也在芯片里面上电先运行它。上电后发生了什么单片机上电 ↓ 运行 Bootloader ↓ 判断需要升级吗 ├─ 需要 → 接收新固件 → 写到 App 区 → 跳去 App └─ 不需要 → 直接跳去 App就这么简单。Bootloader 就是个「入口保安」决定让新固件进来还是直接放行去主程序。 二、Flash 分区地址怎么分Bootloader 和 App 都在 Flash 里怎么区分靠地址划分。Bootloader放在 Flash 最开头从0x08000000开始占 16KBApp紧跟在后面从0x08004000开始占剩下的 240KB为什么 Bootloader 要放最开头因为 Cortex-M 内核上电后默认从 Flash 起始地址0x08000000取中断向量表然后从复位向量开始执行。所以 Bootloader 必须在最前面。Bootloader 工程的地址设置分好区之后两个工程Bootloader 和 App的链接地址要分别设置不能都从0x08000000开始否则会重叠。Bootloader 工程保持在 Flash 最开头不用改。以 STM32 Keil 为例IROM1 Start: 0x08000000, Size: 0x4000 (16KB)Start 0x08000000Flash 的起始地址Bootloader 从这里开始Size 0x400016KBBootloader 自己能用的空间大小⚠️关键点这里的Size不能随便写它必须和 App 的起始地址对得上。Bootloader 占 16KB0x4000App 就必须从0x08000000 0x4000 0x08004000开始。两边一旦对不上跳转就会跑飞。Bootloader 大小怎么定先按经验给个值比如 16KB 或 32KB把功能写完编译一下看看实际占了多少再留 20%~30% 余量。Bootloader 功能越多加密、双备份、协议栈占的空间越大。App 工程的地址设置和 Bootloader 是配套的我们在第四节详细讲。 三、Bootloader 怎么跳转到 App这是 IAP 最核心的操作之一Bootloader 执行完了怎么跳到 App 去运行跳转的原理Cortex-M 的启动过程是这样的从 Flash 起始地址读第 1 个 4 字节 → 作为栈顶地址MSP从 Flash 起始地址读第 2 个 4 字节 → 作为复位中断的入口地址设置 MSP然后跳到复位中断地址执行Bootloader 跳转到 App本质上就是模拟这个启动过程。只不过 App 的中断向量表不是从0x08000000开始而是从0x08004000开始。所以我们要从 App 的起始地址读栈顶地址设置 MSP从 App 的起始地址读复位向量跳过去跳转代码STM32 示例// App 的起始地址根据你的分区来改#defineAPP_START_ADDR0x08004000typedefvoid(*pFunction)(void);// 函数指针类型voidJumpToApp(void){pFunction Jump_To_Application;uint32_tJumpAddress;// 1. 检查 App 是否有效栈顶地址应该在 RAM 范围内// STM32F103 的 RAM 地址是 0x20000000 ~ 0x20005000if(((*(__IOuint32_t*)APP_START_ADDR)0x2FFE0000)0x20000000){// 2. 获取 App 的复位中断向量地址App 起始地址 4 字节JumpAddress*(__IOuint32_t*)(APP_START_ADDR4);Jump_To_Application(pFunction)JumpAddress;// 3. 设置 App 的栈顶地址__set_MSP(*(__IOuint32_t*)APP_START_ADDR);// 4. 跳转Jump_To_Application();}}为什么要检查栈顶地址因为如果 App 区是空的全是 0xFF读出来的值就是 0xFFFFFFFF这显然不是一个合法的栈地址。这时候跳过去就是跑飞所以先检查一下不合法就不跳。跳转前别忘了做清理跳转到 App 之前Bootloader 要把自己用过的硬件都复位干净不然 App 运行起来可能出问题关闭所有中断NVIC关闭所有外设串口、SPI、GPIO 等把系统时钟复位到默认状态清除所有中断挂起位否则 App 启动后可能会遇到莫名其妙的中断或者外设异常。 四、App 程序要改什么App 程序不能直接用原来的工程需要改两个地方起始地址和中断向量表偏移。1. 修改 App 的链接地址以 STM32 Keil 为例原来的设置App 从 0x08000000 开始IROM1 Start: 0x08000000, Size: 0x40000 (256KB)改完之后App 从 0x08004000 开始IROM1 Start: 0x08004000, Size: 0x3C000 (240KB)也就是把 Flash 的前 16KB0x4000让给 BootloaderApp 从后面开始。2. 设置中断向量表偏移Cortex-M 默认从 Flash 起始地址0x08000000取中断向量。但 App 的中断向量表在0x08004000所以要告诉 CPU「我的向量表不在默认位置偏移了 0x4000」。在 App 的main()函数最开头加一行// 设置中断向量表偏移从 Flash 偏移 0x4000SCB-VTORFLASH_BASE|0x4000;或者用 ST 标准库 / HAL 库的函数// HAL 库HAL_NVIC_SetVectorTable(NVIC_VectTab_FLASH,0x4000);// 标准库NVIC_SetVectorTable(NVIC_VectTab_FLASH,0x4000);⚠️这一步非常重要不改的话App 运行时触发中断会跑到 Bootloader 的向量表去结果就是莫名其妙的崩溃或者死机。 五、怎么升级接收固件写到 App 区前面讲了 Bootloader 怎么跳 App现在讲升级的核心怎么把新固件写进 App 区基本思路Bootloader 等待接收固件数据比如通过串口收到一包数据就往 App 区对应的地址写全部写完后校验一下对不对校验通过跳转到新的 App传输方式可以是串口、CAN、网络等等原理都一样一边收数据一边往 Flash 里写。为了把原理讲透这里我们用一个最简单的自定义协议来演示。简单自定义协议设计每一包的格式如下┌──────────┬──────────┬─────────────┬──────────┐ │ 包头 │ 包序号 │ 数据内容 │ 校验和 │ │ (2字节) │ (2字节) │ (N字节) │ (1字节) │ └──────────┴──────────┴─────────────┴──────────┘包头固定值比如0xAA 0x55标记一包的开始方便接收端对齐包序号从 0 开始递增用来发现丢包、重复包数据内容真正的固件数据比如每包 1KB校验和对这包数据做校验防止传输出错 这就是所有传输协议的核心套路包头 序号 数据 校验。不管哪种协议本质都是这个结构只是细节更复杂。升级流程Bootloader 启动 ↓ 检测有没有升级请求 ├─ 有 → 进入升级模式 └─ 没有 → 直接跳 App 升级模式 ① 等待上位机发送固件数据包 ↓ ② 收到一包 → 检查包头对不对 ├─ 不对 → 丢弃继续等 └─ 对 → 继续 ↓ ③ 校验这包数据校验和 ├─ 失败 → 回 NACK要求重发 └─ 成功 → 继续 ↓ ④ 把数据写到 Flash 的 App 区按包序号算好偏移地址 ↓ ⑤ 回 ACK告诉上位机这包收到了发下一包 ↓ ⑥ 重复 ②-⑤直到收完所有数据 ↓ ⑦ 校验整个 App 区的固件CRC 校验 ├─ 通过 → 跳转 App └─ 失败 → 留在 Bootloader等重新升级包序号有什么用包序号不只是用来排序它还能解决两个实际问题丢包检测收到序号 5下一包直接来了序号 7说明序号 6 丢了 → 要求重发断点续传的基础如果中途断电重启Bootloader 可以从上次记录的序号继续接收不用从头再来 这就是「断点续传」的思路——记录已收到的包序号或字节偏移量重启后接着传。这个后面单独讲。接收一包数据的代码框架// 每包数据的最大长度#definePKG_DATA_MAX1024// 固件写入的起始地址#defineAPP_START_ADDR0x08004000// 处理一包固件数据成功返回 0失败返回 -1inthandle_firmware_pkg(uint8_t*pkg,uint16_tpkg_len){// 1. 检查包头if(pkg[0]!0xAA||pkg[1]!0x55){return-1;// 包头不对丢弃}// 2. 取出包序号和数据长度uint16_tpkg_indexpkg[2]|(pkg[3]8);uint16_tdata_lenpkg_len-5;// 减去 包头2 序号2 校验1// 3. 校验这包数据累加和取低 8 位uint8_tchecksum0;for(uint16_ti0;ipkg_len-1;i){checksumpkg[i];}if(checksum!pkg[pkg_len-1]){return-1;// 校验失败要求重发}// 4. 按包序号算出这包要写到 Flash 的哪个地址uint32_twrite_addrAPP_START_ADDR(uint32_t)pkg_index*PKG_DATA_MAX;// 5. 写入 Flashflash_write(write_addr,pkg[4],data_len);return0;// 成功}地址怎么算的包序号 × 每包长度 App 起始地址。第 0 包写到0x08004000第 1 包写到0x080044000x400 1024。每包都有固定位置天然支持乱序和重传。写 Flash 的注意事项写 Flash 不是像写 RAM 那样直接赋值要按 Flash 的操作流程来解锁 FlashFlash 写操作默认是锁着的要先解锁擦除页/扇区Flash 不能直接改必须先擦除变成全 0xFF才能写按字/半字写入一次写 32 位或 16 位要看具体芯片等待操作完成检查 FLASH_SR 寄存器的 BSY 位锁定 Flash写完再锁上防止误写// 伪代码往指定地址写数据voidflash_write(uint32_taddr,uint8_t*data,uint32_tlen){// 1. 解锁 FlashFLASH_Unlock();// 2. 擦除要写的页如果需要// ... 根据地址计算页号然后 FLASH_ErasePage()// 3. 按字写入for(uint32_ti0;ilen;i4){uint32_tword*(uint32_t*)(datai);FLASH_ProgramWord(addri,word);}// 4. 锁定 FlashFLASH_Lock();}⚠️注意擦除操作是按页Page或者扇区Sector来的不能只擦一个字节。所以写入前要先算好地址在哪一页别把不该擦的内容擦掉了。 六、Bootloader 怎么判断「要不要升级」前面讲了怎么收包、怎么跳转但还有个关键问题没解决Bootloader 上电后怎么知道这次是要升级还是直接跑 App这个判断逻辑是 IAP 的「大脑」。下面讲三种最常用的方式从简单到复杂。方式一按键强制升级最简单上电时如果检测到某个按键被按住就进入升级模式否则直接跳 App。上电 / 复位 ↓ 检测按键是否按下比如 PA0 拉低 ├─ 按下 → 进入升级模式等上位机发数据包 └─ 没按 → 直接跳 App// 检查是否要进入升级模式返回 1 表示要升级0 表示不升级intCheckUpgradeRequest(void){// 上电后先延时一下等电平稳定顺便做消抖Delay_ms(50);// 按键按下是低电平表示要强制升级if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)Bit_RESET){return1;}return0;}优点简单可靠不依赖 App 和上位机。就算 App 已经跑不起来也能救回来。缺点需要人工操作用户得能碰到设备、知道按哪个键。适合调试和售后返修。 实际产品里这个方式几乎都会保留一份作为最后的兜底手段。方式二App 收到升级请求 → 记录标志 → 重启进 Bootloader最常用这是量产产品最常用的方式。核心思路是App 正常运行时收到上位机的升级指令把「要升级」这个标志记下来然后主动重启Bootloader 上电检测到这个标志就知道该升级了。① App 正常运行中 ↓ ② App 收到上位机的升级请求串口 / 4G / WiFi 等 ↓ ③ App 把「要升级」标志写到 RAMno-init 区或备份寄存器 ↓ ④ App 调用 NVIC_SystemReset() 主动重启 ↓ ⑤ 重启后先跑 Bootloader ↓ ⑥ Bootloader 检查标志位 ├─ 标志有效 → 进入升级模式 → 回复上位机「开始升级」 │ → 收包 → 写 Flash → 校验 → 跳转新 App └─ 标志无效 → 直接跳 App关键在第三步和第六步标志存在哪、怎么保证重启后还能读到标志存在哪里有两种常见做法。做法 Ano-init RAM 区简单最常用C 语言里全局变量默认会被启动代码清零所以不能直接用普通变量。要在链接脚本里专门划一块「不初始化」的 RAM 区// 约定的标志值随便定只要别和随机值撞上#defineUPGRADE_MAGIC0x5A5A1234// 放在不初始化的 RAM 段重启后内容保留// Keil 里通过 scatter file 指定到 no-init 段__attribute__((section(.noinit),zero_init))uint32_tupgrade_flag;// App 里调用请求升级voidApp_RequestUpgrade(void){upgrade_flagUPGRADE_MAGIC;// 1. 写标志__DSB();// 2. 确保写入完成NVIC_SystemReset();// 3. 主动重启}// Bootloader 里调用检查标志intCheckUpgradeRequest(void){if(upgrade_flagUPGRADE_MAGIC){upgrade_flag0;// 清掉标志防止下次误判return1;// 要升级}return0;// 不升级}为什么要用 no-init 区因为软件复位NVIC_SystemReset不会清 RAM内容还在。但启动代码会主动把.bss/.data段清零所以标志必须放在不被清零的段里才能活过重启。做法 B备份寄存器BKP / RTC Backup RegisterSTM32 有一组备份寄存器BKP_DRx由 VBAT 供电连掉电都能保住// App 里调用请求升级voidApp_RequestUpgrade(void){RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR|RCC_APB1Periph_BKP,ENABLE);PWR_BackupAccessCmd(ENABLE);BKP_WriteBackupRegister(BKP_DR1,UPGRADE_MAGIC);// 写备份寄存器NVIC_SystemReset();// 主动重启}// Bootloader 里调用检查标志intCheckUpgradeRequest(void){RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR|RCC_APB1Periph_BKP,ENABLE);PWR_BackupAccessCmd(ENABLE);if(BKP_ReadBackupRegister(BKP_DR1)UPGRADE_MAGIC){BKP_WriteBackupRegister(BKP_DR1,0);// 清标志return1;// 要升级}return0;// 不升级}两种做法的区别no-init RAM 简单但只在软件复位 / 看门狗复位时有效真断电就没了备份寄存器由 VBAT 供电掉电也能保住更适合「升级中途断电、重启后继续升级」的场景。按需求选。优点全自动用户无感App 里点个「检查更新」就行适合远程 OTA。缺点依赖 App 正常工作。如果 App 已经跑不起来上次升级失败、程序跑飞这条路就走不通了——所以还得留个兜底方式一。方式三上电后短暂等待升级指令折中每次上电Bootloader 先等一小段时间比如 3 秒看看上位机有没有发升级指令上电 ↓ Bootloader 等待 3 秒同时监听串口 ├─ 3 秒内收到升级指令 → 进入升级模式 └─ 3 秒内没收到 → 直接跳 AppintCheckUpgradeRequest(void){// 等待 3 秒看串口有没有收到升级指令if(UART_WaitForCmd(3000)CMD_UPGRADE){return1;}return0;}优点不用按按键也不用改 App上位机随时能触发。缺点每次上电都要多等几秒影响开机速度。适合调试阶段或对开机时间不敏感的产品。三种方式怎么选方式触发方式需要 App 配合掉电可用适用场景按键强制人工按键否是兜底救砖、调试、返修App 触发上位机指令是看标志存哪量产远程 OTA上电等待上位机指令否是调试阶段实战建议方式一 方式二 组合。平时用 App 触发做远程升级用户无感万一 App 挂了还能按住按键上电救回来。两个搭配起来基本覆盖所有情况。 七、完整的 Bootloader 主逻辑把上面的内容合起来Bootloader 的main函数大概长这样intmain(void){// 1. 初始化系统时钟、GPIO、串口SystemClock_Config();UART_Init();// 2. 检查升级条件比如某个引脚被拉低或串口收到特定指令if(CheckUpgradeRequest()){// 3. 进入升级模式循环接收固件数据包if(Receive_Firmware()OK){// 4. 全部接收完校验整个 App 区if(CheckAppCRC()OK){// 5. 校验通过跳转到新 AppJumpToApp();}}// 升级失败停在 Bootloader等待重新升级while(1){// 可以闪个灯提示升级失败}}else{// 6. 不需要升级直接跳 AppJumpToApp();}}其中Receive_Firmware()就是不断收包、调handle_firmware_pkg()写 Flash 的过程// 接收完整固件成功返回 OK失败返回 ERRORintReceive_Firmware(void){uint8_tpkg[PKG_DATA_MAX5];while(1){// 1. 读一包数据超时和长度检查在底层完成uint16_tlenUART_ReadPkg(pkg,sizeof(pkg));if(len0){returnERROR;// 超时或读失败}// 2. 数据长度为 0 的结束包表示传输结束if(len5){returnOK;}// 3. 处理这一包if(handle_firmware_pkg(pkg,len)!0){UART_SendNack();// 校验失败要求重发}else{UART_SendAck();// 成功请求下一包}}}整个升级流程就串起来了收包 → 校验 → 写 Flash → 回 ACK → 下一包 → 收完 → 整体校验 → 跳转。 总结IAP 的原理其实很简单核心就四件事Flash 分区Bootloader 在前App 在后两个工程的地址要分别设置且互相对得上跳转机制Bootloader 读完 App 的向量表设置 MSP跳复位向量升级判断上电后靠按键、标志位no-init RAM / 备份寄存器或等待指令决定是升级还是直接跑 App在线写入通过串口或其他方式接收新固件擦写 Flash 的 App 区搞懂了这四点IAP 就算入门了。后面的断点续传、双备份分区、回滚、加密签名等等都是在这个基础上不断叠加的高级功能。 下一篇预告本篇讲了最基础的 IAP 原理和实现。下一篇我们会讲怎么做到升级中途断电不砖双分区备份方案断点续传怎么实现固件加密和签名防篡改如果觉得有用点赞 收藏 关注不迷路 有问题欢迎评论区交流本文为原创内容转载请注明出处。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

TaoToken 配置实战:用 McEval 多语言代码评测基准验证 40 种编程语言模型能力 2026/9/29 21:30:38

TaoToken 配置实战:用 McEval 多语言代码评测基准验证 40 种编程语言模型能力

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
GitHub开源项目日报 · 2026年3月18日 · 开源AI生态领跑榜单:TaoToken统一Key接入编码代理配置指南 2026/9/29 21:30:31

GitHub开源项目日报 · 2026年3月18日 · 开源AI生态领跑榜单:TaoToken统一Key接入编码代理配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Python 工匠(one-python-craftsman):让函数返回结果的 7 个实战技巧 2026/9/29 21:30:31

Python 工匠(one-python-craftsman):让函数返回结果的 7 个实战技巧

技术博客教程文档 【免费下载链接】one-python-craftsman 来自一位 Pythonista 的编程经验分享,内容涵盖编码技巧、最佳实践与思维模式等方面。 项目地址: https://gitcode.com/gh_mirrors/on/one-python-craftsman 点击查看 免费下载 函数是 Python 语…

阅读更多 →
Vibe Coding 实战:Claude Code 记忆系统与上下文压缩配置指南(含 TaoToken 接入) 2026/9/29 21:30:31

Vibe Coding 实战:Claude Code 记忆系统与上下文压缩配置指南(含 TaoToken 接入)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
技术速递|GitHub Copilot App 堆叠会话与拉取请求配置实战 2026/9/29 21:30:05

技术速递|GitHub Copilot App 堆叠会话与拉取请求配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
AI Coding 实战:接手屎山代码后,我用 Claude Code + TaoToken 重建了可维护的配置骨架 2026/9/29 21:30:05

AI Coding 实战:接手屎山代码后,我用 Claude Code + TaoToken 重建了可维护的配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉