在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. 启动流程

系统上电或复位后,执行以下步骤:

  1. Bootloader 启动: MCU 从片内Flash 起始地址(0x8000000)开始执行。
  2. 模式判断: Bootloader 检查某个按键状态,判断是否进入“下载模式”。
    • 若进入下载模式:通过 UART 接收新的固件数据,并写入到 SPI Flash。
    • 否则,进入加载模式。
  3. 加载应用程序: Bootloader 通过 SPI 接口 从外部 Flash 读取应用镜像,并复制到指定的 RAM 地址(0x20003000)。
  4. 跳转执行: 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代码。

执行测试过程如下:

  1. 构建并烧写Bootloader到片上Flash;
  2. 构建应用,将构建得到的二进制文件app.bin拷贝 到app_fill.py所在的路径下,然后运行 app_fill.pyapp.bin执行填充并写入CRC;
  3. app.bin下载到SPI Flash:按住用于检测是否烧写 应用代码的按键并让板复位,此时进入下载应用代码流程, 确保板已通过串口连接至PC,运行burn.py;
  4. 下载完成后,使板复位,在此过程中不要按下检测 是否烧写应用的按键;
  5. 如果以上步骤均执行正确,跳转到应用代码后可看到 应用控制的LED闪烁。串口输出"Running user app…"。

参考