单片机 > 单片机程序设计 > 详情

STM32_IAP详解(有代码,有上位机)

发布时间:2020-08-27 发布时间:
|

Iap,全名为in applacation programming,即在应用编程,与之相对应的叫做isp,in system programming,在系统编程,两者的不同是isp需要依靠烧写器在单片机复位离线的情况下编程,需要人工的干预,而iap则是用户自己的程序在运行过程中对User Flash 的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。在工程应用中经常会出现我们的产品被安装在某个特定的机械结构中,更新程序的时候拆机很不方便,使用iap技术能很好地降低工作量.


实现iap有两个很重要的前提,首先,单片机程序能对自身的内部flash进行擦写,第二,单片机要有能够和外部进行通讯的方式,无论是网络还是别的方式,只要能传输数据就行


通常实现 IAP 功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通过某种通信方式(如 USB、 USART)接收程序或数据,执行对第二部分代码的更新;第二个项目代码才是真正的功能代码。这两部分项目代码都同时烧录在 User Flash 中,当芯片上电后,首先是第一个项目代码开始运行,它作如下操作: 

1)检查是否需要对第二部分代码进行更新 

2)如果不需要更新则转到 4) 

3)执行更新操作 

4)跳转到第二部分代码执行 


第一部分代码必须通过其它手段,如 JTAG 或 ISP 烧入;第二部分代码可以调用第一部分的功能


也就是说,将iap和app做成两个程序,这是其中的一种策略,还有一种策略,可以把iap程序和app程序做在一个代码中,但是那样耦合性有点高,我们先进行第一种尝试.


要做iap首先我们要知道stm32的启动流程,流程如下

1、单片机从0x80000000位置启动,并将该地址当成系统栈顶地址

2、运行到中断向量表中,默认的中断向量表为0x80000004,该位置存放复位中断

3、跳转到复位中断处理函数当中,进行系统初始化,然后运行main函数


当我们准备用iap的时候,单片机内部是有着两套程序的,这个时候我们就需要在iap中

和app中分别放置两套中断向量表,当iap代码中将app烧写到flash中之后,跳转到app的中断向量表中,程序就可以正常执行了,当然需要修改某些系统设置,使得在app和iap阶段单片机可见的中断向量表只能有一套(具体请查看stm32芯片的启动代码)


而当需要从app跳转到iap的时候,只需要将app的中断向量表修改成iap的中断向量表,同时主动跳转到iap的reset中断处理程序,这样就能再次开始iap流程.


这样,在系统中就需要我们确定几个东西,第一个是iap程序的中断向量表,为0x80000004位置(80000000存放的是msp的初始值),第二个是app程序的中断向量表,该位置需要根据iap程序的长度计算,比如iap占用了64K,那么512K的芯片而言,就还有448K的空间存放app程序,448K的最开始放置中断向量表,位置就应该是0x08000000+0x10004的位置.


Cortex-m3的中断向量并不是在程序中固定的,我们可以通过修改某些寄存器来修改对于当前应用的中断向量表位置.


决定中断向量表的寄存器是如下这个

通过修改这个寄存器的值,我们可以控制对于当前单片机应用来说可见的向量表的位置(也就说说逻辑上我们有两个向量表,但是同一时间只有一个运行)


以上是内核阶段的操作,在此之外我们还需要对stm32的flash进行编程,那么就涉及到删除的编程和擦除操作,这需要参考stm32的闪存编程手册


首先,当单片机复位之后,闪存式被锁住的,需要主动去解锁,向FLASH_KEYR写入两个指定的连续键值用于解锁


然后将需要写入的闪存擦除,擦除之后在进行写入,写入完成,再次上锁


对应的代码如下

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+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

{

if(STMFLASH_BUF[secoff+i]!=0XFFFF)break;//需要擦除    

}

if(i

{

FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区

for(i=0;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();//上锁

该函数可以实现flash的写入操作,接下来我们需要定义一套通讯协议用于串口数据传输

//串口接收缓冲区

u8 serial_Buffer[SERIAL_MAX_LENGTH] = {0};

//串口接收数据长度

u16 serial_Buffer_Length = 0;

u8 receiveMode = 0;//接收参数的中断处理模型,为0的时候是命令模式,为1的时候为下载模式

u8 receiveExpectCount = 0;//串口期望接收长度

//串口中断处理

static void SerialRecv(u8 ch)

{

if(receiveMode == 0)

{

if((serial_Buffer_Length&0x8000) == 0x8000)//已经接收完成,系统还没处理

{

serial_Buffer_Length |= 0x8000;//退出

}

else if((serial_Buffer_Length&0x4000) == 0x4000)//接收到回车还没接收到换行

{

if(ch == 'n')serial_Buffer_Length |= 0x8000;

else

{

//一帧接受失败

serial_Buffer_Length = 0;

}

}

else

{

if((serial_Buffer_Length&0xff) < SERIAL_MAX_LENGTH)

{

if(ch == 'r')serial_Buffer_Length |= 0x4000;

else

{

serial_Buffer[(serial_Buffer_Length&0xff)] = ch;

serial_Buffer_Length++;

}

}

else

{

//一帧接受失败

serial_Buffer_Length = 0;

}

}

}

else

{

//下载模式,只控制字符串的量,数据的第一位是该数据包的长度,接收到这么多长度,接收完成位置一

//注意,在这种模式下,清除serial_Buffer_Length之前应当清除receiveExpectCount的值

if(receiveExpectCount == 0)//期望下载为0,第一个数就是期望下载数

{

receiveExpectCount = ch;

}

else

{

if((serial_Buffer_Length&0x8000) == 0x8000)//已经接收完成,系统还没处理,此时不接收数据

{

serial_Buffer_Length |= 0x8000;//退出

}

else

{

serial_Buffer[(serial_Buffer_Length&0xff)] = ch;//接收数据并保存

serial_Buffer_Length++;

if((serial_Buffer_Length&0xff) == receiveExpectCount)//接收到了期望长度的数据

{

serial_Buffer_Length |= 0x8000;//一包接收完成标志

}

}

}

}

}

这样系统就能接收数据了,接下来定义五个命令

"iap_down"

"iap_jump_app"

"iap_over"

"iap_set_flag"

"iap_clear_flag"

第一个命令为系统开始下载,在这个命令之后上位机就能够将程序数据发下来了,

第二个命令为iap跳转到app的跳转指令

第三个命令是指示iap完成,将系统缓冲区清空的指令

第四个指令为设置app标志,当iap检测到该标志的时候直接跳转到app程序中

第五个指令为清除app标志,让iap程序不自动跳转到app程序中,我们分别来看

首先是iap_set_flag,其响应函数如下

#define APP_CONFIG_ADDR     0X08001FFC //配置地址

#define APP_CONFIG_SET_VALUE    0X5555 //设置值

#define APP_CONFIG_CLEAR_VALUE  0XFFFF //清零值

//设置app固化配置

void iap_set_flag_s(void)

{

Test_Write(APP_CONFIG_ADDR,APP_CONFIG_SET_VALUE);

printf("okrn");

}

我们使用0x08000000-0x08003000来存放iap代码,并将0X08001FFC作为存放app固化标志的地方

//清除app固化配置

void iap_clear_flag(void)

{

Test_Write(APP_CONFIG_ADDR,APP_CONFIG_CLEAR_VALUE);

printf("okrn");

}

对iap_jump2app命令的响应如下

//跳转到应用程序段

//appxaddr:用户代码起始地址.

void iap_load_app(u32 appxaddr)

{

if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000)  //检查栈顶地址是否合法.0x20000000是sram的起始地址,也是程序的栈顶地址

{

printf("okrn");

Delay_Ms(10);

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

MSR_MSP(*(vu32*)appxaddr); //


关键字:STM32  IAP  上位机 

『本文转载自网络,版权归原作者所有,如有侵权请联系删除』

热门文章 更多
PIC单片机基础知识之二