61阅读

嵌入式linux驱动程序设计从入门到精通-Linux入门教程

发布时间:2017-12-31 所属栏目:酒精工厂生产车间设计

一 : Linux入门教程

Linux,在今天的广大电脑爱好者心中已经不再是那个遥不可及的新东西了,如果说几年前的Linux是星星之火的话,如今Linux不仅在服务器领域的应用取得较大进展,而且在桌面应用领域也有越来越多的人选择使用。Linux 的开放性和灵活性使它得以在实验室和其它研究机构中被用于创新性技术变革的前沿,现在Linux已经真正地向广大的电脑爱好者们敞开了大门。

只要你对Linux感兴趣,想要学习Linux,那么本教程将带你走进Linux 的世界。

第一章初识Linux

在学习使用之前我们还是先来了解一下Linux吧。

Linux是什么?按照Linux开发者的说法,Linux是一个遵循POSIX(标准操作系统界面)标准的免费操作系统,具有BSD和SYSV的扩展特性(表明其在外表和性能上同常见的UNIX非常相象,但是所有系统核心代码已经全部被重新编写了)。它的版权所有者是芬兰籍的Linus B. Torvalds 先生。

1991年8月这位来自芬兰赫尔辛基大学的年轻人Linus Benedict Torvalds,对外发布了一套全新的操作系统。

最开始的Linux版本是被放置到一个FTP服务器上供大家自由下载的,FTP服务器的管理员认为这是Linus的Minix,因而就建了一个Linux目录来存放这些文件,于是Linux这个名字就传开了,如今已经成了约定俗成的名称了。

下图就是Linux的吉祥物,一只可爱的小企鹅(起因是因为Linus是芬兰人,因而挑选企鹅作为吉祥物):

linux基础教程 Linux入门教程
Linux的吉祥物

闲话少叙进入正题。我们主要的学习方向有如下几点:

1.熟练掌握基本命令。每个系统都有自己特定的语言环境,Linux 也不例外,只有熟悉并熟练掌握Linux的常用基础命令才可以深入学习。

2.系统管理及运用。系统的管理包括启动、用户、进程以及安全管理等等。大体上都是通过命令来进行配置文件及脚本文件的。

3.源码的学习和研究。由于内核的相似,Linux同UNIX一样都是由C语言开发而成的,所以了解UNIX的朋友学习起来相对容易。

(www.61k.com。

4.内核开发。现在的很多服务器系统,网络设备,安全防护软件以及手机系统和掌上PDA的操作管理系统都是由Linux编程开发而成的,所以内核的开发学习当然必不可少。

5.数据库及服务器领域。如今Linux做的服务器在市场中占有率第一的位置无可动摇,其中包括:WWW服务器,FTP服务器,mail服务器,数据库服务器等等多种服务器。

了解了学习的目的和方向后,下面以Red Hat9.0为例来介绍Linux的安装过程。

第一步:设置电脑的第一启动驱动器为光盘驱动器,插入Linux系统光盘启动计算机。
第二步:系统会自动进入到Linux安装初始画面,第一要选择安装的方式,其中如果要选择文本界面安装需要在引导命令处输入命令linux text,如果要选择图形界面安装的话直接安回车Enter。笔者使用的是图形安装。

linux基础教程 Linux入门教程

第三步:选择完安装方式后便出现了光盘检测界面,出现这个对话框的意思就是在安装之前确定系统盘是否有损坏,如果确定没有损坏选择“Skip”直接跳过检测进入下个环节。如果选择“OK”则自动转到光盘检测程序自动检测光盘。对于初次接触Linux的朋友,还是建议您在安装之前先检测下系统安装光盘,省去在安装过程中所带来的不便。

linux基础教程 Linux入门教程

第四步:检测完光盘后会出现Linux的软件介绍说明以及选择系统语言的对话框,选择“简体中文”,当然如果你精通别的语言也是可以选择其他语言进行安装和使用的。

linux基础教程 Linux入门教程

第五步:键盘以及鼠标设置。在选项中提供了多种型号,品牌,接口和语言的键盘和鼠标,根据你现所用的键鼠进行对应选择。选择完毕后单击“下一步”

linux基础教程 Linux入门教程

配置鼠标

linux基础教程 Linux入门教程

第六步:安装类型。其中包括“个人桌面”,“工作站”,“服务器”,“定制”。四种类型名称不同,内容大同小异。由于篇幅所限这个会在日后的讲座中给大家详细介绍。

linux基础教程 Linux入门教程

第七步:磁盘分区设置。其中包括两个选项, “自动”和“手动”。自动分区会将所有的整个硬盘按照容量大小平均分区格式化,适合没有装任何资料的新电脑,但如果你在这之前装有其他系统,或是其他分区中存在的数据的话,建议您还是“手动分区”,这样不会丢失您原来的文件数据。

linux基础教程 Linux入门教程

第八步:新建分区。在图形界面下比较直观,一般都会显示出你硬盘的容量,厂商等相关信息。直接点击“新建”来创建新的分区。

linux基础教程 Linux入门教程

第九步:创建完新的分区之后,需要添加一个/boot分区(类似Windows的引导分区),类型为ext3,单击“确定”。

linux基础教程 Linux入门教程
 
 

第十步:再点“新建”创建一个swap文件系统(内存交换区)在“文件系统类型”中选择 “swap” 大小设置时,如果你的内存容量是512MB的那么就要设置成 512*2=1024 。大小要设成你内存大小的双倍,这一点要注意

linux基础教程 Linux入门教程

第十一步:建立一个Linux 下的根分区,挂载点处为“/”,大小根据硬盘分区实际大小自己意愿填写。

linux基础教程 Linux入门教程

第十二步:刚才上述的分区及设置是成功安装Linux必须的,将剩余硬盘分区的时候要注意分区路径。下图中的/mnt/linux 便为分区路径

linux基础教程 Linux入门教程

第十三步:设置完分区后进入下一步网络配置,点击“编辑”进入设置栏。与我们熟知的Windows类似,如果多台电脑在同一局域网下的话IP地址的最后以为只要不和别的电脑的IP地址重复就可以了。子网掩码也是255.255.255.0。

当然也可以在系统安装完毕后在图形界面下进入“系统工具,互联网配置向导”进行创建和配置。

linux基础教程 Linux入门教程

第十四步:防火墙配置。这里选择默认的就好,当然也可以选择“无防火墙”。如果设置成“高级”会限制大部分数据包,网页也经常会有打不开等现象。

linux基础教程 Linux入门教程

第十五步:配置完防火墙后会有系统语言以及当前时间的选择和配置,过程十分简单这里就多做介绍了。
第十六步:设置根命令。管理员拥有管理系统的最高权限,根命令其实就是管理员的管理密码。一旦设置,一定要将根命令记牢,否则就连最基本的系统界面都无法登陆。

linux基础教程 Linux入门教程

第十七步:选择软件包组。Linux给我们提供了多个现成的软件包,包括:窗口系统,桌面环境,文本编辑器,科学计算器,图形化文件管理器等多种应用程序。你需要什么软件包只要在其前面勾取即可。方便实用,功能强大。

linux基础教程 Linux入门教程

在随后的操作中直接点击“下一步”即可,直至将三张光盘安装完毕。

linux基础教程 Linux入门教程

点击“退出”后系统自动重启,随后便进入Linux的登陆画面。敲“回车”选择进入。

linux基础教程 Linux入门教程

下图为Linux图形登陆界面

linux基础教程 Linux入门教程

下图为Linux字符登陆型界面

linux基础教程 Linux入门教程

至此Red Hat9.0 Linux 操作系统的安装过程便全部结束

二 : 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

ARM嵌入式Linux系统开发从入门到精通

内容简介:

这是一本以实践为宗旨的嵌入式ARM Linux开发书籍,它不同于一般的教材重点讲述理论而缺乏实践的部分,也不同于许多类似书籍只针对特定开发板讲述,这对于没有开发板的读者来说很难掌握书中的内容。[www.61k.com]ARM是当今最主流的嵌入式微处理器,本书以应用最广泛的新一代ARM9处理器为讲述对象。此外,Linux是一个成熟而稳定的开放源代码操作系统,将Linux植入嵌入式设备具有众多的优点。本书分为三部分:第一部分讲述ARM Linux 系统移植,其中包括嵌入式系统开发入门,交叉编译器的构建,BootLoader的移植与实现以及Linux 2.6内核的编译与下载;第二部分讲述ARM Linux的驱动程序开发,其中包括最常见的字符设备驱动的分析,块设备驱动的分析以及网络设备驱动的分析。其中每一种类型的驱动都是利用典型的实例来讲述,使读者充分了解驱动程序的实现思想;第三部分讲述Qt GUI开发,其中包括Qt的具体安装,Qt的核心技术,以及最新的Qtopia Core开发环境,最后利用实例来讲述Qtopia Core开发过程。总之,本书包括了嵌入式Linux系统移植,底层驱动实例的讲解以及上层应用的实例讲述,针对那些想从事嵌入式开发或已经从事嵌入式开发的读者来说无疑是一本难得的参考书籍。

前言:

嵌入式系统由于芯片、软件、网络和传感器等技术的不断发展,正在成为未来社会的“数字基因”。如今,人类已经进入了后PC时代,嵌入式技术已被广泛应用于科学研究、工程设计、军事技术以及文艺、商业等方方面面,成为后PC时代的主力军。与此同时,嵌入式Linux操作系统也在嵌入式领域蓬勃发展,它不仅继承了Linux源码开放,内核稳定性强,软件丰富等特点,而且还支持几乎所有的主流处理器和硬件平台。嵌入式硬件系统和Linux系统的有机结合,成为后PC时代计算机最普遍的应用形式。嵌入式Linux技术在中国有巨大的发展潜力和市场需求。有数据显示,未来两年里,在计算机、消费电子、通信、汽车电子、工业控制和军事国防这六大主要应用领域,嵌入式Linux产品将达到80亿美元的市场规模,可见这个行业的前景是非常乐观的。当然,Linux嵌入式操作系统本身也有一定的局限性,就是开发难度过高,对于企业需要很高的技术实力。这就要求Linux系统厂商们不光要利用Linux,更要掌握Linux。此外,社会需要更多人加入到学习和使用Linux行业中来。

本书编写的目的:

嵌入式Linux属于一个交叉学科,并且也是一个高起点的学科,它涵盖了微电子技术、电子信息技术、计算机软件和硬件等多项技术领域的应用。另外学习嵌入式Linux最好具备相应的嵌入式开发板和软件,还需要有经验的人进行指导开发,目前国内大部分高校都很难达到这种要求,这也造成了目前国内嵌入式Linux开发人才极其缺乏的局面。

很多希望学习嵌入式Linux的人已经具备了一定的硬件知识,并且对操作系统原理,数据结构等都有相当的了解,但在Linux技术方面又是零起点。目前嵌入式Linux的书籍也是非常之多,但大部分都是要求读者有一定的Linux使用基础,对于初学者来说真的非常困难。写这本书的主要目的就是对那些没有Linux开发经验的初学者有个很好的指导参考作用,从而让他们少走弯路。

其次,笔者希望通过写书来总结这几年在工作中的项目经验,与更多的读者分享自己的技术,也是对自己的所做项目的一个巩固;通过写这本书,让笔者更加清楚了实践与理论之

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

间的联系,从而将自己的亲身经验和教训寄托在书中的每个章节。(www.61k.com]

本书的特点:

首先,本书涵盖了嵌入式Linux系统中最重要的三个部分:ARM Linux系统移植,ARM Linux驱动程序开发以及Qt GUI开发,这在同类书籍中比较少见。

其次,本书的讲述不依赖于具体某个厂家开发板,这样读者可以使用任意一款类似的开发板就可以进行实践学习,同时对于没有开发板的读者也可以学到更多的知识。

另外,本书提供了书中出现的所有实例的源代码,便于读者参考使用,更重要的是读者不用手动输入这些代码,从而节省时间。

本书的主要组成:

本书分为三个部分,共12章节,每一部分由4章内容组成。

第一部分讲述ARM Linux系统移植,首先第1章讲述嵌入式系统开发入门,主要针对初学者,讲述嵌入式系统的概要,ARM处理器,ADS工具,Linux开发环境,以及Linux内核源码等。接着第2章讲述交叉编译工具链的构建,主要讲述交叉工具链的作用,使用分步法构建交叉工具链和使用Crosstool工具构建交叉工具链。第3章讲述嵌入式系统的BootLoader,主要讲述嵌入式BootLoader的作用,基于S3C2410开发板的U-Boot分析与移植以及自己设计BootLoader的方法。最后第4章讲述嵌入式Linux内核移植,主要讲述移植的基本概念,内核配置、内核编译、内核下载以及构建根文件系统。

第二部分讲述ARM Linux驱动程序开发,首先第5章讲述ARM Linux驱动程序开发入门,主要讲述嵌入式Linux驱动介绍,简单的内核模块程序分析,以及Linux驱动开发的基本要点。接着第6章讲述字符设备驱动程序,主要讲述字符设备驱动相关的重要数据结构,字符设备驱动开发实例——触摸屏设备驱动开发。第7章讲述块设备驱动程序,主要讲述块设备相关的数据结构,块设备驱动开发实例——MMC/SD设备驱动开发。最后第8章讲述网络设备驱动程序,主要讲述网络设备驱动相关的重要数据结构,网络设备驱动开发实例——CS8900A网卡驱动开发。

第三部分讲述Qt GUI开发,首先第9章介绍了Qt的概要知识,包括Linux桌面GUI系统,Qt/X11,Qtopia Core等,使读者对Qt及其在Linux GUI系统中的作用有个大概了解。紧接着第10章讲述了Qt/X11的安装以及非常详细的应用实例,使读者可以轻松的编写基本的Qt程序。第11章深入讨论了一些Qt的核心技术,重点是以Qt对象模型为基础的信号和槽等机制,我们通过剖析Qt的源代码来深入的学习Qt的这些核心技术,同时也为读者今后对Qt源代码的自行研习打下基础。最后第12章重点讲述Qtopia Core和Qt/X11的一些不同之处,包括轻量级的窗口系统,QCOP进程间通信机制及调试工具qvfb等,使读者在熟悉了Qt/X11的基础上能够很快过渡到Qtopia Core开发。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

本书的读者对象:

本书通俗易懂,可作为高等院校电子类、电气类、控制类、计算机类等专业本科生、研究生学习嵌入式Linux的参考书目或自学教材,也可供广大希望转入嵌入式领域的科研和工程技术人员参考使用,还可作为广大嵌入式培训班的教材和教辅材料。

致谢:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

首先要感谢这本书的另外一位作者欧文盛,书中Qt GUI部分(第9章到第12章)主要由他来完成,由于他这几年一直在国际知名的通信公司从事Qt方面的开发工作,所以这部分由他来完成,出版社和我都很放心。(www.61k.com]其次,我要感谢我的妻子,很特殊的是我写这本书的时间正是我妻子怀孕的期间,其实在写这本书之前已经得知妻子怀孕,所以本想放弃编写这本书,但是妻子却很坚定的支持我写这本书。所以,我认为这本书的完成离不开她对我的默默支持。其次,要感谢我的岳父、岳母,是他们对我妻子这段时间的精心照顾,才使得我有更多的时间投入到写书中。

最后,要感谢威盛电子的李松,易宏宇,周志勇,张磊等,他们为本书的完成也提供了很多的帮助。

鉴于作者水平有限,加之时间仓促,本书一定有不少错误与不清楚之处,希望得到广大读者批评与指正。有兴趣的读者可以发送E-mail到lyf99526@yahoo.com.cn或登录笔者的个人Blog来做技术上的交流:。

作者

2007年3月28日

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

第一部分 ARM LINUX系统移植...............................................................................................12

第1章 嵌入式系统开发入门.........................................................................................................13

1.1嵌入式系统介绍....................................................................................................................13

1.1.1 嵌入式系统概述............................................................................................................13

1.1.2 嵌入式系统组成............................................................................................................15

1.2 ARM介绍..............................................................................................................................16

1.2.1 ARM处理器介绍............................................................................................................17

1.2.2 ARM处理器的选型........................................................................................................18

1.2.3 S3C2410微处理器介绍..................................................................................................18

1.3 ADS集成开发环境介绍........................................................................................................20

1.3.1 ADS软件组成.................................................................................................................21

1.3.1.1命令行开发工具......................................................................................................................21

1.3.1.2 GUI开发环境..........................................................................................................................23

1.3.1.3实用程序.................................................................................................................................23

1.3.1.4支持的软件.............................................................................................................................24

1.3.2使用Code Warrior IDE...................................................................................................24

1.3.2.1创建项目工程..........................................................................................................................24

1.3.2.2 编译和链接项目工程..............................................................................................................27

1.3.3使用AXD IDE.................................................................................................................29

1.3.3.1打开调试文件..........................................................................................................................29

1.3.3.2设置断点.................................................................................................................................30

1.3.3.3查看寄存器内容......................................................................................................................30

1.3.3.4查看变量值.............................................................................................................................31

1.4嵌入式LINUX开发介绍........................................................................................................32

1.4.1 Linux历史.......................................................................................................................32

1.4.2 Linux开发环境...............................................................................................................33

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

1.4.2.1 GCC介绍................................................................................................................................35

1.4.2.2 GNU Make介绍......................................................................................................................36

1.4.2.3 GDB介绍................................................................................................................................38

1.4.3 ARM Linux系统开发流程...............................................................................................41

1.5 LINUX内核介绍.....................................................................................................................43

1.5.1 Linux内核目录结构.......................................................................................................44

1.5.2 如何阅读Linux内核源代码..........................................................................................45

1.6 本章小节...............................................................................................................................47

1.7常见问题...............................................................................................................................48

第2章 交叉编译工具链的构建.....................................................................................................49

2.1 交叉编译工具链介绍............................................................................................................49

2.2 ARM LINUX交叉编译工具链的构建.....................................................................................49

2.2.1分步构建交叉编译链......................................................................................................50

2.2.1.1建立工作目录..........................................................................................................................50

2.2.1.2建立环境变量..........................................................................................................................51

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

2.2.1.3编译、安装Binutils.................................................................................................................51

2.2.1.4获得内核头文件......................................................................................................................52

2.2.1.5编译安装boot-trap gcc............................................................................................................53

2.2.1.6建立glibc库............................................................................................................................54

2.2.1.7编译安装完整的gcc................................................................................................................55

2.2.1.8测试交叉编译工具链..............................................................................................................55

2.2.2用Crosstool工具构建交叉工具链.................................................................................55

2.2.2.1准备资源文件..........................................................................................................................56

2.2.2.2建立脚本文件..........................................................................................................................56

2.2.2.3 建立配置文件.........................................................................................................................57

2.2.2.4 执行脚本................................................................................................................................57

2.2.2.5 添加环境变量.........................................................................................................................57

2.3本章小节...............................................................................................................................58

2.4常见问题...............................................................................................................................58

第3章 嵌入式系统的BOOTLOADER........................................................................................60

3.1 BOOTLOADER概述.................................................................................................................60

3.2常用的嵌入式LINUX BOOTLOADER.......................................................................................61

3.2.1 U-Boot.............................................................................................................................61

3.2.2 VIVI.................................................................................................................................61

3.2.3 Blob.................................................................................................................................62

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

3.2.4 RedBoot...........................................................................................................................62

3.2.5 ARMboot.........................................................................................................................63

3.2.6 DIY..................................................................................................................................63

3.3基于S3C2410开发板的BOOTLOADER实现.........................................................................63

3.3.1 S3C2410开发板介绍......................................................................................................63

3.3.2 U-Boot分析与移植.........................................................................................................66

3.3.2.1 U-Boot Stage1分析.................................................................................................................66

3.3.2.2 U-Boot Stage2分析.................................................................................................................71

3.3.2.3 U-Boot的移植过程.................................................................................................................72

3.4基于S3C2410开发板自己编写BOOTLOADER......................................................................88

3.4.1 设计系统的启动流程.....................................................................................................88

3.4.2 BootLoader的具体实现..................................................................................................90

3.4.2.1 设置异常向量表.....................................................................................................................91

3.4.2.2初始化看门狗和外围电路.......................................................................................................92

3.4.2.3初始化存储器..........................................................................................................................92

3.4.2.4初始化堆栈.............................................................................................................................93

3.4.2.5初始化数据区..........................................................................................................................94

3.4.2.6跳转到C程序Main函数........................................................................................................96

3.4.2.7 Main函数的具体实现.............................................................................................................96

3.5本章小节...............................................................................................................................97

3.6常见问题...............................................................................................................................97

第4章 嵌入式LINUX内核移植..................................................................................................98

4.1移植的基本概念....................................................................................................................98

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

4.2内核移植的准备....................................................................................................................99

4.3内核移植.............................................................................................................................100

4.3.1 内核配置......................................................................................................................100

4.3.1.1修改Makefile........................................................................................................................100

4.3.1.2设置NAND Flash分区.........................................................................................................101

4.3.1.3配置内核选项........................................................................................................................104

4.3.2 内核编译......................................................................................................................108

4.3.2.1清除冗余文件........................................................................................................................108

4.3.2.2编译内核映像和模块............................................................................................................108

4.3.2.3安装模块...............................................................................................................................109

4.3.3内核下载.......................................................................................................................109

4.4 建立LINUX根文件系统......................................................................................................110

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

4.4.1根文件系统的基本介绍................................................................................................110

4.4.1.1根文件系统的基本目录结构.................................................................................................110

4.4.1.2常见的根文件系统................................................................................................................111

4.4.1.3选择根文件系统....................................................................................................................112

4.4.2建立根文件系统...........................................................................................................113

4.4.2.1Cramfs工具包的使用.............................................................................................................113

4.2.2.2构建Cramfs根文件系统.......................................................................................................114

4.5本章小节.............................................................................................................................117

4.6常见问题.............................................................................................................................117 第二部分 ARM LINUX 设备驱动程序开发...............................................................................119

第5章 ARM LINUX驱动程序开发入门...................................................................................120

5.1嵌入式LINUX驱动程序介绍...............................................................................................120

5.1.1驱动程序的作用...........................................................................................................120

5.1.2 Linux设备驱动程序分类..............................................................................................121

5.2最简单的内核模块举例.......................................................................................................122

5.2.1 编写Hello World模块.................................................................................................122

5.2.2编写Hello World模块的Makefile................................................................................124

5.2.3加载和卸载Hello World模块.......................................................................................125

5.3 LINUX驱动程序开发要点....................................................................................................125

5.3.1 内存与I/O端口...........................................................................................................125

5.3.1.1内存.......................................................................................................................................126

5.3.1.2 I/O端口.................................................................................................................................129

5.3.2并发控制.......................................................................................................................130

5.3.2.1自旋锁(Spinlocks).............................................................................................................131

5.3.2.2信号量(Semaphores).........................................................................................................133

5.3.3阻塞(Blocking)与非阻塞(Nonblocking)...............................................................135

5.3.3.1阻塞(Blocking)与非阻塞(Nonblocking)操作................................................................135

5.3.3.2异步通知(Asynchronous Notification)...............................................................................135

5.3.4中断处理.......................................................................................................................136

5.3.4.1 Linux中断及其相关函数......................................................................................................136

5.3.4.2 ARM中断处理......................................................................................................................137

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

5.3.4.3一个Linux中断相关的实例..................................................................................................139

5.3.5 内核调试......................................................................................................................143

5.3.5.1准备内核调试环境................................................................................................................143

5.3.5.2 KDB的基本用法...................................................................................................................144

5.4本章小结.............................................................................................................................146

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

5.5常见问题.............................................................................................................................147

第6章 字符设备驱动程序...........................................................................................................148

6.1 字符设备驱动介绍..............................................................................................................148

6.1.1字符设备驱动相关的重要结构.....................................................................................148

6.1.1.1 file_operations(文件操作)结构..........................................................................................148

6.1.1.2 file(文件)结构...................................................................................................................151

6.1.1.3 inode(节点)结构...............................................................................................................152

6.1.2主、次设备号...............................................................................................................154

6.1.2.1主、次设备号的内部表示.....................................................................................................155

6.1.2.2静态分配和释放主设备号.....................................................................................................155

6.1.2.3 动态分配主设备号...............................................................................................................156

6.2 字符设备驱动开发实例......................................................................................................157

6.2.1四线电阻式触摸屏原理................................................................................................157

6.2.2 S3C2410触摸屏工作原理............................................................................................158

6.2.3 S3C2410的ADC和触摸屏接口特殊寄存器................................................................159

6.2.3.1 ADC控制(ADCCON)寄存器...........................................................................................159

6.2.3.2 ADC 触摸屏控制(ADCTSC)寄存器................................................................................160

6.2.3.3 ADC开始延迟(ADCDLY)寄存器....................................................................................161

6.2.3.4 ADC 转化数据 (ADCDAT0) 寄存器...................................................................................161

6.2.3.5 ADC转化数据(ADCDAT1)寄存器........................................................................................162

6.2.4 触摸屏驱动概要设计...................................................................................................162

6.2.4.1触摸屏硬件接口....................................................................................................................162

6.2.4.2触摸屏驱动程序流程设计.....................................................................................................163

6.2.5触摸屏驱动程序分析....................................................................................................164

6.2.5.1触摸屏设备初始化................................................................................................................165

6.2.5.2触摸屏设备文件操作............................................................................................................168

6.2.5.3 open和release方法..............................................................................................................168

6.2.5.4 read和poll方法....................................................................................................................169

6.2.5.5 触摸屏中断和ADC中断的实现..........................................................................................170

6.2.6配置和编译驱动程序....................................................................................................172

6.2.7测试触摸屏驱动程序....................................................................................................173

6.2.8触摸屏的校准...............................................................................................................174

6.3本章小节.............................................................................................................................175

6.4常见问题.............................................................................................................................176

第7章 块设备驱动程序..............................................................................................................177

7.1块设备驱动介绍..................................................................................................................177

7.1.1块设备驱动相关的重要结构........................................................................................177

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

7.1.1.1block_device_operations(块设备操作)结构........................................................................177

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

7.1.1.2 gendisk结构..........................................................................................................................178

7.1.1.3 request结构...........................................................................................................................180

7.1.2请求处理.......................................................................................................................182

7.1.2.1 request函数...........................................................................................................................182

7.1.2.2 request函数实例...................................................................................................................182

7.2块设备驱动开发实例...........................................................................................................183

7.2.1 MMC/SD介绍...............................................................................................................184

7.2.2 S3C2410提供的SDI接口............................................................................................186

7.2.3 SDI相关的寄存器........................................................................................................187

7.2.3.1 SDI控制(SDICON)寄存器...............................................................................................188

7.2.3.2 SDI波特率预定标(SDIPRE)寄存器.................................................................................188

7.2.3.3 SDI命令参数(SDICARG)寄存器..........................................................................................188

7.2.3.4 SDI命令控制(SDICCON)寄存器..........................................................................................189

7.2.3.5 SDI命令状态(SDICSTA)寄存器...........................................................................................189

7.2.3.6 SDI响应(SDIRSP)寄存器.....................................................................................................189

7.2.3.7 SDI数据/占用定时器(SDIDTIMER)寄存器..........................................................................190

7.2.3.8 SDI块大小(SDIBSIZE)寄存器..............................................................................................190

7.2.4 MMC/SD驱动概要设计................................................................................................191

7.2.4.1 MMC/SD与主机的接口连接................................................................................................191

7.2.4.2 MMC/SD驱动框架...............................................................................................................191

7.2.4.3 MMC驱动的核心设计..........................................................................................................193

7.2.5 MMC驱动程序分析.....................................................................................................193

7.2.5.1 MMC初始化.........................................................................................................................193

7.2.5.2 open和release方法..............................................................................................................195

7.2.5.3 ioctl方法...............................................................................................................................196

7.2.5.4 MMC驱动的request方法.....................................................................................................196

7.2.6 S3C2410 SDI接口驱动分析.........................................................................................198

7.2.6.1 SDI初始化............................................................................................................................199

7.2.6.2 SDI接口驱动方法.................................................................................................................199

7.2.7配置和编译驱动程序....................................................................................................200

7.3本章小结.............................................................................................................................200

7.4常见问题.............................................................................................................................200

第8章 网络设备驱动程序...........................................................................................................202

8.1网络设备驱动介绍..............................................................................................................202

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

8.1.1 网络设备驱动相关的重要结构....................................................................................202

8.1.1.1 net_device结构......................................................................................................................202

8.1.1.2 sk_buff结构..........................................................................................................................204

8.1.2常见的网络术语...........................................................................................................205

8.1.2.1常见的网络协议....................................................................................................................205

8.1.2.2以太网介绍...........................................................................................................................206

8.2网络设备驱动开发实例.......................................................................................................207

8.2.1CS8900A介绍................................................................................................................207

8.2.1.1CS8900A的组成部分介绍.....................................................................................................207

8.2.1.2 CS8900A的系统应用............................................................................................................208

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

8.2.2CS8900A网卡驱动概要设计.........................................................................................209

8.2.2.1 CS8900A网卡接口...............................................................................................................209

8.2.2.2网络驱动程序的体系结构.....................................................................................................209

8.2.2.3网络驱动程序的主要功能.....................................................................................................210

8.2.3 CS8900A适配器驱动程序分析....................................................................................211

8.2.3.1初始化...................................................................................................................................211

8.2.3.2 open和stop方法...................................................................................................................214

8.2.3.3数据发送...............................................................................................................................216

8.2.3.4数据接收...............................................................................................................................217

8.3本章小结.............................................................................................................................220

8.4常见问题.............................................................................................................................220 第三部分 QT GUI开发...............................................................................................................221

第9章 QT概述...........................................................................................................................222

9.1 LINUX下GUI介绍..............................................................................................................222

9.1.1 Linux桌面GUI系统....................................................................................................222

9.1.1.1 X Window系统.....................................................................................................................223

9.1.1.2 GNOME/Gtk+和KDE/Qt......................................................................................................224

9.1.2 嵌入式Linux下的GUI系统.......................................................................................226

9.2 QT/X11介绍........................................................................................................................227

9.2.1 Qt的历史和Qt/X11的由来..........................................................................................227

9.2.2 Qt/X11的版权问题.......................................................................................................228

9.2.3 Qt/X11及Qt/Windows的系统架构图对比....................................................................228

9.2.4 Qt的特性简介..............................................................................................................228

9.3 QTOPIA CORE 介绍...............................................................................................................229

9.3.1 Qtopia Core与Qt/Embedded........................................................................................229

9.3.2 Qtopia Core的体系结构...............................................................................................230

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

9.3.2.1 Frame Buffer(帧缓存)简介.....................................................................................................230

9.3.2.2 Qtopia Core的窗口系统........................................................................................................231

9.4 本章小结.............................................................................................................................231

9.5常见问题.............................................................................................................................231

第10章 QT/X11初步..................................................................................................................233 10.1 QT/X11的安装...................................................................................................................233 10.1.1 Qt/X11的下载及双重授权问题的说明.......................................................................233 10.1.2 Qt/X11的安装详解.....................................................................................................234 10.2 QT下的HELLO WORLD......................................................................................................235 10.3 温度转换的小例子............................................................................................................237 10.3.1 背景知识....................................................................................................................237 10.3.2 Quit按钮.....................................................................................................................237 10.3.3摄氏温度的显示.........................................................................................................241 10.3.4 华氏温度的显示........................................................................................................243 10.3.5 华氏温度和摄氏温度之间的转换..............................................................................247 10.3.6 保存当前的数值........................................................................................................251

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

10.4 本章小结...........................................................................................................................256 10.5 常见问题...........................................................................................................................257

第11章 QT核心技术..................................................................................................................258 11.1信号(SIGNALS)和槽(SLOTS)................................................................................................258 11.1.1 常见的GUI组件通信方式........................................................................................258 11.1.1.1 回调函数.............................................................................................................................258 11.1.1.2 面向对象的回调.................................................................................................................260 11.1.2 Qt中的信号和槽(Signals and Slots)............................................................................261 11.1.2.1 信号和槽历史和所带来的优点...........................................................................................261 11.1.2.2 信号....................................................................................................................................261 11.1.2.3 槽........................................................................................................................................262 11.1.2.4 信号和槽的效率.................................................................................................................262 11.1.3 自定义信号和槽的小例子..........................................................................................263 11.2 QT对象模型.......................................................................................................................266 11.2.1 元对象系统(Meta-Object System)..........................................................................266 11.2.2 信号和槽机制的实现.................................................................................................272 11.2.2.1 用connection()建立连接.....................................................................................................272 11.2.2.2 信号的发射和槽的执行......................................................................................................278 11.2.3 元对象编译器moc.....................................................................................................282 11.2.3.1 在Makefile中使用moc......................................................................................................282 11.2.3.2 moc用法详解......................................................................................................................282 11.2.3.3 moc及信号和槽机制的局限性............................................................................................283 11.3 QT的窗口系统...................................................................................................................285 11.3.1 窗口部件之间的树型结构..........................................................................................285 11.3.2 窗口部件的布局管理(Layout)...............................................................................288 11.4 国际化...............................................................................................................................291 11.4.1 Qt国际化的基本步骤.................................................................................................291 11.4.1.1 程序员的工作.....................................................................................................................291 11.4.1.2 语言资源管理者和翻译工作者的工作................................................................................292 11.4.2 动态改变语言的小例子.............................................................................................292 11.4.3 一些注意事项............................................................................................................298 11.5 本章小结...........................................................................................................................299 11.6 常见问题...........................................................................................................................299

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

第12章QTOPIA CORE.............................................................................................................301 12.1 QTOPIA CORE的安装..........................................................................................................301 12.2 FRAME BUFFER和QVFB......................................................................................................302 12.2.1 Frame Buffer...............................................................................................................302 12.2.2 编译qvfb....................................................................................................................304 12.2.3 在qvfb上运行Qtopia Core程序...............................................................................305 # ./ DIGITALCLOCK –QWS –DISPLAY QVFB:0.....................................................................306 12.3 移植QT/X11程序到QTOPIA CORE中...............................................................................307 12.4轻量级的窗口系统............................................................................................................309

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

12.5 进程间通信.......................................................................................................................312 12.6 本章小结...........................................................................................................................315 12.7 常见问题...........................................................................................................................316 附录A:光盘内容....................................................................................................................317 附录B:参考文献....................................................................................................................317

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

第一部分 ARM Linux系统移植

为了能让读者快速的了解嵌入式ARM Linux系统的开发过程,本书第一部分讲述ARM Linux的系统移植,这部分内容在实际工作中比较常见,是嵌入式Linux开发人员应该掌握的技能。(www.61k.com)该部分由四章组成:第一章介绍ARM嵌入式Linux系统概述,作为嵌入式开发入门的一章,非常适合初学者阅读,它包括嵌入式系统介绍,ARM介绍,ADS集成开发工具介绍,嵌入式Linux开发介绍以及Linux内核介绍,对那些刚接触嵌入式Linux开发的读者来说,通过本章学习将会对嵌入式Linux开发有个大概的了解和认识;第二章介绍交叉编译工具链的制作,对于非X86硬件平台的设备开发通常使用交叉编译工具链在X86机器上进行,该章内容是编译目标内核和程序的基础,它包括对交叉工具链的介绍,使用分步法构建交叉工具链和使用Crosstool构建交叉工具链,通过本章学习,读者将会对交叉工具链有深刻的认识以及可以构建自己的交叉工具链;第三章讲述ARM Linux的引导程序——BootLoader,这是内核移植的关键,没有一个良好的BootLoader来引导内核工作,再强大、稳定的内核也不能正常工作,它包括对BootLoader的介绍,U-boot移植与分析以及讲述如何自己设计BootLoader。通过本章学习,读者将会对BootLoader的作用有更清楚的认识,以及学会如何移植和设计BootLoader;第四章讲述嵌入式Linux内核移植,也是实际工作中非常重要的内容,它包括移植的基本概念,内核配置,内核编译,以及根文件系统的构建。通过本章学习,读者将会对内核移植以及构建根文件系统有更深入的理解。

总之,读者通过对这部分内容的学习,将会了解构建嵌入式Linux系统所需要的一些工作,比如交叉工具链,BootLoader,内核配置,内核编译等。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

第1章 嵌入式系统开发入门

本章学习目标:

l 了解嵌入式系统基本概念

l 了解嵌入式系统的基本组成

l 了解ARM处理器的特点

l 学会使用ADS集成开发工具(Code Warrior和AXD)

l 熟悉Linux开发环境

l 学会如何有效阅读Linux内核代码

1.1嵌入式系统介绍

本章作为ARM Linux系统移植的第一章,也是本书的第一章。(www.61k.com)俗话说说的好“良好的开始是成功的一半”,虽然这句话并不是真理,但是希望读者在学习任何东西之前都应该有坚定的学习态度和持之以恒的信念,同样学习本书也要有个良好的开端。首先介绍嵌入式系统的概述。

1.1.1 嵌入式系统概述

随着嵌入式系统在消费类电子、工业控制、航空航天、汽车电子、医疗保健、网络通信等各个领域的广泛应用,嵌入式系统这个名词已经被各行各业的人所熟悉, 嵌入式系统已经走进了人们的生活。它正在以各种不同的形式悄悄地改变着人们的生产、生活方式。无庸质疑,社会对嵌入式系统开发人员的需求也越来越大,所以现在越来越多的人已经加入到这个行业中来。嵌入式系统,英文为Embedded System,从广义上讲,凡是带有微处理器的专用软、硬件系统都可称为嵌入式系统。如各类单片机和DSP系统,这些系统在完成较为单一的专业功能时具有简洁高效的特点。但是由于他们没有使用操作系统,所以管理系统硬件和软件的能力有限,在实现复杂的多任务功能时往往困难重重,甚至无法实现。从狭义上讲,那些使用嵌入式微处理器构成的独立系统,并且有自己的操作系统,具有特定功能,用于特定场合的系统。本书中所说的嵌入式系统是指狭义上的嵌入式系统。到目前为止,对于嵌入式系统还没有一个明确的定义。嵌入式系统的核心是嵌入式微处理器,该处理器都是RISC(Reduce Instruction Set Computing,精简指令集计算机)*(注1)的处理器内核。 *注1:RISC和CISC(Complex Instruction Set Computing,复杂指令集计算机)是当前CPU的两种架构。它们的区别在于不同的CPU设计理念和方法。早期的CPU全部是CISC架构,它的设计目的是要用最少的机器语言指令来完成所需的计算任务。比如对于乘法运算,在CISC架构的CPU上,您可能需要这样一条指令:MUL ADDRA, ADDRB就可以将ADDRA和ADDRB中的数相乘并将结果储存在ADDRA中。将ADDRA, ADDRB中的数据读入寄存器,相乘和将结果写回内存的操作全部依赖于CPU中设计的逻辑来实现。这种架构会增加CPU结构的复杂性和对CPU工艺的要求,但对于编译器的开发十分有利。比如C程序中的a*=b就可以直接编译为一条乘法指令。今天只有Intel及其兼容CPU还在使用CISC架构。RISC架构要求软件来指定各个操作步骤。上面的例子如果要在RISC架构上实现,将ADDRA, ADDRB中的数据读入寄存器,相乘和将结果写回内存的操作都必须由软件来实现,比如:MOV A, ADDRA; MOV B, ADDRB;

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

MUL A, B; STR ADDRA, A。[www.61k.com)这种架构可以降低CPU的复杂性以及允许在同样的工艺水平下生产出功能更强大的CPU,但对于编译器的设计有更高的要求。

嵌入式微处理器一般具备以下4个主要特点:

1. 对实时多任务有很强的支持能力,能完成多任务并且有较短的中断响应时间,从而

使内部的代码和实时内核的执行时间减少到最低限度。

2. 具有功能很强的存储区保护功能。这是由于嵌入式系统的软件结构已模块化,而为

了避免在软件模块之间出现错误的交叉作用,需要设计强大的存储区保护功能,同时也有利于软件诊断。

3. 可扩展的处理器结构,以能最迅速地开展出满足应用的最高性能的嵌入式微处理

器。

4. 嵌入式微处理器必须功耗很低,尤其是用于便携式的无线及移动的计算和通信设备

中靠电池供电的嵌入式系统更是如此。

嵌入式处理器内核按照体系结构分类:

1. MIPS处理器

MIPS处理器是由美国MIPS公司研发出来的一套处理器体系,MIPS公司是一家设计制造高性能、高档次及嵌入式32位和64位处理器的厂商,在RISC处理器方面占有重要地位。1984年,MIPS计算机公司成立。1992年,SGI收购了MIPS计算机公司。1998年,MIPS脱离SGI,成为MIPS技术公司。MIPS公司设计RISC处理器始于二十世纪八十年代初,1986年推出R2000处理器,1988年推R3000处理器,1991年推出第一款64位商用微处器R4000。之后又陆续推出R8000(于1994年)、R10000(于1996年)和R12000(于1997年)等型号。随后,MIPS公司的战略发生变化,把重点放在嵌入式系统。1999年,MIPS公司发布MIPS32和MIPS64架构标准,为未来MIPS处理器的开发奠定了基础。新的架构集成了所有原来NIPS指令集,并且增加了许多更强大的功能。MIPS公司陆续开发了高性能、低功耗的32位处理器内核(core)MIPS324Kc与高性能64位处理器内核MIPS64 5Kc。2000年,MIPS公司发布了针对MIPS32 4Kc的版本以及64位MIPS 64 20Kc处理器内核。

2. ARM处理器

ARM(Advanced RISC Machines)处理器是由只设计内核的英国ARM公司研发出来的一套处理器体系,ARM公司成立于1990年11月,其前身是Acorn计算机公司。ARM是微处理器行业的一家知名企业,设计了大量高性能、廉价、耗能低的RISC处理器、相关技术及软件。技术具有性能高、成本低和能耗省的特点。适用于多种领域,比如嵌入控制、消费/教育类多媒体、DSP和移动式应用等。ARM将其技术授权给世界上许多著名的半导体、软件和OEM厂商,每个厂商得到的都是一套独一无二的ARM相关技术及服务。利用这种合伙关系,ARM很快成为许多全球性RISC标准的缔造者。目前,总共有30家半导体公司与ARM签订了硬件技术使用许可协议,其中包括Intel、IBM、三星电子、LG半导体、NEC、SONY、菲利浦和国民半导体这样的大公司。至于软件系统的合伙人,则包括微软、升阳和MRI等一系列知名公司。ARM架构是面向低预算市场设计的第一款RISC微处理器。

3. PowerPC处理器

二十世纪九十年代,IBM(国际商用机器公司)、Apple(苹果公司)和Motorola(摩托罗拉)公司开发PowerPC芯片成功,并制造出基于PowerPC的多处理器计算机。PowerPC架构的特点是可伸缩性好、方便灵活。第一代PowerPC采用0.6微米的生产工艺,晶体管的集成度达到单芯片300万个。MPC860和MPC8260是其最经典的两款PowerPC内核的嵌入式处理器。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

4. 68K/ColdFire处理器

68K/ColdFire处理器是Motorola公司独有的处理器体系。(www.61k.com]68K内核是最早在嵌入式领域广泛应用的内核。其最著名的代表芯片是68360。Coldfire继承了68K的特点并继续兼容它。

由于嵌入式系统一般具有芯片集成度高,软件代码小,高度自动化,响应速度快等特点,特别适合于要求实时性和多任务的体系。RTOS(Real-Time Operating System,实时操作系统)是根据操作系统的工作特性而言的。实时是指物理进程的真实时间。实时操作系统是指具有实时性,能支持实时控制系统工作的操作系统。首要任务是调度一切可利用的资源完成实时控制任务,其次才着眼于提高计算机系统的使用效率,重要特点是要满足对时间的限制和要求。一般Windows、Unix、Linux等桌面系统都属于分时操作系统,在此有必要说明一下实时操作系统与分时操作系统的区别:具体的说,对于分时操作系统,软件的执行在时间上的要求并不严格,时间上的错误,一般不会造成灾难性的后果。而对于实时操作系统,主要任务是对事件进行实时的处理,虽然事件可能在无法预知的时刻到达,但是软件上必须在事件发生时能够在严格的时限内作出响应,即使是在尖峰负荷下,也应该如此,系统时间响应的超时就意味着致命的失败。另外,实时操作系统的重要特点是具有系统的可确定性,即系统能对运行情况的最好和最坏等的情况能做出精确的估计。

到此为止,读者应该对嵌入式系统有了大概的了解,下面将介绍嵌入式系统的一般组成。

1.1.2 嵌入式系统组成

嵌入式系统一般由硬件平台和软件平台两部分组成,如下图1.1所示。其中硬件平台由嵌入式微处理器和外围硬件设备组成,而软件平台由嵌入式操作系统和应用软件组成。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图1.1 嵌入式系统的一般架构

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

随着芯片技术的不断发展,嵌入式处理器的主频也越来越高,通常主频都在40M Hz以上,有的甚至高达500MHz。[www.61k.com]多处理器、多核处理器平台也逐渐应用在嵌入式领域,不过现在大量使用的还是32位单处理器组成的平台。一个典型的硬件平台如图1.2所示[2]。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

图1.2 嵌入式硬件平台基本组成结构

嵌入式软件平台主要由嵌入式操作系统与应用软件组成。目前流行的嵌入式操作系统可以分为两类:一类是从运行在个人电脑上的操作系统向下移植到嵌入式系统中,形成的嵌入式操作系统,如微软公司的Windows CE,SUN 公司的Java 系统,朗讯科技公司的Inferno,嵌入式Linux 等。这类系统经过个人电脑或高性能计算机等产品的长期运行考验,技术日趋成熟,其相关的标准和软件开发方式已被用户普遍接受,同时积累了丰富的开发工具和应用软件资源。另一类是实时操作系统,如WindRiver 公司的VxWorks,ISI 的pSOS,QNX 系统软件公司的QNX,ATI 的Nucleus,中国科学院凯思集团的Hopen 嵌入式操作系统等,这类产品在操作系统的结构和实现上都针对所面向的应用领域,对实时性高可靠性等进行了精巧的设计,而且提供了独立而完备的系统开发和测试工具,较多地应用在军用产品和工业控制等领域中。目前常见的嵌入式系统有:Linux、uClinux、WinCE、PalmOS、Symbian、eCos、uCOS-II、VxWorks、pSOS、Nucleus、ThreadX 、Rtems 、QNX、INTEGRITY、OSE、C Executive等。嵌入式操作系统的发展也必将带动新一轮科技竞争。

应用程序运行在嵌入式操作系统之上,一般情况下应用程序和操作系统是分开的。当处理器上带有MMU(Memory Management Unit,存储器管理单元),它可以从硬件上将应用程序和操作系统分开编译和管理,Linux、WinCE就是这种分离机制。这样做的好处就是系统安全性更高,可维护性更强,更有利于各功能模块的划分。很多情况下在没有MMU的处理器,如ARM7TDMI,经常应用程序和操作系统是编译在一起运行的,对于开发人员来说,操作系统更像一个函数库。

1.2 ARM介绍

ARM是Advanced RISC Machines(高级精简指令系统处理器)的缩写,它既是一种微

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

处理器知识产权(IP)核,也是一个公司的名称。[www.61k.com]在上节中对ARM公司有了大概的介绍,这里就不再赘述,以下将介绍ARM处理器。

1.2.1 ARM处理器介绍

ARM处理器已经成功广泛地应用于无线通信、工业控制、消费类电子、网络产品等领域,并且保持持续增长的势头。目前,基于ARM 技术的微处理器应用约占据了32位RISC 微处理器75%以上的市场份额。采用RISC架构的ARM微处理器一般具有如下特点:

1. 体积小、低功耗、低成本、高性能;

2. 支持Thumb(16位)/ARM(32位)双指令集,能很好的兼容8位/16位器件;

3. 大量使用寄存器,指令执行速度更快;

4. 大多数数据操作都在寄存器中完成;

5. 寻址方式灵活简单,执行效率高;

6. 指令长度固定。

ARM微处理器目前包括下面几个系列,每一个系列的ARM微处理器都有各自的特点和应用领域。

2 ARM7系列:一般包括ARM7TDMI、ARM7TDMI-S、ARM720T、ARM7EJ

几种内核。ARM7TDMI是目前使用最广泛的32位嵌入式RISC处理器之一,

主要应用工业控制、Internet设备、网络和调制解调器设备、移动电话等多种

多媒体和嵌入式应用。TDMI的基本含义为:

T: 支持16为压缩指令集Thumb;

D: 支持片上Debug;

M:内嵌硬件乘法器(Multiplier)

I: 嵌入式ICE,支持片上断点和调试点

2 ARM9系列:包含ARM920T、ARM922T和ARM940T三种类型,主要应用

于无线设备、仪器仪表、安全系统、机顶盒、高端打印机、数字照相机和数字

摄像机等。其中本书中介绍的S3C2410就是ARM9系列的ARM920T类型。

ARM9具有以下特点:

l 5级流水线,指令执行效率更高。

l 提供1.1MIPS/MHz的哈佛结构。

l 支持32位元ARM指令集和16位元Thumb指令集。

l 支持32位元的高速AMBA汇流排界面。

l 全性能的MMU,支持Windows CE、Linux、Palm OS等多种主流嵌

入式操作系统。

l 支持数据Cache和指令Cache,具有更高的指令和数据处理能力。

2 ARM9E系列:包含ARM926EJ-S、ARM946E-S和ARM966E-S三种类型。主

要应用于下一代无线设备、数字消费品、成像设备、工业控制、存储设备和网

络设备等领域。

2 ARM10E系列:包含ARM1020E、ARM1022E和ARM1026EJ-S三种类型。

主要应用于下一代无线设备、数字消费品、成像设备、工业控制、通信和信息

系统等领域。

2 SecurCore系列:包含SecurCore SC100、SecurCore SC110、SecurCore SC200

和SecurCore SC210四种类型,主要应用于一些对安全性要求较高的应用产品

及应用系统,如电子商务、电子政务、电子银行业务、网络和认证系统等领域。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

2 Intel的Xscale:Xscale 处理器是基于ARMv5TE架构的解決方案,是一款全

性能、高成本效益比、低功耗的处理器。(www.61k.com)它支持16位的Thumb指令和DSP

指令集,已使用在许多移动电话、个人数字助理和网络产品等场合。

2 Intel的StrongARM:StrongARM SA-1100处理器是采用ARM架构高度整合的

32位元RISC微处理器。它融合了Intel公司的设计和处理技术以及ARM架

构的电源效率,采用在软件上相容ARMv4架构、同時采用具有Intel技术优

点的架构。Intel StrongARM处理器是便携型通讯产品和消費类电子产品的理

想选择,已成功应用于多家公司的掌上电脑系列[3]。

其中,ARM7、ARM9、ARM9E和ARM10为4个通用处理器系列,每一个系列提供一套相对独特的性能来满足不同应用领域的需求。SecurCore系列专门为安全要求较高的应用而设计。Intel的Xscale和StrongARM也是应用非常广泛的嵌入式处理器系列。

1.2.2 ARM处理器的选型

基于ARM为内核的处理器已经越来越多,并且种类繁多,在选择开发基于ARM的嵌入式系统时,首要任务就是选择ARM微处理器。下面讲述在选择ARM微处理器的一般准则[3]。

1. ARM微处理器内核的选择

? 如果使用Windows CE或标准Linux等操作系统,就需要选择ARM720T以上

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

带有MMU功能的ARM晶片。

? ARM720T、ARM920T、ARM922T、ARM946T、Strong-ARM都带有MMU

功能。

? 而ARM7TDMI则沒有MMU,不支持Windows CE和标准Linux,但目前有

uCLinux等不需要MMU支持的操作系统可执行ARM7TDMI硬件平台。

2. 系统的工作频率

? 系统的工作频率在很大程度上決定了ARM微处理器的处理能力。

? ARM7系列微处理器的典型处理速度为0.9MIPS/MHz,常见的ARM7晶片系

统主时钟为20MHz-133MHz。

? ARM9系列微处理器的典型处理速度为1.1MIPS/MHz,常见的 ARM9的系统

主时钟为100MHz-233MHz,ARM10最高可以达到700MHz。

3. 晶片內部存储体的容量

? 大多数的ARM微处理器晶片內部存储体的容量都不太大。

? 如ATMEL的AT91F40162就具有最高2MB的晶片內部存储空间。

4. 晶片內部周围电路选择

? 如USB接口、IIS接口、IIC接口、LCD控制器、键盘接口、RTC、ADC和

DAC、DSP等,设计者应该分析系统的需求,尽可能采用晶片內部周围电路

完成所需的功能,这样既可以简化系统的设计,同时提高系统的可靠性。

除了上面介绍的四个方面准则之外,还有许多其它的因素考虑,比如价格、兼容性等等。总之,根据设计的需求选择一款适合自己系统的ARM处理器是非常重要的。

1.2.3 S3C2410微处理器介绍

S3C2410是三星电子开发的一种32位RISC微处理器,它是基于ARM920T内核开发

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

的。[www.61k.com)S3C2410是面向低价格、低功耗和高性能的手持设备和小型设备而设计。S3C2410的具体特点有以下[4]:

l 系统管理

? 支持小端/大端方式

? 地址空间:128M字节每一个Bank(总共1G字节)

? 每个BANK可编程为8/16/32位数据总线

? BANK0到BANK6采用固定起始地址和大小

? BANK7具有可编程的BANK起始地址和大小

? 共8个存储器BANK

? 前6个存储器BANK用于ROM、SRAM和其他

? 另外两个存储器BANK用于ROM、SRAM和同步DRAM

? 支持等待信号用以延长总线周期

? 支持掉电时的SDRAM自刷新模式

? 支持不同类型的ROM引导(NOR/NAND Flash、EEPROM和其他)。

l S3C2410的SoC芯片集成单元

? 内部1.8V,存储器3.3V,外部I/O3.3V,16KB数据CACHE,16KB指令CACHE,?

?

?

? MMU 内置外部存储器控制器(SDRAM 控制和芯片选择逻辑) LCD控制器,一个LCD专用DMA 4个带外部请求线的DMA 3个通用异步串行端口(IrDA1.0, 16-Byte Tx FIFO, and 16-Byte Rx FIFO),2通道

SPI

? 一个多主I2C总线,一个I2S总线控制器

? SD主接口版本1.0和多媒体卡协议版本2.11兼容

? 两个USB HOST,一个USB DEVICE(VER1.1)

? 4个PWM定时器和一个内部定时器

? 看门狗定时器

? 117个通用I/O

? 24个外部中断

? 电源控制模式:标准、慢速、休眠、掉电

? 8通道10位ADC和触摸屏接口

? 带日历功能的实时时钟

? 芯片内置PLL

? 设计用于手持设备和通用嵌入式系统

? 16/32位RISC体系结构,使用ARM920T CPU核的强大指令集

? 带MMU的先进的体系结构支持WinCE、EPOC32、Linux

? 指令缓存(CACHE)、数据缓存、写缓冲和物理地址TAG RAM,减小了对主存储

器带宽和性能的影响

? ARM920T CPU 核支持 ARM 调试的体系结构

? 内部先进的位控制器总线(AMBA)(AMBA2.0,AHB/APB)

其中,S3C2410的芯片结构图1.3所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图1.3 S3C2410芯片结构

1.3 ADS集成开发环境介绍

ADS全称为ARM Developer Suite,是ARM公司推出的新一代ARM集成开发工具。[www.61k.com]现在ADS的最新版本是1.2,它取代了早期的ADS1.1和ADS1.0。在ADS工具诞生之前,一直使用的是ARM SDT工具,目前ARM SDT工具已经慢慢被淘汰。ADS除了可以安装在Windows NT4,Windows 2000,Windows 98和Windows 95操作系统下,还支持Windows XP和Windows Me操作系统。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

1.3.1 ADS软件组成

ADS由命令行开发工具,GUI(Graphics User Interface,图形用户界面)开发环境(Code Warrior和AXD),实用程序和支持软件组成。(www.61k.com)有了这些部件,用户就可以为ARM系列的RISC处理器编写和调试自己的开发应用程序了。下面将分别介绍这四个组成部分。

1.3.1.1命令行开发工具

命令行开发工具在实际应用中相对比较广泛,用其最大的好处就是可以将许多编译命令写在一个脚本文件中,然后只执行该脚本文件就可以让工具自动完成所有编译的工作。命令行中常用的命令如下:

? armcc

armcc是ARM C编译器。这个编译器通过了Plum Hall C Validation Suite为ANSI C的一致性测试。armcc用于将用ANSI C编写的程序编译成32位ARM指令代码。 在命令控制台环境下,输入以下命令:

> armcc –help

将可以查看armcc的语法格式以及最常用的一些操作选项

armcc最基本的用法为:

> armcc [options] file1 file2 ... filen

这里的option是编译器所需要的选项,fiel1,file2…filen是相关的文件名。

以下简单介绍一些最常用的操作选项:

-c:表示只进行编译不链接文件;

-C:(注意:这是大写的C)禁止预编译器将注释行移走;

-D<symbol>:定义预处理宏,相当于在源程序开头使用了宏定义语句

-E:仅仅是对C源代码进行预处理就停止;

-g<options>:指定是否在生成的目标文件中包含调试信息表;

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

-I<directory>:将directory所指的路径添加到#include的搜索路径列表中去; -J<directory>:用directory所指的路径代替默认的对#include的搜索路径;

-o<file>:指定编译器最终生成的输出文件名。

-O0:不优化;

-O1:这是控制代码优化的编译选项,大写字母O后面跟的数字不同,表示的优化级别就不同,-O1关闭了影响调试结果的优化功能;

-O2:该优化级别提供了最大的优化功能;

-S:对源程序进行预处理和编译,自动生成汇编文件而不是目标文件;

-U<symbol>:取消预处理宏名,相当于在源文件开头,使用语句#undef symbol; -W<options>:关闭所有的或被选择的警告信息;

有关更详细的选项说明,读者可查看ADS软件的在线帮助文件。

? armcpp

armcpp是ARM C++编译器。它将ISO C++ 或EC++ 编译成32位ARM指令代码。该编译器的命令选项和armcc的选项基本一样,这里就不再重复。

? tcc

tcc是Thumb C 编译器。该编译器通过了Plum Hall C Validation Suite为ANSI 一致性的测试。tcc将ANSI C源代码编译成16位的Thumb指令代码。同时它的编译选项和用法

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

类似armcc,具体使用请参考ADS软件的在线帮助文件。[www.61k.com)

? tcpp

tcpp是Thumb C++ 编译器。它将ISO C++ 和EC++ 源码编译成16位Thumb指令代码。同时它的编译选项和用法类似armcc,具体使用请参考ADS软件的在线帮助文件。 ? armasm

armasm是ARM和Thumb的汇编器。它对用ARM 汇编语言和Thumb 汇编语言写的源代码进行汇编。在命令行输入:armasm –help将会看到armasm汇编器的用法以及它的编译选项。 > armasm [options] sourcefile objectfile

> armasm [options] -o objectfile sourcefile

上述是关于armasm两种基本用法,其中options为它的选项,常用的选项如下:

-LIST:写一个列表文件在指定的文件

-Depend:保存编译后的依赖源文件

-Errors:将标准出错的诊断信息放到指定的文件中

-I:添加目录到源文件的搜索路径

-PreDefine:预执行一个 SET{L,A,S}指令

-NOCache:源缓冲关(默认是开)

-MaxCache:定义最大缓冲的大小(默认是8M)

-NOWarn:关闭打印告警信息

-G:输出调试表

-APCS:使预定义匹配已选择proc-call标准

-Help:打印帮助信息

-LIttleend: Little-endian ARM

-BIgend:Big-endian ARM

-MEMACCESS:说明目标内存系统的属性

-M:写源文件依赖性列表到标准输出

-MD:写源文件依赖性列表到标准输入

-CPU:设置目标ARM内核类型

-FPU:设置目标FP 体系版本,SOFTVFP, SOFTFPA, VFP, FPA, NONE之一 -16:汇编16位Thumb指令

-32:汇编32位ARM指令

? armlink

armlink是ARM链接器。该命令既可以将编译得到的一个或多个目标文件和相关的一个或多个库文件进行链接,生成一个可执行文件,也可以将多个目标文件部分链接成一个目标文件,以供进一步的链接。ARM链接器生成的是ELF格式的可执行映像文件。armlink的一般用法如下:

> armlink option-list input-file-list

其中,option-list:是一个区分大小写的选项表;input-file-list:是一系列库和对象文件。关于armlink的具体使用请参考ADS软件的在线帮助文件。

? armsd

armsd是ARM 和Thumb的符号调试器。它能够进行源码级的程序调试。用户可以在用C或汇编语言写的代码中进行单步调试,设置断点,查看变量值和内存单元的内容。armsd的一般用法如下:

> armsd [options] [<imagefile> [<arguments>]]

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

其中,options:是一系列调试选项;imagefile:定义一个AIF或ELF文件的名字;arguments:是被imagefile接受的命令行参数。(www.61k.com)关于armsd的具体使用请参考ADS软件的在线帮助文件。 讲到这里我们可以举一个简单的应用实例,来看看关于常用的ARM命令行是如何使用的。以附件光盘中的SWI(Sotfware Interrupter)参考项目为例,它的编译命令如下: armasm -g a_swi.s

armcc -c -g -O1 main.c

armcc -c -g -O1 c_swi.c

armlink a_swi.o main.o c_swi.o -o swi.axf

其中,armasm命令用来编译ARM汇编代码,armcc用来编译C代码,armlink用来最终链接目标文件为ELF格式的可执行映像文件。

1.3.1.2 GUI开发环境

ADS GUI开发环境包含Code Warrior和AXD两种,其中Code Warrior是集成开发工具,而AXD是调试工具。下面将分别介绍这两个工具。

CodeWarrior for ARM是一套完整的集成开发工具,充分发挥了ARM RISC 的优势, 使产品开发人员能够很好的应用尖端的片上系统技术。该工具是专为基于ARM RISC的处理器而设计的,它可加速并简化嵌入式开发过程中的每一个环节,使得开发人员只需通过一个集成软件开发环境就能研制出ARM产品,在整个开发周期中,开发人员无需离开CodeWarrior开发环境,因此节省了在操做工具上花的时间,使得开发人员有更多的精力投入到代码编写上来,CodeWarrior集成开发环境(IDE)为管理和开发项目提供了简单多样化的图形用户界面。用户可以使用ADS的CodeWarrior IDE为ARM和Thumb处理器开发用C,C++,或ARM汇编语言的程序代码。CodeWarrior IDE缩短了用户开发项目代码的周期,主要是由于:一是全面的项目管理功能,二是子函数的代码导航功能,使得用户迅速找到程序中的子函数。关于CodeWarrior的具体使用将在下一节中具体介绍。

AXD(ARM eXtended Debugger),即ARM扩展调试器。调试器本身是一个软件,用户通过这个软件使用调试代理可以对包含有调试信息的,正在运行的可执行代码进行比如变量的查看,断点的控制等调试操作。调试代理既不是被调试的程序,也不是调试器。在ARM体系中,它有这几种方式:Multi-ICE(Multi-processor in-circuit emulator),ARMulator和Angel。其中Multi-ICE是一个独立的产品,是ARM公司自己的JTAG在线仿真器,不是由ADS提供的。AXD可以在Windows 和UNIX下,进行程序的调试。它为用C,C++,和汇编语言编写的源代码提供了一个全面的Windows 和UNIX 环境。后面的章节会具体介绍AXD工具的使用方法。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

1.3.1.3实用程序

ADS除了提供上述工具外,它还提供以下的实用工具来配合前面介绍的命令行开发工具的使用。

? Flash downloader

用于把二进制映像文件下载到ARM开发板上的Flash存储器的工具

? fromELF

这是ARM映像文件转换工具。该命令将ELF格式的文件作为输入文件,将该格式转换为各种输出格式的文件,包括plain binary(BIN格式映像文件), Motorola 32-bit S-record

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

format(Motorola 32位S格式映像文件),Intel Hex 32 format(Intel 32位格式映像文件),和 Verilog-like hex format(Verilog 16进制文件)。(www.61k.com)fromELF命令也能够为输入映像文件产生文本信息,例如代码和数据长度。

? armar

ARM库函数生成器将一系列ELF格式的目标文件以库函数的形式集合在一起,用户可以把一个库传递给一个链接器以代替几个ELF文件。

1.3.1.4支持的软件

ADS为用户提供ARMulator软件,使用户可以在软件仿真的环境下或者在基于ARM的硬件环境调试用户应用程序。ARMulator是一个ARM指令集仿真器,集成在ARM的调试器AXD中,它提供对ARM处理器的指令集的仿真,为ARM和Thumb提供精确的模拟。用户可以在硬件尚未做好的情况下,开发程序代码。

关于ADS软件主要由上述四个部分组成,下面将介绍在实际工作中经常用到的Code Warrior和AXD工具的基本使用。

1.3.2使用Code Warrior IDE

Code Warrior IDE提供一个简单通用的图形化用户界面用于管理软件开发项目。可以利用Code Warrior IDE开发C,C++和ARM汇编代码以ARM和Thumb处理器为对象。下面将通过一个实例来讲述Code Warrior IDE的具体使用,为了使读者容易理解,这里还是以附件光盘中提供的SWI项目为例,讲述Code Warrior IDE工具的使用。

1.3.2.1创建项目工程

建立项目工程是嵌入式实际开发中必不可少的一部分,因为工程将所有的源码文件组织在一起,并能够决定最终生成文件存放的路径,输出的格式等。在CodeWarrior中新建一个工程的方法有两种,可以在工具栏中单击“New”按钮,也可以在“File”菜单中选择“New…”菜单。这样就会打开一个如图1.4所示的对话框。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图1.4 新建工程对话框

在Project可选框中为用户提供了7种可选择的工程类型,分别是:

? ARM Executable Image:用于由ARM指令的代码生成一个ELF格式的可执行映像文件; ? ARM Object Library:用于由ARM指令的代码生成一个armar格式的目标文件库; ? Empty Project:用于创建一个不包含任何库或源文件的工程;

? Makefile Importer Wizard:用于将Visual C的nmake或GNU make文件转入到

CodeWarrior IDE 工程文件;

? Thumb ARM Interworking Image:用于由ARM指令和Thumb指令的混和代码生成一个

可执行的ELF格式的映像文件;

? Thumb Executable image:用于由Thumb指令创建一个可执行的ELF格式的映像文件; ? Thumb Object Library:用于由Thumb指令的代码生成一个armar格式的目标文件库。(www.61k.com)

在这里选择ARM Executable Image,然后在“Project name:”里输入名为swi的工程文件名。接着在“Location:”项中点击“Set…”按钮选择项目工程存放的位置,这里存放的位置为C:\TestCode。最后点击“确定”,即可建立一个新的名为swi的工程。

这个时候会出现swi.mcp的窗口,如图1.5所示,有三个标签页,分别为files,link order,target默认的是显示第一个标签页files。通过在该标签页点击鼠标右键,选中“Add Files…”可以把要用到的源程序添加到工程中。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图1.5添加源文件到工程中

为工程添加源码常用的方法有两种,既可以使用入图1.5所示方法,也可以在“Project”菜单项中,选择“Add Files…”,这两种方法都会打开文件浏览框,用户可以把已经存在的文件添加到工程中来。[www.61k.com)当选中要添加的文件时,会出现一个对话框,如图1.6所示,询问用户把文件添加到何类目标中,在这里,我们选择DebugRel目标。这里我们添加了swi.h,a_swi.s c_swi.s和main.c文件。

在建立好一个工程时,默认的target是DebugRel,还有另外两个可用的target,分别为Realse和Debug,这三个target的含义分别为:

DebugRel:使用该目标,在生成目标的时候,会为每一个源文件生成调试信息; Debug:使用该目标为每一个源文件生成最完整的调试信息;

Release:使用该目标不会生成任何调试信息。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图1.6 选择生成目标类型

到目前为止,一个完整的名为swi的项目工程已经建立,下面该对工程进行编译和链接工作。[www.61k.com]

1.3.2.2 编译和链接项目工程

在编译swi项目之前,先进行设置,点击Edit菜单,选择“DebugRel Settings…”,或者按Alt + F7快捷键,显示如图1.7对话框。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

图1.7 DebugRel设置对话框

图1.7的最左边部分是目标设置面板,它包括如下几个大的设置对象:

? Target设置选项

Target Settings:包括Target Name,Linker,Pre-linker和Post-linker等设置。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

Access Paths:主要是用于项目的路径设置。(www.61k.com)

Build Extras:主要用于Build附加的选项设置。

Runtime Settings:包括一般设置、环境设置等。

File Mappings:包含映射信息,文件类型,编辑语言等。

Source Trees:包含源代码树结构信息,以及路径选择等。

ARM Target:定义输出image文件名和类型等。

? Language Settings设置选项

ARM Assembler:对ARM汇编语言的支持选项设置。

ARM C Compiler:对C语言的支持选项设置。

ARM C++ Compiler:对C++语言的支持选项设置。

Thumb C Compiler:对Thumb C语言的支持选项设置。

Thumb C++ Compiler:对Thumb C++语言的支持选项设置。

? Linker设置选项

ARM Linker:对输出的链接类型、RO、RW Base地址设置等选项。

ARM fromELF:定义输出文件格式以及路径等。

? Edit设置选项

Custom Keywords:对客户化关键字高亮颜色的设置。

? Debugger设置选项

Other Executables:制定其他的可执行文件来调试当调试该目标板时。

Debugger Settings:对调试器的一些基本设置。

ARM Debugger:选择调试时调试器(AXD,Armsd和其他等)的选择。

ARM Runner:选择运行时的调试器(AXD,Armsd和其他等)的选择。

? Miscellaneous设置选项

ARM Features:设置一些受限制的特性。

接下来点击CodeWarrior IDE的菜单Project下的make菜单,就可以对swi工程进行编译和链接了。整个编译链接之后生成如图1.8所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图1.8 编译和链接的之后

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

在工程swi所在的目录下,会生成一个名为:工程名_data的目录,即swi_data的目录,在这个目录下不同类别的目标对应不同的目录。(www.61k.com]在本例中由于我们使用的是DebugRe目标,所以生成的最终文件都应该在该目录下。进入到DebugRel目录中去,读者会看到make后生成的映像文件和二进制文件,映像文件用于调试,二进制文件可以烧写到目标板的Flash中运行。关于Code Warrior IDE的具体使用请参考ADS软件的在线帮助文件。

1.3.3使用AXD IDE

AXD是ADS软件中独立于CodeWarrior IDE的图形软件,打开AXD软件,默认是打开的目标是ARMulator。ARMulator也是调试的时候最常用的一种调试工具,本节主要是结合ARMulator介绍在AXD中进行代码调试的方法和过程,使读者对AXD的调试有初步的了解。要使用AXD必须首先要生成包含有调试信息的程序,在上节中,已经生成的swi.axf文件就是含有调试信息的可执行ELF格式的映像文件。这一节还是以swi工程为例讲述AXD调试工具的基本用法。

1.3.3.1打开调试文件

在菜单File中选择“Load image…”选项,打开Load Image对话框,找到要装载的.axf映像文件,点击“打开”按钮,就把映像文件装载到目标内存中了。在所打开的映像文件中会有一个蓝色的箭头指示当前执行的位置。如图1.9所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图1.9 打开swi调试文件

此外,在菜单File中还有一个“Load Debug Symbols.…”选项,该选项是用来调式那些调试器不能访问调试符号的情况,比如调试装载在ROM中的image。通常“Load image…”

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

选项用来调试装载在RAM中的代码。(www.61k.com]

在菜单Execute中选择“Go”,将运行代码。要想进行单步的代码调试,在Execute菜单中选择“Step”选项,或用F10即可以单步执行代码,窗口中蓝色箭头会发生相应的移动。

1.3.3.2设置断点

有时候,用户可能希望程序在执行到某处时,查看一些所关心的变量值,此时可以通过设置断点达到此要求。将光标移动到要进行断点设置的代码处,在Execute菜单中,选择“Toggle Breakpoint”或按F9,就会在光标所在行的起始位置出现一个红色实心圆点,表明该处为已设为断点。假设本例中给62行代码设置断点,首先将光标移至62行,然后按F9或点击“Toggle Breakpoint”按钮,此时如图1.10所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图1.10设置断点

1.3.3.3查看寄存器内容

查看寄存器的值在实际嵌入式开发调试中经常使用,使用方法:从Processor Views菜单中选择“Memory”选项,如图1.11所示。在Memory Start address选择框中,用户可以根据要查看的存储器的地址输入起始地址,在下面的表格中会列出连续的64个地址。从图1.11中可以看出地址为0x0的存储器中的初始值为0x E7FF0010,注意因为用的是little-endian*(注2),所以读数据的时候注意高地址中存放的是高字节,低地址存放的是低字节。

*注2:Big-endian和Little-endian是用来表述一组有序的字节数存放在计算机内存中时的顺序的术语。Big-endian是将高位字节(序列中最重要的值)先存放在低地址处的顺序,而Little-endian是将低位字

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

节(

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

序列中最不重要的值)先存放在低地址处的顺序。(www.61k.com]举例来说,在使用Big-endian顺序的计算机中,要存储一个十六进制数5F48所需要的字节将会以5F48的形式存储(比如5F存放在内存的1000位置,而48将会被存储在1001位置)。而在使用Little-endian顺序的系统中,存储的形式将会是485F(48在地址1000处,5F在地址1001处)。如果将0x5F48写到以0x0000开始的地址中,则存放的顺序如下:

地址 0x0000 0x0001 Big-endian Little-endian 5F 48 48 5F

IBM的370种大型机、大多数基于RISC的计算机以及Motorola的微处理器使用的是Big-endian顺序,TCP/IP协议也是。而Intel的处理器和DEC公司的一些程序则使用的Little-endian方式。

图1.11 查看寄存器值

1.3.3.4查看变量值

在调试过程中,经常需要查看某个变量的值,在AXD工具中,查看变量值的方法是:先用鼠标选中要查看的变量,然后鼠标右击,在探出的对话框中选择“Watch..”,将会显示指定变量的详细信息。此处以62行的res_3为要查看的变量,先选中res_3变量,然后鼠标右击,选择“Watch..”项,将弹出如图1.12的对话框,该对话框显示了res_3变量的地址和值等详细信息。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图1.12 查看变量对话框

总之,AXD工具的使用方法还有很多,关于AXD IDE的具体使用请参考ADS软件的在线帮助文件,这里不再赘述。(www.61k.com)

1.4嵌入式Linux开发介绍

在这一节中主要讲述Linux开发的基础知识,其中包括Linux的发展历史,Linux的开发环境和ARM Linux系统的开发流程,首先让我们来看一下Linux的发展历史。

1.4.1 Linux历史

Linux是Unix操作系统的一个克隆,由名叫Linus Torvalds的大学生在1991年开发诞生的。Linus Torvalds将他写的操作系统源代码放在了Internet上,受到很多计算机爱好者的热烈欢迎,并且这些计算机爱好者不断地添加新的功能和特性,并不断的提高它的稳定性。在1994年,Linux 1.0正式发布。现在,Linux已经成为一个功能超强的32位操作系统。Linux为嵌入操作系统提供了一个极有吸引力的选择,它是个和Unix相似、以核心为基础的、完全内存保护、多任务多进程的操作系统。支持广泛的计算机硬件,包括X86,Alpha,Sparc,MIPS,PPC,ARM,NEC,MOTOROLA等现有的大部分芯片。源代码全部公开,任何人可以修改并在GNU通用公共许可证(GNU General Public License)下发行,这样开发人员可以对操作系统进行定制。同时由于有GPL的控制,大家开发的东西大都相互兼容,不会走向分裂之路。Linux用户遇到问题时可以通过Internet向网上成千上万的Linux开发者请教,这使得最困难的问题也有办法解决。Linux带有Unix用户熟悉的完善的开发工具,几乎所有的Unix系统的应用软件都已移植到了Linux上。Linux还提供了强大的网络功能,有多种可选择窗口管理器(X windows)。其强大的语言编译器gcc、g++等也可以很容易得到。不但成熟完善、而且使用方便。

关于嵌入式Linux的发展也如同Linux发展一样非常迅速,在1999年,Linux开始根植

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

于嵌入式系统开发,同年9月在嵌入式系统会议(Embedded System Conference,ESC)上许多公司宣布支持嵌入式Linux,这些公司包括FSM Labs, MontaVista, Zentropix和Lineo等。[www.61k.com]在2000年,Samsung公司推出一款名为Yopy的PDA,其应用嵌入式Linux系统。同年Ericsson公司推出一款名为HS210的基于Linux的无绳带屏电话,它可以通过无线连接上网,打电话,收发E-mail等功能。同年许多公司采用嵌入式Linux在他们的产品线上。在2001年,最重要的宣布就是发布了Linux内核2.4,该版本被后期采用到许多嵌入式Linux分支中。在2002年,可以看到许多上市的基于Linux的产品,并且Linux已经在向数字娱乐领域发展。在2003年,Motorola宣布A760手机采用Linux作为它的嵌入式操作系统,这一年Linux也在小型办公市场上发展很快。在2004年,LynuxWorks发布基于Linux2.6内核的BlueCat。它作为第一个基于Linux2.6内核的商业嵌入式Linux。在2005年,基于Linux2.6内核的嵌入式产品已经非常广泛,尤其是基于ARM内核的芯片已经广泛使用Linux为其操作系统。现在许多公司已经采用嵌入式Linux作为他们新的设计方案。目前,AMD,ARM,TI,Motorola,Intel和IBM等知名企业把Linux作为一个首选的操作系统。相信嵌入式Linux的发展会越来越好,用户也会越来越多。

1.4.2 Linux开发环境

习惯在Windows下编成的开发人员经常会感觉在Linux系统下编程很复杂,比如环境变量、编译器的选择以及繁琐的命令等等都会让他们头疼,因为往往是Windows下这些东西都已经做好,并且基本上都是图形界面的设置,不像Linux下大都用命令行形式执行。其实Linux环境下编程并不像许多人想象的那么难,一旦你熟悉了Linux操作系统的基本原理和编译原理,相对来说你会觉得Linux开发更容易一些,因为它能让你清楚地知道程序之间的编译关系,以及内部的逻辑结构,总之会让你清楚地知道你所编译的项目而不是只了解最上层的一些应用。常见的Linux开发环境有以下三种组合方式:

1. Windows操作系统 + Cygwin工具

Cygwin于1995年开始开发,是cygnus solutions公司(已经被Red Hat公司收购)的产品。Cygwin是一个windows平台下的Linux模拟环境。它包括一个DLL(cygwin1.dll),这个dll为POSIX系统提供接口调用的模拟层,还有一系列模拟linux平台的工具。Cygwin的dll可以用于windonws95之后的x86系列windows上面。其API竭尽模拟单个Unix和linux的规范。另外Cygwin和linux之间的重要区别是:一是C函数库的不同,前者用newlib而后者用的是glibc。二是shell不同,前者用ash而在大多数linux发行版上用的是bash。Windows + Cygwin组合的开发方式非常适合初学者使用,笔者学习Linux环境下的开发也是从使用Cygwin开始的,但是这种组合不能开发QT等GUI,因为它没有提供X服务器。Cygwin的

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

下载站点是。Cygwin在Windows系统上的打开界面如图1.13所示。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图1.13 Cygwin工具

2. Windows 操作系统 + VMware工具 + Linux操作系统

VMware是一个“虚拟机”软件。[www.61k.com)它使你可以在一台机器上同时运行二个或更多的操作系统,比如WIN2000 / WINNT / WIN9X / DOS / LINUX系统。与“多启动”系统相比,VMware采用了完全不同的概念。多启动系统在一个时刻只能运行一个系统,在系统切换时需要重新启动机器。VMware是真正“同时”运行多个操作系统在主系统的平台上,就象Word / Excel那种标准Windows应用程序那样切换。Windows + VMware这种组合对于实际开发应用来说比较广泛,因为在VMware工具中可以安装Linux系统,可以完全实现Linux系统的开发。几乎和在真正的Linux系统下开发没有什么区别,并且其最大的好处是在Linux系统和Windows系统的之间的切换是非常的方便。所以笔者推荐读者使用这种组合方式学习Linux系统开发,因为它可以开发Qt等图形用户界面程序,与Cygwin工具相比,它更接近Linux真是环境。关于VMware的具体了解可参考站点。图1.14所示的是VMware工具 + ReadHat 9.0系统在Windows系统下的一个登陆界面。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图1.14 VMware工具 + Reahat 9.0操作系统

3. Linux操作系统 + 自带的开发工具

这种组合是最完整和最权威的Linux系统开发方式,不过对于那些习惯Windows系统的Linux初学者来说比较困难,因为Linux下的许多操作都是基于命令行的,所以需要记住常用的命令,并且与Windows系统下的文件共享比较困难。[www.61k.com)一般常用的Linux系统有:Red Hat,红旗Linux等。

总之,以上三种Linux环境的开发组合读者可以根据自己的兴趣进行选择,Linux环境下开发经常用到的工具有GCC或gcc,Make和GDB或gdb,以下将逐一介绍。

1.4.2.1 GCC介绍

在Linux下编译程序一般都用GCC(GNU C Compile)开发工具,无论你是编译内核代码还是应用程序,一般都用GCC工具来完成。GCC是一个全功能的 ANSI C兼容编译器。使用 GCC通常后跟一些选项和文件名来使用。GCC命令的基本用法如下,本书中所有命令操作都是基于bash(Bourne Again shell),常见的shell*(注3)有Bourne shell(/bin/sh),C shell(/bin/csh),Korn shell(/bin/ksh),Bourne again shell(/bin/bash)等。

*注3:什么是Shell?Shell是一种具备特殊功能的程序,它是介于使用者和 UNIX/Linux 操作系统之核心程序(kernel)间的一个接口。众所周知,对计算机下命令需要透过命令(command) 或程序(program)来执行;程序由编译器(compiler)将程序转为二进制代码,可是命令呢?其实shell 也是一个程序,它由输入设备读取命令,再将其转为计算机可以了解的机器码,然后执行它。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

关于gcc命令的使用语法如下: # gcc [options] [filenames]

命令行[options](选项)指定的操作将在命令行上每个给出的文件上执行,GCC 有超过100个的编译选项可用,这些选项中的许多你可能永远都不会用到, 但一些主要的选项将会频繁用到。[www.61k.com]很多的 GCC 选项包括一个以上的字符。因此你必须为每个选项指定各自的连字符,并且就象大多数 Linux 命令一样你不能在一个单独的连字符后跟一组选项, 例如, 下面的两个命令是不同的: # gcc -p -g test.c

# gcc -pg test.c

第一条命令告诉 GCC 编译 test.c 时为 prof 命令建立剖析(profile)信息并且把调试信息加入到可执行的文件里。而第二条命令只告诉 GCC 为 gprof 命令建立剖析信息。所以在使用多个选项时一定要注意。

当你不用任何选项编译一个程序时, GCC 将会建立(假定编译成功)一个名为 a.out 的可执行文件。例如,下面的命令将在当前目录下产生一个叫 a.out 的文件:

# gcc test.c

你可以用 -o 编译选项来为将产生的可执行文件指定一个文件名来代替 a.out。 例如, 将一个叫 test.c 的 C 程序编译为名叫 test的可执行文件,你将输入下面的命令:

# gcc –o test test.c

注意,当你使用 -o 选项时, -o 后面必须跟一个文件名。

GCC 同样有指定编译器处理多少的编译选项,-c 选项告诉 GCC 仅把源代码编译为目标代码而跳过汇编和连接的步骤。这个选项使用的非常频繁因为它使得编译多个 C 程序时速度更快并且更易于管理。缺省时 GCC 建立的目标代码文件有一个 .o 的扩展名。-S 编译选项告诉 GCC 在为 C 代码产生了汇编语言文件后停止编译。GCC 产生的汇编语言文件的缺省扩展名是 .s。 -E 选项指示编译器仅对输入文件进行预处理。当这个选项被使用时,预处理器的输出被送到标准输出而不是储存在文件里。

当你用 GCC 编译 C 代码时,它会试着用最少的时间完成编译并且使编译后的代码易于调试。易于调试意味着编译后的代码与源代码有同样的执行次序,编译后的代码没有经过优化。有很多选项可用于告诉 GCC 在耗费更多编译时间和牺牲易调试性的基础上产生更小更快的可执行文件。这些选项中最典型的是-O 和 -O2 选项。-O 选项告诉 GCC 对源代码进行基本优化。这些优化在大多数情况下都会使程序执行的更快。-O2 选项告诉 GCC 产生尽可能小和尽可能快的代码。-O2 选项将使编译的速度比使用 -O 时慢。但通常产生的代码执行速度会更快。如果想了解GCC的详细描述, 请参考 GCC 的指南页, 在命令行上键入 man gcc 就可以看到所有GCC的选项说明。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

1.4.2.2 GNU Make介绍

GNU Make工具是Linux下非常重要的一个开发工具,当你编译只有几个源文件的程序时也许觉得Make工具并没多大意义,但是当你开发一个庞大的软件系统(比如成千上万个源文件)时,Make工具就变得必不可少了。作为一个Linux开发人员,熟悉make工具的使用以及编写自己的Makefile是必需的。在Linux环境下使用GNU 的make工具能够比较容易的构建一个属于你自己的工程,整个工程的编译只需要一个命令就可以完成编译、连接以至于最后的执行。在make命令后不仅可以出现宏定义,还可以跟其他命令行参数,这些参数指定了需要编译的目标文件。其标准形式为:

target1 [target2 …]:[:][dependent1 …][;commands][#…]

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

[(tab) commands][#…]

方括号中间的部分表示可选项。(www.61k.com]Targets和dependents当中可以包含字符、数字、句点和"/"符号。除了引用,commands中不能含有"#",因为"#"在这里代表注释行的开始,也不允许换行。

在通常的情况下命令行参数中只含有一个":",此时command序列通常和makefile文件中某些定义文件间依赖关系的描述行有关。如果与目标相关连的那些描述行指定了相关的command序列,那么就执行这些相关的command命令,即使在分号和(tab)后面的command字段甚至有可能是NULL。如果那些与目标相关连的行没有指定command,那么将调用系统默认的目标文件生成规则。

如果命令行参数中含有两个冒号"::",则此时的command序列也许会和makefile中所有描述文件依赖关系的行有关。此时将执行那些与目标相关连的描述行所指向的相关命令。同时还将执行build-in规则。

如果在执行command命令时返回了一个非"0"的出错信号,例如makefile文件中出现了错误的目标文件名或者出现了以连字符打头的命令字符串,make操作一般会就此终止,但如果make后带有"-i"参数,则make将忽略此类出错信号。

Make命本身可带有四种参数:标志、宏定义、描述文件名和目标文件名。其标准形式为:

make [flags] [macro definitions] [targets]

Unix/Linux系统下标志位flags选项及其含义为:

-f file 指定file文件为描述文件,如果file参数为"-"符,那么描述文件指向标准输入。如果没有"-f"参数,则系统将默认当前目录下名为makefile或者名为Makefile的文件为描述文件。在Linux中, GNU make 工具在当前工作目录中按照GNUmakefile、makefile、Makefile的顺序搜索 makefile文件。

-i 忽略命令执行返回的出错信息。

-s 沉默模式,在执行之前不输出相应的命令行信息。

-r 禁止使用build-in规则。

-n 非执行模式,输出所有执行命令,但并不执行。

-t 更新目标文件。

-q make操作将根据目标文件是否已经更新返回"0"或非"0"的状态信息。

-p 输出所有宏定义和目标文件描述。

-d Debug模式,输出有关文件的调试信息。

Linux下make标志位的常用选项与Unix系统中稍有不同,下面我们只列出了不同部分: -c dir 在读取 makefile 之前改变到指定的目录dir。

-I dir 当包含其他 makefile文件时,利用该选项指定搜索目录。

-h help文挡,显示所有的make选项。

-w 在处理 makefile 之前和之后,都显示工作目录。

通过命令行参数中的target,可指定make要编译的目标,并且允许同时定义编译多个目标,操作时按照从左向右的顺序依次编译target选项中指定的目标文件。如果命令行中没有指定目标,则系统默认target指向描述文件中第一个目标文件。为了能快速的对make和Makefile有个大体了解,这里给出一个最简单的Makefile实例。假设源文件有四个:main.c, file1.c, file2.c, file1.h和file2.h。Makefile文件编写如下:

1 # 最简单的Makefile

2 CC=gcc

3 exec=test.exe

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

4

5

6

7

8

9

10

11

12 obj=main.o file1.o file2.o $(exec) : $(obj) $(CC) -o $(exec) $(obj) $(objects) : %.o : %.c $(CC) -c $< .PHONY: clean clean: -rm $(exec) $(obj)

Makefile文件有几个非常有用的变量:分别是 $@、$*、$?、$^、$< ,其代表的意义分别是:

$@ -- 完整的目标文件,包括扩展名

$* -- 目标文件去掉后缀的部分

$^ -- 所有的依赖文件

$< -- 比目标文件更新的依赖文件

$? -- 表示被修改的文件

在此解释一下上述的Makefile,这是一个非常简单的 makefile ,make 从最上面开始。(www.61k.com]其中#号用来注释行用得,所以第1行是注释行。第2-4行,用来定义变量,其中定义了编译器为gcc;可执行文件为test.exe;目标文件有main.o,file1.o和file2.o。第5行,表示了可执行文件依赖于目标文件,注意在引用变量前一定要加$符号,否则系统不能正确引用该变量。第6行,该行是命令行,值得注意的是命令行前一定是以[Tab]键开始,否则系统不能执行命令。该行等价于“gcc –o test.exe main.o file1.o file2.o”。第7行,表示目标文件依赖于具体的源文件。第8行,意思是当有文件更新时执行编译。第10-12行,建立一个执行make的清除选项,实现的功能是删除可执行文件和目标文件。

在Makefile建立完成后,在有Makefile文件的目录下通过命令行输入以下命令: # make

上述命令的作用就是执行源代码的编译,编译的顺序和逻辑由所编写的Makefile文件决定,以上述的Makefile为例,执行后会生成一个文件名为test.exe的可执行性文件,然后输入以下命令执行该文件:

# ./test

此外,可以在命令行输入以下命令来清除可执行文件test.exe,目标文件main.o,file1.o 和file2.o。

# make clean

通过上面的例子可以对Make和Makefile有了感性的认识,在实际工作中makefile文件会比较庞大,相对比较复杂,不过万变不离其宗,它的实现方式和目的都是一样,通过具体实践应用读者一定会觉得Makefile的编写并不是什么难事。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

1.4.2.3 GDB介绍

Linux 包含了一个叫 GDB(GNU DeBugger)的 GNU 调试程序。GDB是一个用来调试 C 和 C++ 程序的强大调试器。它使你能在程序运行时观察程序的内部结构和内存的使用情况。以下是 GDB所提供的一些基本功能:

? 监视程序中变量的值

? 设置断点以使程序在指定的代码行上停止执行

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

单步执行代码

在命令行上键入 gdb 并按回车键就可以运行GDB了, 如果已经正常安装的话, GDB将被启动并且将在屏幕上看到以下类似的内容: ?

GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)

Copyright 2003 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are

welcome to change it and/or 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.

This GDB was configured as "i386-redhat-linux-gnu".

(gdb)

GDB支持很多的命令使你能实现不同的功能。(www.61k.com]这些命令从简单的文件装入到允许你检查所调用的堆栈内容的复杂命令, 如表1.1列出了gdb 调试时会用到的一些命令。想了解 GDB的详细使用请参考 GDB的指南页。

表1.1 GDB常用命令描述 命令

break

file

kill

list

make

next

print

quit

run

shell

step

watch 命令描述 在代码里设置断点, 这将使程序执行到这里时被挂起 装入想要调试的可执行文件 终止正在调试的程序 列出产生执行文件的源代码的一部分 使你在不退出 gdb时就可以重新产生可执行文件 执行一行源代码但不进入函数内部 显示表达式的值 终止 gdb 执行当前被调试的程序 使你能不离开 gdb 就执行 shell 命令 执行一行源代码而且进入函数内部 监视一个变量的值而不管它何时被改变

接下来举一个简单的GDB使用实例,使读者能够初步了解在Linux系统下应用程序的调试过程。

首先建立一个被调试的程序名叫test.c,文件内容如下:

#include <stdio.h>

int main()

{

char str1[]="Hello,world";

char str2[11]="";

int i=0;

while(str1[i]!='\0')

{

str2[i]=str1[i];

i++;

}

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

} printf("The string1 is:%s.\n",str1); printf("The string2 is:%s.\n",str2); return 0; 然后对这个源程序进行编译,编译和执行命令如下:

# gcc –g –o test test.c

# ./test

The string1 is:Hello,world.

The string2 is:Hello,world?E?Hello,world.

注意上面的命令中多了一个-g选项,该选项的含义是为接下来调试作准备的,如果在编译时没有加这个选项,那么就不能直接运行gdb命令进行调试该程序,不过可以通过另外一种方式来代替它,就是进入gdb以后,在gdb命令输入行输入以下命令来调试该程序: (gdb) gdb test

通过上面的执行结果可以看出,test程序的目的是打印结果应该为:

The string1 is:Hello,world.

The string2 is:Hello,world.

但实际打印结果却是:

The string1 is:Hello,world.

The string2 is:Hello,world?E?Hello,world.

通过以上输出的结果可以得知出错的大概位置就在while循环执行过程中,接下来用gdb进行调试,进入gdb调试界面后会显示如下:

(gdb) list

1 #include <stdio.h>

2 int main()

3 {

4 char str1[]="Hello,world";

5 char str2[11]="";

6 int i=0;

7

8 while(str1[i]!='\0')

9 {

10 str2[i]=str1[i];

(gdb) list

11 i++;

12 }

13

14 printf("The string1 is:%s.\n",str1);

15 printf("The string2 is:%s.\n",str2);

16 return 0;

17 }

(gdb)

Line number 18 out of range; test.c has 17 lines.

其中用list命令用来显示要调适的程序,一般需要好几页才能显示完程序,所以显示下

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

一页程序继续用list命令或输入回车即可。(www.61k.com)下来就要对程序中可能会出错的地方设置断点,设置断点的方法如下,设置断点在第8行。

(gdb) break 8

Breakpoint 1 at 0x8048373: file test.c, line 8.

然后用run命令运行程序,显示信息如下: (gdb) run

Starting program: /home/mike/test

Breakpoint 1, main () at test.c:8

8 while(str1[i]!='\0')

可见程序在第8行时暂停运行了,因为在第8行对它设置了断点,如果想继续执行一行源代码但不进入函数内部,用next命令即可。 (gdb) next

10 str2[i]=str1[i];

(gdb) watch str2[i]

Watchpoint 2: str2[i]

(gdb) next

Watchpoint 2: str2[i]

Old value = 0 '\0'

New value = 72 'H'

main () at test.c:11

11 i++;

上述调试过程中,用watch命令来监视str2[i]的值,其中可以看出str2[i]的以前存放的值和新赋得值,然后继续用next命令察看str2[i]的值,发现while循环过程中执行没有问题,然后给14和15行设置了断点,结果在执行第15行时程序终止,并且报错如下: Program received signal SIGSEGV, Segmentation fault.

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

0x080483ae in main () at test.c:15

15 printf("The string2 is:%s.\n",str2);

(gdb) next

Program terminated with signal SIGSEGV, Segmentation fault.

The program no longer exists.

说明执行在第15行时出错,经分析是由于str2数组越界造成的,也就是说str2的数组没有’\0’结束符,这样在程序中会经常输出异常的值,该程序改正的方法就是将第5行代码改为:char str2[12]=""; 或者数组的大小大于12即可。

以上通过一个非常简单的例子讲述GDB在实际中的应用,其实在实际中不会像例子中那么简单的程序,但是操作方式都非常类似,请读者具体使用时参考 GDB指南。

1.4.3 ARM Linux系统开发流程

不同于常见的桌面系统开发软件,在开发嵌入式系统时,常常把所有的软件模块最终都生成一个单一的文件,我们把这个单一的文件称为image(通常叫映像文件),它一般布局

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

如下图1.15所示:

图1.15 典型嵌入式系统软件image的逻辑布局

其中最底层是BootLoader(启动加载程序),接着是嵌入式操作系统内核(如Linux内核),内核之上就是设备驱动,然后就是根文件系统和应用程序。(www.61k.com)这里只是给出一般情况下映像文件的逻辑组成,通常还有DSP等其他程序。

一般嵌入式开发方法如图1.16所示,当程序员开始开发一个基于嵌入式系统应用的时候,首先用该嵌入式相关的开发工具在宿主机上进行开发,例如通常使用ARM的ADS工具编写程序,使用ARMulator或者在评估板上来调试,最后将在宿主机上开发生成的image文件烧写到独立的嵌入式应用设备中去。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图1.16嵌入式系统开发的一般方法 通常基于ARM系统的Linux开发步骤如下: a) 开发目标硬件系统:如选择微处理器,flash及其它外设等。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

b) 建立交叉编译工具:一般的GCC工具都是针对X86体系的,为了能够产生目标板

执行的代码必须建立交叉编译工具。(www.61k.com)

c) 开发Bootloader:建立启动系统的主引导程序。

d) 移植Linux内核:如基于ARM的Linux 2.6内核移植。

e) 开发一个根文件系统:如rootfs的制作。

f) 开发相关硬件的驱动程序:如LCD,Keypad等。

g) 开发上层的应用程序:如QT GUI开发。

1.5 Linux内核介绍

Linux内核是Linux系统的心脏,它实现了操作系统五大主要功能模块:进程管理、内存管理、文件系统、设备控制和网络。Linux内核的功能模块如图1.17所示[1]:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图1.17 Linux内核的功能模块划分

进程管理模块可以说是Linux内核的心脏模块,它负责创建和终止进程,并且处理它们和外部世界的联系(输入和输出)。对整个系统功能来讲,不同进程之间的通信(通过信号,管道,进程间通信原语)是基本的,这也是由内核来处理的。另外,调度器应该是整个操作系统中最关键的例程,是进程管理中的一部分。更广义的说,内核的进程管理活动实现了在一个CPU上多个进程的抽象概念。内存管理模块的作用是用于确保所有进程能够安全地共享计算机主内存区,此外,内存管理模块还支持虚拟内存管理方式,使得Linux支持进程使用比实际内存空间更多的内存容量,并可以利用文件系统把暂时不用的内存数据块交换到外部存储设备中去,等需要时再交换回来,这样大大提高了内存使用效率,节省了内存空间。文件系统模块用于支持对外部设备的驱动和存储,虚拟文件系统通过向所有的外部存储设备

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

提供一个通用的文件系统接口,从而隐藏了各种硬件设备的不同细节。[www.61k.com)网络模块提供对多种网络通信标准的访问,并支持许多网络硬件设备[1]。

总之,本书不是讲述操作系统内核原理的书籍,读者可以参考相关的书籍来学习它。但是对于本书中所介绍的内容常常是需要跟内核打交道,所以希望读者对操作系统内核原理有所了解。

1.5.1 Linux内核目录结构

在解压后的Linux内核(这里以Linux 2.6.10内核为例)根目录下输入如下tree命令,将会显示如下目录信息,其中tree命令是由tree工具实现的,用来显示文件目录信息,它的下载站点是。

# tree –L 1

.

|-- COPYING

|-- CREDITS

|-- Documentation

|-- MAINTAINERS

|-- Makefile

|-- README

|-- REPORTING-BUGS

|-- arch

|-- crypto

|-- drivers

|-- fs

|-- include

|-- init

|-- ipc

|-- kernel

|-- lib

|-- mm

|-- net

|-- scripts

|-- security

|-- sound

`-- usr

16 directories, 6 files

内核根目录下的主要目录和文件的意义介绍如下:

COPYING:该文件主要是对Linux内核代码的版权声名。

CREDITS:该文件是对该版本和之前版本Linux内核所做贡献的所有成员列表。

Documentation:该目录存放所有内核相关的技术文档,是学习内核原理的很好参考资料。 MAINTAINERS:该文件记录所有维护内核人员列表以及讲述如何提交一个内核的改变。 Makefile:该文件是编译内核的最上层Makefile文件,也是编译内核的入口文件。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

README:该文件是编译内核的帮助文件,编译前一定要阅读该文件,该文件对于编译内核很有帮助。(www.61k.com]

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

REPORTING-BUGS:该文件是有关提交内核Bug 的一些要求和建议。

Arch:该目录包括了所有和体系结构相关的核心代码。它下面的每一个子目录都代表一种Linux支持的体系结构,例如i386就是Intel CPU及与之相兼容体系结构的子目录。arm就代表是ARM体系结构相关的代码。

Drivers:该目录包含内核中所有硬件相关的驱动实现代码,它又进一步划分成几类设备驱动,比如char目录为字符设备,block目录为块设备等。

Fs:该目录存放Linux支持的文件系统代码。不同的文件系统有不同的子目录对应,如ext3文件系统对应的就是ext3子目录。

Include:该目录包括编译内核所需要的大部分头文件,例如与平台无关的头文件在include/linux子目录下。

Init:该目录包含内核的初始化代码(不是系统的引导代码)。这是研究内核如何工作的好起点。

Ipc:该目录包含了内核进程间的通信代码。

Kernel:该目录包含内核管理的核心代码。同时与处理器结构相关代码都放在arch/*/kernel目录下。

Lib:该目录包含了内核的库代码,与处理器结构相关的库代码被放在arch/*/lib/目录下。 Mm:该目录包含了所有的内存管理代码。与具体硬件体系结构相关的内存管理代码位于arch/*/mm目录下。

Net:该目录里是内核的网络部分代码,其每个子目录对应于网络的一个方面。

Scripts:该目录包含用于配置核心的脚本文件。

1.5.2 如何阅读Linux内核源代码

Linux 0.01版在是在1991年出生的,是Linux内核的第一版,该内核的大小是158K字节,6975行代码。Linux内核在全球热衷于开源项目的计算机高手努力下已经发展为一个庞大成熟的操作系统,到目前为止,最新的内核版本是2.6.18,其大小超过200M字节,代码行超过400万行,如此庞大的源代码如果没有合适的工具或方法去阅读它,那么想在短时间学习它基本上是不可能的。技术的发展总会使人类不断简化所作的工作,同样现在可以利用有效的工具来辅助我们去阅读和研究如此庞大的系统。目前最流行的阅读内核源代码工具有两个,UltraEdit和SourceInsight,这两个工具在实际工作中应用非常广泛。下面简单介绍一下这两个重要工具。

UltraEdit:它是一套功能强大的文本编辑器,可以编辑文本、十六进制、ASCII 码,可以取代记事本,内建英文单字检查、C、C++ 及 VB 指令突显,可同时编辑多个文件,而且即使开启很大的文件速度也是相当的快。软件附有 C/C++ 标签颜色显示、搜寻替换以及无限制的还原功能,一般大家喜欢用其来修改EXE、DLL和源文件等。比如用UE(UltraEdit简称)打开Linux 2.6.10内核的一个名为cpu.c的源文件,如图1.18所示。用UE阅读源代码可以高亮显示源代码的关键字,查找方便,尤其是可以方便编辑。关于UE的具体使用请参考其帮助文档。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图1.18:UltraEdit查看源代码文件

SourceInsight:它实质上是一个支持多种开发语言(java,c ,c++等等)的编辑器,只不过由于其查找、定位、彩色显示等功能的强大,而被我们当成源代码阅读工具使用。(www.61k.com]它和UltraEdit相比较增加了许多功能,比如提供了代码之间调用关系的显示,以及文件之间的关系查找。比如用SourceInsight建立一个Linux 2.6.10内核代码的项目工程,如图1.19所示。关于SourceInsight的具体使用方法请参考其帮助文档。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图1.19:用SourceInsight建立的Linux 2.6.10内核代码工程

注意:UltraEdit和SourceInsight这两个工具通常使用在Windows操作系统下使用。[www.61k.com)

1.6 本章小节

本章是嵌入式系统开发的最基础部分,通过本章的学习读者可以了解嵌入式系统的基本概念,嵌入式系统的基本组成,ARM处理器的基本知识,ADS工具的基本使用方法,Linux开发环境,ARM Linux系统开发的基本流程,以及Linux内核目录结构和阅读Linux内核代码的方法。通过学习本章的内容,为以后更深入的学习嵌入式开发打下良好的基础。下一章将开始介绍如何自己构建交叉编译工具链。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

1.7常见问题

1.嵌入式微处理器有哪些特点?

参考答案:

通常情况下,嵌入式微处理器都具有以下四个基本特点:

1. 对实时多任务有很强的支持能力,能完成多任务并且有较短的中断响应时间,从而使内部的代码和实时内核心的执行时间减少到最低限度。[www.61k.com)

2. 具有功能很强的存储区保护功能。这是由于嵌入式系统的软件结构已模块化,而为了避免在软件模块之间出现错误的交叉作用,需要设计强大的存储区保护功能,同时也有利于软件诊断。

3. 可扩展的处理器结构,以能最迅速地开展出满足应用的最高性能的嵌入式微处理器。

4. 嵌入式微处理器必须功耗很低,尤其是用于便携式的无线及移动的计算和通信设备中靠电池供电的嵌入式系统更是如此。

2.嵌入式系统的组成部分?

参考答案:

嵌入式系统一般由硬件平台和软件平台两部分组成,其中硬件平台由嵌入式微处理器和外围硬件设备组成,而软件平台由嵌入式操作系统和应用软件组成。

3.嵌入式ARM Linux系统的一般开发步骤?

参考答案:

通常基于ARM平台的嵌入式Linux开发步骤如下:

1. 开发目标硬件系统

2. 建立交叉编译工具

3. 开发Bootloader

4. 开发Linux内核

5. 开发一个根文件系统

6. 开发特定硬件的驱动程序

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

7. 开发上层的应用程序。

4.选择ARM处理器的准则有哪些?

参考答案:

选择ARM处理器一般需要关注以下4个主要方面:

1. ARM微处理器内核的选择

2. 系统的工作频率

3. 晶片內部存储体的容量

4. 晶片內部周围电路选择

5.在makefile文件中,特殊符号$@、$*、$?、$^和$<分别代表什么? 参考答案:

$@ -- 完整的目标文件,包括扩展名

$* -- 目标文件去掉后缀的部分

$^ -- 所有的依赖文件

$< -- 比目标文件更新的依赖文件

$? -- 表示被修改的文件

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

第2章 交叉编译工具链的构建

本章学习目标:

l 了解交叉编译工具链

l 理解分步构建交叉编译工具链的方法

l 学会使用Crosstool工具构建交叉编译工具链

2.1 交叉编译工具链介绍

在介绍交叉编译工具链之前读者会有个疑问,为什么要用交叉编译器?交叉编译通俗地讲就是在一种平台上编译出能运行在体系结构不同的另一种平台上,比如在PC平台(X86 CPU)上编译出能运行在ARM为内核 CPU平台上的程序,编译得到的程序在X86 CPU平台上是不能运行的,必须放到ARM CPU平台上才能运行,当然两个平台用的都是Linux系统。(www.61k.com)这种方法在异平台移植和嵌入式开发时非常普遍。相对与交叉编译,平常做的编译叫本地编译,也就是在当前平台编译,编译得到的程序也是在本地执行。用来编译这种跨平台程序的编译器就叫交叉编译器,相对来说,用来做本地编译的工具就叫本地编译器。所以要生成在目标板上运行的程序,必须要用交叉编译工具链来完成。在裁减和定制Linux内核用于嵌入式系统之前,由于一般嵌入式开发系统存储大小有限,通常都要在性能优越的PC机上建立一个用于目标机的交叉编译工具链,用该交叉编译工具链在PC机上编译目标板上要运行的程序。交叉编译工具链是一个由编译器、连接器和解释器组成的综合开发环境。交叉编译工具链主要由 binutils、gcc 和 glibc 三个部分组成。有时出于减小 libc 库大小的考虑,你也可以用别的 c 库来代替 glibc,例如 uClibc、dietlibc 和 newlib。建立一个交叉编译工具链是一个相当复杂的过程,如果你不想自己经历复杂繁琐的编译过程,网上有一些编译好的可用的交叉编译工具链可以下载,但就学习为目的来说读者有必要学习自己制作一个交叉编译工具链。本章通过具体的实例讲述基于ARM的嵌入式Linux交叉编译工具链的制作过程。

2.2 ARM Linux交叉编译工具链的构建

构建交叉编译器的第一个步就是确定目标平台。在GNU系统中,每个目标平台都有一个明确的格式,这些信息用于在构建过程中识别要使用的不同工具的正确版本。因此,当你在一个特定目标机器下运行GCC时,GCC便在目录路径中查找包含该目标规范的应用程序路径。GNU的目标规范格式为CPU-PLATFORM-OS。例如x86/i386 目标机名为:i686-pc-linux-gnu。本章目的是讲述建立基于ARM平台的交叉工具链,所以目标平台名:arm-linux-gnu。

通常构建交叉工具链有三种方法:

方法一:分步编译和安装交叉编译工具链所需要的库和源代码,最终生成交叉编译工具链。该方法相对比较困难,适合想深入学习构建交叉工具链的读者。如果只是想使用交叉工具链,建议使用方法二或方法三构建交叉工具链。

方法二:通过Crosstool脚本工具来实现一次编译生成交叉编译工具链,该方法相对于方法一要简单许多,并且出错的机会也非常少,建议大多数情况下使用该方法构建交叉工具链。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

方法三:直接通过网上(ftp.arm.kernel.org.uk)下载已经制作好的交叉编译工具链。[www.61k.com]该方法的优点不用多说了,当然是简单省事了,与此同时该方法有一定的弊端就是局限性太大,因为毕竟是别人构建好的,也就是固定的没有灵活性,构建所用的库以及编译器的版本也许并不适合你要编译的程序,同时也许会在使用时出现许多莫名的错误,建议读者慎用此方法。

为了让读者真正的学习交叉编译工具链的构建,下面将重点详细地介绍前两种构建ARM Linux交叉编译工具链的方法。

2.2.1分步构建交叉编译链

分步构建,顾名思义就是一步一步的建立交叉编译链,不同于下一节中讲述的Crosstool脚本工具一次编译生成的方法,该方法建议适合那些希望深入学习了解构建交叉编译工具链的读者。该方法相对来说难度较大,通常情况下困难重重,犹如唐僧西天取经,不过本节尽可能详细地介绍构建的每一个步骤,读者完全可以根据本节的内容自己独立去实践,构建自己的交叉工具链。该过程所需的时间较长,希望读者有较强的耐心和毅力去学习和实践它,通过实践可以使读者更加清楚交叉编译器构建过程以及各个工具包的作用。该方法所需资源如表2.1所示。

表2.1所需资源

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

以上资源通过相关站点下载后,就可以开始建立交叉编译工具链了。 2.2.1.1建立工作目录

首先建立工作目录,工作目录就是在什么目录下构建交叉工具链,目录的构建一般没有特别的要求,根据个人喜好建立。以下所建立的目录是作者自定义的,当前的用户定义为mike,因此用户目录为:/home/mike,在用户目录下首先建立一个工作目录(armlinux)。建立工作目录的命令行操作如下:

# cd /home/mike

# mkdir armlinux

再在这个工作目录armlinux下建立三个目录 build-tools、kernel 和 tools。具体操作如下:

# cd armlinux

# mkdir build-tools kernel tools

其中各目录的作用是:

build-tools:用来存放你下载的 binutils、gcc、glibc 等源代码和用来编译这些源代码的

目录。

kernel:用来存放你的内核源代码。

tools:用来存放编译好的交叉编译工具和库文件。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

2.2.1.2建立环境变量

该步骤的目的是为了方便重复输入路径,因为重复操作每件相同的事情总会让人觉得很麻烦,如果读者不习惯使用环境变量就可以略过该步,直接输入绝对路径就可以。(www.61k.com)声明以下环境变量的目的就是在之后编译工具库的时候会用到,并且很方便输入,尤其是可以降低输错路径的风险。 # export PRJROOT=/home/mike/armlinux

# export TARGET=arm-linux

# export PREFIX=$PRJROOT/tools

# export TARGET_PREFIX=$PREFIX/$TARGET

# export PATH=$PREFIX/bin:$PATH

注意,用export声明的变量是临时的变量,也就是当你注销或更换了控制台,这些环境变量就消失了,如果还需要使用这些环境变量就必须重复export操作,所以有时会挺麻烦。值得庆幸的是,环境变量也可以定义在bashrc文件中,这样当注销或更换控制台时,这些变量就一直有效,就不用老是export这些变量了。

2.2.1.3编译、安装Binutils

Binutils是GNU工具之一,它包括连接器,汇编器和其他用于目标文件和档案的工具,它是二进制代码的处理维护工具。安装Binutils工具包含的程序有: addr2line, ar, as, c++filt, gprof, ld, nm, objcopy, objdump, ranlib, readelf, size, strings, strip, libiberty, libbfd和libopcodes。对这些程序的简单解释如下:

addr2line:把程序地址转换为文件名和行号。在命令行中给它一个地址和一个可执行文件名,它就会使用这个可执行文件的调试信息指出在给出的地址上是哪个文件以及行号。

ar:建立、修改、提取归档文件。归档文件是包含多个文件内容的一个大文件,其结构保证了可以恢复原始文件内容。

as:主要用来编译GNU C编译器gcc输出的汇编文件,产生的目标文件由连接器ld连接。

c++filt:连接器使用它来过滤 C++ 和 Java 符号,防止重载函数冲突。

gprof:显示程序调用段的各种数据。

ld:是连接器,它把一些目标和归档文件结合在一起,重定位数据,并链接符号引用。通常,建立一个新编译程序的最后一步就是调用ld。

nm:列出目标文件中的符号。

objcopy:把一种目标文件中的内容复制到另一种类型的目标文件中。

objdump:显示一个或者更多目标文件的信息。使用选项来控制其显示的信息。它所显示的信息通常只有编写编译工具的人才感兴趣。

ranlib:产生归档文件索引,并将其保存到这个归档文件中。在索引中列出了归档文件各成员所定义的可重分配目标文件。

readelf:显示elf格式可执行文件的信息。

size:列出目标文件每一段的大小以及总体的大小。默认情况下,对于每个目标文件或者一个归档文件中的每个模块只产生一行输出。

strings:打印某个文件的可打印字符串,这些字符串最少4个字符长,也可以使用选项-n设置字符串的最小长度。默认情况下,它只打印目标文件初始化和可加载段中的可打印

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

字符;对于其它类型的文件它打印整个文件的可打印字符,这个程序对于了解非文本文件的内容很有帮助。(www.61k.com]

strip:丢弃目标文件中的全部或者特定符号。

libiberty: 包含许多GNU程序都会用到的函数,这些程序有:getopt, obstack, strerror, strtol 和 strtoul。

libbfd:二进制文件描述库.

libopcode:用来处理opcodes的库, 在生成一些应用程序的时候也会用到它。

Binutils工具安装依赖于:Bash, Coreutils, Diffutils, GCC, Gettext, Glibc, Grep, Make, Perl, Sed, Texinfo等工具。

介绍完Binutils工具后,下面将分步介绍安装binutils-2.15的过程。

首先解压binutils-2.15.tar.bz2包,命令如下: # cd $PRJROOT/build-tools

# tar –xjvf binutils-2.15.tar.bz2

接着配置Binutils工具,建议建立一个新的目录用来存放配置和编译文件,这样可以使源文件和编译文件独立开,具体操作如下: # cd $PRJROOT/build-tools

# mkdir build-binutils

# cd build-binutils

# ../ binutils-2.15/configure --target=$TARGET --prefix=$PREFIX

其中选项 –target的意思是制定生成的是 arm-linux 的工具,--prefix 是指出可执行文件安装的位置。执行上述操作会出现很多check信息,最后产生 Makefile 文件。接下来执行make和安装操作,命令如下:

# make

# make install

该编译过程较慢,需要数十分钟,安装完成后察看/home/mike/armlinux/tools/bin目录下文件,察看结果如下,表明此时Binutils工具已经安装结束。 # ls $PREFIX/bin

arm-linux-addr2line arm-linux-ld arm-linux-ranlib arm-linux-strip

arm-linux-ar arm-linux-nm arm-linux-readelf

arm-linux-as arm-linux-objcopy arm-linux-size

arm-linux-c++filt arm-linux-objdump arm-linux-strings

2.2.1.4获得内核头文件

编译器需要通过系统内核的头文件来获得目标平台所支持的系统函数调用所需要的信息。对于Linux内核,最好的方法是下载一个的合适的内核,然后拷贝获得头文件。需要对内核做一个基本的配置来生成正确的头文件;不过,不需要编译内核。对于本例中的目标,arm-linux,需要以下步骤:

在kernel目录下解压linux-2.6.10.tar.gz内核包,执行命令如下:

# cd $PRJROOT/kernel

# tar –xvzf linux-2.6.10.tar.gz

接下来配置编译内核使其生成正确的头文件,执行命令如下:

# cd linux-2.6.10

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

# make ARCH=arm CROSS_COMPILE=arm-linux- menuconfig

其中ARCH=arm表示是以arm为体系结构,CROSS_COMPILE=arm-linux-表示以arm-linux-为前缀的交叉编译器。[www.61k.com]也可以用 config 和 xconfig 来代替 menuconfig,推荐大家用 make menuconfig,这也是内核开发人员用的最多的配置方法。注意在配置时一定要选择处理器的类型,这里选择三星的S3C2410(System Type->ARM System Type->/Samsung S3C2410),如图2.1所示。配置完退出并保存,检查一下的内核目录中的 include/linux/version.h 和 include/linux/autoconf.h 文件是不是生成了,这是编译 glibc 是要用到的,如果version.h 和 autoconf.h 文件存在,这说明生成了正确的头文件。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图2.1 Linux 2.6.10内核配置界面

拷贝头文件到交叉工具链的目录,首先需要在/home/mike/armlinux/tools/arm-linux目录下建立工具的头文件目录inlcude,然后拷贝内核头文件到此目录下,具体操作如下:

# mkdir –p $TARGET_PREFIX/include

# cp –r $PRJROOT/kernel/linux-2.6.10/include/linux $TARGET_PREFIX/include

# cp –r $PRJROOT/kernel/linux-2.6.10/include/asm-arm $TARGET_PREFIX/include/asm

2.2.1.5编译安装boot-trap gcc

这一步的目的主要是建立arm-linux-gcc工具,注意这个gcc没有glibc库的支持,所以只能用于编译内核,bootloader等不需要C库支持的程序,后面创建C库也要用到这个编译器,所以创建它主要是为创建C库做准备,如果只想编译内核和Bootloader,那么安装完这个就可以到此结束。安装命令如下:

# cd $PRJROOT/build-tools

# tar –xvzf gcc-3.3.6.tar.gz

# mkdir build-gcc

# cd gcc-3.3.6

# vi gcc/config/arm/t-linux

由于第一次安装ARM交叉编译工具,那么支持的libc库的头文件也没有,所以在gcc/config/arm/t-linux文件中给变量TARGET_LIBGCC2_CFLAGS 增加操作参数选项-Dinhibit_libc -D__gthr_posix_h来屏蔽使用头文件,否则一般默认会使用/usr/inlcude头文

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

件。[www.61k.com]

将TARGET_LIBGCC2-CFLAGS = -fomit-frame-pointer –fPIC 改为以下:

TARGET_LIBGCC2-CFLAGS=-fomit-frame-pointer–fPIC -Dinhibit_libc -D__gthr_posix_h 修改完t-linux文件后保存,接紧着执行配置操作,如下命令: # cd build-gcc

# ../ build-gcc /configure --target=$TARGET --prefix=$PREFIX --enable-languages=c --disable-threads --disable-shared

其中选项--enable-languages=c 表示只支持C语言,--disable-threads表示去掉 thread 功能,这个功能需要 glibc 的支持。--disable-shared表示只进行静态库编译,不支持共享库编译。 接下来执行编译和安装操作,命令如下:

# make

# make install

安装完成后,在/home/mike/armlinux/tools/bin下查看,arm-linux-gcc等工具已经生成,boot-trap gcc工具已经安装成功。

2.2.1.6建立glibc库

glibc是GUN C 库,它是编译Linux系统程序很重要的组成部分。安装glibc-2.3.2版本之前推荐先安装以下的工具:

l GNU make 3.79或更新

l GCC 3.2 或更新

l GNU binutils 2.13或更新 首先解压 glibc-2.2.3.tar.gz 和 glibc-linuxthreads-2.2.3.tar.gz 源代码,操作如下: # cd $PRJROOT/build-tools

# tar -xvzf glibc-2.2.3.tar.gz

# tar -xzvf glibc-linuxthreads-2.2.3.tar.gz --directory=glibc-2.2.3

然后进行编译配置,glibc-2.2.3配置前必须新建一个编译目录,否则在glibc-2.2.3目录下不允许进行配置操作,此处在$PRJROOT/build-tools目录下建立名为build-glibc的目录,配置操作如下:

# cd $PRJROOT/build-tools

# mkdir build-glibc

# cd build-glibc

# CC=arm-linux-gcc ../glibc-2.2.3 /configure --host=$TARGET --prefix="/usr"

--enable-add-ons --with-headers=$TARGET_PREFIX/include

选项CC=arm-linux-gcc 是把 CC(Cross Compiler)变量设成刚编译完的gcc,用它来编译glibc。--prefix="/usr"定义一个目录用于安装一些与机器无关的数据文件,默认情况下是/usr/local目录。--enable-add-ons是告诉glibc用 linuxthreads 包,在上面已经将它放入了 glibc 源码目录中,这个选项等价于 -enable-add-ons=linuxthreads。--with-headers 告诉 glibc linux 内核头文件的目录位置。

配置完后就可以编译和安装 glibc了,具体操作如下:

# make

# make install

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

2.2.1.7编译安装完整的gcc

由于第一次安装的gcc没有交叉glibc的支持,现在已经安装了glibc,所以需要重新编译来支持交叉glibc。[www.61k.com)并且上面的gcc也只支持C语言,现在可以让它不仅支持C语言还要让它支持C++语言。具体操作如下: # cd $PRJROOT/build-tools/gcc-2.3.6

# ./configure --target=arm-linux --enable-languages=c,c++ --prefix=$PREFIX

# make

# make install

安装完成后会发现在$PREFIX/bin目录下又多了arm-linux-g++ 、arm-linux-c++等文件。 # ls $PREFIX/bin

arm-linux-addr2line arm-linux-g77 arm-linux-gnatbind arm-linux-ranlib arm-linux-ar arm-linux-gcc arm-linux-jcf-dump arm-linux-readelf arm-linux-as arm-linux-gcc-3.3.6 arm-linux-jv-scan arm-linux-size arm-linux-c++ arm-linux-gccbug arm-linux-ld arm-linux-strings arm-linux-c++filt arm-linux-gcj arm-linux-nm arm-linux-strip arm-linux-cpp arm-linux-gcjh arm-linux-objcopy grepjar

arm-linux-g++ arm-linux-gcov arm-linux-objdump jar

2.2.1.8测试交叉编译工具链

到此为止,已经介绍完了用分步构建的方法建立交叉工具链。下面通过一个简单的程序可以测试刚刚建立的交叉工具链看是否能够正常工作。写一个最简单的hello.c源文件,内容如下:

#include <stdio.h>

int main( )

{

printf(“Hello,world!\n”);

return 0;

}

通过以下命令进行编译,编译后生成名为hello的可执行文件。通过file命令可以查看文件的类型。具体操作如下,当显示以下信息时表明交叉工具链正常安装了,通过以下的编译生成了ARM体系可执行的文件。注意,通过该交叉编译链编译的可知性文件只能在ARM体系下执行,不能在基于X86的普通PC上执行该文件。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

# arm-linux-gcc –o hello hello.c

# file hello

hello: ELF 32-bit LSB executable, ARM, version 1 (ARM), for GNU/Linux 2.4.3, dynamically linked (uses shared libs), not stripped

2.2.2用Crosstool工具构建交叉工具链

Crosstool是一组脚本工具集,构建和测试不同版本的gcc和glibc,用于那些支持glibc

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

的许多体系结构。[www.61k.com]它也是一个开源项目,下载地址是:http://kegel.com/crosstool。用Crosstool构建交叉工具链要比上述的分步编译要容易得多,并且也方便许多,对于仅仅为了工作需要构建交叉编译工具链的读者建议使用此方法。用Crosstool工具构建所需资源如表2.2所示。 表2.2所需资源

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

2.2.2.1准备资源文件

首先从网上下载所需资源文件:linux-2.6.10.tar.gz,binutils-2.15.tar.bz2,gcc-3.3.6.tar.gz,glibc-2.3.2.tar.gz ,glibc-linuxthreads-2.3.2.tar.gz和linux-libc-headers-2.6.12.0.tar.bz2。然后将这些工具包文件放在新建的/home/mike/downloads目录下,最后在/home/mike目录下解压crosstool-0.42.tar.gz,命令如下:

# cd /home/mike

# tar –xvzf crosstool-0.42.tar.gz

2.2.2.2建立脚本文件

接着需要建立自己的编译脚本,起名为arm.sh,为了简化编写arm.sh,寻找一个最接近的脚本文件demo-arm.sh作为模版,然后将该脚本的内容复制到arm.sh,修改arm.sh脚本,具体操作如下:

# cd crosstool-0.42

# cp demo-arm.sh arm.sh

# vi arm.sh 修改后的arm.sh的脚本内容如下:

#!/bin/sh

set -ex

TARBALLS_DIR=/home/mike/downloads # 定义工具链源码所存放位置。

RESULT_TOP=/opt/crosstool # 定义工具链的安装目录

export TARBALLS_DIR RESULT_TOP

GCC_LANGUAGES="c,c++" # 定义支持C, C++语言

export GCC_LANGUAGES

# 创建/opt/crosstool目录

mkdir -p $RESULT_TOP

# 编译工具链,该过程需要数小时完成。

eval `cat arm.dat gcc-3.3.6-glibc-2.3.2.dat` sh all.sh --notest

echo Done.

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

2.2.2.3 建立配置文件

在arm.sh脚本文件中需要注意arm.dat和gcc-3.3.6-glibc-2.3.2.dat两个文件,这两个文件是作为crosstool的编译的配置文件。(www.61k.com]其中arm.dat文件内容如下,主要用于定义配置文件,定译生成编译工具链的名称以及定义编译选项等。 KERNELCONFIG=`pwd`/arm.config # 内核的配置

TARGET=arm-linux- # 编译生成的工具链名称

TARGET_CFLAGS="-O" # 编译选项

gcc-3.3.6-glibc-2.3.2.dat文件内容如下,该文件主要定义编译过程中所需要的库以及它定义的版本,如果当在编译过程中发现有些库不存在时,crosstool会自动在相关网站上下载,该工具在这点上相对非常智能,也非常有用。

BINUTILS_DIR=binutils-2.15

GCC_DIR=gcc-3.3.6

GLIBC_DIR=glibc-2.3.2

GLIBCTHREADS_FILENAME=glibc-linuxthreads-2.3.2

LINUX_DIR=linux-2.6.10

LINUX_SANITIZED_HEADER_DIR=linux-libc-headers-2.6.12.0

2.2.2.4 执行脚本

将Crosstool的脚本文件和配置文件准备好之后,开始执行arm.sh脚本来编译交叉变异工具。具体执行命令如下:

# cd crosstool-0.42

# ./arm.sh

经过数小时的漫长编译之后,会在/opt/crosstool目录下生成新的交叉编译工具,其中包括以下内容: arm-linux-addr2line arm-linux-g++ arm-linux-ld arm-linux-size

arm-linux-ar arm-linux-gcc arm-linux-nm arm-linux-strings arm-linux-as arm-linux-gcc-3.3.6 arm-linux-objcopy arm-linux-strip arm-linux-c++ arm-linux-gccbug arm-linux-objdump fix-embedded-paths arm-linux-c++filt arm-linux-gcov arm-linux-ranlib

arm-linux-cpp arm-linux-gprof arm-linux-readelf

2.2.2.5 添加环境变量

然后将生成的编译工具链路径添加到环境变量PATH上去,添加的方法是在系统/etc/ bashrc文件中添加下面一行在文件的最后,如图2.2所示。

export PATH=/opt/crosstool/gcc-3.3.6-glibc-2.3.2/arm-linux/bin:$PATH

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图2.2 用Vi编辑器在bashrc文件中添加环境变量

设置完环境变量,也就意味着交叉编译工具链已经构建完成,然后就可以用2.2.1.8节中的方法进行测试刚刚建立的工具链,此处就不用再赘述。(www.61k.com]

2.3本章小节

本章讲述的内容非常有实用价值,因为交叉工具链的构建是嵌入式系统开发必不可少的一部分,也是嵌入式系统开发的基础。本章首先对交叉工具链进行了大体地介绍,然后分别介绍两种构建交叉工具链的方法:分步构建法和Crosstool工具构建法。这两种构建交叉工具链的方法在实际应用中非常广泛,相信读者通过学习本章的内容可以构建一套自己的交叉编译工具链。下一章将介绍嵌入式系统的启动程序——Bootloader。

2.4常见问题

1.编译boot-trap gcc时出现如下图2.3错误,提示:crti.o: No such file: No such file or directory collect2: ld returned 1 exit status,为什么?

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图2.3:gcc工具编译出错界面

参考答案:由于在配置时没有选择 --disable-shared 选项,该选项的意思是只编译静态库。[www.61k.com)默认选项为--enable-shared,而libf2c 和libiberty 不支持共享库。

2.Glibc里面静态库和共享库有什么区别?

参考答案:应用程序在链接静态库时,会把引用到的数据和代码放到生成的可执行文件中,程序运行时就不再需要库了。应用程序链接共享库时,连接器不会把引用到的数据和代码放到可执行文件中,而仅仅做一个标记,当程序运行时,系统会去加载相应的共享库。链接共享库时,可执行文件的大小会小一些,但运行时依赖于共享库。起动静态库和共享库的方法分别是在配置时用 –disable-shared和—enable-shared选项。

3.本地编译器与交叉编译器的作用?

参考答案:编译器可以生成用来在与编译器本身所在的计算机和操作系统(平台)相同的环境下运行的目标代码,这种编译器叫做本地编译器。另外,编译器也可以生成用来在其它平台上运行的目标代码,这种编译器又叫做交叉编译器。交叉编译器在生成新的硬件平台时非常有用。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

第3章 嵌入式系统的BootLoader

本章学习目标:

l 了解BootLoader的作用

l 熟悉常见的嵌入式Linux BootLoader

l 熟悉S3C2410开发板

l 学会基于嵌入式系统的U-Boot移植

l 学会自己编写BootLoader

3.1 BootLoader概述

一个嵌入式Linux系统从软件的角度看通常分为四个层次:引导加载程序、Linux内核、文件系统、用户应用程序。[www.61k.com]

引导加载程序,是系统加电后运行的第一段代码。大家熟悉的PC中的引导程序一般由BIOS和位于MBR的操作系统BootLoader(例如LILO或者GRUB)一起组成。然而在嵌入式系统中通常没有像BIOS那样的固件程序,因此整个系统的加载启动任务就完全由BootLoader来完成。在嵌入式Linux中,引导加载程序即等效为BootLoader。简单地说,BootLoader就是在操作系统内核运行前执行地一段小程序。通过这段小程序,我们可以初始化必要的硬件设备,创建内核需要的一些信息并将这些信息通过相关机制传递给内核,从而将系统的软硬件环境带到一个合适的状态,最终调用操作系统内核,真正起到引导和加载内核的作用。

BootLoader是依赖于硬件而实现的,特别是在嵌入式系统中。不同体系结构需求的BootLoader是不同的;除了体系结构,BootLoader还依赖于具体的嵌入式板级设备的配置。也就是说,对于两块不同的嵌入式板而言,即使它们基于相同的CPU构建,运行在其中一块电路板上的BootLoader,未必能够运行在另一块电路开发板上。

Bootloader的启动过程可以是单阶段的,也可以是多阶段的。大多数单阶段的BootLoader应用于简单的系统,比如没有操作系统的系统。通常多阶段的BootLoader能提供更为复杂的功能,以及更好的可移植性。从固态存储设备上启动的BootLoader大多数是两阶段的启动过程,也就是启动过程可以分为stage 1和stage 2两部分。依赖于 CPU 体系结构的代码,比如设备初始化代码等,通常都放在 stage1 中,而且通常都用汇编语言来实现,以达到短小精悍的目的。而 stage2 则通常用C语言来实现,这样可以实现更复杂的功能,而且代码会具有更好的可读性和可移植性。

大多数BootLoader都包含两种不同的操作模式。启动加载(Boot loading)模式和下载(Down loading)模式,这种区别仅对于开发人员才有意义。但从最终用户的角度看,BootLoader的作用就是用来加载操作系统,而并不存在所谓的启动加载模式与下载工作模式的区别。

启动加载模式:这种模式也称为自主(Autonomous)模式,即BootLoader从目标机上的某个固态存储设备上将操作系统加载到RAM中运行,整个过程并没有用户的介入。这种模式是BootLoader的正常工作模式。因此在嵌入式产品发布的时候,BootLoader显然必须工作在这种模式下。

下载模式:在这种模式下 目标机上的BootLoader将通过串口连接或网络连接等通信手

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

段从主机上下载文件,比如:下载应用程序、数据文件、内核映像等。[www.61k.com]从主机下载的文件通常首先被BootLoader保存到目标机的RAM中然后再被BootLoader写到目标机上的固态存储设备中。BootLoader的这种模式通常在系统更新时使用。工作于这种模式下的BootLoader通常都会向它的终端用户提供一个简单的命令行接口。比如U-Boot,Blob,vivi等。

3.2常用的嵌入式Linux BootLoader

从上一节的内容可以了解到BootLoader是嵌入式系统中非常重要的一部分,也是系统运行工作的必要组成部分。在嵌入式系统中常见的BootLoader有以下几种:

3.2.1 U-Boot

U-Boot是德国DENX小组的开发用于多种嵌入式CPU的BootLoader程序,它可以运行在基于PowerPC,ARM,MIPS等多种嵌入式开发板上。从或ftp://ftp.denx.de/pub/u-boot/ 站点都可以下载U-Boot的源代码。U-Boot源代码主要目录解释如下:

board:目标板相关文件,主要包含SDRAM、FLASH驱动;

common:独立于处理器体系结构的通用代码,如内存大小探测与故障检测;

cpu:与处理器相关的文件。如mpc8xx子目录下含串口、网口、LCD驱动及中断初始化等文件;

driver:通用设备驱动,如CFI FLASH驱动(目前对INTEL FLASH支持较好); doc:U-Boot的说明文档;

examples:可在U-Boot下运行的示例程序;如hello_world.c,timer.c;

include:U-Boot头文件;尤其configs子目录下与目标板相关的配置头文件是移植过程中经常要修改的文件;

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

lib_xxx: 处理器体系相关的文件,如lib_ppc, lib_arm目录分别包含与PowerPC、ARM体系结构相关的文件;

net:与网络功能相关的文件目录,如bootp,nfs,tftp;

post:上电自检文件目录。尚有待于进一步完善;

rtc:RTC(Real Time Clock,实时时钟)驱动程序;

tools:用于创建U-Boot S-RECORD和BIN镜像文件的工具。

3.2.2 VIVI

VIVI是由韩国MIZI公司开发的专门用于ARM产品线的一种BootLoader。因为VIVI 目前只支持使用串口和主机通信,所以您必须使用一条串口电缆来连接目标板和主机。VIVI的源代码下载地址为:。VIVI一般有如下作用:

1)、 把内核(kernel)从flash复制到RAM,然后启动它

2)、 初始化硬件

3)、 下载程序并写入flash

4)、 检测目标板

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

vivi.tar.bz2源代码包解压后的目录结构如下所示:

# tree –L 1

.

|-- COPYING

|-- CVS

|-- Documentation

|-- Makefile

|-- Rules.make

|-- arch

|-- drivers

|-- include

|-- init

|-- lib

|-- scripts

|-- test

`-- util

10 directories, 3 files

其中VIVI主要目录介绍:

CVS:存放CVS工具相关的文件

Documentation:存放一些使用VIVI的帮助文档

arch:存放一些平台相关的代码文件

drivers:存放VIVI相关的驱动代码

include:存放所有VIVI源码的头文件

init:存放VIVI初始化代码

lib:存放VIVI实现的库函数文件

scripts:存放VIVI脚本配置文件

test:存放一些测试代码文件

util:存放一些Nand Flash写Image相关的实用文件。(www.61k.com]

3.2.3 Blob

Blob是Boot Loader Object的缩写,是一款功能强大的BootLoader。其源码在

Blob最初是由Jan-Derk Bakker和Erik Mouw两可以获取。

人为一块名为LART(Linux Advanced Radio Terminal)的板子写的,该板使用的处理器是StrongARM SA-1100,现在Blob已经被成功地移植到许多基于ARM的CPU上。

3.2.4 RedBoot

RedBoot是一个专门为嵌入式系统定制的引导启动工具,最初由Redhat开发,它是基于eCos(Embedded Configurable Operating System)的硬件抽象层,同时它继承了eCos的高可靠性,简洁性,可配置性和可移植性等特点。RedBoot集Bootloader、调试、Flash烧写于一体。支持串口、网络下载,执行嵌入式应用程序。既可以用在产品的开发阶段(调试功能),

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

也可以用在最终的产品上(Flash更新、网络启动)。[www.61k.com)RedBoot支持下载和调试应用程序,开发板可以通过BOOTP/DHCP协议动态配置IP地址,支持跨网段访问。用户可以通过tftp协议下载应用程序和image。或者通过串口用x-modem/y-modem下载。Redboot支持用GDB(the GNU debugger)通过串口或者网卡调试嵌入式程序。可对gcc编译的程序进行源代码级的调试。相比于简易jtag调试器,可靠、高速(CPU的Cache打开后,通过网卡tftp下在能达到1M bytes,GDB下载的速度能达到2M bps)、稳定。 用户可通过串口或网卡,以命令行的形式管理Flash上的image,下载image到flash。动态配置RedBoot启动的各种参数、启动脚本。上电后Redboot可自动从flash或tftp服务器上下载应用程序执行。RedBoot在http://sourceware.org/redboot站点可以下载其源码,同时可以了解更多地关于RedBoot详细信息,它在嵌入式系统应用中也非常广泛。

3.2.5 ARMboot

ARMboot是一个基于ARM或StrongARM 为内核CPU的嵌入式系统BootLoader固件程序。该软件的主要目标是使新的平台更容易被移植并且尽可能发挥其强大性能。它只基于ARM固件,但是它支持多种类型启动,比如flash,网络下载通过bootp,dhcp,tftp等。它也是开源项目,可以从http://www.sourceforge.net/projects/armboot网站可以获得最新的ARMboot源码和详细资料。它在ARM处理器方面应用非常广泛。

3.2.6 DIY

DIY(Do It Yourself),即自己制作。以上U-Boot,VIVI,Blob,RedBoot和ARMboot等成熟工具移植起来简单快捷,但同时他们都存在着一定的局限性,首先是因为它们是面向大部分硬件的工具,所以说在功能上要满足大部分硬件的需求,但一般情况下我们只需要特定的开发板相关的实现代码,其他型号开发板的实现代码对它来说是没有用的,所以通常它们的代码量较大。其次它们在使用上不够灵活,比如在这些工具上添加自己的特有功能相对比较困难,因为你必须熟悉该代码的组织关系,以及了解它的配置编译等文件。所以用DIY的方式自己编写针对目标板的BootLoader不但代码量短小,同时灵活性很大,最重要的是将来好维护。所以在实际嵌入式产品开发时大都选择DIY的方式编写BootLoader。

3.3基于S3C2410开发板的BootLoader实现

本节将以实例讲述基于S3C2410开发板的BootLoader的具体实现,主要分两个方面进行介绍,一是介绍基于U-Boot的移植,二是介绍DIY方式开发BootLoader。要移植或开发BootLoader首先要清楚具体的硬件系统,在这里就是要了解我们使用的目标板——S3C2410开发板。

3.3.1 S3C2410开发板介绍

本书中所设计的开发板相关的实例都是基于S3C2410开发板设计和测试的。S3C2410开发板是非常通用的一款ARM 9开发板,读者使用任何类型的ARM 9开发板都能参考书中的实例。对于S3C2410开发板基本配置如下:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

? CPU

采用三星的S3C2410 ARM920T,主频203MHz。[www.61k.com)集成有SDRAM内存控制器、NAND Flash控制器、SD卡控制器、USB Host、USB Device控制器、LCD控制器、IIC总线控制器、IIS控制器、SPI接口等多种接口。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

? 存储器

64Mbyte的SDRAM

64Mbyte的 NAND flash*(注1)

? 以太网控制器

10M网口,CS8900Q3,带联接和传输指示灯

? 串行接口

系统提供两个串行收发DB9母口连接器,上面分别表示COM0、COM1

? USB Host接口

两个USB1.1HOST接口

一个USB 1.1Device接口

? 存储接口

一个SD卡接口

一个十针的AD接口

一个IDE接口

? LCD和触摸屏接口

一个50芯LCD接口引出了LCD控制器和触摸屏的全部信号

TFT真彩LCD接口

提供真彩LCD的接口,LCD模块不需要外接电源等,插入该接口直接可以使用。接口另外还带触摸屏的接口

? 调试及下载接口

20针Multi-ICE 标准JTAG接口,支持SDT2.51和ADS1.2调试

? 音频接口

采用IIS接口芯片UDA1341,一路立体声音频输出接口可接耳机或音箱;支持录音,板子自带主机体话筒可直接录音,另有一路话筒输入接口可接麦克风

? 电源接口

5V电源供电,带电源开关和指示灯

? 操作系统

支持Linux 2.4或以上系统,支持WinCE4.2.net

开发板上包括1片64M ×8位数据宽度的NAND Flash(K9F1208)。和2片16M×16位数据宽度的SDRAM,地址范围为 0x30000000~0x34000000。S3C2410将系统的存储空间分为8组(Bank),每组大小为128MB,共1GB。Bank0到Bank5之间的开始地址是固定的,用于ROM或SRAM。Bank6和Bank7用于ROM,SRAM或SDRAM,这两个组是可编程且大小相同。S3C2410具有三种启动方式,通过OM[1:0]管脚进行选择:

OM[1:0] = 00时,处理器通过NAND Flash启动

OM[1:0] = 01时,处理器通过16位宽的ROM启动 OM[1:0] = 10时,处理器通过32位宽的ROM启动

*注1:NOR Flash和NAND Flash是现在市场上两种主要的非易失闪存技术。Intel于1988年首先开发出NOR flash技术,彻底改变了原先由EPROM和EEPROM一统天下的局面。紧接着,1989年,东芝公司发表了NAND flash结构,强调降低每比特的成本,更高的性能,并且象磁盘一样可以通过接口轻松升级。NOR Flash的特点是芯片内执行(XIP, eXecute In Place),这样应用程序可以直接在flash闪存内运行,不必再把代

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

码读到系统RAM中。(www.61k.com]NOR的传输效率很高,在1~4MB的小容量时具有很高的成本效益,但是很低的写入和擦除速度大大影响了它的性能。NAND结构能提供极高的单元密度,可以达到高存储密度,并且写入和擦除的速度也很快。应用NAND的困难在于flash的管理和需要特殊的系统接口。通常NOR的读速度比NAND稍快一些,而NAND的写入速度比NOR快很多。所以在设计中应该考虑这些情况。

由于NAND Flash容量大,比Nor Flash便宜等优势,所以经常选择NAND Flash启动。当从Nor Flash启动时,要把flash芯片的首地址映射到0x00000000位置,系统启动后,启动程序本身把自己从flash搬运到RAM中去

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

。当从NAND Flash启动时,S3C2410会自动把NAND Flash的前4K数据搬到自己内部的RAM中去,并把内部RAM的首地址设为0x00000000,CPU从0x00000000地址开始运行。本章选择的实现启动方式就是通过NAND Flash启动。图3.1所示为通过Nor Flash启动和NAND Flash启动两种方式存储空间分配。其中图a)是nGCS0片选的Nor Flash启动模式存储分配图,图b)是NAND Flash启动模式的存储分配图。其中SFR为Special Function Register的缩写,即特殊功能寄存器。

图3.1 NAND Flash的内存映射

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

3.3.2 U-Boot分析与移植

本章以应用非常广泛的U-Boot为例讲述基于S3C2410开发板的BootLoader分析与移植。[www.61k.com]解压u-boot-1.1.6.tar.bz2包,查看其目录结构如下所示: # tree –L 1 -d

.

|-- board

|-- common

|-- cpu

|-- disk

|-- doc

|-- drivers

|-- dtt

|-- examples

|-- fs

|-- include

|-- lib_arm

|-- lib_avr32

|-- lib_blackfin

|-- lib_generic

|-- lib_i386

|-- lib_m68k

|-- lib_microblaze

|-- lib_mips

|-- lib_nios

|-- lib_nios2

|-- lib_ppc

|-- nand_spl

|-- net

|-- post

|-- rtc

`-- tools

26 directories

大多数BootLoader都包含“启动加载”模式和“下载”模式,U-Boot作为一款强大的BootLoader也支持这两种工作模式,并且常常允许用户在这两种模式之间切换。同时U-Boot也分为Stage1和Stage2两个阶段。其中依赖于CPU体系结构的代码通常都放在Stage1里,并且通常用汇编语言实现。Stage2通常用C语言实现,可以实现更复杂的功能,并且有更好的移植性和可读性。

3.3.2.1 U-Boot Stage1分析

U-Boot的Stage1通常是在start.S文件中实现,并且都是用汇编语言编写。一个可执行

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

性image文件必须有一个入口点,并且只能有一个全局入口点,通常这个入口点的地址放在ROM(Flash)0x0位置,因此必须使编译器知道这个入口地址,该过程通常修改连接器的脚本文件来完成。(www.61k.com)此处以三星的smdk2410开发板为例,该开发板的U-Boot实现代码已经包含在里面。打开board/smdk2410/ u-boot.lds文件,该脚本文件的内容如下所示: OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")

OUTPUT_ARCH(arm)

ENTRY(_start)

SECTIONS

{

. = 0x00000000;

. = ALIGN(4);

.text :

{

cpu/arm920t/start.o (.text)

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

*(.text)

}

. = ALIGN(4);

.rodata : { *(.rodata) }

. = ALIGN(4);

.data : { *(.data) }

. = ALIGN(4);

.got : { *(.got) }

. = .;

__u_boot_cmd_start = .;

.u_boot_cmd : { *(.u_boot_cmd) }

__u_boot_cmd_end = .;

. = ALIGN(4);

__bss_start = .;

.bss : { *(.bss) }

_end = .;

}

其中,ENTRY(_start)定义了入口点在cpu/arm920t/start.S文件,入口地址为0x00000000。在cpu/arm920t/config.mk文件中定义了代码区基地址:TEXT_BASE = 0x33F80000。接下来分析U-Boot的Stage1的核心文件start.S。

? 设置异常向量表

对于ARM处理器一般包括复位、未定义指令、SWI、预取终止、数据终止、IRQ、FIQ等异常,关于ARM处理器这些异常在后面会有专门的介绍,其中U-Boot中关于异常向量的定义如下,当发生异常时执行cpu/arm920t/ interrupts.c文件。

.globl _start

_start: b reset

ldr pc, _undefined_instruction

ldr pc, _software_interrupt

ldr pc, _prefetch_abort

ldr pc, _data_abort

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

ldr pc, _not_used ldr pc, _irq ldr pc, _fiq

? 设置CPU模式为SVC模式

Reset,即复位,在系统中经常会用到,该操作是异常处理的第一个操作,其主要目的是设置CPU模式为SVC模式。(www.61k.com)在此有必要介绍一下ARM处理器的7种工作模式:

l 用户模式(usr):ARM处理器正常的程序执行状态

l 快速中断模式(fiq):用于高速数据传输或通道处理

l 外部中断模式(irq):用于通用的中断处理

l 管理模式(svc):操作系统使用的保护模式

l 数据访问终止模式(abt):当数据或指令预取终止时进入该模式,可用于虚拟

存储及存储保护。

l 系统模式(sys):运行具有特权的操作系统任务。

l 未定义指令中止模式(und):当未定义的指令执行时进入该模式,可用于支

持硬件协处理器的软件仿真。

ARM微处理器共有37个32位寄存器,其中31个为通用寄存器,6个为状态寄存器。但是这些寄存器不能被同时访问,具体哪些寄存器是可编程访问的,取决微处理器的工作状态及具体的运行模式。但在任何时候,通用寄存器R0~R14、程序计数器PC、一个或两个状态寄存器都是可访问的。通用寄存器包括R0~R15,可以分为三类:

l 未分组寄存器R0~R7:在所有的运行模式下,未分组寄存器都指向同一个

物理寄存器,他们未被系统用作特殊的用途, 因此,在中断或异常处理进行运行模式转换时,由于不同的处理器运行模式均使用相同的物理寄存器,可能会造成寄存器中数据的破坏,这一点在进行程序设计时应引起注意。 l 分组寄存器R8~R14:对于分组寄存器,他们每一次所访问的物理寄存器

与处理器当前的运行模式有关。对于R8~R12来说,每个寄存器对应两个不同的物理寄存器,当使用fiq模式时,访问寄存器R8_fiq~R12_fiq;当使用除fiq模式以外的其他模式时,访问寄存器R8_usr~R12_usr。 对于R13、R14来说,每个寄存器对应6个不同的物理寄存器,其中的一个是用户模式与系统模式共用,另外5个物理寄存器对应于其他5种不同的运行模式。 l 程序计数器PC(R15):在ARM状态下,位[1:0]为0,位[31:2]用于保存PC;

在Thumb状态下,位[0]为0,位[31:1]用于保存PC;虽然可以用作通用寄存器,但是有一些指令在使用R15时有一些特殊限制,若不注意,执行的结果将是不可预料的。在ARM状态下,PC的0和1位是0,在Thumb状态下,PC的0位是0。注意,Thumb状态下的寄存器集是ARM状态下寄存器集的一个子集,程序可以直接访问8个通用寄存器(R7~R0)、程序计数器(PC)、堆栈指针(SP)、连接寄存器(LR)和CPSR。同时,在每一种特权模式下都有一组SP、LR和SPSR。

设置CPU模式为SVC模式操作的具体实现代码如下,其中CPSR是Current Program Status Register的缩写,即当前程序状态寄存器,寄存器 R16 用作CPSR,CPSR可在任何运行模式下被访问,它包括条件标志位、中断禁止位、当前处理器模式标志位,以及其他一些相关的控制和状态位。

每一种运行模式下又都有一个专用的物理状态寄存器,称为SPSR是Saved

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

Program Status Register的缩写,即备份程序状态寄存器,当异常发生时,SPSR用于保存CPSR的当前值,从异常退出时则可由SPSR来恢复CPSR。(www.61k.com]关于CPU相关的寄存器设置需要参考S3C2410的用户手册来实现。

mrs r0,cpsr bic r0,r0,#0x1f orr r0,r0,#0xd3 msr cpsr,r0

? 关闭看门狗

看门狗,即watchdog timer,是一个定时器电路, 一般有一个输入叫喂狗,一个输出到MCU(Micro Controller Unit,多点控制单元)的RST端(复位端),MCU正常工作的时候,每隔一段时间输出一个信号到喂狗端,给 WDT(watchdog timer的简写)清零,如果超过规定的时间不喂狗,一般在程序跑飞时WDT 定时超过,就会给出一个复位信号到MCU,然后MCU复位。看门狗的作用就是防止程序发生死循环,或者说程序跑飞。根据S3C2410的用户手册,关闭看门狗的具体实现如下: #if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410)

ldr r0, =pWTCON

mov r1, #0x0

str r1, [r0]

? 禁止所有中断

在SVC模式下,不允许有任何中断发生,根据S3C2410的用户手册,通过设置相应的寄存器值的位来禁止中断,具体实现如下:

mov r1, #0xffffffff

ldr r0, =INTMSK

str r1, [r0]

# if defined(CONFIG_S3C2410)

ldr r1, =0x3ff

ldr r0, =INTSUBMSK

str r1, [r0]

# endif

? 设置CPU的频率

S3C2410的用户手册推荐FCLK:HCLK:PCLK = 1:2:4,其中FCLK默认是120MHz,通常FCLK用于CPU,HCLK用于AHB总线,PCLK用于APB总线。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

ldr r0, =CLKDIVN mov r1, #3 str r1, [r0]

? 设置CP15寄存器

CP15是系统控制协处理器寄存器,用于连接在内存中的页表描述符,此外还用于决定对MMU的操作。设置CP15寄存器的目的是失效ICache(指令Cache)和DCache(数据Cache),然后禁止MMU和Cache。

cpu_init_crit:

/* 失效I/D caches*/

mov r0, #0

mcr p15, 0, r0, c7, c7, 0 /* flush v3/v4 cache */

mcr p15, 0, r0, c8, c7, 0 /* flush v4 TLB */

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

/* 禁止MMU和Caches*/ mrc p15, 0, r0, c1, c0, 0 bic r0, r0, #0x00002300 bic r0, r0, #0x00000087 orr r0, r0, #0x00000002 orr r0, r0, #0x00001000 mcr p15, 0, r0, c1, c0, 0 @ clear bits 13, 9:8 (--V- --RS) @ clear bits 7, 2:0 (B--- -CAM) @ set bit 2 (A) Align @ set bit 12 (I) I-Cache

? 配置内存控制寄存器

配置内存控制寄存器一般是和开发板紧密相关的,寄存器的具体值由开发板商或硬件工程师提供。[www.61k.com)如果你对总线周期和外围芯片非常熟悉,也可以自己定义。在U-Boot中的设置文件是board/smdk2400/lowlevel_init.S,该文件包含lowlevel_init程序段用于内存控制配置。在start.S中的相关实现如下:

mov ip, lr bl lowlevel_init mov lr, ip mov pc, lr

? 配置栈空间

配置代码段的开始地址,动态内存区长度,全局数据的大小以及分配IRQ和FRQ的栈空间。 stack_setup:

ldr r0, _TEXT_BASE /* upper 128 KiB: relocated uboot */

sub r0, r0, #CFG_MALLOC_LEN /* malloc area */ sub r0, r0, #CFG_GBL_DATA_SIZE /* bdinfo

#ifdef CONFIG_USE_IRQ

sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ) #endif

sub sp, r0, #12 /* leave 3 words for abort-stack */

? BSS段清零

BSS(Block Started by Symbol的简称)段是可执行性文件中的一种数据段。通常ARM编译器生成的可执行性文件由两部分数据组成,分别是代码段和数据段。代码段又分为可执行代码段(text)和只读数据段(rodata)。数据段又分为初始化数据段(data)和未初始化数据段(bss)。清除BSS段的具体实现如下:

clear_bss:

ldr r0, _bss_start /* find start of bss segment */

ldr r1, _bss_end /* stop here */

mov r2, #0x00000000 /* clear */ clbss_l:str r2, [r0] /* clear loop... */

add r0, r0, #4

cmp r0, r1

ble clbss_l

? 拷贝NAND Flash代码到RAM

从NAND Flash把数据拷贝到RAM,在U-Boot中没有相关的实现,这里可以参考VIVI源代码的实现,利用copy_myself函数实现,具体实现将在U-Boot移植一节中详细介绍。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

? 进入C代码 进入C代码的实现很简单,利用ldr指令实现到C代码地址的装载,具体实现如下: ldr pc, _start_armboot

_start_armboot: .word start_armboot

到这里已经介绍完了U-Boot Stage1的所有主要实现,接下来讲述U-Boot Stage2实现的内容。[www.61k.com)

3.3.2.2 U-Boot Stage2分析

Stage2部分全是用C语言实现,可读性较强,并且可以实现较为复杂的功能。通过上节可以看出Stage1最后调用的函数名是start_armboot,同时这个函数也就是Stage2的入口函数,其定义在lib_arm/ board.c文件中,该文件主要实现的内容有: ? 定义初始化函数表

init_fnc_t *init_sequence[] = {

cpu_init, /* 基本CPU相关的设置*/

board_init, /*基本开发板相关的设置*/

interrupt_init, /* 设置异常*/

env_init, /*初始化环境变量*/

init_baudrate, /* 初始化波特率*/

serial_init, /* 串行通信的设置*/

console_init_f, /*初始化控制台的stage 1 */

display_banner, /* 通知代码已经运行在何处*/

#if defined(CONFIG_DISPLAY_CPUINFO)

print_cpuinfo, /* 打印CPU相关的信息*/

#endif

#if defined(CONFIG_DISPLAY_BOARDINFO)

checkboard, /* 现实开发板相关的信息*/

#endif

dram_init, /* 配置有效地ram 组*/

display_dram_config,

NULL,

};

? 配置可用的Flash区

利用flash_init ()函数来配置可用的Flash区

? 初始化内存分配

利用mem_malloc_init ()函数来初始化内存分配

? NAND Flash初始化

利用nand_init()函数初始化NAND Flash

? 初始化环境变量

利用env_relocate ()函数来初始化环境变量

? 初始化外围设备

利用devices_init ()函数来初始化外围设备

? 使能中断

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

?

?

?

? 利用enable_interrupts ()函数来使能中断 初始化网卡 利用cs8900_get_enetaddr()函数来初始化cs8900网卡 初始化I2C总线 I2C是飞利浦公司研发的一种片内通信总线技术,利用i2c_init()函数来初始化,具体实现在cpu/arm920t/s3c24x0/i2c.c文件中。[www.61k.com) 初始化LCD 利用drv_lcd_init()函数来实现LCD的初始化,具体实现在common/lcd.c文件中。 进入U-Boot的命令循环

该过程进入U-Boot的命令循环,接受用户输入的命令,然后进行相应的工作。具体实现如下: for (;;)

{

main_loop (); /*具体实现参见common/main.c文件*/

}

上述讲述了U-Boot 在Stage2主要所做的工作,当然上述给出的只是一般情况下的,可以根据具体的开发板相应的增加或减少实现代码。下面将介绍基于S3C2410开发板的U-Boot移植。

3.3.2.3 U-Boot的移植过程

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

前面两节对U-Boot已经有了较为详细地介绍,本节开始讲述一个具体的U-Boot移植实例,该实例的硬件环境是基于前面所述的S3C2410开发板。

软件环境:

l u-boot-1.1.3.tar.bz2(从下载)

l arm-linux-gcc 3.3.6(根据第二章内容自己构建或从直接下载) 准备好软、硬件环境后就开始真正的U-Boot移植工作了,下面将一步一步介绍移植的具体过程。

1.修改Makefile

首先给要建立的S3C2410开发板取名为mike2410,因为U-Boot的移植过程需要它。修改Makefile的具体操作如下:

# tar –xvjf u-boot-1.1.3.tar.bz2

# cd u-boot-1.1.3

# vi Makefile

由于mike2410开发板和三星的smdk2410开发板很相似,所以在移植U-Boot时大部分源代码都以smdk2410为模版,然后在此基础上进行修改。此处修改Makefile如图3.2所示,在Makefile中添加了如下内容:

mike2410_config : unconfig @$(MKCONFIG) $(@:_config=) arm arm920t mike2400 NULL s3c24x0

其中,mike2400_config:unconfig意思是为mike2410开发板建立一个编译项;第二行中arm 的意思是CPU的架构是基于ARM体系的;arm920t的意思是CPU的类型是arm920t;mike2410 的意思是开发版的型号;NULL的意思是开发者或经销商的名称为空;s3c24x0的意思是基于s3c24x0的片上系统。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图3.2:修改U-Boot的Makefile

2.建立mike2410开发板目录

在board目录下建立mike2410开发板子目录,目前board目录下已经有两百多种针对不同开发板的子目录,由于mike2410开发板最接近smdk2410开发板,所以可以用下面的简单方法建立mike2410开发板目录: # cp –fr board/ smdk2410 board/ mike2410

# cd board/ mike2410

# mv smdk2410.c mike2410.c

同时需要修改board/mike2410/Makefile文件,修改如图3.3所示,修改的内容是 将COBJS := smdk2410.o flash.o

改为COBJS := mike2410.o flash.o

由于board/mike2410目录下的源代码文件为mike2410.c,所以编译中间文件为mike2410.o,如果没有修改这个Makefile文件编译就会出错。(www.61k.com]

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图3.3 修改mike2410目录下的Makefile文件

3.建立配置头文件 在include/configs目录下建立mike2410.h头文件,建立方法如下:

# cd include/configs

# cp -fr smdk2410.h mike2410.h

4.指定交叉编译器的路径

在/etc/bashrc文件中添加下面一行来指定交叉编译器的路径,这个路径不是绝对的,要根据交叉编译工具链所放的位置决定。[www.61k.com]

export PATH=/opt/crosstool/gcc-3.3.6-glibc-2.3.2/arm-linux/bin:$PATH

5.测试编译

这个步骤的目的一是为了检查交叉编译器是否正常工作,二是为了测试新建的mike2410配置项是否能够正常编译,具体操作如下:

# cd u-boot-1.1.6

# make mike2410_config

# make CROSS_COMPILE=arm-linux-

如果编译正确,将在u-boot-1.1.6目录下生成u-boot、u-boot.bin和u-boot.srec三个映像文件。其中:u-boot是ELF格式二进制的image文件,u-boot.bin是原始的二进制image文件,u-boot.srec是Motorola S-Record格式的image文件。到这一步说明建立好了mike2410的U-Boot编译项,但是具体的实现部分还需要修改,因为现在的实现代码还是完全和smdk2410开发板一样的,而不能工作在mike2410开发板上,接下来的步骤就是根据mike2410开发板的硬件配置和设计要求来进一步移植。

6.修改start.S文件

首先在ldr pc, _start_armboot一行之前添加下面内容,由于U-Boot中没有支持从NAND Flash启动,所以将程序自己复制到DRAM里面去需要新加代码实现,一般通过

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

copy_myself函数来实现,其参考VIVI的copy_myself代码。[www.61k.com)

#ifdef CONFIG_S3C2410_NAND_BOOT

bl copy_myself

@ jump to ram

ldr r1, =on_the_ram

add pc, r1, #0

nop

nop

1: b 1b @ infinite loop

on_the_ram:

#endif

然后在_start_armboot: .word start_armboot一行之后添加下面内容,该部分的内容基本上都参考VIVI代码实现,这段代码的主要目的是搬运NAND Flash数据到DRAM里面。

#ifdef CONFIG_S3C2410_NAND_BOOT

copy_myself:

mov r10, lr

@ reset NAND

mov r1, #NAND_CTL_BASE

ldr r2, =0xf830 @ initial value

str r2, [r1, #oNFCONF]

ldr r2, [r1, #oNFCONF]

bic r2, r2, #0x800 @ enable chip

str r2, [r1, #oNFCONF]

mov r2, #0xff @ RESET command

strb r2, [r1, #oNFCMD]

mov r3, #0 @ wait

1: add r3, r3, #0x1

cmp r3, #0xa

blt 1b

2: ldr r2, [r1, #oNFSTAT] @ wait ready

tst r2, #0x1

beq 2b

ldr r2, [r1, #oNFCONF]

orr r2, r2, #0x800 @ disable chip

str r2, [r1, #oNFCONF]

@ get read to call C functions (for nand_read())

ldr sp, DW_STACK_START @ setup stack pointer

mov fp, #0 @ no previous frame, so fp=0

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

@ copy vivi to RAM

ldr r0, =UBOOT_RAM_BASE

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

mov r1, #0x0

mov r2, #0x30000

bl nand_read_ll

tst r0, #0x0

beq ok_nand_read

#ifdef CONFIG_DEBUG_LL

bad_nand_read:

ldr r0, STR_FAIL

ldr r1, SerBase

bl PrintWord

1: b 1b @ infinite loop

#endif

ok_nand_read:

#ifdef CONFIG_DEBUG_LL

ldr r0, STR_OK

ldr r1, SerBase

bl PrintWord

#endif

@ verify

mov r0, #0

ldr r1, =UBOOT_RAM_BASE

mov r2, #0x400 @ 4 bytes * 1024 = 4K-bytes go_next:

ldr r3, [r0], #4

ldr r4, [r1], #4

teq r3, r4

bne notmatch

subs r2, r2, #4

beq done_nand_read

bne go_next

notmatch:

#ifdef CONFIG_DEBUG_LL

sub r0, r0, #4

ldr r1, SerBase

bl PrintHexWord

ldr r0, STR_FAIL

ldr r1, SerBase

bl PrintWord

#endif

1: b 1b

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

done_nand_read:

#ifdef CONFIG_DEBUG_LL

ldr r0, STR_OK

ldr r1, SerBase

bl PrintWord

#endif

mov pc, r10

@ clear memory

@ r0: start address

@ r1: length

mem_clear:

mov r2, #0

mov r3, r2

mov r4, r2

mov r5, r2

mov r6, r2

mov r7, r2

mov r8, r2

mov r9, r2

clear_loop:

stmia r0!, {r2-r9}

subs r1, r1, #(8 * 4)

bne clear_loop

mov pc, lr

#endif @ CONFIG_S3C2410_NAND_BOOT

接着在start.S文件最后添加下面几行的内容,用于定义栈地址变量。(www.61k.com]到这里start.S文件已经修改完毕,接着添加NAND Flash读函数。

#ifdef CONFIG_S3C2410_NAND_BOOT

.align 2

DW_STACK_START:

.word STACK_BASE+STACK_SIZE-4

#endif

7.添加nand_read.c和nandflash.h源文件

在start.S文件中调用了nand_read_ll函数,该函数用于NAND Flash读操作,在U-Boot中没有定义,需要新加该函数的实现,该函数的实现可以参考VIVI源代码,在board/mike2410目录下新建nand_read.c源文件,文件内容下:

#include <config.h>

#define __REGb(x) (*(volatile unsigned char *)(x))

#define __REGi(x) (*(volatile unsigned int *)(x))

#define NF_BASE 0x4e000000

#define NFCONF __REGi(NF_BASE + 0x0)

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

#define NFCMD __REGb(NF_BASE + 0x4)

#define NFADDR __REGb(NF_BASE + 0x8)

#define NFDATA __REGb(NF_BASE + 0xc)

#define NFSTAT __REGb(NF_BASE + 0x10)

#define BUSY 1

inline void wait_idle(void) {

int i;

while(!(NFSTAT & BUSY))

for(i=0; i<10; i++);

}

#define NAND_SECTOR_SIZE 512

#define NAND_BLOCK_MASK (NAND_SECTOR_SIZE - 1)

/* low level nand read function */

int nand_read_ll(unsigned char *buf, unsigned long start_addr, int size)

{

int i, j;

if ((start_addr & NAND_BLOCK_MASK) || (size & NAND_BLOCK_MASK)) { return -1; /* invalid alignment */

}

/* chip Enable */

NFCONF &= ~0x800;

for(i=0; i<10; i++);

for(i=start_addr; i < (start_addr + size);) {

/* READ0 */

NFCMD = 0;

/* Write Address */

NFADDR = i & 0xff;

NFADDR = (i >> 9) & 0xff;

NFADDR = (i >> 17) & 0xff;

NFADDR = (i >> 25) & 0xff;

wait_idle();

for(j=0; j < NAND_SECTOR_SIZE; j++, i++) {

*buf = (NFDATA & 0xff);

buf++;

}

}

/* chip Disable */

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

NFCONF |= 0x800; /* chip disable */

return 0;

}

新建完nand_read.c文件后,需要修改相同目录下的Makefile文件,否则编译会出错,修改内容为:

将COBJS := mike2410.o flash.o

改为:COBJS := mike2410.o flash.o nand_read.o

同时需要在board/mike2410目录下添加nandflash.h文件,该文件主要定义了NAND Flash的一些芯片配置函数,具体代码如下:

#include <s3c2410.h>

#if (CONFIG_COMMANDS & CFG_CMD_NAND)

typedef enum {

NFCE_LOW,

NFCE_HIGH

} NFCE_STATE;

static inline void NF_Conf(u16 conf)

{

S3C2410_NAND * const nand = S3C2410_GetBase_NAND();

nand->NFCONF = conf;

}

static inline void NF_Cmd(u8 cmd)

{

S3C2410_NAND * const nand = S3C2410_GetBase_NAND();

nand->NFCMD = cmd;

}

static inline void NF_CmdW(u8 cmd)

{

NF_Cmd(cmd);

udelay(1);

}

static inline void NF_Addr(u8 addr)

{

S3C2410_NAND * const nand = S3C2410_GetBase_NAND();

nand->NFADDR = addr;

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

}

static inline void NF_SetCE(NFCE_STATE s)

{

S3C2410_NAND * const nand = S3C2410_GetBase_NAND();

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

switch (s) {

case NFCE_LOW:

nand->NFCONF &= ~(1<<11);

break;

case NFCE_HIGH:

nand->NFCONF |= (1<<11);

break;

}

}

static inline void NF_WaitRB(void)

{

S3C2410_NAND * const nand = S3C2410_GetBase_NAND();

while (!(nand->NFSTAT & (1<<0)));

}

static inline void NF_Write(u8 data)

{

S3C2410_NAND * const nand = S3C2410_GetBase_NAND();

nand->NFDATA = data; }

static inline u8 NF_Read(void)

{

S3C2410_NAND * const nand = S3C2410_GetBase_NAND();

return(nand->NFDATA);

}

static inline void NF_Init_ECC(void)

{

S3C2410_NAND * const nand = S3C2410_GetBase_NAND();

nand->NFCONF |= (1<<12);

}

static inline u32 NF_Read_ECC(void)

{

S3C2410_NAND * const nand = S3C2410_GetBase_NAND();

return(nand->NFECC);

}

#endif

8.修改mike2410.c文件

修改这个文件的主要目的有两个,一是初始化CPU相关寄存器来支持USB主、从设备;二是初始化NAND Flash设备。[www.61k.com]添加代码如下:

#include "nandflash.h" //添加该头文件

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

int board_init (void)

{

……

/* 根据S3C2410用户手册,设置相应的寄存器值来支持USB*/

gpio->MISCCR |= (1<<3);

gpio->MISCCR &= ~((1<<12)|(1<<13));

……

}

/* 添加以下代码实现NAND flash的初始化*/

#if (CONFIG_COMMANDS & CFG_CMD_NAND)

extern ulong nand_probe(ulong physadr);

static inline void NF_Reset(void)

{

int i;

NF_SetCE(NFCE_LOW);

NF_Cmd(0xFF); /* reset command */

for(i = 0; i < 10; i++); /* tWB = 100ns. */

NF_WaitRB(); /* wait 200~500us; */

NF_SetCE(NFCE_HIGH);

}

static inline void NF_Init(void)

{

#if 0 /* a little bit too optimistic */

#define TACLS 0

#define TWRPH0 3

#define TWRPH1 0

#else

#define TACLS 0

#define TWRPH0 4

#define TWRPH1 2

#endif

NF_Conf((1<<15)|(0<<14)|(0<<13)|(1<<12)|(1<<11)|(TACLS<<8)|(TWRPH0<<4)|(TWRPH1<<0));

NF_Conf((1<<15)|(0<<14)|(0<<13)|(1<<12)|(1<<11)|(TACLS<<8)|(TWRPH0<<4)|(TWRPH1<<0));

}

void nand_init(void)

{

S3C2410_NAND * const nand = S3C2410_GetBase_NAND();

NF_Init();

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

#ifdef DEBUG

printf("NAND flash probing at 0x%.8lX\n", (ulong)nand);

#endif

printf ("%4lu KB\n", nand_probe((ulong)nand) >> 10);

}

#endif

9.修改头文件mike2410.h

修改include/configs/ mike2410.h文件,在该文件中添加如下内容,其中定义了栈的基地址和、栈的大小、RAM的基地址以及定义NAND Flash设置参数等内容。[www.61k.com)

……

#define CONFIG_CMDLINE_TAG 1

#define CONFIG_SETUP_MEMORY_TAGS 1

#define CONFIG_INITRD_TAG 1

……

#define CONFIG_COMMANDS \

(CONFIG_CMD_DFL | \

CFG_CMD_CACHE | \

CFG_CMD_ENV | \

CFG_CMD_PING | \

CFG_CMD_NAND | \

/*CFG_CMD_EEPROM |*/ \

/*CFG_CMD_I2C |*/ \

CFG_CMD_REGINFO | \

CFG_CMD_ELF)

……

/* Nandflash Boot */

#define CONFIG_S3C2410_NAND_BOOT 1

#define STACK_BASE 0x33ff8000

#define STACK_SIZE 0x8000

#define UBOOT_RAM_BASE 0x33f00000

/* NAND Flash Controller */

#define NAND_CTL_BASE 0x4E000000

#define bINT_CTL(Nb) __REG(INT_CTL_BASE + (Nb))

/* Offset */

#define oNFCONF 0x00

#define oNFCMD 0x04

#define oNFADDR 0x08

#define oNFDATA 0x0c

#define oNFSTAT 0x10

#define oNFECC 0x14

/* 以下代码是定义NAND flash 设置参数*/

#if (CONFIG_COMMANDS & CFG_CMD_NAND)

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

#define CFG_MAX_NAND_DEVICE 1 /* Max number of NAND devices */ #define SECTORSIZE 512

#define ADDR_COLUMN 1

#define ADDR_PAGE 2

#define ADDR_COLUMN_PAGE 3

#define NAND_ChipID_UNKNOWN 0x00

#define NAND_MAX_FLOORS 1

#define NAND_MAX_CHIPS 1

#define NAND_WAIT_READY(nand) NF_WaitRB()

#define NAND_DISABLE_CE(nand) NF_SetCE(NFCE_HIGH)

#define NAND_ENABLE_CE(nand) NF_SetCE(NFCE_LOW)

#define WRITE_NAND_COMMAND(d, adr) NF_Cmd(d)

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

#define WRITE_NAND_COMMANDW(d, adr) NF_CmdW(d)

#define WRITE_NAND_ADDRESS(d, adr) NF_Addr(d)

#define WRITE_NAND(d, adr) NF_Write(d)

#define READ_NAND(adr) NF_Read()

/* the following functions are NOP's because S3C24X0 handles this in hardware */

#define NAND_CTL_CLRALE(nandptr)

#define NAND_CTL_SETALE(nandptr)

#define NAND_CTL_CLRCLE(nandptr)

#define NAND_CTL_SETCLE(nandptr)

#define CONFIG_MTD_NAND_VERIFY_WRITE 1

#define CONFIG_MTD_NAND_ECC_JFFS2 1

#endif /* CONFIG_COMMANDS & CFG_CMD_NAND */ 然后修改以下各宏的值,包括内存相关的地址、启动提示字母和网络的设置。(www.61k.com)

#define CFG_MEMTEST_END 0x33F00000 /* 64 MB in DRAM */ #define CFG_LOAD_ADDR 0x30008000 /* default load address */ #define PHYS_SDRAM_1_SIZE 0x04000000 /*64 MB */

#define CFG_PROMPT "MIKE2410 # " /* Monitor Command Prompt */ #define CONFIG_IPADDR 192.168.1.10

#define CONFIG_SERVERIP 192.168.1.1

#define CFG_ENV_IS_IN_NAND 1

#define CFG_ENV_OFFSET 0x30000

10.修改cmd_nand.c文件

cmd_nand.c文件在commom目录下,修改该文件的目的就是添加包含nandflash.h的头文件,因为如果没有包含该头文件,编译时将出错如图3.4所示,正是因为该文件调用了nandflash.h头文件中的变量而没有包含该头文件引起的,包含该头文件后将编译正确。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图3.4 没有包含nandflash.h引起的错误

11.重新编译

如果以上步骤都正确的话,执行下面的编译命令,将会在U-Boot的根目录下重新生成u-boot.bin可执行性文件。(www.61k.com)

# make all ARCH=arm CROSS_COMPILE = arm-linux-

12.用Jtag烧写u-boot.bin文件

用开发板上自带的Jtage线和开发板连接,使用三星公司提供的SJF(Sec Jtag Flash)工具将上面生成的u-boot.bin文件烧写到开发板上,烧写过程如图3.5所示。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图3.5 用SJF烧写U-Boot的过程

正确烧写完u-boot.bin文件后,运行超级终端或DNW软件,DNW是三星公司提供的一个使用方便的串口调试软件,断开Jtag线,连上串口线,给开发板上电,将在屏幕中显示如下信息,表明U-Boot正常启动了。(www.61k.com)

U-Boot 1.1.3 (Jan 21 2007 - 20:26:48)

U-Boot code: 33F80000 -> 33F99A28 BSS: -> 33F9DD4C

RAM Configuration:

Bank #0: 30000000 64 MB

Flash: 512 kB

NAND:65536 KB

*** Warning - bad CRC or NAND, using default environment

In: serial

Out: serial

Err: serial

MIKE2410#

U-Boot提供了许多命令,通常使用help或“?”来查看U-Boot的所有命令,查看结果如下。

MIKE2410# help

? - alias for 'help'

autoscr - run script from memory

base - print or set address offset

bdinfo - print Board Info structure

boot - boot default, i.e., run 'bootcmd'

bootd - boot default, i.e., run 'bootcmd'

bootelf - Boot from an ELF image in memory

bootm - boot application image from memory

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

bootp - boot image via network using BootP/TFTP protocol

bootvx - Boot vxWorks from an ELF image

cmp - memory compare

coninfo - print console devices and information

cp - memory copy

crc32 - checksum calculation

dcache - enable or disable data cache

echo - echo args to console

erase - erase FLASH memory

flinfo - print FLASH memory information

go - start application at address 'addr'

help - print online help

icache - enable or disable instruction cache

iminfo - print header information for application image

imls - list all images found in flash

itest - return true/false on integer compare

loadb - load binary file over serial line (kermit mode)

loads - load S-Record file over serial line

loop - infinite loop on address range

md - memory display

mm - memory modify (auto-incrementing)

mtest - simple RAM test

mw - memory write (fill)

nand - NAND sub-system

nboot - boot from NAND device

nfs - boot image via network using NFS protocol

nm - memory modify (constant address)

ping - send ICMP ECHO_REQUEST to network host

printenv- print environment variables

protect - enable or disable FLASH write protection

rarpboot- boot image via network using RARP/TFTP protocol

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

reset - Perform RESET of the CPU

run - run commands in an environment variable

saveenv - save environment variables to persistent storage

setenv - set environment variables

sleep - delay execution for some time

tftpboot- boot image via network using TFTP protocol

version - print monitor version

以下将介绍U-Boot常用命令:

1.?:得到所有命令列表,和help命令作用相同

2.bdinfo:打印开发板信息,以本文开发板为例,使用该命令可以看到如下信息:

MIKE2410# bdinfo

arch_number = 0x000000C1 //CPU体系结构编号

env_t = 0x00000000 //环境变量

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

boot_params = 0x30000100 //启动引导参数

DRAM bank = 0x00000000 //内存区

-> start = 0x30000000 //SDRAM起始地址

-> size = 0x04000000 //SDRAM大小

ethaddr = 01:23:45:67:89:AB //以太网地址(自己可以设定)

ip_addr = 192.168.1.10 //IP地址

baudrate = 115200 bps //波特率大小

3.tftp(tftpboot):利用tftp协议将PC本地上的映象文件下载到指定地址的SDRAM中,执行该命令的前提是所用PC上已经安装了tftp服务。[www.61k.com)具体使用方法如下:

MIKE2410# tftp 0x30008000 zImage

TFTP from server 192.168.1.5; our IP address is 192.168.1.10 Filename 'zImage'.

Load address: 0x30008000

Loading: ################################################################# ################################################################# ############################################

done

Bytes transferred = 890752 (d9780 hex)

其中,0x30008000为指定下载到SDRAM的地址,zImage为下载映象的文件名。 4.bootm:从内核的入口地址引导内核。具体使用方法如下:

MIKE2410# bootm 0x30008000

## Booting image at 30008000 ... Starting kernel ...

Uncompressing Linux...................................................................................done, booting the kernel.

5.go:直接跳转到可执行性文件的入口地址,执行可执行性文件。使用方法如下:

MIKE2410# go 0x30008000 ## Starting application at 0x30008000 ...

6.printenv:打印环境变量信息。使用方法如下:

MIKE2410# printenv

bootdelay=3

baudrate=115200

ipaddr=192.168.1.10

serverip=192.168.1.5

netmask=255.255.255.0

stdin=serial

stdout=serial

stderr=serial

Environment size: 132/65532 bytes

7.setenv:设置环境变量,使用方法如下:

MIKE2410# setenv ipaddr 192.168.1.2

其中,ipaddr为要设置的环境变量名,192.168.1.2为要设置的环境变量值。

8.nand read:从NAND Flash的具体地址读数据到内存中去,具体使用如下:

MIKE2410# nand read 0x30008000 0 0x100000

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

NAND read: device 0 offset 0, size 1048576 ... 1048576 bytes read: OK

其中,0x30008000为要读到内存的地址,0为要读取的NAND Flash的地址,0x100000为要读取数据的大小。[www.61k.com) 9.nand erase:擦除NAND Flash指定地址的数据,具体使用如下:

MIKE2410# nand erase 0x100000 0x20000

NAND erase: device 0 offset 1048576, size 131072 ... OK

其中,0x100000为指定擦除NAND Flash的起始地址,0x20000擦除数据块的大小。

10.nand write:给NAND Flash中写数据,具体使用如下:

MIKE2410# nand write 0x30200000 0x100000 0x20000

NAND write: device 0 offset 1048576, size 131072 ... 131072 bytes written: OK

其中,0x30200000为内存的起始地址,0x100000为要写的NAND Flash的起始地址,0x20000要写数据的大小。注意,在给NAND Flash写数据前必须先执行擦除操作,否则给NAND Flash写数据会失败。

3.4基于S3C2410开发板自己编写BootLoader

相对于U-Boot移植来说,这种DIY的方法要复杂一些,不过和第二章中介绍分步构建交叉编译工具链一样,通过自己编写BootLoader更能清楚的知道它的工作原理,从而对于深入的学习嵌入式系统开发非常有好处。为了让读者全面学习BootLoader,在本书附件的光盘中附有本节讲述的参考BootLoader实现代码,对于类似的开发板只需要做简单的修改就可以使用。

3.4.1 设计系统的启动流程

系统加电复位后,几乎所有的 CPU都从由复位地址上取指令。比如,基于 ARM920T或ARM7TDMI内核的CPU在复位时通常都从地址 0x00000000处取它的第一条指令。而以微处理器为核心的嵌入式系统通常都有某种类型的固态存储设备(比如EEPROM、FLASH等)被映射到这个预先设置好的地址上。因此在系统加电复位后,处理器将首先执行存放在复位地址处的程序。通过集成开发环境可以将Bootloader定位在复位地址开始的存储空间内,如图3.4用ADS集成开发工具中的ARM Linker设置选项定义RO Base为0x30200000,它的意思就是定义了BootLoader的入口地址,即_ENTRY地址。因此Bootloader在系统加电后、操作系统内核或应用程序运行之前,首先必须运行。对于嵌入式系统来说,有的使用操作系统,也有的不使用操作系统,比如功能简单仅包括应用程序的系统,但是无论你是否使用操作系统,在系统启动时都必须执行Bootloader,为系统运行准备好软硬件运行环境。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图3.4 用ADS工具定义BootLoader的复位地址

系统的启动通常有两种方式:一种是可以直接从Nor Flash启动,另一种是可以将压缩的内存映像文件从Flash(为节省Flash资源、提高速度)中复制、解压到RAM,再从RAM启动。(www.61k.com)这里还是以第二种方式为例进行讲解,当电源打开时,一般的系统会去执行ROM(应用较多的是Flash)里面的启动代码。这些代码是用汇编语言编写的,其主要作用在于初始化CPU和板上的必备硬件如内存、中断控制器等。有时候用户还必须根据自己板子的硬件资源情况做适当的调整与修改。

BootLoader完成基本软硬件环境初始化后,对于有操作系统的情况下,启动操作系统、启动内存管理、任务调度、加载驱动程序等,最后执行应用程序或等待用户命令;对于没有操作系统的系统直接执行应用程序或等待用户命令。在商业的实时操作系统中,启动代码部分一般被称为板级支持包,英文缩写为BSP(Board Support Package)。它的主要功能就是:电路初始化和为高级语言编写的软件运行做准备。基于S3C2410开发板系统的启动流程设计如图3.5所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图3.5基于S3C2410开发板系统的启动流程设计图

接下来讲述基于S3C2410开发板系统的BootLoader具体实现,将根据设计的启动流程逐步进行具体讲述。(www.61k.com)

3.4.2 BootLoader的具体实现

本例中BootLoader的实现和U-Boot类似,也是分为Stage1和Stage2两个阶段实现。首先建立一个名为start.s的文件在src目录下,通过观察这个文件名读者就可以知道start.s是一个汇编源文件,通常BootLoader的启动代码都是用汇编语言来实现的,因为它的执行效率高,并且代码量小,所以它是开发嵌入式系统BootLoader的首选开发语言。这个文件实现Stage1部分,其主要完成以下几个功能:

1.设置异常向量表

2.初始化看门狗和外围电路

3.初始化存储器

4.初始化堆栈

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

5.初始化数据区

6.跳转到C程序Main函数

Stage1的任务结束后就要开始Stage2了,此时定义一个C源文件为bios.c,它主要完成一些高级复杂功能的初始化,包括I/O端口、中断、串口、MMU(内存管理单元)等初始化工作,以及实现装载操作系统的功能。[www.61k.com]下面将分别介绍这些功能的具体实现。

3.4.2.1 设置异常向量表

ARM通常要求异常向量表必须放置在从0地址开始,且放在连续8*4字节的空间内。每当一个中断发生以后,ARM处理器便强制把PC指针置为向量表中对应中断类型的地址值。因为每个中断只占据向量表中1个字的存储空间,所以只能放置一条ARM指令,使程序跳转到存储器的其他地方,再执行中断处理。

本系统的异常向量表的程序实现如下: AREA SelfBoot, CODE, READONLY

ENTRY

b ResetHandler ;handler for Reset

b HandlerUndef ;handler for Undefined mode

b HandlerSWI ;handler for SWI interrupt

b HandlerPabort ;handler for Prefetch Abort

b HandlerDabort;handler for DAbort

b . ;reserved

b HandlerIRQ ;handler for IRQ interrupt

b HandlerFIQ ;handler for FIQ interrupt

其中第一行定义了代码区域的属性,表示了是只读属性的自启动代码区。关键字ENTRY指定编译器保留这段代码,因为编译器可能会认为这是一段冗余代码而加以优化。链接的时候要确保这段代码被链接在0地址处或集成开发工具自定义的地址,并且作为整个程序的入口。这段程序定了ARM处理器常见的7种异常向量,具体含义如下:

ü Reset:即复位异常,通常是系统上电复位或通过软件实现复位。

ü Undefined Instruction:即未定义指令异常,当出现一个既不能被主处理器识别又不能

被协处理器识别的执行指令发生时。

ü Software Interrupt (SWI):即软件中断,这是用户定义的同步中断指令,它允许程序

运行在用户模式。

ü Prefetch Abort:即预取中至异常,当处理器试图执行一个还没有取到的指令时发生,

是因为试图执行一个不合法的地址。

ü Data Abort:即数据中止异常,当一个数据传输指令试图装载或保存一个数据在一个

不合法的地址时发生。

ü IRQ:即中断请求,当处理器的外部中断请求针被设置时发生,此时 CPSR状态寄存

器的第I位是被清除的。

ü FIQ:即快速中断请求,当处理器外部快速中断请求针被设置时发生,此时 CPSR状

态寄存器的第F位是被清除的。

通常当几个异常同时发生时,处理器必须知道先处理那个再处理那个,所以必须定义一个规则来确定它们的先后顺序,这就是异常的优先级。ARM处理器定义了异常的优先级如表3.1所示:

向量地址 异常类型 异常模式 优先级(1=high,

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

6=low)

0x0 0x4 0x8 0xC 0x10 0x14 0x18 0x1C

Reset

Undefined Instruction Software Interrupt (SWI) Prefetch Abort Data Abort Reserved Interrupt (IRQ) Fast Interrupt (FIQ)

Supervisor (SVC) Undef

Supervisor(SVC) Abort Abort 没有定义 Interrupt (IRQ) Fast Interrupt (FIQ)

1 6 6 5 2 没有定义 4 3

3.4.2.2初始化看门狗和外围电路

这一步参考S3C2410用户手册实现了看门狗、中断、PLL(Phase Lock Loop)锁时间计数器(Lock time counter)和MPLL配置寄存器的初始化。(www.61k.com]具体实现请参考以下代码和注释:

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

ldr r0,=WTCON ;关闭看门狗 ldr r1,=0x0 str r1,[r0]

ldr r0,=INTMSK ;屏蔽所有第一级中断 ldr r1,=0xffffffff str r1,[r0]

ldr r0,=INTSUBMSK ;屏蔽所有第二级中断 ldr r1,=0x3ff str r1,[r0]

;为了减少 PLL 锁时间通过调节LOCKTIME寄存器. ldr r0,=LOCKTIME ldr r1,=0xffffff str r1,[r0]

;配置 MPLL控制寄存器 ldr r0,=MPLLCON

ldr r1,=((M_MDIV<<12)+(M_PDIV<<4)+M_SDIV) ;Fin=12MHz,Fout=50MHz str r1,[r0]

3.4.2.3初始化存储器

这一步用来设置内存控制寄存器,具体实现参考以下代码:

;设置内存控制器 adr r0, SMRDATA ;不能使用ldr r0, =xxxx ldr r1, =BWSCON ;BWSCON为总线宽度和等待状态控制寄存器 add r2, r0, #52 ;SMRDATA结束地址

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

ldr r3, [r0], #4

str r3, [r1], #4

cmp r2, r0

bne %B0 其中SMRDATA的定义如下,其作用是定义内存区域控制寄存器值。(www.61k.com)

SMRDATA DATA

; 1) 即使在 HCLK=75Mhz,内存设置的都是安全的参数

; 2) SDRAM 刷新周期是用于HCLK=75Mhz时。

DCD

(0+(B1_BWSCON<<4)+(B2_BWSCON<<8)+(B3_BWSCON<<12)+(B4_BWSCON<<16)+(B5_BWSCON<<20)+(B6_BWSCON<<24)+(B7_BWSCON<<28))

DCD

((B0_Tacs<<13)+(B0_Tcos<<11)+(B0_Tacc<<8)+(B0_Tcoh<<6)+(B0_Tah<<4)+(B0_Tacp<<2)+(B0_PMC)) ;GCS0

DCD

((B1_Tacs<<13)+(B1_Tcos<<11)+(B1_Tacc<<8)+(B1_Tcoh<<6)+(B1_Tah<<4)+(B1_Tacp<<2)+(B1_PMC)) ;GCS1

DCD

((B2_Tacs<<13)+(B2_Tcos<<11)+(B2_Tacc<<8)+(B2_Tcoh<<6)+(B2_Tah<<4)+(B2_Tacp<<2)+(B2_PMC)) ;GCS2

DCD

0x1f7c;((B3_Tacs<<13)+(B3_Tcos<<11)+(B3_Tacc<<8)+(B3_Tcoh<<6)+(B3_Tah<<4)+(B3_Tacp<<2)+(B3_PMC)) ;GCS3

DCD

((B4_Tacs<<13)+(B4_Tcos<<11)+(B4_Tacc<<8)+(B4_Tcoh<<6)+(B4_Tah<<4)+(B4_Tacp<<2)+(B4_PMC)) ;GCS4

DCD

((B5_Tacs<<13)+(B5_Tcos<<11)+(B5_Tacc<<8)+(B5_Tcoh<<6)+(B5_Tah<<4)+(B5_Tacp<<2)+(B5_PMC)) ;GCS5

DCD ((B6_MT<<15)+(B6_Trcd<<2)+(B6_SCAN)) ;GCS6

DCD ((B7_MT<<15)+(B7_Trcd<<2)+(B7_SCAN)) ;GCS7

DCD

((REFEN<<23)+(TREFMD<<22)+(Trp<<20)+(Trc<<18)+(Tchr<<16)+REFCNT)

DCD 0x32 ;SCLK 电压保护模式BANKSIZE 是128M

DCD 0x30 ;MRSR6 CL=3clk

DCD 0x30 ;MRSR7

3.4.2.4初始化堆栈

接下来要初始化堆栈,一般ARM有7种工作模式:用户模式(usr),快速中断模式(fiq),外部中断模式(irq),管理模式(svc),数据访问终止模式(abt),系统模式(sys)和未定义指令中止模式(und)。关于这7种模式在前面的章节中已经介绍,这里不再重复。初始化堆

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

栈也就是初始化这7种模式下的堆栈。(www.61k.com) bl InitStacks ;调用初始化堆栈函数

InitStacks ;实现初始化堆栈

;不要使用DRAM, 如stmfd,ldmfd......

mrs r0,cpsr

bic r0,r0,#MODEMASK

orr r1,r0,#UNDEFMODE|NOINT

msr cpsr_cxsf,r1 ;未定义指令中止模式(und)

ldr sp,=UndefStack

orr r1,r0,#ABORTMODE|NOINT

msr cpsr_cxsf,r1 ;数据访问终止模式(abt)

ldr sp,=AbortStack

orr r1,r0,#IRQMODE|NOINT

msr cpsr_cxsf,r1 ;外部中断模式(irq)

ldr sp,=IRQStack

orr r1,r0,#FIQMODE|NOINT

msr cpsr_cxsf,r1 ;快速中断模式(fiq)

ldr sp,=FIQStack

bic r0,r0,#MODEMASK|NOINT

orr r1,r0,#SVCMODE

msr cpsr_cxsf,r1 ;管理模式(svc)

ldr sp,=SVCStack

;用户模式(usr)和系统模式(sys)没有被初始化

mov pc,lr ;如果当前模式不适SVC模式,LR寄存器将无效

LTORG

3.4.2.5初始化数据区

内核映像一开始总是存储在ROM或Flash里面的,其RO部分既可以在ROM或Flash里面执行,也可以转移到速度更快的RAM中执行;而RW和ZI*(注2)这两部分是必须转移到可写的RAM里去。所谓数据区的初始化,就是完成必要的从ROM到RAM的数据传输和内容清零。这步的代码实现如下:

InitRam

ldr r2, BaseOfBSS ;BaseOfBSS定义RW部分的基地址

ldr r3, BaseOfZero ;BaseOfZero定义ZI部分的基地址

cmp r2, r3

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

1

ldrccr1, [r0], #4 strcc r1, [r2], #4 bcc %B0 mov r0, #0 ldr r3, EndOfBSS ;EndOfBSS定

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

义ZI部分的末地址 cmp r2, r3 strcc r0, [r2], #4 bcc %B1

*注2:当可执行性文件被装载到RAM中后,它在RAM中存放的位置如下:

图3.6 可执行性文件在装载前后的分布

其中BaseOfROM,TopOfROM,BaseOfBSS,BaseOfZero和EndOfBSS的定义如下:

BaseOfROM

TopOfROM

BaseOfBSS

BaseOfZero

EndOfBSS DCD DCD DCD DCD DCD |Image$$RO$$Base| |Image$$RO$$Limit| |Image$$RW$$Base| |Image$$ZI$$Base| |Image$$ZI$$Limit|

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

其中:

|Image$$RO$$Base|:表示RO区开始地址

|Image$$RO$$Limit|:表示RO区末地址后面的地址,即RW数据源的起始地址

|Image$$RW$$Base|:表示RW区在RAM里的执行区起始地址,也就是编译器选项RW_Base指定的地址

|Image$$ZI$$Base|:表示ZI区在RAM里面的起始地址

|Image$$ZI$$Limit|:表示ZI区在RAM里面的结束地址后面的一个地址

上述程序先把ROM里|Image$$RO$$Limt|开始的RW初始数据拷贝到RAM里面|Image$$RW$$Base|开始的地址,当RAM这边的目标地址到达|Image$$ZI$$Base|后就表示RW区的结束和ZI区的开始,接下去就对这片ZI区进行清零操作,直到遇到结束地址|Image$$ZI$$Limit|为止。(www.61k.com)

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

3.4.2.6跳转到C程序Main函数

这一步是进入C程序入口的操作,也是BootLoader必不可少的一个过程。(www.61k.com)它的具体实现如下: ;发送复位状态信息到Main函数

ldr r1, =GSTATUS2

ldr r0, [r1]

str r0, [r1] ;清除复位信息

ldr pc, GotoMain ;调用Main函数,此时GotoMain代表Main。

3.4.2.7 Main函数的具体实现

该部分可以说实现BootLoader的Stage2的功能,该函数主要完成以下工作: 1.初始化系统频率 2.初始化I/O端口 3.初始化中断处理程序表 4.串口初始化 5.其他部分的初始化 6.主程序循环 具体实现代码如下:

int Main(U32 RstStat)

{

int i;

SetClockDivider(0, 1); //设置FCLK:HCLK:PCLK= 1:1:2

SetSysFclk(FCLK_96M); //设置系统FCLK=96MHz

Port_Init(); //I/O端口初始化

Isr_Init(); //中断处理程序表初始化

Uart_Init(0, Console_Baud); //串口初始化

Uart_Select(Console_Uart);

MMU_Init(); //MMU初始化

//使能GPIO,UART0,PWM TIMER,NAND FLASH 等模块时钟

EnableModuleClock(CLOCK_ALL);

…… ……

NandLoadRun(); //定义从NAND Flash装载操作系统

while(1)

{

}

}

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

3.5本章小节

BootLoader是嵌入式系统非常重要的一部分,本章一开始讲述了BootLoader的基本概念,以及它的主要作用;紧接着讲述了常见的几种BootLoader:U-Boot,VIVI,Blob,RedBoot,ARMboot和DIY;最后重点讲述了基于S3C2410开发板的U-Boot移植和编写自己的BootLoader。[www.61k.com]通过学习本章内容,使读者可以清楚地了解嵌入式系统的启动过程,为进一步学习嵌入式系统开发奠定良好的基础。下一章将介绍Linux内核移植。

3.6常见问题

1.什么是BootLoader?

参考答案:简单地说,BootLoader就是在操作系统内核运行前运行地一段小程序。通过这段小程序,我们可以初始化必要的硬件设备,创建内核需要的一些信息并将这些信息通过相关机制传递给内核,从而将系统的软硬件环境带到一个合适的状态,最终调用操作系统内核,真正起到引导和加载内核的作用。

2.常用的嵌入式BootLoader有哪些?

参考答案:有U-Boot, Blob, VIVI,RedBoot和ARMboot等。

3.ARM处理器的有哪几种工作模式?

参考答案:ARM处理器有以下7种工作模式:

l 用户模式(usr): ARM处理器正常的程序执行状态

l 快速中断模式(fiq):用于高速数据传输或通道处理

l 外部中断模式(irq):用于通用的中断处理

l 管理模式(svc): 操作系统使用的保护模式

l 数据访问终止模式(abt):当数据或指令预取终止时进入该模式,可用于虚拟存储及存储保护。

l 系统模式(sys): 运行具有特权的操作系统任务。

l 未定义指令中止模式(und):当未定义的指令执行时进入该模式,可用于支持硬件协处理器的软件仿真。

4.通常BootLoader的Stage1都实现哪些功能?

参考答案:通常情况下完成以下几个任务:

1.设置异常向量表

2.初始化看门狗和外围电路

3.初始化存储器

4.初始化堆栈

5.初始化数据区

6.跳转到C程序Main函

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

第4章 嵌入式Linux内核移植

本章学习目标:

l 了解移植的基本概念

l 熟悉Linux内核的配置过程

l 熟悉Linux内核的编译过程

l 了解根文件系统的作用

l 学会构建Cramfs文件系统

l 学会BusyBox工具的使用

4.1移植的基本概念

在第三章中我们已经接触了真正的移植,但并没有解释什么是移植,在这节里有必要给读者解释一下在嵌入式开发中应用非常广泛的一个概念——移植,英文为Porting。[www.61k.com]从广义上讲,移植包括软件移植和硬件移植。从狭义上讲,移植就是指软件移植,即将一个软件从一个平台迁移到另外一个与其不同的平台上工作。通常情况下,移植分为以下三种情况: ü 从一个硬件平台移植到另外一个硬件平台

在Linux内核代码中,可以看到arch目录下有许多子目录,其中每一个子目录代表一种硬件平台,也就是说Linux内核arch目录下有多少个子目录就代表其支持多少种硬件平台。这里以Linux 2.6.10内核为例,其arch目录下的内容如下: alpha cris ia64 m68knommu ppc sh sparc64 x86_64

arm h8300 m32r mips ppc64 sh64 um

arm26 i386 m68k parisc s390 sparc v850

以上每一种体系结构里又包含许多的子体系,以arm体系结构为例,它又包含mach-h720x ,mach-l7200,mach-sa1100,ach-epxa10db,mach-ixp2000,mach-rpc,mach-s3c2410等子体系结构。

这种形式的移植最常见的就是Linux操作系统移植。比如将基于x86体系的Linux移植到基于ARM体系的嵌入式Linux。首先是工具链的移植,因为基于x86体系的gcc就不能用在基于ARM的体系中,所以在PC机上编译时要建立交叉编译工具链。同时还要考虑binutils、glibc等移植。其次是内核移植,内核移植主要包括两方面的工作,一是arch目录下的体系结构的移植,如从i386移植到arm,二是移植drivers目录下的许多硬件驱动程序。最后是应用程序的移植,如C代码的重新编译,实现一些缺少的库,如Qt/embedded库的移植等。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

由于硬件平台对C语言有一定的影响,移植时必须要考虑。例如处理器的字长,定义处理器一次能处理数据位的个数,通常是32位,处理器字长会影响C语言的数据类型的长度,如int,long等。数据对齐也是这种形式移植时必须考虑的,数据对齐的意思是数据块的地址是某个特定数大小的整数倍,如32位处理器要求是4对齐的,即必须是4的整数倍。字节顺序也是必须要考虑的,字节顺序(byte order)是指一个字中字节排列的顺序。不同硬件可能采用不同的排列顺序,例如x86是little-endian,ppc是big-endian。软件中与时间相关的代码也会影响移植过程,所以建议采用与时间无关的代码可以提高移植性,比如Linux内核里用HZ来表示每秒钟的嘀嗒数。内存页面的大小也是移植过程应该考虑的,不同的体

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

系结构可能会有不同的页面大小,常用的32位处理器用4KB的页面大小,有些体系结构可以支持多种页面大小。(www.61k.com)

ü 从一个操作系统移植到另一个操作系统

这种形式的移植也是最常见的。比如将Windows系统下运行的程序移植到Linux/Unix系统中,这时需要考虑操作系统提供的API,以及所调用的函数库等。

ü 从一种软件库环境移植到另一种软件库环境

这种类型的移植也是比较常见的,例如基于Qt 3.0库的应用程序移植到Qt 4.0库环境中去。再如基于glibc库环境的程序移植到基于uclibc库环境。

4.2内核移植的准备

为什么要选择移植Linux内核?首先让我们来看一下Linux内核都具有哪些特点:

l 开放性:Linux内核遵循GNU GPL (General Public License ) *(注1),所以其

源代码都是免费公开的

l 可移植性:支持几乎所有硬件平台

l 可定制性:不但能够运行在高性能计算机上,也可以运行在资源有限的嵌入式

设备中

l 互操作性:兼容许多标准

l 网络支持:支持许多网络协议

l 安全性:实现许多的安全协议,并且开发人员都非常重视它的安全性

l 稳定性:经过多年许多产品的使用已经表明它具有很好的稳定性

l 模块化:可以使内核只包含系统必须的东西,其他的都可以使用模块化来完成 l 方便编程:可以通过学习已有的代码和网络上丰富的资源。

*注1:GNU GPL,即GNU通用公共许可证,是由自由软件基金会发行的用于计算机软件的许可证。最初由Richard Stallman为GNU计划而撰写。目前大多数的GNU程序和超过半数的自由软件使用此许可证。此许可证最新版本为“版本3”,1991年发布。GNU LGPL(Lesser General Public License),即宽通用公共许可证是由GPL衍生出的许可证,被用于一些GNU程序库。

由于Linux内核具有以上许多特点,这些特点正都是它的优点。除此之外,它和其他操作系统相比还有许多优点。一是平台独立性,它不依赖于某个特定的硬件平台,通常选择一个操作系统也许就会锁定你仅能使用特定的硬件平台,而Linux却可以真正的实现平台独立性。二是快速上市,因为使用Linux系统很容易移植许多硬件到系统中去,所以会大大减少开发时间,从而加快产品上市。三是低成本,它不但可以节约开发成本,而且也可以节约培训成本。四是遵循POSIX(Portable Operating System Interface)*(注2)标准,POSIX的目的就是提升软件的可移植性在UNIX系统上,因此遵循这个标准可以使开发更容易。五是代码开放性,Linux之所以变得如此流行,这应该是一个非常重要的原因。六是支持多种硬件,Linux不但支持最新的高性能硬件,同时也支持低价格和早期的微处理和I/O设备[7]。总之,Linux还有许多独特的优点,所以在选择嵌入式操作系统时被大多数硬件平台选用。它现在已经成为嵌入式系统中的一个主流操作系统。

*注2:POSIX 表示可移植操作系统接口(Portable Operating System Interface ,缩写为 POSIX 是为了读音更像UNIX)。电气和电子工程师协会(Institute of Electrical and Electronics Engineers,IEEE)最初开发 POSIX 标准,是为了提高 UNIX 环境下应用程序的可移植性。然而,POSIX 并不局限于 UNIX。它已被应用许多其它的操作系统,例如 DEC OpenVMS 和 Microsoft Windows NT,都支持 POSIX 标准,尤其是 IEEE Std. 1003.1-1990(1995 年修订)或 POSIX.1,POSIX.1 提供了源代码级别的 C 语言应

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

用编程接口(API)给操作系统的服务程序,例如读写文件。(www.61k.com)POSIX.1 已经被国际标准化组织(International Standards Organization,ISO)所接受,被命名为 ISO/IEC 9945-1:1990 标准。POSIX 现在已经发展成为一个非常庞大的标准族,某些部分新的功能正处在开发过程中。

移植内核首先要确保编译内核的工具是否准备好,Linux内核归根结底也是一个程序,所以它必须通过编译器编译后才能在硬件上执行,由于我们的目标板是基于ARM920T内核的S3C2410处理器,需要能够编译出在ARM处理器上可以运行的程序,这时就需要交叉编译链了,好在我们已经未雨绸缪,在第二章已经介绍了交叉编译工具的制作,同时已经构建了自己的交叉编译工具链,在此直接使用就可以了。

其次,从站点下载要移植的内核代码,这里下载的内核代码为linux-2.6.10.tar.gz,可以看出它的版本为Linux 2.6.10。

最后检查要移植的开发板硬件是否准备就绪,当所有基本条件都准备好了,下面我们就可以正式移植内核了。

4.3内核移植

内核是所有嵌入式Linux系统的核心软件,内核移植是一个比较复杂的任务,当然也是嵌入式系统开发中非常重要的一个过程。内核移植一般包括内核配置、内核编译和内核下载三大部分。下面将具体介绍内核移植的每一个步骤。

4.3.1 内核配置

配置内核是构建一个嵌入式Linux系统内核的第一步,有好几种配置内核的方式,同时有很多内核的配置选项。下面将按执行的步骤讲述内核配置所要做的内容。

4.3.1.1修改Makefile

这一步的作用是修改内核根目录下的Makefile文件,从而指明要用的编译器为arm-linux-交叉编译器,使用的体系结构为ARM。具体操作如下:

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

# cd linux-2.6.10

# vi Makefile

修改内容如图4.1所示,其中修改两行内容如下:

ARCH = arm

CROSS_COMPILE = arm-linux-

其含义:

ARCH = arm说明目标是ARM体系结构,默认的ARCH是指宿主机的体系,如i386; CROSS_COMPILE=arm-linux- 说明使用交叉编译器前缀为arm-linux-,默认情况下CROSS_COMPILE为空,即没有前缀。注意,如果这个时候你还没有把交叉编译工具链的路径添加到环境变量中,那么这里的CROSS_COMPILE必须设置为宿主机上交叉编译工具链的绝对路径。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图4.1 修改Makefile的交叉编译器变量

4.3.1.2设置NAND Flash分区

由于我们的目标板使用的是64M NAND Flash作为Flash存储器,所以首先我们需要建立一个NAND Flash分区表,该分区表用来定义开发板上64M空间划分,以及定义各分区存放的起始地址以及大小等。[www.61k.com)该部分的实现在<arch/arm/mach-s3c2410/devs.c>源文件中,故修改该文件,修改内容如下: //首先添加相应的头文件

#include <linux/mtd/partitions.h>

#include <linux/mtd/nand.h>

#include <asm/arch/nand.h>

……

/*新建64M的Nand Flash分区表*/

static struct mtd_partition partition_info[]={

{/*1MB*/

name:"bootloader",

size:0x00100000,

offset:0x0,

},

{/*3MB*/

name:"kernel",

size:0x00300000,

offset:0x00100000,

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

},

{/*40MB*/

name:"rootfs",

size:0x02800000,

offset:0x00400000,

},

{/*20MB*/

name:"user",

size:0x01400000,

offset:0x02c00000,

}

};

//定义一个NAND Flash分区数据结构

struct s3c2410_nand_set nandset ={

nr_partitions: 4 , //定义分区数

partitions: partition_info , //定义分区表

};

… …

上述代码的作用是建立一个64M的Nand Flash分区表,将其分为:bootloader(启动程序),kernel(内核),rootfs(根文件系统)和user(用户空间)四个分区。[www.61k.com)其中:name:代表分区的名称;size:代表分区的大小;offset:代表分区在Flash中的起始地址。同时后面定义NAND Flash的分区数据结构。

紧接着要建立内核对NAND Flash芯片的支持,同时加入对NAND Flash芯片的支持代码到NAND Flash的驱动程序。具体代码实现如下<arch/arm/mach-s3c2410/devs.c>:

//建立NAND Flash的芯片支持数据结构

struct s3c2410_platform_nand superlpplatform={

tacls:0,

twrph0:1,

twrph1:0,

sets: &nandset,

nr_sets: 1,

};

… …

//修改s3c_device_nand结构体增加对superlpplatform设备的支持

struct platform_device s3c_device_nand = {

.name = "s3c2410-nand",

.id = -1,

.num_resources = ARRAY_SIZE(s3c_nand_resource),

.resource = s3c_nand_resource,

//添加以下内容用来支持NAND Flash芯片

.dev = {

.platform_data = &superlpplatform

}

};

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

… …

上述代码的主要作用是完成内核对NAND Flash芯片驱动的支持,需要指出的是TACLS、TWRPH0和TWRPH1,参考S3C2410用户手册,可以看到这三个参数控制的是NAND Flash信号线CLE/ALE与写控制信号nWE的时序关系。(www.61k.com)我们设的值为TACLS=0,TWRPH0=1,TWRPH1=0,其含义为:TACLS=1个HCLK时钟,TWRPH0=2个HCLK时钟,TWRPH1=1个HCLK时钟。其中sets 定义支持的分区集,nr_sets定义分区集数。最后修改s3c_device_nand结构体变量,添加对NAND Flash设备驱动的支持。其中:name:为设备名称;id:有效设备编号,如果只有一个定义为-1,如果多个则从0开始计算;num_resources:定义有几个NAND Flash芯片资源;resource:定义NAND Flash芯片资源的首地址。

虽然我们已经实现了对新加的NAND Flash芯片的支持,但是现在内核还不能正常工作,因为在内核启动时还没有添加对NAND Flash分区表初始化配置,所以还需要修改<arch/arm/mach-s3c2410/ mach-smdk2410.c>源文件,修改内容如下:

… …

//修改smdk2410_devices[]的初始化项中增加最后一项

static struct platform_device *smdk2410_devices[] __initdata = {

&s3c_device_usb,

&s3c_device_lcd,

&s3c_device_wdt,

&s3c_device_i2c,

&s3c_device_iis,

&s3c_device_nand //添加此行来初始化新增的NAND Flash分区

};

… …

接下来还有一个问题就是要禁止Flash ECC*(注3)校验,由于通常我们使用的BootLoader通过软件已经产生ECC校验码,这与内核ECC校验码不一致,所以这里选择禁止内核ECC校验。完成这个步骤需要修改的文件是drivers/mtd/nand/s3c2410.c,具体操作如下: *注3:在普通的内存上,常常使用一种技术,即Parity,同位检查码(Parity check codes)被广泛地使用在侦错码(error detectioncodes)上,它们增加一个检查位给每个资料的字元(或字节),并且能够侦测到一个字符中所有奇(偶)同位的错误,但Parity有一个缺点,当计算机查到某个Byte有错误时,并不能确定错误在哪一个位,也就无法修正错误。基于上述情况,产生了一种新的内存纠错技术,那就是ECC,英文全称是Error Checking and Correcting,即错误检查和纠正,从这个名称我们就可以看出它的主要功能就是“发现并纠正错误”,它比奇偶校正技术更先进的方面主要在于它不仅能发现错误,而且能纠正这些错误,这些错误纠正之后计算机才能正确执行下面的任务,确保服务器的正常运行。ECC本身并不是一种内存型号,也不是一种内存专用技术,它是一种广泛应用于各种领域的计算机指令中,是一种指令纠错技术。之所以说它不是一种内存型号,那是因为并不是一种影响内存结构和存储速度的技术,可以应用到不同的内存类型之中,就象前讲到的“奇偶校正”内存,它也不是一种内存,最开始应用这种技术的是EDO内存,现在的SD也有应用,而ECC内存主要是从SD内存开始得到广泛应用,而新的DDR、RDRAM也有相应的应用,目前主流的ECC内存其实是一种SD内存。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

… …

//修改s3c2410的NAND Flash芯片初始化函数

static void s3c2410_nand_init_chip(struct s3c2410_nand_info *info,

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

struct s3c2410_nand_mtd *nmtd,

struct s3c2410_nand_set *set)

{

struct nand_chip *chip = &nmtd->chip;

chip->IO_ADDR_R = (char *)info->regs + S3C2410_NFDATA;

chip->IO_ADDR_W = (char *)info->regs + S3C2410_NFDATA;

chip->hwcontrol = s3c2410_nand_hwcontrol;

chip->dev_ready = s3c2410_nand_devready;

chip->cmdfunc = s3c2410_nand_command;

chip->write_buf = s3c2410_nand_write_buf;

chip->read_buf = s3c2410_nand_read_buf;

chip->select_chip = s3c2410_nand_select_chip;

chip->chip_delay = 50;

chip->priv = nmtd;

chip->options = 0;

chip->controller = &info->controller;

nmtd->info = info;

nmtd->mtd.priv = chip;

nmtd->set = set;

if (hardware_ecc) {

chip->correct_data = s3c2410_nand_correct_data;

chip->enable_hwecc = s3c2410_nand_enable_hwecc;

chip->calculate_ecc = s3c2410_nand_calculate_ecc;

chip->eccmode = NAND_ECC_HW3_512;

chip->autooob = &nand_hw_eccoob;

} else {

chip->eccmode = NAND_ECC_NONE; //修改此行的赋值

}

}

… …

到此已经完成了新的内核对NAND Flash分区的支持,下面将介绍配置内核的主要选项。(www.61k.com]

4.3.1.3配置内核选项

配置内核选项是移植内核过程中很重要的一步,也是非常复杂的一步,配置时一定要细心。由于我们的开发板很接近Linux内核中提供的smdk2410开发板,所以可以参考smdk2410开发板的配置文件,将其默认的配置文件拷贝到内核代码的根目录下,然后开始配置内核,操作如下:

# cd linux-2.6.10

# cp arch/arm/configs/smdk2410_defconfig .config

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

# make menuconfig

通常有4种主要的配置内核方法:

1.make config

提供了一个命令行接口方式来配置内核,它会一个接着一个的询问关于每一个选项,这个方式相对非常繁琐,因为有太多的选项要进行配置,并且你不知道什么时候才能配置结束直到配置完最后一个选项才知道,所以在实践中很少应用该方法。[www.61k.com]如果已经有了.config配置文件,它将根据配置文件来设置询问选项的默认值。

2.make oldconfig

它会使用一个已有的.config配置文件,提示行会提示你的是哪些之前你还没有配置过的选项,它与make config相比会简单许多,因为它需要配置的选项不再是所有的了,而是.config文件中还没有配置的选项。

3.make menuconfig

显示一个基于文本的图形化终端配置菜单,目前被公认为是使用最广泛的配置内核方式。如果一个.config文件存在,它将使用该文件设置那些默认的值。

4.make xconfig

显示一个基于X窗口的配置菜单,用户可以通过图形用户界面(GUI)和鼠标来对内核进行配置,使用该方法时必须支持X Window系统。如果一个.config文件存在,它将使用该文件设置那些默认的值[8]。

下面列出了Linux2.6.10内核的主菜单配置选项:

l Code maturity level options --->

l General setup --->

l Loadable module support --->

l System Type --->

l General setup --->

l Parallel port support --->

l Memory Technology Devices (MTD) --->

l Plug and Play support --->

l Block devices --->

l Multi-device support (RAID and LVM) --->

l Networking support --->

l ATA/ATAPI/MFM/RLL support --->

l SCSI device support --->

l Fusion MPT device support --->

l IEEE 1394 (FireWire) support --->

l I2O device support --->

l ISDN subsystem --->

l Input device support --->

l Character devices --->

l I2C support --->

l Multimedia devices --->

l File systems --->

l Profiling support --->

l Graphics support --->

l Sound --->

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

l

l

l

l

l

l

l Misc devices ---> USB support ---> MMC/SD Card support ---> Kernel hacking ---> Security options ---> Cryptographic options ---> Library routines --->

关于内核中每个选项的含义由于内容太多,所以这里就不再介绍,请参考相关的帮助文档。[www.61k.com]由于我们是基于smdk2410开发板的配置选项,所以我们需要在此开发板上修改部分的配置选项来配置我们的内核。

首先,配置可加载模块的支持,具体配置选项如下:

Loadable module support --->

[*] Enable loadable module support # 该选项的目的是使内核支持可加载模块,使用modprobe, lsmod, modinfo, insmod, rmmod等工具需要,所以必须选择。

[*] Module unloading # 卸载模块选项,这里选择该选项。

[*] Forced module unloading # 强制性卸载模块选项,如用rmmod -f 命令强制卸载。

[ ] Module versioning support (EXPERIMENTAL) # 一般地,我们编译的模块是用于当前运行的内核, 选择该选项可以针对其他版本内核编译模块,这里可以不选择。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

[ ] Source checksum for all modules # 检查所以模块的代码,一般不选择。

[*] Automatic kernel module loading # 内核在任务中要使用一些被编译为模块的驱动或特性时, 先使用modprobe命令来加载它,然后该选项自动调用modprobe加载需要的模块,所以该选项一定要选择!

注意:在每个选项前有一个方括号,其中 [*]表示该选项加入内核编译;[ ]表示不选择该选项;[M]表示该选项作为模块编译内核,也就是说可以动态的加载和卸载该模块。

接着加入内核对S3C2410 DMA(Direct Memory Access,直接内存访问)的支持,具体配置选项如下:

System Type -->

[*] S3C2410 DMA support # 该选项来支持S3C2410 DMA特性。

然后在General setup ---> Default kernel command string 菜单下修改默认的内核启动参数,修改后内容如下: noinitrd root=/dev/mtdblock2 init=/linuxrc console= ttySAC0,115200 mem=64M

对上面的参数解释一下,mtdblock2代表使用第3个flash分区(也就是我们建立的rootfs分区),用来作根文件系统;console=ttySAC0,115200使kernel启动期间的信息全部输出到串口0上,波特率为115200;Linux 2.6内核对于串口的命名改为ttySAC0,在2.4内核中,串口名为ttyS0,使用时请注意。mem=64M表示内存大小是64M。

同时还需要添加对浮点算法的支持,在菜单下选择:

General setup --->

[*] NWFPE math emulation #支持NWFPE浮点库,在许多情况下要使用,所以最好选上。

接下来做对MTD设备(如flash, ram等芯片)的配置,如下选择配置:

Memory Technology Devices (MTD) -->

[*] MTD partitioning support

RAM/ROM/Flash chip drivers -->

[*] Detect flash chips by Common Flash Interface (CFI) probe

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

[*] Detect nonCFI AMD/JEDECcompatible flash chips

[*] Support for Intel/Sharp flash chips

[*] Support for AMD/Fujitsu flash chips

[*] Support for ROM chips in bus mapping

NAND Flash Device Drivers -->

[*] NAND Device Support

[*] NAND Flash support for S3C2410/S3C2440 SoC

为了我们要移植的内核支持devfs(Device Filesystem,设备文件系统),以及在启动时能自动加载/dev为devfs。[www.61k.com)需要针对文件系统的设置,由于我们目标板上要用的文件系统是cramfs,所以需要做如下配置:

File systems -->

[ ]Second extended fs support #去除对ext2的支持

Pseudo filesystems -->

[*] /proc file system support

[*] Virtual memory file system support (former shm fs)

[*] /dev file system support (OBSOLETE)

[*] automatically mount at boot (NEW)

Miscellaneous filesystems -->

[*]Compressed ROM file system support (cramfs)

Network File Systems >

[*] NFS file system support

除此之外,还需要配置以下选项来支持S3C2410 RTC、USB和MMC/SD卡驱动,具体选项如下:

Character devices -->

[*] Nonstandard serial port support

[*] S3C2410 RTC Driver

USB Support -->

[*] Support for Host-side USB

MMC/SD Card Support -->

[*] MMC Support

[*] MMC block device driver

以上完成了所有内核相关的基本配置,然后选择保存,默认会保存到.config文件。保存结束显示如图4.2所示,下一步就可以进行编译内核了。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图4.2 内核配置保存

4.3.2 内核编译

编译内核通常也需要几个步骤,一是清除以前编译过的残留文件,二是编译内核image文件和可加载模块,三是安装模块。[www.61k.com]在编译内核之前,一定要阅读内核根目录下的README文件和Documentation/Changes文件,其中该README文件告诉我们一个通用的安装内核的方法,Changes文件主要告诉我们编译和运行该内核需要的最低工具软件列表。由于我们是基于ARM处理器平台移植,所以我们还需要阅读Documentation/arm/README文件,该文档主要告诉我们编译ARM Linux内核的基本方法。通过阅读这些文档,可以让我们更多的了解Linux内核的使用方法,下面将具体介绍编译内核的基本方法。

4.3.2.1清除冗余文件

首先进入内核根目录,执行以下操作,其实这个步骤现在是可以不用的,它目的是清除原先编译过而残留的.config和.o(object文件),如果我们是刚下载的源码,那么这一步就可以省了,但是如果你已经编译过多次内核的话,这一步可是一定要的,不然以后可能会出现很多莫名其妙的小问题。 # cd linux-2.6.10

# make mrproper

注意:该步骤一定要在执行配置命令(make menuconfig/xconfig/config等)之前做,否则你的.config配置文件会被清除掉,这样你的内核就不能正确编译了。

4.3.2.2编译内核映像和模块

接下来就要编译内核image(映像)和可加载模块了,这里有必要介绍一下常见的Linux内核编译相关的命令:

ü make dep:该命令应用在内核2.4或之前,用于建立源文件之间的依赖关系,在执行

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

内核配置命令之后使用,不过在2.6内核中已经取消该命令。(www.61k.com]该功能被内核配置命令实现了。

ü make clean:删除前面留下的中间文件,该命令不会删除.config等配置文件。 ü make zImage:编译生成gzip压缩形式的image。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

ü make bzImage:bzImage是big zImage的缩写,与make zImage不同的是它可以生成

较大一点的内核。

ü make modules:编译在配置时选择为模块的,即选项前为[M]的。

ü make modules_install:将make modules生成的模块文件拷贝到相应的目录。

以上这些命令在内核2.4版本之前大部分都需要用,然而在内核2.6时,只需要在命令行简单得输入make将完成内核image的生成和模块的编译。执行完make操作,会在arch/arm/boot目录下生成Image和zImage两个内核映像文件,其中Image为正常大小的映像文件,而zImage为压缩后的映像文件。就编译过程来说,内核2.6和以前版本相比,现在Makefile的功能已经变得越来越智能,从而使得编译内核和模块变得越来越简单。

4.3.2.3安装模块

由于上面的步骤已经编译了可加载模块,现在需要将编译好的模块进行安装到相应的位置,需要执行的操作如下,默认情况下模块被安装到/lib/modules。由于我们利用交叉编译器编译,并且应用在目标板上,所以需要安装的模块一般不在默认的路径下。通常可以利用选项:INSTALL_MOD_PATH=$TARGETDIR,来指定要安装的位置,此处就是TARGETDIR所定义的位置。 # make modules_install

4.3.3内核下载

内核下载就是将内核映像文件烧写到目标板上,内核下载的前提是已经在目标板上下载了相应的BootLoader程序,这里我们以U-Boot为例,来讲述下载内核映像的过程。首先在开发使用的宿主机(PC)上建立一个tftp服务,然后使用超级终端或DNW工具启动目标板,然后在U-Boot的命令行输入以下命令,具体操作如下: MIKE2410# tftp 0x30008000 zImage

TFTP from server 192.168.1.5; our IP address is 192.168.1.10

Filename 'zImage'.

Load address: 0x30008000

Loading: ################################################################# ################################################################# ############################################

done

Bytes transferred = 890752 (d9780 hex)

其中,0x30008000为指定下载到内存的地址,zImage就是我们在上节中生成的内核映像文件。以上显示信息表明内核下载是正确的。下载完内核可以通过bootm命令来启动内核,具体如下:

MIKE2410# bootm 0x30008000

## Booting image at 30008000 ...

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

Starting kernel ...

Uncompressing Linux...................................................................................done, booting the kernel.

注意,此时下载的内核文件在断电后不能保存,要想固化内核和模块到开发板需要添加根文件系统,所以下一节专门介绍根文件系统的使用。(www.61k.com]

4.4 建立Linux根文件系统

根文件系统是Linux/Unix系统启动的一个重要组成部分,也是操作系统正常工作时的必要组成部分,在启动时内核需要根文件系统来挂载。在现代Linux操作系统中,内核代码映像文件保存在根文件系统中。系统引导启动程序会从这个根文件系统设备上把内核执行代码加载到内存中去运行。本节首先介绍根文件系统的基本知识,然后介绍如何构建自己的根文件系统。

4.4.1根文件系统的基本介绍

本节主要讲述根文件系统的一些基本概念,包括根文件系统的一般目录结构,常见的根文件系统和如何选择根文件系统。

4.4.1.1根文件系统的基本目录结构

在根文件系统的最顶层目录中,每一个目录都有其具体目的和用途。一般建立一个正式的文件系统结构是根据FHS(Filesystem Hierarchy Standard)定义,FHS,即文件系统结构(层次)标准,它在Unix /Linux操作系统的文件系统中用于确定在何处存储何种文件的标准。例如,在/bin 目录下存放基本命令,在/etc 目录下存放设置文件等等。表4.1将提供一个完整的FHS定义的根文件系统顶层目录。 目录名bin

boot

dev

etc

home

lib

mnt

opt

proc

root 提供基本的用户命令库 用于BootLoader的静态文件 设备或其他的特殊文件 系统配置文件,包括启动文件 多个用户的主目录 内 容 基本的系统库,例如C库,内核模块等 用于临时挂载的文件系统 可选择的软件包 内核虚拟文件系统和进程信息 根用户的主目录

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

目录名sbin

tmp

usr

var 基本的系统管理二进制库 临时文件 内 容 它的二级目录里包含许多应用程序和许多有用的文档,包括X server 一些变化的实例和工具等

对于经常使用Linux系统的读者来说,这些目录大部分应该很熟悉了。(www.61k.com)对于一般基于PC的Linux系统都是多用户的,而嵌入式系统通常都不是针对多用户的,所以根文件系统目录会有较大的不同。例如/boot目录,这个目录取决于你所使用的BootLoader是否能够重新获得内核映象从你的根文件系统在内核启动之前。/home这个目录在一般嵌入式Linux中就很少用到。但通常目录:/bin,/dev,/etc,/lib,/proc,/sbin,/usr这些都是必须有的。 通常这几个目录对初学者来说容易混淆,那就是/bin,/sbin,/usr/bin和/usr/sbin。由于这些目录有相似的目的,所以经常会混淆。/bin目录一般存放对于用户和系统来说都是必须的二进制文件,而/sbin目录要存放的是只针对系统管理的二进制文件,该目录的文件将不会被普通用户使用。相反,那些不是必要的用户二进制文件存放在/usr/bin下面,那些不是非常必要的系统管理工具放在/usr/sbin下。此外,对于一些本地的库也非常类似,对于那些要求启动系统和运行的必须命令要存放在/lib目录下,而对于其他不是必须的库存放在/usr/lib目录就可以。

4.4.1.2常见的根文件系统

常见的根文件系统有:RomFS、JFFS2、NFS、EXT2、RAMDISK、Cramfs等。下面将分别介绍各自文件系统的特点。

ü RomFS:是一个空间利用有效的只读文件系统,最初用于Linux和基于Linux的许多

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

项目。它是一个块文件系统,即利用块(或扇区)访问存储驱动(如磁盘,CD和ROM驱动)。内核2.4,2.5和2.6代码都支持RomFS。它有两个特点:一是只读属性,如果你要使用一个RomFS文件系统的磁盘,必须预先写该磁盘的驱动程序,让它看起来是一个块设备。二是要求存储空间最小,它是根文件系统中存储空间要求最小的一个,因为没有修改日期,没有访问权限等属性。

ü JFFS2:JFFS2是The Journalling Flash File System, version 2的缩写, JFFS2是Flash

嵌入式系统上应用最广的一个日志结构的文件系统。它提供的垃圾回收机制,使得我们不需要马上对擦写越界的块进行擦写,而只需要将其设置一个标志,标明为脏块。当可用的块数不足时,垃圾回收机制才开始回收这些节点。同时,由于JFFS2基于日志结构,在意外掉电后仍然可以保持数据的完整性,而不会丢失数据。JFFS2的不足之处有挂载时间过长和扩展性较差等缺点。

ü NFS:NFS是Network File System的缩写,即网络文件系统。它是FreeBSD支持的

文件系统中的一种。 NFS允许一个系统在网络上与它人共享目录和文件。通过使用NFS,用户和程序可以象访问本地文件一样访问远端系统上的文件。它的优点有:一是本地工作站使用更少的磁盘空间,因为通常的数据可以存放在一台机器上而且可以通过网络访问到;二是用户不必在每个网络上机器里头都有一个home目录。Home目录可以被放在NFS服务器上并且在网络上处处可用;三是软驱,CDROM,和 Zip? 之类的存储设备可以在网络上面被别的机器使用。这可以减少整个网络上的可移动介质设备的数量。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

ü EXT2:EXT2是The Second Extended Filesystem的缩写,它最初发布于1993年1月。[www.61k.com]

它仍旧是当前一个主要的文件系统在Linux中,它还可用于NetBSD, FreeBSD, the GNU HURD, Windows 95/98/NT, OS/2 和RISC操作系统等。其特点为存取文件的性能极好,对于中小型的文件更显示出优势,这主要得利于其簇块取层的优良设计。其单一文件大小与文件系统本身的容量上限与文件系统本身的簇大小有关,在一般常见的 x86 电脑系统中,簇最大为 4KB, 则单一文件大小上限为 2048GB, 而文件系统的容量上限为 16384GB。至于Ext3文件系统,它属于一种日志文件系统,是对EXT2系统的扩展。它兼容EXT2,并且从EXT2转换成EXT3并不复杂。

ü RAMDISK:RAMDISK存在于RAM中,其存取功能类似块设备。使用RAMDISK

文件系统,在系统启动时,首先把外存(Flash)上的映像文件解压缩到内存中,构造起RAMDISK环境,然后可以开始运行程序。内核可以在同一时间支持多个活动的RAMDISK,在RAMDISK上可以使用任何磁盘文件系统。RAMDISK通常会从经压缩的磁盘文件系统(例如ext2)加载其内容,因此内核必须具备从存储设备取出initrd(initial RAM disk)映像作为它的根文件系统的能力。启动时,内核会确认引导选项是否指示有initrd的存在,如果有就会从所选定的存储设备取出文件系统映像放入RAMDISK,并且将它安装成根文件系统。

ü Cramfs:Cramfs被设计为简单且非常小的可压缩的文件系统,它主要用于较小ROM

的嵌入式系统。在嵌入式的环境之下,内存和外存资源都需要节约使用。如果使用RAMDISK方式来使用文件系统,那么在系统运行之后,首先要把外存(Flash)上的映像文件解压缩到内存中,构造起RAMDISK环境,才可以开始运行程序。但是它也有很致命的弱点。在正常情况下,同样的代码不仅在外存中占据了空间(以压缩后的形式存在),而且还在内存中占用了更大的空间(以解压缩之后的形式存在),这违背了嵌入式环境下尽量节省资源的要求。使用Cramfs就是一种解决这个问题的方式。Cramfs是一个压缩式的文件系统,它并不需要一次性地将文件系统中的所有内容都解压缩到内存之中,而只是在系统需要访问某个位置的数据的时侯,马上计算出该数据在Cramfs中的位置,将其实时地解压缩到内存之中,然后通过对内存的访问来获取文件系统中需要读取的数据。Cramfs中的解压缩以及解压缩之后的内存中数据存放位置都是由Cramfs文件系统本身进行维护的,用户并不需要了解具体的实现过程,因此这种方式增强了透明度,对开发人员来说,既方便,又节省了存储空间。

总之,在实际开发应用中,根文件系统还有许多种类,这里就不再一一介绍,下面将讲述如何选择根文件系统。

4.4.1.3选择根文件系统

选择一个文件系统用于根文件系统是一个取舍的过程,最后的决定往往是一个文件系统性能和目标用途的折中。通常选择一个文件系统需要注意以下几个特点:

ü 可写:是否该文件系统能被写数据,只有当一个文件系统发现有更新的数据时需要可

写的文件系统,通常嵌入式根文件系统并不需要可写的功能。

ü 可保存:是否该文件系统在在重启后能保存修改后的东西,一般是在有可写功能的基

础上才会有该功能。

ü 可压缩:是否挂载的文件系统内容可被压缩,通常情况下该功能对于嵌入式系统非常

有用,因为它可以节省宝贵的存储空间。

ü 存在RAM:是否可以在挂载之前将该文件系统的内容第一次从存储设备解压到RAM

中,通常许多文件系统被直接从存储设备挂载。文件系统挂载在RAM磁盘必须首先

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

从外存储设备解压到RAM中,然后执行挂载。[www.61k.com] ü 可恢复:当突然断电时能否恢复文件系统的修改

表4.2列出了常见的文件系统特点,在选择文件系统时一定要考虑到这些特点,从而选择最适合的根文件系统。 表4.2常见文件系统特点

常见的文件系统 CRAMFS JFFS2 JFFS

Ext2 over NFTL Ext3 over NFTL Ext2 over RAM disk

可写 可保存 No Yes Yes Yes Yes Yes

N/A Yes Yes Yes Yes No

可恢复 N/A Yes Yes No Yes No

可压缩 Yes Yes No No No No

存在RAM No No No No No Yes

总之,在选择根文件系统时,如果你的系统有非常小的flash,但是有相对比较大的RAM,建议选择RAMDISK作为根文件系统,因为该文件系统可以存在RAM磁盘被完全的压缩在外存设备上。如果你的系统有稍微多的flash或者你希望保存尽可能多的RAM在实际的应用程序运行时,Cramfs根文件系统是一个不错的选择,尽管Cramfs的压缩率低于RAMDISK,但是它的性能通常对于许多嵌入式应用来说非常充分,因为它不要求永久保存数据。如果你的目标系统需要能够更新在文件系统中,那么Cramfs将不是一个可行的选择。如果你需要一个能够经常改变的文件系统,JFFS2文件系统将是一个很好的选择,尽管JFFS2没有Cramfs那么高的压缩率,因为Cramfs没有垃圾回收和元数据结构,但是JFFS2提供了断电可恢复的机制,这点是非常重要的在依靠flash存储的设备中。但是当你使用NAND flash设备时,JFFS2是不可行的。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

下面将以Cramfs根文件系统为例,讲述如何构建自己的根文件系统。

4.4.2建立根文件系统

本小节以Cramfs根文件系统为例,讲述Cramfs工具包和构建Cramfs根文件系统,首先讲述Cramfs工具包的使用。

4.4.2.1Cramfs工具包的使用

从cramfs-1.1.tar.gz。然后解压并且查看解压后的目录结构如下:

# tar xvzf cramfs-1.1.tar.gz # cd cramfs-1.1 # tree .

|-- COPYING |-- GNUmakefile |-- NOTES

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

|-- README

|-- cramfsck.c

|-- linux

| |-- cramfs_fs.h

| `-- cramfs_fs_sb.h

`-- mkcramfs.c

1 directory, 8 files

利用cramfs工具包主要是为了生成mkcramfs和cramfsck两个工具,其中mkcramfs工具是用来创建cramfs文件系统的,而cramfsck工具则用来进行cramfs文件系统的释放以及检查。(www.61k.com)通过执行make命令将生成mkcramfs和cramfsck工具,具体过程如下: # make

# tree

.

|-- COPYING

|-- GNUmakefile

|-- NOTES

|-- README

|-- cramfsck

|-- cramfsck.c

|-- linux

| |-- cramfs_fs.h

| `-- cramfs_fs_sb.h

|-- mkcramfs

`-- mkcramfs.c

1 directory, 10 files

很明显可以从上面的目录结构中看出编译后生成了mkcramfs和cramfsck两个可执行性文件。

4.2.2.2构建Cramfs根文件系统

Cramfs是Linux Torvalds编写的只具备最基本特性的文件系统,它非常简单、经过压缩并且只读,主要用于嵌入式系统,它具有以下限制。

ü 每个文件最大不超过16MB

ü 不提供当前目录“.”和上级目录“..”

ü 文件的UID字段只有16位,GID字段只有8位

ü 所有文件的时间戳为Unix epoch(00:00:00 GMT, January 1, 1970)

ü 内存分页大小必须是4096字节

ü 文件链接计数器永远是1

由于构建Cramfs映射需要使用mkcramfs和cramfsck工具,所以首先介绍以下这两个工具的使用方法。

下面是mkcramfs的命令格式:

mkcramfs [-h] [-e edition] [-i file] [-n name] dirname outfile

其中个参数解释如下:

-h:显示帮助信息

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

-e edition:设置生成的文件系统中的版本号 -i file:将一个文件映像插入这个文件系统之中(只能在Linux2.4.0以后的内核版本中使用) -n name:设定cramfs文件系统的名字 dirname:指明需要被压缩的整个目录树 outfile:最终输出的文件 cramfsck的命令格式如下: cramfsck [-hv] [-x dir] file 其中个参数解释如下:

-h:显示帮助信息

-x dir:释放文件到dir所指出的目录中

-v:输出信息更加详细

file:希望测试的目标文件

在本地建立自己根文件系统目录结构,首先建立名为myrootfs的根目录,然后在其目录下建立所需要的子目录,具体操作如下:

# mkdir myrootfs

# mkdir bin dev etc lib proc sbin tmp usr var

# mkdir usr/bin usr/lib usr/sbin

目录建立好以后,然后就要给各相应的目录复制相应的文件或库,例如给lib目录下要拷贝glibc库和内核模块,给etc目录下建立一些系统配置文件,bin目录下放置常用的命令工具,下面将介绍在嵌入式系统中常用的BusyBox工具来制作命令工具集。(www.61k.com)

BusyBox 是很多标准 Linux 工具的单个可执行实现。BusyBox 包含了一些简单的工具,例如 cat 和 echo,还包含了一些更大、更复杂的工具,例如 grep、find、mount 以及 telnet;有些人将 BusyBox 称为 Linux 工具里的瑞士军刀。BusyBox提供一个公平、完整的POSIX环境用于许多小系统,它是一个可配置的工具,可以根据需要配置所需要的工具,目前它已经提供了七十多种 Linux 上标准的工具程序,仅需要几百KB的磁盘空间,在嵌入式系统中经常被使用。BusyBox 通过将很多必需的工具放入到一个可执行程序,并让它们可以共享代码中相同的部分,从而对它们的大小进行了很大程度的缩减,BusyBox 对于嵌入式系统来说是一个非常有用的工具。从http://busybox.net/downloads/站点可以下载busybox-1.3.2.tar.bz2,首先解压该源码包,然后进行配置,具体操作如下:

# tar xjvf busybox-1.3.2.tar.bz2

# cd busybox-1.3.2

关于BusyBox的配置方法有以下几种:

ü make allnoconfig ——禁止所有的配置选项在.config文件

ü make allyesconfig ——启动所有的配置选项在.config文件

ü make allbareconfig ——启动所有 applets程序不包括任何子目录下的选项 ü make config ——基于文本的配置方式,很少用这种方式

ü make defconfig ——设置.config配置文件为最大通用配置

ü make menuconfig ——交互式图形化配置方式,该方法应用最广

ü make oldconfig ——配置哪些还没有被配置的选项在.config配置文件中 最常用的配置方法是make menuconfig,当执行该命令时将显示图4.3的配置界面,如果你希望选择尽可能多的配置项,那么就可以直接使用make defconfig命令,它会自动配置为最大通用的配置选项,从而使得配置过程变得更加简单、快速。

配置完BusyBox后,接下来就要编译它,编译之前修改BusyBox源代码根目录下的

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

Makefile文件,修改的内容是:ARCH=arm;CROSS_COMPILE=arm-linux-,修改的目的和编译内核时修改是一样的,由于都是基于ARM平台,并且都是使用交叉编译环境制作。(www.61k.com)编译过程非常简单,在命令行输入make即可,该编译过程一般需要几分钟完成。编译正确完成后,执行安装命令,即执行make install命令。安装完成后,默认情况下,会在_install目录下生成bin,sbin,usr/bin和usr/sbin四个目录,并且在每个目录下都会有许多busybox可执行文件的符合连接,busybox可执行文件存在bin目录下。最后将这四个目录下的文件分别拷贝到我们要构建的根文件系统的相应目录下即:myrootfs/bin,myrootfs/sbin,myrootfs/usr/bin和myrootfs/sbin目录下。在准备好要构建的文件系统下所有库和文件后,下面将使用mkcramfs工具来制作Cramfs根文件系统的映象。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图4.3 BusyBox的配置界面

执行以下命令将生成根文件系统的映象文件,下面的命令将生成名为myrootfs.cramfs的映像文件。

# mkcramfs myrootfs myrootfs.cramfs

然后通过mount命令来检查添加的根文件系统目录是否正确,通过以下命令实现,下面的显示结果表明,我们构建的根文件系统目录结构正确,接着进行下载我们的myrootfs.cramfs根文件映象到开发板上,使用DNW或其他工具下载。

# mount –o loop myrootfs.cramfs /mnt

# cd /mnt

# tree –L 2 –d

.

|-- bin

|-- dev

|-- etc

|-- lib

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

|-- proc

|-- sbin

|-- tmp

|-- usr

| |-- bin

| `-- sbin

`-- var

11 directories

到此,关于根文件系统的构建已经完成,读者可以阅读内核中Documentation/filesystems目录下的文档,从而了解更多关于文件系统的知识。[www.61k.com)

4.5本章小节

本章主要讲述了嵌入式Linux系统内核移植的主要过程,本章的内容在实际中应用非常广泛,并且涉及到的内容也非常多。本章首先讲述移植的基本概念,接着讲述内核移植前需要哪些准备工作,然后重点讲述了内核的移植(内核配置、内核编译和内核下载),最后讲述了如何建立自己的根文件系统(包括Cramfs和BusyBox工具介绍)。到此为止,第一部分ARM Linux系统移植已经介绍完毕,其中主要讲述了嵌入式系统地基础概念,Linux开发环境,交叉编译工具链的制作,BootLoader的开发和移植以及Linux内核移植,相信读者通过学习和实践,已经对嵌入式系统的构建有了较深入的了解,如果读者希望再更加深入的学习,请参考其它更高级的开发资料。下面将进入本书的第二部分ARM Linux系统驱动程序设计,这部分内容是实际工作中非常重要并且必不可少的组成部分,相信很多读者对它会感兴趣。

4.6常见问题

1.常见移植的几种情况?

参考答案:

a) 从一个硬件平台移植到另外一个硬件平台

b) 从一个操作系统移植到另一个操作系统

c) 从一种软件库环境移植到另一种软件库环境

2.通常Linux内核移植有哪些基本过程?

参考答案:

a) 对内核进行配置

b) 对内核进行编译

c) 将内核下载到目标板

3.通常根文件系统的目录有哪些?

参考答案: 目录名bin

boot 提供基本的用户命令库 用于BootLoader的静态文件 内 容

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

目录名dev

etc

home

lib

mnt

opt

proc

root

sbin

tmp

usr

var 设备或其他的特殊文件 系统配置文件,包括启动文件 多个用户的主目录 内 容 基本的系统库,例如C库,内核模块等 用于临时挂载的文件系统 可选择的软件包 内核虚拟文件系统和进程信息 根用户的主目录 基本的系统管理二进制库 临时文件 它的二级目录里包含许多应用程序和许多有用的文档,包括X server 一些变化的实例和工具等

5.Cramfs工具包的作用?

参考答案:

利用cramfs工具包主要是为了生成mkcramfs和cramfsck两个工具,其中mkcramfs工具是用来创建cramfs文件系统的,而cramfsck工具则用来进行cramfs文件系统的释放以及检查。(www.61k.com]

6.BusyBox工具的作用?

参考答案:

BusyBox 是很多标准 Linux 工具的单个可执行实现。BusyBox 包含了一些简单的工具,例如 cat 和 echo,还包含了一些更大、更复杂的工具,例如 grep、find、mount 以及 telnet;有些人将 BusyBox 称为 Linux 工具里的瑞士军刀。BusyBox提供一个公平、完整的POSIX环境用于许多小系统,它是一个可配置的工具,可以根据需要配置所需要的工具,目前它已经提供了七十多种 Linux 上标准的工具程序,仅需要几百KB的磁盘空间,在嵌入式系统中经常被使用。BusyBox 通过将很多必需的工具放入到一个可执行程序,并让它们可以共享代码中相同的部分,从而对它们的大小进行了很大程度的缩减,BusyBox 对于嵌入式系统来说是一个非常有用的工具。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

第二部分 ARM Linux 设备驱动程序开发

ARM Linux设备驱动程序开发是本书第二部分要讲述的内容,也是嵌入式系统开发中非常重要的组成部分。[www.61k.com]通常Linux内核把设备驱动程序划分为3种类型:字符设备,块设备和网络设备,本书主要讲述这三种类型的设备驱动程序开发。这部分由第5章到第8章组成,首先第5章讲述了ARM Linux设备驱动程序的基础知识,包括驱动程序作用、分类以及驱动程序中的一些重要概念,对于初学者来说是非常重要的入门级的一章;接着第6章开始讲述Linux设备驱动程序中应用最广的一种类型——字符设备驱动,利用触摸屏设备的驱动开发为实例进行了讲述,使读者对Linux字符设备驱动的开发有全面的了解;然后第7章讲述Linux设备驱动程序中应用最广的另一种类型——块设备驱动,以MMC/SD卡驱动为例,讲述块设备驱动在Linux系统中的实现,使读者对Linux块设备驱动开发有较深入的理解;最后第8章讲述Linux设备驱动程序中应用较多的另一种类型——网络设备驱动,以CS8900A以太网卡驱动为例,讲述网络设备驱动在Linux系统中的具体实现,使读者对Linux网络设备驱动开发有深入的学习。

总之,由于篇幅关系,不可能将每一种Linux设备驱动程序都拿来讲述,并且读者也没有必要了解所有驱动程序的实现过程,希望读者通过这些典型实例的分析与学习,对Linux驱动开发有较深的理解。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

第5章 ARM Linux驱动程序开发入门

本章学习目标:

l 了解驱动程序的作用

l 熟悉Linux设备驱动程序的分类

l 了解Linux内核模块的基本框架

l 熟悉Linux内存与I/O端口概念

l 理解Linux并发控制概念,包括自旋锁与信号量等

l 理解Linux阻塞与非阻塞概念

l 理解Linux中断处理过程

l 熟悉Linux内核调试工具KDB的使用

5.1嵌入式Linux驱动程序介绍

在讲述嵌入式Linux驱动程序开发之前,需要对Linux驱动程序有个大概了解。(www.61k.com)本节主要讲述两个方面,一是驱动程序的作用,二是Linux驱动程序的分类。首先让我们来看一下驱动程序的作用。

5.1.1驱动程序的作用

驱动程序从字面上就可以理解为一类程序,而这类程序的目的一般就是驱动硬件正常工作,所以通常所说的驱动程序都是针对特定的硬件来编写的。驱动程序既可以工作在有操作系统的环境下,也可以工作在无操作系统的环境中。通常在做一些简单的硬件控制时,由于功能比较单一,不需要操作系统来管理,所以针对这种情况下的驱动程序相对来说也比较简单,因为它只完成控制特定硬件的功能而不需要考虑其他的并发任务等情况。但是往往作为一个嵌入式系统,它要实现的任务相对比较多,并且比较复杂,所以需要有操作系统来对它进行管理,所以在这种情况下,编写驱动程序就要考虑到许多其他任务的并发,任务的优先级以及出现中断情况的处理等,所以通常在带有操作系统的环境下编写驱动程序相对比较复杂,但是这也是实际中应用最广的类型,所以对想从事驱动程序开发的读者来说,这部分的内容是很有必要掌握的。本书是以应用最广范的Linux操作系统为参考,讲述如何在Linux系统下开发驱动程序。通常驱动程序在Linux系统中的位置如图5.1所示,在最底层为Linux内核,在其上面有文件系统,网络栈和设备驱动,也就是是说这三部分都是直接工作在Linux内核之上,在最上面一层是我们常见的应用程序,也就是说应用程序的运行是在基于文件系统,网络栈和设备驱动程序之上。所以从图中可以看出设备驱动程序为上层应用程序提供了控制硬件设备的接口,同时它直接与Linux内核打交道,对上层应用程序来说,设备驱动程序封装了Linux内核,所以应用程序不能直接访问内核程序,从而增加了内核的安全性。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图5.1 设备驱动程序在Linux系统中的位置

从嵌入式驱动开发人员角度看,Linux驱动程序是直接操控硬件的软件,它通常完成以下功能:

ü 直接读写硬件寄存器来控制硬件

ü 读写存储设备,如Flash,硬盘等

ü 操作设备缓冲区设备

ü 操作输入/输出设备,如键盘、打印机等

从嵌入式应用程序开发人员角度看,Linux驱动程序为应用程序提供了访问硬件设备的应用编程接口(API,Application Programming Interface),它主要提供以下功能: ü 应用程序通过驱动程序安全有效的访问硬件设备

ü 驱动程序作为嵌入式系统的中间一层软件,它隐藏了底层的细节,从而提高了软件的

可移植性和可复用性

ü 驱动程序文件节点可以方便的提供访问权限控制

总之,驱动程序作为嵌入式系统开发的一部分,具有非常重要的意义,通常情况下,Linux设备驱动程序属于Linux操作系统的一部分。[www.61k.com]

5.1.2 Linux设备驱动程序分类

Linux系统将设备驱动程序一般分为三类:字符设备、块设备和网络设备,不过他们之间的区别并不严格,下面将介绍这三类驱动程序的各自特点:

ü 字符设备

字符设备是能够象字节流(文件)一样被访问的设备,字符设备驱动程序通常会实现open,close,read和write等系统调用函数。系统控制台和并口就是字符设备的例子。通过文件系统节点可以访问字符设备,例如/dev/tty1和/dev/lp1。字符设备和普通文件系统之间的唯一区别是:普通文件允许在其上来回读写,而大多数字符设备仅仅是数据通道,只能顺序读写。此外,字符设备驱动程序不需要缓冲而且不以固定大小进行操作,它直接从用户进程传输数据,或传输数据到用户进程。

ü 块设备

所谓块设备是指对其信息的存取以“块”为单位,如常见的光盘、硬磁盘、软磁盘、磁带等,块长取512字节或1024字节或4096字节。块设备和字符设备一样可以通过文件系统节点来访问。在大多数Unix/Linux系统中,只能将块设备看作多个块进行访问,一个块设备通常是1024字节数据。Linux允许大家象字符设备那样读取块设

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

备,即允许一次传输任意数目的字节。(www.61k.com)块设备和字符设备只在内核内部的管理上有所区别,其中应用程序对于字符设备的每一个I/O操作都会直接传递给系统内核对应的驱动程序;而应用程序对于块设备的操作要经过系统的缓冲区管理间接的传递给驱动程序处理。

ü 网络设备

网络设备驱动在Linux系统中是比较特殊的,它不像字符设备和块设备通常实现read和write等操作,而通常是通过一种套接字(Socket)*(注1)等接口来实现。任何网络事务处理都是通过接口实现的,即可以和其他宿主交换数据的设备。通常接口是一个硬件设备,但也可以象loopback(回路)接口一样是软件工具。网络接口是由内核网络子系统驱动的,它负责发送和接收数据包,而且无需了解每次事务是如何映射到实际被发送的数据包。尽管“telnet”和“ftp”连接都是面向流的,它们使用同样的设备进行传输;但设备并没有看到任何流,仅看到数据报。由于不是面向流的设备,所以网络接口不能象/dev/tty1那样简单地映射到文件系统的节点上。Unix/Linux调用这些接口的方式是给它们分配一个独立的名字(如eth0)。这样的名字在文件系统中并没有对应项。内核和网络设备驱动程序之间的通信与字符设备驱动程序和块设备驱动程序与内核间的通信是完全不一样的。 *注1:套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。可以将套接字看作不同主机间的进程进行双向通信的端点,它构成了单个主机内及整个网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中的套接字交换数据(数据交换也可能穿越域的界限,但这时一定要执行某种解释程序)。各种进程使用这个相同的域互相之间用Internet协议簇来进行通信。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

其实除了这三种类型的驱动程序类型外,还有许多比较特殊的设备驱动程序,如IIC,USB等,只不过在实际中大部分驱动程序都属于这三种类型,所以通常所说的Linux设备驱动程序开发也就是这三类驱动的开发。

5.2最简单的内核模块举例

Linux设备驱动模块属于内核的一部分,这在前面已经介绍,Linux内核的一个模块可以以两种方式被编译和加载,方法一是直接配置编译进Linux内核,随同Linux启动时加载;方法二是编译成一个可加载和卸载的模块,使用insmod加载(modprobe和insmod命令类似,但依赖于相关的配置文件)模块,而rmmod用来卸载模块。通过方法二可以控制内核的大小,而模块一旦被插入内核,它就和内核其他部分一样被使用,此外方法二是动态的加载和卸载模块而不需要重新编译整个内核,所以方便调试。

5.2.1 编写Hello World模块

下面以一个最简单的“Hello World”模块程序为例介绍Linux内核模块程序的框架结构,在准备运行该模块程序之前,建议你已经安装了2.6的内核,并且最好安装的内核版本与你所使用的内核版本是一致的。

1

2

3

4 #include <linux/init.h> #include <linux/module.h> #include <linux/moduleparam.h>

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26 MODULE_LICENSE("Dual BSD/GPL"); static char *who = "world"; static int times = 1; module_param(times, int, S_IRUGO); module_param(who, charp, S_IRUGO); static int hello_init(void) { int i; for (i = 0; i < times; i++) printk(KERN_ALERT "(%d) Hello, %s!\n", i, who); return 0; } static void hello_exit(void) { printk(KERN_ALERT "Goodbye, %s!\n",who); } module_init(hello_init); module_exit(hello_exit);

看到上述代码读者会问这是Linux内核模块程序吗?回答是肯定的,只不过这个模块程序没有任何实际意义,但是它能够说明Linux内核驱动模块程序的一些基部特点。(www.61k.com)第1行包含的头文件是定义__init、__exit、module_init和module_exit必须的宏。第2行包含的头文件定义所有模块相关的宏,如MODULE_LICENSE。第3行包含的头文件是用来定义模块参数所必须的。现在来具体分析一下这段程序,该内核模块程序定义了两个函数,当该模块被装载进内核时调用hello_init函数,当该模块被卸载时调用hello_exit函数。其中module_init和module_exit为内核特殊的宏,用来定义模块被装载和卸载时依次分别调用的函数。其中第5行用MODULE_LICENSE宏来声明该模块的许可协议,该模块声明为BSD(Berkly Software Distribution)和GPL(General Public License)双重许可协议。内核函数printk被定义在Linux内核中,它行为简单类似于标准C库函数printf。也许读者会问为什么不用printf函数呢?细心的读者应该记得在第二章我们讲述交叉编译工具链的时候就说过,编译Linux内核不需要标准C库和其他函数库的支持,所以这里就不能使用printf库函数,然而内核需要自己的打印函数即printk,它通过自身内核运行而不需要C库的帮助。内核模块之所以能够调用printk函数,是因为它是在insmod装载之后,此时内核模块可以与内核公共函数和变量进行链接,从而可以使用printk函数。其中KERN_ALERT宏是标记printk打印出的字符串优先等级,通常有8种消息等级,定义在<include/linux/kernel.h>文件中,具体定义如下:

#define

#define

#define

#define

#define

#define KERN_EMERG "<0>" /* system is unusable */ KERN_ALERT "<1>" /* action must be taken immediately */ KERN_CRIT "<2>" /* critical conditions */ KERN_ERR "<3>" /* error conditions */ KERN_WARNING "<4>" /* warning conditions */ KERN_NOTICE "<5>" /* normal but significant condition */

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

#define KERN_INFO "<6>" /* informational */

#define KERN_DEBUG "<7>" /* debug-level messages

*/ 这些宏的具体说明如下:

ü KERN_EMERG

用于紧急事件发生时的消息说明,一般用在系统崩溃之前的说明消息

ü KERN_ALERT

用于需要立即执行的情况

ü KERN_CRIT

用在临界状态下的情况,如硬件或软件的故障时

ü KERN_ERR

用于报告一个错误条件,设备驱动经常使用它来报告硬件错误

ü KERN_WARNING

用于有问题情况下的警告,通常不会警告一个严重的系统问题

ü KERN_NOTICE

用于正常的通知,许多安全相关的条件由这个级别的宏报告

ü KERN_INFO

用于打印一些相关信息消息,许多驱动程序用这个级别宏打印一些硬件的启动时信息 ü KERN_DEBUG

用于调试信息。(www.61k.com]

上面的程序中还应用到一个重要的概念,即模块参数。在实际设备驱动模块开发中,经常需要传递给硬件设备一些不同的指令来控制不同的功能,其中模块参数就是用来传递给硬件设备指令的。该程序中利用参数who来指定一个对象,用times来指定打印“Hello,who!”信息的次数。其中module_param宏是Linux 2.6内核中新增的,该宏被定义在<include/linux/ moduleparam.h>文件中,具体定义如下:

#define module_param(name, type, perm)

module_param_named(name, name, type, perm) \

其中,参数name为要传递的参数变量,参数type为变量的数据类型,参数perm为访问参数的权限。

5.2.2编写Hello World模块的Makefile

接下来编写一个Makefile文件来编译“Hello World”模块程序,此处Makefile的内容编写如下: EXEC = hello

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

OBJS = hello.o

SRC = hello.c

INCLUDE = /usr/src/linux-2.6.10/include

CC = arm-linux-gcc

LD = arm-linux-ld

MODCFLAGS = -O2 -Wall -D__KERNEL__ -DMODULE -I$(INCLUDE) -march=armv4t -c -o

LDFLAGS = -r

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

all: $(EXEC)

$(EXEC): $(OBJS)

$(LD) $(LDFLAGS) -o $@ $(OBJS)

%.o:%.c

$(CC) $(MODCFLAGS) -mapcs -c $< -o $@

clean:

-rm -f $(EXEC) *.o *~ core

在上面的Makefile文件中,INCLUDE来指定要编译的程序需要的头文件路径,这个路径要根据你的内核所放的位置来定。(www.61k.com]其中CC和LD定义使用的编译器和连接器,由于我们希望在S3C2410开发板上运行,所以需要使用交叉编译器来执行。MODCFLAGS变量来指定编译该模块时所用的编译选项,LDFLAGS变量定义了连接选项。

5.2.3加载和卸载Hello World模块

接下来在存放Makefile和“Hello World”源程序的目录下执行make命令,经过编译会在该目录下生成hello和hello.o的基于ARM体系的可执行文件。然后将这些可执行性文件下载到开发板上执行以下命令:

# insmod hello who=“world” times=5

执行这条命令的作用是,装载该模块到内核,并且传递了两个参数,who参数传递为world,times参数传递为5。执行这条命令将在控制台显示5次“Hello world!”。 接下来执行卸载模块操作,通过执行以下命令来卸载刚才加载的模块,具体操作如下:

# rmmod hello

Goodbye, world!

通过“Hello World”一个完整模块程序的学习,读者应该会觉得Linux内核模块开发并不是一件难事。的确,Linux内核开发并不像许多人想象的那样深奥,其实一个庞大的体系结构往往都是由简单的模块组合而成,我相信读者只要下定决心去学,一定能够成为Linux开发高手。下面将介绍Linux驱动程序开发一些要点。

5.3 Linux驱动程序开发要点

对于初学Linux驱动开发的读者来说,首先需要掌握一些Linux驱动程序开发相关的基本知识,比如内存与I/O端口,并发控制,阻塞与非阻塞操作,中断处理和内核调试等,下面将分别具体讲述这些重要的概念。

5.3.1 内存与I/O端口

内存与I/O端口是Linux驱动设备开发经常用到的两个概念,编写驱动程序大多数情况下都是对内存和I/O端口的操作。下面将分别介绍内存和I/O端口在Linux设备驱动程序中的使用。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

5.3.1.1内存

对于运行标准的Linux内核平台需要提供对MMU(内存管理单元)的支持,并且Linux内核提供了复杂的存储管理系统,使得进程能够访问的内存达到4GB。[www.61k.com]这4GB的空间被人们分为两个部分,一是用户空间,二是内核空间。用户空间的地址分布是从0到3GB,3GB到4GB空间定义为内核空间。编写Linux驱动程序必须知道如何在内核中申请内存,内核中最常用的内存分配和释放函数是kmalloc和kfree,这两个函数非常类似标准C库中的malloc和free。这两个函数原型如下:

void *kmalloc(size_t size, int flags);

void kfree(void *obj);

这两个函数被声名在内核源码<include/linux/slab.h>文件中,设备驱动程序作为内核的一部分,不能使用虚拟内存,必须利用内核提供的kmalloc与kfree来申请和释放内核存储空间。Kmalloc带两个参数,第一个参数(size)是要申请的是内存数量;第二个参数(flags)用来控制kmalloc的优先权。其中flags参数的经常有以下几种:

ü GFP_KERNEL:它的意思是该内存分配(内部是通过调用get_free_pages来实现的,

所以名字中带GFP)是由运行在内核态的进程调用的。也就是说,调用它的函数是属于某个进程的,使用GFP_KERNEL优先允许kmalloc函数在系统空闲内存低于水平线min_free_pages时延迟分配函数的返回。当空闲内存太少时,kmalloc函数会使当前进程进入sleep(睡眠)*(注2)状态,等待空闲页的出现。

ü GFP_ATOMIC:并非使用GFP_KERNEL优先权后一定正确,有时kmalloc是在进程

上下文之外调用的-一比如在中断处理,任务队列处理和内核定时器处理时发生。这些情况下,当前的进程就不应该进入sleep状态,这时应该就使用优先权GFP_ATOMIC。原子性(atomic)的内存分配允许使用内存的空闲位,而与min_free_pages值无关。实际上,这个最低水平线值的存在就是为了能满足原子性的请求。但由于内核并不允许通过换出数据或缩减文件系统缓冲区来满足这种分配请求,所以必须还有一些真正可以获得的空闲内存。

ü GFP_USER:用来分配内存给用户空间的页,当空闲内存太少时,kmalloc函数会使

当前进程进入sleep状态,等待空闲页的出现。

ü GFP_HIGHUSER:类似GFP_USER,只是从高处分配内存。

ü GFP_NOIO:类似GFP_KERNEL,只是增加了禁止任何I/O初始化的限制。 ü GFP_NOFS:类似GFP_KERNEL,不允许执行任何文件系统的调用。

*注2:睡眠(sleep)是一个非常重要的概念在设备驱动程序开发中,当一个进程被置入sleep状态时,它会被标记为一种特殊状态并从调度器的运行队列中移走,直到某些情况下修改了这个状态,进程才会在CPU上调度,也就是运行该进程。进入sleep状态的进程会被搁置在一边,等待将来的某个事件发生。

关于kmalloc与kfree的具体实现,可参考内核源程序中的<include/linux/slab.h>文件。如果希望分配大一点的内存空间,内核会利用一个更好的面向页的机制,分配页的相关函数有以下三个,这三个函数的定义在<mm/ page_alloc.c>文件中。

ü get_zeroed_page(unsigned int gfp_mask);

该函数的作用是申请一个新的页,初始化该页的值为零,并返回页的指针。

ü _ _get_free_page(unsigned int flags);

该函数类似于get_zeroed_page,但是它不初始化页的值为零。

ü _ _get_free_pages(unsigned int flags, unsigned int order);

该函数类似于_ _get_free_page,但是它可以申请多个页,并且返回的是第一个页的指针。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

再介绍一个内存相关的重要概念,虚拟内存空间的分配,以上介绍的内存分配函数都是针对实际的物理内存而言的,但在Linux系统中经常会使用虚拟内存的技术,虚拟内存通俗的说就是系统在硬盘上建立的缓冲区,它并不是真正的实际内存,是计算机使用的临时存储器,用来运行所需内存大于计算机具有的内存的程序。[www.61k.com)说到虚拟内存,首先需要说明一下Linux的地址类型,Linux通常有以下几种地址类型:

ü 用户虚拟地址

这类地址是用户空间编程的常规地址,该地址通常是32或64位,它依赖于使用的硬件体系结构,并且每一个进程有其自己的用户空间。

ü 物理地址

这类地址是用在处理器和系统内存之间的地址,该地址通常是32或64位,在有些情况下,32位系统可以使用更大的物理地址。

ü 总线地址

这类地址用在外围总线和内存之间,通常它们和被CPU使用的物理地址一样。一些体系结构可以提供一个I/O内存管理单元(I/OMMU),它可以重新映射地址在总线和主存之间。总线地址是与体系结构密不可分的。

ü 内核逻辑地址

该类地址是由普通的内核地址空间组成,这些地址映射一部分或全部主存,并且经常被对待如同物理地址。在许多体系结构下,逻辑地址和物理地址之间只是差别一个恒定的偏移量。逻辑地址通常储存一些变量类型,如long, int,void*等。利用kmalloc可以申请返回一个内核逻辑地址。

ü 内核虚拟地址

在从内核空间地址映射到物理地址时,内核虚拟地址与内核逻辑地址类似。内核虚拟地址并不一定是线性的,一对一的映射到物理地址。所有的逻辑地址都是内核虚拟地址,但是许多内核虚拟地址却不是逻辑地址。内核虚拟地址通常储存在指针变量中。 这五种地址类型经常被使用在Linux系统中,如图5.2给出了这几种地址在系统中的逻辑关系。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图5.2 Linux系统中的地址类型

如果你有一个逻辑地址,通过宏__pa(x)可以得到相应的物理地址,另外,通过宏__va(x)可以计算获得物理地址对应的逻辑地址。(www.61k.com)这两个宏被定义在<include/asm/page.h>中,具体定义如下:

#define __pa(x)

#define __va(x)

((unsigned long) (x) - PAGE_OFFSET) ((void *)((unsigned long) (x) + PAGE_OFFSET)) 其中PAGE_OFFSET会根据平台的不同定义也就会不同。

现在让我们来学习虚拟内存分配相关的知识,虚拟内存分配函数通常是vmalloc(也有vmalloc_32和__vmalloc),它分配虚拟地址空间的连续区域。尽管这段区域在物理上可能是不连续的,内核却认为它们在地址上是连续的。分配的内存空间被映射进入内核数据段中,对用户空间是不可见的,这一点上与其他分配技术不同。Vmalloc和其相关函数的原型如下所示,这些函数被包含在<include/linux/vmalloc.h>头文件中。

ü void* vmalloc(unsigned long size);

该函数的作用是申请size大小的虚拟内存空间,发生错误时返回0,成功时返回一个指向一个大小为size的线性地址空间的指针。参数size为申请内存的大小。

ü void vfree(void* addr);

该函数的作用是释放一个由vmalloc(vmalloc_32或__vmalloc)函数申请内存,释放的内存基地址为addr。

ü void *vmap(struct page **pages, unsigned int count, unsigned long flags, pgprot_t prot);

该函数的作用是映射一个数组(其内容为页)到连续的虚拟空间中。第一个参数pages为指向页数组的指针,第二个参数count为要映射页的个数,第三个参数flags为传递vm_area->flags值,第四个参数prot为映射时页保护。

ü void vunmap(void *addr);

该函数的作用是释放由vmap映射的虚拟内存,释放从addr地址开始的连续的虚拟区

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

域。(www.61k.com]

与其他内存分配函数不同的是,vmalloc返回很“高”的地址值,这些地址要高于物理内存的顶部。由于vmalloc对页表调整后允许用连续的“高”地址访问分配得到的页面,因此处理器是可以访问返回得到的内存区域的。内核能和其他地址一样地使用vmalloc返回的地址,但程序中用到的这个地址与地址总线上的地址并不相同。用vmalloc分配得到的地址是不能在微处理器之外使用的,因为它们只有在处理器的分页单元之上才有意义。但驱动程序需要真正的物理地址时,就不能使用vmalloc了。正确使用vmalloc函数的场合是为软件分配一大块连续的用于缓冲的内存区域。注意vmalloc的开销要比__get_free_pages大,因为它处理获取内存还要建立页表。因此,不值得用vmalloc函数只分配一页的内存空间。

5.3.1.2 I/O端口

接下来介绍一下经常会用到的I/O端口概念,也叫I/O寄存器。和硬件打交道离不开I/O端口,在linux下,操作系统没有对I/O端口屏蔽,也就是说,任何驱动程序都可以对任意的I/O端口操作,这样就很容易引起混乱。每个驱动程序应该自己避免误用端口。I/O端口有点类似内存位置,可以用访问内存芯片相同的电信号对它进行读写。但这两者实际上并不一样,端口操作是直接对外设进行的,和内存相比更不灵活,而且有8位的端口,也有16位的端口和32位的端口,不能相互混淆使用。C语言程序必须调用不同的函数来访问大小不同的端口。

有两个重要的内核函数可以保证驱动程序使用正确的端口,以下两个函数定义在<include/linux/ioport.h>中。

ü check_region(unsigned long s, unsigned long n);

该函数作用是察看系统的I/O表,看是否有别的驱动程序占用某一段I/O口。第一个参数s是I/O端口的基地址;第二个参数是I/O端口占用的范围。返回值为0时表示没有占用,非0时表示已经被占用。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

ü request_region(start,n,name);

该函数的作用是如果这段I/O端口没有被占用,在我们的驱动程序中就可以使用它。在使用之前,必须向系统注册,以防止被其他程序占用。注册后,在/proc/ioports 文件中可以看到你注册的I/O端口。第一个参数start是I/O端口的基地址。第二个参数是I/O端口占用的范围。第三个参数是使用这段I/O地址的设备名。

根据CPU体系结构的不同,CPU对I/O端口的编址方式通常有两种:第一种是I/O映射(I/O-mapped)方式,如X86处理器为外设专门实现了一个单独的地址空间,称为I/O地址空间或I/O端口空间,CPU通过专门的I/O指令(如X86的IN和OUT指令)来访问这一空间的地址单元;第二种是内存映射(Memory-mapped)方式,RISC指令系统的CPU(如ARM,PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为了内存的一部分,此时CPU访问I/O端口就像访问一个内存单元不需要单独的I/O指令。这两种方式在硬件实现上的差异对软件来说是完全可见的,驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是I/O内存资源。

I/O端口的主要作用是用来控制硬件,如何控制硬件?其实就是对I/O端口进行具体操作,内核中对I/O端口进行操作的函数同样也分为两类,第一类是基于I/O端口映射方式的,其定义在体系结构相关的<asm/io.h>文件中,常用函数如下:

ü inb(unsigned long port);

按字节(8位宽度)读端口。参数port为要读取的端口号。参数port在一些平台上定义为unsigned long,而在另一些平台上定义为unsigned short。不同平台上inb返回值的

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

类型也不相同。[www.61k.com]

ü outb(unsigned long value, unsigned long port);

按字节(8位宽度)写端口。第一个参数value是要写进端口的值,第二个参数port为要写的端口号。

ü inw(unsigned long port);

按字(16位宽度)读端口。参数port为要读取的端口号。

ü outw(unsigned long value, unsigned long port);

按字(16位宽度)写端口。第一个参数value是要写进端口的值,第二个参数port为要写的端口号。

ü inl(unsigned long port)

按双字(32位宽度)读端口。参数port为要读取的端口号。

ü outb(unsigned long value, unsigned long port)

按双字(32位宽度)写端口。第一个参数value是要写进端口的值,第二个参数port为要写的端口号。

第二类I/O端口操作函数是基于内存映射方式的,常用函数如下:

ü readb(const volatile void __iomem *addr);

按字节(8位宽度)读指定地址单元。参数addr为要读取的内存地址。

ü writeb(unsigned char b, volatile void __iomem *addr);

按字节(8位宽度)写数据到指定地址单元。第一个参数b为要写的值,第二个参数addr为要写到的内存地址。

ü readw(const volatile void __iomem *addr)

与readb类似,只是按字(16位宽度)读指定地址单元。参数addr为要读取的内存地址。 ü writew(unsigned short b, volatile void __iomem *addr);

与writeb类似,只是按字(16位宽度)写到指定地址单元。第一个参数b为要写的值,第二个参数addr为要写到的内存地址。

ü readl(const volatile void __iomem *addr);

与readb类似,只是按双字(32位宽度)读指定地址单元。参数addr为要读取的内存地址。

ü writel(unsigned int b, volatile void __iomem *addr);

与writeb类似,只是按双字(32位宽度)写到指定地址单元。第一个参数b为要写的值,第二个参数addr为要写到的内存地址。

总之,内存与I/O端口是Linux驱动程序开发中非常重要的两个概念,所以希望读者对他们有较深入的理解,以便更好的学习Linux设备驱动程序开发。

5.3.2并发控制

在驱动程序中经常会出现这种情况,即多个进程同时访问相同的资源时可能会出现竟态(race condition),即竞争资源状态,因此我们必须对共享资料进行并发控制。Linux内核中解决并发控制最常用的方法是自旋锁(Spinlocks)和信号量(Semaphores)。下面将分别介绍这两种方法。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

5.3.2.1自旋锁(Spinlocks)

自旋锁作为保护数据并发访问的一种重要方法,在Linux内核及驱动程序编写中经常采用。(www.61k.com]自旋锁的名字来自它的特性,在试图加锁的时候,如果当前锁已经处于“锁定”状态,加锁进程就进行“旋转”,用一个死循环测试锁的状态,直到成功的取得锁。自旋锁的这种特性避免了调用进程的挂起,用“旋转”来取代进程切换。而我们知道上下文切换需要一定时间,并且会使高速缓冲失效,对系统性能影响是很大的,所以自旋锁在多处理器环境中非常方便。当然,被自旋锁所保护的“临界代码”一般都比较短,否则就会浪费过多的CPU资源。自旋锁是一个互斥现象的设备,它只能有两个值:locked(锁定)或unlocked(解锁)。它通常作为一个整型值的单个位来实现。在任何时刻,自旋锁只能有一个保持者,即在同一时刻只能有一个进程获得锁。

自旋锁在内核中的相关函数如下所示:

自旋锁的实现函数有:

ü spin_lock(spinlock_t *lock);

该函数用于获得自旋锁lock,如果能够立即获得锁,它就马上返回,否则它将自旋在那里,直到该自旋锁的保持者释放,这时它获得锁并返回。总之,只有它获得锁才返回。

ü spin_lock_irqsave(spinlock_t *lock, unsigned long flags);

该函数获得自旋锁的同时把标志寄存器的值保存到变量flags中并失效本地中断。 ü spin_lock_irq(spinlock_t *lock);

该函数类似于spin_lock_irqsave,只是该函数不保存标志寄存器的值。禁止本地中断并获取指定的锁。

ü spin_lock_bh(spinlock_t *lock);

类似spin_lock,该函数在获得自旋锁之前要求禁止软件中断,而使硬件中断开启着。 此外,还有两个非阻塞(nonblocking)自旋锁函数,关于非阻塞的概念将在后面的小节介绍。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

ü spin_trylock(spinlock_t *lock);

该函数尽力获得自旋锁lock,如果能立即获得锁,它获得锁并返回真,否则不能立即获得锁,立即返回假。它不会自旋等待lock被释放。

ü spin_trylock_bh(spinlock_t *lock);

该函数如果获得了自旋锁,它也将失效本地软中断。如果得不到锁,它什么也不做。因此如果得到了锁,它等同于spin_lock_bh,如果得不到锁,它等同于spin_trylock。如果该宏得到了自旋锁,需要使用spin_unlock_bh来释放。

类似的还有以下释放自旋锁的函数:

ü spin_unlock(spinlock_t *lock);

该函数释放自旋锁lock,它与spin_trylock或spin_lock配对使用。如果spin_trylock返回假,表明没有获得自旋锁,因此不必使用spin_unlock释放。

ü spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);

该函数释放自旋锁lock的同时,也恢复标志寄存器的值为变量flags保存的值。它与spin_lock_irqsave配对使用。

ü spin_unlock_irq(spinlock_t *lock);

该函数释放自旋锁lock的同时,并激活本地中断。它与spin_lock_irq配对应用。 ü spin_unlock_bh(spinlock_t *lock);

该函数释放自旋锁lock的同时,也使能本地的软中断。它与spin_lock_bh配对使用。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

关于读自旋锁的API函数有:

ü read_lock(rwlock_t *lock);

ü read_lock_irqsave(rwlock_t *lock, unsigned long flags);

ü read_lock_irq(rwlock_t *lock);

ü read_lock_bh(rwlock_t *lock);

ü read_unlock(rwlock_t *lock);

ü read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);

ü read_unlock_irq(rwlock_t *lock);

ü read_unlock_bh(rwlock_t *lock);

关于写自旋锁的API函数有:

ü write_lock(rwlock_t *lock);

ü write_lock_irqsave(rwlock_t *lock, unsigned long flags);

ü write_lock_irq(rwlock_t *lock);

ü write_lock_bh(rwlock_t *lock);

ü write_trylock(rwlock_t *lock);

ü write_unlock(rwlock_t *lock);

ü write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);

ü write_unlock_irq(rwlock_t *lock);

ü write_unlock_bh(rwlock_t *lock);

总之,关于自旋锁的API函数还有许多,这里就不再一一介绍了,下面我们可以看一下自旋锁的通常用法。[www.61k.com] ……

spinlock_t my_slock = SPIN_LOCK_UNLOCKED;

spin_lock(&my_slock);

/*临界区*/

spin_unlock(&my_slock);

……

因为自旋锁在同一时刻至多被一个执行线程持有,所以一个时刻只能有一个线程位于临界区,这就为多处理器提供了防止并发访问所需的保护机制,但是在单处理器上,编译的时候不会加入自旋锁。它仅仅被当作一个设置内核抢占机制是否被启用的开关。注意,Linux内核实现的自旋锁是不可递归的,这一点不同于自旋锁在其他操作系统中的实现,如果你想得到一个你正持有的锁,你必须自旋,等待你自己释放这个锁,但是你处于自旋忙等待中,所以永远没有机会释放锁,于是你就被自己锁死了,一定要注意这种情况的发生。

由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁可以用在中断处理程序中,但是在使用时一定要在获取锁之前,首先禁止本地中断(当前处理器上的中断),否则中断处理程序就可能打断正持有锁的内核代码,有可能会试图力争用这个已经被持有的自旋锁。这样一来,中断处理程序就会自旋,等待该锁重新可用,但是锁的持有者在这个中断处理程序执行完毕之前不可能运行,这就会造成双重请求死锁。

自旋锁与下半部(中断程序下半部,在本章后面会介绍),由于下半部可以抢占进程上下文中的代码,所以当下半部和进程上下文共享数据时,必须对进程上下文中的共享数据进行保护,所以需要加锁的同时还要禁止下半部执行。同样,由于中断处理程序可以抢占下半部,所以如果中断处理程序和下半部共享数据,那么就必须在获取恰当的锁的同时还要禁止中断。对于软中断,无论是否同种类型,如果数据被软中断共享,那么它必须得到锁的保护,因为同种类型的两个软中断也可以同时运行在一个系统的多个处理器上。但是同一个处理器

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

上的一个软中断绝不会抢占另一个软中断,因此这种情况下根本不需要禁止下半部。[www.61k.com]

5.3.2.2信号量(Semaphores)

现在介绍信号量,信号量是一个很好理解的概念,一个信号量是一个结合一对函数的整型值,这对函数通常被称为P操作和V操作。一个进程希望进入一个临界区域时将调用P操作在相应的信号量上,如果这个信号量的值大于0,这个值将被减1同时该进程继续进行。相反如果这个信号量的值等于或小于0,则该进程将等待别的进程释放该信号量,然后才能执行。解锁一个信号量通过调用V操作来完成,这个函数的作用正好与P操作相反,调用V操作时信号量的值将增加1,如果需要,同时唤醒哪些等待的进程。当信号量用于互斥现象(多个进程在同时运行一个相同的临界区域)时,此时信号量的值被初始化为1。信号量只能在一个时刻被一个进程或线程拥有,一个信号量使用在这种模式下通常被称为互斥体(mutex,mutual exclusion的缩写)体。几乎所有的信号量在Linux内核中都是用于互斥现象。

信号量和互斥体的实现相关函数有:

ü sema_init(struct semaphore *sem, int val);

该函数用来初始化一个信号量。其中第一个参数sem为指向信号量的指针,val为赋给该信号量的初始值。

ü DECLARE_MUTEX(name);

该宏声明一个信号量name并初始化它的值为1,即声明一个互斥锁。

ü DECLARE_MUTEX_LOCKED(name);

该宏声明一个互斥锁name,但把它的初始值设置为0,即锁在创建时就处在已锁状态。因此对于这种锁,一般是先释放后获得。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

ü init_MUTEX(struct semaphore *sem);

该函数被用在运行时初始化(如在动态分配互斥体的情况下),其作用类似DECLARE_MUTEX,即它把信号量sem的值设置为1。

ü init_MUTEX_LOCKED(struct semaphore *sem);

该函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处在已锁状态。

ü down(struct semaphore *sem);

该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文(包括IRQ上下文和软中断上下文)使用该函数。该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。 ü down_interruptible(struct semaphore *sem);

该函数功能与down类似,不同之处为down不会被信号(signal)打断,但down_interruptible能被信号打断,因此该函数用返回值来区分是正常返回还是被信号中断,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR。 ü down_trylock(struct semaphore *sem);

该函数试着获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,表示不能获得信号量sem,返回值为非0值。因此它不会导致调用者睡眠,可以在中断上下文使用。

ü up(struct semaphore *sem);

该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

自旋锁和信号量有很多相似之处又一些本质的不同,其相同之处有:一是它们对互斥来说都是非常有用的工具。(www.61k.com]二是在任何时刻最多只能有一个线程获得自旋锁或信号量。不同之处有:一是自旋锁可在不能sleep的代码中使用,如在中断服务程序(ISR)中使用,而信号量不可以。二是自旋锁和信号量的实现机制不一样。三是通常自旋锁被用在多处理器系统。总之,通常自旋锁适合保持时间非常短的情况,它可以在任何上下文中使用,而信号量用于保持时间较长的情况,只能在进程上下文中使用。如果被包含的共享资源需要在中断上下文访问时,就只能使用自旋锁。

针对读者和写者信号量的相关函数有:

ü init_rwsem(struct rw_semaphore *sem);

其中rwsem是reader/writer semaphore的缩写,它是Linux内核提供的一种特殊信号量类型,通过这个函数在运行时初始化rwsem的对象。

ü down_read(struct rw_semaphore *sem);

读者调用该函数来得到读写信号量sem。该函数会导致调用者睡眠,因此只能在进程上下文使用。

ü down_read_trylock(struct rw_semaphore *sem);

该函数类似于down_read,只是它不会导致调用者睡眠。它尽力得到读写信号量sem,如果能够立即得到,它就得到该读写信号量,并且返回1,否则表示不能立刻得到该信号量,返回0。因此,它也可以在中断上下文使用。

ü up_read(struct rw_semaphore *sem);

读者使用该函数释放读写信号量sem。它与down_read或down_read_trylock配对使用。如果down_read_trylock返回0,不需要调用up_read来释放读写信号量,因为根本就没有获得信号量。

ü down_write(struct rw_semaphore *sem);

写者使用该函数来得到读写信号量sem,它也会导致调用者睡眠,因此只能在进程上下文使用。

ü down_write_trylock(struct rw_semaphore *sem);

该函数类似于down_write,只是它不会导致调用者睡眠。该函数尽力得到读写信号量,如果能够立刻获得,就获得该读写信号量并且返回1,否则表示无法立刻获得,返回0。它可以在中断上下文使用。

ü up_write(struct rw_semaphore *sem);

写者调用该函数释放信号量sem。它与down_write或down_write_trylock配对使用。如果down_write_trylock返回0,不需要调用up_write,因为返回0表示没有获得该读写信号量。

ü downgrade_write(struct rw_semaphore *sem);

该函数用于把写者降级为读者,这有时是必要的。因为写者是排他性的,因此在写者保持读写信号量期间,任何读者或写者都将无法访问该读写信号量保护的共享资源,对于那些当前条件下不需要写访问的写者,降级为读者将使得等待访问的读者能够立刻访问,从而不但增加了并发性而且提高了效率。

一个读者和写者信号量(rmsem)允许一个写入者或多个读取者拥有该信号量。写者拥有更高的优先级,当某个给定写者试图进入临界区域,在所有写者完成其工作之前,不允许读者获得访问该信号量。如果有大量写者竞争该信号量,则会导致读者长时间被拒绝访问。所以最好在很少需要写访问且写者只会短暂拥有信号量的时候使用rwsem。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

5.3.3阻塞(Blocking)与非阻塞(Nonblocking)

阻塞(Blocking)和非阻塞(Nonblocking)是开发设备驱动程序时必须考虑的两个方面,下面将具体介绍这两个概念,同时也会介绍相关的异步通知(Asynchronous Notification)概念。(www.61k.com)

5.3.3.1阻塞(Blocking)与非阻塞(Nonblocking)操作

阻塞操作是指在执行设备操作时,若不能获得资源则进程挂起,直到满足可操作的条件再进行操作。被挂起的进程进入sleep状态,被从调度器的运行队列中移走,直到等待的条件被满足。非阻塞操作是在不能进行设备操作时并不挂起,它会立即返回,使得应用程序可以快速查询状态。在处理非阻塞型文件时,应用程序在调用stdio函数时必须小心,因为很容易把一个非阻塞操作返回值误认为是EOF(End of File,文件结束符),所以必须始终检查errno(错误类型)。在内核中定义了一个非阻塞标志的宏,即O_NONBLOCK,通常只有read、write和open文件操作受非阻塞标志的影响。在Linux驱动程序中,我们可以使用等待队列(wait queue)来实现阻塞操作。等待队列很早就被用在Linux内核中了,它以队列为基础数据结构,与进程调度机制紧密结合,能够实现重要的异步通知(Asynchronous Notification)机制。下面将具体介绍异步通知的概念。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

5.3.3.2异步通知(Asynchronous Notification)

什么是异步通知?异步通知是指一旦设备准备就绪,则该设备会主动通知应用程序,这样应用程序就不需要不断的查询设备状态,通常把异步通知称为信号驱动的异步I/O(SIGIO),这点类似于硬件上的中断。下面将给出一段应用异步通知的简单例子,光盘中有该程序的参考代码,文件名为sigio.c。

#include <sys/types.h>

#include <sys/stat.h>

#include <stdio.h>

#include <fcntl.h>

#include <signal.h>

#include <unistd.h>

#define MAX_LEN 200

void input_event(int num) //定义一个输入字符的异步事件

{

char data[MAX_LEN];

int len;

len=read(STDIN_FILENO,&data,MAX_LEN); //读取STDIN_FILENO上的输入 data[len]=0;

printf("Input string is :%s\n",data);

}

main()

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

{

} int oflags; signal(SIGIO,input_event); //启动信号驱动机制 fcntl(STDIN_FILENO,F_SETOWN,getpid()); oflags=fcntl(STDIN_FILENO,F_GETFL); fcntl(STDIN_FILENO,F_SETFL,oflags|FASYNC); while(1); //该循环是非常必要的,如果没有该程序将立即执行结束。(www.61k.com]

这段程序的作用就是实现一个简单的异步通知机制,当你在控制台输入一段字符串时,它将显示你输入的字符串,当你没有输入任何字符时,它将一直在执行循环操作,也就是说只有等你发送一个异步事件时,它将执行该异步事件。

使用非阻塞I/O的应用程序经常也使用poll、select和epoll系统调用,这三个函数的功能是一样的,即都允许进程决定是否可以对一个或多个打开的文件做非阻塞的读取和写入。这些调用也会阻塞进程,直到给定的文件描述符集合中的任何一个可读取或写入。poll、select和epoll用于查询设备的状态,以便用户程序是否能对设备进行非阻塞的访问,他们都需要设备驱动程序中的poll函数支持。驱动程序中poll函数最主要的一个API函数是poll_wait,其原型如下:

ü poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p);

该函数并不阻塞,而是把当前任务添加到指定的一个等待列表中,真正的阻塞动作是在select/poll函数中完成的。该函数的作用是把当前进程添加到p参数指定的等待列表(poll_table)中,第一个参数flip是文件指针,第二个参数wait_address是睡眠队列的头指针地址,第三个参数p是指定的等待队列。

阻塞与非阻塞操作是驱动程序开发中经常要考虑的两个操作,关于阻塞与非阻塞的具体使用会在后面章节的实例中出现,希望在这里读者对这些常用的概念有所了解。

5.3.4中断处理

设备的许多工作通常与处理器的速度完全不同,并且总是要比处理器慢,这种让处理器等待外部事件的情况会明显降低处理器的效率,所以必须有一种方法可以让设备在产生某个事件时通知处理器,这种方法被称为中断。一个中断在Linux系统中仅仅是一个信号,当硬件需要获得处理器对它的关注时就可以发送这个中断信号。Linux处理中断的方式在很大程度上与它在用户空间处理信号是一样的,通常一个驱动程序只需要为它自己设备的中断注册一个处理例程,并且在中断到达时进行正确的处理。

5.3.4.1 Linux中断及其相关函数

与Linux设备驱动程序中断处理相关的函数首先是申请和释放IRQ(中断请求)函数,即request_irq和free_irq,这两个非常重要的中断函数原型如下,在头文件<include/linux/interrupt.h>文件中声明。

ü int request_irq(unsigned int irq, irqreturn_t (*handler)(int irq, void *dev_id, struct pt_regs

*regs),unsigned long flags, const char *dev_name, void *dev_id);

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

该函数的作用是注册一个IRQ,其中参数irq是要申请的硬件中断号,参数handler是向系统登记的中断处理函数,是一个回调函数,中断发生时系统调用这个函数,参数dev_id是设备的ID,参数flags是中断处理的属性,若设置为SA_INTERRUPT,表明中断处理程序是FRQ(快速中断请求),FRQ程序被调用时屏蔽所有中断,而IRQ程序被调用时不屏蔽FRQ。[www.61k.com]若设为SA_SHIRQ,则多个设备共享中断,dev_id在中断共享时会用到。参数dev_name是定义传递给request_irq的字符串,用来在/proc/interrupts中显示中断的拥有者。

ü void free_irq(unsigned int irq, void *dev_id);

该函数的作用是释放一个IRQ,一般是在退出设备或关闭设备时调用。

Linux将中断分为两个部分:上半部和下半部。上半部的功能是注册中断,当一个中断发生时,它进行相应地硬件读写后就把中断处理函数的下半部挂到该设备的下半部执行队列中去。因此上半部执行速度很快,可以服务更多的中断请求。但是仅有中断注册是不够的,因为中断事件可能很复杂,因此引出了下半部,用它来完成中断事件的绝大多数任务。上半部和下半部最大的不同是下半部是可中断的,而上半部是不可中断的,下半部完成了中断处理程序的大部分工作,所以通常比较耗时,因此下半部由系统自行安排运行,不在中断服务上下文中执行。Linux实现下半部的机制主要是tasklet和工作队列。Tasklet是一个可以在由系统决定的安全时刻在软件中断上下文被调用运行的特殊函数,它们可以被多次调用运行,但tasklet的调用并不会累积,也就是说只会运行一次,即使在激活tasklet的运行之前重复请求该tasklet的运行也是这样。Tasklet运行时可以有其他中断发生,因此在tasklet和中断服务程序之间的锁还是需要的。必须使用宏DECLARE_TASKLET(name, func, data)来声明tasklet,其中name是给tasklet起的名字,func是执行tasklet时调用的函数,data是一个用来传递给tasklet函数的值。一些设备可以在很短时间内产生多次中断,例如键盘扫描中断,所以在下半部被执行前肯定会有多次中断发生,驱动程序必须对这种情况有所准备,通常利用tasklet可以记录自从上次被调用产生多少次中断,从而让系统知道还有多少工作需要完成。工作队列函数运行在进程上下文中,因此可在必要时sleep,工作队列的中断服务程序和tasklet版本非常类似,唯一不同就是它调用schedule_work来调度下半部处理,而tasklet使用tasklet_schedule来调度下半部处理。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

5.3.4.2 ARM中断处理

通常ARM中将中断又叫异常(Exception),当中断或异常发生时,系统执行完当前指令后,将跳转到相应的异常中断处理程序处执行。当异常中断处理程序执行完成后,程序返回到发生中断的指令的下一条指令处执行。在进入异常中断处理程序时,要保存被中断的程序的执行现场。从异常中断处理程序退出时,要恢复被中断的程序的执行现场。ARM体系中通常在存储地址的低端固化了一个32字节的硬件中断向量表,用来指定各种异常中断及其处理程序的对应关系。当一个异常出现以后,ARM微处理器会执行以下几步操作: ü 保存处理器当前状态、中断屏蔽位以及各条件标志位;

ü 设置当前程序状态寄存器(CPSR)中相应的位;

ü 将寄存器lr_mode(当异常出现或调用函数时保存下一条指令的位置)设置成返回地

址;

ü 将程序计数器(PC)值设置成该异常中断的中断向量地址,从而跳转到相应的异常中断

处理程序处执行。

在接收到中断请求以后, ARM处理器内核会自动执行以上四步,程序计数器PC总是跳转到相应的固定地址。然而从异常中断处理程序中返回也会有一些操作,主要包括下面两

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

个基本操作:

ü 恢复被屏蔽的程序的处理器状态;

ü 返回到发生异常中断的指令的下一条指令处继续执行。[www.61k.com)

ARM 有两级外部中断,即 FIQ和IRQ。大多数的基于ARM 的系统有大于2个的中断源,因此需要一个中断控制器(通常是地址映射的)来控制中断是怎样传递给ARM的,如图5.3。在许多系统中,一些中断的优先级比其它中断的优先级高,他们要抢先任何正在处理的低优先级中断。通常中断处理程序总是应该包含清除中断源的代码。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图5.3 ARM中的中断映射

当多个中断产生时,FIQ优先级高于IRQ,处理 FIQ时禁止 IRQs,IRQs 将不会被响应直到 FIQ处理完成。FIQs 的设计使中断处理尽可能的快,FIQ 模式有5个额外的私有寄存器 (r8-r12),中断处理必须保护其使用的非私有寄存器,可以有多个FIQ中断源,但是考虑到系统性能应避免嵌套。

关于ARM中断的程序我们在第三章介绍BootLoader的移植时已经接触过了,不过这里还是列举一个很简单的ARM中断实例,从而加深读者对ARM中断处理的理解。其中用ARM汇编指令实现的代码和相应的注释如下:

AREA SWI_Area, CODE, READONLY ;声明该代码的属性

EXPORT SWI_Handler

IMPORT C_SWI_Handler

T_bit EQU 0x20 ;定义一个位来判断指令模式

SWI_Handler

STMFD sp!, {r0-r3, r12, lr} ; 保存相应的寄存器值

MOV r1, sp

MRS r0, spsr ; 获得spsr

STMFD sp!, {r0} ; 保存 spsr 到栈中

TST r0, #T_bit ; 判断是否在Thumb指令模式?

LDRNEH r0, [lr,#-2] ; 如果是Thumb指令模式的话,装载半字长指令

BICNE r0, r0, #0xFF00

LDREQ r0, [lr,#-4] ; 如果是ARM指令模式的话,装载字长指令 BICEQ r0, r0, #0xFF000000

; r0寄存器现在包含了SWI号

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

; r1包含了指向栈寄存器地址

BL C_SWI_Handler ; 调用高级C语言的中断处理函数

LDMFD sp!, {r0} ; 从栈中获得spsr

MSR spsr_cf, r0 ; 恢复spsr

LDMFD sp!, {r0-r3, r12, pc}^ ; 恢复其他相应的寄存器并且返回

END

其中第一行代码是用来声明该代码的属性,必须声明为CODE和READONLY属性,否则编译不能通过,CODE和READONLY属性分别表示该代码属于代码区,并且为只读属性。(www.61k.com]本实例(第1章中的SWI例子)用来实现一个SWI(Soft Ware Interrupter,软件中断)服务程序,其实现由两部分来完成,一部分就是上面的ARM汇编代码实现的中断服务程序,还有一部分就是其调用的C_SWI_Handler函数实现的中断服务程序,该函数是用高级C语言实现,增加了程序的可读性,并且便于实现更加复杂的功能,这里给出了C_SWI_Handler函数的一个简单实现,如下代码所示:

//用高级C语言实现了软件中断服务程序的复杂功能

void C_SWI_Handler( int swi_num, int *regs )

{

switch( swi_num )

{

case 0:

regs[0] = regs[0] * regs[1];

break;

case 1:

regs[0] = regs[0] + regs[1];

break;

case 2:

regs[0] = (regs[0] * regs[1]) + (regs[2] * regs[3]);

break;

default:

break;

}

}

现在读者应该对ARM中断处理有了一定的了解,下面将介绍一个具体的Linux设备中断实现的实例。

5.3.4.3一个Linux中断相关的实例

关于中断处理通常都是针对硬件设备而产生的概念,所以离开了硬件谈中断都是没有任何意义的,所以在此给出了一个关于中断的具体实例——键盘扫描实例,通常在写一个硬件设备相关的驱动时,需要了解相关硬件的知识,最重要的是参考芯片相关的datasheet(也就是用户手册),同时还需要能看懂基本的电路图。本实例使用8个按键来实现4个外部中断响应的功能,电路原理图如5.4,本书的重点讲述软件相关的知识,关于硬件的知识不会进行深入的分析。但是希望读者一定要牢记,要想成为一名优秀的驱动开发人员,掌握硬件的基本知识是必不可少的。通过5.4原理图可以看到,该模块使用了4个外部中断源,分别是:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

EINT0,EINT2,EINT11(KBDINT)和EINT19,通过查阅S3C2410的datasheet,可以看到这四个中断所对应的I/O端口依次是GPF0,GPF2,GPG3和GPG11。[www.61k.com)同时每两个按键共享一个中断,那么当一个中断被触发时如何区分是这两个键中的哪一个呢?其实很简单,就是利用nSS KBD 和nXDACK1信号线来区别在同一中断情况下使用哪一个按键。其中这两个信号线所对应的端口分别是GPB6和GPB7。该键盘实例的硬件工作原理是:首先配置nSS KBD 和nXDACK1对应的端口GPB6和GPB7为输出,同时配置这四个中断都是下降沿触发,正常工作时,当GPB6配置为低电平和GPB7为高电平时,左边一列的键盘被按下时有效,右边按键无效,当GPB6配置为高电平和GPB7为低电平时,右边一列按键有效,而左边按键无效。所以正常情况下将GPB6和 GPB7之间电平互相高低切换,由于他们之间切换的时间非常短,所以一般用户不会感觉到他们在处理按键上是切换响应的。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

GPIO_BDAT |=1<<7;

if( (GPIO_FDAT&(1<< 0)) == 0 ) return 1 ;

else if( (GPIO_FDAT&(1<< 2)) == 0 ) return 3 ;

else if( (GPIO_G&(1<< 3)) == 0 ) return 5 ;

else if( (GPIO_G&(1<<11)) == 0 ) return 7 ;

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

Delay(300) ;

GPIO_BDAT &=~(1<<7);

GPIO_BDAT |=1<<6;

if( (GPIO_FDAT&(1<< 0)) == 0 ) return 2 ;

else if( (GPIO_FDAT&(1<< 2)) == 0 ) return 4 ;

else if( (GPIO_GDAT&(1<< 3)) == 0 ) return 6 ;

else if( (GPIO_GDAT&(1<<11)) == 0 ) return 8 ;

else return 0xff ;

}

static void key_irq_handle(int irq, void *dev_id, struct pt_regs *reg) //中断服务程序. {

if(INTPND==BIT_EINT8_23)

{

SRCPND=INTPND=BIT_EINT8_23;

}

if(INTPND==BIT_EINT0) {

SRCPND=INTPND=BIT_EINT0;

}

if(INTPND==BIT_EINT2)

{

SRCPND=INTPND=BIT_EINT_2;

}

ready = 1;

key_value=Key_Scan();

printk("key is%d.\n",key_value);

GPIO_BCON &=~((3<<12)|(3<<14));

GPIO_BCON |=((1<<12)|(1<<14));

GPIO_BUP &=~(3<<6);

GPIO_BDAT &=~(3<<6);

wake_up_interruptible(&key_wait);

}

static int key_open(struct inode *inode, struct file *filp)

{

int result;

int i = 0;

struct key_info *key_info;

ready = 0;

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

key_info = key;

for(i=0; i<4; i++)

{

set_external_irq(key_info[i].irq_num, EXT_FALLING_EDGE, GPIO_PULLUP_DIS);

result = request_irq(key_info[i].irq_num, key_irq_handle, SA_INTERRUPT, DEVICE_NAME, NULL);

if(result)

{

printk(KERN_INFO DEVICE_NAME " Failed to request irq.\n"); return result;

}

}

MOD_INC_USE_COUNT;

printk(KERN_INFO DEVICE_NAME ": opened.\n");

return 0;

}

……

static struct file_operations key_fops =

{

owner: THIS_MODULE,

read: key_read,

open: key_open,

release: key_release,

};

static devfs_handle_t devfs_handle;

static int __init key_init(void)

{

devfs_handle = devfs_register(NULL, DEVICE_NAME, DEVFS_FL_DEFAULT, KEY_MAJOR, 0, S_IFCHR | S_IRUSR | S_IWUSR, &key_fops, NULL);

return 0;

}

static void __exit key_exit(void)

{

devfs_unregister(devfs_handle);

}

module_init(key_init);

module_exit(key_exit);

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

由于篇幅的原因,这里只列出了我们所关心的中断处理部分代码,其中最重要的就是key_irq_handle函数,该函数就是按键时产生的中断处理函数,每当按键发生时会调用这个函数,这个函数会根据寄存器的值来判断所按键是哪一个,然后再做相应的操作。[www.61k.com)key_irq_handle函数是在key_open函数中由request_irq函数调用。所以应用程序在想使用该键盘相关的功能时,必须先调用open函数来注册键盘的中断请求。通过这个实例,可以让读者接触到了一个有实际应用价值的设备驱动模块程序,虽然读者可能还不理解其具体的实现,不过不用担心,在后面的章节中会具体讲述设备驱动模块程序的编写,从而会加深理解。

5.3.5 内核调试

内核调试是驱动开发人员必须掌握的一项技能,和调试其他程序一样,内核也属于一种特殊的程序。调试内核问题时,能够跟踪内核执行情况并查看其内存和数据结构是非常有用的。Linux 中的内置内核调试器 KDB(Kernel Debugger的缩写)提供了这种功能。Linux 内核调试器(KDB)允许你调试 Linux 内核。这个恰如其名的工具实质上是内核代码的补丁,它允许开发人员访问内核内存和数据结构。KDB 的主要优点之一就是它只需要用一台机器就可以进行调试,比如你可以调试正在运行的内核。设置一台用于 KDB 的机器需要花费一些工作,因为需要给内核打补丁并进行重新编译。KDB 的用户应当熟悉 Linux 内核的编译(在一定程度上还要熟悉内核内部机理),KDB 项目是由 Silicon Graphics 维护的,需要从它的点下载与内核版本有关的补丁。

5.3.5.1准备内核调试环境

首先从上述的站点中下载并应用两个补丁,一个是公共的补丁,包含了对通用内核代码的更改,另一个是特定于体系结构的补丁。目前可用最新的KDB版本是4.4,以运行2.6.10 内核的 x86 机器为例,需要下载kdb-v4.4-2.6.10-common-1.bz2和kdb-v4.4-2.6.10-i386-1.bz2文件。

接下来将这两个压缩补丁拷贝到/usr/src/linux目录下,也就是你放内核源码的目录。然后解压。 # bzip2 –d kdb-v4.4-2.6.10-common-1.bz2

# bzip2 –d kdb-v4.4-2.6.10-i386-1.bz2

执行上面的压缩命令将会生成kdb-v4.4-2.6.10-common-1和kdb-v4.4-2.6.10-i386-1两个补丁文件。然后应用这两个补丁文件,具体命令如下:

# patch –p1 >kdb-v4.4-2.6.10-common-1

# patch –p1 >kdb-v4.4-2.6.10-i386-1

接下来需要构建内核以支持 KDB。第一步是设置 CONFIG_KDB 选项,使用你喜欢的配置方法(make xconfig 和make menuconfig 等)来完成这一步。选择“Kernel hacking”选项并选择“Built-in Kernel Debugger support”选项。还可以根据自己的偏好选择其它两个选项。选择“Compile the kernel with frame pointers”选项(如果有的话)则设置 CONFIG_FRAME_POINTER 标志。这将产生更好的堆栈回溯,因为帧指针寄存器被用作帧指针而不是通用寄存器。还可以选择“KDB off by default”选项。这将设置 CONFIG_KDB_OFF 标志,并且在缺省情况下将关闭 KDB。如果编译期间没有选中 CONFIG_KDB_OFF ,那么在缺省情况下 KDB 是活动的。否则,你需要显式地激活它,可以通过在引导期间将kdb=on 标志传递给内核或者通过在挂装了/proc之后执行以下命令

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

激活: # echo "1" >/proc/sys/kernel/kdb

相反,如果缺省情况下 KDB 是打开的,那么将 kdb=off 标志传递给内核或者执行下面这个操作将会取消激活 KDB:

# echo "0" >/proc/sys/kernel/kdb

最后,保存配置,然后退出。(www.61k.com]重新编译内核。建议在构建内核之前执行“make clean”。用常用方式安装内核并引导它。到此为止,关于KDB的内核调试环境已经建立,下面讲述KDB的一般用法。

5.3.5.2 KDB的基本用法

KDB是一个功能非常强大的工具,它允许进行多个操作,比如内存和寄存器修改、应用断点和堆栈跟踪等。根据这些操作可以将 KDB 命令分成几个类别。下面将介绍每一类中最常用命令的基本用法。

? 内存显示和修改

最常用的命令有md 、 mdr 、 mm 和 mmW。

md 命令以一个地址/符号和行计数为参数,显示从该地址开始的count行的内存。如果没有指定count ,那么就使用环境变量所指定的缺省值。如果没有指定地址,那么 md 就从上一次打印的地址继续。地址打印在开头,字符转换打印在结尾。使用格式1:md [vaddr [line-count [output-radix]] ] 其中显示地址为vaddr的内存的内容。line-count为要显示的内存的行数,output-radix指定以8进制、10进制或者16进制显示。如果省略line-count和output-radix,那么将以设置的环境变量MDCOUNT和RADIX方式显示。如果不带任何参数,md命令将接着上次md命令的后续地址显示内存内容。使用格式2:mdWcn在缺省情况下,md以当前环境变量BYTESPERWORD(确定字的长度)的值读取数据,在读取硬件寄存器的时候,需要指定数据的宽度。这是可以使用mdWcn来进行读取,W是读取的宽度,单位是字节,cn为要读取的数目。例如,显示从 0x1000000 开始的 10 行内存,具体使用如下:

[0]kdb> md 0x1000000 10

mdr命令带有地址或符号以及字节计数,显示从指定的地址开始的count字节数的初始内存内容。它本质上和 md 一样,但是它不显示起始地址并且不在结尾显示字符转换。mdr 命令较少使用。使用格式:mdr <vaddr> <count>

mm 命令修改内存内容。它以地址/符号和新内容作为参数,用 new-contents 替换地址处的内容。使用格式:mm <vaddr> <new content>

mmW 命令更改从地址开始的 W 个字节。请注意, mm 更改一个机器字。使用格式:mmW <vaddr> <new content>

? 寄存器显示和修改

常用命令有 rd 、 rm 和 ef等。

rd 命令(不带任何参数)显示处理器寄存器的内容。它可以有选择地带三个参数。如果传递了 c 参数,则 rd 显示处理器的控制寄存器;如果带有 d 参数,那么它就显示调试寄存器;如果带有 u 参数,则显示上一次进入内核的当前任务的寄存器组。使用格式:rd [c|d|u]

rm 命令修改寄存器的内容。它以寄存器名称和 new-contents 作为参数,用 new-contents 修改寄存器。寄存器名称与特定的体系结构有关。目前,不能修改控

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

制寄存器。[www.61k.com)使用格式:rm <register-name> <register-content> 例如,修改eax寄存器内容为0x10,具体操作如下:

[0]kdb> rm %eax 0x10

ef 命令以一个地址作为参数,它显示指定地址处的异常帧。使用格式:ef <vaddr> ? 断点

常用的断点命令有 bp 、bc 、bd 、be 和 bl。

bp 命令以一个地址/符号作为参数,它在地址处应用断点。当遇到该断点时则停止执行并将控制权交予 KDB。该命令有几个有用的变体。bpa 命令对SMP系统中的所有处理器应用断点。bph 命令强制在支持硬件寄存器的系统上使用它。 bpha 命令类似于bpa命令,差别在于它强制使用硬件寄存器。使用格式:bp [<vaddr>] bc 命令从断点表中除去断点。它以具体的断点号或 * 作为参数,在后一种情况下它将除去所有断点。使用格式:bc <bpnum>

bd 命令禁用特殊断点。它接收断点号作为参数。该命令不是从断点表中除去断点,而只是禁用它。断点号从 0 开始,根据可用性顺序分配给断点。使用格式:bd <bpnum>

be 命令用来启用断点。该命令的参数也是断点号。使用格式:be <bpnum> bl 命令列出当前的断点集。它包含了启用的和禁用的断点。该命令的操作与bp命令相同。

? 堆栈跟踪

堆栈跟踪命令主要有bt 、btp 、btc 和 bta。

bt 命令设法提供有关当前线程的堆栈的信息。它可以有选择地将堆栈帧地址作为参数。如果没有提供地址,那么它采用当前寄存器来回溯堆栈。否则,它假定所提供的地址是有效的堆栈帧起始地址并设法进行回溯。如果内核编译期间设置了 CONFIG_FRAME_POINTER 选项,那么就用帧指针寄存器来维护堆栈,从而就可以正确地执行堆栈回溯。如果没有设置 CONFIG_FRAME_POINTER ,那么 bt 命令可能会产生错误的结果。使用格式:bt [<stack-frame addr>]

btp 命令将进程标识作为参数,并对这个特定进程进行堆栈回溯。使用格式:btp <pid>

btc 命令对每个活动CPU上正在运行的进程执行堆栈回溯。它从第一个活动CPU 开始执行bt ,然后切换到下一个活动 CPU,以此类推。

bta 命令对处于某种特定状态的所有进程执行回溯。若不带任何参数,它就对所有进程执行回溯。可以有选择地将各种参数传递给该命令。将根据参数处理处于特定状态的进程。选项以及相应的状态如下[9]:

l D:不可中断状态

l R:正运行

l S:可中断休眠

l T:已跟踪或已停止

l Z:僵死

l U:不可运行

? 其他用法

id命令用于反汇编。使用格式:id <vaddr> 从vaddr开始的地址反汇编指令。 cpu命令用于切换到另一个CPU。使用格式:cpu <cpunum> 这条命令仅仅在SMP结构下有用,它切换到由cpunum指定的CPU。

ps命令用于显示所有活动的进程。使用格式:ps 显示当前的活动的进程。包括pid、

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

ppid、CPU号、当前状态,以及对应的线程。(www.61k.com]

reboot命令用来重新启动机器。使用格式:reboot 在某些情况下,内核无法返回到正常工作状态,这时可以利用reboot重新启动机器。注意在重启机器前,它不进行任何状态保存的工作。

sections命令列出内核中所有已知的段的信息。使用格式:sections 列出模块和内核的所有已知的段的信息。首先是模块信息,最后是内核信息。包括模块名和一个或者多个段的信息。段信息包括段名、段起始地址、段结束地址和段标识。本命令仅仅是为外部调试器而设立的。

sr命令激活SysRq代码,也就是调用MAGIC_SYSRQ函数。格式:sr <sysrq key> 将sysrq key字符作为参数传递给SysRq函数进行处理,就像你已经键入了SysRq键和该字符一样。如果要使用这个命令,需要在配置内核时,选择Magic SysRq Key。然后在新内核启动后,使用如下命令激活SysRq功能。

# echo “1” > /proc/sys/kernel/sysrq

这是一个功能强大的命令,它使得在kdb中可以使用操作系统提供的SysRq处理函数。

lsmod命令列出内核中加载的所有模块。使用格式:lsmod 显示所有模块的信息。包括模块名、模块大小、模块结构地址、引用计数,以及被哪个模块所引用。

rmmod命令卸载一个模块。使用格式:rmmod <modname> 将由modname指定的模块从内核中卸载。

ll命令对链表中的每个元素重复执行命令。使用格式:ll <addr> <link-offset> <cmd> 它对以地址addr开头的链表的头link-offset个元素,重复执行cmd命令。

help和?命令显示帮助信息。使用格式:help 或者? 显示kdb的命令以及简单的用法。

总之,KDB是一个强大的内核调试工具,通常GDB需要两台机器通过串口才能进行调试,而KDB只需要一台机器即可进行调试,对于普通用户来说,是非常方便的。对于编写内核程序(如可加载模块)的程序员来说,KDB提供的这些命令使得调试工作难度大大降低,使得调试效率得以提高。另外对于内核感兴趣的读者可以使用KDB来查看内核的数据结构和运行状态,从而加深对内核的理解。不足之处是KDB无法提供源码级的调试,要求程序员有一定的汇编程序基础。但总的来说,KDB提供了一种强有力的内核调试手段,值得读者去学习和使用。

5.4本章小结

本章是Linux驱动程序开发中最基础的一章,也是最适合初学者入门的一章。本章首先对驱动程序的作用和分类进行了介绍,让读者对驱动程序有个大概的了解;接着用一个最简单的“Hello World”实例讲述了Linux内核模块的实现框架,通过这个简单的实例,使读者对Linux内核模块有个初步的了解;最后重点介绍了Linux驱动程序开发的几个要点,包括内存与I/O端口,并发控制(自旋锁与信号量),阻塞与非阻塞,中断处理,以及内核调试工具KDB等重要概念,其中还通过一些典型的代码来结合讲述这些概念。总之,通过对本章的学习,可以为后面章节的学习奠定坚实的基础,下一章将讲述Linux设备驱动中最常见的类型——字符设备驱动程序。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

5.5常见问题

1. 在Linux系统中,CPU对I/O端口的编址方式有哪两种?

参考答案:一是I/O映射(I/O-mapped)方式,如X86处理器为外设专门实现了一个单独的地址空间,称为I/O地址空间或I/O端口空间,CPU通过专门的I/O指令(如X86的IN和OUT指令)来访问这一空间的地址单元;二是内存映射(Memory-mapped)方式,RISC指令系统的CPU(如ARM,PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为了内存的一部分,此时CPU访问I/O端口就像访问一个内存单元不需要单独的I/O指令。(www.61k.com]这两种方式在硬件实现上的差异对软件来说是完全可见的,驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是I/O内存资源。

2.自旋锁和信号量的基本概念?

参考答案:自旋锁的概念非常简单,自旋锁是一个互斥现象的设备,它只能有两个值:locked(锁定)或unlocked(解锁)。它通常作为一个整型值的单个位来实现。如果自旋锁已经被别的进程占用,调用者就一直循环查看是否该自旋锁被释放,在任何时刻,自旋锁只能有一个保持者,即在同一时刻只能有一个进程获得锁。一个信号量是一个结合一对函数的整型值,这对函数通常被称为P操作和V操作。一个进程希望进入一个临界区域时将调用P操作在相应的信号量上,如果这个信号量的值大于0,这个值将被减1同时该进程继续进行。相反如果这个信号量的值等于或小于0,则该进程将等待别的进程释放该信号量,然后才能执行。解锁一个信号量通过调用V操作来完成,这个函数的作用正好与P操作相反,调用V操作时信号量的值将增加1,如果需要,同时唤醒哪些等待的进程。当信号量用于互斥现象——多个进程在同时运行一个相同的临界区域,此时信号量的值被初始化为1。信号量只能在一个时刻被一个进程或线程拥有,一个信号量使用在这种模式下通常被称为互斥体(mutex,mutual exclusion的缩写)体。

3. 当一个异常出现以后,ARM微处理器会执行哪几个步操作?

参考答案:当一个异常出现以后,ARM微处理器会执行以下几步操作:

ü 保存处理器当前状态、中断屏蔽位以及各条件标志位;

ü 设置当前程序状态寄存器(CPSR)中相应的位;

ü 将寄存器lr_mode(当异常出现或调用函数时保存下一条指令的位置)设置成返回地址;

ü 将程序计数器(PC)值设置成该异常中断的中断向量地址,从而跳转到相应的异常中断处理程序处执行。

4. 使用KDB工具的优缺点?

参考答案:KDB是一个强大的内核调试工具,通常GDB需要两台机器通过串口才能进行调试,而KDB只需要一台机器即可进行调试,对于普通用户来说,是非常方便的。对于编写内核程序(如可加载模块)的程序员来说,KDB提供的这些命令使得调试工作难度大大降低,使得调试效率得以提高。另外对于内核感兴趣的读者可以使用KDB来查看内核的数据结构和运行状态,从而加深对内核的理解。不足之处是KDB无法提供源码级的调试,要求程序员有一定的汇编程序基础。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

第6章 字符设备驱动程序

本章学习目标:

l 熟悉字符设备驱动程序概念

l 熟悉字符设备驱动相关的三个结构:file_operations, file, inode

l 熟悉主、次设备号作用和使用

l 理解触摸屏设备的硬件实现原理

l 理解触摸屏设备的驱动程序实现

6.1 字符设备驱动介绍

系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。[www.61k.com)设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。本章介绍最常见的设备驱动程序——字符设备驱动,在介绍字符设备驱动程序之前,首先需要学习几个重要的概念:字符设备相关的数据结构和主、次设备号等。

6.1.1字符设备驱动相关的重要结构

编写Linux字符设备驱动程序首先要熟悉这三个结构,即file_operations(文件操作)、file(文件)和inode(节点)。这三个数据结构非常重要,被定义在<include/linux/fs.h>文件中。首先来介绍file_operations结构体。

6.1.1.1 file_operations(文件操作)结构

由于用户进程是通过设备文件同硬件打交道,所以对设备文件的操作方式不外乎就是一些系统调用,如open,read,write,close等,注意,不是fopen,fread,fwrite,fclose。但是如何把系统调用和驱动程序关联起来呢?这时需要一个非常关键的数据结构,即file_operations,它用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。该数据结构体在Linux 2.6.10内核中具体定义如下: struct file_operations {

struct module *owner;

loff_t (*llseek) (struct file *, loff_t, int);

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t);

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

ssize_t (*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);

int (*readdir) (struct file *, void *, filldir_t);

unsigned int (*poll) (struct file *, struct poll_table_struct *);

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

int (*mmap) (struct file *, struct vm_area_struct *);

int (*open) (struct inode *, struct file *);

int (*flush) (struct file *);

int (*release) (struct inode *, struct file *);

int (*fsync) (struct file *, struct dentry *, int datasync);

int (*aio_fsync) (struct kiocb *, int datasync);

int (*fasync) (int, struct file *, int);

int (*lock) (struct file *, int, struct file_lock *);

ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);

ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);

ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);

ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

int (*check_flags)(int);

int (*dir_notify)(struct file *filp, unsigned long arg);

int (*flock) (struct file *, int, struct file_lock *);

};

该结构体的每个成员都对应着驱动内核模块用来处理某个被请求事务的函数的地址。[www.61k.com]下面对这个定义相对复杂且非常重要的数据结构成员进行解释:

n struct module *owner;

该成员是file_operations结构中唯一一个不是声明操作的成员,它是一个指向拥有这个模块的指针,该成员用来在它的操作还在使用时不允许卸载该模块,通常情况下,都被简单的初始化为THIS_MODULE。

n loff_t (*llseek) (struct file *, loff_t, int);

该成员为file_operations结构的一个操作,用来改变当前文件的读写位置,并且将新位置作为返回值。其中loff_t为long long类型的别名。

n ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

该操作用来从设备中获取数据,如果这个成员为一个空指针,则系统调用read将返回一个-EINVAL(Invalid argument,即无效参数)错误,正常情况下,返回一个非负整数代表读取的字节数。其中ssize_t为int或long型,和平台相关。__user用来声明为用户空间。 n ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t);

该操作用来初始化一个异步的读操作,即当一个读操作还没有完成时也许这个函数已经返回。当这个操作为空时,它将由read(同步)操作代替。

n ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

该操作用来发送数据给设备,当为空时,系统调用write将返回-EINVAL错误。正常情况下,返回一个非负整数代表成功写的字节数。

n ssize_t (*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);

该操作用来初始化一个异步写操作,当该操作为空时,调用write操作。

n int (*readdir) (struct file *, void *, filldir_t);

该操作用于文件系统,用来读取目录。对于设备文件时,该操作为空。

n unsigned int (*poll) (struct file *, struct poll_table_struct *);

该操作用用来查询一个或多个文件描述符的读或写是否会阻塞。Poll方法返回一个位掩码来指示是否非阻塞的读或写是可能的,并且提供给内核信息用来使调用进程sleep直到I/O

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

n n n n n n n n n n n n n n

n 端口变为可用。[www.61k.com]如果一个设备驱动的poll方法为空,则设备默认为不阻塞的可读和可写。 int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); 该操作用来提供发出设备特定命令的方法。对于一个未定义ioctl操作的设备,当系统调用时将返回-ENOTTY错误。 int (*mmap) (struct file *, struct vm_area_struct *); 该操作用来请求将设备内存映射到进程的地址空间。如果这个方法为空,nmap系统调用将返回-ENODEV错误。 int (*open) (struct inode *, struct file *); 该操作用来打开设备文件,也是对设备文件进行的第一个操作。如果这个操作为空,则设备打开操作一直成功,但是你的驱动程序将不会被调用。 int (*flush) (struct file *); 该操作用来执行和等待设备未完成的操作,目前flush很少使用,不过SCSI磁带驱动使用了它,用来确保所有写的数据在设备关闭前已经写到磁带上。如果flush为空,内核简单的忽略应用程序的请求。 int (*release) (struct inode *, struct file *); 该操作用来释放文件结构,该操作可以为空。 int (*fsync) (struct file *, struct dentry *, int datasync); 该操作用来刷新任何等待处理的数据,如果这个操作为空,则系统调用fsync将返回-EINVAL。 int (*aio_fsync) (struct kiocb *, int datasync); 该操作是fsync的异步版本。 int (*fasync) (int, struct file *, int); 该操作用来通知设备FASYNC标志的改变。如果该操作为空,则说明该驱动不支持异步通知。 int (*lock) (struct file *, int, struct file_lock *); 该操作用来对文件实行加锁,加锁对常规文件是必不可少的特性,但是设备驱动很少有实现该操作的。 ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); 该操作用来实现多个内存区的单个read操作,该操作不必对数据进行额外拷贝。如果该操作为空,则会调用read方法。 ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); 该操作用来实现多个内存区的单个write操作,该操作不必对数据进行额外拷贝。如果该操作为空,则会调用write方法。 ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *); 该操作用来实现使用最少的拷贝从一个文件描述符搬移数据到另一个。通常设备驱动使sendfile为空。 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); 该操作用来由内核调用来发送数据,一次一页到对应的文件。设备驱动程序实际上不实现sendpage方法。 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); 该操作用来在进程地址空间找一个合适的位置来映射在底层设备上的内存段中。该方法是使驱动能强制满足特殊设备的对齐请求。通常情况下,设置该方法为空。 int (*check_flags)(int);

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

该操作允许模块检查传递给fnctl(F_SETFL…)调用的标志。[www.61k.com)通常情况下,设置该方法为空。 n int (*dir_notify)(struct file *filp, unsigned long arg);

该操作只对文件系统有用,该方法在应用程序使用fcntl函数来请求目录改变通知时调用。设备驱动程序不需要实现dir_notify方法。

n int (*flock) (struct file *, int, struct file_lock *);

该操作用来对文件设备加锁,但是基本上没有设备驱动程序实现该操作。

结构体file_operations的确包含了很多操作,但在实际设备驱动程序中只会用到其中的很少一部分,大部分操作将不会被用到。例如在5.3.4.3小节中的Linux中断实例中的文件操作定义如下: static struct file_operations key_fops =

{

owner: THIS_MODULE,

read: key_read,

open: key_open,

release: key_release,

};

通过这个实例可以看出,该设备驱动模块只实现了read、open和release三个操作,这三个操作所对应的实现函数分别为:key_read、key_open和key_release,其他的操作都没有实现。

6.1.1.2 file(文件)结构

设备驱动程序中第二个非常重要的数据结构,即file(文件)结构,它不同于应用程序空间的FILE指针,FILE指针定义在C库中而不会出现在内核代码中,而struct file只出现在内核代码中,从不出现在用户程序中。结构体file在Linux 2.6.10版本中的定义如下: struct file {

struct list_head f_list;

struct dentry *f_dentry;

struct vfsmount *f_vfsmnt;

struct file_operations *f_op;

atomic_t f_count;

unsigned int f_flags;

mode_t f_mode;

int f_error;

loff_t f_pos;

struct fown_struct f_owner;

unsigned int f_uid, f_gid;

struct file_ra_state f_ra;

unsigned long f_version;

void *f_security;

/* needed for tty driver, and maybe others */

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

void *private_data;

#ifdef CONFIG_EPOLL

/* Used by fs/eventpoll.c to link all the hooks to this file */

struct list_head f_ep_links;

spinlock_t f_ep_lock;

#endif /* #ifdef CONFIG_EPOLL */

struct address_space *f_mapping;

};

下面解释一下file结构体成员,在内核源码中,指向struct file的指针通常被称为file或flip,为了和它结构本身区别,我们这里将该文件指针称为filp,这样就可以和file结构本身区别开。[www.61k.com]由于file结构体中很多成员并不对设备驱动程序有用,所以在这里只介绍一些常用的重要成员。

n struct dentry *f_dentry;

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

该成员是文件对应的目录项结构。除了用flip->f_dentry->d_inode的方法访问索引节点结构之外,设备驱动开发人员一般无需关心dentry结构。

n struct file_operations *f_op;

这个成员是定义与文件关联的操作,也就是上面讲的文件操作。内核在执行open操作时对这个指针赋值,以后需要处理这些操作时就读取这个指针。

n unsigned int f_flags;

该成员是文件标志,如O_RDONLY(只读),O_NONBLOCK(非阻塞)和O_SYNC(同步)。驱动程序应该检查O_NONBLOCK标志看是否是非阻塞操作请求,其他标志很少用到。特别的是,读写权限通过f_mode成员检查而不是f_flags。

n mode_t f_mode;

该成员用于确定文件是可读的或可写的(或者两者都是),通过位FMODE_READ和FMODE_WRITE实现。当文件还没有打开时,试图读或写操作将被拒绝,驱动程序可能都不知道这个情况。

n loff_t f_pos;

该成员用来确定当前的读写位置。如果需要知道当前在文件中的位置,驱动程序可以读这个值,但是不应该改变这个值。读和写操作用它们的最后一个参数指针确定文件位置而不是直接用filp->f_pos实现。

n void *private_data;

该成员是跨系统调用时保存状态信息非常有用的资源。驱动程序可以用这个字段指向已分配的数据,但一定要在内核销毁file结构前在release方法中释放内存。

文件(file)结构代表一个打开的文件描述符,它不是专门给设备驱动使用,系统中每一个打开的文件在内核中都有一个关联的struct file。它由内核在open时创建,并传递给在文件上操作的任何函数,直到最后关闭。当文件的所以实例都关闭后,内核释放这个数据结构。

6.1.1.3 inode(节点)结构

在Linux设备驱动开发中,还有一个非常重要的数据结构,即inode结构。内核中用inode结构表示具体的文件,而用file结构表示打开的文件描述符。对于单个文件,可能会有许多

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

个表示打开的文件描述符file结构,但是他们都指向了单个的inode结构,所以file结构和inode结构是不同的。[www.61k.com]在Linux 2.6.10内核中,inode结构体的具体定义如下:

struct inode {

struct hlist_node i_hash;

struct list_head i_list;

struct list_head i_dentry;

unsigned long i_ino;

atomic_t i_count;

umode_t i_mode;

unsigned int i_nlink;

uid_t i_uid;

gid_t i_gid;

dev_t i_rdev;

loff_t i_size;

struct timespec i_atime;

struct timespec i_mtime;

struct timespec i_ctime;

unsigned int i_blkbits;

unsigned long i_blksize;

unsigned long i_version;

unsigned long i_blocks;

unsigned short i_bytes;

unsigned char i_sock;

spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */

struct semaphore i_sem;

struct rw_semaphore i_alloc_sem;

struct inode_operations *i_op;

struct file_operations *i_fop; /* former ->i_op->default_file_ops */

struct super_block *i_sb;

struct file_lock *i_flock;

struct address_space *i_mapping;

struct address_space i_data;

#ifdef CONFIG_QUOTA

struct dquot *i_dquot[MAXQUOTAS];

#endif

/* These three should probably be a union */

struct list_head i_devices;

struct pipe_inode_info *i_pipe;

struct block_device *i_bdev;

struct cdev *i_cdev;

int i_cindex;

__u32 i_generation;

#ifdef CONFIG_DNOTIFY

unsigned long i_dnotify_mask; /* Directory notify events */

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

struct dnotify_struct *i_dnotify; /* for directory notifications */

#endif

unsigned long i_state;

unsigned long dirtied_when; /* jiffies of first dirtying */

unsigned int i_flags;

atomic_t i_writecount;

void *i_security;

union {

void *generic_ip;

} u;

#ifdef __NEED_I_SIZE_ORDERED

seqcount_t i_size_seqcount;

#endif

};

可以看到inode结构包含了大量有关文件的信息,但通常情况下对设备驱动程序开发有用的成员有下面两个:

n dev_t i_rdev;

该成员表示设备文件的inode结构,它包含了真正的设备编号(将在下一节介绍设备编号)。[www.61k.com) n struct cdev *i_cdev;

该成员表示字符设备的内核的内部结构,当inode指向一个字符设备文件时,该成员包含了指向struct cdev结构的指针,其中cdev结构是字符设备结构体。

其实关于Linux字符设备驱动开发还有一些其它的数据结构,这里只介绍了最经常使用的三个结构体,在后面的章节中还会介绍其他的数据结构。

6.1.2主、次设备号

对字符设备的访问是通过文件系统内的设备名称进行的,那些文件被称为设备文件,他们通常位于/dev目录下。在/dev目录下,通过ls –l命令可以查看系统中的字符设备和块设备。例如在系统中利用ls –l命令查看设备显示如下: brw-rw---- 1 root disk 15, 0 Jan 30 2003 cdu31a

brw-rw---- 1 root disk 24, 0 Jan 30 2003 cdu535

crw-rw---- 1 root disk 67, 0 Jan 30 2003 cfs0

brw-rw---- 1 root disk 30, 0 Jan 30 2003 cm205cd

brw-rw---- 1 root disk 32, 0 Jan 30 2003 cm206cd

drwxr-xr-x 2 root root 4096 Oct 24 17:43 compaq

crw------- 1 root root 5, 1 Feb 11 15:15 console

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

crw------- 1 root root 1, 6 Jan 30 2003 core

drwxr-xr-x 18 root root 4096 Oct 24 17:43 cpu

crw-rw---- 1 root uucp 5, 64 Jan 30 2003 cua0

crw-rw---- 1 root uucp 5, 65 Jan 30 2003 cua1

crw-rw---- 1 root uucp 5, 66 Jan 30 2003 cua2

crw-rw---- 1 root uucp 5, 67 Jan 30 2003 cua3

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

crw-rw---- 1 root uucp 205, 16 Jan 30 2003 cuam0

crw-rw---- 1 root uucp 205, 17 Jan 30 2003 cuam1

crw-rw---- 1 root uucp 205, 18 Jan 30 2003 cuam2

crw-rw---- 1 root uucp 205, 19 Jan 30 2003 cuam3

crw-rw---- 1 root uucp 44, 0 Jan 30 2003 cui0

上面只列举了部分的设备,其中第一列中第一个字符为c(character)的行代表的是字符设备,为b(block)的行代表为块设备。[www.61k.com]在日期的前面有两个数字,并且用逗号分开,这两个数字就是相应设备的主设备号和次设备号。读者会问,用主设备号和次设备号干什么?通常主设备号用来标识该设备对应的驱动程序,而次设备号是由内核使用,用于正确确定设备文件所指的设备。上述设备中/dev/ console,/dev/ cua0,/dev/ cua1,/dev/ cua2,/dev/ cua3的主设备号都是5,所以这些设备都由驱动程序5管理。现代的Linux内核允许多个驱动程序共享主设备号,但是大多数设备仍然按照“一个主设备号对应一个驱动程序”的原则安排。

6.1.2.1主、次设备号的内部表示 在内核中,用dev_t类型来保存设备编号(包括主设备号和次设备号)。在Linux 2.6.10版本中,dev_t是一个32位的数,其中用12位表示主设备号,而其余20位用来表示次设备号。要获得dev_t的主设备号和次设备号,应使用以下宏:

n MAJOR(dev)

n MINOR(dev)

这两个宏定义在<include/linux/kdev_t.h>文件中,具体定义如下:

#define MAJOR(dev)

#define MINOR(dev) ((unsigned int) ((dev) >> MINORBITS)) ((unsigned int) ((dev) & MINORMASK))

通过定义可以看出,获得主、次设备号的方法就是通过左移和位与获得,相反,如果需要将主设备号和次设备号转换成dev_t类型,则使用以下宏:

n MKDEV(int major, int minor)

这个宏的定义也在<include/linux/kdev_t.h>文件中,具体定义如下:

#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

这个宏的实现通过对主设备号移位后然后与次设备号或而产生。关于主设备号的定义在<include/linux/major.h>文件中,关于设备号的分配可参考内核中<Documentation/devices.txt>文件。

在内核2.6以前,限定255个主设备号和255个次设备号,在计算机硬件飞速发展的时代,这些设备号已经不能满足大量设备的需求,所以在内核2.6版本中,它可以容量大于255个的设备。

6.1.2.2静态分配和释放主设备号

在建立一个字符设备之前,首先要获得一个设备编号,在Linux 2.6.10内核中,该工作通过register_chrdev_region函数完成。该函数在include/linux/fs.h文件中声明,该函数的原型如下:

int register_chrdev_region(dev_t from, unsigned count, const char *name)

其中,参数from是要分配的设备编号范围的起始值;参数count是所请求的连续设备编号的个数;如果count值非常大,则所请求的范围可能和下一个主设备号重叠,但是只要

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

所申请的编号范围是可用的,则不会带来任何问题。[www.61k.com)参数name是和该编号范围关联的设备名称。该函数在分配字符设备号成功时返回0,在错误情况下,将返回一个负的错误码,并且不能使用所请求的编号区域。

无论采用register_chrdev_region静态分配设备号还是下一节介绍的alloc_chrdev_region函数动态分配设备编号,都必须在不再使用这些设备编号时释放它们,释放设备编号使用unregister_chrdev函数,该函数的原型如下:

int unregister_chrdev(unsigned int major, const char *name)

其中,参数major为要释放设备的主设备号,参数name为相关的设备名称。

6.1.2.3 动态分配主设备号

如果我们提前知道所需要的设备编号,那么使用register_chrdev_region函数非常合适,但是经常我们不知道设备要使用哪些主设备号,在运行过程中通常使用alloc_chrdev_region函数,内核将会恰当地动态分配所需主设备号,该函数原型如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,

const char *name)

其中,参数dev是仅用于输出的参数,在成功完成调用后将保存已分配范围的第一个编号;参数baseminor是要使用的被请求的第一个次设备号,它通常是0;参数count为所请求的连续设备编号的个数;参数name是和该编号范围关联的设备名称。

对于一个新的驱动程序,笔者强烈建议不要随意选择一个当前没有被使用的设备号作为主设备号,而应该使用动态分配机制获得主设备号。也就是说,建议读者经常使用alloc_chrdev_region函数而取代register_chrdev_region函数。动态分配相对方便且安全,但是它也有缺点,由于分配的主设备号不能保证始终一致,所以无法预先创建设备节点,对于驱动程序的一般用法是从系统/proc/devices中读取得到。因此,为了加载一个使用动态主设备号的设备驱动程序,通常用insmod的调用替换为一个脚本,该脚本在调用insmod之后读取/proc/devices以获得新分配的主设备号,然后创建对应的设备文件。但是通过编写脚步来实现动态主设备号的方法还是有些麻烦,然而在实际中有另外一种更为简单的方法,默认采用动态分配,同时保留在加载或编译时指定主设备号的余地。具体实现是,定义一个全局主设备号变量major,初始化该变量的值为MY_MAJOR,MY_MAJOR的默认值取0,也就是选择动态分配。用户可以使用这个默认值或选择某个特定的主设备号,而且既可以在编译前修改宏定义,也可以通过insmod命令行指定major的值,以下是TTY设备利用这种方式分配主设备号的一段代码:

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

……

if (!major)

{

error = alloc_chrdev_region(&dev, minor_start, num, name);

if (!error)

{

major = MAJOR(dev);

driver->minor_start = MINOR(dev);

}

}

else

{

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

dev = MKDEV(major, minor_start);

error = register_chrdev_region(dev, num, name);

}

if (error < 0)

{

kfree(p);

return error;

}

……

到此为止,对字符设备的一些基本知识有了一个大体的介绍,下面让我们进入最为期待的字符驱动程序的实例开发。[www.61k.com]

6.2 字符设备驱动开发实例

为了使读者对字符设备驱动的开发有更深刻的认识,这里选择典型且实用的例子——Touch screen(触摸屏)设备驱动的开发。对于触摸屏的应用大家应该都比较熟悉,几乎大家都曾用到过。由于触摸屏设备使用简单并且美观,它的应用如今随处可见,工业控制系统、消费电子产品,甚至医疗设备上很多都装备了触摸屏输入装置。我们平时不经意间都会用到触摸屏设备,如在ATM机上取款、签署包裹,办理登机手续或查找电话号码时都可能会用到触摸屏。目前触摸屏技术不但应用到大型设备,而且现在也向小型移动终端发展,比如手机、MP3、MP4、掌上游戏机等等。随着技术的不断发展,应用触摸屏技术的消费类电子产品将会越来越多。常见的触摸屏分为:电阻式触摸屏,电容式触摸屏,声表面波式触摸屏和红外线扫描式触摸屏等。S3C2410芯片内部包含了触摸屏接口,所以需要选择一种触摸屏来工作,我们这里使用应用最广泛的四线电阻式触摸屏。

6.2.1四线电阻式触摸屏原理

四线电阻式触摸屏是是电阻式触摸屏中应用最广、最普及的一种。其结构由下线路(玻璃或薄膜材料)导电ITO(Indium Tin Oxides)*(注1)层和上线路(薄膜材料)导电ITO层组成。中间有细微绝缘点隔开,当触摸屏表面无压力时,上下线路成开路状态。一旦有压力施加到触摸屏上,上下线路导通,控制器通过下线路导电ITO层在X坐标方向上施加驱动电压,通过上线路导电ITO层上的探针,侦测X方向上的电压,由此推算出触点的X坐标。通过控制器改变施加电压的方向,同理

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

可测出触点的Y坐标,从而明确触点的位置。四线电阻式触摸屏的等效电路如图6.1所示。

图6.1电阻式触摸屏的等效电路 *注1:ITO(Indium Tin Oxides)作为纳米铟锡金属氧化物,具有很好的导电性和透明性,可以切断对人

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

体有害的电子辐射,紫外线及远红外线。(www.61k.com)因此,喷涂在玻璃,塑料及电子显示屏上后,在增强导电性和透明性的同时切断对人体有害的电子辐射及紫外、红外线。

6.2.2 S3C2410触摸屏工作原理

S3C2410芯片支持触摸屏接口,其包含触摸屏控制器,四个外部晶体管,还有一个外部电压源。触摸屏接口控制,选择控制信号(nYPON, YMON, nXPON, 和XMON)和模拟垫脚(analog pads)与触摸屏面板的垫脚和外部晶体管相连。触摸屏接口包含一个外部晶体管控制逻辑和一个ADC(Analog-to-Digital Converter,模数转换器)接口逻辑(包含中断发生器逻辑)。图6.2给出了S3C2410的A/D转换和触摸屏接口的功能框图,注意,这里的A/D转化器是可复用类型的。一个上拉电阻连接在AIN[7]和VDDA_ADC之间,所以触摸屏面板的XP垫脚应该与AIN[7]连接,触摸屏面板的YP垫脚应与AIN[5]连接。

S3C2410芯片提供的触摸屏接口模式有四种,这四种模式分别是: ü 正常转化模式

这种模式一般用于通用目的的ADC转化,其中(AUTO_PST=0,XY_PST=0)。该模式通过设置ADCCON和ADCTSC寄存器来初始化。 ü 单独的X/Y位置转化模式

该模式由两个子模式组成:X位置模式和Y位置模式。X位置模式(AUTO_PST=0,XY_PST=1)写X位置转化数据到ADCDAT0寄存器的XPDATA位。转化之后,触摸屏接口产生中断源(INT_ADC)到中断控制器。Y位置模式(AUTO_PST=0,XY_PST=2)写Y位置转化数据到ADCDAT1寄存器的YPDATA位。转化之后,触摸屏接口产生中断源(INT_ADC)到中断控制器。触摸屏面板在单独的X/Y位置转化模式的条件是: 单独的X/Y位置转化模式 X 位置转化 Y 位置转化

XP 外部电压 AIN[7]

XM GND (接地) Hi-Z(高阻抗) YP AIN[5] 外部电压

YM Hi-Z(高阻抗) GND (接地)

ü

自动(有序的)X/Y位置转化模式

这种模式(AUTO_PST=0,XY_PST=0)通常被操作在这些方式下:触摸屏控制器自动地转化X位置和Y位置。触摸屏控制器写X测量数据到ADCDAT0寄存器的XPDATA位,并且写Y测试数据到ADCDAT1寄存器的YPDATA位。在自动位置转化之后,触摸屏控制器产生中断源(INC_ADC)到中断控制器。触摸屏面板在自动(有序)X/Y位置转化模式的条件是:

XP 外部电压 AIN[7]

XM GND (接地) Hi-Z (高阻抗)

YP AIN[5] 外部电压

YM Hi-Z (高阻抗) GND(接地)

自动(有序的)X/Y

位置转化模式 X 位置转化 Y 位置转化

ü

等待中断模式

当触摸屏控制器在等待中断模式时,它将等待触摸笔按下。当触摸笔被按下时,控制器将产生一个中断信号(INT_TC)。在中断发生之后,X和Y位置被合适的转化模式(单独的X/Y位置转化模式或自动(有序)X/Y位置转化模式)读取。触摸屏面板在等待中断模式的条件是:

XP

XM

YP

YM

N/A

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

待中断模式 上拉 Hi-Z (高阻抗) AIN[5] GND (接地)

图6.2 ADC和触摸屏接口的功能框图

6.2.3 S3C2410的ADC和触摸屏接口特殊寄存器

编写驱动程序最重要的一个过程就是阅读芯片用户手册中相关的寄存器,因为设备驱动程序的作用通常都是对相应寄存器的控制,所以现在让我们通过查看S3C2410芯片的用户手册来获得ADC和触摸屏接口的特殊寄存器。[www.61k.com)

6.2.3.1 ADC控制(ADCCON)寄存器 寄存器

ADCCON 地址 0x58000000

果等以1,A/D转化结束

PRSCEN [14] 使能A/D转化器的预分频器 如果等于0,禁止,如果等于1,

则使能

PRSCVL [13:6] A/D转化器的预分频器值。数据值:1 ~ 255 之间,注意,

当预分频器值是N时,分频因子是(N+1)。 0xFF 0 R/W R/W 描述 ADC控制寄存器 描述 复位值 0x3FC4 初始状态 0 ADCCON ECFLG [15] 结束转化标志 (只读)。 如果等于0,A/D转化正在处理,如

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

SEL_MUX [5:3] 模拟输入信道选择。[www.61k.com] 000 = AIN 0 001 = AIN 1

010 = AIN 2 011 = AIN 3 100 = AIN 4 101 = AIN 5 110 = AIN 6 111 = AIN 7 (XP)

STDBM [2] 备用模式选择。 0 = 正常操作模式 1 = 备用模式

1

READ_ START ENABLE_ START

[1] 读的A/D转化开始 0 = 禁止读操作的A/D转化开始 1 = 启动读操作的A/D转化开始

[0] A/D 转化开始根据设定这个位。 如果READ_START 是启动的,这个值将无效。 0 = 无操作 1 = A/D 转化开始并且这个位将被清除在启动之后

6.2.3.2 ADC 触摸屏控制(ADCTSC)寄存器

寄存器 ADCTSC

地址 0x58000004 位 [8] [7]

这位应该为0

选择YMON的输出值。 0 = YMON 输出是0 (YM = 高阻抗). 1 = YMON 输出是1 (YM = 接地)

YP_SEN

[6]

选择nYPON的输出值。0 = nYPON 输出是0 (YP = 外部电压) 1 = nYPON 输出是1 (YP 与AIN[5]连接)

XM_SEN

[5]

选择XMON的输出值。0 = XMON输出是0 (XM = 高阻抗) 1 = XMON 输出是1 (XM =接地)

XP_SEN

[4]

选择nXPON的输出值 0 = nXPON 输出是0 (XP =外部电压) 1 = nXPON 输出是1 (XP与AIN[7]连接)

PULL_UP

[3]

选择上拉 0 = XP 启动上拉 1 = XP 上拉禁止

1 1 0 1

R/W

描述

复位值 0x058 初始状态

0 0

R/W ADC触摸屏控制寄存器

描述

ADCTSC 保留 YM_SEN

AUTO_PST [2] 自动按顺序转化X位置和Y位置 0 = 正常的ADC 转化 1 =0

自动(有序的)X/Y位置转化模式

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

XY_PST [1:0] 手动测量X位置或Y位置 00 = 无操作模式 01 = X位置测

量 10 = Y位置测量 11 = 等待中断模式

注意:在自动(有序的)X/Y位置转化模式,ADCTSC寄存器应该被重新配置在开始读操作之前。[www.61k.com)

6.2.3.3 ADC开始延迟(ADCDLY)寄存器

寄存器 ADCDLY

地址 0x58000008 位

R/W R/W

描述

ADC开始或间隔延迟寄存器

描述

1.正常转化模式,单独的X/Y位置转化模式和自动(有序的)X/Y位置转化模式。->X/Y位置转化延迟值。

DELAY

[15:0]

2.等待中断模式。当触摸笔按下发生在等待中断模式时,这个寄存器为自动X/Y位置转化而产生中断信号(INT_TC)在每间隔几毫秒中。 注意不要使用0值

0x00ff 复位值 0x00ff 初始状态

ADCDLY

注意:

1.在ADC转化之前,触摸屏使用X-tal时钟或外部始终(等待中断模式) 2.在ADC转化期间,触摸屏使用PCLK时钟。

6.2.3.4 ADC 转化数据 (ADCDAT0) 寄存器

寄存器

地址

R/W R

描述

ADC转化数据寄存器

描述

在等待中断模式,触摸笔的抬起或按下状态

[15]

0=触摸笔按下状态 1=触摸笔抬起状态

自动有序的转化X位置和Y位置

AUTO_PST [14]

0=正常ADC转化

1=按顺序自动的测量X位置和Y位置 手动测量X位置或Y位置 00=无操作模式

XY_PST

[13:12] 01=X位置测量

10=Y位置测量 11=等待中断模式

保留 XPDATA

[11:10] 保留 [9:0]

X位置转化数据值(包括正常ADC转化数据值)

- - - - 复位值 - 初始状态

ADCDAT0 0x5800000C

ADCDAT0 UPDOWN

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

(正常的

ADC)

数据值:0 ~ 3FF

6.2.3.5 ADC转化数据(ADCDAT1)寄存器 寄存器

ADCDAT1 地址 0x58000010

[15] 0=触摸笔按下状态

1=触摸笔抬起状态

自动有序的转化X位置和Y位置

AUTO_PST [14] 0=正常ADC转化

1=按顺序自动的测量X位置和Y位置

手动测量X位置或Y位置

00=无操作模式

XY_PST [13:12] 01=X位置测量

10=Y位置测量

11=等待中断模式

保留

YPDATA

[11:10] 保留 Y位置转化数据值(包括正常ADC转化数据值) [9:0] 数据值:0 ~ 3FF

- - - R/W R 描述 ADC转化数据寄存器 描述 在等待中断模式,触摸笔的抬起或按下状态 - 复位值 - 初始状态 ADCDAT1 UPDOWN

6.2.4 触摸屏驱动概要设计

基于S3C2410芯片设计触摸屏驱动接口是一个很实用的例子,首先让我们来看一下该系统的硬件接口连接。(www.61k.com)

6.2.4.1触摸屏硬件接口

对于本节中所列举的触摸屏实例的接口连接图如6.3所示,AIN[7]与XP连接,AIN[5]与YP连接,为了控制触摸屏面板的垫脚(XP,XM,YP和YM),使用四个外部晶体管和四个控制信号(nYPON,YMON,nXPON和XMON),这四个控制信号分别与四个外部晶体管相连。获得X/Y位置坐标时选择单独的X/Y位置转化模式或自动(有序的)X/Y位置转化模式;一般情况下设置触摸屏接口为等待中断模式,如果中断发生,合适的触摸屏接口模式将被激活;在获得正确的X/Y位置坐标值后,系统将返回到等待中断模式。注意,外部电压(VCC)应该为3.3V,外部晶体管的电阻值应该小于5欧姆。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图6.3 S3C2410触摸屏接口连接图

6.2.4.2触摸屏驱动程序流程设计

接下来需要设计驱动程序所需要实现的主要功能,该驱动程序需要实现以下5个主要任务:

1. 配置触摸屏控制器硬件

2. 判断屏幕屏是否被触摸

3. 获得稳定的、去抖动的位置测量数据

4. 校准触摸屏

5. 将触摸状态和位置变化信息发送给更高层的图形软件

确定驱动程序所要实现的任务之后,接下来需要设计它的工作流程,本实例的流程图如

6.4所示。(www.61k.com]该设备驱动程序的工作流程是:首先初始化触摸屏控制器为等待中断模式,同时初始化计时器为延迟10毫秒后检查一次,映射INC_ADC,INC_TC和定时器中断向量到相应的中断服务程序。然后使能中断,并且使计时器准备就绪,当触摸笔按下时,触摸屏中断开始工作,同时启动定时器,等待10毫秒后查看是否有按下事件发生,如果是则确定触摸笔按下,获得触摸点坐标信息,并进行相应的处理。如果没有按下返回继续进行判断。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图6.4触摸屏工作流程

6.2.5触摸屏驱动程序分析

下面就要看到触摸屏驱动实现的具体代码了,为了便于读者理解,这里按照实现该驱动程序的逻辑顺序讲解,首先讲述触摸屏设备初始化。(www.61k.com)

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

6.2.5.1触摸屏设备初始化

由于触摸屏设备具有字符设备的特点,所以将它作为字符设备类型来实现,即对触摸屏设备注册就是对字符设备注册。[www.61k.com)关于触摸屏的初始化是通过调用s3c2410_ts_init模块加载函数实现的,该函数的实现代码如下: int __init s3c2410_ts_init(void)

{

return driver_register(&s3c2410_ts_driver);

}

在上面的加载函数中,其调用driver_register函数来注册驱动程序本身。驱动程序的注册,包括驱动程序本身的注册和设备的注册。驱动程序本身的注册先进行,即由上述的

driver_register函数来完成。而设备注册程序则应当写在驱动程序的probe代码中,在检测设备的时候进行注册,该触摸屏设备的注册是由变量s3c2410_ts_driver的probe方法实现的。关于s3c2410_ts_driver变量的定义如下: static struct device_driver s3c2410_ts_driver = {

.name = DEVICE_NAME,

.bus = &platform_bus_type,

.probe = s3c2410_ts_probe,

.remove = s3c2410_ts_remove,

};

从上面的代码可以看出,s3c2410_ts_driver变量定义了两个方法,分别是probe和remove。其中probe方法是由s3c2410_ts_probe函数实现,它的主要功能就是对触摸屏设备的注册,包括了硬件的初始化工作。remove方法是由s3c2410_ts_remove函数实现,它的功能与

s3c2410_ts_probe函数相反,完成设备的注销功能。首先分析一下s3c2410_ts_probe函数的具体实现,实现代码如下: 1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19 static int __init s3c2410_ts_probe(struct device *dev) { int ret = 0; tsEvent = tsEvent_dummy; adc_clock = clk_get(NULL, "adc"); if (!adc_clock) { printk(KERN_ERR "failed to get adc clock source\n"); return -ENOENT; } clk_use(adc_clock); clk_enable(adc_clock); base_addr=ioremap(S3C2410_PA_ADC,0x20); if (base_addr == NULL) { printk(KERN_ERR "Failed to remap register block\n"); return -ENOMEM; } if(alloc_chrdev_region(&chrdev,0,1,"ts")){

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

20 printk(KERN_ERR"Couldn't alloc chrdev region\n");

21 return 1;

22 }

23 cdev_init(&ts,&s3c2410_fops);

24 if(cdev_add(&ts, chrdev, 1)){

25 unregister_chrdev_region(chrdev,1);

26 printk(KERN_ERR"Couldn't register ts driver\n");

27 return 1;

28 }

29

30 s3c2410_gpio_cfgpin(S3C2410_GPG12, S3C2410_GPG12_XMON); 31 s3c2410_gpio_cfgpin(S3C2410_GPG13, S3C2410_GPG13_nXPON); 32 s3c2410_gpio_cfgpin(S3C2410_GPG14, S3C2410_GPG14_YMON); 33 s3c2410_gpio_cfgpin(S3C2410_GPG15, S3C2410_GPG15_nYPON); 34

35 if (request_irq(IRQ_ADC, s3c2410_isr_adc, SA_SAMPLE_RANDOM, 36 "s3c2410_action", &chrdev)) {

37 printk(KERN_ERR " Could not allocate ts IRQ_ADC !\n"); 38 iounmap(base_addr);

39 return -EIO;

40 }

41 if (request_irq(IRQ_TC, s3c2410_isr_tc, SA_SAMPLE_RANDOM, 42 "s3c2410_action", &chrdev)) {

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

43 printk(KERN_ERR " Could not allocate ts IRQ_TC !\n"); 44 iounmap(base_addr);

45 free_irq(IRQ_ADC,&chrdev);

46 return -EIO;

47 }

48 writel(wait_down_int(), base_addr+S3C2410_ADCTSC);//wait_down_int(); 49 ret = devfs_mk_cdev(chrdev,S_IFCHR | S_IRUGO | S_IWUSR, DEVICE_NAME);

50 if(ret)

51 goto out_chrdev;

52

53 writel(0xFFFF, base_addr+S3C2410_ADCDLY);

54 printk(KERN_INFO "Tochu screen successfully loaded\n");

55

56 goto out;

57 out_chrdev:

58 unregister_chrdev(chrdev, DEVICE_NAME);

59 out:

60 return ret;

61 }

现在对上面的代码进行分析,第6行,用内核提供的时钟API函数clk_get获得ADC时钟

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

源。[www.61k.com)第11-12行,应用和使能ADC时钟源。第14行,其中ioremap()的作用是把一个物理内存地址映射为一个内核指针,被映射数据的长度由size参数设定。该函数的实质是把一块物理区域二次映射到一个可以从驱动程序里访问的虚拟地址上去。第19行,用alloc_chrdev_region函数来获得设备的主设备号和将设备的名称记录到内核的字符设备链表中,该方法将会动态的分配设备号,该函数的功能在前面章节中已经讲过。第23行,用cdev_init函数来初始化cdev(字符设备)结构。第24-28行,用cdev_add函数将字符设备加入到内核的字符设备数组中,如果没有成功加入到内核的字符设备数组中,则需要调用unregister_chrdev_region函数来释放占用的设备号。第30-33行,用来配置S3C2410触摸屏控制端口功能,即配置GPIO*(注2)(General-Purpose I/O,通用输入输出端口)。第35-47行,分别注册IRQ_ADC(ADC中断)和IRQ_TC(触摸屏中断)。第48行,设置触摸屏接口为等待中断模式。第49行,用

devfs_mk_cdev函数自动创建字符设备文件在/dev目录下。对于2.4内核,利用devfs_register函数注册设备文件。第50-51行,如果没有成功注册设备文件,那么需要注销字符设备同时释放占用的设备号,否则返回0表示触摸屏字符设备正确初始化。总之,这段程序完成了触摸屏设备的初始化工作,包括触摸屏字符设备的注册,GPIO的设置,注册中断IRQ_ADC和IRQ_TC,初始化触摸屏为等待中断模式,以及建立触摸屏设备目录等。

*注2:GPIO是General-Purpose I/O的缩写,它映射到CPU的memory map中,可以把一组GPIO当作一个寄存器,该寄存器的每一位对应一个GPIO引脚,因此你可以用内存访问指令来设置和读取GPIO引脚上的信号以驱动外设。

接下来顺便介绍一下s3c2410_ts_remove函数的具体实现,它的作用与上述初始化函数功能相反,它的实现代码如下: 1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19 static int s3c2410_ts_remove(struct device *dev) { unregister_chrdev(chrdev, DEVICE_NAME); disable_irq(IRQ_ADC); disable_irq(IRQ_TC); free_irq(IRQ_TC,&chrdev); free_irq(IRQ_ADC,&chrdev); if (adc_clock) { clk_disable(adc_clock); clk_unuse(adc_clock); clk_put(adc_clock); adc_clock = NULL; } iounmap(base_addr); return 0; }

对上述s3c2410_ts_remove函数的实现代码进行分析,第3行,用来注销字符设备同时释放占用的设备号。第5-8行,先是禁止IRQ_ADC中断和IRQ_TC中断,然后释放IRQ_ADC中断和IRQ_TC中断。第10-15行,用来释放ADC时钟源。第17行,iounmap()函数用于取消ioremap()所做的地址映射。第18行,返回0表示注销字符设备和其它退出操作完成。

其中s3c2410_ts_init函数指定为启动触摸屏设备的入口函数,而s3c2410_ts_exit指定为退

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

出触摸屏设备的卸载函数,s3c2410_ts_exit函数的实现代码如下,它只调用了driver_unregister函数来注销驱动程序。[www.61k.com]

void __exit s3c2410_ts_exit(void)

{

driver_unregister(&s3c2410_ts_driver);

}

6.2.5.2触摸屏设备文件操作

关于设备文件操作(file_operations)在前面的章节中已经介绍,它是一个非常重要的数据结构。通过对file_operations结构体对象的定义,从而知道访问驱动函数都提供哪些操作。该触摸屏字符设备文件操作的对象具体定义如下: static struct file_operations s3c2410_fops = {

owner: THIS_MODULE,

open: s3c2410_ts_open,

read:s3c2410_ts_read,

release: s3c2410_ts_release,

fasync: s3c2410_ts_fasync,

poll: s3c2410_ts_poll,

};

该文件操作对象定义了open,read,release,fasync和poll方法,并且定义了owner属性为THIS_MODULE。其中open方法由s3c2410_ts_open函数实现,read方法由s3c2410_ts_read函数实现,release方法由s3c2410_ts_release函数实现,fasync方法由s3c2410_ts_fasync函数实现和poll方法由s3c2410_ts_poll函数实现。下面将分别介绍这些文件操作对象中定义的方法实现。

6.2.5.3 open和release方法

设备驱动中open方法是用来为以后的操作完成初始化准备工作的,并且通常字符设备驱动程序都会实现它。在大部分驱动程序中,open方法完成如下工作:

ü 检查设备相关错误(诸如设备未就绪或相似的硬件问题)。

ü 如果是首次打开,初始化设备。

ü 标识次设备号,如有必要更新f_op指针。

ü 分配和填写要放在filp->private_data里的数据结构。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

该字符设备的open方法实现根据文件操作s3c2410_fops的定义,是由s3c2410_ts_open函数实现的,具体实现代码如下: 1

2

3

4

5

6

7 static int s3c2410_ts_open(struct inode *inode, struct file *filp) { tsdev.head = tsdev.tail = 0; tsdev.penStatus = PEN_UP; init_timer(&ts_timer); ts_timer.function = ts_timer_handler; tsEvent = tsEvent_raw;

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

8

9

10

11 } init_waitqueue_head(&(tsdev.wq)); return 0;

分析s3c2410_ts_open函数的实现代码,第3-4行,初始化自定义的触摸屏设备结构变量tsdev,它是TS_DEV结构体的对象,关于TS_DEV结构在后面会有介绍。[www.61k.com)第5行,用init_timer函数初始化定时器ts_timer ,ts_timer是全局的timer_list结构体变量,其中timer_list结构用于在未来某一个特定时刻执行某一系列特定任务的功能。第6行,初始化定时器处理函数为ts_timer_handler,ts_timer_handler函数是在定时器时间到时被调用的。第7行,用于初始化原始的触摸屏事件。第8行,初始化触摸屏设备的等待队列。其中上面提到的全局变量tsdev是TS_DEV结构体的对象,TS_DEV结构体的定义如下:

typedef struct {

unsigned int penStatus; /* PEN_UP, PEN_DOWN, PEN_SAMPLE */

TS_RET buf[MAX_TS_BUF]; /* 缓存最大8个关于触摸屏触摸信息*/

unsigned int head, tail; /* 队列事件的头和尾*/

wait_queue_head_t wq; /* 等待队列的头*/

spinlock_t lock; /* 定义一个自旋锁*/

struct fasync_struct *aq; /*定义一个异步结构指针*/

} TS_DEV;

下面来看一下与open方法作用完全相反的release方法,这个设备方法有时也称为close。它一般完成以下任务:

ü 释放open分配在filp->private_data中的内存。

ü 在最后一次关闭操作时关闭设备。

该触摸屏release方法是由s3c2410_ts_release函数完成,它的具体实现如下:

static int s3c2410_ts_release(struct inode *inode, struct file *filp)

{

del_timer(&ts_timer);

return 0;

}

该函数的内容很简单,只是做了删除定时器然后返回的功能,这是由于在open方法中它只实现了初始化定时器的功能。

6.2.5.4 read和poll方法

通常情况下设备驱动程序会实现read和write方法,read是从设备读取数据,而write是向设备发送数据。通常不需要发送数据给触摸屏设备,所以这里不需要实现write方法,而只需要实现read方法即可。这里read方法是由s3c2410_ts_read函数来实现,它的具体实现代码如下:

1

2

3

4

5

6 static ssize_t s3c2410_ts_read(struct file *filp, char *buffer, size_t count, loff_t *ppos) { TS_RET ts_ret; retry: if (tsdev.head != tsdev.tail) { int count;

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

7

8

9

10

11

12

13

14

15

16

17

18

19

20 } count = tsRead(&ts_ret); if (count) copy_to_user(buffer, (char *)&ts_ret, count); return count; } else { if (filp->f_flags & O_NONBLOCK) return -EAGAIN; interruptible_sleep_on(&(tsdev.wq)); if (signal_pending(current)) return -ERESTARTSYS; goto retry; } return sizeof(TS_RET);

分析s3c2410_ts_read函数的代码,第5-9行,当触摸屏事件队列中有事件时,读取触摸屏坐标信息,真正读取函数为tsRead。(www.61k.com] 此外还有一个内核中非常重要的函数,即copy_to_user,它的作用是复制内核空间数据到用户空间,与copy_to_user内核函数作用类似的还有copy_from_user,该函数的作用是复制用户空间的数据到内核空间, 这两个函数的作用类似memcpy函数,但还是和memcpy是有区别的,不然的话就直接用memcpy函数,当内核空间内运行的代码访问用户空间时,被寻址的用户空间的页面可能当前不在内存中,此时虚拟内存子系统会将该进程转入睡眠状态,直到该页面被传送到期望的页面。对于驱动程序编程人员来说,访问用户空间的任何函数都必须是可重入的,并且必需能和其他驱动程序并发执行,同时必须处于能够合法sleep的状态。这两个函数除了在内核空间与用户空间之间拷贝数据之外,它们还会检查用户空间的地址是否有效,如果指针无效就不会进行拷贝,如果在拷贝过程中发现无效指针,则仅仅会复制部分正确的数据。第10-16行,当触摸屏事件队列为空时,如果定义为非阻塞操作,则直接返回-EAGAIN错误信息。打开可中断睡眠队列,此外,检查当前进程是否有信号处理,当signal_pending(current)为非0时表示有信号需要处理,则返回-ERESTARTSYS表示信号函数处理完毕后重新执行信号函数前的某个系统调用。最后执行goto语句调用retry。第19行,返回TS_RET结构体的大小,也就是表示读取字节的个数。

由于触摸屏设备驱动的读取会可能会被阻塞,所以该程序实现了poll方法文件操作,该方法分两步处理:一是在一个或多个可指示poll状态变化的等待队列上调用poll_wait函数,如果当前没有文件描述符可用来执行I/O,则内核将使进程在传递到该系统调用的所有文件描述符对应的等待队列上等待;二是返回一个用来描述操作是否可以立即无阻塞执行的位掩码。该触摸屏设备驱动的poll方法是由s3c2410_ts_poll函数实现,具体实现代码如下:

static unsigned int s3c2410_ts_poll(struct file *filp, struct poll_table_struct *wait)

{

poll_wait(filp, &(tsdev.wq), wait);

return (tsdev.head == tsdev.tail) ? 0 : (POLLIN | POLLRDNORM);

}

6.2.5.5 触摸屏中断和ADC中断的实现

该触摸屏设备驱动最重要的实现部分就是关于中断的实现了,该驱动分两个中断,一个

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

是触摸屏中断(IRQ_TC),另一个是ADC中断(IRQ_ADC)。[www.61k.com]这两个中断及其相关的实现代码如下:

……

/*开始ADC,启动单独的X位置转化模式 */

static inline void start_ts_adc(void)

{

adc_state = 0;

writel(mode_x_axis(), base_addr+S3C2410_ADCTSC); //mode_x_axis();

writel(start_adc_x(), base_addr+S3C2410_ADCCON); // start_adc_x();

tmp=readl(base_addr+S3C2410_ADCCON);

tmp &=~(S3C2410_ADCCON_STDBM);

writel(tmp, base_addr+S3C2410_ADCCON);

readl(base_addr+S3C2410_ADCDAT0);

}

/*获得ADC转化后的X,Y位置 */

static inline void s3c2410_get_XY(void)

{

if (adc_state == 0) {

adc_state = 1;

tmp = readl(base_addr+S3C2410_ADCCON);

tmp &= ~(S3C2410_ADCCON_READ_START);

writel(tmp, base_addr+S3C2410_ADCCON);

y = (readl(base_addr+S3C2410_ADCDAT0) & 0x3ff);

writel(mode_y_axis(), base_addr+S3C2410_ADCTSC);

writel(start_adc_y(), base_addr+S3C2410_ADCCON);

tmp=readl(base_addr+S3C2410_ADCCON);

tmp &=~(S3C2410_ADCCON_STDBM);

writel(tmp, base_addr+S3C2410_ADCCON);

readl(base_addr+S3C2410_ADCDAT1);

}

else if (adc_state == 1) {

adc_state = 0;

tmp = readl(base_addr+S3C2410_ADCCON);

tmp &= ~(S3C2410_ADCCON_READ_START);

writel(tmp, base_addr+S3C2410_ADCCON);

x = (readl(base_addr+S3C2410_ADCDAT1) & 0x3ff);

tsdev.penStatus = PEN_DOWN;

DPRINTK("PEN DOWN: x: %08d, y: %08d\n", x, y);

writel(wait_up_int(), base_addr+S3C2410_ADCTSC);

tsEvent();

}

}

/*ADC中断服务程序*/

static irqreturn_t s3c2410_isr_adc(int irq, void *dev_id, struct pt_regs *reg)

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

{

spin_lock_irq(&(tsdev.lock));

s3c2410_get_XY();

spin_unlock_irq(&(tsdev.lock));

return IRQ_HANDLED;

}

/*触摸屏中断服务程序*/

static irqreturn_t s3c2410_isr_tc(int irq, void *dev_id, struct pt_regs *reg)

{

spin_lock_irq(&(tsdev.lock));

if (tsdev.penStatus == PEN_UP) {

start_ts_adc();

}

else {

tsdev.penStatus = PEN_UP;

DPRINTK("PEN UP: x: %08d, y: %08d\n", x, y);

writel(wait_down_int(), base_addr+S3C2410_ADCTSC);

tsEvent();

}

spin_unlock_irq(&(tsdev.lock));

return IRQ_HANDLED;

}

对于初学驱动程序开发的读者来说,对中断服务程序的执行经常会感觉到迷惑,因为笔者曾经为此也苦恼过。[www.61k.com]所以在这里会详细的介绍上述代码的执行过程:当触摸屏面板感应到有触摸笔触摸屏幕时,这是就会产生触摸屏中断,也就是调用触摸屏的中断服务程序(s3c2410_isr_tc),如果是第一次触发触摸屏中断,那么触摸笔的状态一定是PEN_UP,所以将调用start_ts_adc函数,start_ts_adc函数用来转化系统为单独的X位置转化模式,同时设置ADC控制寄存器,触发ADC中断,也就是调用ADC中断服务程序(s3c2410_isr_adc),s3c2410_isr_adc函数通过调用s3c2410_get_XY函数来获得ADC转化后的X和Y位置,在s3c2410_get_XY函数中有一个全局变量adc_state,通过判断该变量来确定是否已经获得Y位置,如果adc_state等于0表明还没有获得Y位置,所以先获得Y位置,并且设置adc_state为1,同时转化系统为单独的Y位置转化模式,从而又触发ADC中断,即调用ADC中断服务程序(s3c2410_isr_adc),此时adc_state等于1,从而获得X位置,设置触摸笔的状态为PEN_DOWN,并设置系统为等待中断模式,此时X和Y位置都已经获得,通过tsEvent函数将获得的坐标数据发送到触摸屏的事件缓冲(tsdev.buf[MAX_TS_BUF])中。上层应用程序通过读取事件缓冲tsdev.buf[MAX_TS_BUF]中的值来获取坐标。关于触摸屏中断的代码具体分析可以留给读者自己分析,这里已经介绍了它的主要实现功能。

6.2.6配置和编译驱动程序

编译内核驱动程序不同于一般的应用程序,它通常放在内核代码driver/目录下的相应位置。由于我们所开发的触摸屏设备属于字符设备,所以将源程序放在了driver/char目录下。同时需要修改该目录下Kconfig配置文件,添加以下内容到该文件:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

config TOUCHSCREEN_S3C2410

tristate "Samsung S3C2410 touchscreen input driver"

depends on ARCH_SMDK2410

select SERIO

help

Say Y here if you have the s3c2410 touchscreen.

If unsure, say N.

To compile this driver as a module, choose M here: the

module will be called s3c2410_ts.

config TOUCHSCREEN_S3C2410_DEBUG

boolean "Samsung S3C2410 touchscreen debug messages"

depends on TOUCHSCREEN_S3C2410

help

Select this if you want debug messages 添加以上内容后,在用make menuconfig配置内核时会增加关于S3C2410触摸屏的选项,此外,还需要修改driver/char/Makefile文件,否则,触摸屏驱动源程序将不能被编译,添加下面一行内容:

obj-$(CONFIG_TOUCHSCREEN_S3C2410) += s3c2410_ts.o

修改完配置文件和Makefile文件,最后重新配置内核选项,将触摸屏选项以模块形式添加或直接添加到内核,然后就可以编译该驱动程序了。[www.61k.com)

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

6.2.7测试触摸屏驱动程序

现在我们可以通过简单的程序对编写的触摸屏驱动程序进行测试,测试程序相对比较简单,代码如下:

#include <sys/types.h>

#include <sys/ipc.h>

#include <stdio.h>

#include <fcntl.h>

#include <pthread.h>

#include <unistd.h>

#include <sys/ioctl.h>

#define TS_DEV "/dev/TS "

static int ts_fd=-1;

typedef struct {

unsigned short pressure;

unsigned short x;

unsigned short y;

unsigned short pad;

} TS_RET;

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

int main()

{

TS_RET data;

if((ts_fd=open(TS_DEV,O_RDONLY))<0)

{

printf("Error opening %s device!\n",TS_DEV);

return -1;

}

while(1)

{

read(ts_fd,&data,sizeof(data));

printf("x=%d,y=%d,pressure=%d\n",data.x,data.y,data.pressure);

}

return 0;

}

通过阅读上面的测试程序,不难看出驱动程序提供给应用程序的正是那些实现的文件操作,例如本测试程序类似应用程序,其调用了open和read方法,而这两个方法的具体实现是由底层驱动程序实现。(www.61k.com]通过对触摸屏设备驱动程序的分析,相信读者对字符设备驱动程序的编写有了更进一步的理解,相信读者通过自身的努力一定能够编写出自己的字符设备驱动程序。

6.2.8触摸屏的校准

在实际应用中,触摸屏和显示屏配合作为输入设备,为了能使从触摸屏采样得到的坐标与屏幕的显示坐标对应,需要一个映射过程,该映射的过程就是触摸屏的校准。函数TS_Coordinate_Conversion完成触摸屏采样值转换成显示坐标,根据不同的硬件有不同的转换方法。理想的触摸屏映射示意图如6.5所示,本触摸屏采样坐标及显示坐标与图6.5类似。其中TS_MAX_X和TS_MIN_X是触摸屏X坐标采样值的最大和最小值。这里使用的是320×240的TFT屏,所以TFT_X值为320,TFT_Y值为240。假使触摸屏的最左上角坐标为(X1,Y1),最右下角坐标为(X2,Y2),显示屏的最左上角坐标为(x1,y1),最右下角坐标为(x2,y2),由于这里使用的分辨率是320×240,所以,x2 与x1的差值是320,y2与y1的差值是240,假定以左上角坐标为(x1,y1)为原点(0,0)。那么在触摸屏上任意一点(x,y)的坐标影射到显示屏上坐标(X,Y)的转换公式为:

X=320-[320*(x- X2)/( X1 - X2)];

Y=240-[240*(y- Y2)/( Y1- Y2)];

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图6.5 触摸屏与显示屏的坐标映射 下面是触摸屏中任意一点x坐标映射到显示屏的处理程序:

int TS_Coordinate_Conversion(int*x)

{

int tempX;

tempX-=X2;

*x=(tempX*TFT_X)/(X1-X2);

*x=TFT_X-*x;

return *x;

}

利用类似的方法可以计算Y值坐标的映射,读者可以自己完成。[www.61k.com)这里使用的触摸屏校准方法是实际应用中最简单的,因为这里使用的是简单的线形算法,而实际中可能是非线形的,所以计算方法上要复杂许多,不过由于我们这里并不是学习数学算法,所以只给出了简单情况下的例子。

6.3本章小节

本章作为嵌入式Linux驱动开发中应用最广泛的字符设备驱动程序进行了详细的讲解,首先讲述了字符设备驱动中最重要的三个数据结构(file_operations、file和inode),然后讲述主、次设备号的使用,最后重点讲述一个字符设备驱动的典型实例——触摸屏设备驱动,不仅讲述该设备驱动程序的硬件原理,而且重点讲述了其驱动程序的实现过程。总之,通过学习本章知识,使读者真正了解字符设备驱动程序的实现过程,从而使读者熟悉了字符设备驱动程序的实际开发。下一章讲述与字符设备驱动实现方法不同的块设备驱动程序。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

6.4常见问题

1.结构file_operations, file和inode在字符设备驱动程序中的作用?

参考答案:file_operations结构用来定义驱动程序的文件操作,也就是定义驱动程序中底层实现的方法,如open, read, write等。(www.61k.com]而file结构代表一个打开的文件,它由内核在open时创建,并传递给在该文件上进行操作的所有函数,直到最后的close函数来释放这个数据结构。inode结构表示内部文件,与file结构不同,file表示打开的文件描述符,对单个文件,可能会有许多打开的文件描述符的file结构,但它们都指向单个inode结构。

2.在设备驱动程序中,open方法通常完成哪些工作?

参考答案:在设备驱动程序中,open方法通常完成以下工作:

ü 检查设备相关错误(诸如设备未就绪或相似的硬件问题)。

ü 如果是首次打开,初始化设备。

ü 标识次设备号,如有必要更新f_op指针。

ü 分配和填写要放在filp->private_data里的数据结构。

3.根据6.2.6节中计算X坐标的方法,写出计算Y坐标的实现代码? 参考答案:Y坐标的实现代码如下:

int TS_Coordinate_Conversion(int*y)

{

int tempY;

tempY -=Y2;

*y=( tempY *TFT_Y)/ ( Y1- Y2);

*y =TFT_Y-*y;

return *y;

}

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

第7章 块设备驱动程序

本章学习目标:

l 熟悉块设备驱动程序概念

l 熟悉块设备驱动相关的三个结构:block_device_operations, gendisk, request l 理解块设备的请求处理

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

l 理解MMC/SD的硬件实现原理

l 理解MMC块设备驱动程序实现

7.1块设备驱动介绍

上一章介绍了设备驱动程序的第一大类——字符驱动程序,接下来就要介绍第二大类的设备驱动程序——块设备驱动程序。[www.61k.com)所谓块设备,即面向块的设备,是指数据传输是以块为单位的(例如软盘和硬盘),这里硬件上的块一般被称作“扇区(Sector)”。而名词“块”常用来指软件上的概念,驱动程序常常使用1024字节大小为块的大小,而扇区大小为512字节。下面首先介绍一下块设备驱动相关的三个重要数据结构。

7.1.1块设备驱动相关的重要结构

与字符设备中使用的file_operations结构类似,块设备使其专门的数据结构,即在<include/linux/fs.h>文件中声明的block_device_operations。与块设备相关的还有一个重要的数据结构gendisk,它被声明在<include/linux/genhd.h>文件中。此外,块设备驱动中还有一个非常重要的request结构,它被经常使用在request函数中,它在<include/linux/blkdev.h>文件中被声明。接下来分别介绍这三个重要的数据结构。

7.1.1.1block_device_operations(块设备操作)结构

块设备操作block_device_operations结构告诉系统它能提供哪些接口,与file_operations结构的作用类似,只不过它是专门用于块设备驱动而已。首先让我们看一下内核中是如何声明block_device_operations结构的: struct block_device_operations {

int (*open) (struct inode *, struct file *);

int (*release) (struct inode *, struct file *);

int (*ioctl) (struct inode *, struct file *, unsigned, unsigned long);

int (*media_changed) (struct gendisk *);

int (*revalidate_disk) (struct gendisk *);

struct module *owner;

};

block_device_operations结构成员与file_operations结构相比,确实少了很多。接下来分析一下block_device_operations(块设备操作)结构中各个成员的作用。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

int (*open) (struct inode *, struct file *);

该成员是block_device_operations结构的一个方法,当打开设备时调用它,功能与字符设备中对应的函数相同。[www.61k.com]

n int (*release) (struct inode *, struct file *);

该成员是block_device_operations结构的一个方法,当关闭设备时调用它,功能与字符设备中对应的函数相同。如果用户将介质放入设备中锁住,那么在调用release函数时一定要进行解锁。

n int (*ioctl) (struct inode *, struct file *, unsigned, unsigned long);

该成员是block_device_operations结构的一个方法,实现ioctl系统调用的函数。块设备会首先截取大量的标准请求,因此大多数块设备的ioctl函数都比较短小。

n int (*media_changed) (struct gendisk *);

该成员是block_device_operations结构的一个方法,内核调用该函数以检查用户是否更换了驱动器内的介质,如果更换了,则返回一个非零值。该函数适用那些支持可移动介质的驱动器,其他情况下,该函数被省略。

n int (*revalidate_disk) (struct gendisk *);

该成员是block_device_operations结构的一个方法,当介质被更换时,调用该函数做出响应,它告诉驱动程序完成必要的工作,以便使用新的介质。

n struct module *owner;

该成员不是block_device_operations结构的一个方法,它是一个指向拥有该结构的模块指针,通常被初始化为THIS_MODULE,与file_operations结构中对应成员的作用相同。

以上是对block_device_operations结构成员的一一介绍,细心的读者会发现该结构没有包含read/write方法。其实在块设备的I/O子系统中,这些操作是由request函数处理的,这也就是字符设备驱动程序与块设备驱动程序实现的不同之处,本章后面会专门介绍request函数。 n

7.1.1.2 gendisk结构

内核使用gendisk结构来表示一个独立的磁盘设备,此外内核还使用gendisk结构表示分区。gendisk结构的声明如下: struct gendisk {

int major; /* major number of driver */

int first_minor;

int minors; /* maximum number of minors, =1 for

* disks that can't be partitioned. */

char disk_name[32]; /* name of major driver */

struct hd_struct **part; /* [indexed by minor] */

struct block_device_operations *fops;

struct request_queue *queue;

void *private_data;

sector_t capacity;

int flags;

char devfs_name[64]; /* devfs crap */

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

int number; /* more of the same */ struct device *driverfs_dev;

struct kobject kobj;

struct timer_rand_state *random;

int policy;

atomic_t sync_io; /* RAID */ unsigned long stamp, stamp_idle;

int in_flight;

#ifdef CONFIG_SMP

struct disk_stats *dkstats;

#else

struct disk_stats dkstats;

#endif

};

n

n

n

n

n

n

n

n

n

n

n

n

n

n 接下来分析一下gendisk结构的各个成员。(www.61k.com] int major; 该成员表示设备驱动的主设备号。 int first_minor; 该成员表示设备驱动第一个次设备号。 int minors; 该成员表示设备驱动有多少个次设备号,可以包含minors-1个分区。 char disk_name[32]; 该成员表示磁盘设备名字,该名字将显示在/proc/partitions和sysfs中。 struct hd_struct **part; 该成员表示通过次设备号索引的硬件设备扇区信息。 struct block_device_operations *fops; 该成员表示块设备的各种操作。 struct request_queue *queue; 该成员表示设备管理I/O请求。 void *private_data; 该成员用来保存其内部数据的指针。 sector_t capacity; 该成员表示该驱动器可包含的扇区数,以512字节为一个扇区。 int flags; 该成员表示驱动器状态的标志。 char devfs_name[64]; 该成员表示该驱动器的设备文件系统名字。 int number; 该成员表示多少个相同的块设备。 struct device *driverfs_dev; 该成员表示该驱动器的设备文件系统。 struct timer_rand_state *random;

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

该成员表示定时器随机状态。(www.61k.com]

n int policy;

该成员用来表示该设备驱动的可读/可写属性。

n atomic_t sync_io;

该成员用来表示同步I/O的原子计数器。

n unsigned long stamp, stamp_idle;

该成员表示磁盘设备的时间戳。

n int in_flight;

该成员表示设备驱动的时间信息包数。

n struct disk_stats *dkstats;

该成员表示磁盘的状态信息。

虽然gendisk结构的成员比较繁多,但实际应用中有许多成员是没有用到的。内核为gendisk结构提供了一些函数,在这里一同介绍一下。

ü struct gendisk *alloc_disk(int minors);

该函数用来动态分配gendisk结构,并进行初始化。参数minors是磁盘使用次设备号的个数。该函数的实现代码在<drivers/block/genhd.c>文件。

ü void del_gendisk(struct gendisk *disk);

该函数用来卸载磁盘,当不再使用一个磁盘时,调用该函数来卸载它。该函数的实现代码在<fs/partitions/check.c>文件中。

ü void add_disk(struct gendisk *disk);

该函数用来激活磁盘设备,并提供随时调用它提供的方法。由于alloc_disk函数只是分配了gendisk结构并不能使磁盘对系统可用,所以需要调用add_disk函数。该函数的实现代码在<drivers/block/genhd.c>文件。

7.1.1.3 request结构

request结构用来代表一个块设备的I/O执行请求,是块设备驱动中非常重要的一个结构。request结构在内核中的声明如下: struct request {

struct list_head queuelist;

unsigned long flags; /* see REQ_ bits below */

sector_t sector; /* next sector to submit */

unsigned long nr_sectors; /* no. of sectors left to submit */

unsigned int current_nr_sectors;

sector_t hard_sector; /* next sector to complete */

unsigned long hard_nr_sectors; /* no. of sectors left to complete */

unsigned int hard_cur_sectors;

struct bio *bio;

struct bio *biotail;

void *elevator_private;

int rq_status; /* should split this into a few status bits */

struct gendisk *rq_disk;

int errors;

unsigned long start_time;

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

}; unsigned short nr_phys_segments; unsigned short nr_hw_segments; int tag; char *buffer; int ref_count; request_queue_t *q; struct request_list *rl; struct completion *waiting; void *special; unsigned int cmd_len; unsigned char cmd[BLK_MAX_CDB]; unsigned int data_len; void *data; unsigned int sense_len; void *sense; unsigned int timeout; struct request_pm_state *pm;

由于request结构包含许多成员,而在实际应用中只用到很少一部分,所以这里只介绍一些request结构的常用成员:

n struct list_head queuelist;

该成员用于把请求链接到请求队列中,一般不能直接访问它,而是通过blkdev_dequeue_request函数来访问。(www.61k.com)

n sector_t sector;

该成员用来表示下一个要提交的扇区。

n unsigned long nr_sectors;

该成员表示剩下多少个扇区没有被提交。

n sector_t hard_sector;

该成员表示还未传输的第一个扇区。

n unsigned long hard_nr_sectors;

该成员表示等待传输扇区的总数。

n struct bio *bio;

该成员表示该请求的bio结构链表,不能直接访问该成员,而要使用rq_for_each_bio函数访问。其中bio结构是在底层对部分块设备I/O请求的描述。

n unsigned short nr_phys_segments;

该成员表示相邻的页被合并后,在物理内存中被这个请求所占用的段的数目。

n char *buffer;

该成员表示查找需要传输的缓冲区。

n request_queue_t *q;

该成员表示一个请求队列。

每个request结构都代表了一个块设备的I/O请求,在较高层次,它可能是通过对多个独立请求合并而来。一个request结构是作为一个bio结构的链表实现的。当然是通过一些管理信息来组合的,这样保证在执行请求时驱动程序能知道执行的位置。关于request结构和bio结构的结合如图7.1所示。从图中可知,cbio和buffer成员指向第一个未被传输的bio

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

结构。[www.61k.com)下一节将介绍请求处理。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图7.1 正在处理请求的请求队列

7.1.2请求处理

上一节中说到block_device_operations结构中没有负责读写的方法,那是因为块设备驱动程序中是用request函数来实现的。request请求函数是块设备驱动程序的核心,实际的工作中,设备的启动都是在request函数中实现的。磁盘驱动程序的性能是整个操作系统中重要的组成部分。因此内核的块设备子系统在编写的时候就非常注意性能方面的问题,除了从所控制的设备上获得信息之外,块设备子系统为驱动程序完成了所有可能的工作。下面将介绍request函数。

7.1.2.1 request函数

块设备驱动程序中request函数原型如下:

ü void request( request_queue_t * q );

该函数用来实现驱动程序处理读写以及其他对设备的操作,在其返回前,request函数不必完成所有队列中的请求。

对大多数设备而言,request函数可能没有完成任何请求,但是它必须启动对请求的响应,并且保证所有请求最终被驱动程序所处理。每个设备都有一个请求队列,这是由于磁盘数据实际传出和传入发生的时间与内核请求的时间相差较大,因此内核需要有一定的灵活性以安排在适当时刻进行传输。当请求队列生成的时候,request函数就与该队列绑定在一起了。对request函数的调用通常是与用户空间进程的动作是异步的,我们不能假设内核正运行在初始化当前请求进程的上下文中。并且我们也无法知道请求所提供的I/O缓存是在内核空间中还是在用户空间中。因此直接对用户空间的任何类型的访问都是会导致错误,所以块设备驱动程序所需要知道的任何关于请求的信息都是通过请求队列来实现的。接下来介绍一个request函数的实例。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

7.1.2.2 request函数实例

选择内核中应用的Amiga伪设备访问16位RAM在ZorroII空间作为一个块设备的例子,该request函数的实现代码如下: static void do_z2_request(request_queue_t *q)

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

{

struct request *req;

while ((req = elv_next_request(q)) != NULL) {

unsigned long start = req->sector << 9;

unsigned long len = req->current_nr_sectors << 9;

if (start + len > z2ram_size) {

printk( KERN_ERR DEVICE_NAME ": bad access: block=%lu, count=%u\n",

req->sector, req->current_nr_sectors);

end_request(req, 0);

continue;

}

while (len) {

unsigned long addr = start & Z2RAM_CHUNKMASK;

unsigned long size = Z2RAM_CHUNKSIZE - addr;

if (len < size)

size = len;

addr += z2ram_map[ start >> Z2RAM_CHUNKSHIFT ];

if (rq_data_dir(req) == READ)

memcpy(req->buffer, (char *)addr, size);

else

memcpy((char *)addr, req->buffer, size);

start += size;

len -= size;

}

end_request(req, 1);

}

}

该函数中的request结构在本章前面的章节中已经介绍,它表示一个块设备的I/O执行请求。(www.61k.com]内核提供了函数elv_next_request来获得队列中第一个未完成的请求,当没有请求处理时,该函数返回NULL。如果两次调用elv_next_request函数,则两次都返回相同的request结构。其中还有一个非常重要的函数end_request,该函数的原型如下:

ü void end_request(struct request *req, int uptodate);

当传递参数uptodate为0时,表示不能成功的完成请求,而当为非0时则表明成功的完成了请求处理。

7.2块设备驱动开发实例

本章将以一个典型的块设备实例为学习对象,讲述块设备驱动的实现过程。本章的研究实例是MMC/SD驱动开发,它是嵌入式设备中经常用到的存储设备。首先介绍一下什么是MMC/SD。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

7.2.1 MMC/SD介绍

MMC的英文全称是MultiMedia Card,是一种体积小容量大的快闪存储卡,由西门子公司(现在称为Infineon)和首推CF卡的SanDisk于1997年推出。[www.61k.com)由于它的封装技术较为先进,而且目前已经相当成熟,它的外形尺寸大约为 32mm×24mm×1.4mm,重量在2克以下,7针引脚,它的体积甚至比SmartMedia还要小,不怕冲击,可反复读写记录30 万次,驱动电压在2.7-3.6V,目前容量多为64M和128M容量。现在这种闪存卡已广泛用于移动电话,数码相机,数码摄像机,MP3等多种数码产品上。MMC在设计之初是瞄准手机和寻呼机市场,之后因其小尺寸等独特优势而迅速被引进更多的应用领域,如数码相机、PDA、MP3播放器、笔记本电脑、便携式游戏机、数码摄像机乃至手持式GPS等。Siemens公司最初之所以将MMC主要定位在手机上,是因手机产品的发展需求而致。当前的大多数手机存储容量相当有限,而功能和各种通信增值服务的增加又对手机的存储容量提出越来越高的要求(更多的电话号码、短消息、手机铃声、图片、录制的声音、记事本甚至多媒体短消息等),但是为越做越小的手机扩充存储容量的难度比其他掌上电子产品更高。而邮票般大小的MMC会比CF等传统产品更适合手机。JVC公司早在1999年已推出了采用MMC的数码摄像机(保存抓拍的静态画面)。为了推动MMC的应用,在1998年成立了MMC协会(MMCA),成为推广MMC标准化的全球性组织,目前其成员除了发起厂商以外还包括Motorola、Nokia、Ericsson、Intel、TI、HP和Toshiba等数百家业界知名厂商。MMC卡的外观图如7.2左图所示,它有7个管脚,所有的MMC卡被直接MMC总线信号相连,表7-1表示这种情况下,MMC各管脚连接信号。其中MMC模式是指利用MMC总线方式传输数据,SPI(Serial Peripheral Interface)模式是指利用SPI信道传输数据。 表7-1MMC总线信号连接说明 管脚号 1 2 3 4 5 6 7

名字 RSV CMD VSS1 VDD CLK VSS2 DAT

MMC模式 类型 (*注1) 描述 NC I/O/PP/OD S S I S I/O/PP

保留 命令/响应 供电接地 供电 时钟 供电接地 数据

名字 CS DI VSS VDD SCLK VSS2 DO

I I/PP S S I S O/PP SPI模式 类型

描述 芯片选择 数据输入 供电接地 供电 时钟 供电接地 数据输出

*注1:类型中:S代表Power supply;I代表input;O代表output;PP代表push-pull;OD代表open-drain;NC代表Not connected。 只用CMD信道来初始化MMC栈,所以兼容所有的卡,每一个卡都有一组设置信息的寄存器,如表7-2所示。 名字 CID RCA DSR CSD

宽度 (位) 描述 128 16 16 128

Card identification number,卡的唯一识别号。必须的 Relative card address,卡的本地系统地址,主机在初始化时动态的分配。必须的

Driver stage register,配置卡的输出驱动。可选的 Card specific data,关于卡操作条件的信息。必须的

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

OCR 32

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

Operation condition register,用于那些

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

不支持全电压范围的卡,用一个特殊的广播命令探测受限制的卡。(www.61k.com)可选的

图7.2 MMC和SD卡

SD卡(Secure Digital Memory Card)是一种基于半导体快闪记忆器的新一代记忆设备。SD卡由日本松下、东芝及美国SanDisk公司于1999年8月共同开发研制。大小犹如一张邮票的SD记忆卡,重量只有2克,但却拥有高记忆容量、快速数据传输率、极大的移动灵活性以及很好的安全性。SD卡在24mm×32mm×2.1mm的体积内结合了SanDisk快闪记忆卡控制与MLC(Multilevel Cell)技术和Toshiba(东芝)0.16u及0.13u的NAND技术,通过9针的接口界面与专门的驱动器相连接,不需要额外的电源来保持其上记忆的信息。而且它是一体化固体介质,没有任何移动部分,所以不用担心机械运动的损坏。SD卡数据传送和物理规范由MMC发展而来,大小和MMC差不多,尺寸为32mm x 24mm x 2.1mm。长宽和MMC一样,只是厚了0.7mm,以容纳更大容量的存贮单元。SD卡与MMC卡保持着向上兼容,也就是说,MMC可以被新的SD设备存取,兼容性则取决于应用软件,但SD卡却不可以被MMC设备存取。SD卡外型采用了与MMC厚度一样的导轨式设计,以使SD设备可以适合MMC。SD接口除了保留MMC的7针外,还在两边多加了2针,作为数据线。采用了NAND型Flash Memory,基本上和SmartMedia的一样,平均数据传输率能达到2MB/s。SD卡的结构能保证数字文件传送的安全性,也很容易重新格式化,所以有着广泛的应用领域,音乐、电影、新闻等多媒体文件都可以方便地保存到SD卡中。因此不少数码相机也开始支持SD卡。目前市场上SD卡的品牌很多,诸如:SANDISK,Kingmax,松下和Kingston。关于SD卡的示意图如7.2右图所示,关于SD卡各管脚的连接情况如表7-3所示。其中SD模式是指利用SD总线方式传输数据,SPI模式是指利用SPI信道传输数据。 表7-3SD卡管脚分配 管脚号 1 2 3 4 5 6 7

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

名字

SD模式 类型*(注2) 描述

卡探测/数据线[位3]

CMD VSS1 VDD CLK VSS2 DAT0

PP S S I S I/O/PP

命令/响应 供电接地 供电 时钟 供电接地 数据线[位0]

DI VSS VDD SCLK VSS2 DO

I S S I S O/PP

数据输入 供电 供电 时钟 供电接地 数据输出

名字 CS

CD/DAT3 I/O/PP

I

SPI模式 类型

描述 芯片选择

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

8

9 DAT1 DAT2 I/O/PP I/O/PP 数据线[位1] 数据线[位2] RSV RSV - - - -

*注2:类型中:S代表Power supply;I代表input;O代表output;PP代表push-pull。[www.61k.com]

与MMC类似,SD卡也有一组设置信息的寄存器,关于SD卡的这些寄存器描述在表7-4中。

表7-4SD卡寄存器 名字

CID

RCA*(注3)

DSR

CSD

SCR

OCR 宽度 (位) 描述 128 16 16 128 64 32 Card identification number,卡的唯一识别号。必须的 Relative card address,卡的本地系统地址,主机在初始化时动态的分配。必须的 Driver stage register,配置卡的输出驱动。可选的 Card specific data,关于卡操作条件的信息。必须的 SD Configuration register,关于SD卡特殊功能配置的寄存器。必须的 Operation condition register,用于那些不支持全电压范围

的卡,用一个特殊的广播命令探测受限制的卡。可选的

*注3:RCA寄存器是无效的在SPI模式。

7.2.2 S3C2410提供的SDI接口

S3C2410芯片内部提供的SDI(Secure Digital Interface,安全数字接口)支持SD存储卡,SDIO设备和MMC接口。S3C2410芯片的SDI有以下特点:

l 遵守SD存储卡V1.0规范/兼容MMC V2.11规范

l 兼容SDIO卡V1.0规范

l 16字(64字节)的FIFO(先进先出)用于数据的收发

l 40位命令寄存器(SDICARG[31:0]+SDICCON[7:0])

l 136位的响应寄存器(SDIRSPn[127:0]+ SDICSTA[7:0])

l 8位的预定标器逻辑

l CRC7&CRC16发生器

l 正常的模式和DMA数据模式(字节或字传输)

l 1位/4位(总线宽度)模式&块/流模式支持选择

l 支持高达25MHz数据传输模式用于SDI

l 支持高达10MHz数据传输模式用于MMC

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图7.3 SDI内部结构图

关于SDI内部结构图如7.3所示,一个串行时钟线与五条数据线同步用于频移和抽样信息。(www.61k.com)根据传输频率设置SDIPRE寄存器相应的位,可以通过调节波特率数据寄存器的值来修改频率。通常对于SDI模块编程基本步骤是:一是设置SDICON寄存器用来配置合适的时钟和中断;二是设置SDIPRE寄存器配置合适的频率;三是等待74SDCLK时钟用来初始化相应的卡。就命令(CMD)通道编程来说:一是写命令参数(32位)到SDICARG寄存器;二是通过设置SDICCON[8]寄存器确定命令类型和开始命令;三是当SDICSTA寄存器某具体的位被设置时确认结束SDI命令操作;四是通过写SDICSTA寄存器相应的位来清除标识。就数据(DAT)通道编程来说:一是写SDIDTIMER寄存器来设置间歇周期时间;二是写SDIBSIZE寄存器来设置块的大小;三是通过设置SDIDCON寄存器来确定块传输的模式,如宽总线,DMA等。四是通过检查SDIFSTA寄存器确定发送数据的FIFO是否可用,当可用时写发送数据;五是通过检查SDIFSTA寄存器确定接收数据的FIFO是否可用,当可用时读接收数据;六是当数据传输完成位(SDIDSTA[4])被设置时确认结束SDI数据操作;七是清除SDIDSTA寄存器相应的位。注意,如果是MMC,最大的数据传输时钟是10MHz,在MMC写模式,尽管写正确但CRC(Cyclic Redundancy Check,循环冗余码校验)*(注

4)错误还是会发生,为了能可靠的传输数据,在写完数据后就读数据并且比较它。如果遇到长时间的响应,在接收来自SD设备的响应数据之后,CRC错误应该被探测。编程人员应该通过软件来检测接收响应的CRC。 *注4:CRC是一种算法,常用于数据校验,CRC校验的基本思想是利用线性编码理论,在发送端根据要传送的k位二进制码序列,以一定的规则产生一个校验用的监督码(CRC码)r位,并附在信息后边,构成一个新的二进制码序列数共(k+r)位,最后发送出去。在接收端,则根据信息码和CRC码之间所遵循的规则进行检验,以确定传送中是否出错。

7.2.3 SDI相关的寄存器

接下来讲述编程时需要用到的SDI寄存器,关于SDI寄存器的相关信息需要查阅

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

S3C2410芯片的用户手册获得。[www.61k.com)

7.2.3.1 SDI控制(SDICON)寄存器

寄存器 SDICON

地址 0x5A000000 位 [4]

字边界时

0=类型A 1=类型B

[3]

确定是否SD主机接收到来自卡的SDIO中断 0=忽略 1=接收SDIO中断

当SD主机等待下一个块在多个块读模式时确定读等待请求

RWaitEn

[2]

信号产生。这个位需要延迟下一个被传输的块。 0=禁止(不产生) 1=读等待启动(使用SDIO)

FRST ENCLK

[1] [0]

复位FIFO值,这个位自动被清除。 0=正常模式 1=FIFO复位 确定是否SDCLK输出被启动 0=禁止 1=时钟被启动

0 0 0

R/W R/W

SDI控制寄存器

描述

确定字节顺序类型当读(写)数据从(到)SD主机FIFO的

描述

复位值 0x0 初始状态

SDICON ByteOrder RcvIOInt

注意: 字节顺序类型 类型A:D [7:0] -> D [15:8] -> D [23:16] -> D [31:24] 类型B:D [31:24] -> D [23:16] -> D [15:8] -> D [7:0]

7.2.3.2 SDI波特率预定标(SDIPRE)寄存器

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

寄存器 SDIPRE

SDIPRE Prescaler Value

位 [7:0]

确定SDI时钟频率

波特率=PCLK/2/(预定标值+1)

描述

初始状态

地址 0x5A000004

R/W R/W

描述

SDI波特率预定标寄存器

复位值 0x0

7.2.3.3 SDI命令参数(SDICARG)寄存器

寄存器 SDICARG

地址 0x5A000008 位

[31:0] 命令参数

R/W R/W

描述

SDI命令参数寄存器

描述

复位值 0x0 初始状态 0x00000000

SDIPRE CmdArg

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

7.2.3.4 SDI命令控制(SDICCON)寄存器

寄存器 SDICCON

地址 0x5A00000C 位 [12] [11] [10] [9] [8] [7:0]

R/W R/W

描述

SDI命令控制寄存器

描述

确定命令类型是否是异常中止(用于SDIO) 0=正常命令 1=异常中止(CMD12,CMD52) 确定命令类型是否包含数据(用于SDIO) 0=不包含数据 1=包含数据 确定主机是否接收一个136位的长响应。(www.61k.com] 0=短响应 1=长响应 确定主机是否等待一个响应。 0=没有响应 1=等等响应 确定是否命令操作开始

0=命令就绪 1=命令开始 命令索引从2位开始(8位)

复位值 0x0 初始状态

0 0 0 0 0x00

SDICCON AbortCmd WithData LongRsp WaitRsp CMST CmdIndex

7.2.3.5 SDI命令状态(SDICSTA)寄存器

寄存器 SDICSTA

地址 0x5A000010 位 [12] [11] [10] [9] [8] [7:0]

R/W R/W

描述

SDI命令状态寄存器

描述

当命令响应接收时,CRC检测失败 0=没有探测到 1=CRC失败 发送命令(不关心响应)

0=没有探测到 1=命令结束 命令响应时间到。

0=没有探测到 1=时间到 命令响应接收。

0=没有探测到 1=响应结束 命令传输过程中

0=没有探测到 1=命令传输中 响应索引从2位开始(8位)

复位值 0x0 初始状态

0 0 0 0 0x00

SDICSTA RspCrc CmdSent CmdTout RspFin CmdOn RspIndex

7.2.3.6 SDI响应(SDIRSP)寄存器

寄存器 SDIRSP0

地址 0x5A000014

R/W R

描述

SDI响应寄存器0

复位值 0x0

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

SDIRSP0 位 描述 初始状态 0x00000000 复位值 0x0 初始状态

0x00 0x00000000 复位值 0xy0 初始状态 0x00000000 复位值 0x0y 初始状态 0x00000000

Response0 [31:0] 卡状态[31:0](短响应),卡状态[127:96]长响应

寄存器 SDIRSP1

地址 0x5A000018 位 [23:0]

R/W R

描述

SDI响应寄存器1

描述

没有使用(短响应),卡状态[87:64]长响应

R/W R

描述

SDI响应寄存器2

描述

SDIRSP1 RCRC7 Response0

[31:24] CRC7(包含结束位,短响应),卡状态[95:88](长响应)

寄存器 SDIRSP2

地址 0x5A00001C 位

SDIRSP2

Response2 [31:0] 没有使用(短响应),卡状态[63:32]长响应

寄存器 SDIRSP3

地址 0x5A000020 位

R/W R

描述

SDI响应寄存器3

描述

SDIRSP3

Response3 [31:0] 没有使用(短响应),卡状态[31:0]长响应

7.2.3.7 SDI数据/占用定时器(SDIDTIMER)寄存器

寄存器 SDIDTIMER

地址 0x5A000024 位

R/W R/W

描述

SDI数据/占用定时器寄存器

描述

复位值 0x2000 初始状态 0x2000

SDIDTIMER DataTimer

[15:0] 数据/占用时间中止周期(0~65535周期)

7.2.3.8 SDI块大小(SDIBSIZE)寄存器

寄存器 SDIBSIZE

地址 0x5A000028 位 [11:0]

块大小值(0~4095字节)

R/W R/W

描述

SDI块大小寄存器

描述

复位值 0x0 初始状态 0x000

SDIBSIZE BlkSize

注意:如果是多个块,BlkSize必须组成4字节大小。[www.61k.com]

与SDI相关的寄存器还有许多,如果想了解更多寄存器的信息请参考S3C2410芯片用户手册,这里就不再一一介绍。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

7.2.4 MMC/SD驱动概要设计

由于SD卡是继承MMC而诞生的,并且SD卡设备完全兼容MMC,所以在本实例中主要讲述MMC驱动的实现。(www.61k.com]首先让我们来看一下MMC/SD卡的硬件连接图。

7.2.4.1 MMC/SD与主机的接口连接

图7.4是SD/MMC与S3C2410的接口连接图,从这个图我们很清楚的看到SD/MMC的管脚是如何与主机设备相连接的。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图7.4 MMC/SD接口连接

7.2.4.2 MMC/SD驱动框架

关于MMC/SD驱动的设计,首先需要遵守MMC/SD规范,其次要有MMC/SD核心的支持,而MMC/SD驱动程序是基于其核心和通信协议之上的,如图7.5所示。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图7.5 MMC/SD驱动程序的位置

为了不使读者混淆MMC和SD驱动的实现,这里只介绍MMC驱动的实现过程,SD驱动的实现与MMC非常类似。(www.61k.com]根据MMC规范V2.11,MMC的内部实现有两种通信协议可以选择,MMC总线协议和SPI总线协议。MMC总线被设计用于连接电子晶体的大容量内存或在一个卡中的I/O设备,经常用于多媒体应用。该总线可用于低成本系统并且拥有快速数据传输特点。MMC总线系统如图7.6所示,它是单个主控总线连接多个从设备。MMC总线被分成三组:电源,数据传输(命令和数据)和时钟。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

图7.6 MMC总线系统

对任何一种存储卡应用而言,它必须具备下列的基本功能:

? 热插拔功能。

? 当存储卡插入或移除时,中断服务程序(ISR)必须能够立即发现和反应。当存储卡插入

时,ISR必须能将新加入的存储卡地址或名称在应用程式中显示出来。移除时,此存储卡地址或名称也能从应用程式中消失。当存储卡插入时,存储卡必须像磁盘一样,被挂载(mount)在磁盘的根目录底下。同理,当存储卡被移除时,它就会被卸载(unmount)。 ? 如果存储卡内部具有文件系统,它虽然可以被虚拟成磁盘,也就是块设备(block device),

但是当系统发出移除存储卡的请求时,发现存储卡早已不存在了,此时,块程式会不知所

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

措,认为是错误。[www.61k.com]所以,在系统发出移除存储卡的请求之前,块设备层必须能先侦测出存储卡是否有存在。而且如果它不存在,就立即终止这项请求。因此,在设计存储卡的最底层驱动程序时,必须提供一个函式而且它必须指向上层的块设备层。

7.2.4.3 MMC驱动的核心设计

我们即将要介绍的MMC驱动的实现是利用MMC总线实现的,关于MMC规范希望读者在开发MMC驱动之前仔细阅读它,这里就不详细介绍了。设计MMC驱动程序最主要的工作就是实现MMC数据传输的控制,当开机之后,MMC的初始化是通过一种特殊的数据流通信协定来完成。此通信协定的信息格式具有下列几种类型:

命令:表示开始执行一个作业。它是从主机发出,至单一的MMC存储卡(单一位址的命令),或至全部有连接的MMC存储卡(广播命令)。此命令信息是在CMD线上以串行方式传输。

回应:它的传送方向和命令信息相反,它是从一个特定地址的MMC存储卡,或全部的MMC存储卡同步地传送给主机,是对之前接收到的命令信息的回应。

数据:此信息是从主机或MMC存储卡发出。

MMC存储卡的地址是由数据流控制器在初始化时设定的。它们的唯一的CID(Customer Identity)号码代表它们自己。MMC有两种数据传输命令,如下所示:

序列式命令:这个命令会启动连续性的数据流。唯有收到停止命令时,传输作业才会结束。这个模式可以减少传送额外的命令。

块式命令:此命令在传送一个数据块之后,会传送CRC位元。它的读写作业支持单一块或多个块的传输。和序列式命令类似,在收到停止命令时,多块传输作业才会结束。

MMC的数据读写作业可以包含:单一区块、多区块、串流的传输。这些作业可以通过DMA来加速传输,这是由设定模式寄存器(mode register)的位元值来做切换。块的长度也必须在模式寄存器中设定。下面开始分析MMC驱动程序的实现。

7.2.5 MMC驱动程序分析

现在要分析MMC驱动程序的实现过程,读者将会看到它的实现过程与字符设备驱动的实现有很大的不同,从而让读者感受到块设备驱动实现方法与字符设备驱动的不同之处。首先来分析它的初始化过程。

7.2.5.1 MMC初始化

对于MMC的初始化主要完成三个任务:一是注册MMC块设备;二是建立MMC设备文件系统的目录;三是注册具体的MMC介质驱动,实现代码如下所示: static int __init mmc_blk_init(void)

{

int res = -ENOMEM;

res = register_blkdev(major, "mmc");

if (res < 0) {

printk(KERN_WARNING "Unable to get major %d for MMC media: %d\n",

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

major, res);

goto out;

}

if (major == 0)

major = res;

devfs_mk_dir("mmc");

return mmc_register_driver(&mmc_driver);

out:

return res;

}

用register_blkdev函数来实现注册MMC块设备,如果没有指定主设备号,则将动态的分配一个,其中注册的块设备名为mmc。[www.61k.com]其次用devfs_mk_dir函数来建立一个MMC的设备文件目录。最后注册MMC介质的驱动,其中参数mmc_driver是mmc_driver结构的对象,该结构对象的定义如下:

static struct mmc_driver mmc_driver = {

.drv = {

.name = "mmcblk",

},

.probe = mmc_blk_probe,

.remove = mmc_blk_remove,

.suspend = mmc_blk_suspend,

.resume = mmc_blk_resume,

};

该mmc_driver对象定义了MMC驱动的probe,remove,suspend和resume方法,并且程序中相应的实现了这些方法。

顺便让我们看一下卸载MMC设备的实现代码,它的作用正好与初始化相反,首先完成MMC介质驱动的注销,接着删除MMC设备文件系统的目录,最后注销MMC块设备,具体实现代码如下:

static void __exit mmc_blk_exit(void)

{

mmc_unregister_driver(&mmc_driver);

devfs_remove("mmc");

unregister_blkdev(major, "mmc");

}

此外,可以看一下MMC块设备操作的定义,如下代码所示,定义了open,release和ioctl方法,下面将分别介绍这几个方法的具体实现。

static struct block_device_operations mmc_bdops = {

.open = mmc_blk_open,

.release = mmc_blk_release,

.ioctl = mmc_blk_ioctl,

.owner = THIS_MODULE,

};

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

7.2.5.2 open和release方法

块设备的open方法作用与字符设备的open方法类似,它把相关的inode和file结构作为参数,当一个inode指向一个块设备时,inode->i_bdev->bd_disk成员包含了指向相应gendisk结构的指针,该指针可用于获得驱动程序内部的数据结构。(www.61k.com)MMC驱动的open方法实现代码如下: static int mmc_blk_open(struct inode *inode, struct file *filp)

{

struct mmc_blk_data *md;

int ret = -ENXIO;

md = mmc_blk_get(inode->i_bdev->bd_disk);

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

if (md) {

if (md->usage == 2)

check_disk_change(inode->i_bdev);

ret = 0;

}

return ret;

}

其中mmc_blk_data结构表示MMC设备的一个内部数据结构,它的定义以及对各成员的注释如下: struct mmc_blk_data {

spinlock_t lock; /*用于互斥锁*/

struct gendisk *disk; /*gendisk结构*/

struct mmc_queue queue; /*MMC设备请求队列*/

unsigned int usage; /*用户数目*/

unsigned int block_bits; /*块的大小,以位为单位*/

};

用mmc_blk_get函数来获得与参数gendisk结构对应的mmc_blk_data结构体信息,如果正确获得则open方法返回0,否则返回-ENXIO错误信息。当获得的mmc_blk_data结构对象的用户数是2时,调用check_disk_change函数来检查是否更换了磁盘介质,如果更换了介质将使那些所有的磁盘相关的缓冲无效。

接下来讲述release方法,同样它的作用与字符设备中的release方法相似,用它来释放open方法打开的设备,它的实现代码如下:

static int mmc_blk_release(struct inode *inode, struct file *filp)

{

struct mmc_blk_data *md = inode->i_bdev->bd_disk->private_data;

mmc_blk_put(md);

return 0;

}

该方法实现的关键是mmc_blk_put函数,其参数传递的是指向mmc_blk_data结构的指

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

针,该函数完成的任务有,首先将mmc_blk_data结构的对象用户数减1,然后判断它的用户数目是否为0,如果为0,首先释放它的gendisk结构对象,然后清除MMC的请求队列,最后释放mmc_blk_data结构对象的内存。[www.61k.com]

7.2.5.3 ioctl方法

块设备驱动程序提供了ioctl函数执行设备的控制功能,高层的块设备子系统在驱动程序获得ioctl命令之前已经截取了大量的命令,所以实际中ioctl函数的实现比较简单,MMC驱动的ioctl方法实现代码如下: static int mmc_blk_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned

long arg)

{

struct block_device *bdev = inode->i_bdev;

if (cmd == HDIO_GETGEO) {

struct hd_geometry geo;

memset(&geo, 0, sizeof(struct hd_geometry));

geo.cylinders = get_capacity(bdev->bd_disk) / (4 * 16);

geo.heads = 4;

geo.sectors = 16;

geo.start = get_start_sect(bdev);

return copy_to_user((void __user *)arg, &geo, sizeof(geo))? -EFAULT : 0;

}

return -ENOTTY;

}

MMC驱动的ioctl方法只实现了一个命令,就是获得块设备的几何物理信息,如获得柱面,磁头和扇区大小等信息,然后将这些信息发送给用户空间的应用程序。

7.2.5.4 MMC驱动的request方法

块设备的request方法应该是与字符设备驱动的最大不同之处,因为块设备操作中没有定义read/write等方法,所以块设备中这些方法的实现就交给了request方法来实现。其实request方法的实现是在MMC初始化时就被实现的,只是为了能更详细的介绍它的实现所以专门放在这里介绍。MMC驱动的request方法是由mmc_blk_prep_rq和mmc_blk_issue_rq这两个函数实现,这两个函数被mmc_blk_alloc函数调用,而这个函数又被mmc_blk_probe函数调用。mmc_blk_probe函数是在MMC驱动初始化时调用的,所以说request方法也在初始化时被调用。mmc_blk_prep_rq函数用来检查是否有MMC设备和MMC主机,它的实现代码如下所示:

static int mmc_blk_prep_rq(struct mmc_queue *mq, struct request *req)

{

struct mmc_blk_data *md = mq->data;

int stat = BLKPREP_OK;

if (!md || !mq->card) {

printk(KERN_ERR "%s: killing request - no device/host\n",

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

} req->rq_disk->disk_name); stat = BLKPREP_KILL; } return stat;

当检测到没有MMC主机或没有MMC设备时,该函数返回一个非0值。(www.61k.com)否则,返回一个0代表设备都正常存在。 mmc_blk_issue_rq函数实现的块设备的真正read/write等操作,它的实现代码如下所示:

static int mmc_blk_issue_rq(struct mmc_queue *mq, struct request *req)

{

……

do {

struct mmc_blk_request brq;

struct mmc_command cmd;

……

if (rq_data_dir(req) == READ) {

brq.cmd.opcode=brq.data.blocks>1?MMC_READ_MULTIPLE_BLOCK: MMC_READ_SINGLE_BLOCK;

brq.data.flags |= MMC_DATA_READ;

} else {

brq.cmd.opcode = MMC_WRITE_BLOCK;

brq.cmd.flags = MMC_RSP_R1B;

brq.data.flags |= MMC_DATA_WRITE;

brq.data.blocks = 1;

}

brq.mrq.stop = brq.data.blocks > 1? &brq.stop : NULL;

brq.data.sg = mq->sg;

brq.data.sg_len = blk_rq_map_sg(req->q, req, brq.data.sg);

mmc_wait_for_req(card->host, &brq.mrq);

……

do {

int err;

cmd.opcode = MMC_SEND_STATUS;

cmd.arg = card->rca << 16;

cmd.flags = MMC_RSP_R1;

err = mmc_wait_for_cmd(card->host, &cmd, 5);

if (err) {

printk(KERN_ERR "%s: error %d requesting status\n",

req->rq_disk->disk_name, err);

goto cmd_err;

}

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

} while (!(cmd.resp[0] & R1_READY_FOR_DATA));

spin_lock_irq(&md->lock);

ret = end_that_request_chunk(req, 1, brq.data.bytes_xfered);

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

if (!ret) {

add_disk_randomness(req->rq_disk);

blkdev_dequeue_request(req);

end_that_request_last(req);

}

spin_unlock_irq(&md->lock);

} while (ret);

mmc_card_release_host(card);

return 1;

cmd_err:

……

return 0;

}

该函数主要完成MMC的命令请求,以及相应的数据传输。[www.61k.com)它可以是单个块的操作,也可以是多个块的操作。其中mmc_wait_for_req函数用来实现MMC主机的命令请求,用来启动一个MMC主机请求。mmc_wait_for_cmd函数用来启动一个MMC命令并且等待这个命令的完成。用end_that_request_chunk函数来结束一个I/O请求,当该函数返回0时,证明已经处理了这个请求,返回1时,表明这个请求还在悬挂在缓冲中。当结束这个I/O请求时,必须调用mmc_card_release_host函数来释放MMC主机,以便其他MMC主机声明使用MMC驱动操作。

此外,还有一个一般的MMC请求处理函数,那就是mmc_request函数,这个函数被那些MMC主机的队列调用,当MMC主机空闲时,将利用这个函数查找一个等待的请求,然后把它唤醒处理。mmc_request函数的实现代码如下:

static void mmc_request(request_queue_t *q)

{

struct mmc_queue *mq = q->queuedata;

if (!mq->req)

wake_up(&mq->thread_wq);

}

到此为止,关于MMC驱动程序的主要实现代码已经分析完毕,该程序是基于Linux

2.6.10内核中提供的MMC代码为基础的,读者可以通过附件的光盘获取MMC驱动的参考实现代码。

7.2.6 S3C2410 SDI接口驱动分析

要实现MMC设备驱动的正常工作,还需要实现它S3C2410的SDI接口驱动。前面已经介绍过,SDI接口用于驱动SD存储卡、SDIO设备和MMC。首先来看一下该接口驱动的初始化。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

7.2.6.1 SDI初始化

首先在设计S3C2410的SDI接口时,将其作为一个设备驱动来实现的,所以该驱动入口函数就是它的加载函数s3c2410sdi_init,它的实现代码比较简单,具体如下: static int __init s3c2410sdi_init(void)

{

return driver_register(&s3c2410sdi_driver);

}

其调用driver_register函数来注册SDI接口驱动,其中传递参数s3c2410sdi_driver被定义为SDI接口驱动,它的定义如下: static struct device_driver s3c2410sdi_driver =

{

.name = "s3c2410-sdi",

.bus = &platform_bus_type,

.probe = s3c2410sdi_probe,

.remove = s3c2410sdi_remove,

};

通过上面代码可以看出,它定义了SDI接口驱动的两个方法,分别是probe和remove。[www.61k.com]这两个函数的作用类似于MMC设备驱动中的probe和remove方法。这里probe方法是由s3c2410sdi_probe函数来实现,主要完成SDI接口的初始化工作,包括MMC host的初始化,中断的初始等。由于篇幅的关系,这里不再列举它的实现代码,读者可以参考附件光盘中的参考代码。此外,remove方法是由s3c2410sdi_remove函数来完成,用它来实现与s3c2410sdi_probe函数相反功能的操作。

最后再看一下SDI接口驱动的卸载程序,它的实现也非常简单,调用driver_unregister函数来实现SDI接口驱动的卸载功能,具体代码如下:

static void __exit s3c2410sdi_exit(void)

{

driver_unregister(&s3c2410sdi_driver);

}

7.2.6.2 SDI接口驱动方法

在分析SDI接口驱动方法的实现之前,首先看一下SDI接口驱动方法的定义,它是由s3c2410sdi_ops变量定义的,该变量的定义如下:

static struct mmcwww.61k.comops s3c2410sdi_ops = {

.request = s3c2410sdi_request,

.set_ios = s3c2410sdi_set_ios,

};

其中mmcwww.61k.comops结构定义在<include/linux/mmc/host.h>文件中,用于定义MMC host驱动的方法,该结构中只定义了request和set_ios两个方法。其中request方法由s3c2410sdi_request函数实现,主要完成MMC所有请求命令相关的处理功能,包括数据的读/写等实现。另外set_ios方法由s3c2410sdi_set_ios函数实现,主要用于设置MMC电源的

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

开/关,以及开启MMC时钟等功能。[www.61k.com]由于SDI接口驱动方法实现代码较长,并且也不是我们研究的重点,所以这里只是做一个简单的介绍,有兴趣的读者可以分析附件光盘中的提供的参考代码。

7.2.7配置和编译驱动程序

与第6章中介绍的触摸屏设备驱动类似,需要相应的配置才能进行正确的编译。由于MMC设备驱动内核中已经提供,只不过内核中提供的驱动是应用于X86平台的,而我们这里开发的目标平台是ARM,所以需要一定的修改。本实例将源程序放在了driver/mmc目录下。同时需要修改该目录下Kconfig配置文件,添加以下内容到该文件: config MMC_S3C2410

tristate "Samsung S3C2410 Multimedia Card Interface support"

depends on ARCH_S3C2410 && MMC

help

This selects the Samsung S3C2410 Multimedia Card Interface

support.

If unsure, say N.

添加以上内容后,在用make menuconfig配置内核时会增加关于S3C2410的MMC选项,此外,还需要修改driver/mmc/Makefile文件,添加下面一行内容:

obj-$(CONFIG_MMC_S3C2410) += s3c2410mci.o

修改完配置文件和Makefile文件,最后重新配置内核选项,将触摸屏选项以模块形式添加或直接添加到内核,然后就可以编译该驱动程序了。

7.3本章小结

本章讲述了另一类常见的Linux设备驱动程序——块设备驱动,首先讲述了块设备相关的重要数据结构,包括block_device_operations,gendisk,request结构,并且讲述了块设备中非常重要的请求处理函数;然后重点讲述了一个块设备驱动的典型实例——MMC驱动开发,读者通过学习该设备驱动程序将对块设备驱动开发有深入的了解,此外对MMC/SD的工作原理有一定的了解。下一章将介绍另一类常见的Linux设备驱动程序——网络设备。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

7.4常见问题

1.字符设备与块设备之间的主要区别?

参考答案:字符设备与快设备的最大不同就是:字符设备传输数据是按字节的大小传输,而块设备是以块为单位传输,通常块的大小是1024字节。其次字符设备通常是直接作用于I/O端口的,而块设备是间接作用的,因为它与内核之间是有缓冲区的。

2.块设备中请求处理函数的作用?

参考答案:请求函数是块设备驱动程序的核心,实际的工作中,设备的启动,以及对设备的读取都是在request函数中实现的。使用请求函数的原因是块设备利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求时就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

CPU时间。(www.61k.com)

3.MMC与SD卡的主要区别?

参考答案:MMC与SD卡都是当今应用非常广的小型存储设备,当然它们还是有区别的。首先,在外形上,MMC外形尺寸大约为 32mm×24mm×1.4mm,而SD卡外形尺寸是32mm×24mm×2.1mm。其次,MMC有7个针脚。而SD卡有9个针脚。最后,支持SD卡的设备一定支持MMC,而支持MMC的设备不能支持SD卡,所以它们之间并不是互相兼容的。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

第8章 网络设备驱动程序

本章学习目标:

l 熟悉网络设备驱动程序概念

l 熟悉网络设备驱动相关的两个结构:net_device, sk_buff

l 熟悉常见的网络协议和以太网

l 理解CS8900A网卡的硬件接口连接

l 理解CS8900A网卡驱动程序实现

8.1网络设备驱动介绍

在分析了字符设备驱动和块设备驱动之后,接下来应该分析网络驱动程序了。[www.61k.com)与之前两种设备驱动不同的是,网络驱动程序不再是对文件操作了,而是由专门的网络接口来实现。应用程序不能直接访问网络驱动程序,只能由网络子系统与它交互。此外,不象字符设备和块设备在/dev目录下有一个特殊文件作为其设备,而网络设备就没有这样的入口点。网络设备与块设备最重要的不同就是:块设备只响应来自内核的请求,而网络驱动程序异步的接受来自外部世界的数据包。首先让我们来看一下网络设备驱动相关的重要数据结构。

8.1.1 网络设备驱动相关的重要结构

与网络设备驱动相关的重要数据有两个,分别是和net_device和sk_buff结构,下面将详细介绍这两个结构体。

8.1.1.1 net_device结构

该结构是网络设备驱动的核心,它包含了许多成员。由于net_device结构体庞大,这里就不列举它的详细定义,读者可以参考<include/linux/netdevice.h>文件,阅读它的完整定义。这里将介绍一下该结构的一些常用成员。该结构可分为五个部分:

1.全局成员

n char name[IFNAMSIZ];

该成员表示设备的名称。

n unsigned long state;

该成员表示设备的状态,它包含许多标志,驱动程序无需直接操作这些标志,而是内核提供一组工具函数来实现。

n struct net_device *next;

该成员表示全局链表下一个设备的指针,注意,驱动程序不应该修改这个成员。 n int (*init)(struct net_device *dev);

该成员是一个初始化函数,如果这个指针被设置了,则register_netdev函数将调用该函数完成对net_device结构的初始化。目前大多数网络驱动程序不再使用这个函数了,通常他

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

们是在注册接口之前完成初始化工作。(www.61k.com)

2.硬件相关成员

下面的成员包含相关设备的底层硬件信息。

n unsigned long mem_end;

该成员表示共享内存的终止地址。

n unsigned long mem_start;

该成员表示共享内存的起始地址。

n unsigned long base_addr;

该成员表示网络接口的I/O基地址。

n unsigned int irq;

该成员表示设备的中断号。

n unsigned char if_port;

该成员表示多个端口设备上使用哪个端口。

n unsigned char dma;

该成员表示为设备分配的DMA通道。

3.接口相关成员

n unsigned mtu;

该成员表示最大传输单元(MTU)值的接口。

n unsigned short type;

该成员表示硬件类型的接口。

n unsigned short hard_header_len;

该成员表示数据包的头信息的长度。

n unsigned char broadcast[MAX_ADDR_LEN];

该成员表示硬件广播地址。

n unsigned char dev_addr[MAX_ADDR_LEN];

该成员表示硬件地址。

n unsigned char addr_len;

该成员表示硬件地址长度。

n unsigned long tx_queue_len;

该成员表示在设备的传输队列中排队的最大frame个数。

n unsigned short flags;

该成员表示接口标志。

4.设备方法成员

n int (*open)(struct net_device *dev);

该方法用于打开接口,该函数应该注册所有的系统资源(I/O端口,IRQ,DMA等等),打开硬件并对设备执行其所需的设置。

n int (*stop)(struct net_device *dev);

该方法用于终止接口,与open执行的操作相反。

n int (*hard_start_xmit) (struct sk_buff *skb, struct net_device *dev);

该方法用于初始化数据包的传输。

n int (*hard_header) (struct sk_buff *skb, struct net_device *dev, unsigned short type, void

*daddr, void *saddr, unsigned len);

该方法根据先前检索到的源和目标硬件地址建立硬件头。该函数在调用hard_start_xmit函数之前调用,它的任务是将作为参数传递进入的信息组织成设备特有的硬件头信息。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

struct net_device_stats* (*get_stats)(struct net_device *dev);

该方法用于获得接口的统计信息,例如查看ip地址等调用该方法。[www.61k.com]

n int (*rebuild_header)(struct sk_buff *skb);

该方法用于重新建立硬件头,在传输数据包之前并且在完成ARP解析之后调用该方法。 n int (*set_config)(struct net_device *dev, struct ifmap *map);

该方法用于改变接口配置。

n void (*tx_timeout) (struct net_device *dev);

该方法用于解决数据包传输失败并重新开始数据包的传输。

5.工具相关成员

n unsigned long trans_start;

该成员用于保存jiffies值,在传输开始及接收到数据包时负责更新。

n void *priv;

该成员等价于filp->private_data。在现代的驱动程序中,该成员由alloc_netdev设置,不能直接访问,如果要访问需要使用netdev_priv函数。

n int watchdog_timeo;

该成员表示在网络层确定传输已经超时并且调用驱动程序的tx_timeout函数之前的最小时间。

n spinlock_t xmit_lock;

该成员用来避免同时对驱动程序的hard_start_xmit函数的多次调用。

n int xmit_lock_owner;

该成员用于获得xmit_lock的CPU编号。

n struct dev_mc_list *mc_list;

该成员表示处理组播传输。

n int mc_count;

该成员表示mc_list所包含的项数目。

net_device结构还包括许多其他成员,不过在网络驱动程序中很少用它们,所以这里只介绍了常用的一些成员。 n

8.1.1.2 sk_buff结构

该结构是Linux内核网络子系统中一个非常重要的结构,sk_buff是socket(套接字)缓冲区结构,即socket buffer的简写。该结构定义在<include/linux/skbuff.h>文件中。下面将介绍sk_buff结构中那些可能被驱动程序中使用的成员。

n struct sk_buff *next;

该成员指向下一个sk_buff结构对象。

n struct sk_buff *prev;

该成员指向前一个sk_buff结构对象。

n unsigned char *head;

该成员表示指向已分配空间的开头。

n unsigned char *data;

该成员表示有效octet*(注1)的开头。

n unsigned char *tail;

该成员表示有效octet的结尾。

n unsigned char *end;

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

n

n

n

n 该成员指向tail可达到的最大地址。(www.61k.com) unsigned int len; 该成员表示数据包中全部数据的长度。 unsigned int data_len; 该成员表示分隔存储的数据片断的长度。 unsigned char ip_summed; 该成员表示对数据包的校验策略。 unsigned char pkt_type;

该成员表示在发送过程中使用的数据包类型。 *注1:octet在网络中表示的8位数据,和Byte(字节)类似,不过在网络术语中经常使用octet而不使用byte。

8.1.2常见的网络术语

开发网络设备驱动程序需要对网络技术有一定的了解,尤其是对常见的网络通信协议和以太网有所了解。首先介绍一下常见的网络协议。

8.1.2.1常见的网络协议

TCP/IP是Transfer Control Protocol/ Internet Protocol的缩写,即传输控制协议/网际协议。它是互联网中最基本的协议,主要用于定义网络层和网络层之上的协议标准。

下面将介绍在嵌入式设备中经常会用到的几种协议。

l TCP

Transfer Control Protocol,传输控制协议。在发送端,TCP 将要发送的数据拆分为数据段,IP 将数据段组装为包含数据段以及发送方地址和目的地址的分组。然后 IP 将分组发送到路由器以便传递。在接收端,IP 接收分组并将其拆分为数据段。TCP 将数据段组装为原始的数据集。

l IP

Internet Protocol,网际网协议。将数据从一个 Internet 位置传递到另一个位置的 TCP/IP 的部分。IP 负责通过网络定址和发送 TCP 分组。IP 提供不保证分组到达目的地或以发送时的序列接受的最大程度的无连接传递系统。

l ICMP

Internet Control Messages Protocol, 网间控制报文协议。网际协议(IP)的扩展,ICMP 允许出错消息的生成、检测分组和与 IP 相关的信息邮件。

l UDP

User Datagram Protocol,用户数据报协议。它是ISO((International Organization for Standardization,国际标准化组织)参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。UDP 协议基本上是 IP 协议与上层协议的接口。UDP 协议适用端口分别运行在同一台设备上的多个应用程序。

l ARP/RARP

Address Resolution Protocol,地址解析协议。一种网络维护协议,是 TCP/IP 套件的成员(与数据传输没有直接关系)。它用于动态发现与给定主机的高层 IP 地址对应

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

的低层物理网络硬件地址。[www.61k.com]ARP 限于支持广播包的物理网络系统。与其作用相反的是Reverse Address Resolution Protocol, 逆向地址解析协议。

8.1.2.2以太网介绍

1973年,施乐(Xerox)公司设计了第一个局域网系统,被命名为Ethernet,带宽为

2.94Mbps。1982年,DEC、Intel和Xerox联合发表了Ethernet Version 2规范,将带宽提高到了10Mbps,并正式投入商业市场。1983年,IEEE通过了802.3 CSMA/CD规范。IEEE 802标准是由IEEE(国际电气和电子工程师学会)制订的局域网标准,IEEE 802委员会有10多个分委员会。关于802标准分类如下:

l 802.1A,概述、体系结构和网络互连,网络管理

l 802.1B,寻址、网络管理、网间互连及高层接口

l 802.2,逻辑链路控制LLC

l 802.3,CSMA/CD共享总线网,即Ethernet

l 802.5,令牌环网(Token-Ring)

l 802.11,无线局域网

IEEE 802.3命名规则是:IEEE 802.3 X TYPE-Y NAME

其中X表示传输介质,5指粗同轴电缆,2指细同轴电缆,T指双绞线,F指光纤。TYPE代表传输方式,Base指基带传输,Broad指宽带传输。Y与X一样表示传输介质。NAME表示局域网的名称,Ethernet为以太网,FastEthernet为快速以太网,GigaEhternet为千兆以太网,F指光纤。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

以太网的数据链路层,由以下部分组成:

l 逻辑链路控制子层(Logical Link Control,即LLC子层),为网络层定义了各种服务

及接口

l 介质访问控制子层(Media Access Control,即MAC子层),定义了对各种物理传输

介质的访问及控制技术

IEEE 802标准为各种局域网技术定义了统一的LLC子层,而各种局域网技术的MAC子层不尽相同,所以以太网的数据链路层主要是以太网的MAC子层。

CSMA/CD(Carrier Sense Multiple Access/Collision Detected)协议的全称为带冲突检测的载波监听多路访问技术,是以太网中所使用的介质访问控制技术。这种介质访问控制技术是基于共享介质的,采用共享总线的拓扑结构,以广播的形式进行数据传送。在某一时刻,连接在共享总线上的所有站点中,只能有一个站点可以发送数据。

CSMA协议是指:

l 总线有两个状态:“空闲”和“忙”

l 每个站点在使用总线发送数据帧之前首先监听总线,查看总线是否处于“空闲”状态,

如果总线“忙”就继续等待,继续监听,一直到总线“空闲”

l 要发送数据帧的站点在监听到总线“空闲”时,开始发送数据帧

CD技术是指:

l 在使用CSMA协议时,有可能会出现两个或两个以上的站点同时监听到总线“空闲”

的情况,此时这些站点将同时开始发送数据帧,出现这种情况时,总线会发生冲突,导致所有站点的发送全部失败

l 每个站点在发送数据后必须检测是否发生了冲突

l 在发生冲突的情况下,站点使用二进制指数退避算法重发数据帧

MAC地址是指每张网卡中包含一个独一无二的物理地址,由48位二进制构成,前24位

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

代表设备生产商,由IEEE管理分配,后24位为各生产商内部的编号。[www.61k.com)由于CSMA/CD协议中,帧以广播的形式进行传输,所以总线上的每个站点要根据帧中包含的目的MAC地址和自己网卡中的MAC地址是否一致来决定是否接收该帧。

8.2网络设备驱动开发实例

本章的以网络设备中非常典型的实例——CS8900A以太网适配器为开发对象,主要讲述网络设备驱动的开发过程,使读者清楚认识网路驱动开发与前面讲述的字符设备和块设备驱动开发的不同之处。首先介绍一下CS8900A设备。

8.2.1CS8900A介绍

CS8900A是一个真正的单芯片、全双工的以太网解决方案产品。它的主要功能块包括:一个ISA(Industry Standard Architecture,工业标准结构)总线接口,一个802.3MAC(Media Access Control,介质访问控制)引擎,集成内存,一个串行的EEPROM(电可擦除只读存储器)接口和一个拥有10BASE-T和AUI的完整模拟前后端。

8.2.1.1CS8900A的组成部分介绍

下面分别介绍一下CS8900A的五个组成部分。

ü ISA总线接口

它的配置选项有四个可选中断和3个DMA通道(在初始化时选择)。在存储模式,它支持标准的或读总线循环不用引入附加的等待状态。

ü 集成内存

CS8900A合并一个4k字节的页在芯片内存中,不但消除了这方面的成本还节省了连接外部内存芯片空间。不像其他以太网控制器需要复杂且无效的内存管理机制,CS8900A内存完整的传输和接收frame都在芯片内。此外,CS8900A可以操作在内存空间,I/O空间或外部的DMA控制器,提供了设计的最大灵活性。

ü 802.3以太网MAC引擎

CS8900A以太网MAC引擎完全兼容IEEE(Institute of Electrical and Electronics Engineers,电气和电子工程师协会)802.3以太网标准,并且支持全双工操作。它处理以太网frame传输和接收的所有方面,包括:冲突检测,CRC产生和测试等。 ü EEPROM接口

CS8900A提供一个简单且有效的串行EEPROM接口,允许配置信息保存在一个可选的EEPROM,并且在上电时自动转载。

ü 完整的模拟前后端

CS8900A模拟前后端结合一个Manchester编/解码,一个时钟恢复电路,10BASE-T收发器和完整的附加单元接口。它提供一个手动或自动选择10BASE-T或AUI,并且提供3个片上LED驱动用于显示连接状态,总线状态和以太网线的活动情况。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

8.2.1.2 CS8900A的系统应用

CS8900A被设计用于主板应用和适配器应用。(www.61k.com]下面分别介绍这两种系统应用。

ü CS8900A主板方案应用

CS8900A要求最少数量的外部组件用于完全的以太网结点。所以它的PCB尺寸可以设计的非常小,此外CS8900A有良好的省电特点并且他的CMOS设计使得它完美的应用移动设备或桌面PC机上。主板设计选项包括:一个EEPROM用于储存节点相关的信息;20MHz晶体振荡器(也可用20MHz时钟信号代替)。CS8900A的主板方案应用如图8.1所示。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图8.1 CS8900A完整的以太网主板应用方案

ü CS8900A适配器方案应用

CS8900A高效的数据包结构,流传输技术和自动选择DMA选项,这些使得它用于高性能、低成本的ISA适配卡选择。适配卡包括设计选项包括:一个引导PROM(Programmable Read-Only Memory,可编程序的只读存储器)能被添加用于支持无磁盘应用;10BASE-T发射机和接收机;一个外部的可上锁的地址总线解码电路;片上LED端口。该系统应用如图8.2所示。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图8.2 CS8900A全特征的ISA适配器方案

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

8.2.2CS8900A网卡驱动概要设计

在设计CS8900A以太网卡驱动程序之前,首先需要设计其硬件电路接口,不过一般这部分工作是由硬件工程师来完成,但是对于驱动开发人员来说,必须熟悉这些芯片之间的连接接口,进而设计其驱动程序。(www.61k.com]

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

8.2.2.1 CS8900A网卡接口

CS8900A网卡接口连接如图8.3,它主要由三部分组成:RJ45,10BASE-T和CS8900A。其中CS8900A的芯片图没有画出,不过图中给出了10BASE-T与CS8900A连接的针脚,如RXD-,RXD+,TXD-和TXD+。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图8.3 CS8900A网卡接口连接

8.2.2.2网络驱动程序的体系结构

所有的Linux网络驱动程序遵循通用的接口。设计时采用的是面向对象方法*(注2)。一个设备就是一个对象(net_device 结构),它内部有自己的数据和方法。每一个设备的方法被调用时的第一个参数都是这个设备对象本身。这样这个方法就可以存取自身的数据(类似面向对象程序设计时的this引用)。 *注2:面向对象方法(Object-Oriented Method)是一种把面向对象的思想应用于软件开发过程中,指导开

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

发活动的系统方法,简称OO(Object-Oriented)方法,是建立在“对象”概念基础上的方法学。(www.61k.com)

Linux网络驱动程序的体系结构可以划分为四层如图8.4所示,从上到下分别为:网络协议接口层、网络设备接口层、提供实际功能的设备驱动功能层以及网络设备媒介层。我们在设计网络驱动程序时,最主要的工作就是完成设备驱动功能层,使其满足我们所需的功能。在Linux中所有网络设备都抽象为一个接口,这个接口提供了对所有网络设备的操作集合。由数据结构 struct net_device来表示网络设备在内核中的运行情况,即网络设备接口。它既包括纯软件网络设备接口,如环路(Loopback),也包括硬件网络设备接口,如以太网卡。而由以dev_base为头指针的设备链表来集体管理所有网络设备,

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

该设备链表中的每个元素代表一个网络设备接口。数据结构net_device在前面的小结中已经介绍,这里就不再重复。

图8.4 Linux网络驱动体系结构

8.2.2.3网络驱动程序的主要功能

网络驱动程序主要完成系统的初始化、数据包的发送和接收。网络设备的初始化主要由net_device数据结构中的init函数指针所指向的初始化函数来完成,当内核启动或加载网络驱动模块的时候,就会调用这个初始化函数。在初始化函数中通过检测物理设备的硬件特征来侦测网络物理设备是否存在,然后再对设备进行资源配置,接下来构造设备的net_device数据结构,并用检测到的数据对net_device中的变量初始化,最后向Linux内核注册该设备并申请内存空间。数据包的发送和接收是实现Linux网络驱动程序中两个最关键的过程,对这两个过程处理的好坏将直接影响到驱动程序的整体运行质量。在网络设备驱动加载时,通过net_device域中的init函数指针调用网络设备的初始化函数对设备进行初始化,如果操作成功再通过net_device域中的open函数指针调用网络设备的打开函数打开设备,并通过net_device域中建立硬件包头函数指针hard_header来建立硬件包头信息。最后通过协议接口层函数dev_queue_xmit(参见<net/core/dev.c>文件)来调用net_device域中的hard_start_xmit函数指针完成数据包的发送。该函数把存放在套接字缓冲区中的数据发送到物理设备,该缓冲区是由数据结构sk_buff 来表示的。数据包的接收是通过中断机制来完成的。当有数据到

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

达时产生中断信号,网络设备驱动功能层调用中断处理程序(即数据包接收程序)来处理数据包的接收,随后网络协议接口层调用netif_rx(参见<net/core/dev.c>文件)函数,把接收到的数据包传输到网络协议的上层进行处理。(www.61k.com]

8.2.3 CS8900A适配器驱动程序分析

CS8900A适配器驱动程序在Linux内核中已经提供,我们使用内核2.6.10版本中的代码CS8900A适配器驱动程序为分析对象,讲述网络驱动程序的开发过程。首先来分析一下CS8900A模块的初始化。

8.2.3.1初始化

CS8900A适配器的初始化主要由net_device数据结构中的init函数指针所指向的初始化函数来完成,当内核启动或加载网络驱动模块的时候,就会调用这个初始化函数。在初始化函数中通过检测物理设备的硬件特征来侦测网络物理设备是否存在,然后再对设备进行资源配置,接下来构造设备的device数据结构,并用检测到的数据对net_device中的变量初始化,最后向Linux内核注册该设备并申请内存空间。

首先,定义一个全局变量cs8900_dev,它为net_device结构的对象,并且定义了cs8900_dev对象的初始化函数为cs8900_probe,代码如下:

static struct net_device cs8900_dev =

{

init: cs8900_probe

};

接下来,分析cs8900_probe函数的具体实现,它主要用于对网卡进行检测,并初始化系统中网络设备信息用于后面的网络数据的发送和接收。它的完整代码实现如下: 1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18 int __init cs8900_probe (struct net_device *dev) { static cs8900_t priv; int i,result; u16 value; printk (VERSION_STRING"\n"); memset (&priv,0,sizeof (cs8900_t)); ether_setup (dev); dev->open = cs8900_open; dev->stop = cs8900_stop; dev->hard_start_xmit = cs8900_send_start; dev->get_stats = cs8900_get_stats; dev->set_multicast_list = cs8900_set_receive_mode; dev->tx_timeout = cs8900_transmit_timeout; dev->watchdog_timeo = HZ; #if defined(CONFIG_ARCH_SMDK2410) dev->dev_addr[0] = 0x08;

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61

62 dev->dev_addr[1] = 0x00; dev->dev_addr[2] = 0x3e; dev->dev_addr[3] = 0x26; dev->dev_addr[4] = 0x0a; dev->dev_addr[5] = 0x5b; #else dev->dev_addr[0] = 0x00; dev->dev_addr[1] = 0x12; dev->dev_addr[2] = 0x34; dev->dev_addr[3] = 0x56; dev->dev_addr[4] = 0x78; dev->dev_addr[5] = 0x9a; #endif dev->if_port = IF_PORT_10BASET; dev->priv = (void *) &priv; spin_lock_init(&priv.lock); #if defined(CONFIG_ARCH_SMDK2410) dev->base_addr = vSMDK2410_ETH_IO + 0x300; dev->irq = SMDK2410_ETH_IRQ; #endif /* #if defined(CONFIG_ARCH_SMDK2410) */ if ((result = check_mem_region (dev->base_addr, 16))) { return (result); } request_mem_region (dev->base_addr, 16, dev->name); /* verify EISA registration number for Cirrus Logic */ if ((value = cs8900_read (dev,PP_ProductID)) != EISA_REG_CODE) { return (-ENXIO); } /* verify chip version */ value = cs8900_read (dev,PP_ProductID + 2); if (VERSION (value) != CS8900A) { return (-ENXIO); } /* setup interrupt number */ cs8900_write (dev,PP_IntNum,0); result = cs8900_eeprom (dev); if (result == -ENODEV) { /* no eeprom or invalid config block, configure MAC address by hand */ for (i = 0; i < ETH_ALEN; i += 2) cs8900_write (dev,PP_IA + i,dev->dev_addr[i] | (dev->dev_addr[i + 1] << 8)); printk (", no eeprom "); } else if( result == -EFAULT)

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78 } { printk (", eeprom (invalid config block)"); } else { printk (", eeprom ok"); } printk (", addr:"); for (i = 0; i < ETH_ALEN; i += 2) { u16 mac = cs8900_read (dev,PP_IA + i); printk ("%c%02X:%2X", (i==0)?' ':':', mac & 0xff, (mac >> 8)); } printk ("\n"); return (0);

接下来对上述代码进行逐行分析,第1行cs8900_t结构是专门定义的一个用于描述网络设备私有信息的结构,其结构包含的成员有:网络设备的统计信息结构成员(stats),传输数据长度成员(txlen),该网络设备包含字符设备的个数(char_devnum)成员以及自旋锁(lock)成员。[www.61k.com)第9行用ether_setup函数给网络设备填充以太网相关的值,如MTU值、地址长度、以太网类型等信息。第10-15行,用来初始化网络设备的方法,包括:open, stop, hard_start_xmit等。由于我们的程序是基于SMDK2410开发板的,所以内核中会配置CONFIG_ARCH_SMDK2410这个选项,在以下关于这个选项的条件编译代码中,都会执行到它包含的代码,第18-23行,用来定义网络设备的MAC地址,也就是硬件地址(48位长度)。第32行,定义网络设备的接口介质类型为10BaseT*(注3)。第33行,将cs8900_t结构对象的地址赋给网络设备的私有数据成员priv。第34行,以cs8900_t结构对象的自选锁成员lock为参数,传递给宏spin_lock_init,用于动态初始化自选锁。第36-37行,分别定义以太网设备I/O地址和中断号。第39-41行,用来检测网络设备I/O地址空间是否可用。第42行,如果该网络设备I/O地址可用,则注册这块区域。第44-46行,用于检查EISA*(注4)是否正确。第48-51行,用于验证CS8900A芯片的版本号是否正确。第53行,用来设置网路设备的中断号。第54-69行,用来确定网络设备是否有EEPROM*(注5)。由于我们的系统中没有EEPROM设备和其他的配置块,所以需要手动配置MAC地址。第71-75行,用来读取MAC地址。第77行,一切都正确的情况下返回0,表示网络设备接口初始化完成。

*注3:10BaseT-----原始IEEE802.3标准的一部分,1OBaseT是1OMb/s基带以太网规范,它使用两对双绞电缆(3类、4类或5类),一对用于发送数据另一对用于接收数据。1OBaseT每段的距离限制大约为100米。

*注4:EISA: Extended Industry Standard Architecture,即扩充的工业标准体系结构,一种总线标准,用于连接插入到PC主板上的插件,如显示卡、内置调制解调器、声霸卡、驱动控制器以及支持其他外设的插卡。EISA有32位的数据通路,并且使用的连接器可以连接ISA插件,但EISA卡只兼容EISA系统。EISA操作频率和数据吞吐量都远远高于ISA总线。

*注5:EEPROM: Electrically Erasable Programmable ROM,即电可擦可编程只读存储器,为一种将资料写入后即使在电源关闭的情况下,也可以保留一段相当长的时间,且写入资料时不需要另外提高电压,只要写入某一些句柄,就可以把资料写入内存中了。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

到这里为止,完整讲述了网络设备接口的初始化函数cs8900_probe,那么有人会问谁会调用cs8900_probe函数呢?其实前面已经说过,当在内核启动或加载网络驱动模块的时候,就会调用这个初始化函数,这里我们是以模块加载形式来调用的,它的加载函数为cs8900_init,具体实现代码如下:

static int __init cs8900_init (void)

{

strcpy(cs8900_dev.name, "eth%d");

return (register_netdev (&cs8900_dev));

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

}

该加载函数实现的功能是:首先给网络设备取名,然后传递参数cs8900_dev的地址到网络设备注册函数register_netdev,从而完成注册网络设备的任务。(www.61k.com]

介绍了网络设备的加载,顺便看一下它的卸载函数,它的作用与加载函数相反,实现代码如下:

static void __exit cs8900_cleanup (void)

{

cs8900_t *priv = (cs8900_t *) cs8900_dev.priv;

if( priv->char_devnum)

{

unregister_chrdev(priv->char_devnum,"cs8900_eeprom");

}

release_mem_region (cs8900_dev.base_addr,16);

unregister_netdev (&cs8900_dev);

}

该卸载函数实现的功能是:首先判断网络设备中是否定义了字符设备EEPROM,如果定义了,则首先卸载它。实际在我们的系统中没有使用EEPROM,所以就不用执行注销字符设备。然后,释放网络设备I/O地址空间。最后,注销网络设备cs8900_dev。

8.2.3.2 open和stop方法

网络设备的open方法,就是激活网络接口,使它能接收来自网络的数据并且传递到网络协议栈的上层,也可以将数据发送到网络上。cs8900_dev设备的open方法实现代码如下: 1

2

3

4

5

6

7

8

9

10

11

12 static int cs8900_open (struct net_device *dev) { int result; #if defined(CONFIG_ARCH_SMDK2410) set_irq_type(dev->irq, IRQT_RISING); /* enable the ethernet controller */ cs8900_set (dev,PP_RxCFG,RxOKiE | BufferCRC | CRCerroriE | RuntiE | ExtradataiE); cs8900_set (dev,PP_RxCTL,RxOKA | IndividualA | BroadcastA); cs8900_set (dev,PP_TxCFG,TxOKiE | Out_of_windowiE | JabberiE); cs8900_set (dev,PP_BufCFG,Rdy4TxiE | RxMissiE | TxUnderruniE

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48 | TxColOvfiE | MissOvfloiE); cs8900_set (dev,PP_LineCTL,SerRxON | SerTxON); cs8900_set (dev,PP_BusCTL,EnableRQ); #ifdef FULL_DUPLEX cs8900_set (dev,PP_TestCTL,FDX); #endif /* #ifdef FULL_DUPLEX */ udelay(200); /* install interrupt handler */ if ((result = request_irq (dev->irq, &cs8900_interrupt, 0, dev->name, dev)) < 0) { return (result); } #else /* install interrupt handler */ if ((result = request_irq (dev->irq, &cs8900_interrupt, 0, dev->name, dev)) < 0) { return (result); } set_irq_type(dev->irq, IRQT_RISING); /* enable the ethernet controller */ cs8900_set (dev,PP_RxCFG,RxOKiE | BufferCRC | CRCerroriE | RuntiE | ExtradataiE); cs8900_set (dev,PP_RxCTL,RxOKA | IndividualA | BroadcastA); cs8900_set (dev,PP_TxCFG,TxOKiE | Out_of_windowiE | JabberiE); cs8900_set (dev,PP_BufCFG,Rdy4TxiE | RxMissiE | TxUnderruniE | TxColOvfiE | MissOvfloiE); cs8900_set (dev,PP_LineCTL,SerRxON | SerTxON); cs8900_set (dev,PP_BusCTL,EnableRQ); #ifdef FULL_DUPLEX cs8900_set (dev,PP_TestCTL,FDX); #endif /* #ifdef FULL_DUPLEX */ #endif /* #if defined(CONFIG_ARCH_SMDK2410) */ /* start the queue */ netif_start_queue (dev); return (0); }

现在来分析上述代码,第6行,设定网络设备的中断触发类型为上升沿触发。[www.61k.com]第8-17行,配置以太网控制寄存器,包括:接收总线配置寄存器PP_RxCFG,接收控制寄存器PP_RxCTL,发送配置寄存器PP_TxCFG,缓冲配置寄存器PP_BufCFG,线控制寄存器PP_LineCTL,总线控制寄存器PP_BusCTL和测试控制寄存器PP_TestCTL,分别介绍一下这几个寄存器的作用:

l PP_RxCFG:用来确定如何传输帧到主机和哪种类型帧被触发中断。

l PP_RxCTL:位8,C,D,和E定义接收什么样的帧。位6,7,9,A和B配置目

的地址过滤器。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

l PP_TxCFG:用来配置发送数据相关的中断是否可用。(www.61k.com)

l PP_BufCFG:用来配置总线缓冲相关的中断是否可用。

l PP_LineCTL:用来配置MAC引擎和物理接口。

l PP_TestCTL:用来控制ISA总线接口操作。

继续分析上述代码,第21-24行,注册网络设备的中断服务程序cs8900_interrupt,后面会详细讲述这个中断服务程序。如果中断服务程序注册成功将执行第46行,用来启动一个网络接口队列,最后返回一个0代表网络设备被正确打开。

接下来讲述stop方法,即停止网络设备,它的作用与open方法相反,具体实现代码如下: 1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17 static int cs8900_stop (struct net_device *dev) { /* disable ethernet controller */ cs8900_write (dev,PP_BusCTL,0); cs8900_write (dev,PP_TestCTL,0); cs8900_write (dev,PP_SelfCTL,0); cs8900_write (dev,PP_LineCTL,0); cs8900_write (dev,PP_BufCFG,0); cs8900_write (dev,PP_TxCFG,0); cs8900_write (dev,PP_RxCTL,0); cs8900_write (dev,PP_RxCFG,0); /* uninstall interrupt handler */ free_irq (dev->irq,dev); /* stop the queue */ netif_stop_queue (dev); return (0); }

第4-11行,用来禁止以太网控制器各相应的寄存器。第13行,释放网络设备的中断服务程序。第15行,停止网络设备接口队列。最后返回0表示完成停止设备。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

8.2.3.3数据发送

网络设备数据发送是由net_device结构中的hard_start_xmit方法实现的,所有的网络设备驱动程序都必须有这个发送方法。在驱动程序层次中,发送和接收数据都是通过系统低层对硬件的读写来完成的。根据初始化函数cs8900_probe定义网络设备的发送实现函数为cs8900_send_start,首先来看一下cs8900_send_start函数的实现代码:

1

2

3

4

5

6

7

8

9 static int cs8900_send_start (struct sk_buff *skb,struct net_device *dev) { cs8900_t *priv = (cs8900_t *) dev->priv; u16 status; spin_lock_irq(&priv->lock); netif_stop_queue (dev); cs8900_write (dev,PP_TxCMD,TxStart (After5)); cs8900_write (dev,PP_TxLength,skb->len);

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30 } status = cs8900_read (dev,PP_BusST); if ((status & TxBidErr)) { spin_unlock_irq(&priv->lock); priv->stats.tx_errors++; priv->stats.tx_aborted_errors++; priv->txlen = 0; return (1); } if (!(status & Rdy4TxNOW)) { spin_unlock_irq(&priv->lock); priv->stats.tx_errors++; priv->txlen = 0; return (1); } cs8900_frame_write (dev,skb); spin_unlock_irq(&priv->lock); dev->trans_start = jiffies; dev_kfree_skb (skb); priv->txlen = skb->len; return (0);

下面来分析上述代码,在系统调用驱动程序的hard_start_xmit方法时,发送的数据放在一个sk_buff结构中,也就是前面讲过的套接字缓冲结构。[www.61k.com]第6行,获得一个禁止中断的自旋锁。第7行,关闭网络接口队列。第8-9行,主机在发送数据前写PP_TxCMD这个寄存器,利用这个命令告诉CS8900A主机有一个数据帧要传输,并且告诉如何传输这个帧。此外,写PP_TxLength寄存器用来告诉将要传输数据帧的长度。第10行,读取PP_BusST寄存器来获得当前传输操作的状态。第11-17行,数据传输失败由于数据帧的大小不符合要求,比如数据帧大于1518字节。于是释放自旋锁,设置数据传输长度为0,返回1。第18-23,数据传输失败由于传输缓冲空间没有准备好,此时释放自旋锁,设置数据传输长度为0,返回1。第24-29行,如果传输数据的状态都正常,此时将skb指向的数据帧发送给CS8900A。然后释放自旋锁并且释放skb指向的内存空间,最后返回0表示传输数据正确。

8.2.3.4数据接收 数据包的接收实现方式不同于数据发送利用hard_start_xmit方法实现,而它是通过中断机制来完成的。当有数据到达时产生中断信号,网络设备驱动功能层调用中断处理程序(即数据包接收程序)来处理数据包的接收,随后网络协议接口层调用netif_rx函数把接收到的数据包传输到网络协议的上层进行处理。CS8900A的中断服务程序在讲述open方法时已经提到,它是利用cs8900_interrupt函数来实现的,首先来看一下它的具体实现代码: 1

2

3

4

5 static irqreturn_t cs8900_interrupt (int irq,void *id,struct pt_regs *regs) { struct net_device *dev = (struct net_device *) id; cs8900_t *priv; volatile u16 status;

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

6 irqreturn_t handled = 0;

7

8 if (dev->priv == NULL) {

9 printk (KERN_WARNING "%s: irq %d for unknown device.\n",dev->name,irq);

10 return 0;

11 }

12 priv = (cs8900_t *) dev->priv;

13 while ((status = cs8900_read (dev, PP_ISQ))) {

14 handled = 1;

15 switch (RegNum (status)) {

16 case RxEvent:

17 cs8900_receive (dev);

18 break;

19 case TxEvent:

20 priv->stats.collisions += ColCount (cs8900_read (dev,PP_TxCOL)); 21 if (!(RegContent (status) & TxOK)) {

22 priv->stats.tx_errors++;

23 if ((RegContent (status) & Out_of_window)) priv->stats.tx_window_errors++;

24 if ((RegContent (status) & Jabber)) priv->stats.tx_aborted_errors++; 25 break;

26 } else if (priv->txlen) {

27 priv->stats.tx_packets++;

28 priv->stats.tx_bytes += priv->txlen;

29 }

30 priv->txlen = 0;

31 netif_wake_queue (dev);

32 break;

33 case BufEvent:

34 if ((RegContent (status) & RxMiss)) {

35 u16 missed = MissCount (cs8900_read (dev,PP_RxMISS)); 36 priv->stats.rx_errors += missed;

37 priv->stats.rx_missed_errors += missed;

38 }

39 if ((RegContent (status) & TxUnderrun)) {

40 priv->stats.tx_errors++;

41 priv->stats.tx_fifo_errors++;

42 priv->txlen = 0;

43 netif_wake_queue (dev);

44 }

45 break;

46 case TxCOL:

47 priv->stats.collisions += ColCount (cs8900_read (dev,PP_TxCOL));

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

48

49

50

51

52

53

54

55

56

57 } break; case RxMISS: status = MissCount (cs8900_read (dev,PP_RxMISS)); priv->stats.rx_errors += status; priv->stats.rx_missed_errors += status; break; } } return IRQ_RETVAL(handled);

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

接下来分析一下上述代码,当触发一个中断时,首先通过读取PP_ISQ寄存器来判断产生中断的类型,这是在第13行代码实现。(www.61k.com]第15-54行,根据获得的中断类型来执行相应的处理,其中有五种中断类型,按优先级顺序分别为:RxEvent,TxEvent,BufEvent,RxMISS,TxCOL。每次主控制器读ISQ(Interrupt Status Queue)寄存器,相应寄存器中产生中断的位将清零,下一个中断报告将会自动移到中断队列最前面。数据接收的核心功能是在RxEvent中断类型的处理程序中由cs8900_receive函数来完成。这里专门讲述一下cs8900_receive函数,它实现了网络数据接收的核心功能。cs8900_receive函数的实现代码如下: 1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27 static void cs8900_receive (struct net_device *dev) { cs8900_t *priv = (cs8900_t *) dev->priv; struct sk_buff *skb; u16 status,length; status = cs8900_read (dev,PP_RxStatus); length = cs8900_read (dev,PP_RxLength); if (!(status & RxOK)) { priv->stats.rx_errors++; if ((status & (Runt | Extradata))) priv->stats.rx_length_errors++; if ((status & CRCerror)) priv->stats.rx_crc_errors++; return; } if ((skb = dev_alloc_skb (length + 4)) == NULL) { priv->stats.rx_dropped++; return; } skb->dev = dev; skb_reserve (skb,2); cs8900_frame_read (dev,skb,length); #ifdef FULL_DUPLEX dump_packet (dev,skb,"recv"); #endif /* #ifdef FULL_DUPLEX */ skb->protocol = eth_type_trans (skb,dev); netif_rx (skb); dev->last_rx = jiffies;

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

28

29

30 } priv->stats.rx_packets++; priv->stats.rx_bytes += length;

接下来分析上述代码,第4行,定义一个sk_buff(套接字)结构的指针skb,用来存放要传递的数据信息。(www.61k.com]第7-14行,首先读取接收数据寄存器的状态是否正常,如果读取的状态不正确,则将这些错误信息赋给相应的统计变量,最后返回。第15-18行,给skb指针变量申请内存空间,如果没有申请成功则返回。第21行,将CS8900A的数据帧读取到skb指向的内存中。第25行,确定传输数据协议类型,比如802.3、802.2协议等。第26行,调用netif_rx()把skb中存放的数据传送给协议层,它是网络数据接收中必须使用的函数。

8.3本章小结

本章讲述了另一类设备驱动——网络设备驱动。首先介绍了网络设备驱动中的两个重要数据结构,即net_device和sk_buffer结构,然后介绍了常见的网络术语,包括基本的网络协议以及以太网。最后重点讲述了CS8900A以太网适配器驱动的开发,这里不仅讲述了网络驱动程序的概要设计,并且分析了CS8900A以太网适配器驱动程序的重要实现代码。通过本章的学习,使读者学会了另一类Linux驱动设备的开发方法,读者可以看出网络设备驱动程序开发与字符设备和块设备驱动开发的不同之处。下一章将开始讲述本书的第三部分,即Linux系统下的GUI开发——Qt。

8.4常见问题

1. 网络设备驱动实现时经常会用到哪两个数据结构?

参考答案:net_device结构和sk_buff结构是网络驱动实现时最重要的两个数据结构,net_device结构是网络设备驱动的核心,该结构用来定义网络设备。sk_buff结构是套接字缓冲结构,该结构经常用于定义网络协议之间传输的数据。

2. 网络设备驱动程序中数据接收是如何实现的?

参考答案:数据接收的实现是通过中断机制来完成的。当有数据到达时产生中断信号,网络设备驱动功能层调用中断处理程序(即数据包接收程序)来处理数据包的接收,随后网络协议接口层调用netif_rx函数把接收到的数据包传输到网络协议的上层进行处理。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

第三部分 Qt GUI开发

这部分介绍嵌入式Linux下常用的GUI编程——Qt编程。(www.61k.com)Qt是跨平台的C++ GUI应用标准框架,它不仅能运行在Linux/Unix上,而且还可以运行在微软的Windows和Mac OS等。该部分由四章内容组成,首先第9章介绍了Qt的概要知识,包括Linux桌面GUI系统,Qt/X11,Qtopia Core等,通过对本章的学习使读者对Qt及其相关知识有个大概了解。紧接着第10章讲述了Qt/X11的安装以及介绍了它的应用实例,通过对本章的学习使读者对Qt/X11有更多地了解,不但会安装Qt/X11开发环境,而且可以开发基本的Qt/X11程序。

第11章讲述Qt核心技术,重点是通过剖析Qt的源代码来深入的学习Qt的对象模型及信号和槽机制的实现,同时也详细介绍了Qt窗口系统以及国际化等常用知识,通过对本章的学习使读者对Qt的核心技术有更深入的了解,也能为以后实际工作中的Qt开发打下良好的基础。最后第12章讲述Qtopia Core,包括Qtopia Core的安装,Frame Buffer和qvfb,以及轻量级的窗口系统,移植Qt/X11程序到Qtopia Core,进程间通信等,这一章里我们重点讲述Qtopia Core和Qt/X11的一些不同之处,使读者在熟悉了Qt/X11的基础上能够很快过渡到Qtopia Core开发。

总之,目前Qt GUI开发在嵌入式设备中应用越来越广,所以这方面的人才需求也在每年递增,相信会有更多的人加入到这个行业中来,同时希望读者能从这部分内容中学到Qt开发知识的基础知识,并能很快应用于实际开发中。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

第9章 Qt概述

本章学习目标:

l 了解X Window构架

l 了解Qt在Linux GUI系统中的作用

l 了解Qt/X11和Qtopia Core

9.1 Linux下GUI介绍

术语GUI是Graphical User Interface的简写,即指图形用户接口。(www.61k.com)现代操作系统一般都提供图形化的操作界面系统,这种GUI系统一般由视窗、图标、菜单、对话框及其他一些可视特征组成,它允许终端用户可以方便地利用鼠标和键盘来操作电脑。GUI的概念是70年代由施乐公司帕洛阿尔托研究中心提出的,1984年苹果的Macintosh电脑则是首例成功使用GUI并用于商业用途的产品。之后各种GUI系统发展迅速,包括后来居上的微软的Windows系列,以及广泛应用于类Unix系统上的X Window系统(常简称为X11或者X)。我们先来了解一下Linux下的桌面GUI系统和嵌入式GUI系统,以及Qt等开发包在Linux GUI系统中所起到的作用。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

9.1.1 Linux桌面GUI系统

与多数的Unix系统类似,Linux桌面GUI系统同样是基于X Window协议构架来实现的。早在1994年第一个 Linux kernel 1.0 的版本当中,就已经有了XFree86(X Window的一个实现)的支持了。目前各种Linux的发行版本基本都采用X Window的构架,而在X Window的上层则开发了多种高级图形库、开发包及整合的桌面环境可供选择。常见的Linux GUI系统架构如图9.1所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图9.1 常见的Linux 桌面GUI系统架

从图中我们可以看到,各种流行的桌面环境和开发包实际上都是在X Window的基础上开发的,在所有的类Unix系统中,X Window几乎完全占据统治地位。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

9.1.1.1 X Window系统

X起源于1984年麻省理工学院(MIT)计算机科学研究所和Athena计划的共同研究。[www.61k.com]当时MIT的Bob Scheifler正在发展分布式系统,同一时间DEC公司的Jim Gettys也在麻省理工学院做Athena计划的一部分,两个计划都需要一个相同的东西——一套在Unix机器上的优良的视窗系统。因此他们开始合作,从斯坦福大学得到了一套叫做W的实验性视窗系统,并在基于W视窗系统的基础上开始发展,当发展到了足以和原先系统有明显区别时,他们把这个新系统叫做X。X是第一个真正的与硬件和制造商无关的窗口系统环境。

X在之后的几年内迅速发展,1987年9月第11版本即X11发行,并取得明显成功,成为早期的较大规模开源项目之一。后来X发行的版本都被称为X11的某个版本,如X11R2、X11R6等。这也是X Window系统常被简称为X11的原因。

X Window系统架构基于C/S模型,即客户机-服务器模型,主要由X Server和X Client通过X Protocol在网络上通信完成应用任务。当然,在很多情况下,X Server和X Client运行在同一主机上:启动Linux登录到一个图形界面,这时服务器和客户端同时并发的在同一主机运行,这时它们之间的通信只需利用本地的一些通信方式即可。

X Window系统架构图如图9.2所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图9.2 X Window系统架构图

X Window为GUI环境提供了基本的框架:在屏幕上绘图和移动窗口,以及与鼠标和键盘的互动等。X Server用来控制显示器和输入设备,它可以建立视窗,在视窗中画图形和文字,响应Client程序的请求 (Request),但它不会自己动作,只有在Client程序提出需求后才完成动作。而X Client就是X中的应用程序,一个X Client若想要运行,必须打开一个显示器,连接一个X Server,然后通过与X Server之间的通信来完成所有的工作。X Client主要是完成应用程序计算处理的部分,并不接受用户的输入信息,输入信息都是输入给X Server,然后由X Server以Event的形式传递给X Client。需要注意的是,X 的“客户端” 和 “服务器”与普通意义上的C/S模型中的客户端和服务器有所不同,如果在网络上使用,比如使用telnet远程登录某台计算机,并在本地显示所登录的机器的情况,则"服务器" 是使用者本地的显示,而“客户端”反而是远程的机器。

X Protocol是X Server与X Client之间的通信协议。X Client一般通过调用底层库Xlib

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

的接口来和X Server进行通信,Xlib 提供一些基本的函数如连接某个服务器、绘制窗口和响应事件等,它在 X Window System 中的角色,就好比 Windows APIs (或者说 Windows SDK) 在 Microsoft Windows 中的角色一样,是最接近窗口系统的程序设计接口。[www.61k.com)Xlib中的函数最终通过底层通信协议来实现,所采用的通信协议被定义为一种异步的双向协议,任何提供字节流通信的方式都可以使用,可以是IPC,也可以是TCP/IP等。

X并不内置于操作系统,它只是比用户层次稍高一些,在系统中它也是一个相对独立的组件,上层应用稍作移植就可以应用于各种操作系统。X的另外一个优点在于稳定性,在X Server上进行工作时,如果程序异常中断,只会影响到视窗系统,不会造成机器的损坏或操作系统核心的破坏。

9.1.1.2 GNOME/Gtk+和KDE/Qt

尽管Linux桌面GUI系统一般都基于X Window系统, 但X Window本身并不是一个直接的图形操作环境,它只是作为图形环境与UNIX系统内核沟通的中间桥梁,任何厂商都可以在X Window基础上开发出不同的GUI图形环境。上个世纪九十年代中期,以开源模式推进的Linux还没有找到一个可靠并且免费的图形界面——IBM, Sun等大厂商开发的Motif/CDE 桌面环境等价格都非常昂贵,而一些小型的免费系统还远远不够完善。

在这时,就读于图宾根大学德国人Matthias Ettrich发起了KDE(K Desktop Environment)计划,来开发一个易于使用及人性化的桌面系统,并选择了当时新推出的功能强大的Qt作为GUI开发包。很快地他和其它志愿开发人员于1997年初发布了第一个较大规模的 KDE 版本。

由于KDE本身采用GPL(GNU通用公共许可证)发布,而作为KDE底层基础的Qt却是不遵循GPL的商业软件,于是一大批自由程序员对KDE项目的决定深为不满,他们认为利用非自由软件开发违背了GPL的精神,于是在墨西哥程序员Miguel De Icaza的组织下发起了GNOME(GNU Network Object Enviroment)计划来替代KDE,并选择了使用LGPL的Gtk+来作为Qt 开发包的替代,担当GNOME桌面的基础。

KDE和GNOME在之后多年的互相竞争中迅速发展,逐渐成为了Linux下的桌面环境的两大阵营。目前 GNOME/Gtk+ 吸引的公司比较多,而KDE/Qt 则在 Office/嵌入式 环境中先走一步。

KDE和GNOME在设计上稍有差别:一般的说,KDE桌面环境更加倾向于易用性方面,提供了很多图形化的方式来进行各种操作和配置,而GNOME桌面环境设计得比较简捷,速度稍快。GNOME和KDE的桌面截图如下所示:

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图9.3 GNOME桌面示例

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图9.4 KDE桌面示例

尽管KDE和GNOME之间竞争激烈,它们之间却并非水火不相容的关系。[www.61k.com]我们可以在Linux中同时安装这两种桌面系统,并可以方便的互相切换。而且从2003年以来,KDE与GNOME阵营开始逐渐相互支持对方的程序——只要你在KDE环境中安装GTK库,便可以运行GNOME的程序,反之亦然。经过几年多的努力,KDE和GNOME都已经实现高度的互操作性,两大平台的程序都是完全共享的。

作为GNOME所使用的开发包,Gtk+最初只是GIMP(GNU Image Manipulation Program, GNU图像处理程序)的专用开发库,后来随着GNOME的发展而逐渐成为Linux下开发图形界面的应用程序的主流开发工具之一。Gtk+是自由软件,并且是GNU工程的一部分,采用LGPL协议发布。

Gtk 是在 GDK (GIMP Drawing Kit) 和 gdk-pixbuf 的基础上建立起来的,GDK 基本上是对访问窗口的底层函数 (在 X 窗口系统中是 Xlib) 的一层封装,而gdk-pixbuf 则是一个用于客户端图像处理的库。一般的,我们用Gtk代表软件包和共享库,用Gtk+代表Gtk的

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图形构件集。(www.61k.com]Gtk+图形库使用一系列称为“构件”的对象来创建应用程序的图形用户接口。它提供了许多常用的构件如窗口、标签、命令按钮、开关按钮、检查按钮、无线按钮、框架、列表框、组合框、树、列表视图、笔记本、状态条等。可以用它们来构造非常丰富的用户界面。另外,Gtk+也提供了一些独具特色的部件,譬如不包含标签而包含子部件的按钮,几乎可以在这样的按钮上放置任何窗口部件,设计者可以根据自己的需求来自己设计或组合。

在用Gtk+开发GNOME的过程中,由于实际需要,在上面的构件基础上,又开发了一些新构件。一般把这些构件称为GNOME构件(与Gtk+构件相对应)。这些构件都是Gtk+构件库的补充,它们提供了许多Gtk+构件没有的功能。从本质上来说,Gtk+构件和GNOME构件是完全类似的东西。

Gtk+使用C语言开发,但是其设计者使用了面向对象技术,基于类和回调函数 (指向函数的指针) 的思想来实现。如果想使用 C++ 来开发 GTK 应用程序,我们还可以利用一个叫gtkmm的绑定。

Qt 是由挪威 TrollTech 公司于1995年推出的一个跨平台的 C++ 图形用户界面库。基本上,Qt同X Window上的 Motif、Open Look、GTK等图形界面库和Windows平台上的 MFC、OWL、VCL、ATL是同类型的东西,但Qt具有优良的跨平台特性(支持Windows、Linux、各种UNIX、OS390和QNX等)、面向对象机制以及丰富的API,同时也可支持2D/3D渲染和OpenGL API等。如前文所述,正是这些强大的功能使得当时Matthias Ettrich发起了KDE项目时,理所当然地选择了当时新推出Qt作为GUI开发包。

我们将在8.2节详细介绍Qt在桌面系统中的应用。

9.1.2 嵌入式Linux下的GUI系统

由于嵌入式系统中硬件条件的限制,在嵌入式Linux系统中庞大臃肿的X Window不太适合,我们需要一个高性能、轻量级的GUI系统。一般的说,适合于嵌入式Linux系统的

【】GUI应该具有下面的一些特点14:

? 体积小,占用较少的Flash和RAM。安装GUI系统的时候应可以根据实际的需求对

GUI系统进行方便的裁剪和精简,以减少安装所需要的存储空间;在系统运行的时候应占用尽可能少的RAM。

? 耗用系统资源尤其是CPU的资源较少,在硬件性能受限的条件下能达到相对较快的

系统响应速度,同时减小CPU的功耗,以达到节电的效果。

? 系统独立,能适用于不同的硬件。

目前常见的面向嵌入式Linux的GUI系统主要有Qtopia Core(Qt/Embedded), Microwindows(Nano-X Window), Tiny X, 以及国内的MiniGUI等。

MicroWindows(2005年更名为)是一个基于典型客户/服务器体系结构的 GUI 系统,其主要特色在于提供了类似 X 的客户/服务器体系结构并提供了相对完善的图形功能。MicroWindows能够在没有任何操作系统或其他图形系统的支持下运行,它能对裸显示设备进行直接操作。这样,MicroWindows就显得十分小巧,便于移植到各种硬件和软件系统上。然而MicroWindows 项目的进展一直很慢,目前已基本停滞。另外它的图形引擎中也存在不少低效算法。2005年1月由于其名字与微软的Windows商标相冲突,MicroWindows更名为Nano-X Window,但之后也不再有新的版本发布。

Tiny 实际上是XFree86 Project 的一部分,由SuSE公司所赞助,XFree86 Project 的核心成员之一Keith Packard开发,其目标是可以在小内存或几乎无内存的情况下良好运行。目前Tiny X是XFree86自带的编译模式之一,只要通过修改编译

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

选项,就能编译生成Tiny X。[www.61k.com]Tiny X在XFree86的基础上精简了不少东西,在x86 CPU中体积可以减小到1M以下,以适用于嵌入式环境之中。Tiny X的最大优点在于可以方便的移植桌面版本的基于X的软件到嵌入式系统中,不过这个优点有时也会变成缺点,因为从桌面版本移植过去的软件相对于嵌入式环境来说,一般体积都过大,需要一定的简化,这种简化有时还不如开发新的程序来得方便。

MiniGUI()是原清华大学教师魏永明先生所主持开发的一个自由软件项目,旨在为基于 Linux 的实时嵌入式系统提供一个轻量级的图形用户界面支持系统。MiniGUI于1999 年初遵循 GPL 条款发布了第一个版本,目前在国内已广泛应用于手持信息终端、机顶盒、工业控制系统及工业仪表、便携式多媒体播放机、查询终端等产品和领域,可在 Linux/uClinux、VxWorks、uC/OS-II、pSOS、ThreadX、Nucleus等操作系统以及 Win32 平台上运行,并能支持Intel x86、ARM(ARM7/ARM9/StrongARM/xScale)、PowerPC、MIPS、M68K(DragonBall/ColdFire)等硬件平台。MiniGUI的开发建立在比较成熟的图形引擎如Svgalib和LibGGI之上,主要着重于窗口系统、图形接口的开发,面向中低端的嵌入式产品市场。另外由于MiniGUI是中国人自己开发的GUI系统,它对于中文的支持非常好。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

发布的面向嵌入式系统的 Qt 版本。Qtopia Core是TrollTech()

它的前身是Qt/Embedded(常简称为Qt/E)。与桌面版本Qt/X11不同的是 ,Qtopia Core直接取代了X Server 及 X Library 等角色,仅采用Framebuffer作为底层图形接口,从而大大减少了系统开销。因为 Qt 是 KDE 等项目使用的 GUI 支持库,所以有许多基于 Qt 的 X Window 程序可以非常方便地移植到Qt/E 版本上。Qtopia Core延续了Qt在X上的强大功能,但相对消耗系统资源也比较多,一般用于手持式高端信息产品。我们将在8.3节详细介绍Qtopia Core。

9.2 Qt/X11介绍

上一节中我们以较大的篇幅介绍了X Window系统以及GNOME/Gtk+和KDE/Qt,目的是希望读者对Qt在整个Linux GUI系统中的位置和作用有一个全局意义上的了解。下面的两节我们来详细介绍一下Qt在Unix/Linux系统中的对应版本Qt/X11和嵌入式中的版本Qtopia Core(Qt/E)。

9.2.1 Qt的历史和Qt/X11的由来

早在1991年,Qt的两位创始人Haavard Nord (目前Trolltech公司的CEO)和Eirik Chambe-Eng (目前Trolltech公司的总裁)就已经开始着手开发Qt。据说之所以命名为Qt是因为”Q”这个字母在Haavard的Emacs(Linux/Unix上的一个著名的编辑器)的字体中看起来非常漂亮,而”t”则是受到当时的”Xt”(X Toolkit)的启发,代表”toolkit”(工具包)的意思。到1994年的时候Qt已基本成型,于是他们成立了Trolltech公司(在中国现在一般成为“奇趣”),开始用Qt来开发应用软件。次年的5月Qt 0.90版本公开发布,而在这之后由于作为开发KDE的基础而随着KDE的发展得以广泛应用。

作为一个跨平台的GUI开发包,Qt发行了不同的版本以支持各种流行的操作系统,如微软的Windows系列,基于X Window(简称为X11或X)的各种类Unix系统,苹果的Mac OS X及带有Framebuffer支持的嵌入式Linux系统等。其中支持基于X11的类Unix系统的版本我们常简称为Qt/X11。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

9.2.2 Qt/X11的版权问题

Qt/X11作为开发KDE的基础,因为KDE本身采用GPL协议,而Qt却在一定程度上是一种商业软件,其版权问题一直倍受关注。(www.61k.com]如8.1.1.2 中所述,一些不满Qt版权的程序员开发了GNOME,在后来GNOME和KDE之间的竞争中, Trolltech 公司被迫数次修改Qt的版权,直到2000年10月遵循GPL发布了Qt自由版用于X11,彻底解决了屡被攻击的版权问题。因此目前的Qt/X11即Qt的自由版,在Q公共许可证(Q Public License)和GPL下是可以免费使用的,我们可以到Trolltech 公司的官方网站http://www.trolltech.com去下载Qt/X11的安装包。

9.2.3 Qt/X11及Qt/Windows的系统架构图对比

Qt优良的跨平台特性是其得以广泛应用的重要原因之一。与Java图形界面的自成一体不同,Qt通过调用操作系统底层与绘制图形界面相关的API来绘图,因而用Qt开发的应用程序在不同的操作系统下看起来会有所区别——它们都非常贴近各自操作系统的图形界面风格。

下面的图9.5给出了Qt/X11及Qt/Windows版本在各自对应的操作系统之上的系统架构图:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图9.5 Qt/X11及Qt/Windows在各自对应的操作系统之上的系统架构图对比

从图中可以看出,对于Qt针对不同操作系统发布的不同版本,它们所定义的提供给开发应用程序的开发人员的API其实是相同的,在应用程序开发人员看来,他们不必关心当前的操作系统是哪一种,只需要调用同一套API来实现他们的应用即可。用Qt/X11开发的应用只需要在Windows下重新用Qt/Windows版本编译,即可顺利运行于Windows系统中,这种优良的跨平台特性使得跨平台的应用开发显得非常方便。

9.2.4 Qt的特性简介

下面我们简单的介绍一些Qt的特性。由于Qt的不同版本提供同样的API,这些特性基本上不仅在Qt/X11的版本上存在,在其他版本上也都是存在的,对于在下一节我们要介绍的Qtopia Core(Qt/E)也同样适用。

Linux/Unix下的Qt/X11通过调用Xlib来与X Server通信,并在此基础上开发了一整套

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

窗口部件(Widget)。(www.61k.com]这些窗口部件组合起来可用于创建用户界面的可视元素,如按钮,菜单,滚动条,消息框和应用程序窗口等都是窗口部件的实例。所有的窗口部件本身也是容器,一个窗口部件可包含任意数量的子部件,子部件在父部件的区域内显示,没有父部件的部件是顶级部件(比如一个窗口)。父部件和子部件之间形成一种树形结构以便于维护——如果父部件被停用,隐藏或删除,同样的动作会递归地应用于它的所有子部件。Qt提供的这套窗口部件库内容相当丰富,很多时候只需简单的继承已存在的Qt部件,稍作修改即可满足开发者的需求。

还在1992年的时候,Qt的两位创始人之一Eirik想到了一种后来被称为“信号与槽”(Signals and Slots)的技术来方便对象之间的通信,这种简单而强大的技术在后来还被一些其他的开发包所采用。信号与槽的技术主要用来代替传统的不安全的回调技术。在我们所熟知的很多GUI工具包中,窗口部件(Widget)都有一个回调函数用于响应它们能触发的每个动作,这个回调函数通常是一个指向某个函数的指针。采用回调函数的方式不是类型安全的,因为我们无法确定处理函数是否使用了正确的参数来调用回调函数,有些时候这种错误可能导致整个进程的退出。在Qt中,信号和槽取代了这些凌乱的函数指针,信号和槽能携带任意数量和任意类型的参数,他们是类型完全安全的,并且信号和槽之间的耦合比较松散,可以使得我们编写这些通信程序更为简洁明了,是一种比较适合面向对象开发的通信机制。另外除了信号和槽之外,Qt也提供传统的事件模型用来处理诸如鼠标点击、击键等动作,作为信号和槽的补充。

Qt中还提供了一个叫Qt设计器(Qt Designer)的工具来帮助开发人员进行快速的应用程序开发。Qt设计器提供了方便和强大的图形化布局能力,可使用户界面设计变得十分简单。你可以采用图形化的方式来定制你的程序界面,各种控件可以在Qt设计器方便的移动或者缩放。Qt设计器还包含了一个代码编辑器,并支持信号和槽机制以使部件间能够进行有效的通信,你可以在合成的代码里面嵌入自己定制的槽来响应某种输入或者变化。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

Qt普遍使用Unicode并提供一系列的工具和机制来支持国际化。Qt提供了采用Unicode实现的QString类来存储用户可见的文本,并提供了Qt Linguist等工具用来协助翻译。应用程序员可以在代码中用QObject::tr()方法来处理所有需要多语言翻译的文本,翻译工作者针对不同的语言提供不同的翻译后的资源包,Qt程序在运行的时候即可装载不同的资源包并自动寻找对应的翻译之后的文本来进行显示,从而支持不同的语言,这种方法既能支持方便的语言切换,也使得开发人员和翻译人员的工作可以很好的区分开来。

9.3 Qtopia Core 介绍

Qtopia Core是Trolltech公司在Qt/Embedded的基础上,于2006年1月推出一款基于嵌入式Linux的面向单一应用嵌入式产品的开发平台。Qtopia Core采用与桌面版本同样的一套API,但在其内部实现上作了很多调整和优化来适用硬件有所限制的嵌入式平台。

9.3.1 Qtopia Core与Qt/Embedded

Qt/Embedded是Trolltech公司开发的面向嵌入式系统的Qt版本,最早在2000年11月发布了第一个Qt/E版本,而Qtopia是最初是构建于Qt/E之上的类似桌面系统的应用环境,包括了PDA和手机等掌上系统常见的功能如电话簿、图像浏览、Media播放器、日程表等。最初Qtopia和Qt/E是不同的两套程序——Qt/E是基础类库,Qtopia是构建于Qt/E之上的一系列应用程序。但后来Trolltech调整了其产品策略,从版本4.1开始将Qt/E并入了Qtopia,

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

改称为Qtopia Core,作为嵌入式版本的核心,并在此基础上开发了面向于PDA、手机等的版本,称为Qtopia Phone Edition和 Qtopia PDA Edition等。[www.61k.com]

9.3.2 Qtopia Core的体系结构

Qtopia Core与Qt/X11最大的区别在于Qtopia Core不依赖于X Server或者Xlib,而是直接访问帧缓存(Frame Buffer),只需要一个Qtopia Core的动态共享库就足以替代X server、Xlib库和其他嵌入式解决方案的图形工具包,这样做最显著的效果是减少了内存消耗。

图9.6 给出了Qt/X11与Qtopia Core系统架构图的对比:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图9.6 Qt/X11与Qtopia Core系统架构图的对比

可以说,从效率上来看,Qtopia Core是Qt/X11的精简版本,它去掉了一些X Server中需要消耗大量系统资源的特性;而从整个体系结构上来看,Qtopia Core包含的内容其实要更多一些,因为它需要实现直接调用Frame Buffer来实现图形的绘制。

由此我们也可以看到嵌入式系统的一个特点,即很多情况下牺牲一些灵活性来换取效率的提高。与Qt/X11的结构相比,Qtopia Core直接操作Frame Buffer的方式无疑会X架构的灵活性,但在嵌入式系统中,这种牺牲灵活性以换来效率提高的方法往往是必须的,也是值得的。 9.3.2.1 Frame Buffer(帧缓存)简介

图9.6中我们提到的Frame Buffer实际上是一种对图形硬件设备的抽象。Frame Buffer是在Linux内核版本2.2以后推出的标准显示设备驱动接口,它将显示设备抽象为帧缓冲区,应用程序可以通过一组定义好的接口来操作显示设备,从而将底层的硬件细节隐藏起来。

从用户的角度看,Frame Buffer设备与/dev 下的其他设备并无二致,它也是一种的字符设备。按照惯例,Frame Buffer设备一般采用下面的节点:

0 = /dev/fb0 First frame buffer

1 = /dev/fb1 Second frame buffer

... 【13】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

31 = /dev/fb31 32nd frame buffer

缺省情况下,应用程序(例如X Server)使用/dev/fb0作为Frame Buffer设备。[www.61k.com]用户可以通过设定环境变量$FRAMEBUFFER来指定使用其他设备作为Frame Buffer设备。

从程序员的角度来看,Frame Buffer还是一种存储设备,可以对它进行读、写等操作,通过mmap()条用可以将其映射到进程地址空间,另外还可以通过几条ioctl命令,来获得显示设备的一些固定信息(比如显示内存大小)、与显示模式相关的可变信息(比如分辨率、象素结构、每扫描线的字节宽度),以及伪彩色模式下的调色板信息等等。所有这些硬件抽象使得应用程序的开发和移植变得更为简单。

由于Qtopia Core需要Frame Buffer的支持,我们所使用的嵌入式Linux内核要求2.2或之后的版本,或者将Frame Buffer移植到老的内核版本中。

9.3.2.2 Qtopia Core的窗口系统

Qtopia Core的窗口系统仍然采用Client/Server模型,任何一个Qtopia Core的应用程序都需要运行在一个Server应用上,而Qtopia Core的应用程序本身也可以作为Server来运行。一般情况下,我们可以用一个主程序来作为Server应用,而其他的应用程序都运行在Server之上。

与X相比,Qtopia Core的窗口系统是比较轻量级的——很多X中需要由Server完成的工作都直接交给Client去完成,Server和Client之间的通信开销也大大减少了。Server进程通常会生成QWSServer类的一个对象,主要用来分配Client进程的显示区域,并产生鼠标和键盘事件等。而Client进程则通常生成QWSClient类的一个对象,负责处理与各种应用相关的逻辑。Server和Client之间的通信通过socket来实现,这种通信通常被保持在一个较低的水平以减少通信的开销——比如窗口的绘制,并不是象X那样完全由Server来完成,而是由Client进行直接操作Frame Buffer来实现,Server进程所作的仅仅是通知Client需要重绘的事件等。

9.4 本章小结

我们从X Window系统开始介绍了Linux下的GUI系统,以及Qt在其中所起的作用,这有助于我们在Qt应用开发中对整个系统有一个全局性的了解。同时我们也比较了作为桌面版本的Qt/11和嵌入式版本的Qtopia Core之间的一些差别,希望读者能够从中体会到嵌入式开发中常见的平衡哲学——牺牲一些灵活性完整性等来换取效率的提高。

9.5常见问题

1.X Window系统主要由哪几部分组成,分别有什么作用?

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

参考答案:

X Window系统主要由X Server,X Client和X Protocol三部分组成。X Server可以响应X Client的需求来绘制窗口,以及接受输入设备的输入信息并传递给X Client;X Client主要是处理应用程序本身的逻辑,接收X Server的事件并通过给X Server发送请求来绘制窗口;X Protocol是X Server与X Client之间的通信协议。

2.Qt/X11,Qtopia与Qtopia Core三者之间有什么不同?

参考答案:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

Qt/X11是基于X11架构的面向Linux/Unix操作系统的Qt版本。[www.61k.com]Qtopia Core是面向嵌入式系统的Qt版本,它并不基于X11架构,而是直接操作Frame Buffer。Qtopia是建立在Qtopia Core之上的一个面向嵌入式Linux开发的平台,它在Qtopia Core的基础上提供了一系列的应用程序集以方便进一步的开发。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

第10章 Qt/X11初步

本章学习目标:

l 了解Qt/X11的双重授权问题

l 成功搭建Qt/X11开发环境

l 学习利用qmake来协助编译Qt程序

l 了解Qt开发的基础知识

10.1 Qt/X11的安装

我们可以在Trolltech公司的主页下载或其他镜像站点免费下载Qt/X11开发包来进行安装,不过我们需要准备好Gcc等编译工具以及Xlib库等,而且由于Qt/X11的安装过程需要编译整个Qt/X11的源代码,可能需要数小时的时间。(www.61k.com]

10.1.1 Qt/X11的下载及双重授权问题的说明

Qt/X11自由版(Qt/X11 Open Source Edition, 或称为Qt/X11开放源代码版本)可以在Trolltech公司的主页下载:。另外我们也可以选择在国内速度比较快的镜像站点,比如ftp://ftp.qtopia.org.cn/mirror/ftp.trolltech.com/ 或者 。

如上一章中所述,Qt的版权曾经数次修改,目前采用双重版权的方式来授权,即分为自由版和商业版。我们所下载的Qt/X11自由版遵循Q公共许可证(THE Q PUBLIC LICENSE)和GNU通用公共许可证(GPL, GNU GENERAL PUBLIC LICENSE)而发布。在遵循QPL和GPL的条件下,我们可以免费使用Qt/X11自由版来进行应用程序开发。

GPL是由自由软件基金会(Free Software Foundation,FSF)发行的用于计算机软件的许可证,最初由FSF的发起者Richard Stallman为GNU计划而撰写。目前大多数的GNU程序和超过半数的自由软件使用此许可证。Qt所遵循的GPL版本是1991年发布的“版本2”,其详细内容请参见 或者非官方的简体中文翻译版(目前GPL v3正在讨论中并遭到了不少反对,有兴趣的读者可参考)。

QPL是Trolltech公司所发布的许可证,目前使用的1.0版本由1999年发布,用于当时的Qt2.0自由版以使其成为开放源代码的软件,并约束使用Qt2.0自由版的开发者不能用它来开发商业版本。QPL的详细内容可参见Trolltech公司的官方网站。

使用Qt/X11自由版开发的应用程序需要公开源代码及遵循GPL和QPL的一些其他约束。因此,如果用Qt开发商业软件的开发者不希望开放源代码,则可以使用Trolltech提供的Qt的商业版本,这种授权方式不再受GPL的约束。关于Qt商业版的详细情况请参见 。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

10.1.2 Qt/X11的安装详解

安装Qt/X11之前需要保证你的Linux系统下已经安装了gcc, make等编译工具,以及xlib相关的库等。(www.61k.com]如果在编译Qt的过程中出现错误,一般都是因为缺少工具或库文件,需要重新安装这些工具或库文件再继续编译Qt。

假设我们现在已经将Qt/X11自由版的压缩包qt-x11-opensource-src-4.2.2.tar.gz下载到了自己的Linux机器的某个路径下(不妨假设为/home/user_name/),下面我们就可以解压缩并进行安装了,依照机器硬件的配置,安装的时间可能需要数小时:

步骤一:解压缩tar包。 #cd /home/user_name/

#tar xzvf qt-x11-opensource-src-4.2.2.tar.gz

这样解压后会生成/home/user_name/qt-x11-opensource-src-4.2.2的目录,安装Qt所需要的文件都在这个目录下。我们还可以在这个目录下找到Qt的相关文档:在目录doc下有很多html文件,我们可以参考这份本地文档而不必每次都登录到Trolltech的网站上去查看了。

步骤二:运行配置程序

# cd qt-x11-opensource-src-4.2.2

# ./configure

这时会看到一个问你是不是同意GPL/QPL的协议的问题,回答yes就可以继续了。 configure程序可以用来配置很多Qt的安装选项,键入"./configure -help"可以看到很多配置选项以及说明。比如默认情况下,Qt的安装路径是/usr/local/Trolltech/Qt-4.2.2,这样安装的时候需要有root权限,如果我们没有root权限才可以选择将Qt安装到自己的HOME目录下,键入”./configure -prefix /home/user_name /qt/“ 则可以修改安装路径为/home/user_name /qt/。如果没有特殊要求的话,一般默认的配置就可以满足我们的要求,所以我们可以不作修改,直接运行./configure就可以了。

步骤三:编译Qt源代码

# make

上一个步骤完成后,我们可以看到在/home/user_name/qt-x11-opensource-src-4.2.2的目录下生成了一个Makefile,在步骤三里,我们利用make命令来编译Qt需要的共享库、工具以及例子等。这一步骤需要相当长的时间。

步骤四:安装Qt

# su -c "make install"

安装Qt到默认的路径/usr/local/Trolltech/Qt-4.2.2需要有root权限。如果没有的话,如步骤二中所述,需要在运行配置程序的时候修改安装路径。

步骤四完成后,我们可以在目录/usr/local/Trolltech/Qt-4.2.2看到Qt的头文件、库文件、工具及例子程序等。

步骤五:设置PATH

前面的四个步骤实际上已经大致完成了安装Qt的工作了。为了我们在平时的Qt开发中能更方便的使用Qt提供的qmake, moc等工具,我们/usr/local/Trolltech/Qt-4.2.2/bin添加到PATH变量——修改$HOME/.bash_profile或者$HOME/.profile并加入(或修改):

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

PATH=/usr/local/Trolltech/Qt-4.2.2/bin:$PATH

export PATH

修改完成后需要用source命令重新运行修改的脚本,使设置生效,如:

# source .bash_profile

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

这时候再运行 # echo $PATH

可以看到PATH中已经加入了/usr/local/Trolltech/Qt-4.2.2/bin, 我们可以尝试运行Qt自带的工具Qt助手:

# assistant

这时应该弹出如下图所示的Qt助手应用程序。(www.61k.com]Qt助手提供了丰富的帮助文档,并且有索引和查询等功能

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

,在Qt程序的开发中可以提供方便的资料查阅功能。

图10.1 Qt助手

如果上面的五个步骤都很顺利的话,则Qt的安装已经完成,下一节我们就可以尝试用Qt来写著名的Hello World程序了。

10.2 Qt下的Hello World

我们从最简单也是最著名的Hello World来学习Qt。首先创建helloworld.cpp: # mkdir helloworld

# vi helloworld.cpp helloworld.cpp的内容如下所示:

1 #include <QApplication>

2 #include <QLabel>

3

4 int main(int argc, char *argv[])

5 {

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

6 QApplication app(argc, argv);

7

8 QLabel hello("Hello world!");

9

10 hello.show();

11 return app.exec();

12 }

然后我们来试图编译它。(www.61k.com)需要注意的是一般我们不直接用gcc命令或者直接编写Makefile来编译Qt的源代码,因为Qt中有一些对C++的扩展,手工写的话会比较麻烦。通常我们利用Qt提供的qmake工具来编译Qt的源代码。

首先我们键入:

# qmake –project

可以看到qmake工具为我们自动生成了helloworld.pro文件。

接下来利用qmake自动生成Makefile, 键入:

# qmake

可以看到在当前目录生成了Makefile。可以稍微浏览一下这个Makefile,它有100多行,比较繁杂,这就是为什么我们一般不采用手工编写Makefile而利用qmake来生成的原因。

得到了Makefile就可以回到我们熟

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

悉的方式来编译了,键入:

# make

就可以生成可执行的程序helloworld了。来看看我们的第一个Qt程序吧,键入: # ./helloworld

就可以看到helloworld窗口了,调整一下大小,如下所示:

图10.2 Hello World示意图

请注意上面的编译过程。在后面的章节和实际开发中,我们都大致采用相同的顺序来编译Qt程序——从.pro到Makefile 到生成库或应用。当然很多时候我们需要手工去编写或者修改.pro文件,出现编译错误的时候还很可能需要检查所生成的Makefile,我们不能完全依赖于qmake工具。

下面我们回过头来看看helloworld.cpp中的代码。

整体来看,这就是一个我们非常熟悉的main()函数。对比控制台编程中用printf写的helloworld,差别并不是很多。这对于只学习过基础C/C++的读者应该还比较容易过渡,不像当年类似MFC之类的复杂框架,一个helloworld需要几百行代码,还找不到main()在哪里。

当然Qt的helloworld与printf写的helloworld是有些不同的,它是图形界面的程序。通常GUI应用程序和控制台程序比较明显的区别是,后者一般都是顺序执行,而前者总是进入一个循环等待,随着用户的动作而有所响应。这在我们的helloworld.cpp中主要体现在第6行和第11行。第6行生成一个QApplication对象app,第11行将控制权交给了app,进入循环等待。附带一提的是参数argc和argv,它们并不是Qt的特性,而是属于C语言的特性,用来接受命令行参数,前者是命令行变量的数量,后者是命令行变量的数组。

我们再来看看其它的代码:前两行是简单的包含所需要的头文件,头文件结尾并没有加

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

上.h,注意这是Qt版本4以来的改动,以遵循C++标准。[www.61k.com]如果读者所使用的是以前的Qt版本,是编译不过这段示例代码的。第8行和第10行用来显示"Hello world!"。类QLabel是Qt控件集中的一员,用来显示一个标签。Qt中所有控件都是QWidget的子类,它们都能够用来作为程序的主窗口。

这只是一个最简单的Qt程序。下一节我们通过一个复杂一点的小例子来初步了解Qt的一些特性。

10.3 温度转换的小例子

这一节我们通过一步一步的演示,来实现一个温度转换的小例子,希望读者朋友可以从这个小例子中初步了解一些Qt的特性及用Qt进行程序开发的大致方法。

10.3.1 背景知识

摄氏温标(Celsius)和华氏温标(Fahrenheit)是两种不同的温度测量标准,前者被大多数国家所采用,后者由于出现较早,在欧美一些国家一直沿用至今。两者之间的换算关系如下:

F = (C * 9 / 5) + 32

C = (F - 32) * 5 / 9

其中F表示华氏温度,C表示摄氏温度。

10.3.2 Quit按钮

下面我们开始真正的工作了。在这一小节中我们来搭建温度转换程序的主要结构,并创建一个Quit按钮,用来退出程序。

与前面的Helloworld程序相似,首先我们需要一个main()入口函数,并且这个函数内部也需要定义一个QApplication对象来将控制权交给Qt。剩下的工作就是如何安排我们自己的窗口,这比Helloworld要复杂一点,如果还是把所有的代码都放到main()函数中就不太好了,所以我们定义一个类ConversionScreen来绘制窗口。这样我们需要创建三个文件:

main.cpp

ConversionScreen.h

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

ConversionScreen.cpp

这样一来main.cpp变得非常简单了,只有下面短短的数行代码: 1 #include <QApplication>

2

3 #include "ConversionScreen.h"

4

5 int main(int argc, char *argv[])

6 {

7 QApplication app(argc, argv);

8 ConversionScreen screen;

9

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

10 screen.show();

11 return app.exec();

12 }

这段代码与前面的Helloworld非常类似,在这里不作重复解释了。[www.61k.com]注意唯一不同的是我们生成了一个ConversionScreen的对象,而不是一个QLabel对象了。

我们打算一步一步来完善ConversionScreen类。在这一小节里我们仅仅计划添加一个按钮用来关闭窗口,这需要用到Qt的核心特性之一——信号和槽(Signals and Slots)。我们将在下一章详细讲述信号和槽,这里先给大家一个直观的印象。 ConversionScreen.h的代码如下所示:

1 #ifndef CONVERSION_SCREEN_H

2 #define CONVERSION_SCREEN_H

3

4 #include <QWidget>

5

6 class ConversionScreen : public QWidget

7 {

8 public:

9 ConversionScreen();

10 ~ConversionScreen() {};

11

12 private:

13 void createScreen();

14 };

15

16 #endif //CONVERSION_SCREEN_H

一般地当我们创建新的窗口或者窗口部件(Widget)时,都可以从QWidget或者QWidget的某个子类继承,这样可以充分的利用Qt窗口部件集提供的许多功能。关于Qt的窗口系统我们也安排在下一章详细讲述。

ConversionScreen暂时只对外提供了一个构造函数。析构函数没有什么特殊要求,所以我们将它定义为空。私有函数createScreen()用来真正的创建窗口中的内容。

附带一提的是第1、2行和第16行。这是C/C++语言里的一种惯用法,用来防止头文件被重复包含(include)。如果不采取任何措施,在稍具规模的软件开发中很容易出现头文件被重复包含,导致变量被重复定义的编译错误。而第1、2行和16行的做法则利用 C/C++的预编译功能防止了这种现象——一旦头文件被包含了一次,则CONVERSION_SCREEN_H已经被定义,下次预编译器就会略过第1行至第16行之间的内容了。

我们在ConversionScreen.cpp中来实现createScreen()函数——生成一个”Quit”按钮,并实现关闭程序的功能,代码如下所示:

1 #include <QPushButton>

2 #include <QApplication>

3

4 #include "ConversionScreen.h"

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

5

6 ConversionScreen::ConversionScreen() : QWidget()

7 {

8 createScreen();

9 }

10

11 void ConversionScreen::createScreen()

12 {

13 QPushButton* quit = new QPushButton("Quit", this);

14

15 connect(quit, SIGNAL(clicked()), qApp, SLOT(quit()));

16 }

构造函数ConversionScreen()暂时只是简单的调用了createScreen()来创建窗口,并在初始化过程中调用了父类的构造函数QWidget()。[www.61k.com]QWidget的构造函数的声明如下:

QWidget ( QWidget * parent = 0, Qt::WindowFlags f = 0 )

我们在调用的时候两个参数都用默认值。第一个参数用来指定父部件——Qt的窗口部件之间通过一种树型结构来组合,默认值0表示因为我们希望ConversionScreen类成为顶级部件;第二个参数指定部件的风格,默认值0表示普通风格。

createScreen()函数则做了两件事:创建了一个QPushButton对象,并将它的clicked()信号连接到qApp的槽quit()。

第13行中创建QPushButton对象时我们调用的构造函数原型如下:

QPushButton ( const QString & text, QWidget * parent = 0 )

第一个参数指定按钮上显示的字符,而第二个参数则指定这个按钮的父部件,在这里我们将Quit按钮的父部件设为this,即ConversionScreen本身。

第15行我们将Quit按钮发出的clicked()信号连接到qApp的槽quit(),当用户点击这个按钮时,clicked()信号将被发射,从而槽quit()被执行,程序被关闭。这是一个简单的信号和槽如何工作的过程。以前没有接触过信号和槽的读者可能还不是很理解这种过程,没有关系,在下一章我们将详细讲述信号和槽的来龙去脉。

来看一下connect()函数。connect()函数是QObject类所提供的成员函数,而QObject类是所有Qt类的基类,所以它可以方便的在各处应用,其大致用法如下:

connect(sender, SIGNAL(signal), receiver, SLOT(slot));

其中sender和receiver是两个指向QObject对象的指针,sender发射信号signal, receiver接收到信号后则执行slot。SIGNAL和SLOT类似两个关键字,来标志信号和槽。

在我们的代码中,前两个参数比较简单,就是我们刚刚定义的quit按钮和它被点击时发出的信号clicked();后两个参数中的qApp则可能让人疑惑,它并没有在我们的类中定义,那么它是父类中定义的一个公共成员吗?我们来查查Qt的文档。

首先我们来看一下html格式的文档,在Qt安装路径或者解压缩路径下的目录doc/html中我们可以找到很多html文件,打开其中的index.html并把它加到浏览器的书签中,以后我们就可以方便的找到它了。不过在html格式的文档中,我们很难找到关于qApp的内容,因为我们不是很清楚它在哪个类中,事实上在QWidget类的文档中找不到qApp。这种时候我们可以借助一个更方便的工具——Qt提供的助手工具assistant(还记得吗?我们在Qt安装完成后还试过启动它呢),它的索引功能可以帮助我们快速找到qApp的相关说明,如图10.3所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图10.3 利用Qt助手查找qApp的相关说明

从Qt助手的说明中我们可以看到,qApp是在QApplication中的一个宏,它被定义成指向进程中唯一的QApplication对象的指针,这个指针由QCoreApplication::instance()所返回。[www.61k.com]

喜欢寻根问底的朋友可以继续追查下去:对于Qt这样的开源工具包,一个很大的好处是我们可以查看它的源代码。在我们所下载的Qt安装包中,解压缩后可以在src目录中看到它所有的源代码,来看看qApp到底是什么: # cd /home/user_name/qt-x11-opensource-src-4.2.2/src

# find . -name qapplication.h

./gui/kernel/qapplication.h

在qapplication.h的第62行可以看到qApp的定义如下:

#define qApp (static_cast<QApplication *>(QCoreApplication::instance()))

可以看到qApp是一个指向QApplication对象的指针,作为SLOT的quit()函数是QApplication的成员函数,调用它将退出Qt的循环等待,在我们的程序中相当于main.cpp中的第11行app.exec()返回,于是整个应用都退出了。

在这里我们主要是希望能够通过简单的例子来引导大家如何借助Qt 助手和源代码来学习Qt,在后面的章节中我们还会讲解一些Qt的源代码来帮助大家更加深刻的了解Qt的特性。

细心的读者可能还会对第13行有疑问:我们用new创建了一个堆上的对象,却没有用delete删除它,会不会造成内存的泄漏呢?不用担心,Qt中有一种内在机制,任何部件在自身销毁的同时,会自动销毁它所有的子部件,Qt部件之间的树型结构保证了只要顶层的部件被销毁,它下面所有的子部件即整棵树都会被自动销毁,在一定程度上避免了内存泄漏的发生,。在13行我们已经将Quit按钮的父部件设为this,这样在main函数中screen对象自动析构后,Quit按钮对象即会被销毁,并不会造成内存泄漏。在后面的示例代码中我们会看

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

到还有很多类似的情况,并且我们将在下一章详细讲述这种特性。[www.61k.com) 我们采用于Helloworld同样的方法来编译:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

# qmake –project

# qmake

# make

运行一下编译后得到的程序,如下所示:

图10.4 Quit按钮

点击Quit按钮,程序退出了。回顾一下,事实上我们只是搭了一个框架,并没有真正写几行代码,对比Helloworld,我们只是增加了一个连接——希望大家慢慢熟悉这种信号和槽的连接,在下一节中我们马上又会要用到。

10.3.3摄氏温度的显示

这一小节中我们将完成用来显示摄氏温度的滑动条,并进行简单的窗体布局。

我们来选择一个看上去与那种水银温度计尽可能接近的部件来显示摄氏温度——QSlider提供的滑动条看起来还可以(至少比按钮、文本框、菜单等要接近一点吧^_^)。同时我们还需要标志一下这个温度计的读数,简单起见我们就用一个QLabel对象来显示温度吧(还记得我们在Helloworld程序里用过的QLabel类吗)。这个标签上的读数应该随着滑动条的滑动而变化,这需要一个类似上一小节中用到的信号和槽的连接来完成。

现在我们将会有三个部件了:Quit按钮、摄氏温度滑动条以及温度标签,而且滑动条和标签需要离得比较近以方便标记读数。这样我们需要安排一下它们彼此的位置。

我们只需要改动ConversionScreen.cpp中的部分代码就可以完成这些工作,如下所示: 1 #include <QPushButton>

2 #include <QSlider>

3 #include <QLabel>

4 #include <QVBoxLayout>

5 #include <QHBoxLayout>

6 #include <QApplication>

7

8 #include "ConversionScreen.h"

9

10 ConversionScreen::ConversionScreen() : QWidget()

11 {

12 createScreen();

13 }

14

15 void ConversionScreen::createScreen()

16 {

17 QPushButton* quit = new QPushButton("Quit");

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

18

19 QSlider* slider = new QSlider(Qt::Vertical);

20 slider->setRange(0, 100);

21 slider->setValue(0);

22 slider->setTickPosition(QSlider::TicksLeft);

23

24 QLabel* label = new QLabel("0");

25

26 QHBoxLayout* cLayout = new QHBoxLayout;

27 cLayout->addWidget(label, 0, Qt::AlignRight);

28 cLayout->addWidget(slider, 0, Qt::AlignLeft);

29

30 QVBoxLayout* mainLayout = new QVBoxLayout;

31 mainLayout->addWidget(quit);

32 mainLayout->addLayout(cLayout);

33 setLayout(mainLayout);

34

35 slider->setFocus();

36

37 connect(quit, SIGNAL(clicked()), qApp, SLOT(quit()));

38 connect(slider, SIGNAL(valueChanged(int)), label, SLOT(setNum(int))); 39 }

从19到21行我们创建了一个竖向显示的滑动条,简单起见我们假设这个温度转换器的转换范围为0-100摄氏度,其默认值我们暂时设为0度。[www.61k.com)第22行设置刻度的显示在滑动条的左边。

24行我们创建一个标签来显示滚动条的读数,它的默认值也被设为0。

我们需要把滑动条和标签绑定在一起,使得这个标签看起来真的象是温度计的读数,同时我们还要安排一下原来的Quit按钮,这时我们需要借助Qt的布局管理器相关的类,主要有三种基本的布局管理器可以用来安排部件的位置:

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

l QHBoxLayout 在水平方向上从左到右(默认情况)或者从右到左布置部件. l QVBoxLayout 在竖直方向从上往下(默认情况)或者从下往上布置部件.

l QGridLayout 以网格形式布局部件.

从26行到33行我们都在安排这些部件的位置。我们首先将滚动条和标签装到一个QHBoxLayout对象中,并且设置它们的对齐方式(第27行和28)使它们看起来总是比较靠近。成员函数addWidget的定义如下:

ü void addWidget(QWidget * widget, int stretch = 0, Qt::Alignment alignment = 0)

其中第一个参数是我们要加入布局管理器的部件指针;第二个参数用来定义部件的伸展,我们采用默认值0,即不作任何伸展变化;第三个参数设置部件的对齐方式,默认情况下部件将充满整个区域,我们设定标签和滚动条分别向右和向左对齐,这样它们看起来总是在一起。

然后我们把Quit按钮和这个QHBoxLayout对象装到一个QVBoxLayout对象中,按钮在上,而我们绑定后的滚动条和标签放在下方。在32行我们采用的是addLayout()成员函数来将一个布局对象放到另一个布局对象中。在34行我们设置焦点为滚动条,这样当程序运行

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

时键盘的响应焦点在滚动条上,用向上或向下的方向键即可调节温度。[www.61k.com)

类似上一小节,我们来看一下用new生成的堆对象,我们的代码中并没有显式的调用delete来销毁它们,因为Qt提供了自动删除子部件的内在机制。不过我们在17行、19行及24行等处所创建的对象呢并没有指定父部件,而是借助addWidget和addLayout两个函数,将自动删除对象的任务暂时的交给了这些布局对象——这并不是说布局对象成为了它们的父部件,而是当布局对象被setLayout()函数设置为某个部件的布局风格时(如第33行所示),这些对象的父部件将被自动设置为这个部件。这样一来,情况就与上一小节中提到的自动销毁类似了。

我们还需要完成的一件事是使标签上的读数随着滑动条的滑动而变化,这样我们做温度转换的时候才能自由选择度数。这个功能在第38行,同样通过一个类似上一小节中用到的信号和槽的连接来完成——滑动条发生变化时,将发射valueChanged(int)信号,其变化的数值通过int型参数被传递到标签对象的setNum(int)槽,这个槽的作用就是改变标签所显示的数值。注意这个信号和槽的连接稍有不同,它向我们演示了如何在信号和槽之间传递参数。

采用与上一小节相同的方法编译,如果我们只是改动了ConversionScreen.cpp而Makefile还在的话,只需要重新运行make就可以了: # make

make工具会自动为我们找到修改了的源文件并重新编译它。

重新编译之后得到的程序看起来如下图所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图10.5 摄氏温度计

10.3.4 华氏温度的显示

我们用与上面类似的方法,用一个部件来模拟温度计的形状,用另外一个部件来显示读数,不过这一次我们不用滚动条了——我们来画一个转盘型的温度计,这可以利用Qt提供的QDial类。QDial类与QSlider虽然形状不同,其实在功能上非常类似。同时我们采用QLCDNumber类来显示温度计读数,这种模拟液晶显示的数字看起来更像仪表盘上读数。转盘和读数之间同样需要一个信号和槽的连接来使它们保持一致。

加上这个转盘温度计之后我们需要重新摆放一下这些部件了。上一小节中我们简单介绍了窗口的布局管理,这里我们需要进一步的应用。我们将采用QGridLayout来学习如何进行网格布局,将这两个温度计以及Quit按钮摆放到网格中。

为了不使createScreen()函数过于庞大,我们把它的内容分解一下,增添两个私有成员函数来创建两个温度计,同时也增加几个成员变量,这样源文件ConversionScreen.h的代码如下所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

1 #ifndef CONVERSION_SCREEN_H

2 #define CONVERSION_SCREEN_H

3

4 #include <QWidget>

5

6 class QSlider;

7 class QDial;

8 class QHBoxLayout;

9 class QVBoxLayout;

10

11 class ConversionScreen : public QWidget

12 {

13 public:

14 ConversionScreen();

15 ~ConversionScreen() {};

16

17 private:

18 void createScreen();

19 void createCel();

20 void createFah();

21

22 QSlider* slider;

23 QDial* dial;

24

25 QHBoxLayout* celLayout;

26 QVBoxLayout* fahLayout;

27

28 };

29

30 #endif //CONVERSION_SCREEN_H

我们在19行和20行增加了两个函数createCel()和createFah(),分别用来创建摄氏温度计和华氏温度计。[www.61k.com)23-26行增加了四个成员变量,分别对应上文中提到的滚动条、转盘及摄氏温度计的布局和华氏温度计的布局。

增加了成员变量之后我们在6-9行增加了所用到的Qt类的声明。稍微解释一下:如果没有这些类的声明显然是无法通过编译的,那么是否可以采用象第4行那样的包含相应的头文件的方式呢?这样做是可行的,编译或链接都不会有任何问题,得到的二进制文件也不会有任何区别,它主要的不利之处在于增加了预编译的时间及头文件之间的依赖关系。在大型项目的开发中,如果头文件之间的包含非常复杂,预编译处理所耗费的时间会相当的长,头文件之间的依赖关系也很可能导致一些潜在的风险,因此我们一般都采用6-9行的方式,如果用声明的方式足够的话,就不用直接在头文件中包含其他头文件的方式。当然,第4行的包含是不可避免的,因为ConversionScreen类是从QWidget继承来的。

来看看增加新函数之后它们的实现:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

1 #include <QPushButton>

2 #include <QSlider>

3 #include <QLabel>

4 #include <QDial>

5 #include <QLCDNumber>

6 #include <QVBoxLayout>

7 #include <QHBoxLayout>

8 #include <QGridLayout>

9 #include <QApplication>

10

11 #include "ConversionScreen.h"

12

13 ConversionScreen::ConversionScreen() : QWidget() 14 {

15 createScreen();

16 }

17

18 void ConversionScreen::createScreen()

19 {

20 QPushButton* quit = new QPushButton("Quit"); 21

22 createCel();

23 createFah();

24

25 QGridLayout *mainLayout = new QGridLayout; 26 mainLayout->addWidget(quit, 0, 0);

27 mainLayout->addLayout(celLayout, 1, 0);

28 mainLayout->addLayout(fahLayout, 1, 1);

29 mainLayout->setSpacing(40);

30 mainLayout->setMargin(40);

31 setLayout(mainLayout);

32

33 slider->setFocus();

34

35 connect(quit, SIGNAL(clicked()), qApp, SLOT(quit())); 36

37 setWindowTitle("Temperature Conversion"); 38 }

39

40 void ConversionScreen::createCel()

41 {

42 slider = new QSlider(Qt::Vertical);

43 slider->setRange(0, 100);

44 slider->setValue(0);

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

45 slider->setTickPosition(QSlider::TicksLeft);

46

47 QLabel* celLabel = new QLabel("0");

48

49 celLayout = new QHBoxLayout;

50 celLayout->addWidget(celLabel, 0, Qt::AlignRight);

51 celLayout->addWidget(slider, 0, Qt::AlignLeft);

52 celLayout->setSpacing(10);

53

54 connect(slider, SIGNAL(valueChanged(int)), celLabel, SLOT(setNum(int))); 55 }

56

57 void ConversionScreen::createFah()

58 {

59 QLCDNumber* lcdNum = new QLCDNumber(3);

60 lcdNum->setSegmentStyle(QLCDNumber::Filled);

61

62 dial = new QDial;

63 dial->setRange(32, 212);

64 dial->setValue(32);

65 dial->setNotchesVisible(true);

66

67 fahLayout = new QVBoxLayout;

68 fahLayout->addWidget(lcdNum, 0, Qt::AlignBottom | Qt::AlignHCenter); 69 fahLayout->addWidget(dial);

70 fahLayout->setSpacing(10);

71

72 connect(dial, SIGNAL(valueChanged(int)), lcdNum, SLOT(display(int))); 73 }

函数createCel()其实就是把上一小节中createScreen()函数里面的一段代码移过来并稍作修改,并增加了第52行来设置滚动条和标签之间的距离为10个象素。[www.61k.com)

函数createFah()用来创建转盘形状的华氏温度计。59行和60行我们创建了一个模拟液晶显示的数字,并设置其显示风格为向外凸出并用前景色填充。62-64行与创建滑动条的温度计时非常类似,设置范围和初始值。第58行我们将刻度显示设为真。这样我们创建并设置了所需要的两个部件对象:lcdNum和dial。在67行至70行我们将这两个部件装到一个竖向的布局容器中,同样设置布局容器内的部件之间的距离为10个象素。注意第68行我们设置对齐方式时采用了 ”Qt::AlignBottom | Qt::AlignHCenter” 来作为参数,这意味着在竖直方向上采用向底部对齐,而水平方向上则设置为居中。函数最后的第72行采用类似的连接来保证读数随转盘的指针转动而变化。

再来看看createScreen()函数。除了调用私有成员函数createCel()和createFah()来创建两个温度计之外,这个函数的主要工作还有进行整体的布局。我们采用网格布局的方式,利用QGridLayout来进行布局。网格布局时需要指定一个二维的坐标来设置布局的位置,如26

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

-28行所示,addWidget和addLayout两个函数的前两个参数就是指定二维坐标,即第几行和第几列,行和列都从0开始计数。(www.61k.com)我们把Quit按钮安排在第一行第一列,第一行的二列先空着(留给后面的另外一个按钮),第二行的第一列和第二列分别摆放两个温度计。29行和30行设置网格内的部件之间的距离以及网格外的边界大小。注意联系第52行和第70行,如果采用默认设置而不作52行和70行的设置的话,两个温度计内部的部件之间的距离也依照第29行的设置而变为40,这样就显得太松散了,读者可以试一试注释掉52行和70行的效果。createScreen()函数的最后,我们在37行设置了窗口的标题为"Temperature Conversion"。

编译后得到的窗口如图10.6所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图10.6 摄氏温度计和华氏温度计

这时候程序看起来已经很接近我们要完成的目标了,可以试着用鼠标来改变两个温度计的读数,还可以试一试利用键盘的上下键来移动滑块或者转动指针,这时旁边的读数会相应的发生变化。当然,这些都还只是准备工作,真正的温度转换工作还没有做呢。 10.3.5 华氏温度和摄氏温度之间的转换

这一小节我们来真正的完成温度转换的工作。经过前面的一系列准备工作之后,真正的温度转换只需要一些简单的换算工作了。当然,可能有的读者已经猜到了,要使两个温度计之间的读数互相随着对方的变化而变化,我们还需要用到信号和槽的连接,而且由于两个温度计的读数之间需要换算,这次我们不能只依赖于Qt提供的槽了,而需要自己定义槽来完成两种温度之间的换算。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

在类中定义自己的槽的方法与定义成员函数类似,事实上槽就是一种特殊的成员函数,它有一些限制,我们将在下一章中详细讨论,在这里我们先来看看ConversionScreen.h中如何来声明所需要的槽: 1 #ifndef CONVERSION_SCREEN_H

2 #define CONVERSION_SCREEN_H

3

4 #include <QWidget>

5

6 class QSlider;

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

7 class QDial;

8 class QHBoxLayout;

9 class QVBoxLayout;

10

11 class ConversionScreen : public QWidget

12 {

13 Q_OBJECT

14

15 public:

16 ConversionScreen();

17 ~ConversionScreen() {};

18

19 private slots:

20 void celToFah(int celNum);

21 void fahToCel(int fahNum);

22

23 private:

24 void createScreen();

25 void createCel();

26 void createFah();

27

28 QSlider* slider;

29 QDial* dial;

30

31 QHBoxLayout* celLayout;

32 QVBoxLayout* fahLayout;

33

34 };

35

36 #endif //CONVERSION_SCREEN_H

可以看到,在19-21行我们定义了两个槽用来做两种温度之间的转换,Qt自定义的关键字slots用来表示下面的函数是作为槽而不是普通的成员函数,slots前面的private仍然保持C++关键字的意义,被定义为private的槽只能被这个类内部被调用。[www.61k.com)

另外需要注意的是第13行的宏Q_OBJECT,当类中需要自定义的信号或者槽时,必须在这个位置使用宏Q_OBJECT,这是Qt的元对象系统所要求的,只有借助这个宏才能完整的实现元对象系统,才能完成信号和槽的连接,在下一章中我们会详细讨论Qt的元对象系统,这里我们只需要有一个直观的印象就可以了。另外需要说明一下的是,在前面的代码中我们为了简单起见,没有过早的引入这个宏,只有在需要自定义槽时才提到,但一般我们建议在从QObject继承来的类中都添加这个宏,以避免将来无意的改动所带来的危险。

为了节省篇幅在这里我们不列出ConversionScreen.cpp的源代码了,完整的源代码可以在随书所附的光盘上找到。我们仅仅来分析一下相对上一小节所作的改动。

首先我们需要实现两个自定义的槽,在ConversionScreen.cpp的第77行到第87行可以

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

找到: 77 void ConversionScreen::celToFah(int celNum)

78 {

79 int fahNum = (celNum * 9 / 5) + 32;

80 dial->setValue(fahNum);

81 }

82

83 void ConversionScreen::fahToCel(int fahNum)

84 {

85 int celNum = (fahNum - 32) * 5 / 9;

86 slider->setValue(celNum);

87 }

这两个槽的实现非常类似:首先计算两种温度之间的换算后的结果,然后用setValue()函数来改变另外一个温度计的读数。(www.61k.com)

我们还需要在信号和槽之间建立连接,如下所示(我们略去了与上一小节中类似的部分,用省略号表示):

40 void ConversionScreen::createCel()

41 {

...

54 connect(slider, SIGNAL(valueChanged(int)), celLabel, SLOT(setNum(int))); 55 connect(slider, SIGNAL(valueChanged(int)), this, SLOT(celToFah(int))); 56 }

57

58 void ConversionScreen::createFah()

59 {

...

73 connect(dial, SIGNAL(valueChanged(int)), lcdNum, SLOT(display(int))); 74 connect(dial, SIGNAL(valueChanged(int)), this, SLOT(fahToCel(int))); 75 }

我们在57行和74行增加了两个类似的连接:55行将摄氏温度计的变化连接到槽celToFah(),74行则将华氏温度计的变化连接到槽fahToCel(),由于这两个槽都是ConversionScreen中所定义的,所以连接时第三个参数设置为this指针。可以看到这里的连接有一些复杂,slider对象的同一个信号valueChanged被连接到了两个不同的槽,类似的dial对象的同一个信号valueChanged被也连接到了两个不同的槽,这种连接是允许的——在Qt中可以将单个的信号与很多的槽进行连接,也可以将很多信号与单个的槽进行连接。

来编译一下看看结果如何。如果我们还是利用上一小节中的Makefile来编译,输入: # make

则很可能会得到类似下面的链接错误:

ConversionScreen.o(.text+0x20): In function

`ConversionScreen::ConversionScreen[not-in-charge]()':

: undefined reference to `vtable for ConversionScreen'

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

ConversionScreen.o(.text+0x27): In function

`ConversionScreen::ConversionScreen[not-in-charge]()':

: undefined reference to `vtable for ConversionScreen'

ConversionScreen.o(.text+0x74): In function

`ConversionScreen::ConversionScreen[in-charge]()':

: undefined reference to `vtable for ConversionScreen'

ConversionScreen.o(.text+0x7b): In function

`ConversionScreen::ConversionScreen[in-charge]()':

: undefined reference to `vtable for ConversionScreen'

main.o(.text+0x48): In function `main':

: undefined reference to `vtable for ConversionScreen'

main.o(.text+0x4f): more undefined references to `vtable for ConversionScreen' follow collect2: ld returned 1 exit status

链接错误有时候会比较难找出来,不过对这种情况不用慌张,在Qt的开发中可能会常遇到类似的错误,原因在于Qt复杂的元对象系统。[www.61k.com]在这个例子中由于我们增加了两个自定义的槽,并且在ConversionScreen.h的第13行使用了宏Q_OBJECT,这些必须借助Qt的moc(Meta Object Compiler)工具来生成一个额外的源文件,加入这个源文件到编译和链接过程中,才能正确的得到最后的二进制可执行程序。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

我们重新来生成一下Makefile就可以利用moc工具了:

# qmake

# make

这时可以看到由moc工具自动生成了一个名为moc_ConversionScreen.cpp的源文件,这个源文件也参与到编译和链接过程中,这样我们就可以得到可运行的

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

二进制程序了,如下图所示,我们可以拖动左边的滑块或者拉动右边的指针,另外一个的读数会发生相应的变化:

图10.7 温度的转换示意图

如上图所示,让我们来回顾一下这些信号和槽之间的连接:假设我们拖动左边的滑动条到了中间位置,这时滑动条将发射信号valueChanged,一共有两个槽与这个信号相连接:一

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

个是滑动条左边的标签对象的槽setNum,它将标签显示的数字设置为相应的值即50;另一个是我们自定义的槽celToFah,它将温度值转换之后再设置右边的dial对象的值——这次设置将转盘的指针转到了中间位置,并且dial对象的改变也会发射valueChanged信号,这个信号也被连接到两个槽:一个设置液晶显示数字为相应的华氏温度值即122,这样我们完全已经得到了所需要的温度转换的功能了;另一个是设置左边slider对象的值,这样会不会形成死循环呢?设置左边slider对象的值之后,slider对象又发射信号valueChanged,然后前面的又重来一遍,这样一直循环下去?不会的,因为slider对象的值现在已经是50, Qt内部自动做了这种检查,当没有改变的时候就不会发射信号了。(www.61k.com]当然即使Qt不提供这种自动检查,我们也可以自己来作检查以避免死循环,在实现槽的时候加一个判断就可以了。 10.3.6 保存当前的数值

这是我们这个小例子的最后一部分了。前面我们已经完成了完整的温度转换的工作,这一小节我们来增加一个附加功能——保存这次转换后的数值,程序退出后下一次再运行时直接显示上次所保存的数值。对这个例子来说,这样做并没有太多实际的意义,我们主要是想给大家演示一下在Qt中如何实现这种保存设置的功能,以作为实际Qt开发中的参考,因为这毕竟是一种很常见的需求。

Qt提供了QSettings类来将一些数值保存到文件系统中(对Windows系统来说可能是注册表),这样当程序退出时这些数值不致于丢失,在下次需要的时候还可以从文件系统(或注册表)中读取。QSettings类提供了数个构造函数以方便使用,常用的构造函数如下所示: QSettings(const QString & organization, const QString & application = QString(), QObject * parent = 0);

QSettings ( QObject * parent = 0 );

QSettings ( Format format, Scope scope, const QString & organization, const QString & application = QString(), QObject * parent = 0 )

我们使用第一个构造函数时常常需要设定好前两个参数,如下所示:

QSettings settings("MySoft", "MyApp");

对Qt/X11来说,这两个参数指定了所保存的文件存储时的目录和文件名。

而使用第二个构造函数时则需要事先调用QCoreApplication::setOrganizationName()和QCoreApplication::setApplicationName()来设定类似上面的两个参数,即:

QCoreApplication::setOrganizationName("MySoft");

QCoreApplication::setApplicationName("MyApp");

QSettings settings;

这样设定后与上面的构造函数作用是相同的,它的好处在于,当应用程序中多处需要这种QSettings对象时,只需要在这里设定一次,以后就都可以简单的使用第二个构造函数了。

第三个构造函数比较复杂,增加了前两个参数,分别用来设置文件的存储格式和作用范围。对第一个参数我们可以选择两种文件格式来存储设置,分别是QSettings::NativeFormat 和QSettings::IniFormat。对于Windows操作系统来说,NativeFormat使用注册表来存储设置,而IniFormat则顾名思义是采用INI格式的文件来保存设置。对于Linux/Unix来说这两个格式区别不大,其实都是采用INI格式的文件来保存,唯一不同的是文件的扩展名,前者是.conf,后者则是.ini。注意默认情况下存储格式是NativeFormat。另外Qt还支持自定义的文件格式,不过我们的小例子中暂时不需要这种功能,有兴趣的读者可自行参考Qt的官方

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

文档。[www.61k.com]

对第二个参数我们可以选择QSettings::UserScope和QSettings::SystemScope,这两种作用范围主要关系到文件的存储路径,其中UserScope是默认的设置。对于Qt/X11来说,如果采用默认的UserScope,则文件将存储到$HOME/.config路径下;如果采用SystemScope,则路径为/etc/xdg。我们可以看看完整的存储路径:如果采用NativeFormat和UserScope,并且构造QSettings对象时采用上面的示例代码的方法,则文件的路径应该是$HOME/.config/MySoft/MyApp.conf。

了解了这些基本用法之后,我们结合温度转换的小例子来看看如何使用QSettings类来保存当前的数值。我们首先需要在ConversionScreen.h文件中增加槽、成员函数以及成员变量,相关的代码如下所示: ...

19 private slots:

20 void celToFah(int celNum);

21 void fahToCel(int fahNum);

22 void saveSettings();

23

24 private:

...

29 void initSettings();

30 void readSettings();

...

38 int currentCelNum;

39 int currentFahNum;

...

其中第22行我们增加了一个槽,用来响应用户点击”Save”按钮,保存当前的两个温度计数值;第29行和第30行增加了两个私有成员函数,分别用来初始化设置和从文件中读取设置;第38行和39行则是两个成员变量,代表当前两个温度计的读数。 来看看这些新增加的函数和变量在ConversionScreen.cpp中的实现以及应用。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

1 #include <QPushButton>

2 #include <QSlider>

3 #include <QLabel>

4 #include <QDial>

5 #include <QLCDNumber>

6 #include <QVBoxLayout>

7 #include <QHBoxLayout>

8 #include <QGridLayout>

9 #include <QSettings>

10 #include <QApplication>

11 #include <QCoreApplication>

12

13 #include "ConversionScreen.h"

14

15 ConversionScreen::ConversionScreen() : QWidget()

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

16 {

17 initSettings();

18 readSettings();

19 createScreen();

20 }

21

22 void ConversionScreen::createScreen()

23 {

24 QPushButton* quitBtn = new QPushButton("Quit");

25 QPushButton* saveBtn = new QPushButton("Save");

26

27 createCel();

28 createFah();

29

30 QGridLayout *mainLayout = new QGridLayout;

31 mainLayout->addWidget(quitBtn, 0, 0);

32 mainLayout->addWidget(saveBtn, 0, 1);

33 mainLayout->addLayout(celLayout, 1, 0);

34 mainLayout->addLayout(fahLayout, 1, 1);

35 mainLayout->setSpacing(40);

36 mainLayout->setMargin(40);

37 setLayout(mainLayout);

38

39 slider->setFocus();

40

41 connect(quitBtn, SIGNAL(clicked()), qApp, SLOT(quit()));

42 connect(saveBtn, SIGNAL(clicked()), this, SLOT(saveSettings())); 43

44 setWindowTitle("Temperature Conversion");

45 }

46

47 void ConversionScreen::createCel()

48 {

49 slider = new QSlider(Qt::Vertical);

50 slider->setRange(0, 100);

51 slider->setValue(currentCelNum);

52 slider->setTickPosition(QSlider::TicksLeft);

53

54 QLabel* celLabel = new QLabel(QString::number(currentCelNum)); 55

56 celLayout = new QHBoxLayout;

57 celLayout->addWidget(celLabel, 0, Qt::AlignRight);

58 celLayout->addWidget(slider, 0, Qt::AlignLeft);

59 celLayout->setSpacing(10);

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

60

61 connect(slider, SIGNAL(valueChanged(int)), celLabel, SLOT(setNum(int))); 62 connect(slider, SIGNAL(valueChanged(int)), this, SLOT(celToFah(int))); 63 }

64

65 void ConversionScreen::createFah()

66 {

67 QLCDNumber* lcdNum = new QLCDNumber(3);

68 lcdNum->setSegmentStyle(QLCDNumber::Filled);

69 lcdNum->display(currentFahNum);

70

71 dial = new QDial;

72 dial->setRange(32, 212);

73 dial->setValue(currentFahNum);

74 dial->setNotchesVisible(true);

75

76 fahLayout = new QVBoxLayout;

77 fahLayout->addWidget(lcdNum, 0, Qt::AlignBottom | Qt::AlignHCenter); 78 fahLayout->addWidget(dial);

79 fahLayout->setSpacing(10);

80

81 connect(dial, SIGNAL(valueChanged(int)), lcdNum, SLOT(display(int))); 82 connect(dial, SIGNAL(valueChanged(int)), this, SLOT(fahToCel(int))); 83 }

84

85 void ConversionScreen::celToFah(int celNum)

86 {

87 int fahNum = (celNum * 9 / 5) + 32;

88 dial->setValue(fahNum);

89 }

90

91 void ConversionScreen::fahToCel(int fahNum)

92 {

93 int celNum = (fahNum - 32) * 5 / 9;

94 slider->setValue(celNum);

95 }

96

97 void ConversionScreen::initSettings()

98 {

99 QCoreApplication::setOrganizationName("MySoft");

100 QCoreApplication::setApplicationName("Conversion");

101 }

102

103 void ConversionScreen::saveSettings()

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

104 {

105 currentCelNum = slider->value();

106 currentFahNum = dial->value();

107

108 QSettings settings;

109 settings.setValue("Temperature/CelNumber", currentCelNum);

110 settings.setValue("Temperature/FahNumber", currentFahNum);

111 }

112

113 void ConversionScreen::readSettings()

114 {

115 QSettings settings;

116 currentCelNum = settings.value("Temperature/CelNumber", 0).toInt(); 117 currentFahNum = settings.value("Temperature/FahNumber", 32).toInt(); 118 }

这一次我们先来编译运行一下这个例子,再来详细讲解它。(www.61k.com]编译的过程还和以前一样,运行后我们可以看到在Quit

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

按钮旁边增加了一个Save按钮。试一试下面的步骤:

1. 拖动滑块或指针来改变当前的温度计数值;

2. 点击Save按钮;

3. 点击Quit按钮退出程序;

4. 再重新运行该程序,可以看到刚刚保存的数值被读取出来了;

5. 在目录$HOME/.config/MySoft/下可以找到所保存的配置文件Conversion.conf,它所

采用的也是INI文件的格式,内容如下所示:

[Temperature]

CelNumber=20

FahNumber=68

程序运行的示意图如下:

图10.8 保存当前的数值

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

来看看我们在上一小节的基础上增加了些什么:首先在25行我们增加了一个Save按钮,并且在32行把它安排在网格布局容器的第1行第2列,即(0, 1)坐标,后面的第42行为它增添了一个连接,当用户点击这个按钮时,就会发射clicked()信号,于是连接到这个信号的槽saveSettings()被执行,以真正保存当前数值。[www.61k.com]

顺着这个思路我们来看看槽saveSettings(),它在103行-111行,首先得到当前的读数,然后构造一个QSettings对象,并用setValue()方法将读数存储起来。注意我们所用的QSettings的构造函数是最简单的一个,因为我们在initSettings()函数(97行-101行)中调用了QCoreApplication::setOrganizationName()和QCoreApplication::setApplicationName()来设定organization和application。存储时用到的setValue()方法比较简单而且方便,函数原型如下: ü void setValue ( const QString & key, const QVariant & value );

第一个参数为key,第二个参数则是对应key所保存的值。在Qt/X11中,由于我们所用的存储格式总是INI文件格式,我们可以简单的用一个字符串来表示key,也可以采用”group/key”的格式。如果省略前面的group的话,Qt会将这个key自动加到名为”General”的group中。在109和110行我们使用"Temperature/FahNumber"作为第一个参数,正如我们在配置文件Conversion.conf中所看到的,这样生成了一个” Temperature”的group,两个key CelNumber和FahNumber的值也都被保存在这个group中。第二个参数的类型为QVariant,QVariant可以用来表示Qt中的很多数据类型并提供方便的相互转换功能,因此大多数的设置都可以用QSettings类来直接保存。

读取所保存数值的函数readSettings()在113-118行,与保存时非常类似,我们同样用QSettings的构造函数中最简单的一个来创建一个QSettings对象,用QSettings::value()来读取数值。由于这个函数返回的是一个QVariant对象,我们需要用toInt()将它转换为int型数值。QSettings::value()的第一个参数是key,第二个参数是默认值,即当读不到这个key时返回一个默认值,我们将默认值分别设置为0和32,即摄氏0度。

我们在ConversionScreen类的构造函数中的第17、18行调用了initSettings()和readSettings(),在主窗口显示之前先设置好organization和application,并读取上一次保存的值。在第51、54、69和73行将读到的数值分别设置到相关的部件上,注意如果上一次有保存的话我们读到所保存的数值并作相关设置,如果以前并没有保存,或者程序还是第一次运行,则根本不会有Conversion.conf整个文件,这时我们读取时返回的则是调用QSettings::value()所给定的第二个参数,即分别为0和32。

保存程序当前的一些设置是应用开发中经常遇到的需求,希望读者从我们的这个小例子基本了解了应用QSettings类来保存设置的方法。

10.4 本章小结

这一章我们主要来帮助大家了解Qt/X11的初步知识,包括它的授权问题,详细的安装过程,以及通过一个温度转换的小例子向大家介绍了利用Qt进行应用开发的一些基础知识。希望大家对Qt进行应用开发的一整套步骤都有所了解,比如如何利用qmake工具协助编译,如何连接信号和槽以及定义自己的槽等,同时在最后我们还简单介绍了应用QSettings类来保存设置的方法。

通过本章的学习,读者应该已经搭建了Qt/X11的开发环境,了解了Qt的编译过程,并可以尝试简单的Qt编程了。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

10.5 常见问题

1. Qt/X11自由版可以用于商业开发吗?

参考答案:

可以,但是你需要遵循GPL和QPL。(www.61k.com]由于GPL要求你所开发的产品也开发源代码,以及还有其他一些限制,所以多数公司不使用Qt/X11自由版来进行商业开发。一般来说商业开发购买Qt的商业版本比较合适一些。

2.使用QSettings类时,可以将配置文件保存到我所希望的任意路径,而不是默认的$HOME/.config或者/etc/xdg吗?

参考答案:

当然可以。Qt提供的构造函数中有:

QSettings ( const QString & fileName, Format format, QObject * parent = 0 )

第一个参数就是配置文件的全名(包括路径)。另外你还可以使用它的静态成员函数 void setPath ( Format format, Scope scope, const QString & path )

来进行设置,不过需要注意这个函数并不会修改已经存在的QSettings对象。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

第11章 Qt核心技术

本章学习目标:

l 掌握信号和槽的各种用法

l 了解Qt的元对象系统

l 了解信号和槽的实现机制及其局限性

l 理解Qt的对象树及其方便内存管理的特性

l 掌握常用的布局管理方法

l 理解Qt国际化的方法并熟悉翻译的过程

11.1信号(Signals)和槽(Slots)

在上一章中我们应该对信号和槽的机制有了一个直观的认识,这一章我们来详细了解一下,因为它是Qt的核心特性,也是Qt区别于其它工具包的重要地方。(www.61k.com]

我们可以把信号和槽机制看成是Qt自行定义的一种应用于对象之间的通信的高级接口。要注意信号和槽并不是标准C/C++语言的特性,我们可以认为它是Qt对C++特性的一种扩展。采用信号和槽机制编写的代码不能直接被标准C++编译器编译,而需要借助一个被称为moc(Meta Object Compiler)的Qt工具对信号和槽在编译之前进行预处理。 我们先来了解一下信号和槽机制所解决的问题以及其他的一些解决方案。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

11.1.1 常见的GUI组件通信方式

在图形用户界面(GUI)编程中,我们经常希望一个可视对象发生某种变化时通知另一个或几个对象,或者采用某种逻辑或调用某个接口来响应这个变化。例如,用户在点击某个按钮时,当前窗口需要发生某种变化,或者需要打开新的窗口,我们希望当前窗口能够收到按钮被点击的信号,并及时响应用户的需求。进一步的,即使没有用户的输入,当底层的数据发生变化时,也需要通知用户界面进行及时地更新。

11.1.1.1 回调函数

对于类似以上的问题,早期的工具包使用一种被称作“回调”(callback)的通信方式来实现。回调是通过函数指针来实现的,即将一个函数指针(我们称作回调函数)传递给处理函数,处理函数在适当的时候调用回调函数。

常见的回调比如写快速排序函数的时候,即在参数中预先声明一个回调函数的指针,调用者需要自己准备一个函数来比较大小。C语言的标准库函数中快速排序函数的原型如下: void qsort(void *base, size_t n, size_t size,

int (*cmp)(const void *, const void *))

参数中的base是需要排序的数组,n是数组的长度,size是数组中所存储的对象的大小,而最后面的int (*cmp)(const void *, const void *)) 则正是我们需要提供的用来比较数组成员

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

大小的回调函数。(www.61k.com)在qsort这个库函数的实现中,即通过调用cmp这个函数来比较数组base中成员的大小,成员

下面的代码示例说明了如何编写这个回调函数,以调用qsort来进行排序: #include <stdio.h>

#include <stdlib.h>

int cmpFunc(const void *a, const void *b);

int myArray[6] = { 5, 12, 4, 33, 1, 8 };

int cmpFunc(const void *a, const void *b)

{

return *(int*)a - *(int*)b;

}

int main()

{

qsort((void *) myArray, 6,

sizeof(myArray[0]), cmpFunc);

for (int i = 0; i < 6; i++)

{

printf("%d\n", myArray [i]);

}

return 0;

}

上面示例代码的调用关系我们可以用下图来表示:

图11.1 调用关系示意图

如果主程序与库函数之间采用层次结构的话,可以看出,在调用过程中既有上层对下层的调用(主程序调用库函数), 也有下层对上层的间接调用(库函数调用回调函数),这种调用关系既没有增加下层对上层的依赖关系,又实现了灵活性,这正是很多时候我们

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

需要回调函数的原因。

早期的一些开发包如Win32 SDK中即有不少的回调函数,比如Win32程序中用来处理

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

窗口过程的WndProc函数:

WndProc函数在Win32程序中需要开发者自己定义,来处理窗口收到的各种消息,Windows系统的消息处理机制会自动调用WndProc函数来进行处理。[www.61k.com)可以看出,对比上面的快速排序的例子,这里的回调稍有不同——主程序的一部分实际上已经由系统自动完成了,开发者只需要准备好回调函数就可以了。

由于回调是通过函数指针来实现的,而根据C语言的特性,不同类型的指针之间是可能被转换的,这不是一种类型安全的方式——我们无法确定处理函数是否使用了正确的参数来调用回调函数——有些时候这种错误是致命的,比如参数在压栈的时候发生失误,这时便很可能导致整个进程的退出。另外,回调函数和处理函数间的联系非常紧密,而且这种这无疑会增加系统的耦合程度,对于大型系统的开发是非常不利的。

11.1.1.2 面向对象的回调

在面向对象的开发中,我们可以用虚拟和继承以及模版来实现简单的回调机制,比如在父函数中定义一个虚函数来当作回调函数,在主程序中可以调用这个虚函数,而真正的实现则由子函数重写这个虚函数来完成。这种方法可以在一定程序上避免函数指针带来的危险。

比如上面的qsort()函数,如果采用虚拟和继承来实现,我们可以作如下的设计:定义基类CmpObj及其成员函数int CmpObj::cmpFunc(const CmpObj & a, const CmpObj& b)来比较大小,在对不同的一组对象进行快速排序时,通过继承基类CmpObj并在子类中重写cmpFunc方法即可。当然在这个问题上这样做带来的好处是有限的,我们仅仅是以此为例说明一下如何重写虚函数来代替回调,以避免函数指针带来的类型安全问题。

我们还可以对比一下在STL中提供的sort()函数与上面的C标准库中的qsort()。STL中提供的sort()函数充分利用了模版特性,其原型如下: template<class _RandomAccessIter, class _Compare>

void sort(

_RandomAccessIter __first, // 需排序数据的第一个元素位置

_RandomAccessIter __last, // 需排序数据的最后一个元素位置(不参与排序)

_Compare __comp // 排序使用的比较算法(可以是函数指针、函数对象等)

);

这种采用模版的方法要求使用迭代器的对象来作为前两个参数以便遍历需要排序的数据,而第三个参数的作用则类似于回调函数。事实上与采用虚函数的方法相比,STL中的方法可以看作是用模版来代替继承——这正是模版的作用之一。

尽管我们可以上面的一些方法来实现回调机制,但在开发复杂的GUI系统时,基于开发效率和运行效率的考虑,上面的这些方法并不是非常适用。现代的GUI系统中纷纷采用了更加广义上的回调机制,比如消息机制、事件驱动等

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

,来简化应用程序的开发并提高软件复用的程度。这些机制不再需要处理复杂的函数指针,其结构体系也更为完整,但它们都是采用类似回调的这种思路,只是在它们的框架中已经添加了一些功能来隐藏真正的回调过

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

程,比如MFC的框架中就定义了很多宏来支持消息映射机制,Java GUI中则用Observer 模式实现了Listener机制,这些都可以看作一种广义上的更加便于使用的回调。[www.61k.com)

另外,著名的“四人帮”(GOF, Gang of Four)所著的《设计模式》一书中描述了用Command模式来实现回调机制的方法[12],并且能够支持“撤销”功能,有兴趣的读者可以作为参考。 11.1.2 Qt中的信号和槽(Signals and Slots)

11.1.2.1 信号和槽历史和所带来的优点

大约在1992年的时候,Qt的两位创始人之一Eirik在开发Qt的过程中,萌生了Signal-Slot的想法来解决GUI开发中复杂的通信问题,接着Haavard通过扩展C++的特性,实现了这种机制。这种灵活的机制被后来的不少开发包所采用,包括Gtk+,boost库*(注1)等都提供了类似的概念和实现。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

Qt为信号和槽的机制设计了自己的语法来扩展C++的特性,并利用一个称为moc(Meta Object Compiler)的Qt工具来自动将这种Qt自定义的语法转换为C++代码。信号和槽能携带任意数量和任意类型的参数,它们是类型完全安全的,而且信号和槽之间的联系非常灵活,而耦合却比较松散。定义各种类的开发者互相之间并不需要知道别人有哪些信号或者槽,这些类只需要考虑要提供哪些信号和槽,而且一个发射信号的对象不用考虑哪个槽会接收这个信号,接收信号的槽的所在对象也不知道要连接的信号是哪个对象发射的;而连接信号和槽的人则往往不需要关心信号或者槽的实现细节,他们只需要清楚将哪些信号连接到哪些槽就可以了,这样可以为大型开发中的分工和合作提供很大的方便,可以说信号和槽是一种比较适合面向对象开发的通信机制。

信号和槽的机制可以简单地描述为,当一个特定的事件发生时,一个或几个被指定的信号就被发射,槽则是响应信号的函数,如果存在一个或几个槽和该信号相连接,那在该信号被发射后,这个(些)槽就会立刻被执行。

11.1.2.2 信号

信号在Qt中由关键字signals来声明,它的形式类似于一个函数,但是返回值只能是void类型,而且对初学者非常重要的一点是:信号只需要声明即可,千万不要在.cpp文件中去实现在头文件中声明的信号。

信号往往在对象的内部状态发生改变时被发射出去,以通知它所连接的槽,并且发射信号只能由定义了一个信号的类和它的子类才能来完成。发射信号的关键字是emit,注意这些关于信号和槽的关键字(包括signals, emit及后面的slots等)只是Qt所定义的语法,并非标准C++中的关键字,它们都会在由C++编译器编译之前被moc工具转换成标准C++的语法。我们将在下一节讲述Qt的元对象模型时,来仔细探讨这些关键字的真正定义,以及为什么信号只能返回void,不能由开发人员去实现等问题。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

11.1.2.3 槽

槽是一种可以用来连接到信号的成员函数,信号发射时,它所连接的相应的槽将被执行,不过对于槽本身来说,它并不知道自己是否被其它信号连接。(www.61k.com)信号和槽的连接可以非常灵活——我们可以把一个信号和一个槽进行单独连接,这时槽会因为该信号被发射而被执行;也可以把几个信号连接在同一个槽上,这样任何一个信号被发射都会使得该槽被执行;也可以把一个信号和多个槽连接在一起,这样该信号一旦被发射,与之相连接的槽都会被马上执行(但需要特别注意的是,这些槽执行的顺序是不确定的,并且也不可以指定);我们甚至还可以把一个信号和另一个信号进行连接,这样,只要第一个信号被发射,第二个信号立刻就被发射。

我们可以来回顾一下上一章中温度转换的小例子中用到的连接: connect(slider, SIGNAL(valueChanged(int)), celLabel, SLOT(setNum(int)));

connect(slider, SIGNAL(valueChanged(int)), this, SLOT(celToFah(int)));

这里我们将slider对象发射的同一个信号valueChanged()连接到了两个不同的槽,即setNum()和celToFah()。这两个槽在valueChanged()信号发射后都将被执行,不过它们的执行顺序是不确定的,并且也不可以指定。

除了与信号连接之外,槽实际上也可以作为普通的成员函数来使用,它可以被直接调用,这时候我们需要考虑它的访问权限,与普通的成员函数类似,我们可以将槽声明为public, protected或者private,声明为public slots的槽才可以被其他类所直接调用,声明为protected slots的槽则只能被这个类和它的子类直接调用,而声明为private slots的槽则只能被这个类本身所直接调用。但是需要注意当它们真正作为槽来使用,即连接它到某个信号时,这时我们定义的访问权限并没有任何效果,即使声明为private slots的槽也能被任意类的信号所连接。

这里注意比较一下前面提到的信号:信号并不能作为普通的函数来使用,定义它的访问权限也是没有意义的,无论任何情况,都只有定义这个信号的类和它的子类才能发射这个信号,这比较类似于访问权限是protected的情况。

我们还可以把槽定义为虚函数,Qt的元对象系统能够识别这种多态特性,在连接信号的时候执行正确的槽。这在实践开发中可以提供很多便利,可能会经常用到。

信号和槽可以携带参数,但是要求它们的参数类型和个数都是一样的,如果信号携带的参数比槽的多,则多余的参数将会被忽略。在下一节中我们可以看到,由于Qt的元对象系统的特性,信号和槽携带参数的参数还有诸多限制,比如不能用模版,不能用函数指针,不能有默认值等。

11.1.2.4 信号和槽的效率

信号和槽机制的效率当然不如真正的回调那么快,因为Qt在背后需要作很多工作来支持它,需要牺牲一些效率来满足它们所提供的灵活性。尽管如此,在实际应用中这种稍微有点慢的情况经常可以被忽略。根据Qt的官方文档介绍,在不使用虚函数作为槽的情况下,采用信号和槽机制从发射信号到相应的槽执行时的时间,大约比直接调用这个函数的时间要慢十倍,从下一节中我们可以知道,这主要是定位连接对象所需的开销。这个时间开销实际上并不是很大,举个例子来说,它比任何一个“new”或者 “delete”操作还要稍微少一些。当你执行一个字符串、矢量或者列表操作时,如果需要“new”或者 “delete”操作,信号和槽仅仅对一个完整函数调用的时间开销中的一个非常小的部分负责。在一台i585-500机

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

器上,你每秒钟可以发射2,000,000个左右连接到一个接收器上的信号,或者发射1,200,000个左右连接到两个接收器的信号。[www.61k.com)信号和槽机制的简单性和灵活性对于这点时间开销来说是非常值得的,你的用户甚至察觉不出来。

11.1.3 自定义信号和槽的小例子

在Qt提供的类库中已经有了很多定义好的信号和槽,在我们上一章的小例子中用到了一些Qt已经定义好的信号和槽,同时也自己定义了几个槽来处理一些程序逻辑,这里我们进一步向大家来演示如何定义和发射自己的信号,并且将多个信号连接到一个槽中,以及将一个信号连接到另一个信号。

我们总共定义三个类Sender、Mediator和Receiver来分别发送、转发和接收信号,因为这三个类都比较简单,我们把它们放在同一个头文件signals_slots.h中,如下所示: 1 #ifndef SIGNALS_SLOTS_H

2 #define SIGNALS_SLOTS_H

3

4 #include <QObject>

5

6 class Receiver : public QObject

7 {

8 Q_OBJECT

9

10 public:

11 Receiver(QObject *parent=0) : QObject(parent)

12 {

13 }

14

15 public slots:

16 void printNumber(int n);

17 };

18

19 class Mediator : public QObject

20 {

21 Q_OBJECT

22

23 public:

24 Mediator(QObject *parent=0) : QObject(parent)

25 {

26 }

27

28 void doSend();

29

30 signals:

31 void send(int);

32 };

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

33

34 class Sender : public QObject

35 {

36 Q_OBJECT

37

38 public:

39 Sender(QObject *parent=0) : QObject(parent)

40 {

41 }

42

43 void doTransmit();

44

45 signals:

46 void transmit(int);

47 };

48

49 #endif // SIGNALS_SLOTS_H

先从我们已经比较熟悉的自定义的槽开始,我们在类Receiver中声明了一个public的槽,即第16行的void printNumber(int n),用来打印出所接收到的整型参数。(www.61k.com]类Mediator和Sender非常类似,它们都定义了各自的信号(分别在31行和46行),并且定义了公有成员函数用来发射信号。注意所有包含信号或者槽的类必须在它们的声明中用到宏Q_OBJECT,因此这三个类的定义中分别在第8行、第21行和第36行都加上了宏Q_OBJECT。

这三个类的实现也非常简单,我们把它们的实现以及main()函数都放在signals_slots.cpp中。如下所示: 1 #include <QApplication>

2 #include <iostream>

3 #include "signals_slots.h"

4

5 void Receiver::printNumber(int n)

6 {

7 std::cout << "Recieved: " << n << std::endl;

8 }

9

10

11 void Mediator::doSend()

12 {

13 emit send(5);

14 }

15

16 void Sender::doTransmit()

17 {

18 emit transmit(10);

19 }

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

20

21 int main(int argc, char **argv)

22 {

23 QApplication a(argc, argv);

24

25 Receiver r;

26 Mediator m;

27 Sender s;

28

29 QObject::connect(&s, SIGNAL(transmit(int)), &r, SLOT(printNumber(int))); 30 QObject::connect(&s, SIGNAL(transmit(int)), &m, SIGNAL(send(int))); 31 QObject::connect(&m, SIGNAL(send(int)), &r, SLOT(printNumber(int))); 32

33 s.doTransmit();

34 m.doSend();

35

36 return 0;

37 }

我们自定义的槽printNumber非常简单,仅仅调用标准C++的iostream来打印传入的整型参数。(www.61k.com)doSend()和doTransmit()仅仅是发射各自的信号。注意我们在signals_slots.h中声明的信号send()和transmit()都不需要真正的实现,仅仅一个声明就足够了(下一节中我们可以看到自己实现自定义的信号所带来的问题)。另外我们要注意,只有定义信号的类才能发射这个信号,比如想要在类Mediator发射信号transmit()是行不通的。

在main函数中的第29-31行我们将transmit()信号分别连接到槽printNumber()和信号send(),同时将send()信号也连接到了槽printNumber()。分析一下这样做的结果:当transmit()信号发射时,由于槽printNumber()被连接到这个信号,它会被执行一次;同时transmit()信号的发射也会引起send()信号的发射,由于槽printNumber()也被连接到send()信号,这样它被执行第二次。

第33行和第34行分别发射了信号transmit()和send()。注意发射信号所用的关键字是ermit。

同样的方法编译并运行:

# qmake –project

# qmake

# make

#

# ls

Makefile moc_signals_slots.cpp moc_signals_slots.o signals_slots signals_slots.cpp signals_slots.h signals_slots.o signals_slots.pro

#

# ./ signals_slots

Recieved: 10

Recieved: 10

Recieved: 5

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

注意moc工具为我们生成了moc_signals_slots.cpp来参与编译和链接。[www.61k.com]最后的运行结果证实了我们前面的分析:当transmit()信号发射时,槽printNumber()被执行了两次,这时参数的值是doTransmit()函数中所传入的,为10;而第三次执行则是doSend()函数中发射了信号send()并传入参数5所得到的结果。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

希望读者朋友从这个简单的小例子了解到了如何定义和发射信号,以及如何将一个信号连接到另一个信号。这一节中我们详细的介绍了信号和槽的用法,下一节我们将在介绍Qt元对象系统(Meta-Object System)的基础上来仔细研究一下信号和槽到底是怎么一回事。 11.2 Qt对象模型

Qt所提供的灵活利用的特性是建立在它的对象模型的基础上的,而Qt的对象模型所提供的最重要的机制便是上一节中所介绍的信号和槽,同时也提供了其他一些便利比如相对简单的内存管理,可查询和可设计的属性等。

我们先从Qt对象模型的基础——元对象系统(Meta-Object System)说起。

11.2.1 元对象系统(Meta-Object System)

元对象系统(Meta-Object System)在Qt中主要用来实现信号和槽的机制,以及运行时的类型信息和动态属性系统。

在前面的一些例子中我们实际上也提到了元对象系统。当一个类从QObject继承而来,并在它的声明中用到了宏Q_OBJECT时便具备了应用元对象系统的条件。在使用qmake生成Makefile的过程中,qmake工具会识别宏Q_OBJECT,因而在生成的Makefile中将调用moc(Meta Object Compiler)工具来自动生成一个cpp文件,并将生成的cpp文件加入到编译和链接过程中。示意图如下:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图11.2 moc及编译的过程

图中的myclass.h文件中包含有宏Q_OBJECT,于是在Makefile中将有步骤调用moc工具自动生成moc_myclass.cpp文件,这个cpp文件与类的实现文件myclass.cpp一起参与到后面的编译和链接过程,直到生成最后的可执行文件或者库。这些过程都可以由qmake和make自动完成,所以在前面的例子中我们都没有详细介绍。

那么宏Q_OBJECT除了作为一个标志以便moc工具可以识别之外,它本身到底被定义成什么了呢?我们来看看Qt的源代码。在解压缩我们所下载的Qt安装包qt-x11-opensource-src-4.2.2.tar.gz之后,在其中的src/corelib/kernel/目录下可以找到qobjectdefs.h文件,这个文件中包含了Q_OBJECT宏的定义:

97 #define Q_OBJECT \

98 public: \

99 static const QMetaObject staticMetaObject; \

100 virtual const QMetaObject *metaObject() const; \

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

101 virtual void *qt_metacast(const char *); \

102 QT_TR_FUNCTIONS \

103 virtual int qt_metacall(QMetaObject::Call, int, void **); \

104 private:

这里的内容有些复杂,对于初学者来说,知道Q_OBJECT宏为我们自动添加了一些成员变量和成员函数就足够了,再加上了解了moc工具会为我们自动生成moc_myclass.cpp文件,当遇到问题时,根据这些线索就可能找到问题的答案。(www.61k.com]这些隐藏在背后的机制并不一定需要开发者完全了解,毕竟Qt已经为我们自动实现了这些功能。

对于希望深入了解Qt,或者甚至可能需要去修改Qt源代码以适用自己开发的读者朋友,我们在这些为大家提供一些较为深入的介绍——Trolltech公司并没有相关的文档来介绍这些,我们只有通过自己阅读Qt源代码来理解,因此下面的内容可能较为晦涩难懂,初学Qt的读者如果看不懂的话完全可以先略过,等有了更多的使用经验之后回头再来看看,也许会有所收获。

我们首先来详细分析一下Q_OBJECT宏的内容。第99行声明了一个QMetaObject类的静态成员对象,下面的第100行、101行和103行则重写(override)了三个虚函数,注意这三个虚函数最早是在QObject中声明的,在QObject类的定义中,同样使用了宏OBJECT,我们可以在qobject.h文件中找到下面的代码,在第99行用到了宏OBJECT。 97 class Q_CORE_EXPORT QObject

98 {

99 Q_OBJECT

100 Q_PROPERTY(QString objectName READ objectName WRITE setObjectName) 101 Q_DECLARE_PRIVATE(QObject)

...

}

我们甚至可以在Qt的文档中都找到QObject类的成员函数:

virtual const QMetaObject *metaObject() const;

而它完全是由宏OBJECT和moc工具来实现的,qobject.h中并没有显式的定义它,而是将它隐藏在宏OBJECT中。上面提到的宏OBJECT展开时的几个重写的虚函数都是这种类似的情况,102行的QT_TR_FUNCTIONS如果展开后同样是几个虚函数,它们与Qt国际化的实现机制相关,我们留到国际化的相关章节再来详细说明。

读者可能已经注意到宏OBJECT展开后最重要的是99行的QMetaObject静态对象staticMetaObject,它包含了这个类的很多重要信息。我们来看看QMetaObject类的定义: 211 struct Q_CORE_EXPORT QMetaObject

212 {

...

354 struct { // private data

355 const QMetaObject *superdata;

356 const char *stringdata;

357 const uint *data;

358 const QMetaObject **extradata;

359 } d;

360 };

QMetaObject类的定义比较长,我们先来看看它所包含的数据结构——从355行到358

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

行,各个结构体成员的作用如下:

const QMetaObject * superdata —— 指向父类的 QMetaObject 对象的指针;

const char * stringdata —— 包含该类本身的相关信息(包含类名,信号名,槽名,属性名,枚举名,变量名等)的字符串;

const uint * data —— 数据信息,结合stringdata来取得类名,信号名,槽名等 const QMetaObject ** extradata ——附加信息,目前一般为 0,可能是保留给将来扩展时所用

实际上第一个和第四个都是递归定义的指针,最主要的是中间的两个stringdata和data指针,理解了它们怎么结合运用就很比较理解QMetaObject类了。(www.61k.com)stringdata指针实际上就是由数个字符串连接而成的长字符串,而data指针则可以看成是一个整型数组。这两个指针实际上还与一个结构体QMetaObjectPrivate相关,它定义在qmetaobject.cpp中: 150 struct QMetaObjectPrivate

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

151 {

152 int revision;

153 int className;

154 int classInfoCount, classInfoData;

155 int methodCount, methodData;

156 int propertyCount, propertyData;

157 int enumeratorCount, enumeratorData;

158 };

159

160 static inline const QMetaObjectPrivate *priv(const uint* data)

161 { return reinterpret_cast<const QMetaObjectPrivate*>(data); }

这个结构体用来解析data数组的前10个数,154行-157行非常类似,前一个成员表示个数,后一个则表示偏移量,我们在后面结合实际的例子来详细说明。在紧接着结构体的定义之后,第160行的函数即用来作从data数组到结构体QMetaObjectPrivate的转换。事实上每个data数组都是由moc工具所自动生成,前10个数都是按照QMetaObjectPrivate的成员的含义而生成。我们来看看实际的例子,在上一节中moc工具为我们生成了moc_signals_slots.cpp:

19 static const uint qt_meta_data_Receiver[] = {

20

21 // content:

22 1, // revision

23 0, // classname

24 0, 0, // classinfo

25 1, 10, // methods

26 0, 0, // properties

27 0, 0, // enums/sets

28

29 // slots: signature, parameters, type, tag, flags

30 12, 10, 9, 9, 0x0a,

31

32 0 // eod

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

33 };

34

35 static const char qt_meta_stringdata_Receiver[] = {

36 "Receiver\0\0n\0printNumber(int)\0"

37 };

38

39 const QMetaObject Receiver::staticMetaObject = {

40 { &QObject::staticMetaObject, qt_meta_stringdata_Receiver,

41 qt_meta_data_Receiver, 0 }

42 };

43

44 const QMetaObject *Receiver::metaObject() const

45 {

46 return &staticMetaObject;

47 }

静态成员变量staticMetaObject在39行被定义,在必要时可由44行的metaObject()函数获得。(www.61k.com]第39行定义的时候分别给定了指向父类的 QMetaObject 对象、stringdata字符串和data数组。stringdata字符串即qt_meta_stringdata_Receiver变量在35行,由moc工具扫描signals_slots.h之后自动生成,它真正包含了类中的许多信息,多个字符串之间用”\0”隔开,这些字符串需要一定的规则来进行解析,这就要用到data数组了。

从第19行到第33行,data数组被初始化,注意22-27行分别对应了结构体QMetaObjectPrivate的各个成员,这有些类似于文件系统中的文件头或者网络传输中的包的头信息。在signals_slots.h文件中我们只是声明了槽printNumber,因此头信息中有很多0,其中的revision 基本不用可以先略过;classname是类的名字,值为0,表示在stringdata字符串即qt_meta_stringdata_Receiver中从一开始保存的就是类名,即Receiver。我们略过这里没有用到的classinfo, properties和enums/sets,来看看methods:两个值1和10分别对应结构体QMetaObjectPrivate中的methodCount和methodData,即method个数和后面的method数据区的偏移量。在Receiver类中我们仅仅定义了一个槽printNumber,因此methodCount的值为1,而偏移量我们可以数一数到第30行即method数据区,是不是前面正好有10个数字呢?

请注意这里的method并不是指所有的成员函数,而是仅仅包括元对象系统所需要的信号和槽及与属性等相关的方法。在本节的上下文中所提到的method,其含义都在此范畴。

接着来看method数据区,在29行moc工具为我们提供了注释,第30行的5个数分别是signature, parameters, type, tag和flags,我们主要关心signature和flags,signature的值为12,意味着我们从qt_meta_stringdata_Receiver中的第12位去取槽的名字,从字符串"Receiver\0\0n\0printNumber(int)\0"中第12位开始,直到遇到”\0”为止,可以取得” printNumber(int)”,这便是槽的名字。flags的值决定了method的类型和访问权限,在qmetaobject.cpp中有相关的定义:

134 enum MethodFlags {

135 AccessPrivate = 0x00,

136 AccessProtected = 0x01,

137 AccessPublic = 0x02,

138 AccessMask = 0x03, //mask

139

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

140 MethodMethod = 0x00,

141 MethodSignal = 0x04,

142 MethodSlot = 0x08,

143 MethodTypeMask = 0x0c,

144

145 MethodCompatibility = 0x10,

146 MethodCloned = 0x20,

147 MethodScriptable = 0x40

148 };

在moc_signals_slots.cpp中槽printNumber(int)的flag值为0x0a,这实际上等同于0x02+0x08,即AccessPublic + MethodSlot,表示这是定义为公有的槽。(www.61k.com]通过与AccessMask或者MethodTypeMask的“按位与”操作(即”&”操作),这个flag值可以还原用来是公有还是私有,是信号、槽还是MethodMethod类型。比如:

0x0a & 0x03 (AccessMask) = 1010 & 0011 = 0x02,可以判断它是公有的;同样

0x0a & 0x0c (MethodTypeMask) = 1010 & 1100 = 0x08,可以判断它是槽。 我们还可以查看一下moc_signals_slots.cpp中为Mediator类生成的stringdata和data: 70 static const uint qt_meta_data_Mediator[] = {

71

72 // content:

73 1, // revision

74 0, // classname

75 0, 0, // classinfo

76 1, 10, // methods

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

77 0, 0, // properties

78 0, 0, // enums/sets

79

80 // signals: signature, parameters, type, tag, flags

81 10, 9, 9, 9, 0x05,

82

83 0 // eod

84 };

85

86 static const char qt_meta_stringdata_Mediator[] = {

87 "Mediator\0\0send(int)\0"

88 };

内容基本是类似的,只是数值稍有变化,flag值变成了0x05,这同样可以被解析成0x01 + 0x04,即AccessProtected + MethodSignal。

我们可以稍微总结一下这种数据结构:

1. stringdata是一个字符串,其中存放所谓的元对象信息,并通过”\0”将各个部分

分隔开来,以便于读取;

2. data是一个无符号整型数组,这个数组的前10个数保存所谓的头信息,与结构

体QMetaObjectPrivate中的各个成员相对应,并可以互相转换;之后则分别是

classinfo, methods, properties和enums/sets的数据区(如果存在的话),数据区

的偏移量和大小由头信息中的数值所决定(比如我们可以认为methodCount的

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

值就决定了method数据区的大小,因为每一个method在数据区中占用了5个

数字,则总共占用methodCount * 5个数字,余者可类推)。(www.61k.com]

了解这些数据结构之后,可能我们已经能够自己编写代码来解析stringdata中所包含的信息了。可能有读者会产生疑问,为什么我们要用这么复杂的规则和数据结构来保存和解析这些所谓的元对象信息呢?在QMetaObject中多定义一些成员变量不是同样可以实现吗?事实上这种采用data和stringdata结合来保存和读取类的相关信息的方法是在Qt4中新引入的,在之前的Qt版本中我们可以看到采用更多的成员变量来实现的方法,比如下面的代码片段即来自于Qt3.3版的qmetaobject.h文件中的QMetaObject类的定义: private:

QMemberDict *init( const QMetaData *, int );

const char *classname; // class name

const char *superclassname; // super class name

QMetaObject *superclass; // super class meta object

QMetaObjectPrivate *d; // private data for...

void *reserved; // ...binary compatibility

const QMetaData *slotData; // slot meta data

QMemberDict *slotDict; // slot dictionary

const QMetaData *signalData; // signal meta data

QMemberDict *signalDict; // signal dictionary

int signaloffset;

int slotoffset;

这种实现方法应该说是一种比较自然的方法,看起来要简单易懂一些,读者如有兴趣下载Qt3.3版的源代码来作为参考。当然,Qt4中的实现更加紧凑,在理解了之后其实运用起来也非常灵活,作为一种机制的内部实现,牺牲一些可读性是值得的,毕竟大多数的开发者并不一定需要对这种实现机制非常清楚。

我们接着来看在qmetaobject.cpp中是如何编写代码来解析stringdata的,我们挑选一个典型的函数QMetaObject::indexOfSlot(const char *slot):

451 int QMetaObject::indexOfSlot(const char *slot) const

452 {

453 int i = -1;

454 const QMetaObject *m = this;

455 while (m && i < 0) {

456 for (i = priv(m->d.data)->methodCount-1; i >= 0; --i)

457 if ((m->d.data[priv(m->d.data)->methodData + 5*i + 4] & MethodTypeMask)

== MethodSlot

458 && strcmp(slot, m->d.stringdata

459 + m->d.data[priv(m->d.data)->methodData + 5*i]) == 0) { 460 i += m->methodOffset();

461 break;

462 }

463 m = m->d.superdata;

464 }

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

465 return i;

466 }

这个函数的目的是通过槽的名字找到并返回槽的索引,如果找不到则返回-1。[www.61k.com)首先我们进入一个大的while循环,455行和463行表示如果在该类中找不到,则继续从该类的父类中查找,还找不到则继续从父类的父类中查找,直到顶层的类。

while循环之下是456行的for循环,i被初始化为priv(m->d.data)->methodCount-1,即method的数目减1,并且每循环一轮,i自减1,一直到i变成0为止。在循环体中每一轮都检测method数据区的一行是不是所寻找的槽,并且从最下面的一个method开始比对。

第457行用来检查这个method的flag值是不是槽,这里priv(m->d.data)->methodData的值是method数据区的偏移量,加上5*i 之后则这个偏移值到了第i+1个method的开头,在加上4则可以取得该method的flag值。然后再与MethodTypeMask相与,这是我们前面介绍过的解析的方法,与MethodSlot即可以判断这个method是不是一个槽。

查询的另外一个条件,即槽的名字是不是匹配,在458行和459行用strcmp函数来判断,它的第一个参数是所要查询的槽的名字,第二个参数则需要我们从stringdata中去解析,这需要结合data数组中的信息,来确定从stringdata中哪一个位置开始提取字符串。与前面的457行类似,只是我们这次找的不是flag,而是signature,因而从m->d.data[priv(m->d.data)->methodData + 5*i]可直接获得数值。这里我们应该明白为什么要用”\0”来分隔stringdata了吧,类似strcmp,很多c语言中与字符串处理相关的函数在比较或识别字符串的时候遇到”\0”即认为结束,这样我们可以方便的提取某一个串。

第460行是找到了之后的动作,用i + m->methodOffset()来作为最后的返回值,m->methodOffset()实际上是所有父类的method的个数,在Qt的元对象信息中子类的method放在所有父类的method之后,在计算索引(包括计算信号或者槽以及所有的method的索引)的时候需要加上这个值。在找到了之后我们通过461行的break语句程序退出for循环,并且由于i的值将>=0,也会退出while循环,程序将返回索引值i。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

QMetaObject::indexOfSlot()是QMetaObject类中一个很常用的函数,在下一小节中我们将看到,它在信号和槽的实现中被用来定位槽的位置。类似的在QMetaObject中还有一些重要的成员函数,在了解了这种结合stringdata和data的实现机制之后有兴趣的读者可自行阅读相关代码。

11.2.2 信号和槽机制的实现

在充分理解了QMetaObject类之后,我们来看看如何利用QMetaObject类提供的信息来实现信号和槽。我们可以简单的猜想实现信号和槽机制所需要的一些步骤:

1. 用connection()建立连接的时候需要保存这一个连接的相关信息

2. 发射信号之后,需要通过前面保存的连接来查找信号所对应的槽,并且执行它 同样这种内部实现机制是不会有官方文档详细讲述的,我们还是通过Qt的源代码来探讨一下吧。

11.2.2.1 用connection()建立连接 connection()是QObject的成员函数,在qobject.h中可以找到它的原型:

174 static bool connect(const QObject *sender, const char *signal,

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

175 const QObject *receiver, const char *member, Qt::ConnectionType = 176 #ifdef QT3_SUPPORT

177 Qt::AutoCompatConnection

178 #else

179 Qt::AutoConnection

180 #endif

181 );

回顾一下我们在上一节中的连接实例,比如signals_slots.cpp中的代码:

29 QObject::connect(&s, SIGNAL(transmit(int)), &r, SLOT(printNumber(int)));

30 QObject::connect(&s, SIGNAL(transmit(int)), &m, SIGNAL(send(int)));

对照上面的connect()函数的原型,第一个和第三个参数都是指向QObject对象的指针,最后一个参数ConnectionType一般都采用默认值,这些比较好理解;第二个和第四个参数要求是两个指向字符的指针即字符串,那么在我们的实例中如何将SIGNAL(transmit(int))和SLOT(printNumber(int))变成字符串呢?这就是宏SIGNAL和SLOT所起的作用了,在qobjectdefs.h中我们可以找到如下的定义:

147 #define METHOD(a) "0"#a

148 #define SLOT(a) "1"#a

149 #define SIGNAL(a) "2"#a

注意在#define表达式中,参数左边的符号”#”表示宏展开时,这个参数将加上一对双引号,比如常见的C语言编程中用到的例子:

#define dprint(expr) printf(#expr " = %g\n", expr)

这个宏dprint可以更加方便的输出打印信息。(www.61k.com]当使用dprint(abc)时,宏被展开为: printf("abc" " = &g\n", abc);

由于两个字符串会自动合并起来,最后的结果就是我们所需要的:

printf("abc = &g\n", abc);

宏SIGNAL和SLOT的用法与上面类似,前面的数字0,1,2用来标记是哪一种,而后面就将宏所包含的参数展开成带引号的字符,所以比如SIGNAL(transmit(int))展开后就会变成”1 transmit(int)”。这样我们就可以理解第二个和第四个参数的含义了。

那么connect()函数究竟是怎么实现的呢?这个函数比较大,共有200多行,为节省篇幅我们仅仅来分析其中的一些关键部分:

2368 bool QObject::connect(const QObject *sender, const char *signal,

2369 const QObject *receiver, const char *method,

2370 Qt::ConnectionType type)

2371 {

...

2396 QByteArray tmp_signal_name;

2397

2398 if (!check_signal_macro(sender, signal, "connect", "bind"))

2399 return false;

2400 const QMetaObject *smeta = sender->metaObject();

2401 ++signal; //skip code

2402 int signal_index = smeta->indexOfSignal(signal);

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

2403 if (signal_index < 0) {

2404 // check for normalized signatures

2405 tmp_signal_name =

QMetaObject::normalizedSignature(signal).prepend(*(signal - 1)); 2406 signal = tmp_signal_name.constData() + 1;

2407 signal_index = smeta->indexOfSignal(signal);

2408 if (signal_index < 0) {

2409 err_method_notfound(QSIGNAL_CODE, sender, signal, "connect"); 2410 err_info_about_objects("connect", sender, receiver);

2411 return false;

2412 }

2413 }

...

我们省略了前面检查输入的指针是否为空等代码,所列出的这一段代码的主要目的是通过调用indexOfSignal()来查询signal的索引。[www.61k.com)在2398行调用了check_signal_macro()函数来检查输入的参数是不是一个合法的signal格式的字符串,主要是检查第一个字符是否为”2”,即前面我们定义SIGNAL 宏时所加上去的数字标记。所以在这里如果我们传入的参数不用宏SIGNAL 包含起来的话很可能是通不过的。在2401行++signal的含义可能稍有些费解,其实了解了前面宏SIGNAL的定义的话就非常简单了,就是要略过标记的数字,直接跳到表示信号名的字符,这样signal变量就可以作为indexOfSignal()函数的参数了。indexOfSignal()与我们前面分析过的indexOfSlot()的作用和实现都非常类似,它返回我们所查找的信号在元对象系统中的索引值。2403行至2413行处理稍微复杂一些的情况——如果所传入的信号名比较复杂,可能带有数个参数,字符串之间可能有空格什么的,这时先用QMetaObject::normalizedSignature()函数来处理一下变成标准的格式,再调用indexOfSignal()查找,如果还找不到就返回false了。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

下面的一段代码与上面类似,主要通过indexOfSlot()来查询slot的索引。 2415 QByteArray tmp_method_name;

2416 int membcode = method[0] - '0';

2417

2418 if (!check_method_code(membcode, receiver, method, "connect"))

2419 return false;

2420 ++method; // skip code

2421

2422 const QMetaObject *rmeta = receiver->metaObject();

2423 int method_index = -1;

2424 switch (membcode) {

2425 case QSLOT_CODE:

2426 method_index = rmeta->indexOfSlot(method);

2427 break;

2428 case QSIGNAL_CODE:

2429 method_index = rmeta->indexOfSignal(method);

2430 break;

2431 }

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

2432 if (method_index < 0) {

2433 // check for normalized methods

2434 tmp_method_name = QMetaObject::normalizedSignature(method); 2435 method = tmp_method_name.constData();

2436 switch (membcode) {

2437 case QSLOT_CODE:

2438 method_index = rmeta->indexOfSlot(method);

2439 break;

2440 case QSIGNAL_CODE:

2441 method_index = rmeta->indexOfSignal(method);

2442 break;

2443 }

2444 }

2445

2446 if (method_index < 0) {

2447 err_method_notfound(membcode, receiver, method, "connect");

2448 err_info_about_objects("connect", sender, receiver);

2449 return false;

2450 }

...

2479 QMetaObject::connect(sender, signal_index, receiver, method_index, type, types); 2480 const_cast<QObject*>(sender)->connectNotify(signal - 1);

2481 return true;

2482 }

前面的check_method_code()函数也检查slot字符串的第一个字符,这里比较有意思的一行是2416行,它将slot字符串的第一个字符转变成数字,对比前面的check_signal_macro()函数,在调用前是不需要这么做的,类似的转换在check_signal_macro()函数中完成,这种不一致的原因大概是后面的2424行还需要用到2416行得到的membcode吧。[www.61k.com)由于信号也可以被连接到信号,所以2424行到2431行对连接到的对象是信号还是槽分别作了处理,如果找不到的话同样用QMetaObject::normalizedSignature()处理之后再找一遍,还找不到则返回false。2450和2479行之间还有一些检查信号和槽的参数的一致性等的相关代码我们省略了,跳到2479行可以看到我们前面的主要工作只不过是得到了signal_index和method_index,然后再调用QMetaObject::connect()函数。

继续看QMetaObject::connect()函数,它也被定义在qobject.cpp中:

2731 bool QMetaObject::connect(const QObject *sender, int signal_index,

2732 const QObject *receiver, int method_index, int type, int *types)

2733 {

2734 QConnectionList *list = ::connectionList();

2735 if (!list)

2736 return false;

2737 QWriteLocker locker(&list->lock);

2738 list->addConnection(const_cast<QObject *>(sender), signal_index,

2739 const_cast<QObject *>(receiver), method_index, type,

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

types);

2740 return true;

2741 }

这个函数的主要内容就是2738行的addConnection(),即增加一个连接到list对象。(www.61k.com]那么list具体是什么呢?它在2734行用一个全局函数connectList()取得,相关的代码如下:

在qobject.cpp中实际上用宏Q_GLOBAL_STATIC定义了函数connectList():

105 Q_GLOBAL_STATIC(QConnectionList, connectionList)

在qglobal.h中与宏Q_GLOBAL_STATIC相关的代码: 1294 template <typename T>

1295 class QGlobalStatic

1296 {

1297 public:

1298 T *pointer;

1299 inline QGlobalStatic(T *p) : pointer(p) { }

1300 inline ~QGlobalStatic() { pointer = 0; }

1301 };

1302

1303 #define Q_GLOBAL_STATIC(TYPE, NAME) \ 1304 static TYPE *NAME() \ 1305 { \ 1306 static TYPE this_##NAME; \ 1307 static QGlobalStatic<TYPE > global_##NAME(&this_##NAME); \ 1308 return global_##NAME.pointer; \ 1309 }

1310

这些代码结合起来实际上就是定义了一个静态的QConnectionList对象,只不过将它封装到了类QGlobalStatic中,然后可以调用一个全局函数来返回,这种设计非常巧妙,比起单纯的使用全局变量要好多了,我们在平时的开发中也可以借鉴。需要说明的是上面的这段代码只是QT_NO_THREAD被定义时才使用的,需要支持线程时它的实现稍微复杂一点,不过其作用是同样的,我们不再赘述。

取得全局的QConnectionList对象之后,如何来保存这个连接的相关信息呢?这主要涉及到QConnection和QConnectionList两个类:

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

74 struct QConnection {

75 QObject *sender;

76 int signal;

77 QObject *receiver;

78 int method;

79 uint refCount:30;

80 uint type:2; // 0 == auto, 1 == direct, 2 == queued

81 int *types;

82 };

83 Q_DECLARE_TYPEINFO(QConnection, Q_MOVABLE_TYPE);

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

84

85 class QConnectionList

86 {

87 public:

88 QReadWriteLock lock;

89

90 typedef QMultiHash<const QObject *, int> Hash;

91 Hash sendersHash, receiversHash;

92 QList<int> unusedConnections;

93 typedef QList<QConnection> List;

94 List connections;

95

96 void remove(QObject *object);

97

98 void addConnection(QObject *sender, int signal,

99 QObject *receiver, int method,

100 int type = 0, int *types = 0);

101 bool removeConnection(QObject *sender, int signal,

102 QObject *receiver, int method);

103 };

结构体QConnection用来保存一个连接的相关信息,而QConnectionList则用来存储所有的QConnection对象。(www.61k.com)如果我们在代码中加入一个connect()函数,则全局的QConnectionList对象调用addConnection()添加一个连接。注意上面的91行还定义了两个哈希表对象作为成员变量,在addConnection()函数中用它们来记录索引值。addConnection()函数的实现也在qobject.cpp中,如下:

157 void QConnectionList::addConnection(QObject *sender, int signal,

158 QObject *receiver, int method, 159 int type, int *types)

160 {

161 QConnection c = { sender, signal, receiver, method, 0, 0, types };

162 c.type = type; // don't warn on VC++6

163 int at = -1;

164 for (int i = 0; i < unusedConnections.size(); ++i) {

165 if (!connections.at(unusedConnections.at(i)).refCount) {

166 // reuse an unused connection

167 at = unusedConnections.takeAt(i);

168 connections[at] = c;

169 break;

170 }

171 }

172 if (at == -1) {

173 // append new connection

174 at = connections.size();

175 connections << c;

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

176 }

177 sendersHash.insert(sender, at);

178 receiversHash.insert(receiver, at);

179 }

需要增加连接时,首先在164行-171行检查这个连接是不是在unusedConnections中(当我们disconnect()一个连接时,它会保存在unusedConnections中),如果是则直接重用,如果不是则在175行添加新的连接,最后在177行和178行用哈希表在保存与sender和receiver对象相关的索引at,这样以后可以根据sender或receiver比较快的找到它们对应的连接。(www.61k.com)

总结一下,connect()函数所作的工作,其实就是将这个连接相关的信息存储起来,主要步骤和相关的重要函数大概有:

(1). 调用indexOfSignal()和indexOfSlot()分别得到所传入的信号和槽的索引值signal_index和method_index;

(2). 将signal_index和method_index传入QMetaObject::connect()函数中,取得全局的QConnectionList对象并为其增加连接;

(3). QConnectionList::addConnection()真正的保存连接的相关信息到全局的QConnectionList对象中,并且将sender和receiver对象相关的索引值保存在两个哈希表中,以便于以后的快速查找。

connect()函数大概完成了一半的工作,剩余的另一半的疑问是用emit发射信号之后,槽如何被执行的呢?

11.2.2.2 信号的发射和槽的执行

在利用connect()函数保存了连接的相关信息的基础上,信号的发射和槽的执行就显得比较简单一些了,我们的主要工作是要找到与信号对应的槽并执行它。

先来看看信号是怎么发射的。在我们前面的例子中,发射信号就是用emit后面接信号名,而信号则用”signals”来声明,这些Qt的关键字是如何被定义的呢?我们可以在qobjectdefs.h中找到下面的代码:

41 // The following macros are our "extensions" to C++

42 // They are used, strictly speaking, only by the moc.

43

44 #ifndef Q_MOC_RUN

45 # if defined(QT_NO_KEYWORDS)

46 # define QT_NO_EMIT

47 # else

48 # define slots

49 # define signals protected

50 # endif

51 # define Q_SLOTS

52 # define Q_SIGNALS protected

53 # define Q_PRIVATE_SLOT(d, signature)

54 #ifndef QT_NO_EMIT

55 # define emit

56 #endif

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

在55行定义了emit,48行和49行定义了slots和signals,从41行和42的注释我们可以看到,这些Qt的关键字都只是被moc工具所使用,如果被gcc预编译时,emit和slots都是空的,只有signals被定义成protected,这也符合我们前面介绍过的信号的访问权限特性,即发射信号只能由定义了一个信号的类和它的子类才能来完成。(www.61k.com)当然moc工具是可以识别这些关键字的,所以它才能为我们生成类似moc_myclass.cpp的文件。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

既然emit被定义成空,那么发射信号时如何才会调用对应的槽呢?这不仅需要Qt源代码,还需要到moc工具为我们生成的文件中去寻找答案。沿用前面的小例子,我们来看看moc_signals_slots.cpp:

181 void Sender::transmit(int _t1)

182 {

183 void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) }; 184 QMetaObject::activate(this, &staticMetaObject, 0, _a);

185 }

可以发现在moc_signals_slots.cpp中moc工具将信号transmit实现为一个函数,这也是为什么我们在前面谈到信号的用法时强调信号只需要声明,而不能真正实现它,因为它的实现是由moc工具自动来完成的。结合前面emit和signals的定义我们可以看出,其实信号的发射就是调用了对应的函数:比如signals_slots.cpp文件中的第18行emit transmit(10),由于emit实际上被展开后为空,这行代码被gcc识别时就变成了transmit(10),而信号或者说函数transmit(int )在moc_signals_slots.cpp被实现,于是transmit信号的发射相当于执行了上面的183和184行。183行将参数_t1转换成了void*格式,以便之后的统一处理,而184行则通过activate()函数来真正调用相关的槽。

来看看QMetaObject::activate()函数具体怎样调用相关的槽。184行所调用的activate()函数的定义如下:

2979 void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index,

2980 void **argv)

2981 {

2982 int offset = m->methodOffset();

2983 activate(sender, offset + local_signal_index, offset + local_signal_index, argv); 2984 }

2982行的methodOffset()我们在前面讲解QMetaObject::indexOfSlot()时有过介绍,它返回m的所有父类的method的个数,在计算索引值的时候我们总是需要加上这个值。然后2983行调用的重载的activate()函数QMetaObject::activate(QObject *sender, int from_signal_index, int to_signal_index, void **argv),输入参数from_signal_index和to_signal_index的值都是offset + local_signal_index,即只激活索引值为offset + local_signal_index的信号。这个重载的activate()函数也比较长,我们挑一些重要代码来说明一下:

2863 void QMetaObject::activate(QObject *sender, int from_signal_index, int to_signal_index, void **argv)

2864 {

...

2868 QConnectionList * const list = ::connectionList();

2869 if (!list)

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

2870 return;

...

2880 QConnectionList::Hash::const_iterator it = list->sendersHash.constFind(sender); 2881 const QConnectionList::Hash::const_iterator start = it;

2882 const QConnectionList::Hash::const_iterator end = list->sendersHash.constEnd(); ...

2895 int i = 0;

2896 for (it = start; it != end && it.key() == sender; ++it) {

2897 ++i;

2898 }

2899 QVarLengthArray<int> connections(i);

2900 for (i = 0, it = start; it != end && it.key() == sender; ++i, ++it) {

2901 connections.data()[i] = it.value();

2902 ++list->connections[it.value()].refCount;

2903 }

2905 for (i = 0; i < connections.size(); ++i) {

2906 const int at = connections.constData()[connections.size() - (i + 1)];

2907 QConnectionList * const list = ::connectionList();

2908 QConnection *c = &list->connections[at];

2909 --c->refCount;

2910 if (!c->receiver || ((c->signal < from_signal_index || c->signal > to_signal_index) && 2911 c->signal != -1))

continue;

...

2924 const int method = c->method;

...

2936 #if defined(QT_NO_EXCEPTIONS)

2937 c->receiver->qt_metacall(QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);

2938 #else

...

2949 #endif

...

2961 }

...

2967 }

首先我们还是在2868行利用全局函数connectionList()得到全局的QConnectionList对象,在上一节中我们介绍过的connect()函数就是将连接保存在这个全局对象中,这里我们来读取它所保存的值。(www.61k.com)接下来从2880行开始我们从哈希表中根据键sender指针来寻找之前保存的索引值,注意这个索引值对应的是list对象中所保存的连接的位置。由于一个信号可能被连接到多个槽或者信号,我们需要把它们都找出来。2895行-2903行用来找到与sender相连所有信号或者槽,并将索引值保存到相当于数组的connections中。接下来从2905行对这些连接一一比对,2910行根据几个条件来判断连接是否有效,即如果receiver指针为空,或者信号的索引值不在参数from_signal_index和to_signal_index所指定的范围内,则略过这

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

个连接;如果连接有效的话,在2937行将通过qt_metacall()来执行相应的信号或者槽。(www.61k.com)

那么qt_metacall()如何来通过索引值找到相应的信号或者槽呢?还记得我们在前面介绍过的Q_OBJECT宏的定义吗?对的,函数qt_metacall()就是通过展开Q_OBJECT宏来声明的,它的实现则被放在moc工具所生成的cpp文件中,比如moc_signals_slots.cpp中我们可以看到如下的代码: 57 int Receiver::qt_metacall(QMetaObject::Call _c, int _id, void **_a)

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

58 {

59 _id = QObject::qt_metacall(_c, _id, _a);

60 if (_id < 0)

61 return _id;

62 if (_c == QMetaObject::InvokeMetaMethod) {

63 switch (_id) {

64 case 0: printNumber((*reinterpret_cast< int(*)>(_a[1]))); break;

65 }

66 _id -= 1;

67 }

68 return _id;

69 }

在第59行我们先调用父类的qt_metacall()函数。由于每个类的qt_metacall()函数都会首先去调用父类的qt_metacall()函数,其结果是首先调用顶层父类的qt_metacall()函数,然后一级一级往下。我们调用信号或者槽时,第一个参数输入的是QMetaObject::InvokeMetaMethod,这样进入63行开始的switch-case条件判断,输入的_id值即为索引值,而且调用完成之后在66行索引值会减去这个类中的信号和槽的个数的值,这样每一级中调用qt_metacall()函数都会将索引值_id减去相应的的个数的值。

我们用这个例子的具体数值来说明一下,Receiver的父类是QObject,QObject就是顶层的类,只有两层,层次结构比较简单。QObject中method的个数为4个(我们可以从源代码包中找到moc_qobject.cpp,然后查看它的data数组的定义,采用前面所讲述过的indexOfSlot()中的计算方法,即可得到method的个数),而Receiver类中仅有1个,即槽printNumber(),它在Receiver类的本地索引值就是0,则通过indexOfSlot()计算出的索引值为4+0=4。这样在调用qt_metacall()时所传入的参数_id的值也为4,而进入qt_metacall()后,先在59行调用QObject::qt_metacall()并返回_id,这时得到的_id在QObject::qt_metacall()被减去4,所以59行返回的_id的值为0,然后进入switch-case在第64行调用printNumber()函数。

简单的回顾一下信号发射之后如何找到并执行所连接的槽(或信号)的这种机制:

(1). 我们声明的信号在moc所自动生成的moc_myclass.cpp中实现为protected的成员函数,而emit比定义为空,发射信号实际上就是调用moc自动生成的成员函数;

(2). moc自动生成的成员函数中调用了QMetaObject::activate()函数来寻找并调用相应的信号或槽;

(3). activate()函数通过connect()函数中所保存的全局QConnectionList对象来查找对应的信号或槽,并且利用的哈希表所保存的位置的索引值来快速定位

(4). activate()函数中找到所保存的receiver指针和信号或槽的索引值之后,通过虚函数qt_metacall()来真正调用相应的信号或槽。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

11.2.3 元对象编译器moc

我们已经在前面多次谈到并用到了moc(Meta-Object Compiler,元对象编译器)。[www.61k.com)moc工具可以看成是一个C++预处理程序,用来扩展C++的特性,比如前面我们详细讲述的信号和槽的机制,它并不是标准C++的特性,但经过moc的预处理之后,所有信号和槽相关的代码都被“翻译”成了标准的C++,从而能够被gcc等编译。

11.2.3.1 在Makefile中使用moc

我们通常使用Qt提供的qmake工具来自动生成Makefile,这在我们前面的例子中多次用到,这样做的好处是不用自己手动去添加很多Qt需要的规则,比如调用moc工具等。

如果在某些情况下需要手动编写Makefile时,我们可以用下面的规则来调用moc: moc_%.cpp: %.h

moc $< -o $@

我们已经在前面的章节中介绍过Makefile的规则,这里的含义是对所有头文件*.h,利用moc进行处理并得到moc_*.cpp。生成moc_*.cpp之后不要忘记你还同样需要对它进行编译和链接,所以你需要将它加到SOURCES和OBJECTS(可能是其他类似含义的变量)中。

如果你对这种Makefile的写法还不太熟悉,可以参考qmake自动生成的Makefile,依葫芦画瓢就可以了。

11.2.3.2 moc用法详解

moc支持的如下所示的一些命令行选项:

-o file

将输出写到参数file(不指定的话将写到标准输出)。

-f [<file>]

强制在输出文件中生成#include声明。这个选项在你的头文件没有遵循标准命名法则的时候才有用——当头文件的扩展名以H或h开始时,#include声明会自动生成,不需要使用-f 选项。文件名<file>为可选项。

-i

不在输出文件中生成#include声明,与-f正好相反。当一个C++文件包含一个或多个类声明的时候可能用到。

-nw

不产生任何警告。不建议使用。

-p <path>

在元对象编译器生成的#include声明的文件名称中添加路径<path>/。

-I <dir>

在头文件的包含路径中添加<dir>目录。

-E

不生成元对象相关代码(仅用于预编译)

-D<macro>[=<def>]

定义宏<macro>,或者定义宏<macro>=<def>,后者为可选项

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

-U<macro>

取消宏<macro>的定义

-h

显示moc的用法和选项

-v

显示moc的版本

我们还可以使用”#ifndef Q_MOC_RUN”来告诉moc工具不要处理某些代码。[www.61k.com)比如: #ifndef Q_MOC_RUN

...

#endif

则moc会忽略定义在省略号部分的代码。实际上我们在前面分析过的源代码中见识过这个宏,定义signals/slots/emit等Qt关键字的时候,在qobjectdefs.h的第44行即用到了它。 11.2.3.3 moc及信号和槽机制的局限性

moc并不能处理所有的C++特性。我们可以回想上一节的元对象系统及信号和槽的实现机制,这些实现中很少处理C++的重要特性之一——模板,是的moc对模板的支持非常有限,对预编译宏的处理也不够完善。由于信号和槽是基于元对象系统来实现的,moc的局限性也可以看作是信号和槽的局限性,这是我们在使用信号和槽的机制时需要特别注意的。

具体说来,moc或者说信号和槽有如下一些限制: 1. 模板类不能含有信号和槽。比如下面的例子是不支持的:

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

// 错误的用法

class SomeTemplate<int> : public QFrame {

Q_OBJECT

...

signals:

void bugInMocDetected( int );

};

2. QObject(或其子类)作为多重继承的父类之一时,需要把它放在第一个。

如果你使用多重继承,moc在处理时假设首先继承的类是QObject的一个子类,也就是说,我们需要确保首先继承的类是QObject或其子类。示例如下:

class SomeClass : public QObject, public OtherClass {

...

};

3. 函数指针不能作为信号和槽的参数。下面是一个不符合语法的例子:

class SomeClass : public QObject {

Q_OBJECT

...

public slots:

// 不合法的

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

void apply( void (*apply)(List *, void *), char * );

};

对于这种情况,即需要使用函数指针作为信号/槽的参数的情形,一般可以考虑使用继承和虚函数等替代,通常来说使用继承和虚函数是更好的面向对象的实现方法。[www.61k.com)如果一定需要函数指针的话,也可以使用typedef来满足moc的处理过程,比如下面的用法是能够被moc接受的:

typedef void (*ApplyFunctionType)( List *, void * );

class SomeClass : public QObject {

Q_OBJECT

...

public slots:

void apply( ApplyFunctionType, char * );

};

4. enum和typedef变量在作为信号和槽的参数时必须用全名。

由于moc在检查信号和槽的参数时,是逐个字母来比较的,比如Alignment和Qt::Alignment被moc视为不同的参数类型。这个限制主要是Qt 4.0引入命名空间导致修改moc带来的,我们应对的方法是任何时候都在信号和槽的参数中使用全名,比如: class MyClass : public QObject

{

Q_OBJECT

enum Error {

ConnectionRefused,

RemoteHostClosed,

UnknownError

};

signals:

void stateChanged(MyClass::Error error); //Error类型使用全名MyClass::Error };

5. 带参数的宏不能被用于信号和槽的参数。

这主要是因为moc并不展开#define,使用带参数的宏在moc处理的过程中无法被有效的识别。下面是一个不合法的例子:

#ifdef ultrix

#define SIGNEDNESS(a) unsigned a

#else

#define SIGNEDNESS(a) a

#endif

class Whatever : public QObject {

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

...

signals:

void someSignal(SIGNEDNESS(int)); //不合法的参数SIGNEDNESS(int)

...

};

不过不含有参数的宏是可以正常工作的,moc处理的时候把它当作一个普通变量一样就可以了。[www.61k.com)

6. 嵌套类不能含有信号和槽

moc无法处理嵌套类中的信号和槽,它的实现机制中没有完整的考虑嵌套类的情况。下面是一个错误的例子:

class A {

Q_OBJECT

public:

class B {

public slots: // 错误的用法

void b();

...

};

signals:

class B { // 错误的用法

void b();

...

}:

};

11.3 Qt的窗口系统

这一节我们主要来介绍Qt的窗口系统中为我们管理内存提供了很多便利的部件之间的树型结构,以及灵活的布局管理机制。

11.3.1 窗口部件之间的树型结构

Qt的窗口系统的重要特性之一就是我们在第9章提到过的树型结构,即所有的窗口部件本身也是容器,一个窗口部件可包含任意数量的子部件,子部件在父部件的区域内显示,父部件和子部件之间形成一种树型结构以便于维护——当父部件被删除的时候,它所包含的所有子部件都会被自动删除,这大大的方便了部件之间的内存管理。

这种部件之间的树型结构主要得益于我们前面提到的Qt元对象系统,元对象系统除了实现Qt中一些重要的机制比如信号和槽以及属性等等,也附带的利用了QObject作为顶级父类的这一前提,来实现子部件的自动删除机制。事实上不仅是窗口部件之间有这种树型结构,对所有从QObject继承而来的

要注意区分这种树型结构与父类子类所形成的类似结构:窗口部件之间的树型结构是一

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

种对象之间的关系(而不是类之间的关系),我们说父部件与子部件并不意味着它们分别是父类与子类的对象。[www.61k.com)

我们通过一个实际的例子来详细了解一下。为简单起见我们不采用图形部件,仅使用控制台输出来说明子部件是如何自动销毁的: 1 #include <QApplication>

2 #include <QtDebug>

3

4 class TreeMem : public QObject

5 {

6 public:

7 TreeMem(QObject *parent = 0, const QString& name = "")

8 : QObject(parent)

9 {

10 setObjectName(name);

11 qDebug() << "Created: " << objectName();

12 }

13

14 ~TreeMem()

15 {

16 qDebug() << "Deleted: " << objectName();

17 }

18

19 void printName()

20 {

21 qDebug() << "Print Name: " << objectName();

22 }

23 };

24

25 int main( int argc, char **argv )

26 {

27 QApplication a( argc, argv );

28

29 TreeMem top(0, "top");

30 TreeMem *x = new TreeMem(&top, "x");

31 TreeMem *y = new TreeMem(&top, "y");

32 TreeMem *z = new TreeMem(x, "z");

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

33

34 top.printName();

35 x->printName();

36 y->printName();

37 z->printName();

38

39 return 0;

40 }

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

我们定义了类TreeMem来追踪内存释放的过程,在构造函数中增加参数name用来标记不同的对象,在析构函数和函数printName()用qDebug来打印出对象的名字。(www.61k.com]

下面的main()函数中,第29行我们定义了顶层对象,然后定义x, y, z三个对象。这里我们所说的父对象就是指的参数QObject *parent的设定:x, y以top为父对象,而z以x为父对象,这样它们之间的树型关系如图11.3:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图11.3 对象之间的树型关系

由于top定义在栈上,当程序运行到它的作用范围之外时,这个对象就会被自动销毁,这是C++的基本特性之一。当top被自动析构时,它的所有子对象也都会被自动删除,我们来对照一下运行的结果: $ ./treemem

Created: "top"

Created: "x"

Created: "y"

Created: "z"

Print Name: "top"

Print Name: "x"

Print Name: "y"

Print Name: "z"

Deleted: "top"

Deleted: "x"

Deleted: "z"

Deleted: "y"

可以看到正如我们所期望的,x, y, z也都被自动析构了。注意z的析构在y之前,这说明它的析构被当作x析构的一部分来执行,因而比y的析构要早。

这里我们定义的x, y, z几乎没有其他实际意义,其实通常我们创建的是以QWidget为父类的部件对象,读者朋友很快会在Qt的实际开发应用中发现,以这种树型结构为基础的各部件之间的内存管理非常方便,可以经常用到。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

11.3.2 窗口部件的布局管理(Layout)

尽管Qt提供了设计器(Designer)来帮助可视化的开发,但在嵌入式开发中我们往往需要更加精确也更加具有效率的方式来安排各个窗口部件的位置及大小,这些要求可以利用Qt所提供的布局管理器来很好的完成。(www.61k.com)在上一章的小例子中我们已经学习了一些基本的布局管理的知识和用法,这一小节里我们更全面的来介绍Qt的布局管理。

Qt主要提供了下面的四个类来安排部件的位置:

l QHBoxLayout 在水平方向上从左到右(默认情况)或者从右到左布置部件。 l QVBoxLayout 在竖直方向从上往下(默认情况)或者从下往上布置部件。 l QGridLayout 以网格形式布局部件。

l QStackedLayout 以栈的形式来安排一组部件,这组部件每次只有一个显示。 其中比较常用的是前三个,示意图如下面的图11.2,我们分别用了五个按钮部件来作为用法示例,对于其他类型的部件也是类似的:

QHBoxLayout

QVBoxLayout QGridLayout

图11.4 QHBoxLayout,QVBoxLayout和QGridLayout三种布局管理

QHBoxLayout和QVBoxLayout的用法类似,比如上面的QHBoxLayout的图形及水平方向摆放的五个按钮可

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

由下

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

面的代码生成:

1 #include <QApplication>

2 #include <QPushButton>

3 #include <QWidget>

4 #include <QHBoxLayout>

5

6 int main(int argc, char *argv[])

7 {

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

8 QApplication app(argc, argv);

9

10 QWidget window;

11 QPushButton *button1 = new QPushButton("One");

12 QPushButton *button2 = new QPushButton("Two");

13 QPushButton *button3 = new QPushButton("Three");

14 QPushButton *button4 = new QPushButton("Four");

15 QPushButton *button5 = new QPushButton("Five");

16

17 QHBoxLayout *layout = new QHBoxLayout;

18 layout->addWidget(button1);

19 layout->addWidget(button2);

20 layout->addWidget(button3);

21 layout->addWidget(button4);

22 layout->addWidget(button5);

23

24 window.setLayout(layout);

25 window.show();

26

27 return app.exec();

28 }

而需要生成垂直方面摆放的五个按钮时,只需要将上面代码中的QHBoxLayout替换为QVBoxLayout就可以了。[www.61k.com]

QGridLayout稍微有一些不同,因为它在安排部件时需要指定一个二维的坐标来设置布局的位置,而且某些部件可能需要占据两个或多个网格,比如图11.4的”Three”按钮。我们可以用下面的代码来生成图11.4中关于QGridLayout的示意图部分:

1 #include <QApplication>

2 #include <QPushButton>

3 #include <QWidget>

4 #include <QGridLayout>

5

6 int main(int argc, char *argv[])

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

7 {

8 QApplication app(argc, argv);

9

10 QWidget window;

11 QPushButton *button1 = new QPushButton("One");

12 QPushButton *button2 = new QPushButton("Two");

13 QPushButton *button3 = new QPushButton("Three");

14 QPushButton *button4 = new QPushButton("Four");

15 QPushButton *button5 = new QPushButton("Five");

16

17 QGridLayout *layout = new QGridLayout;

18 layout->addWidget(button1, 0, 0);

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

19 layout->addWidget(button2, 0, 1);

20 layout->addWidget(button3, 1, 0, 1, 2);

21 layout->addWidget(button4, 2, 0);

22 layout->addWidget(button5, 2, 1);

23

24 window.setLayout(layout);

25 window.show();

26

27 return app.exec();

28 }

注意addWidget的前两个参数用来指定二维坐标即第几行和第几列,行和列的计数都从0开始,而类似layout->addWidget(button3, 1, 0, 1, 2)的代码,后面的两个参数用来指定部件在纵向和横向分别占据几个网格。[www.61k.com)

QStackedLayout提供的功能与常见的Tab部件有些类似,在某些情况下如果需要控制多组部件的可见性时可以提供方便。

对于这几个布局管理的类,需要注意addWidget方法以及父部件的设定情况,以确定部件是否会被自动销毁。上面的示例代码中,我们初始化几个按钮对象的时候都没有设定它们的父部件,也没有显式的删除它们,那么它们是如何被管理的呢?依靠addWidget()函数和后来的setLayout()函数——addWidget()函数将自动删除对象的任务暂时的交给了这些布局对象,当然这并不是说它们的父部件是布局对象本身,实际情况是当布局对象被setLayout()函数设置为某个部件的布局风格时,比如上面代码中的window->setLayout(layout),这时这些按钮对象的父部件将被自动设置为window对象,当window对象被析构时,正如上一节所介绍的,这些按钮对象也被自动销毁。

我们还可以利用addWidget()函数来设置部件的伸展性,比如QBoxLayout类(注意它是QHBoxLayout和QVBoxLayout的父类)的addWidget()函数的原型如下:

void addWidget(QWidget * widget, int stretch = 0, Qt::Alignment alignment = 0) 其中第二个参数stretch可以用来设置部件的伸展性,如果布局管理对象中的部件都使用默认值0的话,部件通过QWidget::sizePolicy()来设置大小,如果设置为其他值,则当部件可伸展时布局管理对象中的各个部件将按照stretch的比例来调整大小。比如我们将上面的代码稍作修改来调整QHBoxLayout的水平布局: 17 QHBoxLayout *layout = new QHBoxLayout;

18 layout->addWidget(button1, 1);

19 layout->addWidget(button2, 2);

20 layout->addWidget(button3, 3);

21 layout->addWidget(button4, 4);

22 layout->addWidget(button5, 5);

23

24 window.setLayout(layout);

25 window.setGeometry(20, 50, 700, 100);

26 window.show();

这样当调整按钮的大小时,按钮5应该是最长的,注意我们在25行手动设置了一下部件window的大小,这样各个按钮不再是原来默认的大小,我们可以观察它们的伸展程度了。编译运行之后的结果如图11.5:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图11.5 布局管理时的伸缩性示例

另外我们还可以通过QBoxLayout::setStretchFactor()在部件加入到布局管理对象之后来设置其伸缩性。(www.61k.com]对于QGridLayout设置伸展性的方法也是类似的,不再重复。

11.4 国际化

一般来说,程序或者软件系统的国际化,指的是如何使所开发的软件在不重写源代码的情况下能够支持多种语言,在英语中我们用internationalization或者它的简写形式i18n(数字18表示i和n之间有18个字母)来表示。

在Linux/Unix系统中,现在流行的国际化方法是使用GNU的gettext套件。利用gettext套件,程序员将需要翻译的字符串用gettext()函数来取得,而由翻译人员协助提供翻译后的资源文件*.po,在一次编译之后就可以支持多种语言。在这里我们不打算介绍gettext套件的详细内容,有兴趣的读者可参考http://www.gnu.org/software/gettext/。

Qt中的国际化方法与GNU gettext类似,它提供了tr()函数与gettext()函数对应,而翻译后的资源文件则以.qm来命名。如果对GNU gettext熟悉的读者,相信很快就可以了解Qt的国际化机制。不过值得注意的是Qt的国际化的机制与它的元对象系统密切相关,这是与GNU gettext稍微不同的地方。

下面我们简单介绍利用Qt开发国际化程序的基本步骤,并编写一个小例子来了解如何在程序运行的过程中动态改变语言。

11.4.1 Qt国际化的基本步骤

11.4.1.1 程序员的工作

在支持国际化的过程中,程序员的工作一般是在所编写的代码中加入国际化的支持,这在Qt中利用QString、QTranslator等类和tr()函数能够很方便的完成。程序员所需要做的工作有如下一些:

1.使用QString对象来表示所有用户可见的文本。由于QString内部使用Unicode编码来实现,它可以用来表示所有需要向用户呈现的文本。当然对于仅仅程序员可见的文本并不需要都变成QString对象,可利用Qt提供的QCString或者原始的char *。

2.使用tr()函数来取得所有需要翻译的文本。在Qt的翻译机制下,QObject::tr()函数可以帮我们取得翻译之后的文本。对于从QObject继承而来的类,QObject::tr()函数最终由QMetaObject::tr()来实现,我们可以参考11.2.1中提到的Q_OBJECT宏的定义以及在qmetaobject.cpp中QMetaObject::tr()的实现。在某些时候如果无法使用QObject::tr()函数,我们还可以直接调用QCoreApplication::translate()来取得翻译之后的字符串。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

3.使用QString::arg()来组织动态文本。有些时候一段文本需要由一些静态文本和动态变量组合起来,比如常见的情况:printf(“The value of i is: %d”, i)。对于这种动态文本的翻译,

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

由于语言习惯的问题,如果简单的采用这种连接的方法,可能会带来一些问题。(www.61k.com]比如下面的字符串用来表示任务的完成情况:

QString m = tr(“Mission status: “ )+ x + tr(“of “) + y +tr(”are completed”);

其中x和y是动态的变量,三个字符串被x和y分隔开,它们无法被很好的翻译——”x of y”是英语中分数的表示方法,比如4 of 5是分数4/5,在不同的语言中分子和分母的位置可能是颠倒的,这种情况下数字4和5的位置在翻译的时候无法被正确的放置,可见孤立地翻译被分隔开的字符串是不行的,改进的办法是使用QString::arg()方法:

QString m = tr(“Mission status: %1 of %2 are completed”).arg(x).arg(y);

这样翻译工作者可以将整个字符串进行翻译,并将参数%1和%2放到正确的位置。

4.利用QTranslator::load()和QCoreApplication::installTranslator()来读取对应的翻译之后的资源文件。翻译工作者将提供包含有翻译之后的字符串的资源文件*.qm,程序员还需要做的是定义QTranslator对象,并用load()函数读取相应的.qm文件,然后利用QCoreApplication::installTranslator()函数来安装QTranslator对象。我们在后面的小例子中将介绍具体的用法。

11.4.1.2 语言资源管理者和翻译工作者的工作

通常来说组织翻译和管理翻译后的资源文件的工作并不是由编写代码的程序员来完成的,毕竟程序员不可能精通所有语言,翻译的工作通常由专门的工作组来协调管理,并聘请专门的翻译人员来翻译。这方面的工作主要是利用Qt提供的工具lupdate、linguist和lrelease(它们都可以在Qt安装目录的bin文件夹下找到)来协助翻译工作并生成最后需要的.qm文件,包括以下内容:

1.利用lupdate工具从源代码中扫描并提取需要翻译的字符串,生成.ts文件。类似编译时用到的qmake,运行lupdate时我们也需要指定一个.pro的文件,这个.pro文件可以单独创建,也可以利用编译时用到的.pro文件,只需要定义好变量TRANSLATIONS就可以了,具体用法可以参见后面的小例子。

2.利用linguist工具来协助完成翻译工作,即打开前面用lupdate生成的.ts文件,对其中的字符串逐条进行翻译并保存。由于.ts文件采用了xml格式,我们也可以用其它编辑器来打开.ts文件并翻译。

3.利用lrelease工具处理翻译好的.ts文件,生成格式更为紧凑的.qm文件。这便是翻译工作者最终需要提供给程序员的资源文件,它所占的空间比.ts文件小,但基本不具有可读性,只有QTranslator能正确的识别它。

11.4.2 动态改变语言的小例子

下面我们通过一个小例子来具体说明上面提到的利用Qt实现国际化方法。我们设计的界面非常简单,用一个下拉菜单来选择语言,然后下面有一个标签,上面的文字是著名的”Hello World”(或者它的翻译),如图11.6所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

22 void refreshLabel();

23

24 QComboBox* combo;

25 QLabel* label;

26 };

27

28 #endif //LANGSWITCH_H

来看看这些函数在LangSwitch.cpp中的具体实现:

15 void LangSwitch::createScreen()

16 {

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

17 combo = new QComboBox;

18 combo->addItem("English", "en");

19 combo->addItem("Chinese", "zh");

20 combo->addItem("Latin", "la");

21

22 label = new QLabel;

23 refreshLabel();

24

25 QVBoxLayout* layout = new QVBoxLayout;

26 layout->addWidget(combo, 1);

27 layout->addWidget(label, 5);

28

29 setLayout(layout);

30

31 connect(combo, SIGNAL(currentIndexChanged(int)),

32 this, SLOT(changeLang(int)));

33 }

createScreen()用来创建基本的界面。[www.61k.com)首先我们生成了QComboBox和QLabel对象,并设置其内容。18行-20行我们将三个语言选项英语、中文和拉丁语加到了下拉菜单中,并设置三个选项的值分别为”en”、”zh”和”la”(这是ISO标准中语言的简写形式)。23行调用私有函数refreshLabel()设置标签的内容,之所有使用函数函数refreshLabel()是因为我们在后面改变语言的时候还需要重用它。25行-29行用到了我们熟悉的布局管理类QVBoxLayout,31行则将下拉菜单选项变化时发射的信号与我们自定义的槽changeLang()连接起来。

来看看23行用到的函数refreshLabel()的实现:

61 void LangSwitch::refreshLabel()

62 {

63 label->setText(tr("TXT_HELLO_WORLD", "Hello World"));

64 }

这是一个非常简单的实现,不过是用到了我们前面提到过的tr()函数,以获取翻译之后的字符串。前一个参数是提取翻译串时用到的ID,后一个则是提供注释的作用,并且在取不到翻译串时注释串会被采用。比如语言设置为中文时,以TXT_HELLO_WORLD为ID的串在对应的.qm文件中找不到翻译后的字符串的话,就会采用后一个参数,即显示为英文。

最后我们来看看改变语言的具体过程:

35 void LangSwitch::changeLang(int index)

36 {

37 QString langCode = combo->itemData(index).toString();

38 changeTr(langCode);

39 refreshLabel();

40 }

41

42 void LangSwitch::changeTr(const QString& langCode)

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

43 {

44 static QTranslator* translator;

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

45

46 if (translator != NULL)

47 {

48 qApp->removeTranslator(translator);

49 delete translator;

50 translator = NULL;

51 }

52

53 translator = new QTranslator;

54 QString qmFilename = "lang_" + langCode;

55 if (translator->load(qmFilename))

56 {

57 qApp->installTranslator(translator);

58 }

59 }

槽changeLang()中我们先从所选的菜单项中取得对应语言的值(即我们在18-20行中的第二个参数所加入”en”、”zh”和”la”),传给函数changeTr()去读取相应的.qm文件,然后刷新标签上的文字即可。(www.61k.com]而函数changeTr()则负责去读取对应的.qm文件,并调用installTranslator()方法来安装QTranslator对象。由于我们需要动态改变语言,所以如果已经安装了QTranslator对象的话,首先需要调用removeTranslator()移除原来的QTranslator对象,再安装新的,因此在44行我们定义了一个static的QTranslator对象以方便移除和重新安装。另外注意为简单起见我们将.qm文件的路径直接设定在当前路径下,命名分别为lang_en.qm, lang_zh.qm, lang_la.qm。

对于程序员来说,上面的这些工作基本已经足够,剩下的提取需要翻译的字符串并翻译然后生成.qm文件的工作一般有专门的工作组去负责,不过为了完整的了解整个过程,我们也来介绍一下这些工作大致是如何完成的。

首先我们需要对qmake自动生成的langswitch.pro文件稍作修改,在后面加上TRANSLATIONS的定义,如下面的14-16行所示:

1 ################################################################# 2 # Automatically generated by qmake (2.01a) Tue Mar 6 16:04:46 2007

3 ################################################################# 4

5 TEMPLATE = app

6 TARGET =

7 DEPENDPATH += .

8 INCLUDEPATH += .

9

10 # Input

11 HEADERS += LangSwitch.h

12 SOURCES += LangSwitch.cpp main.cpp

13

14 TRANSLATIONS = lang_en.ts \

15 lang_zh.ts \

16 lang_la.ts

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

然后我们可以利用lupdate工具来提取需要翻译的字符串,运行命令及结果如下所示: $ lupdate langswitch.pro

Updating 'lang_en.ts'...

Found 1 source text (1 new and 0 already existing)

Updating 'lang_la.ts'...

Found 1 source text (1 new and 0 already existing)

Updating 'lang_zh.ts'...

Found 1 source text (1 new and 0 already existing)

这时候我们得到了lang_en.ts,lang_la.ts和lang_zh.ts三个文件。[www.61k.com]由于它们都是xml格式的,我们可以直接用cat命令来查看它们的内容,比如: $ cat lang_la.ts

<?xml version="1.0" encoding="utf-8"?>

<!DOCTYPE TS><TS version="1.1">

<context>

<name>LangSwitch</name>

<message>

<location filename="LangSwitch.cpp" line="63"/>

<source>TXT_HELLO_WORLD</source>

<comment>Hello World</comment>

<translation type="unfinished"></translation>

</message>

</context>

</TS>

可以看到ID为TXT_HELLO_WORLD的串此时尚未被翻译。那么接下来的工作就是利用linguist来翻译这几个.ts文件了。如果你的PATH设置正确的话,直接敲入linguist命令就可以启动它,然后打开需要翻译的.ts文件,就可以进行字符串的翻译了,如图11.7所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图11.7 使用linguist来翻译字符串

翻译完一个串之后可以选择菜单”Translation->Done and Next”或者Ctrl加回车键将这个串设置为翻译完成,然后继续下一个(当然这个小例子中我们只有一个串需要翻译)。(www.61k.com]当所有的串都翻译完成后可以保存退出,这时我们可以来查看一下.ts文件的变化: $ cat lang_la.ts

<?xml version="1.0" encoding="utf-8"?>

<!DOCTYPE TS><TS version="1.1" language="en_US">

<context>

<name>LangSwitch</name>

<message>

<location filename="LangSwitch.cpp" line="20"/>

<source>TXT_HELLO_WORLD</source>

<comment>Hello World</comment>

<translation>Orbis, te saluto</translation>

</message>

</context>

</TS>

可以看到行<translation>Orbis, te saluto</translation>,表示这是翻译之后的结果。对于翻译成中文的情况是类似的。

在得到翻译之后的.ts文件后,最后的工作就是使将它们变为更紧凑的格式,即生成相应的.qm文件。这可以利用lrelease来完成:

$ lrelease langswitch.pro

Updating 'lang_en.qm'...

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

Generated 1 translation (1 finished and 0 unfinished)

Updating 'lang_la.qm'...

Generated 1 translation (1 finished and 0 unfinished)

Updating 'lang_zh.qm'...

Generated 1 translation (1 finished and 0 unfinished)

现在所有的准备工作都已经做好了,要编译运行一下我们的程序吧。(www.61k.com)默认情况下的语言是英语,界面正如图11.4所示,我们可以尝试改变语言为中文或者拉丁文,结果如图11.8:

图11.8 中文和拉丁文的界面显示

尽管这个小例子的界面非常简单,不过它完整的实现了动态改变语言的功能,希望能对读者朋友的实际开发起到一定的参考作用。当然稍微复杂一点的软件系统对语言的切换都应该有更加完整和详细的定义,一般应该采用广播事件而不是简单的采用发射信号的方式来处理语言的改变。

11.4.3 一些注意事项

作为程序员我们往往容易忽略.qm文件生成的过程,因为一般来说有一些专门的人员来组织和负责这方面的工作,但事实上由于tr()函数本身以及lupdate等工具的一些局限,很多时候我们会发现语言改变时得不到正确的翻译串,这种问题在大型系统中调试起来也可能比较麻烦,如果程序员和翻译人员互相之间不清楚彼此的工作的话,可能不容易找到问题的关键,这也是为什么我们在前面不仅介绍了源代码的编写,也详细介绍了生成.qm文件的过程的原因,这样有利于我们调试bug时更容易发现问题所在。

程序员常犯的错误是拿到字符串对应的ID之后,便按照他们的要求来随意使用,比如这些ID被存放到一个统一的txt文件中,这样的结果很可能是这些串根本就没被lupdate所提取出来,因为lupdate在扫描的时候只会注意tr()等一些关键字,这样在.qm文件中当然也没有对应的翻译串,最后的结果就是这些串不能被正确的显示翻译出来。

另一个问题是将翻译串放入数组中。简单的将ID放入数组中并不能引起lupdate的注意,这样的结果同上。这时候我们可以使用Qt提供的宏QT_TR_NOOP()或者QT_TRANSLATE_NOOP()

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

便于lupdate识别。比如:

static const char *greeting_strings[] = {

QT_TR_NOOP("Hello"),

QT_TR_NOOP("Goodbye")

};

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

// 或者

static const char *greeting_strings[] = {

QT_TRANSLATE_NOOP("FriendlyConversation", "Hello"),

QT_TRANSLATE_NOOP("FriendlyConversation", "Goodbye")

};

注意QT_TRANSLATE_NOOP宏的第一个参数是const char * context,即上下文,这个概念的引入大概是为了改善同一个串在不同的上下文中可能需要不同的翻译的情况,但也带来了不少问题。(www.61k.com)我们可以看一看qmetaobject.cpp中QMetaObject::tr()的实现:

QString QMetaObject::tr(const char *s, const char *c) const

{

return QCoreApplication::translate(d.stringdata, s,

c, QCoreApplication::CodecForTr);

}

这里context的值被设置为d.stringdata,回顾我们前面介绍过的元对象系统可以知道d.stringdata实际上就是类的名字。但是如果你将某个串放在别的类中,比如说串TXT_1在Class A中被加入某个数组中,并用QT_TR_NOOP宏包起来,但我们在Class B中才真正用tr()函数去使用它,那么lupdate扫描后得到的context值为A,而在Class B中用tr()调用时context值为B,这种不一致也会导致最后翻译的不成功。如果我们不了解整个过程,又凑巧这么使用了的话,找出这种错误是非常费尽的。

以上的问题可以说是我们在实际工作中的一些经验总结,在各类Qt文档或书籍中都鲜有介绍,希望对我们解决类似问题的时候能有一些启发的作用。

11.5 本章小结

本章的核心部分是Qt的元对象系统(Meta Object System)。Qt的元对象系统最初是为实现信号和槽的机制而引入的,在后来的发展中又逐步为Qt提供了一些其他的方便的特性,比如我们谈到过的对象树,tr()函数等,另外还有限于篇幅我们没有详细说明的属性支持等。

当然,作为C++特性的一种扩展,元对象系统还有很多局限性,Trolltech公司也在对它进行不断的改进,有兴趣的读者可以对比Qt3和Qt4的中的实现,变化是相当大的。 11.6 常见问题

1.假设信号S定义在类A中,类B是A的子类,类C中包含有类A的对象a作为成员变量,A,B,C中哪些可以发射信号S?

参考答案:A,B可以发射信号S,但C不能。这是因为信号在元对象系统中被定义为protected,仅仅定义信号的类和它的子类可以发射信号。参见11.2.2.2节。

2.在11.1.3节的小例子中,如果将signals_slots.cpp的第29行改成用下面的方式来连接信号和槽,即在int后加上变量n,这种方法正确吗?

QObject::connect(&s, SIGNAL(transmit(int n)), &r, SLOT(printNumber(int n)));

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

参考答案:不正确。(www.61k.com]连接带有参数的信号和槽的时候,只需要加入参数的类型(形参)就足够了,如果加入变量(实参)反而会导致不能通过参数一致性的检查,connect()函数会返回false。

3.在11.3.2节中,图11.3是将5个按钮的伸展性参数分别设置为1,2,3,4,5所得到的。如果将按钮的伸展性参数都设置为5,结果又会如何呢?

参考答案:其结果是五个按钮的大小长度相等。伸展性参数的意义是当部件可伸展时,布局管理对象中的各个部件将按照参数的比例来调整大小。如果五个参数都设为5,则五个按钮的伸展程度是相当的。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

4.如果发现通过tr()函数没有得到正确的翻译串,应该如何来查找问题的所在?

参考答案:一般我们先去查该串有没有被lupdate提取到.ts文件中,如果没有的话应该检查运行lupdate时所指定的.pro文件。如果.ts文件中能找到该串的话,则进一步检查.ts文件中的context设置与调用tr()函数时的context是否一致,以及经过linguist处理后的.ts文件中,该串是否已经被真正的翻译。另外还可能有问题的地方是QTranslator是否读取到了正确的.qm文件。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

第12章Qtopia Core

本章学习目标:

l 成功搭建Qtopia Core开发环境,包括qvfb的使用

l 了解Qtopia Core和Qt/X11程序的互相之间如何移植

l 理解Qtopia Core的轻量级窗口系统

l 掌握QCOP进程间通信机制

12.1 Qtopia Core的安装

我们在第9章曾经简单介绍了Qtopia Core,它是Trolltech公司开发的面向嵌入式系统的Qt版本。(www.61k.com]Qtopia Core的前身是Qt/Embedded,早在2000年11月就发布了第一个Qt/Embedded,Trolltech从Qt版本4.1开始将Qt/Embedded改称为Qtopia Core,作为嵌入式版本的核心,并在Qtopia Core的基础上发行面向于PDA、手机等的版本,称为Qtopia Phone Edition和 Qtopia PDA Edition等。

Qtopia Core与Qt/X11最大的区别在于Qtopia Core直接访问帧缓存(Frame Buffer),而不依赖于X Server或者Xlib,以减少了内存消耗及提高运行时的效率。

Qtopia Core的安装过程与Qt/X11非常类似。它同样有商业版和自由版两种授权方式,我们可以在Trolltech公司的主页下载Qtopia Core的自有版本:同样也可以选择一些速度可能比,

较快的国内的镜像站点比如ftp://ftp.qtopia.org.cn/mirror/ftp.trolltech.com/ 或者来下载。

如果你在前面已经安装了Qt/X11,基本上按照同样的步骤去安装Qtopia Core就可以了。安装Qt/X11时应该已经确保了你的Linux系统中所需要的gcc, make等编译工具,以及xlib相关的库等都已经没有问题(当然如果你准备要将你的Qtopia Core程序运行在一些特性的开发板上的,你需要按照本书前面章节中所讲述的,配置好你的交叉编译环境),你可以放心的解压缩Qtopia Core自由版的压缩包qtopia-core-opensource-src-4.2.2.tar.gz来进行安装了。 步骤一:解压缩安装包。

#tar xzvf qtopia-core-opensource-src-4.2.2.tar.gz

步骤二:运行配置程序

# cd qtopia-core-opensource-src-4.2.2

# ./configure –embedded [architecture] -qvfb

与Qt/X11的安装稍有不同,这里的configure程序需要使用-embedded选项并指定CPU的体系结构,根据你所用的机器的不同,可将architecture设置为arm,mips,x86,或者generic。同时由于我们后面要用到 虚拟帧缓存工具qvfb(Qt Virtual Frame Buffer),在这里需要添加-qvfb选项。configure程序还有很多其他安装选项,比如-prefix可以设定安装路径等,我们可以键入"./configure -help"来查看。运行configure程序后马上会看到与Qt/X11的安装时的License问题,回答yes就可以继续了。

步骤三:编译Qtopia Core源代码

# make

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

configure程序的主要作用是生成了qmake以及相关的Makefile和.pro文件。[www.61k.com]接下来我们就可以用make工具来编译这些Makefile了。根据机器配置的情况不同,这一步骤需要几十分钟到几个小时。

步骤四:安装Qtopia Core # su -c "make install"

如果前面configure程序配置时使用的是默认安装路径的话,Qtopia Core被安装在/usr/local/Trolltech/QtopiaCore-4.2.2,这需要你有root权限。如果没有root权限你也可以在运行配置程序的时候修改安装路径。

步骤五:设置PATH

为了更方便的使用Qtopia Core提供的各种工具,我们把/usr/local/Trolltech/QtopiaCore -4.2.2/bin添加到PATH变量——修改$HOME/.bash_profile或者$HOME/.profile并加入(或修改):

PATH=/usr/local/Trolltech/QtopiaCore-4.2.2/bin:$PATH

export PATH

如果我们前面已经把Qt/X11的路径加入到了PATH,最好保证将Qtopia Core的路径加在前面,因为后面我们要用到的都是Qtopia Core中的工具。比如我们可以直接将PATH设置为:

PATH=/usr/local/Trolltech/QtopiaCore-4.2.2/bin:/usr/local/Trolltech/Qt-4.2.2/bin:$PATH 修改完脚本后需要用source命令(或命令”. ”)重新运行修改的脚本,使设置生效,如: # source .bash_profile

我们可以来测试一下PATH设置的情况:

# which qmake

/usr/local/Trolltech/QtopiaCore-4.2.2/bin/qmake

这说明我们已经将Qtopia Core的路径加在Qt/X11的路径之前了。注意这时候如果我们需要再回头去编译Qt/X11程序,则需要修改PATH或者指定qmake等的路径了。 12.2 Frame Buffer和qvfb

由于Qtopia Core绕过了Xlib来直接读写Frame Buffer,我们来了解一些Frame Buffer的知识,以及Qt所提供的模拟Frame Buffer以方便调试的工具——qvfb。

12.2.1 Frame Buffer

我们在9.3.2.1中简单介绍了一些Frame Buffer的知识,它为我们提供的硬件抽象对程序员来说提供了一种统一的接口,免去了适应各种硬件的麻烦。注意版本2.2以后的Linux内核可以支持Frame Buffer,而老版本的内核可能需要许多调整并重新编译。关于各种架构下的Frame Buffer的支持不在本书讨论的范畴,读者可参考著名的Frame Buffer HOWTO文档:,里面有非常详细的介绍。

Qtopia Core所带的文档中为我们提供了一段代码来测试Frame Buffer是否支持,我们可以编译运行它来进行测试。为节省篇幅我们不在这里列出相关代码了,读者可以在本书所附的光盘中找到本章目录下的testFb子目录,即包含有相关的测试代码。这段代码仅仅用使用了C的库函数,而没有任何Qt的内容,我们可以直接用gcc来编译它:

# gcc main.cpp –o testFb

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

# ./testFb

The framebuffer device was opened successfully.

800x600, 16bpp

The framebuffer device was mapped to memory successfully.

如果你看到的结果如上,并且能够看到屏幕上方所画出的矩形图案,说明Frame Buffer已经配置好了。[www.61k.com]如果你遇到类似” Error: cannot open framebuffer device”的错误,可以尝试使用”ls /dev/fb0”,如果能看到这个设备,再尝试一下”cat /dev/fb0”,cat的结果很可能是无法打开设备,这说明你的Linux 内核版本已经支持Frame Buffer,但是没有打开,你需要修改你的lilo.conf或者grub.conf,加入”vga=[显示模式数值]”,其中显示模式数值的设置包括分辨率及颜色深度,可参考表12.1:

800x600

256色0x301 0x303

32k色0x310 0x313

64k色0x311 0x314

16M色0x312 0x315

在笔者的机器上,grub.conf的设置如下: 1024x768 0x305 0x316 0x317 0x318 1280x1024 0x307 0x319 0x31A 0x31B 表12.1 vga的显示模式设置表

# grub.conf generated by anaconda

#

# Note that you do not have to rerun grub after making changes to this file

# NOTICE: You have a /boot partition. This means that

# all kernel and initrd paths are relative to /boot/, eg.

# root (hd0,0)

# kernel /vmlinuz-version ro root=/dev/hda2

# initrd /initrd-version.img

#boot=/dev/hda

default=0

timeout=2

splashimage=(hd0,0)/grub/splash.xpm.gz

title Red Hat Linux (2.4.20-8)

root (hd0,0)

kernel /vmlinuz-2.4.20-8 ro root=LABEL=/ vga=0x314

initrd /initrd-2.4.20-8.img

其中vga被设置为0x314,对照表12.1,其设置为64k色,800×600分辨率。

修改完成后重启系统,如果启动过程中看到屏幕左上角的企鹅logo,恭喜你,Frame Buffer被启动了。修改你的/etc/inittab的run level为3,使linux以控制台启动而X11并不起来,来感受一下Qtopia Core程序通过直接读写Frame Buffer而显示的图形界面效果吧:

# cd ~/qtopia-core-opensource-src-4.2.2/examples/widgets/movie

# ./movie –qws

这里我们运行的是安装Qtopia Core后所带的示例程序,-qws表示将当前的程序作为Server来运行。我们可以看到并操作这个movie程序,如图12.1所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图12.1 直接运行在Frame Buffer上的movie程序

12.2.2 编译qvfb

由于Qtopia Core程序直接读写帧缓存Frame Buffer,而开发工作一般在桌面系统中进行,调试程序会比较不方便,因而Qt为我们提供了虚拟的帧缓存,即qvfb工具来方便开发和调试工作。[www.61k.com)

我们在上一节的步骤二中运行配置程序时,已经添加了qvfb选项,但是这还只是配置好了Qtopia Core程序编译和运行时的一些支持,真正的qvfb工具并不是一个Qtopia Core程序,而是一个Qt/X11程序,它被附带在Qt/X11的安装程序中,我们需要到Qt/X11解压缩后的路径下来编译它: # cd /home/user_name/qt-x11-opensource-src-4.2.2/tools/qvfb

# make

编译成功之后我们可以在目录/home/user_name/qt-x11-opensource-src-4.2.2/bin/下找到它。为以后方便使用我们可以把它拷贝到/usr/local/Trolltech/Qt-4.2.2/bin下去。

我们需要在X11启动的情况下来运行qvfb,如果你还在控制台下,则需要先用startx命令来启动X Window。之后可以来试试qvfb的运行效果:

# qvfb

结果如图12.2:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

# cd ~/qtopia-core-opensource-src-4.2.2/examples/widgets/digitalclock

# ./ digitalclock –qws

这个示例程序的在qvfb上运行的结果如图12.3所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图12.3 在qvfb上运行Qtopia Core示例程序

如果Qtopia Core程序没有能够自动检查到你所运行的qvfb,你也可以加入参数-display来指定它: # ./ digitalclock –qws –display QVFb:0

需要小心的是,如果你在没有运行qvfb的情况下,直接用./ digitalclock –qws来运行这个Qtopia Core示例程序,它会直接改写真正的Frame Buffer(而不是虚拟的qvfb),这样你的X Window桌面可能会受到破坏。[www.61k.com]

你可以在qvfb的菜单File->Configure中修改它的默认配置,Qt 4.2中所带qvfb功能已经相当强大,你可以用改变它的skin,使它看起来象个手机或者PDA等设备,而且这些skin上按键也被匹配到键盘上,你可以用鼠标去点击这种模拟的手机或者PDA按键。你也可以自己定义skin,或者修改已经存在的skin,到/home/user_name/qt-x11-opensource-src-4.2.2/tools/qvfb/下去看一看你就能依葫芦画瓢的发现该如何做。图12.4说明了如何改变qvfb的配置,以及将skin修改为Smartphone之后再运行Qtopia Core示例程序digitalclock的效果。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图12.4 修改qvfb的配置及修改之后的效果

我们还可以利用VNC协议来运行Qtopia Core程序,不过在本书中qvfb已经足以满足我们的要求,对将Qtopia Core程序运行在VNC Server上的方法有兴趣的读者可以自行参考Qtopia Core的文档。(www.61k.com)

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

12.3 移植Qt/X11程序到Qtopia Core中

我们先来回顾一下第9章谈到过的Qt/X11与Qtopia Core系统架构图的对比:开发Qt/X11与Qtopia Core应用程序时,我们实际上使用的是同一套API,这使得Qt/X11到Qtopia Core的移植变得非常简单。但是不是我们前面编译好的一些Qt/X11程序就可以运行在qvfb或者直接运行在Frame Buffer上呢?当然不是,因为我们在编译Qt/X11程序时,所链接的.so库是由Qt/X11提供的路径为/usr/local/Trolltech/Qt-4.2.2/lib下的libQtCore.so.4.2.2,同时还需要libX11.so等,而编译Qtopia Core程序时,所链接的.so库是/usr/local/Trolltech/QtopiaCore-4.2.2/lib路径下可以直接读写Frame Buffer的libQtCore.so.4.2.2等,我们可以用diff比较一下/usr/local/Trolltech/Qt-4.2.2/lib和/usr/local/Trolltech/QtopiaCore-4.2.2/lib这两个目录下的.so文件,即使它们的文件名相同,真正的内容也是不一样的。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图12.5 Qt/X11和Qtopia Core提供不同的.so库

理解了Qt/X11与Qtopia Core的这些相同和不同之处,相信读者朋友已经知道进行从Qt/X11到Qtopia Core的移植了:

1. 设置好PATH以保证你调用的qmake是Qtopia Core的qmake而不是Qt/X11的

qmake。[www.61k.com]正如12.1中提到的,你可以用”which qmake”来查看你的PATH设置是

否正确。或者你可以直接显式的用全路径来调用你需要的qmake,即

/usr/local/Trolltech/QtopiaCore-4.2.2/bin/qmake。

2. 去掉以前编译Qt/X11时所生成的可执行文件和makefile等。

3. 按照与编译Qt/X11类似的步骤来重新编译链接。

4. 在后台启动qvfb,运行你编译后的Qtopia Core程序。

我们可以尝试将第10章中的温度转换的小例子移植到Qtopia Core上来,按照上面的方法,唯一的一点改变在main.cpp的第9行,用setGeometry()函数调整设置一下窗口的大小,以适应qvfb的窗口。

1 #include <QApplication>

2

3 #include "ConversionScreen.h"

4

5 int main(int argc, char *argv[])

6 {

7 QApplication app(argc, argv);

8 ConversionScreen screen;

9 screen.setGeometry(0, 0, 240, 320);

10 screen.show();

11 return app.exec();

12 }

重新编译运行(注意加入”-qws”参数)之后的温度转换程序如图12.6所示:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图12.6 在qvfb下运行温度转换程序

前几章中我们所用的一些小例子都可以作类似的移植,几乎不用修改任何源代码(或者作一些象上面这样的稍作调整),这里不再重复。[www.61k.com]

12.4轻量级的窗口系统

与Qt/X11类似,Qtopia Core仍然采用Client/Server模型来显示窗口,但整体构架与Qt/X11有所差别——请读者回顾我们在第9章介绍过的X Window的系统架构(参见图9.2)以及上一节中的图12.5,对X Window架构,我们有一个专门的X Server用来响应Client程序的请求并绘制图形,一个X Client若想要运行,必须连接X Server并提出需求;对于Qtopia Core程序,它们也被要求必须运行在某个Server之上,但是Qtopia Core中没有这样专门的Server,正如本章前面所介绍的,一个Qtopia Core的应用程序在运行时加上参数”-qws”的话,它本身就被作为Server运行。

这种差别主要是为了打造一个轻量级的Client/Server窗口系统,因为在嵌入式应用开发中由于硬件条件的限制,我们往往无法负担起庞大的X Server所需要的系统开销。相对X 的架构,Qtopia Core中的主要差别在于,很多本来需要由Server完成的工作都直接交给Client去完成。比如窗口的绘制,并不是象X系统中那样完全由X Server来完成,而是由Client进行直接操作Frame Buffer来实现,这样一来就可以大大减少Server和Client之间的通信开销,提高运行时的效率。

当QApplication为某个进程生成了一个QWSServer对象时,这个进程就成为了Qtopia Core中的Server,它主要负责分配Client进程的显示区域,并产生鼠标和键盘事件等。而Client进程则通常生成一个QWSClient对象,负责处理与各种应用相关的逻辑并绘制它本身。注意这些逻辑都包含在QApplication之中,我们不需要自己来构造一个QWSServer对象或

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

QWSClient对象。[www.61k.com]

有两种方法可以让QApplication来构造QWSServer对象:一种就是我们前面用到的在运行加入”-qws”参数,另一种是构造QApplication对象时指定第三个参数为QApplication::GuiServer。而访问QApplication所生成的这个QWSServer对象则可以通过全局变量qwsServer来得到。QWSServer类提供了很多函数比如clientWindows()、windowAt()等来管理所有的窗口,并提供了setDefaultMouse()、mouseHandler()、setDefaultKeyboard()、 keyboardHandler()等来处理鼠标和键盘事件等。

尽管对于单独的Qtopia Core的应用程序来说,加入”-qws”来运行即已经足够,但如果我们应用Qtopia Core来开发一个嵌入式系统时,最好专门运行一个程序作为Server应用,来负责处理各种事件消息等,而其他的应用程序都运行在这个Server之上,以便于整个系统的统一管理。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

下面我们用一个简单的例子来说明如何运行一个Server,并且通过Server来启动其他Client进程。为简单起见,我们直接利用12.3中编译好的Conversion程序来作为Client进程,来看看我们在Server进程中如何启动它。

首先是main.cpp: 1 #include <QApplication>

2 #include "Launcher.h"

3

4 int main(int argc, char *argv[])

5 {

6 QApplication app(argc, argv, QApplication::GuiServer);

7 Launcher screen;

8 screen.show();

9 return app.exec();

10 }

注意第6行的改变:我们指定了参数QApplication::GuiServer,使这个程序作为Server来运行。 接下来看看Launch.cpp文件中Conversion是如何被作为Client启动的:

1 #include <QPushButton>

2 #include <QVBoxLayout>

3 #include <QApplication>

4 #include <unistd.h>

5 #include <sys/types.h>

6

7 #include "Launcher.h"

8

9 Launcher::Launcher() : QWidget()

10 {

11 createScreen();

12 }

13

14 void Launcher::createScreen()

15 {

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

16 QPushButton* launcherBtn = new QPushButton("Launch Conversion"); 17 QPushButton* quitBtn = new QPushButton("Quit");

18

19 QVBoxLayout *mainLayout = new QVBoxLayout;

20 mainLayout->addWidget(launcherBtn);

21 mainLayout->addWidget(quitBtn);

22 setLayout(mainLayout);

23

24 connect(launcherBtn, SIGNAL(clicked()), this, SLOT(launchConversion())); 25 connect(quitBtn, SIGNAL(clicked()), qApp, SLOT(quit()));

26

27 setGeometry(0, 25, 240, 320);

28 setWindowTitle("Launcher");

29 }

30

31 void Launcher::launchConversion()

32 {

33 pid_t pid = fork();

34

35 if (pid == 0)

36 {

37 qDebug("new process forked. PID is:%d\n", getpid());

38 int result = execl("../conversion/conversion", "conversion", 0);

39

40 if (result < 0)

41 {

42 qCritical("failed to launch application!\n");

43 }

44 _exit(-1);

45 }

46 else if (pid > 0)

47 {

48 qDebug("A chile process with PID %d is just launched by me.", pid); 49 }

50 }

createScreen()函数中都是我们已经非常熟悉的代码了,我们添加了两个按钮并稍作布局,按钮launcherBtn被连接到了槽launchConversion(),用来启动conversion程序。(www.61k.com]

槽launchConversion()中在第33行用fork()函数创建了一个新进程,35行用来检查刚刚得到的fork()函数的返回值,这是fork()的基本用法,它运行之后的返回值对父进程是子进程的进程号,而对子进程则返回0,我们可以根据这个特点来确定哪个是父进程,哪个是子进程,并对它们分别进行处理。

35行-44行我们在子进程中通过execl来调用12.3中已经编译好的conversion程序,完成之后调用_exit()来退出。我们可以在/usr/include/unistd.h中找到execl的函数原型:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

int execl (__const char *__path, __const char *__arg, ...)

其中第一个参数设定路径,后面是运行程序时的参数值,比如arg[0]为需要运行的程序本身即”conversion”,arg[1]及之后的都是运行conversion时的参数。(www.61k.com]注意这里我们并没有给定”-qws”。如果在Linux终端中我们直接运行../conversion/conversion是行不通的,因为它找不到Server。

第37行用来在子进程中打印出它自己的进程号,用来帮助读者理解fork()的这种做法,可以对照第48行,在父进程中,我们得到的pid值就是子进程的进程号,这样我们打印出来的两个PID应该相等,读者可以在后面运行这个例子时验证这一点。

如果对fork(), getpid(), execl(), _exit()等函数还有疑问的读者,请参考相关的Unix/Linux书籍或文档,这些Unix/Linux的基础知识在常见的书籍或文档中都能找到,在这里我们也就不给出某个具体的文档了。

这个Launcher程序的运行结果如图12.7:

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

图12.7 Launcher程序

注意我们运行时不需要”./launcher -qws”而只需要”./launcher”就可以了。点击”Launch Conversion”按钮之后我们就可以看到如图12.6所示的conversion程序,控制台输出中有类似下面的信息:

new process forked. PID is:2555

A chile process with PID 2555 is just launched by me.

当然PID的值不一定是2555,但这两个PID的值应该是一样的。

12.5 进程间通信

Linux下的进程间通信的方法基本上是从Unix平台上继承而来的,比如管道、信号量、消息队列、共享内存及套接字(socket)等。而在桌面环境中,如KDE就在传统进程间通

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

信方式的基础上发展了更为方便的通信方式DCOP,利用DCOP可以很方便地将强大的脚本功能添加到应用程序中,并且KDE 桌面还附带了dcop 和 kdcop工具,使用和开发都非常方便。[www.61k.com)

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

Qtopia Core中也定义了一种自己的进程间通信机制QCOP,注意QCOP只在Qt的嵌入式版本(如Qtopia Core和以前的Qt/Embedded)中提供,对于Qt/X11还是沿用KDE所提供的DCOP。相对已经是轻量级的DCOP,QCOP作了进一步的简化,以提高它在嵌入式系统中的效率。

在Qtopia Core中QCOP机制用QCopChannel 类来实现。QCopChannel从 QObject 类继承而来,提供了静态函数send()来发送需要传递的消息和数据,以及isRegistered()来查询某个Channel是否已经被注册。当在 channel 中接收消息和数据时,我们需要构造一个 QCopChannel 的子类并重写 receive() 函数,或者提供一个槽并利用 connect() 函数将received()信号连接起来。

下面我们通过改写12.3中移植的温度转换程序(实际上就是第10章中所详细介绍的小例子),来说明一下如何在Qtopia Core中利用QCopChannel来实现进程间的通信。为简单起见我们去掉了保存数值的功能,将两个温度计分别用两个进程来表示,并利用类似上一节中的Launcher程序来当作Server,并启动这两个温度计的进程。

我们在界面上作了一些小小的改动,因为毕竟分成了两个进程,不过这里假设读者朋友已经比较熟悉我们在界面相关代码上的改动了,我们可以在随书所附的光盘上看到完整的代码示例,这里我们重点来消息是怎么用send()来传送的。下面是摄氏温度计程序中Cel.cpp的相关代码片断: 81 void Cel::sendMsg(int celNum)

82 {

83 QByteArray data;

84 QDataStream out(&data, QIODevice::WriteOnly);

85 out << celNum;

86 QCopChannel::send("/System/Temperature", "ConvertCelToFah(int)", data); 87 }

第86行我们用QCopChannel::send()函数来发送消息,它的原型如下:

QCopChannel::send(const QString& channel,

const QString& message,

const QByteArray& data)

我们在发送时需要和接收方协商并定义好发送消息的channel和消息的内容,比如在这个例子中我们定义channel为"/System/Temperature",消息内容为"ConvertCelToFah(int)",注意这里加入了参数的类型,这只是一种惯例,而并不是必须的,你完全可以在发送和接收时直接用"ConvertCelToFah",不过这种写法可以提供参数的信息,便于更准确的辨认消息。第三个参数是需要发送的数据,我们用QByteArray来方便数据的传送,83行到85行实际上就是构造了一个QByteArray对象,并给它赋值。

这个消息的接收在Fah.cpp中,即当摄氏温度计的数值发生变化时,这个进程所发送的消息由华氏温度计的进程所接收,并进行相应的动作:

63 void Fah::listenChannel()

64 {

65 QCopChannel *channel = new QCopChannel("/System/Temperature", this); 66 connect(channel, SIGNAL(received(const QString &, const QByteArray &)),

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

67 this, SLOT(handleMsg(const QString &, const QByteArray &))); 68 }

69

70 void Fah::handleMsg(const QString &message, const QByteArray &data)

71 {

72 QDataStream in(data);

73

74 if (message == "ConvertCelToFah(int)")

75 {

76 int celNum;

77 in >> celNum;

78 celToFah(celNum);

79 }

80 }

首先我们在65行生成一个QCopChannel对象以监听从channel "/System/Temperature"发送过来的消息,然后我们将received()信号连接到槽handleMsg()。(www.61k.com)当然,这个listenChannel()最后是需要在构造函数中被调用,以保证能及时监听消息。

槽handleMsg()我们需要比对所传送过来的各种消息,仅仅当消息是我们之前所定义好的"ConvertCelToFah(int)"时,才从数据包中取出来摄氏温度计的当前数值,并调用私有函数celToFah()来真正改变摄氏华氏温度计的读数。

我们还需要将上一节的Launcher程序修改一下来启动这两个进程。修改后的代码在CopServ.cpp中,主要改动如下:

42 void CopServ::launchApp(const char* path)

43 {

44 if (path == NULL)

45 {

46 qDebug("Launch path is NULL!\n");

47 }

48

49 pid_t pid = fork();

50

51 if (pid == 0)

52 {

53 qDebug("new process forked. PID is:%d\n", getpid());

54 int result = execl(path, path, 0);

55

56 if (result < 0)

57 {

58 qDebug("failed to launch application!\n");

59 }

60 _exit(-1);

61 }

62 }

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

63

64 void CopServ::launchCel()

65 {

66 launchApp("./cel/cel");

67 }

68 void CopServ::launchFah()

69 {

70 launchApp("./fah/fah");

71 }

其实主要就是增加了launchApp()函数——现在它变得更加容易被普遍使用了,没准你以后的Qtopia Core开发中就可以用到它呢。(www.61k.com)在这里我们采用相对路径来启动两个可执行文件cel和fah,注意你需要把它们都编译好。

CopServ类运行起来的结果如图12.8的左图所示,而右图则是分别将两个温度计启动之后的示意图。它们看起来还不如第10章的例子呢,我们在这里只是用这些简单的代码来说明如何利用QCopChannel 类来实现进程间通信,实际应用中这种简单的情况当然还是直接用Signal/Slot来得方便,只是有些复杂的情况需要不同进程间进行通信的话,我们就可以用到这种方便的QCOP机制了。

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

图12.8 进程间的通信示例

12.6

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

本章小

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

这一章我们主要通过比较Qt/X11和Qtopia Core来帮助大家从Qt/X11过渡到Qtopia Core。在前几章已经能够熟练运用Qt/X11的基础上,我们只需要重点注意Qtopia Core的一些区别于Qt/X11的特点就可以很快熟悉Qtopia Core中的开发了。

在Qtopia Core的安装过程与环境搭建时,我们需要注意qvfb的编译和使用,它是一个

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

很方便的调试工具。[www.61k.com]

另外我们还分别举例介绍了Qtopia Core的轻量级窗口系统和QCOP进程间通信机制,这两者都是Qtopia Core中为提高效率而设计的,也需要我们熟练掌握。

12.7 常见问题

1.将Qt/X11程序移植到Qtopia Core中需要注意些什么?反过来呢?

参考答案:将Qt/X11程序移植到Qtopia Core中,通常只需要重新编译就可以了,当然要注意用Qtopia Core的qmake而不是Qt/X11的qmake。反过来从Qtopia Core向Qt/X11移植时则需要注意一些Qtopia Core中特有的类比如 等都不能再使用,实际上这种反过来的移植通常费力不讨好,很少有人这么做。

2.Qtopia Core是Qt/X11的一个子集吗?为什么?

参考答案:尽管Qtopia Core对Qt/X11做了一些精简以减少系统开销、提高运行效率,但这并不意味着Qtopia Core是Qt/X11的子集,事实上由于Qtopia Core需要直接读写Frame Buffer,它需要在Frame Buffer的基础上(而不是Xlib的基础上)来实现图形的绘制等,并且Qtopia Core中还提供了一些Qt/X11所没有的内容,比如QWSServer和QCopChannel等。从某种意义上来说,Qtopia Core甚至可以看作是Qt/X11的一个超集。

3.qvfb工具是一个Qt/X11程序还是一个Qtopia Core程序?

参考答案:qvfb在Qtopia Core的开发中用于模拟Frame Buffer,在Qt/X11开发中它确实没有多少用处,但它本身并不是一个Qtopia Core程序,而是一个Qt/X11程序。实际上它并不直接读写Frame Buffer,而是运行在X11上,用来提供一种虚拟的Frame Buffer,以方便Qtopia Core的开发和调试。

4.如果想让一个Qtopia Core程序以Server模式运行,通常有哪些方法?

参考答案:通常有两种方法:一种是在运行Qtopia Core程序时在后面加上”-qws”参数,另一种是构造QApplication对象时指定第三个参数为QApplication::GuiServer。

嵌入式linux驱动程序设计从入门到精通 《ARM嵌入式Linux系统开发从入门到精通》【一个工程师写的】

附录A:光盘内容

光盘中包含书中出现的所有代码,并且按章节存放,关于章节中的源代码都提供了相应的README说明文件,便于读者学习。(www.61k.com]

附录B:参考文献

1. [译者]魏永明、耿岳、钟书毅. Linux设备驱动程序(第三版)[M]. 北京:中国电力出版社,2006

2. 刘淼. 嵌入式系统接口设计与Linux驱动程序开发[M].北京:北京航天航空大学出版社,2006

3. 李驹光. ARM应用系统开发详解——基于S3C4510B的系统设计[M]. 北京:清华大学出版社,2005

4. Samsung Electronics Co. S3C2410X 32 bit Microprocessor User Manuel, 2003

5. 赵炯. LINUX内核完全注释[M].北京:机械工业出版社,2004

6. Wookey. The GNU Toolchain for ARM Target HOWTO.

7. P. Raghavan.Amol Lad. SriramNeelakandan. Embedded Linux System Design and Development. Auerbach Publications,2006

8. Karim Yaghmour. Building Embedded Linux Systems.O'Reilly, April 2003

9. Linux 内核调试器内幕. IBM软件工程师Hariprasad Nellitheertha. 参考站点:

10. Qt4.2官方文档。Trolltech Corp. 参见

11. C++ GUI Programming with Qt 4, Jasmin Blanchette, Mark Summerfield, 2006, Prentice Hall

12. Design Patterns: Elements of Reusable Object-Oriented Software. Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, 1995, 机械工业出版社(影印本)

13. The Linux/m6k Frame Buffer Device. Geert Uytterhoeven, 1998, 参见

14. The Embedded Linux GUI System. Chen Hanyi, 2001, 参见

15. Thinking in C++, 2nd edition, Bruce Eckel, 2000, MindView Inc.

扩展:嵌入式从入门到精通 / 嵌入式入门到精通 / arm从入门到精通

三 : 基于嵌入式Linux的千兆以太网卡驱动程序设计及测试

基于嵌入式Linux的千兆以太网卡驱动程序设计及测试

一.引言

千兆以太网是1种具有高带宽和高响应的新网络技术,相关协议遵循IEEE802.3规范标准。采用和10M以太网相似的帧格式、网络协议和布线系统,基于光纤和短距离同轴电缆的物理层介质,更适用于交换机、服务器等数据吞吐率大的设备。本文设计实现1种基于嵌入式Linux千兆以太网卡的驱动程序,并完成后续的测试工作和代码移植。

千兆以太网网卡工作在OSI网络架构的物理层和数据链路层,其中物理层由PHY芯片管理,数据链路层由千兆以太网控制器(GMAC)管理。硬件构架上,GMAC控制器由核心层、MTL(MACTransactionLayer)层、DMA层和总线接口层构成,如下图1-1所示。核心层连接PHY芯片,管理和PHY芯片之间的通信;MTL层建立物理层和内存之间的数据通道,调整帧传输结构,控制数据流,转换时钟域;DMA层完成数据的传输任务。GMAC配置寄存器CSR(ControlandStatusregister)通过系统总线和CPU交互,CPU通过总线slave端配置DMA和MAC区的CSR,其中MAC区的CSR可以设置PHY的芯片寄存器,通过对CSR的设置可以控制网卡切换工作状态。

基于嵌入式Linux的千兆以太网卡驱动程序设计及测试

千兆以太网卡的数据传输任务由DMA完成,DMA传输操作通过预先在内存中建立描述符的方式完成。描述符的作用是指定MAC帧数据所在的缓存地址,每个描述符可以最多指定2个缓存地址,缓存大小有严格控制,1个描述符不能指定全部1个帧的缓存数据,需要多个描述符构成描述符链来完成。

基于嵌入式Linux的千兆以太网卡驱动程序设计及测试

有2种描述符链结构:环状描述符和链状描述符。链状描述符中的第二个buffer指定了下1个描述符所在的物理地址,而第1个buffer指定帧数据缓存的位置,环状结构描述符的位置是有序排放的,2个buffer都指向帧数据的缓存地址,最后1个描述符指向第1个描述符所在物理地址形成桶状描述符链。环状和链状结构如图1-2所示,1个描述符链只能用来存储1个MAC帧的数据,DMA每个通道一次最多完成2个MAC帧的传输,多MAC帧的传输需要重新使能DMA通道。

描述符的具体结构如图1-3所示:基于嵌入式Linux的千兆以太网卡驱动程序设计及测试

OWN位控制描述符是由DMA控制还是由host端控制,后面31位是描述符状态信息,DES1为控制描述符并标明2个buffer大小,DES2和DES3描述2个buffer所在地址。

二.千兆以太网卡驱动程序设计

GMAC驱动程序需要完成的内容有:PHY芯片初始化,(www.61k.com)GMAC初始化,GMAC读/写数据,GMAC数据流控制和各种模式设置。

GMAC初始化流程图如图2-1所示:基于嵌入式Linux的千兆以太网卡驱动程序设计及测试


GMAC初始化过程首先检查PHY芯片工作是否正常并配置PHY芯片模式,创建发送和接收描述符,初始化DMA,然后配置MAC工作模式,使能DMA后进入工作状态。

初始化完成后,数据等待发送或接收,DMA根据描述符状态自动完成数据发送或接收的任务。buffer中的帧结构不包含preamble,PADbyte和FCS段,只包含源地址,目的地址和类型/长度域。如果MAC禁止CRC校验和PAD插入,那么buffer就必须包含完整的帧结构,其中必须包含CRC校验位。DMA一次搬运最多两帧数据,所以初始化后如果需要完成多帧数据搬运需要重新使能DMA。

GMAC发送和接收过程如左图2-2所示:

基于嵌入式Linux的千兆以太网卡驱动程序设计及测试

初始化后,第一次帧传输不需要等待中断,DMA自动完成发送接收任务,当有多次帧传输时,DMA在完成一次发送接收任务后会给CPU发送中断,CPU响应中断处理下一次读写任务。如果读写过程中发生帧错误等导致操作未完成,则会产生相应异常中断直到CPU清除中断位标志,此时GMAC停止工作。

驱动程序设计完成寄存器到功能函数的转换,给上层操作系统提供应用接口,Linux嵌入式操作系统有一套标准的接口规范,根据该规范设计驱动需要预先定义2种重要的结构体:

描述符程序结构体设计:

typedefstructDmaDescStruct

{

__u32status;//DMA状态

__u32length;//buffer1和buffer2长度

__u32buffer1;//buffer1地址

__u32buffer2;//buffer2地址

//下面数据仅为驱动使用

__u32extstatus;//接收描述符的扩展位状态

__u32reserved1;//保留部分

__u32timestamplow;//时间戳

__u32timestamphigh;

__u32data1;//buffer1虚拟地址(驱动备用)

__u32data2;//buffer2虚拟地址(驱动备用)

}DmaDesc;

GMAC控制器设备程序结构体设计:

typedefstructGMACDeviceStruct

{

__u32MacBase;//MAC基地址

__u32DmaBase;//DMA基地址

__u32PhyBase;//PHY基地址

__u32Version;

DmaDesc*TxDesc;//发送描述符链开始地址

DmaDesc*RxDesc;//接收描述符链开始地址

__u32BusyTxDesc;//当前DMA所拥有发送描述符链数量

__u32BusyRxDesc;//当前DMA所拥有接搜描述符链数量

__u32RxDescCount;//当前描述符链中接收描述符数量

__u32TxDescCount;//当前描述符链中发送描述符数量

__u32TxBusy;//指明当前发送描述符是否由DMA控制,即OWN位是否为1

__u32TxNext;//指明下1个描述符链是否有效

__u32RxBusy;

__u32RxNext;

DmaDesc*TxBusyDesc;//与TxBusy对应的当前描述符地址

DmaDesc*TxNextDesc;//与TxNext对应的下1个描述符链地址

DmaDesc*RxBusyDesc;

DmaDesc*RxNextDesc;

__u32ClockDivMdc;//时钟分频数

__u32LinkState;//网卡链接状态

__u32DuplexMode;//半双工,全双工等工作模式选择

__u32Speed;//连接速度。10M/100M/1000M

__u32LoopBackMode;//LoopBack模式

}GMACdevice;

三.测试实现

测试程序对GMAC进行了相应的黑盒测试和压力测试,黑盒测试用来测试GMAC千兆网卡的各个模块输入相应信号是否得到正确的输出信号,压力测试用来测试网卡的系统稳定性。

测试程序通过GMAC的LoopBack模式将数据写入内存并让GMAC发送,再从接收描述符指定的内存中读出,判断写入写出数据是否一致,完成数据读写测试。对于功能点测试,主要测试的GMAC功能有:PHY芯片自协商完成验证,GMACLoopBack模式,哈希值和MAC帧过滤,CRC校验和,AV模式和时间戳支持模式。

一次读写测试完成后,测试程序改变GMAC模式和状态,再进行一次数据读写测试,同时加大数据量和功能点以完成压力测试,再次判断写入写出数据是否一致。不断循环进行直到测试程序测试结束,测试过程要完成判定覆盖和条件覆盖的100%代码覆盖率,需要不断改变输入信号和功能以满足测试意图,使用LoopBack模式的测试信号发送和接收结果如图所示:基于嵌入式Linux的千兆以太网卡驱动程序设计及测试

PHY和MAC使用GMII接口连接,全双工模式下发送的同时进行接收操作,实验结果发现在帧传输速度提升到1.3GMbps时,有明显的丢帧现象,MAC层的帧数据FIFO原数据被冲刷,丢帧现象明显。在使能中断的情况下,发生丢帧后会立刻进入异常中断,DMA停止工作等待CPU响应中断。

四.结束语

该驱动程序已经在Linux嵌入式系统下调试通过,所有代码下Linux嵌入式系统下移植完成,驱动程序在MaPU定制指令SOC系统上调试通过,使用该驱动完成后续上层软件开发。验证平台基于ARMCortexA8,测试仿真使用RealViewDebugger完成指令仿真,使用VCS进行时序精确仿真,测试程序代码覆盖率达到90%以上。

四 : 37从精益生产到精益设计(齐二石)

齐二石:从精益生产到精益设计

2008年11月13—14日,由中国工业工程学会、中国标准化协会主办的“第五届中国精益管理国际论坛”在天津召开。图为工业工程学会会长齐二石讲课。

齐二石:各位嘉宾、各位代表上午好!

我很高兴请我在这里汇报一下关于我这次论坛的演讲题目,就是“从精益生产到精益设计”。 (PPT)我们现在的背景大家都知道,中国是一个制造大国,但是我们现在还不能说我们是一个制造强国,因为我们许多的关键技术还要由海外来引进,而我们的制造企业的效益到今天还不能和真正的世界强国的企业进行竞争,所以大家可以看到我们变成制造业大发展的一个迫切愿望;第二个背景,我们在每一个制造企业成千上万在发展的时候,工厂设计问题出了一定的问题;第三个背景循环制造对企业提出新的要求,现在环保、可持续发展,社会要求越来越多。

(PPT)大家看这里,这样一个环境,我们的效率生产周期质量水平,信息化程度和西方国家在产品开发和制造技术上都有很大的差距,这个差距是我们和西方的差距。

第二我们提到设计问题,大家一提到设计就认为是产品设计,但是今天我们又说我们在制造企业的设计问题,除了产品设计外,还有一个制造系统设计,而且我们做质量的人都知道,产品首先是质量设计的,不是干出来的。所以同理,我们制造业系统的管理水平首先决定你的设计水平,当前的制造企业的工厂设计和生产组织设计远没有达到发达国家的要求,远没有达到工业工程的要求,这个问题在我们企业这些年知道这个事但是不一定看到这个问题,以前的竞争条件比如说它竞争的焦点集中在效率、质量和成本,是这三个问题上。现在我们发现效率、质量、成本、知识、服务、管理,现在还提出一个新的理念叫制造服务,实际上更加注重制造业前后环节的重要内容,从采购到销售,还有产品的开发到工厂设计。

(PPT)今天我提出精益设计的理念是从这里开始的。首先是工厂设计,现在的工厂设计主要干三件事,第一件事工厂成立前给你设计你的工艺,叫工艺设计;第二件事叫生产设备的选型与安装设计;第三件事是公用设施设计,一些泵、管道、基础等等,还有厂房设计也做了,我看设计文本上也有很多市场分析等等,但是很粗糙的。基于IE设计不叫工厂设计,叫设施设计。最近南京工业大学有一个教授,年轻的教授翻译了美国的一本书叫《设施设计》,设施设计规划,我写的是序,我看完之后我们和西方国家的企业在一开始生出来就不一样,他还要做流程、生产组织设计,当然我们今天提到精益设计他还缺这些东西。

我们有了工艺、有了设备、有了厂房、有了材料、有了产品你能进行生产了,这个工厂生产完毕,如何进行高效率、低成本的生产,企业家在摸着石头过河,我们的工厂原来在生产完之后第一年达到20%,第二年达到40%,第三年到50%,第五年到70%-80%,第七八年才到100%,可是经过设施设计之后第一年达到50%,第二年达到70%,第三年达到100%,我们首先在制造系统的前期设计打一个不恰当的比较就是“生小孩”,我们生的是短板,就少干几件事。我说这话得罪人,我不管这个,我是对中国制造业负责。如果在基于IE工业工程的设计之后,再加上精益设计的思想和理念,这时候我们的企业从一开始生产投入开始第一年开始它就灌输和工业工程和精益管理的理念。

精益生产早期在80年代的时候,那时候也是金融危机、石油危机、能源危机,在这种情况下,美国人突然间发现,被打败两个人,一个是德国一个是日本,它们的制造业悄悄的上去了,特别是丰田生产方式,国家拿了一笔钱,麻省理工学院教授做了一个调查,调查之后称之为精益生产,美国人从来都说精益生产,不说丰田生产方式,日本人从来都说丰田生产方式不说精益生产,我们中国人不管这个,我们俩全说。后来我也有幸在1996-1998年,日本丰田公司举行三次丰田企业会议,每次请三四十个企业家,请七八个政府官员,请教授帮讲课,我去了,我有幸三次亲临丰田公司现场看,回来1997年我在《中国企业管理》杂志中发表一篇文章叫“丰田生产方式及其在中国的应用”,我概括是一大目标、两大支柱、一大基础,一大目标就是JIT,还有是自动化,这个词我翻译不好,我曾经起个名字叫自主化也不太好,基础叫改善与自我改善。

大野耐一说,什么是丰田生产方式?

在那次培训班上,主讲是大野耐一的姑爷,因为那时候大野耐一已经光荣牺牲了,他说就是工业工程在企业的应用,所以大家注意,丰田生产方式严格的讲它的老师应该还是美国人,在四十年代,丰田到哪里学到的东西,不是照搬照抄,改造成自己的东西,叫TPS,叫丰田生产方式,美国人又把它叫做精益生产,这个事是这么过来的。精益生产我们理解误区有几个问题。第一把精益生产仅仅看成是生产管理,日本人已经不这样了,日本人已经把它发展成全面生产方式,把以前我们注重生产环节对于产品设计、工厂设计、销售重度不够。

第二个误区认为精益生产就是JIT,没有把握精益生产的手法和工具等等这些有价值的东西。因此一句话,要想搞好精益生产,首先要把工业工程弄清,根据自己企业的模式把两者结合起来发挥员工的创新性,这才能实现精益生产。

(PPT)现在我提出这样一个说法,叫DMLP,什么意思?就是说设计阶段,尽量避免企业在运作当中可能出现的问题,这种运作方式从根本上消除企业在建设当中产生的浪费,代替企业在运行当中出现再改善的亡羊补牢式的做法,企业这些问题我们做精益生产是改善消灭企业浪费和问题,哪来的?两个地方来的,第一个地方生产系统建设之后,由人们的习惯创造出来的,这是第一方面的问题,是我们生产出来的;第二方面问题就是一开始从设计方面出来的,一开始设计就有问题,在这里概括这样一个图,产品开发我们老说CAD、CAM、CEPP是产品开发,不对。这CAM、CEPP、CED只能是产品设计,我现在还担任国家科技部的企业科技信息化工程总体专家,在这个问题上我说了多少次,但是其实产品开发首先从需求和科技进步来的,然后概括成产品创意,从产品创意到产品概念的设计。第三步到产品原形的设计,这时候进入CAD、CAM、CEPP没有前面三个环节就没有后面,我们做后面的东西在市场上买不到,就不是产品开发的观念,从产品原形产生之后进行产品设计的时候,第二个问题制造企业的投资决策,它在这里有两方面,一个是工厂设计、工艺设计,从设施设计我们包括物流系统设计、生产组织设计、企业的管理组织设计和管理信息系统设计,至少包括四大设计,然后这个工厂才能进行运营,销售才会稳步上升。

全生命周期精益设计的理念和内容。今天谈这个问题,面向工厂设计的方面,工艺设计、设备造型设计、公用设施设计加上设施规划与设计、物流等等,详细不说了。

在信息化时代到21世纪进入信息化时代,美国人在80年代末发现了日本丰田公司为代表的日本产业界制造业的突飞猛进的发展,还有什么办法?从美国人的文化和美国人的科学发展又不能走和日

本一样的路,所以它就提出一个信息化的问题,信息化的问题在美国提出,它企图用信息技术提高制造业的产品和生产能力来超过日本,但是从现场看,到现在结果看,可能还没有,现在还没有超过,现在的汽车制造业第一把交椅还是丰田公司,2006年丰田公司就制造了美国三大汽车公司的利润总合,但是销售额超不过通用汽车,但是到2007年销售额也盖过通用汽车,2006年中国产业联盟代表到天津大学与我谈合作问题,两天后美国通用汽车公司的精益总裁也来到天大,我当时问,我说你怕谁?他说我怕丰田,我说你为什么怕丰田?你的销售比他高,他说不是这个问题,他说有一个重要的问题,丰田公司的人你把他抠不出来,比如挣十万年前,你给他20万,他不去,他不为金钱所动 ,他是真正的共产党员,他说我那样完了,美国人的观念就是这样,你在这个企业完了,他给的工资高他就跳槽了,你再看一个团体都是共产党员,一个团体里面都是叛徒,这能行么?

所以这里面还引申出一个问题,就是精益生产还有一个重要的一点就是企业文化的问题,企业文化是为了建设一个高水平的制造企业所需要的上下共同认同的价值观和由价值观形成的凝聚力。

(PPT)大家看这个图,美国一开始从工业工程开始叫IE,然后创造了福特车,这是1911年二十世纪初,到1945年的时候,它创造了日本丰田公司从IE开始创造了丰田方式到90年代,创造出TotalTPS,就把它延伸到采购和服务部门,最后提出日本国家管理标准,现在提的叫企业信息化工程,还有很多的模型,比如什么计算机集成制造系统,ERP、MRPtwo、大规模制造等等。中国应该怎么走?中国应该这样,要学习美国和日本的经验,创造自己的,我们创造中国自己的GPS,或者丰田生产方式。我们要提倡IE+IT,就是信息基础和工业工程结合,创造中国的国家管理系统。

所以金融危机的到来我们又是机遇又是挑战。大家看看西方国家这样发展起来的。信息化工程当中我们要掌握一个IE+IT,或者是精益生产+IT技术,现在还有一个理念,我没在课程讲,就是精益信息化,信息化工程当中的精益生产两者的结合才能成功,特别是在离散制造业,什么汽车、电子等等,还有一个产业就是流程行业,像我和大家讲的发电、电力系统和化工系统以及冶金系统,这方面的工业工程怎么做,现在还是个大问题,电力系统已经请我做过讲演。

我们提一点,我们的创造模式,以工业工程为核心,通过规划企业的资源,实现高效低成本的经营,而且我们和西方人不一样,大家千万注意,我们和西方人不处在一个环境当中,我们三十年走完他100年的路,但是大家注意科学技术可简单的买进来,或者复制进来,但是管理不可以,我们代表团到美国访问,访问美国当今活着的管理学专家德鲁克,德鲁克说了一句话,他说告诉中国的同仁,管理者是不能进口的。我原来讲课时说过,管理本身是不能模仿的,一个企业一个样,难就难在这儿,否则大家都行了,摩托罗拉买多少计算机、买多少软件,我花多少钱一买就变摩托罗拉,可能吗?使我们难就难在这里,价值链的环节,上面是产品升值,下面是市场研究,这是IE活动,上面是IT活动。

最后一点时间我这里说一下实践,首先我先谈一个观点,许多的学者在很多场合讲了这个东西,企业家现在都讲糊涂了,今天弄出个精益生产,明天弄个执行力、后天弄出个ERP,目前在管理界世界上流派主要是两大思想体系,第一大思想体系从左边开始,这就是以欧美为代表的,欧美的思想体系的根源和理念来源就是泰勒捷克布雷斯(英翻),这就是设计理念,它强调职能部门的作用,强调执行力的功能,通过各种的管理方法、管理模式由工业工程演化出来的,比如说像精益六西格玛、ERP、MRPtwo,这套东西一直到生产线上来解决效益质量管理问题,这是第一套管理体系,这理论的优点这

个设计体系来的快,一开始来的特别好使,所有主张中国精益生产一开始用工业工程的方法把物理系统好好研究研究。

比如我们汪总裁,我到他们厂看了一件事特受感动,就说两个大厂房,一个厂房12000平米,还要生产上市产品还要建立12000平米,这时候怎么办?通过工业工程的平面设置和各个系统的改善把两个产房的设备装在一起,这太厉害了。这省多少钱就不说了,后面的效益,而且产品两个机床之间空间和距离如果加大了一点点会产生哪些问题,我和大家讲,时间有限不能推这个模型,效率问题、质量问题、成本问题都出来了,就差了一点管理,这是很关键的解释。

另外一个体系就是日本丰田公司为代表的,是以人为中心,通过调动人的积极性,然后应用美国工业工程的手段变成自己的创新方法,然后直接到现场去改善,就这么做的。有的学术界的人士说了,美国的学术水平高,日本的学术水平低,但是现在看来日本的这套比美国的产生效果好,你们知识分子所说的学术水平对企业家没有用,所以在这种情况下,中国一定是把两者结合起来,我当时问了几个日本的朋友,我说你为啥你的钱在现场,我却得到验证,他说一个很简单的问题一句话倒破底,因为企业流动资金95%的钱在现场,你去算吧,你所有流动资金链的钱在现场,你通过各种项目通过多个环节,这些若干环节在解决现场问题,这是可靠性的环节越多可靠性越差,日本直接到现场,这个道理是这样的。

下面用简短的时间给大家汇报一下我们的工作实践。这是我们开发的全生命周期精益设计和管理工具的开发,减少等待时间和排队长度,有效分配资源、消除缺货问题,把故障的负面影响减至最低,把废弃物的负面影响减至最低,研究最佳投资方案等等。为了加大精益周期,精益设计管理的开发工具做了很多这方面的工作,不详细说了。

(PPT)这是在天津某轧钢厂做的精益设计模型,这个厂投产前就把系统设计出来的,才取得这个效果。我就给大家汇报这些,不占用大家更多的时间,简单的说一下、概括一下,第一精益生产要求中国式的精益生产,它的模型、它的结果用一句话概括,就是用工业工程技术+企业文化的研究;第二从精益生产的改善,我们改善的问题出在哪里,日本人成功在哪里?就是把浪费给消灭掉了,或者说把浪费消灭掉最小,基尔布雷斯最小的成就是告诉大家一件事,没经改善的科学管理和工业工程改善前,你的生产系统和管理系统一定会有70%以上的浪费,他把这个消灭掉了。他消灭只剩10%-20%了,或者举一个不恰当的比例,我们和日本、美国人的企业一起跑1万公尺,美国18000公尺到终点了,我们跑了3万公尺到终点,你退绕弯了你也跑不过他。

第二个观点在技术领域来说,一方面我们产品工艺和设备体系,我们叫专业技术,还有工业技术,这两个技术就像人们的两条腿,大家想想两条腿如果不一边粗,一个很粗一个很细能不能跑?跑的能不能快?缺一个就更完蛋了。所以这是我跟大家说的第一件事。

第二件事,尽管我们做了这么多改善现场的努力,但是在一开始就应该消灭,所以提出一个全生命周期这一设计理念,我们现在也在一边研究一边设计,最近我们给一个兵器部的厂子做设计,希望大家在这方面给我们提出更多的指导和帮助,我把这个思想给大家简单汇报到现在,谢谢各位!

精益能够实行成功等于人才实现育成

2008年11月13—14日,由中国工业工程学会、中国标准化协会主办的“第五届中国制造业管理国际论坛”在天津召开。

王月:朋友们,大家早上好!欢迎大家参加第五届中国制造业管理国际论坛。在这个论坛上我们又看到了许多的老朋友,又结识的很多新朋友,刚才王总监已经谈到了,从昨天的主论坛到昨天晚上的精益大餐,我们由衷的感谢各位朋友对爱波瑞公司的支持。

在昨天的主论坛上,尤其是下午的时候,我们在对话过程当中,许多嘉宾朋友还有我们在座的各位大家都对精益生产在企业中如何推进提出了很多相关的问题,我尤其注意到,大家对于精益制造如何培养人才,怎么样实现人才育成都更为关注。从昨天上午齐二石院长到王主任、谢克俭总监进行了诠释。 大家知道次贷危机引发的金融危机,金融危机引发了世界性的经济危机。可能大家现在都在想,TPS(精益生产方式)在这个时候是应该体现出它的强大生命力的。但是大家知道不知道,精益生产方式或者是GPS不管是在日本还是在全世界,不仅仅是生产制造的一个模式和它的工具方法的导入,更关键的是靠人。

丰田人之所以在半个多世纪称霸于全球汽车这早,根源在于什么地方?半个多世纪以来为什么能够成功,一步一步走向世界制造业的顶尖?而且成为现在世界制造业最能赚钱的公司,靠的是什么?非常简单,就是六个字,先造人、再造车。通过半个世纪走过来的历程,丰田人演绎了人才育成的管理机制,把人才育成的管理理念和TPS套的生产模式融合在一起,创造了丰田的辉煌。

今天利用这个时间跟大家一起分享一下精益成功等于人才育成。

(PPT)首先看一看丰田的人才育成文化。大家可可以看到,从丰田精神当中可以体现出来,事业不是一个人所能部办成的事,人和比什么重要。还有把培养人作为丰田精神当中一个非常干间的部分。 二十一世纪到来的时候,丰田前总裁张富士夫在2001年《丰田模式》这篇文章当中,把人才育成和人才第一列入他五个发展方向的其中最关键的一个,就是挑战、改善、现地现物、还有人性化的管理,最后是人才育成。所以我们从这里头就可以看出,丰田人走到今天,靠的是什么?靠的是一个人才育成的文化。丰田的人才育成理念,人才培养是企业的使命。

还有丰田的三造文化,造钱、造物、造人。它把培养人当成企业经营过程当中重要一环,当然企业最重要要创造利润,然后要造出满足客户的车辆来,在这个过程当中,培育成忠诚于企业、知道改善和发现问题的方法,并且有较高的技术技能的这样的员工。这是丰田成功的关键。所以我们说丰田的造人哲学是由人、才、物三个要素组成,突出是丰田既要造车、又要造人。这在我们许多许多的企业当中,有的时候我们是体会不到的。因为在五六年的咨询培训生涯当中,我走过了许多许多的企业,很多的企业急于要掌握丰田生产方式或者是精益生产方式的工具和方法,甚至很多企业跟我说,“王老师能否迅速把这套工具和方法教给我们”,很多企业都这样问,这样想,都想急于求成的解决他们当前在

37从精益生产到精益设计(齐二石)_齐二石

内部流程或者外部流程感到非常棘手和困惑的问题,但是育人、培养人、造就忠诚于企业并且有较高技能的员工,这样的整个的育人机制往往是很薄弱。

丰田人是怎样做的呢?他们怎么做?他们在经营过程当中,除了促进员工形成家庭观念,“一心一意一家”三个一的思想。还有从一而终,培养员工对企业的忠诚度。

另外,大家看一看,自下而上进行角色,这个角色更多是通过全员参与的创意工夫,我们说改善活动来完成。培养员工成为多能工。这是一个机制,这是一个完整的造人体系,所以丰田人应该实现了最终的目标,就是底下说既要造车,也要造人。

我们看一看案例,就是石田的造人运动。现在我们面对是世界金融危机的到来,但是大家应该了解,丰田其实也遇到过两次比较大的危机。第一,40年代到50年代的时候,尤其销售受挫,遭受到劳资纠纷这个重大课题,最之导致第一任丰田公司的社长引咎辞职,接着来的是60岁的石田退三,他怎么做的?他面对复杂的形势,他认为丰田在这个时候(1949年)资源缺乏,销售受挫,劳资纠纷,还有资金紧张、原材料短缺等困扰丰田,石田认为这些都不重要,他认为没有人才,必须要培养人,只有有了人,丰田一定能够成功。因此对于石田来说,培养人才就是用丰田精神教育员工,所以他把丰田的纲领乃至丰田精神当中引入了造人的思想。形成了特有的石田精神。

三大体系的建立,大家看一看,建立教育制度,包括职场教育、(OJT)导入教育(OFFJT)和自我启发教育。这三大体系的建立一直延续到现在。凭借着这些,石田带领他自己的团队,一步一步把丰田一点一点的从低谷当中带出来,造人运动的特点就是充分发挥员工的自主性,通过发挥员工自下而上的参与、去变革、去改善,去发现问题,在这样的特定的环境之下,石田成功了。成功的标志在于36亿的当时在韩战期间,36亿的巨额订单包括打入美国市场的第一辆汽车以及入市。所有这些辉煌的成绩表明丰田已经从低谷当中走出来了。因此造人运动确确实实给丰田带来了勃勃生机。

然后石田退三也因为造人运动的成功被誉为“复兴之子”,截止到现在,丰田董事会和丰田家族的人非常缅怀这位前辈。每到他祭日的时候,所有丰田董事会成员和丰田家族男女老少一律正装祭拜这位先生。

所以我们看一看,从50年代,到现在,丰田人就是这样来培养人的。

我们看一看,丰田人是靠什么?它的育人体系怎么样来做的?这是丰田人才育成的三者关系图。(PPT)大家看一看,从图上关键的核心环就是改善,持续不断的进行改善,这是丰田的一种精神,是丰田的一种文化。大家看看第一环是创意功夫,体现在个人上,通过成本、安全、品质等各项主题的研发,因此把员工培养出来。第二环是通过QC(集体)小组活动创意改善工夫和QC小组活动是整个TPS体系当中的基础。我们常说的是基石。它是靠什么?是靠集体。通过这种品质的改善,培养团队的精神。最后是PTS整体丰田生产方式的导入,这种方式的导入旨在彻底的消除浪费并且创造一个与众不同的全新的生产模式,工厂文化变革。所以从这个三者关系图上可以看出丰田人才育成的三者之间的关联。

大家看一看丰田人才育成的程序图。我们看到有全员导入的教育,我们称之为业余教育,包括知识、技能、资格自我教育以及语言等等,大家知道不知道?所有的丰田员工入社的时候受到的一项教育就是全员参与的创意功夫,每一个新员工都要教育。在很多企业当中,有时候在这方面是做不到的。另外,丰田人赋予所有员工一个神圣的使命,就是它的自动化的原理,任何人遇到不良异常都可以说出来,这样的教育在员工入社的时候,在员工进入公司的时候就已经打下的烙印。

我们再看一看员工的改善技术和OJT的训练,丰田几乎所有的工厂都在他的道场,就是我们说的训练所。这些训练所的目的是什么?就是培养员工的技能,挖掘员工最大的学习力。这是我们国家一些企业当中的一个弱项。

最后我们看一看本部门的教育。从本部门的各项专业教育包括所有人专有人才的培养等等,当然也包括了一些多能工,技能职和事务职的训练。

以上是丰田人才育成的整体程序。

看一看丰田这几位育人大师。石田退三是造人运动,丰田英二被称之为日本的福特,丰田英二从1950年开始倡导的全员创意的改善工程,就是我们所说的创意体验制,旨在挖掘员工的潜力,通过全员参与对公司经营提出了合理化建议,改善人、改善企业的体制,改变人的体制。大野耐一是精益生产的集大成者,从三线主义到连续五个为什么,从这一系列的精益工具的开发到实战运用,其实都是通过人来做的。大野耐一不仅是一个精益生产的集大成者,更关键的是大野耐一也是一个育人大师,包括齐院长昨天讲到他的女婿,包括清水,我们说的白水先生,包括我们谢谢克俭老师的老师林南发老师都是大野耐一的老师。到后来的TPS的人,这些人成为了TPS推进的骨干。张富士夫是丰田的DNA,是新时期丰田文化的文化创造者。他有一句名言,“80年代我们在进军全球市场的时候,并不是一味在全球市场购买设备、制造汽车,更关键的是把丰田的DNA输入到全球,”这句话说得非常有哲理性。 接下来研讨一个案例,看一看中国南车集团精益特性是什么?中国南车集团大家都非常熟悉,在座的各位朋友,当我们坐上宽敞舒适的动车组的时候,我们就会想到这个车是谁做的?这些车辆就出自于中国南车集团之手。中国南车集团在新世纪到来的时候,在整个集团上市的同时,面对着国际市场的激烈的竞争,集团公司在集团领导的率领下,全集团开始导入精益生产,导入精益制造。精益制造的目的是什么?就是打造中国南车集团的品牌,提高企业的竞争实力。我们看一看这些案例。

(PPT)转变观念、积极参与。中国南车集团在推行精益生产的过程当中,从集团公司的领导到集团公司下属各个企业的主要的精益推进的团队和精益推进的担当者,我们说的精益的人才和未来人才多次举行这样的训练营。我们看一看从训练营的开幕到训练营的入场的流程,从训练营会场纪律的遵守,所有的这些全部都是按照非常标准的流程做的。这是全员参与的一个积极的氛围,这样一个氛围在集团公司内部进行的。旨在培养整个集团公司包括下属各个分公司、子公司的精益改善人才。

通过沙盘演练、模拟实战、精益工具的导入来理解精益。大家看一看这些照片,这些多少下属各个工厂、各个企业进行精益人才培养的模拟实战活动。这些原汁原味的仿TPS的模拟实战,积极的研讨,所有的TPS的成员在实战当中拉出来的所有的问题进行了仔细的商讨。

接下来是现场实战。通过实时改善消除工厂的各种浪费,在这个实战过程当中,这是非常重要的一环,在这一环当中主要是培养精益人才发闪问题、改善问题的能力。在训练教官的带领下,这些学员都深入到公司、工厂的现场,到工厂的现场当中挖掘问题、设备、现场、环境、工艺、技术、工装、制造、方法,所有的问题从现场当中都拉出来,拉出来的目的是什么?是培养大家发现问题、改善问题的能力。通过这些问题的提升,为企业未来导入精益生产奠定良好的人才育成的基础。

最后是经验总结和成果的分享。我们看一看这些全是他们的精益人才,是未来的培养梯队,通过自己的训练,通过自己的活动,把自己几天来的经验、改善成果都总结出来,然后进行这方面的分享,和所有的与会者共同进行交流。

我们看一看丰田公司前社长张富士夫的这计划我们最终使的是确实执行与采取行动——我们总是要求员工,何何不采取行动尝试不同的方法呢?于是通过不断改进,或影应该说靠不断尝试的行动已获得的改进就能提升实务与知识。张富士夫作为一个前丰田公司的社长能够把育人当成企业的一个战略要求提出来,这一点是非常非常难能可贵的。因此造成的丰田持续改进的文化。

接下来再看一个案例。在企业如何培养人?在造就人方面,我们可以看一看中国航天工业公司哈飞集团他们的TPM主题改善活动。哈飞集团是我们国家飞机制造行业当中的先驱者,并且它是波音飞机主要的供应商,大家都知道,波音飞机全球战略性的采购,当然也包括哈飞。哈飞有很多很多的产品给波音进行配套。波音飞机通过精益生产的导入现在已经在全球制造业树立了楷模。如何适应波音飞机?如何适应现在飞速发展的航空工业?哈飞在这条路上自己走出来了自己的精益人才的培养之路。特别是他们导入TPM活动的时候,全员参与,整个示范区当中的几百名工人全部都投入到活动当中去,并且创造了各式各样的活动小组。

(PPT)几乎几十个、上百各课题都产生了,比如说如何减少磨削工序当中的返修率,有提高加工效率的问题,有环境的问题,各种各样的主题改善变成了这些员工自发改善的关键课题,每一个小组,每一个TPM主题活动小组都把这些课题当成自己要实施和完成的重要工作,并且集团公司、分公司进行考核。

这是实施主题过程当中的改善,生产效率、品质、环境、工装、工具、技术、工艺,所有这些都拉出来了,拉出来以后进行员工参与的自发的改善。通过这种改善,收到非常大的效果。加工质量越来越好,而且停停机故障率越来越低,生产效率明显提高,综合指标也提升了,现场环境也改善了,包括员工发现问题的能力和改善环境的能力的提升,培养了精益的人才。在这个工厂当中,在参加全省全国和行业内的技术比武比赛当中,涌现出来了一大批的技术能手,行业在全国、全省当中一二三名的获奖者很多,就出自于我们刚才看到的这些团队。所以在座的各位朋友,我们可以想一想,人才育成对于我们来讲在推行精益生产过程当中其实是一个非常非常关键的战略的一环。

我们接下来看一看丰田员工岗位技能的训练。(PPT)这张图可以明显的看到,从基本技能到指导改善能力,一步一步有改善的阶梯,从事年限一步一步造就人才,改善人才。丰田人在这方面来讲已经做到了极致。这是我们看到的技能训练的方法。

接下来再介绍一个案例。就是关于西开集团的精益班组长训练。西开集团是我国重要的大型企业,并且承担着我国三峡等很多的重要工程建设,包括电力工程和国外的一些关键工程。几年来,西开集团(西开高压电器)在企业经营过程当中持续不断的导入精益生产,在精益生产推行更关键的是注重如何培养他们的继承管理者。这个问题现在已经变成了我们许多企业非常关键的一环。我们有许多的企业向我发出邀请,说王老师,希望给我们的基层管理者特别是班组长、车间主任这一层的管理者进行系统的精益生产和精益管理的培训。原因在于什么?企业要发展!在座各位朋友的企业都是这样,不管是国营的民营的、还是中外或者是外企,企业都在不断的发展,人才的梯队能否跟上企业迅猛发展的现实?这是非常关键的。

我们看一看他们,比如说精益理念的培训。

(PPT)通过精益理念的培训,通过国学文化、精益文化和我们说的国学和精益文化相结合的培训。还有学员们自己对企业存在的问题、存在的各种各样的急于改善的问题进行研讨。包括每一个学员发自内心的把自己的问题提出来,通过培训、通过训练来了解精益的知识,通过提出问题寻求改善的方法。 这是现场的实战活动,基层管理者本身就是在生产现场,但是发现问题和改善问题的能力的提升这是基层管理者非常关键的,许多老总都在跟我说,“王老师,我们现在企业不缺别的,就缺一种?缺什么?缺基层管理者发现问题和改善问题的能力。”似乎他们更多充当一个大头兵,更多的是充当一个安排计划、简单照顾一下自己自己班组的生产进度。但是如何用精益思想和精益工具甚至精益文化去发现问题、改善问题、去变革自己的班组,去变革自己的企业?这一点对于很多企业来讲都是软肋。这一点西开集团做到了前面,他们轮番的一批一批对这些班组长进行系列的、系统的教育和培训。比如说5S、TPM,包括作业方法的训练和作业动作,在作业动作当中发现浪费的训练,通过这些提升基层管理者的能力。

我们再看一看丰田的创意功夫。在整个丰田的育人体系当中,创意功夫是一个非常关键的一环。从1949年石石田退三的造人运动,到1950年石田英二借鉴福特公司的“动脑筋创新”制度,创造了具有日本丰田公司特色的创意功夫,是全员参与的合理化改善体验活动。到2000年的时候,合理化建议稿达65万条,人均11.9条。

今年一月份,我带领国家制造业的研究团去日本丰田公司考察,到他们第一工厂,1995年由三星集团从三星集团选送到日本丰田公司研修,第一工厂的时候2000名员工人均体验件数高达17件,大家可以算一下,人均17件是什么样的数字?并且实施采纳率高达95%。看一看丰田创意功夫的业绩。1974年一直到2000年60多万件的提案采纳率高达到99%,这些提案其实都是员工身边的事情,作业方法改善、消除浪费、动作的合理性、齐院长讲到的IE工程、许许多多品质、工艺上的小改小革,通过提案改善的员工的体制,引发员工的创意思维,并且使整体公司的文化和公司的业绩有了提升。 接下来介绍一个案例,看一看中联重科(14.08,0.09,0.64%,吧)的起重机公司的改善提案活动,昨天有嘉宾提到中联重科公司,它是国家大型的国有企业,目前来看它在同行业当中是一个佼佼者。企业为了适应飞速发展的国际化的竞争态势,在全公司开始导入创意提案活动。目的在于什么?很简单,就是提升员工的素质,提升员工发现问题,解决问题的能力,使整个企业的水平向上。

我们看一看活动的案例。总经理亲自挂帅,他作为创意功夫委员会的委员长亲自挂帅,亲自进行培训。还有直上直下,班组长、中基层管理者的培训,体验活动现况板,强调骨干为引领,他们首先来提案,带领员工提案,很多现场都有改善提案的氛围的营造。还有标语板,提示板,在现场非常多。这些优秀的改善提案反映都是在生产、经营活动当中各种各样的问题。

提案带来的什么?这项活动给这个企业带来了什么?朋友们可以看看现场,是非常非常干净,作为下料、做起重设备的工厂,从下料到深加工,从装配到各种各样零配件的生产,现场非常干净,进去以后给人耳目一新的感觉,所有的提案都源自于员工之手。

精益改善人才培养的目的,培养能实现公司精益理念和精益实践的推进者、实践者;造就发展及激励公司员工与团队;营造尊重他人、城市待人、相互理解、互相负责的氛围;培养人才,集合每个人的力量推进精益;建立持续推进精益的人才培养机制,很多企业在这方面做得非常弱;实现精益目标;精益领导和管理人才的诞生;形成精益企业文化。

看精益人才育成的真谛。冰山,表面是精益工具,但是风吹日晒,这些工具迟早会化掉,因为他们没有牢固的基础,但是如果把人才育成和文化变革与这座冰山露出来的冰峰露出在一起的话,底下是员工持续不断的改善,消除浪费,造就一种文化的诞生,那么这种冰山将永远不会熔化,企业的成功也就在此。

非常感谢朋友们,祝大家改善成功

五 : LabVIEW中通用采集卡驱动程序设计

本文标题:嵌入式linux驱动程序设计从入门到精通-Linux入门教程
本文地址: http://www.61k.com/1142303.html

61阅读| 精彩专题| 最新文章| 热门文章| 苏ICP备13036349号-1