【转】在Linux下写⼀个简单的驱动程序
转⾃:wwwblogs/amanlikethis/p/4914510.html
本⽂⾸先描述了⼀个可以实际测试运⾏的驱动实例,然后由此去讨论Linux下驱动模板的要素,以及Linux上应⽤程序到驱动的执⾏过程。相信这样由浅⼊深、由具体实例到抽象理论的描述更容易初学者⼊⼿Linux驱动的⼤门。
⼀、⼀个简单的驱动程序实例
驱动⽂件hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#define HELLO_MAJOR 231
#define DEVICE_NAME "HelloModule"
static int hello_open(struct inode *inode, struct file *file){
printk(KERN_EMERG "hello open.\n");
return 0;
}
static ssize_t hello_write(struct file *file, const char __user * buf, size_t count, loff_t *ppos){
printk(KERN_EMERG "hello write.\n");
return 0;
}
static struct file_operations hello_flops = {
.
owner = THIS_MODULE,
.open = hello_open,
.write = hello_write,
};
static int __init hello_init(void){
int ret;
ret = register_chrdev(HELLO_MAJOR,DEVICE_NAME, &hello_flops);
if (ret < 0) {
printk(KERN_EMERG DEVICE_NAME " can't register major number.\n");
return ret;
}
printk(KERN_EMERG DEVICE_NAME " initialized.\n");
return 0;
}
static void __exit hello_exit(void){
unregister_chrdev(HELLO_MAJOR, DEVICE_NAME);
printk(KERN_EMERG DEVICE_NAME " removed.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
驱动⽂件主要包括函数hello_open、hello_write、hello_init、hello_exit,测试案例中并没有赋予驱动
模块具有实际意义的功能,只是通过打印⽇志的⽅式告知控制台⼀些调试信息,这样我们就可以把握驱动程序的执⾏过程。
在使⽤printk打印的时候,在参数中加上了“KERN_EMERG”可以确保待打印信息输出到控制台上。由于printk打印分8个等级,等级⾼的被打印到控制台上,⽽等级低的却输出到⽇志⽂件中。
编译驱动所需的Makefile
ifneq ($(KERNELRELEASE),)
MODULE_NAME = hellomodule
$(MODULE_NAME)-objs := hello.o
obj-m := $(MODULE_NAME).o
else
KERNEL_DIR = /lib/modules/`uname -r`/build
MODULEDIR := $(shell pwd)
.PHONY: modules
default: modules
modules:
make -C $(KERNEL_DIR) M=$(MODULEDIR) modules
clean distclean:
rm -f *.o *.mod.c .*.*.cmd *.ko
rm -rf .tmp_versions
endif
编译驱动⽂件需要⼀个合适的makefile,因为编译驱动的时候需要知道内核头⽂件,编译规则等。
测试驱动的上层应⽤代码hellotest.c
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
int fd;
int val = 1;
fd = open("/dev/hellodev", O_RDWR);
if(fd < 0){
printf("can't open!\n");
}
write(fd, &val, 4);
return 0;
}
上层测试案例中,⾸先打开设备⽂件,然后向设备中写⼊数据。如此,则会调⽤驱动中对应的xxx_open和xxx_write函数,通过驱动程序的打印信息可以判断是否真的如愿执⾏了对应的函数。
⼆、驱动实例测试
测试的⽅法整体来说就是,编译驱动和上层测试应⽤;加载驱动,通过上层应⽤调⽤驱动;最后,卸载驱动。
1、编译驱动
#make
make命令,直接调⽤Makefile编译hello.c,最后会⽣成“hellomodule.ko”。
2、编译上层应⽤
#gcc hellotest.c -o hellotest
通过这条命令,就能编译出⼀个上层应⽤hellotest。
3、加载驱动
#insmod hellomodule.ko
insmod加载驱动的时候,会调⽤函数hello_init(),打印的调试信息如下。
此外,在"/proc/devices"中可以看到已经加载的模块。
4、创建节点
虽然已经加载了驱动hellomodule.ko,⽽且在/proc/devices⽂件中也看到了已经加载的模块HelloModule,但是这个模块仍然不能被使⽤,因为在设备⽬录/dev⽬录下还没有它对应的设备⽂件。所以,需要创建⼀个设备节点。
#mknod /dev/hellodev c 231 0
在/proc/devices中看到HelloModule模块的主设备号为231,创建节点的时候就是将设备⽂件/dev/hellodev与主设备号建⽴连接。这样在应⽤程序操作⽂件/dev/hellodev的时候,就会定位到模块HelloModule。
/proc/devices 与 /dev的区别
/proc/devices中的设备是驱动程序⽣成的,它可产⽣⼀个major供mknod作为参数。这个⽂件中的内容显⽰的是当前挂载在系统的模块。当加载驱动
HelloModule的时候,并没有⽣成⼀个对应的设备⽂件来对这个设备进⾏抽象封装,以供上层应⽤访问。
/dev下的设备是通过mknod加上去的,⽤户通过此设备名来访问驱动。我以为可以将/dev下的⽂件看做是硬件模块的⼀个抽象封装,Linux下所有的设备都以⽂
件的形式进⾏封装。
5、上层应⽤调⽤驱动
#./hellotest
hellotest应⽤程序先打开⽂件“/dev/hellodev”,然后向此⽂件中写⼊⼀个变量val。期间会调⽤底层驱动中的hello_open和hello_write函数,hellotest的运⾏结果如下所⽰。
6、卸载驱动
#rmmod hellomodule
insmod卸载驱动的时候,会调⽤函数hello_exit(),打印的调试信息如下。
总结⼀个模块的操作流程:
(1)通过insmod命令注册module
(2)通过mknod命令在/dev⽬录下建⽴⼀个设备⽂件"xxx",并通过主设备号与module建⽴连接
(3)应⽤程序层通过设备⽂件/dev/xxx对底层module进⾏操作
三、驱动模板
从宏观上把握了驱动程序的框架,然后再从细节上完善驱动的功能,这是开发驱动程序的⼀般步骤。驱动模板必备要素有头⽂件、初始化函数、退出函数、版权信息,常⽤的扩展要素是增加⼀些功能函数完善底层驱动的功能。
1、头⽂件
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
init.h 定义了驱动的初始化和退出相关的函数
kernel.h 定义了经常⽤到的函数原型及宏定义
module.h 定义了内核模块相关的函数、变量及宏
2、初始化函数
static int __init hello_init(void){
linux下gcc编译的四个步骤int ret;
ret = register_chrdev(HELLO_MAJOR,DEVICE_NAME,&hello_flops);
if (ret < 0) {
printk(KERN_EMERG DEVICE_NAME " can't register major number.\n");
return ret;
}
printk(KERN_EMERG DEVICE_NAME " initialized.\n");
return 0;
}
module_init(hello_init);
当加载驱动到内核的时候,这个初始化函数就会被⾃动执⾏。
初始化函数顾名思义是⽤来初始化模块的,常⽤的功能是通过register_chrdev来注册函数。内核分配了⼀块内存(数组)专门⽤来存放字符设备的函数集,register_chrdev 函数会在这个数组的HELLO_MAJOR位置将hello_flops中的内容进⾏填充,也就是将HelloModule的功能函数地址注册到设备管理内存集中。
形象的⽐喻好像是操作系统提供了很多的⾐服架,注册设备就好像是把⼀个⾐服挂到某⼀个⾐服架上。⾐服上有许多⼝袋,就好像每⼀个模块有许多功能程序接⼝。显然,如果想使⽤设备的某个功能,就可以先到对应的⾐服架,然后到相应的⼝袋,去调⽤对应的函数,执⾏动作。
3、退出函数
static void __exit hello_exit(void){
unregister_chrdev(HELLO_MAJOR, DEVICE_NAME);
printk(KERN_EMERG DEVICE_NAME " removed.\n");
}
module_exit(hello_exit);
当卸载驱动的时候,退出函数便会⾃动执⾏,完成⼀些列清楚⼯作。
在加载驱动的时候,我们向设备管理内存集中注册了该模块的相关功能函数。当卸载驱动的时候,就有必要将这个模块占⽤的内存空间清空。这样当其他的设备注册的时候
便有更多的空间可以选择。
形象的⽐喻是,当卸载驱动的时候,就是把⾐服从⾐服架上取下来,这样⾐服架就腾空了。
4、版权信息
MODULE_LICENSE("GPL");
Linux内核是按照GPL发布的,同样Linux的驱动程序也要提供版权信息,否则当加载到内核中系统会给出警告信息。
5、功能函数
static int hello_open(struct inode *inode, struct file *file){
printk(KERN_EMERG "hello open.\n");
return 0;
}
static int hello_write(struct file *file, const char __user * buf, size_t count, loff_t *ppos){
printk(KERN_EMERG "hello write.\n");
return 0;
}
static struct file_operations hello_flops = {
.owner = THIS_MODULE,
.open = hello_open,
.write = hello_write,
};
功能函数虽然不是⼀个驱动模板所必须的,但是⼀个有实际意义的驱动程序⼀定包含功能函数。功能函数实际上定义了这个驱动程序为⽤户提供了哪些功能,也就是⽤户可以对⼀个硬件设备可以进⾏哪些操作。
常见的功能函数有xxx_open()、xxx_write()、xxx_read()、xxx_ioctl()、xxx_llseek()等。当上层应⽤调⽤open()、write()、read()、ioctl()、llseek()等这些函数的时候,经过层层调⽤最后到达底层,调⽤相应的功能函数。结构体file_operations中的成 View Code
四、从上层应⽤到底层驱动的执⾏过程
1、Linux系统的分层结构
Linux系统的分层结构为:应⽤层 ----> 库 ----> 内核 ----> 驱动程序 ----> 硬件设备。
2、从上层应⽤到底层驱动的执⾏过程
以“open("/dev/hellodev", O_RDWR)”函数的执⾏过程为例来说明。
(1)应⽤程序使⽤库提供的open函数打开代表hellodev的设备⽂件。
(2)库根据open函数传⼊的参数执⾏swi指令,这条指令会引起CPU异常,从⽽进⼊内核。
(3)内核的异常处理函数根据这些参数到相应的驱动程序。
(4)执⾏相应的驱动程序。
(5)返回⼀个⽂件句柄给库,进⽽返回给应⽤程序。
3、驱动程序的执⾏特点
与应⽤程序不同,驱动程序从不主动运⾏,它是被动的:根据应⽤程序的要求进⾏初始化,根据应⽤程序的要求进⾏读写。驱动程序加载进内核,只是告诉内核“我在这⾥,
我能做这些⼯作”,⾄于这些⼯作何时开始,则取决于应⽤程序。
驱动程序运⾏于“内核空间”,它是系统“信任”的⼀部分,驱动程序的错误有可能导致整个系统的崩溃。
参考资料:
《嵌⼊式Linux应⽤开发完全⼿册》
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论