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

  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||(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();//上锁

  该函数可以实现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("ok\r\n");

}

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

//清除app固化配置

void iap_clear_flag(void)

{

Test_Write(APP_CONFIG_ADDR,APP_CONFIG_CLEAR_VALUE);

printf("ok\r\n");

}

对iap_jump2app命令的响应如下

//跳转到应用程序段

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

void iap_load_app(u32 appxaddr)

{

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

{

printf("ok\r\n");

Delay_Ms(10);

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

MSR_MSP(*(vu32*)appxaddr);                //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)

jump2app();                               //跳转到APP.

}

else

{

printf("program in flash
is error\r\n");

}

}

//跳转到app区域运行

void iap_jump_app_s(void)

{

iap_load_app(FLASH_APP1_ADDR);//跳转到app的复位向量地址

}

接下来就是iap_down,用于下载的核心算法

#define FLASH_APP1_ADDR     0x08002000     //第一个应用程序起始地址(存放在FLASH)

//保留的空间为IAP使用

u16 iapbuf[1024] = {0}; //用于缓存数据的数组

u16 receiveDataCur = 0;  //当前iapbuffer中已经填充的数据长度,一次填充满了之后写入flash并清零

u32 addrCur = FLASH_APP1_ADDR;         //当前系统写入地址,每次写入之后地址增加2048

//开始下载

void iap_down_s(void)

{

u16 i = 0;

u16 temp = 0;

u16 receiveCount;

printf("begin,wait data
download\r\n");

receiveMode = 1;//串口进入下载接收数据模式

while(1)

{

//循环接收数据,每次必须要发128个数据下来,如果没有128,说明这是最后一包数据

//接收到一包数据之后,返回一个小数点,发送完成,系统编程完成之后返回一个iap_over

if(serial_Buffer_Length & 0x8000)

{

receiveCount =
(u8)(serial_Buffer_Length&0x00ff);

if(receiveCount == 128)//满足一包,填充并查看是否有了1024字节,有了写入闪存

{

for(i = 0; i <
receiveCount; i+=2)

{

//数据八位融合为16位

temp =
(((u16)serial_Buffer[i+1])<<8) + ((u16)serial_Buffer[i]);

iapbuf[receiveDataCur]
= temp;

receiveDataCur++;//完成之后receiveDataCur++;

}

receiveExpectCount =
0;//清除期望接收模式

serial_Buffer_Length =
0;//清除串口满标志

printf(".");//每次接受一次数据打一个点

//此时需要检测receiveDataCur的值,要是放满了,就需要写入

if(receiveDataCur ==
1024)

{

//写入flash中

STMFLASH_Write(addrCur,iapbuf,1024);

//printf("\r\nwrite
addr %x,length 1024\r\n",addrCur);

addrCur += 2048;//地址+2048

//写完之后receiveDataCur要清零等待下一次传输

receiveDataCur = 0;

}

else //有可能最后一包有128个数据但是最终没有2048个数据,此时扩展一个指令用于完成最后一个的写入

{

}

//还没放满,等待下一次数据过来

}

else   //不满足一包,说明数据传送这是最后一包,写入闪存

{

//没有一包也要传送到缓存中

for(i = 0; i <
receiveCount; i+=2)

{

//数据八位融合为16位

temp =
(((u16)serial_Buffer[i+1])<<8) + ((u16)serial_Buffer[i]);

iapbuf[receiveDataCur]
= temp;

receiveDataCur++;//完成之后receiveDataCur++;

}

receiveExpectCount =
0;//清除期望接收模式

serial_Buffer_Length =
0;//清除串口满标志

printf(".");//每次接受一次数据打一个点

//之后就要将这数据写入到闪存中

STMFLASH_Write(addrCur,iapbuf,receiveDataCur);//将最后的一些内容字节写进去.

//printf("\r\nwrite
addr %x,length %d\r\n",addrCur,receiveDataCur);

//写完之后要把地址恢复到原来的位置

addrCur =
FLASH_APP1_ADDR;

receiveDataCur = 0;

//写完之后要退出下载循环并告诉上位机,已经下载完了

printf("download
over\r\n");

//同时,也要退出下载循环模式

receiveMode = 0;

return;

}

这段代码的核心思想是上位机每次发送128个数据下来,发满了2048个写一次flash,当最后一包数据不是128的时候说明数据发送完成了,这时候退出下载模式,但是当遇到最后一包数据也是128个时候怎么办呢,于是定义了这个指令

  iap_over,上位机侦测到最后一包数据也是128个的时候补充发送该命令,下位机将缓存写入并退出

//最后一包有128个数据但是最终没有2048个数据

//收到这个指令检测receiveDataCur和addrCur的值,

//完成最后的写入

void iap_over_s(void)

{

//这个时候,依然在串口下载模式

if(receiveDataCur != 0)

{

STMFLASH_Write(addrCur,iapbuf,receiveDataCur);//将最后的一些内容字节写进去.

//printf("write addr
%x,length %d",addrCur,receiveDataCur);

addrCur = FLASH_APP1_ADDR;

receiveDataCur = 0;

//同时,也要退出下载模式

receiveMode = 0;

}

printf("ok\r\n");

}

这是iap的核心代码,接下来我们在main函数中检测app固化标志,如果标志位设置,那么跳转到app

if(STMFLASH_ReadHalfWord(APP_CONFIG_ADDR) == 0x5555)

{

//直接跳转到APP

iap_jump_app_s();

}

  到这里基本上就完成了iap的工作,可是想想,还需要设置一个地方,我们要在target中设置使用的flash空间,不能超范围,如下

如果需要flash下载的话还需要设置jlink的flash下载设置如下.

  这样可以直接使用jlink将代码下载到单片机中,而且不会影响原先的app程序,注意,要选择erase sector used,不能全部擦除flash

  桥斗麻袋,我们忘了一件事情,假设我们设置了app标志,那及时app能跳转到iap中,iap岂不是马上会跳转回app,永远不能等待下载?

解决办法就是我们在app中app跳转到iap的指令中将app固化标志清除掉,在app代码中添加一条指令

Iap,其响应方法为

__asm void MSR_MSP(u32 addr)

{

MSR MSP, r0           //set Main Stack value

BX r14

}

void iap_jump(u32 iapxaddr)

{

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

{

printf("ok\r\n");

Delay_Ms(10);

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

MSR_MSP(*(vu32*)iapxaddr);                //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)

jump2iap();                               //跳转到APP.

}

else

{

printf("iap program
loss,please check\r\n");

}

}

#define APP_CONFIG_ADDR     0X08001FFC //配置地址

#define APP_CONFIG_SET_VALUE    0X5555 //设置值

#define APP_CONFIG_CLEAR_VALUE  0XFFFF //清零值

void iap_Func(void)

{

Test_Write(APP_CONFIG_ADDR,APP_CONFIG_CLEAR_VALUE);

iap_jump(FLASH_IAP_ADDR);//跳转到iap的复位向量地址

}

  可以看到,我们先清除了app标志,然后在跳转到iap程序中,就不会影响到iap的流程了,同时app代码也还在单片机里面,另外,app工程里面也要设置两个东西

因为flash的起始地址为0x08000000,而我们用了之前2000的空间作为iap代码空间,那么,app代码的起始空间就变成了0x8002000,还有一个下载界面需要设置

  红框部分也要修改.

  是不是没有说中断向量表的问题,在iap中我们不需要考虑中断向量表,因为默认就是在0x8000000位置的,但是在app中代码的起始位置变了,必须重新设置中断向量表

在system_stm32f10x.c中有一个system_init函数,该函数被启动代码调用,配置系统时钟,在该函数中的最后一句为

#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

其中VECT_TAB_OFFSET就是我们要定义的偏移量,也就是app程序的起始地址偏移,我们知道是2000,那么该值的宏就需要修改,在大约128行的位置

//此处为flash偏移地址,app应当修改这个地址

#define VECT_TAB_OFFSET  0x2000
/*!< Vector Table base offset field.

This value
must be a multiple of 0x200. */

  嗯,完整流程就是这样了,另外,该工程分为三个部分,一个iap,一个app,还有一个当然是下载程序啦,下载程序是这样的

  三个代码的工程我会打包上传到csdn,想更深入了解的可以下载来看看,软件用mfc编写的

最后,下载需要使用bin文件,该文件的生成方法参考另一篇博文,lpc1768-iap

代码打包上传地址

http://download.csdn.net/detail/dengrengong/8499911

时间: 2024-11-10 01:25:24

STM32_IAP详解(有代码,有上位机)的相关文章

数据结构 - 简单选择排序(simple selection sort) 详解 及 代码(C++)

数据结构 - 简单选择排序(simple selection sort) 本文地址: http://blog.csdn.net/caroline_wendy/article/details/28601965 选择排序(selection sort) : 每一趟在n-i+1个记录中选取关键字最小的记录作为有序序列中第i个记录. 简单选择排序(simple selection sort) : 通过n-i次关键字之间的比较, 从n-i+1个记录中选出关键字最小的记录, 并和第i个记录交换. 选择排序需

ios中创建可以拖动的view原理和实现详解(含代码)

有时候我们会需要在界面上拖动view;uiview是继承于uiresponder的,所以可以响应触摸相关的事件. 重点是以下一组方法: - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event - (void)touchesEnded:(NSSet *)touches withEvent:(UIE

数据结构 - 归并排序(merging sort) 详解 及 代码

归并排序(merging sort) 详解 及 代码 本文地址: http://blog.csdn.net/caroline_wendy 归并排序(merging sort): 包含2-路归并排序, 把数组拆分成两段, 使用递归, 将两个有序表合成一个新的有序表. 归并排序(merge sort)的时间复杂度是O(nlogn), 实际效果不如快速排序(quick sort)和堆排序(heap sort), 但是归并排序是稳定排序, 而快速排序和堆排序则不是. 代码: /* * main.cpp

数据结构 - 堆排序(heap sort) 详解 及 代码(C++)

堆排序(heap sort) 详解 及 代码(C++) 本文地址: http://blog.csdn.net/caroline_wendy 堆排序包含两个步骤: 第一步: 是建立大顶堆(从大到小排序)或小顶堆(从小到大排序), 从下往上建立; 如建堆时, s是从大到小; 第二步: 是依次交换堆顶和堆底, 并把交换后的堆底输出, 只排列剩余的堆, 从上往下建立; 如构造时, s始终是1; 代码: /* * main.cpp * * Created on: 2014.6.12 * Author: S

nginx学习三 nginx配置项解析详解及代码实现

nginx配置项解析详解及代码实现 0回顾 在上一节,用nginx简单实现了一个hello world程序:当我们在浏览器中输入lochost/hello ,浏览器就返回:hello world.为什么会这样呢,简单一点说就是当我们请求访问hello这个服务,nginx就会看配置文件中是否有,如果有,根据具体的handler处理后把处理的结果返回给用户,没有就返回not found. location /hello { test_hello ;//无参数的配置 这其实是一个简单的配置.这节我们来

C#的String.Split 分割字符串用法详解的代码

代码期间,把代码过程经常用的内容做个珍藏,下边代码是关于C#的String.Split 分割字符串用法详解的代码,应该对码农们有些用途. 1) public string[] Split(params char[] separator)2) public string[] Split(char[] separator, int count)3) public string[] Split(char[] separator, StringSplitOptions options)4) public

Android自定义View【实战教程】5??---Canvas详解及代码绘制安卓机器人

友情链接: Canvas API Android自定义View[实战教程]3??--Paint类.Path类以及PathEffect类详解 神马是Canvas 基本概念 Canvas:可以理解为是一个为我们提供了各种工具的画布,我们可以在上面尽情的绘制(旋转,平移,缩放等等).可以理解为系统分配给我们一个一个内存空间,然后提供了一些对这个内存空间操作的方法(API), 实际存储是在下面的bitmap. 两种画布 这里canvas可以绘制两种类型的画图,分别是view和surfaceView. V

SylixOS 调试方法详解——静态代码分析

1. SylixOS调试方法介绍 SylixOS 实现了一个功能强大的调试 stub,可在设备或模拟器上在线调试应用程序,RealEvo-IDE 也提供配套的调试插件.目前 RealEvo-IDE 既支持自动推送调试.也支持传统的手动启动 gdbserver 的调试方式.在官方公布的使用手册中没有详细写出在日常开发中非常实用的几种调试方式,我们通过新建几个App工程来做相应的演示,主要有代码静态分析.代码覆盖率检查.性能分析以及一个实战案列的演示,本文主要介绍代码静态分析调试. 2. Sylix

UIWebView用法详解及代码分享

今天加入一个QQ群,也算是一个圈子,群主的要求是满足他的三个条件.经过与群主的沟通,终于得到通过,顺利加入. 群主是一个93年的小伙,而且是河南的老乡.没想到小伙子这么有号召力.我也是偶然在我的空间发现他的,看他每天发的文章,还挺有意思. 90后的小伙子都这么厉害了,有什么理由不努力奋斗呢!多加入以下圈子,认识一下身边的牛人.说不定会有意外的收获. 今天我们来详细UIWebView用法.UIWebView是iOS内置的浏览器控件,可以浏览网页.打开文档等 能够加载html/htm.pdf.doc