stm32f103c8t6使用STM32CubeMX配置IAP

        单片机IAP(In Application Programming,在线应用编程)是一种允许用户程序在运行过程中直接对Flash存储器进行读写操作的功能,主要用于产品发布后的固件升级。‌‌简单来说,就是设备在正常工作状态下,无需借助外部编程工具或拆除硬件,直接通过自身的软件程序完成系统升级。
首先先使用STM32CubeMX配置好基础设置。

配置好后生成程序。

然后更改main.c加入自己的Bootloader内容。

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2025 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include "string.h"
#include "stdio.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
// 内存地址宏定义(根据芯片实际SRAM/FLASH大小调整)
#define RAM_Start       ((uint32_t)0x20000000)  // SRAM起始地址(STM32F1系列通用)
#define RAM_End        ((uint32_t)0x20020000)  // SRAM结束地址(128KB SRAM:0x20000000~0x20020000)
#define RAM_FLASH          ((uint32_t)0x08000000)  // FLASH起始地址(STM32F1主FLASH起始)

// Bootloader分区配置
#define BOOT_SIZE                    0x3000                  // Bootloader大小:12KB(0x3000=12*1024)
#define AppAddress            (RAM_FLASH+ BOOT_SIZE)  // APP起始地址:0x08003000
#define USER_FLASH_PAGES             52                      // APP占用FLASH页数(每页1KB,共52KB)

#define UPDATE_CMD                   "update"                // 升级触发指令(串口发送该字符串则擦除APP区)
#define ADDR_END_FLASH	 ((FLASH_PAGE_SIZE*USER_FLASH_PAGES)+AppAddress)					//App结束地址

// 函数指针类型定义:指向无参数、无返回值的函数(用于跳转APP的复位函数)
typedef  void (*iapfun)(void);

/* 重定向fputc函数:将printf输出重定向到USART1 */
int fputc(int ch, FILE *f)
{
  // 阻塞发送1个字节到串口1,超时时间0xFFFF(无实际超时)
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
  return ch; // 返回发送的字符
}

/* 全局变量定义 */
iapfun jump2app;               // 跳转APP的函数指针
unsigned int count2 = 0;       // 预留计数变量(暂未使用)
unsigned char datatemp[256] = {0}; // 串口接收缓存(每次接收256字节)
unsigned char boot_flag = 0;   // 升级标志:0=不升级,1=触发升级
unsigned char time_out_flag = 0; // 接收超时标志(用于判断APP数据是否接收完成)
/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */

/**
  * @brief  应用程序入口函数
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  unsigned char i; // 10秒倒计时循环变量
  /* USER CODE END 1 */

  /* MCU初始化配置--------------------------------------------------------*/
  /* 1. 复位所有外设,初始化FLASH接口和SysTick定时器 */
  HAL_Init();

  /* USER CODE BEGIN Init */
  /* USER CODE END Init */

  /* 2. 配置系统时钟(此处为72MHz,由SystemClock_Config实现) */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */
  /* USER CODE END SysInit */

  /* 3. 初始化所有已配置的外设(GPIO、USART1) */
  MX_GPIO_Init();
  MX_USART1_UART_Init();

  /* USER CODE BEGIN 2 */
  // Bootloader启动提示
  printf("Bootloader 启动\r\n");
  printf("输入\"update\"擦除用户区FLASH,或等待10秒自动启动用户程序\r\n");
	
  // 10秒倒计时检测升级指令:每秒接收1次串口数据,共10次
  for(i = 0; i<10; i++)
  {
    // 阻塞接收串口数据:缓存datatemp,长度256字节,超时1000ms(1秒)
    HAL_UART_Receive(&huart1, datatemp, 256, 1000); 

    // 判断接收缓存中是否包含升级指令"update",strstr函数查找datatemp中是否有"update"字符串
    if(strstr((const char *)datatemp, UPDATE_CMD) != NULL)
    {
      /* 触发升级:擦除APP区FLASH */
      FLASH_EraseInitTypeDef EraseInitStruct; // FLASH擦除配置结构体
      unsigned int PageError;                 // 擦除错误页地址(用于定位擦除失败位置)

      // 解锁FLASH:FLASH操作前必须解锁,否则硬件拒绝擦写
      HAL_FLASH_Unlock();

      // 配置FLASH擦除参数
      EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES; // 擦除类型:按页擦除
      EraseInitStruct.PageAddress = AppAddress ;  // 起始擦除地址:APP区起始地址
      EraseInitStruct.NbPages = USER_FLASH_PAGES;        // 擦除页数:APP区总页数

      // 执行批量擦除,若失败则打印错误页地址
      if(HAL_FLASHEx_Erase(&EraseInitStruct, &PageError) != HAL_OK)
      {
        HAL_FLASH_Lock(); // 擦除失败,锁定FLASH
        printf("擦除失败,错误页地址:0x%x\n\r",PageError);
        return 0; // 退出程序
      }
      
      HAL_FLASH_Lock(); // 擦除成功,锁定FLASH
      boot_flag = 1;    // 设置升级标志为1
      printf("擦除APP区成功\n\r");				
      break; // 退出10秒循环,进入接收APP数据阶段			
    }
  }

  // 触发升级(收到update指令)
  if(boot_flag == 1)
  {
    HAL_StatusTypeDef	temp;       // HAL库状态变量(接收/写入状态)
    unsigned int Address;         // FLASH编程地址(从APP起始地址开始)
    unsigned int data_32;         // 32位数据缓存(FLASH按字编程,每次写4字节)
    unsigned char j = 0;          // 接收计数(统计已接收/写入的256字节包数)
    
    printf("准备接收APP二进制文件,请在30秒内发送\r\n");
    Address = AppAddress ; // 初始化编程地址为APP起始地址		
    // 阻塞接收APP数据:超时30秒,若超时则退出
    temp = HAL_UART_Receive(&huart1, datatemp, 256, 30*1000); 
    
    // 30秒内未收到任何数据,退出升级流程
    if(temp == HAL_TIMEOUT)
    {
      printf("接收超时,结束等待APP数据\r\n");
      return 0;
    }
    // 成功接收到第一批数据,进入循环接收+写入流程
    else if(temp == HAL_OK)
    {
      while(1)
      {
        unsigned char i;				
        HAL_FLASH_Unlock(); // 解锁FLASH,准备编程

        // 按字(4字节)写入FLASH:256字节=64个字(64*4=256)
        for(i=0; i<64; i++)
        {
          // 从接收缓存中提取4字节数据(i<<2 = i*4,按字对齐)
          data_32 = *(unsigned int *)(&datatemp[i<<2]);
          
          // 确保编程地址未超出APP区范围
          if(Address < ADDR_END_FLASH)
          {
            // 按字编程FLASH:地址=Address,数据=data_32
            if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, Address, data_32)==HAL_OK)
            {
              Address += 4; // 编程成功,地址偏移4字节(指向下一个字)
            }
            else 
            {
              HAL_FLASH_Lock(); // 编程失败,锁定FLASH
              printf("写入失败,错误地址: 0x%x\n\r", Address);
              return 0; // 退出程序
            }						
          }					
        }
        
        HAL_FLASH_Lock();	// 编程完成,锁定FLASH
        // 打印写入状态:当前编程地址、已接收包数
        printf("成功写入256字节,当前地址: 0x%x\t已接收包数: %d\n\r",Address,j++);

        // 继续接收下一批数据:超时2秒(最后一包数据不足256字节时会超时)
        temp = HAL_UART_Receive(&huart1, datatemp, 256, 2*1000); 
        
        // 接收超时:说明无后续数据
        if(temp == HAL_TIMEOUT)
        {
          time_out_flag++; // 超时计数+1
          // 连续2次超时则判定数据接收完成(避免单次超时误判)
          if(time_out_flag == 2)
          {
            printf("APP数据接收完成,写入结束\r\n");
            goto START_APP; // 跳转到启动APP的逻辑
          }
        }
      }
    }
  }	
  // 未触发升级(10秒内未收到update指令)
  else if(boot_flag == 0)  
  {	
START_APP:	// 启动APP的标签(升级完成后也会跳转到此处)		
    printf("准备启动用户APP程序\r\n");	
    HAL_Delay(10); // 短暂延时,确保串口打印完成
    
    /* 校验APP是否存在:判断APP栈顶地址是否合法 */
    // 原理:APP的中断向量表首4字节是栈顶地址,必须在SRAM范围内才视为有效APP
    printf("APP栈顶地址:0x%x\r\n", (*(unsigned int *)AppAddress ));
    // 栈顶地址需在SRAM起始~结束地址之间(0x20000000~0x20020000)
    if(((*(unsigned int *)AppAddress )>= RAM_Start) &&
       ((*(unsigned int *)AppAddress )<= RAM_End))
    {	
      // 可选:关闭全局中断(跳转前关闭,需在APP中重新开启)
      //__disable_irq(); 
      
      // 设置主栈指针(MSP)为APP的栈顶地址(模拟STM32复位流程)
      __set_MSP(*(unsigned int *)AppAddress ); 
      // 提取APP复位函数地址:中断向量表第2项(地址+4)
      jump2app = (iapfun)*(unsigned int *)(AppAddress +4);
      jump2app();	// 跳转到APP的复位函数,执行APP程序	
    }
    else
    {
      printf("未检测到有效用户APP程序\r\n");
      return 0; // 无有效APP,退出程序
    }
  }
  /* USER CODE END 2 */

  /* 无限循环(理论上不会执行到此处:要么跳转APP,要么退出程序) */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/**
  * @brief  系统时钟配置函数
  * @note   配置为:HSE(8MHz) → PLL倍频9倍 → 72MHz系统时钟
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0}; // 振荡器初始化结构体
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; // 时钟初始化结构体

  /** 初始化RCC振荡器参数 */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; // 振荡器类型:外部高速晶振(HSE)
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;                   // 开启HSE
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;    // HSE预分频:1分频(8MHz)
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;                   // 开启内部高速晶振(备用)
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;               // 开启PLL
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;       // PLL时钟源:HSE
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;               // PLL倍频:9倍(8*9=72MHz)
  // 配置振荡器,失败则进入错误处理
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** 初始化CPU、AHB、APB总线时钟 */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 系统时钟源:PLL输出(72MHz)
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;        // AHB总线分频:1分频(72MHz)
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;         // APB1总线分频:2分频(36MHz)
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;         // APB2总线分频:1分频(72MHz)

  // 配置时钟,FLASH延迟:2个周期(72MHz需2周期)
  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */
/* USER CODE END 4 */

/**
  * @brief  错误处理函数(硬件初始化/FLASH操作失败时调用)
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* 用户可自定义错误处理逻辑,如LED闪烁报警 */
  __disable_irq(); // 关闭全局中断
  while (1)        // 死循环,程序卡死
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef USE_FULL_ASSERT
/**
  * @brief  断言失败处理函数(参数校验失败时调用)
  * @param  file: 出错文件名称指针
  * @param  line: 出错行号
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* 用户可自定义打印出错文件和行号,如:
     printf("参数错误:文件 %s 第 %d 行\r\n", file, line); */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

注意写入时需要更改代码写入大小,与代码里宏定义的大小一样。

配置完成后写入stm32f103c8t6单片机。

然后接着编写单片机内运行的程序APP,使用上面的Bootloader程序升级写入,并且跳转运行。

基础配置使用STM32CubeMX,同上一样配置。(APP程序可以是任意程序,我们这里写入一个串口不断发送数据的示例程序)

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2025 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
#define ROM_FLASH           ((uint32_t)0x08000000)					//Flash起始地址
#define ROM_SIZE					 0x3000									//Bootloader程序大小
/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
int fputc(int ch, FILE *f)
{
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
	return ch;
}
/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */
uint32_t SUM = 0;
	
	/* 设置中断向量偏移地址 */
	SCB->VTOR = ROM_FLASH | ROM_SIZE;
  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
		HAL_Delay(10);
		if(SUM%100 == 0)
		{
			printf("this is app!\t%d\n\r",SUM/100);
		}
		SUM++;
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

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

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

关键点是这里需要程序写入改为偏移量,偏移到Bootloader程序之后起始位置为0x8003000字节为0xD000

然后生成为.bin文件使用,命令

//添加这条命令用来生成bin文件
$K\ARM\ARMCC\bin\fromelf.exe --bin --output=Bin\@L.bin !L

生成了一个.bin文件,等下我们引导写入到单片机中。

打开串口工具,sscom5.13.1

配置串口发送文件延时为100ms.因为stm32是接收一包写一包的,比较耗时

启动单片机,写入APP程序,串口提示请在10s内输入升级命令

发送update命令,开始升级

选择.bin文件并发送。

升级

升级完成并运行程序了。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值