3. Linux GPIO子系统框架:GPIO子系统架构、GPIO chip与GPIO desc
好,咱们今天来聊聊GPIO子系统的骨架。说实话,我刚接触Linux内核驱动那会儿,看到GPIO子系统也是一头雾水。代码那么多,结构那么复杂,到底怎么串起来的?
其实你把它想成一个大型工厂就明白了。GPIO子系统就是工厂的管理层,负责调度所有的GPIO引脚。每个引脚就是一个工人,而GPIO chip就是车间主任。嗯,这个比喻可能不太准确,但意思到了。
3.1 GPIO子系统整体架构
先看一张图,这是我个人习惯——先看整体,再看细节。
看到没?GPIO子系统分了三层。最下面是硬件驱动层,中间是核心层,上面是应用层。每一层各司其职,互不干扰。
我个人觉得,理解这个架构的关键在于:核心层是胶水。它把底层的硬件差异全部抹平了,给上层提供统一的接口。你写驱动的时候,根本不用关心底层是哪个SoC的GPIO,还是I2C扩展的GPIO芯片。
3.2 GPIO chip——车间主任
gpio_chip 是什么?说白了,它就是GPIO控制器的软件抽象。每个GPIO控制器对应一个 gpio_chip 结构体。
我记得第一次看这个结构体的时候,觉得字段好多。但用多了就发现,核心的就那么几个:
struct gpio_chip {
const char *label; // 名字,调试用
struct gpio_device *gpiodev; // 指向gpio设备
struct device *parent; // 父设备
struct module *owner; // 所属模块
int base; // 起始编号(已废弃,建议用-1)
u16 ngpio; // 引脚数量
const char *const *names; // 引脚名字数组
// 核心操作函数
int (*request)(struct gpio_chip *chip, unsigned offset);
void (*free)(struct gpio_chip *chip, unsigned offset);
int (*get_direction)(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);
// 中断相关
int (*to_irq)(struct gpio_chip *chip, unsigned offset);
// 内部数据
struct gpio_desc *desc; // 描述符数组
// ... 其他字段
};
关键点:你注册一个 gpio_chip 时,核心层会自动帮你分配 gpio_desc 数组。每个引脚对应一个描述符。你只需要实现那几个操作函数就行。
这里有个坑,我踩过。就是 base 字段。以前老代码喜欢手动指定起始编号,比如 base = 200。但新内核已经不建议这么干了。你设成 -1,让核心层自动分配,省心又安全。
3.3 GPIO desc——每个引脚的身份证
gpio_desc 是每个GPIO引脚在核心层的代言人。它长这样:
struct gpio_desc {
struct gpio_device *gdev; // 所属gpio设备
unsigned long flags; // 标志位
const char *label; // 使用者标签
int owner; // 谁在用
// ... 还有别的,但核心就这些
};
你想想看,每次你调用 gpiod_get() 拿到的是什么?就是这个 gpio_desc 指针。它背后关联着具体的 gpio_chip 和引脚偏移量。
我曾经在调试一个中断问题时,发现 flags 字段里有个 FLAG_IRQ_IS_ENABLED 位。当时查了半天文档才明白,这个位标记了中断是否已使能。嗯,细节都在这些标志位里。
小技巧:调试时可以用 /sys/kernel/debug/gpio 查看所有gpio_desc的状态。包括谁在占用、方向是什么、当前电平。这个debugfs接口救过我很多次。
3.4 注册流程——从chip到desc的完整链路
咱们走一遍注册流程,你就全明白了:
- 分配gpio_chip:驱动里定义一个
gpio_chip结构体,填好操作函数。 - 调用gpiochip_add_data():核心层收到请求后,会做几件事:
- 分配
gpio_device结构体 - 分配
gpio_desc数组(数量 = ngpio) - 建立
gpio_chip → gpio_device → gpio_desc的关联 - 分配全局GPIO编号(如果base设为-1)
- 注册到内核的GPIO编号空间
- 分配
- 导出到sysfs:在
/sys/class/gpio下创建对应目录。
看个实际例子,这是我之前写的一个GPIO扩展芯片驱动的片段:
static int my_gpio_probe(struct platform_device *pdev)
{
struct gpio_chip *chip;
int ret;
chip = devm_kzalloc(&pdev->dev, sizeof(*chip), GFP_KERNEL);
if (!chip)
return -ENOMEM;
chip->label = "my-gpio-expander";
chip->parent = &pdev->dev;
chip->owner = THIS_MODULE;
chip->base = -1; // 自动分配,别手动设!
chip->ngpio = 16; // 16个引脚
chip->direction_input = my_gpio_direction_input;
chip->direction_output = my_gpio_direction_output;
chip->get = my_gpio_get;
chip->set = my_gpio_set;
chip->to_irq = my_gpio_to_irq;
ret = devm_gpiochip_add_data(&pdev->dev, chip, NULL);
if (ret) {
dev_err(&pdev->dev, "Failed to add gpio chip\n");
return ret;
}
dev_info(&pdev->dev, "GPIO expander registered, %d pins\n", chip->ngpio);
return 0;
}
注意:老接口 gpiochip_add() 已经废弃了。新代码一律用 gpiochip_add_data() 或 devm_gpiochip_add_data()。后者带资源管理,probe失败或remove时会自动清理,省得你写一堆错误处理。
3.5 中断触发——让GPIO会说话
GPIO最常用的场景之一就是中断。你想想看,按键按下、传感器触发、外设通知……这些都需要GPIO中断。
在GPIO子系统中,中断的注册路径是这样的:
// 在gpio_chip中实现to_irq回调
static int my_gpio_to_irq(struct gpio_chip *chip, unsigned offset)
{
struct my_gpio_dev *dev = gpiochip_get_data(chip);
// 返回这个引脚对应的中断号
return dev->irq_base + offset;
}
// 使用者通过gpiod_to_irq()获取中断号
int irq = gpiod_to_irq(desc);
ret = request_threaded_irq(irq, NULL, my_handler,
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
"my-gpio-key", dev);
这里有个细节我特别想强调:中断触发类型。你可以在 gpio_chip 里设置支持的触发类型,也可以在设备树里配。我个人习惯在设备树里配,这样更灵活。
设备树里这样写:
gpio-keys {
compatible = "gpio-keys";
button@0 {
label = "Power";
gpios = &mygpio 3 GPIO_ACTIVE_LOW;
linux,code = <KEY_POWER>;
gpio-key,wakeup;
interrupt-parent = &mygpio;
interrupts = <3 IRQ_TYPE_EDGE_FALLING>;
};
};
我曾经遇到过一个坑:中断触发类型没配对,导致按键按下时触发两次中断。后来查出来是设备树里 IRQ_TYPE_EDGE_BOTH 和实际硬件不匹配。改成 IRQ_TYPE_EDGE_FALLING 就好了。
3.6 总结一下
| 概念 | 对应角色 | 核心作用 |
|---|---|---|
| gpio_chip | 车间主任 | 管理一组GPIO引脚,提供硬件操作接口 |
| gpio_desc | 工人身份证 | 描述单个引脚的状态、标签、使用者 |
| gpiolib | 工厂管理层 | 统一调度,提供标准API |
| gpiod_* API | 操作指令 | 获取、设置、方向控制、中断绑定 |
说白了,GPIO子系统就是帮你把硬件差异藏起来。你写驱动的时候,只需要关心 gpio_chip 的操作函数怎么实现,剩下的核心层全包了。这个设计思想,在Linux内核里随处可见——抽象、分层、复用。
嗯,今天就聊到这儿。记住一句话:gpio_chip是根,gpio_desc是叶,gpiolib是树干。理解了这三者的关系,GPIO驱动开发你就入门了。
核心要点回顾:
- GPIO子系统分三层:硬件驱动层、核心层、应用层
- gpio_chip 抽象硬件控制器,需实现 request/get/set/direction 等回调
- gpio_desc 是每个引脚的描述符,包含状态和标签
- 注册时用 devm_gpiochip_add_data(),base 设为 -1 自动分配
- 中断通过 to_irq 回调 + gpiod_to_irq() 获取中断号
- 调试用 /sys/kernel/debug/gpio 查看所有引脚状态