Linux中gpio子系统的现代接口

之前学过gpio子系统的基础知识,以及旧的接口。

如下:

Linux的gpio子系统-CSDN博客

现在一般不使用上面讲的基于 GPIO 编号的传统接口(逐步被淘汰),而是推荐使用基于 gpio_desc 的现代接口

struct gpio_desc

struct gpio_desc是 Linux 内核中用于描述和管理 GPIO 引脚的核心数据结构,它是对底层 GPIO 硬件的抽象,提供了统一的接口来操作 GPIO(如配置方向、读写电平、申请 / 释放等)。

1. gpio_desc简介

在早期 Linux 内核中,GPIO 操作依赖直接的GPIO 编号 (如 gpio = 12),但这种方式存在扩展性问题(编号空间有限、跨平台兼容性差)。gpio_desc 的引入解决了这些问题:

  • 统一抽象 :将每个 GPIO 引脚封装为 struct gpio_desc 实例,屏蔽不同硬件平台(如不同 SOC 的 GPIO 控制器)的差异。
  • 资源管理:跟踪 GPIO 的使用状态(已申请 / 未申请、方向、活跃电平极性等),避免资源冲突。
  • 功能扩展 :支持中断、休眠等高级特性,为 GPIO 操作提供标准化接口(如 gpiod_set_value()gpiod_direction_output() 等)。

2. struct gpio_desc 的关键成员(简化版)

复制代码
struct gpio_desc {
    struct gpio_chip *chip;  // 指向该 GPIO 所属的控制器(芯片)
    unsigned int offset;     // 在控制器内的偏移量(如第 3 个引脚)
    const char *label;       // GPIO 的标签(如 "user-led")
    unsigned long flags;     // 标志位(如 GPIOF_ACTIVE_LOW 低电平有效)
    // ... 其他成员(引用计数、中断信息等)
};
  • chip:关联到管理该 GPIO 的控制器驱动(struct gpio_chip),负责底层硬件操作(如寄存器读写)。
  • offset:GPIO 在控制器内的编号(如控制器有 32 个引脚,offset 范围 0~31)。
  • flags:记录 GPIO 的属性(如是否低电平有效、是否已申请等)。

gpio_desc 是 Linux 内核 GPIO 子系统的核心数据结构,是现代 GPIO 子系统(相较于早期直接使用 GPIO 编号的旧方式)实现设备与 GPIO 资源关联、状态管理、功能控制的核心载体。

3. gpio_desc 的核心定位:GPIO 资源的 "描述符"

可以将 struct gpio_desc 理解为 "GPIO 引脚的身份证 + 控制接口"------ 它不仅记录了一个 GPIO 引脚的硬件属性(如归属的 GPIO 控制器、引脚编号、电气特性等),还提供了内核操作该 GPIO 的统一入口(如设置输出电平、读取输入状态、配置中断等)。

在早期 Linux GPIO 子系统中,开发者直接使用 GPIO 全局编号 (如 gpio = 123)操作引脚,但这种方式存在明显缺陷:

  • 全局编号缺乏硬件上下文,无法区分不同 GPIO 控制器下的引脚;
  • 无法统一管理引脚的状态(如是否已被占用、电气属性配置);
  • 扩展性差,难以支持复杂的 GPIO 功能(如中断、复用)。

gpio_desc 的引入正是为了解决这些问题:将 GPIO 引脚的 "资源信息" 与 "操作接口" 封装为一个结构体,让内核和驱动通过这个 "描述符" 间接操作 GPIO,实现了 GPIO 资源的统一管理和抽象。

4. gpio_desc 的关键作用(在 GPIO 子系统中的角色)

gpio_desc 是 GPIO 子系统中连接 "硬件控制器""驱动层""应用层" 的核心桥梁,主要承担以下职责:

(1)记录 GPIO 引脚的硬件与状态信息

struct gpio_desc 内部包含多个关键字段(不同内核版本字段略有差异,核心逻辑一致),例如:

  • struct gpio_chip *chip:指向该 GPIO 所属的 GPIO 控制器驱动gpio_chip 是 GPIO 控制器的抽象,负责与硬件寄存器交互);
  • unsigned int line:该 GPIO 在所属控制器中的 本地编号 (如控制器 gpiochip0 下的第 5 个引脚,line = 5);
  • unsigned long flags:GPIO 的状态标志,如:
    • GPIO_FLAG_REQUESTED:标记该 GPIO 已被申请(避免重复占用);
    • GPIO_FLAG_ACTIVE_LOW:标记该 GPIO 为 "低电平有效";
    • GPIO_FLAG_IS_OUT:标记该 GPIO 当前为输出模式;
  • const char *label:GPIO 的用途标签(如 "led_usr""button_pwr",方便调试)。

这些字段让内核能清晰掌握每个 GPIO 的 "归属""状态""用途",避免资源冲突和错误配置。

(2)作为 GPIO 操作的统一入口

GPIO 子系统提供的所有核心操作 API(如设置电平、读取状态、配置方向),均以 gpio_desc 为参数,而非直接使用 GPIO 编号。例如:

  • gpiod_set_value(struct gpio_desc *desc, int value):设置 GPIO 输出电平(value=1 高电平,value=0 低电平);
  • gpiod_get_value(struct gpio_desc *desc):读取 GPIO 输入电平;
  • gpiod_direction_output(struct gpio_desc *desc, int value):将 GPIO 配置为输出模式,并设置初始电平;
  • gpiod_request(struct gpio_desc *desc, const char *label):申请使用该 GPIO(会检查是否已被占用,避免冲突)。

你在之前提供的 create_gpio_led 函数中,也能看到这些 API 的实际使用,例如:

复制代码
// 将 GPIO 配置为输出模式,并设置初始状态(state)
ret = gpiod_direction_output(led_dat->gpiod, state);
// 读取 GPIO 当前电平(用于获取默认状态)
state = !!gpiod_get_value_cansleep(led_dat->gpiod);

(3)衔接设备树与 GPIO 资源分配

在设备树(Device Tree)场景下,驱动通常通过设备树节点的 gpios 属性(如 gpios = <&gpio1 5 GPIO_ACTIVE_LOW>)声明所需的 GPIO 资源。此时,GPIO 子系统会通过 devm_gpiod_get() 等函数,将设备树中描述的 GPIO 资源解析为对应的 gpio_desc 结构体 ,并传递给驱动 ------gpio_desc 在这里承担了 "设备树描述" 与 "实际硬件引脚" 的映射角色。

5. 与早期 GPIO 编号的关系

gpio_desc 是对早期 "GPIO 全局编号" 的封装和升级,两者的转换关系如下:

  • 从编号到描述符:gpio_to_desc(unsigned int gpio)(如你代码中 led_dat->gpiod = gpio_to_desc(template->gpio));
  • 从描述符到编号:desc_to_gpio(const struct gpio_desc *desc)(现代驱动已很少直接使用编号)。

内核推荐优先使用 gpio_desc 相关 API (如 devm_gpiod_getgpiod_set_value),而非直接操作 GPIO 编号,以保证代码的可移植性和资源管理的安全性。

总结

gpio_desc 是 Linux GPIO 子系统的 "核心数据结构",它封装了 GPIO 引脚的硬件属性、状态信息和操作接口,实现了 GPIO 资源的统一管理、设备树衔接和跨平台兼容,是现代 Linux 驱动操作 GPIO 的 "标准入口"。

基于 gpio_desc 的现代接口(推荐)

【旧接口】int gpio(已废弃)

【新接口】struct gpiod_desc *(现代标准)


一、旧 GPIO 接口(legacy,整数编号)

1. 申请 / 释放

复制代码
int gpio_request(unsigned gpio, const char *label);
void gpio_free(unsigned gpio);

2. 方向

复制代码
int gpio_direction_input(unsigned gpio);
int gpio_direction_output(unsigned gpio, int value);

3. 读写

复制代码
int gpio_get_value(unsigned gpio);
void gpio_set_value(unsigned gpio, int value);

4. 校验

复制代码
int gpio_is_valid(unsigned gpio);

5. 设备树获取

复制代码
int of_get_gpio(struct device_node *np, int index);
int of_get_gpio_flags(struct device_node *np, int index, unsigned long *flags);

6. 转换

复制代码
struct gpio_desc *gpio_to_desc(unsigned gpio);
int desc_to_gpio(struct gpio_desc *desc);

二、新 GPIO 接口(modern,gpiod 描述符)

1. 从设备树获取(最常用)

复制代码
struct gpio_desc *devm_gpiod_get(struct device *dev, const char *con_id, enum gpiod_flags flags);
struct gpio_desc *devm_gpiod_get_optional(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, int idx, enum gpiod_flags flags);

2. 申请 / 释放(非 devm)

复制代码
struct gpio_desc *gpiod_get(struct device *dev, const char *con_id, enum gpiod_flags flags);
void gpiod_put(struct gpio_desc *desc);

3. 方向

复制代码
int gpiod_direction_input(struct gpio_desc *desc);
int gpiod_direction_output(struct gpio_desc *desc, int value);

4. 读写电平

复制代码
int gpiod_get_value(struct gpio_desc *desc);
void gpiod_set_value(struct gpio_desc *desc, int value);

5. 校验(你要的 gpiod 版本!)

复制代码
bool gpiod_is_valid(const struct gpio_desc *desc);

6. 设备树解析(gpiod 新版)

复制代码
struct gpio_desc *gpiod_get_from_of_node(struct device_node *np, const char *propname, int index, enum gpiod_flags flags, const char *label);

7. 转换

复制代码
int desc_to_gpio(const struct gpio_desc *desc);
struct gpio_desc *gpio_to_desc(unsigned gpio);

8. 错误判断(非常重要)

复制代码
bool IS_ERR(const void *ptr);
int PTR_ERR(const void *ptr);

三、新旧接口 完整一一对应表(最终版)

功能 旧接口(int gpio) 新接口(gpiod *)
申请 gpio_request devm_gpiod_get
释放 gpio_free devm 自动释放 /gpiod_put
读输入 gpio_get_value gpiod_get_value
写输出 gpio_set_value gpiod_set_value
方向输入 gpio_direction_input gpiod_direction_input
方向输出 gpio_direction_output gpiod_direction_output
校验是否有效 gpio_is_valid gpiod_is_valid
从设备树获取 of_get_gpio devm_gpiod_get
从设备树获取(带 flags) of_get_gpio_flags devm_gpiod_get
转换为 desc gpio_to_desc desc_to_gpio
判断错误 IS_ERR / PTR_ERR

现代驱动标准写法示例

复制代码
struct gpio_desc *reset;

reset = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_LOW);
if (IS_ERR(reset))
    return PTR_ERR(reset);

if (gpiod_is_valid(reset)) {
    gpiod_set_value(reset, 0);
    msleep(10);
    gpiod_set_value(reset, 1);
    msleep(10);
}

附:devm_前缀

devm_ 前缀到底是什么?

一句话终极结论

devm_ = 内核帮你自动释放资源,不用你手动写释放代码!

全称:Device Managed Resource(设备托管资源)


一、先讲人话

你写驱动,经常要做这些事:

  • 申请内存
  • 申请 GPIO
  • 申请中断
  • 注册设备
  • 打开时钟

正常流程:

复制代码
申请 → 使用 → 用完必须手动释放

忘记释放 = 内存泄漏 / 资源卡死

加了 devm_ 之后:

复制代码
申请 → 使用 → 设备拔掉时,内核自动帮你释放!

你完全不用写释放代码!


二、最直观对比(一看就懂)

不带 devm_(手动版,麻烦)

复制代码
// 申请
gpio_request(gpio, "test");

// ... 驱动逻辑 ...

// 卸载驱动时必须手动释放!
gpio_free(gpio);

❌ 缺点:

  • 必须在 remove 函数里写释放
  • 容易忘
  • 容易漏
  • 容易 crash

devm_(自动托管版,推荐)

复制代码
// 申请(内核自动托管)
devm_gpio_request(gpio, "test");

// ... 驱动逻辑 ...

// 不用写 gpio_free!

✅ 优点:

  • 设备移除时内核自动释放
  • 永不泄漏
  • 代码少一半
  • 驱动更安全

三、devm_ 不是 GPIO 专属!

所有驱动资源都有 devm_ 版本

比如:

复制代码
devm_kmalloc           // 申请内存,自动释放
devm_gpio_request      // 申请GPIO,自动释放
devm_gpiod_get         // 新GPIO,自动释放
devm_regulator_get     // 电源,自动释放
devm_ioremap           // 地址映射,自动释放
devm_request_irq       // 中断,自动释放

只要看到 devm_,就等于:

我不用管释放,内核会帮我处理!

关于新旧接口

好多驱动里都看到使用旧的GPIO接口

你观察得完全正确 !Linux 内核里90% 的老驱动、网卡驱动、WiFi 驱动、嵌入式驱动,全在用旧 GPIO(int 型)

我告诉你最真实、最扎心、最业内的 4 个原因,你一下就懂了:


1. 历史原因:老内核不支持新 GPIO

  • 旧 GPIO API:Linux 2.6 ~ 3.10 都能用
  • 新 GPIO(gpiod):Linux 3.17 以后才出现

很多嵌入式产品(路由器、摄像头、Android)还在用 Linux 3.10 / 4.4 / 4.9 这种老内核 它们根本没有新 GPIO API,只能用旧的!


2. 驱动太老,厂商懒得改

像你看到的:

  • dm9000
  • sic80211 WiFi
  • aic8800
  • 以太网 MAC
  • 串口、I2C、SPI

这些驱动写于 10~20 年前 厂商只保证能用,不保证升级新 API

能用就行,不重构!


3. 旧 GPIO 写法简单粗暴

老工程师习惯了:

复制代码
int gpio = of_get_named_gpio_flags(...);
gpio_is_valid(gpio);
devm_gpio_request_one(...);
gpio_set_value(gpio, 1);

就 4 行,简单直接。

新 GPIO 要写:

复制代码
struct gpio_desc *gpiod = devm_gpiod_get(...);
if (IS_ERR(gpiod)) ...;
gpiod_set_value...

很多人懒得改


4. 内核保留兼容层,旧代码不会报错

Linux 内核向后兼容 旧 GPIO 函数并没有被删除 只是标记为 deprecated(不推荐) 不是 删除(deleted)

所以:旧代码能跑 → 厂商绝对不改


🔥 最重要结论(你必须记住)

旧 GPIO 现在依然大量存在,而且会长期存在!

新 GPIO 是未来,但旧 GPIO 是现在!


那你应该怎么学?

必须 两套都学:

  1. 旧 GPIO(int) → 看老驱动、厂商驱动、嵌入式驱动必备
  2. 新 GPIO(gpiod) → 新内核、新驱动、官方规范、写新驱动用

你现在的情况:

  • 看驱动 → 遇到旧 GPIO 占 90%
  • 写新驱动 → 必须用新 GPIO

所以你必须两套都懂


我给你一句最真实的行业实话

** 旧 GPIO 是现实,新 GPIO 是规范。

看懂旧的,你能读 90% 驱动;

会写新的,你能写 100% 新驱动。**

旧 GPIO 完整标准模板

以后你在任何驱动里看到 GPIO 代码,套这个模板就能秒懂。

旧 GPIO(int 编号)全套标准流程(驱动里 99% 都是这样)

1. 定义变量

复制代码
int reset_gpio;
unsigned long gpio_flags;
int ret;

2. 从设备树获取 GPIO(OF 函数)

复制代码
reset_gpio = of_get_named_gpio_flags(np, "reset-gpios", 0, &gpio_flags);

3. 判断 GPIO 是否有效

复制代码
if (!gpio_is_valid(reset_gpio)) {
    dev_err(dev, "no reset gpio\n");
    return -ENODEV;
}

4. 申请 GPIO(带 devm_ 自动释放)

复制代码
ret = devm_gpio_request_one(dev, reset_gpio, gpio_flags, "dev_reset");
if (ret)
    return ret;

5. 操作 GPIO

复制代码
gpio_set_value(reset_gpio, 0);
msleep(10);
gpio_set_value(reset_gpio, 1);
msleep(20);

6. 读取 GPIO(输入时)

复制代码
int val = gpio_get_value(irq_gpio);

旧 GPIO 最常用、最核心的 8 个函数(必须记)

这些就是你在驱动里反复看到的那一批

  1. of_get_named_gpio_flags
  2. of_get_gpio
  3. gpio_is_valid
  4. devm_gpio_request_one
  5. gpio_request
  6. gpio_direction_output
  7. gpio_set_value
  8. gpio_get_value

没有更多了!驱动里就这 8 个来回用。


一句话总结旧 GPIO 风格

int 编号 + of_get 拿编号 + gpio_is_valid + devm_gpio_request + gpio_set/get

这就是你在 dm9000、以太网、WiFi、各类老驱动里看到的统一套路

相关推荐
senijusene2 小时前
IMX6ULL 平台 I2C 总线:从硬件原理到裸机驱动
c语言·arm开发·驱动开发
文静小土豆3 小时前
Centos7负载异常过高排查思路(Load Average)
linux
Deitymoon3 小时前
linux——原子操作
linux
亚空间仓鼠3 小时前
OpenEuler系统常用服务(四)
linux·运维·服务器·网络
昪彧翀忞4 小时前
dhcp小实验
linux·服务器·网络
mftang4 小时前
ARM架构和主要内核介绍-D
arm开发·cortex-r·cortex-m·cortex-a
bukeyiwanshui4 小时前
20260407系统间复制文档
linux
23.5 小时前
【Linux】grep -F 及 双横线--的妙用
linux·命令模式
橙露5 小时前
Linux 驱动入门:字符设备驱动框架与编写流程
linux·运维·服务器