利用设备树、Platform总线、BSP、GPIO子系统和Linux的中断处理系统实现第一个中断驱动程序(按键动作为中断源,中断处理函数中打印中断的相关信息-无用户空间的交互)

引言

经过前面一段时间对Linux驱动程序框架的学习,已经对其框架有比较熟悉的了解了,所以这一篇博文直接开始分析中断驱动程序中的关键函数gpio_key_probe

整个驱动程序实际上是用到了设备树、BSP、GPIO子系统、Platform驱动总线,这些相关知识可以通过我的另一篇博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145119224 来获得唤醒。

注意:表面上这篇博文中的代码没用到BSP,实际上是用到了的,因为GPIO子系统实际上要想正常工作,就需要BSP提供的底层函数实现。

在这篇博文中,利用GPIO5_IO01GPIO4_IO14的输入功能,它们引脚上的上升沿和下降沿会触发中断,并对中断信息进行打印输出。

完整源码

中断驱动程序gpio_key_drv.c的源码

#include <linux/module.h>

#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>


struct gpio_key{
	int gpio;
	struct gpio_desc *gpiod;
	int flag;
	int irq;
} ;

static struct gpio_key *gpio_keys_100ask;


static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    struct gpio_key *gpio_key = dev_id;
    int val;

    // 返回引脚电平的逻辑值,注意:如果是低电平有效,则当物理电平为低电平时,其返回值为1;则当物理电平为高电平时,其返回值为0.
	// 如果要得到物理电平值,可以用函数gpiod_get_raw_value()得到
    val = gpiod_get_value(gpio_key->gpiod);

    // 打印中断号、GPIO引脚编号和电平值
    printk("Interrupt number: %d; GPIO pin number: %d; Pin Logical value: %d\n", irq, gpio_key->gpio, val);

    return IRQ_HANDLED;  // 表示中断已处理
}


/* 1. 从platform_device获得GPIO
 * 2. gpio=>irq
 * 3. request_irq
 */
static int gpio_key_probe(struct platform_device *pdev)
{
	int err;

	// 获取设备树节点指针
	struct device_node *node = pdev->dev.of_node;

	// count用于存储设备树中描述的GPIO口的数量
	int count;
	
	int i;
	enum of_gpio_flags flag;
	unsigned flags = GPIOF_IN;
		
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	count = of_gpio_count(node);
	if (!count)
	{
		printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
		return -1;
	}

	gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
	if (!gpio_keys_100ask) {
		printk("Memory allocation failed for gpio_keys_100ask\n");
		return -ENOMEM;
	}


	for (i = 0; i < count; i++)
	{
		//  获取GIPO的全局编号及其标志位信息的代码
		gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
		if (gpio_keys_100ask[i].gpio < 0)
		{
			printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
			return -1;
		}

		// 获取GPIO口的GPIO描述符的代码
		gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
		if (!gpio_keys_100ask[i].gpiod) {
			printk("Failed to get GPIO descriptor for GPIO %d\n", gpio_keys_100ask[i].gpio);
			return -EINVAL;
		}

		// 结构体gpio_key的成员flag用于存储对应的GPIO口是否是低电平有效,假如是低电平有效,成员flag的值为1,假如不是低电平有效,成员flag的值为0。
		// 后续代码实际上并没有用到成员flag,这里出现这句代码只是考虑到代码的可扩展性,所以在这里是可以删除的。
		gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;

		// 每次循环都重新初始化flags
    	flags = GPIOF_IN;

		// 假如GPIO口是低电平有效,则把flags添加上低电平有效的信息
		if (flag & OF_GPIO_ACTIVE_LOW)
			flags |= GPIOF_ACTIVE_LOW;

		// 请求一个GPIO硬件资源与设备结构体`pdev->dev`进行绑定
		// 注意,这个绑定操作会在调用函数platform_driver_unregister()注销platform_driver时自动由内核解除绑定操作,所以gpio_key_remove函数中不需要显示去解除绑定
		// 由`devm`开头的函数通常都会内核自动管理资源,咱们在退出函数中不用人为的去释放资源或解除绑定。
		err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);

		// 获取GPIO口的中断请求号
		gpio_keys_100ask[i].irq  = gpio_to_irq(gpio_keys_100ask[i].gpio);
	}

	for (i = 0; i < count; i++)
	{
		char irq_name[32];  // 用于存储动态生成的中断名称

		//使用snprintf()函数将动态生成的中断名称写入irq_name数组
		snprintf(irq_name, sizeof(irq_name), "swh_gpio_irq_%d", i);  // 根据i生成名称

		//调用函数request_irq()来请求并设置一个中断
		err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, irq_name, &gpio_keys_100ask[i]);
	}
   
    return 0;
    
}

static int gpio_key_remove(struct platform_device *pdev)
{
    struct device_node *node = pdev->dev.of_node;
    int count;
    int i;

    count = of_gpio_count(node);
    for (i = 0; i < count; i++) 
	{
        // 只有在irq有效时才释放中断资源
        if (gpio_keys_100ask[i].irq >= 0) {
            // 释放GPIO中断资源,下面这句代码做了下面两件事:
			// 1、解除 `gpio_keys_100ask[i].irq` 中断号和 `gpio_key_isr` 中断处理函数的绑定。
			// 2、解除 `gpio_keys_100ask[i].irq` 中断号和中断处理函数与 `gpio_keys_100ask[i]` 数据结构的绑定。
            free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
        }

        // 释放GPIO描述符
        if (gpio_keys_100ask[i].gpiod) {
            gpiod_put(gpio_keys_100ask[i].gpiod);
        }
    }

    // 释放内存
    kfree(gpio_keys_100ask);
    return 0;
}


static const struct of_device_id irq_matach_table[] = {
    { .compatible = "swh-gpio_irq_key" },
    { },
};

/* 1. 定义platform_driver */
static struct platform_driver gpio_keys_driver = {
    .probe      = gpio_key_probe,
    .remove     = gpio_key_remove,
    .driver     = {
        .name   = "swh_irq_platform_dirver",
        .of_match_table = irq_matach_table,
    },
};

/* 2. 在入口函数注册platform_driver */
static int __init gpio_key_init(void)
{
    int err;
    
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
    err = platform_driver_register(&gpio_keys_driver); 
	
	return err;
}

/* 3. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
 *     卸载platform_driver
 */
static void __exit gpio_key_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    platform_driver_unregister(&gpio_keys_driver);
}


/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */

module_init(gpio_key_init);
module_exit(gpio_key_exit);

MODULE_LICENSE("GPL");

设备树文件中的相关源码

    gpio_irq_node {
        compatible = "swh-gpio_irq_key";
		gpios = <&gpio5 1 GPIO_ACTIVE_LOW
		 &gpio4 14 GPIO_ACTIVE_LOW>;
		
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_key1 &pinctrl_key2>;
    };

.....

&iomuxc_snvs {
    pinctrl-names = "default_snvs";
    pinctrl-0 = <&pinctrl_hog_2>;
    imx6ul-evk {
        pinctrl_key1: key1_gpio {
            fsl,pins = <
                MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01        0x000110A0
            >;
        };
......


&iomuxc {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_hog_1>;
    imx6ul-evk {
        pinctrl_key2: key2_gpio {
            fsl,pins = <
                MX6UL_PAD_NAND_CE1_B__GPIO4_IO14           0x000010B0
            >;
        };
......

函数gpio_key_probe()的分析

函数gpio_key_probe()的完整代码

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

	// 获取设备树节点指针
	struct device_node *node = pdev->dev.of_node;

	// count用于存储设备树中描述的GPIO口的数量
	int count;
	
	int i;
	enum of_gpio_flags flag;
	unsigned flags = GPIOF_IN;
		
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	count = of_gpio_count(node);
	if (!count)
	{
		printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
		return -1;
	}

	gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
	if (!gpio_keys_100ask) {
		printk("Memory allocation failed for gpio_keys_100ask\n");
		return -ENOMEM;
	}


	for (i = 0; i < count; i++)
	{
		//  获取GIPO的全局编号及其标志位信息的代码
		gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
		if (gpio_keys_100ask[i].gpio < 0)
		{
			printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
			return -1;
		}

		// 获取GPIO口的GPIO描述符的代码
		gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
		if (!gpio_keys_100ask[i].gpiod) {
			printk("Failed to get GPIO descriptor for GPIO %d\n", gpio_keys_100ask[i].gpio);
			return -EINVAL;
		}

		// 结构体gpio_key的成员flag用于存储对应的GPIO口是否是低电平有效,假如是低电平有效,成员flag的值为1,假如不是低电平有效,成员flag的值为0。
		// 后续代码实际上并没有用到成员flag,这里出现这句代码只是考虑到代码的可扩展性,所以在这里是可以删除的。
		gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;

		// 每次循环都重新初始化flags
    	flags = GPIOF_IN;

		// 假如GPIO口是低电平有效,则把flags添加上低电平有效的信息
		if (flag & OF_GPIO_ACTIVE_LOW)
			flags |= GPIOF_ACTIVE_LOW;

		// 请求一个GPIO硬件资源与设备结构体`pdev->dev`进行绑定
		// 注意,这个绑定操作会在调用函数platform_driver_unregister()注销platform_driver时自动由内核解除绑定操作,所以gpio_key_remove函数中不需要显示去解除绑定
		// 由`devm`开头的函数通常都会内核自动管理资源,咱们在退出函数中不用人为的去释放资源或解除绑定。
		err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);

		// 获取GPIO口的中断请求号
		gpio_keys_100ask[i].irq  = gpio_to_irq(gpio_keys_100ask[i].gpio);
	}

	for (i = 0; i < count; i++)
	{
		char irq_name[32];  // 用于存储动态生成的中断名称

		//使用snprintf()函数将动态生成的中断名称写入irq_name数组
		snprintf(irq_name, sizeof(irq_name), "swh_gpio_irq_%d", i);  // 根据i生成名称

		//调用函数request_irq()来请求并设置一个中断
		err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, irq_name, &gpio_keys_100ask[i]);
	}
   
    return 0;
    
}

代码struct device_node *node = pdev->dev.of_node;

struct device_node *node = pdev->dev.of_node;

在这里插入图片描述
这句代码的作用是从平台设备 (platform_device) 中获取与其绑定的设备树节点指针。这句代码实现了驱动程序与设备树的绑定,从设备树中提取硬件配置信息。下面对这句代码进行详细分析:


关键点解析

  1. pdev->dev:

    • pdev 是一个指向 struct platform_device 类型的指针。
    • pdev->dev 是该设备的 struct device 成员,表示设备的通用属性。
  2. pdev->dev.of_node:

    • of_nodestruct device 中的一个成员,类型为 struct device_node *,表示与该设备关联的设备树节点。
    • 如果该设备是通过设备树描述的,那么 pdev->dev.of_node 会指向对应的设备树节点。如果设备树中没有对应的节点,则该值为 NULL
  3. struct device_node *node:

    • node 是一个局部变量,用来保存 pdev->dev.of_node 的值。
    • 通过这个变量,后续代码可以方便地访问设备树节点的属性,例如 GPIO 配置、IRQ 信息等。

后续使用
在后续代码中,node 被GPIO子系统的函数调用,例如:

count = of_gpio_count(node);
  • 通过 node,驱动可以调用相关的设备树 API(如 of_gpio_count)来获取当前设备关联的 GPIO 数量或其他参数。

代码enum of_gpio_flags flag;

enum of_gpio_flags flag;

在这里插入图片描述
这句代码定义了一个名为 flag 的变量,其类型是枚举类型 enum of_gpio_flags

枚举类型的相关基础知识见我的另一篇博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145177544
下面对其详细分析:

代码背景
enum of_gpio_flags 是一个枚举类型,用于表示设备树(Device Tree)中与 GPIO(通用输入输出)相关的标志位。这些标志通常在从设备树解析 GPIO 配置时使用,标志的作用是指示特定 GPIO 的行为或属性。

枚举类型的相关基础知识见我的另一篇博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145177544


关键点解析

  1. enum of_gpio_flags

    • 这是在内核中定义的一个枚举类型,常用于表示 GPIO 的特殊属性。
    • 定义位置:内核代码中的 include/linux/of_gpio.h 文件。
    • 枚举值的常见示例包括:
      enum of_gpio_flags {
          OF_GPIO_ACTIVE_LOW = 0x1,   // GPIO为低电平时表示“活动”状态
          OF_GPIO_SINGLE_ENDED = 0x2, // GPIO是单端类型
          OF_GPIO_OPEN_DRAIN = 0x4,   // GPIO为开漏模式
          OF_GPIO_OPEN_SOURCE = 0x8,  // GPIO为开源模式
      };
      
  2. flag 的作用

    • flag 用于存储某个 GPIO 的标志信息,标志信息通常由设备树的属性中解析出来。
    • 这些标志会影响驱动程序对 GPIO 的配置和行为控制。例如:
      • 如果 flag 包含 OF_GPIO_ACTIVE_LOW,表示该 GPIO 的“活动”状态是低电平,驱动需要根据此配置调整逻辑。
  3. 设备树中的标志来源

    • 设备树节点通常会定义 GPIO 的属性,类似以下形式:
      key-gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
      
      • GPIO_ACTIVE_LOW 是设备树中的标志,会被解析为 OF_GPIO_ACTIVE_LOW
  4. 代码中的使用
    在代码中,flag 被传递给函数,如:

    gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
    
    • 函数 of_get_gpio_flags 会读取设备树节点,解析与某个 GPIO 相关的标志,并存储到 flag 变量中。
  5. 影响后续逻辑
    flag 的值在后续逻辑中被用于条件判断或配置。例如:

    if (flag & OF_GPIO_ACTIVE_LOW)
        .......
    
    • 如果标志位表示 GPIO 是活动低电平,那么 flags 会设置相应的属性,影响 GPIO 的初始化。

总结
这句代码的核心作用是为后续从设备树解析 GPIO 标志位准备变量。通过存储设备树提供的 GPIO 标志信息,驱动程序可以动态适配硬件的具体属性,实现更灵活的驱动设计。这种设计在嵌入式系统中非常常见,尤其是在需要支持多种硬件配置的情况下。

代码gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);

	gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
	if (!gpio_keys_100ask) {
		printk("Memory allocation failed for gpio_keys_100ask\n");
		return -ENOMEM;
	}

这句代码分配了一块内存,用于存储多个 struct gpio_key 类型的结构体实例。

结构体 struct gpio_key 就是自己定义在这个源代码文件(gpio_key_drv.c)中的,其定义如下:

struct gpio_key{
	int gpio;
	struct gpio_desc *gpiod;
	int flag;
	int irq;
} ;

它将用于保存 每个GPIO口的相关信息(如 GPIO 编号、GPIO 描述符、标志、IRQ 编号)。

以下是对这行代码的详细分析:


代码背景

  1. gpio_keys_100ask
    • 这是一个全局变量,类型是指向 struct gpio_key 的指针。相关代码如下:
struct gpio_key{
	int gpio;
	struct gpio_desc *gpiod;
	int flag;
	int irq;
} ;

static struct gpio_key *gpio_keys_100ask;
  • 它将用于保存多个 GPIO 的相关信息(如 GPIO 编号、GPIO 描述符、标志、IRQ 编号等)。
  1. kzalloc 函数

    • kzalloc 是 Linux 内核中常用的动态内存分配函数。
    • 功能:分配一块内存,并将分配的内存清零。
    • 原型:
      void *kzalloc(size_t size, gfp_t flags);
      
      • size:需要分配的内存大小(以字节为单位)。
      • flags:内存分配标志,决定分配行为(如使用何种内存区、是否可以睡眠等)。
    • 返回值:分配的内存首地址。如果分配失败,返回 NULL
  2. sizeof(struct gpio_key) * count

    • sizeof(struct gpio_key):计算单个 struct gpio_key 结构体所占的内存大小。
    • count:GPIO 的数量(通过 of_gpio_count 获取)。
    • 乘积表示需要为 countstruct gpio_key 分配足够的连续内存。
  3. GFP_KERNEL

    • 这是一个内存分配标志,表示可以阻塞进程并调用调度器来完成分配。适用于内核模式下需要分配普通内存的场景。

功能解析

  1. 分配内存

    • 该代码分配了一个足够容纳 countstruct gpio_key 的内存块,确保程序有空间存储所有的 GPIO 键信息。
  2. 初始化为零

    • 使用 kzalloc 而非 kmalloc,确保分配的内存清零(相当于所有字段都初始化为 0)。
    • 清零的好处:
      • 避免未初始化数据导致的潜在问题。
      • 对某些逻辑的默认值要求(如 gpio 字段初始化为 0)尤为有用。
  3. 赋值给 gpio_keys_100ask

    • 分配的内存地址被存储到全局变量 gpio_keys_100ask 中,使后续代码可以通过它访问所有的 GPIO 键信息。

错误处理
虽然代码本身没有显式的错误处理逻辑,但在实际开发中,分配内存可能失败。所以在分配后检查返回值是否为 NULL

gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
if (!gpio_keys_100ask) {
    printk("Memory allocation failed for gpio_keys_100ask\n");
    return -ENOMEM;
}

总结
这句代码的作用是:

  1. 动态分配一块内存,用于存储多个 GPIO 键的配置信息。
  2. 分配的内存会被清零,避免未初始化问题。
  3. 将分配的内存地址赋值给全局变量 gpio_keys_100ask,为后续操作提供基础。

这种内存管理方式是内核驱动程序中处理动态数量设备的典型方法。

获取GIPO的全局编号及其标志位信息的代码

gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);

在这里插入图片描述

这句代码的作用是从设备树节点中获取第 i 个 GPIO 的编号以及相关标志位信息,并将其存储到 gpio_keys_100ask[i].gpio 中。以下是对这句代码的详细分析:


代码背景

  1. gpio_keys_100ask

    • 这是一个指向 struct gpio_key 的数组,存储多个 GPIO 键的配置信息。
    • gpio_keys_100ask[i].gpio 表示数组中第 i 个 GPIO 键的 GPIO 编号。
  2. of_get_gpio_flags 函数

    • 此函数用于从设备树节点中解析 GPIO 的编号,并同时获取与该 GPIO 相关的标志位。
    • 原型:
      int of_get_gpio_flags(struct device_node *np, int index, enum of_gpio_flags *flags);
      
      • np:指向设备树节点的指针。
      • index:要解析的 GPIO 的索引(从 0 开始)。
      • flags:指向 enum of_gpio_flags 类型的指针,用于返回该 GPIO 的标志。
      • 返回值:成功时返回 GPIO 的编号;失败时返回负值(如 -EINVAL-ENOENT)。
  3. node

    • 是一个指向设备树中当前设备节点的指针(由 pdev->dev.of_node 提供)。
    • 表示设备树中与该驱动绑定的硬件描述部分。
  4. i

    • 当前 GPIO 的索引。
    • 在循环中,i 从 0 到 count-1,表示从设备树节点中逐个解析每个 GPIO。
  5. &flag

    • flag 是一个局部变量,用于存储与当前 GPIO 相关的标志位。
    • 标志位的典型值包括:
      • OF_GPIO_ACTIVE_LOW:GPIO 为低电平时表示“活动”。
      • OF_GPIO_SINGLE_ENDED:GPIO 是单端模式。
      • OF_GPIO_OPEN_DRAIN:GPIO 为开漏模式。
      • OF_GPIO_OPEN_SOURCE:GPIO 为开源模式。

功能解析

  1. 解析 GPIO 编号

    • 调用 of_get_gpio_flags,从设备树节点 node 中解析出第 i 个 GPIO 的硬件编号。
    • 该编号可能是 GPIO 控制器中的索引值,用于后续 GPIO 配置和操作。
  2. 获取 GPIO 标志

    • 将设备树节点中解析出的标志存储到 flag 中。
    • 标志信息可以指示该 GPIO 的特殊行为(如电平活动状态),在后续代码中会被用来调整 GPIO 的配置。
  3. 存储结果

    • 解析出的 GPIO 编号被存储到 gpio_keys_100ask[i].gpio 中。
    • 标志位被存储到局部变量 flag 中,并可能在后续逻辑中使用,例如:
      gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
      

错误处理
由于设备树的解析可能失败,应该检查返回值:

  • 如果 of_get_gpio_flags 返回负值(如 -EINVAL),表示解析失败。
  • 当前代码中已处理了这一情况:
    if (gpio_keys_100ask[i].gpio < 0) {
        printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
        return -1;
    }
    

实际举例
假设设备树中有以下描述:

keys {
    compatible = "100ask,gpio_key";
    key-gpios = <&gpio1 5 GPIO_ACTIVE_LOW>, 
                <&gpio2 3 0>;
};
  • 第一个 GPIO 的编号是 5,标志为 GPIO_ACTIVE_LOW
  • 第二个 GPIO 的编号是 3,没有标志。

那么很有可能经 of_get_gpio_flags 解析后的解析后,得到的结果如下:

  • gpio_keys_100ask[0].gpio = 37,flag 包含 OF_GPIO_ACTIVE_LOW
  • gpio_keys_100ask[1].gpio = 67,flag 为 0。

其中的37和67是每一个GPIO口的全局编号,具体的解析过程如下:

  1. of_get_gpio_flags 的工作机制

    • 根据设备树语句中 <&gpioX> 的引用【比如例子中的&gpio1&gpio2】,找到对应的 GPIO 控制器节点(如 gpio1gpio2)。
    • 读取 GPIO 控制器的基编号(base)以及设备树中描述的本地编号(如上面例子中的 53)。
    • 计算全局 GPIO 编号:
      global_gpio_number = base_number + local_pin_number
      
  2. 具体示例

    • 假设:
      • gpio1 的基编号是 32
      • gpio2 的基编号是 64
    • 对于第一个 GPIO:
      • base_number = 32local_pin_number = 5
      • 全局 GPIO 编号 = 32 + 5 = 37
      • gpio_keys_100ask[0].gpio = 37
    • 对于第二个 GPIO:
      • base_number = 64local_pin_number = 3
      • 全局 GPIO 编号 = 64 + 3 = 67
      • gpio_keys_100ask[1].gpio = 67
  3. 标志解析

    • 标志位(如 GPIO_ACTIVE_LOW)保存在 flag 中,用于后续逻辑。
    • 例如:
      gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
      

最终结果

  • 经过 of_get_gpio_flags 解析后:
  • gpio_keys_100ask[0].gpio = 37(GPIO口的全局编号),flag 包含 OF_GPIO_ACTIVE_LOW
  • gpio_keys_100ask[1].gpio = 67(GPIO口的全局编号),flag 为 0。

总结
这句代码的核心作用是:

  1. 从设备树节点解析第 i 个 GPIO 的编号。
  2. 获取与该 GPIO 相关的标志位信息。
  3. 将解析结果存储到 gpio_keys_100ask[i] 中,以便后续配置和操作。

它是设备树驱动程序中处理硬件资源动态分配的关键步骤,确保驱动可以根据设备树的描述自动适配硬件配置。

获取GPIO口的GPIO描述符的代码

gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);

在这里插入图片描述
这句代码的作用是将 gpio_keys_100ask[i].gpio 中的 GPIO 编号转换为对应的 GPIO 描述符(struct gpio_desc),并存储在 gpio_keys_100ask[i].gpiod 中。以下是详细分析:


代码背景

  1. GPIO 描述符(struct gpio_desc

    • 在现代 Linux 内核中,struct gpio_desc 是对 GPIO 控制器中 GPIO 引脚的抽象。
    • 与直接使用 GPIO 编号相比,GPIO 描述符提供了更高级别的接口,更易于管理和扩展。
  2. gpio_to_desc 函数

    • 这是一个内核函数,用于将传统的 GPIO 编号转换为 struct gpio_desc *
    • 原型:
      struct gpio_desc *gpio_to_desc(unsigned int gpio);
      
      • 参数 gpio:GPIO 编号。
      • 返回值:指向 struct gpio_desc 的指针;如果 GPIO 无效,则返回 NULL
  3. gpio_keys_100ask[i].gpio

    • 此字段存储了从设备树解析得到的 GPIO 编号。
    • 编号通常是全局的,表示一个唯一的 GPIO 引脚。
  4. gpio_keys_100ask[i].gpiod

    • 此字段用于存储 GPIO 描述符。
    • 一旦赋值成功,可以通过描述符访问 GPIO 引脚的状态、设置方向、配置中断等。

功能解析

  1. 将 GPIO 编号转换为 GPIO 描述符

    • gpio_to_desc 查找对应于 gpio_keys_100ask[i].gpio 的 GPIO 描述符。
    • 如果找到有效的描述符,则返回指针;否则返回 NULL
  2. 存储转换结果

    • 转换后的描述符存储在 gpio_keys_100ask[i].gpiod 中。
    • 这样,驱动程序可以通过描述符而不是直接操作 GPIO 编号,使用更抽象和统一的接口。
  3. 后续操作依赖描述符

    • 描述符在后续操作中用于访问 GPIO 的状态。例如:
      int val = gpiod_get_value(gpio_keys_100ask[i].gpiod);
      
      • 通过描述符调用 gpiod_get_value 获取 GPIO 的电平状态。

GPIO 描述符的优点
使用 GPIO 描述符代替传统 GPIO 编号的优势:

  1. 更安全:GPIO 描述符通过类型系统和接口封装,减少直接使用编号带来的错误风险。
  2. 统一接口:描述符提供了标准化的 API,便于跨平台开发。
  3. 支持虚拟化:描述符可以支持更复杂的 GPIO 管理机制,例如虚拟 GPIO。

错误处理
虽然这段代码本身未处理错误,但在实际开发中,需注意 gpio_to_desc 可能返回 NULL,导致后续操作失败。因此在转换后作如下检查:

gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
if (!gpio_keys_100ask[i].gpiod) {
    printk("Failed to get GPIO descriptor for GPIO %d\n", gpio_keys_100ask[i].gpio);
    return -EINVAL;
}

用途示例
假设从设备树解析到的 GPIO 编号为 37,通过调用:

gpio_keys_100ask[0].gpiod = gpio_to_desc(37);

如果编号有效,则 gpio_to_desc 返回对应描述符。此描述符可以用于:

  • 设置方向:
    gpiod_direction_input(gpio_keys_100ask[0].gpiod);
    
  • 获取状态:
    int val = gpiod_get_value(gpio_keys_100ask[0].gpiod);
    

总结
这句代码的核心作用是:

  1. 将传统 GPIO 编号转换为更抽象的 GPIO 描述符。
  2. 存储描述符,供后续操作使用。

通过这种转换,驱动程序可以使用统一的接口对 GPIO 引脚进行操作【具体的对寄存器的操作通过调用更底层的BSP函数实现】,提高代码的可移植性和可维护性。

请求一个GPIO硬件资源与设备结构体pdev->dev进行绑定

代码如下:

err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);

这行代码:

err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);

这里,调用了devm_gpio_request_one()函数来请求一个GPIO。接下来对每个参数的分析如下:

  1. &pdev->dev
  • 这是传递给函数的设备指针,pdevplatform_device结构体类型的指针。pdev->dev是这个设备对应的struct device结构体。
  • 这个参数指示GPIO请求与哪个设备关联。GPIO是硬件资源,而驱动通常会将硬件资源和设备绑定起来,以确保资源的正确管理。
  1. gpio_keys_100ask[i].gpio
  • 这是所请求的GPIO编号,它来自数组gpio_keys_100ask中的第i个元素。gpio_keys_100ask[i].gpio是之前通过设备树获取到的GPIO编号,代表一个具体的GPIO引脚。
  • 该参数表示希望通过devm_gpio_request_one()请求和控制的GPIO引脚。
  1. flags
  • flags是请求GPIO时的标志,决定GPIO的工作模式。在本代码中,flagsGPIOF_IN(输入模式)。
  • 如果GPIO是低电平有效的,flags还会通过GPIOF_ACTIVE_LOW来设置,以指示该GPIO是低电平有效。
  1. NULL
  • 这是GPIO请求函数中的一个附加参数,它是一个指向描述符的指针,可以用来传递给设备树、资源管理或其他回调。但在这里,它被传递为NULL,意味着没有附加的描述符或特殊的数据传递。

devm_gpio_request_one()函数作用:

  • devm_gpio_request_one()是内核提供的一个函数,用来请求一个GPIO并将其标记为管理设备(devm,即资源管理模式)。这个函数会将GPIO请求的生命周期与设备的生命周期绑定。
  • 该函数成功时会返回0,失败时会返回负数错误码。
  • devm表示该GPIO在设备注销或驱动卸载时会自动释放,无需手动释放。

总结:
这行代码的作用是:请求一个GPIO(由gpio_keys_100ask[i].gpio表示),并设置其为输入模式(flags中包含GPIOF_IN)。GPIO的生命周期与驱动程序的设备生命周期绑定,确保在设备卸载时自动释放该资源。

如果该请求失败,err将保存返回的错误码。

补充说明:注意,这个绑定操作会在调用函数platform_driver_unregister()注销platform_driver时自动由内核解除绑定操作,所以gpio_key_remove函数中不需要显示去解除绑定,由devm开头的函数通常都会内核自动管理资源,咱们在退出函数中不用人为的去释放资源或解除绑定。

获取GPIO口的中断请求号

gpio_keys_100ask[i].irq  = gpio_to_irq(gpio_keys_100ask[i].gpio);

在这里插入图片描述

这行代码:

gpio_keys_100ask[i].irq  = gpio_to_irq(gpio_keys_100ask[i].gpio);

的作用是将GPIO引脚(由gpio_keys_100ask[i].gpio表示)转换为与之对应的中断请求号(IRQ)。下面是详细分析:

  1. gpio_keys_100ask[i].gpio
  • 这是一个GPIO编号,指向第i个按键的GPIO引脚。在之前的代码中,这个编号是通过函数of_get_gpio_flags获取的。
  • 它表示一个硬件GPIO引脚,可以用来检测按键的按下或释放。
  1. gpio_to_irq()
  • gpio_to_irq()是内核提供的一个函数,用来将一个GPIO引脚转换为与之关联的中断号(IRQ)。这个函数是基于硬件平台和设备树配置的,因此它返回的IRQ编号是具体平台的硬件资源。
  • 在许多嵌入式系统中,GPIO引脚本身不仅用作输入输出,还可以用作外部中断源。调用gpio_to_irq()后,你可以通过中断机制来响应GPIO引脚上的电平变化(例如按键按下或释放)。
  • 该函数的返回值是与指定GPIO引脚相关联的IRQ编号。对于每个GPIO,如果它被配置为中断触发模式(例如,按键按下时触发中断),调用此函数将返回该GPIO对应的IRQ号。
  1. 将结果存储到 gpio_keys_100ask[i].irq
  • gpio_keys_100ask[i].irq是存储中断请求号(IRQ)的变量。通过这行代码,将gpio_to_irq()函数返回的中断号保存到这个位置。这样,你就可以使用这个中断号来注册中断服务程序(ISR)。
  • 中断服务程序(ISR)是一个回调函数,用来处理中断触发后的响应行为。在这个例子中,你会使用gpio_keys_100ask[i].irq来注册中断处理程序(request_irq()),从而在GPIO引脚的电平变化时触发中断。

总结:
这行代码的作用是将指定的GPIO引脚转换为对应的中断请求号(IRQ),并将结果存储在gpio_keys_100ask[i].irq中。之后,你可以使用这个IRQ号来注册中断处理程序,以响应GPIO引脚的电平变化(例如按键按下或释放)。

调用函数request_irq()来请求并设置一个中断

	for (i = 0; i < count; i++)
	{
		char irq_name[32];  // 用于存储动态生成的中断名称

		//使用snprintf()函数将动态生成的中断名称写入irq_name数组
		snprintf(irq_name, sizeof(irq_name), "swh_gpio_irq_%d", i);  // 根据i生成名称

		err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, irq_name, &gpio_keys_100ask[i]);
	}

关键代码:

err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, irq_name, &gpio_keys_100ask[i]);

调用了request_irq()函数来请求一个中断,下面是详细分析每个参数的含义和作用:

  1. gpio_keys_100ask[i].irq
  • 这是中断请求号(IRQ)。它是之前通过gpio_to_irq()获得的GPIO引脚的中断编号。gpio_keys_100ask[i].irq存储了与第i个GPIO引脚关联的中断号。
  • 该参数指定了需要请求的中断源。中断号表示特定的硬件中断线,内核通过它来触发中断处理程序(ISR)。
  1. gpio_key_isr
  • 这是中断服务程序(Interrupt Service Routine,ISR)。它是自己写的一个回调函数(后面会具体分析),当中断发生时,内核会调用此函数来处理相应的事件。
  • gpio_key_isr函数在这里用来处理中断。通常,它会读取GPIO的状态,进行一些处理,例如按键状态的变化(按下或松开)并打印日志,或执行其他响应操作。
  1. IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING
  • 这些是中断触发的条件标志。
  • IRQF_TRIGGER_RISING表示中断在GPIO引脚电平由低变高(上升沿)时触发。
  • IRQF_TRIGGER_FALLING表示中断在GPIO引脚电平由高变低(下降沿)时触发。
  • 将这两个标志结合起来,意味着这个中断会在GPIO引脚的上升沿或下降沿触发。换句话说,不论按键按下或松开,都会触发这个中断。
  1. irq_name
  • 这是中断的名称,中断的具体名称存储于字符串变量irq_name中,用来标识这个中断。它可以帮助内核区分不同的中断请求,特别是在调试和日志记录时非常有用。
  • 该参数并不直接影响中断的处理行为,只是一个标识字符串,用来描述这次中断的来源。
  1. &gpio_keys_100ask[i]
  • 这是与该中断关联的数据指针。在中断发生时,内核会将这个指针作为dev_id参数传递给中断服务程序(gpio_key_isr)。
  • 这个指针指向了gpio_keys_100ask[i]结构体,该结构体包含了GPIO引脚、GPIO描述符、IRQ号等信息。在中断处理程序中,你可以通过dev_id访问到这些信息,进而处理相应的GPIO中断事件。

request_irq()函数:
request_irq()函数的作用是请求一个中断线,并为其指定一个中断服务程序(ISR)来处理该中断。它有几个关键的行为:

  • 分配中断处理程序:在该中断号触发时,内核会调用指定的gpio_key_isr函数。
  • 设置中断触发条件:在本例中,使用了IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING来指定上升沿和下降沿触发中断。
  • 与中断源关联:通过dev_id(这里是&gpio_keys_100ask[i])将中断与特定的GPIO引脚和其他上下文数据关联。

总结:
这行代码的作用是请求一个GPIO引脚的中断,并指定触发条件为上升沿和下降沿(即按键按下或松开)。它将gpio_key_isr函数作为中断服务程序,当该GPIO引脚的电平变化时,内核会调用该函数来处理中断。&gpio_keys_100ask[i]作为dev_id被传递给ISR,以便在中断发生时能够访问与该GPIO引脚相关的数据。

中断处理函数gpio_key_isr()

函数gpio_key_isr()的完整源码

static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    struct gpio_key *gpio_key = dev_id;
    int val;

    // 返回引脚电平的逻辑值,注意:如果是低电平有效,则当物理电平为低电平时,其返回值为1;则当物理电平为高电平时,其返回值为0.
	// 如果要得到物理电平值,可以用函数gpiod_get_raw_value()得到
    val = gpiod_get_value(gpio_key->gpiod);

    // 打印中断号、GPIO引脚编号和电平值
    printk("Interrupt number: %d; GPIO pin number: %d; Pin Logical value: %d\n", irq, gpio_key->gpio, val);

    return IRQ_HANDLED;  // 表示中断已处理
}

输入参数irqdev_id分析

static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
.......
}

在中断处理函数gpio_key_isr中,输入参数irqdev_id有特定的含义和用途。下面是详细分析:

  1. irq参数:
  • 类型: int irq
  • 含义: 这个参数表示触发中断的中断请求号(IRQ)。它指定了发生中断的具体硬件中断源。在内核中,每个硬件设备的中断都有一个唯一的IRQ编号,这个编号由硬件平台分配。
  • 作用: irq参数通常用于处理中断时确认是哪一个硬件中断触发了此处理函数。在一些情况下,中断处理程序可能会处理多个设备的中断,因此通过irq参数可以判断是哪一个设备的中断发生,从而做出不同的响应。
  1. dev_id参数:
  • 类型: void *dev_id

  • 含义: dev_id是一个指向设备特定数据的指针,由request_irq()函数提供。它允许将设备的上下文数据传递给中断处理程序。

  • 作用:gpio_key_isr函数中,dev_id指向函数gpio_key_probe()中进行具体赋值的全局数组gpio_keys_100ask[i],这是一个结构体指针,包含了与该GPIO引脚相关的信息(如GPIO编号、GPIO描述符、IRQ号等)。

    通过dev_id,中断服务程序能够访问到与触发此中断相关的设备上下文数据。在本例中,dev_id用于访问具体的GPIO配置和状态信息,允许ISR(中断服务程序)获取GPIO引脚的电平状态并进行处理。

示例:
gpio_key_isr中,代码通常会通过dev_id参数访问设备的具体数据,如下所示:

static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    struct gpio_key *gpio_key = dev_id;  // 将dev_id转换为gpio_key指针
    int val;

    val = gpiod_get_value(gpio_key->gpiod);  // 获取GPIO的逻辑电平值(如果是低电平有效,则会进行逻辑反转)

    printk("key %d %d\n", gpio_key->gpio, val);  // 打印GPIO编号和电平值

    return IRQ_HANDLED;  // 表示中断已处理
}
  • 在这个例子中,dev_id指向gpio_keys_100ask[i],因此可以通过它访问gpio_key->gpiod(GPIO描述符)以及gpio_key->gpio(GPIO编号)。这使得中断处理函数可以根据不同的GPIO引脚触发不同的响应。

总结:

  • irq:表示中断号,用于标识发生中断的硬件设备或GPIO引脚。
  • dev_id:指向设备特定数据的指针,通常是通过request_irq()传递的。在本例中,dev_id指向与触发该中断的GPIO相关的结构体,允许中断服务程序访问GPIO的配置和状态信息。

上段这段话读完再结合代码注释,应该很好理解函数gpio_key_isr()了,所以这里就不再详细叙述了。

函数gpio_key_remove()

static int gpio_key_remove(struct platform_device *pdev)
{
    struct device_node *node = pdev->dev.of_node;
    int count;
    int i;

    count = of_gpio_count(node);
    for (i = 0; i < count; i++) 
	{
        // 只有在irq有效时才释放中断资源
        if (gpio_keys_100ask[i].irq >= 0) {
            // 释放GPIO中断资源,下面这句代码做了下面两件事:
			// 1、解除 `gpio_keys_100ask[i].irq` 中断号和 `gpio_key_isr` 中断处理函数的绑定。
			// 2、解除 `gpio_keys_100ask[i].irq` 中断号和中断处理函数与 `gpio_keys_100ask[i]` 数据结构的绑定。
            free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
        }

        // 释放GPIO描述符
        if (gpio_keys_100ask[i].gpiod) {
            gpiod_put(gpio_keys_100ask[i].gpiod);
        }
    }

    // 释放内存
    kfree(gpio_keys_100ask);
    return 0;
}

这个代码没什么好说的,注释中已经说得很清楚了,这里要说明的就是为什么通过函数devm_gpio_request_one()实现的&pdev->dev与GPIO硬件资源的绑定操作不需要解除绑定?
因为:这个绑定操作会在调用函数platform_driver_unregister()注销platform_driver时自动由内核解除绑定操作,所以gpio_key_remove函数中不需要显示去解除绑定。由devm开头的函数通常都会内核自动管理资源,咱们在退出函数中不用人为的去释放资源或解除绑定。

代码总结(为什么没有设备号、设备类、设备文件等的分配和创建?)

驱动程序的分类

在 Linux 驱动开发中,常见的驱动程序可以分为下面两类:

  1. 字符设备驱动:需要分配设备号、创建设备文件、设备类等,用来处理用户空间与内核空间之间的交互。比如,设备驱动涉及对硬件设备的操作,如文件操作、I/O控制等,通常包括设备号的分配、openreadwrite等系统调用的实现。

  2. 中断驱动程序:专注于处理硬件中断请求,通常不涉及设备文件的创建或与用户空间的交互。像这篇博文的gpio_key_drv.c,它是一个典型的中断处理驱动,只关注通过GPIO引脚接收中断信号并进行处理。

当然,根据设备的类型可以将驱动程序分为很多类,比如字符设备驱动、块设备驱动、网络设备驱动、总线驱动等,这里为了说明问题,将常见的驱动程序分为字符设备驱动和中断驱动程序。

这里的重点是:中断驱动程序往往不需要设备文件的创建或与用户空间的交互,所以没有分配设备号、创建设备文件、设备类等操作。

和自己之前博文中写的字符设备驱动有什么主要区别?

  1. 没有设备号分配:在字符设备驱动中,会为驱动程序分配设备号,允许用户空间访问该设备。但是在gpio_key_drv.c中,并没有进行设备号的分配或用户空间与驱动的交互。

  2. 没有设备文件创建:字符设备驱动通常会通过mknod()或者通过sysfs接口创建设备文件,用户空间可以通过文件操作来与驱动交互。然而,在这段代码中,所有的交互都是通过硬件中断处理,而不是通过设备文件。

  3. 没有设备类:在字符设备驱动中,通常还会创建一个设备类,并通过sysfsdevtmpfs来管理设备节点,允许用户空间程序通过标准文件操作来访问设备。在这里,驱动只是通过platform_driver绑定到硬件设备,没有涉及设备文件或设备类的创建

这段代码实现了什么?

这段代码的核心功能实际上是一个GPIO中断驱动,它利用platform驱动机制来处理GPIO引脚的中断信号:

  • 通过设备树(devicetree)配置获取GPIO的引脚信息。
  • 将这些GPIO引脚注册为中断源,并设置中断处理程序(gpio_key_isr)。
  • 当GPIO引脚的电平变化时,内核会触发中断,调用中断服务程序进行处理。

这种类型的驱动程序适用于不需要通过用户空间文件接口交互的硬件设备,例如传感器按键等硬件设备的中断处理。你并不需要让用户空间直接与设备交互,而是依赖内核通过中断响应来进行数据处理。

设备树文件的编写

查看实物图和原理图确认用哪两个GPIO口

查看硬件实物图,我们要用的是KEY1和KEY2:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
所以我们要使用的是GPIO5_IO01GPIO4_IO14。明白这个后就可以开始写设备树文件了。

修改设备树描述文件的准备工作

把Linux内核源码中因为上个博文(https://blog.youkuaiyun.com/wenhao_ir/article/details/145119224)而修改的dts和dtb文件删掉。

设备树文件存放于下面这个目录中:

/home/book/100ask_imx6ull-sdk/Linux-4.9.88/arch/arm/boot/dts

在这里插入图片描述

把备份的未经修改的dts文件100ask_imx6ull-14x14 (copy).dts复制到Windows的工程中,并重命为100ask_imx6ull-14x14.dts

在这里插入图片描述
然后就可以在VScode中打开它并进行修改了。

添加设备树节点gpio_irq_node

根据上面的分析,我们要操作设备的GPIO口为GPIO5_IO01GPIO4_IO14,所以书写下面的设备树节点:

    gpio_irq_node {
        compatible = "swh-gpio_irq_key";
		gpios = <&gpio5 1 GPIO_ACTIVE_LOW
		 &gpio4 14 GPIO_ACTIVE_LOW>;
		
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_key1 &pinctrl_key2>;
    };

这里面的compatible属性的值对应于下图中的值:
在这里插入图片描述

对于 pinctrl-0里的引用结构&pinctrl_key1 &pinctrl_key2我们还需要去用IMX6ULL设备树语句生成工具Pins_Tool去生成。

另外,节点gpio-keys也用到了GPIO5_IO01GPIO4_IO14,所以我们需要把这个设备节点禁用:

 status = "disabled";

在这里插入图片描述
对于 pinctrl-0里的引用结构&pinctrl_key1 &pinctrl_key2我们还需要去用IMX6ULL设备树语句生成工具Pins_Tool去生成。

pinctrl-0里的引用结构&pinctrl_key1&pinctrl_key2的完善

博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145119224 里有IMX6ULL设备树语句生成工具Pins_Too的安装和使用方法。

这里参考博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145119224 来书写引用结构&pinctrl_key1&pinctrl_key2

打开IMX6ULL设备树语句生成工具Pins_Tool,勾选GPIO5_IO01
在这里插入图片描述
得到下面的关键信息:

&iomuxc_snvs {
    pinctrl-names = "default_snvs";
    imx6ull-board {
    pinctrl-0 = <&BOARD_InitPinsSnvs>;
        BOARD_InitPinsSnvs: BOARD_InitPinsSnvsGrp {        /*!< Function assigned for the core: Cortex-A7[ca7] */
            fsl,pins = <
                MX6ULL_PAD_SNVS_TAMPER0__GPIO5_IO00        0x000110A0
            >;
        };
    };
};

其中的关键语句如下:

        BOARD_InitPinsSnvs: BOARD_InitPinsSnvsGrp {        /*!< Function assigned for the core: Cortex-A7[ca7] */
            fsl,pins = <
                MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01        0x000110A0
            >;
        };

这段语句实际上就是设置寄存器MX6ULL_PAD_SNVS_TAMPER1的值为0x000110A0,将寄存器MX6ULL_PAD_SNVS_TAMPER1的值配置为0x000110A0就可以实现将<引脚编号为R9,引脚名称为SNVS_TAMPER1>的引脚路由至外设GPIO5的信号gpio_io, 1上了。即这样配置后,引脚编号为R9,引脚名为SNVS_TAMPER1的引脚就当成GPIO5_IO1来使用了。

然后我们根据之前的经验修改标签名和节点为:

        pinctrl_key1: key1_gpio {
            fsl,pins = <
                MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01        0x000110A0
            >;
        };

因为工具生成的语句是在引用结构&iomuxc_snvs中,所以我们也需要加入到引用结构&iomuxc_snvs的下面这个位置:
在这里插入图片描述
同样再去勾选GPIO4_IO14
在这里插入图片描述
得到下面的关键信息:

&iomuxc {
    pinctrl-names = "default";
    pinctrl-0 = <&BOARD_InitPins>;
    imx6ull-board {
        BOARD_InitPins: BOARD_InitPinsGrp {                /*!< Function assigned for the core: Cortex-A7[ca7] */
            fsl,pins = <
                MX6UL_PAD_NAND_CE1_B__GPIO4_IO14           0x000010B0
            >;
        };
    };
};

其中的关键语句:

        BOARD_InitPins: BOARD_InitPinsGrp {                /*!< Function assigned for the core: Cortex-A7[ca7] */
            fsl,pins = <
                MX6UL_PAD_NAND_CE1_B__GPIO4_IO14           0x000010B0
            >;
        };

这段语句实际上就是设置寄存器MX6UL_PAD_NAND_CE1_B的值为0x000010B0,这个值实际上就是把GPIO4_IO14设置为GPIO的功能而不是别的复用功能。

这段语句实际上就是设置寄存器MX6UL_PAD_NAND_CE1_B的值为0x000010B0,将寄存器MX6UL_PAD_NAND_CE1_B的值配置为0x000010B0就可以实现将<引脚编号为B5,引脚名称为NAND_CE1_B>的引脚路由至外设GPIO4的信号gpio_io, 14上了。即这样配置后,引脚编号为B5,引脚名为NAND_CE1_B的引脚就当成GPIO4_IO14来使用了。

然后修改标签名和节点:

        pinctrl_key2: key2_gpio {
            fsl,pins = <
                MX6UL_PAD_NAND_CE1_B__GPIO4_IO14           0x000010B0
            >;
        };

因为工具生成的语句是在引用结构&iomuxc中【注意:这里不再是引用结构&iomuxc_snvs】,所以我们也需要加入到引用结构&iomuxc的下面这个位置:
在这里插入图片描述

至此设备树文件就修改完了。

编译设备树文件

将上面改好的设备文件复制到源码的下面目录中:

/home/book/100ask_imx6ull-sdk/Linux-4.9.88/arch/arm/boot/dts

在这里插入图片描述
然后编译设备树文件

cd /home/book/100ask_imx6ull-sdk/Linux-4.9.88
make dtbs

生成了设备树文件
在这里插入图片描述
把它复制到NFS网络文件系统的目录中,备用。

更新开发板上的设备树文件(dtb文件)

打开串口终端→打开开发板→挂载网络文件系统

mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt

把生成的dtb文件复制到/boot目录。

cp /mnt/simple_key_IRQ/100ask_imx6ull-14x14.dtb /boot/

重启开发板~

reboot

查看下相关的设备树节点存在没有:

ls /sys/firmware/devicetree/base/

有了,如下图所示:
在这里插入图片描述
名字来源:
在这里插入图片描述

Makfile文件内容

# 使用不同的Linux内核时, 一定要修改KERN_DIR,KERN_DIR代表已经配置、编译好的Linux源码的根目录

KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88

all:
	make -C $(KERN_DIR) M=`pwd` modules 

clean:
	make -C $(KERN_DIR) M=`pwd` clean
	rm -rf modules.order

obj-m += gpio_key_drv.o

交叉编译出驱动程序模块

工程文件复制到Ubuntu中…
在这里插入图片描述

make

然后把生成的ko文件复制到网络文件中,备用~
在这里插入图片描述

加载中断驱动程序模块,并测试

挂载网络文件系统:

mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
insmod  /mnt/simple_key_IRQ/gpio_key_drv.ko

在这里插入图片描述

开启控制台输出printk的内容。

echo "7 4 1 7" > /proc/sys/kernel/printk

按下按键KE1:
在这里插入图片描述
输出结果如下:
在这里插入图片描述

[  202.675025] Interrupt number: 208; GPIO pin number: 129; Pin Logical value: 1
[  202.837652] Interrupt number: 208; GPIO pin number: 129; Pin Logical value: 0

原理图如下:
在这里插入图片描述
结合原理图分析如下:
按下按键时,产生下降沿,下降沿会触发中断,进而转入相应的中断服务程序中处理中断。
在按键按下时,物理电平是低电平,但因为这个引脚设备为低电平有效,所以输出的逻辑值为1,而不是0。
在按键弹起时,物理电平是高电平,但因为这个引脚设备为低电平有效,所以输出的逻辑值为0,而不是1。

按下按键KEY2:
在这里插入图片描述
在这里插入图片描述

[  373.790953] Interrupt number: 189; GPIO pin number: 110; Pin Logical value: 1
[  373.938729] Interrupt number: 189; GPIO pin number: 110; Pin Logical value: 0
[  373.945996] Interrupt number: 189; GPIO pin number: 110; Pin Logical value: 0

在这里插入图片描述
结合原理图分析如下:
按下按键时,产生下降沿,下降沿会触发中断,进而转入相应的中断服务程序中处理中断。
在按键按下时,物理电平是低电平,但因为这个引脚设备为低电平有效,所以输出的逻辑值为1,而不是0。
在按键弹起时,物理电平是高电平,但因为这个引脚设备为低电平有效,所以输出的逻辑值为0,而不是1。
但是产生了两个上升沿中断,这多半是由于机械抖动产生的,可以通过硬件或软件的方式来去抖,分析如下:

KEY2产生按键抖动的原因分析及应对方法

先说一下,虽然我在后续的博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145281064 中用内核定时器在软件层面进行了按键去抖,但是这里其实是KEY2按键本身或其相关的电子元件出了问题,原因我在后面分析了。

问题原因

  1. 机械抖动:

    • 当按下或释放机械按键时,按键内部的接触点可能会多次短时间接触和断开。
    • 这会导致多个信号的上升或下降沿,从而触发多次中断。
  2. 硬件滤波不足:

    • 从你的原理图看,按键电路中有一个 100nF 的电容(C37 和 C38),用来抑制抖动信号。
    • 如果滤波电容容量不足或者电阻值选择不合适,可能导致抖动信号未被完全抑制。
  3. 软件层处理不足:

    • 如果驱动程序中没有实现有效的软件去抖动逻辑(例如,通过延时忽略短时间内的重复中断),抖动信号会直接被上报。

解决方案

1. 硬件滤波优化

  • 检查硬件电路的滤波器是否有效:
    • R49/R48 和 C37/C38 组成 RC 滤波器:
      • 其时间常数为 τ = R × C = 470   Ω × 100  nF = 47   μ s \tau = R \times C = 470\ \Omega \times 100\ \text{nF} = 47\ \mu s τ=R×C=470 Ω×100 nF=47 μs
      • 这个时间常数可能对按键抖动抑制不足(机械按键抖动通常持续 5-20ms)。
    • 优化建议:
      • 将滤波电容从 100nF 增大到 1μF(时间常数增大到 470μs),以更好地滤除按键抖动。
        备注:在后续的博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145281064 中,我们发现在软件去抖时至少要把延时增大到150毫秒才行,而正常情况下一般10毫秒就够了,这说明咱们的按键在硬件上是有问题的,要么是按键KEY2有问题,要么是相关的电子元件出现了问题,KEY1与KEY2在原理图上是一样的,但是KEY1是正常的,这也证明是KEY2按键本身或是与其相关的电子元件出现了问题。

2. 软件去抖动

  • 在驱动中加入软件去抖动逻辑:
    • 延时忽略: 当中断触发后,记录时间戳,忽略短时间内的后续中断(例如,10ms 内)。
    • 示例实现:
      static unsigned long last_interrupt_time = 0;
      
      irqreturn_t gpio_irq_handler(int irq, void *dev_id)
      {
          unsigned long flags;
          unsigned long interrupt_time = jiffies;
      
          // 防止重复触发
          if (interrupt_time - last_interrupt_time < msecs_to_jiffies(10)) {
              return IRQ_HANDLED;
          }
          last_interrupt_time = interrupt_time;
      
          // 读取 GPIO 值并处理
          spin_lock_irqsave(&lock, flags);
          int value = gpio_get_value(gpio_pin);
          printk(KERN_INFO "Interrupt number: %d; GPIO pin number: %d; Pin Logical value: %d\n",
                 irq, gpio_pin, value);
          spin_unlock_irqrestore(&lock, flags);
      
          return IRQ_HANDLED;
      }
      
    说明:上面的代码看起来很简单,但是为什么在后续的博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145281064 中我们用内核定时器去实现软件去抖呢?答:因为咱们是要处理多个按键,而不是1个按键,所以要为每个按键分别设一个定时器,所以要用内核定时器,因为内核定时器能为每个按键都单独设置一个定时器。

3. 硬件中断模式改进

  • 检查中断配置:
    • 如果当前使用的是 上升沿触发(IRQ_TYPE_EDGE_RISING),可以考虑切换为 电平触发(IRQ_TYPE_LEVEL_HIGH/LOW),这样可以避免多次触发的问题。
    • 需要注意的是,使用电平触发需要在中断处理程序中正确清除中断状态。

4. 确认 GPIO 配置

  • 确保在设备树或驱动程序中正确设置 GPIO 的中断类型。例如:
    gpio_keys {
        compatible = "gpio-keys";
        key1 {
            label = "KEY1";
            gpios = <&gpio4 14 GPIO_ACTIVE_LOW>;
            linux,code = <KEY_ENTER>;
            debounce-interval = <10>;
        };
    }
    
    • debounce-interval 是一些驱动中的内置去抖动功能,可以避免重复触发。

总结

  • 按键抖动是机械按键的典型问题,可以通过硬件滤波和软件去抖动结合解决。
  • 硬件滤波侧重于消除短时间内的信号毛刺,软件去抖动用于屏蔽多次触发。
  • 推荐优先优化硬件滤波电路,再加入简单的软件逻辑。如果问题持续,可以使用示波器进一步分析信号质量。

卸载中断驱动程序模块

rmmod gpio_key_drv.ko

在这里插入图片描述
没问题~

【测试完毕】

附完整工程文件

https://pan.baidu.com/s/1GjS_PX3pdbrhEm_DwWpP4Q?pwd=pnre

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

昊虹AI笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值