✅作者简介:热爱科研的嵌入式开发者,修心和技术同步精进

❤欢迎关注我的知乎:对error视而不见

代码获取、问题探讨及文章转载可私信。

☁ 愿你的生命中有够多的云翳,来造就一个美丽的黄昏。

🍎获取更多嵌入式资料可点击链接进群领取,谢谢支持!👇

点击领取更多详细资料

一、引言

在嵌入式系统开发中,经常需要对设备的固件进行更新。传统的更新方式可能需要将设备连接到特定的编程器或调试器,操作繁琐且不够灵活。BootLoader (引导加载程序)则为解决这一问题提供了很好的方案,它允许我们通过各种通信接口(如串口、USB、网络等)在系统运行时对固件进行更新,大大提高了系统的可维护性和灵活性。本文将详细介绍 STM32 的 BootLoader 相关知识,包括原理、实现步骤和代码示例。

二、BootLoader 原理

2.1 基本概念

BootLoader 是在系统上电后运行的第一段代码,它的主要任务是初始化硬件环境,然后将应用程序加载到内存中并启动执行。在具备固件更新功能的系统中,BootLoader 还需要提供与外部通信的接口,接收新的固件数据并将其存储到指定的存储区域。

2.2 工作流程

  1. 系统上电:当 STM32 系统上电后,CPU 会从固定的地址(如 0x08000000)开始执行代码,即 BootLoader 的起始地址。
  2. 硬件初始化:BootLoader 首先对系统的硬件进行初始化,包括时钟、GPIO、串口等外设。
  3. 判断是否需要更新固件:可以通过检测特定的引脚状态、按键输入或接收到的特定指令来判断是否需要进行固件更新。
  4. 更新固件:如果需要更新固件,BootLoader 通过通信接口(如串口)接收新的固件数据,并将其存储到指定的存储区域(如内部 Flash)。
  5. 启动应用程序:如果不需要更新固件或固件更新完成后,BootLoader 将应用程序的起始地址加载到 CPU 的程序计数器(PC)中,从而启动应用程序的执行。

三、实现步骤

3.1 划分存储区域

在 STM32 的内部 Flash 中,需要划分出 BootLoader 区域和应用程序区域。例如,可以将前 16KB 的 Flash 空间分配给 BootLoader,剩余的空间用于存储应用程序。

3.2 编写 BootLoader 代码

硬件初始化

首先进行硬件初始化,包括时钟、串口等外设的初始化。以下是使用 STM32 HAL 库进行时钟和串口初始化的示例代码:

#include "stm32f4xx_hal.h"

UART_HandleTypeDef huart1;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();

  // 后续代码...

  while (1)
  {
    // 主循环
  }
}

void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** 初始化 RCC 振荡器 
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /** 初始化 RCC 时钟 
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
  {
    Error_Handler();
  }
}

static void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
}

static void MX_GPIO_Init(void)
{
  __HAL_RCC_GPIOA_CLK_ENABLE();
}

void Error_Handler(void)
{
  while(1)
  {
  }
}
判断是否更新固件

可以通过检测特定引脚的状态来判断是否需要更新固件。例如,当某个引脚为低电平时,进入固件更新模式:

#define UPDATE_PIN GPIO_PIN_0
#define UPDATE_PORT GPIOA

if (HAL_GPIO_ReadPin(UPDATE_PORT, UPDATE_PIN) == GPIO_PIN_RESET)
{
  // 进入固件更新模式
  UpdateFirmware();
}
else
{
  // 启动应用程序
  StartApplication();
}
固件更新

在固件更新模式下,通过串口接收新的固件数据,并将其存储到应用程序区域。以下是一个简单的固件更新函数示例:

#define APPLICATION_ADDRESS 0x08004000

void UpdateFirmware(void)
{
  uint8_t buffer[256];
  uint32_t address = APPLICATION_ADDRESS;
  uint32_t receivedBytes = 0;

  // 擦除应用程序区域
  HAL_FLASH_Unlock();
  FLASH_Erase_Sector(FLASH_SECTOR_1, VOLTAGE_RANGE_3);
  HAL_FLASH_Lock();

  while (1)
  {
    if (HAL_UART_Receive(&huart1, buffer, sizeof(buffer), HAL_MAX_DELAY) == HAL_OK)
    {
      receivedBytes = sizeof(buffer);
      HAL_FLASH_Unlock();
      for (uint32_t i = 0; i < receivedBytes; i += 4)
      {
        uint32_t data = *(uint32_t *)&buffer[i];
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, data);
        address += 4;
      }
      HAL_FLASH_Lock();
    }
    else
    {
      break;
    }
  }
}
启动应用程序

将应用程序的起始地址加载到 CPU 的程序计数器(PC)中,从而启动应用程序的执行:

typedef void (*pFunction)(void);
pFunction Jump_To_Application;
uint32_t JumpAddress;

void StartApplication(void)
{
  if (((*(__IO uint32_t*)APPLICATION_ADDRESS) & 0x2FFE0000 ) == 0x20000000)
  {
    JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4);
    Jump_To_Application = (pFunction) JumpAddress;
    __set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS);
    Jump_To_Application();
  }
}

3.3 编写应用程序

在编写应用程序时,需要注意将应用程序的起始地址设置为 BootLoader 划分的应用程序区域的起始地址。可以在 Keil 等开发工具中进行相关设置。

四、注意事项

  1. Flash 擦除和编程:在进行 Flash 擦除和编程操作时,需要注意保护数据的完整性,避免出现数据丢失或损坏的情况。
  2. 中断向量表:应用程序的中断向量表需要进行相应的偏移,以确保中断能够正确响应。可以通过修改向量表偏移寄存器(VTOR)来实现。
  3. 通信协议:在固件更新过程中,需要定义好通信协议,确保数据的准确传输。例如,可以使用简单的帧头、数据长度、数据内容和校验码等组成的协议。

五、总结

STM32 的 BootLoader 为系统的固件更新提供了一种灵活、方便的解决方案。通过合理划分存储区域、编写 BootLoader 代码和应用程序代码,可以实现系统的在线固件更新功能。在实际应用中,需要注意 Flash 操作、中断向量表和通信协议等问题,以确保系统的稳定性和可靠性。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐