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 间接操作底层硬件。核心层(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_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() |
| 推荐程度 | 旧代码兼容,新开发不推荐 | 新开发首选 |
4.6 调试与验证
在开发过程中,可以通过以下方式验证 GPIO 操作是否正确:
- 查看 /sys/kernel/debug/gpio: 显示所有已注册的 GPIO 控制器和已申请的 GPIO 状态。
- 使用 /sys/class/gpio(旧接口): 通过 export/unexport 文件手动操作 GPIO,适合快速验证。
- 使用 libgpiod 工具(新接口):
gpiodetect、gpioinfo、gpioget、gpioset等命令行工具,与 Descriptor API 对应。
gpio_request 和 gpio_direction_output 的返回值。许多初学者忽略错误检查,导致在 GPIO 被占用时出现难以调试的静默失败。