首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

GPIO按键驱动分析与使用:input_dev层

  • 25-03-04 14:01
  • 3756
  • 6390
blog.csdn.net

往期内容

本专栏往期内容:

  1. input子系统的框架和重要数据结构详解-CSDN博客
  2. input device和input handler的注册以及匹配过程解析-CSDN博客
  3. input device和input handler的注册以及匹配过程解析-CSDN博客
  4. 编写一个简单的Iinput_dev框架-CSDN博客

I2C子系统专栏:

  1. 专栏地址:IIC子系统_憧憬一下的博客-CSDN博客
  2. 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
    – 末篇,有往期内容观看顺序

总线和设备树专栏:

  1. 专栏地址:总线和设备树_憧憬一下的博客-CSDN博客
  2. 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
    – 末篇,有往期内容观看顺序

前言

  • Linux 4.x内核

    • Documentation\devicetree\bindings\input\gpio-keys.txt?gpio-keys.txt
    • drivers\input\keyboard\gpio_keys.c ?gpio_keys.c

设备树

  • IMX6ULL:Linux-4.9.88/arch/arm/boot/dts/100ask_imx6ull-14x14.dts

img
内核提供的input device下层的input_de硬件相关驱动,分析的是内核提供给的gpio_keys.c文件,也就是看看其是如何去实现input_dev层的驱动程序的编写的。

1. 驱动程序框架

img

2. 设备树示例

gpio-keys {
    compatible = "gpio-keys";
    pinctrl-names = "default";

    user1 {
        label = "User1 Button";
        gpios = <&gpio5 1 GPIO_ACTIVE_LOW>; 
        # 用GPIO来描述引脚,可以在驱动程序中根据编号来获取注册中断,可以根据电平来判断按键是按下还是松开
        #它是用gpiod_to_irq,传入gpio编号获得中断号的
        # 使用这种方式使用gpio_isr中断处理函数
        gpio-key,wakeup;
        linux,code = <KEY_1>;  // 具体产生事件的按键, keybit
    };

    user2 {
        label = "User2 Button";
        gpios = <&gpio4 14 GPIO_ACTIVE_LOW>;
        gpio-key,wakeup;
        linux,code = <KEY_2>;
    };
};
gpio-keys@0 {
				compatible = "gpio-keys";
				pinctrl-names = "default";
				pinctrl-0 = <&pinctrl_gpio_keys>;
				status = "okay";

				Key0{
						label = "Key 0";
						gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>;  # 有效电平
						linux,code = <KEY_1>;
				};
};

gpio-keys@1 {
				compatible = "gpio-keys";
				pinctrl-names = "default";
				pinctrl-0 = <&pinctrl_gpio_key1>;
				status = "okay";

				Key0{
						label = "Key 1";
						gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;
						linux,code = <KEY_2>;
				};
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

3. gpio_keys.c驱动程序分析

  • Linux 4.x内核

    • Documentation\devicetree\bindings\input\gpio-keys.txt?gpio-keys.txt
    • drivers\input\keyboard\gpio_keys.c?gpio_keys.c

3.1 套路

  • 根据设备树获得硬件信息:哪个GPIO、对于什么按键

  • 分配/设置/注册input_dev结构体

  • request_irq: 在中断处理函数中确定按键值、上报按键值

    • 有两种IRQ函数
    • gpio_keys_gpio_isr:设备树中的用gpios来描述用到的引脚
    • gpio_keys_irq_isr:设备树中的用interrupts来描述用到的引脚

3.2 probe函数

img

在之前讲解input_dev下层框架编写的时候有提到过,在这里进一步讲解。

static int gpio_keys_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;  // 获取设备结构体
	const struct gpio_keys_platform_data *pdata = dev_get_platdata(dev);  // 从平台设备获取平台数据
	struct gpio_keys_drvdata *ddata;  // 驱动私有数据结构
	struct input_dev *input;  // 输入设备结构体
	size_t size;  // 计算需要分配的内存大小
	int i, error;  // 循环计数器和错误码
	int wakeup = 0;  // 标志位,指示是否支持唤醒功能

	// 如果 pdata 为 NULL,则从设备树获取平台数据
	if (!pdata) {
		pdata = gpio_keys_get_devtree_pdata(dev);
		if (IS_ERR(pdata))
			return PTR_ERR(pdata);  // 返回错误码
	}

	// 计算 gpio_keys_drvdata 和按钮数据结构的大小
	size = sizeof(struct gpio_keys_drvdata) +
			pdata->nbuttons * sizeof(struct gpio_button_data);
	// 分配驱动私有数据内存
	ddata = devm_kzalloc(dev, size, GFP_KERNEL);
	if (!ddata) {
		dev_err(dev, "failed to allocate state\n");  // 分配失败,输出错误信息
		return -ENOMEM;  // 返回内存不足错误码
	}

	// 分配输入设备
	input = devm_input_allocate_device(dev);
	if (!input) {
		dev_err(dev, "failed to allocate input device\n");  // 分配失败,输出错误信息
		return -ENOMEM;  // 返回内存不足错误码
	}

	ddata->pdata = pdata;  // 保存平台数据指针
	ddata->input = input;  // 保存输入设备指针
	mutex_init(&ddata->disable_lock);  // 初始化互斥锁

	// 将驱动数据指针与平台设备相关联
	platform_set_drvdata(pdev, ddata);
	input_set_drvdata(input, ddata);  // 将驱动数据与输入设备关联

	// 设置输入设备名称和物理路径
	input->name = pdata->name ? : pdev->name;  // 如果 pdata 中有名称则使用,否则使用平台设备名称
	input->phys = "gpio-keys/input0";  // 设置物理路径
	input->dev.parent = &pdev->dev;  // 设置设备的父设备
	input->open = gpio_keys_open;  // 设置打开设备的函数
	input->close = gpio_keys_close;  // 设置关闭设备的函数

	// 设置输入设备的 ID
	input->id.bustype = BUS_HOST;  // 设置总线类型
	input->id.vendor = 0x0001;  // 设置厂商 ID
	input->id.product = 0x0001;  // 设置产品 ID
	input->id.version = 0x0100;  // 设置版本号

	// 启用 Linux 输入子系统的自动重复功能
	if (pdata->rep)
		__set_bit(EV_REP, input->evbit);  // 设置 EV_REP 事件位

	// 遍历每个按钮并设置
	for (i = 0; i < pdata->nbuttons; i++) {
		const struct gpio_keys_button *button = &pdata->buttons[i];  // 获取当前按钮信息
		struct gpio_button_data *bdata = &ddata->data[i];  // 获取按钮数据

		error = gpio_keys_setup_key(pdev, input, bdata, button);  // 设置按键,里面包括设置了中断函数
		//--------(1)
        if (error)
			return error;  // 返回错误码

		if (button->wakeup)  // 如果按钮支持唤醒功能
			wakeup = 1;  // 设置唤醒标志
	}

	// 创建 sysfs 组,用于导出按键和开关
	error = sysfs_create_group(&pdev->dev.kobj, &gpio_keys_attr_group);
	if (error) {
		dev_err(dev, "Unable to export keys/switches, error: %d\n", error);  // 输出错误信息
		return error;  // 返回错误码
	}

	// 注册输入设备
	error = input_register_device(input);
	if (error) {
		dev_err(dev, "Unable to register input device, error: %d\n", error);  // 输出错误信息
		goto err_remove_group;  // 错误处理,移除 sysfs 组
	}

	// 初始化唤醒设备功能
	device_init_wakeup(&pdev->dev, wakeup);

	return 0;  // 返回成功

err_remove_group:
	// 在错误情况下移除 sysfs 组
	sysfs_remove_group(&pdev->dev.kobj, &gpio_keys_attr_group);
	return error;  // 返回错误码
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97

其中error = gpio_keys_setup_key(pdev, input, bdata, button);:

static int gpio_keys_setup_key(struct platform_device *pdev,
				struct input_dev *input,
				struct gpio_button_data *bdata,
				const struct gpio_keys_button *button)
{
	const char *desc = button->desc ? button->desc : "gpio_keys";  // 设置按钮描述
	struct device *dev = &pdev->dev;  // 获取设备结构体
	irq_handler_t isr;  // 中断处理程序
	unsigned long irqflags;  // 中断标志
	int irq;  // 中断号
	int error;  // 错误码

	bdata->input = input;  // 保存输入设备指针
	bdata->button = button;  // 保存按钮数据指针
	spin_lock_init(&bdata->lock);  // 初始化自旋锁

	/*
	 * 处理传统的 GPIO 编号,获取 GPIO 资源并
	 * 转换为 GPIO 描述符。
	 */
	if (gpio_is_valid(button->gpio)) {  // 检查 GPIO 是否有效
		unsigned flags = GPIOF_IN;  // 设置 GPIO 为输入模式

		if (button->active_low)  // 如果按钮是低电平有效
			flags |= GPIOF_ACTIVE_LOW;  // 设置活动低标志

		// 请求 GPIO 资源
		error = devm_gpio_request_one(&pdev->dev, button->gpio, flags, desc);
		if (error < 0) {  // 请求失败
			dev_err(dev, "Failed to request GPIO %d, error %d\n",
				button->gpio, error);
			return error;  // 返回错误码
		}

		// 将 GPIO 转换为 GPIO 描述符
		bdata->gpiod = gpio_to_desc(button->gpio);
		if (!bdata->gpiod)  // 如果转换失败
			return -EINVAL;  // 返回无效参数错误

		// 设置防抖动间隔
		if (button->debounce_interval) {
			error = gpiod_set_debounce(bdata->gpiod,
					button->debounce_interval * 1000);
			/* 如果 gpiolib 不支持防抖动,则使用定时器 */
			if (error < 0)
				bdata->software_debounce = button->debounce_interval;  // 使用软件防抖动
		}

		// 设置中断号
		if (button->irq) {
			bdata->irq = button->irq;  // 如果已指定 IRQ,直接使用
		} else {
			// 获取 GPIO 的中断号
			irq = gpiod_to_irq(bdata->gpiod);
			if (irq < 0) {  // 如果获取失败
				error = irq;  // 保存错误码
				dev_err(dev,
					"Unable to get irq number for GPIO %d, error %d\n",
					button->gpio, error);
				return error;  // 返回错误码
			}
			bdata->irq = irq;  // 保存中断号
		}

		// 初始化延迟工作队列
		INIT_DELAYED_WORK(&bdata->work, gpio_keys_gpio_work_func);

		isr = gpio_keys_gpio_isr;  // 设置中断处理程序
		irqflags = IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING;  // 设置中断触发方式

	} else {  // 如果 GPIO 无效
		if (!button->irq) {  // 如果没有指定 IRQ
			dev_err(dev, "No IRQ specified\n");
			return -EINVAL;  // 返回无效参数错误
		}
		bdata->irq = button->irq;  // 使用指定的 IRQ

		if (button->type && button->type != EV_KEY) {  // 检查按钮类型
			dev_err(dev, "Only EV_KEY allowed for IRQ buttons.\n");
			return -EINVAL;  // 返回无效参数错误
		}

		bdata->release_delay = button->debounce_interval;  // 设置释放延迟
		setup_timer(&bdata->release_timer,
			    gpio_keys_irq_timer, (unsigned long)bdata);  // 设置定时器

		isr = gpio_keys_irq_isr;  // 设置中断处理程序
		irqflags = 0;  // 没有中断标志
	}

	// 设置输入设备的能力
	input_set_capability(input, button->type ?: EV_KEY, button->code);

	/*
	 * 安装自定义操作以取消释放定时器和
	 * 工作队列项。
	 */
	error = devm_add_action(&pdev->dev, gpio_keys_quiesce_key, bdata);
	if (error) {
		dev_err(&pdev->dev,
			"failed to register quiesce action, error: %d\n",
			error);
		return error;  // 返回错误码
	}

	/*
	 * 如果平台指定按钮可以禁用,
	 * 我们不希望它共享中断线。
	 */
	if (!button->can_disable)
		irqflags |= IRQF_SHARED;  // 设置共享标志

	// 请求、注册中断
	error = devm_request_any_context_irq(&pdev->dev, bdata->irq,
					     isr, irqflags, desc, bdata);
	if (error < 0) {  // 请求失败
		dev_err(dev, "Unable to claim irq %d; error %d\n",
			bdata->irq, error);
		return error;  // 返回错误码
	}

	return 0;  // 返回成功
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123

其中设置中断处理函数主要是分两种方法:

  • 一种是使用传统的GPIO编号来请、设置中断,其采用的中断处理函数是gpio_keys_gpio_isr

    • 如果 button->gpio 是有效的 GPIO 编号,代码会使用 devm_gpio_request_one 来请求 GPIO 并转换为 GPIO 描述符。
    • 接下来,它通过 gpiod_to_irq 获取 GPIO 的中断号,通常采用 GPIO 的上升沿和下降沿触发来处理中断,这种方式的中断处理函数是 gpio_keys_gpio_isr。
  • 另一种则是使用中断控制器的方式来设置GPIO引脚的中断处理函数gpio_keys_irq_isr

    • 如果不使用传统的 GPIO 编号,且提供了中断号(button->irq),代码会直接使用这个中断号,而不是通过 GPIO 获取中断号。
    • 在这种情况下,按钮类型必须是 EV_KEY,并且代码会将 gpio_keys_irq_isr 设置为中断处理函数。

这两种方式的中断处理函数另开小点解析,看下文

3.3 gpio_keys_gpio_isr分析

img

理想状况是:按下、松开按键,各产生一次中断,也只产生一次中断。但是对于机械开关,它的金属弹片会反复震动。GPIO电平会反复变化,最后才稳定。一般是几十毫秒才会稳定。如果不处理抖动的话,用户只操作一次按键,会发生多次中断,驱动程序可能会上报多个数据。

img

怎么处理按键抖动?

  • 在按键中断程序中,可以循环判断几十亳秒,发现电平稳定之后再上报
  • 使用定时器

显然第1种方法太耗时,违背“中断要尽快处理”的原则,你的系统会很卡。

怎么使用定时器?看下图:

img

核心在于:在GPIO中断中并不立刻记录按键值,而是修改定时器超时时间,10ms后再处理。如果10ms内又发生了GPIO中断,那就认为是抖动,这时再次修改超时时间为10ms。只有10ms之内再无GPIO中断发生,那么定时器的函数才会被调用。在定时器函数中上报按键值。

接下来看看函数:

static irqreturn_t gpio_keys_gpio_isr(int irq, void *dev_id)
{
    struct gpio_button_data *bdata = dev_id;  // 将传入的设备 ID 转换为 gpio_button_data 结构体指针

    BUG_ON(irq != bdata->irq);  // 如果中断号不匹配,则触发内核故障

    // 如果按钮支持唤醒功能,则保持设备唤醒
    if (bdata->button->wakeup)
        pm_stay_awake(bdata->input->dev.parent);

    // 在延迟工作队列中安排工作,以处理按钮按下事件
    mod_delayed_work(system_wq,
                     &bdata->work,
                     msecs_to_jiffies(bdata->software_debounce));  // 使用软件去抖动时间
    //函数mod_delayed_work --------(1)
    //留意一下函数mod_delayed_work的参数bdata->work函数 -----(2)

    return IRQ_HANDLED;  // 表示中断已被处理
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

(1)mod_delayed_work:

\Linux-4.9.88\kernel\workqueue.c
static inline bool mod_delayed_work(struct workqueue_struct *wq,
				    struct delayed_work *dwork,
				    unsigned long delay)
{
	return mod_delayed_work_on(WORK_CPU_UNBOUND, wq, dwork, delay);
}


//mod_delayed_work_on如下:
bool mod_delayed_work_on(int cpu, struct workqueue_struct *wq,
                         struct delayed_work *dwork, unsigned long delay)
{
    unsigned long flags;  // 用于保存中断状态的变量
    int ret;              // 用于保存返回值的变量

    do {
        // 尝试获取 pending 状态(即检查该工作是否已经被排队)
        // 如果工作正在处理,ret 将为 -EAGAIN
        ret = try_to_grab_pending(&dwork->work, true, &flags);
    } while (unlikely(ret == -EAGAIN));  // 如果返回 -EAGAIN,继续尝试

    if (likely(ret >= 0)) {  // 如果成功获取 pending 状态
        __queue_delayed_work(cpu, wq, dwork, delay);  // 将延迟工作加入指定的工作队列
        local_irq_restore(flags);  // 恢复之前保存的中断状态
    }

    /* -ENOENT from try_to_grab_pending() becomes %true */
    return ret;  // 返回 ret 的值,-ENOENT 将被视为 true(表示工作未找到)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 其中__queue_delayed_work(cpu, wq, dwork, delay);如下:
static void __queue_delayed_work(int cpu, struct workqueue_struct *wq,
                                  struct delayed_work *dwork, unsigned long delay)
{
    struct timer_list *timer = &dwork->timer; // 获取定时器
    struct work_struct *work = &dwork->work;   // 获取工作结构体

    // 检查工作队列、定时器的函数指针和数据、定时器是否正在使用以及工作结构体是否为空
    WARN_ON_ONCE(!wq); // 确保工作队列存在
    WARN_ON_ONCE(timer->function != delayed_work_timer_fn ||
                 timer->data != (unsigned long)dwork); // 确保定时器正确设置
    WARN_ON_ONCE(timer_pending(timer)); // 确保定时器没有正在运行
    WARN_ON_ONCE(!list_empty(&work->entry)); // 确保工作结构体未在其他列表中

    /*
     * 如果 @delay 为 0,则立即将 @dwork->work 入队。这是为了优化和正确性。
     * 定时器最早的到期时间是下一个时钟滴答,因此当 @delay 为 0 时,用户依赖于
     * 不存在延迟。
     */
    if (!delay) {
        __queue_work(cpu, wq, &dwork->work); // 直接入队工作
        return; // 退出函数
    }

    // 设置定时器的启动信息
    timer_stats_timer_set_start_info(&dwork->timer);

    // 设置工作队列和 CPU
    dwork->wq = wq;
    dwork->cpu = cpu;

    // 计算定时器到期时间
    timer->expires = jiffies + delay; // 将定时器的到期时间设定为当前时间加上延迟

    // 如果指定的 CPU 不为 WORK_CPU_UNBOUND,使用该 CPU 添加定时器
    if (unlikely(cpu != WORK_CPU_UNBOUND))
        add_timer_on(timer, cpu); // 在指定的 CPU 上添加定时器
    else
        add_timer(timer); // 否则在全局上下文中添加定时器
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 大概就是汇聚中断一起处理,因为按键可能会产生抖动,也就是会有多个中断连续发送,所以需要汇聚起来再去判断,通过add_timer / add_timer_on添加定时器,没中断了,超时了就会去调用timer->function,也就是之前在probe() --->gpio_keys_setup_key()时对data->work->timer->function所设置的,具体看下面

(2)来看一下mod_delayed_work函数的参数&bdata->work:

  • probe函数中的gpio_keys_setup_key函数中有设置:NIT_DELAYED_WORK(&bdata->work, gpio_keys_gpio_work_func);就是将bdata->work->timer->function设置为gpio_keys_gpio_work_func函数:
static void gpio_keys_gpio_work_func(struct work_struct *work)
{
    // 使用 container_of 宏获取包含该工作结构的 gpio_button_data 结构指针
    struct gpio_button_data *bdata =
        container_of(work, struct gpio_button_data, work.work);

    // 调用函数报告 GPIO 按钮事件
    gpio_keys_gpio_report_event(bdata);

    // 如果按钮被设置为唤醒,调用 pm_relax 以允许系统休眠
    if (bdata->button->wakeup)
        pm_relax(bdata->input->dev.parent);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

汇聚中断:为了避免在抖动期间产生过多的中断和不必要的输入事件,内核通过工作队列(work queue)机制来汇聚中断事件。即在中断处理程序中,通常只会安排一个工作项来稍后处理,而不是立即进行事件处理。

延迟工作:gpio_keys_gpio_work_func 函数的执行是在中断上下文之外的工作队列上下文中进行的。这样,工作函数可以在一定的延迟后统一处理按钮的状态,而不是逐个处理中断。这种做法可以有效减少由于抖动引起的错误输入。

工作流程

  1. 中断触发:当 GPIO 按钮被按下或松开时,会触发中断。
  2. 中断处理程序:在中断处理程序中,调用 mod_delayed_work 函数,将按钮的工作项放入工作队列中,并设置一个去抖动的延迟时间(如 bdata->software_debounce)。
  3. 汇聚与判断:工作队列中的 gpio_keys_gpio_work_func 函数会在指定的延迟后被执行。在此函数中,按钮的状态会被检查和更新,确保只有最终的有效状态会被报告。

3.4 gpio_keys_irq_isr分析

img

有个变量key_pressed,用来表示当前按键状态:初始值是false,表示按键没有被按下。

  • 发生中断

    • 上报"按下的值":input_event(input, EV_KEY, button->code, 1); input_sync(input);
    • 如果不延迟(!bdata->release_delay)
    • 马上上报"松开的值":input_event(input, EV_KEY, button->code, 0); input_sync(input);
    • 如果延迟(bdata->release_delay)
    • 启动定时器,过若干毫秒再上报"松开的值"
  • 所以,使用gpio_keys_irq_isr时,一次中断就会导致上报2个事件:按下、松开

  • 缺点:无法准确判断一个按键确实已经被松开了

static irqreturn_t gpio_keys_irq_isr(int irq, void *dev_id)
{
    // 从 dev_id 中获取按钮数据结构
    struct gpio_button_data *bdata = dev_id;
    const struct gpio_keys_button *button = bdata->button; // 获取按钮信息
    struct input_dev *input = bdata->input; // 获取输入设备
    unsigned long flags; // 用于保存中断标志

    // 确保触发的 IRQ 与当前按钮的 IRQ 匹配
    BUG_ON(irq != bdata->irq);

    // 加锁以保护共享数据,保存当前中断状态
    spin_lock_irqsave(&bdata->lock, flags);

    // 如果按键尚未被按下
    if (!bdata->key_pressed) {
        // 如果按钮被标记为可以唤醒设备
        if (bdata->button->wakeup)
            pm_wakeup_event(bdata->input->dev.parent, 0); // 触发设备唤醒

        // 发送按下事件
        input_event(input, EV_KEY, button->code, 1); // 1 表示按下
        input_sync(input); // 确保事件被发送

        // 如果没有设置释放延迟
        if (!bdata->release_delay) {
            // 立即发送松开事件
            input_event(input, EV_KEY, button->code, 0); // 0 表示松开
            input_sync(input); // 确保事件被发送
            goto out; // 跳转到解锁部分
        }

        // 如果设置了释放延迟,标记按键为已按下
        bdata->key_pressed = true;
    }

    // 如果设置了释放延迟,则重新设置释放定时器
    if (bdata->release_delay)
        mod_timer(&bdata->release_timer,
            jiffies + msecs_to_jiffies(bdata->release_delay)); // 设置定时器

out:
    // 解锁并恢复中断状态
    spin_unlock_irqrestore(&bdata->lock, flags);
    return IRQ_HANDLED; // 返回中断已处理
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

3.5 两函数对比

  1. 中断处理函数的基本概念
  • 中断处理函数是操作系统内核用来处理硬件中断信号的程序。当特定的事件发生(如按键被按下或松开),中断处理函数会被调用以响应这些事件。
  1. gpio_keys_gpio_isr 函数
  • 主要目标:处理来自 GPIO 按键的中断,并且利用定时器去抖动。

  • 按键状态管理:没有持久化按键状态(如 key_pressed)。

  • 中断触发:

    • 当 GPIO 按键被按下时,触发中断。
  • 处理中断:

    • 调用 mod_delayed_work 函数,将按键事件处理的工作项排入工作队列,设置延迟时间(如 bdata->software_debounce)。
    • 在这个延迟期间,如果再有中断触发,则认为是抖动,再次修改定时器超时时间。
  • 事件报告:

    • 在延迟工作函数(gpio_keys_gpio_work_func)中,将最终的按键状态(按下或松开)报告给输入子系统。
  • 优点:

    • 通过汇聚中断和使用定时器,可以有效处理按键抖动。
    • 减少了对系统的压力,因为不需要立即处理每次中断。
  • 缺点:

    • 可能会引入一些延迟,尤其是在高频率按键操作时。
  1. gpio_keys_irq_isr 函数
  • 主要目标:直接处理 GPIO 按键的按下和释放事件,同时管理按键状态(key_pressed)。

  • 中断触发:

    • 当 GPIO 按键被按下时,触发中断。
  • 处理中断:

    • 检查按键当前是否已经被标记为按下(key_pressed)。

    • 如果未按下,发送按下事件(input_event(input, EV_KEY, button->code, 1)),并同步输入设备。

    • 检查是否有释放延迟(release_delay):

      • 如果没有,立即发送松开事件(input_event(input, EV_KEY, button->code, 0))。
      • 如果有,启动释放定时器,以便稍后报告松开事件。
  • 按键状态管理:

    • 在按键被按下时,将 key_pressed 标志设置为 true,以防止多次报告按下事件。
  • 优点:

    • 可以立即报告按下和松开事件,适合于不需要去抖动的场景。
    • 更加直接,适用于简单的按键逻辑。
  • 缺点:

    • 无法准确判断一个按键是否真正松开,可能会导致误报。
    • 由于直接处理每个中断,可能会在按键抖动期间产生大量中断,增加了 CPU 的负担。
特性gpio_keys_gpio_isrgpio_keys_irq_isr
中断触发由 GPIO 引脚的状态变化触发由中断控制器或 GPIO 引脚的状态变化触发
事件处理将工作项排入工作队列,并使用定时器进行去抖动立即报告按下和松开事件,并管理按键状态
事件数量通常处理一次中断后只报告一个最终事件一次中断可能报告两个事件(按下和松开)
状态管理无状态管理,依赖工作队列中延迟处理使用 key_pressed 标志进行状态管理
去抖动机制使用定时器进行去抖动,汇聚中断直接在中断中处理,不具备去抖动机制
  • gpio_keys_gpio_isr 适用于需要去抖动和减少中断负担的场景,通过使用定时器和工作队列来延迟事件处理,避免由于抖动而产生的重复事件。
  • gpio_keys_irq_isr 更加直接,可能会在高频率按键操作中引入问题,因为它无法正确处理抖动并可能导致错误的输入报告。
文章知识点与官方知识档案匹配,可进一步学习相关知识
C技能树首页概览220410 人正在系统学习中
注:本文转载自blog.csdn.net的憧憬一下的文章"https://blog.csdn.net/caiji0169/article/details/143276165"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top