第三章 GPIO子系统与LED驱动:GPIO子系统框架、设备树GPIO描述、实现一个简单的LED驱动、用户空间控制接口
3.1 GPIO子系统框架——内核里的“万能接口”
做嵌入式开发这么多年,我越来越觉得GPIO是个神奇的东西。说白了,它就是芯片上那些可以拉高拉低的引脚。但你别小看它,从点亮一个LED到控制电机,从读取按键到模拟I2C协议,几乎每个外设都离不开GPIO。
在Linux内核里,GPIO被抽象成了一个子系统。为什么要这么做?你想想看,不同厂家的芯片,GPIO控制寄存器地址不一样,操作方式也不同。如果没有统一框架,驱动开发者每换一个平台就得重写一遍代码,那得多痛苦。
GPIO子系统的核心思想就四个字:分层抽象。它把硬件相关的部分封装在底层,向上提供统一的API。我习惯把它的架构分成三层:
- GPIO控制器驱动层:这是芯片厂商要干的活。他们需要实现gpio_chip结构体,注册到内核。说白了就是告诉内核:“我这颗芯片有32个GPIO,地址从0x1000开始,读写的函数我都写好了。”
- GPIO核心层:内核自己维护的中间层。它管理所有注册进来的gpio_chip,提供gpio_request()、gpio_direction_output()这些通用接口。
- GPIO使用者层:也就是我们驱动开发者调用的地方。你不需要关心底层寄存器,直接调用gpio_set_value()就能控制引脚电平。
核心数据结构:gpio_chip
每个GPIO控制器对应一个gpio_chip实例。里面包含了该控制器的基本信息,比如基地址、引脚数量、操作函数指针等。我刚开始接触时,总觉得这个结构体字段太多,后来发现其实常用的就那几个。
struct gpio_chip {
const char *label;
struct device *parent;
int base; // GPIO编号的起始值
int ngpio; // 该控制器管理的GPIO数量
int (*request)(struct gpio_chip *chip, unsigned offset);
int (*direction_input)(struct gpio_chip *chip, unsigned offset);
int (*direction_output)(struct gpio_chip *chip, unsigned offset, int value);
int (*get)(struct gpio_chip *chip, unsigned offset);
void (*set)(struct gpio_chip *chip, unsigned offset, int value);
// ... 还有其他字段,但上面这些是最核心的
};
嗯,这里要注意:base字段决定了这个控制器的GPIO在系统中的编号范围。比如base=100,ngpio=32,那这个控制器的引脚编号就是100到131。不过现在内核推荐用动态分配,让系统自动分配base值。
3.2 设备树中的GPIO描述——硬件信息的“身份证”
设备树(Device Tree)是什么?我打个比方:它就像硬件的身份证,告诉内核“我这块板子上有哪些外设,它们连在哪个GPIO上”。在ARM平台下,设备树几乎是标配了。
描述一个GPIO引脚,通常需要三个信息:GPIO控制器、引脚编号、引脚标志。设备树里用gpios或<name>-gpios属性来描述。举个例子:
// 设备树节点示例
led-red {
compatible = "gpio-leds";
label = "red-led";
gpios = <&gpio1 15 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
这里&gpio1引用的是GPIO控制器节点,15是引脚编号,GPIO_ACTIVE_HIGH表示高电平有效。我在项目中遇到过一个问题:有人把GPIO_ACTIVE_HIGH写成了GPIO_ACTIVE_LOW,结果LED死活不亮,查了半天才发现是电平极性搞反了。
设备树里还有几个常用的宏定义:
| 宏定义 | 值 | 说明 |
|---|---|---|
| GPIO_ACTIVE_HIGH | 0 | 高电平有效 |
| GPIO_ACTIVE_LOW | 1 | 低电平有效 |
| GPIO_OPEN_DRAIN | 2 | 开漏输出 |
| GPIO_OPEN_SOURCE | 4 | 开源输出 |
小技巧:在设备树里描述GPIO时,我建议把引脚编号和控制器分开写,不要硬编码。比如用&gpio1 15而不是直接写47(假设gpio1的base是32)。这样代码可读性更高,也方便移植。
3.3 实现一个简单的LED驱动——从零开始点亮一盏灯
好了,理论说完了,咱们动手写个驱动。这个驱动要做的事很简单:控制一个LED的亮灭。但麻雀虽小五脏俱全,它包含了GPIO驱动开发的完整流程。
我个人的习惯是,先看硬件原理图。比如LED接在GPIO1_15上,高电平点亮。那驱动里要做的就是:请求GPIO、设置方向为输出、设置电平。
先看完整的驱动代码:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>
struct led_device {
struct gpio_desc *led_gpio;
};
static int led_probe(struct platform_device *pdev)
{
struct led_device *led;
struct device *dev = &pdev->dev;
led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL);
if (!led)
return -ENOMEM;
// 从设备树获取GPIO描述符
led->led_gpio = devm_gpiod_get(dev, NULL, GPIOD_OUT_LOW);
if (IS_ERR(led->led_gpio)) {
dev_err(dev, "Failed to get GPIO\n");
return PTR_ERR(led->led_gpio);
}
// 设置GPIO方向为输出,初始值为低电平
gpiod_direction_output(led->led_gpio, 0);
platform_set_drvdata(pdev, led);
// 点亮LED
gpiod_set_value(led->led_gpio, 1);
dev_info(dev, "LED driver initialized\n");
return 0;
}
static int led_remove(struct platform_device *pdev)
{
struct led_device *led = platform_get_drvdata(pdev);
// 熄灭LED
gpiod_set_value(led->led_gpio, 0);
dev_info(&pdev->dev, "LED driver removed\n");
return 0;
}
static const struct of_device_id led_of_match[] = {
{ .compatible = "my-led-driver" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, led_of_match);
static struct platform_driver led_driver = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "my_led_driver",
.of_match_table = led_of_match,
},
};
module_platform_driver(led_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple LED driver using GPIO descriptor");
这段代码里,我用了devm_gpiod_get()而不是老式的gpio_request()。为什么?因为devm_系列函数是设备资源管理接口,它会自动释放资源,省去了手动释放的麻烦。我曾经在项目里因为忘记调用gpio_free()导致内存泄漏,后来全部改用devm_系列,再也没出过问题。
对应的设备树节点要这样写:
my-led {
compatible = "my-led-driver";
gpios = <&gpio1 15 GPIO_ACTIVE_HIGH>;
};
注意:设备树里的compatible属性必须和驱动里的of_device_id完全匹配,大小写都不能错。我见过有人把"my-led-driver"写成了"my_led_driver",结果驱动加载时根本匹配不上,内核日志里也找不到任何错误信息,排查起来特别费劲。
3.4 用户空间控制接口——让应用程序也能操作GPIO
驱动写好了,但怎么让用户空间的程序来控制LED呢?Linux提供了几种方式:
- /sys/class/gpio:老式的sysfs接口,现在逐渐被淘汰了。
- /dev/gpiochipX:通过ioctl操作,更灵活,但需要写C程序。
- libgpiod:新式的用户空间库,推荐使用。
我个人推荐用libgpiod。它提供了命令行工具和C库,用起来非常方便。比如控制LED亮灭:
# 查看系统中的GPIO芯片
gpiodetect
# 查看gpiochip0的引脚信息
gpioinfo gpiochip0
# 设置gpiochip0的第15脚为输出,并输出高电平
gpioset gpiochip0 15=1
# 读取gpiochip0的第15脚电平
gpioget gpiochip0 15
如果你想在应用程序里控制,可以这样写:
#include <gpiod.h>
#include <stdio.h>
int main(void)
{
struct gpiod_chip *chip;
struct gpiod_line *line;
int ret;
// 打开GPIO芯片
chip = gpiod_chip_open_by_number(0);
if (!chip) {
perror("Open chip failed");
return -1;
}
// 获取第15脚
line = gpiod_chip_get_line(chip, 15);
if (!line) {
perror("Get line failed");
gpiod_chip_close(chip);
return -1;
}
// 设置为输出,初始值高电平
ret = gpiod_line_request_output(line, "my-app", 1);
if (ret < 0) {
perror("Request output failed");
gpiod_chip_close(chip);
return -1;
}
// 设置低电平(熄灭LED)
gpiod_line_set_value(line, 0);
// 清理
gpiod_line_release(line);
gpiod_chip_close(chip);
return 0;
}
编译时需要链接libgpiod:gcc -o led_control led_control.c -lgpiod
避坑指南:我曾经在项目里直接用system()调用gpioset命令来控制LED,结果发现每次调用都有几百毫秒的延迟。后来改用libgpiod的C库接口,延迟降到了微秒级。所以,如果你对性能有要求,千万别偷懒用命令行工具。
好了,这一章的内容就到这里。从GPIO子系统的框架,到设备树的描述,再到驱动实现和用户空间接口,我们完整地走了一遍。下一章我会讲中断子系统,那是处理按键、传感器等事件的关键技术,敬请期待。