手把手要教你编写Linux设备驱动程序.docx
《手把手要教你编写Linux设备驱动程序.docx》由会员分享,可在线阅读,更多相关《手把手要教你编写Linux设备驱动程序.docx(11页珍藏版)》请在冰豆网上搜索。
![手把手要教你编写Linux设备驱动程序.docx](https://file1.bdocx.com/fileroot1/2022-12/16/8720450e-f3e8-4667-92f6-711d2ac2a73b/8720450e-f3e8-4667-92f6-711d2ac2a73b1.gif)
手把手要教你编写Linux设备驱动程序
如何编写Linux设备驱动程序
Linux是Unix操作系统的一种变种,在Linux下编写驱动程序的原理和思想完全类似于其他的Unix系统,但它dos或window环境下的驱动程序有很大的区别。
在Linux环境下设计驱动程序,思想简洁,操作方便,功能也很强大,但是支持函数少,只能依赖kernel中的函数,有些常用的操作要自己来编写,而且调试也不方便。
以下的一些文字主要来源于khg,johnsonm的Writelinuxdevicedriver,Brennan'sGuidetoInlineAssembly,TheLinuxA-Z,还有清华BBS上的有关devicedriver的一些资料。
一、Linuxdevicedriver的概念
系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。
设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。
设备驱动程序是内核的一部分,它完成以下的功能:
1。
对设备初始化和释放。
2。
把数据从内核传送到硬件和从硬件读取数据。
3。
读取应用程序传送给设备文件的数据和回送应用程序请求的数据。
4。
检测和处理设备出现的错误。
在Linux操作系统下有三类主要的设备文件类型,一是字符设备,二是块设备,三是网络设备。
字符设备和块设备的主要区别是:
在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。
块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待。
已经提到,用户进程是通过设备文件来与实际的硬件打交道。
每个设备文件都都有其文件属性(c/b),表示是字符设备还是块设备?
另外每个文件都有两个设备号,第一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的不同的硬件设备,比如有两个软盘,就可以用从设备号来区分他们。
设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到驱动程序。
最后必须提到的是,在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度。
也就是说,系统必须在你的驱动程序的子函数返回后才能进行其他的工作。
如果你的驱动程序陷入死循环,不幸的是你只有重新启动机器了,然后就是漫长的fsck。
读/写时,它首先察看缓冲区的内容,如果缓冲区的数据
如何编写Linux操作系统下的设备驱动程序
二、实例剖析
我们来写一个最简单的字符设备(比如蜂鸣器)驱动程序。
虽然它什么也不做,但是通过它可以了解Linux的设备驱动程序的工作原理。
把下面的C代码输入机器,你就会获得一个真正的设备驱动程序。
#define__NO_VERSION__
#include
#include
charkernel_version[]=UTS_RELEASE;
这一段定义了一些版本信息,虽然用处不是很大,但也必不可少。
Johnsonm说所有的驱动程序的开头都要包含,一般来讲最好使用。
由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就是一些系统调用,如open,read,write,close…,注意,不是fopen,fread,但是如何把系统调用和驱动程序关联起来呢?
这需要了解一个非常关键的数据结构:
structfile_operations
{
int(*seek)(structinode*,structfile*,off_t,int);//文件定位
int(*read)(structinode*,structfile*,char,int);
int(*write)(structinode*,structfile*,off_t,int);
int(*readdir)(structinode*,structfile*,structdirent*,int);//读取目录
int(*select)(structinode*,structfile*,int,select_table*);
//I/O端口复用,非阻塞的状态下实现设备的访问
int(*ioctl)(structinode*,structfile*,unsinedint,unsignedlong);
//对设备的属性修改
int(*mmap)(structinode*,structfile*,structvm_area_struct*);
//内存映射
int(*open)(structinode*,structfile*);
int(*release)(structinode*,structfile*);
int(*fsync)(structinode*,structfile*);//设备的同步信息
int(*fasync)(structinode*,structfile*,int);//异步
int(*check_media_change)(structinode*,structfile*);//检测数据是否发生改变
int(*revalidate)(dev_tdev);//使设备重新有效
}
这个结构的每一个成员的名字都对应着一个系统调用。
用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。
这是linux的设备驱动程序工作的基本原理。
既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域。
下面就开始写子程序。
#include
#include//文件系统
#include//内存管理
#include
#include//汇编语言编写
unsignedinttest_major=0;//主设备号,自动搜索
staticintread_test(structinode*node,structfile*file,char*buf,intcount)
//读测试,inode*node表示哪一个设备,*file表示文件描述符,*buf表读取时的接口,count表期望读的字节数,staticint返回值—实际读到的数量
{
intleft;
if(verify_area(VERIFY_WRITE,buf,count)==-EFAULT)
//verify_area,验证某一个buf中的数据是否有效
return-EFAULT;
for(left=count;left>0;left--)
{
__put_user(1,buf,1);//从内核空间将数据拷贝到用户空间去,将”1”依次放到用户空间的buf中,每次放的大小是一个字节
buf++;
}
returncount;//返回读的数据数
}
这个函数是为read调用准备的。
当调用read时,read_test()被调用,它把用户的缓冲区全部写1。
buf是read调用的一个参数。
它是用户进程空间的一个地址。
但是在read_test被调用时,系统进入核心态。
所以不能使用buf这个地址,必须用__put_user(),这是kernel提供的一个函数,用于向用户传送数据。
另外还有很多类似功能的函数。
请参考,在向用户空间拷贝数据之前,必须验证buf是否可用。
这就用到函数verify_area。
staticintwrite_tibet(structinode*inode,structfile*file,constchar*buf,intcount)
{
returncount;
}
//*inode表入口节点,*file表设备描述符
staticintopen_tibet(structinode*inode,structfile*file)
{
MOD_INC_USE_COUNT;//宏定义,注册一个驱动之后,模块数自动+1
return0;
}
staticvoidrelease_tibet(structinode*inode,structfile*file)
{
MOD_DEC_USE_COUNT;//模块数自动减1
}
这几个函数都是空操作。
实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。
structfile_operationstest_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*/
/*nothingmore,fillwithNULLs*/
};
设备驱动程序的主体可以说是写好了。
现在要把驱动程序嵌入内核。
驱动程序可以按照两种方式编译。
一种是编译进kernel,另一种是编译成模块(modules),如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。
intinit_module(void)//注册方式
{
intresult;
result=register_chrdev(0,"test",&test_fops);
//注册字符型设备到内核中去,“0”表是自动根据设备节点里的主设备号来获取它的设备号;"test"表示你注册的设备名;&test_fops为注册的接口
if(result<0){
printk(KERN_INFO"test:
can'tgetmajornumber\n");//在内核打印信息必须用printk;而printf只能在用户空间用
returnresult;
}
if(test_major==0)test_major=result;/*dynamic*/
//由内核分配一个设备号给驱动程序,获取设备的主设备号
return0;
}
在用insmod命令将编译好的模块调入内存时,init_module函数被调用。
在这里,init_module只做了一件事,就是向系统的字符设备表登记了一个字符设备。
register_chrdev需要三个参数,参数一是希望获得的设备号,如果是零的话,系统将选择一个没有被占用的设备号返回。
参数二是设备文件名,参数三用来登记驱动程序实际执行操作的函数的指针。
如果登记成功,返回设备的主设备号,不成功,返回一个负值。
voidcleanup_module(void)
{
unregister_chrdev(test_major,"test");
}
在用rmmod卸载模块时,cleanup_module函数被调用,它释放字符设备test在系统字符设备表中占有的表项。
一个极其简单的字符设备可以说写好了,文件名就叫test.c吧。
下面编译(模块方式):
$gcc-O2-DMODULE-D__KERNEL__-ctest.c
//-O2表二级优化;-DMODULE表编译成模块;-D__KERNEL__表加载内核某一个块;2.6内核编译出来是test.ko文件
得到文件test.o就是一个设备驱动程序。
如果设备驱动程序有多个文件,把每个文件按上面的命令行编译,然后
ld-rfile1.ofile2.o-omodulename。
//链接文件,生成相应的模块名
驱动程序已经编译好了,现在把它安装到系统中去。
$insmod–ftest.o//强制加载到内核中去
如果安装成功,在/proc/devices文件中就可以看到设备test,并可以看到它的主设备号。
要卸载的话,运行:
$rmmodtest
下一步要创建设备文件。
mknod/dev/testcmajorminor
c是指字符设备,major是主设备号,就是在/proc/devices里看到的。
用shell命令
$cat/proc/devices
就可以获得主设备号,可以把上面的命令行加入你的shellscript中去。
minor是从设备号,设置成0就可以了。
我们现在可以通过设备文件来访问我们的驱动程序。
写一个小小的测试程序。
#include
#include
#include
#include
main()
{
inttestdev;//返回的设备描述符
inti;
charbuf[10];
testdev=open("/dev/test",O_RDWR);//打开设备并获取设备描述符
if(testdev==-1)
{
printf("Cann'topenfile\n");
exit(0);
}
read(testdev,buf,10);
for(i=0;i<10;i++)
printf("%d\n",buf[i]);
close(testdev);
}
编译运行,看看是不是打印出全1?
以上只是一个简单的演示。
真正实用的驱动程序要复杂的多,要处理如中断,DMA,I/Oport等问题。
这些才是真正的难点。
请看下节,实际情况的处理。
如何编写Linux操作系统下的设备驱动程序
三、设备驱动程序中的一些具体问题
1。
I/OPort。
和硬件打交道离不开I/OPort,老的ISA设备经常是占用实际的I/O端口,在linux下,操作系统没有对I/O口屏蔽,也就是说,任何驱动程序都可对任意的I/O口操作,这样就很容易引起混乱。
每个驱动程序应该自己避免误用端口。
有两个重要的kernel函数可以保证驱动程序做到这一点。
1)check_region(intio_port,intoff_set)
这个函数察看系统的I/O表,看是否有别的驱动程序占用某一段I/O口。
参数1:
io端口的基地址,
参数2:
io端口占用的范围。
返回值:
0没有占用,非0,已经被占用。
2)request_region(intio_port,intoff_set,char*devname)
//端口在Linux内核中映射成内存地址,用于串口驱动
如果这段I/O端口没有被占用,在我们的驱动程序中就可以使用它。
在使用之前,必须向系统登记,以防止被其他程序占用。
登记后,在/proc/ioports文件中可以看到你登记的io口。
参数1:
io端口的基地址。
参数2:
io端口占用的范围。
参数3:
使用这段io地址的设备名。
在对I/O口登记后,就可以放心地用inb(),outb()之类的函来访问了。
在一些PCI设备中,I/O端口被映射到一段内存中去,要访问这些端口就相当于访问一段内存。
经常性的,我们要获得一块内存的物理地址。
(复杂)
2。
内存操作
在设备驱动程序中动态开辟内存,不是用malloc,而是kmalloc,或者用get_free_pages直接申请页(Linux里每一页的大小是4K)。
释放内存用的是kfree,或free_pages。
请注意,kmalloc等函数返回的是物理地址!
注意,kmalloc最大只能开辟128k-16,16个字节是被页描述符结构占用了。
内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000(X86体系结构)以上的地址空间。
在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址。
另外,很多硬件需要一块比较大的连续内存用作DMA传送。
这块程序需要一直驻留在内存,不能被交换到文件中去。
但是kmalloc最多只能开辟128k的内存。
这可以通过牺牲一些系统内存的方法来解决。
3。
中断处理
同处理I/O端口一样,要使用一个中断,必须先向系统登记。
intrequest_irq(unsignedintirq,void(*handle)(int,void*,structpt_regs*),
unsignedintlongflags,constchar*device);
irq:
是要申请的中断号。
handle:
中断处理函数指针。
flags:
SA_INTERRUPT请求一个快速中断,0正常中断。
device:
设备名。
如果登记成功,返回0,这时在/proc/interrupts文件中可以看你请求的中断。
4。
一些常见的问题。
对硬件操作,有时时序很重要。
但是如果用C语言写一些低级的硬件操作的话,gcc往往会对你的程序进行优化,这样时序会发生错误。
如果用汇编写呢,gcc同样会对汇编代码进行优化,除非用volatile关键字修饰。
最保险的办法是禁止优化。
这当然只能对一部分你自己编写的代码。
如果对所有的代码都不优化,你会发现驱动程序根本无法装载。
这是因为在编译驱动程序时要用到gcc的一些扩展特性,而这些扩展特性必须在加了优化选项之后才能体现出来。
cd/dev
ls
mknodtestc2540//创建设备节点(设备名+设备类型+主设备号+从设备号)
lstest–l
cd/proc
ls
ps–e//打印进程所有信息
cd1727(进程)
ls(显示进程的一些信息)
catstat(查看进程的状态信息)
cd..//回到proc目录
catinterrups//显示系统所使用的中断
catiomem//显示内存空间映射(内核代码和内核数据,PCI设备)
catioports//显示I/O端口在内存中的映射(serial寄存器地址,Linux静止直接访问寄存器)
//X86体系:
内存编址和设备编址,我们采用统一编址的方式
catfilesystems//显示支持的文件系统
//proc(进程)、tmpfs(临时文件)、ext2/3(支持日志)、ramfs(在RAM运行的文件系统)
iso9660(光驱支持的文件系统)
catmeminfo//显示内存信息
catdevices//显示当前的设备
//ramdisk(内存块设备)、sd(U盘)
catmodules//系统运行加载了哪些模块(驱动程序)
查找Linux内核源码:
cd/home/s123/
ls
cduclinux/
ls
cduClinux-dist/
cdlinux-2.4.x///Linux内核源码
cddrivers///驱动程序
Linux内核启动流程:
BIOS(Bootloader)—>Linux内核的入口点
//linux-2.4.x/arch/armnommu/boot/compresed/head.S(整个Linux内核的入口点,是用汇编语言编写的,openwithgedit)
linux-2.4.x/init/main.c(整个程序的入口点,SourceInsight研读)
Ø在开发板上运行内核程序(main.c)
运行SecureCRT软件,通过COM1端口连接开发板:
move1000(在Flash中内核存放的位置)c008000(SDRAM位置)100000(大小)
runc008000
/etc/inittab//x86配置文件
cd/etc
ls
catrc//44B0配置文件
Ø蜂鸣器的驱动
第一步:
编写蜂鸣器驱动程序testdriver.c,并将该驱动文件放在/home/s123/uclinux/uClinux-dist/linux-2.4.x/drivers/char/testdriver.c
第二步:
修改makefile文件,该文件在/home/s123/uclinux/uClinux-dist/linux-2.4.x/drivers/char/
添加驱动到makefile中:
obj-$(CONFIG_TESTDRIVER)+=testdriver.o
第三步:
加载配置文件到config.in中
bool‘mytestdriver’CONFIG_TESTDRIVER
第四步:
将test_init(void)函数加载到char/mem.c文件中
声明:
#ifdefCONFIG_TESTDRIVER
externinttest_init(void)
#endif
调用:
#ifdefCONFIG_TESTDRIVER
test_init();
#endif
第五步:
在/home/s123/uclinux/uClinux-dist/vendors/Samsung/44B0/makefile创建设备节点
\
test(设备名),c(设备类型),254(主设备号),0(从设备号)
第六步:
启动Linux进入命令行状态:
cd/home/s123/
cduclinux/
cduClinux-dist///到uclinux顶层目录
makemenuconfig
选中“CustomizekernelSettings”//退出并保存,只配置内核
进入Characterdevices:
选中“mytestdriver(NEW)”//直接编译进内核,退出保存
makedep//创建依赖文件
makeclean
makelib_only//编译生成的库
makeuser_only
makeromfs
makeimage//生成镜像文件
make//生成最后的镜像文件
启动SecureCRT工具软件:
Connect选择com1端口,进入超级