STM32H5_Bootloader与IAP升级机制深度解析

文章目录

  • [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() 收到文件头后:

  1. 解析 FileInfo,获取文件名和文件总长度
  2. 调用 EraseFlash(APP_LOAD_ADDR, file_len) 擦除目标区域
  3. 调用 EraseFlash(CFG_OFFSET, SECTOR_SIZE) 擦除固件信息区
  4. 重置接收计数器 recv_len = 0flash_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 分为 Bank1Bank2,各 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_nochannel != 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),各有独立链接脚本
  • 三个升级层级:主控自升级 → 转发传感器升级 → 远程复位控制

这套框架适用于需要远程固件管理的工业物联网设备,如数据采集网关、边缘计算节点、分布式传感器网络等场景。

相关推荐
达子6663 小时前
Zephyr RTOS 学习系统:第一阶段 —— 入门篇
stm32·单片机·嵌入式硬件
‎ദ്ദിᵔ.˛.ᵔ₎8 小时前
STM32入门第一课:32位MCU
stm32·单片机
Freedom_my11 小时前
STM32简单项目
stm32·单片机·嵌入式硬件
周洲083011 小时前
STM32+ESP8266 WiFi 远程环境监测平台|本地 OLED 显示 + TCP 上传电脑 / 手机
stm32·tcp/ip·电脑
GeekArch12 小时前
第8讲:裸机单片机项目——Vibe快速迭代的最优边界
c语言·stm32·单片机·嵌入式硬件·mcu·物联网·ai编程
辰哥单片机设计1 天前
STM32音乐播放器
stm32·单片机·嵌入式硬件
周洲08301 天前
STM32 串口指令控制系统|上位机下发命令控制 LED、继电器,通用指令解析框架
stm32·嵌入式硬件·mongodb
嵌入式-老费1 天前
esp32开发与应用(和stm32f103用spi通信)
stm32·单片机·嵌入式硬件
GeekArch2 天前
第5讲:嵌入式场景二范式决策树:什么情况快写、什么情况严写
人工智能·stm32·单片机·mcu·物联网·决策树·机器学习