有很多Linux 的驱动都是通过中断的方式来进行内核和硬件的交互。
在 Linux 设备驱动中,使用中断的设备需要申请和释放对应的中断,分别使用内核提供的request_irq()
和free_irq()函数。
驱动程序申请中断和释放中断的调用在include/linux/sched.h里声明。
request_irq()调用的定义:
int request_irq(unsigned int irq, void (*handler)(int irq, void *dev_id, struct pt_regs *regs), unsigned long irqflags,const char * devname,oid *dev_id);
irq 是要申请的硬件中断号。在Intel 平台,范围是0~15。
handler 是向系统登记的中断处理函数。这是一个回调函数,中断发生时,系统调用这个函数,传入的参数包括硬件中断号,device id,寄存器值。
dev_id就是下面的request_irq时传递给系统的参数dev_id。
irqflags 是中断处理的一些属性(下降沿/上升沿的触发)。比较重要的有SA_INTERRUPT,标明中断处理程序是快速处理程序(设置SA_INTERRUPT)还是慢速处理程序(不 设置SA_INTERRUPT)。 快速处理程序被调用时屏蔽所有中断。慢速处理程序不屏蔽。还有一个SA_SHIRQ 属性,设置了以后运行多个设备共享中断。
dev_id 在中断共享时会用到。一般设置为这个设备的device结构本身或者NULL。中断处理程序可以用dev_id找到相应的控制这个中断的设备,或者用irq2dev_map找到中断对 应的设备。
该函数作用:
①分配irqaction结构。
②setup_irq(irq, action);
a.加入中断链表:irq_desc[irq]->action.
b.设置中断引脚和中断类型:desc->chip->settype()
c.使能中断:desc->chip->startup/enable
free_irq()函数
void free_irq(unsigned int irq,void *dev_id);
该函数所需参数和request_irq中所用到的参数一样。同一个中断的不同处理函数必须使用不同的dev_id来区分,这就要求注册共享中断dev_id必须唯一
①从链表取出中断
②禁止中断
代码中用到了
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);//定义等待队列
补充:
阻塞与非阻塞 I/O
驱动程序通常需要提供这样的能力:当应用程序进行read()、write()等系统调用时,若设备的资源不能
获取,而用户又希望以阻塞的方式访问设备,驱动程序应在设备驱动的xxx_read()、xxx_write()等操作中将
进程阻塞直到资源可以获取,此后,应用程序的read()、write()等调用才返回,整个过程仍然进行了正确的
设备访问,用户并没有感知到;
若用户以非阻塞的方式访问设备文件,则当设备资源不可获取时,设备驱动的xxx_read()、xxx_write()等操作应立即返回,read()、write()等系统调用也随即被返回。
阻塞从字面上听起来似乎意味着低效率,实则不然,如果设备驱动不阻塞,则用户想获取设备资源只能查询这反而会无谓地耗费CPU 资源。而阻塞访问时,不能获取资源的进程将进入休眠,它将CPU 资源让给其他进程。
因为阻塞的进程会进入休眠状态,因此,必须确保有一个地方能够唤醒休眠的进程。唤醒进程的地方最大可能发生在中断里面,因为硬件资源获得的同时往往伴随着一个中断。
在 Linux 驱动程序中,可以使用等待队列(wait queue)来实现阻塞进程的唤醒。wait queue 很早就作为一个基本的功能单位出现在Linux 内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制。等待队列可以用来同步对系统资源的访问。
等待队列可理解成保存进程的容器
- 阻塞时,将进程放入等待队列
- 唤醒进程时,从等待队列中取出进程
Linux 2.6 提供如下关于等待队列的操作。
1.定义“等待队列头”。
wait_queue_head_t my_queue;
2.初始化“等待队列头”。
init_waitqueue_head(&my_queue);
而下面的DECLARE_WAIT_QUEUE_HEAD()宏可以作为定义并初始化等待队列头的“快捷方式”。
DECLARE_WAIT_QUEUE_HEAD (name)
3.定义等待队列。
定义并初始化一个名为name 的等待队列
DECLARE_WAITQUEUE(name, tsk)
4.添加/移除等待队列。
void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); void fastcall remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
add_wait_queue() 用于将等待队列wait 添加到等待队列头q 指向的等待队列链表中,
remove_wait_queue()用于将等待队列wait 从附属的等待队列头q 指向的等待队列链表中移除。
5.等待事件。
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
等待第一个参数queue 作为等待队列头的等待队列被唤醒,而且第二个参数condition 必须满足,否则
阻塞。wait_event()和wait_event_interruptible()的区别在于后者可以被信号打断,而前者不能。加上_timeout
后的宏意味着阻塞等待的超时时间,以jiffy 为单位,在第三个参数的timeout 到达时,不论condition 是否
满足,均返回。
wait()当condition 满足时,wait_event()会立即返回,
否则,阻塞等待condition 满足。
6.唤醒队列。
void wake_up(wait_queue_head_t *queue); void wake_up_interruptible(wait_queue_head_t *queue);
上述操作会唤醒以queue 作为等待队列头的所有等待队列中所有属于该等待队列头的等待队列对应的进程。
wake_up() 应与wait_event() 或wait_event_timeout() 成对使用,
而wake_up_interruptible() 则应与wait_event_interruptible() 或wait_event_interruptible_timeout() 成对使用。
wake_up() 可唤醒处于TASK_INTERRUPTIBLE 和TASK_UNINTERRUPTIBLE 的进程,
而wake_up_interruptible()只能唤醒处于TASK_INTERRUPTIBLE 的进程。
等待队列可以认为是保存进程的容器
- 阻塞时,将进程放入等待队列
- 唤醒进程时,从等待队列中取出进程
代码
#include <linux/module.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/init.h> #include <linux/delay.h> #include <asm/uaccess.h> #include <linux/interrupt.h> #include <asm/irq.h> #include <asm/io.h> #include <asm/arch/regs-gpio.h> #include <asm/hardware.h> #define DEVICE_NAME "mybutton" /* 加载模式后,执行”cat /proc/devices”命令看到的设备名称 */ static struct class *button_class; static struct class_device *button_dev_class; int major; static volatile int press_cnt=0;/* 按键被按下的次数(准确地说,是发生中断的次数) */ static DECLARE_WAIT_QUEUE_HEAD(button_waitq);//定义等待队列 /* 中断事件标志, 中断服务程序将它置1,s3c24xx_buttons_read将它清0 */ static volatile int ev_press = 0; static irqreturn_t buttons_interrupt(int irq, void *dev_id) { // volatile int *press_cnt = (volatile int *)dev_id; press_cnt =press_cnt + 1; /* 按键计数加1 */ ev_press = 1; /* 表示中断发生了 */ wake_up_interruptible(&button_waitq); /* 唤醒休眠的进程 */ return IRQ_RETVAL(IRQ_HANDLED);//中断处理程序应该返回一个值,用来表明是否真正处理了一个中断,如果中断例程发现其设备的确要处理,则应该返回IRQ_HANDLED, //否则应该返回IRQ_NONE,我们可以通过这个宏来产生返回值,不是本设备的中断应该返回IRQ_NONE } /* 应用程序对设备文件/dev/xxx 执行open(...)时, * 就会调用button_open函数 * 就会调用button_open函数 */ static int button_open (struct inode *inode, struct file *filep) { int err; err=request_irq(IRQ_EINT2,buttons_interrupt,IRQF_TRIGGER_FALLING,"KEY3",NULL); if (err) { // 释放已经注册的中断 free_irq(IRQ_EINT2, NULL); return -EBUSY; } return 0; } /* 应用程序对设备文件/dev/buttons执行close(...)时, * 就会调用buttons_close函数 */ static int buttons_close(struct inode *inode, struct file *file) { free_irq(IRQ_EINT2, NULL); return 0; } ssize_t button_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { unsigned long err; /* 如果ev_press等于0,休眠 */ wait_event_interruptible(button_waitq, ev_press);//阻塞 /* 执行到这里时,ev_press等于1,将它清0 */ ev_press = 0; /* 将按键状态复制给用户,并清0 */ err = copy_to_user(buf, (const void *)&press_cnt, count); //memset((void *)&press_cnt, 0, sizeof(press_cnt)); return err ? -EFAULT : 0; } /* 这个结构是字符设备驱动程序的核心 * 当应用程序操作设备文件时所调用的open、read、write等函数, * 最终会调用这个结构中指定的对应函数 */ static struct file_operations button_ops= { .owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */ .open = button_open, .read = button_read, .release = buttons_close, }; /* * 执行insmod命令时就会调用这个函数 */ static int button_init(void) { /* 注册字符设备 * 参数为主设备号、设备名字、file_operations结构; * 这样,主设备号就和具体的file_operations结构联系起来了, * 操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数 * LED_MAJOR可以设为0,表示由内核自动分配主设备号 */ major = register_chrdev(0, DEVICE_NAME, &button_ops); if (major < 0) { printk(DEVICE_NAME " can't register major number number::%d\n",major); return 0; } printk(DEVICE_NAME " initialized1\n"); button_class = class_create(THIS_MODULE, "button"); if (IS_ERR(button_class)) return PTR_ERR(button_class); button_dev_class = class_device_create(button_class, NULL, MKDEV(major, 0), NULL, "my_button"); /* /dev/my_button */ return 0; } /* * 执行rmmod命令时就会调用这个函数 */ static void button_exit(void) { class_device_unregister(button_dev_class); class_destroy(button_class); /* 卸载驱动程序 */ unregister_chrdev(major, DEVICE_NAME); } /* 这两行指定驱动程序的初始化函数和卸载函数 */ module_init(button_init); module_exit(button_exit); /* 描述驱动程序的一些信息,不是必须的 */ MODULE_AUTHOR("http://www.100ask.net");// 驱动程序的作者 MODULE_DESCRIPTION("S3C2410/S3C2440 LED Driver");// 一些描述信息 MODULE_LICENSE("GPL"); // 遵循的协议
将代码编译 所需Makefile和上几节差不多稍微修改一下模块名称即可直接用
驱动装载后会产生设备节点 /dev/my_button
cat /proc/interrupts 可以看到已经注册的终端 并无“KEY3”(代码中使用的name)
执行 exec 5</dev/my_button 这句意思是 将/dev/my_button定位到5
cat /proc/interrupts
exec 5<&- 即可关闭这个中断
test.c
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main(int argc,char**argv) { int fd; int cnt = 0; int ret; int i; //打开设备 fd = open("/dev/my_button",O_RDWR); if(fd<0) { perror("open fail \n"); return -1; } while(1) //进程有可能一直在read函数中休眠,当有按键按下时,他才返回值(底层驱动里的read是阻塞) { //按键被按下的次数 ret=read(fd,&cnt, sizeof(cnt)); if (ret<0) { printf("read error!\n"); } if(cnt) printf("button has been pressed %d timea\n",cnt); } close(fd); return 0; }
arm-linux-gcc test.c
./a.out 按下按键即可输出终端次数
参考:
韦东山老师的第一期视频
linux设备驱动开发详解