T6 中断编程

1.中断设备树

1.1概述

  • 在linux内核中通过中断号管理中断,与外设一一对应
  • 中断发生后会有中断回调函数,在linux中通过``函数处理中断
  • 外围设备大部分会产生中断,soc为了方便管理与区分中断,故设计了中断控制器GIC(**Generic Interrupt Controller **),用于选择性屏蔽中断,修改中断优先级,记录中断是否发生,区分中断发生类别等功能
  • 中断控制器与处理器之间通过FIQ或者IRQ方式交互,其中大部分通过IRQ中断
  • 在linux系统中处理中断只需要知道中断号与中断处理方法即可,相较于裸机,其简化了中断初始化操作

1.2中断号

1.2.1概述

  • 中断号就是一个数字,需要通过一定方式去获取.在3.14.0内核中,中断号从设备树中获取

1.2.2中断号获取方法预备(设备树)

  • 宏定义获取,通过IRQ_EINT(号码)获取
  • 设备树文件信息保存在linux-rpi-4.14.y/arch/arm/boot/dts目录下,存放各种平台的设备信息,此处关注exynos4412-itop-elite.dts,打开该文件后如下:
/dts-v1/;
#include 
#include 
#include "exynos4412-itop-scp-core.dtsi"

#include 
#include 
...
  • 可看到其包含exynos4412-itop-scp-core.dtsi文件,打开该文件后如下:
#if 0
#include 
#include 
#include 
#include "exynos4412.dtsi"
#include "exynos4412-ppmu-common.dtsi"
#include "exynos-mfc-reserved-memory.dtsi"
...
  • 该文件又包含exynos4412.dtsi,打开文件后如下:
#include "exynos4.dtsi"
#include "exynos4412-pinctrl.dtsi"
#include "exynos4-cpu-thermal.dtsi"
...
  • 包含exynos4412-pinctrl.dtsi,打开如下.其中定义号中断号的描述:
gpx1: gpx1 {
    gpio-controller;    //描述GPIO控制器
    #gpio-cells = <2>;  //描述子节点宽度

    interrupt-controller;//中断控制器
    interrupt-parent = <&gic>;//继承中断控制器相关特征
    interrupts = ,      //描述24号中断
                 ,
                 ,
                 ,
                 ,
                 ,
                 ,
                 ;
    #interrupt-cells = <2>;         //规定其子类中断描述参数为2个,如gpios = <&gpx1 1 GPIO_ACTIVE_LOW>;
};

gpx2: gpx2 {
    gpio-controller;
    #gpio-cells = <2>;

    interrupt-controller;
    #interrupt-cells = <2>;
};

gpx3: gpx3 {
    gpio-controller;
    #gpio-cells = <2>;

    interrupt-controller;
    #interrupt-cells = <2>;
};
  • 上述设备树描述的是CPU的中断信息,在实际编程中我们需要根据外设描述自己的中断信息(一个节点),用于描述按键,该操作将在exynos4412-itop-elite.dts中进行,该文件才是我们要添加外设信息的文件
  • 打开exynos4412-itop-elite.dts文件,编写按键描述(迅为官方描述):
gpio-keys {                             //设备名字
    compatible = "gpio-keys";             //按键索引

    home {
            label = "GPIO Key Home";        //home按键标签
            linux,code = ;
            gpios = <&gpx1 1 GPIO_ACTIVE_LOW>;  //;
            gpios = <&gpx1 2 GPIO_ACTIVE_LOW>;
    };

    sleep {
            label = "GPIO Key Sleep";
            linux,code = ;
            gpios = <&gpx3 3 GPIO_ACTIVE_LOW>;
    };

    vol-up {
            label = "GPIO Key Vol+";
            linux,code = ;
            gpios = <&gpx2 1 GPIO_ACTIVE_LOW>;
    };

    vol-down {
            label = "GPIO Key Vol-";
            linux,code = ;
            gpios = <&gpx2 0 GPIO_ACTIVE_LOW>;
    };
};
  • 添加自己的节点,注意:需要注释掉讯为官方的dts文件中与自己定义的节点相冲突节点引用,如下,双方都引用了gpx1下的第2个引脚,故需注销一个
        key_init_node {
                compatible = "test_key";
                status = "okay";
                interrupt-parent = <&gpx1>;
                interrupts = <2 4>;
        };

        gpio-keys {
                compatible = "gpio-keys";

                home {
                        label = "GPIO Key Home";
                        linux,code = ;
                        gpios = <&gpx1 1 GPIO_ACTIVE_LOW>;
                };

                back {
                        label = "GPIO Key Back";
                        linux,code = ;
//                      gpios = <&gpx1 2 GPIO_ACTIVE_LOW>;
                };
  • 最后编译设备树文件make dtbs,将编译好的内核烧写进开发板即可

烧写步骤:

1.启动开发板,在倒计时之前按回车进入uboot模式

2.开发板控制台输入fastboot 0进入烧写状态

3.将编译好的dtb文件放入烧写工具的根路径下,打开cmd.exe,输入fastboot.exe flash dtb exynos4412-itop-elite.dtb指令即可

4.重启开发板

  • 之后在/proc/device-tree/目录下可查看添加的设备树文件
/sys/firmware/devicetree/base/gpio-keys # ls
back        compatible  home        name        sleep       vol-down    vol-up
/sys/firmware/devicetree/base/gpio-keys # cd home/
/sys/firmware/devicetree/base/gpio-keys/home # ls
gpios       label       linux,code  name

1.2.3内核代码获取中断号

  • 编写内核代码如下,只实现中断号的获取
#include 
#include 
#include 
#include 



int get_irqno_from_node(void)
{
    //通过节点链表获取设备树中的节点
    struct device_node *np = of_find_node_by_path("/key_init_node");
    if(np)
    {
        printk("find node ok\n");
    }
    else
    {
        printk("find node failed\n");
    }
    //通过节点获取中断号码
    int irqno = irq_of_parse_and_map(np, 0);
    printk("irqno = %d\n", irqno);
    return irqno;
}

static int __init key_drv_init(void)
{
    //获取中断号
    int irqno;
    irqno = get_irqno_from_node();
}

static void __exit key_drv_exit(void)
{
    
}

module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
  • 最终开发板打印信息如下,获取的中断号为94号
[root@iTOP-4412]# insmod key_drv.ko 
[   45.871366] find node ok
[   45.872501] irqno = 94
[   45.874790] do_init_module: 'key_drv'->init suspiciously returned 94, it should follow 0/-E convention
[   45.874790] do_init_module: loading module anyway...
[   45.894450] CPU: 0 PID: 126 Comm: insmod Tainted: G           O    4.14.2 #13
[   45.900122] Hardware name: SAMSUNG EXYNOS (Flattened Device Tree)
[   45.906214] [] (unwind_backtrace) from [] (show_stack+0x10/0x14)
[   45.913928] [] (show_stack) from [] (dump_stack+0x88/0xc0)
[   45.921132] [] (dump_stack) from [] (do_init_module+0x1b4/0x1d4)
[   45.928857] [] (do_init_module) from [] (load_module+0x1d44/0x22a8)
[   45.936842] [] (load_module) from [] (SyS_init_module+0x134/0x188)
[   45.944744] [] (SyS_init_module) from [] (ret_fast_syscall+0x0/0x28)

1.3原理图分析

  • 根据开发板外设原理图,可以看到三个按键对应引脚如下图
  • 以SIM_DEL为例,其在核心板上对应引脚如下图,可以看到对应GPX1_2,对应中断10(XEINT10)
  • 之后进入芯片手册查看对应引脚GPX1_2信息,对应第9章Interrupt Controller
  • 在该章节中找到中断资源表(Interrupt Source),详细说明参见
  • 可看到中断10对应中断号为26,同理其他两个按键对应中断9和中断26,对应中断号为25和32

2.申请中断号

2.1API

  • request_irq
#include <>

/*
功能:在内核申请中断号
参数:
参数1:设备所获取的中断号码
参数2:中断回调函数
    typedef irqreturn_t (*irq_handler_t)(int, void *);
参数3:中断触发方式
    #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          低电平触发,电平突然变低
参数4:中断描述,自定义的字符串,可在/proc/interrupts中查看,供用户查看
参数5:参数,传递给参数2(中断回调函数)
返回值:正确返回0,其他错误
*/
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
        const char *name, void *dev);


/*
功能:释放中断资源
参数:
参数1:设备所获取的中断号码
参数2:传入中断服务函数的参数,必须与request_irq函数的最后一个参数一样
返回值:无
*/
const void *free_irq(unsigned int irq, void *dev_id);
  • of_find_node_by_path
/*
功能:通过路径查找指定节点
参数:带全路径的节点名,也可以是节点的别名
返回值:成功,得到节点的首地址;失败,NULL
*/
struct device_node *of_find_node_by_path(const char *path); 
  • irq_of_parse_and_map
/*
功能:提取中断号
参数:
参数1:获取的设备节点首地址
参数2:节点中的第几个引脚
返回值:成功返回获取的中断号,失败返回0
*/
unsigned int irq_of_parse_and_map(struct device_node *node, int index)

2.2驱动框架代码

  • 框架代码如下
#include 
#include 
#include 
#include 
#include 

static int irqno;   //全局变量,保存中断号

//中断回调函数
irqreturn_t key_irq_handler(int irqno, void *devid)
{
    printk("------%s------\n", __FUNCTION__);
    return IRQ_HANDLED;
}

int get_irqno_from_node(void)
{
    //通过节点链表获取设备树中的节点
    struct device_node *np = of_find_node_by_path("/key_init_node");
    if(np)
    {
        printk("find node ok\n");
    }
    else
    {
        printk("find node failed\n");
    }
    //通过节点获取中断号码
    int irqno = irq_of_parse_and_map(np, 0);
    printk("irqno = %d\n", irqno);
    return irqno;
}

static int __init key_drv_init(void)
{
    int ret;
    irqno = get_irqno_from_node();
    ret = request_irq(irqno, key_irq_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                "key3_eint10", NULL);
    if(ret != 0)
    {
        printk("request irq error\n");
        return ret;
    }
}

static void __exit key_drv_exit(void)
{
    //释放中断
    free_irq(irqno, NULL);
}

module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
  • 编译烧写之后,装载驱动,之后按下对应按键,打印信息如下.因为在驱动代码中设置触发方式为IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING(下降/上升沿触发),而在每次按下按键的时候电平会有一个上升沿和一个下降沿的变化,所以每按一次按键就会触发两次中断,故打印信息为偶数
[root@iTOP-4412]# insmod key_drv.ko 
[ 2711.909727] find node ok
[ 2711.911008] irqno = 94
[ 2770.382352] ------key_irq_handler------
[ 2770.546061] ------key_irq_handler------
[ 2771.325930] ------key_irq_handler------
[ 2772.066640] ------key_irq_handler------
[ 2772.530572] ------key_irq_handler------
[ 2772.711444] ------key_irq_handler------
[ 2773.096049] ------key_irq_handler------
[ 2773.269443] ------key_irq_handler------
  • 最后查看/proc/interrupts信息如下.可看到key3_eint10有8次中断触发,key3_eint10由request_irq函数的参数4定义
[root@iTOP-4412]# cat /proc/interrupts 
           CPU0       
 36:          0     GIC-0  89 Edge      mct_comp_irq
...
 94:          8  exynos4210_wkup_irq_chip   2 Edge      key3_eint10
...
Err:          0

3.中断中内核与应用层交互

3.1概述

  • 上述代码仅仅能够让内核捕获中断,但却没有与应用层app产生任何数据交互,这样的驱动毫无意义
  • 如果与应用层APP交互需要考虑以下问题

1.硬件如何获取数据

2.如何将获取的硬件数据传递给用户

3.用户怎么取得数据

  • 对于按键,就2种状态:按下或者抬起,即0或者1.按照1.3部分所说,当按键按下的时候,该引脚为低电平,按键抬起的时候,该引脚为高电平.所以我们可通过读取按键引脚电平就可以判断按键的状态
  • 读取按键数据:通过读取按键对应引脚的寄存器的某个位来获取按键状态.此处对应gpx1,即读取gpx1寄存器地址,其数据手册描述如下.其基地址为0x1100 0000,偏移量为0x0C24,故其实际地址为0x1100 0C24

3.2框架搭建

  • 驱动框架搭建
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


#define GPX1CON_REG 0x11000C20
#define KEY_ENTER 28

//#define GPX1DATA_REG 0x11000C24


/*描述按键的对象*/
struct key_event{
    int code;          //按键类型,如home,back,v-,v+按键
    int value;         //表示按键状态,按下1,抬起0
};


/*设计全局中断设备对象,描述按键信息*/
struct key_desc{
    unsigned int dev_major;     //主设备号
    struct class *cls;
    struct device *dev;
    int irqno;                  //保存中断号
    void *reg_base;             //保存按键引脚寄存器地址
    struct key_event event;
};

struct key_desc *key_dev;       //实例化对象

//中断回调函数
irqreturn_t key_irq_handler(int irqno, void *devid)
{
    int read_val;   //保存引脚读取结果
    printk("------%s------\n", __FUNCTION__);
    //read_val = readl((key_dev->key_reg_base) && (1<<2));
    read_val = readl(key_dev->reg_base + 4) & (1 << 2);
    if(read_val > 0)
    {
        printk("key up\n");             //按键抬起
        /*将按键状态写入结构体中*/
        key_dev->event.code = KEY_ENTER;//假设其为回车键
        key_dev->event.value = 0;
    }
    else
    {
        printk("key down\n");       //按键按下
        /*将按键状态写入结构体中*/
        key_dev->event.code = KEY_ENTER;
        key_dev->event.value = 1;
    }
    return IRQ_HANDLED;
}

int get_irqno_from_node(void)
{
    int irqno;
    //获取设备树中的节点
    struct device_node *np = of_find_node_by_path("/key_init_node");
    if(np)
    {
        printk("find node ok\n");
    }
    else
    {
        printk("find node failed\n");
    }
    //通过节点获取中断号码
    irqno = irq_of_parse_and_map(np, 0);
    printk("irqno = %d\n", irqno);
    return irqno;
}

int key_drv_open(struct inode *inode, struct file *filp)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

ssize_t key_drv_read(struct file *filp, char __user *buf, size_t count, loff_t *fops)
{
    int ret;
    //printk("------%s------\n", __FUNCTION__);
    /*在驱动中将按键结构体数据给用户*/
    ret = copy_to_user(buf, &key_dev->event, count);
    if(ret)
    {
        printk("copy_to_user error\n");
        return -EFAULT;
    }
    /*应用程序每读取一次数据就将数据清空*/
    memset(&key_dev->event, 0, sizeof(key_dev->event));
    return count;
}
ssize_t key_drv_write(struct file *filp, const char __user *buf, size_t count, loff_t *fops)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

int key_drv_release(struct inode *inode, struct file *filp)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

const struct file_operations key_fops = {
    .open = key_drv_open,
    .read = key_drv_read,
    .write = key_drv_write,
    .release = key_drv_release,    
};
static int __init key_drv_init(void)
{
    int ret;
    /*为key对象分配空间*/
    key_dev = kzalloc(sizeof(struct key_desc), GFP_KERNEL);
    /*申请主设备号*/
    key_dev->dev_major = register_chrdev(0, "key_drv", &key_fops);
    /*创建设备节点文件*/
    key_dev->cls = class_create(THIS_MODULE, "key_cls");
    key_dev->dev = device_create(key_dev->cls, NULL, MKDEV(key_dev->dev_major, 0), 
                        NULL, "key0");
    /*获取中断号*/
    key_dev->irqno = get_irqno_from_node();
    /*向内核申请中断*/
    ret = request_irq(key_dev->irqno, key_irq_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                "key3_eint10", NULL);
    if(ret != 0)
    {
        printk("request irq error\n");
        return ret;
    }
    /*内存映射,映射的为GPX1的配置寄存器地址*/
    key_dev->reg_base = ioremap(GPX1CON_REG, 8);
    return 0;
}

static void __exit key_drv_exit(void)
{
    /*释放相关资源*/
    iounmap(key_dev->reg_base);
    free_irq(key_dev->irqno, NULL);
    device_destroy(key_dev->cls, MKDEV(key_dev->dev_major, 0));
    class_destroy(key_dev->cls);
    unregister_chrdev(key_dev->dev_major, "key_drv");
    kfree(key_dev);
    printk(KERN_ALERT "Goodbye, curel world, this is remove\n");
}

module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
  • 应用层测试代码
#include 
#include 
#include 


#define KEY_ENTER 28
struct key_event{
    int code;          //按键类型,如home,back,v-,v+按键
    int value;         //表示按键状态,按下1,抬起0
};

int main(int argc, char *argv[])
{
    struct key_event event;
    int fd;
    fd = open("/dev/key0", O_RDWR);
    if(fd < 0)
    {
        perror("open");
        exit(-1);
    }
    while(1)
    {
        read(fd, &event, sizeof(struct key_event));
        if(event.code == KEY_ENTER)
        {
            if(event.value == 1)
            {
                printf("APP:key_enter pressed\n");
            }
            else
            {
                printf("APP:key_enter up\n");
            }
        }
    }
    close(fd);
    return 0;
}
  • 运行结果,安装驱动,执行测试程序.当按下按键后,应用程序打印对应字符,松开按键同上
[root@iTOP-4412]# ls
key_test
[root@iTOP-4412]# ./key_test 
[   31.040616] ------key_drv_open------
[   36.056240] ------key_irq_handler------
[   36.058640] key down
APP:key_enter pressed
[   37.149142] ------key_irq_handler------
[   37.151522] key up
APP:key_enter up
[   38.031793] ------key_irq_handler------
[   38.034174] key down
APP:key_enter pressed
[   38.935735] ------key_irq_handler------
[   38.938118] key up
APP:key_enter up
^C[   48.331034] ------key_drv_release------
  • 一些问题即解决方案
Mem: 21948K used, 998012K free, 0K shrd, 3197172312K buff, 0K cached
CPU: 11.5% usr 88.4% sys  0.0% nic  0.0% idle  0.0% io  0.0% irq  0.0% sirq
Load average: 0.39 0.10 0.03 2/48 128
  PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND
  127    99 root     R     1600  0.1   0 99.4 ./key_test
  128    99 root     R     3268  0.3   0  0.2 top
...

1.内核提示空指针错误.经过排查后发现在readl函数中访问了非法地址,故更改了内存映射地址,即在ioremap函数中将映射地址从0x11000C24改为了0x11000C20,之后在readl中偏移4字节即可,故写为readl(key_dev->reg_base + 4)

2.按照这种方式虽然能实现基本功能,但是发现cpu占用率很高,因为我们的应用程序在死循环中不断地调用read函数,其对应在内核中不断调用key_drv_read函数,该函数也在一直将数据从内核给用户(频繁地在用户与内核状态之间切换),其中大部分数据都是重复的,所以导致cpu负载非常高,明显不符合程序设计要求

3.3框架改建(阻塞方式)

3.3.1思路

  • 那么如何解决CPU占用过高的问题呢?占用高的原因是因为该程序导致内核一直在进程上下文之间来回切换.其目的就是一直将内核数据给用户,不管是否产生了新数据.那么假设当没有数据产生(没有中断发生的时候),将整个进程休眠,那么CPU占用率将降低.
  • 解决方案:当我们用户程序在调用read函数的时候会进入内核态调用key_drv_read函数获取数据,而该函数在执行一次后会将数据清空,即没有了数据,当再次调用read函数触发key_drv_read函数调用的时候将该函数休眠,让出CPU使用权,直到有新的数据到达之后再执行,去将数据给用户,之后再休眠即可
  • 休眠实现:

1.虽然驱动代码运行在内核状态,但是其还是属于整个进程空间.故我们可将该进程加入到一个进程等待队列中去,进而将进程休眠

2.为了让进程在休眠的时候仍然能够响应一些特殊信号,如kill信号.故需要将进程设置为TASK_INTERRUPIBLE状态

3.最后让出CPU使用权

  • 补充:文件IO模型

1.阻塞模型

2.非阻塞模式

3.多路复用模式,如select,poll

4.异步通知模式,如faync

3.3.2相关API

  • init_waitqueue_head,为宏定义
/*
功能:初始化等待队列
参数:等待队列的队头指针
返回值:无
*/
init_waitqueue_head(struct wait_queue_head *wq_head);
  • wake_up_interruptible
/*
功能:通过信号唤醒当前进程
参数:等待队列的队头指针
返回值:无
*/
wake_up_interruptible(struct wait_queue_head *wq_head);
  • wait_event_interruptible
/*
功能:休眠当前进程
参数:
参数1:等待队列的队头(不是队头指针)
参数2:条件,0或者非0
    为假的时候就会等待
    为真则不等待
返回值:
使用方法:
    1.定义等待节点,即要插入等待队列的队头
    2.在需要等待的时候将进程休眠
    3.在适当时间将进程唤醒,通过wake_up_interruptible函数
*/
wait_event_interruptible(struct wait_queue_head wq_head, condition);

/*
补充:关于该函数的参数2,查看源码如下.可看到,当该参数为0时候才会调用 __wait_event_interruptible函数,否则不会调用
*/
#define wait_event_interruptible(wq_head, condition)                \
({                                      \
    int __ret = 0;                              \
    might_sleep();                              \
    if (!(condition))                           \
        __ret = __wait_event_interruptible(wq_head, condition);     \
    __ret;                                  \
})
  • 调用wait_event_interruptible函数相当于依次调用以下函数
/*
功能:把一个非互斥进程插入等待队列的队头
*/
add_wait_queue(struct wait_queue_head * wq_head, struct wait_queue_entry * wq_entry);
/*
功能:设置当前进程状态为可打断状态
*/
set_current_state(TASK_INTERRUPIBLE);
/*
功能:让出调度,即将当前进程休眠
*/
schedlue(void);

3.3.3改进后

  • 增加IO阻塞模型之后驱动代码如下
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define GPX1CON_REG 0x11000C20
#define KEY_ENTER 28

//#define GPX1DATA_REG 0x11000C24


/*描述按键的对象*/
struct key_event{
    int code;          //按键类型,如home,back,v-,v+按键
    int value;         //表示按键状态,按下1,抬起0
};


/*设计全局中断设备对象,描述按键信息*/
struct key_desc{
    unsigned int dev_major;     //主设备号
    struct class *cls;
    struct device *dev;
    int irqno;                  //保存中断号
    void *reg_base;             //保存按键引脚寄存器地址
    struct key_event event;     //继承按键对象
    wait_queue_head_t wq_head;  //定义一个等待队列队头节点,不可定义为指针,否则在后面初始化的时候会为野指针
    int is_have_data;           //保存是否产生了按键数据,在kzalloc分配内存时候被初始化为0
};

struct key_desc *key_dev;       //实例化对象

//中断回调函数
irqreturn_t key_irq_handler(int irqno, void *devid)
{
    int read_val;   //保存引脚读取结果
    printk("------%s------\n", __FUNCTION__);
    //read_val = readl((key_dev->key_reg_base) && (1<<2));
    read_val = readl(key_dev->reg_base + 4) & (1 << 2);
    if(read_val > 0)
    {
        printk("key up\n");             //按键抬起
        key_dev->event.code = KEY_ENTER;//假设其为回车键
        key_dev->event.value = 0;
    }
    else
    {
        printk("key down\n");       //按键按下
        key_dev->event.code = KEY_ENTER;
        key_dev->event.value = 1;
    }
    /*有按键数据到达,唤醒进程*/
    wake_up_interruptible(&key_dev->wq_head);
    /*设置标志位,表示有数据到达*/
    key_dev->is_have_data = 1;
    return IRQ_HANDLED;
}

int get_irqno_from_node(void)
{
    int irqno;
    //获取设备树中的节点
    struct device_node *np = of_find_node_by_path("/key_init_node");
    if(np)
    {
        printk("find node ok\n");
    }
    else
    {
        printk("find node failed\n");
    }
    //通过节点获取中断号码
    irqno = irq_of_parse_and_map(np, 0);
    printk("irqno = %d\n", irqno);
    return irqno;
}

int key_drv_open(struct inode *inode, struct file *filp)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

ssize_t key_drv_read(struct file *filp, char __user *buf, size_t count, loff_t *fops)
{
    int ret;
    //printk("------%s------\n", __FUNCTION__);
    /*在没有数据的时候直接阻塞等待数据产生*/
    wait_event_interruptible(key_dev->wq_head, key_dev->is_have_data);
    /*将内核按键数据给用户*/
    ret = copy_to_user(buf, &key_dev->event, count);
    if(ret)
    {
        printk("copy_to_user error\n");
        return -EFAULT;
    }
    /*应用程序每读取一次数据就将数据清空*/
    memset(&key_dev->event, 0, sizeof(key_dev->event));
    /*释放数据后将没有数据,清空标志位*/
    key_dev->is_have_data = 0;
    return count;
}
ssize_t key_drv_write(struct file *filp, const char __user *buf, size_t count, loff_t *fops)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

int key_drv_release(struct inode *inode, struct file *filp)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

const struct file_operations key_fops = {
    .open = key_drv_open,
    .read = key_drv_read,
    .write = key_drv_write,
    .release = key_drv_release,    
};
static int __init key_drv_init(void)
{
    int ret;
    /*为key对象分配空间*/
    key_dev = kzalloc(sizeof(struct key_desc), GFP_KERNEL);
    /*申请主设备号*/
    key_dev->dev_major = register_chrdev(0, "key_drv", &key_fops);
    /*创建设备节点文件*/
    key_dev->cls = class_create(THIS_MODULE, "key_cls");
    key_dev->dev = device_create(key_dev->cls, NULL, MKDEV(key_dev->dev_major, 0), 
                        NULL, "key0");
    /*获取中断号*/
    key_dev->irqno = get_irqno_from_node();
    ret = request_irq(key_dev->irqno, key_irq_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                "key3_eint10", NULL);
    if(ret != 0)
    {
        printk("request irq error\n");
        return ret;
    }
    key_dev->reg_base = ioremap(GPX1CON_REG, 8);
    /*初始化等待队列头*/
    init_waitqueue_head(&key_dev->wq_head);
    return 0;
}

static void __exit key_drv_exit(void)
{
    iounmap(key_dev->reg_base);
    free_irq(key_dev->irqno, NULL);
    device_destroy(key_dev->cls, MKDEV(key_dev->dev_major, 0));
    class_destroy(key_dev->cls);
    unregister_chrdev(key_dev->dev_major, "key_drv");
    kfree(key_dev);
    printk(KERN_ALERT "Goodbye, curel world, this is remove\n");
}

module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
  • 执行结果,可看到在实现原有功能基础上大大减小了CPU使用率
[root@iTOP-4412]# ./key_test &
[root@iTOP-4412]# [   92.628179] ------key_drv_open------

[root@iTOP-4412]# 
[root@iTOP-4412]# [  100.096033] ------key_irq_handler------
[  100.098419] key down
APP:key_enter pressed
[  100.118621] ------key_irq_handler------
[  100.121004] key up
APP:key_enter up
[  101.758766] ------key_irq_handler------
[  101.761151] key down
APP:key_enter pressed
[  102.629313] ------key_irq_handler------
[  102.631694] key up
APP:key_enter up
[root@iTOP-4412]# top
Mem: 21992K used, 997968K free, 0K shrd, 3203770936K buff, 0K cached
CPU:  0.0% usr  0.2% sys  0.0% nic 99.8% idle  0.0% io  0.0% irq  0.0% sirq
Load average: 0.06 0.03 0.01 1/50 127
  PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND
  127    99 root     R     3268  0.3   0  0.1 top
...
  126    99 root     S     1604  0.1   0  0.0 ./key_test
...
[root@iTOP-4412]# kill -9 126
[  299.690324] ------key_drv_release------
[1]+  Killed                     ./key_test

3.4框架改建(非阻塞方式)

3.4.1思路

  • 非阻塞方式就是在读写的时候如果没有数据那么会立刻返回,并返回一个出错码,而不是一直等待
  • 在本示例场景中表现为:当用户调用read函数的时候,会调用底层的key_drv_read函数,如果底层没有数据,那么read会返回一个错误码EAGIN,如果想要读取到数据,那么将会一直调用read函数,直到底层有数据才能读取到数据(轮询).此举跟之前CPU占用率高的实现方式雷同,效率仍然很低

3.4.2非阻塞实现

  • 很简单,在应用层在调用open函数的时候加上O_NONBLOCK即可,例如fd = open("/dev/key0", O_RDWR|O_NONBLOCK);
  • 在驱动中我们需要判断当前模式是阻塞还是非阻塞,通过file中的f_flags来判断即可

3.4.3改进后

  • 在驱动中适配文件打开模式(阻塞或非阻塞),妙哉妙哉,该函数值得细细品味!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
ssize_t key_drv_read(struct file *filp, char __user *buf, size_t count, loff_t *fops)
{
    int ret;
    //printk("------%s------\n", __FUNCTION__);
    /*打开方式为非阻塞打开并且当前设备没有数据的时候立刻返回*/
    if((filp->f_flags & O_NONBLOCK) && !(key_dev->is_have_data))
    {
        return -EAGAIN;
    }
    /*在没有数据的时候直接阻塞等待数据产生*/
    wait_event_interruptible(key_dev->wq_head, key_dev->is_have_data);
    /*将内核按键数据给用户*/
    ret = copy_to_user(buf, &key_dev->event, count);
    if(ret)
    {
        printk("copy_to_user error\n");
        return -EFAULT;
    }
    /*应用程序每读取一次数据就将数据清空*/
    memset(&key_dev->event, 0, sizeof(key_dev->event));
    /*释放数据后将没有数据,清空标志位*/
    key_dev->is_have_data = 0;
    return count;
}

3.5框架改建(多路复用方式)

3.5.1思路

  • 多路复用有select与poll两种方式
  • 这里我们分析poll方式

引入场景:假设有一种场景:有一个进程(单进程,单线程)要处理两种数据,一种为GPS数据,另一种为GPRS数据.在单进程(线程)里面只能在同一时间处理一种数据,那么如何使用单进程(线程)处理两种数据呢?故引入多路复用

原理分析:多路复用使用select或者poll函数,该函数能同时监控多个文件描述符的读写操作,只要被监控对象中存在数据读写,都会被监控到,进而再做出相应处理

比喻:假设某码农回家,他今天要煲汤,同时他还要看一场快开始的球赛,他的好友还要来他家看球.此处他需要注意3件事情,即汤是否做好,节目是否开始,朋友是否到家.但是人毕竟只有一个,在同一时间只能注意一件事情.上述阻塞方案就是在那里死等,等汤是否做好,其他的事情都不用干了.而多路复用就好比给每件事情都设置一个提醒功能,如汤做好了会响铃,节目开始了会有提示,朋友到了会有门铃.此刻该码农将不用只注意某件事情,而是可以去干其他事情,当三者中的某一提醒消息到达再去处理相应事件即可

3.5.2poll使用和API

  • 首先应该打开多个文件(对应多个文件描述符)
  • 之后利用poll来实现监控文件描述符的读,写,出错事件
  • API如下
#include 

/*
功能:监控多个fd
参数:
参数1:表示多个文件描述符的集合
参数2:被监控的文件描述符的个数
参数3:表示要监控的时间,正数表示要监控多少ms,负数则表示一直监控,0则不监控(类似于非阻塞)
返回值:出错返回负数,超时返回0,正数表示fd事件到达
*/
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

/*struct pollfd结构体*/
struct pollfd{
    int fd;         //文件描述符
    short events;   //等待的事件,即我们希望监控文件描述符的啥事件(读(POLLIN),写(POLLOUT),出错(POLLERR))
    short revents;  //实际发生的事件,表示当前fd有啥事件发生
};
  • APP参考样例
#include 
#include 
#include 
#include 

#define KEY_ENTER 28
struct key_event{
    int code;          //按键类型,如home,back,v-,v+按键
    int value;         //表示按键状态,按下1,抬起0
};

int main(int argc, char *argv[])
{
    char in_buf[128];
    struct key_event event;
    
    struct pollfd pfd[2];           //此处监控2个文件描述符
    int ret;                        //保存poll函数返回值
    
    int fd;
    fd = open("/dev/key0", O_RDWR);
    if(fd < 0)
    {
        perror("open");
        exit(-1);
    }

    /*监控按键fd*/
    pfd[0].fd = fd;
    pfd[0].events = POLLIN;
    /*监控标准输入fd(0)*/
    pfd[1].fd = 0;
    pfd[1].events = POLLIN;

    while(1)
    {
        ret = poll(pfd, 2, -1);
        if(ret > 0)
        {
            /*此处表示fd有事件发生*/
            if(pfd[0].revents & POLLIN)
            {
                //有按键数据到达,此时一定可以读到数据
                read(pfd[0].fd, &event, sizeof(struct key_event));
                if(event.code == KEY_ENTER)
                {
                    if(event.value == 1)
                    {
                        printf("APP:key_enter pressed\n");
                    }
                    else
                    {
                        printf("APP:key_enter up\n");
                    }
                }
            }
            if(pfd[1].revents & POLLIN)
            {
                fgets(in_buf, 128, stdin);
                printf("in_buf:%s\n", in_buf);
            }
        }
        else
        {
            perror("poll");
            exit(-1);
        }
    }
    close(pfd[0].fd);
    return 0;
}
  • 样例v1.0执行结果,我们可以发现程序执行结果跟我们预期的不一样,我们预期是监控2个fd,实现效果为:当我们按下按键时候会有打印信息,输入字符串的时候会打印对应输入的字符串.而实际上却只能监控按键.
[root@iTOP-4412]# clear 
[root@iTOP-4412]# ./key_test 
[  121.520247] ------key_drv_open------
hello
[  131.064728] ------key_irq_handler------
[  131.067113] key down
[  131.633143] ------key_irq_handler------
[  131.635523] key up
hel^H^H



^C[  140.456266] ------key_drv_release------

[root@iTOP-4412]# 
  • 按照设想情况,执行APP程序时候poll函数是会阻塞在那里,不会有任何返回值,实际上poll函数却一直返回1,即不管被监控的fd是否产生数据poll函数都会有返回值,明显不和常理.要想其达到预期效果,我们还需要对驱动代码改写,在驱动代码内部实现poll接口

3.5.3驱动改进

  • file_operations结构体内部有一个poll接口需要我们实现
struct file_operations {
    ...
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    ...
};
  • 首先需要实现poll_wait函数
#include 
/*
功能:将当前等待队列注册到系统中
参数:
参数1:设备文件信息的 struct file 结构体的指针参数 struct file *filp,即poll接口的第1个参数
参数2:等待队列头节点地址
参数3:追加到设备驱动上的 poll_table结构体指针参数,即poll接口的第2个参数
返回值:无
*/
poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table * p);
  • 其次在poll内部我们还需要返回一个mask值,当没有数据的时候返回一个0,有数据的时候返回POLLIN
  • 实现后的poll函数
unsigned int key_drv_poll (struct file *filp, struct poll_table_struct *pts)
{
    unsigned int mask;
    /*将当前等待队列注册到系统中*/
    poll_wait(filp, &key_dev->wq_head, pts);
    if(!key_dev->is_have_data)
    {
        mask = 0;       //没有数据产生
    }
    if(key_dev->is_have_data)
    {
        mask |= POLLIN; //有数据产生
    }
    return mask;
}
  • 完整驱动代码
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define GPX1CON_REG 0x11000C20
#define KEY_ENTER 28

//#define GPX1DATA_REG 0x11000C24


/*描述按键的对象*/
struct key_event{
    int code;          //按键类型,如home,back,v-,v+按键
    int value;         //表示按键状态,按下1,抬起0
};


/*设计全局中断设备对象,描述按键信息*/
struct key_desc{
    unsigned int dev_major;     //主设备号
    struct class *cls;
    struct device *dev;
    int irqno;                  //保存中断号
    void *reg_base;             //保存按键引脚寄存器地址
    struct key_event event;     //继承按键对象
    wait_queue_head_t wq_head;  //定义一个等待队列队头节点,不可定义为指针,否则在后面初始化的时候会为野指针
    int is_have_data;           //保存是否产生了按键数据,在kzalloc分配内存时候被初始化为0
};

struct key_desc *key_dev;       //实例化对象

//中断回调函数
irqreturn_t key_irq_handler(int irqno, void *devid)
{
    int read_val;   //保存引脚读取结果
    printk("------%s------\n", __FUNCTION__);
    //read_val = readl((key_dev->key_reg_base) && (1<<2));
    read_val = readl(key_dev->reg_base + 4) & (1 << 2);
    if(read_val > 0)
    {
        printk("key up\n");             //按键抬起
        key_dev->event.code = KEY_ENTER;//假设其为回车键
        key_dev->event.value = 0;
    }
    else
    {
        printk("key down\n");       //按键按下
        key_dev->event.code = KEY_ENTER;
        key_dev->event.value = 1;
    }
    /*有按键数据到达,唤醒进程*/
    wake_up_interruptible(&key_dev->wq_head);
    /*设置标志位,表示有数据到达*/
    key_dev->is_have_data = 1;
    return IRQ_HANDLED;
}

int get_irqno_from_node(void)
{
    int irqno;
    //获取设备树中的节点
    struct device_node *np = of_find_node_by_path("/key_init_node");
    if(np)
    {
        printk("find node ok\n");
    }
    else
    {
        printk("find node failed\n");
    }
    //通过节点获取中断号码
    irqno = irq_of_parse_and_map(np, 0);
    printk("irqno = %d\n", irqno);
    return irqno;
}

int key_drv_open(struct inode *inode, struct file *filp)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

ssize_t key_drv_read(struct file *filp, char __user *buf, size_t count, loff_t *fops)
{
    int ret;
    //printk("------%s------\n", __FUNCTION__);
    /*打开方式为非阻塞打开并且当前设备没有数据的时候立刻返回*/
    if((filp->f_flags & O_NONBLOCK) && !(key_dev->is_have_data))
    {
        return -EAGAIN;
    }
    /*在没有数据的时候直接阻塞等待数据产生*/
    wait_event_interruptible(key_dev->wq_head, key_dev->is_have_data);
    /*将内核按键数据给用户*/
    ret = copy_to_user(buf, &key_dev->event, count);
    if(ret)
    {
        printk("copy_to_user error\n");
        return -EFAULT;
    }
    /*应用程序每读取一次数据就将数据清空*/
    memset(&key_dev->event, 0, sizeof(key_dev->event));
    /*释放数据后将没有数据,清空标志位*/
    key_dev->is_have_data = 0;
    return count;
}
ssize_t key_drv_write(struct file *filp, const char __user *buf, size_t count, loff_t *fops)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

int key_drv_release(struct inode *inode, struct file *filp)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

/*实现poll接口*/
unsigned int key_drv_poll (struct file *filp, struct poll_table_struct *pts)
{
    unsigned int mask;
    /*将当前等待队列注册到系统中*/
    poll_wait(filp, &key_dev->wq_head, pts);
    if(!key_dev->is_have_data)
    {
        mask = 0;       //没有数据产生
    }
    if(key_dev->is_have_data)
    {
        mask |= POLLIN; //有数据产生
    }
    return mask;
}

const struct file_operations key_fops = {
    .open = key_drv_open,
    .read = key_drv_read,
    .write = key_drv_write,
    .release = key_drv_release,    
    .poll = key_drv_poll,
};
static int __init key_drv_init(void)
{
    int ret;
    /*为key对象分配空间*/
    key_dev = kzalloc(sizeof(struct key_desc), GFP_KERNEL);
    /*申请主设备号*/
    key_dev->dev_major = register_chrdev(0, "key_drv", &key_fops);
    /*创建设备节点文件*/
    key_dev->cls = class_create(THIS_MODULE, "key_cls");
    key_dev->dev = device_create(key_dev->cls, NULL, MKDEV(key_dev->dev_major, 0), 
                        NULL, "key0");
    /*获取中断号*/
    key_dev->irqno = get_irqno_from_node();
    ret = request_irq(key_dev->irqno, key_irq_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                "key3_eint10", NULL);
    if(ret != 0)
    {
        printk("request irq error\n");
        return ret;
    }
    key_dev->reg_base = ioremap(GPX1CON_REG, 8);
    /*初始化等待队列头*/
    init_waitqueue_head(&key_dev->wq_head);
    return 0;
}

static void __exit key_drv_exit(void)
{
    iounmap(key_dev->reg_base); 
    free_irq(key_dev->irqno, NULL);
    device_destroy(key_dev->cls, MKDEV(key_dev->dev_major, 0));
    class_destroy(key_dev->cls);
    unregister_chrdev(key_dev->dev_major, "key_drv");
    kfree(key_dev);
    printk(KERN_ALERT "Goodbye, curel world, this is remove\n");
}

module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
  • 执行结果,可以看到,与预期效果相同
[root@iTOP-4412]# ./key_test 
[  403.755361] ------key_drv_open------
hello
ret=1
makabaka
in_buf:hello

[  416.077654] ------key_irq_handler------
[  416.080034] key down
ret=1
APP:key_enter pressed
[  416.799770] ------key_irq_handler------
[  416.802150] key up
ret=1
APP:key_enter up
haha
ret=1
makabaka
in_buf:haha

^C[  422.896506] ------key_drv_release------

[root@iTOP-4412]# 

3.6框架改建(异步信号)

3.6.1思路

  • 在之前的方式中,都是APP主动调用函数去读取数据.而异步信号类似于中断,当驱动有新数据产生的时候就会发送一个SIGIO信号,而在APP进程里面就可以使用signal函数去捕获信号
  • 针对APP有以下步骤

1.设置信号处理方法

2.设置SIGIO信号属主进程,即哪个进程处理SIGIO信号

3.设置异步信号模式(fasync模式)

  • 针对驱动有以下步骤

1.与进程进行关联

2.发送信号

3.6.2API

  • 参考大佬文章:https://blog.csdn.net/fengxinlinux/article/details/51980837
  • fcntl函数
#include
#include
/*
功能:针对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性
参数:
参数1:要操作的文件描述符
参数2:命令,即对文件描述符进行的具体操作
参数3:视情况而定
*/

int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd ,struct flock* lock);
  • fcntl参数2
参数 含义
F_DUPFD 复制由fd指向的文件描述符,调用成功后返回新的文件描述符,与旧的文件描述符共同指向同一个文件
F_GETFD 读取文件描述符close-on-exec标志
F_SETFD 将文件描述符close-on-exec标志设置为第三个参数arg的最后一位
F_GETFL 获取文件打开方式的标志,标志值含义与open调用一致
F_SETF 设置文件打开方式为arg指定方式
F_SETLK 此时fcntl函数用来设置或释放锁。当short_l_type为F_RDLCK为读锁,F_WDLCK为写锁,F_UNLCK为解锁
F_SETLKW 此时也是给文件上锁,不同于F_SETLK的是,该上锁是阻塞方式。当希望设置的锁因为其他锁而被阻止设置时,该命令会等待相冲突的锁被释放
F_GETLK 第3个参数lock指向一个希望设置的锁的属性结构,如果锁能被设置,该命令并不真的设置锁,而是只修改lock的l_type为F_UNLCK,然后返回该结构体。如果存在一个或多个锁与希望设置的锁相互冲突,则fcntl返回其中的一个锁的flock结构
  • 实例,例如设置某文件为异步模式.首先应该获取当前文件描述符状态标志,之后再修改状态标志,最后再应用修改后的状态标志
//设置io模式为异步模式
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);

3.6.3APP程序

  • 针对需求,修改后的异步方式接收数据的APP代码如下
#include 
#include 
#include 
#include 
#include 

#define KEY_ENTER 28
struct key_event{
    int code;          //按键类型,如home,back,v-,v+按键
    int value;         //表示按键状态,按下1,抬起0
};

static int fd;
static struct key_event event;

void signal_handler(int signo)
{
    printf("catch a signal\n");
    if(signo == SIGIO)
    {
        read(fd, &event, sizeof(struct key_event));
        if(event.code == KEY_ENTER)
        {
            if(event.value == 1)
            {
                printf("APP:key_enter pressed\n");
            }
            else
            {
                printf("APP:key_enter up\n");
            }
        }
    }
}
int main(int argc, char *argv[])
{
    char in_buf[128];
    fd = open("/dev/key0", O_RDWR);
    if(fd < 0)
    {
        perror("open");
        exit(-1);
    }

    //设置信号处理方法
    signal(SIGIO, signal_handler);
    //设置属主进程,即当前进程处理SIGIO信号
    fcntl(fd, F_SETOWN, getpid());
    //设置io模式为异步模式
    int flags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flags | FASYNC);
    
    while(1)
    {
        printf("doing otherthings\n");
        sleep(1);
    }
    close(fd);
    return 0;
}

3.6.4驱动程序修改

  • 在驱动中需要与应用进程关联,即驱动要知道把信号发送给谁,需要实现一个fasync接口
  • file_operations结构体内部有一个fasync接口需要我们实现
struct file_operations {
    ...
    int (*fasync) (int, struct file *, int);
    ...
};
  • 在驱动fasync函数内部只需要实现fasync_helper函数即可
//改函数前三个参数对应于要实现的fasync接口的三个参数
//fasync_struct结构体是用来保存前三个参数生成一个结构体,在使用的时候需要定义一个fasync_struct结构体指针
fasync_helper(int fd, struct file * filp, int on, struct fasync_struct * * fapp);
//参考用法

struct fasync_struct *fasync;
int key_drv_fasync (int fd, struct file *filp, int on)
{
    return fasync_helper(fd, filp, on, &fasync);
}
  • 最后在中断来临时候(存在数据的时候)发送信号即可
  • 支持异步信号版本驱动
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define GPX1CON_REG 0x11000C20
#define KEY_ENTER 28

//#define GPX1DATA_REG 0x11000C24


/*描述按键的对象*/
struct key_event{
    int code;          //按键类型,如home,back,v-,v+按键
    int value;         //表示按键状态,按下1,抬起0
};


/*设计全局中断设备对象,描述按键信息*/
struct key_desc{
    unsigned int dev_major;     //主设备号
    struct class *cls;
    struct device *dev;
    int irqno;                  //保存中断号
    void *reg_base;             //保存按键引脚寄存器地址
    struct key_event event;     //继承按键对象
    wait_queue_head_t wq_head;  //定义一个等待队列队头节点,不可定义为指针,否则在后面初始化的时候会为野指针
    int is_have_data;           //保存是否产生了按键数据,在kzalloc分配内存时候被初始化为0
    struct fasync_struct *fasync;
};

struct key_desc *key_dev;       //实例化对象

//中断回调函数
irqreturn_t key_irq_handler(int irqno, void *devid)
{
    int read_val;   //保存引脚读取结果
    printk("------%s------\n", __FUNCTION__);
    //read_val = readl((key_dev->key_reg_base) && (1<<2));
    read_val = readl(key_dev->reg_base + 4) & (1 << 2);
    if(read_val > 0)
    {
        printk("key up\n");             //按键抬起
        key_dev->event.code = KEY_ENTER;//假设其为回车键
        key_dev->event.value = 0;
    }
    else
    {
        printk("key down\n");       //按键按下
        key_dev->event.code = KEY_ENTER;
        key_dev->event.value = 1;
    }
    /*有按键数据到达,唤醒进程*/
    wake_up_interruptible(&key_dev->wq_head);
    /*设置标志位,表示有数据到达*/
    key_dev->is_have_data = 1;
    /*发送SIGIO信号*/
    kill_fasync(&key_dev->fasync, SIGIO, POLLIN);
    return IRQ_HANDLED;
}

int get_irqno_from_node(void)
{
    int irqno;
    //获取设备树中的节点
    struct device_node *np = of_find_node_by_path("/key_init_node");
    if(np)
    {
        printk("find node ok\n");
    }
    else
    {
        printk("find node failed\n");
    }
    //通过节点获取中断号码
    irqno = irq_of_parse_and_map(np, 0);
    printk("irqno = %d\n", irqno);
    return irqno;
}

int key_drv_open(struct inode *inode, struct file *filp)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

ssize_t key_drv_read(struct file *filp, char __user *buf, size_t count, loff_t *fops)
{
    int ret;
    //printk("------%s------\n", __FUNCTION__);
    /*打开方式为非阻塞打开并且当前设备没有数据的时候立刻返回*/
    if((filp->f_flags & O_NONBLOCK) && !(key_dev->is_have_data))
    {
        return -EAGAIN;
    }
    /*在没有数据的时候直接阻塞等待数据产生*/
    wait_event_interruptible(key_dev->wq_head, key_dev->is_have_data);
    /*将内核按键数据给用户*/
    ret = copy_to_user(buf, &key_dev->event, count);
    if(ret)
    {
        printk("copy_to_user error\n");
        return -EFAULT;
    }
    /*应用程序每读取一次数据就将数据清空*/
    memset(&key_dev->event, 0, sizeof(key_dev->event));
    /*释放数据后将没有数据,清空标志位*/
    key_dev->is_have_data = 0;
    return count;
}
ssize_t key_drv_write(struct file *filp, const char __user *buf, size_t count, loff_t *fops)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

int key_drv_release(struct inode *inode, struct file *filp)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

/*实现poll接口*/
unsigned int key_drv_poll (struct file *filp, struct poll_table_struct *pts)
{
    unsigned int mask;
    /*将当前等待队列注册到系统中*/
    poll_wait(filp, &key_dev->wq_head, pts);
    if(!key_dev->is_have_data)
    {
        mask = 0;       //没有数据产生
    }
    if(key_dev->is_have_data)
    {
        mask |= POLLIN; //有数据产生
    }
    return mask;
}

int key_drv_fasync (int fd, struct file *filp, int on)
{
    return fasync_helper(fd, filp, on, &key_dev->fasync);
}

const struct file_operations key_fops = {
    .open = key_drv_open,
    .read = key_drv_read,
    .write = key_drv_write,
    .release = key_drv_release,    
    .poll = key_drv_poll,
    .fasync = key_drv_fasync,
};
static int __init key_drv_init(void)
{
    int ret;
    /*为key对象分配空间*/
    key_dev = kzalloc(sizeof(struct key_desc), GFP_KERNEL);
    /*申请主设备号*/
    key_dev->dev_major = register_chrdev(0, "key_drv", &key_fops);
    /*创建设备节点文件*/
    key_dev->cls = class_create(THIS_MODULE, "key_cls");
    key_dev->dev = device_create(key_dev->cls, NULL, MKDEV(key_dev->dev_major, 0), 
                        NULL, "key0");
    /*获取中断号*/
    key_dev->irqno = get_irqno_from_node();
    ret = request_irq(key_dev->irqno, key_irq_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                "key3_eint10", NULL);
    if(ret != 0)
    {
        printk("request irq error\n");
        return ret;
    }
    key_dev->reg_base = ioremap(GPX1CON_REG, 8);
    /*初始化等待队列头*/
    init_waitqueue_head(&key_dev->wq_head);
    return 0;
}

static void __exit key_drv_exit(void)
{
    iounmap(key_dev->reg_base); 
    free_irq(key_dev->irqno, NULL);
    device_destroy(key_dev->cls, MKDEV(key_dev->dev_major, 0));
    class_destroy(key_dev->cls);
    unregister_chrdev(key_dev->dev_major, "key_drv");
    kfree(key_dev);
    printk(KERN_ALERT "Goodbye, curel world, this is remove\n");
}

module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
  • 实现效果,每个1s打印一句话,在此期间可捕捉到中断信号SIGIO
[root@iTOP-4412]# ./key_test 
[  115.112305] ------key_drv_open------
doing otherthings
doing otherthings
doing otherthings
doing otherthings
doing otherthings
doing otherthings
doing otherthings
[  121.154495] ------key_irq_handler------
[  121.156873] key down
catch a signal
APP:key_enter pressed
doing otherthings
[  121.313837] ------key_irq_handler------
[  121.316221] key up
catch a signal
APP:key_enter up
doing otherthings
doing otherthings
[  123.001891] ------key_irq_handler------
[  123.004273] key down
catch a signal
APP:key_enter pressed
doing otherthings
doing otherthings
[  124.008731] ------key_irq_handler------
[  124.011109] key up
catch a signal
APP:key_enter up
doing otherthings
doing otherthings
doing otherthings
^C[  126.201954] ------key_drv_release------

[root@iTOP-4412]# 

4.中断下半部

4.1概念

  • 中断上半部就是上面所说的中断处理函数
  • 众所周知,当CPU在执行某段代码的时候产生一个中断,那么CPU将会保存现场去执行中断程序,执行某段时间后再恢复现场继续执行原来的程序,在某段时间内执行的部分就是中断上半部
  • 注意:中断要求快进快出
  • 但是有些时候中断处理确实需要比较长的时间,例如网卡驱动.当有网络数据到达的时候会产生一个中断,之后再获取数据,解析数据,最后传输.很明显数据处理过程比较耗时,所以引入中断下半部概念
  • 还是上述网卡场景,中断下半部就是在网络数据到达的时候先在中断中处理一些不耗时的操作,例如将数据存入缓存区,之后一些耗时操作将会被延后操作
  • 综上所述,中断下半部就是处理一些耗时操作,特别注意,中断是一个完整的事件过程,在中断上半部执行完毕返回之前需要启动中断下半部

4.2中断下半部实现方法

  • softirq(软中断),处理速度较快,使用于内核,使用较少
  • tasklet,内部实现实际调用了softirq,使用的时候只需要告诉内核中断下半部需要执行啥操作即可,内核会将此构造为一个节点,在内核线程里面使用链表管理,运行在中断上下文,不可以执行休眠操作,如sleep,kmalloc
  • workqueue(工作队列):原理同tasklet,只是在内核中使用队列管理,运行在进程上下文,可以执行休眠操作

4.3tasklet代码实现

  • tasklet_struct结构体
struct tasklet_struct
{
    struct tasklet_struct *next;    //链表节点
    unsigned long state;            //属性
    atomic_t count;
    void (*func)(unsigned long);    //中断下半部实现方法
    unsigned long data;
};
  • 首先初始化tasklet_struct结构体
/*
功能:初始化tasklet_struct结构体
参数:
参数1:tasklet_struct结构体
参数2:中断下半部实现方法
参数3:中断下半部实现方法的参数
*/
tasklet_init(struct tasklet_struct * t, void(* func)(unsigned long), unsigned long data);
  • 之后在中断上半部启动中断下半部
/*
功能:启动中断下半部
参数:tasklet_struct结构体
*/
tasklet_schedule(struct tasklet_struct * t);
  • 驱动代码实现(tasklet版本)
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define GPX1CON_REG 0x11000C20
#define KEY_ENTER 28

//#define GPX1DATA_REG 0x11000C24


/*描述按键的对象*/
struct key_event{
    int code;          //按键类型,如home,back,v-,v+按键
    int value;         //表示按键状态,按下1,抬起0
};


/*设计全局中断设备对象,描述按键信息*/
struct key_desc{
    unsigned int dev_major;     //主设备号
    struct class *cls;
    struct device *dev;
    int irqno;                  //保存中断号
    void *reg_base;             //保存按键引脚寄存器地址
    struct key_event event;     //继承按键对象
    wait_queue_head_t wq_head;  //定义一个等待队列队头节点,不可定义为指针,否则在后面初始化的时候会为野指针
    int is_have_data;           //保存是否产生了按键数据,在kzalloc分配内存时候被初始化为0
    struct fasync_struct *fasync;
    struct tasklet_struct my_tasklet;
};

struct key_desc *key_dev;       //实例化对象

//中断下半部
void key_tasklet_half_irq(unsigned long data)
{
    printk("------%s------\n", __FUNCTION__);
    /*有按键数据到达,唤醒进程*/
    wake_up_interruptible(&key_dev->wq_head);
    /*设置标志位,表示有数据到达*/
    key_dev->is_have_data = 1;
    /*发送信号*/
    kill_fasync(&key_dev->fasync, SIGIO, POLLIN);
}

//中断回调函数(中断上半部)
irqreturn_t key_irq_handler(int irqno, void *devid)
{
    int read_val;   //保存引脚读取结果
    //read_val = readl((key_dev->key_reg_base) && (1<<2));
    read_val = readl(key_dev->reg_base + 4) & (1 << 2);
    if(read_val > 0)
    {
        printk("key up\n");             //按键抬起
        key_dev->event.code = KEY_ENTER;//假设其为回车键
        key_dev->event.value = 0;
    }
    else
    {
        printk("key down\n");       //按键按下
        key_dev->event.code = KEY_ENTER;
        key_dev->event.value = 1;
    }
    /*启动中断下半部*/
    tasklet_schedule(&key_dev->my_tasklet);
    return IRQ_HANDLED;
}

int get_irqno_from_node(void)
{
    int irqno;
    //获取设备树中的节点
    struct device_node *np = of_find_node_by_path("/key_init_node");
    if(np)
    {
        printk("find node ok\n");
    }
    else
    {
        printk("find node failed\n");
    }
    //通过节点获取中断号码
    irqno = irq_of_parse_and_map(np, 0);
    printk("irqno = %d\n", irqno);
    return irqno;
}

int key_drv_open(struct inode *inode, struct file *filp)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

ssize_t key_drv_read(struct file *filp, char __user *buf, size_t count, loff_t *fops)
{
    int ret;
    //printk("------%s------\n", __FUNCTION__);
    /*打开方式为非阻塞打开并且当前设备没有数据的时候立刻返回*/
    if((filp->f_flags & O_NONBLOCK) && !(key_dev->is_have_data))
    {
        return -EAGAIN;
    }
    /*在没有数据的时候直接阻塞等待数据产生*/
    wait_event_interruptible(key_dev->wq_head, key_dev->is_have_data);
    /*将内核按键数据给用户*/
    ret = copy_to_user(buf, &key_dev->event, count);
    if(ret)
    {
        printk("copy_to_user error\n");
        return -EFAULT;
    }
    /*应用程序每读取一次数据就将数据清空*/
    memset(&key_dev->event, 0, sizeof(key_dev->event));
    /*释放数据后将没有数据,清空标志位*/
    key_dev->is_have_data = 0;
    return count;
}
ssize_t key_drv_write(struct file *filp, const char __user *buf, size_t count, loff_t *fops)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

int key_drv_release(struct inode *inode, struct file *filp)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

/*实现poll接口*/
unsigned int key_drv_poll (struct file *filp, struct poll_table_struct *pts)
{
    unsigned int mask;
    /*将当前等待队列注册到系统中*/
    poll_wait(filp, &key_dev->wq_head, pts);
    if(!key_dev->is_have_data)
    {
        mask = 0;       //没有数据产生
    }
    if(key_dev->is_have_data)
    {
        mask |= POLLIN; //有数据产生
    }
    return mask;
}

int key_drv_fasync (int fd, struct file *filp, int on)
{
    return fasync_helper(fd, filp, on, &key_dev->fasync);
}

const struct file_operations key_fops = {
    .open = key_drv_open,
    .read = key_drv_read,
    .write = key_drv_write,
    .release = key_drv_release,    
    .poll = key_drv_poll,
    .fasync = key_drv_fasync,
};
static int __init key_drv_init(void)
{
    int ret;
    /*为key对象分配空间*/
    key_dev = kzalloc(sizeof(struct key_desc), GFP_KERNEL);
    /*申请主设备号*/
    key_dev->dev_major = register_chrdev(0, "key_drv", &key_fops);
    /*创建设备节点文件*/
    key_dev->cls = class_create(THIS_MODULE, "key_cls");
    key_dev->dev = device_create(key_dev->cls, NULL, MKDEV(key_dev->dev_major, 0), 
                        NULL, "key0");
    /*获取中断号*/
    key_dev->irqno = get_irqno_from_node();
    ret = request_irq(key_dev->irqno, key_irq_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                "key3_eint10", NULL);
    if(ret != 0)
    {
        printk("request irq error\n");
        return ret;
    }
    key_dev->reg_base = ioremap(GPX1CON_REG, 8);
    /*初始化等待队列头*/
    init_waitqueue_head(&key_dev->wq_head);
    /*初始化task_let结构体*/
    tasklet_init(&key_dev->my_tasklet, key_tasklet_half_irq, 77);
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

static void __exit key_drv_exit(void)
{
    tasklet_kill(&key_dev->my_tasklet);
    iounmap(key_dev->reg_base); 
    free_irq(key_dev->irqno, NULL);
    device_destroy(key_dev->cls, MKDEV(key_dev->dev_major, 0));
    class_destroy(key_dev->cls);
    unregister_chrdev(key_dev->dev_major, "key_drv");
    kfree(key_dev);
    printk(KERN_ALERT "Goodbye, curel world, this is remove\n");
}

module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
  • 执行结果,可以看到进入了中断下半部
[root@iTOP-4412]# insmod key_drv.ko 
[   12.365829] key_drv: loading out-of-tree module taints kernel.
[   12.372153] find node ok
[   12.373294] irqno = 94
[   12.375731] ------key_drv_init------
[root@iTOP-4412]# [   18.718387] key down
[   18.719165] ------key_tasklet_half_irq------
[   19.801788] key up
[   19.802351] ------key_tasklet_half_irq------
[   20.672179] key down
[   20.672907] ------key_tasklet_half_irq------
[   21.659733] key up
[   21.660295] ------key_tasklet_half_irq------

4.4workqueue代码实现

  • work_struct结构体
struct work_struct {
/*  atomic_long_t data; */
    unsigned long data;
    struct list_head entry;
    work_func_t func;
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};
typedef void (*work_func_t)(struct work_struct *work);
  • 首先初始化work_struct结构体
INIT_WORK(struct work_struct *work, work_func_t func);
  • 开启中断下半部
/*
功能:启动中断下半部
参数:work_struct结构体
*/
schedule_work(struct work_struct *work);
  • 驱动代码实现(workqueue版本)
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define GPX1CON_REG 0x11000C20
#define KEY_ENTER 28

//#define GPX1DATA_REG 0x11000C24


/*描述按键的对象*/
struct key_event{
    int code;          //按键类型,如home,back,v-,v+按键
    int value;         //表示按键状态,按下1,抬起0
};


/*设计全局中断设备对象,描述按键信息*/
struct key_desc{
    unsigned int dev_major;     //主设备号
    struct class *cls;
    struct device *dev;
    int irqno;                  //保存中断号
    void *reg_base;             //保存按键引脚寄存器地址
    struct key_event event;     //继承按键对象
    wait_queue_head_t wq_head;  //定义一个等待队列队头节点,不可定义为指针,否则在后面初始化的时候会为野指针
    int is_have_data;           //保存是否产生了按键数据,在kzalloc分配内存时候被初始化为0
    struct fasync_struct *fasync;
    //struct tasklet_struct my_tasklet;
    struct work_struct my_work;
};

struct key_desc *key_dev;       //实例化对象

#if 0
//中断下半部(tasklet)
void key_tasklet_half_irq(unsigned long data)
{
    printk("------%s------\n", __FUNCTION__);
    /*有按键数据到达,唤醒进程*/
    wake_up_interruptible(&key_dev->wq_head);
    /*设置标志位,表示有数据到达*/
    key_dev->is_have_data = 1;
    /*发送信号*/
    kill_fasync(&key_dev->fasync, SIGIO, POLLIN);
}
#endif

//中断下半部(workqueue)
void key_workqueue_half_irq(struct work_struct *work)
{
    printk("------%s------\n", __FUNCTION__);
    /*有按键数据到达,唤醒进程*/
    wake_up_interruptible(&key_dev->wq_head);
    /*设置标志位,表示有数据到达*/
    key_dev->is_have_data = 1;
    /*发送信号*/
    kill_fasync(&key_dev->fasync, SIGIO, POLLIN);
}


//中断回调函数(中断上半部)
irqreturn_t key_irq_handler(int irqno, void *devid)
{
    int read_val;   //保存引脚读取结果
    //read_val = readl((key_dev->key_reg_base) && (1<<2));
    read_val = readl(key_dev->reg_base + 4) & (1 << 2);
    if(read_val > 0)
    {
        printk("key up\n");             //按键抬起
        key_dev->event.code = KEY_ENTER;//假设其为回车键
        key_dev->event.value = 0;
    }
    else
    {
        printk("key down\n");       //按键按下
        key_dev->event.code = KEY_ENTER;
        key_dev->event.value = 1;
    }
    /*启动中断下半部*/
    //tasklet_schedule(&key_dev->my_tasklet);
    schedule_work(&key_dev->my_work);
    return IRQ_HANDLED;
}

int get_irqno_from_node(void)
{
    int irqno;
    //获取设备树中的节点
    struct device_node *np = of_find_node_by_path("/key_init_node");
    if(np)
    {
        printk("find node ok\n");
    }
    else
    {
        printk("find node failed\n");
    }
    //通过节点获取中断号码
    irqno = irq_of_parse_and_map(np, 0);
    printk("irqno = %d\n", irqno);
    return irqno;
}

int key_drv_open(struct inode *inode, struct file *filp)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

ssize_t key_drv_read(struct file *filp, char __user *buf, size_t count, loff_t *fops)
{
    int ret;
    //printk("------%s------\n", __FUNCTION__);
    /*打开方式为非阻塞打开并且当前设备没有数据的时候立刻返回*/
    if((filp->f_flags & O_NONBLOCK) && !(key_dev->is_have_data))
    {
        return -EAGAIN;
    }
    /*在没有数据的时候直接阻塞等待数据产生*/
    wait_event_interruptible(key_dev->wq_head, key_dev->is_have_data);
    /*将内核按键数据给用户*/
    ret = copy_to_user(buf, &key_dev->event, count);
    if(ret)
    {
        printk("copy_to_user error\n");
        return -EFAULT;
    }
    /*应用程序每读取一次数据就将数据清空*/
    memset(&key_dev->event, 0, sizeof(key_dev->event));
    /*释放数据后将没有数据,清空标志位*/
    key_dev->is_have_data = 0;
    return count;
}
ssize_t key_drv_write(struct file *filp, const char __user *buf, size_t count, loff_t *fops)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

int key_drv_release(struct inode *inode, struct file *filp)
{
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

/*实现poll接口*/
unsigned int key_drv_poll (struct file *filp, struct poll_table_struct *pts)
{
    unsigned int mask;
    /*将当前等待队列注册到系统中*/
    poll_wait(filp, &key_dev->wq_head, pts);
    if(!key_dev->is_have_data)
    {
        mask = 0;       //没有数据产生
    }
    if(key_dev->is_have_data)
    {
        mask |= POLLIN; //有数据产生
    }
    return mask;
}

int key_drv_fasync (int fd, struct file *filp, int on)
{
    return fasync_helper(fd, filp, on, &key_dev->fasync);
}

const struct file_operations key_fops = {
    .open = key_drv_open,
    .read = key_drv_read,
    .write = key_drv_write,
    .release = key_drv_release,    
    .poll = key_drv_poll,
    .fasync = key_drv_fasync,
};
static int __init key_drv_init(void)
{
    int ret;
    /*为key对象分配空间*/
    key_dev = kzalloc(sizeof(struct key_desc), GFP_KERNEL);
    /*申请主设备号*/
    key_dev->dev_major = register_chrdev(0, "key_drv", &key_fops);
    /*创建设备节点文件*/
    key_dev->cls = class_create(THIS_MODULE, "key_cls");
    key_dev->dev = device_create(key_dev->cls, NULL, MKDEV(key_dev->dev_major, 0), 
                        NULL, "key0");
    /*获取中断号*/
    key_dev->irqno = get_irqno_from_node();
    ret = request_irq(key_dev->irqno, key_irq_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                "key3_eint10", NULL);
    if(ret != 0)
    {
        printk("request irq error\n");
        return ret;
    }
    key_dev->reg_base = ioremap(GPX1CON_REG, 8);
    /*初始化等待队列头*/
    init_waitqueue_head(&key_dev->wq_head);
    
    /*初始化task_let结构体*/
    //tasklet_init(&key_dev->my_tasklet, key_tasklet_half_irq, 77);
    
    /*初始化work_struct结构体*/
    INIT_WORK(&key_dev->my_work, key_workqueue_half_irq);
    printk("------%s------\n", __FUNCTION__);
    return 0;
}

static void __exit key_drv_exit(void)
{
    //tasklet_kill(&key_dev->my_tasklet);
    iounmap(key_dev->reg_base); 
    free_irq(key_dev->irqno, NULL);
    device_destroy(key_dev->cls, MKDEV(key_dev->dev_major, 0));
    class_destroy(key_dev->cls);
    unregister_chrdev(key_dev->dev_major, "key_drv");
    kfree(key_dev);
    printk(KERN_ALERT "Goodbye, curel world, this is remove\n");
}

module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
  • 执行效果
[root@iTOP-4412]# insmod key_drv.ko 
[  110.982052] key_drv: loading out-of-tree module taints kernel.
[  110.988201] find node ok
[  110.989338] irqno = 94
[  111.005903] ------key_drv_init------
[root@iTOP-4412]# [  114.935780] key down
[  114.936777] ------key_workqueue_half_irq------
[  115.787895] key up
[  115.788521] ------key_workqueue_half_irq------
[  117.465067] key down
[  117.465857] ------key_workqueue_half_irq------
[  117.889519] key up
[  117.890138] ------key_workqueue_half_irq------

你可能感兴趣的:(T6 中断编程)