
1. 什么是IAP和ICP?
在单片机开发中,程序固化烧录和在线更新主要分为以下两种方式:
ICP (In-Circuit Programming) ------ 在电路编程
使用专用的烧录器(如 ST-Link、J-Link、DAP-Link)通过 SWD 或 JTAG 接口将程序强制写入 MCU 的内部 Flash。
应用场景:研发调试阶段、工厂出厂首次烧录、Bootloader 的初始注入。
IAP (In-Application Programming) ------ 应用内编程
MCU 内部已经运行了一段引导程序(Bootloader),通过各种通信接口(UART、USB、CAN、Wi-Fi、蓝牙等)接收新固件,并由程序自身擦写 Flash 的指定区域完成升级。
应用场景:产品发布后的远程 OTA 升级、售后免拆壳维护、现场固件修复。
一句话总结:ICP 是"外挂"烧录,IAP 是"自举"升级。
2. 前期准备工作(核心基础)
实现 IAP 升级前,必须理清两个核心问题:
-
Flash 空间怎么划分?
-
第一个程序怎么进去?
2.1 Flash 内存布局划分(以 512KB Flash 为例)
为了实现安全、可靠的升级,必须将 MCU 的 Flash 划分为不同的区域,各司其职。以下是一个典型的双分区备份布局:
| 区域名称 | 起始地址 | 占用大小 | 作用与内容 |
|---|---|---|---|
| Bootloader区 | 0x08000000 | 64 KB | 引导加载程序,负责检查升级标志、校验固件、擦写搬运、跳转。(通过 ICP 烧录,后续不再更改) |
| 标志/配置区 | 0x08010000 | 4 KB | 存放固件版本号、大小、CRC 校验码、升级状态标志位(如 Update_Flag)以及一些用户配置参数 |
| APP 运行区(Slot 0) | 0x08011000 | 220 KB | 用户正常工作的业务逻辑程序(当前运行的旧固件) |
| APP 备份区(Slot 1) | 0x08048000 | 220 KB | 用于在 APP 运行期间通过网络或串口下载新固件(待升级的固件) |
注意:扇区大小取决于具体 MCU 型号(如 STM32F1 系列扇区大小为 1KB 或 2KB,STM32F4 系列有大扇区)。布局时需考虑擦除对齐,建议使用"扇区"而非任意地址。
2.2 前置 ICP 烧录 ------ 核心结论
IAP 的前提:先通过 ICP 注入"灵魂" Bootloader。
在产品量产或开发初期,必须先用 ST-Link/J-Link 等工具,将编译好的 Bootloader 固化烧录到 0x08000000 起始地址。
只有这一步完成后,MCU 才具备了通过串口、无线等方式进行自我升级的能力。
如果没有 Bootloader,芯片上电后只会运行原有的 APP,无法响应任何升级指令。
3. IAP 升级全流程状态机解析
一个严谨的 IAP 双分区升级流程,通常包含以下四个阶段。下图是状态转移的简化描述:
cpp
[APP运行] --(收到升级指令)--> [下载新固件到备份区] --(校验成功)--> [置位升级标志] --(软复位)-->
[Bootloader启动] --(检测到标志)--> [校验备份区固件] --(成功)--> [擦除运行区 & 搬移] --(校验通过)-->
[清除标志] --> [跳转到新APP运行]
3.1 第一阶段:APP 运行中(下载阶段)
-
下发通知:上位机/云平台向当前运行的 APP 发送升级指令(包含新固件版本、大小、MD5/CRC 等信息)。
-
分包下载 :APP 通过通信接口(如 UART + Ymodem、TCP/MQTT、HTTP 分块)分片接收新固件,每收到一包就写入 APP 备份区(Slot 1)。
-
完整性校验:接收完毕后,APP 对备份区固件计算 CRC32 或哈希,与上位机下发的校验值比对。
-
写入升级标志 :校验无误后,在标志/配置区 写入升级请求标志(例如
Update_Flag = 0x5A5A),并记录新固件大小、CRC 值。 -
软重启 :APP 调用内核指令
NVIC_SystemReset()触发系统复位。
3.2 第二阶段:Bootloader 引导(校验阶段)
-
上电复位 :MCU 硬件强制从
0x08000000启动,首先运行 Bootloader。 -
读取标志 :Bootloader 检查
Update_Flag是否等于0x5A5A:-
若不等于:说明无升级请求,直接跳转到 APP 运行区(Slot 0)。
-
若等于:说明有升级请求,进入升级流程。
-
-
完整性二次校验:Bootloader 对备份区(Slot 1)的固件重新执行 CRC32 校验,并与标志区保存的校验值比对:
-
校验失败 :说明备份区固件损坏(比如下载过程中断电),则清除升级标志,报错(如 LED 闪烁),并跳回原 APP 区,防止设备变砖。
-
校验成功:进入下一阶段。
-
3.3 第三阶段:固件覆盖(更新阶段)
-
擦除旧固件 :Bootloader 擦除整个 APP 运行区(Slot 0)的全部内容(注意扇区大小,避免误擦 Bootloader 或标志区)。
-
逐块搬移 :将备份区的新固件从
Slot 1逐字节(或逐字)复制到运行区Slot 0。- 为了提高效率,可按页/扇区循环复制,并开启看门狗喂狗防止擦写超时复位。
-
最终校验:搬移完成后,对运行区的新固件再次进行 CRC32 校验,确保搬移过程中 Flash 没有发生位翻转或写入错误。
-
清除标志 :校验通过后,将标志区的
Update_Flag改为0x0000(或擦除整个标志区),防止下次重启重复执行升级。
3.4 第四阶段:跨程序跳转(执行阶段)
Bootloader 清理自身外设环境,关闭中断,然后通过修改堆栈指针(SP) 和程序计数器(PC),直接跳转到 APP 运行区(Slot 0)的首地址,开始执行新程序。
4. 核心源码实现与深度避坑
4.1 避坑核心一:APP 的中断向量表偏移(VTOR)
由于 Bootloader 占用了默认的中断向量表位置 0x08000000,而 APP 的起始地址偏移到了 0x08011000。如果不重新配置向量表偏移,APP 一旦发生任何中断(如定时器、串口),CPU 会错误地跳转到 Bootloader 的中断服务函数中,导致程序跑飞或死机。
解决方案 :在 APP 工程的 main() 函数最开始 处,重新设置中断向量表偏移寄存器 SCB->VTOR。
cpp
int main(void)
{
/* 1. 必须首先设置中断向量表偏移(与 APP 的起始地址一致) */
/* 假设 APP 偏移量为 0x11000 */
SCB->VTOR = FLASH_BASE | 0x11000; // FLASH_BASE 通常为 0x08000000
/* 2. 再初始化 HAL 库和系统时钟 */
HAL_Init();
SystemClock_Config();
/* 3. 初始化其它外设... */
MX_USART1_Init();
// 后续业务代码...
while(1) {}
}
注意 :如果使用 Keil MDK 或 IAR,也可以在链接脚本中指定向量表偏移,并在编译时设置
VECT_TAB_OFFSET宏,但最稳妥的方式还是在代码中显式写入。
4.2 避坑核心二:跳转前的"战场清理"
从 Bootloader 跳转到 APP 时,Bootloader 可能已经初始化了一些外设(如串口、定时器、DMA)并开启了全局中断。此外,SysTick 定时器也可能正在计数。如果带着这些活跃的中断直接跳转,APP 在初始化过程中(尤其是刚设置完 VTOR 但还未配置好所有中断处理函数时)极易被意外中断打断,导致 Hard Fault 或死机。
标准清理与跳转函数实现(适用于 STM32,HAL 库):
cpp
typedef void (*pFunction)(void); // 定义函数指针类型
/**
* @brief 从 Bootloader 跳转到 APP
* @param AppAddr APP 的起始地址(如 0x08011000)
*/
void Bootloader_JumpToApp(uint32_t AppAddr)
{
/* 1. 检查栈顶指针是否合法(判断 APP 起始地址处的 RAM 地址是否在有效范围内) */
/* STM32 的 RAM 起始地址通常为 0x20000000,大小视芯片而定 */
if (((*(__IO uint32_t*)AppAddr) & 0x2FFE0000) == 0x20000000)
{
/* --- 2. 严谨的战场清理 --- */
__disable_irq(); // 关闭全局中断
/* 关闭所有已开启的外设(根据实际使用情况,这里列出常见外设) */
HAL_UART_DeInit(&huart1); // 例:关闭串口1
HAL_TIM_Base_DeInit(&htim2); // 例:关闭定时器2
HAL_DMA_DeInit(&hdma_usart1_rx); // 关闭 DMA
// ... 可根据需要增加其它外设的 DeInit
/* 复位 RCC 时钟配置(将时钟恢复为默认状态) */
HAL_RCC_DeInit();
/* 关闭 SysTick 定时器并清除其计数器和标志 */
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL = 0;
/* 清除所有 NVIC 挂起的中断标志位(清除 pending 状态) */
for (uint8_t i = 0; i < 8; i++) // 假设最多8个NVIC中断组,实际可改用 NVIC_GetPendingIRQ 循环
{
NVIC->ICER[i] = 0xFFFFFFFF; // 关闭所有中断使能
NVIC->ICPR[i] = 0xFFFFFFFF; // 清除所有挂起位
}
/* 开启存储器屏障,保证上面操作完成 */
__DSB();
__ISB();
/* --- 3. 执行跳转 --- */
/* 获取 APP 的复位向量地址(AppAddr + 4 存放的是 Reset_Handler 的地址) */
uint32_t JumpAddress = *(__IO uint32_t*)(AppAddr + 4);
pFunction JumpToApplication = (pFunction)JumpAddress;
/* 设置 APP 的主堆栈指针(APP 起始地址处存放的是栈顶指针 SP) */
__set_MSP(*(__IO uint32_t*)AppAddr);
/* 也可以使用 __set_PSP 如果 APP 使用进程栈,通常用 MSP 即可 */
/* 正式跳转到 APP 的复位函数 */
JumpToApplication();
}
else
{
/* 报错提示:APP 固件不合法,栈顶指针异常 */
Error_Handler();
}
}
为什么需要检查栈顶指针 ?合法的 APP 固件第一个 32 位必须是一个指向 RAM 地址的栈顶值(例如
0x2000xxxx),如果该值异常,说明 Flash 中没有有效的 APP,不能跳转。
4.3 避坑核心三:APP 与 Bootloader 共用外设的中断处理
如果 Bootloader 和 APP 都使用了同一个外设(比如串口1),且 Bootloader 在跳转前没有完全去初始化该外设,APP 重新初始化时可能会发生冲突。
建议 :在 Bootloader 跳转前,调用对应外设的 DeInit 函数,并复位该外设的时钟 (通过 RCC 寄存器)。更彻底的做法是在跳转前执行一次系统软复位(但是软复位后会再次进入 Bootloader,所以不适用于标准 IAP)。通常,遵循上面的"战场清理"即可。
5. 工程鲁棒性建议(产品级必备)
一个健壮的 IAP 方案不仅要跑通主流程,还必须考虑各种异常和极端情况。
5.1 断电保护 ------ 双分区备份的价值
-
场景:在擦除运行区或搬移固件的过程中突然断电。
-
双分区方案:由于备份区(Slot 1)始终保留着完整的完整新固件(或者运行区还残留旧固件),下次上电时 Bootloader 可以:
-
检测到升级标志但校验失败 → 清除标志,回滚到旧 APP。
-
或者检测到运行区为空(擦除后断电) → 从备份区恢复。
-
-
进阶:可增加"版本号回滚"机制,若新固件连续崩溃重启多次(由看门狗检测),自动回退到旧版本。
5.2 看门狗(IWDG)的正确喂狗策略
-
Bootloader 中的 Flash 擦写操作耗时可能较长(几十毫秒到几百毫秒),如果看门狗超时时间设置过短,会导致复位。
-
建议:
-
在 Bootloader 进入擦写循环前,开启独立看门狗 ,并在每次擦除或写入一个扇区/页后喂狗。
-
在复制固件的大循环中,每复制 N 字节(如 1024 字节)喂狗一次。
-
-
注意:喂狗间隔应远小于看门狗溢出时间(例如溢出时间 2 秒,喂狗间隔 500ms)。
5.3 固件加密 ------ 防止抄板和恶意篡改
-
风险:在 IAP 传输过程中,固件暴露在串口、Wi-Fi、蓝牙等通信介质上,容易被抓包窃取。
-
推荐方案:
-
上位机使用 AES-256 或 SM4 加密固件二进制文件。
-
Bootloader 和 APP 中内置相同的密钥(可结合芯片唯一 ID 分散存储)。
-
下载时先将加密固件存入备份区,Bootloader 在解密后写入运行区。
-
-
进阶:搭配安全启动(Secure Boot),对 Bootloader 本身进行签名校验,防止恶意 Bootloader 替换。
5.4 通信协议的选择与设计
| 通信接口 | 适用场景 | 推荐传输协议 | 注意事项 |
|---|---|---|---|
| UART | 本地串口升级 | Ymodem / Xmodem | 自带 CRC 校验,简单可靠 |
| Ethernet | 局域网 OTA | TFTP / HTTP | 需实现简单文件下载 + 断点续传 |
| Wi-Fi | 物联网远程 OTA | MQTT + 分块 + HTTP | 注意弱网下的分包重组与校验 |
| CAN | 车载或工控 | 自定义分片协议 | 每帧最多 8 字节,需组包与重传 |
关键点 :无论使用哪种协议,必须对每个包进行顺序编号和 CRC 校验,并支持丢包重传请求。建议在 APP 中实现"固件接收状态机",能够中断后续传。
5.5 固件版本管理与防降级
-
在标志区存储当前运行固件版本(主版本+次版本+修订号)和新固件版本。
-
Bootloader 在升级前比较版本:禁止向更低版本升级(除非有特殊安全补丁需要强制降级,可增加"强制降级"标志位)。
-
防止利用旧版本漏洞进行攻击。
6. 完整例程:基于 STM32F103 + Ymodem 的串口 IAP
以下给出一个精简但完整可运行的结构示例(伪代码,可根据实际 MCU 调整地址和扇区大小)。
6.1 Bootloader 工程配置
-
起始地址:
0x08000000 -
大小:64 KB
-
不需要修改向量表偏移(因为 Bootloader 本身就是从
0x08000000启动)
主要流程代码:
cpp
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_USART1_Init(); // 用于打印调试和 Ymodem 接收(可选)
// 1. 检测升级标志
if (CheckUpdateFlag() == 0x5A5A)
{
// 2. 校验备份区固件
if (VerifyBackupFirmwareCRC() == PASS)
{
// 3. 擦除运行区
EraseAppSlot0();
// 4. 复制固件
CopyFirmware(SLOT1_ADDR, SLOT0_ADDR, FIRMWARE_SIZE);
// 5. 最终校验运行区
if (VerifySlot0FirmwareCRC() == PASS)
{
ClearUpdateFlag(); // 清除标志
}
else
{
// 搬移失败,保留标志,尝试再次升级或报错
Error_Handler();
}
}
else
{
ClearUpdateFlag(); // 备份区固件无效,清除标志,跳回原APP
}
}
// 6. 跳转到 APP
if (CheckAppValid(SLOT0_ADDR))
{
Bootloader_JumpToApp(SLOT0_ADDR);
}
else
{
// APP 无效,进入 Bootloader 的等待升级模式(例如通过串口接收固件)
while(1)
{
// 等待 Ymodem 接收新固件并直接写入备份区,然后设置标志并软复位
}
}
}
6.2 APP 工程配置
-
起始地址:
0x08011000 -
向量表偏移:
0x11000 -
在
main开头设置SCB->VTOR = 0x08011000; -
APP 中实现升级接收逻辑,将固件写入
SLOT1_ADDR(0x08048000),完成后置标志并复位。
6.3 生成 .bin 文件供 IAP 使用
IAP 通常传输原始的 .bin 文件(不含地址信息),而非 .hex 或 .axf。在 Keil 中配置:
-
"User" 选项卡,在 After Build 中添加:
cppfromelf.exe --bin -o ./output/App.bin ./output/App.axf
然后将 App.bin 作为升级固件发送给 MCU。
7. 常见问题排雷(FAQ)
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 跳转到 APP 后死机,进入 HardFault | 1. 未设置 VTOR 偏移 2. 未清理中断 3. 栈指针不对 | 在 APP 最开头设置 VTOR;跳转前关闭全局中断并清理 NVIC;检查 APP 的起始地址是否合法 |
| 串口 IAP 接收固件时偶尔丢包 | 流控未开启或接收缓冲区溢出 | 使用 Ymodem(自带 CRC 和重传);增大接收缓冲区;使用 DMA 接收 |
| 擦除 Flash 时程序卡死 | 未解锁 Flash 或擦除地址不对 | 确保调用 HAL_FLASH_Unlock();擦写地址按页/扇区对齐 |
| 升级后程序版本没有变化 | 标志区未正确写入或 Bootloader 未清除 | 检查标志区地址是否被 Bootloader 或 APP 意外修改,用调试器观察内存 |
| 看门狗在擦写 Flash 时导致复位 | 擦写耗时超过看门狗溢出时间 | 在擦写循环中添加喂狗指令,或适当增加看门狗超时时间 |
8. 总结
通过本文的解析和代码示例,您应该能够掌握:
-
ICP 与 IAP 的区别与使用场景。
-
双分区 Flash 布局的设计方法。
-
完整的 IAP 升级状态机(下载→校验→擦写→搬移→跳转)。
-
APP 中向量表偏移的关键配置。
-
Bootloader 跳转前必须执行的"战场清理"操作。
-
产品级鲁棒性建议:断电保护、看门狗、加密、版本管理。
IAP 是嵌入式产品实现远程维护 和持续迭代的核心技术。一个设计良好的 IAP 方案,可以让你的设备在用户手中也能像手机一样"系统更新",极大降低后期维护成本。希望本文能帮助你避开常见的坑,快速实现稳定可靠的在线升级功能。
最后再提醒一句:在开发 IAP 时,务必准备一个可靠的恢复手段(如通过 ICP 重新烧录),以防在试验阶段把 Bootloader 自己弄坏导致无法启动。建议先在一个开发板上完整验证所有流程,再移植到产品中。