标题: 如何编写Linux的设备驱动程序
carol
荣誉斑竹
Rank: 14Rank: 14Rank: 14Rank: 14
幻想懒王++


UID 1859
精华 66
积分 5139
帖子 10006
活跃指数 32
LU金币 2596 个
LU金条 0 个
阅读权限 200
注册 2003-11-7
 
发表于 2004-3-10 21:51  资料  个人空间  短消息  加为好友 
如何编写Linux的设备驱动程序

作者:Roy G

序言

Linux是Unix操作系统的一种变种,在Linux下编写驱动程序的原理和思想完全类似于其他的Unix系统,但它dos或window环境下的驱动程序有很大的区别.

在Linux环境下设计驱动程序,思想简洁,操作方便,功能也很强大,但是支持函数少,只能依赖kernel中的函数,有些常用的操作要自己来编写,而且调试也不方便.本人这几周来为实验室自行研制的一块多媒体卡编制了驱动程序,获得了一些经验,愿与Linux fans共享,有不当之处,请予指正.

以下的一些文字主要来源于khg,johnsonm的Write linux device driver,Brennan's Guide to Inline Assembly,The Linux A-Z,还有清华BBS上的有关device driver的一些资料. 这些资料有的已经过时,有的还有一些错误,我依据自己的试验结果进行了修正.

顶部
carol
荣誉斑竹
Rank: 14Rank: 14Rank: 14Rank: 14
幻想懒王++


UID 1859
精华 66
积分 5139
帖子 10006
活跃指数 32
LU金币 2596 个
LU金条 0 个
阅读权限 200
注册 2003-11-7
 
发表于 2004-3-10 22:00  资料  个人空间  短消息  加为好友 
二.实例剖析

我们来写一个最简单的字符设备驱动程序.虽然它什么也不做,但是通过它可以了解Linux的设备驱动程序的工作原理.把下面的C代码输入机器,你就会获得一个真正的设备驱动程序.不过我的kernel是2.0.34,在低版本的kernel上可能会出现问题,我还没测试过.//xixi

CODE
#define __NO_VERSION__
#include <linux/modules.h>
#include <linux/version.h>
char kernel_version [] = UTS_RELEASE;


这一段定义了一些版本信息,虽然用处不是很大,但也必不可少.Johnsonm说所有的驱动程序的开头都要包含<linux/config.h>,但我看倒是未必.

由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就是一些系统调用,如 open,read,write,close...., 注意,不是fopen, fread.,但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构:

CODE
struct file_operations {
 int (*seek) (struct inode * ,struct file *, off_t ,int);
 int (*read) (struct inode * ,struct file *, char ,int);
 int (*write) (struct inode * ,struct file *, off_t ,int);
 int (*readdir) (struct inode * ,struct file *, struct dirent * ,int);
 int (*select) (struct inode * ,struct file *, int ,select_table *);
 int (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long
 int (*mmap) (struct inode * ,struct file *, struct vm_area_struct *);
 int (*open) (struct inode * ,struct file *);
 int (*release) (struct inode * ,struct file *);
 int (*fsync) (struct inode * ,struct file *);
 int (*fasync) (struct inode * ,struct file *,int);
 int (*check_media_change) (struct inode * ,struct file *);
 int (*revalidate) (dev_t dev);
}


这个结构的每一个成员的名字都对应着一个系统调用.用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数.这是linux的设备驱动程序工作的基本原理.既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域.

相当简单,不是吗?

下面就开始写子程序.

CODE
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/errno.h>
#include <asm/segment.h>

unsigned int test_major = 0;
static int read_test(struct inode *node,struct file *file, char *buf,int count)
{
 int left;
 if (verify_area(VERIFY_WRITE,buf,count) == -EFAULT )
   return -EFAULT;
 for(left = count; left > 0; left--)
 {
   __put_user(1,buf,1);
   buf++;
 }
 return count;
}

这个函数是为read调用准备的.当调用read时,read_test()被调用,它把用户的缓冲区全部写1.

buf 是read调用的一个参数.它是用户进程空间的一个地址.但是在read_test被调用时,系统进入核心态.所以不能使用buf这个地址,必须用__put_user(),这是kernel提供的一个函数,用于向用户传送数据.另外还有很多类似功能的函数.请参考<linux/mm.h>.在向用户空间拷贝数据之前,必须验证buf是否可用.

这就用到函数verify_area.

CODE
static int write_tibet(struct inode *inode,struct file *file, const char *buf,int count)
{
 return count;
}

static int open_tibet(struct inode *inode,struct file *file )
{
 MOD_INC_USE_COUNT;
 return 0;
}

static void release_tibet(struct inode *inode,struct file *file )
{
 MOD_DEC_USE_COUNT;
}


这几个函数都是空操作.实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。
CODE
struct file_operations test_fops = {
 NULL,
 read_test,
 write_test,
 NULL, /* test_readdir */
 NULL,
 NULL, /* test_ioctl */
 NULL, /* test_mmap */
 open_test,
 release_test, NULL, /* test_fsync */
 NULL, /* test_fasync */
 /* nothing more, fill with NULLs */
};


设备驱动程序的主体可以说是写好了。现在要把驱动程序嵌入内核。驱动程序可以按照两种方式编译。一种是编译进kernel,另一种是编译成模块(modules),如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。

CODE
int init_module(void)
{
 int result;
 result = register_chrdev(0, "test", &test_fops);
 if (result < 0) {
   printk(KERN_INFO "test: can't get major number ");
   return result;
 }
 if (test_major == 0) test_major = result; /* dynamic */
   return 0;
}


在用insmod命令将编译好的模块调入内存时,init_module 函数被调用。在这里,init_module只做了一件事,就是向系统的字符设备表登记了一个字符设备
register_chrdev需要三个参数,参数一是希望获得的设备号,如果是零的话,系统将选择一个没有被占用的设备号返回。参数二是设备文件名,参数三用来登记驱动程序实际执行操作的函数的指针。

如果登记成功,返回设备的主设备号,不成功,返回一个负值。

CODE
void cleanup_module(void)
{
 unregister_chrdev(test_major, "test");
}


在用rmmod卸载模块时,cleanup_module函数被调用,它释放字符设备test在系统字符设备表中占有的表项。

一个极其简单的字符设备可以说写好了,文件名就叫test.c吧。

下面编译

$ gcc -O2 -DMODULE -D__KERNEL__ -c test.c

得到文件test.o就是一个设备驱动程序。

如果设备驱动程序有多个文件,把每个文件按上面的命令行编译,然后

ld -r file1.o file2.o -o modulename.

驱动程序已经编译好了,现在把它安装到系统中去。

$ insmod -f test.o

如果安装成功,在/proc/devices文件中就可以看到设备test,并可以看到它的主设备号。

要卸载的话,运行

$ rmmod test

下一步要创建设备文件。

mknod /dev/test c major minor

c 是指字符设备,major是主设备号,就是在/proc/devices里看到的。

用shell命令

$ cat /proc/devices | awk "\$2=="test" {print \$1}"

就可以获得主设备号,可以把上面的命令行加入你的shell script中去。

minor是从设备号,设置成0就可以了。

我们现在可以通过设备文件来访问我们的驱动程序。写一个小小的测试程序。

CODE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
main()
{
 int testdev;
 int i;
 char buf[10];
 testdev = open("/dev/test",O_RDWR);
 if ( testdev == -1 )
 {
   printf("Cann't open file ");
   exit(0);
 }
 read(testdev,buf,10);
 for (i = 0; i < 10;i++)
 printf("%d ",buf[i]);
 close(testdev);
}


编译运行,看看是不是打印出全1 ?

以上只是一个简单的演示。真正实用的驱动程序要复杂的多,要处理如中断,DMA,I/O port等问题。这些才是真正的难点。请看下节,实际情况的处理。

顶部
carol
荣誉斑竹
Rank: 14Rank: 14Rank: 14Rank: 14
幻想懒王++


UID 1859
精华 66
积分 5139
帖子 10006
活跃指数 32
LU金币 2596 个
LU金条 0 个
阅读权限 200
注册 2003-11-7
 
发表于 2004-3-10 22:04  资料  个人空间  短消息  加为好友 
三、设备驱动程序中的一些具体问题

1. I/O Port.

和硬件打交道离不开I/O Port,老的ISA设备经常是占用实际的I/O端口,在linux下,操作系统没有对I/O口屏蔽,也就是说,任何驱动程序都可以对任意的I/O口操作,这样就很容易引起混乱。每个驱动程序应该自己避免误用端口。

有两个重要的kernel函数可以保证驱动程序做到这一点。

1)check_region(int io_port, int off_set)

这个函数察看系统的I/O表,看是否有别的驱动程序占用某一段I/O口。

参数1:io端口的基地址,
参数2:io端口占用的范围。
返回值:0 没有占用, 非0,已经被占用。

2)request_region(int io_port, int off_set,char *devname)

如果这段I/O端口没有被占用,在我们的驱动程序中就可以使用它。在使用之前,必须向系统登记,以防止被其他程序占用。登记后,在/proc/ioports文件中可以看到你登记的io口。

参数1:io端口的基地址。
参数2:io端口占用的范围。
参数3:使用这段io地址的设备名。

在对I/O口登记后,就可以放心地用inb(), outb()之类的函来访问了。

在一些pci设备中,I/O端口被映射到一段内存中去,要访问这些端口就相当于访问一段内存。经常性的,我们要获得一块内存的物理地址。在dos环境下,(之所以不说是dos操作系统是因为我认为DOS根本就不是一个操作系统,它实在是太简单,太不安全了)只要用段:偏移就可以了。在window95中,95ddk提供了一个vmm 调用 _MapLinearToPhys,用以把线性地址转化为物理地址。但在Linux中是怎样做的呢?

2. 内存操作

在设备驱动程序中动态开辟内存,不是用malloc,而是kmalloc,或者用get_free_pages直接申请页。释放内存用的是kfree,或free_pages.

注意,kmalloc等函数返回的是物理地址!而malloc等返回的是线性地址!关于kmalloc返回的是物理地址这一点本人有点不太明白:既然从线性地址到物理地址的转换是由386cpu硬件完成的,那样汇编指令的操作数应该是线性地址,驱动程序同样也不能直接使用物理地址而是线性地址。
但是事实上kmalloc返回的确实是物理地址,而且也可以直接通过它访问实际的RAM,我想这样可以由两种解释,一种是在核心态禁止分页,但是这好像不太现实;另一种是linux的页目录和页表项设计得正好使得物理地址等同于线性地址。我的想法不知对不对,还请高手指教。

言归正传,要注意kmalloc最大只能开辟128k-16,16个字节是被页描述符结构占用了。kmalloc用法参见khg.

内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址。

另外,很多硬件需要一块比较大的连续内存用作DMA传送。这块内存需要一直驻留在内存,不能被交换到文件中去。但是kmalloc最多只能开辟128k的内存。

这可以通过牺牲一些系统内存的方法来解决。

具体做法是:比如说你的机器由32M的内存,在lilo.conf的启动参数中加上mem=30M,这样linux就认为你的机器只有30M的内存,剩下的2M内存在vremap之后就可以为DMA所用了。

请记住,用vremap映射后的内存,不用时应用unremap释放,否则会浪费页表。

3. 中断处理

同处理I/O端口一样,要使用一个中断,必须先向系统登记。

CODE
int request_irq(unsigned int irq ,
               void(*handle)(int,void *,struct pt_regs *),
               unsigned int long flags,
               const char *device);


irq: 是要申请的中断。
handle:中断处理函数指针。
flags:SA_INTERRUPT 请求一个快速中断,0 正常中断。
device:设备名。

如果登记成功,返回0,这时在/proc/interrupts文件中可以看你请求的中断。

4. 一些常见的问题

对硬件操作,有时时序很重要。但是如果用C语言写一些低级的硬件操作的话,gcc往往会对你的程序进行优化,这样时序就错掉了。如果用汇编写呢,gcc同样会对汇编代码进行优化,除非你用volatile关键字修饰。最保险的办法是禁止优化。这当然只能对一部分你自己编写的代码。如果对所有的代码都不优化,你会发现驱动程序根本无法装载。这是因为在编译驱动程序时要用到gcc的一些扩展特性,而这些扩展特性必须在加了优化选项之后才能体现出来。

关于kernel的调试工具,我现在还没有发现有合适的。有谁知道请告诉我,不胜感激。我一直都在printk打印调试信息,倒也还凑合。

关于设备驱动程序还有很多内容,如等待/唤醒机制,块设备的编写等。

我还不是很明白,不敢乱说。

欢迎大家批评指正。

来源:http://plinux.org/html/sections.php3?op=viewarticle&artid=41

顶部
jinxin112688
LU幼天使
Rank: 2



UID 27743
精华 2
积分 47
帖子 86
活跃指数 4
LU金币 2019 个
LU金条 0 个
阅读权限 20
注册 2004-11-19
 
发表于 2004-12-28 17:29  资料  个人空间  短消息  加为好友 
To :carol兄!
我按上面的例子抄下来,编译时出现问题,提示:找不到<asm/mrs.h>
如何解决啊?我用的Kernel 是2.4.18-3版本。
帮帮忙!谢谢了!

顶部
无双
荣誉斑竹
Rank: 14Rank: 14Rank: 14Rank: 14
天才猪



UID 4
精华 84
积分 5863
帖子 11390
活跃指数 0
LU金币 4248 个
LU金条 0 个
阅读权限 200
注册 2003-9-16
来自 杭州
 
发表于 2004-12-29 12:42  资料  个人空间  主页 短消息  加为好友 
精华里面我也写过一篇教程的

另外 如果真想学习内核开发

那可以买

linux设备驱动程序开发 这本书来看看





不要问我结果 我只研究过程与思路
无双客栈
顶部
irror (彬哥儿)
LU幼天使
Rank: 2


UID 29989
精华 2
积分 144
帖子 274
活跃指数 5
LU金币 2105 个
LU金条 0 个
阅读权限 20
注册 2005-3-4
 
发表于 2005-3-7 16:58  资料  个人空间  短消息  加为好友 
无双版主可以去CU论坛找相关内核资料.
还有一个网址: www.oldlinux.org 有详细的资料





###########################
# Long long live chairs Mao !#
###########################
顶部
[广告] 记录自己的思想火花,留住每日的技术积累,尽在拥有属于自己独立域名的博客。
无双
荣誉斑竹
Rank: 14Rank: 14Rank: 14Rank: 14
天才猪



UID 4
精华 84
积分 5863
帖子 11390
活跃指数 0
LU金币 4248 个
LU金条 0 个
阅读权限 200
注册 2003-9-16
来自 杭州
 
发表于 2005-3-7 19:24  资料  个人空间  主页 短消息  加为好友 
谢谢

oldlinux不是那本pdf的.0.99内核的注释吗

里面写的是不错





不要问我结果 我只研究过程与思路
无双客栈
顶部
[广告] 记录自己的思想火花,留住每日的技术积累,尽在拥有属于自己独立域名的博客。
carol
荣誉斑竹
Rank: 14Rank: 14Rank: 14Rank: 14
幻想懒王++


UID 1859
精华 66
积分 5139
帖子 10006
活跃指数 32
LU金币 2596 个
LU金条 0 个
阅读权限 200
注册 2003-11-7
 
发表于 2005-3-7 20:31  资料  个人空间  短消息  加为好友 
哈哈

顶部
[广告] 记录自己的思想火花,留住每日的技术积累,尽在拥有属于自己独立域名的博客。
irror (彬哥儿)
LU幼天使
Rank: 2


UID 29989
精华 2
积分 144
帖子 274
活跃指数 5
LU金币 2105 个
LU金条 0 个
阅读权限 20
注册 2005-3-4
 
发表于 2005-3-9 08:49  资料  个人空间  短消息  加为好友 
是的,我这里有linux下的内核情景分析,可惜太大了,传不上来.呵呵!





###########################
# Long long live chairs Mao !#
###########################
顶部
[广告] 记录自己的思想火花,留住每日的技术积累,尽在拥有属于自己独立域名的博客。
ling
LU幼天使
Rank: 2



UID 29203
精华 0
积分 23
帖子 45
活跃指数 3
LU金币 2016 个
LU金条 0 个
阅读权限 20
注册 2005-1-26
 
发表于 2005-3-28 14:19  资料  个人空间  短消息  加为好友 
按照上面的例子我编译了一下 当用测试程序测试时 每回都得到Cann't open file的结果 最后我cat test模块出现了提示信息 :段错误 . 不知是什么原因? 该怎么解决? 我的内核为2.6.5. 望高手指点

顶部
liyongbo
LU新生
Rank: 1



UID 38901
精华 0
积分 1
帖子 1
活跃指数 0
LU金币 2007 个
LU金条 0 个
阅读权限 10
注册 2005-12-28
 
发表于 2005-12-28 10:04  资料  个人空间  短消息  加为好友 
我是新手

刚学这东西,我按照上面的内容写了以下一下,但是出现了一些问题,我用的版本是2.4.20-8 的
出现的问题是:
Kernel module version mismatched!
test .o was compiled for kernel version 2.4.20-8 custom
while this kernel is version 2.4.20-8
  

unresolved symbol verigy _area
unresolved symbol _put_user


请各位大峡指点啊 不胜感激!!!!!!!!!!!!!!!!!!!!!!!!!!!11

顶部
SyncMaster
LU新生
Rank: 1



UID 41112
精华 0
积分 1
帖子 2
活跃指数 1
LU金币 3 个
LU金条 0 个
阅读权限 10
注册 2006-2-20
 
发表于 2006-2-20 17:19  资料  个人空间  短消息  加为好友 
请问怎样做GPIO中断程序? 比如用GPIO做模拟串口。
另外,在中断程序中收到数据后要怎样通知应用程序呢?

顶部
 



当前时区 GMT+8, 现在时间是 2008-12-6 01:49
乐悠LoveUnix论坛-京ICP备05005823号

Thanks to Discuz!  © 2001-2007    Power by LoveUnix.net
Processed in 0.096820 second(s), 6 queries , Gzip enabled

清除 Cookies - 联系我们 - 乐悠LoveUnix - Archiver