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程序运行流程
正常运行
上图为stm32程序正常运行的流程图。
stm32的flash地址从0x08000000开始,这里一般固定保存这栈顶地址。然后偏移4个字节,找到复位中断向量,它就是中断向量表的起始地址。复位中断向量保存着复位中断程序,所以接下来就会开始执行复位中断程序Reset_Handle()。执行完毕后,指针就会继续跳转到main函数并执行。main函数运行时,产生中断请求,指针随之跳转到中断向量表寻找对应的中断向量。找到之后,运行对应的中断服务函数。运行结束,继续回到main函数。
IAP运行流程
我们可以看到,IAP流程中,falsh内增加了一个IAP程序。在正常运行的程序前,系统会先运行它,然后再运行app程序。一般地,我们把相对于flash0地址的程序称为bootloader程序,也就是先运行的那个程序。boot loader程序运行完成后,再跳转的新的app程序里运行。
但是,我们可以从图中看到,此时的flash中含有两个中断向量表:一个是旧的,一个是新的。main函数在收到收到中断请求后,指针依然会先跳转的旧的中断向量表里,获得对应的偏移量,才能跳转到新的向量表里执行正确的中断程序。
因此,我们也能够得出结论,IAP程序必须满足以下两个要求:
- 新程序必须在IAP程序之后,某个偏移量x的新地址中开始运行。
- 必须将新程序的中断向量表对应地偏移x位。
基于UART的IAP
运行程序地址设置
在stm32f10x.h中可以查到stm32相关起始地址的宏定义。flash的起始地址,为0x08000000,那么我们偏移0x10000(64k字节),用这段空间存储bootloader程序。如下图,我们在option-->target里设置app程序的起始地址为0x08010000,F103rct6的falsh大小为0x40000,减去0x10000,还剩下0x30000能用来存放app程序。
这里给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程序的简单移植。更多的需求,可以根据不同项目的要求不断优化。