当前位置:首页>Linux>嵌入式Linux实战:GPIO 输入机制 - 配置和读取那些事儿

嵌入式Linux实战:GPIO 输入机制 - 配置和读取那些事儿

  • 2026-10-11 07:17:31
嵌入式Linux实战:GPIO 输入机制 - 配置和读取那些事儿

嵌入式Linux实战:GPIO 输入机制 - 配置和读取那些事儿

仓库已经开源!所有教程,主线内核移植,跑新版本imx-linux/uboot都在这里,或者一起来尝试跑7.0的Linux!欢迎各位大佬观摩!喜欢的话点个⭐!

仓库地址:https://github.com/Awesome-Embedded-Learning-Studio/imx-forge

静态网页:https://awesome-embedded-learning-studio.github.io/imx-forge/

上一章我们讲了为什么要先学轮询方式,现在我们深入到代码层面,看看 GPIO 输入到底是怎么工作的。

说实话,第一次写 GPIO 输入驱动的时候,我以为会很复杂。结果发现 Linux 的 GPIO 子系统把事情简化了不少,大部分复杂操作都被封装好了。

输入和输出的 API 对比

我们先回顾一下输出设备怎么用 GPIO:

/* 输出模式:LED/蜂鸣器 */
structgpio_desc *led = gpiod_get(dev, NULL, GPIOD_OUT_LOW);
gpiod_set_value(led, 1);  // 点亮 LED

再看输入模式:

/* 输入模式:按键 */
structgpio_desc *key = gpiod_get(dev, NULL, GPIOD_IN);
int state = gpiod_get_value(key);  // 读取按键状态

API 的命名很有规律:

  • gpiod_get()
     获取 GPIO 描述符
  • GPIOD_OUT_LOW
    /GPIOD_IN 指定方向
  • gpiod_set_value()
    /gpiod_get_value() 操作 GPIO

PS: 关于 Descriptor API,你可能听说过还有 Legacy API(gpio_request、gpio_direction_input 之类的)。那些是老接口,现在不推荐用了。Descriptor API 是新的标准,功能更强,也更安全。我们教程统一用 Descriptor API。

配置 GPIO 为输入

在我们的硬件抽象层里,初始化函数是这样的:

intkey_hw_init(struct device *dev, struct gpio_desc **gpio)
{
structgpio_desc *gpiod;

/* GPIOD_IN 表示配置为输入 */
    gpiod = gpiod_get(dev, NULL, GPIOD_IN);
if (IS_ERR(gpiod)) {
return PTR_ERR(gpiod);
    }

    *gpio = gpiod;
return0;
}

这个 gpiod_get() 做了几件事:

  1. 解析设备树
    ——从 gpios = <&gpio1 18 GPIO_ACTIVE_LOW> 提取信息
  2. 申请 GPIO
    ——防止被其他驱动占用
  3. 配置方向
    ——设置为输入模式
  4. 返回描述符
    ——后续操作用这个描述符

::: tip 错误处理要重视

gpiod_get() 可能失败,比如 GPIO 已经被占用,或者设备树配置错误。所以一定要检查返回值。

IS_ERR() 和 PTR_ERR() 是内核的错误处理模式。很多内核函数用指针返回结果,成功时返回有效指针,失败时返回错误码编码的"错误指针"。这个模式和普通的返回值判断不太一样,一开始用的时候容易搞混。 :::

读取 GPIO 状态

读取状态的代码也很简单:

intkey_get_state(struct gpio_desc *gpio)
{
int val;

    val = gpiod_get_value(gpio);

/* 返回 0=按下,1=松开 */
return !val;
}

等等,为什么要 !val 反转一下?这涉及到 GPIO_ACTIVE_LOW 的处理。

逻辑值和物理值

GPIO 有两个层面的值:物理电平和逻辑值。

物理电平是实际电压:

  • 高电平(3.3V)对应物理 1
  • 低电平(0V)对应物理 0

逻辑值是应用层的语义:

  • 按键按下对应逻辑 1
  • 按键松开对应逻辑 0

我们的硬件是低电平触发,所以物理和逻辑是反着的:

物理电平    逻辑值
────────────────────
高(松开) → 0
低(按下) → 1

gpiod_get_value() 已经帮我们处理了一次转换:

/* 内核内部实现(简化) */
intgpiod_get_value(struct gpio_desc *desc)
{
int raw_val = gpiod_get_raw_value(desc);  // 读取物理电平

/* 如果设置了 GPIO_ACTIVE_LOW,反转逻辑值 */
if (test_bit(GPIOD_FLAG_ACTIVE_LOW, &desc->flags))
return !raw_val;
else
return raw_val;
}

所以如果设备树里写了 GPIO_ACTIVE_LOW:

  • 物理低 → gpiod_get_value() 返回 1
  • 物理高 → gpiod_get_value() 返回 0

但我们的应用层约定是:

  • 按键按下 → 返回 0
  • 按键松开 → 返回 1

所以需要在 key_get_state() 里再反转一次:return !val。这个约定是从 LED 驱动继承来的。LED 的约定是:1=亮,0=灭。按键我们就约定:1=松开(高电平),0=按下(低电平)。

实际上这个约定可以随便定,只要驱动和应用层统一就行。但我们为了保持一致性,沿用了 LED 的约定。

深入内核源码

如果你想看 gpiod_get_value() 的完整实现,它在 drivers/gpio/gpiolib.c 里:

intgpiod_get_value(struct gpio_desc *desc)
{
/* 省略参数检查和锁操作 */

if (test_bit(GPIOD_FLAG_ACTIVE_LOW, &desc->flags))
return !gpiod_get_raw_value(desc);
else
return gpiod_get_raw_value(desc);
}
EXPORT_SYMBOL(gpiod_get_value);

gpiod_get_raw_value() 就直接读 GPIO 控制器的寄存器了,具体实现取决于硬件平台。对于 i.MX6ULL,它会读写 GPIO 数据寄存器(GPIO_DR)。

关于 GPIO 释放

你可能注意到了,我们的硬件抽象层没有释放 GPIO 的函数。这是因为我们用了 devm_ API:

gpiod = devm_gpiod_get(dev, NULL, GPIOD_IN);

devm_ 前缀表示"managed resource"(托管资源)。当设备卸载时,内核会自动释放这些资源。所以我们的代码里不需要显式调用 gpiod_put()。

PS:托管资源的好处 托管资源机制最大的好处是防止资源泄漏。你想想,如果驱动在某个错误路径返回,忘记了释放 GPIO,这个 GPIO 就永远被占用了。托管资源自动处理这些清理工作,少写代码还更安全。当然,我们的教程代码为了演示完整流程,会显示调用释放函数。但在实际工程里,托管资源是更好的选择。

小结一下

GPIO 输入的核心就这么几个函数:

/* 1. 获取并配置为输入 */
gpiod = gpiod_get(dev, NULL, GPIOD_IN);

/* 2. 读取状态 */
val = gpiod_get_value(gpiod);

/* 3. (可选)释放 */
gpiod_put(gpiod);

剩下的工作就是怎么用这些基本操作实现一个完整的按键驱动了。下一章我们看轮询方式的实现,在 read() 函数里循环等待按键事件。看完这些代码你会发现,GPIO 输入并没有想象中那么复杂。Linux 的 GPIO 子系统把硬件差异都封装好了,我们用统一的高层 API 就能操作。这种抽象做得挺到位的。


相关阅读

  1. 现代Qt开发教程(新手篇)1.15——正则与文本处理 - 相似度 100%

最新文章

随机文章