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

所有评论(0)