ESC
输入关键词搜索文章标题和内容

嵌入式Linux GPIO驱动开发:丢掉旧接口,用gpiod写驱动

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

嵌入式Linux GPIO驱动开发:丢掉旧接口,用gpiod写驱动

导读:GPIO是嵌入式开发里最基础的外设操作。Linux内核3.12重构了GPIO子系统,用描述符接口替换旧的整型编号接口。本文梳理GPIO子系统架构,详解gpiod接口用法,让你写的驱动代码符合当前内核规范。

一、原理简析

Linux GPIO子系统分三层:用户空间接口层、GPIO核心层(gpiolib)、GPIO控制器驱动层(gpio_chip)。gpiolib是中间层,向上给设备驱动提供统一的描述符接口,向下通过gpio_chip抽象屏蔽不同SoC的硬件差异。每个GPIO控制器驱动实现gpio_chip中的回调函数(request、free、direction_input/output、get/set等),注册到gpiolib后就能被上层驱动调用。驱动代码不再直接操作硬件寄存器。

flowchart TB A["用户空间<br/>libgpiod / sysfs"] --> B["GPIO核心层<br/>gpiolib"] B --> C["GPIO控制器A<br/>gpio_chip"] B --> D["GPIO控制器B<br/>gpio_chip"] C --> E["硬件GPIO组A"] D --> F["硬件GPIO组B"] style A fill:#E8F5E9 style B fill:#E3F2FD style C fill:#E3F2FD style D fill:#E3F2FD style E fill:#F3E5F5 style F fill:#F3E5F5

核心数据结构关系:struct gpio_desc是描述符接口的句柄,内部关联到struct gpio_chip;gpiolib通过gpio_desc管理每个GPIO的状态(方向、值、活跃低等),驱动只持有不透明的gpio_desc指针,没法伪造也没法越界访问。

二、新旧API对比

旧接口(legacy)基于整型编号,用unsigned int标识GPIO。新接口(gpiod)基于描述符,用struct gpio_desc *标识GPIO。内核文档写得很清楚:旧接口被强烈反对(strongly discouraged),新代码只用描述符接口。

旧接口的问题:

  • 无类型安全:整型编号随便构造,编译器查不出错
  • 方向需手动管理:获取GPIO后必须显式设置方向,容易漏
  • 无设备树集成:需要手动从设备树解析编号,代码冗余
  • 无自动释放:必须手动调用gpio_free(),异常路径容易泄漏

新接口的好处:

  • 类型安全:gpio_desc指针只能通过gpiod_get()获取,造不了假
  • 方向自动管理:通过flags参数在获取时即指定方向和初始值
  • 设备树集成:con_id自动映射到设备树<function>-gpios属性
  • devm自动释放:devm_gpiod_get()在设备解绑时自动释放,不会泄漏

三、gpiod接口用法

头文件:#include <linux/gpio/consumer.h>

3.1 获取与释放GPIO

// 基本获取(必须存在,否则返回错误)
struct gpio_desc *gpiod_get(struct device *dev, const char *con_id,
                            enum gpiod_flags flags);

// 带索引获取(同一功能有多个GPIO时使用)
struct gpio_desc *gpiod_get_index(struct device *dev, const char *con_id,
                                  unsigned int idx, enum gpiod_flags flags);

// 可选获取(GPIO不存在时返回NULL而非错误)
struct gpio_desc *gpiod_get_optional(struct device *dev, const char *con_id,
                                     enum gpiod_flags flags);

// 批量获取(一次获取同一功能的多个GPIO)
struct gpio_descs *gpiod_get_array(struct device *dev, const char *con_id,
                                   enum gpiod_flags flags);

// 释放
void gpiod_put(struct gpio_desc *desc);
void gpiod_put_array(struct gpio_descs *descs);

// 设备托管版本(推荐,设备解绑时自动释放)
struct gpio_desc *devm_gpiod_get(struct device *dev, const char *con_id,
                                 enum gpiod_flags flags);
struct gpio_desc *devm_gpiod_get_index(struct device *dev, const char *con_id,
                                       unsigned int idx, enum gpiod_flags flags);
struct gpio_desc *devm_gpiod_get_optional(struct device *dev, const char *con_id,
                                          enum gpiod_flags flags);

返回值规则:gpiod_get()系列返回有效描述符或IS_ERR()可检查的错误码(不会返回NULL)。gpiod_get_optional()系列在GPIO不存在时返回NULL。

con_id与设备树的映射规则:con_id是设备树属性<function>-gpios中的<function>部分(不含连字符和gpios后缀)。内核内部执行snprintf("%s-%s", con_id, "gpios")来查找属性。例如设备树中led-gpios对应con_id = "led"。

3.2 GPIOD flags定义

Flag 值 含义
GPIOD_ASIS 0 不初始化方向,需后续手动设置
GPIOD_IN 1 初始化为输入
GPIOD_OUT_LOW 2 初始化为输出,逻辑值为0
GPIOD_OUT_HIGH 3 初始化为输出,逻辑值为1
GPIOD_OUT_LOW_OPEN_DRAIN 4 输出逻辑0,强制开漏模式
GPIOD_OUT_HIGH_OPEN_DRAIN 5 输出逻辑1,强制开漏模式

关键:flags中的值是逻辑值。如果GPIO在设备树中配置为GPIO_ACTIVE_LOW,那么GPIOD_OUT_HIGH会让物理引脚输出低电平。gpiod接口就是这么设计的——驱动只关心逻辑语义,物理极性交给设备树描述。

3.3 方向设置

int gpiod_direction_input(struct gpio_desc *desc);
int gpiod_direction_output(struct gpio_desc *desc, int value);

gpiod_direction_output()的value参数同样是逻辑值。如果GPIO是active-low,value=1对应物理低电平。

3.4 读写GPIO值

// 可休眠版本(适用于所有GPIO控制器)
int gpiod_get_value(struct gpio_desc *desc);
void gpiod_set_value(struct gpio_desc *desc, int value);

// 原始值版本(忽略active-low,直接操作物理电平)
int gpiod_get_raw_value(struct gpio_desc *desc);
void gpiod_set_raw_value(struct gpio_desc *desc, int value);

// 原子上下文安全版本(仅适用于内存映射型控制器)
int gpiod_get_value_cansleep(struct gpio_desc *desc);
void gpiod_set_value_cansleep(struct gpio_desc *desc, int value);

使用原则:优先用gpiod_get_value/set_value(逻辑值),除非有特殊需求才用raw版本。如果GPIO控制器挂在I2C/SPI总线上,必须用_cansleep版本。

3.5 GPIO与中断映射

int gpiod_to_irq(struct gpio_desc *desc);

返回GPIO对应的Linux中断号,失败返回负数错误码。拿到后直接传给request_irq()或devm_request_irq()就行。

四、设备树配置

GPIO在设备树中通过<function>-gpios属性声明,格式为<phandle pin_num flags>:

/ {
    my_led {
        compatible = "myvendor,my-led";
        led-gpios = <&gpio1 25 GPIO_ACTIVE_LOW>;
    };

    my_button {
        compatible = "myvendor,my-button";
        button-gpios = <&gpio1 17 GPIO_ACTIVE_LOW>;
    };

    my_device {
        compatible = "myvendor,my-device";
        /* 多个同功能GPIO,用索引区分 */
        data-gpios = <&gpio2 0 GPIO_ACTIVE_HIGH>,
                     <&gpio2 1 GPIO_ACTIVE_HIGH>,
                     <&gpio2 2 GPIO_ACTIVE_HIGH>,
                     <&gpio2 3 GPIO_ACTIVE_HIGH>;
        /* 可选GPIO */
        reset-gpios = <&gpio1 30 GPIO_ACTIVE_LOW>;
    };
};

常用flag宏(定义在include/dt-bindings/gpio/gpio.h):

来自 linuxros.cn · linuxROS
宏 值 含义
GPIO_ACTIVE_HIGH 0 高电平有效
GPIO_ACTIVE_LOW 1 低电平有效
GPIO_OPEN_DRAIN 4 开漏输出
GPIO_OPEN_SOURCE 8 开源输出

con_id映射示例:

设备树属性 con_id参数 获取代码
led-gpios "led" gpiod_get(dev, "led", flags)
button-gpios "button" gpiod_get(dev, "button", flags)
reset-gpios "reset" gpiod_get(dev, "reset", flags)
data-gpios (index 2) "data" gpiod_get_index(dev, "data", 2, flags)

注意:<function>-gpio(单数)属性内核也支持,但只是兼容旧绑定,新绑定用<function>-gpios(复数)。

五、完整驱动示例

5.1 LED控制驱动

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>

struct my_led_priv {
    struct gpio_desc *led;
};

static ssize_t brightness_store(struct device *dev,
                                struct device_attribute *attr,
                                const char *buf, size_t count)
{
    struct my_led_priv *priv = dev_get_drvdata(dev);
    unsigned long val;

    if (kstrtoul(buf, 10, &val))
        return -EINVAL;

    gpiod_set_value(priv->led, val ? 1 : 0);
    return count;
}
static DEVICE_ATTR_WO(brightness);

static int my_led_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_led_priv *priv;
    int ret;

    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* 获取LED GPIO,初始化为输出低电平(逻辑0,LED灭) */
    priv->led = devm_gpiod_get(dev, "led", GPIOD_OUT_LOW);
    if (IS_ERR(priv->led))
        return dev_err_probe(dev, PTR_ERR(priv->led),
                             "failed to get led GPIO\n");

    platform_set_drvdata(pdev, priv);

    ret = device_create_file(dev, &dev_attr_brightness);
    if (ret)
        return ret;

    dev_info(dev, "LED driver probed\n");
    return 0;
}

static void my_led_remove(struct platform_device *pdev)
{
    device_remove_file(&pdev->dev, &dev_attr_brightness);
    /* devm_gpiod_get获取的GPIO无需手动释放 */
}

static const struct of_device_id my_led_of_match[] = {
    { .compatible = "myvendor,my-led" },
    { }
};
MODULE_DEVICE_TABLE(of, my_led_of_match);

static struct platform_driver my_led_driver = {
    .probe  = my_led_probe,
    .remove = my_led_remove,
    .driver = {
        .name = "my-led",
        .of_match_table = my_led_of_match,
    },
};
module_platform_driver(my_led_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Developer");
MODULE_DESCRIPTION("GPIO LED driver using gpiod interface");

5.2 按键输入驱动(中断方式)

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/input.h>

struct my_button_priv {
    struct gpio_desc *button;
    int irq;
    struct input_dev *input;
};

static irqreturn_t button_isr(int irq, void *dev_id)
{
    struct my_button_priv *priv = dev_id;
    int val;

    val = gpiod_get_value(priv->button);
    input_report_key(priv->input, BTN_0, val);
    input_sync(priv->input);

    return IRQ_HANDLED;
}

static int my_button_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_button_priv *priv;
    int ret;

    priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* 获取按键GPIO,初始化为输入 */
    priv->button = devm_gpiod_get(dev, "button", GPIOD_IN);
    if (IS_ERR(priv->button))
        return dev_err_probe(dev, PTR_ERR(priv->button),
                             "failed to get button GPIO\n");

    /* GPIO转中断号 */
    priv->irq = gpiod_to_irq(priv->button);
    if (priv->irq < 0)
        return dev_err_probe(dev, priv->irq,
                             "failed to get IRQ for button\n");

    /* 注册input设备 */
    priv->input = devm_input_allocate_device(dev);
    if (!priv->input)
        return -ENOMEM;

    priv->input->name = "my-button";
    priv->input->dev.parent = dev;
    input_set_capability(priv->input, EV_KEY, BTN_0);
    input_set_drvdata(priv->input, priv);

    ret = input_register_device(priv->input);
    if (ret)
        return ret;

    /* 申请中断,双边沿触发 */
    ret = devm_request_irq(dev, priv->irq, button_isr,
                           IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
                           "button-irq", priv);
    if (ret)
        return dev_err_probe(dev, ret, "failed to request IRQ\n");

    platform_set_drvdata(pdev, priv);
    dev_info(dev, "Button driver probed, IRQ=%d\n", priv->irq);
    return 0;
}

static const struct of_device_id my_button_of_match[] = {
    { .compatible = "myvendor,my-button" },
    { }
};
MODULE_DEVICE_TABLE(of, my_button_of_match);

static struct platform_driver my_button_driver = {
    .probe  = my_button_probe,
    .driver = {
        .name = "my-button",
        .of_match_table = my_button_of_match,
    },
};
module_platform_driver(my_button_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Developer");
MODULE_DESCRIPTION("GPIO button driver using gpiod + input subsystem");

5.3 用户空间操作

旧方式(sysfs,已废弃):

# 导出GPIO
echo 25 > /sys/class/gpio/export
# 设置方向
echo out > /sys/class/gpio/gpio25/direction
# 设置值
echo 1 > /sys/class/gpio/gpio25/value
# 取消导出
echo 25 > /sys/class/gpio/unexport

内核文档已经标明:sysfs GPIO接口被GPIO字符设备接口取代,将来会移除。

新方式(libgpiod,推荐):

# 安装工具
sudo apt install gpiod

# 查看GPIO控制器
gpiodetect

# 查看GPIO线状态
gpioinfo

# 读取输入
gpioget gpiochip1 17

# 设置输出
gpioset gpiochip1 25=1

# 监控边沿事件
gpiomon gpiochip1 17

六、对比表格

功能 旧接口(legacy) 新接口(gpiod)
头文件 <linux/gpio.h> <linux/gpio/consumer.h>
GPIO标识 unsigned int编号 struct gpio_desc *描述符
获取 gpio_request(num, label) gpiod_get(dev, con_id, flags)
释放 gpio_free(num) gpiod_put(desc)
托管获取 无 devm_gpiod_get(dev, con_id, flags)
设输入 gpio_direction_input(num) gpiod_direction_input(desc)
设输出 gpio_direction_output(num, val) gpiod_direction_output(desc, val)
读值 gpio_get_value(num) gpiod_get_value(desc)
写值 gpio_set_value(num, val) gpiod_set_value(desc, val)
转中断 gpio_to_irq(num) gpiod_to_irq(desc)
DT集成 手动of_get_named_gpio() 自动映射con_id
类型安全 无(整型可伪造) 有(不透明描述符)
Active-low 手动翻转 自动处理逻辑值
用户空间 sysfs(已废弃) libgpiod(字符设备)
内核状态 强烈反对使用 当前标准接口

七、核心流程图

GPIO子系统架构

flowchart TB A["设备驱动<br/>gpiod_get/put/set"] --> B["gpiolib核心层<br/>描述符管理+逻辑值映射"] B --> C["gpio_chip A<br/>SoC内置GPIO"] B --> D["gpio_chip B<br/>I2C GPIO扩展器"] C --> E["物理引脚组A"] D --> F["物理引脚组B"] G["设备树<br/>xxx-gpios属性"] --> B H["pinctrl子系统<br/>引脚复用"] --> C H --> D style A fill:#E8F5E9 style B fill:#E3F2FD style C fill:#E3F2FD style D fill:#E3F2FD style E fill:#F3E5F5 style F fill:#F3E5F5 style G fill:#FFF8E1 style H fill:#FFF8E1

gpiod驱动开发流程

flowchart TB A(["开始"]) --> B["设备树定义<br/>xxx-gpios属性"] B --> C["devm_gpiod_get()<br/>获取GPIO描述符"] C --> D{"IS_ERR()?"} D -->|"是"| E["dev_err_probe()<br/>返回错误"] D -->|"否"| F{"方向?"} F -->|"输出"| G["GPIOD_OUT_LOW/HIGH<br/>获取时即设方向"] F -->|"输入"| H["GPIOD_IN<br/>获取时即设方向"] G --> I["gpiod_set_value()<br/>控制输出"] H --> J["gpiod_get_value()<br/>读取输入"] H --> K["gpiod_to_irq()<br/>获取中断号"] K --> L["devm_request_irq()<br/>注册中断处理"] I --> M(["设备解绑时<br/>devm自动释放"]) J --> M L --> M style A fill:#E8F5E9 style E fill:#FFEBEE style M fill:#E8F5E9 style D fill:#FFF8E1 style F fill:#FFF8E1

八、常见问题解决

Q1:gpiod_get返回-ENOENT怎么办?

检查设备树属性名是否与con_id匹配。设备树中led-gpios对应con_id为"led"(不含连字符和gpios后缀)。同时确认GPIO控制器节点有gpio-controller和#gpio-cells属性。

Q2:gpiod_set_value设了1但引脚是低电平?

这是正常的active-low行为。如果设备树中GPIO声明为GPIO_ACTIVE_LOW,逻辑值1对应物理低电平。gpiod接口自动处理极性翻转,驱动不用管物理电平。如需直接操作物理电平,用gpiod_set_raw_value()。

Q3:devm_gpiod_get和gpiod_get怎么选?

优先用devm_gpiod_get()。设备托管版本在probe失败或设备解绑时自动释放GPIO,避免资源泄漏。只有在需要精确控制GPIO生命周期时才用gpiod_get()配合手动gpiod_put()。

Q4:旧代码用of_get_named_gpio()获取编号,如何迁移?

将of_get_named_gpio(np, "led-gpios", 0) + gpio_request()替换为devm_gpiod_get(dev, "led", flags)。新接口一步完成获取和方向设置,不用手动解析设备树。

Q5:用户空间操作GPIO用sysfs还是libgpiod?

必须用libgpiod。内核文档已把sysfs GPIO接口标记为obsolete,新功能只往字符设备接口加。libgpiod提供命令行工具(gpiodetect/gpioget/gpioset/gpiomon)和C/Python库,功能更全。

九、总结

GPIO驱动开发的关键转变就是从整型编号换到描述符接口。gpiod接口用类型安全的描述符、自动的设备树映射、逻辑值极性处理和devm资源管理,把驱动开发中容易踩的坑都填了。旧接口(gpio_request/gpio_free等)和sysfs用户空间接口都已废弃,新项目直接用gpiod + libgpiod。下期讲pinctrl子系统,看引脚复用和GPIO怎么配合。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页