文章目录
- [STM32H5 Bootloader与IAP升级机制深度解析](#STM32H5 Bootloader与IAP升级机制深度解析)
-
- 一、概述
- [二、Flash 内存布局](#二、Flash 内存布局)
-
- [2.1 物理分区](#2.1 物理分区)
- [2.2 关键地址宏定义](#2.2 关键地址宏定义)
- [2.3 两个独立的链接脚本](#2.3 两个独立的链接脚本)
- 三、FirmwareInfo:固件信息结构体
-
- [3.1 数据结构](#3.1 数据结构)
- [3.2 `bEnterBootloader` 标志的三态语义](#3.2
bEnterBootloader标志的三态语义) - [3.3 读写函数](#3.3 读写函数)
- [四、Bootloader 启动流程](#四、Bootloader 启动流程)
-
- [4.1 启动流程图](#4.1 启动流程图)
- [4.2 `isBootloader()` ------ 我是谁?](#4.2
isBootloader()—— 我是谁?) - [4.3 `isNeedToUpdate()` ------ 需要升级吗?](#4.3
isNeedToUpdate()—— 需要升级吗?) - [4.4 main() 中的决策逻辑](#4.4 main() 中的决策逻辑)
- [4.5 `start_app()` ------ 汇编实现的跳转](#4.5
start_app()—— 汇编实现的跳转)
- [五、IAP 固件升级协议](#五、IAP 固件升级协议)
-
- [5.1 升级触发方式](#5.1 升级触发方式)
-
- [升级主控自身(channel == 0)](#升级主控自身(channel == 0))
- [5.2 固件传输协议(Modbus Write File Record)](#5.2 固件传输协议(Modbus Write File Record))
-
- [文件头(record_no == 0)](#文件头(record_no == 0))
- [数据包(record_no >= 1)](#数据包(record_no >= 1))
- [5.3 Flash 擦除操作](#5.3 Flash 擦除操作)
- [5.4 Flash 编程操作](#5.4 Flash 编程操作)
- 六、升级过程中的状态保护
-
- [6.1 Flash 编程互斥](#6.1 Flash 编程互斥)
- [6.2 升级期间暂停通道数据采集](#6.2 升级期间暂停通道数据采集)
- [6.3 紧急命令的"先回复后复位"策略](#6.3 紧急命令的"先回复后复位"策略)
- 七、多级升级:主控作为"升级代理"
-
- [7.1 三级升级体系](#7.1 三级升级体系)
- [7.2 固件转发流程](#7.2 固件转发流程)
- 八、启动与升级的完整时序
-
- [8.1 首次烧录(空芯片)](#8.1 首次烧录(空芯片))
- [8.2 正常运行中远程升级](#8.2 正常运行中远程升级)
- 九、关键设计要点总结
-
- [✅ 设计亮点](#✅ 设计亮点)
- [⚠️ 注意事项与潜在改进](#⚠️ 注意事项与潜在改进)
- 十、总结
STM32H5 Bootloader与IAP升级机制深度解析
一、概述
IAP(In-Application Programming)即在应用编程,是一种在系统运行时通过通信接口对用户程序进行擦除和重新编程的技术。本项目基于 STM32H563RIVx,实现了一套完整的双区Bootloader + 多级IAP升级框架,不仅支持主控自身升级,还能作为"升级代理"向下级传感器转发固件。
| 项目 | 详情 |
|---|---|
| 芯片 | STM32H563RIVx (Cortex-M33) |
| Flash 容量 | 2MB (双Bank,各1MB) |
| Bootloader | 位于 Bank1 起始区域,256KB |
| Application | 从 Bank1 中部起始,可跨越到 Bank2,最大约 1.75MB |
| 固件信息区 | 位于 Bank2 最末尾 8KB(0x081F_E000) |
| 升级协议 | Modbus Write File Record(功能码 0x15) |
二、Flash 内存布局
2.1 物理分区
0x0800_0000 ┌──────────────────────────────────────┐
│ Bootloader │
│ (256KB) │
0x0804_0000 ├──────────────────────────────────────┤
│ │
│ Application │
│ (起始于 Bank1, 可跨越到 Bank2) │
│ │
│ │
0x081F_E000 ├──────────────────────────────────────┤
│ 固件信息配置区 FirmwareInfo (8KB) │
0x0820_0000 └──────────────────────────────────────┘
Flash 总容量 2MB,地址从 0x0800_0000 到 0x081F_FFFF 连续编址。
Bank1 = 0x0800_0000 ~ 0x080F_FFFF (1MB)
Bank2 = 0x0810_0000 ~ 0x081F_FFFF (1MB)
APP 链接脚本声明加载区域 0x08040000 + 2MB,
可跨越 Bank1/Bank2 边界,最大用到 0x081F_DFFF
(固件信息区 CFG_OFFSET = 0x081FE000 之前)。
2.2 关键地址宏定义
c
#define APP_LOAD_ADDR 0x08040000 // APP 加载地址(Bootloader 结束后)
#define CFG_OFFSET 0x081FE000 // 固件信息结构体存储地址(Bank2 末尾)
#define SECTOR_SIZE (8*1024) // 扇区大小 8KB
2.3 两个独立的链接脚本
本项目的 Bootloader 和 Application 是两个独立的工程/镜像,各有自己的链接脚本:
Bootloader 链接脚本(加载地址 0x0800_0000):
LR_IROM1 0x08000000 0x00200000 {
ER_IROM1 0x08000000 0x00200000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x000a0000 {
.ANY (+RW +ZI)
}
}
Application 链接脚本(加载地址 0x0804_0000):
LR_IROM1 0x08040000 0x00200000 {
ER_IROM1 0x08040000 0x00200000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x000a0000 {
.ANY (+RW +ZI)
}
}
两者的区别仅在于 ER_IROM1 的起始地址------Bootloader 从 0x0800_0000 开始,APP 从 0x0804_0000 开始。
三、FirmwareInfo:固件信息结构体
3.1 数据结构
固件信息存储在 Flash 的固定地址 CFG_OFFSET (0x081FE000),是 Bootloader 和 App 之间通信的"桥梁":
c
typedef struct FirmwareInfo {
uint32_t version; // 固件版本号(保留,当前为0)
uint32_t file_len; // 固件文件长度(字节)
uint32_t load_addr; // 固件加载地址(固定为 APP_LOAD_ADDR)
uint32_t crc32; // CRC32 校验值(保留,当前为0,未使用)
uint8_t file_name[16]; // 固件文件名
uint32_t bEnterBootloader; // ★ 核心标志:是否进入 Bootloader
} FirmwareInfo, *PFirmwareInfo;
3.2 bEnterBootloader 标志的三态语义
| 值 | 含义 | 说明 |
|---|---|---|
0xFFFF_FFFF |
未初始化/无固件 | Flash 擦除后的默认值,表示从未烧写过固件 |
1 |
需要进入 Bootloader | 上位机要求进入升级模式,下次启动停留在 Bootloader 等待固件 |
0 |
正常启动 APP | 固件已烧录完毕,正常引导到 APP |
3.3 读写函数
读取 :直接从 Flash 地址 CFG_OFFSET 读取,如果内容全 0xFF 则返回失败:
c
static int GetLocalFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
volatile PFirmwareInfo ptFlashInfo = (PFirmwareInfo)CFG_OFFSET;
if (ptFlashInfo->file_len == 0xFFFFFFFF) // 从未烧写过
return -1;
*ptFirmwareInfo = *ptFlashInfo; // 拷贝到内存
return 0;
}
写入:先擦除固件信息区所在扇区(8KB),再逐 16 字节写入:
c
static int WriteFirmwareInfo(PFirmwareInfo ptFirmwareInfo)
{
// 1. 解锁 Flash 控制器
HAL_FLASH_Unlock();
// 2. 擦除 CFG_OFFSET 所在扇区
tEraseInit.Banks = FLASH_BANK_2;
tEraseInit.Sector = (CFG_OFFSET - 0x08000000 - 0x100000) / SECTOR_SIZE;
tEraseInit.NbSectors = 1;
HAL_FLASHEx_Erase(&tEraseInit, &SectorError);
// 3. 逐 16 字节写入 FirmwareInfo
for (int i = 0; i < sizeof(FirmwareInfo); i += 16)
HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD, flash_addr, (uint32_t)src_buf);
HAL_FLASH_Lock();
}
四、Bootloader 启动流程
4.1 启动流程图
上电 / 复位
│
▼
Cortex-M33 从 0x0800_0000
取向量表 → 执行 Reset_Handler
│
▼
__main() → 进入 main()
│
▼
┌── main() 入口 ────────────────────┐
│ │
│ isBootloader()? │
│ (检查函数链接地址) │
│ │ │
│ 是 Bootloader │
│ │ │
│ ▼ │
│ isNeedToUpdate() │
│ │ │
│ ┌──┴──┐ │
│ │ │ │
│ 无需 需要 │
│ 升级 升级 │
│ │ │ │
│ ▼ ▼ │
│ 跳转 停留在 │
│ APP Bootloader │
│ (调 (外设初始化 │
│ 用 + USB Modbus │
│ start 等待上位机 │
│ _app) 下发固件) │
│ │
└────────────────────────────────────┘
4.2 isBootloader() ------ 我是谁?
这是一个非常巧妙的链接时自识别技术。通过判断当前函数自身的链接地址来决定运行的是 Bootloader 还是 APP:
c
int isBootloader(void)
{
uint32_t link_addr = (uint32_t)isBootloader;
if (link_addr < APP_LOAD_ADDR) // 0x08040000
return 1; // 链接地址 < APP 起始地址 → 是 Bootloader
else
return 0; // 链接地址 >= APP 起始地址 → 是 APP
}
原理:
- Bootloader 镜像链接在
0x0800_0000区域,isBootloader函数地址 <0x0804_0000 - APP 镜像链接在
0x0804_0000区域,isBootloader函数地址 >=0x0804_0000 - 无需依赖任何外部标志,仅通过函数本身的链接地址即可区分
注意:这个函数名的选取很讲究------如果它在Bootloader中被调用且链接地址<0x08040000就返回1。该函数在两个镜像中同时存在,但由于链接地址不同,返回值不同。
4.3 isNeedToUpdate() ------ 需要升级吗?
c
int isNeedToUpdate(void)
{
FirmwareInfo tFirmwareInfo;
if (GetLocalFirmwareInfo(&tFirmwareInfo)) // ① 没有固件信息(Flash全FF)
return 1; // → 需要升级
if (tFirmwareInfo.bEnterBootloader == 1) // ② 标志明确要求进入Bootloader
return 1; // → 需要升级
if (tFirmwareInfo.bEnterBootloader == 0) // ③ 标志明确要求启动APP
return 0; // → 无需升级
return 1; // ④ 其他情况(比如标志既不是0也不是1),保守起见进入升级
}
4.4 main() 中的决策逻辑
c
int main(void)
{
// 在 HAL 初始化之前执行!
if (isBootloader() && !isNeedToUpdate())
{
// 情况:当前是 Bootloader,且无需升级
// → 直接跳转到 APP,不再执行 HAL_Init() 等初始化
start_app(get_app_vector());
}
// 以下两种情况会执行到:
// ① 当前是 APP(isBootloader() 返回 0)
// ② 当前是 Bootloader 且需要升级
HAL_Init();
SystemClock_Config();
// ... 外设初始化 ...
// ... 启动 FreeRTOS ...
}
为什么跳转放在 HAL_Init() 之前?
因为 APP 有自己的向量表和中断服务函数,如果在 Bootloader 中初始化了外设和中断,跳转到 APP 后中断状态不一致可能导致异常。所以"发现无需升级"就立即跳转,不碰任何外设。
4.5 start_app() ------ 汇编实现的跳转
assembly
start_app PROC
EXPORT start_app
; 参数 r0 = APP 向量表基地址 (0x08040000)
; 1. 设置 VTOR(向量表偏移寄存器)
; 告诉 CPU 中断向量表的新位置
LDR R1, =0xE000ED08 ; VTOR 寄存器地址
STR R0, [R1] ; VTOR = 0x08040000
; 2. 读取 APP 向量表的第一个字 → 主栈指针 MSP
LDR R1, [R0] ; R1 = *(0x08040000) = 新的 MSP
MOV SP, R1 ; 设置主栈指针
; 3. 读取 APP 向量表的第二个字 → Reset_Handler 地址
LDR R1, [R0, #4] ; R1 = *(0x08040004) = Reset_Handler
BX R1 ; 跳转到 APP 的 Reset_Handler
ENDP
ARM Cortex-M 向量表规范:
向量表基地址 (VTOR):
+0x00: 栈顶指针 (MSP) ← start_app 中 MOV SP, R1
+0x04: Reset_Handler ← start_app 中 BX R1
+0x08: NMI_Handler
+0x0C: HardFault_Handler
...
跳转到 APP 的 Reset_Handler 后,APP 会重新执行完整的启动流程(包括再次初始化 HAL、系统时钟、外设等),相当于一次"热启动"。
五、IAP 固件升级协议
5.1 升级触发方式
上位机通过 Modbus 协议发送 Write Single Register 请求,向 MODBUS_UPDATE_REG_ADDR(寄存器地址 0)写入私有命令:
| 写入值 | 宏定义 | 含义 |
|---|---|---|
0x55 |
MODBUS_PRIVATE_CMD_ENTER_BOOT |
进入 Bootloader,准备升级 |
0xAA |
MODBUS_PRIVATE_CMD_ENTER_APP |
启动 Application |
升级主控自身(channel == 0)
c
// process_emergency_cmd() 中:
if (val == MODBUS_PRIVATE_CMD_ENTER_BOOT)
{
modbus_reply(ctx, msg, msg_len, mb_mapping); // 先回复PC
ResetToBootloader(); // 写 FirmwareInfo 并软复位
}
ResetToBootloader() 的完整流程:
c
void ResetToBootloader(void)
{
FirmwareInfo tFirmwareInfo;
memset(&tFirmwareInfo, 0xff, sizeof(FirmwareInfo));
GetLocalFirmwareInfo(&tFirmwareInfo); // 读取当前固件信息
tFirmwareInfo.bEnterBootloader = 1; // 设置"需要进入Bootloader"
WriteFirmwareInfo(&tFirmwareInfo); // 写入Flash
SoftReset(); // 软件复位
}
5.2 固件传输协议(Modbus Write File Record)
固件数据通过 Modbus Write File Record 功能码(0x15)传输,分为文件头和数据包两种类型。
文件头(record_no == 0)
Modbus PDU:
┌──────┬──────────┬──────────┬──────────┬──────────┬──────────────────────┐
│ FC │ RefType │ FileNo │ RecordNo │ Len │ Data │
│ 0x15 │ 0x06 │ 0x0100 │ 0x0000 │ 0x00?? │ FileInfo 结构体 │
└──────┴──────────┴──────────┴──────────┴──────────┴──────────────────────┘
↑ ↑
file_no=0x0100 record_no=0 表示文件头
channel=0(主控)
file_type=1(固件)
burn_firmware() 收到文件头后:
- 解析
FileInfo,获取文件名和文件总长度 - 调用
EraseFlash(APP_LOAD_ADDR, file_len)擦除目标区域 - 调用
EraseFlash(CFG_OFFSET, SECTOR_SIZE)擦除固件信息区 - 重置接收计数器
recv_len = 0和flash_addr = APP_LOAD_ADDR
数据包(record_no >= 1)
Modbus PDU:
┌──────┬──────────┬──────────┬──────────┬──────────┬──────────────────────┐
│ FC │ RefType │ FileNo │ RecordNo │ Len │ Data │
│ 0x15 │ 0x06 │ 0x0100 │ 0x0001 │ 0x00?? │ 固件数据块 (N字节) │
└──────┴──────────┴──────────┴──────────┴──────────┴──────────────────────┘
↑
record_no=1,2,3...
burn_firmware() 收到数据包后:
c
void burn_firmware(uint8_t *msg, uint16_t msg_len)
{
if (record_no == 0) {
// 文件头:解析 FileInfo,擦除 Flash
EraseFlash(APP_LOAD_ADDR, tFileInfo.file_len);
EraseFlash(CFG_OFFSET, SECTOR_SIZE);
recv_len = 0;
flash_addr = APP_LOAD_ADDR;
} else {
// 数据包:写入 Flash
cur_len = msg[2] - 7; // 计算有效数据长度
recv_len += cur_len;
WriteFirmware(&msg[10], cur_len, flash_addr); // 写入 Flash
flash_addr += cur_len; // 地址递增
if (recv_len >= tFileInfo.file_len) {
// 固件接收完毕,写入 FirmwareInfo
tFirmwareInfo.bEnterBootloader = 0; // 下次启动进入 APP
tFirmwareInfo.file_len = tFileInfo.file_len;
tFirmwareInfo.load_addr = APP_LOAD_ADDR;
strcpy((char *)tFirmwareInfo.file_name, tFileInfo.file_name);
WriteFirmwareInfo(&tFirmwareInfo);
}
}
}
5.3 Flash 擦除操作
STM32H5 的 Flash 分为 Bank1 和 Bank2,各 1MB。擦除时需要考虑跨 Bank 的情况:
c
int EraseFlash(uint32_t flash_addr, uint32_t len)
{
uint32_t sectors = (len + SECTOR_SIZE - 1) / SECTOR_SIZE;
uint32_t flash_offset = flash_addr - 0x08000000;
HAL_FLASH_Unlock();
// 如果起始地址在 Bank1 范围内
if (flash_offset < 0x100000)
{
tEraseInit.Banks = FLASH_BANK_1;
tEraseInit.Sector = flash_offset / SECTOR_SIZE;
bank_sectors = (0x100000 - flash_offset) / SECTOR_SIZE;
erased_sectors = min(sectors, bank_sectors);
tEraseInit.NbSectors = erased_sectors;
HAL_FLASHEx_Erase(&tEraseInit, &SectorError);
}
// 如果还有剩余扇区要擦除(进入 Bank2)
sectors -= erased_sectors;
if (sectors)
{
tEraseInit.Banks = FLASH_BANK_2;
// ...
HAL_FLASHEx_Erase(&tEraseInit, &SectorError);
}
HAL_FLASH_Lock();
}
5.4 Flash 编程操作
写入时采用 Quad Word(16字节) 编程模式,这是 STM32H5 支持的最快编程方式:
c
static int WriteFirmware(uint8_t *firmware_buf, uint32_t len, uint32_t flash_addr)
{
HAL_FLASH_Unlock();
len = (len + 15) & ~15; // 向上对齐到16的倍数
for (int i = 0; i < len; i += 16)
{
HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD,
flash_addr,
(uint32_t)firmware_buf);
flash_addr += 16;
firmware_buf += 16;
}
HAL_FLASH_Lock();
}
FLASH_TYPEPROGRAM_QUADWORD一次写入 16 字节,相比单字(4字节)编程速度提升约 4 倍。
六、升级过程中的状态保护
6.1 Flash 编程互斥
g_bFlashProgramming 标志用于指示是否正在编程 Flash:
c
static int g_bFlashProgramming = 0;
int isFlashProgramming(void)
{
return g_bFlashProgramming;
}
在 EraseFlash() 和 WriteFirmware() 中,该标志在操作开始时置 1,操作结束后清 0。
6.2 升级期间暂停通道数据采集
当主控正在升级或向下转发固件时,通道任务暂停对传感器数据采集:
c
// control.c
static int g_bUpdating = 0;
void SetUpdateStatus(int on) {
g_bUpdating = on;
}
int isUpdating(void) {
return g_bUpdating;
}
在 CHn_Task 中:
c
while (1)
{
if (!isUpdating()) // 不在升级中 → 正常采集
{
loop_once(ctx, ptChannelInfo->channel, mb_mapping);
}
else // 正在升级 → 暂停,等待
{
wait_cnt++;
if (wait_cnt >= 1000) // 超时100秒,强制退出升级状态
SetUpdateStatus(0);
}
vTaskDelay(100);
}
6.3 紧急命令的"先回复后复位"策略
当上位机写入 0x55(进入Bootloader)时,如果直接复位,Modbus 回复帧就来不及发出,上位机会收到超时错误。所以代码中:
c
if (val == MODBUS_PRIVATE_CMD_ENTER_BOOT)
{
modbus_reply(ctx, msg, msg_len, mb_mapping); // ★ 先回复!
ResetToBootloader(); // 再复位
}
七、多级升级:主控作为"升级代理"
7.1 三级升级体系
| 级别 | 升级路径 | 函数 | 说明 |
|---|---|---|---|
| L1 | 上位机 → 主控 | burn_firmware() |
升级主控自身固件 |
| L2 | 上位机 → 主控 → 传感器 | send_firmware_to_device() |
主控转发固件给下级传感器 |
| L3 | 上位机远程复位控制 | process_emergency_cmd() |
通过私有命令控制主控/传感器复位 |
7.2 固件转发流程
当 file_no 中 channel != 0 时,process_file_record() 识别出这是需要转发的固件包:
c
static int send_firmware_to_device(uint8_t *msg, uint16_t msg_len)
{
file_no = ((uint16_t)msg[4]<<8) | msg[5]; // 如 0x2103
channel = (file_no >> 12) & 0xf; // channel = 2 → 走 CH2
dev_addr = (file_no) & 0xff; // dev_addr = 3 → 从机地址3
file_no = (file_no >> 8) & 0xf; // file_no = 1 → 固件数据
ptChannelInfo = get_channelinfo(channel);
xSemaphoreTake(ptChannelInfo->xMutex, portMAX_DELAY);
modbus_set_slave(ptChannelInfo->ctx, dev_addr); // 选中目标从机
rc = modbus_write_file_record(ptChannelInfo->ctx,
file_no, // 1:固件
record_no, // 分包序号
&msg[10], len); // 数据
xSemaphoreGive(ptChannelInfo->xMutex);
}
八、启动与升级的完整时序
8.1 首次烧录(空芯片)
上电 → Flash全FF → isBootloader()=1(从0x08000000启动)
→ isNeedToUpdate()=1(无FirmwareInfo)
→ 停留在Bootloader
→ 上位机通过USB连接
→ 下发文件头(record_no=0) → 擦除APP区+配置区
→ 下发固件数据包(record_no=1~N) → 写入Flash
→ 固件接收完毕 → 写入FirmwareInfo(bEnterBootloader=0)
→ 上位机写0x55 → ResetToBootloader()
→ 复位 → isBootloader()=1
→ isNeedToUpdate()=0(bEnterBootloader=0)
→ start_app(0x08040000) → 跳转到APP
8.2 正常运行中远程升级
APP 正常运行
→ 上位机通过Modbus写寄存器0 = 0x55
→ process_emergency_cmd()检测到紧急命令
→ modbus_reply()先回复PC
→ ResetToBootloader()
→ 写FirmwareInfo(bEnterBootloader=1)
→ SoftReset()
→ 复位 → isBootloader()=1
→ isNeedToUpdate()=1(bEnterBootloader==1)
→ 停留在Bootloader
→ 上位机下发新固件(同上)
→ 固件接收完毕 → FirmwareInfo(bEnterBootloader=0)
→ 上位机写0xAA → ResetToApplication()
→ 写FirmwareInfo(bEnterBootloader=0)
→ SoftReset()
→ 复位 → isBootloader()=1
→ isNeedToUpdate()=0(bEnterBootloader==0)
→ start_app(0x08040000) → 跳转新APP
九、关键设计要点总结
✅ 设计亮点
| 设计 | 说明 |
|---|---|
| 双区独立镜像 | Bootloader 和 APP 各有一个链接脚本,编译成两个独立的 .axf/.hex 文件 |
| 链接自识别 | isBootloader() 通过函数链接地址判断当前运行的是 BL 还是 APP,无需外部标志 |
| 提前跳转 | main() 中在 HAL_Init() 之前就判断是否跳转,避免外设状态冲突 |
| 先回复后复位 | 紧急命令先回 Modbus 应答再复位,避免上位机超时 |
| 多级升级 | 主控不仅能升级自己,还能向下转发固件给传感器 |
| 状态保护 | 升级期间暂停数据采集,防止 Flash 操作与通信冲突 |
⚠️ 注意事项与潜在改进
| 问题 | 现状 | 改进建议 |
|---|---|---|
| CRC校验 | crc32 字段保留但未使用 |
固件接收完成后进行 CRC32 校验,防止损坏的固件被写入 |
| 固件备份 | 无备份,升级失败可能变砖 | 保留一份"回退固件"在 Bank2 中,升级失败自动回滚 |
| 超时保护 | wait_cnt 100秒超时较粗暴 |
根据固件大小和波特率动态计算合理超时 |
| 安全升级 | 无加密/签名验证 | 增加固件签名验证,防止恶意固件被烧录 |
十、总结
本项目实现了一套工业级、生产可用的双区 IAP 升级方案。其核心设计可以概括为:
一个结构体 + 两个独立镜像 + 三个升级层级
- 一个结构体 :
FirmwareInfo存储在 Bank2 末尾,作为 Bootloader 和 APP 之间的通信桥梁- 两个独立镜像:Bootloader(0x0800_0000)和 APP(0x0804_0000),各有独立链接脚本
- 三个升级层级:主控自升级 → 转发传感器升级 → 远程复位控制
这套框架适用于需要远程固件管理的工业物联网设备,如数据采集网关、边缘计算节点、分布式传感器网络等场景。