首页资源分类嵌入式系统其他 > 大话操作系统——做坚实的工程实践派

大话操作系统——做坚实的工程实践派

已有 445117个资源

下载专区

上传者其他资源

    文档信息举报收藏

    标    签:大话操作系统

    分    享:

    文档简介

    大话操作系统——做坚实的工程实践派

    文档预览

    硬件部分 开始之前 操作系统的功能及为什么需要它 硬件平台 选择平台 mini2440 mini2440 平台的信息 必须要关注的硬件 原因 RTC 定时器 uart 中断控制器 SDRAM norflash nandflasH 小结 目录 处理器 ARM ARM 介绍 ARM920T CPU ARM920T CPU 结构 ARM920T 储存体系 ARM920T 地址空间 ARM920T 储存器格式。 ARM920T 储存地址对齐 ARM920T 状态 ARM 状态 Thumb 状态 ARM920T 工作模式 特权模式 数据终止模式 预取终止模式 IRQ 模式 FRQ 模式 系统模式 用户模式 寄存器 ARM920T 寄存器 R0-R7 寄存器 R8-R12 寄存器 R13 寄存器 R14 寄存器 R15 寄存器 CPSR 寄存器 SPSR 寄存器 异常和中断 什么是中断和异常 异常 中断 异常中断向量 指令集 指令及其编码格式 分支跳转指令 数据处理指令 装载和储存指令 程序状态寄存器操作指令 协处理器操作指令 异常中断产生指令 MMU MMU 概述 为什么要有 MMU ARM920T CP15 协处理器 MMU 页表 MMU 页面访问权限的控制 MMU 的快表 TLB Cache ARM920T 的 CACHE CACHE 的原理 CACHE 的类型及要注意的问题 ARM920T CACHE 的编程接口 小结 软件部分 操作系统内核的设计与构建 操作系统内核的设计 内核要完成的功能 内核的架构 分离硬件的相关性 我们的选择 开发环境及相关工具 Linux 环境 文本编辑器 GCC LD Make LMOSEM 的构建系统 LMOSEM 的 makefile LMOSEM 的链接脚本 开发板的安装 安装 mini2440 小结 语言间调用约定与基本数据结构 寄存器使用约定 寄存器别名 参数传递与返回值 基本数据结构 C 语言的基本数据结构 list_h_t spinlock_t kwlst_t sem_t 数据结构存在内存中的形式 C 数据结构到内存映像 C 与汇编的混用 GCC 中嵌入汇编 小结 初始化 开始 第一行汇编代码 第一个 C 函数 板级初始化 搞定 MMU 复制中断跳转表 串口初始化 串口硬件 内核的 printf 机器数据结构 设计数据结构 确定一些重要数据结构及内核的地址 初级内存管理初始化 设计一些数据结构 初始化内存位图 建立起内存分配数据结构 中断初始化 设计一些数据结构 初始中断线路描述符 小结 内存管理 内核功能层入口 Init_krl 函数 内存管理设计 内存管理器结构 块级内存管理 块级内存管理数据结构视图 块级内存管理接口 主分配函数 分配时查找 alcfrelst_t 分配时查找及操作 mmapdsc_t 分配代码写的对吗 主释放函数 释放时查找 alcfrelst_t 释放时查找及操作 mmapdsc_t 测试块级内存管理层 页级内存管理 页级内存接口及调用流程 相关的数据结构 页级内存管理初始化 分配主函数 分配时查找 mplhead_t 分配时新建页级内存池 分配时操作 mplhead_t 分配代码写的对吗 释放主函数 释放时查找 mplhead_t 释放时操作 mplhead_t 释放时删除页级内存池 测试页级内存管理层 字级内存管理 字级内存接口及调用流程 相关的数据结构 分配主函数 分配时查找 mplhead_t 分配时新建字级内存池 分配时操作 mplhead_t 分配代码写的对吗 释放主函数 释放时查找 mplhead_t 释放时操作 mplhead_t 释放时删除字级内存池 测试字级内存管理层 小结 中断管理 中断与中断控制器 什么是中断 s3c2440a 中断控制器 中断管理的架构及相关数据结构 中断管理的架构 intfltdsc_t 和 intserdsc_t 中断处理 中断辅助例程 从中断向量开始 保存 CPU 上下文 中断主分派例程 确定中断源 调用中断处理例程 安装中断回调例程 设备回调例程的安装接口 小结 驱动模型 操作系统内核如何管理设备 分权而治 设备类型 驱动程序 相关数据结构 驱动 派发例程类型 设备 ID 设备 io 包 设备表 驱动模型的基础设施 驱动从哪里执行 新建与注册设备 注册回调函数 发送 IO 包 调用驱动程序函数 等待驱动服务 完成驱动服务 驱动模型辅助例程 systick 驱动实例 systick 硬件 systick 驱动框架 systick 驱动实现 测试 systick 设备驱动 RTC 驱动实例 RTC 硬件 RTC 驱动实现 小结 进程 应用程序的运行 程序运行需要什么资源 任何时刻资源都可用吗 提出多道程序模型 相关的数据结构 进程 调度进程表 内核的第一个进程 进程的初始化 建立空转进程 空转进程运行 新建进程 分配进程描述符 分配内存空间 加入进程调度表 进程调度 调度算法 处理进程 TICK 检查调度状态 选择进程 进程切换 进程等待与唤醒 进程测试 小结 文件系统 文件系统设计 文件系统只是一个设备 数据格式与储存块 如何组织文件 关于我们文件系统的限制 相关的数据结构 超级块 位图 目录 文件管理头 文件系统格式化 建立超级块 建立位图 建立根目录 文件系统基础操作 获取与释放根目录文件 字符串操作 分解路径名 检查文件是否存在 文件操作 新建文件 删除文件 打开文件 读写文件 关闭文件 整合驱动 文件系统测试 格式化测试 文件操作测试 小结 系统调用与应用程序库 系统调用机制 软中断指令 传递系统调用参数 系统调用分发器 时间管理调用 获取时间 进程管理调用 进程的运行与退出 进程的 ID 内存管理调用 内存块的分配与释放 设备与文件调用 设备与文件的打开 设备与文件的关闭 设备与文件的读写 设备与文件的控制 应用程序库 库函数 测试 两个应用程序 小结 后记 开始之前 2010 年下半年,我开始准备着要写个操作系统内核,没有其它目的,只是出于学习,出 于兴趣。由于是自己独立从零开始设计、编写的,然而我觉得自己这种行为有点疯。索性用 LMOS(liberty,madness,operating,system)命名了我的操作系统。LMOS 经过我这几年 的独立开发,现在已经发布了 6 个测试版本。先后从 32 位单 CPU 架构发展到 64 位多 CPU 架 构,现在的 LMOS 已经是多进程、多线程、多 CPU、支持虚拟内存的 x86_64 体系下的操作系 统内核。 嵌入式系统不是今天才有的,但从未有过今天的火热。各种嵌入式操作系统更是百花齐 放。2013 下半年,我也开始学习嵌入式系统了,并写了个嵌入式操作系统——LMOSEM。在互 联网上也认识了不少搞嵌入式的朋友,在他们的要求和规劝下,我终于有了勇气把我捣鼓出 来的东西归纳、整理成册,也算是我学习的笔记,于是就有了这本书。虽然有很多的顾虑, 怕贻笑方家、怕误导同道……当然我的每行代码、每个点子,都在真机上测试并证明其正确 性,所以也就心下一片坦然,无所顾忌了。如果这本书能够被后来者借鉴一二,或者解决后 来者们的一些疑惑,我自然是欣慰万分。 本书很简单,没有拐弯抹角,没有反复修饰,但是必要的细节从不漏掉。宁可在细节上 罗嗦一点,也不在不相关的地方多写一句。 本书的最终目的是构建一个用于学习的嵌入式操作系统内核,并工作在真正的物理机上。 为了达到这一目的,本书大体上分为两部分:硬件部分和软件部分。 硬件部分首先分析了我们选择的平台,以及这个平台上的组件。对平台组件的分析是这 样的:先使用概述的方式分析了一些平台的外围组件:如实时时钟、定时器、串口、中断控 制器、内存芯片、flash 芯片、CPU、MMU 等……让读者有个初步的印象,在写代码用到某个 组件时再详述其内部编程细节。紧接着重点分析了 CPU 和 MMU 的细节,其实还有内存芯片, 只是它比较简单,因为这三个部件是程序运行的基石。没有它们程序根本无法运行,所以在 写代码之前必须详细了解它们的细节。 软件部分首先分析了操作系统内核是干什么的,其中都有些什么组件,这些个组件分别 是干什么的有什么作用。然后是如何设计操作系统架构并将这些重要的组件组合在一起,以 及设计时需要注意些什么。最后介绍了构建操作系统的工具。接着介绍了 C 调用约定和基本 的数据结构以及 C 数据结构在内存中的镜像。再就开始写代码完成这些组件了:初始化、内 存管理、中断管理、设备管理、进程管理、文件系统、系统调用接口、应用程序库。对这些 组件的介绍采用这种方式:一、介绍的是这个组件要完成什么功能,达到什么要求。二、详 述这个组件必须要关注的硬件的细节,三、把我们的想法和设计归纳成数据结构。四、编写 完成这些功能的代码。系统调用接口为了顾及读者们已有的知识体系,模拟了类 UNIX 调用 接口,方便读者理解核心原理。 LMOSEM 不支持实时性功能,嵌入式操作系统不一定要是实时性的操作系统,何况我们 是出于学习的目的。为了代码的清晰、简单,我们暂不考虑安全性和性能方面的问题。等到 我们明白了操作系统原理和代码成熟之后,我们再去不断的修正、优化,使之功能变得更多, 性能变得更强。 笔者开发 LMOSEM 操作系统这个项目,是在 linux 操作系统下开发的,用到了 linux 操 作系统的很多工具。笔者不会和读者讨论为什么不用常用的 windows 系统,也不会说谁好谁 不好。如果读者非常喜欢 windows 系统,那么读者也可以尝试着把这个项目迁移到 windows 系统下。但是笔者书中演示的环境还是 linux 系统。关于如何搭建开发环境,后面的章节有 详细的介绍。在那里读者会发现用 linux 系统开发 LMOSEM 内核有很多方便之处。比如我们 会用到的 MAKE、GCC、LD 等……这些工具在 linux 系统下都很容易得到,在 windows 系统下 虽然也能做到,但相对麻烦一点。何况今天的 linux 系统已经很好用了。 为了能更轻松的阅读这本书,笔者建议读者了解 C 这门编程语言,如果读者对数据结 构有所了解就更好了,除这些外笔者假定读者没有其它任何技能。读者可能有点疑惑,世 上这么多编程语言为什么单单选择 C 语言。首先 C 语言编译成的二进制可执行代码非常高 效,它的语法简洁、灵活,C 语言是静态的、可预知的……C 语言的这些特性正是编写操作 系统所需要的。C 语言第一个成功的产品就是 UNIX 系统,当然 linux 系统内核和 windows 系统的 NT 内核也不例外都是用 C 语言编写的。这些产品几乎是改写计算机历史的产品。笔 者在写了两个操作系统内核雏形后,有个最真切的感触:开发操作系统内核就是,首先构 建不同的数据结构,然后写代码操作这些数据结构,最后将这些代码和数据结构合理的接 合在一起。有句话不是说:“程序=算法+数据结构”。当然读者也许现在不能真正明白这 句话,在后面的章节中读者会逐渐明白的。除了读者需要上述的技能外,读者还需要对操 作系统有强大的兴趣爱好和求知欲,要有坚强的意志、永远不放弃的精神。开发操作系统 内核本身就不是件容易的事,必然会有很多问题在等着我们,但是遇到问题不要害怕,静 下心从容面对,只要我们不放弃,问题最终会被解决。 我,自幼患脑瘫,一残缺之人,计算机成了唯一的兴趣爱好,也成了我生活的一部 分,没有父母的长期支持,连生活都尚且不能自理,更别说完成此书了,他们对我的帮助 和爱,纵使千万言语也难表一二……由于经常在物理机上测试内核,要拆装一些设备和器 件,这得多亏了我的小弟,因为他一有时间就帮我做这部分工作。当然还有帮助过我的朋 友,有一些是身边的,有些是网络中的。对父母、所有的亲人、朋友,我也只有常怀感恩 之心,说声谢谢,谢谢他们一直的支持、帮助,谢谢他们一直对我那满满的爱…… 让笔者和你一起带着未知,带着好奇,带着兴奋,踏上操作系统的旅程吧…… 1 操作系统的功能及为什么需要它 你或许已经卷起了衣袖,或许在摩拳擦掌,正准备大干一场,打一场硬仗。年轻人嘛, 行事总是风风火火的。但不是笔者扫你的兴,泼你冷水,在我们写代码之前还有很长一段路 要走,要静下心来。如果写操作系统是一次旅行的话,那么千万不要错过沿途的风景…… 操作系统也是软件,也是一大堆程序组成的,所以不要觉得它多么神秘。既然是程序, 我们就要知道它是干什么的以及为什么需要操作系统,笔者想用大多数人写的第一个程序— —“hello world!!”来开始描述这个原因。当然这个程序是用 C 语言写的。大致过程如图(P1.1.1.1)所示: 图(P-1.1.1.1) 这个程序正常运行后,会在屏幕上输出 hello,world!!这个字符串。显然这个程序中最重要 的语句是:printf 这个函数,然而我们并没有实现它。如果你能发现这一点,并迫切的想知 道它究竟在背后干了些什么,那说明你和笔者一样有着对问题追根索源的性子。这个也是我 们研究问题本相的原动力。 那么 printf 它在哪儿呢?都干了些什么呢? 它就在我们的应用程序库中,我们的代码虽然没有实现它,但库中实现了,程序链接器 最终 把它和我们的 代码链接装 配在一起,这 样我们的程 序也就能正确 工作了。如 图( P1.1.1.2)所示: 图(P-1.1.1.2) 实际中可能有差异,可能 printf 不是在单独的库中、也可能不只是链接这一个库就行了。 但是道理都一样。 printf 函数把程序中传给它的“hello,world!!”字符串数据复制到一个内存缓冲区中, 然后调用操作系统提供的 API(应用程序编程接口)函数。可能不同的实现之间有差异,但 是大致动作如图(P-1.1.1.3)所示: 图(P-1.1.1.3) 一旦调用操作系统 API,代码的控制权就交给了操作系统,执行的也是操作系统的代码。 至于是怎么实现这一机制的,我们后面的章节会详细讨论。操作系统会根据不同的 API 函数 以及应用程序传递进来的参数,完成不同的动作和功能。比如程序中的 printf 它调用的 API 和传递的参数,是要告诉操作系统把它缓冲区中的数据写入到标准输出设备上。于是我们在 屏幕上就看到了 hello,world!!,然后程序就逐层返回到 main 函数,最后 main 函数退出, 这个小程序也就结束了。下面用图来描述这一过程:如图(P-1.1.1.4)所示。 由图(P-1.1.1.4)可知,真正操控计算机硬件完成显示 hello,world!!的,是操作系统 这个软件。是它在背后默默的工作着。当然它的功能并不只是操控计算机硬件,我们后面会 慢慢讨论。 图(P-1.1.1.4) 现在来看看没有操作系统能完成这个 hello,world!!的程序吗。能!只是我们要做的工 作多了很多,我们得亲自实现 printf 函数、以及完成控制计算机硬件的程序,并且还要求 计算机内部这时只有我们的这一个应用程序在运行,因为有些硬件同一时间只能被一个程序 使用。如图(P-1.1.1.5)所示。 图(P-1.1.1.5) 啊!写上面这样的程序可能对程序员的要求有点高哦,因为要知道计算机硬件的每个细 节,不然程序不会出现正常的结果。好吧,我们还是应该承认这个世界存在这种编程高手。 关于 hello,world 这个程序,我们就说到这里。我们有一个感觉,没有操作系统无论是 开发程序还是运行程序,难度不是一般的高。 现在去看看我们先辈们是怎么一步一步的创造出操作系统的。 在很久以前确实存在上述这种模式的程序,并且每次计算机中只存在一道这样的程序, 运行完一道,手工装入第二道……你会发现这太慢了,也太麻烦了,对昂贵的计算机硬件的 利用率也太低了,因为这样的程序是不可能让计算机内部所有的设备都忙起来的。 然而首先知道这个问题的不是我们,我们的先辈们早已看到这类问题,他们想了个方法, 在计算机的内部放入一道监控程序,然后把要执行的所有程序放在一个固定的介质上,每次 那个监控程序选择一道要运行的程序装入计算机内部去执行,这道程序执行完了,再装入下 一道程序……直到运行完所有的程序。如图(P-1.1.1.6)所示。从全局看效率高了不少, 但是从局部来看,它仍然没有让计算机的所有设备都忙起来。同时也还是没有降低程序员的 难度,因为还是要操控他们程序要用到的硬件。 我们马上会想到,能不能把那个所谓的监控程序稍稍改进和扩展一下,把那些操作各种 硬件的代码和实现重要功能的代码,放进监控程序里。然后我们对这些功能进行编号,比如 1 号功能是操作键盘的、2 号功能是打开硬盘上的文件……最后应用程序要完成什么功能, 只要 call xx 号功能,这时计算机的控制权就会交给监控程序,当监控程序中的代码完成这 个功能号的服务时就返回到应用程序中。图(P-1.1.1.7)展示了这一过程。 图(P-1.1.1.6) 图(P-1.1.1.7) 啊哈,这样给应用程序开发人员带来了不只是一点点方便,对他们而言,完全是种解脱。 方便是方便了不少,但这还是只运行了一个应用程序,你或许想到了 DOS 操作系统。是的, DOS 就是这样的操作系统。 我们继续思考一种情景:你正在用 QQ 软件和你的朋友聊天,QQ 软件等待你输入消息, 然后把这个消息通过网络发送给你的朋友。QQ 软件等待你的输入,这个时间可能是几秒钟, 甚至更久。这几秒钟对如此之快的计算机来说,无疑是巨大的浪费。如果这时内存中存在着 另一个软件,这个几秒钟是否可以拿来运行另一个软件呢,那当然是可以的。比如我们让这 几秒钟切换到 KUGOU 软件上,等到用户输入消息时,再换回来,于是这样我们就可以一边聊 天,一边听音乐了……如图(P-1.1.1.8)所示。当然这是其中一种比较常见的情景,还有 例如,等待网卡上的网络数据包、等待磁盘上的数据、等待声卡缓冲区空闲、等待打印机打 印完文档、等待扫描仪的输入……之所以会有以上这些“等待”,那是因为计算机内部各种 备的速度不相同,就像键盘的输入速度赶不上 CPU 的速度、磁盘的数据传输速度比不上 CPU 的数据处理速度…… 图(P-1.1.1.8) 还是上面的情景,我们继续进一步的思考一些问题。QQ 和 KUGOU 同时存在于内存中, 那么就要占据不同的物理内存空间。作为操作系统当然就要知道哪些内存是空闲的,哪些内 存是已经占用的,当然比如 QQ 程序退出了操作系统就要标识 QQ 占用的内存已经是空闲了, 从而可以被用于其它用途。QQ 和 KUGOU 是两个不同的程序,操作系统要知道它们的存在, 包括它们的一些信息,例如,它们各自占用多大内存、在内存中的什么地址,它们各自执行 到哪里了,以及当前 CPU 上执行的是哪个程序。QQ 要记录你的聊天数据,KUGOU 要读取 MP3 音乐数据,操作系统就要组织这些数据的存放、读取,以及控制这些数据的访问权限。最后 QQ 要发送消息,KUGOU 要播放音乐,这最终都要用到计算机内部具体的硬件设备,QQ 要用 到网卡和磁盘、KUGOU 要用到磁盘和声卡,操作系统当然要知道计算机内部有多少设备,每 个设备的状态、类型及访问操作方式,以便代替应用程序访问控制设备。 上述情景中,我们思考的问题,当然也是操作系统重点要解决的问题,也可以说是操作 系统要完成的功能。先辈们还对其进行规划并定义了专业的术语,如下: 1. 内存管理:最简单的原因是因为要在内存中放入不止一道程序。 2. 进程管理:既然内存中有多道程序,首先要知道它们的存在、状态及以这道程序相 关的重要信息…… 3. 文件系统:用户有那么多的数据,比如音乐、电影、文档、其它数据,怎么组织它 们,以何种形式查找访问它们,怎么才能把它们安全的保存在计算机中。 4. 设备管理:由上可知,操作系统必须要知道计算机内部有多少个设备、设备各自是 什么类型,设备当前能否被访问,最重要的任务是对应用程序屏蔽设备细节,代替 应用软件访问控制设备。 如果站在全局较高的角度看,就是这样的:把计算机内部的设备和设备中的数据统称为 “资源”,操作系统即是众多资源的管理者。应用程序无非就是通过操作系统这层软件:获 取资源、使用资源、释放资源。而操作系统则一边维护众多资源的状态,一边通过众多资源 状态,例如该资源当前能否使用,或者该资源是共享访问的资源还是互拆访问的资源,从而 调度安排众多应用程序有序、高效的运行。最后达到高效使用计算机的目的。 上面所有这些描述你或许看上去很陌生,先不要急,我们后面慢慢的一步一步的来。我 们根据上述所有的描述,用一张图来大致表示现代整个计算机系统的结构。如图(P-1.1.1.9) 所示。 从图(P-1.1.1.9)不难看出,今天如此复杂的计算机,有如此多的功能,时刻影响并 改变着人们的生活。然而它也是一层一层的建立起来的。或许我们还有很多疑问:如何实现 设备管理、进程又是什么玩意儿、怎么实现系统调用……这正是我们后面要解决的问题,现 在我们心中只要明白计算机中有一层叫操作系统的软件,它在背后默默的干了些十分重要的 事儿,如果没有它事情会变得很糟糕。 这一章节就要结束了,我们马上就可以轻松轻松了,现在我们来回想一下: 1. 我们从 hello,world 开始,发现完成输出功能的是一个叫操作系统的软件; 2. 后来我们想不用操作系统这玩意,我们也能完成这个小程序,只是困难了很多,重 要的是计算机的利用率很低; 3. 然后我们做一个叫监控程序的东西,让它负责运行这种类型的程序,并对它进行改 进,降低了编程者的编程难度,计算机的利用率依然很低下; 4. 再然后我们把内存中同时放入多个程序,让一个程序因为一些原因不能继续向下运 行时,我们就切换到别的程序,让其开始运行; 5. 最后,我们基于第 4 点的想法,不断的改进和扩展,终于建造出现代操作系统的模 型。 让我们休息一下,进行下一步探索,相信会更精彩…… 图(P-1.1.1.9) 2 硬件平台 这可能有点天马行空了。上一章节,我们刚刚粗略的了解了操作系统的功能以及为什么 需要它,接着我们就说到了硬件平台。或许你还在想操作系统的第一行代码是什么、该从哪 儿开始呢……不要急,现在所做的一切,都是为了最终的目标而努力着。必然做任何事情都 有个过程不能一蹴而就,平静心态很重要。本章节将首先了解为什么要选择 mini2440 这款 硬件平台,最后了解一下我们必须要关注这个硬件平台上的一些组件…… 开始吧…… 2.1 选择平台 读者可能没有听说过 mini2440,它是一种 ARM 开发板,这里先搞清楚怎样选择一款硬 件平台,为什么要选择 mini2440,然后了解一下 mini2440 相关信息…… 2.1.1 mini2440 现代计算机系统大都可以从逻辑上分为三大层:硬件层、系统软件层、应用软件层,当 然还可以继续细分。如图(P-2.1.1.1)所示。每个层隐藏自己相关的实现细节,向上层提供 相关接口,根据情况决定是否需要调用下层提供的接口。就像是上一章节我们分析的 printf 函数一样,你不必思考它是怎么实现的,只要调用它就行了,具体怎么做是它的事。这就是 为什么写一个应用程序,首先要去了解各种应用程序库和这个应用程序运行所在的操作系统 平台的 API 的原因,因为我们要知道这个系统平台提供给应用程序的编程接口。 没有硬件的软件是空中楼阁,没有软件的硬件是一堆废物,二者缺一不可。就像人一样, 不仅要有强健的身躯,还要有智慧的大脑。想象一下植物人和弱智者的悲剧。 我们的目的是写操作系统内核,那么好,由图(P-2.1.1.1)可知,我们首先应该了解某 一具体硬件平台提供给我们的“接口”。 现在世面上的计算机平台何止千万,有各种型号的超级计算机、服务器、PC、嵌入式开 发板,当然你可能不会选择超级计算机和服务器来开发操作系统内核,我们要从中选择一个 适合我们的。 既然这样,就要选择一个简单、常用、便宜的计算平台作为开发平台。而满足前三个条 件的就是 mini2440 开发板。嵌入式开发板,比通常用的 PC 机简单多了,也便宜多了,而大 多数嵌入式开发者或者学习嵌入式的同学都是从 mini2440 开发板开始的。下面就开始仔细 研究这个开发板。 图(P-2.1.1.1) 2.1.2 mini2440 平台的信息 mini2440 开发板,是友善之臂公司基于三星公司的 S3C2440A 芯片并结合了一系列的外 围组件,开发出来的一款嵌入式开发板。组件非常丰富,性能不错,也很常用,非常适合嵌 入式系统的入门学习。 下面来看看 mini2440 开发板都有些什么,各自都有什么作用,即用来干什么的,先来 看看 mini2440 开发板的概况图,如图(P-2.1.2.1)所示。图(P-2.1.2.1)是友善之臂公司提 供的。 图(P-2.1.2.1) 哇,这看上去各种硬件接口、芯片……真不少。我们来归纳一下,如下: 1. Samsung S3C2440A CPU 处理器,主频 400MHz,最高 533Mhz。 2. 由 2 片 32MB 的 SDRAM 芯片组成的 64MB 内存 ,32 位的内存数据总线,SDRAM 时钟 频率高达 100MHz。 3. FLASH 存 储 板 载 256M/1GB Nand Flash, 掉 电 非 易 失 ( 用 户 可 定 制 64M/128M/256M/512M/1G)。 4. 2MB Nor Flash,掉电非易失,已经安装 superVIVI 引导程序相当于 PC 机上的 BIOS。 5. LCD 显示接口,板上集成 4 线电阻式触摸屏接口,可以直接连接四线电阻触摸屏支 持黑白、4 级灰度、16 级灰度、256 色、4096 色 STN 液晶屏,尺寸从 3.5 寸到 12.1 寸,屏幕分辨率可以达到 1024x768 象素。标准配置为 3.5 真彩 LCD,分别率 240x320, 带触摸屏; 6. 1 个 100M 以太网 RJ-45 接口(采用 DM9000 网络芯片) 7. 3 个串行口。 8. 1 个 USB Host。 9. 1 个 USB Slave B 型接口。 10. 1 个 SD 卡存储接口。 11. 1 路立体声音频输出接口,一路麦克风接口。 12. 1 个 2.0mm 间距 10 针 JTAG 接口。 13. 4 个 USER LED 灯。 14. 6 个 USER 按键(带引出座)。 15. 1 个 PWM 控制蜂鸣器。 16. 1 个可调电阻,用于 AD 模数转换测试. 17. 1 个 I2C 总线 AT24C08 芯片,用于 I2C 总线测试。 18. 1 个 2.0mm 间距 20pin 摄像头接口。 19. 1 个板载实时时钟电池。 20. 电源接口(5V),带电源开关和指示灯。 21. 系统时钟源,12MHz 无源晶振。 22. 内部实时时钟(带后备锂电池)。 23. 1 个 34pin 2.0mm GPIO 接口。 24. 1 个 40pin 2.0mm 系统总线接口。 啊!24 条,可真不少啊,还好我们开操作系统内核时,并不需要关心这个开发板上全部 的硬件资源。 现在把那些硬件提供的什么插口,什么小灯,什么按键,什么电池,啥的……我们都把 它们扔了,搅在一起可能看不清楚。看下图或许会清爽许多,心情也会愉悦不少,如图(P2.1.2.2)所示。根据图(P-2.2.1.2)我们可以这里可以把它们分为这几类: 1. 处理器,这是用来执行程序的。 2. 存储器,两个不同类型的 FLASH,当然它们也是存储器,2 片 32MBSDRAM 的内存。 3. 关于开发板电源的,我不必过多关注。 4. 网络,就是网卡。 5. 音频。 6. 串口,注意这图上芯片不是用于处理串口发送、控制等逻辑的芯片。 7. I2C 总线,这个我们暂时不必关注。 上面这 7 种类型的部件,除第 1 类处理器外,其它部件功能和用途都相对单一,比如 FLASH 和 SDRAM 都是用于储存数据的、音频芯片就是用来处理声音的,它们不干其它的事儿。 但是那个 S3C2440A 处理器它就不同了,别看它只有指头大小,完成的功能却不可小视,下 面来掀开它神秘的面纱。 S3C2440A 是三星公司生产的微处理器,这可不是通常我们认为的微处理器。S3C2440A 基于 ARM920T 核心,这个 ARM920T 核心才是我们通常说的 CPU、处理器,它支持 16/32 位精 简指令集。它还实现了内存管理单元 MMU。S3C2440A 还提供了丰富的内部设备,为了组织和 连接这些内部设备,S3C2440A 它内部采用了先进的微控制总线构架 AMBA。为了提高程序的 执行速度,S3C2440A 还采用了哈佛结构高速缓冲体系结构,这一结构具有独立的 16KB 指令 高速缓存和 16KB 数据高速缓存,每个都是由具有 8 字长的行(line )组成。 图(P-2.1.2.2) 看了一大堆介绍,可能人都晕了,有时又不能不介绍。下面把 S3C2440A 里面的东西都 列出来:看看到底是什么,如下: 1. ARM920T CPU,这才是我们通常说执行程序的 CPU。 2. 内存控制器,包括 SDRAM 控制和地址空间的片选逻辑。但本身不包含 SDRAM 储存芯 片,只是用于控制 SDRAM 储存芯片的。 3. LCD 控制器,最大支持 4K 色 STN 和 256K 色 TFT ,提供 1 通道 DMA 供 LCD 专用。 4. 4 通道 DMA 并有外部请求引脚。 5. 3 通道 UART ,支持 64 字节发送 FIFO 和 64 字节接收 FIFO。 6. 2 通道 SPI。 7. 1 通道 IIC 总线接口,支持多主机。 8. 1 通道 IIS 总线音频编码器接口。 9. AC’97 编解码器接口。 10. 支持 SD 主接口协议 1.0 版和 MMC 卡协议 2.11 的兼容版。 11. 2 通道 USB 接口,1 个主机通道,1 个 USB 设备通道。 12. 4 通道 PWM 定时器和 1 通道内部定时器、看门狗定时器。 13. 8 通道 10 位 ADC 和触摸屏接口。 14. 实时时钟 RTC。 15. 摄像头接口,最大支持 4096×4096 像素输入。 16. 130 个通用 I/O 口和 24 通道外部中断源。 17. 电源管理,具有普通,慢速,空闲和掉电模式。 18. 时钟,具有 PLL 片上时钟发生器。 19. 中断控制器。 你一定在感叹人类的制造工艺,一定在努力的思考人类是怎么把这么多东西集成在一个 指头大小的芯片内的,甚至还集成了总线,可以想象那得多复杂。的确,计算机系统是人类 发展史上人类自己做过的最复杂的东西之一。 上面的文字也许不够形象,甚至有点心烦,那么好我们就去弄一幅图,用它来打开我们 面前这个黑盒子——S3C2440A。如图(P-2.1.2.3)所示。 可以看到 S3C2440A 这个黑盒子里,还有很多黑盒子,我们不继续打开它们了,等到后 面用到它们时,在一一打开瞧个仔细。最后这个 S3C2440A 芯片提供了一大堆引脚,这些引 脚各有各的功能,它们有的是输入信号,有的是输出信号的。这个芯片就是这样和开发板上 外围部件进行交互的。诸如从 SDRAM 内存芯片中读取程序指令执行、向 FLASH 储存芯片中存 入数据,控制音频芯片发出声音,用网卡访问网络等…… 或许你还沉浸在漫漫无迹的思索中,在思考这么多设备和芯片都有些什么功能、又是怎 么工作的、我如何编程控制它们……那我们继续…… 图(P-2.1.2.3) 2.2 必须要关注的硬件 本节将先了解为什么操作系统内核必须要关注或者操作硬件平台中的一些设备,然后分 别介绍必须要关注和操作的一些设备的简要信息…… 2.2.1 原因 如果现在,在只有五行代码的程序中有一个错误,我们很快就能找到并解决它。如果 100 行代码的程序中有一个错误,我们也能容易的找到它。如果一万代码中有一个错误,找到它 有点难度了。如果一千万行代码中有一个错误要找到它,这可真不是件容易的事。而且可能 在一百行代码中有两个错误,一千行代码中可能八个错误甚至更多。还有可能在修复一个错 误时产生新的错误……面对几千万行代码的操作系统可能会让你疯掉的。 降低代码行数是个好主意,可是用五行代码去写个功能完善的操作系统,这是不可能的。 于是人们把操作系统分为很多层,每个层又分为多个模块,这样对于每个分层里的每个模块, 它的代码规模就小了很多很多。人们还把最重要的功能和一些机制放在一个独立的模块中, 这个模块就是操作系统内核。有些功能模块在需要的时候才去装载,这样一个小而精致的内 核最后配合那些独立的功能模块就能实现一个功能相当完善的操作系统。 既然操作系统内核是运行在这个硬件平台上的第一层软件,那么它是肯定要或多或少的 了解一些这个平台上的硬件的。它究竟要知道多少,和它设计的规模有关,上面说了那么多 就是为了证明一点:小而精致的内核更加稳定可靠。除非有必要,我们应该尽可能减少内核 的功能,而把那些功能以独立模块的形式存在。 我们用的是 mini2440 这个平台。那么内核要关注这个平台的哪些硬件呢,我们后面再 说内核的设计,先在此想象性的归纳一下: 1. CPU,内核也是程序要 CPU 去运行的,当然要知道它的细节,我们平台的 CPU 在 S3C2440A 那个黑盒子里叫 ARM920T。 2. MMU,内核是放在内存中的,CPU 要访问内存首先得经过 MMU 内存管理单元,我 们平台的 MMU 在 S3C2440A 那个黑盒子里一个叫 CP15 的黑盒子里。 3. 内存,内核本身和其它程序都是放在这个里面的,我们平台的内存是用两片 32MBSDRAM 芯片并接在一起做成的 64MB 的内存。 4. RTC,实时时间,内核当然要知道现在是什么时间了。 5. 定时器,内核中有很多功能的实现都依赖于定时器。比如进程调度器的实现。 6. UART,串口,在调试内核时能够输出一些信息,表示内核是否正常工作的。 7. 中断控制器。设备有时要通知 CPU,或者表示它的任务已经完成,而平台中那么多 设备,这就必须要有专门的控制逻辑来控制它们,这就是中断控制器。内核要管理 这些设备硬件,当然要知道它们什么时候需要 CPU 的关注,什么时候已经完成它们 的任务。 对于一个小而精致的内核来说,知道这么多硬件已经够了。你可能会说,我们都还没设计内 核呢,这只是想象中的。是的,后面万一不行了,在加入一些不就行了吗,没必要一步到位。 对上面这些玩意的介绍,除了 CPU 和 MMU 外,其它的我们都在本章中以小节的形势先 概述一下它们功能和用途,先让大家有个印象,做个铺垫,然后就过去了。等到后面写操作 系统内核真正用到它们时,我们在来详细讨论。CPU 和 MMU,这两个东西有点小复杂,完 成的功能很多,而且在开始之前又必须认真的了解它们。所以后面用专用的章节详细讨论它 们。 2.2.2 RTC RTC 是时实时钟,表示的是我们通常用的时间。比如人们常说现在什么时间了,就是说 的这个 RTC 时间。 说到自然时间,听到最多的可能就是:“现在几点啦”。就再也没有往下思考,可以假想 一下,物理世界没有了时间,是不是很有趣、也很可怕,可能世间万物会止步不前,可能我 们的青春可以永驻…… 在 mini2440 开发板上,只有一颗 RTC 备用电池,而完成 RTC 功能的部件在 S3C2440A 那 个黑盒子里面。既然是自然界的时间,那么即使在开发板断电后也不能停止运行,这就是它 为什么要有备用电池的原因。 RTC 可以通过 8 位二至十进制交换码(BCD)数据和 CPU 通信。这些数据包括年、月、 日、星期、时、分和秒的时间信息。RTC 单元工作在外部 32.768kHz 晶振,并且可以执行闹 钟功能。 我们来看看 S3C2440A 里的 RTC 单元支持哪些功能,归纳如下: 1. 时间:年、月、日、星期、时、分和秒,支持 CPU 以 BCD 数据的方式读写。 2. 闰年发生器:支持实现和识别哪年是闰年。 3. 闹钟功能:能产生闹钟定时中断或者从省电模式下唤醒开发板。 4. 已经解决 2000 年闰年的问题。 5. 支持独立电源引脚也就是用备用电池供电的功能。 6. 支持时钟节拍中断,比如一秒钟内产生固定次数的中断。 S3C2440A 的 RTC 单元的闰年发生器能够基于天 、月和年的 BCD 数据,从 28、29、30 或 31 中决定哪个是每月的最后日。此模块决定最后日时会考虑闰年因素。8 位计数器只能够 表示为 2 个 BCD 数字,因此其不能判决"00"年的问题,即最后两位数为 0 的年份是否为 闰年。例如,不能判别 1900 和 2000 年。请注意 1900 年不是闰年,而 2000 年是闰年。因 此,S3C2440A 中 00 的两位数是表示 2000 年,而不是表示 1900 年。 RTC 还能在开发板省电模式中或正常工作模式中在指定的时间上产生一个闹钟信号。在 正常工作模式中,只激活闹钟中断信号。在开发板省电模式中,除了激活闹钟中断信号外还 激活电源管理唤醒信号。 RTC 时间节拍中断可以用于操作系统内核的时间节拍,也可用于内核时间的同步更新 等…… 虽然 RTC 单元是操作系统内核必须要关注的硬件之一,但并不在此详细介绍,只是先让 大家有个印象,它就是处理自然时间的,还有闹钟等功能。等到写操作系统用到它时再详细 的讨论其细节…… 2.2.3 定时器 上次说的 RTC 它表示的是自然时间,就是我们常说的:“现在什么时间了”,而这次说的 定时器是什么呢,它表示的是从现在开始到将来某一个时刻之间的时间,比如从下午 3 点到 下午 4 点之间过去了多少时间…… 定时器单元也没有在 mini2440 开发板上,而是在 S3C2440A 这个“黑盒子”里。 S3C2440A 内部有 5 个 16 位定时器。其中定时器 0、1、2、3 具有脉宽调制功能。定时 器 4 是一个无输出引脚的内部定时器。定时器 0 还包含用于大电流驱动的死区发生器。 定时器 0 和 1 共用一个 8 位预分频器,定时器 2、3、4 共用一个另外的 8 位预分频器。 每个定时器都有一个可以生成 5 种不同分频信号的时钟分频器。每个定时器模块从相应 8 位 预分频器得到时钟,然后再通过时钟分频器分频后得到其自己的时钟信号。8 位预分频器是 可编程的。 每个定时器的计数缓冲寄存器中,包含了一个当使能了定时器时被加载到递减计数器中 的初始值。定时比较缓冲寄存器中,包含了一个被加载到比较寄存器中与递减计数器相比较 的初始值。这种双缓冲特征保证了改变频率时定时器产生稳定的输出。 每个定时器有它自己的由定时器时钟驱动的 16 位递减计数器。当递减计数器到达零时, 产生定时器中断请求,通知 CPU 定时器操作已经完成。当定时器计数器到达零时,相应的计 数值将自动被加载到递减计数器以继续下一次操作。 这 5 个定时器所有的特性如下: 1. 五个 16 位定时器。 2. 两个 8 位预分频器和两个 4 位分频器。 3. 可编程输出波形的占空比控制。 4. 自动重载模式或单稳态脉冲模式。 5. 死区发生。 什么是预分频器和分频器,定时器内部工作是靠时钟信号驱动的,事实上不只是定时器, CPU 也是,几乎计算平台所有的部件的运行都与时钟信号有关。比如我们这里的定时器,来 一个时钟信号就对自己内部计数器的值减 1,减到 0 时就发出中断通知 CPU。分频器就是对 时钟频率进行处理,得到一个比当前更低或者更高的频率,当然定时器的分频器是得到更低 的时钟频率,同时配合定时器中的计数值,达到精准的定时功能。定时器输入时的钟频率计 算公式如下: 定时器输入时钟频率=PCLK/{预分频值+1}/{分频值} {预分频值}=0~255 {分频值}=2,4,8,16 PCLK 是 S3C2440A 内部 APB 总线的时钟频率。 这些定时器还可以自动重复定时或者一次性定时,自动重复定时是:比如开始设定定时 器每隔 2 秒产生一次中断,中断产生后它会继续计时到下一个 2 秒钟又产生中断,如此反 复。一次性定时是指,时间到达产生中断后就不继续定时了,直到下一次设定它。 来看看定时器最常用的一个情景:我们让定时器每隔 1 毫秒产生一次中断,然后在中断 处理程序中检查一个进程已经运行了多长时间,分配给这个进程的时间它是否已经用完,如 果已经用完,就调度其它进程运行。如果没有定时器,我们在应用软件中写入一个死循环代 码,操作系统立马就会被这个应用软件锁死,因为系统已经没有任何手段从这个应用程序手 里夺回 CPU 的使用权了。想想吧,这有多可怕…… 同样我们的操作系统内核,也必须了解这个平台上的定时器,不然就会出现上述情况。 但是也不会在这里详述其细节,依然等到用到它时再了解它的编程细节…… 2.2.4 串口 一个计算平台有许多输入输出设备:键盘、屏幕、音频、串口、甚至是网络……当然各 种计算平台可能不同,比如 mini2440 开发板就没有像 PC 机那样的键盘。 我们选择一个最简单且开发板上又有的设备——串口来说说。 mini2440 开板上只有三个用于连接串口线的接口。真正串口功能单元在 S3C2440A 芯片 内部,有三个功能相同的串口功能单元。 S3C2440A 中每个串口单元都可以基于中断或基于 DMA 模式操作的。换句话说,串口可 以通过产生中断或 DMA 请求来进行 CPU 和串口之间的数据传输。串口使用系统时钟可以支 持最高 115.2Kbps 的比特率。如果是外部器件提供的外部时钟,则串口可以工作在更高的速 度上。 S3C2440A 的串口包括了可编程波特率,红外发送/接收,插入 1 个或 2 个停止位,5 位、 6 位、7 位或 8 位的数据宽度以及奇偶校验。 每个串口包含一个波特率发生器、发送器、接收器和一个控制单元。波特率发生器可以 由总线时钟或外部输入时钟驱动。发送器和接收器包含了 64 字节先进先出缓冲区和数据移 位器。将数据写入到先进先出缓冲区中接着在发送前复制到发送移位器中。随后将在发送数 据引脚移出数据。与此同时从接收数据引脚移入收到的数据,接着从移位器复制到先进先出 缓冲区中。 每个串口发送数据是可编程的。由 1 个起始位、5 至 8 位数据位、1 个可选奇偶校验位 以及 1 至 2 个停止位组成的帧,这是由行控制寄存器指定的。发送器也可以产生单帧发送期 间强制串行输出为逻辑 0 状态的断点状态。 与发送类似,接收数据帧也是可编程的。由 1 个起始位、5 至 8 位数据位、1 个可选奇 偶校验位以及 1 至 2 个停止位组成,也是由行控制寄存器指定的。接收器能够检测出溢出错 误、奇偶校验错误、帧错误和断点状态。 这些由 1 个起始位、5 至 8 位数据位、1 个可选奇偶校验位以及 1 至 2 个停止位组成的 帧。在串口单元中,是一位、一位的在串口线上发送的,发送的速度与串口单元的时钟速度 有关,时钟速度越快发送数据的速度越快。如图(P-2.2.4.1)所示。 图(P-2.2.4.1) 这些数据可能是其它数据,也可能是字符串的 ASCII 码。通常情况下我们发送的就字符 串,这样我们的开发板上的串口线连接上 PC 机,在 PC 机上就能看到开发板输出的信息了。 到时我们的操作系统内核就用 S3C2440A 中的第一个串口作为默认的输出端口。这样就 能看到操作系统内核输出的信息了,比如系统是否运行正常等,先在此初步认识一下,到时 我们写代码时再来看看怎么使用它。 2.2.5 中断控制器 我们先不说中断的概念,现在只要知道中断是各种设备通知 CPU 的一种方式。由于计 算平台上不止一个设备,所以需要一个专门的功能单元来管理这些通知信号即中断。让它来 决定各设备中断信号的先后次序以及是否允许各设备的中断信号到达 CPU,还有识别是哪个 设备的中断信号。这个功能单元就是中断控制器。如图(P-2.2.5.1)所示。 图(P-2.2.5.1) mini2440 开发板上没有这个功能单元,它存在于 S3C2440A 这个芯片内部。 S3C2440A 中的中断控制器接受来自 60 个中断源的请求。提供这些中断源的是内部外设 或者外部设备,如 DMA 控制器、串口、IIC、GPIO 等等。外部设备就是通过 GPIO 与 S3C2440A 内部连接起来的。GPIO 是什么,怎么连接的,我们不在此处介绍,那也不是必须要关心的。 当从内部外设和外部中断请求引脚收到多个中断请求时,中断控制器在仲裁先后次序后 才发送请求到 ARM920T 内核的 FIQ 或 IRQ 引脚,ARM920T 内核就是上图中的 CPU。这个我们 后面会介绍的。仲裁步骤由硬件优先级逻辑决定并且写入结果到中断挂起寄存器中,从而通 告用户是各种中断源中的哪一个发生了的中断。 什么是中断优先级仲裁,就是当两个不同的设备同时产生中断时,而 CPU 一次只能响应 一个中断请求,所以中断控制器要在两个或者多个中断信号中选择一个,然后把那个中断请 求发送给 CPU。和自然世界中一样,事情有轻重缓急之分,当然中断控制器最好选择一个要 求 CPU 尽可能快的处理其中断请求的设备。 S3C2440A 中的中断控制器只能连接 32 根中断信号线,那么它是怎么支持 60 个中断源 的呢?它采用了二级中断源,就是把一级中断的几根中断信号线,把它们其中每根都连接上 几个设备的中断信号线。所以当那个中断发生时,我们就不能单纯的认为是一个设备发出了 中断,而是要进一步根据次级中断的相关信息确定是哪个设备发生了中断。如图(P-2.2.5.2) 所示。 图(P-2.2.5.2) 操作系统内核的重要功能就是要代替应用软件操控设备,然而设备与 CPU 的通信大多 数是由中断信号来驱动的。中断控制器就是它们之间的必经之路。所以操作系统内核必须要 了解并控制中断控制器。在此有个初步印象即可,后面介绍操作系统内核的中断管理时,那 时我们会详细介绍它…… 2.2.6 SDRAM SDRAM 是同步动态随机存储器,同步是指它工作需要同步时钟,内部的命令的发送与数 据的传输都以这个时钟为基准的。动态是指它内部的存储阵列需要不断的刷新来保证数据不 丢失。它是可以随机访问的,就是说可以自由的在指定的地址上进行数据读写。当然也不是 绝对自由的,这个访问地址是由储存控制器和总线决定的。我们通常说的内存就是用这种芯 片和技术实现的。 mini2440 开发板有 64MB 的 SDRAM,它是用两片 32MB 的 SDRAM 芯片组成的 ,这两片 32MB 的 SDRAM 芯片存在于开发板上,而它的储存控制功能单元则存在于 S3C2440A 芯片内 部。通过 S3C2440A 芯片的引脚和外部的 SDRAM 芯片通信的。 我们先介绍 S3C2440A 芯片内部的储存控制功能单元,然后在来介绍 mini2440 开发板 上的两片 SDRAM 芯片。 S3C2440A 芯片内的存储器控制器为访问外部存储器的需要提供了存储器控制的信号。 S3C2440A 芯片内的存储器控制器包含以下特性: 1. 可通过软件选择大、小端。 2. 总共 8 个存储器 Bank,每个 Bank 有 128M 字节,总共 1G 地址空间。 3. 除了 Bank0(16/32 位)之外,其它全部 Bank 都可编程访问宽度(8/16/32 位)。 4. 六个存储器 Bank 为 ROM,SRAM 等。其余 2 个存储器 Bank 为 ROM,SRAM ,SDRAM 等。 5. 7 个固定的存储器 Bank 起始地址,1 个可变的存储器 Bank 起始地址并且 Bank 大 小可编程。 6. 所有存储器 Bank 的访问周期可编程。 7. 支持外部等待扩展总线周期。 8. 支持 SDRAM 自刷新和掉电模式。 如图(P-2.2.6.1)所示。 图(P-2.2.6.1) 上图中的 SROM 表示的是这个 Bank 只能连接 SRAM 或者 ROM 类型的芯片。Bank6 和 Bank7 上 所连接的芯片大小必须相等。比如 Bank6 上连接是 32MB 的 SDRAM,那么如果 Bank7 上要连 接储存芯片的话,也必须是 32MB 大小的 SDRAM。上图也是我们 mini2440 开发板上的实际情 况。除 norflash 和两片 32MB 的 SDRAM 芯片外,其余的都在 S3C2440A 芯片内部,norflash 和两片 32MB 的 SDRAM 芯片是在开发板上的。 上面简单的说明了 S3C2440A 的储存控制器和储存系统的物理地址空间,接着来看看 SDRAM 芯片。数据和程序都是放在它里面运行的,当然操作系统内核也毫不例外的是放在它 里面的。 mini2440 开发板使用了两片外接的 32MB 的 SDRAM 芯片,型号为:HY57V561620 或者 MT48LC16M16A2,它们并接在一起形成 32 位的总线数据宽度,这样可以增加访问的速度,并 且它们的物理起始地址为 0x30000000。可能不同时期出厂的开发板用 SDRAM 芯片型号不一 样,但是功能是一样的。 SDRAM 内部是一个存储阵列。可以把它想象成一个表格。和表格的检索原理一样,先指 定行,再指定列,就可以准确找到所需要的存储单元。这个表格称为逻辑 Bank。目前的 SDRAM 基本都是 4 个 Bank。寻址的流程就是先指定 Bank 地址,再指定行地址,最后指定列地址。 这就是 SDRAM 的寻址原理。笔者的开发板是用的 SDRAM 芯片是 32M 的 HY57V561620,这是一 个 4Banks*4M*16bit 的 SDRAM,也就是由 4 个逻辑块即 Logical Bank,简称 L-Bank 组成, 每个 L-Bank 有 4MB 存储单元。2 片总共空间为 64MB,连接到 S3C2440A 的 Bank6 上,所以访 问的物理地址空间为 0x30000000~0x33ffffff。SDRAM 芯片结构如图(P-2.2.6.2)所示。图 有点专业,暂时看不懂也没什么问题,有个印象即可。 图(P-2.2.6.2) 对于我们程序员来说,只要认为它就是内存并且是这样的:由 8 个可以储存 1 位二进制 数据的空间组成的小格子,每个小格子有个地址,通过这个地址可以向小格子里读写 8 位二 进制的数据,2 个小格子就可以存放 16 位二进制数据、4 个小格子就可以存放 32 位二进制 数据。最后许多这样的小格子组成一个很大的储存空间。如图(P-2.2.6.3)所示。 图(P-2.2.6.3) 注意的是,内存芯片即 SDRAM 并不决定内存的物理地址从哪里开始,它只是一个存放数据的 空间,如果 32MB 大小的内存,每个字节一个地址,那么它就有 32M 个地址编码。内存开始 的物理地址是储存控制器和内存芯片地址信号线决定的,比如储存控制器地址译码信号是从 0x30000000 地址开始的,那么这个内存可以访问的有效地址空间就是从 0x30000000 开始到 0x31ffffff 结束。 我们了解了 S3C2440A 芯片的地址空间,又粗略的看了下 SDRAM 芯片的内部结构,最后 只要知道 mini2440 开发板上有两片 32MB 的 SDRAM 芯片,并且它们并接到 S3C2440A 芯片的 Bank6 上。因此地址空间是 0x30000000~0x33ffffff。操作系统内核和应用软件、数据,都 是放在这个里面的,CPU 就能通过内存地址运行其中的程序和操作其中的数据。后面章节中 还有更多关于内存的内容,到时会详细介绍的。 2.2.7 Norflash FLASH 存储器称为闪存,它结合了 ROM 和 RAM 的特点,不仅具备电子可擦除、可编程的 特点,还可以快速读写数据,然而数据不会因为断电而丢失,这是它主要的特点。这种特性 使它可以用于 U 盘、MP3、智能手机等众多移动智能设备上。 目前 FLASH 闪存芯片主要有两种:Norflash 和 Nandflash。我们说的就是 Norflash。 mini2440 开发板上有一片 2MB 大小的 Norflash 芯片,它连接在 S3C2440A 的 Bank0 上, 并且可以通过跳线控制。 mini2440 开发板上的 Norflash 芯片特性如下: 1. 有 22 根地址信号线 和 16 根数据信号线。 2. 掉电不会丢失数据。 3. 支持“片上运行”。 由于它有 22 根地址信号线 和 16 根数据信号线,所以它能索引 Norflash 芯片每个个 储存字,一次可以传输 16 位数据,刚好是一个字。这就像 SDRAM 芯片一样,可以被 CPU 直 接读写。掉电它也不会丢失数据。这样写入其中的代码和数据就可以长久的保存下来。如果 它里面包含可执行代码,那么就可以在不需要 SDRAM 芯片的情况下就能执行程序。什么情况 下需要这样的储存芯片呢。例如 PC 机上 BIOS 系统,CPU 运行第一个程序就是 BIOS 程序, 那个时候连内存都还没初始化呢。再就是 mini2440 嵌入式开发板的上引导程序,也可以说 是 BIOS,它也完成了 BIOS 绝大部分工作,只是叫法不同而已。mini2440 开发板在上电时, S3C2440 芯片时钟和 SDRAM 芯片控制器,都还没有初始化呢。所以要借助 Norflash 储存技 术。 我们利用 JTAG 技术,向 Norflash 芯片中烧入引导程序,这个引导程序负责初始化开发 板上的各种设备,例如时钟、SDRAM、串口等。初始化完设备之后,就装载操作系统内核。 mini2440 开发板上 Norflash 芯片中烧写的是 supervivi 引导程序。这个引导程序功能非常 多,我们要用到的是从 PC 上下载我们的操作系统内核到 mini2440 开发板上的 SDRAM 芯片 中,这非常方便。 Norflash 芯片内部的实现细节我们不需要知道,我们只要知道 mini2440 开发板上有 2MB 的 Norflash 芯片,它就相当于掉电不丢失数据的内存,当然它的速度赶不上内存,它 里面存放的是一个引导程序,我们还可以通过拨动 mini2440 开发板上一个开关就可以从 Norflash 芯片开始启动,并且它连接在 S3C2440A 的 Bank0 上,开始的物理地址是 0,S3C2440A 中的 ARM920TCPU,一上电就是从地址 0 开始执行第 1 条指令,这样刚好就运行了 Norflash 芯片中的引导程序。 2.2.8 Nandflash Nandflash 也是 flash 闪存的一种,其内部采用非线性单元设计模式,为固态大容量闪存 的实现提供了廉价有效的解决方案。Nandflash 存储器具有容量较大,改写速度快等优点, 适用于大量数据的存储,广泛应用于嵌入式产品中。 由于内部结构相对简单,Nandflash 芯片使用复杂的 I/O 口来串行地存取数据,各个产 品或厂商的方法可能各不相同。8 个引脚用来传送控制、地址和数据信息。所以不能像 Norflash、SDRAM 等芯片,可以直接和 CPU 等设备相连并且寻址到每个储存字节。因此 Nandflash 芯片需要为它设计专门的控制器才能和其它设备进行通信。 mini2440 开发板上有两种 Flash 闪存,一种是前面说过的 Norflash 闪存,大小为 2MB; 另一种是 Nandflash,型号为 K9F1G08,大小为 128MB,旧版本为 K9F1208,大小为 64MB。 Norflash 和 Nandflash 闪存的芯片都存在于 mini2440 开发板上,前面说过 Norflash 有足 够的地址引脚和数据引脚,因此它可以直接提供类似 SRAM 接口和 CPU 相连、通信。而 Nandflash 闪存则不行,它需要专门的 Nandflash 控制器,这个 Nandflash 控制器就存在于 S3C2440A 这个芯片中。S3C2440A 支持这两种 Flash 启动系统,通过拨动跳线开关,可以选 择从 Norflash 还是从 Nandflash 启动系统。实际的产品中大都使用一片 Nandflash 就够了, 因为 mini2440 开发板是为了方便用户开发学习,因此还保留了 Norflash。 要操作 Nandflash 芯片,从中读取和写入数据。那么就要 Nandflash 控制器进行操作, 对它进行编程,通过 Nandflash 控制器去操作 Nandflash 芯片。对 S3C2440A 芯片上的 Nandflash 控制器的编程步骤如下: 1. 写命令寄存器,向 Nandflash 控制器写入相关命令,对应于 Nandflash 芯片的命令 周期。 2. 写地址寄存器,向 Nandflash 控制器写入相关地址,比如上面输入的读写命令它是 需要读写地址的,对应于 Nandflash 芯片的地址周期 3. 读/写数据寄存器,读/写入数据到 Nandflash 存储器,根据上面相应的命令做相应 的动作,可能是读也可能是写数据,对应于 Nandflash 芯片的读/写周期。 4. 读 Nandflash 控制器的主 ECC 寄存器和备份 ECC 寄存器。对数据进行校验。 等到我们用到 Nandflash 控制器时,再来详细研究其细节。下面我们去看看 Nandflash 芯片的结构。 Nandflash 芯片的数据是以位的方式保存在存储单元里,一般来说,一个存储单元中只 能存储一个位。这些存储单元以 8 个或者 16 个为单位,连接成位行,这些位行会再组成页, 这些页面会再组成块。Nandflash 芯片有多种结构,笔者使用的 Nandflash 芯片是 K9F1208, 它是这样的,每页 528 字节大小,其中 512 字节用于存放数据,还有 16 字节存放这个页面 的数据的校验信息,每 32 个页面形成一个块,块大小为 32*528 字节。具体一片 flash 上 有多少个块是根据需要决定的。K9F1208 的 Nandflash 芯片具有 4096 个块,故总容量为 4096*(32*528 字节)一共是 66MB,但是其中的 2MB 是用来保存 ECC 校验码等额外数据的, 故实际中可使用的空间为 64MB。如图(P-2.2.8.1)所示。 图(P-2.2.8.1) Nandflash 芯片以页为单位读写数据,所以一次读写最少为 512 字节大小的数据,不能 像 Norflash 芯片一样可以以字为单位读写数据。而 Nandflash 芯片是以块为单位擦除数据 的。按照这样的组织方式可以形成所谓的三类地址: 1. 列地址。 2. 页地址。 3. 块地址。 对于 Nandflash 来讲,地址和命令只能在 I/O[7:0]引脚上传递,数据宽度是 8 位。 由于 Nandflash 芯片的工艺不能保证存储单元在其生命周期中保持性能的可靠,因此, 在 Nandflash 芯片的生产中及使用过程中会产生坏块。为了检测其可靠性,在应用 Nandflash 芯片的系统中一般都会采用一定的坏区管理策略,而管理坏区的前提是能比较可靠的进行坏 区检测。如果操作时序和电路稳定性不存在问题的话,Nandflash 芯片出错的时候一般不会 造成整个块或是页面不能读取或是全部出错,而是整个页面例如 512 字节中只有一个或几 个位出错。对数据的校验常用的有奇偶校验、CRC 校验等,而在 Nandflash 芯片处理中,一 般使用一种比较专业的 ECC 校验。ECC 能纠正单比特错误和检测双比特错误,而且计算速度 很快。 我们只要知道 Nandflash 芯片就和 PC 机中硬盘一样,只能一次以页面为单位读取数据, 最少是 512 字节。CPU 要访问 Nandflash 芯片需要通过专门的 Nandflash 控制器,其动作是 对 Nandflash 控制器发送相关的命令。Nandflash 芯片就说到这里吧,脑中有个印象即可。 2.8 小结 本章从我们选择的计算平台——mini2440 开发板开始,描述了为什么要选择这个计算 平台、这个计算平台上都有些什么芯片、我们的操作系统内核要运行起来必须要关注这个计 算平台的哪些芯片和功能单元、分别简单的概述了这些必须要关注的芯片、功能单元,以便 于我们的大脑中有个印象,使用到它们时,知道它们是什么,有什么功能和作用。我们了解 到的芯片、功能单元如下: 1. RTC;处理自然时间的,可以用备用电池供电维持时间。 2. 定时器;S3C2440A 芯片内部有 5 个定时器,可以用于处理间隔时间,比如一个进程 要等待 5ms 后去访问硬件,等等…… 3. 串口;用于和其它设备通信或者传输数据,比如连接 PC 机输出信息,并且 S3C2440A 芯片内部有三个相同的串口硬件。 4. 中断控制器;它存在于 S3C2440A 芯片内部,它是设备和 CPU 通信的重要机制,它 支持多达 60 个设备中断源,同时也支持中断优先级,哪些设备的中断要尽快发送 给 CPU 内核进行处理,也可以屏蔽某个设备的中断信号。 5. SDRAM;即常说的内存,mini2440 开发板用了两片 32MBSDRAM 芯片并接在一起, 一共 64MB 大小,SDRAM 开始的物理地址是 0x30000000。我们的操作系统内核和 各个应用软件都是放在这个里面运行的。 6. Norflash;就像是掉电不会丢失数据的内存,支持在其上运行程序。mini2440 开发板 上有 2MB 大小的 Norflash 芯片,连接在 S3C2440A 芯片的储存控制器的 Bank0 上, 并能通过跳线开关控制。这个芯片里烧写了一个叫 supervivi 的开发板引导程序,相 当于 PC 机上的 BIOS 程序,它负责初始化开发板的时钟和 SDRAM 等其它设备,然 后加载操作系统内核。 7. Nandflash;它需要专门设计的控制器来进行读写,一次读写最少是 1 页,1 页最少 是 528B,512B 数据加 16B 的校验信息,32 页组成一个块。我们的开发板上这个芯 片是 64MB 大小。可以用于操作系统构建文件系统,在其上存放用户数据、应用软 件、操作系统等。它也支持系统引导,S3C2440A 芯片自动把它的前 4KB 空间的数据 读到一个内部 4KB 大小的 SRAM 中,把那个 SRAM 的首地址映射为 0,S3C2440A 芯 片内部的 CPU 内核就从内部 4KB 的 SRAM 中开始运行了。 走到这里,已经对我们的计算平台有了初步的印象,了解了组建一个计算平台需要些什 么,同时初步的知道了一个操作系统内核至少要实现什么功能,以及必须要与之打交道的硬 件…… 3 处理器 一个计算平台最重要的部分,是要有一颗性能良好、功能强大的处理器,就是通常说的 CPU,它完成各种数据处理、计算、控制等任务。既然我们要写一个具体计算平台上的操作 系统内核,就必须要仔细了解它。我们的这个计算平台上的 CPU 是一个叫 ARM 的公司设计 的。那么好,首先从这个公司开始、接着了解了我们要关注的处理器叫 ARM920T 并且了解了 它的内部结构,知道了它的内部结构,然后就是了解它的储存体系、运行状态、工作模式、 有哪些寄存器、还有它的中断异常处理机制、有多少条指令、以及 MMU 和 cache 等…… 好了,本章节我们就去详细讨论处理器的细节…… 3.1 ARM ARM 即 Advanced RISC Machines,是一种基于精简指令集的处理器,通常也指代这种处 理器体系,同时也可代表开发这种处理器体系的公司。 下面去看看关于 ARM 的介绍…… 3.1.1 ARM 介绍 1990 年 12 个富有远见和梦想的人,在英国剑桥大学的一个谷仓之中成立了 ARM 公司。 ARM 是微处理器行业的一家知名企业,设计了大量高性能、廉价、耗能低的 RISC 处理器、 还有相关的技术及软件。 ARM 的商业模式,主要是出售芯片设计技术的授权,而非生产和销售实际的半导体芯片。 ARM 向所有合作伙伴授予 ARM 技术知识产权许可证。这些合作伙伴来至世界领先的半导体公 司和各大 IT 企业。这些合作伙伴可利用 ARM 技术的授权设计创造和生产片上系统,但需要 向 ARM 公司支付技术授权的许可费用并为生产的每块芯片交纳版税。除了处理器技术授权 外,ARM 还提供了一系列软件工具。正因为如此,基于 ARM 解决方案的芯片和软件体系十分 庞大。 采用 ARM 技术授权的微处理器,就是我们通常所说的 ARM 微处理器,已遍及工业控制、 消费类电子产品、通信系统、网络系统、无线系统等各类产品市场,比如 MP3、数字机顶盒、 智能手机、汽车多媒体和制动系统等,基于 ARM 技术的微处理器应用约占据了 32 位 RISC 微 处理器 90%以上的市场份额,ARM 技术正在逐步渗入到我们生活的各个方面。 ARM 处理器到今天已经有 8 个体系版本了。这些体系版本各有特点,比如支持 32 位地 址空间,最开始只有 26 位地址空间、有的支持乘法指令、有的支持调试接口、有的支持 Thumb 精简指令集等…… 根据这些体系版本,各大生产商们生产了不同的 ARM 微处理器系列,如下: 1. ARM Securcore 系列 2. ARM7 系列 3. ARM9 系列 4. ARM11 系列 5. Cortex-M 系列 6. Cortex-R 系列 7. Cortex-A 系列 关于 ARM 公司我们就说到这里,因为这个公司并不是我们关注的重点,我们要关注的是 这个公司的微处理器。 3.2 ARM920T 三星 S3C2440A 芯片自称为处理器,但其实它是采用了 ARM 公司设计的 ARM920TCPU 加上一些其它组件,然后把它们封装在一起形成的一个多功能芯片,对于这里我们关注的是 ARM920TCPU,下面就去看一看它的内部结构…… 3.2.1 ARM920T CPU 结构 mini2440 开发板采用了 S3C2440A 芯片,而 S3C2440A 芯片又采用了 ARM 公司设计的 ARM920T CPU。唉,人类的世界可真复杂啊…… 上次研究了计算平台——mini2440 开发板。发现上面有个叫 S3C2440A 的黑盒子,并打 开了它,发现它里面又有许多黑盒子,并且对其中一些黑盒子做了简要的介绍,如图( P3.2.1.1)所示。另外为了便于阅读,才把重复的东西又搬到这儿的,由此也增加了篇幅,但 是笔者认为这是值得的,因为来回翻书会浪费宝贵的时间。 图(P-3.2.1.1)就是 S3C2440A 这个黑盒子内部结构图,其实早已看过,不过上次我们没 有继续拆开它里面的黑盒子。它其中有个非常重要的黑盒子——ARM920T,它才是这次要关 注的重点,下面打开它,看看它里面又有什么神奇的玩意儿。如图(P-3.2.1.2)所示。 图(P-3.2.1.1) 图(P-3.2.1.2) ARM920T 里面的黑盒子以及功能单元又不少,如下: 1. JTAG;一种调试或者测试芯片的接口,可以通过 JTAG 接口访问 CPU 的内部寄存器 和挂在总线上的设备,如 FLASH,RAM 等,我们不必过多关注。 2. ARM9TMDI;它才是真正运行程序的。 3. 指令、数据 MMU;包括两个 C13 一起处理内存地址映射的。 4. 指令、数据 CACHE;包括写回 PA TAG RAM、写缓冲,一起完成高速缓存的功能。 5. CP15;这里是用于控制 MMU 和 CACHE 的,它还可以控制别的器件,不同的系统配 置下是不同的。 6. 外部协处理器接口;ARM920T 可以加入多达 15 个协处理器,比如增加一个浮点运 算处理器等。 7. AMBA 总线接口;ARM920T 内部就是通过它和外部内存及其它设备通信的。 不过不需要把所有的东西都弄的一清二楚。我们主要关注四个部分: 1. CPU 内核;它和 ARM920T 内部的其它部件一起工作,用来运行程序的。包括操作系 统内核和应用程序。 2. MMU;映射内存的,主要用于实现虚拟内存,什么是虚拟内存后面会介绍的。 3. CACHE;缓存内存中的指令和数据的,但它比内存小很多但快很多,把内存中的一 部分内容放在 CACHE 里面,这样可以加快程序的运行速度,后面也会详细介绍的。 4. CP15;它在 ARM920T 内部称为协处理器,在这里它是专门用于控制 MMU 和 CACHE 的。可以看到 ARM920T 还能通过外部协处理器接口增加其它协处理器。 CPU 内核对应于图(P-3.2.1.2)中的是 ARM9TMDI。我们操作系统和应用程序都得它去 执行,所以我们写操作系统内核之前,必须先仔细了解它,以及 ARM920T 的其它组件。后 面会重点研究 ARM920T 的程序员模型,即指令集、工作模式、寄存器等。 MMU 对应于图(P-3.2.1.2)中的是指令 MMU、数据 MMU。为什么两个 MMU,通常情 况下一个 MMU 不就够了吗?是的,大部分 CPU 确实只有一个 MMU,然而这里有两个,这 是因为 ARM920T 内部采用了哈佛架构。哈佛架构是一种将程序指令存储和数据存储分开的 存储器架构。是一种并行体系架构,指令和数据可以独立存取,这样就大大的提高了程序的 运行性能。 CACHE 对应于上图(P-3.2.1.2)中的是指令 CACHE、数据 CACHE,同样也是因为哈佛架 构的原因才会有两个 CACHE。它们都是 16KB 大小。 关于 CACHE 的工作机制,后面会专门 介绍,现在只要知道它是用来缓存内存中的数据的,速度比内存快很多。 CP15 对应于上图(P-3.2.1.2)中的是 CP15,它被称为协处理器。在 ARM920T 中它控制 着 MMU 和 CACHE,比如我们要打开或者关闭 MMU、改变地址映射关系、打开或者关闭 CACHE、锁定 CACHE 中的内容、使 CACHE 中的数据变为无效等。这些功能都将通过操作 CP15 协处理器而达到,后面会专门详细介绍它。 我们大致了解了 ARM920TCPU 内部结构,下面来简单的了解一下它的特性。 首先往大的说 ARM920T CPU 支持特性如下: 1. 支持 16 位 Thumb 压缩指令集。 2. 支持片上 Debug。 3. 内嵌硬件乘法器。 4. 嵌入式 ICE,支持片上辅助断点调试。 随着后面的介绍,会逐步了解这些特性。 ARM920T CPU 是 RISC 体系的,即精简指令集体系。当然以之对应就是 CISC 体系,即复 杂指令集体系,CISC 体系有个著名的实例——早期 x86 CPU,现在的 X86 CPU 不完全是 CISC, 它也结合了 RISC 优点。CISC 体系它的指令多、指令长短不一、每条指令完成的功能相当多 等……这里不过多的讨论 CISC 体系。 ARM920T CPU 是 RISC 体系但也改进了 RISC 体系。RISC 体系特性及 ARM 改进的特性如 下: 1. 指令长度固定。 2. 有大量的寄存器。 3. 大多数操作都在寄存器中进行。 4. 通过装载、储存两个操作,在寄存器和内存之间交换数据。 5. 内存寻址方式简单。 6. ARM 改进了,在一条数据处理指令中同时加入算术、逻辑、移位等操作。 7. 使用地址自动增加或者减少技术,ARM 改进和优化了程序中的循环处理。 8. ARM 改进了装载和储存指令,使之能一次装载或者储存多个内存字,由此增加了数 据传送性能。 9. ARM 还让大部分指令能根据前一条指令执行的结果状态,决定是否执行当前这条指 令。这大大提高了程序的运行效率。 上面这多的特性,可能一时一刻还不能全明白。别怕,因为计算机的知识体系从来就没 有顺序,它们有时是互相交错的,暂时不明白,没事儿,只要我们不放弃,走完这一段路, 再回首,就会全明白的。 ARM920T 内部结构和它的一些简单的特性就说到这里,下面我们去仔细研究一下它, 这个有点难,但是别怕,慢慢来,要相信自己。要知道,处理器从来就不是一个简单的玩意 儿。 3.3 ARM920T 储存体系 处理器主要是运行程序和处理数据的,程序和数据大多数情况下是放在储存器中的,处 理器要运行程序和处理数据,首先要解决访问储存器的一系列问题。 ARM920T 储存体系有点小复杂,因为它里面还有 MMU 和 CACHE,这些玩意儿的关系很复 杂。开始时我们还不能一竿子捅到底,得一步一步慢慢来,先把 MMU 和 CACHE 从 ARM920T 中 拿掉,相当于 ARM920T 中没有了 MMU 和 CACHE 这些部件。本节中都是假定 ARM920T 中没有了 MMU 和 CACHE。MMU 和 CACHE 这两个部件,后面章节会详细介绍的。到那时我们再把它们联 系起来。 3.3.1 ARM920T 地址空间 开始之前,我们先看看 ARM 体系中的字节、半字、字、双字,它们究竟表示能存放多少 个二进制位。可能其它 CPU 体系与此不同。 1. 字节;能存放 8 个二进制位。 2. 半字;能存放 16 个二进制位。 3. 字;能存放 32 个二进制位。 4. 双字;能存放 64 个二进制位。 ARM920T CPU 使用的是单一扁平的地址空间。由于 ARM920T CPU 是 32 位的,所以它使 用了 0~232-1 之间的无符号整数来表示一个地址值。其地址空间大小是 232 个可以存放 8 个二 进制位的字节。可以把它想象成线性的字节数组,用伪 C 语言描述是这样的:unsigned char a[232]。 ARM920T 的地址空间也可以看成是 230 个 32 位的字单元,用伪 C 语言描述是这样的: unsigned int a[230]。每个字单元的地址都能被 4 整除,即地址值的最低两个二进制位是 00。这个 32 位的数据如果从 0 地址开始存放,那么地址 0、地址 1、地址 2、地址 3 就包含 这整个 32 位的数据。 ARM920T 的地址空间还可以看成是 231 个 16 位的半字单元,用伪 C 语言描述是这样的: unsigned short int a[231]。每个半字单元的地址都能被 2 整除,即地址值的最低 1 个二 进制位是 0。这个 16 位的数据如果从 0 地址开始存放,那么地址 0、地址 1 就包含这整个 16 位的数据。 3.3.2 ARM920T 储存器格式 要运行程序就得把程序指令和一些数据存放在储存器中,这个储存器大多数情况下是内 存,即 SDRAM 芯片,下面我们就认为这里的储存器即是内存,数据在内存中是怎么存放的, 有些什么方式,这都是问题。 先来看看一个字节数据:“0xFF”怎么存放在内存中的。假如数据都是从内存地址 0 开 始存放的。因为内存可以寻址的最小单位是 1 字节,而数据也只有 1 个字节,因此只有一种 情况,如图(P-3.3.2.1)所示。 图(P-3.3.2.1) 然后来看看一个 16 位的半字数据:“0xEEFF”怎么存放在内存中的。因为数据有了两个 字节,放在内存中也要占用两个字节。因此有两种情况,如图(P-3.3.2.2)所示。 图(P-3.3.2.2) 图(P-3.3.2.2)中,一种情况是:半字数据“0xEEFF”的最高有效位放在内存的高地址字节 中。第二种情况是:半字数据“0xEEFF”的最高有效位放在内存的低地址字节中。 最后来看看一个 32 位的字数据:“0xCCDDEEFF”怎么存放在内存中的。因为数据有了四 个字节,放在内存中也要占用四个字节。因此还是有至少两种情况,如图(P-3.3.2.3)所示。 图(P-3.3.2.3) 和上述一样,还是有两种情况分别是:数据的最高有效位放在内存的高地址字节中或者数据 最高有效位放在内存的低地址字节中。 人们常说,某某处理器是大端或者小端的。表示的就是上面的这些情况。 1. 小端模式;数据的最高有效位放在内存的高地址字节中。 2. 大端模式;数据的最高有效位放在内存的低地址字节中。 ARM920T CPU 既支持小端模式也支持大端模式。这是通过 CP15 协处理器控制的,后面 会介绍怎么对它进行编程,达到这一目的。 3.3.3 ARM920T 储存地址对齐 在 ARM920T 中访问储存器时通常希望的是:如果是字单元访问,那么这个访存地址就 应该是字对齐的,即地址的最低两个二进制位是 00;如果是半字单元访问,那么这个访存 地址就应该是半字对齐的,即地址的最低二进制位是 0;如果字单元访问或者半字单元访问 的访存地址不是字对齐或者半字对齐,这就称为地址非对齐的访存操作,有很多处理器非对 齐的访存操作,最多影响处理器的性能,不会导致程序运行结果错误,而 ARM920T 是不允 许的,对它而言非对齐的访存操作会带来灾难性后果。 当 ARM920T 处于 ARM 状态时(什么是 ARM 状态和 Thumb 状态后面会有介绍),ARM920T 运行的是 ARM 指令集,每条指令长度都是 32 位,即一个字。这时写入 PC 寄存器即程序指 针寄存器中的值如果不是字对齐的。要么程序的运行结果不可预知,要么地址的最低两个二 进制位被硬件忽略。当 ARM920T 处于 Thumb 状态时, ARM920T 运行的是 Thumb 指令集, 每条指令长度都是 16 位,即一个半字。这时写入 PC 寄存器中的值如果不是半字对齐的。也 和 ARM 状态时一样,要么程序的运行结果不可预知,或者地址的最低二进制位被硬件忽略。 3.4 ARM920T 状态 ARM920T 处理器有两种状态,ARM 状态和 Thumb 状态。这两种状态之间可以通过软件 或者硬件来回切换。ARM920T 处理器根据 CPSR 寄存器中的 T 位来分辨这两种状态。先不必 管这是个什么寄存器,后面会有介绍的。 1. CPSR 中 T 位为 0;表示 ARM920T 运行在 ARM 状态。 2. CPSR 中 T 位为 1;表示 ARM920T 运行在 Thumb 状态。 要注意的是,这里说的处理器状态和后面要介绍的处理器的工作模式是两码事,不可等 同而论。 3.4.1 ARM 状态 处于 ARM 状态时,ARM920T 运行的都是 32 位的指令,指令地址是字对齐的,即指令 地址的最低两位二进制位为 0。指令的运行效率非常高。同时也能访问系统中所有的资源和 寄存器等。因为 ARM 状态时的指令都是 32 位的长度,因此也增加了程序运行时的储存空 间。在以下几种情况下 ARM920T 处理器会自动进入 ARM 状态。 1. 第一次上电时;处理器完成自身硬件初始化后,进入 ARM 状态从 0 地址开始读取 第一条指令并开始运行。 2. 响应中断和异常时;当处理器响应外部硬件中断和处理处理器异常时,ARM920T 会 自动切换到 ARM 状态。 上面描述的两种情况,都是处理器硬件自动完成的,不需要程序员的任何操作。不管发生上 述两种情况时处理器先前是什么状态,处理器都会自动切换到 ARM 状态。当然程序员可以 手工的切换处理器状态,用处理器的 BX 指令可以做到,这是个带状态切换的程序跳转指令, 实际上它是根据跳转的地址值的第一个二进制位来判断的,如果该位为 1 就切换到 Thumb 状态,否则该位为 0 就切换到 ARM 状态。 3.4.2 Thumb 状态 处于 Thumb 状态时,ARM920T 运行的都是 16 位的指令,指令地址是半字对齐的,即指 令地址的最低一位二进制位为 0。这些指令都是对 ARM 的一些指令进行重新编码得到的一 个 ARM 指令集的子集。只能访问系统中有限的寄存器和资源。因为 Thumb 状态时的指令都 是 16 位的长度,因此也大大减小了程序运行时的储存空间。同时这对 16 位的储存系统很有 效。Thumb 指令并没有改变 ARM 底层的设计模型,只是在高层增加了一些限制,它的数据 操作和程序寻址还是 32 位的。ARM920T 处理器不会从任何状态自动切换到 Thumb 状态。 这需要程序员手动切换,用 BX 指令,可以从 ARM 状态切换到 Thumb 状态,执行一段 Thumb 指令的程序,然后从 Thumb 状态返回到 ARM 状态。如果在 Thumb 状态中运行 Thumb 程序 时产生中断或者异常,处理器自动进入 ARM 状态,处理完中断或者异常,然后处理器可以 从 ARM 状态中返回到 Thumb 状态下继续运行。 不必过多关注 Thumb 状态和 Thumb 指令,因为我们的操作系统代码始终是运行在 ARM 状态下的。并且还要求我们操作系统下的应用程序也运行在 ARM 状态下,这样就简单多了。 3.5 ARM920T 工作模式 ARM920T 处理器一共有七种工作模式,如下: 1. 系统管理模式。 2. 数据访问终止模式。 3. 未定义指令终止模式。 4. 外部中断模式。 5. 快速中断模式。 6. 系统模式。 7. 用户模式。 以上这 7 种工作模式除用户模式外,其它 6 种模式都可以称为特权模式。ARM920T 处 理器用 CPSR 寄存器中几个二进制位来识别处理器的工作模式,等到介绍寄存器时,再来详 细讨论它及其中的二进制位。为什么要有这么多工作模式,并且还要分有没有特权?有一点 很重要:就是为了保护系统资源,比如一个行为不端的应用软件它去访问硬件,但是由于它 运行在用户模式下没有访问操作硬件的权力,处理器就能捕获这个错误。 3.5.1 系统管理模式 系统管理模式,这是一种特权模式,在这种模式下软件程序可以自由访问所有硬件资源, 还可以自由切换到其它工作模式。当 ARM920T 处理器第一次上电时,就会自动进入这种模 式,如果用户模式下软件执行 SWI 指令,即软中断指令,处理器执行完指令后就会切换到系 统管理模式。这种工作模式通常用于执行操作系统内核代码。 3.5.2 数据访问终止模式 当 ARM920T 处理器操作、访问数据时,数据的地址不存在,或者这个地址上的数据不 允许被操作或者访问,这时处理器就会自动进入到这个数据访问终止模式。它也是一种特权 模式,在这种模式下程序可以自由访问所有硬件资源,也可以自由切换到其它工作模式。这 种工作模式通常被操作系统用于虚拟内存管理。比如对操作系统的内存空间进行保护,不允 许应用程序访问系统代码数据的内存空间。 3.5.3 未定义指令终止模式 当 ARM920T 处理器执行程序时,执行到一条未定义的指令,即它不认识这条指令时。 这时处理器就会自动进入到这个未定义指令终止模式。此工作模式也是一种特权模式,在这 种模式下程序可以自由访问所有硬件资源,同时可以自由切换到其它工作模式。这种工作模 式通常被系统用于支持软件仿真硬件协处理器。比如用软件仿真硬件浮点计算单元,假如我 们在程序指令数据中放一个处理器不认识的数据充当未定义的指令,这时处理器就自动切换 这个工作模式,最后我们在这种模式下用软件模拟完成浮点计算任务。 3.5.4 外部中断模式 当 ARM920T 处理器外部中断引脚有电子信号时并且 ARM920T 处理器这时允许响应外 部中断时,这时处理器就会自动进入到这个外部中断模式。这种工作模式也是一种特权模式, 在这种模式下程序可以自由访问所有硬件资源,同时可以自由切换到其它工作模式。这种工 作模式通常被系统用于普通的外部设备中断,以便和外部设备进行通信。 3.5.5 快速中断模式 当 ARM920T 处理器快速中断引脚有电子信号时并且 ARM920T 处理器这时允许响应快 速中断时,这时处理器就会自动进入到这个快速中断模式。这种工作模式也是一种特权模式, 在这种模式下程序可以自由访问所有硬件资源,同时可以自由切换到其它工作模式。这种工 作模式通常被系统用于高速的外部设备中断或者要求实时性响应的设备,以便处理器可以快 速、实时的和外部设备进行通信。 3.5.6 系统模式 系统模式除了有特权外,其它别的都和用户模式一样,但要切换到这种模式下,一定要 在其它特权模式下,通过程序手动切换。这种工作模式有特权,所以这种模式下程序可以自 由访问所有硬件资源,同时还可以自由切换到其它工作模式。通常用于运行操作系统服务程 序,这种程序像普通的应用软件,但是它需要自由访问硬件和系统资源。这种工作模式就刚 好派上用场了。 3.5.7 用户模式 用户模式没有特权,不能访问和操作硬件资源,同样也不能自由的切换到其它特权模式, 但是若出现下列三种情况 ARM920T 处理器就会自动切换到其它特权模式。 1. 数据访问和未定义指令异常;在用户模式下处理器操作数据和执行程序若遇到这两 种情况,处理器会分别切换到数据访问终止模式或者未定义指令终止模式。 2. 外部中断;在用户模式下处理器快速中断和外部中断引脚上有信号,并且处理器允 许响应它们。处理器会分别切换到快速中断模式或者外部中断模式。 3. SWI 指令;在用户模式下处理器若执行 SWI 软中断指令,处理器会切换到系统管理 模式。 这种用户模式最大的优点是它能有效的保护系统资源,防止恶意软件的不良行为,所以专门 用于运行应用软件,使构建现代意义的操作系统成为可能。 3.6 寄存器 处理器要处理数据和执行程序都必须要用到寄存器,寄存器是包含在处理器内部的一种 高速的暂存器,用于存放访存地址、临时数据和计算结果的,其大小大数多情况和处理器的 字长相等,比如 ARM920T 的寄存器都是 4 字节 32 位的,下面就去看看 ARM920T 处理器到 底有哪些寄存器。 3.6.1 ARM920T 寄存器 ARM920T 处理器是 RISC 体系的,这种体系最明显的特点就是有大量的寄存器。ARM920T 处理器一共算起来有多达 37 个寄存器。这 37 个寄存器大致可分为以下几类: 1. 通用寄存器。 2. 栈指针寄存器。 3. 程序链接寄存器。 4. 程序指针寄存器。 5. 程序状态寄存器。 这 37 个寄存器每个寄存器都是 32 位的,虽然有些寄存器只用到其中的几个位,比如程序状 态寄存器,但是它仍然是 32 位的。对程序代码来说同一时刻最多能看到 17 个寄存器,这取 决于 ARM920T 处理器的运行状态和工作模式,如图(P-3.6.1.1)所示。 图(P-3.6.1.1) 为什么会是这样,因为 ARM 公司采用了一种称为备份寄存器的技术,不同的工作模式下, 虽然使用了相同的寄存器名称,但是操作的却不是同一个物理寄存器。比如在快速中断模式 下向 R13 寄存器中写入“0”值,但这并不会改变其它模式下 R13 寄存器中的值。这些寄存 器的分布如下。 1. R0~R7、R15、CPSR;这几个寄存器在所有的工作模式下,都使用相同的物理寄存器。 2. R8~R12;这些寄存器除快速中断模式外,其它工作模式都是使用相同的物理寄存器。 3. R13、R14,这两个寄存器除系统模式和用户模式下使用相同的物理寄存器,其它工 作模式都是使用各自的备份寄存器,即不同的物理寄存器。 4. SPSR;这个寄存器,在系统模式和用户模式下没有这个寄存器,其它工作模式都有 各自的 SPSR 备份物理寄存器。比如在快速中断模式下访问 SPSR,访问的是快速中 断模式下的 SPSR 物理寄存器,在系统管理模式下访问 SPSR,访问的是系统管理模 式下的 SPSR 物理寄存器。 下面就去分别介绍这些寄存器。 3.6.2 R0-R7 寄存器 R0~R7 这 8 个寄存器可以称为通用寄存器,也可以称为“低寄存器”。这 8 个寄存器没 有用备份寄存器,也就是说 ARM920T 处理器不管运行在哪个工作模式下,对 R0~R7 的操作 都是访问的同一个物理寄存器。如果 ARM920T 工作在 Thumb 状态下,那么就只能访问这 8 个通用寄存器。 3.6.3 R8-R12 寄存器 R8~R12 这 5 个寄存器也是通用寄存器,同时也可以称为“高寄存器”。这 5 个寄存器除 快速中断模式使用了备份寄存器,其它工作模式都是使用相同的物理寄存器。也就是说除快 速中断模式外,ARM920T 处理器不管运行在哪个工作模式下,对 R8~R12 的操作都是访问的 同一个物理寄存器。如果 ARM920T 处理器工作在 Thumb 状态下,那么就不能访问这 5 个通 用寄存器。快速中断模式用于处理高速设备的中断,中断处理过程需要首先保存大量的寄存 器,即运行中断处理程序之前的 CPU 上下文。现在 ARM920T 处理器在响应快速中断时,就 不需要保存 R8~R12 这 5 个寄存器,因为在快速中断模式下 ARM920T 处理器有 R8~R12 这 5 个备份物理寄存器,在快速中断模式下使用这 5 个寄存器,并不会影响其它模式下的 R8~R12 寄存器,同时也加快了中断处理速度。 3.6.4 R13 寄存器 R13 寄存器,又可以称为 SP 寄存器,这个寄存器除系统模式和用户模式共用相同的物 理寄存器外,其它处理器工作模式都有各自的物理备份寄存器。这个寄存器常用作栈指针寄 存器。栈是一块内存空间,它存放数据有一定的规则,必须后进先出即最后存放进去的数据 要最先取出来,栈指针寄存器的值始终指向最后存放数据的内存地址。如果要用 C 语言写程 序就必须要用到栈。因为函数调用和函数中的局部变量都会用到栈。在 ARM920T 处理器上 从用户模式切换到系统管理模式下,程序的栈也会随着切换,因为用户模式和系统管理模式 的 R13 寄存器不是同一个物理寄存器。相应的其它模式也是一样的道理,除用户模式和系统 模式之间切换不会切换栈,其它模式的切换,程序的栈也会随着切换。另外如果 ARM920T 工作在 Thumb 状态下也能用这个 R13 寄存器作为栈指针寄存器。 3.6.5 R14 寄存器 R14 寄存器,又可以称为 LR 寄存器,这个寄存器除系统模式和用户模式共用相同的物 理寄存器外,其它处理器工作模式都有各自的物理备份寄存器。这个寄存器常用作程序链接 寄存器。我们的程序中有分支代码和函数调用,这就要使用跳转指令,对函数调用后要返回, 当我们用带返回的跳转指令时,这条指令就会把当前指令的下一条指令的地址放进处理器当 前模式下的 R14 寄存器中,函数返回时只要将 R14 寄存器的内容送给程序指针寄存器就行 了。如果处理器响应中断、异常时,处理器会自动将当前模式下程序指针寄存器的内容放在 相应中断、异常模式下的 R14 寄存器中,后面会详细介绍的。另外如果 ARM920T 工作在 Thumb 状态下也能用这个 R14 寄存器作为程序链接寄存器。其作用也和上面说的一样。 3.6.6 R15 寄存器 R15 寄存器,又可以称为 PC 寄存器,即程序指针寄存器。这个寄存器在所有的工作模 式下都共用相同的物理寄存器,并且在 Thumb 状态下也是使用这个寄存器作为程序指针寄 存器。它总是指向下一条要读取代码指令的地址,除跳转指令之外,每读取一条代码指令, 处理器就会自动对这个 R15 寄存器加上一条指令的储存大小,使其指向下一条要读取代码 指令的地址。当处理器处于 ARM 状态时,该寄存器中的值始终是字对齐的,而处理器处于 Thumb 状态时该寄存器中的值始终是半字对齐的。这个寄存器还可以用于数据的相对寻址, 后面我们可以看到它神奇的作用。 3.6.7 CPSR 寄存器 这个寄存器有点复杂,我们慢慢看。CPSR 寄存器,即当前程序状态寄存器。这个寄存 器在所有的工作模式下都共用相同的物理寄存器,并且在 Thumb 状态下也是使用这个寄存 器作为程序的状态寄存器。CPSR 也是 32 位的,但却没有完全用到 32 位,也就是说有许多 位是保留而留作后用的。它里面保存了如下信息,如图(P-3.6.7.1)所示。 图(P-3.6.7.1) 1. 模式位;它控制处理器的运行模式。如表(T-3.6.7.1)所示。 CPSR M[4:0]位 ARM920T 处理器模式 0b10000 用户模式 0b10001 快速中断模式 0b10010 外部中断模式 0b10011 系统管理模式 0b10111 数据访问终止模式 0b11011 未定义指令终止模式 0b11111 系统模式 表(T-3.6.7.1) 注意:0b 表示它后面的数字是二进制数。 2. CPSR T 位;控制处理器的运行状态。如表(T-3.6.7.2)所示。 CPSR T 位 ARM920T 处理器状态 0b0 ARM 状态 0b1 Thumb 状态 表(T-3.6.7.2) 注意:0b 表示它后面的数字是二进制数。 3. CPSR I、F 位;控制中断的使能与禁止。如表(T-3.6.7.3)所示。 CPSR [I、F] 禁止或者允许 ARM920T 外 禁 止 或 者 允 许 ARM920T 部中断 快速中断 0b00 允许 允许 0b01 允许 禁止 0b10 禁止 允许 0b11 禁止 禁止 表(T-3.6.7.3) 注意:0b 表示它后面的数字是二进制数。 4. CPSR [N、Z、C、V]位;保存了最近处理器的运算部件执行的信息,即运行了某些数 据运算指令后的状态。如表(T-3.6.7.4)所示。 CPSR [N、Z、C、V]位 说明 N位 有符号整数运算时,N=1 时表示结果为负数,N=0 时表示 结果为正数或者是 0。 Z位 Z=1 时表示运算结果为 0,Z=0 时表示运算结果不为 0。 C位 1. 在加法指令中,结果产生了进位即上溢出时,C=1,其 它情况下 C=0。 2. 在减法指令中,结果产生了借位即下溢出时,C=0,其 它情况下 C=1。 3. 在移位指令中,C 中包含了最后一次被移位溢出的位 的数值 V位 在加、减法指令中,如果操作数和运算结果为二进制的补 码表示的带符号数时,V=1 时表示符号位溢出。 表(T-3.6.7.4) 5. 保留位。在程序中最好不要使用。这些位可能被 ARM 公司在未来的 ARM 处理器另作 他用。 3.6.8 SPSR 寄存器 SPSR 寄存器,即程序状态备份寄存器。除系统模式和用户模式下没有这个寄存器外, 其它处理器工作模式都有各自的物理备份寄存器。并且在 Thumb 状态下也是使用这个寄存 器作为程序状态备份寄存器。当处理器响应中断、异常时,处理器会自动将 CPSR 寄存器的 内容放在相应中断、异常模式下的 SPSR 寄存器中,当中断、异常处理返回时就将该工作模 式下 SPSR 寄存器的内容写入到 CPSR 中。这其中有些步骤是 ARM920T 处理器硬件负责完成 的,后面会详细介绍的。 3.7 异常和中断 如果读者开发过应用程序,或许听说过异常和中断,不过那种异常或者中断,和这里的 异常和中断还是有区别的。本节首先了解一下什么是异常和中断,接着分别介绍 ARM920T 处 理器上的异常和中断以及 ARM920T 处理器上的处理方式,最后了解一下什么又是异常中断 向量。 3.7.1 什么是异常和中断 处理器执行一条指令,这条指令能不能运行成功,要看它的运行条件能不能满足。比如 一条访问储存器的指令,如果访问储存器的地址不存在或者不允许访问,那么这条指令就不 能执行成功,先不管失败后怎么处理,反正最好的方式是去执行另一段程序,但是当前这段 程序肯定要被打断的。另一种情况,处理器正在运行程序,这时某个设备急切的需要处理器 的关注,最好的方式也是去执行一段关注这个设备的代码,这也是要打断当前正在运行的程 序。这种打断当前程序执行流转而执行其它程序的方式,如图(P-3.7.1.1)所示,并且还 对其进行了分类,如下: 1. 异常:同步的,是由于程序指令本身的问题,或者不能满足指令的执行条件而程序 不能继续往下执行时,又或者执行到专门产生异常的指令时,所表现的一种状态。 2. 中断:异步的,是由外部设备发起的,处理器不知道什么时刻什么设备需要关注, 它只是每执行完一条指令就去检测它的中断信号引脚,看看有无中断信号。如果没 有就继续执行指令。 下面就去分别看看 ARM920T 是怎么处理这两种情况的。 图(P-3.7.1.1) 3.7.2 异常 异常,是因为不能满足程序指令的执行条件,或者违反了指令的执行规则,又或者是执 行了用于产生异常的指令而发生的。异常又是同步的,如果程序中有一条会产生异常的指令, 你不修正这个程序,那么不管重新运行这个程序多少遍,都会在相同的程序地址上发生异常。 先来看看 ARM920T 处理中规定了哪些异常。 1. ARM920T 复位异常; 2. 未定义指令异常; 3. 预取指令终止异常; 4. 数据访问终止异常; 5. SWI 指令异常 复位异常:其实这个也不能完全划归为异常,只是它的行为和异常一样,它是当处理器 的复位引脚收到一个信号,就会马上中断当前执行的程序。处理器硬件执行如下操作并且这 些操作不可中断: 1. 把处理器设为 ARM 状态和系统管理模式。 2. 系统管理模式下 R14、SPSR 寄存器中是不确定的数据,因为复位嘛。 3. 禁止快速中断和外部中断。 4. 强制 R15 寄存器中的值为复位向量,这个复位向量后面会有介绍。 未定义指令异常:当 ARM 处理器执行协处理器指令时,它必须等待任何一个外部协处理 器应答后,才能真正执行这条指令,此时若没有协处理器回应,则产生未定义指令异常。如 果尝试执行未定义的指令,也会产生未定义指令异常。处理器硬件执行如下操作并且这些操 作不可中断: 1. 处理器被切换到 ARM 状态和未定义指令终止模式。 2. 把未定义指令后的下一条指令的地址放进处理器未定义指令终止模式下的 R14 寄存 器中,把 CPSR 寄存器的内容放进未定义指令终止模式下的 SPSR 寄存器中。 3. 禁止外部中断。 4. 强制 R15 寄存器中的值为未定义指令终止异常向量。 预取指令终止异常:当指令预取访问存储器失败时,存储器系统向处理器发出存储器中 止信号,预取的指令被记为无效,但只有当处理器试图执行该无效指令时,指令预取终止异 常才会发生,如果该指令未被执行,例如在指令流水线中发生了跳转,则预取指令中止异常 不会发生。如果预取终止异常发生,处理器硬件执行如下操作并且这些操作不可中断: 1. 处理器被切换到 ARM 状态和数据访问终止模式。 2. 把预取终止指令后的下一条指令的地址放进处理器数据访问终止模式下的 R14 寄存 器中,把 CPSR 寄存器的内容放进数据访问终止模式下的 SPSR 寄存器中。 3. 禁止外部中断。 4. 强制 R15 寄存器中的值为预取指令终止异常向量。 数据访问终止异常:如果数据访问指令的目标地址不存在,或者该地址不允许当前指令 访问,处理器会产生数据访问终止异常。处理器硬件执行如下操作并且这些操作不可中断: 1. 处理器被切换到 ARM 状态和数据访问终止模式。 2. 把终止指令后的第二条指令的地址(这是因为指令流水线的原因),放进处理器数据 访问终止模式下的 R14 寄存器中,把 CPSR 寄存器的内容放进数据访问终止模式下 的 SPSR 寄存器中。 3. 禁止外部中断。 4. 强制 R15 寄存器中的值为数据访问终止异常向量。 SWI 指令异常:其实它也不能称为异常,只是它的行为和异常一样,使用指令 SWI,会使 处理器模式切换到系统管理模式运行特定的程序。这条指令通常用于作为系统调用的陷入指 令。处理器硬件执行如下操作并且这些操作不可中断: 1. 把处理器设为 ARM 状态和系统管理模式。 2. 把 SWI 指令后的下一条指令的地址放进系统管理模式下的 R14 寄存器中、把 CPSR 寄存器的内容放进系统管理模式下的 SPSR 寄存器中。 3. 禁止外部中断。 4. 强制 R15 寄存器中的值为 SWI 向量。 3.7.3 中断 处理器的速度比外部设备的速度快得多,然而外部设备又需要处理器的关注,于是这样 高速的处理器就不得不等待低速的外部设备,这对高速的处理器无疑是巨大的浪费。人们就 想了个方法,设备在需要处理器关注时就给处理器发送个信号,于是这样处理在没收到信号 之前就可以高速的做它自己的事情。处理器在每运行完一条指令后,首先看有没有这个信号, 如果有这个信号 还要看系统允不允许响应这个信号,如果系统允许响应这个信号,那么处 理器就开始处理这个信号。由于处理器不知道什么设备会在什么时刻发送这个信号,所以这 种信号是异步的。这种信号就是中断信号,由于各种外部设备有轻重缓急之分,有的设备需 要快速响应,有的则不那么急。因此 ARM920T 处理器定义两种中断类型: 1. 外部设备中断。 2. 快速外部设备中断。 外部设备中断:通过处理器上的 IRQ 输入引脚,由外部设备产生 IRQ 中断,若此时 CPSR 的 I 位为 0,处理器会产生外部设备中断请求 IRQ 中断。此时,处理器硬件执行如下操作并 且这些操作不可中断: 1. 把处理器设为 ARM 状态和外部中断模式。 2. 把中断后将要执行的第二条指令的地址(这也是因为指令流水线的原因),放进处理 器外部中断模式下的 R14 寄存器中,把 CPSR 寄存器的内容放进外部中断模式下的 SPSR 寄存器中。 3. 禁止外部设备中断。 4. 强制 R15 寄存器中的值为外部中断向量。 快速外部设备中断:通过处理器上的 FIQ 输入引脚,由外部设备产生 FIQ 中断,若此时 CPSR 的 F 位为 0,处理器会产生快速设备中断请求 FIQ 中断。此时,处理器硬件执行如下操 作并且这些操作不可中断: 1. 把处理器设为 ARM 状态和快速中断模式。 2. 把中断后将要执行的第二条指令的地址(这也是因为指令流水线的原因),放进处理 器快速中断模式下的 R14 寄存器中,把 CPSR 寄存器的内容放进快速中断模式下的 SPSR 寄存器中。 3. 禁止快速设备中断和外部设备中断。 4. 强制 R15 寄存器中的值为快速中断向量。 3.7.4 异常中断向量 所谓的异常中断向量,在 ARM920T 中实际上是一组储存地址,每个地址都是字对齐的, 一个地址对应一个字大小的空间,这些字对齐的地址是连续的。根据它们的开始地址分为两 种类型: 1. 低地址向量。 2. 高地址向量。 无论是低地址向量还是高地址向量,它们的地址不是由软件随意安排的,而是从某个特 定的地址开始的,地址如下: 1. 0x00000000 地址;低地址向量,一般情况下 ARM920T 上电时都是从 0 地址开始的低 地址向量。 2. 0xFFFF0000 地址;高地址向量,可由软件通过操作 CP15 协处理器中的一个寄存器 达到这一目的,我们后面会详细了解的。 ARM920T 有 7 个向量,外加一个保留向量,一共是 8 个向量。这些向量包含异常和中断, 如表(T-3.7.4.1)所示。 低/高 向量地址 向量名称 ARM920T 进入的状态和模式 0x00000000 / 0xFFFF0000 复位向量 ARM 状态,系统管理模式 0x00000004 / 0xFFFF0004 未定义指令终止向量 ARM 状态,未定义指令终止模式 0x00000008 / 0xFFFF0008 SWI 向量 ARM 状态,系统管理模式 0x0000000c / 0xFFFF000c 预取指令终止向量 ARM 状态,数据访问终止模式 0x00000010 / 0xFFFF0010 数据访问终止向量 ARM 状态,数据访问终止模式 0x00000014 / 0xFFFF0014 保留 保留 0x00000018 / 0xFFFF0018 外部中断向量 ARM 状态,外部中断模式 0x0000001c / 0xFFFF001C 快速中断向量 ARM 状态,快速中断模式 表(T-3.7.4.1) 由表(T-3.7.4.1)可知,这些向量只有一个字的储存空间,显然这是放不下整个异常 或者中断的处理代码的。于是这个地址上就放了一条跳转指令,就由这条跳转指令跳转到处 理程序的地址开始运行。正因为如此,快速中断向量放在向量表最后,允许快速中断处理程 序直接放在地址 0x0000001C 或 0xFFFF001C 开始的位置,而不需要由跳转指令跳转到快速 中断处理程序,这可以提高快速中断响应的速度。 有些情况下,会在同一时刻发生多个异常或者中断,而 ARM920T 处理器在同一时刻又只 能处理一个异常或者中断。怎么办呢,ARM920T 处理器采用了一个固定的优先级来决定它们 的处理顺序。如下,数字越小的优先级越高,优先级最高的最先被 ARM920T 处理。 1. 复位 2. 数据访问终止 3. 快速中断 4. 外部中断 5. 预取指令终止 6. 未定义指令终止、SWI 注意,并不是所有的异常和中断都可以在同一时刻发生!比如未定义指令终止和 SWI, 因为 SWI 指令不是一条未定义指令,它是软中断指令。所以在执行 SWI 指令发生软中断异常 的时刻,绝对不可能发生未定义指令终止异常。 正是因为有了异常和中断这种机制,使得 ARM920T 处理器不仅在高速执行程序时能捕 获程序中的问题并且进行相关问题的处理、还能高效的和外部设备进行通信。同时还能让应 用程序通过执行 SWI 指令调用操作系统服务。 3.8 ARM920T 指令集 ARM920T 处理器是精简指令集体系的,指令不多,每条指令都是必要的、指令都是通过 精心设计的,同时指令都是等长的,即指令占用的储存空间都是相同的。ARM920T 的所有 指令都可以分为如下几类。 1. 分支跳转指令。 2. 数据处理指令。 3. 装载和储存指令。 4. 程序状态寄存器操作指令。 5. 协处理器操作指令。 6. 异常中断产生指令。 本小节按这样顺序介绍 ARM920T 指令集:概要说明指令及编码格式,然后按上面的分类再 分别介绍指令,可能有些类型有很多条指令,有些类型只有一条指令,因为指令是程序最基 本的组成元素,所以我们必须认真的了解它。 3.8.1 指令及其编码格式 首先看看 ARM920T 处理都有哪些指令,如表(T-3.8.1.1)。 ARM 汇编程序指令助记符号 指令相关说明 B 分支跳转 bl 带链接的分支跳转 bx 带状态切换的分支跳转 blx 带链接和状态切换的分支跳转 mov 数据传送 mvn 数据求反传送 add 加法 adc 带进位的加法 sub 减法 sbc 带借位的减法 rsb 逆向减法 rsc 带借位的逆向减法 mul 乘法 mla 乘加 and 逻辑与 orr 逻辑或 bic 位清零 eor 逻辑异或 cmp 比较 cmn 基于相反数比较 teq 相等测试 tst 位测试 swp 寄存器与存储器内容交换 ldr 加载字数据从存储器到寄存器 str 存储字数据从寄存器到存储器 ldm 加载多个字数据从存储器到多寄存器 stm 存储多个字数据从多个寄存器到存储器 mrs 传送程序状态寄存器到通用寄存器 msr 传送通用寄存器到程序状态寄存器 mrc 传送协处理器寄存器到 ARM920T 寄存器 mcr 传送 ARM920T 寄存器到协处理器寄存器 ldc 加载存储器字数据到协处理器寄存器 stc 存储协处理器寄存器到存储器 cdp 协处理器数据操作指令 swi 软中断异常 表(T-3.8.1.1) 35 条指令,当然有很多指令在不同的情况下有很多变形,后面会详细介绍的。 虽然我们不是开发编译器的,但是我们还是粗略的了解一下这些指令是怎么编码的、是 怎么变成二进制数据储存在储存器中的。这对开发操作系统内核是有很多好处的。如图(P3.8.1.1)。 图(P-3.8.1.1) 可以看出 ARM920T 处理器 ARM 状态下的指令都是等长的 32 位编码。每条指令最高 4 位都 是一个条件码,还有许多我们不认识的位和符号,这个留到介绍指令时再介绍,这里我们研 究一下条件码是干什么的。 ARM920T 处理器大部分指令都可以根据上条指令执行结果的状态,决定是否执行当前 这条指令,如果条件满足就执行当前指令,否则就不执行当前指令,相当于执行了一条空指 令。设计人员为了达到这一目的,才给每条指令加上了一个条件码段,当然条件码是可选的, 可以不写条件码的助记符,那么指令就是无条件执行的。一共有 16 种可能的条件,每种条 件表示为在指令助记符后面附加两个字符后缀。例如,一条分支跳转指令“b”变成了“beq”, 那么即为“如果相等则分支跳转”,这意味着只有 CPSR 寄存器中的 Z 标志位被置位了才会执 行分支跳转。在实际应用当中,将会使用到 15 种不同的条件,保留第 16 种(1111),这些 条件码如表(T-3.8.1.2)。 条件码 助记符 CPSR 寄存器中的标志位 相关说明 0b0000 EQ Z 置位 相等 0b0001 NE Z 清零 不相等 0b0010 CS C 置位 无符号大于或等于 0b0011 CC C 清零 无符号小于 0b0100 MI N 置位 负数 0b0101 PL N 清零 正数或零 0b0110 VS V 置位 溢出 0b0111 VC V 清零 未溢出 0b1000 HI C 置位并且 Z 清零 无符号大于 0b1001 LS C 清零或 Z 置位 无符号小于或等于 0b1010 GE N 等于 V 大于或等于 0b1011 LT N 不等于 V 小于 0b1100 GT Z 清零和(N 等于 V)逻辑与 大于 0b1101 LE Z 置位和(N 不等于 V)逻辑或 小于或等于 0b1110 AL 忽略 始终 表(T-3.8.1.2) 注释:0b 表示后面的数字是二进制数 要注意的是,很多指令不会主动影响 CPSR 寄存器中的标志位。有时候可以在指令助记符后 面加入“S”符号,表示要求这条指令的执行结果将影响 CPSR 寄存器中的标志位。具体什么 样的指令可以加“S”符号,这得视具体情况而定。后介绍每条指令时,在那里会详细介绍。 本书不会过于详细的介绍 ARM920T 处理器所有的指令以及每条指令所有的详细使用方 式。因为,一、篇幅所限,二、本书不是介绍某一特定处理器的指令集的专著。但是也不用 害怕,笔者会尽量做到写操作系统内核时足够使用。 3.8.2 分支跳转指令 如果一个处理器不能跳转执行指令,那么程序的判断、循环结构以及函数调用将不能实 现。这样的处理器是不是糟透了,我想肯定是的。庆幸的是大部分处理器都支持跳转执行指 令,ARM920T 也不例外。 ARM920T 支持如下几条分支跳转指令,来实现程序的跳转执行。 1. b 2. bl 3. bx 4. blx 1. b、bl 指令 指令在汇编程序中的用法: b{l}{cond} | 1. {}中的表示可选项。 2. <>中的表示必须项。 3. |,表示多选一。 4. cond,表示条件码,比如 EQ、NE、CS。 5. l,链接位,如果在“b”后面写上“l”表示在执行跳转动作的同时,将原来的 PC 值 写入到当前模式下的 R14 寄存器中。 6. lable_offset,表示跳转标号即跳转地址。 7. Rn,表示一个合法的寄存器,一般通用寄存器都行。 分支跳转指令中包含了一个有符号补码的 24 位偏移量。该值左移两位后并将其有符号 位扩展到 32 位,因为有效位数为 25 位加 1 位符号位,所以最多能表示 32M,然后再加入 到 R15 中。因此该指令可以指定±32M 字节的分支跳转。分支跳转偏移还必须考虑预取操 作,因为 PC 超前于当前指令的 2 个字。分支跳转地址如果超过了±32M 字节,必须将这个 地址,事先装载到某一通用寄存器中。例如:将这个地址先装入到 R0 中,再执行“bl r0”。 例子: main: ;标号,汇编器和程序链接器最后会把它转换成地址。 mov r0,#2 ;r0=2。 mov r1,#3 ;r1=3。 bl addfunc1 ;r14=pc;pc=pc+offset(addfunc1)。保存 pc 至 r14 并跳转到 addfunc1 ;地址上运行。注意 offset(addfunc1)是编译工具处理的,表示当前 ;pc 和 addfunc1 的绝对地址之间的差值 lable: ;标号。 b lable ;PC= offset(lable);死循环。注意 offset(lable)是编译工具处理的, ;表示当前 pc 和 lable 的绝对地址之间的差值 addfunc1: ;标号。 add r0,r0,r1 ;r0=r0+r1。 b r14 ;PC=r14 实现子程序返回。 这段程序非常简单,看代码注释就能明白它是干什么的,但我们主要是为了解 b、bl 指 令的使用方式,当然没有写条件码,所以缺省情况下指令总是会执行。 2. bx 指令 bx 指令是将一个寄存器中的内容,通常这个内容是一个程序的地址,放进 PC 寄存器中, 导致程序的跳转执行并且它还改变处理器的状态:ARM 状态到 Thumb 状态或者 Thumb 状态 到 ARM 状态。bx 指令用 Rn 寄存器中的数据和 0xFFFFFFFE 做与操作,把结果写到 PC 寄存器 中,然后根据 Rn 寄存器的第 0 位的值决定处理器的运行状态,即用这个值设置 CPSR 寄存 器的 T 位,也就是处理器的状态位。 汇编程序中的用法: bx{cond} 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rn,表示一个合法的寄存器。 例子: .code32 main: adr r0,thumb_ins+1 bx r0 .code16 ;32 位的 ARM 状态指令。 ;r0=thumb_ins+1 ;pc=r0,并进行处理器状态切换。 ;16 位的 thumb 状态指令。 thumb_ins: adr r0,main bx r0 ;r0=main, main 的地址 ;pc=r0,并进行处理器状态切换。 3. blx 指令 blx 指令有两种情况,一种是无条件执行,只能跳转到 thumb 指令的地址并切换处理器 状态为 thumb 状态,一种是有条件执行的,可以在 ARM 状态和 thumb 状态之间互相切换和 跳转。这两种情况都会在跳转的同时先保存 pc 至 r14 中。 在汇编中的用法: 1. blx 1. <>中的表示必须项。 2. lable_offset,表示跳转标号即跳转地址。使用规则和 bl 指令一样 2. blx{cond} 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rn,表示一个合法的寄存器。 例子: .code32 ;32 位的 ARM 状态指令。 main: adr r0,thumb_ins+1 ;r0=thumb_ins+1 blx r0 ;r14=pc,pc=r0 并且进行处理器状态切换。 .code16 ;16 位的 thumb 状态指令。 thumb_ins: adr r1,main ;r1=main,r1=main 的地址 blx r1 ;r14=pc,pc=r1,并进行处理器状态切换。 3.8.3 数据处理指令 处理器的大部分功能是为了完成数据的处理,所以必然要提供很多数据处理指令,首先 来看看 ARM920T 有哪些数据处理指令,如表(T-3.8.3.1)。 数 据 传 加 法 运 减 法 运 乘 法 运 逻 辑 运 比较 测试 交换 送 算 算 算 算 mov add sub mul and cmp teq swp mvn adc sbc mla orr cmn tst rsb bic rsc eor 表(T-3.8.3.1) 1. mov 指令 mov 指令是将一个常数或者寄存器中的数据传送到目标寄存器中。 在汇编中的用法: mov{cond}{s} , 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rd,表示目标寄存器。 5. operand,表示常数或者是可用的寄存器。 6. s,表示指令执行是否影响 CPSR 中标志位,还有另外一种情况,如果 Rd 是 pc 的 话,s,表示将数据写进 pc 的同时还将当前模式下的 SPSR 寄存器复制到 CPSR 中,如 果当前代码运行在用户模式或者系统模式,这两种模式下没有 SPSR。这种方式会导 致结果不可预知。 例子: main: mov r0,#0 movs r1,r0 moveq r2,#0x40 movs pc,r2 ;r0=0,把 0 放进 r0 中 ;r1=r0,有 s 所以根据结果设置 CPSR 标志位 ;上步中 r1=0,条件满足所以会执行这条指令,r2=0x40 ;目标寄存器为 pc,又有 s 所以 CPSR=SPSR,pc=r2,当然假定这 ;程序执行除系统和用户模式之外的模式下。最后处理器会 跳到 ;0x40 地址开始执行。 2. mvn 指令 mvn 指令是将一个常数或者寄存器中的数据按位取反后传送到目标寄存器中。 在汇编中的用法: mvn{cond}{s} , 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rd,表示目标寄存器。 5. operand,表示常数或者是可用的寄存器。 6. s,表示指令执行是否影响 CPSR 中的标志位,还有另外一种情况,参见 mov 指令。 例子: main: mov r0,#0 ;r0=0,把 0 放进 r0 中 mvn r1,r0 ;r1=r0,执行后,r1 中是 0xffffffff,r0 中是 0,因为没有 s 所以 ;不影响 CPSR 中的标志位 3. add 指令 add 指令完成第一个源寄存器和第二个常数或者第二个源寄存器相加,并把结果放进目 标寄存器中。也可以影响 CPSR 中的标志位。 在汇编中的用法: add{cond}{s} ,, 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rd,表示目标寄存器,和 mov 指令用法一样,也有 pc 的特殊情况。 5. Rn,表示第一个源寄存器,只要是通用寄存器即可。 6. operand,表示常数或者是可用的寄存器。 7. s,和 mov 指令用法一样,也有 pc 的特殊情况。 例子: main: mov r0,#5 mov r1,#5 add r0,r0,#5 add r0,r0,r1 ;r0=5。 ;r1=5。 ;r0=r0+5,执行后 r0 中为 10。 ;r0=r0+r1,执行后 r0 中为 15。 4. adc 指令 adc 指令完成第一个源寄存器和第二个常数或者第二个源寄存器相加,再加上 CPSR 中 的 C 标志位,并把结果放进目标寄存器中。根据结果也可以影响 CPSR 中的标志位。 在汇编中的用法: adc{cond}{s} ,, 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rd,表示目标寄存器,和 add 指令相同。 5. Rn,表示第一个源寄存器,只要是通用寄存器即可。 6. operand,表示常数或者是可用的寄存器。 7. s,和 add 指令相同。 例子: main: adds r7,r3,r4 ;r7=r3+r4,有 s 所以指令的执行会设置 CPSR 中的标志位。 adc r8,r5,r6 ;r8=r5+r6+CPSR 中 C 位。 ;如果 r3、r4 中分别放了两个 64 位数据的低 32 位,而 r5、r6 分别放了这两个 64 位数据 ;的高 32 位,那么这两条指令完成了两个 64 位数据的加法,结果的低 32 位放在 r7 中,结 ;果的高 32 位放在 r8 中。 5. sub 指令 sub 指令完成第一个源寄存器和第二个常数或者第二个源寄存器相减,并把结果放进目 标寄存器中。也可以影响 CPSR 中的标志位。需要注意的是减法若发生借位,CPSR 中的 C 位 会设置成 0。 在汇编中的用法: sub{cond}{s} ,, 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rd,表示目标寄存器,和 add 指令情况相同。 5. Rn,表示第一个源寄存器,只要是通用寄存器即可。 6. operand,表示常数或者是可用的寄存器。 7. s,和 add 指令情况相同。 例子: main: mov r0,#10 ;r0=10。 mov r1,#5 ;r1=5。 sub r0,r0,#5 ;r0=r0-5,执行后 r0 中为 5 subs r0,r0,r1 ;r0=r0-5,执行后 r0 中为 0,有 s 所以会影响 CPSR 标志位。 subne r5,r7,#0 ;r5=r7-0, 然而因为上条指令的执行结果导致这条指令的运行 ;条件不满足,因此这条指令不会执行。 6. sbc 指令 sbc 指令完成第一个源寄存器和第二个常数或者第二个源寄存器相减,再减去 CPSR 中 的 C 标志位的反码,并把结果放进目标寄存器中。根据结果也可以影响 CPSR 中标志位。 在汇编中的用法: sbc{cond}{s} ,, 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rd,表示目标寄存器,和 add 指令情况相同。 5. Rn,表示第一个源寄存器,只要是通用寄存器即可。 6. operand,表示常数或者是可用的寄存器。 7. s,和 add 指令情况相同。 例子: main: subs r7,r3,r4 sbc r8,r5,r6 ;r7=r3-r4,有 s 所以指令的执行会设置 CPSR 中的标志位。 ;r8=r5-r6-CPSR 中 C 位反码。 ;同 adc 的思想一样,这两条指令完成了两个 64 位数的减法。 7. rsb 指令 rsb 指令完成第二个常数或者第二个源寄存器和第一个源寄存器相减,并把结果放进目 标寄存器中。正因为这样它也称为逆向减法指令。也可以影响 CPSR 中的标志位。需要注意 的是减法若发生借位,CPSR 中的 C 位会设置成 0。 在汇编中的用法: rsb{cond}{s} ,, 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rd,表示目标寄存器,和 add 指令情况相同。 5. Rn,表示第二个源寄存器,只要是通用寄存器即可。 6. operand,表示第一个常数或者是可用的寄存器。 7. s,和 add 指令情况相同。 例子: main: mov r0,#10 ;r0=10。 mov r1,#5 ;r1=5。 rsb r0,r0,#15 ;r0=15-r0,执行后 r0 中为 5 rsbs r0,r0,r1 ;r0=r1-r0,执行后 r0 中为 0,有 s 所以会影响 CPSR 标志位。 rsbne r5,r7,#0 ;r5=0-r7, 然而因为上条指令的执行结果导致这条指令的运行 ;条件不满足,因此这条指令不会执行。 8. rsc 指令 rsc 指令完成第二个常数或者第二个源寄存器和第一个源寄存器相减,再减去 CPSR 中的 C 标志位的反码,并把结果放进目标寄存器中。正因为这样它也称为带借位的逆向减法指令。 也可以影响 CPSR 中的标志位。 在汇编中的用法: rsc{cond}{s} ,, 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rd,表示目标寄存器,和 add 指令情况相同。 5. Rn,表示第二个源寄存器,只要是通用寄存器即可。 6. operand,表示第一个常数或者是可用的寄存器。 7. s,和 add 指令情况相同。 例子: main: sbs r4,r0,#0 ;r4=0-r0,有 s 所以会影响 CPSR 标志位。 rsc r5,r7,#0 ;r5=0-r7, 这两条指令完成了求一个 64 位数据的负数的操作,两个 ;寄存器存放 64 位数据,两个寄存器存放操作后的 64 位结果。 9. mul 指令 mul 指令实现两个 32 位数的乘法运算,把结果放在目标寄存器中。这两个数可以是无 符号整数也可以是有符号整数,并且这两个数要放在两个寄存器中,当然根据结果可能影响 CPSR 中的标志位。由于两个 32 位数的乘积是 64 位数,但是 mul 指令仅仅保存了这个乘积 的低 32 位。 在汇编中的用法: mul{cond}{s} ,, 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. s,表示是否根据结果影响 CPSR 中的标志位 5. Rd,表示目标寄存器,用于存放结果即乘积。 6. Rn1,表示存放第一个 32 位数据的寄存器。 7. Rn2,表示存放第二个 32 位数据的寄存器。 例子: main: mov r0,#10 mov r7,#20 ;r0=10。 ;r7=20。 mul r2,r0,r7 ;r2=r0*r7,执行 r2 中等于 200,没有 s 所以不会影响 CPSR 标志位。 10. mla 指令 mla 指令实现两个 32 位数的乘法运算,再加上第三个 32 位数,把结果放在目标寄存器 中。这第一、二个数可以是无符号整数也可以是有符号整数,这三个数据必须放在三个寄存 器中,当然根据结果可能影响 CPSR 中的标志位。同样 mla 指令也是只保存前两个数据乘积 的低 32 位。 在汇编中的用法: mla{cond}{s} ,,, 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. s,表示是否根据结果影响 CPSR 中的标志位 5. Rd,表示目标寄存器,用于存放结果即前两个数的乘积加上第三个数的结果。 6. Rn1,表示存放第一个 32 位数据的寄存器。 7. Rn2,表示存放第二个 32 位数据的寄存器。 8. Rn3,表示存放第三个 32 位数据的寄存器。 例子: main: mov r0,#10 ;r0=10。 mov r7,#20 ;r7=20。 mov r3,#50 ;r3=50。 mlas r2,r0,r7,r3 ;r2=r0*r7+r3,执行后 r2 中等于 250,有 s 所以会影响 CPSR 标志位。 11. and 指令 and 指令完成第一个源寄存器与第二个常数或者第二个源寄存器,按位做逻辑与操作, 把结果放进目标寄存器中,同时根据结果可能影响 CPSR 中的标志位。逻辑与操作是参加与 运算的两个数据都为“1”时,结果才为“1”。例如 0xf 与上 0xf 的结果还是为 0xf;0xf 与上 0 的结果为 0;0 与上 0xf 的结果也为 0;0x1 与上 0xf 的结果为 0x1。 在汇编中的用法: and{cond}{s} ,, 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rd,表示目标寄存器,和 mov 指令用法一样,也有 pc 的特殊情况。 5. Rn,表示第一个源寄存器,只要是通用寄存器即可。 6. operand,表示常数或者是可用的寄存器。 7. s,和 mov 指令用法一样,也有 pc 的特殊情况。 例子: main: mov r0,#0 ;r0=0。 mov r1,#0xf ;r1=0xf。 ands r2,r0,#0xf ;r2=r0 AND 0xf,执行后 r2 中为 0,有 s 所以影响 CPSR 标志位 andeq r3,r0,r1 ;由于上条指令的执行,导致这条指令运行条件满足,r3=r0 AND ;r1,执行后 r3 中为 0。 mov r0,#1 ;r0=1。 and r4,r1,r0 ;r4=r1 AND r0,执行后 r4 中为 1。. 12. orr 指令 orr 指令完成第一个源寄存器与第二个常数或者第二个源寄存器,按位做逻辑或操作, 把结果放进目标寄存器中,同时根据结果可能影响 CPSR 中的标志位。逻辑或操作是参加或 运算的两个数据有一个为“1”时,结果就为“1”。例如 0xf 或上 0xf 的结果还是为 0xf;0xf 或上 0 的结果为 0xf;0 或上 0xf 的结果为 0xf;0x1 或上 0xf 的结果为 0xf。 在汇编中的用法: orr{cond}{s} ,, 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rd,表示目标寄存器,和 and 指令用法一样。 5. Rn,表示第一个源寄存器,只要是通用寄存器即可。 6. operand,表示常数或者是可用的寄存器。 7. s,和 and 指令用法一样。 例子: main: mov r0,#0 mov r1,#0x1 ;r0=0。 ;r1=0x1。 orrs r2,r0,#0xf orrne r3,r0,r2 ;r2=r0 ORR 0xf,执行后 r2 中为 0xf,有 s 所以影响 CPSR 标志位 ;由于上条指令的执行,导致这条指令运行条件满足,r3=r0 ORR ;r2,执行后 r3 中为 0xf。 mov r0,#1 orr r4,r1,r0 ;r0=1。 ;r4=r1 ORR r0,执行后 r4 中为 1。. 13. bic 指令 bic 指令完成第一个源寄存器与第二个常数或者第二个源寄存器的反码操作,把结果放 进目标寄存器中,同时根据结果可能影响 CPSR 中的标志位。这条指令经常用于数据位清零 操作。 在汇编中的用法: bic{cond}{s} ,, 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rd,表示目标寄存器,和 and 指令用法情况一样。 5. Rn,表示第一个源寄存器,只要是通用寄存器即可。 6. operand,表示常数或者是可用的寄存器。 7. s,和 and 指令用法情况一样。 例子: main: mov r0,#0xf mov r1,#0xf mov r4,#0 bics r2,r0,#0xf biceq r3,r0,r4 ;r0=0xf。 ;r1=0xf。 ;r4=0 ;r2=r0 BIC 0xf,执行后 r2 中为 0,有 s 所以影响 CPSR 标志位 ;由于上条指令的执行,导致这条指令运行条件满足,r3=r0 BIC ;r4,执行后 r3 中为 0xf。 14. eor 指令 eor 指令完成第一个源寄存器与第二个常数或者第二个源寄存器,按位做逻辑异或操作, 把结果放进目标寄存器中,同时根据结果可能影响 CPSR 中的标志位。逻辑异或操作是参加 异或运算的两个数据位相同时,结果就为“0”,不同时为“1”。例如 0xf 异或上 0xf 的结果 为 0;0xf 异或上 0 的结果为 0xf;0 异或上 0xf 的结果为 0xf;0x0 或上 0x0 的结果为 0x0。 在汇编中的用法: eor{cond}{s} ,, 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rd,表示目标寄存器,和 and 指令用法一样。 5. Rn,表示第一个源寄存器,只要是通用寄存器即可。 6. operand,表示常数或者是可用的寄存器。 7. s,和 and 指令用法一样。 例子: main: mov r0,#0 ;r0=0。 mov r1,#0x1 ;r1=0x1。 eors r2,r0,#0xf ;r2=r0 EOR 0xf,执行后 r2 中为 0xf,有 s 所以影响 CPSR 标志位 orrne r3,r1,r2 ;由于上条指令的执行,导致这条指令运行条件满足,r3=r0 ORR ;r2,执行后 r3 中为 0xe。 15. cmp 指令 cmp 指令用第一个源寄存器减去第二个常数或者第二个源寄存器,总是根据结果影响 CPSR 中的标志位,但是不回写结果。 在汇编中的用法: cmp{cond} , 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rn,表示第一个源寄存器,只要是通用寄存器即可。 5. operand,表示常数或者是可用的寄存器。 例子: main: mov r0,#0 ;r0=0。 mov r1,#0x1 mov r2,#0x1 cmp r0,r1 ;r1=0x1。 ;r2=0x1 ;r0-r1,执行后 r0、r1 中数据不变,会影响 CPSR 标志位,会根 ;据结果是否为 0、是否为正、负数,决定设置 CPSR 哪些标志 ;位 cmpne r1,#1 ;由于上条指令的执行,导致这条指令的运行条件满足 ;会根据结果是否为 0、是否为正、负数,决定设置 CPSR 哪些 ;标志位 16. cmn 指令 cmn 指令用第一个源寄存器加上第二个常数或者第二个源寄存器,总是根据结果影响 CPSR 中的标志位,但是不回写结果。 在汇编中的用法: cmn{cond} , 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rn,表示第一个源寄存器,只要是通用寄存器即可。 5. operand,表示常数或者是可用的寄存器。 例子: main: cmp r0,#0 cmn r0,#0 ;r0-0,CPSR 中 C 标志位等于 0。 ;r0+0, CPSR 中 C 标志位等于 1 17. tst 指令 tst 指令完成第一个源寄存器与第二个常数或者第二个源寄存器按位逻辑与操作。总是 根据结果影响 CPSR 中的标志位,C 位、V 位通常是不受影响的。同样也不回写结果。 在汇编中的用法: tst{cond} , 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rn,表示第一个源寄存器,只要是通用寄存器即可。 5. operand,表示常数或者是可用的寄存器。 例子: main: mov r0,#0 mov r1,#0x1 tst r0,#0 mov r2,#1 tst r1,r2 ;r0=0。 ;r1=0x1。 ;r0AND0,执行后 CPSR 中的 N 标志位=0、Z 标志位=1 ;r2=1。 ;r1ANDr0,执行后 CPSR 中的 N 标志位=0、Z 标志位=0 18. teq 指令 teq 指令完成第一个源寄存器与第二个常数或者第二个源寄存器按位逻辑异或操作。总 是根据结果影响 CPSR 中的标志位,C 位、V 位通常是不受影响的。同样也不回写结果。 在汇编中的用法: teq{cond} , 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rn,表示第一个源寄存器,只要是通用寄存器即可。 5. operand,表示常数或者是可用的寄存器。 例子: main: mov r0,#0 mov r1,#0x1 teq r0,#0 ;r0=0。 ;r1=0x1。 ;r0EOR0 执行后 CPSR 中 N 标志位=0、Z 标志位=1 mov r2,#1 teq r1,r2 ;r2=1。 ;r1EORr2 执行后 CPSR 中 N 标志位=0、Z 标志位=1 19. swp 指令 swp 指令完成从一个内存字单元中读取一个数据到目标寄存器,这个内存字的地址放在 第二个源寄存器中,以此同时把第一个源寄存器中的内容写入到这个内存字单元中。因此这 条指令完成内存字和寄存器之间数据的原子交换。还有一条其功能、用法都和 swp 指令差 不多的指令即 swpb 指令,它唯一和 swp 指令不同的是:它只是交换一个内存字节单元的数 据。执行后,swpb 自动把目标寄存器的高 24 位清零,只保留低 8 位,即一个字节。 在汇编中的用法: swp{cond} ,,[] 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。 4. Rd,表示目标寄存器。 5. Rn1,表示第一个源寄存器。 6. Rn2,第二个源寄存器,存放内存字单元的地址。 7. [],表示用它里面的寄存器的内容作为访问内存的地址,去访问内存。 例子: main: mov r0,#0 mov r1,#0x40 swp r0,r0,[r1] ;r0=0。 ;r1=0x40。 ;执行后,r0 等于内存地址 0x40 开始的一个 32 位数据,内存 ;地址 0x40 开始的一个字单元中的数据等于 0,并且这两个动 ;作是原子的,即在不可分割的状态下执行的。 3.8.4 装载和储存指令 ARM920T 处理器,是 RISC 体系的,它有大量的寄存器,并且大部分数据的操作都是在 寄存器中完成的,这点我们从前面的诸多指令可以得到证明。那么有个问题,那么多程序和 数据,不太可能通过某种机制一次性全部放进寄存器中,何况对于我们庞大的程序和数据, 寄存器实在少得可怜。所以大部分程序和数据还是要放在内存里的。那么就需要有一种指令 用于装载内存数据到寄存器中和把寄存器中的数据储存到内存中。于是 ARM 设计人员设计 两种类型的装载、储存指令。一种用于装载、储存单个数据,一种用于装载、储存数据块。 指令如下。 1. ldr,装载单个数据指令。 2. str,储存单个数据指令。 3. ldm,装载数据块指令。 4. stm,储存数据块指令。 我们先把 ldr、str 指令放在一起介绍,然后再把 ldm、stm 指令放在一起介绍。这里要仔细 一点,这几条指令是最常用的,也是对它们的理解上最容易出问题的。 1. ldr、str 指令 ldr 指令完成从指定地址的内存单元中装载数据到目标寄存器中,而 str 指令将一个寄存 器中的数据储存到指定地址的内存单元中。然而指定地址的形式很繁琐,装载或者储存数据 的大小也有不同。所以下面要仔细看看。 在汇编程序中这两条指令的用法: {cond}{h|b} Rd, 1. {},表示可选项。 2. <>,表示必须项。 3. |,表示多选一。 4. ins,表示指令助记符 1) ldr,表示从表示的地址的内存单元中装载数据到 Rd 表示的目标寄 存器中。 2) str,表示把 Rd 表示的目标寄存器中的数据储存到表示的地址的内 存单元中。 5. cond,表示条件码。条件满足的情况下才会执行这条指令。 6. h|b,表示装载、储存数据的宽度。 1) h,表示装载、储存半字数据即 16 位,并且 ldr 自动清除 Rd 寄存器的高 16 位。 2) b,表示装载、储存字节数据即 8 位,并且 ldr 自动清除 Rd 寄存器的高 24 位。 7. Rd,表示目标寄存器。 8. addr_mode,表示内存地址。它有如下几种形势。 1) [Rn] 表示用 Rn 寄存器中的值,作为装载、储存内存单元的地址。 2) [Rn,+/-Rm] 表示用 Rn 寄存器的值加上或者减去 Rm 寄存器的值的结果,作为装载、储存内 存单元的地址。 3) [Rn, +/-Rm]! 表示用 Rn 寄存器的值加上或者减去 Rm 寄存器的值的结果,作为装载、储存内 存单元的地址。然后把 Rn 寄存器的值加上或者减去 Rm 寄存器的值的结果回写 到 Rn 寄存器中。 4) [Rn],+/-Rm 表示用 Rn 寄存器的值,作为装载、储存内存单元的地址。然后把 Rn 寄存器的 值加上或者减去 Rm 寄存器的值的结果回写到 Rn 寄存器中。 5) [Rn,+/-#offset_8] 表示用 Rn 寄存器的值加上或者减去 8 位偏移量的结果,作为装载、储存内存单 元的地址。 6) [Rn, +/-#offset_8]! 表示用 Rn 寄存器的值加上或者减去 8 位偏移量的结果,作为装载、储存内存单 元的地址。然后把用 Rn 寄存器的值加上或者减去 8 位偏移量的值的结果回写 到 Rn 寄存器中。 7) [Rn], +/-#offset_8 表示用 Rn 寄存器的值作为装载、储存内存单元的地址。然后把用 Rn 寄存器的 值加上或者减去 8 位偏移量的值的结果回写到 Rn 寄存器中。 例子: main: mov r0,#0x40 ;r0=0x40 ldr r1,[r0] ;把地址 0x40 的内存字单元装载到 r1 寄存器中。 ldrh r2,[r0,#4] ;把地址 0x44(r0+4)的内存半字单元装载到 r2 寄存器中并且其 ;高 16 位清零 ldrb r3,[r0,-#8] ;把地址 0x38(r0-8)的内核字节单元装载到 r3 寄存器中并且其 ;高 24 位清零 str r1,[r0] ;把 r1 寄存器的数据储存到地址 0x40 的内存字单元中。 strh r2,[r0,#4] ;把 r2 寄存器的低 16 位数据储存到地址 0x44(r0+4)的内存半 ;字单元中。 strb r3,[r0,-#8] ;把 r3 寄存器的低 8 位数据储存到地址 0x38(r0-8)的内存字 ;节单元中。 mov r0,#0x20 ;r0=0x20 mov r1,#0x10 ;r1=0x10 ldr r2,[r0,r1] ;把地址 0x30(r0+r1)的内存字单元装载到 r2 寄存器中。 ldrh r3.[r0],r1 ;把地址 0x20 的内存字半单元装载到 r3 寄存器中。然后 ;r0=r0+r1 所以 r0=0x30。 ldrb r4,[r0,r1]! ;把地址 0x40(r0+r1)的内存字节单元装载到 r4 寄存器中。然后 ;r0=r0+r1,所以 r0=0x40。 strb r4,[r0,]-r1 ;把 r4 寄存器低 8 位数据储存到地址 0x40 的内存字节单元中。 ;然后 r0=r0-r1,所以 r0=0x30。 strh r3,[r0,#8]! ;把 r3 寄存器低 16 位的数据储存到地址 0x38(r0+8)的内存半字 ;单元中。然后 r0=r0+8,所以 r0=0x38 str r2,[r0],#0 ;把 r2 寄存器的数据储存到地址 0x38 的内存字单元中。然后 ;r0=r0+0,所以 r0=0x38。 2. ldm、stm 指令 ldm 指令从一个内存地址开始的连续内存单元中,装载多个字到多个寄存器中。stm 指 令把多个寄存器存放到从一个内存地址开始的连续内存空间中。 在汇编程序中这两条指令的用法: {cond} {!},<{><}>{^} 1. {},表示可选项。 2. <>,表示必须项。 3. ins,表示指令 1) ldm,表示从 Rn 里的地址开始装载多个内存字到一组寄存器中,低地址的内存 字装载进低寄存器中(即编号小的寄存器中),高地址的内存字装载进高寄存 器中(即编号大的寄存器中)。 2) stm,表示从 Rn 里的地址开始储存一组寄存器到多个内存字单元中,小编号寄 存器储存在低地址的内存字单元中,大编号寄存器储存在高地址的内存字单元 中。 4. cond,表示条件码,条件满足时才执行指令。 5. addr_mode,表示内存地址形势,如下几种。 1) ia,表示事后递增方式。每次传送数据后,地址加上一个内存字单元大小。 2) ib,表示事先递增方式。每次传送数据前,地址加上一个内存字单元大小。 3) da,表示事后递减方式。每次传送数据后,地址减去一个内存字单元大小。 4) db,表示事先递减方式。每次传送数据前,地址减去一个内存字单元大小。 5) fd,表示满递减的栈操作。 6) ed,表示空递减的栈操作。 7) fa,表示满递增的栈操作。 8) ea,表示空递增的栈操作。 6. Rn,表示用于存放内存空间的开始地址的寄存器。 7. !,表示这条指令执行后,是否将已经变化的地址回写到 Rn 寄存器中。回写的地址 是这样计算的,用 Rn 里的值加上或者减去 regs_list 里的寄存器个数乘以 4。加上 或者减去取决于 addr_mode 的设定。 8. regs_list,表示寄存器列表,可以是 R0 到 R15 这 16 个寄存器,可以写成:“r0- r15”,“r0-r1,r3-r4,r7-r15”,“r0,r2,r3,r10,r14”……当然不是一定要包含所 有寄存器,可以是一个寄存器那么它就只读取或者储存一个内存字单元,如果是两 个寄存器,它就读取或者储存两个内存字单元…… 9. ^,根据 regs_list 里的不同寄存器,设置这个“^”符号有如下两种情况。 1) ldm 指令的 regs_list 里面有 PC 即 r15 寄存器时,表示装载 regs_list 里面 的寄存器时,同时将当前模式下的 SPSR 装载到 CPSR 中,当然这个当前模式不 能是系统模式和用户模式。 2) ldm、stm 指令的 regs_list 里面没有 r15 寄存器时,表示当处理器工作在非用 户模式下的时候,装载和储存的 regs_list 里的寄存器是用户模式下相应的寄 存器。 例子: 这个例子有点大,有点乱,我们要慢慢的看: main: mov r0,#1 ;r0=1 mov r1,#2 ;r1=2 mov r2,#3 ;r2=3 mov r3,#4 ;r3=4 mov r4,#5 ;r4=5 mov r5,#6 ;r5=6 mov r6,#7 ;r6=7 mov,r7,#8 ;r7=8 mov,r8,#9 ;r8=9 mov r9,#10 ;r9=10 mov r10,#11 ;r10=11 mov r11,#12 ;r11=12 mov r12,#13 ;r12=13 mov r13,#0x40 ;r13=0x40 mov r14,#0x80 ;r14=0x80 stmia r13,{r0-12,r14} ;把 r0 到 r12 和 r14 这 14 个寄存器储存在以 r13 寄 stmib r13,{r0-12,r14} stmda r13,{r0-12,r14} stmdb r13,{r0-12,r14} ldmia r13,{r0-12,r14} ldmib r13,{r0-12,r14} ldmda r13,{r0-12,r14} ;存器值为开始地址的内存空间中,执行过程如图 ;(P-3.8.4.1(a))所示。 ;把 r0 到 r12 和 r14 这 14 个寄存器储存在以 r13 寄 ;存器值为开始地址的内存空间中,执行过程如图 ;(P-3.8.4.1(b))所示。 ;把 r0 到 r12 和 r14 这 14 个寄存器储存在以 r13 寄 ;存器值为开始地址的内存空间中,执行过程如图 ;(P-3.8.4.2(a))所示。 ;把 r0 到 r12 和 r14 这 14 个寄存器储存在以 r13 寄 ;存器值为开始地址的内存空间中,执行过程如图 ;(P-3.8.4.2(b))所示。 ;从以 r13 寄存器值为开始地址的内存空间,装载 14 ;个内存字到 r0 至 r12 和 r14 这 14 个寄存器中,执 ;行过程如图(P-3.8.4.3(a))所示。 ;从以 r13 寄存器值为开始地址的内存空间,装载 14 ;个内存字到 r0 至 r12 和 r14 这 14 个寄存器中,执 ;行过程如图(P-3.8.4.3(b))所示。 ;从以 r13 寄存器值为开始地址的内存空间,装载 14 ;个内存字到 r0 至 r12 和 r14 这 14 个寄存器中,执 ;行过程如图(P-3.8.4.4(a))所示。 ldmdb r13,{r0-12,r14} ;从以 r13 寄存器值为开始地址的内存空间,装载 14 ;个内存字到 r0 至 r12 和 r14 这 14 个寄存器中,执 ;行过程如图(P-3.8.4.4(b))所示。 stmfd r13!,{r0-r12,r14} ;把 r0 到 r12 和 r14 这 14 个寄存器储存在以 r13 寄 ;存器值为开始地址的内存空间中,注意这条指令 r13 ;里的地址值是向下递减的,每次储存一个寄存器之前 ;r13 就先减 4,直到储存完最后一个寄存器。最后把 ;这个减到最后的地址值回写到 r13 寄存器中改变 r13 ;的值,执行过程如图(P-3.8.4.5(a))所示。 ldmfd r13!,{r0-r12,r14} ;从以 r13 寄存器值为开始地址的内存空间,装载 14 ;个内存字到 r0 至 r12 和 r14 这 14 个寄存器中,每 ;装载一个内存字之后,r13 寄存器的值加 4,直到 ;装载完最后一个寄存器,最后把这个减到最后的地址 ;值回写到 r13 寄存器中改变 r13 的值,执行过程如图 ;(P-3.8.4.5(b))所示。 stmfd r13!,{r0-r12,r14}^ ;把 r0 到 r12 和 r14 这 14 个用户模式的寄存器储存 ;在以 r13 寄存器值为开始地址的内存空间中,注意 ;这条指令 r13 里的地址值是向下递减的,每次储存一 ;个寄存器之前 r13 就先减 4,直到储存完最后一个 ;寄存器。最后把这个减到最后的地址值回写到 r13 ;寄存器中改变 r13 的值,执行过程如图 ;(P-3.8.4.6(a))所示。 ldmfd r13!,{r0-r12,pc}^ ;从以 r13 寄存器值为开始地址的内存空间,装载 14 ;个内存字到 r0 至 r12 和 pc 这 14 个寄存器中,每 ;装载一个内存字之后,r13 寄存器的值加 4,直到 ;装载完最后一个寄存器,最后把这个减到最后的地址 ;值回写到 r13 寄存器中改变 r13 的值,注意这有 pc ;寄存器且有“^”符号,所以该指令最后还会把某个 ;特定模式下的 SPSR 复制到 CPSR 中。执行过程如图 ;(P-3.8.4.6(b))所示。 stmfd r13!,{r0,r5,r7,r14} ;把 r0、r5、r7 和 r14 这 4 个寄存器储存在以 ;r13 寄存器值为开始地址的内存空间中,注意这 ;条指令 r13 里的地址值是向下递减的,每次储存一 ;个寄存器之前 r13 就先减 4,直到储存完最后一个 ;寄存器。然后把这个减到最后的地址值回写到 ;r13 寄存器中改变 r13 的值,执行过程如图 ;(P-3.8.4.7(a))所示。 ldmfd r13!,{r0,r5,r7,r14}^ ;从以 r13 寄存器值为开始地址的内存空间,装载 4 ;个内存字到 r0、r5、r7 和 r14 这 4 个寄存器中, ;每装载一个内存字之后,r13 寄存器的值加 4,直到 ;装载完最后一个寄存器,然后把这个减到最后的地址 ;值回写到 r13 寄存器中改变 r13 的值,执行过程如图 ;(P-3.8.4.7(b))所示。 图(P-3.8.4.1) 图(P-3.8.4.2) 图(P-3.8.4.3) 图(P-3.8.4.4) 图(P-3.8.4.5) 图(P-3.8.4.6) 图(P-3.8.4.7) 3.8.5 程序状态寄存器操作指令 ARM920T 处理器有一个 CPSR 寄存器和 5 个 SPSR 寄存器。CPSR 是当前程序状态寄存器, 所有处理器工作模式和状态下都使用同一个 CPSR 寄存器,SPSR 是程序备份状态寄存器,除 系统模式和用户模式没有这个寄存器外,其它 5 种模式都各自有一个 SPSR。然而向这些程 序状态寄存器中写入数据和读取数据都要使用专用的指令,不能简单的使用 mov 等指令。 ARM920T 处理器一共提供了两条指令来操作 CPSR、SPSR 寄存器。 1. mrs 指令,把程序状态寄存器中的数据读取到通用寄存器中。 2. msr 指令,把通用寄存器中的数据写入到程序状态寄存器中。 1. mrs 指令 mrs 指令将 CPSR 或者 SPSR 寄存器中的数据读取到通用寄存器中,例如读取到 r0 到 r12 等各个寄存器中。 在汇编中的用法: mrs{cond} , 1. {}中的表示可选项。 2. <>中的表示必须项。 3. cond,表示条件码。条件满足时执行,忽略条件码时表示无条件执行。 4. Rd,表示目标寄存器,只要是通用寄存器即可,如 r0、r1、r2、r6 等。 5. SR,表示程序状态寄存器。有如下选择: 1) CPSR,表示将 CPSR 寄存器中的数据读取到 Rd 表示的目标寄存器中。 2) SPSR,表示将特定模式下的 SPSR 寄存器中的数据读取到 Rd 表示的目标寄存器 中。 例子: main: mov r8,#0xd3 mov r9,#0 mrs r9,cpsr mrs r7,spsr ;r8=0xd3。 ;r9=0。 ;r9=cpsr。把 CPSR 读取到 r9。 ;r7=spsr。把一个特定模式的 SPSR 读取到 r7,这个模式可 ;能是系统管理模式也可能是 IRQ 模式等。 2. msr 指令 msr 指令将一个立即数或者一个通用寄存器中的内容,写入到 CPSR 寄存器或者某个特 定模式下的 SPSR 寄存器中。 在汇编中的用法: msr{cond} {<_>}, 1. {},中的表示可选项。 2. <>,中的表示必须项。 3. |,表示多选一。 4. cond,表示条件码。条件满足时执行,忽略条件码时表示无条件执行。 5. SR,表示程序状态寄存器,有如下两种选择。 1) CPSR,表示将立即数或者通用寄存器中的值写入到 CPSR 寄存器中。 2) SPSR,表示将立即数或者通用寄存器中的值写入到 SPSR 寄存器中。 6. _,表示连接符。 7. fds,表示 4 个不同的位段,有如下 4 种选择。执行指令时表示将改写哪个位段。 1) f,表示改写状态寄存器中的 24 位到 31 位。 2) s,表示改写状态寄存器中的 16 位到 23 位。 3) x,表示改写状态寄存器中的 8 位到 15 位。 4) c,表示改写状态寄存器中的 0 位到 7 位。 8. Rn,表示通用寄存器。 9. immed,表示立即数。 例子: main: mov r8,#0xd3 ;r8=0xd3。 mov r9,#0xd3 ;r9=0xd3。 msr cpsr,r9 ;CPSR=r9。把 r9 写入到 CPSR,将改写 CPSR 所有的位。 msr spsr,r9 ;SPSR=r9。把 r9 写入到 SPSR,将改写 SPSR 所有的位。 msr cpsr,#0xd3 ;CPSR=0xd3。把 0xd3 写入到 CPSR,将改写 CPSR 所有的位。 msr spsr,#0xd3 ;SPSR=0xd3。把 0xd3 写入到 SPSR,将改写 SPSR 所有的位。 msr cpsr_f,#0x10 ;把 0x10 写入到 CPSR 寄存器的 24 位到 31 位。 msr spsr_f,#0x10 ;把 0x10 写入到 SPSR 寄存器的 24 位到 31 位。 msr cpsr_s,#0x10 ;把 0x12 写入到 CPSR 寄存器的 16 位到 23 位。 msr spsr_s,#0x10 ;把 0x12 写入到 SPSR 寄存器的 16 位到 23 位。 msr cpsr_x,#0 ;把 0 写入到 CPSR 寄存器的 8 位到 15 位。 msr spsr_x,#0 ;把 0 写入到 SPSR 寄存器的 8 位到 15 位。 msr cpsr_c,#0xd3 ;把 0xd3 写入到 CPSR 寄存器的 0 位到 7 位。 msr spsr_c,#0xd3 msr cpsr_c,r8 msr spsr_c,r8 ;把 0xd3 写入到 SPSR 寄存器的 0 位到 7 位。 ;把 r8 低 8 位写入到 CPSR 寄存器的 0 位到 7 位。这导致处理器 ;切换到系统管理模式、ARM 状态、关掉 IRQ、IFQ 中断。 ;把 r8 低 8 位写入到 SPSR 寄存器的 0 位到 7 位。 3.8.6 协处理器操作指令 ARM920T 处理器为了能扩展系统功能,支持最多加入 15 个协处理器,比如内存管理单 元、浮点运算器、图像协处理器、通信调试协处理器等,每种协处理器都有各自特定的功能, 并且有些协处理器还能执行自己特定的指令。要控制并和这些协处理器进行通信,当然需要 一些特定的指令,ARM920T 处理器一共提供了五条指令,如下。 1. mrc 指令。 2. mcr 指令。 3. ldc 指令。 4. stc 指令。 5. cdp 指令。 1. mrc 指令 mrc 指令将协处理器寄存器的内容传送到 ARM920T 处理器的寄存器中,如果协处理器 不能完成这一操作,ARM920T 处理器将产生未定义指令的异常。 在汇编中的用法: mrc{cond} ,,,,{,opcode_2} 1. {},表示可选项。 2. <>,表示必须项。 3. cond,表示条件码,条件满足则执行,不写条件码则无条件执行。 4. coproc,表示协处理器编号。 5. opcode_1,表示协处理器的执行操作码。 6. Rd,表示目标寄存器,即 ARM920T 处理器的寄存器。 7. CRn,表示协处理器的寄存器,存放第一个源操作数。 8. CRm,表示附加的协处理器寄存器或者附加操作数的寄存器。 9. opcode_2,表示协处理器的执行操作码 2,有时可能需要几个操作码。 例子: main: mov r0,#0 mov r4,#0 ;r0=0。 ;r4=0。 mrc p15,0,r0,c5,c0,0 mrc p15,0,r4,c6,c0,0 ;把 cp15 协处理器的 c5 寄存器,传送到 ARM920T 处理器 r0 寄 ;存器中,两个操作码都是 0。 ;把 cp15 协处理器的 c6 寄存器,传送到 ARM920T 处理器 r4 寄 ;存器中,两个操作码都是 0。 2. mcr 指令 mcr 指令将 ARM920T 处理器的寄存器中的内容传送到协处理器的寄存器中,如果协处 理器不能完成这一操作,ARM920T 处理器将产生未定义指令的异常。 在汇编中的用法: mcr{cond} ,,,,{,opcode_2} 1. {},表示可选项。 2. <>,表示必须项。 3. cond,表示条件码,条件满足则执行,不写条件码则无条件执行。 4. coproc,表示协处理器编号。 5. opcode_1,表示协处理器的执行操作码。 6. Rd,表示 ARM920T 处理器的寄存器。 7. CRn,表示协处理器的寄存器。 8. CRm,表示附加的协处理器寄存器或者附加操作数的寄存器。 9. opcode_2,表示协处理器的执行操作码 2,有时可能需要几个操作码。 例子: main: mov r0,#0 ;r0=0。 mov r4,#0 ;r4=0。 mcr p15,0,r0,c1,c0,0 ;把 ARM920T 处理器 r0 寄存器,传送到 cp15 协处理器的 c1 寄 ;存器中,两个操作码都是 0。 mcr p15,0,r4,c2,c0,0 ;把 ARM920T 处理器 r4 寄存器,传送到 cp15 协处理器的 c2 寄 ;存器,两个操作码都是 0。 3. ldc 指令 ldc 指令从一系列连续的内存单元中装载数据到协处理器的寄存器中,如果协处理器不 能完成这一操作,ARM920T 处理器将产生未定义指令的异常。 在汇编中的用法: ldc{cond}{l} ,, 1. {},表示可选项。 2. <>,表示必须项。 3. cond,表示条件码,条件满足则执行,不写条件码则无条件执行。 4. l,表示读取长数据,比如双精度数据。 5. coproc,表示协处理器编号。 6. CRd,表示协处理器的寄存器,用于作为目标寄存器。 7. addr_mode,表示内存地址,参见 ldr、str 指令。 例子: main: mov r0,#0x80 ;r0=0x80。 mov r4,#0x40 ;r4=0x40。 ldc p6,c4,[r4,#4] ;把 ARM920T 处理器 r4 寄存器值加 4 的地址的内存单元的值 ;装载到 p6 协处理器的 c4 寄存器中。 ldc p3,c4,[r0] ;把 ARM920T 处理器 r0 寄存器的值为地址的内存单元的值 ;装载到 p3 协处理器的 c4 寄存器中。 4. stc 指令 stc 指令将协处理器的寄存器储存到一系列连续的内存单元中,如果协处理器不能完成 这一操作,ARM920T 处理器将产生未定义指令的异常。 在汇编中的用法: stc{cond}{l} ,, 1. {},表示可选项。 2. <>,表示必须项。 3. cond,表示条件码,条件满足则执行,不写条件码则无条件执行。 4. l,表示储存长数据,比如双精度数据。 5. coproc,表示协处理器编号。 6. CRd,表示协处理器的寄存器,用于作为目标寄存器。 7. addr_mode,表示内存地址,参见 ldr、str 指令。 例子: main: mov r0,#0x80 mov r4,#0x40 stc p8,c8,[r4] ;r0=0x80。 ;r4=0x40。 ;把 p8 协处理器的 c8 寄存器储存到以 ARM920T 处理器 r4 寄存 ;器的值为地址的内存单元中。 stc p3,c4,[r0] ;把 p3 协处理器的 c4 寄存器储存到以 ARM920T 处理器 r0 寄存 ;器的值为地址的内存单元中 5. cdp 指令 cdp 指令用于 ARM920T 处理器通知相关协处理器执行特定的操作,如果协处理器不能成 功完成特定的操作,ARM920T 处理器将产生未定义指令的异常。 在汇编中的用法: cdp{cond} ,,,,, 1. {},表示可选项。 2. <>,表示必须项。 3. cond,表示条件码,条件满足则执行,不写条件码则无条件执行。 4. coproc,表示协处理器编号。 5. opcode_1,表示协处理器的执行操作码。 6. CRd,表示协处理器的寄存器,作为目标寄存器。 7. CRn,表示协处理器的寄存器,存放第一个操作数。 8. CRm,表示附加的协处理器寄存器或者附加操作数的寄存器。 9. opcode_2,表示协处理器的执行操作码 2,有时可能要几个操作码。 例子: main: mov r0,#0 ;r0=0。 mov r4,#0 ;r4=0。 cdp p5,2,c12,c10,c3,4 ;该指令完成协处理器 p5 的初始化。 3.8.7 异常中断产生指令 几乎所有现代处理器都提供至少一条异常中断产生指令,比如 x86 处理器上的 int 指 令……为什么要有这样的指令,因为现代处理器要安全的隔离操作系统内核和应用软件,使 应用软件不能随便运行特权指令和随意访问操作系统内核的内存空间,然而应用软件又必须 和操作系统内核通信、调用操作系统服务。于是开发处理器的工程师们就想了个方法:设计 一条指令,只要处理器一旦执行这条指令,处理器就跳转到一个固定的地址开始运行,而这 个地址上放的就是操作系统内核的代码,就这样处理器的控制权就稳定的从应用软件手里转 移到操作系统内核手里。若应用软件以其它方式访问内核内存空间或者是运行特权指令,都 会被处理器认为是“非法操作”,处理器捕获到“非法操作”,最终处理器的控制权也会转移 到操作系统内核代码手里。详情可以参阅异常和中断章节。ARM920T 也提供一条这种类型 的指令如下。 1. swi 指令。 1. swi 指令 swi 指令的运行将导致 ARM920T 处理器产生软件中断异常,执行这条指令时会使处理 器硬件执行一些操作,一、把处理器设为 ARM 状态和系统管理模式;二、把 swi 指令后的 下一条指令的地址放进系统管理模式下的 r14 寄存器中,把 CPSR 寄存器的内容放进系统管 理模式下的 SPSR 寄存器中;三、禁止外部中断;四、强制 pc 寄存器中的值为 swi 向量,这 个向量的地址根据系统配置的不同而不同,即低地址向量是 0x00000008,高地址向量是 0xffff0008。这条指令通常用于作为系统调用的陷入指令。 在汇编中的用法: swi{cond} 1. {},表示可选项。 2. <>,表示必须项。 3. cond,表示条件码,条件满足则执行,不写条件码则无条件执行。 4. immed_24,表示一个 24 位的立即数。处理器运行这条指令时忽略这条指令的低 24 位,它不对这低 24 位数据进行检查。即不管在这低 24 个二进制位里写入任何数据 处理器始终认为这是一条 swi 指令。很多操作系统用这低 24 位存放系统调用号。不 过我们不用这种方式。 例子: mian: swi #0 ;ARM920T 处理器产生软中断异常,swi 指令的低 24 位存放的是 0。 swi #5 mov r0,#10 swi #0 ;ARM920T 处理器产生软中断异常,swi 指令的低 24 位存放的是 5。 ;r0=10,用 r0 寄存器传递系统调用号。 ;ARM920T 处理器产生软中断异常,swi 指令的低 24 位存放的是 0。 啊!这可真是一个痛苦而又漫长的过程,还好我们已经完成了。一共了解了 35 条指令, 后面用这些指令就能写出操作系统内核关键部分的代码了…… 3.9 MMU MMU 是一种硬件实现的用于内存管理的器件,本节先概述 MMU 是什么,在 ARM920T 中是 什么角色,有什么作用,接着了解为什么需要 MMU,然后看一看 ARM920T 中完成 MMU 功能的 设备,最后详细了解一下 MMU 用于管理和控制内存的页表。 3.9.1 MMU 概述 MMU 即内存管理单元,它是 ARM920T 处理器储存系统的重要组成部分。它对处理器发出 的访存地址进行映射和检查,可以让处理器发出的访存地址去访问不同的物理内存单元,例 如:处理器发出的地址是 0,通过 MMU 后则访问的可能是以 4096 为物理地址的内存单元。 同时它还检查这个地址是不是存在、以及有没有访问权限。例如我们可以通过对 MMU 的设 置,让应用软件不能访问操作系统内核的储存空间。 前面的章节中,我们介绍的内容都假定系统中没有 MMU,或者 MMU 处于关闭状态。如下 图(P-3.9.1.1)所示。 图(P-3.9.1.1) 图(P-3.9.1.1)中,我们看到没有 MMU 或者 MMU 关闭状态下,ARM920T 的内核即 ARM9TDMI 发出的地址,直接被送到总线上作为物理地址去访问内存。有很多简单的硬件系统中确实和 图(P-3.9.1.1)一样没有 MMU,或者有个比 MMU 更简单的器件叫 MPU 即内存保护单元,它 没有 MMU 的地址映射功能。 当然 ARM920T 处理器中采用的是功能相当强大的 MMU 器件。这些功能在后面会有所介 绍。当开启 ARM920T 处理器上的 MMU 时,情况如图(P-3.9.1.2)所示。 图(P-3.9.1.2) 图(P-3.9.1.2)所示的是启用 ARM920T 处理器的 MMU 时的情况,当然这是个简易的图, ARM920T 处理器的 MMU 比这个图上的 MMU 复杂多了。我们可以暂时把这个图上的 MMU 想象成 一个黑盒子,ARM920T 处理器的内核即 ARM9TDMI 发出的地址,首先要通过这个黑盒子再才 能被送到总线上去访问内存。当 ARM920T 处理器的内核 ARM9TDMI 发出的地址到达 MMU 时, MMU 首先检查这个地址是否能被访问,然后用一套机制对这个地址进行转换,生成一个新的 地址,最后把这个新生成的地址放到总线上去访问内存。人们对这两种地址用了两个专业的 术语,从处理器内核 ARM9TDMI 到 MMU 的地址,叫虚拟地址,后面我们会看到 ARM920T 比这 个情况更为复杂,因为它还有一种地址……把 MMU 放到总线上的地址称为物理地址。 后面介绍相关内容时,都假定是图(P-3.9.1.2)的情况。那么好,下面就去仔细看看 这个 MMU。 3.9.2 为什么要有 MMU 上节中我们大概了解了 MMU 是干什么的,不外乎就是两点,一,内存地址映射;二,内 存空间保护。那么为什么要有 MMU 这个硬件呢,或者说它给操作系统及应用软件的设计与开 发带来什么方便以及影响呢。 我们先从一个小程序的例子开始,程序代码如下: main: mov r0,#1 mov r1,#2 ldr r2,add_func_l ;这是条伪指令,把 add_func_l 标号地址的内容装入 r2 ;寄存器 bl r2 die: b die add_func: add r0,r0,r1 bx lr .align 4 add_func_l: .word add_func ;定义一个内存字空间存放标号 add_func 的地址。 来看看汇编器和链接器对这段代码的处理结果,我们同时指定链接器从 0 地址开始链 接和生成纯指令的二进制文件。装入内存后的情况如图(P-3.9.2.1)所示。 图(P-3.9.2.1) 为了便于阅读,笔者画上了标号和物理地址,并且在内存字单元中还是用的汇编指令符号。 注意内存芯片并不认识标号和这些符号,它只能在每个内存字单元中存放它自己都不认识的 32 个二进制的数据位。笔者这样做仅仅是为了便于阅读。ARM920T 处理器的 ARM 状态的指令 都是一个字大小即 32 位。在汇编语言中 ldr 指令是条伪指令,它具体被汇编器编译成什么 样的机器指令,是由汇编器根据具体情况决定的。这里它会编译成图(P-3.9.2.1)所示的 那样。 我们只关注上面两指令: 1. ldr r2,[pc,#20],这条指令执行后 r2 中为 0x14,当前 pc 加 20 的地址正好是 0x1c, 这里的 20 和 0x14 是由编译器和链接器写入的。 2. bl r2,保存该条指令的下一条指令的地址到 r14,然后跳转到 0x14 地址上执行。 可以发现即使是再简单的程序,也会和内存地址打交道。因为内存芯片只能根据地址来 读取或者存放指令和数据。那么我们的编译器和链接器在把我们的代码转换成二进制可执行 文件时,自然也要在程序指令数据中写入相应的地址值,程序才能正常工作。 根据上面这个例子我们发现有下面两个问题。 1. 程序只能装载到 0 地址开始内存中运行,因为“bl r2”指令执行时它会跳转到 0x14 地址,一旦移动这个程序的位置,0x14 地址的内存单元中不在是这个程序的指令。 2. 如果我们的另一个程序,也是从 0 地址开始链接并且也要装载到 0 地址开始的内存 中运行,这样就无法同时运行多个程序了,因为第二个程序会覆盖第一个程序的内 存空间。当然让每个程序从不同的地址开始链接也是可以的,如果你的操作系统有 数于万计的应用,并且这些应用来至不同的厂商,你会发现这种情况有多么糟糕。 我们期望结果是下面这样的。 1. 每个应用软件或者说进程,都能看到一个完整且连续的地址空间。例如 ARM920T 处 理器的地址可以从 0 到 0xffffffff,有 4G 字节的地址空间。 2. 对每个应用软件或者说进程,这 4G 字节的地址空间是独立的,其它程序是不能占 有的。 3. 由于 1、2 两点,编译器和链接器总是可以从这个连续的地址空间的某个地址开始 编译链接我们的程序,而且每个应用软件都一样。 然而这只是一个美好的想法,我们十分清楚的知道,一个程序要运行就必须装进物理内 存中。那么有没有办法可以解决这种问题呢。那些敢想敢做的人,经过不懈的努力终于开发 出 MMU 这种器件。 人们在 CPU 和内存之间加入了 MMU,如图(P-3.9.1.2)所示。让 CPU 在运行指令时发 出的访存地址先通过 MMU 的某种机制转换之后再去访问内存。还是上面那个例子,如图(P3.9.2.2)所示。 图(P-3.9.2.2) 由图(P-3.9.2.2)可知,MMU 把从 0 开始的虚拟地址空间映射到了从 0x1000 开始的物 理内存空间。当处理器从 0 地址开始读取程序指令时,实则是读取到是 0x1000 地址内存单 元中的指令……这时我们发现改变 MMU 的映射规则,就能把程序装载进以任意地址开始的 物理内存空间中,只要这个空间可以装载且装得下这个程序就行。 再来看看多道程序的情况,我们假定每个应用程序都是从 0 地址开始链接的,并且它们 拥有 4G 字节的虚拟地址空间。如图(P-3.9.2.3)所示。 图(P-3.9.2.3) 观察图(P-3.9.2.3)不难发现,操作系统内核只要牢牢的控制每个应用程序的 MMU 的地址 转换规则,并且调度另一个应用程序运行时就让 MMU 使用另一个应用程序的地址转换规则。 操作系统就可以自由的装载应用程序到任意物理内存空间中。应用软件开发商也少了不少麻 烦,因为他们始终可以认为他们拥有一个非常大而且连续的内存空间,并且这个内存空间就 是他们的,神圣而不可侵犯。 进一步的,开发 MMU 的人,还让 MMU 能够通过具体的地址转换规则判断当前这个地址能 不能被转换成物理内存地址去访问内存。如果不能被转换,那么 MMU 就向 CPU 发送一个信 号,请求 CPU 来处理这个问题。由此操作系统就能利用这种机制。例如对内存实施保护,还 可以实现这样的机制:只装载应用软件的一部分,当应用程序运行到下一部分时,由于 MMU 发现地址不能转换,所以向 CPU 发出地址转换失败的信号,CPU 运行操作系统的处理程序: 再次装载应用程序的下一部分,并填写好地址转换规则……由于这样操作系统就可以充分利 用有限的物理内存,使之同时运行足够多的应用软件。 MMU 的地址转换规则,后面会详细介绍的,在这里我们不必知道的那么清楚。 到这里应该明白了,为什么要有 MMU。MMU 这个神器,给我们开发软件带来了巨大的方 便,而且还能让我们的程序跑的更加稳定、可靠,下面我们就去了解 ARM920T 处理器中的 MMU 器件的具体实现…… 3.9.3 ARM920T CP15 协处理器 从前面章节中我们得知,ARM920T 处理器可以通过协处理器接口增加多达 16 个协处理 器。CP15 协处理器就是其中之一。 CP15 是专门负责 ARM920T 处理器的储存系统的,比如 MMU、CACHE、MPU 的控制等。不 同的版本 ARM 处理器的实现都有所不同。有的 ARM 处理器没有 CACHE、有的 ARM 处理器没有 MMU、有的 ARM 处理器只有 MPU 即内存保护单元。CP15 结合其它技术共同实现了 ARM 处理器 的储存系统。 ARM920T 处理器实现了 MMU 和 CACHE,但是没有 MPU,因为 MMU 已经包含了 MPU 的功能。 如图(P-3.9.3.1)所示。这个图可能在前面就见过。我们重新把它搬到这里。 图(P-3.9.3.1) 从图(P-3.9.3.1)中,不难发现 MMU、CACHE 都直接或者间接的连接在 CP15 上。程序 通过操作 CP15 就能达到控制 MMU、CACHE 的目的。当然 CP15 提供给程序的接口就是一大堆 寄存器。 CP15 一共有 16 个 32 位的寄存器。如表(T-3.9.3.1)所示。 寄存器 MMU 中的作用 MPU 中的作用 c0 ID 编码和 CACHE 类型 c1 各种控制位 c2 页表基地址 Cachability 的控制位 c3 域访问控制位 Bufferablity 控制位 c4 保留 保留 c5 内存访问失败的状态 访问权限控制位 c6 内存访问失败的地址 保护区域控制 c7 高速缓存和写缓存控制 c8 TLB 控制 保留 c9 高速缓存锁定 c10 TLB 锁定 保留 c11 c12 c13 进程标识符 c14 c15 因不同设计而异 因不同设计而异 表(T-3.9.3.1) 注意:表中空格表示定义不明确,不用担心我们也不用关心这些。 下面我们来分别看看这些寄存器,注意我们不会关注 MPU 下的情况。我们要关注的 CP15 寄存器如下: 1. c0 寄存器。 2. c1 寄存器。 3. c2 寄存器。 4. c3 寄存器。 5. c5 寄存器。 6. c6 寄存器。 7. c7 寄存器。 8. c8 寄存器。 9. c9 寄存器。 10. c10 寄存器。 11. c13 寄存器。 1. c0 寄存器 c0 寄存器稍微有点复杂,它对应两个不同的物理寄存器,这两个不同的物理寄存器分 别存放 ARM 处理器相关信息及 ID 和 CACHE 类型,操作时用 mrc 指令的第二个操作码 opcode_2 区分。关于访问协处理器及其指令,请参阅指令集的相关章节。opcode_2 编码如下表(T- 3.9.3.2)。 opcode_2 编码 对应的寄存器 0b000 主 ID 标识寄存器 0b001 CACHE 类型寄存器 其它 保留 表(T-3.9.3.2) 注意 0b 表示紧跟其后的是二进制数据。 表(T-3.9.3.2)中两个寄存器都是 32 位的,先来看看主 ID 标识寄存器的格式,如图(P- 3.9.3.2)所示。 图(P-3.9.3.2) 主 ID 标识寄存器的各位段如表(T-3.9.3.3)。 寄存器中的位段 相关说明 位[3:0] 生产商定义的处理器版本号 位[15:4] 生产商定义的产品主编号,其中最高 4 位即 位[15:12]可能的取值为 0~7 但不能是 0 或 7 位[19:16] ARM 体系的版本号,可能的取值如下: 1. 0x1 ARM 体系版本 4。 2. 0x2 ARM 体系版本 4T。 3. 0x3 ARM 体系版本 5。 4. 0x4 ARM 体系版本 5T。 5. 0x5 ARM 体系版本 5TE。 6. 其他 由 ARM 公司保留将来使用。 位[23:20] 生产商定义的产品子编号,当产品主编号相 同时,使用子编号来区分不同的产品子类, 如产品中不同的高速缓存的大小等。 位[31:24] 生产厂商的编号,现在已经定义的有以下 值: 1. 0x41=A 表示 ARM 公司。 2. 0x44=D 表示 Digital Equipment 公司。 3. 0x69=I 表示 intel 公司。 表(T-3.9.3.3) 根据表(T-3.9.3.2)知道,当 mrc 指令的 opcode_2 编码为 0b001 时,访问的就是 CACHE 类型寄存器。这个的寄存器格式如图(P-3.9.3.3)所示。 图(P-3.9.3.3) CACHE 类型寄存器各位段如表(T-3.9.3.4)。 寄存器中 相关说明 的位段 位[11:0] 定义指令 CACHE 的相关属性,如下: 位段 相关说明 位[1:0] 定义块大小,其编码如下: 编码 CACHE 块大小 0b00 8 字节 0b01 16 字节 0b10 32 字节 0b11 64 字节 位[2:2] 取值 0/1,将影响其它位段。 位[5:3] 定义 CACHE 的相联特性,其编码如下: 编码 位[2:2]=0 时的情 位[2:2]=1 时的情 位 [23:12] 况 况 0b000 1 路相联 没有 CACHE 0b001 2 路相联 3 路相联 0b010 4 路相联 6 路相联 0b011 8 路相联 12 路相联 0b100 16 路相联 24 路相联 0b101 32 路相联 48 路相联 0b110 64 路相联 96 路相联 0b111 128 路相联 192 路相联 位[8:6] 定义 CACHE 的容量,其编码如下: 编码 位[2:2]=0 时的情 位[2:2]=1 时的情 况 况 0b000 0.5KB 0.75KB 0b001 1KB 1.5KB 0b010 2KB 3KB 0b011 4KB 6KB 0b100 8KB 12KB 0b101 16KB 24KB 0b110 32KB 48KB 0b111 64KB 96KB 位 保留,值为 0b000 [11:9] 定义数据 CACHE 的相关属性,如下: 位段 相关说明 位 定义块大小,其编码如下: [13:12] 编码 CACHE 块大小 0b00 8 字节 0b01 16 字节 0b10 32 字节 0b11 64 字节 位 取值 0/1,将影响其它位段。 [14:14] 位 定义数据 CACHE 的相联特性,其编码如下: [17:15] 编码 位[14:14]=0 时的 位[14:14]=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 路相联 位 定义数据 CACHE 的容量,其编码如下: [20:18] 编码 位[14:14]=0 时的 位[14:14]=1 时的 情况 情况 0b000 0.5KB 0.75KB 0b001 1KB 1.5KB 0b010 2KB 3KB 0b011 4KB 6KB 0b100 8KB 12KB 0b101 16KB 24KB 0b110 32KB 48KB 0b111 64KB 96KB 位 保留,值为 0b000 [23:21] 位 为 0 时系统使用统一的 CACHE,为 1 时系统分别使用指令 CACHE 和数据 CACHE。 [24:24] 为 0 时,位[23:12]、位[11:0]都定义同一个 CACHE 的相关属性。 位 定义 CACHE 的其它属性,如下: [28:25] 编码值 CACHE 类型 CACHE 内容清除 CACHE 内容锁定 方法 方法 0b0000 写通类型 不需要内容清除 不支持内容锁定 0b0001 写回类型 数据块读取 不支持内容锁定 0b0010 写回类型 由 c7 寄存器定义 不支持内容锁定 0b0011 写回类型 由 c7 寄存器定义 支持格式 A,后面 会介绍 0b0100 写回类型 由 c7 寄存器定义 支持格式 B,后面 会介绍 位 保留 0b000。 [31:29] 表(T-3.9.3.4) 注意 0b 表示紧跟其后的是二进制数据。 2. c1 寄存器 c1 寄存器是个控制寄存器,控制 ARM920T 处理器的储存系统。例如打开、关闭 MMU,打 开、关闭 CACHE,设置中断异常向量地址即高地址向量,控制储存器访问对齐等…… c1 寄存器的格式如图(P-3.9.3.4)所示。 图(P-3.9.3.4) c1 寄存器各位段如表(T-3.9.3.5)所示。 寄存器中的位段 相关说明 M 位[0:0] 0:禁止 MMU 或者 MPU;1:使能 MMU 或者 MPU;如果没有 MMU 或者 MPU, 读取该位时返回 1,写入时忽略该位。 A 位[1:1] 0:禁止地址对齐检查;1:使能地址对齐检查; C 位[2:2] 0:禁止数据/整个 CACHE;1:使能数据/整个 CACHE;如果没有 CACHE, W 位[3:3] P 位[4:4] D 位[5:5] L 位[6:6] B 位[7:7] S 位[8:8] R 位[9:9] F 位[10:10] Z 位[11:11] I 位[12:12] V 位[13:13] RR 位[14:14] L4 位[15:15] 读取该位时返回 0,写入时忽略该位。如果不能禁止 CACHE 时,读取 该位时返回 1,写入时忽略该位。 0:禁止写缓冲;1:使能写缓冲;如果没有写缓冲,读取该位时返回 0,写入时忽略该位。如果不能禁止写缓冲时,读取该位时返回 1,写 入时忽略该位。 0:异常中断处理程序进入 32 位地址模式;1:异常中断处理程序进入 26 位地址模式;由于早期 ARM 处理器支持 26 位地址,如果不向前兼 容 26 位地址时,读取该位时返回 1,写入时忽略该位。 0:禁止 26 位地址异常检查;1:使能 26 位地址异常检查;如果不向 前兼容 26 位地址时,读取该位时返回 1,写入时忽略该位。 0:选择早期中止模型;1:选择后期中止模型;对 ARM 版本 3 以后的 处理器,读取该位时返回 1,写入时忽略该位。 0:小端储存格式;1:大端储存格式;对于只支持小端储存格式的系 统,读取该位时返回 0,写入时忽略该位;对于只支持大端储存格式的 系统,读取该位时返回 1,写入时忽略该位; 在基于 MMU 的存储系统中,本位用作系统保护。后面会详细介绍。 在基于 MMU 的存储系统中,本位用作 ROM 保护。后面会详细介绍。 由具体生产商定义。 0:禁止跳转预测功能;1:使能跳转预测功能;如果不支持跳转预测, 读取该位时返回 0,写入时忽略该位; 0:禁止指令 CACHE;1:使能指令 CACHE;如果使用统一的 CACHE 或者 没有 CACHE 时,读取该位时返回 0,写入时忽略该位;如果不能禁止 指令 CACHE 时,读取该位时返回 1,写入时忽略该位; 0:选择低端异常中断向量 0x00000000~0x0000001c;1:选择高端异常 中断向量 0xffff0000~ 0xffff001c;如果不支持高端向量,读取该位 时返回 0,写入时忽略该位; 0:常规的 CACHE 淘汰算法,如随机淘汰;1:预测性淘汰算法,如 roundrobin 淘汰算法; 0:保持 ARMv5 以上版本的正常功能;1:将 ARMv5 以上版本与以前版 位[31:16] 本处理器兼容,不根据跳转地址的 bit[0]进行 ARM 状态和 Thumb 状态 切换;对于 ARM920T 处理器该位保留。 这些位保留将来使用。 表(T-3.9.3.5) 3. c2 寄存器 c2 寄存器保存的是 32 位的一级页表基地址。注意这个基地址是物理地址,并且这个物 理地址是 16K 对齐的即这个地址的低 14 位是 0。关于什么是页表,后面会详细介绍。c2 寄 存器的格式如图(P-3.9.3.5)所示。 图(P-3.9.3.5) 4. c3 寄存器 c3 寄存器定义了 ARM920T 处理器的 16 个域的访问权限。域就是一些页面的集合,关于 页面后面会详细介绍的。这个寄存器也是 32 位的,每两位表示一个域,故能表示 16 个域。 其格式如图(P-3.9.3.6)所示。 图(P-3.9.3.6) 每个域的编码及含义如表(T-3.9.3.6)所示。 域编码 访问类型 相关说明 0b00 没有访问权限 这时访问该域,将产生储存器访问失败。 0b01 用户类型权限 根据页表条目(后面会介绍)中的相关控制位,决定是否允 许特定的储存器访问。 0b10 保留 该值将产生不可预料的结果。 0b11 管理类型权限 不考虑页表条目中的相关控制位,这种情况下不会产生储存 器访问失败。 表(T-3.9.3.6) 注意 0b 表示紧跟其后的是二进制数据。 5. c5、c6 寄存器 当访问储存器失败时,c5 寄存器存放储存器访问失败时的状态,访问储存器失败的地 址则存放在 c6 寄存器中。MMU 在这几种情况下:地址不对齐、地址转换失败、域控制失 败、访问权限不够时,会产生储存器访问失败。c5、c6 寄存器的编码格式如图(P-3.9.3.7) 所示。 图(P-3.9.3.7) c5 寄存器各位段如表(T-3.9.3.7)所示。 寄存器中的位段 相关说明 位[3-0] 存放了引起储存器访问失败的访存类型。各类型值和相关说明如下: 类型值 储 存 访问 失败 域标识 有/无 c6 失败地址寄 的原因 效 存器 有/无效 0b0010 极端异常 无效 由生产商定义 0b0000 中 断 向量 访问 无效 异常 有效 0b00x1* 地址对齐 无效 有效 0b1100 一 级 页表 访问 无效 失败 有效 0b1110 二 级 页表 访问 有效 有效 失败 0b0101 基 于 段的 地址 无效 变换失败 有效 0b0111 基 于 页的 地址 有效 有效 0b1001 0b1011 0b1101 0b1111 0b0100 0b0110 0b1000 0b1010 变换失败 基 于 段的 存储 有效 器访问中域控 制失败 基 于 页的 存储 有效 器访问中域控 制失败 基 于 段的 存储 有效 器访问中访问 权限控制失败 基 于 页的 存储 有效 器访问中访问 权限控制失败 基 于 段 的 有效 CACHE 预取时 外部存储系统 失败 基 于 页 的 有效 CACHE 预取时 外部存储系统 失败 基 于 段 的 非 有效 CACHE 预取时 外部存储系统 失败 基 于 页 的 非 有效 CACHE 预取时 外部存储系统 失败 有效 有效 有效 有效 有效 有效 有效 有效 位[7-4] 存放了储存器访问失败时所在的域。 位[8-8] 这个位为 0。 位[31-9] 保留将来使用。 表(T-3.9.3.7) 注意 0b 表示紧跟其后的是二进制数据。0b00x1*中的“x”表示未定义,可以是 0 也可以是 1。关于段、页表,后面会详细介绍的。 6. c7 寄存器 c7 寄存器它是一个只写寄存器,读操作将产生不可预知的后果。用来控制 CACHE 和写 缓存。这个寄存器有点复杂,寄存器编码格式如图(P-3.9.3.8)所示。 图(P-3.9.3.8) 访问 c7 寄存器的指令格式如下所示: mcr p15,0,,,crm, 取值的不同组合将实现不同功能。如表(T-3.9.3.8)所示。这个 表里可能有些东西不是很明白,没事,读者也许还不知道什么是 CACHE,我们后面会专门介 绍的。 ( 表 示 相关说明 写入的数 据) 0 c0 4 等待中断激活,这是因为 ARM 处理器进入省电状态 时,需要外部中断激活。 0 c5 0 将整个指令 CACHE 的所有内容标识为无效。访问时 将不命中,CACHE 会从储存器中加载数据。对于写 回类型的 CACHE,并不将标识无效的内容回写到储 存器中。 虚拟地址 c5 1 将指令 CACHE 中的某一块标识为无效。访问到这块 时,将从储存器中加载数据。对于写回类型的 CACHE 组号/ c5 2 组内序号 0 c5 4 0 c5 6 生产商定义 c5 7 0 c6 0 虚拟地址 c6 1 CACHE 组号/ c6 2 组内序号 0 c7 0 虚拟地址 c7 1 CACHE 组号/ c7 2 组内序号 CACHE,并不将标识无效的内容回写到储存器中。 将指令 CACHE 中的某一块标识为无效。访问到这块 时,将从储存器中加载数据。对于写回类型的 CACHE,并不将标识无效的内容回写到储存器中。 清空预取缓冲区,预取缓冲区是生产商定义的,预 取缓冲区的内容会被回写到储存器中。 清空整个跳转目标 CACHE,跳转目标 CACHE 是生产 商定义的,跳转目标 CACHE 的内容会被回写到储存 器中。 清空整个跳转目标 CACHE 中的某块。 将整个数据 CACHE 的所有内容标识为无效。访问时 将不命中,CACHE 会从储存器中加载数据。对于写 回类型的 CACHE,并不将标识无效的内容回写到储 存器中。 将数据 CACHE 中的某一块标识为无效。访问到这块 时,将从储存器中加载数据。对于写回类型的 CACHE,并不将标识无效的内容回写到储存器中。 将数据 CACHE 中的某一块标识为无效。访问到这块 时,将从储存器中加载数据。对于写回类型的 CACHE,并不将标识无效的内容回写到储存器中。 将统一 CACHE 或者数据 CACHE 和指令 CACHE 的所有 内容标识为无效。访问时将不命中,CACHE 会从储 存器中加载数据。对于写回类型的 CACHE,并不将 标识无效的内容回写到储存器中。 将统一 CACHE 中的某一块标识为无效。访问到这块 时,将从储存器中加载数据。对于写回类型的 CACHE,并不将标识无效的内容回写到储存器中。 将统一 CACHE 中的某一块标识为无效。访问到这块 时,将从储存器中加载数据。对于写回类型的 0 c8 2 虚拟地址 c10 1 CACHE 组号/ c10 2 组内序号 0 c10 4 虚拟地址 c11 1 CACHE 组号/ c11 2 组内序号 虚拟地址 c13 1 虚拟地址 c14 1 CACHE 组号/ c14 2 组内序号 虚拟地址 c15 1 CACHE 组号/ c15 2 组内序号 CACHE,并不将标识无效的内容回写到储存器中。 等待中断激活,这是因为 ARM 处理器进入省电状态 时,需要外部中断激活。 清空数据 CACHE 中的某一块。将该块内容回写到储 存器中。 清空数据 CACHE 中的某一块。将该块内容回写到储 存器中。 清空写缓冲区。将写缓冲区中的内容全部回写到储 存器中。 清空统一 CACHE 中的某一块。将该块内容回写到储 存器中。 清空统一 CACHE 中的某一块。将该块内容回写到储 存器中。 清空预取指令 CACHE 中某一块。将该块内容回写到 储存器中。 清空并标识无效数据 CACHE 中的某一块。 清空并标识无效数据 CACHE 中的某一块。 清空并标识无效统一 CACHE 中的某一块。 清空并标识无效统一 CACHE 中的某一块。 表(T-3.9.3.8) 7. c8 寄存器 c8 寄存器用来控制清除 TLB 的内容,是只写寄存器,读操作将产生不可预知的后果。 TLB 是 MMU 用于加速查询地址转换规则的一种快表。一些地址转换之后就存放在这个表中, 下次用到这个地址时就不用从储存器中读取转换规则了。寄存器编码格式如图(P-3.9.3.9) 所示。 图(P-3.9.3.9) 访问 c8 寄存器的指令格式如下所示: mcr p15,0,,,crm, 的不同取值组合实现不同功能。如表(T-3.9.3.9)所示。 ( 表 示 相关说明(对 CACHE 的影响) 写入的数 据) 0 c7 0 将统一 CACHE 或者数据 CACHE 和指令 CACHE 标识为 无效。 虚拟地址 c7 1 将统一 CACHE 中的单个地址转换规则标识为无效。 0 c5 0 将整个指令 CACHE 标识为无效。 虚拟地址 c5 1 将整个指令 CACHE 中的单个地址转换规则标识为无 效。 0 c6 0 将整个数据 CACHE 标识为无效。 虚拟地址 c6 1 将整个数据 CACHE 中的单个地址转换规则标识为无 效。 表(T-3.9.3.9) 8. c9 寄存器 c9 寄存器用于控制 CACHE 内容锁定。寄存器编码格式如图(P-3.9.3.10)所示。 图(P-3.9.3.10) 访问 c9 寄存器的指令格式如下所示: mcr p15,0,,,c0, mrc p15,0,,,c0, 如果系统中包含独立的指令 CACHE 和数据 CACHE,那么对应于数据 CACHE 和指令 CACHE 分别 有一个独立的 CACHE 内容锁定寄存器,用来选择其中的某个寄存器: 1. =1,选择指令 CACHE 的内容锁定寄存器; 2. =0,选择数据 CACHE 的内容锁定寄存器; 其中 index 表示当下一次发生 CACHE 未命中时,将预取的存储块存入 CACHE 中该块对应的 组中序号为 index 的 CACHE 块中。此时序号为 0~index-1 的 CACHE 块被锁定,当发生 CACHE 块替换时,从序号为 index 到其它关联性的块中选择被替换的块。 9. c10 寄存器 c10 寄存器用于控制 TLB 内容锁定。寄存器编码格式如图(P-3.9.3.11)所示。 图(P-3.9.3.11) c10 寄存器各位段如表(T-3.9.3.10)所示。 寄存器位段 相关说明 P 位[0:0] 1:写入 TLB 的地址转换规则条目不会受到使整个 TLB 标识无效操作 的影响,一直保持有效;0:写入 TLB 的地址转换规则条目将会受到 使整个 TLB 标识无效操作的影响。 位[19:1] 读取时不可预知,写入时应为 0。 victim 位[25:20] 指定下一次 TLB 没有命中(所需的地址转换规则条目没有包含在 TLB 中)时,从内存页表中读取所需的地址转换规则条目,并把该地址转 换规则条目保存在 TLB 中的地址 victim 处。 base 位[31:26] 指定 TLB 替换时,所使用的地址范围,从(base)到(TLB 中条目数 -1);字段 victim 的值应该包含在该范围内。 表(T-3.9.3.10) 访问 c10 寄存器的指令格式如下所示: mcr p15,0,,,c0, mrc p15,0,,,c0, 如果系统中包含独立的指令 TLB 和数据 TLB,那么对应于数据 TLB 和指令 TLB 分别有一个独 立的 TLB 内容锁定寄存器,用来选择其中的某个寄存器: 1. =1,选择指令 TLB 的内容锁定寄存器; 2. =0,选择数据 TLB 的内容锁定寄存器; 10. c13 寄存器 c13 寄存器用于快速上下文切换。这个寄存器说简单也简单说复杂也复杂,因为它关系 到快速上下文切换技术。我们慢慢的看,首先 c13 寄存器的编码格式如图(P-3.9.3.12)所 示: 图(P-3.9.3.12) 访问 c13 寄存器的指令格式如下所示: mcr p15,0,,,c0,0 mrc p15,0,,,c0,0 图(P-3.9.3.12)中,PID 表示当前进程所在的进程空间块的编号,即当前进程的进程标识 符,取值为 0~127。若 PID 为 0:MVA(变换后的虚拟地址)= VA(虚拟地址),即禁止快速 上下文切换技术,系统复位后 PID=0;非 0:即开启快速上下文切换技术。 快速上下文切换位于 CPU 和 MMU 之间,可以参见图(P-3.9.3.1)的 C13。如果两个 进程使用了同样的虚拟地址空间,则对 CPU 而言,两个进程就使用了相同的虚拟地址空间。 快速上下文切换机构对各个进程的虚拟地址再次进行变换,这样系统中除了 CPU 之外的部 分看到的是已经经过快速上下文切换机构变换的虚拟地址。如图(P-3.9.3.13)所示。 图(P-3.9.3.13) 在 ARM 系统中,4GB 的虚拟空间被分成了 128 个进程空间块,每一个进程空间块大小为 32MB。 该块中包含一个进程,该进程可以使用虚拟地址空间为 0x00000000~0x01FFFFFF,这个地址 范围也就是 CPU 看到的一个进程的虚拟地址空间。系统中有 128 个进程空间块编号即 0~127, 标 号 为 X 的 进 程 空 间 块 中 的 进 程 实 际 使 用 的 虚 拟 地 址 空 间 为 ( X*0x02000000 ) 到 (X*0x02000000+0x01FFFFFF),这个地址空间是系统中除了 CPU 之外的其他部分看到的该进 程所占用的虚拟地址空间。如图(P-3.9.3.14)所示。 图(P-3.9.3.14) 到这里我们关注的所有寄存器就介绍完了,肯定这些寄存器中我们还有一些不明白的, 不要怕,等到后面介绍相关内容时,再回过头来看这里就全明白了。一时的不懂,不要觉得 灰败…… 3.9.4 MMU 页表 前面我们说到 MMU 把虚拟地址转换成对应的物理地址,需要一些转换规则,实现这一 转换规则的东西,实际上有个专业的术语叫页表。页表中的每个条目表示一个地址转换规则。 MMU 地址映射时是按照地址空间块映射的,即一块连续的虚拟地址空间对应一块连续 的物理地址空间。这个地址空间块叫页面。如图(P-3.9.4.1)所示。可能一下子不能明白, 但看了后面的页表之后就明白了。 图(P-3.9.4.1) 当然图(P-3.9.4.1)中的地址空间块的大小是可变的。ARM920T 处理器中的 MMU 支持 4 种不同大小的地址空间块,分别如下: 1. 1MB,称为段。 2. 64KB,称为大页。 3. 4KB,称为小页。 4. 1KB,称为极小页。 下面我们分别仔细介绍这 4 种不同大小的页面。在这之前先来看看什么是页表,页表在哪 里,它里面是什么数据。页表是存在于内存中的一块数据,这块数据中有很多条目,在 ARM920T 的 MMU 下每个条目是一个储存字大小即 32 位。相当一个整形数组,用 C 语言可 以这样描述:int pagetbl[xxxx]。具体有多少条目要视情况而定,后面会有介绍。如图(P-3.9.4.2) 所示。这些条目的位段,有一定的格式。根据这些条目中不同的数据,MMU 转换后的物理 地址和地址空间块即页面的大小也会有所不同。 图(P-3.9.4.2) 1. 段 段映射下,ARM920T 的 MMU 把虚拟地址空间和物理地址空间分成 1MB 大小的地址空 间块,即每个页面大小为 1MB。由于是 1MB 大小的页面,所以只需要一级页表。这个页表 中有 4096 个条目。每个条目映射 1MB 大小的空间,正好对应于 ARM920T 4GB 的虚拟地址 空间。段映射下的页表条目格式如图(P-3.9.4.3)所示。 图(P-3.9.4.3) 图(P-3.9.4.3)中的段页表条目各位段如表(T-3.9.4.1)所示。 条目位段 相关说明 位[1:0] 页表条目类型标识,对于段页必须是二进制 10。00 表示无效映射,MMU 会 发出访存失败的信号。 位[3:2] 表示这个条目映射的内存空间,是否为回写缓存、写通缓存、非缓存、缓冲, 或者无缓存并且无缓冲。 位[4:4] 为 1,为 0 都行,但为了向后兼容应该为 1。 位[8:5] 表示这个条目映射的内存空间属于哪个域,参见 c3 寄存器。 位[9:9] 段页条目下总是为 0。 位[11:10] 表示这个条目映射的内存空间的访问权限。 位[19:12] 段页条目下总是为 0。 位[31:20] 表示这个条目映射的内存空间的物理地址,由于段页是 1MB 大小,所以地 址的低 20 位始终为 0,正因为如此这个条目的低 20 位才可以用于访问属性 控制等。 表(T-3.9.4.1) 下面来看看段页映射下,MMU 是如何利用内存中的页表实现虚拟地址到物理地址的转换的。 如图(P-3.9.4.4)所示。 图(P-3.9.4.4) 图(P-3.9.3.4),MMU 的执行步骤如下: 1. MMU 拿到虚拟地址。 2. MMU 将 CP15 C2 寄存器中的高位[31:14]和虚拟地址的高位[31:20]并且外加两个为 0 的二进制位,一起组成一个 32 位的地址,这个地址就是指向页表条目的地址。 3. 用第 2 步中合成的地址读取一个页表条目。 4. 用页表条目中的高位[31:20]与虚拟地址的低位[19:0]一起组成 32 位的物理地址。 5. 用第 4 步中合成的物理地址去访问物理内存。 我们来看个例子:如图(P-3.9.4.5)所示。 图(P-3.9.4.5) 这个例子演示了,从虚拟地址:0xFFFF00FF 到物理地址:0x1F00FF 的映射过程。这个过程如 下: 1. MMU 拿到虚拟地址:0xFFFF00FF。 2. MMU 将 CP15 C2 寄存器中的高位[31:14]:0x1 和虚拟地址的高位[31:20]: 0xFFF 并且 外加两个为 0 的二进制位,一起组成一个 32 位的地址:0x7FFC,这个地址就是指向 页表条目的地址。 3. 用第 2 步中合成的地址:0x7FFC,读取一个页表条目。 4. 用页表条目中的高位[31:20]:0x1 与虚拟地址的低位[19:0]:0xF00FF 一起组成 32 位 的物理地址:0x1FF0FF。 5. 用第 4 步中合成的物理地址:0x1FF0FF 去访问物理内存。 2. 大页 大页映射下,ARM920T 的 MMU 把虚拟地址空间和物理地址空间分成 64KB 大小的地址 空间块,即每个页面大小为 64KB。由于是 64KB 大小的页面,所以需要两级页表。一级页表 中有 4096 个条目。每个条目映射二级页表的空间,二级页表中有 256 个条目,对于只有大 页的情况下,只用到了前 16 个条目。每个二级页表的条目可以映射 64KB 大小的地址空间, 大页映射下一级页表条目和二级页表条目的格式如图(P-3.9.4.6)所示。 图(P-3.9.4.6) 图(P-3.9.4.6)中的大页的一级页表条目的各位段如表(T-3.9.4.2)所示。 条目位段 相关说明 位[1:0] 页表条目类型标识,对于大页必须是二进制 01。 位[3:2] 应该为 0。 位[4:4] 为 1,为 0 都行,但为了向后兼容应该为 1 。 位[8:5] 表示这个条目映射的内存空间属于哪个域,参见 c3 寄存器。 位[9:9] 应该为 0。 位[31:10] 指向二级页表的物理地址,二级页表地址是 1KB 对齐的,所以地址的低 10 位始终为 0,正因为如此这个条目的低 10 位才可以用于访问属性控制等。 表(T-3.9.4.2) 图(P-3.9.4.6)中的大页的二级页表条目的各位段如表(T-3.9.4.3)所示。 条目位段 相关说明 位[1:0] 页表条目类型标识,对于大页必须是二进制 01 位[3:2] 表示这个条目映射的内存空间,是否为回写缓存、写通缓存、非缓存、缓冲, 或者无缓存并且无缓冲。 位[11:4] 一个大页可分为 4 个子页,每个子页 16KB。 AP0:控制第 1 个子页的访问权限。 AP1:控制第 2 个子页的访问权限。 AP2:控制第 3 个子页的访问权限。 AP3:控制第 4 个子页的访问权限。 位[15:12] 应该为 0。 位[31:16] 表示这个条目映射的内存空间的物理地址,由于大页是 64KB 大小,所以地 址的低 16 位始终为 0,正因为如此这个条目的低 16 位才可以用于访问属性 控制等。 表(T-3.9.4.3) 下面我们来看看大页映射下 MMU 是如何用内存中的两级页表实现虚拟地址到物理地址的 转换的。如图(P-3.9.4.7)所示。 图(P-3.9.4.7) 图(P-3.9.4.7),MMU 的执行步骤如下: 1. MMU 拿到虚拟地址。 2. MMU 将 CP15 C2 寄存器中的高位[31:14]和虚拟地址的高位[31:20]并且外加两个为 0 的二进制位,一起组成一个 32 位的地址,这个地址就是指向一级页表条目的地址。 3. 用第 2 步中合成的地址读取一个一级页表条目。 4. 用一级页表条目中的高位[31:10]与虚拟地址的低位[19:16]并且外加两个为 0 的二进 制位,一起组成一个 32 位的地址,这个地址就是指向二级页表条目的地址。 5. 用第 4 步中合成的地址读取一个二级页表条目。 6. 用二级页表条目中的高位[31:16]与虚拟地址的低位[15:0]一起组成 32 位的物理地址。 7. 最后用第 6 步中组成 32 位的物理地址去访问内存。 我们来看个例子:如图(P-3.9.4.8)所示。 图(P-3.9.4.8) 这个例子演示了,从虚拟地址:0x100000 到物理地址:0xF0000 的映射过程。这个过程如下: 1. MMU 拿到虚拟地址:0x100000。 2. MMU 将 CP15 C2 寄存器中的高位[31:14]:0x1 和虚拟地址的高位[31:20]: 0x1 并且外 加两个为 0 的二进制位,一起组成一个 32 位的地址:0x4004,这个地址就是指向 一级页表条目的地址。 3. 用第 2 步中合成的地址读取一个一级页表条目。 4. 用一级页表条目中的高位[31:10]:0x40 与虚拟地址的低位[19:16]:0 并且外加两个 为 0 的二进制位,一起组成一个 32 位的地址:0x8000,这个地址就是指向二级页 表条目的地址。 5. 用第 4 步中合成的地址读取一个二级页表条目。 6. 用二级页表条目中的高位[31:16]:0xF 与虚拟地址的低位[15:0]:0 一起组成 32 位的 物理地址:0xF0000 7. 最后用第 6 步中组成 32 位的物理地址:0xF0000 去访问内存。 3. 小页 小页映射下,ARM920T 的 MMU 把虚拟地址空间和物理地址空间分成 4KB 大小的地址 空间块,即每个页面大小为 4KB。由于是 4KB 大小的页面,所以需要两级页表。一级页表中 有 4096 个条目。每个条目映射二级页表的空间,二级页表中有 256 个条目,每个二级页表 条目可以映射 4KB 大小的地址空间,小页映射下一级页表条目和二级条目的格式如图(P3.9.4.9)所示。 图(P-3.9.4.9) 图(P-3.9.4.9)中的小页的一级页表条目中的各位段如表(T-3.9.4.4)所示。 条目位段 相关说明 位[1:0] 页表条目类型标识,对于小页可以是二进制 01 也可以是 11。11 下情况有所 不同,这里以 01 为准。 位[3:2] 应该为 0。 位[4:4] 为 1,为 0 都行,但为了向后兼容应该为 1 。 位[8:5] 表示这个条目映射的内存空间,属于哪个域,参见 c3 寄存器。 位[9:9] 应该为 0。 位[31:10] 指向二级页表的物理地址,二级页表地址是 1KB 对齐的,所以地址的低 10 位始终为 0,正因为如此这个条目的低 10 位才可以用于访问属性控制等。 表(T-3.9.4.4) 图(P-3.9.4.9)中的小页的二级页表条目中的各位段如表(T-3.9.4.5)所示。 条目位段 相关说明 位[1:0] 页表条目类型标识,对于小页必须是二进制 10。 位[3:2] 表示这个条目映射的内存空间,是否为回写缓存、写通缓存、非缓存、缓冲, 或者无缓存并且无缓冲。 位[11:4] 一个小页可以分为 4 个子页,每个子页 1KB。 AP0:控制第 1 个子页的访问权限。 AP1:控制第 2 个子页的访问权限。 AP2:控制第 3 个子页的访问权限。 AP3:控制第 4 个子页的访问权限。 位[31:12] 表示这个条目映射的内存空间的物理地址,由于小页是 4KB 大小,所以地址 的低 12 位始终为 0,正因为如此这个条目的低 12 位才可以用于访问属性控 制等。 表(T-3.9.4.5) 下面我们来看看小页映射下 MMU 是如何用内存中的两级页表实现虚拟地址到物理地址的 转换的。如图(P-3.9.4.10)所示。 图(P-3.9.4.10) 图(P-3.9.4.10)中,MMU 的执行步骤如下: 1. MMU 拿到虚拟地址。 2. MMU 将 CP15 C2 寄存器中的高位[31:14]和虚拟地址的高位[31:20]并且外加两个为 0 的二进制位,一起组成一个 32 位的地址,这个地址就是指向一级页表条目的地址。 3. 用第 2 步中合成的地址读取一个一级页表条目。 4. 用一级页表条目中的高位[31:10]与虚拟地址的低位[19:12]并且外加两个为 0 的二进 制位,一起组成一个 32 位的地址,这个地址就是指向二级页表条目的地址。 5. 用第 4 步中合成的地址读取一个二级页表条目。 6. 用二级页表条目中的高位[31:12]与虚拟地址的低位[11:0]一起组成 32 位的物理地址。 7. 最后用第 6 步中组成 32 位的物理地址去访问内存。 我们来看个例子:如图(P-3.9.4.11)所示。 图(P-3.9.4.11) 这个例子演示了,从虚拟地址:0x100040 到物理地址:0x40 的映射过程。这个过程如下: 1. MMU 拿到虚拟地址:0x100040。 2. MMU 将 CP15 C2 寄存器中的高位[31:14]:0x1 和虚拟地址中的高位[31:20]:0x1 并 且外加两个为 0 的二进制位,一起组成一个 32 位的地址:0x4004,这个地址就是 指向一级页表条目的地址。 3. 用第 2 步中合成的地址:0x4004 读取一个一级页表条目。 4. 用一级页表条目中的高位[31:10]:0x40 与虚拟地址中的低位[19:12] :0 并且外加两 个为 0 的二进制位,一起组成一个 32 位的地址:0x8000,这个地址就是指向二级 页表条目的地址。 5. 用第 4 步中合成的地址:0x8000 读取一个二级页表条目。 6. 用二级页表条目中的高位[31:12]:0 与虚拟地址位[11:0]:0x40 一起组成 32 位的物 理地址:0x40。 7. 最后用第 6 步中组成 32 位的物理地址:0x40 去访问内存。 4. 极小页 极小页映射下,ARM920T 的 MMU 把虚拟地址空间和物理地址空间分成 1KB 大小的地 址空间块,即每个页面大小为 1KB。由于是 1KB 大小的页面,所以需要两级页表。一级页表 中有 4096 个条目。每个条目映射二级页表的空间,二级页表中有 1024 个条目,每个二级页 表条目可以映射 1KB 大小的地址空间,极小页映射下一级页表条目和二级条目的格式如图 (P-3.9.4.12)所示。 图(P-3.9.4.12) 图(P-3.9.4.12)中的极小页的一级页表条目中的各位段如表(T-3.9.4.6)所示。 条目位段 相关说明 位[1:0] 页表条目类型标识,对于极小页是二进制 11。 位[3:2] 应该为 0。 位[4:4] 为 1,为 0 都行,但为了向后兼容应该为 1 。 位[8:5] 表示这个条目映射的内存空间,属于哪个域,参见 c3 寄存器。 位[11:9] 应该为 0。 位[31:12] 指向二级页表的物理地址,极小页的二级页表地址是 4KB 对齐的,所以地址 的低 12 位始终为 0,正因为如此这个条目的低 12 位才可以用于访问属性控 制等。 表(T-3.9.4.6) 图(P-3.9.4.12)中的极小页的二级页表条目中的各位段如表(T-3.9.4.7)所示。 条目位段 相关说明 位[1:0] 页表条目类型标识,对于小页必须是二进制 11。 位[3:2] 表示这个条目映射的内存空间,是否为回写缓存、写通缓存、非缓存、缓冲, 或者无缓存并且无缓冲。 位[5:4] AP:控制这个页的访问权限。极小页没有子页。 位[9:6] 应该为 0。 位[31:10] 表示这个条目映射的内存空间的物理地址,由于极小页是 1KB 大小,所以地 址的低 10 位始终为 0,正因为如此这个条目的低 10 位才可以用于访问属性 控制等。 表(T-3.9.4.7) 下面我们来看看极小页映射下 MMU 是如何用内存中的两级页表实现虚拟地址到物理地址 的转换的。如图(P-3.9.4.13)所示。 图(P-3.9.4.13) 图(P-3.9.4.13)中,MMU 的执行步骤如下: 1. MMU 拿到虚拟地址。 2. MMU 将 CP15 C2 寄存器中的高位[31:14]和虚拟地址中的高位[31:20]并且外加两个 为 0 的二进制位,一起组成一个 32 位的地址,这个地址就是指向一级页表条目的 地址。 3. 用第 2 步中合成的地址读取一个一级页表条目。 4. 用一级页表条目中的高位[31:12]与虚拟地址的低位[19:10]并且外加两个为 0 的二进 制位,一起组成一个 32 位的地址,这个地址就是指向二级页表条目的地址。 5. 用第 4 步中合成的地址读取一个二级页表条目。 6. 用二级页表条目中的高位[31:10]与虚拟地址的低位[9:0]一起组成 32 位的物理地址。 7. 最后用第 6 步中组成 32 位的物理地址去访问内存。 我们来看个例子:如图(P-3.9.4.14)所示。 图(P-3.9.4.14) 这个例子演示了,从虚拟地址:0x400800 到物理地址:0xFFFFFC00 的映射过程。这个过程 如下: 1. MMU 拿到虚拟地址:0x400800。 2. MMU 将 CP15 C2 寄存器中的高位[31:14]:0x1 和虚拟地址的高位[31:20]:0x4 并且 外加两个为 0 的二进制位,一起组成一个 32 位的地址:0x4010,这个地址就是指 向一级页表条目的地址。 3. 用第 2 步中合成的地址读取一个一级页表条目。 4. 用一级页表条目中的高位[31:12]:0x8 与虚拟地址的低位[19:10]:0x2 并且外加两个 为 0 的二进制位,一起组成一个 32 位的地址:0x8008,这个地址就是指向二级页 表条目的地址。 5. 用第 4 步中合成的地址读取一个二级页表条目。 6. 用二级页表条目中的高位[31:10]:0x3FFFFF 与虚拟地址的低位[9:0]:0 一起组成 32 位的物理地址:0xFFFFFC00。 7. 最后用第 6 步中组成 32 位的物理地址:0xFFFFFC00 去访问内存。 上面介绍的 MMU 的 4 种分页方式,在同一时刻都是使用的同一种分页方式。比如要么 全部使用大页,要么全部使用小页,或者全部使用段,其实 ARM920T 的 MMU,设计的非常 优秀,它不仅可以同时使用一种分页方式,还可以同时把 4 分页方式混合起来一起使用,如 图(P-3.9.4.15)所示。 图(P-3.9.4.15) 我们看到 MMU 正是用页表条目的最低两位来区分当前使用的是哪种分页方式。注意!混合 使用时要注意虚拟地址空间与你分页的大小以及 MMU 所要求的空间块基地址的对齐标准。 好了,ARM920T 处理器的 MMU 支持的 4 种分页方式就介绍完了,下面我们去看看 MMU 是如何控制物理页面的访问权限的。 3.9.5 MMU 页面访问权限的控制 ARM920T 处理器的 MMU 页面访问权限控制非常简单:它是用 CP15 的 c1 寄存器中的 S,R 位,联合页表条目的 AP 位,一起工作的。它是根据 AP、S、R 中的数据进行判断的。 如表(T-3.9.5.1)所示。 C1 S 位 C1 R 位 AP 位 处于特权级时的访问权限 处于用户级时的访问权限 0b0 0b0 0b00 没有访问权限,进行访问将 没有访问权限,进行访问将 导致 MMU 发出访存失败的 导致 MMU 发出访存失败的 信号。 信号。 0b1 0b0 0b00 只读,但不可写,写入将导 没有访问权限,进行访问将 致 MMU 发出访存失败的信 导致 MMU 发出访存失败的 号。 信号。 0b0 0b1 0b00 只读,但不可写,写入将导 只读,但不可写,写入将导 致 MMU 发出访存失败的信 致 MMU 发出访存失败的信 号。 号。 0b1 0b1 0b00 保留。 保留。 0bx 0bx 0b01 可读/写。 没有访问权限,进行访问将 导致 MMU 发出访存失败的 信号。 0bx 0bx 0b10 可读/写。 只读,但不可写,写入将导 致 MMU 发出访存失败的信 号。 0bx 0bx 0b11 可读/写。 可读/写。 表(T-3.9.5.1) 注意 0b 表示紧跟其后的是二进制数据。“x”表示未定义,可以是 0 也可以是 1。 正是因为 MMU 可以通过表(T-3.9.5.1)中所述,对访问储存器的地址进行检查,我们 才可以利用这种特性,对操作系统内核的内存空间或者各个进程的内存空间进行保护,以防 止其它程序的恶意访问。也正是因为这样,才使得构建现代健壮的操作系统成为可能。 3.9.6 MMU 的快表 TLB 从前面关于页表的章节,我们看到了一个虚拟地址到物理地址的转换过程,其实说白了, 这个过程就是查询页表的过程。 我们的页表是放在内存里的,查询一次页表 MMU 最少要访问一次内存,有时要访问多 次。可是人类今天的技术、成本,导致内存的速度和 CPU 的速度相比还是太慢。本来查询页 表就是为了访问内存的,可是查询页表就需要巨大的时间代价。 怎么解决上述问题呢,人们基于这样的观察:程序的运行大多数总是在访问当前地址的 附近的内存地址,程序中的循环代码更是体现了这一点。于是人们把当前页表的附近的页表 装进 MMU 内部的一个小的且高速的储存器中,这个寄存器就叫 TLB,也叫快表,它的速度 和 CPU 中的寄存器差不多,大小有几十 KB。并且把页表装载进 TLB 的过程是 MMU 自动完 成的,不需要人为编程操作。基于这种情况,在特定的一段时间内,MMU 总是可以从 TLB 中获取页表内容,这样地址的转换速度在那一段时间内就上去了,进而程序的执行速度也上 去了。 从上面的描述中,看到 MMU 中的 TLB 确实是个好东西,然而需要我们操作系统开发人 员时刻注意的问题出现了。那就是当我们修改内存中的页表内容以达到改变地址映射规则 时,问题来了,假如我们修改的这部分内容在修改之前,已经被 MMU 装进 TLB 中了,这时 TLB 中和内存中的内容就不一致了,从而不能达到改变地址映射规则的目的。这个问题非常 危险。所以我们要能通知 MMU,让它把 TLB 中的内容或者一部分内容变得无效,重新从内 存中装载最新的内容。ARM920T 的 MMU 当然提供了这个操作接口,我们只要操作 CP15 的 c8 寄存器就行了,操作细节,请参见前面的 c8 寄存器。 相反,有时为了达到系统的最高性能,把一些经常访问的数据或者经常运行的程序的地 址对应的页表的内容,锁定在 MMU 的 TLB 中,使得这些页表的内容长期存在于 MMU 的 TLB 中,这样 MMU 就能总是在 TLB 中找到这些页表的内容,从而最大限度的加快地址的转换速 度。ARM920T 的 MMU 当然也提供了这个操作接口,我们只要操作 CP15 的 c10 寄存器就行 了,操作细节,请参见前面的 c10 寄存器。 对于 MMU 的 TLB,只要明白它是什么、干什么的、要注意什么问题,以及能利用它的 什么特性,达到提高程序运行效能的目的就行了。 3.9.7 MMU 的编程接口 MMU 的编程接口很简单,不过就是操作 CP15 协处理器中的几个寄存器而已,其实硬 件提供给软件的接口就是一堆寄存器。先在此简单的提一下,等到我们操作系统初始化时, 要操作 MMU 时再用代码加于实现。在这里只是用文字大致描述一下编程 MMU 的过程。 大致过程和要操作的寄存器如下: 1. 选择一块开始地址以 16KB 对齐的物理内存,在其中初始化一级页表或者根据需要 可能还要初始化二级页表,填写好每个页表条目的内容,比如:访问权限、映射的 物理地址等…… 2. 把第 1 步中的一级页表的开始物理地址写入到 CP15 的 C2 寄存器中。根据需要可能 要操控 CP15 的 C3 寄存器、CP15 的 C13 寄存器。 3. 把 CP15 的 C1 寄存器的 M 位即第 0 位,置为 1,即打开了 ARM920T 的 MMU。 注意,上述操作有一定的顺序,不能在还没有页表时就打开了 MMU,如果这样,系统要么 崩溃,要么执行结果不可预知。 确实很简单,MMU 的编程接口就先说到这里吧,不是很清楚也没问题,等到写代码时, 就自然明白了。 到这里我们了解的内容大致如下: 1. MMU 是什么,没有它会有什么问题。 2. 接着我们看了 ARM920T 处理器中的 CP15 协处理器。并详细了解了它的一些寄存 器,还知道它就是用来控制 ARM920T 中的 MMU 和 CACHE 的。 3. 我们又看了 ARM920T 中 MMU 的地址转换机理以及 MMU 的页表。 4. 然后了解了 ARM920T 中 MMU 如何利用 CP15 的 C1 寄存器中的 S、R 位和页表条目 中的 AP 位,控制物理页面的访问权限。 5. 粗略的了解了一下 MMU 中的 TLB。 6. 最后,大致了解了如何开启 MMU,和开启 MMU 的前提条件。 对于 MMU 我们知道了这些就够了…… 3.10 CACHE CACHE 是一种比常见的内存(RAM)更快的一种储存器,但它一般情况下比内存小很多, 所以被称为高速缓冲储存器。下面将先概述一下 ARM920T 中的 CACHE,接着了解一下 CACHE 的原理,然后看一看 CACHE 的类型以及要注意的问题,最后了解一下 CACHE 的编程接口。 3.10.1 ARM920T 的 CACHE CACHE 即高速缓冲储存器,也是 ARM920T 储存系统中的重要组成部分,它用来缓存一 小部分内存中的内容。 在前面的介绍中,我们都假定系统中没有 CACHE 或者 CACHE 处于关闭状态。有些简单 的嵌入式系统中确实没有 CACHE。但我们用的 ARM920T 处理器中提供了 CACHE 组件。下面 来看看加入或者打开 CACHE 后,ARM920T 的储存系统是什么样的,如图(P-3.10.1.1)所示。 当然笔者画的图可能与实际芯片中的实现有差异。因为这里只需要知道 ARM920T 处理器的 储存系统是有哪些部件构成的即可。 图(P-3.10.1.1) ARM920T 处理器没有使用统一的 CACHE,它使用了指令 CACHE 和数据 CACHE 分开的形 势,图(P-3.10.1.1)中没有画出来,如果有需要请参见图(P-3.9.3.1)。下面先来看看 CACHE 的工作机理。 3.10.2 CACHE 的原理 我们知道程序和数据都是存放在内存中的,CPU 要执行程序就要访问内存。内存一般是 用 SDRAM 芯片做的,出于技术和成本问题无法让内存的速度和 CPU 的速度一样快,通常内 存的速度比 CPU 的速度慢很多。于是内存的速度成了制约系统性能的关键…… 人们观察到 CPU 访问内存并不是完全随机的。在某个时间段内,CPU 总是访问当前内 存地址的相邻内存地址,想象一下指令顺序执行和循环执行的情景,这种情景就是著名的程 序局部性原理,前面已经提到过了。 基于程序局部性原理,人们在 CPU 与内存之间加了一个小而快的储存器,它的速度和 CPU 的速度差不多,事实上它还是慢那么一点点,但是它比内存快的多。它就是高速缓冲储 存器即 CACHE。在 ARM920T 处理器中有 16KB 的指令 CACHE 和 16KB 的数据 CACHE。 CACHE 把整个内存分成大小相同的块,块的大小因不同 CACHE 芯片的实现而不同。因 此 CACHE 内部的地址就是由块号和块内偏移组成的。 逻辑上 CACHE 的工作机制,如图(P-3.10.2.1)所示。可能具体的实现有所差别。 图(P-3.10.2.1) CACHE 的运行如下: 1. CACHE 收到 CPU 访问内存的地址。 2. CACHE 的地址变换逻辑,把 CPU 访问内存的地址,分解成块号和块内偏移。 3. 用第 2 步中分解的块号去找 CACHE 内部的 CACHE 块。 4. 如果用第 2 步中的块号找到一个 CACHE 块,即表示命中,然后用第 2 步中分解的块 内偏移去索引该块中的数据,如果 CPU 是读内存操作的话,就把这个数据返回给 CPU。如果 CPU 是写内存操作,根据 CACHE 的类型不同 CACHE 的动作也不同。 5. 如果第 3 步中没有在 CACHE 内找到对应的 CACHE 块,即表示未命中。 6. 如果 CACHE 未命中,CACHE 首先查找 CACHE 内部有没有空闲块。 7. 如果第 6 步中 CACHE 找到一个空闲块,就在该块中装入 CPU 访问内存地址对应的 内存块,同时如果 CPU 是读内存操作就把这个地址对应的数据返回给 CPU。如果 CPU 是写内存操作,根据 CACHE 的类型不同 CACHE 的动作也不同,后面会介绍的。 8. 如果第 6 步中 CACHE 没有找到一个空闲块,就让 CACHE 块替换逻辑,找出 CACHE 中最值得替换出去的块。并且有相关的替换算法,只是这种算法是硬件实现的。如 果 CPU 是读内存操作,那么根据替换块的块号和状态,CACHE 会决定是否把这个块 回写到内存中,或者直接废除,最后在该替换出去的块中装入 CPU 访问内存地址对 应的内存块,同时把这个地址对应的数据返回给 CPU。如果 CPU 是写内存操作,根 据 CACHE 的类型不同 CACHE 的动作也不同。 由于前面介绍的程序局部性原理,CPU 在某一特定的时间段内会对 CACHE 保持很高的 命中次数。这样在那一段时间内 CPU 就可以直接从 CACHE 中获取指令或者数据,而 CACHE 的速度远远高于内存,所以这样就用了合理的成本,达到了很高的系统性能。 3.10.3 CACHE 的类型及要注意的问题 根据 CACHE 的工作机制,可以把 CACHE 分成很多类型,我们主要看看下面两种类型, 在 ARM920T 中知道这两种就够了,当然了解这两种类型是为了要说明后面要注意的问题, “要注意的问题”才是重要的: 1. 回写式 CACHE。 2. 写通式 CACHE。 1. 回写式 CACHE 这种类型的 CACHE,当 CPU 执行写数据操作时,只把该数据写入其数据地址对应的 CACHE 块中,不直接写入内存。仅当该 CACHE 块需要替换时,才把该 CACHE 块回写入内存 中。这种类型的 CACHE,每个 CACHE 块中都有个对应的修改位。只要该 CACHE 块中的任何 单元被修改,该位就为 1,否则该位为 0。当该 CACHE 块需要替换时,如果其修改位为 1, 那么就必须先将该 CACHE 块写入内存,如果其修改位为 0,就可以直接用新的内存块覆盖该 CACHE 块即可。这种类型的 CACHE 的稳定性不太高,因为它不能实时的保证内存中有 CACHE 中的数据的最新副本。该类型的 CACHE 和内存的通信量很少,因为 CACHE 在特定的时间段 内有很高的命中率,CPU 访问的是 CACHE,只有在发生 CACHE 块替换时才访问内存。这种 CACHE 硬件的实现较为简单。 2. 写通式 CACHE 这种类型的 CACHE,当 CPU 执行写数据操作时,必须同时把该数据写入其数据地址对 应的 CACHE 块和内存中。这种类型的 CACHE,每个 CACHE 块中不需要有对应的修改位。当 该 CACHE 块需要替换时,也不必把该 CACHE 块写入内存中。新的内存块可以直接覆盖该 CACHE 块。因为这种类型的 CACHE,它能始终保持 CACHE 中的数据和内存中的数据是一致 的。这种类型的 CACHE 的稳定性很高,因为它始终实时的保证内存中有 CACHE 中的数据的 最新副本。这同时也增加了 CACHE 和内存的通信量。这种 CACHE 硬件的实现也复杂很多。 采用 CACHE 后要注意什么问题呢?说到底就是数据的一致性问题。 如果硬件系统中有多核心的 CPU,每个 CPU 核心内部都有独立的 CACHE。这时如果内 存中有个全局数据 A 且 A=0。如果两个 CPU 核都对其执行加 1 操作。如果按照下面的执行 顺序,就会出现问题。而事实上如果不加控制我们无法对这种硬件的执行顺序进行估计,很 有可能就发生下面的执行顺序: 1. CPU0 读取内存中 A 的数据到 R0 寄存器,并且缓存 A 的数据到 CPU0 的 CACHE 中。 CPU0 的 R0 寄存器中为 0,CPU0 的 CACHE 中 A 数据对应的 CACHE 块中的单元为 0。 2. CPU0 对它的 R0 寄存器执行加 1 操作,并且把结果写入 CPU0 的 CACHE 中,假定这 个 CACHE 是回写式 CACHE,CPU0 的 CACHE 中 A 数据对应的 CACHE 块中的单元为 1。CPU0 接着执行下一条指令。 3. CPU1 读取内存中 A 的数据到 R1 寄存器,并且缓存 A 的数据到 CPU1 的 CACHE 中。 由于第 2 步中 CPU0 的 CACHE 中的 A 数据没有回写到内存中,内存中 A 数据没有改 变。所以 CPU1 的 R1 寄存器中为 0,CPU1 的 CACHE 中 A 数据对应的 CACHE 块中的 单元为 0。 4. CPU1 对它的 R1 寄存器执行加 1 操作,并且把结果写入 CPU1 的 CACHE 中,假定这 个 CACHE 是回写式 CACHE,CPU1 的 CACHE 中 A 数据对应的 CACHE 块中的单元为 1。CPU1 接着执行下一条指令。 5. 这时不管哪个 CPU 的 CACHE 中 A 数据对应的 CACHE 块发生了替换,内存中数据 A 为 1。 我们看到这个执行顺序下,明明对 A 数据(A=0)执行了两次加 1 动作,可是结果还是 为 1。如果这个 A 数据是个局部数据,或许这不是个严重的问题。可是这个 A 数据是个全局 数据,更有可能 A 数据是一个信号量数据结构中的信号值变量,你会发现这个问题会有多么 严重。对于开发操作系统的人员,这种问题太可怕了,要时刻注意避免类似问题的发生。 上面这个例子只是从逻辑上说明了 CACHE 的数据一致性问题是多么严重,然而 ARM920T 不是多核心的 CPU。现在来看看 ARM920T 系统中一个非常实际的问题。 s3c2440a 处理器提供了丰富的设备,例如 RTC,串口等。这些设备提供了一系列的寄存 器作为编程接口。要操作控制这些设备,就是读写这些设备的相关寄存器。但是要访问这些 寄存器就要知道这些寄存器的地址。ARM920T 没有提供用于专门访问设备寄存器的指令。 怎么办呢,硬件设计人员想了个办法,他们把这些设备的寄存器映射到储存器地址空间中。 访问特定的储存器单元就是访问某个设备的某个特定的寄存器。例如访问“0x57000040”这 个地址的储存单元就是访问 RTC 的控制寄存器,访问“0x50000004” 这个地址的储存单元 就是访问第一个串口设备的控制寄存器,等…… 在打开 CACHE 的情况下,如果读取 RTC 寄存器中的状态数据时,很有可能读取的不是 RTC 寄存器的最新数据而是读取缓存在 CACHE 中的数据。这会使我们获取的数据不准确,有 时会产生致命的错误。如果写入数据到 RTC 寄存器中对 RTC 设备进行控制。这个数据会首 先写入 CACHE 中(如果是回写式 CACHE)。我们满心以为 RTC 设备已经得到了控制,然而我 们错了,这时控制数据也许还在 CACHE 中,还没有到 RTC 设备控制寄存器中去呢。当然其 它设备也是一样。这种问题非常危险,而且这种类型的错误根本就不是程序代码本身的问题 导致的,查起来也不是一般的困难。 解决上述问题的最好方法是,对映射设备寄存器的储存器地址空间,不执行 CACHE 缓 存。在 MMU 的章节中,了解到每个页表条目中有 C、B 两个位。这两个位就是控制这个页 表条目映射的地址空间,是否被 CACHE 部件(C 位)或者写缓冲部件(B 位,写缓冲也是一 种小型的 CACHE,但是不详细的研究它)缓存。只要把设备寄存器所在的页面的页表条目中 的 C、B 位设置为 0,就解决上述问题了。 CACHE 技术带来了系统性能的提升,同时也带来了不小的麻烦,这就像世间其它事物一 样,有优点就有不足…… 3.10.4 ARM920T CACHE 的编程接口 CACHE 实现的功能大部分对程序员是透明的,因为它的功能逻辑几乎全部是用硬件实现 的。所以程序员只需要少量的控制操作即可。 ARM920T 处理器 CACHE 的操作很简单,综合起来无非就是以下三点。 1. 打开或者关闭 CACHE。 2. 清空 CACHE。 3. 锁定 CACHE。 1. 打开或者关闭 CACHE ARM920T 处理器使用了独立的数据 CACHE 和指令 CACHE,它们可以独立关闭或者开启。 把 CP15 的 C2 寄存器的 C 位设置为 1 就开启了 ARM920T 的数据 CACHE,否则即为关闭 ARM920T 的数据 CACHE;把 CP15 的 C2 寄存器的 I 位设置为 1 就开启了 ARM920T 的指令 CACHE,否则即为关闭 ARM920T 的指令 CACHE。打开 CACHE 后,还可以通过设置 CP15 的 C2 寄存器的 RR 位为 1,从而改变 CACHE 替换 CACHE 块的算法。具体操作请参阅 AM920T CP15 协处理器相关的章节。 2. 清空 CACHE 清空 CACHE,实际上是强制 CACHE 把其中已修改的数据块回写到内存中去。基于前面 所述 CACHE 的问题,有时修改了重要数据之后必须要手动的清空 CACHE,使内存中的数据 得到更新。ARM920T 处理器中可以分别清空指令 CACHE 或者数据 CACHE。可以向 CP15 的 C7 寄存器中写入虚拟地址或者组号(许多块组成一个组),以清空特定的 CACHE 块,也可以 清空整个 CACHE。操作时相关的参数和数据请参阅 AM920T CP15 协处理器相关的章节。 3. 锁定 CACHE 锁定 CACHE 就是让 CACHE 在替换 CACHE 块时,把被锁定的 CACHE 块不替换出去。例如 把一些运行密度非常高的指令或者数据所在的内存块,锁定到 CACHE 块中,这样就可以大 大提升系统的性能。ARM920T 可以分别锁定指令或者数据 CACHE 中的块。可以向 CP15 的 C9 寄存器中写入组内序号,以锁定特定的 CACHE 块,操作时相关的参数和数据请参阅 AM920T CP15 协处理器相关的章节。 通过上述 CACHE 的编程接口,合理利用 CACHE 的特性,既可以避免 CACHE 带来的问题 又可以使系统性能提高。 3.11 小结 本章就要结束了,这意味着我们又走了一大步,同时也考验了我们的耐性。现在终于可 以休息了,顺便回顾一下,这章我们了解到了什么: 1. ARM920T 处理器,它是由一个名叫 ARM 的公司设计的,广泛应用于嵌入式领域。 2. 在 ARM920T 储存体系中,了解了 ARM920T 处理器的地址空间、储存器格式以及储 存地址对齐方式。 3. ARM920T 处理器的运行状态,包括 ARM 状态和 THUMB 状态。 4. ARM920T 处理器的工作模式,它共有 7 种工作模式,并且这 7 种工作模式可以分为 特权模式和用户模式。 5. ARM920T 处理器共有 37 个寄存器。这些寄存器按照不同的工作模式分成了几组, 并且有些寄存器在不同工作模式下有不同的物理寄存器。 6. ARM920T 处理器的中断和异常,这些中断和异常有固定的向量地址。 7. ARM920T 处理器的指令集,一共了解了 35 条常用的指令及使用方式,这些指令可 以分类为:分支跳转指令、数据处理指令、装载和储存指令、程序状态寄存器操作 指令、协处理器操作指令、异常中断产生指令。 8. ARM920T 处理器内部的 MMU,了解了为什么要有 MMU、用于控制 MMU 的 CP15 协处理器、MMU 实施地址转换的页表、MMU 如何控制内存的访问权限、MMU 用 于加快地址转换速度的 TLB 以及如何开启 MMU。 9. ARM920T 处理器内部的 CACHE,了解了 CACHE 的工作原理,利用它可以大大提高 系统的性能,同时也因为它的特性给程序的正确运行带来了很严重的问题,但是可 以通过 CACHE 的一些编程接口对其进行控制。 或许这内容不少,但这是为了我们写操作系统内核的第一行代码以及操作系统内核今后 在这个平台上稳定高效的运行而准备的,所以要勇敢的了解这个平台上硬件的一些细节。有 了这些硬件基础之后,后面就可以一边写代码一边关注操作系统要用到的某些具体的硬件设 备…… 4. 操作系统内核的设计与构建 前面了解了一大堆硬件平台相关的东西,说实话那些东西很枯燥、很无趣,但是为了更 加轻松的写操作系统内核,又不得不了解那些东西。 是不是走到这里就可以开始写操作系统内核了,当然不是的。看了这句话,你的兴趣可 能掉了一大半,别急,相信花点时间了解下面这些东西一定是值得的。 操作系统内核是一个十分巨大的软件工程项目,了解如何设计、管理、编译、测试这个 项目,将十分重要。 这里将从操作系统内核需要完成什么功能开始,分析各种操作系统内核的架构,然后在 这个架构上组建各种功能模块,到选择适合我们自己操作系统内核的设计,最后,研究了如 何配置系统开发环境、各种工具的使用,以至于最终能高度自动化编译、测试我们的操作系 统内核。 4.1 操作系统内核的设计 操作系统内核的设计包括,安全、性能、稳定、可移植、可扩展、可维护、BUG 的排查、 各功能组件的测试、开发人员的协作等……要详细讨论这些问题,可能一整本书都不够,所 以得简单点,因为我们不是操作系统内核架构师,我们只是想做出自己的操作系统内核,过 多的理论概念,我们并不喜欢。 首先从内核需要完成的功能开始,了解什么功能应该由内核实现,哪些功能又不应该由 内核实现。接着研究两种成熟的内核架构,主要研究这两种内核架构的优缺点。然后讨论软 件分层带来的好处。最后看看我们自己选择设计的架构。 4.1.1 内核要完成的功能 一个操作系统内核必需要有什么功能,没有一个统一的标准来定义,有的系统内核功能 很多,比如 windows 系统的 NT 内核,有的系统内核功能非常少,比如 minix 系统的内核。 第一章节,通过分析一个小程序执行的过程细节,大概了解了一个现代意义的操作系统 内核需要些什么功能,如下: 1. 内存管理,因为程序运行要占用内存。 2. 进程管理,表示一个运行中的程序。 3. 设备管理,系统里有多个设备及它们的状态。 4. 文件管理,管理存放用户的数据。 以上是从全局来说,需要这 4 大块功能,然而这也只是在第一章节中,观察现有操作系 统上的一个小程序的执行过程大致得出的一个结论。但这不是标准,没有人规定操作系统内 核一定要实现这 4 大块功能。 一个软件的功能越多,复杂程度越大,出现问题的可能性越高,并且查找问题就越困难。 内核是操作系统之灵魂,它一旦有什么问题,整个操作系统必将崩溃。由此可以想到越简单 的内核也就越可靠。所以必须将最少的、最重要的、非内核不可的功能交由内核实现,除此 之外的,都不应该由内核来实现,虽然有许多模块具有内核一样的特权,然而也应该把它们 和内核分开。例如,许多驱动程序或者一些功能模块,虽然它们有内核一样权力,可以操作 硬件或者完成特殊的功能,但是它们是以独立的模块存在的。 现在来思考一下哪些功能是需要由内核来实现的,如下: 1. 时间管理,时间是自然界最重要的量,对计算机系统也是如此,内核理当实现它。 2. 中断管理,中断是内核得以正常工作的重要机制,所以无论是软中断还是硬中断, 都应该由内核管理和控制。 3. 内存管理,内存是所有程序运行的基础,包括内核本身,能否高效分配、释放内存, 是系统性能的关键。当然内存管理又分为物理内存管理和虚拟内存管理。 4. 进程管理,操作系统都是多任务的,必须要管理多个应用程序,把每个应用程序分 配到一个 CPU 上运行。即把每个应用程序抽象成一个进程,并对其行为进行管理和 控制。 5. 设备管理,内核必须要管理计算机内的设备,并且要提供一个统一的设备模型。方 便加载设备驱动程序,同时向应用程序提供访问操作设备的接口。当然内核并不实 现设备驱动程序本身。 6. 文件系统,主要管理、组织用户的数据,但是由于设备管理的存在,文件系统又可 以作为一个虚拟设备加载到系统中运行,并且还可以支持各种不同的文件系统。因 此文件系统可以不和内核一起实现。 以上这些功能就是内核要实现的,但是这些功能都还没有细化。例如进程管理它包括: 怎样表示一个进程,进程的创建、进程的调度、进程的退出等。 先暂且不要考虑如何细化这些功能,只要在心中知道一个内核需要这么多功能,好了, 知道一个操作系统内核大致需要这么多功能,接下来就去研究一下,用什么结构或者架构来 组织这么多功能。 4.1.2 内核的架构 操作系统经过 40 多年的发展,人们开发了很多不同的系统架构,它们有的不够成熟, 有的非常成熟并且广泛应用于商业操作系统之上。例如,宏内核架构、微内核架构、混合内 核架构、外内核架构等…… 这里主要看一看两种架构:宏内核架构和微内核架构,混合内核架构也只是宏内核架构 的一个简单的变形。其实宏内核架构和微内核架构,业界一直在争论不休,那么它们到底各 有什么优势呢…… 先来看看宏内核架构,操作系统内核主要是给应用程序提供服务的,如内存管理、IO 操 作等。从前面知道,现代操作系统至少要提供:进程管理、内存管理、设备管理、文件管理 等服务功能,宏内核就是把进程管理代码、内存管理代码、设备管理代码、文件管理代码、 各种设备驱动程序代码以及其它功能模块的代码,这所有的代码经过编译,最后链接在一起, 形成一个大的可执行程序。这个大程序里有实现支持这些功能的所有代码,向用户应用软件 提供一些接口,这些接口就是常说的系统 API 函数。这个大程序运行在处理器的特权模式下, 这个模式通常被称为内核模式。如图(P-4.1.2.1)所示,这就是宏内核架构图。 图(P-4.1.2.1) 再来看一个例子:宏内核提供内存分配功能的服务过程,如下: 1. 应用程序调用内存分配的 API 函数。 2. 处理器切换到特权模式,开始运行内核代码。 3. 内核里的内存管理代码按照特定的算法,分配一块内存。 4. 把分配的内存块的首地址,返回给内存分配的 API 函数。 5. 内存分配的 API 函数返回,处理器开始运行用户模式下的应用程序,应用程序就得 到了一块内存的首地址并可以使用这块内存了。 上面这个过程和一个实际的操作系统中的运行过程,可能有差异,但大同小异,当然系统 API 和应用程序之间可能还有库函数,也可能只是分配了一个虚拟地址空间,但是我们关注的只 是这个过程。 从图(P-4.1.2.1)和以上的服务过程不难发现,宏内核的代码耦合度非常高,甚至内核 内部的功能组件的代码可以互相调用,例如文件系统的代码可以直接调用内存管理的代码。 但是,由于宏内核代码的高耦合度,若其中有一行代码有问题的话,那么整个内核将崩溃。 经长期证明只要开发者对宏内核进行精密的实现与测试,宏内核是非常稳定高效的。同时还 不难发现,如果要给这个内核增加一个功能,就不得不把整个内核代码全部编译、链接,然 后重启计算机启用新版本的内核。 宏内核的经典代表作,就是早期的 UNIX 和 LINUX 系统,注意是早期的,现在的 UNIX 和 LINUX 系统包括 WINDOWS NT 都是混合内核,属于宏内核的变形。 微内核架构正好与宏内核架构相反,它提倡内核功能尽可能的少:仅仅只有进程调度、 处理中断、内存空间映射、进程间通信等功能。这样的内核是不能完成什么实际功能的,开 发者们把实际的进程管理、内存管理、设备管理、文件管理等服务功能,做成一个个服务进 程,和用户应用进程一样,只是它们很特殊,它们是为了专门完成传统宏内核提供的那些功 能的。微内核定义了一种良好的进程间通信的机制——消息。应用程序要请求相关服务,就 向微内核发送一条与此服务对应的消息,微内核再把这条消息转发给相关的服务进程,服务 进程完成相关的服务。如图(P-4.1.2.2)所示。服务进程的编程模型就是循环处理来至其它 进程的消息,完成相关的服务功能。 图(P-4.1.2.2) 来看看微内核提供内存分配功能的服务过程,如下: 1. 应用程序发送内存分配的消息,这个发送消息的函数是微内核提供的,相当于系统 API,微内核的 API 相当少,极端情况下仅需要两个,一个接受消息的 API 和一个发 送消息的 API。 2. 处理器切换到特权模式,开始运行内核代码。 3. 微内核代码让当前进程停止运行并根据消息包中的数据,确定发送给谁,分配内存 的消息当然是发送给内存管理服务进程。 4. 内存管理服务进程收到消息,分配一块内存。 5. 内存管理服务进程,也通过消息的形式返回分配内存块的地址给内核。继续等待下 一条消息。 6. 微内核把包含内存块地址的消息返回给发送内存分配消息的应用程序。 7. 处理器开始运行用户模式下的应用程序,应用程序就得到了一块内存的首地址并可 以使用这块内存了。 微内核的架构实现虽然不同,但是大致过程和上面一样。同样是分配内存,在微内核下拐了 几个弯,一来一去的消息带来了非常大的开销,当然各个服务进程的切换开销也不小。这样 系统性能就大打折扣。但是微内核有很多优点,首先,系统结构相当清晰利于协作开发。其 次系统有良好的移植性,微内核代码量非常少,就算重写整个内核也不是难事。最后微内核 有相当好的伸缩性、扩展性,因为那些系统功能只是一个进程,可以随时拿掉一个服务进程 以减少系统功能,或者增加几个服务进程以增强系统功能。 微内核的代表作有 MACH、MINIX、L4 系统,这些系统都是微内核,但是它们不是商业 级的系统,商业级的系统不采用微内核的原因主要还是因为性能…… 好了,粗略的了解了宏内核和微内核两大系统内核架构的优、缺点,以后设计我们自己 的系统内核时,心里也就有了底了,到时就可以扬长避短了…… 4.1.3 分离硬件的相关性 经常听说,windows 内核有什么 hal 层、linux 内核有什么 arch 层。这些 xx 层就是 windows 和 linux 内核设计者,给他们的系统内核分的第一个层。 从前面章节中就不难知道,今天如此庞杂的计算机,不过也是一层一层的构建起来的, 从硬件层到操作系统层再到应用软件层,分层的主要目的和好处在于屏蔽底层细节,使上层 开发更加简单。计算机领域的一个基本方法是增加一个抽象层,从而使得抽象层的上下两层 独立的发展,所以在内核内部再分若干层也不足为怪。 来看两个例子:进程调度、内存分配,通过这两个例子,来看看分层对系统内核的设计 与开发有什么影响。 一般操作系统理论书籍花大量篇幅去写进程相关的概念,说到底进程是操作系统开发者 为了实现多任务而提出的,并让每个进程在 CPU 上运行一小段时间以实现多任务同时运行 的假象。当然这种假象十分凑效。要实现这种假象,就要实现如下两种机制: 1. 进程调度,它的目的是要从众多进程中选择一个将要运行的进程,当然有各种选择 的算法,例如,轮转算法、优先级算法等…… 2. 进程切换,它的目的是停止当前进程,运行新的进程,主要动作是保存当前进程的 机器上下文,装载新进程的机器上下文。 不难发现,不管是在 ARM 硬件平台上还是在 x86 硬件平台上,选择一个进程的算法和代码 是不容易发生改变的,需要改变的代码是进程切换的相关代码,因为不同的硬件平台的机器 上下文是不同的。这时最好是将进程切换的代码放在一个独立的层中实现,比如硬件平台相 关层,当操作系统要运行在不同的硬件平台上时,就只是需要修改硬件平台相关层中的相关 代码,这样操作系统的移植性就大大增强了。 内存分配也是一样,主要动作如下: 1. 分配一块空闲内存,用相关的内存管理算法,例如伙伴系统算法、工作集算法分配 一块空闲的物理内存。 2. 进行地址映射,操作硬件平台上的 MMU 和页表,以映射新分配的内存,返回其对 应的虚拟地址。 同样,实现伙伴系统算法、工作集算法的代码在不同的硬件平台上是不会发生改变的,当然 不同的硬件平台,机器代码是不同的,但是编译器会解决这个问题。要想操作系统运行在不 同的硬件平台上,会发生改变的代码是地址映射的相关代码,因为不同的硬件平台上的 MMU 和页表都不尽相同。如果把这段代码放在独立的层中,系统移植起来就会容易许多。 如果把所有硬件平台相关的代码,都抽离出来,放在一个独立硬件相关层中实现并且定 义好相关的调用接口。再在这个层之上开发内核的其它功能代码,就会方便的多,结构也会 清晰很多,操作系统的移植性也会大大增强,移植到不同的硬件平台就构造开发一个与之对 应的硬件相关层。这就是分离硬件相关性的好处…… 4.1.4 我们的选择 从前面内容中,知道了内核必须要完成的功能,宏内核架构和微内核架构各自的优、缺 点,最后分析了分离硬件相关层的重要性,其实说了这么多,就是为了设计我们自己的操作 系统内核,虽然前面的内容对操作系统设计这个领域是远远不够的,但是对于我们自己从零 开始的操作系统内核已经够了。 首先大致将我们的操作系统内核分为三个大层,如下: 1. 内核接口层。 2. 内核功能层。 3. 内核硬件层。 内核接口层,定义了一系列接口,主要有两点内容,如下: 1. 定义了一套 UNIX 接口的子集,我们出于学习和研究的目的,用 UNIX 接口的子集, 一则是接口少,只有几个,并且这几个接口又能大致定义出操作系统的功能,二则 用 UNIX 接口,也有一定的参考价值。 2. 这套接口的代码,就是检查其参数是否合法,如果参数有问题就返回相关的错误, 接着调用下层完成功能的核心代码。 内核功能层,主要完成各种实际功能,这些功能按照其类别可分为各种模块,当然这些 功能模块最终会用具体的算法、数据结构、代码去实现它,内核功能层的模块如下: 1. 进程管理,主要是实现进程的创建、销毁、调度进程,这当然要设计几套数据结构 用于表示进程和组织进程,还要实现一个简单的进程调度算法…… 2. 内存管理,在内核功能层中只有内存池管理,分两种内存池:页面内存池和任意大 小的内存池,可能不明白什么是内存池,后面研究它的时候会有介绍。 3. 中断管理,这个在内核功能层中非常简单:就是把一个中断回调函数安插到相关的 数据结构中,一旦发生相关的中断就会调用这个函数。 4. 设备管理,这个是最难的 ,需要用一系列的数据结构表示驱动程序模块、驱动程序 本身、驱动程序创建的设备,最后把它们组织在一起,还要实现创建设备、销毁设 备、访问设备的代码,这些代码最终会调用设备驱动程序,达到操作设备的目的。 内核硬件层,主要包括一个具体硬件平台相关的代码,如下: 1. 初始化,初始化代码是内核被加载到内存中最先运行的代码,例如初始化少量的设 备、CPU、内存、中断的控制、内核用于管理的数据结构等…… 2. CPU 控制,提供 CPU 模式设定、开、关中断、读写 CPU 特定寄存器等功能的代码。 3. 中断处理,保存中断时机器的上下文,调用中断回调函数,操作中断控制器等…… 4. 物理内存管理,提供分配、释放大块内存,内存空间映射,操作 MMU、CACHE 等…… 5. 平台其它相关的功能,有些硬件平台上有些特殊的功能,需要额外的处理一下。 如果上述文字让你看的头晕,我们来画幅图,可能就会好很多,如图(P-4.1.4.1)所示, 当然没有画出用户空间的应用进程,API 接口以下的为内核空间,这才是设计、开发内核的 重点。 图(P-4.1.4.1) 从上述文字和图,可以发现,我们的操作系统内核没有任何设备驱动程序,甚至没有文 件系统和网络组件,内核所实现的功能很少,这吸取了微内核的优势,内核小出问题的可能 性就少,扩展性就越强,同时把文件系统、网络组件、其它功能组件作为虚拟设备交由设备 管理,比如需要文件系统时就写一个文件系统虚拟设备的驱动,完成文件系统的功能,需要 网络时就开发一个网络虚拟设备的驱动,完成网络功能。这些驱动一旦被装载就是内核的一 部分了,并不是像微内核一样作为服务进程运行。这又吸取了宏内核的优势,代码高度偶合, 性能强劲。这样的内核架构既不是宏内核架构也不是微内核架构,而是这两种架构综合的结 果,可以说是混合内核架构,也可以说这是我们自己的内核架构…… 我们设计了内核,确定了内核的功能并且设计了一种内核架构用来组织这些功能,这离 完成我们自己的操作系统内核又进了一步。 4.2 开发环境及相关工具 工欲善其事必先利其器,开发操作系统内核,无非就是编写各个功能模块的代码、接着 用编译器编译这些功能模块,然后用链接器把这些功能模块链接成可执行文件即内核,最后 在硬件平台上安装运行我们的操作系统内核。这和开发普通的应用软件并无本质上的差别。 从上述开发过程得知,我们最起码需要一个文本编辑器来编写代码,然后需要编译器等 工具把代码转换成可执行文件,然而这些工具不过是某一个成熟操作系统下的应用软件而 已,所以还需要一个可以运行这些工具的操作系统环境。幸运的是我们并不是这个世界上第 一个开发操作系统和这些工具的人,否则麻烦和困难要大几个数量级。这个世界上已经有 linux、windows 并且它们上面已经有了这些工具,我们只需要拿来用就好了。 本节从 linux 环境开始详细介绍其上开发操作系统内核可能用到的一些工具,并且本书 始终用的是 linux 环境,并不是说这些工作在 windows 下无法完成,也不是说 linux 一定比 windows 优秀,如果硬是要询问原因的话,笔者只能说是喜好而已,如果你是一个 windows 狂热者,那么请见谅…… 4.2.1 Linux 环境 linux 是一款开源的类 unix 的操作系统内核,1991 年由 linus benedict torvalds 发起的, 由于他的理念是:开放、自由、平等,迅速引来了世界各地的内核黑客加入到 linux 内核的 开发中。这使得 linux 快速成为一个,安全、性能、稳定、功能集一身的成熟的操作系统内 核,并且能运行在不同的硬件体系下,从嵌入式到 PC 再到服务器、超级计算机,linux 内核 都可以在上面流畅的运行。关于 linux 内核就到这里,如果读者想了解更多关于 linux 的信 息,可自行谷歌…… 可是 linux 只是一个操作系统内核,它还需要配备一些工具软件、用户应用程序、图形 系统等相关组件……才能组成一个完整的操作系统。如果你有能力,完全可以下载 linux 内 核代码并选择你自己喜欢应用软件、相关组件,构建一个属于你自己唯一的 linux 操作系统。 当然世界上有很多公司已经帮我们做了这个工作,叫作 linux 发行版,目前世界上已经有上 百种 linux 发行版。其中有非常著名的 red hat linux、fedora、debian 等……由 debian 又派生 出了 ubuntu,这个系统应用的非常广泛。由 ubuntu 又派生出了 mint、deepin 等……由此可 见 linux 的世界非常繁荣。这也是 linus benedict torvalds 始终坚持开放、自由、平等的理念 所造就的。 笔者所使用的 linux 是国内的 deepin,这个发行版出现没有几年,然而已经位居世界 linux 发行版的前列。这个发行版有一系列的自主原创应用软件,例如:深度软件中心、深度音乐、 深度影音、深度截图、深度终端、深度翻译等……最重要的是他们自主研发了一个非常漂亮、 非常完善、交互体验非常棒的桌面环境,如图(P-4.2.1.1)所示。 图(P-4.2.1.1) 如果你的计算机上没有 linux 发行版,而且又想知道怎么安装笔者的这款 linux 发行版, 请参照笔者的博客(http://blog.chinaunix.net/uid-28032128-id-4017703.html)。由于限于篇幅, 本书不会介绍如何安装 linux,如果你不会在物理 PC 上或者在现有的虚拟机中安装 ubuntu 或者 deepinlinux 的话,请自行谷歌或者参阅其它资料。因为你的计算机上必须要有 linux 操 作系统,最好是 ubuntu 或者 deepinlinux,才能运行后面那些开发我们自己操作系统内核的 工具。并且笔者强烈建议使用 deepinlinux,因为本书后面的描述全部是基于 deepinlinux 的。 linux 操作系统以前是以向操作系统输入命令为主要交互方式的操作系统,要想完成什 么功能,就要向系统输入功能对应的命令。即使是今天 linux 系统有了非常漂亮的图形操作 环境,但有些功能还是依赖于有关命令行程序,可是在图形环境下怎么输入命令运行程序呢, linux 操作系统的图形环境都会提供一个名叫“终端”的软件,相当于 windows 的命令提示 符窗口。当然 ubuntu 下就叫“终端”,deepinlinux 下叫“深度终端”。打开深度终端,如图 (P-4.2.1.2)所示。 如图(P-4.2.1.2) 下面通过一个例子,来熟悉一下 linux 环境。首先在 deepinlinux 上安装两个软件,这两 个软件也是后面必须要用到的软件。 首先来安装第一个软件:minicom。minicom 是一个串口通信工具,就像 windows 下的 超级终端。可用来与串口设备通信,到时我们的 mini2440 开发板就是用串口和 PC 机通信 的,通过它就可以观察到开发板的运行状态了。如何安装 minicom 呢,步骤如下: 1. 在 deepinlinux 下打开“深度终端”。 2. 在“深度终端”中输入:“sudo apt-get install minicom”后回车即可。如图(P-4.2.1.3) 所示。 图(P-4.2.1.3) 再来安装一个库:libusb-dev。这个库是一个操作 USB 设备的库,后面就是用它在 PC 机 上操作开发板,把开发板作为一个 USB 设备,下载我们的操作系统内核到开发板上运行,以 便测试我们的操作系统内核。安装 libusb-dev 的步骤如下: 1. 在 deepinlinux 下打开“深度终端”。 2. 在“深度终端”中输入:“sudo apt-get install libusb-dev”后回车即可。如图(P-4.2.1.4) 所示。 图(P-4.2.1.4) 上述安装软件的操作,要确保你的计算机处于联网状态,因为 apt-get 软件包管理器是通过 专门的软件源服务器,下载对应的软件包并进行安装的。 到此为止,如果你觉得意犹未尽或者觉得这一小节写得不伦不类,那么请参阅其它相关 专著,本书不是介绍 linux 操作系统的专著,但是上述两个软件必须要安装。如果你真得完 全不会使用 linux 操作系统,笔者只能感到有点小遗憾,不过也不用害怕,在介绍后面的内 容时,也会时不时的写到怎么使用 linux 操作系统…… 4.2.2 文本编辑器 首先如果读者是一个十分了解 linux 系统的人或者只是对我们操作系统内核的实现感兴 趣,大可略过这一小节。 操作系统内核最终是由一行行代码构成的,一行行代码构成一个个以“.c”(C 程序文件) 或者“.s”(汇编程序文件)为后缀名的文件。这两种文件就是普通的文本文件,也就是说任 何文本编辑器软件都可以用来写 C 程序或者汇编程序代码,现在就来看看 linux 操作系统下 应该选择什么样的文本编辑器。 linux 操作系统下有很多针对程序员写代码的利器,例如:emacs、vim、sublime text 2 等……这些“高大上”的工具对于绝大多数非专业人员来说,首先玩转它们本身难度是极大 的,所以得选择几个简单的,比如:gedit、qt creator。 下面就用 gedit、qt creator,这两软件分别来写我们最熟悉的 C 程序:“hello world!!”为 了让读者更加熟悉 linux 命令行环境,我们依然用终端输入相关命令来完成这个例子。 先用 gedit 试试,如下: 1. 首先新建一个文件夹“test”,在“深度终端”中输入:“mkdir test”回车即可。mkdir 命令就是 linux 下建立文件夹的命令。 2. 切换到 test 文件夹下,在“深度终端”中输入:“cd test”回车即可。cd 命令就是 linux 下切换文件夹的命令。 3. 建立一个“helloworld.c”文件,在“深度终端”中输入:“touch helloworld.c”回车 即可。touch 命令就是 linux 下建立一个空文件的命令。 4. 用“gedit ~/test/helloworld.c”命令打开这个文件,就会出现 gedit 编辑器的图形编 辑界面,输入代码,保存并关闭 gedit 软件,如图(P-4.2.2.2)所示。“~”符号,表 示当前用户的工作文件夹。“/”符号,表示文件夹分界符。 5. 用“gcc helloworld.c -o helloworld”命令编译这个 C 程序,在 test 文件夹下会出现一 个可执行文件:helloworld。 6. 用“./helloworld”命令运行这个程序,“.”符号,表示当前文件夹。 上述过程如图(P-4.2.2.1)所示。当然你也可以用图形环境完成上述 1 到 4 步的过程, 和在 windows 下的操作并没有多大差别。 图(P-4.2.2.1) 图(P-4.2.2.2) qt creator 是最近新出来的一款 IDE,可以用来编写 C/C++代码,当然也可以用来编写其 它的文本文件,笔者经常使用它编写内核代码。功能非常强大,例如:代码补全、自动保存、 代码函数跟踪等。当然如果你的 ubuntu 或者 deepinlinux 上没有 qt creator,可以在终端中输 入:“sudo apt-get install qt creator”命令安装 qt creator。还是上面那个例子,用 qt creator 打 开 helloworld.c,如图(P-4.2.2.3)所示。 图(P-4.2.2.3) 以上这些都不是重点,关键是读者能挑选一款自己用得顺手的文本编辑器编写代码即可。 至于什么文本编辑器并不重要,如果读者能配置好 emacs、vim,能亲自领略一下这两大神 器的强大,自然也是再好不过。 4.2.3 GCC GCC 即 GNU Compiler Collection GNU 编译器套装,是一套由 GNU 开发的编译器。它是 一套以 GPL 及 LGPL 许可证所发布的自由软件,是 linux 操作系统上的标准编译器,当然也 可以运行在 windows、unix、mac x os 上。GCC 不仅仅是 c 语言编译器,它还可以编译 c++、 fortran、pascal、objective-c、java、ada、go,以及其他语言。如果上面 GCC 跨语言、 跨操作系统的特性已经让你感觉很牛的话,下面的可能让你更加震憾,GCC 还能将上述这些 语言编译成不同 CPU 体系下的机器语言,比如 Alpha、ARM、H8/300、x86、x86-64、IA-64、 Motorola 68000、MIPS、PDP-11、PowerPC、SPARC、VAX 等处理器。因此 GCC 也常被认为是 跨平台编译器的事实标准。 前面用 linux 系统中自带的 GCC 编译了 helloworld 程序,这个 GCC 是 x86 体系下的 GCC 因为我们的 linux 系统安装在 PC 上,PC 机都是采用 x86 体系的 CPU,编译出的程序也只是 x86CPU 上能执行。而我们要开发的是 mini2440 平台上的操作系统内核,并且是在 PC 机上 开发,这引来了个问题,如何在 PC 上编译出 ARMCPU(因为 mini2440 平台上用的 ARM CPU) 上程序,前面已经知道 GCC 是可以编译出 ARM 体系下的程序的,而且在一种平台上(PC 平 台)编译另一个平台上(mini2440 平台)的程序的方式叫做:交叉编译。 交叉编译,首先要安装 ARM 的 GCC 交叉编译器,对于 mini2440 平台的 GCC 安装步骤如 下: 1. 打 开 deepinlinux 下 的 网 络 浏 览 器 chrome , 输 入 网 址 : “ http://arm9download.cncncn.com/mini2440/linux/arm-linux-gcc-4.4.320100728.tar.gz”下载 arm-linux-gcc-4.4.3 到相关目录,也可以从本书光盘中 得到这款 GCC。 2. 用 “tar -xf arm-linux-gcc-4.4.3-20100728.tar.gz”命 令解 压刚刚 下载的 tar.gz 文件包。 3. 把刚刚解压出的文件夹复制到“/opt/”目录下,可以用命令:“sudo cp -r ~/opt/ /opt/”实现,sudo 是因为 linux 是多用户系统,有的目录只有 root 用户(超级用 户)权限才能访问。 4. 用“sudo gedit /etc/profile”命令打开/etc 下的 profile 文件,把“export PATH="/opt/opt/FriendlyARM/toolschain/4.4.3/bin:$PATH"”增加到这个文件 的末尾,然后执行“source /etc/profile”命令,即把 arm-linux-gcc 加入到环 境变量中,以便在当前工作目录下可以使用 arm-linux-gcc 编译器。 5. 在深度终端中,输入:“arm-linux-gcc -v”测试一下,这时终端中会出现 GCC 版 本信息。 上述过程如图(P-4.2.3.1)所示。 图(P-4.2.3.1) 交叉编译器已经安装好了,下面来看看 arm-linux-gcc 的一些编译选项,arm-linux- gcc 的编译选项太多了,不必每个都弄得一清二楚,这里只是关注对于编译操作系统内核需 要关注的选项,如表(T-4.2.3.1)所示。 相关选项 说明 -c 编译生成可链接目标文件。 -I dir 指定头文件目录,dir 是头文件所在的目录。 -E 只做预处理而不编译。 -P 禁止 GCC 在预处理后的文件中插入任何额外的信息。 -Ox “x”可以是 0、1、2、3、s,表示 GCC 对代码的优化级别, 0 表示不优化,3 表示最优化,s 表示优化程序大小。 -std 表示 gcc 编译时所用程序语言的标准,-std=C99 表示采用 C 语言的 C99 标准。 -S 只将 C 语言程序编译生成相关机器的汇编代码。 -mtune 指定某一型号 cpu 的选项,比如我们的是-mtune=arm7tdmi。 -march march 指定的是当前 cpu 的架构,-march=armv4 表示生成 ARM 体系 4 的可执行文件。 -save-temps 保存所有 GCC 编译时生成的临时文件。 -fno-ident 禁止 GCC 在可执行文件末尾生成编译器信息。 -fno-builtin 除非利用前缀 __builtin_ 进行引用,否则不使用 GCC 所有 的内建函数,编译内核时最好不要用编译器的内建函数。GCC 为了提高程序性能,把很多常用函数都做好了。比如 strcopy。 -ffreestanding 按独立环境编译,该环境可以没有标准库,且对 main()函数 没有要求。最典型的例子就是操作系统内核。 -fno-stack-protector 禁用栈保护,有些应用利用栈溢出进行攻击,GCC 实现了一种 保护机制,但对内核没有用,而且会增加无用的保护栈的代 码。 -fomit-frame-pointer 对于不需要栈指针的函数就不在特定寄存器中保存栈指针, 因此可以忽略存储和检索地址的代码,同时对许多函数可多 提供一个额外的寄存器。 -Wunused-variable 用来警告存在一个定义了却未使用的局部变量或者非常量的 static 变量。 -Wno-unused-parameter 禁用警告一个函数的参数在函数的实现中并未被用到。 -Wno-sign-conversion 禁用警告符号和无符号整数之间的转换。 表(T-4.2.3.1) 了解了 GCC 的一些常用的选项,再来看看 GCC 编译一个 C 程序的过程,当然以上的选项 和下面的编译过程对 arm-linux-gcc 是等价的。如图(P-4.2.3.2)所示,可以发现 GCC 编 译器并不是一个程序,而是由多个程序:预处理程序、c 编译器、汇编器一起完成编译工作 的。 图(P-4.2.3.2) 安装了交叉编译器 GCC,知道了它的许多选项,又看了它全部的工作流程,后面就可以 熟练的应用 GCC 编译我们的操作系统内核了,并且可以利用 GCC 的一些特性,达到一些编译 之外的特殊功能。 4.2.4 LD LD 是一款由 GNU 开发的程序链接器。是以 GPL 及 LGPL 许可证所发布的自由软件,也 是 linux 操作系统上的标准链接器,其实它是 GCC 编译器套装中一个组件,通常是和 GCC 一 起工作的,当然也是跨操作系统平台、跨硬件平台的,前面安装了 GCC 交叉编译器,当然 LD 也就包含在其中了。 LD 其实就是把诸多 GCC 生成的可链接的目标文件,合并成一个大的可执行文件,主要 完成如下功能: 1. 把每个目标文件中各个相同的段,合并成各个相同的大段,一个目标文件中通常默 认情况下至少有三个段:“.text 段”即代码段,“.data 段”即已初始化的数据段, “.bss 段”即未初始化或者初始化为 0 的段。后面会了解到怎么让编译器生成我们 自定义的段。 2. 重定位程序中的地址,完成装载程序运行时的地址绑定,一般程序都必须从某一固 定的地址开始运行。 3. 解决不同目标文件之间符号引用的关系,例如 A 目标文件中的一个函数调用了 B 目 标文件中一个函数。 上述过程如图(P-4.2.4.1)所示。 图(P-4.2.4.1) 看了 LD 链接器的工作过程,现在来看一下 LD 链接器的一些选项,如表(T-4.2.4.1)所 示。 LD 选项 相关说明 -e 指定程序的入口点,一般是从程序中的“_start”标号开始的。 -T -T 后面跟着 LD 要打开的文件,一般是 LD 链接脚本文件。 -Map 指示 LD 输出可执行文件的 Map 文件,这个文件里有这个可执行文 件所有的段、符号以及符号所在的地址信息。 -Bstatic 禁止链接共享库,操作系统内核是不需要共享库的。 -nostdlib 禁止搜索标准库,内核也是不需要标准库的。 -s 删除所有可执行文件中的符号信息。 -Ttext 后面跟一地址值,表示 LD 输出的可执行文件 text 段运行时的地址。 -Tdata 后面跟一地址值,表示 LD 输出的可执行文件 data 段运行时的地址。 -Tbss 后面跟一地址值,表示 LD 输出的可执行文件 bss 段运行时的地址。 表(T-4.2.4.1) 关于 LD 的选项就介绍到这里,后面所用到的也就这么多,现在去研究一下 LD 的链接 脚本,这个链接脚本才是 LD 真正强大的地方。 什么是链接脚本呢,简单的说就是一个文本文件,它里面存放了一些信息,这些信息有 一定的规则,相当于一种编程语言,LD 读取这个文件里的信息,然后根据这些信息完成相 关的动作。 先来看一看一个非常简单的 LD 链接脚本: INPUT(file.o file1.o) OUTPUT(binfile.elf) OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") OUTPUT_ARCH(arm) ENTRY(_start) SECTIONS { . = 0x30008000; __begin_text = .; .text ALIGN(4) : { *(.text) } __end_text = .; __begin_data = .; .data ALIGN(4) :{ *(.data) } __end_data = .; __begin_bss = .; .bss ALIGN(4) : { *(.bss) } __end_bss = .; } 上面 LD 链接脚本中的内容,我们要关注的内容如下: 1. INPUT(),表示需要链接的目标文件。 2. OUTPUT(),表示链接后输出可执行文件的名称。名称随意。 3. OUTPUT_FORMAT(),表示输出可执行文件的格式,比如"elf32-littlearm"。 4. OUTPUT_ARCH(),表示输出可执行文件的机器体系,比如 ARM。 5. ENTRY(),表示程序中某一标号为可执行文件的入口点,即表示程序从这里开始运行。 例如:_start 标号。 6. SECTIONS,表示定义一个输出段,内部按照一定的规则组织合并所有可链接目标文 件内的所有的段,最后形成一个输出的大段。以一对“{}”结束。 7. .,表示当前地址计数器,LD 创建程序加载运行地址时,就是以这个基准的,最开始 应该给初始值,LD 就是以那个初始值为地址开始链接程序的。 8. __begin_text、__end_text、__begin_data、__end_data、__begin_bss、__end_bss,诸 如这些标号不是必须的,标号可由英文字符串组成,也可以没有这些标号,它们是 属于自定义的,如果定义了这些标号,就可以在程序代码中访问它们,它们的值表 现为地址,例如上面的__begin_text 的值为 0x30008000,关于访问它们的方法后面 会有介绍,上面的“=”相当于 C 语言里的赋值号,“. = 0x30008000;、__begin_text = .;”这相当于两个表达式,分别要以“;”结束。 9. “.text ALIGN(4) : { *(.text) }、.data ALIGN(4) :{ *(.data) }、.bss ALIGN(4) : { *(.bss) }”其 中的 ALIGN(4)都表示每个大段链接时的地址按 4 字节对齐,ALIGN(4)不是必须的。 ALIGN(4)前面的.text、.data、.bss,表示 LD 合成可执行文件时将会输出大段的段名, 大段名后面要以“:”开始,紧跟其后的是“{}”,“{}”里面是链接规则。例如“{ *(.text) }” 表示将所有目标文件中的.text 段(用“()”括起来)组成一个大的.text 段,其中的 “*”表示通配符,当然你可以在此写上所有目标文件的名称。其它的.data、.bss 段 是也一样的道理。可以仔细观察一下图(P-4.2.4.1)。 关于 LD 链接脚本就介绍到这里了,在后面的介绍中,还会有一个用于我们操作系统内 核的链接脚本,有了这里的基础一定可以看懂那个链接脚本。也正是因为操作系统内核的特 殊性,我们不能用 LD 提供的默认的链接脚本,那个脚本是用于普通应用的。 正是因为有了 LD 链接器的存在,使得编译器在编译程序时不必考虑程序运行时的地址, 使得一个大程序可以分解成许多小模块以便于软件的维护,使得程序模块之间可以互相调 用,以便程序的开发可以分工协作化,还使得可以把常用的功能做成库以供其它软件使用, 增加代码的可重用性。这当然也是所有软件开发人员所希望的。 了解了 LD 是做什么的,也看了它今后要用到的一些选项,探索了指挥它的链接脚本, 基本上也就能使用它为我们开发操作系统内核而服务了,至此也结束对 LD 链接器的探讨。 4.2.5 Make 前面了解了 GCC、LD,并且以后操作系统内核的代码就是用 GCC 编译的,一个最简单 操作系统内核至少也有几十个 C 程序文件和几个汇编程序文件构成的,如果一个个程序文 件都要在终端中输入 GCC 及相关命令去编译的话,那完全对我们的身心是种巨大的折磨。 怎么办呢,还好开发操作系统的祖先们,开发了一个软件叫 make。 在软件开发中,make 是一个工具程序,它读取一个叫“makefile”的文件,也是一种文 本文件,这个文件中写好了构建软件的规则,它能根据这些规则自动化构建软件。 makefile 文件中规则是这样的,首先有一个或者许多构建目标称为“target”;目标后面 紧跟着用于构建该目标所需要的文件,目标下面是构建该目标所需要的命令及参数。与此同 时,它也检查文件的依赖关系,如果需要的话,它会调用一些外部软件来完成任务。第一次 构建目标后,下一次执行 make 时,它会根据该目标所需的文件是否更新了,如果没有更新 且该目标又存在,那么它便不会构建该目标。这种特性非常有利于编译程序源代码。 任何一个 linux 发行版中都默认自带这个 make 程序,所以不需要额外的安装工作,可 以直接使用即可。 还是来看看 make 程序的选项,make 常用的选项没有几个。如表(T-4.2.5.1)所示。 选项 相关说明 -B 认为所有的目标都需要更新。 -C 指定读取 makefile 文件的目录。 -f 指定需要执行的 makefile。makefile 是可以重命名的。 -k 出错也不停止运行。 -n 仅输出执行过程中的命令序列,但并不执行命令。 表(T-4.2.5.1) 下面来通过一个简单的 makefile 实例,来看看怎么写好一个 makefile。如代码(C-4.2.5.1) 所示。 helloworld : helloworld.c gcc –o $@ $< 代码(C-4.2.5.1) 这就是一个最简单的 makefile。其中:构建目标为“helloworld”,目标所依赖的文件是: “helloworld.c”,它们之间用“:”隔开。要执行的命令是“gcc –o $@ $<”,“$@ $<”是 make 程序内部使用的两个变量,分别代表目标及目标所依赖的文件,也就是说 make 程序最终会 把“gcc -o helloworld helloworld.c”送到终端里去执行。注意 make 规定要执行的命令的前面 必有一个“tab”符。 再来看一个有点复杂的 makefile。如代码(C-4.2.5.2)所示。 CC = gcc #定义一个宏 CC 等于 gcc CFLAGS = -c #定义一个宏 CFLAGS 等于-c OBJS_FILE = file.c file1.c file2.c file3.c file4.c #定义一个宏 .PHONY : all everything #定义两个伪目标 all、everything all:everything #伪目标 all 依赖于伪目标 everything everything :$( OBJS_FILE) #伪目标 everything 依赖于 OBJS_FILE,而 OBJS_FILE 是宏会被 #替换成 file.c file1.c file2.c file3.c file4.c %.o : %.c $(CC) $(CFLAGS) -o $@ $< 代码(C-4.2.5.2) make 规定“#”后面为注释,make 处理 makefile 时会自动丢弃。makefile 中可以定义宏,方 法是在一个字符串后跟一个“=”或者“:=”符号,引用宏时要用“$(宏名)”,宏最终会在宏 出现的地方替换成相应的字符串,例如:$(CC)会被替换成 gcc,$( OBJS_FILE) 会被替换成 file.c file1.c file2.c file3.c file4.c。.PHONY 在 makefile 中表示定义伪目标,所谓伪目标,就是它不代 表一个真正的文件名,在执行 make 时可以指定这个目标来执行其所在规则定义的命令。但 是伪目标可以依赖于另一个伪目标或者文件,例如:all 依赖于 everything,everything 最终 依赖于 file.c file1.c file2.c file3.c file4.c。然而发现 everything 下面并没有相关的执行命令,但 是下面有个通用规则:“%.o : %.c”。其中的“%”表示通配符,表示所有以“.o”结尾的文件 依赖于所有以“.c”结尾的文件,例如:file.c file1.c file2.c file3.c file4.c 通过这个通用规则会 自动转换为:file.o: file.c 、file1.o: file1.c、 file2.o: file2.c、 file3.o: file3.c、file4.o: file4.c,然 后针对这 5 个规则,分别会执行:$(CC) $(CFLAGS) -o $@ $<命令,当然最终转换为:gcc –c – o xxxx.o xxxx.c,“xxxx”表示一个具体的文件名。 现在来把上面的那个 makefile 稍稍改进一下,如果细心观察不难发现上面的 makefile 一 共有 4 大部分:构建时用到的命令、依赖文件、构建目标、构建规则。 首先建立一个文件:cmd.mh,文件名随意。用于存放相关的命令,文件内容如代码(C- 4.2.5.3)所示。 CC = gcc #定义一个宏 CC 等于 gcc CFLAGS = -c #定义一个宏 CFLAGS 等于-c 代码(C-4.2.5.3) 实际中或许有更多的命令,这里为了简单起见,只有一个命令和一个命令选项。 再建立一个文件:objs.mh,文件名随意。用于存放需要编译的文件即依赖文件,文件内 容如代码(C-4.2.5.4)所示。 OBJS_FILE = file.c file1.c file2.c file3.c file4.c #定义一个宏 #当然可以有更多的文件。 代码(C-4.2.5.4) 然后再建立一个文件:rule.mh,文件名随意。存放用于构建目标的通用规则,文件内容 如代码(C-4.2.5.5)所示。 %.o : %.c $(CC) $(CFLAGS) -o $@ $< #也许有更多的规则例如: #%.o : %.s # $(AS) $(ASFLAGS) -o $@ $< 代码(C-4.2.5.5) 最后在任何情况下,我们可能就只需建立这样的 makefile,如代码(C-4.2.5.6)所示。 include cmd.mh include objs.mh .PHONY : all everything all:everything everything :$( OBJS_FILE) include rule.mh 代码(C-4.2.5.6) 代码(C-4.2.5.6)中的“include”和 C 语言中的“include”是一样的工作机制,同样也 是包含一个文本文件,只是 make 不需要“<>”和“”””。并且 make 会在 include 的地方开 始展开包含的这个文件。也就是说上面这个 makefile 最终会变成和前面代码(C-4.2.5.2)中 的那个 makefile 一样。这样写起 makefile 文件就容易多了,有时甚至只需要改变 everything : 后面的这个宏就可以了,这对于管理构建操作内核模块文件,非常有效。 通过三个简单的例子,大致了解了 make 的工作机制,无非就是层层替换,然后根据目 标的依赖文件是否已更新,决定是否重新构建目标,最后执行构建目标的命令。后面会有一 个用于构建我们操作系统内核的 makefile 例子。也许那时就会对 make 明白许多…… 4.3 LMOSEM 的构建系统 兴趣、自由、自主、外加有点小疯狂,是我们研究开发操作系统内核的核心理念,因此 用 LMOSEM(liberty madness operating system embedded)来命名我们的操作系统内核。后 面就可以直接用 LMOSEM 来代指我们的操作系统内核了。 前面了解了 make 完全可以用来管理 LMOSEM 这个软件工程,下面就去研究一下怎么用 众多的 makefile 一起协作生成相关的文件,比如生成要编译的对象列表文件、用于 LD 链接 器的链接脚本,以至于最后能自动化编译 LMOSEM。 4.3.1 LMOSEM 的 makefile 开发操作系统内核和开发普通应用并无太大区别,就是写好代码然后编译代码。不要说 你会把整个操作系统写成一个源代码文件,就是 LMOSEM 这种用于学习的操作系统内核也 有六、七十个源代码文件。这么多源代码文件,怎么自动化的编译它们,就成了非常重要的 问题。 make 是个好东西且历史悠久,最初开发它的人就是为了自动化编译像操作系统这样的 大型软件,然而几十个源代码文件用一个 makefile 文件去处理,一是没有结构可言,二是容 易变得混乱,三是不好控制。 LMOSEM 的编译一共是由 11 个 makefile 文件互相协同完成的,如下: 1. Makefile,是顶层全局的,它负责调用 make 执行所有下层的 makefile。 2. krnlbuidcmd.mh,这个 makefile 中包括了编译的常用命令,例如 GCC、LD 等。其它 以 “.mk”结尾的 makefile 文件中都要用“include”包含它。 3. krnlbuidrule.mh,这个 makefile 中包括了编译的通用规则,其它以 “.mk”结尾的 makefile 文件中都要用“include”包含它。 4. pretreatment.mk,该 makefile 是最开始执行的,它调用 GCC 预处理功能,用 krnlobjs.S、 lmemknllink.S 两文件生成 krnlobjs.mkh 和 lmemknllink.lds。 5. krnlobjs.mkh,该文件由 krnlobjs.S 生成,里面主要是一些要编译的源文件列表。其 它以“.mk”结尾的 makefile 文件中都要用“include”包含它。 6. lmosemhal.mk,该 makefile 主要用于编译生成硬件相关层的各个模块。 7. lmosemkrl.mk,该 makefile 主要用于编译生成内核层(包括内核功能层和内核接口 层)的各个模块。 8. lmosemdrv.mk,该 makefile 主要用于编译生成一些驱动程序的各个模块。 9. lmosemlib.mk,该 makefile 主要用于编译生成库的各个模块。 10. lmosemtask.mk,该 makefile 主要用于编译生成应用程序。 11. lmosemlink.mk,该 makefile 首先根据前面生成的 lmemknllink.lds 链接脚本文件,然 后把上面编译生成的各个模块链接在一起,生成最后的 LMOSEM 可执行文件。 下面再来看一幅图可能更加形象,如图(P-4.3.1.1)所示。 图(P-4.3.1.1) 再来看看这一整个编译过程,首先从顶层全局的 Makefile 开始分析,如代码(C-4.3.1.1) 所示,由于代码很多,全部列举出来会占用大量的篇幅,不利于阅读,省略的部分会用“……” 代替。代码文件的路经比如“XXXX/lmosem/build/lmosemhal.mk”中的“XXXX”表示 lmosem 工程目录所在的路经。 …… CD = cd RM = rm MAKE = make …… PREMENTMFLGS = -C $(BUILD_PATH) -f pretreatment.mk ARMHALYMFLGS = -C $(BUILD_PATH) -f lmosemhal.mk ARMKRNLMFLGS = -C $(BUILD_PATH) -f lmosemkrl.mk ARMDRIVMFLGS = -C $(BUILD_PATH) -f lmosemdrv.mk ARMLIBSMFLGS = -C $(BUILD_PATH) -f lmosemlib.mk ARMTASKMFLGS = -C $(BUILD_PATH) -f lmosemtask.mk ARMLINKMFLGS = -C $(BUILD_PATH) -f lmosemlink.mk …… BUILD_PATH = ./build EXKNL_PATH = ./exckrnl .PHONY : build print clean all cpkrnl knlexc writekrl downkrnl build: clean print all all: $(MAKE) $(PREMENTMFLGS) $(MAKE) $(ARMHALYMFLGS) $(MAKE) $(ARMKRNLMFLGS) $(MAKE) $(ARMDRIVMFLGS) $(MAKE) $(ARMLIBSMFLGS) $(MAKE) $(ARMTASKMFLGS) $(MAKE) $(ARMLINKMFLGS) @echo '恭喜我,系统编译构建完成! ^_^' clean: $(CD) $(BUILD_PATH); $(RM) -f *.mkh *.lds *.o *.bin *.i *.elf *.krnl *.s *.map *.lib $(CD) $(EXKNL_PATH); $(RM) -f *.mkh *.lds *.o *.bin *.i *.elf *.krnl *.s *.map *.lib @echo '清理全部已构建文件... ^_^' print: @echo '*********正在开始编译构建系统*************' 代码(C-4.3.1.1 [XXXX/lmosem/Makefile]) 首先从代码(C-4.3.1.1)中.PHONY 开始定义了一些伪目标,我们主要关注“build print clean all”。其中“build”伪目标依赖于“clean print all”,所以它们会依次执行,“clean”伪目标会 切换到“build”、“exckrnl”目录下删除一些编译器的产生的文件,“@echo”在 makefile 中表 示输出“’’”中字符串。“CD”、“RM”分别是两个宏,会替换成“cd”(切换目录)、“rm”(删 除文件)命令。“clean”伪目标执行完后会执行“print”伪目标,它是输出字符串的。最后 会执行“all”伪目标,“all”伪目标会执行 7 条 make 命令。我们选择其中两条来分析一下。 第一条“$(MAKE) $(PREMENTMFLGS)”,因为它会产生两重要文件,这两个文件是所有其 它 makefile 所 依 赖 的 , 所 以 它 要 最 先 被 执 行 , 它 被 替 换 后 就 是 :“ make -C ./build -f pretreatment.mk”。也就是说 make 会到 build 目录下读取 pretreatment.mk 文件执行。看一 看 pretreatment.mk 文件中是什么,如代码(C-4.3.1.2)所示。 …… KERNELCE_PATH = ../script/ HEADFILE_PATH = -I ../include/script/ -I ../include/ -I ../include/bastypeinc -I ../include/halinc CCBUILDPATH = $(KERNELCE_PATH) include krnlbuidcmd.mh PREMENT_OBJS = krnlobjs.mkh lmemknllink.lds .PHONY : all everything build_kernel all: build_kernel build_kernel:everything everything : $(PREMENT_OBJS) include krnlbuidrule.mh 代码(C-4.3.1.2 [XXXX/lmosem/build/pretreatment.mk]) 代码(C-4.3.1.2)中有两个 include 分别包含了 krnlbuidcmd.mh、krnlbuidrule.mh 两个文件, 我们把它们展开了看看,如代码(C-4.3.1.3)所示。 …… KERNELCE_PATH = ../script/ HEADFILE_PATH = -I ../include/script/ -I ../include/ -I ../include/bastypeinc -I ../include/halinc CCBUILDPATH = $(KERNELCE_PATH) CC = arm-linux-gcc …… CPPFLGSLDS = $(HEADFILE_PATH) -E -P PREMENT_OBJS = krnlobjs.mkh lmemknllink.lds .PHONY : all everything build_kernel all: build_kernel build_kernel:everything everything : $(PREMENT_OBJS) …… %.lds : $(CCBUILDPATH)%.S $(CC) $(CPPFLGSLDS) -o $@ $< $(PRINTCSTR) %.mkh : $(CCBUILDPATH)%.S $(CC) $(CPPFLGSLDS) -o $@ $< $(PRINTCSTR) 代码(C-4.3.1.3) 代码(C-4.3.1.3)中,KERNELCE_PATH 表示要编译的源代码文件的目录,HEADFILE_PATH 表示 GCC 查询头文件的目录列表。目标就是生成 krnlobjs.mkh、lmemknllink.lds 两个文件, 然而 script 目录下确实有 krnlobjs.S、lmemknllink.S 正好匹配下面两条规则,会分别执行“armlinux-gcc -I ../include/script/…… -E -P -o lmemknllink.lds ../script/ lmemknllink.S”、“arm-linux-gcc -I ../include/script/…… -E -P -o krnlobjs.mkh ../script/krnlobjs.S”。这两条命令,是预处理命令。 这里主要分析一下 krnlobjs.S 文件,如代码(C-4.3.1.4)所示。lmemknllink.lds 文件以后 再分析。 …… #include "buildfile.h" BUILD_MK_BOOT_OBJS := BUILD_MK_HALY_OBJS := …… BUILD_MK_BOOT_OBJS +=BUILD_BOOT_OBJS BUILD_MK_HALY_OBJS +=BUILD_HALY_OBJS …… 代码(C-4.3.1.4 [XXXX/lmosem/script/krnlobjs.S]) 代码(C-4.3.1.4)中不是汇编语言代码,也不是 C 程序代码,所以不能被编译只能作预处理 操作,这个文件中包含了 buildfile.h 文件,其内容如代码(C-4.3.1.5)所示。 #ifndef BUILDFILE_H #define BUILDFILE_H #define BUILD_BOOT_OBJS #define BUILD_HALY_OBJS init.o lmosemhal_start.o interrupt.o intabtdistr.o…… …… #endif 代码(C-4.3.1.5 [XXXX/lmosem/include/script/buildfile.h]) 看到这里,会发现 krnlobjs.S 文件中的 BUILD_BOOT_OBJS 、BUILD_HALY_OBJS 不过是 buildfile.h 文件中定义的两个宏,所谓对 krnlobjs.S 文件进行预处理,也不过就是进行宏替换操作。到 这里你也许会恍然大悟,最终生成的 krnlobjs.mkh 文件如代码(C-4.3.1.6)所示。 BUILD_MK_BOOT_OBJS := BUILD_MK_HALY_OBJS := …… BUILD_MK_BOOT_OBJS += BUILD_MK_HALY_OBJS +=init.o lmosemhal_start.o interrupt.o intabtdistr.o…… …… 代码(C-4.3.1.6) 到这里我们看到了将 krnlobjs.S 文件转换成其它 makefile 文件都要包含的编译对象列表文件 krnlobjs.mkh 的整个过程,并且只需要通过改变一个 buildfile.h 文件就能决定哪些代码文件 需要编译。 分析了生成 krnlobjs.mkh 文件的过程,并且它是其它 LMOSEM 组件编译的基础。再来看 看怎么编译 LMOSEM 硬件相关层的代码模块,由代码(C-4.3.1.1)知道,执行完“make -C ./build -f pretreatment.mk”命令,接着会执行“make -C ./build -f lmosemhal.mk”命令。lmosemhal.mk 中的内容如代码(C-4.3.1.7)所示。 …… KERNELCE_PATH = ../hal/ HEADFILE_PATH = -I ../include -I ../include/bastypeinc -I ../include/halinc…… CCBUILDPATH = $(KERNELCE_PATH) include krnlbuidcmd.mh include krnlobjs.mkh .PHONY : all everything build_kernel all: build_kernel build_kernel:everything everything :$(BUILD_MK_HALY_OBJS) include krnlbuidrule.mh 代码(C-4.3.1.7 [XXXX/lmosem/build/lmosemhal.mk]) 其实所有的“lmosem***.mk”文件都和代码(C-4.3.1.7)是一样的,唯一不同的是代码中的 KERNELCE_PATH、HEADFILE_PATH 两个宏后面的字符串,分别表示要编译源代码文件的目录 和 GCC 查询头文件的目录列表,还有 everything 伪目标所依赖的文件对象也不同,这里它是 依赖于 krnlobjs.mkh 文件中 BUILD_MK_HALY_OBJS。可以看到 lmosemhal.mk 中包含了三个文 件: krnlbuidcmd.mh、krnlobjs.mkh、krnlbuidrule.mh。同样的,也把它们在包含的地方展开, 以便看的更清楚,如代码(C-4.3.1.8)所示。 …… KERNELCE_PATH = ../hal/ HEADFILE_PATH = -I ../include -I ../include/bastypeinc -I ../include/halinc…… CCBUILDPATH = $(KERNELCE_PATH) CC = arm-linux-gcc CFLAGS = $(HEADFILE_PATH) -c -O2 -fno-builtin -ffreestanding -std=c99…… …… BUILD_MK_BOOT_OBJS := BUILD_MK_HALY_OBJS := …… BUILD_MK_BOOT_OBJS += BUILD_MK_HALY_OBJS +=init.o lmosemhal_start.o interrupt.o intabtdistr.o…… …… .PHONY : all everything build_kernel all: build_kernel build_kernel:everything everything :$(BUILD_MK_HALY_OBJS) CCSTR = 'CC -[M] 正在构建... '$< PRINTCSTR = @echo $(CCSTR) %.o : $(CCBUILDPATH)%.c $(CC) $(CFLAGS) -o $@ $< $(PRINTCSTR) %.o : $(CCBUILDPATH)%.S $(CC) $(CFLAGS) -o $@ $< $(PRINTCSTR) …… 代码(C-4.3.1.8) 看到代码(C-4.3.1.8),一个完整的用于编译 LMOSEM 硬件相关层各代码模块的 makefile 文 件就这样完成了,其中有了编译目录、编译命令、编译命令的选项、编译文件对象的列表、 还有用于编译特定代码文件的规则。其它用于编译 LMOSEM 的其它组件的 makefile 也是同 样的原理。由于篇幅所限,编译 LMOSEM 的内核层、编译 LMOSEM 的驱动程序等,就不再 详细介绍了,读者能看懂这个,也应该能看懂其它的。 通过对 LMOSEM 的众多 makefile 文件的了解,并且看到了如何应用这许多的 makefile 文件组成一个结构良好的自动化编译环境,同时也加深了对 make 的认识,感受到了 make 的强大。但 make 远远不止如此,这不过是对 make 一点小小的应用,然而这已经够了,我 们还有其它别的问题要探索,所以要对 make 说再见了。 4.3.2 LMOSEM 的链接脚本 编译器编译出众多的 LMOSEM 模块,这些模块是不能直接运行的,因为代码指令中的 一些地址没有绑定,模块与模块之间可能存在互相调用,这就需要链接器来处理这些问题。 这也是构建 LMOSEM 的最后一步——链接。 然而链接器的工作需要一个链接脚本,这个链接脚本也使得我们有机会告诉链接器,怎 么链接这些目标文件,我们只需要按照要求,写好这个链接脚本就行了。 下面就来看看 LMOSEM 的链接脚本,前面分析 pretreatment.mk 文件时,就知道 LMOSEM 的链接脚本就是它调用 GCC 对 lmemknllink.S 文件预处理后产生的,首先粗略的看一看 lmemknllink.S 文件是什么样的,如代码(C-4.3.2.1)所示。 #include "buildfile.h" INPUT(LINKR_IPUT_FILE) OUTPUT(LINKR_OPUT_FILE) OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") OUTPUT_ARCH(arm) ENTRY(_start) SECTIONS { . = 0x30008000; __begin_kernel = .; __begin_lmosem_hal_head_text = .; .head.text ALIGN(4) : { *(.head.text) } __end_lmosem_hal_head_text = .; __begin_lmosem_hal_head_data = .; .head.data ALIGN(4) : { *(.head.data) } __end_lmosem_hal_head_data = .; …… __begin_lmosem_hal_intvect = .; .lmosem_hal.intvect ALIGN(4) : { *(.lmosem_hal.intvect) } __end_lmosem_hal_intvect = .; __begin_text = .; .text ALIGN(4) : { *(.text) } __end_text = .; __begin_data = .; .data ALIGN(4) :{ *(.data) } __end_data = .; …… __begin_bss = .; .bss ALIGN(4) : { *(.bss) } __end_bss = .; __end_kernel = .; …… } 代码(C-4.3.2.1 [XXXX/lmosem/script/lmemknllink.S]) 代码(C-4.3.2.1)中只是链接脚本的几个片段,其中唯一要经过 GCC 预处理的只有: INPUT(LINKR_IPUT_FILE)、OUTPUT(LINKR_OPUT_FILE)两行代码,INPUT、OUTPUT 中的两个宏 定义在 buildfile.h 文件中,如代码(C-4.3.2.2)所示。 …… #define BUILD_LINK_OBJS BUILD_BOOT_OBJS BUILD_HALY_OBJS\ BUILD_KRNL_OBJS BUILD_MEMY_OBJS\ BUILD_FSYS_OBJS BUILD_DRIV_OBJS\ BUILD_LIBS_OBJS BUILD_TASK_OBJS #define LINKR_IPUT_FILE BUILD_LINK_OBJS #define LINKR_OPUT_FILE lmosemkrnl.elf …… 代码(C-4.3.2.2 [XXXX/lmosem/include/script/buildfile.h]) 这就非常清楚了,LINKR_IPUT_FILE 会替换成 BUILD_LINK_OBJS,最终会替换成 LMOSEM 中所 有被 GCC 编译的目标文件,而 LINKR_OPUT_FILE 会替换成 lmosemkrnl.elf。于是 LMOSEM 的 链接脚本就变成了这样,如代码(C-4.3.2.3)所示。 INPUT( init.o lmosemhal_start.o interrupt.o intabtdistr.o halglobal.o halinit.o……) OUTPUT(lmosemkrnl.elf) OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") OUTPUT_ARCH(arm) ENTRY(_start) SECTIONS { . = 0x30008000; __begin_kernel = .; __begin_lmosem_hal_head_text = .; .head.text ALIGN(4) : { *(.head.text) } __end_lmosem_hal_head_text = .; __begin_lmosem_hal_head_data = .; .head.data ALIGN(4) : { *(.head.data) } __end_lmosem_hal_head_data = .; __begin_lmosem_hal_init = .; .lmosem_hal.init ALIGN(4) : { *(.lmosem_hal.init) } __end_lmosem_hal_init = .; __begin_lmosem_hal_vector = .; .lmosem_hal.vector ALIGN(4) : { *(.lmosem_hal.vector) } __end_lmosem_hal_vector = .; __begin_lmosem_hal_intvect = .; .lmosem_hal.intvect ALIGN(4) : { *(.lmosem_hal.intvect) } __end_lmosem_hal_intvect = .; __begin_text = .; .text ALIGN(4) : { *(.text) } __end_text = .; __begin_data = .; .data ALIGN(4) :{ *(.data) } __end_data = .; __begin_rodata = .; .rodata ALIGN(4) :{ *(.rodata) *(.rodata.*) } __end_rodata = .; __begin_kstrtab = .; .kstrtab ALIGN(4) : { *(.kstrtab) } __end_kstrtab = .; __begin_bss = .; .bss ALIGN(4) : { *(.bss) } __end_bss = .; __end_kernel = .; …… } 代码(C-4.3.2.3) 从代码(C-4.3.2.3)中的链接脚本不难看出,生成 LMOSEM 的可执行文件是 ARM 小端 32 位的 ELF 格式,运行地址是从 0x30008000 开始的。LMOSEM 可执行文件中有如下各个段: 1. .head.text 段,存放最开始运行的指令。 2. .head.data 段,存放最开始运行的数据结构。 3. .lmosem_hal.init 段,包含 LMOSEM 硬件相关层的初始化指令。 4. .lmosem_hal.vector 段,包含 ARM 平台的中断向量。 5. .lmosem_hal.intvect 段,包含中断处理程序的指令。 6. .text 段,包含所有目标文件的默认指令段。 7. .data 段,包含所有目标文件的默认数据段。 8. .rodata 段,包含所有目标文件的默认只读数据段。 9. .kstrtab 段,包含所有目标文件的默认字符串表段。 10. .bss 段,包含所有目标文件的默认未初始化或者初始化为 0 的数据段,它不占用可 执行文件的空间。 LD 链接器也一定会按照上述段的顺序合成 LMOSEM 可执行文件,比如 LMOSEM 可执行文件 最开始的地方放的是.head.text 段,然后放的是.head.data 段……依次类推。诸如__begin_XXXX、 __end_XXXX,这些符号对链接脚本来说不是必须的,但是后面会用到它们,以确定内核各段 的大小,后面在使用它们的地方会有介绍。 关于 LMOSEM 的链接脚本就介绍到这里,非常简单,如果不能一下子全明白可以结合 前面 LD 链接器那一章节一起看,应该不难。当然也可以参阅 LD 链接器的官方手册。不过这 些内容对于 LMOSEM 已经是足够了…… 4.4 开发板的安装 操作系统内核编译好了,然后就是测试,以证明操作系统内核的健壮性。操作系统内核 的测试必须需要一套计算平台。 读者可能想到了虚拟机,虚拟机用于测试操作系统内核故然好,可是虚拟机并不能仿真 硬件的每个特性,比如时钟硬件,虚拟机能仿真,但不一定像真实时钟一样精确……并且虚 拟机可能有 BUG,但是虚拟机确实能完美的运行现有的操作系统,因为虚拟机的开发者大部 分情况下,总是用现有的操作系统测试他们的虚拟机,现有的操作系统可能并没有用到计算 平台上特定硬件的所有特性,并且我们的操作系统内核的代码绝对不可能和现有的操作系统 的代码完全相同。所以用虚拟机测试我们的操作系统内核,很有可能出现未知的问题,这个 问题直接导致你会怀疑你的代码,事实上那是虚拟机的问题。 由上可知,我们应该来真的,用真实的物理硬件去运行我们的操作系统内核。前面就写 到,是用 mini2440 开发板作为开发平台的,下面就去探讨怎么安装和使用它。 4.4.1 安装 mini2440 前面章节中就了解了 mini2440,现在来看看怎么安装它并且怎么和 PC 机进行连接,以 便下载操作系统内核到 mini2440 中运行,达到测试操作系统内核的目的。 安装 mini2440 其实很简单,无非就是连接上电源、连接上串口线,连接上 USB 下载线。 先来看看 mini2440 提供上述连接的接口,如图(P-4.4.1.1)所示。 图(P-4.4.1.1) 图(P-4.4.1.1)中从左至右依次是:音频接口、USB 接口、网络接口、串口、电源接口。其 中音频接口和网络接口我们并不关心。 由于现代 PC 桌面机和笔记本电脑上可能已经都没有串口硬件了,所以需要一个额外的 部件:串口转 USB 接口部件,如图(P-4.4.1.2)所示。该部件接受串口数据然后通过 USB 接 口发送到 PC 机,PC 机的操作系统中有专门处理这个设备和其中相关数据的驱动程序。 图(P-4.4.1.2) 依次连接上电源线、USB 下载线、串口线及串口转 USB 接口部件,如图(P-4.4.1.3)所 示。 图(P-4.4.1.3) 按照图(P-4.4.1.3)中连接好后,把 USB 下载线和串口转 USB 接口部件分别插入 PC 机上两 个不同的 USB 端口,然后把电源插入交流电源的插座中就行了。 上面配置了硬件,下面就要去配置软件了,在 linux 环境下需要如下软件: 1. minicom,主要显示串口数据的,前面已经安装了。 2. dnw2,LMOSEM 代码中自带的一个工具,用于下载内核到 mini2440 中的。 3. libusb-dev,一个 USB 库 dnw2 需要通过它识别 mini2440。同样前面也已经安装了。 4. USB 转串口设备驱动,用于获取 USB 转串口设备的数据。见图(P-4.4.1.2)。大部分 情况下 linux 内核中都自带这个驱动,不需要额外安装。 minicom 这个软件需要配置一下,因为它必须要知道 USB 转串口的设备文件和设置一下 串口的通信速率。linux 内核把设备管理纳入文件系统了,所有设备一旦插入计算机并装载 该设备驱动后,就会在文件系统的“/dev/”目录下创建一个设备文件,拔掉该设备就会删掉 这个设备文件。USB 转串口设备的文件名一般是由“ttyUSB”开始后面接一个数字命名的, 读者的计算机中可能有多个 USB 转串口设备,请多拔插几下 USB 转串口设备,观察“/dev/” 目录下文件的变化,以确定 USB 转串口设备的设备文件,确定之后就常用这个 USB 插口就 行了。笔者机器上的 USB 转串口设备的设备文件名是 ttyUSB0。 minicom 这个软件的配置很简单,步骤如下: 1. 首先在终端中输入命令:“sudo minicom –s”回车。 2. 选择“Serial port setup”如图(P-4.4.1.2)所示。 3. 输入“a”配置 USB 转串口设备的设备文件名,输入“/dev/ttyUSB0”如图(P-4.4.1.3) 所示。 4. 输入两次回车后,返回 minicom 的主菜单选择“Save setup as dfl”然后回车。 5. 选择“Exit”退出 minicom 的配置主菜单。 由上述第 3 步中,发现 minicom 默认情况下,串口数据通信速率就是 115200,这正好以我 们开发板上的串口通信速率是吻合的。 图(P-4.4.1.2) 图(P-4.4.1.3) 上面配置了 minicom,现在就可以启动 mini2440 开发板了,开发板上一共有两个开关, 一个是电源接口这边的,这个是电源开关。一个是音频接口这边的,这个是启动方式的跳线 开关。先把启动方式的跳线开关拨向 NOR 端,这是启动 mini2440 开发板上的引导程序,相 当于 PC 机的 BIOS。然后打开电源开关,mini2440 开发板就启动了,如图(P-4.4.1.4)所示。 图(P-4.4.1.4) mini2440 开发板的安装和配置,非常简单,不必过多在此浪费时间,不过要相信,现在 所做的一切,都是为了后面我们开发 LMOSEM 时尽可能少的遇到麻烦,这并不是徒劳无功 的行为…… 4.5 小结 本章就要结束了,这是让人兴奋的事情,这章主要了解了操作系统内核设计、操作系统 内核开发环境和开发工具相关的内容: 1. 操作系统内核的设计,介绍了内核需要什么功能;关于内核的一些架构,介绍了两 种内核架构的优势和缺点;考虑了内核将来的可移植性,这主要通过分离硬件相关 性而达到;最后利用这些知识点设计了我们的操作系统内核。 2. 开发环境及相关工具,从 linux 环境开始,介绍了一些开发中要用到的工具,例如: GCC、LD、make。 3. LMOSEM 的构建系统,主要介绍了两个例子:LMOSEM 的 makefile 和 LMOSEM 的 链接脚本,通过它们组成了 LMOSEM 的构建系统。 4. 开发板的安装,安装并测试了 mini2440 开发板,用它作为 LMOSEM 的运行平台。 我们设计了操作系统内核,了解了开发工具,后面的内容就轻松多了,我们正一步一步 的完成这些开发前的准备工作…… 8 中断管理 除了初始化之外,操作系统对应用提供的服务,当然这些服务有些是操作系统中的一些 软件完成的,而有时要借助于设备完成,但不管怎么样,它们都是从中断开始的,后面读者 慢慢就会认同这句话的。 从上面的言语中,好像感觉中断就是操作系统之灵魂。我们暂且先带着这样的感觉,看 看什么是中断、了解中断控制器、如何处理中断、怎么添加中断回调函数。下面就要脚踏实 地,一步一步的来…… 8.1 中断与中断控制器 中断到底是什么,是干什么的有什么作用,能否被控制,用什么方法控制,下面就去解 决这些问题。 8.1.1 什么是中断 先来看三种情景:“你在开车中,突然汽车引擎坏了,你需要修复它才能继续驾驶汽 车……”,“你在外旅游,你女朋友突然来电话了,你可以选择接电话或者不接电话,当然不 接电话的后果很严重(笑)……”,“在老鼠必经的路上放上老鼠夹,只要老鼠走上这条路踩 上老鼠夹,就必然被老鼠夹夹住,如果不解开老鼠夹,老鼠无法继续前行……” 在以上三种情景中,虽然不十分恰当,但都是在做一件事时,因为一些原因从而要做另 一件事。例如开车中汽车引擎坏了,在外旅游女朋友来电话了,老鼠踩上老鼠夹…… 计算机中的 CPU 也是一样,在做一件事时,因为一些原因从而要做另一件事,于是中 断产生了…… 因为原因的类型不同,所以中断被分为三类: 1. 异常,是同步的,是因为错误和故障,例如汽车引擎坏了。不修复错误就不能继续 运行,所以这时 CPU 会跳到这种错误的处理代码那里开始运行,运行完了会返 回……如果不修改程序中的错误,下次运行程序到这里同样会发生异常,所以它是 同步的。 2. 中断,是异步的,我们通常说的中断就是这种类型,它是因为外部事件而产生的, 例如,旅游时女朋友来电话了。通常是设备需要 CPU 关注时,给 CPU 发送一个中断 信号,所以这时 CPU 会跳到处理这种事件的代码那里开始运行,运行完了会返 回……,由于不知道何种设备何时发出这种中断信号,所以它是异步的…… 3. 陷入,也是同步的,例如老鼠只要踩上老鼠夹,就必然被老鼠夹夹住,CPU 中也有 专门制造老鼠夹的指令——swi 指令,只要一运行这条 swi 指令,CPU 就会跳到专 门安排的代码处运行,运行完了会返回。只要一运行这条指令,CPU 就会发生这种 行为,所以我们说它也是同步的。 中断,一句话:打断当前 CPU 的代码执行流,转而执行另一段代码的行为。 或许上面的例子还不能让你明白中断有什么用,下面就来看看几个中断的应用场景: 一,如果当前程序中有条指令 CPU 不认识,而 CPU 又没有中断异常机制,CPU 该何去 何从,可能机器重启是最好的方法,但是这是不是太粗暴了。如果 CPU 有中断异常机制, CPU 就能在这种情况下,跳转到相应的处理代码处开始运行并修复错误。 二,CPU 向设备发送一条命令以读取设备中的数据,可是设备的数据还没有准备好,而 CPU 又没有中断异常机制,CPU 只能在那里循环等待设备,不能做任何其它事,这是不是效 率太低了。如果 CPU 有中断异常机制,CPU 就能在这种情况下去做别的事情,设备准备好相 关的数据后给 CPU 发送一个相应的中断信号,然后 CPU 在中断处理代码中就能读取设备的 数据了。 三,操作系统代码是为应用程序服务的,如果 CPU 没有陷入机制,应用程序只能直接 调用操作系统的代码,这种情况下根本就不受控制,系统代码也不能被保护,系统的安全、 可靠性无从谈起。如果 CPU 有了陷入机制后,系统代码就可以被保护起来,应用程序要得 到服务,只有执行陷入指令,一旦执行陷入指令,就会被操作系统的代码接管。 总之,中断综合起来就是,通过打断当前 CPU 代码的执行流,转而执行另一段代码的 方式:处理异常事件、解决 CPU 以设备之间的通信效率、保证内核代码安全的为应用程序 提供服务。中断本身是简单的,因为它而产生的一切功能却不简单,这里了解了中断是什么 就行了,下面我们继续…… 8.1.2 s3c2440a 中断控制器 中断带来的作用是巨大的,可是中断要不要被适当控制,答案是肯定的,如果不可被控 制,那是相当可怕的。 前面发现中断有三种类型,其中异常和陷入是同步的,本身就是可控的,只要代码中没 有错误或者包含陷入指令,CPU 就不会产生异常和陷入。可是设备中断就不同了,它是异步 的,我们不知道它何时向 CPU 发出中断信号。例如有时临界区中代码的运行是绝对不能产 生中断的。 由上可知对于设备产生的异步中断一定要被控制,因为不仅仅是允不允许 CPU 响应中 断,还要确定中断的来源(因为系统中不止一个设备),对每个中断源分别控制,允不允许 它们各自产生中断,它们产生一个中断后要等待 CPU 处理,不然它们不能产生下一次中断 等……由于这些控制很复杂,所以产生了专门的控制芯片——中断控制器。 关于 s3c2440a 中断控制器的介绍,请参阅前面章节,这里我们直接进入系统编程需要 的东西。s3c2440a 中断控制器在 s3c2440a 芯片内部,最多支持 60 个中断源,这 60 个中断 源分两层级连的,我们只能直接获取顶层的 32 个中断源,也就是说这顶层的有些中断源, 包含了几个下级中断源。如图(P-8.1.2.1)所示。 图(P-8.1.2.1) 图(P-8.1.2.1)中也画出了一些重要的寄存器,其实硬件提供给软件的编程接口就是这 一个个寄存器,所以作为编程人员了解这些寄存器就行了。下面分别看看这些寄存器。 我们从下向上看,先来看看子中断源寄存器,如表(T-8.1.2.1)所示。 名称(地址) 读/写 说明 中断源 SUBSRCPND 读/写 15~31 位保留。0~14 位,每位对应一个 参见 6.6.1 节。 (0X4A000018) 中断源,其位为 1 时表示有中断请求,为 0 时表示无中断请求。初始化时为 0。 表(T-8.1.2.1) 与子中断源寄存器直接相连的是子中断屏蔽寄存器,子中断屏蔽寄存器就像一扇门,它不打 开,这些子中断信号是不能通过的。如表(T-8.1.2.2)所示。 名称(地址) 读/写 说明 附加 INTSUBMSK 读/写 15~31 位保留为 0。0~14 位,每位对应 该寄存器位与 (0X4A00001C) 一个子中断源,其位为 1 时表示屏蔽该位 子中断源寄存 对应的中断,为 0 时表示放行该位对应的 器位位对应。 中断。初始化时 0~14 位为 1,15~31 位 为 0。 表(T-8.1.2.2) 接着看看右边的外部中断源寄存器,它和子中断源寄存器是平级的,它负责外部器件的 中断信号。如表(T-8.1.2.3)所示。 名称(地址) 读/写 说明 中断源 EINTPEND 读/写 0~3 位保留为 0。4~23 位,每位对应一 参见 6.6.1 节。 0x560000A8 个外部中断源,其位为 1 时表示有中断请 求,为 0 时表示无中断请求。初始化时为 0。注意 4~23 位,各位要清 0 的话,必 须向其位写入 1 才行。 表(T-8.1.2.3) 与外部中断源寄存器直接相连的也是外部中断屏蔽寄存器,同样的,外部中断屏蔽寄存器也 像一扇门,它不打开,这些外部中断信号也不能通过。如表(T-8.1.2.4)。 名称(地址) 读/写 说明 附加 EINTMASK 读/写 0~3 位保留为 1。4~23 位,每位对应一 该寄存器位也 (0x560000A4) 个外部中断源,其位为 1 时表示屏蔽该位 是与外部中断 对应的中断,为 0 时表示放行该位对应的 源寄存器位位 中断。初始化时 4~19 位为 1,20~23 位 对应的。 为 0。 表(T-8.1.2.4) 再来看看中断源寄存器,它其中有些位连接着多个子中断源和多个外部中断源,所以如 果它有些位为 1 了,还要进一步确定是那个子中断源或者外部中断源。见表(T-8.1.2.5)所 示。 名称(地址) 读/写 说明 中断源 SRCPND 读/写 0~31 位,每位对应一个中断源,其位为 参见 6.6.1 节。 (0X4A000000) 1 时表示有中断请求,为 0 时表示无中断 请求。初始化时为 0。 表(T-8.1.2.5) 同样的,中断源寄存器上面也有一扇门,就是中断屏蔽寄存器,它其中相关的位不打开,中 断源信号是不能通过的,如表(T-8.1.2.6)所示。 名称(地址) 读/写 说明 附加 INTMSK 读/写 0~31 位,每位对应一个中断源,其位为 该寄存器位与 (0X4A000008) 1 时表示屏蔽该位对应的中断,为 0 时表 中断源寄存器 示放行该位对应的中断。初始化时所有位 位位对应。 为 1。 表(T-8.1.2.6) 由于 CPU 支持两种中断方式即 IRQ 和 FIQ 中断方式,所以得有个模式寄存器,如表(T-8.1.2.7) 所示。注意,FIQ 模式的中断并不经过中断优先级机制仲裁,直接就到达 CPU 的 FIQ 管脚上 去了。中断优先级机制,其实就是选择一个需要最先响应的中断源送给 CPU 处理,这里不 介绍其细节。 名称(地址) 读/写 说明 附加 INTMOD 读/写 0~31 位,每位对应一个中断源,如果某 该寄存器位与 (0X4A000004) 个指定位被设置为 1,则在 FIQ(快中断) 中断源寄存器 模式中处理相应中断。否则在 IRQ 模式中 位位对应。 处理。初始化时所有位为 0。 表(T-8.1.2.7) 如果多个 IRQ 模式的中断源同时发生了中断,会首先通过中断优先级机制仲裁出一个最 先需要响应的中断源,并在中断源挂起寄存器相应位中标记,同一时刻其中只会有一位被置 1。如表(T-8.1.2.8)所示。 名称(地址) 读/写 说明 附加 INTPND 读/写 0~31 位,每位对应一个中断源,如果某 该寄存器位与 (0X4A000010) 个指定位被设置为 1,即表明了相应未屏 中断源寄存器 蔽并且正在等待中断服务的中断源请求, 位位对应。 具有最高的优先级。所以只有 1 位可以设 置为 1,并且产生 IRQ 模式中断请求给 CPU。中断服务程序读取此寄存器来决定 是哪个中断源发生了中断。初始化时为 0。 表(T-8.1.2.8) 最后来看看,中断偏移寄存器,这个寄存器也是 32 位的,它里面保存着当前中断源挂 起寄存器中为 1 的位号,即 0~31 个中断源,哪个中断源产生了中断,如表(T-8.1.2.9)所 示。 名称(地址) 读/写 说明 附加 INTOFFSET 只读 0~31 位,只读的,寄存器的值从 0~31, 无 (0x4A000014) 中断服务程序读取此寄存器来决定哪个 中断源发生了中断。初始化时为 0。 表(T-8.1.2.9) 中断控制器的一些关键寄存器就到这里了,图(P-8.1.2.1)中画得可能与具体芯片的实 现不一致,不过这也没什么问题,因为我们了解了这 9 个寄存器就可以了。 经过前面几章和这里的介绍,对 s3c2440a 的中断控制器的理解,已经了然于胸了,这 仅仅是硬件,下面就要做软件了,写代码合理的管理和控制这些中断了…… 8.2 中断管理的架构及相关数据结构 前面已经熟悉了中断控制器,操作系统内核只要和中断控制器打交道就行了,在开始写 代码之前,我们要做到对中断管理架构有所了解,比如它在内核中是处于哪个层次的,又是 怎么组织的……当然了,要合理管理那么多的中断还需要设计相应的数据结构才行。 话不多说,开始吧…… 8.2.1 中断管理的架构 如同内存管理组件一样,中断管理也需要设计相应的架构,这样才能便于内核代码以后 的移植和维护,所以对内核中的每个组件设计良好的架构是必要的。 LMOSEM 内核整体上有三层,这个前面已经说过了。但是几乎每个计算平台的中断控制 器都不一样,所以操作中断控制器本身的代码在不同的计算平台下肯定是要改变的,那么操 作中断控制器的代码和函数,就应该放在内核的 hal 层(硬件平台相关层)中。而操作中断 控制器的接口和一些通用功能的代码,应该放在内核功能层中,因为这些代码不太可能发生 改变。由上所述,中断管理组件可分为两层,上层是中断管理接口和针对中断管理的通用功 能,下层是中断管理的核心。 再来看看中断管理的下层中有些什么,不外乎就是:中断向量、保存 CPU 上下文的代 码、中断分发器、中断控制器相关的代码等。 所以 LMOSEM 内核的中断管理组件的架构,如图(P-8.2.1.1)所示。 图(P-8.2.1.1) 一图解千言,图(P-8.2.1.1)中清晰的展示了中断管理组件的架构,但是除了中断向量 外,其它部分都好像不太明白,不要紧张,因为这正是我们后面要实现的。这里只要清楚这 个架构就行了。 设计好了中断管理组件,接着就要编码实现了,我们继续…… 8.2.2 intfltdsc_t 和 intserdsc_t 中断管理组件的框架已经设计好了,下面就要开始写代码具体实现了,不过首先要设计 相应的数据结构。 中断管理组件重要的数据结构,无非就是以下两个: 1. 中断源描述符,一个中断源描述符管理一个中断源。这个数据结构在初始化章节中 已经看过了。 2. 中断回调函数描述符,中断管理的重点,是要让设备驱动程序能处理设备的中断。 后面会发现,中断处理分为公共部分和特定的设备部分。 先来看看中断源描述符,这个数据结构在前面章节中就说过了,并且我们用这种数据结 构类型的数组,平级映射了 mini2440 上的外部中断源,子中断源、主中断源。在这里仍然 贴出它的代码,如代码(C-8.2.2.1)所示。 typedef struct s_INTFLTDSC { spinlock_t i_lock; //保护其自身 list_h_t i_serlist; //设备中断服务程序链,后面会介绍的 uint_t i_sernr; //设备中断服务程序链的个数 u32_t i_flg; //标志位 u32_t i_stus; //状态位 u32_t i_pndbitnr; //中断源寄存器中的位序 uint_t i_irqnr; //中断号 uint_t i_deep; //保留 uint_t i_indx; //产生中断的次数 }intfltdsc_t; 代码(C-8.2.2.1 [xxxx/lmosem/include/halinc/halintupt_t.h]) intfltdsc_t 已然不必多说了,下面来看看 intserdsc_t 数据结构,它可以称为中断回调函 数描述符、也可以称为中断服务函数描述符。总之,该数据结构中存放着具体设备中断处理 代码的地址和相关信息。为什么不直接放在 intfltdsc_t 中呢,因为在有些计算平台上,多个 设备的中断信号线可能是连接在同一个中断源上。既然可以有多个设备,那么就不可能只包 含一个中断回调函数,还需要包含一些设备信息。因此我们需要设计另一个数据结构,即 intserdsc_t。如代码(C-8.2.2.2)所示。 typedef sint_t drvstus_t;//设备返回值类型 typedef drvstus_t (*intflthandle_t)(uint_t ift_nr,void* device,void* sframe);//中断回调函数的 指针类型:该函数类型返回值为 drvstus_t,参数类型为 uint_t ift_nr(中断号),void* device (设备指针),void* sframe(中断栈指针) typedef struct s_INTSERDSC { list_h_t s_list; //该链表是挂在 intfltdsc_t 中的 i_serlist 上的 list_h_t s_indevlst; //该链表是挂在设备描述符中的相关链表上的 u32_t s_flg; //一些标志 intfltdsc_t* s_intfltp; //指向它所在的 intfltdsc_t,通过这个可快速找到 intfltdsc_t void* s_device; //指向其设备描述符,可以通过它找到这是哪个设备的中断 uint_t s_indx; //该中断回调函数的运行计数 intflthandle_t s_handle; //回调函数的指针,保存回调函数的地址 }intserdsc_t; 代码(C-8.2.2.2 [xxxx/lmosem/include/halinc/halintupt_t.h]) 注意,intfltdsc_t 是静态定义的,但是 intserdsc_t 不是,它是由驱动程序动态分配建立的。 后面就会发现,驱动程序动态分配建立一个 intserdsc_t 数据结构后,然后就把它挂在 intfltdsc_t 中的 i_serlist 上。 还是用一幅图来展示一下 intfltdsc_t 和 intserdsc_t 数据结构之间的关系,心中有图,理 解起来就容易多了,如图(P-8.2.2.1)所示。 图(P-8.2.2.1) 图(P-8.2.2.1)中,只画出了数据结构中的关键域,实际中也可能没有这么多的 intserdsc_t, 这不过是可能会出现的一种情况。 设计好了数据结构,并清楚了它们之间的关系,下面就去编写相关的代码了…… 8.3 中断处理 一个硬件中断的到来,接下来的动作当然是中断处理,这也是中断管理组件的核心部分, 这节首先会写几个中断辅助例程,然后从中断向量开始,一步步到最后调用中断处理例程结 束整个中断处理过程。 前面已经了解了中断控制器,也知道了中断管理组件的架构,同时也设计好了相关的数 据结构,下面就去用代码实现中断处理的整个过程…… 8.3.1 中断辅助例程 在中断处理开始之前,首先要实现一些处理中断中要用到的辅助功能函数,如下: 1. 获取中断源描述符。 2. 确定中断源。 3. 清除中断源挂起寄存器。 4. 打开和关闭中断源。 先来看看怎么获取中断源描述符,现在就来写这个函数,如代码(C-8.3.1.1)所示。 intfltdsc_t* hal_retn_intfltdsc(uint_t ifdnr) { if(ifdnr>=osmach.mh_intfltnr) { return NULL; } return &osmach.mh_intfltdsc[ifdnr]; } (C-8.3.1.1 [xxxx/lmosem/hal/halintupt.c]) 代码(C-8.3.1.1)中首先对参数 ifdnr 进行检查,看它是否大于等于 osmach.mh_intfltnr,osmach 这个数据结构在前面已经写过了,并且我们知道 intfltdsc_t 类型数组的个数就是放在 osmach.mh_intfltnr 中的,而 intfltdsc_t 类型数组的首地址则放在 osmach.mh_intfltdsc 中。 中断来了要获取中断源,即是哪个中断源发生了中断,如何获取中断源呢,前面知道我 们所用的计算平台上的中断控制器中有个中断偏移寄存器,它里面的数据正是表示哪个中断 源发生了中断,中断发生后读取它就行了,如代码(C-8.3.1.2)所示。 uint_t hal_retn_intnr() { return (uint_t)hal_io32_read(INTOFFSET_R); } (C-8.3.1.2 [xxxx/lmosem/hal/halintupt.c]) 代码(C-8.3.1.2)中的 hal_io32_read 函数在前面章节中已经写过了,INTOFFSET_R 宏正是中 断偏移寄存器的地址 0x4a000014。代码很简单不必多说。 中断处理完了,必须要清除这个中断源在中断挂起寄存器中的相关位,它才能响应下一 次中断。所以要写一个清除中断源挂起寄存器的函数,如代码(C-8.3.1.3)所示。 drvstus_t hal_clear_srcpnd(uint_t ifdnr) { u32_t inttmp; uint_t flg; uint_t phylinenr; intfltdsc_t* ifdp=hal_retn_intfltdsc(ifdnr); if(ifdp==NULL) { return DFCERRSTUS; } flg=ifdp->i_flg&0xff; phylinenr=ifdp->i_pndbitnr; if(flg==MINT_FLG) { inttmp=(1<i_flg&0xff; phylinenr=ifdp->i_pndbitnr; if(flg==MINT_FLG) { inttmp=hal_io32_read(INTMSK_R); inttmp&=(~(1<i_flg&0xff; phylinenr=ifdp->i_pndbitnr; if(flg==MINT_FLG) { inttmp=hal_io32_read(INTMSK_R); inttmp|=(1<r0); printfk("USR_REG r1:%x\n\r",intstkp->r1); printfk("USR_REG r2:%x\n\r",intstkp->r2); printfk("USR_REG r3:%x\n\r",intstkp->r3); printfk("USR_REG r4:%x\n\r",intstkp->r4); printfk("USR_REG r5:%x\n\r",intstkp->r5); printfk("USR_REG r6:%x\n\r",intstkp->r6); printfk("USR_REG r7:%x\n\r",intstkp->r7); printfk("USR_REG r8:%x\n\r",intstkp->r8); printfk("USR_REG r9:%x\n\r",intstkp->r9); printfk("USR_REG r10:%x\n\r",intstkp->r10); printfk("USR_REG r11:%x\n\r",intstkp->r11); printfk("USR_REG r12:%x\n\r",intstkp->r12); printfk("USR_REG r13:%x\n\r",intstkp->r13); printfk("USR_REG r14:%x\n\r",intstkp->r14); printfk("SVE_REG lr:%x\n\r",intstkp->s_lr); printfk("SVE_REG spsr:%x\n\r",intstkp->s_spsr); //下面打印了当前进程的一些信息,先不管它,只看上面打印寄存器值的代码就行了 printfk("CSP_REG sp:%x INTPND:%x\n\r", hal_read_currmodesp(),hal_io32_read(INTPND_R)); printfk("CCR_REG cpsr:%x INTOFST:%x\n\r",hal_read_cpuflg(),hal_retn_intnr()); thread_t* prev=krlsched_retn_currthread(); printfk("CURR_THREAD:%x CURR_THREAD_KSTKTOP:%x\n\r", (uint_t)prev,(uint_t)prev->td_krlstktop); return; } 代码(C-8.3.4.2 [xxxx/lmosem/hal/intabtdistr.c]) 代码(C-8.3.4.2)中,主要调用 printfk 函数完成打印的,这个函数不必多说,关键是要理解 intstkregs_t 数据结构。intstkregs_t 数据结构定义在 cpu_t.h 文件中,如代码(C-8.3.4.3)所 示。 typedef struct s_INTSTKREGS { reg_t s_spsr;//相当于发生异常前的 CPSR reg_t c_lr;//当前 CPU 工作模式下的 lr reg_t r0; reg_t r1; reg_t r2; reg_t r3; reg_t r4; reg_t r5; reg_t r6; reg_t r7; reg_t r8; reg_t r9; reg_t r10; reg_t r11; reg_t r12; reg_t r13; reg_t r14; reg_t s_lr; //相当于发生异常前的 PC }intstkregs_t; 代码(C-8.3.4.3 [xxxx/lmosem/include/halinc/cpu_t.h]) 代码(C-8.3.4.3)中的 reg_t 类型就是 u32_t 类型。hal_dbug_print_reg 函数的参数,是调用 它的异常分配器函数的参数 sframe,直接传递进来的,那么 sframe 又是谁传递的呢,返回 前一小节保存 CPU 寄存器值的代码中,它在调用一些分配器函数之前都把内核栈指针寄存 器 sp,赋给了 r0 或者 r1 寄存器,从前面的章节中知道 ARM 体系下的 C 语言调用约定,是 用 r0~r3 四个寄存器传递前四个参数的,自然 sp 中的值就成了 intstkregs_t 类型的指针,那 么自然栈中数据的格式,也就是和 intstkregs_t 数据结构是一致的。 再来看看 hal_irq_distr 函数,它是设备中断分配器函数,设备中断的处理就是从这个函 数开始的,所以必须要写好它。它首先调用 hal_retn_intnr 函数获取中断源,即是哪个设备 发生了中断,然后用 switch 语句,依次判断该中断源下是否有子中断源或者外部中断源,如 果有就进一步调用相关的函数来处理。最后检查系统是否需要执行进程调度。这里先不深入 到其内部,具体的中断处理后面再探索,这里理解框架即可。 从前面的代码看,中断处理才刚刚开始,下面就去确定具体的中断源…… 8.3.5 确定中断源 从前面的章节中了解到,s3c2440a 中的中断控制器上有一些中断源,是采用级联方式连 接的,即主中断源上可能连接着几个子中断源或者外部中断源。而中断控制器中的中断偏移 寄存器只能呈现是哪个主中断源发生了中断,所以这给中断处理程序带来了麻烦。 上一小节中,hal_irq_distr 函数已经调用 hal_retn_intnr 函数获取了主中断源,可是有了 主中断源后,并不能立即进行中断的处理,还要看看这个主中断源下有没有子中断源或者外 部中断源,如果有,就要进一步确定是哪个子中断源或者是哪个外部中断源之后,再才能进 行中断处理。 下面就去实现这部分的代码。在这里依然列出 IRQ 中断分配器中的框架代码,便于观 察,如代码(C-8.3.5.1)所示。 void hal_irq_distr(void* sframe) { uint_t intoset=hal_retn_intnr();//获取主中断源 switch(intoset)//依次判断和处理,子中断源和外部中断源 { case EINT4_7: hal_eint_distr(sframe,intoset,EI4_7_PNDBTS,EI4_7_PNDBTE); break; case EINT8_23: hal_eint_distr(sframe,intoset,EI8_23_PNDBTS,EI8_23_PNDBTE); break; case INT_CAM: hal_sint_distr(sframe,intoset,ICAM_PNDBTS,ICAM_PNDBTE); break; case INT_WDT_AC97: hal_sint_distr(sframe,intoset,IACWDT_PNDBTS,IACWDT_PNDBTE); break; case INT_UART2: hal_sint_distr(sframe,intoset,IUART2_PNDBTS,IUART2_PNDBTE); break; case INT_UART1: hal_sint_distr(sframe,intoset,IUART1_PNDBTS,IUART1_PNDBTE); break; case INT_UART0: hal_sint_distr(sframe,intoset,IUART0_PNDBTS,IUART0_PNDBTE); break; case INT_ADC: hal_sint_distr(sframe,intoset,IADC_PNDBTS,IADC_PNDBTE); break; default: hal_int_distr(sframe,intoset); break; } hal_clear_intpnd(intoset);//清除主中断源挂起寄存器中的相关位 krlsched_chkneed_pmptsched();//检查是否需要进程调度,这里不必清楚该函数 return; } 代码(C-8.3.5.1 [xxxx/lmosem/hal/intabtdistr.c]) 代码(C-8.3.5.1)中 hal_irq_distr 函数首先获取主中断源,hal_retn_intnr 函数就是读取中断 偏移寄存器的。因为主中断源上只有几个中断源下,级联的有外部中断源和子中断源,所以 可以用 switch 语句,如果当前主中断源下,级联的是外部中断源就调用 hal_eint_distr 函数 来处理,如果级联的是子中断源就调用 hal_sint_distr 函数来处理,否则就调用 hal_int_distr 函数来处理,下面我们分别写好这三个函数。 先来看看遇到外部中断源后,该如何处理,当 intoset 等于 EINT4_7 或者 EINT8_23 时, 就调用 hal_eint_distr 函数来处理,我们来写好这个函数,如代码(C-8.3.5.2)所示。 void hal_eint_distr(void* sframe,uint_t mintnr,uint_t pndbts,uint_t pndbte) { u32_t pnd=hal_io32_read(EINTPEND_R);//读取外部中断源挂起寄存器 pnd&=EINTPEND_BITS_MASK;//处理掉无关的位,参阅外部中断源挂起寄存器 for(uint_t bi=pndbts;bi>bi)&1)==1)//确定一个外部中断源上有无中断,一旦有了中断,相应的外 部中断源挂起寄存器中的位就会置 1 { hal_run_intflthandle(EINT_IFDNR(bi),sframe); hal_clear_srcpnd(EINT_IFDNR(bi)); } } return; } 代码(C-8.3.5.2 [xxxx/lmosem/hal/intabtdistr.c]) hal_eint_distr 函数首先读取外部中断源挂起寄存器并进行处理,所谓处理也就是把 pnd 中无 关的位清 0,因为外部中断源挂起寄存器中有许多位是保留的。处理好了之后,就开始运行 一个循环,循环 pndbts 到 pndbte 次,每循环一次,让 pnd 右移 bi 位后与上 1,并且判断其 结果是否为 1。说白了,这个循环就是扫描 pnd 中 pndbts 到 pndbte 位段中所有为 1 的位。 pndbts 和 pndbte 是作为函数的参数传递进来的,对于 EINT4_7,传递进来的是 EI4_7_PNDBTS (值为 4)和 EI4_7_PNDBTE(值为 8),当我们回过头去看看外部中断源挂起寄存器时,一 切都明白了,而扫描 pnd 相关位段就是扫描外部中断源挂起寄存器中的相关位段,为什么要 这样,这是因为中断控制器,不能唯一确定的给出是哪一个具体的外部中断源,发生了中断, 所以,只能通过扫描当前主中断源,对应的外部中断源挂起寄存器中,所有的相关位段,可 是,又为什么要扫描与此对应的所有位段呢,因为同一时刻可能有多个已经挂起的中断源, 所以不能漏掉,要依次调用它们的中断处理程序。先不用管 if 语句内部的函数是干什么的, 这里只要明白这个循环和右移语句的含意就行了。 处理好了外部中断源,还有子中断源呢,例如代码(C-8.3.5.1)中,case 到 INT_UART0 后就调用 hal_sint_distr 函数来处理,下面就来写好这个函数,如代码(C-8.3.5.3)所示。 void hal_sint_distr(void* sframe,uint_t mintnr,uint_t pndbts,uint_t pndbte) { u32_t pnd=hal_io32_read(SUBSRCPND_R); //读取子中断源挂起寄存器 pnd&=SUBSRCPND_BITS_MASK; //处理掉无关的位,参阅外部中断源挂起寄存器 for(uint_t bi=pndbts;bi>bi)&1)==1) //确定一个子中断源上有无中断,一旦有了中断,相应的子 中断源挂起寄存器中的位就会置 1 {// hal_run_intflthandle(SINT_IFDNR(bi),sframe); hal_clear_srcpnd(SINT_IFDNR(bi)); } } return; } 代码(C-8.3.5.3 [xxxx/lmosem/hal/intabtdistr.c]) 哇,和 hal_eint_distr 函数的原理一样,也是读取相应的中断源挂起寄存器,然后用循环扫描 其位段,来确定这些中断源上是否发生了中断。只不过这里读取的是子中断源挂起寄存器, 还有相应的位段也不同,对于 INT_UART0,所对应的子中断源挂起寄存器中的位段是 IUART0_PNDBTS(值为 0)和 IUART0_PNDBTE(值为 3),如果不明白,回到前面看看子中断 挂起寄存器中,第一个串口的三个中断源对应的位段就明白了。 如果既不是子中断源也不是外部中断源,那就是主中断源。如果是主中断源,就调用 hal_int_distr 函数来处理,下面就去写好它,如代码(C-8.3.5.4)所示。 void hal_int_distr(void* sframe,uint_t mintnr) { hal_run_intflthandle(MINT_IFDNR(mintnr),sframe); hal_clear_srcpnd(MINT_IFDNR(mintnr)); return; } 代码(C-8.3.5.4 [xxxx/lmosem/hal/intabtdistr.c]) hal_int_distr 这个函数非常简单,直接用 hal_irq_distr 函数传递进来的 intoset 参数,因为这 个参数就表示是哪个主中断源,直接使用就行了。 到这里,从硬件的角度来说,只是确定了实际发生中断的中断源,并未调用实际的中断 处理程序,但同时也发现,在 hal_eint_distr、hal_sint_distr、hal_int_distr 函数中,都调用了 hal_run_intflthandle 函数,就是在这个函数中调用具体的中断处理程序的,运行完中断处理 程序后,会调用 hal_clear_srcpnd 函数,清除相应中断源挂起寄存器中的相关位,使之能响 应下一次中断。 至于 hal_run_intflthandle 函数,是如何调用具体的中断处理程序的,下面我们就一起去 实现这个函数。 8.3.6 调用中断处理例程 确定了产生中断的中断源之后,就要调用与此对应的中断处理程序。中断处理程序在哪 里呢,读者可能对 intfltdsc_t 和 intserdsc_t 这两个数据结构还记忆犹新,中断处理程序,实 际上是个函数,这个函数的地址就保存在 intserdsc_t 中。而 intserdsc_t 是挂在 intfltdsc_t 中 的。一个 intfltdsc_t 中可能有多个 intserdsc_t,因为多个设备可能共享同一根中断信号线。 根据以上描述,调用实际中断处理函数的过程如下: 1. 根据中断源找到中断源描述符 intfltdsc_t 数据结构。 2. 在找到的 intfltdsc_t 中查找 intserdsc_t,找到一个 intserdsc_t 就调用其中的中断处 理函数。 3. 回到第 2 步,继续在刚才的 intfltdsc_t 中查找下一个 intserdsc_t 并执行其中的中断 处理函数,直到查找完所有的 intserdsc_t。 有了上面的步骤,写代码就容易多了,下面就去实现这个 hal_run_intflthandle 函数,如 代码(C-8.3.6.1)所示。 void hal_run_intflthandle(uint_t ifdnr,void* sframe) { intserdsc_t* isdscp; list_h_t* lst; intfltdsc_t* ifdscp=hal_retn_intfltdsc(ifdnr); if(ifdscp==NULL) {//如果没有中断源描述符就死机 hal_sysdie("hal_run_intfdsc err"); return; } list_for_each(lst,&ifdscp->i_serlist)//遍历 intfltdsc_t 中的 i_serlist 链表 { isdscp=list_entry(lst,intserdsc_t,s_list);//获取具体的 intserdsc_t 的指针 isdscp->s_handle(ifdnr,isdscp->s_device,sframe);//调用具体的中断处理函数 } return; } 代码(C-8.3.6.1 [xxxx/lmosem/hal/intabtdistr.c]) 代码(C-8.3.6.1)中,首先调用 hal_retn_intfltdsc 返回一个中断源描述符,然后遍历 intfltdsc_t 中的 i_serlist 链表,获取其中的 intserdsc_t 的指针,最后调用 intserdsc_t 的 s_handle 中的中 断处理函数。可是 ifdnr 是哪里来的,在前一小节中我们看到,它是由中断源挂起寄存器中 的位号再经过几个宏转换后,传递给 hal_run_intflthandle 函数的。这几个宏如代码(C-8.3.6.2) 所示。 #define MINT_OFFSET 0 #define SINT_OFFSET 32 #define EINT_OFFSET 47 …… #define MINT_IFDNR(x) (x+MINT_OFFSET)//转换主中断源描述符在其数组中的下标 #define SINT_IFDNR(x) (x+SINT_OFFSET) //转换子中断源描述符在其数组中的下标 #define EINT_IFDNR(x) (x+EINT_OFFSET) //转换外部中断源描述符在其数组中的下标 代码(C-8.3.6.2 [xxxx/lmosem/include/halinc/halintupt_t.h]) 对代码(C-8.3.6.2),回到初始化中断章节那里,看看是如何把外部中断源、子中断源、主中 断源平级映射成一个 intfltdsc_t 类型数组的,就明白了。 那么对于几个设备共享一条中断信号线的情况,究竟是哪个设备产生了中断,这应该交 给设备中断处理程序去处理,内核只需要依次调用所有挂在这个中断源描述符上的中断处理 函数即可。 好了,到这里 hal_run_intflthandle 函数的完成,意味着 LMOSEM 内核的中断处理的部 分已经完成,具体的中断处理函数是设备驱动程序编写的,操作系统内核不可能对每个设备 都了如指掌。 我们发现,LMOSEM 内核的中断处理框架,很好的解决了,一个中断源对应多个中断(因 为中断源的级连),也解决了一个中断源对应多个设备的情况。但是,好像少了点什么,少 了点什么呢,下面继续…… 8.4 安装中断回调例程 一个计算平台上有许多设备,每个设备都不同,操作系统内核开发者,不可能了解计算 平台上的所有设备的细节。例如,不同设备发生了中断,虽然对中断的响应是相同的,但是 对于一个设备发生中断后的处理,各有不同。所以把中断的响应交给内核,把具体一个设备 的中断处理交给设备驱动程序。 既然中断处理函数是由驱动程序编写的,那么就要给驱动程序提供一个接口,让其可以 安装自己设备的中断处理函数,或者是安装中断回调例程。下面我们就去干这件事。 8.4.1 设备回调例程的安装接口 为什么要提供设备回调例程的安装接口,刚刚说了是因为设备的种类千差万别,同时为 了操作系统内核更具有通用性和扩展性,所以把具体设备的中断处理交给具体设备的驱动程 序,这样如果计算平台更换了更先进的设备,只需要加载新版本的驱动程序就行了。 前面章节中,我们已经设计了 intserdsc_t 数据结构,这个数据结构中有个域就是用来保 存设备回调例程地址的。于是,安装设备回调例程的方法如下: 1. 提供一些信息:设备描述符(后面会有介绍)、设备回调例程地址、设备中断源对应 的中断源描述符数组的下标。之所以要提供设备描述符,是因为一个设备中断总是 与具体的设备有关。 2. 分配并实例化一个 intserdsc_t 数据结构变量,把以上等信息填入其中。 3. 把这个 intserdsc_t 数据结构变量,挂载到对应的 intfltdsc_t 中断源描述符中。 有了以上的方法,写起代码时就容易多了,但是别急着写代码,对中断管理这个组件来 说,它分为两个部分,一个部分在内核的硬件相关层(hal)中,另一个部分在内核的功能层 中,对于设备回调例程的安装接口,我们应该把它放在内核功能层中。所以先在 “xxxx/lmosem/kernel”目录下新建一个 C 程序文件:krlintupt.c,和相应的头文件,并按照前 面章节中的方法组织好这些文件。 有了 krlintupt.c 文件,就可以开始写代码实现上述步骤了,如代码(C-8.4.1.1)所示。 intserdsc_t* krladd_irqhandle(void* device,intflthandle_t handle,uint_t phyiline) { if(device==NULL||handle==NULL) {//如果设备描述符或者中断回调函数地址其中一个为 NULL,则返回 NULL 表示出错 return NULL; } intfltdsc_t* intp=hal_retn_intfltdsc(phyiline);//返回中断源描述符 if(intp==NULL) { return NULL;//如果没有中断源描述符,则返回 NULL 表示出错 } intserdsc_t* serdscp=(intserdsc_t*)krlnew(sizeof(intserdsc_t));//分配一个 intserdsc_t 数 据结构变量的空间 if(serdscp==NULL) { return NULL; //如果分配空间失败,则返回 NULL 表示出错 } intserdsc_t_init(serdscp,0,intp,device,handle);//初始化该 intserdsc_t 数据结构变量 if(hal_add_ihandle(intp,serdscp)==FALSE) {//如果 intserdsc_t 加入到 intfltdsc_t 中失败,则释放 intserdsc_t 数据结构变量的空间 if(krldelete((adr_t)serdscp,sizeof(intserdsc_t))==FALSE) {//内存空间释放失败时,就死机,不过这种情况不太可能发生 hal_sysdie("krladd_irqhandle ERR"); } return NULL; } return serdscp;//返回 intserdsc_t 变量的地址,表示成功 } 代码(C-8.4.1.1 [xxxx/lmosem/kernel/krlintupt.c]) 代码(C-8.4.1.1)中的注释已经够明白了,首先是对参数进行了检察,然后以 phyiline 为参 数返回一个中断源描述符,调用内存管理接口动态分配了一个 intserdsc_t 数据结构变量的空 间,接着调用 intserdsc_t_init 函数对这个空间进行了初始化,因为动态分配的内存空间必须 要初始化。而 hal_add_ihandle 才是使 intserdsc_t 和 intfltdsc_t 结合在一起的函数。可是函数 在哪儿呢,还没写呢,现在就去写好它们,如代码(C-8.4.1.2)所示。 void intserdsc_t_init(intserdsc_t* initp , u32_t flg,intfltdsc_t* intfltp,void* device,intflthandle_t handle) { list_init(&initp->s_list); list_init(&initp->s_indevlst);//初始化两个链表 initp->s_flg=flg; initp->s_intfltp=intfltp;//把它属于哪个 intfltdsc_t 的地址,放进去 initp->s_indx=0; initp->s_device=device;//把它属于哪个设备描述符的地址,放进去 initp->s_handle=handle;//把回调函数的地址,放进去 return; } bool_t hal_add_ihandle(intfltdsc_t* intdscp,intserdsc_t* serdscp) { if(intdscp==NULL||serdscp==NULL) {//对 intfltdsc_t、intserdsc_t 进行检察,有一个为 NULL 则返回 FALSE 表示失败 return FALSE; } cpuflg_t cpuflg; hal_spinlock_saveflg_cli(&intdscp->i_lock, &cpuflg);//加锁并关中断 list_add(&serdscp->s_list,&intdscp->i_serlist);//把 intserdsc_t 挂入 i_serlist 链表中 intdscp->i_sernr++;//增加表示 intserdsc_t 个数的计数变量 hal_spinunlock_restflg_sti(&intdscp->i_lock,&cpuflg); //开锁并恢复中断 return TRUE;//返回 TRUE 表示成功 } 代码(C-8.4.1.2 [xxxx/lmosem/hal/halintupt.c]) intserdsc_t_init、hal_add_ihandle 这两个函数非常简单,不过有一点需要注意,在把 intserdsc_t 挂入 intfltdsc_t 的 i_serlist 链表中的时候要加锁,还是前面多次提到的,可能有多个设备共 享同一根中断信号线,也许在同一时刻下,内核正在开始调用一个设备的中断处理函数,而 另一个设备驱动程序正在向其中加入另一个设备的中断处理程序,读者可能会说目前情况下 不可能,是的,可是如果 LMOSEM 内核将来要支持内核级可抢占、或者多 CPU 时,问题就 来了,所以不如在写每个函数时,多下点功夫。 为了以后让驱动程序最少化调用内核硬件相关层的函数,因为这里的函数在不同的计算 平台下可能会改变,一旦改变,驱动程序就得重写了。读者可能会疑问,计算平台都改变了, 旧的驱动程序还有什么用,因为不同的计算平台可能会用到相同的设备。 前面章节中,可以发现,中断控制器可以开启、关闭单个设备的中断信号通路,所以我 们还要写两个函数:开、关具体设备中断信号通路的接口,如代码(C-8.4.1.3)所示。 drvstus_t krlenable_intline(uint_t ifdnr) { return hal_enable_intline(ifdnr); } drvstus_t krldisable_intline(uint_t ifdnr) { return hal_disable_intline(ifdnr); } 代码(C-8.4.1.3 [xxxx/lmosem/kernel/krlintupt.c]) 代码(C-8.4.1.3)中的 krlenable_intline、krldisable_intline 两个函数,是直接对 LMOSEM 内核 硬件相关层的 hal_enable_intline、hal_disable_intline 两个函数的封装,而这两个函数,在前 面章节中已经介绍过了。 一直以来,我们都是在写中断管理的代码,从来没有测试代码的正确性,那是因为我们 还没有驱动模型,不能装载设备驱动程序,从内核的角度来说,还没有设备呢,没有设备哪 来的中断,没有中断怎么测试中断管理代码呢,等后面章节中完成了驱动模型,写好了第一 个设备驱动程序,再一起来测试中断管理的代码。 到这里,对于驱动程序安装其设备中断处理的回调函数,和用于开启或者关闭设备中断 信号通路的接口就完成了,同时也宣告 LMOSEM 内核的中断管理组件已经完成了,或许还 有许多不完善的地方,但是基本上可以工作了,对于探索操作系统内核的原理,足够了…… 8.5 小结 又到了小结,意味着中断管理这一章节的结束,从此我们捣腾的操作系统内核 LMOSEM, 可以响应和处理异常、中断、陷入了。 异常、中断、陷入,广义上都可以理解为“突发事件”。从前面章节中就明白了,为了 处理这种“突发事件”,CPU 和中断控制器,共同提供了一些基础的硬件机制。 但处理这种“突发事件”,硬件只是自动完成了最基础、最必要的动作。至于剩下的事 情就是软件该做的,不管是异常还是中断或是陷入,软件首要的任务就是保存发生这种“突 发事件”之前 CPU 寄存器中的值,由于 CPU 的实现和 LMOSEM 内核的设计,保存 CPU 寄存 器中的值,我们是大费周折。接着是调用异常、中断、陷入的分发器函数,对于中断,还需 要进一步的确定设备中断源,然后调用具体的设备中断处理函数。但由于设备之间差别很大, 我们把响应中断的部分交给内核,把具体设备的中断处理,交给设备驱动程序,并给驱动程 序,提供了安装中断处理函数的接口,同时为了操作中断控制器,还写了一些其它的辅助函 数。 以上就是这一章节中我们实现的功能,虽不多,但至关重要。LMOSEM 内核的又一个基 础设施,被实现了,虽然简单,但能工作,等有时间了再来完善它也不迟。现在休息一下, 还有很多事情等着我们去做呢……

    Top_arrow
    回到顶部
    EEWORLD下载中心所有资源均来自网友分享,如有侵权,请发送举报邮件到客服邮箱bbs_service@eeworld.com.cn 或通过站内短信息或QQ:273568022联系管理员 高进,我们会尽快处理。