版权信息
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 这个名字:
- 前缀 “enable” 可以自定义,这是我们后续使用API函数查找这个GPIO的索引关键字。
- 后缀“-gpios” 是规范的,固定的。
2.3. step2.在驱动中调用API函数
请转到第4小节。
3. GPIO子系统的工作原理浅析
3.1. 系统结构
Linux 内核通过 gpiolib 框架统一管理 GPIO。该框架包括三部分:
-
GPIO 控制器驱动:芯片厂商提供底层的 GPIO 控制器驱动。硬件相关的代码(位于
drivers/gpio/目录),负责初始化和操作具体的 GPIO 控制器,实现了“读寄存器、写寄存器”的具体函数。 -
GPIO 核心库(gpiolib):处于中间的连接层。gpiolib 是 Linux GPIO 子系统的核心,它管理着一张大表,维护所有 GPIO 控制器(gpio_chip)、所有 GPIO 描述符(gpio_desc)、控制器之间的映射关系。提供API函数并转换为底层驱动的调用。
-
GPIO 字符设备接口驱动:也就是我们写的设备驱动。
Pinctrl 和 GPIO 的底层交互:在某些 SoC 架构中,当你调用
gpiod_direction_output()时,GPIO 子系统会在后台自动调用 Pinctrl 子系统的接口,确保引脚复用被强制切回 GPIO 模式。这是一种保险机制,但我们在写 DTS 时,最好还是显式地写清楚 Pinctrl 配置。
3.2. 工作流程
一般来说,gpio控制器节点都会有以下两个属性:
gpio-controller:告诉内核这是一个 GPIO 控制器。#gpio-cells:告诉内核控制器使用几个参数描述 GPIO(如编号和 flags),用于解析。
从代码层面看,GPIO 子系统的工作流程其实就是三个核心结构体之间的交互:
- struct gpio_chip:代表硬件控制器(Provider,厂商写的)。
- struct gpio_desc:代表每一个具体的引脚(Core,内核维护的)。
- 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 个)
// ...
};
- 动作:调用
gpiochip_add_data(chip)。 - 结果:内核收到这个
chip,知道有 32 个引脚可用,并为每一个引脚创建了下一步的struct gpio_desc。
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; // 引脚名字
// ...
};
- 状态:此时这些
desc都在内存里躺着,flags是空的,等待被申请。
3.2.3. 获取阶段:驱动申请 (Consumer)
现在轮到你的驱动代码运行了。
- 你的代码:
desc = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW); - 内核流程:
- 解析 DTS:内核依据
#gpio-cells解析设备树,找到reset-gpios = <gpio1 5 ...> - 定位:根据 handle 找到对应的
gpio_chip(控制器 1),再根据偏移量(5)在数组中找到第 5 个 struct gpio_desc。 - 配置 Flags:读取 DTS 里的
GPIO_ACTIVE_LOW,把这个信息记录在desc->flags里的FLAG_ACTIVE_LOW位。 - 绑定:把你的驱动名字填入
desc->label(标记占用)。 - 结果:你拿到了指向这个 struct gpio_desc 的指针。
- 解析 DTS:内核依据
3.2.4. 控制阶段:函数调用链 (Runtime)
当你调用 gpiod_set_value(desc, 1) 时,内核发生了一次完整的“结构体跳跃”:
- 逻辑判断 (GPIOLIB 层):函数
gpiod_set_value接收到你的desc和1。
它检查desc->flags:
// 伪代码逻辑
if (test_bit(FLAG_ACTIVE_LOW, &desc->flags))
value = !value; // 如果是低有效,把逻辑 1 翻转为物理 0
- 硬件操作 (Driver 层):它通过
desc找到所属的chip,然后调用chip里注册的函数指针:
// 伪代码逻辑
chip->set(chip, gpio_index, value);
- 寄存器写入:最终执行到了厂商驱动里具体的代码(例如
rockchip_gpio_set),向物理地址写入电平。
3.2.5. 总结
对于工程师来说,只要记住这个链条:
- Provider 提供 struct gpio_chip(含操作函数
set/get)。 - Core 维护 struct gpio_desc(含状态标志
flags,如 Active Low)。 - Consumer 持有 **struct gpio_desc ***(句柄)。
- 调用链:
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,防止内存泄漏。
- devm_gpiod_get:
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。
- devm_gpiod_get_optional:
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 时,可以直接指定初始状态:
GPIOD_ASIS: 不改变当前状态(保持原样)。GPIOD_IN: 配置为输入。GPIOD_OUT_LOW: 配置为输出,且初始值为逻辑低(关闭)。GPIOD_OUT_HIGH: 配置为输出,且初始值为逻辑高(开启)。
4.3. 读写控制 (Read & Write)
拿到 struct gpio_desc * 指针后,就可以操作它了。
4.3.1. A. 设置输出值
- gpiod_set_value
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. 读取输入值
- gpiod_get_value
int gpiod_get_value(const struct gpio_desc *desc);
功能:读取 GPIO 当前电平。
返回值:1 表示逻辑有效,0 表示逻辑无效。
4.3.3. C. 修改方向
int gpiod_direction_input(struct gpio_desc *desc)int gpiod_direction_output(struct gpio_desc *desc, int value)
4.4. 睡眠与原子操作 (Sleeping vs Atomic)
这是一个容易踩坑的地方。
- SoC 内部 GPIO:读写速度极快,可以在中断上下文(Atomic Context)中调用。
- 扩展芯片 GPIO (如 I2C/SPI 扩展器):读写需要通过 I2C/SPI 总线,耗时较长,会引起睡眠,不能在中断或自旋锁中调用。
为了代码的健壮性,如果你不确定 GPIO 是 SoC 内部的还是外部扩展的,或者你在非原子上下文(如线程化的中断、工作队列、普通进程),建议使用 cansleep 后缀的函数:
-
gpiod_set_value_cansleep(desc, value) -
gpiod_get_value_cansleep(desc)
如果你的 GPIO 确实连在 I2C 扩展器上,而你用了普通的 gpiod_set_value,内核会打印警告栈回溯 (WARN_ON)。
4.5. 中断相关 (Interrupts)
如果要把 GPIO 当作中断源(比如按键):
- gpiod_to_irq
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 |