[信息] STM32编写Bootloader详解(含完整代码)

[复制链接]
404|0
观海 发表于 2025-11-8 10:41 | 显示全部楼层 |阅读模式
一、前期规划(关键!决定后续实现难度)
1. 明确需求与功能边界
核心功能:是否需要串口/I2C/SPI/OTA升级?是否需要固件校验(CRC/MD5)?是否需要升级失败回滚?
触发方式:按键触发、串口指令触发、定时器触发(如定时检查升级标志)?
硬件限制:Flash大小(决定Bootloader和应用分区大小)、RAM大小(接收缓冲区大小)。
2. 内存分区规划(以STM32F103C8T6为例,64KB Flash + 20KB RAM)

39821690d562995de4.png

原则:

Bootloader大小需预留冗余(基础功能16KB足够,复杂功能需32KB)。
应用程序起始地址必须是Flash扇区对齐的(如F103每页1KB,0x08004000是第4页起始)。
二、搭建Bootloader工程(CubeMX + MDK)
1. 新建CubeMX工程
选择芯片型号(如STM32F103C8T6)。
配置最小系统资源:
时钟:启用HSE,配置PLL(如72MHz,保证串口等外设稳定)。
升级通信外设:如串口(USART1,115200波特率,无校验)、GPIO(按键引脚,下拉输入)。
调试接口:保留SWD(方便调试Bootloader)。
2. 生成工程代码
选择MDK-ARM工具链,生成代码时勾选“Generate peripheral initialization as .c/.h files”。
打开生成的工程,确认时钟初始化(SystemClock_Config)、外设初始化(如MX_USART1_UART_Init)正确。
三、实现Bootloader核心功能
按“初始化→检测升级条件→升级流程→跳转应用”的逻辑编写代码。

1. 系统初始化(最小资源初始化)
仅初始化Bootloader必需的外设(避免占用过多资源影响应用跳转):

void Bootloader_Init(void) {
  HAL_Init();               // HAL库初始化( systick 等)
  SystemClock_Config();     // 时钟初始化(CubeMX生成)
  MX_USART1_UART_Init();    // 升级用串口初始化
  MX_GPIO_Init();           // 按键GPIO初始化(若用按键触发)
}


2. 升级触发条件检测
判断是否进入升级模式(示例:按键按下或串口接收“UPGRADE”指令):

uint8_t Check_Upgrade_Trigger(void) {
  // 条件1:按键按下(PA0为高电平,消抖处理)
  if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) {
    HAL_Delay(20);  // 消抖
    if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) {
      return 1;  // 进入升级模式
    }
  }

  // 条件2:串口接收“UPGRADE”指令(500ms内检测)
  uint8_t rx_cmd[8] = {0};
  if (HAL_UART_Receive(&huart1, rx_cmd, 7, 500) == HAL_OK) {  // 超时500ms
    if (memcmp(rx_cmd, "UPGRADE", 7) == 0) {
      return 1;  // 进入升级模式
    }
  }

  return 0;  // 直接跳转应用
}




3. Flash操作函数(擦除与写入)
STM32 Flash操作需先解锁,按页擦除,按字写入(4字节对齐):

#include "stm32f1xx_hal_flash_ex.h"

// Flash解锁
void Flash_Unlock(void) {
  HAL_FLASH_Unlock();
  __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR);
}

// Flash锁定
void Flash_Lock(void) {
  HAL_FLASH_Lock();
}

// 擦除应用程序区域(从0x08004000开始,共48页)
uint8_t Flash_Erase_AppArea(void) {
  FLASH_EraseInitTypeDef erase_init;
  uint32_t page_error = 0;

  erase_init.TypeErase = FLASH_TYPEERASE_PAGES;
  erase_init.PageAddress = 0x08004000;  // 应用起始地址
  erase_init.NbPages = 48;  // 48页=48KB(F103C8T6共64KB,Bootloader占16KB)

  if (HAL_FLASHEx_Erase(&erase_init, &page_error) != HAL_OK) {
    return 0;  // 擦除失败
  }
  return 1;  // 擦除成功
}

// 写入数据到Flash(addr:起始地址,data:数据,len:4字节单位的长度)
uint8_t Flash_Write(uint32_t addr, uint32_t *data, uint32_t len) {
  for (uint32_t i = 0; i < len; i++) {
    if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + i*4, data) != HAL_OK) {
      return 0;  // 写入失败
    }
  }
  return 1;  // 写入成功
}







4. 接收固件并写入Flash(以串口为例)
需定义简单通信协议(示例:先发送4字节固件大小,再发送固件数据,每包1024字节):

#define APP_ADDR 0x08004000  // 应用程序起始地址
uint8_t rx_buf[1024];       // 接收缓冲区(1024字节=256个32位字)

uint8_t Receive_and_Write_Firmware(void) {
  uint32_t firmware_size = 0;  // 固件总大小(字节)
  uint32_t received = 0;       // 已接收字节数

  // 1. 接收固件大小(4字节,小端模式)
  if (HAL_UART_Receive(&huart1, (uint8_t*)&firmware_size, 4, 10000) != HAL_OK) {
    HAL_UART_Transmit(&huart1, (uint8_t*)"SizeErr", 7, 100);
    return 0;
  }

  // 2. 擦除应用区域(先解锁)
  Flash_Unlock();
  if (!Flash_Erase_AppArea()) {
    HAL_UART_Transmit(&huart1, (uint8_t*)"EraseErr", 8, 100);
    Flash_Lock();
    return 0;
  }

  // 3. 循环接收固件数据并写入
  while (received < firmware_size) {
    // 计算当前包长度(不超过缓冲区和剩余大小)
    uint16_t pkg_len = (firmware_size - received) > 1024 ? 1024 : (firmware_size - received);

    // 接收一包数据(超时10秒)
    if (HAL_UART_Receive(&huart1, rx_buf, pkg_len, 10000) != HAL_OK) {
      HAL_UART_Transmit(&huart1, (uint8_t*)"RxErr", 5, 100);
      Flash_Lock();
      return 0;
    }

    // 转换为32位数据(4字节对齐,不足补0)
    uint32_t write_data[256] = {0};  // 1024字节=256个32位
    memcpy(write_data, rx_buf, pkg_len);

    // 写入Flash(地址=应用起始+已接收字节数,长度=pkg_len/4向上取整)
    uint32_t write_len = pkg_len / 4 + (pkg_len % 4 ? 1 : 0);
    if (!Flash_Write(APP_ADDR + received, write_data, write_len)) {
      HAL_UART_Transmit(&huart1, (uint8_t*)"WriteErr", 8, 100);
      Flash_Lock();
      return 0;
    }

    received += pkg_len;
    HAL_UART_Transmit(&huart1, (uint8_t*)"OK", 2, 100);  // 发送确认
  }

  Flash_Lock();  // 写完锁定Flash
  return 1;
}





5. 跳转至应用程序(核心!必须保证跳转正确)
应用程序的首地址是栈顶,次地址是复位函数入口,需按如下步骤跳转:

typedef void (*pFunction)(void);  // 函数指针类型

void Jump_To_Application(void) {
  pFunction app_reset_handler;    // 应用程序复位函数
  uint32_t app_stack_top;         // 应用程序栈顶地址

  // 1. 检查应用程序是否存在(栈顶地址是否在RAM范围内)
  app_stack_top = *(volatile uint32_t*)APP_ADDR;  // 应用首地址是栈顶
  if (app_stack_top < 0x20000000 || app_stack_top > 0x20005000) {  // F103C8T6 RAM:0x20000000~0x20005000(20KB)
    HAL_UART_Transmit(&huart1, (uint8_t*)"NoApp", 5, 100);
    return;  // 无有效应用,不跳转
  }

  // 2. 关闭中断,避免Bootloader中断干扰应用
  __disable_irq();

  // 3. 复位外设(可选,根据应用需求,如关闭Bootloader开启的时钟)
  HAL_RCC_DeInit();  // 复位RCC,恢复默认时钟(应用需重新初始化时钟)

  // 4. 设置中断向量表偏移(应用的中断向量表在自己的起始地址)
  SCB->VTOR = APP_ADDR;  // 向量表偏移到应用起始地址

  // 5. 设置栈顶指针,跳转至应用复位函数
  __set_MSP(app_stack_top);  // 更新主栈指针为应用栈顶
  app_reset_handler = (pFunction)*(volatile uint32_t*)(APP_ADDR + 4);  // 应用次地址是复位函数
  app_reset_handler();  // 跳转执行应用程序
}




6. 主函数逻辑整合
int main(void) {
  Bootloader_Init();  // 初始化最小系统
  HAL_UART_Transmit(&huart1, (uint8_t*)"Bootloader Ready\r\n", 19, 100);

  // 检测升级触发条件
  if (Check_Upgrade_Trigger()) {
    HAL_UART_Transmit(&huart1, (uint8_t*)"Enter Upgrade Mode\r\n", 19, 100);

    // 接收并写入固件
    if (Receive_and_Write_Firmware()) {
      HAL_UART_Transmit(&huart1, (uint8_t*)"Upgrade Success\r\n", 17, 100);
    } else {
      HAL_UART_Transmit(&huart1, (uint8_t*)"Upgrade Failed\r\n", 16, 100);
    }
  }

  // 跳转至应用程序
  HAL_UART_Transmit(&huart1, (uint8_t*)"Jump to App...\r\n", 16, 100);
  Jump_To_Application();

  // 若跳转失败,进入死循环
  while (1) {
    HAL_Delay(1000);
    HAL_UART_Transmit(&huart1, (uint8_t*)"Jump Failed\r\n", 13, 100);
  }
}




四、应用程序适配(必须修改!否则应用无法运行)
应用程序需修改起始地址和中断向量表偏移,才能被Bootloader正确跳转:

1. 修改应用程序的链接地址(MDK)
打开应用工程→Options for Target→Linker→取消勾选“Use Memory Layout from Target Dialog”→启用“Scatter File”,编写分散加载文件(如app.sct):
LR_IROM1 0x08004000 0x0000C000 {  ; 起始地址0x08004000,大小48KB(0xC000字节)
  ER_IROM1 0x08004000 0x0000C000 { *.o }  ; 代码段
  RW_IRAM1 0x20000000 0x00005000 { *.o }  ; 数据段(RAM)
}


2. 设置应用程序的中断向量表偏移
在应用程序main函数开头添加:

int main(void) {
  // 设置中断向量表偏移到应用程序起始地址(0x08004000)
  SCB->VTOR = 0x08004000;

  // 后续初始化(HAL_Init、时钟等)
  HAL_Init();
  SystemClock_Config();
  // ... 其他代码
}



3. 生成应用程序固件(.bin文件)
MDK中通过fromelf工具生成:在工程Options for Target→User→After Build/Rebuild添加命令:
fromelf --bin -o ./Output/app.bin ./Output/app.axf
(需替换app.axf为实际输出文件名)。
五、测试与验证
1. 烧录Bootloader
通过ST-Link将Bootloader程序烧录到STM32的0x08000000地址(默认启动地址)。
2. 测试升级流程
上电后,Bootloader启动,若触发升级(如按下按键),通过串口工具发送固件:
① 先发送4字节固件大小(小端模式,如0x12345678表示固件大小为0x78563412字节);
② 再发送应用程序的.bin文件数据;
③ 接收“Upgrade Success”后,重启开发板,Bootloader会自动跳转至新应用。
3. 验证跳转功能
未触发升级时,Bootloader应直接跳转至应用程序,应用能正常运行(如LED闪烁、串口打印)。
六、关键注意事项
Flash对齐:写入地址和数据必须4字节对齐(STM32 Flash硬件要求)。
中断关闭:跳转前必须用__disable_irq()关闭中断,避免Bootloader的中断服务程序干扰应用。
栈顶检查:应用程序的栈顶地址必须在RAM范围内(否则视为无效应用)。
固件校验:实际产品中需添加CRC32校验(发送固件时附加校验值,Bootloader接收后验证),防止固件损坏。
兼容性:不同STM32系列(如F1/F4/H7)的Flash操作函数略有差异(如擦除类型、页大小),需根据手册调整。
————————————————
版权声明:本文为CSDN博主「Baizhou12138」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Baizhou12138/article/details/153644863

您需要登录后才可以回帖 登录 | 注册

本版积分规则

160

主题

4412

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部