引 言
编写 Linux 设备驱动程序无疑是一项复杂的工作。本文将集中介绍非标准硬件的设备驱动程序编写,探讨硬件应用编程接口,并借用 Cirrus Logic EP9312 片上系统嵌入式平台添加设备驱动程序这一案例来进行分析。
如果有些编程内容未能在本文中涉及,那么读者亦可以查阅相似的设备驱动程序编码,以做参考。还有一种方法,就是检索历史档案或者向 Linux 内核问讯中心去函问讯。
Linux 概述
Linux 是 UNIX 操作系统的翻版,1991 年由 Linus Torvalds 最先开发出来,并通过开放源代码开发模式不断得到开放源代码组织的改进。任何使用 Linux 的个人和团体都无需支付任何版权费用。
只有内核还不够,通常Linux 与一些在内核上运行的视窗环境、视窗管理器和应用捆绑在一起。然而,由于具备了嵌入式平台,视窗环境并非必不可少。与微软的视窗操作系统不同的是,Linux 并不需要一套固定的、必须采用的应用软件或实用程序,因此能够十分符合嵌入式市场终端解决方案的客制化要求。
操作系统最基本的组成部分包括 1个资源管理器、1个调度程序、1个介于硬件和应用软件之间的接口、1个网络管理器和 1 个文档系统管理器。Linux操作系统也包括这些组成部分,当然还有其他部分。本文主要阐述介于硬件和应用软件之间的接口--设备驱动程序。
图1解释了用户应用软件、操作系统内核和硬件平台之间的区别。
设备驱动程序类型
设备驱动程序可分为2大类:硬件设备驱动程序和软件设备驱动程序。硬件设备驱动程序和物理硬件设备相连接,如UART设备或IDE设备,而软件设备驱动程序则作为低级数据结构间的接口,或硬件设备驱动程序和高级数据结构间的接口。图形控制台驱动程序就是一个软件设备驱动程序。其中,1个LCD控制器驱动程序装载并管理该显示器,同时图形控制台对即将显示的字符进行着色,并获取从键盘输入的信息。软件设备驱动程序的另一个例子是文档系统执行--文档系统驱动程序采用1个硬盘驱动程序存储数据,而该硬盘驱动程序直接与物理硬盘相连接。
设备驱动程序的分类
Linux 设备驱动程序有几类:字符、区块、网络和其他。通常,驱动程序根据设备的访问方式分类。然而,也有些设备无法按照此类方式得到区分,因此被归到"其他类型"。字符设备包括那些使数据成为数据流的设备,可通过1个文档系统的特殊文件获得(文档系统的特殊文件将在后文中加以讨论)。鉴于字符设备的特性,该设备只能根据顺序访问数据,即无法往前或往后搜索数据。串行端口和音频设备都是这种类型。图2是Cirrus Logic的EP9312 片上系统结构图,其中Linux字符设备以绿色标出。
区块设备能够照管1个文档系统。该类设备和字符设备一样,也是通过文档系统特殊文件访问。但是,区块设备与文档设备的差异在于其可被随机访问。这意味着,应用软件可查找在该设备中的随机位置。硬盘驱动器和CD驱动器都是区块设备,它们内部的文件指针可以指向设备内部的任何位置,惟一的限制来自设备本身。当区块设备通过文档系统特殊文件访问时,该应用接口即同字符设备一样,只是与内核的接口有所差别而已。图2中的红色部分即为Cirrus Logic EP9312 片上系统结构中Linux区块设备。
网络接口设备既可以是硬件设备,也可以是软件设备。硬件设备如以太网卡,软件设备如低端网络协议堆栈(本文将此类接口视为软件设备)。中间件和协议堆栈有时会被看作是软件设备。网络接口设备是信息包数据的通信设备,一般拥有惟一名称,并且无法通过文档系统特殊文件访问。相反,它们只对内核网络堆栈开放。通常,用户级应用软件可访问内核网络堆栈,而不能访问网络接口设备。图2中的蓝色部分即为Cirrus Logic EP9312 片上系统结构中的Linux网络接口设备。
其他的设备驱动程序还包括数据总线驱动程序(USB, I2C, AMBA等)、 /proc 接口和视频驱动程序。这些类型的设备无法被归入以上的3个类型中,但仍然是与Linux内核接口的设备驱动程序。
文档系统特殊文件
文档系统特殊文件提供了从文档系统访问硬件设备的可行性。这些访问点使用mknod 命令在文档系统/dev 目录中生成。命令如下:
mknod
其中, 是给予硬件设备的名称,如 /dev/hda1 是给予硬盘驱动器的通用名称。 是设备驱动程序的类型--字符(char)、区块等。 代表设备类别和与之相配的驱动程序。 表示设备类别中的一个实例,并仅对设备驱动程序适用。例如,某个系统中同时采用2个硬盘驱动器,它们都具有同样的主要编号,使用同样的设备驱动软件,但是该设备驱动程序软件却会在内部根据次要编号区分这2个硬盘驱动器。
值得注意的是,并非所有的设备都执行特殊文件接口。如同本文前面已经提及的,网络设备驱动程序就不采用这种接口访问设备。
这种情况下,在设备文档系统里,就会使用 devfs来获得文档设备特殊文件。devfs 目前广受欢迎,但仍然还不是内核的默认功能。如果采用devfs 文档系统,那么就无需mknod 来生成特殊文件了。相反,设备驱动程序软件会使用直接的devfs 文档系统接口在空闲时刻或者设备刚被初始化时生成特殊文件。
编程实例概述
为便于示范非标准嵌入式平台的Linux设备驱动程序,本文将说明EP9312的设备驱动程序实现情况。其中,EP9312 IDE设备驱动程序是区块设备, EP9312触摸屏为字符设备,代码中的高级API/硬件接口、初始化序列和应用软件编码均将予以说明。
1、字符设备驱动程序实例:触摸屏设备驱动程序
EP9312触摸屏控制器因其数据只能按顺序获取而被列为Linux字符设备。触摸屏字符驱动程序的执行是相当简单的--设备向操作系统注册,并通过文档系统特殊文件进行访问。有关硬件代码包含在文档操作表的一套函数中。我们将从内核初始化开始,解释该驱动程序的执行情况。
初始化EP9312触摸屏的函数是:
int __init ep93xx_ts_init(void)
该函数处理2项工作:当设备被中断驱动时获取设备IRQ和在操作系统内注册触摸屏设备。
函数request_irq() 在请求IRQ时被调用,并注册中断处理器函数以在设备发生系统中断时处理所需的任务。
而函数 register_chrdev() 则是用来注册字符设备的。该函数表现形式如下:
int register_chrdev(unsigned int major,
const char * name,
struct file_operations *fops)
该函数安装了字符设备硬件的内核接口。主要编号用于把驱动程序映射到 /dev 目录中的文档系统特殊文件。设备被赋予一个名称,以便内核识辨。此外,file_operations 结构具有对函数指针表的一个指针,该表指向硬件的相应函数。
然而,仍然有一些字符设备不符合预先确定的字符设备范畴。这些设备就用主编号10一起归于"其他类型",注册设备用以下函数:
int misc_register(struct miscdevice * misc)
misc_register()用主编号10调用 register_chrdev(),设备名称和函数表指针通过miscdevice数据结构获得。同样,miscdevice 数据结构还保存设备驱动程序所使用的次要号码。
以下是在设备驱动程序代码内注册 EP9312 触摸屏采用的函数调用:
misc_register(&ep93xx_ts_miscdev)
数据结构 ep93xx_ts_miscdev 是对触摸屏硬件的内核访问,定义如下:
static struct miscdevice ep93xx_ts_miscdev =
{
EP93XX_TS_MINOR, /* device minor number */
"ep93xx_ts", /* name of the device */
&ep93xx_ts_fops /* device file operations */
/* table pointer */
}
其他类型设备驱动程序采用次要号码区分设备。
硬件接口函数在设备驱动器内即被静态定义,当设备注册时,由内核通过传递给操作系统的文档操作函数指针获得。指针列表定义如下:
static struct file_operations ep93xx_ts_fops =
{
owner: THIS_MODULE,
read: ep93xx_ts_read,
write: ep93xx_ts_write,
poll: ep93xx_ts_poll,
open: ep93xx_ts_open,
release: ep93xx_ts_release,
fasync: ep93xx_ts_fasync,
}
初始化触摸屏设备后,即需创建文档系统特殊文件,以便协助应用程序代码访问设备。创建 EP9312 触摸屏特殊文件的 mknod 命令如下:
mknod /dev/misc/ep93xx_ts c 10 240
该步骤即可在根目录系统下的初始化文档初始化 Linux 时得到执行,也可在命令提示里实现手动操作。
以下是用户级应用代码的一个实例,通过文档系统特殊文件访问触摸屏设备:
#define TS_DEV "/dev/misc/ep93xx_ts"
int read_ts()
{
int fd, nbytes;
short data[3];
fd = open("/dev/misc/ep93xx_ts", O_NONBLOCK);
if ( fd < 0 )
{
printf("Unable to open touch screen device %s!/n", TS_DEV);
exit(1);
}
nbytes = read(pd_fd, data, sizeof(data));
close(fd);
if (nbytes != sizeof(data))
return 0;
return 1;
}
2、区块设备驱动器实例:IDE 设备驱动
与 EP9312 IDE 控制器接口的 IDE 设备被划分为 Linux 区块设备,其中包括硬盘驱动器和 CDROM 驱动器。这些设备上的数据可以随机读取是将其划分为区块设备的主要原因。
与简单的触摸屏接口执行相比,IDE 区块设备驱动器是相当复杂的。该设备驱动器被分成几部分,包括 IDE 区块设备内核接口、为 IDE 控制器设置的内部驱动器硬件接口(附加的独立 IDE 设备多达 4 个)、针对硬盘、软盘等 IDE 设备类型的模块,以及结构特别接口。通过允许硬件或结构特殊函数的调用,IDE 设备类型数据结构内的函数指针可以实现非标准结构的灵活性和可延展性。图3为IDE区块设备驱动程序结构示意图。下面从设备驱动程序初始化开始说明该驱动程序。
高级IDE驱动程序在Linux内核初始化或模块安装(如果驱动程序被设置为模块)时得到初始化。本文不详述高级IDE 驱动程序初始化或安装细节,而是着重讨论为初始化定制并与硬件接口的驱动程序各片断。在高级IDE驱动程序初始化过程中,以下函数被用于设置IDE控制器:static __inline__ void ide_init_default_hwifs(void)
该函数在文件中被定义为:include/asm/mach/ide.h,为非标准IDE控制器配置硬件接口数据结构,注册高级IDE驱动程序EP9312 IDE接口,并为接口设置IRQ。
在结构特殊初始化代码内完成的IRQ设置仅仅在硬件接口数据结构内设置IDE接口所需的平台IRQ号码。调用request_irq() 由高级IDE驱动程序负责。
IDE硬件接口数据结构通过调用以下函数得到配置,并同时在include/asm/mach/ide.h内得到定义:
static __inline__ void
ide_init_hwif_ports(hw_regs_t *hw,
int data_port,
int ctrl_port,
int *irq)
该函数通过设置硬件接口数据结构内的命令和控制注册地址配置了非标准EP9312 IDE 接口,并设置和实现EP9312上的接口。
在ide_init_default_hwifs(void) 函数设置IDE控制器并由高级IDE驱动程序注册硬件接口后,结构特殊接口通过以下函数调用得到进一步初始化:
void ep93xx_ide_init(unsigned int * pointer)
该函数在文档驱动器/ide/ide-ep93xx.c 内被定义,并同时执行几个任务--把结构特殊函数映射到硬件接口数据结构内的函数指针函数,如果平台设有DMA则设置DMA接口。
IDE硬件接口数据结构的结构特殊函数指针如下所示:
typedef struct hwif_s
{
…
ide_rw_proc_t *rwproc;
ide_ideproc_t *ideproc;
ide_dmaproc_t *dmaproc; …
} ide_hwif_t;
ideproc 处理PIO模式转换,并被映射到结构特殊函数 ep93xx_ideproc()。rwproc 和dmaproc 都处理DMA模式转换。rwproc 向ep93xx_rwproc()映射,dmaproc向ep93xx_dmaproc()映射。高级IDE驱动程序检测这些指针是否无效。如果确为无效,则放弃结构特殊函数而采用默认函数。ideproc()和dmaproc()均系基于IOCTL的函数,可执行一系列高级IDE驱动程序定义的ioctls命令。rwproc()函数为特殊转换速度和方向设置IDE控制器。这些EP9312结构特殊函数都在文件驱动程序/ide/ide-ep93xx.c内得到定义。函数原型示意如下:
static void ep93xx_ideproc(ide_ide_action_t action, ide_drive_t * drive, void * buffer, unsigned int count)
static void ep93xx_dmaproc(ide_dma_action_t action, ide_drive_t *drive)
static void ep93xx_rwproc(ide_drive_t * drive, ide_dma_action_t action)
此外,一部分结构特殊执行命令也是几个IDE普通宏命令的再定义。它们是直接读写IDE设备的宏命令。文件 /include/asm/mach/ide.h 下的宏映射到EP9312 定义。
#define OUT_BYTE(b, p) ep93xx_ide_outb((b), (p))
#define OUT_WORD(w, p) ep93xx_ide_outw((w), (p))
#define IN_BYTE(p) ep93xx_ide_inb((p))
#define IN_WORD(p) ep93xx_ide_inw((p))
硬件接口(EP9312 IDE控制器接口)被初始化并与高级IDE驱动程序一起注册后,高级IDE驱动程序通过探测相连的IDE设备硬件接口继续初始化。如果设备被探测到,则与操作系统一起注册。设备与操作系统一起注册后,向能在设备上执行的操作表上映射。这样,操作系统也获得了设备的额外信息,并需要对设备进行资源管理。这些额外信息包括大小和分区数量等。以下是注册IDE硬盘的函数调用:
register_disk(struct gendisk *gd, int drive,
unsigned minors,
struct block_device_operations *ops,
long size)
高级IDE驱动程序用探测设备时获得的的函数参数值调用这个函数。第一个参数是gd,它是描述盘片布局的数据结构。第二个参数--drive,是设备编号。对于EP9312而言,设备编号或为0,或为1,因为硬件只支持的两台设备。第三个参数--minors,是设备被探测时发现的盘片分区。第四个参数--block_device_operations,是函数指针列表,系IDE驱动程序硬盘执行所定义。被映射到该列表中的函数采用结构特殊函数执行不同任务。最后一个参数--size,是指设备的扇区数,它同样也是从设备中直接获得。
设备指针列表包括以下区块设备操作:
· open - 设备和驱动程序实例初始化
· release - 关闭设备或清除驱动程序实例
· ioctl - 填补空白,是通过内核向设备驱动程序传递的一种信息的一种方式
· check media change - 处理支持可移动媒体的设备
· revalidate - 处理支持可移动媒体的设备(通常为设备指定)
区块设备的设备操作列表不包括任何输入输出操作。对于区块设备而言,request方法用于处理设备输入输出,并与等待的输入输出操作队列相关,因此进一步与字符设备有所区分。Request方法和队列均由高级IDE设备驱动器定义,与操作系统一起注册并与设备主要编号相连。
除了将设备和操作系统一起注册,高级IDE设备驱动程序还通过数据结构在本地管理该设备,数据结构包括映射到IDE设备特别函数的函数指针。下面是映射到针对IDE硬盘函数的该数据结构的一部分:
static ide_driver_t idedisk_driver = {
…
cleanup: idedisk_cleanup,
standby: do_idedisk_standby,
flushcache: do_idedisk_flushcache,
do_request: do_rw_disk,
end_request: NULL,
ioctl: NULL,
open: idedisk_open,
release: idedisk_release,
media_change: idedisk_media_change,
revalidate: idedisk_revalidate,
pre_reset: idedisk_pre_reset,
capacity: idedisk_capacity,
special: idedisk_special,
proc: idedisk_proc,
reinit: idedisk_reinit,
…
};
值得注意的是,一些函数指针直接向与操作系统一起注册的文件操作列表函数指针映射,而此时IDE设备驱动器内部使用其他函数指针。例如,高级设备驱动程序内部使用函数指针do_request 和 end_request处理要求方法输入输出。
这就涵盖了IDE设备驱动器的结构特殊API。下一步是创建文档系统特殊文件,从而帮助用户级应用进入该设备。使用以下命令生成IDE硬盘驱动特殊文件:
mknod /dev/hda1 b 3 1
正如在触摸屏特殊文件创建中谈及,可在系统初始化阶段安排自动执行该步骤,或者用户可以在系统启动运行显示操作提示时手工操作该命令。
用户级应用较少直接调用区块设备。一般而言,区块设备直接通过内核级文档系统执行接入。用户级应用通常获取具有操作系统实用程序的区块设备,以执行文档系统创建、安装访问文档系统的设备等文档系统操作。命令行工具涵盖分割、格式化、安装和验证区块设备。例如,以下是用mnknod命令创建的设备的一个安装设备命令:
mount -t ext3 -o rw /dev/hda1 /mnt/drive
-t ext3 指出设备由一个Extended 3文档系统配置;-o rw 则说明设备应该具备读写函数;/dev/hda1是被安装设备的文档系统特殊文件;/mnt/drive 则是用户获取设备所存文档系统内容的安装位置。
3、添加Linux内核的新设备驱动程序支持
Linux内核用以下三个命令建立:
·make menuconfig (config, xconfig, oldconfig, etc.)
·make dep
·make
首先,Linux内核针对目标运行环境进行配置。用户还可选择添加支持各种设备、支持各种文档系统和配置引导参数等。当一个新的设备驱动程序在Linux内核中得到执行时,必须增加对该新设备的配置支持,所以要先更新驱动程序目录中合适设备类型子目录下的Makefile。在Makefile 中,必须增加新选项建立设备驱动程序二进制文件,并且直接与Linux内核相连或创建一个模块。第二步需要更新驱动程序目录设备类型子目录下的Config. in。此新设备的配置选项必须加入Config.in。
小结
本文无意阐述Linux设备驱动程序的各个环节,因为包括Linux源代码在内的各种资源都已对此做出了解释。相反,本文旨在探索针对嵌入式非标准设备、用以执行设备驱动程序的硬件API。对于几个不同类型的设备驱动程序,本文以EP9312片上系统平台为例,详解了这些为硬件接口定制的API。了解如何设计并执行这些API是为新设备编写驱动程序的第一步。