4. GPIO子系统概述:Linux GPIO子系统架构、GPIO控制器与GPIO号
好,我们进入GPIO子系统的正题了。
说实话,GPIO是嵌入式开发里最基础、也最绕不开的模块。你点个灯、读个按键、控制个外设使能,几乎都离不开它。但很多工程师用了好几年GPIO,对背后的子系统架构还是一知半解。今天我就带你把它彻底理清楚。
4.1 Linux GPIO子系统架构
Linux的GPIO子系统,说白了就是一套标准化的接口框架。它把千奇百怪的硬件GPIO控制器,封装成统一的API给驱动开发者用。你想想看,没有这个框架的话,每换一款SoC,GPIO的操作方式都得重学,那得多痛苦。
整个架构分三层:
- 硬件层:就是SoC内部的GPIO控制器,或者外接的GPIO扩展芯片(比如PCA9535)。
- 内核层:GPIO子系统核心代码,提供gpio_request()、gpio_direction_output()、gpio_set_value()这些标准API。
- 驱动层:具体GPIO控制器的驱动,负责实现底层寄存器操作。
我在项目中遇到过一种情况:某款芯片的GPIO控制器比较特殊,不支持中断触发沿的动态配置。当时我不得不写了一个小补丁,在gpiolib的irq_chip结构体里做了一些hack。嗯,这种时候你就知道,理解架构层次有多重要了。
核心要点:GPIO子系统是典型的"分层抽象"设计。上层驱动不用关心底层寄存器怎么配,只要调用标准API就行。底层驱动只需要实现gpio_chip结构体里的回调函数。
4.2 GPIO控制器与gpio_chip结构体
每个GPIO控制器在内核里都对应一个 struct gpio_chip 结构体。这个结构体就是硬件和内核之间的桥梁。
我挑几个关键字段说说:
| 字段 | 说明 |
|---|---|
label |
控制器名字,调试的时候很有用 |
ngpio |
这个控制器管理多少个GPIO |
base |
全局GPIO号的起始编号 |
request |
申请GPIO时的回调 |
free |
释放GPIO时的回调 |
direction_input |
设置输入方向 |
direction_output |
设置输出方向并设置初始值 |
get |
读取GPIO电平 |
set |
设置GPIO电平 |
举个例子,假设你有一个GPIO扩展芯片挂在I2C总线上,驱动里会这样注册:
static struct gpio_chip my_expander_chip = {
.label = "my-i2c-expander",
.ngpio = 8,
.base = -1, // 动态分配
.request = my_expander_request,
.free = my_expander_free,
.direction_input = my_expander_dir_in,
.direction_output = my_expander_dir_out,
.get = my_expander_get,
.set = my_expander_set,
.can_sleep = true, // I2C操作可能睡眠
};
个人习惯:我一般把base设为-1,让内核动态分配GPIO号。这样可以避免不同板子间的编号冲突。除非你有非常明确的硬件设计约束,否则别手写固定base值。
4.3 GPIO号的迷思
GPIO号,是很多新手最容易搞混的地方。我刚开始学的时候也踩过坑。
Linux内核里,每个GPIO都有一个全局唯一的编号,范围从0到最大支持的GPIO数。这个编号是怎么来的?
假设你有两个GPIO控制器:
- 控制器A:管理GPIO0~GPIO31,base=0
- 控制器B:管理GPIO32~GPIO63,base=32
那么,控制器B的第0号引脚,全局编号就是32。你在设备树里看到的 gpios = <&gpio2 5 GPIO_ACTIVE_LOW>,这里的5是控制器内部的偏移量,不是全局号。
我曾经踩过的坑:在用户空间通过/sys/class/gpio操作时,直接用了设备树里的偏移量5,结果操作的完全是另一个引脚。后来才意识到,sysfs里用的是全局GPIO号。那次排查花了我整整一个下午。
为什么会这样设计?说白了,就是为了解耦。驱动开发者只需要关心自己控制器内部的偏移量,不用管全局编号怎么分配。内核的gpiolib会自动帮你做映射。
4.4 设备树中的GPIO描述
设备树里描述GPIO的方式,其实很直观。看一个典型例子:
&i2c1 {
status = "okay";
temp_sensor: temperature-sensor@48 {
compatible = "ti,tmp102";
reg = <0x48>;
interrupt-parent = <&gpio1>;
interrupts = <15 IRQ_TYPE_EDGE_RISING>;
};
};
leds {
compatible = "gpio-leds";
led0 {
gpios = <&gpio2 5 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
这里 &gpio2 5 表示:使用gpio2这个控制器,偏移量是5。内核解析时,会找到gpio2对应的gpio_chip,用它的base加上5,得到全局GPIO号。
我个人习惯在设备树里把GPIO相关的节点注释写清楚,比如:
// GPIO2_5 对应原理图上的 J9-3 引脚,用于控制电源使能
别小看这个习惯。项目做到后期,硬件改版、引脚重分配的时候,这些注释能救你的命。
4.5 用户空间的GPIO访问
虽然我们做内核驱动,但有时候调试阶段需要在用户空间快速验证GPIO功能。Linux提供了两种方式:
- sysfs接口(老方式,但简单):
/sys/class/gpio/ - chardev接口(新方式,推荐):
/dev/gpiochipN
我建议你尽快切换到chardev方式。sysfs接口虽然用起来方便,但已经被标记为deprecated了。而且chardev支持更丰富的操作,比如批量读写、事件监听等。
举个例子,用libgpiod库操作GPIO:
# 读取GPIO状态
gpioinfo gpiochip0
# 设置GPIO13为输出并输出高电平
gpioset gpiochip0 13=1
# 监听GPIO4的上升沿中断
gpiomon --rising-edge gpiochip0 4
一句话总结:GPIO子系统把复杂的硬件操作抽象成了简单的API调用。你只要理解gpio_chip、GPIO号映射、设备树描述这三块,就能应对绝大多数场景。
下一章,我们会深入PinCtrl子系统。到时候你会发现,GPIO和PinCtrl其实是密不可分的两兄弟。嗯,先消化今天的内容吧。