嵌入式Linux设备驱动程序开发指南(原书第2版)
上QQ阅读APP看书,第一时间看更新

第4章 字符设备驱动

一般来说,操作系统被设计为对设备使用者或者用户应用隐藏底层硬件细节。但是,应用程序需要访问硬件外设捕获的数据的能力,以及通过向设备输出数据以驱动外设的能力。外设的寄存器只能由Linux内核访问,因此只有内核能够收集外设捕获的数据流。

Linux需要一种能够将数据从内核态传给用户态的机制。这种数据的传输通过设备节点来处理,这些设备节点也称作虚拟文件。设备节点存在于根文件系统中,尽管它们并不是真正的文件。当用户读取设备节点时,内核将底层驱动捕获的数据流拷贝到应用程序的内存空间。当用户写设备节点时,内核将应用程序提供的数据流拷贝到驱动程序的数据缓冲区,这些数据最终向底层硬件输出。这些虚拟文件可以被用户应用程序通过标准的系统调用方式打开、读取或者写入。

用户应用程序的请求最终被送往驱动核心,每个设备都有专门的驱动来处理这些请求。Linux支持三种类型的设备:字符设备块设备以及网络设备。尽管在概念上一致,每种设备在驱动上的差异主要体现在文件的打开、读取和写入行为上。字符设备是最常见的设备,这种设备的读写直接进行而无须经由缓冲区,比如键盘、显示器、打印机、串口等。块设备的读写以块大小为单位,一次读写整数倍的块大小,块大小一般为512或者1024字节。它们可以被随机读取,任何块都可以被读取,不管它们在设备的什么位置。一个典型的块设备的例子就是硬盘驱动器。网络设备则通过BSD套接字接口和网络子系统来访问。

在列出文件信息的第一列,字符设备通过字符c标识,块设备则用字符b表示。设备的访问权限、所有者以及组信息则针对每个设备分别列出。

从应用程序的角度看,一个字符设备本质上就是一个文件。进程只知道一个/dev文件路径。进程通过open()系统调用打开文件,通过read()write()来执行标准的文件操作。

为了实现上述目标,字符设备驱动必实现file_operations数据结构中描述的各种操作并注册它们。file_operations数据结构定义在include/linux/fs.h中。下面的file_operations数据结构定义中,只列出了针对字符设备驱动最常见的一些操作:

075-01

Linux文件系统层负责确保调用驱动相关的操作,这些操作在用户态应用程序执行相应的系统调用时被触发(在内核部分,驱动负责实现并注册这些回调操作)。

内核驱动通过copy_from_user()copy_to_user()这两个特定的函数与用户态应用程序交换数据,如图4-1所示。

076-01

图4-1 Linux文件系统

如果发生错误,read()write()方法都会返回负值。反之,返回值大于等于0则告诉调用者有多少个字节被成功传输。如果部分数据被正确传输,然后发生了错误,返回值则必须是已经成功传输的字节数。错误的上报则要等到下一次函数调用。当然,实现这样的规范要求你的驱动程序记住发生的错误,这样函数可以在将来返回错误状态。

read()的返回值则由调用该函数的应用程序负责解释:

1. 如果该值等于传递给read系统调用的字节个数参数,则请求传输的字节已经完成。这是最佳情况。

2. 如果该值是正数但是小于字节个数参数,那么仅有部分数据传输完成。发生这种情况有多种原因,取决于具体的设备。最常见的情况是由应用程序重试读取。如果你使用fread()函数读取,这个库函数会重新发送系统调用,直到请求的数据传输完成。如果返回值是0,则表示已经到达文件末尾(没有数据被读取)。

3. 负值则表示有错误发生。根据<linux/errno.h>,该值表示具体发生了何种错误。错误时返回的典型值包括-EINTR(系统调用被中断打断)和-EFAULT(非法地址)。

在Linux中,设备通过两个设备号来标识:一个设备号和一个设备号。这些设备号可以通过调用ls -l/dev查看。设备驱动将自己的主设备号注册到内核并负责管理从设备号。当访问一个设备文件时,主设备号决定了执行输入/输出操作时调用哪个设备驱动。访问设备时,内核使用主设备号来识别正确的设备驱动。从设备号的使用则取决于具体设备,由驱动负责。比如,i.MX7D有多个物理UART端口。同样的驱动被用于控制所有的UART设备。但是每个物理UART需要自己的设备节点,因此这些UART设备节点具有相同的主设备号,但从设备号各不相同。