4. GPIO 子系统基础:控制器、GPIO 号与核心 API
GPIO(General Purpose Input/Output)子系统是 Linux 内核中管理通用输入输出引脚的框架。在嵌入式系统中,按键、LED、传感器中断等外设都通过 GPIO 与 SoC 连接。理解 GPIO 子系统的分层结构、编号规则以及核心操作 API,是编写设备树 GPIO 按键驱动的第一步。
4.1 GPIO 子系统三层架构
Linux GPIO 子系统采用分层设计,从上到下依次为:GPIO 消费者层(驱动开发者使用)、GPIO 核心层(通用框架)和 GPIO 控制器层(硬件相关实现)。
架构要点:消费者层通过标准 API 请求和操作 GPIO,无需关心底层寄存器细节;核心层负责管理 GPIO 号分配、中断映射和 sysfs 接口;控制器层由具体芯片驱动实现,操作硬件寄存器。
4.2 GPIO 号:从全局到局部
在传统 Linux GPIO 子系统中,每个 GPIO 引脚由一个 全局整数编号 标识,范围通常为 0~511(取决于架构)。这个编号由 GPIO 控制器注册时动态分配或静态映射。
| 概念 | 说明 | 示例 |
|---|---|---|
| GPIO 控制器基号 | 每个 GPIO 控制器(如 GPIO0、GPIO1)在注册时分配一个起始编号 | GPIO0 基号 = 32,GPIO1 基号 = 64 |
| 引脚偏移 | 控制器内部引脚从 0 开始编号 | GPIO0_15 的偏移 = 15 |
| 全局 GPIO 号 | 基号 + 偏移 | 32 + 15 = 47 |
| 设备树中的 GPIO 号 | 通过 <控制器节点 引脚编号 标志> 三元组描述 | <&gpio0 15 GPIO_ACTIVE_LOW> |
新内核趋势:从 Linux 4.x 开始,内核推荐使用
gpiod 描述符 API(基于 struct gpio_desc *),不再直接操作整数 GPIO 号。但设备树中仍使用整数偏移,驱动内部通过 gpio_to_desc() 或 devm_gpiod_get() 转换。
4.3 核心 API 详解
以下 API 是 GPIO 驱动开发中最常用的函数,涵盖申请、方向设置、读写和释放四个基本操作。
4.3.1 申请与释放
#include <linux/gpio.h>
// 申请一个 GPIO 引脚
// gpio: 全局 GPIO 号
// label: 申请者名称(用于调试)
// 返回值:0 成功,负值错误码
int gpio_request(unsigned gpio, const char *label);
// 释放 GPIO 引脚
void gpio_free(unsigned gpio);
常见错误:未调用
gpio_request() 直接设置方向会导致内核警告或驱动加载失败。同一 GPIO 号不能被两个驱动同时申请。
4.3.2 方向设置
// 设置为输入模式
// gpio: 全局 GPIO 号
// 返回值:0 成功,负值错误码
int gpio_direction_input(unsigned gpio);
// 设置为输出模式,并设置初始值
// gpio: 全局 GPIO 号
// value: 初始输出值(0 或 1)
// 返回值:0 成功,负值错误码
int gpio_direction_output(unsigned gpio, int value);
4.3.3 读写值
// 读取 GPIO 引脚电平
// gpio: 全局 GPIO 号
// 返回值:0(低电平)或非 0(高电平)
int gpio_get_value(unsigned gpio);
// 设置 GPIO 引脚输出电平
// gpio: 全局 GPIO 号
// value: 0(低电平)或 1(高电平)
void gpio_set_value(unsigned gpio, int value);
4.3.4 中断相关
// 将 GPIO 映射为中断号
// gpio: 全局 GPIO 号
// 返回值:中断号(正数),负值错误码
int gpio_to_irq(unsigned gpio);
// 示例:申请 GPIO 并获取中断号
if (gpio_request(BUTTON_GPIO, "my_button") == 0) {
gpio_direction_input(BUTTON_GPIO);
int irq = gpio_to_irq(BUTTON_GPIO);
if (irq > 0) {
request_irq(irq, button_isr, IRQF_TRIGGER_FALLING,
"button", dev);
}
}
4.4 API 调用流程与状态机
一个典型的 GPIO 按键驱动初始化流程如下:
- 解析设备树:获取 GPIO 编号和标志(如上升沿/下降沿触发)。
- 申请 GPIO:调用
gpio_request()确保引脚未被占用。 - 设置方向:按键场景调用
gpio_direction_input()。 - 获取中断号:调用
gpio_to_irq()将 GPIO 转换为中断号。 - 注册中断:调用
request_irq()绑定中断处理函数。 - 释放资源:在驱动卸载时依次释放中断和 GPIO。
4.5 设备树中的 GPIO 描述
在设备树中,GPIO 引脚通过 gpios 或 xxx-gpios 属性描述,格式为:
// 语法:<&控制器节点 引脚偏移 标志位>
// 标志位常用值:
// GPIO_ACTIVE_HIGH = 0
// GPIO_ACTIVE_LOW = 1
gpios = <&gpio0 15 GPIO_ACTIVE_LOW>;
// 多个 GPIO 示例
button-gpios = <&gpio0 15 GPIO_ACTIVE_LOW>,
<&gpio1 3 GPIO_ACTIVE_HIGH>;
设备树与 API 的对应关系:驱动通过
of_get_named_gpio() 解析设备树属性,返回全局 GPIO 号,再传入 gpio_request() 等 API 使用。
4.6 完整示例:按键 GPIO 初始化
#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/interrupt.h>
static int button_gpio;
static int button_irq;
static irqreturn_t button_isr(int irq, void *dev_id) {
int val = gpio_get_value(button_gpio);
pr_info("Button pressed, value=%d\n", val);
return IRQ_HANDLED;
}
static int __init button_init(void) {
struct device_node *np;
int ret;
// 1. 获取设备树节点
np = of_find_node_by_path("/button");
if (!np)
return -ENODEV;
// 2. 解析 GPIO
button_gpio = of_get_named_gpio(np, "gpios", 0);
if (!gpio_is_valid(button_gpio)) {
pr_err("Invalid GPIO\n");
return -EINVAL;
}
// 3. 申请 GPIO
ret = gpio_request(button_gpio, "button");
if (ret) {
pr_err("gpio_request failed\n");
return ret;
}
// 4. 设置输入方向
gpio_direction_input(button_gpio);
// 5. 获取中断号
button_irq = gpio_to_irq(button_gpio);
if (button_irq < 0) {
gpio_free(button_gpio);
return button_irq;
}
// 6. 注册中断
ret = request_irq(button_irq, button_isr,
IRQF_TRIGGER_FALLING, "button", NULL);
if (ret) {
gpio_free(button_gpio);
return ret;
}
pr_info("Button driver loaded, gpio=%d, irq=%d\n",
button_gpio, button_irq);
return 0;
}
static void __exit button_exit(void) {
free_irq(button_irq, NULL);
gpio_free(button_gpio);
pr_info("Button driver unloaded\n");
}
module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");
4.7 新旧 API 对比
| 功能 | 传统 API(整数 GPIO 号) | 新 API(描述符 gpiod) |
|---|---|---|
| 申请 | gpio_request() |
devm_gpiod_get() |
| 设置输入 | gpio_direction_input() |
gpiod_direction_input() |
| 读取值 | gpio_get_value() |
gpiod_get_value() |
| 获取中断 | gpio_to_irq() |
gpiod_to_irq() |
| 设备树解析 | of_get_named_gpio() |
devm_gpiod_get_index() |
| 资源管理 | 手动释放 | devm 自动管理 |
建议:新项目优先使用
gpiod 描述符 API,它提供类型安全、自动资源管理和更简洁的设备树集成。但理解传统 API 对阅读旧代码和调试仍然重要。
4.8 本章小结
- GPIO 子系统分为消费者层、核心层和控制器层,驱动开发主要与消费者层 API 交互。
- 全局 GPIO 号由控制器基号和引脚偏移组成,设备树中使用
<控制器 偏移 标志>描述。 - 核心 API 包括
gpio_request()、gpio_direction_input()、gpio_get_value()、gpio_to_irq()等。 - 按键驱动典型流程:解析设备树 → 申请 GPIO → 设置方向 → 获取中断号 → 注册中断。
- 新内核推荐使用
gpiod描述符 API,但传统 API 在维护旧驱动时仍不可或缺。