《ARM 系列处理器应用技术完全手册》

分类:电子电工 日期: 点击:0
《ARM 系列处理器应用技术完全手册》-0 《ARM 系列处理器应用技术完全手册》-1 《ARM 系列处理器应用技术完全手册》-2 《ARM 系列处理器应用技术完全手册》-3 《ARM 系列处理器应用技术完全手册》-4 《ARM 系列处理器应用技术完全手册》-5 《ARM 系列处理器应用技术完全手册》-6 《ARM 系列处理器应用技术完全手册》-7 《ARM 系列处理器应用技术完全手册》-8 《ARM 系列处理器应用技术完全手册》-9

《ARM 系列处理器应用技术完全手册》 作者:华清远见 第 1 章 ARM 系列微处理器简介 专业始于专注 卓识源于远见       ‐  2  ‐            1.1 什么是 ARM ARM(Advanced RISC Machines)有三种含义,它是一个公司的名称,是一类微处理器的通称,还是一种 技术的名称。 ARM 公司是微处理器行业的一家知名企业,设计了大量高性能、廉价、低耗能的 RISC(Reduced Instruction Set Computing,精简指令集计算机处理器)芯片,并开发了相关技术和软件。ARM 处理器具有性能高、 成本低和能耗低的特点,适用于嵌入式控制、消费/教育类多媒体、DSP 和移动式应用等领域。 ARM 公司本身不生产芯片,靠转让设计许可,由合作伙伴公司来生产各具特色的芯片。ARM 这种商业模 式的强大之处在于其价格合理,它在全世界范围的合作伙伴超过 100 个,其中包括许多著名的半导体公司。 ARM 公司专注于设计,设计的芯片内核耗电少,成本低,功能强,特有 16/32 位双指令集。ARM 已成为 移动通信、手持计算和多媒体数字消费等嵌入式解决方案的 RISC 实际标准。 1.1.1 ARM 公司历史 1990 年 11 月 ARM 公司成立于英国,原名 Advanced RISC Machine 有限公司,是苹果电脑、Acorn 电 脑集团和 VLSI Technology 的合资企业。Acorn 曾推出世界首个商用单芯片 RISC 处理器,而苹果电脑 当时希望将 RISC 技术应用于自身系统,ARM 微处理器新标准因此应运而生。ARM 公司成功地研制了 首个低成本 RISC 架构,迅速在市场上崭露头角。与此同时,RISC 结构的竞争对手都着眼于提高性能, 发展适合高端工作站处理器的 RISC 结构。 1991 年 ARM 公司推出首个嵌入式 RISC 核心——ARM6™系列处理器后不久,VLSI 率先获得授权,一年 后夏普和 GEC Plessey 也成为授权用户。1993 年德州仪器和 Cirrus Logic 也签署了授权协议。从此 ARM 公 司的知识产权产品和授权用户都急剧增多。1993 年 Nippon Investment and Finance(NIF)成为 ARM 公司 股东后,ARM 公司开始向全球拓展,分别在亚洲、北美洲和欧洲设立了办事处。1998 年 4 月 ARM 公司 在伦敦证券交易所和纳斯达克交易所上市。 ARM 公司现已发展成为一家全球性大公司,公司在英国、法国和美国设有研发中心,在中国、法国、德 国、日本、韩国、以色列、英国和美国建立了销售、行政和技术支持办事处。ARM 中国—安谋咨询上海 有限公司于 2002 年 7 月成立。 1.1.2 ARM 的商业模式 ARM Holdings(伦敦证交所:ARM:纳斯达克:ARMHY)在半导体革新过程中初露峥嵘,被 Dataquest 誉 为世界第一的知识产权供应商。20 世纪 90 年代初,ARM 公司率先推出 32 位 RISC 微处理器芯片系统(SoC) 知识产权公开授权概念。ARM 公司通过出售芯片技术授权而非生产或销售芯片,建立起新型的微处理器 设计、生产和销售商业模式。采用 ARM 技术的微处理器遍及各类电子产品,在汽车电子、消费娱乐、成 像、工业控制、网络、存储安保和无线等领域 ARM 技术无处不在。 ARM 公司知识产权授权用户众多,其中包括世界顶级的半导体公司。全球 20 家最大的半导体厂商中有 19 家是 ARM 公司的用户。这些合作伙伴通过使用 ARM 公司低价、高效的 IP 核技术研制生产微处理器、外 围设备和系统芯片。迄今这些厂商共发售了超过 10 亿个 ARM 微处理器内核。 为支持和增补 ARM 公司的现有 RISC 微处理器内核和 SoC IP,公司开发了功能强大的软件。ARM 公司的 伙伴企业能够获得各种基于软件的 IP、操作系统端口和软件设计服务,从而大大降低产品开发风险,缩短 上市时间。 首先是 ARM PrimeXsys 平台。这是一种取出即用的 IP,以平台的形式为专门应用提供支持。第一个 PrimeXsys 平台是 2001 年 9 月推出的 PrimeXsys Wireless 平台。它是一个高集成度的可扩展平台,包 含了所有必需的硬件、软件和集成工具。ARM 公司的伙伴企业可以利用这个平台轻松开发一系列基于 ARM 处理器的面向应用的设备,既迅速,风险又低。 专业始于专注 卓识源于远见       ‐  3  ‐            ARM 公司推出的另一新技术是 Jazelle,这项技术能将 Java 技术和全球领先的 32 位嵌入式 RISC 结构结合 在一起,使平台开发人员能够在同一处理器上与现有操作系统、中间软件和应用编码同时运行 Java 应用程 序,从而提高性能,降低系统成本,比协处理器和双处理器解决方案能耗更低。 1.2 ARM 体系结构的命名规则 ARM 体系结构是 CPU 产品所使用的一种体系结构,ARM 公司开发了一套拥有知识产权的 RISC 体系结构 的指令集。每个 ARM 处理器都有一个特定的指令集架构,而一个特定的指令集架构又可以由多种处理器 实现。 特定的指令集架构随着嵌入式市场的发展而发展。由于所有产品均采用一个通用的软件体系,所以相同的 软件可在所有产品中运行(理论上如此)。 ARM 产品通常以 ARM【x】【y】【z】【T】【D】【M】【I】【E】【J】【F】【-S】形式出现。表 1.1 显示了 ARM 体系结构的命名规则中这些后缀的具体含义。 表 1.1 ARM 体系结构的命名规则 后 缀 变 量 含 义 x 系列,号如 ARM7、ARM9 y 存储管理/保护单元 z Cache T Thumb16 位译码器 D JTAG 调试器 M 快速乘法器 I 嵌入式跟踪宏单元 E 增强指令(基于 TDMI) J Jazelle 加速 F 向量浮点单元 S 可综合版本 另外,还有一些附加的要点: ① ARM7TDMI 之后的所有 ARM 内核,即使“ARM”标志后没有包含“TDMI”字符,也都默认包含了 TDMI 的功能特性; ② JTAG 是由 IEEE 1149.1 标准测试访问端口和边界扫描结构来描述的,它是 ARM 用来发送和接收处理 器内核与测试仪器之间调试信息的一系列协议; ③ 嵌入式 ICE 宏单元是建立在处理器内部用来设置断点和观察点的调试硬件; ④ 可综合,意味着处理器内核是以源代码形式提供的。这种源代码形式可被编译成一种易于 EDA 工具使 用的形式。 1.3 初识 ARM 系列处理器 ARM 处理器当前有 6 个产品系列:ARM7、ARM9、ARM9E、ARM10E、ARM11 和 SecurCore,其中 ARM11 为最近推出的产品。进一步的产品来自于合作伙伴,例如 Intel Xscale ARM7、ARM9、ARM9E、ARM10E 是 4 个通用处理器系列。每个系列提供一套特定的性能来满足设计者对功耗、性能、体积的需求。SecurCore 是第 5 个产品系列,是专门为安全设备而设计的。 表 1.2 总结了 ARM 各系列处理器所包含的不同类型。 专业始于专注 卓识源于远见       ‐  4  ‐            表 1.2 ARM 各系列处理器所包含的不同类型 ARM 系列 包 含 类 型 ARM7 系列 ARM7EJ-S ARM7TDMI ARM7TDMI-S ARM720T ARM9/9E 系列 ARM920T 续表 ARM 系列 包 含 类 型 ARM9/9E 系列 ARM922T ARM926EJ-S ARM940T ARM946E-S ARM966E-S ARM968E-S 向量浮点运算(Vector Floating Point)系列 VFP9-S VFP10 ARM10E 系列 ARM1020E ARM1022E ARM1026EJ-S ARM11 系列 ARM1136J-S ARM1136JF-S ARM1156T2(F)-S ARM1176JZ(F)-S ARM11 MPCore SecurCore 系列 SC100 SC110 SC200 SC210 其他合作伙伴产品 StrongARM XScale Cortex-M3 MBX 本节简要介绍 ARM 各个系列处理器的特点。 1.3.1 ARM7 系列 ARM7 内核采用冯·诺伊曼体系结构,数据和指令使用同一条总线。内核有一条 3 级流水线,执行 ARMv4 指令集。 ARM7 系列处理器主要用于对功耗和成本要求比较苛刻的消费类产品。其最高主频可以到达 130MIPS (MIPS 指每秒执行的百万条指令数)。ARM7 系列包括 ARM7TDMI、ARM7TDMI-S、ARM7EJ-S 和 ARM720T 4 种类型,主要用于适应不同的市场需求。 ARM7 系列处理器主要具有以下特点:  成熟的大批量的 32 位 RICS 芯片;  最高主频到达 130MIPS;  功耗低;  代码密度高,兼容 16 位微处理器; 专业始于专注 卓识源于远见       ‐  5  ‐             开发工具多、EDA 仿真模型多;  调试机制完善;  提供 0.25µm、0.18µm 及 0.13µm 的生产工艺;  代码与 ARM9 系列、ARM9E 系列以及 ARM10E 系列兼容。 1.3.2 ARM9 系列 ARM9 系列于 1997 年问世。由于采用了 5 级指令流水线,ARM9 处理器能够运行在比 ARM7 更高的时钟 频率上,改善了处理器的整体性能;存储器系统根据哈佛体系结构(程序和数据空间独立的体系结构)重 新设计,区分了数据总线和指令总线。 ARM9 系列的第一个处理器是 ARM920T,包含独立的数据指令 Cache 和 MMU。此处理器能够被用在要 求有虚拟存储器支持的操作系统上。此系列的 ARM922T 是 ARM920T 的变种,只有一半大小的数据指令 Cache。 ARM940T 包含一个更小的数据指令 Cache 和一个 MPU。它是针对不要求运行操作系统的应用而设计的。 ARM920T、ARM940T 都执行 v4T 架构指令。 1.3.3 ARM9E 系列 ARM9 系列的下一个处理器是基于 ARM9E-S 内核的。这个内核是 ARM9 内核带有 E 扩展的一个可综合版 本。它有 ARM946E-S 和 ARM966E-S 两个变种。两者都执行 v5TE 架构指令。它们也支持可选的嵌入式跟 踪宏单元,支持开发者实时跟踪处理器上指令和数据的执行。当调试对时间敏感的程序段时,这种方法非 常重要。 ARM946E-S 包括 TCM、Cache 和一个 MPU。TCM 和 Cache 的大小可配置。该处理器是针对要求有确定 的实时响应的嵌入式控制而设计的。ARM966E-S 有可配置的 TCM,但没有 MPU 和 Cache 扩展。 ARM9 系列的 ARM926EJ-S 内核为可综合的处理器内核,发布于 2000 年。它是针对小型便携式 Java 设备, 诸如 3G 手机和 PDA 应用而设计的。ARM926EJ-S 是第一个包含 Jazelle 技术,可加速 Java 字节码执行的 ARM 处理器内核。它还有一个 MMU、可配置的 TCM 以及具有零或非零等待存储器的数据/指令 Cache。 1.3.4 ARM10 系列 ARM10 发布于 1999 年,具有高性能、低功耗的特点。它所采用的新的体系使其在所有 ARM 产品中具有 最高的 MIPS/MHz。它将 ARM9 的流水线扩展到 6 级,也支持可选的向量浮点单元 VFP,对 ARM10 的流 水线加入了第 7 段。VFP 明显增强了浮点运算性能并与 IEEE 754.1985 浮点标准兼容。 ARM10E 系列处理器采用了新的节能模式,提供了 64 位的 Load/Store 体系,支持包括向量操作的满足 IEEE 754 的浮点运算协处理器,系统集成更加方便,拥有完整的硬件和软件开发工具。ARM10E 系列包括 ARM1020E、ARM1022E 和 ARM1026EJ-S 3 种类型。 1.3.5 ARM11 系列 ARM1136J-S 发布于 2003 年,是针对高性能和高能效应而设计的。ARM1136J-S 是第一个执行 ARMv6 架 构指令的处理器。它集成了一条具有独立的 Load/Stroe 和算术流水线的 8 级流水线。ARMv6 指令包含了 针对媒体处理的单指令流多数据流扩展,采用特殊的设计改善视频处理能力。 专业始于专注 卓识源于远见       ‐  6  ‐            1.3.6 SecurCore 系列 SecurCore 系列处理器提供了基于高性能的 32 位 RISC 技术的安全解决方案。SecurCore 系列处理器除了具 有体积小、功耗低、代码密度高等特点外,还具有它自己特别优势,即提供了安全解决方案支持。下面总 结了 SecurCore 系列的主要特点: ① 支持 ARM 指令集和 Thumb 指令集,以提高代码密度和系统性能; ② 采用软内核技术以提供最大限度的灵活性,可以防止外部对其进行扫描探测; ③ 提供了安全特性,可以抵制攻击; ④ 提供面向智能卡和低成本的存储保护单元 MPU; ⑤ 可以集成用户自己的安全特性和其他的协处理器。 SecurCore 系列包含 SC100、SC110、SC200 和 SC210 4 种类型。 1.3.7 其他系列处理器 StrongARM 处理器最初是 ARM 公司与 Digital Semiconductor 公司合作开发的,现在由 Intel 公司单独许可。 在低功耗、高性能的产品中应用很广泛。它是哈佛架构的,具有独立的数据和指令 Cache,有 MMU(Memory Management Unit)。StrongARM 是第一个包含 5 级流水线的高性能 ARM 处理器,但它不支持 Thumb 指令 集。 Intel 公司的 Xscale 是 Strong ARM 的后续产品,在性能上有显著改善。它执行 v5TE 架构指令,也是哈佛 结构的,类似于 StrongARM 也包含一个 MMU。 1.3.8 Cortex-M3 和 MPCore 为了适应市场的需要,ARM 推出了两个新的处理器:Cortex-M3 和 MPCore。Cortex-M3 主要针对微控制 器市场,而 MPCore 主要针对高端消费类产品。 Cortex-M3 改进了代码密度,减少了中断延时并有更低的功耗。Cortex-M3 中实现了最新了 Thumb-2 指令 集。MPCore 提供了 Cache 一致性,每个支持 1~4 个 ARM11 核,这种设计为现代消费类产品对性能和功 耗的需求作了很好的平衡。ARM 还引入了 L2Cache 控制器来改进系统的整体性能。 1.4 ARM 系列处理器的应用领域 1.4.1 ARM7 系列 ARM7 系列处理器主要应用于下面一些场合:  个人音频设备(MP3 播放器、WMA 播放器、AAC 播放器);  接入级的无线设备;  喷墨打印机;  数码照相机;  PDA。 专业始于专注 卓识源于远见       ‐  7  ‐            1.4.2 ARM9 系列 ARM9 系列处理器具体应用于下面一些场合:  下一代无线设备,包括视频电话和 PDA 等;  数字消费品,包括机顶盒、家庭网关、MP3 播放器和 MPEG4 播放器;  成像设备,包括打印机、数码照相机和数码摄像机;  汽车、通信和信息系统。 1.4.3 ARM9E 系列 ARM9E 系列处理器具体应用于下面一些场合:  下一代无线设备,包括视频电话和 PDA 等;  数字消费品,包括机顶盒、家庭网关、MP3 播放器和 MPEG4 播放器;  成像设备,包括打印机、数码照相机和数码摄像机;  存储设备,包括 DVD 或 HDD 等;  工业控制,包括电机控制等;  汽车、通信和信息系统的 ABS 和车体控制;  网络设备,包括 VoIP、WirelessLAN 和 xDSL 等。 1.4.4 ARM10E 系列 ARM10E 系列处理器具体应用于下面一些场合:  下一代无线设备,包括视频电话和 PDA、笔记本电脑和互联网设备;  数字消费品,包括机顶盒、家庭网关、MP3 播放器和 MPEG4 播放器;  成像设备,包括打印机、数码照相机和数码摄像机;  汽车、通信和信息系统等;  工业控制,包括马达控制等。 1.4.5 SecureCore 系列 SecureCore 系列处理器主要应用于一些安全产品及应用系统,包括电子商务、电子银行业务、网络、移动 媒体和认证系统等。 1.5 ARM 芯片的特点与选型 1.5.1 不同系列处理器间的比较 表 1.3 显示了 ARM7、ARM9、ARM10 及 ARM11 内核之间属性的比较。有些属性依赖于生产过程和工艺, 具体芯片需参阅其芯片手册。 表 1.3 ARM 系列处理器属性比较 专业始于专注 卓识源于远见       ‐  8  ‐            项 目 ARM7 ARM9 ARM10 ARM11 流水线深度 3 级 5 级 6 级 8 级 典型频率(MHz) 80 150 260 335 功耗(mw/ MHz) 0.06 0.19(+Cache) 0.5(+Cache) 0.4(+Cache) MIPS/ MHz 0.97 1.1 1.3 1.2 架构 冯诺伊曼 哈佛 哈佛 哈佛 乘法器 8×32 8×32 16×32 16×32 表 1.4 总结了各种处理器的不同功能。 表 1.4 ARM 处理器不同功能特性 CPU 核 MMU/MPU Cache Jazelle Thumb 指令集 E ARM7TDMI 无 无 否 是 v4T 否 ARM7EJ-S 无 无 是 是 v5TEJ 是 ARM720T MMU 统一 8KBCache 否 是 v4T 否 ARM920T MMU 独立 16KB 指令和数据 Cache 否 是 v4T 否 ARM922T MMU 独立 8KB 指令和数据 Cache 否 是 v4T 否 ARM926EJ-S MMU Cache 和 TCM 可配置 是 是 v5TEJ 是 ARM940T MPU 独立 4KB 指令和数据 Cache 否 是 v4T 否 ARM946E-S MPU Cache 和 TCM 可配置 否 是 v5TE 是 ARM966E-S 无 Cache 和 TCM 可配置 否 是 v5TE 是 ARM1020E MMU 独立 32KB 指令和数据 Cache 否 是 v5TE 是 ARM1022E MMU 独立 16KB 指令和数据 Cache 否 是 v5TE 是 ARM1026EJ-S MMU Cache 和 TCM 可配置 是 是 v5TE 是 ARM1036J-S MMU Cache 和 TCM 可配置 是 是 v6 是 ARM1136JF-S MMU Cache 和 TCM 可配置 是 是 v6 是 1.5.2 ARM 芯片的选型 随着国内嵌入式应用领域的发展,ARM 芯片必然会获得广泛的重视和应用。但是由于 ARM 芯片有多达十 几种的芯核结构、70 多芯片生产厂家以及千变万化的内部功能配置组合,开发人员在选择方案时会有一定 的困难。所以对 ARM 芯片做对比研究是十分必要的。 1.ARM 芯片选择的一般原则  从应用角度看,在选择 ARM 芯片时应从以下几个方面考虑。 (1)ARM 芯核 如果希望使用 Windows CE 或 Linux 等操作系统以减少软件开发时间,就需要选择 ARM720T 以上带有 MMU(Memory Management Unit)功能的 ARM 芯片,ARM720T、StrongARM、ARM920T、ARM922T、 ARM946T 都带有 MMU 功能。而 ARM7TDMI 没有 MMU,不支持 Windows CE 和大部分的 Linux;但目 前有 uCLinux 等少数几种 Linux 不需要 MMU 的支持。 (2)系统时钟控制器 专业始于专注 卓识源于远见       ‐  9  ‐            系统时钟决定了 ARM 芯片的处理速度。ARM7 的处理速度为 0.97MIPS/MHz,常见的 ARM7 芯片系统主 时钟为 20~133MHz,ARM9 的处理速度为 1.1MIPS/MHz,常见的 ARM9 的系统主时钟为 100~233MHz, ARM10 最高可以达到 700MHz。不同芯片对时钟的处理不同,有的芯片只有一个主时钟频率,这样的芯片 可能不能同时顾及 UART 和音频时钟准确性,如 Cirrus Logic 的 EP7312 等;有的芯片内部时钟控制器可 以分别为 CPU 核和 USB、UART、DSP、音频等功能部件提供同频率的时钟,如 PHILIPS 公司 SAA7750 等芯片。 (3)内部存储器容量 在不需要大容量存储器时,可以考虑选用有内置存储器的 ARM 芯片。表 1.5 列出了内置存储器的 ARM 芯 片。 表 1.5 内置存储器的 ARM 芯片 芯 片 型 号 供 应 商 Flash 容量 ROM 容量 SDAM 容量 AT91F40162 ATMEL 2MB 256KB 4KB AT91FR4081 ATMEL 1MB 128KB SAA7750 Philips 384KB 64KB PUC3030A Micornas 256KB 56KB HMS30C7272 Hynix 192KB LC67F500 Snayo 640KB 32KB (4)USB 接口 许多 ARM 芯片内置有 USB 控制器,有些芯片甚至同时有 USB Host 和 USB Slave 控制器。表 1.6 显示了 内置 USB 控制器的 ARM 芯片。 表 1.6 内置 USB 控制器的 ARM 芯片 芯 片 型 号 ARM 内核 供 应 商 USB Slave USB Host IIS 接口 S3C2410 ARM920T Samsung 1 2 1 S3C2400 ARM920T Samsung 1 2 1 S5N8946 ARM7TDMI Samsung 1 0 0 L7205 ARM720T Linkup 1 1 0 L7210 ARM720T Linkup 1 1 0 EP9312 ARM920T Cirrus logic 0 3 1 Dragonball MX1 ARM920T Motorola 1 0 1 SAA7750 ARM720T Plilips 1 0 1 TMS320DSC2x ARM7TDMI TI 1 0 0 PUC3030A ARM7TDMI Micronas 1 0 5 ML67100 ARM7TDMI OKI 1 0 0 ML7051LA ARM7TDMI OKI 1 0 0 SA-1100 StrongARM Intel 1 0 0 续表 芯 片 型 号 ARM 内核 供 应 商 USB Slave USB Host IIS 接口 LH7979531 ARM7TDMI Sharp 1 0 0 GMS320C7201 ARM720T Hynix 1 0 1 (5)GPIO 数量 在某些芯片供应商提供的说明书中,往往申明的是最大可能的 GPIO 数量,但是有许多引脚是和地址线、 数据线、串口线等引脚复用的。这样在系统设计时需要计算实际可以使用的 GPIO 数量。 (6)中断控制器 专业始于专注 卓识源于远见       ‐  10  ‐            ARM 内核只提供快速中断(FIQ)和标准中断(IRQ)两个中断向量。但各个半导体厂家在设计芯片时加 入了自己定义的中断控制器,以便支持诸如串行口、外部中断、时钟中断等硬件中断。外部中断控制是选 择芯片必须考虑的重要因素,合理的外部中断设计可以很大程度地减少任务调度工作量。例如 PHILIPS 公 司的 SAA7750,所有 GPIO 都可以设置成 FIQ 或 IRQ,并且可以选择上升沿、下降沿、高电平和低电平 4 种中断方式。这使得红外线遥控接收、指轮盘和键盘等任务都可以作为背景程序运行。而 Cirrus Logic 公 司的 EP7312 芯片只有 4 个外部中断源,并且每个中断源都只能是低电平或高电平中断,这样在接收红外 线信号的场合必须用查询方式,浪费大量 CPU 时间。 (7)IIS(Integrate Interface of Sound)接口 即集成音频接口。如果设计音频应用产品,IIS 总线接口是必需的。 (8)nWAIT 信号 这是一个外部总线速度控制信号。不是每个 ARM 芯片都提供这个信号引脚,利用这个信号与廉价的 GAL 芯片就可以实现与符合 PCMCIA 标准的 WLAN 卡和 Bluetooth 卡的接口,而不需要外加高成本的 PCMCIA 专用控制芯片。另外,当需要扩展外部 DSP 协处理器时,此信号也是必需的。 (9)RTC(Real Time Clock) 很多 ARM 芯片都提供实时时钟功能,但方式不同。如 Cirrus Logic 公司的 EP7312 的 RTC 只是一个 32 位 计数器,需要通过软件计算出年月日时分秒;而 SAA7750 和 S3C2410 等芯片的 RTC 直接提供年月日时分 秒格式。 (10)LCD 控制器 有些 ARM 芯片内置 LCD 控制器,有的甚至内置 64KB 彩色 TFT LCD 控制器。在设计 PDA 和手持式显示 记录设备时,选用内置 LCD 控制器的 ARM 芯片(如 S3C2410)较为适宜。 (11)PWM 输出 有些 ARM 芯片有 2~8 路 PWM 输出,可以用于电机控制或语音输出等场合。 (12)ADC 和 DAC 有些 ARM 芯片内置 2~8 通道 8~12 位通用 ADC,可以用于电池检测、触摸屏和温度监测等。PHILIPS 的 SAA7750 更是内置了一个 16 位立体声音频 ADC 和 DAC,并且带耳机驱动。 (13)扩展总线 大部分 ARM 芯片具有外部 SDRAM 和 SRAM 扩展接口,不同的 ARM 芯片可以扩展的芯片数量即片选线 数量不同,外部数据总线有 8 位、16 位或 32 位。为某些特殊应用设计的 ARM 芯片(如德国 Micronas 的 PUC3030A)没有外部扩展功能。 (14)UART 和 IrDA 几乎所有的 ARM 芯片都具有 1~2 个 UART 接口,可以用于和 PC 机通信或用 Angel 进行调试。一般的 ARM 芯片通信波特率为 115200bit/s,少数专为蓝牙技术应用设计的 ARM 芯片的 UART 通信波特率可以 达到 920kbit/s,如 Linkup 公司 L7205。 (15)DSP 协处理器 表 1.7 总结了 ARM+DSP 结构的 ARM 芯片。 表 1.7 ARM+DSP 结构的 ARM 芯片 芯 片 型 号 供 应 商 DSP Core DSP MIPS 应 用 TMS320DSC2X TI 16bit C5000 500 数码照相机 Dragonball MX1 Motorola 24bit 56000 MP3 播放器 SAA7750 Philips 24bit EPIC 73 CD-MP3 VWS22100 Philips 16bit OAK GSM STLC1502 ST D950 52 VoIP GMS30C3201 Hynix 16bit Piccolo AT75C220 ATMEL 16bit OAK 40 AT75C310 ATMEL 16bit OAK 专业始于专注 卓识源于远见       ‐  11  ‐            AT75C320 ATMEL 16bit OAK 40×2 L7205 Linkup 16bit Piccolo 60×2 无线应用 L7210 Linkup 16bit Piccolo Quatro OAK 16bit OAK (16)内置 FPGA 有些 ARM 芯片内置有 FPGA,适合于通信等领域。表 1.8 总结了 ARM+FPGA 结构的 ARM 芯片。 表 1.8 ARM+FPGA 结构的 ARM 芯片 芯 片 型 号 供 应 商 ARM 芯核 FPGA 门数 引 脚 数 EPXA1 Altera ARM922T 100000 484 EPXA4 Altera ARM922T 400000 672 EPXA10 Altera ARM922T 1000000 1020 TA7S20 系列 Triscend ARM7TDMI 多种 多种 (17)时钟计数器和看门狗 一般 ARM 芯片都具有 2~4 个 16 位或 32 位时钟计数器和一个看门狗计数器。 (18)电源管理功能 ARM 芯片的耗电量与工作频率成正比,一般 ARM 芯片都有低功耗模式、睡眠模式和关闭模式。 (19)DMA 控制器 有些 ARM 芯片内部集成有 DMA(Direct Memory Access)接口,可以和硬盘等外部设备高速交换数据, 同时减少数据交换时对 CPU 资源的占用。 另外,还可以选择的内部功能部件有:HDLC、SDLC、CD-ROM Decoder、Ethernet MAC、VGA controller、 DC-DC。可以选择的内置接口有:IIC、SPDIF、CAN、SPI、PCI、PCMCIA。 最后需说明的是封装问题。ARM 芯片现在主要的封装有 QFP、TQFP、PQFP、LQFP、BGA、LBGA 等形 式,BGA 封装具有芯片面积小的特点,可以减少 PCB 板的面积,但是需要专用的焊接设备,无法手工焊 接。另外一般 BGA 封装的 ARM 芯片无法用双面板完成 PCB 布线,需要多层 PCB 板布线。 2.多芯核结构 ARM 芯片的选择  为了增强多任务处理能力、数学运算能力、多媒体以及网络处理能力,某些供应商提供的 ARM 芯片内置 多个芯核,目前常见的 ARM+DSP,ARM+FPGA,ARM+ARM 等结构。 (1)多 ARM 芯核 为了增强多任务处理能力和多媒体处理能力,某些 ARM 芯片内置多个 ARM 芯核。例如 Portal player 公司的 PP5002 内部集成了两个 ARM7TDMI 芯核,可以应用于便携式 MP3 播放器的编码器或解码器。 从科胜讯公司(Conexant)分离出来的专门致力于高速通信芯片设计生产的 MinSpeed 公司在其多款高 速通信芯片中集成了 2~4 个 ARM7TDMI 内核。 (2)ARM 芯核+DSP 芯核 为了增强数学运算功能和多媒体处理功能,许多供应商在其 ARM 芯片内增加了 DSP 协处理器。通常加入 的 DSP 芯核有 ARM 公司的 Piccolo DSP 芯核、OAK 公司 16 位定点 DSP 芯核、TI 的 TMS320C5000 系列 DSP 芯核和 Motorola 的 56K DSP 芯核等。见表 1.7。 (3)ARM 芯核+FPGA 为了提高系统硬件的在线升级能力,某些公司在 ARM 芯片内部集成了 FPGA。见表 1.8。 3.选择方案举例  专业始于专注 卓识源于远见       ‐  12  ‐            表 1.9 列举的最佳方案仅供参考,由于 SoC 集成电路的发展非常迅速,今天的最佳方案到明天就可能不是 最佳的了。因此任何时候在选择方案时,都应广泛搜寻一下主要的 ARM 芯片供应商,以找出最适合的芯 片。 表 1.9 最佳应用方案推荐 应 用 第 一 方 案 第 二 方 案 备 注 高档 PDA S3C2410 Dragon ball MX1 便携 CD MP3 播放器 SAA7750 USB 和 CDROM 解码器 FLASH MP3 播放器 SAA7750 PUC3030A 内置 USB 和 FLASH WLAN和BT应用产品 L7205,L7210 Dragon ball MX1 高速串口和 PCMCIA 接口 VoiceOver IP STLC1502 数码照相机 TMS320DSC24 TMS320DSC21 内置高速图像处理 DSP 续表 应 用 第 一 方 案 第 二 方 案 备 注 便携式语音 email 机 AT75C320 AT75C310 内置双 DSP,可以分别处理 MODEM 和语音 GSM 手机 VWS22100 AD20MSP430 专为 GSM 手机开发 ADSL Modem S5N8946 MTK-20141 电视机顶盒 GMS30C3201 VGA 控制器 3G 移动电话机 MSM6000 OMAP1510 10G 光纤通信 MinSpeed 公司系列 ARM 芯片 多 ARM 核+DSP 核 1.6 ARM 开发工具 用户选用 ARM 处理器开发嵌入式产品时,选择合适的开发工具可以加快开发进度,节省开发成本。根据 功能不同,ARM 应用软件的开发工具分别有编译软件、汇编软件、连接软件、调试软件、评估板、JTAG 仿真器和在线仿真器等,目前世界上大约有四十多家公司提供以上不同种类的开发产品。 Realview 系列开发工具的英文全称为 Realview Developer Suite,是 ARM 公司(www.arm.com)为方便用 户在 ARM 芯片上进行应用软件开发而推出的一整套集成开发工具。该套工具包括软件开发套件和硬件仿 真工具。经过 ARM 公司逐年的维护和更新,目前的最新版本为 3.0。 ARM RVDS 起源于 ARM ADS(ARM Developer Suite),它对一些 ADS 的模块进行了增强并替换了一些 ADS 的组成部分。它支持几乎所有的 ARM 处理器,包括最新的 ARMv6 体系结构。支持的操作系统除了 Windows 外,还有 Linux。 ARM RVDS 主要包括以下几部分。 1.Realview compilation Tools  Realview compilation Tools 由编译器、汇编器和连接器组成。ARM 公司针对 ARM 系列每一种结构都进行 了专门的优化和处理,这一点除了作为 ARM 结构的设计者 ARM 公司外,其他公司都无法办到。 Realview compilation Tools 主要包括以下组件:  ARM/Thumb 汇编器 armasm;  连接器 armlink;  格式转换工具 fromelf;  库管理器 armar;  C 和 C++应用程序库; 专业始于专注 卓识源于远见       ‐  13  ‐             工程管理。 这些工具的使用过程如图 1.1 所示。 以上工具为命令行开发工具,同时也被集成在它的 IDE 开发环境中。 图 1.1 ARM 开发工具组件使用过程 2.集成开发环境  (1)CodeWarrior CodeWarrior 是 Metrowerks 公司一套比较著名的集成开发环境,是一个直观、易用的环境,集成了很多 ARM 开发工具。CodeWarrior 界面风格独特,如图 1.2 所示。 图 1.2 CodeWarrior 集成开发环境 专业始于专注 卓识源于远见       ‐  14  ‐            CodeWarrior 包含项目管理、代码生成、语法敏感编辑器、C/C++源文件浏览器、类浏览器以及文件比较器 等。项目管理有直观的 GUI,可以通过隐藏底层目录结构来简单地管理复杂的项目。强大的内置编辑器是 编写软件的理想工具。可配置的接口让用户可以根据喜好裁减外形,以提高效率。 (2)AXD AXD 即 ARM 扩展调试器(ARM extended Debugger)是运行在主机上的嵌入式开发调试工具。其界面如 图 1.3 所示。 图 1.3 AXD 图形界面 AXD 包含新型的 GUI、图形窗口管理、数据显示、命令行接口等组件。它使用户不用改变调试器就可以 选择不同的调试目标,如 ARMulator、Angel 或 Multi_ICE 等,扩展了 ARM 调试目标接口。 3.Multi_ICE  Multi_ICE 是 ARM 公司自己的 JTAG 仿真器,其 JTAG 链时钟可以设置为 5kHz~10MHz。它支持 ARM7、 ARM9、ARM9E、ARM10 等 ARM 系列处理器。 Multi_ICE 主要有以下特点。  快速的下载和单步速度;  用户控制的输入、输出位;  可编程的 JTAG 位传送速率;  开放的接口,允许调试非 ARM 核和 DSP;  网络连接到多个调试器。 联系方式 集团官网:www.hqyj.com 嵌入式学院:www.embedu.org 移动互联网学院:www.3g-edu.org 企业学院:www.farsight.com.cn 物联网学院:www.topsight.cn 研发中心:dev.hqyj.com 集团总部地址:北京市海淀区西三旗悦秀路北京明园大学校内 华清远见教育集团 专业始于专注 卓识源于远见       ‐  15  ‐            北京地址:北京市海淀区西三旗悦秀路北京明园大学校区,电话:010-82600386/5 上海地址:上海市徐汇区漕溪路银海大厦 A 座 8 层,电话:021-54485127 深圳地址:深圳市龙华新区人民北路美丽 AAA 大厦 15 层,电话:0755-22193762 成都地址:成都市武侯区科华北路 99 号科华大厦 6 层,电话:028-85405115 南京地址:南京市白下区汉中路 185 号鸿运大厦 10 层,电话:025-86551900 武汉地址:武汉市工程大学卓刀泉校区科技孵化器大楼 8 层,电话:027-87804688 西安地址:西安市高新区高新一路 12 号创业大厦 D3 楼 5 层,电话:029-68785218 《ARM 系列处理器应用技术完全手册》 作者:华清远见 第 2 章 ARM 体系结构 专业始于专注 卓识源于远见       ‐  2  ‐            2.1 ARM 体系结构的特点 ARM 内核采用精简指令集结构(RISC,Reduced Instruction Set Computer)体系结构。RISC 技术产生于上 世纪 70 年代。其目标是设计出一套能在高时钟频率下单周期执行、简单而有效的指令集,RISC 的设计重 点在于降低硬件执行指令的复杂度,这是因为软件比硬件容易提供更大的灵活性和更高的智能。与其相对 的传统复杂指令级计算机(CISC)则更侧重于硬件执行指令的功能性,使 CISC 指令变得更复杂。 RISC 的设计思想主要有以下特性。  Load/Store 体系结构。 Load/Store 体系结构也称为寄存器/寄存器体系结构或者 RR 系统结构。在这类机器中,操作数和运算结果 不是通过主存储器直接取回而是借用大量标量和矢量寄存器来取回的。与 RR 体系结构相反,还有一种存 储器/存储器体系结构,在这种体系结构中,源操作数的中间值和最后的运算结果是直接从主存储器中取回 的。这类机器的缩写符号是 SS 体系结构。  固定长度指令。 固定长度指令使得机器译码变得比较容易。由于指令简单,需要更多的指令来完成相同的工作,但是随着 存储器存取速度的提高,处理器可以更快地执行较大代码段(即大量指令)。  硬联控制。 RISC 机以硬联控制指令为特点,而 CISC 的微代码指令则相反。使用 CISC(常常是可变长度的)指令集 时处理器的语义效率最大,而简单指令往往容易被机器翻译。像 CISC 那样通过执行较少指令来完成工作 未必省时,因为还要包括微代码译码所需要的时间。因此,由硬件实现指令在执行时间方面提供了更好的 平衡。除此之外,还节省了芯片上用于存储微代码的空间并且消除了翻译微代码所需的时间。  流水线。 指令的处理过程被拆分为几个更小的、能够被流水线并行执行的单元。在理想情况下,流水线每周期前进 一步,可获得更高的吞吐率。  寄存器。 RICS 处理器拥有更多的通用寄存器,每个寄存器都可存放数据或地址。寄存器可为所有的数据操作提供 快速的局部存储访问。 表 2.1 总结了 RISC 和 CISC 之间主要的区别。 表 2.1 RISC 和 CISC 之间主要的区别 指 标 RISC CISC 指令集 一个周期执行一条指令,通过简单指令 的组合实现复杂操作;指令长度固定 指令长度不固定,执行需要多个周期 流水线 流水线每周期前进一步 指令的执行需要调用微代码的一个微程序 寄存器 更多通用寄存器 用于特定目的的专用寄存器 Load/Store 结构 独立的 Load 和 Store 指令完成数据在寄 存器和外部存储器之间的传输 处理器能够直接处理存储器中的数据 为了使 ARM 指令集能够更好地满足嵌入式应用的需要,ARM 指令集和单纯的 RISC 定义有以下几方面的 不同。  一些特定指令的周期数可变 并非所有的 ARM 指令都是单周期的。例如,多寄存器转载/存储的 Load/Store 指令的周期数就不确定,必 须根据被传送的寄存器个数而定。如果是访问连续的存储器地址,就可以改善性能,因为连续的存储器访 问通常比随机访问要快。同时,代码密度也得到了提高,因为在函数的起始和结尾,多个寄存器的传输是 很常用的操作。  内嵌桶形移位器产生更复杂的指令 专业始于专注 卓识源于远见       ‐  3  ‐            内嵌桶形移位器是一个硬件部件,在一个输入寄存器被一条指令使用之前,内嵌桶形移位器可以处理该寄 存器中的数据。它扩展了许多指令的功能,改善了内核的性能,提高了代码密度。  Thumb 指令集 ARM 处理器根据 RICS 原理设计,但是由于各种原因,在低代码密度上它比其他多数 RICS 要好一些,然 而它的代码密度仍不如某些 CISC 处理器。在代码密度重要的场合,ARM 公司在某些版本的 ARM 处理器 中加入了一个称为 Thumb 结构的新型机构。Thumb 指令集是原来 32 位 ARM 指令集的 16 位压缩形式,并 在指令流水线中使用了动态解压缩硬件。Thumb 代码密度优于多数 CISC 处理器达到的代码密度。  条件执行 只有当某个特定条件满足时指令才会被执行。这个特性可以减少分支指令数目,从而改善性能,提高代码 密度。  DSP 指令 一些功能强大的数字信号处理(DSP)指令被加入到标准的 ARM 指令中,以支持快速的 16×16 位乘法操 作及饱和运算。在某些应用中,传统的方法需要微处理器加上 DSP 才能实现。这些增强指令,使得 ARM 处理器也能够满足这些应用的需要。 综上所述,ARM 体系结构的主要特征如下:  大量的寄存器,它们都可以用于多种用途;  Load/Store 体系结构;  每条指令都条件执行;  多寄存器的 Load/Store 指令;  能够在单时钟周期执行的单条指令内完成一项普通的移位操作和一项普通的 ALU 操作;  通过协处理器指令集来扩展 ARM 指令集,包括在编程模式中增加了新的寄存器和数据类型。 如果把 Thumb 指令集也当作 ARM 体系结构的一部分,那么还可以加上:  在 Thumb 体系结构中以高密度 16 位压缩形式表示指令集。 2.2 流水线 2.2.1 流水线的概念与原理 处理器按照一系列步骤来执行每一条指令。典型的步骤如下: ① 从存储器读取指令(fetch); ② 译码以鉴别它是属于哪一条指令(dec); ③ 从指令中提取指令的操作数(这些操作数往往存在于寄存器中)(reg); ④ 将操作数进行组合以得到结果或存储器地址(ALU); ⑤ 如果需要,则访问存储器以存储数据(mem); ⑥ 将结果写回到寄存器堆(res)。 并不是所有的指令都需要上述每一个步骤,但是,多数指令需要其中的多个步骤。这些步骤往往使用不同 的硬件功能,例如,ALU 可能只在第 4 步中用到。因此,如果一条指令不是在前一条指令结束之前就开始, 那么在每一步骤内处理器只有少部分的硬件在使用。 有一种方法可以明显改善硬件资源的使用率和处理器的吞吐量,这就是当前一条指令结束之前就开始执 行下一条指令,即通常所说的流水线(Pipeline)技术。流水线是 RISC 处理器执行指令时采用的机制。 使用流水线,可在取下一条指令的同时译码和执行其他指令,从而加快执行的速度。可以把流水线看作 是汽车生产线,每个阶段只完成专门的处理器任务。 采用上述操作顺序,处理器可以这样来组织:当一条指令刚刚执行完步骤①并转向步骤②时,下一条指令 就开始执行步骤①。图 2.1 说明了这个过程。从原理上说,这样的流水线应该比没有重叠的指令执行快 6 倍,但由于硬件结构本身的一些限制,实际情况会比理想状态差一些。 专业始于专注 卓识源于远见       ‐  4  ‐            2.2.2 流水线的分类 从 Acorn Computer 公司在 1983~1985 年间开发的第一个 3µm 器件,到 ARM 公司在 1990~1995 年间开发 的 ARM6 和 ARM7,ARM 整数处理器核的组织结构变化很小,这些处理器都是采用 3 级流水线,而这一 时期 CMOS 工艺的发展,几乎将特征尺寸减少了一个数量级。因此,核的性能提高很快,但基本的操作原 理大部分没有变化。 图 2.1 流水线的指令执行过程 从 1995 年以来,ARM 公司推出了几个新的 ARM 核。它们采用 5 级流水线和哈佛架构,获得了显著的高 性能。例如,ARM9 增加了存储器访问段和回写段,这使得 ARM9 的处理能力可达到平均 1.1 Dhrystone1 MISP/MHz,与 ARM7 相比,指令吞吐量提高了约 13%。 注意 在许多高性能处理器内部,一级 Cache 一般都设置有两个,其中,一个是指令 Cache,另一个 是数据 Cache。这样可以减少取指令和读操作数的访问冲突,这种结构被称为哈佛架构。 把主存储器分成两个独立编址的存储器,一个专门存放指令,称为指令存储器,简称指存;另 一个专门存放操作数,称为数据存储器,简称数存。两个存储器可以同时访问,这样就解决了 取指令和读操作数的冲突。如果在此基础上规定在执行指令阶段产生的运算结果只写到通用寄 存器中,不写到主存,那么取指令、分析指令和执行指令就可以同时进行。 ARM10 更是把流水线增加到 6 级。ARM10 的平均处理能力达到 1.3 Dhrystone MISP/MHz,与 ARM7 相比, 指令吞吐量提高了约 34%。 注意 虽然 ARM9 和 ARM10 的流水线不同,但它们都使用了与 ARM7 相同的流水线执行机制,因此 ARM7 上的代码也可以在 ARM9 和 ARM10 上运行。 1.3 级流水线 ARM 组织  3 级流水线 ARM 组织如图 2.2 所示,其主要的组成如下: ① 处理器状态寄存器堆(Rigister Bank)。它有两个读端口和一个写端口,每个端口都可以访问任意寄存 器。另外还有附加的可以访问 PC 的一个读端口和一个写端口。 注意 PC 的附加写端口可以在取指地址增加后更新 PC,读端口可以在数据地址发出之后从新开始取 指。 ② 桶形移位寄存器(Barrel Shifter)。它可以把一个操作数移位或循环移位任意位数。 1 Dhrystone 是测量处理器运算能力的最常见基准程序之一,Dhrystone 的计量单位为每秒计算多少次 Dhrystone。 专业始于专注 卓识源于远见       ‐  5  ‐            ③ ALU。完成指令集要求的算术或逻辑功能。 地址寄存器 PC 写 PC 读 增值器 处理器状态寄存器堆 桶形移位 寄存器 运算器 数据输出寄存器 数据输入寄存器 指令译码器 和控制逻辑 图 2.2 3 级流水线 ARM 的组织 ④ 地址寄存器(Address Register)和增值器(Incrementer)。可选择和保存所用的存储器地址并在需要时 产生顺序地址。 ⑤ 数据输出寄存器(data-out register)和数据输入寄存器(data-in register)。用于保存传输到存储器和从 存储器输出的数据。 ⑥ 指令译码器和相关的控制逻辑(instruction decode and control)。 例 2.1 显示了一条单周期指令在流水线上的执行过程。 【例 2.1】 ADD r1,r2 指令在流水线上的执行过程如图 2.3 所示。 专业始于专注 卓识源于远见       ‐  6  ‐            寄存器堆 ALU 移位寄存器 寄存器堆 移位寄存器 ALU 图 2.3 单周期指令在流水线上的执行过程 在 ADD 指令中,需要访问两个寄存器操作数,B 总线上的数据移位后与 A 总线上的数据在 ALU 中组合, 再将结果写回寄存器堆。在指令执行过程中,程序计数器的数据放在地址寄存器中,地址寄存器的数据送 入增值器。然后将增值后的数据拷贝到寄存器堆的 r15(程序计数器),同时还拷贝到地址寄存器,作为下 一次取指的地址。 到 ARM7 为止的 ARM 处理器使用简单的 3 级流水线,包括下列流水线级:  取指(fetch):从寄存器装载一条指令。  译码(decode):识别被执行的指令,并为下一个周期准备数据通路的控制信号。在这一级,指令占有 译码逻辑,不占用数据通路。  执行(excute):处理指令并将结果写回寄存器。 图 2.4 显示了 3 级流水线指令执行过程。 取指 译码 执行 图 2.4 3 级流水线 注意 在任一时刻,可能有 3 种不同的指令占有这 3 级中的每一级,因此,每一级中的硬件必须能够 独立操作。 当处理器执行简单的数据处理指令时,流水线使得平均每个时钟周期能完成 1 条指令。但 1 条指令需要 3 个时钟周期来完成,因此,有 3 个时钟周期的延时(latency),但吞吐率(throughput)是每个周期一条指 令。例 2.2 通过一个简单的例子说明了流水线的机制。 【例 2.2】 指令序列为: ADD r1 r2 SUB r3 r2 CMP r1 r3 流水线指令序列如图 2.5 所示。 专业始于专注 卓识源于远见       ‐  7  ‐            预取 ADD 译码 执行 预取 SUB 译码 执行 预取 CMP 译码 执行 图 2.5 流水线指令顺序 在第一个周期,内核从存储器取出指令 ADD;在第二个周期,内核取出指令 SUB,同时对 ADD 译码;在 第三个周期,指令 SUB 和 ADD 都沿流水线移动,ADD 被执行,而 SUB 被译码,同时又取出 CMP 指令。 可以看出,流水线使得每个时钟周期都可以执行一条指令。 当执行多条指令时,流水线的执行不一定会如图 2.5 那么规则,图 2.6 显示了有 STR 指令的流水线状态。 图 2.6 含有存储器访问指令的流水线状态 图 2.6 中在单周期指令 ADD 后出现了一条数据存储指令 STR。访问主存储器的指令用阴影表示,可以看 出在每个周期都使用了存储器。同样,在每一个周期也使用了数据通路。在执行周期、地址计算和数据传 输周期,数据通路都是被占用的。在译码周期,译码逻辑负责产生下一周期用到的数据通路的控制信号。 注意 对于 STR 这种存储器访问指令,实际是在地址计算时由译码逻辑产生下一周期数据传输所需要 的数据通路控制信号。 在图 2.6 中的指令序列中,处理器的每个逻辑单元在每个指令都是活动的。可以看出流水线的执行与存储 器访问密切相关。存储器访问限制了程序执行必须花费的指令周期数。 ARM 的流水线执行模式导致了一个结果,就是程序计数器 PC(对使用者而言为 r15)必须在当前指令执 行前计数。例如,指令在其第一个周期为下下条指令取指,这就意味着 PC 必须指向当前指令的后 8 个字 节(其后的第 2 条指令)。 当程序中必须用到 PC 时,程序员要特别注意这一点。大多数正常情况下,不用考虑这一点,它由汇编器 或编译器自动处理这些细节。 例 2.3 显示了流水线下程序计数器 PC 的使用情况。 【例 2.3】 指令序列为: 0x8000 LDR pc,[pc,#0] 0x8004 NOP 0x8008 DCD jumpAdress 专业始于专注 卓识源于远见       ‐  8  ‐            当指令 LDR 处于执行阶段时,pc=address+8 即 0x8008。 2.5 级流水线 ARM 组织  所有的处理器都要满足对高性能的要求。直到 ARM7 为止,在 ARM 核中使用的 3 级流水线的性价比是很 高的。但是,为了得到更高的性能,需要重新考虑处理器的组织结构。执行一个给定的程序需要的时间由 下式决定: Tprog = (Ninst×CPI)/ fclk 式中: Ninst:表示在程序中执行的 ARM 指令数; CPI:表示每条指令的平均时钟周期; fclk:表示处理器的时钟频率。 因为对给定程序(假设使用给定的优化集并用给定的编译器来编译)Ninst 是常数,所以,仅有两种方法来 提供性能。 第一,提高时钟频率。时钟频率的提高,必然引起指令执行周期的缩短,所以要求简化流水线每一级的逻 辑,流水线的级数就要增加。 第二,减少每条指令的平均指令周期数 CPI。这就要求重新考虑 3 级流水线 ARM 中多于 1 个流水线周期 的实现方法,以便使其占有较少的周期,或者减少因指令相关造成的流水线停顿,也可以将两者结合起来。 3 级流水线 ARM 核在每一个时钟周期都访问存储器,或者取指令,或者传输数据。只是抓紧存储器不用 的几个周期来改善系统系统性能,效果是不明显的。为了改善 CPI,存储器系统必须在每个时钟周期中给 出多于一个的数据。方法是在每个时钟周期从单个存储器中给出多于 32 位数据,或者为指令或数据分别 设置存储器。 基于以上原因,较高性能的 ARM 核使用了 5 级流水线,而且具有分开的指令和数据存储器。把指令的执 行分割为 5 部分而不是 3 部分,进而可以使用更高的时钟频率,分开的指令和数据存储器使核的 CPI 明显 减少。 注意 分开的指令和数据存储器。一般是分开的 Cache 连接到统一的指令和数据存储器上。 在 ARM9TDMI 中使用了典型的 5 级流水线。ARM9TDMI 的组织结构如图 2.7 所示。 5 级流水线包括下面的流水线级:  取指(fetch):从存储器中取出指令,并将其放入指令流水线。  译码(decode):指令被译码,从寄存器堆中读取寄存器操作数。在寄存器堆中有 3 个操作数读端口, 因此,大多数 ARM 指令能在 1 个周期内读取其操作数。  执行(execute):将其中一个操作数移位,并在 ALU 中产生结果。如果指令是 Load 或 Store 指令,则 在 ALU 中计算存储器的地址。  缓冲/数据(buffer/data):如果需要则访问数据存储器,否则 ALU 只是简单地缓冲一个时钟周期。  回写(write-back):将指令的结果回写到寄存器堆,包括任何从寄存器读出的数据。 图 2.8 显示了 5 级流水线指令的执行过程。 专业始于专注 卓识源于远见       ‐  9  ‐            指令 Cache 指令译码 寄存器读 乘 +4 移位 +4 ALU MUX 数据 Cache 寄存器写 移位/符号 扩展 pc+8 取指 指令译码 执行 缓冲/数据 回写 前变址 Load/Store 后变址 图 2.7 5 级流水线的组织结构 图 2.8 5 级流水线 在程序执行过程中,PC 值是基于 3 级流水线操作特性的。5 级流水线中提前 1 级来读取指令操作数,得到 的值是不同的(PC+4 而不是 PC+8)。这产生的代码不兼容是不容许的。但 5 级流水线 ARM 完全仿真 3 级流水线的行为。在取指级增加的 PC 值被直接送到译码级的寄存器,穿过两极之间的流水线寄存器。下 一条指令的 PC+4 等于当前指令的 PC+8,因此,未使用额外的硬件便得到了正确的 r15。 3.6 级流水线 ARM 组织  在 ARM10 中,将流水线的级数增加到 6 级,使系统的平均处理能力达到了 1.3Dhrystone MISP/MHz。图 2.9 显示了 6 级流水线上指令的执行过程。 图 2.9 6 级流水线 2.2.3 影响流水线性能的因素 专业始于专注 卓识源于远见       ‐  10  ‐            1.互锁  在典型的程序处理过程中,经常会遇到这样的情形,即一条指令的结果被用做下一条指令的操作数。如例 2.4 所示。 【例 2.4】 有如下指令序列: LDR r0,[r0,#0] ADD r0,r0,r1 ;在 5 级流水线上产生互锁 从例 2.4 中可以看出,流水线的操作产生中断,因为第一条指令的结果在第二条指令取数时还没有产生。 第二条指令必须停止,直到结果产生为止。 2.跳转指令  跳转指令也会破坏流水线的行为,因为后续指令的取指步骤受到跳转目标计算的影响,因而必须推迟。 但是,当跳转指令被译码时,在它被确认是跳转指令之前,后续的取指操作已经发生。这样一来,已 经被预取进入流水线的指令不得不被丢弃。如果跳转目标的计算是在 ALU 阶段完成的,那么,在得到 跳转目标之前已经有两条指令按原有指令流读取。 解决的办法是,如果有可能最好早一些计算转移目标,当然这需要硬件支持;如果转移指令具有固定格式, 那么可以在解码阶段预测跳转目标,从而将跳转的执行时间减少到单个周期。但要注意,由于条件跳转与 前一条指令的条件码结果有关,在这个流水线中,还会有条件转移的危险。 尽管有些技术可以减少这些流水线问题的影响,但是,不能完全消除这些困难。流水线级数越多,问题就 越严重。对于相对简单的处理器,使用 3~5 级流水线效果最好。 显然,只有当所有指令都依照相似的步骤执行时,流水线的效率达到最高。如果处理器的指令非常复杂, 每一条指令的行为都与下一条指令不同,那么就很难用流水线实现。 2.3 ARM 存储器 ARM 处理器内核广泛应用于嵌入式系统和其他行业应用中。为了适应不同系统的需要,ARM 采用了灵活 多样的存储管理体系。从平板式内存映射到灵活方便的 MMU 内存管理单元,用户可以根据自己的需要使 用不同的存储管理策略。 在 ARM 体系结构中可使用的存储管理策略包括:  多类型的存储单元(可以使用 SDRAM、FLASH 等);  Cache;  写缓存;  虚拟内存地址。 另外,内存映射 I/O 机制可以使开发者灵活、方便地增加大量外设。 可以通过下面的几种方法实现对存储系统的管理:  使能 Cache,加快存储器的访问速度;  启动虚拟地址到物理地址的映射;  使用“域管理”策略,对存储单元的访问进行保护;  对 I/O 映射地址空间的访问加以限制。 标准的对 ARM 处理器的存储管理是使用协处理器 CP15 来实现的。ARM 体系的存储系统将在第 15 章详 细介绍。 专业始于专注 卓识源于远见       ‐  11  ‐            2.4 I/O 管理 ARM 系统完成 I/O 功能的标准方法是使用存储器映射 I/O。这种方法使用特定的存储器地址。当从这些地 址加载或向这些地址存储时,它们提供 I/O 功能。某些 ARM 系统也可能有直接存储器访问(DMA,Direct Memory Access)硬件。 外围设备(如串行线控制器)中包含一些寄存器。在存储器映射系统中,这些寄存器就像特定地址的存储 器一样。(在其他的系统组织中,I/O 功能可能与存储器件有不同的寻址空间。)串行线控制器可能有以下 5 种寄存器。 ① 发送数据寄存器(只写):写入这个位置的数据被送往串行线。 ② 接受数据寄存器(只读):保存从串行线送来的数据。 ③ 控制寄存器(读/写):设置数据速率,管理 RTS(请求发送)和其他类似信号。 ④ 中断使能寄存器(读/写):控制中断的硬件事件。 ⑤ 状态寄存器(读/写):指示读数据是否有效、写缓存是否满等。 要接受数据,必须用软件适当地设置器件。通常在接收到有效数据或检测到错误时产生一个中断。中断程 序必须将数据复制到缓存器中并进行错误检测。 应该注意的是,存储器映射外围寄存器的行为与存储器不同。连续两次读数据寄存器,即使对该寄存器没 有写操作,其结果也很可能不同。而对真正存储器的读是幂等的(idempotent)(可多次重复读,结果一致)。 对外围寄存器的读操作可能清除当前值,致使下一次读结果不同。这种寄存器称为读敏感(read-sensitive) 的。 当涉及读敏感寄存器时,编程必须小心。特别是不能将这种寄存器的数据复制到 Cache 存储器。 在许多 ARM 系统中,不能在用户模式下访问 I/O 寄存器。要访问这些器件,只能通过监控调用(SWI) 或通过使用这种调用的 C 库函数。 注意 在 ARM 编程中,通常将存储器的 I/O 区域标记为非 Cache 区(uncacheable),并绕过 Cache 访 问。通常 Cache 与读敏感(read-sensitive)器件相互排斥。显示帧缓存器(DisplayFrame Buffers) 也需要仔细考虑,通常也设为不可 Cache 的。 2.5 ARM 开发调试方法 用户选用 ARM 处理器开发嵌入式系统时,选择合适的开发工具可以加快开发进度,节省开发成本。因此 一套含有编辑软件、编译软件、汇编软件、链接软件、调试软件、工程管理及函数库的集成开发环境(IDE) 一般来说是必不可少的,如 ARM 公司的 RealView 开发环境。至于嵌入式实时操作系统、评估板等其他开 发工具则可以根据应用软件规模和开发计划选用。 使用集成开发环境开发基于 ARM 的应用软件,包括编辑、编译、汇编、链接等工作全部在 PC 机上即可 完成,调试工作则需要配合其他的模块或产品方可完成。目前常见的调试方法有以下几种。 1.指令集模拟器  部分集成开发环境提供了指令集模拟器,可方便用户在 PC 机上完成一部分简单的调试工作。但是,由于 指令集模拟器与真实的硬件环境相差很大,因此即使用户使用指令集模拟器调试通过的程序也有可能无法 在真实的硬件环境下运行,用户最终必须在硬件平台上完成整个应用的开发。 2.驻留监控软件  专业始于专注 卓识源于远见       ‐  12  ‐            驻留监控软件(Resident Monitors)是一段运行在目标板上的程序,集成开发环境中的调试软件通过以太 网口、并行端口、串行端口等通信端口与驻留监控软件进行交互,由调试软件发布命令通知驻留监控软件 控制程序的执行、读写存储器、读写寄存器、设置断点等。 利用驻留监控软件是一种比较低廉有效的调试方式,不需要任何其他的硬件调试和仿真设备。ARM 公司 的 Angel 就是该类软件,大部分嵌入式实时操作系统也采用该类软件进行调试,不同的是在嵌入式实时操 作系统中,驻留监控软件是作为操作系统的一个任务存在的。 驻留监控软件的不便之处在于它对硬件设备的要求比较高,一般在硬件稳定之后才能进行应用软件的开 发,同时它占用目标板上的一部分资源,而且不能对程序的全速运行进行完全仿真,所以对一些要求严格 的情况不是很适合。 3.JTAG 仿真器  JTAG 仿真器也称为 JTAG 调试器,是通过 ARM 芯片的 JTAG 边界扫描口进行调试的设备。JTAG 仿真器 比较便宜,连接比较方便,通过现有的 JTAG 边界扫描口与 ARM 处理器核通信,属于完全非插入式(即 不使用片上资源)调试。它无需目标存储器,不占用目标系统的任何端口,而这些是驻留监控软件所必需 的。另外,由于 JTAG 调试的目标程序是在目标板上执行,仿真更接近于目标硬件,因此,许多接口问题, 如高频操作限制、AC 和 DC 参数不匹配、电缆长度的限制等被最小化了。使用集成开发环境配合 JTAG 仿真器进行开发是目前采用最多的一种调试方式。 4.在线仿真器  在线仿真器使用仿真头完全取代目标板上的 ARM 处理器,可以完全仿真 ARM 芯片的行为,提供更进一 步的调试功能。但这类仿真器为了能够全速仿真时钟速度高于 100MHz 的处理器,通常必须采用极其复杂 的设计和工艺,因而其价格比较昂贵。在线仿真器通常用在 ARM 的硬件开发中,在软件的开发中较少使 用。其价格高昂是在线仿真器难以普及的原因。 联系方式 集团官网:www.hqyj.com 嵌入式学院:www.embedu.org 移动互联网学院:www.3g-edu.org 企业学院:www.farsight.com.cn 物联网学院:www.topsight.cn 研发中心:dev.hqyj.com 集团总部地址:北京市海淀区西三旗悦秀路北京明园大学校内 华清远见教育集团 北京地址:北京市海淀区西三旗悦秀路北京明园大学校区,电话:010-82600386/5 上海地址:上海市徐汇区漕溪路银海大厦 A 座 8 层,电话:021-54485127 深圳地址:深圳市龙华新区人民北路美丽 AAA 大厦 15 层,电话:0755-22193762 成都地址:成都市武侯区科华北路 99 号科华大厦 6 层,电话:028-85405115 南京地址:南京市白下区汉中路 185 号鸿运大厦 10 层,电话:025-86551900 武汉地址:武汉市工程大学卓刀泉校区科技孵化器大楼 8 层,电话:027-87804688 专业始于专注 卓识源于远见       ‐  13  ‐            西安地址:西安市高新区高新一路 12 号创业大厦 D3 楼 5 层,电话:029-68785218 《ARM 系列处理器应用技术完全手册》 作者:华清远见 第 3 章 ARM 微处理器的编程模型 专业始于专注 卓识源于远见       ‐  2  ‐            3.1 数据类型 3.1.1 ARM 的基本数据类型 ARM 采用的是 32 位架构,ARM 的基本数据类型有以下 3 种。  Byte:字节,8bit。  Halfword:半字,16bit(半字必须于 2 字节边界对齐)。  Word:字,32bit(字必须于 4 字节边界对齐)。 存储器可以看作是序号为 0~232−1 的线性字节阵列。图 3.1 所示为 ARM 存储器的组织结构。 字3 字2 字1 字节4 字节3 字节2 字节1 半字2 半字1 图 3.1 ARM 存储器组织结构 图 3.1 所示为存储器的一小片区域,其中每一个字节都有惟一的地址。字节可以占用任一位置,图中给出 了几个例子。长度为 1 个字的数据项占用一组 4 字节的位置,该位置开始于 4 的倍数的字节地址(地址最 末两位为 00)。图 3.1 中包含了 3 个这样的例子。半字占有两个字节的位置,该位置开始于偶数字节地址 (地址最末一位为 0)。 注意 ① ARM 系统结构 v4 以上版本支持以上 3 种数据类型,v4 以前版本仅支持字节和字。 ② 当将这些数据类型中的任意一种声明成 unsigned 类型时,N 位数据值表示范围为 0~2n−1 的非负数,通常使用二进制格式。 ③ 当将这些数据类型的任意一种声明成 signed 类型时,N 位数据值表示范围为−2n−1~2n−1−1 的整数,使用二进制的补码格式。 ④ 所有数据类型指令的操作数都是字类型的,如“ADD r1,r0,#0x1”中的操作数“0x1” 就是以字类型数据处理的。 ⑤ Load/Store 数据传输指令可以从存储器存取传输数据,这些数据可以是字节、半字、字。加 载时自动进行字节或半字的零扩展或符号扩展。对应的指令分别为 LDR/BSTRB(字节操作)、 LDRH/STRH(半字操作)、LDR/STR(字操作)。详见后面的指令参考。 ⑥ ARM 指令编译后是 4 个字节(与字边界对齐)。Thumb 指令编译后是 2 个字节(与半字边 界对齐)。 3.1.2 浮点数据类型 浮点运算使用在 ARM 硬件指令集中未定义的数据类型。 尽管如此,但 ARM 公司在协处理器指令空间定义了一系列浮点指令。通常这些指令全部可以通过未定义 指令异常(此异常收集所有硬件协处理器不接受的协处理器指令)在软件中实现,但是其中的一小部分也 可以由浮点运算协处理器 FPA10 以硬件方式实现。 专业始于专注 卓识源于远见       ‐  3  ‐            另外,ARM 公司还提供了用 C 语言编写的浮点库作为 ARM 浮点指令集的替代方法(Thumb 代码只能使 用浮点指令集)。该库支持 IEEE 标准的单精度和双精度格式。C 编译器有一个关键字标志来选择这个历程。 它产生的代码与软件仿真(通过避免中断、译码和浮点指令仿真)相比既快又紧凑。 3.1.3 存储器大/小端 从软件角度看,内存相对于一个大的字节数组,其中每个数组元素(字节)都是可寻址的。 ARM 支持大端模式(big-endian)和小端模式(little-endian)两种内存模式。 图 3.2 和图 3.3 分别显示了内存的大端模式和小端模式。 MSB LSB 0 31 M+3 M+2 M+1 M+0 图 3.2 大端模式 图 3.3 小端模式 下面的例子显示了使用内存大/小端(big/little endian)的存取格式。 【例 3.1】 程序执行前: r0=0x11223344 执行指令: r1=0x100 STR r0,[r1] LDRB r2,[r1] 执行后: 小端模式下:r2=0x44 大端模式下:r2=0x11 上面的例子向我们提示了一个潜在的编程隐患。在大端模式下,一个字的高地址放的是数据的低位,而在 小端模式下,数据的低位放在内存中的低地址。要小心对待存储器中一个字内字节的顺序。 3.2 处理器工作模式 专业始于专注 卓识源于远见       ‐  4  ‐            ARM 处理器共有 7 种工作模式,如表 3.1 所示。 表 3.1 ARM 处理器的工作模式 处理器工作模式 简 写 描 述 用户模式(User) usr 正常程序执行模式,大部分任务执行在这种模式下 快速中断模式(FIQ) fiq 当一个高优先级(fast)中断产生时将会进入这种模式,一 般用于高速数据传输和通道处理 外部中断模式(IRQ) irq 当一个低优先级(normal)中断产生时将会进入这种模式, 一般用于通常的中断处理 特权模式(Supervisor) svc 当复位或软中断指令执行时进入这种模式,是一种供操作系 统使用的保护模式 数据访问中止模式(Abort) abt 当存取异常时将会进入这种模式,用于虚拟存储或存储保护 未定义指令中止模式(Undef) und 当执行未定义指令时进入这种模式,有时用于通过软件仿真 协处理器硬件的工作方式 系统模式(System) sys 使用和 User 模式相同寄存器集的模式,用于运行特权级操 作系统任务 除用户模式外的其他 6 种处理器模式称为特权模式(Privileged Modes)。在这些模式下,程序可以访问所 有的系统资源,也可以任意地进行处理器模式切换。其中的 5 种又称为异常模式,分别为:  FIQ(Fast Interrupt reQuest);  IRQ(Interrupt request);  管理(Supervisor);  中止(Abort);  未定义(Undefined)。 处理器模式可以通过软件控制进行切换,也可以通过外部中断或异常处理过程进行切换。 大多数的用户程序运行在用户模式下。当处理器工作在用户模式时,应用程序不能够访问受操作系统保护 的一些系统资源,应用程序也不能直接进行处理器模式切换。当需要进行处理器模式切换时,应用程序可 以产生异常处理,在异常处理过程中进行处理器模式切换。这种体系结构可以使操作系统控制整个系统资 源的使用。 当应用程序发生异常中断时,处理器进入相应的异常模式。在每一种异常模式中都有一组专用寄存器以供 相应的异常处理程序使用,这样就可以保证在进入异常模式时用户模式下的寄存器(保存程序运行状态) 不被破坏。 系统模式,不能有任何异常进入。仅 ARM 体系结构 v4 及以上版本有该模式。它和用户模式具有完全相同 的寄存器。但是系统模式属于特权模式,可以访问所有的系统资源,也可以直接进行处理器模式切换,它 主要供操作系统任务使用。通常操作系统的任务需要访问所有的系统资源,同时该任务仍然使用用户模式 的寄存器组而不是异常模式下相应的寄存器组,这样可以保证当异常中断发生时任务状态不被破坏。 3.3 ARM 寄存器组织 ARM 处理器有 37 个 32 位长的寄存器。  1 个用作 PC(Program Counter)。  1 个用作 CPSR(Current Program Status Register)。  5 个用作 SPSR(Saved Program Status Registers)。  30 个用作通用寄存器。 注意 以上 37 个寄存器中,1 个 CPSR 和 5 个 SPSR 通称为状态寄存器,虽然这些寄存器是 32 位的, 但目前只使用了其中的 12 位。除了这 6 个状态寄存器外,其余的 31 个寄存器又称为通用寄存器。 专业始于专注 卓识源于远见       ‐  5  ‐            ARM 处理器共有 7 种不同的处理器模式,在每一种处理器模式中有一组相应的寄存器组。表 3.2 显示了 ARM 的寄存器组织概要。 表 3.2 寄存器组织概要 User FIQ IRQ SVC Undef Abort R0 User mode R0~R7,R15,and CPSR User mode R0~R12,R15 and CPSR User mode R0~R12,R15 and CPSR User mode R0~R12,R15 and CPSR User mode R0~R12,R15 and CPSR R1 R2 R3 R4 R5 R6 R7 R8 R8 R9 R9 R10 R10 续表 User FIQ IRQ SVC Undef Abort R11 R11 R12 R12 R13(SP) R13(SP) R13 R13 R13 R13 R14(LR) R14(LR) R14 R14 R14 R14 R15(PC) CPSR SPSR SPSR SPSR SPSR SPSR 注意 System 模式使用和 User 模式相同的寄存器集 当前处理器的模式决定着哪组寄存器可操作,任何模式都可以存取。  相应的 r0~r12。  相应的 r13(the stack pointer, sp)和 r14(the link register, lr)。  相应的 r15(the program counter, pc)。  相应的 CPSR(current program status register, cpsr)。 特权模式(除 System 模式)还可以存取。  相应的 SPSR(saved program status register)。 3.3.1 通用寄存器 通用寄存器根据其分组与否和使用目的分为以下 3 类。  未分组寄存器(The unbanked registers),包括 r0~r7。  分组寄存器(The banked register),包括 r8~r14。  程序计数器(Program Counter),即 r15。 1.未分组寄存器  专业始于专注 卓识源于远见       ‐  6  ‐            未分组寄存器包括 r0~r7。顾名思义,在所有处理器模式下对于每一个未分组寄存器来说,指的都是同一 个物理寄存器。未分组寄存器没有被系统用于特殊的用途,任何可采用通用寄存器的应用场合都可以使用 未分组寄存器。但由于其通用性,在异常中断所引起的处理器模式切换时,其使用的是相同的物理寄存器, 所以也就很容易使寄存器中的数据被破坏。 2.    分组寄存器  r8~r14 是分组寄存器,它们每一个访问的物理寄存器取决于当前的处理器模式。 对于这些分组寄存器 r8~r12 来说,每个寄存器对应两个不同的物理寄存器。一组用于除 FIQ 模式外的所 有处理器模式,而另一组则专门用于 FIQ 模式。这样的结构设计有利于加快 FIQ 的处理速度。不同模式下 寄存器的使用,要使用寄存器名后缀加以区分,例如,当使用 FIQ 模式下的寄存器时,寄存器 r8 和寄存 器 r9 分别记做 r8_fiq, r9_fiq;当使用用户模式下的寄存器时,寄存器 r8 和 r9 分别记做 r8_usr, r9_usr 等。 在 ARM 体系结构中,r8~r12 没有任何指定的其他的用途,所以当 FIQ 中断到达时,不用保存这些通用寄 存器,也就是说 FIQ 处理程序可以不必执行保存和恢复中断现场的指令,从而可以使中断处理过程非常迅 速。所以 FIQ 模式常被用来处理一些时间紧急的任务,如 DMA 处理。 对于分组寄存器 r13 和 r14 来说,每个寄存器对应 6 个不同的物理寄存器。其中的一个是用户模式和系统 模式公用的,而另外 5 个分别用于 5 种异常模式。访问时需要指定它们的模式。名字形式如下:  r13_  r14_ 其中可以是以下几种模式之一:usr、svc、abt、und、irp 及 fiq。 r13 寄存器在 ARM 中常用作堆栈指针,称为 SP。当然,这只是一种习惯用法,并没有任何指令强制性的 使用 r13 作为堆栈指针,用户完全可以使用其他寄存器作为堆栈指针。而在 Thumb 指令集中,有一些指令 强制性的将 r13 作为堆栈指针,如堆栈操作指令。 每一种异常模式拥有自己的 r13。异常处理程序负责初始化自己的 r13,使其指向该异常模式专用的栈地址。在 异常处理程序入口处,将用到的其他寄存器的值保存在堆栈中,返回时,重新将这些值加载到寄存器。通过这 种保护程序现场的方法,异常不会破坏被其中断的程序现场。 寄存器 r14 又被称为连接寄存器(Link Register,LR),在 ARM 体系结构中具有下面两种特殊的作用。 (1)每一种处理器模式用自己的 r14 存放当前子程序的返回地址。当通过 BL 或 BLX 指令调用子程序时, r14 被设置成该子程序的返回地址。在子程序返回时,把 r14 的值复制到程序计数器 PC。典型的做法是使 用下列两种方法之一。  执行下面任何一条指令。 MOV PC, LR BX LR  在子程序入口处使用下面的指令将 PC 保存到堆栈中。 STMFD SP!, {,LR} 在子程序返回时,使用如下相应的配套指令返回。 LDMFD SP!, {,PC} (2)当异常中断发生时,该异常模式特定的物理寄存器 r14 被设置成该异常模式的返回地址,对于有些模 式 r14 的值可能与返回地址有一个常数的偏移量(如数据异常使用 SUB PC, LR,#8 返回)。具体的返回方 式与上面的子程序返回方式基本相同,但使用的指令稍微有些不同,以保证当异常出现时正在执行的程序 的状态被完整保存。 R14 也可以被用作通用寄存器使用。 专业始于专注 卓识源于远见       ‐  7  ‐            注意 当嵌套中断被允许时(即异常可重入),r13 和 r14 的使用要特别小心。例如,在用户模式下一 个 IRQ 中断发生,这时两种模式分别使用不同的 r13 和 r14,换句话讲,用户模式使用 r13_usr 和 r14_usr,而 IRQ 模式使用 r13_irq 和 r14_irq,这样不会造成寄存器使用冲突。但是,当程序 运行在 IRQ 模式下,又有 IRQ 中断进入,此时,第二级中断使用 r13_irq 和 r14_irq,冲掉了第 一级 IRQ 的堆栈指针和返回地址,导致程序异常。 解决办法是在第二级中断发生前,将第一级中断用到的寄存器压栈。 3.3.2 程序计数器 r15 程序计算器 r15 又被记为 PC。它有时可以被和 r0-r14 一样用作通用寄存器,但很多特殊的指令在使用 r15 时有些限制。当违反了这些指令的使用限制时,指令的执行结果是不可预知的。 程序计数器在下面两种情况下用于特殊的目的。  读程序计数器。  写程序计数器。 1.程序计数器读操作  由于 ARM 的流水线机制,指令读出的 r15 的值是指令地址加上 8 个字节。由于 ARM 指令始终是字对齐的, 所以读出的结果位[1∶0]始终是 0(但在 Thumb 状态下,指令为 2 字节对齐,bit[0]=0)。 读 PC 主要用于快速地对临近的指令或数据进行位置无关寻址,包括程序中的位置无关分支。 需要注意的是,当使用指令 STR 或 STM 对 r15 进行保存时,保存的可能是当前指令地址加 8 或当前指令 地址加 12。到底是哪种方式,取决于芯片的具体设计方式。当然,在同一个芯片中,要么采用当前指令地 址加 8,要么采用当前指令地址加 12,不可能有些指令采用当前地址加 8,有些采用当前地址加 12。程序 开发人员应尽量避免使用 STR 或 STM 指令来对 r15 进行操作。当不可避免要使用这种方式时,可以先通 过一小段程序来确定所使用的芯片是使用哪种方式实现的。例如: SUB R1,PC,#4 ;r1 中存放 STR 指令地址 STR PC,[R0] ;将 PC=STR 地址+offset 保存到 r0 中 LDR R0,[R0] SUB R0,R0,R1 ;offset=PC-STR 地址 2.程序计数器写操作  当指令向 r15 写入地址数据时,如果指令成功返回,它将使程序跳转到该地址执行。由于 ARM 指令是字 对齐的,写入 r15 的地址值应满足 bit[1:0]=0b00,具体的规则根据 ARM 版本的不同也有所不同:  对于 ARM 版本 3 以及更低的版本,写入 r15 的地址值 bit[1:0]被忽略,即写入 r15 的地址值将与 0xFFFFFFFC 做与操作。  对于 ARM 版本 4 以及更高的版本,程序必须保证写入 r15 寄存器的地址值的 bit[1:0]为 0b00,否则将 会产生不可预知的结果。 对于 Thumb 指令集来说,指令是半字对齐的。处理器将忽略 bit[0],即写入 r15 寄存器的值在写入前要先 和 0XFFFFFFFE 做与操作。 有些指令对 r15 的操作有特殊的要求。比如,指令 BX 利用 bit[0]来确定需要跳转到的子程序是 ARM 状态 还是 Thumb 状态。 注意 这种读取 PC 值和写入 PC 值的不对称操作需要特别注意。 专业始于专注 卓识源于远见       ‐  8  ‐            3.3.3 程序状态寄存器 当前程序状态寄存器 CPSR(Current Program Status Register)可以在任何处理器模式下被访问,它包含下 列内容。  ALU(Arithmetic Logic Unit)状态标志的备份。  当前的处理器模式。  中断使能标志。  设置处理器的状态(只在 4T 架构)。 每一种处理器模式下都有一个专用的物理寄存器作备份程序状态寄存器 SPSR(Saved Program Status Register)。当特定的异常中断发生时,这个物理寄存器负责存放当前程序状态寄存器的内容。当异常处理 程序返回时,再将其内容恢复到当前程序状态寄存器。 注意 由于用户模式和系统模式不属于异常中断模式,所以它们没有 SPSR。当在用户模式或系统模 式中访问 SPSR,将会产生不可预知的结果。 CPSR 寄存器(和保存它的 SPSR 寄存器)中的位分配如图 3.4 所示。 0 M0 M1 M2 M3 M4 T F I V C Z N 1 2 3 4 5 6 7 28 29 30 31 模式位 状态位 FIQ使能位 IRQ使能位 溢出标志 进位标志 零标志 负数标志 图 3.4 程序状态寄存器格式 1.标志位  N(Negative)、Z(Zero)、C(Carry)和 V(oVerflow)通称为条件标志位。这些条件标志位会根据程序 中的算术或逻辑指令的执行结果进行修改,而且这些条件标志位可由大多数指令检测以决定指令是否执 行。 在 ARM 4T 架构中,所有的 ARM 指令都可以条件执行,而 Thumb 指令却不能。 各条件标志位的具体含义如下。  N 本位设置成当前指令运行结果的 bit[31]的值。当两个由补码表示的有符号整数运算时,N=1 表示运算的结 果为负数;N=0 表示结果为正数或零。  Z Z=1 表示运算的结果为零,Z=0 表示运算的结果不为零。 注意 对于 CMP 指令,Z=1 表示进行比较的两个数相等。  C 下面分 4 种情况讨论 C 的设置方法。 ① 在加法指令中(包括比较指令 CMN),当结果产生了进位,则 C=1,表示无符号数运算发生上溢出; 其他情况下 C=0。 专业始于专注 卓识源于远见       ‐  9  ‐            ② 在减法指令中(包括比较指令 CMP),当运算中发生错位(即无符号数运算发生下溢出),则 C=0,;其 他情况下 C=1。 ③ 对于在操作数中包含移位操作的运算指令(非加/减法指令),C 被设置成被移位寄存器最后移出去的位。 ④ 对于其他非加/减法运算指令,C 的值通常不受影响。  V 下面分两种情况讨论 V 的设置方法。 ① 对于加/减运算指令,当操作数和运算结果都是以二进制的补码表示的带符号的数时,V=1 表示符号位 溢出。 ② 对于非加/减法指令,通常不改变标志位 V 的值(具体可参照 ARM 指令手册)。 尽管以上 C 和 V 的定义看起来颇为复杂,但使用时在大多数情况下用一个简单的条件测试指令即可,不需 要程序员计算出条件码的精确值即可得到需要的结果。 注意 下面两种情况会对 CPSR 的条件标志位产生影响。 1.比较指令(CMN、CMP、TEQ、TST)。 2.目的寄存器不是 r15 的算术逻辑运算和数据传输指令。这些指令可以通过在指令末尾加标志 “S”来通知处理器指令的执行结果影响标志位。 【例 3.2】 使用 SUBS 指令从寄存器 r1 中减去常量 1,然后把结果写回到 r1,其中 CPSR 的 Z 位将受到影响。 指令执行前: CPSR 中 Z=0 r1=0x00000001 SUBS r1,r1,#1 SUB 指令执行结束后: r1=0x0 CPSR 中 Z=1 目的寄存器是 r15 的带“位设置”的算术和逻辑运算指令,也可以将 SPSR 的值复制到 CPSR 中,这种操 作主要用于从异常中断程序中返回。 用 MSR 指令向 CPSR/SPSR 写进新值。 目的寄存器位 r15 的 MRC 协处理器指令通过这条指令可以将协处理器产生的条件标志位的值传送到 ARM 处理器。 在中断返回时,使用 LDR 指令的变种指令可以将 SPSR 的值复制到 CPSR 中。 2.Q 标志位  在带 DSP 指令扩展的 ARM v5 及更高版本中,bit[27]被指定用于指示增强的 DAP 指令是否发生了溢出, 因此也就被称为 Q 标志位。同样,在 SPSR 中 bit[27]也被称为 Q 标志位,用于在异常中断发生时保存和恢 复 CPSR 中的 Q 标志位。 在 ARM v5 以前的版本及 ARM v5 的非 E 系列处理器中,Q 标志位没有被定义。属于待扩展的位。 3.控制位  CPSR 的低 8 位(I、F、T 及 M[4∶0])统称为控制位。当异常发生时,这些位的值将发生相应的变化。另 外,如果在特权模式下,也可以通过软件编程来修改这些位的值。 ① 中断禁止位 专业始于专注 卓识源于远见       ‐  10  ‐            I=1,IRQ 被禁止。 F=1,FIQ 被禁止。 ② 状态控制位 T 位是处理器的状态控制位。 T=0,处理器处于 ARM 状态(即正在执行 32 位的 ARM 指令)。 T=1,处理器处于 Thumb 状态(即正在执行 16 位的 Thumb 指令)。 当然,T 位只有在 T 系列的 ARM 处理器上才有效,在非 T 系列的 ARM 版本中,T 位将始终为 0。 ③ 模式控制位 M[4∶0]作为位模式控制位,这些位的组合确定了处理器处于哪种状态。表 3.3 列出了其具体含义。 只有表中列出的组合是有效的,其他组合无效。 表 3.3 状态控制位 M[4∶0] M[4∶0] 处理器模式 可以访问的寄存器 0b10000 User PC,r14~r0,CPSR 0b10001 FIQ PC,r14_fiq~r8_fiq,r7~r0,CPSR,SPSR_fiq 0b10010 IRQ PC,r14_irq~r13_irq,r12~r0,CPSR,SPSR_irq 0b10011 Supervisor PC,r14_svc~r13_svc,r12~r0,CPSR,SPSR_svc 0b10111 Abort PC,r14_abt~r13_abt,r12~r0,CPSR,SPSR_abt 0b11011 Undefined PC,r14_und~r13_und,r12~r0,CPSR,SPSR_und 0b11111 System PC,r14~r0,CPSR(ARM v4 及更高版本) 注意 由于用户模式(User)和系统模式(System)是非异常模式,所以没有单独的 SPSR 保存程 序状态字。在用户模式或系统模式下,读 SPSR 将返回一个不可预知的值,而写 SPSR 将被 忽略。 3.4 异常中断处理 异常或中断是用户程序中最基本的一种执行流程和形态。这部分主要对 ARM 架构下的异常中断做详细说 明。 ARM 有 7 种类型的异常,按优先级从高到低的排列如下:复位异常(Reset)、数据异常(Data Abort)、快 速中断异常(FIQ)、外部中断异常(IRQ)、预取异常(Prefetch Abort)、软件中断(SWI)和未定义指令异常 (Undefined instruction)。 注意 在 ARM 文档中,使用术语 Exception 来描述异常。Exception 主要是从处理器被动接受异常的 角度出发,而 Interrupt 带有向处理器主动申请的色彩。在本书中,对“异常”和“中断”不做 严格区分,两者都是指请求处理器打断正常的程序执行流程,进入特定程序循环的一种机制。 3.4.1 异常种类 ARM 体系结构中,存在 7 种异常处理。当异常发生时,处理器会把 PC 设置为一个特定的存储器地址。这 一地址放在被称为向量表(vector table)的特定地址范围内。向量表的入口是一些跳转指令,跳转到专门 处理某个异常或中断的子程序。 专业始于专注 卓识源于远见       ‐  11  ‐            存储器映射地址 0x00000000 是为向量表(一组 32 位字)保留的。在有些处理器中,向量表可以选择定位 在存储空间的高地址(从偏移量 0xffff0000 开始)。一些嵌入式操作系统,如 Linux 和 Windows CE 就要利 用这一特性。 表 3.4 列出了 ARM 的 7 种异常。 表 3.4 ARM 的 7 种异常 异 常 类 型 处理器模式 执行低地址 执行高地址 复位异常(Reset) 特权模式 0x00000000 0xFFFF0000 未定义指令异常(Undefined interrupt) 未定义指令中止模式 0x00000004 0xFFFF0004 软中断异常(Software Abort) 特权模式 0x00000008 0xFFFF0008 预取异常(Prefetch Abort) 数据访问中止模式 0x0000000C 0xFFFF000C 数据异常(Data Abort) 数据访问中止模式 0x00000010 0xFFFF0010 外部中断请求 IRQ 外部中断请求模式 0x00000018 0xFFFF0018 快速中断请求 FIQ 快速中断请求模式 0x0000001C 0xFFFF001C 异常处理向量表如图 3.5 所示。 当异常发生时,分组寄存器 r14 和 SPSR 用于保存处理器状态,操作伪指令如下。 R14_ = return link SPSR_ = CPSR CPSR[4∶0] = exception mode number CPSR[5] = 0 /*进入 ARM 状态*/ If = = reset or FIQ then CPSR[6] = 1 /*屏蔽快速中断 FIQ*/ CPSR[7] = 1 /*屏蔽外部中断 IRQ*/ PC = exception vector address 图 3.5 异常处理向量表 异常返回时,SPSR 内容恢复到 CPSR,连接寄存器 r14 的内容恢复到程序计数器 PC。 专业始于专注 卓识源于远见       ‐  12  ‐            1.复位异常  当处理器的复位引脚有效时,系统产生复位异常中断,程序跳转到复位异常中断处理程序处执行。复位异 常中断通常用在下面两种情况下。  系统上电。  系统复位。 当复位异常时,系统执行下列伪操作。 R14_svc = UNPREDICTABLE value SPSR_svc = UNPREDICTABLE value CPSR[4∶0] = 0b10011 /*进入特权模式*/ CPSR[5] = 0 /*处理器进入 ARM 状态*/ CPSR[6] = 1 /*禁止快速中断*/ CPSR[7] = 1 /*禁止外设中断*/ If high vectors configured then PC = 0xffff0000 Else PC = 0x00000000 复位异常中断处理程序将进行一些初始化工作,内容与具体系统相关。下面是复位异常中断处理程序的主 要功能。  设置异常中断向量表。  初始化数据栈和寄存器。  初始化存储系统,如系统中的 MMU 等。  初始化关键的 I/O 设备。  使能中断。  处理器切换到合适的模式。  初始化 C 变量,跳转到应用程序执行。 2.未定义指令异常  当 ARM 处理器执行协处理器指令时,它必须等待一个外部协处理器应答后,才能真正执行这条指令。若 协处理器没有相应,则发生未定义指令异常。 未定义指令异常可用于在没有物理协处理器的系统上,对协处理器进行软件仿真,或通过软件仿真实现指 令集扩展。例如,在一个不包含浮点运算的系统中,CPU 遇到浮点运算指令时,将发生未定义指令异常中 断,在该未定义指令异常中断的处理程序中可以通过其他指令序列仿真浮点运算指令。 仿真功能可以通过下面步骤实现。 ① 将仿真程序入口地址链接到向量表中未定义指令异常中断入口处(0x00000004 或 0xffff0004),并保存 原来的中断处理程序。 ② 读取该未定义指令的 bits[27∶24],判断其是否是一条协处理器指令。如果 bits[27∶24]值为 0b1110 或 0b110x,该指令是一条协处理器指令;否则,由软件仿真实现协处理器功能,可以同过 bits[11∶8]来判断 要仿真的协处理器功能(类似于 SWI 异常实现机制)。 ③ 如果不仿真该未定义指令,程序跳转到原来的未定义指令异常中断的中断处理程序执行。 当未定义异常发生时,系统执行下列的伪操作。 r14_und = address of next instruction after the undefined instruction SPSR_und = CPSR CPSR[4∶0] = 0b11011 /*进入未定义指令模式*/ CPSR[5] = 0 /*处理器进入 ARM 状态*/ 专业始于专注 卓识源于远见       ‐  13  ‐            /*CPSR[6]保持不变*/ CPSR[7] = 1 /*禁止外设中断*/ If high vectors configured then PC = 0xffff0004 Else PC = 0x00000004 3.软中断 SWI  软中断异常发生时,处理器进入特权模式,执行一些特权模式下的操作系统功能。软中断异常发生时,处 理器执行下列伪操作。 r14_svc = address of next instruction after the SWI instruction SPSR_und = CPSR CPSR[4∶0] = 0b10011 /*进入特权模式*/ CPSR[5] = 0 /*处理器进入 ARM 状态*/ /*CPSR[6]保持不变*/ CPSR[7] = 1 /*禁止外设中断*/ If high vectors configured then PC = 0xffff0008 Else PC = 0x00000008 4.预取指令异常  预取指令异常使由系统存储器报告的。当处理器试图去取一条被标记为预取无效的指令时,发生预取异常。 如果系统中不包含 MMU 时,指令预取异常中断处理程序只是简单地报告错误并退出。若包含 MMU,引 起异常的指令的物理地址被存储到内存中。 预取异常发生时,处理器执行下列伪操作。 r14_svc = address of the aborted instruction + 4 SPSR_und = CPSR CPSR[4∶0] = 0b10111 /*进入特权模式*/ CPSR[5] = 0 /*处理器进入 ARM 状态*/ /*CPSR[6]保持不变*/ CPSR[7] = 1 /*禁止外设中断*/ If high vectors configured then PC = 0xffff000C Else PC = 0x0000000C 5.数据访问中止异常  数据访问中止异常是由存储器发出数据中止信号,它由存储器访问指令 Load/Store 产生。当数据访问指令 的目标地址不存在或者该地址不允许当前指令访问时,处理器产生数据访问中止异常。 当数据访问中止异常发生时,处理器执行下列伪操作。 r14_abt = address of the aborted instruction + 8 专业始于专注 卓识源于远见       ‐  14  ‐            SPSR_abt = CPSR CPSR[4∶0] = 0b10111 CPSR[5] = 0 /*CPSR[6]保持不变*/ CPSR[7] = 1 /*禁止外设中断*/ If high vectors configured then PC = 0xffff000C10 Else PC = 0x00000010 当数据访问中止异常发生时,寄存器的值将根据以下规则进行修改。 ① 返回地址寄存器 r14 的值只与发生数据异常的指令地址有关,与 PC 值无关。 ② 如果指令中没有指定基址寄存器回写,则基址寄存器的值不变。 ③ 如果指令中指定了基址寄存器回写,则寄存器的值和具体芯片的 Abort Models 有关,由芯片的生产商 指定。 ④ 如果指令只加载一个通用寄存器的值,则通用寄存器的值不变。 ⑤ 如果是批量加载指令,则寄存器中的值是不可预知的值。 ⑥ 如果指令加载协处理器寄存器的值,则被加载寄存器的值不可预知。 6.外部中断 IRQ  当处理器的外部中断请求引脚有效,而且 CPSR 寄存器的 I 控制位被清除时,处理器产生外部中断 IRQ 异 常。系统中各外部设备通常通过该异常中断请求处理器服务。 当外部中断 IRQ 发生时,处理器执行下列伪操作。 r14_irq = address of next instruction to be executed + 4 SPSR_irq = CPSR CPSR[4∶0] = 0b10010 /*进入特权模式*/ CPSR[5] = 0 /*处理器进入 ARM 状态*/ /*CPSR[6]保持不变*/ CPSR[7] = 1 /*禁止外设中断*/ If high vectors configured then PC = 0xffff0018 Else PC = 0x00000018 7.快速中断 FIQ  当处理器的快速中断请求引脚有效且 CPSR 寄存器的 F 控制位被清除时,处理器产生快速中断请求 FIQ 异 常。 当快速中断异常发生时,处理器执行下列伪操作。 r14_fiq = address of next instruction to be executed + 4 SPSR_fiq = CPSR CPSR[4∶0] = 0b10001 /*进入 FIQ 模式*/ CPSR[5] = 0 CPSR[6] = 1 CPSR[7] = 1 If high vectors configured then 专业始于专注 卓识源于远见       ‐  15  ‐            PC= 0xffff001c Else PC = 0x0000001c 3.4.2 异常优先级 每一种异常按表 3.5 中设置的优先级得到处理。 表 3.5 异常优先级 优 先 级 异 常 最高 1 复位异常 2 数据中止 3 快速中断请求 4 中断请求 5 预取指令异常 6 软件中断 最低 7 未定义指令 异常可以同时发生,处理器按表 3.5 的优先级顺序处理异常。例如,复位异常的优先级最高,处理器上电 时发生复位异常。所以当产生复位时,它将优先于其他异常得到处理。同样,当一个数据访问中止异常发 生时,它将优先于除复位异常外的其他所有异常。 优先级最低的 2 种异常是软件中断和未定义指令异常。因为正在执行的指令不可能既是一条 SWI 指令,又 是一条未定义指令,所以软件中断异常 SWI 和未定义指令异享有相同的优先级。 3.4.3 处理器模式和异常 每一种异常都会导致内核进入一种特定的模式。表 3.6 显示了 ARM 处理器异常及其对应的模式。此外, 也可以通过编程改变 CPSR,进入任何一种 ARM 处理器模式。 注意 用户和系统模式是仅有的不可通过异常进入的两种模式,也就是说,要进入这两种模式,必须 通过编程改变 CPSR。 表 3.6 ARM 处理器异常及其对应模式 异 常 模 式 用 途 快速中断请求 FIQ 进行快速中断请求处理 外部中断请求 IRQ 进行外部中断请求处理 SWI SVC 进行操作系统的高级处理 复位 SVC 进行操作系统的高级处理 预取指令中止异常 ABORT 虚存和存储器保护 数据中止异常 ABORT 虚存和存储器保护 未定义指令 Undefined 软件模拟硬件协处理器 3.4.4 异常响应流程 专业始于专注 卓识源于远见       ‐  16  ‐            1.判断处理器状态  当异常发生时,处理器自动切换到 ARM 状态,所以在异常处理函数中要判断在异常发生前处理器是 ARM 状态还是 Thumb 状态。这可以通过检测 SPSR 的 T 位来判断。 通常情况下,只有在 SWI 处理函数中才需要知道异常发生前处理器的状态。所以在 Thumb 状态下,调用 SWI 软中断异常必须注意以下两点。 ① 发生异常的指令地址为(lr-2)而不是(lr-4)。 ② Thumb 状态下的指令是 16 位的,在判断中断向量号时使用半字加载指令 LDRH。 下面的例子显示了一个标准的 SWI 处理函数,在函数中通过 SPSR 的 T 位判断异常发生前的处理器状态。 T_bit EQU 0x20 ; bit 5. SPSR 中的 ARM/Thumb 状态位, : : SWIHandler STMFD sp!, {r0-r3,r12,lr} ; 寄存器压栈,保护程序现场 MRS r0, spsr ; 读 SPSR 寄存器,判断异常发生前的处理器状态 TST r0, #T_bit ; 检测 SPSR 的 T 位,判断异常发生前是否为 Thumb 状态 LDRNEH r0,[lr,#-2] ; 如果是 Thumb 状态,使用半字加载指令读取发生异常的指令地址 BICNE r0,r0,#0xFF00 ; .提取中断向量号. LDREQ r0,[lr,#-4] ; 如果是 ARM 状态,使用字加载指令,读取发生异常的指令地址 BICEQ r0,r0,#0xFF000000 ; 提取中断向量号并将中断向量号存入 r0 ; r0 存储中断向量号 CMP r0, #MaxSWI ; 判断中断是否超出范围 LDRLS pc, [pc, r0, LSL#2] ; 如果未超出范围,跳转到软中断向量表 Switable B SWIOutOfRange ; 如果超出范围,跳转到软中断越界处理程序 switable DCD do_swi_1 DCD do_swi_2 : : do_swi_1 ; 1 号软中断处理函数 LDMFD sp!, {r0-r3,r12,pc}^ ; Restore the registers and return. ; 恢复寄存器并返回 do_swi_2 ; 2 号软中断处理函数 : 2.向量表  如前面介绍向量表时提到的,每一个异常发生时总是从异常向量表开始跳转。最简单的一种情况是向量表 里面的每一条指令直接跳向对应的异常处理函数。其中快速中断处理函数 FIQ_handler()可以直接从地址 0x1C 处开始,省下一条跳转指令,如图 3.6 所示。 专业始于专注 卓识源于远见       ‐  17  ‐            图 3.6 异常处理向量表 但跳转指令 B 的跳转范围为±32MB,但很多情况下不能保证所有的异常处理函数都定位在向量的 32MB 范围内,需要更大范围的跳转,而且由于向量表空间的限制,只能由一条指令完成。具体实现方法有下面 两种。 (1)MOV PC,#imme_value 这种办法将目标地址直接赋值给 PC。但这种方法受格式限制不能处理任意立即数。这个立即数由一个 8 位数值循环右移偶数位得到。 (2)LDR PC,[PC+offset] 把目标地址先存储在某一个合适的地址空间,然后把这个存储器单元的 32 位数据传送给 PC 来实现跳转。 这种方法对目标地址值没有要求。但是存储目标地址的存储器单元必须在当前指令的±4KB 空间范围内。 注意 在计算指令中引用 offset 数值的时候,要考虑处理器流水线中指令预取对 PC 值的影响。 3.4.5 从异常处理程序中返回 当一个异常处理返回时,一共有 3 件事情需要处理:通用寄存器的恢复、状态寄存器的恢复以及 PC 指针 的恢复。通用寄存器的恢复采用一般的堆栈操作指令即可,下面重点介绍状态寄存器的恢复以及 PC 指针 的恢复。 1.恢复被中断程序的处理器状态  PC 和 CPSR 的恢复可以通过一条指令来实现,下面是 3 个例子。  MOVS PC,LR  SUBS PC,LR,#4  LDMFD SP!,{PC}^ 这几条指令是普通的数据处理指令,特殊之处在于它们把程序计数器寄存器 PC 作为目标寄存器,并且带 了特殊的后缀“S”或“^”。其中“S”或“^”的作用就是使指令在执行时,同时完成从 SPSR 到 CPSR 的 拷贝,达到恢复状态寄存器的目的。 专业始于专注 卓识源于远见       ‐  18  ‐            2.异常的返回地址  异常返回时,另一个非常重要的问题就是返回地址的确定。前面提到过,处理器进入异常时会有一个保存 LR 的动作,但是该保持值并不一定是正确中断的返回地址。以一个简单的指令执行流水状态图来对此加 以说明,如图 3.7 所示。 F D E F D E F D E F D E 0x8000 A 0x8004 B 0x8008 C 0x800C D 图 3.7 3 级流水线示例 在 ARM 架构里,PC 值指向当前执行指令地址加 8。也就是说,当执行指令 A(地址 0x8000)时,PC 等 于 0x8000+8=0x8008,即等于指令 C 的地址。假设指令 A 是 BL 指令,则当执行时,会把 PC 值(0x8008) 保存到 LR 寄存器。但是,接下来处理器会对 LR 进行一次自动调整,使 LR=LR-0x4。所以,最终保存 在 LR 里面的是图 3.5 中所示的 B 指令地址。所以当从 BL 返回时,LR 里面正好是正确的返回地址。 同样的跳转机制在所有的 LR 自动保存操作中都存在。当进入中断响应时,处理器对保存的 LR 也进行一 次自动调整,并且跳转动作也是 LR=LR-0x04。由此,就可以对不同异常类型的返回地址依次比较。 假设在指令 B 处(地址 0x8004)发生了异常,进入异常相应后,LR 经过跳转保存的地址值应该是 C 的地 址 0x8008。 (1)软中断异常 如果发生软中断异常,即指令 B 为 SWI 指令,从 SWI 中断返回后下一条执行指令就是 C,正好是 LR 寄 存器保存的地址,所以只有直接把 LR 恢复给 PC 即可。 (2)IRQ 或 FIQ 异常 如果发生的是 IRQ 或 FIQ 异常,因为外部中断请求中断了正在执行的指令 B,当中断返回后,需要重新回 到 B 指令执行,也就是说,返回地址应该是 B(0x8004),需要把 LR 减 4 送 PC。 (3)Data Abort 数据中止异常 在指令 B 处进入数据异常的相应,但导致数据异常的原因却应该是上一条指令 A。当中断处理程序恢复数 据异常后,要回到 A 重新执行导致数据异常的指令,因此返回地址应该是 LR 加 8。 为方便起见,表 3.7 总结了各异常和返回地址的关系 表 3.7 异常和返回地址 异 常 地 址 用 途 复位 - 复位没有定义 LR 数据中止 LR-8 指向导致数据中止异常的指令 FIQ LR-4 指向发生异常时正在执行的指令 IRQ LR-4 指向发生异常时正在执行的指令 预取指令中止 LR-4 指向导致预取指令异常的那条指令 SWI LR 执行 SWI 指令的下一条指令 未定义指令 LR 指向未定义指令的下一条指令 3.4.6 在应用程序中安装异常处理程序 专业始于专注 卓识源于远见       ‐  19  ‐            1.使用汇编语言安装异常处理程序  如果系统启动不依赖于 Debug 或 Debug monitor 软件,可以使用汇编语言在系统启动时直接安装异常处理 程序。 下面的例子显示了系统从 0x0 地址启动,直接安装异常处理程序的方法。 Vector_Init_Block LDR PC, Reset_Addr LDR PC, Undefined_Addr LDR PC, SWI_Addr LDR PC, Prefetch_Addr LDR PC, Abort_Addr NOP ;保留向量 LDR PC, IRQ_Addr LDR PC, FIQ_Addr Reset_Addr DCD Start_Boot Undefined_Addr DCD Undefined_Handler SWI_Addr DCD SWI_Handler Prefetch_Addr DCD Prefetch_Handler Abort_Addr DCD Abort_Handler DCD 0 ;保留向量 IRQ_Addr DCD IRQ_Handler FIQ_Addr DCD FIQ_Handler 有些情况下,系统 0x0 地址不一定是 ROM。如果 0x0 地址为 RAM,那么就系统将中断向量表从 ROM 复 制 RAM,下面的例子显示了这样一个过程。 MOV R8, #0 ADR R9, Vector_Init_Block LDMIA R9!,{r0-r7} ;复制中断向量表 (8 words) STMIA R8!,{r0-r7} LDMIA R9!,{r0-r7} ;复制由伪操作 DCD 定义的地址 STMIA R8!,{r0-r7} 注意 可以使用 Scatter 文件定义加载向量表的地址,这样上述代码的拷贝工作由 C 库函数完成。 2.使用 C 语言安装异常处理程序  程序中有时需要在 main()函数中使用 C 语言安装中断向量表。这就要求指令经编译后的解码能安装在内存 的正确位置。 (1)向量表中使用跳转指令的情况 如果在向量表中使用跳转指令,使用下面的步骤完成向量表的安装。 ① 读取异常处理程序的地址。 ② 从异常处理程序地址中减去向量表中的偏移。 ③ 为适应指令流水线,将上一步得到的地址减 8。 ④ 将得到的结果右移 2 位,得到以字为单位的地址偏移量。 ⑤ 将结果的高 8 位清零,得到跳转指令的 24 位偏移量。 专业始于专注 卓识源于远见       ‐  20  ‐            ⑥ 将上一步得到的结果和 0xea000000(无条件跳转指令编码)做逻辑与操作,从而得到要写到向量表中 的跳转指令的正确编码。 下面的例子显示了这样一个标准过程。 unsigned Install_Handler (unsigned routine, unsigned *vector) { unsigned vec, oldvec; vec = ((routine - (unsigned)vector - 0x8)>>2); if ((vec & 0xFF000000)) { /* diagnose the fault */ prinf ("Installation of Handler failed"); exit (1); } vec = 0xEA000000 | vec; oldvec = *vector; *vector = vec; return (oldvec); } (2)在向量表中使用加载 PC 指令 在向量表中使用加载 PC 指令,按照下面的步骤完成。 ① 读取异常处理程序地址。 ② 从异常处理程序地址中减去向量表中的偏移。 ③ 为适应指令流水线,将上一步得到的地址减 8。 ④ 保留结果的后 12 位。 ⑤ 将结果与 0xe59ff000(LDR PC, [PC,#offset])做逻辑或操作,从而得到要写到向量表中的跳转指令的正 确编码。 ⑥ 将异常处理程序的地址放到相应的存储单元。 下面的例子显示了一个标准的 C 语言过程。 unsigned Install_Handler (unsigned location, unsigned *vector) { unsigned vec, oldvec; vec = ((unsigned)location - (unsigned)vector - 0x8) | 0xe59ff000; oldvec = *vector; *vector = vec; return (oldvec); } 3.4.7 FIQ 和 IRQ 中断处理函数的设计 1.中断分支  ARM 内核只有两个外部中断输入信号 nFIQ 和 nIRQ。但对于一个系统来说,中断源可能多达几十个。为 此,在系统集成的时候,一般都会有一个异常控制器来处理异常信号,如图 3.8 所示。 专业始于专注 卓识源于远见       ‐  21  ‐            图 3.8 中断系统 这时候用户程序可能存在多个 IRQ/FIQ 的中断处理函数。为了使从向量表开始的跳转始终能找到正确的处 理函数入口,需要设置一套处理机制和方法。 多数情况下是由软件来处理异常分支的,因为软件可以通过读取中断控制器来获得中断源的信息,如图 3.9 所示。 有些芯片可能支持特殊的硬件分支功能,这需要查看具体的芯片说明。 因为软件的灵活性,可以设计出比图 3.9 更好的流程控制方法,如图 3.10 所示。 Int_vector_table 是用户自己开辟的一块存储器空间,里面按次序存放异常处理函数的地址。IRQ_Handler() 从中断控制器获取中断源信息,然后再从 Int_vector_table 中的对应地址单元得到异常处理函数的入口地址, 完成一次异常响应的跳转。这种方法的好处是用户程序在运行过程中,能够很方便地动态改变异常服务内 容。 IRQ_Handler: Switch(int_source) { Case1: Case2: …… } IRQ Int1_handler () Int2_handler () 图 3.9 软件控制中断分支 IRQ_Handler: Switch(int_source) { Case1: Case2: …… CaseN: } IRQ Address of Intn_Handler () Address of Int2_Handler () Address of Int1_Handler () Int1_handler () Int2_handler () { Int_vector_table 专业始于专注 卓识源于远见       ‐  22  ‐            图 3.10 灵活的软件分支设计 进入异常处理程序后,用户可以完全按照自己的意愿来进行程序设计,包括调用 Thumb 状态的函数等。但 对于绝大多数的系统来说,有两个步骤必须处理,一是现场保护,二是要把中断控制器中对应的中断状态 标识清除,表明该中断请求已经得到响应,否则,中断函数退出以后,又会被再一次触发,从而进入周而 复始的死循环。 2.ARM 编译器对中断处理函数编写的扩展  考虑到中断处理函数在现场保护和返回地址的处理上与普通函数的不同之处,不能直接把普通函数体连接 到异常向量表上,需要在上面加上一层封装,下面是一个例子。 IRQ_Handler ;中断相应函数 STMFD SP!,{r0-r12,lr} ;保护现场,一般只需要保护{r0-r3,LR} BL IrqHandler ;进入普通处理函数,C 或汇编均可 …… LDMFD sp!,{r0-r12,LR} ;恢复现场 SUBS pc,lr,#4 ;中断返回,注意返回地址 为了方便使用高级语言直接编写异常处理函数,ARM 编译器对此做了特定的扩展,可以使用函数声明关 键字_irq,这样编译出来的函数就可以满足异常响应对现场保护和恢复的需要,并且自动加入 LR 减 4 的处 理,符合 IQR 和 FIQ 中断处理的要求。 下面的例子显示了使用_irq 对中断处理函数产生的影响。 C 语言源程序如下。 __irq void IRQHandler (void) { volatile unsigned int *base = (unsigned int *) 0x80000000; if (*base == 1) { /*调用 C 语言中断处理函数*/ C_int_handler(); } /*清楚中断标志*/ *(base+1) = 0; } 使用 armcc 编译出的汇编代码如下。 IRQHandler PROC STMFD sp!,{r0-r4,r12,lr} MOV r4,#0x80000000 LDR r0,[r4,#0] SUB sp,sp,#4 CMP r0,#1 BLEQ C_int_handler MOV r0,#0 STR r0,[r4,#4] ADD sp,sp,#4 LDMFD sp!,{r0-r4,r12,lr} SUBS pc,lr,#4 ENDP 专业始于专注 卓识源于远见       ‐  23  ‐            如果不使用_irq 子程序声明关键字,编译出的汇编代码如下。 IRQHandler PROC STMFD sp!,{r4,lr} MOV r4,#0x80000000 LDR r0,[r4,#0] CMP r0,#1 BLEQ C_int_handler MOV r0,#0 STR r0,[r4,#4] LDMFD sp!,{r4,pc} ENDP 3.可重入中断设计  在缺省情况下,ARM 中断是不可重入的。因为一旦进入异常响应状态,ARM 自动关闭中断使能。如果在 异常处理过程中,简单地打开中断使能而发生中断嵌套时,显然新的异常处理将破坏原来的中断现场而导 致出错。但有时需要中断必须是可重入的,因此要通过程序设计来解决这个问题。其中有两个关键问题。 ① 新中断使能之前,必须要保护好前一个中断的现场信息。比如 LR_irq 和 SPSR_irq 等,这一点比较容易 做的。 ② 中断处理过程中对 BL 进行保护。 在中断处理函数中发生函数调用 BL 是很常见的,假设有下面一种情况。 IRQ_Handler: …… BL Foo ADD 其中, Foo: STMFD SP!,{r0-r3,LR} …… LDMFD SP!{r0-r3,PC} 上述程序,在 IRQ 处理函数 IRQ_Handler()中调用了函数 Foo()。若是在 IRQ_Handler()里面中断可重入的 话,可能发生问题,考察下面的情况:当新的中断请求恰好在“BL Foo”指令执行完成后发生。这时候 LR_irq 寄存器(因在 IRQ 模式下,所以是 LR_irq)的值将调整为 BL 指令的下一条指令(ADD)地址, 使其能从 Foo()正确返回;但是因为这时候发生了中断请求,接下来要进行新中断的响应,处理器在新中 断响应过程中也要进行 LR_irq 保存。这次对 LR_irq 的操作发生了冲突,当新中断返回后,往下执行 STMFD 指令,这时候压栈的 LR 已不是原来的 ADD 指令地址,从而使子程序 Foo()无法正确返回。 这个问题无法通过增加额外的现场保护指令来解决。一个办法就是在重新使能中断之前改变处理器模式, 也就是使上面程序的“BL Foo”指令不要运行在 IRQ 模式下。这样当新的中断发生时,就不会造成 LR 寄存器的冲突。考虑 ARM 的所有运行模式,采用 SYSTEM 模式是比较合适的,因为它是特权模式,不是 IRQ 模式,与中断响应无关。 下面的例子显示了标准的 IRQ/FIQ 异常中断处理程序。 PRESERVE8 AREA INTERRUPT, CODE, READONLY IMPORT C_irq_handler IRQ 专业始于专注 卓识源于远见       ‐  24  ‐            SUB lr, lr, #4 ;跳转返回地址 STMFD sp!, {lr} ;保存返回地址 MRS r14, SPSR ;读取 SPSR STMFD sp!, {r12, r14} ;保存寄存器 ; 清除中断源 MSR CPSR_c, #0x1F ;切换到 SYSTEM 模式, STMFD sp!, {r0-r3, lr} ;保存 lr_USR 和其他使用到的寄存器 BL C_irq_handler ;跳转到 C 中断处理函数 LDMFD sp!, {r0-r3, lr} ;恢复用户模式寄存器 MSR CPSR_c, #0x92 ;切换回 irq 模式 LDMFD sp!, {r12, r14} MSR SPSR_cf, r14 LDMFD sp!, {pc}^ END 3.4.8 SWI 异常处理函数的设计 本小节主要介绍编写 SWI 处理程序时需要注意的几个问题,包括下面内容。  判断 SWI 中断号。  使用汇编语言编写 SWI 异常处理函数。  使用 C 语言编写 SWI 异常处理函数。  在特权模式下使用 SWI 异常中断处理。  从应用程序中调用 SWI。  从应用程序中动态调用 SWI。 1.判断 SWI 中断号  当发生 SWI 异常,进入异常处理程序时,异常处理程序必须提取 SWI 中断号,从而得到用户请求的特定 SWI 功能。 在 SWI 指令的编码格式中,后 24 位称为指令的“comment field”。该域保存的 24 位数,即为 SWI 指令的 中断号,如图 3.11 所示。 图 3.11 SWI 指令编码格式 第一级的 SWI 处理函数通过 LR 寄存器内容得到 SWI 指令地址,并从存储器中得到 SWI 指令编码。通常 这些工作通过汇编语言、内嵌汇编来完成。 下面的例子显示了提取中断向量号的标准过程。 PRESERVE8 AREA TopLevelSwi, CODE, READONLY ;第一级 SWI 处理函数. EXPORT SWI_Handler SWI_Handler STMFD sp!,{r0-r12,lr} ;保存寄存器 LDR r0,[lr,#-4] ;计算 SWI 指令地址. 专业始于专注 卓识源于远见       ‐  25  ‐            BIC r0,r0,#0xff000000 ;提取指令编码的后 24 位 ; ; 提取出的中断号放 r0 寄存器,函数返回 ; LDMFD sp!, {r0-r12,pc}^ ;恢复寄存器 END 例子中,使用 LR-4 得到 SWI 指令的地址,再通过“BIC r0, r0, #0xFF000000”指令提取 SWI 指令中断号。 2.汇编语言编写 SWI 异常处理函数  最简单的方法是利用得到的中断向量号,使用跳转表直接跳转到实现相应 SWI 功能的处理程序。 下面的例子,使用汇编语言实现了这种跳转。 CMP r0,#MaxSWI ;中断向量范围检测 LDRLS pc, [pc,r0,LSL #2] B SWIOutOfRange SWIJumpTable DCD SWInum0 DCD SWInum1 ; 使用 DCD 定义各功能函数入口地址 SWInum0 ;0 号中断 B EndofSWI SWInum1 ;1 号中断 B EndofSWI ; EndofSWI 3.使用 C 语言编写 SWI 异常处理函数  虽然第一级 SWI 处理函数(完成中断向量号的提取)必须用汇编语言完成,但第二级中断处理函数(根据 提取的中断向量号,跳转到具体处理函数)就可以使用 C 语言来完成。 因为第一级的中断处理函数已经将中断号提取到寄存器 r0 中,所以根据 AAPCS 函数调用规则,可以直接 使用 BL 指令跳转到 C 语言函数,而且中断向量号作为第一个参数被传递到 C 函数。 例如汇编中使用了“BL C_SWI_Handler”跳转到 C 语言的第二级处理函数,则第二级的 C 语言函数示例 如下所示。 void C_SWI_handler (unsigned number) { switch (number) { case 0 : /* SWI number 0 code */ break; case 1 : /* SWI number 1 code */ break; ... default : /* Unknown SWI - report error */ 专业始于专注 卓识源于远见       ‐  26  ‐            } } 另外,如果需要传递的参数多于 1 个,那么可以使用堆栈,将堆栈指针作为函数的参数传递给 C 类型的二 级中断处理程序,就可以实现在两级中断之间传递多个参数。 例如: MOV r1, sp ;将传递的第二个参数(堆栈指针)放到 r1 中 BL C_SWI_Handler ;调用 C 函数 相应的 C 函数的入口变为: void C_SWI_handler(unsigned number, unsigned *reg) 同时,C 函数也可以通过堆栈返回操作的结果。 4.在特权模式下使用 SWI 异常处理  在特权模式下使用 SWI 异常处理,和 IRQ/FIQ 中断嵌套基本类似。当执行 SWI 指令后,处理器执行下面 操作。 ① 处理器进入特权模式。 ② 将程序状态字内容 CPSR 保存到 SPSR_svc。 ③ 返回地址放入 LR_svc。 如果处理器已经处于特权模式,再发生 SWI 异常,则 LR_svc 和 SPSR_svc 寄存器的值将丢失。 所以在特权模式下,调用 SWI 软中断异常,必须先将 LR_svc 和 SPSR_svc 寄存器的值压栈保护。下面的 例子显示了一个可以在特权模式下调用的 SWI 处理函数。 AREA SWI_Area, CODE, READONLY PRESERVE8 EXPORT SWI_Handler IMPORT C_SWI_Handler T_bit EQU 0x20 SWI_Handler STMFD sp!,{r0-r3,r12,lr} ;寄存器压栈保护 MOV r1, sp ;堆栈指针放 r1 作为参数传递. MRS r0, spsr ;读取 spsr. STMFD sp!, {r0, r3} ;将 spsr 压栈保护 ; ; LDR r0,[lr,#-4] ;计算 SWI 指令地址. BIC r0,r0,#0xFF000000 ;读取 SWI 中断向量号. ; r0 存放中断向量号 ; r1 堆栈指针 BL C_SWI_Handler ;调用 C 程序的 SWI 处理函数. LDMFD sp!, {r0, r3} ;从堆栈中读取 spsr. MSR spsr_cf, r0 ;恢复 spcr LDMFD sp!, {r0-r3,r12,pc}^ ;恢复其他寄存器并返回. END 5.从应用程序中调用 SWI  专业始于专注 卓识源于远见       ‐  27  ‐            可从汇编语言或 C/C++ 中调用 SWI。 (1)从汇编应用程序中调用 SWI 从汇编语言程序中调用 SWI,只要遵循 AAPCS 标准即可。调用前,设定所有必须的值并发出相关的 SWI。 例如: MOV r0, #65 ; 将软中断的子功能号放到 r0 中 SWI 0x0 注意 SWI 指令和其他所以 ARM 指令一样,可以被条件执行。 (2)从 C 应用程序中调用 SWI 在 C 或 C++应用程序中调用 SWI,要将 C 语言的子程序用编译器扩展_swi 声明,例如: __swi(0) void my_swi(int); …… …… …… my_swi(65); 编译器扩展_swi 确保了 SWI 以内联方式进行编译,而没有额外的开销。但有如下的 AAPCS 限制。  函数调用参数只能使用 r0~r3 传递。  函数返回值只能通过 r0~r3 传递。 向内联的 SWI 函数传递参数和向实际的子函数传递参数基本类似。但返回值的情况比较复杂。如果有两到 四个返回值,则必须告诉编译程序返回值是以结构形式返回的,并使用__value_in_regs 伪操作声明。这是 因为基于结构值的函数通常被处理为一个 void(空)型函数,且第一个自变量必须为存放结果结构的地址。 下面的例子显示了对编号为 0x0、0x1、0x2 和 0x3 的 SWI 软中断的调用。其中,SWI0x0 和 SWI0x1 传递 两个整型参数并返回一个单一结果;SWI0x2 传递 4 个参数并返回一个单一结果;而 SWI0x3 传递 4 个参 数并通过结构体返回 4 个结果。 #include #include "swi.h" unsigned *swi_vec = (unsigned *)0x08; extern void SWI_Handler(void); int main( void ) { int result1, result2; struct four_results res_3; Install_Handler( (unsigned) SWI_Handler, swi_vec ); printf("result1 = multiply_two(2,4) = %d\n", result1 = multiply_two(2,4)); printf("result2 = multiply_two(3,6) = %d\n", result2 = multiply_two(3,6)); printf("add_two( result1, result2 ) = %d\n", add_two( result1, result2 )); printf("add_multiply_two(2,4,3,6) = %d\n", add_multiply_two(2,4,3,6)); res_3 = many_operations( 12, 4, 3, 1 ); printf("res_3.a = %d\n", res_3.a ); printf("res_3.b = %d\n", res_3.b ); printf("res_3.c = %d\n", res_3.c ); printf("res_3.d = %d\n", res_3.d ); return 0; } __swi(0) int multiply_two(int, int); __swi(1) int add_two(int, int); 专业始于专注 卓识源于远见       ‐  28  ‐            __swi(2) int add_multiply_two(int, int, int, int); struct four_results { int a; int b; int c; int d; }; __swi(3) __value_in_regs struct four_results many_operations(int, int, int, int); (3)应用程序中动态调用 SWI 在某些情形下,需要调用直到运行时才会知道其编号的 SWI。例如,当有很多相关操作可在同一目标上执 行,并且每一个操作都有其自己的 SWI 时,就会发生这种情况。在此情况下,上一小节的方法不适用。 解决的方法有两种。  在运行时得到 SWI 功能号,然后构造出相应的 SWI 指令的编码,将该编码保存在某个存储单元中,将 PC 指针指向该单元,执行指令。  使用一个通用的 SWI 异常中断处理程序,将运行时需要调用的 SWI 功能号作为参数传递给该通用的 SWI 异常处理程序,通用的 SWI 异常中断处理程序根据参数值调用相应的 SWI 处理程序完成需要的操作。 通过汇编语言可以实现第二种解决办法:通过寄存器(通常为 r0 或 r12)传递所需要的操作数,这样可以 重新编写 SWI 处理程序,对相应寄存器中的值进行处理。 但有些情况下,为了节省程序开销,需要直接使用 SWI 中断号对程序调用。例如,操作系统可能会使用单 一的一条 SWI 指令并用寄存器来传递所需运算的编号。这使得其他 SWI 空间可用于特定应用程序的 SWI。 在一个特定的应用程序中,如果从指令中提取 SWI 编号的开销太大,就可使用这个方法。ARM(0x123456) 和 Thumb(0xAB)半主机方式的 SWI 就是这样实现的。 下面的例子显示了如何使用_swi 将 C 函数调用映射到半主机方式的 SWI。 #ifdef __thumb /* Thumb 状态的 Semihosting 软中断处理*/ #define SemiSWI 0xAB #else /* ARM 状态下的 Semihosting 的软中断处理*/ #define SemiSWI 0x123456 #endif /* 使用 Semihosting 软中断输出一个字符*/ __swi(SemiSWI) void Semihosting(unsigned op, char *c); #define WriteC(c) Semihosting (0x3,c) void write_a_character(int ch) { char tempch = ch; WriteC( &tempch ); } 编译程序含有一个机制,用以支持使用 r12 来传递所需运算的值。根据 AAPCS 标准,r12 为 IP 寄存器, 并且专用于函数调用。其他时间内可将其用作暂存寄存器。如前面所述,通用 SWI 参数和返回值通过 r0~ r3 寄存器传递。而 r12 可用于传递通用 SWI 调用的中断功能编号。 下面的例子显示了通用 SWI 的 C 语言程序框架。 __swi_indirect(0x80) unsigned SWI_ManipulateObject(unsigned operationNumber, unsigned object,unsigned parameter); unsigned DoSelectedManipulation(unsigned object, 专业始于专注 卓识源于远见       ‐  29  ‐            unsigned parameter, unsigned operation) { return SWI_ManipulateObject(operation, object, parameter); } 生成的汇编代码如下。 DoSelectedManipulation PROC STMFD sp!,{r3,lr} MOV r12,r2 SWI 0x80 LDMFD sp!,{r3,pc} ENDP 联系方式 集团官网:www.hqyj.com 嵌入式学院:www.embedu.org 移动互联网学院:www.3g-edu.org 企业学院:www.farsight.com.cn 物联网学院:www.topsight.cn 研发中心:dev.hqyj.com 集团总部地址:北京市海淀区西三旗悦秀路北京明园大学校内 华清远见教育集团 北京地址:北京市海淀区西三旗悦秀路北京明园大学校区,电话:010-82600386/5 上海地址:上海市徐汇区漕溪路银海大厦 A 座 8 层,电话:021-54485127 深圳地址:深圳市龙华新区人民北路美丽 AAA 大厦 15 层,电话:0755-22193762 成都地址:成都市武侯区科华北路 99 号科华大厦 6 层,电话:028-85405115 南京地址:南京市白下区汉中路 185 号鸿运大厦 10 层,电话:025-86551900 武汉地址:武汉市工程大学卓刀泉校区科技孵化器大楼 8 层,电话:027-87804688 西安地址:西安市高新区高新一路 12 号创业大厦 D3 楼 5 层,电话:029-68785218 《ARM 系列处理器应用技术完全手册》 作者:华清远见 第 4 章 ARM 指令寻址方式 本章目标 ARM 指令集可以分为跳转指令、数据处理指令、程序状态寄存器传输指令、 Load/Store 指令、协处理器指令和异常中断产生指令。根据适用的指令类型不同, 指令的寻址方式分为:数据处理指令操作数寻址方式和内存访问指令寻址方式。 专业始于专注 卓识源于远见 ‐ 2 ‐ 4.1 数据处理指令的寻址方式 4.1.1 数据处理指令的寻址方式概要 数据处理指令的基本语法格式如下。 {} {S} ,, 其中有下面 11 种形式,如表 4.1 所示。 表 4.1 的寻址方式 语 法 寻 址 方 式 1 # 立即数寻址 2 寄存器寻址 3 , LSL # 立即数逻辑左移 4 , LSL 寄存器逻辑左移 5 , LSR # 立即数逻辑右移 6 , LSR 寄存器逻辑右移 7 , ASR # 立即数算术右移 8 , ASR 寄存器算术右移 9 , ROR # 立即数循环右移 10 , ROR 寄存器循环右移 11 , RRX 寄存器扩展循环右移 数据处理指令的寻址方式根据的不同,相应的分为 11 种。 4.1.2 指令解码 图 4.1 显示了数据处理指令不同寻址方式下的解码格式。 32 位立即数 31 28 27 26 25 24 21 20 19 16 15 12 11 8 7 0 立即数移位 寄存器移位 opcode cond 0 0 1 S Rn Rd Rotate_imm Immed_8 opcode cond 0 0 0 S Rn Rd shift_imm shift 0 Rm opcode cond 0 0 0 S Rn Rd Rs shift 0 Rm 1 31 28 27 26 25 24 21 20 19 16 15 12 11 7 6 0 31 28 27 26 25 24 21 20 19 16 15 12 11 8 7 0 5 4 6 5 4 3 图 4.1 数据操作指令编码格式 编码格式中各域含义如下。  :确定具体指令。 专业始于专注 卓识源于远见 ‐ 3 ‐  S:标识指令是否影响程序状态寄存器 CPSR 条件标志。  Rd:指令操作的目的寄存器。  Rn:指令第一源操作数。  bit[11∶0]:移位操作,详见本章移位操作一节。  bit[25]:被用来区分是立即数移位操作还是寄存器移位操作。 如果指令编码出现下面情况:bit[25] = 0 并且 bit[4] = 1 并且 bit[7] = 1,则指令并非数据处理指令,它可能 是 Load/Store 指令或算术指令。 4.1.3 移位操作 数据处理指令是在算术逻辑单元 ALU 中完成。ARM 处理器一个显著特征就是可以在操作数进入 ALU 之 前,对操作数进行指定位数的左移或右移操作。这种功能明显增强了数据处理操作的灵活性。 移位操作可能产生进位,更新程序状态寄存器 CPSR 的进位标志 C。移位操作有下面 3 种基本方式。 1.立即数方式 没有任何一条 ARM 指令可以包含一个 32 位的立即数,数据处理指令编码格式中,第二个操作数有 12 位。 指令的编码格式如图 4.1 所示。 指令中的立即数是由一个 8 bit 的常数移动 4 bit 偶数位(0,2,4,…,26,28,30)得到的。所以,每 一条指令都包含一个 8 bit 的常数 X 和移位值 Y,得到的立即数=X 循环右移(2×Y)。 注意 8 位立即数一定要移偶数位。 下面列举了一些有效的立即数。 0xFF、0x104、0xFF0、0x FF00、0x FF000、0x FF000000、0x F000000F 下面是一些无效的立即数。 0x101、0x102、0x FF1、0x FF04、0x FF003、0x FFFFFFFF、0x F000001F 下面是一些应用立即数的指令。 MOV r0,#0 ;送 0 到 r0 ADD r3,r3,#1 ;r3 的值加 1 CMP r7,#1000 ;r7 的值和 1000 比较 BIC r9,r8,#0x FF00 ;将 r8 中 8~15 位清零,结果保存在 r9 中 2.寄存器方式 寄存器的值可以被直接用于数据操作指令,如: MOV r2,r0 ;r0 的值送 r2 ADD r4,r3,r2 ;r2 加 r3,结果送 r4 CMP r7,r8 ;比较 r7 和 r8 的值 3.寄存器移位方式 寄存器的值在被送到 ALU 之前,可以事先经过桶形移位寄存器的处理。预处理和移位发生在同一周期内, 所以有效的使用移位寄存器,可以增加代码的执行效率。 具体的移位(或者循环移位)方式有下面几种。 专业始于专注 卓识源于远见 ‐ 4 ‐  ASR:算术右移。  LSL:逻辑左移。  LSR:逻辑右移。  ROR:循环右移。  RRX:扩展的循环右移。 以上 5 种移位方式,移位值均可以由立即数或寄存器指定。下面是一些在指令中使用了移位操作的例子。 ADD r2,r0,r1,LSR #5 MOV r1,r0,LSL #2 RSB r9,r5,r5,LSL #1 SUB r1,r2,r0,LSR #4 MOV r2,r4,ROR r0 4.1.4 寻址方式分类详解 数据处理指令的寻址方式根据的不同,相应的分为 11 种。详见表 4.1。下面对各类寻址方 式进行详细说明。 1.# (1)编码格式 指令的编码格式如图 4.2 所示。 31 28 27 26 25 24 21 20 19 16 15 12 11 8 7 0 opcode cond 0 0 1 S Rn Rd Rotate_imm Immed_8 图 4.2 数据处理指令——立即数寻址编码格式 立即数寻址为数据处理指令提供了一个可直接操作的立即数。立即数的生成方法见前面章节介绍。如果移 位值为 0,则移位进位值为程序状态寄存器 CPSR 的 C 标志位;否则,为 32-bit 立即数的 bit[31]。 (2)操作伪代码 Shifter_operand = immed_8 Rotate_Right (rotate_imm*2) if rotate_imm == 0 then shifter_carry_out = C flag else /* rotate_imm != 0*/ shifter_carry_out = shifter_operand[31] (3)说明 ① 并不是所有的 32-bit 立即数都是可以使用的合法立即数。只有那些通过将一个 8-bit 的立即数循环右移 偶数位可以得到的立即数才可以在指令中使用。 ② 有些立即数可以通过不止一种方法得到。由于立即数的构造方法中移位包含了循环操作,而循环移位 操作会影响 CPSR 的条件标志位 C。因此,同一个合法的立即数由于采用了不同的编码方式,将使这些指 令的执行产生不同的结果,这是不能允许的。ARM 汇编器按照下面的规则来生成立即数的编码。  当立即数数值在 0 和 0xFF 范围时,令 immed_8=,immed_4=0。  其他情况下,汇编编译器选择使用 immed_4 数值最小的编码方式。 ③ 为了更精确地控制立即数的生成,可以使用下面的语法格式控制立即数的生成。 #, 专业始于专注 卓识源于远见 ‐ 5 ‐ 其中, = 2*rotate_imm (4)举例 SUBS r0,r0,#1 ;寄存器 r0 中的数值减 1,结果保存到 r0 MOV r0,#0xff00 ; 0xff00 → r0 ;将立即数 0xff00 放入 r0 保存 2. (1)编码格式 指令的编码格式如图 4.3 所示。 opcode cond 0 0 0 S Rn Rd 00000 000 Rm 31 28 27 26 25 24 21 20 19 16 15 12 11 7 6 0 4 3 图 4.3 数据处理指令——寄存器寻址编码格式 指令的操作数即为寄存器中的数值。移位寄存器的进位为程序状态寄存器 CPSR 的 C 标志位。 指令的语法格式为: {} {S} ,, (2)操作伪代码 Shifter_operand = Rm Shifter_carry_out = C Flag (3)说明 ① 从指令的解码格式来看,寄存器寻址方式和使用立即数逻辑左移寻址解码格式是相同的,只是其移位 数为 0。 ② 如果指令中的 Rm 或 Rn 指定为程序计数器 r15,则操作数的值为当前指令地址加 8。 (4)举例 MOV r1,r2 ; r2 → r1 SUB r0,r1,r2 ; r1 – r2 → r0 3., LSL # (1)编码格式 指令的编码格式如图 4.4 所示。 opcode cond 0 0 0 S Rn Rd shift_imm 000 Rm 31 28 27 26 25 24 21 20 19 16 15 12 11 7 6 0 4 3 图 4.4 数据处理指令——立即数逻辑左移寻址编码格式 指令的操作数为寄存器 Rm 的数值逻辑左移 shift_imm 位。左移的范围在 0 到 31 之间。左移移出的位用 0 补齐。进位标志位是最后移出的位(如果移位数为 0,则为 C 标志位)。 指令的语法格式为: {} {S} ,,,LSL #,其中:  为进行逻辑左移操作的寄存器;  LSL 为逻辑左移操作标识;  为逻辑左移位数,范围为 0~31。 (2)操作伪代码 专业始于专注 卓识源于远见 ‐ 6 ‐ if shift_imm == 0 then /*执行寄存器操作*/ shifter_operand = Rm shifter_carry_out = C flag else /*移位寄存器大于零*/ shifter_operand = Rm logical_shift_left shift_imm shifter_carry_out = Rm[32 – shift_imm] (3)说明 ① 如果移位立即数 =0,则该寻址方式为立即数直接寻址。 ② 如果指令中的 Rm 或 Rn 指定为程序计数器 r15,则操作数的值为当前指令地址加 8。 (4)举例 SUB r0,r1,r2,LSL #10 ;r1 的值减去 r2 的值左移 10bit,结果放到 r0 寄存器 MOV r0,r2,LSL #3 ;r2 的值左移 3bit,结果放入 r0,即 r0 = r2×8 4., LSL (1)编码格式 指令的编码格式如图 4.5 所示。 opcode cond 0 0 0 S Rn Rd Rs 0001 Rm 31 28 27 26 25 24 21 20 19 16 15 12 11 8 7 0 4 3 图 4.5 数据处理指令——寄存器逻辑左移寻址编码格式 寄存器逻辑左移十分适合寄存器值乘 2 的倍数操作。 这个指令是将寄存器 Rm 的值逻辑左移一定的位数。位移的位数由 Rs 的最低 8 位 bit[7∶0]决定。Rm 移出 的位用 0 补齐。进位值是移位寄存器最后移出的位,如果移位数大于 0,则进位值为 0。 (2)语法格式 {} {S} ,,,LSL 其中:  为指令被移位的寄存器;  LSL 为逻辑左移操作标识;  为包含逻辑左移位数的寄存器。 (3)操作伪代码 if Rs[7:0] = = 0 then shifter_operand = Rm shifter_carry_out = C flag else if Rs[7:0] < 32 then shifter_operand = Rm logical_shift_left Rs[7:0] shifter_carry_out = Rm[32 – Rs[7:0]] else if Rs[7:0] = = 32 then shifter_operand = 0 shifter_carry_out = Rm[0] else /*Rs 的后 8 位大于零*/ shifter_operand = 0 shifter_carry_out = 0 (4)说明 专业始于专注 卓识源于远见 ‐ 7 ‐ 如果程序计数器 r15 被用作 Rd,Rm,Rn 或 Rs 中的任意一个,则指令的执行结果不可预知。 (5)举例 MOV r0,r2,LSL r3 ;r2 的值左移 r3 位,结果放入 r0 ANDS r1,r1,r2,LSL r3 ;r2 的值左移 r3 位,然后和 r1 相与,结果放入 r1 5., LSR # (1)编码格式 指令的编码格式如图 4.6 所示。 opcode cond 0 0 0 S Rn Rd shift_imm 010 Rm 31 28 27 26 25 24 21 20 19 16 15 12 11 7 6 0 4 3 图 4.6 数据处理指令——立即数逻辑右移寻址编码格式 指令的操作数为寄存器 Rm 的值右移位,相当于 Rm 的值除以一个 2 的倍数。 值的范围为 0~31,移位后空出的位添 0。循环器进位值为 Rm 最后移出的位。 (2)语法格式 {} {S} ,,,LSR # 其中:  为被移位的寄存器;  LSR 为逻辑右移操作标识;  为逻辑右移位数,范围为 0~31。 (3)操作伪代码 if shift_imm == 0 then /*执行寄存器操作*/ shifter_operand = 0 shifter_carry_out = Rm[31] else /*移位立即数大于零*/ shifter_operand = Rm logical_shift_Right shift_imm shifter_carry_out = Rm[shift_imm - 1] (4)说明 ① shift_imm 的取值范围为 0~31,当 shift_imm=0 时,移位位数为 32,所以移位位数范围为 1~32 位。 ② 如果指令中的 Rm 或 Rn 指定为程序计数器 r15,则操作数的值为当前指令地址加 8。 6., LSR (1)编码格式 指令的编码格式如图 4.7 所示。 opcode cond 0 0 0 S Rn Rd Rs 0011 Rm 31 28 27 26 25 24 21 20 19 16 15 12 11 8 7 0 4 3 图 4.7 数据处理指令——寄存器逻辑右移寻址编码格式 专业始于专注 卓识源于远见 ‐ 8 ‐ 此操作将寄存器 Rm 的数值逻辑右移一定的位数。移位的位数由 Rs 的最低 8 位 bit[7∶0]决定。移出的位 由 0 补齐。当 Rs[7∶0]大于 0 而小于 32 时,进位标志 C 由最后移出的位决定,当 Rs[7∶0]大于 32 时,进 位标志位为 0,当 Rs[7∶0]等于 0 时,进位标志不变。 (2)语法格式 {} {S} ,,,LSR 其中:  为指令被移位的寄存器;  LSR 为逻辑右移操作标识;  为包含逻辑右移位数的寄存器。 (3)操作伪代码 if Rs[7:0] = = 0 then shifter_operand = Rm shifter_carry_out = C flag else if Rs[7:0] < 32 then shifter_operand = Rm logical_shift_Right Rs[7:0] shifter_carry_out = Rm[Rs[7:0] - 1] else if Rs[7:0] = = 32 then shifter_operand = 0 shifter_carry_out = Rm[31] else /*Rs 的后 8 位大于零*/ shifter_operand = 0 shifter_carry_out = 0 (4)说明 如果程序计数器 r15 被用作 Rd、Rm、Rn 或 Rs 中的任意一个,则指令的执行结果不可预知。 7., ASR # (1)编码格式 指令的编码格式如图 4.8 所示。 opcode cond 0 0 0 S Rn Rd shift_imm 100 Rm 31 28 27 26 25 24 21 20 19 16 15 12 11 7 6 0 4 3 图 4.8 数据处理指令——立即数算术右移寻址编码格式 指令的操作数为寄存器 Rm 的数值逻辑右移位。的值范围为 0~31,当 等于 0 时,移位位数为 32,所以移位位数范围为 1~32 位。进位移位操作后,空出的位添 Rm 的最高位 Rm[31]。进位标志为 Rm 最后被移出的数值。 (2)语法格式 {} {S} ,,,ASR # 其中:  为被移位的寄存器;  ASR 为算术右移操作标识;  为算术右移位数,范围为 1~32,当 shift_imm 等于 0 时移位位数为 32。 (3)操作伪代码 专业始于专注 卓识源于远见 ‐ 9 ‐ if shift_imm == 0 then /*执行寄存器操作*/ if Rm[31] = = 0 then shifter_operand = 0 shifter_carry_out = Rm[31] else /*Rm[31] = = 1*/ shifter_operand = 0xffffffff shifter_carry_out = Rm[31] else /*shift_imm > 0*/ shifter_operand = Rm Arithmetic_shift_Right shifter_carry_out = Rm[shift_imm - 1] (4)说明 ① 如果指令中的 Rm 或 Rn 指定为程序计数器 r15,则操作数的值为当前指令地址加 8。 8., ASR (1)编码格式 指令的编码格式如图 4.9 所示。 opcode cond 0 0 0 S Rn Rd Rs 0101 Rm 31 28 27 26 25 24 21 20 19 16 15 12 11 8 7 0 4 3 图 4.9 数据处理指令——寄存器算术右移寻址编码格式 此操作将寄存器 Rm 的数值算术右移一定的位数。移位后空缺的位由 Rm 的符号位(Rm[31])填充。位移的 位数由 Rs 的最低 8 位 bit[7∶0]决定。当 Rs[7∶0]大于零而小于 32 时,指令的操作数为寄存器 Rm 的数值算 术右移 Rs[7∶0]位,进位标志 C 为 Rm 最后被移出的位。 (2)语法格式 {} {S} ,,,ASR 其中:  为指令被移位的寄存器;  ASR 为算术右移操作标识;  为包含算术右移位数的寄存器。 (3)操作伪代码 if Rs[7:0] = = 0 then shifter_operand = Rm shifter_carry_out = C flag else if Rs[7:0] < 32 then shifter_operand = Rm Arithmeticl_shift_Right Rs[7:0] shifter_carry_out = Rm[Rs[7:0] - 1] else if Rm[31] = =0 then shifter_operand = 0 shifter_carry_out = Rm[31] else shifter_operand = 0xffffffff shifter_carry_out = Rm[31] (4)说明 专业始于专注 卓识源于远见 ‐ 10 ‐ 如果程序计数器 r15 被用作 Rd、Rm、Rn 或 Rs 中的任意一个,则指令的执行结果不可预知。 9., ROR # (1)编码格式 指令的编码格式如图 4.10 所示。 opcode cond 0 0 0 S Rn Rd shift_imm 110 Rm 31 28 27 26 25 24 21 20 19 16 15 12 11 7 6 0 4 3 图 4.10 数据处理指令——立即数循环右移寻址编码格式 指令的操作数由寄存器 Rm 的数值循环右移一定的位数得到。移位的位数由 Rs 的最低 8 位 bits[7∶0]决定。 当 Rs[7∶0]=0 时,指令的操作数为寄存器 Rm 的值,循环器的进位值为 CPSR 中的 C 条件标志位;否则, 循环器的进位值为 Rm 最后被移出的位。 (2)语法格式 {} {S} ,,,ROR # 其中:  为被移位的寄存器;  ROR 为循环右移操作标识;  为循环右移位数,范围为 1~31,当 shift_imm 等于 0 时执行 RRX 操作。 (3)操作伪代码 if shift_imm == 0 then /*执行寄存器操作*/ 执行 RRX 操作 else shifter_operand = Rm Rotate_Right shift_imm shifter_carry_out = Rm[shift_imm - 1] (4)说明 如果指令中的 Rm 或 Rn 指定为程序计数器 r15,则操作数的值为当前指令地址加 8。 10., ROR (1)编码格式 指令的编码格式如图 4.11 所示。 opcode cond 0 0 0 S Rn Rd Rs 0111 Rm 31 28 27 26 25 24 21 20 19 16 15 12 11 8 7 0 4 3 图 4.11 数据处理指令——寄存器循环右移寻址编码格式 指令的操作数由寄存器 Rm 的数值循环右移一定的位数。移位的位数由 Rs 的最低 8 位 bits[7∶0]决定。当 Rs[7∶0]=0 时,指令的操作数为寄存器 Rm 的值,循环器的进位值为 CPSR 中的 C 条件标志位;否则,循 环器的进位值为 Rm 最后被移出的位。 (2)语法格式 {} {S} ,,,ROR 专业始于专注 卓识源于远见 ‐ 11 ‐ 其中:  为指令被移位的寄存器;  ROR 为循环右移操作标识;  为包含循环右移位数的寄存器。 (3)操作伪代码 if Rs[7:0] = = 0 then shifter_operand = Rm shifter_carry_out = C flag else if Rs[4:0] == 0 then shifter_operand = Rm shifter_carry_out = Rm[31] else shifter_operand = Rm Rotate_Right Rs[4:0] shifter_carry_out = Rm[Rs[4:0] - 1] (4)说明 如果程序计数器 r15 被用作 Rd、Rm、Rn 或 Rs 中的任意一个,则指令的执行结果不可预知。 11., RRX (1)编码格式 指令的编码格式如图 4.12 所示。 opcode cond 0 0 0 S Rn Rd 00000110 Rm 31 28 27 26 25 24 21 20 19 16 15 12 11 0 4 3 图 4.12 数据处理指令——扩展右移寻址编码格式 指令的操作数为寄存器 Rm 的数值右移一位,并用 CPSR 中的 C 条件标志位填补空出的位。CPSR 中的 C 条件标志位则用移出的位代替。 (2)语法格式 {} {S} ,,,RRX 其中:  为指令被移位的寄存器;  RRX 为扩展的循环右移操作。 (3)操作伪代码 shifter_operand = (C flag logical_shift_left 31) OR (Rm logical_shift_Right 1) shifter_carry_out = Rm[0] (4)说明 ① 此种寻址方式的编码形式和“ROR #0”一致。 ② 如果程序计数器 r15 被用作 Rd、Rm、Rn 或 Rs 中的任意一个,则指令的执行结果不可预知。 ③ 可以实现 ADC 指令的功能。 4.2 内存访问指令寻址 根据内存访问指令的分类,内存访问指令的寻址方式可以分为以下几种。 专业始于专注 卓识源于远见 ‐ 12 ‐ ① 字及无符号字节的 Load/Store 指令的寻址方式。 ② 杂类 Load/Store 指令的寻址方式。 ③ 批量 Load/Store 指令的寻址方式。 ④ 协处理器 Load/Store 指令的寻址方式。 4.2.1 字及无符号字节的 Load/Store 指令的寻址方式 字及无符号字节的 Load/Store 指令语法格式如下: LDR|STR{}{B}{T} 其中共有 9 种寻址方式,如表 4.2 所示。 表 4.2 字及无符合字节的 Load/Store 指令的寻址方式 格 式 模 式 1 [Rn,#±] 立即数偏移寻址 (Immediate offset) 2 [Rn,±Rm] 寄存器偏移寻址 (Register offset) 3 [Rn,Rm,#< offset_12>] 带移位的寄存器偏移寻址 (Scaled register offset) 4 [Rn,#±< offset_12>]! 立即数前索引寻址 (Immediate pre-indexed) 5 [Rn,±Rm]! 寄存器前索引寻址 (Register post-indexed) 6 [Rn,Rm,#< offset_12>]! 带移位的寄存器前索引寻址 (Scaled register pre-indexed) 7 [Rn],#±< offset_12> 立即数后索引寻址 (Immediate post-indeded) 8 [Rn],± 寄存器后索引寻址 (Register post-indexed) 9 [Rn],±#< offset_12> 带移位的寄存器后索引寻址 (Scaled register post-indexed) 字及无符号字节的 Load/Store 指令的解码格式如图 4.13 所示。 立即数 cond 01 0 P U B W L Rn Rd offset_12 31 28 27 26 25 20 19 16 15 12 11 0 cond 01 1 P U B W L Rn Rd 00000000 31 28 27 26 25 20 19 16 15 12 11 0 cond 01 1 P U B W L Rn Rd shi_imm 31 28 27 26 25 20 19 16 15 12 11 0 Rm shift L Rm 4 3 7 6 4 3 5 寄存数 寄存器偏移 图 4.13 字及无符号字节的 Load/Store 指令的解码格式 编码格式中各位的含义如表 4.3 所示。 专业始于专注 卓识源于远见 ‐ 13 ‐ 表 4.3 字和无符号半字 Load/Store 指令编码格式各位含义 位 标 识 取 值 含 义 P P=0 使用后索引寻址 P=1 使用偏移地址或前索引寻址(由 W 位决定) U U=0 访问的地址=基址寄存器的值-偏移量(offset) U=1 访问的地址=基址寄存器的值+偏移量(offset) B B=0 字访问 Load/Store B=1 无符号字节访问 Load/Store W W=0 如果 P=0,该指令为 LDR、LDRB、STR 或 STRB 指令,且内存访问 指令为正常访问指令;如果 P=1,指令执行不更新基地址 W=1 如果 P=0,该指令为 LDRBT、LDRT、STRBT 或 STRT,且指令为非 特权(用户模式)访问指令;如果 P=1,计算内存地址并更新基地址 L L=0 Store 指令 L=1 Load 指令 1.[Rn,#±] (1)编码格式 指令的编码格式如图 4.14 所示。 cond 01 0 1 U B 0 L Rn Rd offset_12 31 28 27 26 25 20 19 16 15 12 11 0 图 4.14 内存访问指令——立即数偏移寻址编码格式 内存访问地址为基址寄存器 Rn 的值加(或减)立即数 offset_12。 编程中,在访问结构体或记录(record)类型的变量时,这些内存的操作指令是十分有效的。另外,在子 程序中也常用这些指令访问本地变量和堆栈。 (2)语法格式 LDR|STR{}{B}{T} ,[,#±] 其中:  Rn 为基址寄存器,该寄存器包含内存访问的基地址;  为 12 位立即数,内存访问地址偏移量。 (3)操作伪代码 If U = = 1 then Address = Rn + offset_12 Else Address = Rn – offset_12 (4)说明 ① 如果指令中没有指定立即数,使用[],编译器按[,#0]形式编码。 ② 如果 Rn 被指定为程序计数器 r15,其值为当前指令地址加 8。 2.[Rn,±Rm] 专业始于专注 卓识源于远见 ‐ 14 ‐ (1)编码格式 指令的编码格式如图 4.15 所示。 cond 01 1 1 U B 0 L Rn Rd 00000000 31 28 27 26 25 20 19 16 15 12 11 0 Rm 图 4.15 内存访问指令——寄存器偏移寻址编码格式 内存访问地址为基址寄存器 Rn 的值加(或减)偏移寄存器 Rm 的值。 该寻址方式适合使用指针访问字节数组中的数据成员。 (2)语法格式 LDR|STR{}{B}{T} ,[,±] 其中:  Rn 为基址寄存器,该寄存器包含内存访问的基地址;  为偏移地址寄存器,包含内存访问地址偏移量。 (3)操作伪代码 If U = = 1 then Address = Rn + Rm Else Address = Rn – Rm (4)说明 如果 Rn 被指定为程序计数器 r15,其值为当前指令地址加 8;如果 r15 被用作偏移地址寄存器 Rm 的值, 指令的执行结果不可预知。 3.[Rn,Rm,#< offset_12>] (1)编码格式 指令的编码格式如图 4.16 所示。 cond 01 1 1 U B 0 L Rn Rd shift_imm 31 28 27 26 25 20 19 16 15 12 11 0 shift 0 Rm 7 6 4 3 5 图 4.16 内存访问指令——带移位的寄存器偏移寻址编码格式 内存地址为 Rn 的值加/减通过移位操作后的 Rm 的值。 当数组中的成员长度大于 1 个字节时,使用该寻址方式可高效率地访问数组成员。 (2)语法格式 语法格式有以下 5 种。 LDR|STR{}{B}{T} ,[,±,LSL #< offset_12>] LDR|STR{}{B}{T} ,[,±,LSR #< offset_12>] LDR|STR{}{B}{T} ,[,±,ASR #< offset_12>] LDR|STR{}{B}{T} ,[,±,ROR #< offset_12>] LDR|STR{}{B}{T} ,[,±,RRX] 其中:  Rn 为基址寄存器,该寄存器包含内存访问的基地址;  为偏移地址寄存器,包含内存访问地址偏移量;  LSL 表示逻辑左移操作; 专业始于专注 卓识源于远见 ‐ 15 ‐  LSR 表示逻辑右移操作;  ASR 表示算术右移操作;  ROR 表示循环右移操作;  RRX 表示扩展的循环右移。  为移位立即数。 (3)操作伪代码 Case shift of 0b00 /*LSL*/ Index = Rm logic_shift_left shift_imm 0b01 /*LSR*/ If shift_imm = = 0 then /*LSR #32*/ Index = 0 Else Index = Rm logical_shift_right shift_imm 0b10 /*ASR*/ If shift_imm = = 0 then /*ASR #32*/ If Rm[31] = = 1 then Index = 0xffffffff Else Index = 0 Else Index = Rm Arithmetic_shift_Right shift_imm 0b11 /* ROR or RRX*/ If shift_imm = = 0 then /*RRX*/ Index = (C flag Logical_shift_left 31) OR (Rm logical_shift_Right 1) Else /*ROR*/ Index = Rm Rotate_Right shift_imm Endcase If U = = 1 then Address = Rn + index Else /*U = = 0*/ Address = Rn – index (4)说明 如果 Rn 被指定为程序计数器 r15,其值为当前指令地址加 8;如果 r15 被用作偏移地址寄存器 Rm 的值, 指令的执行结果不可预知。 4.[Rn,#±< offset_12>]! (1)编码格式 指令的编码格式如图 4.17 所示。 cond 01 0 1 U B 1 L Rn Rd offset_12 31 28 27 26 25 20 19 16 15 12 11 0 图 4.17 内存访问指令——前索引立即数偏移寻址编码格式 内存地址为基址寄存器 Rn 加/减立即数 offset_8 的值。当指令执行的条件满足时,生成的地址写回基 址寄存器 Rn 中。 专业始于专注 卓识源于远见 ‐ 16 ‐ 该寻址方式适合访问数组自动进行数组下标的更新。 (2)语法格式 LDR|STR{}{B}{T} ,[,±] ! 其中:  Rn 为基址寄存器,该寄存器包含内存访问的基地址;  为 12 位立即数,内存访问地址偏移量;  !设置指令编码中的 W 位,更新指令基址寄存器。 (3)操作伪代码 If U == 1 then Address = Rn + offset_12 Else Address = Rn – offset_12 If ConditionPassed{cond} then Rn = address (4)说明 ① 如果指令中没有指定立即数,使用[],编译器按[,#0] ! 形式编码。 ② 如果 Rn 被指定为程序计数器 r15,指令的执行结果不可预知。 5.[Rn,±Rm]! (1)编码格式 指令的编码格式如图 4.18 所示。 cond 01 1 1 U B 1 L Rn Rd 00000000 31 28 27 26 25 20 19 16 15 12 11 0 Rm 图 4.18 内存访问指令——前索引寄存器偏移寻址编码格式 内存访问地址为基址寄存器 Rn 的值加(或减)偏移寄存器 Rm 的值。当指令的执行条件满足时,生 成地地址将写回基址寄存器。 (2)语法格式 LDR|STR{}{B}{T} ,[,±] 其中:  Rn 为基址寄存器,该寄存器包含内存访问的基地址;  为偏移地址寄存器,包含内存访问地址偏移量;  !设置指令编码中的 W 位,更新指令基址寄存器。 (3)操作伪代码 If U = = 1 then Address = Rn + Rm Else Address = Rn – Rm If ConditionPassed{cond} then Rn = address (4)说明 如果 Rn 和 Rm 指定为同一寄存器,指令的执行结果不可预知。 专业始于专注 卓识源于远见 ‐ 17 ‐ 6.[Rn,±Rm,#< offset_12>]! (1)编码格式 指令的编码格式如图 4.19 所示。 cond 01 1 1 U B 1 L Rn Rd shift_imm 31 28 27 26 25 20 19 16 15 12 11 0 shift 0 Rm 7 6 4 3 5 图 4.19 内存访问指令——带移位的前索引寄存器偏移寻址编码格式 内存地址为 Rn 的值加/减通过移位操作后的 Rm 的值。当指令的执行条件满足时,生成地地址将写回 基址寄存器。 (2)语法格式 语法格式有以下 5 种。 LDR|STR{}{B}{T} ,[,±,LSL #< offset_12>] ! LDR|STR{}{B}{T} ,[,±,LSR #< offset_12>] ! LDR|STR{}{B}{T} ,[,±,ASR #< offset_12>] ! LDR|STR{}{B}{T} ,[,±,ROR #< offset_12>] ! LDR|STR{}{B}{T} ,[,±,RRX] ! 其中:  Rn 为基址寄存器,该寄存器包含内存访问的基地址;  为偏移地址寄存器,包含内存访问地址偏移量;  LSL 表示逻辑左移操作;  LSR 表示逻辑右移操作;  ASR 表示算术右移操作;  ROR 表示循环右移操作;  RRX 表示扩展的循环右移。  为移位立即数。  !设置指令编码中的 W 位,更新指令基址寄存器。 (3)操作伪代码 Case shift of 0b00 /*LSL*/ Index = Rm logic_shift_left shift_imm 0b01 /*LSR*/ If shift_imm = = 0 then /*LSR #32*/ Index = 0 Else Index = Rm logical_shift_right shift_imm 0b10 /*ASR*/ If shift_imm = = 0 then /*ASR #32*/ If Rm[31] = = 1 then Index = 0xffffffff Else Index = 0 Else Index = Rm Arithmetic_shift_Right shift_imm 0b11 /* ROR or RRX*/ If shift_imm = = 0 then /*RRX*/ 专业始于专注 卓识源于远见 ‐ 18 ‐ Index = (C flag Logical_shift_left 31) OR (Rm logical_shift_Right 1) Else /*ROR*/ Index = Rm Rotate_Right shift_imm Endcase If U = = 1 then Address = Rn + index Else /*U = = 0*/ Address = Rn – index If ConditionPassed{cond} then Rn = address (4)说明 ① 当 PC 用作基址寄存器 Rn 或 Rm 时,指令执行结果不可预知。 ② 当 Rn 和 Rm 是同一个寄存器时,指令的执行结果不可预知。 7.[Rn],#±< offset_12> (1)编码格式 指令的编码格式如图 4.20 所示。 cond 01 0 1 U B 1 L Rn Rd offset_12 31 28 27 26 25 20 19 16 15 12 11 0 图 4.20 内存访问指令——后索引立即数偏移寻址编码格式 指令使用基址寄存器 Rn 的值作为实际内存访问地址。当指令的执行条件满足时,将基址寄存器的值加/减 偏移量产生新的地址值回写到 Rn 寄存器中。 (2)语法格式 LDR|STR{}{B}{T} ,[],± 其中:  Rn 为基址寄存器,该寄存器包含内存访问的基地址;  为 12 位立即数,内存访问地址偏移量。 (3)操作伪代码 Address = Rn If conditionPassed{cond} then If U = = 1 then Rn = Rn + offset_12 Else Rn = Rn – offset_12 (4)说明 ① LDRBT、LDRT、STRBT 和 STRT 指令只支持后索引寻址。 ② 如果 Rn 被指定为程序计数器 r15,指令的执行结果不可预知。 8.[Rn],± (1)编码格式 专业始于专注 卓识源于远见 ‐ 19 ‐ 指令的编码格式如图 4.21 所示。 cond 01 1 0 U B 0 L Rn Rd 00000000 31 28 27 26 25 20 19 16 15 12 11 0 Rm 图 4.21 内存访问指令——后索引寄存器偏移寻址编码格式 指令访问地址为实际的基址寄存器的值。当指令的执行条件满足时,将基址寄存器的值加/减索引寄存器 Rm 的值回写到 Rn 基址寄存器。 (2)语法格式 LDR|STR{}{B}{T} ,[Rn],± 其中:  Rn 为基址寄存器,该寄存器包含内存访问的基地址;  为偏移地址寄存器,包含内存访问地址偏移量。 (3)操作伪代码 Address = Rn If conditionPassed{cond} then If U = = 1 then Rn = Rn + Rm Else Rn = Rn – Rm (4)说明 ① LDRBT、LDRT、STRBT 和 STRT 指令只支持后索引寻址。 ② 如果 Rm 和 Rn 指定为同一寄存器,指令的执行结果不可预知。 9.[Rn],±Rm,#< offset_12>] (1)编码格式 指令的编码格式如图 4.22 所示。 cond 01 1 0 U B 0 L Rn Rd shift_imm 31 28 27 26 25 20 19 16 15 12 11 0 shift 0 Rm 7 6 4 3 5 图 4.22 内存访问指令——带移位的后索引寄存器偏移寻址编码格式 实际的内存访问地址为寄存器 Rn 的值。当指令的执行条件满足时,将基址寄存器值加/减一个地址偏移量 产生新的地址值。 (2)语法格式 语法格式有以下 5 种。 LDR|STR{}{B}{T} ,[],±,LSL #< offset_12> LDR|STR{}{B}{T} ,[],±,LSR #< offset_12> LDR|STR{}{B}{T} ,[],±,ASR #< offset_12> LDR|STR{}{B}{T} ,[],±,ROR #< offset_12> LDR|STR{}{B}{T} ,[],±,RRX 其中:  Rn 为基址寄存器,该寄存器包含内存访问的基地址; 专业始于专注 卓识源于远见 ‐ 20 ‐  为偏移地址寄存器,包含内存访问地址偏移量;  LSL 表示逻辑左移操作;  LSR 表示逻辑右移操作;  ASR 表示算术右移操作;  ROR 表示循环右移操作;  RRX 表示扩展的循环右移。  为移位立即数。 (3)操作伪代码 Address = Rn Case shift of 0b00 /*LSL*/ Index = Rm logic_shift_left shift_imm 0b01 /*LSR*/ If shift_imm = = 0 then /*LSR #32*/ Index = 0 Else Index = Rm logical_shift_right shift_imm 0b10 /*ASR*/ If shift_imm = = 0 then /*ASR #32*/ If Rm[31] = = 1 then Index = 0xffffffff Else Index = 0 Else Index = Rm Arithmetic_shift_Right shift_imm 0b11 /* ROR or RRX*/ If shift_imm = = 0 then /*RRX*/ Index = (C flag Logical_shift_left 31) OR (Rm logical_shift_Right 1) Else /*ROR*/ Index = Rm Rotate_Right shift_imm Endcase If ConditionPassed{cond} then If U = = 1 then Rn = Rn + index Else /*U = = 0*/ Rn = Rn – index (4)说明 ① LDRBT、LDRT、STRBT 和 STRT 指令只支持后索引寻址。 ② 当 PC 用作基址寄存器 Rn 或 Rm 时,指令执行结果不可预知。 ③ 当 Rn 和 Rm 是同一个寄存器时,指令的执行结果不可预知。 4.2.2 杂类 Load/Store 指令的寻址方式 使用该类寻址方式的指令的语法格式如下。 LDR|STR{}H|SH|SB|D 专业始于专注 卓识源于远见 ‐ 21 ‐ 使用该类寻址方式的指令包括:(有符号/无符号)半字 Load/Store 指令、有符号字节 Load/Store 指令和双 字 Load/Store 指令。 该类寻址方式分为 6 种类型,如表 4.4 所示。 表 4.4 杂类 Load/Store 指令的寻址方式 格 式 模 式 1 [Rn,#±] 立即数偏移寻址 (Immediate offset) 2 [Rn,±Rm] 寄存器偏移寻址 (Register offset) 3 [Rn,#±< offset_8>]! 立即数前索引寻址 (Immediate pre-indexed) 4 [Rn,±Rm]! 寄存器前索引寻址 (Register post-indexed) 5 [Rn],#±< offset_8> 立即数后索引寻址 (Immediate post-indeded) 6 [Rn],± 寄存器后索引寻址 (Register post-indexed) 杂类 Load/Store 指令的解码格式如图 4.23 所示。 cond 000 P U 1 W L Rn Rd immedH 31 28 27 26 25 20 19 16 15 12 11 0 immedL 7 6 4 3 5 24 1 S H 1 8 立即数 cond 000 P U 0 W L Rn Rd SBZ 31 28 27 26 25 20 19 16 15 12 11 0 Rm 7 6 4 3 5 1 S H 1 8 寄存数 图 4.23 杂类 Load/Store 指令解码格式 编码格式中各标志位的含义如表 4.5 所示。 表 4.5 杂类 Load/Store 指令编码格式各标志位含义 位 标 识 取 值 含 义 P P=0 使用后索引寻址 P=1 使用偏移地址或前索引寻址(由 W 位决定) 续表 位 标 识 取 值 含 义 U U=0 访问的地址=基址寄存器的值-偏移量(offset) U=1 访问的地址=基址寄存器的值+偏移量(offset) W W=0 如果 P=0,使用后索引寻址;P=1,指令不改变基址寄存器的值 W=1 如果 P=0,未定义指令;如果 P=1,将计算的内存访问地址回写到 基址寄存器 L L=0 Store 指令 L=1 Load 指令 S S=0 无符号半字内存访问 专业始于专注 卓识源于远见 ‐ 22 ‐ S=1 有符号半字内存访问 H H=0 字节访问 H=1 半字访问 注意 当 S=0 并且 H=0 时,并非无符号的字节内存访问指令。无符号的内存访问指令不使用该种寻址 方式,详见本章上一节。 当 S=1 并且 L=0 时,并非是有符号的存储指令,而是未定义指令。ARM 指令并未区分有符号 和无符号的字节和半字存储。 1.[Rn,#±] (1)编码格式 指令的编码格式如图 4.24 所示。 cond 0001 U 10 L Rn Rd immedH 31 28 27 26 25 20 19 16 15 12 11 0 immedL 7 6 4 3 5 24 1 S H 1 8 立即数 图 4.24 杂项内存访问指令——立即数偏移寻址编码格式 内存访问地址为基址寄存器 Rn 的值加(或减)立即数 offset_8。 编程中,在访问结构体或记录(record)类型的变量时,这些内存的操作指令是十分有效的。另外,在子 程序中,也常用这些指令访问本地变量和堆栈。当 offset_8=0 时,内存访问地址即基址寄存器 Rn 的值。 (2)语法格式 LDR|STR{}H|SH|SB|D ,[,#±] 其中:  Rn 为基址寄存器,该寄存器包含内存访问的基地址。  为 8 位立即数,内存访问地址偏移量。 (3)操作伪代码 offset_8 = (immedH << 4) OR immedL If U = = 1 then Address = Rn + offset_8 Else Address = Rn – offset_8 (4)说明 ① 如果指令中没有指定立即数,使用[],编译器按[,#0]形式编码。 ② 如果 Rn 被指定为程序计数器 r15,其值为当前指令地址加 8。 2.[Rn,±Rm] (1)编码格式 指令的编码格式如图 4.25 所示。 专业始于专注 卓识源于远见 ‐ 23 ‐ cond 0001 U 00 L Rn Rd SBZ 31 28 27 20 19 16 15 12 11 0 Rm 7 6 4 3 5 24 1 S H 1 8 图 4.25 杂项内存访问指令——寄存器偏移寻址编码格式 内存访问地址为基址寄存器 Rn 的值加(或减)偏移寄存器 Rm 的值。 该寻址方式适合使用指针访问数组中的单个数据成员。 (2)语法格式 LDR|STR{}H|SH|SB|D ,[,±] 其中:  Rn 为基址寄存器,该寄存器包含内存访问的基地址;  为偏移地址寄存器,包含内存访问地址偏移量。 (3)操作伪代码 If U = = 1 then Address = Rn + Rm Else Address = Rn – Rm (4)说明 如果 Rn 被指定为程序计数器 r15,其值为当前指令地址加 8;如果 r15 被用作偏移地址寄存器 Rm 的值, 指令的执行结果不可预知。 3.[Rn,#±< offset_8>]! (1)编码格式 指令的编码格式如图 4.26 所示。 cond 0001 U 11 L Rn Rd immedH 31 28 27 20 19 16 15 12 11 0 immedL 7 6 4 3 5 24 1 S H 1 8 图 4.26 杂类内存访问指令——前索引立即数偏移寻址编码格式 内存地址为基址寄存器 Rn 加/减立即数 offset_8 的值。当指令执行的条件满足时,生成的地址写回基 址寄存器 Rn 中。 该寻址方式适合访问数组自动进行数组下标的更新。 (2)语法格式 LDR|STR{}H|SH|SB|D ,[,±] ! 其中:  Rn 为基址寄存器,该寄存器包含内存访问的基地址;  为 8 位立即数,内存访问地址偏移量,在指令编码格式中被拆为 immedH 和 immedL 两部分;  !设置指令编码中的 W 位,更新指令基址寄存器。 (3)操作伪代码 offset_8 = (immedH) << 4 OR immedL If U == 1 then Address = Rn + offset_8 专业始于专注 卓识源于远见 ‐ 24 ‐ Else Address = Rn – offset_8 If ConditionPassed{cond} then Rn = address (4)说明 ① 如果指令中没有指定立即数,使用[],编译器按[,#0] ! 形式编码。 ② 如果 Rn 被指定为程序计数器 r15,指令的执行结果不可预知。 4.[Rn,±Rm] ! (1)编码格式 指令的编码格式如图 4.27 所示。 cond 0001 U 01 L Rn Rd SBZ 31 28 27 20 19 16 15 12 11 0 Rm 7 6 4 3 5 24 1 S H 1 8 图 4.27 杂项内存访问指令——前索引寄存器偏移寻址编码格式 内存访问地址为基址寄存器 Rn 的值加(或减)偏移寄存器 Rm 的值。当指令的执行条件满足时,生 成地地址将写回基址寄存器。 (2)语法格式 LDR|STR{}H|SH|SB|D ,[,±] 其中:  Rn 为基址寄存器,该寄存器包含内存访问的基地址;  为偏移地址寄存器,包含内存访问地址偏移量;  !设置指令编码中的 W 位,更新指令基址寄存器。 (3)操作伪代码 If U = = 1 then Address = Rn + Rm Else Address = Rn – Rm If ConditionPassed{cond} then Rn = address (4)说明 ① 如果 Rn 和 Rm 指定为同一寄存器,指令的执行结果不可预知。 ② 如果程序计数器 r15 被用作 Rm 或 Rn,则指令的执行结果不可预知。 5.[Rn],#±< offset_8> (1)编码格式 指令的编码格式如图 4.28 所示。 cond 0000 U 10 L Rn Rd immedH 31 28 27 20 19 16 15 12 11 0 immedL 7 6 4 3 5 24 1 S H 1 8 专业始于专注 卓识源于远见 ‐ 25 ‐ 图 4.28 杂项内存访问指令——后索引立即数偏移寻址编码格式 指令使用基址寄存器 Rn 的值作为实际内存访问地址。当指令的执行条件满足时,将基址寄存器的值加/减 偏移量生产新的地址值回写到 Rn 寄存器中。 (2)语法格式 LDR|STR{}H|SH|SB|D ,[],± 其中:  Rn 为基址寄存器,该寄存器包含内存访问的基地址;  为 8 位立即数,内存访问地址偏移量。 (3)操作伪代码 Address = Rn Offset_8 = (immedH << 4) OR immedL If conditionPassed{cond} then If U = = 1 then Rn = Rn + offset_8 Else Rn = Rn – offset_8 (4)说明 ① 当指令中没有指定立即数时,汇编器按“[],#0”编码。 ② 如果 Rn 被指定为程序计数器 r15,指令的执行结果不可预知。 6.[Rn],± (1)编码格式 指令的编码格式如图 4.29 所示。 cond 0000 U 00 L Rn Rd SBZ 31 28 27 20 19 16 15 12 11 0 Rm 7 6 4 3 5 24 1 S H 1 8 图 4.29 杂项内存访问指令——后索引寄存器偏移寻址编码格式 指令访问地址为实际的基址寄存器的值。当指令的执行条件满足时,将基址寄存器的值加/减索引寄存器 Rm 的值回写到 Rn 基址寄存器。 (2)语法格式 LDR|STR{}H|SH|SB|D ,[Rn],± 其中:  Rn 为基址寄存器,该寄存器包含内存访问的基地址;  为偏移地址寄存器,包含内存访问地址偏移量。 (3)操作伪代码 Address = Rn If conditionPassed{cond} then If U = = 1 then Rn = Rn + Rm Else Rn = Rn – Rm (4)说明 专业始于专注 卓识源于远见 ‐ 26 ‐ ① 程序寄存器 r15 被指定为 Rm 或 Rn,指令的执行结果不可预知。 ② 如果 Rm 和 Rn 指定为同一寄存器,指令的执行结果不可预知。 4.2.3 批量 Load/Store 指令寻址方式 批量 Load/Store 指令将一片连续内存单元的数据加载到通用寄存器组中或将一组通用寄存器的数据存储到 内存单元中。 批量 Load/Store 指令的寻址模式产生一个内存单元的地址范围,指令寄存器和内存单元的对应关系满足这 样的规则,即编号低的寄存器对应于内存中低地址单元,编号高的寄存器对应于内存中的高地址单元。 指令的语法格式如下。 LDM|STM{} {!},<^> 指令的寻址方式如表 4.6 所示。 表 4.6 批量 Load/Store 指令的寻址方式 格 式 模 式 1 IA(Increment After) 后递增方式 2 IB(Increment Before) 先递增方式 3 DA(Decrement After) 后递减方式 4 DB(Decrement Before) 先递减方式 指令的编码格式如图 4.30 所示。 cond 100 P U S W L Rn 31 28 27 25 20 19 16 15 0 Register list 24 图 4.30 批量 Load/Store 指令编码格式 编码格式中各标志位的含义如表 4.7 所示。 表 4.7 批量 Load/Store 指令编码格式各标志位含义 位标识 取 值 含 义 P P=0 Rn 包含的地址,是所要访问的内存块的高地址(U=0)还是低地址(U=1) P=1 标识 Rn 所指向的内存单元是否被访问 U U=0 Rn 所指内存单元为所要访问的内存单元块的高地址 U=1 Rn 所指内存单元为所要访问的内存单元块的低地址 S S=0 当程序计数器 PC 作为要加载的寄存器之一时,S 标识是否将 spsr 内容拷贝 到 cpsr;对于不加载 PC 的 load 指令和所有 store 指令,S 标识特权模式下, 使用用户模式寄存器组代替当前模式下寄存器组 S=1 W W=0 数据传送完毕,更新地址寄存器内容 W=1 L L=0 Store 指令 L=1 Load 指令 1.IA 寻址 专业始于专注 卓识源于远见 ‐ 27 ‐ (1)编码格式 指令的编码格式如图 4.31 所示。 该寻址方式指定一片连续的内存地址空间,地址空间的大小等于寄存器列表中寄存器数目 的 4 倍。内存地址范围起始地址等于基址寄存器 Rn 的值。结束地址等于起始 地址加上地址空间大小。 cond 100 0 1 S W L Rn 31 28 27 25 20 19 16 15 0 Register list 24 图 4.31 批量 Load/Store 指令——后增加寻址 地址空间中的每个内存单元对应寄存器列表中的一个寄存器。编号低的寄存器对应于内存中低地址单元, 编号高的寄存器对应于内存中的高地址单元。 当指令执行条件满足并且指令编码格式中 W 位置位,基址寄存器 Rn 的值等于内存地址范围结束地址 加 4。 (2)语法格式 LDM|STM{}IA {!},<^> 其中:  IA 标识指令使用“后增加”寻址方式;  Rn 为基址寄存器,包含内存访问的基地址;  为指令操作的寄存器列表;  <^>表示如果寄存器列表中包含程序计数器 PC,是否将 spsr 拷贝到 cpsr。 (3)操作伪代码 Start_address = Rn End_address = Rn + (Number_of_Set_Bits_In(register_list)*4) – 4 If conditionPassed(cond) and W = = 1 then Rn = Rn + (Number_of_Set_Bits_In(register_list)*4) 2.DA 寻址 (1)编码格式 指令的编码格式如图 4.32 所示。 cond 100 0 0 S W L Rn 31 28 27 25 20 19 16 15 0 Register list 24 图 4.32 批量 Load/Store 指令——后递减寻址 该寻址方式指定一片连续的内存地址空间,地址空间的大小等于寄存器列表中寄存器数目 的 4 倍。内存地址范围起始地址等于基址寄存器 Rn 的值减去地址空间大小 并加 4。结束地址等于基址寄存器的值。 地址空间中的每个内存单元对应寄存器列表中的一个寄存器。编号低的寄存器对应于内存中低地址单元, 编号高的寄存器对应于内存中的高地址单元。 当指令执行条件满足并且指令编码格式中 W 位置位时,基址寄存器 Rn 的值等于内存地址范围起始地址 减 4。 (2)语法格式 专业始于专注 卓识源于远见 ‐ 28 ‐ LDM|STM{}IA {!},<^> 其中:  DA 标识指令使用“后递减”寻址方式;  Rn 为基址寄存器,包含内存访问的基地址;  为指令操作的寄存器列表;  <^>表示如果寄存器列表中包含程序计数器 PC,是否将 spsr 拷贝到 cpsr。 (3)操作伪代码 Start_address = Rn – (Number_of_Set_Bits_In(register_list)*4) + 4 End_address = Rn If conditionPassed(cond) and W = = 1 then Rn = Rn - (Number_of_Set_Bits_In(register_list)*4) 3.IB 寻址 (1)编码格式 指令的编码格式如图 4.33 所示。 cond 100 1 1 S W L Rn 31 28 27 25 20 19 16 15 0 Register list 24 图 4.33 批量 Load/Store 指令——前增加寻址 该寻址方式指定一片连续的内存地址空间,地址空间的大小等于寄存器列表中寄存器数目 的 4 倍。内存地址范围起始地址等于基址寄存器 Rn 的值加 4。结束地址等于 起始地址加上地址空间大小。 地址空间中的每个内存单元对应寄存器列表中的一个寄存器。编号低的寄存器对应于内存中低地址单元, 编号高的寄存器对应于内存中的高地址单元。 当指令执行条件满足并且指令编码格式中 W 位置位,基址寄存器 Rn 的值等于内存地址范围结束地址 。 (2)语法格式 LDM|STM{}IB {!},<^> 其中:  IB 标识指令使用“前增加”寻址方式;  Rn 为基址寄存器,包含内存访问的基地址;  为指令操作的寄存器列表;  <^>表示如果寄存器列表中包含程序计数器 PC,是否将 spsr 拷贝到 cpsr。 (3)操作伪代码 Start_address = Rn + 4 End_address = Rn + (Number_of_Set_Bits_In(register_list)*4) If ConditionPassed(cond) and W= = 1 then Rn = Rn + (Number_Of_Set_Bits_In(register_list)*4) 4.DB 寻址 (1)编码格式 专业始于专注 卓识源于远见 ‐ 29 ‐ 指令的编码格式如图 4.34 所示。 cond 100 1 0 S W L Rn 31 28 27 25 20 19 16 15 0 Register list 24 图 4.34 批量 Load/Store 指令——前递减寻址 该寻址方式指定一片连续的内存地址空间,地址空间的大小等于寄存器列表中寄存器数目 的 4 倍。内存地址范围起始地址等于基址寄存器 Rn 的值减地址空间的大小。 结束地址等于基址寄存器的值减 4。 地址空间中的每个内存单元对应寄存器列表中的一个寄存器。编号低的寄存器对应于内存中低地址单元, 编号高的寄存器对应于内存中的高地址单元。 当指令执行条件满足并且指令编码格式中 W 位置位,基址寄存器 Rn 的值等于内存地址范围起始地址 。 (2)语法格式 LDM|STM{}DB {!},<^> 其中:  DB 标识指令使用“前递减”寻址方式;  Rn 为基址寄存器,包含内存访问的基地址;  为指令操作的寄存器列表;  <^>表示如果寄存器列表中包含程序计数器 PC,是否将 spsr 拷贝到 cpsr。 (3)操作伪代码 Start_address = Rn - (Number_Of_Set_Bits_In(register_list)*4) End_address = Rn - 4 If ConditionPassed(cond) and W = = 1 then Rn = Rn – (Number_Of_Set_Bits_In(register_list)*4) 4.2.4 堆栈操作寻址方式 堆栈操作寻址方式和批量 Load/Store 指令寻址方式十分类似。但对于堆栈的操作,数据写入内存和从内存 中读出要使用不同的寻址模式,因为进栈操作(pop)和出栈操作(push)要在不同的方向上调整堆栈。 下面详细讨论如何使用合适的寻址方式实现数据的堆栈操作。 根据不同的寻址方式,将堆栈分为以下 4 种。 ① Full 栈:堆栈指针指向栈顶元素(last used location)。 ② Empty 栈:堆栈指针指向第一个可用元素(the first unused location)。 ③ 递减栈:堆栈向内存地址减小的方向生长。 ④ 递增栈:堆栈向内存地址增加的方向生长。 根据堆栈的不同种类,将其寻址方式分为以下 4 种。 ① 满递减 FD(Full Descending)。 ② 空递减 ED(Empty Descending)。 ③ 满递增 FA(Full Ascending)。 ④ 空递增 EA(Empty Ascending)。 注意 如果程序中有对协处理器数据的进栈/出栈操作,最好使用 FD 或 EA 类型堆栈。这样可以使用 一条 STC 或 LDC 指令将数据进栈或出栈。 表 4.8 显示了堆栈的寻址方式和批量 Load/Store 指令寻址方式的对应关系。 专业始于专注 卓识源于远见 ‐ 30 ‐ 表 4.8 堆栈寻址方式和批量 Load/Store 指令寻址方式对应关系 批量数据寻址方式 堆栈寻址方式 L 位 P 位 U 位 LDMDA LDMFA 1 0 0 LDMIA LDMFD 1 0 1 LDMDB LDMEA 1 1 0 LDMIB LDMED 1 1 1 STMDA STMED 0 0 0 STMIA STMEA 0 0 1 STMDB STMFD 0 1 0 STMIB STMFA 0 1 1 4.2.5 协处理器 Load/Store 寻址方式 协处理器 Load/Store 指令的语法格式如下。 {}{L} 表 4.9 显示了该类指令的寻址方式。 表 4.9 协处理器 Load/Store 指令寻址方式 格 式 说 明 1 [,#±*4] 立即数偏移寻址 2 [,#±*4]! 前索引立即数偏移寻址 3 [],#±*4 后索引立即数偏移寻址 4 [],

:该条指令实现了相对于 pc 的跳转 专业始于专注 卓识源于远见       ‐  31  ‐             LDR pc,[pc,offset]:这条指令将异常处理程序的入口地址从存储器装载到 pc。该地址是一个 32 位的 绝对地址。由于有额外的存储器访问,装载跳转地址会使分支跳转到特定处理程序,给系统执行带来延时。 不过,可以使用这种方法跳转到存储空间内的任意地址。  MOV pc,#immediate:将一个立即数复制到 pc。使用该指令可以跨越整个地址空间,但是受到地址对 齐问题的限制。这个地址必须由 8 位立即数循环右移偶数次得到。 另外,也可以在向量表中使用其他类型的指令。例如,FIQ 处理程序可以从地址 0x1c 处开始执行。因为它 位于向量表的最后,这样 FIQ 处理程序就可以不用跳转,立即从 FIQ 向量地址处开始执行。 下面的例子显示了一个使用 LDR 指令的向量表装载过程。 ;********************************** ;* VECTOR TABLE * ;********************************** AREA vectors, CODE ENTRY ; 定义标准的 ARM 向量表 INT_Vectors LDR PC, INT_Reset_Addr LDR PC, INT_Undef_Addr LDR PC, INT_Software_Addr LDR PC, INT_Prefetch_Addr LDR PC, INT_Data_Addr LDR PC, INT_Reserved_Addr LDR PC, INT_IRQ_Addr LDR PC, INT_FIQ_Addr 在向量表的入口处要有 ENTRY 标识。该标识通知链接程序该代码是一个可能的入口点,因而在链接时, 不能被清除。 13.5.3 ROM/RAM 重映射 启动时,0x0 处必须要有一条有效指令,因此,复位时 0x0000 地址必须为非易失性存储器,如 ROM 或 FLASH。 注意 有些系统是从 0xffff0000 处开始执行的,对于这样的系统,地址 0xffff0000 处必须为非易失性 存储器。 可以将 ROM 定位在 0x0 处。但是,这样配置有几个缺点。首先 ROM 存取速度通常较 RAM 要慢,当跳转 到异常处理程序时,系统性能可能会大受影响。其次,将向量表放于 ROM 中,运行时不能修改。 存储器地址重映射(Memory Remap)是当前很多先进控制器所具有的功能。所谓地址重映射就是可以通 过软件配置来改变存储器物理地址的一种机制或方法。 当一段程序对运行自己得存储器进行重映射时,需要特别注意保证程序执行流程在重映射前后的承接关 系。实现重映射的关键就是要使程序指针在 remap 以后能继续往下得到正确的指令。本书中介绍两种实现 重映射的机制,不同的系统可能会有多种灵活的 remap 方案,用户在具体实现时要具体分析。 1.先搬移后映射(Remap after Copy)  图 13.15 显示一种典型的存储器地址重映射情况。 专业始于专注 卓识源于远见       ‐  32  ‐            0x0000 0x4000 0x10000 0x18000 向量表 Reset异常处理程序 初始存储器视图 0x0000 0x4000 Reset异常处理程序 向量表 0x10000 0x18000 RAM/ROM重映射后 ROM RAM RAM ROM 图 13.15 ROM/RAM 重映射(1) 原来 RAM 和 ROM 各有自己的地址,进行重映射以后 RAM 和 ROM 的地址都发生了变化。这种情况下, 可以采用以下方案。 ① 上电后,从 0x0 地址的 ROM 开始往下执行。 ② 根据映射前的地址,对 RAM 进行必要的代码和数据拷贝。 ③ 拷贝完后,进行 remap 操作。 ④ 因为 RAM 在 remap 前准备好了内容,使得 PC 指针能继续在 RAM 里取到正确的指令。 2.先映射后搬移(Copy after Remap)  系统上电后的缺省状态是 0x0 地址上放有 ROM。这块 ROM 有两个地址:从 0 起始和从 0x10000 起始,里 面存储了初始化代码。当进行地址 remap 以后,从 0x0 起始的地址被定向到 RAM 上,ROM 则只保留有惟 一的从 0x10000 起始的地址。 如果存储在 ROM 里的复位异常处理程序(Reset-Handler)一直在 0x0~0x4000 的地址上运行,则当执行 完 remap 以后,下面的指令将从 RAM 里预取,这必然会导致程序执行流程的中断。根据系统特点,可以 用下面的办法来解决这个问题。 ① 上电后系统从 0x0 地址开始自动执行,设计跳转指令在 remap 发生前使 PC 指针指向 0x10000 开始的 ROM 地址中去,因为不同地址指向的是同一块 ROM,所有程序能够顺利执行。 ② 这时候 0x0~0x4000 的地址空间空闲,不被程序引用,执行 remap 后把 RAM 引进。因为程序一直在 0x10000 起始的 ROM 空间里运行,remap 对运行流程没有任何影响。 ③ 通过在 ROM 里运行的程序,对 RAM 进行相应的代码和数据拷贝,完成应用程序运行的初始化。 图 13.16 显示了 ROM 和 RAM 重映射的第二种解决方案。 0x0000 0x4000 0x10000 0x18000 复位异常处理程序 复位异常处理程序 由存储控 制器分配 给ROM的 第二地址 ROM 复位异常处理程序 复位异常处理程序 跳转到实 际的ROM 向量表 复位异常处理程序 清除ROM第二地址 1 2 3 RAM 专业始于专注 卓识源于远见       ‐  33  ‐            图 13.16 ROM/RAM 重映射(2) 该 ROM 与 RAM 地址重映射的方法可以应用于任何具有 ROM/RAM 重映射机制的平台,但是内存重映射 的地址根据具体平台的不同而不同。 图 13.16 显示的地址重映射例子中,第一条指令实现从 ROM 临时地址(0x0 地址)到实际 ROM 的跳转。 然后,控制寄存器的重映射位,清除 ROM 的临时地址设置。该代码通常在系统复位后立即执行。重新映 射必须在执行 C 库初始化代码前完成。 在具有 MMU 的系统中,可通过在系统启动时配置 MMU 来实现重映射。 下面的例子显示了在 ARM 的 Integrator 开发板上实现的 ROM/RAM 重映射过程。 ; --- Integrator CM control reg CM_ctl_reg EQU 0x1000000C ;定义 CM 控制寄存器地址 Remap_bit EQU 0x04 ;CM 控制寄存器重映射掩码 ENTRY ;复位异常处理程序开始 ; 执行跳转指令,转到实际的 ROM 执行 LDR pc, =Instruct_2 Instruct_2 ; 设置 CM 控制寄存器的重映射位 LDR r1, =CM_ctl_reg LDR r0, [r1] ORR r0, r0, #Remap_bit STR r0, [r1] ; 重映射后,RAM 在 0x0 地址 ; 将向量表从 ROM 拷贝到 RAM (由 __main 函数完成) 13.5.3 局部存储器设置有关的考虑事项 许多 ARM 处理器内核具有片上存储器系统,如 MMU 或 MPU。这些设备通常是在系统启动过程中进行设 置并启用的。因此,带有局部存储器系统的内核的初始化序列需要特别地考虑。 在前面所述的代码启动的过程中,__main 中 C 库初始化代码负责建立代码执行时的内存映像,在跳转到 __main 前,必须建立处理器内核的运行时存储器视图。这就是说,在复位处理程序中必须设置并启用 MMU 或 MPU。 另外,在跳转到__main 前(通常在 MMU/MPU 设置前),必须启用紧耦合存储器 TCM(Tightly coupled Memory),因为在通常情况下都是采用分散加载方法将代码和数据装入 TCM。当 TCM 启用后,用户不必 存取由 TCM 屏蔽的存储器。 在跳转到__main 前,如果启用了 Cache,可能还会遇到 Cache 一致性的问题,__main 中的函数将程序代码 从其加载域拷贝到执行域,在此过程中将指令作为数据进行处理。这样,一些指令可能被放入数据 Cache 中,在执行这些指令时,由于找不到地址路径而产生错误。为了避免 Cache 一致性的问题,在 C 库初始化 序列执行完成后再启用 Cache。 13.5.4 栈指针初始化 在程序的初始化代码中,用户必须要为处理器用到的各种模式设置堆栈,也就是说,复位处理程序必须为 应用程序所使用的任何执行模式的栈指针分配初始值。 下面的例子显示了如何在初始化代码中启用不同模式下的堆栈。 ; 启用系统模式堆栈 专业始于专注 卓识源于远见       ‐  34  ‐            LDR r2,INT_System_Stack ;将系统堆栈的全局变量放到 r2 中 STR sp,[r2] ;将系统堆栈指针存储到系统模式下的 sp ; 启用系统堆栈限制 (为 ARM 编译器的堆栈检测做准备) SUB r1,sp,#SYSTEM_STACK_SIZE ;跳转堆栈指针 BIC r1,r1,#0x03 ;4 字节对齐 MOV r10,r1 ;将堆栈的限制放入 r10 寄存器(AAPCS 规则) LDR r2,INT_System_Limit ;得到堆栈限制全局变量地址 STR r1,[r2] ;将堆栈限制存入全局变量 ; 切换到 IRQ 模式 MRS r0,CPSR ;得到当前的 CPSR 值 BIC r0,r0,#MODE_MASK ;清除模式位 ORR r1,r0,#IRQ_MODE ;设为 IRQ 模式 MSR CPSR_cxsf,r1 ;切换到 IRQ 模式 ;启用 IRQ 模式堆栈 LDR sp,=INT_Irq_SP ;将 IRQ 模式堆栈指针放入 sp_irq ; 切换到 FIQ ORR r1,r0,#FIQ_MODE ;设置 FIQ 模式位 MSR CPSR_cxsf,r1 ;切换到 FIQ 模式 ; Set-up FIQ stack LDR sp,=INT_Fiq_SP ;得到 FIQ 模式指针 ; 切换到 Abort 模式 ORR r1,r0,#ABT_MODE ;设置 Abort 模式位 MSR CPSR_cxsf,r1 ;切换到 ABT 模式 ; 启用 Abort 堆栈 LDR sp,=INT_Abort_SP ; 切换到未定义异常模式 ORR r1,r0,#UNDEF_MODE MSR CPSR_cxsf,r1 ;启用未定义指令模式堆栈 LDR sp,=INT_Undefined_SP ; 启用系统/用户堆栈 …… …… 专业始于专注 卓识源于远见       ‐  35  ‐            为了设置栈指针,进入每种模式(中断禁用)并为栈指针分配适合的值。要利用软件栈检查,也必须在此 设置栈限制。 复 位 处 理 程 序 中 设 置 的 栈 指 针 和 栈 限 制 值 由 C 库 初 始 化 代 码 作 为 参 数 自 动 传 递 给 __user_initial_stackheap()。因此,不允许__user_initial_stackheap()更改这些值。 下面的例子显示了如何实现__user_initial_stackheap(),该段代码可以和上面的堆栈指针设置程序配合使用。 IMPORT heap_base EXPORT __user_initial_stackheap() __user_initial_stackheap() ; 程序中指定栈基地址或在描述文件中指定该地址 LDR r0,=heap_base ; r1 contains SB value MOV pc,lr 13.5.5 硬件初始化 一般情况下,系统初始化代码和主应用程序是分开的。系统初始化要在主应用程序启动前完成。但部分与 硬件相关的系统初始化过程,如启用 Cache 和中断,必须在 C 库初始化代码执行完成后才能执行。 为了在进入主应用程序之前,完成系统初始化,可以使用$sub 和$super 函数标识符在进入主程序之前插入 一个例程。这一机制可以在不改变源代码的情况下扩展函数的功能。 下面的例子说明了如何使用$sub 和$super 函数标识。链接程序通过调用$sub$$main()函数取代对 main() 的调用。所以用户可以在自己编写的$sub$$main()例程中启用 Cache 或使能中断。 extern void $Super$$main(void); void $Sub$$main(void) { cache_enable(); // 使能 caches int_enable(); // 使能中断 $Super$$main(); // 调用原来的 main()函数 } 在$Sub$$main(void)函数中,链接程序通过调用$Super$$main(),使代码跳转到实际的 main()函数。 在完成硬件初始化之后,必须考虑主应用程序运行在何种模式。如果应用程序运行在特权模式(Privileged mode),只需在退出复位处理程序前切换到适当的模式;如果应用程序运行在用户模式下,要在完成系统 初始化之后,再切换到用户模式。模式的切换工作,一般在$Sub$$main(void)函数中完成。 13.6 进一步存储器映射考虑事项 上一节介绍了如何使用 Scatter 文件对程序的代码和数据进行放置。但这些方法只有在外设和堆栈限制在源 文件或头文件中定义好的前提下才能使用。为了增加程序的灵活性,最好在 Scatter 文件中设置这些信息, 本节将介绍这些方法。 13.6.1 在 Scatter 文件中定位目标外设 通常情况下,外设寄存器的内存映射地址是在源文件或头文件中定义的“硬编码(hard-code)”。但为了 增加代码的可移植性,可以在源文件中声明一个映射到外设寄存器的结构,并在这个结构在 Scatter 文件中 定位。 下面的例子定义了映射 32 位寄存器的定时器的 C 结构。 struct { 专业始于专注 卓识源于远见       ‐  36  ‐            volatile unsigned ctrl; /* timer 控制寄存器 */ volatile unsigned tmr; /* timer 值 */ } timer_regs; 要把该结构放在存储器映射的特定地址,需创建一个新的执行区来装入该结构。 下面的例子说明了在 Scatter 文件中将 timer_regs 结构定位在 0x40000000 地址处。 ROM_LOAD 0x24000000 0x04000000 { ; ... TIMER 0x40000000 UNINIT { timer_regs.o (+ZI) } ; ... } 需要特别注意的是,在应用程序启动过程中不将这些寄存器的内容初始化为零,因为这些地址对应的是外 设寄存器,如果将其初始化为 0,很可能改变系统的状态。必须将执行域的属性标记为 UNINIT,这样可 避免该区中的 ZI 数据初始化为零。 13.6.2 在 Scatter 文件中放置堆和栈 ARM 公司建议,在 Scatter 文件中指定堆和栈的位置。这主要有两个主要优点:  有关存储器映射的所有信息保存在一个文件中;  改变堆和栈只要求重新链接,而不需要重新编译。 1.显示放置标号  为了在 Scatter 文件中放置堆栈,必须在源文件中定义 Scatter 文件的参照符号。下面的例子在名为 stackheap.s 的汇编文件中创建标有 stack_base 和 heap_base 的符号。这样就可以在 Scatter 文件的执行域中定位每个符 号。 AREA stacks, DATA, NOINIT EXPORT stack_base stack_base SPACE 1 AREA heap, DATA, NOINIT EXPORT heap_base heap_base SPACE 1 END 下面的 Scatter 文件说明了如何在地址 0x20000 放置堆基址,如何在地址 0x40000 放置栈基址。堆基址和栈 基址的位置可通过分别编辑其执行区予以改变。但该方法的缺点是在该栈区的上部占用一个字的内存区域 放置 SPACE(stack_base)变量。 LOAD_FLASH 0x24000000 0x04000000 { ; ... HEAP 0x20000 UNINIT { stackheap.o (heap) } 专业始于专注 卓识源于远见       ‐  37  ‐            STACKS 0x40000 UNINIT { stackheap.o (stacks) } ; ... } 图 13.17 显示了堆和栈在内存中的放置情况。 Stack_base Heap_base 堆 栈 0x20000 0x40000 图 13.17 显示设置符号放置堆栈 2.使用链接程序生成符号  该方法需要在目标文件中指定堆和栈的大小。首先,在一个汇编源文件中为堆和栈定义一个适当大小的区 域。使用 SPACE 命令保留一个清零的存储器块。然后,为该区域设置 NOINIT 属性,避免在链接时被修 改。这样避免了显示放置堆栈标号而浪费内存空间。 下面的例子显示了如何在汇编源文件中预留出堆栈区域。 AREA stack, DATA, NOINIT SPACE 0x3000 ;为栈预留的空间 AREA heap, DATA, NOINIT SPACE 0x3000 ;为堆预留的空间 END 最后,可以在 Scatter 文件中定义执行域放置系统堆栈。 下面的例子显示了如何在 Scatter 文件中使用由联接器生成的符号放置堆栈。 LOAD_FLASH 0x24000000 0x04000000 { : STACK 0x1000 UNINIT ;length = 0x3000 { stackheap.o (stack) ;stack = 0x4000 to 0x1000 } HEAP 0x15000 UNINIT ;length = 0x3000 { stackheap.o (heap) ;heap = 0x15000 to 0x18000 } } 专业始于专注 卓识源于远见       ‐  38  ‐            链接程序生成了指向每个执行区基址和限制的符号,可将其引入目标代码,供__user_initial_stackheap()函 数使用: Image$$STACK$$ZI$$Limit = 0x4000 Image$$STACK$$ZI$$Base = 0x1000 Image$$HEAP$$ZI$$Base = 0x15000 Image$$HEAP$$ZI$$Limit = 0x18000 下面的例子通过使用 DCD 伪操作赋予这些链接符号更有意义的名称,可使该代码可读性更高。 IMPORT ||Image$$STACKS$$ZI$$Base|| IMPORT ||Image$$STACKS$$ZI$$Limit|| IMPORT ||Image$$HEAP$$ZI$$Base|| IMPORT ||Image$$HEAP$$ZI$$Limit|| stack_base DCD ||Image$$STACKS$$ZI$$Limit|| ; = 0x4000 stack_limit DCD ||Image$$STACKS$$ZI$$Base|| ; = 0x1000 heap_base DCD ||Image$$HEAP$$ZI$$Base|| ; = 0x15000 heap_limit DCD ||Image$$HEAP$$ZI$$Limit|| ; = 0x18000 这样如果需要改变系统堆栈的设置,可以通过编辑 Scatter 文件中的执行域很容易地改变,而不需要重新编 译源文件。 3.使用 Scatter 文件的 EMPTY 属性  该方法使用了 Scatter 文件执行域的 EMPTY 属性。该属性使得定义的区域不包括目标代码或数据。这是定 义堆和栈的一个方便方法。区域的长度在 EMPTY 属性后指定。对于存储器中向上增长的堆,其区域的长 度为正。对于栈,其长度被标为负数,说明其在存储器中是向下增长的。 下面的例子显示了如何在 Scatter 文件中使用 EMPTY 属性定义堆栈。 ROM_LOAD 0x24000000 0x04000000 { ... HEAP 0x30000 EMPTY 0x3000 { } STACKS 0x40000 EMPTY -0x3000 { } ... } 该方法的优点是堆和栈的大小和位置是在一个地方定义的,即在 Scatter 文件中,而不必为堆栈创建 stackheap.s 源文件。 链接时,链接程序生成代表这些 EMPTY 区的符号。 Image$$HEAP$$ZI$$Base = 0x30000 Image$$HEAP$$ZI$$Limit = 0x33000 Image$$STACKS$$ZI$$Base = 0x3D000 Image$$STACKS$$ZI$$Limit = 0x40000 应用程序代码可处理这些符号,如下例所示。 IMPORT ||Image$$HEAP$$ZI$$Base|| IMPORT ||Image$$HEAP$$ZI$$Limit|| 专业始于专注 卓识源于远见       ‐  39  ‐            heap_base DCD ||Image$$HEAP$$ZI$$Base|| heap_limit DCD ||Image$$HEAP$$ZI$$Limit|| IMPORT ||Image$$STACKS$$ZI$$Base|| IMPORT ||Image$$STACKS$$ZI$$Limit|| stack_base DCD ||Image$$STACKS$$ZI$$Limit|| stack_limit DCD ||Image$$STACKS$$ZI$$Base|| 联系方式 集团官网:www.hqyj.com 嵌入式学院:www.embedu.org 移动互联网学院:www.3g-edu.org 企业学院:www.farsight.com.cn 物联网学院:www.topsight.cn 研发中心:dev.hqyj.com 集团总部地址:北京市海淀区西三旗悦秀路北京明园大学校内 华清远见教育集团 北京地址:北京市海淀区西三旗悦秀路北京明园大学校区,电话:010-82600386/5 上海地址:上海市徐汇区漕溪路银海大厦 A 座 8 层,电话:021-54485127 深圳地址:深圳市龙华新区人民北路美丽 AAA 大厦 15 层,电话:0755-22193762 成都地址:成都市武侯区科华北路 99 号科华大厦 6 层,电话:028-85405115 南京地址:南京市白下区汉中路 185 号鸿运大厦 10 层,电话:025-86551900 武汉地址:武汉市工程大学卓刀泉校区科技孵化器大楼 8 层,电话:027-87804688 西安地址:西安市高新区高新一路 12 号创业大厦 D3 楼 5 层,电话:029-68785218 《ARM 系列处理器应用技术完全手册》 作者:华清远见 第 14 章 高效的 C 编程 本章目标 本章将帮助读者在 ARM 处理器上编写高效的 C 代码。本章涉及的一些技术不 仅适用于 ARM 处理器,也适用于其他 RISC 处理器。本章首先从 ARM 编译器及其 优化入手,讲解 C 编译器在优化代码时所碰到的一些问题。理解这些问题,将有助 于编写出在提高执行速度和减少代码尺寸方面更高效的 C 源代码。 本章假定读者熟悉 C 语言,并且有一些汇编语言编程方面的知识。有关 ARM 编程的详细信息,请参阅本书的相关章节。 专业始于专注 卓识源于远见       ‐  2  ‐            14.1 C 编译器及其优化 本章主要讲解 C 编译器在代码优化时遇到的一些问题。要编写高效的 C 语言源代码,必须了解 C 编译器 对什么形式的代码有所改动,编译器涉及的处理器结构的限制,以及一些特殊的 C 编译器的限制。 14.1.1 为编译器选择处理器结构 在编译 C 源文件时,必须为编译器指定正确的处理器类型。这样可以使编译的代码最大限度地利用处理器 的硬件结构,如对半字加载(Halfword Load)、存储指令(Store Instructions)和指令调度(Instruction Scheduling)的支持。所以编译程序时,应该尽量准确地告诉编译器该代码是运行在什么类型的处理器上。 有些处理器类型编译器是不能直接支持,如 SA-1100,这时可以使用与该类型处理器为同一指令集的基本 处理器,比如对于 SA-100,可以使用 StrongARM。 注意 指定目标处理器可能使代码与其他 ARM 处理器不兼容。例如,编译时指定了 ARMv6 体系结 构的代码,可能不能运行在 ARM920T 的处理器上(如果代码中使用了 ARMv6 体系结构中特 有的指令)。 选择处理器类型可以使用--cpu name 编译选项。该选项生成用于特定 ARM 处理器或体系结构的代码。 如果 name 是处理器名称。  输入名称必须和 ARM 数据表中所示严格一致,例如 ARM7TDMI。该选项不接受通配符字符。有效值 是任何 ARM6 或更高版本的 ARM 处理器。  选择处理器操作会选择适当的体系结构、浮点单元 (FPU) 以及存储结构。  某些--cpu 选择暗含--fpu 选择。例如,当使用--arm 选项编译时,--cpu ARM1136JF-S 暗含--fpu vfpv2。 隐式 FPU 只覆盖命令行上出现在--cpu 选项前面的显式--fpu 选项。如果没有指定--fpu 选项和--cpu 选项, 则使用--fpu softvfp。 14.1.2 调试选项 如果在编译 C 源程序时,设置了调试选项,这将很大程度地影响最终代码的大小和执行效率。因为带调试 信息的代码映像,为了能够在调试程序时正确地显示变量或设置断点,包含很多冗余的代码和数据。所以 如果想最大限度地提供程序执行效率、减少代码尺寸,就要在编译源文件时,去除编译器的调试选项。 以下选项指定调试表生成方法。  -g (--debug):该选项启用生成当前编译的调试表。无论是否使用-g 选项,编译器都生成的代码是相 同的。惟一差别是调试表的存在与否。编译器是否对代码进行优化是由-O 选项指定调的。默认情况下,使 用-g 选项等价于使用:-g -dwarf2 --debug_macros。 注意 编译程序时,只使用-g 选项而没有使用优化选项,编译器会提示警告信息。  --no_debug:该选项禁止生成当前编译的调试表。这是默认选项。  --no_debug_macros:当与-g 一起使用时,该选项禁止生成预处理程序宏定义的调试表条目(Entry)。这 会减小调试映像的大小。-gt-p 是-gtp 的同义字。 --debug_macros 当与 -g 一起使用时,该选项启用生成预处理程序宏定义的调试表条目。这是默认选项, 会增加调试映像的大小。一些调试程序忽略预处理程序条目。 14.1.3 优化选项 专业始于专注 卓识源于远见       ‐  3  ‐            使用-Onum 选择编译器的优化级别。优化级别分别为。  -O0:除一些简单的代码编号之外,关闭所有优化。使用该编译选项可以提供最直接的优化信息。  -O1:关闭严重影响调试效果的优化功能。使用该编译选项,编译器会移除程序中未使用到的内联函数 和静态函数。如果与 --debug 一起使用,该选项可以在较好的代码密度下,给出最佳调试视图。  -O2:生成充分优化代码。如果与 --debug 一起使用,调试效果可能不令人满意,因为目标代码到源代 码的映射可能因为代码优化而发生变化。 如果不生成调试表,这是默认优化级别。  -O3:最高优化级别。使用该优化级别,使生成的代码在时间和空间上寻求平衡。该选项常和-Ospace 和-Otime 配合使用。  -O3 –Otime:使用该选项编译的代码比-O2 –Otime 选项编译的代码,在执行速度上要快,但占用的空间 也更大。  -O3 -Ospace:产生的代码比使用-O2 -Ospace 选项产生的代码尺寸小,但执行效率可能会差。 如果要使编译的代码更侧重于代码的尺寸或执行效率(两者往往不可兼得),可以使用下面的编译选项。  -Ospace:指示编译程序执行优化,以延长执行时间为代价减小映像大小。例如,由外部函数调用代替 内联函数。如果代码大小比性能更重要,则使用该选项。这是编译器的默认设置。  -Otime:指示编译程序执行优化,以增大映像大小为代价缩短执行时间。如果执行时间比代码大小更重 要,则使用该选项。例如,它编译: while (expression) body; 为: if (expression) { do body; while (expression); } 如果既不指定-Otime 也不指定-Ospace,则编译器默认使用-Ospace。可使用-Otime 编译代码中对时间要求 严格的部分,使用-Ospace 编译其余部分。但不能在同一编译程序调用中同时指定-Otime 和-Ospace。 14.1.4 AAPCS 选项 ARM 结构过程调用标准 AAPCS(Procedure Call Standard for the ARM Architecture)是 ARM 体系结构二进 制接口 ABI(Application Binary Interface for the ARM Architecture【BSABI】)标准的一部分。使用该标准 可以很方便的执行 C 和汇编语言的相互调用。 编译程序时,使用--apcs 选项可以指定所使用得 AAPCS 标准的版本。如果没有指定--apcs 或--cpu 选项, 则编译器使用下面默认编译选项。 --apcs /noswst/nointer/noropi/norwpi --cpu ARM7TDMI --fpu softvfp 有关 AAPCS 的详细信息,请参加 ARM 相关文档。 14.1.5 编译选项对代码生成影响示例 本节举例说明编译器的优化选项如何影响代码生成。 1.使用­O0 选项  专业始于专注 卓识源于远见       ‐  4  ‐            下面的例子显示了即使使用-O0 编译选项对代码进行编译时,有些冗余代码还是会被编译器自动清除。 int f(int *p) { return (*p = = *p); } 使用 armcc -c -O0 对源程序进行编译,生成的汇编代码如下所示。 f MOV r1, r0 MOV r0, #1 MOV pc, lr 通过上面的例子可以看到,编译出的最终代码中没有加载(Load)指针 P 的值,变量*p 被编译器优化掉了。 如果不想让编译器对变量*p 做优化,可以使用“volatile”对变量进行声明。下面的例子,显示了将变量声 明为“volatile”类型后,使用 armcc 编译(-O2 的优化级别)后的结果。 f LDR r1,[r0] LDR r0,[r0] CMP r1,r0 MOVNE r0,#0 MOVEQ r0,#1 MOV pc,lr 另外,编译的代码中的“MOV r1, r0”并没有实际意义,只是为了方便调试程序时设置断点使用。 2.冗余代码的清除  下面例子显示了一段急待优化的代码。 int dummy() { int a=10, b=20; int c; c=a+b; return 0; } 当使用 arm –c –O0 进行编译时,产生的汇编码如下所示。 dummy: 0000807C E3A0100A MOV r1,#0xa >>> REDUNDANT\#3 int a=10,b=20; 00008080 E3A02014 MOV r2,#0x14 >>> REDUNDANT\#5 c=a+b; 00008084 E0813002 ADD r3,r1,r2 >>> REDUNDANT\#6 return 0; 00008088 E3A00000 MOV r0,#0 >>> REDUNDANT\#7 } 0000808C E12FFF1E BX r14 从上面的汇编输出可以看到,编译器并没有对程序中的冗余变量做任何工作。但上面这段代码在编译时, 编译器会给出警告,警告信息如下所示。 专业始于专注 卓识源于远见       ‐  5  ‐            Warning : #550-D: variable "c" was set but never used Redundant.c line 4 int c; 但如果将编译器的优化级别提高,如使用 arm –c –O1 命令,则编译器输出的汇编代码如下所示。 dummy: 0000807C E3A00000 MOV r0,#0 >>> REDUNDANT\#7 } 00008080 E12FFF1E BX r14 从上面的例子看出,当优化级别提高到-O1 时,程序中的冗余变量就会被清除。 3.指令重排  当指定编译器对程序代码进行优化时,编译器会对程序中排列不合理的汇编指令序列进行重排(只有在-O1 及其以上的优化级别中才有),重排的目的是为了减少指令互锁(interload)。所谓互锁就是指如果一条指 令需要前一条指令的执行结果,而这时结果还没有出来,那么处理器就会等待。这被称为流水线冒险 (pipeline hazard),也被称为流水线互锁。 下面例子显示了对同一程序使用代码重排和不使用代码重排所产生的汇编码的区别。÷ 程序的源代码如下所示。 int f(int *p, int x) { return *p + x * 3; } 使用-O0 选项对代码进行编译(无代码重排),产生的结果如下所示。 ADD r1,r1,r1,LSL #1 LDR r0,[r0,#0] ADD r0,r0,r1 ; ARM9 上产生互锁 MOV pc,lr 使用-O1 选项对代码进行编译(存在代码重排),产生的结果如下所示。 ADD r1,r1,r1,LSL #1 ADD r0,r0,r1 MOV pc,lr 指令重排发生在寄存器定位和代码产生阶段。代码重排只对 ARM9 及其以后的处理器版本产生作用。当 使用代码重排时,代码的执行速度平均提供 4%。可以使用-zpno_optimize_ scheduling 编译选项关闭代码重排。 4.内嵌函数  通常情况下,如果不指定编译选项,编译器会将一些代码量小且调用次数少的函数内嵌进调用函数中。如 果某段子程序在其他模块中没有被调用,请使用 Static 关键字将其标识。 编译选项的--autoinline 和--no_autoinline 可以作为内嵌函数的使能开关。--no_autoinline 选项为-O0 和-O1 选项的默认选项,但如果指定-O2 或-O3 的优化选项,编译器将默认使用--autoinline 选项。 有关内嵌函数的详细信息,请参见本书内嵌函数一节。 下面的例子显示了同一段程序,使用内嵌功能和不使用内嵌功能编译出的不同结果。 要编译的源文件如下。 int bar(int a) { 专业始于专注 卓识源于远见       ‐  6  ‐            a=a+5; return a; } int foo(int i) { i=bar(i); i=i-2; i=bar(i); i++; return i; } 下面的汇编程序为不使用内嵌功能时编译出的结果。 bar ADD r0,r0,#5 MOV pc,lr foo STR lr,[sp,#-4]! BL bar SUB r0,r0,#2 BL bar ADD r0,r0,#1 LDR pc,[sp],#4 下面的汇编码是使用内嵌功能时编译出的结果。 foo ADD r0,r0,#5 SUB r0,r0,#2 ADD r0,r0,#5 ADD r0,r0,#1 MOV pc,lr 从上面的例子可以看出在使用内嵌功能时,函数间的相互调用减少了数据的压栈和出栈,节省了程序的执 行时间,但如果内嵌函数被调用多次会造成空间的浪费。 14.2 除法运算 因为 ARM 体系结构本身并不包含除法运算硬件,所以在 ARM 上实现除法是十分耗时的。ARM 指令集中 没有直接提供除法汇编指令,当代码中出现除法运算时,ARM 编译器会调用 C 库函数(有符合除法调用 _rt_sdiv,无符合除法调用_rt_udiv),来实现除法操作。根据除数和被除数的不同,32bit 的除法运算一般 要占有 20-140 个指令周期。除法运算占用的指令周期,由下面公式计算。 Time(除数 n / 被除数 d) = C0 + C1 * log2(除数 n / 被除数 d) = = C0 + C1 * (log2(除数) -log2(被除数)). 为了避免在程序中出现除法操作,编程时尽量使用其他运算来代替除法操作。如,使用 x>(z×y)来代替 (x/y)>z。 另外,在无法避免的除法运算中,尽量使用无符合除法代替有符号除法。这是因为在 ARM 库函数中,无 符合除法的运算速度要快于有符合除法。 下面章节将详细讨论如何在代码中提高除法运算的执行效率。 专业始于专注 卓识源于远见       ‐  7  ‐            14.2.1 合并除法和求余运算 ARM 的除法运算库函数能同时返回运算的商和余数。 在一些同时需要商和余数的情况下,编译器将调用一次除法运算函数同时存储运算的商和余数。 下面是一个编译器调用除法库,同时存储运算的商和余数的例子。 源程序如下。 int combined_div_mod (int a, int b) { return (a / b) + (a % b); } 下面是编译器编译出的汇编代码。 combined_div_mod STMDB sp!,{lr} MOV a3,a2 MOV a2,a1 MOV a1,a3 BL __rt_sdiv ADD a1,a1,a2 LDMIA sp!,{pc} 从上面的例子可以看出,调用一次除法运算,同时返回了商和余数。 14.2.2 使用 2 的整数次幂做除数 当 2 的整数次幂做除数时,编译器会自动将除法运算转换成移位运算。所以在编写程序算法时,尽量使用 2 的整数次幂做除数。 下面的例子显示了编译器对除法运算的自动优化。 源程序如下。 typedef unsigned int uint; uint div16u (uint a) { return a / 16; } int div16s (int a) { return a / 16; } 编译器的编译结果如下。 div16u MOV a1,a1,LSR #4 MOV pc,lr div16s CMP a1,#0 ADDLT a1,a1,#&f MOV a1,a1,ASR #4 MOV pc,lr 从上面的例子可以看出,无符号除法的运算速度快于有符号除法。 专业始于专注 卓识源于远见       ‐  8  ‐            14.2.3 求余运算 为了避免在程序中使用除法运算,可以将一些典型的求余运算进行转换。下面的例子提供一种转换方法。 uint counter1 (uint count) { return (++count % 60); } 转换成, uint counter2 (uint count) { if (++count >= 60) count = 0; return (count); } 下面是两个功能函数编译后的汇编代码。 counter1 STMDB sp!,{lr} ADD a2,a1,#1 MOV a1,#&3c BL __rt_udiv MOV a1,a2 LDMIA sp!,{pc} counter2 ADD a1,a1,#1 CMP a1,#&3c MOVCS a1,#0 MOV pc,lr 上面的例子清晰的显示了使用 if 语句代替除法运算后,代码的执行效率有很大提高。 14.2.4 除数是常数的除法 因为除法和模运算执行起来比较慢,所以应该尽可能地避免使用。但是除数是常数的除法运算和用同一个 除数的重复除法,执行效率会比较高。在 ARM 的除法库中,存在除数为 10 的除法运算库,其中包括有符 号除法和无符号除法。如果除数是 10 以外的其他常数,用户可以编写自己的功能函数。ARM 的开发工具 集中,提供了关于除数是常数的示例程序和算法分析,以供用户编写自己的代码时参考。 14.3 条件执行 ARM 指令都是可以条件执行的。在代码中使用条件执行指令可以减小代码密度并提高程序执行效率。典 型的条件执行语句用在比较指令之后,形成程序的分支跳转结构。下面的例子显示了条件执行指令的典型 用法。 CMP x, #0 MOVGE y, #1 MOVLT y, #0 专业始于专注 卓识源于远见       ‐  9  ‐            但当代码中连续的条件执行指令超过 4 条时,就会影响程序的执行速度。所以编译器在编译程序时,限制 条件指令连续出现的次数。 ARM 编译器常把 C 语言中的 if…else 结构编译成条件执行指令,但子程序调用一般是不能条件执行的。所 以在编程时尽可以地使用简单的 if…else 结构完成程序的分支操作,而避免使用过多的子程序调用。 下面的例子显示了编译器如何利用 ARM 指令的条件执行。 int g(int a, int b, int c, int d) { if (a > 0 && b > 0 && c < 0 && d < 0) /* 程序分组条件 */ return a + b + c + d; return -1; } g CMP a1,#0 CMPGT a2,#0 BLE |L000024.J4.g| CMP a3,#0 CMPLT a4,#0 ADDLT a1,a1,a2 ADDLT a1,a1,a3 ADDLT a1,a1,a4 MOVLT pc,lr |L000024.J4.g| MVN a1,#0 MOV pc,lr 14.4 布尔表达式 14.4.1 范围检测 通常,布尔表达式被用来检测某个数值是否在特定的范围内。例如,在图形窗口处理程序中,常使用布尔 表达式判断屏幕中一个点是否在当前活动窗口范围内。 下面的程序使用结构体定义点坐标并计算坐标的当前位置。 bool PointInRect1(Point p, Rectangle *r) { return (p.x >= r->xmin && p.x < r->xmax && p.y >= r->ymin && p.y < r->ymax); } 上面的功能函数,被编译为下面的指令序列。 PointInRect1 LDR a4,[a3,#0] CMP a1,a4 BLT |L000034.J5.PointInRect1| LDR a4,[a3,#4] CMP a4,a1 BLE |L000034.J5.PointInRect1| LDR a1,[a3,#8] CMP a2,a1 BLT |L000034.J5.PointInRect1| 专业始于专注 卓识源于远见       ‐  10  ‐            LDR a1,[a3,#&c]! CMP a2,a1 MOVLT a1,#1 MOVLT pc,lr |L000034.J5.PointInRect1| MOV a1,#0 MOV pc,lr 但上面的代码并不是最精简的。编译器对(x >= min && x < max)形式的布尔表达式的处理过程比较复杂。 它将以(unsigned)(x-min) < (max-min)形式实现布尔操作。所有对于上面范围判断的代码,建议将函数写成 如下形式。 bool PointInRect2(Point p, Rectangle *r) { return ((unsigned) (p.x - r->xmin) < r->xmax && (unsigned) (p.y - r->ymin) < r->ymax); } 这样编译出的汇编指令序列如下所示。 PointInRect2 LDR a4,[a3,#0] SUB a1,a1,a4 LDR a4,[a3,#4] CMP a1,a4 LDRCC a1,[a3,#8] SUBCC a1,a2,a1 LDRCC a2,[a3,#&c]! CMPCC a1,a2 MOVCS a1,#0 MOVCC a1,#1 MOV pc,lr 14.4.2 和零的比较操作 比较指令(CMP)将设置程序状态字的条件标志位。另外,基本的算术指令也可以设置条件标志位,如使 用指令 MOVS、ADDS 等。如果程序中的算术指令的执行目的是为了将计算结果和零比较,那么就可以直 接使用带标志扩展的基本算术指令。如下面的两条语句: ADD R0, R0, R1 CMP R0, #0 可以合并为一条带符号扩展的加法指令: ADDS R0, R0, R1 事实上,C 语言中的和零相关的关系操作都可以利用状态标志寄存器的 N 位和 Z 位。如:x < 0, x >= 0, x = 0, x != 0,和无符号操作 x = 0, x != 0 (or x > 0)。 对于每一条 C 语言中的关系操作,汇编器都将产生一条比较指令。如果关系操作和零相关,则可以将产生 的比较指令移除。 下面是 C 语言中的关系操作被编译的例子。 C 源文件如下所示。 int g(int x, int y) { 专业始于专注 卓识源于远见       ‐  11  ‐            if ((x + y) < 0) return 1; else return 0; } 编译后的结果如下。 g ADDS a1,a1,a2 MOVPL a1,#0 MOVMI a1,#1 MOV pc,lr 所以,在使用 C 语言编程时,关系操作最好转换成和零相关的,这样既可以减少代码密度,也可以提高程 序的执行效率。 C 语言中,没有和程序状态寄存器的 C 位和 V 位直接相关的指令,所以要在程序中检测这些标志,只能使 用内嵌汇编。但 C 编译器支持无符号溢出操作,下面的例子显示了在有溢出操作时,编译器对程序的处理。 C 源代码如下所示。 int sum(int x, int y) { int res; res = x + y; if ((unsigned) res < (unsigned) x) /* 判断进位标志是否进位 */ res++; return res; } 编译的汇编文件如下所示。 sum ADDS a2,a1,a2 ADC a2,a2,#0 MOV a1,a2 MOV pc,lr 14.5 C 循环结构 循环体是程序设计与优化的重点考虑对象。本节将着重讲解在 ARM 上处理 for 和 while 循环最有效的方法。 14.5.1 循环中止 首先来看下面的例子,两个不同的循环退出条件,产生的不同汇编代码。 C 源程序如下所示。 int fact1 (int n) { int i, fact = 1; for (i = 1; i <= n; i++) fact *= i; return (fact); 专业始于专注 卓识源于远见       ‐  12  ‐            } int fact2 (int n) { int i, fact = 1; for (i = n; i != 0; i--) fact *= i; return (fact); } 产生的汇编代码如下所示。 fact1 MOV a3,#1 MOV a2,#1 CMP a1,#1 BLT |L000020.J5.fact1| |L000010.J4.fact1| MUL a3,a2,a3 ADD a2,a2,#1 CMP a2,a1 BLE |L000010.J4.fact1| |L000020.J5.fact1| MOV a1,a3 MOV pc,lr fact2 MOVS a2,a1 MOV a1,#1 MOVEQ pc,lr |L000034.J4.fact2| MUL a1,a2,a1 SUBS a2,a2,#1 BNE |L000034.J4.fact2| MOV pc,lr 从产生的汇编代码中,可以看出两个函数虽然实现的功能相同,但产生的代码效率却不尽相同。这里的关 键是,循环的中止条件应为计数减到零(count down to zero),而不是计数增加到某个值。由于减计数结果 已存储在条件标志里,与零比较的指令就可以省略。同时也可以少用一个寄存器来存储循环中止值。 注意 上面的例子使用了-O2 –Otime 的编译选项,如果使用-Ospace 选项,编译结果会有不同。 对循环计数值 i 来说,如果 i 是无符号的,则循环继续的条件既可以是 i!=0,也可以是 i > 0。由于 i 不可 能是负数,所以这两个条件是等价的。而对一个有符号的循环计数值来说,最好不要用条件 i > 0 作为循环 继续执行的条件。如果使用 i > 0 作为循环继续执行的条件,编译器将生成下面的代码。 SUB a2,a2,#1 CMP r1,#0 BGT |L000034.J4.fact2| 这时,编译器多增加了一条 CMP 指令,主要是为了防止有符号数 i= −0x8000000。总之,无论对于有符号 还是无符号的循环计数值,都应该使用 i != 0 作为循环的结束条件。对于有符号数 i,这比使用 i > 0 少了 一条指令。 专业始于专注 卓识源于远见       ‐  13  ‐            14.5.2 循环展开 在 14.5.1 节中可以发现,每次循环需要在循环体外加两条指令:一条减法指令来减少循环计数值和一条条 件分支指令。通常这些指令称为循环开销(Loop Overhead)。在 ARM7 或 ARM9 处理器上,加法指令需要 1 个周期,条件分支指令需要 3 个周期,这样每个循环就需要 4 个周期的开销。 可以通过展开循环体(Loop Unrolling),即重复循环主体多次,同时按同样的比例减少循环次数来降低循 环开销。 下面的例子通过将循环体展开 4 次,来达到减少循环开销的目的。 int countbit1(uint n) { int bits = 0; while (n != 0) { if (n & 1) bits++; n >>= 1; } return bits; } 将循环主体展开。 int countbit2(uint n) { int bits = 0; while (n != 0) { if (n & 1) bits++; if (n & 2) bits++; if (n & 4) bits++; if (n & 8) bits++; n >>= 4; } return bits; } 这里减少了 4N 的循环开销(N=4,即循环体执行的次数)。如果循环体中存在耗时的 Store/Load 指令,则 代码执行效率的提高将更明显。 ARM 编译器不会自动将循环体展开,只有用户自己判断何时将循环体展开,到底应该展开多少次,如果 循环的次数不是循环展开的倍数该怎么办?下面就将详细讨论,用户编写自己的循环展开程序时,需要注 意的问题。 ① 只有当循环展开对提高应用程序的整体性能非常重要时,才进行循环展开;否则反而会增加代码尺寸。 ② 应设法使循环的次数是循环展开的倍数。如果难以实现,那么就要增加额外的代码来处理数组的剩余 元素。这将增加少许代码量,但可以保持较好的性能。 14.6 Switch 语句 编译器通常将 C 语言中的 Switch 语句编译一个查找表(Table Lookup)以便跳转到合适的入口处。 下面的例子显示了编译器如何处理程序中的 Switch 语言的。 C 源程序如下。 专业始于专注 卓识源于远见       ‐  14  ‐            char * ConditionStr1(int condition) { switch(condition) { case 0: return "EQ"; case 1: return "NE"; case 2: return "CS"; case 3: return "CC"; case 4: return "MI"; case 5: return "PL"; case 6: return "VS"; case 7: return "VC"; case 8: return "HI"; case 9: return "LS"; case 10: return "GE"; case 11: return "LT"; case 12: return "GT"; case 13: return "LE"; case 14: return ""; default: return 0; } } 编译后的结果如下。 ConditionStr1: 0000807C E1A01000 MOV r1,r0 >>> SWITCH\#3 switch(condition) 00008080 E351000E CMP r1,#0xe 00008084 908FF101 ADDLS pc,pc,r1,LSL #2 00008088 EA00003B B 0x817c 0000808C EA00000D B 0x80c8 00008090 EA00000F B 0x80d4 00008094 EA000011 B 0x80e0 00008098 EA000013 B 0x80ec 0000809C EA000015 B 0x80f8 000080A0 EA000017 B 0x8104 000080A4 EA000019 B 0x8110 000080A8 EA00001B B 0x811c 000080AC EA00001D B 0x8128 000080B0 EA00001F B 0x8134 000080B4 EA000021 B 0x8140 000080B8 EA000023 B 0x814c 000080BC EA000025 B 0x8158 000080C0 EA000027 B 0x8164 000080C4 EA000029 B 0x8170 对于 ARM 代码,查找表的入口为 4 字节;Thumb 代码的查找表入口为 1 或 2 个字节(当 Case 情况小于 32 时,使用入口为 1 字节的查找表)。所以当使用 Switch 语句时,应尽量较少 Case 分支。 另外,为了提高系统性能,也可以手工编写代码,形成程序跳转来避免使用 Switch 语句。 下面的例子显示对上面 Switch 分支语句的改写。 char * ConditionStr2(int condition) 专业始于专注 卓识源于远见       ‐  15  ‐            { if ((unsigned) condition >= 15) return 0; return "EQ\0NE\0CS\0CC\0MI\0PL\0VS\0VC\0HI\0LS\0GE\0LT\0GT\0LE\0\0" + 3 * condition; } 编译后的代码如下所示。 ConditionStr2: 00008188 E1A01000 MOV r1,r0 >>> SWITCH\#26 if ((unsigned) condition >= 15) return 0; 0000818C E351000F CMP r1,#0xf 00008190 3A000001 BCC 0x819c >>> SWITCH\#26 if ((unsigned) condition >= 15) return 0; 00008194 E3A00000 MOV r0,#0 >>> SWITCH\#30 } 00008198 E12FFF1E BX r14 >>> SWITCH\#26 if ((unsigned) condition >= 15) return 0; >>> SWITCH\#27 return 0000819C E28F005C ADR r0,{pc}+0x64 ; #0x8200 000081A0 E3A02003 MOV r2,#3 000081A4 E0200291 MLA r0,r1,r2,r0 000081A8 EAFFFFFA B 0x8198 >>> SWITCH\#33 { 从两段汇编代码的分析可以看出,使用跳转表需要 240bytes,而第二种做法只用了 72bytes。 14.7 寄存器分配 编译器一项很重要的优化功能就是对寄存器的分配。与分配在寄存器中的变量相比,分配到内存的变量访 问要慢得多。所以如何将尽可能多的变量分配到寄存器,是编程时应该重点考虑的问题。 注意 当使用-g 或-dubug 选项编译程序时,为了确保调试信息的完整性,寄存器分配的效率比不使用 -g 或-dubug 选项低很多。 14.7.1 变量寄存器分配 一般情况下,编译器会对 C 函数中的每一个局部变量分配一个寄存器。如果多个局部变量不会交迭使用, 那么编译器会对它们分配同一个寄存器。当局部变量多于可用的寄存器时,编译器会把多余的变量存储到 堆栈。这些被写入堆栈需要访问存储器的变量被称为溢出(Spilled)变量。 为了提高程序的执行效率:  使溢出变量的数量最少;  确保最重要的和经常用到的变量被分配在寄存器中。 可以被分配到寄存器的变量包括:  程序中的局部变量;  调用子程序时传递的参数;  与地址无关变量。 另外,在一些特定条件下,结构体中的域也可以被分配到寄存器中。 表 14.1 显示了当 C 编译器采用 ARM-Thumb 过程调用标准时,内部寄存器的编号、名字和分配方法。 专业始于专注 卓识源于远见       ‐  16  ‐            表 14.1 C 编译器寄存器用法 寄存器编号 可选寄存器名 特殊寄存器名 寄存器用法 r0 a1 函数调用时的参数寄存器,用来存放前 4 个函数参数和 存放返回值。在函数内如果将这些寄存器用作其他用 途,将破坏其值。 r1 a2 r2 a3 r3 a4 r4 v1 通用变量寄存器 r5 v2 r6 v3 r7 v4 r8 v5 r9 v6 或 SB 或 TR 平台寄存器,不同的平台对该寄存器的定义不同 r10 v7 通用变量寄存器。在使用堆栈边界检测的情况下,r10 保存堆栈边界的地址 r11 v8 通用变量寄存器。 r12 IP 临时过渡寄存器,函数调用时会破坏其中的值 r13 SP 堆栈指针 r14 LR 链接寄存器 r15 PC 程序计数器 从表 14.1 可以看出,编译器可以分配 14 个变量到寄存器而不会发生溢出。但有些寄存器编译器会有特殊 用途(如 r12),所以在编写程序时应尽量限制变量的数目,使函数内部最多使用 12 个寄存器。 注意 在 C 语言中,可以使用关键词 register 给指定变量分配专用寄存器。但不同的编译器对该关键 词的处理可能不同,使用时要查阅相关手册。 14.7.2 指针别名 C 语言中的指针变量可以给编程带来很大的方便。但使用指针变量时要特别小心,它很可能使程序的执行 效率下降。在一个函数中,编译器通常不知道是否有 2 个或 2 个以上的指针指向同一个地址对象。所以编 译器认为,对任何一个指针的写入都将会影响从任何其他指针的读出,但这样会明显降低代码执行的效率。 这就是著名的“寄存器别名(Pointer Aliasing)”问题。 注意 一些编译器提供了“忽略指针别名”选项,但这可能给程序带来潜在的 bug。ARM 编译器是遵 循 ANSI/ISO 标准的编译器,不提供该选项。 1.局部变量指针别名问题  通常情况下,编译器会试图对 C 函数中的每一个局部变量分配一个寄存器。但当局部变量是指向内存地址 的指针时,情况有所不同。先来看一个简单的例子。 void add(int * i) { int total1=0,total2=0; total1+= *i; 专业始于专注 卓识源于远见       ‐  17  ‐            total2+= *i; } 编译后生成: add: 0000807C E3A01000 MOV r1,#0 >>> POINTALIAS\#3 int total1=0,total2=0; 00008080 E3A02000 MOV r2,#0 >>> POINTALIAS\#5 total1+= *i; 00008084 E5903000 LDR r3,[r0,#0] 00008088 E0831001 ADD r1,r3,r1 >>> POINTALIAS\#6 total2+= *i; 0000808C E5903000 LDR r3,[r0,#0] 00008090 E0832002 ADD r2,r3,r2 >>> POINTALIAS\#8 } 00008094 E12FFF1E BX r14 >>> POINTALIAS\#11 { 注意程序中 i 的值被装载了两次。因为编译器不能确定指针*i 是否有别名存在,这就使得编译器不得不增 加一条额外的 Load 指令。 另一个问题,当在函数中要获得局部变量地址时,这个变量就被一个指针所对应,就可能与其他指针产生 别名。为了防止别名发生,在每次对变量操作时,编译器就会从堆栈中重新读入数据。考虑下面的例子程 序,分析其产生的编译结果。 void f(int *a); int g(int a); int test1(int i) { f(&i); /* now use ’i’ extensively */ i += g(i); i += g(i); return i; } 编译结果如下所示。 test1 STMDB sp!,{a1,lr} MOV a1,sp BL f LDR a1,[sp,#0] BL g LDR a2,[sp,#0] ADD a1,a1,a2 STR a1,[sp,#0] BL g LDR a2,[sp,#0] ADD a1,a1,a2 ADD sp,sp,#4 LDMIA sp!,{pc} 专业始于专注 卓识源于远见       ‐  18  ‐            从上面代码的编译结果可以看出,对每一次 i 操作,编译器都将会从堆栈中读出其值。这是因为,一旦在 函数中出现对 i 的取值操作,编译器就会担心别名问题。为了避免这种情况,尽量不要在程序中使用局部 变量地址。如果必须这么做,那么可以在使用之前先把局部变量的值复制到另外一个局部变量中。下面的 程序是对 test1 函数的优化。 int test2(int i) { int dummy = i; f(&dummy); i = dummy; /* now use ’i’ extensively */ i += g(i); i += g(i); return i; } 编译后的结果如下。 test2 STMDB sp!,{v1,lr} STR a1,[sp,#-4]! MOV a1,sp BL f LDR v1,[sp,#0] MOV a1,v1 BL g ADD v1,a1,v1 MOV a1,v1 BL g ADD a1,a1,v1 ADD sp,sp,#4 LDMIA sp!,{v1,pc} 从编译结果可以看出,修改后的代码只使用了 2 次内存访问,而 test1 为 4 次内存访问。 总上所述,为了在程序中避免指针别名,应该做到:  避免使用局部变量地址;  如果程序中出现多次对同一指针的访问,应先将其值取出并保存到临时变量中。 2.全局变量  通常情况下,编译器不会为全局变量分配寄存器。这样在程序中使用全局变量,很可能带来内存访问上的 开销。所有尽量避免在循环体内使用全局变量,以减少对内存的访问次数。 如果在一段程序体内大量使用了同一个全局变量,建议在使用前先将其拷贝到一个局部的临时变量中,当 完成对它的全部操作后,再将其写回到内存。 比较下面两个完成同样功能的函数,分析全局变量的操作对程序性能的影响。 int f(void); int g(void); int errs; void test1(void) { 专业始于专注 卓识源于远见       ‐  19  ‐            errs += f(); errs += g(); } void test2(void) { int localerrs = errs; localerrs += f(); localerrs += g(); errs = localerrs; } 编译结果如下。 test1 STMDB sp!,{v1,lr} BL f LDR v1,[pc, #L00002c-.-8] LDR a2,[v1,#0] ADD a1,a1,a2 STR a1,[v1,#0] BL g LDR a2,[v1,#0] ADD a1,a1,a2 STR a1,[v1,#0] LDMIA sp!,{v1,pc} L00002c DCD |x$dataseg| test2 STMDB sp!,{v1,v2,lr} LDR v1,[pc, #L00002c-.-8] LDR v2,[v1,#0] BL f ADD v2,a1,v2 BL g ADD a1,a1,v2 STR a1,[v1,#0] LDMIA sp!,{v1,v2,pc} 从编译的结果中可以看出,test1 中每次对全局变量 errs 的访问都会使用耗时的 Load/Store 指令;而 test2 只使用了一次内存访问指令。这对提高程序的整体性能有很大帮助。 3.指针链  指针链(Pointer Chains)常被用来访问结构体内部变量。下面的例子显示了一个典型的指针链的使用。 typedef struct { int x, y, z; } Point3; typedef struct { Point3 *pos, *direction; } Object; void InitPos1(Object *p) { p->pos->x = 0; p->pos->y = 0; p->pos->z = 0; 专业始于专注 卓识源于远见       ‐  20  ‐            } 上面的代码每次使用“p->pos”时都会对变量重新取值。为了提高代码效率,将程序改写如下。 void InitPos2(Object *p) { Point3 *pos = p->pos; pos->x = 0; pos->y = 0; pos->z = 0; } 经过改写的代码,减少了内存访问次数,提高程序的执行效率,另外也可以在object结构体中增加一个point3 域,专门作为指向 p->pos 的指针。 14.8 变量类型 ARM C 编译器支持基本的数据类型:char、short、int、long long、float 和 double。表 14.2 说明了 armcc 对 C 语言所使用的数据类型的映射。 表 14.2 C 编译器数据类型映射 C 数据类型 表示的意义 char 无符号 8 位字节数据 short 有符号 16 位半字数据 int 有符号 32 位字数据 long 有符号 32 位字数据 long long 有符号 64 位双字数据 ARM 指令集中,无论是数据处理指令还是数据加载/存储指令,处理的数据类型不同,指令的执行效率是 不一样的。本章将详细讨论,如何在程序中为变量分配合理的数据类型,来提高代码的执行效率。 14.8.1 局部变量 ARM 属于 RISC 的体系结构,所有大多数的数据处理都是在 32 位的寄存器中进行的。基于这个原因,局 部变量应尽可能使用 32 位数据类型 int 或 long。 注意 一些情况下不得不使用 char 或 short 类型,例如要使用 char 或 short 类型的数据溢出指令时归零 特性时,如模运算 255+1=0,就要使用 char 类型。 为了说明局部变量类型的影响,先来看一个简单的例子。 char charinc (char a) { return a + 1; } 编译出的结果如下。 charinc ADD a1,a1,#1 AND a1,a1,#&ff MOV pc,lr 专业始于专注 卓识源于远见       ‐  21  ‐            再把上面的程序段中变量 a 声明位 int 型,代码如下。 int wordinc (int a) { return a + 1; } 比较一下编译器输出结果。 wordinc ADD a1,a1,#1 MOV pc,lr 分析上面的两段代码不难发现,当把变量声明为 char 型时,编译器增加了额外的 ADD 指令来保证其范围 在 0~255 之间。 14.8.2 有符号数和无符号数 上一节讨论了对于局部变量和函数参数,使用 int 型比使用 char 或 short 型要好。本节将对程序中的有符 号整数(signed int)和无符号整数(unsigned int)的执行效率进行分析比较。 首先来看上一节的例子,如果将变量指定为有符号的半字类型(编译器默认 short 型为有符号类型),程序 的源代码如下。 short shortinc (short a) { return a + 1; } 编译后的结果如下。 shortinc ADD a1,a1,#1 MOV a1,a1,LSL #16 MOV a1,a1,ASR #16 MOV pc,lr 分析发现,该结果比使用 int 型的变量多增加了两条指令(LSL 和 ASR)。编译器先将变量左移 16 位,然 后右移 16 位,以实现一个 16 位符号扩展。右移是符号位扩展移位,它复制了符号位来填充高 16 位。 通常情况下,如果程序中只有加法、减法和乘法,那么有符号和无符号数的执行效率相差不大。但是,如 果有了除法,情况就不一样了。详细内容可参加除法运算优化一节。 14.8.3 全局变量 1.边界对齐  对于 RISC 体系结构的处理器来说,访问边界对齐的数据要比访问非对齐的数据更高效。表 14.3 显示了 ARM 结构下各数据类型所占的字节数。 表 14.3 各数据类型所占字节数 C 数据类型 所占字节数 char,singed char,unsigned char 1 short,unsigned short 2 专业始于专注 卓识源于远见       ‐  22  ‐            int,unsigned int,long,unsigned long 4 float 4 double 4 long long 4 变量定义虽然很简单,但是也有很多值得注意的地方。先看下面的例子。 定义 1: char a; short b; char c; int d; 定义 2: char a; char c; short b; int d; 这里定义的 4 个变量形式都一样,只是次序不同,却导致了在最终映像中不同的数据布局,如同 14.1 所示, 其中 pad 为无意义的填充数据。 图 14.1 变量在数据区里的布局 从图中可以看出,第二种方式节约了更多的存储器空间。 由此可见,在变量声明的时候需要考虑怎样最佳的控制存储器布局。当然,编译器在一定程度上能够优化 这类问题,但最好的方法还是在编译的时候把所有相同类型的变量放在一起定义。 2.访问外部变量  首先来看一个例子。下面的例子定义了一些全局变量,在 main( )中为这些变量赋值并将其打印输出。 /************ * access.c * ************/ #include char tx; char rx; char byte; char c; unsigned state; unsigned flags; int main () { tx = 1; rx = 2; byte = 3; 专业始于专注 卓识源于远见       ‐  23  ‐            c = 4; state = 5; flags = 6; printf("%u %u %u %u %u %u\n", tx, rx, byte, c, state, flags); return 0; } 使用 armcc 编译,生成的代码大小如下。 C$$code 132 C$$data 12 如果将全局变量声明为 extern,变量的定义在其他文件中,那么生成的代码量将有所增加。 将全局变量声明为 extern,生成的代码大小如下。 C$$code 168 C$$data 12 这是因为当将变量声明为 extern 后,每次访问变量编译器都将从内存重新加载,而不是使用内存偏移,直 接访问。 下图显示编译器对声明为 extern 变量的访问。 解决的办法是将要从外部引用的 extern 变量定义在一个结构体中。在程序中通过结构体访问外部变量。具 体用法如下例所示。 /************* * globals.h * *************/ /* DECLARATIONS of globals - included in all sources */ #ifdef __arm struct globs { char tx; char rx; extern int a; extern int b; void foo (int x, int y) { a=x; b=y; } LDR r2, [pc, #12] STR r0, [r2, #0] LDR r3, [pc, #8] STR r1, [r3, #0] MOV pc, 1r DCD "address of a" DCD "address of b" int a; int b; void foo (int x, int y) { a=x; b=y; } LDR r2, [pc, #8] STR r0, [r2, #0] STR r1, [r2, #4] MOV pc, 1r DCD "base address of a and b" 图 14.2 对 extern 变量的访问 char byte; char c; unsigned state; unsigned flags; 专业始于专注 卓识源于远见       ‐  24  ‐            }; extern struct globs g; #define tx g.tx #define rx g.rx #define byte g.byte #define c g.c #define state g.state #define flags g.flags #else extern char tx; extern char rx; extern char byte; extern char c; extern unsigned state; extern unsigned flags; #endif /************* * globals.c * *************/ /* DEFINITIONS of globals - single source file */ #ifdef __arm # include "globals.h" struct globs g; #else char tx; char rx; char byte; char c; unsigned state; unsigned flags; #endif /************ * access.c * ************/ #include #include "globals.h" int main () {tx = 1; rx = 2; byte = 3; c = 4; state = 5; flags = 6; printf("%u %u %u %u %u %u\n", tx, rx, byte, c, state, flags); return 0; } 将变量定义在结构体内有以下几点好处。  全局变量使用更小的内存空间。(没有使用结构体占有 24 字节,而使用结构体之后只占有 12 字节)  全局变量被放置在 ZI 段而不是 RW 段,这样就减少了 ROM 映像文件的大小。 专业始于专注 卓识源于远见       ‐  25  ‐            14.9 函数调用 函数设计的基本原则是使其函数体尽量的小。这样编译器可以对函数做更多的优化。 14.9.1 减少函数调用开销 ARM 上的函数调用开销比非 RISC 体系结构上的调用开销小:  调用返回指令“BL”或“MOV pc,lr”一般只需要 6 个指令周期(ARM7 上)。  在函数的入口和出口使用多寄存器加载/存储指令 LDM 和 STM(Thumb 指令使用 PUSH 和 POP)提高 函数体的执行效率。 ARM 体系结构过程调用标准 AAPCS 定义了如何通过寄存器传递参数和返回值。函数中的前 4 个整型参数 是通过 ARM 的前 4 个寄存器 r0、r1、r2 和 r3 来传递的。传递参数可以是与整型兼容的数据类型,如字符 类型 char、半字类型 short 等。 注意 如果是双字类型,如 long long 型,只能通过寄存器传递两个参数。 不能通过寄存器传递的参数,通过函数堆栈来传递。这样不论是函数的调用者还是被调用者都必须通过访 问堆栈来访问参数,使程序的执行效率下降。 下面的例子显示了函数调用是传递 4 个参数和多于 4 个参数的区别。 传递 4 个参数的函数调用源文件如下。 int func1(int a, int b, int c, int d) { return a+b+c+d; } int caller1(void) { return func1(1,2,3,4); } 编译的结果如下。 func1 ADD r0,r0,r1 ADD r0,r0,r2 ADD r0,r0,r3 MOV pc,lr caller1 MOV r3,#4 MOV r2,#3 MOV r1,#2 MOV r0,#1 B func1 如果程序需要传递 6 个参数,变为如下形式。 int func2(int a, int b, int c, int d,int e,int f) { return a+b+c+d+e+f; 专业始于专注 卓识源于远见       ‐  26  ‐            } int caller2(void) { return func1(1,2,3,4,5,6); } 则编译后的汇编文件如下。 func2 STR lr, [sp,#-4]! ADD r0,r0,r1 ADD r0,r0,r2 ADD r0,r0,r3 LDMIB sp,{r12,r14} ADD r0,r0,r12 ADD r0,r0,r14 LDR pc,{sp},#4 caller2 STMFD sp!,{r2,r3,lr} MOV r3,#6 MOV r2,#5 STMIA sp,{r2,r3} MOV r3,#4 MOV r2,#3 MOV r1,#2 MOV r0,#1 BL func2 LDMFD sp!,{r2,r3,pc} 综上所述,为了在程序中高效的调用函数,最好遵循以下规则。  尽量限制函数的参数,不要超过 4 个,这样函数调用的效率会更高。  当传递的参数超过 4 个时,要将多个相关参数组织在一个结构体中,用传递结构体指针来代替多个参 数。  避免将传递的参数定义为 long long 型,因为传递一个 long long 型的数据将会占用两个 32 位寄存器。  函数中存在浮点运算时,避免使用 double 型参数。 14.9.2 使用__value_in_regs 返回结构体 编译选项__value_in_regs 指示编译器在整数寄存器中返回 4 个整数字的结构或者在浮点寄存器中返回 4 个 浮点型或双精度型值,而不使用存储器。 下面的例子显示了__value_in_regs 选项的用法。 typedef struct { int hi; uint lo; } int64; // 注意该结构中,高位为有符号整数,低位为无符号整数 __value_in_regs int64 add64(int64 x, int64 y) { int64 res; res.lo = x.lo + y.lo; res.hi = x.hi + y.hi; if (res.lo < y.lo) res.hi++; // carry from low word return res; } 专业始于专注 卓识源于远见       ‐  27  ‐            void test(void) { int64 a, b, c, sum; a.hi = 0x00000000; a.lo = 0xF0000000; b.hi = 0x00000001; b.lo = 0x10000001; sum = add64(a, b); c.hi = 0x00000002; c.lo = 0xFFFFFFFF; sum = add64(sum, c); } 编译后的结果如下所示。 add64 ADDS a2,a2,a4 ADC a1,a3,a1 MOV pc,lr test STMDB sp!,{lr} MOV a1,#0 MOV a2,#&f0000000 MOV a3,#1 MOV a4,#&10000001 BL add64 MOV a3,#2 MVN a4,#0 LDMIA sp!,{lr} B add64 当使用__value_in_regs 定义结构体时,编译的代码大小为 52 字节,如果不使用__value_in_regs 选项,则编 译出的结果为 160 字节(本书中没有列出未使用__value_in_regs 时的编译结果,读者有兴趣可以自己上机 试验)。 14.9.3 叶子函数 所谓叶子函数(leaf function)就是在其函数体内不存在对其他函数调用,它也常被称为终级函数。因为叶 子函数不需要调用其他函数,所有没有保存/恢复寄存器的操作,因此执行效率比一般函数要高。 当函数中必须对一些寄存器进行保存时,可以使用高效率的多寄存器存储指令 STM,对需要保存的寄存器 内存一次性存储。 正是由于叶子函数执行的高效性,所以在编程时,尽量将子程序编写为叶子函数,这样即使程序中多次调 用也不会影响代码性能。 为了高效的调用函数,可以遵循下面函数调用原则。  避免在被频繁调用的函数中调用其他函数,以保证被频繁调用的函数被编译器编译为叶子函数。  把比较小的被调用函数和调用函数放在同一个源文件中,并且要先定义后调用,编译器就可以优化函 数调用或内联较小的函数。  对性能影响较大的重要函数可使用关键字_inline 进行内联。 14.9.4 嵌套优化 专业始于专注 卓识源于远见       ‐  28  ‐            注意 嵌套优化(Tail-Call optimization)只适用于 armcc。编译时如果使用-g 或-debug 选项,编译器 自动关闭该功能。 一个函数如果在其结束时调用了另一个函数,则编译器使用 B 指令调转到被调用函数,而非 BL 指令。这 样就避免了一级不必要的函数返回。图 14.3 显示了嵌套优化的调用过程。 int main() { int x = f(); : } int f() { int y = g(); return y; } int g() { return 10; } BL f : STR lr,[sp,#-4]! BL g MOV r1,r0 MOV r0,r1 LDR pc,[sp],#4 MOV r0, #10 MOV pc, lr BL f : BL g: MOV r0, #10 MOV pc, lr 图 14.3 嵌套优化函数调用过程 当编译时使用-O1 或-O2 选项时,编译器都执行这种嵌套优化。需要注意的是,当函数中引用了局部变量 地址,由于指针别名问题的影响,即使函数在返回时调用了其他函数,编译器也不会使用嵌套优化。 下面通过一个例子来分析嵌套优化是如何提高代码执行效率的。 extern int func2(int); int func1 (int a, int b) { if (a > b) return (func2(a - b)); else return (func2(b - a)); } 编译后的代码如下所示。 func1 CMP a1,a2 SUBLE a1,a2,a1 SUBGT a1,a1,a2 B func2 首先,func1 中使用 B 指令代替 BL 指令,不用担心 lr 寄存器被破坏,减少了对寄存器压栈保护操作。另 外,程序直接从 func2 返回到调用 func1 的函数,减少一次函数返回。如果说正常的指令调用过程为: BL + BL+ MOV pc,lr + MOV pc,lr 那么经过嵌套优化的函数调用过程就可以表示为: 专业始于专注 卓识源于远见       ‐  29  ‐            BL + BL+ MOV pc,lr 这样,总的开销将减少 25%。 14.9.5 单纯子函数 所谓单纯子函数(Pure Functions)是指那些函数返回值只和调用参数有关。换句话说,就是如果调用函数 的参数相同,那么函数的返回结果也相同。如果程序中存在这样的函数,可以在函数定义时使用_pure 进 行声明,这样在程序编译时编译器会根据函数的调用情况对其进行优化。 下面的例子显示了当函数用_pure 声明时,编译器对其所做的优化。 程序源码文件如下。 int square(int x) { return x * x; } int f(int n) { return square(n) + square(n) } 编译后的结果如下。 square MOV a2,a1 MUL a1,a2,a2 MOV pc,lr f STMDB sp!,{lr} MOV a3,a1 BL square MOV a4,a1 MOV a1,a3 BL square ADD a1,a4,a1 LDMIA sp!,{pc} 上面的程序中,square 函数为“单纯子函数”,当使用_pure 声明该函数时编译器在调用该函数时,将对程 序进行优化。 声明的方法和编译后的结果如下所示。 __pure int square(int x) { return x * x; } f STMDB sp!,{lr} BL square MOV a1,a1,LSL #1 LDMIA sp!,{pc} 从编译后的代码中可以看到,用_pure 声明的函数在 f 函数中只调用了一次。 专业始于专注 卓识源于远见       ‐  30  ‐            虽然“单纯子函数”可以提高代码执行效率,但同时也会带来一些负面影响。比如,在“单纯子函数”中, 不能直接或间接访问内存地址。所以在程序中使用“单纯子函数”时要特别小心。 另外,还可以使用#pragma 声明“单纯子函数”,下面的代码显示了它的声明过程。 #pragma no_side_effects /* function definition */ #pragma side_effects 14.9.6 内嵌函数 ARM 编译器支持函数内嵌功能。使用关键字“_inline”声明函数,可以使函数内嵌。下面的例子显示了如 何使用函数内嵌功能。 程序源文件如下。 __inline int square(int x) { return x * x; } #include double length(int x, int y) { return sqrt(square(x) + square(y)); } 编译结果如下所示。 length STMDB sp!,{lr} MUL a3,a1,a1 MLA a1,a2,a2,a3 BL _dflt LDMIA sp!,{lr} B sqrt 使用函数内嵌有以下好处:  减少了函数调用开销(如寄存器的压栈保护);  减少了参数传递开销;  进一步提高了编译器对代码优化的可能性(如编译器可将 ADD 和 MUL 指令合并为一条 MLA 指令)。 但使用函数内嵌将增加代码尺寸。也正是处于这种原因,armcc 和 tcc 都没有提供函数自动内嵌的编译选项。 一般来说,只有对性能影响较大的重要函数才使用关键字_inline 进行内嵌。 14.9.7 函数定义 使用函数时要先定义后调用是 ARM 编程的基本规则之一。在函数调用之前定义函数,编译器可以检查被 调用函数的寄存器使用情况,从而对其进行进一步的优化。 首先来看下面的例子。 int square(int x); int sumsquares1(int x, int y) { 专业始于专注 卓识源于远见       ‐  31  ‐            return square(x) + square(y); } /* square 函数可以在本文件中定义,也可以在其他源文件中定义 */ int square(int x) { return x * x; } int sumsquares2(int x, int y) { return square(x) + square(y); } 编译的结果如下所示。 sumsquares1 STMDB sp!,{v1,v2,lr} MOV v1,a2 BL square MOV v2,a1 MOV a1,v1 BL square ADD a1,v2,a1 LDMIA sp!,{v1,v2,pc} square MOV a2,a1 MUL a1,a2,a2 MOV pc,lr sumsquares2 STMDB sp!,{lr} MOV a3,a2 BL square MOV a4,a1 MOV a1,a3 BL square ADD a1,a4,a1 LDMIA sp!,{pc} 从编译的结果可以看出,将 square 函数定义放在 sumsquares 函数前,编译器可以判断寄存器 a3 和 a4 并未 使用,所有在调用函数入口处并未将其压栈。这样,减少了内存访问,提高了代码执行效率。 14.10 浮点运算 大多数的 ARM 处理器硬件上并不支持浮点运算。但 ARM 上提供了以下几个选项来实现浮点运算。  浮点累加协处理器 FPA(Floating-Point Accelerator):ARM 上提供了一组协处理器指令专门实现浮点运 算。但这需要硬件支持,具体某一处理器上是否有 FPA 协处理器支持,可以查看 ARM 相关手册。  浮点运算仿真(FPE):使用软件仿真了 FPA 协处理器的执行。  浮点运算库(FPLib):使用 ARM 的浮点运算库函数实现程序中的浮点运算操作。这就意味着 C 编译 器要把每一个浮点操作转换成一个子程序调用。C 库中的子函数使用整型运算来模拟浮点操作。这些代码 是用高效的汇编语言编写而成的。尽管如此,浮点运算执行起来还是要比相应整型运算慢得多。 专业始于专注 卓识源于远见       ‐  32  ‐            注意 Thumb 指令不支持协处理器指令,所以在 Thumb 状态下实现浮点运算,只能调用 ARM 浮点运 算库。 为了在 ARM 上高效地实现浮点运算,请遵循以下规则。  避免使用浮点除法运算。通常情况下,除法运算的执行速度是普通加法或乘法运算速度的 1/2。在无法 避免除法的情况下,尽量使除法的除数为常数。如,x=x/3.0,可将其变为 x = x * (1.0/3.0)。这样除数为常 数(1.0/3.0),该值在编译阶段由编译器计算。  使用 float 型代替 double 型。float 型要比 double 使用更少的内存和寄存器。  避免使用三角函数功能。实现三角函数功能,如 sin、cos,使用了大量的乘加运算,它的运算速度大约 是普通乘法运算的十倍。  当编译器处理浮点运算操作时,由于精度的影响很多优化不能实现。比如,表达式 3 * (x / 3),编译器 不能判断其值和 x 是等价的。所以在使用浮点运算表达式时,最好先人工的做一些必要的优化。 14.11 移植问题 当对源代码使用不同的编译器时,可能会出现一些移植上的问题,这时可以宏将一些 ARM 特有的关键字 “打包”。例如: #ifdef __arm # define INLINE __inline # define VALUE_IN_REGS __value_in_regs # define PURE __pure #else # define INLINE # define VALUE_IN_REGS # define PURE #endif 这样在使用是可以直接使用 INLINE、VALUE_IN_REGS 等关键字,例如, INLINE int square(int x) { return x*x; } 这样,在代码的移植过程中可以避免很多可能出现的问题。 联系方式 集团官网:www.hqyj.com 嵌入式学院:www.embedu.org 移动互联网学院:www.3g-edu.org 企业学院:www.farsight.com.cn 物联网学院:www.topsight.cn 研发中心:dev.hqyj.com 集团总部地址:北京市海淀区西三旗悦秀路北京明园大学校内 华清远见教育集团 北京地址:北京市海淀区西三旗悦秀路北京明园大学校区,电话:010-82600386/5 上海地址:上海市徐汇区漕溪路银海大厦 A 座 8 层,电话:021-54485127 深圳地址:深圳市龙华新区人民北路美丽 AAA 大厦 15 层,电话:0755-22193762 专业始于专注 卓识源于远见       ‐  33  ‐            成都地址:成都市武侯区科华北路 99 号科华大厦 6 层,电话:028-85405115 南京地址:南京市白下区汉中路 185 号鸿运大厦 10 层,电话:025-86551900 武汉地址:武汉市工程大学卓刀泉校区科技孵化器大楼 8 层,电话:027-87804688 西安地址:西安市高新区高新一路 12 号创业大厦 D3 楼 5 层,电话:029-68785218 《ARM 系列处理器应用技术完全手册》 作者:华清远见 第 15 章 ARM 存储器 专业始于专注 卓识源于远见       ‐  2  ‐            ARM 存储系统有非常灵活的体系结构,可以适应不同的嵌入式应用系统的需要。ARM 存储器系统可以使 用简单的平板式地址映射机制(就像一些简单的单片机一样,地址空间的分配方式是固定的,系统中各部 分都使用物理地址),也可以使用其他技术提供功能更为强大的存储系统。比如:  系统可能提供多种类型的存储器件,如 FLASH、ROM、SRAM 等;  Caches 技术;  写缓存技术(write buffers);  虚拟内存和 I/O 地址映射技术。 大多数的系统通过下面的方法之一实现对复杂存储系统的管理。  使能 Cache,缩小处理器和存储系统速度差别,从而提高系统的整体性能。  使用内存映射技术实现虚拟空间到物理空间的映射。这种映射机制对嵌入式系统非常重要。通常嵌入 式系统程序存放在 ROM/FLASH 中,这样系统断电后程序能够得到保存。但是通常 ROM/FLASH 与 SDRAM 相比,速度慢很多,而且基于 ARM 的嵌入式系统中通常把异常中断向量表放在 RAM 中。利用内存映射 机制可以满足这种需要。在系统加电时,将 ROM/FLASH 映射为地址 0,这样可以进行一些初始化处理; 当这些初始化处理完成后将 SDRAM 映射为地址 0,并把系统程序加载到 SDRAM 中运行,这样很好地满 足嵌入式系统的需要。  引入存储保护机制,增强系统的安全性。  引入一些机制保证将 I/O 操作映射成内存操作后,各种 I/O 操作能够得到正确的结果。在简单存储系统 中,不存在这样问题。而当系统引入了 Cache 和 write buffer 后,就需要一些特别的措施。 在 ARM 系统中,要实现对存储系统的管理通常是使用协处理器 CP15,它通常也被称为系统控制协处理器 (System Control Coprocessor)。 ARM 的存储器系统是由多级构成的,每级都有特定的容量和速度。 图 15.1 显示了存储器的层次结构。 ① 寄存器。处理器寄存器组可看作是存储器层次的顶层。这些寄存器被集成在处理器内核中,在系统中 提供最快的存储器访问。典型的 ARM 处理器有多个 32 位寄存器,其访问时间为 ns 量级。 Flash 和其他非易失级的存储器 硬盘、磁带和网络存储 SRAM DRAM 寄存器组 TCM Cache 写缓存 内核 芯片级 板卡级 外设 存储量 速度 图 15.1 存储器的层次结构 ② 紧耦合存储器 TCM。为弥补 Cache 访问的不确定性增加的存储器。TCM 是一种快速 SDRAM,它紧挨 内核,并且保证取指和数据操作的时钟周期数,这一点对一些要求确定行为的实时算法是很重要的。TCM 位于存储器地址映射中,可作为快速存储器来访问。 ③ 片上 Cache 存储器的容量在 8KB~32KB 之间,访问时间大约为 10ns。 ④ 高性能的 ARM 结构中,可能存在第二级片外 Cache,容量为几百 KB,访问时间为几十 ns。 ⑤ DRAM。主存储器可能是几 MB 到几十 MB 的动态存储器,访问时间大约为 100ns。 ⑥ 后援存储器,通常是硬盘,可能从几百 MB 到几个 GB,访问时间为几十 ms。 专业始于专注 卓识源于远见       ‐  3  ‐            注意 TCM 和 SRAM 在技术上相同,但在结构排列上不同;TCM 在片上,而 SRAM 在板上。 15.1 协处理器 CP15 ARM 处理器支持 16 个协处理器。在程序执行过程中,每个协处理器忽略属于 ARM 处理器和其他协处理 器的指令。当一个协处理器硬件不能执行属于它的协处理器指令时,将产生一个未定义指令异常中断,在 该异常中断处理程序中,可以通过软件模拟该硬件操作。比如,如果系统不包含向量浮点运算器,则可以 选择浮点运算软件模拟包来支持向量浮点运算。 CP15,即通常所说的系统控制协处理器(System Control Coprocesssor)。它负责完成大部分的存储系统管 理。除了 CP15 外,在具体的各种存储管理机制中可能还会用到其他的一些技术,如在 MMU 中除 CP15 外,还使用了页表技术等。 在一些没有标准存储管理的系统中,CP15 是不存在的。在这种情况下,针对协处理器 CP15 的操作指令将 被视为未定义指令,指令的执行结果不可预知。 CP15 包含 16 个 32 位寄存器,其编号为 0~15。实际上对于某些编号的寄存器可能对应多个物理寄存器, 在指令中指定特定的标志位来区分这些物理寄存器。这种机制有些类似于 ARM 中的寄存器,当处于不同 的处理器模式时,某些相同编号的寄存器对应于不同的物理寄存器。 CP15 中的寄存器可能是只读的,也可能是只写的,还有一些是可读可写的。在对协处理器寄存器进行操 作时,需要注意以下几个问题。  寄存器的访问类型(只读/只写/可读可写)。  不同的访问引发的不同功能。  相同编号的寄存器是否对应不同的物理寄存器。  寄存器的具体作用。 15.1.1 CP15 寄存器访问指令 通常对协处理器 CP15 的访问使用以下两种指令。 MCR:将 ARM 寄存器的值写入 CP15 寄存器中; MRC:将 CP15 寄存器的值写入 ARM 寄存器中。 注意 通过协处理器访问指令 CDP、LDC 和 STC 指令对协处理器 CP15 进行访问将产生不可预知的 结果。 其中,CDP 为协处理器数据操作指令,这个指令初始化一些与协处理器相关的操作; LDC 为一个或多个字的协处理器数据读取指令,此指令从存储器读取数据到指定的协处理器中; STC 为一个或多个 32 位字的协处理器数据写入指令,此指令初始化一个协处理器的写操作, 从给定的协处理器把数据传送到存储器中。 指令 MCR 和 MRC 指令访问 CP15 寄存器使用通用语法。 语法格式为: MCR{} p15,{,} MRC{} p15,{,} 其中: 为指令的执行条件。当条件域为空时,指令无条件执行; 在标准的 MRC 指令中,为协处理器的,即操作数 1。对于 CP15 来说,此操作数恒为 0,即 0b000。当针对 CP15 的 MRC 指令中不为 0 时,指令的操作结果不可预知; 专业始于专注 卓识源于远见       ‐  4  ‐            为 ARM 寄存器,在 ARM 和协处理器交换数据时使用。在 MRC 指令中作为目的寄存器,在 MCR 中 作为源寄存器。 注意 r15 不能作为 ARM 寄存器出现在 MRC 或 MCR 指令中,如果 r15 作为出现在这里,那么 指令的执行结果不可预知。 是 CP15 协处理器指令中用到的主要寄存器。在 MRC 指令中为源寄存器,在 MCR 中为目的寄存器。 CP15 协处理器的寄存器 c0、c1、…、c15 均可出现在这里。 是附加的协处理器寄存器,用于区分同一个编号的不同物理寄存器和访问类型。当指令中不需要提 供附加信息时,将指定为 C0,否则指令的操作结果不可预知。 提供附加信息,用于区分同一个编号的不同物理寄存器,当指令中没有指定附加信息时,省略 或者将其指定为 0,否则指令的操作结果不可预知。 MCR 和 MRC 指令只能操作在特权模式下,如果处理器运行在用户模式,指令的执行结果不可预知。 注意 在用户模式下,如果要访问系统控制协处理器,通常的做法是由操作系统提供 SWI 软中断调 用来完成系统模式的切换。由于不同型号的 ARM 处理器对此管理差别很大,所以建议用户 在应用时将 SWI 作为一个独立的模块来管理并向上提供通用接口,以屏蔽不同型号处理器之 间的差异。 例 15.1 给出了一个典型的利用 SWI 进行模式切换的例子。 【例 15.1】 典型的在 SWI 中进行模式切换的例子。利用此例,调用 SWI 0 来完成系统模式切换。 EHT_SWI LDR sp,=EHT_Exception_Stack ;更新 SWI 堆栈指针 ADD sp,sp,#EXCEPTION_SIZE ;得到栈顶指针 STMDB sp!,{r0-r2,lr} ;保存程序中用到的寄存器 MRS r0,SPSR ;得到 SPSR STMDB sp!,{r0} ;保持 SPSR LDR r0,[lr,#-4] ;计算 SWI 指令地址 BIC r0,r0,#0xFF000000 ;提取中断向量号 CMP r0,#MAX_SWI ;检测中断向量范围 LDRLS pc,[pc,r0,LSL #2] ;如果在范围内,跳转到软中断向量表 B EHT_SWI_Exit ;为定义的 SWI 指令出口 EHT_Jump_Table DCD EHT_SU_Switch DCD EHT_Disable_Interrupts ;********************************************************************************* ;用户可在此添加更多的自定义软中断,在此 SWI0 作为系统保留的软中断,调用例程 EHT_SU_Switch,来进行模式切换 ;********************************************************************************* EHT_SU_Switch MMU_DISABLE ;转换前禁用 MMU LDMIA sp!,{r0} ;从堆栈中取出 SPSR BIC r0,r0,#MODE_MASK ;清除模式位 专业始于专注 卓识源于远见       ‐  5  ‐            ORR r0,r0,#SYS_MODE ;设置程序状态字的 supper 模式位 STMDB sp!,{r0} ;从新将 SPSR 放入堆栈 B EHT_SWI_Exit EHT_Disable_Interrupts LDMIA sp!,{r0} ;从堆栈中读出 SPSR ORR r0,r0,#LOCKOUT ;禁止中断 STMDB sp!,{r0} ;存储 SPSR 到中断 ; B EHT_SWI_Exit EHT_SWI_Exit LDMIA sp!,{r0} ;从堆栈中读出 SPSR MSR SPSR_cf,r0 ;将 SPSR 放入 SPSR_cf LDMIA sp!,{r0-r2,pc}^ ;寄存器出栈并返回 END 15.1.2 CP15 中的寄存器 表 15.1 给出了 CP15 主要寄存器的功能和作用。 表 15.1 CP15 寄存器 寄存器编号 基 本 作 用 特 殊 用 途 0 ID 编号(只读) ID 和 Cache 类型 1 控制位 各种控制位 2 存储器保护和控制 MMU:地址转换表基地址 PU:Cache 属性设置 3 内存保护和控制 MMU:域访问控制 PU:写缓存控制 4 内存保护和控制 保留 5 内存保护和控制 MMU:错误状态 PU:访问权限控制 6 内存保护和控制 MMU:错误状态 PU:保护区域控制 7 Cache 和写缓存 Cache 和写缓存控制 8 内存保护和控制 MMU:TLB 控制 PU:保留 9 Cache 和写缓存 Cache 锁定 续表 寄存器编号 基 本 作 用 特 殊 用 途 10 内存保护和控制 MMU:TLB 锁定 PU:保留 11 保留 保留 专业始于专注 卓识源于远见       ‐  6  ‐            12 保留 保留 13 进程 ID 进程 ID 14 保留 保留 15 芯片生产厂商定义 芯片生产厂商定义 15.1.3 寄存器 c0 寄存器 c0 包含的是 ARM 本身或芯片生产厂商的一些标识信息。当使用 MRC 指令读 c0 寄存器时,根据第 二个操作码 opcode2 的不同,读出的标识符也是不同的。操作码与标识符的对应关系如表 15.2 所示。寄存 器 c0 是只读寄存器,当用 MCR 指令对其进行写操作时,指令的执行结果不可预知。 表 15.2 操作码和标识符的对应关系 操作码 opcode2 对应的标识符寄存器 0b000 主标识符寄存器 0b001 Cache 类型寄存器 其他 保留 在操作码 opcode2 的取值中,主标识符(opcode2=0)是强制定义的,其他标识符由芯片的生产厂商定义。 如果操作码 opcode2 指定的值未定义,指令将返回主标识符。其他标识符的值应与主标识符的值不同,可 以由软件编程来实现,同时读取主标识符和其他标识符,并将两者的值进行比较。如果两个标识符值相同, 说明未定义该标识符;如果两个标识符值不同,说明定义了该标识符,并且得到该标识符的值。 (1)主标识符寄存器 当协处理器指令对 CP15 进行操作,并且操作码 opcode=2 时,处理器的主标识符将被读出。从主标识符中, 可以确定 ARM 体系结构的版本型号。同时也可以参考由芯片生产厂商定义的其他标识符,来获得更详细 的信息。 在主标识信息中,bit[15:12]区分了不同的处理器版本:  如果 bit[15:12]为 0x0,说明处理器是 ARM7 之前的处理器;  如果 bit[15:12]为 0x7,说明处理器为 ARM7 处理器;  如果 bit[15:12]为其他值,说明处理器为 ARM7 之后的处理器。 对于 ARM7 之后的处理器,其标识符的编码格式如图 15.2 所示。 其中各部分的编码含义说明如下。 bit[3:0]:包含生产厂商定义的处理器版本型号。 bit[15:4]:生产厂商定义的产品主编号,可能的取值为 0x0~0x7。 bit[19:16]:ARM 体系的版本号,可能的取值如表 15.3(其他值由 ARM 公司保留将来使用)所示。 由生产商决定 (Implementor) 产品子编号 (Variant) Arm系统版本号 (Architecture) 产品主编号 (Primary Part Number) 处理器版本 号 (Revision) 3 0 4 15 16 19 20 23 24 30 19 处理器版本型号 图 15.2 ARM7 之后处理器标识符编码 表 15.3 bit[19:16]与 ARM 版本号 可能的取值 版 本 号 0x1 ARM 体系版本 4 0x2 ARM 体系版本 4T 0x3 ARM 体系版本 5 专业始于专注 卓识源于远见       ‐  7  ‐            0x4 ARM 体系版本 5T 0x5 ARM 体系版本 5TE bit[23:20]:生产厂商定义的产品子编号。当产品主编号相同时,使用子编号区分不同的产品子类,如产品 中不同的 cache 的大小。 bit[31:24]:生产厂商的编号现已定义的如表 15.4 所示。其他的值 ARM 公司保留将来使用。 表 15.4 bit[31:24]值与 ARM 生产厂商 可能的取值 ARM 芯片生产厂商 0x41(A) ARM 公司 0x44(D) Digital Equipment 0x69(i) Intel 公司 对于 ARM7 系统的处理器,其主标识符的编码如图 15.3 所示。 由生产商决定 (Implementor) A Arm 系统版本号 (Architecture) 产品主编号 (Primary Part Number) 处理器版本型号 (Revision) 30 24 23 22 16 15 4 3 0 图 15.3 ARM7 处理器标识符编码 其中各部分的含义说明如下。 bit[3:0]:包含生产厂商定义的处理器版本型号。 bit[15:4]:生产厂商定义的产品主编号,其最高 4 位的值为 0x7。 bit[22:16]:生产商定义的产品子编号。当产品的主编号相同时,使用子编号区分不同的产品子类,如产品 中不同的产品子类、不同产品中高速缓存的大小。 bit[23]:ARM7 处理器支持下面两种 ARM 体系的版本号。0x0 代表 ARM 体系版本 3;0x1 代表 ARM 体系 版本 4T。 bit[31:24]:生产厂商的编号已定义的如表 15.5 所示,其他的值 ARM 公司保留将来使用。 表 15.5 bit[31:24]值与 ARM 生产厂商 可能的取值 ARM 芯片生产厂商 0x41(A) ARM 公司 0x44(D) Digital Equipment 0x69(i) Intel 公司 对于 ARM7 系统的处理器,其主标识符的编码如图 15.4 所示。 处理器 版本型号 30 4 3 0 处理器标识 图 15.4 ARM7 之前处理器标识符编码 其中各部分的含义说明如下。 bit[3:0]:包含生产厂商定义的处理器版本型号。 bit[31:4]:处理器标识符及其含义如表 15.6 所示。 表 15.6 ARM 之后处理器标识符与含义 处理器标识符 含 义 0x4156030 ARM3(体系版本 2) 专业始于专注 卓识源于远见       ‐  8  ‐            0x4156060 ARM600(ARM 体系版本 3) 0x4156061 ARM610(ARM 体系版本 3) 0x4156062 ARM620(ARM 体系版本 3) (2)Cache 类型标识符寄存器 如前所述,对于指令 MRC 来说,当协处理器寄存器为 r0,而第二操作数 opcode2 为 0b001 时,指令读取 值为 Cache 类型,即可以用下面的指令将处理器的 Cache 类型标识符寄存器的内容读取到寄存器 r0 中。 MRC P15,0,r0,c0,c0,1 Cache 类型标识符定义了关于 Cache 的信息,具体内容如下所述。  系统中的数据 Cache 和指令 Cache 是分开的还是统一的。  Cache 的容量、块大小以及相联特性。  Cache 类型是直(write-through)写还是回写(write-back)1。  对于回写(write-back)类型的 Cache 如何有效清除 Cache 内容。  Cache 是否支持内容锁定。 Cache 类型标识符寄存器各控制字段的含义编码格式如图 15.5 所示。 000 属性字段 31 29 28 25 24 23 12 11 0 S 数据 Cache 相关 属性 指令 Cache 相关 字段 图 15.5 Cache 属性寄存器标识符编码格式 其中各控制字段的含义说明如下。 属性字段(ctype):指定没有在 S 位、数据 Cache 相关属性位、指令 Cache 相关属性类中指定的属性,其 具体编码参见表 15.7。 表 15.7 Cache 类型标识符寄存器属性字段含义 编 码 Cache 类型 Cache 内容清除方法 Cache 内容锁定方法 0b0000 直写 不需要内容清除 不支持 0b0001 回写 数据块读取 不支持 0b0010 回写 由寄存器定义 不支持 0b0110 回写 由寄存器定义 支持格式 A,见后 0b0111 回写 由寄存器定义 支持格式 B,见后 S 位:定义系统中的数据 Cache 和指令 Cache 是分开的还是统一的。如果 S=0,说明指令 Cache 和数据 Cache 是统一的,如果 S=1,则说明数据 Cache 和指令 Cache 是分离的。 数据 Cache 相关属性:定义了数据 Cache 容量、行大小和相联(associativity)特性(如果 S≠0)。 指令 Cache 相关属性:定义了指令 Cache 容量、行大小和相联(associativity)特性(如果 S≠0)。 数据 Cache 相关属性和指令 Cache 相关属性分别占用控制字段[23:12]和[11:0],它们的结构相同,图 15.6 以指令 Cache 为例,显示了编码结构。 000 Cache 容量 Cache 相关特性 M 块大小 11 9 8 6 5 3 2 1 0 1 关于 Cache 写策略 write-through 和 write-back,本书将其分别译为“直写”和“回写”。Cache 控制器提供 2 种写策略。它 可以同时向 Cache 行和相应的主存位置中写入数据,将存储在两个位置上的数据同时更新,这种做法成为直写。另外,也 可以只把数据写入相应的 Cache 行,而不写入主存,只有当相应的 Cache 行被替换或清理 Cache 行时,才被写入主存,这 种做法被成为回写。具体内容在相应章节中会有详细介绍。 专业始于专注 卓识源于远见       ‐  9  ‐            图 15.6 指令 Cache 编码结构 其中,各部分的含义说明如下。 bit[11:9]:保留用于将来使用。 bit[8:6]:定义 Cache 的容量,其编码格式及含义如表 15.8 所示。 表 15.8 类型标识符寄存器控制字段 bit[8:6]含义 编 码 M=0 时的含义 M=1 时的含义 0b000 0.5KB 0.75KB 0b001 1KB 1.5KB 0b010 2KB 3KB 0b011 4KB 6KB 续表 编 码 M=0 时的含义 M=1 时的含义 0b100 8KB 12KB 0b101 16KB 24KB 0b110 32KB 48KB 0b111 64KB 96KB bit[1:0]:定义 Cache 的块大小,其编码格式及含义如表 15.9 所示。 表 15.9 类型标识符寄存器控制字段 bit[1:0]含义 编 码 Cache 块大小 0b00 2 个字(8 字节) 0b01 4 个字(16 字节) 0b10 8 个字(32 字节) 0b11 16 个字(64 字节) bit[5:3]:定义了 Cache 的相联属性,其编码格式及含义如表 15.10 所示。 表 15.10 类型标识符寄存器控制字段 bit[5:3]含义 编 码 M=0 时的含义 M=1 时的含义 0b000 1 路相联 (直接映射) 没有 Cache 0b001 2 路相联 3 路相联 0b010 4 路相联 6 路相联 0b011 8 路相联 12 路相联 0b100 16 路相联 24 路相联 0b101 32 路相联 48 路相联 0b110 64 路相联 96 路相联 0b111 128 路相联 192 路相联 15.1.4 寄存器 c1 CP15 中的寄存器 c1 包括以下控制功能:  禁止/使能 MMU 以及其他与存储系统有关的功能; 专业始于专注 卓识源于远见       ‐  10  ‐             配置存储系统以及 ARM 处理器中相关的工作。 注意 在寄存器 c1 中包含了一些没有使用的位,这些位在将来可能被扩展其他功能时使用。因此为了 编写代码在将来更高版本的 ARM 处理器中仍可以使用,在修改寄存器 c1 中的位时应该使用“读 取-修改特定位-写入”的操作序列。 当对寄存器 c1 进行读操作时,指令中 CRm 和 opcode2 的值将被处理器忽略,所以要人工将其置位为 0。 例 15.2 用 MRC/MCR 指令将协处理器寄存器 c1 的值进行读取和写入。 【例 15.2】 MRC P15,0,r0,c1,0,0 ;将寄存器 c1 的值读取到 ARM 寄存器 r0 中 MCR P15,0,r0,c1,0,0 ;将 ARM 寄存器 r0 的值写入寄存器 c1 图 15.7 显示了寄存器 c1 的编码格式。 SBZP/ UNP L4 RR V I Z F R S B L D P W C A M 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 31 图 15.7 寄存器 c1 编码格式 寄存器 c1 各控制字段的含义如表 15.11 所示。 表 15.11 寄存器 c1 中各控制位字段的含义 C1 中的控制位 含 义 M(bit[0]) 禁止/使能 MMU 或者 MPU 0:禁止 MMU 或者 MPU 1:使能 MMU 或者 MPU 如果系统中没有 MMU 或者 MPU,读取时该位返回 0,写入时忽略 A(bit[1]) 对于可以选择是否支持内存访问时地址对齐检查的系统,本位禁止/使能地址对齐检查 功能 0:禁止地址对齐检查功能 1:使能地址对齐检查功能 对寄存器进行写操作时,忽略该位 C(bit[2]) 当数据 Cache 和指令 Cache 分开时,本控制位禁止/使能数据 Cache。 当数据 Cache 和指令 Cache 统一时,该控制位禁止/使能整个 Cache 0:禁止 Cache 1:使能 Cache 如果系统中不含 Cache,读取时该位返回 0,写入时忽略 当系统中 Cache 不能禁止时,读取返回 1,写入时忽略 W(bit[3]) 禁止/使能写缓存 0:禁止写缓存 1:使能写缓存 如果系统中不含写缓存,读取时该位返回 0,写入时忽略 当系统中的写缓存不能禁止时,读取时该位返回 0,写入时忽略 P(bit[4]) 对于向前兼容 26 位 ARM 处理器,本控制位控制 PRGC32 控制信号 0:异常中断处理程序进入 32 位地址模式 1:异常中断处理程序进入 26 位地址模式 如果系统不支持向前兼容 26 位地址,读取该位时返回 1,写入时被忽略 D(bit[5]) 对于向前兼容 26 位 ARM 处理器,本控制位控制 DATA32 控制信号 0:禁止 26 位地址异常检查 专业始于专注 卓识源于远见       ‐  11  ‐            1:使能 26 位地址异常检测 如果系统不支持向前兼容 26 位地址,读取该位时返回 1,写入时被忽略 续表 C1 中的控制位 含 义 L(bit[6]) 对于 ARMv3 及以前版本,本控制位可以控制处理器的中止模式 0:选择早期中止模式 1:选择后期中止模式 对于以后的处理器读取该位时返回 1,写入时忽略 B(bit[7]) 对于存储系统同时支持大/小端(big-endian/little-endian)的 ARM 处理器,该控制位配 置系统使用哪种内存模式 0:使用小端(little-endian) 0:使用大端(big-endian) 对于只支持小端(little-endian)的系统,读取时该位返回 0,写入时忽略 对于只支持大端(big-endian)的系统,读取时该位返回 1,写入时忽略 S(bit[8]) 支持 MMU 的存储系统中,本控制位用作系统保护 R(bit[9]) 支持 MMU 的存储系统中,本控制位用作 ROM 保护 F(bit[10]) 本控制位由生产厂商定义 Z(bit[11]) 对于支持跳转预测的 ARM 系统,本控制位禁止/使能跳转预测功能 0:禁止跳转预测功能 1:使能跳转预测功能 对于不支持跳转预测的 ARM 系统,读取时该位返回 0,写入时忽略 I(bit[12]) 当数据 Cache 和指令 Cache 是分开的,本控制位禁止/使能指令 Cache 0:禁止指令 Cache 1:使能指令 Cache 如果系统中使用统一的指令 Cache 和数据 Cache 或者系统中不含 Cache,读取该位时返 回 0,写入时忽略该位 当系统中的指令 Cache 不能禁止时,读取该位返回 1,写入时忽略该位 V(bit[13]) 支持高端异常向量表的系统中,本控制位控制向量表的位置 0:选择 0x00000000~0x0000001c 1:选择 0Xffff0000~0xffff001c 对于不支持高端中断向量表的系统,读取时返回 0,写入时忽略 RR(bit[14]) 如果系统中 Cache 的淘汰算法可以选择的话,本控制位选择淘汰算法 0:选择常规的淘汰算法,如随机淘汰算法 1:选择预测性的淘汰算法,如轮转(round-robin)淘汰算法 如果系统中淘汰算法不可选择,写入该位时被忽略,读取该位时,根据其淘汰算法是 否可以比较简单地预测最坏情况返回 1 或者 0 L4(bit[15]) ARM 版本 5 及以上的版本中,本控制位可以提供兼容以前的 ARM 版本的功能 0:保持当前 ARM 版本的正常功能 1:对于一些根据跳转地址的 bit[0]进行状态切换的指令,忽略 bit[0],不进行状态切换, 保持和以前 ARM 版本兼容 此控制位可以影响以下指令:LDM、LDR 和 POP 对于 ARM 版本 5 以前的处理器,该位没有使用,应作为 UNP/SBZP 对于 ARM 版本 5 以后的处理器,如果不支持向前兼容的属性,读取时该位返回 0,写 入时忽略 Bit(bit[31:16]) 这些位保留将来使用,应为 UNP/SBZP 15.2 片上存储器 专业始于专注 卓识源于远见       ‐  12  ‐            如果微处理器要达到最佳性能,那么采用片上存储器是必需的。通常 ARM 处理器的主频为几十 MHz 到 200MHz。而一般的主存储器采用动态存储器(ROM),其存储周期仅为 100ns~200ns。这样指令和数据都 存放在主存储器中,主存储器的速度将会严重制约整个系统的性能。在当前的时钟速度下,只有片上存储 器能支持零等待状态访问速度。同时,与片外存储器相比,片上存储器有较好的功耗效率,并减少了电磁 干扰。 在许多嵌入式系统中采用简单的片上 RAM 而不是 Cache,因为 Cache 有以下特点:  结构简单、价格便宜且功耗低;  Cache 有更不确定的行为。 在设计中,为了使 Cache 有效的工作需要巨大的逻辑开销。同时,如果没有合适、现成的 Cache,则设计 费用是惊人的。而且,Cache 存储器的操作很复杂,这使得在大多数情况下很难预测它将如何工作,在某 种程度上导致了系统的不可预知性。特别是在有中断的情况下,很难保证中断响应时间。 与 Cache 相比,片上 RAM 的缺点是需要程序员直接管理。而 Cache 对于程序员来说是透明的。如果程序 的混合(program mix)、定义好并且不需要程序员对其进行控制,那么片上 RAM 就能有效的作为软件控 制的 Cache 来使用。如果应用程序的混合不能预知,那么控制任务就变得非常困难。因此,应用程序的混 合不可预知的通用系统中,通常采用 Cache。 片上 RAM 的一个重要特点是,它使程序员能够根据对将来处理工作量的了解划分 RAM 空间。而 Cache 只能记录运行过的程序状态,无法预测以后要运行的程序,因此,无法实现对将来的关键任务预先作准备。 在一些嵌入式系统中,关键任务必须满足严格的实时约束,这个差别就显得特别重要。 15.3 高速缓冲存储器 Cache 当第一代 RISC 微处理器刚出现时,标准存储器元件的速度比当时微处理器的速度快。很快,半导体工艺 技术的进展被用来提高微处理器的速度。标准 DRAM 部件虽然也快了一些,但其发展的主要精力则放在 提高存储容量上。 1980 年,典型 DRAM 部件的容量为 4KB。1981 年和 1982 年开发出了 16KB 芯片。这些部件的随机访问 速率为 3MHz 或 4MHz,局部访问(页模式)时速率大约快 1 倍。当时的微处理器每秒需要访问存储器 2M 次。 到 2000 年,DRAM 部件每片的容量到达 256Mbit,随机访问速率在 30MHz 左右。微处理器每秒需要访问 存储器几百兆次。如果处理器速率远高于存储器,那么只能借助 Cache 才能满足其全部性能。 Cache 存储器是一个容量小但存取速度非常快的存储器,它保存最近用到的存储器数据拷贝。对于程序员 来说,Cache 是透明的。它自动决定保存哪些数据、覆盖哪些数据。现在 Cache 通常与处理器在同一芯片 上实现。Cache 能够发挥作用是因为程序具有局部性特性。所谓局部性就是指,在任何特定的时间,微处 理器趋于对相同区域的数据(如堆栈)多次执行相同的指令(如循环)。 Cache 经常与写缓存器(write buffer)一起使用。写缓存器是一个非常小的先进先出(FIFO)存储器,位 于处理器核与主存之间。使用写缓存的目的是,将处理器核和 Cache 从较慢的主存写操作中解脱出来。当 CPU 向主存储器做写入操作时,它先将数据写入到写缓存区中,由于写缓存器的速度很高,这种写入操作 的速度也将很高。写缓存区在 CPU 空闲时,以较低的速度将数据写入到主存储器中相应的位置。 通过引入 Cache 和写缓存区,存储系统的性能得到了很大的提高,但同时也带来了一些问题。比如,由于 数据将存在于系统中的不同的物理位置,可能造成数据的不一致性;由于写缓存区的优化作用,可能有些 写操作的执行顺序不是用户期望的顺序,从而造成操作错误。 15.3.1 Cache 的分类 Cache 有多种构造方法。在最高层次,微处理器可以采用下面两种组织中的一组。 (1)统一 Cache。指令和数据用同一个 Cache。结构如图 15.8 所示。 专业始于专注 卓识源于远见       ‐  13  ‐            指令的拷贝 寄存器 处理器 数据的拷贝 Cache 指令 数据 存储器 指令和数据 地址 指令和数据 地址 图 15.8 统一的指令 Cache 和数据 Cache (2)指令和数据分开的 Cache。有时这种组织方式也被称为改进的哈佛结构。 图 15.9 显示了这种组织方式。 这两种组织方式各有优缺点。统一 Cache 能够根据当前程序的需要自动调整指令在 Cache 存储器的比例, 比固定划分的有更好的性能。另一方面,分开的 Cache 使 Load/Store 指令能够单周期执行。 15.3.2 Cache 性能的衡量 只有当所需要的 Cache 存储器内容已经在 Cache 时,微处理器才能以高时钟速率工作。因此,系统的总体 性能就可以用存储器访问中命中 Cache 的比例来衡量。当要访问的内容在 Cache 时称为命中(hit),而要 访问的内容不在 Cache 时称为未命中(miss)。在给定时间间隔内,Cache 命中的次数与总的存储器请求次 数的比值被称为命中率。 数据的拷贝 Cache 指令 数据 存储器 指令和数据 地址 指令和数据 地址 寄存器 处理器 指令的拷贝 Cache 指令和数据 地址 图 15.9 指令 Cache 和数据分开的 Cache 专业始于专注 卓识源于远见       ‐  14  ‐            命中率用下面的公式进行计算: 命中率=(Cache 命中次数÷存储器请求次数)×100% 未命中率与命中率形式相似,即在给定时间间隔内,Cache 未命中的总次数除以总的存储器请求次数所得 的百分比。未命中率与命中率之和等于 100。 目前设计良好的处理器,Cache 的未命中率只有百分之几。未命中率依赖多个 Cache 参数,包括 Cache 大 小和组织。 15.3.3 Cache 工作原理 Cache 的基本存储单元为 Cache 行(Cache line)。存储系统把 Cache 和主存储器都划分为相同大小的行。 Cache 与主存储器交换数据是以行为基本单位进行的。每一个 Cache 行都对应于主存中的一个存储块 (memory block)。 Cache 行的大小通常是 2L 字节。通常情况下是 16 字节(4 个字)和 32 字节(8 个字)。如果 Cache 行的大小 为 2L 字节,那么对主存的访问通常是 2L 字节对齐的。所以对于一个虚拟地址来说,它的 bit[31∶L]位,是 Cache 行的一个标识。当 CPU 发出的虚拟地址的 bit[31∶L]和 Cache 中的某行 bit[31∶L]相同,那么 Cache 中包含 CPU 要访问的数据,即成为一次 Cache 命中。 为了加快 Cache 访问的速度,又将多个 Cache 行划分成一个 Cache 组(Cache Set)。Cache 组中包含的 Cache 行的个数通常也为 2 的 N 次方的倍数。为了方便起见,取 N=S。这样,一个 Cache 组中就包含 2S 个 Cache 行。这时,虚拟地址中的 bit[L+S-1∶L]为 Cache 组的标识。虚拟地址中余下的位 bit[31∶L+S]成为一个 Cache 标(Cache-tag)。它标识了 Cache 行中的内容和主存间的对应关系。 图 15.10 显示了 Cache 的访问过程。 Tag Set Pos 虚拟地址 31 L+S L+S–1 L–1 0 选择 Cache 组 使用 Tag 选择 Cache 行 L Cache 命中,返回所要 访问的数 未命中,访问内存 图 15.10 Cache 访问过程 15.3.4 Cache 与主存的关系 专业始于专注 卓识源于远见       ‐  15  ‐            在 Cache 中采用地址映射将主存中的内容映射到 Cache 地址空间。具体的说,就是把存放在主存中的程序 按照某种规则装入到 Cache 中,并建立主存地址到 Cache 地址之间的对应关系。而地址变换是指当程序已 经装入到 Cache 后,在实际运行过程中,把主存地址变换成 Cache 地址。 地址的映射和变换是密切相关的。采用什么样的地址映射方法,就必然有与之对应的地址变换。 常用的地址映射和变换方式包括直接映射和变换方式、组相联映射和变换方式以及全相联和变换方式。 (1)直接(direct-mapped)映射方式 直接映射是一种最简单,也是最直接的映射方式。主存中的每个地址都对应 Cache 存储器中惟一的一行。 由于主存的容量远远大于 Cache 存储器,所以在主存中很多地址被映射到同一个 Cache 行。 图 15.11 显示了主存与 Cache 的直接映射关系。 Cache Cache 行 0x00000127 0x00000227 0x00002127 主存 图 15.11 主存和 Cache 的直接映射 直接映射 Cache 是一种简单的解决方法,但这种设计使得每个主存块在 Cache 中只有一个特定的行可以存 放。如果程序同时用到对应于 Cache 同一行的两个主存块,那么就会发生冲突,冲突的结果是导致 Cache 行的频繁变换。这种由直接映射导致的 Cache 存储器中的软件冲突称为颠簸(thrashing)问题。 (2)组相联映射方式 为了减少颠簸问题,有些 Cache 使用了组相联的映射策略。在组相联的地址映射和变换中,把主存和 Cache 按同样大小划分成组(set),每个组都由相同的行数组成。 由于主存的容量比 Cache 容量大得多,因此,主存的组数要比 Cache 的组数多。从主存的组到 Cache 的组 之间采用直接映射方式。主存中的一组与 Cache 中的一组之间建立了之间映射方式后,在两个对应的组内 部采用全相联映射方式。 在ARM中采用的是组相联的地址映射和变换方式。如果Cache的行大小为2L,则同一行中各地址的bit[31∶ L]是相同的。如果 Cache 中组的大小(每组中包含的行数)为 2S,则虚地址位 bit[L+S∶L]用于选择 Cache 中的某个组。 图 15.12 显示了一个 Cache 与主存储器的组相联映射 专业始于专注 卓识源于远见       ‐  16  ‐            主存储器中的组 主存储器中的组 Cache 行地址 主存 4G 地址 Cache 组 n 组 m 组 l 标签 组索引 数据索引 图 15.12 Cache 与主存储器组相联映射 拥有相同组索引的 Cache 行称为组相联的(set associative)。主存中的程序或代码段可以在不影响程序执行 的情况下被分配到 Cache 中的某一组中。也就是说,将数据或代码存入 Cache 行中的操作不会影响程序的 执行。 (3)全相联映射方式 随着 Cache 控制器的相联度的提高,冲突的可能性减少了。理想的目标是,尽量提高组相联程度,使主存 地址能够映射到任意 Cache 行。这样的 Cache 被称为全相联 Cache。然而,随着相联度的提高,与之相匹 配的硬件的复杂度也在提高。硬件设计者提高 Cache 相联度的一种方法就是使用内容寻址寄存器 CAM (Content Addressable Memory)。 CAM 使用一组比较器,以比较输入的标签地址和存储在每一个有效 Cache 行中的标签位。CAM 采取了与 RAM 相反的工作方式;RAM 在得到一个地址后再给出数据;而 CAM 则是在检测到给定的数据值在存储 器中后,再给出该数据的地址。使用 CAM 允许同时比较更多的地址中的标签位,从而增加了可以包含在 一个组中的 Cache 行数。 在 ARM920T 和 ARM940T 存储器核中,ARM 使用了 CAM 来定位地址中的标签域。ARM920T 和 ARM940T 中的 Cache 是 64 组组相联的。图 15.13 所示为 ARM940T 的 Cache 结构图。Cache 控制器把地址标签域作为 CAM 的输入,它的输出选择了包含有效 Cache 行的组。 标签 组索引 数据索引 处理器内核请求地址 Cache 控制器 CAM 组 选择逻辑 Cam3 Cam2 Cam1 Cam0 64 组 选择 Cache 组 Cache 专业始于专注 卓识源于远见       ‐  17  ‐            图 15.13 ARM940T64 路组相联 Cache 访问地址的标签部分被作为 4 个 CAM 的输入,输入标签的同时与存储在 64 组中的所有 Cache 标签比较。 如果有一个匹配,那么数据就由 Cache 寄存器提供;如果没有匹配,存储器就会产生一个失效(misss)信 号。 控制器使用组索引位(set index)在 4 个 CAM 中选择一个。被选中的 CAM 会在 Cache 存储器中选择一个 Cache 行,该地址的数据索引部分(data index)在该 Cache 行中选择出所需的字、半字或者字节。 15.3.5 Cache 的写策略 当 CPU 更新了 Cache 内容时,要将结果写回到主存中,通常有两种方法:  直写法(write-through);  回写法(write-back)。 直写法是指,当 CPU 在执行写操作时,必须把数据同时写入 Cache 和主存,以确保 Cache 和主存数据一 致。在这种写策略下,处理器在每次写 Cache 时也要写相应的主存单元。由于要访问主存,直写法的速度 比回写法要慢一些。 回写法是指,当处理器和写 Cache 命中时,只向 Cache 存储器写数据,而不立即写入主存。这样,主存储 器与相应的 Cache 行数据有可能不一致。Cache 中的数据是新的,而主存中的数据可能是较早的、没有被 更新过的。 配置成回写法的 Cache 要使用 Cache 行的状态信息块中的一个或多个脏位(dirty bit)。当回写 Cache 控制 器向 Cache 存储器中的某一行写入数据时,它会将脏位设置为 1。如果控制器内核此后访问该 Cache 行, 那么通过脏位的状态就可以知道该 Cache 行中含有主存储器中没有的数据。如果 Cache 控制器要将一个脏 位被设置的 Cache 行替换出 Cache 存储器,那么该 Cache 行数据会自动被写入主存单元中。控制器通过这 种方法来防止只存在于 Cache 中而主存中没有的重要信息的丢失。 表 15.12 比较了直写法和回写法的优缺点。 表 15.12 直写法与回写法 写 策 略 直 写 法 回 写 法 可靠性 高 低 与主存的通信量 多 少 控制的复杂性 简单 复杂 硬件实现代价 大 小 下面分析产生这些性能差异的原因。  可靠性。直写法要优于回写法。这是因为直写法始终保证 Cache 是主存的正确副本。当 Cache 发生错 误时,可以从主存中纠正。  与主存的通信量。一般情况下,回写法少于直写法。这是因为,一方面,Cache 的命中率很高,对于回 写法来说,CPU 绝大多数操作只需要写 Cache,不必写主存。另一方面,当 Cache 失效时,要将 Cache 中 的行替换到主存,而直写法每次只写一个字到主存。总的来说,由于直写法在每次写 Cache 时,同时写主 存,从而增加了写操作的开销。而回写法是把与主存的数据交换集中到一次主存操作,可能要一次性的进 行多个字的操作。  控制的复杂性。直写法必回写法简单。直写法在 Cache 的行状态表中不需要修改位。同时,直写法的 纠错技术相对简单。  硬件代价。回写法比直写法好。因为直写法中,每次写操作都要写主存,因此为了节省写主存所花费 的时间,通常要采用一个高速小容量的缓存存储器,把要写的数据和地址写到这个缓存中。在每次读主存 时,也要首先判断所读的数据是否在这个缓存中。而回写法不需要上述操作,相对硬件代价要小。 专业始于专注 卓识源于远见       ‐  18  ‐            15.3.6 Cache 的替换策略 在 Cache 访问过程中,发现查找的 Cache 行已经失效,则需要从主存中调入新的行到 Cache 中。在采用组 相联的 Cache 中,一个来自主存的行可以放入多个 Cache 组中。当所有组中的对应行都已经装满时,就要 使用 Cache 替换算法,从这些组中找出一个 Cache,把它调回到主存中原来存放它的地方,腾出新行来存 放新调入的行。被选中替换的 Cache 行被称为丢弃者(victim)。如果丢弃者中包含有效的脏数据,那么在 该行被写入新数据之前,控制器必须把该行中的数据写到主存。选择和替换丢弃 Cache 行的过程被称为淘 汰(eviction)。 Cache 控制器选择下一个丢弃 Cache 行的策略被称为替换策略。在 ARM 常用的替换算法有两种:轮转算 法和随机替换算法。 轮转算法又叫循环法,这种算法维护一个逻辑计数器,每进行一次替换,计算器加 1,当计算器达到最大值 时,就被复位成预先定义好的一个基值。这种算法容易预测最坏情况下的 Cache 性能。但它一个明显缺点就 是,在程序发生很小变化时,可能造成 Cache 性能急剧下降。 随机算法从特定的位置上随机地选出一行替换出去。它通过一个随机发生器来完成上述操作。当每次需要 替换 Cache 行时,随机发生器将产生一个随机数,用新行将编号为该随机数的行替换出去。这种算法与轮 转算法最大的区别在于它在每次产生替换行时,增加的是一个非连续值,这个值是由控制器随机产生的。 同样,当丢弃计算器达到最大值时,会被复位成预先定义好的一个基值。 相比之下,随机算法没有考虑到程序的局部性特点,因而效果有时不尽人意,同时这种算法不易预测最坏 情况下 Cache 性能。而轮转法就有更好的可预测性,容易预测最坏情况下 Cache 性能,在一些实时系统中, 十分重视这一点。但是,轮转法替换策略在存储器访问发生很小变化时,可能造成 Cache 性能有较大变化。 表 15.13 显示了目前比较流行的 ARM 核所使用的策略。 表 15.13 常见 ARM 核使用的替换策略 内 核 写 策 略 替 换 策 略 ARM720T 直写法 随机 ARM740T 直写法 随机 ARM920T 直写法、回写法 随机、轮转 ARM940T 直写法、回写法 随机 ARM926EJ-S 直写法、回写法 随机、轮转 ARM946E 直写法、回写法 随机、轮转 ARM1020E 直写法、回写法 随机、轮转 ARM1026EJS 直写法、回写法 随机、轮转 Intel Strong ARM 回写法 轮转 Intel Xscale 直写法 轮转 15.3.7 与 Cache 相关的编程接口 与 Cache 编程相关的 CP15 的寄存器共有 3 个,它们分别为 c1、c7 及 c9。 (1)寄存器 c1 中与 Cache 相关的位 c1 寄存器在前面 CP15 寄存器一节中已经介绍过,下面对 Cache 的控制位进行详细介绍。 表 15.14 显示了 c1 中与 Cache 有关位的作用。 表 15.14 c1 中与 Cache 相关的位 相 关 位 作 用 C(bit[2]) 当数据 Cache 和指令 Cache 分开时,本控制位禁止/使能数据 Cache 当数据 Cache 和指令 Cache 统一时,本控制位禁止/使能整个 Cache 专业始于专注 卓识源于远见       ‐  19  ‐            0:禁止 Cache 1:使能 Cache 如果系统中不含 Cache,读取时该位返回 0,写入时忽略该位 当系统中 Cache 不能禁止时,读取返回 1,写入时忽略该位 续表 相 关 位 作 用 I(bit[12]) 当数据 Cache 和指令 Cache 是分开的,本控制位禁止/使能指令 Cache 0:禁止指令 Cache 1:使能指令 Cache 如果系统中使用统一的指令 Cache 和数据 Cache 或者系统中不含 Cache,读取该位时 返回 0,写入时忽略该位 当系统中的指令 Cache 不能禁止时,读取该位返回 1,写入时忽略该位 RR(bit[14]) 如果系统中 Cache 的淘汰算法可以选择的话,本控制位选择淘汰算法 0:选择常规的淘汰算法,如随机淘汰算法 1:选择预测性的淘汰算法,如轮转(round-robin)淘汰算法 如果系统中淘汰算法不可选择,写入该位时被忽略,读取该位时,根据其淘汰算法 可以简单地预测最坏情况,并返回 1 或者 0 (2)寄存器 c7 CP15 中的寄存器 c7 主要用于控制 Cache 和写缓存。 注意 c7 有时也用于其他相似的功能,如果系统中存在预测缓存(prefetch buffers)和分支目标(branch target)Cache,c7 也将负责对它们进行控制。 c7 是一个只写存储器,可以使用协处理器指令 MCR 对其进行操作。如果程序中包含读 c7 的操作,那么指 令的结果不可预知。 使用 MCR 指令写该寄存器的命令格式如下所示。 MCR P15,0, 其中,CRm 和 opcode2 的不同组合,决定指令执行的不同操作。具体组合与操作的对应关系见表 15.15。 表 15.15 CRm 与 opcode2 不同组合与操作的应用关系 CRm Opcode2 含 义 数 据 c0 4 等待中断 0(SBZ,should be zero) c5 0 使整个指令 Cache 无效 0 c5 1 使指令 Cache 中某行无效 虚拟地址 c5 2 使指令 Cache 中某行无效 组号/索引 c5 4 清空2预取缓存区 0 c5 6 清空整个分支目标 Cache 0 c5 7 清空分支目标 Cache 中的某入口项 生产商定义 c6 0 使整个数据 Cache 无效 0 续表 CRm Opcode2 含 义 数 据 c6 1 使数据 Cache 中的某行无效 虚拟地址 2 这里要注意清空(flush)和清理(clean)的区别。 清空是指,清除 Cache 中存储的全部数据。对处理器而言,清空操作只要清零相应 Cache 行的有效位即可。有时也用术语 使无效(invalidate)来代替。 清理是指把脏的 Cache 行强行写到主存,并把 Cache 行中的脏位清零。 专业始于专注 卓识源于远见       ‐  20  ‐            c6 2 使数据 Cache 中的某行无效 组号/索引 c7 0 使整个统一 Cache 无效 哈佛结构中,使整个数据 Cache 和指令 Cache 无效 0 c7 1 使统一 Cache 中某行无效 虚拟地址 c7 2 使统一 Cache 中某行无效 组号/索引 c8 2 等待中断 0 c10 1 清理数据 Cache 行 虚拟地址 c10 2 清理数据 Cache 行 组号/索引 c10 4 清除写缓存区 0 c11 1 清理统一 Cache 行 虚拟地址 c11 2 清理统一 Cache 行 组号/索引 c13 1 预取指令 Cache 中的某行 虚拟地址 c14 1 清理并使数据 Cache 中的某行无效 虚拟地址 c14 2 清理并使数据 Cache 中的某行无效 组号/索引 c15 1 清理并使统一 Cache 中的某行无效 虚拟地址 c15 2 清理并使统一 Cache 中的某行无效 组号/索引 (3)寄存器 c9 将 Cache 进入存储系统的注意目的是要提高系统的平均访问速度。但 Cache 是一把双刃剑,在某些情况下, 可能使系统的性能更遭。下面列出了 3 种使 Cache 性能明显下降的原因。 ① Cache 访问未命中,处理器转向主存寻址数据,这期间的延时对系统性能影响很大。 ② 在回写型 Cache 中,如果 Cache 中的数据所在地址被存储管理单元重新定位(即 Cache 中存储的为虚 地址数据),那么数据回写的操作延时很大。 ③ 当处理器需要一个字节数据,而此数据恰好不在 Cache 中,那么 Cache 的替换策略就会将整个 Cache 行换进,增加了系统不必要的开销。 以上 3 点对实时系统来说,影响更为明显。 为了减少这种不利的影响,在 ARM 系统中引入了 Cache 内容锁定技术。这种技术允许编程人员人为地将 一些关键代码或数据预取到 Cache 中后,通过寄存器操作对其设定一定的属性,这样当有 Cache 未命中发 生,需要进行 Cache 替换时,将这些数据保护起来,使这些关键代码或数据不会被换出。 这种策略在很大程度上保证了处理器对关键代码或数据访问时的性能。 Cache 的锁定操作是分组块(block)为单位进行的,它的分块方法如下。 为了叙述方便,作下述假设。 L(Length of the Line):Cache 的基本存储单元行的大小。 A(Associativity):表示每个 Cache 组中的行数。 N(Number of Sets):Cache 中的组数。 M 表示 Cache 中的锁定块。 每个锁定块(lockdown block)包括 Cache 每组中的一行。这样 Cache 中共有 A 个锁定块,其编号为从 0 到 A-1。其中编号为 0 的锁定块中包含 Cache 组 0 中的 0 #行,组 1 中的 0 #行,直到组 A-1 中的 0#行。 依此类推,锁定块 1 包含 Cache 组 0 中的 1#行,组 1 中的 1#行,直到组 A-1 中的 1 #行。这样每个锁定块 中包含了 N 个 Cache 行。 当编号为 0~M 的锁定块被锁定在 Cache 中,编号为 M+1~A 的锁定块可以用于正常的 Cache 替换操作。 注意 编程中不能将全部 Cache 锁定,至少要留出一个未锁定的块来支持存储器的正常操作。 专业始于专注 卓识源于远见       ‐  21  ‐            每一个锁定块都包含有 N 个不同组中的 Cache 行。建议程序在使用 Cache 锁定时,使每个 Cache 块中的 N 个 Cache 行映射的为存储器中的连续地址。也就是说,存储器中 N×L 大小的联系区域被映射到 Cache 中 锁定,这块区域是 Cache 行边界对齐的(如果一个 Cache 行包含 4 字节,那么被锁定的区域要是 4 字节对 齐的,如果一个 Cache 行包含 8 字节,那么被锁定的 Cache 行就是 8 字节对齐的)。 在 ARM 的存储管理体系中,主要依靠系统协处理器和协处理器的寄存器 c9 来实现和管理 Cache 锁定。如 果系统中使用的是数据和指令分离的 Cache,那么就依靠协处理器指令 MCR 和 MRC 中的2 来区 分:  =0 使用数据 Cache 锁定寄存器;  =1 使用指令 Cache 锁定寄存器。 如果系统使用的是数据和指令统一的 Cache,那么要设置成 0。 另外,无论是 MCR 指令还是 MCR 指令,指令中的通常设为 c0。 寄存器 c9 有两种主要的格式:格式 A 和格式 B。 格式 A 的编码如图 15.14 所示。 Cache组内行号 UNP/SBZ 31 32-W 31-W 0 图 15.14 格式 A 编码 程序员通过指令对寄存器中的 Cache 组内行号进行操作。读取格式 A 的寄存器 c9,将返回最后一次写入 寄存器 c9 的值。将数据 index 写入寄存器 c9,就是对要锁定的 Cache 行进行设置。当用 MCR 指令向寄存 器写入数据时,执行以下操作。 ① 当下一次发生 Cache 未命中时,将预取的存储器行存入 Cache 中与该行相对应的组中编号为 index 的 Cache 行中。 ② 这时被锁定的 Cache 块包括序号为 0~index-1 的锁定块。当发生 Cache 替换时,从编号为 index 到 A -1 的块中选择被替换的块。 格式 B 的编码如图 15.15 所示。 程序员通过指令对寄存器中的 Cache 组内行号进行操作。读取格式 B 的寄存器 c9,将返回最后一次写入寄 存器 c9 的值。将数据 index 写入寄存器 c9,就是对要锁定的 Cache 行进行设置。当用 MCR 指令向寄存器 写入数据时,执行以下操作。 Cache组内行号 UNP/SBZ 31 30 W 0 L W-1 图 15.15 格式 B 编码 ① 当 L=0 时,如果方式 Cache 未命中,将预取的存储行存入 Cache 中与该行对应的组中序号为 index 的 Cache 行中。 ② 当 L=1 时,如果本次写操作之前 L=0,并且 index 值小于本次写入的 index,本次写操作执行的结果不 可预知;否则,这时被锁定的 Cache 块包括序号为 0~index-1 的块。当发生 Cache 替换时,从序号为 index~ A-1 的块中选择被替换的块。 下面以锁定块 N 来说明要锁定一个 Cache 块的步骤。 ① 首先确保在下面的整个 Cache 锁定过程不会被中断打断。如果程序要求中断不能关闭,那么必须确保 被打开的中断相关代码和数据位于非缓存(uncachable)的存储区域。 关中断的典型做法如下所示。 MRS r2,CPSR ;读出当前程序状态字 CPSR ORR r2,r2,# 0x000000C0 ;关中断 MSR CPSR_cxsf,r2 ;设置当前程序状态字 专业始于专注 卓识源于远见       ‐  22  ‐            ② 如果锁定是指令 Cache 或者统一 Cache,必须保证锁定过程所执行的代码位于非缓存的存储域。 ③ 如果锁定的是数据 Cache 或者统一的 Cache,必须保证锁定过程所执行的数据位于非缓存的存储域。 ④ 保证要锁定的代码和数据位于缓存的存储区域中。 ⑤ 如果要锁定的代码和数据不在 Cache 中,使用 Cache 清除或清理指令,将其置换到 Cache 中。 ⑥ N 次循环执行下面的操作。  index=I 写入寄存器 c9,当使用 B 格式的锁定寄存器时,令 L=0。  如果锁定的是数据 Cache 或数据和指令统一 Cache,使用 LDR 指令将数据从内存读出,这个读操作将 使要锁定的内容存在于 Cache 行中。  如果锁定的是指令 Cache,那么要借助 c7 寄存器,相关指令详细内容,参见 c7 寄存器一节。 ⑦ 将 index=N 写入寄存器 c9,当使用 B 格式的锁定寄存器时,令 L=0。 如果要解除对 N 锁定块的锁定,执行以下操作。  将 index=0 写入寄存器 c9。  当使用格式 B 的锁定寄存器时,令 L=0。 15.3.8 内存一致性 当一个系统中同时使用了 Cache、写缓存时,同一地址的数据可能同时出现在包括系统内存在内的多个不 同的物理位置中。如果 Cache 引入了哈佛架构,使用数据和指令分类的 Cache,那情况将更复杂。 由于上述存储系统的多样性特点,当从内存中读取数据时,不能保证读取的是数据的最新值(即有可能出 现下述情况:写操作将数据写入到 Cache 中,但更新数据还没有被回写到内存)。 ARM 存储系统中,数据不一致问题一方面可以通过存储系统自动保证解决,另一方面编写程序时要遵循 一定的规则,防止数据不一致性发生。 下面就几个常出现数据不一致的地方进行讨论。  地址映射发生变化时  指令和数据分离的 Cache  系统执行 DMA(Direct Memory Access)操作 (1)地址映射发生的变换 当系统中使用 MMU 时,Cache 行对应的地址可能是: ① 内存中的实际地址; ② 经过地址转换后的虚拟地址3。 如果查询 Cache 时相联地址比较使用的是虚拟地址,则当系统地址到物理地址的映射发生变换时,可能造 成 Cache 中数据与主存中的不一致。 同时,当系统中使用了写缓存,处理器对写缓存中的数据处理也是按虚拟地址进行的,所以同样会发生数 据不统一的问题。比如,当前处理器使用虚拟地址向某个内存单元写数据,该写操作已经将虚拟地址和数 据写入到写缓存区中,此时,虚拟地址到物理地址的映射关系发生变换,使先前要写入数据的虚拟地址发 生了变化,当写缓存将上面被延时的写操作写到主存时,使用的是变换后的地址,从而写操作执行失败。 为了避免发生这种数据不统一的情况,在系统虚拟地址到物理地址的映射关系发生变换前,根据系统的具 体情况,执行下面的操作序列中的一种或几种。 3 Cache 因所处的处理器内核的位置不同,可以将其分为逻辑 Cache 和物理 Cache。 逻辑 Cache 在虚拟地址空间存储数据,它位于处理器和 MMU 之间。处理器可以直接通过逻辑 Cache 访问数据,而无须通 过 MMU。逻辑 Cache 又被称为虚拟 Cache。 物理 Cache 使用物理地址存储数据,它位于 MMU 和主存之间。当处理器访问存储器时,MMU 必须先把虚拟地址转换成 物理地址,Cache 存储器才可以向内核提供数据。 带有 Cache 和 MMU 的 ARM 处理器中,从 ARM7 到 ARM10,包括 Intel StrongARM 和 Intel Xscale 处理器,都使用逻辑 Cache。ARM11 处理器(ARMv6 体系结构)系列使用物理 Cache。 专业始于专注 卓识源于远见       ‐  23  ‐             如果数据 Cache 为写回型 Cache,清空该数据 Cache。  使数据 Cache 中相应的行无效。  使指令 Cache 中相应的行无效。  将写缓存区中被延时的操作全部执行。  有些情况可能还要求相关的存储区域被置换成非缓存的。 (2)指令 Cache 当系统中采用分离的数据 Cache 和指令 Cache 时,下面的几种情况可能造成指令不一致情况的发生。 ① 地址为 A1 的指令被预取,该指令的数据行被取到 Cache 中。 ② 和 A1 同在一个数据行的地址为 A2 的数据被一条存储器写操作修改。这个数据写操作可能影响数据 Cache 中、写缓存中和主存的地址为 A2 的存储单元内容,但不影响指令 Cache 中地址为 A2 的存储单元中 的内容。 ③ 如果地址 A2 存放的是指令,当该指令执行时,就可能发生指令不一致问题。如果地址 A2 所在的行还 在指令 Cache 中,系统将执行修改前的指令;如果地址 A2 所在的行不在指令 Cache 中,地址将执行修改 后的指令。 为了避免这种指令不一致的情况发生,要在地址 A2 的数据被修改前执行一些防护性的操作。也就是说, 在步骤①和②之间插入下面必要的操作。  如果系统中使用的数据、指令统一的 Cache,程序跳到步骤②继续执行。  对于使用数据和指令分离 Cache 的系统,使指令 Cache 的内容无效。  对于使用数据和指令分离 Cache 的系统,如果数据 Cache 是写回类型的,清空数据 Cache。 上述操作系列可作为一种标准,应用于一些典型的场合。 注意 当可执行文件加载到主存中后,在程序跳转到入口点处开始执行之前,先执行上述操作序列, 以保证新加载的可执行代码正确执行。 (3)DMA 造成的数据不一致 DMA 操作直接访问内存,不更新 Cache 和写缓存区中相应内容,这样就很可能造成数据不一致。 为了避免 DMA 造成的数据不统一,根据系统情况,执行下面操作的一种和几种。  将 DMA 访问的存储器设置成非缓存的  将 DMA 访问的存储区所涉及的数据 Cache 中的行设置成无效,或者清空数据 Cache。  清空写缓存区(将写缓存区中延时操作全部执行)。  在 DMA 访问期间限制存储器访问 DMA 所访问的存储区域。 15.3.9 Cache 初始化子程序示例 下面给出了一段例子代码,此代码以 ARM740T 芯片为参考,显示了 Cache 初始化的标准过程。 ;下面代码必须运行于处理器的特权模式下。 AREA INIT740, CODE, READONLY ;设置段属性 ENTRY EXPORT Cache_Init ;以便作为子程序被其他程序使用 Cache_Init ;禁止 MMU/MPU ;清理数据 Cache ; 专业始于专注 卓识源于远见       ‐  24  ‐            ; MRC p15, 0, r0, c1, c0, 0 ;读 CP15 寄存器 c1 到 r0 BIC r0, r0, #0x1 ;清除 bit[0] MCR p15, 0, r0, c1, c0, 0 ;将设置的新值写回 MOV r0,#0 ;准备禁止其他域 MCR p15, 0, r0, c6, c1, 0 MCR p15, 0, r0, c6, c2, 0 MCR p15, 0, r0, c6, c3, 0 MCR p15, 0, r0, c6, c4, 0 ; MCR p15, 0, r0, c6, c5, 0 ; MCR p15, 0, r0, c6, c6, 0 ; MCR p15, 0, r0, c6, c7, 0 ; ; 区域 0:背景区:从 0x0 地址开始的 4GB 存储空间 ; 区域 1:SRAM 区:从 0x0 地址开始的 0x4000 字节存储空间 ; 区域 2:FLASH:从 0x24000000 开始的 0x02000000 字节存储空间 ; 区域 3:外设区:从 0x10000000 地址开始的 0x10000000 字节存储空间 ;开启 region 0 MOV r0,#2_111111 MCR p15, 0, r0, c6, c0, 0 ; region 0 区域 0 ; 开启 region 1 MOV r0,#2_100011 MCR p15, 0, r0, c6, c1, 0 ; region 1 区域 1 ; 开启 region 2 LDR r0,=2_110001+0x24000000 MCR p15, 0, r0, c6, c2, 0 ; region 2 区域 2 ; 开启 region 3 LDR r0,=2_110111 + 0x10000000 MCR p15, 0, r0, c6, c3, 0 ; region 3 区域 3 ; 开启 Cache/写缓存 MOV r0, #2_0110 MCR p15, 0, r0, c2, c0, 0 ;Cache MCR p15, 0, r0, c3, c0, 0 ;写缓存 ; 开启 access 允许 MOV r0, #2_11111100 MCR p15, 0, r0, c5, c0, 0 ;允许访问 ; ; 设置全局配置 ; MRC p15, 0, r0, c1, c0, 0 ;读 CP15 寄存器到 r0 专业始于专注 卓识源于远见       ‐  25  ‐            ORR r0, r0, #(0x1 <<2) ;开启 Cache ORR r0, r0, #0x1 ;开启 MPU ; ;增加的配置选项 ; ; ORR r0, r0, #(0x1 <<13) ;开启 Hi Vectors ; ORR r0, r0, #(0x1 <<7) ;开启大端模式 MCR p15, 0, r0, c1, c0, 0 ;写 CP15 寄存器 c1 MOV pc,lr ;返回 END 上述程序端可作为参考,可以在程序中直接使用,也可以作为子程序被其他程序调用。如果作为子程序调 用,下面两种调用方式作为参考。 ① 在汇编程序中调用。 IMPORT Cache_Init BL Cache_Init ② 在 C 语言中调用。 extern void Cache_Init(void); Cache_Init(); 15.4 存储保护单元 MPU 一些嵌入式系统使用多任务的操作和控制。这些系统必须提供一种机制来保证正在运行的任务不破 坏其他任务的操作。即要防止系统资源和其他一些任务不受非法访问。要达到这一目的通常有软件保护 和硬件保护两种途径。这里软件保护是指仅靠软件来保护系统资源。系统中无保护硬件或硬件没启动。 在多任务的系统中,通常要运行操作系统来达到任务间同步与通信。所以,这种软件的资源保护通常由 操作系统来完成。但这种通过软件来协调任务运行,保护系统资源的做法有时会出现一些不可避免的问 题。如当对一个通信用串口寄存器进行操作时,如果一个任务正在使用串口,则它没有办法来防止其他 任务使用同一个串口。因此,若要成功使用该串口,则必须通过一个访问该串口的系统调用来协调。使 用这些调用任务的非授权访问,很容易破坏经过该串口的通信。因此资源的不合理使用也许是不可避免 的。 相反,受保护系统有专门的硬件来检测和限制系统资源的访问。它能保证资源的所有权,任务需要遵守一 组由操作环境定义的、由硬件维护的规则,在硬件级上授予监视和控制资源程序的特殊权限。受保护系统 主动防止一个任务使用其他任务的资源。因此使用硬件主动监视系统比协调加强的软件历程,提供了更好 的保护。 ARM 中配备的有效保护系统资源的硬件,有两种:  MPU(Memory Protection Unit);  MMU(Memory Management Unit)。 MMU 是比 MPU 提供了功能更强大的内存保护机制,MPU 只提供了内存区域保护,而 MMU 是在此基础 上提供了虚拟地址映射技术,而且在操作上,MMU 要比 MPU 负责。本节主要讨论带 MPU 的处理器内核, MMU 将在下一节详细介绍。 专业始于专注 卓识源于远见       ‐  26  ‐            15.4.1 保护域(Protection Regions) ARM 处理器中的 MPU 使用“域(regions)”来对内存单元进行管理。域是与存储空间相关联的属性,处 理器核将这些数据保存在协处理器 CP15 的一些寄存器中。通常域的个数为 8 个,编号为从 0~7。 域的大小和起始地址保存在 CP15 的寄存器 c6 中。大小可以是 4KB~4GB 的任何 2 的乘幂。域的起始地 址必须是其大小的倍数。比如,一个定义为 4KB 的域其起始地址可以是 0x12345000,而一个大小定义为 8KB 的域起始地址只能是 0x2000 的倍数。 另外,操作系统可以为这些域分配更多的属性:访问权限、cache 和写缓存。存储器基于当时的处理器模 式(管理模式或用户模式)可以设定这些区域的访问权限为读/写、只读和不可访问。 当处理器访问主存的一个域时,MPU 比较该域的访问权限属性和当时的处理器模式。如果请求符合域的 访问标准,则 MPU 允许内核读/写主存;如果存储器请求不符号域的访问标准,将产生一个异常信号。 异常信号被送到处理器核。处理器核执行一个异常向量,然后跳转到异常处理程序,异常处理程序 判断异常类型为预取指或数据中止,然后根据异常类型,跳转到相应的服务例程。 对于 ARM 处理器,存储空间的某一部分可以被分配给一个以上的区域。也就是说域可以重叠。在重叠的 域内,可以设置域的优先级。在分配访问权限时重叠域比非重叠域有更大的灵活性。后面一节将会详细介 绍域的重叠。 15.4.2 内存访问顺序 当 ARM 处理器产生一个内存访问信号时,内存保护单位 MPU 将负责检查要访问的地址是否在被定义的 域中。 ① 如果地址不在任何域中,存储器产生异常。如果内核预取指令则 MPU 产生预取中止异常;如果是存储 器数据请求,则产生数据中止异常。 ② 如果地址在多个域内,由 MPU 判断域的有效级来决定存储区域的访问属性。访问属性可以在 CP15 的 寄存器中设定,可设定的位为 C(Cache)、B(Buffer)、AP(Access Permission)。这些属性的具体定义为:  C 和 B 可以控制 Cache 和写缓存属性的 Cache 策略。例如,可以设置一个域使用回写(write-back) 策略访问存储器,而另一个域则以无 Cache 和无写缓存方式访问;  AP(access permission)决定域是否可以被访问。如果在当前处理器模式下,该域不能被访问,MPU 将 产生一个存储器访问异常。 图 15.16 显示了一个存储器访问过程。 访问控制 逻辑 协处理器寄存器 c5 虚拟/物理地址 AP 判断 Abort C、B 位 选择域 协处理器寄存器 c2、c3 域优先级控制 域地址比较 Cache 行 预取逻辑 Cache 和 写缓存 ARM 内存 图 15.16 存储器访问过程 专业始于专注 卓识源于远见       ‐  27  ‐            15.4.3 使能 MPU 通过对协处理器 CP15 的寄存器 c1 中的 bit[0]置 1,可以使能存储器保护单元 MPU。在系统上电时,默认 状态是该位清零,所有保护域无效。 在使能 MPU 之前,至少一个域要被设定,而且该域的属性和访问权限要预先设定好。 注意 在数据和指令域分离的系统中,如 ARM940T,在指令和数据域中都要有一个有效域被预先设 定好。 另外,使能 MPU 的指令要设在有效的域中。如果在使能 MPU 之前,域的属性和访问权限没有设定,那么 系统的运行结果将不可预知。 当 MPU 无效(将协处理器 CP15 寄存器 r1 的 bit[0]置 0)时,整个内存区域都被处理器视为无 Cache、无 写缓存、无存储保护状态。 15.4.4 重叠域 域的定义在 MPU 的作用下可以重叠。当重叠的域被访问时,MPU 会判断域的优先权,决定使用那个域的 属性来操作重叠域。 域属性优先级的排列顺序为:域 7 的有效级最高,其次为域 6,域 0 的优先级最低。 【例 15.3】 假设将一个从 0x3000 起始的 4KB 地址空间定义为域 2,其访问属性 AP 定义为 0b10(AP=0b10,特权模 式读/写访问,用户模式只读)。 将起始地址为 0x0 的 16KB 地址空间定义为域 1,其访问属性 AP 定义为 0b01(AP=0b01,特权模式只读)。 系统域划分如图 15.17 所示。 0x4000 0x3000 0x0 域 2 域 1 0x3010 图 15.17 重叠域的访问 当处理器在用户模式下执行 Load 指令,从 0x3010 地址取数据时,0x3010 地址既在域 1 中也在域 2 中,因 为域 2 的属性优先级高于域 1,所有 MPU 执行域 2 的访问属性从 0x3010 地址取数据。域 2 是用户模式可 读,所以不会发生数据异常。 在分配访问权限时重叠区域比非重叠区域有更大的灵活性,它可以使内存的某个特定联系内存单位在程序 中担任背景的作用,用来给一块大存储空间分配相同的属性的低优先级域。其他具有较高优先级域的区域 与该背景域某些部分重叠,用来改变已定义的背景域的较小子集的属性。这样,具有较高优先级的域可以 改变背景域属性的子集。背景域可以用来保护一些睡眠状态的存储空间,使其不受非法访问,而此时由另 一个不同域控制下的背景域的其他部分可以处于活跃状态。 专业始于专注 卓识源于远见       ‐  28  ‐            15.4.5 与 MPU 相关的 CP15 寄存器 与 MPU 相关的协处理器寄存器主要是 c2,c3,c5 及 c6。另外还有寄存器 c1 中的 1 到 2 位。 (1)c1 中的 MPU 相关位 c1 的编码格式如图 15.18 所示。 SBZP/ UNP L4 RR V I Z F R S B L D P W C A M 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 31 图 15.18 协处理器寄存器 c1 编码格式 M(bit[0])控制控制 MPU 的使能。  M=0:禁止 MPU  M=1:使能 MPU A(bit[1])选择是否支持内存访问地址对齐检查。  B=0:禁止地址对齐检查  B=1:使能地址对齐检查 (2)c2 中的 MPU 相关位 c2 的编码格式如图 15.19 所示。 C 0 C 1 C 2 C 3 C 4 C 5 C 6 C 7 U N P / S B Z 0 1 2 3 4 5 6 7 3 1 图 15.19 协处理器寄存器 c2 编码格式 寄存器位 0~7 分别对应域 0~7 的 Cache 属性。位 8~31 应该设置成 0。 注意 在数据和指令分离的系统中,通过 MRC 和 MCR 指令的第二个操作数来决定读写 D -Cache 和 I-Cache 属性。 (3)c3 中的 MPU 相关位 c3 的编码格式如图 15.20 所示。 B 0 B 1 B 2 B 3 B 4 B 5 B 6 B 7 U N P / S B Z 0 1 2 3 4 5 6 7 3 1 B7 图 15.20 协处理器寄存器 c3 编码格式 寄存器位 0~7 分别对应域 0~7 的写缓存属性。位 8~31 应该设置成 0。 当用指令 MCR/MRC 对 c3 进行读写时,第二个操作数将被忽略,在指令要设置成 0。 当配置数据域时,域的 Cache 位和写缓存区位一起决定域的访问策略。写缓存位有两个用途:使能/禁止域 的写缓存和设置域的 Cache 写策略。域的 Cache 位控制写缓存位的作用。具体位分配见表 15.16。 表 15.16 Cache 位和写缓存位的分配策略 Cache 位 写缓存区位 域 属 性 C=0 B=0 禁止 Cache、禁止写缓存 C=0 B=1 禁止 Cache、使能写缓存 C=1 B=0 使能 Cache,域使用回写策略 专业始于专注 卓识源于远见       ‐  29  ‐            C=10 B=1 使能 Cache,域使用直写策略 (4)访问权限寄存器 c5 协处理器 CP15 的寄存器 c5 设置内存域的访问权限。 寄存器 c5 的编码格式如图 15.21 所示。 A P 0 U N P / S B Z 0 1 2 3 4 5 6 7 3 1 A P 1 A P 2 A P 3 A P 4 A P 5 A P 6 A P 7 8 9 1 0 1 1 1 2 1 3 1 4 1 5 1 6 图 15.21 寄存器 c5 的编码格式 读寄存器 c3 的 bits[15:0]存放域的 AP(access permission,访问权限),其中 bits[2n+1:2n]对于域 n 的访问 权限。AP 编码与访问权限的对应关系如表 15.17 所示。 表 15.17 AP 编码与访问权限的对应关系 AP 编码 管 理 者 用 户 00 不可访问 不可访问 01 读/写 不可访问 10 读/写 只读 11 读/写 读/写 对于 Arm940T、Arm940T 两个内核版本来说,使用 MRC 和 MCR 指令对其进行读写时,第二个协处理器 寄存器将被忽略,指令中以 c0 的形式出现。对于指令数据统一的域,第二操作数要设成 0,而对于数据和指令分离的系统,如果 opcode2=0,说明操作对数据域有效,如果 opcode=1,说明操作 对指令域有效。 注意 对于 Arm946E-S 和 Arm1026EJ-S 两个内核版本,它们的访问权限机制更复杂,采用的是扩 展 AP,扩展组 AP 位域编码支持两个增强的权限域,对其进行操作的 MRC 和 MCR 指令形式 更复杂,有关更详细的内容,请参加 Arm 公司的用户手册。 (5)域大小控制寄存器 c6 Arm 系统中通过写协处理器 c6 来定义域的大小,通过 MCR 指令中第二个操作寄存器赋不同的值来指示是 对哪个具体域进行操作。第二个操作寄存器取值为 c0~c7,分别对应域 0~域 7。 每个域的起始地址必须对齐到其大小的整数倍。比如,一个域的大小位 64KB,其起始地址可以是 0x10000 的整数倍的任何数。域的大小可以是 4KB~4GB 的 2 的任意乘幂。 寄存器 c6 的编码格式如图 15.22 所示。 E Size UNP/SBZP Base address 0 1 5 6 11 12 31 图 15.22 域大小控制寄存器 c6 编码格式 编码含义如表 15.18 所示 表 15.18 寄存器 c6 编码含义 位 名 称 对 应 位 注 释 起始地址 [31:12] 保护域的第一个字节起始地址,具体见表 2.18 SBZ [11:6] 必须设为 0 Size [5:1] 设 Size=N,则域尺寸为 2N+1,其中 11≤N≤31 E [0] 域使能,E=1 使能,E=0 禁止 专业始于专注 卓识源于远见       ‐  30  ‐            关于 c6 中 bits[31:12],因为域的起始要是域大小的整倍数,域最小为 4KB,所有域起始地址的 bits[11:0]通常 为 0,不用设置。具体 c6 中起始地址的设置和 c6 中 Size(bits[5:1])的对应关系如表 15.19 所示。 表 15.19 域尺寸编码 Size(bits[5:1]) 域 尺 寸 起始地址(bits[31:12]) 0b00000~0b01010 未定义 - 0b01011 4KB 无 0b01100 8KB bit[12]必须为 0 0b01101 16KB bits[13:12]必须为 0 0b01110 32KB bits[14:12]必须为 0 0b01111 64KB bits[15:12]必须为 0 0b10000 126KB bits[16:12]必须为 0 0b10001 256KB bits[17:12]必须为 0 0b10010 512KB bits[18:12]必须为 0 0b10011 1MB bits[19:12]必须为 0 0b10100 2MB bits[20:12]必须为 0 0b10101 4MB bits[21:12]必须为 0 0b10110 8MB bits[22:12]必须为 0 0b10111 16MB bits[23:12]必须为 0 0b11000 32MB bits[24:12]必须为 0 0b11001 64MB bits[25:12]必须为 0 0b11010 128MB bits[26:12]必须为 0 0b11011 256MB bits[27:12]必须为 0 0b11100 512MB bits[28:12]必须为 0 0b11101 1GB bits[29:12]必须为 0 续表 Size(bits[5:1]) 域 尺 寸 起始地址(bits[31:12]) 0b11110 2GB bits[30:12]必须为 0 0b11111 4GB bits[31:12]必须为 0 15.5 存储管理单元 MMU 在创建多任务嵌入式系统时,最好有一个简单的方式来编写、装载及运行各自独立的任务。目前大多数的 嵌入式系统不再使用自己定制的控制系统,而使用操作系统来简化这个过程。较高级的操作系统采用基于 硬件的存储管理单元 MMU 来实现上述操作。 MMU 提供的一个关键服务是使各个任务作为各自独立的程序在其自己的私有存储空间中运行。在带 MMU 的操作系统控制下,运行的任务无须知道其他与之无关的任务的存储需求情况,这就简化了各个任务的设 计。 MMU 提供了一些资源以允许使用虚拟存储器(将系统物理存储器重新编址,可将其看成一个独立于系统 物理存储器的存储空间)。MMU 作为转换器,将程序和数据的虚拟地址(编译时的连接地址)转换成实际 的物理地址,即在物理主存中的地址。这个转换过程允许运行的多个程序使用相同的虚拟地址,而各自存 储在物理存储器的不同位置。 专业始于专注 卓识源于远见       ‐  31  ‐            这样存储器就有两种类型的地址:虚拟地址和物理地址。虚拟地址由编译器和连接器在定位程序时分配; 物理地址用来访问实际的主存硬件模块(物理上程序存在的区域)。 15.5.1 MMU 概述 内存管理单位 MMU 对处理器内存提供了很好的管理。这种管理主要是通过一个叫作传输表的数据结构来 实现的。这个传输表存在于内存中,它有多个称为 Entry 的入口,每个入口定义了存储空间的一个页,页 的大小从 1KB 到 1MB,同时定义了这些页的属性。 ARM 系统中,MMU 主要完成以下工作: ① 虚拟存储空间到物理存储空间的映射,它能够实现从虚拟地址到物理地址的转换; ② 存储器访问权限的控制; ③ 设置虚拟存储空间的缓存特性。 MMU 通过它的协处理器寄存器来确定传输表在内存中的位置,并通过这些寄存器来向 ARM 处理器提供 内存访问错误信息。 从虚拟地址到物理地址的变换过程是查询传输表的过程,由于传输表放在内存中,这个查询过程通常代价 很大。这个访问时间通常是 1~2 个内存周期。为了减少平均内存访问时间,ARM 结构体系中采用一个容 量更小(通常为 8~16 个字)、访问速度和 CPU 中通用寄存器相当的存储器件来存放当前访问需要的地址 变换条目,它是一个小容量的 Cache。这个小容量的页表 Cache 称为 TLB(Translation Lookaside Buffer)。 注意 如果系统中使用数据和指令统一存储系统,那么 TLB 也将是统一的。如果系统是数据和指令分 开的存储系统,那么 TLB 也将分为数据 TLB 和指令 TLB。 MMU 可以将整个存储空间分为最多 16 个域(domain)。每个域对应一定的内存区域,该内存区域具有相 同的访问控制属性。MMU 中寄存器 c3 用于控制与域有关的属性配置。 表 15.20 列出了与 MMU 有关的协处理器寄存器及其作用。 表 15.20 与 MMU 有关的协处理器寄存器 协处理器寄存器 作 用 c1 中某些位 配置 MMU 中的一些操作 c2 保存内存中页表基地址 c3 设置域访问权限 c4 保留 c5 内存访问失效状态标准 c6 内存访问失效时失效地址 c8 控制与清除 TLB 内容相关的操作 c10 控制与锁定 TLB 内容相关的操作 15.5.2 MMU 与 MPU 在 ARM 体系结构中,MMU 将 MPU 的功能大大地增加,使系统内存管理更加灵活、方便。在 MPU 中 引入了“域”的概念来管理内存,而且域是在专用寄存器中设置的。而 MMU 将域设置从寄存器移到了 内存单位,这样使域的设置更加灵活,但同时也增加了系统访问时间。 专业始于专注 卓识源于远见       ‐  32  ‐            另外,除了提供内存保护功能外,MMU 还增加了虚拟地址到物理地址的映射。在只有 MPU 的系统中, 每个任务被编译和运行在彼此不同的、固定的主存地址空间,每个任务只能在一个进程空间中运行,任何 两个任务都不能在主存中有重叠地址。为了运行一个任务,一个保护区域被设置在固定地址的程序上,以 允许任务访问由该区域定义的一段存储空间。保护区域的放置使得该任务得以运行,而其他任务空间被保 护。 而使用 MMU 中虚拟地址到物理地址的映射功能,即使任务被编译、连接、运行在主存中有重叠地址 的区域中,它们仍然可以运行。MMU 中对虚存的支持可使构建后的嵌入式系统具有多个虚拟存储映 射和单个物理存储器映射。每个任务拥有自己的虚拟存储器映射,以编译和连接组成此任务的代码和 数据。内核层管理各个任务在物理存储器中的放置,使得它们在物理存储器中拥有彼此不同的地址, 这个地址与其设计时的虚拟运行地址不一样。 15.5.3 内存访问过程 当处理器产生一个内存访问请求时,将传输一个虚拟地址给 MMU,MMU 首先遍历 TLB(如果使用分离 的存储系统,它将分别遍历数据 TLB 和指令 TLB)。如果 TLB 中不保护虚拟地址入口(Entry),那么它将 转入保存在内存中的传输主表,来获得所有访问地址的物理地址和访问权限。一旦访问成功,它将新的虚 拟地址入口(Entry)信息保存在 TLB 中,以备下次查询使用。 当得到了地址变换入口(Entry)后,将进行以下操作: ① 根据入口(Entry)中的 C(cachable)控制位和 B(Bufferable)控制位决定是否缓存该内存访问结果。 ② 根据访问权限控制位和域访问控制位确定该内存访问是否被允许。如果该内存访问不被允许,CP15 向 ARM 处理器报告存储访问中止。 ③ 对应不允许缓存的存储访问,直接得到物理地址访问内存。对于允许缓存的存储访问,如果在 Cache 命中,则忽略物理地址;如果 Cache 没有命中,则使用物理地址访问内存,并把该数据块读到 Cache 中。 图 15.23 为带 Cache 的 MMU 存储访问示意图。 ARM Cache和写缓存 Cache内容获取 硬件 主存储系统 页表遍历硬件 TLB 访问权限控 制硬件 虚拟 地址 C位、B位 物理地址 访问 控制 域控 制位 图 15.23 带 Cache 的 MMU 存储访问示意图 15.5.4 MMU 的使能与禁止 MMU 的使能/禁止可以通过 CP15 寄存器的 c1 的 bit[0]来控制。 专业始于专注 卓识源于远见       ‐  33  ‐             bit[0]=0,MMU 禁止。  bit[0]=1,MMU 使能。 下面的例子显示了典型的 MMU 使能过程。 【例 15.4】典型的 MMU 使能过程。 MRC p15,0,r0,c1,0,0 ORR r0,#01 MCR p15,0,r0,c1,0,0 当 MMU 被禁止时,存储访问执行下列过程。 ① 当禁止 MMU 时,存储系统是否支持 Cache 和写缓存,根据不同芯片设计不同而有所不同(ARM 公司 将设计权交给芯片厂商)。  如果芯片规定当禁止 MMU 时禁止 Cache 和写缓存,则存储访问不考虑 C、B 控制位。  如果芯片规定禁止 MMU 时使能 Cache 和写缓存,则数据访问被视为无 Cache(uncachable)和写缓存 (unbufferable)的,即 C=0、B=0。读取指令时,如果系统是统一的 TLB,则 C=0;如果使用分开的 TLB, 则 C=1。 ② 存储访问不受权限控制,MMU 也不会产生存储访问中止信号。 ③ 所有物理地址和虚拟地址相等,即使用平板存储模式。 使能/禁止 MMU 时需要注意以下几个问题。  在使能 MMU 之前,正确的传输表要在内存中事先建立,CP15 的相关寄存器必须完成初始化操作。  如果使用的不是平板存储模式(物理地址和对应虚拟地址相等),在禁止/使能 MMU 时,虚拟地址和物 理地址的对应关系发生变化,这时应该清除(Flush)Cache 中的当前地址变换入口(Entry)。  如果完成禁止/使能 MMU 的代码的物理地址和虚拟地址不同,则禁止/使能 MMU 将带来很大麻烦,因 此建议完成使能/禁止 MMU 的代码的物理地址和虚拟地址相同。 15.5.5 虚拟地址到物理地址的转换 (1)地址重定位 为了使任务有各自的虚拟存储器映射,MMU 硬件采用地址重定位,在地址访问主存之前,转换有处理器 输出的虚拟地址。当处理器产生一个虚拟地址时,MMU 取出这个虚拟地址的高位,遍历传输表,从而形 成一个物理地址。 虚拟存储空间到物理存储空间的映射是以内存块为单位进行的。也就是说,虚拟存储空间中一块连续的存 储空间被映射到物理存储空间中同样大小的一块连续存储空间。 虚拟存储空间到物理存储空间地址重映射过程如图 15.24 所示。 虚拟存储器 MMU地址重 定位 物理存储器 内存块1 0x80000000 内存块1 0x20000000 图 15.24 虚拟存储空间到物理存储空间地址重映射过程 ARM 支持的存储块的大小有以下几种。 专业始于专注 卓识源于远见       ‐  34  ‐             段(Sections):大小为 1M 的存储块。  大页(Large pages):大小为 64KB。  小页(Small pages):大小为 4KB。  极小页(Tiny Pages):大小为 1KB。 段和大页只需通过一次映射就可以将虚拟地址转换成物理地址,也可以根据需要增加一级映射,采用两级 映射的方式再将大页分成 16KB 的子页,小页分成 1KB 的子页。极小页不能再分,只能以 1KB 大小的整 页为单位。 ARM 在内存中存在两级页表以实现上述地址映射过程。  一级页表:一级页表包括两种类型的页表项,即保持指向二级页表起始地址的页表项和保存用于转换 段(Section)地址的页表项。一级页表也称为段页表(section page table)。  二级页表:二级页表包含以大页和小页为单位的地址变换页表项。 一级页表将 4G 地址空间划分为多个 1MB 的段(Section),因此一级页表包含 4096 个页表项。一级页表是 一个混合表,可以作为二级页表的目录表,也可以作为用于转换 1MB 段(也可视为 1MB 的虚拟页)的普 通页表。当一级页表作为页目录时,其页表项包含的是代表 1MB 虚拟空间的二级页表指针。二级页表分 为粗页表(Coarse)和细页表(Fine)。当一级页表用于转换一个 1MB 的段时,其页表项包含的是物理存 储器中对应 1MB 页帧(page frame)的首地址。 注意 目录页表项和 1MB 的段页表项可以共存于一级页表中。 一个粗二级页表(Coarse)包含 256 个页表项,占有 1KB 的主存空间,每个页表项将一个 4KB 的虚拟存 储块转换成一个 4KB 的物理存储块。粗二级页表支持 4KB 和 64KB 的页,页表项包含的是 4KB 或 64KB 的页帧地址。如果转换的是一个 64KB 的页,则对于每个 64KB 的页,同一个页表项必须在页表中重复 16 次。 一个细二级页表(Fine)有 1024 个页表项,占有 4KB 的主存空间,每个页表项转换一个 1KB 的存储块。 细页表支持 1KB、4KB、64KB 虚存页,每个页表项包含 1KB、4KB 或 64KB 的物理页帧首地址。如果转 换的是 4KB 的页,则同一个页表项必须在页表中连续重复 4 次;如果转换的是 64KB 的页,则同一个页表 项需要在页表中连续重复 64 次。 一级页表和二级页表的特征如表 15.21 所示。 表 15.21 一级页表和二级页表特征 类 型 页表占用的存储空间 (单位:KB) 支持的页大小 (单位:KB) 页表项数目 一级页表 16 1024 4096 粗二级页表 1 1,4,64 1024 细二级页表 4 1,4,64 256 (2)传输表基地址 当处理器发出地址请求信号,而其要求的虚拟地址没有包含在 TLB 中时,MMU 将会初始化一个产生过程。 传输过程需要的地址转换表——传输表的基地址存放在协处理器寄存器 c2 中,MMU 通过此基地址找到传 输表,准备一次地址传输过程。 (3)基于一级页表的地址变换过程 基于一级页表的地址变换过程是指从虚拟地址到物理地址的转换只需要一级页表就能完成的地址转换。一 级页表地址转换过程如图 15.25 所示。 专业始于专注 卓识源于远见       ‐  35  ‐            图 15.25 一级页表地址转换过程 图 15.25 中,CP15 寄存器 c2 中存放的是内存中一级页表的基地址。因为一级页表大小为 16KB,也就是说, 一级页表是 16KB 地址对齐的,所以 c2 中 bits[13∶0]=0,bits[31∶14]为内存中页表基地址。 CP15 的寄存器 c2 的 bits[31∶14]和虚拟地址的 bits[31∶20]结合作为一个 31 位数的高 30 位值,忽略 32 位 值的最后两位,可以使用该值从页表中查到一个 4 字节的地址页表项。 一级页表支持以下 4 种类型的页表项。  1MB 段转换项;  指向细二级页表的目录项;  指向粗二级页表的目录项;  产生中止异常的错误项。 系统通过页表项的低两位 bits[1:0]来确定页表项的类型。页表项的格式要求二级页表的地址必须与其页大 小的倍数对齐。一级页表的各种页表项的格式如图 15.26 所示。 段 0 1 B C 1 域 0 AP 0 段基地址 0 1 2 3 4 5 8 9 10 11 12 19 20 31 段 0 1 B C 1 域 0 AP 0 段基地址 0 1 2 3 4 5 8 9 10 11 12 19 20 31 段 0 1 B C 1 域 0 AP 0 段基地址 0 1 2 3 4 5 8 9 10 11 12 19 20 31 段 0 1 B C 1 域 0 AP 0 段基地址 0 1 2 3 4 5 8 9 10 11 12 19 20 31 图 15.26 一级页表项 如果 bits[1:0]=0b10 时,该页表项为段描述符(Section Descriptor),段描述符定义了对应的 1MB 的虚拟存 储空间的地址映射关系。 如果 bits[1:0]=0b01 时,该页表项包含了粗二级页表的物理地址。该粗二级页表定义了对应的 1MB 虚拟存 储空间的地址映射关系。它可以实现以大页和小页为单位的地址映射。 如果 bits[1:0]=0b11 时,该页表项包含了细二级页表的物理地址。该细二级页表定义了对应的 1MB 虚拟存储空 间的地址映射关系。它可以实现以大页、小页和极小页为单位的地址映射。 专业始于专注 卓识源于远见       ‐  36  ‐            如果 bits[1:0]=0b00 时,说明此页表项是一个错误页表项。它将产生一个存储页错误。错误条件会导致预 取指令中止或数据中止,这取决于具体的存储器访问类型。 (4)段描述符及其地址变换过程 如果一级页表页表项的 bits[1∶0]=0b10,说明此页表项指向一个 1MB 的存储段。页表项的高 12 位代替虚 拟地址的高 12 位来产生物理地址。该页表项还包含域属性、Cahche 属性、缓冲器属性和访问权限属性。 具体定义如表 15.22 所示。 表 15.22 段页表项中各字段含义 字 段 含 义 bits[1:0] 段页表项标识 bits[3:2] 定义段的 Cache 和写缓存属性 bit[4] 生产商定义 bits[8:5] 本段所在的域 bit[9] 当前未被使用,设置成 0 bits[11:10] 访问权限控制 AP 位,见表 15.23 bits[19:12] 当前未被使用,设置成 0 bits[31:20] 该段对应的物理空间基地址的高 12 位 表 15.23 访问权限控制位的编码及其含义 访问权限控制 AP S R 特 权 模 式 用 户 模 式 0b00 0 0 不可访问 不可访问 0b00 1 0 只读 不可访问 0b00 0 1 只读 只读 0b00 1 1 不可预知 不可预知 0b01 X X 读/写 无访问 0b10 X X 读/写 只读 0b11 X X 读/写 读/写 表中 S 和 R 位是 CP15 寄存器 c1 中的控制位,它们分别对应系统(S)和 ROM(R)位。这两位用来在不 同模式加速系统中访问大的存储块。 设置 S 位使得所有页具有不可访问权限,从而允许特权模式任务对页有读访问权限。因此通过改变 CP15 寄存器 c1 中的一位,所有标识为不可访问的空间一下子变为可用,而不需要改变每个页表项的 AP 位,节 省了开销。 改变 R 位使得所有页具有不可访问权限,因而特权模式任务和用户模式任务对页都有读访问权限。同样, 这一位可以加速对大块存储块的访问,而不需要修改许多页表项的值。 注意 地址转换过程中,在物理地址产生之前,将会按表 2.22 的编码对访问地址的权限进行检测。 基于段的地址变换过程如图 15.27 所示。 专业始于专注 卓识源于远见       ‐  37  ‐            图 15.27 基于段的地址变换过程 (5)粗二级页表描述符及其地址变换过程 如果一级页表项的 bits[1∶0]=0b01,说明此页表项包含一个粗二级页表首地址指针,同时还包含一级页表 项代表的 1MB 虚存段的域信息。粗页表必须与 1KB 的倍数地址对齐。页表项具体定义见表 15.24。 表 15.24 粗二级页表项中各字段含义 字 段 含 义 bits[1:0] 粗二级页表描述符标识 bits[4:2] 生产商定义 bits[8:5] 域标识符 bit [9] 当前未被使用,设置成 0 bits[31:10] 粗二级页表基地址,该地址 1KB 对齐 基于粗二级页表的地址变换过程如图 15.28 所示。 (6)细二级页表描述符及其地址变换过程 如果一级页表项的 bits[1:0]=0b11,说明此页表项包含一个细二级页表首地址指针,同时还包含一级页表项 代表的 1MB 虚存段的域信息。细页表必须与 4KB 的倍数地址对齐。页表项具体定义如表 15.25 所示。 专业始于专注 卓识源于远见       ‐  38  ‐            图 15.28 基于粗二级页表的地址变换过程 表 15.25 细二级页表项中各字段含义 字 段 含 义 bits[1:0] 细二级页表描述符标识 bits[4:2] 生产商定义 bits[8:5] 域标识符 bits [11:9] 当前未被使用,设置成 0 bits[31:12] 细二级页表基地址,该地址 4KB 对齐 基于细二级页表的地址变换过程如图 15.29 所示。 (7)基于二级页表的地址变换过程 二级页表有 4 种可能的页表项:  定义 64KB 页帧属性的大(Large)页表项;  定义 4KB 页帧属性的小(Small)页表项;  定义 1KB 页帧属性的微(tiny)页表项;  访问中止异常的错误项。 系统通过页表项的最低位[1:0]来确定页表项的类型。二级页表的页表项格式如图 15.30 所示。 当 bits[1:0]=0b01 时,该页表项为大页表项,它包含了一个 64KB 物理存储块的基地址。如果页表是细二级 页表,那么大页表项将在表中重复 64 次;如果页表是粗二级页表,那么大页表项将在表中重复 16 次。 图 15.29 基于细二级页表的地址变换过程 专业始于专注 卓识源于远见       ‐  39  ‐            图 15.30 二级页表的页表项格式 当 bits[1:0]=0b10 时,该页表项为小页表项,它保存一个 4KB 物理存储块的基地址。如果页表是细二级页 表,那么小页表项将在表中重复 4 次;如果页表是粗二级页表,那么大页表项只需在表中出现 1 次。 当 bits[1:0]=0b11 时,该页表项为微页表项,它保存一个 1KB 物理存储块的基地址。如果页表是细二级页 表,那么微页表项只需在表中重复 1 次;微页表项不会出现在粗二级页表中,如果出现,那么访问结果不 可预知。 当 bits[1:0]=0b00 时,该页表项产生存储页访问错误。错误条件会导致预取指中止或数据中止,这取决于 具体的存储器访问类型。 (8)大页表描述符及其地址变换过程 如果二级页表项 bits[1:0]=0b01,说明该页表项为大页表项,它不仅包含了一个 64KB 物理存储块基地址, 同时还含有 4 组权限位域以及页的 Cache 和写缓存属性。每一组访问权限域代表虚存页的 1/4,这些页表 项可以看成是 16KB 子页,以更好的控制 64KB 页的访问权限。 具体定义如表 15.26 所示。 表 15.26 大页表项中各字段含义 字 段 含 义 bits[1:0] 大页表项类型标识符 bits[3:2] Cache 和写缓存属性 bits[11:4] 访问权限控制位,具体编码见表 15.27。 一个大页分为 4 个子页 AP0 子页 0 的访问权限 AP1 子页 1 的访问权限 AP2 子页 2 的访问权限 AP3 子页 3 的访问权限 bits [15:12] 当前未使用,应为 0 bits[31:16] 该大页对应的物理页帧的基地址的高 16 位 图 15.31 说明了基于大页表的地址变换过程。 专业始于专注 卓识源于远见       ‐  40  ‐            图 15.31 基于大页表的地址变换过程 (9)小页表描述符及其地址变换过程 如果二级页表项 bits[1∶0]=0b10,说明该页表项为小页表项,它不仅包含了一个 4KB 物理存储块基地址, 同时还含有 4 组权限位域以及页的 Cache 和写缓存属性。每一组访问权限域代表虚存页的 1/4,这些页表 项可以看成是 1KB 子页,以更好的控制 4KB 页的访问权限。 页表项的具体定义如表 15.27 所示。 表 15.27 小页表项中各字段含义 字 段 含 义 bits[1:0] 小页表项类型标识符 bits[3:2] Cache 和写缓存属性 bits[11:4] 访问权限控制位,具体编码见表 15.22。 一个小页分为 4 个子页 AP0 子页 0 的访问权限 AP1 子页 1 的访问权限 AP2 子页 2 的访问权限 AP3 子页 3 的访问权限 bits [15:12] 当前未使用,应为 0 bits[31:16] 该小页对应的物理页帧的基地址的高 20 位 图 15.32 说明了基于小页表的地址变换过程。 图 15.32 基于小页表的地址变换过程 (10)微页表描述符及其地址变换过程 如果二级页表项 bits[1∶0]=0b11,该二级页表项是微页表项,它提供了一个 1KB 物理存储块的基地址,同 时含有一个访问权限位域以及页的 Cache 和写缓存属性。微页表项的具体含义如表 15.28 所示。 表 15.28 微页表项中各字段含义 字 段 含 义 专业始于专注 卓识源于远见       ‐  41  ‐            bits[1:0] 微页表项类型标识符 bits[3:2] Cache 和写缓存属性 bits[5:4] 访问权限控制位,具体编码见表 15.29 bits [9:6] 当前未使用,应为 0 bits[31:10] 该微页对应的物理页帧的基地址的高 22 位 图 15.33 说明了基于微页表的地址变换过程。 图 15.33 基于微页表的地址变换过程 注意 ARMv6 体系结构不包含微页,如果打算创建一个很容易移植到以后体系结构的系统,则建议 在该系统中避免使用 1KB 微页。 15.5.6 域(domain)和存储器访问权限 域指的是一些段、大页或者小页的集合。编程的中,设计者最多可以使用 16 个域,每个域的访问控制特 征由 CP15 中的 c3 中的两位控制。 CP15 中的寄存器 c3 的格式如图 15.34。 专业始于专注 卓识源于远见       ‐  42  ‐            图 15.34 CP15 寄存器 c3 编码格式 其中,每两个位控制一个域的访问控制特性,其编码及对应的含义如表 15.29 所示。 表 15.29 域访问控制字段编码及含义 控制位编码 访问类型 含 义 0b00 无访问权限 这时访问该域将产生访问失效 0b01 客户类型(client) 根据页表中地址变换页表项的域访问权限控制位决定是否允 许特定的存储访问 0b10 保留 使用该值会产生不可预知的结果 0b11 管理者权限(Manager) 不考虑页表中页表项内的访问控制权限位,所以这种情况下 不产生访问失效 综上所述,有两种不同的控制来管理一个任务的存储器访问权限。  管理者(manager)用于主控(primary control),不考虑每个段、大页和小页的访问权限。  客户(client)使用页表中的访问权限用于次控(secondary control)。 当多个段或者页从属于一个域时,这些段或者页的访问权限可以很容易的由域来统一控制。存储器采用这 种管理策略将不同的存储单元“打包”。 注意 即使不使用 MMU 提供的虚拟存储功能,仍然可以把这些内核用作简单的存储保护单元。首先 将虚拟存储空间直接映射到物理存储空间,然后为每个任务分配一个不同的域,最后使用这些 域来保护睡眠任务(通过将它们的域访问设置成不可访问)。 15.5.7 与 TLB 相关的操作 (1)清除 TLB 如果操作系统改变了页表中的数据,那么缓存在 TLB 中的转换数据可能就不再有效了。存储器核有一些 CP15 命令用于清除 TLB,从而使 TLB 中的数据作废。表 15.30 是一些可用的命令:清除所有 TLB 数据, 清除指令 TLB,清除数据 TLB,也可以一次只清除一行 TLB 数据。 表 15.30 清除 TLB 的 CP15 命令 命 令 MCR 指令 Rd 的值 支持的内核 使所有 TLB 无效 MCR p15,0,Rd, c8,c7,0 0 ARM720T 、 ARM920T 、 ARM922T 、 ARM926EJ-S、ARM1022E、ARM1026EJ-S、 StrongARM、Xscale 按行使 TLB 无效 MCR p15,0,Rd, c8,c7,1 要使之无效的虚 拟地址 ARM720T 使指令 TLB 无效 MCR p15,0,Rd, c8,c5,0 要使之无效的虚 拟地址 ARM920T 、 ARM922T 、 ARM926EJ-S 、 ARM1022E、ARM1026EJ-S、StrongARM、Xscale 按行使指令 TLB 无效 MCR p15,0,Rd, c8,c5,1 要使之无效的虚 拟地址 ARM920T 、 ARM922T 、 ARM926EJ-S 、 ARM1022E、ARM1026EJ-S、StrongARM、Xscale 使数据 TLB 无效 MCR p15,0,Rd, c8,c6,0 要使之无效的虚 拟地址 ARM920T 、 ARM922T 、 ARM926EJ-S 、 ARM1022E、ARM1026EJ-S、StrongARM、Xscale 按行使数据 TLB 无效 MCR p15,0,Rd, c8,c6,1 要使之无效的虚 拟地址 ARM920T 、 ARM922T 、 ARM926EJ-S 、 ARM1022E、ARM1026EJ-S、StrongARM、Xscale 下面的例子显示了一个使 TLB 无效的过程。 【例】一个使 TLB 无效的过程。 MOV r1,0; 专业始于专注 卓识源于远见       ‐  43  ‐            MCR p15,0,r1,c8,c7,0 (2)锁定 TLB 由于对 TLB 表的查询经常会使系统访问内存(要查询的段、页不在 TLB 中),这就使得系统的平均访问时 间大大增加。对于实时系统,就需要将一些关键的页表项锁定在访问速度相对较快的 TLB 中。 ARM920T、ARM922T、ARM926EJ-S、ARM1022E 和 ARM1026EJ-S 内核版本支持 TLB 转换数据的锁定。 如果 TLB 中的某一行是锁定的,则当 TLB 清除命令发出时,它仍然保留在 TLB 中。 与 TLB 锁定相关的操作可以通过对 CP15 寄存器 r10 编程来实现。 各种 ARM 核的可用锁定命令如表 15.31 所示。 表 15.31 访问 TLB 锁定寄存器的命令 命 令 MCR 指令 Rd 的值 支持的内核 读数据 TLB 锁定寄存器 MRC p15,0,Rd, c10,c0,0 TLB 锁定 ARM920T、ARM922T、ARM926EJ-S、ARM1022E、 ARM1026EJ-S、StrongARM、Xscale 写数据 TLB 锁定寄存器 MCR p15,0,Rd, c10,c7,1 TLB 锁定 ARM920T、ARM922T、ARM926EJ-S、ARM1022E、 ARM1026EJ-S、StrongARM、Xscale 读指令 TLB 锁定寄存器 MRC p15,0,Rd, c8,c5,0 TLB 锁定 ARM920T、ARM922T、ARM926EJ-S、ARM1022E、 ARM1026EJ-S、StrongARM、Xscale 写指令 TLB 锁定寄存器 MCR p15,0,Rd, c8,c5,1 TLB 锁定 ARM920T、ARM922T、ARM926EJ-S、ARM1022E、 ARM1026EJ-S、StrongARM、Xscale 其中 Rd 的格式如图 15.35 所示。 31 0 32-W 31-W 32-2W 31-2W 1 base victim UNP/SBZP P 图 15.35 Rd 格式详解 其中,  W=log2N,N 为 TLB 中入口(entry)的个数。对 ARM920T、ARM922T、ARM926EJ-S、ARM1022E 版本的内核来讲,W=6;而对于 ARM1026EJ-S 内核版本,W=3。  victm 位域:确定下次被换出的 TLB 入口(entry)。  base 位域:从第 0 个入口(entry)到 base −1 入口的 TLB 值,被锁定。 锁定 TLB 中 N 条地址入口的操作序列如下。 ① 确保在整个锁定过程中不会发生异常中断,可以通过禁止中断等方法实现。 ② 如果锁定的是指令 TLB 或指令/数据统一的 TLB,将 base=N、victim=N、P=0 写入寄存器 c10。 ③ 使整个将要锁定的 TLB 无效。 ④ 如果要锁定指令 TLB,确保与锁定过程有关的指令地址变换地址入口已经加载到指令 TLB 中。 注意 在此过程中,TLB 的一个地址变换入口可以涵盖所有与锁定 TLB 相关的指令。这通常是由使 整个 TLB 无效后的第一条指令实现的。 如果要锁定的是数据 TLB,确保与锁定过程有关的数据地址变换地址入口已经加载到数据 TLB 中。 注意 在此过程中避免使用内嵌语法(inline literal)。所有锁定 TLB 用到的数据可以被 TLB 中一个地 址变换条目所覆盖。 如果系统使用统一的数据 TLB 和指令 TLB,上述两条都要保证。 ⑤ 对于 I=0 到 N,重复执行下列操作:  将 base=I、victim=I、P=1 写入寄存器 c10 中; 专业始于专注 卓识源于远见       ‐  44  ‐             将每一条想要锁定到 TLB 的变换地址入口读取到 TLB 中。对于数据 TLB 和数据/指令统一的 TLB 可以 使用 LDR 指令读取一个涉及该变换地址入口的数据,将该地址变换入口读取到 TLB 中。对于指令 TLB, 通过操作寄存器 c7,将相应的变换地址读取到指令 TLB 中。 ⑥ 将 base=N、victim=N、P=0 写入寄存器 c10 中。 要解除 TLB 中被锁定的变换地址入口,可以使用下面的操作序列。 ① 通过操作寄存器 c8,使 TLB 中各被锁定的变换地址入口无效。 ② 将 base=0、victim=0、P=0 写入寄存器 c10 中。 15.5.8 存储访问失效 ARM 中有两种存储访问失效(Fault)可以导致处理器停止执行。  MMU 失效(MMU Fault):由 MMU 检测到失效(Fault)并通知处理器。  外部存储器访问中止(External Abort):由外部存储器向存储器报告无效的存储器访问请求。 上述两种情况统称为存储访问中止(Abort)。如果存储访问中止发生在数据访问周期,CPU 将产生数据访 问中止异常中断(Data Abort);如果存储访问发生在指令预取周期,当该指令执行时,CPU 产生指令预取 异常中断(Prefetch Abort)。 注意 预取指令时发生错误,只有当该指令执行时,CPU 才会产生指令预取异常中断。 (1)MMU 失效 MMU 可以产生 4 种类型的访问失效,分别是:  地址对齐失效(Alignment Fault);  地址变换失效(Translation Fault);  域控制失效(Domain Fault);  访问权限控制失效(Permission Fault)。 存储系统可以中止 3 种类型的存储访问:  Cache 行预取(line fetch);  无 Cache 和写缓存的存储器访问(uncached or unbuffered accesses);  传输表访问(translation table accesses)。 MMU 失效优先于外部存储器访问中止请求。当存储访问失效发生时,系统控制协处理器中有两个寄存器 分别负责保存发生中止的失效状态和地址。 注意 如果一条指令在预取阶段发生错误,它仍将进入指令流水线,直到该条指令被执行时,预取异 常才发生。但当预取错误指令在进入执行阶段前,指令发生跳转,那么该预取异常不会发生, 协处理器错误寄存器的状态也不会被更新。 (2)MMU 中与存储访问失效相关的寄存器 MMU 中与存储访问失效相关的寄存器有两个:  失效状态寄存器(FSR,Fault Status Register);  失效地址寄存器(FAR,Fault Address Register)。 失效状态寄存器是协处理器寄存器 c5。失效地址寄存器为协处理器寄存器 c6。 当存储访问失效发生时,失效状态寄存器中的字段被更新以反映所发生的存储访问失效的相关的信息,包 括存储访问所属的域以及存储访问的类型。同时存储访问失效的虚拟地址被保存到地址寄存器 c6 中。 在数据访问周期发生存储访问失效更新了失效状态寄存器后,如果系统尚未进入存储异常模式,这时发生 了指令预取引起的存储失效,则该指令预取引起的访问失效将不会更新失效状态寄存器的值。这样就保证 了数据访问周期发生的存储访问失效状态信息不会被指令预取周期发生的存储访问失效破坏。 专业始于专注 卓识源于远见       ‐  45  ‐            引起存储访问失效的存储访问类型如表 15.32 所示。 表中,对齐失效的编码可以为 0b0001 或 0b0011。 表 15.32 存储访问失效的存储访问类型 优先级 引起存储访问失效的原因 失效状态字段 域字段 失效地址寄存器 c6 最高 极端异常(Terminal Exception) 0b0010 无效 生产商定义 中断向量访问异常(Vector Exception) 0b0000 无效 有效 地址对齐(Alignment) 0b00x1 无效 有效 扩 展 地 址 变 换失效(页表 访问失效) 一级页表 0b1100 有效 有效 二级页表 0b1110 无效 有效 地 址 变 换 失 效 段失效 0b0101 无效 有效 页失效 0b0111 有效 有效 域控制失效 段失效 0b1001 有效 有效 页失效 0b1011 有效 有效 访 问 权 限 控 制失效 段失效 0b1101 有效 有效 页失效 0b1111 有效 有效 基于 Cache 的 外 部 存 储 访 问系统异常 段失效 0b0100 有效 有效 页失效 0b0110 有效 有效 最低 非 Cache 预取 时 外 部 存 储 访问异常 段失效 0b1000 有效 有效 页失效 0b1010 有效 有效 在域控制字段(bits[3:0])中存在无效值,是因为无效发生在域访问之前。 当不同的存储访问类型同时引起存储访问失效时,按照优先级由高到低的次序,先保存优先级高的存储访 问失效相关信息,在表中各存储访问优先级由上到下依次递减。 图 15.36 显示了判断存储访问失效的全过程。 下面分别介绍各种类型的存储访问失效方式。 ① 极端异常(terminal exception) 极端异常指的是发生了不可恢复的存储访问失效。具体属于哪种情况,有生产商定义。 ② 中断向量访问异常(vector exception) 在数据访问周期,如果访问异常中断向量表(地址 0x0 到 0x1f)时发生存储访问失效,这种存储访问失效 称为中断向量访问异常。当 MMU 被禁止时是否产生中断向量访问异常由生产商决定。 ③ 地址对齐失效 在数据访问周期,如果访问字单元地址时地址 bits[1:0]位不是 0b00,或者访问半字单元时地址 bits[0]位不 是 0b0,则产生的存储访问失效称为地址对齐失效。在指令预取周期不会产生地址对齐失效。在数据访问 周期,如果访问字节单位,不会产生地址访问失效。 ④ 地址变换失效 有两种类型的地址变换失效。一种是基于段的地址变换失效,它指当一级页表描述符的位 bits[1:0]=0b00 时,表示该一级描述符页表项无效,这时产生基于段的地址变换失效。第二种是基于页的地址变换失效。 当二级描述符的位 bits[1:0]=0b00 时,表示该二级描述符页表项无效,这时产生基于页的地址变换失效。 专业始于专注 卓识源于远见       ‐  46  ‐            允许地址对齐检测? 取一级页标识符 是 页表访问失效? 一级页表 访问失效 描述符失效? 基于段的 地址交换 失效 段还是页? 二级页表 访问失效 地址变换 失效 域控制检测 基于段的 域控制权 限失效 有访问权限? 基于段的 控制权限 失效 物理地址 基于页的 域控制权 限失效 有访问权限? 基于子页 的控制权 限失效 虚拟地址 地址对齐检测 对齐么? 地址对齐 失效 取二级页表 页表访问失效? 描述符失效? 域控制检测 判断访问类型 判断访问类型 否 否 页 是 是 否 否 无访问 无权限 无访问 管理者 客户 有 有 无 客户 图 15.36 判断存储访问失效的全过程。 ⑤ 域控制位失效 专业始于专注 卓识源于远见       ‐  47  ‐            域控制位失效包括两种类型。一种基于段的存储访问域控制失效。在一级描述符中包含 4 位的域标识符。 该标识符指定了本段所属的域,在 MMU 读取一级描述符时,它检查域访问控制寄存器 c3 中对应于该域 的控制位,如果相应的两位控制位为 0b00,说明该域不允许存储访问,这时,就产生了基于段的存储访问 域控制失效。第二种是基于页的存储访问中域控制位失效。在一级描述符中包含 4 位的域标识符。该标识 符指定了本页所属的域,在 MMU 读取一级描述符时,它检查域访问控制寄存器 c3 中对应于该域的控制 位,如果相应的两位控制位为 0b00,说明该域不允许存储访问,这时就产生了基于页的存储访问域控制失 效。 ⑥ 访问权限失效 访问权限失效的检查是在域控制位失效检查时进行的。这时如果域访问控制器中对应于该域的控制位为 0b01,则要进行相应的权限检查。访问权限失效有两种类型。一种基于段的存储访问权限控制失效,对于 基于段的存储访问,在一级描述符中包含一个两位的访问权限控制位 AP。如果字段 AP 标识了不允许进行 相关存储访问时,产出基于段的存储访问权限控制失效。第二种是基于页的存储访问控制失效。对于基于 页的存储访问,在二级描述符中定义的可能为大页、小页或者微页。当二级描述符中定义的为微页时,该 二级描述符中包含一个对应于该微页的访问控制字段 AP,如果字段 AP 标识了不允许进行相关的存储访 问,这时产生基于子页的存储访问权限控制失效。同样,当二级页表描述符中定义的为小页或大页时,操 作过程同微页。 (3)外部存储访问失效 除处理器内部 MMU 向 CPU 报告错误外,ARM 体系结构还定义了一个外部访问中断引脚。该引脚可以用 于外部存储器向 CPU 访问失效异常。但是,并不是所有失效异常都可以通过这种方式报告,所以该引脚 在连线时要非常注意。下面列举了存储访问操作,可以通过这种机制中止和重启动。  读操作(reads)。  非缓存的写操作(unbuffered writes)。  一级描述符预取(first-level descriptor fetch)。  二级描述符预取(second-level descriptor fetch)。  非缓存的信号量操作(semaphores in uncachable/unbufferablememory areas)。 在 Cache 预取时,可以在任意字时终止存储访问过程。如果存储访问发生在存储器想要获取的数据中,这 时该存储访问将立即被中止。如果产生中止的数据是在 Cache 预取时,从存储器顺序读出的,那么直到这 些数据被存储器访问时,该存储访问才会被中止。 带缓存的写操作不能通过这种方式向 CPU 报告异常。因此,在系统中标记为可外部中止的存储区域不要 进行可缓存的写操作。 15.5.9 快速上下文切换扩展(FCSE,Fast Context Switch Extension) (1)快速上下文切换扩展原理 快速上下文切换扩展(FCSE,Fast Context Switch Extension)是 MMU 中的一个附加硬件,用于提高 ARM 嵌入式系统的系统性能。FCSE 使得多个独立任务可以运行在一个固定的重叠存储空间中,而在上下文切 换时,不需要清理(clean)或清除(flush)Cache 和 TLB。FCSE 主要特征就是不需要清除 Cache 和 TLB。 通常情况下,如果两个进程占有的虚拟地址空间有重叠,系统在两个进程之间进行切换时,必须进行虚拟 地址到物理地址的重映射。而虚拟地址到物理地址重映射涉及到重建 MMU 中页表,而且 Cache 及 TLB 中的内容都必须使无效。这样操作将带来巨大的系统开销,一方面重建 MMU 和使无效 Cache 及 TLB 的内 容需要很大的开销,另一方面重建 Cache 和 TLB 内容也需要很大的开销。 快速上下文切换扩展的引入避免了这种开销。它位于 CPU 和 MMU 之间,如果两个进程使用了同样的虚 拟地址空间,则对 CPU 而言,两个进程的空间地址是一样的。快速上下文切换扩展对各进程的虚拟地址 进行变换,这样系统中 CPU 之外的部分看到的是经过快速上下文切换扩展变换的虚拟地址。快速上下文 切换扩展将各进程的虚拟空间变换成不同的虚拟空间。这样在进行进程间切换时就不需要进行虚拟地址到 物理地址的重映射。 专业始于专注 卓识源于远见       ‐  48  ‐            快速上下文切换扩展将 CPU 发出的每个虚拟地址按照上述的规则进行变换,然后发送到系统中的其他部 分。变换过程如图 15.37 所示。 ARM FCSE MMU 主存储器 Cache 虚拟 地址 变换后的 虚拟地址 物理 地址 图 15.37 快速上下文切换扩展变换过程 使用快速上下文切换扩展,虚拟存储管理增加了一次地址转换。快速上下文切换扩展在虚拟地址到达 Cache 和 TLB 前,使用一个特殊的、包含进程 ID 值的重定位寄存器来修改虚地址。把第一次变换前的地址称为 虚地址 VA(Virtual Address),把第一次变换后的地址称为修改后虚拟地址 MVA(Modified virtual Address)。 这样,任务间的切换就不用涉及到改变页表,只需简单地将新任务的进程 ID 写到位于 CP15 地 FCSE 进程 ID 寄存器。正是因为任务切换不需要改变页表,因而切换后 Cache 和 TLB 中的值依然保持有效,不需要 清除。 ARM 系统中,4GB 的虚拟空间被分为 128 个进程空间快,每个进程空间块大小为 32MB。每个进程空间 块中可以包含一个进程,该进程可以使用虚拟地址空间 0x00000000~0x01ffffff,这个地址范围也就是 CPU 看到的进程的虚拟空间。系统 128 个进程空间块的编号为 0~127,编号为 1 的进程空间块中的进程实际使 用虚拟地址空间为 1×0x02000000~1×0x02000000+0x01ffffff。这个地址空间是系统中除 CPU 之外的其他 部分看到的该进程所占有的虚拟地址空间。 由地址 VA 到 MVA 的变换算法如下所示。 MVA=VA+(ox02000000×进程 ID) 保存在 CP15 寄存器 c13 寄存器中的值包含进程 ID,c13 中从 bit[31]~bit[25]共 7 位标识进程 ID,因此可 以有 128 个进程。寄存器格式如图 15.38。 图 15.38 快速上下文切换寄存器 c13 访问寄存器 c13 的指令格式如下所示: MCR p15,0,,c0,0 MRC p15,0,,c0,0 其中,在读操作时,结果中位[31:25]返回 PID,其他位的数值是不可预知的。写操作将设置 PID 的值。 当 PID=0 时,MVA=VA,相当于禁止了 FCSE。系统复位后 PID 为 0。 当正在运行的进程访问别的进程时,被访问的进程标识不能为 0。这时,CPU 发生的地址 VA 的高 7 位不 是全 0。 完整的 VA 到 MVA 的变换算法如下所示。 If (VA[31:25]==0b0000000)then MVA=VA|(PID<<25= Else 专业始于专注 卓识源于远见       ‐  49  ‐            MVA=VA (2)一个快速上下文切换的例子 图 15.39 显示了一个从任务 1 切换到任务 2 之前和之后的存储器布局。 RTOS 任务1 任务2 物理存储器 RTOS 任务1 任务2 内核 任务1 任务2 内核访 问权限 读任务1 域控制 读任务2 域控制 进程ID RTOS PID==1 任务2 Cache和TLB MVA 域访问权限 FCSE VA 任务 1 正在运行 RTOS 任务1 任务2 物理存储器 RTOS 任务1 任务2 内核 任务1 任务2 内核访 问权限 读任务1 域控制 读任务2 域控制 进程ID RTOS 任务1 PID==2 Cache和TLB MVA 域访问权限 FCSE VA 任务 2 正在运行 图 15.39 快速上下文切换扩展例子 从图中可以看出,任务 1 和任务 2 都运行在 0x00000000~0x01ffffff 的地址空间。从任务 1 切换到任务 2 域控制要做相应的改变。通过在 CPU 和 MMU 之间加 FCSE 使系统的虚拟地址空间映射没有改变,所以不 需要清除(Flush)或清理(Clean)Cache 或 TLB。 使用 FCSE 时执行一次上下文切换需要的步骤: ① 保存执行任务的上下文,并将执行任务设置为睡眠态; ② 将唤醒任务的进程 ID 写到 CP15 的寄存器 c13 中; ③ 通过写 CP15 的寄存器 c3,将当前任务的域设置为不可访问,而唤醒任务的域设置为客户访问; ④ 恢复唤醒任务的上下文; ⑤ 继续执行被恢复的任务。 下面是关于 FCSE 的一些提示。 ① 任务在大小上有固定的最大 32MB 的限制。 专业始于专注 卓识源于远见       ‐  50  ‐            ② 存储管理必须使用有固定起始地址(32MB 的倍数)的固定 32MB 分区。 ③ 除非想为每个任务管理一个异常向量表,否则使用 CP15 寄存器 c1 的 V 位将异常向量表放置在虚拟地 址 0xffff0000。 ④ 必须定义和使用一个活跃的域控制系统。 ⑤ 如果使用域来保护各个任务,则除非修改一级页表中域的相应位,并在上下文切换时清除 TLB,否则 最多只能有 16 个并发任务。 联系方式 集团官网:www.hqyj.com 嵌入式学院:www.embedu.org 移动互联网学院:www.3g-edu.org 企业学院:www.farsight.com.cn 物联网学院:www.topsight.cn 研发中心:dev.hqyj.com 集团总部地址:北京市海淀区西三旗悦秀路北京明园大学校内 华清远见教育集团 北京地址:北京市海淀区西三旗悦秀路北京明园大学校区,电话:010-82600386/5 上海地址:上海市徐汇区漕溪路银海大厦 A 座 8 层,电话:021-54485127 深圳地址:深圳市龙华新区人民北路美丽 AAA 大厦 15 层,电话:0755-22193762 成都地址:成都市武侯区科华北路 99 号科华大厦 6 层,电话:028-85405115 南京地址:南京市白下区汉中路 185 号鸿运大厦 10 层,电话:025-86551900 武汉地址:武汉市工程大学卓刀泉校区科技孵化器大楼 8 层,电话:027-87804688 西安地址:西安市高新区高新一路 12 号创业大厦 D3 楼 5 层,电话:029-68785218 《ARM 系列处理器应用技术完全手册》 作者:华清远见 第 16 章 ARM 体系结构的发展 专业始于专注 卓识源于远见       ‐  2  ‐            16.1 ARM 体系结构的发展过程 随着片上系统设计变得更加精密、复杂,ARM 处理器已成为包含多个处理部件和子系统的系统核心处理 器。每个 ARM 处理器都有一个特定的指令集架构 ISA,ISA 随着嵌入式市场的需求而发展。每一个 ISA 的发布都是相后兼容的,这使得在较早的架构版本上编写的代码也可以在后续版本上执行。 图 16.1 说明了 ISA 的发展过程。 1994 1996 1998 2000 2002 2004 2006 ARM7TDMI StrongARM ARM720T ARM720T ARM1020 ARM9E ARM926EJ ARM1022E XScale ARM10EJ V6 cores ARMv4 ARMv5 ARMv6 图 16.1 ISA 发展过程 ISA 的每一次发展,体现在命名上,就是在版本名称中增加新的变量。ISA 的发展分下面几个过程。 ① V3 架构中引入了 32-bit 寻址和 16-bit 指令执行,而且在版本号中增加了变量 T 和变量 M 其中:  T 变量:16bit 指令执行。  M 变量:长乘法的支持。 ② V4 中增加半字 Load/Store 指令。 ③ V5 中增加 ARM/Thumb 交互工作机制,而且在版本号中增加了变量 E 和变量 J 其中,  E 变量:增强的 DSP 指令。  J 变量:Jazelle 状态。 注意 所有这些“TEJ”变量特性都集成在 ARMv6 体系结构中。 ARM 面临的挑战是要满足不断变化的市场需求,同时在计算效率方面,继续保持工业界最强的竞争优势。 16.2 ARMv6 增加的系统支持 为了满足目前无线网络、汽车电子和消费类电子产品不断增长的市场需要,ARM 公司在 ARMv6 中引入新 的技术和结构组成,包括增强的 DSP 支持和对多处理器环境的支持。 16.2.1 存储管理 专业始于专注 卓识源于远见       ‐  3  ‐            由于在 ARMv6 体系结构中引入新的存储管理机制,处理器的整体性能得到提高。在新的体系结构中,平 均指令预取和数据等待时间大幅度减少,存取过程中 Cache 命中率显著提高。由于存储机制的改善,系统 整体性能的提高达到 30%。 另外,存储系统的改善使系统总线(BUS)使用更加合理,从而减少了系统总线使用频度,降低了系统功 耗。 图 16.2 显示了 ARMv6 体系结构存储系统示意图。 紧耦合内存 地址传输 虚拟地址 物理地址 二级 Cache Cache 附加处理器 TCM Load Store 寄存器组 ARM core DRAM SRAM FLASH ROM 图 16.2 ARMv6 存储系统示意图 1.ARMv6 L1 Cache  ARMv6 采用“分层”的存储管理,存储层次的最顶层在处理器内核中。该存储器被称为寄存器文件(register file)。这些寄存器被集成在处理器内核中,在系统中提供最快的存储访问。 ARMv6 体系结构处理器使用物理索引 Cache(Physically tagged caches),即地址转换在 CPU 和 Cache 之间, 这样就减少了 CPU 在运行大的操作系统时由于上下文切换而带来的系统开销。使用这种物理 Cache,可以 使 CPU 的整体性能提高近 20%。 为了减少在内容转换时,刷新 Cache 的 CPU 开销,ARMv6 将 L1 Cache 构建为使用物理寻址的存储系统。 系统中设有 TCM 作为物理可寻址的快速访问内存,存在于存储系统中,作为 Cache 的补充。无论 Cache 还是 TCM,都可以配置为指令和数据分离的 Harvard 架构或指令和数据统一的冯·诺依曼架构。另外,L1 DMA 子系统可以使数据在没有 CPU 参与的情况下,直接和 TCM 进行数据传输。 2.页表格式  在 ARMv6 体系结构中,页表格式也发生了变化。图 16.3 显示了新的一级页表格式。 图 16.3 ARMv6 页表格式 专业始于专注 卓识源于远见       ‐  4  ‐            协处理器CP15中的XP-bit 可以指定是否使用这种新的页表格式。如果不设置该位,则系统继续使用ARMv5 架构的页表格式。 从图 16.3 可以看出,新的页表格式增加了以下特性:  XN:从不执行位(execute never bit)。  nG:非全局地址映射位(not Global bit for address matching)。 应用程序空间指示 ASID(Application Space Identifier)是 ARMv6 体系中增加的又一关键特性。当 nG 位 置位时,地址转换使用虚拟地址和 ASID 相结合的方法以减少上下文切换的时间。同时,应用程序空间指 示提供了一种任务可知调试方法(task-aware debugging)。 有关 ARMv6 存储系统的详细内容请参阅 ARM 相关文档。 3.增加的页表基地址寄存器  为了提高地址转换的处理速度,ARMv6 体系结构中增加了一个新的页表基地址寄存器,以存储二级页表 的基地址。CP15 同时支持 TTBR0 和 TTBR1。专门的控制寄存器用来保存用户设定的整数 N,N 的取值范 围为 0~7。当 N 的值不等于 0 时,0~232-N 的地址空间使用 TTBR0,而其他空间使用 TTBR1 进行传输控 制。一级页表根据 N 取值的不同,占有 128bytes~16KB 存储空间。 16.2.2 多处理单元支持 由于片上系统 Soc 结构的复杂化,ARM 内核现在经常被用于有多个处理单元的设备,这些处理单元竞争 使用系统的共享资源。为了满足多处理单元任务间同步的需要,Load/Store 互斥指令引入到新的 ARMv6 体系结构中来。新指令包括:  LDREX:加载互斥指令。  STREX:存储互斥指令。 LDREX 指令从存储器中装载一个值到寄存器,在处理这个数据时,不会有任何其他因素改变该值。STREX 指令存储一个值到寄存器,并返回一个指示值。 16.2.3 异常处理和中断 ARMv6 体系结构提供了对向量中断(vectored Interrupt)的支持。向量中断控制器(VIC,Vectored Interrupt Controller)由 CP15 的寄存器 1 中的 VE – bit 来控制。当向量中断控制器使能时,该控制器可以向 CPU 提 供发生中断的向量。 另外,在 ARMv6 的体系结构中,程序状态寄存器 CPSR 扩展了 A 位来控制 Abort 异常。这种机制类似于 程序状态寄存器 CPSR 中 I 和 F bit 对 IRQ 和 FIQ 的控制。 操作系统通常在堆栈中保存一次中断或异常处理的返回状态。ARMv6 增加了新的指令来提高这类操作的 效率。这种操作在中断/调度程序驱动系统中,出现的频率是很高的。这些新增加的指令包括:  SRS:保存返回状态在特定模式的堆栈中。  RFE:异常返回。  CPSID/CPSIE:改变处理器状态,开中断或关中断。 16.2.4 混和大小端支持 专业始于专注 卓识源于远见       ‐  5  ‐            AMRv6 体系结构中增加了同时处理大端和小端数据的能力。新增加了指令 SETEND 来设置一段代码处理 数据的字节排列方式,另外还增加了一些单独的处理指令来提高在混和大小端环境下的处理效率。 指令 SETEND 的标准格式如下: SETEND 该指令根据参数的值来改变默认的数据端格式。 SETEND 指令的设置直接和程序状态寄存器 CPSR 中新增加的 E 位相对应。E 位对数据大小端的控制如图 16.4 所示。 16.2.5 对媒体处理的支持 为了进一步提高体系结构的 DSP 和媒体处理能力,单指令流多数据流(SIMD)技术被引入到 ARMv6 体 系结构中。这种技术对于处理大量复杂运算和并行地存储大流量数据十分有效。 Byte3 Byte2 Byte1 Byte1 Byte0 Byte1 Byte2 Byte3 Byte3 Byte2 Byte1 Byte0 0 31 31 0 CPSR E-bit=0 CPSR E-bit=1 图 16.4 E 位对数据大小端的控制 ARMv6 对 SIMD 的实现简单而又不失其灵活性。它将现存的 32 位 ARM 数据通道划分成 4 个 8 位或 2 个 16 位的片段,为 SIMD 操作增加了独立的数据总线。这种实现方法硬件代价小,遵循了 ARM 低功耗、高 计算效率的设计原则。 为了支持 SIMD 算法,ARMv6 中引入一些新的指令,有关这些指令的详细信息请参阅 ARM 的相关文档。 联系方式 集团官网:www.hqyj.com 嵌入式学院:www.embedu.org 移动互联网学院:www.3g-edu.org 企业学院:www.farsight.com.cn 物联网学院:www.topsight.cn 研发中心:dev.hqyj.com 集团总部地址:北京市海淀区西三旗悦秀路北京明园大学校内 华清远见教育集团 北京地址:北京市海淀区西三旗悦秀路北京明园大学校区,电话:010-82600386/5 上海地址:上海市徐汇区漕溪路银海大厦 A 座 8 层,电话:021-54485127 深圳地址:深圳市龙华新区人民北路美丽 AAA 大厦 15 层,电话:0755-22193762 成都地址:成都市武侯区科华北路 99 号科华大厦 6 层,电话:028-85405115 南京地址:南京市白下区汉中路 185 号鸿运大厦 10 层,电话:025-86551900 专业始于专注 卓识源于远见       ‐  6  ‐            武汉地址:武汉市工程大学卓刀泉校区科技孵化器大楼 8 层,电话:027-87804688 西安地址:西安市高新区高新一路 12 号创业大厦 D3 楼 5 层,电话:029-68785218

标签:

版权声明

1. 本站所有素材,仅限学习交流,仅展示部分内容,如需查看完整内容,请下载原文件。
2. 会员在本站下载的所有素材,只拥有使用权,著作权归原作者所有。
3. 所有素材,未经合法授权,请勿用于商业用途,会员不得以任何形式发布、传播、复制、转售该素材,否则一律封号处理。
4. 如果素材损害你的权益请联系客服QQ:77594475 处理。