IAP串口下载实验

523 阅读6分钟

IAP简介

IAP是in-application programming的缩写,中文翻译为“应用程序内编程”。它可以在应用程序运行时,调用特定的IAP程序对另外一段程序的flash空间进行读写。它主要用于固件的升级和数据存储。IAP程序一般有两个部分,一个为bootloader程序,不执行正常功能,只是以某种方式接收第二部分的程序,并进行更新。第二部分的程序才是IAP程序执行的主体,用于实现功能。这两部分的代码都储存在UserFlash中。 具体流程如下:

graph TB
A(烧录进芯片的bootloader程序与app程序) --> C{系统上电检查是否需要更新app程序}  
C{系统上电检查是否需要更新app程序} -- 是 --> D[执行更新app程序]  
C{系统上电检查是否需要更新app程序} -- 否 --> E[继续执行原来的程序]

STM32程序运行流程

正常运行

中断流程.png
上图为stm32程序正常运行的流程图。
stm32的flash地址从0x08000000开始,这里一般固定保存这栈顶地址。然后偏移4个字节,找到复位中断向量,它就是中断向量表的起始地址。复位中断向量保存着复位中断程序,所以接下来就会开始执行复位中断程序Reset_Handle()。执行完毕后,指针就会继续跳转到main函数并执行。main函数运行时,产生中断请求,指针随之跳转到中断向量表寻找对应的中断向量。找到之后,运行对应的中断服务函数。运行结束,继续回到main函数。

IAP运行流程

IAP.png
我们可以看到,IAP流程中,falsh内增加了一个IAP程序。在正常运行的程序前,系统会先运行它,然后再运行app程序。一般地,我们把相对于flash0地址的程序称为bootloader程序,也就是先运行的那个程序。boot loader程序运行完成后,再跳转的新的app程序里运行。
但是,我们可以从图中看到,此时的flash中含有两个中断向量表:一个是旧的,一个是新的。main函数在收到收到中断请求后,指针依然会先跳转的旧的中断向量表里,获得对应的偏移量,才能跳转到新的向量表里执行正确的中断程序。
因此,我们也能够得出结论,IAP程序必须满足以下两个要求:

  1. 新程序必须在IAP程序之后,某个偏移量x的新地址中开始运行。
  2. 必须将新程序的中断向量表对应地偏移x位。

基于UART的IAP

运行程序地址设置

在stm32f10x.h中可以查到stm32相关起始地址的宏定义。flash的起始地址,为0x08000000,那么我们偏移0x10000(64k字节),用这段空间存储bootloader程序。如下图,我们在option-->target里设置app程序的起始地址为0x08010000,F103rct6的falsh大小为0x40000,减去0x10000,还剩下0x30000能用来存放app程序。

起始地址.png
这里给bootloader留下的空间不是固定的,理论上只需要确保app程序在bootloader程序之后,并且偏移量是0x200的倍数即可。

中断向量表的偏移量设置

系统启动时,会先调用systemInit()函数进行初始化时钟系统,同时还会初始化中断向量表。我们在systemInit()的结尾处可以看到如下代码:

#ifdef VECT_TAB_SRAM
  SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#else
  SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif 

可以看到,VTOR寄存器存放的是中断向量表的起始地址 。默认的情况VECT_TAB_SRAM没有定义,所以执行SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;,我们的程序存放在flash中,因此可以在app程序的main函数开头添加以下代码,以实现中断向量表起始地址的重设:

SCB->VTOR = FLASH_BASE | 0x10000;

只要固件大小不超过定义的大小,就可以成功更新代码。至此,我们就成功配置了IAP烧写程序的步骤了。 需要提醒的是,IAP烧写的是bin格式文件,而不是hex。通过MDK自带的工具fromelf.exe可以进行转换。

bootloader程序编写

将接收到的app程序写入到flash中。

typedef  void (*iapfun)(void);				//定义一个函数类型的参数.   
#define FLASH_APP1_ADDR		0x08010000  	//第一个应用程序起始地址(存放在FLASH)

void iap_load_app(u32 appxaddr);			//跳转到APP程序执行
void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 applen);	//在指定地址开始,写入bin
#endif

/
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)	//检查栈顶地址是否合法.
	{ 
		jump2app = (iapfun)*(vu32*)(appxaddr+4);
                //用户代码区第二个字为程序开始地址(复位地址)
		MSR_MSP(*(vu32*)appxaddr);
                //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
		jump2app();//跳转到APP.
	}
}		 

main函数编写

int main(void)
{ 
	u16 oldcount=0;	//老的串口接收数据值
	u16 applenth=0;	//接收到的app代码长度
	u8 clearflag=0; 
	delay_init(); //延时函数初始化	  
	DEBUG_UART_Config();//串口初始化为115200,中断优先级分组2
	LED_GPIO_Config();//初始化LED	
   
	while(1)
	{
	 	if(USART_RX_CNT)
		{
                    if(oldcount==USART_RX_CNT)//新周期内,没有收到任何数据,认为本次数据接收完成.
                    {
                         applenth=USART_RX_CNT;
                         oldcount=0;
                         USART_RX_CNT=0;
                         printf("用户程序接收完成!\r\n");
                         printf("代码长度:%dBytes\r\n",applenth);
                     }else    oldcount=USART_RX_CNT;			
		}
                
		delay_ms(100);
		GPIO_ToggleBits(GPIOA,GPIO_Pin_8);
                
		if(applenth)
		{
			printf("开始更新固件...\r\n");	
			printf("Copying APP2FLASH...\r\n");
                        if(((*(vu32*)(0X20001000+4))&0xFF000000)==0x08000000)
                        //判断是否为0X08XXXXXX.
			{
                             iap_write_appbin(FLASH_APP1_ADDR,USART_RX_BUF,applenth);
                             //更新FLASH代码
                             printf("固件更新完成!\r\n");
                             iap_load_app(FLASH_APP1_ADDR);//执行FLASH APP代码                 
                             printf("开始执行FLASH用户代码!\r\n");
                         }else 
                         {
                                printf("Illegal FLASH APP!  \r\n"):
                                printf("非FLASH应用程序!\r\n");
                          }
                 }else 
			{
				printf("没有可以更新的固件!\r\n");
				printf("No APP!\r\n");
			}
			clearflag=7;//标志更新了显示,并且设置7*300ms后清除显示
        }
}

以上,我们就完成了IAP程序的简单移植。更多的需求,可以根据不同项目的要求不断优化。