1. IAP技术的本质与工程价值

在嵌入式系统生命周期中,固件更新能力直接决定产品的可维护性、功能迭代效率和现场部署成本。IAP(In-Application Programming)并非一种孤立的烧录技巧,而是嵌入式系统架构设计的关键环节——它将程序存储空间划分为可独立管理的逻辑区域,并赋予运行时代码动态重写自身Flash的能力。这种能力使设备摆脱了物理连接下载器的束缚,从开发阶段的调试工具升维为产品级的核心服务机制。

IAP与ICP(In-Circuit Programming)、ISP(In-System Programming)存在本质区别。ICP依赖专用编程器(如J-Link、ST-Link)通过SWD/JTAG接口直接访问芯片内部调试逻辑,在开发阶段用于首次烧录或深度调试;ISP则利用芯片内置Bootloader,通过UART、USB等通用外设接口实现固件更新,但其功能由芯片厂商固化,用户无法定制协议与流程。而IAP的核心在于: Bootloader完全由开发者自主实现 ,它不再是芯片出厂时的只读固件,而是成为产品固件架构中可演进的第一层软件基础设施。

以STM32F103为例,其内部Flash并非一块连续的“空白画布”。当采用IAP方案时,必须进行严格的分区规划:低地址区域存放Bootloader,高地址区域存放Application(APP)。这种分区不是简单的地址划分,而是涉及向量表重定位、中断响应链重构、Flash擦写权限管理等底层硬件约束。Bootloader在每次上电复位后首先执行,其核心职责是判断是否进入升级模式,若否,则跳转至APP区起始地址执行用户功能;若是,则接管通信、解析固件、校验数据、擦写Flash、校验写入结果等全流程。这一过程要求开发者对STM32的启动流程、向量表结构、Flash编程时序有精确控制能力。

工业现场的应用场景凸显了IAP不可替代的价值。设想一个安装在高压配电柜内的传感器采集终端,其外壳密封且无物理调试接口。若需修复一个偶发的ADC采样偏差Bug,传统ICP/ISP方案意味着必须停电、开柜、接线、烧录、复位、封柜——单次操作耗时超过30分钟。而基于CAN总线的IAP方案仅需主控PLC发送一条升级指令,终端在下一个维护窗口自动完成固件切换,全程无需人工干预。这种运维效率的跃迁,正是IAP从实验室概念走向工业级应用的根本驱动力。

2. 启动模式与Bootloader加载机制

STM32的启动行为由BOOT引脚电平状态与内部Flash映射关系共同决定,这是理解IAP执行起点的基础。STM32F103具有三种启动模式:主闪存存储器(Main Flash Memory)、系统存储器(System Memory)和嵌入式SRAM(Embedded SRAM)。其中,IAP方案必须工作在主闪存存储器模式,即BOOT0 = 0、BOOT1 = X(X表示任意状态),此时CPU复位后从地址0x08000000开始取指执行——该地址映射到内部Flash的起始位置。

关键点在于: 0x08000000处存放的并非用户APP代码,而是Bootloader的向量表 。这意味着在IAP架构中,开发者必须主动将Bootloader的中断向量表(包含初始堆栈指针MSP、复位向量Reset_Handler等)放置于Flash的起始地址。当芯片上电时,硬件自动从0x08000000读取MSP值并初始化栈指针,再从0x08000004读取复位向量地址并跳转执行。此时执行的是Bootloader的Reset_Handler,而非APP的入口函数。

这种向量表重定位带来两个强制性工程约束:
1. Flash分区必须预留向量表空间 :Bootloader代码段起始地址必须为0x08000000,且其大小需覆盖完整的向量表(通常为256字节,对应前64个中断向量)。若Bootloader功能简单,实际代码可能仅占用几KB,但向量表占据的首256字节不可挪用。
2. APP向量表需动态重映射 :当Bootloader跳转至APP执行时,APP的中断向量表位于其自身代码段起始处(如0x08004000),但CPU仍会从0x08000000读取中断向量。因此必须在跳转前调用 NVIC_SetVectorTable(NVIC_VectTab_FLASH, APP_VECTOR_OFFSET) ,将向量表基址重映射至APP所在区域。此处 APP_VECTOR_OFFSET 为APP向量表相对于Flash起始地址的偏移量(如0x4000),该值必须与链接脚本中APP的 VECT_TAB_OFFSET 严格一致。

此机制解释了为何IAP方案必须预先烧录Bootloader:只有当Flash起始地址固化了Bootloader向量表,芯片才能在每次上电时可靠地进入Bootloader执行环境。后续所有固件更新操作,均建立在此确定的启动入口基础之上。

3. 升级触发机制的工程实现路径

在IAP系统中,“何时进入升级模式”是首个必须解决的决策点。该决策直接影响用户体验、系统可靠性及硬件设计复杂度。工程实践中存在硬件触发与软件触发两大类方案,需根据产品形态与使用场景权衡选择。

3.1 硬件触发方案:物理开关与GPIO检测

最直接的方式是利用外部GPIO引脚电平状态作为升级标志。典型实现是在PCB上预留一个拨码开关或跳线帽,将其连接至MCU某个GPIO(如GPIOA_Pin0)。Bootloader在初始化后立即读取该引脚状态:

// 初始化GPIOA时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP; // 默认上拉,开关闭合时拉低
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 检测升级标志
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
    // 进入升级模式
    EnterUpgradeMode();
} else {
    // 跳转至APP
    JumpToApp();
}

此方案优势在于逻辑绝对可靠、无时间依赖、掉电状态保持。但缺陷同样显著:需要额外的物理接口与PCB空间,对于已封装的工业设备,用户无法触达开关,导致升级功能失效。更严重的是,若开关意外闭合,设备将永远无法启动APP,形成“变砖”风险。

3.2 软件触发方案:非易失性标志位存储

为规避硬件依赖,主流方案是将升级标志存储于非易失性介质中。由于外部EEPROM会增加BOM成本与电路复杂度,优先选择片内Flash模拟EEPROM。STM32F103的Flash支持按页擦除(1KB/页)、按字节/半字/全字编程,但 擦除操作不可逆且耗时较长(约20ms/页) ,因此标志位存储需精心设计。

推荐采用“双页循环存储”策略:分配两页Flash(如Page0: 0x0800F000, Page1: 0x0800F400),每页仅使用首4字节存储标志值(如0x5AA5表示升级请求,0xA55A表示取消请求)。每次更新标志时:
1. 擦除当前页(若非空)
2. 编程新标志值至另一页
3. 更新页状态标记(通过特定地址写入页头信息)

Bootloader启动时扫描两页,以最后写入的有效页内容为准。此设计避免单页擦写失败导致标志丢失,且擦除操作仅在用户主动发起升级时发生,不影响日常启动速度。

3.3 超时等待方案:通信握手触发

最符合“零硬件改动”理念的方案是超时等待。Bootloader启动后初始化指定通信外设(如CAN控制器),进入有限时间窗口(如3秒)监听升级指令。若在窗口期内收到有效握手帧(如CAN ID=0x7FF,Data=[0x55,0xAA,0xFF,0x00]),则进入升级流程;超时则跳转APP。

此方案要求Bootloader具备实时通信能力,其实现关键在于:
- CAN外设初始化必须极简 :仅配置波特率、过滤器、中断使能,避免复杂协议栈初始化
- 超时机制需硬件保障 :使用SysTick或独立看门狗定时器,确保即使通信中断也能可靠超时
- 握手帧需强校验 :包含CRC16校验,防止误触发

该方案完美适配工业CAN网络,主站PLC可在设备上电瞬间广播升级请求,终端在毫秒级内响应,无需任何物理操作。但需注意:若设备处于强电磁干扰环境,握手帧可能丢失,需设计重传机制或降级为按键触发。

4. 固件镜像格式与生成流程

IAP升级的本质是将编译生成的机器码二进制流(Binary Image)完整写入Flash指定区域。然而MDK-ARM默认输出的Intel HEX文件并非直接可用的二进制镜像,其包含地址信息、校验和等ASCII文本字段,需经格式转换才能被Bootloader解析。

4.1 HEX与BIN文件的本质差异

HEX文件是文本格式,每行以冒号开头,包含字节数、起始地址、记录类型、数据、校验和五部分。例如:

:10010000214601360121470136012148013601217E

其中 0100 为地址, 2146... 为16字节数据。而BIN文件是纯二进制流,地址信息隐含在文件偏移中:文件第0字节对应Flash地址0x08000000,第1字节对应0x08000001,以此类推。Bootloader在Flash编程时只需按顺序读取BIN数据并写入对应地址,无需解析地址字段,极大简化了固件解析逻辑。

4.2 MDK自动生成BIN的配置方法

MDK-ARM提供 fromelf 工具实现HEX到BIN的转换。需在工程Options → User → After Build/Rebuild中添加命令:

fromelf --bin --output ./Objects/$(ProjectName).bin ./Objects/$(ProjectName).axf

此处 $(ProjectName).axf 为MDK链接生成的ELF格式可执行文件, fromelf 工具位于MDK安装目录 \ARM\ARMCC\bin\fromelf.exe 。若未配置环境变量,需使用绝对路径:

"C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe" --bin --output ./Objects/$(ProjectName).bin ./Objects/$(ProjectName).axf

配置生效后,每次Build成功即在 Objects 目录生成同名BIN文件。该文件即为Bootloader可直接处理的固件镜像。

4.3 BIN文件的Flash布局验证

生成BIN文件后,必须验证其地址范围与APP分区规划一致。使用 fromelf -z 命令可查看符号地址:

fromelf -z ./Objects/$(ProjectName).axf | findstr "Image"

输出中 Load Region LR_IROM1 显示加载区域起始地址(如0x08004000)与长度(如0x0003C000)。BIN文件长度应等于该长度,且其数据内容应从地址0x08004000开始映射。若BIN长度超出APP分区,Bootloader写入时将越界擦除Bootloader区域,导致设备永久失效。

5. 基于CAN总线的IAP通信协议设计

在工业现场,CAN总线凭借其高抗干扰性、多主结构及完善的错误处理机制,成为IAP通信的理想载体。设计CAN-IAP协议需兼顾可靠性、实时性与资源占用,避免过度设计导致Bootloader体积膨胀。

5.1 帧结构定义

采用标准帧格式(11位ID),定义三类帧:
- 握手帧(ID=0x7FF) [0x55, 0xAA, 0x01, 0x00, CRC16] ,用于触发升级模式
- 固件数据帧(ID=0x100~0x1FF) [Block_Index_H, Block_Index_L, Data_Bytes..., CRC16] ,每帧传输最多6字节有效数据(预留2字节用于索引与CRC)
- 控制帧(ID=0x200) [Command, Param_H, Param_L, CRC16] ,支持擦除(0x01)、校验(0x02)、重启(0x03)等指令

5.2 可靠性增强机制

  1. 块级应答确认 :每接收一帧数据,Bootloader回传 ACK(ID) 帧。若上位机未收到ACK,启动重传(最多3次),超时则终止升级。
  2. Flash写入校验 :写入每个数据块后,立即读回该地址数据并与原始值比对,不一致则返回 NACK 并重传。
  3. 整包CRC校验 :固件传输完成后,上位机发送固件总CRC16值,Bootloader对已写入的全部Flash区域计算CRC并比对,确保数据完整性。

5.3 Bootloader的CAN驱动精简实现

为最小化Bootloader体积,CAN驱动仅实现必要功能:
- 使用CAN基本模式(Basic Mode),禁用FIFO与时间戳
- 配置单个接收过滤器(Filter 0),仅接收ID=0x7FF、0x100~0x1FF、0x200的帧
- 中断服务函数(CAN_RX0_IRQHandler)中仅做:读取FIFO → 解析ID → 存入环形缓冲区 → 退出
- 主循环中轮询缓冲区,解析帧并执行对应动作

此设计将CAN驱动代码压缩至不足500字节,为加密、签名等高级功能预留空间。

6. Flash擦写操作的底层时序控制

STM32F103的Flash编程必须严格遵循参考手册规定的时序,否则将导致写入失败或Flash损坏。IAP过程中最关键的环节是Flash擦除与编程,其操作流程与参数设置直接决定升级成功率。

6.1 擦除操作的页粒度约束

STM32F103的Flash按页组织,每页容量为1KB(0x400字节)。擦除操作的最小单位是整页,无法擦除单个字节或字。当升级固件需覆盖APP区域时,必须先擦除目标页。例如,若APP起始地址为0x08004000,长度为0x10000字节,则需擦除地址范围0x08004000~0x08013FFF内的16页(0x08004000, 0x08004400, …, 0x08013C00)。

擦除前必须检查Flash是否已解锁:

// 解锁Flash编程
HAL_FLASH_Unlock();

// 擦除第i页(页地址为FLASH_BASE + i*FLASH_PAGE_SIZE)
FLASH_EraseInitTypeDef EraseInitStruct;
EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
EraseInitStruct.PageAddress = FLASH_APP_START_ADDR + i * FLASH_PAGE_SIZE;
EraseInitStruct.NbPages = 1;
uint32_t PageError = 0;
HAL_FLASHEx_Erase(&EraseInitStruct, &PageError);

6.2 编程操作的字对齐要求

Flash编程要求数据按字(32位)对齐写入。若固件BIN数据为字节流,需在Bootloader中进行缓冲重组:

uint32_t data_word = 0;
uint8_t word_buffer[4] = {0};
uint8_t byte_index = 0;

// 将接收到的字节逐个填入缓冲区
word_buffer[byte_index++] = received_byte;
if (byte_index == 4) {
    // 组成一个32位字
    data_word = (word_buffer[3] << 24) | (word_buffer[2] << 16) |
                (word_buffer[1] << 8)  | word_buffer[0];
    // 写入Flash
    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, 
                       write_address, data_word);
    write_address += 4;
    byte_index = 0;
}

未填满4字节时,需在固件末尾补零,确保最终写入的每个字均为有效数据。

6.3 电源与时钟稳定性保障

Flash编程期间,VDD电压波动或HCLK频率不稳定将导致写入错误。工程实践中必须:
- 在擦除/编程前调用 HAL_PWR_EnableBkUpAccess() 启用备份域访问,确保电源监控电路正常工作
- 禁用所有可能影响系统时钟的外设(如USB、FSMC)
- 若使用HSI作为系统时钟,需确保HSI校准值准确( RCC->CR |= RCC_CR_HSICAL

7. 安全加固与防误刷机制

IAP功能在提升便利性的同时,也引入了安全风险:恶意固件注入、升级过程断电导致Flash损坏、误操作擦除Bootloader等。工业级IAP方案必须集成多重防护机制。

7.1 Bootloader自保护

通过设置Flash写保护寄存器(WRPR),禁止对Bootloader区域(0x08000000~0x08003FFF)的擦除与编程:

// 设置写保护页(以STM32F103C8T6为例,WRPR寄存器bit0-31对应页0-31)
// 保护页0-15(0x08000000~0x08003FFF)
FLASH->WRPR = 0xFFFF0000; // 低16位为0表示受保护

此操作在Bootloader初始化时执行,确保即使APP代码存在漏洞,也无法修改Bootloader自身。

7.2 固件签名验证

在固件生成阶段,使用私钥对BIN文件计算ECDSA签名,将签名附加于BIN末尾。Bootloader在写入前,使用预置公钥验证签名有效性:

// 伪代码:验证固件签名
if (VerifyECDSASignature(firmware_data, firmware_length, signature) != SUCCESS) {
    // 签名无效,拒绝升级
    SendNACK(0x01); // 0x01表示签名错误
    return;
}

此机制杜绝了未经授权的固件注入,是满足IEC 62443等工业安全标准的必要条件。

7.3 断电恢复机制

为应对升级中意外断电,采用“双区备份”策略:将APP区域划分为Active区与Backup区。每次升级时,新固件写入Backup区,校验通过后更新跳转地址指向Backup区,再擦除原Active区。若升级中断,设备下次启动仍可运行旧固件,避免“半砖”状态。

8. 实际项目中的典型问题与规避策略

在多个工业IAP项目落地过程中,以下问题高频出现,其解决方案已沉淀为工程最佳实践:

8.1 CAN总线唤醒失败

某风电变流器项目中,Bootloader无法响应CAN唤醒帧。排查发现:CAN收发器(SN65HVD230)的RS引脚被误接为高电平,导致进入高速模式,而Bootloader初始化时配置为静默模式。解决方案:RS引脚通过10kΩ电阻下拉,确保默认为斜率控制模式;并在CAN初始化代码中显式配置 hcan.Init.SleepMode = CAN_SLEEP_MODE_LOOPBACK

8.2 Flash擦除后校验失败

某智能电表项目中,擦除页后读取全0xFF,但编程后读取值异常。根本原因是未等待Flash编程完成即进行读取。STM32F103的Flash编程需约25μs/字,必须插入 __DSB() 数据同步屏障指令:

HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, data);
__DSB(); // 确保编程操作完成
// 此后才可读取addr地址

8.3 多任务环境下中断冲突

某基于FreeRTOS的网关项目中,Bootloader在升级时被RTOS定时器中断打断,导致Flash状态机错乱。解决方案:在关键Flash操作前后禁用全局中断:

__disable_irq();
// 执行擦除/编程操作
__enable_irq();

而非依赖RTOS的临界区保护,因Bootloader需在RTOS启动前运行。

这些问题的共性在于:过度依赖抽象层API而忽视底层硬件时序。真正的IAP工程师必须手持《STM32F103xx Reference Manual》,在每一次Flash操作前确认时序图,在每一行CAN初始化代码后核对电气特性参数。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐