单片机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文件并发送。

升级

升级完成并运行程序了。
1556

被折叠的 条评论
为什么被折叠?



