一、核心定义与关键参数
| 升级方式 | 全称 | 核心原理 | 依赖组件 | 通信接口 | 典型工具 |
|---|---|---|---|---|---|
| ICP | In-Circuit Programming(在电路编程) | 直接访问 MCU 主 Flash,无需 bootloader,通过调试接口擦写固件 | 专用烧录器 | JTAG、SWD | J-Link、ST-Link |
| ISP | In-System Programming(在系统编程) | 依赖 MCU 出厂内置 bootloader,从系统存储区启动,接收固件写入主 Flash | 出厂预置 bootloader | UART、SPI | STC-ISP、FlyMCU |
| IAP | In-Application Programming(在应用编程) | 自定义 bootloader+APP 分区,启动先运行 bootloader,按需下载新固件更新 APP 区 | 自研 bootloader | UART、WiFi、蓝牙 | 自研升级工具、OTA 模块 |
二、核心差异与适用场景
1. ICP 特点
- 优势:烧录速度快,支持单步调试,可灵活擦写 Flash 任意区域
- 劣势:需外置烧录器,产品现场不便操作,有额外硬件成本
- 适用场景:开发调试阶段、工厂小批量烧录(需近距离操作)
2. ISP 特点
- 优势:免拆板,无需复杂硬件,仅需串行接口,适合现场操作
- 劣势:速率受串口波特率限制,仅支持厂商预置协议,无调试功能
- 适用场景:工厂批量现场烧录、无远程需求的设备售后局部升级
3. IAP 特点
- 优势:灵活性极高,可实现无线远程升级(OTA),无需现场干预
- 劣势:需自研 bootloader,设计复杂度高,占用额外 Flash 空间
- 适用场景:已部署设备远程维护、需频繁升级的消费电子 / 工业设备
三、关键优先级选型
- 开发调试优先选 ICP(兼顾烧录与调试,效率最高)
- 工厂批量现场烧录选 ISP(低成本、免拆板,适配量产)
- 已落地设备升级选 IAP(支持远程,降低售后成本)
以下是 IAP 双 APP 分区的核心实现代码框架(以 STM32 为例,适配多数 MCU,包含防 “变砖” 机制),主要分为 Bootloader 和 APP 两部分:
一、核心设计思路(防 “变砖” 关键)
- 双 APP 分区:Flash 划分为
Bootloader区+APP1区(主程序)+APP2区(备用/升级区)+标志区(存升级状态、校验值)。 - 升级流程:新固件先写入 APP2 区,校验通过后更新标志位,下次启动 Bootloader 自动将 APP2 区复制到 APP1 区,完成升级。
- 回滚机制:若 APP1 区启动失败(如校验错、跑飞),Bootloader 自动切换到 APP2 区运行,避免设备变砖。
二、Flash 分区定义(需根据 MCU Flash 大小调整)
// flash_partition.h
#define FLASH_BASE_ADDR 0x08000000 // STM32 Flash起始地址
// 分区大小(单位:字节,需按MCU页大小对齐)
#define BOOTLOADER_SIZE 0x8000 // Bootloader区:32KB(示例)
#define APP1_SIZE 0x20000 // APP1区(主程序):128KB
#define APP2_SIZE 0x20000 // APP2区(备用/升级):128KB
#define FLAG_SIZE 0x1000 // 标志区:4KB(存状态、校验值)
// 各分区起始地址
#define BOOTLOADER_ADDR FLASH_BASE_ADDR
#define APP1_ADDR (BOOTLOADER_ADDR + BOOTLOADER_SIZE)
#define APP2_ADDR (APP1_ADDR + APP1_SIZE)
#define FLAG_ADDR (APP2_ADDR + APP2_SIZE)
// 标志区数据结构(存升级状态和校验值)
typedef struct {
uint8_t upgrade_flag; // 升级标志:0=正常,1=待升级(APP2→APP1)
uint8_t app1_valid; // APP1有效性:0=无效,1=有效
uint8_t app2_valid; // APP2有效性:0=无效,1=有效
uint32_t app1_crc; // APP1的CRC校验值
uint32_t app2_crc; // APP2的CRC校验值
} FlagTypeDef;
三、Bootloader 程序(核心升级逻辑)
// bootloader.c
#include "flash_partition.h"
#include "stm32f1xx_hal.h"
// 函数指针:用于跳转至APP程序
typedef void (*pFunction)(void);
pFunction JumpToApp;
uint32_t AppStackAddr;
// 读取标志区数据
FlagTypeDef flag;
void Flag_Read(void) {
// 从Flash标志区读取数据到flag变量(需实现Flash读函数)
Flash_Read(FLAG_ADDR, (uint8_t*)&flag, sizeof(FlagTypeDef));
}
// 写入标志区数据(带CRC校验,防止标志位损坏)
void Flag_Write(FlagTypeDef *f) {
// 擦除标志区,写入新数据(需实现Flash擦除/写函数)
Flash_Erase(FLAG_ADDR, FLAG_SIZE);
Flash_Write(FLAG_ADDR, (uint8_t*)f, sizeof(FlagTypeDef));
}
// 校验APP区有效性(通过CRC)
uint8_t App_Check(uint32_t app_addr, uint32_t app_size, uint32_t expected_crc) {
uint32_t crc = HAL_CRC_Calculate(&hcrc, (uint32_t*)app_addr, app_size/4);
return (crc == expected_crc) ? 1 : 0; // 1=有效,0=无效
}
// 复制APP2到APP1(升级核心步骤)
void App_Copy(void) {
// 擦除APP1区
Flash_Erase(APP1_ADDR, APP1_SIZE);
// 从APP2区复制数据到APP1区
for (uint32_t i=0; i 0x20000000 + RAM_SIZE) {
return; // 栈地址无效,不跳转
}
// 初始化APP中断向量表(偏移到APP地址)
SCB->VTOR = app_addr;
// 获取APP入口地址(复位向量),跳转
JumpToApp = (pFunction)*(uint32_t*)(app_addr + 4);
__set_MSP(AppStackAddr); // 设置APP的栈指针
JumpToApp(); // 跳转执行APP
}
int main(void) {
HAL_Init();
// 初始化外设(Flash、CRC、通信接口等)
Flash_Init();
CRC_Init();
UART_Init(); // 如需接收新固件,初始化通信接口
Flag_Read(); // 读取标志区
// 步骤1:如果有待升级标志,先执行APP2→APP1复制
if (flag.upgrade_flag == 1 && flag.app2_valid == 1) {
App_Copy(); // 复制完成后标志会自动更新
}
// 步骤2:优先尝试启动APP1
if (flag.app1_valid == 1 && App_Check(APP1_ADDR, APP1_SIZE, flag.app1_crc)) {
Jump_To_App(APP1_ADDR); // 跳转至APP1
}
// 步骤3:APP1启动失败,尝试启动APP2(回滚机制)
if (flag.app2_valid == 1 && App_Check(APP2_ADDR, APP2_SIZE, flag.app2_crc)) {
Jump_To_App(APP2_ADDR); // 跳转至APP2
}
// 步骤4:所有APP都无效,进入Bootloader待机(如等待串口烧录)
while (1) {
// 可在此处实现接收新固件到APP2区的逻辑(如通过UART/WiFi)
// 接收完成后更新flag.app2_valid=1、flag.app2_crc=新CRC、flag.upgrade_flag=1
// 然后复位重启,触发升级
}
}
四、APP 程序(配合 Bootloader 的关键处理)
// app.c
#include "flash_partition.h"
// APP启动时更新自身有效性标志(防止Bootloader误判)
void App_Init(void) {
FlagTypeDef flag;
Flash_Read(FLAG_ADDR, (uint8_t*)&flag, sizeof(FlagTypeDef));
// 计算当前APP(APP1区)的CRC,更新标志
uint32_t crc = HAL_CRC_Calculate(&hcrc, (uint32_t*)APP1_ADDR, APP1_SIZE/4);
flag.app1_crc = crc;
flag.app1_valid = 1; // 标记APP1有效
Flash_Write(FLAG_ADDR, (uint8_t*)&flag, sizeof(FlagTypeDef));
}
// APP中触发升级的函数(如收到新固件后)
void App_Trigger_Upgrade(uint8_t *new_firmware, uint32_t firmware_size) {
FlagTypeDef flag;
Flash_Read(FLAG_ADDR, (uint8_t*)&flag, sizeof(FlagTypeDef));
// 1. 擦除APP2区,写入新固件
Flash_Erase(APP2_ADDR, APP2_SIZE);
Flash_Write(APP2_ADDR, new_firmware, firmware_size);
// 2. 计算新固件CRC,更新标志
flag.app2_crc = HAL_CRC_Calculate(&hcrc, (uint32_t*)APP2_ADDR, firmware_size/4);
flag.app2_valid = 1; // 标记APP2有效
flag.upgrade_flag = 1; // 标记待升级(下次启动Bootloader会执行复制)
Flag_Write(&flag);
// 3. 复位重启,进入Bootloader执行升级
NVIC_SystemReset();
}
int main(void) {
HAL_Init();
// 初始化外设
App_Init(); // 启动时更新自身有效性标志
while (1) {
// 正常业务逻辑
// ...
// 若收到升级指令,调用App_Trigger_Upgrade()
// 例如:if (upgrade_command_received) {
// App_Trigger_Upgrade(new_firmware, size);
// }
}
}
五、防 “变砖” 关键机制说明
- 双重校验:每个 APP 区都有 CRC 校验,Bootloader 启动前会先校验,无效则不跳转。
- 双区备份:APP1 和 APP2 互为备份,任一区有效即可启动,避免单区损坏导致设备失效。
- 标志区保护:标志区数据带校验,防止意外改写导致升级逻辑混乱。
- 原子操作:升级时先写备用区(APP2),校验通过后再更新标志,确保升级中断后设备仍可回退。
六、适配其他 MCU 的注意事项
- Flash 操作:需替换
Flash_Read/Flash_Write/Flash_Erase为对应 MCU 的 Flash 驱动(如 STM32 用 HAL_FLASH,ESP32 用 spi_flash_* 函数)。 - 中断向量表:不同 MCU 的中断向量表偏移配置不同(如 STM32 用
SCB->VTOR,NRF52 用NVIC_SetVectorTable)。 - 分区大小:根据固件大小和 Flash 总容量调整分区,确保不超出实际 Flash 范围。
以下补充 STM32(以 STM32F103 为例)的具体 Flash 操作函数实现,适配上面的 IAP 框架,可直接整合使用:
一、Flash 操作函数(基于 HAL 库,stm32_flash.c)
#include "stm32f1xx_hal.h"
#include "flash_partition.h"
// Flash句柄(全局)
static FLASH_EraseInitTypeDef EraseInitStruct;
static uint32_t PageError = 0;
// Flash初始化(STM32无需额外初始化,直接使用HAL库函数)
void Flash_Init(void) {
// 使能Flash预取指(提升读取速度,可选)
__HAL_FLASH_PREFETCH_BUFFER_ENABLE();
}
// Flash读取函数(从指定地址读取n字节到buf)
void Flash_Read(uint32_t addr, uint8_t *buf, uint32_t len) {
// 地址必须在Flash范围内,且不超出分区上限
if (addr < FLASH_BASE_ADDR || addr + len > (FLAG_ADDR + FLAG_SIZE)) {
return;
}
// 逐字节读取
for (uint32_t i = 0; i < len; i++) {
buf[i] = *(volatile uint8_t *)(addr + i);
}
}
// Flash写入函数(向指定地址写入n字节,需先擦除对应页)
// 注意:STM32 Flash按页擦除,写入前需确保目标页已擦除
HAL_StatusTypeDef Flash_Write(uint32_t addr, uint8_t *buf, uint32_t len) {
HAL_StatusTypeDef status = HAL_OK;
uint32_t data32 = 0; // 按32位写入(STM32F1最小写入单位为半字,这里用32位更高效)
// 地址合法性检查(必须在Flash范围内,且对齐到4字节)
if (addr < FLASH_BASE_ADDR || addr + len > (FLAG_ADDR + FLAG_SIZE) || (addr % 4 != 0)) {
return HAL_ERROR;
}
// 解锁Flash(写入前必须解锁)
status = HAL_FLASH_Unlock();
if (status != HAL_OK) {
return status;
}
// 逐32位写入数据
for (uint32_t i = 0; i < len; i += 4) {
// 拼接4字节数据(不足4字节的补0)
data32 = (buf[i] << 0) | (buf[i+1] << 8) | (buf[i+2] << 16) | (buf[i+3] << 24);
// 写入32位数据(地址需强制转换为uint32_t*)
status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + i, data32);
if (status != HAL_OK) {
break;
}
}
// 锁定Flash(写入完成后锁定,防止误操作)
HAL_FLASH_Lock();
return status;
}
// Flash擦除函数(擦除指定地址开始的n字节,按页擦除)
// 注意:STM32F1每页大小为1KB(小容量)、2KB(中容量)或4KB(大容量),需根据型号调整
HAL_StatusTypeDef Flash_Erase(uint32_t addr, uint32_t len) {
HAL_StatusTypeDef status = HAL_OK;
uint32_t start_page, end_page;
uint32_t page_size = 0x400; // 假设为中容量STM32F103(每页2KB=0x800?此处按实际型号修改!)
// 小容量:0x400(1KB),中容量:0x800(2KB),大容量:0x1000(4KB)
// 地址合法性检查
if (addr < FLASH_BASE_ADDR || addr + len > (FLAG_ADDR + FLAG_SIZE)) {
return HAL_ERROR;
}
// 计算起始页和结束页(Flash页地址 = 基地址 + 页号×页大小)
start_page = (addr - FLASH_BASE_ADDR) / page_size;
end_page = ((addr + len - 1) - FLASH_BASE_ADDR) / page_size;
// 配置擦除参数
EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
EraseInitStruct.PageAddress = FLASH_BASE_ADDR + start_page * page_size;
EraseInitStruct.NbPages = end_page - start_page + 1; // 擦除页数
// 解锁Flash
status = HAL_FLASH_Unlock();
if (status != HAL_OK) {
return status;
}
// 执行擦除(带页错误检查)
status = HAL_FLASHEx_Erase(&EraseInitStruct, &PageError);
if (status != HAL_OK) {
HAL_FLASH_Lock();
return status;
}
// 锁定Flash
HAL_FLASH_Lock();
return status;
}
二、CRC 校验初始化(配合 APP 校验,stm32_crc.c)
#include "stm32f1xx_hal.h"
CRC_HandleTypeDef hcrc; // CRC句柄(全局,供Bootloader和APP使用)
// CRC初始化(用于计算固件校验值)
void CRC_Init(void) {
hcrc.Instance = CRC;
hcrc.Init.DefaultPolynomialUse = DEFAULT_POLYNOMIAL_ENABLE; // 使用默认多项式(0x04C11DB7)
hcrc.Init.DefaultInitValueUse = DEFAULT_INIT_VALUE_ENABLE; // 使用默认初始值(0xFFFFFFFF)
hcrc.Init.InputDataInversionMode = CRC_INPUTDATA_INVERSION_NONE; // 输入数据不反转
hcrc.Init.OutputDataInversionMode = CRC_OUTPUTDATA_INVERSION_DISABLE; // 输出数据不反转
hcrc.InputDataFormat = CRC_INPUTDATA_FORMAT_WORDS; // 按32位字输入
if (HAL_CRC_Init(&hcrc) != HAL_OK) {
// 初始化失败处理(如死循环提示)
while(1);
}
}
// CRC MSP初始化(HAL库底层回调,配置时钟)
void HAL_CRC_MspInit(CRC_HandleTypeDef* crcHandle) {
if(crcHandle->Instance==CRC) {
__HAL_RCC_CRC_CLK_ENABLE(); // 使能CRC时钟
}
}
三、关键适配说明(针对 STM32F103)
Flash 页大小:
- 小容量 STM32F103(≤32KB Flash):每页 1KB(0x400 字节)
- 中容量(64KB~128KB):每页 2KB(0x800 字节)
- 大容量(256KB~512KB):每页 4KB(0x1000 字节)需在
Flash_Erase函数中修改page_size为实际值,否则擦除会出错。
Flash 解锁 / 锁定:STM32 Flash 默认处于锁定状态,写入 / 擦除前必须调用
HAL_FLASH_Unlock(),操作完成后用HAL_FLASH_Lock()锁定,防止误操作。写入单位:STM32F1 支持半字(16 位)或字(32 位)写入,代码中用
FLASH_TYPEPROGRAM_WORD按 32 位写入,效率更高(需确保地址 4 字节对齐)。中断向量表偏移:STM32 的中断向量表默认在 0x08000000(Bootloader 区),APP 启动时需通过
SCB->VTOR将向量表偏移到 APP 实际地址(如APP1_ADDR),否则中断会跳回 Bootloader 区导致崩溃。
四、使用方法
- 将上述
stm32_flash.c和stm32_crc.c加入工程,在 Bootloader 和 APP 中包含头文件。 - Bootloader 中调用
Flash_Init()和CRC_Init()初始化硬件。 - APP 中如需升级,通过
App_Trigger_Upgrade()函数将新固件写入 APP2 区,触发重启升级。
浙公网安备 33010602011771号