GPIO 子系统基础:从硬件原理到 Linux 操作接口
GPIO 是嵌入式开发里最基础、也最绕不开的外设。说白了,它就是芯片上那些可以编程控制的引脚——你可以让它们输出高电平点个灯,也可以读它们的状态判断按键有没有按下。我刚开始做驱动时,总觉得 GPIO 太简单,不就是读写寄存器吗?后来踩了几个坑才明白,Linux 的 GPIO 子系统把这件事包装得挺讲究,理解透了,后面做 PWM、I2C 模拟、中断触发都会顺手很多。
GPIO 硬件原理:引脚内部长什么样?
先看硬件侧。一个 GPIO 引脚内部,通常包含这几个关键模块:
- 输出驱动器:推挽或开漏结构,决定引脚能输出什么电平。
- 输入缓冲器:施密特触发器或简单比较器,把引脚电压转成数字 0/1。
- 上拉/下拉电阻:内部可配置的弱上拉或弱下拉,防止引脚浮空。
- 复用选择器:决定这个引脚是 GPIO 还是其他功能(比如 UART TX)。
- 中断控制器:检测上升沿、下降沿或电平变化,产生中断信号。
全志 T113 的 GPIO 控制器叫 PIO(Port I/O),它管理着 9 组 Bank(PA~PI),每组最多 32 个引脚。每个引脚有 4 个配置寄存器:
| 寄存器 | 功能 | 位宽 |
|---|---|---|
| GPIO_CFGx | 引脚功能选择(输入/输出/外设复用) | 每引脚 4 位 |
| GPIO_DAT | 数据寄存器,读/写引脚电平 | 32 位 |
| GPIO_DRVx | 驱动能力配置(电流强度) | 每引脚 4 位 |
| GPIO_PULx | 上拉/下拉电阻配置 | 每引脚 2 位 |
Linux GPIO 子系统框架:三层架构
Linux 的 GPIO 子系统分三层,每层各司其职。我画了张图帮你理清关系:
这张图从左到右看,调用方向是从用户空间往下走到底层硬件。反过来,中断信号从硬件往上传递。我重点说下中间两层:
- gpiolib(核心层):提供
gpiod_get()、gpiod_set_value()等统一 API。它不关心底层是哪个芯片,只维护一个全局 GPIO 描述符表。 - pinctrl + gpio_chip(硬件抽象层):每个 GPIO 控制器注册一个
gpio_chip结构体,里面包含.request、.free、.direction_input、.direction_output、.get、.set等回调函数。pinctrl 负责引脚复用和电气属性配置。
设备树 GPIO 配置:怎么写才规范?
设备树是描述硬件拓扑的语言。GPIO 相关的节点,通常包含这几部分:
/* 全志 T113 设备树 GPIO 配置示例 */
&pio {
/* 引脚别名定义 */
led_pin: led_pin@0 {
pins = "PB2";
function = "gpio_out";
bias-pull-up;
drive-strength = <10>;
};
key_pin: key_pin@0 {
pins = "PH5";
function = "gpio_in";
bias-pull-up;
};
};
/* 使用 GPIO 的外设节点 */
led {
compatible = "gpio-leds";
status = "okay";
pinctrl-0 = <&led_pin>;
pinctrl-names = "default";
power_led {
gpios = <&pio 2 0 GPIO_ACTIVE_HIGH>; /* PB2, 高电平点亮 */
label = "power:blue";
default-state = "on";
};
};
button {
compatible = "gpio-keys";
status = "okay";
pinctrl-0 = <&key_pin>;
pinctrl-names = "default";
power_key {
gpios = <&pio 7 5 GPIO_ACTIVE_LOW>; /* PH5, 低电平按下 */
label = "power";
linux,code = <KEY_POWER>;
};
};
这里有几个容易搞混的地方,我单独列出来:
| 写法 | 含义 | 常见错误 |
|---|---|---|
&pio 2 0 |
Bank 2 (PB), 引脚偏移 0 | 把 Bank 编号和引脚号搞反 |
GPIO_ACTIVE_HIGH |
高电平表示"有效" | 忘记定义宏,直接写 0/1 |
bias-pull-up |
内部上拉 50kΩ | 输入引脚没配上拉,导致电平不稳 |
drive-strength |
输出驱动电流 (mA) | 设太大可能引起信号过冲 |
gpios = &pio 2 0 GPIO_ACTIVE_HIGH,但内核死活不认。查了半天发现是 pinctrl 节点里 function = "gpio_out" 写成了 "gpio"——少了个 _out,pinctrl 根本没把引脚切到 GPIO 模式。所以,pinctrl 和 gpios 两个属性必须同时配对,缺一不可。
GPIO 操作接口:sysfs vs chardev
Linux 提供了两种用户空间操作 GPIO 的方式。早期用 sysfs,现在推荐 chardev。我分别讲下。
sysfs 接口(传统方式)
sysfs 把每个 GPIO 暴露为一个文件。操作步骤很简单:
# 导出 GPIO
echo 66 > /sys/class/gpio/export
# 配置方向
echo out > /sys/class/gpio/gpio66/direction
# 写值
echo 1 > /sys/class/gpio/gpio66/value
# 读值
cat /sys/class/gpio/gpio66/value
# 用完释放
echo 66 > /sys/class/gpio/unexport
但 sysfs 有个问题:它用 GPIO 编号(全局整数)来标识引脚。不同内核版本、不同板子,编号可能不一样。而且它不支持中断、不支持批量操作。所以内核社区已经标记它为 deprecated。
chardev 接口(推荐方式)
chardev 接口通过 /dev/gpiochipN 设备文件操作 GPIO。它使用 libgpiod 库,API 更清晰:
# 查看 GPIO 芯片信息
gpioinfo
# 设置输出并写值
gpioset gpiochip0 2=1
# 读取输入
gpioget gpiochip0 5
# 监控中断事件
gpiomon --rising-edge gpiochip0 5
在 C 代码里,使用 libgpiod 的流程是这样的:
#include <gpiod.h>
struct gpiod_chip *chip;
struct gpiod_line *line;
int ret;
/* 打开 GPIO 芯片 */
chip = gpiod_chip_open_by_name("gpiochip0");
if (!chip) {
perror("gpiod_chip_open");
return -1;
}
/* 获取引脚(PB2 = chip0, offset 2) */
line = gpiod_chip_get_line(chip, 2);
if (!line) {
perror("gpiod_chip_get_line");
gpiod_chip_close(chip);
return -1;
}
/* 配置为输出,初始高电平 */
ret = gpiod_line_request_output(line, "my-led", 1);
if (ret < 0) {
perror("gpiod_line_request_output");
gpiod_chip_close(chip);
return -1;
}
/* 写值 */
gpiod_line_set_value(line, 0); /* 灭 */
sleep(1);
gpiod_line_set_value(line, 1); /* 亮 */
/* 清理 */
gpiod_line_release(line);
gpiod_chip_close(chip);
gpiod_line_set_value() 的延迟大约 2~5μs,比 sysfs 的 write 系统调用快一个数量级。
调试与故障定位:三板斧
GPIO 出问题,通常就三种情况:电平不对、方向不对、复用冲突。我的调试流程是这样的:
- 查设备树:
dtc -I dtb -O dts -o dump.dts /sys/firmware/fdt,确认 pinctrl 和 gpios 属性正确。 - 查内核日志:
dmesg | grep gpio,看有没有 probe 失败或资源冲突。 - 查寄存器:用
devmem直接读 PIO 寄存器,确认 CFG 和 DAT 值。
举个例子,T113 上 PB2 的 CFG 寄存器地址是 0x02000000 + 2*0x24 + 0x00(Bank 偏移 + 引脚组偏移)。你可以这样读:
# 读 PB2 的 CFG 寄存器(假设 PB 是 Bank 2)
devmem 0x02000048 32
# 如果返回值是 0x00000001,说明 PB2 被配成了输出模式
# 如果返回值是 0x00000000,说明是输入模式
echo 66 > /sys/class/gpio/unexport 释放掉,再读寄存器。
嗯,GPIO 子系统的基础就这些。从硬件寄存器到设备树配置,再到用户空间接口,这条链路你走通了,后面做任何外设驱动都会顺手很多。记住:先确认硬件电平,再查软件配置——这是调试的黄金法则。