第一章

Size: px
Start display at page:

Download "第一章"

Transcription

1 第一章引言 嵌入式系统概述 嵌入式系统历史 嵌入式系统定义 嵌入式系统的应用 嵌入式系统的特点 嵌入式处理器 嵌入式微控制器 嵌入式微处理器 嵌入式 DSP 处理器 嵌入式片上系统 (System On Chip) 嵌入式操作系统 嵌入式系统设计 嵌入式系统开发流程 嵌入式系统开发模式 小结 习题 第二章 ARM 微处理器结构及其指令系统 ARM 微处理器简介 ARM 微处理器的历史发展 ARM 微处理器的特点和应用 ARM 微处理器系列 ARM 编程模型 ARM 微处理器模式 ARM 寄存器 ARM 异常中断处理 ARM 存储体系 ARM/Thumb 指令系统 ARM 指令的编码格式 ARM 指令寻址方式 ARM 指令集 Thumb 指令及应用 ARM 汇编语言程序设计 ARM 汇编器所支持的伪指令 汇编语言的程序结构 小结 习题 第三章嵌入式 Linux 开发基础 GNU GCC 集成编译环境... 63

2 3.1.1 GNU GCC 构成 GNU GCC 处理过程 C/C++ 交叉编译器 arm-elf-gcc 交叉汇编器 arm-elf-as 交叉链接器 arm-elf-ld Makefile 原理与使用 Makefile 原理 Makefile 语法 应用程序调试 ARM 调试方法简介 gdb 本地调试 gdb 远程调试 小结 习题 第四章嵌入式 linux 操作系统 嵌入式操作系统简介 WINCE VxWorks 嵌入式 LINUX 嵌入式 uclinux uclinux 简介 uclinux 体系结构 uclinux 存储管理 嵌入式 Linux 文件系统 文件系统概述 ROMFS 文件系统 JFFS 与 JFFS2 文件系统 嵌入式根文件系统内容 uclinux 进程管理 嵌入式操作系统裁减与移植 嵌入式开发环境建立 嵌入式 uclinux 裁减 嵌入式 ucli nu x 在 S3C44B0X 上的移植 uclinux 启动分析 小结 习题 第五章嵌入式设备驱动及应用开发 嵌入式系统设备驱动开发 设备驱动程序的作用

3 5.1.2 设备和模块的分类 构建模块 模块编程 驱动模块设计 嵌入式应用程序开发 嵌入式应用程序开发流程 uclinux 下邮件收发系统应用程序开发实例 小结 习题 第六章 S3C44B0X 嵌入式微处理器 S3C44B0X 芯片概述 S3C44B0X 内部结构图 芯片引脚定义 小结 习题 第 7 章 S3C44B0X 中断管理 S3C44B0X 中断管理概述 简介 中断控制器 中断控制器相关寄存器 S3C44B0X 平台下异常中断向量设计 向量中断模式 非向量中断模式 外部中断实例 小结 习题 第 8 章 S3C44B0X 存储设计 S3C44B0X 存储系统概述 S3C44B0X 存储系统特性 大小端模式选择 Bank0 总线宽度 SDRAM 设计 SDRAM 器件原理 电路设计 编程实例 Flash 设计 Flash 概述 Nor Flash NAND Flash

4 小结 习题 第 9 章串口设计 S3C44B0X 串口概述 UART 操作 数据发送 数据接收 自动流控制 (AFC) 非自动流控制 ( 通过软件控制 nrts 和 ncts) 调制解调器接口 UART 寄存器 串行接口电路设计及编程 串行接口电路设计 串口功能函数设计 小结 习题 第 10 章 LCD 接口及其应用 LCD 简介 S3C44B0X 的内部 LCD 控制器 S3C44B0X 的内部 LCD 控制器简介 LCD 控制器的外部接口信号 LCD 控制器的操作 LCD 接口电路设计 设计思路 硬件连接 LCD 驱动设计 初始化 图像数据的显示 LCD 显示简单应用函数 小结 习题 第 11 章触摸屏设计 触摸屏原理 触摸屏驱动设计 设计思路 驱动设计 触摸屏应用程序设计 界面的设计

5 触摸操作处理函数 小结 第 12 章网络设计及编程 网络概述 OSI 七层模型 TCP/IP 模型 以太网接口设计 以太网概述 以太网控制器 RTL8019AS 以太网接口电路设计 RTL8019AS 底层函数编程 Linux 操作系统下 RTL8019AS 驱动程序设计 初始化函数 模块卸载函数 打开函数 关闭函数 发送函数 接收函数 第 13 章嵌入式 BOOTLOADER 设计 BOOTLOADER 概述 嵌入式系统软件架构 pc 机引导加载概述 嵌入式系统 Boot Loader 概念 Linux 平台下各式各样的 Boot Loader BOOTLOADER 设计流程 bootloader 设计关键技术分析 Boot Loader 的使用 环境设置 bootloader 对串口终端的设置 使用 xmoden,tftp 协议下载内核镜像, 操作 flash 小结 习题 附录 A Linux 系统常用命令 A.1 Linux 文件的复制 删除和移动命令 A.2 Linux 目录的创建与删除命令 A.3 Linux 文本处理命令 A.4 Linux 备份与压缩命令 A.5 Linux 改变文件或目录的访问权限命令 A.6 Linux 与用户有关的命令

6 A.7 网络命令

7 第一章引言 随着社会信息化的发展, 嵌入式产品已经走进人们的日常生活中, 比如, 小到 MP3 PDA 手机等微型数字产品, 大到智能家电 车载设备 航空航天等领域, 都可以看到嵌入式设备的影子 那么, 什么是嵌入式? 它有哪些特点? 本章将简单介绍嵌入式系统的一些基本概念 应用领域及特点, 引导读者进入嵌入式技术的殿堂 本章主要内容 : (1) 嵌入式系统概述 (2) 嵌入式微处理器简介 (3) 嵌入式操作系统简介 (4) 嵌入式系统设计方法 1.1 嵌入式系统概述 嵌入式系统历史 随着信息技术的飞速发展, 嵌入式技术已经成为微电子技术和计算机技术一个重要的分支, 同时也使得计算机的分类从以前的巨型机 大型机 小型机以及微机之分变为通用计算机和嵌入式系统之分 当今时代, 制造技术的集成化和小型化发展趋势越来越明显, 能否设计和生产出个性化需求的电子产品也将决定一个企业能否生存和继续发展 所有这些, 都决定了嵌入式技术必将成为后 PC 的主宰 嵌入式系统并不是一个新的概念, 其实早在 1970 年左右就有嵌入式系统应用的例子 当时的嵌入式系统并没有专门的操作系统, 它们只是为了实现某个控制功能, 使用简单的循环控制语句来完成 当应用系统设计越来越复杂, 功能越来越强大时, 传统的设计方法已经无法胜任, 这时随着通用操作系统的迅速发展, 人们将目光汇聚到设计专用的嵌入式操作系统来解决上述问题 因此, 从上世纪 80 年代开始, 出现了各种各样专用的嵌入式操作系统, 比较著名的有 Vxworks psos WinCE Linux 等等 嵌入式系统最大的特点是其具有专用性, 也就是说, 每一套嵌入式系统都有其特殊的应用场合与特定功能, 它们都是为了特定的目的而设计的, 而且往往还受环境 成本 体积 功耗等条件的制约, 需要最大限度的在硬件和软件上 量身定做, 以提高资源的利用率, 这也是嵌入式系统与通用计算机最大的一个区别

8 1.1.2 嵌入式系统定义 1.IEEE 定义 嵌入式系统并没有一个标准的定义,IEEE 是这样定义嵌入式系统 : 嵌入式系统是 控制 监视或者辅助设备 机器和车间运行的装置 ( 原文为 devices used to control, monitor, or assist the operation of equipment,machinery or plants) 从这个定义, 我们可以看出嵌入式系 统功能比较广泛 2. 国内定义不过,IEEE 的定义并不能够反应出嵌入式系统的本质特征 目前, 国内普遍采用下 列描述来定义嵌入式系统 : 嵌入式系统以应用为中心, 以计算机技术为基础, 软硬件可裁减, 适应应用系统对功能 可靠性 成本 体积 功耗严格要求的专用计算机系统 可以从下列几个方面来理解上述嵌入式系统的定义 : 嵌入式系统以应用为中心, 也就是说, 嵌入式系统必须与具体应用相结合才会有生命力, 这也是嵌入式技术诞生和发展的不竭动力的源泉 目前, 嵌入式应用的场合及其广泛, 金融 航天 通信 家电 医疗 工业控制以及军事等各个领域 都可以看到嵌入式技术应用的例子 嵌入式系统是一个技术高度密集型的综合体, 它包含了计算机技术 电子技术 半导体技术 通信技术 多媒体技术等等, 可以说, 嵌入式系统是各种科学技术 的集成系统 嵌入式系统包括软件和硬件两个组成部分, 可以涵盖机械和其它的附属装置 其中, 硬件部分涉及到芯片设计 存储设计以及各个接口部分的设计 ; 软件部分包 括专用嵌入式操作系统 硬件驱动 通信协议 图形界面等各个方面 嵌入式系统软硬件必须可裁减 做为一个嵌入式产品, 必须考虑到可靠性 成本 体积和功耗等各个方面因素, 另外, 很多用于工控的嵌入式产品, 还要求具有实 时性的特点 所以, 嵌入式设计时必须对多余的软件和硬件进行裁减, 使其最小化, 从而可以降低功耗 成本 体积, 提高整个系统的可靠性 通常情况下, 嵌入式系统的架构包含微处理 器 存储单元 接口单元和软件单元几个部分, 其中软件单元至少包含嵌入式操作系统和应用程序 当然嵌入式操作系统和底层硬件单元之间必 须要有相应驱动程序做为接口 典型嵌入式系统架构如图 1-1 所示 从图中可以看出, 嵌入式系统和普通微机在体系结构上 并没有本质的区别, 两者非常类似, 因此普通微机上开发程序的那些知识照样可以用在嵌入式系统开发上 当然嵌入式系统的微处理器和操作系 统具有专用性, 开发嵌入式系统应用软件时, 必须首先掌握这两个核心内容 软件部分 驱动 硬件部分 接口单元 应用程序 操作系统 驱动 驱动 微处理器 存储单元 图 1-1 典型嵌入式系统组成

9 1.1.3 嵌入式系统的应用 嵌入式技术具有非常广泛的应用场合, 无论在航天 军事 信息家电等各个领域, 人 们都会无时无刻的接触到嵌入式产品 1. 航天领域嵌入式系统在航空航天领域得到了非常广泛的应用, 比如在美国宇航局的 极地登陆 者 号 深空二号 和火星气候轨道器等登陆火星探测器上, 就采用了 VxWorks, 负责火星探测器全部飞行控制, 包括飞行纠正 载体自旋和降落时的高度控制等, 而且还负责数据收集和与地球的通信工作 2. 军事领域进入 90 年代以来, 随着高新技术的迅猛发展及其在军事领域的广泛应用, 武器装备在军事斗争和军队建设中的作用日益突出 各国在新军事革命的大潮中发展武器装备的一 个明显共性是, 利用现有信息技术的发展成果对现有的武器装备进行改造, 最终实现武器装备的智能化, 作战系统的网络化 例如, 美国战略核武器民兵 (MinuteMan) 系列导弹的导航系统就采用了较为先进的嵌入式技术 3. 信息家电及智能仪器 (1) 手机 PDA 类产品 : 图 1-2 给出的是采用 ecos 操作系统, 基于 MiniGUI 开发的高端智能手机的应用程序界面 图 1-2 基于 MiniGUI 开发的智能手持设备 (2) 仪表及控制系统 : 图 1-3 给出的是基于 Linux 和 MiniGUI 操作系统开发的数控系统 工业仪表及医疗仪器的界面

10 图 1-3 工业仪表及控制系统 嵌入式系统的特点 从嵌入式系统的定义可以看出嵌入式系统具有下面几个非常明显的特征 : 1. 微内核 由于嵌入式系统多用于手持设备和小型电子装置, 系统资源比较少, 所以内核较通用操作系统要小的多 比如经过裁减的嵌入式专用 linux 内核一般只有 200KB 左右, 而通用的 PC 机上的 linux 内核可能要达到几 MB 2. 专用性强嵌入式产品都是以特定应用为背景来进行设计的, 不同的任务实现, 往往设计人员会对原系统的软硬件进行较大的修改, 这和通用软件的 升级 是完全不同的概念 3. 可裁减嵌入式系统一般只是实现一个特定的功能, 同时它对最后产品的功耗 体积 成本都有着严格的要求, 这就需要能够进行软硬件的裁减, 把多余的软件和硬件部分裁减到, 以 达到设计的要求 4. 专用操作系统的支撑嵌入式系统的应用程序可以没有操作系统而直接在芯片上运行 ; 但是为了合理地调度 多任务, 利用系统资源 系统函数以及已有的函数接口, 用户必须选择某一专用嵌入式操作系统做为应用软件设计的底层开发平台, 这样才能保证程序执行的实时性 可靠性, 缩短项目开发周期, 提高产品的竞争力 5. 专用的开发工具和环境由于嵌入式系统本身并不具备自主开发能力, 即使设计完成以后, 用户通常也不能对其中的程序功能直接进行修改, 因此必须有一套开发工具和环境才能进行开发, 这些工具 和环境一般是基于通用计算机上的软硬件设备以及各种逻辑分析仪 信号示波器等 开发时有主机和目标机两个部分构成, 主机用于程序的开发, 目标机作为目标代码的执行载体, 在进行开发时, 往往需要在主机和目标机之间交替结合进行

11 1.2 嵌入式处理器 嵌入式系统由软件和硬件两个部分构成, 从硬件角度来说, 嵌入式处理器是嵌入式硬 件中最核心的部分 目前, 世界上具有嵌入式功能特点的处理器有 1000 多种, 分为嵌入式微控制器 (MCU) 嵌入式 DSP 处理器 嵌入式微处理器 (MPU) 嵌入式片上系统 (SOC) 几类 嵌入式微控制器 嵌入式微控制器实际上指的是单片机, 顾名思义, 就是将整个计算机系统集成到一块芯片中 嵌入式微控制器一般以某一种微处理器内核为核心, 芯片内部集成 ROM/EPROM RAM 总线 总线逻辑 定时 / 计数器 WatchDog I/O 串行口 脉宽调制输出 A/D D/A Flash RAM EEPROM 等各种必要功能和外设 为适应不同的应用需求, 一般一个系列的单片机具有多种衍生产品, 每种衍生产品的处理器内核都是一样的, 不同的是存储器和外设的配置及封装 这样可以使单片机最大限度地和应用需求相匹配, 功能不多不少, 从而减少功耗和成本 和嵌入式微处理器相比, 微控制器的最大特点是单片化, 体积大大减小, 从而使功耗和成本下降 可靠性提高 微控制器是目前嵌入式系统工业的主流之一 微控制器的片上外设资源一般比较丰富, 适合于控制, 因此称微控制器 嵌入式微控制器目前的品种和数量最多, 比较有代表性的通用系列包括 8051 P51XA MCS-251 MCS-96/196/296 C166/167 MC68HC05/11/12/ 等 另外还有许多半通用系列如 : 支持 USB 接口的 MCU 8XC930/931 C540 C541; 支持 I2C CAN-Bus LCD 及众多专用 MCU 和兼容系列 目前 MCU 占嵌入式系统约 70% 的市场份额 特别值得注意的是近年来提供 X86 微处理器的著名厂商 AMD 公司, 将 Am186CC/CH/CU 等嵌入式处理器称之为 Microcontroller, MOTOROLA 公司把以 Power PC 为基础的 PPC505 和 PPC555 亦列入单片机行列,TI 公司亦将其 TMS320C2XXX 系列 DSP 做为 MCU 进行推广 嵌入式微处理器 嵌入式微处理器由通用计算机 CPU 演变而来, 通常情况下, 嵌入式微处理器都是 32 位以上的处理器, 具有体积小 重量轻 成本低 可靠性高等优点 目前主要的嵌入式微处理器类型有如下几种 : 1.ARM/StrongARM ARM(Advanced RISC Machine) 公司是全球领先的 16/32 位 RISC 微处理器芯片设计商 ARM 公司并没有自己的芯片生产线, 它专注于设计高性能 低成本 低功耗的嵌入式微

12 处理器核, 同时 ARM 公司在全球有很多合作伙伴, 它们通过购买 ARM 公司的芯片设计技术 专利等方式, 在配上自己的外围电路, 生产出各具特色的嵌入式微处理器芯片 在嵌入式微处理器市场中,ARM 微处理器芯片占据绝对的统治地位, 目前全球有 75 % 的市场份额被 ARM 微处理器所垄断 ARM 微处理器有三个主要特点 : 小体积 低功耗 低成本和高性能 ;16/32 位双指令集 ; 全球合作伙伴众多 ARM 处理器目前包括下面几个系列的处理器产品以及其它厂商实现的基于 ARM 体系架构的处理器 ARM7 系列 ARM9 系列 ARM9E 系列 ARM10E 系列 SecurCore 系列 Intel 的 Xscal Intel 的 StrongARM 2. MIPS MIPS 是最早的, 最成功的 RISC(Reduced Instruction Set Computer) 处理器之一, 起源于 Stanford Univ 的电机系 其创始人 John L. Hennessy 在 1984 年在硅谷创立了 MIPS INC. 公司 ( John L. Hennessy 目前是 Stanford Univ. 的校长, 在此之前, 他是 Stanford 电子工程学院的 Dean CS 专业的学生都知道两本著名的书 : Computer Organization and Design : The Hardware/Software Interface 和 Computer Architecture : A Quantitative Approach, 其 Co-author 就是 Hennessy MIPS 的名字为 Microcomputer without interlocked pipeline stages" 的缩写, 另外一个通常的非正式的说法是 Millions of instructions per second", 从这里我们可以看出 MIPS 最主要的特点是架构简洁和运算速度较快 MIPS 是高效精简指令集计算机 (RISC) 体系结构中最优雅的一种, 这可以从 MIPS 对于后来研制的新型体系结构比如 DEC 的 Alpha 和 HP 的 Precision 产生的强烈影响看出来 虽然自身的优雅设计并不能保证在充满竞争的市场上长盛不衰, 但是 MIPS 微处理器却经 常能在处理器的每个技术发展阶段保持速度最快的同时保持设计的简洁 相对的简洁对于 MIPS 来说是一种商业需要,MIPS 起源于一个学术研究项目, 该项目的设计小组连同几个半导体厂商合伙人希望能制造出芯片并拿到市场上去卖 结果是该结构得到了工业领域内最大范围的具有影响力的制造商们的支持, 从生产专用集成电路核心 (ASIC Cores) 的厂家 (LSI Logic,Toshiba, Philips, NEC) 到生产低成本 CPU 的厂家 (NEC, Toshiba, 和 IDT), 从低端 64 位处理器生产厂家 (IDT, NKK, NEC) 到高端 64 位处理器生产厂家 (NEC, Toshiba 和 IDT) 都对 MIPS 给予了强有力的重视和支持 MIPS CPU 是一种 RISC 结构的 CPU, 它产生于一个特殊的蓬勃发展的学术研究与开发时期 RISC( 精简指令集计算机 ) 是一个极有吸引力的缩写名词, 与很多这类名次相似, 可能遮掩的真实含义超过了它所揭示的 但是它的确对于那些在 1986 到 1989 年之间投放市场的新型 CPU 体系结构提供了一个有用的标识名, 这些新型体系结构的非凡的性能主要归功于几年前的几个具有开创性的研究项目所产生的思想 有人曾说 :" 任何在 1984 年以后定义的计算机体系结构都是 RISC"; 虽然这是对于工业领域广泛使用这个缩写名词的嘲

13 讽, 但是这个说法也的确是真实的 1984 年以后没有任何一款计算机能够忽视 RISC 先驱者们的工作 在斯坦福大学开展的 MIPS 项目是这些具有开创性的项目中的一个 该项目命名为 MIPS, 主要是无内锁流水段微型计算机关键短语的缩略, 同时也是 " 每秒百万条指令数 " 的双关语 斯坦福研究小组的工作表明虽然流水线已经是一种众所周知的技术, 但是以前的 体系结构对它研究的远远不够, 流水线技术其实能够被更好的利用, 尤其是当结合了 1980 年的硅材料设计水平时 从 MIPS 处理器发明到现在, 已经被广泛应用于宽带接入 路由器 调制解调器 游 戏机等各个领域 ( 如图 1-4 所示 ) 图 1-4 MIPS 应用领域 3. 68K/Cold Fire 在摩托罗拉的 68K 系列通讯集成处理器 ( 如 68302\68360) 逐渐被 MPC8xx 等 PowerPC 处理器所代替之后, 摩托罗拉的 Coldfire 处理器填补了 68K 原来所占有的中端嵌入式市场 和应用场合 Coldfire 系列嵌入式处理器有 : 5206E: 时钟 40MHz 与 54MHz 两种版本, 其运行在 54MHz 时候, 计算能力为 50 个 MIPS 5307: 时钟 66MHz 与 90MHz 两种版本, 其运行在 90MHz 时候, 计算能力为 70

14 个 MIPS 5272: 时钟 66MHz, 计算能力 63 个 MIPS 5407: 时钟 162MHz, 计算能力 257 个 MIPS 4. Power PC PowerPC 是早期 Motorola 和 IBM 联合为 Apple 的 MAC 机开发的 CPU 芯片, 商标权 同时属于 IBM 和 Motorola, 并成为他们的主导产品 IBM 主要的 PowerPC 产品有 PowerPC604s( 深蓝内部的 CPU) PowerPC750 PowerPCG3(1.1GHz) Motorola 主要有 MC 和 MPC 系列 尽管他们产品不一样, 但都采用 PowerPC 的内核, 这些产品大都用在 嵌入式系统中 MPC860 PowerQUICC (Quad Integrated Communications Controller) 内部集成了微处理器和一些控制领域的常用外围组件, 特别适用于通信产品, 包括器件的适应性, 扩展能力 和集成度等 MPC860 PowerQUICC 集成了两个处理块, 一个处理块是嵌入的 PowerPC 核, 另一个是通信处理模块 ( CPM, Communications Processor Module), 通信处理模块支持四个串行通信控制器 (SCC, Serial Communication Controller), 实际上它有八个串行通道 : 四个 SCC, 两个串行管理控制器 (SMC, Serial Management Channels), 一个串行外围接口电路 ( SPI, Serial Peripheral Interface ) 和一个 I2C( Inter-Integrated Circuit ) 接口 由于 CPM 分担了嵌入式 PowerPC 核的外围工作任务, 这种双处理器体系结构功耗要低于传统的体系结构 的处理器 摩托罗拉的 PowerPC 嵌入式处理器的未来发展走势是主要满足高端嵌入式系统计算的需要, 诸如在电信级设备上的应用 在骨干网上的数据通信设备上的应用 嵌入式 DSP 处理器 DSP 处理器对系统结构和指令进行了特殊设计, 使其适合于执行 DSP 算法, 编译效率 较高, 指令执行速度也较高 在数字滤波 FFT 谱分析等方面 DSP 算法正在大量进入嵌入式领域,DSP 应用正从在通用单片机中以普通指令实现 DSP 功能过渡到采用嵌入式 DSP 处理器 (Embedded Digital Signal Processor, EDSP) 来实现 嵌入式 DSP 处理器有两个发展来 源, 一是 DSP 处理器经过单片化 EMC 改造 增加片上外设成为嵌入式 DSP 处理器,TI 的 TMS320C2000 /C5000 等属于此范畴 ; 二是在通用单片机或 SOC 中增加 DSP 协处理器, 例如 Intel 的 MCS-296 和 Infineon(Siemens) 的 TriCore 推动嵌入式 DSP 处理器发展的另一个因素是嵌入式系统的智能化, 例如各种带有智能逻辑的消费类产品, 生物信息识别终端, 带有加解密算法的键盘, ADSL 接入 实时语音压解系统, 虚拟现实显示等 这类智能化算法一般都是运算量较大, 特别是向量运算 指针线性寻址等较多, 而这些正是 DSP 处理器的长处所在 嵌入式 DSP 处理器比较有代表性的产品是 Texas Instruments 的 TMS320 系列和 Motorola 的 DSP56000 系列

15 1.2.4 嵌入式片上系统 (System On Chip) 随着 EDI 的推广和 VLSI 设计的普及化, 及半导体工艺的迅速发展, 在一个硅片上实 现一个更为复杂的系统的时代已来临, 这就是 System On Chip(SOC) 各种通用处理器内核将作为 SOC 设计公司的标准库, 和许多其它嵌入式系统外设一样, 成为 VLSI 设计中一种标准的器件, 用标准的 VHDL 等语言描述, 存储在器件库中 用户只需定义出其整个应 用系统, 仿真通过后就可以将设计图交给半导体工厂制作样品 这样除个别无法集成的器件以外, 整个嵌入式系统大部分均可集成到一块或几块芯片中去, 应用系统电路板将变得很简洁, 对于减小体积和功耗 提高可靠性非常有利 SOC 可以分为通用和专用两类 通用系列包括 Infineon 的 TriCore,Motorola 的 M-Core, 某些 ARM 系列器件,Echelon 和 Motorola 联合研制的 Neuron 芯片等 专用 SOC 一般专用于某个或某类系统中, 不为一般用户所知 一个有代表性的产品是 Philips 的 Smart XA, 它将 XA 单片机内核和支持超过 2048 位复杂 RSA 算法的 CCU 单元制作在一块硅片上, 形成一个可加载 JAVA 或 C 语言的专用的 SOC, 可用于公众互联网如 Internet 安全方面 1.3 嵌入式操作系统 嵌入式操作系统是嵌入式技术的发展到一定阶段的必然产物, 从 20 世纪 80 年代开始, 一些著名的公司和大学就开始设计研发专用的嵌入式操作系统, 到目前为止, 各种各样的嵌入式操作系统可能有几百种, 下面就简单介绍最常用的几种 1.VxWorks 嵌入式操作系统 VxWorks 嵌入式操作系统是美国 WindRiver 公司在 1983 年设计研发出来的 VxWorks 具有很多优越的性能 : 高性能内核 界面友好的用户开发环境 实时性能出众 高可靠性等 VxWorks 这些特点使得它成为嵌入式系统领域使用最广泛 市场占有率最高的系统, 在通信 航天 军事等领域得到广泛的使用, 比如, 美国 JPL 实验室研制的 索杰纳 火星车采用的就是该操作系统 2.RT-Linux RT-Linux 是由美国墨西哥理工学院开发的嵌入式 Linux 操作系统 到目前为止, RT-Linux 已经成功地应用于航天飞机的空间数据采集 科学仪器测控和电影特技图像处理等广泛领域 RT-Linux 开发者并没有针对实时操作系统的特性而重写 Linux 的内核, 因为这样做的工作量非常大, 而且要保证兼容性也非常困难 为此,RT-Linux 提出了精巧的内核, 并把标准的 Linux 核心作为实时核心的一个进程, 同用户的实时进程一起调度 这样对 Linux 内核的改动非常小, 并且充分利用了 Linux 下现有的丰富的软件资源 3.uClinux uclinux 是一种优秀的嵌入式 Linux 版本, 是 micro-conrol-linux 的缩写, 它主要是针对目标处理器没有存储管理单元 MMU (Memory Management Unit) 的嵌入式系统而设计的, 已经被成功地移植到了很多平台上

16 uclinux 秉承了标准 Linux 的优良特性, 经过各方面的小型化改造, 形成了一个高度优化的 代码紧凑的嵌入式 Linux 虽然它的体积很小, 却仍然保留了 Linux 的大多数的 优点 : 稳定 良好的移植性 优秀的网络功能 对各种文件系统完备的支持和标准丰富的 API 它专为嵌入式系统做了许多小型化的工作, 目前已支持多款 CPU 其编译后目标文件可控制在几百 KB 数量级, 并已经被成功地移植到很多平台上 但是由于没有 MMU,uCLinux 多任务的实现需要一定技巧 4. 红旗嵌入式 Linux 由北京中科院红旗软件公司推出的嵌入式 Linux 是国内做得较好的一款嵌入式操作系 统 目前, 中科院计算所自行开发的开放源码的嵌入式操作系统 Easy Embedded OS(EEOS) 也已经开始进入实用阶段了 该款嵌入式操作系统重点支持 p-java 系统目标一方面是小型化, 另一方面能重用 Linux 的驱动和其它模块 由于有中科院计算所的强大科研力量做 后盾,EEOS 有望发展成为功能完善 稳定 可靠的国产嵌入式操作系统平台 5.Wince 虽然微软公司 windows 系统已经在 PC 市场占据了绝对统治地位, 但是对于嵌入式系 统这个大饼, 微软也是垂涎已久 桌上型的 windows 系统对于嵌入式系统来说自然是庞然大物, 于是微软推出精简版的 windows CE 作为进攻嵌入式操作系统的主力 Windows CE 目前主要用于 PDA 上面 图 1-5 为西门子公司生产的采用 windows CE 操作系统的 SIMPAD 产品 图 1-5 采用 windows CE 的 SIMPAD 图 1-6 采用 palmm505 型的 PDA 6.Palm 由 palm computing 公司的嵌入式操作系统, 目前应用在 PDA 市场上, 是市场占有率最 高的 PDA 操作系统 Palm 操作系统架构非常简洁, 具有耗电量少, 硬件需求低等优点, 当然它的功能也是比较简单的 图 1-6 为 palmm505 型 PDA 小结 : 嵌入式操作系统种类繁多, 但是上面介绍的这几种操作系统占据了市场绝大部 分份额 各种操作系统市场占用情况如图 1-7 所示

17 图 1-7 各操作系统市场占有率 1.4 嵌入式系统设计 嵌入式系统开发流程 本节将结合实际情况介绍嵌入式系统设计的具体方法, 其典型开发流程如图 1-8 所示 用户需求分析 硬件选型 : 选择合适的微处理器类型 存储介质和接口软件选型 : 选择合适的操作系统平台和相应的开发工具 否 功能测试达到设计目标是开发结束 硬件设计 软件设计 系统集成 图 1-8 嵌入式系统开发流程

18 嵌入式系统以应用为中心, 从本质上来说, 嵌入式系统设计应该属于软件工程范畴, 所以, 开发嵌入式项目时, 同样可以采用软件工程所描述的流程来进行 在嵌入式系统开发过程中, 进行用户需求分析是非常重要的 在设计的最初阶段, 从用户那里得到口头或书面的项目描述, 并把它进行提炼, 整理成一个书面的规格说明书, 说明书里详细描述了项目所要实现的功能 开发周期 软硬件成本等等信息, 最后规格说 明书要得到用户的认可, 这样第一阶段的工作才算完成 软硬件选型是整个嵌入式项目开发最为关键的一个步骤 嵌入式微处理器和嵌入式操作系统都有几百种之多, 如何能够快速准确的选出合适的微处理器和操作系统是整个项目 成败的关键 通常情况下, 我们都会选择一个成熟的 较流行的微处理器和操作系统, 这样在整个项目开发过程中, 可以得到足够多的网络资源, 减少系统硬件错误的机会, 同时利用操作系统提供的 API 函数完成大部分工作, 缩短项目开发周期, 提高系统的稳定性 软硬件选型确定以后, 接下来就是要进行软件和硬件设计 硬件设计包括电路设计和 PCB 版制作等过程 ; 而软件设计所涉及到的方面比较广, 包括 bootloader 设计 操作系统裁减和移植 驱动程序开发和应用软件编写等工作 在实际开发中, 既可以选择软件和硬 件设计同时进行, 也可以先进行硬件设计再进行软件设计, 当然通常情况下都会采用前者 软硬件设计完成以后, 就可以集成到一起进行测试, 嵌入式系统的测试方法和传统的测试方法有一定的差异, 读者可以参考相关的资料, 本书对这方面的内容不再介绍 嵌入式系统开发模式 软件开发有两种模式, 其一是 native 模式, 其二是宿主机 (HOST) 目标板 (TARGET) 的开发模式 传统的软件开发都是以 native 方式进行的, 即本机 (HOST) 开发 调试, 本机运行的方式 但是这种方式通常不适合于嵌入式系统的软件开发, 因为 对于嵌入式系统的开发, 没有足够的资源在本机 ( 即板子上系统 ) 运行开发工具和调试工具 通常的嵌入式系统的软件开发采用一种交叉编译调试的方式 交叉编译调试环境建立在宿主机 ( 即一台 PC 机 ) 上, 对应的开发板叫做目标板 这种宿主机 (HOST) 目 标板 (TARGET) 的开发模式下的系统连接图如图 1-9 所示 : 网线 网线 串口线 并口线 图 1-9 嵌入式系统典型开发模式 开发时使用宿主机上的交叉编译 汇编及连接工具形成可执行的二进制代码,( 这种

19 可执行代码并不能在宿主机上执行, 而只能在目标板上执行 ) 然后把可执行文件下载到目标机上运行 调试时的方法很多, 可以使用串口, 以太网口等, 具体使用哪种调试方法可以根据目标机处理器所提供的支持作出选择 宿主机和目标板的处理器一般都不相同, 宿主机为 Intel 处理器, 而目标板如 CY-ARM7-EDU 为 SAMSUNG S3C44B0x,GNU 编译器提供这样 的功能, 在编译编译器时可以选择开发所需的宿主机和目标机从而建立开发环境 所以在进行嵌入式开发前第一步的工作就是要安装一台装有指定操作系统的 PC 机作宿主开发机, 对于嵌入式 Linux, 宿主机上的操作系统一般要求为 Redhat Linux, 小结 本章介绍了下列几个重要的概念 : 1. 嵌入式系统定义 IEEE 定义 : 嵌入式系统并没有一个标准的定义,IEEE 是这样定义嵌入式系统 : 嵌入式系统是 控制 监视或者辅助设备 机器和车间运行的装置 ( 原文为 devices used to control, monitor, or assist the operation of equipment,machinery or plants) 国内定义 : 不过,IEEE 的定义并不能够反应出嵌入式系统的本质特征 目前, 国内普遍采用下列描述来定义嵌入式系统 : 嵌入式系统以应用为中心, 以计算机技术为基础, 软硬件可裁减, 适应应用系统对功能 可靠性 成本 体积 功耗严格要求的专用计算机系统 2. 嵌入式微处理器分类 ARM MIPS POWERPC 68K/Cold Fire 3. 嵌入式操作系统 VxWorks RTLinux uclinux 红旗嵌入式 Linux wince palm 本章最后详细介绍了嵌入式系统设计相关概念, 包括嵌入式系统开发流程 HOST-TARGET 开发模式以及相关测试技术 希望通过本章的学习, 可以对上述知识初步掌握, 为以后章节知识打好基础

20 习题 1. 填空题 (1)ARM 是 的英文缩写 (2) 嵌入式系统以 为中心, 以计算机技术为基础, 可裁减, 适应应用系统对 严格要求的专用计算机系统 (3) 常用的硬件调试工具有 : 和 2. 简答题 (1)ARM 处理器的分类以及各自特点是什么? (2) 在嵌入式系统设计中, 如何选择嵌入式微处理器类型和嵌入式操作系统类型? (3) 简述嵌入式系统的设计流程

21 第二章 ARM 微处理器结构及其指令 系统 在第一章中简单介绍了嵌入式相关概念, 包括嵌入式微处理器分类以及各自特点和应用场合 本章将就其中一款微处理器 ARM 进行详细介绍, 包括下列一些主要内容 : (1)ARM 微处理器简介 (2)ARM 编程模型 (3)ARM 指令系统 (4)ARM 程序设计 2.1 ARM 微处理器简介 ARM 微处理器的历史发展 谈到嵌入式, 就不得不说 ARM 这个词, 甚至好多嵌入式初学者往往把 嵌 入式 和 ARM 两个词等同起来, 这当然比较偏颇, 但在一定程度上反映了在嵌入式领域中 ARM 这个词的重要地位 那么, 究竟什么是 ARM 呢?ARM 其实是一家位于英国剑桥区的电子公司的名字, 全名的意思是 Advanced RISC Machine 该 公司成立于 1990 年 11 月, 是苹果电脑,Acorn 电脑集团和 VLSI Technology 的合资企业 Acorn 曾推出世界上首个商用单芯片 RISC 处理器, 而苹果电脑当时希望将 RISC 技术应用于自身系统,ARM 微处理器新标准因此应运而生 80 年代末 90 年代初半导体行业产业链刚刚出现分工, 台积电, 联电等半导体代工厂正悄悄崛起, 美国硅谷中的一些 fabless 公司也如雨后春笋一样涌现出来, 所谓的 fabless 公司自己设计芯片, 但是生产过程则包给台积电等代工厂生产 而 ARM 更是为天下先,12 年前首创了 chipless 的生产模式, 即该公司既不生产芯片, 也不设计芯片, 而是设计出高效的 IP 内核, 授权给半导体公司使用, 半导体公司在 ARM 技术的基础上添加自己的设计并推出芯片产品, 最后由 OEM 客户采用这些芯片来 构建基于 ARM 技术的系统产品 这种方式有点象通信行业的高通和半导体行业的 RAMBUS, 他们站在了半导体产业链上游的上游 12 年前成立的 ARM 可能面临着很大风险, 因为没有人知道这条路能不能行得通, 但是现在的事实已经证明,ARM 走了一条没人走过, 却是正确的道路 ARM 的核心业务是销售芯片核心技术 IP, 目前全球有 103 家巨型 IT 公司在采

22 用 ARM 技术,20 家最大的半导体厂商中有 19 家是 ARM 的用户, 包括德州仪器, 意法半导体,Philips, Intel 等 20 大巨头中唯一没有购买 ARM 授权的是 Intel 的老 对头 AMD, 因为 Intel 便携式处理器采用的是 StrongARM, 而 AMD 则收购了 Alchemy 公司与之抗衡, 采用的是 MIPS 结构 微处理器核是 ARM 技术的重中之中, 目前面向市场的有 ARM7, ARM9, ARM9E-S,StrongARM 和 ARM10 系列 ARM 专利技术收入主要来自两个方面, 一个是专利授权费用, 客户如果采用 ARM 专利时一次性付给 ARM 的费用 ; 另一部分是按照一定比例收取客户产品的专利使用费, 即客户每卖出一片芯片, 就收取 同等比例的费用 这两项收入占公司总收入的 70% 目前在中国已经有中兴通讯 中芯国际 上海华虹和南京博芯等多家公司购买了 ARM 的内核授权, 生产自己的芯片 总之,ARM 是英国全球著名的 32 位嵌入式 RISC 芯片内核的设计公司, 也是 ARM 的产品商标, 其产品 ARM 嵌入式内核已被全球各大芯片厂商采用, 基于 ARM 的开发技术席卷了全球嵌入式市场, 已成为嵌入式系统主流技术之一 图 2-1 为 2004 年 ARM 公司市场占有率分布图 图 2-1 嵌入式微处理器市场占有情况表 ARM 微处理器的特点和应用 1.ARM 微处理器特点 ARM 微处理器采用 RISC 架构, 具有下列一些显著特点 : 体积小 功耗低 成本低 高性能 支持 Thumb(16 位 )/ARM(32 位 ) 双指令集, 能够 8 位和 16 位器件 具有大量的寄存器, 因而指令执行速度快 绝大多数操作都在寄存器中进行, 通过 Load/Store 的体系架构在内存和寄

23 存器之间传递数据 寻址方式简单 采用固定长度的指令格式 除此之外,ARM 体系还采用一些特别的技术用来保证芯片高性能的同时, 尽可能减小芯片体积, 降低芯片功耗 这些技术包括 : 在同一条数据处理指令中包含算术逻辑处理单元处理和移位处理 使用地址自动增加 ( 减少 ) 来优化程序中循环处理 Load/Store 指令可以批量传输数据, 从而提高数据传输的效率 所有指令都可以进行分支预测功能, 即根据前面指令执行结果, 决定是否执行, 以提高指令的执行效率 2.ARM 微处理器应用场合 ARM 微处理器及技术已经深入到各个领域, 取得很大的成功 无线通讯领域 : 无线通信领域是 ARM 微处理器应用最为广泛的领域之一, 目前全球超过 85% 的无线通讯设备都采用 ARM 技术 比如手机 PDA 等 设备中都有 ARM 技术的应用 蓝牙技术 :ARM 已经为蓝牙技术的推广应用做好了准备, 像爱立信 英特尔 朗讯 阿尔卡特等 20 多家公司的元器件产品都采用了 ARM 技术 网络应用领域 : 随着宽带技术的推广, 采用 ARM 技术的 ADSL 芯片组正逐渐取得竞争优势 消费类电子产品领域 :ARM 技术在数字音频领域 数字机顶盒和游戏机中 得到广泛应用 另外, 采用 ARM 技术的存储产品包括硬盘系列 微型闪存和可读写光盘等, 已经投入生产 信息家电领域 : 现在很多数码相机 打印机都使用了 ARM 技术, 另外, 汽车上包括驾驶 安全和车载娱乐等各种功能都可以使用 ARM 微处理器来完成 图 2-2 为 2004 年第一季度 ARM 处理器市场应用分布示意图 图 2-2 以市场细分的 2004 第一季度的出货量

24 2.1.3 ARM 微处理器系列 ARM 微处理器目前包括下面几个系列, 以及其它厂商基于 ARM 体系结构的处 理器 除了具有 ARM 体系结构的共同特点以外, 每一个系列的 ARM 微处理器都有各自的特点和应用领域 ARM7 系列 ARM9 系列 ARM9E 系列 ARM10E 系列 SecurCore 系列 Intel 的 Xscale Intel 的 StrongARM 其中,ARM7 ARM9 ARM9E 和 ARM10 为 4 个通用处理器系列, 每一个系列提供一套相对独特的性能来满足不同应用领域的需求 SecurCore 系列专门为安全要求较高的应用而设计 下面让我们来了解一下各种处理器的特点及应用领域 1.ARM7 系列 ARM7 系列处理器是低功耗的 32 位 RISC 处理器 它主要用于对功耗和成本要 求比较苛刻的消费类产品 其最高主频可以达到 130MIPS ARM7 系列处理器支持 16 位的 Thumb 指令集, 使用 Thumb 指令集可以以 16 位的系统开销得到 32 位的系统性能 特点 : 具有嵌入式 ICE-RT 逻辑, 调试开发比较方便 极低的功耗, 适合对功耗要求较高的产品, 如便携式产品 能够提供 0.9MIPS/MHz 的三级流水线结构 程序代码密度高并兼容 16 位的 Thumb 指令集 对操作系统的支持广泛, 包括 Windows CE Linux Palm OS 等 指令系统与 ARM9 系列 ARM9E 系列和 ARM10E 系列兼容, 便于用户的产品升级换代 主频最高可达 130MIPS, 高速的运算处理能力能胜任绝大多数的复杂应用 ARM7 系列微处理器包括如下几种类型的核 :ARM7TDMI ARM7TDMI-S ARM720T ARM7EJ 其中,ARM7TMDI 是目前使用最广泛的 32 位嵌入式 RISC 处理器, 属低端 ARM 处理器核 TDMI 的基本含义为 : T: 支持 16 位压缩指令集 Thumb; D: 支持片上 Debug; M: 内嵌硬件乘法器 (Multiplier); I: 嵌入式 ICE, 支持片上断点和调试点 ; 本书所介绍的 Samsung 公司的 S3C44B0X 即属于该系列的微处理器

25 2.ARM9 系列 ARM9 系列处理器使用 ARM9TDMI 核, 其中包括了 16 位的 Thumb 指令集 ARM9 系列包括 ARM920T ARM922T 和 ARM940T 三种类型, 主要用于适应不同的市场需求 ARM9 系列处理器具有以下一些特点 : 支持 32 位 ARM 指令集和 16 位 Thumb 指令集的 32 位 RISC 处理器 五级流水线 单一的 32 位 AMBA 总线 MMU 支持, 能够运行 WinCE Linux 等操作系统 MPU 支持实时操作系统, 包括 Vxworks 统一的数据 cache 和指令 cache 提供 0.18um 0.15um 及 0.13um 的生产工艺 3.ARM9E 系列 ARM9E 系列处理器使用单一的处理器内核提供了微控制器 DSP Java 应用系 统的解决方案, 减小了整个芯片的体积, 降低了功耗, 缩短了产品的研发周期 ARM9E 系列处理器包括 ARM926EJ-S ARM946E-S 和 ARM966E-S 三种类型 在 ARM9E 系列处理器中, 都提供了增强的 DSP 处理能力, 非常适合那些同时需要 使用 DSP 和微控制器的应用场合 ARM9E 系列处理器具有以下一些特点 : 支持 DSP 指令集, 适合于需要高速数字信号处理的场合 5 级整数管线, 指令执行效率更高 支持 32 位 ARM 指令集和 16 位 Thumb 指令集 支持 32 位的高速 AMBA 总线接口 支持 VFP9 浮点处理协处理器 全性能的 MMU, 支持 Windows CE Linux Palm OS 等多种主流嵌入式操作系统 MPU 支持实时操作系统 支持数据 Cache 和指令 Cache, 具有更高的指令和数据处理能力 主频最高可达 300MIPS 4.ARM10E 系列 ARM10E 系列处理器采用新的体系架构和新的节能模式, 提供了 64 位读取 / 写入体系, 支持包括向量操作的满足 IEEE754 的浮点运算协处理器, 系统集成更加方 便 ARM10E 系列包括 ARM1020E ARM1022E 和 ARM1026EJ-S 三种类型, 其主要特点如下 : 支持 DSP 指令集, 适合于需要高速数字信号处理的场合 6 级流水线结构, 指令执行效率更高 支持 32 位 ARM 指令集和 16 位 Thumb 指令集 支持 32 位的高速 AMBA 总线接口

26 支持 VFP10 浮点处理协处理器 全性能的 MMU, 支持 Windows CE Linux Palm OS 等多种主流嵌入式操作系统 支持数据 Cache 和指令 Cache, 具有更高的指令和数据处理能力 主频最高可达 400MIPS 内嵌并行读 / 写操作组件 5.SecurCore 微处理器系列 SecurCore 系列微处理器专为安全需要而设计, 提供了完善的 32 位 RISC 安全解 决方案, 因此,SecurCore 系列微处理器除了具有 ARM 架构的低功耗 高性能的特点外, 还具有其独特的优势, 即提供了对安全解决方案的支持 系统安全方面具有如下的特点 : 带有灵活的保护单元, 以确保操作系统和应用数据的安全 采用核心技术, 防止外部对其进行扫描探测 可整合用户自己的安全特性和其它协处理器 SecurCore 系列微处理器主要应用于一些对安全性要求较高的产品中, 如电子商务 电子政务 电子银行业务 网络和认证系统等领域 SecurCore 系列微处理器包含 SecurCore SC100 SecurCore SC110 SecurCoreSC200 和 SecurCore SC210 四种类型, 以适用于不同的应用场合 6.StrongARM 微处理器系列 Intel StrongARM SA-1100 处理器是采用 ARM 架构高度整合的 32 位 RISC 微处理器 它融合了 Intel 公司的设计和处理技术以及 ARM 架构的电源效率, 采用在软件上兼容 ARMv4 架构 同时吸取了 Intel 技术的技术优势 IntelStrongARM 处理器是便携式通讯产品和消费类电子产品的理想选择, 已成功应用于多家公司的掌上计算机产品中 7.Xscale 处理器 Xscale 处理器是基于 ARMv5TE 架构的解决方案, 是一款全性能 高成本效益比 低功耗的处理器 它支持 16 位的 Thumb 指令和 DSP 指令集, 已使用在数字移动电话 个人数字助理和网络产品等场合 Xscale 处理器是 Intel 目前主要推广的一款 ARM 微处理器 特点 : 采用 7~8 级流水线结构, 性能更高, 功耗更低 Intel 媒体处理技术, 可有效处理多媒体指令 128 个跳转指令目的地址缓存可统计的存储跳转指令的目的地址, 让指令预取和指令流水线获得更高效率 32k 数据缓存和指令缓存 调试单元具有硬件中断功能, 可存储 256 个断点位置 64 位内核内存数据宽度, 可以让内核在 600MHz 时钟频率下获得 4.8GB/S 的高速数据流目前, 主要有 PXA25X PXA26X 和 PXA27X 系列处理器

27 图 2-3 给出了 ARM 处理器系列家谱示意图 图 2-3 ARM 处理器系列家谱 2.2 ARM 编程模型 ARM 微处理器模式 ARM 微处理器的运行模式有 7 种, 分别为 : 用户模式 (User,usr): 正常程序执行时,ARM 处理器所处的状态 快速中断模式 (FIQ,fiq): 用于快速数据传输和通道处理 外部中断模式 (IRQ,irq): 用于通常的中断处理 特权模式 (Supervisor,sve): 供操作系统使用的一种保护模式 数据访问中止模式 (Abort,abt): 当数据或指令预取终止时进入该模式, 用于虚拟存储及存储保护 未定义指令终止模式 (Undefined,und): 用于支持硬件协处理器软件仿真 系统模式 (System,sys): 用于运行特权级的操作系统任务 通常情况下, 应用程序运行在用户模式下, 这时应用程序不能访问一些受操作 系统保护的系统资源, 同时应用程序也不能直接进行处理器模式的切换 当应用程序发生异常中断时, 处理器进入相应的异常模式 在每一种异常模式中都有一组属于自己的寄存器, 供相应的异常处理程序使用, 这样可以保证异常模 式时, 用户程序下的寄存器值可以不被破坏 系统模式属于特权模式, 它和用户模式具有完全一样的寄存器, 在该模式下, 可以访问所有的系统资源, 也可以直接进行处理器模式切换 但是有一点大家要注 意, 从用户模式进入到系统模式, 并不是通过异常过程进入的

28 2.2.2 ARM 寄存器 ARM 处理器共有 37 个寄存器, 其中 31 个为通用寄存器,6 个状态寄存器, 这 些寄存器都是 32 位 ARM 处理器运行在每一种模式下时, 都会使用属于自己的一组寄存器组, 通常包括 15 个通用寄存器 (R0~R14) 一个或两个状态寄存器及程序计数器 (PC) 每 一种模式下的寄存器组是部分重叠的, 表 2-1 列出了各处理器模式下可见的寄存器情况 用户模式 系统模式 特权模式 表 2-1 各种处理器模式下的寄存器 数据访问中止模式 未定义指令终止模式 快速中断模式 外部中断模式 R0 R0 R0 R0 R0 R0 R0 R1 R1 R1 R1 R1 R1 R1 R2 R2 R2 R2 R2 R2 R2 R3 R3 R3 R3 R3 R3 R3 R4 R4 R4 R4 R4 R4 R4 R5 R5 R5 R5 R5 R5 R5 R6 R6 R6 R6 R6 R6 R6 R7 R7 R7 R7 R7 R7 R7 R8 R8 R8 R8 R8 R8 R8_fiq R9 R9 R9 R9 R9 R9 R9_fiq R10 R10 R10 R10 R10 R10 R10_fiq R11 R11 R11 R11 R11 R11 R11_fiq R12 R12 R12 R12 R12 R12 R12_fiq R13 R13 R13_svc R13_abt R13_und R13_irq R13_fiq R14 R14 R14_svc R14_ abt R14_ und R14_ irq R14_fiq PC PC PC PC PC PC PC CPSR CPSR CPSR SPSR_svc CPSR SPSR_ abt CPSR SPSR_ und CPSR SPSR_ irq CPSR SPSR_fiq 下面对一些特殊的寄存器进行简单的介绍 1. 通用寄存器通用寄存器中 R0~R7 是所有处理器模式共用的一组寄存器, 也就是说, 在从一种模式切换到另一种模式时, 必须保存它们的值 R8~R14 为备份寄存器, 其中对于 R8~R12 来说, 每一个寄存器对应两个不同的物理寄存器,R13 和 R14 对应 6 个不

29 同的物理寄存器 R13 通常用做堆栈指针, 采用下面的记号来区分各个物理寄存器 : R13_<MODE> <MODE> 取下列几个值 :usr svc abt und irq 及 fiq R14 寄存器有两种特殊的作用 : 用户模式下,R14 用做链接寄存器 (LR), 存放子程序被调用时的返回地址 异常处理模式下,R14 用来保存异常的返回地址 R15 为程序计数器, 又被记做 PC 由于 ARM 采用了流水线机制, 因此 PC 的值为当前指令地址的值加 8 个字节, 也就是说,PC 指向当前指令的下两条指令的地 址 2. 程序状态寄存器在 ARM 处理器中, 程序状态寄存器用来保存程序执行时的各种状态值, 包括 条件标志位 中断禁止位 当前处理器模式标志和其它一些位 程序状态寄存器分为 CPSR 和 SPSR 两种类型 在任何一种处理器模式下, 都会有一个共用的 CPSR, 另外异常模式下还会有一个专用的 SPSR( 备份程序状态寄存器 ) 当异常中断发生时, 这个寄存器用于存放当前程序状态寄存器的内容, 当退出异常中断时, 再把 SPSR 中的值恢复到 CPSR 中 CPSR 和 SPSR 格式相同, 如图 2-4 所示 条件标志位保留位控制位 ~ N Z C V Q I F T M4 M3 M2 M1 M0 模式控制 T 控制位 FIQ 允许 / 禁止位 IRQ 允许 / 禁止位图 2-4 CPSR 和 SPSR 格式 ARM 异常中断处理 1.ARM 异常中断处理类型异常是 ARM 微处理器中非常重要的一个事件, 它由内部或外部源产生而引起 处理器处理 ARM 微处理器支持表 2-2 所列几种类型的异常 表 2-2 ARM 体系中异常类型 异常类型 含义

30 复位 未定义指令 软件中断 指令预取中止 数据访问中止 外部中断请求 (IRQ) 快速中断请求 (FIQ) 当处理器的复位引脚有效时 ( 可以有软硬件引起 ), 系统产生复位异常中断, 程序跳转到复位程序处执行 当 ARM 处理器或者协处理器执行到未定义指令时, 产生未定义的指令异常中断, 转到相应的处理程序处执行 软件中断由用户执行指令 SWI 而引起, 可用于用户模式下的程序调用调用特权操作指令 若处理器预取的指令的地址不存在, 或该地址不允许当前指令访问时, 当该被预取的指令执行时, 才会产生指令预取中止异常 如果数据访问指令的目标地址不存在, 或者该地址不允许当前指令访问, 处理器产生数据访问中止异常中断 当处理器的外部中断请求引脚有效, 而且 CPSR 寄存器的 I 位为 0 时, 处理器产生外部中断请求 IRQ 异常中断 系统的外设通常通过该异常中断请求中断服务 当处理器的快速中断请求引脚有效, 并且 CPSR 寄存器的 F 位为 0 时, 处理器产生外部中断请求 FIQ 异常中断 2.ARM 异常处理过程 (1) ARM 异常处理响应过程当异常发生时,ARM 微处理器按下列步骤自动进行响应 : 保存现场 主要做两件事, 首先, 将下一条指令的地址保存在相应的 LR 寄存器中 ; 然后将 CPSR 寄存器保存到相应的 SPSR 寄存器中 设置当前程序状态寄存器 CPSR 中相应位的值, 包括重新设置模式位, 使处理器进入相应的模式工作 ; 然后设置 CPSR 中的中断位, 禁止 IRQ 和 FIQ 中断 将程序计数器值 (PC), 设置成该异常中断的中断向量地址, 从而跳转到相应的中断处理程序处执行上述过程可以用如下的伪代码来描述 R14_<exception_mode>=return link SPSR<exception_mode>=CPSR CPSR[4:0]= exception_mode number CPSR[5]=0 If <exception_mode> = = reset or FIQ THEN [ CPSR[6]=1 CPSR[7]=1 PC= exception vector address ] (2)ARM 异常处理返回当异常处理程序结束时,ARM 处理器会返回到原保存的现场, 通常执行下列两

31 个操作来完成 : 拷贝 SPSR_mode 寄存器内容到 CPSR 中 将 LR_mode 寄存器的内容复制到程序计数器 PC 中, 即返回到发生异常中断的指令的下一条指令处继续执行 注意 :1 复位异常中断处理程序不需要返回 因为在复位后, 系统会从复位异 常中断处理程序处重新开始 2 如果进入异常处理程序时保存了部分通用寄存器的值, 此时需要恢复这些被保存的通用寄存器的值 3. ARM 异常处理函数典型设计 (1) 复位异常中断处理函数设计复位异常中断处理函数是嵌入式系统设计时首先必须设计的一个函数, 它主要 在系统加电或复位时被执行, 进行一些初始化的工作, 具体内容和整个硬件平台息息相关, 但是通常情况下都会完成下列一些任务 设置异常向量表 关中断和看门狗 初始化数据栈和寄存器 初始化存储器系统 初始化外部设备 打开中断 构建 c 语言环境, 跳到应用程序或操作系统处执行下面是广泛应用在 ARM7 和 ARM9 处理器平台上的复位异常中断处理函数典型设计 SYS_RST_HANDLER mrs r0, cpsr ;enter svc mode and disable irq,fiq bic r0, r0, #ModeMask orr r0, r0, #(SVC32Mode :OR: IRQ_BIT :OR: FIQ_BIT) msr cpsr_c, r0 IMPORT InitSystem bl InitSystem ; 初始化硬件 0 ;InitRamData ldr r0, TopOfROM ldr r2, BaseOfBSS ldr r3, BaseOfZero cmp r2, r3 ; 初始化 bss section ldrcc r1, [r0], #4 ;r0:topofrom strcc r1, [r2], #4

32 bcc %B0 1 mov r0, #0 ldr r3, EndOfBSS cmp r2, r3 ; 初始化 zi section strcc r0, [r2], #4 bcc %B1 ; 将执行码搬运到 sdram 执行 ldr r0, BaseOfROM ldr r1, TopOfROM ldr r2, =RSFORBOOT 2 cmp r0, r1 ldrcc r3, [r0], #4 strcc r3, [r2], #4 bcc %B2 ldr r0, GotoMain ldr r2, =RSFORBOOT add r0, r0, r2 mov pc, r0 ; 跳到 c 语言 main 函数 GotoMain DCD $MainEntry (2)FIQ 和 IRQ 异常中断处理函数设计 ARM 处理器提供 FIQ 和 IRQ 异常中断用于外部设备向 CPU 请求中断服务 程 序员可以设置 CPSR 寄存器中 I 位来允许或屏蔽这两个异常中断请求 : 当 CPSR 的 I 位为 1 时,CPU 屏蔽 FIQ 和 IRQ 的请求 ; 当 CPSR 的 I 位为 0 时,CPU 响应 FIQ 和 IRQ 的请求 FIQ 为快速异常中断, 它比 IRQ 异常中断优先级高, 主要表现在下面两个方面 : 当 FIQ 和 IRQ 异常中断同时发生时,ARM 微处理器优先处理 FIQ 异常中断 ARM 微处理器响应 FIQ 异常中断后,IRQ 中断被禁止 由于 FIQ 异常中断通常用于系统中对于相应时间要求比较苛刻的任务,ARM 在设计时也有一些特殊的安排, 以尽量减小 FIQ 异常中断的响应时间 详细的 FIQ 和 IRQ 异常中断处理函数设计请见本书第七章第二节

33 2.2.4 ARM 存储体系 1.ARM 处理器支持三种数据类型 字 (word): 在 ARM 体系结构中, 字长度为 32 位, 占 4 个字节 半字 (half-word): 在 ARM 体系结构中, 半字的长度为 16 位 字节 (byte): 各种微处理器体系结构中, 字节的长度为 8 位 2.ARM 体系中的存储空间 ARM 体系使用平板地址空间, 该地址空间的大小为 2 32 个 8 位字节, 即 ARM 体系结构最大寻址空间为 4GB ARM 地址空间首地址为 0, 从零字节 ~ 三字节放置 第一个存储的字数据, 从第四字节 ~ 第七字节放置第二个存储的字数据, 依次排列 ARM 体系采用两种方法存储字数据, 分别称为大端模式和小端模式, 具体格式如下 : (1) 大端模式在该种模式下, 字数据的高字节存放在低地址中, 而字数据的低字节部分则存放在高地址中 如图 2-5 所示 字单元 半字单元 ( 低地址部分 ) 半字单元 ( 高地址部分 ) 最低字节单元第二字单元第三字单元最高字单元 (1) 小端模式 图 2-5 大端格式的存储系统 小端模式与大端模式存储格式恰好相反, 在小端模式下, 低地址中存放字数据的低字节部分, 而高地址存放的是字数据的高地址部分 如图 2-6 所示 字单元 半字单元 ( 低地址部分 ) 半字单元 ( 高地址部分 ) 最低字节单元第二字单元第三字单元最高字单元 图 2-6 小端格式的存储系统 例 : r3=0x storeb r3,[r1]

34 3.MMU 不同的嵌入式应用系统中, 其存储体系也会差别很大 比如在 ARM7TDMI 核中, 存储体系使用最简单的平板式地址映射机制 该方式下, 地址空间的分配是固 定的, 系统使用物理地址, 就像单片机系统一样, 这种方式会带来以下几个问题 : 程序员必须自己管理物理内存的分配 使用和回收, 增加了编程的困难 应用程序出错可能会带来整个内核的崩溃 为此, 在很多 ARM 微处理器核中, 都使用虚拟内存映射机制, 整个内存由内存管理单元 ( 即 MMU) 进行管理, 整个系统使用虚拟地址, 而由 MMU 将其映射为实际的物理地址 图 2-7 为一款嵌入式微处理器 MMU 转换示意图 访问控 域位 转换表遍 制硬件 TLB 历硬件 主 异常 C,B 位 物理地址 存储 ARM 高速缓存 高速缓存 器 微处理器 虚拟地址 和写缓冲 行取硬件 图 2-7 高速缓存的 MMU 存储器系统 这种映射机制对于嵌入式系统非常重要, 通常情况下,MMU 主要完成以下工 作 : 虚拟存储空间到物理存储空间的映射 在 ARM 中, 无论物理地址还是虚

35 拟地址都使用分页机制, 即把空间分为一个个大小固定的块, 每一块称为一页 物理空间的页和虚拟地址的页大小相同 存储器访问权限的控制 2.3 ARM/Thumb 指令系统 ARM 指令的编码格式 1.ARM 指令编码格式 ARM 指令字长为固定的 32 位, 典型 ARM 指令编码格式如下图 2-8 所示 cond 001 opcode s Rn Rd shifter_operand 图 2-8 ARM 编码格式 其中, 各字段含义为 : cond: 决定指令是否执行的条件编码 opcode: 指令操作符编码 s: 包含第一个操作数的寄存器编码 Rn: 源寄存器编码 Rd: 目标寄存器编码 shifter_operand: 表示第二个操作数 2.ARM 指令语法格式 通常情况下, 一个典型的 ARM 指令的语法格式如下 : <opcode>{<cond>}{s} <Rd>,<Rn>,<shifter_operand> 其中 : opcode: 指令助记符, 如 ADD 表示算术加操作指令 code: 决定指令是否执行的条件编码大多数 ARM 指令都可以条件执行, 也就是根据 CPSR 中的条件标志位决定是 否执行该指令 当条件满足时执行该指令, 条件不满足时该指令被当作一条 NOP 指令 每一条指令中包含 4 位条件码, 共有 16 个条件码可供选择, 各条件码的含义和 助记符如表 2-3 所示 条件码 <cond> 条件码助记符 表 2-3 ARM 指令条件码 含义 0000 EQ 相等 Z=1 CPSR 条件标志位值

36 0001 NE 不相等 Z= CS/HS 无符号数大于 / 等于 C= CC/LO 无符号数小于 C= MI 负数 N= PL 非负数 N= VS 上溢出 V= VC 没有上溢出 V= HI 无符号数大于 C=1 且 Z= LS 无符号数小于等于 C=0 且 Z= GE 有符号数大于等于 N=1,V=1 或 N=0,V= LT 有符号数小于 N=1,V=0 或 N=0,V= GT 有符号数大于 Z=0 且 N=V 1101 LE 有符号数小于 / 等于 Z=1 或 N!=V 1110 AL 无条件执行 1111 NV 未定义 AL 该指令从不执行该指令执行结果不可预知该指令无条件执行 ARM V3 之前 ARM V3 及 ARM V4 ARM V5 及以上版本 ARM 指令寻址方式 1. 寻址方式概念寻址方式在 ARM 微处理器中是一个较为重要的概念, 它的含义为微处理器根据指令中给出的地址信息来寻找物理地址的方式 目前 ARM 指令系统常见的寻址方式有下列几种 2.ARM 指令寻址方式类型 (1) 立即寻址立即寻址也叫立即数寻址, 这是一种特殊的寻址方式, 操作数本身就在指令中给出, 只要取出指令也就取到了操作数 这个操作数被称为立即数, 对应的寻址方式也就叫做立即寻址 例如以下指令 : SUBS R0,R0,#1 ;RO R0-1 MOV R0,0x1f ; 将立即数 0x1f 装入 R0 在以上两条指令中, 第二个源操作数即为立即数, 要求以 # 为前缀, 对于以十六进制表示的立即数, 还要求在 # 后加上 0x 或 &

37 (2) 寄存器寻址立即寻址方式中操作数在指令中给出, 如果操作数的值放在寄存器中, 指令中 的地址码字段指出该寄存器编号, 则这种寻址方式为寄存器寻址方式, 它是各类微处理器经常采用的一种方式, 也是一种执行效率较高的寻址方式 以下指令 : MOV R1,R2 ;R1 R2 ADD R0,R1,R2 ;R0 R1+R2 这两条指令的执行效果是将寄存器 R1 和 R2 的内容相加, 其结果 ( 实际为 2R2) 存放在寄存器 R0 中 (3) 寄存器间接寻址寄存器间接寻址就是将操作数的地址存放在寄存器中, 而操作数本身存放在存储器中 举例如下 : LDR R1,[R2] ; 将 R2 指向的存储单元的数据读出, 保存在 R1 中 ADD R0,R1,[R2] ;R0 R1+[R2] 在该条指令中, 以寄存器 R2 的值作为操作数的地址, 在存储器中取得一个操作数后与 R1 相加, 结果存入寄存器 R0 中 (4) 基址变址寻址基址变址寻址就是将基址寄存器的内容与指令中给出的地址偏移量相加, 从而得到一个操作数的有效地址 基址变址寻址方式常用于访问某基地址附近的地址单元 采用变址寻址方式的指令常见有以下几种形式, 如下所示 : LDR R0,[R1,#4] ;R0 [R1+4] LDR R0,[R1,#4]! ;R0 [R1+4] R1 R1+4 LDR R0,[R1],#4 ;R0 [R1] R1 R1+4 LDR R0,[R1,R2] ;R0 [R1+R2] 在第一条指令中, 将寄存器 R1 的内容加上 4 形成操作数的有效地址, 从而取得操作数存入寄存器 R0 中 在第二条指令中, 将寄存器 R1 的内容加上 4 形成操作数的有效地址, 从而取得操作数存入寄存器 R0 中, 然后,R1 的内容自增 4 个字节 在第三条指令中, 以寄存器 R1 的内容作为操作数的有效地址, 从而取得操作数存入寄存器 R0 中, 然后,R1 的内容自增 4 个字节 在第四条指令中, 将寄存器 R1 的内容加上寄存器 R2 的内容形成操作数的有效地址, 从而取得操作数存入寄存器 R0 中 (5) 多寄存器寻址采用多寄存器寻址方式, 一条指令可以完成多个寄存器值的传送 这种寻址方式可以用一条指令完成传送最多 16 个通用寄存器的值 以下指令 : LDMIA R0,{R1,R2,R3,R4} ;R1 [R0] ;R2 [R0+4] ;R3 [R0+8] ;R4 [R0+12] 该指令的后缀 IA 表示在每次执行完加载 / 存储操作后,R0 按字长度增加, 因此, 指令可将连续存储单元的值传送到 R1~R4

38 (6) 相对寻址与基址变址寻址方式相类似, 相对寻址以程序计数器 PC 的当前值为基地址, 指令中的地址标号作为偏移量, 将两者相加之后得到操作数的有效地址 以下程序段完成子程序的调用和返回, 跳转指令 BL 采用了相对寻址方式 : BL NEXT ; 跳转到子程序 NEXT 处执行 NEXT MOV PC,LR ; 从子程序返回 (7) 堆栈寻址堆栈是一种数据结构, 按先进后出 (First In Last Out,FILO) 的方式工作, 使用一个称作堆栈指针的专用寄存器指示当前的操作位置, 堆栈指针总是指向栈顶 当堆栈指针指向最后压入堆栈的数据时, 称为满堆栈 (Full Stack), 而当堆栈指针指向下一个将要放入数据的空位置时, 称为空堆栈 (Empty Stack) 同时, 根据堆栈的生成方式, 又可以分为递增堆栈 (Ascending Stack) 和递减堆栈 (Decending Stack), 当堆栈由低地址向高地址生成时, 称为递增堆栈, 当堆栈由高地址向低地址生成时, 称为递减堆栈 这样就有四种类型的堆栈工作方式,ARM 微处理器支持这四种类型的堆栈工作方式, 即 : 满递增堆栈 : 堆栈指针指向最后压入的数据, 且由低地址向高地址生成 满递减堆栈 : 堆栈指针指向最后压入的数据, 且由高地址向低地址生成 空递增堆栈 : 堆栈指针指向下一个将要放入数据的空位置, 且由低地址向高地址生成 空递减堆栈 : 堆栈指针指向下一个将要放入数据的空位置, 且由高地址向低地址生成 ARM 指令集 ARM 微处理器是基于 RISC 原理而设计的 通常情况下,ARM 微处理器的指令可以分为以下两类 :ARM 指令集和 THUMB 指令集 其中 ARM 指令集为 32 位长度的指令, 而 THUMB 指令集的指令均为 16 位长度, 所以使用 THUMB 指令可以节省至少 30%~40% 的存储空间 ARM 微处理器的所有指令都是加载 / 存储型的, 这也意味 ARM 指令仅能处理寄存器中的数据, 而且处理结果要放回寄存器中, 对系统存储器的访问需要通过专门的加载 / 存储指令来完成 ARM 微处理器的指令集可以分为下列六大类 : 跳转指令 数据处理指令 程序状态寄存器 (PSR) 处理指令

39 加载 / 存储 (Load/Store) 指令 协处理器指令 异常中断产生指令 1. 跳转指令 跳转指令用于实现程序流程的跳转, 在 ARM 程序中有两种方法可以实现程序流程的跳转 : 一种是使用专门的跳转指令, 另一种是直接向程序计数器 PC 写入跳转地址值来实现跳转 通过向程序计数器 PC 写入跳转地址值, 可以实现在 4GB 的地址空间中的任意跳转, 在跳转之前结合使用 MOV LR,PC 等类似指令, 可以保存将来的返回地址值, 从而实现在 4GB 连续的线性地址空间的子程序调用 ARM 指令集中的跳转指令可以完成从当前指令向前或向后的 32MB 的地址空 间的跳转, 包括以下 4 条指令 : 表 2-4ARM 跳转指令 指令助记符 指令格式 功能描述 示例 B B{ 条件 } 目标地 B 指令是最简单的跳转指令 一旦遇 例 1:B Label 址 到一个 B 指令,ARM 处理器将立即跳转到给定的目标地址, 从那里继续执行 注意存储在跳转指令中的实际值是相对当前 PC 值的一个偏移量, 而不是一个绝对地址, 它的值由汇编器来计算 ( 参考寻址方式中的相对寻址 ) 解释 : 程序无条件跳转到标号 Label 处执行例 2: CMP R1,#0 BEQ Label 解释 : 当 CPSR 寄存器中的 Z 条件码置位时, 程序跳转到标号 Label 处执行 BL BL{ 条件 } 目 标 BL 是另一个跳转指令, 但跳转之前, 例 1:BL Label 地址 会在寄存器 R14 中保存 PC 的当前内容, 因此, 可以通过将 R14 的内容重新加载到 PC 中, 来返回到跳转指令之后的那个指令处执行 该指令是实现子程序调用的一个基本但常用的手段 解释 : 当程序无条件跳转到标号 Label 处执行时, 同时将当前的 PC 值保存到 R14 中 BLX BLX 目标地址 BLX 指令从 ARM 指令集跳转到指令中所指定的目标地址, 并将处理器的工作状态有 ARM 状态切换到 Thumb 状态, 该指令同时将 PC 的当前内容

40 BX BX{ 条件 } 目 标地址 保存到寄存器 R14 中 因此, 当子程 序使用 Thumb 指令集, 而调用者使 用 ARM 指令集时, 可以通过 BLX 指 令实现子程序的调用和处理器工作状态的切换 同时, 子程序的返回可以通过将寄存器 R14 值复制到 PC 中来完成 BX 指令跳转到指令中所指定的目标地址, 目标地址处的指令既可以是 ARM 指令, 也可以是 Thumb 指令 ADRL R0,Thumb Fun+1 BX R0 解释 : 跳转到 R0 指定的地址, 并根据 R0 的最低位来切换处理器状态 2. 数据处理指令 ARM 数据处理指令可分为数据传送指令 算术逻辑运算指令和比较指令等 其指令格式和功能如表 2-5 所示 数据传送指令用于在寄存器和存储器之间进行数据的双向传输 算术逻辑运算指令完成常用的算术与逻辑的运算, 该类指令不但将运算结果保存在目的寄存器中, 同时更新 CPSR 中的相应条件标志位 比较指令不保存运算结果, 只更新 CPSR 中相应的条件标志位 表 2-5 ARM 数据处理指令 指 指令 指令格式及功能描述 示例 令 助记 类 符 型 数据传送指令 MOV 格式 :MOV{ 条件 }{S} 目的寄存器, 源操作数功能 :MOV 指令可完成从另一个寄存器 被移位的寄存器或将一个立即数加载到目的寄存器 其中 S 选项决定指令的操作是否影响 CPSR 中条件标志位的值, 当没有 S 时指令不更新 CPSR 中条件标志位的值 MVN 格式 :MVN{ 条件 }{S} 目的寄存器, 源操作数功能 :MVN 指令可完成从另一个寄存器 被移位的寄存器 或将一个立即数加载到目的寄存器 与 MOV 指令不同之处是在传送之前按位被取反了, 即把一个被取反的值传送到目的寄存器中 其中 S 决定指令的操作是否影响 CPSR 中条件标志位的值, 当没有 S 时指令不更新 CPSR 中条件标志位的值 例 : MOV R1,R0,LSL#3 解释 : 将寄存器 R0 的值左移 3 位后传送到 R1 例 : MVN R0,#0 解释 : 将立即数 0 取反传送到寄存器 R0 中, 完成后 R0=-1

41 算术逻辑运算指令 ADD 格式 :ADD{ 条件 }{S} 目的寄存器, 操作数 1, 操作数 2 功能 :ADD 指令用于把两个操作数相加, 并将结果存放到目的寄存器中 操作数 1 应是一个寄存器, 操作数 2 可以是一个寄存器, 被移位的寄存器, 或一个立即数 ADC 格式 :ADC{ 条件 }{S} 目的寄存器, 操作数 1, 操作数 2 功能 :ADC 指令用于把两个操作数相加, 再加上 CPSR 中的 C 条件标志位的值, 并将结果存放到目的寄存器中 它使用一个进位标志位, 这样就可以做比 32 位大的数的加法, 注意不要忘记设置 S 后缀来更改进位标志 操作数 1 应是一个寄存器, 操作数 2 可以是一个寄存器, 被移位的寄存器, 或一个立即数 SUB 格式 :SUB{ 条件 }{S} 目的寄存器, 操作数 1, 操作数 2 功能 :SUB 指令用于把操作数 1 减去操作数 2, 并将结果存放到目的寄存器中 操作数 1 应是一个寄存器, 操作数 2 可以是一个寄存器, 被移位的寄存器, 或一个立即数 该指令可用于有符号数或无符号数的减法运算 SBC 格式 :SBC{ 条件 }{S} 目的寄存器, 操作数 1, 操作数 2 功能 : 于把操作数 1 减去操作数 2, 再减去 CPSR 中的 C 条件标志位的反码, 并将结果存放到目的寄存器中 操作数 1 应是一个寄存器, 操作数 2 可以是一个寄存器, 被移位的寄存器, 或一个立即数 该指令使用进位标志来表示借位, 这样就可以做大于 32 位的减法, 注意不要忘记设置 S 后缀来更改进位标志 该指令可用于有符号数或无符号数的减法运算 RSB 格式 :RSB{ 条件 }{S} 目的寄存器, 操作数 1, 操作数 2 功能 : 指令称为逆向减法指令, 用于把操作数 2 减去操作数 1, 并将结果存放到目的寄存器中 操作数 1 应是一个寄存器, 操作数 2 可以是一个寄存器, 被移位的寄存器, 或一个立即数 该指令可用于有符号数或无符号数的减法运算 RSC 格式 :RSC{ 条件 }{S} 目的寄存器, 操作数 1, 操作数 2 功能 : 指令用于把操作数 2 减去操作数 1, 再减去 CPSR 中的 C 条件标志位的反码, 并将结果存放到目的寄存器中 操作数 1 应是一个寄存器, 操作数 2 可以是一个寄存器, 被移位的寄存器, 或一个立即数 该指令使用进位标志来表示借位, 这样就可以做大于 32 位的减法, 注意不要忘记设置 S 后缀来更改进位标志 该指令可用于有符号数或无符号数的减法运算 ADD R0,R1,R2 ; R0 = R1 + R2 ADD R0,R2,R3,LSL#1 ; R0 = R2 + (R3 << 1) ADC R3,R7,R11 ; R3=R7+R11+C SUB R0,R1,R2 ; R0 = R1 - R2 SUB R0,R1,#256 ; R0 = R1-256 SUB R0,R2,R3,LSL# ;R0 = R2 - (R3 << 1) SUBS R0,R1,R2 ; R0 = R1 - R2 -!C, 并根据结果设置 CPSR 的进位标志位 RSB R0,R1,R2 ; R0 = R2 R1 RSB R0,R1,#256 ; R0 = 256 R1 RSB R0,R2,R3,LSL#1 ; R0 = (R3 << 1) - R2 RSC R0,R1,R2 ; R0 = R2 R1 -!C

42 比较指令 AND 格式 :AND{ 条件 }{S} 目的寄存器, 操作数 1, 操作数 2 功能 : 指令用于在两个操作数上进行逻辑与运算, 并把结果放置到目的寄存器中 操作数 1 应是一个寄存器, 操作数 2 可以是一个寄存器, 被移位的寄存器, 或一个立即数 该指令常用于屏蔽操作数 1 的某些位 ORR 格式 :ORR{ 条件 }{S} 目的寄存器, 操作数 1, 操作数 2 功能 : 指令用于在两个操作数上进行逻辑或运算, 并把结果放置到目的寄存器中 操作数 1 应是一个寄存器, 操作数 2 可以是一个寄存器, 被移位的寄存器, 或一个立即数 该指令常用于设置操作数 1 的某些位 EOR 格式 :EOR{ 条件 }{S} 目的寄存器, 操作数 1, 操作数 2 功能 : 指令用于在两个操作数上进行逻辑异或运算, 并把结果放置到目的寄存器中 操作数 1 应是一个寄存器, 操作数 2 可以是一个寄存器, 被移位的寄存器, 或一个立即数 该指令常用于反转操作数 1 的某些位 BIC 格式 :BIC{ 条件 }{S} 目的寄存器, 操作数 1, 操作数 2 功能 : 指令用于清除操作数 1 的某些位, 并把结果放置到目的寄存器中 操作数 1 应是一个寄存器, 操作数 2 可以是一个寄存器, 被移位的寄存器, 或一个立即数 操作数 2 为 32 位的掩码, 如果在掩码中设置了某一位, 则清除这一位 未设置的掩码位保持不变 CMP 格式 :CMP{ 条件 } 操作数 1, 操作数 2 功能 :CMP 指令用于把一个寄存器的内容和另一个寄存器的内容或立即数进行比较, 同时更新 CPSR 中条件标志位的值 该指令进行一次减法运算, 但不存储结果, 只更改条件标志位 标志位表示的是操作数 1 与操作数 2 的关系 ( 大 小 相等 ), 例如, 当操作数 1 大于操作操作数 2, 则此后的有 GT 后缀的指令将可以执行 CMN 格式 :CMN{ 条件 } 操作数 1, 操作数 2 功能 :CMN 指令用于把一个寄存器的内容和另一个寄存器的内容或立即数取反后进行比较, 同时更新 CPSR 中条件标志位的值 该指令实际完成操作数 1 和操作数 2 相加, 并根据结果更改条件标志位 TST 格式 :TST{ 条件 } 操作数 1, 操作数 2 功能 :TST 指令用于把一个寄存器的内容和另一个寄存器的内容或立即数进行按位的与运算, 并根据运算结果更新 CPSR 中条件标志位的值 操作数 1 是要测试的数据, 而操作数 2 是一个位掩码, 该指令一般用来检测是否设置了特定的位 AND R0,R0,#3 ; 该指令保持 R0 的 0 1 位, 其余位清零 ORR R0,R0,#3 ; 该指令设置 R0 的 0 1 位, 其余位保持不变 EOR R0,R0,#3 ; 该指令反转 R0 的 0 1 位, 其余位保持不变 BIC R0,R0,#%1011 ; 该指令清除 R0 中的位 0 1 和 3, 其余的位保持不变 例 :CMP R1,R0 解释 : 将寄存器 R1 的值与寄存器 R0 的值相减, 并根据结果设置 CPSR 的标志位例 :CMN R1,R0 解释 : 将寄存器 R1 的值与寄存器 R0 的值相加, 并根据结果设置 CPSR 的标志位例 :TST R1,#0xffe 解释 : 将寄存器 R1 的值与立即数 0xffe 按位与, 并根据结果设置 CPSR 的标志位

43 TEQ 格式 :TEQ{ 条件 } 操作数 1, 操作数 2 功能 :TEQ 指令用于把一个寄存器的内容和另一个寄存器的内容或立即数进行按位的异或运算, 并根据运算结果更新 CPSR 中条件标志位的值 该指令通常用于比较操作数 1 和操作数 2 是否相等 例 :TEQ R1,R2 解释 : 将寄存器 R1 的值与寄存器 R2 的值按位异或, 并根据结果设置 CPSR 的标志位 3. 乘法指令与乘加指令 ARM 微处理器支持的乘法指令与乘加指令共有 6 条, 可分为运算结果为 32 位 和运算结果为 64 位两类, 与前面的数据处理指令不同, 指令中的所有操作数 目的 寄存器必须为通用寄存器, 不能对操作数使用立即数或被移位的寄存器, 同时, 目 的寄存器和操作数 1 必须是不同的寄存器 表 2-6 乘法指令与乘加指令 指令助 指令格式 功能及示例 记符 MUL 格式 :MUL{ 条件 }{S} 目的寄存器, 操作数 1, 操作数 2 功能 : 指令完成将操作数 1 与操作数 2 的乘法运算, 并把结果放置到目的寄存器中, 同时可以根据运算结果设置 CPSR 中相应的条件标志位 其中, 操作数 1 和操作数 2 均为 32 位的有符号数或无符号数 例 :MUL R0,R1,R2 ;R0 = R1 R2 MULS R0,R1,R2 ;R0 = R1 R2, 同时设置 CPSR 中的相关条 件标志位 MLA 格式 :MLA{ 条件 }{S} 目的寄存器, 操作数 1, 操作数 2, 操作数 3 功能 :MLA 指令完成将操作数 1 与操作数 2 的乘法运算, 再将乘积加上操作数 3, 并把结果放置到目的寄存器中, 同时可以根据运算结果设置 CPSR 中相应的条件标 志位 其中, 操作数 1 和操作数 2 均为 32 位的有符号数或无符号数 例 :MLA R0,R1,R2,R3 ;R0 = R1 R2 + R3 MLAS R0,R1,R2,R3 ;R0 = R1 R2 + R3, 同时设置 CPSR 中 的相关条件标志位 SMULL 格式 :SMULL{ 条件 }{S} 目的寄存器 Low, 目的寄存器低 High, 操作数 1, 操作数 2 功能 :SMULL 指令完成将操作数 1 与操作数 2 的乘法运算, 并把结果的低 32 位放置 到目的寄存器 Low 中, 结果的高 32 位放置到目的寄存器 High 中, 同时可以根据运算 结果设置 CPSR 中相应的条件标志位 其中, 操作数 1 和操作数 2 均为 32 位的有符 号数 例 :SMULL R0,R1,R2,R3 ;R0 = (R2 R3) 的低 32 位 ;R1 = (R2 R3) 的高 32 位

44 SMLAL 格式 :SMLAL{ 条件 }{S} 目的寄存器 Low, 目的寄存器低 High, 操作数 1, 操作数 2 功能 :SMLAL 指令完成将操作数 1 与操作数 2 的乘法运算, 并把结果的低 32 位同目 的寄存器 Low 中的值相加后又放置到目的寄存器 Low 中, 结果的高 32 位同目的寄存 器 High 中的值相加后又放置到目的寄存器 High 中, 同时可以根据运算结果设置 CPSR 中相应的条件标志位 其中, 操作数 1 和操作数 2 均为 32 位的有符号数 对于目的寄存器 Low, 在指令执行前存放 64 位加数的低 32 位, 指令执行后存放 结果的低 32 位 对于目的寄存器 High, 在指令执行前存放 64 位加数的高 32 位, 指令执行后存放 结果的高 32 位 例 :SMLAL R0,R1,R2,R3 ;R0 = (R2 R3) 的低 32 位 + R0 ;R1 = (R2 R3) 的高 32 位 + R1 UMULL 格式 :UMULL{ 条件 }{S} 目的寄存器 Low, 目的寄存器低 High, 操作数 1, 操作数 2 功能 :UMULL 指令完成将操作数 1 与操作数 2 的乘法运算, 并把结果的低 32 位放置 到目的寄存器 Low 中, 结果的高 32 位放置到目的寄存器 High 中, 同时可以根据运算 结果设置 CPSR 中相应的条件标志位 其中, 操作数 1 和操作数 2 均为 32 位的无符 号数 例 :UMULL R0,R1,R2,R3 ;R0 = (R2 R3) 的低 32 位 ;R1 = (R2 R3) 的高 32 位 UMLAL 格式 :UMLAL{ 条件 }{S} 目的寄存器 Low, 目的寄存器低 High, 操作数 1, 操作数 2 功能 :UMLAL 指令完成将操作数 1 与操作数 2 的乘法运算, 并把结果的低 32 位同目 的寄存器 Low 中的值相加后又放置到目的寄存器 Low 中, 结果的高 32 位同目的寄存 器 High 中的值相加后又放置到目的寄存器 High 中, 同时可以根据运算结果设置 CPSR 中相应的条件标志位 其中, 操作数 1 和操作数 2 均为 32 位的无符号数 对于目的寄存器 Low, 在指令执行前存放 64 位加数的低 32 位, 指令执行后存放 结果的低 32 位 对于目的寄存器 High, 在指令执行前存放 64 位加数的高 32 位, 指令执行后存放 结果的高 32 位 例 :UMLAL R0,R1,R2,R3 ;R0 = (R2 R3) 的低 32 位 + R0 4. 程序状态寄存器访问指令 ;R1 = (R2 R3) 的高 32 位 + R1 ARM 微处理器支持程序状态寄存器访问指令, 用于在程序状态寄存器和通用寄存器之间传送数据, 程序状态寄存器访问指令包括以下两条 : 表 2-7 程序状态寄存器访问指令 指令助 指令格式 功能及示例 记符 MRS 格式 :MRS{ 条件 } 通用寄存器, 程序状态寄存器 (CPSR 或 SPSR) 功能 : 用于将程序状态寄存器的内容传送到通用寄存器中 该指令一般用在以下几种

45 情况 : 当需要改变程序状态寄存器的内容时, 可用 MRS 将程序状态寄存器的内容 读入通用寄存器, 修改后再写回程序状态寄存器 当在异常处理或进程切换时, 需要保存程序状态寄存器的值, 可先用该指令读出程序状态寄存器的值, 然后保存 例 : MRS R0,CPSR ; 传送 CPSR 的内容到 R0 MRS R0,SPSR ; 传送 SPSR 的内容到 R0 MSR 格式 :MSR{ 条件 } 程序状态寄存器 (CPSR 或 SPSR)_< 域 >, 操作数功能 :MSR 指令用于将操作数的内容传送到程序状态寄存器的特定域中 其中, 操作数可以为通用寄存器或立即数 < 域 > 用于设置程序状态寄存器中需要操作的位,32 位的程序状态寄存器可分为 4 个域 : 位 [31:24] 为条件标志位域, 用 f 表示 ; 位 [23:16] 为状态位域, 用 s 表示 ; 位 [15:8] 为扩展位域, 用 x 表示 ; 位 [7:0] 为控制位域, 用 c 表示 ; 该指令通常用于恢复或改变程序状态寄存器的内容, 在使用时, 一般要在 MSR 指令中指明将要操作的域 例 : MSR CPSR,R0 ; 传送 R0 的内容到 CPSR MSR SPSR,R0 ; 传送 R0 的内容到 SPSR MSR CPSR_c,R0 ; 传送 R0 的内容到 SPSR, 但仅仅修改 CPSR 中的控制位域 5. 加载 / 存储指令 ARM 微处理器支持加载 / 存储指令用于在寄存器和存储器之间传送数据, 加载指令用于将存储器中的数据传送到寄存器, 存储指令则完成相反的操作 常用的加载存储指令如表 2-8 所示 表 2-8 加载 / 存储指令指令助指令格式 功能及示例记符格式 :LDR{ 条件 } 目的寄存器,< 存储器地址 > 功能 :LDR 指令用于从存储器中将一个 32 位的字数据传送到目的寄存器中 该指令通常用于从存储器中读取 32 位的字数据到通用寄存器, 然后对数据进行处理 当程序计数器 PC 作为目的寄存器时, 指令从存储器中读取的字数据被当作目的地址, 从而可以实现程序流程的跳转 该指令在程序设计中比较常用, 且寻址方式灵活多样, 请读者 LDR 认真掌握 例 : LDR R0,[R1] ; 将存储器地址为 R1 的字数据读入寄存器 R0 LDR R0,[R1,R2] ; 将存储器地址为 R1+R2 的字数据读入寄存器 R0 LDR R0,[R1,#8] ; 将存储器地址为 R1+8 的字数据读入寄存器 R0 ; 将存储器地址为 R1+R2 的字数据读入寄存器 R0, 并将地址 R1+R2 写入 R1

46 LDR R0,[R1,R2]! ; 将存储器地址为 R1+8 的字数据读入寄存器 R0, 并将新地址 R1+8 写入 R1 LDR R0,[R1,#8]! ; 将存储器地址为 R1 的字数据读入寄存器 R0, 并将新地址 R1+R2 写入 R1 LDR R0,[R1],R2 ; 将存储器地址为 R1+R2 4 的字数据读入寄存器 R0, 并将新地址 R1+R2 ; 4 写入 R1 LDR R0,[R1,R2,LSL#2]! ; 将存储器地址为 R1 的字数据读入寄存器 R0, 并将新地址 R1+R2 4 写入 ; R1 LDR R0,[R1],R2,LSL#2 LDRB 格式 :LDR{ 条件 }B 目的寄存器,< 存储器地址 > 功能 :LDRB 指令用于从存储器中将一个 8 位的字节数据传送到目的寄存器中, 同时将寄存器的高 24 位清零 该指令通常用于从存储器中读取 8 位的字节数据到通用寄存器, 然后对数据进行处理 当程序计数器 PC 作为目的寄存器时, 指令从存储器中读取的字数据被当作目的地址, 从而可以实现程序流程的跳转 例 : ; 将存储器地址为 R1 的字节数据读入寄存器 R0, 并将 R0 的高 24 位清零 LDRB R0,[R1] ; 将存储器地址为 R1+8 的字节数据读入寄存器 R0, 并将 R0 的高 24 位清零 LDRB R0,[R1,#8] LDRH 格式 :LDR{ 条件 }H 目的寄存器,< 存储器地址 > 功能 :LDRH 指令用于从存储器中将一个 16 位的半字数据传送到目的寄存器中, 同时将寄存器的高 16 位清零 该指令通常用于从存储器中读取 16 位的半字数据到通用寄存器, 然后对数据进行处理 当程序计数器 PC 作为目的寄存器时, 指令从存储器中读取的字数据被当作目的地址, 从而可以实现程序流程的跳转 例 : ; 将存储器地址为 R1 的半字数据读入寄存器 R0, 并将 R0 的高 16 位清零 LDRH R0,[R1] ; 将存储器地址为 R1+8 的半字数据读入 R0, 并将 R0 的高 16 位清零 LDRH R0,[R1,#8] ; 将存储器地址为 R1+R2 的半字数据读入 R0, 并将 R0 的高 16 位清零 LDRH R0,[R1,R2] STR 格式 :STR{ 条件 } 源寄存器,< 存储器地址 > 功能 :STR 指令用于从源寄存器中将一个 32 位的字数据传送到存储器中 该指令在程序设计中比较常用, 且寻址方式灵活多样, 使用方式可参考指令 LDR 例 : 将 R0 中的字数据写入以 R1 为地址的存储器中, 并将新地址 R1+8 写入 R1 STR R0,[R1],#8 STR R0,[R1,#8] ; 将 R0 中的字数据写入以 R1+8 为地址的存储器中 STRB 格式为 :STR{ 条件 }B 源寄存器,< 存储器地址 > 功能 :STRB 指令用于从源寄存器中将一个 8 位的字节数据传送到存储器中 该字节数

47 据为源寄存器中的低 8 位 例 : STRB R0,[R1] ; 将 R0 中的字节数据写入以 R1 为地址的存储器中 STRB R0,[R1,#8] ; 将 R0 中的字节数据写入以 R1+8 为地址的存储器中 STRH 格式 :STR{ 条件 }H 源寄存器,< 存储器地址 > 功能 :STRH 指令用于从源寄存器中将一个 16 位的半字数据传送到存储器中 该半字 数据为源寄存器中的低 16 位 例 :STRH STRH R0,[R1] ; 将寄存器 R0 中半字数据写入以 R1 为地址的存储器中 R0,[R1,#8]; 将寄存器 R0 中半字数据写入以 R1+8 为地址的存储器中 6. 批量数据加载 / 存储指令 ARM 微处理器所支持批量数据加载 / 存储指令可以一次在一片连续的存储器单 元和多个寄存器之间传送数据, 批量加载指令用于将一片连续的存储器中的数据传 送到多个寄存器, 批量数据存储指令则完成相反的操作 常用的加载存储指令如表 2-9 所示 表 2-9 批量数据加载 / 存储指令 指 令 指令格式 功能及示例 助 记 符 格式 :LDM( 或 STM){ 条件 }{ 类型 } 基址寄存器 {!}, 寄存器列表 { } 功能 :LDM( 或 STM) 指令用于从由基址寄存器所指示的一片连续存储器到寄存器列表所指示的多个寄存器之间传送数据, 该指令的常见用途是将多个寄存器的内容入栈或出栈 其中,{ 类型 } 为以下几种情况 : IA 每次传送后地址加 1; IB 每次传送前地址加 1; DA 每次传送后地址减 1; DB 每次传送前地址减 1; LDM FD 满递减堆栈 ; STM ED 空递减堆栈 ; FA 满递增堆栈 ; EA 空递增堆栈 ; {!} 为可选后缀, 若选用该后缀, 则当数据传送完毕之后, 将最后的地址写入基址 寄存器, 否则基址寄存器的内容不改变 基址寄存器不允许为 R15, 寄存器列表可以为 R0~R15 的任意组合 { } 为可选后缀, 当指令为 LDM 且寄存器列表中包含 R15, 选用该后缀时表示 : 除了正常的数据传送之外, 还将 SPSR 复制到 CPSR 同时, 该后缀还表示传入或传出 的是用户模式下的寄存器, 而不是当前模式下的寄存器 例 : ; 将寄存器列表中的寄存器 (R0,R4 到 R12,LR) 存入堆栈 STMFD R13!,{R0,R4-R12,LR}

48 LDMFD R13!,{R0,R4-R12,PC} ; 将堆栈内容恢复到 R0,R4 到 R12,LR 7. 数据交换指令 ARM 微处理器所支持数据交换指令能在存储器和寄存器之间交换数据 数据交换指令有如下两条 : SWP 字数据交换指令 SWPB 字节数据交换指令 表 2-10 数据交换指令 指 令 指令格式 功能及示例 助 记 符 SWP 格式 :SWP{ 条件 } 目的寄存器, 源寄存器 1,[ 源寄存器 2] 功能 :SWP 指令用于将源寄存器 2 所指向的存储器中的字数据传送到目的寄存器中, 同时将源寄存器 1 中的字数据传送到源寄存器 2 所指向的存储器中 显然, 当源寄存器 1 和目的寄存器为同一个寄存器时, 指令交换该寄存器和存储器的内容 例 :SWP R0,R1,[R2] ; 将 R2 所指向的存储器中的字数据传送到 R0, 同时将 R1 ; 中的字数据传送到 R2 所指向的存储单元 SWP R0,R0,[R1]; 将 R1 所指向的存储器中的字数据与 R0 中的字数据交换 SWPB 格式 :SWP{ 条件 }B 目的寄存器, 源寄存器 1,[ 源寄存器 2] 功能 :SWPB 指令用于将源寄存器 2 所指向的存储器中的字节数据传送到目的寄存器中, 目的寄存器的高 24 清零, 同时将源寄存器 1 中的字节数据传送到源寄存器 2 所指向的 存储器中 显然, 当源寄存器 1 和目的寄存器为同一个寄存器时, 指令交换该寄存器和 存储器的内容 例 :SWPB R0,R1,[R2] ; 将 R2 所指向的存储器中的字节数据传送到 R0,R0 ; 的高 24 位清 ; 零, 同时将 R1 中的低 8 位数据传送到 R2 所指向的存储单元 SWPB R0,R0,[R1] ; 该指令完成将 R1 所指向的存储器中的字节数据与 ;R0 中的低 8 位数据交换 8. 移位指令 ( 操作 ) ARM 微处理器内嵌的桶型移位器 (Barrel Shifter), 支持数据的各种移位操作, 移位操作在 ARM 指令集中不作为单独的指令使用, 它只能作为指令格式中是一个字段, 在汇编语言中表示为指令中的选项 例如, 数据处理指令的第二个操作数为寄存器时, 就可以加入移位操作选项对它进行各种移位操作 移位操作包括有 6 种类型, 其中 ASL 和 LSL 是等价的 表 2-11 移位指令指令助指令格式 功能及示例记符 LSL ( 或格式为 : 通用寄存器,LSL( 或 ASL) 操作数 ASL) 功能 :LSL( 或 ASL) 可完成对通用寄存器中的内容进行逻辑 ( 或算术 ) 的左移操作,

49 按操作数所指定的数量向左移位, 低位用零来填充 其中, 操作数可以是通用寄存器, 也可以是立即数 (0~31) 例 :MOV R0, R1, LSL#2 ; 将 R1 中的内容左移两位后传送到 R0 中 LSR ASR ROR RRX 操作的格式为 : 通用寄存器,LSR 操作数功能 :LSR 可完成对通用寄存器中的内容进行右移的操作, 按操作数所指定的数量向右移位, 左端用零来填充 其中, 操作数可以是通用寄存器, 也可以是立即数 (0~ 31) 例 :MOV R0, R1, LSR#2; 将 R1 中的内容右移两位后传送到 R0 中, 左端用零来填充格式 : 通用寄存器,ASR 操作数功能 :ASR 可完成对通用寄存器中的内容进行右移的操作, 按操作数所指定的数量向右移位, 左端用第 31 位的值来填充 其中, 操作数可以是通用寄存器, 也可以是立即数 (0~31) 例 :MOV R0, R1, ASR#2 ; 将 R1 中的内容右移两位后传送到 R0 中, 左端用第 31 位的值来填充 格式 : 通用寄存器,ROR 操作数功能 :ROR 可完成对通用寄存器中的内容进行循环右移的操作, 按操作数所指定的数量向右循环移位, 左端用右端移出的位来填充 其中, 操作数可以是通用寄存器, 也可以是立即数 (0~31) 显然, 当进行 32 位的循环右移操作时, 通用寄存器中的值不改变 例 :MOV R0, R1, ROR#2 ; 将 R1 中的内容循环右移两位后传送到 R0 中 格式 : 通用寄存器,RRX 操作数功能 :RRX 可完成对通用寄存器中的内容进行带扩展的循环右移的操作, 按操作数所指定的数量向右循环移位, 左端用进位标志位 C 来填充 其中, 操作数可以是通用寄存器, 也可以是立即数 (0~31) 例 :MOV R0, R1, RRX#2; 将 R1 的内容进行带扩展的循环右移两位后传送到 R0 中 9. 协处理器指令 ARM 微处理器可支持多达 16 个协处理器, 用于各种协处理操作, 在程序执行的过程中, 每个协处理器只执行针对自身的协处理指令, 忽略 ARM 处理器和其他协处理器的指令 ARM 的协处理器指令主要用于 ARM 处理器初始化 ARM 协处理器的数据处理操作, 以及在 ARM 处理器的寄存器和协处理器的寄存器之间传送数据, 和在 ARM 协处理器的寄存器和存储器之间传送数据 表 2-12 协处理器指令 指令助记符 指令格式 功能及示例

50 CDP 格式 :CDP{ 条件 } 协处理器编码, 协处理器操作码 1, 目的寄存器, 源寄存器 1, 源寄存器 2, 协处理器操作码 2 功能 :CDP 指令用于 ARM 处理器通知 ARM 协处理器执行特定的操作, 若协处理器不能成功完成特定的操作, 则产生未定义指令异常 其中协处理器操作码 1 和协处理器操作码 2 为协处理器将要执行的操作, 目的寄存器和源寄存器均为协处理器的寄存器, 指令不涉及 ARM 处理器的寄存器和存储器 例 :CDP P3,2,C12,C10,C3,4 ; 该指令完成协处理器 P3 的初始化 LDC 格式 :LDC{ 条件 }{L} 协处理器编码, 目的寄存器,[ 源寄存器 ] 功能 :LDC 指令用于将源寄存器所指向的存储器中的字数据传送到目的寄存器中, 若协处理器不能成功完成传送操作, 则产生未定义指令异常 其中,{L} 选项表示指令为长读取操作, 如用于双精度数据的传输 例 :LDC P3,C4,[R0] ; 将寄存器 R0 所指向的存储器中的字数据传送到协处理器 P3 的寄存器 C4 中 STC 格式 :STC{ 条件 }{L} 协处理器编码, 源寄存器,[ 目的寄存器 ] 功能 :STC 指令用于将源寄存器中的字数据传送到目的寄存器所指向的存储器中, 若协处理器不能成功完成传送操作, 则产生未定义指令异常 其中,{L} 选项表示指令为长读取操作, 如用于双精度数据的传输 例 :STC P3,C4,[R0] ; 将协处理器 P3 的寄存器 C4 中的字数据传送到寄存器 R0 所指向的存储器中 MCR 格式 :MCR{ 条件 } 协处理器编码, 协处理器操作码 1, 源寄存器, 目的寄存器 1, 目的寄存器 2, 协处理器操作码 2 功能 :MCR 指令用于将 ARM 处理器寄存器中的数据传送到协处理器寄存器中, 若协处理器不能成功完成操作, 则产生未定义指令异常 其中协处理器操作码 1 和协处理器操作码 2 为协处理器将要执行的操作, 源寄存器为 ARM 处理器的寄存器, 目的寄存器 1 和目的寄存器 2 均为协处理器的寄存器 例 :MCR P3,3,R0,C4,C5,6 ; 该指令将寄存器 R0 中的数据传送到协处理器 P3 的寄存器 C4 和 C5 中 MRC 格式 :MRC{ 条件 } 协处理器编码, 协处理器操作码 1, 目的寄存器, 源寄存器 1, 源寄存器 2, 协处理器操作码 2 功能 :MRC 指令用于将协处理器寄存器中的数据传送到 ARM 处理器寄存器中, 若协处理器不能成功完成操作, 则产生未定义指令异常 其中协处理器操作码 1 和协处理器操作码 2 为协处理器将要执行的操作, 目的寄存器为 ARM 处理器的寄存器, 源寄存器 1 和源寄存器 2 均为协处理器的寄存器 例 :MRC P3,3,R0,C4,C5,6 ; 该指令将协处理器 P3 的寄存器中的数据传送到 ARM 处理器寄存器中 10. 异常产生指令 ARM 微处理器所支持的异常指令有如下两条 :

51 指令助记符 SWI SWI 软件中断指令 BKPT 断点中断指令 表 2-13 异常产生指令指令格式 功能及示例 格式 :SWI{ 条件 } 24 位的立即数功能 :SWI 指令用于产生软件中断, 以便用户程序能调用操作系统的系统例程 操作系统在 SWI 的异常处理程序中提供相应的系统服务, 指令中 24 位的立即数指定用户程序调用系统例程的类型, 相关参数通过通用寄存器传递, 当指令中 24 位的立即数被忽略时, 用户程序调用系统例程的类型由通用寄存器 R0 的内容决定, 同时, 参数通过其他通用寄存器传递 例 :SWI 0x02 ; 该指令调用操作系统编号位 02 的系统例程 BKPT 格式 :BKPT 16 位的立即数功能 :BKPT 指令产生软件断点中断, 可用于程序的调试 Thumb 指令及应用 为兼容数据总线宽度为 16 位的应用系统,ARM 体系结构除了支持执行效率很高的 32 位 ARM 指令集以外, 同时支持 16 位的 Thumb 指令集 Thumb 指令集是 ARM 指令集的一个子集, 允许指令编码为 16 位的长度 与等价的 32 位代码相比较, Thumb 指令集在保留 32 代码优势的同时, 大大的节省了系统的存储空间 所有的 Thumb 指令都有对应的 ARM 指令, 而且 Thumb 的编程模型也对应于 ARM 的编程模型, 在应用程序的编写过程中, 只要遵循一定调用的规则,Thumb 子程序和 ARM 子程序就可以互相调用 当处理器在执行 ARM 程序段时, 称 ARM 处理器处于 ARM 工作状态, 当处理器在执行 Thumb 程序段时, 称 ARM 处理器处于 Thumb 工作状态 与 ARM 指令集相比较,Thumb 指令集中的数据处理指令的操作数仍然是 32 位, 指令地址也为 32 位, 但 Thumb 指令集为实现 16 位的指令长度, 舍弃了 ARM 指令集的一些特性, 如大多数的 Thumb 指令是无条件执行的, 而几乎所有的 ARM 指令都是有条件执行的 ; 大多数的 Thumb 数据处理指令的目的寄存器与其中一个源寄存器相同 由于 Thumb 指令的长度为 16 位, 即只用 ARM 指令一半的位数来实现同样的功能, 所以, 要实现特定的程序功能, 所需的 Thumb 指令的条数较 ARM 指令多 在一般的情况下,Thumb 指令与 ARM 指令的时间效率和空间效率关系为 : Thumb 代码所需的存储空间约为 ARM 代码的 60%~70% Thumb 代码使用的指令数比 ARM 代码多约 30%~40% 若使用 32 位的存储器,ARM 代码比 Thumb 代码快约 40% 若使用 16 位的存储器,Thumb 代码比 ARM 代码快约 40%~50%

52 与 ARM 代码相比较, 使用 Thumb 代码, 存储器的功耗会降低约 30% 显然,ARM 指令集和 Thumb 指令集各有其优点, 若对系统的性能有较高要求, 应使用 32 位的存储系统和 ARM 指令集, 若对系统的成本及功耗有较高要求, 则应使用 16 位的存储系统和 Thumb 指令集 当然, 若两者结合使用, 充分发挥其各自的优点, 会取得更好的效果 2.4 ARM 汇编语言程序设计 ARM 汇编程序由机器指令和伪指令组成 前面指令集中已经详细介绍了 ARM 中常用的机器指令的格式 功能以及使用示例 那么, 伪指令和机器指令又有什么区别呢?ARM 汇编程序该如何设计呢? 本节将就这些问题展开讨论, 内容包括 : ARM 汇编语言的伪指令 汇编语言的语句格式和汇编语言的程序结构 ARM 汇编器所支持的伪指令 在 ARM 汇编语言程序里, 有一些特殊指令助记符, 这些助记符与指令系统的助记符不同, 没有相对应的操作码, 它们并不是在运行期间由机器执行, 而是在汇编程序对源程序汇编期间由汇编程序处理 这些特殊指令助记符被称作为伪指令, 它们在源程序中的作用是为完成汇编程序作各种准备工作的, 也就是说这些伪指令仅在汇编过程中起作用, 一旦汇编结束, 伪指令的使命就完成了 在 ARM 的汇编程序中, 伪指令种类繁多, 可以细分为如下几种伪指令 : 符号定义伪指令 (Symbol Definition) 数据定义伪指令 (Data Definition) 汇编控制伪指令 (Assembly Control) 宏指令 其他伪指令 1. 符号定义伪指令符号定义伪指令用于定义 ARM 汇编程序中的变量 对变量赋值以及定义寄存器的别名等操作 常见的符号定义伪指令有如下几种 : 用于定义全局变量的 GBLA GBLL 和 GBLS 用于定义局部变量的 LCLA LCLL 和 LCLS 用于对变量赋值的 SETA SETL SETS 为通用寄存器列表定义名称的 RLIST 该类伪指令语法格式及其功能描述如表 2-14 所示 表 2-14 符号定义伪指令 语法格式 功能描述 GBLA(GBLL 和 GBLS) 全局变量名 功能 : 定义全局变量, 并将其初始化 其中,

53 GBLA 伪指令用于定义一个全局的数字变量, 并初始化为 0; GBLL 伪指令用于定义一个全局的逻辑变量, 并初始化为 F( 假 ); GBLS 伪指令用于定义一个全局的字符串变量, 并初始化为空 ; 例 :GBLA number ; 定义一个全局的数字变量, 变量名为 number number SETA 0xaa ; 将该变量赋值为 0xaa GBLS String1; 定义一个变量名为 String1 全局的字符串变量 String1 SETS hello ; 将该变量赋值为 hello LCLA(LCLL 或 LCLS) 局部变量名功能 : 定义局部变量, 并将其初始化 其中, 变量名 SETA(SETL 或 SETS) 表 达式 LCLA 伪指令用于定义一个局部的数字变量, 并初始化为 0; LCLL 伪指令用于定义一个局部的逻辑变量, 并初始化为 F( 假 ); LCLS 伪指令用于定义一个局部的字符串变量, 并初始化为空 ; 例 :LCLA number ; 定义一个局部的数字变量, 变量名为 number number SETA 0xaa ; 将该变量赋值为 0xaa LCLS String1; 定义一个变量名为 String1 全局的字符串变量 String1 SETS hello ; 将该变量赋值为 hello 功能 : 伪指令 SETA SETL SETS 用于给一个已经定义的全局变量或 局部变量赋值 其中, SETA 伪指令用于给一个数学变量赋值 ; SETL 伪指令用于给一个逻辑变量赋值 ; SETS 伪指令用于给一个字符串变量赋值 ; 名称 RLIST { 寄存器列表 } 功能 : 为一个通用寄存器列表定义名称 例 :RegLst RLIST {R0-R5,R8,R10} ; 将寄存器列表 R0-R5, R8,R10 定义为 RegLst, 可在 ARM 指令 LDM/STM 中通过该名称访问 寄存器列表 2. 数据定义伪指令数据定义伪指令用于数据表定义 文字池 数据空间分配等 该类伪指令语法格式和功能描述如表 2-15 所示 LTORG 语法格式 表 2-15 数据定义伪指令功能描述功能 :LTORG 用于声明一个文字池 在使用 LDR 伪指令时, 要在适当的地址处加入 LRORG 声明文字池, 这样就会把要加载的数据保存在文字池内, 再用 ARM 的加载指令读出数据 LTORG 伪指令常放在无条件分支指令之后, 或者子程序返回指令之后 例 : LDR R0,=0X ADD R1,R1,R0 MOV PC,LR LTORG ; 声明文字池, 此地址存储

54 0x 标号 DCB 表达式功能 :DCB 伪指令用于分配一片连续的字节存储单元并用伪指令中 标号 DCW 表达式 表达式初始化 功能 :DCW( 或 DCWU) 伪指令用于分配一片连续的半字存储单 元并用伪指令中指定的表达式初始化 标号 DCD 表达式功能 :DCD( 或 DCDU) 伪指令用于分配一片连续的字存储单元并 标号 DCFD 表达式 用伪指令中指定的表达式初始化 功能 :DCFD( 或 DCFDU) 伪指令用于为双精度的浮点数分配一片 连续的字存储单元并用伪指令中指定的表达式初始化 每个双精度 的浮点数占据两个字单元 标号 DCFS 表达式 功能 :DCFS( 或 DCFSU) 伪指令用于为单精度的浮点数分配一片 连续的字存储单元并用伪指令中指定的表达式初始化 每个单精度 的浮点数占据一个字单元 标号 DCQ 表达式 功能 :DCQ( 或 DCQU) 伪指令用于分配一片以 8 个字节为单位的 连续存储区域并用伪指令中指定的表达式初始化 标号 SPACE 表达式 功能 :SPACE 伪指令用于分配一片连续的存储区域并初始化为 0 其中, 表达式为要分配的字节数 SPACE 也可用 % 代替 MAP 表达式 {, 基址寄存器 } 功能 :MAP 伪指令用于定义一个结构化的内存表的首地址 MAP 也可用 ^ 代替 表达式可以为程序中的标号或数学表达式, 基 址寄存器为可选项, 当基址寄存器选项不存在时, 表达式的值即为内存表的首地址, 当该选项存在时, 内存表的首地址为表达式的值与基址寄存器的和 标号 FIELD 表 达 功能 :FIELD 伪指令用于定义一个结构化内存表中的数据域 FILED 式 也可用 # 代替 表达式的值为当前数据域在内存表中所占的字节数 FIELD 伪指令常与 MAP 伪指令配合使用来定义结构化的内存表 MAP 伪指令定义内存表的首地址,FIELD 伪指令定义内存表中的各个数据域, 并可以为每个数据域指定一个标号供其他的指令引用 注意 MAP 和 FIELD 伪指令仅用于定义数据结构, 并不实际分配存储单元 例 : MAP 0x00,R9; 定义结构化内存表首地址值为 R9 A FIELD 16 ; 定义 A 的长度为 16 字节, 位置为 R9 B FIELD 32 ; 定义 B 的长度为 32 字节, 位置为 R9+16 C FIELD 256 ; 定义 C 的长度为 256 字节, 位置为 R 汇编控制 (Assembly Control) 伪指令 汇编控制伪指令用于控制汇编程序的执行流程, 比如条件汇编 宏定义和重复

55 汇编控制等 该类伪指令语法定义和功能描述如表 2-16 所示 表 2-16 汇编控制伪指令 语法格式 IF 逻辑表达式指令序列 1 ELSE 指令序列 2 ENDIF 功能描述功能 :IF ELSE ENDIF 伪指令能根据条件的成立与否决定是否执行某个指令序列 当 IF 后面的逻辑表达式为真, 则执行指令序列 1, 否则执行指令序列 2 其中,ELSE 及指令序列 2 可以没有, 此时, 当 IF 后面的逻辑表达式为真, 则执行指令序列 1, 否则继续执行后面的指令 WHILE 逻辑表达式 指令序列 功能 :WHILE WEND 伪指令能根据条件的成立与否决定是否循环执 行某个指令序列 当 WHILE 后面的逻辑表达式为真, 则执行指令序列, WEND 该指令序列执行完毕后, 再判断逻辑表达式的值, 若为真则继续执行, 一直到逻辑表达式的值为假 $ 标号宏名 $ 参数 1,$ 功能 :MACRO MEND 伪指令可以将一段代码定义为一个整体, 称为参数 2, 宏指令, 然后就可以在程序中通过宏指令多次调用该段代码 其中,$ 指令序列标号在宏指令被展开时, 标号会被替换为用户定义的符号, 宏指令可 MEND 以使用一个或多个参数, 当宏指令被展开时, 这些参数被相应的值替换 MEXIT 功能 :MEXIT 用于从宏定义中跳转出去 4. 其他常用的伪指令除了上门介绍的伪指令外, 还有一些其他的伪指令, 在汇编程序中经常会被使用, 如段定义伪指令 入口点设置伪指令 包含文件伪指令 标号导出或导入声明等 该类伪指令如表 2-17 所示 表 2-17 AREA 2, 语法格式段名属性 1, 属性 功能描述功能 :AREA 伪指令用于定义一个代码段或数据段 ARM 汇编程序设计采用分段式程序设计, 一个 ARM 程序至少需要一个代码段 属性字段表示该代码段 ( 或数据段 ) 的相关属性, 多个属性用逗号分隔 常用的属性如下 : CODE 属性 : 用于定义代码段, 默认为 READONLY DATA 属性 : 用于定义数据段, 默认为 READWRITE READONLY 属性 : 指定本段为只读, 代码段默认为 READONLY READWRITE 属性 : 指定本段为可读可写, 数据段的默认属性为 READWRITE ALIGN 属性 : 使用方式为 ALIGN 表达式 在默认时,

56 ELF( 可执行连接文件 ) 的代码段和数据段是按字 ( 即 4 字节 ) 对齐的, 表达式的取值范围为 0~31, 相应的对齐 方式为 2 表达式次方 COMMON 属性 : 该属性定义一个通用的段, 不包含任何 的用户代码和数据 各源文件中同名的 COMMON 段共 享同一段存储单元 例 :AREA Init,CODE,READONLY 指令序列 ; 该伪指令定义了一个代码段, 段名为 Init, 属性只读 ALIGN { 表达式 {, 偏移量 }} 功能 :ALIGN 伪指令可通过添加填充字节的方式, 使当前位置满足一定的对其方式 其中, 表达式的值用于指定对齐方式, 可能的取值为 2 的幂, 如 等 若未指定表达式, 则将当前位置对齐到下一个字的位置 CODE16( 或 CODE32) 功能 :CODE16 伪指令通知编译器, 其后的指令序列为 16 位 的 Thumb 指令 CODE32 伪指令通知编译器, 其后的指令序列为 32 位的 ARM 指令 ENTRY 功能 :ENTRY 伪指令用于指定汇编程序的入口点 在一个完 整的汇编程序中至少要有一个 ENTRY( 也可以有多个, 当有 多个 ENTRY 时, 程序的真正入口点由链接器指定 ), 但在一 个源文件里最多只能有一个 ENTRY( 可以没有 ) 例 : AREA Init,CODE,READONLY ENTRY ; 指定应用程序的入口点 END 功能 :END 伪指令用于通知编译器已经到了源程序的结尾 使用示例 : AREA Init,CODE,READONLY END ; 指定应用程序的结尾 名称 EQU 表达式 {, 类型 } 功能 :EQU 伪指令用于为程序中的常量 标号等定义一个等效的字符名称, 类似于 C 语言中的 #define 其中 EQU 可用 * 代替 名称为 EQU 伪指令定义的字符名称, 当表达式为 32 位的常量时, 可以指定表达式的数据类型为 CODE16 CODE32 或 DATA EXPORT 标号 {[WEAK]} 功能 :EXPORT 伪指令用于在程序中声明一个全局的标号, 该标号可在其他的文件中引用 EXPORT 可用 GLOBAL 代替 标号在程序中区分大小写,[WEAK] 选项声明其他的同名标号

57 优先于该标号被引用 IMPORT 标号 {[WEAK]} 功能 :IMPORT 伪指令用于通知编译器要使用的标号在其他源文件中定义, 但要在当前源文件中引用, 而且无论当前源文件是否引用该标号, 该标号均会被加入到当前源文件符号表中 EXTERN 标号 {[WEAK]} 功能 :EXTERN 伪指令用于通知编译器要使用的标号在其他源文件中定义, 但要在当前源文件中引用, 如果当前源文件实际并未引用该标号, 该标号不会被加入到当前源文件符号表中 汇编语言的程序结构 1. 汇编语句格式 ARM(Thumb) 汇编语言的语句格式为 : { 标号 } { 指令或伪指令 } {; 注释 } 在汇编语言程序设计中, 每一条指令的助记符可以全部用大写 或全部用小写, 但不用许在一条指令中大 小写混用 同时, 如果一条语句太长, 可将该长语句分为若干行来书写, 在行的末尾用 \ 表示下一行与本行为同一条语句 2. 汇编语言的程序结构在 ARM(Thumb) 汇编语言程序中, 以程序段为单位组织代码 段是相对独立的指令或数据序列, 具有特定的名称 段可以分为代码段和数据段, 代码段的内容为执行代码, 数据段存放代码运行时需要用到的数据 一个汇编程序至少应该有一个代码段, 当程序较长时, 可以分割为多个代码段和数据段, 多个段在程序编译链接时最终形成一个可执行的映象文件 可执行映象文件通常由以下几部分构成 : 一个或多个代码段, 代码段的属性为只读 零个或多个包含初始化数据的数据段, 数据段的属性为可读写 零个或多个不包含初始化数据的数据段, 数据段的属性为可读写 链接器根据系统默认或用户设定的规则, 将各个段安排在存储器中的相应位置 因此源程序中段之间的相对位置与可执行的映象文件中段的相对位置一般不会相同 3. 汇编语言与 C/C++ 的混合编程在应用系统的程序设计中, 若所有的编程任务均用汇编语言来完成, 其工作量是可想而知的, 同时, 不利于系统升级或应用软件移植, 事实上,ARM 体系结构支持 C/C+ 以及与汇编语言的混合编程, 在一个完整的程序设计的中, 除了初始化部分用汇编语言完成以外, 其主要的编程任务一般都用 C/C++ 完成 汇编语言与 C/C++ 的混合编程通常有以下几种方式 : 在 C/C++ 代码中嵌入汇编指令 在汇编程序和 C/C++ 的程序之间进行变量的互访 汇编程序 C/C++ 程序间的相互调用

58 在以上的几种混合编程技术中, 必须遵守一定的调用规则, 如物理寄存器的使用 参数的传递等, 这对于初学者来说, 无疑显得过于烦琐 在实际的编程应用中, 使用较多的方式是 : 程序的初始化部分用汇编语言完成, 然后用 C/C++ 完成主要的编程任务, 程序在执行时首先完成初始化过程, 然后跳转到 C/C++ 程序代码中, 汇编程序和 C/C++ 程序之间一般没有参数的传递, 也没有频繁的相互调用, 因此, 整个程序的结构显得相对简单, 容易理解 例 : ;******************************************************************* ; Institute of Computer Science&Technology, NJUPT ;File Name: begin.s ;Description: ;Author: Chao,Li ;Date: ;******************************************************************* AREA Init,CODE,READONLY ENTRY ;Asm Entry b Reset b HandlerUndef b HandlerSWI b HandlerPabort b HandlerDabort b. b HandlerIRQ b HandlerFIQ LTORG ; Reset VECTOR_r, start point Reset ldr r0,=wtcon ;Disable watchdog ldr r1,=0x0 str r1,[r0] ldr r0,=intmsk ldr r1,=0xfffff;//0x07ffffff,disable all the int enable eint4567 str r1,[r0] ;Setup clock register ldr r0,=locktime ldr r1,=0xfff str r1,[r0]

59 ldr r0,=pllcon ldr r1,=0x str r1,[r0] ldr r0,=clkcon ;Set system clock as 60MHZ ldr r1,=0x7ff8 ;Enable clock functions str r1,[r0] ;set BDMA to its default value ldr r0,=bdides0 ldr r1,=0x ;BDIDESn reset value should be 0x str r1,[r0] ldr r0,=bdides1 ldr r1,=0x ;BDIDESn reset value should be 0x str r1,[r0] ;Set memory controller ldr r0,=smrdata ldmia r0,{r1-r13} ldr r0,=0x01c80000 ;BWSCON Address stmia r0,{r1-r13} ;Set stack ldr bl sp, =SVCStack InitStacks ;Set interrupt VECTOR_r ldr r0,=vector_irq ;This routine is needed ldr r1,=isrirq ;if there isn't 'subs pc,lr,#4' at 0x18, 0x1c str r1,[r0] ;Copy and paste RW data/zero initialized data LDR r0, = Image$$RO$$Limit ; Get pointer to ROM data LDR r1, = Image$$RW$$Base ; and RAM copy LDR r3, = Image$$ZI$$Base ;Zero init base => top of initialised data CMP r0, r1 ; Check that they are different

60 0 1 2 BEQ %F1 CMP r1, r3 ; Copy init data LDRCC r2, [r0], #4 ;--> LDRCC r2, [r0] + ADD r0, r0, #4 STRCC r2, [r1], #4 ;--> STRCC r2, [r1] + ADD r1, r1, #4 BCC %B0 LDR MOV r2, #0 r1, = Image$$ZI$$Limit ; Top of zero init segment CMP r3, r1 ; Zero init STRCC r2, [r3], #4 BCC %B2 BL Main ;Go into code ;******************************************************************* ****** ; Institute of Computer Science&Technology, NJUPT ;File Name: main.c ;Description: ;Author: ;Date: Chao,Li ;******************************************************************* ***** // 主函数, 整个系统从这里开始执行 ; void Main(void) { FUNC fp; unsigned short *org; unsigned short *dst; unsigned int len; unsigned int i; //char buf[256]; SYSCFG = 0xe; //8K cache //Disable I/O cache NCACHBE0 = 0xc ; //port init; PCONA = 0x3ff;

61 PDATA = 0xfff; PDATB = 0x7ff; PCONB = 0xfff; PDATC = 0x0000; PCONC = 0x5f555555; PUPC = 0x3000; //PULL UP RESISTOR should be enabled to I/O PDATD = 0x55; PCOND = 0xaaaa; PUPD = 0x00; PDATE = 0x157; PCONE = 0x5568; PUPE = 0xff; PDATF = 0x0; PCONF = 0x22445a; PUPF PDATG PCONG = 0x1d3; = 0xff; = 0xFFFF; PUPG = 0x00; //should be enabled SPUCR = 0x7; //D15-D0 pull-up disable EXTINT = 0x0040; //init uart0 and send a byte out; ULCON0 = 0x3;// 八位数据, 一位停止位, 无奇偶校验 // 中断或是轮询方式接收和发送数据 UCON0 = 0x245;//enable rx and tx level interrupt; UBRDIV0 = 0x20;//the baut rate is set to be ; } org = (unsigned short *)0x1000; dst = (unsigned short *)0xC000000; fp = (FUNC)dst; len = (0x10000)>>1; if(*org!=0xffff) { } PDATC (*fp)(); for (i=0;i<len;i++) *dst++ = *org++; = 0x0002; // 点灯

62 小结 本章介绍了 ARM 程序设计的一些基本概念, 以及在汇编语言程序设计中常见的 伪指令 汇编语言的基本语句格式等, 汇编语言程序的基本结构等内容, 这些内容均为程序设计中的基本问题, 希望读者掌握, 注意本章最后的示例均与后面章节介绍的基于 S3C44B0X 的硬件平台有关系, 读者可以参考后面章节的相关内容 习题 1. 填空题 (1)ARM 汇编程序由 指令 指令和 指令构成 (2)ARM 伪指令可以分为以下几类 : 和 等 2. 简答题 (1) 伪指令在源程序中起什么作用? 它和机器指令有何区别? (2)ARM 异常中断类型有哪几种? 以 S3C44B0X 为例, 如何设计异常中断代码? (3) 举例说明 ARM 汇编和 x86 汇编异同

63 第三章 嵌入式 Linux 开发基础 要想在 Linux 系统下进行开发, 必须掌握 GNU 集成编译环境 GCC(GNU Compiler Collection) GCC 是开发嵌入式处理器平台的必选工具, 它功能强大, 支 持多种编程语言 支持多款处理器, 包括 ARM MIPS PowerPC 等嵌入式处理器, 此外, 它还提供了多种优化选项, 可以进行分布编译 3.1 GNU GCC 集成编译环境 GNU GCC 构成 GNU 提供的集成编译工具包括汇编器 as C 编译器 gcc C++ 编译器 g++ 链接器 ld 和其它一些实用工具 1.C/C++ 编译器 arm-elf-gcc arm -elf-gcc 主要功能是将源程序编译成汇编代码, 它有十分丰富的命令选项, 可以控制编译的各个阶段 2. 汇编器 arm-elf-as 汇编器 arm-elf-as 将 arm-elf-gcc 编译的汇编代码转换为目标代码, 通常情况下在 uclinux 中目标代码的格式为 ELF(Executable and Linking Format) as 象预处理器和连接器一样, 通常情况下可以被 gcc 调用, 但是也可以手工调用 3. 连接器 arm-elf-ld 在编写一个大的程序时, 经常把它分成许多独立的模块, 这时需要连接器所有的模块组合起来, 并结合 c 函数库和初始化代码, 产生最后可执行的文件 连接器 在产生可执行文件之前, 起到非常重要的作用 通常情况下,ld 被编译器所调用, 产生可执行代码, 但是如果想更好的控制连接过程, 最好手工调用 ld 4. 库管理器 arm-elf-ar 可以使用 ar 程序建立静态库, 把几个小文件合并成一个大文件 建立静态库时, 必须把几个.O 文件合并成一个单独的.a 文件 5. 其它实用工具如目标文件格式转换工具 arm-elf-objcopy, 它可以将 ELF 格式文件转换为其它格式的文件 目前 arm-elf-objcopy 支持的文件格式有 H-record S-record ABS BIN COFF 和 ELF 等 GNU GCC 处理过程 编译过程隐藏很多的细节, 了解这些细节可以帮助我们深入的了解程序在编译过程中产生的大量警告和错误信息, 从而可以更精确的编译程序和控制连接 GNU GCC 处理流程如下 : 1.C 预处理器 cpp

64 c 预处理器 cpp 是用来完成宏的求值 条件编译 以及其它一些需要把代码传递到编译器前的工作 (1) cpp 使用方式 可以在命令行中直接使用 cpp 也可以使用 gcc E 调用 cpp (2) 通常情况下,cpp 完成下列一些工作 : 解释宏 处理包含文件 处理 #if 和 #ifdef 声明, 还有其它以 # 开头的标志 例 : #define num (2+3) main( ) { } printf( %d,num*4); 经过 cpp 预处理后, 代码变成下面形式 : main( ) { printf( %d,(2+3)*4); } 2. 将预处理后的文件转换成汇编语言, 生成文件.s 3.GNU 汇编器 as 使用 gcc 编译程序时, 产生汇编代码,as 会处理这些汇编代码, 从而产生目标文件 4.GNU 连接器 ld arm-elf-ld 根据链接定位文件 Linkcmds 中的代码区 数据区 BSS 区和堆栈区等定位信息, 将可重定位的目标模块链接成一个单一的 绝对定位的目标程序 5. 文件处理器 ar 可以使用 ar 程序建立静态库, 把几个小文件合并成一个大文件 建立静态库时, 必须把几个.O 文件合并成一个单独的.a 文件 6. 库显示 ldd 一个可执行文件可能要使用一些共享函数库, 可以通过 ldd 工具显示它们 图 3-1 显示了 gcc 的编译过程

65 C/C++ 源文件 arm-elf-gcc 头文件 汇编文件 arm-elf-as 源文件列表 目标文件 生成库 arm-elf-ar 库列表 链接命令文件 用户库 可重定位模块 arm-elf-ld 符号表文件 可执行程序 图 3-1 嵌入式系统 gcc 编译流程 C/C++ 交叉编译器 arm-elf-gcc 1. 概述 arm-elf-gcc 是 uclinux 嵌入式系统平台下的 gcc, 是 GNU 推出的功能强大 性能优越的多平台编译器, 是 GNU 的代表作品之一 arm-elf-gcc 可以在多种硬体平台上编译出可执行程序的超级编译器, 其执行效率与一般的编译器相比平均效率要高 20%~30% 2. arm-elf-gcc 语法形式 arm-elf-gcc 语法 : arm-elf-gcc [ option filename ]... 其中 option 为 arm-elf-gcc 使用时的选项 ( 后面会再详述 ), 而 filename 为欲以 arm-elf-gcc 处理的文件 说明 : 通常情况下, 产生一个新的程序需要经过四个阶段 : 预处理 编译 汇编, 连结 而 C 的 compiler 已将产生新程序的相关程序整合起来 虽然原始程序的扩展名可 用来分辨编写原始程序码所用的语言, 但不同的 compiler, 其预设的处理程序却各不相同

66 原始程序码的扩展名指出所用编写程序所用的语言, 以及相对应的处理方法 : c C 原始程序 ; 预处理 编译 汇编 C C++ 原始程序 ; 预处理 编译 汇编 cc C++ 原始程序 ; 预处理 编译 汇编 cxx C++ 原始程序 ; 预处理 编译 汇编 m Objective-C 原始程序 ; 预处理 编译 汇编 i 已经过预处理之 C 原始程序 ; 编译 汇编 ii 已经过预处理之 C++ 原始程序 ; 编译 汇编 s 组合语言原始程序 ; 汇编 S 组合语言原始程序 ; 预处理 汇编 h 预处理文件 ( 标头文件 ) ; ( 不常出现在指令行 ) 其他扩展名的文件是由连结程序来处理, 通常有 : Object file a Archive file 3.arm-elf-gcc 常用命令选项使用详解 : -x language filename 功能 : 指定使用的语言 根据约定 C 语言的后缀名称是.c 的, 而 C++ 的后缀名是.C 或者.cpp, 但是你可以随意决定你的 C 代码文件的后缀名, 假设是.pig, 你就可以采用 如下的例子来完成这个功能 : arm-elf-gcc -x c hello.pig 如果要关掉上一个选项, 让 arm-elf-gcc 根据文件名后缀, 自动识别文件类型, 可以采用 : -x none filename 例 : arm-elf-gcc -x c hello.pig -x none hello2.c -c 功能 : 只对文件进行编译和汇编, 但是并不进行连接, 也就是说只把程序做成 obj 文件 例 : arm-elf-gcc -c hello.c 将生成.o 的 obj 文件 -S 功能 : 只对文件进行编译, 但是并不进行汇编和连接例 : arm-elf-gcc -S hello.c 将生成.s 的汇编代码, 可以用文本编辑器察看

67 -E 功能 : 只对文件进行预处理, 但不需要进行编译 汇编和连接 例 : arm-elf-gcc -E hello.c > pianoapan.txt arm-elf-gcc -E hello.c more -o 功能 : 制定目标名称, 缺省的时候,gcc 编译出来的文件是 a.out 例 : arm-elf-gcc -o hello.exe hello.c arm-elf-gcc -o hello.asm -S hello.c -pipe 功能 : 使用管道代替编译中临时文件 例 : arm-elf-gcc -pipe -o hello.exe hello.c -ansi 功能 : 关闭 gnu c 中与 ansi c 不兼容的特性, 激活 ansi c 的专有特性 ( 包括禁止一些 asm inline typeof 关键字, 以及 UNIX,vax 等预处理宏 ) -fno-asm 功能 : 此选项实现 ansi 选项的功能的一部分, 它禁止将 asm,inline 和 typeof 用作关键字 -fno-strict-prototype 功能 : 只对 g++ 起作用, 使用这个选项,g++ 将对不带参数的函数都认为是没有显式的对参数的个数和类型说明, 而不是没有参数 而 gcc 无论是否使用这个参数, 都将对没有带参数的函数认为是没有显式说明的类型 -fthis-is-varialble 功能 : 向传统 c++ 看齐, 可以使用 this 当一般变量使用 -fcond-mismatch 功能 : 允许条件表达式的第二和第三参数类型不匹配, 表达式的值将为 void 类型 -funsigned-char 或 -fno-signed-char 或 -fsigned-char 或 -fno-unsigned-char

68 功能 : 这四个参数是对 char 类型进行设置, 决定将 char 类型设置成 unsigned char( 前 两个参数 ) 或者 signed char( 后两个参数 ) -include file 功能 : 包含某个代码, 简单来说, 就是编译某个文件时需要另一个文件的时候, 就可以用它设定, 功能就相当于在代码中使用 #include 例 : arm-elf-gcc hello.c -include /root/pianopan.h -imacros file 功能 : 将 file 文件的宏扩展到 gcc/g++ 的输入文件, 宏定义本身并不出现在输入文件中 -Dmacro 功能 : 相当于 C 语言中的 #define macro -Dmacro=defn 功能 : 相当于 C 语言中的 #define macro=defn -Umacro 功能 : 相当于 C 语言中的 #undef macro -undef 功能 : 取消对任何非标准宏的定义 -Idir 功能 : 如果使用 #include"file" 的时候,gcc/g++ 会先在当前目录查找你所制定的头文件, 如果没有找到, 编译器会到缺省的头文件目录找 ; 如果使用 -I 指定了目录, 编译器会先到你所指定的目录查找, 然后再按常规的顺序去找 -I- 功能 : 就是取消前一个参数的功能, 所以一般在 -Idir 之后使用 -idirafter dir 功能 : 在 -I 的目录里面查找失败 -iprefix prefix 和 -iwithprefix dir

69 功能 : 这两个参数一般一起使用, 当 -I 的目录查找失败, 会到 prefix+dir 下查找 -nostdinc 功能 : 使编译器不再系统缺省的头文件目录里面找头文件 一般和 -I 联合使用, 明确 限定头文件的位置 -nostdin C++ 功能 : 规定不在 g++ 指定的标准路经中搜索, 但仍在其他路径中搜索,. 此选项在创建 libg++ 库使用 -C 功能 : 在预处理的时候, 不删除注释信息, 一般和 -E 使用, 有时候分析程序用这个选项很方便 -M 功能 : 生成文件关联的信息, 包含目标文件所依赖的所有源代码 例 : gcc M hello.c -MM 功能 : 和 -M 选项的功能一样, 但是它将忽略由 #include 造成的依赖关系 -MD 功能 : 和 -M 相同, 但是输出将导入到.d 的文件里面 -MMD 功能 : 和 -MM 相同, 但是输出将导入到.d 的文件里面 -Wa,option 功能 : 此选项传递 option 给汇编程序 ; 如果 option 中间有逗号, 就将 option 分成多个选项, 然后传递给会汇编程序 -Wl.option 功能 : 此选项传递 option 给连接程序 ; 如果 option 中间有逗号, 就将 option 分成多个选 项, 然后传递给会连接程序. -llibrary 功能 : 指定编译的时候使用的库

70 例 : arm-elf-gcc -lcurses hello.c 指定使用 ncurses 库编译程序 -Ldir 功能 : 指定编译的时候, 搜索库的路径 比如自己的库, 就可以用它指定到你的库所在的目录, 不然编译器将只在标准库的目录找 这个 dir 就是目录的名称 -O0 -O1 -O2 -O3 功能 : 编译器的优化选项的 4 个级别, -O0: 表示没有优化 ; -O1: 为缺省值 : -O3 优化级别最高 -g 功能 : 编译器在编译的时候产生调试信息 -gstabs 功能 : 此选项以 stabs 格式生成调试信息, 但是不包括 gdb 调试信息 -gstabs+ 功能 : 此选项以 stabs 格式生成调试信息, 并且包含仅供 gdb 使用的额外调试信息 -ggdb 功能 : 此选项将尽可能的生成 gdb 的可以使用的调试信息 交叉汇编器 arm-elf-as 1. 概述 arm-elf-as 将汇编语言程序转换为 ELF(executable and linking format) 目标代码, 这些代码同其它目标模块或库易于定位和链接 arm-elf-as 产生一个交叉参考表和一个标准的符号表, 产生的代码和数据能够放 在多个段中 2. 命令格式 arm-elf-as 命令格式如下 : arm-elf-as [OPTION ] [ 汇编文件 ]

71 例 : 将 file.s 编译成目标文件, 并且设置头文件搜索目录为 c:\include arm-elf-as I//c/include file.s 3. 命令选项 -a[dhlns] 功能 : 显示 arm-elf-as 信息 其中 dhlns 为其子选项, 分别表示如下 d: 不显示调试信息 h: 显示源码信息 l: 显示汇编列表 n: 不进行格式处理 s: 显示符号列表 -f 功能 : 不进行预处理 arm-elf-as 内部的预处理程序, 完成以下工作 : 调整并删除多余空格, 删除注释, 将字符常量改成对应的数值 -I path 功能 : 用于添加路径 path 到 arm-elf-as 的搜索路径例 : 编译 file.s 时指定两个搜索目录, 当前目录和 c:/include arm-elf-as I../ -I//c/include file.s -o 功能 : 用于控制每次运行 arm-elf-as 只输出一个目标文件, 默认文件名为 a.out 可以通过 -o 选项指定输出文件名字 例 : 编译 file.s 输出目标文件 file.o arm-elf-as I/include o file.o file.s -v 功能 : 显示版本信息 -w 功能 : 不再显示警告信息 例 : 编译 file.s 输出目标文件 file.o, 不输出警告信息 arm-elf-as W o file.o file.s -Z 功能 : 不再显示错误信息 交叉链接器 arm-elf-ld 1. 概述 arm-elf-ld 根据链接定位文件 linkcmds 中代码段 数据段 BSS 段和堆栈段等定 位信息, 将可重定位的目标模块链接成一个单一的 绝对定位的目标程序, 该目标程序是 ELF 格式, 并且可以包含调试信息 arm-elf-ld 可以输出一个内存映像文件, 该文件显示所有目标模块 段和符号的绝对定位地址, 它也产生目标模块对全局符号引用的交叉参考列表

72 2. 命令格式 arm-elf-ld 命令格式如下 : arm-elf-ld [option] file 例 : 若链接输入文件为 file.o, 输出文件为 file.elf, 链接库为 libxxx.a, 生成内存映像文件 map.txt, 链接定位文件为 linkcmds, 则命令如下 arm-elf-ld Map map.txt N T linkcmds L./lib o file.elf file.o lxxx 3. 命令选项列表 -e entry 功能 : 指定程序入口地址例 : 链接的输入文件为 file.o, 输出文件为 file.elf, 链接定位文件为 linkcmds, 程序入口地址为 _start, 则链接命令如下 arm-elf-ld T linkcmds e _start o file.elf file.o -Map mapfile 功能 : 将链接的符号映像表和内存分布信息输出到文件 mapfile 中 -M 功能 : 在标准端口打印出符号映像表和内存分布信息 -lar 功能 : 用于指定库文件 libar.a 为链接的库 可以重复使用 -l 来指定多个链接的库 库的命名规则为 libxxx.a, 在 -l 指定库名时使用的格式为 -lxxx 例 : 若链接输入文件为 file.o, 输出文件为 file.elf, 链接库为 libxxx.a, 则命令 如下 arm-elf-ld o file.elf file.o lxxx -Ldir 功能 : 用于将 dir 添加到搜索路径 搜索顺序按照命令行中输入的顺序, 并且优先与默认的搜索路径 例 : 若链接输入文件为 file.o, 输出文件为 file.elf, 将 /lib 添加到库的搜索路径, 命令如下 arm-elf-ld L./lib o file.elf file.o -o outputfilename 功能 : 用于将输出文件名字设定为 outputfilename 如果不指定输出文件名, arm-elf-ld 生成的文件名默认为 a.out 3.2 Makefile 原理与使用 Makefile 原理 在大型的开发项目中, 通常有几十到上百个的源文件, 如果每次均手工键入 gcc 命令进行编译的话, 则会非常不方便 因此, 人们通常利用 make 工具来自动完成编译工作 这些工作包括 : 如果仅修改了某几个源文件, 则只重新编译这几个源文件 ;

73 如果某个头文件被修改了, 则重新编译所有包含该头文件的源文件 利用这种自动编译可大大简化开发工作, 避免不必要的重新编译 实际上,make 工具通过一个称为 makefile 的文件来完成并自动维护编译工作 makefile 需要按照某种语法进行编写, 其中说明了如何编译各个源文件并连接生成可执行文件, 并定义了源文件之间的依赖关系 当修改了其中某个源文件时, 如果其他源文件依赖于该文件, 则也要重新编译所有依赖该文件的源文件 makefile 文件是许多编译器, 包括 Windows NT 下的编译器维护编译信息的常用方法, 只是在集成开发环境中, 用户通过友好的界面修改 makefile 文件而已 因此 Makefile 的宗旨就是 : 让编译器知道要编译一个文件需要依赖其他的哪些文件 当那些依赖文件有了改变, 编译器会自动的发现最终的生成文件已经过时, 而重新编译相应的模块 默认情况下,GNU make 工具在当前工作目录中按如下顺序搜索 makefile: GNUmakefile makefile Makefile 在 UNIX 系统中, 习惯使用 Makefile 作为 makfile 文件 如果要使用其他文件作为 makefile, 则可利用类似下面的 make 命令选项指定 makefile 文件 : $ make -f Makefile.debug Makefile 语法 1. 基本结构 targets : prerequisites command... 或是这样 : targets : prerequisites ; command command... 说明 : targets 是目标名, 以空格分开, 可以使用通配符 一般来说, 我们的目标基本上是一个文件, 但也有可能是多个文件 command 是命令行, 如果其不与 target:prerequisites 在一行, 那么, 必须以 [Tab 键 ] 开头, 如果和 prerequisites 在一行, 那么可以用分号做为分隔 表示创建每个项目时需要运行的命令 prerequisites 也就是目标所依赖的文件 ( 或依赖目标 ) 如果其中的某个文件要比目标文件要新, 那么, 目标就被认为是 过时的, 被认为是需要重生成的

74 如果命令太长, 可以使用反斜框 ( \ ) 作为换行符 make 对一行上有多少个字符没有限制 规则告诉 make 两件事, 文件的依赖关系和如何成成目标文件 例如, 假设有一个 C 源文件 example.c, 该源文件包含有自定义的头文件 example.h, 则目标文件 example.o 明确依赖于两个源文件 :example.c 和 example.h 另外, 可能只希望利用 gcc 命令来生成 example.o 目标文件 这时, 就可以利用如下的 makefile 来定义 example.o 的创建规则 : # This makefile just is a example. # The following lines indicate how example.o depends # example.c and example.h, and how to create example.o example.o: example.c example.h gcc -c g o example.o example.c 说明 : 从上面的例子注意到, 第一个字符为 # 的行为注释行 第一个非注释行指定 example.o 为目标, 并且依赖于 example.c 和 example.h 文件 随后的行指定了如何从目标所依赖的文件建立目标 当 example.c 或 example.h 文件在编译之后又被修改, 则 make 工具可自动重新编译 example.o, 如果在前后两次编译之间,example.C 和 example.h 均没有被修改, 而且 example.o 还存在的话, 就没有必要重新编译 这种依赖关系在多源文件的程序编译中尤其重要 通过这种依赖关系的定义,make 工具可避免许多不必要的编译工作 当然, 利用 Shell 脚本也可以达到自动编译的效果, 但是,Shell 脚本将全部编译任何源文件, 包括哪些不必要重新编译的源文件, 而 make 工具则可根据目标上一次编译的时间和目标所依赖的源文件的更新时间而自动判断应当编译哪个源文件 一个 makefile 文件中可定义多个目标, 利用 make target 命令可指定要编译的目标, 如果不指定目标, 则使用第一个目标 通常,makefile 中定义有 clean 目标, 可用来清除编译过程中的中间文件, 例如 : clean: rm -f *.o 运行 make clean 时, 将执行 rm -f *.o 命令, 最终删除所有编译过程中产生的所有中间文件 2. 配符的使用如果想定义一系列比较类似的文件, 就很自然地想起使用通配符 make 支持三个通配符 : *,? 和 [...] 这是和 Unix 的 B-Shell 是相同的 波浪号 ( ~ ) 字符在文件名中也有比较特殊的用途 如果是 ~/test, 这就表示当前用户的 $HOME 目录下的 test 目录 而 ~lic/test 则表示用户 lic 的宿主目录下的 test 目录 通配符代替了一系列的文件, 如 *.c 表示所以后缀为 c 的文件 ( 注意 : 如

75 果我们的文件名中有通配符, 如 : *, 那么可以用转义字符 \, 如 \* 来表示真实的 * 字符, 而不是任意长度的字符串 ) 例 : clean: rm -f *.o 这个例子说明了通配符表明通配符可以用在命令中的 print: *.c lpr -p $? touch print 这个例子说明了通配符也可以在我们的规则中, 目标 print 依赖于所有的 [.c] 文件 其中的 $? 是一个自动化变量 objects = *.o 这个例子说明了通配符同样可以用在变量中 并不是说 [*.o] 会展开, 不! objects 的值就是 *.o Makefile 中的变量其实就是 C/C++ 中的宏 如果你要让通配符在变量中展开, 也就是让 objects 的值是所有 [.o] 的文件名的集合, 那么, 你可以这样 : objects := $(wildcard *.o) 3. 伪目标最早先的一个例子中, 我们提到过一个 clean 的目标, 这是一个 伪目标, # This makefile just is a example. # The following lines indicate how example.o depends # example.c and example.h, and how to create example.o example.o: example.c example.h gcc -c -g example.c clean: rm *.o 由于在编译的过程中, 产生了许多中间文件, 所以应该提供一个清除它们的 目标, 以备完整地重编译而用 而 clean 目标就是完成这个任务的, 我们把这种形式的目标称为 伪目标 因为并不生成 clean 这个文件, 所以 伪目标 并不是一个文件, 只是一个标签, 由于 伪目标 不是文件, 所以 make 无法生成它的依赖关系和决定它是否要执行 我们只有通过显示地指明这个 目标 才能让其生效 当然, 伪目标 的取名不能和文件名重名, 不然其就失去了 伪目标 的意义了 为了避免和文件重名的这种情况, 在 Makefile 中可以使用一个特殊的标记.PHONY 来显示地指明一个目标是 伪目标, 向 make 说明, 不管是否有这个文件, 这个目标就是 伪目标.PHONY : clean

76 只要有这个声明, 不管是否有 clean 文件, 要运行 clean 这个目标, 只有采用下列命令的形式 : make clean 这样 于是整个过程可以这样写 : # This makefile just is a example. # The following lines indicate how example.o depends # example.c and example.h, and how to create example.o example.o: example.c example.h gcc -c -g example.c.phony: clean clean: rm *.o 伪目标一般没有依赖的文件, 但是, 也可以为伪目标指定所依赖的文件 伪目标同样可以作为 默认目标, 只要将其放在第一个 比如, 如果要用 Makefile 生成若干个可执行文件, 就可以按照下面的方法进行编写 all : example1 example2 example3.phony : all example1 : example1.o utils.o cc -o example1 example1.o utils.o example2 : example2.o cc -o example2 example2.o example3 : example3.o sort.o utils.o cc -o example3 example3.o sort.o utils.o 说明 :Makefile 中的第一个目标会被作为其默认目标 上面例子中声明了一个 all 的伪目标, 其依赖于其它三个目标 由于伪目标的特性是, 总是被执行的, 所以其依赖的那三个目标就总是不如 all 这个目标新 所以, 其它三个目标的规则总是会被决议 也就达到了生成多个目标的目的.PHONY : all 声明了 all 这个目标为 伪目标 4. makefile 变量 GNU 的 make 工具除提供有建立目标的基本功能之外, 还有许多便于表达依赖性关系以及建立目标的命令的特色 其中之一就是变量或宏的定义能力 如果你要以相同的编译选项同时编译十几个 C 源文件, 而为每个目标的编译指定冗长的编译选项的话, 将是非常乏味的 但利用简单的变量定义, 可避免这种乏味的工作 : # Define macros for name of compiler CC = gcc # Define a macr o for the CC flags

77 CCFLAGS = -D_DEBUG -g -m486 # A rule for building a object file example.o: example.c example.h $(CC) -c $(CCFLAGS) example.c 在上面的例子中,CC 和 CCFLAGS 就是 make 的变量 GNU make 通常称之为变量, 而其他 UNIX 的 make 工具称之为宏, 实际是同一个东西 在 makefile 中引用变量的值时, 只需变量名之前添加 $ 符号, 如上面的 $(CC) 和 $(CCFLAGS) 5. GNU make 的主要预定义变量 GNU make 有许多预定义的变量, 这些变量具有特殊的含义, 可在规则中使用 表 3-1 给出了一些主要的预定义变量, 除这些变量外,GNU make 还将所有的环境 变量作为自己的预定义变量 表 3-1 GNU make 的主要预定义变量 预定义变量 含义 $* 不包含扩展名的目标文件名称 $+ 所有的依赖文件, 以空格分开, 并以出现 的先后为序, 可能包含重复的依赖文件 $< 第一个依赖文件的名称 $? 所有的依赖文件, 以空格分开, 这些依赖 文件的修改日期比目标的创建日期晚 $@ 目标的完整名称 $^ 所有的依赖文件, 以空格分开, 不包含重复的依赖文件 $% 如果目标是归档成员, 则该变量表示目标 的归档成员名称 例如, 如果目标名称为 mytarget.so(image.o), 则 $@ 为 mytarget.so, 而 $% 为 image.o AR 归档维护程序的名称, 默认值为 ar ARFLAGS 归档维护程序的选项 AS 汇编程序的名称, 默认值为 as ASFLAGS 汇编程序的选项 CC C 编译器的名称, 默认值为 cc CCFLAGS C 编译器的选项 CPP C 预编译器的名称, 默认值为 $(CC) -E CPPFLAGS C 预编译的选项 CXX C++ 编译器的名称, 默认值为 g++ CXXFLAGS C++ 编译器的选项 FC FORTRAN 编译器的名称, 默认值为 f77 FFLAGS FORTRAN 编译器的选项

78 6. GNU make 隐含规则 GNU make 包含有一些内置的或隐含的规则, 这些规则定义了如何从不同的依 赖文件建立特定类型的目标 GNU make 支持两种类型的隐含规则 : 后缀规则 (Suffix Rule) 后缀规则是定义隐含规则的老风格方法 后缀规 则定义了将一个具有某个后缀的文件 ( 例如,.c 文件 ) 转换为具有另外一种后缀的文件 ( 例如,.o 文件 ) 的方法 每个后缀规则以两个成对出现的后缀名定义, 例如, 将.c 文件转换为.o 文件的后缀规则可定义为 :.c.o: $(CC) $(CCFLAGS) $(CPPFLAGS) -c -o $@ $< 模式规则 (pattern rules) 这种规则更加通用, 因为可以利用模式规则定义更加复杂的依赖性规则 模式规则看起来非常类似于正则规则, 但在目标名称的前面多了一个 % 号, 同时可用来定义目标和依赖文件之间的关系, 例如下面的模式规则定义了如何将任意一个 X.c 文件转换为 X.o 文件 %.c:%.o $(CC) $(CCFLAGS) $(CPPFLAGS) -c -o $@ $< 7. 静态模式如果一条规则中定义了多个目标的话, 可以采用静态模式, 这种模式可以让我们的规则变得更加的有弹性和灵活 静态规则的语法如下 : <targets...>: <target-pattern>: <prereq-patterns...> <commands>... targets 定义了一系列的目标文件, 可以有通配符 是目标的一个集合 target-parrtern 指明了 targets 的模式, 也就是的目标集模式 prereq-parrterns 目标的依赖模式, 它对 target-parrtern 形成的模式再进行一次依赖目标的定义 这样描述这三个东西, 可能还是没有说清楚, 还是举个例子来说明一下吧 如果 <target-parrtern> 定义成 %.o, 意思是 <target> 集合中都是以.o 结尾的, 而如果 <prereq-parrterns> 定义成 %.c, 意思是对 <target-parrtern> 所形成的目标集进行二次定义, 其计算方法是, 取 <target-parrtern> 模式中的 % ( 也就是去掉了 [.o] 这个结尾 ), 并为其加上 [.c] 这个结尾, 形成的新集合 所以, 目标模式 或是 依赖模式 中都应该有 % 这个字符, 如果你的文件名中有 % 那么你可以使用反斜杠 \ 进行转义, 来标明真实的 % 字符 例子 : objects = example1.o example2.o all: $(objects)

79 $(objects): %.o: %.c $(CC) -c $(CFLAGS) $< -o 上面的例子中, 指明了目标从 $object 中获取, %.o 表明要所有以.o 结尾的目标, 也就是 example1.o example2.o, 也就是变量 $object 集合的模式, 而依赖模式 %.c 则取模式 %.o 的 %, 也就是 example1 example2, 并为其加下.c 的后缀, 于是, 依赖目标就是 example1.c example2.c 而命令中的 $< 和 则是自动化变量, $< 表示所有的依赖目标集 ( 也就是 example1.c example2.c ), $@ 表示目标集 ( 也就是 example1.o example2.o ) 于是, 上面的规则展开后等价于下面的规则 : example1.o : example1.c $(CC) -c $(CFLAGS) example1.c -o example1.o example2.o : example2.c $(CC) -c $(CFLAGS) bar.c -o bar.o 如果 %.o 有几百个, 那种只要用这种很简单的 静态模式规则 就可以写完一堆规则, 效率非常高 8.makefile 范例现在大型项目的开发, 都用 autoconf 和 automake 程序自动生成 Makefile 文件 但能读懂 Makefile 却是开发者进行项目开发时必备的基本功 现在以一个自动生成 Makefile 为例, 讲解 Makefile 的使用 下面给出两个 Makefile 实例 : 一个是上层目录的 Makefile; 另一个是子目录 sub 的 Makefile (1) 上层目录的 Makefile 文件 EXE=test # 子目录的目标文件列表 OTHEROBJS=sub/receive.o sub/send.o DIRS=sub LDFLAGS= LIBS= # 子目录列表 # 编译器链接选项 # 编译所需的库选项 # 输出 C 编译器和基本选项给子目录 Export CC=gcc Export CFLAGS=-Wall O # 输出 C++ 编译器和基本选项给子目录 Export CXX=g++

80 Export CXXFLAGS=$(CFLAGS) # 得到当前 C/C++ 语言源文件和目标文件列表 SRCS:=$(wildcard *.c) $(wildcard *.cc) OBJS:=$(patsubst %.c,%.o,$srcs) $(patsubst %.cc,%.o,$(cxxsrcs)) # 定义 make 目的标号, 有子目录 可执行文件 扩展 3 个部分 all:subdirs $(exe) $(others) # 递归调用子目录下的 MAKEFILE 文件 dir in $(DIRS); do $(MAKE) C $$dir;done # 链接生成可执行文件 $(EXE):subdirs $(OBJS) $(CC) $(LDFLAGS) $(LIBS) o $@ $(OBJS) $(OTHEROBJS) # 递归清除 clean: -rm $(OBJS) $(EXE) $(OTHERS) dir in $(DIRS);do $(MAKE) C $$dir clean ;done (2) 子目录 sub 的 Makefile 文件代码如下 : OTHERS= INCLUDES= SRCS:=$(wildcard *.c) OBJS:=$(patsubst %.c,%.o,$(srcs)) # 子目录扩展选项 CFLAGS+=G all:$(objs) $(OTHERS) # 生成当前目录下的目标文件 %.o:%.c $(CC) $(CFLAGS) $(INCLUDES) c $< > $@

81 9. Make 的运行 我们知道, 直接在 make 命令的后面键入目标名可建立指定的目标, 如果直接运行 make, 则建立第一个目标 同时还可以用 make -f mymakefile 这样的命令指定 make 使用特定的 makefile, 而不是默认的 GNUmakefile makefile 或 Makefile GNU 的 make 工作时的执行步骤入下 : 读入所有的 Makefile 读入被 include 的其它 Makefile 初始化文件中的变量 推导隐晦规则, 并分析所有规则 为所有的目标文件创建依赖关系链 根据依赖关系, 决定哪些目标要重新生成 执行生成命令 1-5 步为第一个阶段,6-7 为第二个阶段 第一个阶段中, 如果定义的变量被使用了, 那么,make 会把其展开在使用的位置 但 make 并不会完全马上展开,make 使用的是拖延战术, 如果变量出现在依赖关系的规则中, 那么仅当这条依赖被决定要使用了, 变量才会在其内部展开 GNU make 命令还有一些其他选项, 表 3-2 给出了这些选项 表 3-2 GNU make 命令的常用命令行选项 命令行选项 含义 -C DIR 在读取 makefile 之前改变到指定的目录 DIR -f FILE 以指定的 FILE 文件作为 makefile -h 显示所有的 make 选项 -i 忽略所有的命令执行错误 -I DIR 当包含其他 makefile 文件时, 利用该选项指定搜索目录 -n 只打印要执行的命令, 但不执行这些命令 -p 显示 make 变量数据库和隐含规则 -s 在执行命令时不显示命令 -w 在处理 makefile 之前和之后, 显示工作目录 -W FILE 假定文件 FILE 已经被修改 3.3 应用程序调试 ARM 调试方法简介 用户选用 ARM 处理器开发嵌入式系统时, 选择合适的开发工具可以加快开发进度, 节省开发成本 因此一套含有编辑软件 编译软件 汇编软件 链接软件 调试软件 工程管理及函数库的集成开发环境 (IDE) 一般来说是必不可少的, 至于嵌

82 入式实时操作系统 评估板等其他开发工具则可以根据应用软件规模和开发计划选用 使用集成开发环境开发基于 ARM 的应用软件, 包括编辑 编译 汇编 链接等工作全部在 PC 机上即可完成, 调试工作则需要配合其他的模块或产品方可完成, 目前常见的调试方法有以下几种 : 1 指令集模拟器部分集成开发环境提供了指令集模拟器, 可方便用户在 PC 机上完成一部分简单的调试工作, 但是由于指令集模拟器与真实的硬件环境相差很大, 因此即使用户使用指令集模拟器调试通过的程序也有可能无法在真实的硬件环境下运行, 用户最终必须在硬件平台上完成整个应用的开发 2 驻留监控软件驻留监控软件 (Resident Monitors) 是一段运行在目标板上的程序, 集成开发环境中的调试软件通过以太网口 并行端口 串行端口等通讯端口与驻留监控软件进行交互, 由调试软件发布命令通知驻留监控软件控制程序的执行 读写存储器 读写寄存器 设置断点等 驻留监控软件是一种比较低廉有效的调试方式, 不需要任何其他的硬件调试和仿真设备 ARM 公司的 Angel 就是该类软件, 大部分嵌入式实时操作系统也是采用该类软件进行调试, 不同的是在嵌入式实时操作系统中, 驻留监控软件是作为操作系统的一个任务存在的 驻留监控软件的不便之处在于它对硬件设备的要求比较高, 一般在硬件稳定之后才能进行应用软件的开发, 同时它占用目标板上的一部分资源, 而且不能对程序的全速运行进行完全仿真, 所以对一些要求严格的情况不是很适合 3 JTAG 仿真器 JTAG 仿真器也称为 JTAG 调试器, 是通过 ARM 芯片的 JTAG 边界扫描口进行调试的设备 JTAG 仿真器比较便宜, 连接比较方便, 通过现有的 JTAG 边界扫描口与 ARM CPU 核通信, 属于完全非插入式 ( 即不使用片上资源 ) 调试, 它无需目标存储器, 不占用目标系统的任何端口, 而这些是驻留监控软件所必需的 另外, 由于 JTAG 调试的目标程序是在目标板上执行, 仿真更接近于目标硬件, 因此, 许多接口问题, 如高频操作限制 AC 和 DC 参数不匹配, 电线长度的限制等被最小化了 使用集成开发环境配合 JTAG 仿真器进行开发是目前采用最多的一种调试方式 4 在线仿真器在线仿真器使用仿真头完全取代目标板上的 CPU, 可以完全仿真 ARM 芯片的行为, 提供更加深入的调试功能 但这类仿真器为了能够全速仿真时钟速度高于 100MHz 的处理器, 通常必须采用极其复杂的设计和工艺, 因而其价格比较昂贵 在线仿真器通常用在 ARM 的硬件开发中, 在软件的开发中较少使用, 其价格高昂也是在线仿真器难以普及的因素 gdb 本地调试 1.gdb 简介

83 GNU 的调试器称为 gdb, 它是一个用来调试 c 和 c++ 程序的功能强大的调试器, 它能在程序运行时观察程序的内部结构和内存的使用情况 如果没有 gdb 调试 工具, 程序员为了跟踪某个错误, 可能要编写许多附加的语句, 产生一些特定的输出 但这样做可能会带来许多问题, 比如, 可能会导致更多的错误产生 gdb 是一个交互式工具, 工作在字符模式下 在 X Window 系统中, 有一个 gdb 的前端图形工具, 称为 xxgdb gdb 功能非常强大, 其主要功能如下 : 设置断点, 保证你的程序在指定的条件下停止 ; 监视程序变量的值 ; 程序的单步执行 ; 修改变量的值 运行 gdb 调试程序时通常使用如下的命令 : gdb 文件名 gdb 支持很多的命令使你能实现不同的功能, 这些命令从简单的文件装入到允许你检查所调用的堆栈内容的复杂命令 表 3-3 列出了在用 gdb 调试时会用到的一些命令 如果想要了解 gdb 的详细使用请参考 gdb 的指南页或在 gdb 提示符处键入 : help 命令表 3-3 基本 gdb 命令 命令 描述 file 装入想要调试的可执行文件. kill 终止正在调试的程序. list 列出产生执行文件的源代码的一部分. next 执行一行源代码但不进入函数内部. step 执行一行源代码而且进入函数内部. run quit 执行当前被调试的程序 终止 gdb watch 使你能监视一个变量的值而不管它何时被改变. break 在代码里设置断点, 这将使程序执行到这里时被挂起. make 使你能不退出 gdb 就可以重新产生可执行文件. shell 使你能不离开 gdb 就执行 UNIX shell 命令. gdb 支持很多与 UNIX shell 程序一样的命令编辑特征 你能象在 bash 或 tcsh 里那样按 Tab 键让 gdb 帮你补齐一个唯一的命令, 如果不唯一的话 gdb 会列出所有匹配的命令, 你也能用光标键上下翻动历史命令 2.Gdb 命令详解

84 上面一节中简要介绍了 gdb 的主要功能和常用命令, 下面以一个具体实例对每种命令做详细的阐述 1 #include <stdio.h> 2 void main() 3 { 4 int k,j; 5 j=0; 6 for(k =0; k <10; k ++) 7 { 8 j+=5; 9 printf( j=%d,j); 10 } (1) 运行程序 在命令行上键入 gdb 并按回车键就可以运行 gdb 了, 如果一切正常的话,gdb 将被启动并且将在屏幕上看到类似的内容 : GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details.gdb 4.14 (i486-slakware-linux), Copyright 1995 Free Software Foundation, Inc. (gdb) 当启动 gdb 后, 可以在命令行上指定很多的选项 也可以以下面的方式来运行 gdb: gdb <fname> 当用这种方式运行 gdb, 程序员能直接指定想要调试的程序 这将告诉 gdb 装入名为 fname 的可执行文件 (2) 设置 / 显示命令行参数设置命令行形式 : set args <arg list> 显示命令行形式 : show args (3) 程序运行相关命令 gdb 提供了 8 条控制程序运行相关的命令 :cont handle jump kill next nexti step 和 stepi 它们的功能如下: cont 功能 : 该命令使程序在信号发生后或是停在断点之后再继续运行 如果是在断点之后继续程序的运行, 则该命令后可以带参数 N, 该参数表示在以后的执行过程中忽略断点的次数为 N-1 次, 也就是说在第 N 次执行到该断点时才暂停程序的执行

85 handle 功能 : 该命令用来对信号设置处理函数的 其使用格式为 : (gdb) handle 信号信号处理函数信号可以用符号表示, 也可以用数字 (1~15) 表示 Gdb 定义的信号处理函数有 : stop: 如果信号发生重新进入调试器, 并打印出提示信息 ; print: 如果信号发生就打印出一条提示信息 ; pass: 让程序能够看到这个信号发生 jump 功能 :jump 命令用来指定程序开始调试的地址, 其使用格式为 : (gdb) jump 行号或指令地址 kill 功能 :kill 命令用来结束当前程序的调试 在 gdb 下直接输入这条命令即可 next 功能 : 使程序向前运行一条语句, 它会越过子程序调用 Next 后面可以带一个参数 N, 这个参数指定执行这种操作的次数 step 功能 : 使程序向前运行一条语句, 它不会越过子程序调用 nexti 功能 : 使程序向前运行一条指令, 它不会越过子程序调用 stepi: 功能 : 使程序向前运行一条指令, 它不会越过子程序调用, 它的功能相当于 nexti 和 step 的结合 (4) 断点相关命令断点的作用是当程序运行到断点时, 无论它在做什么都会被停止下来 对于每个断点 程序员都可以设置一些更高级的信息以决定断点在什么时候起作用 可以使用 'break' 命令来在程序中设置断点, 在前面的例子中我们已经提到过一些这个命令的使用方法 可以在行上, 函数上, 甚至在确切的地址上设置断点 在含有异常处理的语言 ( 比如像 c++) 中, 还可以在异常发生的地方设置断点 1 设置断点的方法 : break FUNCTION 此命令用来在某个函数上设置断点 当使用允许函数重载的语言比如 C++ 时, 有可能同时在几个重载的函数上设置了断点 break +OFFSET 或 break OFFSET 此命令用来在当前程序运行到的前几行或后几行设置断点 OFFSET 为行号 break LINENUM 此命令用来在行号为 LINENUM 的行上设置断点 程序在运行到此行之前停止

86 break FILENAME:LINENUM 此命令用来在文件名为 FILENAME 的原文件的第 LINENUM 行设置断点 break FILENAME:FUNCTION 此命令用来在文件名为 FILENAME 的原文件的名为 FUNCTION 的函数上设置断点 当你的多个文件中可能含有相同的函数名时必须给出文件名 break *ADDRESS 此命令用来在地址 ADDRESS 上设置断点, 这个命令允许你在没有调试信息的程序中设置断点 break 当 'break' 命令不包含任何参数时,'break' 命令在当前执行到的程序运行栈中的下一条指令上设置一个断点 除了栈底以外, 这个命令使程序在一旦从当前函数返回时停止 相似的命令是 'finish', 但 'finish' 并不设置断点 这一点在循环语句中很有用 break... if COND 这个命令设置一个条件断点, 条件由 COND 指定 ; 在 gdb 每次执行到此断点时 COND 都被计算当 COND 的值为非零时, 程序在断点处停止 这意味着 COND 的值为真时程序停止 tbreak ARGS 此命令用来设置断点只有效一次 ARGS 的使用同 'break' 中的参量的使用 hbreak ARGS 此命令用来设置一个由硬件支持的断点 ARGS 同 'break' 命令, 设置方法也和 'break' 相同 但这种断点需要由硬件支持, 所以不是所有的系统上这个命令都有效 这个命令的主要目的是用于对 EPROM/ROM 程序的调试 因为这条命令可以在不改变代码的情况下设置断点 这可以同 SPARCLite DSU 一起使用 当程序访问某些变量和代码时,DSU 将设置 " 陷井 " 注意 : 一次只能使用一个断点, 在新设置断点时, 先删除原断点 thbreak ARGS 设置只有一次作用的硬件支持断点 ARGS 用法同 'hbreak' 命令 这个命令和 'tbreak' 命令相似, 它所设置的断点只起一次作用, 然后就被自动的删除 这个命令所设置的断点需要有硬件支持 rbreak REGEX 在所有满足表达式 REGEX 的函数上设置断点 这个命令在所有相匹配的函数上设置无条件断点, 当这个命令完成时显示所有被设置的断点信息 这个命令设置的断点和 'break' 命令设置的没有什么不同 这样可以象操作一般的断点一样对这个命令设置的断点进行删除 使能 使不能等操作 当调试 C++ 程序时这个命令在重载函数上设置断点时非常有用 2 查看断点的方法 info breakpoints 每次设置一个断点后, 都会为每一个断点分配一个断点号, 从 1 开始, 依次递

87 增 3 关闭 / 打开断点方法 : 打开断点方法 :enable 断点号关闭断点方法 :disble 断点号 4 删除断点的方法 : delete breakpoints <Breakpoint Number> (5). 设置观察点 1 观察点的作用 : 可以使用观察点来停止一个程序, 当某个表达式的值改变时, 观察点会将程序停止, 而不需要先指定在某个地方设置一个断点 2 设置观察点的方法 watch EXPR 这个命令使用 EXPR 作为表达式设置一个观察点 GDB 将把表达式加入到程序中并监视程序的运行, 当表达式的值被改变时 GDB 就使程序停止 这个也可以被用在 SPARClite DSU 提供的新的自陷工具中 当程序存取某个地址或某条指令时 ( 这个地址在调试寄存器中指定 ),DSU 将产生自陷 对于数据地址 DSU 支持 'watch' 命令, 然而硬件断点寄存器只能存储两个断点地址, 而且断点的类型必须相同, 即是两个 'rwatch' 型断点, 或是两个 'awatch' 型断点 rwatch EXPR 设置一个观察点, 当 EXPR 被程序读时, 程序被暂停 awatch EXPR 设置一个观察点, 当 EXPR 被读出然后被写入时程序被暂停 这个命令和 'awatch' 命令合用 3 显示观察点的方法 info watchpoints 显示所设置的观察点的列表, 和 'info break' 命令相似 注意 : 在多线程的程序中, 观察点的作用很有限,GDB 只能观察在一个线程中的表达式的值如果你确信表达式只被当前线程所存取, 那么使用观察点才有效 GDB 不能注意一个非当前线程对表达式值的改变 (6) 关于数据的命令关于数据的命令比较多, 下面介绍几个较常用的命令 display 功能 :display 命令用来显示一些表达式的值, 使用该命令后, 每当程序运行到断点时都会显示该表达式的值 使用格式为 : (gdb) display 要显示值的表达式 info display 功能 : 该命令用来显示当前所有的要显示值的表达式的有关情况 它有利于用户增加或删除要显示值的表达式 其使用格式就是直接输入该命令

88 delete display 功能 : 用来删除一个要显示值的表达式, 调用这个命令删除一个表达式后, 被 删除的表达式的值将不再被显示 其使用格式如下 : (gdb) delete display 要删除的显示表达式编号注意 : 如果 delete display 后面没有跟参数,gdb 将会删除所有的要显示值的 表达式 disable display 功能 :disable display 命令使一个要显示值的表达式暂时无效, 而在需要显 示的时候可以再调用 enable display 命令使其重新有效 其使用格式为 : (gdb) disable display 要屏蔽的显示值的表达式编号 enable display 功能 :enable display 命令的作用和 disable display 命令正好相反, 它使显示值被屏蔽的表达式恢复显示 使用格式如下 : (gdb) enable display 要恢复的被屏蔽显示值的表达式编号 whatis 功能 :whatis 命令用来显示某个表达式的数据类型 其使用格式为 : (gdb) whatis 需要查询类型的表达式 print 功能 :print 命令用来打印某个表达式的值, 它也可以用来打印内存中从某个变量开始的一段区域的内容 其使用格式为 : (gdb) print 需要打印的表达式或 (gdb) print 要打印的连续内存空间的大小说明 : 开始表达式必须是内存中的一个表达式, 这条命令是以数组的形式输出 结果的, 数组的第零个元素就是开始表达式的值, 第一个元素就是在内存中紧挨着开始表达式的空间存放的值, 依次类推 set 功能 :set 命令有很多子命令, 它们有的是针对远程调试的, 有的针对多次装入符号表的, 有的用来设置 gdb 一行的字符数 这里仅介绍一种常用的 set 的子命令 set variable, 其功能是用来为变量赋值 使用格式如下 : (gdb) set 变量 = 表达式或 (gdb) set 变量 := 表达式值得注意的是这里的变量可以是程序中的变量, 还可以是寄存器 (7) 关于文件的命令 gdb 提供了十三条关于文件的命令 : add-shared-symbol-file add-symbol-file cd core-file directory file list load path pwd reverse-search 和 search add-shared-symbol-file 功能 :add-shared-symbol-file 命令用来从动态的连接映射的共享目标文件中装入符号表, 如果用户只有一个目标文件, 而且该目标文件的符号表已经装入,gdb

89 将给出如下提示 : This command is not available in this configuration of GDB. add-symbol-file 功能 :add-symbol-file 用来从已经动态装入的文件中装入符号表, 它的使用格式是 : (gdb) add-symbol-file FILE ADDR 其中 FILE 给出的是动态装入的文件的名称,ADDR 是文件中正文的起始地址 cd 功能 :cd 命令用来改变当前工作目录, 这和 shell 里的 cd 命令是一样的 core-file 功能 :core-file 命令使某个文件成为 core dump, 从而可以检查内存和寄存器 directory 功能 :directory 用来向源文件搜索路径中增加一个目录, 其使用格式为 : (gdb) directory 要增加的目录 file 功能 :file 命令是用来装入待调试程序的命令 如果用户在进入 gdb 时不指定待调试程序, 可以用 file 命令来装入待调试程序 它的使用格式如下 : (gdb) file 要装入的文件的名称在装入文件时,gdb 要先将文件的符号表读入, 从而得到 pure memory 的内容 如果在用户指定的目录中没有找到文件,gdb 将会在目录 $PATH 下寻找相同名称的文件 list 功能 : 该命令是用来进行文件内容列表的, 可以有下列几种方式 (gdb) list 要列表的开始行号或 (gdb) list 起始行号截止行号 load 功能 :load 命令用来动态的往正在调试中装入文件, 并记录它的符号表, 准备连接 其格式如下 : (gdb) load 准备装入的文件名称 forward 功能 :forward 命令用来从列表当前行开始向后查找第一个匹配某个字符的程序行 它的使用格式如下 : (gdb) forward 要匹配的字符 path 功能 : 该命令用来向目标文件的搜索路径中增加目录的, 其中也可以使用表示当前目录 $cwd, 这个路径和 shell 下的 $PATH 是等同的, 它由一串目录组成, 中间用逗号分隔开 格式如下 : (gdb) path 要增加到搜索路径中的目录名称 pwd

90 功能 :pwd 命令和 shell 中的 pwd 命令的功能是一样的, 用来显示当前工作路径 reverse-search 功能 :reverse-search 命令的功能和 forward 命令的功能恰好相反, 它是从列表当前行开始向前查找第一个匹配某个字符串的程序行, 其使用格式为 : (gdb) reverse-search 字符串 search 功能 :search 命令和 forward 命令的功能和用法完全一致 3.gdb 应用举例本节用一个实例教你一步步的用 gdb 调试程序, 被调试的程序相当的简单, 但它展示了 gdb 的典型应用 下面列出了将被调试的程序, 这个程序被称为 greeting, 它显示一个简单的问候, 再用反序将它列出 #include <stdio.h> main () { char my_string[] = "hello there"; my_print (my_string); my_print2 (my_string); } void my_print (char *string) { printf ("The string is %s\n", string); } void my_print2 (char *string) { char *string2; int size, i; size = strlen (string); string2 = (char *) malloc (size + 1); for (i = 0; i < size; i++) string2[size - i] = string[i]; string2[size+1] = `\0'; printf ("The string printed backward is %s\n", string2); } 用下面的命令编译它 : gcc -o test test.c 这个程序执行时显示如下结果 :

91 The string is hello there The string printed backward is 输出的第一行是正确的, 但第二行打印出的东西并不是我们所期望的. 我们所设想的输出应该是 : The string printed backward is ereht olleh 由于某些原因, my_print2 函数没有正常工作. 让我们用 gdb 看看问题究竟出在哪儿, 先键入如下命令 : gdb greeting 注意 : 记得在编译 greeting 程序时把调试选项打开. 如果你在输入命令时忘了把要调试的程序作为参数传给 gdb, 你可以在 gdb 提示符下用 file 命令来载入它 : (gdb) file greeting 这个命令将载入 greeting 可执行文件就象你在 gdb 命令行里装入它一样. 这时你能用 gdb 的 run 命令来运行 greeting 了. 当它在 gdb 里被运行后结果大约会象这样 : (gdb) run Starting program: /root/greeting The string is hello there The string printed backward is Program exited with code 041 这个输出和在 gdb 外面运行的结果一样. 问题是, 为什么反序打印没有工作? 为了找出症结所在, 我们可以在 my_print2 函数的 for 语句后设一个断点, 具体的做法是在 gdb 提示符下键入 list 命令三次, 列出源代码 : (gdb) list (gdb) list (gdb) list 技巧 : 在 gdb 提示符下按回车健将重复上一个命令.

92 第一次键入 list 命令的输出如下 : 1 #include <stdio.h> 2 3 main () 4 { 5 char my_string[] = "hello there"; 6 7 my_print (my_string); 8 my_print2 (my_string); 9 } 10 如果按下回车, gdb 将再执行一次 list 命令, 给出下列输出 : 11 my_print (char *string) 12 { 13 printf ("The string is %s\n", string); 14 } my_print2 (char *string) 17 { 18 char *string2; 19 int size, i; 20 再按一次回车将列出 greeting 程序的剩余部分 : 21 size = strlen (string); 22 string2 = (char *) malloc (size + 1); 23 for (i = 0; i < size; i++) 24 string2[size - i] = string[i]; 25 string2[size+1] = `\0'; 26 printf ("The string printed backward is %s\n", string2); 27 } 根据列出的源程序, 你能看到要设断点的地方在第 24 行, 在 gdb 命令行 提示符下键入如下命令设置断点 : (gdb) break 24 gdb 将作出如下的响应 : Breakpoint 1 at 0x139: file greeting.c, line 24 (gdb) 现在再键入 run 命令, 将产生如下的输出 :

93 Starting program: /root/greeting The string is hello there Breakpoint 1, my_print2 (string = 0xbfffdc4 "hello there") at greeting.c :24 24 string2[size-i]=string[i] 你能通过设置一个观察 string2[size - i] 变量的值的观察点来看出错误是怎样产生的, 做法是键入 : (gdb) watch string2[size - i] gdb 将作出如下回应 : Watchpoint 2: string2[size - i] 现在可以用 next 命令来一步步的执行 for 循环了 : (gdb) next 经过第一次循环后, gdb 告诉我们 string2[size - i] 的值是 `h`. gdb 用如下的显示来告诉你这个信息 : Watchpoint 2, string2[size - i] Old value = 0 `\000' New value = 104 `h' my_print2(string = 0xbfffdc4 "hello there") at greeting.c:23 23 for (i=0; i<size; i++) 这个值正是期望的. 后来的数次循环的结果都是正确的. 当 i=10 时, 表达式 string2[size - i] 的值等于 `e`, size - i 的值等于 1, 最后一个字符已经拷到 新串里了. 如果你再把循环执行下去, 你会看到已经没有值分配给 string2[0] 了, 而它是新串的第一个字符, 因为 malloc 函数在分配内存时把它们初始化为空 (null) 字符. 所以 string2 的第一个字符是空字符. 这解释了为什么在打印 string2 时没 有任何输出了. 现在找出了问题出在哪里, 修正这个错误是很容易的. 你得把代码里写入 string2 的第一个字符的的偏移量改为 size - 1 而不是 size. 这是因为 string2 的大小为 12, 但起始偏移量是 0, 串内的字符从偏移量 0 到偏移量 10, 偏移量 11 为空字符保留. 为了使代码正常工作有很多种修改办法. 一种是另设一个比串的实际大小小 1 的变量. 这是这种解决办法的代码 :

94 #include <stdio.h> main () { char my_string[] = "hello there"; my_print (my_string); my_print2 (my_string); } my_print (char *string) { } printf ("The string is %s\n", string); my_print2 (char *string) { char *string2; } int size, size2, i; size = strlen (string); size2 = size -1; string2 = (char *) malloc (size + 1); for (i = 0; i < size; i++) string2[size2 - i] = string[i]; string2[size] = `\0'; printf ("The string printed backward is %s\n", string2); gdb 远程调试 1. 树桩 (stub) 简介 GDB 调试器与一个运行于目标处理器的小 树桩 (stub) 交流 主机调试器与一个使用简单的支持读写注册表和内存的协议交流 你将会发现 GDB 源目录中有许多处理器体系的写好的 树桩, 比如 sh-stub.c, i386-stub.c, and m68k-stub.c 如果没有找到一个适合的 树桩, 需要编译和连接合适的 树桩 到目标嵌入程序和提供两个通讯函数 :getdebugchar() 和 putdebugchar() 系统初始化代码也会调用 set_debug_traps() 函数来初始化 树桩 和安装必要的调试异常处理 编写自己的 树桩 (stub) : 如果无法找到已经写好的 树桩, 就不得不自己编写 树桩 如果对目标 CPUx 体系很熟悉的话, 这样的工程也不是很复杂 最简单的方法就是对现成的 树桩 进行更改 树桩 中绝大多数的代码都与通讯协议有关, 可以可以不变地使用于不同处理器之间 将需要更改内嵌汇编程序部分, 这主要处理安装和处理处理器的异常问题, 包括断点, 单步, 以及通用的保护错误

95 异常处理相对比较简单 : 它们必须将 CPU 注册存放在一个静态缓冲器, 然后输入 handle_exception() 函数, 这一函数带有一个显示异常原因的整数参数 handle_exception() 函数然后取得对 CPU 的控制并处理与主机调试部分的所有通讯 将需要对 handle_exception() 函数作一些特别是更改, 但绝大部分的代码都是可以利用的 2.GDB 远程调试功能介绍在 GDB 里面有一个调试目标 (Target) 的概念 调试目标就是你的程序所获得的执行环境 一般的情况下, 要调试的程序和当前所在的环境是完全一样的, 那么 你用 file 或者 core 命令可以指定调试目标 (file 命令用来指定用来调试的可执行文件,core 命令用来指定调试的时候需要导入的 core dump 文件 ) 另外一种情况就是这里需要介绍的 如果需要调试的程序和 GDB 所运行的环境不同, 或者说需要调试 的环境上根本无法运行起 GDB, 那么就没有办法使用 file 或者 core 命令来指定调试目标了 这里, 就需要使用远程调试功能, 通过一台可以使用 GDB 的机器, 通过串口的通讯协议和被调试的程序所在的机器连接, 从而调试程序 这种情况是很多的, 比如说要调试一个独立的系统, 或者说是实时系统, 都需要和这个系统建立起连接才能完成整个调试过程 在 GDB 里面就是使用 target 命令完成这项工作的 指定需要调试的远程机器的方法是使用 target remote 命令, 后面紧接着需要和远 程机器连接的设备, 如 /dev/ttys0( 串口 1) 等等 在 GDB 里面内嵌有串口的通信协议, 并且规范了和远程机器的调试命令的一些数据传输包的格式 ; 在远程机器上, 需要实现一个 stub 文件, 在这个文件里面需要提供串口连接的协议, 和传送数据信 息的方法 可以这样说,stub 文件代替了在本地主机上 GDB 串口协议的位置, 从而实现两台机器的连接 注意, 所有的符号表数据都放在本地主机上, 在调试之前将符号表导入, 这样调试的时候就可以对着源代码进行调试了 GDB 远程调试的原理 可以参看图 3-2: 应用程序 GDB/XGDB 内核 Stub 程序 远程主机 串口或 TCP/IP 连接 Xwindow 境 本地主机 环 图 3-2:GDB 远程调试环境原理图 在下面几节里将详细介绍这三个内容 :stub 中要实现的串口协议 ; 双方进行通信的数据包格式和调试环境运行的步骤 3.GDB 远程调试建立的条件

96 利用 GDB 进行远程调试并不像在本机上调试一个可执行程序那么简单, 因为需要在两台机器的连接的基础上进行调试 (1). 远程主机上 stub 要实现的函数接口一般的做法是把需要调试的程序在本地主机上进行编译, 注意要加上 -g 调试选项, 然后将程序的一份拷贝放在远程机器上, 本机上保留该程序的符号表, 用于调试的时候读入符号和程序代码 然后就需要把两台机器连接起来, 这里介绍的是通过串口线的方式 在本地主机上输入 target remote /dev/ttys0 命令, 本地主机就通过串口 1 和远程主机里面的 stub 程序相连接 当然, 对于不同的体系结构的系统, 需要编写不同的 stub 程序, 在 GDB 的发布套件里面提供了缺省 stub 文件, 如针对 Sparc 机器的 sparc-stub.c 文件, 针对 m68000 的 m68k-stub.c 和 intel 386 的 i386-stub.c 文件 在这个 stub 文件里面实现的函数接口有如下这些 : 1) 串口驱动支持 : int getdebugchar() 从串口设备读取一个字节的数据 ; void putdebugchar() 向串口设备写一个字节的数据 ; 2) 连接之后控制程序的运行和中止 : set_debug_traps() 用于在你调试的程序停止的时候挂在中断上, 如果有调试的中断到达, 就进入 handle_exception() 函数 那么在你需要调试的程序的开始一定要加上 handle_exception() 函数的调用 handle_exception() 是中断处理的整个过程 可以说, 调试的大部分内容都是在这里完成的 要知道的是, 程序并不会显式的调用它, 而是通过 set_debug_traps() 函数里面给中断处理函数指针初始化的时候把它写进去的 在程序停止运行 ( 比如说, 出现了断点 ) 的时候, 通过这个函数内部的操作和主机的 GDB 进行通信, 那么还可以说, 就是在这里实现和串口通信, 从而完成调试的 实际上, 我们可以认为 handle_exception() 函数完成的就是 GDB 在主机里面完成的工作 它首先是发送一些主机的状态信息, 比如说是寄存器的值一类的, 然后继续运行, 检索和发送 GDB 需要的数据信息, 直到你的 GDB 要求程序继续运行, 这个时候 handle_exception() 把控制权交回给机器 break_point() 这个函数使得程序里面包含有一个断点, 在某些特殊的情况下, 这可能是你的 GDB 获得控制权的唯一办法 (2). 调试双方数据包的传送格式在本地主机上的 GDB 里面的 remote.c 文件负责向远程主机上的 stub.c 发送数据包 所有的调试信息数据包都是使用调试信息 + 校验码组成并进行传送的, 在调试信息的开始都是用 $ 符号作为标记, 而在调试信息的结尾都用 # 符号作为标记, 如下所示 :

97 $< 调试信息 >#< 校验码 > 校验码的值是调试信息里面每个字符的 ASCII 码和模操作 256 的结果 在接收到数据包之后, 用 + 的回答作为接收到正确的数据, 用 - 表示接收出错, 要求重新发送数据 另外, 从主机的 GDB 可以发送一些命令消息数据, 具体描述如下 : g: CPU 寄存器的值 G: 设置 CPU 寄存器的值 maddr,count: 在 addr 位置读取 count 个字节的数据 Maddr,count: 在 addr 位置写 count 个字节的数据 c/caddr: 在当前位置, 重新开始执行或者是从 addr 的位置开始 s/saddr: 单步执行当前的指令, 或者执行到指定的 addr 位置. k: 杀掉 target 进程?: 打印出最近的信号 (signal) 如果这些命令消息都能实现, 在使用 GDB 的时候, 就可以像在本机调试程序一 样 因为 GDB 调试程序只需要向应用程序发出这些控制信号就足够了 (3). 调试步骤的介绍需要远程调试程序时, 首先要对你需要进行远程调试的程序做一些改造, 在程 序的开始插入 set_debug_traps() 和 break_point() 函数, 然后重新编译, 并且将 GDB Stub, 你的程序, 还有串口驱动程序等能一起连接在一起成为新的可执行程序, 将它的一份拷贝到远程主机上 然后按照如下的步骤 : 将两台机器用串口线连接起来 将需要调试的程序拷贝到远程主机 在本地主机启动 GDB, 读入需要调试的程序的符号表和程序代码 使用 target remote 命令建立和远程主机的连接 然后就像和使用一般的 GDB 一样进行程序的调试了 小结 为了进行 Linux 软件开发, 程序员必须掌握如何在该平台下进行编译 链接以及调试 本章对上述知识点逐一进行讲解, 并给出了具体实例 本章的内容是一个嵌入式开发人员必备的基础知识, 在实际的软件开发中经常用到, 希望读者通过本章的学习能够掌握这些知识点, 并在以后的实际编程中不断丰富自己的经验, 这可能是嵌入式 Linux 开发人员最为宝贵的经验之一

98 习题 1. 填空题 (1)GNU 的英文全称为 (2)gcc 的整个编译过程分为 和 四步 (3) 对于通用的 Linux 系统,GNU 编译器生成的目标文件默认格式为 格式, 而 uclinux 则为 格式 2. 问答题 (1)GNU 编译开发环境中编译器 gcc 汇编器 as 以及连接器 ld 各有何作用? (2) 编写一个小程序, 打印 hello world! 字符串, 试用本章学到的知识进行编译连接成可执行文件

99 第四章嵌入式 linux 操作系统 4.1 嵌入式操作系统简介 嵌入式系统是不同于常见计算机系统的一种计算机系统, 它不以独立设备的物理形态出现, 即它没有一个统一的外观, 它的部件根据主体设备以及应用的需要嵌入在设备的内部, 发挥着运算 处理 存储以及控制作用 从体系结构上看, 嵌入 式系统主要由嵌入式处理器 支撑硬件和嵌入式软件组成 其中嵌入式处理器通常是单片机或微控制器 ; 支撑硬件主要包括存储介质 通信部件和显示部件等 ; 嵌入式软件则包括支撑硬件的驱动程序 操作系统 支撑软件以及应用中间件等 可见, 嵌入式系统是一个很大的概念, 一旦嵌入式处理器和支撑硬件选定了, 那么工作最多的就集中在嵌入式软件当中了 而嵌入式软件中的嵌入式操作系统部分和应用软件部分就成了重中之中 它们与通常说的操作系统与应用软件的概念是 相似的, 但也有区别 嵌入式操作系统种类繁多, 目前世界上各种嵌入式操作系统有上千种, 可以从不同角度去划分嵌入式操作系统的分类 比如嵌入式操作系统与应用环境密切相关, 从应用范围角度来看, 大致可以分为通用型的嵌入式操作系统如 Windows CE VxWorks 嵌入式 Linux 等和专用型的嵌入式操作系统如 Palm OS Symbian 等, 从实时性的角度看, 大致可以分为实时嵌入式操作系统和一般嵌入式操作系统, 如图 4-1 所示 嵌入式操作系从应用范围角度 从实时性角度 通用型嵌入式操作系统 专用型嵌入式操作系统 实时嵌入式操作系统 Windows CE VxWorks 嵌入式 Linux Palm OS Symbian 非实时嵌入式操作系统 图 4-1 嵌入式操作系统分类 99

100 从原理上说, 嵌入式操作系统仍旧是一种操作系统, 因此它同样具有操作系统在进程管理 存储管理 设备管理 处理器管理和输入输出管理几方面的基本功能, 但是由于硬件平台和应用环境与一般操作系统的不同, 那么它也有自身的特点, 最大的特点就是可定制性, 也就是它能够提供可配置或可剪裁的内核功能和其他功能, 可以根据应用的需要有选择的提供或不提供某些功能以减少系统开销 WINCE 1.Wince 发展简史众所周知, 微软公司的 windows 系统已经在桌面 PC 上占据了绝对的统治地位, 但是随着后 PC 时代的来临, 微软公司也感觉到了生存的压力 为了在嵌入式系统领域占据一席之地, 微软公司在 1996 年推出了自己的第一个嵌入式操作系统 Windows CE 操作系统 Windows CE 操作系统凭借微软公司的强大实力, 从一推出就得到广泛的关注, 其市场份额也逐年增加 微软公司推出了如下几款嵌入式 windows CE 操作系统 : 1996 年开始发布 Windows CE 1.0 版本 ; 1997 年推出 windows CE2.0 版, 而后推出 windws CE 3.0 版 ; 2003 年 4 月 23 日 Microsoft 公司在旧金山市的 嵌入式系统大会 上正式发布了原先代号为 McKendric 的 Microsoft Windows CE.NET 4.2 版操作系统 ; 2004 年 7 月发布了 Windows CE.NET 5.0 版本 ; 目前用得最多的是 Windows CE.NET 4.2 版本, 下面就以这个版本来详细介绍 wince 平台 2.windows CE 主要功能和特点 windows CE 是一个全新的嵌入式操作系统, 它像 windows95 那样界面友好 windows CE 是一个模块化的操作系统,OEM 厂家可以加入自己所需要的任何模块, 并且即使把系统全部装下也仅需要 500K 的 RAM 就可以了 windows CE 的设计目标是 : 模块化及可伸缩性 实时性能好 通信能力强大 支持多种 CPU 其主要功能特点有以下几点: (1) 程序和服务开发 Windows CE.NET 中合为一体的 Web 和应用程序服务为用户开发能够对 Windows 操作系统 应用程序 数据库和 Internet 进行一体化访问的智能移动连接设备提供了空前的机遇 Active Template Library(ATL, 活动模板库 ) C 库和运行时 组件服务 Component Object Model(COM, 组件对象模型 ) 和 Distributed Component Object Model (DCOM, 分布式组件对象模型 ) 设备管理 100

101 轻量级目录访问服务 (Lightweight Directory Access Protocol,LDAP) 客户端 Microsoft Message Queuing(MSMQ,Microsoft 消息队列 ) Microsoft Foundation Classes(MFC,Microsoft 基础类库 ) Object Exchange Protocol(OBEX, 对象交换协议 ) Microsoft Rich Edit Control 2.0 Pocket Outlook Object Model(POOM) API Simple Object Access Protocol(SOAP, 简单对象访问协议 ) 工具包 Windows CE.NET 标准 SDK.NET Compact Framework SQL Server CE 2.0 XML (2) 应用 - 最终用户随时可用的应用程序依靠底层服务来执行常见任务, 能够在特定类型的设备上 迅速部署应用程序, 例如移动手持设备 数据采集设备以及瘦客户机等 ActiveSync CAB 文件安装程序和卸载程序 文件阅读器 (Excel 图像 PDF PowerPoint Word) FLASH 升级示例程序 游戏 ( 纸牌, 空当接龙 ) 帮助 Inbox( 收件箱 ) 远程桌面连接 终端模拟程序 IP 语音 (Voice over Internet Protocol,VoIP) 电话程序 Windows Messenger WordPad (3) 核心操作系统服务核心操作系统服务包含了有关 Windows CE 内核的信息, 以及所有 Windows CE 平台共用的其它功能特性 核心的操作系统服务可以完成很多低级操作, 例如 : 进程 线程和内存的管理或者提供某些文件系统功能 USB Host 支持 调试工具 电源管理 内核特性 实时支持 字体 国际化 : 为了在国际市场中获得成功, 您的软件必须能够轻松适应各个国家在语言 文化和硬件设备方面的差异 101

102 (4) 通信服务和网络连接 Windows CE.NET 提供了网络和通信功能, 允许设备更加以无线或有线方式安 全地连接到其它设备和人员并展开通信 网络连接能力 Protected Extensible Authentication Protocol(PEAP, 受保护的可扩展身份验证协议 ) 防火墙 网络驱动程序接口规范(Network Driver Interface Specification,NDIS)5.1 实用程序 通用即插即用(UpnP) VoIP TCP/IP TCP/IPv6 局域网 (LAN)( x Wireless Protected Access) 个人区域网络 (PAN)( 蓝牙 红外 ) 广域网 (WAN) 拨号连接 点对点 电话 API 虚拟专用网络(VPN) 服务器 文件传输协议 (FTP) 文件和打印 简单网络时间协议(SNTP) Telnet Web 服务器 (5) 文件系统和数据存储文件系统和数据存储使得设备可以压缩或存储数据, 或者从 RAM 或者 ROM 中读取数据, 并且负责从过滤到分区的各种操作 文件和数据库复制 文件系统 注册表存储 存储管理器 系统口令 (6) 多媒体和浏览服务区 Internet 连接模块让您能够开发出最复杂的 Internet 访问设备 各种层面上的现成协议为您提供了多种 Internet 访问选择 Windows CE.NET 内置了高性能的 DirectX API 以及构建于桌面计算机基础之上的 Microsoft Windows Media 技术, 能够在基于 Windows CE.NET 的设备上实现高性能的音频 视频和流媒体服务 Internet Explorer 6 for Windows CE Pocket Internet Explorer 脚本 (Jscript 5.5 VBScript 5.5) 基本的多媒体服务 (WMA 和 MP3 本地播放 流媒体 WMV 以及 MPEG-4) 多媒体组件 ( 音频 数字版权管理 (DRM) DirectX 8 Windows Media 9 Series 编解码器 ) (7) 安全性 Windows CE.NET 4.2 所支持的安全服务能够帮助用户更加安全地通过网络连接在一起, 或者在多台设备之间直接进行连接, 并且可以更好地保护用户的个人内容和数据 身份验证服务 Security Support Provider Interface(SSPI, 安全支持提供者接口 ) NTLM Kerberos 102

103 Secure Socket Layer(SSL, 安全套接字层 ) 加密服务 带有 High Encryption Provider( 高等级加密提供者 ) 的 CryptoAPI 1.0 智能卡支持 (8) 外壳和用户界面 在根据用户要求创建复杂 易用并且具有图形化界面的设备时, 随时听候调遣的内置用户界面 (UI) 和 UI 服务可以节省大量时间 可定制的消息框 图形 窗口和事件 外壳 (Shell) 用户界面 ( 可定制的 UI 软件输入面板 语音接口 触摸屏 ) 基于 Web 的用户界面 ( 网关 ) 3.windows CE 的应用领域 windows CE 是一个具有抢先式多任务功能, 并具有强大通信能力的嵌入式操作 系统 windows CE 是微软专门为信息设备 移动应用 消费类电子产品 嵌入式应用等非 PC 领域而全新设计的战略性操作系统产品 4.windows CE 集成开发环境 windows CE 内核是通过集成开发环境 Platform Builder 进行定制后编译链接而成 Platform Builder 集成开发环境 (IDE) 可使开发商能够对新一代高度模块化设计进行配置 创建与调试, 以实现嵌入式系统灵活性与可靠性同 Windows 与 Web 功能特性之间的紧密结合 图 4-2 为集成开发环境 Platform Builder 界面示意 图 4-2 集成开发环境 Platform Builder 103

104 "Build" 工具栏 此工具栏上按钮 下拉框都用于编译 调试 数字 1 指向的下拉框是编译指令集, 可以指定不同的指令集来编译 CE 平台或者应用 程序 "Workspace" 窗口 此窗口有三个子视图, 分别为 FeatureView ParameterView FileView 当打开一个平台工程文件后,FeatureView 显示 这个平台所有的特征 如设备驱动程序 各个软件组件等 ParameterView 显示所有平台通用的配置文件和当前平台的配置文件, 这些配置文件扩展名为 *.bib *.reg *.db *.dat FileView 显示在当前 CE 平台上建立的应用 程序源码文件 资源文件 资源脚本文件等 也就是说如果在当前 CE 平台上建立一个应用程序工程, 那么所有的文件都在 FileView 中显示出来 类似 EVC VC 的 "Workspace" 窗口中的 "FileView" 注 : 关于 FeatureView 和 ParameterView 包含的内容在以后的文章中讲解 "Output" 窗口 用于显示输出信息 类似 EVC VC 的 "Output" "Target" 窗口 此工具栏上按钮分别用于下载内核文件到模拟器或实际平 台 连接 断开 当一个 CE 平台编译好了之后, 就可以按下载按钮将平台 (nk,bin) 文件下载到模拟器上运行 "Catalog" 窗口 这个窗口包含所有的 CE 支持的特征 状态栏图标 位于状态栏最右端的四个图标中, 最左边的图标表示当前下载状态 另外三个表示三种服务状态, 这三种服务运行在目标机 (target device) 上 在这里就是模拟器 集成开发环境 Platform Builder 共有 3 万多个文件,2400 多个子文件夹, 因此其文件夹结构比较复杂, 如果不清楚的话在以后的开发过程当中将会带来很大的麻烦, 也时甚至会发生找不到文件的现象 Platform Builder 文件夹可以大体上分 成两个部分, 一个是 PB 的安装文件夹, 一个是 CE 文件夹, 下面分别来了解一下 先来看 PB 的安装文件夹 该文件夹一般装在系统盘的 Program Files\Windows CE Platform Builder\4.20 文件夹下, 文件夹结构如下所示 : cec 文件夹是很重要的一个文件夹, 它是包组件文件 (.CEC 文件 ) 的安放位置, 在 PB 安装以后这里面包含了很多标准的操作系统组件 设备驱动程 序组件 板支持包组件 平台管理组件等, 如果用户想要扩展组件的话, 只需要把相应的 CEC 文件安放在这个文件夹中即可, 因此它是系统组件的配置文件所在的文件夹 Utilities 文件夹中包含的是一个有用的工具, 通过它可以生成系统的启 104

105 动盘, 从而可以引导我们自己定制的 CE 操作系统 Wcetk 文件夹中包含的是另一个有用的工具, 通过它可以测试 CE 的性能 接下来我们看一下 CE 的文件夹, 其结构如下所示 : PLATFORM 文件夹下存放的是与具体平台相关的程序, 当修改某一平台的内核时就要到具体的平台所在的文件夹下去修改, 比如 EMULATOR 平台即模拟器的 KERNEL 部分, 那就要到 EMULATOR 的文件夹下改其 KERNEL 子文件夹下 的源程序 SDK 文件夹包含了 PB 在编译时用到的如 LINK.exe 等程序, 如果我们需要手工编译些什么东西那么可以到这个文件夹下来找相应的工具程序 PUBLIC 文件夹下是各平台要用到的公共的源程序, 也是子文件夹最多的一个文件夹, 它的结构如下 : 其中大部分都是系统组件的源程序比如 IE SHELL, 如果我们想要修改某个组件的行为就可以到相应的文件夹下去找 用得最多的是其中的 COMMON 文件夹, 在该文件夹下的 SDK 文件夹下的 SAMPLES 子文件夹中有一些示范样例程序, 比如大键盘 的输入法的源程序等, 我们可以更改这些源程序 在该文件夹下的 OAK 文件夹中的 CSP 文件夹为 CPU 支持组件, 里面的各子文件夹都是针对特定的 CPU 的内容, 比如针对 ARM I486 SA11X1 等, 如果我们需要处理和特定 CPU 相关的部分就可以到此 文件夹下来操作 在该文件夹下的 DRIVERS 文件夹为微软做好的各种典型设备的驱动程序的源程序, 比如 1394 的驱动 网卡的驱动 串口的驱动等, 如果想要修改驱动或重新驱动, 都可以以这个文件夹下的源程序做参考 5.windows CE 操作系统内核结构 105

106 windows CE 操作系统的内核结构如图 4-3 所示, 可以看出这与 windows 操作系统的内核结构有所不同 不过对于上层应用来说,windows CE 与 windows 操作系统 的 win32 API 兼容, 所以移植 windows 程序到 windows CE 上还是非常简单的 应用程序 动态链接库接口 Win32 API 本地进程服务 堆管理 软中断 Nk.exe 进程接口 虚拟内存进程 / 线程异常处理 调度同步进程切换 内存映射文件 硬件抽象 内存映射 中断地址映射 Nk.lib 进程切换中断处理中断处理 hal.lib 文件系统设备 硬件 图 4-3 windows CE 内核结构 VxWorks 1.VxWorks 简介 VxWorks 操作系统是美国 WindRiver 公司于 1983 年设计开发的一种嵌入式实 106

107 时操作系统 (RTOS), 是嵌入式开发环境的关键组成部分 良好的持续发展能力 高性能的内核以及友好的用户开发环境, 在嵌入式实时操作系统领域占据一席之地 它以良好的可靠性和卓越的实时性在被广泛应用在通信 军事 航空 航天等高精尖技术及实时性要求极高的领域中, 如卫星通讯 军事演习 弹道制导 飞机导航等 在美国的 F-16 FA-18 战斗机 B-2 隐形轰炸机和爱国者导弹上, 甚至连 1997 年 4 月在火星表面登陆的火星探测器上也使用到了 VxWorks VxWorks 支持多种处理器, 如 ARM StrongARM x86 i960 Power PC MC68K 等等, 并且 VxWorks 还提供非常丰富的 API 接口函数, 采用 GNU 的编译和调试器 2.VxWorks 内核结构 VxWorks 嵌入式操作系统包括了进程管理 存储管理 设备管理 文件系统管理 网络协议以及应用程序等几个部分, 其典型结构如图 4-4 所示 应用程序 I/O 网络 子系统 多进程 虚拟内存 子系统 微内核 wind 图 4-4 VxWorks 内核结构 微内核 wind VxWorks 的内核被叫做 wind, 它包括多任务调度 任务间通信机制 中断管理 看门狗和内存管理机制 I/O 子系统 VxWorks 提供了一个快速灵活且与 ANSI C 兼容的 I/O 子系统, 包括 UNIX 系统标准的缓冲 I/O 和 POSIX 标准的异步 I/O 文件系统 VxWorks 系统支持多种文件系统, 如 :DOS 文件系统 rt11 文件系统 raw 文件系统和 tape 文件系统 3.VxWorks 特点 VxWorks 是可裁减的实时操作系统, 由于它具有底层丰富的 API 函数支持, 同时又采用 GNU 的编译和调试器, 这都使得开发者可以在很短的时间内设计出用户需 求的产品来 总的来说,VxWorks 具有下列特点 : 高可靠性操作系统的用户希望在一个工作稳定, 可以信赖的环境中工作, 所以操作系统 的可靠性是用户首先要考虑的问题 而稳定 可靠一直是 VxWorks 的一个突出优点 自从对中国的销售解禁以来,VxWorks 以其良好的可靠性在中国赢得了越来越多的用户 高实时性 107

108 实时性是指能够在限定时间内执行完规定的功能并对外部的异步事件作出响应的能力 实时性的强弱是以完成规定功能和作出响应时间的长短来衡量的 VxWorks 的实时性做得非常好, 其系统本身的开销很小, 进程调度 进程间通信 中断处理等系统公用程序精练而有效, 它们造成的延迟很短 VxWorks 提供的多任务机制中对任务的控制采用了优先级抢占 (Preemptive Priority Scheduling) 和轮转调度 (Round-Robin Scheduling) 机制, 也充分保证了可靠的实时性, 使同样的硬件配置能满足更强的实时性要求, 为应用的开发留下更大的余地 可裁减性用户在使用操作系统时, 并不是操作系统中的每一个部件都要用到 例如图形显示 文件系统以及一些设备驱动在某些嵌入系统中往往并不使用 VxWorks 由一个体积很小的内核及一些可以根据需要进行定制的系统模块组成 VxWorks 内核最小为 8kB, 即便加上其它必要模块, 所占用的空间也很小, 且不失其实时 多任务的系统特征 由于它的高度灵活性, 用户可以很容易地对这一操作系统进行定制或作适当开发, 来满足自己的实际应用需要 4.VxWorks 开发工具 Tornado Tornado 是 Windriver 公司开发的工业上领先的嵌入式软件开发系统, 于 1995 年获得了权威的 EDN 杂志颁发的年度产品创新奖 TornadoII 开发环境上市后延续了第一代 Tornado 开发环境的突出的技术上和商业上的成功, 全球的用户超过了 家 Tornado II 开发环境是嵌入式实时领域里最新一代的开发调试环境, 是实现嵌入式实时应用程序的完整的软件开发平台, 是交叉开发环境运行在主机上的部分, 是开发和调试 VxWorks 系统不可缺少的组成部分 Tornado 给嵌入式系统开发人员提供了一个不受目标机资源限制的超级开发和调试环境 Tornado 的特点 : Tornado 可以运行在多种主机上,Tornado 是一个友好的开发环境, 支持 UNIX Windows NT Windows98/95 等 独立于硬件环境而先行开发应用程序的目标机仿真器 VxSim,Tornado II 提供的目标机仿真器 VxSim, 使开发者可独立于硬件环境而先行开发应用程序,, 从而节省了新产品的研发时间和硬件方面的开销 Tornado 具有可视化图形界面的调试工具 : 核心工具和 WindPower 工具 Tornado 具有可视化图形界面的调试工具很大程度地方便了开发者的调试工作 它的工具包括 : 核心工具 : a. 图形化的交叉调试器 (Debugger)CrossWind/WDB: 远程的源代码集成调试器, 支持任务级和系统级调试, 支持混合源代码和汇编代码显示, 支持多目标同时调试 b. 工程配置工具 (Project Facility/Configuration): 图形化的工程配置工具, 自动 makefile 的生成, 可配置和裁减 VxWorks c. 集成仿真器 (Integrated Simulator): 提供与真实目标机一致的调试和 108

109 仿真运行环境 d. 诊断分析工具 (WindView for the Integrated Simulator): WindView 是 一个图形化的动态诊断和分析工具, 主要是向开发者提供目标机硬件上实际运行的应用程序的许多的详细情况 e. C/C++ 编译环境 (C/C++ Compilation Environment) :Tornado 提供交叉编译器,iostreams 类库和一些列的工具来支持 C 语言和 C++ 语言 f. 命令行执行工具 (WindSh) :Tornado 的命令行执行工具 WindSh 是 Tornado 所独有的功能强大的命令行解释器, 可以直接解释执行 C 语句表达式 调用目标机上的 C 函数 访问系统符号表中登记的变量 WindPower 工具 : i. 软件逻辑分析仪 WindView:WindView 是一个图形化的动态诊断和分析工具, 主要是向开发者提供目标机硬件上实际运行的应用程序的许多的详细情况 ii. 原型仿真器 VxSim:VxSim 是一个原型仿真器, 主要是使开发者在没有实际的目标硬件情况下, 先进行原型机应用程序的开发, 包括网络设计和基于多处理器的设计 VxSim 还可以使开发者在开发周期中较早地进行大部分的应用软件测试, 使开发者能够以较小的代价纠正错误 iii. 显示软件包 ScopePak: 显示软件包 ScopePak 允许开发者在应用程序运行时可以监视数据和函数调用, 这些工具不需要重新编译 显示软件包包括以下两个工具 : 软件示波器 StethoScope: 是一个实时数据收集 显示 文档和调试工具, 主要是使开发者可以在应用程序运行时对其进行分析 跟踪示波器 TraceScope: 是一个追踪程序执行过程的工具 iv. 性能检测包 PerformancePak ProfileScope: 主要是提供细节的 到每个函数的运行信息, 可以帮助开发者查看 CPU 工作情况和确定性瓶颈 MemScope: 主要是帮助开发者控制内存使用 检查内存泄露 查看内存使用情况 e. 代码测试器 CodeTest Coverage Module: 主要是确定代码中未经测试的部分和提供系统的动态视图 Memory Module: 主要是显示内存的动态分配情况, 检查内存泄露 f. 编辑工具 Visual SlickEdit for Tornado Visual SlickEdit for Tornado 主要是为 Tornado 开发环境提供特殊支持的编辑工具 109

110 4.1.3 嵌入式 LINUX 1.Linux 发展 Linux 是一种 UNIX 操作系统的克隆, 它 ( 的内核 ) 由 Linus Torvalds 以及网络上组织松散的黑客队伍一起从零开始编写而成 Linux 的目标是保持和 POSIX 的兼容 它具备现代一切功能完整的 UNIX 系统所具备的全部特征, 其中包括真正 的多任务 虚拟内存 共享库 需求装载 共享的写时复制程序执行 优秀的内存管理以及 TCP/IP 网络支持等 同时 Linux 的发行遵守 GNU 的通用公共许可证 目前 Linux 已经成为最稳定成熟的操作系统之一 Linux 起初为基于 386/486 的 PC 机开发, 但现在,Linux 也可以运行在 DEC Alpha SUN Sparc M68000, 以及 MIPS 和 PowerPC 等计算机上 迄今为止, 世界上已有近 40% 的 Internet 主机采用 Linux 作为操作系统, 而且它们工作的非常稳 定 Linux 具备下列基本特征 : 真正的多用户 多任务操作系统 ; 符合 POSIX 标准 ; 具有完善的内核的编程接口 ; 提供具有内置安全措施的分层的文件系统 ; 提供 shell 命令解释程序和编程语言 ; 提供强大的管理功能 ; 具有功能强大的图形用户接口 ; 具有丰富的应用程序和工具资源 兼容多种高级程序设计语言, 是理想的应用软件开发平台, 同时具备良好的跨平台移植能力 2.Linux 内核结构 Linux 内核主要由五个子系统组成 : 进程调度, 内存管理, 虚拟文件系统, 网络接口, 进程间通信, 如图 4-5 所示 内存管理 虚拟文件系统 进程调度 进程间通信 网络接口 图 4-5 Linux 内核结构 进程调度 (SCHED) 控制进程对 CPU 的访问 当需要选择下一个进程运 110

111 行时, 由调度程序选择最值得运行的进程 可运行进程实际上是仅等待 CPU 资源的进程, 如果某个进程在等待其它资源, 则该进程是不可运行进程 Linux 使用了比较简单的基于优先级的进程调度算法选择新的进程 内存管理 (MM) 允许多个进程安全的共享主内存区域 Linux 的内存管理支持虚拟内存, 即在计算机中运行的程序, 其代码, 数据, 堆栈的总量可以超过实际内存的大小, 操作系统只是把当前使用的程序块保留在内存中, 其余的程序块则保留在磁盘中 必要时, 操作系统负责在磁盘和内存间交换程序块 内存管理从逻辑上分为硬件无关部分和硬件有关部分 硬件无关部分提供了进程的映射和逻辑内存的对换 ; 硬件相关的部分为内存管理硬件提供了虚拟接口 虚拟文件系统 (VirtualFileSystem,VFS) 隐藏了各种硬件的具体细节, 为所有的设备提供了统一的接口,VFS 提供了多达数十种不同的文件系统 虚拟文件系统可以分为逻辑文件系统和设备驱动程序 逻辑文件系统指 Linux 所支持的文件系统, 如 ext2,fat 等, 设备驱动程序指为每一种硬件控制器所编写的设备驱动程序模块 网络接口 (NET) 提供了对各种网络标准的存取和各种网络硬件的支持 网络接口可分为网络协议和网络驱动程序 网络协议部分负责实现每一种可能的网络传输协议 网络设备驱动程序负责与硬件设备通讯, 每一种可能的硬件设备都有相应的设备驱动程序 进程间通讯 (IPC) 支持进程间各种通信机制 处于中心位置的进程调度, 所有其它的子系统都依赖它, 因为每个子系统都需要挂起或恢复进程 一般情况下, 当一个进程等待硬件操作完成时, 它被挂起 ; 当操作真正完成时, 进程被恢复执行 例如, 当一个进程通过网络发送一条消息时, 网络接口需要挂起发送进程, 直到硬件成功地完成消息的发送, 当消息被成功的发送出去以后, 网络接口给进程返回一个代码, 表示操作的成功或失败 其他子系统以相似的理由依赖于进程调度 3. 嵌入式 Linux 种类嵌入式 linux 种类繁多, 各有各的特点, 下面简单介绍几种较常用的嵌入式 Linux 版本 (1)RT-Linux 这是由美国墨西哥理工学院开发的嵌入式 Linux 操作系统 到目前为止, RT-Linux 已经成功地应用于航天飞机的空间数据采集 科学仪器测控和电影特技图像处理等广泛领域 RT-Linux 开发者并没有针对实时操作系统的特性而重写 Linux 的内核, 因为这样做的工作量非常大, 而且要保证兼容性也非常困难 为此,RT-Linux 提出了精巧的内核, 并把标准的 Linux 核心作为实时核心的一个进程, 同用户的实时进程一起调度 这样对 Linux 内核的改动非常小, 并且充分利用了 Linux 下现有的丰富的软件资源 (2)uClinux uclinux 是 Lineo 公司的主打产品, 同时也是开放源码的嵌入式 Linux 的典范之 111

112 作 uclinux 主要是针对目标处理器没有存储管理单元 MMU(Memory Management Unit) 的嵌入式系统而设计的 它已经被成功地移植到了很多平台上 由于没有 MMU, 其多任务的实现需要一定技巧 uclinux 是一种优秀的嵌入式 Linux 版本, 是 micro-conrol-linux 的缩写 它秉承了标准 Linux 的优良特性, 经过各方面的小型化改造, 形成了一个高度优化的 代码紧凑的嵌入式 Linux 虽然它的体积很小, 却仍然保留了 Linux 的大多数的优点 : 稳定 良好的移植性 优秀的网络功能 对各种文件系统完备的支持和标准丰富的 API 它专为嵌入式系统做了许多小型化的工作, 目前已支持多款 CPU 其编译后目标文件可控制在几百 KB 数量级, 并已经 被成功地移植到很多平台上 (3) 红旗嵌入式 Linux 由北京中科院红旗软件公司推出的嵌入式 Linux 是国内做得较好的一款嵌入式 操作系统 目前, 中科院计算所自行开发的开放源码的嵌入式操作系统 Easy Embedded OS(EEOS) 也已经开始进入实用阶段了 该款嵌入式操作系统重点支持 p-java 系统目标一方面是小型化, 另一方面能重用 Linux 的驱动和其它模块 由于有中科院计算所的强大科研力量做后盾,EEOS 有望发展成为功能完善 稳定 可靠的国产嵌入式操作系统平台 4.2 嵌入式 uclinux uclinux 简介 uclinux 是嵌入式 Linux 中非常重要的一个分支, 它的诞生说明了 Linux 操作系统具有非常好的适应性和较强的生存能力 据统计全球每年生产的 CPU 的数量大概在二十亿颗左右, 其中大部分是应用于专用性很强的各类嵌入式系统 我们知道, 嵌入式产品具有功耗低 成本低等优点, 而生产出的嵌入式 CPU 为了减少系统复杂程度 降低硬件开发成本和运行功耗, 在硬件设计中取消了内存管理单元 (MMU) 模块 但是我们知道,Linux 是需要运行在有 MMU 的 CPU 基础之上, 因此标准 Linux 是没有办法在此类 CPU 上运行, 导致运行于这类没有 MMU 的 CPU 之上的都是一些很简单的单任务操作系统, 或者更简单的控制程序, 甚至根本就没有操作系统而直接运行应用程序 在这种情况下, 系统无法运行复杂的应用程序, 或者效率很低, 并且所有的应用程序需要重新开发, 还要求开发人员十分了解硬件特性 这些都阻碍了不含 MMU 的嵌入式产品开发的速度和应用水平 uclinux 专门针对没有 MMU 的 CPU, 并且为嵌入式系统做了许多小型化的工作 uclinux 是一个完全符合 GNU/GPL 公约的项目, 完全开放代码 最初的 uclinux 仅仅支持 Palm 硬件系统, 基于 Linux 2.0 内核 随着系统的日益改进, 支持的内核版本从 一直到现在最新的 2.6 系统的开发人员从两人增加到了目前的 12 人, 支持的硬件系统也从一种增加到了目前的十余种 ( 支持 112

113 的硬件平台如 Motorola 公司的 M68328 M68EN322 MC68360 DragonBall 系列如 68EZ328 68VZ328,ColdFire 系列的如 ,ARM 7TDMI MC68EN302 ETRAX Intel i960 PRISMA Atari 68k 等等 ) uclinux 应用实例一 : 苹果的 ipod 是一款时尚的 MP3 播放器, 采用苹果公司的专用 MP3 播放软件, 是市场上销量最好的 MP3 之一 最近狂热的 Linux 爱好者对 其进行了改造, 在不改动硬件的环境下移植了内核版本为 的 uclinux( 基于 ARM 32 位处理器内核 ARM 7TDMI), 并使其具备了 FramBuffer 硬盘支持 远程控制等功能, 使一台 MP3 变成了一台 PDA 该项目一直处于活跃阶段, 许多新的 功能正在添加之中 图 4-6 运行 uclinux 的苹果 ipod-mp3 播放器 uclinux 应用实例二 : 图 4-7 显示了在 Cisco 3000 上路由器运行 uclinux 的图片 事实上, 在基于 MIPS 处理器的 Cisco 2500/3000/4000 系列路由器上,uClinux 都得到了移植, 并能够出色稳定地工作 图 4-7 运行 uclinux 的 Cisco 3000 路由器 以上两个例子显示了作为一个开放源代码的操作系统,uClinux 有着优良的可移植性和广泛的兼容性, 其应用遍及 DVD 播放器 PDA 路由器 防火墙 机顶盒 工业控制设备 VOIP 设备 数码摄像头 智能家具设备等 113

114 4.2.2 uclinux 体系结构 uclinux 体系结构如下图 4-8 所示 系统调用函数 内核初 始化 调度 虚拟文件系统管理 文件系统 字符设备驱动 Socket 驱动网络模块块设备驱动网络设备驱动 IPC 模块 MM 模块 C 库文件 启动模块 捕获 Handler 图 4-8 uclinux 体系结构 1. 启动模块 : 启动模块用来启动 Linux, 它首先进行硬件初始化, 建立堆栈, 然后把压缩的 Linux 映像文件从 Flash 设备中搬到 SDRAM 中, 并解压缩, 最后跳到内核的入口处, 把控制权交给内核 2. 内核初始化 : 启动模块把控制权交给内核后, 就正式拉开了 Linux 启动的序 幕, 内核初始化的入口函数为位于 init/main.c 文件中的 start_kernel() 函数, 它进行一系列内核的初始化, 包括建立中断表并打开中断 内存页初始化 调度初始化, 最后启动 init 进程进入多任务环境 3. 系统调用函数 :Linux 内核启动起来后, 它不会再对内核进行直接控制, 而是通过系统调用的形式给用户应用程序提供服务, 比如中断机制 用户想要得到系统资源, 必须通过系统调用 ( 例如中断的形式 ), 内核得到系统调用请求后, 把用户 挂起, 并进行处理, 最后把结果返回给用户 4. 文件系统 :Linux 中, 文件系统处理非常巧妙, 它支持多种文件系统类型, 比如 uclinux 中常用的文件系统类型就包括 ROMFS JFFS/JFFS2 YAFS 等, 在这些 文件系统上层,Linux 提供了一个统一的接口 虚拟文件系统 (VFS), 这样上层应用程序就可以不必理会底层到底采用了哪种具体的文件系统类型, 提高了编程的效率 5. 驱动程序 : 在嵌入式 Linux 开发中, 驱动程序占据了非常大的部分, 以至于一个公司大部分的人员都是从事驱动开发的人员 驱动程序控制着操作系统和外部设备的交互, 在 Linux 系统中, 可以通过两种方式把驱动程序加载到内核中 : 第一, 直接把驱动程序编译进内核 ; 第二, 通过模块的形式进行加载 6. 内存管理 :uclinux 没有 MMU, 但是仍然采用存储器的分页管理, 系统在启动 114

115 时把实际存储器进行分页 7.C 库文件 : 在 uclinux 中使用的 C 库文件是 uclibc, 它是由 glibc 库移植而 来, 除了去掉大部分不适合嵌入式系统的代码以外, 还对应用程序入口代码, 系统调用的 API, 输入输出过程等进行了修改, 并增加了对平面存储器结构的支持 uclinux 内核组成 uclinux 内核目录树如下图 4-9 所示 图 4-9 uclinux 内核目录树 115

116 arch:arch 目录下有多个子目录, 它的每一个子目录都代表内核支持的一种 CPU 体系结构, 每个子目录中又进一步分解为 boot mm kernel 等子目录, 分别包 含与系统引导 内存管理 系统调用的进入和返回 终端处理以及其它内核中依赖于 CPU 和系统结构的底层代码 与 ARM 处理器 ( 不带有 MMU) 相关的代码放在目录 arch/armnommu 下, 与 S3C44B0X 相关的代码则放在目录 arch/armnommu/match-s3c44b0x include:include 子目录包括编译核心所需要的大部分头文件 与平台无关的头文件在 include/linux 子目录下, 与 ARM 处理器 ( 不带 MMU ) 相关的头文件在 include/asm-armnommu 子目录下, 与 S3C44B0X 相关的代码在 include/asm-armnommu/arch-s3c44b0x 目录下 ; init: 这个目录包含核心的初始化代码 ( 注意 : 不是系统的引导代码 ), 包含两个文件 main.c 和 Version.c, 这是研究核心如何工作的一个非常好的起点 kernel: 主要的核心代码, 此目录下的文件实现了大多数 linux 系统的内核函数, 其中最重要的文件当属 sched.c; 同样, 和体系结构相关的代码在 arch/*/kernel 中 ; drivers: 放置系统所有的设备驱动程序 ; 每种驱动程序又各占用一个子目录 : 如,/block 下为块设备驱动程序, 比如 ide(ide.c) 其他 : 例如 mm, 这个目录包括所有独立于处理器体系结构的内存管理代码, 如页式存储管理内存的分配和释放等 ;lib 放置核心的库代码 ;net, 核心与网络相关的代码 ;ipc, 这个目录包含核心的进程间通讯的代码 ;fs, 所有的文件系统代码和各种类型的文件操作代码, 它的每一个子目录支持一个文件系统, 例如 fat 和 ext2;scripts, 此目录包含用于配置核心的脚本文件等 一般在每个目录下, 都有一个.depend 文件和一个 Makefile 文件, 这两个文件都是编译时使用的辅助文件, 仔细阅读这两个文件对弄清各个文件这间的联系和依托关系很有帮助 ; 而且, 在有的目录下还有 Readme 文件, 它是对该目录下的文件的一些说明, 同样有利于我们对内核源码的理解 uclinux 存储管理 uclinux 同标准 Linux 的最大区别就在于内存管理, 同时也由于 uclinux 的内存管理引发了一些标准 Linux 所不会出现的问题 本文将把 uclinux 内存管理同标准 Linux 的内存管理部分进行比较分析 1. 标准 Linux 内存管理标准 Linux 使用虚拟存储器技术, 这种技术是针对有内存管理单元的处理器设计的 在这种处理器上, 虚拟地址被送到内存管理单元 (MMU), 把虚拟地址映射为物理地址 由于标准 Linux 具有 MMU 机制, 因此其内存管理具有下列特点 : (1) 可以运行比内存还要大的程序, 理想情况下应该可以运行任意大小的程序 116

117 物理内存是有限的, 但是虚拟内存可以是任意大的 也就是说标准 Linux 内存管理提供了比计算机系统中实际使用的物理内存大得多的内存空间 使用者将感觉到好像程序可以使用非常大的内存空间, 从而使得编程人员在写程序时不用考虑计算机中的物理内存的实际容量 为了支持虚拟存储管理器的管理,Linux 系统采用分页 (paging) 的方式来载入进程 所谓分页既是把实际的存储器分割为相同大小的段, 例如每个段 1024 个字节, 这样 1024 个字节大小的段便称为一个页面 (page) 虚拟存储器由存储器管理机制及一个大容量的快速硬盘存储物理器支持 它的实现基于局部性原理, 当一个程序在运行之前, 没有必要全部装入内存, 而是仅将那些当前要运行的那些部分页面或段装入内存运行 (copy-onwrite), 其余暂时留在硬盘上程序运行时如果它所要访问的页 ( 段 ) 已存在, 则程序继续运行, 如果发现不存在的页 ( 段 ), 操作系统将产生一个页错误 (page fault), 这个错误导致操作系统把需要运行的部分加载到内存中 必要时操作系统还可以把不需要的内存页 ( 段 ) 交换到磁盘上 利用这样的方式管理存储器, 便可把一个进程所需要用到的存储器以化整为零的方式, 视需求分批载入, 而核心程序则凭借属于每个页面的页码来完成寻址各个存储器区段的工作 (2) 提供内存保护, 进程不能以非授权方式访问或修改页面, 内核保护单个进程的数据和代码以防止其它进程修改它们 否则, 用户程序可能会偶然 ( 或恶意 ) 的破坏内核或其它用户程序 标准 Linux 是针对有内存管理单元的处理器设计的 在这种处理器上, 虚拟地址被送到内存管理单元 (MMU), 把虚拟地址映射为物理地址 通过赋予每个任务不同的虚拟 物理地址转换映射, 支持不同任务之间的保护 地址转换函数在每一个任务中定义, 在一个任务中的虚拟地址空间映射到物理内存的一个部分, 而另一个任务的虚拟地址空间映射到物理存储器中的另外区域 计算机的存储管理单元 (MMU) 一般有一组寄存器来标识当前运行的进程的转换表 在当前进程将 CPU 放弃给另一个进程时 ( 一次上下文切换 ), 内核通过指向新进程地址转换表的指针加载这些寄存器 MMU 寄存器是有特权的, 只能在内核态才能访问 这就保证了一个进程只能访问自己用户空间内的地址, 而不会访问和修改其它进程的空间 当可执行文件被加载时, 加载器根据缺省的 ld 文件, 把程序加载到虚拟内存的一个空间, 因为这个原因实际上很多程序的虚拟地址空间是相同的, 但是由于转换函数不同, 所以实际所处的内存区域也不同 而对于多进程管理当处理器进行进程切换并执行一个新任务时, 一个重要部分就是为新任务切换任务转换表 此外, 还可以提供下列几个功能 : 可以运行只加载了部分的程序, 缩短了程序启动的时间 可以使多个程序同时驻留在内存中提高 CPU 的利用率 可以运行重定位程序 即程序可以放在内存中的任何一处, 而且可以在执行过程中移动 写机器无关的代码 程序不必事先约定机器的配置情况 减轻程序员分配和管理内存资源的负担 117

118 可以进行共享 例如, 如果两个进程运行同一个程序, 它们应该可以共享程序代码的同一个副本 虚存系统并不是没有代价的 内存管理需要地址转换表和其他一些数据结构, 留给程序的内存减少了 地址转换增加了每一条指令的执行时间, 而对于有额外内存操作的指令会更严重 当进程访问不在内存的页面时, 系统发生失效 系统处理该失效, 并将页面加载到内存中, 这需要极耗时间的磁盘 I/O 操作 总之内存管理活动占用了相当一部分 cpu 时间 ( 在较忙的系统中大约占 10%) 2.uClinux 内存管理 uclinux 针对 NOMMU 的特殊处理对于 uclinux 来说, 其设计针对没有 MMU 的处理器, 即 uclinux 不能使用处理器的虚拟内存管理技术 ( 应该说这种不带有 MMU 的处理器在嵌入式设备中相当普偏 ) 但是要提醒读者的是, 由于 uclinux 是从 Linux 裁减而来, 因此它仍然采用存储器的分页管理, 系统在启动时把实际存储器进行分页 在加载应用程序时程序分页加载 但是由于没有 MMU 管理, 所以实际上 uclinux 采用实存储器管理策略 (real memeory management) 这一点影响了系统工作的很多方面 uclinux 系统对于内存的访问是直接的,( 它对地址的访问不需要经过 MMU, 而是直接送到地址线上输出 ), 所有程序中访问的地址都是实际的物理地址 操作系统对内存空间没有保护 ( 这实际上是很多嵌入式系统的特点 ), 各个进程实际上共享一个运行空间 ( 没有独立的地址转换表 ) 一个进程在执行前, 系统必须为进程分配足够的连续地址空间, 然后全部载入主存储器的连续空间中 与之相对应的是标准 Linux 系统在分配内存时没有必要保证实际物理存储空间是连续的, 而只要保证虚存地址空间连续就可以了 另外一个方面程序加载地址与预期 (ld 文件中指出的 ) 通常都不相同, 这样 relocation 过程就是必须的 此外磁盘交换空间也是无法使用的, 系统执行时如果缺少内存将无法通过磁盘交换来得到改善 uclinux 对内存的管理减少同时就给开发人员提出了更高的要求 如果从易用性这一点来说,uClinux 的内存管理是一种倒退, 退回了到了 UNIX 早期或是 Dos 系统时代 开发人员不得不参与系统的内存管理 从编译内核开始, 开发人员必须告诉系统这块开发板到底拥有多少的内存 ( 假如你欺骗了系统, 那将在后面运行程序时受到惩罚 ), 从而系统将在启动的初始化阶段对内存进行分页, 并且标记已使用的和未使用的内存 系统将在运行应用时使用这些分页内存 从内存的访问角度来看, 开发人员的权利增大了 ( 开发人员在编程时可以访问任意的地址空间 ), 但与此同时系统的安全性也大为下降 此外, 系统对多进程的管理将有很大的变化, 这一点将在 uclinux 的多进程管理中说明 虽然 uclinux 的内存管理与标准 Linx 系统相比功能相差很多, 但应该说这是嵌入式设备的选择 在嵌入式设备中, 由于成本等敏感因素的影响, 普偏的采用不带有 MMU 的处理器, 这决定了系统没有足够的硬件支持实现虚拟存储管理技术 从嵌入式设备实现的功能来看, 嵌入式设备通常在某一特定的环境下运行, 只要实现特定的功能, 其功能相对简单, 内存管理的要求完全可以由开发人员考虑 图 4-10 为标准 Linux 系统和 uclinux 系统的进程内存布局图, 在标准 Linux 系统 118

119 中 ( 如左图 ), 系统数据段, 代码段, 堆和栈在虚存层面是连续的 堆向上增长, 栈向下增长, 在堆底和栈顶之间有 256MB 的内存可供分配 uclinux 采用了实内存 模式, 各个内存段在物理内存 ( 没有虚存 ) 层面都是连续的, 栈段在同数据段在一起, 堆有系统内存管理, 所有进程共享, 由于内存连续和保护的要求 ( 详见正文 ), 栈段, 数据段, 代码段都是在程序加载是分配, 栈段的大小固定 ( 在生成应用时可 以指定栈段大小 ), 开发人员在开发时不得不使用一些方法估计判断栈段的大小, 使其即能满足程序的需要, 又不浪费内存 图 4-10 内存分布图 应用在嵌入式系统开发板上跑起来后,CPU 中的内存映象 ( 如图 4-11): 119

120 图 4-11 CPU 内存映射图 这种内存空间布局还阻碍了动态连接库的运用, 一个很简单的方面, 应用程序 将不知道到哪儿去寻找已被加载的动态库的位置 ( 使用虚拟内存的方式可以利用虚拟内存的重映射表 ) 这个问题的一个解决办法事先给定动态库被加载物理内存的某个位置, 并且确定要使用的内存空间, 在系统启动是首先加载动态库 ( 加载到事 先假定的位置 ) 另一个问题是这个动态库必须是绝对可重入的, 由于没有了内存保护的机制 ( 通常由 MMU 单元提供 ), 所有动态库内的数据需要使用局部变量, 以使得这些数据分配于各个进程的栈段, 如果考虑使用锁的机制实现这种共享访问可 能更加复杂 3.uCLinux 的内存初始化 在标准 Linux 的启动过程中, 有很大一部分代码是用来进行内存模块的初始化工作, 比如页目录和页表映射的建立等, 本节将对 uclinux 内存模块启动初始化部 分进行分析 下面首先介绍 uclinux 内存管理相关的数据结构 120

121 (1) uclinux 内存管理数据结构众所周知,Linux 采用 Node Zone 和页三级结构来描述物理内存, 如图 4-12 所示,uClinux 虽然没有 MMU 机制, 但它沿袭了 Linux 描述内存的三级结构 因此 uclinux 和内存相关的数据结构主要有下列几个 : 存储节点结构体 struct pglist_data 区域结构体 struct zone_struct 页结构体 struct page 图 4-12 Linux 中 Node Zone 和页的关系 1 存储节点结构体 struct pglist_data 在传统计算机体系中, 整个物理空间都是均匀一致的, 也就是 CPU 访问某一个 地址的时间与位置无关, 于是称为 均匀存储结构 (Uniform Memory Architecture), 简称 UMA 但现在这种传统被打破了, 特别表现在多 CPU 结构的系统中, 均匀的存储结构已经被破坏了, 这就导致了一种可能, 比如有 4 个页面的需要要储存, 但 本地的空闲空间却只有 3 页, 如果按照 UMA 的做法, 会把前三个页面存入本地, 而剩下的一个存入公用存储区, 但由于是不均匀的, 这显然不适合 在 NUMA 结构的系统中, 属于同一个 CPU 节点的存储器模块通常是匀质的, 因此也被称为 " 存储节点 " (Memory Node) 而为了解决 NUMA 这种问题, 便提出了 pglist_data 这种结构, 对存储节点进行可选择的管理 uclinux 在头文件 include/linux/mmzone.h 中定义了数据结构 pglist_data 来 描述一个存储节点 : typedef struct pglist_data { zone_t node_zones[max_nr_zones]; //3 种分配区 121

122 zonelist_t node_zonelists[nr_gfpindex]; //256 种分配类型 struct page *node_mem_map; // 分配域的页结构表 unsigned long *valid_addr_bitmap; struct bootmem_data *bdata; unsigned long node_start_paddr; // 分配域的起始物理地址 unsigned long node_start_mapnr; // 分配域的起始物理页号 unsigned long node_size; // 分配域页表总数 int node_id; // 分配域编号 struct pglist_data *node_next; // 分配域链表 } pg_data_t; // 分配域结构 结构体每个成员定义如下所示 : 结构体成员 Node_zones 说明 该结点的 zone 类型, 一般包括 ZONE_HIGHMEM ZONE_NORMAL 和 ZONE_DMA 三类 分配时内存时 zone 的排序 它是由 free_area_init_core() Node_zonelists 通过 page_alloc.c 中的 build_zonelists() 设置 zone 的顺序该结点的 zone 个数, 可以从 1 到 3, 但并不是所有的结点 nr_zones 都需要有 3 个 zone 内核自举时所使用的分配域, 大家在初始化的时候要极度注意 bdata 该区域, 在初始化的时候, 是该结构负责管理内存的使用的它是 struct page 数组的第一页, 该数组表示结点中的每个 node_mem_map 物理页 根据该结点在系统中的顺序, 它可在全局 mem_map 数组中的某个位置 Valid_addr_bitmap 用于描述结点内存空洞的位图 node_start_paddr 该结点的起始物理地址给出在全局 mem_map 中的页偏移, 在 free_area_init_core() node_start_mapnr 计算在 mem_map 和 lmem_map 之间的该结点的页数目 node_size 该 zone 内的页总数 node_id 该结点的 ID, 全系统结点 ID 从 0 开始说明 : 若干存储节点的 pglist_data 数据结构通过指针 node_next 形成一个单向链接的存储节点队列, 在 init_bootmem_core 函数中完成该列表初始化工作 系统仍然维护一个全局的 page 结构数组 mem_map, 但是每个存储节点都通过 pglist_data 结构中的指针 node_mem_map 指向 mem_map 数组中属于该存储节点的起始物理页帧的 page 结构 122

123 数组 node_zones[max_nr_zones] 定义了属于该存储节点的内存区 数组 node_zonelists[nr_gfpindex] 定义了拥有该存储节点的 CPU 在分配 连续的物理页帧块时可以选择使用物理内存区 现在虽然有的板子暂时不支持 NUMA, 但是内核的数据结构还是按照 NUMA 的方式构建的 在文件 linux/mmnommu/numa.c 中定义了存储节点, 形式如下 : static bootmem_data_t contig_bootmem_data; pg_data_t contig_page_data = { bdata: &contig_bootmem_data }/* 它是一个代表本节点的内存信息的一个数据结构, 相当于 contig_page_data->bdata = &contig_bootmem_data*/ 2 区域结构体 struct zone_struct 每个结点的内存被分为多个块, 称为 zones, 它表示内存中一段区域 一个 zone 用 struct_zone_t 结构描述,zone 的类型主要有 ZONE_DMA ZONE_NORMAL 和 ZONE_HIGHMEM ZONE_DMA 位于低端的内存空间, 用于某些旧的 ISA 设备 ZONE_NORMAL 的内存直接映射到 Linux 内核线性地址空间的高端部分, 许多内核操作只能在 ZONE_NORMAL 中进行 例如, 在 X86 中,zone 的物理地址如下 : 类型地址范围 ZONE_DMA 前 16MB 内存 ZONE_NORMAL 16MB - 896MB ZONE_HIGHMEM 896 MB 以上 uclinux 在头文件 include/linux/mmzone.h 中定义了数据结构 zone_struct typedef struct zone_struct { spinlock_t lock; unsigned long free_pages; // 空闲页面的数量 unsigned long pages_min, pages_low, pages_high; // 判断页面使用情况的阈值 int need_balance; free_area_t free_area[max_order]; // 描述空闲页面 wait_queue_head_t * wait_table; unsigned long wait_table_size; unsigned long wait_table_shift; struct pglist_data *zone_pgdat; // 指向该内存区 (zone) 属于的节点信息 struct page *zone_mem_map; // 指向本 zone 的第一个 page 结构 unsigned long zone_start_paddr; // 本 zone 开始的物理地址 unsigned long zone_start_mapnr; // 123

124 char *name; unsigned long size; } zone_t; 该结构中的主要成员含义如下 : 结构体成员 说明 Lock free_pages pages_min, pages_low, pages_high need_balance Free_area wait_table 旋转锁, 用于保护该 zone 该 zone 空闲页总数 Zone 的阈值, 每个 zone 有三个阈值, 称为 pages_low, pages_min 和 pages_high, 用于跟踪该 zone 的内存压力 pages_min 的页框数是由内存初始化 free_area_init_core 函数, 根据该 zone 内页框的比例计算的, 最小值为 20 页, 最大值一般为 255 页 当到达 pages_min 时, 分配器将采用同步方式进行 kswapd 的工作 ; 当空闲页的数目达到 pages_low 时,kswapd 被 buddy 分配器唤醒, 开始释放页 ; 当达到 pages_high 时,kswapd 将被唤醒, 此时 kswapd 不会考虑如何平衡该 zone, 直到有 pages_high 空闲页为止 一般情况下, pages_high 缺省值是 pages_min 的 3 倍 该标志告诉 kswapd 需要对该 zone 的页进行交换 空闲区域的位图, 用于 buddy 分配器, 注意 : uclinux 通过 zone_struct 数据结构中一组 " 空闲区 " (free area) 队列来管理包含在其中的物理页帧, 也即 free_area[max_order] 数组 为什么是通过一组 " 空闲区 "(free area) 队列, 而不是一个 " 空闲区 " (free area) 队列呢? 这是因为我们常常需要成 " 块 " 地分配物理地址连续的多个物理页帧 因此, 在 zone_struct 数据结构中即要有一个队列来保持一些离散 ( 连续长度为 1) 的物理页帧, 还要有其他多个队列来保持连续长度为 MAX_ORDER 的页帧块 常数 MAX_ORDER 值为 10, 因此在 Linux 中最大的连续页帧块可以达到 210=1024 个页帧, 即 4M 字节大小 等待释放该页进程的队列散列表, 这对 wait_on_page() 和 unlock_page() 是非常重要的 当进程都在一条队列上等待时, 将引起进程的抖动 124

125 zone_mem_map zone_start_paddr zone_start_mapnr Name Size 全局 mem_map 中该 zone 所引用的第一页 本 zone 开始的物理地址 用来表示该内存区所包含的物理页帧在 page 数组中的起始页帧序号 该 zone 的名字 如, DMA, Normal 或 HighMem Zone 的大小, 以页为单位 3 页结构体 struct page uclinux 把整个连续物理地址空间看作是一个由许多物理页帧 (Physical Page Frame) 组成的 " 页帧数组 ", 一般来说, 页帧的大小则为 4KB 在文件 linux/include/asm-arm/proc-armv/page.h 中 #define PAGE_SIZE (1UL << PAGE_SHIFT) #define PAGE_SHIFT 12 从上面定义可以看出每个物理页帧的起始物理地址的低 12 位必定全部为 0, 而它的高 20 位在右移 12 位后就得到该页帧的序号, 也就是该物理页帧在数组中的下标索引 uclinux 在头文件 include/linux/mm.h 中定义了数据结构 page 来描述一个物理页帧 : typedef struct page { struct list_head list; // 链接入各种 page list 的结构 } mem_map_t; uclinux 在系统初始化时根据系统物理内存的实际大小建立起 page 结构数组 mem_map, 每个物理页帧都在该数组中对应有一个成员来描述它, 物理页帧的序号就是它在 mem_map 数组中的下标索引 对于 UMA 来说,mem_map 描述了所有的物理内存页面 2.uClinux 启动函数分析大家知道,linux 内核启动首先执行 head-armv.s 中的汇编代码, 然后跳到 start_kernel 函数 ( 在 /linux/init/main.c 中定义 ) 开始漫长的内核启动过程, 其中内存初始化的代码就在 start_kernel 函数调用, 名称为 setup_arch 函数, 代码如下 : void init setup_arch(char **cmdline_p) { 1 if (meminfo.nr_banks == 0) { 125

126 2 meminfo.nr_banks = 1; 3 meminfo.bank[0].start = PAGE_OFFSET;//PHYS_OFFSET; 4 meminfo.bank[0].size = MEM_SIZE; 5 } 6 init_mm.start_code = (unsigned long) &_text; 7 init_mm.end_code = (unsigned long) &_etext; 8 #ifndef CONFIG_RAM_ATTACHED_ROMFS 9 init_mm.end_data = (unsigned long) &_edata; 10 init_mm.brk = (unsigned long) &_end; 11 #else 12 init_mm.end_data = (unsigned long) _ramstart; 13 init_mm.brk = (unsigned long) _ramstart; 14 #endif 15 bootmem_init(&meminfo); // 初始化 bootmem 16 paging_init(&meminfo, mdesc); // mem_map is set up here! } 解释 : 1-5 行 : 在这里, 我们设定 meminfo 结构, 用此结构描述我们的内存基本情况, 之后会用该变量初始化 contig_page_data 的数据,meminfo 结构定义如下 : /* * Memory map description */ #define NR_BANKS 8 struct meminfo { int nr_banks; unsigned long end; struct { unsigned long start; unsigned long size; int node; } bank[nr_banks]; }; 正常的内存管理应该由 pg_data_t zone mem_map 等数据结构来管理, 但此时这些结构还没有建立,bootmem 是在初始化的时候用来描述内存使用情况 6-14 行 : 126

127 每一个任务都有一个 mm_struct 结构管理任务内存空间,init_mm 是内核的 mm_struct, 其中设置成员变量 * mmap 指向自己, 意味着内核只有一个内存管 理结构, 设置 * pgd=swapper_pg_dir,swapper_pg_dir 是内核的页目录, 在 arm 体系结构有 16k, 所以 init_mm 定义了整个 kernel 的内存空间, 下面我们会碰到内核线程, 所有的内核线程都使用内核空间, 拥有和内核同样的访问权限 init_mm.start_code = (unsigned long) &_text;// 内核代码段开始 init_mm.end_code = (unsigned long) &_etext; // 内核代码段结束 init_mm.end_data = (unsigned long) &_edata; // 内核数据段开始 init_mm.brk = (unsigned long) &_end; // 内核数据段结束 15 行 : bootmem_init(&meminfo) 的作用就是要在内核的结尾部分划出一页做位图使用, 根据系统内存的大小映射整个 RAM 该函数定义在 Init.c (arch\armnommu\mm) 文件中, 具体内容如下 : /* * Initialise the bootmem allocator for all nodes. This is called * early during the architecture specific initialisation. */ void init bootmem_init(struct meminfo *mi) { struct node_info node_info[nr_nodes], *np = node_info; unsigned int bootmap_pages, bootmap_pfn, map_pg; int node, initrd_node; bootmap_pages = find_memend_and_nodes(mi, np); /* 为了在启动阶段描述内存使用情况我们需要一些内存空间, 这些空间叫做 bootmem, 实际上它就是一个 bit 位图, 每个 bit 代表一个 page frame 在建立起页管理机制之前用他来管理 page 此时 bootmap_pages 表明了 bootmem 所需要的 pages 的数目 */ bootmap_pfn = find_bootmap_pfn(0, mi, bootmap_pages); /* 通过这个函数,bootmap_pfn 设置了 bootmem 所在的初始页号 也就是说从 bootmap_pfn 到 bootmap_pfn + bootmap_pages 的内存页被用来描述初始化的时候的内存的使用情况 */ initrd_node = check_initrd(mi); map_pg = bootmap_pfn; /* 127

128 * Initialise the bootmem nodes. * * What we really want to do is: * * unmap_all_regions_except_kernel(); * for_each_node_in_reverse_order(node) { * map_node(node); * allocate_bootmem_map(node); * init_bootmem_node(node); * free_bootmem_node(node); * } * * but this is a 2.5-type change. For now, we just set * the nodes up in reverse order. * * (we could also do with rolling bootmem_init and paging_init * into one generic "memory_init" type function). */ np += numnodes - 1; /* numnodes 是一个全局变量, 对于 UMA 平台来说, 其值为 numnodes = 1; 所以下列 for 循环只循环了一次 */ for (node = numnodes - 1; node >= 0; node--, np--) { /* * If there are no pages in this node, ignore it. * Note that node 0 must always have some pages. */ if (np->end == 0) { if (node == 0) BUG(); } continue; /* * Initialise the bootmem allocator. */ init_bootmem_node(node_data(node), map_pg, np->start, np->end); /* 上面这个函数是 bootmem_init 函数的核心, 它的主要作用就是要设置 bootmem 分配器 它实际上是 init_bootmem_core 函数的外层包裹, 详细代码下面我们会详细分析它 */ 128

129 free_bootmem_node_bank(node, mi); /* 释放所有内存, 也就是把 bootmem 的区域全部设置为 0 */ map_pg += np->bootmap_pages; } /* * If this is node 0, we need to reserve some areas ASAP - * we may use bootmem on node 0 to setup the other nodes. */ if (node == 0) reserve_node_zero(bootmap_pfn, bootmap_pages); #ifdef CONFIG_BLK_DEV_INITRD /* * For arches that compile in the ramdisk data we do not need * to reserve the memory. Memory for the initrd device will be * reserved with the kernel data section. */ #if!defined(config_blk_dev_ramdisk_data) if (initrd_node >= 0) reserve_bootmem_node(node_data(initrd_node), pa(initrd_start), #endif #endif initrd_end - initrd_start); } if (map_pg!= bootmap_pfn + bootmap_pages) BUG(); /* * Called once to set up the allocator itself. */ static unsigned long init init_bootmem_core (pg_data_t *pgdat, unsigned long mapstart, unsigned long start, unsigned long end) { bootmem_data_t *bdata = pgdat->bdata; /* 首先初始化 bdata, 让它指向节点 pgdat 的 bdata 成员, 这样做方便使用 */ unsigned long mapsize = ((end - start)+7)/8; 129

130 /* 计算 bitmap 位图大小并把它存放在 mapsize 变量中,end-start 为整个物理页帧的数目, 因为一个字节有 8 个 bit, 也就是 8 个页帧, 所以要除以 8, 上式中加上 7 是为了取上限, 比如如果有 41 个物理页帧, 就必须分配 6 个字节 */ pgdat->node_next = pgdat_list; pgdat_list = pgdat; /* pgdat_list 是整个存储节点 (pglist_data) 链的头部, 因为现在就一个存储节点, 所以指向自身 */ mapsize = (mapsize + (sizeof(long) - 1UL)) & ~(sizeof(long) - 1UL); /* 因为是字操作, 所以要把 mapsize 修改为 4 的倍数 比如有 40 个物理页帧, 原来 mapseze 已经被设置为 5, 因此上式操作为 : mapsize=(5+(4)-1)&~(4-1)=8&~3= & , 因此最低两位被 mask 掉, mapsize 为 4 的倍数 */ bdata->node_bootmem_map = phys_to_virt(mapstart << PAGE_SHIFT); /* 节点成员 node_bootmem_map 指向内核的底部 */ bdata->node_boot_start = (start << PAGE_SHIFT); /* 节点成员 node_boot_start 指向物理内存的开始 */ bdata->node_low_pfn = end; /* 节点成员 node_low_pfn 为物理内存的底部 */ /* * Initially all pages are reserved - setup_arch() has to * register free RAM areas explicitly. */ memset(bdata->node_bootmem_map, 0xff, mapsize); /* 把位图所有位置为 1, 表明所有的页在开始都作为保留使用 */ return mapsize; } 16 行 : 在内存初始化过程中,paging_init 是一个非常重要的函数, 它创建内核页表, 映射所有物理内存和 io 空间, 对于不同的处理器, 这个函数差别很大 /* * paging_init() sets up the page tables, initialises the zone memory * maps, and sets up the zero page, bad page and bad page tables. */ void init paging_init(struct meminfo *mi, struct machine_desc *mdesc) { void *zero_page; 130

131 int node; memcpy(&meminfo, mi, sizeof(meminfo)); /* * allocate the zero page. Note that we count on this going ok. */ zero_page = alloc_bootmem_low_pages(page_size); /* 分配一个页面的内存, 很显然, 我们刚才做的工作起了作用, 我们会在表示该页面的那个 bootmem 中的 bit 设置值为 1, 以此表示该页面已经被分配出去了 */ #ifndef CONFIG_UCLINUX { 这里面有很大一段代码, 主要是针对有 MMU 的 linux, 故在这里就进行分析 } #else { unsigned long zone_size[max_nr_zones] = {0,0,0}; zone_size[zone_dma] = 0; zone_size[zone_normal] = (END_MEM - PAGE_OFFSET) >> PAGE_SHIFT; NULL); free_area_init_node(0, NULL, NULL, zone_size, PAGE_OFFSET, /* 此函数初始化内存 zone, 建立映射 */ } #endif } /* * finish off the bad pages once * the mem_map is initialised */ memzero(zero_page, PAGE_SIZE); empty_zero_page = virt_to_page(zero_page); flush_dcache_page(empty_zero_page); void init free_area_init_node(int nid, pg_data_t *pgdat, struct pa ge *pmap, unsigned long *zones_size, unsigned long zone_start_paddr, unsigned long *zholes_size) { 131

132 } free_area_init_core(0, &contig_page_data, &mem_map, zones_size, zone_start_paddr, zholes_size, pmap); /******************************************************************* **/ /* * Set up the zone data structures: * - mark all pages reserved * - mark all memory queues empty * - clear the memory bitmaps * * static in this version because I haven't though it out yet ;-) */ void init free_area_init_core(int nid, pg_data_t *pgdat, struct page **gmap, unsigned long *zones_size, unsigned long zone_start_paddr, unsigned long *zholes_size, struct page *lmem_map) { unsigned long i, j; unsigned long map_size; unsigned long totalpages, offset, realtotalpages; const unsigned long zone_required_alignment = 1UL << (MAX_ORDER-1); if (zone_start_paddr & ~PAGE_MASK) BUG(); /* 检查 zone 是否的起始位置是否是页对齐, 下面的 for 循环就是要计算一个存 储节点中页的总数目 */ totalpages = 0; for (i = 0; i < MAX_NR_ZONES; i++) { } unsigned long size = zones_size[i]; totalpages += size; realtotalpages = totalpages; if (zholes_size) for (i = 0; i < MAX_NR_ZONES; i++) realtotalpages -= zholes_size[i]; 132

133 printk("on node %d totalpages: %lu\n", nid, realtotalpages); INIT_LIST_HEAD(&active_list); INIT_LIST_HEAD(&inactive_list); */ /* * Some architectures (with lots of mem and discontinous memory * maps) have to search for a good mem_map area: * For discontigmem, the conceptual mem map array starts from * PAGE_OFFSET, we need to align the actual array onto a mem map * boundary, so that MAP_NR works. */ map_size = (totalpages + 1)*sizeof(struct page); if (lmem_map == (struct page *)0) { lmem_map = (struct page *) alloc_bootmem_node(pgdat, map_size); lmem_map = (struct page *)(PAGE_OFFSET + MAP_ALIGN((unsigned long)lmem_map - PAGE_OFFSET)); } /* 上面代码分配内存映射需要的空间, 然后下面代码就初始化存储节点的成员 *gmap = pgdat->node_mem_map = lmem_map; pgdat->node_size = totalpages; pgdat->node_start_paddr = zone_start_paddr; pgdat->node_start_mapnr = (lmem_map - mem_map); pgdat->nr_zones = 0; /* * as we free pages we mark the first page that is usable */ bit_map_size = totalpages; bit_map = (unsigned char *) alloc_bootmem_node(pgdat, LONG_ALIGN(bit_map_size / 8)); memset(bit_map, 0, LONG_ALIGN(bit_map_size / 8)); #ifdef CONFIG_UNCACHED_MEM bit_map_size_common = bit_map_size - bit_map_size_special; #endif 133

134 /* * Initially all pages are reserved - free ones are freed * up by free_all_bootmem() once the early boot process is * done. */ first_usable_page = totalpages; offset = lmem_map - mem_map; /* 下面这个大循环实际上就是去初始化 zone 成员 */ for (j = 0; j < MAX_NR_ZONES; j++) { zone_t *zone = pgdat->node_zones + j; unsigned long mask; unsigned long size, realsize; int idx; zone_table[nid * MAX_NR_ZONES + j] = zone; realsize = size = zones_size[j]; if (zholes_size) realsize -= zholes_size[j]; printk("zone(%lu): %lu pages.\n", j, size); zone->size = size; zone->realsize = realsize; zone->name = zone_names[j]; zone->lock = SPIN_LOCK_UNLOCKED; zone->zone_pgdat = pgdat; zone->free_pages = 0; zone->need_balance = 0; zone->nr_active_pages = zone->nr_inactive_pages = 0; if (!size) continue; /* * The per-page waitqueue mechanism uses hashed waitqueues * per zone. 134

135 */ zone->wait_table_size = wait_table_size(size); zone->wait_table_shift = BITS_PER_LONG - wait_table_bits(zone->wait_table_size); zone->wait_table = (wait_queue_head_t *) alloc_bootmem_node(pgdat, zone->wait_table_size * sizeof(wait_queue_head_t)); for(i = 0; i < zone->wait_table_size; ++i) init_waitqueue_head(zone->wait_table + i); pgdat->nr_zones = j+1; */ zone->watermarks[j].min = 0; /* agressive values */ zone->watermarks[j].low = 0; zone->watermarks[j].high = realsize; /* now set the watermarks of the lower zones in the "j" classzone for (idx = j-1; idx >= 0; idx--) { zone_t * lower_zone = pgdat->node_zones + idx; unsigned long lower_zone_reserve; if (!lower_zone->size) continue; mask = lower_zone->watermarks[idx].min; lower_zone->watermarks[j].min = 0; lower_zone->watermarks[j].low = 0; lower_zone->watermarks[j].high = realsize; /* now the brainer part */ lower_zone_reserve = realsize / lower_zone_reserve_ratio[idx]; lower_zone->watermarks[j].min += lower_zone_reserve; lower_zone->watermarks[j].low += lower_zone_reserve; lower_zone->watermarks[j].high += lower_zone_reserve; } realsize += lower_zone->realsize; zone->zone_mem_map = mem_map + offset; 135

136 zone->zone_start_mapnr = offset; zone->zone_start_paddr = zone_start_paddr; if ((zone_start_paddr >> PAGE_SHIFT) & (zone_required_alignment-1)) printk("bug: wrong zone alignment, it will crash\n"); /* * Initially all pages are reserved - free ones are freed * up by free_all_bootmem() once the early boot process is * done. Non-atomic initialization, single-pass. */ for (i = 0; i < size; i++) { struct page *page = mem_map + offset + i; } set_page_zone(page, nid * MAX_NR_ZONES + j); set_page_count(page, 0); SetPageReserved(page); set_bit(page-mem_map, bit_map); INIT_LIST_HEAD(&page->list); if (j!= ZONE_HIGHMEM) set_page_address(page, va(zone_start_paddr)); zone_start_paddr += PAGE_SIZE; } offset += size; build_zonelists(pgdat); } 4. 可执行程序加载 在 Linux 中, 由于使用虚拟内存管理技术, 它最重要的特点之一就是开发人员不需要关心应用程序放到具体的哪块内存区, 因为即使不同应用程序之间使用的地址相同, 那也不过是虚拟地址相同, 经过 MMU 后, 会映射到不同的物理地址上 但由于 uclinux 不具备 MMU 硬件支持, 那么每一个应用程序使用的地址实际上都是物理地址, 因此开发人员必须关注应用程序的加载问题 标准 Linux 系统通常使用 ELF 可执行文件格式, 也就是说 gcc 编译器把一个应用程序编译成 ELF 格式的可执行文件,elf 格式有很大的文件头, 因此 uclinux 系统虽然支持 ELF 格式的可执行文件, 但是 FLAT 可执行文件格式更适合于它 在讲述 FLAT 可执行文件格式之前, 我们必须要先讲一下 uclinux 平台下的 reloc 段机制 (1)reloc 段机制 136

137 在 Linux 系统中, 程序连接时, 连接器 ld 会起到非常重要的作用, 而连接器由连接脚本文件所控制, 它决定将程序的各个段加载到内存的具体某一个位置 可执行程序被加载时, 它的加载地址 ( 即运行空间 ) 由内核的页面分配机制来决定, 由于标准 Linux 系统中, 用户程序使用的是虚拟地址, 它可以不去关心它的实际加载地址 ( 即运行时的地址 ) 可是由于 uclinux 没有 MMU 机制, 它被连接时的地址就是一个真实的物理地址, 在加载时也必须被加载到这个位置, 否则就会出错 例如 : jsr app_start; 这一条指令采用直接寻址, 跳转到 app_start 地址处执行, 连接程序将在编译完成是计算出 app_start 的实际地址 ( 设若实际地址为 0x10000), 这个实际地址是根据 ld 文件计算出来 ( 因为连接器假定该程序将被加载到由 ld 文件指明的内存空间 ) 但实际上由于内存分配的关系, 操作系统在加载时无法保证程序将按 ld 文件加载 这时如果程序仍然跳转到绝对地址 0x10000 处执行, 通常情况这是不正确的 因此必须采用 reloc 段机制的模式进行修改 解决办法就是增加一个变量在程序加载时动态修正 app_start 的实际地址 该变量的类型如下 : struct flat_reloc{ signed long offset:30;// 表明绝对地址的变量在数据段中离开始位置的偏移量 unsigned long type:2;// 表明该变量指示的是代码地址还是数据地址 } 设若使用变量 addr 表示这个存储空间 则以上这句程序将改为 : movl addr, a0; jsr (a0); 增加的变量 addr 将在数据段中占用一个 4 字节的空间, 连接器将 app_start 的绝对地址存储到该变量 在可执行文件加载时, 可执行文件加载器根据程序将要加载的内存空间计算出 app_start 在内存中的实际位置, 写入 addr 变量 系统在实际处理是不需要知道这个变量的确切存储位置 ( 也不可能知道 ), 系统只要对整个 reloc 段进行处理就可以了 (reloc 段有标识, 系统可以读出来 ) 处理很简单只需要对 reloc 段中存储的值统一加上一个偏置 ( 如果加载的空间比预想的要靠前, 实际上是减去一个偏移量 ) 偏置由实际的物理地址起始值同 ld 文件指定的地址起始值相减计算出 (2)FLAT 可执行文件格式 FLAT 可执行文件格式如图 4-13 所示 137

138 reloc 段 代码段 数据段 BSS 段 堆栈段 图 4-13 FLAT 可执行文件格式 从图 4-13 可以看出, 可执行文件头就是前面描述的 reloc 段 (3) 可执行程序加载流程假如有个 c 程序 : int main(int argc, ch ar **argv[]) { } printf("hello world!\n"); return 0; 这是一个最简单不过的程序了, 一般一个 C 语言程序, 都从 main 开始执行 那么,main 函数是不是与其他函数有所区别, 地位有些特殊呢? 不是的 main 函数和其他函数地位一样 其实, 我们完全可以做到让一个 c 程 序从任何地方开始执行 比如 linux, 它就没有 main 函数, 大家都知道, 系统执行过启动的一段汇编后, 就会跳转到位于 init/main.c 中的 start_kernel 中开始执行 那么为什么用户程序都要从 main 函数执行呢? 这就是用户 C 库的原因 一般用 户用 c 语言开发时会调用一些库函数, 编译成 obj 文件后, 在链接过程中把库函数的二进制代码链接进入程序, 最后形成二进制可执行文件 链接过程中, 链接器会在用户程序前插入一些初始化的代码 uclinux 下是在 crt0.s 中 ( 我移植的是 uclibc 库 ) 不管什么平台下什么形式的 crt0.s, 这个文件最后几行代码中肯定有一个 jmp( 或者 call 或 br 等转移指令 ) main( 或 uclibc_main) 这就是为什么应用程序都从 main 开始执行 如果把这个跳转标号改成任意一个标号, 比如 foo 而你的程 序里面既有 main, 又有 foo, 则这种情况下, 程序就先从 foo 开始执行 所以,main 函数和其他函数一样, 并没有特殊地位 在 uclinux 的 shell 下执行一个文件 foo x y,foo 是程序名,x, y 是参数 执行这个程序的时候, 操作系统会调用 do_execve (char *filename, char**argv, char**envp, struct pt_regs *regs), 这个操作会根据文件路径打开文件, 装入内存,argv 就是放到命令行参数,envp 是环境变量 参数 在装入文件时, 系统会根据不同的文件格式调用不同文件装入的 handler, 如果是 flat 格式, 就会调用 load_flat_binary(),load_flat_binary() 通过调用 138

139 do_ load_flat_binary() 函数完成加载功能 嵌入式 Linux 文件系统 在嵌入式系统开发环境中, 由于成本等因素的限制, 一般外部存储设备的容量 都比较小, 因此 linux 中原有的一些文件系统并不一定适合于嵌入式环境下使用 在 uclinux 系统下, 比较常用的文件系统有 ROMFS 文件系统 RAMDISK 文件系统 JFFS 文件系统和 YAFFS 文件系统等, 下面就详细介绍这些文件系统 文件系统概述 众所周知, 文件系统是 Unix 系统最基本的资源 最初的 Unix 系统一般都只支 持一种单一类型的文件系统, 在这种情况下, 文件系统的结构深入到整个系统内核中 而现在的系统大多都在系统内核和文件系统之间提供一个标准的接口, 这样不同文件结构之间的数据可以十分方便地交换 系统内核和文件系统之间的标准接口叫做虚拟文件系统 (VFS,Virtual file system) 这样, 文件系统的代码就分成了两部分 : 上层用于处理系统内核的各种表格和数据结构 ; 而下层用来实现文件系统本身的函数, 并通过 VFS 来调用 这些函数主要包括 : 管理缓冲区 (buffer.c) 响应系统调用 fcntl() 和 ioctl()(fcntl.c and ioctl.c) 将管道和文件输入 / 输出映射到索引节点和缓冲区 (fifo.c, pipe.c) 锁定和不锁定文件和记录 (locks.c) 映射名字到索引节点 (namei.c, open.c) 实现 select( ) 函数 (select.c) 提供各种信息 (stat.c) 挂接和卸载文件系统 (super.c) 调用可执行代码和转存核心 (exec.c) 装入各种二进制格式 (bin_fmt*.c) VFS 接口则由一系列相对高级的操作组成, 这些操作由和文件系统无关的代码调用, 并且由不同的文件系统执行 其中最主要的结构有 inode_operations 和 file_operations file_system_type 是系统内核中指向真正文件系统的结构 每挂接一次文件系统, 都将使用 file_system_type 组成的数组 file_system_type 组成的数组嵌入到了 fs/filesystems.c 中 相关文件系统的 read_super 函数负责填充 super_block 结构 下图 4-14 显示了 Linux 核心的虚拟文件系统和它的真实的文件系统之间的关系 虚拟文件系统必须管理任何时间安装的所有的不同的文件系统 为此它管理描述整个文件系统 ( 虚拟 ) 和各个真实的 安装的文件系统的数据结构 139

140 用户空间 系统空间 应用程序 VFS 文件系统操作的系统调用 界面, 包括 read() write() 等 通过 file 结构中的 f_op 指针 实现的 文件系统总线 EXT2 ROMF FAT16 设备文件 图 4-14 VFS 文件系统逻辑图 当每一个文件系统初始化的时候, 它自身向 VFS 登记 这发生在系统启动操作系统初始化自身的时候 真实的文件系统自身建立在内核中或者是作为可加载的模块 文件系统模块在系统需要的时候加载, 所以, 如果 VFAT 文件系统用核心模块 的方式实现, 那么它只有在一个 VFAT 文件系统安装的时候才加载 当一个块设备文件系统安装的时候,( 包括 root 文件系统 ), VFS 必须读取它的超级块 每一个文件系统类型的超级块的读取例程必须找出这个文件系统的拓扑结构, 并把这些信 息映射到一个 VFS 超级块的数据结构上 VFS 保存系统中安装的文件系统的列表和它们的 VFS 超级块列表 每一个 VFS 超级块包括了文件系统的信息和完成特定功能的例程的指针 例如, 表示一个安装的 EXT2 文件系统的超级块包括一个 EXT2 相关的 inode 的读取例程的指针 这个 EXT2 inode 读取例程, 象所有的和文件系统相关的 inode 读取例程一样, 填充 VFS inode 的域 每一个 VFS 超级块包括文件系统中的一个 VFS inode 的指针 对于 root 文件系统, 这是表示 / 目录 的 inode 这种信息映射对于 EXT2 文件系统相当高效, 但是对于其他文件系统相对效率较低 ROMFS 文件系统 1.ROMFS 文件系统概述 ROMFS 文件系统是一种只读文件系统, 最初用于 linux 中 它是一个基于块的 文件系统, 比较适合用于硬盘 CDROM 等块设备 创建 ROMFS 文件系统需要使用专门的开发工具 (genromfs) romfs 文件系统相对于一般的 ext2 文件系统要求更少的空间 空间的节约来自 于两个方面, 首先内核支持 romfs 文件系统比支持 ext2 文件系统需要更少的代码, 其次 romfs 文件系统相对简单, 在建立文件系统超级块 (superblock) 需要更少的存储空间 2.ROMSFS 文件系统结构 ROMFS 通常运行在块设备上, 它的底层结构是非常简单的 表 4-1 显示了 ROMFS 映像文件头部结构 140

141 表 4-1 ROMFS 映像文件头部结构 偏移量 内容 0~3 R O 4~7 1 F S 8~11 字节总数 12~15 校验和 16 卷名称 xx 文件头 M 前 8 个字节存放的内容的 ASCII 码为 -ROMFS-, 它表明该映像文件系统类型 为 ROMFS 文件系统 从文件头开始的第三个字 ( 即第 8~11 字节 ) 存放的是该映像文件的字节总数, 第四个字中存放的从文件头开始的 512B 的校验码, 后面放的就是各个文件的文件头 ( 每个文件头都是 16B 的整数倍 ), 文件头部结构如表 4-2 所示 表 4-2 ROMFS 中的文件头结构. 偏移量 0~3 4~7 8~11 12~15 16 xx 内容 距离下个文件头偏移量 Spec.info 该文件的大小 校验和 文件名 文件数据 因为文件头总是 16B 的整数倍, 因此最低的四个 bit 肯定为 0, 因此系统把这四个 bit 拿做它用 : 比如指示文件的类型, 以及是否可执行等信息, 其中 bit0~bit2 取值含义如下表 4-3 所示 表 4-3 值文件类型 Spec.info 含义 0 硬链接此时 Spec.info 域的内容为用于链接的目标文件 1 目录 Spec.info 域的内容为第一个文件的文件头 2 普通文件 Spec.info 域的内容无效 3 符号链接 Spec.info 域的内容无效 4 块设备 Spec.info 域的内容为 16 位主设备号和 16 位从设备号 5 字符设备 Spec.info 域的内容无效 6 socket 套结字 Spec.info 域的内容无效 7 fifo 管道文件 Spec.info 域的内容无效 3.ROMFS 文件系统使用 141

142 文件系统实际上是应用程序的一个集合, 系统开发人员可以白手起家来构造一个文件系统, 也可以基于已有的软件包来完成 前者的做法可以参考后面的章节, 现在我们基于 uclinux 软件包来生成 ROMFS 文件系统 第一步 : 从 http: // 上免费下载 uclinux-dist 的源码包 (uclinux-dist tar.gz) 到任意目录下 ( 比如 /home) 这个源码包里包含了 uclinux uclibc 和已经移植到 uclinux 下的用户应用程序 第二步 :tar zxvf uclinux-dist tar.gz, 当 tar 程序运行完毕后, 在 /home 目录下会有一个 /home/uclinux-dist 的新目录, 这个目录就是 uclinux 的源码根目录, 里 面有进行 uclinux 开发的所有的源代码 第三步 : 进入 uclinux-dist 目录, 执行 makemenuconfig, 则出现如下主选单 : 如上图选择后, 选择 Exit, 再 Exit, 则提示 : 选择 Yes, 则退出当前的菜单, 进入下面的应用配置主菜单 这里用户可以任意添加 / 删除自己的应用程序集 如果用户不懂的话, 最后不要乱选, 因为有的选项并不能 保证一定能编译通过, 使用默认选项就可以了 142

143 第四步 : 内核编译 内核经过裁减后, 就可以采用下列命令来进行编译 Make dep Make lib_only Make user_only Make romfs genromfs v V ROMdisk f romfs.img d romfs 第五步 : 现在 ROMFS 文件系统就制作完成了, 我们可以直接使用 mount 命令将其挂接到合适的目录下 JFFS 与 JFFS2 文件系统 1.flash 设备的特点对于嵌入式系统,flash 是很常见的一种设备, 而大部分的嵌入式系统都是把文 件系统建立在 flash 之上, 由于对 flash 操作的特殊性, 使得在 flash 上的文件系统和普通磁盘上的文件系统有很大的差别, 对 flash 操作的特殊性包括 : 不能对单个字节进行擦除, 最小的擦写单位是一个 block, 有时候也称为一 个扇区 典型的一个 block 的大小是 64k 不同的 flash 会有不同, 具体参考 flash 芯片的规范 写操作只能对一个原来是空 ( 也就是该地址的内容是全 f) 的位置操作, 如 果该位置非空, 写操作不起作用, 也就是说如果要改写一个原来已经有内容的空间, 只能是读出该 sector 到 ram, 在 ram 中改写, 然后写整个 sector 擦写次数限制, 所有的 flash 设备在写的时候总是先擦除后写入 但是 flash 设备擦写次数有限, 因此如何保证 flash 每一块上都能够平均被使用到是一个非常重要的课题 由于这些特殊性, 所以在 flash 这样的设备上建立文件也有自己独特的特点, 下 143

144 面我们就以 jffs 为例进行分析 2.JFFS 概述 JFFS 是日志文件系统, 由瑞典 Axis 公司在 2000 年设计完成 JFFS 文件系统专用于嵌入式 flash 设备, 它根据 flash 设备的特点进行了很多修改, 目的是使该文件系统在 flash 设备上使用时可以获得高的效率 2001 年,Red Hat 公司在此基础上推出了 JFFS2 文件系统 和 JFFS 文件系统一样,JFFS2 文件系统也是针对嵌入式系统中的 flash 存储器进行设计的 3.JFFS 版本 1 JFFS 文件系统迎合了 flash 技术的特点而诞生的, 因为随着嵌入式系统和低功耗设备的普及, 我们必须确保这些设备上能为用户提供可靠安全的操作 (1) 存储格式 最初的 JFFS 是一种日志结构的文件系统, 储存数据信息的节点被顺序存放在 flash 设备上, 处理时也是严格线性进行的 在 jffs 中, 所有的文件和目录是一样对待的, 都是用一个 jffs_raw_inode 来表示, 整个 flash 上就是由一个一个的 raw inode 排列组成, 一个目录只有一个 raw inode, 对于文件则是由一个或多个 raw inode 组成 struct jffs_raw_inode 结构体定义如下 : struct jffs_raw_inode jl { u32 magic; /* A constant magic number. */ w$g} u32 ino; /* Inode number. */ "X>a u32 pino; /* Parent's inode number. */ Wk u32 version; /* Version number. */ "GM u32 mode; /* The file's type or mode. */ rw u16 uid; /* The file's owner. */ e3#. u16 gid; /* The file's group. */ ; u32 atime; /* Last access time. */ k u32 mtime; /* Last modification time. */ =9O0W u32 ctime; /* Creation time. */ ~4> u32 offset; /* Where to begin to write. */ ` u32 dsize; /* Size of the node's data. */ j~#pxc u32 rsize; /* How much are going to be replaced? */ ls0 u8 nsize; /* Name length. */ 1Ka.fa u8 nlink; /* Number of links. */ " u8 spare : 6; /* For future use. */ ak;xiq u8 rename : 1; /* Rename to a name of an already existing file? */ (G d-; u8 deleted : 1; /* Has this file been deleted? */ D u8 accurate; /* The inode is obsolete if accurate == 0. */ &'r u32 dchksum; /* Checksum for the data. */ ){ }' u16 nchksum; /* Checksum for the name. */ >_ if' 144

145 ); u16 chksum; /* Checksum for the raw inode. */ _t 从结构体可以看出, 每个节点包含简单的头部 辅助信息以及存储的数据 其中存储的辅助信息不仅包括通常所记录的文件 inode 信息, 也包含了它所关联的文件的名字等 由于在 JFFS 中, 大的文件需要分为很多节点存放, 所以除了存放数据 之外, 还要额外保存这段数据在文件中的偏移量 设备节点和符号链接等一些特殊的文件只需要存储很少量的文件信息, 所以这种文件往往占用很小的空间, 通常不会超过一个节点 删除文件的时候只是在辅助信息存放的地方设置一个已删除标记, 所有关于被删除文件的 flash 块都会加上这个已删除标记, 在使用该文件的文件句柄释放以后, 这些节点就成为废弃节点, 等待在回收空间的时候被重新分配利用 (2) 操作 JFFSv1 实现的操作比较简单, 有下列几种类型 1 挂接 (mount): 挂接 JFFS 文件系统时, 会扫描整个存储设备, 每个存储块读 取一次, 然后根据所有节点中存储的信息来生成一个文件系统的目录树, 同时生成了一个文件在 flash 中物理存储位置的对应表, 用来进行文件的寻址操作 2 读取 (read):jffsv1 在 mount 时存储所有文件信息, 文件目录结构可以很方便的根据这些信息进行操作 读取文件内容的时候, 同样可以根据这些信息轻松的将指定的内容读入到缓冲区 3 属性改变 (metadata changes): 当改变文件属性时,JFFSv1 处理的非常简单, 它只需要将一个记录新的信息的节点写到 flash 已利用存储空间的末尾, 然后把原有的节点标记为废弃节点就可以了 4 文件写 (write): 文件写操作和属性改变操作类似, 它也是重新将一个记录新的信息的节点写到 flash 已利用存储空间的末尾, 然后把原有的节点标记为废弃节点, 唯一不同的是它还有数据要写入 (3) 垃圾回收 (Garbage Collection) 从上面对 JFFS 操作的介绍可以看出, 它不会对废弃的空间进行回收, 那么就必然会造成一个结果 :flash 空间用光, 这时文件系统就对废弃的空间进行回收利用了 在进行空间回收时, 系统自动从所有存储块的第一个进行分析, 将还在使用的节点进行合并, 并把废弃的节点回收, 这样在 flash 设备的末尾就合并出一块废弃的存储块, 这样就可以将它擦除信息, 重新被用来使用 (4) 缺点 1 垃圾回收时,JFFSv1 并没有优化, 它按照顺序, 从第一个数据块开始腾出空间, 如果第一块写满了有效数据, 它也会将这些数据后移, 腾出第一块做为空闲卡 在极端的情况下, 效率非常低 当然这样做也有好处, 就是保证了 flash 设备上每一块都会得到相同的擦写次数, 提高了 flash 设备的使用寿命 2 不支持压缩 3 不支持硬链接, 每一个存储块都保存了对应的文件名, 这样就是很常用的改名操作, 也需要增加一个新的存储块 145

146 4.JFFS2 JFFS2 是 JFFS 的后继者, 由 Red Hat 公司重新改写而成 它的功能就是管理在 MTD 设备上实现的日志型文件系统 与其它的存储设备存储方案相比,JFFS2 并不提供让传统文件系统也可以使用此类设备的转换层 它只能在直接在 MTD 设备上实现日志结构的文件系统 JFFS2 会在安装的时候, 扫描 MTD 设备的日志内容, 并 在 RAM 中重新建立文件系统结构本身 相对于 JFFS 文件系统,JFFS2 文件系统改进如下 : JFFS2 的节点头部中增加了新的信息, 包括节点的总长度, 节点类型和 CRC 校验码 克服 JFFSv1 垃圾回收方式的缺陷 在 JFFS2 中, 所有的存储节点都不可以跨越 flash 的块界限 在回收的时候, 按照 flash 的各个块为单位, 进行选 择, 将最合适的块腾出来, 擦除之后做为新的空闲块 这样, 虽然不能保证 flash 中每一块都有相同的擦除次数, 但可以提高整个系统的效率 JFFS2 中节点类型有 3 种, 分别用于表示擦除块的标记 普通文件和目录 文件系统的信息并不完全保存在内存中, 可以很快取得的数据并不保存在内存中, 这样可以提高内存得利用率 增加了对硬链接得支持 5.JFFS/JFFS2 的使用 JFFS/JFFS2 文件系统必须建立在 MTD 之上, 典型结构如图 4-15 所示 JFFS/JFFS2 MTD Flash 硬件驱动 图 4-15 JFFS/JFFS2 文件系统结构图因此从软件层次上来说,JFFS2 的建立包括 3 个层次 : JFFS2: 文件系统驱动 MTD: 存储技术设备驱动 Memory Technology Devices driver 特定的 flash 硬件驱动其中 MTD 驱动提供给 JFFS2 一个挂载点 通用 NAND 驱动提供必要的识别 读 写和擦除功能 与特定的硬件相关的功能函数则由底层的硬件驱动提供 (1) 配置内核 # cd /home/uclinux-dist # make menuconfig 选中内核设置项和用户选项 : [*] Customize Kernel Settings [*] Customize Vendor/User Settings 按两次 ESC 键, 首先进入内核配置 1 选择支持可载入的模块 选中 Loadable mudule support, 敲空格进入 选中第 1 项 : 146

147 [*] Enable loadable module support 按 ESC 退出 2 对 MTD 进行配置选中 Memory Technology Device (MTD) 如下 : <*> Memory Technology Device (MTD) support [*] Debugging (0) Debugging verbosity (0= quiet, 3=noisy) <*> MTD partitioning support --- User Modules And Translation Layers, 敲空格进入 需要选中的项目 <*> Direct char device access to MTD devices <*> Caching block device access to MTD devices 选中 NAND Flash Device Drivers -, 敲空格进入 需要选中的项目如下 : <*> NAND Device Support <*> NAND Flash device on ARMSYS board 敲两次 ESC 退出 MTD 配置 3 对文件系统进行配置选中 File system -, 敲空格进入 需要选中的项目如下 : <*> Journalling Flash File System v2 (JFFS2) support (0) JFFS2 debugging verbosity (0 = quiet, 2 = noisy ) 敲两次 ESC, 敲回车, 退出内核配置 然后进入用户应用程序配置界面, 按如下步骤操作 : 1 对 MTD 工具进行配置选中 Flash Tools -, 敲空格键进入 需要选中的项目如下 : [*] mtd-utils [*] erase [*] eraseall [*] mkfs.jffs2 敲 ESC 键退出 2 对 BusyBox 进行配置选中 BusyBox -, 敲空格键进入 需要选中的项目如下 : [*] mount [*] umount 147

148 [*] vi 敲 2 次 ESC 键, 并回车保存 到此为之, 所需的配置选项就全部设好了 下面就开始编译内核, 产生内核映像和文件系统映像, 将它们下载到目标板后, 就可以把 uclinux 启动起来, 界面如下 (2) 创建和拷贝 JFFS2 映像文件在目标板上运行以下的命令行 : /dev> cd /var/tmp /var/tmp> mkdir jffs2 /var/tmp> c /var/tmp/jffs2> vi file1 Hello, this is JFFS2 on Nandflash. /var/tmp/jffs2> cat file1 Hello, this is JFFS2 on Nandflash. /var/tmp/jffs2> mkdir folder1 /var/tmp/jffs2> mkdir folder2 /var/tmp/jffs2> mkdir folder3 /var/tmp/jffs2> cd.. /var/tmp> mkfs.jffs2 -d jffs2 -o jffs2.img /var/tmp> ls jffs2 jffs2.img /var/tmp>eraseall /dev/mtd0 /var/tmp> cp jffs2.img /dev/mtd0 MTD_open MTD_write MTD_close /var/tmp> 同样可以使用 /dev/mtd1, 那么在下面的 Mount 中要使用 mtdblock1 (3) 挂接 JFFS2 分区在目标板上运行以下命令行 : /var/tmp> mount -t jffs2 /dev/mtdblock0 /mnt 我们还可以看看现在 mnt 下面的内容 : /var/tmp> cd /mnt /mnt> ls file1 folder1 folder2 148

149 folder3 /mnt> cat file1 Hello, this is JFFS2 on Nandflash. /mnt> 这样我们就可以在 /mnt 下操作 Nandflash 中的文件内容 例如 : /mnt> rm file1 /mnt> vi file2 This is ARMSYS board. /mnt> ls file2 folder1 folder2 folder3 /mnt> (4) 卸载 JFFS2 分区 在开发板上运行以下命令行 : /> umount /mnt /> cd mnt /mnt> ls /mnt> 嵌入式根文件系统内容 嵌入式 Linux 内核在系统启动期间进行得最后操作之一就是安装根文件系统, 根文件系统是所有 Unix 系统不可或缺得组件, 每一个 Linux 系统中必须有一个根文件系统, 当然根文件系统中还可以内嵌若干个其它文件系统 ) 本节将会首先探讨根文件系统的基本结构, 然后探讨如何构建根文件系统部件 1. 根文件系统的基本结构我们知道,Linux 系统中文件系统是按照标准得目录结构来存放所有文件的 这些目录结构是根据 FHS(Filesystem Hierarchy Standard) 的规则来设定得, 当然你可以 不必遵循这些规则 表 3-2 提供了根文件系统顶层目录得完整清单以及 FHS 指定的内容 目录 bin boot dev etc 内容 必要的用户命令引导加载程序使用的静态文件设备文件和其它特殊文件系统配置文件, 包括启动文件 表 3-2 根文件系统顶层目录 149

150 home lib mnt opt proc root sbin tmp usr var 用户主目录, 包括供服务帐号所使用的主目录, 例如 FTP 必要的链接库, 例如 C 链接库 内核模块 安装点, 用于暂时安装文件系统附加的软件套件用来提供内核与进程信息的虚拟文件系统 root 用户的主目录必要的系统管理员命令暂时性的文件 在第二层包含对大多数用户都有用得大量应用程序和文件, 包括 X 服务器监控程序和工具程序所存放得可变数据 2. 构建嵌入式根文件系统为了建立根文件系统, 我们必须首先建立上述目录 (1) 首先为根文件系统建一个目录叫做 rootfs, 然后进入 rootfs 目录内 # mkdir rootfs # cd rootfs 然后为 root filesystem 建立一些标准的目录 # mkdir dev etc etc/rc.d lib bin proc mnt tmp var usr sbin # chmod 755 dev etc etc/rc.d lib bin proc mnt tmp var usr # chmod 555 proc 如果可以的话, 继续在一些顶层目录下建立二级目录, 比如可以继续建立 usr 的目录结构 : #mkdir usr/bin usr/lib usr/sbin 其它类似, 这里就不一一示范了 (2) 然后进入 /dev 目录下建立根文件系统必须的一些设备文件 按照 Unix 系统的传统, 在 Linux 中所有的对象 ( 包括设备 ) 都可视为文件 在 Linux 根文件系统中, 所有设备文件 ( 或称作设备节点 ) 都放在 /dev 目录下 标准 Linux 环境下,/dev 目录下设备节点条目非常多, 但因为嵌入式 Linux 系统是定制系统, 所以目标板 /dev 目录里并不需要填入那么多条目 事实上, 只需建立让系统正常操作得必要条目就可以了 内核源码树得 Documentation/devices.txt 文档就是静态设备主要和次要编号的正式信息来源 如果你无法确定某个设备的名称或编号时可以查阅此文档 建立一般终端机设备 # mknod tty c 5 0 # mkdir console c 5 1 # chmod 666 tty console 建立 VGA Display 虚拟终端机设备 150

151 # mknod tty0 c 4 0 # chmod 666 tty0 建立 null 设备 # mknod null c 1 3 # chmod 666 null 建立串口设备 # mknod ttys0 c 4 64 (3) 编辑有关的 shell script 首先进入到 /floppy-linux/etc/ 这个目录下编辑 inittab,rc.sysinit, fstab 这三个文件, 内容分别如下 : inittab ::sysinit:/etc/rc.d/rc.sysinit ::askfirst:/bin/sh rc.sysinit #!/bin/sh mount a fstab proc /proc proc defaults 0 0 然后修改 inittab,rc.sysinit,fstab 这三个文件的权限 # chmod 644 inittab # chmod 755 rc.sysinit # chmod 644 fstab 配置完 shell script 后, 我们注意到这些 shell script 会使用一些 /bin 目录下的命令, 但是 /bin 目录下是空的 现在就使用 BusyBox 来制作这些常用命令 (4) 定制系统应用程序 读者都知道,Linux 具有非常丰富的命令集 问题是标准的工作站或服务器发行套件都配备有数以千计的二进制命令文件, 而且不同发行套件提供的命令集各不相同 显然, 开发人员没有能力逐一交叉编译这么多二进制文件, 况且嵌入式系统也不需要这么多二进制文件 因此有两种做法 : 其一, 挑选需要的应用程序, 逐一交叉编译生成相应命令 ; 其二, 利用已有的套件如 BusyBox TinuLogin 和 Embutils 来定制系统应用程序 本书只讨论第二种方法 在 LINUX 系统下, 有一个这些常见 SHELL 命令的工具集合, 就是大名鼎鼎的 busybox busybox 是一个著名的开源项目, 它集成了很多的 LINUX 下 SHELL 命令, 除了常见的 ps/ls/pwd 等之外, 它还集成了 ftp/ftp server/telnet/telnet server/dhcp client/server/login/adduser/nfs mount 等众多的工具软件 它的版本到 1.00 之后就提供了一个 make menuconfig 的可视化配置界面, 可更为方便的让用户选择增删里 151

152 面的各个小命令工具 首先我们从官方网站上下载 BusyBox 的最新版 本 :busybox-1.00-rc3.tar.gz 并且解开 : #tar zxvf busybox-1.00-rc3.tar.gz #cd busybox-1.00-rc3 #make menuconfig 弹出如下工具选择界面 : make menuconfig 弹出如下工具选择界面 : 这个选单里面对 busybox 里面集成的众多的命令进行了分类, 您要根据您所要添加的命令的功能来到它应该所属的分 类里面去找, 并选中 完成自己要添加的命令的选择之后, 退出主界面, 选择保存配置 设定好 BusyBox 后, 接着进行编译和安装的设置 欲链接 glibc, 使用下列命 令 : #make TARGET_ARCH=arm CROSS=arm-linux- PREFIX={PRJROOT}/rootfs all install 其中 TARGET_ARCH 变量让 Makefile 判断是否可以进行某些架构无关的优化 CROSS 变量用来指定跨平台开发工具的前置名称 最后,PREFIX 变量指定根文件系统的基础目录 Makefile 会把所有的 BusyBox 组件安装到此目录 欲链接 uclibc 来建立 BusyBox, 可使用如下命令 : 152

153 #make TARGET_ARCH=arm CROSS=arm-elf- PREFIX={PRJROOT}/rootfs all install (5) 生成根文件系统映像这样在 rootfs 目录下就构建了一个文件系统, 我们根据内核的需要把它制成相应类型的文件系统映像文件, 比如在 uclinux 下, 根文件系统为 ROMFS 类型, 可以 采用如下命令实现 : genromfs v V ROMdisk f romfs.img d rootfs uclinux 进程管理 1. 标准 Linux 系统的进程相关概念 进程 : 进程是一个运行程序并为其提供执行环境的实体, 它包括一个地址空间和至少一个控制点, 进程在这个地址空间上执行单一指令序列 进程地址空间包括可以访问或引用的内存单元的集合, 进程控制点通过一个一般称为程序计数器 (program counter,pc) 的硬件寄存器控制和跟踪进程指令序列 fork: 由于进程为执行程序的环境, 因此在执行程序前必须先建立这个能 跑 程序的环境 Linux 系统提供系统调用拷贝现行进程的内容, 以产生新的进程, 调 用 fork 的进程称为父进程 ; 而所产生的新进程则称为子进程 子进程会承袭父进程的一切特性, 但是它有自己的数据段, 也就是说, 尽管子进程改变了所属的变量, 却不会影响到父进程的变量值 Linux 系统中亲子进程之间的关系可用下图 4-16 表 示 : 图 4-16 Linux 系统中亲子进程之间关系图 153

154 父进程和子进程共享一个程序段, 但是各自拥有自己的堆栈 数据段 用户空间以及进程控制块 换言之, 两个进程执行的程序代码是一样的, 但是各有各的程序计数器与自己的私人数据 当内核收到 fork 请求时, 它会先查核三件事 : 首先检查存储器是不是足够 ; 其次是进程表是否仍有空缺 ; 最后则是看看用户是否建立了太多的子进程 如果上述说三个条件满足, 那么操作系统会给子进程一个进程识别码, 并且设定 cpu 时间, 接着设定与父进程共享的段, 同时将父进程的 inode 拷贝一份给子进程运用, 最终子进程会返回数值 0 以表示它是子进程, 至于父进程, 它可能等待子进程的执行结束, 或与子进程各做个的 exec 系统调用 : 该系统调用提供一个进程去执行另一个进程的能力,exec 系统调用是采用覆盖旧有进程存储器内容的方式, 所以原来程序的堆栈 数据段与程序段都会被修改, 只有用户区维持不变 vfork 系统调用 : 由于在使用 fork 时, 内核会将父进程拷贝一份给子进程, 但是这样的做法相当浪费时间, 因为大多数的情形都是程序在调用 fork 后就立即调用 exec, 这样刚拷贝来的进程区域又立即被新的数据覆盖掉 因此 Linux 系统提供一个系统调用 vfork,vfork 假定系统在调用完成 vfork 后会马上执行 exec, 因此 vfork 不拷贝父进程的页面, 只是初始化私有的数据结构与准备足够的分页表 这样实际在 vfork 调用完成后父子进程事实上共享同一块存储器 ( 在子进程调用 exec 或是 exit 之前 ), 因此子进程可以更改父进程的数据及堆栈信息, 因此 vfork 系统调用完成后, 父进程进入睡眠, 直到子进程执行 exec 当子进程执行 exec 时, 由于 exec 要使用被执行程序的数据, 代码覆盖子进程的存储区域, 这样将产生写保护错误 (do_wp_page) ( 这个时候子进程写的实际上是父进程的存储区域 ), 这个错误导致内核为子进程重新分配存储空间 当子进程正确开始执行后, 将唤醒父进程, 使得父进程继续往后执行 2.uClinux 的多进程处理 uclinux 没有 mmu 管理存储器, 在实现多个进程时 (fork 调用生成子进程 ) 需要实现数据保护 uclinux 的 fork 等于 vfork, 因此实际上 uclinux 的多进程管理通过 vfork 来实现 这意味着 uclinux 系统 fork 调用完程后, 有下列两个选择 : 选择一 : 子进程代替父进程执行 ( 此时父进程已经 sleep) 直到子进程调用 exit 退出 ; 选择二 : 调用 exec 执行一个新的进程, 这个时候将产生可执行文件的加载, 即使这个进程只是父进程的拷贝, 这个过程也不能避免 当子进程执行 exit 或 exec 后, 子进程使用 wakeup 把父进程唤醒, 父进程继续往下执 uclinux 的这种多进程实现机制同它的内存管理紧密相关 uclinux 针对 nommu 处理器开发, 所以被迫使用一种 flat 方式的内存管理模式, 启动新的应用程序时系统必须为应用程序分配存储空间, 并立即把应用程序加载到内存 缺少了 MMU 的内存重映射机制,uClinux 必须在可执行文件加载阶段对可执行文件 reloc 154

155 处理, 使得程序执行时能够直接使用物理内存 图 4-17uCLinux 多进程实现图 4.3 嵌入式操作系统裁减与移植 嵌入式开发环境建立 由于 linux 的版本很多, 下面就以 uclinux 为例讲述嵌入式 linux 内核的裁减和移植 uclinux 是一个可配置 可裁减的系统和标准 Linux 一样, 它也是源码公开的, 使用者可以自由修改以便进行移 ; 同时,uClinux 也大量采用了条件编译, 通过条件编译, 在内核中选择需要的模块, 去掉不必要的模块不参与编译, 这样就能将内 核的大小控制在一定的范围内, 以适合嵌入式应用的需求 1. 建立 uclinux 的开发环境 为 uclinux 提供了 GNU 的交叉编译器, 包括以下组件 : Gcc 交叉编译器, 即在宿主机上开发编译目标上可运行的二进制文件 ; Binutils 辅助工具, 包括 objdump as ld 等 ; Gdb 调试器 以在 ARM7 上开发 uclinux 为例 : 第一步 : 获得 uclinux-dist 的源码包 上定期为新推出的 Linux 内核推出相应的源码包, 可以从 上免费下载得到 源码包里包含了 uclinux-2.4.x uclibc 和已经移植到 uclinux 下的用户应用程序 下载完后, 会得到一个 uclinux-dist tar.gz 的文件, 把它保存到 /home 目录下 第二步 : 解压缩源码包 155

156 执行 : tar zxvf uclinux-dist tar.gz 当 tar 程序运行完毕后, 在 /home 目录下会有一个 /home/uclinux-dist 的新目录, 这个目录就是 uclinux 的源码根目录, 里面有进行 uclinux 开发的所有的源代码 第三步 : 获得 ARM 开发工具, 提供 uclinux 源码的同时还提供相应的交叉编译工具 要在开发主机上为 ARM7 目标系统编译 uclinux, 还需要从 上下载 ARM 交叉编译器 : arm-elf-tools sh 得到这个文件以后, 执行以下命令 : sh arm-elf-tools sh 这个命令会在开发主机上自动建立一个 uclinux-arm 的交叉编译环境 键入 arm-elf-gcc, 如果能看到下面的输出信息 : Reading specs from /usr/local/lib/gcc-lib/arm-elf/2.95.3/specsgcc version (release) (ColdFire patches from XIP and shared lib patches from 表示 uclinux-arm 的交叉编译环境已经建立起来了 现在开发主机里已经有了 uclinux 的源代码和编译这些源代码的工具 嵌入式 uclinux 裁减 1. 内核裁减进入 uclinux-dist 目录, 执行命令 : make menuconfig 弹出如下对话框界面 : 156

157 回车后出现主选单, 选择 [*] Customize Kernel Settings 即表示进行内核配置 然后选择 Exit, 再 Exit, 选择 Yes, 则退出当前的菜单, 进入下面的内核配置主菜单 这里便是整个 uclinux 内核所有的配置选项, 配置的结果记录在两个文件 uclinux-dist/linux-2.0.x/.config 这是个隐藏文件 uclinux-dist/linux-2.0.x/include/linux/autoconf.h 手工改写这两个文件可以达到相同的效果, 不过不如这个配置界面友好直观, 而且容易出错, 因此建议使用 make menuconfig, 而不是手工改写文件 下面介绍常用一些选项的含义 157

158 Code maturity level options 对话框 : 含义 : 代码成熟等级 选项内容 : prompt for development and/or incomplete code/drivers 说明 : 如果你要试验现在仍处于实验阶段的功能, 比如 khttpd IPv6 等, 就 必须把该项选择为 Y 了 ; 否则可以把它选择为 N prompt for obsolete code/drivers 说明 : 如果对选项的含义不清楚的话, 可以按 help 按钮, 弹出下列对话框 从这个对话框我们可以了解每一个选项的含义 Loadable module support 158

159 对话框菜单如下 : 含义 : 对模块的支持 选项内容 : Enable loadable module support: 说明 : 除非你准备把所有需要的内容都编译到内核里面, 否则该项应该是必选的 Processor type and features 对话框菜单如下 : 选项内容 : ARM system type 说明 : 从上面对话框中选出你的主板上的 cpu 类型, 按空格键选中 159

160 Generate big endian code 说明 : 根据 cpu 类型, 选择大端模式还是小端模式, 默认值为小端模式, 也就是说, 如果你的处理器是大端模式的话, 一定要把这一项选中 Set flash/sdram size and base address 说明 : 这一项主要设置 flash sdram 的基地址和大小, 由于笔者所设计的嵌入式系统板上 flash 为 2M, 从 0x 开始 ;sdram 大小为 8M, 从 0xc 开始, 所以设置如下 General setup 对话框菜单如下 : 160

161 含义 : 这里是对最普通的一些属性进行设置 这部分内容非常多, 一般使用缺省设置就可以了 下面介绍一下经常使用的一些选项 : 选项内容 : Networking support: 说明 : 网络支持, 必须选, 没有网卡也建议选上 PCI support: 说明 :PCI 支持, 根据你的板卡是否支持决定选不选上 一般嵌入式系统板都不支持 pci Support for hot-pluggabel devices: 说明 : 热插拔设备支持, 可选 PCMCIA/CardBus support: 说明 :PCMCIA/CardBus 支持, 有 PCMCIA 就必选了 System V IPC BSD Process Accounting Sysctl support: 说明 : 以上三项是有关进程处理 /IPC 调用的, 主要就是 System V 和 BSD 两种风格 如果你不是使用 BSD, 就按照缺省吧 Memory Technology Device(MTD) 说明 :MTD 设备支持, 大家如果文件系统选择了 jffs2 的话, 一定要把这个选项选上 Networking options 对话框菜单 : 161

162 含义 : 网络选项 这里配置的是网络协议 一般情况下肯定要选中 TCP/IP networking, 因为没有它, 你的网卡是用不起来的, 其它的就看你的需要了 Network device support 对话框菜单 : 含义 : 网络设备支持 上面选好协议了, 现在该选设备 为了免得麻烦, 编译到内 核里面好了, 不选 M 了, 选 Y 比如如果要通过 modem 上网的话, 就必须把 ppp 协议选上 File systems 对话框菜单 : 162

163 含义 : 文件系统 目前嵌入式支持的文件系统非常多, 像 romfs cramfs jffs ntfs 等等, 大家在选择的时候, 只要把自己要实现的文件系统类型选上就可以了 最后别忘了将你的配置保存到一个文件里, 否则下次编译时又要重选, 保存对话框菜单如下 : 2. 系统应用程序裁减仍然建议用 make menuconfig 来选择, 不要手工修改, 手工非常麻烦 进入 uclinux-dist 目录, 执行 makemenuconfig, 则出现如下主选单 : 163

164 如上图选择后, 选择 Exit, 再 Exit, 则提示 : 选择 Yes, 则退出当前的菜单, 进入下面的应用配置主菜单 这里用户可以任意添加 / 删除自己的应用程序集 例如添加 Microwindows 应用软件, 则选中 MicroWindows 回车, 进入其选择配置菜单, 选择后退出保存即可 3. 内核编译内核经过裁减后, 就可以采用下列命令来进行编译 make dep make lib_only 164

165 make user_only make romfs genromfs v V ROMdisk f romfs.img d romfs make Image 嵌入式 ucli nu x 在 S3C44B0X 上的移植 1. 选择内核编译连接器 因为我们使用的处理器为 S3C44B0X, 所以对 linux 最顶层 Makefile 做如下修改 : arch:=armnommu CROSS_COMPILE:=arm-elf- 2. 压缩内核代码启动地址修改要修改的文件 :uclinux /linux/arch/armnommu/boot/makefile ifeq ($(CONFIG_ARCH_S3C44B0),y) ZRELADDR = 0x0c ZTEXTADDR = 0x0c endif 说明 : ZREALADDR 决定 KERNEL 解压后数据输出的地址, 此处为 0xc008000; ZTEXTADDR 带 BOOTLOADER 的压缩内核文件烧入 FLASH 的起始地址, 即从哪个位置开始执行, 此处为 0xc 处理器选项的修改修改 $topdir/arch/armnommu/config.in ============ 修改 =========== if [ "$CONFIG_ARCH_S3C44B0" = "y" ]; then define_bool CONFIG_NO_PGT_CACHE y define_bool CONFIG_CPU_32 y define_bool CONFIG_CPU_26 n define_bool CONFIG_CPU_ARM710 y define_bool CONFIG_CPU_WITH_CACHE y define_bool CONFIG_CPU_WITH_MCR_INSTRUCTION n define_hex DRAM_BASE 0x0c define_hex DRAM_SIZE 0x 要修改 dram 和 flash 的大小 define_hex FLASH_MEM_BASE 0x define_hex FLASH_SIZE 0x fi ============ 说明 :DRAM_BAS 为 SDRAM 的基地址,DRAM_SIZE 为 SDRAM 大小 ;FLASH_MEM_BASE 为 flash 的基地址,FLASH_SIZE 为 flash 大小 比如, 在上面的设置中,SDRAM 放 165

166 在 bank6 区, 大小为 8M,flash 放在 0 区, 大小为 2M 4. 内核起始地址的修改 要修改的文件 :uclinux /linux/arch/armnommu/makefile ifeq ($(CONFIG_ARCH_S3C44B0),y) TEXTADDR = 0x0c MACHINE = s3c44b0 endif 说明 :TEXTADDR 决定 KERNEL 起始运行地址, 即 IMAGE 应 DOWN 到的位置 0xc ; 5.Romfs 文件系统的定位要修改的文件 :uclinux /linux/drivers/block/blkmem.c 修改 blkmem_init() 加入此二句 : arena[0].length = (unsigned long)(-1); arena[0].address = (unsigned long)0x0c700000; 说明 : 在装载内核的时候, 要把 romfs.img 装载到 0xc70000 处. 6. 修改存储空间配置要修改的文件 : uclinux /linux/include/asm-armnommu/arch-s3c44b0/memory.h #ifndef ASM_ARCH_MEMORY_H #define ASM_ARCH_MEMORY_H #define TASK_SIZE (0x0d000000UL) #define TASK_SIZE_26 TASK_SIZE #define PHYS_OFFSET (DRAM_BASE) #define PAGE_OFFSET PHYS_OFFSET #define END_MEM (DRAM_BASE + DRAM_SIZE) #endif 说明 :PHYS_OFFSET 为第一个 bank 的起始地址 7. 定义二级异常中断矢量表的起始地址要修改的文件 :uclinux /linux/include/asm-armnommu/proc-armv system.h 中 #ifdef CONFIG_ARCH_S3C44B0 #undef vectors_base() #define vectors_base() 0x0c000008) #endif 说明 :vectors_base() 定义二级异常中断矢量表的起始地址, 这个地址和 bootloader 中的地址一定要相对应 8.ID 检查问题要修改的文件 :uclinux /linux /arch/armnommu/kernel/head-armv.s 说明 : 在 head-armv.s 应加入对 MACH_TYPE 的设定 在 uclinux 启动时, 首先 166

167 会检查 CPU 的 ID 和平台的 TYPE, 如果不对会陷入死循环 在 TPU 的 Patch 中, 加入了对 CPU ID 的设定, 但没有加入对平台类型的设定 平台类型可能是应该从 BootLoader 中设好传进来, 但对我们自已的 BootLoader, 没有完成此设定, 所以应把 MACH_TYPE(178) 设到 r1 寄存器 否则, 在系统调用 lookup_architecture_type 之后就没戏了 修改如下 : mov r1, #178 ; == 此句为新加的 mov r0, #F_BIT I_BIT make sure svc mode msr cpsr_c, and all irqs disabled 网卡修改说明 : 要移植 8019 网卡, 必须注意下列问题 确定网卡的基地址 中断无误要修改的文件 : 修改如下 : #elif defined(config_board_mba44) static int once = 0; if (once) return -ENXIO; dev->base_addr = base_addr = 0x ;//NE2000_ADDR; dev->irq = 24;//NE2000_IRQ_VECTOR; once++; #endif 注意网卡的中断的模式和处理对应的外部中断是不是一致对于 IO 和 RAM 统一编址的处理器, 注意缓冲区范围的设置注意 ARMv3 和 ARMv4 等一些和处理器结构相关的底层函数库带来的问题 uclinux 启动分析 在嵌入式平台中存储设备通常有 SDRAM 和 flash 两种, 由于 SDRAM 存储设备为易挥发的存储器, 掉电以后其上的内容就全部消失掉, 所以必须把操作系统内核和根文件系统的内容存放到 flash 设备上 但是并不能一开机就跳到操作系统内核处运行, 这主要因为操作系统在运行时, 需要一定的运行环境, 比如动态的创建一些数据段 堆栈 页表等内容, 而这些都需要在 RAM 中进行, 因此操作系统必须被首先搬运到 RAM 中, 然后再从 RAM 中执行操作系统 搬运操作系统的任务通常都有 bootloader 来完成地 Bootloader 搬运完操作系统后, 会有一条如下的跳转语句 : (*fp)( ); 其中 fp 定义如下 : 167

168 地址 void (*fp)(void) = (void (*)(void))kernel_start; KERNEL_START 就是 bootloader 把在 flash 上的操作系统内核搬运到 RAM 中 1.head-armv.S 分析 这个文件是 linux/arch/armnommu/kernel/head-armv.s, 用汇编代码完成, 是内核最先执行的一个文件 这一段汇编代码的主要作用, 是检查 cpu id,architecture number, 初始化页表 cpu bbs 等操作, 并跳到 start_kernel 函数 // 整个 uclinux 内核入口点 ENTRY(stext) /*ro 应该为 0,r1 为 architecture number, 它们从 bootloader 传过来, 有时 bootloader 没有做这一步, 用户应该自己添加代码来完成 */ mov r12, r0 // 程序状态, 禁止 FIQ IRQ, 设定 Supervisor 模式 mov r0, #F_BIT I_BIT make sure svc mode msr cpsr_c, and all irqs disabled: 设置 cpu 为特权模式, 关中断 /* 跳转到判断 cpu 类型, 查找运行的 cpu 的 id 值, 和此 linux 编译支持的 id 值, 是否有相等从该函数返回后, 寄存器内容如下 : R8 = page data flag R9 = processor ID R10 = pointer to processor structure 详细的内容请看 lookup_architecture_type 的分析 */ bl lookup_processor_type /* 如果没有则跳到 error 处执行 */ teq r10, invalid processor? moveq r0, yes, error 'p' beq error /* 跳转到判断体系类型 */ bl lookup_architecture_type // 至此正式进入内核中 b SYMBOL_NAME(start_kernel) 2.Start_kernel 函数 start_kerel 位于是 linux/init/main.c 下的 start_kernel( ) 函数, 它是 uclinux 启动最 168

169 为核心的一个函数, 所有工作都是在这个函数中完成地 代码如下 : /* * Activate the first processor. */ asmlinkage void init start_kernel(void) { char * command_line; extern char saved_command_line[]; /* * Interrupts are still disabled. Do necessary setups, then * enable them */ lock_kernel(); printk(linux_banner); setup_arch(&command_line); printk("kernel command line: %s\n", saved_command_line); parse_options(command_line); trap_init(); init_irq(); sched_init(); softirq_init(); time_init(); /* * HACK ALERT! This is early. We're enabling the console before * we've done PCI setups etc, and console_init() must be aware of * this. But we do want output early, in case something goes wrong. */ console_init(); #ifdef CONFIG_MODULES init_modules(); #endif if (prof_shift) { unsigned int size; /* only text is profiled */ prof_len = (unsigned long) &_etext - (unsigned long) &_stext; prof_len >>= prof_shift; 169

170 } size = prof_len * sizeof(unsigned int) + PAGE_SIZE-1; prof_buffer = (unsigned int *) alloc_bootmem(size); kmem_cache_init(); sti(); calibrate_delay(); #ifdef CONFIG_BLK_DEV_INITRD if (initrd_start &&!initrd_below_start_ok && initrd_start < min_low_pfn << PAGE_SHIFT) { } #endif mem_init(); printk(kern_crit "initrd overwritten (0x%08lx < 0x%08lx) - " "disabling it.\n",initrd_start,min_low_pfn << PAGE_SHIFT); initrd_start = 0; kmem_cache_sizes_init(); pgtable_cache_init(); /* * For architectures that have highmem, num_mappedpages represents * the amount of memory the kernel can use. For other architectures * it's the same as the total pages. We need both numbers because * some subsystems need to initialize based on how much memory the * kernel can use. */ if (num_mappedpages == 0) num_mappedpages = num_physpages; fork_init(num_mappedpages); proc_caches_init(); vfs_caches_init(num_physpages); buffer_init(num_physpages); page_cache_init(num_physpages); #if defined(config_arch_s390) ccwcache_init(); #endif signals_init(); 170

171 #ifdef CONFIG_PROC_FS proc_root_init(); #endif check_bugs(); printk("posix conformance testing by UNIFIX\n"); /* * We count on the initial thread going ok * Like idlers init is an unlocked kernel thread, which will * make syscalls (and thus be locked). */ smp_init(); #if defined(config_sysvipc) ipc_init(); #endif rest_init(); } start_kernel 中的函数个个都是重量级的, 下面逐个进行分析解释 (1)lock_kernel() extern inline void lock_kernel(void) { if (!++current->lock_depth) spin_lock(&kernel_flag); } kernel_flag 是一个内核大自旋锁, 所有进程都通过这个大锁来实现向内核态的迁移 只有获得这个大锁的处理器可以进入内核 ( 如中断处理 ) 这个函数相当简单, 它获得全局内核锁 -- 在任何一对 lock_kernel/unlock_kernel 函数里至多可以有一个 CPU 进程的 lock_depth 成员初始化为 -1( 参见 kerenl/fork.c) 在它小于 0 时 ( 若小于 0 则恒为 -l), 进程不拥有内核锁 ; 当大于或等于 0 时, 进程得到内核锁 对于单 cpu, 该函数为空 (2)printk(linux_banner); start_kernel 一开始的 printk(linux_banner) 实际只将输入写入了缓冲, 等在串口和 console 初始化后, 对 printk 的调用才一次将缓冲中的内容向串口和 console 输出 其中,linux_banner 定义在 init/version.c 中, 它主要定义了 Linux 内核的版本信息 代码为 : const char *linux_banner = "Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@" LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "\n" 171

172 (3)setup_arch(&command_line) 该函数主要做与体系结构相关的初始化工作 主要工作有下列几项 : 初始化内存, 建立页表 创建启动设备节点 设置处理器和体系结构相关的信息 分析传来的参数 (4)parse_options(command_line), 该函数解释系统参数 (5)trap_init() 该函数设置系统异常的入口点, 在这里要说明的是, 异常中断矢量表 (Exception Vector Table) 是 Bootloader 与 uclinux 内核发生联系关键的地方之一 即使 uclinux 内核已经得到处理器的控制权运行, 一旦发生中断, 处理器还是会自动跳转到从 0x0 地址开始的第一级异常中断矢量表中的某个表项 ( 依据于中断类型 ) 处读取指令运行 对于 uclinux 内核, 它在 RAM 空间中基地址为 0xc 处建立了自己的二级异常中断矢量表 (6)init_IRQ(), 初始化系统中断服务 (7)sched_init(), 初始化系统调度器 (8)time_init(), 初始化时钟 定时器 (9)console_init(), 控制台初始化, 该函数执行完后, 超级终端上就可以看到内核输出的信息 (10)kmem_cache_init(), 内核 cache 初始化 (11)sti() sti() 函数的编译代码如下 : mrs r3,cpsr// 将 cpsr 中的内容写到 r3 中, bic r3,r3,#0x80// 将 r3 低位的第 8 位清零 ( 这一位位中断允许位,) msr cpsr_c,r3// 重新写到 cpsr 中 因此 sti() 代码为开中断, 如果 bootloader 中没有设置中断跳转指令或者中断设置的不对, 系统执行到这个地方就会死机, 因为中断来了它不知跳到哪里去了 (12)calibrate_delay(), 校准时钟 (13)mem_init(), 内存初始化 (14)fork_init(num_mappedpages), 建立 uidcache, 并根据系统内存大小来确定最大进程目 (15)check_bugs(), 检查体系结构漏洞 (16)rest_init() 该函数代码如下 : static void rest_init(void) { kernel_thread(init, NULL, CLONE_FS CLONE_FILES CLONE_SIGNAL); unlock_kernel(); 172

173 current->need_resched = 1; cpu_idle(); } 首先, kernel_thread(init, NULL, CLONE_FS CLONE_FILES CLONE_SIGNAL) 创建第一个核心进程 init, 并启动该进程 然后 cpu_idle() 运行 idle 进程 Init 进程首先锁定内核, 然后调用 do_basic_setup() 来完成外部设备以及驱动程序的初始化 外设的初始化要根据内核的配置来决定, 但至少包括下列一些初始化工作 : 网络初始化 文件系统初始化 加载文件系统在 do_basic_setup() 函数执行完成以后,init 进程会释放初始化函数所用的内存, 并且打开 /dev/console 设备重定向控制台, 让系统调用 execve 来执行程序 init 到此为止,uCLinux 内核的初始化工作已经全部完成 小结 嵌入式系统开发最坚实的基础就是具备操作系统的支撑, 这和原来普通的单片机开发具有本质的区别 本章对目前常用的几种嵌入式操作系统进行了简单的介绍, 指出它们的体系架构 市场应用场合以及相关的开发工具 在这些嵌入式操作系统介绍中, 本章重点介绍了嵌入式 uclinux 操作系统, 涉及到 uclinux 体系结构 内存管理 进程管理以及文件系统等知识点 本章最后还对嵌入式 uclinux 的裁减与移植做了深入浅出的介绍 这些知识点嵌入式开发人员必须牢牢掌握 习题 1. 简述嵌入式操作系统选用 Linux 具有哪些优点, 缺点是什么? 2.uClinux 和标准 Linux 在内存管理上有何异同点? 3. 简述 uclinux 内存管理机制,reloc 段机制是如何处理的, 为何在 uclinux 中采用 reloc 段机制? 4. 简述 Linux 文件系统中为何能同时支撑多种文件系统,VFS 在其中承担了哪些任务? 5. 简述 ROMFS 和 JFFS 文件系统体系结构, 试比较它们效率上哪个更好 6. 根文件系统和文件系统之间关系是什么? 除了本章介绍到的文件系统类型外, 上网查询还有哪些其它的文件系统类型, 并对它们的性能进行比较 173

174 第五章嵌入式设备驱动及应用开发 5.1 嵌入式系统设备驱动开发 设备驱动程序的作用 系统调用是操作系统内核和应用程序之间的接口, 设备驱动程序是操作系统内核和机器硬件之间的接口 设备驱动程序为应用程序屏蔽了硬件的细节, 这样在应 用程序看来, 硬件设备只是一个设备文件, 应用程序可以象操作普通文件一样对硬件设备进行操作 图 5-1 嵌入式设备驱动接口示意图 设备驱动程序是内核的一部分, 它完成以下的功能 : 对设备初始化和释放 ; 174

175 把数据从内核传送到硬件和从硬件读取数据 ; 读取应用程序传送给设备文件的数据和回送应用程序请求的数据 ; 检测和处理设备出现的错误 ; 在 Linux 操作系统下有两类主要的设备文件类型, 一种是字符设备, 另一种是块设备 字符设备和块设备的主要区别是 : 在对字符设备发出读 / 写请求时, 实际的硬 件 I/O 一般就紧接着发生了, 块设备则不然, 它利用一块系统内存作缓冲区, 当用户进程对设备请求能满足用户的要求, 就返回请求的数据, 如果不能, 就调用请求函数来进行实际的 I/O 操作 块设备是主要针对磁盘等慢速设备设计的, 以免耗费过多的 CPU 时间来等待 已经提到, 用户进程是通过设备文件来与实际的硬件打交道. 每个设备文件都都有其文件属性 (c/b), 表示是字符设备还是块设备? 另外每个文件都有两个设备号, 第一个是主设备号, 标识驱动程序, 第二个是从设备号, 标识使用同一个设备驱动程序的不同的硬件设备, 比如有两个软盘, 就可以用从设备号来区分他们, 设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致, 否则用户进程将无法访问到驱动程序 设备和模块的分类 1. 基本概念 UNIX 的一个基本特点是它抽象了设备的处理 所有的硬件设备都象常规文件一样看待 : 它们可以使用和操作文件相同的 标准的系统调用来进行打开 关闭和读写 系统中的每一个设备都用一个设备特殊文件代表 例如系统中第一个 IDE 硬盘用 /dev/had 表示 对于块 ( 磁盘 ) 和字符设备, 这些设备特殊文件用 mknod 命令创建, 并使用主 ( major ) 和次 ( minor ) 设备编号来描述设备 网络设备也用设备特殊文件表达, 但是它们由 Linux 在找到并初始化系统中的网络控制器的时候创建 同一个设备驱动程序控制的所有设备都由一个共同的 major 设备编号 次设备编号用于在不同的设备和它们的控制器之间进行区分 例如, 主 IDE 磁盘的不同分区都由一个不同的次设备编号 所以, /dev/hda2, 主 IDE 磁盘的第 2 个分区的主设备号是 3, 而次设备号是 2 Linux 使用主设备号表和一些系统表 ( 例如字符设备表 chrdevs ) 把系统调用中传递的设备特殊文件 ( 比如在一个块设备上安装一个文件系统 ) 映射到这个设备的设备驱动程序中 2. 设备分类 Linux 支持三类的硬件设备 : 字符 块和网络 (1) 字符设备可以象文件一样访问字符设备, 字符设备驱动程序负责实现这些行为 这样的驱动程序通常会实现 open,close,read 和 write 系统调用 系统控制台和并口就是字符设备的例子, 它们可以很好地用流概念描述 通过文件系统节点可以访问字符设备, 例如 /dev/tty1 和 /dev/lp1 在字符设备和普通文件系统间的唯一区别是: 175

176 普通文件允许在其上来回读写, 而大多数字符设备仅仅是数据通道, 只能顺序读写 当然, 也存在这样的字符设备, 看起来象个数据区, 可以来回读取其中的数据 (2) 块设备块设备是文件系统的宿主, 如磁盘 在大多数 Unix 系统中, 只能将块设备看作多个块进行访问为, 一个块设备通常是 1K 字节数据 Linux 允许你象字符设备那样读取块设备 允许一次传输任意数目的字节 结果是, 块设备和字符设备只在内核内部的管理上有所区别, 因此也就是在内核 / 驱动程序间的软件接口上有所区别 就象字符设备一样, 每个块设备也通过文件系统节点来读写数据, 它们之间的不同对用户来说是透明的 块设备驱动程序和内核的接口和字符设备驱动程序的接口是一样的, 它也通过一个传统的面向块的接口与内核通信, 但这个接口对用户来说时不可见的 (3) 网络接口任何网络事务处理都是通过接口实现的, 即, 可以和其他宿主交换数据的设备 通常接口是一个硬件设备, 但也可以象 loopback( 回路 ) 接口一样是软件工具 网络接口是由内核网络子系统驱动的, 它负责发送和接收数据包, 而且无需了解每次事务是如何映射到实际被发送的数据包 尽管 telnet 和 ftp 连接都是面向流的, 它们使用同样的设备进行传输 ; 但设备并没有看到任何流, 仅看到数据报 由于不是面向流的设备, 所以网络接口不能象 /dev/tty1 那样简单地映射到文件系统的节点上 Unix 调用这些接口的方式是给它们分配一个独立的名字 ( 如 eth0) 这样的名字在文件系统中并没有对应项 内核和网络设备驱动程序之间的通信与字符设备驱动程序和块设备驱动程序与内核间的通信是完全不一样的 内核不再调用 read,write, 它调用与数据包传送相关的函数 Linux 有许多不同的设备驱动程序 ( 这也是 Linux 的力量之一 ) 但是它们都具有一些一般的属性 : Kernel code : 设备驱动程序和核心中的其他代码相似, 是 kenel 的一部分, 如果发生错误, 可能严重损害系统 一个写错的驱动程序甚至可能摧毁系统, 可能破坏文件系统, 丢失数据 Kenel interface:s 设备驱动程序必须向 Linux 核心或者它所在的子系统提供一个标准的接口 例如, 终端驱动程序向 Linux 核心提供了一个文件 I/O 接口, 而 SCSI 设备驱动程序向 SCSI 子系统提供了 SCSI 设备接口, 接着, 向核心提供了文件 I/O 和 buffer cache 的接口 Kernel mechanisms and services: 设备驱动程序使用标准的核心服务例如内存分配 中断转发和等待队列来完成工作 Loadable Linux : 大多数的设备驱动程序可以在需要的时候作为核心模块加载, 在不再需要的时候卸载 这使得核心对于系统资源非常具有适应性和效率 Configurable Linux: 设备驱动程序可以建立在核心 哪些设备建立到核心在核心编译的时候是可以配置的 Dynamic: 在系统启动, 每一个设备启动程序初始化的时候它查找它管理 176

177 的硬件设备 如果一个设备驱动程序所控制的设备不存在并没有关系 这时这个设备驱动程序只是多余的, 占用很少的系统内存, 而不会产生危害 构建模块 模块编程 1. 模块基本概念 Linux 内核是一个整体是结构, 因此向内核添加任何东西, 或者删除某些功能, 都十分困难 为了解决这个问题引入了内核机制 从而可以动态的想内核中添加或者删除模块 模块不被编译在内核中, 因而控制了内核的大小. 然而模块一旦被插入内核, 他就和内核其他部分一样. 这样一来就会曾家一部分系统开销 同时, 如果模块出现问题, 也许会带来系统的崩溃 2. 模块的实现机制启动时, 由函数 void inti_modules() 来初始化模块, 因为启动事很多时候没有模块, 这个函数往往把内核自身当作一个虚模块 如由系统需要, 则调用一系列以 sys 开头的函数, 对模块进行操作 如 : sys_creat_modules(); sys_inti_modules() ; sys_deldte_modules() 等等. 这里会用到一些模块的数据就结构, 在 /usr/scr/linux/include/linux/module.h 中, 有兴趣的朋友可以找出来看一看 3. 模块编程写一个模块, 必须有一定的多进程编程基础, 因为你编得程序不是以一个独立的程序的来运行的 另外, 因为模块需要在内核模式下运行, 会遇到在内和空间和用户空间数据交换的问题. 一般的数据复制函数无法完成这一个过程 因此系统已入了一些特殊的函数以用来完成内核空间和用户空间数据的交换 这些函数有 : void put_user (type valude,type *u_addr) memcpy_tofs() 等等, 有兴趣的朋友可以仔细的看看所有的函数, 以及它们的用法 需要说明的是, 模块编程和内核的版本有很大的关系 如果版本不同可能造成内核模块不能编译, 或者在运行这个模块时, 出现不可预测结果, 如系统崩溃等 对于每一个内核模块来说, 必定包含下面两个函数 : int init_module(): 这个函数在插入内核时启动, 在内核中注册一定的功能函数, 或者用他的代码代替内和中某些函数的内容 ( 估计这些函数是空的 ) 177

178 int cleanup_module(): 当内核模块卸载时调用, 它能将模块从内核中清除 当然, 一个内核模块还会包含其它函数, 下面是内核模块的结构体定义, 从中 读者可以看出一个内核模块通常情况下具有几个接口函数 struct module { unsigned long size_of_struct; /* == sizeof(module) */ struct module *next; const char *name; unsigned long size; union { atomic_t usecount; long pad; } uc; /* Needs to keep its size - so says rth */ unsigned long flags; /* AUTOCLEAN et al */ unsigned nsyms; unsigned ndeps; }; struct module_symbol *syms; struct module_ref *deps; struct module_ref *refs; int (*init)(void); void (*cleanup)(void); const struct exception_table_entry *ex_table_start; const struct exception_table_entry *ex_table_end; const struct module_persist *persist_start; const struct module_persist *persist_end; int (*can_unload)(void); int runsize; /* In modutils, not currently used */ const char *kallsyms_start; /* All symbols for kernel debugging */ const char *kallsyms_end; const char *archdata_start; /* arch specific data for module */ const char *archdata_end; const char *kernel_data; /* Reserved for kernel internal use */ 178

179 下面我们给出一个内核编程的简单例子 我们编写一个模块, 该模块在加载到内核后可以打印出 hello world 例 : (1) 首先编写内核模块初始化函数和卸载函数 /*hello.c a module programm*/ /* the program runing under kernel mod and it is a module*/ #include" Linux/kernerl.h" #include"llinux/module.h" /* pross the CONFIG_MODVERSIONS*/ #if CONFIG_MODVERSIONS==1 #define MODVERSIONS #include""linux/modversions.h" #end if /* the init function*/ int init_module() { printk(" hello world!\n'); } printk(" I have runing in a kerner mod@!!\n"); return 1; /* the distory function*/ int cleanup_module() { } printk(" I will shut down myself in kernerl mod /n)"; retutn 0; (2) 编写 makefile 上面的源代码写完后, 就可以直接用 gcc 进行编译了, 但是为了复习一下 makefiel 的使用, 下面就这段代码我们编写一个 makefile 例子 下面是其内容 : # a makefile for a module CC=gcc MODCFLAGS:= -Wall _DMODULE -D_KERNEL_ -DLinux hello.o hello.c /usr/inculde?linux/version.h CC $(MODCFLAGS) 0c hello.c echo the module is complie completely (3) 编译然后运行 make 命令得到 hello.o 这个模块 (4) 加载运行 179

180 $insmod hello.o hello world! I will shut down myself in kernerl mod $lsmod hello (unused). $remmod I will shut down myself in kernerl mod 这样该模块就可以随意的插入和删除了 驱动模块设计 上面讲述的是模块的简单编程, 在 Linux 中, 大部分驱动程序是以模块的形式编写的, 这些驱动程序源码可以修改到内核中, 也可以把他们编译成模块形势, 在需要的时候动态加载 一个典型的驱动程序, 大体上可以分为这么几个部分 : 1. 注册设备在系统初启, 或者模块加载时候, 必须将设备登记到相应的设备数组, 并返回设备的主驱动号, 例如 : 对快设备来说调用 refister_blkdec() 将设备添加到数组 blkdev 中, 并且获得该设备号, 并利用这些设备号对此数组进行索引 对于字符驱动设备来说, 要使用 module_register_chrdev() 来获得祝设备的驱动号, 然后对这个设备的所有调用都用这个设备号来实现 2. 定义功能函数对于每一个驱动函数来说, 都有一些和此设备密切相关的功能函数, 那最常用的块设备或者字符设备来说, 都存在着诸如 open() read() write() ioctrol() 这一类的操作 当系统社用这些调用时, 将自动的使用驱动函数中特定的模块, 来实现具体的操作 而对于特定的设备, 上面的系统调用对应的函数是一定的 如 : 在块驱动设备中. 当系统试图读取这个设备 ( 即调用 read() 时 ), 就会运行驱动程序中的 block_read() 这个函数 打开新设备时会调用这个设备驱动程序的 device_open() 这个函数. 3. 卸载模块在不用这个设备时, 可以将他卸载, 主要是从 /proc 中取消这个设备的特殊文件, 可用特定的函数实现 下面我们列举一个字符设备驱动程序的框架. 来说明这个过程 例 : /* a module of a character device */ /* some include files*/ #include"param.h" #include"user.h" #include"tty.h" 180

181 #include"dir.h" #include fs.h" /* the include files modules need*/ #include"linux/kernel.h" #include"linux/module.h" #if CONFIG_MODBERSIONS==1 degine MODBERSIONS #include" Linux.modversions.h" #endif #difine devicename mydevice /* the init funcion*/ int init_module() { int tag=module_register_chrdev(0,mydevice,&fops); if (tag<0) { printk("the device init is erro!\n"); return 1; } return 0; } /*the funcion which the device will be used */ int device_open () { } int device_read () { } int device_write () { } int device_ioctl () { } 181

182 /* the deltter function of this module*/ int cleanup_module() { } int re=module_unregister_chrdev(tag,mydevice); if( re<0) { printk("erro unregister the module!!\n"); return 1; } return 0; 5.2 嵌入式应用程序开发 嵌入式应用程序开发流程 uclinux 以其优异的性能 免费开放的代码等优点, 博得众多嵌入式开发者的青 睐 和过去基于简单 RTOS 甚至没有使用任何操作系统的嵌入式程序设计相比, 基于 Linux 这样的成熟的 高效的 健壮的 可靠的 模块化的 易于配置的操作系统来开发自己的应用程序, 无疑能进一步提高效率, 并具有很好的可移植性 我们知道, 在主流的 Linux 平台上, 已经有了非常丰富的 开源的应用程序, 使得开发者很容易获得前人的成果作为参考, 编写更适合自己的程序 然而, 对于很多已经在标准 Linux 环境中工作得很好的程序, 并不能直接在 uclinux 环境上运行 一方面, 是由于嵌入式的 uclinux 所使用的处理器和普通 PC 不同, 指令集 CPU 结构上的差异导致 uclinux 上运行的程序需要专门为该类型处理器交叉编译产生 ; 另一方面,uCLinux 是为了没有内存管理单元 (MMU) 的处理 器 控制器设计, 并做了较大幅度的精简, 所以, 在标准 Linux 上可以使用的一些函数和系统调用在 uclinux 上有可能就行不通了 下面我们首先介绍一下嵌入式 uclinux 下应用程序的开发流程 1. 编写应用程序第一步就是要进行应用程序的设计, 在这个阶段, 和标准 Linux 平台下进行应用程序开发并没有本质的区别 唯一需要注意的是 uclinux 和标准 Linux 在体系结 构以及库等方面的不同就可以了 如果你是一个新手, 不妨首先阅读 uclinux 2.4.x 的目录内的 Document, 具体目录为../Documentation/Adding-User-Apps-HOWTO.txt 2. 将应用程序添加到 uclinux 182

183 例如源程序名为 app1.c, 进入 uclinux-samsung/user 目录并建立一个自己的子目录 mkdir myapp 这样在 user 目录下就建立了一个新的子目录 myapp, 把 app1.c 拷贝到 myapp 目录下, 并编写 Makefile, 如下所示 : EXEC = app1 OBJS = app1.o all: $(EXEC) $(EXEC): $(OBJS) $(CC) $(LDFLAGS) o $@ $(OBJS) $(LDLIBS) romfs: $(ROMFSINST) /bin/$(exec) clean: rm f $(EXEC) *.elf *.gdb *.o 进入 user 目录, 增加一行语句到该目录下的 Makefile 文件中 : dir_$(config_user_myapp_app1) += myapp 该语句的作用是让编译器可以访问到我们所创建的 myapp 目录下的 makefile 文 件, 保存后退出 切换到目录 / uclinux-samsung/config 下, 编辑 Configure.help 文件, 即输入一下命令 cd../config vi Configure.help 这是一个包含了在配置的时候出现的所有文本信息的文件 在这个文件中加入 类似下面的语句块 : CONFIG_USER_ MYAPP_APP1 This program is an example. 注意第二行文本信息必须要空两格开始 每行的字符要小于 70 个 添加完毕后, 保存退出 不过, 用户也可不必修改该文件, 因为它仅仅是提供一个在线文本信息显示的功能, 对于添加用户程序到 ucllinux 影响不大 接下来需要修改 uclinux 系统中对编译器来讲比较重要的一个文件 config.in 仍然是在 config 目录下, 打开该文件, 在最后增加类似下面的语句 : ################################################################# mainmenu_option next_comment comment My Application bool app1 CONFIG_USER_MYAPP_ APP1 comment My Application endmenu ################################################################# 183

184 现在我们已经把要做的修改的相关工作完成了, 接下来需要进行内核的编译工作, 键入 : make menuconfig 按下面界面选择 : 图 5-2 添加应用程序配置 图 5-3 选择要配置的用户应用程序 当用户应用程序做了修改后, 需要重新编译内核, 如下步骤 : 184

185 make lib_only make user_only make romfs genromfs v V ROMdisk f romfs.img d romfs make Image 上面介绍的方法中, 在将用户应用程序添加到 uclinux 内核运行时, 都需要对内核进行部分或全部的编译, 每次对内核编译完成后, 都要先将 FLASH 存储器中的内容擦除, 然后重新烧写新编译好的内核到 FLASH 存储器中去, 这对于程序开发来说, 是非常不方便的 实际操作中往往采用下面的做法 : 首先编写应用程序, 在主机端把应用程序编译好, 然后通过网络来传输可执行文件到目标板上, 这样可以避免每次测试程序运行效果时都要编译一次内核 uclinux 下邮件收发系统应用程序开发实例 嵌入式 系统是嵌入式操作系统及其应用软件的一个重要组成部分, 但是现有 PC 上的嵌入式 系统都不能适应类 PC 嵌入式系统的硬件环境要求 本节主要讲述如何设计和实现一种嵌入式邮件收发系统 ucmail uclinux 下的邮件收发系统 ucmail 被设计成两个部分 : 邮件发送客户端 ucsm 和邮件接收客户端 ucrm ucrm 实质上是一个 uclinux 下的 POP3 客户机, 它使用 POP3 协议, 可以与 POP3 邮件服务器进行通信, 从服务器接收邮件 邮件系统的附件模块负责对附件进行处理 下面分别描述 ucsm 和 ucrm 的设计方案 1. 邮件发送客户端 ucsm 设计 ucsm 实质上是一个 uclinux 下的 SMTP 客户机, 它的设计遵循 SMTP 协议和 MIME 协议, 可以与 SMTP 邮件服务器进行通信, 读入用户输入, 产生并向目的地发送符合 MIME 格式的邮件 ucsm 的软件结构图如图 5-4 所示 当用户发送邮件时, 将所发送的信息输入到用户界面模块, 经过附件处理模块后, 进入编码解码模块将二进制文件按 base64 编码成 ASCII 文本, 由协议模块与 ESMTP SMTP 远端邮件服务器通信, 将邮件通过网络模块发送到邮件服务器, 从而到达目的地 3. 邮件接收客户端 ucrm 设计 ucrm 实质上是一个 uclinux 下的 POP3 客户机, 它遵照 POP3 协议和 MIME 协议, 可以与 POP3 邮件服务器进行通信, 从服务器接收邮件 ucrm 的软件结构图跟 ucsm 的相似, 如图 5-5 所示 当用户接收邮件的时候, 用户将收件人的基本信息填入用户界面模块后, 经过附件处理模块处理附件, 由协议模块与远端服 POP3 服务器进行通信, 通过网络模块将邮件信息从服务器收取, 已编码附件再经过编码解码模块解码成二进制文件并保存, 最后用户界面模块显示邮件内容, 供用户阅读 185

186 图 5-4 ucsm 的软件结构图 3. 核心代码数据结构 (1)MIME 格式邮件结构 下面是 ucsm 使用的邮件结构, struct mailheader { char subject[max_subject+1]; char sender[max_sndr+1]; char recipient[max_rcpt+1]; 图 5-5 ucrm 的软件结构图 // 主题 // 发送方 // 接收方 char specialheaders[max_special+1]; //MIME-Version char contenttype[max_content+1]; //content-type char contenttype1[max_content+1]; // char boundary[80]; char boundary2[80]; // char encoding[80]; char filename[80]; char *contents; char *attachments; }; //boundary // 编码方式 // 附件文件名 // 邮件正文 // 附件 (2) 编码函数 to64frombits() 函数原形 : 186

187 void to64frombits(unsigned char *out, const unsigned char *in, int inlen) 读入参数 : char *out: 指向目标字符数组的指针 char *in: 指向源字符数组的指针 int inlen: 源字符串长度 返回值 : void 为空功能 : 将含有二进制数据的源字符串按 base64 编码成 ASCII 码字符串输出到目标字符数组, 要求指明源串的长度 (3) 解码函数 from64tobits() 函数原形 : int from64tobits(char *out, const char *in) 读入参数 : char *out: 指向目标字符数组的指针 char *in: 指向源字符数组的指针返回值 : 返回完成解码字符串的长度 :int len 功能 : 读入源串中已编码的 ASCII 串, 进行解码输出到目标字符串数组 (4)sendmail() 函数原形 : int sendmail(struct mailheader *mail) 读入参数 : mailheader *mail 指向邮件头结构类型的邮件指针返回值 : 0 发送成功,-1 有错误功能 : 读入邮件, 并进行 MIME 格式封装, 在把邮件发送到 SMTP 服务器 (5) 核心函数 recei() 函数原形 : int recei(int s, int flag) 读入参数 : int s: 已建立的 socket 套接字描述符 int flag: 用来控制选项操作的参数 flag = 4 进行 RETR 操作 flag = 5 进行 DELE 操作返回值 : 1 成功执行,-1 有错误 4. 核心代码清单 187

188 (1) 发送邮件函数 // sendmail() // // Send the mail to the mail server // // Returns 0 on success, -1 on error int sendmail(struct mailheader *mail) { int connfd, result, ret, goodmsg = 0; struct sockaddr_in servaddr; char mailrcpt[129]; char line[512]; connfd = socket(af_inet, SOCK_STREAM, 0); bzero((void *)&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons (25); servaddr.sin_addr.s_addr = inet_addr(mailserver); if (servaddr.sin_addr.s_addr == 0xffffffff){ struct hostent *hptr = (struct hostent *)gethostbyname(mailserver); if (hptr == NULL){ return -1; }else{ struct in_addr **addrs; addrs = (struct in_addr **)hptr->h_addr_list; memcpy(&servaddr.sin_addr, *addrs, sizeof(struct in_addr)); } } result = connect(connfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); do { //initial if ( dialog(connfd, NULL, "220") ) break; //HELO if ( dialog(connfd, hello_msg, "250") ) break; //MAIL FROM sprintf(line, "MAIL FROM: <%s>\n", mail->sender); if ( dialog(connfd, line, "250") ) break; //RCPT TO 188

189 sprintf(line, "RCPT TO: <%s>\n", mail->recipient); if ( dialog(connfd, line, "250") ) break; //DATA if ( dialog(connfd, "DATA\n", "354") ) break; //send out the header sprintf(line, "From: %s\n", mail->sender); if ( dialog(connfd, line, NULL) ) break; sprintf(line, "To: %s\n", mail->recipient); if ( dialog(connfd, line, NULL) ) break; if (mail->specialheaders[0]!= 0){ if ( dialog(connfd, mail->specialheaders, NULL) ) break; } sprintf(line, "Subject: %s\n", mail->subject); if ( dialog(connfd, line, NULL) ) break; if (mail->contenttype[0]!= 0){ sprintf(line, "Conten-Type: %s;\n", mail->contenttype); if ( dialog(connfd, line, NULL) ) break; } sprintf(line,"\tboundary=\"%s\"\n\n", mail->boundary); if ( dialog(connfd, line, NULL) ) break; sprintf(line, "%s\n", mail->boundary); if ( dialog(connfd, line, NULL) ) break; if (mail->contenttype[0]!= 0){ sprintf(line, "Conten-Type: %s;\n", mail->contenttype3); if ( dialog(connfd, line, NULL) ) break; } sprintf(line,"\tboundary=\"%s\"\n\n", mail->boundary2); if ( dialog(connfd, line, NULL) ) break; sprintf(line, "\n%s\n", mail->boundary2); if ( dialog(connfd, line, NULL) ) break; if (mail->contenttype1[0]!= 0){ sprintf(line, "Conten-Type: %s;\n", mail->contenttype1); if ( dialog(connfd, line, NULL) ) break; } if ( dialog(connfd, mail->contents, NULL) ) break; sprintf(line, "\n%s\n", mail->boundary2); if ( dialog(connfd, line, NULL) ) break; sprintf(line, "\n%s\n", mail->boundary); if ( dialog(connfd, line, NULL) ) break; 189

190 if (mail->contenttype2[0]!= 0){ sprintf(line, "Conten-Type: %s;name=\"%s\"\n", mail->contenttype2, mail->filename); if ( dialog(connfd, line, NULL) ) break; } sprintf(line,"content-transfer-encoding:%s\n", mail->encoding); if ( dialog(connfd, line, NULL) ) break; sprintf(line, " filename=\"%s\"\n\n", mail->filename); if ( dialog(connfd, line, NULL) ) break; if ( dialog(connfd, mail->attachments, NULL) ) break; sprintf(line, "\n%s--\n", mail->boundary); if ( dialog(connfd, line, NULL) ) break; //send mail-end if ( dialog(connfd, "\n.\n", "250") ) break; //QUIT if ( dialog(connfd, "QUIT\n", "221") ) break; goodmsg = 1; } while (0); close(connfd); return(goodmsg); } (2) 接收邮件函数 int recei(int s, int flag) { int n=0, i, j, k; int index; int size; char buf[bufsize]; //data buffer char cmd[bufsize]; //cmd buffer char buffer[bufsize]; char cache[bufsize]; char filename[100]; fd_set readfd; FILE* cin; FILE* cout; char* num; char* byt; char tp[30]; char* p = " "; char* rear; 190

191 k = 0; while (1){ //set tge flag and check the stats FD_ZERO(&readfd); FD_SET(s, &readfd); select( s+1, &readfd, (fd_set*)0, (fd_set*)0, (struct timeval*)0 ); //get the server's resp and print it if (FD_ISSET(s, &readfd)){ if ((n = recv(s, &buf[k], 1, 0)) <= 0){ fprintf(stderr, "connection closed.\n"); return 0; }else{ rear = &buf[k]; if (*rear == '\n'){ *rear = '\0'; printf("%s\n", buf); break; }else{ if (k > (BUFSIZE-1)) break; k++; } } } } sscanf(buf, "%s", cmd); if ((strcmp(cmd, "+OK"))!= 0){ return -1; } //get the num and byt of the msg if (flag == 4){ //check the resp of STAT strtok(buf, p); num = strtok(null, p); byt = strtok(null, p); msg_num = atoi(num); msg_byt = atoi(byt); } if (flag == 5){ //check the resp of RETR cin = fopen("mbox","a+"); //save a msg 191

192 while(1){ FD_ZERO(&readfd); FD_SET(s, &readfd); select(s+1, &readfd, (fd_set *) 0, (fd_set *) 0,(struct timeval *) 0 ); if(fd_isset(s, &readfd)){ n = recv(s, (void *)buf, (BUFSIZE-1), 0); if (n <= 0){ fprintf(stderr, "connection closed.\n"); fclose(cin); return 0; } else{ fwrite((const void *)buf, n, 1, (FILE *)cin); // exact the attachment //grab the filename for (i=1; i<n; i++){ if (buf[i] =='f'){ if (!strncmp(&buf[i], "filename=", 9)){ i += 10; index = 0; while((buf[i]!= 0x0d) && (buf[i]!= 0x0a)){ if(buf[i] == '"') i++; else filename[index++] = buf[i++]; } filename[index] = 0; break; } } } if(i == n){ printf("couldn't find the filename...\n"); return(-1); }else { printf("the filename was [%s]\n", filename); } //find the start of data and decode for(; i<n; i++){ //looking for a CR/LF on a blank line after the filename specification if ((buf[i] == 0x0d)&& (buf[i+1])== 0x0a && 192

193 (buf[i+2] == 0x0d)&& (buf[i+3] == 0x0a)){ i += 4; break; } } if (i == n){ printf("coulen't find the base64 MIME section...\n"); return (-1); } for(j=0; j<=bufsize; ){ cache[j++] = buf[i++]; if (buf[i] == '='){ cache[j] = buf[i]; size = j; break; } } //decode the data and save from64tobits((char *)buffer, (const char *)cache); cout = fopen(filename, "wb+"); fwrite((const void *)buffer, size, 1, (FILE *)cout); fclose(cout); //check end of mail if (strstr(buf, "\r\n.\r\n")!= NULL) break; } } } //while fclose(cin); } return 1; } 小结 据调查, 嵌入式系统相关的项目中绝大部分的工作都是和驱动开发密切相关的, 因此本章所涉及到的知识重要性不言而喻 大家在学习本章时, 要特别注意下面两个问题 : 第一, 在嵌入式 Linux 环境下, 设备也被当作文件来对待, 因此对设备的访问 193

194 实际上就是对一个文件的访问 第二, 设备驱动可以以模块的形式加载到内核中, 也可以静态的加载到内核中, 但通常情况下我们采用前者, 因为这样调试起来非常方便, 而且可以减少内核的大小 习题 1. 简述嵌入式操作系统中如何管理设备, 以 Linux 为例画出其体系结构示意图 2. 设备驱动开发和文件系统有何关联, 举出常用的几个接口函数, 并说明它们有何功能 3. 简述驱动开发的步骤 4. 通过本章的学习, 谈谈你对嵌入式应用程序开发和普通 PC 机上进行应用程序开发有哪些异同点 194

195 第六章 S3C44B0X 嵌入式微处理器 6.1 S3C44B0X 芯片概述 Samsung 公司推出的 16/32 位 RISC 处理器 S3C44B0X 为手持设备和一般类型应用提供了高性价比和高性能的微控制器解决方案 为了降低成本,S3C44B0X 提供了丰富的内置部件, 包括 :8KB cache, 内部 SRAM,LCD 控制器, 带自动握手 的 2 通道 UART,4 通道 DMA, 系统管理器 ( 片选逻辑,FP/EDO/SDRAM 控制器 ), 代用 PWM 功能的 5 通道定制器,I/O 端口,RTC,8 通道 10 位 ADC,IIC-BUS 接口,IIS-BUS 接口, 同步 SIO 接口和 PLL 倍频器 S3C44B0X 采用了 ARM7TDMI 内核,0.25um 工艺的 CMOS 标准宏单元和存储编译器 它的低功耗精简和出色的全静态设计特别适用于对成本和功耗敏感的应用 同样 S3C44B0X 还采用了一种新的总线结构, 即 SAMBAII( 三星 ARM CPU 嵌入式微处理器总线结构 ) S3C44B0X 的杰出特性是它的 CPU 核, 是由 ARM 公司设计的 16/32 位 ARM7TDMI RISC 处理器 (66MHZ) ARM7TDMI 体系结构的特点是它集成了 Thumb 代码压缩器, 片上的 ICE 断点调试支持, 和一个 32 位的硬件乘法器 S3C44B0X 通过提供全面的 通用的片上外设, 大大减少了系统电路中除处理器以外的元器件配置, 从而最小化系统的成本 2.5V ARM7TDMI 内核, 带有 8K 高速缓存器 (SAMBA II 总线体系结构, 主频高至 6MHz); 外部存储器控制器 (FP/EDO/SDRAM 控制, 片选逻辑 ); LCD 控制器 ( 最大支持 256 色 STN,LCD 具有专用 DMA); 2 通道通用 DMA 2 通道外设 DMA 并具有外部请求引脚 ; 2 通道 UART 带有握手协议 ( 支持 IrDA1.0, 具有 16-byte FIFO)/1 通道 SIO; 1 通道多主 IIC-BUS 控制器 ; 1 通道 IIS-BUS 控制器 ; 5 个 PWM 定时器和 1 通道内部定时器 ; 看门狗定时器 ; 71 个通用 I/O 口 /8 通道外部中断源 ; 功耗控制 : 具有普通, 慢速, 空闲和停止模式 ; 8 通道 10 位 ADC; 具有日历功能的 RTC; 具有 PLL 的片上时钟发生器 195

196 6.2 S3C44B0X 内部结构图 图 6-1 S3C44B0X 内部结构图 6. 3 芯片引脚定义 下图 6-2 是 S3C44B0X 的引脚分布图 : 196

197 各引脚信号描述如表 6-1 所示 : 总线控制 图 6-2 S3C44B0X 引脚分布图 信号输入 / 输出描述 OM[1:0] I 设置 S3C44B0X 测试模式和确定 ngcs0 的总线宽度, 逻辑电平 在复位期间由这些管脚的上拉下拉电阻确定 00:8-bit 01:16-bit 10:32-bit 11:Test mode ADDR[24:0] O 地址总线输出相应 bank 的存储器地址 DATA[31:0] I/O 数据总线, 总线宽度可编程为 8/16/32 位 197

198 ngcs[7:0] O 芯片选择, 当存储器地址在相应段的地址区域时被激活. 存取 周期和段尺寸可编程 nwe O 写允许信号, 指示当前的总线周期为写周期 nwbe[3:0] O 写字节允许信号 noe O 读允许信号, 指示当前的总线周期为读周期 nxbreq I 总线控制请求信号, 允许另一个总线控制器请求控制本地总线,nXBACK 信号激活指示已经得到总线控制权 nxback O 总线应答信号 nwait I nwait 请求延长当前的总线周期, 只要 nwait 为低, 当前的总线周期不能完成 ENDIAN I 它确定数据类型是 little endian 还是 big endian, 逻辑电平在复位期间由该管脚的上拉下拉电阻确定. 0:little endian 1:big endian DRAM/SDRAM/RAM nras[1:0] O 行地址选通信号 ncas[3:0] O 列地址选通信号 nsras O SDRAM 行地址选通信号 nscas O SDRAM 列地址选通信号 nscs[1:0] O SDRAM 芯片选择信号 DQM[3:0] O SDRAM 数据屏蔽信号 SCLK O SDRAM 时钟信号 SCKE O SDRAM 时钟允许信号 nbe[3:0] O 在使用 SRAM 情况下 16 位字允许信号 LCD 控制单元 VD[7:0] O LCD 数据线, 在驱动 4 位双扫描的 LCD 时,VD[3:0] 为上部显示区数据,VD[7:4] 为下部显示区数据 VFRAME O LCD 场信号, 指示一帧的开始, 在开始的第一行有效 VM O VM 极性变换信号, 变化 LCD 行场扫描电压的极性, 可以每帧或可编程多少个 VLINE 信号打开 VLINE O LCD 行信号, 在一行数据左移进 LCD 驱动器后有效 VCLK O LCD 点时钟信号, 数据在 VCLK 的上升沿发送, 在下降沿被 LCD 驱动器采样 TIMER/PWM TOUT[4:0] O 定时器输出信号 TCLK I 外部时钟信号输入 198

199 中断控制单元 EINT[7:0] I 外部中断请求信号 DMA nxdreq[1:0] I 外部 DMA 请求信号 nxdack[1:0] O 外部 DMA 应答信号 UART RxD[1:0] I UART 接收数据输入线 TxD[1:0] O UART 发送数据线 ncts[1:0] I UART 清除发送输入信号 nrts[1:0] O UART 请求发送输出信号 IIC-BUS IICSDA I/O IIC 总线数据线 IICSCL I/O IIC 总线时钟线 IISLRCK I/O IIS 总线通道时钟选择信号线 IISDO O IIS 总线串行数据输出信号 IISDI I IIS 总线串行数据输入信号 IISCLK I/O IIS 总线串行时钟 CODECLK O CODEC 系统时钟 SIO SIORXD I SIO 接收数据输入线 SIOTXD O SIO 发送数据线 SIOCK I/O SIO 时钟信号 SIORDY I/O 当 SIO 的 DMA 完成 SIO 操作时的握手信号 ADC AIN[7:0] AI ADC 模拟信号输入 AREFT AI ADC 顶参考电压输入 AREFB AI ADC 底参考电压输入 AVCOM AI ADC 公共参考电压输入 通用口线 P[70:0] I/O 通用 I/O 口 ( 一些口只有输出模式 ) Reset&Clock nreset ST 复位信号,nRESET 挂起程序, 放 S3C44B0X 进复位状态 在 电源打开已经稳定时,nRESET 必须保持低电平至少 4 个 MCLK 周期 199

200 OM[3:2] I OM[3:2] 确定时钟模式 00 = Crystal(XTAL0,EXTAL0), PLL on 01 = EXTCLK, PLL on 10, 11 = Chip test mode. EXTCLK I 当 OM[3:2] 选择外部时钟时的外部时钟输入信号线, 不用时 必须接高 (3.3V) XTAL0 AI 系统时钟内部振荡线路的晶体输入脚 不用时必须接高 (3.3V) EXTAL0 AO 系统时钟内部振荡线路的晶体输出脚, 它是 XTAL0 的反转输出信号 不用时必须悬空 PLLCAP AI 接系统时钟的环路滤波电容 (700PF) XTAL1 AI RTC 时钟的晶体输入脚 EXTAL1 AO RTC 时钟的晶体输出脚 它是 XTAL1 的反转输出信号 CLKout O 时钟输出信号 JTAG 测试逻辑 ntrst 输入 I TAP 控制器复位信号,nTRST 在 TAP 启动时复位 TAP 控制器 若使用 debugger, 必须连接一个 10K 上拉电阻, 否则 ntrst 必须为低电平 TMS 输入 I TAP 控制器模式选择信号, 控制 TAP 控制器的状态次序, 必须连接一个 10K 上拉电阻 TCK I TAP 控制器时钟信号, 提供 JTAG 逻辑的时钟信号源, 必须连接一个 10K 上拉电阻 TDI I TAP 控制器数据输入信号, 是测试指令和数据的串行输入脚, 必须连接一个 10K 上拉电阻 TDO O TAP 控制器数据输出信号, 是测试指令和数据的串行输出脚 电源 VDD P S3C44B0X 内核逻辑电压 (2.5V) VSS P S3C44B0X 内核逻辑地 VDDIO P S3C44B0X I/O 口电源 (3.3V) VSSIO P S3C44B0X I/O 地 RTCVDD P RTC 电压 (2.5V 或 3V, 不支持 3.3V) VDDADC P ADC 电压 (2.5V) VSSADC P ADC 地. 200

201 小结 S3C44B0X 是韩国三星公司推出的一款 ARM7TDMI 嵌入式微处理芯片, 这款 处理器接口丰富, 得到很广泛的使用 本章主要介绍了其基本体系结构以及外围接口, 读者在掌握这些知识后, 就可以继续学习后面的接口设计相关的知识了 在这里提醒读者 : 要进行嵌入式开发, 读懂核心芯片的 datasheet 是最为重要的事情 习题 1. 简述 S3C44B0X 的体系结构 2. 在学习 S3C44B0X 芯片内容后, 找一找市场上同类型的芯片, 比较一下它们都有哪些异同点 201

202 第 7 章 S3C44B0X 中断管理 7.1 S3C44B0X 中断管理概述 简介 S3C44B0X 的中断控制器可以接收来自 30 个中断源的请求 中断控制器的角色, 就是响应来自 FIQ 或 IRQ 的中断, 并请求内核对中断进行处理 当有多个中断 同时发生的时候, 中断控制器要决定首先处理哪一个中断 中断控制器 1. 中断模式 ARM7TDMI 有 2 种类型的中断模式,FIQ( 快速中断请求 ) 或 IRQ( 普通中断请求 ) 2. PSR 的 F 位和 I 位 PSR 指 ARM7TDMI 处理器的程序状态寄存器 如果 PSR 的 F 位被设置为 1, 处理器将不接受来自中断控制器的 FIQ 如果 PSR 的 I 位被设置为 1, 处理器将不接受来自中断控制器的 IRQ 因此, 为了使能中断相应机制,PSR 的 F 位或 I 位必须被清 0, 同时 INTMASK 的相应位必须被清 0 3. Pending 寄存器如果你将所有的中断源定义为 IRQ 中断 ( 通过设置 中断模式 ), 这时如果在同一时刻发生了 10 个中断请求, 你可以通过读取 pending 寄存器来了解哪些中断发生了, 并对产生的中断依次进行处理, 这也就是通过软件的方式 ( 即非向量中断方式 ) 决定了中断服务的优先级 Pending 寄存器中的位显示了某个中断请求是否还未被处理 一旦某个 pending 位被置位, 当 PSR 的 I 标志位或 F 标志位被清 0 时, 中断服务程序就会启动执行 Pending 寄存器是一个只读寄存器, 所以在服务程序中要想清除 pending 位时, 要采用在 I_ISPC 或 F_ISPC 中相应位写入 1 的方式实现 4. INTMSK 中断屏蔽寄存器如果该寄存器的某一个位被置 1, 则该位对应的中断响应被禁止了 如果某个中断在 INTMSK 寄存器的对应位为 0, 则这个中断发生时将会正常被响应 如果某个中断的在 INTMSK 的寄存器中的对应位为 1, 但是这个中断发生了, 它的 pending 位还 202

203 是会置位 如果全局屏蔽位被置 1, 那么当中断发生时, 中断 pending 位还是会置位, 但是所有的中断都不会得到服务 5. 中断源在 30 个中断源中, 有 26 个中断源提供给中断控制器 4 个外部中断 (EINT4/5/6/7) 请求是通过 或 的形式提供为 1 个中断源送至中断控制器,2 个 UART 错误中断 (UERROR0/1) 也是如此 如表 7-1 所示 表 7-1 S3C44B0X 中断源 注意 :EINT4, EINT5, EINT6 和 EINT7 分享同一个中断请求源 因此,ISR( 中 断服务程序 ) 要通过读取 EXTINPND[3:0] 寄存器来区别这 4 个中断源 它们的中断处理程序 (ISR) 必须在处理结束时将 EXTINPND[3:0] 中对应位写 1 来清除该位 6. 中断优先级产生模块 在前面介绍 Pending 位时, 说明了 S3C44B0X 通过软件方式决定中断优先级的方式 这种方式有一个缺陷, 就是在跳到相应的服务程序之前需要一个较长延迟时间 因此现在 S3C44B0X 还提供了另一种硬件决定中断优先级的方式 在多个中断源 同时请求中断时, 硬件优先级逻辑决定哪一个中断应该得到响应 同时, 这个硬件 203

204 逻辑提供矢量表的一个条跳转指令到 0x18( 或 0x1C), 在这个地址上提供了跳转到相应服务程序的跳转指令 与前一种软件方式相比, 这种方式将大大减少中断延迟 只有 IRQ 中断请求具有中断优先级产生模块 如果采用了矢量方式并且某个中断源在 INTMOD 寄存器中被设置为 IRQ 模式的中断, 那么中断优先级产生模块将会处理该中断 中断优先级产生模块包含 5 个单元 :1 个主单元和 4 个辅单元 每个辅 单元管理 6 个中断源 主单元管理 4 个辅单元和 2 个中断源 每个辅单元有 4 个可编程的优先级源 (sgn) 和 2 个固定的优先级源 (sgkn) 在每个辅单元的 4 个中断源的优先级都是可编程的 另外 2 个固定优先级的中断源 在这 6 个中优先级是最低的 主单元决定了 4 个辅模块和 2 个中断源之间的优先级排列 这里所说的 2 个中断源 :INT_RTC 和 INT_ADC, 在 26 个中断源中具有最低的优先级 图 7-1 优先级产生模块 7. 中断优先级如果中断源 A 被设置为 FIQ 中断而中断源 B 设置为 IRQ 中断, 那么源 A 比源 B 具有更高的中断优先级, 因为在任何情况下,FIQ 中断都比 IRQ 中断具有更高的优先级 ; 如果中断源 A 和中断源 B 在不同的 master groups 中 ( 参见表 5-4), 并且 A 所 在的 mastergroups 的优先级比 B 所在的 master groups 优先级高, 则中断源 A 的优先级肯定比中断源 B 的优先级高 如果中断源 A 和 B 在同一个 master groups 中, 且中断源 A 的优先级比 B 高, 则 A 具有更高的优先级 204

205 位于 sga, sgb, sgb, 和 sgd 的中断优先级总是高于位于 sgka 和 sgkb 的中断 在 sga,sgb, sgb, 和 sgd 之间的优先级的高低是可编程或者通过 round-robin 方式 来决定 在 sgka 和 sgkb 之间,sGKA 总是拥有更高的优先级 mga, mgb, mgc,mgd 组中的中断优先级总是高于 mgka 和 mgkb 因此,mGKA 和 mgkb 在所有中断源之中优先级是最低的 在 mga, mgb, mgc,mgd 组中, 优先级的 高低是可编程的或者通过 round-robin 方式来决定 在 mgka 和 mgkb 之间,mGKA 总是具有更高的优先级 8. 中断矢量地址 每个中断源对应的矢量地址如表 7-2 所示 表 7-2 中断源矢量地址表 中断控制器相关寄存器 下面我们来了解中断控制寄存器, 在实际编程中, 就是对这些寄存器进行读取 和设置来实现对中断的响应和控制 1. 中断控制寄存器 INTCON 中断控制寄存器 INTCON 的位定义如表 7-3 所示 205

206 表 7-3 中断控制寄存器 INTCON 位定义 2. 中断请求寄存器 INTPND INTPND 寄存器中的 26 个位对应着每一个中断源 当某个中断产生时,INTPND 中相应的 pending 位就会置 1, 说明该中断还未被处理 中断服务程序中必须清除该 pending 位, 从而使系统能够及时响应下一次中断 INTPND 是一个只读寄存器, 清除 pending 位的方式是向 I_ISPC/F_ISPC 的相应位写入 1 在多个中断同时发生时,INTPND 将所有发生的中断 pending 位都置 1 虽然中断请求可以通过 INTMSK 寄 存器屏蔽, 但是如果被屏蔽的中断发生了,INTPND 中的 pending 位仍然会被置 1 3. 中断模式寄存器 INTMOD INTMOD 寄存器中的 26 个位对应着每一个中断源 当 INTMOD 中的某个个位都设 置位 1, 则 ARM7TDMI 内核将以 FIQ( 快速中断 ) 模式操作那个中断 否则, 将以 IRQ ( 普通中断 ) 模式操作 4. 中断屏蔽寄存器 INTMSK 在 INTMSK 中, 除了全局屏蔽位, 其余的 26 位依次对应着每个中断源 当 INTMSK 的某个屏蔽位为 1 同时该位对应的中断事件发生了, 此时 CPU 事不会对中断请求进行响应的 如果屏蔽位为 0, 则 CPU 将对中断请求进行响应 如果全局屏蔽位为 1, 则所有的中断请求都不会被响应, 但是当中断发生时, 相应的 pending 位仍将被置 1 5. IRQ 矢量模式寄存器 优先级产生模块包括 5 个单元,1 个主单元和 4 个辅单元 每个辅单元管理 6 个中断源 主优先级产生单元管理 4 个辅单元和 2 个中断源 每个辅单元有 4 个可编程的优先级源 (sgn) 和 2 个固定优先级源 (kn) 每个辅单元的 4 个源中, 它们的优先级由 I_PSLV 寄存器决定 另外 2 个固定源的优先级在 6 个源中是最低的 主优先级产生单元通过 I_PMST 寄存器决定 4 个辅单元和 2 个中断源之间的优先级 2 个中断源 :INT_RTC 和 INT_ADC 在 26 个中断源中优先级是最低的 如果几个中断请求同时发生,I_ISPR 寄存器中将其中具有最高优先级的中断源 对应位置 1 206

207 寄存器的地址和描述如表 7-4 所示 表 S3C44B0X 平台下异常中断向量设计 在 S3C44B0X 平台下, 提供了两种中断模式 : 向量中断模式和非向量中断模式 可以通过设置中断控制寄存器 INTCON 的相应位来决定到底使用何种中断方式 下面 就这两种方式下中断的设计简单进行说明 向量中断模式 当 CPU 收到一个中断请求时,CPU 将执行位于 0x 处的指令 在向量中断模式下, 当 CPU 取得 0x 地址处的指令时, 中断控制器将一条跳转指令放在数 据总线上 对应不同的中断源, 这条跳转指令将赋给 PC 唯一对应的地址 例如, 如果外部中断 0 是一个 IRQ 中断, 当它发生时, 中断控制器将产生一个跳转指令将 PC 从指向 0x18 转为指向 0x20 典型中断处理程序实例如下 : ENTRY b ResetHandler ; 0x00 b HandlerUndef ; 0x04 b HandlerSWI ; 0x08 b HandlerPabort ; 0x0c b HandlerDabort ; 0x10 b. ; 0x14 b HandlerIRQ ; 0x18 b HandlerFIQ ; 0x1c ldr pc,=handlereint0 ; 0x20 ldr pc,=handlereint1 207

208 ldr pc,=handlereint2 ldr pc,=handlereint3 ldr pc,=handlereint4567 ldr pc,=handlertick ; 0x34 b. b. ldr pc,=handlerzdma0 ; 0x40 ldr pc,=handlerzdma1 ldr pc,=handlerbdma0 ldr pc,=handlerbdma1 ldr pc,=handlerwdt ldr pc,=handleruerr01 ; 0x54 b. b. ldr pc,=handlertimer0 ; 0x60 ldr pc,=handlertimer1 ldr pc,=handlertimer2 ldr pc,=handlertimer3 ldr pc,=handlertimer4 ldr pc,=handlertimer5 ; 0x74 b. b. ldr pc,=handlerurxd0 ; 0x80 ldr pc,=handlerurxd1 ldr pc,=handleriic ldr pc,=handlersio ldr pc,=handlerutxd0 ldr pc,=handlerutxd1 ; 0x94 b. b. ldr pc,=handlerrtc ; 0xa0 b. b. b. b. b. b. ldr pc,=handleradc ; 0xb4 208

209 7.2.2 非向量中断模式 当 CPU 收到一个中断请求时,CPU 仍将执行位于 0x 处的指令, 通过该指 令进入中断服务程序 在中断服务程序中, 通过对 I_ISPR/F_ISPR 寄存器的分析判断出中断的类型, 然后再把 PC 指向相应的中断处理程序 典型示例如下 : ENTRY b ResetHandler ; for debug b HandlerUndef ; handlerundef b HandlerSWI ; SWI interrupt handler b HandlerPabort ; handlerpabort b HandlerDabort ; handlerdabort b. ; handlerreserved b IsrIRQ b HandlerFIQ IsrIRQ sub sp,sp,#4 ; reserved for PC stmfd sp!,{r8-r9} ldr r9,=i_ispr ldr r9,[r9] mov r8,#0x0 0 movs r9,r9,lsr #1 bcs %F1 add r8,r8,#4 b %B0 1 ldr r9,=handleadc add r9,r9,r8 ldr r9,[r9] str r9,[sp,#8] ldmfd sp!,{r8-r9,pc} HandleADC # 4 HandleRTC # 4 HandleUTXD1 # 4 HandleUTXD0 # HandleEINT3 # 4 209

210 HandleEINT2 # 4 HandleEINT1 # 4 HandleEINT0 # 4 ; 0xc1(c7)fff 外部中断实例 假设开发板上有 4 个按键, 它们分别与 EXINT4~7 引脚相连 配置好相应 I/O 口使其工作在外部中断模式下, 然后通过某个按键来触发中断, 中断触发后打印相应消 息到控制台 1. 电路设计 图 7-2 外部键盘中断电路 其中 KEYIN0~KEYIN3 和芯片 S3C44B0X 引脚 EXINT4~7 连接对应关系为 : KEYIN0< > EXINT4(GP4) KEYIN1< > EXINT5(GP5) 2.I/O 口设置 KEYIN2< > EXINT6(GP6) KEYIN3< > EXINT7(GP7) 因为四个按键的引脚分别接到芯片 S3C44B0X 的 PG 口相应引脚上, 因此必须首先对 PG 口的工作模式进行设置, 让 PG4~PG7 工作在外部中断输入状态 通过查看芯片的 datasheet 可知应采用如下语句 : rpcong = 0xff00 其中 rpcong 为 PG 口地址, 定义如下 : #define rpcong 3. 外部中断触发模式设置因为中断触发模式可以有下列三种 : 电平触发 (*(volatile unsigned *)0x1d20040) 210

211 上升沿触发 下降沿触发 利用外部中断控制器寄存器来设置外部中断的触发模式, 假设采用上升沿触发, 则中断控制寄存器做下列设置 : rextint = 0X 其中,rEXTINT 为中断控制寄存器地址, 定义如下 : #define rextint 4. 中断寄存器设置 void intmain(void) { (*(volatile unsigned *)0x1d20050) 将 EINT4~7 设置为 IRQ 中断模式, 并采用非向量中断模式, 禁止 FIQ 中断, 使能 IRQ 中断, 语句如下 : rintcon = 0x5 rintmsk = ~(0x ) 其中 rintcon 和 rintmsk 定义如下 : #define rintcon (*(volatile unsigned *)0x1e00000) #define rintmsk (*(volatile unsigned *)0x1e0000c) 5. 主程序设计 volatile char IntNumber=0; pisr_eint4567=(int) ExIntIsr; rpcong = 0xff00 rextint = 0X rintcon = 0x5 rintmsk = ~(0x ) printf( press the button,please\n ); whiel (!IntNumber); switch(intnumber) { case 1: printf( s4 button is pressed and EXINT4 occured\n ); break; case 2: printf( s3 button is pressed and EXINT5 occured\n ); break; case 3: printf( s2 button is pressed and EXINT6 occured\n ); break; 211

212 case 4: printf( s1 button is pressed and EXINT7 occured\n ); break; } } 6. 中断服务程序设计 void irq ExIntIsr(void) { } IntNumber= rextintpnd; rextintpnd=0x0f; ri_ispc=bit_eint4567; //clear EXTINTPND reg. //clear pending_bit 小结 本章主要介绍了 S3C44B0X 中断相关的知识, 包括下列重要知识点 : S3C44B0X 中断控制器体系结构 S3C44B0X 中断控制寄存器 S3C44B0X 中断向量表设计 外部中断举例中断是嵌入式知识体系中最为灵活 也最为重要的知识点之一, 读者应该充分重视本章知识点的学习 习题 1. 简述中断机制的原理和用途 2.S3C44B0X 中断方式有哪几种? 它们之间有何异同? 3. 如果考虑到 uclinux 操作系统下的中断处理机制, 如何在 BootLoader 中设计中断程序 ( 以非向量中断默认为例 ) 212

213 第 8 章 S3C44B0X 存储设计 8.1 S3C44B0X 存储系统概述 S3C44B0X 存储系统特性 S3C44B0X 本身并没有内置存储模块, 也就是说, 它的所有存储系统必须依靠外扩得到 在 S3C44B0X 芯片内部有一个存储控制器, 通过该存储控制器, 用户能够很 方便的把外扩存储部件挂接到 S3C44B0X 微处理器上 S3C44B0X 内部存储控制器的作用是为外部存储器操作提供必要的控制信号, 具有以下特性 : 小 / 大端选择 ( 通过外部引脚选择 ) 地址空间 :S3C44B0X 提供了最大 256MB 存储空间, 分为 8 个 bank 体, 其中每个 bank 体最大空间为 32MB 所有 bank 都具有可编程的总线宽度 (8/16/32 位 ) 总共 8 个存储器 banks, 其中 6 个 bank 可用作 ROM SRAM 类型存储器映射空间, 另外 2 个 bank 可做为 FP/EDO/SDRAM 类型存储器映射空间 8 个存储器 bank 体中, 其中 7 个 bank 体的起始地址固定 大小可编程, 另外 1 个 bank 体具有灵活起始地址 大小可编程 对所有的存储器 bank, 具有可编程的操作周期 采用外部等待来扩展总线周期 专用 DRAM/SDRAM 接口支持自刷新模式 支持异步和同步 DRAM 图 8-1 是 S3C44B0X 复位后的存储器映射表, 其中 BANK6/BANK7 的地址表如表 8-1 所示 213

214 图 8-1 S3C44B0X 复位后的内存映射图 表 8-1 BANK6/BANK7 地址 address 2MB 4MB 8MB 16MB 32MB BANK6 Start address 0xc00_0000 0xc00_0000 0xc00_0000 0xc00_0000 0xc00_0000 End address 0xc1f_ffff 0xc3f_ffff 0xc7f_ffff 0xcff_ffff 0xdff_ffff BANK7 Start address 0xc20_0000 0xc40_0000 0xc80_0000 0xd00_0000 0xe00_0000 End address 0xc3f_ffff 0xc7f_ffff 0xcff_ffff 0xdff_ffff 0xfff_ffff 大小端模式选择 S3C44B0X 具有一个输入引脚 ENDIAN, 处理器通过它的输入逻辑电平来确定数据类型是小端还是大端, 逻辑电平在复位期间由该管脚的上拉或下拉电阻确定 214

215 8.1.3 Bank0 总线宽度 BOOT ROM 在地址上位于 ARM 处理器的 Bank0 区, 它可能具有多种数据总线 宽度, 这个宽度可以通过硬件来设定, 即通过 OM[1:0] 引脚上的逻辑电平值确定 bank0 的数据总线宽度, 如表 8-2 所示 : 表 8-2ROM bank 0 的数据总线宽度设定 OM[1:0] 数据总线宽度 00 8bit(byte) 01 16bit(half word) 10 32bit(word) 11 Test Mode 8.2 SDRAM 设计 SDRAM 器件原理 在嵌入式系统中使用到的大量 DRAM 都是 SDRAM SDRAM 的存储位单元的基本原理和 DRAM 基本相同, 但是这些存储位单元的组织和控制与 DRAM 的差别就比较大 首先,SDRAM 是同步内存, 因此它的接口信号中多了系统 CLK 信号, 所有数据 地址和控制信号都是和 CLK 的上升沿对齐的 ; 另外,SDRAM 内部的控制逻辑更加复杂, 集成了一个命令控制器, 处理器访问 SDRAM 都是通过向 SDRAM 发送命令来实现的 SDRAM 是多 bank 结构, 例如在一个具有两个 bank 的 SDRAM 的模组中, 其中一个 bank 在进行预充电期间, 另一个 bank 却可以被读取数据, 这样当进行一次读取后, 又马上可以去读取已经预充电完毕的那个 bank 的数据, 无需等待, 可以提 高存储器的访问速度 电路设计 HY57V 是同步 CMOS 型 SDRAM, 由 4 个 bank 构成, 其中每个 bank 的容量为 1M 16 位, 工作频率在 100HHz~183MHz 之间, 图 8-2 为其芯片引脚电路 图 215

216 图 8-2 HY57V 芯片引脚封装 1.BA 地址线在图 8-2 中, 有两个 BA 地址线 :BA0 和 BA1 它为 SDRAM 内部 bank 的地址 线, 用于寻址每一个 bank 假如 SDRAM 为 16MB, 则需要 24 根地址线引脚来寻址, 那么此时 BA1~BA0 就要接到 A23~A22 引脚上 BA1 BA0 表 8-3 BA 地址线对应 bank 体 寻址 bank 体 0 0 寻址 bank0 0 1 寻址 bank1 1 0 寻址 bank2 1 1 寻址 bank3 2.A0~A11 地址线 A0 地址线接到处理器的哪一个脚上, 要视具体情况而定 比如 S3C44B0X 为 32 位处理器, 如果 SDRAM 为 16 位宽度时, 就应该采用图 8-3 的接法 216

217 图 位 SDRAM 硬件链接图 如果 SDRAM 为 32 位的话, 则需要 2 块 16 位 SDRAM 联合起来使用, 其硬件设计如图 8-4 所示 图 位 SDRAM 硬件电路设计 编程实例 1. 寄存器设置 SDRAM 硬件电路设计好后, 还需要相应软件配合才能正常工作, 在 S3C44B0X 217

218 中, 和内存相关的寄存器有 BWSCON BANKCONn 和 REFRESH 等寄存器 关于这些寄存器如何设置, 请参考本书第 11 章 bootloader 设计相关内容 2. 对特定地址的写入和读出操作如下示例 : #define _WR(addr,data) *((U16 *)(addr<<1))=(u16)data #define _RD(addr) (*(U16 *)(addr << 1)) 8.3 Flash 设计 Flash 概述 S3C44B0X 本身并不具有 ROM, 因此必须外接 ROM 器件, 用来存储整个系统掉电后仍需要保存的信息, 如 bootloader 操作系统内核 文件系统以及其它应用程 序和数据 闪存 (flash memory) 是非易失性器件, 且可以轻易擦写, 非常适合于嵌入式系统中使用 目前,NOR flash 和 NAND flash 是现在市场上两种主要的非易失闪存技 术 其中 NOR flash 技术由 Intel 于 1988 年首先开发出, 彻底改变了原先由 EPROM 和 EEPROM 一统天下的局面, 紧接着, 东芝公司于 1989 年发表了 NAND flash 结构, 强调降低每比特的成本, 提供更高的性能, 并且象磁盘一样可以通过接口轻松升级 NOR flash 的特点是芯片内执行 (xip, executc hiplace), 应用程序可以直接在 flash 闪存内运行, 不必再把代码读到系统 RAM 中 NOR flash 的传输效率很高, 小容量情况下具有很高的成本效益, 但是 NOR flash 写入和擦除速度很低, 极大影响了它的性能 NANDflash 是高存储密度数据的理想解决方案, 并且写入和擦除的速度也很快 但是使用 NAND flash 必须具有特殊的接口 下面从几个方面介绍这两种 flash 技术之间的差别 1. 硬件接口 NOR flash 带有 SRAM 接口, 有足够的地址引脚来寻址, 可以很容易地存取其内部的每一个字节 NAND flash 器件使用复杂的 IO 口来串行的存取数据, 同时通过这些 IO 口传递控制和地址等信息 NAND flash 读和写操作以块为单位进行, 每块 512 字节, 这和硬盘操作类似, 因此基于 NAND flash 的存储器就可以取代硬盘或其它块设备 2. 擦除 / 写入操作 flash 闪存是非易失存储器, 可以对存储器单元块进行擦写和再编程 但是任何 flash 器件的写入操作只能在空或已擦除的单元内进行, 所以大多数情况下, 在进行写入操作之前必须先执行擦除 218

219 NAND flash 器件执行擦除操作非常简单, 而 NOR flash 则要求在进行擦除前先要将目标块内所有的位都写为 0 擦除 NOR flash 器件是以 KB 的块为单位进行, 执行一个写入 / 擦除操作的时间为 55ms, 与此相反, 擦除 NAND flash 器件是以 8-32KB 的块为单位进行, 执行相同的操作最多只需要 4ms 执行擦除时块尺寸的不同进一步拉大了 NOR 和 NADN 之间的性能差距, 统计表明, 对于给定的一套写入操作 ( 尤其是更新小文件时 ), 更多的擦除操作必须在基于 NOR 的单元中进行 因此当选择存储解决方案时, 设计师必须权衡以下的各项因素 : NOR flash 的读速度比 NAND flash 稍快一些 NAND flash 的写入速度比 NOR flash 快很多 NAND flash 的 4ms 擦除速度远比 NOR flash 的 55ms 快 大多数写入操作需要先进行擦除操作 NAND flash 的擦除单元更小, 相应的擦除电路更少 3. 容量 成本和使用寿命 NAND flash 的单元尺寸几乎是 NOR flash 器件的一半, 由于生产过程更为简单, NAND flash 结构可以在给定的模具尺寸内提供更高的容量, 也就相应地降低了价格 NOR flash 占据了容量为 1-16MB 闪存市场的大部分, 而 NAND flash 多用在 8-128MB 的产品当中, 这也说明 NOR flash 主要应用在代码存储介质中, 而 NAND flash 更适合于数据存储 NAND flash 闪存中每个块的最大擦写次数是一百万次, 而 NOR flash 的擦写次数是十万次 NAND flash 存储器除了具有 10 比 1 的块擦除周期优势, 典型的 NAND flash 块尺寸要比 NOR flash 器件小 8 倍, 每个 NAND 存储器块在给定的时间内的 删除次数要少一些 Nor Flash 1. 硬件设计 S3C44B0X 已经提供了较为完善的 flash 接口, 可以很方便的和市场上各种 NOR flash 芯片链接 如在 S3C44B0X 实验平台上使用了一片 SST39VF160 芯片, 其硬件电路如图 8-5 所示 219

220 图 8-5 NOR Flash 硬件设计原理图 从图 8-5 可以看出, 因为 SST39VF160 芯片的数据宽度是 16 位的, 而 S3C44B0X 按照字节进行编址, 因此处理器的 ADDR1 与 SST39VF160 的 ADDR0 相连 同时, SST39VF160 映射在处理器的 BANK0 区域内, 因此其片选信号 nce 与处理器的 ngcs0 相连 2.NOR Flash 软件设计当 S3C44B0X 复位后,SST39VF160 就可以使用了, 它映射到处理器的 BANK0 区, 地址从 0X ~0X , 共 2MB 也就是说,NOR flash 器件其实并不需要什么软件驱动支持, 它的读 / 写 / 擦除等操作按照该芯片手册上的时序图来进行, 下面分别讨论这些操作 (1) 芯片识别 SST39VF160 器件具有一个产品鉴定识别码 (ID), 系统控制器可以读出这个 ID, 从而起到识别器件的作用 根据 SST39VF160 的使用手册, 读 ID 的代码如下 #define ROM_BASE 0 static U32 GetFlashID(void) { U32 i; 220

221 } outportw(0xaaaa, ROM_BASE+0xaaaa);//CMD_ADDR0 = 0xaaaa; outportw(0x5555, ROM_BASE+0x5554);//CMD_ADDR1 = 0x5555; outportw(0x9090, ROM_BASE+0xaaaa);//CMD_ADDR0 = 0x9090; //i = *(U16 *)(0x0+ROM_BASE);i = (*(U16 *)(2+ROM_BASE))<<16; i = inportw(rom_base); i = inportw(rom_base+2)<<16; SWPIDExit(); return i; (2) 芯片擦除 SST39VF160 具有 3 种擦除方式 : 整片擦除 扇区擦除和块擦除 这里仅介绍扇区擦除, 其它两种方式比较类似, 读者可以参考该芯片的手册来设计 static int SectorErase(U32 sector) { U32 tm, d1,d2; sector += ROM_BASE; outportw(0xaaaa, ROM_BASE+0xaaaa); outportw(0x5555, ROM_BASE+0x5554); outportw(0x8080, ROM_BASE+0xaaaa); outportw(0xaaaa, ROM_BASE+0xaaaa); outportw(0x5555, ROM_BASE+0x5554); outportw(0x3030, sector); d2 = inportw(sector); tm = CHECK_DELAY; while(1) { tm--; if(!tm) return -1; d1 = d2; d2 = inportw(sector); if((d1^d2)&(1<<6)) { } continue; //D6 == D6 if(inportw(sector)==0xffff) { 221

222 } } return 0; } (3) 芯片写入 SST39VF160 芯片在写入前, 必须先把整片擦除 U32 WriteSector(U32 index,u8 *buff) { volatile U16 *Dest; volatile U16 *Org; U16 i,k; Org = (volatile U16 *)buff; Dest = (volatile U16 *)(GetSecAddr(index)); *(volatile U16 *)(0x0000AAAA)=0xAAAA; *(volatile U16 *)(0x )=0x5555; *(volatile U16 *)(0x0000AAAA)=0x8080; *(volatile U16 *)(0x0000AAAA)=0xAAAA; *(volatile U16 *)(0x )=0x5555; *Dest =0x3030; for(k=0;k<65000;k++); for(k=0;k<65000;k++); //for(k=0;k<65000;k++); printf("."); if (!buff) return 0; for (i=0;i<seclen;i++) { *(volatile U16 *)(0x0000AAAA)=0xAAAA; } *(volatile U16 *)(0x )=0x5555; *(volatile U16 *)(0x0000AAAA)=0xA0A0; *(Dest+i)=*(Org+i); for(k=0;k<200;k++); if(*(dest)!=*(org)) return 0; 222

223 } return SECLEN; 3.NOR Flash 在 Linux 系统下的使用上面介绍的关于 NOR Flash 基本函数的实现, 经过简单修改就可以移植到 Linux 系统下使用 但是这样做有很多缺点 : 首先, 程序员必须自己管理 Flash, 已有的许多文件系统并不能够直接使用 ; 同时,Flash 设备的读写有一定的次数限制, 如果没有专门的文件系统来管理, 不仅读写效率低, 而且会减少 Flash 的使用寿命 Linux 下驱动程序的设计可以分为两大类 : 字符设备驱动程序设计和块设备驱动程序设计 字符设备以字节为单位进行读写, 而块设备则是以块为单位进行读写 Flash 恰好具有这两者的特点, 比如 Flash 读操作以字节为单位进行, 而写操作 ( 因 为首先要以块为单位进行擦除 ) 则是以块为单位进行 为了更好的支持 Flash 设备, Linux 系统中提供了一类新的机制 -MTD(Memory Technology Device) MTD 的基本思想是在文件系统和下层硬件之间建立一个抽象的映像层, 屏蔽下层的硬件特性 MTD(memory technology device 内存技术设备 ) 是用于访问内存设备 (ROM 或 flash) 的 Linux 的子系统 MTD 的主要目的是为了使新的内存设备的驱动更加简单, 为此它在硬件和上层之间提供了一个抽象的接口 MTD 设备分为四层 ( 从设备节点 直到底层硬件驱动 ): 设备节点 MTD 设备层 MTD 原始设备层和硬件驱动层 根文件系统 文件系统 字符设备节点 块设备节点 MTD 字符设备 MTD 块设备 MTD 原始设备 FLASH 硬件驱动 图 8-6 MTD 分层示意图 Flash 硬件驱动层 : 硬件驱动层负责在 init 时驱动 Flash 硬件,Linux MTD 设备的 NOR Flash 芯片驱动遵循 CFI 接口标准, 其驱动程序位于 drivers/mtd/chips 子目录下 NAND 型 Flash 的驱动程序则位于 /drivers/mtd/nand 子目录下 MTD 原始设备 : 原始设备层有两部分组成, 一部分是 MTD 原始设备的通用代 223

224 码, 另一部分是各个特定的 Flash 的数据, 例如分区 用于描述 MTD 原始设备的数据结构是 mtd_info, 在这个数据结构中定义了大量 的关于 MTD 的数据成员和操作函数 mtd_table 则是所有 MTD 原始设备的列表, mtd_part 用于表示 MTD 原始设备分区的结构, 其中包含了 mtd_info, 因为每一个分区都是被看成一个 MTD 原始设备加在 mtd_table 中的,mtd_part.mtd_info 中的大部 分数据都从该分区的主分区 mtd_part->master 中获得 在 drivers/mtd/maps/ 子目录下存放的是特定的 flash 的数据, 每一个文件都描述了一块板子上的 flash 其中调用 add_mtd_device() del_mtd_device() 建立 / 删除 mtd_info 结构并将其加入 / 删除 mtd_table ( 或者调用 add_mtd_partition() del_mtd_partition() 建立 / 删除 mtd_part 结构并将 mtd_part.mtd_info 加入 / 删除 mtd_table 中 ) MTD 设备层 : 基于 MTD 原始设备,linux 系统可以定义出 MTD 的块设备 ( 主设备号 31) 和字符设备 ( 设备号 90) MTD 字符设备的定义位于文件 mtdchar.c 中, 实际上就是编程实现了一系列 file operation 接口函数 (lseek open close read write) MTD 块设备则是定义了一个描述 MTD 块设备的结构 mtdblk_dev, 并声明了一个名为 mtdblks 的指针数组, 这数组中的每一个 mtdblk_dev 和 mtd_table 中的每一个 mtd_info 一一对应 设备节点 : 通过 mknod 在 /dev 子目录下建立 MTD 字符设备节点 ( 主设备号为 90) 和 MTD 块设备节点 ( 主设备号为 31), 通过访问此设备节点即可访问 MTD 字符设备和块设备 注意 : 要在 Linux 系统下使用 MTD 设备, 首先在编译内核时要加上相关设备选项, 比如 MTD 和 JFFS2 的支持, 并且修改 drivers/mtd/maps/physmap.c 文件, 在该文件中增加分区 这样系统启动后, 只需执行下列命令 : mount t jffs2 /dev/mtdblock1 /mnt 就可以在 /mnt 目录下方便的使用 Flash 来存储各种数据和程序了 NAND Flash NAND Flash 存储器是 flash 存储器中一种技术规格, 其内部采用非线性宏单元 模式, 为固态大容量存储器的实现提供了廉价有效的解决方案, 因而得到非常广泛的应用 目前, 生产 NAND Flash 的厂家比较多, 如 Samsung TOSHIBA 和 Fujistu 等都是 NAND Flash 主要的芯片生产厂商, 下面就以 Sansung 公司生产的一款 K9F5608U0B 为例来介绍 NAND Flash 的硬件和软件设计及使用 1.NAND Flash 组织结构 K9F5608U0B 的容量为 264Mb, 其组织形式为 : 共有 页 ( 行 ), 每页由 528 个字节 ( 列 ) 构成, 其中 512 个字节用于存放数据,16 字节用于存放其它信息 ( 如块的逻辑地址 前面 512 字节的 ECC 校验和等 ) 备用的 16 列, 位于列地址的 512~527, 其功能模块图如下所示 224

225 图 8-7 K9F5608U0B 功能模块图 前面讲过,NAND Flash 的基本操作单位为块, 每一块由 32 页组成, 因此 K9F5608U0B 中一共有 2048 块, 这种 块 - 页 的结构, 恰好能满足文件系统中划分族和扇区的结构要求,K9F5608U0B 的内部结构如图 8-8 所示 图 8-8 K9F5608U0B 阵列组织结构 2.NAND Flash 接口信号如表 8-4 所示,NAND Flash 接口信号非常少, 和 NOR Flash 相比, 其数据线宽度只有 8bit, 没有地址线, 另外多了 CLE 和 ALE 两个信号线用来区分总线上的数据 类别 225

226 表 8-4 K9F5608U0B 接口信号列表 引脚信号名称 信号描述 I/O0~I/O7 这 8 个引脚线用来输入指令 地址信息, 在读周期时输出数据信息 /CE 芯片使能信号, 低电平表示选中该芯片 CLE 命令锁存信号, 写操作时给出此信号表示进行写命令操作 ALE 地址 / 数据锁存信号, 写操作时给出此信号表示进行写地址或数据操作 /RE 读使能, 低电平表示当前总线为读操作 /WE 写使能, 低电平表示当前总线操作为写操作 /WP 写保护信号 R/B 读 / 忙输出 V CC 缓冲器输出电源 V SS 地 K9F5608U0B 设备容量为 32MB, 那么访问其中内容, 需要 25 根地址线, 但是其引脚只有 IO0~IO7 共 8 根线, 因此必须经过 3 个时钟周期才能把全部地址信息接收 下来, 表 8-5 给出了每个时钟周期, 地址线上每个 bit 所对应的地址 表 8-5 K9F5608U0B 地址周期示意 I/O0 I/O1 I/O2 I/O3 I/O4 I/O5 I/O6 I/O7 第一个周期 A0 A1 A2 A3 A4 A5 A6 A7 列地址 第二个周期 A9 A10 A11 A12 A13 A14 A15 A16 第三个周期 A17 A18 A19 A20 A21 A22 A23 A24 行地址 从上表可以看出, 第一个时钟周期给出的是目标地址在一个页内的偏移量, 而后面三个时钟周期给出的是页地址 由于一个页内有 512B, 需要 9bit 地址寻址, 第 226

227 一个时钟周期只给出了低 8bit, 最高位 A8 由不同的读命令来区分 K9F5608U0B 的的 CLE 和 ALE 信号用来实现 I/O 口上指令和地址的复用 指令 地址和数据都通过拉低 /WE 和 /CE, 通过 I/O 口写入器件中 3.NAND Flash 命令在 K9F5608U0B 中, 提供了下列指令用于操作 K9F5608U0B 其中有些指令只 需要一个总线周期就能完成, 例如, 复位指令 读指令等 而有些指令就需要 2 个总线周期才能完成, 其中一个周期用于启动, 而另一个周期用来执行 表 8-6 描述了 K9F5608U0B 具备的指令 表 8-6 K9F5608U0B 的指令及其功能 命令名称 第一个周期得到第二个周期器件忙时可得到的命令 读方式 1 00h/01h 读方式 2 50h 读芯片 ID 号 90h 复位 FFH 0 页写入 80H 10H 回拷贝 00H 8AH 块擦除 60H D0H 读当前状态 70H 0 具体命令操作可参考其 K9F5608U0B 芯片的使用手册 4.NAND Flah 电路设计 K9F5608U0B 和 S3C44B0X 的接口电路设计如图 8-9 所示 227

228 图 8-9 K9F5608U0B 和 S3C44B0X 的接口电路 其中,SMCWE 和 SMCOE 信号通过图 8-10 电路产生 5. 软件设计 图 8-10 控制信号产生电路 由于对 NAND Flash 的操作基本上是由 /CE CLE 和 ALE 三条信号线的逻辑电平决定的, 下面定义的几个接口函数描述了对这几个引脚的控制 (1) 宏定义 #define NAND_DAT 0x #define NAND_ALE 0x #define NAND_CLE 0x #define READCMD0 0 #define READCMD1 1 #define READCMD2 0x50 #define ERASECMD0 0x60 #define ERASECMD1 0xd0 228

229 #define PROGCMD0 0x80 #define PROGCMD1 0x10 #define QUERYCMD 0x70 #define READIDCMD 0x90 (2) 接口函数设计 因为 /CE 脚由 GPC9 控制, 所以要使 /CE 脚使能, 可以采取 GPC9 口置低, 代码如下 : void NFChipSel(U32 sel) { } if(sel) rpdatc &= ~(1<<9); else rpdatc = 1<<9; 同样, 要向 NAND Flash 写入地址 命令和数据时可采用下列接口函数 向其写入命令 : void NFWrCmd(int cmd) { } *(volatile U8 *)NAND_CLE = cmd; 向其写入地址 : void NFWrAddr(int addr) { *(volatile U8 *)NAND_ALE = addr; } 向其写入数据 : void NFWrDat(int dat) { *(volatile U8 *)NAND_DAT = dat; } (3) 读器件 ID 接口函数 K9F5608U0B 器件具有一个产品鉴定识别码 (ID), 系统控制器可以读出这个 ID, 从而起到识别器件的作用 根据 K9F5608U0B 的 datasheet, 读 ID 的步骤如下 : 首先, 写入 90H 指令, 然后写入一个地址 00H, 在两个读周期下, 厂商代码和 器件代码将被连续输出到 I/O 口 参考代码如下 : static U32 NFReadID(void) { U32 id, loop = 0; 229

230 NFChipEn();// 使芯片使能 NFWrCmd(READIDCMD);// 写入命令 90H NFWrAddr(0);// 首先引脚 ALE 有效, 然后写入地址 00H while(nfisbusy()&&(loop<10000)) loop++; if(loop>=10000) return 0; id = NFRdDat()<<8;// 读出厂商代码和器件代码 id = NFRdDat(); NFChipDs(); return id; } (4) 块擦除子程序 K9F5608U0B 在写入之前必须被擦除, 擦除以块为单位进行 代码如下 : static U32 NFEraseBlock(U32 addr) { U8 stat; addr &= ~0x1f; NFChipEn(); NFWrCmd(ERASECMD0); NFWrAddr(addr); NFWrAddr(addr>>8); if(nandaddr) NFWrAddr(addr>>16); NFWrCmd(ERASECMD1); stat = NFWaitBusy(); NFChipDs(); #ifdef ER_BAD_BLK_TEST if(!((addr+0xe0)&0xff)) stat = 1; //just for test bad block #endif printf("erase block 0x%08x %s\n", addr, stat?"fail":"ok"); } return stat; 230

231 (5) 页写入子程序 //addr = page address static U32 NFWritePage(U32 addr, U8 *buf) { U16 i, stat; NFChipEn(); NFWrCmd(PROGCMD0); NFWrAddr(0); NFWrAddr(addr); NFWrAddr(addr>>8); if(nandaddr) NFWrAddr(addr>>16); for(i=0; i<512; i++) NFWrDat(buf[i]); if(!addr) { NFWrDat('b'); NFWrDat('o'); } NFWrDat('o'); NFWrDat('t'); NFWrCmd(PROGCMD1); stat = NFWaitBusy(); NFChipDs(); if(stat) printf("write nand flash 0x%x fail\n", addr); else { } U8 RdDat[512]; NFReadPage(addr, RdDat); for(i=0; i<512; i++) if(rddat[i]!=buf[i]) { printf("check data at page 0x%x, offset 0x%x fail\n", addr, i); stat = 1; } break; } return stat; (6) 页读出子程序 231

232 static void NFReadPage(U32 addr, U8 *buf) {//addr = page address } U16 i; NFChipEn(); NFWrCmd(READCMD0); NFWrAddr(0); NFWrAddr(addr); NFWrAddr(addr>>8); if(nandaddr) NFWrAddr(addr>>16); NFWaitBusy(); for(i=0; i<512; i++) buf[i] = NFRdDat(); NFChipDs(); 小结 本章主要介绍了 S3C44B0X 存储管理机制, 并对 SDRAM 和 flash 设计进行了较为详细的描述 习题 1. 问答题 (1) 目前市场上常见的 flash 有哪几种, 它们之间有何不同? (2)S3C44B0X 为 32 位处理器, 如果 SDRAM 为 16 位宽度, 两者之间硬件电路该如何连接? 另外考虑 SDRAM 分别为 8 位和 32 位时又该如何设计? 232

233 第 9 章串口设计 串行通讯口 (Serial Communication Port) 在通用计算机系统和嵌入式系统中都 处于非常重要的地位, 虽然计算机和嵌入式系统不停的更新, 但是串口始终得到保留 串行通讯采用同步或异步两种方式, 通常情况下采用异步方式, 因此采用 9 针 的插头就可以了, 下面就是采用 RS232 标准的串口头形状和各个脚定义 图 针串口外形和各脚定义 9.1 S3C44B0X 串口概述 S3C44B0X 的 UART( 通用异步收发器 ) 单元提供两个独立的异步串行 I/O 端口, 每个都可以在中断或 DMA 模式下工作 它们支持的最高波特率为 115.2Kbps 每个 UART 通道包含 2 个 16 位 FIFO 部件, 分别用来接收数据和发送数据 S3C44B0X 的 UART 可以进行以下参数的设置 : 可编程的波特率, 红外收 / 发模式,1 或 2 个停止位, 奇偶位校验,5 位 6 位 7 位或 8 位数据宽度选择 每个 UART 包含波特率产生器 发送器 接收器和控制单元等部件 其中, 波特率发生器以 MCLK 作为时钟源 ; 发送器和接收器各包含一个 16 字节的 FIFO 部件和移位寄存器, 要被发送的数据, 首先被写入发送 FIFO 部件, 然后拷贝到发送移 位寄存器中, 最后从数据输出端口 (TxDn) 依次被移位输出 被接收的数据也同样从数据接收端口 (RxDn) 移位输入到移位寄存器, 然后拷贝到接收 FIFO 部件中 其逻辑框图如图 9-2 所示 233

234 图 9-2 S3C44B0X 下 UART 逻辑框图 特性 : RxD0,TxD0,RxD1,TxD1 可以在中断模式或 DMA 模式下工作 ; UART 通道 0 符合 IrDA 1.0 要求, 且具有 16 字节的 FIFO; UART 通道 1 符合 IrDA 1.0 要求, 且具有 16 字节的 FIFO; 支持收发时握手模式 9.2 UART 操作 下面将介绍 UART 的相关操作, 包括数据发送, 数据接收, 中断发生, 波特率发生, 回送模式, 自动流控制等内容 数据发送 串口数据的发送或接收是以帧为单位进行 在 S3C44B0X 平台下, 数据发送帧格 式包含一个开始位,5 到 8 个数据位, 一个可选的奇偶位和 1 到 2 个停止位, 这些都可以通过线控制寄存器 (UCONn) 来设置 发送器也能够产生发送中止条件, 中 234

235 止条件迫使串口输出保持在逻辑 0 状态, 这种状态保持超过一个传输帧的时间长度 通常在一帧数据完整地传输完之后, 再通过这个全 0 状态将中止信号发送给对方 中 止信号发送之后, 传送数据将持续地放入到输出 FIFO 中 ( 在不使用 FIFO 模式下, 将被放到输出保持寄存器中 ) 数据接收 与发送帧格式相同, 接收的数据帧格式同样是可编程的 它包括了一个起始位, 5 到 8 个数据位, 一个可选的奇偶校验位和 1 到 2 个停止位, 这些都可以通过线控制寄存器 (UCONn) 来设置 接收器还可以检测到溢出错误, 奇偶校验错误, 帧错误和中止状况, 每种情况下都会将一个错误标志置位 溢出错误表示新的数据已经覆盖了旧的数据, 因为旧的数据没有被及时读走 ; 奇偶校验错误表示接收器对接收到的数据进行奇偶校验, 奇偶校验结果错误 ; 帧错误表示接收到的数据没有有效的停止位 ; 中止状况表示 RxDn 的输入被保持为 0 状态超过了一个帧传输的时间 在 FIFO 模式下接收 FIFO 不为空, 但接收器已经在 3 个字时间内没有接收到任 何数据, 就认为发生了接收超时状况 自动流控制 (AFC) S3C44B0X 的 UART 通过 nrts 和 ncts 信号支持自动流控制, 在这种情况下必须是 UART 与 UART 连接 如果用户将 UART 连接到调制解调器, 就应该在 UMCONn 寄存器中禁止自动流控制位, 并通过软件控制 nrts 在 AFC 中,nRTS 由接收器的接收情 况来控制,nCTS 则控制了发送器的工作 UART 发送器在 ncts 信号被置 1 的时候发送 FIFO 中的数据 ( 在 AFC 中,nCTS 意味着对方 UART 的 FIFO 已经准备好接收数据 ) 在 UART 接收数据时, 如果它的接收 FIFO 中还有多于 2 个字节的空余空间就必须将 nrts 置 1, 从而告诉对方 接收准备好 ; 当接收 FIFO 的剩余空间少于 1 字节时, 必须将 nrts 清 0, 说明 不能再接收 AFC 接口如下图所示 : 图 9-3 UART AFC 接口 235

236 9.2.4 非自动流控制 ( 通过软件控制 nrts 和 ncts) 接收操作 : 1. 选择接收模式 ( 中断和 BDMA 模式 ) 2. 检查 UFSTATn 寄存器中接收 FIFO 计数器的值, 如果值小于 15, 用户必须设置 UMCONn[0] 的值为 1 ( 即激活 nrts), 并且如果它等于或大于 15, 用 户必须设置该位值为 0 ( 即失活 nrts) 3. 重复第 2 步 发送操作 : 1. 选择发送模式 ( 中断或 BDMA 模式 ) 2. 检查 UMSTATn[0] 的值, 如果为 1 (ncts 被激活 ), 用户就可以写数据到输出缓冲区或输出 FIFO 寄存器中 调制解调器接口 如果用户需要调制解调器接口, 就需要 nrts ncts ndsr ndtr DCD 和 nri 信号 但由于 AFC 是不支持 RS-232C 接口的, 在这种情况下, 用户可以用其它 I/O 口来模拟这些接口信号, 即由软件来模拟产生这些控制信号 9.3 UART 寄存器 1. UART 线控制寄存器前面多次提到了线控制寄存器, 它主要用来规定传输帧的格式 下面就来看看线控制寄存器的位定义 : 236

237 2. UART 控制寄存器 237

238 3. FIFO 控制寄存器 4. UART 的 Modem 控制寄存器 5. UART 发送 / 接收状态寄存器 238

239 6. UART 错误状态寄存器 7. UART 的 FIFO 状态寄存器 8. UART 的 Modem 状态寄存器 239

240 9. UART 发送 / 接收保持 ( 缓冲区 ) 寄存器 UART 接收保持 ( 缓冲区 ) 寄存器和 FIFO 寄存器 :URXH0, URXH1 注意 : 如果发生了溢出错误, 必须读一次 URXHn 如果不读, 即使 USTATn 中的溢出错误位被清除了, 下一个接收的数据仍然会发生一个溢出错误 10. UART 波特率除数寄存器 每个 UART 的波特率发生器为传输提供了串行移位时钟 波特率产生器的时钟源 240

241 可以通过 S3C44B0X 的内部系统时钟来选择 波特率时钟通过时钟源 16 分频和一个由 UART 波特率除数寄存器 (UBRDIVn) 指定的 16 位除数决定 UBRDIVn 的值可以 按照下式确定 : UBRDIVn =( 取整 )(MCLK/(bps 16))-1 例如, 如果波特率为 bps 且系统主频 (MCLK) 为 64MHz, 则 UBRDIVn 为 : UBRDIVn = (int)( /( ))-1 =35-1 = 串行接口电路设计及编程 串行接口电路设计 由于处理器都会内置串口收发模块, 因此串行接口电路设计非常简单, 只需要 加上串口电平转换电路就可以了 图 9-4 就是 S3C44B0X 下串口设计电路 图 9-4 串口电路设计 注意 : 由于 S3C44B0X 输出的电平不是 RS232 电平信号, 因此图 9-4 采用 MAX3232 芯片进行电平转换 最简单的串口通信只需要 3 根信号线即可 当然要设计一个全串口通信电路, 比如和一个 modem 进行通信的话, 需要实现全部 8 根数据线的电平转换, 当然由于 DSR DTR DCD 和 RI 信号比较少用, 因此 S3C44B0X 微处理器内置的串口模块并没有提供这些接口信号, 只能用微处理器空闲的 GPIO 脚代替, 并用软件来实现 这些信号的相关功能 241

242 9.4.2 串口功能函数设计 1.I/O 接口配置初始化 由于要把 S3C44B0X 的 GPIO 口 PORTC12-PORTC13 用作串口的 TXD1 和 RXD1,PORTE1-PORTE2 用作 TXD0 和 RXD0, 因此必须对这些端口进行初始化, 配置如下 : //PORT C GROUP //BUSWIDTH=16 /* PC */ /* o o RXD1 TXD1 o o o o */ /* NC NC Uart1 Uart1 NC NC NC NC */ /* */ /* PC */ /* o o o o o o o o */ /* NC NC NC NC LED LED LED LED */ /* */ rpdatc = 0x0000; //All IO is low rpconc = 0x5f555555; rpupc = 0x3000; //PULL UP RESISTOR should be enabled to I/O //PORT E GROUP /* PE */ /*ENDLAN o o TOU2 o o RXD0 TXD0 FOUT*/ /* */ rpdate = 0x157; //Beep = 10 rpcone = 0x556b; rpupe = 0xff; 2. 串口初始化函数 void UartInit(int ch, int baud) { U8 a; if(!ch) {// 配置串口 0 242

243 rufcon0 = 0x0; // 禁止使用 FIFO rumcon0 = 0x0; // 禁止使用 FIFO rulcon0 = 0x3; // 正常无奇偶校验, 一个停止位,8 个数据位 rucon0 = 0x45; //TX RX 都用 PULSE 非 LEVEL 中断 //rx=edge,tx=level,disable timeout int.,enable rx error //int.,normal,interrupt or polling rubrdiv0 = (int)(mclk/(16.0*baud)+0.5)-1; a = rurxh0; } else {// 配置串口 1 rufcon1 = 0x0; // 禁止使用 FIFO rumcon1 = 0x0; // 禁止使用 FIFO rulcon1 = 0x3; // 正常无奇偶校验, 一个停止位,8 个数据位 rucon1 = 0x45; //rx=edge,tx=level,disable timeout int.,enable rx error //int.,normal,interrupt or polling //baud *= 16; rubrdiv0 = (int)(mclk/(16.0*baud)+0.5)-1; a = rurxh1; } } 3. 字符发送函数 void UartSend(int ch, char data) { if(!ch) {// 利用串口 0 发送数据 if(data=='\n') { while(!(rutrstat0&0x2)); Delay(10); // 由于超级终端反应较慢, 有一个微小延迟 WrUTXH0('\r'); } while(!(rutrstat0&0x2)); // 等待知道 THR 变空 Delay(10); rutxh0 = data; } else {// 利用串口 1 发送数据 243

244 } } if(data=='\n') { while(!(rutrstat1&0x2)); Delay(10); rutxh1 = '\r'; } while(!(rutrstat1&0x2)); Delay(10); rutxh1 = data; // 由于超级终端反应较慢, 有一个微小延迟 // 等待知道 THR 变空 4. 字符接收函数 int UartReceive(int ch) { if(!ch) {// 串口 0 接收数据 while(!(rutrstat0&0x1)); return rurxh0; } else {// 串口 1 接收数据 while(!(rutrstat1&0x1)); return rurxh1; } } // 等待直到接受到一个数据 // 等待直到接受到一个数据 小结 本章首先介绍了串口的基本概念, 然后重点说明了 S3C44B0X 微处理器下所带串口的基本情况, 包括 UART 的操作和寄存器的详细介绍 在读者了解和掌握这些知识之后, 较详细的说明了在 S3C44B0X 平台下串口电路的设计以及相关底层函数 的编写 最后, 要提醒读者注意的是 : 串口芯片和串口电平转换芯片是两个不同的概念, 千万不可混淆 比如本章中由于 S3C44B0X 微处理器已经带有串口, 所以电路设计 中不需要串口芯片, 但是由于其输出电平并不是 RS232 电平, 故需要一块 MAX3232 电平转换芯片来进行电平转换 244

245 习题 1. 填空题 (1) 串行通信接口标准是, 它采用 逻辑进行电平转换 (2) 串行通信基本的通信方式有 和 两种 (3)RS232C 逻辑 1 代表 A.-5V~-15V B.+5V~+15V C.-15V~+15V D.-5V~+5V 2. 简答题 (1) 如果在项目设计中, 需要将 S3C44B0X 下的 UART1 设置如下 : 波特率 9600b/s,7 位数据位,2 个停止位,1 位奇偶校验位, 并采用流控制工作, 该如何设置? 并写出相应的底层函数 245

246 第 10 章 LCD 接口及其应用 10.1 LCD 简介 1.LCD 显示原理 LCD 俗称液晶, 它的分子晶体以液态存在 LCD 显示的基本原理就是给不同的液晶单元供电, 控制其光线的通过与否, 达到显示的目的 LCD 的驱动控制归于对每个液晶单元的通断电的控制 每个液晶单元都对用对应一个电极, 对其通电, 便可使光线通过 2.LCD 显示屏的结构 两块无钠玻璃夹着一个由偏光板 液晶层和彩色虑光片构成的夹层组成 偏光板 彩色滤光片决定了有多少光可以通过以及生成何种颜色的光线 液晶被灌在两个制作精良的平面之间构成液晶层, 这两个平面上列有许多沟槽, 单独平面上的沟槽都是平行的, 但是这两个平行的平面上的沟槽是互相垂直的 位于两个平面间液晶分子的排列会形成一个 Z 轴向 90 度的逐渐扭曲状态 背光源光线通过液晶显示屏背面的背光板和反光膜, 产生均匀的背光光线, 这些光线通过后层会被液晶进行 Z 轴向的扭曲, 从而能够通过前层平面 如果给液晶层加电压将会产生一个电场, 液晶分子就会重新排列, 光线无法扭转从而不能通过前层平面, 以此来阻断光线 3.LCD 的分类 扭曲向列型 (TN-Twisted Nematic); 超扭曲向列型 (STN-Super TN); 双层超扭曲向列型 (DSTN-Dual Scan Tortuosity Nomograph); 薄膜晶体管型 (TFT- Thin Film Transistor) 其中 TN-LCD STN-LCD 和 DSYN-LCD 的基本显示原理都相同, 只是液晶分子的扭曲角度不同而已 STN-LCD 的液晶分子扭曲角度为 180 度甚至 270 度 而 TFT -LCD 则采用与 TN 系列 LCD 截然不同的显示方式 4.LCD 性能指标 主要的性能指标是分辨率, 亮度, 对比度 分辨率 : 在 LCD 液晶板上通过网格来划分液晶体, 一个液晶体为一个象素点 那么, 输出分辨率为 320 x 240 时, 就是说在 LCD 液晶板的横向上划分了 620 个象素点, 竖向上划分了 240 个象素点 亮度 : 国际标准单位是 ANSI 流明 对比度 : 是黑与白的比值, 也就是从黑到白的渐变层次 比值越大, 从黑到白 246

247 的渐变层次就越多, 从而色彩表现越丰富 10.2 S3C44B0X 的内部 LCD 控制器 S3C44B0X 的内部 LCD 控制器简介 S3C44B0X 内置 LCD 控制器可以支持规格为每像素 2 位 (4 级灰度 ) 或每像素 4 位 (16 级灰度 ) 的黑白 LCD 也可以支持每像素 8 位 (256 级颜色 ) 的彩色 LCD 屏 LCD 控制器可以通过编程支持不同 LCD 屏的要求, 例如行和列像素数, 数据总线宽度, 接口时序和刷新频率等 LCD 控制器的主要的工作, 是将定位在系统存储器中的显示缓冲区中的 LCD 图像数据传送到外部 LCD 驱动器 LCD 控制器的主要特性 : - 支持彩色 / 灰度 / 黑白 LCD 屏 ; - 支持 3 种显示类型 LCD 屏 :4 位双扫描,4 位单扫描,8 位单扫描显示类型 ; - 支持多种虚拟显示屏 ( 支持硬件方式的水平 / 垂直滚动 ); - 采用系统存储器作为显示缓冲区存储器 ; - 专门的 DMA 操作用于支持图像数据的获取 ; - 支持多种屏幕大小 : 典型的屏幕尺寸 : , , , , 等等 最大虚拟屏幕大小 ( 彩色模式 ): , , 等 - 支持黑白,4 级灰度和 16 级灰度 ; - 支持 STN 型 256 级色彩 LCD 显示屏 ; - 支持低功耗模式 (SL_IDLE 模式 ) LCD 控制器的外部接口信号 VFRAME:LCD 控制器和 LCD 驱动器之间的帧同步信号 该信号告诉 LCD 屏的新的一帧开始了 LCD 控制器在一个完整帧显示完成后立即插入一个 VFRAME 信号, 开始新一帧的显示 ; 该信号与 LCD 模块的 YD 信号相对应 VLINE:LCD 控制器和 LCD 驱动器之间的线同步脉冲信号, 该信号用于 LCD 驱动器将水平线 ( 行 ) 移位寄存器的内容传送给 LCD 屏显示 LCD 控制器在 整个水平线 ( 整行 ) 数据移入 LCD 驱动器后, 插入一个 VLINE 信号 ; 该信号与 LCD 模块的 LP 信号相对应 VCLK:LCD 控制器和 LCD 驱动器之间的像素时钟信号, 由 LCD 控制器送出 247

248 的数据在 VCLK 的上升沿处送出, 在 VCLK 的下降沿处被 LCD 驱动器采样 ; 该信号与 LCD 模块的 XCK 信号相对应 VM:LCD 驱动器的 AC 信号 VM 信号被 LCD 驱动器用于改变行和列的电压极性, 从而控制像素点的显示或熄灭 VM 信号可以与每个帧同步, 也可以与可变数量的 VLINE 信号同步 ; 该信号与 LCD 模块的 DISP 信号相对应 VD[3:0]:LCD 像素点数据输出端口 与 LCD 模块的 D[3:0] 相对应 VD[7:4]:LCD 像素点数据输出端口 与 LCD 模块的 D[7:4] 相对应 其逻辑框架如图 10-1 所示 图 10-1 LCD 控制器逻辑框图 S3C44B0X 内部的 LCD 控制器用来输出视频数据, 以及产生必须的控制信号, 如 :VFRAME,VLINE,VCLK, 和 VM 和控制信号一样,S3C44B0X 也有与视频数据对应的数据输出端口, 这就是 VD[7:0], 如图 8-1 所示 LCD 控制器包括 REGBANK,LCDCDMA,VIDPRCS 和 TIMEGEN 几个部分 REGBANK 有 18 个可编程的寄存器集组成, 用来配置 LCD 控制器 LCDCDMA 是一个专用的 DMA, 它可以自动地将帧内存区中的视频数据传输给 LCD 驱动器 通过使用这个特殊的 DMA, 视频数据可以在没有 CPU 干预的情况下在屏幕上显示 VIDPRCS 接收来自 LCDDMA 的视频数据, 并且将它转换成一种合适的数据格式, 比如 4/8 位单扫描和 4 位双扫描模式, 然后通过数据端口 VD[7:0] 发送给 LCD 驱动器 TIMEGEN 由一些 可编程的逻辑电路组成 它可以支持不同的 LCD 驱动器在接口时序和接口速率等方面不同的要求 TIMEGEN 模块产生 VFRAME,VLINE,VCLK 等 数据流的描述如下 : FIFO 存储区出现在 LCDCDMA 中 当 FIFO 全为空和部分为空,LCDCDMA 基于突发内存传输模式 ( 一个突发请求连续地获取 4 个字,16 个字节 ) 请求从帧存储器获取数据 当内存控制器中的总线仲裁器接到这种类型的传输请求后, 就会有 4 个连续 的字的数据从系统内存传输到内部的 FIFO FITO 的总大小为 24 个字, 由 12 个字的 FIFOL 和 12 个字的 FIFOH 组成 S3C44B0X 有两个 FIFO 因为它需要支持双扫描的显示模式 在单扫描模式下, 只要使用一个就可以了 248

249 LCD 控制器的操作 1. 显示类型 S3C44B0X 的 LCD 控制器支持 3 种 LCD 驱动器 :4 位双扫描,4 位单扫描,8 位单扫描显示模式 其中,8 位单扫描方式如图 10-2 所示 : 图 位单扫描方式 8 位单扫描显示采用 8 位并行数据线进行 行 数据连续移位输出, 直到整个帧的数据都被移出为止 彩色像素点的显示要求 3 种颜色的图像数据, 这使得行数据移位寄存器需要传输 3 倍于每行像素点个数的数据 这个 RGB 数据通过平行数据 线连续地移位至 LCD 驱动器 如上图 10-2 所示 图 10-3 是 LM057QC1T01 的扫描模式图, 可见 LM057QC1T01 是按照 8 位单扫描模式工作的 在 8 位单扫描方式中,LCD 控制器的 8 条 (VD[7:0]) 数据输出可 以直接与 LCD 驱动器连接 图 10-3 LM057QC1T01 的扫描模式 249

250 10.3 LCD 接口电路设计 设计思路 实现 LCD 的显示功能实际上是对显存的操作, 只要把显示数据存到显示缓冲存储器中,LCD 控制器会通过 DMA 从显示缓冲区中获取数据, 不需要 CPU 的干预 本系统采用的 LCD 分辨率为 显示缓冲区中的一个字节数据代表 LCD 上 的一个点 由于具有虚拟显示功能, 所以实际上显示缓冲区应该大于显示屏对应的大小 参考了一些资料, 考虑将所有的显示数据当作图像数据显示, 所以首先要实现图像数据的显示功能 然后将字符 汉字等等均转化为图像数据, 调用图像显示函数处理 这里要解决的问题是如何将它们转换为图像数据 能够实现基本的转换后, 为了应用程序设计的需要, 拟设计一些简单的显示应用函数, 入画直线, 矩形, 圆等 硬件连接 CA320240G 是广州松山科技公司生产的 LCD 屏, 分辨率为 , 其引脚分配如表 10-1 所示 表 10-1 CA320240G 引脚 引脚号 CA320240G S3C44B0X 板 引脚号 CA320240G S3C44B0X 板 1 D0 14 VDD 2 D1 15 VSS 3 D2 16 VOUT -19v 4 D3 17 V0 5 DISPOFF 18 SK/X1 6 FRAME 19 DO/X2 7 VM 20 DI/Y1 8 LOAD 21 CS/Y2 9 CP 22 INT 10 NC 23 EL1/VEL 11 NC 24 EL2/ELON 12 VEE v 13 NC 因此 CA320240G 液晶屏和 S3C44B0X 的 LCD 接口之间的硬件连接关系如表

251 所示 表 10-2 CA320240G 液晶屏和 S3C44B0X 的 LCD 接口之间的硬件连接 液晶模块嵌入式板 在上表中, 液晶模块的 1 至 4 引脚 D0~D3 是数据线, 对应的 VD0~VD3 使用的是 D 端口的 0~3 位 PD0~PD3( 拟使用四位单扫描模式 ) 液晶的第 5 脚 DISPOFF 接嵌入板的第 18 脚, 使用的是 C 端口的第 8 脚 PC8 液晶的 6~9 脚对应嵌入板的 9,114,12,11 脚, 使用的 D 端口的 7~4 脚 PD7,PD6,PD5,PD4 液晶的 14 脚接嵌入板的 19 脚供 5V 电源, 但是实际操作中由于接 5V 电源时液晶模块太亮, 所以改接嵌入板上的 3.3V 电源 液晶的 15 脚接地 17 脚和 25 脚共同起到一个作用,25 脚输出负压连接到嵌入板的第 8 脚, 通过一个可变电阻再连接到嵌入板的第 13 脚, 从 13 脚输出到液晶的 17 脚 V0, 这样通过调节可变电阻可以调节显示模块的亮度 液晶的 18,19,20,21 四个脚分别是 SK,DO,DI,CS, 对应嵌入板的 5,6,7, 16 脚, 使用的 C 端口的 PC7,PC6,PC5, 和 PC LCD 驱动设计 初始化 从表 10-2 硬件连接关系中可以看出, 与 LCD 控制相关的是 DISP_ON 脚以及 D 端口的所有引脚, 其中 PD0~PD3 作为 LCD 的 VD0~VD3,PD4 作为 VCLK;PD5 作为 VLINE;PD6 作为 VM;PD7 作为 VFRAME, 这些端口的功能和模式控制字在第 7 章中均有介绍 它们的工作模式均设置为输出 按照上面的思路对 LCD 端口进行初始化, 其过程如下 : 1. 初始化 LCD 端口 : PC8 口作为 DISP_ON 脚, rpconc = (0x01<<16); rpconc &= (0xfffdffff); rpdatc &= (0xfeff); 251

252 PORTD 定义成 LCD 控制端口 :rpcond=0xaaaa; 2. 下面配置 LCD 的寄存器以控制 LCD 的工作模式 LCD 的控制寄存器和地址寄存器的控制字前文已经介绍, 这里不再赘述 分别配置 LCD 控制寄存器 1 和 2, 设置为 4 位单扫描单色模式, 分辨率 ,16 级灰度,Bmp 为帧缓冲地址 设置 LCDBASEU, LCDBASEL, LCDBANK 以及 PAGEWIDTH 和 VIRTUALLINEVAL 的值, 使得可以实现虚拟屏的显示 现在可以通过控制 PC8 口的数据来控制 LCD 显示的开关, 依此得到两个函数 LcdTurnOn() 和 LcdTurnOff() 通过填充缓存区可以将整个液晶屏或者虚拟屏填充某一值 可以点亮或者熄灭指定的点, 清除指定的行列等等 3. 代码示例 /********************************************* * 初始化液晶模块 **********************************************/ void LcdInit(void) { // 打开显示 DISP_ON,PC8 脚输出低电平 rpconc = (0x01<<16); rpconc &= (0xfffdffff); rpdatc &= (0xfeff); //PORTD 定义成 LCD 控制端口 rpcond=0xaaaa; rdithmode = 0x12210; // 无意义,CPU 要求 // 配置 LCD 控制寄存器 1, 关 ENVID, 进入参数修改模式,4bit 单扫描 rlcdcon1=(2) (INVFRAME<<2) (INVLINE<<3) (INVCLK<<4) (1<<5) (MVAL_USE D<<7) (WDLY<<8) (WLH<<10) (CLKVAL_SL<<12); // 配置 LCD 控制寄存器 2,240 行,(320/4=80) 列,LINEBLANK 为 10, //10 个空闲时钟 rlcdcon2=(lineval) (((PAGEWIDTH*16/VDBUSWIDTH)-1)<<10) (10<<21); LCDBANK = (U32)Bmp>>22; LCDBASEU = M5D((U32)Bmp>>1); LCDBASEL = (U32)(LCDBASEU + PAGEWIDTH*(LINEVAL+1)); // 配置 LCD 地址寄存器 1, 配置显示模式, 单色模式,Bmp 为帧缓冲起始地址 rlcdsaddr1= (0x0<<27) ((U32)LCDBANK<<21) (U32)LCDBASEU; //16level gray 252

253 rlcdsaddr2= (1<<29) (MVAL<<21) (U32)LCDBASEL; rlcdsaddr3= (PAGEWIDTH) ( OFFSIZE<<9 ); // 打开 ENVID, 完成修改 rlcdcon1=(1) (INVFRAME<<2) (INVLINE<<3) (INVCLK<<4) (1<<5) (M VAL_USED<<7) (1<<8) (1<<10) (CLKVAL_SL<<12); } 图像数据的显示 LCD 显示的原理是将数据传到显示缓冲区 所有的数据都是作为图像数据显示 出来的, 无论是字符还是汉字 所以首先必须设计图像显示函数 图像数据显示的原理是这样的 : 逐行逐列扫描图像数据, 根据每一个象素点相位的正反确定该字节的数值, 放到显示缓冲区当中去 字符和汉字是作为图像数据来显示的, 所以首先要设计将它们转化为图像数据 字符是作为一个 8 16 的图像来显示的 所有的字符图像数据存放在一个字库当中, 该款显示屏已经集成了字库 将字库烧到芯片存储器指定的位置 也就是字符库的首地址是已知的, 命名为 asciimatrix, 字符本身的 ASCII 码通过一个转换, 可以得到该字符图像数据在字符库当中的偏移量, 依次去访问字符库, 找到图像数据即可, 转换的程序如下 : if((*ascvalue <= 0x20) (*ascvalue >= 0x80)) { *ascvalue = 0x7f; } ascpic = asciimatrix + (*ascvalue+1-0x21)*16; 汉字的显示原理与字符的显示类似, 不过是作为一个 的图像数据显示 将汉字转换为图像数据的方式是查找汉字库 类似于字符库, 汉字库也已经被烧到芯片存储器指定的地址, 也就是说汉字库的首地址已知, 命名为 HZK16, 汉字机内码通过转换公式可以得到偏移量, 转换的程序如下 : // 获取区位码 qvcode = *ahanzi - 0xa1; weicode = *(ahanzi+1) - 0xa1; // 计算汉字字模入口地址 hanzipic = HZK16 + (qvcode*94 + weicode)*32; 通过实现这些基本功能,LCD 已经能够实现显示各种数据 253

254 LCD 显示简单应用函数 1. 画点函数 点亮某个点函数 LcdPointOn(S16 row,s16 col), 点的坐标为 (row,col), 单位为象素 : 该函数实现的原理是将点的坐标转换为对应的显示缓冲区的地址, 将该字节的 值置为 1 代码示例 : /************************************************************** 点亮某个点, 点的坐标为 (row,col), 单位为象素 row<240,col<320 ***************************************************************/ void LcdPointOn(S16 row,s16 col) { S16 col8; // 转换成 8 象素点为基本单位的坐标 U8 pos; U8 data; // 误差 if((row>virtuallineval) (col >= (PAGEWIDTH*16))) { return; } col8 = (col/8); pos = col - col8*8; data = *(Bmp+ row*(pagewidth*2) + col8); *(Bmp+row*(PAGEWIDTH*2) + col8) = data (1<<(7-pos)); } 2. 画线函数画直线函数 LcdLine: 调用了 LcdPointOn 函数 该函数的实现原理是逐个扫描 从起始点到终点的象素点, 将每个点对应的显示缓冲区的字节值置 1 代码示例 : /******************************************************** 画一条直线, 从 (startx,starty) 到 (endx,endy), 点的位置以象素为基本单位 linetype: 线的类型, 如果为 0 为实线, 如果为 1, 为虚线, 如果为 2, 则是黑线, 与背景色相同, 相当于擦除 *********************************************************/ 254

255 void LcdLine(S16 startrow,s16 startcol,s16 endrow,s16 endcol,u8 linetype) { float cline; S16 i; S16 temp; //x+1, 对应 y 的增加值 if(startcol > endcol) { temp = startrow; startrow = endrow; endrow = temp; // 如果终点在起点左边, 交换起点和终点 } temp = startcol; startcol = endcol; endcol = temp; if((startcol == endcol) && (startrow > endrow)) // 如果终点在起点正上方, 交换起点和终点 { } temp = startrow; startrow = endrow; endrow = temp; if(startcol == endcol) // 画垂直线 { for(i=startrow; i<=endrow; i++) { if(linetype == 2) { LcdPointOff(i,startCol); } else { LcdPointOn(i,startCol); if(linetype == 1) { i ++; 255

256 } startrow)) } } return; } else // 画斜线 { if((abs(endrow - startrow) >= abs(endcol - startcol)) (endrow == { { } else { cline = (float)(endrow-startrow)/(endcol - startcol); for(i=startcol; i<=endcol; i++) { if(linetype == 2) } { } else { } LcdPointOff(FloatToS16(startRow+((i-startCol)*cline)),i); LcdPointOn(FloatToS16(startRow+((i-startCol)*cline)),i); if(linetype == 1) { i ++; } if(endrow >= startrow) { cline = (float)(endcol-startcol)/(endrow - startrow); for(i=startrow; i<=endrow; i++) { if(linetype == 2) LcdPointOff(i,FloatToS16(startCol+((i-startRow)*cline))); } 256

257 } else { } else { LcdPointOn(i,FloatToS16(startCol+((i-startRow)*cline))); if(linetype == 1) { } } i ++; cline = (float)(endcol-startcol)/(startrow-endrow); for(i=endrow; i<=startrow; i++) { } if(linetype == 2) { LcdPointOff(i,FloatToS16(endCol - ((i-endrow)*cline))); } else { LcdPointOn(i,FloatToS16(endCol - ((i-endrow)*cline))); if(linetype == 1) } { } i ++; } } } } 画矩形框函数 LcdRectangle(): 调用了 LcdLine 函数, 根据带回的顶位置 左边位置 宽度和高度画相应的直线 ( 代码略 ) 显示一个按钮函数 LcdButton(): 调用了画矩形框函数 LcdRectangle(), 按钮被设计为大框中存在一个小框, 在小框中调用字符数组显示该按钮的主题 ( 代码略 ) 257

258 小结 本章主要介绍了 S3C44B0X 的 LCD 接口, 并以松山有限公司生产的一款 LCD 显示屏为例, 详细介绍了其电路接口设计, 底层驱动函数编写等内容 在学习本章内容时, 作者首先讲解一个点是如何在屏上进行显示, 在此基础上, 再讲解一条线如何显示, 最终让读者明白字符 图片等内容如何在 LCD 屏上进行显 示 习题 1. 简述 LCD 显示的原理 2.LCD 显示屏分为哪几类, 都有何特点 3. 以显示一个汉字为例, 简述 LCD 屏驱动程序的设计流程 4. 目前市场上支持 S3C44B0X 平台的 GUI 有哪些, 试谈谈它们的特点, 并掌握如何在这些 GUI 产品上设计自己的图形程序 258

259 第 11 章触摸屏设计 11.1 触摸屏原理 1. 触摸功能原理为了操作上的方便, 用触摸屏来代替鼠标或键盘 工作时, 首先触摸安装在显示器前端的触摸屏, 系统根据触摸的图标或菜单位置来定位选择信息输入 触摸屏 由触摸检测部件和触摸屏控制器组成 ; 触摸检测部件安装在显示器屏幕前面, 用于检测用户触摸位置, 送触摸屏控制器 ; 触摸屏控制器从触摸点检测装置上接收触摸信息, 将它转换成触点坐标, 送给 CPU, 它同时能接收 CPU 发来的命令并加以执行 2. 触摸屏的主要类型触摸屏按照其敏感原理可以分为 : 矢量压力传感式 电阻式 红外线式和表面声波式 本次设计选用四线电阻式触摸屏, 下面详细介绍, 其余类型简介 电阻式触摸屏 : 电阻触摸屏的屏体部分是一块与显示器表面非常配合的多层复合薄膜, 表面涂有一层透明的导电层 (OTI, 氧化铟 ), 上面再盖有一层外表面硬化处理的塑料层, 它的内表面也涂有一层 OTI, 在两层导电层之间有许多细小 ( 小于千 分之一英寸 ) 的透明隔离点把它们隔开绝缘 其结构如下图 11-1 图 11-1 电阻式触摸屏 当手指接触屏幕, 两层 OTI 导电层出现一个接触点, 因其中一面导电层接通 Y 轴方向的 5V 均匀电压场, 使得侦测层的电压由零变为非零, 控制器侦测到这个接通后, 进行 A/D 转换, 并将得到的电压值与 5V 相比, 即可得触摸点的 Y 轴坐标, 同理得出 X 轴的坐标, 这就是电阻技术触摸屏共同的最基本原理 电阻屏根据引出 线数多少, 分为四线 五线等多线电阻触摸屏 电阻式触摸屏的 OTI 涂层比较薄且容易脆断, 涂得太厚又会降低透光且形成内反射降低清晰度,OTI 外虽多加了一层薄塑料保护层, 但依然容易被锐利物件所破 坏 ; 且由于经常被触动, 表层 OTI 使用一定时间后会出现细小裂纹, 甚至变型, 如 259

260 其中一点的外层 OTI 受破坏而断裂, 便失去作为导电体的作用, 触摸屏的寿命并不长久 但电阻式触摸屏不受尘埃 水 污物影响 表面声波触摸屏 : 表面声波触摸屏的边角有 X Y 轴声波发射器和接收器, 表面有 X Y 轴横竖交叉声波传输 当触摸屏幕时, 从触摸点开始的声波能量部分被吸收, 控制器根据到达 X Y 轴的声波变化和传输速度计算声波变化的起点, 即触摸 点 电容感应触摸屏 : 人相当于地, 给屏幕表面通上一个很低的电压, 当用户触摸屏幕时, 手指吸收一个很小的电流 这个电流分别从触摸屏四个角或四条边上的电极中流出 理论上, 流经这四个电极的电流与手指到四角的距离成正比 控制器对这四个电流比例进行计算, 得到触摸点的位置 红外线触摸屏 : 在红外线触摸屏显示器屏幕的前面安装一个外框, 外框里有电路板, 在 X Y 方向布置红外发射管和红外接受管, 一一对应形成横竖交叉的红外线矩阵 当有触摸时候, 手指或者其它物体就会挡住经过该处的横竖红外线, 由控制器判断触摸点在屏幕的位置 3. 触摸屏基本技术特性 (1) 透明性能 : 包括透明度 色彩失真度 反光性和清晰度这四个特性 (2) 绝对坐标系统 : 触摸屏是一种绝对坐标系统, 每一次定位坐标与上一次定位坐标没有关系, 每次触摸的数据通过校准转为屏幕上的坐标 (3) 检测与定位 : 各种触摸屏技术都是依靠传感器来工作的 各自的定位原理和各自所用的传感器决定了触摸屏的反应速度 可靠性 稳定性和寿命 (4) 触摸屏的原点电阻式触摸屏通过电压的变化范围来判定按下触摸屏的位置, 所以其原点就是触摸屏 X 电阻面和 Y 电阻面接通产生的最小电压处 随着电阻的增大,A/D 转化所产生的数值不断增大, 形成坐标范围 有很多方法确定触摸原点, 常见的是对角定位法 四点定位法和实验室法 对角定位法 : 系统先对触摸屏的对角坐标进行采样, 根据数值确定坐标范围, 可采样一条对角线或两条对角线的定点坐标 这种方法简单易用, 但是需要多次采样操作并且进行比较, 以取得定位的准确性 四点定位法 : 同对角定位法一样, 需要进行数据采样 采样四个定点坐标以确定有效坐标范围, 程序根据四个采样值的大小关系进行坐标定位 这种方法的定位比对角定位法可靠, 所以现在被许多带有触摸屏的设备终端所使用 实验室法 : 触摸屏的坐标原点, 坐标范围由生产厂家在出厂前定义好 定位方法时按照触摸屏和硬件电路的系统参数, 对批量硬件进行最优处理确定 这种方法适用于触摸屏构成的电路系统有较好的电气特性, 且不同产品有较大的相似性的场合 (5)A/D 转换器 A/D 转换器是触摸屏的重要组成部分 A/D 转换器的工作原理 : 260

261 A/D 转换的方法很多 下面介绍常用的计数式和逐次逼近式 A/D 转换原理 计数式 : 由 D/A 转换器, 计数器和比较器组成, 计数器由零开始计数, 将 其计数值送往 D/A 转换器进行转换, 将生成的模拟信号与输入的模拟信号进行比较 若前者小于后者, 则计数值加一, 重复以上过程 因为计数值是递增的, 所以输出的模拟信号是一个逐步增加的量 当这个信号值与输出模拟量相等时候, 比较其产生停止计数信号, 计数器立即停止计数 逐次逼近式 : 由比较器,D/A 转换器, 寄存器和逻辑控制电路组成 与计数式相同, 逐次逼近式也要比较, 以得到转换数字值 但在逐次逼近式当中, 用一个寄存器控制转换器 逐次逼近式是从高位到低位逐次试探比较, S3C44B0X 处理器集成了这种转换器 A/D 转换器的主要性能指标 : 分辨率, 转换时间, 量程, 精度 11.2 触摸屏驱动设计 设计思路 在实现 LCD 显示的基础上, 考虑触摸功能驱动的设计 为了实现对于触摸屏点的控制, 所以必须定义触摸点的结构体, 相应的必须在 LCD 驱动的头函数中增加 LCD 显示屏的点结构, 由于触摸屏与 LCD 显示屏的分辨率不同, 所以需要定义一个坐标转换函数, 将触摸屏的坐标转换为 LCD 的坐标, 则在之后的应用中针对 LCD 的分辨率计算坐标范围即可 触摸屏的驱动首先涉及的是触摸屏的初始化, 这是分两步完成的, 即端口的初始化和寄存器写控制字 通过阅读该触摸屏的相关资料, 我了解到, 实际上产品内部已经实现了大部分的功能, 例如检测触摸, 发送中断改变寄存器的状态, 进行 A/D 转换, 将触摸点坐标存入寄存器等等 要设计的驱动实际上是通过读写寄存器实现 驱动设计 触摸屏的初始化实际上是两步完成, 即端口的初始化和寄存器写控制字 1. 端口初始化根据前面介绍的硬件连接情况和引脚的功能, 必须对相应的端口工作模式进行初始化 对于触摸屏的控制来说, 涉及到的是 C 端口 对 C 端口初始化 : 使用了 C 端口的 PC5~PC9 位,PC5 对应触摸屏的 DI, 作为主板来说应该是输出口,PC6 对应触摸屏的 DO, 作为主板来说应该是输入口,PC7 对应触摸屏的 SK, 是时钟信号, 上跳沿有效, 对于主板来说应该是输出口,PC8 对用的是 LCD 显示控制的 DISPON 信号, 应该是输出口,PC9 对应 CS, 是片选信号, 261

262 也应该是输出口 纵上所述, 参照上文的 C 端口控制寄存器控制字表, 设置 rpconc=0x5f554555; 2. 寄存器写控制字首先介绍触摸屏微控制器的工作 (1) 触摸屏微控制器 MK715: CA320240C 中使用的触摸屏控制器是 MK715 它的引脚图如图 11-2 所示 : 图 11-2 MK715 引脚图 MK715 提供了了显示驱动,A/D 转换, 可以与四线式电阻触摸屏连接 它本身有一个控制时钟 该控制器时钟监视屏幕等待触摸事件 当触摸时间发生的时候, 进行 A/D 转换, 确定坐标的位置, 将 X 和 Y 坐标存储到寄存器中, 并且产生一个内部中断 MK715 的内部框图如图 11-3 所示 : 图 9-3 MK715 的内部框图 从以上的框图中看到,MK715 包含四个寄存器, 分别是状态, 速率, 结果和控制寄存器,MK715 和外界的交流只是通过四个串口实现的, 即 DI,DO,CS,SK 查阅 MK715 的相关资料, 了解了通过 MK715 有两种方式来查询触摸点的坐标, 都是通过寄存器来控制的 262

263 第一种是设置状态寄存器中的 ENCORE 位 在速率寄存器中设置一个固定的时间周期, 则 MK715 将周期性的以这个速率进行转换 芯片在一个低电流模式下监控 触摸屏, 当检测到触摸事件时候, 芯片电流上升开始触摸点的转换 INT 脚输出 1 产生一个中断使得状态寄存器中改变 转换器输出一个 Y 坐标, 接着一个 X 坐标, 再一个 Y 坐标, 一个 X 坐标, 周而复始 X 和 Y 坐标都放在同一个寄存器即结果寄 存器中, 并且后一次覆盖前一次的数据 当一个坐标被存储了后, 状态寄存器当中的转换完成位被设置为 1 只有当坐标数据被读走之后, 这一位才被清 0 每一个坐标转换后, 中断 INT 脚置高, 检查是否仍然有触摸, 如果没有, 则转换停止, 芯片 转入低电压模式, 等待触摸 第二种方式是通过设置控制寄存器中的 RD1PT 位实现的 芯片将进行两个转化, 一个 Y 坐标然后一个 X 坐标 X 坐标的值会覆盖 Y 坐标的值所以 Yz 坐标的值 必须先被读出 最后,RD1PT 被清零 经过比较, 决定采用第一种方式实现触摸屏坐标的检测, 其流程图见图 11-4 所示 图 11-4 MK715 触摸屏坐标检测 263

264 (2)MK715 寄存器无论是两种方法中的哪一种, 寄存器的作用都是非常重要的, 所以接下来详细 介绍四个寄存器的控制方式 MK715 有四个 12 位的寄存器 但是, 每个寄存器只有八位可以写 (D0~D7), 其它四位 (D8~D11) 只能读不能写 结果寄存器结果寄存器有两种工作模式, 只读和只写模式 只读模式得到触摸屏转换的结果, 只写模式改变四个控制位 注意其中的 1 和 7 位, 与第一种方式实现触摸点的检测有很大关联 需要说明的是, 在初始化过程中, 为了保证没有之前的异常中断影响, 所以首先将 2 位置 1, 再置零, 可以保证没有之前的异常中断影响 结果寄存器的控制字描述如图 11-5 所示 : 图 11-5 结果寄存器 注意只读状态下的 10 位, 它标识了结果寄存器中的是 X 坐标还是 Y 坐标 速率寄存器速率寄存器的控制字描述如下 : 图 11-6 速率寄存器 速率寄存器设置了检测触摸事件发生的频率, 这个频率是很重要的, 因为如果 264

265 时间间隔过长, 可能造成对触摸不响应 控制寄存器 控制寄存器的控制字描述如下 : 图 11-7 控制寄存器 控制寄存器的主要目的是设置时钟频率, 这里用系统的默认值即可 (3) 寄存器的读写 : 上文已经说明, 对触摸屏的控制是通过寄存器实现的 那么对寄存器的读写就成为了一个十分重要的问题 寄存器的输入输出口是 DI 和 DO 对寄存器的读写是通过四个信号的协调控制实现的, 即 SK,CS,DI 和 DO SK 是时钟信号, 上升沿有效, CS 是片选信号, DI 和 DO 都是串行口,DI 负责数据写入寄存器, 通过 DO 输出寄存器的当前状态值 读写寄存器要注意的是硬件的时序逻辑 该时序逻辑是在出厂时候设定好的, 如图

266 图 11-8 读写寄存器硬件时序逻辑 从以上的时序图可以看出 SK 上升沿有效 当 CS 也就是片选信号翻转为高之后, 开始一个循环 DI 的第一个 1 信号被认为是起始位 (START), 接下来的是读写位 (WR), 决定了后面的数据实际上是否传到指定的寄存器, 如果该位为零, 则八位写数据 (D7~D0) 是无效的 A1A0 两位决定了要读写的是哪一个寄存器, 对应关系如表 11-1 所示 : 表 11-1 A1A 寄存器 StateReg RateReg ResultReg ControlReg 可以看出, 这个循环实际上是分为两个部分的,A1A0 之后开始的八个时钟周期是写寄存器的过程, 隔两个时钟周期之后的 12 个时钟周期是读寄存器的过程 最后直到片选信号 CS 转换为低, 这个循环才结束 根据以上的时序过程, 编写了寄存器读和寄存器写的函数 TsReadReg() 和 TsWriteReg() 在介绍这两个函数的实现之前, 先介绍一些简单的宏和函数, 他们将在后面的 266

267 设计中不断被使用到 #define TsEnable (rpdatc &= 0xfdff) // 因为有非门起作用 #define TsDisable (rpdatc = 0x200) #define TsSetSK (rpdatc = 0x80) #define TsResetSK (rpdatc &= 0xff7f) #define TsSetDI (rpdatc = 0x20) #define TsResetDI (rpdatc &= 0xffdf) #define TsDO (rpdatc & 0x40) 可以看出 PC8 是作为 CS 信号,PC7 作为 SK,PC5 作为 DI,PC6 作为 DO 控制 SK 信号, 得到一个上升沿翻转函数 : void RiseEdgeSK(void) { TsResetSK; TsSetSK; } 有了以上的宏和函数, 就可以开始编写寄存器读和写函数 1 寄存器读函数 TsReadReg(u8 regno): 首先判断如果 regno 大于 3, 则返回错误 : if (regno > 3) {return(0);} 如果 regno 在范围之内, 则芯片使能, 设置开始位, 读写位, 寄存器选择位 : TsResetDI; //leading zeros TsEnable;//start RiseEdgeSK(); TsSetDI; RiseEdgeSK(); TsResetDI; RiseEdgeSK(); if (regno&0x02) {TsSetDI; } else{tsresetdi; } RiseEdgeSK(); if (regno&0x01) {TsSetDI; } //start bit //WR bit //A1 bit //A0 bit else{tsresetdi; } RiseEdgeSK(); 由于这是读寄存器, 所以将读写位设置为 0, 使得写的八位无意义 : for (i=0; i<9; i++) //8 zero bits for WR data and one idle clock {RiseEdgeSK();} 接下来是开始寄存器的读过程, 该过程的流程图如下所示 : 267

268 data=0 i=0 RiseEdgeSK() TsDisable 否 是 i<12 RiseEdgeSK() TsDo 否 TsWait() 是 return(data) i++ data += (u16)(1<<(11-i)) 该过程的源码如下 : data = 0; for (i=0; i<12; i++) {RiseEdgeSK(); if(tsdo) {data += (u16)(1<<(11-i));} } RiseEdgeSK(); TsDisable; TsResetSK; 图 11-9 MK715 寄存器读流程 //read 12bit data from DO TsWait(); return(data); 2 寄存器写函数 TsWriteReg(U8 regno,u8 data): 参数中 regno 为要写的寄存器的号码,data 为要写入的数值 首先如果 regno 大于 3, 返回错误, 如果正确, 则芯片使能, 设置开始位, 读写位, 寄存器选择位, 这部分代码除了一处之外其余和读寄存器部分相同, 不同之 处在于读写位置 1, 因为写寄存器必须使得写的八位有效 寄存器的写过程的流程图如下所示 : i=0 否 i<8 是 data&0x80 否 TsResetDI RiseEdgeSK() data<<=1 i++ 是 TsSetDI 图 MK715 寄存器写流程 268

269 周期的后一部分对于写寄存器是没有意义的, 所以只是设置时钟上跳, 不做任何操作, 最后设置循环结束, 源码如下 : for (i=0; i<13; i++) {RiseEdgeSK(); } RiseEdgeSK(); TsDisable; TsResetDI; TsResetSK; TsWait(); 3 读坐标点函数 TsReadPoint(tspoint *nowpoint) 有了读写寄存器的函数, 就可以用来实现坐标点的检测 该函数的参数 nowpoint 是一个触摸屏的点结构 检测触摸点坐标的流程框图如下所示 : 图 检测触摸点坐标的流程 269

270 部分源代码如下 : for(i=0; i<2; i++){ TsReadReg(TsRegStatus); tsdata = TsReadReg(TsRegStatus); while(!(tsdata & 0x80)){ TsReadReg(TsRegStatus); tsdata = TsReadReg(TsRegStatus); if(!(tsdata&0x40)){ return(0); } } tsdata = TsReadReg(TsRegResult); if(tsdata & 0x400) {nowpoint->x = tsdata&0x3ff;} else {nowpoint->y = tsdata&0x3ff;} 以上是两次从结果寄存器中读出坐标, 判断它们是 X 坐标还是 Y 坐标, 存到触摸屏点结构的成员中 触摸屏的初始化 : 有了寄存器的读写和触摸屏点的检测函数, 触摸屏驱动基本上已经完成, 剩下的就是对触摸屏的初始化 首先是端口的初始化, 包括定义 PC6 为输入端口,PC5,PC7,PC9 为输出端口 这一部在之前的端口初始化中已经实现 接下来是对 CS,DI,SK 清 0: TsDisable; TsResetSK; TsResetDI; 初始化的最后一步是寄存器写控制字, 配置工作模式, 扫描速率 : TsWriteReg(TsRegStatus,4); TsWriteReg(TsRegStatus,2); 以上两行代码确定了控制器工作在第一种模式下, 同时翻转一次 PD 位, 保证没有之前的异常中断干扰 TsWriteReg(TsRegRate,30);// 扫描频率为 100pps TsWriteReg(TsRegControl,0);// 其余时钟使用默认值 ; LCD 触摸屏点结构和两者的转换: 由于要对坐标进行操作, 所以定义了触摸屏和 LCD 的点结构如下 : typedef struct tspoint{ u16 x; u16 y; }tspoint;// 触摸屏的点结构 270

271 typedef struct lcdpoint { u16 row; u16 col; }lcdpoint;//lcd 点结构 触摸屏的分辨率是 ,LCD 显示屏是 的, 必须设计一个坐标转换的函数, 将触摸屏点的坐标转换为 LCD 显示屏的点坐标, 则之后一切的坐标计算可以按照 LCD 显示屏的 来实现 该转换函数如下 : void TsPoint2LcdPoint(lcdpoint * lp,tspoint * tp) { lp->col= (u16)((tp->x-lcdoriginx)*320/(lcd320240x-lcdoriginx)+0.5); lp->row= (u16)((tp->y-lcdoriginy)*240/(lcd320240y-lcdoriginy)+0.5); } 3. 验证代码为了检验触摸屏检测点的功能正常与否, 在主程序中添加下一段代码 : while(1){ if(tsreadpoint(&nowtspoint)) TsPoint2LcdPoint(&nowLcdPoint,& nowtspoint); LcdPointOn(nowLcdPoint.row,nowLcdPoint.col); printf("x is d%,y is d%",nowlcdpoint.row,nowlcdpoint.col); }; 这段代码通过查询方式检测触摸事件的发生, 一旦检测到触摸, 就将触摸点的坐标转换为 LCD 显示屏的坐标, 点亮这个点, 并且在超级终端上输出该点的坐标值 11.3 触摸屏应用程序设计 为了将触摸屏功能投入到使用中去, 考虑设计一个简单的触摸式的 10 以内的整数计算器来验证触摸屏的功能 设计这一应用程序主要考虑以下一些问题 : 首先是要设计该程序的界面, 这一过程要调用 LCD 显示的一些基本功能函数 然后要考虑的问题是如何将触摸点的坐标与要对应的操作对应起来, 这里要考虑的问题就是将触摸点的坐标转换为 LCD 的坐标, 根据设计程序应用界面时候的坐标范围判断使用者的意图, 进行相应的操作 271

272 界面的设计 准备设计的是一个简单的 10 以内的整数计算器, 所以必须提供给用户数字和操 作符按钮, 当然必不可少的要有结构显示窗口 该图形界面如下图 所示 : / = 图 计算器界面 该图形界面的设计调用了画矩形框 LcdRectangle() 和显示字符串 LcdString() 函 数 LcdRectangle(21,81,160,40,0,0,0);// 结果框 LcdRectangle(61,81,40,40,0,0,0);//1 LcdRectangle(101,81,40,40,0,0,0);//4 LcdRectangle(141,81,40,40,0,0,0);//7 LcdRectangle(181,81,40,40,0,0,0);//0 LcdRectangle(61,121,40,40,0,0,0);//2 LcdRectangle(101,121,40,40,0,0,0);//5 LcdRectangle(141,121,40,40,0,0,0);//8 LcdRectangle(181,121,40,40,0,0,0);//'/' LcdRectangle(61,161,40,40,0,0,0);//3 LcdRectangle(101,161,40,40,0,0,0);//6 LcdRectangle(141,161,40,40,0,0,0);//9 LcdRectangle(181,161,80,40,0,0,0);//'=' LcdRectangle(61,201,40,40,0,0,0);//'+' LcdRectangle(101,201,40,40,0,0,0);//'-' LcdRectangle(141,201,40,40,0,0,0);//'*' LcdString(80,12.5,0,"1"); LcdString(80,17.5,0,"2"); LcdString(80,22.5,0,"3"); LcdString(80,27.5,0,"+"); LcdString(120,12.5,0,"4"); LcdString(120,17.5,0,"5"); 272

273 LcdString(120,22.5,0,"6"); LcdString(120,27.5,0,"-"); LcdString(160,12.5,0,"7"); LcdString(160,17.5,0,"8"); LcdString(160,22.5,0,"9"); LcdString(160,27.5,0,"*"); LcdString(200,12.5,0,"0"); LcdString(200,17.5,0,"/"); LcdString(200,25,0,"="); 触摸操作处理函数 检测到有触摸后, 将该触摸点的坐标转换为 LCD 显示的坐标, 根据坐标范围确定该点的位置, 进行对应的操作 程序设计中, 使用一个整型的字符串 a[4] 来保存算式中的字符, 首先编写一个数学计算函数, 来对算式进行计算, 等待调用, 该函数如下 : void CalOperat(int arg[4]) { int op1=arg[0],op2=arg[2],optor=arg[1],rsl=0; switch(optor){ case '+':rsl=op1+op2;break; } case '-':rsl=op1-op2;break; case '*':rsl=op1*op2;break; case '/':rsl=op1/op2;break; }; arg[4]=(0,0,0,0); j=0; LcdString(40,25,0,rsl); 该函数的功能一目了然, 判断操作符的类型进行相应的运算, 将结果显示到界 面的结果框中, 将字符串清 0 接下来要解决的问题是如何根据坐标进行相应的操作, 该过程可以描述如下 : 判断坐标的位置, 决定用户触摸的是数字还是操作符 273

274 如果是数字 temp(0=<temp<=9), 则根据以下流程图进行 : 图 触摸到数字时处理流程 如果是运算符, 则首先判断是不是 =, 如果不是, 则按照以下流程图进行 : 图 触摸到字符时处理流程 如果是 =, 则调用之前定义的运算函数, 输出结果 小结 本章主要介绍了触摸屏的工作机理, 并重点以一款触摸屏为例讲述如何设计触摸屏底层驱动函数 本章最后, 以一个简单的计算器为例, 讲解了触摸屏应用程序设计流程 274

275 第 12 章网络设计及编程 网络技术的出现是 20 世纪人类历史上最伟大的发明之一, 它彻底改变了人们的 生活以及工作习惯, 使人类第一次真正意义上实现了共享全球信息 : 人们可以通过网络进行聊天 发送和接收 看网络电影以及网上购物等等, 可以说网络的出现已经渗入到人们生活和工作的各个方面, 今天很难相信如果人类的生活和工作中没有网络 ( 哪怕就 1 分钟 ), 将会变成什么样子? 嵌入式系统, 做为后 PC 时代的产物, 将不可避免的打上网络的烙印 因此, 目前嵌入式系统微处理器一般都会内嵌网络接口, 即使如果没有内嵌网络接口, 一般也会比较容易加上一款网络芯片, 使其能够方便的为用户提供网络接口 本章将主要以 S3C44B0X 微处理器平台为例, 详细介绍以太网接口电路设计和底层函数编程 本章内容如下 : 网络概述, 向读者介绍著名的 OSI 七层模型和 TCP/IP 模型 ; 以太网的几种帧格式 ; 以 RTL8019 为例介绍以太网硬件接口设计 底层函数编程等内容 ; 详细介绍了在 Linux 平台下以太网驱动程序的设计 12.1 网络概述 OSI 七层模型 1. 概述 开放式系统互联模型 (OSI) 由国际标准化组织 (ISO)1984 年提出的一个参考模型 作为一个概念性框架, 它是不同制造商的设备和应用软件在网络中进行通信的标准 现在此模型已成为计算机间和网络间进行通信的主要结构模型 目前使用的大多数网络通信协议的结构都是基于 OSI 模型的 2.OSI 分层 OSI 将通信过程定义为七层, 即将连网计算机间传输信息的任务划分为七个更小 更易于处理的任务组 每一个任务或任务组则被分配到各个 OSI 层 每一层都是独立存在的, 因此分配到各层的任务能够独立地执行 这样使得变更其中某层提供的方案时不影响其他层 OSI 七层模型的每一层都具有清晰的特征 基本来说, 第七至第四层处理数据 275

276 源和数据目的地之间的端到端通信, 而第三至第一层处理网络设备间的通信 另外, OSI 模型的七层也可以划分为两组 : 上层 ( 层 7 层 6 和层 5 ) 和下层 ( 层 4 层 3 层 2 和层 1 ) OSI 模型的上层处理应用程序问题, 并且通常只应用在软件上 最高层, 即应用层是与终端用户最接近的 OSI 模型的下层是处理数据传输的 物理层和数据链路层应用在硬件和软件上 最底层, 即物理层是与物理网络媒 介 ( 比如说, 电线 ) 最接近的, 并且负责在媒介上发送数据 OSI 七层模型示意如图 12-1 所示 通话方 : 通话内容 : 美国公司的 通话内容 : 获得美国 通话方 : 美国老板 老板要向中国公司订货 第七层应用层 公司的订货要求 中国老板 通话方 : 通话内容 : 将订货请求 通话内容 : 将信函内 通话方 : 美国秘书 写成 2 封英文商务信函 第六层表示层 容翻译成中文 中国秘书 通话内容 : 将 2 封信函编 号并写上中国公司地址 第 5 层会话层 通话内容 : 去掉信封, 并 将 2 封信内容连接起来 通话方 : 通话内容 : 根据地址, 通话内容 : 根据信函 通话方 : 快递公司 决定传输方式和手段, 给每信封贴上标签 第 4 层传输层 上的地址检查邮件 快递公司 通话内容 : 根据标签 通话内容 : 根据标签 选择投递路线 第 3 层网络层 选择投递路线 通话内容 : 在投递路线 通话内容 : 在投递路 的每个节点检查邮件是否损坏 第 2 层链路层 线的每个节点检查邮 件是否损坏 通话方 : 通话内容 : 通话内容 : 通话方 : 美国交通工具 传递信件 第 1 层物理层 传递信件 中国交通工具 各层的具体描述如下 : (1) 应用层功能 : 图 12-1 OSI 七层模型示意 定义了用于在网络中进行通信和数据传输的接口 - 用户程式 ; 提供标准服务, 比如虚拟终端 文件以及任务的传输和处理 ; 276

277 (2) 表示层 : 掩盖不同系统间的数据格式的不同性 ; 指定独立结构的数据传输格式 ; 数据的编码和解码 ; 加密和解密 ; 压缩和解压缩 (3) 会话层 : 管理用户会话和对话 ; 控制用户间逻辑连接的建立和挂断 ; 报告上一层发生的错误 (4) 传输层 : 管理网络中端到端的信息传送 ; 通过错误纠正和流控制机制提供可靠且有序的数据包传送 ; 提供面向无连接的数据包的传送 ; (5) 网络层 : 定义网络设备间如何传输数据 ; 根据唯一的网络设备地址路由数据包 ; 提供流和拥塞控制以防止网络资源的损耗 (6) 数据链路层 : 定义操作通信连接的程序 ; 封装数据包为数据帧 ; 监测和纠正数据包传输错误 (7) 物理层 : 定义通过网络设备发送数据的物理方式 ; 作为网络媒介和设备间的接口 ; 定义光学 电气以及机械特性 通过 OSI 层, 信息可以从一台计算机的软件应用程序传输到另一台的应用程序上 例如, 计算机 A 上的应用程序要将信息发送到计算机 B 的应用程序, 则计算 机 A 中的应用程序需要将信息先发送到其应用层 ( 第七层 ), 然后此层将信息发送到表示层 ( 第六层 ), 表示层将数据转送到会话层 ( 第五层 ), 如此继续, 直至物理层 ( 第一层 ) 在物理层, 数据被放置在物理网络媒介中并被发送至计算机 B 计 算机 B 的物理层接收来自物理媒介的数据, 然后将信息向上发送至数据链路层 ( 第二层 ), 数据链路层再转送给网络层, 依次继续直到信息到达计算机 B 的应用层 最后, 计算机 B 的应用层再将信息传送给应用程序接收端, 从而完成通信过程 下面图示说明了这一过程 277

278 应用层数 A B 应用层数 头 表示层数 头 表示层数 头 会话层数据 头 会话层数据 头 传输层数据 头 传输层数据 头 网络层数据 头 网络层数据 头 链路层数据 头 链路层数据 头 物理层数据 头 物理层数据 图 12-2 网络通信流程示意 虽然 OSI 模型可以让人更好的理解网络, 但其相关协议却没有在现实的网络中 流行 Andrew S. Tanenbaum 在 Computer Network 一书中这样总结了下面几个原因 : 糟糕的时机 :OSI 协议出现时,TCP/IP 协议已经被广泛应用在大学和科研 机构中, 而且 TCP/IP 非常简单, 没有人再去开发 OSI 相关协议 糟糕的技术 :OSI 协议非常复杂, 实现过于困难, 七层协议太过繁琐, 在现实的网络中, 表示层和会话层几乎就是空的, 而数据层和网络层又包含 太多东西 糟糕的实现 :OSI 协议过于复杂, 因而实现起来显得庞大而笨拙 糟糕的策略 :OSI 基本上是欧洲电信和美国政府的产物 有人甚至开玩笑 说,OSI 模型应该还有个第 8 层 : 政治层, 这是对 OSI 模型的复杂和官僚的莫大讽刺 TCP/IP 模型 1. 概述 TCP/IP 是美国政府资助的高级研究计划署 (ARPA) 在二十世纪七十年代的一个研究成果, 用来使全球的研究网络联在一起形成一个虚拟网络, 也就是国际互联网 原始的 Internet 通过将已有的网络如 ARPAnet 转换到 TCP/IP 上来而形成, 而这个 278

279 I nternet 最终成为如今的国际互联网的骨干网 TCP/IP( 传输控制协议 / 网间协议 ) 是一种网络通信协议, 它规范了网络上的所 有通信设备, 尤其是一个主机与另一个主机之间的数据往来格式以及传送方式 TCP/IP 是 INTERNET 的基础协议, 也是一种电脑数据打包和寻址的标准方法 在数据传送中, 可以形象地理解为有两个信封,TCP 和 IP 就像是信封, 要传递的信息被 划分成若干段, 每一段塞入一个 TCP 信封, 并在该信封面上记录有分段号的信息, 再将 TCP 信封塞入 IP 大信封, 发送上网 在接受端, 一个 TCP 软件包收集信封, 抽出数据, 按发送前的顺序还原, 并加以校验, 若发现差错,TCP 将会要求重发 因此,TCP/IP 在 INTERNET 中几乎可以无差错地传送数据 在任何一个物理网络中, 各站点都有一个机器可识别的地址, 该地址叫做物理地址 物理地址有两个特点 : (1) 物理地址的长度, 格式等是物理网络技术的一部分, 物理网络不同, 物理地址 也不同 (2) 同一类型不同网络上的站点可能拥有相同的物理地址 2.TCP/IP 分层 TCP/IP 分层模型 (TCP/IP Layening Model) 被称作因特网分层模型 (Internet Layering Model) 因特网参考模型(Internet Reference Model) 图 12-3 表示了 TCP/IP 分层模型 的四层 D F W F H G T S N I H T T O E M 第四层, 应用层 S N O P T P L T G I P H E P E S E T R R 第三层, 传输层 TCP UDP 第二层, 网间层 IP ICMP 图 12-3 TCP/IP 四层参考模型 TCP/ IP 协议被组织成四个概念层, 其中有三层对应于 ISO 参考模型中的相应层 ICP/IP 协议族并不包含物理层和数据链路层, 因此它不能独立完成整个计算机网络系统的功能, 必须与许多其他的协议协同工作 TCP/IP 分层模型的四个协议层分别完成以下的功能 : 第一层 : 网络接口层网络接口层包括用于协作 IP 数据在已有网络介质上传输的协议 实际上 TCP/IP 标准并不定义与 ISO 数据链路层和物理层相对应的功能 相反, 它定义像地址解析协议 (Address Resolution Protocol,ARP) 这样的协议, 提供 TCP/IP 协议的数据结构和实际物理硬件之间的接口 第二层 : 网间层 279

280 网间层对应于 OSI 七层参考模型的网络层 本层包含 IP 协议 RIP 协议 (Routing Information Protocol, 路由信息协议 ), 负责数据的包装 寻址和路由 同时还包含网间控制报文协议 (Internet Control Message Protocol,ICMP) 用来提供网络诊断信息 第三层 : 传输层传输层对应于 OSI 七层参考模型的传输层, 它提供两种端到端的通信服务 其中 TCP 协议 (Transmission Control Protocol) 提供可靠的数据流运输服务,UDP 协议 (Use Datagram Protocol) 提供不可靠的用户数据报服务 第四层 : 应用层应用层对应于 OSI 七层参考模型的应用层和表达层, 它定义了应用程序使用互联网的规程 因特网的应用层协议包括 Finger Whois FTP( 文件传输协议 ) Gopher HTTP( 超文本传输协议 ) Telent( 远程终端协议 ) SMTP( 简单邮件传送协议 ) IRC( 因特网中继会话 ) NNTP( 网络新闻传输协议 ) 等, 关于这些协议的具体相关内容请参考相关书籍 TCP/IP 如此重要的原因, 在于它允许独立的网格加入到 Internet 或组织在一起形成私有的内部网 (Intranet) 构成内部网的每个网络通过一种- 做路由器或 IP 路由器的设备在物理上联接在一起 路由器是一台用来从一个网络到另一个网络传输数据包的计算机 在一个使用 TCP/IP 的内部网中, 信息通过使用一种独立的叫做 IP 包 (IPpacket) 或 IP 数据报 (IP datagrams) 的数据单元进 -- 传输 TCP/IP 软件使得每台联到网络上的计算机同其它计算机 看 起来一模一样, 事实上它隐藏了路由器和基本的网络体系结构并使其各方面看起来都像一个大网 如同联入以太网时需要确认一个 48 位的以太网地址一样, 联入一个内部网也需要确认一个 32 位的 IP 地址 我们将它用带点的十进制数表示, 如 给定一个远程计算机的 IP 地址, 在某个内部网或 Internet 上的本地计算机就可以像处在同一个物理网络中的两台计算机那样向远程计算机发送数据 12.2 以太网接口设计 以太网概述 1. 以太网原理 以太网是 Xerox 公司在 20 世纪 70 年代发明的基带 LAN 标准, 最初以太网是为解决网络中零散的和偶然的堵塞而开发的, 它采用带冲突检测的载波监听多路访问协议 (CSMA/CD), 速率为 10Mbps, 传输介质为同轴电缆 1980 年 IEEE802.3 标准在最初的以太网技术基础上开发成功, 现在以太网一词泛指所有采用 CSMA/ CD 协议的局域网 以太网 2.0 版由数字设备公司 Intel 公司和 Xerox 公司联合开发, 它与 IEEE802.3 兼容 以太网和 IEEE802.3 通常由接口卡 ( 网卡 ) 或主电路板上的电路实现 以太网 280

281 电缆协议规定用收发器将电缆连到网络物理设备上 收发器执行物理层的大部分功能, 其中包括冲突检测及收发器电缆将收发器连接到工作站上 通常情况下以太网卡由下面几个部分构成, 详见图 12-4 所示 发送控制部分 L NRZ 到曼彻 A 斯特转换器 发送器 N 管理 曼彻斯特到 NRZ 转换器 载波监听 接收器 部件 接收控制部分 微处理器 图 12-4 以太网卡结构示意图 根据对 CSMA/CD 访问方法的描述, 节点网卡要执行多种任务, 因此, 每个网 口要有自己的控制器, 用以确定何时发送, 何时从网络上接受数据, 并负责执行 所规定的规程, 如构成帧, 计算帧检验序列 执行编码译码转换等 各个部件功能如下 : LAN 管理部分和微处理器 :LAN 的管理部分是网卡的核心, 负责执行所有规程和数据处理 微处理器部分包括微处理器芯片,RAM 芯片和 ROM 芯片 这一部分在 PC 机和 LAN 管理部分间提供链接 当 PC 机有数据要发 送时, 便中断微处理器部分, 并将数据存储在微处理器部分的 RAM 芯片中, 命令它发送数据 微处理器还将来自 PC 机的信号转换为 LAN 管理部分可接受的格式, 随后命令 LAN 管理部分将数据发送到网络上 微处理监 视发送过程, 经常访问 LAN 管理部分, 以检查发送是否成功 一旦 PC 机准备好从网络上接收帧, 它便中断微处理器, 并通知它能进行帧的接收 微处理器通过命令 LAN 管理部分开始接收帧来响应 微处理器对帧的接收过 程进行监视 一旦接收的帧由 LAN 管理部分处理结束, 微自理器便中断 PC, 将接收的数据传给 PC 机 应该指出, 有些网板不含有微处理器部分, 在这种情况下,PC 机直接控制和监视 LAN 管理部分的工作 曼彻斯特编译码器 :IEEE802.3 或 Ethernet 规定数据的传输必须用曼彻斯特编码进行 当 PC 机希望将数据发送到网络上时, 总是以并行方式逐字节地传给 LAN 管理部分,LAN 管理部分串行传给 不归零 (NRZ) 曼彻斯特编 码器, 在这里进行曼彻斯特编码 发送和发送控制部分 : 发送和接收控制部分负责帧的发送 由图 10-3 可以看出, 发送部分接受来自 NRZ 曼彻斯特转换器 的曼彻斯特码的数据, 并在发送控制部分允许的条件下将数据发送到媒体, 发送的数据称为 TxD 281

282 发送控制部分判定是否进行发送, 这种判定基于 LAN 基于管理部分和 TxD 来进行 接收和接收控制部分 : 接收和接收控制部分负责帧的接收 这一部分产生网络是否有载波存在的信号, 产生的依据是从 RxD 中获得 因此, 网络上来的信号一方面馈送给接收器, 另一方面要馈送给接收控制部分 接收控 制部分根据 LAN 管理部分和媒体上接收的信号判定是否使接收器工作 2. 以太网帧格式目前, 有四种不同格式的以太网帧在使用, 它们分别是 : Ethernet II 即 DIX 2.0:Xerox 与 DEC Intel 在 1982 年制定的以太网标准帧格式 Cisco 名称为 :ARPA Ethernet raw:novell 在 1983 年公布的专用以太网标准帧格式 Cisco 名称为 :Novell-Ether Ethernet SAP:IEEE 在 1985 年公布的 Ethernet 的 SAP 版本以太网帧格式 Cisco 名称为 :SAP Ethernet SNAP:IEEE 在 1985 年公布的 Ethernet 的 SNAP 版本以太网帧格式 Cisco 名称为 :SNAP 在每种格式的以太网帧的开始处都有 64 比特 (8 字节 ) 的前导字符, 如图 12-5 所示 其中, 前 7 个字节称为前同步码 (Preamble), 内容是 16 进制数 0xAA, 最后 1 字节为帧起始标志符 0xAB, 它标识着以太网帧的开始 前导字符的作用是使接收节点进行同步并做好接收数据帧的准备 图 12-5 以太网帧前导字符 除此之外, 不同格式的以太网帧的各字段定义都不相同, 彼此也不兼容 下面简单介绍这几种帧格式 : (1) Ethernet II 帧格式 如图 10-6 所示, 是 Ethernet II 类型以太网帧格式 6 字节 6 字节 2 字节 46~1500 字节 4 字节 目标 MAC 地址源 MAC 地址类型 / 长度数据段 校验段 图 12-6 Ethernet II 帧格式 Ether net II 类型以太网帧的最小长度为 64 字节 ( ), 最大长度为 1518 字节 ( ) 其中前 12 字节分别标识出发送数据帧的源节点 MAC 地址和接收数据帧的目标节点 MAC 地址 接下来的 2 个字节标识出以太网帧所携带的上层数据类型, 如 16 进制数 0x0800 代表 IP 协议数据,16 进制数 0x809B 代表 AppleTalk 协议数据,16 进制数 0x8138 代表 Novell 类型协议数据等 在不定长的数据字段后是 4 个字节的帧校验序列 (Frame Check Sequence,FCS), 采用 32 位 CRC 循环冗余校验对从 " 目标 MAC 地址 " 字段到 " 数据 " 字段的数据进行校 282

283 验 (2) Ethernet raw 帧格式如图 12-7 所示, 是 Ethernet raw 类型以太网帧格式 6 字节 6 字节 2 字节 2 字节 44~1498 字节 4 字节 目标 MAC 地址源 MAC 地址总长度 0XFFFF 数据段 校验段 图 12-7 Ethernet raw 帧格式 在 Ethernet raw 类型以太网帧中, 原来 Ethernet II 类型以太网帧中的类型字 段被 " 总长度 " 字段所取代, 它指明其后数据域的长度, 其取值范围为 : 接下来的 2 个字节是固定不变的 16 进制数 0xFFFF, 它标识此帧为 Novell 以太类型数据帧 (3) Ethernet SAP 帧格式如图 12-8 所示, 是 Ethernet SAP 类型以太网帧格式 6 字节 6 字节 2 字节 1 字节 1 字节 1 字节 43~1497 字节 4 字节 目标 MAC 地址源 MAC 地址总长度 DSAP SSAP 控制数据段 图 12-8 Ethernet SAP 帧格式 从图中可以看出, 在 Ethernet SAP 帧中, 将原 Ethernet raw 帧中 2 个 字节的 0xFFFF 变为各 1 个字节的 DSAP 和 SSAP, 同时增加了 1 个字节的 " 控制 " 字段, 构成了 逻辑链路控制 (LLC) 的首部 LLC 提供了无连接 (LLC 类型 1) 和面向连接 (LLC 类型 2) 的网络服务 LLC1 是应用于以太网中, 而 LLC2 应用在 IBM SNA 网络环境中 新增的 LLC 首部包括两个服务访问点 : 源服务访问点 (SSAP) 和目标服务访问点 (DSAP) 它们用于标识以太网帧所携带的上层数据类型, 如 16 进制数 0x06 代表 IP 协议数据,16 进制数 0xE0 代表 Novell 类型协议数据,16 进制数 0xF0 代表 IBM NetBIOS 类型协议数据等 至于 1 个字节的 " 控制 " 字段, 则基本不使用 ( 一般被设为 0x03, 指明采用无连 接服务的 无编号数据格式 ) ( 4) Ethernet SNAP 帧格式如图 12-9 所示, 是 Ethernet SNAP 类型以太网帧格式 校验段 6 字节 6 字节 2 字节 1 字节 1 字节 1 字节 3 字节 2 字节 38~1492 字节 4 字节 目标 MAC 地址源 MAC 地址总长度 0XAA 0XAA 0X03 OUI ID 类型数据段 校验段 图 12-9 Ethernet SNAP 帧格式 Ethernet SNAP 类型以太网帧格式和 Ethernet SAP 类型以太网帧格式的主要区别在于 : 2 个字节的 DSAP 和 SSAP 字段内容被固定下来, 其值为 16 进制数 0xAA 1 个字节的 " 控制 " 字段内容被固定下来, 其值为 16 进制数 0x03 增加了 SNAP 字段, 由下面两项组成 : 新增了 3 个字节的组织唯一标识符 (Organizationally Unique Identifier, 283

284 OUI ID) 字段, 其值通常等于 MAC 地址的前 3 字节 通常情况下, 我们使用 Ethernet II 帧格式, 可以采用如下形式定义以太网帧的结构体格式 : /* 以太网帧头 */ typedef struct { unsigned char h_dest[eth _ALEN]; /* destination eth addr */ unsigned char h_source[eth_alen]; /* source ether addr */ unsigned short h_proto; /* packet type ID field */ } ETHHDR; /* 以太网帧格式 */ typedef struct { ETHHDR efh; /* 帧头 */ unsigned char data[1500]; /* 帧数据 */ DWORD crc; /*CRC 校验码 */ }ETHERFRAME; 以太网控制器 RTL8019AS 1.RTL8019 简介 RTL8019AS 是集成度高以太网控制器, 它能够简单的解答即插即用 NE2000 兼容适配器, 这种适配器具有二重和功率下降特性 通过三电平控制特性,RTL8019AS 是已制的对网络设备 GREEN PC 理想的选择 全二重功能能够模拟传播和接收在双绞线到全二重以太网交换机 这个特性不仅强带宽从 10 到 20MBPS, 而且避免了由于以太网频道争夺特性导致的读出多路存取协议的问题 微软公司的即插即用功能能减轻用户较差的营业收入而注意适配器资源, 如 IRQ, 输入输出, 和存储器地址等等 然而, 为了特殊的应用而得不到即插即用功能的兼容性,RTL8019AS 支持 JUMPER 和 JUMPERLESS 选项 为了提供完全解决即插即用方案,RTL8019AS 在集成 10BASET 收发器,BNC, 和 AUI 接口之间的自动检测功能 此外,8 条 IRQ 总线和 16 条基本地址总线为大资源情况下提供了宽松的环境 RTL8019AS 支持 16 k,32k, 和 64k 字节 BROM 和闪存接口 它仍然提供页面模式功能, 这种功能能支持在仅 16k 字节内存系统空间下的 4M 字节的 BROM 此外,BROM 的无用命令被用来释放 BROM 内存空间 RTL8019AS 用 16k 字节 SRAM 设计在单片芯片上, 它的设计不仅提供了更多友好的功能, 而且节省了 SRAM 存储资源 2.RTL8019 引脚结构 RTL8019AS 芯片是 100 脚 PQFP 封装, 其引脚配置如图 所示 284

285 图 RTL8019AS 引脚配置这 100 个引脚分为下列几组 : 电源脚编号名称类型描述 6,17,47,57,70,89 VDD P +5V 14,28,44,52,83,86 GND P GROUND ISA 总线接口管脚 编号名称类型描述 34, AEN I 地址 使能 脚 ISA 信号对有效的输入输出命令, 必须是低电平 , INT7-0 O 中断请求总线 : 能够分别映射到 1-4 IRQ15,IRQ12,IRQ10, IRQ5,IRQ4,IRQ3,IRQ2/9. 285

286 唯一一条线被选择在一个时间里反映中断请求 其他的线都是 tri-stated.rtl8019as 仍然 用这些脚座位输入线, 从而管理 ISA 总线上实际相应的中断线上的状态 结果记录在 INTR 寄存器中, 这个寄存器可用软件用来保护中断冲突 35 IOCHRDY O 受低电平作用循环等待当前读写指令 96 IOCS16B [SLOT16] O 根据电源复位, 以 SLOT16 作为输入信号名称来检测 16-bit 或者 8-bit 在使用中 它被连接到一个电位器上 (27kw) 在 RSTDRV 的下降沿,RTL8019AS 能判断它的状态 如果为高电平, 适配器是放置在 16bit 槽中, 其脚被连接到主机的 IOCS16B 脚上, 这支脚同样被主板上的 300w 电阻拔起 如果是低电平, 适配器是放置在 8-bit 槽中, 被 27vkw 电阻拔起 通过锁住输入状态, 它被转变作为 IOCS16B 信号, 其是一个开放排水沟型输出端, 而且在 16bit 主机数据转变下为低电平 它被解码为 AEN 和 SA IORB I 输入输出读指令端 30 IOWB I 输入输出写指令端 33 RSTDRV I ISA 总线上的高效硬件复位端 少于 800ns 的 高电平脉冲被忽略 27-18, SA19-0 I 地址总线 SA10 用来实现 PNP 端口的完全 16-15, 13-17,5 解码, 地址为 279h 和 A79h 在 RTL8019AS 中,SA10 未被解码 SA10 以 0 作为可提供的接近 pnp 端口 87-88, 90-95, SD15-0 I/O 数据总线 31 SMEMR B I 存储器读命令 286

287 32 SMEMWB I 存储器写命令 用来闪存写命令解码 存储器接口管脚 ( 包括 BROM,EEPROM) 编号 名称 类 描述 型 75 BCSB O BROM 片选端 低电平有效, 为读信号 当 SA19-14 和被选的 BROM 地址想匹配以及满足一下两个条件之一时,RTL8019AS 驱动其为低电平 1,SMEMRB 为低 2,SMEMRB 为低并且 RTL8019AS 的闪存读功能禁止 76 EECS O 9346 片选 高电平有效,9346 读 / 写 66-69, 71-74, 77-82,84-85 [79] [78] [77] [66] [72-71,69-67] [85-84,82-81] [77,74] [80-78] 65 BA21-14 BD7-0 [EESK] [EEDI] [EEDO] [PNP] [BS4-0] [IOS3-0] [PL1-0] [IRQS2-0] JP O I/O O O I I I I I I I BROM 地址 BROM 数据线 9346 串行数据时钟 9346 串行数据输入 9346 串行数据输出下列管脚是为了定义跳跃者选项 它们在 RSTDRV 下降沿时被锁定, 然后它们被用作 SRAM 总线 每个被 100KW 的内部电阻下拉 因此, 当左开而且为高电平当其被 10k 电阻上拉时, 输入为低 当 jp=low,til8019as 被强制为即插即用模式下列各项不需要注意 jumperless 模式 (jp=low) 选择 BROM 大小和地址选择 I/O 地址选择网络媒体类型在 IN7-0 中选择一个中断当为高电平, 将选择 jumper 模式 当为低, 选择 jumperless 模式 287

288 媒体接口管脚 编号 名称 类 描述 型 64 AUI I 用来检测在 AUI 接口的外部 MAU 的使用情况 输入对 嵌入的 BNC 必须为低电平, 对 MAU 必须为高 当输入 54, 53 为高电平,RTL8019AS 设置 AUI 位为 CONFIGO 并且驱动 LEDBNC 为低电平从而使 BNC 禁用 当这支脚未用时, 应该接地 详细见 CONGIG0 CD+,CD- I 是从 MAU 来的微分输入信号的进位 56, RX+,RX- I 这是 AUI 接收端对 MAU 接收微分输入信号的进位 55 49, 48 TX+,TX- O 这是一对传输输出的包含微分线性的驱动器, 它用来发送满切斯特编码数据到 MAU 这些输出是源输出和需要 270 欧姆的下拉电阻到地 59, TPIN+ I 这对 IP 输入收到 10Mbp s 的微分满切斯特编码 58 TPIN- 45, TPOUT+ O 这是一对进位微分的 tp 传输输出 输出满切斯特编码信 46 TPOUT- 号有预扭曲性, 以防止在双绞线媒体的 overcharge 因此减少资源紧张 50 X1 I 20Mhz 的晶体或者外部振荡器输入 51 X2 O 晶体反馈输出 这个输出是位移的晶体连接方法 它必 须是当 X1 在受外部振荡器驱动时左开的 LED 输出端口 编号 名称 类型 描述 60 LEDBNC O 当 RTL8019AS 媒体类型设置为 10BASE2 或者自动 检测模式并且有炼环测试失败时为高电平 可用来 控制对 CX MAU 的直流转换能量, 而且连接到 LED 以表明所用媒体类型 61 LED0 O 当 LEDS0 位为 ( 在 RTL8019AS 第三页 CONFIG3 register ) 0, 作为 LED_ COL. 当 LEDS0=1, 作为 LED_LINK. 62,63 LED1,LED2 O 当 LEDS1 位 ( 在 RTL8019AS 第三页 CONFIG3 register) 为 0, 这两个端作为 LED_RX 和 LED_TX 当 LEDS=1, 作为 LED_CRS 和 MCSB 详细见 6.5 中 288

289 2. 寄存器描述 RTL8019AS 中的寄存器根据地址和功能能够概略的分为两组 : 一组为 NE2000 兼容配置的, 另外一组为即插即用配置 本章的示例全为第一种形式, 因此这里只 简单介绍第一组寄存器, 有关第二组寄存器的使用请参考 RTL8019 的 datasheet 文档 NE200 0 寄存器组分为 4 个页面, 可以在 CR 寄存器中通过 PS0 和 PS1 被选择 每一页面包括 16 个寄存器, 这些寄存器除了和 NE2000 兼容外, RTL8019AS 为软 件结构和为了增强特性还定义了其它一些寄存器 由于寄存器较多, 下面简单介绍几个较重要的寄存器 : 1CR: 指令寄存器 (00H;Type=R/W) 这个寄存器用来选择寄存器页面, 使能或者禁止远程 DMA 操作和命令操作 其位定义如表 12-1 所示 表 12-1 CR( 命令寄存器 ) 位定义 位 符号 描述 选择寄存器页 PS1 PS0 寄存器页 注释 第 7~6 位 PS1,PS NE2000 兼容 NE2000 兼容 NE2000 兼容 RTL8019AS 配置 第 5~3 位 RD2~0 RD2 RD1 RD 0 功能 不允许 远程读取以太网控制器内存 远程写入以太网控制器内存 发送包 1 中止 / 完成远程 DMA 第 2 位 TXP 在发送数据包时, 将该位置 1, 这 1 位可能在发送完成后或者发送中止时内部清 0, 对该位进行写 0 操作没有任何作用 第 1 位 STA 写 STP 组合使用 该位是停止命令 如果为 1: 停止接收或发送任何数据包 0 STP STA STP 功能 1 0 启动命令 0 1 停止命令 289

290 位 2 ISR: 中断状态寄存器 (07H, type=r/w in page0) 这个寄存器反映 NIC 状态 主机读它来决定中断的原因 通过对相应的位写 1 来清除该位, 它必须在上电源后清除 其位定义如表 10-2 所示 符号 描述 表 12-2 ISR( 中断状态寄存器 ) 位定义 7 RST 当 NIC 输出设置状态时该位被设置 ; 当启动对 CR 命令时该位被清除 当收到冲器溢出时该位被设置 ; 当一个或者多个数据包从缓冲器中读取时该位被清楚 6 RDC 当远程 DMA 操作完成时被设置 5 CNT 当一个或多个网络计数器的 MSB 设置完成时该位被设置 4 OVW 当接收缓冲器用完时该位被设置 3 TXE 由于过分冲突导致成组传送失败时对该位传输错误位设置 2 RX E 当一个数据包接收有一下错误时该位被设置 : CRC 错误 帧同步错误 遗失数据包 1 PTX 该位表明无错误的数据包传送 0 PRX 该位表明无错误的数据包接收 3 其它寄存器 IMR: 中断屏蔽寄存器 (0F H ;Type= w in page0,type=r in page2) 每一位对应 ISR 中的每一位, 启动时全为 0, 因此此时屏蔽所有中断, 如果把某位置为 1, 将允许相对应的中断发生 CLDA0,1: 当前局部 DMA 寄存器 ( 01H 和 02H,type=R in page0) 通过读这两个寄存器来得到当前 D MA 地址 PSTART: 页面开始寄存器 ( 01H,Type=2 in page0, Type= R in page2) 该寄存器用来设置接收缓冲器的开始页面地址 PS TOP: 页面停止寄存器 (02H; Type=W in Page0, Type=R in Page2) 该寄存器设置接收缓冲器停止页面寄存器地址 在 8 位方式 下 PSTO P 寄存器不应该超过 0X60, 在 6 位方式下阿 PSTOP 寄存器应该不超过 0x80 BN RY: 边界寄存器 (03H;Type=R/ W in page0) 这个寄存器是用来放置接收缓冲器的重写 它代表性的作用是作为接收缓冲器最后页面的指针 TPSR: 传送页面开始寄存器 (04H;Type=W in page0) 用来设置传送数据包开始页面地址 290

291 TBCR0,1: 传输字节计算寄存器 (05H&06H;Type= 置传输数据包的字节计数 W in page0) 用来设 NCR: 冲突数寄存器 (05H;type=R in page0) 用来记录在数据包传送过程重的冲突节点数 FIFO: 先进先出寄存器 (06H; Type=R in Page0) 这个寄存器允 许主机检查在 loopback 后的 FIFO 内容 CRDA0,1: 当前远程 DMA 寄存器 ( 08H & 09H; Type=R in Page0) 这个两个寄存器包括当前远程 DMA 地址 RSAR0,1: 远程起始地址寄存器 ( 08H & 09H; Type=W in Page0) 这两个寄存器设置远程 DMA 起始地址 RBCR0,1: 远程字节数寄存器 (0AH & 0BH; Type=W in Page0) 设置远程 DMA 数据字节数 CNTR0: 帧同步错误计数寄存器 (0D CNTR1 H; Type=R in Page0) :CRC 错误数记录寄存器 (0EH; Type=R in Page0) CNTR2: 遗失数据包数记录寄存器 (0FH; Type=R in Page0) PAR0-5: 实际地址寄存器 (01H - 06H; Type=R/W in Page1) 这 些寄存器包括以太网节点地址且用来对目标地收或者拒绝接收 址数据包进行比较来确定接 CURR: 当前页面寄存器 (07H; Type=R/W in Page1) 这个寄存器 指出首先接收缓冲器页面地址, 这个页面用来对数据包的接待 MAR0-7: 多点地址寄存器 (08H - 0FH; Type=R/W in Page1) 些寄存器提供被 CRC 逻辑变位无用的多点地址的滤波位 这 291

292 以太网接口电路设计 图 12-3 S3C44B0X 平台下网络接口 ( 采用 RTL8019AS 为例 ) 设计 RTL8019AS 底层函数编程 1. 寄存器映射由于在图 10-3 中, 采用 ngcs3 作为以太网模块的片选信号, 因此该以太网卡映射到实验平台的 Bank3 上, 基地址为 0x , 定义如下 : #define BaseAddr0x #define SHIFT(x) (x<<1) #define RWPORT (BaseAddr+SHIFT(0x10)) /* dma read write address, form 0x10-0x17 */ #define RstAddr (BaseAddr+SHIFT(0x18)) /* reset register, 0x18, 0x1a, 0x1c, 0x1e even address is recommanded */ /* page 0 */ #define Pstart (BaseAddr+SHIFT(1)) /* page start */ #define Pstop (BaseAddr+SHIFT(2)) /* page stop */ 292

293 #define BNRY #define TPSR (BaseAddr+SHIFT(3)) (BaseAddr+SHIFT(4)) /* transmit page start */ #define TSR (BaseAddr+SHIFT(4)) #define TBCR0 (BaseAddr+SHIFT(5)) #define TBCR1 (BaseAddr+SHIFT(6)) #define ISR (BaseAddr+SHIFT(7)) /* interrupt status register */ #define RSAR0 (BaseAddr+SHIFT(8)) /* dma read address */ #define RSAR1 (BaseAddr+SHIFT(9)) #define RBCR0 (BaseAddr+SHIFT(10)) /* dma read byte count */ #define RBCR1 (BaseAddr+SHIFT(11)) #define RCR (BaseAddr+SHIFT(12)) /* receive config */ #define RSR (BaseAddr+SHIFT(12)) #define TCR (BaseAddr+SHIFT(13)) /* transmit config */ #define DCR (BaseAddr+SHIFT(14)) /* data config */ #define IMR (BaseAddr+SHIFT(15)) /* interrupt mask */ #define ID8019L (BaseAddr+SHIFT(10)) #define ID8019H (BaseAddr+SHIFT(11)) /* page 1 */ #define PAR0 (BaseAddr+SHIFT(1)) #define PAR1 (BaseAddr+SHIFT(2)) #define PAR2 (BaseAddr+SHIFT(3)) #define PAR3 (BaseAddr+SHIFT(4)) #define PAR4 (BaseAddr+SHIFT(5)) #define P AR5 (BaseAddr+SHIFT(6)) #define CURR #define MAR0 #define MAR1 #define MAR2 #define MAR3 #define MAR4 # define MAR5 #define MAR6 #define MAR7 (BaseAddr+SHIFT(7)) (BaseAddr+SHIFT(8)) (BaseAddr+SHIFT(9)) (BaseAddr+SHIFT(10)) (BaseAddr+SHIFT(11)) (BaseAddr+SHIFT(12)) (BaseAddr+SHIFT(13)) (BaseAddr+SHIFT(14)) (BaseAddr+SHIFT(15)) 293

294 /* page 2 */ /* page 3 */ #define CR9346 (BaseAddr+SHIFT(1)) #define CONFIG0 (BaseAddr+SHIFT(3)) #define CONFIG1 (BaseAddr+SHIFT(4)) # define CONFIG2 (BaseAddr+SHIFT(5)) #define CONFIG3 (BaseAddr+SHIFT(6)) 2. 初始化函数要想使用 RTL8019AS 进行通信, 必须首先对该芯片进行初始化并设置网卡物理地址, 参考程序如下 : int s3c44b0_eth_init(void) { U32 i; U8 MacId[ETH_ALEN] ={0x01,0x03,0xe3,0x84,0xf1,0x37}; SetRegPage(3); outportb(cr9346, 0xcf);//enable write config led_crs; outportb(config3, 0x70);//clear the sleep mode and pwrdn,set led0 as led_link,led1 as outportb(cr9346, 0x3f);//disable write config outportb( RstAddr, 0x5a);//reset nic i = 20000; while(i--);//wait for reset //inportb(isr); //? outportb(baseaddr, 0x21); /* set page 0 and stop */ outportb(pstart, RPSTART); /* set Pstart 0x4c set the start address of the receive buffe ring*/ outportb(pstop, RPSTOP); /* set Pstop 0x80 set the end address of the receive buffe ring* / outportb(bnry, RPSTART); /* BNRY-> the last page has been read */ outportb(tpsr, SPSTART); /* SPSTART page start register, set the start address of the transmit page*/ outportb(rcr, 0xcc); /* set RCR 0xcc */ //outportb(rcr, 0xcf); outportb(tcr, 0xe0); /* set TCR 0xe0 nomal operation and crc checker and generator*/ outportb(dcr, 0xc9); /* set DCR 0xc9,word-wide dma transfer 16bit DMA */ outportb(imr, 0x03); /* set IMR 0x03, enable tx rx int */ 294

295 outportb( ISR, 0xff); /* clear ISR */ SetRegPage(1); for(i=0;i< ETH_ALEN;i++) outportb(baseaddr+(1+i)*2,macid[i]);//set the mac addr of the nic; for(i=0;i<eth_alen;i++) SrcMacID[i]=MacId[i];//set the mac addr of the nic; SetRegPage(1); outportb(curr, RPSTART+1); outportb(mar0, 0x00);//set the multicast addr; outportb(mar1, 0x41); outportb(mar2, 0x00); outportb(mar3, 0x80); outportb(mar4, 0x00); outportb(mar5, 0x00); outportb(mar6, 0x00); outportb(mar7, 0x00); outportb(baseaddr,0x22); //start command rbnry = RPSTART; return 0; } 其中 SetRegPage( ) 为选择寄存器页, 代码如下 : void SetRegPage(U8 PageIdx) { U8 temp; temp = inportb(baseaddr); temp = (temp&(0x3b)) (PageIdx<<6); outportb(baseaddr, temp); } 3. 发送数据函数 int s3c44b0_eth_send(unsigned short *data, unsigned int s_len) { int i; int len,txlen; len=s_len<eth_zlen?eth_zlen:s_len; //len=data_len; //printf("the data length is %d bytes\n",len); 295

296 outportb(baseaddr,0x22);//switch to page 0 and stop remote dma; if(inportb(baseaddr)& 4)//last remote dma not complete return 1; SetRegPage(0); outportb(rsar0,0);//set the low of the remote start addr registers; outportb(rsar1,spstart);//set the hight of the remote start addr registers; outportb(rbcr0,len& 0xff);//set the low of the remote byte count registers; outportb(rbcr1,len>>8);//set the higt of the remote byte count registers; outportb(baseaddr,0x12);//begin remote write; TxLen=(len+1)/2; for(i=0;i<txlen;i++) outportw(rwport,data[i]);//copy data to nic; //printf("have copy the data to nic dma buff\n"); outportb(tpsr,spstart);//set the transmit page start register outportb(tbcr0,len&0xff);//set the byte counts of teh packet to be transmitted; outportb(tbcr1,len>>8); outportb(baseaddr,0x1e);//begin to send packet return 0; } 4. 接收数据函数 int s3c44b0_eth_rcv(unsigned short *data, unsigned int *len) { U8 RxPageBeg, RxPageEnd; U8 RxNextPage; U8 RxStatus; U16 temp; U16 i, RxLength,RxLen; if(inportb(isr)&0x1) //printf("receivex packet...\n"); outportb(isr, 0x1); //clr Rx interupt else return -1; outportb(bnry, rbnry); SetRegPage(1); 296

297 RxPageEnd = inportb(curr); SetRegPage(0); RxPageBeg = rbnry+1; if(rxpagebeg>=rpstop) RxPageBeg = RPSTART; outportb(baseaddr, 0x22); //stop remote dma outportb(rsar0, 0); outportb(rsar1, RxPageBeg); outportb(rbcr0, 4); outportb(rbcr1, 0); outportb(baseaddr, 0xa) ;//start remote dma read temp = inportw(rwport); RxNextPage = temp>>8; RxStatus = temp&0xff; RxLength = inportw(rwport); RxLength -= 4; if(rxlength>eth_frame_len) { if(rxpageend==rpstart) rbnry = RPSTOP-1; else rbnry = RxPageEnd-1; outportb(bnry, rbnry); printf("rxlength more long than %d\n", ETH_FRAME_LEN); return -1; } //eth_copy_and_sum(skb, data, len, 0); outportb(rsar0, 4); outportb(rsar1, RxPageBeg); outportb(rbcr0, RxLength); outportb(rbcr1, RxLength>>8); outportb( BaseAddr, 0xa); i = 2; data -= 2; 297

298 RxLen=(RxLength+1)/2; for(; RxLen--;) { if(!(i&0x7f)) { outportb(bnry, RxPageBeg); RxPageBeg++; if(rxpagebeg>=rpstop) } RxPageBeg = RPSTART; data[i++] = inportw(rwport); //printf("%2x,%2x,", data[i-1]&0xff,data[ i-1]>>8); } //printf("\n"); } outportb(bnry, RxPageBeg); rbnry = RxPageBeg; return 0; Linux 操作系统下 RTL8019AS 驱动程序设计 8019 芯片是和 NE2000 网卡兼容的, 因此在 Linux 操作系统下就有现成的网络驱动程序, 但是如果把它移植到相应的 ARM 平台上时, 还是要做一些修改, 下面就以模块的形式介绍在 Linux 系统下如何设计 RTL8019AS 芯片驱动程序 初始化函数 在 Linux 系统下有一个 device 数据结构, 它在设备驱动编写中占据非常重要的作用 其实, 编写 Linux 下的程序就是填充该数据结构的一个过程, 比如设备初始化就是通过 device 结构体中的 init 函数指针所指向的初始化函数来完成的 加载网络驱动模块后, 就会调用该函数进行初始化 初始化过程主要完成下列一些任务 : 检测网络物理设备是否存在 配置设备需要的资源, 如中断号 基地址等 构造设备的 device 数据结构, 并初始化其中的相关量 static int nic_8019_init(struct net_device *dev) 298

299 { int i; TRACE("init\n") ; ether_setup(dev); // Assign some of the fields // set net_device data members dev->watchdog_timeo = timeout; dev->irq = INT_EINT3; dev->dma = 0; // 这里设置网卡的 MAC 地址 printk(kern_info "%s: ", dev->name); for(i=0; i<6; i++) { dev->dev_addr[i] = SrcMacID[i]; printk("%2.2x%c", dev->dev_addr[i], (i==5)? ' ' : ':'); } printk("\n"); // 设置 device 结构中相关接口的函数指针 dev->open = nic_8019_open; dev->stop = nic_8019_stop; dev->get_stats = nic_8019_get_stats; dev->hard_start_xmit = nic_8019_start_xmit; SET_MODULE_OWNER(dev); dev->priv = kmalloc(sizeof(struct nic_8019_priv), GFP_KERNEL); if(dev->priv == NULL) return -ENOMEM; memset(dev->priv, 0, sizeof(struct nic_8019_priv)); spin_lock_init(&((struct nic_8019_priv *) dev->priv)->lock); return 0; } /* 写成模块的形式, 首先定义 net_device 结构类型变量 nic_8019_netdevs, 然后在 nic_8019_init_module() 函数中调用 register_netdev(&nic_8019_netdevs) 注册到内核中 */ static struct net_device nic_8019_netdevs = { init: nic_8019_init, 299

300 }; /* * Finally, the module stuff */ int init nic_8019_init_module(void) { int result; TRACE("init_module\n"); //Print version information printk(kern_info "%s", version); //register_netdev will call nic_8019_init() if((result = register_netdev(&nic_8019_netdevs))) printk("rtl8019as eth: Error %i registering device \"%s\"\n", result, nic_8019_netdevs.name); return result? 0 : -ENODEV; } 模块卸载函数 模块加载后, 如果要将其从内核中再次卸载下来, 必须要有相应的卸载函数来完成 在本例中,nic_8019_cleanup 函数在模块卸载时运行, 主要完成资源的释放工作, 如取消设备注册 释放内存等和 nic_8019_init 函数相反的一些动作 void exit nic_8019_cleanup(void) { TRACE("cleanup\n"); kfree(nic_8019_netdevs.priv); unregister_netdev(&nic_8019_netdevs); return; } 打开函数 打开函数在网络设备驱动程序里的作用是在网络设备被激活时调用的, 在本例中,RTL8019AS 网卡的许多初始化工作都放到此处来完成 /* * Open and Close */ 300

301 stat { ic int nic_8019_open(struct net_device *dev) int i,j; MOD_INC_USE_COUNT;// 增加模块计数 TRACE("open\n"); // Disable irqs disable_irq(dev->irq); // register rx isr if(request_irq(dev->irq, &nic_8019_ rx,\* // 函数指针, 当中断发生的时候该函数将会被调用. SA_INTERRUPT,\* 中断类型标志 "eth rx isr", dev)) { printk(kern_err "Rtl8019: Can't get irq %d\n", dev->irq); return -EAGAIN; } // wake up Rtl8019as SetRegPage(3); outportb(cr9346, 0xcf); // set eem1-0, 11,enable write config register outportb(config3, 0x60);/* 设置为全双式工作方式 clear pwrdn, sleep mode, set led0 as led_col, led1 as led_crs */ outportb(cr9346, 0x3f); //disable write config register // initialize outportb(rstaddr, 0x5a);// 向该地址写入或是读任何数据都将引起网卡的复位. i = 20000; while(i--);// 延时 SetRegPage(0); inportb(isr) ; outportb(baseaddr, 0x21); /* set page 0 and stop */ outportb(pstart, RPSTART); /* set Pstart 0x4c */ outportb(pstop, RPSTOP); /* set Pstop 0x80 */ outportb(bnry, RPSTART); /* BNRY-> the last page has been read */ outportb(tpsr, SPSTART); /* SPSTART page start register, 0x40 */ outportb(rcr, 0xcc); /* set RCR 0xcc */ outportb(tcr, 0xe0); /* set TCR 0xe0 */ #if RTL8019_OP_ 16 outportb(dcr, 0xc9); /* set DCR 0xc9, 16bit DMA */ #else 301

302 outportb(dcr, 0xc8); /* 8bit DMA */ #endif outportb(imr, 0x03); /* set IMR 0x03, enable tx rx int */ outportb(isr, 0xff); /* clear ISR */ SetRegPage(1); for(i=0; i<6; i+ +) outportb(baseaddr+(1+i)*2, dev->dev_addr[i]); // set mac id outportb(curr, RPSTART+1); outportb(mar0, 0x00); outportb( MAR1, 0x41); outportb(mar2, 0x00); outportb(mar3, 0x80); outportb(mar4, 0x00); outportb(mar5, 0x00); outportb(mar6, 0x00); outportb(mar7, 0x00); outportb(baseaddr, 0x22); /* set page 0 and start */ rbnry = RPSTART; enable_irq(dev->irq); // Start the transmit queue netif_start_queue(dev); return 0; } 在上述函数中, 通过 request_irq(dev->irq, &nic_8019_rx, SA_INTERRUPT, "eth rx is r", dev) 函数注册中断, 当有数据到来时, 发生中断, 调用函数 nic_8019_rx() 进行处理 该函数代码参看 节 关闭函数 当网络设备状态由 up 转为 down 时, 表明网络设备不可用, 这时应该调用关闭函数释放资源 另外, 如果以模块装入的设备驱动程序, 还要在关闭函数中调用 MOD_DEC_USE_COUNT 宏, 减少设备被引用的次数, 以便卸载驱动程序 static int nic_8019_stop(struct net_device *dev) { TRACE("stop\n"); SetRegPage(3); 302

303 } outportb(cr9346, 0xcf); // set eem1-0, 11,enable write config register // enter pwrdn, sleep mode, set led0 as led_col, led1 as led_crs outportb(config3, 0x66); outportb(cr9346, 0x3f); // disable write config register free_irq(dev->irq, dev); netif_stop_queue(dev); MOD_DEC_USE_ COUNT; return 0; 发送函数 网卡的作用是用来收发数据包, 因此发送函数和接收函数就是 Linux 网络驱动程序中最关键的两个函数 在网络设备驱动被加载时, 通过 device 结构体中的 init 函数指针调用网络设备的初始化函数对设备进行初始化, 然后通过 device 结构体中的 open 函数指针调用网络设备的打开函数打开设备, 再通过 d evice 结构体中的建立硬件包头函数指针 hard_hea der 来建立硬件包头信息 最后通过协议接口层函数 dev_queue_xmit 调用 devi ce 结构体中的 har_start_xmit 函数指针指向的函数来完成数据包的发送 RTL 8019AS 发送函数代码如下 : // 发送 static int nic_8019_start_xmit(struct sk_buff *skb, struct net_device *dev) { int i; u16 len,txlen; u16 *data; struct nic_8019_priv *priv = (struct nic_8019_priv *) dev->priv; TRACE("start_xmit\n"); len = skb->len < ETH_ZLEN? ETH_ZLEN : skb->len; TRACE("\nTx Length = %i,%x,%x\n", len, skb->data[12], skb->data[13]); data =(u16*) skb->data; outportb(baseaddr,0x22); //switch to page 0 and stop remote dma if(inportb(baseaddr)&4) return 1; /* last remote dma not complete,return 1 echo busy(error),retransmit next*/ 303

304 outportb(rsar0, 0); outportb(rsar1, SPSTART); outportb(rbcr0, len&0xff); outportb(rbcr1, len>>8); outportb(baseaddr, 0x12); dev->trans_start = jiffies; #ifdef RTL8019_OP_16 TxLen=(len+1)/2; #else TxLen=len; #endif //begin remote write for(i=0; i<txlen; i++) { #ifdef RTL8019_OP_16 outportw(rwport, data[i]); // copy data to nic ram TRACE("%2X,%2X,",data[i]&0xff,data[i]>>8); #else outportb(rwport, data[i]); // copy data to nic ram TRACE("%2X,",skb->data[i]); #endif } } TRACE("\n"); outportb(tpsr, SPSTART); // transmit begin page 0x40 outportb(tbcr0, len&0xff); outportb(tbcr1, len>>8); outportb(baseaddr, 0x1e); // begin to send packet dev_kfree_skb(skb); return 0; 接收函数 驱动程序并不存在一个接收方法 有数据收到应该是驱动程序来通知系统的 一般设备收到数据后都会产生一个中断, 在中断处理程序中驱动程序申请一块 sk_b uff(skb), 从硬件读出数据放置到申请好的缓冲区里 接下来填充 sk_buff 中的一些信息 skb->dev = dev, 判断收到帧的协议类型, 填入 skb->protocol( 多协议的支持 ) 把指针 skb->mac.raw 指向硬件数据然后丢弃硬件帧头 (skb_pull) 还要设置 304

305 skb- >pkt_type, 标明第二层 ( 链路层 ) 数据类型 可以是以下类型 : PACKET_BROADCAST : 链路层广播 PACKET_MULTICAST : 链路层组播 PACKET_SELF : 发给自己的帧 PACKET_OTHERHOST : 发给别人的帧 ( 监听模式时会有这种帧 ) 最后调用 netif_rx() 把数据传送给协议层 netif_rx() 里数据放入处理队列然后返最后调用 netif_rx() 把数据传送给协议层 netif_rx() 里数据放入处理队列然后返回, 真正的处理是在中断返回以后, 这样可以减少中断时间 调用 netif_rx() 以后, 驱动程 序就不能再存取数据缓冲区 skb 代码如下 : /* * 接收数据函数 */ static void nic_8019_rx(int ir q, void *dev_id, struct pt_regs *regs) { u8 RxPageBeg, RxPageEnd; u8 RxNextPage; u8 RxStatus; u16 *data,temp; u16 i, RxLength,RxLen; struct sk_buff * skb; struct net_device *dev = (struct net_device *) dev_id; struct nic_8019_priv *priv = (struct nic_8019_priv *) dev->priv; errors TRACE("TX/RX Interupt!\n"); spin_lock(&priv->lock); SetRegPage(0); outportb(bnry, rbnry); //??? RxStatus = inportb(isr);// 获得中断寄存器的状态. if(rxstatus&2)// 第二位置位 1:this bit indicates packet transmitted with no { outportb(isr, 0x2); //clr TX interupt priv->stats.tx_packets++; TRACE("transmit one packet complete!\n"); } if(rxstatus&1)// 第一位置位 0:this bit indicates packet received with no errors { 305

306 TRACE("Receivex packet...\n"); outportb(isr, 0x1); //clr Rx interupt SetRegPage(1); RxPageEnd = inportb(curr); SetRegPage(0); RxPageBeg = rbnry+1; if(rxpagebeg>=rpstop) RxPageBeg = RPSTART; outportb(baseaddr, 0x22); // stop remote dma //outport(rsar0, RxPageBeg<<8); //outport(rbcr0, 256); outportb(rsar0, 0); outportb(rsar1, RxPageBeg); outportb(rbcr0, 4); outportb(rbcr1, 0); outportb(baseaddr, 0xa); #ifdef RTL8019_OP_16 temp = inportw(rwport); RxNextPage = temp>>8; RxStatus = temp&0xff; RxLength = inportw(rwport); # else RxStatus = inportb(rwport); RxNextPage = inportb(rwport); RxLength = inportb(rwport); RxLength = inportb(rwport)<<8; #endif TRACE("\nRxBeg = %x, RxEnd = %x, nextpage = %x, size = %i\n", RxPageBeg, RxPageEnd, RxNextPage, RxLength); RxLength -= 4; if(rxlength>eth_frame_len) { if(rxpageend==rpstart) rbnry = RPSTOP-1; else rbnry = RxPageEnd-1; 306

307 } outportb(bnry, rbnry); TRACE("RxLength more long than %x\n", ETH_FRAME_LEN); return; skb = dev_alloc_sk if(!skb) { } b(rxlength+2); TRACE("Rtl8019as eth: low on mem - packet dropped\n"); priv->stats.rx_dropped++; return; skb->dev = dev; skb_reserve(skb, 2); skb_put(skb, RxLength); data = ( u16 *)skb->data; // eth_copy_and_sum(skb, data, len, 0); outportb(rsar0, 4); outportb(rsar1, RxPageBeg); outportb(rbcr0, RxLength); outportb(rbcr1, RxLength>>8); outportb(baseaddr, 0xa); #ifdef RTL8019_OP_16 i = 2; #else data -= 2; RxLen=(RxLength+1)/2; i = 4; data -= 4; RxLen=RxLength; #endif for(; RxLen--;) { #ifdef RTL8019_OP_16 if(!(i&0x7f)) #else if(!(i&0xff)) 307

308 #endif { outportb( BNRY, RxPageBeg); RxPageBeg++; if(rxpagebeg>=rpstop) RxPageBeg = RPSTART; } #ifdef RTL8019_OP_16 #else #endif data[i++] = inportw(rwport); TRACE("%2X,%2X,", data[ i-1]&0xff,data[i-1]>>8); data[i++] = inportb(rwport); TRACE("%2X,", data[i-1]); } TRACE("\n"); outportb(bnry, RxPageBeg); rbnry = RxPageBeg; skb->protocol = eth_type_trans(skb, dev); TRACE("\nprotocol=%x\n", skb->protocol)priv->stats.rx_packets++; priv->stats.rx_bytes +=RxLength; netif_rx(skb); } else { } outportb(isr, 0xfe); } spin_unlock(& priv->lock); 308

309 第 13 章嵌入式 BOOTLOADER 设计 13.1 BOOTLOADER 概述 嵌入式系统软件架构 嵌入式系统平台是一种软硬件结合的平台, 它和普通的单片机系统最大的区别就是嵌入式系统平台上具有专用的嵌入式操作系统, 比如很多嵌入式开发板上都会 支持 linux 操作系统 wince 操作系统 vxworks 操作系统等等 由于具有专用的操作系统支持, 在嵌入式系统平台上开发应用软件和在普通 pc 机上一样方便 快捷 所以从软件角度来看, 一个嵌入式系统可以分为下列四个层次, 如图 13-1 所示 引导加载程序 : 固化在硬件 flash 上的一段引导代码, 用于完成硬件的一些基本配置, 引导嵌入式操作系统内核启动 专用嵌入式操作系统内核 特定于嵌入式板子的定制内核以及内核的启动 参数 文件系统 包括根文件系统和建立于 Flash 内存设备之上文件系统 通常用 ram disk 来作为 root fs 用户应用程序 特定于用户的应用程序 有时在用户应用程序和内核层之间可能还会包括一个嵌入式图形用户界面 常用的嵌入式 GUI 有 : MicroWindows MiniGUI Embeded QT 等 用户应用程序 文件系统 专用嵌入式操作系统内核 引导加载程序 图 13-1 嵌入式系统软件架构 pc 机引导加载概述 引导加载程序是系统加电后运行的第一段软件代码 回忆一下 PC 的体系结构我们可以知道,PC 机中的引导加载程序由 BIOS( 其本质就是一段固件程序 ) 和位于 309

310 硬盘 MBR 中的 OS Boot Loader( 比如,LILO 和 GRUB 等 ) 一起组成 BIOS 在完成硬件检测和资源分配后, 将硬盘 MBR 中的 Boot Loader 读到系统的 RAM 中, 然后将控制权交给 OS Boot Loader Boot Loader 的主要运行任务就是将内核映象从硬盘上读到 RAM 中, 然后跳转到内核的入口点去运行, 也即开始启动操作系统 而在嵌入式系统中, 通常并没有像 BIOS 那样的固件程序 ( 注, 有的嵌入式 CPU 也会内嵌一段短小的启动程序 ), 因此整个系统的加载启动任务就完全由 Boot Loader 来完成 比如在一个基于 ARM7TDMI core 的嵌入式系统中, 系统在上电 或复位时通常都从地址 0x 处开始执行, 而在这个地址处安排的通常就是系统的 Boot Loader 程序 在 ARM 处理器的嵌入式系统中,Bootloader 代码的作用主要有以下几点 : 设计程序入口指针 建立异常中断处理向量 初始化 CPU 各种模式的堆栈和寄存器 ; 初始化系统中要使用的各种片内外设 ; 初始化目标板 ; 引导操作系统 本章将从 Boot Loader 的概念 Boot Loader 的设计 Boot Loader 的框架结构以及 Boot Loader 的使用等方面来讨论嵌入式系统的 Boot Loader 嵌入式系统 Boot Loader 概念 简单地说,Boot Loader 就是在操作系统内核运行之前运行的一段小程序 通 过这段小程序, 我们可以初始化硬件设备 建立内存空间的映射图, 从而将系统的软硬件环境带到一个合适的状态, 以便为最终调用操作系统内核准备好正确的环境 通常,Boot Loader 是严重地依赖于硬件而实现的, 特别是在嵌入式世界 因 此, 在嵌入式世界里建立一个通用的 Boot Loader 几乎是不可能的 尽管如此, 我们仍然可以对 Boot Loader 归纳出一些通用的概念来, 以指导用户特定的 Boot Loader 设计与实现 1.Boot Loader 所支持的 CPU 和嵌入式板每种不同的 CPU 体系结构都有不同的 Boot Loader 有些 Boot Loader 也支持多种体系结构的 CPU, 比如 U-Boot 就同时支持 ARM 体系结构和 MIPS 体系 结构 除了依赖于 CPU 的体系结构外,Boot Loader 实际上也依赖于具体的嵌入式板级设备的配置 这也就是说, 对于两块不同的嵌入式板而言, 即使它们是基于同一种 CPU 而构建的, 要想让运行在一块板子上的 Boot Loader 程序也能运行在 另一块板子上, 通常也都需要修改 Boot Loader 的源程序 2.Boot Loader 的安装媒介 (Installation Medium) 系统加电或复位后, 所有的 CPU 通常都从某个由 CPU 制造商预先安排的地 310

311 址上取指令 比如, 基于 ARM7TDMI core 的 CPU 在复位时通常都从地址 0x 取它的第一条指令 而基于 CPU 构建的嵌入式系统通常都有某种类 型的固态存储设备 ( 比如 :ROM EEPROM 或 FLASH 等 ) 被映射到这个预先安排的地址上 因此在系统加电后,CPU 将首先执行 Boot Loader 程序 下图 13-2 就是一个同时装有 Boot Loader 内核的启动参数 内核映像和根文 件系统映像的固态存储设备的典型空间分配结构图 图 13-2 固态存储设备的典型空间分配结构 3. 用来控制 Boot Loader 的设备或机制主机和目标机之间一般通过串口建立连接,Boot Loader 软件在执行时通常会通过串口来进行 I/O, 比如 : 输出打印信息到串口, 从串口读取用户控制字符等 4.Boot Loader 的启动过程是单阶段 (Single Stage) 还是多阶段 (Multi-Stage) 通常多阶段的 Boot Loader 能提供更为复杂的功能, 以及更好的可移植性 从固态存储设备上启动的 Boot Loader 大多都是 2 阶段的启动过程, 也即启动过程 可以分为 stage 1 和 stage 2 两部分 而至于在 stage 1 和些任务将在下面讨论 5.Boot Loader 的操作模式 (Operation Mode) stage 2 具体完成哪 大多数 Boot Loader 都包含两种不同的操作模式 :" 启动加载 " 模式和 " 下载 " 模式, 这种区别仅对于开发人员才有意义 但从最终用户的角度看,Boot Loader 的作用就是用来加载操作系统, 而并不存在所谓的启动加载模式与下载工作模式的区 别 启动加载 (Boot loading) 模式 : 这种模式也称为 " 自主 "(Autonomous) 模式 也即 Boot Loader 从目标机上的某个固态存储设备上将操作系统加载到 RAM 中 运行, 整个过程并没有用户的介入 这种模式是 Boot Loader 的正常工作模式, 因此在嵌入式产品发布的时侯,Boot Loader 显然必须工作在这种模式下 下载 (Downloading) 模式 : 在这种模式下, 目标机上的 Boot Loader 将通过 串口连接或网络连接等通信手段从主机 (Host) 下载文件, 比如 : 下载内核映像和根文件系统映像等 从主机下载的文件通常首先被 Boot Loader 保存到目标机的 RAM 中, 然后再被 Boot Loader 写到目标机上的 FLASH 类固态存储设备中 Boot Loader 的这种模式通常在第一次安装内核与根文件系统时被使用 ; 此外, 以后的系统更新也会使用 Boot Loader 的这种工作模式 工作于这种模式下的 Boot Loader 通常都会向它的终端用户提供一个简单的命令行接口 像 Blob 或 U-Boot 等这样功能强大的 Boot Loader 通常同时支持这两种工 311

312 作模式, 而且允许用户在这两种工作模式之间进行切换 比如,Blob 在启动时处于正常的启动加载模式, 但是它会延时 10 秒等待终端用户按下任意键而将 blob 切换到下载模式 如果在 10 秒内没有用户按键, 则 blob 继续启动 Linux 内核 6.BootLoader 与主机之间进行文件传输所用的通信设备及协议最常见的情况就是, 目标机上的 Boot Loader 通过串口与主机之间进行文件传 输, 传输协议通常是 xmodem/ymodem/zmodem 协议中的一种 但是, 串口传输的速度是有限的, 因此通过以太网连接并借助 TFTP 协议来下载文件是个更好的选择 此外, 在论及这个话题时, 主机方所用的软件也要考虑 比如, 在通过以太网连接和 TFTP 协议来下载文件时, 主机方必须有一个软件用来的提供 TFTP 服务 在讨论了 BootLoader 的上述概念后, 下面我们来具体看看 BootLoader 的应该完 成哪些任务 Linux 平台下各式各样的 Boot Loader 因为 Linux 有许多 Boot Loader 可用, 再加上嵌入式电路板成千上万, 而每块相同的电路板也可能有不同的引导配置 因此不可能在一节中对每个 Boot Loader 都进行深入的讨论 本节将重点介绍 Linux 平台下各种常用的 Boot Loader, 见表 13-1 所示 表 13-1 Linux 的开放源码 Boot Loader 以及其所支持的架构 Boot 监说明 架构 Loader 控 X86 ARM PowerPC MIPS M68k SuperH 程序 LILO 否 Linux 主要的磁盘引 * 导加载程序 GRUB 否 LILO 的 GNU 版后继者 ROLO 否 不需要 BIOS 可直接 从 ROM 加载 Linux * * Loadlin 否从 DOS 加载 Linux * Etherboot 否 从 ETHERNET 卡启动系统的 Romable loader LinuxBIOS 否 以 Linux 为基础的 BIOS 替代品 * * Compaq 的是主要用于 Compaq * bootldr ipaq 的多功能加载 312

313 程序 blob 否来自 LART 硬件计划 的加载程序 * PMON 是 Agenda VR3 中所使 * 用的加载程序 sh-boot 否 LinuxSH 计划的主要 加载程序 * U-Boot 是 以 PPCBoot 和 * * * ARMBoot 为基础的通用加载程序 RedBoot 是 以 ecos 为基础的加 * * * * * * 载程序 注意 : 不同出版物会特别区分引导加载程序和监控程序的差异 : 引导加载程序只是用来启动设备以及执行主要软件的组件, 监控程序除了引导功能外还可以用来调试 读写内存 重新编写 flash 设备 配置等等的命令行接口 在本章中都被叫做引导加载程序 13.2 BOOTLOADER 设计流程 芯片的 Bootloader 代码 ( 即启动代码 ) 就是芯片复位后进入操作系统之前执行的一段代码, 主要是为运行操作系统提供基本的运行环境, 如初始化 CPU 堆栈 初始化存储器系统等 Bootloader 代码与 CPU 芯片的内核结构 具体芯片和使用的操作系统等因素有关 其功能有点类似于 PC 机的 BIOS(Basic Input/Output System, 基本输入输出系统 ) 程序, 但是由于嵌入式系统的软硬件都要比 PC 机的简单, 所以它的 Bootloader 代码要比 BIOS 程序简单得多 比如在一个基于 ARM7TDMI core 的嵌入式系统中, 系统在上电或复位时通常都从地址 0x 处开始执行, 而在这个地址处安排的通常就是系统的 BootLoader 程序 嵌入式系统的资源有限, 应用程序通常都是固化在 ROM 中运行 ROM 中的程序执行前, 需要对系统硬件和软件运行环境进行初始化 这些工作是用汇编语言和 C 语言编写的 Bootloader 代码完成的 Bootloader 代码是嵌入式系统中应用程序的开头部分, 它与应用程序一起固化在 ROM 中, 并首先在系统上运行 设计好 Bootloader 代码是设计嵌入式程序的关键, 也是系统能够正常工作的前提 Bootloader 代码所执行的操作主要信赖于 CPU 内核的类型, 以及正在开发的嵌入式系统软件中需要使用 CPU 芯片上的哪些资源 Bootloader 代码的一般流程 ( 即 Bootloader 代码应该进行的操作 ) 如图 13-3 所示 313

314 设计程序入口指针 建立异常中断处理向量 初始化 CPU 各种模式的堆栈和寄存器 初始化系统中要使用的各种片内外设 初始化目标板 引导操作系统 图 13-3 Bootloader 代码设计的流程图 13.3 bootloader 设计关键技术分析 1. 设计程序入口指针程序入口指针位于整个 bootloader 代码的第一部分, 它指明程序入口地址, 一 般采用 ARM 汇编语言中的 ENTRY 语句, 如下所示 AREA Init,CODE,READONLY ENTRY b ResetHandler ;for debug b HandlerUndef ;handlerundef b HandlerSWI ;SWI interrupt handler b HandlerPabort ;handlerpabort b HandlerDabort ;handlerdabort 解释 : 在开发板刚启动时, 并不能直接运行 c 语言的代码, 所以 bootloader 中有一部分代码是需要用 ARM 汇编来写的, 在 bootloader 准备好 c 程序所要求的环境 后, 就可以用 c 语言来编写 bootloader 了 ENTRY 是整个程序的入口, 一般我们编译时会将上述代码文件 ( 比如为 vector.s) 放在 0 地址处, 所以 b ResetHandler 是整个系统的第一条语句, 大家可想而知, 系统重启或者刚开机, 肯定是发生了复 314

315 位中断, 并且要进入复位中断处理程序 所以 0 地址处放的是复位中断处理程序, 只不过这里进行了变通, 它在 0 地址处存放了一条跳转指令, 该指令将跳转到实际的中断处理程序处, 此处是 ResetHandler 标号所在的位置 2. 异常中断向量设计 中断向量设计并不是 bootloader 必须的一项内容, 如果不需要在 bootloader 中使用中断向量, 可以省略这一步骤 但是如果 bootloader 中使用到相关硬件中断时, 必须建立中断向量表, 此外, 对于操作系统是 uclinux 这样的操作系统而言, 也必须 在 bootloader 中建立中断向量表, 因为异常中断矢量表是 Bootloader 与 uclinux 内核发生联系关键的地方之一 即使 uclinux 内核已经得到处理器的控制权运行, 一旦发生中断, 处理器还是会自动跳转到从 0x0 地址开始的第一级异常中断矢量表中 的某个表项 ( 依据于中断类型 ) 处读取指令运行 但是通常情况下, 很多嵌入式操作系统内核启动后会抛弃 bootloader 建立的中断向量表, 自己重构中断向量表, 所以 bootloader 异常中断向量表的设计是和具体的操作系统息息相关, 如果不考虑操 作系统的因素, 我们可以采用下面的设计方法 ( 以 S3C44B0X 平台为例 ) 通常情况下,S3C44B0X 具有向量中断和非向量中断两种模式 下面分别介绍这两种模式下异常中断向量设计 (1) 向量中断模式所谓向量中断模式, 就是 S3C44B0X CPU 收到一个来自中断控制器的中断请求, 它会执行位于 0X18 地址处的指令, 同时中断控制器将一条跳转指令放在数据总 线上 对于不同的中断源, 这条跳转指令将会给 PC 相对应的地址, 比如, 发生 RTC 中断, 该跳转指令将 PC 从 0X18 转为指向 0XA0, 从 0XA0 处得到 RTC 中断服务程序的地址, 然后 PC 再跳到该中断服务程序处执行, 如下图 13-4 所示 ; 所用 IRQ 中断程序入口 b HandlerIRQ ;RTC 中断程序入口 ldr pc,=handlerrtc ; RTC 中断服务程序 RTC_INT_HANDLER( ) { 中断服务程序主体 } 图 13-4 向量中断模式下中断服务程序流程图 315

316 向量中断模式可以快速响应中断, 减少中断响应的延迟 向量中断模式下中断代码可以如下设计 : ENTRY b ResetHandler ;for debug b HandlerUndef ;handlerundef b HandlerSWI ;SWI interrupt handler b HandlerPabort ;handlerpabort b HandlerDabort ;handlerdabort b. ;handlerreserved b HandlerIRQ b HandlerFIQ ldr pc,=handlereint0 ;mga H/W interrupt vector table ldr pc,=handlereint1 ; ldr pc,=handlereint2 ; ldr pc,=handlereint3 ; ldr pc,=handlereint4567 ; ldr pc,=handlertick ;mga b. b. ldr pc,=handlerzdma0 ;mgb ldr pc,=handlerzdma1 ; ldr pc,=handlerbdma0 ; ldr pc,=handlerbdma1 ldr pc,=handlerwdt ; ; ldr pc,=handleruerr01 ;mgb b. b. ldr pc,=handlertimer0 ;mgc ldr pc,=handlertimer1 ; ldr pc,=handlertimer2 ; ldr pc,=handlertimer3 ldr pc,=handlertimer4 ; ; ldr pc,=handlertimer5 ;mgc b. b. ldr pc,=handlerurxd0 ;mgd ldr pc,=handlerurxd1 ; ldr pc,=handleriic ; 316

317 ldr pc,=handlersio ; ldr pc,=handlerutxd0 ; ldr pc, =HandlerUTXD1 ;mgd b. b. ldr pc,=handlerrtc ;mgka b. ; b. ; b. ; b. ; b. ;mgka b. b. ldr pc,=handleradc ;mgkb b. ; b. ; b. ; b. ; b. ;mgkb b. b. (1) 非向量中断模式如果对中断延迟没有严格的限制, 在 S3C44B0X 平台上可以采用非向量中断模式, 其处理步骤如下 : 中断发生后, 在 I_ISPR/F_ISPR 寄存器中会把相应位置 1, 所以可以通过查询 I_ISPR/F_ISPR 寄存器的内容来分析判断发生中断的类型, 然后把 PC 指向相应的中断处理程序 示例代码如下 : ENTRY b ResetHandler ;for debug b HandlerUndef ;handlerundef b HandlerSWI ;SWI interrupt handler b HandlerPabort ;handlerpabort b HandlerDabort ;handlerdabort b. ;handlerreserved b HandlerIRQ b HandlerFIQ ;***IMPORTANT NOTE*** ;If the H/W vectored interrutp mode is enabled, The above two instructions 317

318 should ;be changed like below, to work-around with H/W bug of S3C44B0X interrupt controller. ; b HandlerIRQ -> subs pc,lr,#4 ; b HandlerIRQ -> subs pc,lr,#4 VECTOR_BRANCH ldr pc,=handlereint0 ;mga H/W interrupt vector table ldr pc,=handlereint1 ; ldr pc,=handlereint2 ; ldr pc,=handlereint3 ; ldr pc,=handlereint4567 ; ldr pc,=handlertick ;mga b. b. ldr pc,=handlerzdma0 ;mgb HandlerFIQ HANDLER HandleFIQ HandlerIRQ HANDLER HandleIRQ HandlerUndef HANDLER HandleUndef HandlerSWI HANDLER HandleSWI HandlerDabort HANDLER HandleDabort HandlerPabort HANDLER HandlePabort HandlerADC HANDLER HandleADC HandlerRTC HANDLER HandleRTC HandlerUTXD1 HANDLER HandleUTXD1 HandlerUTXD0 HANDLER HandleUTXD0 HandlerSIO HANDLER HandleSIO HandlerIIC HANDLER HandleIIC HandlerURXD1 HANDLER HandleURXD1 HandlerURXD0 HANDLER HandleURXD0 HandlerTIMER5 HANDLER HandleTIMER5 HandlerTIMER4 HANDLER HandleTIMER4 HandlerTIMER3 HandlerTIMER2 HandlerTIMER1 HandlerTIMER0 HANDLER HandleTIMER3 HANDLER HandleTIMER2 HANDLER HandleTIMER1 HANDLER HandleTIMER0 318

319 HandlerUERR01 HANDLER HandleUERR01 HandlerWDT HANDLER HandleWDT HandlerBDMA1 HANDLER HandleBDMA1 HandlerBDMA0 HANDLER HandleBDMA0 HandlerZDMA1 HANDLER HandleZDMA1 HandlerZDMA0 HANDLER HandleZDMA0 ;One of the following two routines can be used for non-vectored interrupt. IsrIRQ ;using I_ISPR register. sub sp,sp,#4 ;reserved for PC stmfd sp!,{r8-r9} ;IMPORTANT CAUTION ;if I_ISPC isn't used properly, I_ISPR can be 0 in this routine. ldr ldr r9,=i_ispr r9,[r9] cmp r9, #0x0 ;If the IDLE mode work-around is used, ;r9 may be 0 sometimes. beq %F2 0 mov r8,#0x0 movs r9,r9,lsr #1 bcs %F1 add r8,r8,#4 b %B0 1 ldr add ldr str ldmfd r9,=handleadc r9,r9,r8 r9,[r9] r9,[sp,#8] sp!,{r8-r9,pc} 2 319

320 ldmfd add sp!,{r8-r9} sp,sp,#4 subs pc,lr,#4 AREA RamData, DATA, READWRITE ^ (_ISR_STARTADDRESS-0x500) UserStack # 256 ;c1(c7)ffa00 SVCStack # 256 ;c1(c7)ffb00 UndefStack # 256 ;c1(c7)ffc00 AbortStack # 256 ;c1(c7)ffd00 IRQStack # 256 ;c1(c7)ffe00 FIQStack # 0 ;c1(c7)fff00 ^ _ISR_STARTADDRESS HandleReset # 4 HandleUndef # 4 HandleSWI # 4 HandlePabort # 4 HandleDabort # 4 HandleReserved # 4 HandleIRQ # 4 HandleFIQ # 4 ;Don't use the label 'IntVectorTable', ;because armasm.exe cann't recognize this label correctly. ;the value is different with an address you think it may be. ;IntVectorTable HandleADC # 4 HandleRTC # 4 HandleUTXD1 # 4 HandleUTXD0 # 4 HandleSIO # 4 HandleIIC # 4 HandleURXD1 # 4 HandleURXD0 # 4 HandleTIMER5 # 4 320

321 HandleTIMER4 # 4 HandleTI MER3 # 4 HandleTI MER2 # 4 HandleTIMER1 # 4 HandleTIMER0 # 4 HandleUERR01 HandleWDT # # 4 4 HandleBDMA1 # 4 HandleBDMA0 # 4 HandleZDMA1 # 4 HandleZDMA0 # 4 HandleTICK # 4 HandleEINT4567 # 4 HandleEINT3 # 4 HandleEINT2 # 4 HandleEINT1 # 4 HandleEINT0 # 4 ;0xc1(c7)fff84 END 前面说过,bootloader 有时和操作系统是息息相关的, 比 uclinux 操作系统它发生中断时, 处理器还是会自动跳转到从 0x0 地址开始的第一级异常中断矢量表中的某个表项 ( 依据于中断类型 ) 处读取指令运行 如果考虑到 bootloader 和 uclinux 下中断都可以使用, 我们可以采取下列设计方法 在编写 Bootloader 时, 地址 0x0 处的一级异常中断矢量表只需简单地包含向二级异常中断矢量表的跳转指令就可以 这样, 就能够正确地将发生的事件交给 uclinux 的中断处理程序来处理 对于 uclinux 内核, 假设它在 RAM 空间中基地址为 0xc 处建立了自己的二级异常中断矢量表, 因此,Bootloader 的第一级异常中断矢量表如下所示 : b ResetHandler ; Reset Handler ldr pc,=0x0c ;Undefined Instruction Handler ldr pc,=0x0c ;Software Interrupt Handler ldr pc,=0x0c00000c ;Prefetch Abort Handler ldr pc,=0x0c ;Data Abort Handler b. ldr pc,=0x0c ;IRQ Handler ldr pc,=0x0c00001c ;FIQ Handler 存放矢量表 : ;IRQ ==the program put this phrase to 0xc

322 ExceptionHanlderBegin b. ldr pc, MyHandleUndef ; HandlerUndef ldr pc, MyHandleSWI ; HandlerSWI ldr pc, MyHandlePabort ; HandlerPabort ldr pc, MyHandleDabort ; HandlerDAbort b. ; HandlerReserved ldr pc, MyHandleIRQ ; HandlerIRQ ldr pc, MyHandleFIQ ; HandlerFIQ MyHandleUndef DCD HandleUndef ;reserve a word(32bit) MyHandleSWI DCD HandleSWI MyHandlePabort DCD HandlePabort MyHandleDabort DCD HandleDabort MyHandleIRQ DC D HandleIRQ MyHandleFIQ DC D HandleFIQ ExceptionHanlderEnd 建立二级矢量表 : ;**************************************************** ;* Setup IRQ handler * ;**************************************************** ldr r0,=(_irq_baseaddress + 0x100) ldr r2,=_irq_baseaddress add r3,r0, #0x100 0 CMP r0, r3 STRCC r2, [r0], #4;cc:Carry clear;save R2 to R0 address, R0 =R0+ 4 BCC %B0 ldr r1,=_irq_baseaddress ldr r0,=exceptionhanlderbegin ;if there isn't 'subs pc,lr,#4' at 0x18, 0x1c ldr r3,=exceptionhanlderend 0 CMP r0, r3 ;put the vector table at _IRQ_BASEADDRESS(0xc000000) LDRCC r2, [r0], #4 STRCC r2, [r1], #4 BCC %B0 ldr r1,=disrirq;put the IRQ judge program at _IRQ_BASEADDRESS+0x80(0xc000080) ldr r0,=isrirq ;if there isn't 'subs pc,lr,#4' at 0x18, 0x1c ldr r3,=isrirqend 322

323 0 CMP r0, r3 LDRCC r2, [r0], #4 STRCC r2, [r1], #4 BCC %B0 ldr r1, =MyHandleIRQ ;MyHandleIRQ point to DIsrIRQ ldr r0, =ExceptionHanlderBegin ldr r4, =_IRQ_BASEADDRESS; sub r0, r1, r0 add r0, r0,r4 ldr r1, =DIsrIRQ str r1, [r0] 定义 Handlexxx: ^ (_IRQ_BASEADDRESS) HandleReset # 4 HandleUndef # 4 HandleSWI # 4 HandlePabort # 4 HandleDabort # 4 HandleReserved # 4 HandleIRQ # 4 HandleFIQ # 4 ^ (_IRQ_BASEADDRESS+0x80) DIsrIRQ # 4 ;IntVectorTable ^ (_IRQ_BASEADDRESS+0x100) HandleADC # 4 HandleRTC # 4 HandleUTXD1 # 4 HandleUTXD0 # 4 HandleSIO # 4 HandleIIC # 4 HandleURXD1 # 4 HandleURXD0 # 4 HandleTIMER5 # 4 HandleTIMER4 # 4 HandleTIMER3 # 4 HandleTIMER2 # 4 HandleTIMER1 # 4 323

324 HandleTIMER0 # 4 HandleUERR01 # 4 HandleWDT # 4 HandleBDMA1 # 4 HandleBDMA0 # 4 HandleZDMA1 # 4 HandleZDMA0 # 4 HandleTICK # 4 HandleEINT4567 # 4 HandleEINT3 # 4 HandleEINT2 # 4 HandleEINT1 # 4 HandleEINT0 # 4 将异常中断矢量重构到 SDRAM, 这样的好处就是可以在其它的功能程序内对中 断处理程序的地址任意赋值 为此, 我们在 44b.h 文件中定义 : #define pisr_reset (*(unsigned *)(_IRQ_BASEADDRESS+0x0)) #define pisr_undef (*(unsigned *)(_IRQ_BASEADDRESS+0x4)) #define pisr_swi (*(unsigned *)(_IRQ_BASEADDRESS+0x8)) #define pisr_pabort (*(unsigned *)(_IRQ_BASEADDRESS+0xc)) #define pisr_dabort (*(unsigned *)(_IRQ_BASEADDRESS+0x10)) #define pisr_reserved (*(unsigned *)(_IRQ_BASEADDRESS+0x14)) #define pisr_irq (*(unsigned *)(_IRQ_BASEADDRESS+0x18)) #define pisr_fiq (*(unsigned *)(_ IRQ_BASEADDRESS+0x1c)) #define pisr_ ADC (*(unsigned *)(_IRQ_BASEADDRESS+0x100))//0x20)) #define pisr_rtc (*(unsigned *)(_IRQ_BASEADDRESS+0x104))//0x24)) #define pisr_utxd1 (*(unsigned *)(_IRQ_BASEADDRESS+0x108))//0x28)) #define pisr_utxd0 (*(unsigned *)(_IRQ_BASEADDRESS+0x10c))//0x2c)) #define pisr_sio (*(unsigned *)(_IRQ_BASEADDRESS+0x110))//0x30)) #define pisr_iic (*(unsigned *)(_IRQ_BASEADDRESS+0x114))//0x34)) #define pisr_urxd1 (*(unsigned *)(_IRQ_BASEADDRESS+0x118))//0x38)) #define pisr_urxd0 (*(unsigned *)(_IRQ_BASEADDRESS+0x11c))//0x3c)) #define pisr_timer5 (*(unsigned ))(_IRQ_BASEADDRESS+0x120))//0x40)) #define pisr_timer4 (*(unsigned *)(_IRQ_BASEADDRESS+0x124))//0x44)) #define pisr_timer3 (*(unsigned *)(_IRQ_BASEADDRESS+0x128))//0x48)) #define pisr_timer2 (*(unsigned *)(_IRQ_BASEADDRESS+0x12c))//0x4c)) #define pisr_timer1 (*(unsigned *)(_IRQ_BASEADDRESS+0x130))//0x50)) #define pisr_timer0 (*(unsigned *)(_IRQ_BASEADDRESS+0x134))//0x54)) #define pisr_uerr01 (*(unsigned *)(_IRQ_BASEADDRESS+0x138))//0x58)) #define pisr_wdt (*(unsigned *)(_IRQ_BASEADDRESS+0x13c))//0x5c)) 324

325 #define pisr_bdma1 (*(unsigned *)(_IRQ_BASEADDRESS+0x140))//0x60)) #define pisr_bdma0 (*(unsigned *)(_IRQ_BASEADDRESS+0x144))//0x64)) #define pisr_zdma1 (*(unsigned *)(_IRQ_BASEADDRESS+0x148))//0x68)) #define pisr_zdma0 (*(unsigned *)(_IRQ_BASEADDRESS+0x14c))//0x6c)) #define pisr_tick (*(unsigned *)(_IRQ_BASEADDRESS+0x150))//0x70)) #define pisr_eint4567 (*(unsigned *)(_IRQ_BASEADDRESS+0x154))//0x74)) #define pisr_eint3 (*(unsigned *)(_IRQ_BASEADDRESS+0x158))//0x78)) #define pisr_eint2 (*(unsigned *)(_IRQ_BASEADDRESS+0x15c))//0x7c)) #define pisr_eint1 (*(unsigned *)(_IRQ_BASEADDRESS+0x160))//0x80)) #define pisr_eint0 (*(unsigned *)(_IRQ_BASEADDRESS+0x164))//0x84)) 例如, 我们要使用到 Exint4567 中断, 定义好中断处理程序 Meint4567Isr() 后, 仅需要一条语句 : pisr_eint4567= (int)meint4567isr; 就能使中断发生后正确跳转到我们编写的处理程序上 3. Bootloader stage1 阶段代码设计 上面说过,stage1 阶段主要是有关 CPU 体系结构的代码设计, 比如设备初始化代码等 按照执行的先后顺序,Boot Loader 在 stage1 阶段要实现的功能包括以下几个主要的内容 : 硬件设备初始化 为加载 Boot Loader 的 stage2 准备 RAM 空间 拷贝 Boot Loader 的 stage2 到 RAM 空间中 设置好堆栈 跳转到 stage2 的 C 入口点 ( 1) 基本的硬件初始化 boot loader 首要任务就是要进行硬件的初始化工作, 为 stage2 的执行以及随后的 kernel 的执行准备好一些基本的硬件环境 通常情况下, 编写 boot loader 代码时, 主要考虑一下几个方面的设计 1 屏蔽所有的中断 为中断提供服务通常是 OS 设备驱动程序的责任, 因此在 Boot Loader 的执行全过程中可以不必响应任何中断 中断屏蔽可以通过写 CPU 的中断屏蔽寄存器或状态寄存器 ( 比如 ARM 的 CPSR 寄存器 ) 来完成 ; 禁止看门狗 ldr r0, =WTCON ldr r1, =0 str r1,[r0] ; 禁止所有中断 ldr r0,=intmsk ldr r1,=0x07ffffff 325

326 str r1,[r0] 2 设置 CPU 的速度和时钟频率 ldr r0, =LOCKTIME ldr r1, =0xfff str r1, [r0] ;[ PLLONSTART ldr r0, =PLLCON ; 锁相环倍频设定 ldr r1, =((M_DIV<<12)+(P_DIV<<4)+S_DIV) ; 设定系统主时钟频率, 倍频为 ((P_DIV+2)*(2 的 S_DIV 次方 ))/(M_DIV+8) str r1, [r0] 3 RAM 初始化包括正确地设置系统的内存控制器的功能寄存器以及各内存库控制寄存器等 ;***************************************************************** SMRDATA DATA ;***************************************************************** ; 存储器最好配置成最优的性能, 下面的参数不是最优化的 ;***************************************************************** ;*** memory access cycle parameter strategy *** ; 1) Even FP-DRAM, EDO setting has more late fetch point by half-clock ; 2) The memory settings,here, are made the safe parameters even at 66Mhz. ; 3) FP-DRAM Parameters:tRCD=3 for trac, tcas=2 for pad delay, tcp=2 for bus load. ; 4) DRAM refresh rate is for 40Mhz. ;bank0 16bit BOOT ROM ;bank1 8bit NandFlash ;bank2 16bit IDE ;bank3 rtl8019 ;bank4 ;bank5 8bit sl811hst ;bank6 16bit SDRAM ;bank7 16bit SDRAM ;LZ44B0X rtl8019 buswide = 16! [ BUSWIDTH=16 DCD 0x ;Bank0=16bit BootRom(AT29C010A*2) :0x0 ;BUSWIDTH=32 DCD 0x ;Bank0=OM[1:0], Bank1~Bank7=32bit 326

327 ] DCD ((B0_Tacs<<13)+(B0_ Tcos<<11)+(B0_Tacc<<8)+(B0_Tcoh<<6)+(B0_Tah<<4)+(B 0_Tacp<<2)+(B0_PMC)) ;GCS0 DCD ((B1_Tacs<<13)+(B1_Tcos<< 11)+(B1_Tacc<<8)+(B1_Tcoh<<6)+(B1_Tah<<4)+(B 1_Tacp<<2)+(B1_PMC)) ;GCS1 DCD ((B2_Tacs<<13)+(B2_Tcos<<11)+(B2_Tacc<<8)+(B2_Tcoh<<6)+(B2_Tah<<4)+(B 2_Tacp<<2)+(B2_PMC)) ;GCS2 DCD ((B3_Tacs<<13)+(B3_Tcos<<11)+(B3_Tacc<<8)+(B3_Tcoh<<6)+(B3_Tah<<4)+(B 3_ Tacp<<2)+(B3_PMC)) ;GCS3 DCD ((B4_Tacs<<13)+(B4_Tcos<<11)+(B4_Tacc<<8)+(B4_Tcoh<<6)+(B4_Tah<<4)+(B 4_ Tacp<<2)+(B4_PMC)) ;GCS4 DCD ((B5_Tacs<<13)+(B5_Tcos<<11)+(B5_Tacc<<8)+(B5_Tcoh<<6)+(B5_Tah<<4)+(B 5_ Tacp<<2)+(B5_PMC)) ;GCS5 [ BDRAMTYPE="DRAM" DCD ((B6_ MT<<15)+(B6_Trcd<<4)+(B6_Tcas<<3)+(B6_Tcp<<2)+(B6_CAN)) ;GCS6 check the MT value in parameter.a DCD ((B7_MT<<15)+(B7_Trcd<<4)+(B7_Tcas<<3)+(B7_Tcp<<2)+(B7_CAN)) ;GCS7 ;"SDRAM" DCD ((B6_MT<<15)+(B6_Trcd<<2)+(B6_SCAN)) ;GCS6 ;DCD ((B7_MT<<15)+(B7_Trcd<<2)+(B7_SCAN)) ;GCS7 DCD ((B1_Tacs<<13)+(B1_Tcos<<11)+(B1_Tacc<<8)+(B1_Tcoh<<6)+(B1_Tah<<4)+(B 1_ Tacp<<2)+(B1_PMC)) ;GCS7 ] DCD ((REFEN<<23)+(TREFMD<<22)+(Trp<<20)+(Trc<<18)+(Tchr<<16)+REFCNT) ;REFRESH RFEN=1, TREFMD=0, trp=3clk, trc=5clk, tchr=3clk, count=

328 DCD 0x10 ;SCLK power down mode, BANKSIZE 32M/32M DCD 0x20 ;MRSR6 CL=2clk DCD 0x20 ;MRSR7 4 初始化 LED 典型地, 通过 GPIO 来驱动 LED, 其目的是表明系统的状态是 OK 还是 Error 如果板子上没有 LED, 那么也可以通过初始化 UART 向串口打印 Boot Loa der 的 Logo 字符信息来完成这一点 5 关闭 CPU 内部指令 / 数据 cache (2) 为加载 stage2 准备 RAM 空间为了获得更快的执行速度, 通常把 stage2 加载到 RAM 空间中来执行, 因此必须为加载 Boot Loader 的 stage2 准备好一段可用的 RAM 空间范围 这个阶段要完成下面几个任务 : 第一 内存空间大小设计第二 内存空间测试为了得到一段干净的 RAM 空间范围, 我们也可以将所安排的 RAM 空间范围进行清零操作 ( 3) 拷贝 stage2 到 RAM 中拷贝时要确定两点 : 1 stage2 的可执行映象在固态存储设备的存放起始地址和终止地址 ; 2 RAM 空间的起始地址 ( 4) 设置堆栈指针 sp ( 5) 跳转到 stage2 的 C 入口点 bl call main // 如果正常, 一去不复返的了在上述一切都就绪后, 就可以跳转到 Boot Loader 的 stage2 去执行了 比如, 在 AR M 系统中, 这可以通过修改 PC 寄存器为合适的地址来实现 4.Bootloader stage2 阶段代码设计 Boot Loader 的 stage2 通常包括以下步骤 ( 以执行的先后顺序 ): 初始化本阶段要使用到的硬件设备 检测系统内存映射 (memory map) 将 kernel 映像和根文件系统映像从 flash 上读到 RAM 空间中 为内核设置启动参数 调用内核 正如前面所说,stage2 的代码通常用 C 语言来实现, 以便于实现更复杂的功能和取得更好的代码可读性和可移植性 但是与普通 C 语言应用程序不同的是, 在编译和链接 boot loader 这样的程序时, 我们不能使用 glibc 库中的任何支持函数 其原因是显而易见的 这就给我们带来一个问题, 那就是从那里跳转进 main() 函数呢? 直接把 main() 函数的起始地址作为整个 stage2 执行映像的入口点或许 328

329 是最直接的想法 但是这样做将带来两个缺点 : 首先, 无法通过 main() 函数传递函数参数 ; 其次, 无法处理 main() 函数返回的情况 为了解决上述两个问题, 可以采用一种更为巧妙的方法, 该方法叫做 tram poline( 弹簧床 ) 程序 trampoline( 弹簧床 ) 程序也就是一段小汇编小代码, 其基本思路是 : 用这段汇编小代码 ( 即 trampoline 小程序 ) 来作为 main() 函数的外部包裹 (external wrapper) 其具体做法如下列代码 :.text.globl _trampoline _trampoline: bl main /* if main ever returns we just call it again */ b _trampoline 我们用这段汇编小代码 ( 即 trampoline 小程序 ) 作为 stage2 可执行映象的执行入口点 然后在 trampoline 汇编小程序中用 CPU 跳转指令跳入 main() 函数中去执行 ; 而当 main() 函数返回时,CPU 执行路径显然再次回到我们的 trampoline 程序 当然也就重新执行 main() 函数, 这也就是 trampoline( 弹簧床 ) 一词的意思所在 (1) 初始化本阶段要使用到的硬件设备这通常包括以下几个部分 : 初始化至少一个串口, 以便和终端用户进行 I/O 输出信息 ; 初始化计时器等 在初始化这些设备之前, 也可以重新把 LED 灯点亮, 以表明我们已经进入 main() 函数执行 设备初始化完成后, 可以输出一些打印信息, 程序名字字符串 版本号等 (2) 检测系统的内存映射 ( memory map) ( 3 ) 加载内核映像和根文件系统映像 1 规划内存占用的布局这里包括两个方面 : 内核映像所占用的内存范围 ; 根文件系统所占用的内存范围 在规划内存占用的布局时, 主要考虑基地址和映像的大小两个方面 对于内核映像, 一般将其拷贝到从 (MEM_START+0x8000) 这个基地址开始的大约 1MB 大小的内存范围内 ( 嵌入式 Linu x 的内核一般都不操过 1MB) 为什么要把从 MEM_START 到 MEM_START+0x8000 这段 32KB 大小的内存空出来呢? 这是因为 Linux 内核要在这段内存中放置一些全局数据结构, 如 : 启动参数和内核页表等信息 而对于根文件系统映像, 则一般将其拷贝到 MEM_START+0x0010,0000 开始的地方 如果用 Ramdisk 作为根文件系统映像, 则其解压后的大小一般是 1MB 329

330 2 从 Flash 上拷贝由于像 ARM 这样的嵌入式 CPU 通常都是在统一的内存地址空间中寻址 Fla sh 等固态存储设备的, 因此从 Flash 上读取数据与从 RAM 单元中读取数据并没有什么不同 用一个简单的循环就可以完成从 Flash 设备上拷贝映像的工作 : While(count) { *dest++ = *src++; /* they are all aligned with word boundary */ count -= 4; /* byte number */ }; (4) 设置内核的启动参数 (5) 调用内核 Boot Loader 调用 Linux 内核的方法是直接跳转到内核的第一条指令处, 也即直接跳转到 MEM_START+0x8000 地址处 如果用 C 语言, 可以像下列示例代码这样来调用内核 : void (*thekernel)(int zero, int arch, u32 params_addr)= (void (*)(int, int, u32) )KERNEL_RAM_BASE; thekernel(0, ARCH_NUMBER, (u32) kernel_params_start); 注意,theKernel() 函数调用应该永远不返回的 如果这个调用返回, 则说明出错 13.4 Boot Loader 的使用 环境设置 绝大多数的 Linux 软件开发都是以 native 方式进行的, 即本机 (HOST) 开发 调试, 本机运行的方式 这种方式通常不适合于嵌入式系统的软件开发, 因为对于嵌入式系统的开发, 没有足够的资源在本机 ( 即板子上系统 ) 运行开发工具和调试工具 通常的嵌入式系统的软件开发采用一种交叉编译调试的方式 交叉编译调试环境建立在宿主机 ( 即一台 PC 机 ) 上, 对应的开发板叫做目标板 开发时使用宿主机上的交叉编译 汇编及连接工具形成可执行的二进制代码, ( 这种可执行代码并不能在宿主机上执行, 而只能在目标板上执行 ) 然后把可执行文件下载到目标机上运行 调试时的方法很多, 可以使用串口, 以太网口等, 具体使用哪种调试方法可以根据目标机处理器所提供的支持作出选择 宿主机和目标板的处理器一般都不相同, 宿主机为 INTEL 处理器, 而目标板如笔者所设计的嵌入式系统目标板为 SAMSUNG S3C44B0X,GNU 编译器提供这样的功能, 在编译编译器时可以选择开发所需的宿主机和目标机从而建立开发环境 所以在进行嵌入式开发前第一步的工作就是要安装一台装有指定操作系统的 PC 机作宿主开发机, 对于嵌入式 LINUX, 宿主机上的操作系统一般要求为 REDHAT LINUX 嵌入式开发通 330

331 常要求宿主机配置有网络, 支持 NFS( 为交叉开发时 mount 所用 ), 支持 TFTP 服务器 ( 为下载烧写所用 ) 等等 然后要在宿主机上建立交叉编译调试的开发环境, 下图 13-6 为典型的嵌入式系统宿主机开发环境模型 目标板 并 口 串 口 网 口 pc 宿主开发机 图 13-5 嵌入式系统典型开发环境 从上图我们可以看出, 嵌入式系统开发板和 pc 主机之间一般有三根线相连, 分 别连接 : pc机的并口和目标板的 JTAG 口, 这主要是为了调试目标板 下载文件用 pc 机的串口和目标板的串口, 这主要是为了将目标板上的调试信息输出到 pc 机这边来, 当然还可以把 pc 机上的命令通过串口线传到目标板上, 达到 pc 机和目标板交互的目的 pc 机的网口和目标板的网口, 主要是为了更快速加载文件系统或内核文件 使用 按照上图将硬件连接好后, 就可以通过 JTAG 口将 boot loader 的二进制文件加载到 SDRAM 中, 这一步可以通过 ARM SDT 软件来完成 当然如果想要将信息输出到 pc 主机 端来, 必须在主机端进行串口设置 ( 建立超级终端 ) bootloader 对串口终端的设置 在大多数嵌入式系统中的目标系统, 是没有配置显示设备的, 那么在调试和配置目标系统时, 我们大都采用串口来管理的方法, 即把目标系统的串口当作目标系 统的显示终端, 无论是打印输出, 还是管理配置输入, 都使用串口 这就需要在主机端建立超级终端, 下面分别介绍 windows 平台和 linux 平台下建立超级终端的方法 1.windows 平台下建立超级终端 在 windows 平台下建立超级终端非常简单, 下面就简单介绍建立步骤, 如果大家还是不太清楚的话, 可以到网上找一些相关资料看看 首先要确保 windows 系统中已经安装好超级终端组件, 即在 我的电脑 -> 控 制面板 -> 添加删除程序 ->Windows 安装程序 -> 通讯 中将 超级终端 选中, 确定安装即可 将 超级终端 打开 : 开始菜单 -> 运行, 输入 C:\Program Files\Accessories 331

332 \HyperTerminal \Hypertrm.exe, 按回车键确定 新建一个连接, 在弹出的对话框中输入连接名 在下面的对话框 ( 图 13-6) 中设置波特率 奇偶校验位 停止位等基本信息 这个对话框描述了主机和目标板之间的连接通道的基本情况, 一定要仔细填写 图 13-6 超级终端设置 本例中波特率为 bps, 数据位 8 位, 无奇偶校验位, 停止位为 1 位, 没有数据流控制 2.Linux 平台下建立超级终端 在缺省的 linux 中, 这项功能是存在的 UUCP 软件包就提供了这项功能 配置如下 : 首先编辑 /etc/uucp/sys 文件, 设置为 : # /dev/ttys0 at bp system SO@ port Serial0_ time any 然后编辑 /etc/uucp/port 文件, 设置为 : # /dev/ttys0 at bps port Serial0_ s 332

333 type direct device /dev/ttys0 speed hardflow false 这样, 超级终端的设置就完成了, 通过 UUCP 提供的命令文件 cu, 就可以使 用超级终端 : $ cu S0@ Connected. 这样 linux 主机和目标板的超级终端就建立起来了 注意 : 在建立主机和目标板之间的串口连接时, 要特别注意两端的串口速度一定要一致, 同时主机端的串口应该是没有硬件流控的, 否则两端的连接是建立不起来的 使用 xmoden,tftp 协议下载内核镜像, 操作 flash 这一步根据每一个 bootloader 会有很大不同, 比如 U_BOOT 就是一款功能非常强大的 bootloader, 其命令接口非常丰富, 如下面几个常用的功能接口 : printenv: 主要用于打印环境变量的值 printenv 打印所有环境变量的值 printenv name 打印某一个环境变量的值 setenv: 设置环境变量的值 setenv name value 将环境变量 name 的值设置为 value md: 用于显示存储器中某段地址的值 md 0x 显示我的目标系统中 Application Flash 中的值 tftpboot: 用于通过 TFTP 从主机上下载内核镜像文件 tftpboot 0x pima ge 从主机下载 pimage 文件放到地址 0x 处 bootm: 从某一地址启动应用镜像 bootm 0x 从 0x 处开始运行镜像 boot: 运行环境变量 bootcmd 中的命令 printenv bootcmd bootcmd=tftpboot 0x pimage;bootm 可以有多条命令, 用分号隔开 boot 首先运行 tftpboot 0x pimage, 然后运行 bootm help [command ]: 显示某一命令的用法 help go go addr [arg...] - start application at address 'addr',passing 'arg' as arguments 笔者开发的 S3C44BOX 嵌入式开发系统中, 具有下列几个接口功能 : load : 加载内核或文件系统到 SDRAM 中 comload: 通过串口加载内核或文件系统到 SDRAM 中 333

334 prog: 烧写内核或文件系统到 flash 中 boot: 启动 linux 内核 run: 运行内核或文件系统 ifconfig: 配置目标板上的网口 IP 地址 help: 如果对上面的命令不清楚, 可以通过 help 命令查看 小结 本章对嵌入式系统下 BootLoader 进行了阐述, 并详细介绍了嵌入式平台下如何设计 BootLoader, 最后, 简单介绍了 BootLoader 的使用方法和命令集 习题 ( 略 ) 334

335 附录 A Linux 系统常用命令 A.1 Linux 文件的复制 删除和移动命令 1. 复制命令命令形式 :cp [ 选项 ] 源文件或目录目标文件或目录功能 : 该命令把指定的源文件复制到目标文件或把多个源文件复制到目标目录中, 同 MSDOS 下的 copy 命令一样, 功能十分强大 命令选项含义 : - a 该选项通常在拷贝目录时使用 它保留链接 文件属性, 并递归地拷贝目录, 其作 用等于 dpr 选项的组合 - d 拷贝时保留链接 - f 删除已经存在的目标文件而不提示 - i 和 f 选项相反, 在覆盖目标文件之前将给出提示要求用户确认 回答 y 时目标文件将被覆盖, 是交互式拷贝 - p 此时 cp 除复制源文件的内容外, 还将把其修改时间和访问权限也复制到新文件中 - r 若给出的源文件是一目录文件, 此时 cp 将递归复制该目录下所有的子目录和文件 此时目标文件必须为一个目录名 - l 不作拷贝, 只是链接文件 2. 移动命令命令形式 :mv [ 选项 ] 源文件或目录目标文件或目录功能 : 该命令为文件或目录改名或将文件由一个目录移入另一个目录中 该命令如同 MSDOS 下的 ren 和 move 的组合 命令选项含义 : - I 交互方式操作 如果 mv 操作将导致对已存在的目标文件的覆盖, 此时系统询问是 否重写, 要求用户回答 y 或 n, 这样可以避免误覆盖文件 - f 禁止交互操作 在 mv 操作要覆盖某已有的目标文件时不给任何指示, 指定此选项后,i 选项将不再起作用 说明 : 根据 mv 命令中第二个参数类型的不同 ( 是目标文件还是目标目录 ),mv 命令将文件重命名或将其移至一个新的目录中 当第二个参数类型是文件时,mv 命令完成文件重命名, 此时, 源文件只能有一个 ( 也可以是源目录名 ), 它将所给的源文件或目录重命名为给定的目标文件名 当第二个参数是已存在的目录名称时, 源文件或目录参数可以有多个,mv 命令将各参数指定的源文件均移至目标目录中 在跨文件系统移动文件时,mv 先拷贝, 再将原有文件删除, 而链至该文件的链接也将丢失 3. 删除命令 335

336 命令形式 :rm [ 选项 ] 文件 功能 : 用户可以用 rm 命令删除不需要的文件 该命令的功能为删除一个目录中的一个或多个文件或目录, 它也可以将某个目录及其下的所有文件及子目录均删除 对于链接文件, 只是断开了链接, 原文件保持不变 命令选项含义 : - f 忽略不存在的文件, 从不给出提示 - r 指示 rm 将参数中列出的全部目录和子目录均递归地删除 - i 进行交互式删除 A.2 Linux 目录的创建与删除命令 1. 目录创建命令形式 :mkdir [ 选项 ] dir-name 功能 : 创建一个目录, 类似 MSDOS 下的 md 命令 命令选项含义 : - m 对新建目录设置存取权限 也可以用 chmod 命令设置 - p 可以是一个路径名称 此时若路径中的某些目录尚不存在, 加上此选项后, 系统将自动建立好那些尚不存在的目录, 即一次可以建立多个目录 2. 目录删除命令形式 :rmdir [ 选项 ] dir-name 功能 : 删除空目录 命令选项含义 : - p 递归删除目录 dirname, 当子目录删除后其父目录为空时, 也一同被删除 如果整个路径被删除或者由于某种原因保留部分路径, 则系统在标准输出上显示相应的信息 3. 改变当前工作目录命令命令形式 :cd [directory] 功能 : 改变工作目录 说明 : 该命令将当前目录改变至 directory 所指定的目录 若没有指定 directory, 则回到用户的主目录 4.pwd 命令语法 :pwd 说明 : 此命令显示出当前工作目录的绝对路径 5.ls 命令命令形式 :ls [ 选项 ] [ 目录或是文件 ] 功能 : 对于每个目录, 该命令将列出其中的所有子目录与文件 对于每个文件,ls 将输出其文件名以及所要求的其他信息 默认情况下, 输出条目按字母顺序排序 当未给出目录名或是文件名时, 就显示当前目录的信息 命令选项含义 : 336

337 - a 显示指定目录下所有子目录与文件, 包括隐藏文件 - A 显示指定目录下所有子目录与文件, 包括隐藏文件 但不列出. 和.. - b 对文件名中的不可显示字符用八进制逃逸字符显示 - c 按文件的修改时间排序 - C 分成多列显示各项 - d 如果参数是目录, 只显示其名称而不显示其下的各文件 往往与 l 选项一起使用, 以得到目录的详细信息 - f 不排序 该选项将使 lts 选项失效, 并使 au 选项有效 - F 在目录名后面标记 /, 可执行文件后面标记 *, 管道 ( 或 FIFO) 后面标记,socket 文件后面标记 = - i 在输出的第一列显示文件的 i 节点号 - L 若指定的名称为一个符号链接文件, 则显示链接所指向的文件 - m 输出按字符流格式, 文件跨页显示, 以逗号分开 - n 输出格式与 l 选项相同, 只不过在输出中文件属主和属组是用相应的 UID 号和 GID 号来表示, 而不是实际的名称 - o 与 l 选项相同, 只是不显示拥有者信息 - p 在目录后面加一个 / - q 将文件名中的不可显示字符用? 代替 - r 按字母逆序或最早优先的顺序显示输出结果 - R 递归式地显示指定目录的各个子目录中的文件 - s 给出每个目录项所用的块数, 包括间接块 - t 显示时按修改时间 ( 最近优先 ) 而不是按名字排序 若文件修改时间相同, 则按字典顺序 修改时间取决于是否使用了 c 或 u 选顶 缺省的时间标记是最后一次修改时间 - u 显示时按文件上次存取的时间 ( 最近优先 ) 而不是按名字排序 即将 -t 的时间标记修改为最后一次访问的时间 - x 按行显示出各排序项的信息 - l 以长格式来显示文件的详细信息 这个选项最常用 每行列出的信息依次是 : 文件类型与权限链接数文件属主文件属组文件大小建立或最近修改的时间名字 用 ls - l 命令显示的信息中, 开头是由 10 个字符构成的字符串, 其中第一个字符表示文件类型, 它可以是下述类型之一 : 普通文件 d 目录 l 符号链接 b 块设备文件 c 字符设备文件后面的 9 个字符表示文件的访问权限, 分为 3 组, 每组 3 位 第一组表示文件属主的权限, 第二组表示同组用户的权限, 第三组表示其他用户的权限 每一组的三个字符分别 表示对文件的读 写和执行权限 各权限如下所示 : 337

338 r 读 w 写 x 执行 对于目录, 表示进入权限 s 当文件被执行时, 把该文件的 UID 或 GID 赋予执行进程的 UID( 用户 ID) 或 GID( 组 ID) t 设置标志位 ( 留在内存, 不被换出 ) 如果该文件是目录, 在该目录中的文件只能被超级用户 目录拥有者或文件属主删除 如果它是可执行文件, 在该文件执行后, 指向其正文段的指针仍留在内存 这样再次执行它时, 系统就能更快 地装入该文件 备注 : 对于符号链接文件, 显示的文件名之后有 和引用文件路径名 对于设备文件, 其 文件大小 字段显示主 次设备号, 而不是文件大小 目录中的总块数显示在长格式列表的开头, 其中包含间接块 A.3 Linux 文本处理命令 1. sort 命令 命令形式 :sort [ 选项 ] 文件功能 :sort 命令对指定文件中所有的行进行排序, 并将结果显示在标准输出上 如不指定输入文件或使用 -, 则表示排序内容来自标准输入 命令选项含义 : - m 若给定文件已排好序, 合并文件 - c 检查给定文件是否已排好序, 如果它们没有都排好序, 则打印一个出错信息, 并以 状态值 1 退出 - u 对排序后认为相同的行只留其中一行 - o 输出文件将排序输出写到输出文件中而不是标准输出, 如果输出文件是输入文件之一,sort 先将该文件的内容写入一个临时文件, 然后再排序和写输出结果 改变缺省排序规则的选项主要有 : - d 按字典顺序排序, 比较时仅字母 数字 空格和制表符有意义 - f 将小写字母与大写字母同等对待 - I 忽略非打印字符 - M 作为月份比较 : JAN < FEB - r 按逆序输出排序结果 +posl - pos2 指定一个或几个字段作为排序关键字, 字段位置从 posl 开始, 到 pos2 为止 ( 包括 posl, 不包括 pos2) 如不指定 pos2, 则关键字为从 posl 到行尾 字段和字符的位置从 0 开始 - b 在每行中寻找排序关键字时忽略前导的空白 ( 空格和制表符 ) - t separator 指定字符 separator 作为字段分隔符 338

339 2.uniq 命令命令形式 :uniq [ 选项 ] 文件 功能 : 这个命令读取输入文件, 并比较相邻的行 在正常情况下, 第二个及以后更多 个重复行将被删去, 行比较是根据所用字符集的排序序列进行的 该命令加工后的结果写到输出文件中 输入文件和输出文件必须不同 如果输入文件用 - 表示, 则从标准输入读取 命令选项含义 : - c 显示输出中, 在每行行首加上本行在文件中出现的次数 它可取代 - u 和 - d 选项 - d 只显示重复行 - u 只显示文件中不重复的各行 - n 前 n 个字段与每个字段前的空白一起被忽略 一个字段是一个非空格 非制表符的字符串, 彼此由制表符和空格隔开 ( 字段从 0 开始编号 ) +n 前 n 个字符被忽略, 之前的字符被跳过 ( 字符从 0 开始编号 ) - f n 与 - n 相同, 这里 n 是字段数 - s n 与 +n 相同, 这里 n 是字符数 A. 4 Linux 备份与压缩命令 1. tar 命令命令形式 :tar [ 主选项 + 辅选项 ] 文件或者目录功能 :tar 可以为文件和目录创建档案 利用 tar, 用户可以为某一特定文件创建档案 ( 备份文件 ), 也可以在档案中改变文件, 或者向档案中加入新的文件 命令选项含义 : 主选项 : c 创建新的档案文件 如果用户想备份一个目录或是一些文件, 就要选择这个选项 r 把要存档的文件追加到档案文件的未尾 例如用户已经作好备份文件, 又发现还有一个目录或是一些文件忘记备份了, 这时可以使用该选项, 将忘记的目录或文件追加到备份文件中 t 列出档案文件的内容, 查看已经备份了哪些文件 u 更新文件 就是说, 用新增的文件取代原备份文件, 如果在备份文件中找不到要更新的文件, 则把它追加到备份文件的最后 x 从档案文件中释放文件 辅助选项 : b 该选项是为磁带机设定的 其后跟一数字, 用来说明区块的大小, 系统预设值为 20 (20*512 bytes) f 使用档案文件或设备, 这个选项通常是必选的 k 保存已经存在的文件 例如我们把某个文件还原, 在还原的过程中, 遇到相同的文 339

340 件, 不会进行覆盖 m 在还原文件时, 把所有文件的修改时间设定为现在 M 创建多卷的档案文件, 以便在几个磁盘中存放 v 详细报告 tar 处理的文件信息 如无此选项,tar 不报告文件信息 w 每一步都要求确认 z 用 gzip 来压缩 / 解压缩文件, 加上该选项后可以将档案文件进行压缩, 但还原时也一定要使用该选项进行解压缩 2. gzip 命令 命令形式 :gzip [ 选项 ] 压缩 ( 解压缩 ) 的文件名功能 :gzip 是在 Linux 系统中经常使用的一个对文件进行压缩和解压缩的命令, 既方便又好用 命令选项含义 : -c 将输出写到标准输出上, 并保留原有文件 -d 将压缩文件解压 -l 对每个压缩文件, 显示下列字段 : 压缩文件的大小未压缩文件的大小压缩比未压缩文件的名字 -r 递归式地查找指定目录并压缩其中的所有文件或者是解压缩 -t 测试, 检查压缩文件是否完整 -v 对每一个压缩和解压的文件, 显示文件名和压缩比 -num 用指定的数字 num 调整压缩的速度,-1 或 --fast 表示最快压缩方法 ( 低压缩比 ), -9 或 --best 表示最慢压缩方法 ( 高压缩比 ) 系统缺省值为 6 3. unzip 命令命令形式 : unzip [ 选项 ] 压缩文件名.zip 功能 : 该命令用于解扩展名为.zip 的压缩文件 命令选项含义 : -x 文件列表解压缩文件, 但不包括指定的 file 文件 -v 查看压缩文件目录, 但不解压 -t 测试文件有无损坏, 但不解压 -d 目录把压缩文件解到指定目录下 -z 只显示压缩文件的注解 -n 不覆盖已经存在的文件 -o 覆盖已存在的文件且不要求用户确认 -j 不重建文档的目录结构, 把所有文件解压到同一目录下 340

341 A.5 Linux 改变文件或目录的访问权限命令 Linux 系统中的每个文件和目录都有访问许可权限, 用它来确定谁可以通过何种方式 对文件和目录进行访问和操作 文件或目录的访问权限分为只读, 只写和可执行三种 以文件为例, 只读权限表示只允许读其内容, 而禁止对其做任何的更改操作 可执行权限表示允许将该文件作为一个程序执行 文件被创建时, 文件所有者自动拥有对该文件的读 写和可执行权限, 以便于对文件的阅读和修改 用户也可根据需要把访问权限设置为需要的任何组合 有三种不同类型的用户可对文件或目录进行访问 : 文件所有者, 同组用户 其他用户 所有者一般是文件的创建者 所有者可以允许同组用户有权访问文件, 还可以将文件的访问权限赋予系统中的其他用户 在这种情况下, 系统中每一位用户都能访问该用户拥有的文件或目录 每一文件或目录的访问权限都有三组, 每组用三位表示, 分别为文件属主的读 写和执行权限 ; 与属主同组的用户的读 写和执行权限 ; 系统中其他用户的读 写和执行权限 当用 ls -l 命令显示文件或目录的详细信息时, 最左边的一列为文件的访问权限 例如 : $ ls -l abc.tgz -rw-r--r-- 1 root root Ju1 l5 17:3l abc. tgz 横线代表空许可 r 代表只读,w 代表写,x 代表可执行 ( 注意这里共有 10 个字符, 第一个字符指定了文件类型 在通常意义上, 一个目录也是一个文件 如果第一个字符是横线, 表示是一个非目录的文件 如果是 d, 表示是一个目录 ) 例如上面例子中 -rw-r--r 表示的含义如下 : -: 表示 abc.tgz 是一个普通文件 rw-:abc.tgz 的属主有读写权限 ; r--: 与 sobsrc. tgz 属主同组的用户只有读权限 ; r--: 其他用户只有读权限 确定了一个文件的访问权限后, 用户可以利用 Linux 系统提供的 chmod 命令来重新设定不同的访问权限 也可以利用 chown 命令来更改某个文件或目录的所有者 利用 chgrp 命令来更改某个文件或目录的用户组 下面分别对这些命令加以介绍 1. chmod 命令功能 :chmod 命令用于改变文件或目录的访问权限 用户用它控制文件或目录的访问权限 该命令有两种用法 一种是包含字母和操作符表达式的文字设定法 ; 另一种是包含数字的数字设定法 (1) 文字设定法命令形式 :chmod [who] [+ - =] [mode] 文件名命令选项含义 : 操作对象 who 可是下述字母中的任一个或者它们的组合 : 341

342 u 表示 用户 (user), 即文件或目录的所有者 g 表示 同组 (group) 用户, 即与文件属主有相同组 ID 的所有用户 o 表示 其他 (others) 用户 a 表示 所有 (all) 用户 它是系统默认值 操作符号可以是 : + 添加某个权限 - 取消某个权限 = 赋予给定权限并取消其他所有权限 ( 如果有的话 ) 设置 mode 所表示的权限可用下述字母的任意组合 : r 可读 w 可写 x 可执行 X 只有目标文件对某些用户是可执行的或该目标文件是目录时才追加 x 属性 s 在文件执行时把进程的属主或组 ID 置为该文件的文件属主 方式 u+s 设置 文件的用户 ID 位, g+s 设置组 ID 位 t 保存程序的文本到交换设备上 u 与文件属主拥有一样的权限 g 与和文件属主同组的用户拥有一样的权限 o 与其他用户拥有一样的权限 文件名 : 以空格分开的要改变权限的文件列表, 支持通配符 在一个命令行中可给出多个权限方式, 其间用逗号隔开 例如 :chmod g+r,o+r example 使同组和其他用户对文件 example 有读权限 (2) 数字设定法命令形式 :chmod [mode] 文件名数字表示的属性的含义 : 0 表示没有权限 ; 1 表示可执行权限 ; 2 表示可写权限 ; 4 表示可读权限 ; 然后将其相加 所以数字属性的格式应为 3 个从 0 到 7 的八进制数, 其顺序是 (u)(g) (o) 例如, 如果想让某个文件的属主有 读 / 写 二种权限, 需要把 4( 可读 )+2( 可写 )= 6 ( 读 / 写 ) 2.chgrp 命令 命令形式 :chgrp [ 选项 ] group filename 功能 : 该命令改变指定指定文件所属的用户组 其中 group 可以是用户组 ID, 也可以是 /etc/gro up 文件中用户组的组名 文件名是以空格分开的要改变属组的文件列表, 支持通 配符 如果用户不是该文件的属主或超级用户, 则不能改变该文件的组 342

343 命令选项含义 : - R 递归式地改变指定目录及其下的所有子目录和文件的属组 3.chown 命令命令形式 :chown [ 选项 ] 用户或组文件功能 :chown 将指定文件的拥有者改为指定的用户或组 用户可以是用户名或用户 ID 组可以是组名或组 ID 文件是以空格分开的要改变权限的文件列表, 支持通配符 例如 root 用户把自己的一个文件拷贝给用户 xu, 为了让用户 xu 能够存取这个文件,root 用户应该把这个文件的属主设为 xu, 否则, 用户 xu 无法存取这个文件 命令选项含义 : - R 递归式地改变指定目录及其下的所有子目录和文件的拥有者 - v 显示 chown 命令所做的工作 A.6 Linux 与用户有关的命令 1.useradd 命令命令形式 : 功能 : 命令选项含义 : 2.passwd 命令命令形式 :passwd [ 用户名 ] 功能 : 出于系统安全考虑,Linux 系统中的每一个用户除了有其用户名外, 还有其对应的用户口令 因此使用 useradd 命令增加时, 还需使用 passwd 命令为每一位新增加的用户设置口令, 用户以后还可以随时用 passwd 命令改变自己的口令 该命令的使用方法如下 : 输入 passwd< Enter>; 在 (current) UNIX passwd: 下输入当前的口令在 new password: 提示下输入新的口令 ( 在屏幕上看不到这个口令 ): 系统提示再次输入这个新口令 输入正确后, 这个新口令被加密并放入 /etc/shdow 文件 选取一个不易被破译的口令是很重要的 选取口令应遵守如下规则 : 口令应该至少有六位 ( 最好是八位 ) 字符 ; 口令应该是大小写字母 标点符号和数字混杂的 超级用户修改其他用户 (xxq) 的口令的过程如下, # passwd root New UNIX password: Retype new UNIX password: 343

344 passwd: all authentication tokens updated successfully # 3.su 命令命令形式 : su [ 选项 ] [? ] [ 使用者帐号 ] 功能 : 它可以让一个普通用户拥有超级用户或其他用户的权限, 也可以让超级用户以普通用户的身份做一些事情 普通用户使用这个命令时必须有超级用户或其他用户的口令 如要离开当前用户的身份, 可以打 exit 若没有指定使用者帐号, 则系统预设值为超级用户 root 命令选项含义 :? c 执行一个命令后就结束? 加了这个减号的目的是使环境变量和欲转换的用户相同? m 保留环境变量不变 password: 输入超级用户的密码 A. 7 网络命令 第一类 : 设置工具 1.netconf 命令 netconf 是 Red Hat Linux 提供的 Linuxconf 的一部分, 主要用于设置与网络相关的参数 它可以在 consle 下运行 ( 文本菜单 ), 也可以在 X-Window 中运行 ( 图形界面 ) netconf 的使用比较简单, 下面就简要介绍其使用步骤 2.ifconfig 命令 ifconfig 是 Linux 系统中最常用的一个用来显示和设置网络设备的工具 其中 if 是 interface 的缩写 它可以用来设备网卡的状态, 或是显示当前的设置 常用的命令组合 : 将第一块网卡的 IP 地址设置为 : ifconfig eth ( 格式 :ifconfig 网络设备名 IP 地址 ) 暂时关闭或启用网卡 : 关闭第一块网卡 :ifconfig eth0 down 启用第一块网卡 :ifconfig eth0 up 将第一块网卡的子网掩码设置为 : 格式 :ifconfig 网络设备名 netmask 子网掩码 ifconfig eth0 netmask 我们也可以同时设置 IP 地址和子网掩码 : ifconfig eth netmask 将第一块网卡的广播地址设置为 : ifconfig eth0 -broadcast

345 将第一块网卡设置为不接收多播数据包 : ifconifg eth0 allmulti 如果要让其接收, 则使用命令 :ifconfig eth0 -allmulti 查看第一块网卡的状态 : ifconfig eth0 如果要查看所有的网卡状态, 则直接使用不带参数的 ifconfig 命令即可 ifconfig 输出的状态信息是十分有用的, 下面, 我们就简单说明一下几个比较重要的状态 : UP/DOWN: 网卡是否启动了, 如果是 DOWN 的话, 那肯定无法用的 ; RX packets 中的 errors 包的数量如果过大说明网卡在接收时有问题 ; TX packets 中的 errors 包的数量如果过大说明网卡在发送时有问题 ; 3.route 命令功能 :route 命令是用来查看和设置 Linux 系统的路由信息, 以实现与其它网络的通讯 要实现两个不同的子网之间的网络通讯, 需要一台连接两个网络路由器或者同时位于 两个网络的网关来实现 在 Linux 系统中, 我们通常设置路由是为了解决以下问题 : 该 Linux 机器在一个局域网中, 局域网中有一个网关, 能够让你的机器访问 Internet, 那么我们就需要将这台机器 的 IP 地址设置为 Linux 机器的默认路由 常用命令组合 : 增加一个默认路由 : route add gw 网关地址 删除一个默认路由 : route del gw 网关地址 显示出当前路由表 route 第二类 : 诊断工具 1.ping 命令命令形式 : 功能 : ping 是一个最常用的检测是否能够与远端机器建立网络通讯连接 它是通过 Inter net 控制报文协议 ICMP 来实现的 而现在有些主机对 ICMP 进行过滤, 在这种特殊的情况下, 有可能使得一些主机 Ping 不通, 但能够建立网络连接 这是一种特例, 在此事先说明 常用命令组合 : 检测与某机器的连接是否正常 : ping ping 也就是说, 我们可以用 IP 地址或域名来指定机器 指定 ping 回应次数为 n: 345

346 在 Linux 下, 如果你不指定回应次数,ping 命令将一直不断地向远方机器发送 ICMP 信息 我们可以通过 -c 参数来限定 : ping -c n 通过特定的网卡进行 ping: 有时, 我们需要检测某块网卡 ( 系统中有多块 ) 能否 ping 通远方机器 我们需要在执行 ping 命令时指出 : ping -I eth traceroute 命令命令形式 :traceroute 远程主机 IP 地址或域名功能 : 如果你 ping 不通远方的机器, 想知道是在什么地方出的问题 ; 或者你想知道你的信息到远方机器都经过了哪些路由器, 可以使用 traceroute 命令 顾名思义 : trace 是跟踪,route 是路由, 也就是跟踪路由 这个命令的输出类似于下面的输出条目 : 1 路由器 ( 网关 ) 的 IP 地址访问所需时间 1 访问所需时间 2 访问所需时间 3 2 路由器 ( 网关 ) 的 IP 地址访问所需时间 1 访问所需时间 2 访问所需时间 3 其中, 每一列的含义如下 : 最前面的数字代表 经过第几站 ; 路由器 ( 网关 ) 的 IP 地址就是 该站 的 IP 地址 ; 访问所需时间 是指访问到这个路由器 ( 网关 ) 需要的时间 3.netstat 命令命令形式 : 功能 : 在 Linux 系统中, 提供了一个功能十分强大的查看网络状态的工具 :netstat 它可以让您得知整个 Linux 系统的网络情况 常用命令组合 : 1) 统计出各网络设备传送 接收数据包的情况 : 使用命令 :netstat -i 这个命令将输出一张表, 其中包括 : Iface: 网络接口名 MTU: 最大传输单元 RX-OK: 共成功接收多少个包 RX-ERR: 接收的包中共有多少个错误包 RX-DRP: 接收时共丢失多少个包 RX-OVR: 共接收了多少个碰撞包 TX-OK: 共成功发送多少个包 TX-ERR: 发送的包中共有多少个错误包 TX-DRP: 发磅时共丢失多少个包 TX-OVR: 共接收了多少个碰撞包 2) 显示网络的统计信息使用命令 :netstat -s 使用这个命令, 将会以摘要的形式统计出 IP ICMP TCP UDP TCPEXT 形式的通信信息 3) 显示出 TCP 传输协议的网络连接情况 : 使用命令 :netstat -t 346

347 这个命令的输出也是一张表, 其中包括 : Local Address: 本地地址, 格式是 IP 地址 : 端口号 Fore ign Address: 远程地址, 格式也是 IP 地址 : 端口号 State: 连接状态, 包括 LISTEN ESTABLISHED TIME_WAIT 等 4) 只显示出使用 UDP 的网络连接情况 : 使用命令 :netstat -t 输出格式也是一样的 5) 显示路由表 : 使用命令 :netstat -r 这个命令的输出与 route 命令的输出相同 347

348 内容简介 本书以 ARM 微处理器为例, 详细介绍了嵌入式系统基本原理和相关设计技术 书中内容均为作者多年从事嵌入式教学和科研经验之积累, 故内容翔实, 阐述清晰, 使读者能够深刻掌握嵌入式系统的基本原理和应用程序的设计与开发 全书共分 13 章, 内容包括 : 第 1 章详细介绍了嵌入式系统的基本概念, 给读者构建了一个嵌入式系统的轮廓 ; 第 2 章重点介绍了 ARM 相关知识, 包括 ARM 微处理器 ARM 编程模型 ARM 指令系统和 ARM 程序设计 ; 第 3~5 章介本章绍了嵌入式 Linux 相关概念, 通过的学习, 读者可以了解到嵌入式 Linux 系统下程序开发基础知识 嵌入式 Linux 体系结构 内存管理 文件系统 进程管理和驱动程序设计等知识 ; 第 6~12 章以 S3C44B0X 嵌入式微处理器为例, 介绍了其体系结构 接口电路设计以及相关底层函数编写等内容 ; 第 13 章对嵌入式系统的启动部件 BootLoader 进行了深入浅出的剖析, 详细描述了 BootLoader 的机理, 并在最后详细介绍 BootLoader 如何设计与使用 本书有完善的实验设备和教学课件与之配套, 可作为高等院校通信类 电子类 信息类和理工类以及其它学科本科生教材使用, 也可供研究生和嵌入式开发人员使用 348

349 前言 嵌入式系统的概念早在上世纪 70 年代就已经出现, 最近十几年随着半导体技术和信息技术的发展, 嵌入式成为当前最热门的技术之一 现在, 嵌入式技术已经渗透到科学研究 军事 航空航天 信息家电等各个领域, 极大改变着人们的生活 随着国内外各种各样嵌入式产品的设计和流行, 嵌入式技术的重要性日益显现出来, 这方面的人才需求也日益增多, 但是嵌入式人才的培养在高校教学中却还是空白, 因此高等院校如何根据社会需求培养出合格的嵌入式人才是一个非常紧迫的任务 在此背景下, 作者于 2004 年编写了 嵌入式系统原理与设计 讲义, 在南京邮电大学计算机学院试用, 并在多次社会培训中使用 本书在原讲义的基础之上, 结合广大师生的反馈意见修改而成, 可以作为高等院校通信类 电子类 信息类和理工类以及其它学科本科生教材使用, 也可供研究生和嵌入式开发人员使用 嵌入式系统是软硬件相结合的应用系统, 涉及多门科学知识 在这些知识体系结构中, 有两个核心 : 嵌入式微处理器和嵌入式操作系统, 要想培养出合格的嵌入式人才, 必须紧紧围绕这两个核心因材施教 本书以 S3C44B0X 微处理为例, 结合嵌入式 Linux 操作系统, 详细讨论了其硬件结构和软件架构, 从嵌入式理论和应用的角度阐述嵌入式系统的基本原理和实际应用开发 本书内容丰富, 包括下列知识点 : 第 1 章详细介绍了嵌入式系统的基本概念, 给读者构建了一个嵌入式系统的轮廓 ; 第 2 章重点介绍了 ARM 相关知识, 包括 ARM 微处理器 ARM 编程模型 ARM 指令系统和 ARM 程序设计 ; 第 3~5 章介本章绍了嵌入式 Linux 相关概念, 通过的学习, 读者可以了解到嵌入式 Linux 系统下程序开发基础知识 嵌入式 Linux 体系结构 内存管理 文件系统 进程管理和驱动程序设计等知识 ; 第 6~12 章以 S3C44B0X 嵌入式微处理器为例, 介绍了其体系结构 接口电路设计以及相关底层函数编写等内容 ; 第 13 章对嵌入式系统的启动部件 BootLoader 进行了深入浅出的剖析, 详细描述了 BootLoader 的机理, 并在最后详细介绍 BootLoader 如何设计与使用 嵌入式系统软中有硬, 硬中含软, 软硬结合 本书在描述软硬件时, 都会从软硬两个角度去探讨, 力求让读者能够深刻掌握嵌入式系统的精华 另外, 本书特别重视理论和实践相结合, 力求既面向教学, 又面向实际开发, 因此本书既可作为高等院校通信类 电子类 信息类和理工类以及其它学科本科生教材使用, 又可供广大嵌入式开发人员在科学研究和产品开发时使用 本书涉及内容广泛, 加之时间仓促, 作者水平有限, 书中难免出现错误和不妥之处, 349

350 恳请广大读者和同行批评指正 如果在学习开发中发现问题, 或有好的建议, 欢迎来函 电子邮件 编者 参考文献 1. 李岩等. 基于 S3C44B0X 嵌入式 uclinux 系统原理及应用. 北京 : 清华大学出版社, 廖日坤.ARM 嵌入式应用开发技术白金手册. 北京 : 中国电力出版社, 杜春雷.ARM 体系结构与编程. 北京 : 清华大学出版社, ( 美 )Karim Yagbmour. 构建嵌入式 LINUX 系统. 韩存兵等改编. 北京 : 中国电力出版社, 李善平等.Linux 与嵌入式系统. 北京 : 清华大学出版社, 王田苗. 嵌入式系统设计与实例开发. 北京 : 清华大学出版社, 邹思铁. 嵌入式 Linux 设计与应用. 北京清华大学出版社, 胥静. 嵌入式系统设计与开发实例详解 - 基于 ARM 的应用. 北京 : 北京航空航天大学出版社, 三星公司.S3C44B0X Data Sheet 数据手册 ( 美 )ALESSANDRO RUBINI.LINUX 设备驱动程序.LISOLEG 译. 北京 : 中国电力出版社 350

<4D F736F F D20B5DAC8FDCBC4D5C2D7F7D2B5B4F0B0B82E646F63>

<4D F736F F D20B5DAC8FDCBC4D5C2D7F7D2B5B4F0B0B82E646F63> 第三章 Q3 1 1. 省略了 I/O 操作的复杂逻辑, 易实现, 耗费低 ; 2. 可以利用丰富的内存寻址模式实现灵活的 I/O 操作 Q3 2 假设存储单元 ds1 处寄存器地址为 0x2000, 代码如下 #define ds1 0x2000 while ( *ds1 == 0 ) ; Q3 3 假设设备 (dev1) 中有两个寄存器 ds1 和 dd1,dev1 的地址为 0x1000,ds1

More information

,,, PCB, AR M VxWorks DSP,,,,,,,,,,, (CIP) /,,.:,2005 ISBN TP36 CIP (2005) : ( 10 ) : : (010 ) : (010)

,,, PCB, AR M VxWorks DSP,,,,,,,,,,, (CIP) /,,.:,2005 ISBN TP36 CIP (2005) : ( 10 ) : : (010 ) : (010) ,,, PCB, AR M VxWorks DSP,,,,,,,,,,, (CIP) /,,.:,2005 ISBN 7-5635-1099-0...............TP36 CIP (2005)076733 : ( 10 ) :100876 : (010 )62282185 : (010)62283578 : [email protected] : : : 787 mm960 mm 1/

More information

Ch03_嵌入式作業系統建置_01

Ch03_嵌入式作業系統建置_01 Chapter 3 CPU Motorola DragonBall ( Palm PDA) MIPS ( CPU) Hitachi SH (Sega DreamCast CPU) ARM StrongARM CPU CPU RISC (reduced instruction set computer ) CISC (complex instruction set computer ) DSP(digital

More information

Microsoft PowerPoint sun-arm isa2.ppt [Compatibility Mode]

Microsoft PowerPoint sun-arm isa2.ppt [Compatibility Mode] 嵌入式系统设计与应用 第二章 ARM 指令系统 (2) 西安交通大学电信学院 孙宏滨 汇编伪指令 汇编伪指令 : 在 ARM 汇编语言里, 有一些特殊指令助记符, 没有相对应的操作码 ( 或直接对应指令 ) 通常称这些特殊指令助记符为伪指令, 它们所完成的操作叫做伪操作 伪指令在源程序中的作用是为完成汇编程序作各种准备工作 这些伪指令仅在汇编过程中起作用, 一旦汇编结束, 伪指令的使命完成 ADR:

More information

ARM JTAG实时仿真器安装使用指南

ARM JTAG实时仿真器安装使用指南 ARM JTAG Version 1.31 2003. 11. 12 ARM JTAG ARM JTAG.3 ARM 2.1.4 2.2.4 ARM JTAG 3.1 18 3.2 18 3.2.1 Multi-ICE Server.18 3.2.2 ADS..21 ARM JTAG 4.1 Multi-ICE Server 33 4.1.1 Multi-ICE Server..... 33 4.1.2

More information

一个开放源码的嵌入式仿真环境 ― SkyEye

一个开放源码的嵌入式仿真环境 ― SkyEye SkyEye SkyEye http://hpclab.cs.tsinghua.edu.cn/~skyeye/ I hear and I forget, I see and I remember, I do and I understand. SkyEye SkyEye SkyEye SkyEye SkyEye 1. SkyEye PC pervasive computing PC I O PDA

More information

Microsoft Word - 正文.doc

Microsoft Word - 正文.doc 1 2 1 2 3 4 5 6 7 8 9 10 3 1 150 2 150 1 1 1.1 1.1.1 1.2 1.2.1 1.2.2 1.2.3 1.3 1.3.1 1.3.2 1.4 1.4.1 CPU 1.4.2 I/O 1.4.3 I/O 1.5 1.5.1 CISC RISC 1.5.2 1.5.3 1.6 1.6.1 1.6.2 N 1.6.3 2 2.1 2.1.1 2.1.2 2.1.3

More information

常用ARM指令集及汇编

常用ARM指令集及汇编 常用 ARM 指令集及汇编 2003 年 12 月 1 日 前 言 ARM(Advanced RISC Machines) 是 微 处 理 器 行 业 的 一 家 知 名 企 业, 该 企 业 设 计 了 大 量 高 性 能 廉 价 耗 能 低 的 RISC 处 理 器 相 关 技 术 及 软 件 技 术 具 有 性 能 高 成 本 低 和 能 耗 省 的 特 点, 适 用 于 多 种 领 域, 比

More information

CH559指令周期.doc

CH559指令周期.doc CH55X 汇编指令周期表 CH55X 汇编指令概述 : 型号包含 : a. 指令单周期型号 :CH557 CH558 CH559; b. 指令 4 周期型号 :CH551 CH552 CH553 CH554; c. 非跳转指令的指令周期数与指令字节数相同 ; d. 跳转指令含 MOVC/RET/CALL 通常比字节数多若干个周期 ; e.movc 指令多 4 或 5 个周期 ( 下条指令地址为奇数时多

More information

MSP430ϵÁе¥Æ¬»úµÄÖ¸Áîϵͳ.pps [¼æÈÝģʽ]

MSP430ϵÁе¥Æ¬»úµÄÖ¸Áîϵͳ.pps [¼æÈÝģʽ] 作者 : 利尔达 MSP430 系列单片机的指令系统 1 CPU 内核组成 : 16 位的 (ALU) 算术运算单元 16 个寄存器 (PC SP SR R4~R15) 指令控制单元 2 存储器组织结构 3 外围模块寄存器地址 它们被分配在相应的字模块或字节模块当中 分配在 00-FFH 中为字节, 分配在 100-1FFH 中为字 4 寻址模式 : 5 指令格式 : 1) 书写格式标号指令助记符源操作数,

More information

ARM中C和汇编混合编程及示例.doc

ARM中C和汇编混合编程及示例.doc ARM 中 C 和汇编混合编程及示例 在嵌入式系统开发中, 目前使用的主要编程语言是 C 和汇编,C++ 已经有相应的编译器, 但是现在使用还是比较少的 在稍大规模的嵌入式软件中, 例如含有 OS, 大部分的代码都是用 C 编写的, 主要是因为 C 语言的结构比较好, 便于人的理解, 而且有大量的支持库 尽管如此, 很多地方还是要用到汇编语言, 例如开机时硬件系统的初始化, 包括 CPU 状态的设定,

More information

目录 1 IPv6 快速转发 IPv6 快速转发配置命令 display ipv6 fast-forwarding aging-time display ipv6 fast-forwarding cache ipv6 fas

目录 1 IPv6 快速转发 IPv6 快速转发配置命令 display ipv6 fast-forwarding aging-time display ipv6 fast-forwarding cache ipv6 fas 目录 1 IPv6 快速转发 1-1 1.1 IPv6 快速转发配置命令 1-1 1.1.1 display ipv6 fast-forwarding aging-time 1-1 1.1.2 display ipv6 fast-forwarding cache 1-1 1.1.3 ipv6 fast-forwarding aging-time 1-3 1.1.4 ipv6 fast-forwarding

More information

1 CPU

1 CPU 2000 Tel 82316285 82317634 Mail [email protected] 1 CPU 2 CPU 7 72 A B 85 15 3 1/2 M301 2~17 : 3/4 1/2 323 IBM PC 1. 2. 3. 1. 2. 3. 1.1 Hardware Software 1.2 M3 M2 M1 1.2 M3 M1 M2 M2 M1 M1 M1 1.2 M3 M1

More information

投影片 1

投影片 1 2 理 1 2-1 CPU 2-2 CPU 理 2-3 CPU 類 2 什 CPU CPU Central Processing Unit ( 理 ), 理 (Processor), CPU 料 ( 例 ) 邏 ( 例 ),, 若 了 CPU, 3 什 CPU CPU 了, 行, 利 CPU 力 來 行 4 什 CPU 5 2-2-1 CPU CPU 了 (CU, Control Unit) / 邏

More information

<4D6963726F736F667420576F7264202D20C7B6C8EBCABDCFB5CDB3C9E8BCC6CAA6BFBCCAD4B4F3B8D92E646F63>

<4D6963726F736F667420576F7264202D20C7B6C8EBCABDCFB5CDB3C9E8BCC6CAA6BFBCCAD4B4F3B8D92E646F63> 嵌 入 式 系 统 设 计 师 考 试 大 纲 一 考 试 说 明 1 考 试 要 求 : (1) 掌 握 科 学 基 础 知 识 ; (2) 掌 握 嵌 入 式 系 统 的 硬 件 软 件 知 识 ; (3) 掌 握 嵌 入 式 系 统 分 析 的 方 法 ; (4) 掌 握 嵌 入 式 系 统 设 计 与 开 发 的 方 法 及 步 骤 ; (5) 掌 握 嵌 入 式 系 统 实 施 的 方 法

More information

《计算机应用基础》学习材料(讲义)

《计算机应用基础》学习材料(讲义) 计 算 机 应 用 基 础 学 习 材 料 ( 讲 义 ) Fundamentals of Computer Application 2014-3-22 JIANGSU OPEN UNIVERSITY 第 二 学 习 周 计 算 机 基 础 知 识 ( 一 ) 导 学 在 本 学 习 周, 我 们 主 要 的 任 务 是 认 识 计 算 机 你 将 知 道 计 算 机 是 什 么 时 候 产 生 的,

More information

Microsoft PowerPoint - 05-第五讲-寻址方式.pptx

Microsoft PowerPoint - 05-第五讲-寻址方式.pptx 第五讲 授课教师 : 陆俊林王箫音 2012 年春季学期 主要内容 一 寻址方式概述 二 数据的寻址方式 三 转移地址的寻址方式 教材相关章节 : 微型计算机基本原理与应用 ( 第二版 ) 第 4 章寻址方式与指令系统 1 主要内容 一 寻址方式概述 二 数据的寻址方式 三 转移地址的寻址方式 2 指令的组成 指令由操作码和操作数两部分组成 操作码操作数 MOV AX, 8726H ADD AX,

More information

目录 1 IPv6 快速转发 IPv6 快速转发配置命令 display ipv6 fast-forwarding aging-time display ipv6 fast-forwarding cache ipv6 fas

目录 1 IPv6 快速转发 IPv6 快速转发配置命令 display ipv6 fast-forwarding aging-time display ipv6 fast-forwarding cache ipv6 fas 目录 1 IPv6 快速转发 1-1 1.1 IPv6 快速转发配置命令 1-1 1.1.1 display ipv6 fast-forwarding aging-time 1-1 1.1.2 display ipv6 fast-forwarding cache 1-1 1.1.3 ipv6 fast-forwarding aging-time 1-3 1.1.4 ipv6 fast-forwarding

More information

未完成

未完成 ARM S3C4510B 1 ARM S3C4510B ARM S3C4510B 2 16/32 RISC ARM ARM Samsung ARM S3C4510B ARM ARM ARM 16/32 ARM S3C4510B 3 ARM S3C4510B 4 ARM S3C4510B 5 1 ARM... 7 1.1 ARM Advanced RISC Machines... 7 1.2 ARM...

More information

ARM处理器中ARM和THUMB状态的切换(Interworking)

ARM处理器中ARM和THUMB状态的切换(Interworking) http//www.elecfans.com 电子发烧友 http//bbs.elecfans.com ARM 处理器中 ARM 和 Thumb 状态的切换 (Interworking) 潘朝霞北京交通大学电气学院王毅北京交通大学电气学院 摘要 主要介绍了在 ARM 处理器中,ARM/Thumb 状态切换的原因和方法 在基于 ARM 处理器的嵌入式开发中, 为了增强系统的灵活性以及提高系统的整体性能经常需要使用

More information

HD ( ) 18 HD ( ) 18 PC 19 PC 19 PC 20 Leica MC170 HD Leica MC190 HD 22 Leica MC170 HD Leica MC190 HD Leica MC170 HD

HD ( ) 18 HD ( ) 18 PC 19 PC 19 PC 20 Leica MC170 HD Leica MC190 HD 22 Leica MC170 HD Leica MC190 HD Leica MC170 HD Leica MC170 HD Leica MC190 HD 5 6 7 8 11 12 13 14 16 HD ( ) 18 HD ( ) 18 PC 19 PC 19 PC 20 Leica MC170 HD Leica MC190 HD 22 Leica MC170 HD Leica MC190 HD 22 23 24 26 Leica MC170 HD Leica MC190 HD ( ) 28

More information

帝国CMS下在PHP文件中调用数据库类执行SQL语句实例

帝国CMS下在PHP文件中调用数据库类执行SQL语句实例 帝国 CMS 下在 PHP 文件中调用数据库类执行 SQL 语句实例 这篇文章主要介绍了帝国 CMS 下在 PHP 文件中调用数据库类执行 SQL 语句实例, 本文还详细介绍了帝国 CMS 数据库类中的一些常用方法, 需要的朋友可以参考下 例 1: 连接 MYSQL 数据库例子 (a.php)

More information

水晶分析师

水晶分析师 大数据时代的挑战 产品定位 体系架构 功能特点 大数据处理平台 行业大数据应用 IT 基础设施 数据源 Hadoop Yarn 终端 统一管理和监控中心(Deploy,Configure,monitor,Manage) Master Servers TRS CRYSTAL MPP Flat Files Applications&DBs ETL&DI Products 技术指标 1 TRS

More information

第5章:汇编语言程序设计

第5章:汇编语言程序设计 第 5 章 : 汇编语言程序设计 程 汇编语言指令格式 系统伪指令 存储器选择方式 常用子程序 1 汇编语言程序设计 PIC 指令系统 语言系统 指 CPU 编 器语言 器语言 器语言 设计 用 语言 设计 语言 汇编语言 2 汇编语言指令格式 汇编语言指令格式 ( 指令 ) label opcode operand comment 指令 用 存 指令 指令语 3 汇编语言指令格式 1 指令 用 指令

More information

01

01 ZEBRA 技术白皮书 条码编码 101 相关知识介绍 引言 20 70 数据 80 20 90 (JIT) AIAG EIA HIBCC HAZMAT 条码的优势提高数据准确性 99% 85% / / 提升效率 / 2 Zebra Technologies 保持一致性 ID 改进库存和资产管理 成本 / 效益分析 ID ID ID (ERP) RFID Zebra Technologies 3 ID

More information

02 看 見 躍 動 的 創 新 力 量 04 矽 數 十 年 金 矽 創 意 十 年 有 成 16 築 夢 之 際 18 20 22 24 26 28 30 32 34 36 38 你 所 不 知 道 的 金 矽 獎 40 樂 在 其 中

02 看 見 躍 動 的 創 新 力 量 04 矽 數 十 年 金 矽 創 意 十 年 有 成 16 築 夢 之 際 18 20 22 24 26 28 30 32 34 36 38 你 所 不 知 道 的 金 矽 獎 40 樂 在 其 中 02 看 見 躍 動 的 創 新 力 量 04 矽 數 十 年 金 矽 創 意 十 年 有 成 16 築 夢 之 際 18 20 22 24 26 28 30 32 34 36 38 你 所 不 知 道 的 金 矽 獎 40 樂 在 其 中 我 們 相 信, 科 技 創 新 是 影 響 台 灣 競 爭 力 的 主 軸, 而 培 育 國 內 高 科 技 人 才, 正 是 金 矽 獎 創 辦 的 理 念

More information

エスポラージュ株式会社 住所 : 東京都江東区大島 東急ドエルアルス大島 HP: ******************* * 关于 Java 测试试题 ******

エスポラージュ株式会社 住所 : 東京都江東区大島 東急ドエルアルス大島 HP:  ******************* * 关于 Java 测试试题 ****** ******************* * 关于 Java 测试试题 ******************* 問 1 运行下面的程序, 选出一个正确的运行结果 public class Sample { public static void main(string[] args) { int[] test = { 1, 2, 3, 4, 5 ; for(int i = 1 ; i System.out.print(test[i]);

More information

器之 间 向一致时为正 相反时则为负 ③大量电荷的定向移动形成电 流 单个电荷的定向移动同样形成电流 3 电势与电势差 1 陈述概念 电场中某点处 电荷的电势能 E p 与电荷量 q Ep 的比值叫做该点处的电势 表达式为 V 电场中两点之间的 q 电势之差叫做电势差 表达式为 UAB V A VB 2 理解概念 电势差是电场中任意两点之间的电势之差 与参考点的选择无关 电势是反映电场能的性质的物理量

More information

作 業 系 統 簡 介 光 有 電 腦 硬 體, 會 不 容 易 使 用 必 須 要 有 適 當 的 程 式, 才 方 便 操 作 硬 體 衍 生 作 業 系 統 需 求 : 提 供 方 便 使 用 者 執 行 程 式 平 台 有 效 使 用 各 種 電 腦 硬 體 資 源 Jingo C. Lia

作 業 系 統 簡 介 光 有 電 腦 硬 體, 會 不 容 易 使 用 必 須 要 有 適 當 的 程 式, 才 方 便 操 作 硬 體 衍 生 作 業 系 統 需 求 : 提 供 方 便 使 用 者 執 行 程 式 平 台 有 效 使 用 各 種 電 腦 硬 體 資 源 Jingo C. Lia 第 三 章 作 業 系 統 概 論 Reporter : Jingo C. Liao 廖 正 宏 E-mail : [email protected] 章 節 列 表 1. 什 麼 是 作 業 系 統 2. 作 業 系 統 的 主 要 功 能 3. 作 業 系 統 的 核 心 程 式 4. 作 業 系 統 的 演 進 歷 史 5. 常 見 流 行 的 作 業 系 統 Jingo C. Liao

More information

<4D6963726F736F667420576F7264202D20C7B6C8EBCABDCFB5CDB3C9E8BCC6CAA6B0B8C0FDB5BCD1A75FD1F9D5C22E646F63>

<4D6963726F736F667420576F7264202D20C7B6C8EBCABDCFB5CDB3C9E8BCC6CAA6B0B8C0FDB5BCD1A75FD1F9D5C22E646F63> 因 为 路 过 你 的 路, 因 为 苦 过 你 的 苦, 所 以 快 乐 着 你 的 快 乐, 追 逐 着 你 的 追 逐 内 容 简 介 本 书 根 据 2005 年 下 半 年 实 施 的 全 国 计 算 机 技 术 与 软 件 专 业 技 术 资 格 ( 水 平 ) 考 试 嵌 入 式 系 统 设 计 师 级 考 试 大 纲 精 神, 在 深 入 研 究 历 年 计 算 机 技 术 与 软

More information

untitled

untitled http://www.embedded-soc.com/ J-LINK J-Link Rev2.1 http://www.embedded-soc.com/ 2007-11-11 http://www.embedded-soc.com/ J-LINK J-Link ARM JTAG J-LINK J-LINKJLINK J-FLASH ARM F.A.Q jlink GDBserver J-Flash

More information

201406002+大学计算机基础B.doc

201406002+大学计算机基础B.doc 目 录. 大 学 计 算 机 基 础 B( 非 独 立 设 课 ).... 计 算 机 操 作 基 础 ( 独 立 设 课 )...3 3. 程 序 设 计 基 础 ( 非 独 立 设 课 )...5 4. 面 向 对 象 程 序 设 计 ( 非 独 立 设 课 )...8 5. 数 据 库 原 理 ( 非 独 立 设 课 )...0 6. 算 法 设 计 与 分 析 ( 非 独 立 设 课 )...

More information

Microsoft PowerPoint - BECKHOFF技术_ADS通讯 [Compatibility Mode]

Microsoft PowerPoint - BECKHOFF技术_ADS通讯 [Compatibility Mode] 的架构 ADS 的通讯机制 ADS-Client Request -> Confirmation Indication

More information

HMI COM1 RS SIEMENSE S7-200 RS485

HMI COM1 RS SIEMENSE S7-200 RS485 目录 第一部分维控人机界面串口引脚定义...2 1 LEVI777T COM1 引脚定义原理图...2 2 LEVI777T COM2 引脚定义原理图...2 3 LEVI908T COM1 引脚定义原理图...2 4 LEVI908T COM2/COM3 引脚定义原理图...3 第二部分通信针头...4 1 通信针头...4 第三部分各 PLC 与 LEVI 通信线接法...5 1 西门子 S7-200

More information

第七章 中断

第七章 中断 嵌入式系统 SOPC(SOC),A case study [email protected] 内容提要 本讲的目的 : 介绍嵌入式系统 ; 了解 SOPC 的开发过程 现代计算机系统 嵌入式系统的应用 [email protected] 4/87 嵌入式控制系统的软 / 硬件框架 [email protected] 5/87 基于 FPGA 的嵌入式系统硬件平台 基于 FPGA 的嵌入式系统结构 FPGA

More information

数据库系统概论

数据库系统概论 所谓寻址方式, 就是指令中用于说明操 作数所在地或者所在地地址的方法 8088/8086 的寻址方式分为两类 : 关于寻找数据的寻址方式 关于寻找转移地址的寻址方式 下面讲关于数据的寻址方式时, 均以数 据传送指令 MOV 为例讲解 MOV 指令格式如下 : MOV DST, SRC 助记符 目的操作数 指令完成的功能 : (DST) 源操作数 (SRC) 一. 关于寻找数据的寻址方式 ( 共 8

More information

FPGAs in Next Generation Wireless Networks WPChinese

FPGAs in Next Generation Wireless Networks WPChinese FPGA 2010 3 Lattice Semiconductor 5555 Northeast Moore Ct. Hillsboro, Oregon 97124 USA Telephone: (503) 268-8000 www.latticesemi.com 1 FPGAs in Next Generation Wireless Networks GSM GSM-EDGE 384kbps CDMA2000

More information

L15 MIPS Assembly

L15 MIPS Assembly Lecture 19: MIPS Assembly Language 程序的机器级表示主要内容 MIPS 指令格式 R- 类型 / I- 类型 / J- 类型 MIPS 寄存器 长度 / 个数 / 功能分配 MIPS 操作数 寄存器操作数 / 存储器操作数 / 立即数 / 文本 / 位 MIPS 指令寻址方式 立即数寻址 / 寄存器寻址 / 相对寻址 / 伪直接寻址 / 偏移寻址 MIPS 指令类型

More information

PROTEUS VSM

PROTEUS  VSM Proteus VSM-- 1/1 PROTEUS VSM Proteus VSM ISIS Prospice VSM Proteus PROSPICE ARM7 PIC AVR HC11 8051 CPU LCD RS232 LED IAR Keil Hitech C make 6000 SPICE SPICE DLL SPICE3F5 14 FM PROTEUS PCB LED/LCD / 300

More information

目 录

目 录 1 Quick51...1 1.1 SmartSOPC Quick51...1 1.2 Quick51...1 1.3 Quick51...2 2 Keil C51 Quick51...4 2.1 Keil C51...4 2.2 Keil C51...4 2.3 1 Keil C51...4 2.4 Flash Magic...9 2.5 ISP...9 2.6...10 2.7 Keil C51...12

More information

* r p . 4 6 12 3 5 7 8 9bk bm btbsbrbqbp bo bn bl [ ] [ ] [ ] [ ] [SET] 1 2 3 4 5 6 7. cmcl ck 8 9 0 bk bl bm bn bo 1 2 1 2+ - bp bq 8 2 4 6 br r bs p bt ck cl cm 3 3 . 1 2 3 4 5 6 7 8 9 bk bl bm

More information

Autodesk Product Design Suite Standard 系统统需求 典型用户户和工作流 Autodesk Product Design Suite Standard 版本为为负责创建非凡凡产品的设计师师和工程师提供供基本方案设计和和制图工具, 以获得令人惊叹叹的产品

Autodesk Product Design Suite Standard 系统统需求 典型用户户和工作流 Autodesk Product Design Suite Standard 版本为为负责创建非凡凡产品的设计师师和工程师提供供基本方案设计和和制图工具, 以获得令人惊叹叹的产品 Autodesk Product Design Suite Standard 20122 系统统需求 典型用户户和工作流 Autodesk Product Design Suite Standard 版本为为负责创建非凡凡产品的设计师师和工程师提供供基本方案设计和和制图工具, 以获得令人惊叹叹的产品设计 Autodesk Product Design Suite Standard 版本包包括以下软件产产品

More information

Microsoft Word - 39.doc

Microsoft Word - 39.doc 摘 基 于 ARM 的 嵌 入 式 无 线 AP 的 设 计 杨 健 陈 永 泰 ( 武 汉 理 工 大 学 信 息 工 程 学 院, 武 汉 430070) 要 : 本 文 首 先 介 绍 了 无 线 AP 的 基 本 原 理, 然 后 重 点 描 述 基 于 AT76C510 的 无 线 AP 的 硬 件 设 计 及 嵌 入 式 系 统 uclinux 最 后 对 IEEE802.11b 的 安

More information

单片机应用编程技巧(专家:邓宏杰)

单片机应用编程技巧(专家:邓宏杰) 编 者 注 : 本 文 件 为 电 子 工 程 专 辑 网 站 编 辑 部 原 创, 电 子 工 程 专 辑 享 有 本 文 章 完 全 著 作 权, 如 需 转 载 该 文 章, 必 须 经 过 电 子 工 程 专 辑 网 站 编 辑 部 同 意 联 系 电 子 工 程 专 辑 网 站 编 辑 部, 请 发 信 至 [email protected] 单 片 机 应 用 编 程 技

More information

目录 1 H3C R4900 G2 服务器可选部件与操作系统兼容性列表 控制卡 GPU 卡 网卡 FC HBA 卡 TPM/TCM 模块 NVMe SSD PCle 加速卡 1-31 i

目录 1 H3C R4900 G2 服务器可选部件与操作系统兼容性列表 控制卡 GPU 卡 网卡 FC HBA 卡 TPM/TCM 模块 NVMe SSD PCle 加速卡 1-31 i 目录 1 H3C R4900 G2 服务器可选部件与操作系统兼容性列表 1-1 1.1 控制卡 1-1 1.2 GPU 卡 1-5 1.3 网卡 1-8 1.4 FC HBA 卡 1-21 1.5 TPM/TCM 模块 1-29 1.6 NVMe SSD PCle 加速卡 1-31 i 1 H3C R4900 G2 服务器可选部件与操作系统兼容性列表 本手册为产品通用资料 对于定制化产品, 请用户以产品实际情况为准

More information

嵌入式系统bootloader开发移植.ppt

嵌入式系统bootloader开发移植.ppt 嵌入式培训专家 嵌入式系统引导程序开发 www.farsight.com.cn Linux Market 今天的内容 v 体系结构开发与引导程序初始化 v 引导程序功能与内核加载 v 引导程序移植与体系结构 以上内容均以 arm 体系结构 u-boot 为例 嵌入式系统定义 v 嵌入式系统是以应用为中心, 以计算机技术为基础, 并且软硬件可裁剪, 适用于应用系统对功能 可靠性 成本 体积 功耗有严格要求的专用计算机系统

More information

HighPoint产品的FAQ手册

HighPoint产品的FAQ手册 一 引 言 首 先 承 蒙 贵 公 司 赐 顾, 使 用 HighPoint ( 简 称 HPT) 系 列 产 品 以 下 是 根 据 多 年 来 合 作 的 客 户 所 提 出 的 问 题 而 总 结 出 的 有 关 HighPoint 系 列 产 品 的 FAQ, 欢 迎 您 随 时 提 出 批 评 建 议 以 便 我 们 及 时 改 进 谢 谢! 二 HighPoint RAID 产 品 技

More information

untitled

untitled ATARM AT91SAM7S ARM Team Mcuzone http://www.mcuzone.com Rev1.0a 2006-11 Rev1.0a: 2006-11 QQ 8204136 13957118045 MSN [email protected] 1 ARM 2 AT91 3 ARM KEIL,IAR,ADS,RV,WINARM 4 ARM WIGGLER,MULTI-ICE,XLINK,ULINK

More information

Microsoft Word - 95年報.doc

Microsoft Word - 95年報.doc 股 票 代 號 :5351 95 年 度 年 報 中 華 民 國 九 十 六 年 五 月 十 五 日 刊 印 本 年 報 查 詢 網 址 :http://newmops.tse.com.tw http://www.etron.com.tw 一 公 司 發 言 人 代 理 發 言 人 姓 名 職 稱 電 話 及 電 子 郵 件 信 箱 發 言 人 代 理 發 言 人 姓 名 : 徐 初 發 郎 文 郁

More information

P4i45GL_GV-R50-CN.p65

P4i45GL_GV-R50-CN.p65 1 Main Advanced Security Power Boot Exit System Date System Time Floppy Drives IDE Devices BIOS Version Processor Type Processor Speed Cache Size Microcode Update Total Memory DDR1 DDR2 Dec 18 2003 Thu

More information

Microsoft PowerPoint - 微原-第3章2.ppt [兼容模式]

Microsoft PowerPoint - 微原-第3章2.ppt [兼容模式] 本教案内容 第 3 章 8086CPU 指令系统 1. 汇编语言指令 9. 转移指令 10. 2. 8086 指令分类循环控制指令 11. 子程序调用返回 3. 数据与转移地址的指令寻址方式 12. 中断调用返回指 4. 数据传送类指令令 5. 算术运算类指令 13. 字符串操作指令 6. 逻辑运算类指令 14. I/O 输入输出指令 7. 移位类指令 15. 其它指令 8. 标志位操作指令 16.

More information

(Microsoft Word - 92\246~\263\370)

(Microsoft Word - 92\246~\263\370) 壹 致 股 東 報 告 書 九 十 一 年 為 創 見 收 穫 頗 豐 之 年 度, 雖 產 業 環 境 仍 處 於 不 佳 狀 態, 但 在 創 見 公 司 全 體 同 仁 辛 勤 耕 耘 之 下, 我 們 仍 能 順 利 達 成 財 務 目 標 : 營 收 為 64.55 億 元, 較 九 十 年 度 47.16 億 元 大 幅 成 長 37% 稅 後 淨 利 10.82 億 元, 每 股 稅

More information

Abstract arm linux tool-chain root NET-Start! 2

Abstract arm linux tool-chain root NET-Start! 2 Lab III - Embedding Linux 1 Abstract arm linux tool-chain root NET-Start! 2 Part 1.4 Step1. tool-chain 4 Step2. PATH 4 Part 2 kernel 5 Step1. 5 Step2... 6 Step3...8 Part 3 root. 8 Step1. 8 Step2. 8 Part

More information

HP and Canon 单色通用芯片表 SCC 芯片 图片 HP 700 M712, 700 M725 CF214X (14X) 17.5 HP 5200 Q7516A U16-2CHIP SSS 846 芯片记号 (U16-2) Canon LBP-3500, LBP-3900, LBP-392

HP and Canon 单色通用芯片表 SCC 芯片 图片 HP 700 M712, 700 M725 CF214X (14X) 17.5 HP 5200 Q7516A U16-2CHIP SSS 846 芯片记号 (U16-2) Canon LBP-3500, LBP-3900, LBP-392 HP and Canon 单色通用芯片表在线访问我们的网站, 可以得到更多的信息 : www.scc-inc.com/chipcenter 全部开始都是专利通用芯片一个芯片, 多个不同型号的硒鼓 注意 : 当在这个文档上要寻找一个特殊的 或打印机的型号时, 在你的键盘上同时按 CTRL 键和 F 键就能搜索到 HP and Canon 单色通用芯片表 SCC 芯片 图片 HP 700 M712, 700

More information

第十四章 STC单片机比较器原理及实现

第十四章 STC单片机比较器原理及实现 第 14 章 STC 单片机比较器 原理及实现 何宾 2015.02 1 本章主要内容 STC 单片机比较器结构 STC 单片机比较器寄存器组 STC 单片机比较器应用 2 STC 单片机比较器结构 STC15W 系列单片机内置了模拟比较器 对于 STC15W201S STC15W404S, 以及 STC15W1K16S 系 列单片机的比较器内部结构 3 STC 单片机比较器结构 S T C 15W

More information

手册 doc

手册 doc 1. 2. 3. 3.1 3.2 3.3 SD 3.4 3.5 SD 3.6 3.7 4. 4.1 4.2 4.3 SD 4.4 5. 5.1 5.2 5.3 SD 6. 1. 1~3 ( ) 320x240~704x288 66 (2G SD 320x2401FPS ) 32M~2G SD SD SD SD 24V DC 3W( ) -10~70 10~90% 154x44x144mm 2. DVR106

More information

ATMEL AT90S8515 AVR CPU AVR AVR AVR ATMEL RISC 32 8 r0 r X Y Z R0 R1 R2 R13 R14 R15 R16 R17 R26 R27 R28 R29 R30 R31 0x00 0x

ATMEL AT90S8515 AVR CPU AVR AVR AVR ATMEL RISC 32 8 r0 r X Y Z R0 R1 R2 R13 R14 R15 R16 R17 R26 R27 R28 R29 R30 R31 0x00 0x 115 AVR W.V. Awdrey ATMEL AVR PIC AVR PIC AVR RISC AVR PIC AVR AVR AVR AVR AVR ATtiny15 AVR AVR AVR RAM ROM 121 116 122 ATMEL AT90S8515 AVR CPU AVR AVR AVR ATMEL RISC 32 8 r0 r31 3 16 X Y Z 6-1 118 7 0

More information

<4D6963726F736F667420576F7264202D20D5D0B9C9CBB5C3F7CAE9A3A8C9EAB1A8B8E5A3A92E646F63>

<4D6963726F736F667420576F7264202D20D5D0B9C9CBB5C3F7CAE9A3A8C9EAB1A8B8E5A3A92E646F63> 本 次 发 行 概 况 发 行 股 票 类 型 : 人 民 币 普 通 股 (A 股 ) 发 行 股 数 : 1,120 万 股 每 股 面 值 : 1.00 元 每 股 发 行 价 格 : [ ] 元 预 计 发 行 日 期 : [ ] 年 [ ] 月 [ ] 日 拟 上 市 的 证 券 交 易 所 : 发 行 后 总 股 本 : 深 圳 证 券 交 易 所 4,460 万 股 本 公 司 控 股

More information

第四章 102 图 4唱16 基于图像渲染的理论基础 三张拍摄图像以及它们投影到球面上生成的球面图像 拼图的圆心是相同的 而拼图是由球面图像上的弧线图像组成的 因此我 们称之为同心球拼图 如图 4唱18 所示 这些拼图中半径最大的是圆 Ck 最小的是圆 C0 设圆 Ck 的半径为 r 虚拟相机水平视域为 θ 有 r R sin θ 2 4畅11 由此可见 构造同心球拼图的过程实际上就是对投影图像中的弧线图像

More information

东南大学硕士学位论文 LCD 显示中灰度控制机理的研究及电路实现姓名 : 曹志香申请学位级别 : 硕士专业 : 微电子学与固体电子学指导教师 : 孙大有 20040327 LCD 显示中灰度控制机理的研究及电路实现 作者 : 曹志香 学位授予单位 : 东南大学 相似文献 (1 条 ) 1.

More information

DSP2000.ppt

DSP2000.ppt The success's road TI 2000 系列 DSP 开发应用 www.farsight.com.cn TI 2000 系列 DSP 开发应用 v1.tms320c2000 系列 DSP 介绍 v2.tms320c2000 系列 DSP 体系结构 v3.tms320c2000 系列 DSP 开发环境 v4.tms320c2000 系列 DSP 开发案例 1. TMS320C2000 系列

More information

Chapter #

Chapter # 第三章 TCP/IP 协议栈 本章目标 通过本章的学习, 您应该掌握以下内容 : 掌握 TCP/IP 分层模型 掌握 IP 协议原理 理解 OSI 和 TCP/IP 模型的区别和联系 TCP/IP 介绍 主机 主机 Internet TCP/IP 早期的协议族 全球范围 TCP/IP 协议栈 7 6 5 4 3 应用层表示层会话层传输层网络层 应用层 主机到主机层 Internet 层 2 1 数据链路层

More information

路由器基本配置

路由器基本配置 路由器基本配置 本章内容 路由器的基本操作 实验练习 常用的路由器配置方法 TFTP Console MODEM AUX telnet web 任何 Interface AUX 备份接口, 一般用于路由器的管理备份接口 路由器的操作模式 : 配置模式 1. 线路配置模式 Router(config-line)# 配置路由器的线路参数 2. 路由协议配置模式 Router(config-router)#

More information

PCM-3386用户手册.doc

PCM-3386用户手册.doc PCM-3386 BBPC-4x86 10/100M PC/104 (Lanry technology Co. Ltd. Zhuhai) 38 1012836 (Address: Room 1012,Linhai Building,No. 38,west of Shihua Road,Zhuhai City,Guangdong Province,China) (post code)519015 (phone)0756-3366659

More information

1. ( B ) IT (A) (B) (C) (D) 2. ( A ) (A) (B) (C) (D) 3. ( B ) (A) GPS (B) GIS (C) ETC (D) CAI 4. ( D ) (A) (B) (C) (D) 5. ( B ) (Stored Program) (A) H

1. ( B ) IT (A) (B) (C) (D) 2. ( A ) (A) (B) (C) (D) 3. ( B ) (A) GPS (B) GIS (C) ETC (D) CAI 4. ( D ) (A) (B) (C) (D) 5. ( B ) (Stored Program) (A) H ... 2... 4... 6... 8... 10... 12... 14... 16... 18... 20... 22... 24... 25... 26... 28 1. ( B ) IT (A) (B) (C) (D) 2. ( A ) (A) (B) (C) (D) 3. ( B ) (A) GPS (B) GIS (C) ETC (D) CAI 4. ( D ) (A) (B) (C)

More information

版权声明 龙芯 免责声明 据 龙芯 2 Building No.2, Loongson Industrial Park, Zhongguancun Environmental Protection Park (Tel) (Fax)

版权声明 龙芯 免责声明 据 龙芯 2 Building No.2, Loongson Industrial Park, Zhongguancun Environmental Protection Park (Tel) (Fax) 2018 9 29 龙芯 版权声明 龙芯 免责声明 据 龙芯 2 Building No.2, Loongson Industrial Park, Zhongguancun Environmental Protection Park (Tel) 010-62546668 (Fax) 010-62600826 阅读指南 龙芯 1C101 处理器数据手册 龙芯 1C101 修订历史 序号 更新日期 版本号

More information

Contents Viewpoint Application Story 05 News & Events 06 Technology Forum Customer Partnership Cover Story Advisory Board Inside Advantech Beautiful L

Contents Viewpoint Application Story 05 News & Events 06 Technology Forum Customer Partnership Cover Story Advisory Board Inside Advantech Beautiful L Summer 2016 No.35 IoT Your Gateway to lot Contents Viewpoint Application Story 05 News & Events 06 Technology Forum Customer Partnership Cover Story Advisory Board Inside Advantech Beautiful Life Joyful

More information

% 25% 1-1-1

% 25% 1-1-1 1400 1.00 5554.8 100% 25% 1-1-1 1-1-2 1 4154.80 1400 5554.8 1 2 5 25% 2 2006 3 4 [2000]25 2000 6 24 2010 17% 3% 2004 2005 2006 2007 1-6 5,617,891.04 6,960,795.01 9,749,964.27 3,959,433.83 37.37%35.09%

More information

ICD ICD ICD ICD ICD

ICD ICD ICD ICD ICD MPLAB ICD2 MPLAB ICD2 PIC MPLAB-IDE V6.0 ICD2 usb PC RS232 MPLAB IDE PC PC 2.0 5.5V LED EEDATA MPLAB ICD2 Microchip MPLAB-IDE v6.0 Windows 95/98 Windows NT Windows 2000 www.elc-mcu.com 1 ICD2...4 1.1 ICD2...4

More information

F515_CS_Book.book

F515_CS_Book.book /USB , ( ) / L R 1 > > > 2, / 3 L 1 > > > 2 + - 3, 4 L 1 了解显示屏上显示的图标 Wap 信箱收到一条 Wap push 信息 ( ) GSM 手机已连接到 GSM 网络 指示条越多, 接收质量越好 2 ...........................4.............................. 4 Micro SD (

More information

Next Generation Internet

Next Generation Internet workstation, PC, PDA ) 3C 1980 s Current Mainframe Computing Desktop Computing Ubiquitous Computing 14 1 2040 50 CPU 2 50 60 CPU FORTRAN 3 60 70 CPUSSI MSI, SSIMSI 4 2070 CPULSI VLSI, LSIVLSI ( ) 目标

More information

01

01 Zebra Technologies 白皮书 移动打印给仓储运营带来显著优势 综述 RFID RFID (RF) RFID RFID / ROI LAN 采用移动打印机, 享受显而易见的业务成效 - 49.74 28.11 Zebra 2 Zebra Technologies 移动打印机成本效益分析 示例数据固定式打印机移动打印机每年节省资金 10 10 8 8 48 48 3840 3840 15

More information

CH01.indd

CH01.indd 3D ios Android Windows 10 App Apple icloud Google Wi-Fi 4G 1 ( 3D ) 2 3 4 5 CPU / / 2 6 App UNIX OS X Windows Linux (ios Android Windows 8/8.1/10 BlackBerry OS) 7 ( ZigBee UWB) (IEEE 802.11/a/b/g/n/ad/ac

More information