关于Stm32的IAP详细和应用

IAP简介(在程序中编程)

通过任何一种通信接口(如IO端口,USB,CAN,UART,I2C,SPI等)下载程序或者应用数据到存储器中。也就是说,STM32允许用户在应用程序中重新烧写闪存存储器中的内容。然而,IAP需要至少有一部分程序已经使用ICP方式烧到闪存存储器中(Bootloader)。 在不需要操作硬件平台的情况下实现升级(远程)。

STM32编程方式
1. 在线编程(ICP,In-Circuit Programming):
通过JTAG/SWD协议或者系统加载程序(Bootloader)下载用户应用程序到微控制器中。
2.IAP

STM32F10x系列芯片系统存储器区域:
关于Stm32的IAP详细和应用
每种STM32芯片(M0,M3,M4),它们的主存储器结构可能不一样,但是他们都有一个叫“系统存储器”的区域,此区域是留给ST自己用来存放芯片的bootloader程序,此程序在芯片出厂的时候已经固化在芯片内部。
系统存储器的Bootloader程序会通过串口1接受应用程序。

STM32启动模式选择
关于Stm32的IAP详细和应用

ICP下载流程
关于Stm32的IAP详细和应用
如果没有使用下载软件的情况下利用串口1进行下载的话就要B0接1,B1接0,系统存储器被选为启动区域。启动代码从串口1接受程序,从地址0x08000000开始写入。
JTAG/SWD下载,直接下载到FLASH指定区域。

一般的程序执行流程
关于Stm32的IAP详细和应用
STM32的内部闪存(FLASH)地址起始于0x08000000,一般情况下,程序文件就从此地址开始写入。
0x08000004开始存放中断向量表。
当中断来临,STM32的内部硬件机制亦会自动将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。
1.STM32复位后,从0X08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序。
2.在复位中断服务程序执行完之后,会跳转到我们的main函数。
main函数执行过程中,如果收到中断请求(发生重中断),此时STM32强制将PC指针指3.回中断向量表处。
4.根据中断源进入相应的中断服务程序。
5.在执行完中断服务程序以后,程序再次返回main函数执行

加入IAP之后程序运行流程
关于Stm32的IAP详细和应用
关于Stm32的IAP详细和应用
复位之后怎么跳转到main?
启动文件有定义:startup_stm32f10x_hd.s

; Reset handler
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit
                BLX     R0               
                LDR     R0, =__main
                BX      R0
                ENDP

关于Stm32的IAP详细和应用
IAP升级应用程序过程
关于Stm32的IAP详细和应用
IAP程序必须满足两个要求
1.新程序(APP)必须在IAP程序(bootloader)之后的某个偏移量为x的地址开始;
2.必须将新程序(APP)的中断向量表相应的移动,移动的偏移量为x;

APP程序的生成步骤:
1.设置APP程序的起始地址和存储空间大小
关于Stm32的IAP详细和应用
0x08000000是flash的起始地址,加上0x10000后变成0x08010000表示你app偏移后的地址,0x70000是空间大小;大容量的flash 64k SIZE为0x80000; 偏移了0x10000剩余0x70000。
注意:要想知道自己的IAP函数偏移了多少可以看编译器的编译信息:
关于Stm32的IAP详细和应用
这一条编译信息中占有flash空间为:Code+RO-data的大小单位字节,如果没有记错的话
SRAM占用大小:RW-dat+ZI-data(这个不保证正确)。这样就可以算出APP要偏移的空间大小了;

  1. 设置中断向量表偏移量
    设置SCB->VTOR的值即可。如:SCB->VTOR=FLASH_BASE|0x010000在FLASH基地址偏移0x10000.

  2. 设置MDK编译后运行fromelf.exe,生成.bin文件.
    通过在MDK User选项卡,设置编译后调用fromelf.exe,根据.axf文件生
    成.bin文件,用于IAP更新。
    关于Stm32的IAP详细和应用
    上图中的2照着下面这行填:D:\Program Files\MDK5\ARM\ARMCC\bin\fromelf.exe --bin -o …\OBJ\RTC.bin …\OBJ\RTC.axf 其中D:\Program Files\MDK5\ARM\ARMCC\bin\fromelf.exe 是你安装MDK的地址,就是MDK文件夹的ARM 文件夹关于Stm32的IAP详细和应用
    找里面的ARMCC
    关于Stm32的IAP详细和应用
    再打开
    关于Stm32的IAP详细和应用
    关于Stm32的IAP详细和应用
    的地址。
    –bin -o …\OBJ\RTC.bin :生成bin文件命令,用OBJ文件夹的输出文件生成RTC.bin文件。其中RTC.bin必须和你的hex文件同名,比如:你的hex文件为LED.hex你生成的bin文件名就必须叫LED.bin

IAP驱动程序:

程序是原子的,我改了部分,让程序可以IAP与APP互相跳转
编写flash的函数

//读取指定地址的半字(16位数据)
//faddr:读地址(此地址必须为2的倍数!!)
//返回值:对应数据.
u16 STMFLASH_ReadHalfWord(u32 faddr)
{
	return *(vu16*)faddr; 
}
#if STM32_FLASH_WREN	//如果使能了写   
//不检查的写入
//WriteAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数   
void STMFLASH_Write_NoCheck(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)   
{ 			 		 
	u16 i;
	for(i=0;i<NumToWrite;i++)
	{
		FLASH_ProgramHalfWord(WriteAddr,pBuffer[i]);
	    WriteAddr+=2;//地址增加2.
	}  
} 
//从指定地址开始写入指定长度的数据
//WriteAddr:起始地址(此地址必须为2的倍数!!)
//pBuffer:数据指针
//NumToWrite:半字(16位)数(就是要写入的16位数据的个数.)
#if STM32_FLASH_SIZE<256
#define STM_SECTOR_SIZE 1024 //字节
#else 
#define STM_SECTOR_SIZE	2048
#endif		 
u16 STMFLASH_BUF[STM_SECTOR_SIZE/2];//最多是2K字节
void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)	
{
	u32 secpos;	   //扇区地址
	u16 secoff;	   //扇区内偏移地址(16位字计算)
	u16 secremain; //扇区内剩余地址(16位字计算)	   
 	u16 i;    
	u32 offaddr;   //去掉0X08000000后的地址
	if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址
	FLASH_Unlock();						//解锁
	offaddr=WriteAddr-STM32_FLASH_BASE;		//实际偏移地址.
	secpos=offaddr/STM_SECTOR_SIZE;			//扇区地址  0~127 for STM32F103RBT6
	secoff=(offaddr%STM_SECTOR_SIZE)/2;		//在扇区内的偏移(2个字节为基本单位.)
	secremain=STM_SECTOR_SIZE/2-secoff;		//扇区剩余空间大小   
	if(NumToWrite<=secremain)secremain=NumToWrite;//不大于该扇区范围
	while(1) 
	{	
		STMFLASH_Read(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容
		for(i=0;i<secremain;i++)//校验数据
		{
			if(STMFLASH_BUF[secoff+i]!=0XFFFF)break;//需要擦除  	  
		}
		if(i<secremain)//需要擦除
		{
			FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区
			for(i=0;i<secremain;i++)//复制
			{
				STMFLASH_BUF[i+secoff]=pBuffer[i];	  
			}
			STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区  
		}else STMFLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);//写已经擦除了的,直接写入扇区剩余区间. 				   
		if(NumToWrite==secremain)break;//写入结束了
		else//写入未结束
		{
			secpos++;				//扇区地址增1
			secoff=0;				//偏移位置为0 	 
		   	pBuffer+=secremain;  	//指针偏移
			WriteAddr+=secremain;	//写地址偏移	   
		   	NumToWrite-=secremain;	//字节(16位)数递减
			if(NumToWrite>(STM_SECTOR_SIZE/2))secremain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完
			else secremain=NumToWrite;//下一个扇区可以写完了
		}	 
	};	
	FLASH_Lock();//上锁
}
#endif

//从指定地址开始读出指定长度的数据
//ReadAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数
void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead)   	
{
	u16 i;
	for(i=0;i<NumToRead;i++)
	{
		pBuffer[i]=STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节.
		ReadAddr+=2;//偏移2个字节.	
	}
}

//////////////////////////////////////////////////////////////////////////////////////////////////////
//WriteAddr:起始地址
//WriteData:要写入的数据
void Test_Write(u32 WriteAddr,u16 WriteData)   	
{
	STMFLASH_Write(WriteAddr,&WriteData,1);//写入一个字 
}

flash编程函数是必须的。

IAP函数:

typedef  void (*iapfun)(void);				//定义一个函数类型的参数.

#define FLASH_APP1_ADDR		0x08010000  	//第一个应用程序起始地址(存放在FLASH)
iapfun jump2app; 
u16 iapbuf[1024];   
//appxaddr:应用程序的起始地址
//appbuf:应用程序CODE.
//appsize:应用程序大小(字节).
void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 appsize)
{
	u16 t;
	u16 i=0;
	u16 temp;
	u32 fwaddr=appxaddr;//当前写入的地址
	u8 *dfu=appbuf;
	for(t=0;t<appsize;t+=2)
	{						    
		temp=(u16)dfu[1]<<8;
		temp+=(u16)dfu[0];	  
		dfu+=2;//偏移2个字节
		iapbuf[i++]=temp;	    
		if(i==1024)
		{
			i=0;
			STMFLASH_Write(fwaddr,iapbuf,1024);	
			fwaddr+=2048;//偏移2048  16=2*8.所以要乘以2.
		}
	}
	if(i)STMFLASH_Write(fwaddr,iapbuf,i);//将最后的一些内容字节写进去.  
}

//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
	if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000)	//检查栈顶地址是否合法.
	{ 
		 //RCC_DeInit(); //关闭外设
     __disable_irq ();         //关闭所有的中断
		 NVIC_DeInit();          //复位中断寄存器
		jump2app=(iapfun)*(vu32*)(appxaddr+4);		//用户代码区第二个字为程序开始地址(复位地址)		
		MSR_MSP(*(vu32*)appxaddr);					//初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
		jump2app();									//跳转到APP.
	}
}		 

APP函数

int main(void)
 {	 
 	u8 t=0;	
	SCB->VTOR = FLASH_BASE | 0x10000;      //设置中断向量的偏移量
	delay_init();	    	 //延时函数初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	 	//串口初始化为115200
 	LED_Init();			     //LED端口初始化
	LCD_Init();			 	
	usmart_dev.init(SystemCoreClock/1000000);	//初始化USMART	
	RTC_Init();	  			//RTC初始化
	POINT_COLOR=RED;//设置字体为红色 
	LCD_ShowString(60,50,200,16,16,"WarShip STM32");	
	LCD_ShowString(60,70,200,16,16,"RTC TEST");	
	LCD_ShowString(60,90,200,16,16,"JUMPIAP123");
	LCD_ShowString(60,110,200,16,16,"2015/1/14");		
	//显示时间
	POINT_COLOR=BLUE;//设置字体为蓝色
	LCD_ShowString(60,130,200,16,16,"    -  -  ");	   
	LCD_ShowString(60,162,200,16,16,"  :  :  ");	
  LED1=!LED1;	 
	while(1)
	{			
		if(t!=calendar.sec)
		{
			t=calendar.sec;
			LCD_ShowNum(60,130,calendar.w_year,4,16);									  
			LCD_ShowNum(100,130,calendar.w_month,2,16);									  
			LCD_ShowNum(124,130,calendar.w_date,2,16);	 
			switch(calendar.week)
			{
				case 0:
					LCD_ShowString(60,148,200,16,16,"Sunday   ");
					break;
				case 1:
					LCD_ShowString(60,148,200,16,16,"Monday   ");
					break;
				case 2:
					LCD_ShowString(60,148,200,16,16,"Tuesday  ");
					break;
				case 3:
					LCD_ShowString(60,148,200,16,16,"Wednesday");
					break;
				case 4:
					LCD_ShowString(60,148,200,16,16,"Thursday ");
					break;
				case 5:
					LCD_ShowString(60,148,200,16,16,"Friday   ");
					break;
				case 6:
					LCD_ShowString(60,148,200,16,16,"Saturday ");
					break;  
			}
			LCD_ShowNum(60,162,calendar.hour,2,16);									  
			LCD_ShowNum(84,162,calendar.min,2,16);									  
			LCD_ShowNum(108,162,calendar.sec,2,16);
			LED0=!LED0;
		}	
		 if(KEY_Scan(0)==KEY0_PRES)	
        APP_jump_IAP(0x08000000);	     //跳转到IAP		 
		delay_ms(10);								  
	};  
 }

APP跳转到IAP的函数:

第一种:下面的函数是APP跳转到IAP的函数,其中NVIC_SystemReset()函数为固件库的函数,直接软复位回到0x08000000地址从头执行任务。
关于Stm32的IAP详细和应用

第二种:

typedef  void (*iapfun)(void);				//定义一个函数类型的参数. 
void NVIC_DeInit(void)
{
  u32 index = 0;
  
  NVIC->ICER[0] = 0xFFFFFFFF;
  NVIC->ICER[1] = 0x000007FF;
  NVIC->ICPR[0] = 0xFFFFFFFF;
  NVIC->ICPR[1] = 0x000007FF;
  
  for(index = 0; index < 0x0B; index++)
  {
     NVIC->IP[index] = 0x00000000;
  } 
}

void APP_jump_IAP(u32 IapAddr)
{
	   u32  Iap_sp;        //跳转到IAP程序的sp初值
	   iapfun jump_IAP;    //IAP函数指针
	   //RCC_DeInit(); //关闭外设
	   NVIC_DeInit();
	   //__disable_irq();      
     INTX_DISABLE ();         //关闭所有的中断
	   SCB->VTOR = FLASH_BASE;
	   Iap_sp=(*(vu32*)IapAddr);      //IAP的栈顶地址
	   jump_IAP=(iapfun)*(vu32*)(IapAddr+4);   //设置IAP函数入口地址
	   MSR_MSP(Iap_sp);       //设置SP.,堆栈栈顶地址
	 
	   jump_IAP();  
	
	
	
	
//	SCB->VTOR = FLASH_BASE;
//	NVIC_SystemReset();     //复位相当按键复位,不过有一段时间延时,为防止这段时间有中断响应加下面一句
//	__set_FAULTMASK(1);      //关闭所有中端
	
}

IAP跳转APP

void iap_load_app(u32 appxaddr)
{
	if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000)	//检查栈顶地址是否合法.
	{ 
		 //RCC_DeInit(); //关闭外设
     __disable_irq ();         //关闭所有的中断
		 NVIC_DeInit();          //复位中断寄存器
		jump2app=(iapfun)*(vu32*)(appxaddr+4);		//用户代码区第二个字为程序开始地址(复位地址)		
		MSR_MSP(*(vu32*)appxaddr);					//初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
		jump2app();									//跳转到APP.
	}
}		 

代码难以理解部分理解:

	if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000)	//检查栈顶地址是否合法.

((vu32)appxaddr)解指针引用,访问appxaddr地址的栈顶值,即取0x8010000开始到0x8010003 的4个字节的值。通过判断栈顶地址值是否正确(是否在0x2000 0000 - 0x 2000 2000之间) 来判断是否应用程序已经下载了,因为应用程序的启动文件刚开始就去初始化化栈空间,如果栈顶值对了,说应用程已经下载了启动文件的初始化也执行了

jump2app=(iapfun)*(vu32*)(appxaddr+4);		//用户代码区第二个字为程序开始地址(复位地址)		

因为开始前4个字节是栈顶地址,所以偏移4个字节,执行中断入口函数RESET中断向量跳转函数,跳转到main函数。
我们看一下启动文件startup_stm32f10x_md_vl.s 中的启动代码,更容易理解
关于Stm32的IAP详细和应用

关于APP与IAP互跳之间的需要注意的问题

1.两者跳转之前一定要关闭所有中断,并复位NVIC中断寄存器的值,因为你跳转函数是用程序指针完成的,NVIC寄存器的值还保持原来main的值,所以一到发生中断就会指向跳转前的main函数的中断函数入口地址,程序就会卡死。这是我个人理解
复位NVIC中断寄存器函数:

void NVIC_DeInit(void)
{
  u32 index = 0;
  
  NVIC->ICER[0] = 0xFFFFFFFF;
  NVIC->ICER[1] = 0x000007FF;
  NVIC->ICPR[0] = 0xFFFFFFFF;
  NVIC->ICPR[1] = 0x000007FF;
  
  for(index = 0; index < 0x0B; index++)
  {
     NVIC->IP[index] = 0x00000000;
  } 
}

也可以用这个:

NVIC_SystemReset()

2.两main函数的跳转SRAM的值会清空。
3.app的地址不能与IAP重合

这里附上借鉴的博客地址:
https://blog.****.net/super_demo/article/details/32133257