M3启动文件.tmp_图文


1.1 1.2 1.3 2.1



第 1 章 CMSIS 标准 ······································································································· 1
CMSIS 标准简介 ·············································································································· 1 CMSIS 标准的软件架构 ·································································································· 1 CMSIS 标准的文件结构 ·································································································· 2

第 2 章 注解 startup.s 文件(以 startup_LPC17xx.s 为例) ··········································· 3
堆栈以及堆的初始化 ······································································································· 3 2.1.1 堆栈的初始化 ··········································································································· 3 2.1.2 堆的初始化··············································································································· 3 2.2 中断向量表的初始化 ······································································································· 4 2.3 调用 Reset Handler ··········································································································· 5 2.4 其他的代码······················································································································· 5 ARM 芯片的启动过程概述 ····························································································· 7 结合代码来看 ARM 芯片的启动过程 ············································································ 8 3.2.1 调试环境的搭建及测试代码 ··················································································· 8 3.2.2 跟踪启动代码 ··········································································································· 8 3.2.3 详细的启动过程 ······································································································· 9 3.2.4 __main ····················································································································· 10 3.2.5 __rt_entry ················································································································ 10 3.2.6 __rt_lib_init ············································································································· 11 关于 microlib ·················································································································· 11 x.map ······························································································································· 11 3.4.1 关于链接器············································································································· 12 3.4.2 RW 段在 RAM 的存放··························································································· 12 关于 ARM 程序的 Memory 管理 ·················································································· 12 3.5.1 ARM 镜像文件的组成(image) ········································································· 12 3.5.2 关于 image 文件(镜像文件) ············································································· 13 3.5.3 RO 段 ······················································································································ 13 3.5.4 RW 段 ····················································································································· 14 3.5.5 ZI 段(初始化为 0 或未初始化的变量) ···························································· 15 缺省内存映射 ················································································································· 16 内存模型························································································································· 17 什么时候使用分散加载文件 ························································································· 18 适用范围························································································································· 18 再谈 ARM Image(镜像文件) ···················································································· 18 节放置····························································································································· 19 一个简单的加载过程 ····································································································· 20 分散加载文件语法 ········································································································· 21 4.6.1 加载时域(区)描述 ····························································································· 21 4.6.2 运行时域(区)描述 ····························································································· 22 4.6.3 输入段描述············································································································· 23
i

第 3 章 ARM 芯片的启动过程详解 ··············································································· 7
3.1 3.2

3.3 3.4

3.5

3.6 3.7 4.1 4.2 4.3 4.4 4.5 4.6

第 4 章 关于分散加载文件 ··························································································· 18

4.7 4.8

实战测试(一个简单的例程) ····················································································· 24 实战分散加载 ················································································································· 26 4.8.1 多块 RAM 的分散加载文件配置 ·········································································· 27 4.8.2 多块 Flash 的分散加载文件配置 ·········································································· 28

第 5 章 编写自己的启动代码(基于 LPC17xx) ························································ 32
最简单的启动代码(Startup.s Version-1.0)································································ 32 5.1.1 启动代码必须完成的三项工作 ············································································· 32 5.1.2 程序实现(Startup.s Version-1.0) ······································································· 32 5.1.3 指定加载方式让程序跑起来 ················································································· 33 5.1.4 Version-1.0 的缺点 ································································································· 33 5.2 一个实用的启动代码(Startup.s Version-2.0)···························································· 33 5.2.1 设计描述················································································································· 33 5.2.2 程序实现(Startup.s) ··························································································· 34 5.2.3 向量表 Vectors.h 和 Vectors.c ················································································ 34 5.2.4 指定加载方式 ········································································································· 37 5.1

ii

第1章 CMSIS 标准
1.1 CMSIS 标准简介

ARM 公司于 2008 年 11 月 12 日发布了 ARMCortex 微控制器软件接口标准 CMSIS 1.0。 CMSIS 是独立于供应商的 Cortex-M 处理器系列硬件抽象层,为芯片厂商和中间件供应商提供 了简单的处理器软件接口,简化了软件复用工作,降低了 Cortex-M 上操作系统的移植难度,并 减少了新入门的微控制器开发者的学习曲线和新产品的上市时间。 根据近期的调查研究,软件开发已经被嵌入式行业公认为最主要的开发成本。因此,ARM 与 Atmel、IAR、KEIL、Luminary Micro、Micrium、NXP、SEGGER 和 ST 等诸多芯片和软件 工具厂商合作,将所有 Cortex 芯片厂商的产品的软件接口标准化,制定了 CMSIS 标准。此举 意在降低软件开发成本,尤其针对进行新设备项目开发或将已有的软件移植到其他芯片厂商提 供的基于 Cortex 处理器的微控制器的情况。有了该标准,芯片厂商就能够将他们的资源专注于 对其产品的外设特性进行差异化,并且能够消除对微控制器进行编程时需要维持的不同的、互 相不兼容的标准的需求,从而达到降低开发成本的目的。

1.2

CMSIS 标准的软件架构

图 1.1 基于 CMSIS 标准的软件架构

从图 1.1 基于 CMSIS 标准的软件架构中可以看到,基于 CMSIS 标准的软件架构主要分为 以下四层:用户应用层,操作系统层,CMSIS 层以及硬件寄存器层。其中 CMSIS 层起着承上 启下的作用, 一方面该层对硬件寄存器层进行了统一的实现, 屏蔽了不同厂商对 Cortex-M 系列 微处理器核内外设寄存器的不同定义,另一方面又向上层的操作系统和应用层提供接口,简化 了应用程序开发的难度,使开发人员能够在完全透明的情况下进行一些应用程序的开发。也正 是如此,CMSIS 层的实现也相对复杂,下面将对 CMSIS 层次结构进行剖析。 CMSIS 层主要分为以下 3 个部分:

1

l

l

l

l l l

核内外设访问层(CPAL,Core Peripheral Access Layer) :该层由 ARM 负责实现包括 对寄存器名称、地址的定义,对核寄存器、NVIC、调试子系统的访问接口定义以及 对特殊用途寄存器的访问接口(例如:CONTROL,xPSR)定义。由于对特殊寄存 器的访问以内联方式定义,所以针对不同的编译器 ARM 统一用__INLINE 来屏蔽差 异。该层定义的接口函数均是可重入的。 片上外设访问层 (DPAL, Device Peripheral Access Layer) 该层由芯片厂商负责实现。 : 该层的实现与 CPAL 类似,负责对硬件寄存器地址以及外设访问接口进行定义。该 层可调用 CPAL 层提供的接口函数同时根据设备特性对异常向量表进行扩展,以处 理相应外设的中断请求。 外设访问函数(AFP , Access Functions for Peripherals) :该层也由芯片厂商负责实现, 主要是提供访问片上外设的访问函数,这一部分是可选的。 对一个 Cortex-M 微控制系统而言,CMSIS 通过以上三个部分实现了: 定义了访问外设寄存器和异常向量的通用方法; 定义了核内外设的寄存器名称和核异常向量的名称; 为 RTOS 核定义了与设备独立的接口,包括 Debug 通道。

这样芯片厂商就能专注于对其产品的外设特性进行差异化,并且消除他们对微控制器进 行编程时需要维持的不同的、互相不兼容的标准需求,以达到低成本开发的目的。

1.3

CMSIS 标准的文件结构

图 1.2 CMSIS 的文件基本组织结构

如上图 5.2 中 core_cm<x>.c 以及 core_cm<x>.h 中位内核外设访问层, 其中定义了内核中的 外设地址以及一些内核的访问及控制函数。 Startup_<device>.s 文件是系统的启动文件,其包括堆和栈的初始化配置、中断向量表的配 置以及将程序引导到 main()函数等功能。 而 system_<device>.h 和 system_<device>.c 文件则是由 ARM 公司提供模板, 各芯片制造商 根据自己芯片的特点来编写的。

2

第2章 注解 startup.s 文件(以 startup_LPC17xx.s 为例)
下面以 startup_LPC17xx.s 为例,简单看一下 CMSIS 中的启动代码完成了哪些工作。 主要完成了三项工作: l l l 堆栈以及堆的初始化; 定位中断向量表; 调用 Reset Handler。

2.1

堆栈以及堆的初始化

2.1.1 堆栈的初始化
程序清单 2.1 startup_LPC17xx.s 中的堆栈初始化代码 Stack_Size EQU 0x00000200

AREA Stack_Mem __initial_sp SPACE

STACK, NOINIT, READWRITE, ALIGN=3 Stack_Size

Stack_Size EQU 0x00000200,这个语句相当于 Stack_Size 这个标号(标号:链接器的术语, 下文中提到的所有“标号” ,指的都是链接器中的标号)等于 0x00000200,相当于 C 语言中的 #define Stack_Size 0x00000200,也就是说此语句只是一个声明,并未分配地址。 AREA STACK, NOINIT, READWRITE, ALIGN=3,此语句定义了一个名叫 STACK 的代码 段,并指明 8 字节对齐(ALIGN=3) 。 Stack_Mem SPACE Stack_Size,为 Stack_Mem 分配 Stack_Size 大小的一块内存区域,注意 这里分配的是 RAM。
__initial_sp

此标号有一层隐含的意思那就是在 M3 中堆栈是满递减堆栈,因为它指定了堆栈指针位于 堆栈的高地址(在 Stack_Mem 之后) ,具体如下图所示。

图 2.1 堆栈指针 SP 位置

上图来自一个 LPC1700 工程的.map 文件。可以看出栈的起始地址为 0x10001f70,大小为 512 字节(即 0x00000200 = Stack_Size) 。而堆栈指针的位置在 0x10002170,其等于栈的起始地 址 0x10001f70 + 0x00000200,说明 LPC1700 系列的 Cortex-M3 微控制器的堆栈为满递减堆栈。 2.1.2 堆的初始化
程序清单 2.2 startup_LPC17xx.s 中的堆初始化代码 Heap_Size EQU 0x00000200

AREA

HEAP, NOINIT, READWRITE, ALIGN=3

3

__heap_base Heap_Mem __heap_limit SPACE Heap_Size

具体过程与栈的初始化相同。

2.2

中断向量表的初始化
程序清单 2.3 中断向量表的初始化代码 PRESERVE8 THUMB

AREA

RESET, DATA, READONLY

EXPORT __Vectors

__Vectors

DCD DCD DCD DCD DCD DCD DCD DCD DCD DCD

__initial_sp Reset_Handler NMI_Handler HardFault_Handler MemManage_Handler BusFault_Handler UsageFault_Handler 0 0 0

; Top of Stack ; Reset Handler ; NMI Handler ; Hard Fault Handler ; MPU Fault Handler ; Bus Fault Handler ; Usage Fault Handler ; Reserved ; Reserved ; Reserved

......(以下代码省略)

PRESERVE8 指定了以下的代码位 8 字节对齐,这是 keil 编译器的一个编程要求,对齐情 况如下图所示:

图 2.2 .list 文件中的 8 字节对齐示意图

THUMB 指定了接下来的代码为 THUMB 指令集。 AREA RESET, DATA, READONLY,此语句声明 RESET 数据段。
4

EXPORT __Vectors,导出向量表标号,EXPORT 作用类似于 C 语言中的 extern。之后的代 码就是为向量表分配存储区域。

2.3

调用 Reset Handler
程序清单 2.4 调用 Reset Handler 的代码

Reset_Handler

PROC EXPORT IMPORT LDR BX ENDP Reset_Handler __main R0, =__main R0 [WEAK]

此段代码只完成了一个功能,引导程序进入__main。__main 的具体行为在第三章中有具体 的描述。

2.4

其他的代码
程序清单 2.5 CRP 加密级别 IF AREA : LNOT::DEF:NO_CRP |.ARM.__at_0x02FC|, CODE, READONLY 0xFFFFFFFF

CRP_Key

DCD ENDIF

此段代码指定了接下来的代码存储与 0x02FC 的地址,具体情况如下图所示。

这段代码是 NXP 公司的 LPC1700 系列的 MCU 特有的一段代码,其他公司的 Cortex-M3 MCU 的启动程序是没有这段代码的。 这段代码是指定 LPC1700 的 CRP 加密级别的代码段,芯片上电后会自动读取这一地址的 值以确定加密方式,其中 CRP_Key = 0xffffffff 为不加密(0 级加密) ,CRP_Key = 0x12345678 为 1 级加密,CRP_Key = 0x87654321 为 2 级加密,CRP_Key = 0x43218765 为 3 级加密(最高 级加密) 级加密将会禁止所有的 ISP 指令,也就是说,芯片将不能读写、不能擦除。 ,3
程序清单 2.6 具体的堆栈以及堆的初始化行为 ; User Initial Stack & Heap IF :DEF:__MICROLIB

EXPORT EXPORT EXPORT

__initial_sp __heap_base __heap_limit

ELSE

IMPORT

__use_two_region_memory 5

EXPORT __user_initial_stackheap LDR LDR LDR LDR BX

__user_initial_stackheap

R0, = Heap_Mem R1, =(Stack_Mem + Stack_Size) R2, = (Heap_Mem + Heap_Size) R3, = Stack_Mem LR

关于此段代码将在第三章有具体叙述,在此略过。

6

第3章 ARM 芯片的启动过程详解
“ARM 程序” 是指在 ARM 系统中正在执行的程序, 而非保存在 ROM 中的 bin 映像 (image) 文件。这一点清注意区别。 一个 ARM 程序包含 3 部分:RO,RW 和 ZI l l l RO 就是只读数据,是程序中指令和常量; RW 是可读写的数据,程序中已初始化变量; ZI 是程序中未初始化的变量和初始化为 0 的变量。 RO 就是 readonly; RW 就是 read/write; ZI 就是 zero。

由以上 3 点说明可以理解为: l l l

3.1

ARM 芯片的启动过程概述

图 3.1 ARM 芯片的启动过程详解

注意,以上的过程并非绝对的,不同的 ARM 架构或是不同的代码以上的执行过程是不同 的。 复位处理程序是在汇编器中编写的短模块,系统一启动就立即执行。复位处理程序最少要 为应用程序的运行模式初始化堆栈指针。对于具有本地内存系统(如缓存、TCM、MMU 和 MPU)的处理器,某些配置必须在初始化过程的这一阶段完成。复位处理程序在执行之后,通 常跳转到__main 以开始 C 库初始化序列。 __main 负责设置内存,而__rt_entry 负责设置运行时环境。__main 执行代码和数据复制、 解压缩以及 ZI 数据的零初始化。然后,它跳转到__rt_entry,设置堆栈和堆、初始化库函数和 静态数据,并调用任何顶级 C++构造函数。然后,__rt_entry 跳转到应用程序的入口 main()。主 应用程序结束执行后,__rt_entry 将库关闭,然后把控制权交还给调试器。函数标签 main()具有
7

特殊含义。main()函数的存在强制链接器链接到__main 和__rt_entry 中的初始化代码。如果没有 标记为 main()的函数,则没有链接到初始化序列,因而部分标准 C 库功能得不到支持。

3.2

结合代码来看 ARM 芯片的启动过程

3.2.1 调试环境的搭建及测试代码 使用 keil for ARM(uVision4)的软件模拟,工程的硬件设定为 LPC1700 系列,不使用 microlib。 这里不使用 microlib, 则系统自动加载标准 C Library, 这样我们才能看到标准的 ARM 芯片的标准启动过程。随后我们会对 microlib 进行探讨。 测试代码如下:
程序清单 3.1 启动过程测试代码 #include "LPC17xx.h" /* LPC17xx 外设寄存器 */

int main (void) { SystemInit(); while (1) { /* 系统初始化,切勿删除 */

} }

我们的测试代码使用的是一段最简单的代码, 代码本身只包含 CMSIS 标准的必备文件, 即 stdint.h、core_cm3.h、core_cm3.c、system_LPC17xx.h、system_LPC17xx.c、LPC17xx.h、 startup_LPC17xx.s 和 main.c。 3.2.2 跟踪启动代码 开始调试之前,须将工程设置的 DEBUG 栏中取消掉 Run to main()的勾选,否则代码会直 接运行到 main()函数,我们也就无法看到芯片的启动过程了。 然后启动调试,最先进入 Reset_Handler,如下图所示。

图 3.2 Reset_Handler

继续单步运行,程序跳入__main(C Library 的代码,并非用户代码) ,如下图所示。

8

图 3.3 __main

继续单步运行,经过一系列的代码(主要是 scatter load 过程)后,程序进入__rt_entry(同 样是由 C Library 管理,并非用户代码) 。

图 3.4 __rt_entry

继续单步运行, 再次经过一系列的代码之后 (主要是堆栈和堆的初始化以及 C Library 的初 始化) ,程序进入 main()。如下图所示。

图 3.5 进入 main 函数

以上是整个测试代码启动过程的跟踪调试的大致过程,这个过程对 ARM 系列的芯片来说 都是相同的,不同的是里面具体的细节。 3.2.3 详细的启动过程 使用的依然是上面的测试代码,详细启动过程如下图所示。

9

图 3.6 芯片启动的详细过程

上图显示的就是测试代码在 LPC17xx 上启动运行的全过程(详细过程) ,由此图可见 LPC17xx 的启动过程与图 3.1 所示的启动过程是基本一致的,但是还有差别,可以说是图 3.1 所示启动过程的简化版。 注意:并非所有代码的启动过程全部相同,此启动过程与所使用的链接器、用户代码以及 其所集成的 C Library 密切相关。此图为由“armlink”的链接器为程序清单 3.2 所示的测试代 码产生的启动过程,其他情况可能会有一些差异(比如有的代码的启动过程就会运行一段 RW 段的解压代码等,而本例程中则没有) 。 3.2.4 __main 若程序使用的是 C 或 C++语言编写的代码,那么 C/C++程序的入口是在 C Library 中的 __main。库代码在此处执行以下操作。 l 1 将非根(RO 和 RW)执行区从其加载地址复制到执行地址(这里的根区指的就是 __main 和__rt_entry) 。另外,如果压缩了任何数据节,则会将它们从加载地址解压 缩到执行地址(此数据压缩及解压缩过程并不是对所有代码都会执行) ; 将 ZI 区清零; 跳转到__rt_entry。

l l

3.2.5 __rt_entry __rt_entry 符号是使用 ARM C 库的程序的起点。 将所有分散加载区重定位到其执行地址后, 会将控制权传递给__rt_entry。其有如下缺省实现: l l l 设置堆和堆栈。 调用__rt_lib_init 以初始化 C Library。 调用 main()。
10

l l

调用__rt_lib_shutdown 以关闭 C Library。 退出。

注意:最后两步是在程序退出 main()函数时才会执行,而嵌入式程序一般都是死循环,所 以基本不会执行这两个过程。还有,以上过程是对标准 C Library 而言,不包括使用 microlib 的情况。 3.2.6 __rt_lib_init 这是库初始化函数,它与__rt_lib_shutdown()配合使用。 这是库初始化函数。它是紧靠__rt_stackheap_init()后面调用的,并传递一个要用作堆的初 始内存块。此函数是标准 ARM 库初始化函数,不能重新实现此函数。

3.3

关于 microlib

microlib 是缺省 C 库的备选库。它旨在与需要装入到极少量内存中的深层嵌入式应用程序 配合使用。这些应用程序不在操作系统中运行。microlib 进行了高度优化以使代码变得很小。 它的功能比缺省 C 库少,并且根本不具备某些 ISO C 特性。某些库函数的运行速度也比较慢, 例如,memcpy()。 microlib 与缺省 C 库之间的主要差异是: l l l l l l l l l l l l microlib 不符合 ISO C 库标准。 不支持某些 ISO 特性, 并且其他特性具有的功能也较 少; microlib 不符合 IEEE 754 二进制浮点算法标准; microlib 进行了高度优化以使代码变得很小; 无法对区域设置进行配置。缺省 C 区域设置是唯一可用的区域设置; 不能将 main()声明为使用参数,并且不能返回内容; 不支持 stdio,但未缓冲的 stdin、stdout 和 stderr 除外; microlib 对 C99 函数提供有限的支持; microlib 不支持操作系统函数; microlib 不支持与位置无关的代码; microlib 不提供互斥锁来防止非线程安全的代码; microlib 不支持宽字符或多字节字符串; 与 stdlib 不同,microlib 不支持可选择的单或双区内存模型。microlib 只提供双区内 存模型,即单独的堆栈和堆区。

3.4

x.map

想要更好的了解启动代码的运行机制,我们就有必要了解一下由 Keil 的链接器“armlink” 生成的描述文件即 x.map 文件。

11

3.4.1 关于链接器

图 3.7 目标文件的组成

上图即是 armlink 的链接器为程序清单 3.1 所示的测试代码生成的.map 文件中的一部分, 其描述了镜像文件的组成信息,其中可以明显的看到其由两部分构成: l l 由 User Code 生成的目标文件; 由 C Library 生成的目标文件。

可见我们在上文中所描述的启动过程中看到的__main、__rt_entry、__scatterload 以及 __rt_lib_init 等,就是 C Library 中的代码。 同理,我们每次烧录的可执行的 ARM 的 bin 文件中不仅有开发者编写的代码,还有 C Library 的代码。 3.4.2 RW 段在 RAM 的存放

图 3.8 由 armlink 生成的 RW 段在 RAM 中存放的描述

3.5

关于 ARM 程序的 Memory 管理

3.5.1 ARM 镜像文件的组成(image) 所谓 ARM 映像文件就是指烧录到 ROM 中的 bin 文件,也成为 image 文件。以下用 Image 文件来称呼它。Image 文件包含了 RO 和 RW 数据(注意:不包含 ZI 数据) 。之所以 Image 文
12

件不包含 ZI 数据,是因为 ZI 数据都是 0,没必要包含,只要程序运行之前将 ZI 数据所在的区 域(执行区域)一律清零即可。包含进去反而浪费存储空间。 3.5.2 关于 image 文件(镜像文件) 从以上两点可以知道, 烧录到 ROM 中的 image 文件与实际运行时的 ARM 程序之间并不是 完全一样的。 因此就有必要了解 ARM 程序是如何从 ROM 中的 image 到达实际运行状态的。 实 际上,RO 中的指令(启动程序)至少应该有这样的功能: l l 将 RW 从 ROM 中搬到 RAM 中,因为 RW 是变量,变量不能存在 ROM 中。 将 ZI 所在的 RAM 区域全部清零,因为 ZI 区域并不在 Image 中,所以需要程序根据 编译器给出的 ZI 地址及大小来将相应得 RAM 区域清零。ZI 中也是变量,同理:变 量不能存在 ROM 中。

在程序运行的最初阶段,RO 中的指令(启动程序)完成了这两项工作后(也就是分散加 载的过程)C 程序才能正常访问变量。否则只能运行不含变量的代码。说了这么多可能还是有 些迷糊,RO,RW 和 ZI 到底是什么,下面我将给出几个例子,最直观的来说明 RO,RW,ZI 在 C 语言中的含义。 3.5.3 RO 段 看下面两段程序,它们之间差了一条语句,这条语句就是声明一个字符常量。因此按照之 前的内容,它们之间应该只会在 RO 数据中相差一个字节(字符常量为 1 字节) 。
Prog1: #include <stdio.h> void main(void) { ; }

Prog2: #include <stdio.h> const char a = 5; void main(void) { ; }

Prog1 编译出来后的信息如下(来自.map 文件) : ============================================================== Code 948 RO Data 60 RW Data 0 ZI Data 96 Debug 0 Grand Totals

============================================================== Total RO Size(Code + RO Data) Total RW Size(RW Data + ZI Data) Total ROM Size(Code + RO Data + RW Data) 1008 96 1008 ( 0.98kB) ( 0.09kB) ( 0.98kB)

==============================================================
13

Prog2 编译出来后的信息如下(来自.map 文件) : ============================================================== Code 948 RO Data 61 RW Data 0 ZI Data 96 Debug 0 Grand Totals

============================================================== Total RO Size(Code + RO Data) Total RW Size(RW Data + ZI Data) 1009 96 ( 0.99kB) ( 0.09kB) ( 0.99kB)

Total ROM Size(Code + RO Data + RW Data) 1009

============================================================== 以上两个程序编译出来后的信息可以看出:Prog1 和 Prog2 的 RO 包含了 Code 和 RO Data 两类数据。他们的唯一区别就是 Prog2 的 RO Data 比 Prog1 多了 1 个字节。这正和之前的推测 一致。如果增加的是一条指令而不是一个常量,则结果应该是 Code 数据大小有差别。 3.5.4 RW 段 同样再看两个程序,它们之间只相差一个“已初始化的变量” ,按照之前所讲的,已初始化 的变量应该是算在 RW 中的,所以两个程序之间应该是 RW 大小有区别。
Prog3: #include <stdio.h> void main(void) { ; }

Prog4: #include <stdio.h> char a = 5; void main(void) { ; }

Prog3 编译出来后的信息如下(来自.map 文件) : ============================================================== Code 948 RO Data 60 RW Data 0 ZI Data 96 Debug 0 Grand Totals

============================================================== Total RO Size(Code + RO Data) Total RW Size(RW Data + ZI Data) Total ROM Size(Code + RO Data + RW Data) 1008 96 1008 ( 0.98kB) ( 0.09kB) ( 0.98kB)

============================================================== Prog4 编译出来后的信息如下(来自.map 文件) :
14

============================================================== Data 948 RO Data 60 RW Data 1 ZI Data 96 Debug 0 Grand Totals

============================================================== Total RO Size(Code + RO Data) Total RW Size(RW Data + ZI Data) Total ROM Size(Code + RO Data + RW Data) 1008 97 1009 ( 0.98kB) ( 0.09kB) ( 0.99kB)

============================================================== 以上两个程序编译出来后的信息可以看出:Prog1 和 Prog2 的 RO 包含了 Code 和 RO Data 两类数据。他们的唯一区别就是 Prog2 的 RO Data 比 Prog1 多了 1 个字节。这正和之前的推测 一致。如果增加的是一条指令而不是一个常量,则结果应该是 Code 数据大小有差别。 3.5.5 ZI 段(初始化为 0 或未初始化的变量) 再看两个程序,他们之间的差别是一个未初始化的变量“a” ,从之前的了解中,应该可以 推测,这两个程序之间应该只有 ZI 大小有差别。
Prog3: #include <stdio.h> void main(void) { ; }

Prog4: #include <stdio.h> char a; void main(void) { ; }

Prog3 编译出来后的信息如下(来自.map 文件) : ============================================================== Code 948 RO Data 60 RW Data 0 ZI Data 96 Debug 0 Grand Totals

============================================================== Total RO Size(Code + RO Data) Total RW Size(RW Data + ZI Data) 1008 96 ( 0.98kB) ( 0.09kB) ( 0.98kB)

Total ROM Size(Code + RO Data + RW Data) 1008 Prog4 编译出来后的信息如下(来自.map 文件) :

==============================================================

==============================================================
15

Code 948

RO Data 60

RW Data 0

ZI Data 97

Debug 0 Grand Totals

============================================================== Total RO Size(Code + RO Data) Total RW Size(RW Data + ZI Data) 1008 97 ( 0.98kB) ( 0.09kB) (0.98kB)

Total ROM Size(Code + RO Data + RW Data) 1008

============================================================== 编译的结果完全符合推测, 只有 ZI 数据相差了 1 个字节。 这个字节正是未初始化的一个字 符型变量“a”所引起的。 注意:如果一个变量被初始化为 0,则该变量的处理方法与未初始化华变量一样放在 ZI 区域。即:ARM C 程序中,所有的未初始化变量都会被自动初始化为 0。 以上代码是再 ADS 下编译的,keil 环境下与之不同,比如在 keil 下生成 ZI 数据段就必须 定义一个大于 8 字节的未初始化或初始化位 0 的变量,且必须在源代码中引用此变量才会在链 接的描述文件中看到其生成的 ZI 文件。

3.6

缺省内存映射
对于没有描述内存映射的映像,链接器根据缺省内存映射放置代码和数据。如下图所示。

图 3.9 缺省内存映射

创建一个可以在其中执行 C 或 C++程序的环境。这包括: l l l l l 创建一个堆栈; 创建一个堆(如果需要) ; 初始化程序所用的库的部分组成内容; 调用 main()以开始执行程序; 支持程序使用 ISO 定义的函数;

16

l

捕获运行时错误和信号,如果需要,还可以在出现错误或程序退出时终止执行。

注意:这个内存映射并非对所有芯片都有效,不同的芯片的内存映射是不同的。在 Cortex-M3 中的内存映射与上图是一致的。

3.7

内存模型
在 Keil for ARM 下你可以选择以下任意内存模型: 1. 单内存区

堆栈从内存区顶部向下增长。堆从内存区底部向上增长。这是缺省设置。由堆管理的内存 从来不会缩减。不能将通过调用 free()释放的堆内存再次用于其他用途。 2. 双内存区 一个内存区用于堆栈,另一个内存区用于堆。堆区大小可以是零。堆栈区可以位于分配的 内存中,也可以从执行环境中继承。要使用双区模型而不是缺省的单区模型,请使用以下任一 方法: l 汇编语言中的 IMPORT __use_two_region_memory; l C 中的#pragma import(__use_two_region_memory)。 例如下图所示,此代码来自 startup_LPC17xx.s。

如果使用双区内存模型,并且未提供任何堆内存,则无法调用 malloc()、使用 stdio 或获取 main()的命令行参数。如果将堆区大小设置为 0,并且将__user_heap_extend()定义为可扩展堆的 函数,则会在需要时创建堆。

17

第4章 关于分散加载文件
4.1 什么时候使用分散加载文件
l l 存在复杂的地址映射:例如代码和数据需要分开放在在多个区域。 存在多种存储器类型:例如包含 Flash、ROM、SDRAM 快速 SRAM。我们根据代码 与数据的特性把他们放在不同的存储器中,比如中断处理部分放在快速 SRAM 内部 来提高响应速度,而把不常用到的代码放到速度比较慢的 Flash 内。 函数的地址固定定位:可以利用 Scatter file 实现把某个函数放在固定地址,而不管 其应用程序是否已经改变或重新编译。 利用符号确定堆与堆栈: 内存映射的 IO:采用 scatter file 可以实现把某个数据段放在精确的地指处。

l l l

因此对于嵌入式系统来说 scatter file 是必不可少的,因为嵌入式系统采用了 ROM,RAM, 和内存映射的 IO。通过使用分散加载机制,可以使用文本文件中的描述为链接器指定映像的内 存映射。 分散加载为您提供了对映像组件分组和位置的全面控制。 分散加载可以用于简单映像, 但它通常仅用于具有复杂内存映射的映像,即多个区在加载和执行时分散在内存映射中。

4.2

适用范围

有时候用户希望将不同代码放在不同存储空间,也就是通过编译器生成的映像文件需要包 含多个域,每个域在加载和运行时可以有不同的地址。要生成这样的映像文件,必须通过某种 方式告知编译器相关的地址映射关系。 Scattter loading 是 ARM 连接器(armlink)提供的一种机制,该机制让你能够把可执行映像 文件分成多个区域,然后分别为它们指定在存储器上的存储位置。Scatter loading 机制用于定位 装载区域和运行区域在分离的存储映像中的位置。

4.3

再谈 ARM Image(镜像文件)

ARM 中的源文件经过编译器编译生成的目标文件.obj(Object file)和相应的 C/C++运行时 库(Runtime Library)经过连接器的处理后,生成 ELF 格式的映像文件(image)——它可以被 写入目标设备的 ROM 中直接运行或加载后运行。 镜像文件的组成如图所示。

18

图 4.1 镜像文件组成示意图

可执行文件由映像、区、输出节和输入节的层次结构构成: l l l l l l 映像由一个或多个区组成。每个区由一个或多个输出节组成。 每个输出节包含一个或多个输入节。 输入节是对象文件中的代码和数据信息。 输入节:输入节包含代码、初始化数据,或描述未初始化的或在映像执行前必须设 为 0 的内存片断。这些特性通过 RO、RW 和 ZI 这样的属性来表示。 输出节:一个输出节由若干个具有相同 RO、RW 或 ZI 属性的相邻输入节组成。输 出节的属性与组成它的输入节的属性相同。 区:一个区由一个、两个或三个相邻的输出节组成。区中的输出节根据其属性排序。 首先是 RO 输出节,然后是 RW 输出节,最后是 ZI 输出节。区通常映射到物理内存 设备,如 ROM、RAM 或外围设备。

关于 RO、RW 以及 ZI 数据段的详细内容在前文中已有叙述,在此就不在详述。 ARM 映像文件各组成部分在存储系统中的地址有两种: 1. 装载区域 程序在装载之后、运行之前,所占有的存储区域能够被分成多个装载区域,每个装载区域 就是一个连续的字节块。 2. 运行区域 程序在运行时所占有的存储区域能够被分成多个装载区域,每个装载区域也是一个连续的 字节块。 注意:一个装载区域可能包含多个运行区域;一个运行区域只能属于一个装载区域。

4.4

节放置

链接器根据输入节的属性在一个区内对它们进行排序。具有相同属性的输入节在区内形成 相邻块。 每个输入节的基址由链接器定义的排序顺序确定, 并且在包含它的输出节中正确对齐。 通常,生成映像时链接器按以下顺序对输入节进行排序: l l l 按属性。 按输入节名称。 按其在输入列表中的位置,除非由 FIRST 或 LAST 覆盖。
19

注意:此排序顺序不受分散加载描述文件或对象文件名中的排序的影响。如果执行区包含 4MB 的 Thumb 代码、16M 的 Thumb-2 代码或超过 32MB 的 ARM 代码,则链接器可能会更改 排序顺序,从而将长跳转中间代码的数量减至最小。在缺省情况下,链接器创建由 RO、RW 和 可选的 ZI 输出节组成的映像。RO 输出节在具有内存管理硬件的系统上运行时可以受到保护。 RO 节也可以放在目标的 ROM 中。 部分映像集合在一起,形成最小数量的相邻区。armlink 按如下属性对输入节排序: 1. 只读代码 2. 只读数据 3. 读写代码 4. 读写数据 5. 零初始化数据 具有相同属性的输入节按名称排序。名称是区分大小写的,并且使用 ASCII 字符排列顺序,对 名称按字母顺序进行比较。属性和名称相同的输入节根据它们在输入文件中的相对位置进行排 序。这些规则意味着库中所含属性和名称相同的输入节的位置不可预判。如果需要更精确的定 位,可以显式指定各个模块并将这些模块包含在输入列表中。

4.5

一个简单的加载过程

在一个简单的嵌入式计算机系统中,存储器一般被分成 ROM 和 RAM。连接器生成的映像 被分成“Read-Only”段和“Read-Write”段(包含已初始数据和未初始化数据,未初始化数据 也叫 ZI 数据) 。通常,在程序下载(烧入)的时候,它们会被一块下载到 ROM 上;而在程序 开始执行时,Read-Write 段会从 ROM 被 Copy 到 RAM。 如下图所示:

图 4.2 加载过程描述示意图

以上是一个简单的例子,但在复杂的嵌入式系统中,其存储器往往包括 ROM,SRAM, DRAM,FLASH 等等,加载过程比如上实例要复杂一些。

20

4.6

分散加载文件语法

分散加载文件主要由一个加载时域(区)和多个运行时域(区)组成,其大致结构如下图 所示。

图 4.3 分散加载文件的语法结构

4.6.1 加载时域(区)描述 加载时域语法格式如程序清单 4.1 所示,其每项含义解释如下。
程序清单 4.1 加载时域描述语法描述 load_region_name(base_address|("+"offset))[attribute_list][max_size] { execution_region_description+ }

l l

load_region_name:为本加载时域的名称,名称可以按照用户意愿自己定义,该名称中 只有前 31 个字符有意义; base_designator:用来表示本加载时域的起始地址,可以有下面两种格式中的一种: u base_address:表示本加载时域中的对象在连接时的起始地址,地址必须是字对齐 的; u +offset:表示本加载时域中的对象在连接时的起始地址是在前一个加载时域的结 束地址后偏移量 offset 字节处。本加载时域是第一个加载时域,则它的起始地址即为 offset,offset 的值必须能被 4 整除。

l

attribute_list:指定本加载时域内容的属性,包含以下几种,默认加载时域的属性是 ABSOLUTE。 u ABSOLUTE:绝对地址; u PI:与位置无关; u RELOC:可重定位; u OVERLAY:覆盖; u NOCOMPRESS:不能进行压缩。

l

max_size:指定本加载时域的最大尺寸。如果本加载时域的实际尺寸超过了该值,连
21

接器将报告错误,默认取值为 0xFFFFFFFF; l execution_region_description:表示运行时域,后面有个+号,表示其可以有一个或者多 个运行时域,关于运行时域的介绍请看后面。

注:程序清单 4.1 中’|’、 ‘[]’符号的使用请参考 BNF 语法。

4.6.2 运行时域(区)描述 运行时域语法格式如程序清单 4.2 所示,其每项含义解释如下。
程序清单 4.2 运行时域语法描述 exec_region_name(base_address|"+"offset)[attribute_list][max_size|" "length] { input_section_description* }

l l

exec_region_name:为为本加载时域的名称,名称可以按照用户意愿自己定义,该名 称中只有前 31 个字符有意义; base_address:用来表示本加载时域的起始地址,可以有下面两种格式中的一种: u base_address:表示本加载时域中的对象在连接时的起始地址,地址必须是字对齐 的; u +offset:表示本加载时域中的对象在连接时的起始地址是在前一个加载时域的结 束地址后偏移量 offset 字节处,offset 的值必须能被 4 整除。

l

attribute_list:指定本加载时域内容的属性: u ABSOLUTE:绝对地址; u PI:与位置无关; u RELOC:可重定位; u OVERLAY:覆盖; u FIXED:固定地址。区加载地址和执行地址都是由基址指示符指定的,基址指示 符必须是绝对基址,或者偏移为 0。 u ALIGNalignment:将执行区的对齐约束从 4 增加到 alignment。alignment 必须为 2 的正数幂。如果执行区具有 base_address,则它必须为 alignment 对齐。如果执行区具 有 offset,则链接器将计算的区基址与 alignment 边界对齐; u EMPTY:在执行区中保留一个给定长度的空白内存块,通常供堆或堆栈使用。 u ZEROPAD:零初始化的段作为零填充块写入 ELF 文件,因此,运行时无需使用 零进行填充; u PADVALUE:定义任何填充的值。如果指定 PADVALUE,则必须为其赋值; u NOCOMPRESS:不能进行压缩; u UNINIT:未初始化的数据。

l l

max_size:指定本加载时域的最大尺寸。如果本加载时域的实际尺寸超过了该值,连 接器将报告错误,默认取值为 0xFFFFFFFF; length: 如果指定的长度为负值, 则将 base_address 作为区结束地址。 它通常与 EMPTY 一起使用,以表示在内存中变小的堆栈。

22

l

input_section_description:指定输入段的内容。

4.6.3 输入段描述 输入段语法描述如程序清单 4.3 所示。
程序清单 4.3 输入段语法描述 module_select_pattern [ "(" input_section_selector ( "," input_section_selector )* ")" ] ("+" input_section_attr | input_section_pattern | input_symbol_pattern)

l

module_select_pattern:目标文件滤波器,支持使用通配符“*”与“?” 。其中符号“*” 代表零个或多个字符,符号“?”代表单个字符。进行匹配时所有字符不区分大小写。 当 module_select_pattern 与以下内容之一相匹配时,输入段将与模块选择器模式相匹 配: u 包含段和目标文件的名称; u 库成员名称(不带前导路径名) ; u 库的完整名称(包括路径名) 。如果名称包含空格,则可以使用通配符简化搜索。 例如,使用*libname.lib 匹配 C:\lib dir\libname.lib。

l

nput_section_attr:属性选择器与输入段属性相匹配。每个 input_section_attr 的前面有 一个“+”号。如果指定一个模式以匹配输入段名称,名称前面必须有一个“+”号。 可以省略紧靠“+”号前面的任何逗号。选择器不区分大小写。可以识别以下选择器: u RO-CODE; u RO-DATA; u RO,同时选择 RO-CODE 和 RO-DATA; u RW-DATA; u RW-CODE; u RW,同时选择 RW-CODE 和 RW-DATA; u ZI; u ENTRY:即包含 ENTRY 点的段。

可以识别以下同义词: u CODE 表示 RO-CODE; u CONST 表示 RO-DATA; u TEXT 表示 RO; u DATA 表示 RW; u BSS 表示 ZI。 可以识别以下伪属性: u FIRST; u LAST。 通过使用特殊模块选择器模式.ANY 可以将输入段分配给执行区,而无需考虑其父模块。 可以使用一个或多个.ANY 模式以任意分配方式填充运行时域。 在大多数情况下, 使用单个.ANY 等效于使用*模块选择器。

23

4.7

实战测试(一个简单的例程)
程序清单 4.4 Keil 默认的分散加载描述文件代码

; ************************************************************* ; *** Scatter-Loading Description File generated by uVision ; *************************************************************

LR_IROM1 0x00000000 0x00040000 { ER_IROM1 0x00000000 0x00040000 *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) }

; load region size_region { ; load address = execution address

RW_IRAM1 0x10000000 0x00008000 { ; RW data .ANY (+RW +ZI) } }

程序清单 4.5 所示的即为 keil 默认的分散加载文件,加载结果如下图所示。

图 4.4 加载结果图

这仅仅只是一个片段,不过可以看出其与上文的分散加载代码的描述是相吻合的。现在按 照代码清单 4.5 修改代码,看看是不是 RO 段的加载区域会改变。
程序清单 4.5 测试代码 LR_IROM1 0x00010000 0x00040000 { ER_IROM1 0x00010000 0x00040000 *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } 24 ; load region size_region { ; load address = execution address

RW_IRAM1 0x10000000 0x00008000 { ; RW data .ANY (+RW +ZI) } }

按照上面对分散加载文件的介绍可知, 这段代码运行后, 段的加载地址将由 0x00000000 RO 变为 0x00010000,下面让我们实际操作一下,看看我们预想的是否正确。 要想直接执行这段代码是行不通的,正常的 Keil 编译器默认是不会自动执行.sct(分散加 载描述)文件的,需要按下图所示修改相应的链接器配置。

图 4.5 需要修改的 keil 配置示意图

这样我们编写的分散加载文件就可以运行了。 重新链接工程, 再次查看 LPC1700.map 文件, 如下图所示。

25

图 4.6 再次加载结果示意图

与图 4.5 对比,从图中我们可以明显看出程序加载的基址变为了 0x00010000,这与我们编 写的分散加载代码的内容一致,说明代码成功运行,结果和我们预想的一样。

4.8

实战分散加载
假设,一个 Cortex-M3 内核的 LPC17xx 微控制器有 Flash、RAM 的资源如下: l l Flash 基址:0x00000000,大小:256 KByte; RAM 基址:0x10000000,大小:32 Kbyte。

那这一个分散加载文件应该怎样描述呢?可参考如下所示的配置。
程序清单 4.6 一个普通的分散加载文件配置 LR_IROM1 0x00000000 0x00040000 { ; 定义一个加载时域,域基址:0x00000000,域大 ; 小为 0x00040000,对应实际 Flash 的大小 ER_IROM1 0x00000000 0x00040000 { ; 定义一个运行时域,第一个运行时域必须和加载 ; 时域起始地址相同,否则库不能加载到该时域的 ; 错误,其域大小一般也和加载时域大小相同 *.o (RESET, +First) ; 将 RESET 段最先加载到本域的起始地址外,即 ; RESET 的起始地址为 0,RESET 存储的是向量表 .ANY (+RO) ; 加载所有匹配目标文件的只读属性数据,包含: ; Code、RW-Code、RO-Data。 } ; 定义一个运行时域,域基址:0x10000000,域大 ; 小为 0x00008000,对应实际 RAM 大小 * (+RW +ZI) ; 加载所有区配目标文件的 RW-Data、ZI-Data ; 这里也可以用.ANY 替代*号 } }

RW_IRAM1 0x10000000 0x00008000 {

26

4.8.1 多块 RAM 的分散加载文件配置 还是上述的 MCU,假设其增加了另外一块 RAM,其资源如下: l l l Flash 基址:0x00000000,大小:256 KByte; RAM1 基址:0x10000000,大小:32 Kbyte; RAM2 基址:0x2007C000,大小:32 Kbyte。

如果我想将这两块不连续的 RAM 都使用起来(可使用 64KB RAM)?分散加载文件应怎 样描述呢?可参考如下所示配置。
程序清单 4.7 双 RAM 的加载文件配置 LR_IROM1 0x00000000 0x00040000 { ER_IROM1 0x00000000 0x00040000 { *.o (RESET, +First) .ANY (+RO) } ; 定义 RAM1 的运行时域 ; 使用.ANY 进行随意分配变量,这里不能使用*号 ; 替代,*表示匹配有所有的目标文件,这样变量就 ; 无法分配到第二块 RAM 空间了 } ; 定义第 RAM2 的运行时域 ; 同样使用.ANY 随意分配变量的方式

RW_IRAM1 0x10000000 0x00008000 { .ANY (+RW +ZI)

RW_IRAM2 0x2007C000 0x00008000 { .ANY (+RW +ZI) }

; 如果还有另多的 RAM 块,在这里增加新的运行 ; 时域即可,格式和 RAM2 的定义相同 }

如上所示的分散加载文件配置, 确实可将两块 RAM 都使用起来, 即有 64KB 的 RAM 可以 使用,但其并不能完全等价于一个 64KB 的 RAM,实际应用可能会碰到如下的问题。 如我在 main.c 文件中声明了 1 个 40KB 的数组,如程序清单 4.所示,程序中红色部分已注 释,可暂不理会。
程序清单 4.8 定义大数组出错 // main.c 文件 … unsigned char //unsigned char //unsigned char … // end of file GucTest0[40 * 1024]; GucTest1[20 * 1024]; GucTest2[20 * 1024]; // 定义一个 40KB 的数组 // 定义第一个 20KB 数组 // 定义第二个 20KB 数组

如程序清单 4.8 所示的程序在编译时会出现错误,并提示没有足够的空间,为什么呢?原 因为数组是一个整体,其内部元素的地址是连续的,不能分割的,但是在两个不连续的 32KB
27

空间中,是没办法分配出一个连续的 40KB 地址空间,所以编译会提示空间不足,分配 40KB 数组失败。 还是程序清单 4.8 所示的程序,去掉 40KB 的数据,换成 2 个 20KB 的数组(即红色注释 代码部分) ,编译结果又会如何呢? 编译结果还是提示空间不足,这又是为什么呢?这里出错的原因其实和上面的原因是相同 的,首先,重温一个,.ANY 的作用,.ANY 是一个通配符,当其与以下内容之一相匹配时将进 行选择。 l l l 包含段和目标文件的名称; 库成员名称(不带前导路径名) ; 库的完整名称 (包括路径名) 如果名称包含空格, 。 则可以使用通配符简化搜索。 例如, 使用*libname.lib 匹配 C:\lib dir\libname.lib。

后面两个和本次讨论的话题无关,再仔细的看第一个匹配项为:包含段和目标文件,段这 里先不用理会, 因为这里没有用到段, 所以只剩下目标文件。 要注意是 “目标文件” 不是其它, , 即是说一个 C 文件编译后,其所有的变量、代码都会作为一个整体。所以定义两个 20KB 和定 义了一个 40KB,在编译器看来都是一样,就是这个 C 文件总共定义了 40KB 的空间,我要用 40KB 的空间来分配它,因此会出现同样的错误。 关于大数组分配的解决方法,有两种,分别如下: l l 将数组分开在不同的 C 文件中定义, 避免在同一个 C 文件定义的数据大小总量超过其 中最大的分区; 将一个 C 数组,使用段定义,使其从该 C 文件中独立出来,这样编译器就不会将它们 作为一个整体来划分空间了,其示例如程序清单 4.9 所示。
程序清单 4.9 使用段的方式分配多数组 #pragma arm section zidata = "SRAM" unsigned char GucTest1[20 * 1024]; // 在 C 文件中定义新的段 // 定义第一个 20KB 数组 // 恢复原有的段 // 定义第二个 20KB 数组,这 20KB 数组不会和 // GucTest1 作为一个整体来划分空间

#pragma arm section

unsigned char

GucTest2[20 * 1024];

4.8.2 多块 Flash 的分散加载文件配置 再一下上述的 MCU,假其增加多了一块 Flash,不是 RAM,其资源如下。 l l l Flash1 基址:0x00000000,大小:256 KByte; Flash2 基址:0x20000000,大小:2048 KByte; RAM 基址:0x10000000,大小:32 Kbyte;

注意这里多增加的一块的不是 RAM,而是 Flash,其情况会如何呢?假设其相同,那写法 应该就是如程序清单 4.10 所示的样子。
程序清单 4.10 双 Flash 的错误配置示例 1 LR_IROM1 0x00000000 0x00040000 { ER_IROM1 0x00000000 0x00040000 { *.o (RESET, +First) ; 定义 Flash1 运行时域 ; 先加载向量表 28

.ANY (+RO) }

; 随意分配只读数据

ER_IROM2 0x20000000 0x00200000 { .ANY (+RO) }

; 定义 Flash2 运行时域 ; 随意分配只读数据

RW_IRAM1 0x10000000 0x00008000 { .ANY (+RW +ZI) } }

如程序清单 4.10 的分散加载配置,进行编译却出错了,错误提示如下所示。
..Error: L6202E: __main.o(!!!main) cannot be assigned to non-root region 'ER_IROM2' ..Error: L6202E: __scatter.o(!!!scatter) cannot be assigned to non-root region 'ER_IROM2' ..Error: L6202E: __scatter_copy.o(!!handler_copy) cannot be assigned to non-root region 'ER_IROM2' ..Error: L6202E: __scatter_zi.o(!!handler_zi) cannot be assigned to non-root region 'ER_IROM2' ..Error: L6202E: anon$$obj.o(Region$$Table) cannot be assigned to non-root region 'ER_IROM2' ..Error: L6203E: Entry point (0x20000001) lies within non-root region ER_IROM2.

该错误的意思是说,__main.o、__scatter.o、__scatter_copy.o 等不能被加载到第二块 Flash 的运行时域 ER_IROM2,也就是说这几项目数据只能加载到 ER_IROM1 的运行时域。 为什么这些数据不能放到第二个运行时域呢?后面再进行解释。 无论如何,但总的来说是使用.ANY 引起的错误,.ANY 是让编译器随意分配数据,所以数 据有可能被分配到 ER_IROM2。如果手动这出错的几项到 ER_IROM1,结果又会如何呢?请看 如程序清单 4.11 所示的配置。
程序清单 4.11 双 Flash 的错误配置示例 2 LR_IROM1 0x00000000 0x00040000 { ER_IROM1 0x00000000 0x00040000 { *.o (RESET, +First) __main.o __scatter.o __scatter_copy.o __scatter_zi.o * (Region$$Table) .ANY (+RO) } ; 先加载向量表 ; 手动加载到 ER_IROM1,避免自动分配引起错误 ; 避免自动分配 ; 避免自动分配 ; 避免自动分配 ; 避免自动分配

ER_IROM2 0x20000000 0x00200000 { .ANY (+RO) }

RW_IRAM1 0x10000000 0x00008000 { .ANY (+RW +ZI)

29

} }

终于,编译没有错误了,但在软件仿真下,现象明显不正确,main()函数都跑不到,这? 这个问题得从第一个时域与加载时域的关系来进行说明,其关系如下所示。 1. 第一个运行时域存放的代码不会进行额外拷贝 因为分散加载文件有一项很强大的功能, 就是可以将 Flash 的代码拷贝到 RAM 中运行, 这 一段拷贝代码就存在于__main()函数中,但拷贝代码不能拷贝自身,所以必须规定有一个运行 时域中存放的代码是不会被拷贝的,这个指的就是第一个运行时域。 拷贝代码为什么不能拷贝自身呢?举个例子, 假设 A 代码是要被拷贝到 RAM 执行的代码, 那 A 代码必先存储于 Flash 中,然后被拷贝到 RAM 中。这样 A 代码不就存在两段了么,但是 只能有一段是有效的,就像定义了两个相同名字的函数,最终只能留下一个。所以最终被认定 为拷贝后的代码才是有效的。那就是说 A 代码从 Flash 中拷贝到了 RAM,RAM 中代码才是有 效,Flash 中的代码是无效的,其它程序调用 A 代码也是调用 RAM 中的代码而不是 Flash 的。 那如果 A 是一段拷贝代码,那就会发生如下的现象,一段程序调用 RAM 中的 A 代码,RAM 中的 A 代码再将自己从 Flash 中拷贝到 RAM。 结论,一段代码必须先完成拷贝,才能被执行。换句理解就是拷贝代码前包括自身的所有 代码都不能拷贝,也就是说这些代码全部都必须放在第一个运行时域中。 2. 规定其余运行时域中存放的代码均会被拷贝 一个加载时域,只需要一个不拷贝的运行时域即可。所以规定其余所有的运行时域中的代 码均会被拷贝。 3. 第一个运行时域的基址必须与加载域基址相同 为了保证第一个运行时域的代码能够被正确存储和执行,因此要求第一个运行时域的基址 必须和加载时域的基址相同。 看完了第一个运行时域与加载时域的关系,那程序清单 4.10、程序清单 4.11 出现错误的原 因就很明了了。 l l 程序清单 4.10 出错原因,是因为__main()等拷贝代码必须存放在第一个运行时域中; 程序清单 4.11 出错的原因如程序清单 4.12 所示。
程序清单 4.12 程序清单 5.7 出错的原因 ER_IROM2 0x20000000 0x00200000 { .ANY (+RO) ; 定义 Flash1 运行时域 ; 随意分配只读数据,但代码存放在第二个运行时 ; 域中,所以该代码是运行时才被拷贝到这里, ; 那就是说要往 Flash1 直接写数据,当然会导致 ; 程序出错了 }

程序清单 4.13 给出了双 Flash 的分散加载文件正确配置示例,如下所示。
程序清单 4.13 双 Flash 的正确配置示例 LR_IROM1 0x00000000 0x00040000 { ER_IROM1 0x00000000 0x00040000 { *.o (RESET, +First) .ANY (+RO) ; 随机分配只读数据 30 ; 定义 Flash1 的加载域

}

RW_IRAM1 0x10000000 0x00008000 { .ANY (+RW +ZI) } } ; 定义 Flash2 的加载域 ; 随机分配只读数据,代码不会进行拷贝

LR_IROM2 0x20000000 0x00200000 { ER_IROM2 0x20000000 0x00200000 { .ANY (+RO) } }

31

第5章 编写自己的启动代码(基于 LPC17xx)
5.1 最简单的启动代码(Startup.s Version-1.0)

经过上面的理论知识的铺垫,我们已经对 ARM(LPC17xx)的启动过程有了一个清晰的认 识,下面就来实战一下,编写一个自己的 ARM 启动代码。 5.1.1 启动代码必须完成的三项工作 对于一个复杂系统的话,则启动代码需要完成的工作比较多过程也比较复杂,比如一个存 储体系比较复杂的系统(在一个系统中由内部、外部的 RAM 又有内部和外部的 ROM)或带有 操作系统的。对于不同的 ARM 架构的芯片其启动代码差别也比较大,比如在 ARM9 中各种运 行模式的堆栈的分配、管理以及切换全部都要由用户自己实现,而在 M0 或 M3 中就不需要用 户去实现这些过程。 对于 Cortex-M3 内核的处理器,其启动代码必须完成三项任务: l l l 指定 MSP 的初始值; 指定程序运行的入口; 初始化一个堆栈(如果执行的是 C 代码则需要执行此过程,否则可以省略) 。

5.1.2 程序实现(Startup.s Version-1.0) ARM 处理器的运行是从 ROM 空间的 0x00000000 地址开始的,而在 Cortex-M3 中从 0x00000000 地址开始的区域里维护了一张中断向量表。 向量表中的第一个字中存储的是主堆栈 指针 MSP 的值,第二个字中存储的是程序的入口地址(一些资料上说这个字里存储的是复位 向量或复位程序的入口地址,实际上这种说法不够准确,这个字可以不用来指定复位程序的入 口。,之后就是真正的中断向量表了。 ) 结合 Cortex-M3 内核的处理器启动代码必须完成三项任务,首先需要指定 ROM 空间的第 一个以及第二个字的内容,其中第二个字我们将其指定为__main,为 C 语言的运行搭建环境, 以此为入口开始运行程序,其中__main 的运行需要用户自己实现__user_initial_stackheap,故启 动代码实现如下:
程序清单 5.1 Startup.s(V1.0) ;/* ;* ;* ;* ;* ;* ;* ; */ LPC1700 ARM Cortex-M3 启动代码 File Version Date Author : : : : Startup.s 1.0 11-18-2011 ZhengYuanChao

; /* ; * 说明:Cortex-M3 启动需要完成以下三项工作(在使用标准 C Library 的情况下) ; * ; * ; * 1. 指定栈顶 2. 引入__main 并跳转过去 3. 重写__user_initial_stackheap 32

; */ PRESERVE8 AREA ; 指定 8 字节对齐(keil 的要求) ; 导入__main 标号 ; 指定 MSP ; 跳转到__main ; 指定代码段 ; 导出此标号供__main 调用 LPC1700Startup, DATA, READONLY ; 指定数据段

IMPORT __main __Vectors DCD DCD 0x10008000 __main

AREA

|.text|, CODE, READONLY

EXPORT __user_initial_stackheap

__user_initial_stackheap BX ALIGN END LR ; 不执行任何操作直接跳回__mian ; 对齐填充使代码段双字对齐

5.1.3 指定加载方式让程序跑起来 仅仅完成了 Startup.s 的编写还是不够的,还需要为程序生成的镜像文件指定加载方式(第 四章有具体描述) ,否则程序无法运行。 对于 Version-1.0 的启动代码加载方式的指定比较简单,只需将__Vectors 放在 ROM 的顶端 即可,至于其他代码的摆放则无关紧要,可由链接器自行安排。描述如下:
程序清单 5.2 分散加载代码(V1.0) ROM_LOAD 0x00000000 { VECTOR 0x00000000 { *.o (LPC1700Startup, +FIRST) .ANY (+RO) } SRAM 0x10000000 { * (+RW,+ZI) } } ; 将__Vectors 放在 ROM 的顶端

现在程序可以运行了,使用软模拟调试可以发现程序顺利的进入了 main()函数。 5.1.4 Version-1.0 的缺点 Version-1.0 只对处理器进行了最简单配置并令其运行起来,它甚至连中断向量表都没有初 始化,也就是说处理器将不能响应中断,这当然是不可以的。故此代码是没有什么使用价值的。

5.2

一个实用的启动代码(Startup.s Version-2.0)

5.2.1 设计描述 要让启动代码真正具有实用性必须还要定位一张向量表,某些程序还可能用到堆,所以我 们还要再管理一个堆,对于 NXP 的 LPC17xx 还要在启动代码中指定芯片的加密方式。
33

因为 Version-1.0 的启动代码已经成功的配置了 C 语言的运行环境, 故接下来的代码我们将 用 C 语言来编写。 5.2.2 程序实现(Startup.s)
程序清单 5.3 Startup.s(V2.0) ; /* ; * 说明:Cortex-M3 启动需要完成以下三项工作(在使用标准 C Library 的情况下) ; * ; * ; * ; * ; */ SP_TOP HEAP_TOP EQU EQU 0x10008000 0x10007000 ; 指定 8 字节对齐(keil 的要求) ; 指定数据段 ; 导入__main 标号 ; 指定 MSP ; 跳转到__main 1. 指定栈顶 2. 引入__main 并跳转过去 3. 重写__user_initial_stackheap 4. 对于 NXP 的 LPC1700 系列还要指定其加密级别

PRESERVE8 AREA StartupEntry, DATA, READONLY

IMPORT __main

__Start

DCD DCD IF

SP_TOP __main :LNOT::DEF:NO_CRP

AREA CRP_Key

|.ARM.__at_0x02FC|, CODE, READONLY ; 加密标志存储于 0x02fc 地址 DCD ENDIF ; 指定代码段 ; 导出此标号供__main 调用 0xFFFFFFFF ; 0 级加密(即不加密)

AREA

|.text|, CODE, READONLY

EXPORT __user_initial_stackheap

__user_initial_stackheap LDR BX ALIGN END R0, =HEAP_TOP LR ; 指定堆顶 ; 跳回__mian ; 对齐填充使代码段双字对齐

5.2.3 向量表 Vectors.h 和 Vectors.c
程序清单 5.4 Vectors.h Vectors.h 的代码如下: #ifndef __VECTORS_H__ #define __VECTORS_H__

#define MAX_VICTORS

51

34

#define NMI_Handler #define HardFault_Handler #define MemManage_Handler #define BusFault_Handler #define UsageFault_Handler #define SVC_Handler #define DebugMon_Handler #define PendSV_Handler #define SysTick_Handler

defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle

#define WDT_IRQHandler #define TIMER0_IRQHandler #define TIMER1_IRQHandler #define TIMER2_IRQHandler #define TIMER3_IRQHandler #define UART0_IRQHandler #define UART1_IRQHandler #define UART2_IRQHandler #define UART3_IRQHandler #define PWM1_IRQHandler #define I2C0_IRQHandler #define I2C1_IRQHandler #define I2C2_IRQHandler #define SPI_IRQHandler #define SSP0_IRQHandler #define SSP1_IRQHandler #define PLL0_IRQHandler #define RTC_IRQHandler #define EINT0_IRQHandler #define EINT1_IRQHandler #define EINT2_IRQHandler #define EINT3_IRQHandler #define ADC_IRQHandler #define BOD_IRQHandler #define USB_IRQHandler #define CAN_IRQHandler #define DMA_IRQHandler #define I2S_IRQHandler #define ENET_IRQHandler #define RIT_IRQHandler #define MCPWM_IRQHandler #define QEI_IRQHandler #define PLL1_IRQHandler #define USBActivity_IRQHandler

defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle defaultVectorHandle

35

#define CANActivity_IRQHandler

defaultVectorHandle

#endif

因为中断向量表存储的是中断函数的入口地址而且又是一张表,故我们用函数指针数组来 表示中断向量表,Vectors.c 的代码如下:
程序清单 5.5 Vectors.c #include #include #include "vectors.h" "stddef.h" "syscfg.h"

void * const {

__VectorsTable[MAX_VICTORS] =

(void *)(NMI_Handler), (void *)(HardFault_Handler), (void *)(MemManage_Handler), (void *)(BusFault_Handler), (void *)(UsageFault_Handler), (void *)defaultVectorHandle, (void *)defaultVectorHandle, (void *)defaultVectorHandle, (void *)defaultVectorHandle, (void *)(SVC_Handler),

(void *)(DebugMon_Handler), NULL, (void *)(PendSV_Handler), (void *)(SysTick_Handler),

(void *)(WDT_IRQHandler), (void *)(TIMER0_IRQHandler), (void *)(TIMER1_IRQHandler), (void *)(TIMER2_IRQHandler),

(void *)(TIMER3_IRQHandler), (void *)(UART0_IRQHandler), (void *)(UART1_IRQHandler), (void *)(UART2_IRQHandler), (void *)(UART3_IRQHandler), (void *)(PWM1_IRQHandler), (void *)(I2C0_IRQHandler), (void *)(I2C1_IRQHandler), (void *)(I2C2_IRQHandler), (void *)(SPI_IRQHandler),

36

(void *)(SSP0_IRQHandler), (void *)(SSP1_IRQHandler), (void *)(PLL0_IRQHandler), (void *)(RTC_IRQHandler), (void *)(EINT0_IRQHandler), (void *)(EINT1_IRQHandler), (void *)(EINT2_IRQHandler), (void *)(EINT3_IRQHandler), (void *)(ADC_IRQHandler), (void *)(BOD_IRQHandler),

(void *)(USB_IRQHandler), (void *)(CAN_IRQHandler), (void *)(DMA_IRQHandler), (void *)(I2S_IRQHandler), (void *)(ENET_IRQHandler), (void *)(RIT_IRQHandler), (void *)(MCPWM_IRQHandler), (void *)(QEI_IRQHandler), (void *)(PLL1_IRQHandler), (void *)(USBActivity_IRQHandler), (void *)(CANActivity_IRQHandler) };

这里需要注意的是,数组必须用 const 限定,因为中断向量表必须是 Read-Only 的,最后还 需要在创建一个文件重写 defaultVectorHandle 函数即可(此段代码省略) 。 5.2.4 指定加载方式 向量表必须定位在__Start 标号之后,否则是无意义的,故描述如下:
程序清单 5.6 分散加载代码(V2.0) ROM_LOAD 0x00000000 { STARTUP 0x00000000 { *.o (StartupEntry, +FIRST) } VECTORS 0x00000008 { vectors.o(+RO-DATA, +FIRST) .ANY (+RO) } SRAM 0x10000000 {

37

* (+RW,+ZI) } }

之后再将芯片的时钟以及 MPU 等具体的硬件初始化代码添加进来我们的芯片就可以正常 运行了。

38


相关文档

TMP文件是什么,怎么打开
将TMP文件从TMP文件夹中拷贝出来
tmp文件
什么是TMP文件
TMP文件是什么意思
tmp是什么文件
tmp文件处理
住宅房屋建筑招标文件.tmp
如何清理TMP文件
关于Proe配置文件选项.tmp
电脑版