STM32F407基础总结系列(三)
STM32F4bootloader及在线升级IAP基本原理
一 、前言
在线升级是产品开发维护的必要功能之一,对产品的迭代优化、需求定制、稳定性提升以及人力节约上提供了很大帮助。就像我们的手机更新系统一样不断地升级不断地有新的体验。
此部分内容从基于STM32F407进行BootLoader、内存分布、IAP升级原理、升级模式等方面进行介绍,其它类型的单片机可进行参考设置。
二、背景知识补充
先梳理一下STM32启动过程以及变量、代码在内存中分布过程。
2.1 系统启动过程
了解STM32的启动过程可以从启动文件以及上电执行了一些流程开始。
首先是启动文件,启动文件的结构如下图所示:
这里还有一点也要说一下,如果使用了c++的类定义了对象,构造函数会在标准库__main中执行,也就是在执行主函数main前所有的全局变量构造函数均已初始化完成。
中断函数的地址是由keil编译器固定好的,更改编程ROM初始地址,中断函数地址会相应的在生成的bin文件中被改掉。
2.2 内存映射分布
下面说一下内存映射的过程:
单片机有两个引脚会引出来,当外部给不同电平时产生不同的内部地址映射,正常用的就是从主Flash映射,从系统存储器映射常用来ISP串口一键下载,从SRAM映射常用来调试程序,解锁flash等。
当从0x00000000映射到FLASH上时,0x00000000-0x000fffff就相当于FLASH0x08000000-0x080fffff的别名,操作谁都一样,就像指向指针的指针。
接下来就是代码变量堆栈的分布情况了,这里以从flash启动为例。
正常编写完程序,用keil编译后生成的bin文件或是烧写程序到单片机都是只有FLASH(相当于ROM)里的内容,具体可以通过map文件查看。
Memory Map of the image
Image Entry point : 0x08000189
Load Region LR_IROM1 (Base: 0x08000000, Size: 0x000005a8, Max: 0x00080000, ABSOLUTE)
Execution Region ER_IROM1 (Exec base: 0x08000000, Load base: 0x08000000, Size: 0x00000590, Max: 0x00080000, ABSOLUTE)
Exec Addr Load Addr Size Type Attr Idx E Section Name Object
0x08000000 0x08000000 0x00000188 Data RO 232 RESET startup_stm32f40_41xxx.o
0x08000188 0x08000188 0x00000008 Code RO 290 * !!!main c_w.l(__main.o)
0x08000190 0x08000190 0x00000034 Code RO 449 !!!scatter c_w.l(__scatter.o)
0x080001c4 0x080001c4 0x0000001a Code RO 451 !!handler_copy c_w.l(__scatter_copy.o)
0x080001de 0x080001de 0x00000002 PAD
0x080001e0 0x080001e0 0x0000001c Code RO 453 !!handler_zi c_w.l(__scatter_zi.o)
0x080001fc 0x080001fc 0x00000002 Code RO 317 .ARM.Collect$$libinit$$00000000 c_w.l(libinit.o)
0x080001fe 0x080001fe 0x00000004 Code RO 323 .ARM.Collect$$libinit$$00000001 c_w.l(libinit2.o)
0x08000202 0x08000202 0x00000000 Code RO 326
0x08000226 0x08000226 0x00000002 PAD
0x08000228 0x08000228 0x00000010 Code RO 214 .emb_text sys.o
0x08000238 0x08000238 0x0000001c Code RO 3 .text main.o
0x08000254 0x08000254 0x0000001a Code RO 139 .text stm32f4xx_it.o
0x0800026e 0x0800026e 0x00000002 PAD
0x08000270 0x08000270 0x00000210 Code RO 188 .text system_stm32f4xx.o
0x08000480 0x08000480 0x00000040 Code RO 233 .text startup_stm32f40_41xxx.o
0x080004c0 0x080004c0 0x0000002c Code RO 259 .text iap.o
0x080004ec 0x080004ec 0x00000006 Code RO 288 .text c_w.l(heapauxi.o)
0x080004f2 0x080004f2 0x0000004a Code RO 308 .text c_w.l(sys_stackheap_outer.o)
0x0800053c 0x0800053c 0x00000012 Code RO 310 .text c_w.l(exit.o)
0x0800054e 0x0800054e 0x00000002 PAD
0x08000550 0x08000550 0x00000008 Code RO 318 .text c_w.l(libspace.o)
0x08000558 0x08000558 0x0000000c Code RO 379 .text c_w.l(sys_exit.o)
0x08000564 0x08000564 0x00000002 Code RO 390 .text c_w.l(use_no_semi.o)
0x08000566 0x08000566 0x00000000 Code RO 392 .text c_w.l(indicate_semi.o)
0x08000566 0x08000566 0x0000000a Code RO 377 x$fpl$fpinit fz_wm.l(fpinit.o)
0x08000570 0x08000570 0x00000020 Data RO 447 Region$$Table anon$$obj.o
Execution Region RW_IRAM1 (Exec base: 0x20000000, Load base: 0x08000590, Size: 0x00000678, Max: 0x00020000, ABSOLUTE)
Exec Addr Load Addr Size Type Attr Idx E Section Name Object
0x20000000 0x08000590 0x00000014 Data RW 189 .data system_stm32f4xx.o
0x20000014 0x080005a4 0x00000004 Data RW 260 .data iap.o
0x20000018 - 0x00000060 Zero RW 319 .bss c_w.l(libspace.o)
0x20000078 - 0x00000200 Zero RW 231 HEAP startup_stm32f40_41xxx.o
0x20000278 - 0x00000400 Zero RW 230 STACK startup_stm32f40_41xxx.o
通过编译的信息可以总结出stm32内存分布的情况,编译烧写目标为flash,上电运行到main函数前完成对ram的分配,ram中全局不为零变量从flash中复制,全局为零以及堆栈大小的信息包含在flash中的code中(具体在哪里暂时没找到,不过启动文件中可以设置堆栈大小的就证明了此部分的信息)在c语言库函数__mian()执行,运行过程中对堆栈进行操作。这里有一点药注意,当你操作msp堆栈指针时会清空局部变量的值。所以在BootLoader中尽量使用全局变量。
到这里基本启动过程以及内存分布就差不多了,iap操作就是操作这些地址及变量的分布。
三、bootloader
BootLoader的意思就是启动引导,跟电脑启动先加载主板BIOS一样,引导程序从哪一部分映射地址启动。
BootLoader在stm32可分成两类:一类是原厂自带的boot,也叫做自举程序,一般用作ISP串口下载程序;另一类属于用户自定义的,用于制作IAP,跳转不同flash区域的代码。
3.1 原厂BootLoader
原厂BootLoader在0x1fff 0000 - 0x1fff 77ff的stm32内部存储器内,大小为30K。通过boot引脚设置 boot0= 1;boot1= 0即可进入。
STM32F407支持串口、can、USB进行自举升级程序。具体升级协议可自行参考stm32F407中文参考手册。原厂bootloader与flash的组合构成一种基本的升级模式,但会覆盖源程序。
3.2 自定义BootLoader
此部分位于用户操作的flash区域,定义为执行用户逻辑程序APP前的一段引导。
这部分可以引导具体执行从flash中哪个地址开始的程序。但基本流程是烧写两短代码BootLoader段和APP段。
跳转代码很简单,执行一个函数即可,如下:
iapfun jump2app; //定义函数指针
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法.
{
jump2app=(iapfun)*(vu32*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址)
MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app(); //跳转到APP.
}
}
总的来说是两个操作,一个是将复位函数地址赋值给函数指针,设置堆栈栈顶的地址,第二个是执行函数指针指向的复位函数的地址。
flash源码文件的正确执行就是要正确设置PC和堆栈指针。PC指针指向当前运行的函数,如复位函数;主堆栈指针MSP与函数的嵌套有关,没有这个函数之间的嵌套跳转无法实现。
具体使用可以这样,根据不同标志位跳转不同flash区域的代码,起到了一个引导加载不同程序的作用。
switch (Flag_Table.ull_updaterun_flag)
{
case 0x00000011://跳转APP1
{
if(((*(vu32*)(STM32_FLASH_APP1_BASE+4))&0xFF000000)==0x08000000)//判断是否为Flash区域
{
if(Flag_Table.ull_vetofflen!=0x20000)
{
Flag_Table.ull_vetofflen =0x20000;//写入APP1的向量表偏移长度
WriteFlagTable(&Flag_Table);//将标志位的表写入
}
iap_load_app(STM32_FLASH_APP1_BASE);//程序跳转
}
}
break;
case 0x00000022://跳转APP2
{
if(((*(vu32*)(STM32_FLASH_APP2_BASE+4))&0xFF000000)==0x08000000)//判断是否为Flash区域
{
if(Flag_Table.ull_vetofflen!=0x40000)
{
Flag_Table.ull_vetofflen =0x40000;//写入APP1的向量表偏移长度
WriteFlagTable(&Flag_Table);//将标志位的表写入
}
iap_load_app(STM32_FLASH_APP2_BASE);//程序跳转
}
}
break;
default://默认在当前BootLoader中
break;
}
四、iap升级
单片机程序的烧录除了使用烧写器下载(ICP)和原厂BootLoader通过串口等外设烧录(ISP)外便是在源程序正常运行的过程中进行修改内部FLASH进行升级操作(IAP),如果对接IAP的外设是无线网模块则可实现远程空中升级(OTA)。
IAP升级意味着需要在程序运行过程中进行升级跳转,无需操作boot引脚,实现更加自由灵活的升级方式。
4.1 基本原理
IAP升级的基本原理就是流程图的后半段,通过BootLoader确定是否升级,升级的话去读写flash将代码写进入,不升级就跳转运行正常逻辑代码。
4.2 升级模式
升级模式大致分为以下几种更多的均在此基础上进行的演变。
4.2.1 原厂bootloader+app
此部分常用的名字叫做ISP自举下载。常规项操作,使用不方便需要操作外部boot的电平高低,基本不推荐。
4.2.2 自定义bootloader+app
单区模式,用自定义的BootLoader代替原厂的,操作方便,通过正常逻辑代码中的外设进行指令级别的跳转无需配置boot引脚电平。缺点是无备份功能,在大容量flash单片机中有些空间未被使用,不用来做程序旧版本保存太浪费。
4.2.3 自定义bootloader+app1+app2
双区模式,代码备份功能具备,当然对应的对于小容量产品就显的比较占用空间。双区模式又根据APP区的用法分为两种:一种是APP1只作为运行区域的代码,APP2只做备份区。另外一种是,APP1和APP2均可作为运行区域的代码区,随意切换,APP1运行时APP2区便作为备份区。
第一种比较好实现,缺点就是每次升级都需要做一次代码的对调。第二种比较难以实现,因为flash中中断向量表地址偏移是固定的以及中断向量地址也是固定的,每一次编译后地址不会更改。简单点说就是同一份代码更改向量表偏移地址,生成的flash烧写文件BIN文件不一样,因此每次升级需要在单片机中更改源码文件。
五、iap升级实例
这里以我测试用的双区模式为例,使用一个标志位进行APP1和APP2区域的切换。只讲基本过程,具体的串口升级协议没有添加,大家仅供参考。
5.1 flash分区
首先需要根据flash的情况进行分区确定每个扇区的功能,这次采用双BootLoader的设置,使用BootLoader1跳过sector1的分区,直接引导至BootLoader2,这样做的好处是把标志位放在了一个较小的分区,尽量减少空间浪费,正常Flag标志也存不了几个量。
BootLoader2占空扇区2-4,大小96k。sector5-7存储APP1,sector8-10存储APP2.最后sector11预留使用。
标志位区内容
目前标志位存两个变量,一个是用于跳转的flag当值为0x11时BootLoader2引导跳转至APP1区,当值为0x22时跳转至APP2区,其它值不跳转,,vetOffLen存储向量表偏移值,APP1对应0x20000,APP2对应0x40000。
5.2 升级方案设计
升级方案如下图所示
5.3 BootLoader设计
然后是BootLoader设计,BootLoader1的功能就是引导跳转至BootLoader2区,跳过flag区。
源码如下:
int main(void)
{
while(1)
{
if(((*(vu32*)(0x08008000+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
{
iap_load_app(0x08008000);//执行FLASH APP代码
}
}
}
BootLoader2的方案也很简单根据跳转标志跳转至APP1还是APP2,否则停留在当前BootLoader2中,同时进行将相对应的的中断向量表偏移长度写进flag区中。
源码:
#include <string.h>
#include "stm32_flash.h"
#include "App_Init.h"
int main(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
Sysint();//系统初始化
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);
/* GPIO初始化 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOF, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOF, &GPIO_InitStructure);
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);
/* GPIO初始化 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_Init(GPIOE, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_Init(GPIOE, &GPIO_InitStructure);
GPIO_SetBits(GPIOF, GPIO_Pin_9);
GPIO_ResetBits(GPIOF, GPIO_Pin_10);
while(1)
{
delay_ms(100);
ReadFlagTable(&Flag_Table);//将标志位的表读
if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)==0)
{
Flag_Table.ull_updaterun_flag =0x00000011;//写入APP1的向量表偏移长度
}
else if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)==0)
{
Flag_Table.ull_updaterun_flag =0x00000022;//写入APP1的向量表偏移长度
}
switch (Flag_Table.ull_updaterun_flag)
{
case 0x00000011://跳转APP1
{
if(((*(vu32*)(STM32_FLASH_APP1_BASE+4))&0xFF000000)==0x08000000)//判断是否为Flash区域
{
if(Flag_Table.ull_vetofflen!=0x20000)
{
Flag_Table.ull_vetofflen =0x20000;//写入APP1的向量表偏移长度
WriteFlagTable(&Flag_Table);//将标志位的表写入
}
iap_load_app(STM32_FLASH_APP1_BASE);//程序跳转
}
}
break;
case 0x00000022://跳转APP2
{
if(((*(vu32*)(STM32_FLASH_APP2_BASE+4))&0xFF000000)==0x08000000)//判断是否为Flash区域
{
if(Flag_Table.ull_vetofflen!=0x40000)
{
Flag_Table.ull_vetofflen =0x40000;//写入APP1的向量表偏移长度
WriteFlagTable(&Flag_Table);//将标志位的表写入
}
iap_load_app(STM32_FLASH_APP2_BASE);//程序跳转
}
}
break;
default://默认在当前BootLoader中
{
GPIO_ToggleBits(GPIOF, GPIO_Pin_9);
GPIO_ToggleBits(GPIOF, GPIO_Pin_10);
}
break;
}
}
}
这个增加了一个按键选择功能,帮助测试APP1和APP2区代码的跳转。
5.4 APP中升级处理
讲APP中升级处理前,先将一下APP区代码的结构,如下所示,不同分区中每一份代码都要包含三部分,第一部分中断向量表,表中存储了各个中断函数的地址;第二部分为中断函数;第三部分为主逻辑代码,可理解为main函数。正常中断的执行过程是:发生中断,单片机查询中断向量表,通过中断向量表找到中断函数的地址,执行中断函数后跳回源main函数中。每一APP代码只能使用自己中断向量表以及中断函数,因此有一个向量表偏移长度,让单片机能够通过相对于0X0800 0000的偏移找到对应APP代码的中断函数。
偏移设置原位置在启动文件中对应的系统初始化void SystemInit(void)
中,因此在每一份程序的main函数要要增加一行代码设置向量表偏移,如下所示在flash基地址0x0800 0000 基础上偏移0x20000:
SCB->VTOR = FLASH_BASE | Flag_Table.ull_vetofflen; /* Vector Table Relocation in
示例如下:
int main(void)
{
ReadFlagTable(&Flag_Table);//将标志位的表读
SCB->VTOR = FLASH_BASE | Flag_Table.ull_vetofflen; /* Vector Table Relocation in Internal FLASH */
Sysint();//系统初始化
while(1)
{
}
APP中升级方案如下:
前半部分为正常逻辑流程,后半部分为升级流程,当单片机收到升级指令后,将升级文件写到另外一个APP区,最后根据发送的校验信息完成最终flag标志位的写入然后进行复位操作。
测试代码如下:
单片机中将bin形式的升级文件接收到数组auch_UartRxBuf[]中,接收完成将标志位uch_updateflag置位,主循环中判断成功后,将数组auch_UartRxBuf[]写入另外一个APP对应的flash区域内。写入成功后将运行标志ull_updaterun_flag以及偏移长度ull_vetofflen写入flag分区中。然后关中断重启即可。
int main(void)
{
ReadFlagTable(&Flag_Table);//将标志位的表读
SCB->VTOR = FLASH_BASE | Flag_Table.ull_vetofflen; /* Vector Table Relocation in Internal FLASH */
Sysint();//系统初始化
for(int i=0;i<5;i++)
{
delay_ms(500);
GPIO_ToggleBits(GPIOF, GPIO_Pin_9);
}
while(1)
{
delay_ms(1000);
GPIO_ToggleBits(GPIOF, GPIO_Pin_9);
if(uch_updateflag==1)
{
uch_updateflag=0;
if(Flag_Table.ull_updaterun_flag==0x00000011)
{
ull_vetselect = 0x40000;//更换BIN文件中的偏移
if(WriteAppBinToFlash2(STM32_FLASH_APP2_BASE,auch_UartRxBuf,uch_updataLen))
{
Flag_Table.ull_updaterun_flag=0x00000022;
Flag_Table.ull_vetofflen = 0x40000;
WriteFlagTable(&Flag_Table);//将标志位的表写入
INTX_DISABLE();//close interupt
NVIC_SystemReset();//重启
}
}
else if(Flag_Table.ull_updaterun_flag==0x00000022)
{
ull_vetselect = 0x20000;//更换BIN文件中的偏移
if(WriteAppBinToFlash2(STM32_FLASH_APP1_BASE,auch_UartRxBuf,uch_updataLen))
{
Flag_Table.ull_updaterun_flag=0x00000011;
Flag_Table.ull_vetofflen = 0x20000;
WriteFlagTable(&Flag_Table);//将标志位的表写入
INTX_DISABLE();//close interupt
NVIC_SystemReset();//重启
}
}
}
}
因为不同烧写地址偏移对应的bin文件不同(最大不同是向量表地址都是固定的),所以在升级文件写入flash时要更改源码文件。
更改程序烧写首地址如图所示。
相同源码更改烧写地址后bin文件的不同。
通过对比发现固定不同的是偏移,图中左边偏移为0x40000右边为0x20000,如果想要将不同的bin文件从APP1移动到APP2所在的flash区域,只需要将bin文件中的偏移更改掉即可。更改方式如下:
//将升级代码写入临时存储区
u32 WriteAppBinToFlash2(u32 appxaddr,u8 *appbuf,u32 appsize)
{
u32 t;
u32 iapbuf[UpdatePacketMaxLen/4];//缓存
u16 i=0;
u32 temp;
u32 fwaddr=appxaddr;//当前写入的地址
u8 *dfu=appbuf;
memset(&iapbuf,0xff,sizeof(iapbuf));
for(t=0;t<appsize;t+=4)
{
temp=(u32)dfu[3]<<24;
temp|=(u32)dfu[2]<<16;
temp|=(u32)dfu[1]<<8;
temp|=(u32)dfu[0];
dfu+=4;//偏移4个字节
if((((temp)&0xFFFF0000)==(FLASH_BASE | Flag_Table.ull_vetofflen))&&(((temp)&0x0000FFFF)!=0X00000000))
{
temp = ((temp)&0x0000FFFF)|(FLASH_BASE|ull_vetselect);
}
iapbuf[i++]=temp;
if(i==UpdatePacketMaxLen/4)
{
i=0;
if(STMFLASH_Write(fwaddr,iapbuf,UpdatePacketMaxLen/4))
fwaddr+=UpdatePacketMaxLen;//偏移 UpdatePacketMaxLen=N*4
else
return FALSE;
}
}
if(i)
{
if(!STMFLASH_Write(fwaddr,iapbuf,i))//将最后的一些内容字节写进去.
return FALSE;
}
return fwaddr;
}
在读取每个4字节数据时判断偏移量是否为当前APP区域的偏移,等于就改写偏移。
if((((temp)&0xFFFF0000)==(FLASH_BASE | Flag_Table.ull_vetofflen))&&(((temp)&0x0000FFFF)!=0X00000000))
{
temp = ((temp)&0x0000FFFF)|(FLASH_BASE|ull_vetselect);
}
最后补充一下boot表以及bin文件生成的知识点。
bin文件的生成需要设置一下
红色圈中文件需要对应你们工程目录中的.axf文件。
正常情况下BootLoader1、BootLoader2、APP1、APP2四份代码需要更改烧写首地址用keil进行四次烧写。为了方便我们可以在APP1中将BootLoader1、flag、BootLoader2以boot数组表的形式存在。如下
const u32 bootflag[] __attribute__((at(STM32_FLASH_UPDATERUN_FLAG_BASE))) ={
0x00000011,//升级运行标志
0x00008000 //变量表偏移长度
};
const u8 bootloader1[] __attribute__((at(STM32_FLASH_BASE))) ={
0x78,0x06,0x00,0x20,0x81,0x04,0x00,0x08,0x55,0x02,0x00,0x08,0x57,0x02,0x00,0x08,
0x5B,0x02,0x00,0x08,0x5F,0x02,0x00,0x08,0x63,0x02,0x00,0x08,0x00,0x00,0x00,0x00,
。。。
0x00,0x7A,0x03,0x0A,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x02,0x03,0x04,
0x06,0x07,0x08,0x09,0x00,0x00,0x00,0x00
};
这样每次烧写,APP1会烧写在对应的偏移区域,BootLoader1、BootLoader2、flag会烧写在指定地址内。
boot表数组值从bin文件中提取,可以使用UltraEdit进行打开复制提取。
最后进行复制粘贴到keil代码里的数组中(轻松加愉快!)
5.5 测试
为方便测试此次升级直接将bin文件通过单片机串口发送至数组中,发送16进制的 7d 7d 7b 7b当做校验。
使用vofa+进行了串口测试,也可以使用其它的,
测试资源文件可参考下载使用
示例工程代码
六、总结
总体上内容看起来很多其实原理非常简单,基本上就是flash的读写,BootLoader方案设计好基本都能实现。
本文仅从原理及基本实现上进行了讲解,实际使用可做具体优化:BootLoader函数指针数组化,升级串口增加通信协议,使用.s19文件或hex文件代替bin文件进行升级等。
加油!遇到问题迎难而上,每次的积累都是一次进步!