LoveUnix » 编程开发 & Rational » 如何编写Linux的设备驱动程序
让LU留住您的每

一天 让LU博客留住您的每一天
2004-3-10 21:51 carol
<span style='font-size:14pt;line-height:100%'><span style='color:blue'><b>如何编写Linux的设备驱动程序</b></span></span><br /><br />作者:Roy G <br /><br /><span style='font-size:14pt;line-height:100%'><b>序言 </b></span><br /><br />Linux是Unix操作系统的一种变种,在Linux下编写驱动程序的原理和思想完全类似于其他的Unix系统,但它dos或window环境下的驱动程序有很大的区别.<br /><br />在Linux环境下设计驱动程序,思想简洁,操作方便,功能也很强大,但是支持函数少,只能依赖kernel中的函数,有些常用的操作要自己来编写,而且调试也不方便.本人这几周来为实验室自行研制的一块多媒体卡编制了驱动程序,获得了一些经验,愿与Linux fans共享,有不当之处,请予指正. <br /><br />以下的一些文字主要来源于khg,johnsonm的Write linux device driver,Brennan&#39;s Guide to Inline Assembly,The Linux A-Z,还有清华BBS上的有关device driver的一些资料. 这些资料有的已经过时,有的还有一些错误,我依据自己的试验结果进行了修正.

2004-3-10 22:00 carol
<span style='font-size:14pt;line-height:100%'><b>二.实例剖析 </b></span><br /><br />我们来写一个最简单的<span style='color:green'>字符设备驱动程序</span>.虽然它什么也不做,但是通过它可以了解Linux的设备驱动程序的工作原理.把下面的C代码输入机器,你就会获得一个真正的设备驱动程序.不过我的kernel是2.0.34,在低版本的kernel上可能会出现问题,我还没测试过.//xixi <br /><br /><!--c1--><div class='codetop'>CODE</div><div class='codemain'><!--ec1-->#define __NO_VERSION__ <br />#include &#60;linux/modules.h&#62; <br />#include &#60;linux/version.h&#62; <br />char kernel_version &#91;&#93; = UTS_RELEASE; <!--c2--></div><!--ec2--><br /><br />这一段定义了一些版本信息,虽然用处不是很大,但也必不可少.Johnsonm说所有的驱动程序的开头都要包含&lt;linux/config.h&gt;,但我看倒是未必. <br /><br />由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就是一些系统调用,如 open,read,write,close...., 注意,不是fopen, fread.,但是如何<span style='color:blue'>把系统调用和驱动程序关联起来</span>呢?这需要了解一个非常关键的数据结构: <br /><br /><!--c1--><div class='codetop'>CODE</div><div class='codemain'><!--ec1-->struct file_operations { <br /> &nbsp;int &#40;*seek&#41; &#40;struct inode * ,struct file *, off_t ,int&#41;; <br /> &nbsp;int &#40;*read&#41; &#40;struct inode * ,struct file *, char ,int&#41;; <br /> &nbsp;int &#40;*write&#41; &#40;struct inode * ,struct file *, off_t ,int&#41;; <br /> &nbsp;int &#40;*readdir&#41; &#40;struct inode * ,struct file *, struct dirent * ,int&#41;; <br /> &nbsp;int &#40;*select&#41; &#40;struct inode * ,struct file *, int ,select_table *&#41;; <br /> &nbsp;int &#40;*ioctl&#41; &#40;struct inode * ,struct file *, unsined int ,unsigned long <br /> &nbsp;int &#40;*mmap&#41; &#40;struct inode * ,struct file *, struct vm_area_struct *&#41;; <br /> &nbsp;int &#40;*open&#41; &#40;struct inode * ,struct file *&#41;; <br /> &nbsp;int &#40;*release&#41; &#40;struct inode * ,struct file *&#41;; <br /> &nbsp;int &#40;*fsync&#41; &#40;struct inode * ,struct file *&#41;; <br /> &nbsp;int &#40;*fasync&#41; &#40;struct inode * ,struct file *,int&#41;; <br /> &nbsp;int &#40;*check_media_change&#41; &#40;struct inode * ,struct file *&#41;; <br /> &nbsp;int &#40;*revalidate&#41; &#40;dev_t dev&#41;; <br />} <!--c2--></div><!--ec2--><br /><br />这个结构的每一个成员的名字都对应着一个系统调用.用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数.这是<span style='color:blue'>linux的设备驱动程序工作的基本原理</span>.既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域. <br /><br />相当简单,不是吗? <br /><br />下面就开始写子程序. <br /><br /><!--c1--><div class='codetop'>CODE</div><div class='codemain'><!--ec1-->#include &#60;linux/types.h&#62; <br />#include &#60;linux/fs.h&#62; <br />#include &#60;linux/mm.h&#62; <br />#include &#60;linux/errno.h&#62; <br />#include &#60;asm/segment.h&#62; <br /><br />unsigned int test_major = 0; <br />static int read_test&#40;struct inode *node,struct file *file, char *buf,int count&#41; <br />{ <br /> &nbsp;int left; <br /> &nbsp;if &#40;verify_area&#40;VERIFY_WRITE,buf,count&#41; == -EFAULT &#41; <br /> &nbsp; &nbsp;return -EFAULT; <br /> &nbsp;for&#40;left = count&#59; left &#62; 0&#59; left--&#41; <br /> &nbsp;{ <br /> &nbsp; &nbsp;__put_user&#40;1,buf,1&#41;; <br /> &nbsp; &nbsp;buf++; <br /> &nbsp;} <br /> &nbsp;return count; <br />} <!--c2--></div><!--ec2--><br />这个函数是为read调用准备的.当调用read时,read_test()被调用,它把用户的缓冲区全部写1. <br /><br />buf 是read调用的一个参数.它是用户进程空间的一个地址.但是在read_test被调用时,系统进入核心态.所以不能使用buf这个地址,必须用__put_user(),这是kernel提供的一个函数,用于向用户传送数据.另外还有很多类似功能的函数.请参考&lt;linux/mm.h&gt;.在向用户空间拷贝数据之前,必须验证buf是否可用. <br /><br />这就用到函数verify_area. <br /><br /><!--c1--><div class='codetop'>CODE</div><div class='codemain'><!--ec1-->static int write_tibet&#40;struct inode *inode,struct file *file, const char *buf,int count&#41; <br />{ <br /> &nbsp;return count; <br />} <br /><br />static int open_tibet&#40;struct inode *inode,struct file *file &#41; <br />{ <br /> &nbsp;MOD_INC_USE_COUNT; <br /> &nbsp;return 0; <br />} <br /><br />static void release_tibet&#40;struct inode *inode,struct file *file &#41; <br />{ <br /> &nbsp;MOD_DEC_USE_COUNT; <br />} <!--c2--></div><!--ec2--><br /><br />这几个函数都是空操作.实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。 <br /><!--c1--><div class='codetop'>CODE</div><div class='codemain'><!--ec1-->struct file_operations test_fops = { <br /> &nbsp;NULL, <br /> &nbsp;read_test, <br /> &nbsp;write_test, <br /> &nbsp;NULL, /* test_readdir */ <br /> &nbsp;NULL, <br /> &nbsp;NULL, /* test_ioctl */ <br /> &nbsp;NULL, /* test_mmap */ <br /> &nbsp;open_test, <br /> &nbsp;release_test, NULL, /* test_fsync */ <br /> &nbsp;NULL, /* test_fasync */ <br /> &nbsp;/* nothing more, fill with NULLs */ <br />}; <!--c2--></div><!--ec2--><br /><br />设备驱动程序的主体可以说是写好了。现在要把驱动程序嵌入内核。驱动程序可以按照两种方式编译。一种是<span style='color:blue'>编译进kernel</span>,另一种是<span style='color:blue'>编译成模块(modules</span>),如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。 <br /><br /><!--c1--><div class='codetop'>CODE</div><div class='codemain'><!--ec1-->int init_module&#40;void&#41; <br />{ <br /> &nbsp;int result; <br /> &nbsp;result = register_chrdev&#40;0, &#34;test&#34;, &amp;test_fops&#41;; <br /> &nbsp;if &#40;result &#60; 0&#41; { <br /> &nbsp; &nbsp;printk&#40;KERN_INFO &#34;test&#58; can&#39;t get major number &#34;&#41;; <br /> &nbsp; &nbsp;return result; <br /> &nbsp;} <br /> &nbsp;if &#40;test_major == 0&#41; test_major = result; /* dynamic */ <br /> &nbsp; &nbsp;return 0; <br />} <!--c2--></div><!--ec2--><br /><br />在用<span style='color:blue'>insmod</span>命令将编译好的模块调入内存时,init_module 函数被调用。在这里,init_module只做了一件事,就是<span style='color:blue'>向系统的字符设备表登记了一个字符设备</span>。<br />register_chrdev需要三个参数,参数一是希望获得的设备号,如果是零的话,系统将选择一个没有被占用的设备号返回。参数二是设备文件名,参数三用来登记驱动程序实际执行操作的函数的指针。 <br /><br />如果登记成功,返回设备的主设备号,不成功,返回一个负值。 <br /><br /><!--c1--><div class='codetop'>CODE</div><div class='codemain'><!--ec1-->void cleanup_module&#40;void&#41; <br />{ <br /> &nbsp;unregister_chrdev&#40;test_major, &#34;test&#34;&#41;; <br />} <!--c2--></div><!--ec2--><br /><br />在用<span style='color:blue'>rmmod</span>卸载模块时,cleanup_module函数被调用,它释放字符设备test在系统字符设备表中占有的表项。 <br /><br />一个极其简单的字符设备可以说写好了,文件名就叫test.c吧。 <br /><br />下面编译 <br /><br /><span style='color:green'>$ gcc -O2 -DMODULE -D__KERNEL__ -c test.c </span><br /><br />得到文件test.o就是一个设备驱动程序。 <br /><br />如果设备驱动程序有多个文件,把每个文件按上面的命令行编译,然后 <br /><br /><span style='color:green'>ld -r file1.o file2.o -o modulename. </span><br /><br />驱动程序已经编译好了,现在把它安装到系统中去。 <br /><br /><span style='color:green'>$ insmod -f test.o </span><br /><br />如果安装成功,在/proc/devices文件中就可以看到设备test,并可以看到它的主设备号。 <br /><br />要卸载的话,运行 <br /><br /><span style='color:green'>$ rmmod test </span><br /><br />下一步要创建设备文件。 <br /><br /><span style='color:green'>mknod /dev/test c major minor </span><br /><br />c 是指字符设备,major是主设备号,就是在/proc/devices里看到的。 <br /><br />用shell命令 <br /><br /><span style='color:green'>$ cat /proc/devices | awk &quot;\$2==&quot;test&quot; {print \$1}&quot; </span><br /><br />就可以获得主设备号,可以把上面的命令行加入你的shell script中去。 <br /><br />minor是从设备号,设置成0就可以了。 <br /><br />我们现在可以通过设备文件来访问我们的驱动程序。写一个小小的测试程序。 <br /><br /><!--c1--><div class='codetop'>CODE</div><div class='codemain'><!--ec1-->#include &#60;stdio.h&#62; <br />#include &#60;sys/types.h&#62; <br />#include &#60;sys/stat.h&#62; <br />#include &#60;fcntl.h&#62; <br />main&#40;&#41; <br />{ <br /> &nbsp;int testdev; <br /> &nbsp;int i; <br /> &nbsp;char buf&#91;10&#93;; <br /> &nbsp;testdev = open&#40;&#34;/dev/test&#34;,O_RDWR&#41;; <br /> &nbsp;if &#40; testdev == -1 &#41; <br /> &nbsp;{ <br /> &nbsp; &nbsp;printf&#40;&#34;Cann&#39;t open file &#34;&#41;; <br /> &nbsp; &nbsp;exit&#40;0&#41;; <br /> &nbsp;} <br /> &nbsp;read&#40;testdev,buf,10&#41;; <br /> &nbsp;for &#40;i = 0; i &#60; 10;i++&#41; <br /> &nbsp;printf&#40;&#34;%d &#34;,buf&#91;i&#93;&#41;; <br /> &nbsp;close&#40;testdev&#41;; <br />} <!--c2--></div><!--ec2--><br /><br />编译运行,看看是不是打印出全1 ? <br /><br />以上只是一个简单的演示。真正实用的驱动程序要复杂的多,要<span style='color:blue'>处理如中断,DMA,I/O port等问题。这些才是真正的难点</span>。请看下节,实际情况的处理。

2004-3-10 22:04 carol
<span style='font-size:14pt;line-height:100%'><b>三、设备驱动程序中的一些具体问题 </b></span><br /><br /><b>1. I/O Port. </b><br /><br />和硬件打交道离不开I/O Port,老的ISA设备经常是占用实际的I/O端口,在linux下,操作系统没有对I/O口屏蔽,也就是说,任何驱动程序都可以对任意的I/O口操作,这样就很容易引起混乱。每个驱动程序应该自己避免误用端口。 <br /><br />有两个重要的kernel函数可以保证驱动程序做到这一点。 <br /><br /><span style='color:blue'>1)check_region(int io_port, int off_set) </span><br /><br />这个函数察看系统的I/O表,看是否有别的驱动程序占用某一段I/O口。 <br /><br />参数1:io端口的基地址, <br />参数2:io端口占用的范围。 <br />返回值:0 没有占用, 非0,已经被占用。 <br /><br /><span style='color:blue'>2)request_region(int io_port, int off_set,char *devname) </span><br /><br />如果这段I/O端口没有被占用,在我们的驱动程序中就可以使用它。在使用之前,必须向系统登记,以防止被其他程序占用。登记后,在/proc/ioports文件中可以看到你登记的io口。 <br /><br />参数1:io端口的基地址。 <br />参数2:io端口占用的范围。 <br />参数3:使用这段io地址的设备名。 <br /><br />在对I/O口登记后,就可以放心地用inb(), outb()之类的函来访问了。 <br /><br />在一些pci设备中,I/O端口被映射到一段内存中去,要访问这些端口就相当于访问一段内存。经常性的,我们要获得一块内存的物理地址。在dos环境下,(之所以不说是dos操作系统是因为我认为DOS根本就不是一个操作系统,它实在是太简单,太不安全了)只要用段:偏移就可以了。在window95中,95ddk提供了一个vmm 调用 _MapLinearToPhys,用以把线性地址转化为物理地址。但在Linux中是怎样做的呢? <br /><br /><b>2. 内存操作 </b><br /><br />在设备驱动程序中动态开辟内存,不是用malloc,而是<span style='color:blue'>kmalloc</span>,或者用<span style='color:blue'>get_free_pages</span>直接申请页。释放内存用的是<span style='color:blue'>kfree</span>,或<span style='color:blue'>free_pages</span>. <br /><br />请<span style='color:red'>注意</span>,kmalloc等函数返回的是物理地址!而malloc等返回的是线性地址!关于kmalloc返回的是物理地址这一点本人有点不太明白:既然从线性地址到物理地址的转换是由386cpu硬件完成的,那样汇编指令的操作数应该是线性地址,驱动程序同样也不能直接使用物理地址而是线性地址。<br />但是事实上kmalloc返回的确实是物理地址,而且也可以直接通过它访问实际的RAM,我想这样可以由两种解释,一种是在核心态禁止分页,但是这好像不太现实;另一种是linux的页目录和页表项设计得正好使得物理地址等同于线性地址。我的想法不知对不对,还请高手指教。 <br /><br />言归正传,要注意kmalloc最大只能开辟128k-16,16个字节是被页描述符结构占用了。kmalloc用法参见khg. <br /><br />内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址。 <br /><br />另外,很多硬件需要一块比较大的连续内存用作DMA传送。这块内存需要一直驻留在内存,不能被交换到文件中去。但是kmalloc最多只能开辟128k的内存。 <br /><br />这可以通过牺牲一些系统内存的方法来解决。 <br /><br />具体做法是:比如说你的机器由32M的内存,在lilo.conf的启动参数中加上mem=30M,这样linux就认为你的机器只有30M的内存,剩下的2M内存在vremap之后就可以为DMA所用了。 <br /><br />请记住,用vremap映射后的内存,不用时应用unremap释放,否则会浪费页表。 <br /><br /><b>3. 中断处理 </b><br /><br />同处理I/O端口一样,要使用一个中断,必须先向系统登记。 <br /><br /><!--c1--><div class='codetop'>CODE</div><div class='codemain'><!--ec1-->int request_irq&#40;unsigned int irq , <br /> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;void&#40;*handle&#41;&#40;int,void *,struct pt_regs *&#41;, <br /> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;unsigned int long flags, <br /> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;const char *device&#41;; <!--c2--></div><!--ec2--><br /><br />irq: 是要申请的中断。 <br />handle:中断处理函数指针。 <br />flags:SA_INTERRUPT 请求一个快速中断,0 正常中断。 <br />device:设备名。 <br /><br />如果登记成功,返回0,这时在/proc/interrupts文件中可以看你请求的中断。 <br /><br /><b>4. 一些常见的问题 </b><br /><br />对硬件操作,有时<span style='color:blue'>时序</span>很重要。但是如果用C语言写一些低级的硬件操作的话,gcc往往会对你的程序进行优化,这样时序就错掉了。如果用汇编写呢,gcc同样会对汇编代码进行优化,除非你用<span style='color:blue'>volatile</span>关键字修饰。最保险的办法是<span style='color:blue'>禁止优化</span>。这当然只能对一部分你自己编写的代码。如果对所有的代码都不优化,你会发现驱动程序根本无法装载。这是因为在编译驱动程序时要用到gcc的一些扩展特性,而这些扩展特性必须在加了优化选项之后才能体现出来。 <br /><br />关于<span style='color:blue'>kernel的调试工具</span>,我现在还没有发现有合适的。有谁知道请告诉我,不胜感激。我一直都在printk打印调试信息,倒也还凑合。 <br /><br />关于设备驱动程序还有很多内容,如<span style='color:blue'>等待/唤醒机制,块设备的编写</span>等。 <br /><br />我还不是很明白,不敢乱说。 <br /><br />欢迎大家批评指正。 <br /><br />来源:http://plinux.org/html/sections.php3?op=viewarticle&amp;artid=41

2004-12-28 17:29 jinxin112688
To :carol兄!<br />我按上面的例子抄下来,编译时出现问题,提示:找不到&lt;asm/mrs.h&gt;<br />如何解决啊?我用的Kernel 是2.4.18-3版本。<br />帮帮忙!谢谢了!

2004-12-29 12:42 无双
精华里面我也写过一篇教程的<br /><br />另外 如果真想学习内核开发<br /><br />那可以买<br /><br />linux设备驱动程序开发 这本书来看看

2005-3-7 16:58 irror
无双版主可以去CU论坛找相关内核资料.<br />还有一个网址: www.oldlinux.org 有详细的资料

2005-3-7 19:24 无双
谢谢<br /><br />oldlinux不是那本pdf的.0.99内核的注释吗<br /><br />里面写的是不错

2005-3-7 20:31 carol
哈哈

2005-3-9 08:49 irror
是的,我这里有linux下的内核情景分析,可惜太大了,传不上来.呵呵&#33;

2005-3-28 14:19 ling
按照上面的例子我编译了一下   当用测试程序测试时  每回都得到Cann&#39;t open file的结果  最后我cat test模块出现了提示信息 :段错误 .  不知是什么原因? 该怎么解决? 我的内核为2.6.5.  望高手指点<br />

2005-12-28 10:04 liyongbo
我是新手

刚学这东西,我按照上面的内容写了以下一下,但是出现了一些问题,我用的版本是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

2006-2-20 17:19 SyncMaster
请问怎样做GPIO中断程序? 比如用GPIO做模拟串口。
另外,在中断程序中收到数据后要怎样通知应用程序呢?:L

2006-5-31 16:48 crazyprince
to:liyongbo
#define __NO_VERSION__
版本当然不匹配,会给出警告,但不会影响运行(内核会提示版本不一致的模块)

unresolved symbol verigy _area
unresolved symbol _put_user
..........

_put_user是低版本linux里用的,2.4 以上该用
copy_to_user(buf, rbuff, cnt);
copy_from_user(wbuff, buf, cnt);
头文件:#include <asm/uaccess.h>

因为你的测试程序属于用户空间,不能直接操作内核空间的数据
所以要把数据拷贝到用户空间

页: [1]


Powered by Discuz! Archiver 5.5.0  © 2001-2006 Comsenz Inc.