Linux下PCI设备驱动开发详解 下载本文

一、设备驱动程序概述

自Linux在中国发展以来,得到了许多公司的青睐。在国内的玩家也越来越多了,但目前还是停留在玩的水平上,很少有玩家对Linux的系统 进行研究。因为它的开放,我们可以随时拿来“把玩”。这也是Linux一个无可比拟的优势,这样我们可以修改后再加入到里面。但很少有专门的书籍讲到 Linux驱动程序的开发,像上海这样的大城市也很少有讲Linux驱动开发的资料,唉,谁让这个是人家的东西呢,我们还是得跟着人家跑。

我现在讲的这些驱动开发的细节,并不特定哪个版本的内核,这只是大体的思路与步骤。因为大家都知道Linux 2.6.x 与Linux 2.4.x是有不少改动的。所以,具体的大家可以去参考Linux Device Driver 2.4 和Linux Device Driver 2.6这几本书。这是我们学习开发驱动必不可少的东西。好了,下面就开始学习吧。

根据设备的行为,我们可以把设备分为字符设备和块设备,还有网络设备。字符设备是以字节为单位进行顺序读写,数据缓冲系统对它们的访问不提 供缓存。而块设备则是允许随机访问与读写,每次读写的数据量都是数据块长度的整数倍,并且访问还会经过缓冲区缓存系统才能实现。与Unix版本不同的是: Linux的内核允许不是数据块长度整数倍的数据量被读取,用官方的语言就是:但这种不同只是纯粹学术方面的东西。 大多数设备驱动程序都要通过文件系统来进行访问的,但网络设备是不同的。 /dev子目录里都是关于设备的特殊文件,但看起来它们与普通的目录没有什么两样。 如下: $ ls -l /dev ...

brw-rw--- 1 root disk 22, 1 May 5 1998 hdc1 crw-rw--- 1 root daemon 6 0 May 5 1998 lp0

与普通文件有所不同是开头的“C” 和“B”, 即char 和 block的意思,即字符设备和块设备。再后面的“22,1” 和“6,0”即设备的主设备号和次设备号,主设备号表明它是哪一种设备,这与你在Windows里添加硬件时看到的那些是一个意思。次设备号表明是哪一个 具体的设备。内核对这个设备名称不怎么关心,它只关心它的类型和主设备号。 这里提一下如何创建这些特殊文件: mknod name type major minor ,但你必须是root用户。

下面讲一下用户空间与内核空间:

Linux运行在两种模式下,一种是内核模式,也称为超级用户模式;别一种是用户模式。 Intel的X86(X>3),把自己的执行模式命名为Ring(环)0、1、2、3, 第0环的优先级是最高的。在Linux里,第0环代表内核模式,第3环代表用户执行模式,其余两环是没有使用。所以,如果你有用户模式下访问硬件与I/O 是不可能的。 内核模块与内核进行链接,在使用的它们自己的向外提供的函数方面也是有限制的。一个做为内核模块编写出来的设备驱动程序的运行并不是普通意义上的运行,模 块中的符号是在它被加载到内核里去的时候得到解析的。 编写内核的也是注意原则的:

(1) 不要使用浮点运算。内核在切换处理器执行模式时不保存它的FP状态,如果你要使

用的话,你就要自己保存FP状态。 但通常也没有什么理由要用浮点数的。

(2) 在驱动程序里不要进行繁忙的等待;用户空间里的一个应用程序永远也不可能完全独占CPU;但内核里的一个用时1秒的循环看上去也会把系统挂起很长的时间,而且期间什么也不能做的;

(3) 要谦虚,不要自以为是,在内核里增加一条打印语句都会把程序搞坏。 功能取舍

原则就是:只要能在用户空间里编程实现的东西,就绝不要放到内核空间里! 代码有错时,用户空间的错误会输出内存映象,而内核空间里的错误则可能会完全挂起。 建立模块

gcc -D__KERNEL__ -D__SMP__ -DMODULE -DMODVERSIONS -I/usr/src/linux/include -Wall -O2 -o module.o module.c

__KERNEL__: 内核本身用的东西,没有多少好讲的 __SMP__: (Symmetric Multi Processor) 对称多处理器专用品; MODULE: 告诉系统编译成内核模块;

MODVERSIONS:检查内核与模块之间的不兼容性,必须包含这个#include 如下解释,O2是告诉gcc编译期间时行几遍优化, -I是Include路径,如:-I/usr/src/linux, 但如这样的话,#include 实际是指:/usr/src/linux/include/linux/module.h这个文件。 还有注意的就是:名字的空间,在做内核时开发时,一不要把全局性的内核名字空间弄乱。 在导出函数的名字之前加上驱动程序的名字是一个避免出现名称冲突现象的好方法,另一个好方法就是只导出将会被其它驱动程序用到的函数和变量。 把全局变量和函数声明为静态变量和静态函数也可以,但也会有些副作用。其它变量和函数的导出要明确地EXPORT_SYMBOL宏命令来进行,它会把它们 添加到内核的全局符号表里去。一般说来,只有在准备把驱动程序分为几个模块或者准备暴露驱动程序的内部细节以做它用的情况下才需要考虑这一问题。 但还是要把全局名字空间的“污染”降低到最小程度。有关赛马场命令的语法定义如下: EXPORT_SYMBOL(name) 导出代表变量或者函数的符号name;

EXPORT_SYMBOL_NOVERS(name) 导出代表变量或者函数的符号name,但不加上模块版本检查后缀

——即使定义了也不加 EXPORT_NO_SYMBOLS 不导出任何符号 这些是定义在#include文件中。

数据类型

Linux定义了一些标准类型,它们可以在各种平台上的尺寸长度都是一致的如: __u8, ...... __u64 字符到64位长度之间带正负号和不带正负号的变量 __s8, ...... __s64

还有如下的类型:

ssize_t schar_read(...,size_t count, loff_t *offset)

这些数据类型都定义在 linux/types.h 和 asm/posix_types.h

下面,我们温习一个例子来结束今天的东西: #include

#if defined(CONFIG_SMP) #define __SMP__ #endif

#if defined(CONFIG_MODVERSIONS) #define MODVERSIONS #include #endif

#include int init_module(void) {

printk(KERN_DEBUG \ return 0; }

void cleanup_module(void) {

printk(KERN_DEBUG \ }

Printk语句中的KERN_DEBUG的作用就是设置被打印消息的优先级,优先级定义在linux/kernel.h这个文件中。用命令: gcc -D__KERNEL__ -I/usr/src/linux/include -DMODULE -Wall -O2 -c hello.c -o hello.o编译,用如下命令挂载: insmod hello.o dmesg | tail -n1 rmmod hello.o lsmod

这里主要讲一下字符设备,字符设备

字符设备必须向内核注册自己,让内核知道“它”能做什么;向内核提供一些必要的信息使它能够在有应用程序希望与这个设备互动时用正确的函数进行处理,而register_chrdev就是这干这个活的,如下:

int register_chrdev(unsigned int major, const char *name,struct file_operations *fops)

它在失败进返回一个负值,成功就返回一个非负值,即它返回的major的值,如果这个函数把major设置为0,内核将给这个设备动动态分配一个主编号。 这个使用不是困难的,但如果你在上次加载过侬这个模块后主编号又发生了变化,你这次就不得不创建一个正确的特殊文件才能对设备进行访问。但主编号42和 120-127是为本地设备预留的,成品模块是不使用这几个号码的。可以参考Documentation/device.txt文件。

第二个参数是name,只有一个用途就是在/proc/devicess进行注册。即抽象出来的与设备进行互动的参数;最后一个参数是最 有意义的,它定义了设备与外界互动的方式方法;特别是规定哪些功能由它自己来负责完成,又有哪些些功能需要由内核的缺省函数来完成。 可以参考 linux/fs.h,由一系列的指针来完成。 文件操作

对设备的访问需要文件系统的特殊文件,这样的话,设备驱动程序需要注册一组文件操作,这些文件操作定义了设备提供的特定功能。下面是所有的最新的构架定义,但很少设备需要定义所有的函数。

struct file_operations{

loff_t (*llseek) (struct file *, loff_t, int);

ssize_t (*read) (struct file *,char *, size_t, loff_t *); ssize_t (*write) (struct file *,const char *, 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); 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 entry *); int (*fasync) (int , struct file * , int); int (*check_media_change) (Kdev_t dev); int (*revalidate) (Kdev_t dev);

int (*lock) (struct file *, int , struct file_lock*); }