3. GPIO子系统基础

3.1 GPIO硬件原理与电气特性

GPIO(General Purpose Input/Output)是嵌入式系统中最基础、最灵活的硬件接口。理解其硬件原理是编写正确驱动的前提。

3.1.1 GPIO内部结构

一个典型的GPIO引脚内部包含以下关键单元:

  • 输出驱动器:推挽(Push-Pull)或开漏(Open-Drain)结构,决定引脚驱动能力。
  • 输入缓冲器:施密特触发器或TTL电平缓冲,用于读取外部信号。
  • 上拉/下拉电阻:可配置的内部弱上拉或弱下拉,防止引脚浮空。
  • 复用选择器:将引脚连接到GPIO控制器或其它外设(如UART、I2C、SPI)。
  • 中断检测逻辑:支持上升沿、下降沿、高电平、低电平触发。
GPIO引脚内部硬件结构框图 GPIO引脚内部硬件结构框图 PAD 输出驱动器 输入缓冲器 上拉/下拉电阻 复用选择器 中断检测逻辑 控制寄存器 说明:PAD是物理焊盘,通过复用选择器决定连接至GPIO控制器还是其它外设。 输出驱动器决定驱动能力(2mA/4mA/8mA等),输入缓冲器决定电平阈值。

3.1.2 关键电气参数

参数 典型值 说明
VOH(输出高电平) VDD - 0.4V 输出高电平时的最小电压
VOL(输出低电平) 0.4V 输出低电平时的最大电压
VIH(输入高电平) 0.7 × VDD 被识别为高电平的最小输入电压
VIL(输入低电平) 0.3 × VDD 被识别为低电平的最大输入电压
Isource/Isink 2~16 mA 引脚可提供的拉电流/灌电流
上拉/下拉电阻 40~100 kΩ 内部弱上拉/下拉的典型阻值
实战提示:驱动LED时,注意GPIO的灌电流能力通常强于拉电流。建议采用低电平驱动方式:LED阳极接VDD,阴极经限流电阻接GPIO,GPIO输出低电平时点亮。

3.2 Linux GPIO子系统架构

Linux GPIO子系统采用分层架构设计,将硬件操作细节与驱动开发者隔离,提供统一的API接口。

3.2.1 架构层次

从底层到上层,GPIO子系统分为以下三层:

  1. GPIO控制器驱动层:由芯片厂商实现,操作硬件寄存器,注册gpio_chip结构体。
  2. gpiolib核心层:管理所有GPIO控制器,提供统一的编号分配、请求/释放、读写接口。
  3. GPIO使用者层:驱动开发者通过gpiolib API或GPIO consumer接口使用GPIO。
Linux GPIO子系统分层架构图 Linux GPIO子系统分层架构图 用户空间 (sysfs / libgpiod / 字符设备) 内核空间:GPIO使用者层 (Consumer) gpiolib 核心层 (gpio_desc / gpiolib.c) GPIO控制器驱动层 (gpio_chip / 硬件寄存器) 硬件层 (SoC GPIO模块 / 外部GPIO扩展芯片) 说明:箭头方向表示上层调用下层接口,下层向上层注册回调。

3.2.2 关键数据结构

gpiolib核心层使用两个核心数据结构管理GPIO:

数据结构 作用 关键字段
struct gpio_chip 描述一个GPIO控制器,包含操作硬件的方法 request, free, get, set, direction_input, direction_output, ngpio, base
struct gpio_desc 描述一个具体的GPIO引脚,包含描述符和标志 flags, label, chip, offset
架构设计要点:gpiolib通过gpio_chip的base和ngpio字段确定该控制器管理的GPIO编号范围。新内核(4.8+)已弃用全局编号,改用描述符(descriptor)接口,推荐使用gpiod_*系列API。

3.3 gpiolib API介绍

gpiolib提供两套API:传统整数编号API(legacy)和描述符API(descriptor-based)。新驱动开发应使用描述符API。

3.3.1 描述符API(推荐)

描述符API通过设备树或ACPI获取GPIO,返回struct gpio_desc *指针,操作更安全、更清晰。

函数 功能 返回值
gpiod_get() 根据设备树属性名获取GPIO描述符 struct gpio_desc * 或 ERR_PTR
gpiod_get_index() 获取带索引的GPIO描述符(用于GPIO数组) struct gpio_desc * 或 ERR_PTR
gpiod_direction_input() 设置GPIO为输入方向 0 成功,负值错误码
gpiod_direction_output() 设置GPIO为输出方向并设置初始值 0 成功,负值错误码
gpiod_get_value() 读取GPIO电平值 0 或 1
gpiod_set_value() 设置GPIO电平值 void
gpiod_to_irq() 获取GPIO对应的中断号 中断号,负值错误码
gpiod_put() 释放GPIO描述符 void

3.3.2 传统整数API(仅维护兼容)

函数 功能 说明
gpio_request() 申请GPIO引脚 需传入全局GPIO编号
gpio_free() 释放GPIO引脚 与request配对使用
gpio_direction_input() 设置输入方向 基于整数编号
gpio_direction_output() 设置输出方向 基于整数编号
gpio_get_value() 读取电平 基于整数编号
gpio_set_value() 设置电平 基于整数编号

3.3.3 实战代码示例:使用描述符API驱动LED

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>

struct my_led_data {
    struct gpio_desc *led_gpio;
};

static int my_led_probe(struct platform_device *pdev)
{
    struct my_led_data *data;
    int ret;

    data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    // 从设备树获取名为"led-gpios"的GPIO描述符
    data->led_gpio = gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW);
    if (IS_ERR(data->led_gpio)) {
        dev_err(&pdev->dev, "Failed to get LED GPIO\n");
        return PTR_ERR(data->led_gpio);
    }

    // 设置输出方向并输出高电平(点亮LED)
    gpiod_set_value(data->led_gpio, 1);

    platform_set_drvdata(pdev, data);
    dev_info(&pdev->dev, "LED GPIO driver probed successfully\n");
    return 0;
}

static int my_led_remove(struct platform_device *pdev)
{
    struct my_led_data *data = platform_get_drvdata(pdev);

    // 关闭LED并释放GPIO
    gpiod_set_value(data->led_gpio, 0);
    gpiod_put(data->led_gpio);
    return 0;
}

static const struct of_device_id my_led_of_match[] = {
    { .compatible = "mycompany,my-led" },
    { }
};
MODULE_DEVICE_TABLE(of, my_led_of_match);

static struct platform_driver my_led_driver = {
    .probe  = my_led_probe,
    .remove = my_led_remove,
    .driver = {
        .name = "my_led",
        .of_match_table = my_led_of_match,
    },
};
module_platform_driver(my_led_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Developer");
MODULE_DESCRIPTION("GPIO LED driver using descriptor API");
避坑指南:
  • 使用gpiod_get()时,设备树属性名不要加-gpios后缀,API会自动追加。例如设备树写led-gpios,代码中传"led"。
  • 初始化标志GPIOD_OUT_LOW或GPIOD_OUT_HIGH会在获取GPIO时直接设置方向和初始值,避免竞态。
  • 中断上下文中不能使用gpiod_get()/gpiod_put(),它们可能睡眠。

3.4 设备树中的GPIO描述

设备树是描述GPIO连接的标准方式。以下是一个典型示例:

/ {
    my_led {
        compatible = "mycompany,my-led";
        led-gpios = &gpio1 15 GPIO_ACTIVE_HIGH;
        status-gpios = &gpio2 3 GPIO_ACTIVE_LOW;
    };
};
字段 含义
led-gpios 属性名,对应gpiod_get(dev, "led", ...)中的第二个参数
&gpio1 15 GPIO控制器phandle + 引脚偏移(GPIO1控制器的第15号引脚)
GPIO_ACTIVE_HIGH 标志:高电平有效。API会自动处理电平反转逻辑
最佳实践:始终在设备树中明确GPIO_ACTIVE_HIGH或GPIO_ACTIVE_LOW标志。驱动代码中调用gpiod_set_value(desc, 1)时,gpiolib会根据标志自动反转电平,驱动开发者无需关心硬件电平极性。