发 帖  
原厂入驻New

[经验] 关于STM32启动流程,分散加载脚本与处理器架构

2018-8-21 09:36:29  1277 STM32 处理器
分享
0
最近看了好多关于处理器内部架构和原理的内容,脱离具体处理器学习总感觉不太容易,因为STM32资料较多,于是先研究下STM32启动流程和执行架构,结果发现里面水很深,竟花近一个礼拜时间方才完全领悟,网上资料虽多,但也比较分散,于是想自己整理一个出来分享给大家。
首先,是关于处理器的哈佛架构和冯诺依曼架构的区别,简单来说,哈佛架构是一种将程序指令储存和数据储存分开的存储器结构。冯诺依曼架构是一种将程序指令存储器和数据存储器合并在一起的电脑设计概念结构。怎么理解呢?具体到STM32上,STM32是哈佛架构,它在运行时,指令存储在片内的noRFlash上,数据存储在片内SRAM上,也就是说,STM32的程序是可以直接在FLASH上运行的,而不是先将FLASH上的程序全部copy到RAM再运行,这一点需要注意。
   接下来说下STM32启动流程。
STM整个启动过程是指从上电开始,一直到运行到main函数之间的这段过程,步骤为:
①上电后硬件设置SP、PC
②设置系统时钟
③软件设置SP
④加载.data、.bss,并初始化栈区
⑤跳转到C文件的main函数

     在真正讲解启动过程之前,先要讲解程序下载到Flash上的结构和程序运行时(执行到main函数)时的SRAM数据结构。程序在用户Flash上的结构如下图所示。下图是通过阅读hex文件和在MDK下调试综合提炼出来的。
1.png
MSP初始值        编译器生成,主堆栈的初始值
异常向量表        不多说
外部中断向量表      不多说
代码段          存放代码
初始化数据段       .data
未初始化数据段      .bss
加载数据段和初始化栈的参数
  加载数据段和初始化栈的参数分别有4个,这里只讲解加载数据段的参数,至于初始化栈的参数类似。
0x0800 033c  Flash上的数据段(初始化数据段和未初始化数据段)起始地址
0x2000 0000  加载到SRAM上的目的地址
0x0000 000c  数据段的总大小
0x0800 02f4  调用函数_scatterload_copy
  需要说明的是初始化栈的函数--0x0800 0304与加载数据段的函数不一样,为_scatterload_zeroinit,它的目的就是将栈空间清零。
2.png
三、数据在SRAM上的结构
  程序运行时(执行到main函数)时的SRAM数据结构
3.png
上述是复制的,这里说下自己的理解,想理解这部分内容,必须了解分散加载脚本和__main函数,很多文章讲启动流程却没有着重讲解这两部分,导致理解困难。

很多朋友对分散加载不是很理解,其实它的原来很简单,这些加载的原理都源自生活。
由于现在的嵌入式技术发展比较快,各类存储器也层出不穷,但是它们在容量、成本和速度上有所差异,嵌入式系统又对成本比较敏感,那么合理的选择存储器和充分的利用存储器资源成为一个必要解决的问题。咋们工程师最喜欢的就是发掘问题,然后解决问题,基于嵌入式系统对存储器的敏感,那么要合理的利用存储器资源,就必须找到一种合理的方式。工程师们发现,可以把运行的程序放在不同成本的存储器中来寻找这个成本的支点,比如把没有运行的但是较为庞大的程序放在容量大、成本低、速度也较低的FLASH存储器中,要用的时候再去拿。但是,这里面又有一个问题,嵌入式本身就对信号的处理速度有较高的要求,这点在实时操作系统的定义上上有所体现。所以那些经常要用的程序段如果要保证其高速的运行那么就得放在一个在高速的存储器中,不过这是有代价的:较高成本,小容量。但是,相信由于技术的发展这个问题终将被解决,到时候寻找平衡点的问题也就不存在了。好了,说了多了点。切入正题。
         程序总有两种状态:运行态和静止态。当系统掉电的时候程序需要被保存在非易失性的存储器中,且这个时候程序的排放是按照地址依次放的,换句话说:我才懒得管它怎么放,只要不掉就行。当系统上电后,CPU就要跑起来了,CPU属于高速器件,存储器总是不怎么能跟得上,既然跟不上那么我们就尽量缩短它们之间的差距,那留下一条路,那就是尽量提高存储器的读取速度,存储器类型决定其速度的水平,那么尽量放在速度高的存储器就成为首选解决方案。那么我们就把要执行的程序暂时拿到速度较快的RAM中。那么拿的过程就牵涉到程序的加载了。这就是要解决的问题。
         一个映像文件由域(region)、输出段(output sections)和输入段(input sections)组成。不要想得太复杂,其实他们之间就是包含与被包好的关系。具体关系是这样的:
        映像文件 >   域 >  输出段 >  输入段
输入段:
        输入段就是我们写的代码+初始化的数据+应该被初始化为0的数据+没有初始化的数据,用英文表示一下就是:RO(ReadOnly),RW(ReadWrite),ZI(ZeroInitialized),NOINIT(Not Initialized)。ARM连接器根据各个输入段不同的属性把相同的拿再一起组合一下就成为了输出段。
        请看看平时写的东东:
         AREA    RESET, CODE, READONLY
         AREA    DSEG1, DATA, READWRITE
         AREA    HEAP, NOINIT, READWRITE
看出其属性没?
输出段:
        为了简化编译过程和更容易取得各种段的地址,那么把多个同属性的输入段按照一定的规律组合在一起,当然这个输出段的属性就和它包含的输入段的属性一样咯。输入段的排放规律就是:最先排放RO属性的输入段,然后是RW属性段,最后是ZI或NOINIT段。
域:      
        为什么还要加一层域,我的理解是由于代码的功能不同,那么我们有必要把不同功能的代码分类放。我们可以把需要高速执行的代码放在一起、把对速度要求不高的放在一起、把执行频率高的放在一起,把执行频率低的放在一起...那么按照这种方式放的代码就可以根据其具体需要放在不同的存储器中了。这样可以提高程序执行速度。一个域中包含1~3个输出段。
映像文件:
         我暂时把映像文件理解成烧到存储器中的文件,由N个域组成。这些域其实可以看做是独立的模块,只是他们按照一定的顺序(这个顺序还是:RO+RW+ZI)被捆绑在一起,这样才方便烧写到非易失存储器中去。
        好了,了解了映像文件的组成,那么来看看映像文件是怎么跑起来的。
       映像文件就是有N节车厢的火车,车厢(域)里装着要送到不同站(不同类型的存储器)的货物。到相应的站了,那么就把相应的车厢拿下来。指挥拿这个的就是scatter文件。拿下货物车厢后,我们就解开它,把里面的品牌为RO的货物提取出来,按照scatter的指示发给某个地址,然后再先后把品牌为RW和ZI的货物发到scatter指定的地址。
       看看这个加深理解:
  1. LOAD_ROM1     0X00000000    ; 从火车上取出来时的地址(如:成都站)

  2. {

  3.             EXEC_ROM1       0x40000000   

  4.             {

  5.                    PROGRAM.O(+RO)  ;把品牌RO的货物发给0x40000000去

  6.                     RAM1          0x80000000

  7.                     {

  8.                     PROGRAM.O(+RW,+ZI);把品牌RW,ZI的货物依次发给0x80000000

  9.                      }                  

  10.              }
复制代码

5.png


相关经验

kszdj113 2018-8-21 13:53:59
把复杂的事物理得通俗易懂了,希望以后能运用上
回复

举报

评论

高级模式
您需要登录后才可以回帖 登录 | 注册

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题,请联系本站作侵删。 侵权投诉
发经验
关闭

站长推荐 上一条 /7 下一条

快速回复 返回顶部 返回列表