STM32 Bootloader 自升级:IAP 方案完整设计

先说结论

STM32 IAP 方案适合有串口/USB/CAN 通信通道的产品。Bootloader 约 8-16KB,占 Flash 前 2-4 页,App 从第 5 页开始。升级流程 10 秒内完成。

产品出厂后固件需要更新,但 MCU 没有 OTA 云端推送。IAP(In-Application Programming)是最经典的方案:MCU 内部跑两段代码——Bootloader 和 App。

  • Bootloader:负责接收新固件写入 Flash
  • App:负责业务逻辑

升级时重启进 Bootloader,写完再跳到 App。


一、Flash 分区设计

以 STM32F407VE(512KB Flash)为例:

0x08000000 - 0x08003FFF  Bootloader  16KB   (4页×4KB)
0x08004000 - 0x08007FFF  参数区       16KB   (NVS/标志位)
0x08008000 - 0x0807FFFF  App固件      480KB  (业务代码)

分区表:

#define BOOTLOADER_ADDR  0x08000000
#define BOOTLOADER_SIZE  0x4000      // 16KB

#define PARAM_ADDR       0x08004000
#define PARAM_SIZE       0x4000      // 16KB

#define APP_ADDR         0x08008000
#define APP_SIZE         0x78000     // 480KB

// 升级标志
#define FLAG_ADDR        (PARAM_ADDR + 0x00)
#define FLAG_UPGRADE     0xDEADBEEF  // 需要升级
#define FLAG_NORMAL      0x12345678  // 正常启动
#define FLAG_RECOVERY    0xCAFEBABE  // 恢复模式

// 固件信息
#define FW_INFO_ADDR    (PARAM_ADDR + 0x10)
#define FW_SIZE_ADDR    (PARAM_ADDR + 0x14)
#define FW_CRC_ADDR     (PARAM_ADDR + 0x18)

二、Bootloader 主流程

#include "stm32f4xx_hal.h"
#include "flash.h"

typedef void (*pFunction)(void);

int main(void) {
    HAL_Init();
    SystemClock_Config();

    // 初始化通信接口(UART)
    UART_Init();

    // 检查升级标志
    uint32_t flag = *(__IO uint32_t *)FLAG_ADDR;

    if (flag == FLAG_UPGRADE) {
        printf("Upgrade mode\n");
        firmware_upgrade_mode();
    } else if (flag == FLAG_RECOVERY) {
        printf("Recovery mode\n");
        recovery_mode();
    } else {
        // 正常启动 → 跳转到App
        if (check_app_valid()) {
            jump_to_app();
        } else {
            printf("App invalid, entering upgrade mode\n");
            firmware_upgrade_mode();
        }
    }

    while (1);
}

// 检查App是否有效
int check_app_valid(void) {
    // 检查栈顶地址是否合法
    uint32_t stack_ptr = *(__IO uint32_t *)APP_ADDR;
    if ((stack_ptr & 0xFFFE0000) != 0x20000000) {
        return 0;  // 栈指针不在SRAM范围
    }

    // 检查CRC
    uint32_t expected_crc = *(__IO uint32_t *)FW_CRC_ADDR;
    uint32_t actual_crc = calc_crc32(
        (uint8_t *)APP_ADDR,
        *(__IO uint32_t *)FW_SIZE_ADDR
    );

    return expected_crc == actual_crc;
}

// 跳转到App
void jump_to_app(void) {
    // 关闭所有中断
    __disable_irq();

    // 关闭外设
    HAL_DeInit();

    // 清除所有中断挂起位
    for (int i = 0; i < 8; i++) {
        NVIC->ICER[i] = 0xFFFFFFFF;
        NVIC->ICPR[i] = 0xFFFFFFFF;
    }

    // 设置向量表
    SCB->VTOR = APP_ADDR;

    // 获取App的栈指针和复位地址
    uint32_t app_sp = *(__IO uint32_t *)APP_ADDR;
    uint32_t app_pc = *(__IO uint32_t *)(APP_ADDR + 4);

    // 设置MSP
    __set_MSP(app_sp);

    // 跳转
    ((pFunction)app_pc)();
}

三、固件升级流程

Bootloader 接收固件数据并写入 Flash:

#define PACKET_SIZE  1024  // 每包1KB
#define MAX_RETRY    3

// 升级协议帧格式:
// [0x55] [0xAA] [cmd] [data_len] [data...] [crc_h] [crc_l]
typedef enum {
    CMD_HANDSHAKE    = 0x01,
    CMD_ERASE        = 0x02,
    CMD_WRITE        = 0x03,
    CMD_READ         = 0x04,
    CMD_VERIFY       = 0x05,
    CMD_FINISH       = 0x06,
    CMD_JUMP         = 0x07,
    CMD_NACK         = 0xFF,
} cmd_t;

void firmware_upgrade_mode(void) {
    printf("Waiting for firmware...\n");

    uint32_t total_size = 0;
    uint32_t write_offset = 0;

    while (1) {
        uint8_t rx_buf[PACKET_SIZE + 8];
        int len = uart_receive(rx_buf, sizeof(rx_buf), 5000);

        if (len <= 0) {
            // 超时
            send_nack();
            continue;
        }

        // 校验帧头
        if (rx_buf[0] != 0x55 || rx_buf[1] != 0xAA) {
            send_nack();
            continue;
        }

        // CRC校验
        uint16_t crc = crc16(rx_buf, len - 2);
        uint16_t rx_crc = (rx_buf[len-2] << 8) | rx_buf[len-1];
        if (crc != rx_crc) {
            send_nack();
            continue;
        }

        uint8_t cmd = rx_buf[2];
        uint16_t data_len = (rx_buf[3] << 8) | rx_buf[4];
        uint8_t *data = &rx_buf[5];

        switch (cmd) {
            case CMD_HANDSHAKE:
                // 握手:返回Bootloader版本和Flash大小
                send_handshake_response();
                break;

            case CMD_ERASE:
                // 擦除App区域
                flash_erase(APP_ADDR, APP_SIZE);
                write_offset = 0;
                send_ack();
                break;

            case CMD_WRITE: {
                // 写入固件数据
                if (write_offset + data_len > APP_SIZE) {
                    send_nack();
                    break;
                }

                flash_write(APP_ADDR + write_offset, data, data_len);
                write_offset += data_len;
                send_ack();
                break;
            }

            case CMD_VERIFY: {
                // CRC校验
                uint32_t expected = (data[0]<<24)|(data[1]<<16)|
                                    (data[2]<<8)|data[3];
                uint32_t actual = calc_crc32(
                    (uint8_t *)APP_ADDR, write_offset
                );

                if (expected == actual) {
                    // 保存固件信息
                    flash_write_word(FW_SIZE_ADDR, write_offset);
                    flash_write_word(FW_CRC_ADDR, expected);
                    send_ack();
                } else {
                    send_nack();
                }
                break;
            }

            case CMD_FINISH:
                // 升级完成,清除标志
                flash_write_word(FLAG_ADDR, FLAG_NORMAL);
                printf("Upgrade complete, restarting...\n");
                HAL_Delay(100);
                NVIC_SystemReset();
                break;

            case CMD_JUMP:
                // 直接跳转到App(不升级)
                flash_write_word(FLAG_ADDR, FLAG_NORMAL);
                jump_to_app();
                break;
        }
    }
}

四、App 端触发升级

App 运行中收到升级命令后,设置标志并重启:

// App端代码
void trigger_upgrade(void) {
    // 写入升级标志
    HAL_FLASH_Unlock();
    FLASH_EraseInitTypeDef erase = {0};
    erase.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase.Sector = FLASH_SECTOR_4;  // 参数区所在扇区
    erase.NbSectors = 1;
    uint32_t page_error;
    HAL_FLASHEx_Erase(&erase, &page_error);

    HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,
                      FLAG_ADDR, FLAG_UPGRADE);
    HAL_FLASH_Lock();

    // 等数据写入完成
    HAL_Delay(50);

    // 重启进Bootloader
    NVIC_SystemReset();
}

// App启动时检查标志
void app_init(void) {
    uint32_t flag = *(__IO uint32_t *)FLAG_ADDR;
    if (flag == FLAG_UPGRADE || flag == FLAG_RECOVERY) {
        // 不应该到这里,但万一跳转失败
        NVIC_SystemReset();
    }
}

五、CRC32 校验

固件完整性校验用 CRC32:

#include "stm32f4xx_hal_crc.h"

uint32_t calc_crc32(uint8_t *data, uint32_t len) {
    // 用硬件CRC外设(STM32自带)
    hcrc.Instance = CRC;
    HAL_CRC_Init(&hcrc);

    // 4字节对齐处理
    uint32_t *data32 = (uint32_t *)data;
    uint32_t len32 = len / 4;

    uint32_t crc = HAL_CRC_Calculate(&hcrc, data32, len32);

    // 处理剩余字节
    if (len % 4 != 0) {
        uint8_t *remainder = data + len32 * 4;
        uint32_t last = 0;
        memcpy(&last, remainder, len % 4);
        // 手动计算剩余字节
        crc = crc ^ last;
    }

    return crc;
}

六、Python 升级工具

PC 端升级脚本:

import serial
import struct, zlib

class Stm32Bootloader:
    def __init__(self, port):
        self.ser = serial.Serial(port, 115200, timeout=5)

    def send_frame(self, cmd, data=b''):
        frame = bytes([0x55, 0xAA, cmd, len(data) >> 8, len(data) & 0xFF])
        frame += data
        crc = self.crc16(frame)
        frame += bytes([crc >> 8, crc & 0xFF])
        self.ser.write(frame)

    def wait_ack(self):
        resp = self.ser.read(2)
        return resp == b'\x55\xAA'

    def upgrade(self, firmware_path):
        with open(firmware_path, 'rb') as f:
            fw = f.read()

        print(f"Firmware size: {len(fw)} bytes")

        # 握手
        self.send_frame(0x01)
        assert self.wait_ack(), "Handshake failed"

        # 擦除
        self.send_frame(0x02)
        assert self.wait_ack(), "Erase failed"

        # 分包写入
        offset = 0
        packet_size = 1024
        while offset < len(fw):
            chunk = fw[offset:offset+packet_size]
            self.send_frame(0x03, chunk)
            assert self.wait_ack(), f"Write failed at offset {offset}"
            offset += len(chunk)
            progress = offset / len(fw) * 100
            print(f"\rProgress: {progress:.1f}%", end='')

        # CRC校验
        crc = zlib.crc32(fw) & 0xFFFFFFFF
        self.send_frame(0x05, struct.pack('>I', crc))
        assert self.wait_ack(), "Verify failed"

        # 完成
        self.send_frame(0x06)
        print("\nUpgrade complete!")

七、实测数据

STM32F407VE @168MHz,UART @115200bps:

步骤 耗时
擦除480KB Flash 1.2秒
写入480KB固件 45秒
CRC32校验480KB 30ms
跳转到App <1ms
总升级时间 ~47秒

如果 UART 提到 921600bps,写入时间降到 6 秒


八、踩坑清单

  1. 中断向量表:App 的向量表在 App Flash 地址。Bootloader 跳转前必须设 SCB->VTOR = APP_ADDR,否则 App 的中断跑飞
  2. 栈指针:App 固件的第一字节是初始 MSP 值。跳转前 __set_MSP() 设置,不然 HardFault
  3. Flash 解锁:每次写 Flash 前 HAL_FLASH_Unlock(),写完 HAL_FLASH_Lock()。中间有中断可能出问题
  4. 擦除粒度:STM32F4 以 Sector 为单位擦除,最小 16KB。Bootloader 和参数区各占一个 Sector,不能跨 Sector
  5. 双 Bank:STM32F42x/F43x 有双 Bank Flash,可以在运行一个 Bank 时写另一个。普通 STM32F407 单 Bank,写 Flash 时 CPU 停顿
  6. 看门狗:升级过程中喂狗。Flash 擦写耗时几秒,不看门狗会复位
  7. Bootloader 升级 Bootloader:Bootloader 自己不能升级自己。如果 Bootloader 要升级,只能用 ST-Link/JTAG
  8. 编译链接脚本:App 的链接脚本起始地址要改成 APP_ADDR。标准 STM32 项目默认从 0x08000000 开始
  9. MSP/PSP:跳转前设 MSP 不是 PSP。Bootloader 在 Handler 模式用 MSP,App 初始也在 Handler 模式
  10. 电源:升级过程中断电会导致 Flash 写了一半。加标志位 + 恢复机制:Bootloader 检测到不完整固件时进入恢复模式
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐