我总是很希望自己能产生一种感知电压变化的能力,就像B站上的教学动图中,电流从电源流出时导线就像LED亮起来一样,我将指尖触到导线上就能感受到实时的电压变化。我在上学和工作时经常由于无法理解或者认知错误陷入非常迷惘和痛苦中,比如在我理解数学和电磁场的基本理论时,或者我的代码运行中出现了我认为不可能出现的现象。前者在我不转行的前提下暂时不会遇到,而后者几乎就是我工作的常态了。
了解微控制器内部是怎么运行的对一个单片机工程师来说极其重要。在新手无知时期我以为学习单片机就是学习外设,毕竟当时我就用用串口、ADC和定时器,再点两个灯。启动时硬件做了什么,代码存在哪里,CPU跑起来后怎么取指令,数据保存在哪里,C语言怎么跑起来的,堆在哪里,栈在哪里,变量在哪里,常量在哪里,外设怎么初始化,收发怎么进行,中断触发条件是否设置好了,中断优先级是否设置合理了,标志位复位的时机是否适合,针对指针的处理是否读错了范围,写操作有无越界,任务的设置、业务逻辑的设计是否合理,状态机的设计的容错性能够不够……逻辑怎么在跑、程序什么时候崩掉永远是个疑问。
单片机是怎么启动的?以i.MX rt 1060为讨论对象我们的代码大多从main函数开始运行,而且电子工程师习惯使用C语言进行编程。单片机在进入main函数之前进行了哪些操作呢?从硬件上讲,从上电到CPU执行第一条指令前,再从CPU执行第一条代码到进入main函数时,各进行了哪些操作呢?
其实我并不知道单片机硬启动做了什么,如果我是设计师,大概是检查供电是否正常或温度是否正常吧,再确定指令的加载位置。对于我们用户来讲,有个要点是如何决定CPU从哪个存储器加载第一条代码,或者说把哪里的代码映射到0位置。ST的手册说是SYSCLK的第4个上升沿锁存BOOT引脚的值,决定从Boot,SRAM还是从flash启动,CH32V307的参考手册中也是这样。
恩智浦的跨界微控制器在18年开始被我关注,当时我被它宣称的600MHz主频深深迷住。如此强悍的性能想必能做出很多有意思的东西。然而从入手至今,我也没有用它做出能拿得出手的东西,非常尴尬。原因其实非常简单,我并不会用这个芯片,想把这个芯片的外设用起来,并设计合理的程序让内核不受约束地跑起来其实并不简单。
很多单片机上电后必然会先运行厂商自带的Boot程序,Boot程序根据其他的信息(比如引脚状态)选择工作模式,或者进行从外部的串口或者USB下载代码,比如i.MX rt系列。单片机启动过程可以分为硬件启动和软件启动,从上电到内核执行第一条代码前的这段时间可以称之为硬件启动,从内核开始执行第一条代码开始到进入main函数即为软件启动。感谢前人制定了C语言的标准,让我们能以一个共识编写启动之后的代码。
启动模式注意本文是对i.MX rt 1060参考手册的个人理解,必然存在错误。所谓的跨界微控制器想必就是恩智浦用cortex m7的内核搭配它以往设计的外设做出来的奇特产品,最麻烦的一条是,它没有供用户存放代码的大容量非易失性存储器——是的,它需要外接flash。我挺不能理解这点的,1064不就合封了flash吗?按照这个芯片最广泛的用法,大家肯定会外接一个8脚的QSPI nor-flash,然后配一个SDRAM,代码就存在nor-flash中。当我们将Boot_mode[2]的两个位设为00b的时候,芯片可以从外部设备启动,而后面通过BOOT_CFGx[X]这几个位可以设为特定的设备启动。其实这里非常复杂,因为3种启动模式中的 Boot From Fuses和Internal Boot其实挺相似,他们的用法被启动所使用的熔丝及其位域定义给环环相套得麻烦且复杂。首先Boot_mode[2]两位为0时是从Boot From Fuses启动,这个启动会参照Boot eFUSE的值BT_FUSE_SEL这位,如果这位是0,就直接跳到串行下载器(Serial Downloader,就是Boot_mode[2]=1时的工作模式),如果是1,则按照Boot eFUSE的其他配置位(BOOT_CFGx[X])选择从哪里启动。Boot_mode[2]两位为2时,如果BT_FUSE_SEL位是0,则查询BOOT_CFGx[X]的映射pad的状态,这些Pad的状态决定BOOT_CFGx[X]的值;如果BT_FUSE_SEL为1,则由Boot eFUSE的BOOT_CFG1[7:4]位来确定,注意需要注意的是,此时Boot eFUSE其他各位也各有新的定义。在i.MX rt系列中我发现了挺多熔丝(Fuse)的,其实熔丝是对芯片初始设置的一些定义,我发现相当多的寄存器定义在熔丝中是有体现的,在熔丝中甚至能对SDRAM的SEMC进行初始化配置,而熔丝的性质决定其是一次性编程(OTP)的,初始化应该都是0,经修改后为1,这为用户的产品化提供了方便,毕竟可以对芯片硬启动状态也进行配置,与将配置存在flash中相比,熔丝不在哈佛结构的4GB绝对地址空间中,需要特殊的操作,这样可以避免误操作。Boot_mode[2]两位为1时即为从外部串口或者USB口进行配置,我没有深入研究不叙述。
所以在出厂状态下(未对Boot eFUSE进行编程状态下),应将Boot_mode[2]设为01b,将BOOT_CFG1[7:4]设为0000b,即可实现在QSPI nor-flash中存储代码。
nor-flash存放的image1060以特定的配置和nor-flash通讯上后,也不能直接从0地址开始读代码就当是第一条指令了,1060默认的加载nor-flash的配置应该会存在熔丝中,但是1060的默认设置只是比较基础的一部分,1060以这个基础的配置和nor-flash进行起初的通讯。在微控制器正常跑起来时,应以最优化的通讯配置和nor-flash进行通讯,因此还有部分针对所选nor-flash的配置内容需要从nor-flash中加载,也就是说nor-flash存储的image中存储的不只是代码段和读写的数据。由于这个nor-flash不合封在一个chip中,所以nor-flash中的数据分布和传统微控制器的flash是不一样的。
由于1060将nor-flash映射到0x6000-0000的区域,这里做个实验,将0x6000-0000开始向后偏程序大小再偏8KB空间位置的内容打印出来。代码如下图。
图一 打印出全部image
偏移8KB是个经验值,实际上我确实成功得打印出全部image,一直打印到全ff区域了。
从分散加载文件和手册里可以看出,存储在nor-flash中image分为配置参数(Configuration parameter),镜像向量表(Image vector table,IVT),启动数据(Boot data)域, 设备配置数据(Device configuration data,DCD),后面才是传统微控制器镜像bin文件中常见的中断向量表,代码段和数据段。
配置参数从打印的数据来看,第一段,是配置存储镜像的flash的参数(Configuration parameter),大小为512(0x200)字节,这里截取一部分:
图二 配置参数截图的一部分
根据参考手册FlexSPI Configuration Block及Serial NOR configuration block这两节的解释,这里选部分解析下:
表一 配置参数各域定义分析
值
偏移(字节)
域名
含义
0x42464346
0
Tag
FCFB的ascii码
0x56010400
4
版本
1.40版本
0x01
0xC
读样本时钟源
DQS内部回环
0x03
0xD
片选保持时间
默认应该是0x03
0x03
0xE
片选建立时间
默认应该是0x03
0x00
0xF
卷地址宽度
0x00
0x44
设备类型
1是串行nor,2是串行NAND;
(这个字节为什么不填1呢)
0x04
0x45
Flash引脚类型
4脚,即QSPI
0x06
0x46
串行时钟频率
100MHz
0x0a1804eb
0x80
查找表(从此起始)
256字节的查找表
0x00000100
0x1C0
页大小
一页256字节,不被ROM使用
(后面半句看不懂)
0x00001000
0x1C4
扇区大小
每个扇区4KB,不被ROM使用
(后面半句看不懂)
0x00000000
0x1C8
时钟速度
不改变。不被ROM使用。
(不改变的意思应该是继续使用熔丝里的设置,全程使用30MHz)
(后面半句看不懂)
可将其中原来由熔丝配置的内容根据选定的nor-flash型号重新配置一次,达到最优通讯技能。配置参数中也有比如查找表这种更为复杂的部分,还有一些看似重复的设置。这里并不追求每个位都了解,知道有这个部分就行了,未来根据需要再详细了解。
镜像向量表镜像向量表(Image vector table,IVT)是一个指示镜像各部分位置的向量表,是放在固定位置的,nor-flash是放在4KB的地方。这里截取IVT的内容:
图三 IVT截图
根据参考手册Image vector table structure这节的解释,这里尝试人肉解析下:
表二 镜像向量表部分的解析
值
偏移(字节)
域名
含义
0x412000d1
0
头
版本:A,长度32字节
0x60002000
4
入口
镜像的第一条可执行指令的绝对地址
0x000022ac
12
DCD地址
设备配置数据(DCD)的绝对地址
0x60001020
16
Boot数据
启动数据域的绝对地址
0x60001000
20
自己
指示自己(IVT)的绝对地址
启动数据域启动数据(Boot data)域只有一个字的大小,它的作用就是指示整个镜像的地址和长度。
图四 启动数据域的截图
这个域存放在0x6000-1020的地址,整个镜像放在0x6000-0000处,大小为8MB,后面的组件标志就先不管了。启动数据域的地址也符合镜像向量表的说明,镜像也确实存放在0x6000-0000的位置,从FLEX-SPI启动就从FLEX-SPI运行呗,镜像就存在nor-flash上,不需要跳到别的位置取镜像。
设备配置数据设备配置数据(Device configuration data,DCD),这个简写很有迷惑性啊。这个数据按照参考手册的说法,其存在的意义是在使用之前对某些外设的寄存器进行配置让其能够直接使用。挺,ummm,无语的。我想到一个应用就是在__mian进行分散加载的RW数据搬运前就将SDRAM初始化好,免得在应用数据中初始化一次并自己进行分散加载。IVT指示这个功能存放在0x000022ac,这里就有点疑惑了。
0x000022ac这个地址是在ITCM空间的。DCD数据由软件来定义,即为SDK中evkmimxrt1060_sdram_ini_dcd.c的dcd_data[]数组,在我实验的工程中为内容就一个字节,且为0,我也确实在镜像向量表指示的CDC的位置找到了这个0。但是ITCM的内容布局是由分散加载文件确定的,而且起码在上电后确定ITCM的大小和硬件位置时,ITCM中都是乱码,这就要求从nor-flash拷贝数据到ITCM中这个行为应该也是由CPU实现的,属于软启动的一部分。CDC代码被设计存放到ITCM的空间,似乎就能说明CDC段运行时间是在分散加载之后。可能就是__mian中的一个部分。
用户程序代码这里才是传统微控制器的镜像文件部分,即为中断向量表,代码段,只读数据段和读写数据段。按照镜像向量表的指示,这部分数据存放在0x6002-0000的位置,按照我的分散加载文件的布局设计,用户程序代码段在nor-flash布局设计为:
表三 用户代码段的布局
顺序
代码名称
解释
startup_MIMXRT1062.O(RESET,+FIRST)
.S文件中的中断向量表
* (InRoot$$Sections)
__main
system_MIMXRT1062.O(+RO)
system_MIMXRT1062.c的代码段
startup_MIMXRT1062.O(+RO)
.S文件中代码段
按照上表,用户程序代码的第一部分是中断向量表,这部分的源码为:
图五 中断向量表的源码
在镜像中对应部分的十六进制代码为:
图六 用户程序代码中的中断向量表的截图
都只截图了部分啊,都太长了。可以看到中断向量表的第一个字为栈地址,0x2002-0000,DTCM首地址向后128KB的地址,DTCM默认按熔丝设置,其大小刚好也就是128KB,也就栈地址放在DTCM的最后,栈向下生长嘛~其后都是各中断的位置,0x60002419就是reset中断的位置,但是别说8字节对齐了,这个地址连4字节对不对齐,我查了下map文件,Reset_Handler还真的被放在这个9结尾的地址,所有的中断服务函数的地址都不是.s文件要求的8字节对齐。无法理解。
中断向量表共1个栈指针和255个中断服务函数的入口,共1KB,其中最重要的就是Reset_Handler,毕竟其他所有的函数都从这里开始。
按照上表,中断向量表接下来被布局的就是"* (InRoot$$Sections)"这个代码,看map文件,这个里面就是__mian的实现。
接下来就是各种用户函数的机器指令代码了。跟着一些只读和初始化不为0的变量们。
至此,对i.MX rt 1060的nor-flash镜像的解读就结束了。
i.MX rt 1060的硬启动和软启动我这里将硬启动重新定义为:从上电到内核执行第一条用户指令前的这段时间可以称之为硬件启动,将内核执行用户的第一条指令开始即认为开始软启动。但实际上我所知的仍然非常之少,这里仍然仅仅只能是记录我目前的认知。
第一条用户代码指的是.S里定义的Reset_Handler里第一条指令,关闭全局中断,CPSID,十六进制码为B672。之前应该是也存在内核执行指令的行为的,但是作为用户这之前我无法干预内核的具体行为,所以也归入硬启动的范畴。
硬启动的过程其实已经在上章隐晦地说完了。1060上电按照OCROM中的厂商代码启动起来,并按照熔丝中的配置驱动通讯外设,1060读取Boot_mode[2]和BOOT_CFGx[X]的值决定从哪里去取用户的指令,我将其配置成从FLEX-SPI的nor-flash启动,启动的初始化参数设置由熔丝和BOOT_CFGx[X]共同决定。1060从固定的镜像向量表位置去读配置参数,根据配置参数更加高效地通过FLEX-SPI和nor flash通讯。
问题只在于DCD是在什么时候执行,我认为其在.S文件之前执行,由厂商代码唤起,属于硬件启动的一部分。
硬软之间在上述操作完成之后,并不是说内核就取Reset_Handler里的指令了,内核怎么会知道Reset_Handler的位置?镜像向量表里可没有这个参数。镜像的第一条可执行指令的绝对地址是0x60002000,内核会取这个地址的值0x2002-0000,为栈指针,即内核将0x2002-0000存进主栈指针寄存器MSP,然后偏移4个字节取第二个字的值,认定其为Reset_Handler的地址,进入Reset_Handler取第一条指令,关闭全局中断,自此硬启动结束,软启动开始。
.S文件的分析.S文件即常说的startup_MIMXRT1062.s文件,用户代码范畴的启动文件。是用户能控制的第一个运行的代码文件。
那么.S文件需要完成什么任务呢?作为main函数之前的文件,我觉得它的任务有二:
其一:main函数是由C写成的,C语言的函数在内核上运行是需要运行环境的,单片机作为硬件需要为C语言的运行搭建环境,比如,清零RW和ZI段,堆和栈,搬运RW数据;
其二:定义中断向量表,告知内核中断向量表的位置;
其三:唤起main函数;
.S文件中先定义了中断向量表:
PRESERVE8;要求全文8字节对齐,听说8字节对齐在M7的双发射结构下运行效率最高
THUMB;使用thumb指令集
AREA RESET, DATA, READONLY;定义一个DATA区,名称为RESET,只读
EXPORT __Vectors;导出__Vectors
EXPORT __Vectors_End;导出__Vectors_End
EXPORT __Vectors_Size;导出__Vectors_Size
IMPORT |Image$$ARM_LIB_STACK$$ZI$$Limit|;导入这个定义
__Vectors DCD |Image$$ARM_LIB_STACK$$ZI$$Limit| ; 栈地址
DCD Reset_Handler ; 复位函数
DCD NMI_Handler;不可屏蔽中断
DCD HardFault_Handler;硬件错误中断
DCD MemManage_Handler ;存储管理器中断
DCD BusFault_Handler ;总线错误中断
DCD UsageFault_Handler ;应用错误中断
DCD 0
DCD 0
DCD 0
DCD 0
DCD SVC_Handler
DCD DebugMon_Handler
DCD 0
DCD PendSV_Handler
DCD SysTick_Handler
DCD在汇编中应该是分配一个字的空间的意思。这里注意和镜像中的DCD区分。上文加下来是1060的外设中断,这里不继续枚举。下面讨论Reset_Handler中断的内容。
__Vectors_Size EQU __Vectors_End - __Vectors;定义了__Vectors_Size的意义
AREA |.text|, CODE, READONLY;定义了一个CODE区的代码段,只读
Reset_Handler PROC;函数开始
EXPORT Reset_Handler [WEAK] ;导出Reset_Handler,且弱定义
IMPORT SystemInit;导入SystemInit
IMPORT __main;导入__main
CPSID I;关闭全局中断
LDR R0, =0xE000ED08;将中断向量偏移寄存器的地址写进R0
LDR R1, =__Vectors;将__Vectors写进R1
STR R1, [R0];将__Vectors(0x6002-0000)写进中断向量偏移寄存器,
LDR R2, [R1];取R1指向的值写进R2,
MSR MSP, R2;将R2的值写进主栈指针寄存器
LDR R0, =SystemInit;将SystemInit函数的地址写进R0
BLX R0;执行SystemInit函数
CPSIE I;使能全局中断
LDR R0, =__main;将__main的地址加载到R0中
BX R0;执行__main
ENDP;函数结束
其后还弱定义了一些函数,免得编译报错找不到。但都是进去就死循环。
可以看到软启动就是.S文件中Reset_Handler所完成的就是重定义中断向量表位置,写主栈指针,执行系统初始化函数,执行__main,由__main唤起main函数。系统初始化函数中主要是一些关闭看门狗,cache相关的操作,并不一定是时钟初始化。
编译器行为和分散加载文件微控制器的RAM在什么位置,flash在什么位置,各有多大,是可以由分散加载文件确定的。对于1060这种可以说是很复杂的微控制器系统,各个存储器有其鲜明的特点,从内核寄存器到cache,到TCM,到OCRAM,再到片外RAM,延迟和读取时间都会递增;从另一方面来说,镜像的各个部分都需要放在指定的位置,IVT指定了大部分只读数据的位置,这些都不可以让链接器自行分配存储位置。
我有个印象是i.MX rt系列是以恩智浦的应用处理器为原型来设计系统框架的,那么它的结构肯定和应用处理器有相似之处。了解i.MX rt的存储器分配,可以充分用起这个芯片几百兆的主频,将微控制器这一领域玩到极致,并为未来的应用处理器的原理理解打前站,为了Linux的bootloader的编写提供一些硬件基础。
用户代码一般是使用C语言写的,而内核只能运行机器指令,这两种是存在矛盾的。C语言的存在是为了更方便人类理解,方便人类在不需要了解底层的详情就能写出各种复杂的应用。但是如果要从事底层相关C代码的设计,那么了解内核了解汇编和机器指令是必须的,毕竟底层代码的追求永远是高效,稳定,方便调用和精简。
浅聊编译编译是将C或C艹写成的源码工程转成内核能直接执行的机器码的过程。上面提到的几乎所有行为都是微控制器的行为,而在镜像生成之前,对工程相关处理都是工具链行为或者说是编译器行为。作为用C写代码的工程师,需要非常清楚代码中哪些语句是内核在运行,哪些是编译器帮忙做的,那些是看似内核在做但是实际上被优化成编译器已经帮忙做了。
C源码工程从编译角度可以分为这几个部分:函数,全局变量,临时变量,常量。工程经过编译器,首先应该是预编译过程,各.C文件该包含的包含,该宏替换的替换;然后进到编译器和汇编器,转成.O文件,此时已经将C源文件分成了代码段(Code),只读数据段(RO),读写数据段(RW)和初始化为零的数据段(ZI),括号中的简写只是MDK的习惯标识;最终通过链接器,将各个.O文件里的各个段合并到一个文件中,生成镜像文件。镜像文件的大小取决于代码段加只读数据段加读写数据段,这些是需要保存到非易失性存储器的,而读写数据段和初始化为零的数据段是处理器可能会频繁读写的,需要放在RAM中,所以RAM的占用由读写数据段和初始化为零的数据段决定,map文件的最后一部分声明体现了这一点。
图七 镜像运行对ROM和RAM的要求
内核运行中,每执行一个指令都要行一次取指,每进行一次加载或者存取都可能访问RAM,这些都可能会引起内核等待,毕竟nor-flash之类的非易失性存储器每访问一次都是非常慢的,而SRAM结构可能会很快,但是相比于600兆的i.MX rt M7内核,还是太慢了。
SCF文件解析在C语言中,读取寄存器的值,比如 buff[i]= USART1->DATA,这只需要一句话,但是翻译成机器码可能需要:1计算栈偏移;2读外设数据到R0;3将R0存进栈;这3个指令完成,耗费四五个时钟周期。这还是能接受的。但是如果取指令存在延迟呢?如果存进栈也存在延迟呢?
这里提出了很多个问题,但是我这次只想解释下加载文件的条目结构。这里以MDK的scf文件为例。
首先是定义了存储器位置和大小:
#define m_itcm_start 0x00000000
#define m_itcm_size 0x00040000
;ITCM空间,代码段运行域,默认熔丝设置为128KB,理论上可以做到做小1个时钟读写
#define m_flash_config_start 0x60000000
#define m_flash_config_size 0x00001000
;nor-flash空间,flash配置参数空间,存储nor-flash交互的数据,设为4KB,nor-flash和1060之间通讯有FIFO可以保证不需要每次要数据都重读一遍,但读取一次nor-flash的延迟和耗时还是可预见得很大。
#define m_ivt_start 0x60001000
#define m_ivt_size 0x00001000
;nor-flash空间,镜像向量表,硬启动最重要的数据了,4KB
#define m_interrupts_start 0x60002000
#define m_interrupts_size 0x00000400
;nor-flash空间,中断向量表,1KB。这里是未来修改的一个方向,如果每次中断来了内核都要读nor-flash取中断服务函数的地址,这个中断响应时间必然不小。
#define m_text_start 0x60002400
#define m_text_size 0x007FDC00
;nor-flash空间,代码段加载域,紧跟中断向量表,nor-flash的最后部分,其大小不要让代码存到nor-flash的尽头之外就行了。
#define m_data_start 0x20000000
#define m_data_size 0x00020000
;DTCM空间,放置RW数据和ZI数据的,默认最大空间也是128KB,访问耗时最小1个时钟
#define m_data2_start 0x20200000
#define m_data2_size 0x000C0000
;OCRAM空间,不被设为TCM空间的片上SRAM结构就是OCRAM空间了。访问耗时4个时钟,一般给DMA用。
#if (defined(__stack_size__))
#define Stack_Size __stack_size__
#else
#define Stack_Size 0x0400
#endif
;1KB的栈
#if (defined(__heap_size__))
#define Heap_Size __heap_size__
#else
#define Heap_Size 0x0400
#endif
;1KB的堆
上面的定义也仅仅是定了位置和大小,实际存什么进去由下部分的语句约束:
LR_m_text m_flash_config_start m_text_start+m_text_size-m_flash_config_start
{;LR_m_text为这个section的名字,后面跟着起始地址和长度
RW_m_config_text m_flash_config_start FIXED m_flash_config_size
{;设一个区域为RW_m_config_text,从0x6000-0000开始,固定4KB大小
* (.boot_hdr.conf, +FIRST);这个区域存放配置参数,定格对齐存放
}
RW_m_ivt_text m_ivt_start FIXED m_ivt_size
{ ;设一个区域为RW_m_ivt_text,从0x6000-1000开始,固定4KB大小
* (.boot_hdr.ivt, +FIRST);定格先存放镜像向量表
* (.boot_hdr.boot_data);再存放启动数据域
* (.boot_hdr.dcd_data);最后存放DCD数据
}
VECTOR_ROM m_interrupts_start FIXED m_interrupts_size
{; 设一个区域为VECTOR_ROM,从0x6000-2000开始,固定1KB大小
startup_MIMXRT1062.O(RESET,+FIRST);存放.S中的RESET区域,即中断向量表
}
ER_m_text m_text_start FIXED m_text_size
{;设一个区域为ER_m_text,从0x6000-2400开始,固定m_text_size大小
* (InRoot$$Sections);存放__main部分
system_MIMXRT1062.O(+RO);存放ystem_MIMXRT1062.O的只读部分
startup_MIMXRT1062.O(+RO);存放.S文件的只读部分
}
ER_m_text_ITCM m_itcm_start m_itcm_size
{;设一个区域为ER_m_text_ITCM,从0x0000-0000开始,128KB大小
.ANY (+RO);存放除上面定义之外的所有文件的只读部分
}
RW_m_data m_data_start m_data_size-Stack_Size-Heap_Size
{;设一个区域为RW_m_data,从0x2000-0000开始,128KB减去堆和栈的大小
.ANY (+RW +ZI);存放所有的RW数据和初始化为0的数据
flexspi_nor_flash_ops.o (+RO +RW +ZI);存放flexspi_nor_flash_ops.o
fsl_flexspi.o (+RO +RW +ZI);存放fsl_flexspi.o
* (NonCacheable.init);存放不需要在cache中运行的数据
* (NonCacheable)
}
ARM_LIB_HEAP +0 EMPTY Heap_Size
{;堆空间
}
ARM_LIB_STACK m_data_start+m_data_size EMPTY -Stack_Size
{;栈空间
}
}