为什么需要自定义 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区执行。
跳转的关键步骤:
- 检查App区首字是否为合法栈顶地址(通常为
0x20000000到RAM末尾之间的值)。 - 设置主堆栈指针(MSP)为App的栈顶地址。
- 将函数指针指向App的复位向量,并跳转执行。
串口通信协议设计
为了可靠传输固件,自定义简单协议:
-
帧格式:
帧头(0xAA55) + 命令(1字节) + 长度(2字节) + 数据(长度字节) + CRC16(2字节) -
命令定义:
-
0x01:握手,获取版本信息 -
0x02:擦除App区 -
0x03:写入数据(每帧最多256字节) -
0x04:升级完成,校验并跳转
-
CRC16采用标准多项式 0x8005,保证数据传输完整性。
配置步骤
- 创建工程:使用STM32CubeMX生成基础工程,选择对应型号(如STM32F103C8),配置USART1(波特率115200)、LED指示灯、按键输入。
-
设置Flash起始地址:在工程选项中,将IROM1起始地址改为
0x08004000,大小设为0x1C000(若Flash为64KB)。这是App工程的设置,Bootloader工程保持默认。 - 编写Bootloader代码:实现串口接收、协议解析、Flash擦写和跳转逻辑。
- 编写App测试程序:在App中实现一个简单的LED闪烁,并打印版本号,验证跳转是否成功。
完整代码示例
以下为Bootloader核心代码(基于STM32 HAL库):
/* 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 开头添加:
/* 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 开头执行:
SCB->VTOR = APP_START_ADDR;
注意事项
- Flash擦写时序:擦除前必须关闭全局中断,避免中断向量表冲突。
- 串口缓冲区:建议使用环形缓冲区,避免数据丢失。
- 校验机制:除了CRC,可在升级完成后对App区进行整体校验(如累加和)。
- 看门狗:升级过程中喂狗,防止复位。
- App起始地址对齐:通常为0x200的倍数,确保Flash页对齐。
- 测试环境:先用USB转TTL模块连接PC,通过串口助手发送固件文件(需将生成的bin文件按协议分包发送)。
总结
本文从原理到实现,详细介绍了基于STM32的串口IAP Bootloader设计。通过合理的Flash分区、可靠的通信协议和严谨的跳转逻辑,可以实现稳定的固件升级功能。实际产品中,可进一步扩展为OTA(通过无线模块)或加密升级,提升安全性和便利性。希望本文能帮助你快速上手,打造属于自己的Bootloader。