浅谈STM32在应用中编程(IAP)的应用(俗称在线更新程序)

demi的头像
demi 发布于:周三, 01/20/2021 - 17:26 ,关键词:

STM32等单片机是可编程处理器,内部运行着我们编写的程序,而把我们编写的程序“下载”到单片机中,方法有两种:

一、使用烧写器,如jlink,stlink,串口下载(需要配置boot0,boot1)。

二、通过IAP实现一个在线更新功能。

对于很多使用单片机作为主要处理器的电子产品,如遇到需要替换芯片内部程序以满足需求的情况,通常的解决办法是寄回该产品然后通过烧写器直接替换程序。但这样做无疑会增加相关的成本。所以很多的电子产品都会实现一个能远程更新(通过网络更新芯片程序,如手机更新操作系统等)或者自主更新程序(通过U盘,SD卡等方式更新,芯片读取并识别这些外部存储器存放的程序,并读取到自身内部空间中)。

首先简单说一下STM32等单片机,程序的存储位置是内部Flash空间,查看STM32F1参考手册等相关资料可以得知程序存放的起始地址为0x08000000(不同的单片机这个可能会有所不同)。

实现IAP功能的基本思路:划分芯片内部的Flash空间,分别存放不同的程序实现不同的功能。实现一个简单的跳转函数,使我们能控制芯片什么时候运行什么程序。通常划分的做法是:Bootloader+APP+APPBackup。

先介绍一下跳转函数,先看代码:

//定义一个函数类型:返回类型是void,函数参数是void
typedef  void (*iapfun)(void);
				
//声明jump2app函数的类型
iapfun jump2app; 
 
 
//
//appxaddr(需要跳转的地址)
//
void iap_load_app(u32 appxaddr)
{
    //判断栈顶地址是否合法
	if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000)	
	{ 
        //指定程序跳转的位置
		jump2app=(iapfun)*(vu32*)(appxaddr+4);		
		MSR_MSP(*(vu32*)appxaddr);
        
        //直接跳转,运行用户指定的程序。					
		jump2app();								
	}else
	{
		printf("error....\r\n");
	}
}		 

介绍一下Flash空间划分及对应存放的程序:

①:0x08000000 - 0x0800FFFF :共64Kb,存放Bootloader程序,用于判断是否需要更新及负责获取更新的数据并进行Flash操作写入数据。

②:0x08010000 - 0x0802FFFF :共128Kb,存放APP程序,即用户程序,会获得实际运行权的程序。

③:0x08030000 - 0x0804FFFF :共128Kb,作为IAP缓存区,Bootloader在更新过程中获取到的数据会先写入到这部分Flash空间,等待所有数据获取完成后,通过Flash搬移操作,把这部分Flash上的数据复制到第②部分的存储空间中。Bootloader不直接写第②部分的空间是为了避免在更新过程中出现的意外情况,如电量耗尽等,损坏了原来在芯片中的可运行程序。即确保芯片至少有一个可运行的程序,防止设备“变砖”。

④:0x08050000 - 0x0806FFFF:共128Kb,作为出厂程序备份区,针对更新出错等意外情况,提供一种解决办法能让设备恢复原来的状态。

⑤:0x08070000 - 0x0807FFFF:共64Kb,作为用户重要数据存储区。

运行流程:系统启动后,运行Bootloader程序,通过读取相关存储标志,如果需要更新,启动数据获取-转换-写Flash-跳转的更新流程。如果不需要更新,则直接跳转至用户程序:iap_load_app(0x08010000);

更新流程:程序数据获取-数据转换-写入Flash,循环直到数据全部写入完成。由于可能会出现需要更新的程序(目标程序)比较大,多达几十k上百k,因此要求Bootloader一次读取完全部程序数据是不现实的。更为可靠的做法是每次读入一定数量的程序数据,如2k等。

由于程序数据获取的方式太多,有串口输入,网络请求,读取外部存储器等方式,这里就不再介绍数据获取部分,无论是哪种方式实现的,其本质都是一样的,只是把需要更新的程序通过某一种方式让处理器能获取到。

顺带提一下,目标程序的文件格式是.bin文件(编译选项加入参数,见下图),且在编译前指定好了相关的Flash位置(可直接在target中设置)和中断向量偏移位置(通过修改SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;)。如按照上述的Flash分区设置,这里VECT_TAB_OFFSET的值应为0x10000。


假设我们的更新程序的前面2k数据已经获取完成了,保存在了u8 appbuf[1024*2+1]的数组中,我们需要把这些数据写入到内存中,但由于ST提供的Flash操作函数要求的是半字写入,即u16类型,所以这里我们获取到的数据还需要处理一下,转换成u16类型。

//不检查的写入
//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);//读出整个扇区的内容
//		printf("\nwrite flash\n");
		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
 
//
//用户接口程序
//实现写入用户获取到的程序数据
//
u32 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;
		iapbuf[i++]=temp;	    
	}
	STMFLASH_Write(fwaddr,iapbuf,i);	
	return fwaddr+appsize; 
}

声明:以上贴出的代码片出自正点原子提供的例程中。

调用 iap_write_appbin(0x08030000+x,appbuf, 1024*2) 。即可完成 转换-写入的操作流程。其中x为已经写入的大小。

由于每个程序大小都是不固定的,因此会出现获取最后一帧数据的时候,数据量不足2k的情况,这时候需要对最后的这一部分做处理,只写入实际获取到的长度,而不能直接写入2k的数据。

全部数据获取完成后,由于之前写入的地址是0x08030000,而这个地址不是我们设置的跳转地址,它在这里只是起到一个存储的功能,不会获得实际的程序运行权,所以,我们还需要实现一个功能,把0x08030000开始的128k数据复制到0x08010000开始的存储空间里。这里的实现方法可以参考我们用烧写器烧录的流程:擦除-写入-校验。代码片在这里就不再贴出了,可以照这个思路自己实现一下。

到这里整个在线更新的流程差不多就走完了,可以在清除更新标志后,直接跳转到用户程序区继续执行,也可以通过主动软件复位功能重启系统,然后让Bootloader判断跳转。

(完)2020.01.21

版权声明:本文为CSDN博主(weymin)原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/u013053268/article/details/104058622

围观 507