https://m.toutiao.com/is/2M8V1m3/?=Linux中级
驱动就是对底层硬件设备的操作进行封装,并向上层提供函数接口。
设备分类: linux系统将设备分为3类:字符设备、块设备、网络设备。
我们来举一个例子来说一下整体的调用过程
在这里插入图片描述
我们写驱动无非就是做添加驱动: 添加驱动做哪些事呢?
设备名
设备号
设备驱动函数 (操作寄存器 来驱动 IO口)
==综上所述== 如果想要打开dev下面的pin4引脚,过程是:用户态调用open(“/de/pin4”,O_RDWR),对于内核来说,上层调用open函数会触发一个软中断(系统调用专用,中断号是0x80,0x80代表发生了一个系统调用),系统进入内核态,并走到system_call,可以认为这个就是此软中断的中断服务程序入口,然后通过传递过来的系统调用号来决定调用相应的系统调用服务程序(在这里是调用VFS中的sys_open)。sys_open会在内核的驱动链表里面根据设备名和设备号查找到相关的驱动函数(每一个驱动函数是一个节点),**==驱动函数里面有通过寄存器操控IO口的代码,进而可以控制IO口实现相关功能==**。
用户态:
内核态:
==驱动链表==
管理所有设备的驱动,添加或查找
添加是发生在我们编写完驱动程序,加载到内核。
查找是在调用驱动程序,由应用层用户空间去查找使用open函数。驱动插入链表的顺序由设备号检索,就是说主设备号和次设备号除了能区分不同种类的设备和不同类型的设备,还能起到将驱动程序加载到链表的某个位置,在下面介绍的驱动代码的开发无非就是添加驱动(添加设备号、设备名和设备驱动函数)和调用驱动。
补充:
字符设备驱动工作原理 在linux的世界里一切皆文件,所有的硬件设备操作到应用层都会被抽象成文件的操作。我们知道如果应用层要访问硬件设备,它必定要调用到硬件对应的驱动程序。Linux内核有那么多驱动程序,应用怎么才能精确的调用到底层的驱动程序呢?
==必须知道的知识:==
(1) 当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备),还会分配一个struct file结构体。
(2) 根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备都有一个struct cdev结构体。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数接口。
(3) 找到struct cdev结构体后,linux内核就会将struct cdev结构体所在的内存空间首地址记录在struct inode结构体i_cdev成员中,将struct cdev结构体中的记录的函数操作接口地址记录在struct file结构体的f_ops成员中。
(4) 任务完成,VFS层会给应用返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层应用程序就可以通过fd找到struct file,然后在struct file找到操作字符设备的函数接口file_operation了。
其中,cdev_init和cdev_add在驱动程序的入口函数中就已经被调用,分别完成字符设备与file_operation函数操作接口的绑定,和将字符驱动注册到内核的工作。
#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>void main(){ int fd,data; fd = open('/dev/pin4',O_RDWR); if(fd<0){ printf('open fail\n'); perror('reson:'); } else{ printf('open successful\n'); } fd=write(fd,'1',1);}
-内核驱动 **==最简单的字符设备驱动框架==**:
#include <linux/fs.h> //file_operations声明#include <linux/module.h> //module_init module_exit声明#include <linux/init.h> //__init __exit 宏定义声明#include <linux/device.h> //class devise声明#include <linux/uaccess.h> //copy_from_user 的头文件#include <linux/types.h> //设备号 dev_t 类型声明#include <asm/io.h> //ioremap iounmap的头文件static struct class *pin4_class; static struct device *pin4_class_dev;static dev_t devno; //设备号,devno是用来接收创建设备号函数的返回值,销毁的时候需要传这个参数static int major =231; //主设备号static int minor =0; //次设备号static char *module_name='pin4'; //模块名//led_open函数static int pin4_open(struct inode *inode,struct file *file){ printk('pin4_open\n'); //内核的打印函数和printf类似 return 0;}//led_write函数static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos){ printk('pin4_write\n'); //内核的打印函数和printf类似 return 0;}//将上面的函数赋值给一个结构体中,方便下面加载到到驱动链表中去static struct file_operations pin4_fops = {//static防止其他文件也有同名pin4_fops//static限定这个结构体的作用,仅仅只在这个文件。 .owner = THIS_MODULE, .open = pin4_open, .write = pin4_write,};/*上面的代码等同于以下代码(但是在单片机keil的编译环境里面不允许以上写法):里面的每个pin4_fops结构体成员单独赋值static struct file_operations pin4_fops; pin4_fops.owner = THIS_MODULE; pin4_fops.open = pin4_open; pin4_fops.write = pin4_write;*///static限定这个结构体的作用,仅仅只在这个文件。int __init pin4_drv_init(void) //真实的驱动入口{ int ret; devno = MKDEV(major,minor); //2. 创建设备号 ret = register_chrdev(major, module_name,&pin4_fops); //3. 注册驱动 告诉内核,把这个驱动加入到内核驱动的链表中 pin4_class=class_create(THIS_MODULE,'myfirstdemo');//由代码在dev下自动生成设备,创建一个类 pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //创建设备文件,先有上面那一行代码,创建一个类然后这行代码,类下面再创建一个设备。 return 0;}void __exit pin4_drv_exit(void){ device_destroy(pin4_class,devno);//先销毁设备 class_destroy(pin4_class);//再销毁类 unregister_chrdev(major, module_name); //卸载驱动}module_init(pin4_drv_init); //入口,内核加载驱动的时候,这个宏(不是函数)会被调用,去调用pin4_drv_init这个函数module_exit(pin4_drv_exit);MODULE_LICENSE('GPL v2');
手动创建设备名
驱动模块代码编译(模块的编译需要配置过的内核源码,编译、连接后生成的内核模块后缀为.ko,编译过程首先会到内核源码目录下,读取顶层的Makefile文件,然后再返回模块源码所在目录。):
#include <linux/fs.h> //file_operations声明#include <linux/module.h> //module_init module_exit声明#include <linux/init.h> //__init __exit 宏定义声明#include <linux/device.h> //class devise声明#include <linux/uaccess.h> //copy_from_user 的头文件#include <linux/types.h> //设备号 dev_t 类型声明#include <asm/io.h> //ioremap iounmap的头文件static struct class *pin4_class;static struct device *pin4_class_dev;static dev_t devno; //设备号static int major =231; //主设备号static int minor =0; //次设备号static char *module_name='pin4'; //模块名//led_open函数static int pin4_open(struct inode *inode,struct file *file){ printk('pin4_open\n'); //内核的打印函数和printf类似 return 0;}//read函数static int pin4_read(struct file *file,char __user *buf,size_t count,loff_t *ppos){ printk('pin4_read\n'); //内核的打印函数和printf类似 return 0;}//led_write函数static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos){ printk('pin4_write\n'); //内核的打印函数和printf类似 return 0;}static struct file_operations pin4_fops = { .owner = THIS_MODULE, .open = pin4_open, .write = pin4_write, .read = pin4_read,};//static限定这个结构体的作用,仅仅只在这个文件。int __init pin4_drv_init(void) //真实的驱动入口{ int ret; devno = MKDEV(major,minor); //创建设备号 ret = register_chrdev(major, module_name,&pin4_fops); //注册驱动 告诉内核,把这个驱动加入到内核驱动的链表中 pin4_class=class_create(THIS_MODULE,'myfirstdemo');//让代码在dev下自动>生成设备 pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //创建设备文件 return 0;}void __exit pin4_drv_exit(void){ device_destroy(pin4_class,devno); class_destroy(pin4_class); unregister_chrdev(major, module_name); //卸载驱动}module_init(pin4_drv_init); //入口,内核加载驱动的时候,这个宏会被调用,去调用pin4_drv_init这个函数module_exit(pin4_drv_exit);MODULE_LICENSE('GPL v2');
编译过程中,经历了这样的步骤:
先进入Linux内核所在的目录,并编译出pin4drive.o文件
运行MODPOST会生成临时的pin4drive.mod.c文件, 而后根据此文件编译出pin4drive.mod.o,
之后连接pin4drive.o和pin4drive.mod.o文件得到模块目标文件pin4drive.ko,
最后离开Linux内核所在的目录。
将pin4test.c (上层调用代码) 进行 交叉编译后发送给树莓派,就可以看到pi目录下存在发送过来的.ko文件和pin4test这两个文件,如下图所示:
然后使用指令:sudo insmod pin4drive.ko加载内核驱动(相当于通过insmod调用了module_init这个宏,然后将整个结构体加载到驱动链表中) 加载完成后就可以在dev下面看到名字为pin4的设备驱动(这个和驱动代码里面static char *module_name='pin4'; //模块名这行代码有关),设备号也和代码里面相关。
lsmod可以查看驱动已经装进去了。
然后再次执行pin4test表面上看没有任何信息输出,其实内核里面有打印信息只是上层看不到 如果想要查看内核打印的信息可以使用指令:dmesg |grep pin4。 如下图所示:表示驱动调用成功
在装完驱动后可以使用指令:sudo rmmod +驱动名(不需要写ko)将驱动卸载。
联系客服