GoDm@'s Blog

gpio子系统

版权信息

warning

本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。


衔接上一篇介绍 Pinctrl 子系统的文章。

1. GPIO 子系统简介

如果说 Pinctrl 子系统是负责把“路”铺好(配置引脚模式、电气属性),那么 GPIO 子系统就是负责在路上“跑车”(控制高低电平、读取输入状态)。

对于嵌入式工程师来说,GPIO(通用输入输出)是最简单、最基础,但也是使用频率最高的子系统。

2. GPIO 子系统的使用

2.1. step0.查阅规范

不多说。上篇文章已经提到过。

2.2. step1.在设备树 (DTS) 中描述

我们默认已经配置好了GPIO控制器节点。一般来说也不需要配置,官方已配置好。如果想要配置GPIO控制器节点,请查阅编写规范文档,官方已给出例子和要求。控制器基地址查阅参考手册。

我们需要告诉内核:这个设备用到了哪个 GPIO,以及它是高电平有效还是低电平有效。

/* 例子:一个由 GPIO 控制的蜂鸣器节点 */
beep_device {
    compatible = "my-beep-driver";

    /* 1. 引用 Pinctrl (先铺路) */
    pinctrl-names = "default";
    pinctrl-0 = <&beep_gpio_pin>;

    /* 2. 定义 GPIO (后跑车) */
    /* 格式:<&gpio控制器 引脚索引 标志位> */
    enable-gpios = <&gpio1 0 GPIO_ACTIVE_HIGH>;
};

不同厂商写法不同,请对应的参见规范文档。

关于 enable-gpios 这个名字:

2.3. step2.在驱动中调用API函数

请转到第4小节。

3. GPIO子系统的工作原理浅析

3.1. 系统结构

Linux 内核通过 gpiolib 框架统一管理 GPIO。该框架包括三部分:

Pinctrl 和 GPIO 的底层交互:在某些 SoC 架构中,当你调用gpiod_direction_output() 时,GPIO 子系统会在后台自动调用 Pinctrl 子系统的接口,确保引脚复用被强制切回 GPIO 模式。这是一种保险机制,但我们在写 DTS 时,最好还是显式地写清楚 Pinctrl 配置。

3.2. 工作流程

一般来说,gpio控制器节点都会有以下两个属性:

从代码层面看,GPIO 子系统的工作流程其实就是三个核心结构体之间的交互:

  1. struct gpio_chip:代表硬件控制器(Provider,厂商写的)。
  2. struct gpio_desc:代表每一个具体的引脚(Core,内核维护的)。
  3. struct gpio_ops:代表底层操作方法(读/写寄存器的函数指针)。

3.2.1. 注册阶段:硬件上线 (Provider)

在系统启动时,芯片厂商的驱动加载。它初始化了一个核心结构体 struct gpio_chip 并注册给内核。

核心结构体:struct gpio_chip
这是底层驱动与 GPIO 子系统的接口。它包含了“怎么操作硬件”的所有函数指针。

struct gpio_chip {
    const char *label;     // 芯片名字,如 "gpio-rockchip"
    struct module *owner;

    // 核心操作函数指针(这是它的灵魂)
    int (*get)(struct gpio_chip *gc, unsigned int offset); // 读引脚
    void (*set)(struct gpio_chip *gc, unsigned int offset, int value); // 写引脚
    int (*direction_output)(struct gpio_chip *gc, unsigned int offset, int value);

    int base;              // 全局 GPIO 编号基址(现代内核多用 -1 动态分配)
    u16 ngpio;             // 这个控制器有多少个引脚(比如 32 个)
    // ...
};

3.2.2. 初始化阶段:生成描述符 (Core)

内核为了管理这 32 个引脚,会分配一个数组,数组里存的就是 struct gpio_desc

核心结构体:struct gpio_desc
这是 GPIO 子系统中最重要的对象。每一个物理引脚对应一个 gpio_desc

struct gpio_desc {
    struct gpio_device *gdev;  // 指回所属的 gpio_chip/device
    unsigned long flags;       // 记录状态:是输入还是输出?是 Active Low 吗?有没有被占用?
    const char *label;         // 谁在使用它?(用于 debug 显示)
    const char *name;          // 引脚名字
    // ...
};

3.2.3. 获取阶段:驱动申请 (Consumer)

现在轮到你的驱动代码运行了。

3.2.4. 控制阶段:函数调用链 (Runtime)

当你调用 gpiod_set_value(desc, 1) 时,内核发生了一次完整的“结构体跳跃”:

  1. 逻辑判断 (GPIOLIB 层):函数 gpiod_set_value 接收到你的 desc1
    它检查 desc->flags
// 伪代码逻辑
if (test_bit(FLAG_ACTIVE_LOW, &desc->flags))
    value = !value; // 如果是低有效,把逻辑 1 翻转为物理 0
  1. 硬件操作 (Driver 层):它通过 desc 找到所属的 chip,然后调用 chip 里注册的函数指针:
// 伪代码逻辑
chip->set(chip, gpio_index, value);
  1. 寄存器写入:最终执行到了厂商驱动里具体的代码(例如 rockchip_gpio_set),向物理地址写入电平。

3.2.5. 总结

对于工程师来说,只要记住这个链条:

  1. Provider 提供 struct gpio_chip(含操作函数 set/get)。
  2. Core 维护 struct gpio_desc(含状态标志 flags,如 Active Low)。
  3. Consumer 持有 **struct gpio_desc ***(句柄)。
  4. 调用链
gpiod_set_value()
⬇️
检查 desc->flags (处理极性)
⬇️
调用 desc->gdev->chip->set() (硬件回调)
⬇️
writel(REG) (物理生效)

4. 相关API函数

这些 API 定义在头文件 #include <linux/gpio/consumer.h> 中。相比老的 gpio_ 接口(基于整数索引),gpiod_ 接口(基于描述符)是目前内核推荐的标准用法。

4.1. 核心头文件

在驱动代码中,必须包含:

#include <linux/gpio/consumer.h>

4.2. 获取与释放 (Acquire & Release)

在驱动的 probe 函数中,我们需要获取 GPIO 的句柄(Descriptor)。

4.2.1. A. 资源托管方式 (推荐)

使用 devm_ 前缀的函数,驱动卸载时内核会自动释放 GPIO,无需手动调用 put,防止内存泄漏。

struct gpio_desc *devm_gpiod_get(struct device *dev, const char *con_id, enum gpiod_flags flags);

功能:获取一个 GPIO 描述符。
参数
- dev: 设备指针 (通常是 &pdev->dev)。
- con_id: 字符串 ID。对应 DTS 中的 xxx-gpios 中的 xxx。如果传 NULL,则匹配 gpios 属性。
- flags: 初始化标志(见下文)。
返回值:成功返回描述符指针;失败返回 ERR_PTR。

struct gpio_desc *devm_gpiod_get_optional(struct device *dev, const char *con_id, enum gpiod_flags flags);

功能:同上,但如果 DTS 里没有定义这个 GPIO,它不会报错,而是返回 NULL
场景:用于某些非必须的功能(例如:有的板子有 LED,有的没有,驱动想兼容两者)。

4.2.2. B. 初始化标志 (Flags)

在获取 GPIO 时,可以直接指定初始状态:

4.3. 读写控制 (Read & Write)

拿到 struct gpio_desc * 指针后,就可以操作它了。

4.3.1. A. 设置输出值

void gpiod_set_value(struct gpio_desc *desc, int value);

功能:设置 GPIO 输出电平。
参数 value
- 1: 设置为 逻辑有效 (Active)
- 0: 设置为 逻辑无效 (Inactive)
注意:内核会自动处理“高电平有效”还是“低电平有效”。如果 DTS 定义了 GPIO_ACTIVE_LOW,你写 1,实际物理引脚会输出低电平。

4.3.2. B. 读取输入值

int gpiod_get_value(const struct gpio_desc *desc);

功能:读取 GPIO 当前电平。
返回值1 表示逻辑有效,0 表示逻辑无效。

4.3.3. C. 修改方向

4.4. 睡眠与原子操作 (Sleeping vs Atomic)

这是一个容易踩坑的地方。

为了代码的健壮性,如果你不确定 GPIO 是 SoC 内部的还是外部扩展的,或者你在非原子上下文(如线程化的中断、工作队列、普通进程),建议使用 cansleep 后缀的函数

如果你的 GPIO 确实连在 I2C 扩展器上,而你用了普通的 gpiod_set_value,内核会打印警告栈回溯 (WARN_ON)。

4.5. 中断相关 (Interrupts)

如果要把 GPIO 当作中断源(比如按键):

int gpiod_to_irq(const struct gpio_desc *desc);

功能:将 GPIO 描述符转换为 Linux 中断号 (IRQ Number)。
用法:拿到返回值 irq 后,传给 request_irq()devm_request_threaded_irq() 来注册中断服务函数。

4.6. 常用API函数总结

功能 普通 API (原子上下文) 允许睡眠 API (进程上下文)
设置值 gpiod_set_value gpiod_set_value_cansleep
读取值 gpiod_get_value gpiod_get_value_cansleep
设置方向 gpiod_direction_output N/A (通常初始化时设定)
转中断号 gpiod_to_irq N/A
获取句柄 devm_gpiod_get N/A

共计约2.6k字。于2025/11/25首次发布,最后更新于2025/11/25。

本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

AI辅助创作:本文部分内容由 Gemini 3 Pro 生成,最终版本由作者审核与修改。了解该AI模型

  1. 1. GPIO 子系统简介
  2. 2. GPIO 子系统的使用
    1. 2.1. step0.查阅规范
    2. 2.2. step1.在设备树 (DTS) 中描述
    3. 2.3. step2.在驱动中调用API函数
  3. 3. GPIO子系统的工作原理浅析
    1. 3.1. 系统结构
    2. 3.2. 工作流程
  4. 4. 相关API函数
    1. 4.1. 核心头文件
    2. 4.2. 获取与释放 (Acquire & Release)
    3. 4.3. 读写控制 (Read & Write)
    4. 4.4. 睡眠与原子操作 (Sleeping vs Atomic)
    5. 4.5. 中断相关 (Interrupts)
    6. 4.6. 常用API函数总结