专题**-按键驱动程序设计


混杂设备核心理论

在Linux系统中,存在一类字符设备,他们拥有相同的主设备号(10),但次设备号不同,我们称这类设备为混杂设备(miscdevice)。所有的混杂设备形成一个链表,对设备访问时内核根据次设备号查找到相应的混杂设备。

Linux里面驱动很多,但是它们都有一个共性的东西,比如说Linux这么多设备中,我首先要找出Linux内核给我们提供了什么样的结构来描述这个设备的,其次我们要知道怎么来注册这个设备的,最后是我们怎么来注销这个设备的。

混杂设备描述结构

// include/linux/miscdevice.h
struct miscdevice  {
    int  minor;         //Device minor,混杂设备主设备号为10
    const char *name;       //device name
    const struct file_operations *fops; //device file operations
    struct list_head list;
    struct device *parent;
    struct device *this_device;
    const char *nodename;
    mode_t mode;
};

混杂设备的注册与注销

extern int misc_register (struct miscdevice * misc);
extern int misc_deregister (struct miscdevice * misc);

学习新的设备驱动的学习方法

不管是字符设备、混杂设备等等这些设备,首先我们要先找出在Linux内核中使用什么结构来描述这些设备的,我们要使用结构中哪些成员,哪些成员由内核完成。
然后寻找怎么注册这个设备的。最后就是怎么注销这个设备的。


混杂设备key.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
MODULE_LICENSE("GPL");

struct file_operations mdev_fops;

struct miscdevice mdev = {
    .minor = 200,
    .name = "mdevice",
    .fops = &mdev_fops
};

static int key_init (void)
{
    misc_register(&mdev);
    return 0;
}
static void key_init (void)
{
    misc_deregister (&mdev);
    return ;
}

module_init (key_init);
module_exit (key_exit);

key.c对应的Makefile

obj-m:=key.o
KDIR:=/home/redhat6/linux-mini2440
all:
    make modules M=$(PWD) $(KDIR) ARCH=arm CROSS_COMPILE=arm-linux-
.PHONY:clean
clean:
    rm -rf *.o *.elf *.ko *.order* *.sym* *.mod* *.bin

Linux中断处理深层解析


为什么会在按键驱动过程中去插入一个中断处理的知识点呢?

在我们前面学习裸机按键驱动的时候,我们是以中断的方式让按键工作的,所以说我们要让按键能够按照中断方式工作,所以说我们必须要让处理器中断这种机制能够工作起来;而Linux下面的按键驱动程序,我们一样要了解在我们的Linux系统中是怎么来处理中断的,整个Linux处理中断的流程是怎么样的,中断处理程序该怎么去设计等等,那么这是我们在学习Linux按键驱动之前必须要掌握的知识点。


回顾裸机按键中断处理流程

在start.S中,对中断会有一个统一的入口(irq),当我们硬件发生中断的时候,不管是哪种类型的中断,它都会跳转到中断向量表这个地方来,然后从irq入口,也就是统一的入口,统一入口的任务就是环境的保存、执行处理函数、环境的恢复;跳转到中断函数处理中,该函数首先会读取产生中断的设备,也就是读取中断源,然后根据中断源编号调用相应的中断处理程序(switch-case),而这些中断处理程序是事先实现并注册进来的程序,最后清除中断。


Linux系统中断处理流程

entry-armv.S中有个标号是__irq_svc,那么这个标处实际上就是Linux中断的统一入口,进入到这个统一入口后,它同样去做环境保存啊这些工作,首先是要拿到产生中断源的编号,然后根据这个中断号去做接下来的事情,利用获得的中断号找出irq_desc结构,比如说irq_desc[0]就代表0号中断,irq_desc[1]就代表1号中断,以此类推,硬件产生的每个中断编号都和irq_desc相对应,在irq_desc结构当中就会有action这个选项,而在action中就是存放的用户事先填写进去的中断处理函数,这些函数在当前irq_desc下串联成一个任务链表,当该中断号发生时,这个irq_desc里的每个中断处理函数都会去执行一遍,那没有产生中断的函数也要执行吗?当然要,只不过处理函数里面首先要添加中断检查,检查该函数所属硬件是否产生中断,如果没有就直接退出,这样就不会影响到其他中断处理函数的执行了。


为什么我要给同学们讲这个中断处理过程呢?

第一,让同学们对中断有个更深刻的认识,其次我们通过我的中断处理过程的分析,那么要得出一个结论,这个结论就是我们的驱动程序到底该做什么?那么从上面的整个流程来看,我们的驱动程序其实总的有两件事要做,首先第一点是要去实现中断处理程序,第二点,我们自己实现好的这个中断处理程序要能够被Linux系统能够调用到,也就说当相应中断产生的时候,要能调用到你这个驱动程序的中断处理程序,那么还要把这个中断处理程序注册到Linux系统当中来,说得更具体点就是注册到irq_desc这个结构当中来,最后当卸载该模块的时候要注销中断。


Linux中注册中断详解

int request_irq(); //0表示成功,或者返回一个错误码。
函数参数解析:
flags:和中断相关的选项,比如说IRQF_DISABLED(SA_INTERRUPT),如果设置该位,表示这是一个快速中断处理程序,否则就是一个慢速中断处理程序,快速中断主要区别在于快速中断保证中断处理的原子性(不被打断);IRQF_SHARED(SA_SHIRQ),该位表明该中断号是多个设备共享的,比如irq_desc[0]表示对应0号中断结构,但是同学们有没有发现它的里面怎么挂了多个中断处理函数?这个就是Linux中比较有特色的地方共享中断,我们多个设备可以共享一个中断号,比如说我的串口可以用0号中断,网卡可以用0号中断,然后你们看到没有,实际上irq_desc[0]里面的处理函数形成一个链表,内核会把该链表中的每一个处理函数都去调用一遍,那么都去调用一遍会不会有问题呢?肯定会有问题,你的网卡根本就没有产生中断,那它的处理函数也会被调用一遍,所以说我们在要求编写我们的中断处理程序的时候首先要检查你的这个设备是不是产生了中断,比如说调用到你这个网卡驱动程序,没有关系,你调用就调用呗,调用进来之后我的网卡中断处理程序自己一检测发现我的网卡根本就没产生中断,那我就立刻退出咯,就不会有任何问题了。
devname:中断的对应的设备的名字。
void *dev_id:共享中断时使用。


中断处理程序详解

我们的驱动程序必须要去实现这个中断处理程序,那么中断处理程序一般是由C代码来写的,它和一般的C程序有什么区别呢?中断处理程序是运行在中断上下文当中的,那么它的行为要受到一定的限制,比如说你不能使用引起阻塞的函数,为什么呢?如果说你一旦阻塞了,那你想一想,特别是对于我们的快速中断,一旦在这儿阻塞了,停下来了,那么我整个系统的中断都没有办法进一步的处理,这个时候对我的系统的性能是一个很大的影响;第二个我们不能使用引起调度的函数。好,这是我们的两个原则。
我们来看看中断处理程序的一般流程,首先我们中断处理程序一进来之后,大家养成一个良好的习惯,就是检查你这个设备是否产生了这个中断,这是第一步;第二步,清除中断标志,因为你不去把这个产生中断的标志清除掉,那么下一步的中断是没有办法进一步产生的;然后我们就去完成相应的硬件操作,比如说我们要从设备拷贝数据啊,或者要往设备里面写数据啊,那么你就根据你这设备的逻辑完成剩下的操作;最后,当我们不再使用中断的时候,比如说我的驱动程序要注销的时候,那么我们一般就要使用free_irq()这个函数把我们注册好的这个中断处理函数给注销掉,其中传入参数irq(中断号),那么这里又有一个问题,如果我只提供中断号,对于共享中断我怎么办?比如说我提供了一个中断号0(irq_desc[0]),但是0下面挂了多个中断处理函数,那你到底是注销哪个呢?所以说我们在注册的时候,特别对于共享中断,我们通常会让它的dev_id不一样,那么它就会根据dev_id注销掉我们想要注销的中断处理函数了。


注册中断处理函数:request_irq()

int request_irq(unsigned int irq,
        irq_handler_t handler,
        unsigned long flags, const char *devname, void *dev_id)

其中传入参数:
irq:对应中断号
handler:中断处理函数
Flags: IRQF_DISABLED: 如果设置该位,表示是一个快速中断处理程序.
IRQF_SHARED: 该位表明该中断号是多个设备共享的。
devname:设备名
dev_id:共享中断时使用。
中断处理程序的特别之处是在中断上下文中运行的,它的行为受到如下限制:
不能使用可能引起阻塞的函数,不能使用可能引起调度的函数。


handler:中断处理函数

检查设备是否产生了中断
清除中断产生标志
相应的硬件操作


注销中断:free_irq()

当设备不再需要使用中断时(通常在驱动卸载时),应当把它们注销掉:
void free_irq (uint irq, void *dev_id);


Linux中断处理框架代码搭建

#include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/irqreturn.h>
MODULE_LICENSE("GPL");

struct file_operations mdev_fops;

struct miscdevice mdev = {
    .minor = 200,
    .name = "mdevice",
    .fops = &mdev_fops
};

static int dev_init (void)
{
    misc_register(&mdev);
    request_irq (irq, dev_init, "mdevice", 0, IRQ_PROBE_SHARED);
    return 0;
}
static void dev_exit (void)
{
    misc_deregister (&mdev);
    free_irq (irq, 0);
    return ;
}

module_init (dev_init);
module_exit (dev_exit);

Linux中断按键驱动硬件操作实现

分析裸机阶段的按键驱动程序
Linux驱动中,硬件初始化的地方:open函数或module_init函数中
我们能不能直接把裸机阶段的按键驱动移植过来呢?答案是能的。

首先我们的按键是与GPIO相连的,因此我们的GPIO功能你首先要去做选择,这是其一,其二,我们的按键是以中断的方式来工作的,所以说我们要从这两个点来复习一下。

今天我们就写一个按键的驱动就行了,其它类似。

在我们Linux驱动程序当中,我们应该在哪个地方去完成这个硬件的初始化呢?实际上在我们Linux驱动开发当中,我们一般会选择两个地方来做硬件的初始化,一个地方呢是我们的open函数,另外一个地方呢是在我们的模块初始化当中来完成硬件的初始化。选哪一个其实没有一个严格的规定,具体情况看公司怎么规定,都可以。

接下来呢我们就首先去Linux驱动代码中去实现按键硬件初始化的代码,我们这里选择在模块初始化中初始化硬件,如下代码:

static int dev_init (void)
{
    misc_register(&mdev);
    request_irq (irq, dev_init, "mdevice", 0, IRQ_PROBE_SHARED);

    //Device hardware initialization
    dev_hw_init ();
    return 0;
}

接下来呢我们就要来实现这个函数,这个函数的实现呢,当然要参考裸机代码,这里我们按键的初始化工作其实只有一项,就是对GPIO引脚的控制寄存器做相应初始化,我们把GPIO物理地址控制寄存器地址定义过来,而在我们Linux内核中不能直接去使用物理地址,我们必须要先把它转化成虚拟地址,通过ioremap()函数转换,所以接下来我们就来操作这个虚拟地址,首先我们要读取这个寄存器里面的原值,为什么呢?因为我们不能破坏里面原有的值,所以我们先读出来,在去设置该值得相应位,最后才写进这个寄存器。
下面是我们实现硬件初始化函数范例:如下代码:

static void dev_hw_init (void)
{
//From your board:K1:EINT8:GPG0:
#define GPGCON  0x56000060
    unsigned int *gpio_config;
    unsigned int data;

    gpio_config = (int *)ioremap(GPGCON, 4);
    data = readw(gpio_config);
    data &= ~(0x3);
    data |= 0x2;
    writew (data, gpio_config);
    return ;
}

那么这样呢,我们的按键硬件方面初始化的函数就实现完了,我们接下来来看硬件的第二部分,我们第一部分就对按键引脚初始化,第二部分呢就是我们要对按键的中断处理做工作,下面我们就是来修改这个注册中断函数,我们前面在大框架过程中我们是在这个注册这个环节留下一些尾巴的,就是注册标志以及中断号,因为当时我们还不清楚怎么设置这些参数,之前只是为了搭建框架而填充的,我们的按键有两个状态,一个是按下,一个是弹起,所以我们选择的标志是IRQF_TRIGGER_FALLING标志,除了这些标志我们可以通过vim的Ctrl-n或sourceinsight中的自动匹配来查询还有其它标志列表。如下:

static int dev_init (void)
{
    misc_register(&mdev);
    request_irq (irq, dev_init, IRQ_TRIGGER_FALLING, "mdevice", 0);

    //Device hardware initialization
    dev_hw_init ();
    return 0;
}

那么我们来完成下一个尾巴,我们下一个尾巴是个大尾巴,就是我们的中断号,我们裸机中获取中断号可以通过寄存器相应位来获取,然而在Linux驱动开发中我们怎么获取这个中断号呢?也就是我们的中断类型怎么和我们的中断号对应呢?我们先教大家来怎么查这个东西,我们通过LookupFile搜索irqs.h这个头文件,或者通过soueceinsight来搜索相应板子的这个irq.h然后找出相应板子的头文件即可,如下图:
这里写图片描述

下面我们打开相应开发板的头文件,会看到一大串定义的中断宏,如下图所示:
这里写图片描述

这里写图片描述

但是如果你想把中断号这个知识给搞透了,那么你还得继续听谢老师讲讲,讲讲这个中断号,它的值是等于多少的?它和我们芯片手册上的中断类型到底是一个什么样的关系?我们先来看它的值,IRQF_EINIT0这个宏展开之后,那么得到IRQF_EINIT0的值是16,那么我们这里通过芯片手册查看到EINT0的中断号是0,那么为什么这里的值是16呢?前面的16个中断号又是谁的呢?在2440上面是16,在6410是32,板子不同有所差异。那么这个OFFSET的中断号是留给谁的呢?答案是留给软中断的,也就是留给软件模拟的中断的。于是我们的这个OFFSET来源就搞清楚了。那么有的同学又问了,你这些中断号是用来干嘛的?根据我们前面裸机所学的知识,我们知道中断号是用来寻找中断表的。

在我们Linux系统当中,当我们的硬件产生中断的时候,处理器会把这个中断号拿到,然后根据这个中断号,利用这个中断号去找到这个描述结构(irq_desc),然后从这个结构当中的处理函数链表中取到相应中断处理函数。那么我们现在得到的中断号是16+实际的序号,有人会问,那么硬件产生的中断号也是16+这个序号吗?下面我们来打开一个文件来分析,打开entry-macro.S,我们中断号获取其实是由get_irqnr_and_base这个宏来做的,如下图:
这里写图片描述

我们先找到这个irqnr(irq_number),实际上这里面保存的就是硬件的中断号,我们高亮这个变量,看看这个中断号是怎么拿到的,高亮这个变量,然后我们可以看到哪些地方操作了irqnr这个变量,所以我们通过高亮之后的代码里很快发现,irqnr里面的值实际上是从INTOFFSET这个寄存器得到的,如下图:
这里写图片描述

当我们的硬件产生中断之后,那么这个中断编号首先是从我们的INTOFFSET这个寄存器里面去读,打开我们的芯片手册,我们来看看这个INTOFFSET寄存器。所以我们这个irqnr读出来这个寄存器里面中断的序号,但是我们这里仅仅是需要也不行啊,这个序号和我们最后要用到的中断号还要差16呀,那么你和这个结构找不到呀,于是我们来看看接下来是怎么做的,如下图:
这里写图片描述

通过上图这个步骤之后,我们后面Linux要用的irq_number于是就产生出来了。

所以我们这里来个总结:在Linux系统中,我们硬件实际上产生的是一个序号或者叫做偏移号,这是硬件实际产生出来的,产生出来之后在寄存器里保存起来的,但是这个号呢和我们后面irq_desc使用方法有些出入,所以说我们去给这个序号+16, 然后就形成了为Linux所用的中断号,那么这就是Linux系统中的中断号,所以我们得出,Linux系统中的中断号是等于序号+基数,为什么要有这个基数,前面我们已经说过了,这个基数是留给软中断使用的。


我们接下来实现按键中断处理函数

函数里我们首先要做的就是检测是否发生了按键中断,其次清除已经发生的按键中断,最后进行中断处理,这里我们就已简单打印来代替;其中的中断检测部分我们可以不做,因为我们的按键在该中断号下只有按键这么一个中断处理函数挂上,所以这一步我们可以暂时不用检查,还有我们后面的中断清除操作是针对外围硬件内部的中断清除,而我们按键的中断清除的寄存器GPIO属于处理器级别的,那么处理器界别的寄存器我们不需要去清除,处理器会自动去清除它,比如说我们用的网卡芯片,我们网卡芯片内部本身就有中断相关的状态寄存器,所以我们对于网卡这种外围设备来说是必须要做中断清除这个步骤的。


基于中断的key.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/io.h>           //ioremap, etc.
#include <linux/fs.h>
#include <linux/interrup.h>     //request_irq, etc.
#include <linux/irqreturn.h>
MODULE_LICENSE("GPL");

struct file_operations key_fops;

struct miscdevice mdev = {
    .minor = 200,
    .name = "mdevice",
    .fops = &key_fops
};

static void key_hw_init (void)
{
//From your board:K1:EINT8:GPG0:
#define GPGCON  0x56000060
    unsigned int *gpio_config;
    unsigned int data;

    gpio_config = (int *)ioremap(GPGCON, 4);
    data = readw(gpio_config);
    data &= ~(0x3);
    data |= 0x2;
    writew (data, gpio_config);
    return ;
}
irqreturn_t key_int(int irq, void *dev_id)
{
    //01.check whether irq or not for the device
    ;
    //02.clear irq issued
    ;
    //03.print value of key
    printk("key down\n");
    return 0;
}
static int key_init (void)
{
    misc_register(&mdev);
    request_irq (IRQ_EINT0, key_int, IRQF_TRIGGER_FALLING, "mdevice", 0);

    //Device hardware initialization
    dev_hw_init ();
    return 0;
}
static void key_exit (void)
{
    misc_deregister (&mdev);
    free_irq (irq, 0);
    return ;
}

module_init (key_init);
module_exit (key_exit);

中断key.c对应的Makefile文件

obj-m:=key.o
KDIR:=/home/redhat6/linux-mini2440
all:
    make modules M=$(PWD) $(KDIR) ARCH=arm CROSS_COMPILE=arm-linux-
.PHONY:clean
clean:
    rm -rf *.o *.elf *.ko *.order* *.sym* *.mod* *.bin

这里写图片描述


Linux中断分层技术


Linux是如何来处理中断嵌套的?

什么叫做中断嵌套呢?就是当一种类型中断发生的时候,又产生了其它的中断,那么其它的中断可以是同类型的,也可以是不同类型的中断,这就是所谓的中断嵌套,不同的OS对中断嵌套有不同的处理办法;我们这里以Linux为主,分为两种比较大的类型,一种就是慢速中断,什么是慢速中断呢?就是指的是在进行中断处理的时候,这个中断的总开关是不关闭的,也就说允许其它类型的中断产生,这就是慢速中断;假如说一个串口中断产生并开始进行处理,处理总时间为7秒钟,当执行到第3秒的时候又来了一个其它类型的中断,比如说网卡中断,那么这个时候Linux系统怎么处理呢?那么Linux呢它会暂停上面的串口处理,转而去执行网卡中断处理程序,加入网卡中断处理程序执行了3秒之后执行完毕,那么它又会回到串口接着执行剩下的中断处理程序,直到把它执行完毕;上面是第一种情况,那么第二种情况是同样是处理串口在执行前3秒时候又来了个串口中断,这个时候Linux会不会去执行新来的串口中断程序呢?答案是不会的,因为这两种类型中断是同类型的,Linux在处理同类型中断的时候,即使你再产生中断,它也不会暂停下来去处理新中断,它就会把它忽略掉,这就意味第二个中断的数据就丢失掉了;当然上面情况都是针对慢速中断情况。下面我们看看快速中断处理情况,快速中断就是在执行中断处理时候,这个中断的总开关是关闭的,也就是说是不允许其它中断打断的,比如说串口执行到第3秒时,这时候来了个网卡,你的网卡根本就产生不了中断,因为中断总开关是被关闭的,所以说你这个网卡中断是被忽略掉的,也就是意味数据丢掉,同类型新串口中断一样被忽略掉;综上得知,快速中断或慢速中断中的同类型中断都会出现这种“中断丢失”的现象,那么中断丢失是不是我们希望看到的情况呢?当然不是,因为我们的新中断需要处理的数据得不到处理,比如说我们的网卡收到一个数据包,那么它产生不了中断,那么这个数据包自然而然也没有办法送到系统当中去了,这显然不是我们希望看到的情况,那么怎么来解决这个问题呢?这里从而我们引出了“中断分层”的技术。


中断分层技术

假如说一个中断处理总共需要10秒钟的时间,现在当我们这个中断处理程序运行到第7秒钟的时候,这时候产生新类型中断,但是这个新类型中断呢很不幸丢失掉了,那么人们就在想怎么解决这个问题,于是想了一个很简单的办法就是我们能不能把这个中断处理的时间尽量缩短,比如说我们让它在第6秒时候就完成中断处理,这样就极大减少了中断丢失的可能,因为你后面4秒钟都不属于这个中断处理程序了,那你再来其它类型的中断也能得到处理了,所以说就减少了中断丢失可能;其实中断处理程序里面会做两种类型的工作,一种类型的工作是和硬件相关的,比如说网卡接收数据这个中断处理程序,那你要从相应寄存器中去读取收到的这个数据,那么这部分工作肯定就是和硬件密切相关的,第二部分工作就是和硬件无关的工作,比如说做一些检测或数据的相应处理啊,这些工作和硬件本身就没有太大关系,那么它就不一定要放在中断处理程序当中来做了;于是人们想了个办法,就是把和硬件密切相关的工作放到中断处理程序当中完成,把和硬件无关的操作就丢到系统给的队列里让内核空闲时候去完成,于是出现了“上半部”和“下半部”的概念,上半部就是和硬件密切相关的,这部分就必须在中断处理程序当中做,下半部呢就是硬件无关的处理,就不放在中断处理中执行了;那么问题来了,下半部要抛出去,怎么实现抛出去的工作呢?


中断分层方式

中断分层方式分为“软中断”、“tasklet”、“工作队列”,我们先在用得最广泛的是工作队列,你可以把工作队列的场景想象成有一个核CPU窗口,窗口外面排满打饭的学生,这些学生每个学生就是每个工作,我们的内核为每一个窗口都安排了一个为学生打饭的内核线程,这个内核线程就会选择相对比较空闲时候就会为排队的每一个学生打饭,每个打完饭的学生就会从队列中离开,也就是说这个专门打饭的内核线程就会在内核相对空闲的时候自动去检查学生队列有没有要打饭的学生,如果有,就帮他打饭并让他离开队列。
这里写图片描述

我们内核中是如何来描述这个工作队列的呢?我们内核用struct workqueue_struct来描述一个工作队列的,用struct work_struct来描述一个工作的,这项工作其实就是去执行一个函数,所以说工作这个结构中func这个成员是比较重要的。所以我们的步骤就出来了,首先是创建工作队列(create_workqueue),其次创建工作(INIT_WORK),最后是提交工作(queue_work),提交工作就是把这个工作挂到工作队列中去。
但是大多数情况下,驱动并不需要自己去建立工作队列,只需要创建工作,然后将工作提交到内核已经定义好的工作队列keventd_wq,通过schedule_work函数提交工作到默认队列即可。

还有个问题,为什么硬件中断的时候不直接把硬件相关和软件相关的操作都放到队列中呢?这是不可能的,因为硬件的工作依赖于中断机制,没有中断做前提,硬件无法工作。

queue.c工作队列范例

#include <linux/init.h>
#include <linux/module.h>

struct workqueue_struct *my_wq;
struct work_struct *work1;
struct work_struct *work2;

MODULE_LICENSE("GPL");

void work1_func(struct work_struct *work)
{
    printk("this is work1->\n");    
}

void work2_func(struct work_struct *work)
{
    printk("this is work2->\n");    
}
int init_que(void)
{   
    //1. 创建工作队列
    my_wq = create_workqueue("my_que");

    //2. 创建工作1
    work1 = kmalloc(sizeof(struct work_struct),GFP_KERNEL);
    INIT_WORK(work1, work1_func);

    //3. 挂载(提交)工作
    queue_work(my_wq,work1);

    //2. 创建工作2
    work2 = kmalloc(sizeof(struct work_struct),GFP_KERNEL);
    INIT_WORK(work2, work2_func);

    //3. 挂载(提交)工作
    queue_work(my_wq,work2);

    return 0;
}
void clean_que()
{
    kfree (work1);
    kfree (work2);
}

module_init(init_que);
module_exit(clean_que);

queue.c对应的Makefile

obj-m:=key.o
KDIR:=/home/redhat6/linux-mini2440
all:
    make modules M=$(PWD) $(KDIR) ARCH=arm CROSS_COMPILE=arm-linux-
.PHONY:clean
clean:
    rm -rf *.o *.elf *.ko *.order* *.sym* *.mod* *.bin

Linux中断分层技术修改key.c

// 提交下半部(硬件无关工作)

#include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/io.h>           //ioremap, etc.
#include <linux/fs.h>
#include <linux/interrup.h>     //request_irq, etc.
#include <linux/irqreturn.h>
#include <linux/workqueue.h>    //workqueue
MODULE_LICENSE("GPL");

struct file_operations key_fops;

struct miscdevice mdev = {
    .minor = 200,
    .name = "mdevice",
    .fops = &key_fops
};
static struct work_struct work;
//static struct workqueue_struct *workqueue;

void key_work (struct work_struct *work)
{
    printk("key down\n");
    return ;
}

static void key_hw_init (void)
{
//From your board:K1:EINT8:GPG0:
#define GPGCON  0x56000060
    unsigned int *gpio_config;
    unsigned int data;

    gpio_config = (int *)ioremap(GPGCON, 4);
    data = readw(gpio_config);
    data &= ~(0x3);
    data |= 0x2;
    writew (data, gpio_config);
    return ;
}

irqreturn_t key_int(int irq, void *dev_id)
{
    //01.check whether irq or not for the device
    printk("check whether irq or not for the device\n");
    //02.clear irq issued
    printk("clear irq issued\n");

    //hand on work
    INIT_WORK(&work, key_work);
    schedule_work(&work);

    return 0;
}
static int key_init (void)
{
    misc_register(&mdev);
    request_irq (IRQ_EINT0, key_int, IRQF_TRIGGER_FALLING, "mdevice", 0);

    //Device hardware initialization
    dev_hw_init ();
    return 0;
}
static void key_exit (void)
{
    misc_deregister (&mdev);
    free_irq (irq, 0);
    return ;
}

module_init (key_init);
module_exit (key_exit);

按键定时器去抖

前面一课把我们的按键驱动程序放到开发板上安装测试时,我们会发现我们按一下按键,它会打印多条信息,这并不是我们想要的,我们只是需要按一下打印一条信息。这个其实就是由于这个按键的抖动造成的,那么什么是按键的抖动呢?我们按键所用的开关通常都是机械弹性开关,当机械触点断开闭合时,由于机械触点的弹性作用,开关不会马上的接通或断开。因而在闭合断开的瞬间总是伴随有一连串的抖动。
按键祛痘的方法有两种,一种是单片机中常用的电路祛痘,另一种就是软件的延时祛痘,软件延时方法可以是轮询和定时器。但是延时会大大影响处理器性能,所以我们在Linux中使用定时器来搞定。Linux内核使用struct timer_list来描述一个定时器,其中比较重要成员是expires和function函数指针,expires是指定延时时间。
那么我们怎么来用这个定时器呢?首先定义定时器变量,其次是初始化,初始化中需要我们使用init_timer()函数初始化和自己去设置超时函数,然后就是add_timer来注册定时器,最后启动通过mod_timer这个定时器。那么我们的定时器是不是循环计时呢?比如说我们定时器5秒过时后,会不会又从0开始计时呢?答案是不会的,我们的定时器只能执行一次,然后就失效了,如果你还想它延时,就再执行一次mod_timer,前面的初始化也可以不用做了。注销定时器使用del_timer来注销定时器。

一般初始化定时器的工作就放在模块的初始化当中来完成,首先我们初始化定时器第一步,就是初始化这个定时器init_timer(),然后设置超时函数,其中最重要的是我们的启动定时器放在什么位置,这个我们必须要搞清楚启动定时器和超时函数之间的关系,也就是说根据超时函数和启动定时器的关系,我们把定时器要执行的函数就放到超时函数里实现了,那么这个定时器的启动就放到中断后半部的函数处理中来执行。所以中断后半部函数主要通过mod_timer来间接执行按键打印工作。

定时器工作原理其实就是,在多次抖动中选择第一个产生的触发来执行计时,计时时间到了之后再来检查是否还有触发,有就执行超时函数,否则不执行。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/io.h>           //ioremap, etc.
#include <linux/fs.h>
#include <linux/interrup.h>     //request_irq, etc.
#include <linux/irqreturn.h>
#include <linux/workqueue.h>    //workqueue
#include <linux/timer.h>        //timer
MODULE_LICENSE("GPL");
//From your board:K1:EINT8:GPG0:
#define GPGCON  0x56000060
#define GPGDAT  0x56000064
unsigned int *gpio_config;
unsigned int data;

struct file_operations key_fops;

struct miscdevice mdev = {
    .minor = 200,
    .name = "mdevice",
    .fops = &key_fops
};
static struct work_struct work;
static struct timer_list key_timer;
//static struct workqueue_struct *workqueue;

//void key_work (struct work_struct *work)
//{
//  printk("key down\n");
//  return ;
//}

void key_timer_func(unsigned long data)
{
    unsigned int key_value;
    gpio_data = ioremap(GPGDAT, 4);
    key_value = readw (gpio_data);

    if (key_value == 0)
        printk("Key down!\n");
    return ;
}

void key_work_func (struct work_struct *work)
{
    mod_timer (&key_timer, jiffies+HZ/10);  //1000/10 = 100ms
    return ;
}

static void key_hw_init (void)
{
    gpio_config = (int *)ioremap(GPGCON, 4);
    data = readw(gpio_config);
    data &= ~(0x3);
    data |= 0x2;
    writew (data, gpio_config);
    return ;
}

irqreturn_t key_int(int irq, void *dev_id)
{
    //01.check whether irq or not for the device
    printk("check whether irq or not for the device\n");
    //02.clear irq issued
    printk("clear irq issued\n");

    //hand on work
    schedule_work(&work);

    return 0;
}
static int key_init (void)
{
    misc_register(&mdev);
    request_irq (IRQ_EINT0, key_int, IRQF_TRIGGER_FALLING, "mdevice", 0);

    //Device hardware initialization
    dev_hw_init ();

    INIT_WORK(&work,key_work_func);

    init_timer(&key_timer);
    key_timer.function = key_timer_func;

    add_timer (&key_timer);
    return 0;
}
static void key_exit (void)
{
    misc_deregister (&mdev);
    free_irq (IRQ_EINT0, 0);
    return ;
}

module_init (key_init);
module_exit (key_exit);

驱动支持多按键优化

我们首先要知道的理论是同一个设备可以注册多个中断,所以于是就有了我们对下面这个注册中断修改:

static int key_init (void)
{
    misc_register(&mdev);
    request_irq (IRQ_EINT0, key_int, IRQF_TRIGGER_FALLING, "mdevice", 0);
    request_irq (IRQ_EINT11, key_int, IRQF_TRIGGER_FALLING, "mdevice", 0);

    //Device hardware initialization
    dev_hw_init ();

    INIT_WORK(&work,key_work_func);

    init_timer(&key_timer);
    key_timer.function = key_timer_func;

    add_timer (&key_timer);
    return 0;
}

然后我们继续检查,发现其它地方大多数不需要修改,中断上半部的硬件初始化部分需要修改一下,因为里面的寄存器相应位要添加相应的中断设置位,如下代码所示:

static void key_hw_init (void)
{
    gpio_config = (int *)ioremap(GPGCON, 4);
    data = readw(gpio_config);
    data &= ~(0x3);
    data |= 0x2;        //K1:GPG0:[1:0]
    data |= (0x2<<6);   //K2:GPG3:[7:6]
    writew (data, gpio_config);
    return ;
}

然后我们继续检查,发现下半部处理的时候需要分别检测KEY1和KEY2是否按下,根据原理图得知,按键按下为低电平,所以最后修改如下代码:

void key_timer_func(unsigned long data)
{
    unsigned int key_value;
    gpio_data = ioremap(GPGDAT, 4);

    key_value = readw (gpio_data)&0x01; //0001
    if (key_value == 0)
        printk("Key1 down!\n");

    key_value = readw (gpio_data)&0x04; //0100
    if (key_value == 0)
        printk("Key2 down!\n");
    return ;
}

多按键驱动完整代码key.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/io.h>           //ioremap, etc.
#include <linux/fs.h>
#include <linux/interrup.h>     //request_irq, etc.
#include <linux/irqreturn.h>
#include <linux/workqueue.h>    //workqueue
#include <linux/timer.h>        //timer
MODULE_LICENSE("GPL");
//From your board:K1:EINT8:GPG0:
#define GPGCON  0x56000060
#define GPGDAT  0x56000064
unsigned int *gpio_config;
unsigned int data;

struct file_operations key_fops;

struct miscdevice mdev = {
    .minor = 200,
    .name = "mdevice",
    .fops = &key_fops
};
static struct work_struct work;
static struct timer_list key_timer;
//static struct workqueue_struct *workqueue;

//void key_work (struct work_struct *work)
//{
//  printk("key down\n");
//  return ;
//}

void key_timer_func(unsigned long data)
{
    unsigned int key_value;
    gpio_data = ioremap(GPGDAT, 4);

    key_value = readw (gpio_data)&0x01; //0001
    if (key_value == 0)
        printk("Key1 down!\n");

    key_value = readw (gpio_data)&0x04; //0100
    if (key_value == 0)
        printk("Key2 down!\n");
    return ;
}

void key_work_func (struct work_struct *work)
{
    mod_timer (&key_timer, jiffies+HZ/10);  //1000/10 = 100ms
    return ;
}

static void key_hw_init (void)
{
    gpio_config = (int *)ioremap(GPGCON, 4);
    data = readw(gpio_config);
    data &= ~(0x3);
    data |= 0x2;        //K1:GPG0:[1:0]
    data |= (0x2<<6);   //K2:GPG3:[7:6]
    writew (data, gpio_config);
    return ;
}

irqreturn_t key_int(int irq, void *dev_id)
{
    //01.check whether irq or not for the device
    printk("check whether irq or not for the device\n");
    //02.clear irq issued
    printk("clear irq issued\n");

    //hand on work
    schedule_work(&work);

    return 0;
}
static int key_init (void)
{
    misc_register(&mdev);
    request_irq (IRQ_EINT0, key_int, IRQF_TRIGGER_FALLING, "mdevice", 0);
    request_irq (IRQ_EINT11, key_int, IRQF_TRIGGER_FALLING, "mdevice", 0);

    //Device hardware initialization
    dev_hw_init ();

    INIT_WORK(&work,key_work_func);

    init_timer(&key_timer);
    key_timer.function = key_timer_func;

    add_timer (&key_timer);
    return 0;
}
static void key_exit (void)
{
    misc_deregister (&mdev);
    free_irq (IRQ_EINT0, 0);
    return ;
}

module_init (key_init);
module_exit (key_exit);

按键应用层测试代码key_app.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
int main (void)
{
    int fd, key_number;

    fd = open ("/dev/my_device200", O_RDONLY);
    if (fd < 0)
    {
        printf ("dev/my_device200 open error!\n");
        exit (1);
    }

    read (fd, &key_number, 4);

    if (key_number == 1)
        printf ("key number: %d\n", key_number);
    else if (key_number == 2)
        printf ("key number: %d\n", key_number);
    else
        printf ("key number: %d\n", key_number);
    return 0;
}

对应的Makefile

obj-m := key.o
KDIR := /home/S5-driver/lesson7/linux-tq2440/
all:
    make -C $(KDIR) M=$(PWD) modules CROSS_COMPILE=arm-linux- ARCH=arm
    arm-linux-gcc -static key_app.c -o key_app.elf
clean:
    rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.bak *.order *.elf

阻塞型驱动设计

其中比较重要的就是读写阻塞和等待队列的概念(候车室睡觉)。


阻塞存在必要性

当一个设备无法立刻满足用户的读写请求时应当如何处理?
例如:调用read时,设备没有数据提供,但以后可能会有;或者一个进程试图向设备写入数据,但是设备暂时没有准备好接收数据。当上述情况发生的时候,驱动程序应当阻塞进程,使它进入等待状态,直到请求可以得到满足。就好像坐公交车,到了车站但没有公交车,所以我们只有等待。


阻塞型驱动的访问模型

A进程向mem读数据,但mem暂时没数据,于是mem驱动将A放入候车室并让其睡眠,过段时间之后mem有数据了,于是mem驱动就从候车室中唤醒A并让A读取数据。

驱动程序有阻塞应用程序的责任,特别是进程读写设备数据的时候,驱动程序就必须把进程放进等待队列里并让其睡眠,当条件为真时,驱动再去唤醒队列里的进程,并让其出来执行读写操作。

内核等待队列

在实现阻塞驱动的过程中,也需要有一个候车室来安排被阻塞的进程休息,当唤醒它们的条件成熟时,则可以从候车室中将这些进程唤醒。而这个候车室就是等待队列。

注意:我们所有驱动程序的读写功能都要实现阻塞型的驱动模型!,也就是说我们前面的所有驱动设计模型都是不对的。


等待队列操作流程

step01 定义等待队列:
wait_queue_head_t my_queue;

step02 初始化等待队列:
init_waitqueue_head(&my_queue);

step02 定义+初始化等待队列:
DECLARE_WAIT_QUEUE_HEAD(my_queue);

step03 进入等待队列,睡眠
wait_event (queue, condition);
condition为真时,立即返回;否则让进程进入TASK_UNINTERRUPTIBLE模式的睡眠,并挂在queue参数所指定的等待队列上。

step03
wait_event_interruptible(queue,condition);
condition为真时,立即返回;否则让进程进入TASK_INTERRUPTIBLE模式的睡眠,并挂在queue参数所指定的等待队列上。

step03
int wait_event_killable(queue, condition);
当condition为真时,立即返回;否则让进程进入TASK_KILLABLE的睡眠,并挂在queue参数所指定的等待队列上。

step04 从等待队列中唤醒进程
wake_up(wait_queue_t *q);
从等待队列q中唤醒状态为TASK_UNINTERRUPTIBLE, ASK_INTERRUPTIBLE, ASK_INTERRUPTIBLE的所有进程。

wake_up_interruptible(wait_queue_t *q);
从等待队列q中唤醒状态为TASK_INTERRUPTIBLE的进程。

首先我们要初始化等待队列,这步操作一般都是在模块初始化中完成,如下图所示:

static int key_init (void)
{
    my_class = class_create (THIS_MODULE, "my_device");
    device_create (my_class, NULL, MKDEV(10,200), NULL, "my_device%d", 200);
    misc_register(&mdev);
    request_irq (IRQ_EINT0, key_int, IRQF_TRIGGER_FALLING, "mdevice", 0);
    request_irq (IRQ_EINT11, key_int, IRQF_TRIGGER_FALLING, "mdevice", 0);

    //Device hardware initialization
    dev_hw_init ();

    INIT_WORK(&work,key_work_func);

    init_timer(&key_timer);
    key_timer.function = key_timer_func;

    add_timer (&key_timer);

    init_waitqueue_head (&wait_queue);
    return 0;
}

其次我们在驱动的读写操作中假设没有数据读写,来设立队列的等待,并在读写完成后再把条件值改回等待需要的条件,如下图所示:

ssize_t key_read (struct file *filp, char __user *buf, size_t size, loff_t *pos)
{
    //if condition be true, continue, or not be waited
    //following: if key_number !=0, go to execute following code
    wait_event (wait_queue, dev_number !=0);
    copy_to_user(buf, &key_number, 4);
    key_number = 0;
    return 0;
}

最后我们要在合适的地方唤醒,这里我们根据应用程序要读的是dev_number值,所以我们只要找到驱动中设置dev_number值的位置就可以了,也就是说,启动中设置dev_number值之后就可以唤醒应用程序了,如下图所示:

void key_timer_func(unsigned long data)
{
    unsigned int key_value;
    gpio_data = ioremap(GPGDAT, 4);

    key_value = readw (gpio_data)&0x01; //0001
    if (key_value == 0)
    {
        key_number = 1;
        printk("Key1 down!\n");
    }


    key_value = readw (gpio_data)&0x04; //0100
    if (key_value == 0)
    {
        key_number = 2;
        printk("Key2 down!\n");
    }

    wake_up (&wait_queue);  //wake up queue if key_number be value

    return ;
}

下面我们根据出错提示来补充相应头文件和定义全局变量,如下图所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/io.h>           //ioremap, etc.
#include <linux/fs.h>
#include <linux/interrup.h>     //request_irq, etc.
#include <linux/irqreturn.h>
#include <linux/workqueue.h>    //workqueue
#include <linux/timer.h>        //timer
MODULE_LICENSE("GPL");
//From your board:K1:EINT8:GPG0:
#define GPGCON  0x56000060
#define GPGDAT  0x56000064
unsigned int *gpio_config;
unsigned int data;
static struct work_struct work;
static struct timer_list key_timer;
struct class *my_class;
unsigned int key_number;
wait_queue_head_t wait_queue;
struct file_operations key_fops;

阻塞型按键驱动完整代码key.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/io.h>           //ioremap, etc.
#include <linux/fs.h>
#include <linux/interrup.h>     //request_irq, etc.
#include <linux/irqreturn.h>
#include <linux/workqueue.h>    //workqueue
#include <linux/timer.h>        //timer
MODULE_LICENSE("GPL");
//From your board:K1:EINT8:GPG0:
#define GPGCON  0x56000060
#define GPGDAT  0x56000064
unsigned int *gpio_config;
unsigned int data;
static struct work_struct work;
static struct timer_list key_timer;
struct class *my_class;
unsigned int key_number;
wait_queue_head_t wait_queue;
struct file_operations key_fops;


ssize_t key_read (struct file *filp, char __user *buf, size_t size, loff_t *pos)
{
    //if condition be true, continue, or not be waited
    //following: if key_number !=0, go to execute following code
    wait_event (wait_queue, dev_number !=0);
    copy_to_user(buf, &key_number, 4);
    key_number = 0;
    return 0;
}

struct miscdevice mdev = {
    .minor = 200,
    .name = "mdevice",
    .fops = &key_fops
};
static struct work_struct work;
static struct timer_list key_timer;
//static struct workqueue_struct *workqueue;

//void key_work (struct work_struct *work)
//{
//  printk("key down\n");
//  return ;
//}

void key_timer_func(unsigned long data)
{
    unsigned int key_value;
    gpio_data = ioremap(GPGDAT, 4);

    key_value = readw (gpio_data)&0x01; //0001
    if (key_value == 0)
    {
        key_number = 1;
        printk("Key1 down!\n");
    }


    key_value = readw (gpio_data)&0x04; //0100
    if (key_value == 0)
    {
        key_number = 2;
        printk("Key2 down!\n");
    }

    wake_up (&wait_queue);  //wake up queue if key_number be value

    return ;
}

void key_work_func (struct work_struct *work)
{
    mod_timer (&key_timer, jiffies+HZ/10);  //1000/10 = 100ms
    return ;
}

static void key_hw_init (void)
{
    gpio_config = (int *)ioremap(GPGCON, 4);
    data = readw(gpio_config);
    data &= ~(0x3);
    data |= 0x2;        //K1:GPG0:[1:0]
    data |= (0x2<<6);   //K2:GPG3:[7:6]
    writew (data, gpio_config);
    return ;
}

irqreturn_t key_int(int irq, void *dev_id)
{
    //01.check whether irq or not for the device
    printk("check whether irq or not for the device\n");
    //02.clear irq issued
    printk("clear irq issued\n");

    //hand on work
    schedule_work(&work);

    return 0;
}
static int key_init (void)
{
    my_class = class_create (THIS_MODULE, "my_device");
    device_create (my_class, NULL, MKDEV(10,200), NULL, "my_device%d", 200);
    misc_register(&mdev);
    request_irq (IRQ_EINT0, key_int, IRQF_TRIGGER_FALLING, "mdevice", 0);
    request_irq (IRQ_EINT11, key_int, IRQF_TRIGGER_FALLING, "mdevice", 0);

    //Device hardware initialization
    dev_hw_init ();

    INIT_WORK(&work,key_work_func);

    init_timer(&key_timer);
    key_timer.function = key_timer_func;

    add_timer (&key_timer);

    init_waitqueue_head (&wait_queue);
    return 0;
}
static void key_exit (void)
{
    misc_deregister (&mdev);
    free_irq (IRQ_EINT0, 0);
    return ;
}

module_init (key_init);
module_exit (key_exit);



下面的应用程序就是上面驱动程序的测试程序,代码如下图所示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
int main (void)
{
    int fd, key_number;

    fd = open ("/dev/my_device200", O_RDONLY);
    if (fd < 0)
    {
        printf ("dev/my_device200 open error!\n");
        exit (1);
    }

    read (fd, &key_number, 4);

    if (key_number == 1)
        printf ("key number: %d\n", key_number);
    else if (key_number == 2)
        printf ("key number: %d\n", key_number);
    else
        printf ("key number: %d\n", key_number);
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值