关于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芯片(M0,M3,M4),它们的主存储器结构可能不一样,但是他们都有一个叫“系统存储器”的区域,此区域是留给ST自己用来存放芯片的bootloader程序,此程序在芯片出厂的时候已经固化在芯片内部。
系统存储器的Bootloader程序会通过串口1接受应用程序。
STM32启动模式选择
ICP下载流程
如果没有使用下载软件的情况下利用串口1进行下载的话就要B0接1,B1接0,系统存储器被选为启动区域。启动代码从串口1接受程序,从地址0x08000000开始写入。
JTAG/SWD下载,直接下载到FLASH指定区域。
一般的程序执行流程
STM32的内部闪存(FLASH)地址起始于0x08000000,一般情况下,程序文件就从此地址开始写入。
0x08000004开始存放中断向量表。
当中断来临,STM32的内部硬件机制亦会自动将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。
1.STM32复位后,从0X08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序。
2.在复位中断服务程序执行完之后,会跳转到我们的main函数。
main函数执行过程中,如果收到中断请求(发生重中断),此时STM32强制将PC指针指3.回中断向量表处。
4.根据中断源进入相应的中断服务程序。
5.在执行完中断服务程序以后,程序再次返回main函数执行
加入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
IAP升级应用程序过程
IAP程序必须满足两个要求
1.新程序(APP)必须在IAP程序(bootloader)之后的某个偏移量为x的地址开始;
2.必须将新程序(APP)的中断向量表相应的移动,移动的偏移量为x;
APP程序的生成步骤:
1.设置APP程序的起始地址和存储空间大小
0x08000000是flash的起始地址,加上0x10000后变成0x08010000表示你app偏移后的地址,0x70000是空间大小;大容量的flash 64k SIZE为0x80000; 偏移了0x10000剩余0x70000。
注意:要想知道自己的IAP函数偏移了多少可以看编译器的编译信息:
这一条编译信息中占有flash空间为:Code+RO-data的大小单位字节,如果没有记错的话
SRAM占用大小:RW-dat+ZI-data(这个不保证正确)。这样就可以算出APP要偏移的空间大小了;
-
设置中断向量表偏移量
设置SCB->VTOR的值即可。如:SCB->VTOR=FLASH_BASE|0x010000在FLASH基地址偏移0x10000. -
设置MDK编译后运行fromelf.exe,生成.bin文件.
通过在MDK User选项卡,设置编译后调用fromelf.exe,根据.axf文件生
成.bin文件,用于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 文件夹
找里面的ARMCC
再打开
的地址。
–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地址从头执行任务。
第二种:
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 中的启动代码,更容易理解
关于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