STM32 自定义 Bootloader:串口 IAP 升级从原理到实践
👁 8 阅读 · 2026-08-19 · STM32 Bootloader
在嵌入式产品开发中,固件升级能力至关重要。本文深入讲解基于STM32的串口IAP(In-Application Programming)自定义Bootloader设计,涵盖Flash分区、跳转逻辑、串口通信协议及CRC校验等核心要点,并给出完整可运行的代码示例,帮助开发者快速实现稳定可靠的远程固件升级方案。
## 为什么需要自定义 Bootloader
在产品量产或现场维护时,通过JTAG/SWD烧录固件既不方便也不现实。自定义Bootloader允许设备通过串口、CAN或网络等接口接收新固件,并写入内部Flash,实现IAP升级。这不仅能降低维护成本,还能支持远程升级,是产品化必备技术。
## 核心原理:Flash 分区与跳转
STM32内部Flash通常分为两个区域:
- **Bootloader区**:存放引导程序,起始地址 `0x08000000`,大小可设为16KB或32KB。
- **App区**:存放用户应用程序,起始地址 `0x08004000`(若Bootloader为16KB),大小根据Flash容量而定。
系统上电后,CPU从 `0x08000000` 执行Bootloader。Bootloader判断是否需要升级:若需要,则接收固件并写入App区;否则直接跳转到App区执行。
跳转的关键步骤:
1. 检查App区首字是否为合法栈顶地址(通常为 `0x20000000` 到RAM末尾之间的值)。
2. 设置主堆栈指针(MSP)为App的栈顶地址。
3. 将函数指针指向App的复位向量,并跳转执行。
## 串口通信协议设计
为了可靠传输固件,自定义简单协议:
- **帧格式**:`帧头(0xAA55) + 命令(1字节) + 长度(2字节) + 数据(长度字节) + CRC16(2字节)`
- **命令定义**:
- `0x01`:握手,获取版本信息
- `0x02`:擦除App区
- `0x03`:写入数据(每帧最多256字节)
- `0x04`:升级完成,校验并跳转
CRC16采用标准多项式 `0x8005`,保证数据传输完整性。
## 配置步骤
1. **创建工程**:使用STM32CubeMX生成基础工程,选择对应型号(如STM32F103C8),配置USART1(波特率115200)、LED指示灯、按键输入。
2. **设置Flash起始地址**:在工程选项中,将IROM1起始地址改为 `0x08004000`,大小设为 `0x1C000`(若Flash为64KB)。这是App工程的设置,Bootloader工程保持默认。
3. **编写Bootloader代码**:实现串口接收、协议解析、Flash擦写和跳转逻辑。
4. **编写App测试程序**:在App中实现一个简单的LED闪烁,并打印版本号,验证跳转是否成功。
## 完整代码示例
以下为Bootloader核心代码(基于STM32 HAL库):
```c
/* bootloader.c */
#include "main.h"
#include "flash_if.h"
#include "crc16.h"
#define APP_START_ADDR 0x08004000
#define APP_MAX_SIZE 0x1C000 // 112KB,根据实际调整
uint8_t rx_buffer[256];
uint32_t app_write_addr;
void JumpToApp(void) {
uint32_t app_stack = *(volatile uint32_t*)APP_START_ADDR;
if ((app_stack & 0x2FFE0000) != 0x20000000) {
// 栈顶地址不合法,停留在Bootloader
return;
}
void (*app_reset)(void) = (void (*)(void))(*(volatile uint32_t*)(APP_START_ADDR + 4));
__set_MSP(app_stack);
app_reset();
}
void ProcessCommand(uint8_t cmd, uint8_t *data, uint16_t len) {
switch (cmd) {
case 0x01: // 握手
SendResponse(0x01, "BOOT_V1.0", 9);
break;
case 0x02: // 擦除
FLASH_If_Erase(APP_START_ADDR, APP_MAX_SIZE);
app_write_addr = APP_START_ADDR;
SendResponse(0x02, NULL, 0);
break;
case 0x03: // 写入
if (FLASH_If_Write(app_write_addr, data, len) == 0) {
app_write_addr += len;
SendResponse(0x03, NULL, 0);
} else {
SendResponse(0x03, "ERR", 3);
}
break;
case 0x04: // 完成
SendResponse(0x04, NULL, 0);
JumpToApp();
break;
default:
SendResponse(0xFF, NULL, 0);
break;
}
}
void USART_RX_IRQHandler(void) {
// 接收一帧数据,解析并调用ProcessCommand
// 具体实现略,需处理帧头、长度、CRC校验
}
```
App工程中,需在 `main.c` 开头添加:
```c
/* app_main.c */
#include "stm32f1xx_hal.h"
void SystemClock_Config(void);
int main(void) {
HAL_Init();
SystemClock_Config();
// 初始化LED、串口等
while (1) {
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
HAL_Delay(500);
printf("App running...\r\n");
}
}
```
注意:App工程中必须将中断向量表重映射到 `0x08004000`,在 `SystemInit` 或 `main` 开头执行:
```c
SCB->VTOR = APP_START_ADDR;
```
## 注意事项
- **Flash擦写时序**:擦除前必须关闭全局中断,避免中断向量表冲突。
- **串口缓冲区**:建议使用环形缓冲区,避免数据丢失。
- **校验机制**:除了CRC,可在升级完成后对App区进行整体校验(如累加和)。
- **看门狗**:升级过程中喂狗,防止复位。
- **App起始地址对齐**:通常为0x200的倍数,确保Flash页对齐。
- **测试环境**:先用USB转TTL模块连接PC,通过串口助手发送固件文件(需将生成的bin文件按协议分包发送)。
## 总结
本文从原理到实现,详细介绍了基于STM32的串口IAP Bootloader设计。通过合理的Flash分区、可靠的通信协议和严谨的跳转逻辑,可以实现稳定的固件升级功能。实际产品中,可进一步扩展为OTA(通过无线模块)或加密升级,提升安全性和便利性。希望本文能帮助你快速上手,打造属于自己的Bootloader。