嵌入式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后就能被上层驱动调用。驱动代码不再直接操作硬件寄存器。
核心数据结构关系: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):
| 宏 | 值 | 含义 |
|---|---|---|
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子系统架构
gpiod驱动开发流程
八、常见问题解决
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怎么配合。