1. 嵌入式系统在线升级(IAP)技术实现详解
在工业控制、物联网终端、智能仪表等长期部署的嵌入式设备中,程序更新需求日益频繁。现场更换硬件或返厂升级已无法满足快速迭代与远程维护的要求。在线升级(In-Application Programming, IAP)技术成为保障设备持续可用性与功能演进的核心能力。本文以STM32F103RB为硬件平台,完整阐述一种基于双应用区(App1 + App2)与BootLoader协同工作的可靠IAP方案。该方案不依赖外部调试器,仅通过串口即可完成固件安全更新,具备分区管理、校验保护、向量表重映射及异常回滚等工程级特性。
1.1 系统设计目标与约束条件
本方案面向资源受限的Cortex-M3内核MCU,需在128KB片内Flash(共128页,每页1KB)约束下达成以下目标:
- 零外部依赖:升级过程不中断设备主功能,无需JTAG/SWD调试接口参与;
- 断电安全:升级过程中意外掉电后,系统仍能从已验证的旧版本启动;
- 版本可控:支持运行时识别当前固件版本,并在升级失败时自动回退;
- 协议可扩展:底层升级逻辑与传输协议解耦,便于后续替换为Wi-Fi、BLE或LoRa等无线通道;
- 内存隔离:BootLoader、App1、App2三者代码空间严格分离,互不覆盖。
上述目标决定了必须采用“引导程序+双应用区”的经典架构,而非单区覆盖式升级。其本质是将Flash划分为功能明确的物理区域,通过运行时跳转与数据搬运实现新旧固件的原子切换。
2. Flash存储分区规划与地址映射
STM32F103RB的主Flash地址空间为0x08000000 ~ 0x0801FFFF(128KB)。为支撑IAP机制,需对该空间进行静态划分。本方案采用三级分区策略,各区域起始地址、大小及用途如下表所示:
| 分区名称 | 起始地址 | 大小 | 页范围 | 主要用途 |
|---|---|---|---|---|
| BootLoader区 | 0x08000000 | 20KB | 第0~19页 | 存放引导程序,永不更新 |
| App1区 | 0x08005000 | 56KB | 第20~75页 | 当前运行的应用程序主区 |
| App2区 | 0x08014000 | 44KB | 第76~127页 | 升级包暂存区(备份区) |
注:20KB BootLoader预留空间已充分考虑未来功能扩展(如USB DFU、加密校验等),实际初始BootLoader代码仅占用约8KB。
该划分遵循三项工程原则:
- BootLoader不可覆盖性:将其置于Flash起始位置,Keil默认链接脚本即从
0x08000000开始加载,避免因配置失误导致引导代码被擦除; - App1与App2容量非对称设计:App1作为主运行区需容纳完整业务逻辑,故分配更大空间;App2仅用于暂存待升级的bin文件,按典型固件尺寸(≤40KB)预留;
- 页对齐擦除:所有分区边界严格对齐1KB页边界(
0x08005000= 20×1024),确保Flash擦除操作可精确控制,避免误擦相邻区域。
3. BootLoader核心机制解析
BootLoader是系统上电后首条执行的代码,其唯一职责是决定启动哪个应用程序。本方案BootLoader流程高度精简,仅包含三个确定性步骤:
3.1 启动判据:App2区有效性标志
为避免每次启动均执行冗余拷贝,BootLoader需快速判断App2区是否存有有效升级包。此处采用标志位法:利用App2区末尾地址0x0801FFFC(即最后1个字)作为状态寄存器。
- 空闲状态:Flash擦除后该地址值为
0xFFFFFFFF(全1); - 有效升级包:App程序在完成bin文件写入后,将该地址写入特征值
0xAAAAAAAA。
此设计优势显著:
- 零开销检测:仅一次32位读取操作,耗时<1μs;
- 抗干扰性强:
0xAAAAAAAA与擦除态0xFFFFFFFF汉明距离达16位,单粒子翻转(SEU)极难误触发; - 无额外存储开销:复用Flash物理空间,不占用RAM或独立EEPROM。
3.2 固件搬运:App2→App1安全拷贝
当检测到0x0801FFFC == 0xAAAAAAAA时,BootLoader执行关键动作——将App2区全部内容复制至App1区。该过程需严格遵循Flash操作规范:
// 伪代码:App2→App1拷贝流程 if (*(uint32_t*)0x0801FFFC == 0xAAAAAAAA) { // 1. 擦除App1区全部页(第20~75页) for (uint16_t page = 20; page <= 75; page++) { FLASH_ErasePage(0x08000000 + page * 1024); } // 2. 逐页拷贝App2数据(源地址0x08014000,目标0x08005000) uint32_t src_addr = 0x08014000; uint32_t dst_addr = 0x08005000; uint32_t len = 0x0000B000; // 44KB while (len > 0) { uint32_t word = *(uint32_t*)src_addr; FLASH_ProgramWord(dst_addr, word); src_addr += 4; dst_addr += 4; len -= 4; } // 3. 清除App2有效标志,防止重复拷贝 FLASH_ProgramWord(0x0801FFFC, 0xFFFFFFFF); }关键约束:Flash编程必须以“字”(32位)为单位,且目标地址需4字节对齐;擦除操作以“页”为单位,不可跨页部分擦除。
3.3 应用跳转:栈指针重置与向量表重映射
拷贝完成后,BootLoader需跳转至App1入口。此过程涉及两个底层硬件操作:
(1)主栈指针(MSP)初始化
Cortex-M3复位后从地址0x08000000读取初始MSP值(位于向量表首字)。App1的向量表位于0x08005000,故需手动加载其首字:
__asm void MSR_MSP(uint32_t ulAddr) { MSR MSP, r0 // 将ulAddr值载入主栈指针 BX r14 // 返回调用者 }(2)向量表偏移寄存器(VTOR)配置
为使CPU从中断时能正确索引App1的向量表,需设置SCB->VTOR寄存器:
SCB->VTOR = 0x08005000; // 指向App1向量表基址(3)绝对跳转执行
获取App1复位向量(向量表第二个字,地址0x08005004)并执行:
typedef void (*pFunction)(void); pFunction JumpToApp; uint32_t jumpAddress = *(uint32_t*)(0x08005004); // 复位地址 JumpToApp = (pFunction)jumpAddress; MSR_MSP(*(uint32_t*)0x08005000); // 加载App1栈顶 JumpToApp(); // 跳转执行安全性校验:跳转前检查复位地址高位是否为
0x2000xxxx(SRAM)或0x0800xxxx(Flash),防止跳转至非法地址导致HardFault。
4. App1应用程序升级逻辑实现
App1作为用户业务逻辑载体,在运行期间承担升级包接收与写入App2区的任务。其核心模块包括:串口通信驱动、YModem协议解析、Flash编程接口及版本管理。
4.1 YModem协议在嵌入式端的轻量化实现
YModem是XModem的增强版,支持1024字节大数据包及文件名传输,非常适合固件升级场景。本方案针对资源受限MCU进行裁剪,仅实现关键帧类型:
| 帧类型 | 字节值 | 作用说明 |
|---|---|---|
| SOH | 0x01 | 1024字节数据包起始标识 |
| EOT | 0x04 | 文件传输结束标识 |
| ACK | 0x06 | 正确接收确认 |
| NAK | 0x15 | 校验失败请求重传 |
| CCC | 0xC0 | 连续三次0xC0表示会话建立 |
接收状态机设计:
采用有限状态机(FSM)管理升级流程,状态定义如下:
TO_START:等待客户端发送SOH起始帧TO_RECEIVE_DATA:接收连续数据包(包序号递增)TO_RECEIVE_EOT2:收到首个EOT后等待第二个EOT(YModem要求双EOT确认)TO_RECEIVE_END:升级完成,触发系统复位
CRC16校验实现:
使用标准CRC-16-CCITT算法(多项式0x1021),对SOH帧后1024字节数据计算校验值,与帧末2字节比对:
uint16_t CRC16_CCITT(const uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= data[i] << 8; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x8000) crc = (crc << 1) ^ 0x1021; else crc <<= 1; } } return crc; }4.2 App2区Flash编程关键流程
接收完一个SOH数据包后,需将其有效载荷(1024字节)写入App2区对应地址。由于Flash编程需按“字”操作,需进行数据对齐处理:
// 将1024字节数据写入App2区指定页偏移 void WriteFlash(uint32_t WriteAddr, uint32_t *pBuffer, uint16_t NumToWrite) { HAL_FLASH_Unlock(); // 解锁Flash __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR); for (uint16_t i = 0; i < NumToWrite; i += 4) { uint32_t word = pBuffer[i/4]; if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, WriteAddr + i, word) != HAL_OK) { // 编程失败处理:记录错误,停止升级 Error_Handler(); } } HAL_FLASH_Lock(); // 锁定Flash }关键注意事项:
- 写入前必须确保目标页已擦除(本方案在接收首帧时执行
Erase_page(Application_2_Addr, 40)); HAL_FLASH_Program()返回HAL_OK才代表写入成功,需严格检查;- 避免在Flash编程期间响应中断(可临时关闭全局中断)。
4.3 版本信息管理与向量表重映射
App1启动后首要任务是重映射向量表,否则中断服务例程(ISR)将指向BootLoader区,导致不可预知行为:
// 在App1的main()函数开头执行 SCB->VTOR = 0x08005000; // 强制向量表定位到App1区 __set_MSP(*(uint32_t*)0x08005000); // 初始化主栈指针版本信息通过宏定义固化在代码中,编译时生成:
#define APP_VERSION_MAJOR 0 #define APP_VERSION_MINOR 0 #define APP_VERSION_PATCH 1 #define APP_VERSION_STR "0.0.1"升级完成后,App1通过串口打印当前版本,供运维人员验证:
printf("App Version: %s\r\n", APP_VERSION_STR);5. Keil MDK工程配置要点
IAP方案成功落地高度依赖正确的IDE配置。本节列出Keil v5.37环境下关键设置项:
5.1 BootLoader工程配置
| 配置项 | 值 | 说明 |
|---|---|---|
| Target → IRAM1 | 0x20000000,20K | RAM区大小,满足BootLoader栈需求 |
| Target → IROM1 | 0x08000000,20K | Flash起始地址与大小 |
| Output → Create HEX File | ✅启用 | 生成.hex用于烧录 |
| C/C++ → Define | BOOTLOADER | 条件编译标识 |
分散加载文件(scatter file)示例:
LR_IROM1 0x08000000 0x00005000 { ; load region size_region ER_IROM1 0x08000000 0x00005000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00005000 { .ANY (+RW +ZI) } }5.2 App1工程配置
| 配置项 | 值 | 说明 |
|---|---|---|
| Target → IROM1 | 0x08005000,56K | 起始地址必须与分区规划一致 |
| Target → IRAM1 | 0x20000000,20K | RAM区需容纳YModem接收缓冲区 |
| Output → Create BIN File | ✅启用 | 生成.bin供YModem传输 |
| C/C++ → Define | APP1 | 区分BootLoader与App1编译环境 |
关键链接脚本修改:
在App1的startup_stm32f103xb.s中,将向量表起始地址修正为0x08005000:
; 修改前(默认) ; VECT_TAB_OFFSET EQU 0x00000000 ; 修改后 VECT_TAB_OFFSET EQU 0x00005000 ; 相对于IROM1基址的偏移6. 升级流程实操验证
6.1 分步烧录流程
BootLoader烧录:
- 使用ST-Link Utility或J-Flash,选择
BootLoader.hex文件; - 设置擦除方式为Erase Sectors,地址范围
0x08000000~0x08004FFF(20KB); - 烧录完成后复位,通过串口1观察输出:
> BootLoader running... > Checking App2... empty > Jumping to App1...
- 使用ST-Link Utility或J-Flash,选择
App1首次烧录:
- 打开App1工程,修改
APP_VERSION_STR为"0.0.1"; - 编译生成
App1.bin; - 使用ST-Link Utility烧录至
0x08005000,擦除范围0x08005000~0x08013FFF; - 启动后串口1输出:
> App Version: 0.0.1 > Ready for upgrade...
- 打开App1工程,修改
6.2 YModem升级操作
- 使用Xshell连接开发板串口2(升级专用通道);
- 在Xshell中执行
rz -y命令(需安装lrzsz工具),选择App1_v0.0.2.bin文件; - 观察串口1(调试通道)实时日志:
> Receive start... > Receive data bag:128 byte > Receive data bag:256 byte > ... > Receive end... > Upgrade success! Resetting... - 系统复位后,串口1输出变为:
> App Version: 0.0.2
6.3 断电恢复测试
在YModem传输过程中强制断电,再次上电后BootLoader检测到App2区未写入完整0xAAAAAAAA标志,跳过拷贝直接启动App1(v0.0.1),确保业务连续性。
7. BOM清单与硬件资源占用
本方案纯软件实现,无需额外硬件器件。所依赖的STM32F103RB片上资源占用情况如下:
| 资源类型 | 占用详情 | 备注 |
|---|---|---|
| Flash | BootLoader: ≤8KB, App1: ≤56KB | App2区仅存储bin文件,不占代码空间 |
| SRAM | ≈8KB(含YModem 1024B接收缓冲区) | 未启用堆内存,全部静态分配 |
| UART | USART1(调试打印),USART2(YModem) | 可复用同一串口,但需时分复用 |
| GPIO | 无 | 全部通过串口通信,无需LED指示灯 |
| 定时器 | SysTick(HAL_Delay) | 用于超时控制 |
兼容性说明:本方案可无缝迁移至STM32F103C8T6(64KB Flash)、STM32F103VET6(512KB Flash)等同系列芯片,仅需调整分区地址与页数参数。
8. 方案扩展与工程优化方向
本基础IAP框架具备良好可扩展性,实际项目中可按需增强:
- 安全加固:在App2写入前增加AES-128解密步骤,BootLoader校验App1区SHA256哈希值;
- 多区冗余:扩展为App1/App2/App3三区,支持A/B/C滚动升级,彻底消除升级窗口;
- 无线升级:将YModem接收模块替换为ESP8266 AT指令解析器,通过HTTP GET下载bin文件;
- 差分升级:集成bsdiff算法,在服务器端生成差分包,终端侧执行bspatch还原,降低传输流量;
- OTA云平台对接:集成MQTT协议,从云端获取升级包URL及数字签名,实现远程集中管控。
所有扩展均不改变BootLoader核心逻辑——其职责始终是“验证→搬运→跳转”,这正是嵌入式IAP设计的稳定性基石。