在STM32F103上实现一个简单的Bootloader
在STM32F103上实现了一个简单的Bootloader,可以从 SPI Flash 加载应用代码执行,支持使用串口下载/更新 SPI Flash 中的应用代码。
完整demo代码下载地址:https://www.jiawei.site/downloads/stm32/simple_bootloader.zip
Bootloader 简介
Bootloader是在嵌入式系统上电后最先运行的一段程序,负责完成系统启动前的准备工作。
在MCU开发中,Bootloader通常用于以下任务:
- 加载应用程序: 从片内 Flash、外部存储器(如 SPI Flash、SD 卡) 或通信接口读取应用程序代码,并将其复制到指定位置执行。
- 固件更新: 通过 UART、USB、CAN 等通信接口接收新固件, 实现程序在线升级。
- 系统初始化与安全验证: 在启动前进行硬件初始化、程序完整性校验或安全认证。
实现方案
主要的硬件设备
- MCU: STM32F103ZETX
- SPI Flash: W25Q128BV
系统架构
整个系统分为两大部分:Bootloader程序和应用程序。
Bootloader位于MCU片内FLash,应用程序位于外部SPI Flash。 系统上电后,MCU首先运行Bootloader,由其决定是否加载 外部应用并跳转执行。
1. 系统组成
| 模块 | 存储位置 | 主要功能 |
|---|---|---|
| Bootloader | 片内 Flash | 初始化外设、判断启动模式、从 SPI Flash 读取应用到 RAM、跳转执行 |
| 应用程序 | 外部 SPI Flash | 用户应用程序代码,通过 UART 下载更新 |
| SPI Flash 驱动 | Bootloader 内 | 负责读取和写入外部 Flash |
| UART 下载模块 | Bootloader 内 | 接收新固件数据并写入 SPI Flash |
| RAM 运行区 | 片内 SRAM | 存放从 SPI Flash 加载的应用程序镜像 |
2. 启动流程
系统上电或复位后,执行以下步骤:
- Bootloader 启动: MCU 从片内Flash 起始地址(0x8000000)开始执行。
- 模式判断:
Bootloader 检查某个按键状态,判断是否进入“下载模式”。
- 若进入下载模式:通过 UART 接收新的固件数据,并写入到 SPI Flash。
- 否则,进入加载模式。
- 加载应用程序: Bootloader 通过 SPI 接口 从外部 Flash 读取应用镜像,并复制到指定的 RAM 地址(0x20003000)。
- 跳转执行: Bootloader 设置应用程序的栈顶地址和复位向量地址,然后跳转到应用程序入口函数执行。
实现细节
Bootloader
链接脚本中,在堆栈段后新增加段.app_buf,起始地址为0x20003000,
为外部应用代码加载到片内SRAM的位置:
.app_buf 0x20003000 (NOLOAD):
{
*(.app_buf)
} >RAM
main()函数中,执行按键检测,如果复位后按键是按下的,执行
从串口下载应用程序到SPI Flash的流程;否则,从SPI Flash加载
应用程序并跳转执行:
int main(void)
{
// 外设(gpio, uart, spi, crc等模块)初始化
GPIO_PinState key_state = HAL_GPIO_ReadPin(key0_GPIO_Port, key0_Pin);
if(key_state == GPIO_PIN_RESET){
HAL_Delay(10);
key_state = HAL_GPIO_ReadPin(key0_GPIO_Port, key0_Pin);
}
if(key_state == GPIO_PIN_RESET){
// 按键是按下的,执行下载应用流程
while(!receive_burn_request()); // 读取下载应用请求
send_burn_ack(1); // 下载应用请求收到,回复ack,开始接收应用代码
do{
if(receive_burn_data()){
// 应用代码收到并校验成功,回复ack
send_burn_ack(1);
break;
} else{
// 应用代码接收出错,回复nack后启用重传
send_burn_ack(0);
}
} while(1);
if(!burn_data()){ // 应用烧写到SPI Flash并执行校验
while(1){
HAL_GPIO_TogglePin(led1_GPIO_Port, led1_Pin);
HAL_Delay(100);
}
}
} else{
// 按键未按下,从SPI Flash加载应用并跳转执行
spi_flash_read_buffer(app_buf, APP_FLASH_OFFSET, APP_MAX_SIZE);
if(valify_app_data(app_buf))
jump_to_app((uint32_t)app_buf);
}
while(1){
// 下载应用结束后进入到此位置;
// 如果跳转到外部应用执行, 则不会进入到这里
}
}
跳转到应用代码实现如下:
typedef void (*app_entry_t)(void);
void jump_to_app(uint32_t app_addr)
{
// 从应用代码的向量表获取应用入口地址和主栈指针
uint32_t app_stack = *(uint32_t *)app_addr;
uint32_t app_reset = *(uint32_t *)(app_addr + 4U);
app_entry_t app_entry = (app_entry_t)app_reset;
__disable_irq();
SysTick->CTRL = 0;
HAL_DeInit();
// 修改限量表起始地址为应用的向量表起始地址(0x20003000)
SCB->VTOR = app_addr;
// 设置主栈和程序栈指针
__set_MSP(app_stack);
__set_PSP(app_stack);
__enable_irq();
app_entry();
}
外部应用程序
修改默认的链接脚本,MEMORY块仅使用RAM:
MEMORY
{
RAM (xrw) : ORIGIN = 0x20003000, LENGTH = 52K
}
.text、.rodata等段的位置要修改为位于RAM中:
.text :
{
. = ALIGN(4);
*(.text) /* .text sections (code) */
*(.text*) /* .text* sections (code) */
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
_etext = .; /* define a global symbols at end of code */
} >RAM
/* Constant data goes into RAM */
.rodata :
{
. = ALIGN(4);
*(.rodata) /* .rodata sections (constants, strings, etc.) */
*(.rodata*) /* .rodata* sections (constants, strings, etc.) */
. = ALIGN(4);
} >RAM
由于.data段的内容在从SPI Flash加载应用完成后便
初始化完成了,所以在启动代码中,以下内容不再需要:
/* Copy the data segment initializers from flash to SRAM */
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_sidata
movs r3, #0
b LoopCopyDataInit
CopyDataInit:
ldr r4, [r2, r3]
str r4, [r0, r3]
adds r3, r3, #4
LoopCopyDataInit:
adds r4, r0, r3
cmp r4, r1
bcc CopyDataInit
应用程序的其余部分无特别的地方,在main()函数中,
执行初始化操作后通过串口输出特定的字符串并让
板载LED闪烁:
int main(void)
{
// do some initializing
printf("Running user app...\r\n");
while (1){
HAL_GPIO_TogglePin(led1_GPIO_Port, led1_Pin);
HAL_Delay(1000);
}
}
测试与验证
除了用于在STM32F103上运行的代码工程外,使用 Python编写了两个脚本程序:
- app_fill.py: 用于将构建得到的应用二进制 文件进行填充并加上CRC;
- burn.py: 用于烧写应用程序到SPI Flash
其实现可见本文开头附带链接提供的完整demo代码。
执行测试过程如下:
- 构建并烧写Bootloader到片上Flash;
- 构建应用,将构建得到的二进制文件
app.bin拷贝 到app_fill.py所在的路径下,然后运行app_fill.py为app.bin执行填充并写入CRC; - 将
app.bin下载到SPI Flash:按住用于检测是否烧写 应用代码的按键并让板复位,此时进入下载应用代码流程, 确保板已通过串口连接至PC,运行burn.py; - 下载完成后,使板复位,在此过程中不要按下检测 是否烧写应用的按键;
- 如果以上步骤均执行正确,跳转到应用代码后可看到 应用控制的LED闪烁。串口输出"Running user app…"。