【Linux驱动】Linux中断(二)—— 按键中断驱动

前一篇已经在设备树的 gpio-led 节点中引入了中断信息,接下来将通过API来获取设备树中的中断信息。gpio-led 节点具体内容如下:

gpio-key0 {
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_gpio_keys>;            // pinctrl子系统配置电气属性
	key-gpio = <&gpio1 18 GPIO_ACTIVE_HIGH>;     // gpio子系统进行引脚初始化
	interrupt-parent = <&gpio1>;                 // 中断类型为 gpio1
	interrupts = <18 IRQ_TYPE_EDGE_FALLING>;     // 中断引脚为 GPIO1_IO18,触发方式为下降沿
	status = "okay";
};

一、中断 API

1、获取中断信息

获取设备树中 interrupts 属性的信息有两种方式,一种是针对 gpio 的方式,另一种是比较通用的方式。最终获取到的都是中断号,这里的中断号和裸机开发时的中断号不一样,裸机开发我们是根据参考文档来获取中断号

而下面通过 API 获取到的中断号是经过映射的,类似于虚拟内存和物理内存的映射,这样做的目的是保护原中断号。这也是为什么后续获取到的中断号会与裸机开发时使用的中断号不一致。

gpio_to_irq

gpio_to_irq 是仅用于获取 gpio 中断相关信息,要求对应节点的父类中断控制器为 gpio,即 interrupt-parent 属性引用的是 gpio 控制器。该接口的声明在 <asm/gpio.h>,接口原型如下: 

#define gpio_to_irq	__gpio_to_irq
/**
 * @ param gpio  表示根据gpio设备树节点获取到的 gpio 编号
 * @ return      成功返回中断号,失败返回负值
 */
int __gpio_to_irq(unsigned gpio);

irq_of_parse_and_map

irq_of_parse_and_map 是比较通用的中断信息获取方式,不仅仅适用于 gpio,也适用于其他外设中断。该接口的声明在 <linux/of_irq.h>,接口原型如下:

/**
 * @ param dev   设备树节点
 * @ param index interrupts属性索引
 * @ return      成功返回中断号,失败返回负值
 */
unsigned int irq_of_parse_and_map(struct device_node *dev,
						          int index);

注意:interrupts属性中可以包含多个中断信息,需要index来获取当前驱动所需的中断信息

2、注册中断

注册中断时主要告诉内核以下内容:

  • 中断号: 映射后的中断号
  • 触发方式:如何触发中断
  • 中断服务函数:中断触发后如何处理
  • 中断服务函数参数

注册中断使用的 API 为 request_irq,函数原型的声明在 <linux/interrupt.h>

/**
 * @param irq       映射后的中断号
 * @param handler   中断服务函数
 * @param flags     触发方式
 * @param name      中断名
 * @param dev       给中断服务函数传递的参数
 * @return          成功返回0,失败返回负值
 */
int request_irq(unsigned int irq, 
                irq_handler_t handler, 
                unsigned long flags,
	            const char *name, 
                void *dev);

中断服务函数声明:

typedef irqreturn_t (*irq_handler_t)(int, void *);

触发方式 flags(linux/irq.h)

中断名 name:

        设置以后可以在/proc/interrupts 文件中看到对应的中断名字,以此来判断中断是否注册成功

3、释放中断

注册中断后,如果模块被卸载,需要释放中断,释放中断 free_irq 的接口原型声明在 <linux/interrupt.h>

/**
 * @param irq       映射后的中断号
 * @param dev       给中断服务函数传递的参数
 */
void free_irq(unsigned int irq, void * dev);

二、驱动完善

1、驱动入口函数

驱动入口函数主要是获取中断号,并申请中断,其他的诸如申请设备号、自动创建驱动节点等操作将不再赘述。下面使用变量 status 来代表某个外设的状态,按键按下时中断触发,此时反转设备状态。

struct chrdev_t 
{
    // ...

	struct device_node* gpioNode;			/* 设备树节点 */
	uint32_t 			gpioNum;			/* gpio 引脚编号 */
	uint32_t 			irqNum;				/* 中断号 */

	uint32_t            status;				/* 设备状态 */
};
static struct chrdev_t chrdev;

/* 驱动入口函数 */
static int __init kerneltimer_init(void)
{
	uint32_t ret = 0;

	/* 获取key0设备树节点 */
	chrdev.gpioNode = of_find_node_by_path("/gpio-key0");
	if(chrdev.gpioNode == NULL)
	{	
		printk("node cannot be found!\n");
		return -1;
	}
    // 获取 gpio 编号
	chrdev.gpioNum = of_get_named_gpio(chrdev.gpioNode, "key-gpio", 0);
	if (chrdev.gpioNum < 0)
	{
		printk("gpio property fetch failed!\n");
		return -1;
	}
    // 配置 gpio 为输入
	ret = gpio_direction_input(chrdev.gpioNum);
	if (ret < 0)
	{
		printk("gpio set failed!\n");
		return -1;
	}

#if 1
	// 根据gpio编号获取中断信息
	chrdev.irqNum = gpio_to_irq(chrdev.gpioNum);
	if (chrdev.irqNum < 0)
	{
		printk("irq number fetch failed!\n");
		return -1;
	}
#else
	// 根据节点获取中断号
	chrdev.irqNum = irq_of_parse_and_map(chrdev.gpioNode, 0);
	if (chrdev.irqNum < 0)
	{
		printk("irq number fetch failed!\n");
		return -1;
	}
#endif
	printk("中断号: %u\n", chrdev.irqNum);
    // 设备初始状态为 0
	chrdev.status = 0;
	// 注册中断
	ret = request_irq(chrdev.irqNum, key0_handler, IRQ_TYPE_EDGE_FALLING, "key0-int", &chrdev);
	if (ret < 0)
	{
		printk("irq subscribe failed!\n");
		return -1;
	}

    // ... 
}

2、中断服务函数

上面在介绍注册中断 API 时已经提及了中断服务函数的声明,第一个参数为 中断号,第二个参数为注册时传递给中断服务函数的参数

static irqreturn_t key0_handler(int irq, void * dev)
{
	struct chrdev_t* pdev = (struct chrdev_t*)dev;

	// 状态反转
	pdev->status = !pdev->status;
	return IRQ_RETVAL(IRQ_HANDLED);
}

3、read 操作函数

static ssize_t chrdev_read(struct file *pfile, char __user * pbuf, size_t size, loff_t * poff)
{
    // 在 open 函数中 pfile->private_data = &chrdev;
	struct chrdev_t* pdev = pfile->private_data;
	unsigned long ret = 0;
	
    // 将设备状态返回给应用层
	ret = copy_to_user(pbuf, &pdev->status, sizeof(pdev->status));
	if (ret != 0)
	{
		printk("kernel send data failed!\n");
		return -1;
	}
	
	return sizeof(pdev->status);
}

4、驱动退出函数

驱动退出函数需要释放中断

static void __exit kerneltimer_exit(void)
{
    // ... 

	/* 注销中断 */
	free_irq(chrdev.irqNum, &chrdev);
}

三、测试

在应用程序中每隔 1s 调用 read 函数来获取设备状态

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

#define delayms(x)        usleep(x * 1000)

void printHelp()
{
    printf("usage: ./xxxApp <driver_path>\n");
}

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        printHelp();
        return -1;
    }
    
    char* driver_path = argv[1];       // 位置0 保存的是 ./chrdevbaseApp
    int state = 0;
    int ret = 0;
    int fd = 0;

    fd = open(driver_path, O_RDONLY);
    if (fd < 0)
    {
        perror("open file failed");
        return -2;
    }

    while (1)
    {
        ret = read(fd, &state, sizeof(state));
        if (ret < 0)
        {
            printf("read data error\n");
            break;
        }

        printf("中断触发状态:%d\n", state);
        delayms(1000);
    }
    
    close(fd);
    return 0;
}

裸机开发时,GPIO1_IO18 对应的中断号为99,现在因为经过一层映射,屏蔽了真正的中断号,使用了虚拟中断号,所以这里的中断号为 47 

应用程序的测试结果如下,按下按键时触发中断,此时状态反转。