制作第一个Linux驱动程序
学习目标:
- 编写一个简单的Linux驱动程序,实现应用程序能通过系统调用,调用相应驱动函数,驱动程序中仅打印信息,不实现一些特定功能
写第一个驱动程序需要以下几个步骤:
1)写出open、read等系统调用在进入内核空间中调用的对应函数xxx_open、xxx_read
2)定义file_operations类型结构体,用写好的xxx_open、xxx_read函数填充file_operations结构体成员
3)写一个xxx_init函数调用register_chardev函数注册填充好的file_operations类型结构体,目的时当调用它时将相关信息告诉内核
4)通过module_init()来修饰xxx_init入口函数
5)写驱动的xxx_exit出口函数,调用这个unregister_chrdev()函数卸载挂入内核中的file_operations类型结构体
6)通过module_exit()来修饰出口函数
7)模块许可证声明, 最常见的是以MODULE_LICENSE( "GPL v2" )来声明
1、创建一个驱动源文件first_drv.c
代码如下:
#include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/init.h> #include <linux/delay.h> #include <linux/device.h> #include <asm/uaccess.h> #include <asm/irq.h> #include <asm/io.h> //#include <asm/arch/regs-gpio.h> //#include <asm/hardware.h> int major;
static int first_drv_open(struct inode *inode, struct file *file); static ssize_t first_drv_read(struct file *file, char __user *buf, size_t count, loff_t *ppos); static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos); struct file_operations first_drv_fileop = { //---->① .owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */ .open = first_drv_open, .read = first_drv_read, .write = first_drv_write, };
/*inode表示具体文件,file结构用来追踪运行时的信息 */ static int first_drv_open(struct inode *inode, struct file *file) { printk("first_drv_open\n"); /*内核的打印用printk,而不是printf函数) return 0; } static ssize_t first_drv_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { printk("first_drv_read\n"); return 0; } static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { printk("first_drv_read\n"); return 0; } static int first_drv_init(void) { major = register_chrdev(0, "first_drv", &first_drv_fileop); //---->② return 0; } static void first_drv_exit(void) { unregister_chrdev(major, "first_drv"); //---->③ } module_init(first_drv_init); //---->④ module_exit(first_drv_exit); //---->⑤ MODULE_LICENSE("GPL");
①定义file_operations结构体,并填充相关成员函数,如open时,通过系统调用,最终会调用到.open函数指针指向函数
②注册file_operation结构体,将相关信息告诉内核,只有注册后file_operations内部成员才有效。register_chrdev函数第一项参数设置为0,让系统自动分配主设备号
③将相关信息从内核中去除
④修饰函数,当执行insmod时,将调用first_drv_init函数(此例中,即调用register_chrdev函数)
⑤修饰函数,当执行rmmod时,将调用first_drv_exit函数(此例中,即调用unregister_chrdev函数)
2、创建一个编译驱动的Makefile
KERN_DIR = /home/book/self_learn/01_linux_develop/02_embeded_dir/02_kernel/linux-3.4.2 #----->① all: make -C $(KERN_DIR) M=`pwd` modules #---->② clean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf modules.order obj-m += first_drv.o #----->③
①编译驱动时依赖内核存放的目录,内核必须是已经编译好的
②make -C切换到内核目录,调用内核目录的Makefile;M='pwd',当用户需要以内核为基础编译一个外部模块,需要将M='pwd'加入其中,表示到驱动的目录中查找编译源代码,modules是一个参数,就是告诉内核在编译是将驱动编译成模块,不编译进内核
③表示将first_drv.o编译成模块
3、执行make,编译生成first_drv.ko
4、将first_drv.ko拷贝到跟文件系统并加载驱动模块
执行insmod命令将first_drv.ko加载到内核
5、手动创建设备节点
在注册file_operations结构体时,我们选择了让系统自动分配主设备号,在创建设备节点之前,应先获取系统自动分配的主设备号。执行cat /proc/devices 命令可以看出系统自动分配主设备号
系统自动分配的主设备号时252,后面跟的是设备名称,设备名称是由register_chrdev传入的第二个参数决定的。获取主设备号之后,执行 mknod /dev/first c 252 0 命令在dev目录中创建设备节点
6、编写应用程序,进行驱动测试
应用代码first_drv_test.c:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char **argv) { int fd; int val = 1; fd = open("/dev/first", O_RDWR); if(fd == -1) { printf("can't open...\n"); exit(EXIT_FAILURE); } read(fd, &val, sizeof(val)); exit(EXIT_SUCCESS); }
执行arm-linux-gcc -o first_drv_test first_drv_test.c编译测试应该程序,并将编译成功的应用程序拷贝到网络文件系统中,执行应用程序
运行成功,由打印结果可以看出,应用程序中的open函数和read函数调用了对应驱动函数中的first_open和first_read,第一个驱动程序测试成功!
7、改进first_drv.c驱动程序
由上述操作结果可以看出,每加载一次驱动程序都要手动在/dev目录中创将相应设备节点,这样做太麻烦了。可以使用自动创建设备节点,Linux有udev、mdev的机制,而我们的ARM开发板上移植的busybox有mdev机制,然后mdev机制会通过/sys目录中class类来找到相应类的驱动设备来自动创建设备节点 (前提需要有mdev)
1)首先创建一个class设备类,class是C语言中的一个结构体,是软件开发的一个设备的高级视图,它抽象出低级的实现细节,然后在class类下,创建一个class_device,即类下面创建类的设备:
static struct class *first_drv_class; static struct class_device *first_drv_class_dev;
2)在first_drv_init后面添加如下代码
//创建类,它会在sys/class目录下创建firstdrv_class这个类
firstdrv_class= class_create(THIS_MODULE,"firstdrv"); //在该类下创建设备 firstdrv_class_devs=class_device_create(firstdrv_class,NULL,MKDEV(major,0),NULL,"xyz");
注意:在高版本的linux内核中class_device_create用device_create函数代替
3)同理,在first_drv_exit中添加卸载类和设备的函数,在卸载驱动程序后,自动删除创建的设备节点
class_device_unregister(firstdrv_class_devs); //注销类设备,与class_device_create对应 class_destroy(firstdrv_class); //注销类,与class_create对应
4)按照上述过程,重新编译驱动程序,并将编译好的驱动程序加载到内核(注意在加载新驱动程序时,先卸载之前驱动程序,并删除手动创建的设备节点)
由此可见,第一个简单驱动程序创建成功,值得注意的是,自动创建设备节点,必须在mdev机制使能时才能正常使用。