• 1. Linux驱动开发入门与实战
  • 2. 第1章 Linux驱动开发概述设备驱动程序是计算机硬件与应用程序的接口,是软件系统与硬件系统沟通的桥梁。如果没有设备驱动程序,那么硬件设备就只是一堆废铁,没有一点的功能。本章将对Linux驱动开发进行简要的概述,使读者理解一些常见的概念。
  • 3. 1.1 Linux设备驱动的基本概念本节对中断相关概念进行了简要的分析,并对中断进行了分类。根据不同的中断类型,写中断驱动程序的方法也不一样。下面将主要介绍中断的基本概念和常见分类。
  • 4. 1.1.1 设备驱动程序概述 设备驱动程序(Device Driver),简称驱动程序(Driver)。它是一个允许计算机软件(Computer Software)与硬件(Hardware)交互的程序。这种程序建立了一个硬件与硬件,或硬件与软件沟通的界面。CPU经由主板上的总线(Bus)或其它沟通子系统(Subsystem)与硬件形成连接,这样的连接使得硬件设备(Device)之间的数据交换成为可能。
  • 5. 1.1.2 设备驱动程序的作用设备驱动程序是一种可以使计算机和设备通信的特殊程序,可以说相当于硬件的接口。操作系统只有通过这个接口,才能控制硬件设备的工作。假如某设备的驱动程序未能正确安装,便不能正常工作。正因为这个原因,驱动程序在系统中的所占的地位十分重要。一般当操作系统安装完毕后,首要的便是安装硬件设备的驱动程序。
  • 6. 1.1.3 设备驱动的分类计算机系统的主要硬件由CPU、存储器和外部设备组成。驱动程序的对象一般是存储器和外部设备。随着芯片制造工艺的提高,为了节约成本,通常将很多原属于外部设备的控制器嵌入到CPU内部。所以现在驱动程序应该支持CPU中的嵌入控制器。Linux将这些设备分为3大类:字符设备、块设备、网络设备。
  • 7. 1.2 Linux操作系统与驱动的关系Linux操作系统与设备驱动之间的关系如图所示。用户空间包括应用程序和系统调用两层。应用程序一般依赖于函数库,而函数库是由系统调用来编写的,所以应用程序间接地依赖于系统调用。
  • 8. 1.3 Linux驱动程序开发Linux驱动程序的开发与应用程序的开发有很大的差别。这些差别导致了编写Linux设备驱动程序与编写应用程序有本质的区别,所以对于应用程序的设计技巧很难直接应用在驱动程序的开发上。本节将对Linux驱动程序的开发进行简要的讲解。
  • 9. 1.3.1 用户态和内核态Linux操作系统分为用户态和内核态。用户态处理上层的软件工作。内核态用来管理用户态的程序,完成用户态请求的工作。驱动程序与底层的硬件交互,所以工作在内核态。
  • 10. 1.3.2 模块机制模块是可以在运行时加入内核的代码,这是Linux的一个很好的特性。这个特性使内核可以很容易的扩大或者缩小,一方面扩大内核可以增加内核的功能,另一方面缩小内核可以减小内核的大小。
  • 11. 1.3.3 编写设备驱动程序需要了解的知识Linux操作系统有三四百万行代码,其中驱动程序代码就有四分之三左右。所以对于驱动开发者来说,学习和编写设备驱动程序都是一个漫长的过程。在这个过程中,读者应该掌握如下的一些知识: (1)驱动开发人员应该有良好的C语言基础。 (2)驱动开发人员应该有良好的硬件基础。 (3)驱动开发人员应该对Linux内核源代码有初步的了解。 (4)驱动开发人员应该有多任务程序设计的能力。
  • 12. 1.4 编写设备驱动程序的注意事项大部分程序员都比较熟悉应用程序的编写,但是对于驱动程序的编写可能不是很熟悉。关于应用程序的很多编程经验不能直接的应用于驱动程序的编写中来。下面给出编写驱动程序的一些注意事项,希望引起读者注意。
  • 13. 1.4.1 应用程序开发与驱动程序开发的差异在Linux上的程序开发一般分为两种,一种是内核及驱动程序开发,另一种是应用程序开发。这两种开发种类对应Linux的两种状态,分别是内核态和用户态。内核态用来管理用户态的程序,完成用户态请求的工作;用户态处理上层的软件工作。驱动程序与底层的硬件交互,所以工作在内核态。
  • 14. 1.4.2 GUN C开发驱动程序GUN C语言最开始起源于一个GUN计划,GUN的意思是“GUN is not UNIX”。GUN计划开始于1984年,这个计划的目的是开发一个类似UNIX并且软件自由的完整操作系统。这个计划一直进行,直到Linus开发Linux操作系统时,GNU计划已经开发出来了很多高质量的自由软件。其中就包括著名的GCC编译器,GCC编译器能够编译GUN C语言。Linus考虑到GUN计划的自由和免费,所以选择了GCC编译器来编写内核代码,之后的很多开发者也使用这个编译器,所以直到现在,驱动开发人员也使用GUN C语言来开放驱动程序。
  • 15. 1.4.3 不能使用C库来开发驱动程序与用户空间的应用程序不同,内核不能调用标准的C函数库,主要的原因在于对于内核来说完整的C库太大了。一个编译的内核大小可以是1M左右的字节,而一个标准的C语言库大小可能操作5M字节。这对于存储容量较小的嵌入式设备来说,是不实用的。缺少标志C语言库,并不是说驱动程序就只能做很好的事情了。
  • 16. 1.4.4 没有内存保护机制当一个用户应用程序由于编程错误,试图访问一个非法的内存空间,那么操作系统内核会结束这个进程,并返回错误码。应用程序可以在操作系统内核的帮助下,恢复过来,而且应用程序并不会对操作系统内核有太大的影响。但是如果当操作系统内核访问了一个非法的内存,那么就有可能苹的诤说拇或者数据。这将导致内核处于未知的状态,内核会通过oops错误给用户一些提示,但是这些提示都是不支持,难以分析的。
  • 17. 1.4.5 小内核栈用户空间的程序可以从栈上分配大量的空间来存放变量,甚至用栈存放巨大的数据结构或者数组都没问题。之所以能这样做是因为应用程序是非常驻内存的,它们可以动态的申请和释放所有可用的内存空间。内核要求使用固定常驻的内存空间,因此要求尽量少的占用常驻内存,而尽量多的留出内存提供给用户程序使用。因此内核栈的长度是固定大小的,不可动态增长的32位机的内核栈是8KB;64位机的内核栈是16KB。
  • 18. 1.4.6 重视可移植性对于用户空间的应用程序来说,可移植性一直是一个重要的问题。一般可移植性通过两种方式来实现。一种方式是定义一套可移植的API,然后对这套API在个这两个需要移植的平台上分别实现。应用程序开发人员,只要使用这套可移植的API,就可以写出可移植的程序。在嵌入式领域,比较常见的API套件是QT。另一种方式是使用类似JAVA、actionscript等可移植到很多操作系统上的语言。这些语言一般通过虚拟机来执行,所以可以移植到很多平台上。
  • 19. 1.5 Linux驱动的发展趋势随着嵌入式技术的发展,使用Linux的嵌入式设备也越来越多。同样地,工业上对Linux驱动的开发也越来越重视。本节对Linux驱动的发展做简要的介绍。
  • 20. 1.5.1 Linux驱动的发展Linux和嵌入式Linux软件在过去几年里已经越来越普遍的为IT业、半导体公司、嵌入式系统所认可和接受,它已经成为一个可以替代微软的Windows和众多传统的RTOS的重要的操作系统。Linux内核和基本组件和工具已经是成熟的软件,面向行业,应用和设备的嵌入式Linux工具软件和嵌入式Linux操作系统平台是未来发展的必然趋势。符合标准,遵循开放是大势所趋,人心所向,嵌入式Linux也不例外。
  • 21. 1.5.2 驱动的应用计算机系统已经融入到了各行各业、各个领域;计算机系统在电子产品中无处不在,从手机、游戏机、冰箱、电视、洗衣机等小型设备,到汽车、轮船、火车、飞机等大型设备都有它的身影。这些设备都需要驱动程序来使之运行,可以说驱动程序的运用前景是非常广泛的。每一天都有很多驱动程序需要去编写,所以驱动程序开发人员的前途是无比光明的。
  • 22. 1.5.3 相关学习资源学习Linux设备驱动程序,单单只是学习理论是不够的,还需要自动动手来写各种设备的驱动程序。写驱动程序不仅需要读者的软件知识,还需要读者的硬件知识。在这里,推荐一些国内外优秀的驱动开发网站,希望读者的学习有所帮助。 (1)Linux内核之旅网站:http://www.kerneltravel.net/ (2)知名博客:http://www.lupaworld.com/26540 (3)Linux中国:http://www.linux-cn.com/ (4)一个不错的Linux中文社区:http://www.linux-cn.com/ (5)csdn内核驱动研究社区:http://topic.csdn.net/s/Linux_Dirver/0.html (6)Linux伊甸园:http://bbs.linuxeden.com/index.php
  • 23. 1.6 小结本章首先对Linux设备驱动程序的基本概念进行了详细的讲述;并且讲述了设备驱动程序的作用;接着讲述了设备驱动程序的分类、特点、与操作系统之间的关系等;然后讲述了驱动程序开发的一些重要知识和一些注意事项。最后讲述了Linux驱动程序的发展趋势。通过本章的学校,读者可以对Linux设备驱动程序的开发有一个概要的了解。 随着嵌入式设备的迅猛出现,有越来越多的驱动程序需要程序员去编写,所以学习驱动程序的开发对个人的进步是非常有帮助的。本章作为驱动程序开发的入门,希望能够引起读者的学习兴趣。
  • 24. 第2章 嵌入式处理器和开发板简介在实际的工程项目中,Linux驱动程序一般是为嵌入式系统而写的。因为嵌入式系统因用途、功能、设计厂商不同,硬件之间存在很多的差异。这些差异性,不能通过写一个通用的驱动程序来完成,需要针对不同的设备书写不同的驱动程序。要写驱动程序,必须了解处理器和开发板的相关信息,本章将对这些信息进行详解讲解。
  • 25. 2.1 处理器的选择本节对处理器的概念进行了简要的讲解,并介绍了一些常用的处理器种类,以使读者对嵌入式系统的处理器有初步的认识。
  • 26. 2.1.1 处理器简述处理器是解释并执行指令的功能部件。每个处理器都有一个独特的诸如mov、add或sub这样的操作命令集,这个操作集被称为指令系统。在计算机诞生初期,设计者喜欢将计算机称为机器,所以该指令系统有时也称作机器指令系统。
  • 27. 2.1.2 处理器的种类处理器作为一种高科技产品,其技术含量非常高,目前全世界只有少数厂商能够设计。这些厂商主要有Intel、AMD、ARM、中国威盛、Cyrix、IBM和龙芯等。目前,处理器在嵌入式领域应用十分广泛,各大厂商都推出了自己的嵌入式处理器,主要的嵌入式处理器有:英特尔的PXA系列处理器、StrongARM 系列处理器、MIPS处理器、摩托罗拉龙珠 (DragonBall)系列处理器、日立SH3处理器和德州仪器OMAP系列处理器。了解这些嵌入式处理器的特性,是驱动开发人员必须补的一课,所以本节对这些常用的处理器进行简要的介绍。 1.英特尔的PXA系列处理器 2.StrongARM 系列处理器 3.MIPS处理器 4.摩托罗拉龙珠 (DragonBall) 系列处理器 5.日立SH3处理器 6.德州仪器OMAP系列处理器
  • 28. 2.2 ARM处理器ARM处理器价格便宜,功能相对较多,是目前最为流行的嵌入式处理器之一。ARM处理器分为很多种类,适用于不同的应用。本节对其进行详细介绍。
  • 29. 2.2.1 ARM处理器简介ARM处理器是目前最为流行的处理器之一,下面对该处理器的一些常识进行介绍。 1.ARM处理器 2.ARM处理器的特点
  • 30. 2.2.2 ARM处理器系列ARM处理器当前有6个产品系列:ARM7、ARM9、ARM9E、ARM10、ARM11 和SecurCore,其中ARM11为最近推出的产品。进一步产品来自于合作伙伴,例如Intel Xscale 微体系结构和StrongARM 产品。ARM7、ARM9、ARM9E、ARM10是4个通用处理器系列。每个系列提供一套特定的性能来满足设计者对功耗、性能、体积的需求。SecurCore是第5个产品系列,是专门为安全设备而设计的。目前中国市场应用较成熟的ARM处理器以ARM7TDMI和ARM9核为主。主要的厂家有SAMSUNG、ATMEL、OKI等知名半导体厂商。
  • 31. 2.2.3 ARM处理器的应用虽然8位微控制器仍然占据着低端嵌入式产品的大部分市场,但是随着应用的增加,ARM处理器的应用也越来越广泛。
  • 32. 2.2.4 ARM处理器的选型随着国内外嵌入式应用领域的发展,ARM芯片必然会获得广泛的重视和应用。但是,由于ARM芯片有多达十几种的芯核结构,100多家芯片生产厂家,以及千变万化的内部功能配置组合,给开发人员在选择方案时带来一定的困难。本节将从应用的角度,介绍ARM芯片选择的一般原则。
  • 33. 2.2.5 ARM处理器选型举例在选择处理器的过程中,应该选择合适的处理器。所谓合适就是在能够满足功能的前提下,选择价格尽量便宜的处理器,这样开发出来的产品更具有市场竞争力。消费者也可以从合适的搭配中,找到性价比高的产品,满足消费者的需求。
  • 34. 2.3 S3C2440开发板S3C2440开发板上集成了一块S3C2440处理器。S3C2440处理器是ARM处理器中的一款。其广泛使用在无线通信、工业控制、消费电子领域。本节将对S3C2440开发板进行详细的介绍。
  • 35. 2.3.1 S3C2440开发板简介 目前大多数拥有ARM处理的开发板都是基于S3C2440处理器的。基于S3C2440的开发板由于资料全面、扩展功能好、性能稳定三大特点,深受广大嵌入式学习者和嵌入式开发工程师喜爱。这种开发板由于性能较高,一般可以应用于车载手持、GIS平台、Data Servers、VOIP、网络终端、工业控制、检测设备、仪器仪表、智能终端、医疗器械、安全监控等产品中。
  • 36. 2.3.2 S3C2440开发板的特性基于S3C2440开发板包含了许多实用的特性,这些特性都是驱动开发人员练习驱动开发的好的材料。下面对这些开发板一般都具有的特性进行介绍。 1.CPU 处理器 2.SDRAM内存 3.FLASH 存储 4.LCD 显示 5.接口和资源 6.系统时钟源 7.实时时钟 8.扩展接口 9.操作系统支持
  • 37. 2.4 小结本章简单的讲解了驱动开发人员必备的处理器器知识,详细介绍了S3C2440处理器构建的开发板。对驱动开放人员来说更为重要的是,处理器选型问题。本章不仅给出了详细的准则,而且对常见应用的选型进行了举例,相信读者从本章中会有所收获。
  • 38. 第3章 构建嵌入式驱动程序开发环境在编写驱动程序之前,需要构建一个合适的开发环境。这个环境包括合适的Linux操作系统、网络、交叉编译工具、以及NFS服务等。为了使读者顺利地完成开发环境的构建,本章将对这些主要内容进行讲解。
  • 39. 3.1 虚拟机和Linux安装由于驱动开发需要涉及不同操作系统的功能,所以需要安装不同的操作系统。一般开发者偏好在Windows系统上安装虚拟机,然后在虚拟机上安装Linux系统。这种方式,可以使一台主机模拟多台主机的功能,从而提高开发的效率。这里,首先介绍安装虚拟机的方法。
  • 40. 3.1.1 在Windows上安装虚拟机 在Window上安装虚拟机,可以有多种选择。目前流行的虚拟机软件有VMware和Virtual PC。它们都能在Windows系统上虚拟出多个计算机,用于安装Linux、OS/2、FreeBSD等其他操作系统。微软在2003年2月份收购Connectix后,很快发布了Microsoft Virtual PC。但出于种种考虑,新发布的Virtual PC已不再明确支持Linux、FreeBSD、NetWare、Solaris等操作系统,只保留了OS/2,如果要虚拟一台Linux计算机,只能自己手工设置。
  • 41. 3.1.2 在虚拟机上安装Linux本节将介绍怎样在虚拟机上安装Fedora 9.0,并详细介绍了如何建立Linux开发环境。下面对安装步骤进行详细的说明: (1)在虚拟机的光驱上选择Fedora 9.0的光盘镜像文件,然后启动虚拟机,进入安装界面 (2)然后进入检查安装盘的界面,如果节约时间,这里可以直接跳过(Skip) (3)当检查完之后,就会进入图形安装界面,这里的安装方法和Windows的安装方法类似。在安装过程中,用户可以选择安装的语言、键盘类型(一般为U.S.English式键盘)、网络地址等。安装过程较为简单,用户可以根据提示进行选择和设置,这里就不详细讲解了。
  • 42. 3.1.2 设置共享目录在网络连接畅通的情况下,虚拟机和Windows之间可以通过共享文件,来完成两个系统的通信。设置共享文件,需要在Windows设置共享文件夹,而且还需要在虚拟机上进行一些设置,这个过程如下所示: (1)在Windows系统中共享文件夹share,右键单击文件夹,然后选择【共享这个文件夹】单选框 (2)在虚拟机中设置网络连接(Network connection)为Birdged方式,这种方式可以使同一台机器上的两个操作系统之间能够通信 (3)在Fedora9中,打开Connect to Server对话框,填写相应的服务器ip地址、共享文件夹、用户名和密码就能够访问Windows上的共享文件夹
  • 43. 3.2 代码阅读工具Source Insight单独用一节来讲解代码阅读工具是否值得,答案是值得的。因为Linux内核有500多万行代码,其中驱动程序占了2/3以上。阅读和理解这些代码,对编写设备驱动程序来说是非常有帮助的,所以本节将告诉大家怎样有效的使用代码阅读工具阅读代码。
  • 44. 3.1.2 Source Insight简介 Source Insight是一个非常好的代码阅读、编辑和分析的工具。Source Insight支持目前大多数流行编程语言,如C、C++、ASM、PAS、ASP、HTML等。这个软件还支持关键字定义,对开发人员来说是非常有用的。Source Insight不但能够编写程序,有代码自动提示的功能,而且还能过显示引用树、类图结构、调用关系等等。
  • 45. 3.1.2 阅读源代码1.建立Source Insight工程 2.更新数据库 3.Source Insight使用示例
  • 46. 3.2 小结本章简要的介绍了驱动程序开发的一般环境,主要介绍了虚拟机和Linux操作系统的安装。另外,在驱动程序开发过程中,Windows系统和Linux操作系统之间传输数据也非常的重要,所以本章也介绍了文件共享的方法。最后介绍了一个分析和阅读源代码的工具,在实际的应用中非常有用。
  • 47. 第4章 构建嵌入式Linux操作系统目前流行的嵌入式操作系统有Linux、WinCE、VxWorks等。Linux作为一种免费的类UNIX操作系统,由于其功能强大,在嵌入式产品的应用中非常广泛。本章将对Linux操作系统做简单的介绍,并简述怎么自己构建一个可以运行的Linux操作系统。
  • 48. 4.1 Linux操作系统的介绍Linux操作系统是嵌入式系统的主流操作系统,本节将对Linux操作系统进行简要的介绍。同时对Linux操作系统适用与嵌入式系统的原因进行简要分析。
  • 49. 4.1.1 Linux操作系统Linux操作系统是一个类Unix计算机操作系统。Linux操作系统的内核的名字也是“Linux”。Linux这个词本身只表示Linux内核,但在实际上人们已经习惯了用Linux来形容整个基于Linux内核的操作系统。Linux的最初版本由Linus Torvalds开发,此后得到互联网上很多计算机高手的支持,目前的版本已经到了2.6,已经是一个非常成熟稳定的操作系统。下面从不同方面对Linux操作系统进行简要的介绍。
  • 50. 4.1.2 Linux操作系统的优点Linux操作系统有很多优点,具有十分丰富的应用功能。这些功能特别适用于嵌入式系统,这些优点如下所示: 1.价格低廉 2.高效性和灵活性 3.广泛性
  • 51. 4.2.2 内存管理内存是计算机的主要资源之一,可以将内存理解为一个线性的存储结构。用来管理内存的策略是决定系统性能的主要因素。内核在有限的资源上为每一个进程创建一个虚拟地址空间,并对虚拟地址空间进行管理。为了方便内存的管理,内核提供了一些重要的函数。这些函数包括kmalloc、kfree等。另外设备驱动程序需要使用内存分配,不同的分配方式对驱动程序的影响不同,所以需要对内存分配有比较清晰的了解。
  • 52. 4.2.3 文件系统在Linux操作系统中,文件系统是用来组织、管理、存放文件的一套管理机制。Linux文件系统的一大优点是,它几乎可以支持所有的文件格式。任何一种新的文件格式,都可以容易的写出相应的支持代码,并无缝的添加入内核中。虽然不同文件格式的文件以不同的存储方式存放在磁盘设备中,但是在用户看来,文件总以树形结构显示给用户。
  • 53. 4.2.4 设备管理无论是桌面系统还是嵌入式系统,都存在各种类型的设备。操作系统的一个重要功能就是对这些进行统一的管理。由于设备的种类繁多,不同设备的操作方法都不一样,使管理设备成为操作系统中非常复杂的部分。Linux系统通过某种方式较好的解决了这个问题,使设备的管理得到了统一。 设备管理的一个主要任务是完成数据从设备到内存的传输。一个完全的数据传输过程是:数据首先从设备传入内存,然后CPU对其进行处理,处理完后将数据传入内存或设备中。
  • 54. 4.2.5 网络功能网络功能也由操作系统来完成。大部分的网络操作与用户进程都是分离的,数据包的接收和发送操作都是由相应的驱动程序来完成的,而与用户进程无关。进程处理数据之前,驱动程序必须先收集,标识和发送或重组数据。当数据准备好后,系统负责用户进程和网络接口之间的数据传送。另外内核也负责实现网络通信协议。
  • 55. 4.3 Linux源代码结构分析了解Linux源代码结构对理解Linux如何实现各项功能是非常重要的。对驱动程序的编写也非常重要,这样,驱动开发人员知道应该在何处找到相关的驱动程序,一方面可以对其进行改写移植,另一个方面可以模仿以往的驱动程序,写出新的驱动程序。Linux源代码以目录的方式组织,每一个目录中有相关的内核代码。下面对各个主要的目录进行介绍。
  • 56. 4.3.1 arch目录随着Linux操作系统的广泛应用,特别是Linux在嵌入式领域的发展,越来越多的人开始投身到 Linux驱动的开发中。面对日益庞大的Linux内核源代码,驱动开发者在完成自己的内核代码后,都将面临着同样的问题,即如何将源代码融入到Linux内核中,增加相应的Linux配置选项,并最终被编译进Linux内核。这就需要对Linux源代码结构进行详细的介绍,首先介绍arch目录。
  • 57. 4.3.2 drivers目录drivers目录中包含了Linux内核支持的大部分驱动程序。每种驱动程序都占用一个子目录。
  • 58. 4.3.3 fs目录fs目录中包含了Linux所支持的所有文件系统相关的代码。每一个子目录中包含一种文件系统,例如msdos和ext3。Linux几乎支持目前所有的文件系统,如果发现一种没有支持的新文件系统,那么可以很方便的在fs目录中添加一个新的文件系统目录,并实现一种文件系统。
  • 59. 4.3.4 其他目录除了上面介绍的目录外,内核中还有其他一些重要的目录和文件。每一个目录和文件都有自己特殊的功能。
  • 60. 4.4 内核配置选项自己构建嵌入式Linux操作系统,首先需要对内核源代码进行相应的配置。这些配置决定了嵌入式Linux操作系统所支持的功能,为了理解编译程序是怎样通过配置文件配置系统的,下面对配置编译过程进行详细的讲解。
  • 61. 4.4.1 配置编译过程面对日益庞大的Linux内核源代码,要手动的编译内核是十分困难的。幸好Linux提供了一套优秀的机制,简化了内核源代码的编译。这套机制由以下几个方面组成: Makefile文件 Kconfig文件 配置文件 配置工具
  • 62. 4.4.2 常规配置常规配置包含关于内核的大量配置,这写配置包含代码成熟度、版本信息、模块配置等 。
  • 63. 4.4.3 模块配置模块作为Linux的一种非常重要的组件,其有很多参数和功能可以配置。
  • 64. 4.4.4 块设备层配置块设备层包含对系统使用的块设备的配置,其主要包含调度器的配置,硬盘设备的配置。
  • 65. 4.4.5 CPU类型和特性配置Linux内核几乎支持所有体系结构上的CPU。内核不能自动识别相应的CPU类型和一些相关的特性,需要在配置内核时根据实际的情况进行相应的配置。
  • 66. 4.4.6 电源管理配置电源管理是操作系统中一个非常重要的模块,随着硬件设备省电节能能力的增强,该模块越来越重要。在嵌入式系统中,由于一般以电池供电,有低功耗的要求,所以在为嵌入式系统配置内核时,需要对相应的硬件配置电源管理模块。
  • 67. 4.4.7 总线配置
  • 68. 4.4.8 网络配置网络是嵌入式系统与外部通信的主要方式。目前,许多嵌入式设备都具有网络功能,为了使内核支持网络功能,需要对其做一些特殊的配置。
  • 69. 4.4.9 设备驱动配置Linux内核实现了一些常用的驱动程序,如鼠标、键盘、常见的U盘驱动等。这些驱动非常繁多,许多驱动对于嵌入式系统来说,并不需要。在实际的应用中,为了使配置的内核高效和小巧,值需要配置主要的一些驱动程序。
  • 70. 4.4.10 文件系统文件系统是操作系统的主要组成部分。Linux支持很多文件系统,为了内核的高效和小巧性,支持哪些文件系统都是可以配置。
  • 71. 4.5 嵌入式文件系统基础知识对于嵌入式系统来说,除了一个嵌入式操作系统以外,还需要一个嵌入式文件系统来管理和存储数据和程序。目前,嵌入式Linux操作系统支持很多种文件系统,具体使用哪种文件系统需要根据存储介质、访问速度、存储容量等来选择。本章将对嵌入式文件系统的基础知识进行简单的介绍,首先需啊哟对嵌入式系统的存储介质有一定的了解。
  • 72. 4.5.1 嵌入式文件系统Linux支持多种文件系统,包括ext2、ext3、vfat、ntfs、iso9660、jffs、romfs、cramfs和nfs等,为了对各类文件系统进行统一管理,Linux引入了虚拟文件系统VFS(Virtual File System),为各类文件系统提供一个统一的操作界面和应用编程接口。
  • 73. 4.5.1 嵌入式系统的存储介质Linux操作系统支持大量的文件系统,在嵌入式领域,使用哪种文件系统需要根据存储芯片的类型来决定。目前市场上,嵌入式系统主流的两种存储介质是NOR和NAND Flash。Intel公司于1988年首先开发了NOR Flash存储器。NOR Flash 的特点是芯片内执行(XIP ,eXecute In Place),这样应用程序可以直接在Flash闪存内运行,不必再把代码读到系统RAM中。NOR 的传输效率很高,在1~4MB的小容量时具有很高的成本效益,但是很低的写入和擦除速度大大影响到它的性能。
  • 74. 4.5.2 JFFS文件系统瑞典的Axis Communications公司基于Linux2.0的内核为嵌入式操作系统开发的JFFS文件系统。其升级版JFFS2是RedHat公司基于JFFS开发的闪存文件系统,最初是针对RedHat公司的嵌入式产品eCos开发的嵌入式文件系统,所以JFFS2也可以用在Linux, uCLinux等操作系统之中。JFFS的全称是日志闪存文件系统。
  • 75. 4.5.3 YAFFS文件系统YAFFS是第一个专门为NAND Flash存储器设计的嵌入式文件系统,适用于大容量的存储设备;并且是在GPL(General Public License)协议下发布的,可在其网站免费获得源代码。YAFFS文件系统有4个优点,分别是速度快、占用内存少、不支持压缩和只支持NAND Flash存储器。
  • 76. 4.6 构建根文件系统当内核启动后,第一件要做的事情就是到存储设备上找到根文件系统。根文件系统包含了使系统运行的主要程序和数据。本节对系统运行所必须的根文件系统进行详细的分析。
  • 77. 4.6.1 根文件系统概述根文件系统是Linux操作系统运行需要的一个文件系统。根文件系统被存储在Flash存储器中,存储器被分为多个分区,例如分区1、分区2、分区3等,如图4.6所示。分区1一般存储Linux内核映象文件,在Linux操作系统中,内核映象文件一般存储在单独的分区中。分区2存放根文件系统,根文件系统中存放着系统启动必须的文件和程序。这些文件和程序包括:提供用户界面的shell程序、应用程序依赖的库、配置文件等。
  • 78. 4.6.2 Linux根文件系统目录结构根文件系统以树形结构来组织目录和文件的结构。系统启动后,根文件系统被挂接到根目录“/”上,这是根目录下就包含了根文件系统的各个目录和文件,例如/bin、/sbin、/mnt等。根文件系统应该包含的目录和文件遵循FHS标准(Filesystem Hierarchy Standard,文件系统层次标准)。这个标准包含了根文件系统中最少应该包含哪些目录和文件,以及这些目录和文件的组织原则。
  • 79. 4.6.3 BusyBox构建根文件系统要使Linux操作系统能够正常的运行起来,至少需要一个内核和根文件系统。根文件系统除了应该以FHS标准的格式组织之外,还应该包含一些必要的命令。这些命令提供给用户使用,以使用户能轻易的操作系统。
  • 80. 4.7 小结本章主要讲解了怎样构建一个嵌入式操作系统的全过程。首先对Linux操作系统的特性做了简单的介绍,然后阐述了Linux操作系统的主要内核子系统。在4.3节,讲解了Linux内核源代码的结构,为修改内核,编写驱动程序打下了基础。第4.4节,讲解了内核配置的常用选项,这些知识对构建适合自己的嵌入式设备的操作系统内核有非常大的帮助。第4.5节,在前述基础上,讲解了嵌入式文件系统的基础知识,特别是YAFFS文件系统,这是一种很常用的基于NAND Flash的文件系统。最后详细讲解了使用Busybox构建一个根文件系统的全过程。
  • 81. 第5章 构建第一个驱动程序万事开头难,写驱动程序也一样,本章将构建第一个驱动程序。驱动程序和模块的关系非常密切,所以这里将详细讲解模块的相关知识。而模块编程成败与否的先决条件是要有统一的内核版本,所以这里将讲解怎样升级内核版本。最后为了提高程序员的编程效率,这里将介绍两种集成开发环境。
  • 82. 5.1 开发环境配置之内核升级构建正确的开发环境,对写驱动程序非常重要。错误的开发环境,编写出的驱动程序不能正确运行。特别是关于内核版本的问题,内核版本不匹配,会使驱动程序根本不能在系统中运行,所以需要对内核进行升级。本节我们将对Fedora Core 9进行内核升级,首先将说明为什么要升级内核。
  • 83. 5.1.1 为什么升级内核 内核是一个提供硬件抽象层、磁盘及文件系统控制、多任务等功能的系统软件。根据内核是否被修改过,可以将内核分为标准内核和厂商内核两类。
  • 84. 5.1.2 内核升级尽管在Fedora Core 9中可以使用“软件包管理器工具”对内核进行升级,但是毕竟是开发厂商编译的内核有其局限性。里面添加了很多驱动开发系统不需要的模块,而驱动开发需要的模块却没有开启。因此,学会自己手动编译升级内核也是很必要的。这里,我们将内核升级为linux 2.6.29.4。
  • 85. 5.1.3 make menconfig的注意事项在升级内核的过程中,第6步需要非常的注意。第6步是对内核进行配置,特别是对CPU进行配置。标准内核源码对CPU的默认配置是:Pentium-Pro,其是高性能奔腾处理器。在很多情况下,如果使用这个CPU配置编译内核,那么很可能会出现系统引导时无法识别CPU的错误。所以建议将CPU类型改为目前通用的X586类型。
  • 86. 5.2 Hello World驱动程序本节将带领读者编写第一个驱动模块,该驱动模块的功能是在加载时,输出“Hello, World”;在卸载时,输出“Goodbye, World”。这个驱动模块虽然非常简单,但是也包含了驱动模块的重要组成部分。在本节的开始,将先对模块的重要组成部分进行介绍。
  • 87. 5.2.1 驱动模块的组成一个驱动模块主要有如下部分组成,如图5.2所示。图5.2表示的是一个规范的驱动模块应该包含的结构。这些结构在图中的顺序也是在源文件中的顺序。不按照这样的顺序来编写驱动模块也不会出错,只是大多数开发人员都喜欢这样的顺序规范。下面对主要的结构部分进行说明。
  • 88. 5.2.2 Hello World模块任何一本关于编程的书,几乎都以“Hello World”开始。现在,来看一下最简单的一个驱动模块。
  • 89. 5.2.3 编译Hello World模块在对Hello World模块进行编译时,需要满足一定的条件: 1.编译内核模块的条件 2.Makefile文件 3.Makefile文件的执行过程 4.编译模块
  • 90. 5.2.4 模块的操作Linux为用户提供了modutils工具,用来操作模块。这个工具集主要包括: insmod命令加载模块。 rmmod命令卸载模块。 modprobe命令是比较高级的加载和删除模块命令,其可以解决模块之间的依赖性问题。 lsmod命令列出已经加载的模块和其信息。modinfo命令用于查询模块的相关信息,比如作者,版权等。
  • 91. 5.2.5 Hello World模块加载后文件系统的变化当使用insmod hello.ko加载模块后文件系统会发生什么样的变化呢?文件系统存储着有关模块的属性信息。程序员可以从这些属性信息中了解目前模块在系统中的状态,这些状态对开发调试非常重要。
  • 92. 5.3 模块参数和模块之间通讯为了增加模块的灵活性,可以给模块添加参数。模块参数可以控制模块的内部逻辑,从而使模块可以在不同的情况下,完成不同的功能,下面首先对模块参数进行介绍。
  • 93. 5.3.1 模块参数用户空间的应用程序可以接受用户的参数,设备驱动程序有时候也需要接受参数。例如一个模块可以实现两种相似的功能,这时可以传递一个参数到驱动模块,以决定其使用哪一种功能。参数需要在加载模块时指定,例如inmod xxx.ko param=1。
  • 94. 5.3.2 模块的文件格式ELF了解模块以何种格式存储在硬盘中,对于理解模块间怎样通讯时非常有必要的。
  • 95. 5.3.3 模块之间的通讯模块是为了完成某种特定任务而设计的。其功能比较的单一,为了丰富系统的功能,所以模块之间常常进行通信。其之间可以共享变量,数据结构,也可以调用对方提供的功能函数。
  • 96. 5.3.4 模块之间的通讯实例本实例通过两个模块来介绍模块之间的通信。模块add_sub提供了两个导出函数add_integer()和sub_integer(),分别完成两个数字的加法和减法。模块test用来调用模块add_sub提供的两个方法,完成加法或者减法操作。 1.add_sub模块 2.test模块 3.编译模块 4.测试模块
  • 97. 5.4 将模块加入内核当编译了模块,如果希望模块随系统一起启动,那么需要将模块静态编译进内核。将模块静态编译入内核,需要完成一些必要的步骤。
  • 98. 5.4.1 向内核添加模块向Linux内核中添加驱动模块,需要完成4个工作: (1)编写驱动程序文件。 (2)将驱动程序文件放到Linux内核源码的相应目录中,如果没有合适的目录,可以自己建立一个目录来存放驱动程序文件。 (3)在目录的Kconfig文件中添加新驱动程序对应的项目编译选择。 (4)在目录的Makefile文件中添加新驱动程序的编译语句。
  • 99. 5.4.2 Kconfig内核源码树的目录下都有两个文件Kconfig和Makefile。分布到各目录的Kconfig文件构成了一个分布式的内核配置数据库,每个Kconfig文件分别描述了所属目录源文档相关的内核配置菜单。在内核配置make menuconfig(或xconfig等)时,从Kconfig中读出菜单,用户选择后保存到.config这个内核配置文档中。在内核编译时,主目录中的Makefile调用这个.config文件,就知道了用户的选择。
  • 100. 5.4.3 Kconfig的语法Kconfig语法较为简单,其语法在Documentation/kbuild/kconfig-language.txt文件中做了介绍。归纳起来Kconfig的语法主要包括以下几个方面: 1.主要语法总览 2.菜单入口(config) 3.菜单结构(menu) 4.选择菜单(choice) 5.注释菜单(comment)
  • 101. 5.4.4 应用实例:在内核中新增加add_sub模块下面讲解一个综合实例,假设我们将要在内核中添加一个add_sub模块。考虑add_sub模块的功能,决定将该模块加到内核源码的drivers目录中。在drivers目录中增加一个add_sub_Kconfig子目录。
  • 102. 5.4.5 对add_sub模块进行配置当将add_sub模块的源文件加入到内核源代码中后,需要对其进行配置,才能编译模块。
  • 103. 5.5 小结本章主要讲解了怎样构建一个驱动程序,这一章是后面章节的基础。首先讲解了为什么要升级内核。然后对一个Hello World程序进行了简单的介绍。在这个基础上,又详细的讲解了模块之间的通信,这些都是驱动程序开发的基础。在最后,讲解了怎样将模块加入到内核中,让模块运行起来。
  • 104. 第6章 简单的字符设备驱动程序在Linux设备驱动程序的家族中,字符设备驱动程序是较为简单的驱动程序,同时也是应用非常广泛的驱动程序。所以学习字符设备驱动程序,对构建Linux设备驱动程序的知识结构非常的重要。本章将带领读者编写一个完整的字符设备驱动程序。
  • 105. 6.1 字符设备驱动程序框架本节对字符设备驱动程序框架进行了简要的分析。字符设备驱动程序中有许多非常重要的概念,下面将从将从最简单的概念讲起:字符设备和块设备。
  • 106. 6.1.1 字符设备和块设备Linux系统将设备分为3种类型:字符设备、块设备和网络接口设备。其中字符设备和块设备难以区分,这里将对其进行重要讲解。 1.字符设备 2.块设备 3.字符设备和块设备的区分
  • 107. 6.1.2 主设备号和次设备号一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。
  • 108. 6.1.3 申请和释放设备号内核维护着一个特殊的数据结构,用来存放设备号与设备的关系。在安装设备时,应该给设备申请一个设备号,使系统可以明确设备对应的设备号。设备驱动程序中的很多功能,是通过设备号来操作设备的。这里,首先对申请设备号进行简述。
  • 109. 6.2 初识cdev结构当申请字符设备的设备号后,这时,需要将字符设备注册到系统中,才能使用字符设备。为了理解这个实现过程,首先解释一下cdev结构体。
  • 110. 6.2.1 cdev结构体 在linux内核中使用cdev结构体来描述字符设备。该结构体是所有字符设备的抽象,其包含了大量字符设备所共有的特性。
  • 111. 6.2.2 file_operations结构体file_operations是一个对设备进行操作的抽象结构体。linux内核的设计非常巧妙。内核允许为设备建立一个设备文件,对设备文件的所有操作,就相当于对设备的操作。这样的好处是,用户程序可以使用访问普通文件的方法,来访问设备文件,进而访问设备。这样的方法,极大地减轻了程序员的编程负担,程序员不必要去熟悉新的驱动接口,就能够访问设备。
  • 112. 6.2.3 cdev和file_operations结构体的关系一般来说,驱动开发人员会将特定设备的特定数据放到cdev结构体后,组成一个新的结构体。如图6.3所示,“自定义字符设备”中就包含特定设备的数据。该“自定义设备”中有一个cdev结构体。cdev结构体中有一个指向file_operations的指针。这里,file_operations中的函数就可以用来操作硬件,或者“自定义字符设备”中的其他数据,从而起到控制设备的作用。
  • 113. 6.2.4 inode结构体内核使用inode结构在内部表示文件。inode一般作为file_operations结构中函数的参数传递过来。例如,open函数将传递一个inode指针进来,表示目前打开的文件节点。需要注意的是,inode的成员已经被系统赋予了合适的值,驱动程序只需要使用该节点中的信息,而不用去更改。
  • 114. 6.3 字符设备驱动的组成了解字符设备驱动程序的组成,对编写驱动程序非常有用。因为字符设备在结构上都有很多相似的地方,所以只要会编写一个字符设备驱动程序,那么相似的字符设备驱动程序的编写,就不难了。在linxu系统中,字符设备驱动程序由以下几个部分组成。
  • 115. 6.3.1 字符设备加载和卸载函数在字符设备的加载函数中,应该实现字符设备号的申请和cdev的注册。相反,在字符设备的卸载函数中应该实现字符设备号的释放和cdev的注销。 cdev是内核开发者对字符设备的一个抽象。除了cdev中的信息外,特定的字符设备还需要特定的信息,常常将特定的信息放在cdev之后,形成一个设备结构体,如代码中的xxx_dev。
  • 116. 6.3.2 file_operations结构体和其成员函数file_operations结构体中的成员函数都对应着驱动程序的接口,用户程序可以通过内核来调用这些接口,从而控制设备。大多数字符设备驱动都会实现read()、write()和ioctl()函数。
  • 117. 6.3.3 驱动程序与应用程序的数据交换驱动程序和应用程序的数据交换是非常重要的。file_operations中的read()和write()函数,就是用来在驱动程序和应用程序间交换数据的。通过数据交换,驱动程序和应用程序可以彼此了解对方的情况。但是驱动程序和应用程序属于不同的地址空间。驱动程序不能直接访问应用程序的地址空间;同样应用程序也不能直接访问驱动程序的地址空间,否则会破坏彼此空间中的数据,从而造成系统崩溃,或者数据损坏。
  • 118. 6.3.4 字符设备驱动程序组成小结字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operation结构体中操作函数,并实现file_operations结构体中的read()、write()、ioctl()等重要函数。
  • 119. 6.4 VirtualDisk字符设备驱动从本章开始,后续的几章都将以一个VirtualDisk设备为蓝本进行讲解。VirtualDisk是一个虚拟磁盘设备,在这个虚拟磁盘设备中分配了8K的连续内存空间,并定义了两个端口数据(port1和port2)。驱动程序可以对设备进行读写、控制和定位操作,用户空间的程序可以通过Linux系统调用访问VirtualDisk设备中的数据。
  • 120. 6.4.1 VirtualDisk的头文件、宏和设备结构体VirtualDisk驱动程序应该包含必要的头文件和宏信息,并定义一个与实际设备相对应的设备结构体,相关的定义如下面的代码所示。
  • 121. 6.4.2 加载和卸载驱动程序第6.3节已经对字符设备驱动程序的加载和卸载模板进行了介绍。VirtualDisk的加载和卸载函数也和6.3节介绍的类似。
  • 122. 6.4.3 cdev的初始化和注册 上面代码的第25行调用的VirtualDisk_setup_cdev()函数完成了cdev的初始化和注册。
  • 123. 6.4.4 打开和释放函数当用户程序调用open()函数打开设备文件时,内核会最终调用VirtualDisk_open()函数。
  • 124. 6.4.5 读写函数当用户程序调用read()函数读设备文件中的数据时,内核会最终调用VirtualDisk_read()函数。
  • 125. 6.4.6 seek()函数当用户程序调用fseek()函数在设备文件中移动文件指针时,内核会最终调用VirtualDisk_llseek()函数。
  • 126. 6.4.7 ioctl()函数当用户程序调用ioctl()函数改变设备的功能时,内核会最终调用VirtualDisk_ioctl()函数。
  • 127. 6.5 小结本章主要讲解了字符设备驱动程序。字符设备是Linux中的三大设备之一,很多设备都可以看成是字符设备,所以学习字符设备驱动程序的编程是很有用的。首先从整体上介绍了字符设备的框架结构。然后介绍了字符设备结构体struct cdev。接着介绍了字符设备的组成,最后详细讲解了一个VirtualDisk字符设备驱动程序。
  • 128. 第7章 设备驱动中的并发控制现代操作系统有三大特性:中断处理、多任务处理和多处理器(SMP)。这些特性导致当多个进程、线程或者CPU同时访问一个资源时,可能导致错误,这些错误是操作系统运行所不允许的。在操作系统中,内核需要提供并发控制机制,对公用资源进行保护。本章将对保护这些公用资源的方法进行简要的介绍。
  • 129. 7.1 并发与竞争并发是指在操作系统中,一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。并发容易导致竞争的问题。竞争就是两个或者两个以上的进程同时访问一个资源,从而引起资源的错误。
  • 130. 7.2 原子变量操作原子变量操作是Linux中提供的一种简单的同步机制。原子变量操作是一种在操作过程中不会被打断的操作,所以在内核驱动程序中非常有用。本节对Linux中原子变量的操作进行详细的分析。
  • 131. 7.2.1 原子变量操作所谓原子变量操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断。也就说,原子变量操作是一种不可以被打断的操作。原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都定义在内核源码树的include/asm/atomic.h文件中,它们都使用汇编语言实现,因为C语言并不能实现这样的操作。
  • 132. 7.2.2 原子整形操作有时候需要共享的资源可能只是一个简单的整形数值。例如在驱动程序中,需要对包含一个count的计数器。这个计数器表示有多少个应用程序打开了设备所对应的设备文件。通常在设备驱动程序的open()函数中,将count变量加1。在close()函数中,将count减1。如果只有一个应用程序执行打开和关闭操作,那么这里的count计数不会出现问题。但是如果有多个应用程序同时打开或者关闭设备文件,那么就可能导致count多加或者少加,出现错误。 为了避免这个问题,内核提供了一个原子整形变量,称为atomic_t。
  • 133. 7.2.3 原子位操作除了原子整数操作外,还有原子位操作。原子位操作是根据数据的每一位单独进行操作。根据体系结构的不同,原子位操作函数的实现也不同。
  • 134. 7.3 自旋锁自旋锁是一种简单的并发控制机制,其是实现信号量和完成量的基础。自旋锁对资源有很好的保护作用,在Linux驱动程序中进程使用,本节将对自旋锁进行详细的介绍。
  • 135. 7.3.1 自旋锁概述在Linux中提供了一些锁机制来避免竞争条件,最简单的一种就是自旋锁。引入锁的机制,是因为单独的原子操作不能满足复杂的内核设计需要。例如,当一个临界区域要在多个函数之间来回运行时,原子操作就显得无能无力了。 Linxu中一般可以认为有两种锁,一种是自旋锁,另一种是信号量。这两种锁是为了解决内核中遇到的不同问题开发的。其实现机制和应用场合有所不同,下文将分别对这两种锁机制进行介绍。
  • 136. 7.3.2 自旋锁的使用在Linux中,自旋锁的类型为struct spinlock_t。内核提供了一系列的函数来对struct spinlock_t进行操作。下面将对自旋锁的操作方法进行简要的介绍。 1.定义和初始化自旋锁 2.锁定自旋锁 3.释放自旋锁 4.使用自旋锁
  • 137. 7.3.3 自旋锁的使用注意事项在使用自旋锁时,有几个注意事项需要读者理解,这几个注意事项是: 自旋锁是一种忙等待。 自旋锁不能递归使用。
  • 138. 7.4 信号量本节介绍锁的另一种实现机制,这种机制就是Linux中常用的信号量。Linux中提供了两种信号量,一种用于内核程序中,一种用于应用程序中。由于这里讲解的是内核编程的知识,所以只对内核中的信号量进行详细讲述。
  • 139. 7.4.1 信号量概述和自旋锁一样,信号量也是保护临界资源的一种有用方法。信号量与自旋锁的使用方法基本一样。与自旋锁相比,信号量只有当得到信号量的进程或者线程才能够进入临界区,执行临界代码。信号量与自旋锁的最大不同点在于,当一个进程试图去获取一个已经锁定的信号量时,进程不会像自旋锁一样在远处忙等待,在信号量中采用了另一种方式,这中方式如下所述。
  • 140. 7.4.2 信号量的实现根据不同的平台,其提供的指令代码有所不同,所以信号量的实现也有所不同。 下面详细介绍一下这个结构体的各个成员变量: 1.lock自旋锁 2.count变量 3.等待队列
  • 141. 7.4.3 信号量的使用在Linux中,信号量的类型为struct semaphore。内核提供了一系列的函数来对struct semaphore进行操作。下面将对信号量的操作方法进行简要的介绍。 1.定义和初始化自旋锁 2.锁定信号量 3.释放信号量 4.使用信号量 5.信号量用于同步操作
  • 142. 7.4.4 自旋锁与信号量的对比自旋锁和信号量是解决并发控制的两个很重要的方法。在使用时,应该如何选择它们其中的一种方法呢?这要根据被包含资源的特定来确定。 自旋锁是一种最简单的保护机制,从上面的代码分析中,可以看出自旋锁的定义只有一个结构体成员。当被包含的代码能够在很短的时间内执行完成时,那么使用自旋锁是一种很好的选择。因为自旋锁只是忙等待,不会进入睡眠。要知道,睡眠是一种非常浪费时间的操作。
  • 143. 7.5 完成量在驱动程序开发中,一种常见的情况是:一个线程需要等待另一个线程执行完某个操作后,才能够继续执行。上面讲的信号量其实也能够完成这种工作,不过其效率不如Linux中专门针对这种情况的完成量机制。本节将对完成量进行详细的介绍。
  • 144. 7.5.1 完成量概述Linux中提供了一种机制来实现一个线程发送一个信号来通知另一个线程开始完成某个任务,这种机制就是完成量。完成量的目的是告诉一个线程某个事件已经发生,可以在此事件基础上做你想做的另一个事件了。其实完成量和信号量比较类似,但是在这种线程通信的情况下,使用完成量有更高的效率。在内核中,可以进程看见使用完成量的代码。完成量是一种轻量级的机制,这种机制在一个线程希望告诉另一个线程某个工作已经完成的情况下是非常有用的。
  • 145. 7.5.2 完成量的实现完成量是实现两个任务之间同步的简单方法,在内核中完成量由struct completion结构体来表示。该结构体定义在include\linux\completion.h文件中。 下面详细介绍一下这个结构体的两个成员变量: 1.done成员 2.wait成员
  • 146. 7.5.3 完成量的使用在Linux中,信号量的类型为struct completion。内核提供了一系列的函数来对struct completion进行操作。下面将对完成量的操作方法进行简要的介绍: 1.定义和初始化完成量 2.等待完成量 3.释放完成量 4.使用完成量
  • 147. 7.6 小结本章介绍了Linux中内核的并发控制机制,分别介绍了完成并发控制功能的原子变量操作、自旋锁、信号量和完成量。这些都是内核中广泛使用的机制。每一种机制都有自己的一些特点和适用范围。读者在使用时,应该对这些特定进行比较,选择出适合要求的并发控制机制。只有这样,才可以写出高效稳定的程序。
  • 148. 第8章 设备驱动中的阻塞和同步机制阻塞和非阻塞是两种设备访问的基本方式,使用这两种方式驱动程序可以灵活的支持阻塞与非阻塞的访问。在写阻塞与非阻塞的驱动程序时,经常用到等待队列,所以本章也对等待队列进行简要的介绍。
  • 149. 8.1 阻塞和非阻塞阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。有人也许会把阻塞调用和同步调用等同起来,实际上它们是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
  • 150. 8.2 等待队列本节介绍驱动程序编程中常用的等待队列机制。这种机制使等待的进程暂时睡眠,当等待的信号到来时,便唤醒等待队列中进程继续执行。本节将详细的介绍等待队列。
  • 151. 8.2.1 等待队列概述在Linux驱动程序中,阻塞进程可以使用等待队列(Wait Queue)来实现。由于等待队列很有用,在Linux 2.0的时代,就已经引入了等待队列机制。等待队列的基本数据结构是一个双向链表,这个链表存储睡眠的进程。等待队列也与进程调度机制紧密结合,能够用于实现内核中异步事件通知机制。等待队列可以用来同步对系统资源的访问。例如,当完成一项工作之后,才允许去完成另一项工作。 在内核里面,等待队列是有很多用处的,尤其是在中断处理、进程同步、定时等场合。可以使用等待队列在实现阻塞进程的唤醒。它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制,同步对系统资源的访问等。
  • 152. 8.2.2 等待队列的实现根据不同的平台,其提供的指令代码有所不同,所以信等待队列的实现也有所不同。 下面详细介绍一下这个结构体的各个成员变量: 1.lock自旋锁 2.task_list变量
  • 153. 8.2.3 等待队列的使用在Linux中,等待队列的类型为struct wait_queue_head_t。内核提供了一系列的函数来对struct wait_queue_head_t进行操作。下面将对等待队列的操作方法进行简要的介绍: 1.定义和初始化等待队列头 2.定义等待队列 3.添加和移除等待队列 4.等待事件 5.唤醒等待队列
  • 154. 8.3 小结阻塞和非阻塞在驱动程序中进程用到。阻塞在I/O操作暂时不能进行时,让进程进入等待队列。后者在I/O操作暂时不能进行时,立刻返回。两种方式各有优劣,在实际应用中,应该有选择的使用。由于阻塞和非阻塞也是由等待队列来实现的,所以本章也概要的讲解了一些等待队列的用法。
  • 155. 第9章 中断与时钟机制中断和时钟机制是Linux驱动重要的两项技术。使用这些技术,可以帮助驱动程序更高效的完成任务。在写设备驱动程序的过程中,为了使系统知道硬件在做什么,必须使用中断。如果没有中断,设备几乎什么都不能做。本章将详细讲解中断与时钟机制。
  • 156. 9.1 中断简述本节对中断相关概念进行了简要的分析,并对中断进行了分类。根据不同的中断类型,写中断驱动程序的方法也不一样。下面将主要介绍中断的基本概念和常见分类。
  • 157. 9.1.1 中断的概念 中断是计算机中的一个十分重要的概念。如果没有中断,那么设备和程序就无法高效利用计算机的CPU资源。 1.什么是中断 2.中断在Linux中的实现
  • 158. 9.1.2 中断的宏观分类在Linux操作系统中,中断的分类是非常复杂的。根据不同的角度,可以将中断分为不同的类型。各种类型之间的关系并非相互独立,往往是相互交叉的。从宏观上可以分为两类,分别是硬中断和软中断。
  • 159. 9.1.3 中断产生的位置分类 从中断产生的位置,可以将中断分为外部中断和内部中断。 1.外部中断 外部中断一般是指由计算机外设发出的中断请求,键盘中断、打印机中断、定时器中断等。外部中断是可以通过编程方式给予屏蔽的。 2.内部中断 内部中断是指因硬件出错(如突然掉电、奇偶校验错等)或运算出错(除数为零、运算溢出、单步中断等)所引起的中断。内部中断是不可屏蔽的中断。通常情况下,大多数内部中断都由Linux内核进行了处理,所以驱动程序员往往不需要关心这些问题。
  • 160. 9.1.4 同步和异步中断从指令执行的角度,中断又可以分为同步中断和异步中断。 1.同步中断 同步中断是指令执行的过程中由CPU控制的,CPU在执行完一条指令后才发出中断。 2.异步中断 异步中断是由硬件设备随机产生的,产生中断时并不考虑与处理器的时钟同步问题,及该类型的中断是可以随时产生的。
  • 161. 9.1.5 中断小结以上三小节从不同的角度对Linux中的中断进行了分类,但这不是严格的分类。例如,硬件中断可以是外部中断也可以是异步中断,同时软件中断可以是内部中断也可以是同步中断,如图所示。
  • 162. 9.2 中断的实现过程中断的实现过程一个比较复杂的过程。其中涉及中断信号线、中断控制器等概念。首先介绍中断信号线的概念。
  • 163. 9.2.1 中断信号线(IRQ)中断信号线是对中断输入线和中断输出线的统称。中断输入线是指接收中断信号的引脚。中断输出线是指发送中断信号的引脚。每一个能够产生中断的外设都有一条或者多条中断输出线(Interrput ReQquest,简称IRQ)。其用来通知处理器产生中断。相应地,处理器也有一组中断输入线,用来接收连接到它上的外部设备发出的中断信号。
  • 164. 9.2.2 中断控制器中断控制器位于ARM处理器核心和中断源之间。外部中断源将中断发到中断控制器。中断控制器根据优先级进行判断,然后通过引脚将中断请求发送给ARM处理器核心。
  • 165. 9.2.3 中断处理过程Linux处理中断的整个过程如图所示。
  • 166. 9.2.4 中断的安装与释放当设备需要中断功能时,应该安装中断。如果驱动程序员没有通过安装中断的方式通知Linux内核需要使用中断,那么内核只会简单的应答并且忽略该中断。
  • 167. 9.3 按键中断实例掌握了足够多的关于中断的知识后,下面将介绍一个按键驱动程序。该按键驱动程序当按键被按下时,打印按键按下的提示信息。 作为一个驱动程序开发人员,要做的第一件事情,就是要读懂电路图。在实际的项目开发过程中,硬件设计有时非常复杂。这时驱动开发人员应该多和硬件开发人员沟通,掌握足够多的硬件知识,以避免写出错误的驱动程序。
  • 168. 9.3.1 按键设备原理图首先应该仔细看懂按键设备的原理图。作为一名驱动开发人员这是最基本的素质。按键设备在实际项目中是一种非常简单的设备,硬件原理图也非常简单。本实例的原理图可以从mini2440开发板的官方网站免费下载(http://www.arm9.net/)。
  • 169. 9.3.2 有寄存器设备和无寄存器设备从设备的角度来看,设备可以分为有寄存器的设备和无寄存器的设备。按键设备就是一种没有寄存器的设备。按键设备内部没有寄存器并不能代表其没有相应的外部寄存器。为了节约成本,外部寄存器常常被集成到了处理器芯片内部。这样,处理器可以通过内部寄存器来控制外部设备的功能。所以目前的处理器已经不在像是以前纯粹的处理器了,其更像一台简易的计算机。
  • 170. 9.3.3 按键设备相关端口寄存器与按键K1相关的寄存器是端口G控制寄存器,如图所示。按键K1连接到EINT8引脚,该引脚对应GPG0端口的第0位。 端口G有三个控制寄存器,分别为GPGCON,GPGDAT和GPGUP。
  • 171. 9.4 按键中断实例程序分析现在开始对按键设备程序进行分析。按键驱动程序由初始化函数,退出函数,中断处理函数组成。
  • 172. 9.4.1 按键驱动程序组成按键驱动程序初始化函数,退出函数,中断处理函数的关系如图所示。
  • 173. 9.4.2 初始化函数s3c2440_buttons_init()初始化函数s3c2440_buttons_init()主要负责模块的初始化工作。模块初始化主要包括设置中断触发方式,注册中断号等。
  • 174. 9.4.3 中断处理函数isr_button()当按键按下时,中断被触发,就会触发中断处理函数。该函数主要功能是判断按键K1是否按下。 中断处理函数由isr_button()函数实现。该函数的参数由系统调用该函数时传递过来。参数irq表示被触发的中断的中断号。参数dev_id是为共享中断线而设立的,因为按键驱动不使用共享中断,所以这里传进来的是NULL值。参数regs是一个寄存器组的结构体指针。寄存器组保存了处理器进入中断代码之前处理器的上下文。这些信息一般只在调试时使用,其他时候很少使用。所以对于一般的驱动程序来说,该参数通常是没有用的。
  • 175. 9.4.4 退出函数s3c2440_buttons_exit()当模块不在使用时,需要退出模块。按键的退出模块由s3c2440_buttons_exit()函数实现,其主要功能是释放中断线。
  • 176. 9.5 时钟机制Linux驱动程序中经常会使用一些时钟机制,主要是用来延时一段时间。在这段时间中硬件设备可以完成相应的工作。本节将对Linux的时钟机制作一个简要的介绍。
  • 177. 9.5.1 时间度量Linux内核中一个重要的全局变量是HZ,这个变量表示与时钟中断相关的一个值。时钟中断是由系统定时硬件以周期性的间隔产生,这个周期性的值有HZ来表示。根据不同的硬件平台,HZ的取值是不一样的。
  • 178. 9.5.1 时间延时在C语言中,经常使用sleep()函数来将程序延时一段时间,这个函数能够实现毫秒级的延时。在设备驱动程序中,很多对设备的操作也需要延时一段时间,来使设备完成某些特定的任务。在Linux内核中,延时技术有很多种,这里只讲解其中重要的两种。 1.短时延时 2.长时延时
  • 179. 9.6 小结大多数设备以中断方式来驱动代码的执行。例如本章讲解的按键驱动程序,当用户按下键盘时,才会触发先前注册的中断处理程序。这种机制具有很多的优点,可以节约很多CPU时间。除了中断之外,本章还简要地介绍了时钟机制,硬件工作的速度一般较慢,在操作硬件的某些寄存器时,一般需要内核延时一段时间,在短时延时时可以使用忙等待机制,但是对于长时延时则最好使用等待延时机制。
  • 180. 第10章 内外存访问驱动程序加载成功的一个关键因素,就是内核能够为驱动程序分配足够的内存空间。这些空间一部分用于驱动程序必要的数据结构,另一部分用于数据的交换。同时,内核也应该具有访问外部设备端口的能力。一般来说,外部设备被连接到内存空间或者I/O空间中。本章将对内外存设备的访问进行详细的介绍。
  • 181. 10.1 内存分配本节主要介绍内存分配的一些函数,包括kmalloc()函数和vmalloc()函数等。在介绍了这两个重要的函数之后,又重点的讲解了后备高速缓存的内容,这些知识对于驱动开发来说非常重要,需要引起注意。
  • 182. 10.1.1 kmalloc()函数在C语言中,经常会遇到malloc()和free()这两个函数“冤家”。malloc()函数用来进行内存分配,free()函数用来释放内存。kmalloc()函数类似于malloc()函数,不同的是kmalloc()函数用于内核态的内存分配。kmalloc()函数是一个功能强大的函数,如果内存充足,这个函数将运行的非常快。
  • 183. 10.1.2 vmalloc()函数vmalloc()函数用来分配虚拟地址连续但是物理地址不连续的内存。这就是说,用vmalloc()函数分配的页在虚拟地址空间中是连续的,而在物理地址空间是不连续的。这是因为如果需要分配200M的内存空间,而实际的物理内存中现在不存在一块连续的200M内存空间,但是内存有大量的内存碎片,其容量大于200M,那么就可以使用vmalloc()函数将不连续的物理地址空间映射层连续的虚拟地址空间。
  • 184. 10.1.3 后备高速缓存在驱动程序中,会经常反复地分配很多同一大小的内存块,也会频繁的将这些内存块给释放掉。如果频繁的申请和释放内存,很容易产生内存碎片,使用内存池很好的解决了这个问题。在Linux中,为一些反复分配和释放的结构体预留了一些内存空间,使用内存池来管理,管理这种内存池的技术叫做slab分配器。这种内存叫做后备高速缓存。 slab分配器的相关函数定义在linux/slab.h文件中,使用后备告诉缓存前,需要创建一个kmem_cache的结构体。 1.创建slab缓存函数 2.分配slab缓存函数 3.销毁slab缓存函数 4.slab缓存举例
  • 185. 10.2 页面分配在Linux中提供了一系列的函数来分配和释放页面。当一个驱动程序进程需要申请内存时,内核会根据需要分配请求的页面数给申请者。当驱动程序不需要申请的内存时,必须申请申请的页面数,以防止内存泄漏。本节将对页面的分配方法进行详细的讲述,这些知识对驱动程序开发非常的重要。
  • 186. 10.2.1 内存分配Linux内核内存管理子系统提供了一系列函数用来进行内存分配和释放。为了管理方便,Linux中是以页为单位来进行内存分配的。在32位的机器上,一般一页大小为4KB;在64位的机器上,一般一页大小为8KB,具体根据平台而定。当驱动程序的一个进程申请空间时,内存管理子系统会分配所请求的页数给驱动程序。如果驱动程序不需要内存时,也可以释放内存,将内存归还给内核为其他程序所用。下面将介绍内存管理子系统提供了哪些函数来进行内存的分配和释放。 1.内存分配函数的分类 2.alloc_page()和alloc_pages()函数 3.__get_free_page()和__get_free_pages()函数 4.内存释放函数
  • 187. 10.2.2 物理地址和虚拟地址之间的转换在内存分配的大多数函数中,大都涉及到物理地址和虚拟地址之间的转换。使用virt_to_phys()函数可以将内核虚拟地址转换为物理地址,virt_to_phys()函数定义如下代码所示。
  • 188. 10.3 设备I/O端口的访问设备有一组外部寄存器用来存储和控制设备的状态。存储设备状态的寄存器叫做数据寄存器;控制设备状态的寄存器叫做控制寄存器。这些寄存器可能位于内存空间,也可能位于I/O空间,本节介绍这些空间的寄存器访问方法。
  • 189. 10.3.1 Linux I/O端口读写函数设备内部集成了一些寄存器,程序可以通过寄存器来控制设备。大部分外部设备都有多个寄存器,例如看门狗控制寄存器WTCON、数据寄存器(WTDAT)和计数寄存器(WTCNT)等;有如IIC设备也有4个寄存器来完成所有IIC操作,这些寄存器是IICCON、IICSTAT、IICADD、IICCDS。
  • 190. 10.3.2 I/O内存读写可以将I/O端口映射到I/O内存空间来访问。如图10.3所示是I/O内存的访问流程,在设备驱动模块的加载函数或者open()函数中可以调用request_mem_region()函数来申请资源。使用ioremap()函数将I/O端口所在的物理地址映射到虚拟地址上。之后,就可以readb() readw() readl()等函数来读写寄存器中的内容了。当不在使用I/O内存时,可以使用iounmap()函数来释放物理地址到虚拟地址的映射。最后,使用release_mem_region()函数来释放申请的资源。
  • 191. 10.3.3 使用I/O端口对于使用I/O地址空间的外部设备,需要通过I/O端口和设备传输数据。在访问I/O端口前,需要向内核申请I/O端口使用的资源。如图10.4所示,在设备驱动模块的加载函数或者open()函数中可以调用request_region()函数来请求I/O端口资源;然后使用inb()、outb()、inw()、outw()等函数来读写外部设备的I/O端口;最后,在设备驱动程序的模块卸载函数或者release()函数中,释放申请的I/O内存资源。
  • 192. 10.4 小结外部设备可以处于内存空间或者I/O空间中,对于嵌入式产品来说,一般外部设备处于内存空间中。本章对外部设备处于内存空间和I/O空间的情况分别进行了讲解。在Linux中,为了编写驱动程序的方面,对内存空间和I/O空间的访问提供了一套统一的方法,这个方法是“申请资源->映射内存空间->访问内存->取消映射->释放资源”。 Linux中使用了后备高速缓存来频繁地分配和释放同一种对象,不但减少了内存碎片的出现,而且还提高了系统的性能。为驱动程序的高效性打下了基础。 Linux中提供了一套分配和释放页面的函数,这些函数可以根据需要分配物理连续或者不连续的内存,并将其映射到虚拟地址空间中,然后对其进行访问。
  • 193. 第11章 设备驱动模型在早期的Linux内核中并没有为设备驱动提供统一的设备模型。随着内核的不断庞大和系统的不断复杂,编写一个驱动程序越来越困难,所以在Linux 2.6内核中添加了一个统一的设备模型。这样,写设备驱动程序就稍微容易一些了。本章将对设备模型进行详细的介绍。
  • 194. 11.1 设备驱动模型概述设备驱动模型比较复杂,Linux系统将设备和驱动归一到设备驱动模型中来管理。设备驱动模型的提出,解决了以前编写驱动程序没有统一方法的局面。设备驱动模型给各种驱动程序提供了很多辅助性的函数,这些函数经过严格测试,可以很大程度地提高驱动开发人员的工作效率。
  • 195. 11.1.1 设备驱动模型的功能Linux内核的早期版本为编写驱动程序提供了简单的功能:分配内存、分配I/O地址、分配中断请求等。写好驱动之后,直接把程序加入到内核的相关初始化函数中,这是一个非常复杂的过程,所以开发驱动程序并不简单。并且,由于没有统一的设备驱动模型,几乎每一种设备驱动程序都需要自己完成所有的工作,驱动程序中不免会产生错误和大量的重复代码。 有了设备驱动模型后,现在的情况就不一样了。设备驱动模型提供了硬件的抽象,内核使用该抽象可以完成很多硬件重复的工作。这样很多重复的代码就不需要重新编写和调试了,编写驱动程序的难度有所下降。这些抽象包括如下几个方面: 1.电源管理 2.即插即用设备支持 3.与用户空间的通信
  • 196. 11.1.2 sysfs文件系统sysfs文件系统是Linux众多文件系统中的一个。在Linux系统中,每一个文件系统都有其特殊的用途。例如ext2用于快速读写存储文件;ext3用来记录日志文件。 Linux设备驱动模型由大量的数据结构和算法组成。这些数据结构之间的关系非常的复杂,多数结构之间通过指针互相关联,构成树形或者网状关系。显示这种关系的最好方法是利用一种树形的文件系统,但是这种文件系统需要具有其他文件系统没有的功能,例如显示内核中的一些关于设备、驱动和总线的信息。为了这个目的,Linux内核开发者创建了一种新的文件系统,这就是sysfs文件系统。
  • 197. 11.1.3 sysfs文件系统的目录结构sysfs文件系统中包含了一些重要的目录,这些目录中包含了与设备和驱动等相关的信息,现对其详细介绍如下: 1.sysfs文件系统的目录 2.block目录 3.bus目录 4.class目录
  • 198. 11.2 设备驱动模型的核心数据结构设备驱动模型由几个核心的数据结构组成:kobject、kset、subsystem。这些结构使设备驱动模型组成了一个层次的结构。这个层次结构将驱动、设备和总线等联系起来,形成一个完整的设备模型。下面分别对这些结构进行详细的介绍。
  • 199. 11.2.1 kobject结构体宏观上来说,设备驱动模型是一个设备和驱动组成的层次结构。例如一条总线上挂接了很多设备,总线在Linux中也是一种设备,为了表述清楚,这里将其命名为A。在A总线上挂接了一个USB控制器硬件B,在B上挂接了设备C和D,当然如果C和D是一种可以挂接其他设备的父设备,那么在C和D设备下也可以挂接其他设备,但这里认为他们是普通设备。另外在A总线上还挂接了E和F设备,那么这些设备的关系如图所示:
  • 200. 11.2.2 设备属性kobj_type每个kobject对象都有一些属性,这些属性由kobj_type结构体表示。最开始,内核开发者考虑将属性包含在kobject结构体中,后来考虑到同类设备会具有相同的属性,所以将属性隔离开来,有kobj_type表示。kobject中有指向kobj_type的指针,如图所示:
  • 201. 11.3 注册kobject到sysfs中的实例为了对kobject对象有一个清晰的认识,这里将尽快给读者展现一个完整的实例代码。在讲解这个实例代码之前,需要重点讲解一下到目前为止,我们需要知道的设备驱动模型结构。
  • 202. 11.3.1 设备驱动模型结构在Linux设备驱动模型中,设备驱动模型在内核中的关系用kobject结构体来表组织。在用户空间的关系用sysfs文件系统的结构来表示。如图所示,左边是bus子系统在内核中的关系,使用kobject结构体来组织。右边是sysfs文件系统的结构关系,使用目录和文件来表示。左边的kobject和右边的目录或者文件是一一对应的关系,如果左边有一个kobject对象,那么右边就对应一个目录。文件表示该kobject的属性,并不于kobject相对应。
  • 203. 11.3.2 kset集合kobject通过kset组织成层次化的结构。kset是具有相同类型的kobject的集合,如驱动程序一般放在/sys/drivers/目录下,目录drivers是一个kset对象,包含系统中的驱动程序对应的目录,驱动程序的目录由kobject来表示。
  • 204. 11.3.3 kset与kobject的关系kset是kobject的一个集合,用来与kobject建立层次关系。内核可以将相似的kobject结构连接在kset集合中,这些相似的kobject可能有相似的属性,使用统一的kset来表示。图显示了kset集合和koject之间的关系:
  • 205. 11.3.3 kset相关的操作函数kset相关的操作函数与kobject的函数相似,也有初始化、注册和注销等函数。下面对这些函数进行介绍: 1.初始化函数kset_init() 2.注册函数kset_register() 3.注销函数kset_unregister() 4.kset的引用计数
  • 206. 11.3.4 注册kobject到sysfs中的实例对kobject和kset有所了解后,将在本小节讲解一个实例程序,以使读者对这些概念有更清楚的认识。这个实例程序的功能是:在/sys目录中添加了一个名为kobject_test的目录名,并在该目录下添加了一个名为kobject_test_attr的文件,这个文件就是属性文件。本实例可以通过kobject_test_show()函数显示属性的值;也可以通过kobject_test_store()函数向属性中写入一个值。
  • 207. 11.3.5 实例测试使用make命令编译kboject_test.c文件,得到kobject_test.ko模块,然后使用insmod命令加载该模块。当模块加载后会在/sys目录中增加一个kobject_test的目录。
  • 208. 11.4 设备驱动模型的三大组件设备驱动模型有三个重要组件,分别是总线(bus_type)、设备(device)、驱动(driver)。下面对这三个重要组件进行分别介绍。
  • 209. 11.4.1 总线从硬件结构上来讲,物理总线有数据总线和地址总线。物理总线是处理器与一个或者多个设备之间的通道。在设备驱动模型中,所有设备都通过总线连接,此处的总线与物理总线不同,总线是物理总线的一个抽象,同时还包含一些硬件中不存在的虚拟总线。在设备驱动模型中,驱动程序是附属在总线上的。下面将首先介绍总线、设备和驱动之间的关系。 1.总线、设备、驱动关系 2.总线数据结构bus_type 3.bus_type声明实例 3.总线私有数据bus_type_private 4.总线注册bus_register()
  • 210. 11.4.2 总线属性和总线方法bus_type中还包含表示总线属性和总线方法的成员。属性使用成员bus_attrs表示,相对该成员介绍如下: 1.总线的属性bus_attribute 2.创建和删除总线属性 3.总线上的方法
  • 211. 11.4.3 设备在Linux设备驱动模型中,每一个设备都由一个device结构体来描述。device结构体包含了设备所具有的一些通用信息。对于驱动开发人员来说,当遇到新设备时,需要定义一个新的设备结构体,将device作为新结构体的成员。这样就可以在新结构体中定义新设备的一些信息,而设备通用的信息就使用device结构体来表示。使用device结构体的另一个好处是,可以通过device轻松的将新设备加入设备驱动模型的管理中。现对device结构体进行简要的介绍: 1.device结构体 2.设备注册和注销 3.设备属性
  • 212. 11.4.4 驱动在设备驱动模型中,记录了注册到系统中的所有设备。有些设备可以使用,有些设备不可以使用,原因是设备需要与对应的驱动程序绑定才能够使用,本节重点介绍设备驱动程序。 1.设备驱动device_driver 2.驱动举例 3.驱动程序注册和注销 4.驱动的属性
  • 213. 11.5 小结设备驱动模型是编写Linux驱动程序需要了解的重要知识。设备驱动模型中主要包括三大组件,分别是总线、设备和驱动。这三种结构之间的关系非常复杂,为了使驱动程序对用户进程来说是可见的,内核提供了sysfs文件系统来映射设备驱动模型各组件的关系。通过本章的学习,相信会对后面驱动实例的学习有很大的帮助。
  • 214. 第12章 RTC实时时钟驱动RTC实时时钟为操作系统提供一个可靠的时间,并且在断电的情况下,RTC实时时钟也可以通过电池供电,一直运行下去。在计算机系统中,经常会用到RTC实时时钟。例如,手机在关机模式下,仍然能够保证时间的正确性,就是因为RTC实时时钟可以在很小的耗电量下工作。在嵌入式系统中,RTC设备是一种常用的设备,所以学会写RTC实时时钟驱动程序是一件非常重要的工作。
  • 215. 12.1 RTC实时时钟硬件原理在编写驱动程序之前,需要首先了解一下RTC实时时钟的概念和硬件原理。熟悉RTC实时时钟的概念和硬件原理对驱动程序的编写又非常大的好处。首先来看看什么是RTC实时时钟。
  • 216. 12.1.1 RTC实时时钟RTC的英文全称是Real-Time Clock,一般称为RTC实时时钟。实时时钟(RTC)单元可以在系统电源关半闭的情况下依靠备用电池工作,一般主板上都有一个纽扣电池作为实时时钟的电源。RTC可以通过使用STRB/LDDRB这两个ARM指令向CPU传递8位数据(BCD码)。数据包括秒、分、小时、日期、天、月、和年。RTC实时时钟依靠一个外部的32.768kHZ的石晶体,产生周期性的脉冲信号。每一个脉冲信号到来时,计数器就加1,通过这种方式,完成计时功能。
  • 217. 12.1.2 RTC实时时钟的功能XTIrtc和XTOrtc产生脉冲信号。传给215的一个时钟分频器,得到一个128Hz的频率,这个频率用来产生滴答计数。当TICNT计数为0时,产生一个TIME TICK中断信号。RTCCON寄存器用来控制RTC实时时钟的功能。RTCRST是重置寄存器,用来重置SEC和MIN寄存器。Leap Year Generator是一个闰年发生器,用来产生闰年逻辑。RTCALM用来控制是否产生报警信号。
  • 218. 13.1.2 RTC实时时钟的工作原理RTC实时时钟的工作由多个寄存器来控制。RTCCON寄存器用来控制RTC实时时钟的整体功能。
  • 219. 12.1 RTC实时时钟架构本节对RTC实时时钟的整体架构进行了简要的分析。主要包括驱动程序的加载卸载函数、探测函数、使能函数、频率设置函数等。从这些函数的分析中,读者可以了解到整个驱动程序的架构,也能对RTC实时时钟的工作原理更为了解。
  • 220. 12.2.1 加载卸载函数RTC实时时钟的驱动程序包含在/drivers/rtc/Rtc-s3c.c文件中。RTC实时时钟的驱动模块逻辑比较简单,首先注册一个平台设备驱动,然后由平台设备驱动负责完成对RTC实时时钟的驱动工作。RTC模块的加载函数是s3c_rtc_init(),卸载函数是s3c_rtc_exit()。
  • 221. 12.2.2 RTC实时时钟的平台驱动在文件/drivers/rtc/Rtc-s3c.c中定义了RTC实时时钟的平台设备驱动。其中平台设备驱动的一些函数没有用处,所以没有定义。
  • 222. 12.2.3 RTC驱动探测函数当调用platform_driver_register()函数注册驱动之后,会触发平台设备和驱动的匹配函数platform_match()。匹配成功,则会调用平台设备驱动中的probe()函数,RTC实时时钟驱动中对应的函数就是s3c_rtc_probe()。
  • 223. 12.2.5 RTC实时时钟设置频率函数s3c_rtc_setfreq()时钟脉冲1秒钟产生128次时钟滴答。可以给TICNT寄存器的低7位赋值,取值范围为0到127,用n来表示。
  • 224. 12.2.6 RTC设备注册函数 rtc_device_register()RTC实时时钟设备必须注册到内核中才可以使用。在注册设备的过程中,将设备提供的应用程序的接口ops也指定到设备上。这样,当应用成员读取设备的数据时,就可以调用这些底层的驱动函数。注册RTC设备的函数是rtc_device_register()。
  • 225. 12.3 RTC文件系统接口和字符设备一样,RTC实时时钟驱动程序也定义了一个与flie_operation对应的rtc_class_ops结构体。这个结构体中的函数定义了文件系统中的对应函数。本节将对这些函数进行简要的分析,以使读者对驱动程序的读写有详细的了解。
  • 226. 12.3.1 文件系统接口rtc_class_opsrtc_class_ops是一个对设备进行操作的抽象结构体。内核允许为设备建立一个设备文件,对设备文件的所有操作,就相当于对设备的操作。这样的好处是,用户程序可以使用访问普通文件的方法,来访问设备文件,进而访问设备。这样的方法,极大地减轻了程序员的编程负担,程序员不必要去熟悉新的驱动接口,就能够访问设备。
  • 227. 12.3.2 RTC实时时钟打开函数s3c_rtc_open()RTC设备的打开函数由s3c_rtc_open()来实现。用户空间调用open()时,最终会调用s3c_rtc_open()函数。该函数主要申请了两个中断,一个报警中断,另一个是计时中断。
  • 228. 12.3.3 RTC实时时钟关闭函数s3c_rtc_release()RTC设备的释放函数由s3c_rtc_release()来实现。用户空间调用close()时,最终会调用s3c_rtc_release()函数。该函数主要释放s3c_rtc_open()函数中申请的两个中断,一个报警中断,另一个是计时中断。
  • 229. 12.3.4 RTC实时时钟获得时间函数s3c_rtc_gettime()当调用read()函数时会间接的调用s3c_rtc_gettime()函数来获得实时时钟的时间。时间值分别保存在RTC实时时钟的各个寄存器中。这些寄存器是秒寄存器(BCDSEC)、日期寄存器(SECDATA)、分钟寄存器(BCDMIN)、小时寄存器(BCDHOUR)。s3c_rtc_gettime()函数中会使用一个struct rtc_time的结构体,这个结构体表示一个时间值。
  • 230. 12.3.5 RTC实时时钟设置时间函数s3c_rtc_settime()当调用write()函数向设备驱动程序写入时间时,会间接的调用s3c_rtc_settime()函数来设置实时时钟的时间。时间值分别保存在RTC实时时钟的各个寄存器中。这些寄存器是秒寄存器(BCDSEC)、日期寄存器(SECDATA)、分钟寄存器(BCDMIN)、小时寄存器(BCDHOUR)。对应驱动程序中的S3C2410_RTCSEC、S3C2410_RTCDATE、S3C2410_RTCMIN、S3C2410_RTCHOUR等寄存器。
  • 231. 12.3.6 RTC驱动探测函数s3c_rtc_getalarm()在正常模式和掉电模式下,RTC在指定的时刻会产生一个报警信号。正常模式下,报警中断ALMINT有效,对应INT_RTC引脚。掉电模式下,报警中断ALMINT有效外还产生一个唤醒信号PMWKUP,对应PMWKUP引脚。RTC报警寄存器RTCALM决定是否使能报警状态和设置报警条件。
  • 232. 12.3.7 RTC实时时钟设置报警时间函数s3c_rtc_setalarm()与s3c_rtc_getalarm()函数对应的函数是s3c_rtc_setalarm()函数。s3c_rtc_setalarm()函数用来设置报警时间。
  • 233. 12.3.8 RTC设置脉冲中断使能函数s3c_rtc_setpie()s3c_rtc_setpie()函数用来设置是否允许脉冲中断。第一个参数是RTC设备结构体,第二个参数表示是否允许脉冲中断。enalbed等于1表示允许,enabled等于0表示不允许脉冲中断。
  • 234. 12.3.9 RTC时钟脉冲中断判断函数s3c_rtc_proc()在proc文件系统中,可以读取proc文件系统来判断RTC实时时钟是否支持脉冲中断。脉冲中断由TICNT寄存器的最高位来决定,最高位为1则表示使能脉冲中断,为0表示不允许脉冲中断。proc文件系统中的读取命令,一般为cat命令,会调用内核中的s3c_rtc_proc()函数。
  • 235. 12.4 小结RTC实时时钟是计算机中一个非常重要的计时系统。这个时钟为操作系统提供一个可靠的时间,并且在断电的情况下,RTC实时时钟也可以通过电池供电,一直运行下去。所以当断电后开机,操作系统仍然能够从RTC实时时钟中读出正确的时间来。另外RTC实时时钟也支持唤醒功能,可以在指定时刻将设备从睡眠或者关机状态唤醒。总之,在实际应用中,RTC实时时钟是一种广泛使用的设备。
  • 236. 第13章 看门狗驱动程序大多数设备中都有看门狗硬件,所以驱动开发人员需要去实现这种设备的驱动。看门狗的用途是当CPU进入错误状态后,无法恢复的情况下,使计算机重新启动。本章将对看门狗的原理和驱动程序进行详细分析。
  • 237. 13.1 看门狗硬件原理了解看门狗硬件的原理是写驱动程序的第一步,本节将对看门狗硬件的主要原理进行分析。
  • 238. 13.1.1 看门狗由于计算机在工作时不可避免的要受到各种各样因素的干扰,即使再优秀的计算机程序也可能因为这种干扰使计算机进入一个死循环,更严重的就是导致死机。有两种方法来处理这种情况,一是采用人工复位的方法,二是依赖某种硬件来执行这个复位工作。这种硬件通常叫做看门狗(Watch Dog,WD)。
  • 239. 13.1.2 看门狗工作原理S3C2440处理器内部集成了一个看门狗硬件。其提供了3个寄存器对看门狗进行操作。这3个寄存器分别是WTCON(看门狗控制寄存器)、WTDAT(看门狗数据寄存器)、WTCNT(看门狗计数寄存器)。
  • 240. 13.2 平台设备模型看门狗驱动中涉及到两种设备模型,分别是平台设备和混杂设备。本节分别对两种设备模型进行讲解。
  • 241. 13.2.1 平台设备模型从Linux 2.6起引入了一套新的驱动管理和注册模型:平台设备platform_device和平台驱动platform_driver。Linux中大部分的设备驱动,都可以使用这套机制,设备用platform_device表示,驱动用platform_driver进行表示。
  • 242. 13.2.2 平台设备在Linux设备驱动中,有一种设备叫做平台设备。平台设备是指处理器上集成的额外功能的附加设备,如Watch Dog、IIC、IIS、RTC、ADC等设备。这些格外功能设备是为了节约硬件成本、减少产品功耗、缩小产品形状而集成到处理器内部的。需要注意的是,平台设备并不是与字符设备、块设备和网络设备并列的概念,而是一种平行的概念,其从另一个角度对设备进行了概括。如果从内核开发者的角度来看,平台设备的引入是为了更容易的开发字符设备、块设备和网络设备驱动。
  • 243. 13.2.3 平台设备驱动每一个平台设备都对应一个平台设备驱动,这个驱动用来对平台设备进行探测、移除、关闭、电源管理等操作。平台设备用驱动platform_device结构体来描述。
  • 244. 13.2.4 平台设备驱动的注册和注销内核关于平台设备最主要的两个函数是注册和注销函数,本节对这两个函数进行说明。
  • 245. 13.1.5 混杂设备混杂设备并没有一个明确的定义。由于设备号比较紧张,所以一些不相关的设备可以使用同一个主设备,不同的次设备号。主设备号通常是10。由于这个原因,一些设备也可以叫做混杂设备。
  • 246. 13.1.6 混杂设备的注册和注销驱动程序中需要对混杂设备进行注册和注销,内核提供了misc_register ()和misc_deregister()两个函数。
  • 247. 13.3 看门狗设备驱动程序分析Linux 2.6内核中已经实现了S3C2440处理器的看门狗驱动。由于S3C2440与S3C2410的看门狗硬件没有变化,所以内核沿用了S3C2410的看门狗驱动。本节将对这个驱动进行详细的分析,通过这个驱动的学习,希望读者能举一反三,写出其他更好的驱动。
  • 248. 13.3.1 看门狗驱动的一些变量定义Linux内核中的linux-2.6.29.4\drivers\watchdog\s3c2410_wdt.c文件实现了看门狗驱动程序。此文件中也定义了看门狗驱动的一些变量,理解这些变量的意义,是理解看门狗驱动的前提。
  • 249. 13.3.2 看门狗模块的加载和卸载函数看门狗模块的加载函数watchdog_init()中调用platform_driver_register()函数来注册平台设备驱动。
  • 250. 13.3.3 看门狗驱动探测函数当调用platform_driver_register()函数注册驱动之后,会触发平台设备和驱动的匹配函数platform_match()。匹配成功,则会调用平台设备驱动中的probe()函数。
  • 251. 13.3.4 设置看门狗复位时间函数s3c2410wdt_set_heartbeat()在探测函数s3c2410wdt_probe()中的大多数函数,在前面的章节都有说明。这里重点讲解一下s3c2410wdt_set_heartbeat()函数,该函数的参数接收看门狗复位时间,默认是15秒。该函数先后完成如下几个功能: (1)使用clk_get_rate()函数获得看门狗的时钟频率PCLK。 (2)判断复位时间timeout是否超过计数寄存器WTCNT能表示的最大值,该寄存器的最大值为65536。 (3)设置第一个分频器的分频系数。 (4)设置数据寄存器WTDAT。
  • 252. 13.3.5 看门狗的开始函数s3c2410wdt_start()和停止函数s3c2410wdt_stop()为了控制看门狗的开始和停止,驱动中提供了开始和停止函数。 1.开始函数s3c2410wdt_start() 2.停止函数s3c2410wdt_stop()
  • 253. 13.3.6 看门狗驱动移除函数s3c2410wdt_remove()S3C2440肯门狗驱动的移除函数完成与探测函数相反的功能。其包括释放I/O内存资源、释放IRQ资源、禁止看门狗时钟源和注销混杂设备。
  • 254. 13.3.7 平台设备驱动s3c2410wdt_driver中的其他重要函数平台设备驱动s3c2410wdt_driver中的s3c2410wdt_probe ()和s3c2410wdt_remove()函数都已经说明,剩下另外几个重要的函数需要说明。 1.关闭函数s3c2410wdt_shutdown() 2.挂起函数s3c2410wdt_suspend() 3.恢复函数s3c2410wdt_resume()
  • 255. 13.3.9 混杂设备的file_operations中的函数混杂设备是一种特殊的字符设备,所以混杂设备的操作方法和字符设备的操作方法基本一样。看门狗的驱动中,混杂设备的定义如下所示。
  • 256. 13.4 小结本章详细地讲解了看门狗驱动程序的编写。首先介绍了看门狗的硬件原理,然后详细地介绍了看门狗的平台设备模型。最后对看门狗驱动程序进行了详细的分析。看门狗驱动程序中的函数主要用来控制看门狗硬件设备的相关寄存器,从而控制看门狗设备的功能。这是设备驱动程序的一种常见写法,需要引起读者的注意。
  • 257. 第14章 IIC设备驱动程序IIC设备是一种通过IIC总线直接连接的设备,由于其简单性,被广泛引用于电子系统中。在现代电子系统中,有很多的IIC设备需要进行相互之间的通信。为了提高硬件的效率和简化电路的设计,PHILIPS公司开发了IIC总线。IIC总线可以用于设备间的数据通信。本章将对IIC设备及其驱动进行详细的讲解。
  • 258. 14.1 IIC设备的总线及其协议IIC总线是由PHILIPS公司开发的两线式串行总线,用于连接微处理器和外部IIC设备。IIC设备产生于20世纪80年代,最初专用于音频和视频设备,现在在各种电子设备中都有广泛的应用。
  • 259. 14.1.1 IIC总线的特点IIC总线有两条总线线路:一条是串行数据线(SDA),一条是串行时钟线(SCL)。SDA负责数据传输,SCL负责数据传输的时钟同步。IIC设备通过这两条总线连接到处理器的IIC总线控制器上。 与其他总线相比,IIC总线有许多重要的特点。在选择一种设备来完成特定功能时,这些特点是选择IIC设备的重要依据。下面对IIC设备的主要特点进行简要的总结。
  • 260. 14.1.2 IIC总线的信号类型IIC总线在传输数据的过程中有3种类型的信号:开始信号、结束信号和应答信号。这些信号由SDA线和SCL线的电平高低变化来表示。
  • 261. 14.1.3 IIC总线的数据传输在分析IIC总线的数据传输前需要知道主机和从机的概念: 1.主机和从机 IIC总线中发送命令的设备称为主机,对于ARM处理器来说,主机就是IIC控制器。接受命令并响应命令的设备称为从机。 2.主机向从机发送数据 主机通过数据线SDA向从机发送数据。当总线空闲时,SDA和SCL信号都处于高电平。
  • 262. 14.2 IIC设备的硬件原理在写设备驱动程序之前,应该先了解一下IIC设备的硬件原理。S3C2440处理器中集成了一个IIC控制器,本节将对这个控制器的硬件结果进行详细的讲解。S3C2440中集成了一个IIC控制器,用来管理IIC设备,实现设备的数据接收和发送功能。
  • 263. 14.3 IIC设备驱动程序的层次结构因为IIC设备种类丰富,如果为每一个IIC设备写一个驱动程序,那么Linux内核中关于IIC设备的驱动就将非常庞大。这不设计方式不符合软件工程中的代码复用规则,所以需要对IIC设备驱动中的代码进行层次化组织。
  • 264. 14.3.1 IIC设备驱动的层次结构这里简单的将IIC设备驱动的层次分为:设备层,总线层。理解这两个层次的重点是理解4个数据结构,这4个数据结构是:i2c_driver、i2c_client、i2c_algorithm、i2c_adapter。i2c_driver、2c_client属于设备层;i2c_algorithm、i2c_adapter属于总线层。
  • 265. 14.3.2 IIC设备层IIC设备层由IIC设备和对应的设备驱动程序组成,分别用数据结构i2c_client和i2c_driver表示。
  • 266. 14.3.3 i2c_driver和i2c_client的关系结构体i2c_driver和i2c_client的关系较为简单,其中i2c_driver表示一个IIC设备驱动程序,i2c_client表示一个IIC设备。这两个结构体之间通过指针连接起来,其关系如图所示:
  • 267. 14.3.4 IIC总线层IIC总线层由总线适配器和适配器驱动程序组成,分别用数据结构i2c_adaptert和i2c_algorithm表示。
  • 268. 14.3.5 IIC设备层和总线层的关系大体上,IIC设备驱动程序可以分为设备层和总线层。设备层包括一个重要的数据结构i2c_client。总线层包括两个重要的数据结构,分别是i2c_adapter和i2c_algorithm。一个i2c_client结构表示一个物理的IIC设备;一个i2c_adapter结构对应一个物理上的适配器;一个i2c_algorithm结构表示适配器对应的传输数据的方法。这三个数据结构的关系如图所示:
  • 269. 14.3.6 写IIC设备驱动的步骤IIC设备层次结构较为简单,但是写IIC设备驱动程序却相当的复杂。当工程师拿到一个新的电路板时,面对复杂的Linux IIC子系统,应该如下下手编程呢?首先需要思考的是哪些工作需要自己完成,哪些工作内核已经提供了。这个问题的答案如图所示:
  • 270. 14.4 IIC子系统的初始化在启动系统时,需要对IIC子系统进行初始化。这些初始化函数包含在i2c-core.c文件中。该文件中包含IIC子系统中的公用代码,驱动开发人员只需要用它,而不需要修改它。下面对这些公用代码的主要部门进行介绍。
  • 271. 14.4.1 IIC子系统初始化函数i2c_init()IIC子系统是作为模块加载到系统中的。在系统启动中的模块加载阶段,会调用i2c_init()函数初始化IIC子系统。
  • 272. 14.4.2 IIC子系统退出函数i2c_exit ()与i2c_init()函数对应的退出函数是i2c_exit()。该函数完成i2c_init()函数相反的功能。
  • 273. 14.5 适配器驱动程序适配器驱动程序是IIC设备驱动程序需要实现的主要驱动程序,这个驱动程序需要根据具体的适配器硬件来编写,本节将对失陪器驱动程序进行详细的讲解。
  • 274. 14.5.1 s3c2440对应的适配器结构体i2c_adapter结构体为描述各种IIC适配器提供了通用“模板”,它定义了注册总线上所有设备的clients链表、指向具体IIC适配器的总线通信方法i2c_algorithm的algo指针、实现i2c总线操作原子性的lock信号量。但i2c_adapter结构体只是所有适配器的共有属性,并不能代表所有的类型的适配器。
  • 275. 14.5.2 IIC适配器加载函数i2c_add_adapter()当驱动开发人员拿到一块新的电路板,并研究了响应的IIC适配器之后,就应该使用内核提供的框架函数向IIC子系统中添加一个新的适配器。这个过程如下所示: (1)分配一个IIC适配器,并初始化相应的变量。 (2)使用i2c_add_adapter()函数向IIC子系统添加适配器结构体i2c_adapter。这个结构体已经在第一步初始化了。i2c_add_adapter()函数的代码如下所示:
  • 276. 14.5.3 IDR机制IDR机制在Linux内核中指的就是整数ID管理机制。从实质上来讲,这就是一种将一个整数ID号和一个指针关联在一起的机制。这个机制最早是在2003年2月加入内核的,当时是作为POSIX定时器的一个补丁。现在,在内核的很多地方都可以找到IDR的身影。
  • 277. 14.5.4 适配器卸载函数i2c_del_adapter()与适配器加载函数i2c_add_adapter()对应的卸载函数是i2c_del_adapter()。该函数完成与加载函数相反的功能。i2c_del_adapter()函数用于注销适配器的数据结构、删除其总线上所有设备的i2c_client数据结构和对应的i2c_driver驱动程序。并减少其代表总线上所有设备的相应驱动程序数据结构的引用计数(如果到达0,则卸载设备驱动程序)。
  • 278. 14.5.5 IIC总线通信方法s3c24xx_i2c_algorithm结构体s3c24xx_i2c适配器的成员变量adap中的algo成员指向了该适配器的通讯方法s3c24xx_i2c_algorithm结构体。
  • 279. 14.5.6 适配器的传输函数s3c24xx_i2c_doxfer()上一小节的函数s3c24xx_i2c_xfer()的第09行调用了自定义的传输函数s3c24xx_i2c_doxfer()。该函数操作适配器来完成具体的数据传输任务。
  • 280. 14.5.7 适配器的中断处理函数s3c24xx_i2c_irq()顺着通讯函数s3c24xx_i2c_xfer()的执行流分析,函数最终会返回,但并没有传输数据。传输数据的过程被交到了中断处理函数中。这是因为IIC设备的读写是非常慢的,需要使用中断的方法提高处理器的效率,这在操作系统的课程中非常常见。这里,首先复习一个数据通信方法的调用关系。 1.数据通信方法的调用关系 2.中断处理函数s3c24xx_i2c_irq()
  • 281. 14.5.8 字节传输函数i2s_s3c_irq_nextbyte()i2s_s3c_irq_nextbyte()函数用来传送下一个字节。
  • 282. 14.5.9 适配器传输停止函数s3c24xx_i2c_stop()s3c24xx_i2c_stop()函数主要完成个功能,如下所示: (1)向总线发出结束P信号。 (2)唤醒等待在队列s3c24xx_i2c->wait中的进程,一次传输完毕。 (3)禁止中断,适配器中不产生中断信号。
  • 283. 14.5.10 中断处理函数的一些辅助函数i2s_s3c_irq_nextbyte()函数中使用了一些辅助函数,集中在这里介绍。这些函数如下所示: 1.is_lastmsg()函数 2.is_msgend()函数 3.禁止应答信号函数s3c24xx_i2c_disable_ack()
  • 284. 14.6 IIC设备层驱动程序 本节将详细的讲解IIC设备层程序。这个驱动程序中包括模块加载和卸载函数、探测函数、初始化化函数等,具体的实现在本节详细的分析。
  • 285. 14.6.1 IIC设备驱动模块加载和卸载IIC设备驱动被作为一个单独的模块加入进内核,在模块的加载和卸载函数中需要注册和注销一个平台驱动结构体platform_driver。平台驱动的概念在13章已经详细讲解,不熟悉的读者可以查阅前面的章节。 1.平台驱动的加载和卸载 2.平台驱动s3c2410_i2c_driver
  • 286. 14.6.2 探测函数s3c24xx_i2c_probe()平台设备注册函数platform_driver_register()中会调用探测函数3c24xx_i2c_probe()。在该函数中将初始化适配器、IIC等硬件设备。其主要完成如下几个功能: (1)申请一个适配器结构体i2c,并对其赋初值。 (2)获得i2c时钟资源。 (3)将适配器的寄存器资源映射到虚拟内存中。 (4)申请中断处理函数。 (5)初始化IIC控制器。 (6)添加适配器i2c到内核中。
  • 287. 14.6.3 移除函数s3c24xx_i2c_remove()与s3c24xx_i2c_probe()函数完成相反功能的函数是s3c24xx_i2c_remove()函数,它在模块卸载函数调用platform_driver_unregister()函数时通过platform_driver的remove指针被调用。
  • 288. 14.6.4 控制器初始化函数s3c24xx_i2c_init()探测函数s3c24xx_i2c_probe()中调用s3c24xx_i2c_init()函数来初始化乎适配器。
  • 289. 14.6.5 设置控制器数据发送频率函数s3c24xx_i2c_clockrate()控制器初始化函数s3c24xx_i2c_init()中调用了s3c24xx_i2c_clockrate()函数来设置数据发送频率。此发送频率由IICCON寄存器来控制。发送频率可以由一个公式得到,这个公式是:
  • 290. 14.7 小结IIC设备是嵌入式系统中一种常见的设备。由于生产厂商很多,所以IIC设备的种类也很多。主机与IIC设备之间的通讯需要遵守IIC通讯协议,本章在第一节详细介绍了IIC总线通讯协议。然后重点介绍了IIC子系统中几个关键的数据结构和它们之间的关系。最后以一个驱动程序为例,贯穿了整个章节。通过本章的学习,希望读者能够触类旁通,学会IIC设备驱动程序的编写方法。
  • 291. 第15章 LCD设备驱动程序LCD是Liquid Crystal Display 的简称,也就是经常所说的液晶显示器。在日常应用的推动下,LCD的应用越来越广泛。从手机、掌上电脑、MP3到大型工业设备,都可以看到LCD的身影。LCD能够支持彩色图像的显示和视频的播放,是一种非常重要的输出设备,本章对LCD设备及其驱动程序进行详细的介绍。
  • 292. 15.1 FrameBuffer概述Framebuffer 是Linux系统为显示设备提供的一个接口,它将显示缓冲区抽象,屏蔽图像硬件的底层差异,允许上层应用程序在图形模式下直接对显示缓冲区进行操作。本章对FrameBuffer进行简要的概述。
  • 293. 15.1.1 FrameBuffer的概念FrameBuffer又叫帧缓冲,是Linux为操作显示设备提供的一个用户接口。用户应用程序可以通过FrameBuffer透明的访问不同类型的显示设备。从这个方面来说,FrameBuffer是硬件设备的显示缓存区的抽象。Linux抽象出FrameBuffer这个帧缓冲区可以供用户应用程序直接读写,通过更改FrameBuffer中的内容,就可以立刻显示在LCD显示屏上。
  • 294. 15.1.2 FrameBuffer与应用程序的交互在Linux中,Framebuffer是一种能够提取图形的硬件设备,是用户进入图形界面的很好接口。Framebuffer是显存抽象后的一种设备,它允许上层应用程序在图形模式下直接对显示缓冲区进行读写操作。这种操作是抽象的,统一的。用户不必关心物理显存的位置、换页机制等等具体细节。这些都是由Framebuffer设备驱动来完成的。有了Framebuffer,用户的应用程序不需要对底层的驱动深入了解就能够做出很好的图形。
  • 295. 15.1.3 FrameBuffer显示原理 通过framebuffer ,应用程序用mmap()把显存映射到应用程序虚拟地址空间。应用程序只需要将要显示的数据写入这个内存空间,然后LDC控制器会自动的将这个内存空间(显存)中的数据显示在LCD显示屏上。
  • 296. 15.1.4 LCD显示原理 简单的讲,framebuffer驱动的功能就是分配一块内存作显存,然后对LCD控制器的寄存器作一些设置。LCD显示器会不断的从显存中获得数据,并将其显示在LCD显示器上。LCD显示器可以显示显存中的一个区域或者整个区域。framebuffer驱动程序提供了操作显存的功能,例如复制显存、向显存中写入数据(画圆、画方型等)。 具体来说,实现这些操作的方法是:填充一个fbinfo结构用reigster_framebuffer(fbinfo*)将fbinfo结构注册到内核,对于fbinfo结构,最主要的是它的fs_ops成员,需要针对具体设备实现fs_ops中的接口。
  • 297. 15.2 FrameBuffer的结构分析FrameBuffer是LCD驱动中重要的一部分。通过FrameBuffer使Linux内核可以使用大多数显示设备的功能。本节将对FrameBuffer进行详细的分析。
  • 298. 15.2.1 FrameBuffer架构和其关系 在Linux内核中,FrameBuffer设备驱动的源码主要位于linux/inlcude/fb.h和linux/drivers/video/fbmem.c这两个文件中,它们处于FrameBuffer驱动体系结构的中间层,它为上层的用户程序提供系统调用,也为底层特定硬件驱动提供了接口。
  • 299. 15.2.2 FrameBuffer驱动程序的实现从应用程序和操作系统方面来看,FrameBuffer应该提供一些通用的功能来配合应用程序和操作系统的绘制。一般来说,应用程序通过内核对Framebuffer的控制,主要有下面3种方式: (1)读/写/dev/fb相当于读/写屏幕缓冲区。 (2)通过映射操作,可将屏幕缓冲区的物理地址映射到用户空问的一段虚拟地址中,之后用户就可以通过读写这段虚拟地址访问屏幕缓冲区,在屏幕上绘图。 (3)I/O控制对于帧缓冲设备,设备文件的ioctl()函数可读取/设置显示设备及屏幕的参数,如分辨率、显示颜色数、屏幕大小等等。ioctl()函数是由底层的驱动程序来完成的。
  • 300. 15.2.3 FrameBuffer架构及其关系 FrameBuffer驱动程序由几个重要的数据结构组成。这几个数据结构是fb_info、fb_ops、fb_cmap、fb_var_screeninfo和fb_fix_screeninfo,下面对这些数据结构进行详细的介绍。
  • 301. 15.3 LCD驱动程序分析LCD驱动程序以平台设备的方式实现,其中涉及到关于LCD控制器的一些重要概念。下面对LCD驱动程序的主要函数进行详细的分析。
  • 302. 15.3.1 LCD模块的加载和卸载函数LCD设备驱动可以作为一个单独的模块加入进内核,在模块的加载和卸载函数中需要注册和注销一个平台驱动结构体platform_driver。平台驱动的概念在13章已经详细讲解,不熟悉的读者可以查阅前面的章节。本节以LCD控制器的驱动程序为例,讲解LCD控制器的驱动程序。
  • 303. 15.3.2 LCD驱动程序的平台数据为了方面管理,Linxu内核将LCD驱动程序归入平台设备的范畴。这样就可以使用操作平台设备的方法来操作LCD设备。LCD驱动程序的平台设备定义为s3c_device_lcd结构体。
  • 304. 15.3.3 LCD模块的探测函数s3c2412fb_probe()函数中调用了s3c24xxfb_probe()函数,该函数的第二个参数是处理器的类型。
  • 305. 15.3.4 移除函数与s3c2412fb_probe()函数完成相反功能的函数是s3c2410fb_remove()函数,它在模块卸载函数调用platform_driver_unregister()函数时通过platform_driver的remove指针被调用。
  • 306. 15.4 小结本章实现了基于FrameBuffer的LCD驱动程序,首先简要的介绍了LCD设备的工作原理。然后讲述了FrameBuffer的显卡技术,重点讲解了操作FrameBuffer的主要数据结构和函数。最后重点讲述了基于FrameBuffer机制的LCD设备驱动程序,讲述了LCD控制器设备的初始化化,卸载函数等。同时,也讲述了通过LCD控制器驱动程序如何操作FrameBufffer的方法。通过本章的学习,读者可以学到基于FrameBuffer技术的LCD驱动程序的实现,同时对Frame的工作原理有一定的了解。
  • 307. 第16章 触摸屏设备驱动程序由于触摸屏设备使用简单、价格相对低廉,它的应用随处可见。在消费电子产品、工业控制系统、甚至航空领域都所有应用。随着触摸屏设备技术的成熟和价格的日益下降,在我们的日常生活中也经常使用带触摸屏的设备。例如银行的ATM机、机场的查询等级系统、手机、MP3、掌上电脑等。正因为触摸屏设备应用如此广泛,所以掌握触摸屏设备驱动程序的编写对驱动开发者来说非常重要。本章将对触摸屏设备驱动程序进行详细的介绍。
  • 308. 16.1 触摸屏设备工作原理本节对触摸屏设备的工作原理进行了简要的介绍,并介绍了触摸屏设备的主要类型。其中重点介绍了电阻式触摸屏设备。这些都是写触摸屏设备驱动程序的基础,下面对这些主要内容分别进行介绍。
  • 309. 16.1.1 触摸屏设备概述触摸屏作为一种最新的电脑输入设备,它是目前最简单、方便、自然的一种人机交互方式。它具有坚固耐用、反应速度快、节省空间、易于交流等许多优点。利用这种技术,用户只要用手指轻轻地碰计算机显示屏上的图符或文字就能实现对主机操作,从而使人机交互更为直截了当,这种技术大大方便了那些不懂电脑操作的用户。事实上,触摸屏是一个使多媒体信息系统改头换面的设备。它赋予多媒体系统以崭新的面貌,是极富吸引力的全新多媒体交互设备。
  • 310. 16.1.2 触摸屏设备的类型从技术原理来区别触摸屏,可分将触摸屏分为五个种类:矢量压力传感技术触摸屏、电阻技术触摸屏、电容技术触摸屏、红外线技术触摸屏、表面声波技术触摸屏。
  • 311. 16.1.3 电阻式触摸屏电阻触摸屏的屏体部分是一块与显示器表面相匹配的多层复合薄膜,由一层玻璃或有机玻璃作为基层,表面涂有一层透明的导电层,上面再盖有一层外表面硬化处理、光滑防刮的塑料层,它的内表面也涂有一层透明导电层,在两层导电层之间有许多细小(小于千分之一英寸)的透明隔离点把它们隔开绝缘。
  • 312. 16.2 触摸屏设备硬件结构要完全理解触摸屏设备驱动程序,必须对触摸屏接口有所了解。本节针对S3C2440处理器的触摸屏接口,对触摸屏接口的硬件原理进行详细的讲述。
  • 313. 16.2.1 S3C2440触摸屏接口概述S3C2440芯片支持触摸屏接口。这个触摸屏接口包括一个外部晶体管控制逻辑和一个模数转换器ADC。S3C2440芯片具有一个8通道的10位CMOS模数转换器(ADC)。它将输入的模拟信号转换为10位的二进制数字数据。在2.5MHz的A/D转换器频率下,最大转化速率可达到500KSPS。A/D转换器支持片上采样和保持功能,并支持掉电模式。
  • 314. 16.2.2 S3C2440触摸屏接口的工作模式S3C2440触摸屏接口有4种工作模式。在不同的工作模式下,触摸屏设备完成不同的功能。在某些情况下,几种工作模式需要互相配合,才能够完成一定的功能。这4种工作模式分别是: 1.正常转换模式 2.等待中断模式 3.独立的X/Y位置转换模式 4.自动X/Y位置转换模式
  • 315. 16.2.3 S3C2440触摸屏设备寄存器寄存器是主机控制设备的最主要方式之一。下面对触摸屏设备的相关寄存器进行详细的介绍,这些寄存器包括ADC控制寄存器、ADC触摸屏控制寄存器、ADC延时寄存器、ADC转换数据寄存器。在具体的代码中,遇到对这些寄存器的操作时,读者应该会过来对照本节的知识,以完整的领会程序的功能。
  • 316. 16.3 触摸屏设备驱动程序分析Linux 2.6内核中已经实现了S3C2440处理器的触摸屏驱动程序。由于S3C2440与S3C2410的触摸屏硬件变化不大,所以稍微对S3C2410的触摸屏驱动进行改写,就能够得到S3C2440处理器的触摸屏驱动程序。本节将对这个驱动程序进行详细的分析,通过这个驱动程序的学习,希望读者能举一反三,写出其他更好的驱动。
  • 317. 16.3.1 触摸屏设备驱动程序组成触摸屏设备驱动程序的初始化函数,退出函数,中断处理函数的关系如图所示。
  • 318. 16.3.2 S3C2440触摸屏驱动模块的加载和卸载函数首先分析触摸屏设备驱动程序的初始化和退出,了解触摸屏设备驱动程序的加载和卸载函数的实现。 1.加载和卸载函数 2.触摸屏设备驱动驱动结构体
  • 319. 16.3.3 S3C2440触摸屏驱动模块的探测函数当调用driver_register()函数注册成功驱动之后,内核会以s3c2410ts_driver中的name成员为依据,在系统中查找已经注册的具有相同name的设备,如果找到相应的设备,就调用s3c2410ts_driver中定义的探测函数probe()。 这里的probe()函数就是s3c2410ts_probe()。这个函数在触摸屏设备的初始化过程中,检查设备是否准备就绪、映射物理地址到虚拟地址、配置GPIO引脚、注册相应的中断等。
  • 320. 16.3.4 触摸屏设备配置触摸屏设备接口和处理器芯片的引脚连接如图所示,从图中可以看出触摸屏接口和处理器之间的关系。明白这些关系,是驱动程序设计的基础,下面对这些关系进行详细的分析。
  • 321. 16.3.5 触摸屏设备中断处理函数当触摸屏设备驱动的探测函数s3c2410ts_probe()执行完成之后,驱动程序处于等待状态。在等待状态中,驱动程序可以接收两个中断信号,并触发中断处理函数。这两个中断是触摸屏中断(IRQ_TC)和ADC中断(IRQ_ADC)。在s3c2410ts_probe()函数中,调用request_irq()函数注册了两个中断,现对这两个中断进行详细的讲解。 1.stylus_updown()函数 2.touch_timer_fire()函数 3.stylus_action()函数 4.touch_timer定时器
  • 322. 16.3.6 S3C2440触摸屏驱动模块的remove函数remove()函数是Linux设备驱动程序中一个非常重要的函数,这个函数实现了与probe()函数相反的功能。体现了Linux内核中,资源分配和释放的思想。资源应该在使用时分配,在不使用时释放。触摸屏设备驱动程序的remove()函数由s3c2410ts_remove()函数来实现。这个函数中释放了申请的中断、时钟、内存等。
  • 323. 16.4 测试触摸屏驱动程序测试触摸屏驱动程序是否工作正确,最简单的一种方法是在驱动程序中加入一些打印坐标的信息,从这些坐标中分析触摸屏设备驱动程序是否工作正常。touch_timer_fire()函数会不断的调用去读输入缓冲区中的数据,在touch_timer_fire()函数中加入第14到21行,就能够打印出调试信息。
  • 324. 16.5 小结本章讲解了触摸屏设备驱动程序的实例。首先对触摸屏设备的硬件原型进行详细的讲述。然后对触摸屏设备的接口电路和寄存器也进行了详细的讲述。接着详细讲述了触摸屏设备驱动程序的加载和卸载函数、probe()、中断处理函数等。通过对本章的学习,读者能够知道触摸屏设备驱动在Linux中的具体实现,并对触摸屏设备的工作原理有一定的了解。
  • 325. 第17章 输入子系统设计本章将介绍Linux输入子系统的驱动开发。Linux的输入子系统不仅支持鼠标、键盘等常规输入设备,而且还可以支持蜂鸣器、触摸屏等设备。本章将对Linux输入子系统进行详细的分析。
  • 326. 17.1 input子系统入门输入子系统又叫input子系统。其构建非常灵活,只需要调用一些简单的函数,就可以将一个输入设备的功能呈现给应用程序。本节将从一个实例开始,介绍编写输入子系统驱动程序的方法。
  • 327. 17.1.1 简单的实例本节将讲述一个简单的输入设备驱动实例。这个输入设备只有一个按键,按键被连接到一条中断线上,当按键被按下时,将产生一个中断,内核将检测到这个中断,并对其进行处理。
  • 328. 17.1.2 注册函数input_register_device()button_init()函数中的28行调用了input_register_device()函数来注册输入设备结构体。input_register_device()函数是输入子系统核心(input core)提供的函数。这个函数将input_dev结构体注册到输入子系统核心中,input_dev结构体必须由前面讲的input_allocate_device()函数来分配。input_register_device()函数如果注册失败,必须调用input_free_device()函数来释放分配的空间。如果该函数注册成功,在卸载函数中应该调用input_unregister_device()函数来注销输入设备结构体。
  • 329. 17.1.3 向子系统报告事件在本节button_interrupt()函数的06行调用了input_report_key()函数来向输入子系统报告发生的事件,这里就是一个按键事件。在button_interrupt()中断函数中,不需要考虑重复按键的重复点击情况,input_report_key()函数会自动检查这个问题,并报告一次事件给输入子系统。
  • 330. 17.2.1 输入子系统的组成前面主要讲解了input_dev相关的函数,本小节将总结前面的知识,并引出新的知识。为了使读者对输入子系统有整体的了解,对输入子系统的组成进行简要的介绍。后面的章节将围绕输入子系统的各个组成部分来学习。首先,看看图示,表示了输入子系统的组成。
  • 331. 17.2.2 input_handler结构体input_handler是输入设备的事件处理接口,为处理事件提供一个统一的函数模板,程序员应该根据具体的需哟实现其中的一些函数,并将其注册到输入子系统中。
  • 332. 17.2.3 注册input_handlerinput_register_handler()函数注册一个新的input handler处理器。这个handler将为输入设备使用,一个handler可以添加到多个支持它的设备中,也就是一个handler可以处理多个输入设备的事件。
  • 333. 17.2.4 input_handle结构体input_register_handle()函数用来注册一个新的handle到输入子系统中。input_handle的主要功能是用来连接input_dev和input_handler。
  • 334. 17.2.5 注册input_handleinput_handle是用来连接input_dev和input_handler的一个中间结构体。事件通过input_handle从input_dev发送到input_handler,或者从input_handler发送到input_dev进行处理。在使用input_handle之前,需要对其进行注册,注册函数是input_register_handle()。
  • 335. 17.3 input子系统为了对输入子系统有一个清晰的认识,本节将分析输入系统的初始化过程。在Linux中,输入子系统作为一个模块存在。向上,为用户层提供接口函数。向下,为驱动层程序提供统一的接口函数。这样,就能够使输入设备的事件通过输入子系统发送给用户层应用程序,用户层应用程序也可以通过输入子系统通知驱动程序完成某项功能。
  • 336. 17.3.1 子系统初始化函数input_init()输入子系统作为一个模块存在,必然有一个初始化函数。在/drivers/input/input.c文件中定义了输入子系统的初始化函数input_init()。
  • 337. 17.3.2 文件打开函数input_open_file()文件操作指针中定义了input_open_file()函数,该函数将控制转到input_handler中定义的fops文件指针的open()函数。该函数在input_handler中实现,这样就使不同的handler处理器对应了不同的文件打开方法,为完成不同功能提供了方面。
  • 338. 17.4 evdev输入事件驱动分析evdev输入事件驱动,为输入子系统提供了一个默认的事件处理方法。其接收来自底层驱动的大多数事件,并使用相应的逻辑对其进行处理。evdev输入事件驱动从底层接收事件信息,将其反映到sys文件系统中,用户程序通过对sys文件系统的操作,就能够达到处理事件的能力。下面先对evdev的初始化进行简要的分析。
  • 339. 17.4.1 evdev的初始化evdev以模块的方式被组织在内核中,与其他模块一样,也具有初始化函数和卸载函数。evdev的初始化主要完成一些注册工作,使内核认识evdev的存在。 1.evdev_init()初始化函数 2.evdev_connect()函数
  • 340. 17.4.1 evdev设备的打开用户程序通过输入子系统创建的设备节点的open(),read(),write()等函数打开和读写输入设备。创建的设备节点显示在/dev/input/目录下,由eventx来表示。
  • 341. 17.5 小结在本章中,分析了整个输入子系统的架构。Linux设备驱动采用了分层的模式,从最下层的设备模型到设备、驱动、总线再到input子系统最后到input device。这样的分层结构使得最上层的驱动不必关心下层是怎么实现的,而下层驱动又为多种型号同样功能的驱动提供了一个统一的接口。
  • 342. 第18章 块设备驱动程序除了字符设备、网络设备外,Linux系统中还有块设备。字符设备和块设备在内核中的结构有很大的不同,总体来说,块设备要比字符设备复杂很多。块设备主要包含磁盘设备、SD卡等,这些设备是Linux系统中不可缺少的存储设备。计算机中都需要这样的设备来存储数据,所以学会块设备驱动程序的写法是非常重要的。
  • 343. 18.1 块设备简介本节对块设备的相关概念进行了简要的分析。理解这些概念对写块设备驱动程序具有十分重要的意义。
  • 344. 18.1.1 块设备总体概述Linux内核中,I/O设备大致分为两类:块设备和字符设备。块设备将信息存储在固定大小的块中,每个块都有自己的地址。数据块的大小通常在512字节到4K字节之间。块设备的基本特征是每个块都能独立于其它块而读写。磁盘就是最常见的块设备。在Linux内核中,块设备与内核其他模块的关系如图所示:
  • 345. 18.1.2 块设备的结构在写块设备驱动程序之前,了解典型块设备的结构是非常重要的。图显示的是磁盘的一个盘面,一些重要的概念将在下面讲述。
  • 346. 18.2 块设备驱动程序的架构相对于字符设备来说,块设备的驱动程序架构要稍微复杂一些,其中涉及到很多重要的概念。对这些概念的理解是编写驱动程序的前提,本节将对块设备的整体架构进行详细讲解。
  • 347. 18.2.1 块设备加载过程在块设备的模块加载函数中,需要完成的一些重要工作,这些工作涉及到的一些重要概念,将在后面的小节中进行讲解,本节的目的是为了给出一个整体的概念。块设备驱动加载模块中需要完成的工作如下图所示:
  • 348. 18.2.2 块设备卸载过程在块设备驱动的卸载模块中完成与模块加载函数相反的工作。 (1)使用del_gendisk()函数删除gendisk设备,并使用put_disk()函数删除对gendisk设备的引用。 (2)使用blk_cleanup_queue()函数清除请求队列,并释放请求队列所占用的资源。 (3)如果在模块加载函数中使用了register_blkdev()注册设备,那么需要在模块卸载函数中使用unregister_blkdev()函数注销块设备,并释放对块设备的引用。
  • 349. 18.3 通用块层通用块层是块设备驱动的核心部分,这部分主要包含块设备驱动程序的通用代码部分。本节将介绍通用块层的主要函数和数据结构。
  • 350. 18.3.1 通用块层通用块层是一个内核组件,它处理来自系统其他组件发出的块设备请求。换句话说,通用块层包含了块设备操作的一些通用函数和数据结构。图是块设备加载函数中用到的一些重要数据结构,如通用磁盘结构gendisk、请求队列结构request_queue、请求结构request、块设备I/O操作结构bio、块设备操作结构block_device_operations等。这些结构将在下面的几小节详细简述。
  • 351. 18.3.2 alloc_disk()函数对应的gendisk结构体现实生活中有许多具体的物理块设备,例如磁盘、光盘等。不同的物理块设备其结构是不一样的,为了将这些块设备公用属性在内核中统一,内核开发者定义了一个gendisk结构体来描述磁盘。gendisk是general disk的简称,一般称为通用磁盘。
  • 352. 18.3.3 块设备的注册和注销为了使内核知道块设备的存在,需要使用块设备注册函数。在不使用块设备时,也需要注销块设备。块设备的注册和注销如下所述: 1.注册块设备函数register_blkdev() 2.注销块设备函数unregister_blkdev()
  • 353. 18.3.4 请求队列简单的讲,一个块设备的请求队列就是包含块设备I/O请求的一个队列。这个队列使用链表线性的排列。请求队列中存储未完成的块设备I/O请求,并不是所有的I/O块请求都可以顺利的加入请求队列中。请求队列中定义了自己能处理的块设备请求限制。这些限制包括:请求的最大尺寸、一个请求能够包含的独立段数、硬盘扇区大小等。
  • 354. 18.3.5 设置gendisk属性中的block_device_operations结构体在块设备中有一个和字符设备中file_operations对应的结构体block_device_operations。其也是一个对块设备操作的函数集合。 下面对这个结构体的主要成员进行分析。 1.打开和释放函数 2.I/O控制函数 3.介质改变函数 4.使介质有效函数 5.获得驱动器信息的函数 6.模块指针
  • 355. 18.4 不使用请求队列的块设备驱动这里,有两个原因需要向读者介绍不使用请求队列的块设备驱动程序。第一个原因是,希望尽快的向读者展现一个完整的块设备驱动程序;第二个原因是,不使用请求队列的块设备驱动程序相对来说,比较简单。
  • 356. 18.4.1 不使用请求队列的块设备驱动程序的组成块设备函数驱动程序主要有一个加载函数、卸载函数和一个自定义的请求处理函数组成。本节将写一个虚拟的块设备驱动程序Virtual_blkdev。这个驱动程序在内存中开辟了一个8M的内存空间来模拟实际的物理块设备。这个块设备驱动程序代码比较简单,但功能却非常强大。对实际物理设备的操作命令同样可以应用在Virtual_blkdev这个块设备上,例如mkdir、mkesfs等命令。
  • 357. 18.4.2 宏定义和全局变量Virtual_blkdev块设备驱动中定义了一些重要的宏和全局指针,包括主设备号、设备名、设备的大小等。
  • 358. 18.4.3 加载函数Virtual_blkdev设备的加载函数主要完成分配磁盘、初始化请求队列、设置磁盘属性和激活磁盘的工作。
  • 359. 18.4.4 卸载函数Virtual_blkdev设备的卸载函数中主要完成与设备加载函数中相反的工作: (1)使用del_gendisk()函数删除gendisk设备。 (2)使用put_disk()函数清楚gendisk的引用计数。 (3)使用blk_cleanup_queue()函数清除请求队列。
  • 360. 18.4.5 自定义请求处理函数内核将I/O读写请求放入请求结构request中,并连接到请求队列request_queue中。因为Virtual_blkdev设备是一个基于内存的设备,可以随机读取数据,并不需要复杂的I/O调度(I/O调度的作用是对请求结构request进行排序,最大限度的提高读写速率)。所以当请求到来时,将直接使用blk_init_queue()函数中注册的请求处理函数Virtual_blkdev_do_request()函数,对请求进行实际的操作。这里的操作就是将数据赋值的Virtual_blkdev设备或者从Virtual_blkdev设备中读取数据。
  • 361. 18.4.6 驱动的测试为了了解Virtual_blkdev这个块设备的特性,需要对其进行各方面的测试,这些测试如下所述。 1.编译Virtual_blkdev.c文件 2.加载模块文件 3.lsmod查看模块 4.创建块设备文件 5.在该设备上创建ext2文件系统 6.挂载文件系统 7.测试文件系统 8.卸载和移除设备模块
  • 362. 18.5 I/O调度器Linux内核中,I/O调度器涉及到很多复杂的数据结构,而结构之间的关系又非常复杂。要精通这些知识,远非一章一节知识所能够达到。但本节力图给读者一个清晰的概念,随着内核的升级,这些概念可能有所细微的变化,但是其主要的原理是基本不会变化的。在详细讲解I/O调度器之前,需要知道数据是怎样从内存到达磁盘的。
  • 363. 18.5.1 数据从内存到磁盘的过程内存是一个线性的结构,Linux系统将内存分为页。一页最大可以是64K,但是目前主流的系统页的大小都是4K。现在假设数据存储在内存的相邻几页中,希望将这些数据写到磁盘上。那么每一页的数据会被先封装为一个段,用bio_vec表示。多个页会被封装成多个段,这些段被组成以一个bio_vec为元素的数组,这个数组用bio_io_vec表示。
  • 364. 18.5.2 块I/O请求(bio)数据从内存到磁盘或者从磁盘到内存的过程,叫做I/O操作。内核使用一个核心数据结构bio来描述I/O操作。 1.bio结构体 bio结构体包含一个块设备完成一次I/O操作所需要的一切信息。 2.bio_vec结构体 bio中的段用bio_vec结构体来表示。 3.bio结构体的相关宏 为了程序的可移植性,在写驱动程序时,不应该直接的操作bio结构和bi_io_vec数组,而应该使用内核开发者提供的一系列宏。由于在驱动中会使用这些宏,这里对其主要的宏进行介绍。
  • 365. 18.5.3 请求结构(request)几个连续的页面会组成一个bio结构,几个相邻的bio结构就会组成一个请求结构request。这样当磁盘在接收一个与request对应的命令,就不需要大幅度的移动磁头,这样就节省了I/O操作的时间。
  • 366. 18.5.4 请求队列(request_queue)每个块设备驱动程序都维护着自己的请求队列request_queue,其包含设备将要处理的请求链表。请求队列主要用来连接对同一个块设备的多个request请求结构。同时请求队列中的一些字段还保存了块设备所支持的请求类型信息、请求的个数、段的大小、硬件扇区数等与设备相关的信息。总之,内核负责对请求队列的正确配置,使请求队列不会给块设备发送一个不能处理的请求。
  • 367. 18.5.5 请求队列、请求结构、bio等之间的关系可能读者对请求队列request_queue、请求结构request、bio、bio_vec、gendisk等结构的关系还并不清楚,除了建议读者查阅内核源码外,认真查看图也是不错的方法。
  • 368. 18.5.6 四种调度算法对于像磁盘这样的块设备来说,是不能随机访问数据的。在访问实际的扇区数据以前,磁盘控制器必须花费很多时间来寻找扇区的位置,如果两个请求写操作在磁盘中的位置相离很远,那么写操作的大部分时间将花在寻找扇区上。所以内核需要提供一些调度方法,来使物理位置相邻的请求尽可能先后执行,那么就可以减少寻找扇区的时间,这种调度就叫做I/O调度。
  • 369. 18.6 自定义I/O调度器本节接着上一节简介I/O调度器,并且仍然使用Virtual_blkdev设备,只是对其进行了一些简单的改进,使其效率更高。
  • 370. 18.6.1 Virtual_blkdev块设备的缺陷Virtual_blkdev块设备的数据都是存储在内存中的,对内存的访问可以随机进行,不需要对数据进行I/O调度。18.4节中的Virtual_blkdev块设备使用了默认的I/O调度器。实际上,对于Virtual_blkdev来说,一个好的I/O调度器丝毫起不了一点作用,反而会浪费不少的CPU时间和内存。 出现这个问题的原因是因为I/O调度器的原理所致。I/O调度器试图合并一系列的I/O请求,将相邻的请求合并,从而减少寻道时间。对于内存设备来说,这根本没有必要,因为内存设备根本不需要所谓的寻道时间,它读取各个位置的块的时间几乎相等。本节将通过自定义I/O调度器的方法,将其屏蔽掉。
  • 371. 18.6.2 指定noop调度器linux内核中包含4个I/O调度器:Anticipatory、CFQ、Deadline和Noop。2.6.18之前的linux默认使用anticipatory,而之后的默认使用cfq。关于这4个调度器的原理已经在上一节做过介绍,这里不重复讲述。这里主要用到的是Noop调度器。Noop调度器是一个基本上不错任何事情的空调度器,它直接将I/O请求传递给通用块层,告诉通用块层已经对请求做了相应的调度处理。
  • 372. 18.6.3 Virtual_blkdev的改进实例18.4节的Virtual_blkdev使用了默认的调度算法,但是并不符合内存设备的要求,这里对Virtual_blkdev_init()函数进行了简单的修改,使用noop调度算法取代了默认的调度算法。
  • 373. 18.6.4 编译和测试本节对Virtual_blkdev的修改非常少,但是已经使Virtual_blkdev的效率提高了不少。使用和18.4节一样的编译方法编译新的Virtual_blkdev模块,make命令如下所示。
  • 374. 18.7 脱离I/O调度器为了使读者详细的了解内核是怎么对数据进行读写的,这里对通用块层的函数调用关系进行了仔细分析。本节试图拜托繁琐的I/O调度器,对数据读写的本质进行分析,通过这种本质的学习读者将对数据读写的整个流程有一个深刻的理解。首先,将从请求队列中的bio处理函数开始。
  • 375. 18.7.1 请求队列中的bio处理函数尽管上一节的noop调度器已经相当简单。它除了告诉内核一个bio已经调度完成,正在等待处理之外,几乎什么都不做。许多程序员错误的以为noop调度器的效率很高,是的,它确实比其他三种调度器效率要高,但是有比noop调度器效率更高的方法,那就是不用I/O调度器。
  • 376. 18.7.2 通用块层函数调用关系有了上面关于请求队列的基础知识后,将分析一个块设备的读写过程。 1.通用块层函数调用关系 2.使用I/O调度器和不使用I/O调度器的分析 3.使用I/O调度器和不使用I/O调度器的效率分析
  • 377. 18.7.3 对Virtual_blkdev块设备的改进对Virtual_blkdev块设备的改进,首先需要修改Virtual_blkdev_init()函数,使其使用自定义的制造请求函数,修改后的代码如下所示: 1.Virtual_blkdev_init()函数的修改 2.请求制造函数Virtual_blkdev_make_request()
  • 378. 18.7.4 编译和测试使用make命令对新的Virtual_blkdev块设备进行编译,命令如下。
  • 379. 18.8 块设备的物理结构上几节介绍的块设备,其基本功能还不完善。本节将块设备的物理结构进行完善,首先介绍分区。
  • 380. 18.8.1 为Virtual_blkdev块设备添加分区对于实际的物理磁盘,一般都有多个分区,本节将对Virtual_blkdev设备进行分区,分区的概念是: 1.分区 分区是物理磁盘的一部分,将物理磁盘分为几个单独的单元,每一个单元就是一个分区。每一个分区可以用来存放文件和数据。 2.alloc_disk()函数增加分区 比起I/O调度来,对磁盘进行分区则非常容易,因为内核做了大部分的工作。这些工作大都由fs/partitions目录中的文件来完成。
  • 381. 18.8.2 对新的Virtual_blkdev代码的分析只要对18.7节中的代码做一点简单的修改,就能够使设备支持分区。首先在文件开始添加一个分区数目的宏,代码如下所示。
  • 382. 18.8.3 编译和测试在使用Virtual_blkdev设备前,需要先编译和调试代码,这些步骤如下所示: 1.编译代码 2.加载模块 3.分区 4.测试分区数
  • 383. 18.8.4 分区数的计算一个磁盘的最大分区数目由两方面决定,第一是alloc_gendisk()函数中指定的最大分区数,第二是磁盘的物理磁道数。在上述的Virtual_blkdev块设备的代码中,并没有为磁盘指定磁道数,这样情况下,Linux内核只能猜测磁盘的磁道数。
  • 384. 18.8.5 设置Virtual_blkdev的结构Virtual_blkdev是一个内存设备,对于内存设备来说,其物理结构并不十分重要。驱动开发人员可以根据需要对其进行设置,这些设置包括磁盘盘面数、扇区数、磁道数等。如果对于实际的物理设备,那么就不能对其物理结构顺便设置,因为这些物理结构在硬件出厂时,就已经固定了。
  • 385. 18.8.6 编译和测试在使用Virtual_blkdev设备前,需要先编译和调试代码,这些步骤如下所示: 1.编译代码 2.加载模块 3.分区 4.查看分区 5.创建文件系统 6.挂载文件系统
  • 386. 18.9 小结块设备的操作与字符设备非常不同,在本章中介绍了大量的与块设备相关的数据结构,例如request_queue、request、bio等。在本章中反复的讲到请求,请求是完成块设备读写操作的基本单位。 从块设备驱动整体代码构架来看,请求队列是必须的。根据块设备的不同物理结构,请求队列可以使用系统提供的“制造请求”函数,也可能使用自定义的“请求制造”函数。自定义的“请求制造”函数有更高的效率,当缺少通用性,只能针对特定的块设备。 如果读者从本章开始一直读来,那么会发现Virtual_blkdev块设备最开始有很多缺陷,随着讲述的深入,这种缺陷慢慢减少,越来越像实际的物理块设备了。在使用fdisk对块设备分区时,使用了block_device_operatioans结构体及相应的getgeo()函数,使块设备支持更多的分区。 对于块设备,本章只讲述这些,更多的信息读者需要自寻查找相关信息或者阅读源代码,通过学习,相信读者能够取得快速的进步。
  • 387. 第19章 USB设备驱动程序USB设备是计算机中一种非常常见的设备。日常生活中,常见的U盘,就是其中之一。USB设备只使用4条线进行连接,数据在线路中的传输规范已经从1.0升级到3.0,OTG规范也在完善之中。从长远来看,USB设备将称为计算机上主流的可插拔设备,越来越多的外设会使用USB规范来设计。从常见的外置光驱、移动硬盘、鼠标、键盘、手写笔,到外置网卡、蓝牙、手机数据接口、数码相机等,可见USB设备的使用会多么的广泛,不久的将来,甚至可以想象两台电路之间可以直接通过USB线进行数据传输,其速度可以达到480Mbit/s。随着USB设备在日常生活的广泛应用,学习USB设备驱动的价值也越来越大,本章将对编写USB设备驱动进行详细的阐述。
  • 388. 19.1 USB概述USB作为一种重要的通讯规范,目前应用越来越广泛。USB协议中,除了定义了通讯物理层和电气层的标准外,还定义了一套比较完整的软件协议栈。这样就使大多数符合协议的USB设备能够很容易的工作在各种平台上。基本上,各个平台上的USB设备驱动的逻辑都很相似。由于USB协议是一套规范的协议,所以编写各种USB设备的驱动程序也非常相似,本节对USB协议的相关内容做一个简要的介绍。
  • 389. 19.1.1 USB概念USB是一个外部总线标准,用于规范电脑与外部设备的连接和通讯。USB接口支持设备的即插即用和热插拔功能。USB接口可用于连接多达127种外设,如鼠标、调制解调器和键盘等。USB是在1994年底由英特尔、康柏、IBM、Microsoft等多家公司联合提出的,自1996年推出后,已成功替代串口和并口,并成为当今个人电脑和大量智能设备的必配的接口之一。从1994年11月11日发表了USB V0.7版本以后,USB版本经历了多年的发展,到现在已经发展为3.0版本。
  • 390. 19.1.2 USB的特点USB设备应用非常广泛,例如USB键盘、USB鼠标、USB光驱、U盘等,并且许多手持设备上也提供了USB接口,方面与电脑或其他设备传递数据。USB设备之所以会被大量应用,主要具有以下优点,这些优点在编程中是需要注意的。 1、可以热插拔。 2、携带方便。 3、标准统一。 4、可以连接多个设备。
  • 391. 19.1.2 USB总线拓扑结构USB设备的连接如图所示,对于每个PC来说,都有一个或者多个称为主机(Host)控制器的设备,该主机控制器和一个根集线器(Hub)作为一个整体。这个根Hub下可以接多级的Hub,每个子Hub又可以接子Hub。每个USB设备作为一个节点接在不同级别的Hub上。
  • 392. 19.1.3 USB驱动总体架构在Linux系统中,USB驱动由USB主机控制器驱动和USB设备驱动组成。USB主机控制器驱动,主要用来驱动芯片上的主机控制器硬件。USB设备驱动是具体的USB设备驱动,例如USB鼠标、USB摄像头等。如图是USB驱动的总体架构。
  • 393. 19.2 USB设备驱动模型19.2.1 USB驱动初探 Linux操作系统提供了大量的缺省驱动程序。一般来说,这些驱动程序适用于大多数硬件,但也有许多特殊功能的硬件不能在操作系统中找到相应的驱动程序。这时,驱动开发人员一般在内核中找到一份相似的驱动代码,再根据实际的硬件情况进行修改。所以通过什么样的方法找到相似的驱动程序非常重要。
  • 394. 19.2.2 USB设备驱动模型理解USB驱动程序,首先需要理解什么是USB设备驱动模型。Linux的设备驱动模型在前面的章节中已经讲述过,USB设备驱动模型是Linux设备驱动模型的扩展,这里主要介绍USB设备驱动的模型。 1.总线、设备和驱动 2.设备与驱动的绑定
  • 395. 19.2.3 usb驱动结构usb_driver在USB设备驱动模型中,usb设备驱动使用usb_driver结构体来表示。该结构体中包含了与具体设备相关的核心函数,对于不同的USB设备,驱动开发人员需要实现不同功能的函数,USB核心通过在框架中调用这些自定义的函数来完成相应的功能。下面对usb_driver结构体进行简要的介绍。 1.usb_driver结构体 2.驱动支持的设备列表结构体usb_device_id 3.初始化usb_device_id结构的宏 4.USB驱动注册函数usb_register()
  • 396. 19.3 USB设备驱动程序USB驱动程序相对比较复杂,最为简单的是加载和卸载函数。在加载函数中完成了USB设备的大部分初始化工作,同时涉及到很多重要的数据结构。下面对这些概念进行详细的解释。
  • 397. 19.3.1 USB设备驱动加载和卸载函数USB设备驱动程序对应一个usb_driver结构体,这个结构体相当于Linux设备驱动模型中的driver结构体。
  • 398. 19.3.2 USB协议中的设备USB核心调用probe()函数并传递进struct usb_interface和struct usb_device_id*类型的参数,要理解struct usb_interface参数的意义,就必须了解什么是USB设备(usb_device)。要了解什么是usb_device,就必须了解什么是USB协议。
  • 399. 19.3.4 端点的传输方式上文中已经对设备、接口、配置和端点进行了说明。但是对于USB驱动来说,端点的说明还不足够。USB通信的最基本的形式就是通过端点来进行的。这里以U盘为例,其至少有一个控制端点和两个传输端点。那么端点到底是用来干什么的呢?简单的说,端点就是用来传输数据的。
  • 400. 19.3.5 设置一个手机可能有多个配置,例如震动还是铃声可以算两种配置。当配置确定后,还可以调节其大小,如铃声的大小,这可以算是一种设置。通常用大小关系来表示USB协议中的概念更好理解,设备大于配置,配置大于接口,接口大于设置。也就是说,一个设备可以有多个配置,一个配置可以有一个或者多个接口,一个接口也可以有一个或者多个设置。
  • 401. 19.3.6 探测函数storage_probe() 对于本章简述的usb-storage模块,usb_stor_init()函数是它的开始,已经在上文中讲过了。但是对于U盘驱动程序,真正驱动U盘正常工作的是storage_probe()函数。storage_probe()函数是在usb_storage_driver中指定的。如果读者还不知道这个函数的由来,那么一定是你跳过了前面的章节,而忽略了一些重要内容了。 USB核心为设备寻找合适的驱动程序并不是一件简单的事情。当设备插入时,USB核心会为每一个设备调用总线上所有驱动的probe()函数。在probe()函数中检查驱动是否真的和设备相适应。在probe()函数中应该尽量去了解设备足够多的信息,这样才能知道驱动是否能够支持这种设备。
  • 402. 19.4 获得USB设备信息在主机与USB设备通信之前,需要获得USB设备的信息。这个过程中,涉及一次USB通信,理解一次USB通信,就能够理解整个USB通信。下面对这个过程介绍进行详细的介绍。
  • 403. 19.4.1 设备关联函数associate_dev()在探测函数storage_probe()的第25行有一个关联设备的函数associate_dev(),这个函数主要使用usb_interface结构体来初始化us指针。
  • 404. 19.4.2 获得设备信息函数get_device_info() 在整个usb-storage 模块的代码中,其中最为重要的函数是usb_stor_control_thread()。它创建一个线程,并控制主机与U盘的信息交互。这个函数在usb_stor_acquire_resources()函数中调用。这个函数在storage_probe()函数的第40行被调用。在调用这个函数之前,有4个函数摆在我们面前,它们是get_device_info(),get_transport(),get_protocol(),get_pipes()。这4个函数是让驱动去认识设备,例如了解设备的一些信息,它的传输方式,传输管道等。这些函数只是做了一些准备工作,并不没有完成主机控制器和设备的交互功能。这几个函数只是对后面的数据传输做好一些铺垫。
  • 405. 19.4.3 得到传输协议get_transport()函数在探测函数storage_probe()的第31行有一个get_transport()函数,这个函数主要获得USB设备支持的通信协议,并设置USB驱动的传输类型。
  • 406. 19.4.4 获得协议信息函数get_protocol()get_protocol()函数用来设置协议传输函数,根据不同的协议,使用不同的传输函数。get_protocol()函数根据us->subclass来判断,应该给us->protocol_name和us->proto_handler赋什么值。对于U 盘来说,USB协议中规定us->subclass为US_SC_SCSI,所以这里的switch()中的两条语句,一个是令us的protocol_name为"Transparent SCSI",另一个是给us的proto_handler赋值为usb_stor_transparent_scsi_command(),这里暂不对这个函数进行说明,当用到时在详细阐述。
  • 407. 19.4.5 获得管道信息函数get_pipes()get_pipes()函数用来获得传输管道。这个函数用涉及到接口、端点、管道等概念。简单的说,接口代表设备的一种功能。端点是USB通信的最基本形式。主机只能通过端点与USB设备进行通信,也就是只能通过端点传输数据。
  • 408. 19.5 资源的初始化19.5.1 storage_probe()函数调用过程 对于storage_probe()函数,前面用来很长的篇幅来分析,因为它是USB设备最主要的函数之一。首先,分配了一个重要的struct us_data结构体。如图19.7所示,在storage_probe()函数中,主要调用了五个重要的函数,分别是assocaite_dev(),get_device_info(),get_transport(),get_protocol()和get_pipes()。这些函数的唯一目的就是为us结构体赋初值,这样us结构体就可以带上这些重要的数据,来回于USB核心之间,完成特定的任务。当为us赋上正确的初始值后,会调用usb_stor_acquire_resources()函数,得到设备需要的动态资源。
  • 409. 19.5.2 资源获取函数usb_stor_acquire_resources()在storage_probe()函数中,最为重要的一个函数就是usb_stor_acquire_resources()了。该函数的主要功能是初始化设备,并创建数据传输的控制线程。在这个函数中,调用了kthread_run()函数,用来创建一个内核线程。在Linux驱动中,有时候找不到驱动执行的流程,这时候如果发现了kthread_run()函数,那么就表示驱动另外创建了一个线程,主要的逻辑就有可能是在这个新线程里执行了。
  • 410. 19.5.3 USB请求块(urb)USB请求块(USB request block,urb)是USB主机控制器和设备通讯的主要数据结构,主机与设备之间通过urb来进行数据传输。在urb中,包含了执行usb传输所需要的所有信息。当主机控制器需要与设备交互时,只需填充一个urb结构,然后将其提交给USB核心,由USB核心负责对其进行处理。在Linux中,USB请求块有struct urb结构体来描述。
  • 411. 19.6 控制子线程控制子线程用来完成数据的接收和发送。这个线程会一直运行,直到驱动程序退出。本节将对控制子线程进行详细的介绍。
  • 412. 19.6.1 控制线程控制线程usb_stor_control_thread()是一个守护线程。在Linux中,与Windows不同,是不区分线程和进程的概念的,因为线程也是用进程来实现的。usb_stor_control_thread()函数是整个USB模块中最有意思的函数,这个函数中执行一个for(;;),这是一个死循环,也就是这个函数作为一些线程用不停息运行。
  • 413. 19.6.2 扫描线程usb_stor_scan_thread()usb_stor_scan_thread()函数是一个线程函数,这个线程的生命周期非常短,完成的功能也比较简单。这个函数的功能命是让你能通过cat /proc/scsi/scsi看到U盘设备。同样在/dev目录下,也可以看到U盘设备。
  • 414. 19.6.3 获得LUN函数usb_stor_Bulk_max_lun()usb_stor_scan_thread()函数的第18行调用了usb_stor_Bulk_max_lun()函数。这个函数非常重要,是USB驱动第一次向设备获取信息的函数。只有了解了这个函数,那么就大概了解了一次USB设备通信的过程,图是usb_stor_Bulk_max_lun()函数的调用过程,本节将对这些函数进行详细的介绍。
  • 415. 19.7 小结USB驱动程序比较复杂,需要仔细的分析。从整体上看,USB驱动程序分为USB主机驱动程序和USB设备驱动程序。主机驱动程序可以硬件实现也可以软件模拟,这部分符合相应的USB规范,所以大部分代码都是沿用通用的代码。USB程序中一个重要的概念是urb请求块,一个完整的urb请求块的生命周期是创建、初始化、提交和传输完成。本章对这些过程有详细的介绍,如果不理解,读者可以回过来复习。