说明:我参考正点原子战舰开发板的例程和实验进行深入思考学习,读者若觉得有哪里描述不全的可以去这里下载资料查阅:stm32f103战舰开发板
上图是keil在工程编译时会提示的信息:“Program Size:Code=xx RO-data=xx RW-data=xx ZI-data=xx”,意义如下:
我们成功编译一个工程,点开生成的 .map文件。
可以查看内存分布详情和程序大小,如下图,程序大小可以用来参考芯片选型。
接下来我们仔细查看 .map文件,先观察内存(RAM)分布。
上图是内存RAM的分布,正如文章开始所诉,MDK-ARM编译器将RAM分成4个区域,分别为data区、bass区、堆区、栈区,每次上电启动的时候,STM32将FLASH中的RW-data复制到RAM中的data区,而bss区则初始化为0。看图便知,data区的内容是从ROM地址区复制过来的。RAM的起始地址为0X20000000,ROM的起始地址为0X08000000,这个可以查阅STM32中文参考手册存储器章节。当然这个起始地址可以修改,在制作Bootloader和应用APP程序时可以通过MDK-RAM中option->target->IROM和option->target->IRAM选项来修改,后面制作Bootloader程序再说。话归正传,FLASH ROM的分布如下,从起始地址先存放中断向量表,后代码、常量、RW-data。
STM32启动流程
我们先来看看 STM32 正常的程序运行流程,如下图所示:
STM32 的内部闪存(FLASH)地址起始于 0x08000000,一般情况下,程序文件就从此地址开始写入。此外 STM32 是基于 Cortex-M3 内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,而这张“中断向量表”的起始地址是 0x08000004,当中断来临,STM32 的内部硬件机制亦会自动将 PC 指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。
在上图中,STM32 在复位后,先从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的 main 函数,如图标号②所示;而我们的 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求(发生重中断) ,此时 STM32 强制将 PC 指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号⑤所示。
接下来我们打开启动文件 startup_stm32f10x_hd.s,如下图,文件中最上面定义了堆和栈的大小,栈为0X00000400(1K字节),堆为0X00000200(512字节),如果需要,你可以把栈改大点。
如下图,启动文件中罗列了中断向量表。注意中断向量表中的地址指的是偏移地址,如果程序从0X08000000存放,则0X08000000中存放的是栈顶地址,0X08000004中存放的是复位中断函数入口地址。如果程序从0X08001000存放,则0X08001000中存放的是栈顶地址,0X08001004中存放的是复位中断函数入口地址。另外启动文件中还申明了中断服务函数名,我们在编写自己的中断服务函数名时,必须跟这个名字一样。
重点!!!,一起来看芯片是如何启动运转起来的。下图是启动文件中的复位中断函数,当你按下复位键的那一刻,程序就从这里开始执行,SystemInit 函数用于设置系统时钟、滴答定时器等等M3内核的东西,不多介绍;随后是跳转到 __main 函数,这个函数是我们自己编写的 main 函数吗?答案不是的!!!在运行用户编写的 main 函数之前,芯片还有很多事要做,但在工程里找不到这个函数的身影。
还是回到那个 .map文件,搜索 __main 就会发现,__main 与 main 之间还有好多函数,这些scatterload大概意思就是将ROM中存放的RW-data数据复制到RAM中的data区,并且初始化RAM中的bass区,为程序运行做准备工作。RW-data(初值不为0的全局变量和静态变量)是尾随代码一起下载到FLASH ROM中的。
前面知道了,我们的编写的代码存放在FLASH ROM中,初值不为0的全局变量和静态变量最开始从ROM中加载到RAM中的data区,初值为0的全局变量和静态变量被安置在RAM中的bss区。不管data区还是bss区,其内容本身就是我们定义的各种各样的全局变量或静态变量,其生存周期为永远,直到芯片掉电关机。我还想一探究竟局部变量是怎样存放的?下面做个实验:
编译工程,打开debug选项。如下图所示,准备调用 LCD_ShowNum() 函数,但先得调用 my_mem_perused() 函数,通过汇编窗口可以看到 my_mem_perused() 函数的入口地址为0X080076C
我们进入到my_mem_perused() 函数,如下图所示。MDK编译器把C语言翻译成汇编,第一步进行了PUSH入栈保护操作,这就是编译器自动操作栈空间。在函数内部,定义了两个局部变量,一个 used ,一个 i ,但编译器用的通用寄存器来保存这两个局部变量,起原因有二:第一、两个局部变量通用寄存器够用;第二、用通用寄存器更快,因为通用寄存器在CM3的内核中,CPU访问起来很方便。CM3 拥有12个通用寄存器,有关汇编指令和CM3寄存器组的知识可以查阅Cortex-M3权威指南。所以这里还体现不出局部变量存放在栈空间当中。
我在my_mem_perused() 函数又多定义了18个局部变量,这下通用寄存器就不够用了吧,看你编译器怎么安排,vu8是告诉编译器忽略优化的字节类型。见下图,现实入栈操作,然后栈顶指针SP自减48H(十进制72),用来保存局部变量。由于我没有对局部变量初始化,所以汇编指令并没有对相应的栈空间赋值,SUB sp,sp,#0x48
这句指令实际上就是在为局部变量开辟栈空间,看上去比较隐晦。后面的汇编操做看图都懂了,当然在最后有释放栈空间和POP出栈操作,太长截屏不下。
于是乎,栈区(stack)确实由编译器自动分配和释放,存放函数的参数、局部变量等,这加深了我对内存分配和STM32怎样运作的印象。
还说说堆区,在电脑上写C,用malloc函数就能申请堆空间,那是因为有操作系统和内存管理的加持。而在STM32的裸奔程序上,我还没有用到过堆空间。正点原子的例程有内存管理实验,那实际上是定义了一个超级大的数组(未初始化),也能实现内存申请和释放,但定义的大数组实际上存在于前面所诉的bss区,并没有使用到堆区。
当加入 IAP 程序之后,程序运行流程如下图所示:
STM32 复位后,还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数,如图标号①所示; 在执行完 IAP 以后 (即将新的 APP 代码写入 STM32的 FLASH,灰底部分。新程序的复位中断向量起始地址为 0X08000004+N+M) ,跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数,如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时 STM32 的 FLASH,在不同位置上,共有两个中断向量表。在 main 函数执行过程中,如果 CPU 得到一个中断请求,PC 指针仍强制跳转到地址0X08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。
通过以上两个过程的分析,我们知道 IAP 程序必须满足两个要求:
1) 新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始;
2) 必须将新程序的中断向量表相应的移动,移动的偏移量为 x;
下面演示设计一个FLASH的APP程序。
1、程序起始地址的设置方法:
打开一个实例工程,点击 Options for Target→Target 选项卡,如下图所示:
默认的条件下, 图中 IROM1 的起始地址 (Start) 一般为 0X08000000, 大小 (Size) 为 0X80000,即从 0X08000000 开始的 512K空间为我们的程序存储 (因为我们的 STM32F103ZET6 的FLASH大小是 512K) 。而图中,我们设置起始地址(Start)为 0X08010000,即偏移量为 0X10000(64K字节) ,因而,留给 APP 用的 FLASH 空间(Size)只有 0X80000-0X10000=0X70000(448K 字节)大小了。设置好 Start 和 Szie,就完成 APP 程序的起始地址设置。这里的 64K 字节, 需要大家根据 Bootloader 程序大小进行选择, 比如我的 Bootloader程序为35K左右,如下图。理论上我们只需要确保APP起始地址在Bootloader之后, 并且偏移量为0X200的倍数即可,想关知识参考: http://www.openedv.com/posts/list/392.htm)。
在系统启动的时候,会首先调用 systemInit 函数初始化时钟系统,同时systemInit 还完成了中断向量表的设置,我们可以打开 systemInit 函数,看看函数体的结尾处有这样一句指令SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
,意思是设置中断向量表的偏移地址。注释写到,偏移地址VECT_TAB_OFFSET
的值需是0X200的倍数,如下图所示。
中断向量表的偏移设置方法:
从 代 码 可 以 理 解 , VTOR 寄 存 器 存 放 的 是 中 断 向 量 表 的 起 始 地 址 。 默 认 的 情 况VECT_TAB_SRAM 是没有定义,所以执行 SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
对于 FLASH APP,我们设置为 FLASH_BASE+偏移量 0x10000,所以我们可以在 FLASH APP 的main 函数最开头处添加如下代码实现中断向量表的起始地址的重设:
SCB->VTOR = FLASH_BASE | 0x10000;
软件设计
我们需要 2个程序:1,Bootloader;2,FLASH APP;其中,Bootloader (起始地址为0X08000000),大小为34.83kB ;FLASH APP 程序(起始地址为0X08010000,偏移地址64kB,前面64kB空间足够存放Bootloader程序) 。
iap.c, 代码如下:
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "stmflash.h"
#include "iap.h"
//
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK战舰STM32开发板
//IAP 代码
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//修改日期:2012/9/24
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 广州市星翼电子科技有限公司 2009-2019
//All rights reserved
//
typedef void (*iapfun)(void); //取别名,iapfun 就代表定义函数指针的关键字
iapfun jump2app; //定义一个函数指针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)&0xFFFF0000)==0x20000000)//检查栈顶地址是否合法。for STM32F103ZET6 RAM地址范围0x20000000-0x2000ffff
//(vu32*)appxaddr是将地址强制转换为32位类型的指针,
//*(vu32*)appxaddr是对指针进行解引用,就是取值,实际上取到的值就是用户代码的栈顶地址,
//前面内存分布描述过,用户代码第一字(4字节)存放的就是栈顶地址,
//栈顶地址是由编译器赋值的,它取决于你的代码占用RAM的大小,即data区大小+bss区大小+堆大小+栈大小
{
jump2app=(iapfun)*(vu32*)(appxaddr+4);//用户代码区第二个字(四字节)存放的复位中断向量,它指向复位中断函数入口地址
//(vu32*)(appxaddr+4)是将地址强制转换为32位类型的指针,
//*(vu32*)(appxaddr+4)是对指针进行解引用,就是取值,实际上取到的值就是中断函数入口地址,
//(iapfun)*(vu32*)(appxaddr+4)是将取到的中断函数入口地址强制转换为一个(无入口参数,无返回类型)函数指针
MSR_MSP(*(vu32*)appxaddr);//初始化APP堆栈指针。*(vu32*)appxaddr为栈顶指针
jump2app();//函数指针调用方法:函数指针名+(),即跳转到FLASH APP(新的用户代码)
}
}
跳转代码看的有点蒙的可以结合下面这张图来理解
可能你还对这句代码有所不解MSR_MSP(*(vu32*)appxaddr);
下面是这个函数的定义,它设计到的是汇编指令了,如下图。为什么要重新设置栈顶指针?仔细想想,你的Bootloader是烧录在FLASH里开头的一段代码(起始地址为0x08000000),开机启动后进入复位中断函数,先调用SystemInit()
函数,再调用__main
,在__main
函数里会对RAM进行排兵布阵,比如从ROM中加载RW-data到RAM的data区,还有初始化bss空间,并赋值成0。显然Bootloader占用RAM的大小取决于你的Bootloader程序里定义了多少全局变量和静态变量,以及堆空间和栈空间定义多大。
假想现在我要跳转至FLASH APP(起始地址为0x08010000)运行我的APP程序,首先是进入复位中断函数,操作流程和上面一样,在APP的__main
函数里肯定会重新排兵布阵我的RAM空间,如果不把栈顶地址修改为APP的栈顶地址,那不就乱套了吗,所以说这里要修改栈顶地址。怎样理解下面这两行汇编代码,往下看。
//设置栈顶地址
//addr:栈顶地址
__asm void MSR_MSP(u32 addr)
{
MSR MSP, r0 //set Main Stack value //写入R0值到主堆栈中
BX r14
}
预备知识:MSR指令、MSP主堆栈指针、R14连接寄存器、BX指令
看完预备知识就有点明白了,MSR MSP, r0
是将R0寄存的内容写入主堆栈寄存器MSP中,BX r14
是返回调用 MSR_MSP(u32 addr)
函数的地方。
那么不禁好奇R0中的内容是什么?addr
这个形参好像没有使用到啊,难道说R0寄存器默认存放了形参?我们来验证一下吧。若函数只有一个形参,MDK编译器通常将通用寄存器R0来存放形参,例子看下面:delay_ms(10);
这句C代码被翻译成了两条汇编指令,R0来存放形参10 ,真是拍案叫绝!恍然大悟!
再检验一下!看下面
真是妙啊!到这里相信你对STM32的启动流程非常明了。
实际上,如果你的Bootloader占用RAM空间,比APP程序所需的RAM空间大,即使你不修改栈顶指针MSP,那APP的程序也能正常运行,但这样显得不规范。就如下面这样
bootloader原先占用RAM的空间比APP程序所需的RAM空间大,我们注释掉修改栈顶指针这一条代码,测试APP程序依然可以正常运行,反之则崩溃。