写在前面的话:本篇文档是本人亲自测试验证后所得的总结,其中可能有部分内容描述得不够准确,希望各位读者慎重甄别,但本人保证这篇文档绝对是本人对CORTEX-M0 IAP内容的所有真实的总结与描述。IAP功能其实并不复杂,复杂的其实是网上那些好心网友的各种不同的实现方法容易把我们绕晕,其实,基本功能我早就可以实现,只不过发现了一个比较特殊的问题,且网上目前没有找到有网友解答,故在自己解决后编写了这篇文档,希望能帮助那些和我掉进同一个坑里的朋友,并且加深自己的印象,保留一份存稿,方便日后查询。文档篇幅比较长,各位有耐心就读下去,没耐心就直接下载代码验证,但是代码下载到单片机中前一定先看看第三部分内容。最后再奉劝各位读者两句:“纸上得来终觉浅,绝知此事要躬行”;“要独立思考问题,不要人云亦云”。
/***************************************************************************************************************************************************************************************************************/
源码连接:https://download.csdn.net/download/qq_33784286/13507135
一:什么是IAP?
什么是IAP?IAP的全称是In Application Programming,意思是在应用里编程。我们开发单片机的时候需要将程序烧录进单片机中,往往是借助像jlink这样的烧录工具进行烧录,直接将程序固化到flash中,这种方法称之为ISP编程。而IAP是在ISP烧录了一段基础引导程序(bootloader)后可以借助其他的通信外设例如UART,IIC,SPI,SDCARD等对程序进行在线升级或者在线更新的一种烧录方式。所以我理解的IAP并不是编程过程,而是烧录程序的方法。
二、CORTEX-M0的IAP实现过程及原理
与其说是CORTEX-M0不如说是目前市面上大多数(不敢肯定的说是所有的)带有IAP功能的芯片的实现过程及原理。
首先我们需要简单的了解一下M0的存储单元结构问题,我们知道存储器分为ROM和RAM,其中ROM一般用来存储程序这类不用经常修改的数据,RAM一般用来存储程序运行过程中的变量数据等,ROM称之为非易失存储器,掉电可保存数据,而RAM称之为易失性存储器,掉电不保存数据。那么在M0中我们的flash就充当了ROM的角色,所以我们的程序内容都是保存在flash中的,了解这一点很重要。
接着我们需要了解一般情况下M0程序的存储方式,这里篇幅有限我只告诉您一般情况下M0是从0x0800 0000这个位置开始存储程序的,具体原因您可以去查一下M0的芯片手册。换言之,一般情况下0x0800 0000这个地址就是我们的程序入口地址,我们只要能找到这个地址就能去运行我们的程序,这是芯片的默认入口地址,所以一般情况下我们不需要去操作他。
我们还知道我们可以获取程序中函数的入口地址,通过访问函数的入口地址去访问函数,也就是说我们只要知道函数的入口地址是不是就能去调用任意的函数呢?好的,现在我们有了这些基础知识储备就可以更好的理解我们的IAP的内容了。
总结一下:1、M0的程序是保存在flash中的
2、M0的程序默认入口地址为0x0800 0000
3、我们可以通过访问函数的入口地址去调用函数
那么接下来我们可以思考,如果我们在M0的flash存储区中划出两部分空间分别用来存储两段不同的完整的工程代码,在两段工程中分别通过访问彼此的main()函数的入口地址是不是就能实现两段代码彼此跳转的过程呢?
怎么理解上面一段话呢,例如现在我们有两段独立的工程代码,分别为APP1和APP2,那么APP1有自己的main函数我们记为APP1_main(),APP2的main函数我们记为APP2_main(),注意在各自的工程中函数名还是“main”。那么如果在APP1的工程中我们知道了APP2_main()的入口地址(0x0801 0000)那我们是不是可以在APP1中通过访问该入口地址来进入切换到APP2的工程中呢?毫无疑问当然是这样的,那么在APP2中同样也可以通过访问APP1_main()的入口地址来切换到APP1的工程中。这段话有点绕,但是其实并不难理解,各位读者多看几遍,那笔在纸上面整理一下应该就没问题了。
While(等待各位读者理解上面那段话的意思);
/***************************************************************************************************************************************************************************************************************/三、如果下载两个工程代码和获取main函数的入口地址
现在我们弄清楚了不同工程的切换过程,但是我相信各位读者现在对上面的描述有几个问题还有疑问。
疑问一:两段工程代码如何放到单片机中?
疑问二:如何知道每段工程的main函数的起始地址?
接下来我们就来解决这两个问题
网上好多网友都是介绍先下载一段程序然后通过串口通信的方式将另一段程序烧写到单片机中,这里我认为对刚接触IAP的朋友来说是及其不友好的,首先我们还得找到跟他们教程描述一样的上位机,然后还得做串口通信,下载协议等等乱七八糟的东西,让原本举步维艰的我们变得更是雪上加霜,所以为什么我们不能简单一点呢,先把功能验证完再说,我在网上找了好久也没有找到不借助第三方上位机的教程(果然原创还是少,大部分人都只会炒剩饭)。
这里我介绍的教程就不需要第三方上位机。首先我们烧录代码应该都会烧写,我这里以jlink的SWD模式为例,当我们用MDK的在线下载功能时,可用通过设置options for target
来设置代码的下载区间,如下图所示:
我这里设置的是该工程的代码的起始存储地址是0X0800 0000 大小为0X10000,上面我们见过0X0800 0000这是一个特殊地址,只是芯片的默认程序入口地址,所以这段工程代码就是我们的芯片上电默认启动代码,就像我们电脑的uboot程序一下,所以我们一般把我们默认启动的代码的起始地址设置为0X0800 0000,
然后我们把代码烧进后这段工程代码的存储区域为0X0800 0000 ——0X0800 FFFF,那么我们的代码大小千万不能超过这段区域。也就是说这段工程的main函数的入口地址可以暂且认为是0X0800 0000;
设置好存储区域后我们还要把它下载到芯片中,我们还要设置一下下载参数如下图所示:
注意flash的擦除烧写区域要更options target中一样。因为是第一段代码所以我们可以选择Erase Full C(擦除整个区域)。
现在我们烧写好了一段工程代码,那么我们接着烧写第二段工程代码,一样的我们需要在哎options中设置存储区域,设置的存储区域中还有一些问题需要注意,这里我们只先交代一下设置的区域最好不要重叠。这里我们设置如下图所示:
我们这里IROM1的区域设置为0x0801 0000大小为0x10000,也就是存储区域为0x0801 0000——0x0801 FFFF,注意到我这里还设置了一下IRAM1也就是说明我们这段工程的RAM区为0x2000 00C0大小为0x4000,为什么我们不像上面一样从0X2000 0000,这里与我们的中断向量还有一定的关系,这里我先卖个关子,下面再介绍,接下来就是要修改我们的下载配置了:
这里我们就不能擦除所有的区域了,不然就不前面那段工程的代码也都擦除了。
现在我们两段代码都烧写进了我们的单片机中了,也能知道两段工程的main函数的入口地址了。这就解决了我们上述的两个问题了。
四、实现跳转的代码
其实如果做到这一步,各位可以做两个简单的没有涉及到外设中断的工程尝试一下,但是注意跳转的地址为我们上面设置的地址的基础上加4,也就是说如果你要跳转到第二段工程,应该跳转到的地址是0x0801 0000+4,也就是0x0801 0004,为什么要这样呢?这就涉及到单片机的启动过程了,这里我们不做介绍,有兴趣的朋友可以查阅一下相关资料。注意我上面说的是没有外设中断的工程可以尝试一下。
但是我们一般编写的工程都是需要有外设中断的,难道我介绍的方法不能用外设中断吗?当然不是,接下来我就来介绍一下具体的实现过程。
首先我们需要了解一下单片机程序的执行过程,一般的我们单片机是按照主程序去执行的,一旦发生中断,会立即去执行中断服务程序,执行完中断服务程序就会返回中断前的断点继续执行主程序,这其中涉及到保护现场,恢复现场等硬件操作,讲起来很复杂,这里不做介绍。但是我们知道的是中断服务函数也是函数,也是有函数入口地址,那么现在我们想想两段工程的中断函数入口地址是不是一样的呢?很明显不是一个函数入口地址,所以这里我们需要对第二个工程的中断函数的入口地址做点小动作,因为第一个工程的所有配置都是默认的所以不需要配置。
那么我现在怎么来做这个小动作呢?这里我们还是简单了解一下M0的程序处理机制吧,我们知道单片机可以通过boot硬脚来配置单片机的启动方式,是从flash中还是SRAM中启动,这里并不是说真的把程序放在SRAM中,而是说将程序一些中断映射放在SRAM中。从flash中启动的时候单片机会默认从0X0800 0000开始取地址来执行程序,其中对于M0来说前面的几十个字(不同型号会不同,我的是F030系列有48个中断源,就是48个字)为各个中断源的地址映射,而从SRAM中启动的时候会从0x2000 0000开始取中断源地址映射。
所以我们跳转到了第二个工程中,如果我们没有修改中断向量的映射,那么一旦发生中断,单片机就会从0x0800 0000那边找中断向量的地址,这明显不是我们想看到的,因为这是第一个工程的内容,我们并不想两个工程相互干扰。
那么我们现在的解决方案就是将第二个工程的中断向量映射到SARM中,也就是将中断向量映射到0x2000 0000为始的地址中。
然后将程序设置为从SARM中启动,其实就是切换映射内容。那么这样子我们就可以实现两个工程间的跳转而不互相干扰了。
在这里我们就可以说明第二个工程中为什么我的IRAM起始地址为0x2000 00c0,因为我们M0中有48个中断,一个中断向量映射我们需要4个字的空间,所以总共我们需要0xC0个地址空间来保证我们的映射内容,而我们的第一个工程因为是从flash中去映射地址,所以可以从0x2000 0000开始使用IRAM。
讲了这么多,相信各位都看烦了,我的手也写酸了,那么接下来我们来总结一下,总结之前我们先声明几个内容方便我们描述。
- 第一个工程项目我们称之为“bootloader”,起始地址为0x0800 0000,为默认启动的工程项目,后面我们称之为启动引导程序。
- 第二个工程项目称之为“APP1”,起始地址为0x0801 0000,后面我们称之为应用程序。
总结一下实现不同工程间的挑战实现:
- 需要获取目的工程的起始入口地址。
- 对中断向量地址进行重新映射。
- 设置是从flash中还是SRAM中获取向量地址。
- 开始跳转。
While(等待各位读者理解上面那段话的意思);
/***************************************************************************************************************************************************************************************************************/
五、避坑!!!避坑!!!避坑!!!
你以为到这里就结束了吗?然而并没有!
有把握有兴趣的朋友可以按照上述过程尝试一下,你会发现一个非常有趣的过程,当您没有涉及到中断的时候,可能不会发现什么问题,也有可能发现少部分中断也可以运行,但是你尝试一样bootloader和APP1中同时使用串口发送中断试试,你就会发现跳转到APP1中时你的程序就会死,当你用debug时就会发现一直不停的进入串口中断。
这时候你可能就会在网上找各种教程来解决这个问题,但是你发现大家都是说在跳转前先用__disable_irq()来关闭全部中断,然后在跳转后用__enable_irq()来打开中断,这个方法我试过了,但是并不是向他们介绍的那样防止APP1的开头,而是要放在所有程序都初始化之后,这样不是不行,但是总觉得哪里怪怪的有没有。所以我觉得这个方案并不是最好的,后来我就想解决的思路肯定是关闭所有中断,但并不是使用__disable_irq(),因为用这个关闭的中断必须要使用__enable_irq()才能打开,所以会导致一关全关,一开全开的现象,这样放在APP1的开头,在bootloader中如果有中断没处理完用__enable_irq()开启中断后会立即进入中断服务函数,不开又不能用中断,所以只能在全部外设初始化后再打开中断,这样好吗?这样不好!那么我是怎么解决的呢,简单粗暴,失能所有中断!有人可能会问,这不是一样吗?不!肯定不一样,一个是关闭所有中断,一个是失能所有中断,失能后我可以通过初始化一个个使能回来,就不会造成一开全开的效果,这时候可能又有人会问你这一个个失能不是更麻烦吗?然并卵,我们可以直接操作NVIC寄存器NVIC中有一个寄存器是ICER,在这个寄存器的对应位置中写1就可以失能相应中断,我这里很粗暴,直接一句话搞定 NVIC->ICER[0] = 0XFFFFFFFF;搞定!
现在我们就可以使用我们的IAP来切换工程了!
六、bootloader向APP跳转整理归纳流程
跳转过程归纳:
1、检查跳转的地址是否合法,判断依据为取出跳转地址的数据(该数据为栈顶数据),判断是否在RAM区间中。
2、失能所有中断
3、获取跳转入口地址
4、映射中断向量到SRAM区
5、将入口地址映射到SRAM
6、开始跳转
七、APP向APP跳转流程整理归纳
同bootloader向APP跳转一样
八、APP向bootloader跳转
大体流程差不多,但需要把启动从SRAM区修改回flash区
不需要修改中断向量的映射地址,因为flash区用默认的就可以了。
总结:我写得好累,你们要是不愿意看,就直接下载代码尝试吧。
另外网上其他的教程都是在跳转前需要处理代码,跳转后需要处理代码,而我不一样,只需要处理前处理一下接OK了!!!使用更简单,操作移植更方便!!!