Linux驱动开发十.中断——4.中断的下半部
在前面讲中断等过程,我们通过request_irq申请中断,然后注册了中断服务函数。事件触发中断以后所有待处理的任务都在中断服务函数中执行。一般来说,中断服务函数会在中断请求关闭的条件的执行,一般简单的事件这样做没问题,满足外部中断讲究一个快进快出的原则,一旦要处理复杂的事物时,中断禁止的时间一长,很有可能导致CPU不能响应其他的中断请求,这种方式就不能满足我们的需求了。总之就是Linux内核需要尽快的处理完当前中断请求,然后尽可能的把其他要处理的事物向后推迟。比方有一个数据从网线进来了,中断控制器收到中断请求信号,Linux内核对中断的响应只是标志有数据到来,然后跳出中断让CPU恢复到其中断前的运行状态,其余类似读取数据、处理数据的事件会在稍后合适的时机执行。为了满足这一过程所以这里要引入一个叫做上半部和下半部的概念。
上半部与下半部
上半部一般用来响应中断,也就是我们前面一直做的中断服务函数,这些函数处理任务的过程一般来说都会比较快,不会占用过多的资源或者消耗很长的时间。
下半部一般是将那些中断中需要处理比较复杂、耗时较久的任务提取出来放在下半部执行,那样就保证了中断处理函数可以快进快出。
所以,Linux内核就会将中断分成上半部和下半部两个过程,那些对时间敏感、执行速度快的操作可以放在中断处理函数中,也就是上半部,剩下所有的工作都放到下半部去执行。这里的分界线比较模糊,一般遵循下面的思路:
- 如果需要处理的任务不希望被其他的中断打断, 该任务应该在上半部
- 待处理的任务对时间敏感,放在上半部
- 待处理的任务和硬件有关,放在上半部
- 其余任务优先考虑放在下半部
总之,上半部和下半部的整体的框架如下图所示
下半部处理机制
因为内核将中断分为两个部分,那么该以什么形式来组织这两个部分呢?因为上半部主要用来响应中断并做标识,下半部的组织方式是我们需要考虑的,这就是下半部处理机制。在早期的Linux内核中,是通过一个叫做“Bottom half"的机制来实现下半部的(BH机制),后来又延伸出了软中断、tasklet来替代BH。 因为BH有两个缺点:
- 在任意时刻,系统只能有1个CPU执行BH代码,以防止两个或多个CPU同时来执行BH而互相干扰,所以BH代码是严格串行化的。
- BH函数不允许嵌套
所以在2.6以后的Linux内核版本中,BH已经被取代了。
软中断
软中断是结合了硬件中断等概念,用软件的方式模拟来实现异步执行效果。在很多地方软中断和”信号“有些类似,通常由内核或进程产生一个中断。软中断是在BH基础上的升级,用以适应多CPU、多线程的中断处理。其主要目的是实现系统API调用的方法。Linux内核使用结构体softirq_action来表示软中断,这个结构体的定义在include/linux/interrupt.h路径下
/* softirq mask and active fields moved to irq_cpustat_t in * asm/hardirq.h to get better cache usage. KAO */ struct softirq_action { void (*action)(struct softirq_action *); };
并且在文件kernel/softirq.c里定义了10个软中断
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
soft_irq_vec是一个数组来描述软中断,后面的NR_SFOTIRQS是一个枚举类型的变量,定义在include/linux/interrupt.h文件里
enum { HI_SOFTIRQ=0, //高优先级软中断 TIMER_SOFTIRQ, //定时器软中断 NET_TX_SOFTIRQ, //网络数据发送软中断 NET_RX_SOFTIRQ, //网络数据接收软中断 BLOCK_SOFTIRQ, BLOCK_IOPOLL_SOFTIRQ, TASKLET_SOFTIRQ, //tasklet软中断 SCHED_SOFTIRQ, //调度软中断 HRTIMER_SOFTIRQ, //高精度定时器软中断 RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ NR_SOFTIRQS };
一共10种。内核的开发者们不建议我们擅自增加软中断的数量,如果需要新的软中断,尽可能把它们实现为基于软中断的tasklet形式。与上面的枚举值相对应,内核定义了一个softirq_action的结构数组,每种软中断对应上面数组中的一项。因为一共有是个软中断,所以NO_SOFTIRQS的值就为10(这里我没搞太明白),即softirq_vec数组里有10个元素。softirq_action结构体中的action成员变量就是软中断的服务函数。softirq_vec是个全局数组,所以所有的CPU都可以访问到,每个CPU都有自己的触发和控制机制,并且只执行自己触发的软中断。想要使用软中断,必须先使用open_softirq函数注册对应软中断的中断处理函数。要注意的是软中断必须在编译的时候静态注册,也就是我们在编译内核的时候就要把这些函数编译好。明显这个方法不是很实用。
tasklet
因为软中断需要我们在编译内核的时候进行编译,使用很不方便。不过软中断里提供了一个接口供我们使用——TASKLET_SOFTIRQ,我们只需写好中断处理函数,然后注册给tasklet就可以直接使用中断。所以tasklet就是利用软中断的原理来实现的另外一种下半部机制。先看下内核里对tasklet的简单描述
1 /* Tasklets --- multithreaded analogue of BHs. 2 3 Main feature differing them of generic softirqs: tasklet 4 is running only on one CPU simultaneously. 5 6 Main feature differing them of BHs: different tasklets 7 may be run simultaneously on different CPUs. 8 9 Properties: 10 * If tasklet_schedule() is called, then tasklet is guaranteed 11 to be executed on some cpu at least once after this. 12 * If the tasklet is already scheduled, but its execution is still not 13 started, it will be executed only once. 14 * If this tasklet is already running on another CPU (or schedule is called 15 from tasklet itself), it is rescheduled for later. 16 * Tasklet is strictly serialized wrt itself, but not 17 wrt another tasklets. If client needs some intertask synchronization, 18 he makes it with spinlocks. 19 */
Linux内核使用tasklet_struct结构体来表示tasklet(代码还在那个interrupt.h文件里)
struct tasklet_struct { struct tasklet_struct *next; unsigned long state; atomic_t count; void (*func)(unsigned long); unsigned long data; };
其中的func就是tasklet要执行的函数,相当于中断处理函数。如果想要使用tasklet机制来实现中断下半部,必须先定义一个tasklet,再用相关函数进行初始化
extern void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
其中t就是要初始化的tasklet,func就是要处理的函数,data是要传递给func的参数。除此以外interrupt.h里还提供了一个宏来完成tasklet的初始化
#define DECLARE_TASKLET(name, func, data) \ struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
参数name就是要定义的tasklet的名字,这个名字是一个tasklet_struct类型的变量,func是tasklet的处理函数,data是func的参数。完成初始化的tasklet会在中断在上半部响应以后,通过在上半部使用tasklet_schedule函数使tasklet在合适的时间运行。
static inline void tasklet_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) __tasklet_schedule(t); }
所以整个tasklet的框架应该大致是这样的
1 /*定义tasklet*/ 2 struct tasklet_struct testtasklet; 3 4 /*tasklet处理函数*/ 5 void testtasklet_func(unsigned long data){ 6 /*需要tasklet具体处理的内容*、 7 } 8 9 /*中断处理函数*/ 10 irqreturn_t test_handler(int irq, void *dev){ 11 tasklet_schedule(&testtasklet); //调度tasklet 12 } 13 14 /*驱动入口*/ 15 static int __init dev_init(void){ 16 //设备初始化 17 18 task_init(&testtasklet,testtasklet_func,data); //tasklet初始化 19 20 request_irq(irqnum,test_handler,"irqname",&dev);//中断请求 21 }
我们可以按照上面的思路修改下前面那个按键触发的驱动。
/** * @file irq.c * @author your name (you@domain.com) * @brief tasklet测试程序 * @version 0.1 * @date 2022-07-27 * * @copyright Copyright (c) 2022 * */ #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/io.h> #include <linux/types.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/of.h> #include <linux/of_address.h> #include <linux/of_irq.h> #include <linux/gpio.h> #include <linux/of_gpio.h> #include <linux/irq.h> #include <linux/interrupt.h> #define DEVICE_CNT 1 #define DEVICE_NAME "imx6uirq" #define KEY_NUM 1 #define KEY0VALUE 0x01 #define INVALKEYS 0xFF /** * @brief 按键中断结构体 * */ struct irq_keydesc { int gpio; //io编号 int irqnum; //中断号 unsigned char value; //键值 char name[10]; //按键名字 irqreturn_t (*handler)(int,void*); //中断处理函数 struct tasklet_struct tasklet; }; /** * @brief 设备结构体 * */ struct new_dev { dev_t dev_id; int major; int minor; struct class *class; struct device *device; struct cdev cdev; struct device_node *dev_nd; int dev_gpio; struct irq_keydesc irqkey[KEY_NUM]; //按键描述数组 struct timer_list timer; //定时器 int timer_per; atomic_t keyvalue; atomic_t keyreleased; //1表示1次有效按键并被释放,0表示未被释放 }; struct new_dev new_dev; static int new_dev_open(struct inode *inode, struct file *filp) { filp->private_data = &new_dev; /* 设置私有数据 */ return 0; } static ssize_t new_dev_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt) { int ret = 0; unsigned char keyvalue; unsigned char keyreleased; struct new_dev *dev = filp->private_data; //私有数据 keyvalue = atomic_read(&dev->keyvalue); //获取按键状态标志及按键值 keyreleased = atomic_read(&dev->keyreleased); if(keyreleased){ //出现按键被按下并释放的过程 if(keyvalue&0x80){ //获取最高位,如果为1表示出现过按键被按下后释放掉状态,是有效状态 keyvalue &= ~0x80; ret = copy_to_user(buf,&keyvalue,sizeof(keyvalue)); } else{ goto data_err; } atomic_set(&dev->keyreleased,0); //恢复keyreleased的状态 } else{ goto data_err; //返回负数,在用户态跳出循环 } return ret; data_err: return -EINVAL; }; /** * @brief 文件操作集合 * */ static const struct file_operations key_fops = { .owner = THIS_MODULE, // .write = new_dev_write, .open = new_dev_open, .read = new_dev_read, // .release = new_dev_release, }; /** * @brief 外部中断处理函数 * * @param irq * @param dev_id * @return irqreturn_t */ static irqreturn_t key0_handle_irq(int irq, void *dev_id) { struct new_dev *dev = dev_id; tasklet_schedule(&dev->irqkey[0].tasklet); return IRQ_HANDLED; } /** * @brief tasklet调用函数 * * @param data */ static void key_tasklet(unsigned long data) { struct new_dev *dev = (struct new_dev*)data; dev->timer.data = data; mod_timer(&dev->timer,jiffies + msecs_to_jiffies(10)); //激活定时器 } /** * @brief Construct a new timer func object * 定时器调用函数 * @param arg */ timer_func(unsigned long arg){ int value = 0; struct new_dev *dev =(struct new_dev*)arg; value = gpio_get_value(dev->irqkey[0].gpio); if(value == 0){ //按下 atomic_set(&dev->keyvalue,dev->irqkey[0].value); } else{ //释放 atomic_set(&dev->keyvalue,0x80|(dev->irqkey[0].value)); //将最高位置一,表示按钮释放 atomic_set(&dev->keyreleased,1); } } static int dev_gpio_init(struct new_dev *dev) { int ret = 0; int i = 0; //搜索设备树节点 dev->dev_nd = of_find_node_by_path("/key"); if(dev->dev_nd == NULL){ printk("can't find device key\r\n"); ret = -EINVAL; goto fail_nd; } for(i=0;i<KEY_NUM;i++) { dev->irqkey[i].gpio = of_get_named_gpio(dev->dev_nd,"key-gpios",i); //多个按键获取 if(dev->irqkey[i].gpio<0){ ret = -EINVAL; goto fail_gpio_num; } ret = gpio_request(dev->irqkey[i].gpio,dev->irqkey[i].name); if(ret){ ret = -EBUSY; goto fail_gpio_request; } gpio_direction_input(dev->irqkey[i].gpio); dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio); //获取中断号 // dev->irqkey[i].irqnum = irq_of_parse_and_map(dev->dev_nd,i) //方法2获取中断号 } dev->irqkey[0].handler = key0_handle_irq; dev->irqkey[0].value = KEY0VALUE; for(i=0;i<KEY_NUM;i++){ memset(dev->irqkey[i].name,0,sizeof(dev->irqkey[i].name)); sprintf(dev->irqkey[i].name,"KEY%d",i); //将格式化数据写入字符串中 ret = request_irq(dev->irqkey[i].irqnum, //中断号 key0_handle_irq, //中断处理函数 IRQ_TYPE_EDGE_RISING|IRQ_TYPE_EDGE_FALLING, //中断处理函数 dev->irqkey[i].name, //中断名称 dev //设备结构体 ); if(ret){ printk("irq %d request err\r\n",dev->irqkey[i].irqnum); goto fail_irq; } tasklet_init(&dev->irqkey[i].tasklet,key_tasklet,(unsigned long)dev); } //此处不设置定时值,防止定时器add后直接运行 init_timer(&dev->timer); dev->timer.function = timer_func; return 0; fail_gpio_request: fail_irq: for(i=0; i<KEY_NUM;i++){ gpio_free(dev->irqkey[i].gpio); } fail_gpio_num: fail_nd: return ret; } static int __init key_init(void){ int ret = 0; // unsigned int val = 0; //申请设备号 new_dev.major = 0; if(new_dev.major){ //手动指定设备号,使用指定的设备号 new_dev.dev_id = MKDEV(new_dev.major,0); ret = register_chrdev_region(new_dev.dev_id,DEVICE_CNT,DEVICE_NAME); } else{ //设备号未指定,申请设备号 ret = alloc_chrdev_region(&new_dev.dev_id,0,DEVICE_CNT,DEVICE_NAME); new_dev.major = MAJOR(new_dev.dev_id); new_dev.minor = MINOR(new_dev.dev_id); } printk("dev id geted!\r\n"); if(ret<0){ //设备号申请异常,跳转至异常处理 goto faile_devid; } //字符设备cdev初始化 new_dev.cdev.owner = THIS_MODULE; cdev_init(&new_dev.cdev,&key_fops); //文件操作集合映射 ret = cdev_add(&new_dev.cdev,new_dev.dev_id,DEVICE_CNT); if(ret<0){ //cdev初始化异常,跳转至异常处理 goto fail_cdev; } printk("chr dev inited!\r\n"); //自动创建设备节点 new_dev.class = class_create(THIS_MODULE,DEVICE_NAME); if(IS_ERR(new_dev.class)){ //class创建异常处理 printk("class err!\r\n"); ret = PTR_ERR(new_dev.class); goto fail_class; } printk("dev class created\r\n"); new_dev.device = device_create(new_dev.class,NULL,new_dev.dev_id,NULL,DEVICE_NAME); if(IS_ERR(new_dev.device)){ //设备创建异常处理 printk("device err!\r\n"); ret = PTR_ERR(new_dev.device); goto fail_device; } printk("device created!\r\n"); ret = dev_gpio_init(&new_dev); if(ret<0){ goto fail_gpio_init; } //初始化原子变量 atomic_set(&new_dev.keyvalue,INVALKEYS); atomic_set(&new_dev.keyreleased,0); return ret; fail_gpio_init: fail_device: //device创建失败,意味着class创建成功,应该将class销毁 printk("device create err,class destroyed\r\n"); class_destroy(new_dev.class); fail_class: //类创建失败,意味着设备应该已经创建成功,此刻应将其释放掉 printk("class create err,cdev del\r\n"); cdev_del(&new_dev.cdev); fail_cdev: //cdev初始化异常,意味着设备号已经申请完成,应将其释放 printk("cdev init err,chrdev register\r\n"); unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT); faile_devid: //设备号申请异常,由于是第一步操作,不需要进行其他处理 printk("dev id err\r\n"); return ret; } static void __exit key_exit(void){ int i = 0; //释放中断 for(i=0;i<KEY_NUM;i++){ free_irq(new_dev.irqkey[i].irqnum,&new_dev); } for(i=0;i<KEY_NUM;i++){ gpio_free(new_dev.irqkey[i].gpio); } del_timer_sync(&new_dev.timer); cdev_del(&new_dev.cdev); unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT); device_destroy(new_dev.class,new_dev.dev_id); class_destroy(new_dev.class); } module_init(key_init); module_exit(key_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("ZeqiZ");
tasklet的测试程序和前面带消抖的按键驱动差不多,区别就是中断处理函数里不放定时器了
1 static irqreturn_t key0_handle_irq(int irq, void *dev_id) 2 { 3 struct new_dev *dev = dev_id; 4 tasklet_schedule(&dev->irqkey[0].tasklet); 5 return IRQ_HANDLED; 6 }
而是将tasklet调度处理放在了中断处理函数里。剩下的事情全交给tasklet完成。tasklet绑定的函数里执行定时器的启动任务,这样就完成了中断处理的快进快出。
按下或释放按键,可以看到调试信息被打印出来,和前面的效果一样,就不再截图了(驱动里没有打印信息,可以结合前一章节的APP或添加printf打印调试信息。但是一般情况是不建议把printf放在中断里的,不要依赖这种方法)。
工作队列
工作队列是另一种下半部处理机制。和软中断不同,工作队列将要推后的工作交给一个内核线程去执行,他运行在上下文中,是可调度的也可以休眠。所以如果中断的服务函数里面有需要休眠的情况就建议使用工作队列,否则就可以直接使用tasklet。同样因为工作队列是伴随着上下文切换的,他的时效性就不及tasklet。
在Linux内核里,使用work_struct结构体来表示一个工作(路径为include/linux/workqueue.h)
1 struct work_struct { 2 atomic_long_t data; 3 struct list_head entry; 4 work_func_t func; 5 #ifdef CONFIG_LOCKDEP 6 struct lockdep_map lockdep_map; 7 #endif 8 };
很多个工作会被组织成一个工作队列workqueue,这个工作队列也有个结构体用来描述workqueue_struct
1 /* 2 * The externally visible workqueue. It relays the issued work items to 3 * the appropriate worker_pool through its pool_workqueues. 4 */ 5 struct workqueue_struct { 6 struct list_head pwqs; /* WR: all pwqs of this wq */ 7 struct list_head list; /* PR: list of all workqueues */ 8 9 struct mutex mutex; /* protects this wq */ 10 int work_color; /* WQ: current work color */ 11 int flush_color; /* WQ: current flush color */ 12 atomic_t nr_pwqs_to_flush; /* flush in progress */ 13 struct wq_flusher *first_flusher; /* WQ: first flusher */ 14 struct list_head flusher_queue; /* WQ: flush waiters */ 15 struct list_head flusher_overflow; /* WQ: flush overflow list */ 16 17 struct list_head maydays; /* MD: pwqs requesting rescue */ 18 struct worker *rescuer; /* I: rescue worker */ 19 20 int nr_drainers; /* WQ: drain in progress */ 21 int saved_max_active; /* WQ: saved pwq max_active */ 22 23 struct workqueue_attrs *unbound_attrs; /* WQ: only for unbound wqs */ 24 struct pool_workqueue *dfl_pwq; /* WQ: only for unbound wqs */ 25 26 #ifdef CONFIG_SYSFS 27 struct wq_device *wq_dev; /* I: for sysfs interface */ 28 #endif 29 #ifdef CONFIG_LOCKDEP 30 struct lockdep_map lockdep_map; 31 #endif 32 char name[WQ_NAME_LEN]; /* I: workqueue name */ 33 34 /* 35 * Destruction of workqueue_struct is sched-RCU protected to allow 36 * walking the workqueues list without grabbing wq_pool_mutex. 37 * This is used to dump all workqueues from sysrq. 38 */ 39 struct rcu_head rcu; 40 41 /* hot fields used during command issue, aligned to cacheline */ 42 unsigned int flags ____cacheline_aligned; /* WQ: WQ_* flags */ 43 struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */ 44 struct pool_workqueue __rcu *numa_pwq_tbl[]; /* FR: unbound pwqs indexed by node */ 45 };
Linux内核使用工作者线程(worker thread)来处理这些工作队列中断各个工作,Linux内核中使用worker结构体来表示工作者线程,worker的结构体如下:
1 struct worker { 2 /* on idle list while idle, on busy hash table while busy */ 3 union { 4 struct list_head entry; /* L: while idle */ 5 struct hlist_node hentry; /* L: while busy */ 6 }; 7 8 struct work_struct *current_work; /* L: work being processed */ 9 work_func_t current_func; /* L: current_work's fn */ 10 struct pool_workqueue *current_pwq; /* L: current_work's pwq */ 11 bool desc_valid; /* ->desc is valid */ 12 struct list_head scheduled; /* L: scheduled works */ 13 14 /* 64 bytes boundary on 64bit, 32 on 32bit */ 15 16 struct task_struct *task; /* I: worker task */ 17 struct worker_pool *pool; /* I: the associated pool */ 18 /* L: for rescuers */ 19 struct list_head node; /* A: anchored at pool->workers */ 20 /* A: runs through worker->node */ 21 22 unsigned long last_active; /* L: last active timestamp */ 23 unsigned int flags; /* X: flags */ 24 int id; /* I: worker id */ 25 26 /* 27 * Opaque string set with work_set_desc(). Printed out with task 28 * dump for debugging - WARN, BUG, panic or sysrq. 29 */ 30 char desc[WORKER_DESC_LEN]; 31 32 /* used only by rescuers to point to the target workqueue */ 33 struct workqueue_struct *rescue_wq; /* I: the workqueue to rescue */ 34 };
从上面的代码可以看出来,每个worker都有一个工作队列(33行),工作者线程自己处理工作队列中的所有工作,我们作为驱动开发人员不需要对这里的流程搞的太清楚,只需知道怎么操作这里面的东西就行了,其实和前面的tasklet用法大致相同。我们要做的就是定义一个工作(work_struct)然后对其初始化就可以了。整个过程框架如下:
1 /*定义工作*/ 2 struct work_struct testwork; 3 4 /*work处理函数*/ 5 void testwork_func_t(struct work_struct *work) 6 { 7 /*work具体处理内容*/ 8 } 9 10 /*中断处理函数*/ 11 irqreturn_t test_handler(int irq,void *dev) 12 { 13 schedule_work(&testwork); //work调度 14 } 15 16 17 /*驱动入库*/ 18 static int __init dev_init(void) 19 { 20 21 INIT_WORK(&testwork,testwork_func_t) //work初始化 22 23 request_irq(irq_num,test_handler,"irqname",&dev) //中断申请注册 24 }
整个驱动放出来。
/** * @file work.c * @author your name (you@domain.com) * @brief 工作队列下半部驱动测试 * @version 0.1 * @date 2022-07-28 * * @copyright Copyright (c) 2022 * */ #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/io.h> #include <linux/types.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/of.h> #include <linux/of_address.h> #include <linux/of_irq.h> #include <linux/gpio.h> #include <linux/of_gpio.h> #include <linux/irq.h> #include <linux/interrupt.h> #define DEVICE_CNT 1 #define DEVICE_NAME "imx6uirq" #define KEY_NUM 1 #define KEY0VALUE 0x01 #define INVALKEYS 0xFF /** * @brief 按键中断结构体 * */ struct irq_keydesc { int gpio; //io编号 int irqnum; //中断号 unsigned char value; //键值 char name[10]; //按键名字 irqreturn_t (*handler)(int,void*); //中断处理函数 struct work_struct testwork; }; /** * @brief 设备结构体 * */ struct new_dev { dev_t dev_id; int major; int minor; struct class *class; struct device *device; struct cdev cdev; struct device_node *dev_nd; int dev_gpio; struct irq_keydesc irqkey[KEY_NUM]; //按键描述数组 struct timer_list timer; //定时器 int timer_per; atomic_t keyvalue; atomic_t keyreleased; //1表示1次有效按键并被释放,0表示未被释放 }; struct new_dev new_dev; static int new_dev_open(struct inode *inode, struct file *filp) { filp->private_data = &new_dev; /* 设置私有数据 */ return 0; } static ssize_t new_dev_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt) { int ret = 0; unsigned char keyvalue; unsigned char keyreleased; struct new_dev *dev = filp->private_data; //私有数据 keyvalue = atomic_read(&dev->keyvalue); //获取按键状态标志及按键值 keyreleased = atomic_read(&dev->keyreleased); if(keyreleased){ //出现按键被按下并释放的过程 if(keyvalue&0x80){ //获取最高位,如果为1表示出现过按键被按下后释放掉状态,是有效状态 keyvalue &= ~0x80; ret = copy_to_user(buf,&keyvalue,sizeof(keyvalue)); } else{ goto data_err; } atomic_set(&dev->keyreleased,0); //恢复keyreleased的状态 } else{ goto data_err; //返回负数,在用户态跳出循环 } return ret; data_err: return -EINVAL; }; /** * @brief 文件操作集合 * */ static const struct file_operations key_fops = { .owner = THIS_MODULE, // .write = new_dev_write, .open = new_dev_open, .read = new_dev_read, // .release = new_dev_release, }; static irqreturn_t key0_handle_irq(int irq, void *dev_id) { int value = 0; struct new_dev *dev = dev_id; schedule_work(&dev->irqkey[0].testwork); return IRQ_HANDLED; } /** * @brief * * @param work */ static void testwork_func(struct work_struct *work) { new_dev.timer.data = (unsigned long)&new_dev; mod_timer(&new_dev.timer,jiffies +msecs_to_jiffies(20)); } timer_func(unsigned long arg){ int value = 0; struct new_dev *dev =(struct new_dev*)arg; value = gpio_get_value(dev->irqkey[0].gpio); if(value == 0){ //按下 atomic_set(&dev->keyvalue,dev->irqkey[0].value); } else{ //释放 atomic_set(&dev->keyvalue,0x80|(dev->irqkey[0].value)); //将最高位置一,表示按钮释放 atomic_set(&dev->keyreleased,1); } } static int dev_gpio_init(struct new_dev *dev) { int ret = 0; int i = 0; //搜索设备树节点 dev->dev_nd = of_find_node_by_path("/key"); if(dev->dev_nd == NULL){ printk("can't find device key\r\n"); ret = -EINVAL; goto fail_nd; } for(i=0;i<KEY_NUM;i++) { dev->irqkey[i].gpio = of_get_named_gpio(dev->dev_nd,"key-gpios",i); //多个按键获取 if(dev->irqkey[i].gpio<0){ ret = -EINVAL; goto fail_gpio_num; } ret = gpio_request(dev->irqkey[i].gpio,dev->irqkey[i].name); if(ret){ ret = -EBUSY; goto fail_gpio_request; } gpio_direction_input(dev->irqkey[i].gpio); dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio); //获取中断号 // dev->irqkey[i].irqnum = irq_of_parse_and_map(dev->dev_nd,i) //方法2获取中断号 } dev->irqkey[0].handler = key0_handle_irq; dev->irqkey[0].value = KEY0VALUE; for(i=0;i<KEY_NUM;i++){ memset(dev->irqkey[i].name,0,sizeof(dev->irqkey[i].name)); sprintf(dev->irqkey[i].name,"KEY%d",i); //将格式化数据写入字符串中 ret = request_irq(dev->irqkey[i].irqnum, //中断号 key0_handle_irq, //中断处理函数 IRQ_TYPE_EDGE_RISING|IRQ_TYPE_EDGE_FALLING, //中断处理函数 dev->irqkey[i].name, //中断名称 dev //设备结构体 ); if(ret){ printk("irq %d request err\r\n",dev->irqkey[i].irqnum); goto fail_irq; } INIT_WORK(&dev->irqkey[i].testwork,testwork_func); } //此处不设置定时值,防止定时器add后直接运行 init_timer(&dev->timer); dev->timer.function = timer_func; return 0; fail_gpio_request: fail_irq: for(i=0; i<KEY_NUM;i++){ gpio_free(dev->irqkey[i].gpio); } fail_gpio_num: fail_nd: return ret; } static int __init key_init(void){ int ret = 0; // unsigned int val = 0; //申请设备号 new_dev.major = 0; if(new_dev.major){ //手动指定设备号,使用指定的设备号 new_dev.dev_id = MKDEV(new_dev.major,0); ret = register_chrdev_region(new_dev.dev_id,DEVICE_CNT,DEVICE_NAME); } else{ //设备号未指定,申请设备号 ret = alloc_chrdev_region(&new_dev.dev_id,0,DEVICE_CNT,DEVICE_NAME); new_dev.major = MAJOR(new_dev.dev_id); new_dev.minor = MINOR(new_dev.dev_id); } printk("dev id geted!\r\n"); if(ret<0){ //设备号申请异常,跳转至异常处理 goto faile_devid; } //字符设备cdev初始化 new_dev.cdev.owner = THIS_MODULE; cdev_init(&new_dev.cdev,&key_fops); //文件操作集合映射 ret = cdev_add(&new_dev.cdev,new_dev.dev_id,DEVICE_CNT); if(ret<0){ //cdev初始化异常,跳转至异常处理 goto fail_cdev; } printk("chr dev inited!\r\n"); //自动创建设备节点 new_dev.class = class_create(THIS_MODULE,DEVICE_NAME); if(IS_ERR(new_dev.class)){ //class创建异常处理 printk("class err!\r\n"); ret = PTR_ERR(new_dev.class); goto fail_class; } printk("dev class created\r\n"); new_dev.device = device_create(new_dev.class,NULL,new_dev.dev_id,NULL,DEVICE_NAME); if(IS_ERR(new_dev.device)){ //设备创建异常处理 printk("device err!\r\n"); ret = PTR_ERR(new_dev.device); goto fail_device; } printk("device created!\r\n"); ret = dev_gpio_init(&new_dev); if(ret<0){ goto fail_gpio_init; } //初始化原子变量 atomic_set(&new_dev.keyvalue,INVALKEYS); atomic_set(&new_dev.keyreleased,0); return ret; fail_gpio_init: fail_device: //device创建失败,意味着class创建成功,应该将class销毁 printk("device create err,class destroyed\r\n"); class_destroy(new_dev.class); fail_class: //类创建失败,意味着设备应该已经创建成功,此刻应将其释放掉 printk("class create err,cdev del\r\n"); cdev_del(&new_dev.cdev); fail_cdev: //cdev初始化异常,意味着设备号已经申请完成,应将其释放 printk("cdev init err,chrdev register\r\n"); unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT); faile_devid: //设备号申请异常,由于是第一步操作,不需要进行其他处理 printk("dev id err\r\n"); return ret; } static void __exit key_exit(void){ int i = 0; //释放中断 for(i=0;i<KEY_NUM;i++){ free_irq(new_dev.irqkey[i].irqnum,&new_dev); } for(i=0;i<KEY_NUM;i++){ gpio_free(new_dev.irqkey[i].gpio); } del_timer_sync(&new_dev.timer); cdev_del(&new_dev.cdev); unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT); device_destroy(new_dev.class,new_dev.dev_id); class_destroy(new_dev.class); } module_init(key_init); module_exit(key_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("ZeqiZ");
驱动太长了就折叠起来,整个文件的结构和前面的tasklet差不多,但是有一点要注意一点计时work的处理函数
1 static void testwork_func(struct work_struct *work) 2 { 3 new_dev.timer.data = (unsigned long)&new_dev; 4 mod_timer(&new_dev.timer,jiffies +msecs_to_jiffies(20)); 5 }
因为通过work结构体传进来的参数只有一个work,但是我们启动timer是需要把整个设备结构体对象new_dev作为参数传给定时器回调函数的。
1 timer_func(unsigned long arg){ 2 3 int value = 0; 4 struct new_dev *dev =(struct new_dev*)arg; 5 6 value = gpio_get_value(dev->irqkey[0].gpio); 7 if(value == 0){ 8 //按下 9 atomic_set(&dev->keyvalue,dev->irqkey[0].value); 10 } 11 else{ 12 //释放 13 atomic_set(&dev->keyvalue,0x80|(dev->irqkey[0].value)); //将最高位置一,表示按钮释放 14 atomic_set(&dev->keyreleased,1); 15 } 16 }
上面定时器处理函数的第4行就是将传进来的参数arg通过类型转换恢复成了new_dev形式的结构体。但是work初始化的时候并没有向work结构体绑定这个参数
INIT_WORK(&dev->irqkey[i].testwork,testwork_func);
所以我们直接把new_dev对象作为一个全局变量来使用了。这里应该注意一下,如果我们会用container_of函数,就可以通过该函数获取new_dev的地址。内核中已经有的驱动基本上都是按这个方法去获取的new_dev地址,就不用全局变量了,只需要从已有的testwork成员获取到new_dev的地址(前提是testwork是new_dev的成员,如果使用这个方式整个设备结构体的以及程序结构要改很多,就不再演示了)