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 接口;控制器层由具体芯片驱动实现,操作硬件寄存器。
GPIO 子系统三层架构 GPIO 子系统三层架构 消费者层(Consumer Layer) 驱动开发者调用:gpio_request() / gpio_direction_input() / gpio_get_value() 核心层(Core Layer) GPIO 号分配 / 中断映射 / sysfs 接口 / 描述符管理 控制器层(Controller Layer) 芯片驱动实现:方向设置 / 数据读写 / 中断控制(如 gpio-omap, gpio-rockchip) 消费者层 API 通过核心层间接调用控制器层硬件操作,实现跨平台兼容

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 按键驱动初始化流程如下:

  1. 解析设备树:获取 GPIO 编号和标志(如上升沿/下降沿触发)。
  2. 申请 GPIO:调用 gpio_request() 确保引脚未被占用。
  3. 设置方向:按键场景调用 gpio_direction_input()。
  4. 获取中断号:调用 gpio_to_irq() 将 GPIO 转换为中断号。
  5. 注册中断:调用 request_irq() 绑定中断处理函数。
  6. 释放资源:在驱动卸载时依次释放中断和 GPIO。
GPIO 按键驱动初始化流程 GPIO 按键驱动初始化流程 1. 解析设备树获取 GPIO 编号 2. gpio_request() 申请引脚 3. gpio_direction_input() 4. gpio_to_irq() 获取中断号 5. request_irq() 绑定处理函数 驱动卸载时按相反顺序释放:free_irq() → gpio_free()

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 在维护旧驱动时仍不可或缺。