4. GPIO 子系统基础:架构与核心 API

GPIO(General Purpose Input/Output)是嵌入式 Linux 驱动开发中最基础也最常用的外设接口。Linux 内核通过 GPIO 子系统 对 SoC 或外部 GPIO 控制器进行统一抽象,为驱动开发者提供一套与硬件平台无关的 API。本章将深入剖析 GPIO 子系统的分层架构,并详细讲解 gpio_request、gpio_direction_output、gpio_set_value 等核心 API 的用法与注意事项。

4.1 GPIO 子系统分层架构

Linux GPIO 子系统采用经典的分层设计,从上到下依次为:GPIO 消费者层、GPIO 核心层 和 GPIO 控制器驱动层。这种架构使得驱动开发者无需关心底层硬件细节,只需调用统一的 API 即可操作 GPIO。

架构要点: 消费者层调用核心 API → 核心层通过 gpio_chip 回调 → 控制器驱动操作硬件寄存器。
Linux GPIO 子系统分层架构 GPIO 消费者层 驱动模块 / 用户空间 (sysfs / libgpiod) GPIO 核心层 (gpiolib) gpio_chip 注册 / 中断映射 / 描述符管理 GPIO 控制器驱动层 实现 gpio_chip 结构体 / 操作硬件寄存器 硬件:SoC GPIO / 外部 GPIO 扩展芯片 API 调用方向

从上图可以看出,驱动开发者主要工作在 消费者层,通过核心层提供的 API 间接操作底层硬件。核心层(gpiolib)负责管理所有已注册的 GPIO 控制器,并提供统一的编号空间和描述符接口。

4.2 核心数据结构:gpio_chip

每个 GPIO 控制器在 Linux 内核中对应一个 struct gpio_chip 实例。该结构体封装了控制器的属性与操作函数指针。下表列出了最关键的字段:

字段名 类型 说明
label const char * 控制器名称,用于调试和日志
ngpio int 该控制器管理的 GPIO 数量
base int 全局 GPIO 编号的起始值(已弃用,推荐使用动态分配)
request int (*)(struct gpio_chip *, unsigned offset) 申请 GPIO 时的回调
free void (*)(struct gpio_chip *, unsigned offset) 释放 GPIO 时的回调
direction_input int (*)(struct gpio_chip *, unsigned offset) 设置 GPIO 为输入方向
direction_output int (*)(struct gpio_chip *, unsigned offset, int value) 设置 GPIO 为输出方向并指定初始电平
get int (*)(struct gpio_chip *, unsigned offset) 读取 GPIO 当前电平
set void (*)(struct gpio_chip *, unsigned offset, int value) 设置 GPIO 输出电平
提示: 在编写 GPIO 控制器驱动时,开发者只需填充 gpio_chip 结构体并调用 gpiochip_add_data() 注册即可。核心层会自动处理编号分配和消费者 API 的路由。

4.3 核心 API 详解

对于驱动开发者而言,最常用的是 GPIO 消费者 API。以下四个函数构成了 GPIO 操作的基本流程:申请 → 设置方向 → 读写值 → 释放。

4.3.1 gpio_request

函数原型:

int gpio_request(unsigned gpio, const char *label);

功能: 申请对一个 GPIO 编号的独占使用权。在操作 GPIO 之前必须调用此函数,否则内核会发出警告。

  • gpio: 全局 GPIO 编号(通常由平台代码或设备树指定)。
  • label: 为 GPIO 指定的名称,用于调试和 /sys/kernel/debug/gpio 输出。
  • 返回值: 成功返回 0,失败返回负的错误码(如 -EBUSY 表示已被占用)。
注意: 在设备树时代,推荐使用 devm_gpio_request() 或 gpiod_get() 等资源管理版本,可以自动释放,避免遗漏 gpio_free()。

4.3.2 gpio_direction_output

函数原型:

int gpio_direction_output(unsigned gpio, int value);

功能: 将 GPIO 设置为输出方向,并同时设置初始输出电平。

  • value: 初始输出值,0 表示低电平,非 0 表示高电平。
  • 返回值: 成功返回 0,失败返回负错误码。

对应的输入方向设置函数为:

int gpio_direction_input(unsigned gpio);

4.3.3 gpio_set_value 与 gpio_get_value

函数原型:

void gpio_set_value(unsigned gpio, int value);
int  gpio_get_value(unsigned gpio);

功能: 设置或读取 GPIO 的电平值。

  • gpio_set_value 用于输出 GPIO,value 为 0 或非 0。
  • gpio_get_value 用于输入 GPIO,返回 0 或 1。
性能提示: 在中断上下文或需要高性能的场景下,应使用 gpio_set_value_cansleep() 和 gpio_get_value_cansleep() 变体,它们允许 GPIO 控制器通过 I2C/SPI 等慢速总线访问。

4.3.4 gpio_free

函数原型:

void gpio_free(unsigned gpio);

功能: 释放之前通过 gpio_request 申请的 GPIO,使该编号可被其他模块使用。通常在驱动卸载或模块移除时调用。

4.4 完整操作流程示例

以下代码展示了一个典型的 GPIO 输出操作流程,用于控制一个 LED:

#include <linux/gpio.h>
#include <linux/delay.h>

#define LED_GPIO  17   // 假设 GPIO 17 连接 LED

static int __init led_init(void)
{
    int ret;

    // 1. 申请 GPIO
    ret = gpio_request(LED_GPIO, "my-led");
    if (ret) {
        pr_err("Failed to request GPIO %d: %d\n", LED_GPIO, ret);
        return ret;
    }

    // 2. 设置为输出,初始低电平(LED 灭)
    ret = gpio_direction_output(LED_GPIO, 0);
    if (ret) {
        pr_err("Failed to set direction for GPIO %d\n", LED_GPIO);
        gpio_free(LED_GPIO);
        return ret;
    }

    // 3. 操作:闪烁 3 次
    for (int i = 0; i < 3; i++) {
        gpio_set_value(LED_GPIO, 1);  // LED 亮
        msleep(500);
        gpio_set_value(LED_GPIO, 0);  // LED 灭
        msleep(500);
    }

    return 0;
}

static void __exit led_exit(void)
{
    // 4. 释放 GPIO
    gpio_set_value(LED_GPIO, 0);  // 确保关闭 LED
    gpio_free(LED_GPIO);
}

module_init(led_init);
module_exit(led_exit);

4.5 新旧 API 对比:legacy 与 descriptor

Linux 内核 GPIO 子系统存在两套 API:legacy 整数编号 API(如上文所述)和 descriptor 描述符 API(基于 struct gpio_desc *)。下表对比了二者的关键差异:

对比维度 Legacy API (gpio_*) Descriptor API (gpiod_*)
标识方式 全局整数编号 struct gpio_desc * 指针
设备树匹配 需手动解析 gpios 属性 自动通过 gpiod_get() 匹配
资源管理 需手动 gpio_free 支持 devm_gpiod_get 自动释放
中断支持 gpio_to_irq() gpiod_to_irq()
推荐程度 旧代码兼容,新开发不推荐 新开发首选
建议: 本课程后续章节将主要使用 Descriptor API,因为它更安全、更符合设备树驱动模型。但理解 Legacy API 对于维护旧代码和阅读内核源码仍然非常重要。
Legacy API 与 Descriptor API 调用流程对比 Legacy API 流程 gpio_request(gpio, label) gpio_direction_output(gpio, val) gpio_set_value(gpio, val) gpio_free(gpio) Descriptor API 流程 struct gpio_desc *desc; desc = gpiod_get(dev, "led", GPIOD_OUT_LOW) gpiod_set_value(desc, val) gpiod_put(desc) // 或 devm 自动释放 核心区别:Legacy 使用整数编号,Descriptor 使用指针,后者更安全且支持设备树自动匹配

4.6 调试与验证

在开发过程中,可以通过以下方式验证 GPIO 操作是否正确:

  • 查看 /sys/kernel/debug/gpio: 显示所有已注册的 GPIO 控制器和已申请的 GPIO 状态。
  • 使用 /sys/class/gpio(旧接口): 通过 export/unexport 文件手动操作 GPIO,适合快速验证。
  • 使用 libgpiod 工具(新接口): gpiodetect、gpioinfo、gpioget、gpioset 等命令行工具,与 Descriptor API 对应。
实战要点: 在编写 LED 驱动时,务必检查 gpio_request 和 gpio_direction_output 的返回值。许多初学者忽略错误检查,导致在 GPIO 被占用时出现难以调试的静默失败。