简介:
IAP,全称是“In-Application Programming”,中文解释为“在程序中编程”。IAP是一种对通过微控制器的对外接口(如USART,IIC,CAN,USB,以太网接口甚至是无线射频通道)对正在运行程序的微控制器进行内部程序的更新的技术(注意这完全有别于ICP或者ISP技术)。ICP(In-Circuit Programming)技术即通过在线仿真器对单片机进行程序烧写,而ISP技术则是通过单片机内置的bootloader程序引导的烧写技术。IAP技术使用远距离或无线的数据传输方案,甚至可以实现远程编程和无线编程。STM32微控制器带有可编程的内置闪存,同时STM32拥有在数量上和种类上都非常丰富的外设通信接口,因此在STM32上实现IAP技术是完全可行的。
实现IAP技术的核心是一段预先烧写在单片机内部的IAP程序。这段程序主要负责与外部的上位机软件进行握手同步,然后将通过外设通信接口将来自于上位机软件的程序数据接收后写入单片机内部指定的闪存区域,然后再跳转执行新写入的程序,最终就达到了程序更新的目的。
在STM32微控制器上实现IAP程序之前首先要回顾一下STM32的内部闪存组织架构和其启动过程。STM32的内部闪存地址起始于0x8000000,一般情况下,程序文件就从此地址开始写入。此外STM32是基于Cortex-M3内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动。而这张“中断向量表”的起始地址是0x8000004,当中断来临,STM32的内部硬件机制亦会自动将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。最后还需要知道关键的一点,通过修改STM32工程的链接脚本可以修改程序文件写入闪存的起始地址。
简单介绍STM32的启动过程和中断响应方式:
STM32的内部闪存(FLASH)地址起始于0x08000000,一般情况下,程序文件就从此地址开始写入。此外STM32是基于Cortex-M3内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,而这张“中断向量表”的起始地址是0x08000004,当中断来临,STM32的内部硬件机制亦会自动将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。
图中,STM32在复位后,先从0X08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的main函数,如图标号②所示;而我们的main函数一般都是一个死循环,在main函数执行过程中,如果收到中断请求(发生重中断),此时STM32强制将PC指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回main函数执行,如图标号⑤所示。
加入IAP:
1、 STM32复位后,从地址为0x8000004处取出复位中断向量的地址,并跳转执行复位中断服务程序,随后跳转至IAP程序的main函数,如图2中标号①、②所示。这个过程和图1相应部分是一致的。
2、 执行完IAP过程后(STM32内部多出了新写入的程序,图2中以灰色底纹方格表示,地址始于0x8000004+N+M)跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的main函数,其过程如图2的标号③所示。新程序的main函数应该也具有永不返回的特性。同时应该注意在STM32的内部存储空间在不同的位置上出现了2个中断向量表。
3、 在新程序main函数执行的过程中,一个中断请求来临,PC指针仍会回转至地址为0x8000004中断向量表处,而并不是新程序的中断向量表,如图2中标号⑤所示。注意到这是由STM32的硬件机制决定的。
4、 根据中断源跳转至对应的中断服务,如图2中标号⑥所示。注意此时是跳转至了新程序的中断服务程序中。
5、 中断服务执行完毕后,返回main函数。如图2中标号⑧所示。
从上述两个过程的分析可以得知,对将使用IAP过程写入的程序要满足2个要求:
1、新程序必须从IAP程序之后的某个偏移量为x的地址开始;
2、必须将新程序的中断向量表相应的移动,移动的偏移量为x;
IAP配置方法
1.设置APP程序的起始地址和存储空间大小 对于在FLASH里面运行的APP程序,我们只需要设置APP程序的起始地址,和存储空间大小即可。而对于在SRAM里面运行的APP程序,我们还需要设置SRAM的起始地址和大小。无论哪种APP程序,都需要确保APP程序的大小和所占SRAM大小不超过我们的设置范围。
2. 设置中断向量表偏移量 此步,我们通过修改SCB->VTOR 寄存器的值,实现对中断向量表偏移量的设置。这个偏移量的大小,其实就等于程序起始地址相对于0X08000000或者0X20000000的偏移。
3. 设置编译后运行fromelf.exe,生成.bin文件. 通过在User选项卡,设置编译后调用fromelf.exe,根据.axf文件生成.bin文件,用于IAP更新。
fromelf --bin !L -o .\BIN\A.bin //将hex文件生成当前文件夹下BIN目录下的A.bin文件
而设置程序起始位置的方法是(keil uvision4集成开发环境)在工程的“Option for Target….”界面中的“Target”页里将“IROM”的“Start”列改为欲使程序起始的地方,如图3中将程序起始位置设为0x8010000。
这里用的芯片是STM32F103VC;偏移地址为64KB,这里的0x40000意思是256kb
这边也需要对应的配置哦!
IAP是根据正点原子的串口IAP修改来的,这里的起始地址是0x800000;(可以去正点原子官网去找相关的资料,这里就不多赘述了)
//设置Bootloader的大小,算出APP起始位置 FLASH_BASE = 0x8000000
#define BOOTLOADER_SIZE (64*1024) /*64K bootloader*/
#define FIRMWARE_START_ADDR (FLASH_BASE + BOOTLOADER_SIZE)
#define BOOTLOADER_START_ADDR FLASH_BASE
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "stmflash.h"
#include "iap.h"
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:
#include <string.h>
#include "sys.h"
#include "led.h"
#include "iap.h"
#include "stmflash.h"
int main(void)
{
bool updateFlag = false; //是否需要升级 升级标志位
LCD_BL_ON();
while(1)
{
if(updateFlag)
{
//可以将升级的bin文件下载进app区 然后重启进行跳转
//........
iap_load_app(FIRMWARE_START_ADDR); //跳转到应用APP
}
else
{
LED_OFF();
iap_load_app(FIRMWARE_START_ADDR); //跳转到应用APP
}
}
}
APP:
这里需要修改misc.h 中 NVIC_VectTab_FLASH 的值为0x08010000
#define NVIC_VectTab_RAM ((uint32_t)0x20000000)
//只需要改这个即可
#define NVIC_VectTab_FLASH ((uint32_t)0x08000000 | 0x10000)
#define IS_NVIC_VECTTAB(VECTTAB) (((VECTTAB) == NVIC_VectTab_RAM) || \
((VECTTAB) == NVIC_VectTab_FLASH))
misc.c中调用这个;时钟初始化之后即可调用
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset)
{
/* Check the parameters */
assert_param(IS_NVIC_VECTTAB(NVIC_VectTab));
assert_param(IS_NVIC_OFFSET(Offset));
SCB->VTOR = NVIC_VectTab | (Offset & (uint32_t)0x1FFFFF80);
}
//此代码在初始化中
SysTickInit(); //时钟初始化
NVIC_SetVectorTable(FIRMWARE_START_ADDR,0);
//设置栈顶地址
//addr:栈顶地址
__asm void MSR_MSP(u32 addr)
{
MSR MSP, r0 //set Main Stack value
BX r14
}
typedef void (*iapfun)(void); //定义一个函数类型的参数.
iapfun jump2bootloader;
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_bootloader(u32 appxaddr)
{
int i;
if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法.
{
jump2bootloader = (iapfun)*(vu32*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址)
//MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
#if 1
for(i = 0; i < 8; i++)
{
NVIC->ICER[i] = 0xFFFFFFFF; /* 关闭中断*/
NVIC->ICPR[i] = 0xFFFFFFFF; /* 清除中断标志位 */
}
#endif
jump2bootloader(); //跳转到APP.
}
}
至于APP中通过上位机远程http升级等需自己根据相应功能改动等;
我这里用的GD32 APP程序跑的rt_thread nano,不需要改动其他的 只需要上文中misc.h中的改动,以及keil中图片显示的改动,初始化调用NVIC_SetVectorTable(FIRMWARE_START_ADDR,0);即可实现跳转并升级。