正文
不得不提的启动方式
STM32支持三种启动方式
1. FLASH启动
2. SRAM启动
3. 系统存储器启动这三种启动顺序决定了上电后第一条指令的位置。如果你选择FLASH启动,则上电复位后PC指针指向第一条指令位置——0x08000000,如果你选择SRAM启动,则上电复位后PC指针指向第一条指令位置——0X20000000,若你选择系统存储器启动,则上电复位后PC指针指向第一条指令位置——0X1FFF0000。
为什么会这样呢?接下来我们分析一下STM32F4的存储区地址结构。
上图摘自《Cortex-M3与M4权威指南》。从这里我们可以看出M4内核支持4GB的存储空间,从0X00000000-0X1FFFFFFF共512MB的空间是Code区,0x20000000-0x3FFFFFFF共512MB的空间是SRAM,其他的我们暂且不分析。我们再来看看《STM32F4xx Reference manual 》中 关于F4系列的物理内存映射
从上面的物理内存映射图我们可以看出:0x08000000-0x080FFFFF: FLASH
0x1FFF0000-0x1FFF77FF: System memory
0x20000000-0x2001FFFF: SRAM其中FLASH大小为1MB, SRAM大小为12KB, System memory大小为30KB。在这里解释三个问题:
- 《Cortex-M3&M4权威指南》中讲到“系统上电复位后,从0x00000000处加载第一条指令…..”为什么上面提到的三种启动方式都是从非0x00000000加载第一条的呢?-这就是“地址映射”的作用了。我们通过boot0&1引脚的配置,可以选择不同的启动方式,同时将flash或sram的基地址映射到启动空间0x00000000处,这样,加载的第一条指令位置便是我们相应的启动方式对应的存储设备的地址了。
- 三种启动方式有何区别?显然,第一点不同就是地址空间不同,导致了三种启动方式启动时代码加载空间随之改变;此外System memory是个真正的ROM,里面固化了厂家出厂时的BootLoader代码,不可重写。FLASH和SRAM则可自己重写程序完成启动,但是由于芯片特性原因,两者的用途也不一样。FLASH就是我们常说的闪存,具有”掉电不失性”,烧写代码后,代码不会因为掉电而丢失,下次上电启动时仍然可以正常启动,为常见的启动方式,但是缺点是运行速度较慢;SRAM是“静态随机存储器”,具有“掉电易失性”,属于RAM的一种,那么显然我们不可能将代码长保存到SRAM中,因为下次上电后仍旧没有代码。这种启动方式常用于程序调试,即上电后下载代码,运行,进行调试,运行速度比FLASH快。
- 为什么FLASH不是RAM也可以运行程序?FLASH分为两种,一种是Nor FLASH, 一种是Nand FLASH。这两者区别就不一一赘述,网上资料很多,在这里我们只需要明确:Nor FLASH 支持片内执行,Nand FLASH不支持片内执行,而我们使用的STM32F4系列芯片内置的1MB的FLASH属于Nor FLASH,所以便有从FLASH启动这一说法。
解释完这三个问题,我们便可以谈谈IAP了
关于IAP
IAP全称为 In Application Programing,即“应用内编程”,按照我个人的理解就是:我们通常下载到开发板上的程序其实是两部分——BOOT+APP, BOOT代码负责必要的堆栈,中断向量表等初始化,而APP才是我们真正需要的功能代码,而STM32已经有.s启动文件支持,所以我们无需关注BOOT部分,只需完成相应的功能代码即可,通常无论你代码内有多少功能,但你下载的文件只有一个,即BOOT+APP,且只能通过特定的方式写入到FLASH或者SRAM内运行。而应用内编程,则打破了这个规则,原则上只要存储空间无限大,我们就可以使用IAP可以下载到FLASH内部无限多个应用程序。下面我简单的画一下两者的运行机理:
应该不难看出,其实所谓的IAP,就是先写一个APP作为伪BOOT,在系统初始化完成之后,引导系统加载真正的APP程序,使之运行,完成真正的APP程序的功能。这样我们就可以下载多个APP在不同空间内,只要让引导程序做出相应的跳转工作,便可以实现运行对应APP功能。讲到这里,我们先来思考下面四个问题:
- 伪BOOT完成了哪些工作?
- APP程序是如何下载到开发板上去的,对空间有特殊要求吗?
- 伪BOOT到APP之间的跳转如何完成?
- APP程序需要具有哪些功能?
下来我们就来分析分析刚才已经提到过,伪BOOT其实就是我们平时写的APP程序,为什么我要叫他伪BOOT呢?因为他并不是一个真正的BOOT程序,他也是一个BOOT+APP程序,只不过这个程序在IAP编程中完成的工作很像一个BOOT程序,所以我起了个名字叫做伪BOOT。
我们先来看看一般BOOT程序完成的工作:单片机上电复位之后,程序指针PC指向0x00000000(映射到FLASH的0x08000000或者SRAM的0x20000000处),这里存放的是
用户堆栈栈顶地址,获取到栈顶指针后,程序指针向后偏移4个字节,指向0x00000004(映射到FLASH的0X80000004或者SRAM的0X20000004),这里存放的是
系统中断复位的指针,然后程序跳转到Reset_Handler执行SystemInit(系统复位)工作,在系统复位中系统关闭了中断,初始化系统时钟等等。然后跳转到 __main进行堆栈初始化,最后跳转到.C文件中的main函数中执行相应的功能。【这里只做概述,过两天有时间了专门写一篇关于STM32启动流程分析的文章^-^】。我们可以看到,系统初始化部分是必须的,关键我们要修改的就是最后这一步跳转,如和让程序跳转跳转到我们编写的APP代码中而不是跳到伪BOOT中的APP程序呢?其实这也是可以的,我们只需要将我们编写的用户APP程序中启动代码删掉,其余代码将伪BOOT中的用户程序代码覆盖掉就可以,但这样好像失去了IAP的意义。IAP本来就是期望用户“不拆机在线升级”。这样搞岂不是更复杂了?所以,更常见的做法是:伪BOOT完成系统初始化后,进入APP程序,此时利用APP实现两个功能:
- 检测是否需要升级某个用户APP
- 跳转到指定APP运行空间运行
我们可以依靠UART,USB,IIC等一切板上通信外设完成第一个功能。如果需要升级或更新某个APP,则通过某种通信方式将我们编译好的bin文件发送到单片机上,单片机伪BOOT中的程序接收这些数据并根据需要使用FLASH写操作将这些代码写到指定的FLASH空间(这里我们只讨论FLASH)。当需要执行某个用户APP程序时,可以在伪BOOT的主程序中修改程序指针PC,使系统跳转到指定用户APP程序所在的FLASH空间,然后运行,就可以完成跳转到相应的APP程序的功能。道理很简单。
我们编写伪BOOT程序使之有通信,FLASH读写,地址跳转功能就可,在启动启动后,因为程序还运行在伪BOOT里,我们依靠BOOT程序中的功能,检测是否需要更新用户APP,需要更新则进行数据通信,并将数据通过FLASH操作写入FLASH,然后执行程序跳转指令,使系统跳转到对应的APP程序空间,就可以执行对应的程序功能。这里有几个问题需要注意一下:
- 我们并没有裁减掉用户APPx中的boot代码,所以某种程度来说,烧进FLASH中的至少是两套或两套以上的【boot+app】程序,只不过我们的伪BOOT是为了完成引导,其余的APP都是用户功能程序罢了。
- 从问题1来看,既然每一个程序都是完整的,如果我们的伪BOOT通过串口接收到我们编译好的完成工程的bin文件,并写入到FLASH中0x08004000之后,那么0x08004000这个地址里面存放的是什么东西?用户APPx main函数地址?错!应该是用户APPx程序的BOOT代码的第一行,也就是程序代码的用户堆栈栈顶地址,那0x08004004理所当然就是APPx程序的BOOT代码里的系统中断复位指针。
- 从问题1和2我们可以推断出,由伪BOOT引导后跳转到用户APP程序,系统又进行了一次复位操作,而且和伪BOOT是一模一样的复位操作,但此时有个明显的问题就是,我们的中断向量表多套,每套中断向量表都对应的是自己APP的中断入口,但是用户程序执行时遇到中断,跳转会跳到哪里去呢?如果我们在用户APP中不加以修改,中断向量表的偏移量仍旧是以0x08000000为基地址计算得到的,所以任何一个用户APP都会跳转到伪BOOT中的中断向量,造成不可预料错误。但是幸好STM32提供了中断向量偏移寄存器,我们可以通过修改这个寄存器的值,将每个APP程序的中断向量对应到自己所在空间的正确地址。完成中断操作。
- FLASH空间是在编译链接后就确定的,所以在你下载程序之前,你的程序地址已经确定,这样我们就需要在keil的配置文件中更改ROM的地址,使APP程序可以烧写到一个正确的FLASH空间,即不会破坏其他程序空间,又能被正确引导启动,一般来讲,伪BOOT文件大小越小越好,这样可以为APP程序节留下很多空间。而我个人建议将伪BOOT文件单独放在第一扇区,即0x08000000-0x08003FFF【16KB】,因为程序中在进行FLASH写操作之前可能需要擦除,但往往擦除会擦除掉一整个扇区,如果对内存理解不到位或者程序编写有误,也会将伪BOOT擦除掉,使引导系统崩溃。
- 我们的传输的APP程序一定经过keil编译过的完整的工程形成的二进制——bin文件。因为我们是将文件数据直接发送到开发板上,不经过特殊的解析,直接写入到FLASH中,所以这就要求我们发送的文件一定是内存中最终存储的二进制文件,而不是hex和axf文件。hex是十六进制文件,我们打开可以看到一串ASCII码,对应着地址信息和数据,而axf则包含着调试器调试信息,是通过JLINK或者STLINK解析后下载到开发板上的。至于bin文件的生成,我们可以使用keil自带的fromelf.exe工具。
而APP程序的功能丝毫没有限制,和大家平时写的一模一样。这样,就完成了整个IAP编程。
简单总结一哈
IAP用途很大,比如无线下载,快速升级,远程系统维护等等,这些都是IAP的具体应用,当然还有更多好玩的等你去发现。其实我学习IAP主要是想玩玩无线下载,因为环境问题整天抱着电脑跑太累了,如果再改一改代码,也可以实现BOOT-APPx之间的任意跳转,可以让下载和引导随心所欲。不过今天主要还是分享一下IAP编程的思想和一些细节问题,关于具体的编码过程,后面有时间我会在下一篇文章里分享。
正文
上篇基本将IAP工作的机理和程序组成以及运行路程分析过了,所以我们只看看关键模块的编码。
首先分析IAP,关键模块有三部分:通讯,FLASH操作,引导跳转。
一、通讯
我们先来谈谈通讯问题。可以将,无论什么通讯,都可以完成代码的传输,USART也好,USB也好,CAN也好,等等,只要是通讯外设,你都可以用来传输外设,但是考虑到实用性,也许USART是简单也是最最常用的。我们以就以USART为例来讲通讯。
文件的传输一定要稳定,传输过程中不可以丢数据,否则传输的文件就有问题,导致最后的APP程序存在问题。所以我们使用USART时需要选择合适波特率,
要尽量大保证传输速率,但又不能太大导致丢帧。然后开启中断在中断将接收到的数据存放到一个数组里即可,随后处理即可。编码相信大家都会,就不在这里复述了。
二、FLASH操作
我们需要将接受到的APP程序写到合适的FLASH地址,以便后面引导启动。
首先应该明确STM32F4的FLASH地址为0x08000000-0x080FFFFF,若果越过这个空间写肯定是有问题的。下面我们看看FLASH写入的步骤。1.校验写入地址有效性
if(WriteAddr < FLASH_BASEADDR || WriteAddr%4)
return ;
2.FLASH解锁,缓冲区除能
FLASH_Unlock();
FLASH_DataCacheCmd(DISABLE);
3.判断是否要擦除缓冲区
if(FLASH_ReadWord(AddStart) != 0XFFFFFFFF)
{
FlashStatus = FLASH_EraseSector(FLASH_GetFlashSector(AddStart), VoltageRange_3);
if(FlashStatus != FLASH_COMPLETE)
break;
}
4.重写【我们通过串口或其他外设接收到的数据为八位,而FLASH操作要求按字写入,所以这块一定要注意数据的转化】
while(WriteAddr < AddEnd)
{
if(FLASH_ProgramWord(WriteAddr, *pBuffer)!= FLASH_COMPLETE)
break;
WriteAddr += 4;
pBuffer++;
}
4.失能缓存,FLASH上锁,完成写入操作
FLASH_DataCacheCmd(ENABLE);
FLASH_Lock();
到这里我们的FLASH写入模块算是完成了,主要抓住FLASH操作的核心便可以完成FLASH的写入操作。
三、引导跳转
引导跳转的核心步骤有两步:1.地址空间有效性校验
if(( LoadAddr)&0x2FFE0000) == 0x20000000)
- 我们来分析一下这句代码。LoadAddr为我们伪BOOT接收到的代码数据的第一个字,我们会想一下上篇文章提到的,正常下载一个程序到FLASH后存储在0x08000000的是一个什么数据?用户堆栈地址!对!就是用户堆栈地址。我们的LoadAddr也是接收到用户程序代码的第一个字,本来也是存放在0x08000000地址空间内的,只不过现在被我们接收进行处理了,所以我们的LoadAddr就是接收到的程序的用户地址空间那么这句话很显然了,取LoadAddr空间内存放的数据,这是什么?这就是用户地址空间栈顶指针 然后&0x2FFE0000和0x20000000判等。我们先来看看为什么要用栈顶指针和0x2FFE0000。上篇文章我们提到过STM32F4的SRAM空间范围为0x20000000-0X2001FFFF共128KB,当用栈顶指针&0x2FFE0000后,我们忽略了低十六位,我们只关心栈顶指针高十六位是否在0x2000-0x2002之间,也就是SRAM空间。这就是栈顶指针的校验。换句话说,让程序的栈顶指针在SRAM时,我们便可以认为这是有效的栈顶指针。
2.保存
复位中断向量typedef void(*LoadAddrFunVar)(void);
//...
LoadAddrFunVar StartLoad;
//...
StartLoad = (LoadAddrFunVar)*(vu32*)(LoadAddr+4); //load the addr
- 我们知道栈顶指针往后偏移四个字节便是复位中断向量的位置,这行代码的意思就是定义一个函数指针,并将这个函数指针指向复位中断向量,我们通过调用函数的调用便可以实现复位中断的执行,进而进行程序复位。
3.将用户堆栈指针指向用户堆栈栈顶
__asm void MSR_MSP(u32 addr)
MSR_MSPLoadAddr);
- 这里调用了ARM的汇编指令,通过MSP指令将刚才校验过的堆栈栈顶地址赋值给用户堆栈寄存器MSP,然后返回。这一步很关键,相当于对新APP的用户堆栈重新构建,保证堆栈的正确性。
4.引导跳转
StartLoad();
- 即调用刚才的函数,使程序跳转到复位中断,执行系统复位操作。如此一来,系统便会复位重新启动,只不过此时不再加载0X08000000处的代码,而是加载你刚才制定地址的代码,此刻我们就完成了IAP的引导工作。
那么,我们的APP程序该怎么编写呢?
其实我们的APP程序和我们平时写的程序没有任何区别,唯独要增加两个工作:
- 设置对应的ROM空间
- 中断向量表重定向
我们首先需要通过KEIL->Options->Target->IROM1设置我们的APP程序需要写入的FLASH空间
一般来讲,这个空间可以设置成为我们之前的引导程序所占空间之外其余的任何FLASH空间,但是我们设置的时候需要考虑:
- 避免烧写到引导程序所在扇区
- 地址必须为0X200倍数【76个中断向量所占空间的扩展到2的整数次幂,否则覆盖某个APP中断向量代码空间】
- APP大小
比如图中我将APP写入到了扇区1【引导后面的扇区】,Size设置成剩余的FLASH大小。这是没有问题的。还有很重要的一步便是中断向量表重定向,原因在上篇文章里已经讲过,这里我们看看具体怎么操作。
STM32提供了中断向量表的偏移设置寄存器SCB->VTOR,我们可以在程序开始的时候调用它完成重定向,也可以调用函数NVIC_SetVectorTable()完成中断向量表的重定向。
SCB->VTOR = (FLASH_BASE |0x4000);
NVIC_SetVectorTable(FLASH_BASE,0x4000);
这里的0x4000就是我代码写入FLASH地址0x08004000相对于FLASH_BASE[0x08000000]的偏移量。那么我们的APP程序写好了,如何传输呢?
这里要强调一下我们需要使用keil自带的fromelf.exe,将编译连接后生成的axf文件转化为可以直接写入FLASH的二进制数据文件——.bin文件。只有将程序转化为存储器中直接存储的二进制文件,才能不经过任何处理写入到存储器后运行,而hex和axf文件则不满足要求。如何使用fromelf.exe呢?我们点击option->User->After Build/Rebuild,勾选RUN#1,后面输入
//根据实际情况修改keil安装目录和链接输出文件夹位置
keil安装目录KEILARMARMCCinfromelf.exe --bin -o ..OBJFileName.bin ..OBJFileName.axf
这样我们在build或者rebuild后会执行这段代码,使用fromelf工具将axf转化为bin文件。然后通过串口助手将bin文件发送到开发板就ok啦。这样,我们整个IAP从boot到APP都基完成啦~
总结一哈
以上就是IAP的完整的学习笔记啦~笔记1主要分享了IAP的工作和引导机理,笔记2主要进行了关键代码解析。写完这两篇,个人对STM32的启动,程序加载,存储器存储有了更深的了解,相信大家也对学到了不少知识。以上内容纯属个人见解,如果大家发现有什么不对的地方,欢迎指正哈~最后,贴上自己的IAP程序,依靠USART控制,亲测可行。
http://download.csdn.net/detail/zat907943815/9728130