从零开始之驱动发开、linux驱动(十二、字符驱动之按键中断驱动)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_16777851/article/details/82670854

前面几节我简要分析了linux中的异常的初始化以及调用流程。

详细分析了中断向量表的搬移,中断的初始化流程,中断注册,以及中断发生后的调用流程。

本节就使用一下中断。

下面先看一下源码。

#include <linux/fs.h>       /* 包含file_operation结构体 */
#include <linux/init.h>     /* 包含module_init module_exit */
#include <linux/module.h>   /* 包含LICENSE的宏 */
#include <asm/uaccess.h>
#include <linux/io.h>
#include <linux/device.h>
#include <linux/gpio.h>
#include <mach/gpio.h>
#include <asm/gpio.h>
#include <linux/gfp.h>
#include <linux/interrupt.h>


static unsigned int major;
static struct class *third_class;
static struct device *third_dev;

/* 自定义的中断处理函数 */
static irqreturn_t irq_handler(int irq, void *dev_id)
{
    printk(KERN_INFO"irq_handler %d\n", irq);
    return IRQ_HANDLED;
}

/* 打开设备 */
static int third_drv_open(struct inode *inode, struct file *file)
{
    int ret = 0;

    /* 申请中断,我们的是外部中断,触发方式是上升和下降沿 */
    ret = request_irq(IRQ_EINT(2), irq_handler, IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING, "irq-eint2",NULL);
    if(ret)
    {
        printk(KERN_ERR"request_irq IRQ_EINT(2) fail");
    }
    ret = request_irq(IRQ_EINT(3), irq_handler, IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING, "irq-eint3",NULL);
    if(ret)
    {
        printk(KERN_ERR"request_irq IRQ_EINT(3) fail");
    }

    printk(KERN_INFO"printk third_drv_open\n");

    return 0;
}

/* 关闭设备 */
int third_drv_close(struct inode *inode, struct file *file)
{
    free_irq(IRQ_EINT(2), NULL);
    free_irq(IRQ_EINT(3), NULL);

    return 0;
}

static const struct file_operations third_drv_file_operation = {
    .owner      = THIS_MODULE,
    .open       = third_drv_open,
    .release    = third_drv_close,
};



static int __init third_drv_init(void)
{
    /* 获取一个自动的主设备号 */
    major =  register_chrdev(0,"third_drv",&third_drv_file_operation);
    if(major < 0)
    {
        printk(KERN_ERR"register_chrdev third_drv fail \n");
        goto err_register_chrdev;
    }

    /* 创建一个类 */
    third_class = class_create(THIS_MODULE, "third_class");
    if(!third_class)
    {
        printk(KERN_ERR"class_create third_class fail\n");
        goto err_class_create;
    }

    /* 创建从属这个类的设备 */
    third_dev = device_create(third_class,NULL,MKDEV(major, 0), NULL, "button");
    if(!third_dev)
    {
        printk(KERN_ERR"device_create third_dev fail \n");
        goto err_device_create;
    }


/* 倒影式错误处理机制 */
err_device_create:
    class_destroy(third_class);
err_class_create:
    unregister_chrdev(major,"third_drv");
err_register_chrdev:

    return -EIO;
}


static void __exit third_drv_exit(void)
{
    /* 注销类里面的设备 */
    device_unregister(third_dev);
    /* 注销类 */
    class_destroy(third_class);
    /* 注销字符设备 */
    unregister_chrdev(major,"third_drv");
}

module_init(third_drv_init);
module_exit(third_drv_exit);
MODULE_LICENSE("GPL");
 

其它的和我们之前的都是一样的,这里唯一的就是加入了中断和release(close)函数。

其中release函数对应着应用层的close函数。

当然这里我们偷了个懒,两个中断源,只做了1个中断处理函数。

通常是要写两个中断处理函数的,我们上面的虽然只做了一个,但也可以用中断号来区分,并不影响我们对程序的理解。

其中request_irq会为其分配并添加cation结构,使能中断标志等。

其中request_irq会为检查并释放action结构,如果是共享中断的最后一个或非共享中断,则删除并释放掉该cation,并关机该中断。否则如果是共享中断且有多个,则只删除并释放掉该action。

接下,来看一下中断请求函数的参数。

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev);

第一个参数irq,表示中断号,可以通过内核代码查看到

第二个参数handler,就是我们自己写的中断处理程序

第三个参数flag,用来标记是该中断是那种类型

第四个参数name,无实际作用,方便在/proc 或其它调试接口查看

第五个参数dev,用来区别共享中断的。

先说flag

flag标志有很多,包括外部中断的边沿触发,电平触发等,是否线程化中断,是否可以挂起等,其中最重要的是一个叫做共享中断标志的

#define IRQF_TRIGGER_NONE	0x00000000
#define IRQF_TRIGGER_RISING	0x00000001
#define IRQF_TRIGGER_FALLING	0x00000002
#define IRQF_TRIGGER_HIGH	0x00000004
#define IRQF_TRIGGER_LOW	0x00000008
#define IRQF_TRIGGER_MASK	(IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
				 IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE	0x00000010

#define IRQF_DISABLED		0x00000020
#define IRQF_SHARED		0x00000080        /* 共享中断 */
#define IRQF_PROBE_SHARED	0x00000100
#define __IRQF_TIMER		0x00000200
#define IRQF_PERCPU		0x00000400
#define IRQF_NOBALANCING	0x00000800
#define IRQF_IRQPOLL		0x00001000
#define IRQF_ONESHOT		0x00002000
#define IRQF_NO_SUSPEND		0x00004000
#define IRQF_FORCE_RESUME	0x00008000
#define IRQF_NO_THREAD		0x00010000
#define IRQF_EARLY_RESUME	0x00020000

#define IRQF_TIMER		(__IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD)

前面我们分析过中断的注册函数,以及调用流程,知道了每个中断的注册都是一个action和中断源的挂接。

而action则是按照单向链表形式来实现的,即一个中断号,可以对应有多个cation(注册多个处理函数)

假设中断号60为共享中断,则它的在它里面注册的所有处理函数,在注册时都必须标记为共享中断。

并用第五个参数dev来做好标记,将来释放该中断时通过dev来查找到链表中的该action项,并删除然后释放。

非共享中断(独立中断),则不要求dev是否设置(因为一个中断只对应一个中断处理函数,只要发生中断,肯定就是这个设备的,所以不用检查)

关于中断处理函数的写法

/* 下面这种类型的函数,它的输入参数分别是中断号和dev标记 */
typedef irqreturn_t (*irq_handler_t)(int, void *);
/* 它的返回值则是固定的宏 */

/**
 * enum irqreturn
 * @IRQ_NONE		interrupt was not from this device
 * @IRQ_HANDLED		interrupt was handled by this device
 * @IRQ_WAKE_THREAD	handler requests to wake the handler thread
 */
enum irqreturn {
	IRQ_NONE		= (0 << 0),        /* 共享中断中不是该设备所以返回这个 */
	IRQ_HANDLED		= (1 << 0),        /* 非共享中断和共享中断找到设备返回这个 */
	IRQ_WAKE_THREAD		= (1 << 1),    /* 有的线程可能在睡眠等待这个中断,这个表示执行完该中断了,系统应该唤醒等待的线程了 */
};

typedef enum irqreturn irqreturn_t;


通常中断处理函数应该写为下面形式
irqreturn_t irq_handler_name(int irq,void *dev_id)
{
    /* 如果该中断号是共享中断,则进入函数先检查是否是本dev_id所在外设的中断 */
    if(!is_this_dev_irq(dev_id))
    {
        /* 不是,则返回该标号 */
        rerurn IRQ_NONE;
    }
    
    /* 是本中断,则中断处理 */
    .....
    
    /* 最后返回处理完毕 */
    retutn IRQ_HANDLED;
}

接下来看一下实验效果。

我们上面的驱动程序只写了open和release函数。

其中中断函数的申请和释放分别就是在open和release函数中的。

所以只要能打开设备文件和关机设备文件就可以调用对应的函数。

一种方式是我们写一个应用程序,如下我们按下键盘,发生中断会打印出调试语句。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc,char *argv[])
{
    char buf[2];
    int fd = open("/dev/button", O_RDWR);

    if(fd < 0)
    {   
        printf("open /dev/%s fail\n",argv[1]);
        return -1; 
    }   

    while(1)
    {   
       
    }   

    return 0;
}

另一种是使用sh自带的命令打开设备文件

可以看到,安装驱动前并没有button设备1

安装后就有了2

查看系统中断使用情况3

使用exec命令后,可以发现申请到了中断 ,其函数名称和我们软件中的一样 4

这里简单说明一下exec命令

exec命令通常在sh中用于调用其它命令

exec 5</dev/button 

比如上面命令的作用是把/dev/button重定向到当前sh应用进程的第五个文件中。

其实际作用是以当前sh为应用程序,用文件记录表中的第五项来打开/dev/button(其中文件记录表时应用程序使用open函数打开的文件每打开一个都会把该文件的描述指针保存在文件记录表中,open的返回值就是该表(数组)的下标(文件描述符))】

查看当前进程的PID,为1042

未使用exec命令打开前可以看到,5这个文件描述符fd并没被使用。而使用exec 命令打开后,可以发现5这个文件描述符已经是被打开的。

其中每个任务默认的文件描述符的0,1,2分别代表标准输入,标准输出和标准错误。

因为我们的5是没用的,所以我们用5号描述符可以打开,如果使用了,则会打开失败出错。

其中关闭命令也很简单,使用exec 5<&-,其作用是把&-标号重定位到5文件描述符。

即调用当前sh,使用close函数,关闭当前sh进程的5号文件。

exec只是为了减少写应用程序,加快调试驱动速度。

因为中断框架中已经帮我们实现好了,在我们注册中断时使能中断。所以我们上面的外部中断可以直接调试。

即只要打开设备文件,申请好中断,触发中断,即会打印出相应的调试信息。

分别按下外部中断2和外部中断3,可以看到打印出来了中断函数里面的内容。(因为是上升和下降沿触发,所以每次按键有两个中断,因为没做消抖处理,所以35号中断按键,多次进入中断函数。)

/* 自定义的中断处理函数 */
static irqreturn_t irq_handler(int irq, void *dev_id)
{
    printk(KERN_INFO"irq_handler %d\n", irq);
    return IRQ_HANDLED;
}

这里要注意一点:

之前我的启动信息中加入了每条信息的时间,后来为了加快启动速度去掉来时间。结果这个程序调试了好久都不能打印出中断里面的信息。

之后查找是因为没加打印等级。


#define KERN_EMERG	KERN_SOH "0"	/* system is unusable */
#define KERN_ALERT	KERN_SOH "1"	/* action must be taken immediately */
#define KERN_CRIT	KERN_SOH "2"	/* critical conditions */
#define KERN_ERR	KERN_SOH "3"	/* error conditions */
#define KERN_WARNING	KERN_SOH "4"	/* warning conditions */
#define KERN_NOTICE	KERN_SOH "5"	/* normal but significant condition */
#define KERN_INFO	KERN_SOH "6"	/* informational */
#define KERN_DEBUG	KERN_SOH "7"	/* debug-level messages */

#define KERN_DEFAULT	KERN_SOH "d"	/* the default kernel loglevel */

内核中使用printk函数,是需要设置打印等级的,这样最终的版本可以设置打印等级,高于这个等级的不重要信息,启动或运行时就可以不用打印出来。

猜你喜欢

转载自blog.csdn.net/qq_16777851/article/details/82670854