3. GPIO子系统基础
3.1 GPIO硬件原理与电气特性
GPIO(General Purpose Input/Output)是嵌入式系统中最基础、最灵活的硬件接口。理解其硬件原理是编写正确驱动的前提。
3.1.1 GPIO内部结构
一个典型的GPIO引脚内部包含以下关键单元:
- 输出驱动器:推挽(Push-Pull)或开漏(Open-Drain)结构,决定引脚驱动能力。
- 输入缓冲器:施密特触发器或TTL电平缓冲,用于读取外部信号。
- 上拉/下拉电阻:可配置的内部弱上拉或弱下拉,防止引脚浮空。
- 复用选择器:将引脚连接到GPIO控制器或其它外设(如UART、I2C、SPI)。
- 中断检测逻辑:支持上升沿、下降沿、高电平、低电平触发。
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子系统分为以下三层:
- GPIO控制器驱动层:由芯片厂商实现,操作硬件寄存器,注册gpio_chip结构体。
- gpiolib核心层:管理所有GPIO控制器,提供统一的编号分配、请求/释放、读写接口。
- GPIO使用者层:驱动开发者通过gpiolib API或GPIO consumer接口使用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会根据标志自动反转电平,驱动开发者无需关心硬件电平极性。