前言:本文为手把手教学的基于 STM32 的 IAP 固件代码升级项目教程,项目使用 STM32 作为 IAP 升级的工程 MCU,使用 Qt Creator 平台制作 IAP 升级上位机软件。IAP 升级功能是嵌入式工程必须掌握的技能之一,通过在 MCU 的 FLASH 上划分出 Bootloader 和用户代码 APP 区域来实现工程代码的便携式增加新功能或修复 BUG。本项目的 MCU 代码与 IAP 上位机软件的代码均开源,代码框架清晰,可以满足简单的项目应用需求。希望这篇博文能给读者朋友的工程项目给予些许帮助,Respect(代码开源)!

硬件与软件:STM32F103ZET6、iKun IAP Studio

项目结果图:

一、IAP 固件升级讲解

1.1 IAP 固件升级概述

IAP,即 In Application Programming,IAP 是用户自己的程序在运行过程中对 USER FLASH 的部分区域进行烧写。简单来说,就是开发者代码出 BUG 或者增加新功能,能够利用预留的通讯接口,对代码进行升级。

IAP 固件升级可根据设备类型、应用场景和网络环境选择多种通信协议,常见的协议类型及适用场景包括:1、USB:适用于近距离本地升级,如消费电子(手机、打印机)和嵌入式开发调试,传输速度快( USB 3.0 可达 5 Gbps),支持即插即用;UART:低成本、低速率(≤ 115200bps),广泛用于单片机、传感器等简单设备的本地升级,硬件实现简单;3、SPI/I2C:多用于设备内部芯片间的固件升级(如 MCU 与 Flash 之间),速率较高( SPI 可达几十 Mbps),适用于板级内部通信;Ethernet:基于 TCP/IP 的有线网络协议,适用于工业设备、智能家居网关等固定设备的远程升级,传输稳定,支持大文件传输等;

补充说明:MCU 存在的烧录方式可以分为三种,分别为ICP(在电路编程)、IAP(在应用编程)以及ISP(在系统编程)。

1、ICP:ICP,In Circuit Programing,通过专用调试接口(如 SWD、JTAG )直接访问 MCU 内核,绕过用户程序操作闪存。

2、IAP:ISP,In System Programing,利用 MCU 内置的固化 Bootloader(出厂预置),通过标准通信接口(如 UART、USB、SPI )更新程序。

3、ISP:IAP,In applicating Programing,USER 用户程序在运行时主动修改自身闪存,通常需预置分区管理程序。

1.2 STM32 代码启动流程

读者朋友想设计出 IAP(In Application Programming),首先需要对 MCU 的代码启动过程有个非常详尽的了解,作者就以最常见的 STM32 单片机的代码启动流程为例进行简单讲解:

推荐学习博客:https://www.cnblogs.com/gulan-zmc/p/12248509.html

在《Cortex-M3权威指南》有讲述:芯片复位后首先会从中断向量表里面取出两个值(下图来自 Cortex-M3 权威指南):

1、从 0x0000 0000 地址取出 MSP(主堆栈寄存器)的值

2、从 0x0000 0004 地址取出 PC(程序计数器)的值

3、取出第一条指令执行

启动文件源代码分析:

;******************** (C) COPYRIGHT 2011 STMicroelectronics ********************
;* File Name          : startup_stm32f10x_hd.s
;* Author             : MCD Application Team
;* Version            : V3.5.0
;* Date               : 11-March-2011
;* Description        : STM32F10x High Density Devices vector table for MDK-ARM 
;*                      toolchain. 
;*                      This module performs:
;*                      (上电复位后会做下面的几件事情)
;*                      - Set the initial SP(设置堆栈,就是设置MSP的值)
;*                      - Set the initial PC == Reset_Handler(设置PC的值)
;*                      - Set the vector table entries with the exceptions ISR address(设置中断向量表的地址)
;*                      - Configure the clock system and also configure the external (设置系统时钟;如果芯片外部由挂载SRAM,还需要配置SRAM,默认是没有挂外部SRAM的)
;*                        SRAM mounted on STM3210E-EVAL board to be used as data 
;*                        memory (optional, to be enabled by user)
;*                      - Branches to __main in the C library (which eventually      (调用C库的__main函数,然后调用main函数执行用户的)
;*                        calls main()).
;*                      After Reset the CortexM3 processor is in Thread mode,
;*                      priority is Privileged, and the Stack is set to Main.
;* <<< Use Configuration Wizard in Context Menu >>>   
;*******************************************************************************
 
 
 
; ------------------分配栈空间----------------
Stack_Size      EQU     0x00000400      ;EQU指令是定义一个标号;标号名是Stack_Size; 值是0x00000400(有点类似于C语言的#define)。Stack_Size标号用来定义栈的大小
                AREA    STACK, NOINIT, READWRITE, ALIGN=3  ;AREA指令是定义一个段;这里定义一个 段名是STACK,不初始化,数据可读可写,2^3=8字节对齐的段(详细的说明可以查看指导手册)
Stack_Mem       SPACE   Stack_Size   ;SPACE汇编指令用来分配一块内存;这里开辟内存的大小是Stack_Size;这里是1K,用户也可以自己修改
__initial_sp      ;在内存块后面声明一个标号__initial_sp,这个标号就是栈顶的地址;在向量表里面会使用到
 
                                           
; <h> Heap Configuration
;   <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
; ------------------分配堆空间----------------
;和分配栈空间一样不过大小只是512字节
Heap_Size       EQU     0x00000200
                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
 
__heap_base        ;__heap_base堆的起始地址
Heap_Mem        SPACE   Heap_Size      ;分配一个空间作为堆空间,如果函数里面有调用malloc等这系列的函数,都是从这里分配空间的
__heap_limit       ;__heap_base堆的结束地址
 
                PRESERVE8 ;PRESERVE8 指令作用是将堆栈按8字节对齐
                THUMB;THUMB作用是后面的指令使用Thumb指令集
 
; ------------------设置中断向量表----------------
; Vector Table Mapped to Address 0 at Reset
                AREA    RESET, DATA, READONLY      ;定义一个段,段名是RESET的只读数据段
                ;EXPORT声明一个标号可被外部的文件使用,使标号具有全局属性
                EXPORT  __Vectors          ;声明一个__Vectors标号允许其他文件引用          
                EXPORT  __Vectors_End      ;声明一个__Vectors_End标号允许其他文件引用
                EXPORT  __Vectors_Size     ;声明一个__Vectors_Size标号允许其他文件引用
 
 
                
;DCD 指令是分配一个或者多个以字为单位的内存,并且按四字节对齐,并且要求初始化
 
;__Vectors 标号是 0x0000 0000 地址的入口,也是向量表的起始地址
__Vectors       DCD     __initial_sp               ;* Top of Stack     定义栈顶地址;单片机复位后会从这里取出值给MSP寄存器,
                                                   ;* 也就是从0x0000 0000 地址取出第一个值给MSP寄存器 (MSP = __initial_sp) 
                                                   ;* __initial_sp的值是链接后,由链接器生成
 
                DCD     Reset_Handler              ;* Reset Handler    定义程序入口的值;单片机复位后会从这里取出值给PC寄存器,
                                                   ;* 也就是从0x0000 0004 地址取出第一个值给PC程序计数器(pc = Reset_Handler)
                                                   ;* Reset_Handler是一个函数,在下面定义
                ;后面的定义是中断向量表的入口地址了这里就不多介绍了,想要了解的可以参考《STM32中文手册》和《Cortex-M3权威指南》
                DCD     NMI_Handler                ; NMI Handler      
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler
 
 
                .....由于文件太长这里省略了部分向量表的定义,完整的可以查看工程里的启动文件
 
                DCD     DMA2_Channel1_IRQHandler   ; DMA2 Channel1
                DCD     DMA2_Channel2_IRQHandler   ; DMA2 Channel2
                DCD     DMA2_Channel3_IRQHandler   ; DMA2 Channel3
                DCD     DMA2_Channel4_5_IRQHandler ; DMA2 Channel4 & Channel5
__Vectors_End                                    ;__Vectors_End向量表的结束地址
 
__Vectors_Size  EQU  __Vectors_End - __Vectors   ;定义__Vectors_Size标号,值是向量表的大小
 
                AREA    |.text|, CODE, READONLY  ;定义一个代码段,段名是|.text|,属性是只读
 
;PROC指令是定义一个函数,通常和ENDP成对出现(标记程序的结束)               
; Reset handler
Reset_Handler   PROC                                      ;定义 Reset_Handler函数;复位后赋给PC寄存器的值就是Reset_Handler函数的入口地址值。也是系统上电后第一个执行的程序
 
                EXPORT  Reset_Handler             [WEAK]  ;*[WEAK]指令是将函数定义为弱定义。所谓的弱定义就是如果其他地方有定义这个函数,
                                                          ;*编译时使用另一个地方的函数,否则使用这个函数
                                                
                                                          ;*IMPORT   表示该标号来自外部文件,跟 C 语言中的 EXTERN 关键字类似
                IMPORT  __main                            ;*__main 和 SystemInit 函数都是外部文件的标号
                IMPORT  SystemInit                        ;* SystemInit 是STM32函数库的函数,作用是初始化系统时钟
                LDR     R0, =SystemInit
                BLX     R0              
                LDR     R0, =__main                       ;* __main是C库的函数,主要是初始化堆栈和代码重定位,然后跳到main函数执行用户编写的代码
                BX      R0
                ENDP
                
; Dummy Exception Handlers (infinite loops which can be modified)
;下面定义的都是异常服务函中断服务函数
NMI_Handler     PROC
                EXPORT  NMI_Handler                [WEAK]
                B       .
                ENDP
.....由于文件太长这里省略了部分函数的定义,完整的可以查看工程里的启动文件
SysTick_Handler PROC
                EXPORT  SysTick_Handler            [WEAK]
                B       .
                ENDP
 
Default_Handler PROC
 
                EXPORT  WWDG_IRQHandler            [WEAK]
                EXPORT  PVD_IRQHandler             [WEAK]
                .....由于文件太长这里省略了部分中断服务函数的定义,完整的可以查看工程里的启动文件
                EXPORT  DMA2_Channel2_IRQHandler   [WEAK]
                EXPORT  DMA2_Channel3_IRQHandler   [WEAK]
                EXPORT  DMA2_Channel4_5_IRQHandler [WEAK]
 
WWDG_IRQHandler
PVD_IRQHandler
TAMPER_IRQHandler
.....由于文件太长这里省略了部分标号的定义,完整的可以查看工程里的启动文件
DMA2_Channel1_IRQHandler
DMA2_Channel2_IRQHandler
DMA2_Channel3_IRQHandler
DMA2_Channel4_5_IRQHandler
                B       .
 
                ENDP
 
                ALIGN    ;四字节对齐
 
;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
;下面函数是初始化堆栈的代码
                 IF      :DEF:__MICROLIB     
                 ;如果定义了__MICROLIB宏编译下面这部分代码,__MICROLIB在MDK工具里面定义
                 ;这种方式初始化堆栈是由 __main 初始化的
                 EXPORT  __initial_sp   ;栈顶地址 (EXPORT将标号声明为全局标号,供其他文件引用)
                 EXPORT  __heap_base    ;堆的起始地址
                 EXPORT  __heap_limit   ;堆的结束地址
                
                 ELSE
                 ;由用户初始化堆
                 ;否则编译下面的
                 IMPORT  __use_two_region_memory      ;__use_two_region_memory 由用户实现
                 EXPORT  __user_initial_stackheap
                 
__user_initial_stackheap
                 
                 LDR     R0, =  Heap_Mem              ;堆的起始地址
                 LDR     R1, =(Stack_Mem + Stack_Size);栈顶地址
                 LDR     R2, = (Heap_Mem +  Heap_Size);堆的结束地址
                 LDR     R3, = Stack_Mem              ;栈的结束地址
                 BX      LR
 
                 ALIGN
 
                 ENDIF
 
                 END
 
;******************* (C) COPYRIGHT 2011 STMicroelectronics *****END OF FILE*****

STM32 的启动步骤如下:

1、上电复位后,从 0x0000 0000 地址取出栈顶地址赋给MSP寄存器(主堆栈寄存器),即MSP = __initial_sp。这一步是由硬件自动完成的

2、从0x0000 0004 地址取出复位程序的地址给 PC 寄存器(程序计数器),即 PC = Reset_Handler。这一步也是由硬件自动完成调用 SystemInit 函数初始化系统时钟

3、跳到 C 库的 __main 函数初始化堆栈(初始化时是根据前面的分配的堆空间和栈空间来初始化的)和代码重定位(初始 RW 和 ZI 段),然后跳到 main 函数执行应用程序

1.3 IAP 程序升级思路

MCU 部分的 IAP 程序升级设计大体分为两个部分:Bootloader 和 APP 代码设计,Bootloader 用于检查 APP 区代码是否需要更新,以及跳转到 APP 区执行 APP 程序。

MCU 的 IAP 升级流程图

Flash 分区

以 STM32F103ZET6 为 MCU 的 Flash 分区,各区域功能如下:

1、Boot 区:0x0800 0000 到 0x0800 b7FF 地址的 Flash 块划分给 bootloader,用于升级固件,大小是 46kb

2、USER 参数区:0x0800 B800 到 0x0800 BFFF 的flash块划分为用户参数区(Parameters),用于存储用户的一些参数,大小是 2Kb

3、APP 区:0x0800 C000 到 0x0804 3FFF 的 Flash 块划分为 APP 区 ,(Application)用于存放用户功能应用代码,大小是 224Kb

4、APP缓存区:0x0804 4000 到 0x0807 BFFF 的 Flash 块划分为 APP 缓存区 (update region),用于暂存下发的固件,大小跟应用程序区一样 224kb

5、未定义:0x0807 C000 到 0x0807 FFFF 的 Flash 块划分未定义区,可以根据具体用途定义,大小是 16Kb

概述:STM32 的 IAP 升级就是在 MCU 的 FLASH 前段区域(0x0800 0000)部署 Bootloader 用来接收上位机发送过来的 APP(用户代码),Bootloader 代码中负责接收、校验和写入 FLASH 等功能。APP 部分就是正常嵌入式工程项目代码,Bootloader 中也需要加入程序跳转代码,能够实现从 Bootloader 跳转到 APP 程序。

二、STM32 IAP 升级项目概述 

2.1 STM32 的 Bootloader 和 APP

本项目的 STM32 部分代码分为两部分,分别为:Bootloader(引导加载程序代码)和 APP( 用户代码)两个部分。其中,Bootloader 代码:Bootloader 部分代码功能是利用 UART + DMA 中断来接收 IAP Studio(IAP 升级的上位机软件)烧录的 APP.bin 文件,并利用 MCU 的读写 FLASH 函数在指定的 FLASH 地址上写入 Bin 文件,随后使用汇编或 API 函数进行跳转;APP 代码:APP 部分代码功能是就正常运行项目常态化的功能代码(需要注意的点很多)!

补充说明:本篇博客提供的 IAP 升级项目流程只涉及从 Bootloader 代码区成功下载程序后跳转到 APP 代码进行运行。正常嵌入式项目中一般都是上电检测之后运行 APP 代码,在 APP 代码中判断是否需要进行 IAP 升级,如果需要的话就自动跳转到 Bootloader 代码中运行,之后进行程序升级后再跳转回 APP 代码区。Ps:作者后续会在沁恒 WCH 专利中的 USB2.0 鼠标项目中提供上述功能实现。

2.2 Qt 制作 IAP Studio

本项目的 IAP 升级需要搭配 Qt Creator 制作的 IAP Studio 软件进行使用,IAP Studio 软件选择最为普通简单的 UART 串口进行程序下载烧录,后续作者将增加 USB 协议的程序下载烧录功能。本项目的 IAP 升级需要选择使用 .bin 进行下载,关于本项目中设计的 IAP 程序下载数据包格式如下:

本项目使用 Qt Creator 设计的 UI 布局简洁干净,数据传输过程中拥有实时的下载字节数、校验字节数和下载进度条的数据提供,IAP Studio 的软件布局如下:

补充说明:作者提供的 IAP 升级的数据传输格式是非常简单的,只需要做非常简单的数据处理即可。读者朋友们日常开发过程中,可以在此基础上进行改进优化!

三、STM32CubeMX 配置

1、RCC配置外部高速晶振(精度更高)——HSE;

2、SYS配置:Debug设置成Serial Wire否则可能导致芯片自锁);

3、UART1配置:借助CH340芯片将串口数据发送给iKun IAP Studio上位机;

4、UART1配置:开启串口 DMA 模式,并启用串口中断;

5、时钟树配置

6、工程配置

四、代码实现

4.1 FLASH 读写操作

根据作者上述 IAP 固件升级原理的讲述,IAP 程序升级的本质核心就是通过接收到上位机发送过来的 APP.bin 文件后将其内容写入 MCU 指定的 FLASH 地址下即可。作者使用的 MCU 是 STM32F103ZET6,设计的 BSP_FLASH_Read( uint32_t ReadAddr, uint16_t *pBuffer, uint16_t len )  和 BSP_FLASH_Write( uint32_t WriteAddr, uint16_t * pBuffer, uint16_t len ) 来进行 FLASH 的读写操作

flash.c:

/********************************** (C) COPYRIGHT *******************************
* File Name          : flash.c
* Author             : 混分巨兽龙某某
* Version            : V1.0.0
* Date               : 2025/09/15
* Description        : Perform the flash read and write operation functions of the MCU
********************************************************************************/

#include "flash.h"

#define STM32_FLASH_SIZE        512         
#define STM_SECTOR_SIZE         2048        
uint16_t STMFLASH_BUF[STM_SECTOR_SIZE / 2]; 

/************************************************************
  * @brief   读取2字节数据
 * @param[in]   uint32_t faddr
  * @return  NULL
  * @github  
  * @date    2021-xx-xx
  * @version v1.0
  * @note    NULL
  ***********************************************************/
uint16_t BSP_FLASH_ReadHalfWord(uint32_t raddr)
{
 return *(__IO uint16_t*)raddr; 
}
/************************************************************
  * @brief      读取n(uint16_t)字节数据
 * @param[in]   uint32_t ReadAddr
 * @param[out]  uint16_t *pBuffer
 * @param[in]   uint16_t len
  * @return  NULL
  * @github  
  * @date    2021-xx-xx
  * @version v1.0
  * @note    NULL
  ***********************************************************/
void BSP_FLASH_Read( uint32_t ReadAddr, uint16_t *pBuffer, uint16_t len )    
{
 uint16_t i;
 
 for(i=0;i<len;i++)
 {
  pBuffer[i]=BSP_FLASH_ReadHalfWord(ReadAddr);   //读取2个字节.
  ReadAddr+=2;                   //偏移2个字节. 
 }
}

/************************************************************
  * @brief   写入n(uint16_t)字节数据
 * @param[in]   uint32_t ReadAddr
 * @param[out]   uint16_t *pBuffer
 * @param[in]   uint16_t len
  * @return  NULL
  * @github  
  * @date    2021-xx-xx
  * @version v1.0
  * @note    NULL
  ***********************************************************/
void BSP_FLASH_Write_NoCheck( uint32_t WriteAddr, uint16_t * pBuffer, uint16_t len )   
{        
 uint16_t i; 
 
 for(i=0;i<len;i++)
 {
		HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,WriteAddr,pBuffer[i]);
		WriteAddr+=2;                                    //地址增加2.
 }  
} 
/************************************************************
  * @brief     写入n(uint16_t)字节数据
 * @param[in]  uint32_t WriteAddr
 * @param[in]  uint16_t *pBuffer
 * @param[in]  uint16_t len
  * @return  NULL
  * @github  
  * @date    2021-xx-xx
  * @version v1.0
  * @note    NULL
  ***********************************************************/
void BSP_FLASH_Write( uint32_t WriteAddr, uint16_t * pBuffer, uint16_t len ) 
{
	uint32_t SECTORError = 0;
	uint16_t sector_off;    //扇区内偏移地址(16位字计算)
	uint16_t sector_remain; //扇区内剩余地址(16位字计算)    
	uint16_t i;    
	uint32_t secor_pos;    //扇区地址
	uint32_t offaddr;   //去掉0X08000000后的地址

	if(WriteAddr<FLASH_BASE||(WriteAddr>=(FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址

	HAL_FLASH_Unlock();         //解锁

	offaddr=WriteAddr-FLASH_BASE;    //实际偏移地址.
	secor_pos=offaddr/STM_SECTOR_SIZE;   //扇区地址  0~127 for STM32F103RBT6
	sector_off=(offaddr%STM_SECTOR_SIZE)/2;  //在扇区内的偏移(2个字节为基本单位.)
	sector_remain=STM_SECTOR_SIZE/2-sector_off;  //扇区剩余空间大小   
	if(len<=sector_remain)sector_remain=len;//不大于该扇区范围

	while(1) 
	{ 
		BSP_FLASH_Read(secor_pos*STM_SECTOR_SIZE+FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容

		for(i=0;i<sector_remain;i++)//校验数据
		{
			if(STMFLASH_BUF[sector_remain+i]!=0XFFFF)
			break;//需要擦除     
		}
		if(i<sector_remain)//需要擦除
		{
			//擦除这个扇区
			/* Fill EraseInit structure*/
			FLASH_EraseInitTypeDef EraseInitStruct;      // FLASH?????
			EraseInitStruct.TypeErase     = FLASH_TYPEERASE_PAGES;
			EraseInitStruct.PageAddress   = secor_pos*STM_SECTOR_SIZE+FLASH_BASE;
			EraseInitStruct.NbPages       = 1;
			HAL_FLASHEx_Erase(&EraseInitStruct, &SECTORError);
			for(i=0;i<sector_remain;i++)//复制
			{
				STMFLASH_BUF[i+sector_off]=pBuffer[i];   
			}
			BSP_FLASH_Write_NoCheck(secor_pos*STM_SECTOR_SIZE+FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区  
		}
		else 
			BSP_FLASH_Write_NoCheck(WriteAddr,pBuffer,sector_remain);//写已经擦除了的,直接写入扇区剩余区间.        
		if(len==sector_remain)
			break;//写入结束了
		else//写入未结束
		{
			secor_pos++;    //扇区地址增1
			sector_off=0;    //偏移位置为0   
			pBuffer+=sector_remain;   //指针偏移
			WriteAddr+=sector_remain; //写地址偏移    
			len-=sector_remain; //字节(16位)数递减
	 
			if(len>(STM_SECTOR_SIZE/2))
				sector_remain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完
			else 
				sector_remain=len;//下一个扇区可以写完了
		}  
	}; 
	HAL_FLASH_Lock();//上锁
}

4.2 串口 DMA 数据传输

作者本篇博客使用了 UART + DMA 中断来接收上位机发送的 APP.bin 文件,并在接收过程中使用 BSP_FLASH_Read( )  和 BSP_FLASH_Write( ) 函数进行 FLASH 的读写。在接收数据的时候,按照规定的数据格式进行处理,并与上位机完成设计的应答操作!

uart_dma.c:

/********************************** (C) COPYRIGHT *******************************
* File Name          : uart_dma.c
* Author             : 混分巨兽龙某某
* Version            : V1.0.0
* Date               : 2025/09/15
* Description        : UART + DMA CODE
********************************************************************************/

#include "uart_dma.h"
#include "main.h"
#include "stm32f1xx_it.h"
#include "usart.h"
#include "flash.h"
#include "stdio.h"
#include "iap.h"

extern DMA_HandleTypeDef hdma_usart1_rx;
extern DMA_HandleTypeDef hdma_usart1_tx;
extern UART_HandleTypeDef huart1;

extern uint8_t IAP_FLAG;

uint16_t RXBUFF[1280] = {0x00};

void USART1_IRQHandler(void)
{
	uint32_t temp;
	static uint8_t i = 0;
	
	if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE)!= RESET)//如果接受到了一帧数据
	{ 
		__HAL_UART_CLEAR_IDLEFLAG(&huart1);//清除标志位

		HAL_UART_DMAStop(&huart1); //
		temp  =  __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);// 获取DMA中未传输的数据个数   
		//temp  = hdma_usart1_rx.Instance->NDTR;//读取NDTR寄存器 获取DMA中未传输的数据个数,
		//这句和上面那句等效
		rx_len =  BUFFER_SIZE - temp; //总计数减去未传输的数据个数,得到已经接收的数据个数

		BSP_FLASH_Write(0x08020000 + i*0x400, (uint16_t *)(rx_buffer+2), (rx_len - 8)/2);
		BSP_FLASH_Read(0x08020000 + i*0x400, RXBUFF, rx_len - 8);				
				
		if(rx_buffer[rx_len - 1] == 0xFF && rx_buffer[rx_len - 2] == 0xFF && rx_buffer[rx_len - 3] == 0xFF)
		{
			printf("Verify");
		}

		if(memcmp((uint8_t*)(RXBUFF), rx_buffer+2, rx_len-8) == 0)
		{
			printf("Next");
		}
		
		if(rx_buffer[rx_len - 1] == 0xEE && rx_buffer[rx_len - 2] == 0xEE && rx_buffer[rx_len - 3] == 0xEE)
		{
			printf("OK");
			IAP_FLAG = 1;			
			i = 0;
		}
		
		i++;
	}
	HAL_UART_Receive_DMA(&huart1,rx_buffer,BUFFER_SIZE);//重新打开DMA接收
  HAL_UART_IRQHandler(&huart1);
}


4.3 IAP 功能代码

作者规划的 APP 代码区域的 FLASH 地址为 0x0802 0000,通过一套标准化的 APP 跳转代码进行操作。读者朋友们可以参考借鉴一下作者的 IAP 程序升级的代码,根据自己的代码大小修改一下 APP 代码区域地址即可。当然,进行 IAP 跳转前需要关闭和清理部分中断操作!

iap.c:

/********************************** (C) COPYRIGHT *******************************
* File Name          : iap.c
* Author             : 混分巨兽龙某某
* Version            : V1.0.0
* Date               : 2025/09/15
* Description        : IAP Function
********************************************************************************/

#include "iap.h"
#include "stm32f1xx_hal.h"
#include "usart.h"

// 应用程序起始地址(根据实际Flash布局修改)
// 假设IAP程序占用前64KB,应用程序从0x08020000开始
#define APPLICATION_ADDRESS 0x08020000

// 函数指针类型定义,用于跳转
typedef void (*pFunction)(void);

void IAP_JumpToApplication(void)
{
	pFunction Jump_To_Application;
	uint32_t JumpAddress;
	
	// 检查应用程序首地址是否为有效的栈顶指针
	if (((*(__IO uint32_t*)APPLICATION_ADDRESS) & 0x2FFE0000) == 0x20000000)
	{
		// 关闭所有中断,防止跳转过程中被打断
		__disable_irq();
		
		// 关闭HAL库定时器和其他外设
		HAL_RCC_DeInit();
		HAL_DeInit();		
								
		// 获取应用程序复位向量地址(栈顶指针之后的第一个字)
		JumpAddress = *(__IO uint32_t*)(APPLICATION_ADDRESS + 4);
		
		// 初始化函数指针
		Jump_To_Application = (pFunction)JumpAddress;
		
		// 设置主栈指针MSP
		__set_MSP(*(__IO uint32_t*)APPLICATION_ADDRESS);
		
		// 使能中断
		__enable_irq();
		
		// 跳转到应用程序
		Jump_To_Application();
	}
}

4.4 IAP main 函数

利用 IAP_FLAG 全局变量在 while(1) 循环中进行 IAP 功能的跳转,切记不可以放在中断中进行操作,否则可能导致以下问题,问题1:APP 代码无法正常跳转;问题2:APP 代码中的中断向量表无法链接成功!

main.c:

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

  /* 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_DMA_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  printf("By:混分巨兽龙某某\r\n");
  printf("This is IAP CODE\r\n");
  printf("IAP is running!!!\r\n");
  /* USER CODE END 2 */

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

    /* USER CODE BEGIN 3 */
		if(IAP_FLAG == 1)
		{
			IAP_FLAG = 0;
			HAL_Delay(500);
			IAP_JumpToApplication();
		}
  }
  /* USER CODE END 3 */
}

4.5 APP main 函数

读者朋友们的 APP 代码就使用正常的嵌入式工程代码即可,可能唯一需要注意的就是为了防止 APP 代码的中断向量表地址索引错误,建议使用 SCB->VTOR = 0x08020000 进行强制锁定中断向量表的位置,并利用 __enable_irq() 使能中断。

main.c:

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

  /* USER CODE END 1 */

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

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  SCB->VTOR = 0x08020000; 
  __enable_irq();
  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 */
  HAL_Delay(1000);
  printf("By:混分巨兽龙某某\r\n");
  printf("This is APP CODE\r\n");
  /* USER CODE END 2 */

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

    /* USER CODE BEGIN 3 */
		
	printf("APP is running!!!\r\n");
	HAL_Delay(500);
  }
  /* USER CODE END 3 */
}

五、IAP Studio 设计

5.1 UI 布局设计

IAP Studio 的设计:

1、QSerialPort:IAP 程序升级的核心,使用 Serial Port 进行 APP.bin 文件的数据传输;

2、QProgressBar:IAP 程序升级进度条插件,实时显示程序下载进度;

3、QTextEdit:使用此组件显示 IAP 升级过程中的代码写入与程序校验;

4、QPushButton:程序下载按键、串口功能打开按键

5.2 Serial Port 功能实现

Serial Port 初始化需要选择 PC端现在存在的 COM 端口,之后需要设置 Serial Port 波特率、数据的停止位、数据位和奇偶校验。然后,点击串口操作按钮 PushButton  打开串口,作者这里使用了 Qt 槽函数进行连接;

补充说明:Qt 提供了一系列特别方便的 Serial Port 串口助手库函数,我们可以直接使用 mySerialPort->readAll() 和 mySerialPort->writeAll() 针对串口提供的数据进行操作;

// Serial Port初始化,并打开Serial Port功能
void MainWindow::on_btnSwitch_clicked()
{
    QSerialPort::BaudRate baudRate;
    QSerialPort::DataBits dataBits;
    QSerialPort::StopBits stopBits;
    QSerialPort::Parity   checkBits;

    // 获取串口波特率
    baudRate = (QSerialPort::BaudRate)ui->cmbBaudRate->currentText().toUInt();
    // 获取串口数据位
    dataBits = (QSerialPort::DataBits)ui->cmbData->currentText().toUInt();
    // 获取串口停止位
    if(ui->cmbStop->currentText() == "1"){
        stopBits = QSerialPort::OneStop;
    }else if(ui->cmbStop->currentText() == "1.5"){
        stopBits = QSerialPort::OneAndHalfStop;
    }else if(ui->cmbStop->currentText() == "2"){
        stopBits = QSerialPort::TwoStop;
    }else{
        stopBits = QSerialPort::OneStop;
    }

    // 获取串口奇偶校验位
    if(ui->cmbCheck->currentText() == "无"){
        checkBits = QSerialPort::NoParity;
    }else if(ui->cmbCheck->currentText() == "奇校验"){
        checkBits = QSerialPort::OddParity;
    }else if(ui->cmbCheck->currentText() == "偶校验"){
        checkBits = QSerialPort::EvenParity;
    }else{
        checkBits = QSerialPort::NoParity;
    }

    // 想想用 substr strchr怎么从带有信息的字符串中提前串口号字符串
    // 初始化串口属性,设置 端口号、波特率、数据位、停止位、奇偶校验位数
    mySerialPort->setBaudRate(baudRate);
    mySerialPort->setDataBits(dataBits);
    mySerialPort->setStopBits(stopBits);
    mySerialPort->setParity(checkBits);
    //mySerialPort->setPortName(ui->cmbSerialPort->currentText());// 不匹配带有串口设备信息的文本
    // 匹配带有串口设备信息的文本
    QString spTxt = ui->cmbSerialPort->currentText();
    spTxt = spTxt.section(':', 0, 0);//spTxt.mid(0, spTxt.indexOf(":"));
    //qDebug() << spTxt;
    mySerialPort->setPortName(spTxt);

    // 根据初始化好的串口属性,打开串口
    // 如果打开成功,反转打开按钮显示和功能。打开失败,无变化,并且弹出错误对话框。
    if(ui->btnSwitch->text() == "打开串口"){
        if(mySerialPort->open(QIODevice::ReadWrite) == true){
            //QMessageBox::
            ui->btnSwitch->setText("关闭串口");
            // 让端口号下拉框不可选,避免误操作(选择功能不可用,控件背景为灰色)
            ui->cmbSerialPort->setEnabled(false);
            ui->cmbBaudRate->setEnabled(false);
            ui->cmbStop->setEnabled(false);
            ui->cmbData->setEnabled(false);
            ui->cmbCheck->setEnabled(false);
        }else{
            QMessageBox::critical(this, "错误提示", "串口打开失败!!!\r\n\r\n该串口可能被占用,请选择正确的串口\r\n或者波特率过高,超出硬件支持范围");
        }
    }else{
        mySerialPort->close();
        ui->btnSwitch->setText("打开串口");
        // 端口号下拉框恢复可选,避免误操作
        ui->cmbSerialPort->setEnabled(true);
        ui->cmbBaudRate->setEnabled(true);
        ui->cmbStop->setEnabled(true);
        ui->cmbData->setEnabled(true);
        ui->cmbCheck->setEnabled(true);
        // 再次使能下载按键
        ui->download->setEnabled(true);
    }
}
// 串口接收显示,槽函数
void MainWindow::serialPortRead_Slot()
{
    /* 利用QtSerial库接收数据 */
    QByteArray recBuf = mySerialPort->readAll();
    Plot_Num = recBuf;
    // 判断是否为16进制接收,将以后接收的数据全部转换为16进制显示(先前接收的部分在多选框槽函数中进行转换。最好多选框和接收区组成一个自定义控件,方便以后调用)
    if(ui->chkRec->checkState() == false){
        // GB2312编码输入
        QString strb = QString::fromLocal8Bit(recBuf);//QString::fromUtf8(recBuf);//QString::fromLatin1(recBuf);
        // 在当前位置插入文本,不会发生换行。如果没有移动光标到文件结尾,会导致文件超出当前界面显示范围,界面也不会向下滚动。
        ui->txtRec->insertPlainText(strb);
    }else{
        // 16进制显示,并转换为大写
        QString str1 = recBuf.toHex().toUpper();//.data();
        // 添加空格
        QString str2;
        for(int i = 0; i<str1.length (); i+=2)
        {
            str2 += str1.mid (i,2);
            str2 += " ";
        }
        ui->txtRec->insertPlainText(str2);
    }
 
    /* 1.计算接收到的字节数 */
    RecvNum += recBuf.size();
 
    /* 2.格式化并显示总字节数 */
    ui->RxCount->setText(QString::number(RecvNum));
 
    /* 3.计算并显示接收速度 */
    QString speedText = calculateSpeed(RecvNum);
    ui->RxSpeed->setText(speedText);
 
    /* 4.更新上一次记录(用于下次计算速度)*/
    lastRecvNum = RecvNum;
    lastUpdateTime = QDateTime::currentMSecsSinceEpoch();
 
    /* 5.移动光标到文本结尾 */
    ui->txtRec->moveCursor(QTextCursor::End);
}

5.3 IAP 功能实现

IAP 部分的代码划分 3 部分:

1、选择 IAP 升级的固件代码

这部分代码就是从 PC 端选中需要下载的 APP.bin 文件,我们需要使用 Keil 这个 IDE 制作一个.bin 文件(方法下文会讲解)!

// 选择IAP升级的固件代码
void MainWindow::on_pushButton_clicked()
{
    // 1. 获取文件路径(增加取消操作处理)
    fileLocation = QFileDialog::getOpenFileName(this, "打开文件",
                                               QStandardPaths::writableLocation(QStandardPaths::DesktopLocation),
                                               tr("二进制文件 (*.bin)"));
    if (fileLocation.isEmpty())  return;  // 用户取消选择
    ui->lineEdit->setText(fileLocation);

    // 2. 安全打开文件
    QFile file(fileLocation);
    if (!file.open(QIODevice::ReadOnly))  {
        QMessageBox::critical(this, "文件打开失败",
                             QString("错误: %1\n路径: %2").arg(file.errorString()).arg(fileLocation));
        return;
    }

    QByteArray IAPDATA = file.readAll();
    binSize = IAPDATA.size();

    // 3. 资源清理
    file.close();
}

2、IAP 固件数据回流

这部分代码就是简单的点击程序下载按钮之后,进行 APP.bin 文件的数据下发,且上位机与下位机需要满足存在的应答机制!

// IAP固件数据回流
void MainWindow::readData ()
{
    usart_temp += mySerialPort->readAll ();         // 存储收到数据
    if(usart_temp == "Verify")
    {
        ui->textEdit->insertPlainText (tr("成功写入1K,总共写入%1K\n").arg (binLoadCnt));
        usart_temp.clear ();
    }
    if(usart_temp == "VerifyNext")
    {
        ui->textEdit->insertPlainText (tr("成功写入1K,总共写入%1K\n").arg (binLoadCnt));
        ui->textEdit->insertPlainText (tr("成功校验1K,总共成功校验%1K\n").arg (binLoadCnt));
        emit transmitData(binLoadCnt++);            // 发送写入完成信号
        usart_temp.clear ();
    }
    if(usart_temp == "NextOK")
    {
        ui->textEdit->insertPlainText (tr("成功写入1K,总共写入%1K\n").arg (binLoadCnt));
        ui->textEdit->insertPlainText (tr("成功校验1K,总共成功校验%1K\n").arg (binLoadCnt));
        ui->textEdit->insertPlainText (tr("IAP结束\n\n"));
        usart_temp.clear ();
    }
    ui->textEdit->moveCursor(QTextCursor::End);
}

// 启动IAP程序下载
void MainWindow::on_download_clicked()
{
    ui->textEdit->insertPlainText (tr("正在下载\n"));
    binLoadCnt = 1;                         // 初始化 文件偏移值 binLoadCnt*1024
    usart_temp.clear();                     // 缓冲区清零
    emit transmitData (0);                  // 发送首K内容
}

3、IAP 过程中的数据格式处理

这段代码就是按照作者设计的 IAP 数据传输格式进行编写的代码,作者的处理是非常简单化处理的,读者朋友们可以根据自己实际需求去修改一下这边的数据格式!

// IAP过程中的数据格式处理
void MainWindow::transmitDataFun (int cnt)             //发送文件
{
    qint32 temp = 0;            //剩余待传数据

    ui->download->setEnabled (false);             //将下载按钮设置不可选择
    QFile *binFile = new QFile(fileLocation);
    binFile->open (QIODevice::ReadOnly);
    binFile->seek (cnt * 1024);
    char * binByte = new char[1024];         //bin缓存
    memset (binByte, 0xff, 1024);
    QByteArray binTransmit;
    binTransmit.resize (8);
    temp = binSize - 1024*cnt;

    if(temp < 1024)
    {
        binTransmit[0] = temp % 256;
        binTransmit[1] = temp / 256;
        //index CRC 暂空
        binTransmit[2] = cnt % 256;
        binTransmit[3] = cnt / 256;
        binTransmit[4] = 0xee;
        binTransmit[5] = 0xee;
        binTransmit[6] = 0xee;
        binTransmit[7] = 0xee;

        binFile->read (binByte, temp);
        binTransmit.insert(2, binByte, 1024);
        ui->progressBar->setValue (100);
    }
    else
    {

        binTransmit[0] = 0x00;
        binTransmit[1] = 0x04;
        //index CRC 暂空
        binTransmit[2] = cnt % 256;
        binTransmit[3] = cnt / 256;
        binTransmit[4] = 0xff;
        binTransmit[5] = 0xff;
        binTransmit[6] = 0xff;
        binTransmit[7] = 0xff;

        binFile->read (binByte, 1024);
        binTransmit.insert(2, binByte, 1024);
        int i = (1 - float(temp) / float(binSize)) * 100;
        ui->progressBar->setValue (i);
    }
    delete binByte;
    mySerialPort->write(binTransmit);
}

5.4 核心代码

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    setWindowTitle("iKun IAP Studio");        /* 修改APP标题 */

    // 状态栏
    QStatusBar *sBar = statusBar();
    // 实例化两个按钮对象,并设置其显示文本为窗口皮肤1和窗口皮肤2
    AuthorButton = new QPushButton("作者信息", this);
    AuthorButton->setStyleSheet("border: none;color: blue;"); // 隐藏边框字体蓝色
    // 设定两个QPushButton对象的位置
    sBar->addPermanentWidget(AuthorButton);
    // 状态栏添加超链接
    QLabel *lblLinkBlog = new QLabel(this);
    lblLinkBlog->setOpenExternalLinks(true);
    //lblLinkBlog->setText("<a href=\"https://blog.csdn.net/Mark_md/article/details/108928314\">博客");// 有下划线
    lblLinkBlog->setText("<style> a {text-decoration: none} </style> <a href=\"https://blog.csdn.net/black_sneak?spm=1000.2115.3001.5343\">博客");// 无下划线
    QLabel *lblLinkSource = new QLabel(this);
    lblLinkSource->setOpenExternalLinks(true);
    //lblLinkSource->setText("<a href=\"https://github.com/ZhiliangMa/Qt-SerialDebuger\">源码下载");
    lblLinkSource->setText("<style> a {text-decoration: none} </style> <a href=\"https://blog.csdn.net/black_sneak/article/details/151840514\">源码下载");// 无下划线
    lblLinkBlog->setMinimumSize(40, 20);
    lblLinkSource->setMinimumSize(60, 20);
    // 从左往右依次添加
    sBar->addWidget(lblLinkBlog);
    sBar->addWidget(lblLinkSource);
    // 初始化IAP进度条数值
    ui->progressBar->setValue(0);

    // 新建一串口对象
    mySerialPort = new QSerialPort(this);

    // IAP数据回读的槽函数连接
    connect (mySerialPort, SIGNAL(readyRead()), this, SLOT(readData()));

    // 数据传输的槽函数连接
    connect (this, SIGNAL(transmitData(int)), this, SLOT(transmitDataFun(int)));

    // 信号槽连接,打开作者信息的按钮
    connect(AuthorButton, SIGNAL(clicked()), this, SLOT(AuthorButton_Clicked()));
}

MainWindow::~MainWindow()
{
    delete ui;
}

// 作者信息的信号槽函数
void MainWindow::AuthorButton_Clicked()
{
    static uint8_t num = 0;
    QMessageBox::information(NULL, "作者信息","作者:混分巨兽龙某某\n联系方式:1178305328\n版本信息:V1.0");
    num++;

    if(num == 3){
        num = 0;
       QMessageBox::information(NULL, "Miss You!","枯木是否可以逢春?");
    }
}

// Serial Port初始化,并打开Serial Port功能
void MainWindow::on_btnSwitch_clicked()
{
    QSerialPort::BaudRate baudRate;
    QSerialPort::DataBits dataBits;
    QSerialPort::StopBits stopBits;
    QSerialPort::Parity   checkBits;

    // 获取串口波特率
    baudRate = (QSerialPort::BaudRate)ui->cmbBaudRate->currentText().toUInt();
    // 获取串口数据位
    dataBits = (QSerialPort::DataBits)ui->cmbData->currentText().toUInt();
    // 获取串口停止位
    if(ui->cmbStop->currentText() == "1"){
        stopBits = QSerialPort::OneStop;
    }else if(ui->cmbStop->currentText() == "1.5"){
        stopBits = QSerialPort::OneAndHalfStop;
    }else if(ui->cmbStop->currentText() == "2"){
        stopBits = QSerialPort::TwoStop;
    }else{
        stopBits = QSerialPort::OneStop;
    }

    // 获取串口奇偶校验位
    if(ui->cmbCheck->currentText() == "无"){
        checkBits = QSerialPort::NoParity;
    }else if(ui->cmbCheck->currentText() == "奇校验"){
        checkBits = QSerialPort::OddParity;
    }else if(ui->cmbCheck->currentText() == "偶校验"){
        checkBits = QSerialPort::EvenParity;
    }else{
        checkBits = QSerialPort::NoParity;
    }

    // 想想用 substr strchr怎么从带有信息的字符串中提前串口号字符串
    // 初始化串口属性,设置 端口号、波特率、数据位、停止位、奇偶校验位数
    mySerialPort->setBaudRate(baudRate);
    mySerialPort->setDataBits(dataBits);
    mySerialPort->setStopBits(stopBits);
    mySerialPort->setParity(checkBits);
    //mySerialPort->setPortName(ui->cmbSerialPort->currentText());// 不匹配带有串口设备信息的文本
    // 匹配带有串口设备信息的文本
    QString spTxt = ui->cmbSerialPort->currentText();
    spTxt = spTxt.section(':', 0, 0);//spTxt.mid(0, spTxt.indexOf(":"));
    //qDebug() << spTxt;
    mySerialPort->setPortName(spTxt);

    // 根据初始化好的串口属性,打开串口
    // 如果打开成功,反转打开按钮显示和功能。打开失败,无变化,并且弹出错误对话框。
    if(ui->btnSwitch->text() == "打开串口"){
        if(mySerialPort->open(QIODevice::ReadWrite) == true){
            //QMessageBox::
            ui->btnSwitch->setText("关闭串口");
            // 让端口号下拉框不可选,避免误操作(选择功能不可用,控件背景为灰色)
            ui->cmbSerialPort->setEnabled(false);
            ui->cmbBaudRate->setEnabled(false);
            ui->cmbStop->setEnabled(false);
            ui->cmbData->setEnabled(false);
            ui->cmbCheck->setEnabled(false);
        }else{
            QMessageBox::critical(this, "错误提示", "串口打开失败!!!\r\n\r\n该串口可能被占用,请选择正确的串口\r\n或者波特率过高,超出硬件支持范围");
        }
    }else{
        mySerialPort->close();
        ui->btnSwitch->setText("打开串口");
        // 端口号下拉框恢复可选,避免误操作
        ui->cmbSerialPort->setEnabled(true);
        ui->cmbBaudRate->setEnabled(true);
        ui->cmbStop->setEnabled(true);
        ui->cmbData->setEnabled(true);
        ui->cmbCheck->setEnabled(true);
        // 再次使能下载按键
        ui->download->setEnabled(true);
    }
}

// 选择IAP升级的固件代码
void MainWindow::on_pushButton_clicked()
{
    // 1. 获取文件路径(增加取消操作处理)
    fileLocation = QFileDialog::getOpenFileName(this, "打开文件",
                                               QStandardPaths::writableLocation(QStandardPaths::DesktopLocation),
                                               tr("二进制文件 (*.bin)"));
    if (fileLocation.isEmpty())  return;  // 用户取消选择
    ui->lineEdit->setText(fileLocation);

    // 2. 安全打开文件
    QFile file(fileLocation);
    if (!file.open(QIODevice::ReadOnly))  {
        QMessageBox::critical(this, "文件打开失败",
                             QString("错误: %1\n路径: %2").arg(file.errorString()).arg(fileLocation));
        return;
    }

    QByteArray IAPDATA = file.readAll();
    binSize = IAPDATA.size();

    // 3. 资源清理
    file.close();
}

// IAP固件数据回流
void MainWindow::readData ()
{
    usart_temp += mySerialPort->readAll ();         // 存储收到数据
    if(usart_temp == "Verify")
    {
        ui->textEdit->insertPlainText (tr("成功写入1K,总共写入%1K\n").arg (binLoadCnt));
        usart_temp.clear ();
    }
    if(usart_temp == "VerifyNext")
    {
        ui->textEdit->insertPlainText (tr("成功写入1K,总共写入%1K\n").arg (binLoadCnt));
        ui->textEdit->insertPlainText (tr("成功校验1K,总共成功校验%1K\n").arg (binLoadCnt));
        emit transmitData(binLoadCnt++);            // 发送写入完成信号
        usart_temp.clear ();
    }
    if(usart_temp == "NextOK")
    {
        ui->textEdit->insertPlainText (tr("成功写入1K,总共写入%1K\n").arg (binLoadCnt));
        ui->textEdit->insertPlainText (tr("成功校验1K,总共成功校验%1K\n").arg (binLoadCnt));
        ui->textEdit->insertPlainText (tr("IAP结束\n\n"));
        usart_temp.clear ();
    }
    ui->textEdit->moveCursor(QTextCursor::End);
}

// 启动IAP程序下载
void MainWindow::on_download_clicked()
{
    ui->textEdit->insertPlainText (tr("正在下载\n"));
    binLoadCnt = 1;                         // 初始化 文件偏移值 binLoadCnt*1024
    usart_temp.clear();                     // 缓冲区清零
    emit transmitData (0);                  // 发送首K内容
}

// IAP过程中的数据格式处理
void MainWindow::transmitDataFun (int cnt)             //发送文件
{
    qint32 temp = 0;            //剩余待传数据

    ui->download->setEnabled (false);             //将下载按钮设置不可选择
    QFile *binFile = new QFile(fileLocation);
    binFile->open (QIODevice::ReadOnly);
    binFile->seek (cnt * 1024);
    char * binByte = new char[1024];         //bin缓存
    memset (binByte, 0xff, 1024);
    QByteArray binTransmit;
    binTransmit.resize (8);
    temp = binSize - 1024*cnt;

    if(temp < 1024)
    {
        binTransmit[0] = temp % 256;
        binTransmit[1] = temp / 256;
        //index CRC 暂空
        binTransmit[2] = cnt % 256;
        binTransmit[3] = cnt / 256;
        binTransmit[4] = 0xee;
        binTransmit[5] = 0xee;
        binTransmit[6] = 0xee;
        binTransmit[7] = 0xee;

        binFile->read (binByte, temp);
        binTransmit.insert(2, binByte, 1024);
        ui->progressBar->setValue (100);
    }
    else
    {

        binTransmit[0] = 0x00;
        binTransmit[1] = 0x04;
        //index CRC 暂空
        binTransmit[2] = cnt % 256;
        binTransmit[3] = cnt / 256;
        binTransmit[4] = 0xff;
        binTransmit[5] = 0xff;
        binTransmit[6] = 0xff;
        binTransmit[7] = 0xff;

        binFile->read (binByte, 1024);
        binTransmit.insert(2, binByte, 1024);
        int i = (1 - float(temp) / float(binSize)) * 100;
        ui->progressBar->setValue (i);
    }
    delete binByte;
    mySerialPort->write(binTransmit);
}

六、STM32 IAP 升级

6.1 APP 的 Bin 文件制作

keil Bin 文件产生

输入:

D:\MDK5\ARM\ARMCC\bin\fromelf.exe --bin -o IAPStudio_APP.bin IAPStudio_APP\IAPStudio_APP.axf

参数意义:

D:\MDK5\ARM\ARMCC\bin\fromelf.exe: fromelf.exe 文件目录地址,一般 Keil 下自带有

--bin -o IAP.bin :

输出bin文件名称

IAPStudio_APP\IAPStudio_APP.axf:axf文件目录及文件

6.2 IAP 的程序制作

作者设计的 IAP 代码起始地址为默认的:0x0800 0000,Size:0x1F000,APP 代码的起始地址为:0x0802 0000,Size:0x60000;也就是说,设计的 APP 代码是从 0x0802 0000开始的

6.3 IAP 固件升级步骤

1、准备好 APP 和 IAP 两部分的代码;

2、使用 Link 下载 IAP 代码,准备利用 iKun IAP Studio 下载 APP 代码;

3、下载 APP 代码,且 MCU 代码默认跳转到 APP 区域运行

七、作者有话

IAP 固件升级说嵌入式工程师必须掌握的核心技能之一,在嵌入式研发的日常生活中,IAP 升级的这项功能将给研发的产品进行极大的赋能。作者本篇博客只是提供了简单的 IAP 升级程序以及 IAP Studio 上位机升级软件的制作,与实际工程研发中的优秀 IAP 还是有很大区别的。当然,作者后续还会编写一篇进阶版本的 IAP 程序升级博客,提供更加详尽代码例程,包括:签名、验签、加解密、校验固件完整性等。制作的 IAP Studio 上位机也会进行升级(提供更多通信协议选择),届时也一并提供给读者朋友们,希望对各位嵌入式工程师的项目开发有所帮助!

八、代码开源

代码地址: 【免费】基于STM32的IAP固件升级与上位机软件IAPStudio项目代码资源-CSDN下载

如果积分不够的朋友,点波关注评论区留下邮箱,作者无偿提供源码和后续问题解答。求求啦关注一波吧 !!!

Logo

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

更多推荐