1. 中断基本概念
1.1 中断基础介绍
中断就是CPU正常运行期间,由于内、外部事件引起的CPU暂时停止正在运行的程序,去执行该内部事件或外部事件的引起的服务中去,服务执行完毕后再返回断点处继续执行的情形。
中断的意义:极大提高CPU运行效率。
中断处理程序:在中断发生时被调用的函数称为中断服务函数。
中断服务函数的原则:linux是多进程操作系统。
中断不属于任何一个进程,因此不能在中断程序中休眠和调用schedule函数放弃CPU,实现终端处理函数有一个原则,就是尽可能的处理并返回。
1.2 linux中断顶部、底部概念
为保证系统实时性,中断服务程序必须足够简短,但实际应用中某些时候发生中断时必须处理大量的事物,这时候如果都在中断服务程序中完成,则会严重降低中断的实时性,基于这个原因,linux系统提出了一个概念:把中断服务程序分为两部分-顶半部-底半部。
顶半部
完成尽可能少的比较急的功能,它往往只是简单的读取寄存器的中断状态,并清除中断标志后就进行“中断标记”(也就是把底半部处理程序挂到设备的底半部执行队列中)的工作。
特点:响应速度快。
底半部:
中断处理的大部分工作都在底半部,它几乎做了中断处理程序的所有事情。
特点:处理相对来说不是非常紧急的事件。
底半部机制主要有:tasklet、工作队列和软中断。
接下来以按键中断为例编写第一个简单的中断驱动。编写驱动分下面几步:
1. 查看原理图、数据手册,了解设备的操作方法;
2. 在内核中找到相近的驱动程序,以它为模板进行开发,有时候需要从零开始;
3. 设计所要实现的操作,比如open、close、read、write 等函数;
4. 测试。
2. 分析设备
本人使用的是JZ2440v3开发板,该开发板CPU使用的是S3C2440A,按键与CPU连接如下:
可以看到4个按键分别连接到2440的GPF0、GPF2、GPG3、GPG11引脚上面,4个按键接上拉电阻,可知按键按下时引脚输入低电平,按键松开时引脚输入高电平。
再定位到S3C2440A芯片手册:
从上面资料可知信息:
1. 想要控制开发板上面的4个按键,需要控制2440的GPF0、GPF2、GPG3、GPG11三个引脚为Input模式,所以要配置GPFCON寄存器的位[5:4]=00,[1:0]=00,配置GPGCON寄存器的位[23:22]=00,[7:6]=00。
2. 想要控制4个按键还需要控制GPFDAT、GPGDAT寄存器对应位。
3. GPFCON寄存器的物理地址为0x56000050,GPFDAT寄存器的物理地址为0x56000054。GPGCON寄存器的物理地址为0x56000060,GPGDAT寄存器的物理地址为0x56000064。
3. 编写代码
驱动程序button_drv.c如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/cdev.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 struct cdev button_cdev;
static struct class *button_class;
volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;
volatile unsigned long *gpgcon;
volatile unsigned long *gpgdat;
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
/* 中断事件标志, 中断服务程序将它置1,button_drv_read将它清0 */
static volatile int ev_press = 0;
struct pin_desc{
unsigned int pin;
unsigned int key_val;
};
/* 键值: 按下时, 0x01, 0x02, 0x03, 0x04 */
/* 键值: 松开时, 0x81, 0x82, 0x83, 0x84 */
static unsigned char key_val;
struct pin_desc pins_desc[4] = {
{S3C2410_GPF0, 0x01},
{S3C2410_GPF2, 0x02},
{S3C2410_GPG3, 0x03},
{S3C2410_GPG11, 0x04},
};
/*
* 确定按键值
*/
static irqreturn_t buttons_irq(int irq, void *dev_id)
{
struct pin_desc * pindesc = (struct pin_desc *)dev_id;
unsigned int pinval;
pinval = s3c2410_gpio_getpin(pindesc->pin);
if (pinval)
{
/* 松开 */
key_val = 0x80 | pindesc->key_val;
}
else
{
/* 按下 */
key_val = pindesc->key_val;
}
ev_press = 1; /* 表示中断发生了 */
wake_up_interruptible(&button_waitq); /* 唤醒休眠的进程 */
return IRQ_RETVAL(IRQ_HANDLED);
}
static int button_drv_open(struct inode *inode, struct file *file)
{
/* 配置GPF0,2为输入引脚 */
/* 配置GPG3,11为输入引脚 */
request_irq(IRQ_EINT0, buttons_irq, IRQT_BOTHEDGE, "S2", &pins_desc[0]);
request_irq(IRQ_EINT2, buttons_irq, IRQT_BOTHEDGE, "S3", &pins_desc[1]);
request_irq(IRQ_EINT11, buttons_irq, IRQT_BOTHEDGE, "S4", &pins_desc[2]);
request_irq(IRQ_EINT19, buttons_irq, IRQT_BOTHEDGE, "S5", &pins_desc[3]);
return 0;
}
ssize_t button_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
int count;
if (size != 1)
return -EINVAL;
/* 如果没有按键动作, 休眠 */
wait_event_interruptible(button_waitq, ev_press);
/* 如果有按键动作, 返回键值 */
count=copy_to_user(buf, &key_val, 1);
ev_press = 0;
return count;
}
int button_drv_close(struct inode *inode, struct file *file)
{
free_irq(IRQ_EINT0, &pins_desc[0]);
free_irq(IRQ_EINT2, &pins_desc[1]);
free_irq(IRQ_EINT11, &pins_desc[2]);
free_irq(IRQ_EINT19, &pins_desc[3]);
return 0;
}
static struct file_operations button_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = button_drv_open,
.read = button_drv_read,
.release = button_drv_close,
};
static int button_drv_init(void)
{
int result;
dev_t devid = MKDEV(major, 0); //从主设备号major,次设备号0得到dev_t类型
if (major)
{
result=register_chrdev_region(devid, 1, "button"); //注册字符设备
}
else
{
result=alloc_chrdev_region(&devid, 0, 1, "button"); //注册字符设备
major = MAJOR(devid); //从dev_t类型得到主设备
}
if(result<0)
return result;
cdev_init(&button_cdev, &button_drv_fops);
cdev_add(&button_cdev, devid, 1);
button_class = class_create(THIS_MODULE, "button_drv");
class_device_create(button_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
gpfdat = gpfcon + 1;
gpgcon = (volatile unsigned long *)ioremap(0x56000060, 16);
gpgdat = gpgcon + 1;
return 0;
}
static void button_drv_exit(void)
{
class_device_destroy(button_class, MKDEV(major, 0));
class_destroy(button_class);
cdev_del(&button_cdev);
unregister_chrdev_region(MKDEV(major, 0), 1);
iounmap(gpfcon);
iounmap(gpgcon);
}
module_init(button_drv_init);
module_exit(button_drv_exit);
MODULE_AUTHOR("LVZHENHAI");
MODULE_LICENSE("GPL");
应用程序button_test.c如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
/* button_test
*/
int main(int argc, char **argv)
{
int fd;
unsigned char key_val;
fd = open("/dev/buttons", O_RDWR);
if (fd < 0)
{
printf("can't open!\n");
}
while (1)
{
read(fd, &key_val, 1);
printf("key_val = 0x%x\n", key_val);
}
return 0;
}
Makefile如下:
KERN_DIR = /work/system/linux-2.6.22.6 //内核目录
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += button_drv.o
3. 测试
内核:linux-2.6.22.6
编译器:arm-linux-gcc-3.4.5
环境:ubuntu9.10
将button_drv.c、button_test.c、Makefile三个文件放入网络文件系统内,在ubuntu该目录下执行:
make
arm-linux-gcc -o button_test button_test.c
在挂载了网络文件系统的开发板上进入相同目录,执行“ls”查看:
装载驱动:
insmod button_drv.ko
运行测试程序:
./button_test
按下开发板的4个按键,结果如下:
4. 程序说明
当调用insmod装载驱动后,会调用button_drv_init注册/dev/buttons字符设备,./button_test运行应用程序后,应用程序执行open进行系统调用到button_drv_open,驱动程序调用request_irq进行4个按键中断的注册。然后应用程序继续执行read进行系统调用到button_drv_read,进而调用wait_event_interruptible程序休眠,只有当按键按下或松开触发中断程序buttons_irq执行,函数里调用wake_up_interruptible唤醒休眠的进程(button_drv_read),然后将驱动里读到的按键值调用copy_to_user拷贝到用户空间。应用程序再将按键值打印出来。
5. 知识点
5.1 中断
Linux使用request_irq()函数为中断服务例程分配一个硬件中断号并登记相应的中断程序处理例程,即所谓的注册中断。
函数原型:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id)
参数:
irq:要分配的硬件中断号。
handler:向系统登记的中断处理例程,这是一个回调函数,中断发生时,系统调用这个函数,传入的参数包括硬件的中断号,dev_id,寄存器值。dev_id是request_ird函数传递给系统的参数的参数dev_id.
irqflags:标记中断处理属性的标志位,常用SA_INTERRUPT标志和SA_SHIRQ标志,二者可以通过位或者与运算一起使用。SA_INTERRUPT标志是快速中断标志位,如果设置了该标志,则屏蔽所有中断,中断处理程序进行快速处理,否则不屏蔽所有中断,进行慢速处理。SA_SHIRQ标志是中断共享标志位,如果设置了改标志,多个设备共享改中断。
dev_name:驱动设备的字符串名称,用来在/pro/interrupts中显示中断的所有者。
dev_id:用于共享中断号的非NULL标识,若中断没有被共享,dev_id可以设置为NULL。共享中断号时,该标识必须是全局唯一的,在释放中断时会用到它。驱动程序也可以用它来指向自己的私有数据区。一般将它设置为这个设备device结构本身,中断处理程序可以用dev_id找到相应的产生这个中断的设备。
对应释放函数原型:
void free_irq(unsigned int irq, void *dev_id);
PS:在驱动中,设备申请资源最好放在打开驱动时也就是.open函数中,然后在.close函数中释放资源。
5.2 队列
在Linux驱动程序中, 可以使用等待队列(Wait Queue) 来实现阻塞进程的唤醒。
5.2.1 创建“等待队列头部”
方式一:
wait_queue_head_t my_queue; //定义等待队列头
init_waitqueue_head(&my_queue); //初始化等待队列头
方式二:
static DECLARE_WAIT_QUEUE_HEAD(button_waitq); //定义并初始化等待队列头
5.2.2 添加/移除等待队列
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
add_wait_queue()用于将等待队列元素wait添加到等待队列头部q指向的双向链表中, 而
remove_wait_queue()用于将等待队列元素wait从由q头部指向的链表中移除。
5.2.3 等待事件
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
等待第1个参数queue作为等待队列头部的队列被唤醒, 而且第2个参数condition必须满足, 否则继续阻塞。 wait_event() 和wait_event_interruptible() 的区别在于后者可以被信号打断, 而前者不能。 加上timeout后的宏意味着阻塞等待的超时时间, 以jiffy为单位, 在第3个参数的timeout到达时, 不论condition是否满足, 均返回。
5.2.4 唤醒队列
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的进程。
5.3 休眠函数
函数原型:
#define wait_event_interruptible(wq, condition) \
({ \
int __ret = 0; \
if (!(condition)) \
__wait_event_interruptible(wq, condition, __ret); \
__ret; \
})
#define __wait_event_interruptible(wq, condition, ret) \
do { \
DEFINE_WAIT(__wait); \
\
for (;;) { \
prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE); \
if (condition) \
break; \
if (!signal_pending(current)) { \
schedule(); \
continue; \
} \
ret = -ERESTARTSYS; \
break; \
} \
finish_wait(&wq, &__wait); \
} while (0)
深入分析:函数先将当前进程的状态设置成TASK_INTERRUPTIBLE(该状态下的进程如果休眠的话可以被信号和wake_up()唤醒),然后调用schedule(),而schedule()会将位于TASK_INTERRUPTIBLE状态的当前进程从runqueue队列中删除。从runqueue队列中删除的结果是,当前这个进程将不再参与调度,除非通过其他函数将这个进程重新放入这个runqueue队列中(wake_up()唤醒runqueue)。
由于这一段代码位于一个由condition控制的for(;;)循环中,所以当由shedule()返回时(当然是被wake_up之后,通过其他进程的schedule()而再次调度本进程),如果条件condition不满足,本进程将自动再次被设置为TASK_INTERRUPTIBLE状态,接下来执行schedule()的结果是再次被从runqueue队列中删除。这时候就需要再次通过wake_up()重新添加到runqueue队列中。如此反复,直到condition为真的时候被wake_up()。
可见,成功地唤醒一个被wait_event_interruptible()的进程,需要满足:
(1) condition为真的前提下
(2) 调用wake_up()
所以,如果你仅仅修改condition,那么只是满足其中一个条件,这个时候,被wait_event_interruptible()起来的进程尚未位于runqueue队列中,因此不会被 schedule()。这个时候只要wake_up一下就立刻会重新进入运行调度。