单片机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文件并发送。
升级
升级完成并运行程序了。