跳转至

04.Linux中断实验

实验说明

使用正点原子开发板,用按键来测试中断服务

修改设备节点

在根节点“/”下创建 KEY 节点,节点名为“mini_key”,节点内容如下

mini_key {
        compatible = "alientek,gpio_key";
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_key>;
        key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;/* KEY0 */
        interrupt-parent = <&gpio1>;
        interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */
        status = "okay";
    };

然后在&iomuxc中添加pinctrl引脚

pinctrl_key: keygrp {
    fsl,pins = <
        MX6UL_PAD_UART1_CTS_B__GPIO1_IO18       0xF080  /* KEY0 */
    >;
};

函数API和流程

初始化

1、在初始化probe中,获得设备树节点信息,以下两种方式均可

struct device_node *node

//1.这种方式需要传入的probe的参数pdev
node = pdev->dev.of_node;

//2.这种方式需要知道节点名称信息
node = of_find_node_by_path("/mini_key");

2、获得GPIO编号,也有两种方式,但是第二种未测试

int gpio;

//1.of_get_named_gpio 函数获取 GPIO 编号
//参数 1:设备树节点信息 2:设备树节点中描述引脚的名称 3:第几个引脚
gpio = of_get_named_gpio(node ,"key-gpio", 0);

enum of_gpio_flags flags;
//2.of_get_gpio_flags 函数获取 GPIO 编号
//参数 1:设备树节点信息 2:第几个引脚 3:配置的状态
//这种方式好像需要把gpio配置引脚的变量名称设置为gpios
gpio = of_get_gpio_flags(node, 0, &flags);

3、设置引脚为输入模式,同时获取gpio对应的中断号,获取中断号也有两种方式

//配置输入模式
//参数 1:GPIO编号
gpio_direction_input(gpio);

int irq;
//1.gpio_to_irq获取对应的中断号
//参数 1:GPIO编号
irq = gpio_to_irq(gpio);

//2.irq_of_parse_and_map获取对应的中断号
//参数 1:设备树节点信息 2:第几个中断配置
irq = irq_of_parse_and_map(node, 0);

4、获得设备描述符,可以传入设备描述信息,通过gpio_to_desc函数,但是此步不必要,相关功能有其他方法可实现,我看正点原子和讯为都没用,只有韦东山教程用了。

struct gpio_desc *gpiod;

//参数 1:GPIO编号
gpiod = gpio_to_desc(gpio);

5、申请中断,使用request_irq申请中断,要提前定义好中断服务函数

int irq;

//中断服务函数
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{

    return IRQ_RETVAL(IRQ_HANDLED);;
}

//参数 1:中断号 2:中断服务函数 3:中断中断标志,意为上升沿触发下降沿都触发 
//     4:中断名字 5:中断触发时传入中断服务函数的参数
ret = request_irq(irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "alitenk_gpio_key", &key);

5、然后就是注册字符设备的流程了,不细说

程序结束

在程序退出的时候需要将注册的中断注销,使用free_irq函数。

//参数 1:中断号 2:中断触发时传入中断服务函数的参数
free_irq(irq, &key);

程序编写

先定义两个结构体,方便后面描述

struct key_dev{ 
    struct class *class;        
    struct device *device;
    struct device_node  *nd;
    struct gpio_desc *gpiod;
    int gpio;
    int irq;
    int major;              
    int minor;              
};

struct key_dev key; /* key设备 */

在按键初始化的时候注册中断

static int gpio_key_probe(struct platform_device *pdev)
{
    int ret;

    //1.获得设备树节点信息
    struct device_node *node = pdev->dev.of_node;
    //2.获得gpio编号    
    key.gpio = of_get_named_gpio(node ,"key-gpio", 0);
    //3.获得中断号
    gpio_direction_input(key.gpio); 
    key.irq  = gpio_to_irq(key.gpio);
    //4.设置gpio描述符
    key.gpiod = gpio_to_desc(key.gpio);
    //5.注册gpio中断
    ret = request_irq(key.irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "alitenk_gpio_key", &key);

    /*
        注册字符设备
    */

    return 0;

}

中断服务函数

通过gpiod_get_value就可以获得设备引脚的按键值

static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    struct key_dev *gpio_key = dev_id;
    int val;
    val = gpiod_get_value(gpio_key->gpiod);

    printk("key %d %d\n", gpio_key->gpio, g_key);   
    g_key = val;
    return IRQ_RETVAL(IRQ_HANDLED);
}

实验测试文件如下

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

/*
 * ./button_test /dev/100ask_button0
 *
 */
int main(int argc, char **argv)
{
    int fd;
    int val;
    int last_val;
    /* 1. 判断参数 */
    if (argc != 2) 
    {
        printf("Usage: %s <dev>\n", argv[0]);
        return -1;
    }

    /* 2. 打开文件 */
    fd = open(argv[1], O_RDWR);
    if (fd == -1)
    {
        printf("can not open file %s\n", argv[1]);
        return -1;
    }

    while (1)
    {
        /* 3. 读文件 */
        read(fd, &val, 4);
        if(val != last_val)
        {
           printf("get button : 0x%x\n", val);     
        }
        last_val = val;
    }

    close(fd);

    return 0;
}

中断下半部

这部分程序在定时器后面

先在结构体中定义struct tasklet_struct tasklet

struct key_dev{ 
    struct class *class;        
    struct device *device;
    struct device_node  *nd;
    struct gpio_desc *gpiod;
    struct timer_list timer;
    struct tasklet_struct tasklet;
    int gpio;
    int irq;
    int major;              
    int minor;              
};

然后在初始化程序中初始化tasklet。需要先定义好处理函数。

//tasklet处理函数
static void key_tasklet_func(unsigned long data)
{
    /* data ==> gpio */
    struct gpio_key *gpio_key = (struct gpio_key *)data;
    int val;
    val = gpiod_get_value(gpio_key->gpiod);
    printk("key_tasklet_func key %d %d\n", gpio_key->gpio, val);
}

static int gpio_key_probe(struct platform_device *pdev)
{
    /*注册按键中断*/

    tasklet_init(&key.tasklet, key_tasklet_func, &key);

    /*注册设备*/

    return 0;
}

在中断服务函数中使用tasklet_schedule触发这个下半部

static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    struct key_dev *gpio_key = dev_id;
    tasklet_schedule(&gpio_key->tasklet);
    return IRQ_HANDLED;
}

中断使用内核线程

在初始化的时候,使用request_threaded_irq注册中断。

//参数1:中断号 2:上半部 3:线程函数 4:中断触发方式 5:名字 6:用户参数
ret = request_threaded_irq(key.irq, gpio_key_isr, gpio_key_thread_func, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "alitenk_gpio_key", &key);

注册时写好中断上半部和线程函数,重点是上半部返回IRQ_WAKE_THREAD触发线程函数。线程中再返回IRQ_HANDLED

static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    struct key_dev *gpio_key = dev_id;
    schedule_work(&gpio_key->work);
    return IRQ_WAKE_THREAD;
}

static irqreturn_t gpio_key_thread_func(int irq, void *data)
{
    struct key_dev *gpio_key = (struct key_dev *)data;
    int val;

    val = gpiod_get_value(gpio_key->gpiod);

    printk("gpio_key_thread_func: the process is %s pid %d\n",current->comm, current->pid); 
    printk("gpio_key_thread_func key %d %d\n", gpio_key->gpio, val);

    return IRQ_HANDLED;
}