内核 GPIO、Pinctrl、Gpiolib 通俗详解(实操向)
本文针对内核驱动初学者,用「生活化比喻 + 可直接复用的代码」拆解核心逻辑 ------ 不用死记术语,跟着流程就能上手控制硬件(如 LED、按键)。
一、先搞懂:三者是啥关系?
把硬件引脚比作 "员工":
-
Pinctrl:"人事部门"------ 给员工分配岗位(是当 GPIO,还是当 I2C/UART 接口),避免一个人干多份活;
-
Gpiolib:"业务工具包"------ 员工被分配成 "GPIO 岗" 后,用这个工具包干活(输出电平、读信号、触发中断);
-
GPIO:最终的 "工作内容"------ 控制硬件的具体动作(亮 LED、检测按键)。
二、Pinctrl:给引脚 "分配岗位"(避坑关键!)
核心目标
让引脚明确 "自己该干啥"------ 比如指定 "第 0 组第 5 号引脚" 只当 GPIO,不抢 I2C 的活,否则后续操作全失败。
两步配置法(DTS + 驱动)
1. 第一步:在设备树(DTS)写 "岗位说明书"
提前告诉内核 "哪个引脚做什么",直接复用改参数即可:
// 1. 定义"人事部门"(引脚控制器):管理某一片区的引脚
pinmux: pinmux@13000000 {
reg = <0x13000000 0x1000>; // 控制器硬件地址(看芯片手册)
pinctrl-controller; // 标记为"人事部门"
#pinctrl-cells = <2>; // 配置引脚需2个参数(组号+功能)
// 2. 给具体引脚定岗位:第0组第5号 → 当GPIO
gpio_func: gpio-func {
pins = <0 5>; // 引脚:第0组第5号(改这里换引脚)
function = "gpio"; // 岗位:GPIO(当I2C写"i2c")
};
};
// 3. 绑定到设备(如控制LED的设备)
led_device: led-device {
compatible = "demo,led-gpio"; // 驱动匹配标识(和代码对应)
pinctrl-names = "default"; // 状态名:默认用这个配置
pinctrl-0 = <&gpio_func>; // 绑定GPIO配置
led-gpio = <&gpio0 5 GPIO_ACTIVE_HIGH>; // GPIO属性(高电平亮)
status = "okay"; // 启用配置(必须写)
};
2. 第二步:在驱动里 "激活岗位"
代码里告诉内核:"按 DTS 的规则,让引脚变成 GPIO":
#include <linux/pinctrl/pinctrl.h> // 必须包含的头文件
static int led_probe(struct platform_device *pdev) {
// 1. 拿到"人事部门"联系方式(引脚控制器)
struct pinctrl *pctrl = pinctrl_get(&pdev->dev, NULL);
if (IS_ERR(pctrl)) { // 判断是否拿到,避免报错
return PTR_ERR(pctrl);
}
// 2. 找到DTS里的"GPIO岗位"
struct pinctrl_state *pstate = pinctrl_lookup_state(pctrl, "gpio-func");
if (IS_ERR(pstate)) {
pinctrl_put(pctrl); // 没找到就归还控制器
return PTR_ERR(pstate);
}
// 3. 激活岗位:让引脚正式变GPIO
if (pinctrl_select_state(pctrl, pstate)) {
pinctrl_put(pctrl);
return -EINVAL;
}
printk("Pinctrl配置完成,引脚已设为GPIO!\n");
return 0;
}
三、Gpiolib:用 GPIO "干活"(核心操作)
当引脚被 Pinctrl 设为 GPIO 后,用 Gpiolib 的工具函数实现 "读 / 写 / 中断",全是现成接口,直接抄!
1. 先 "占坑":申请 GPIO(避免被抢)
像 "登记工具归属",用gpio_request
函数:
#include <linux/gpio.h> // Gpiolib核心头文件
#include <linux/of_gpio.h> // 从DTS读GPIO的头文件
static int led_gpio; // 存储要操作的GPIO编号
static int led_probe(struct platform_device *pdev) {
struct device_node *np = pdev->dev.of_node; // 拿到DTS节点
// 1. 从DTS读GPIO编号(对应"led-gpio"属性)
led_gpio = of_get_named_gpio_flags(np, "led-gpio", 0, NULL);
if (led_gpio < 0) {
printk("从DTS读GPIO失败!\n");
return led_gpio;
}
// 2. 申请GPIO:参数2是"用途说明"(调试用)
if (gpio_request(led_gpio, "control_led")) {
printk("GPIO被占用了!\n");
return -EBUSY;
}
printk("GPIO申请成功,编号:%d\n", led_gpio);
return 0;
}
2. 基础活:读 / 写电平(控制 LED / 读按键)
需求 | 函数 | 通俗解释 | 代码示例 |
---|---|---|---|
设为输入(读) | gpio_direction_input(gpio) |
让 GPIO "听命令"(检测外部信号) | gpio_direction_input(led_gpio); |
读当前电平 | gpio_get_value(gpio) |
得到 0(低)或 1(高) | int level = gpio_get_value(led_gpio); |
设为输出(写) | gpio_direction_output(gpio, val) |
让 GPIO "发命令"(输出电平) | gpio_direction_output(led_gpio, 0); (初始低) |
改输出电平 | gpio_set_value(gpio, val) |
0 灭 / 1 亮(控制 LED) | gpio_set_value(led_gpio, 1); (亮) |
实操示例:控制 LED 亮灭
// 在probe里加:设为输出+初始灭
gpio_direction_output(led_gpio, 0);
// 亮LED(可在其他函数调用)
void led_on(void) {
gpio_set_value(led_gpio, 1);
printk("LED亮了!\n");
}
// 灭LED
void led_off(void) {
gpio_set_value(led_gpio, 0);
printk("LED灭了!\n");
}
3. 进阶活:GPIO 触发中断(按键检测)
"按键按下时内核立刻反应",核心是 "GPIO 转中断号 + 注册处理函数":
#include <linux/interrupt.h> // 中断相关头文件
static int irq_num; // 存储GPIO对应的中断号
// 3.1 中断处理函数:按键按下时自动执行
static irqreturn_t key_irq_handler(int irq, void *dev_id) {
// 读按键电平:0=按下,1=松开(按硬件调整)
int level = gpio_get_value(led_gpio);
if (level == 0) {
printk("按键按下了!\n");
led_on(); // 按下亮LED(自定义逻辑)
} else {
printk("按键松开了!\n");
led_off(); // 松开灭LED
}
return IRQ_HANDLED; // 告诉内核:中断已处理
}
// 3.2 在probe里注册中断
static int led_probe(struct platform_device *pdev) {
// ... 前面的Pinctrl配置、GPIO申请 ...
// 1. GPIO转中断号:每个GPIO对应一个中断号
irq_num = gpio_to_irq(led_gpio);
if (irq_num < 0) {
gpio_free(led_gpio); // 失败就释放GPIO
return irq_num;
}
// 2. 注册中断处理函数
if (request_irq(
irq_num, // 中断号
key_irq_handler, // 处理函数
IRQF_TRIGGER_FALLING | IRQF_ONESHOT, // 下降沿+单次触发(避中断风暴)
"key_gpio_irq", // 中断名称(调试用)
pdev // 传给处理函数的参数
)) {
gpio_free(led_gpio);
printk("中断注册失败!\n");
return -EBUSY;
}
return 0;
}
4. 收尾:释放资源(不用了要还)
驱动卸载时,归还资源(写在remove
函数里):
static int led_remove(struct platform_device *pdev) {
struct pinctrl *pctrl = pinctrl_get(&pdev->dev, NULL);
// 1. 释放中断
free_irq(irq_num, pdev);
// 2. 释放GPIO
gpio_free(led_gpio);
// 3. 释放Pinctrl控制器
pinctrl_put(pctrl);
printk("资源已释放!\n");
return 0;
}
四、完整流程:控制 LED 的全步骤
-
DTS 配置:定义引脚控制器→指定引脚为 GPIO→绑定到 LED 设备;
-
驱动初始化:
-
激活 Pinctrl(让引脚变 GPIO);
-
申请 GPIO + 设为输出;
-
注册中断(若需按键控制);
-
业务逻辑 :调用
gpio_set_value
亮灭 LED,中断触发时执行处理函数; -
卸载驱动:释放中断→GPIO→Pinctrl 资源。
五、避坑指南(新手必看)
-
Pinctrl 必须先配:没给引脚分配 "GPIO 岗" 就用 Gpiolib,100% 失败!先跑通 Pinctrl 再往下走;
-
中断触发方式别乱选:
-
按键用「边沿触发」(
IRQF_TRIGGER_FALLING
/RISING
),避免一直触发; -
传感器(如温度检测)用「电平触发」(
IRQF_TRIGGER_HIGH
/LOW
),加IRQF_ONESHOT
避 "中断风暴";
-
调试小技巧(不用写驱动):
内核支持用文件系统直接操作 GPIO,适合快速测硬件:
1. 导出GPIO(操作5号GPIO)
echo 5 > /sys/class/gpio/export
2. 设为输出
echo out > /sys/class/gpio/gpio5/direction
3. 亮LED(输出高电平)
echo 1 > /sys/class/gpio/gpio5/value
4. 灭LED(输出低电平)
echo 0 > /sys/class/gpio/gpio5/value
5. 用完删除
echo 5 > /sys/class/gpio/unexport
总结
GPIO 核心逻辑 8 个字:「定功能→做操作→还资源」:
-
定功能:Pinctrl 给引脚分配 GPIO 岗位;(部分不需要)
-
做操作:Gpiolib 读 / 写 / 中断;(实际上,很多默认为 GPIO ,直接进行此步就可使用)
-
还资源:卸载时释放,不占坑。
实操中若遇 "GPIO 申请失败""中断不触发",先查 Pinctrl 配置和硬件接线