第3章 RK GPIO驱动实战:从框架到调试
GPIO,说白了就是芯片最基础的“手脚”。你让它高它就高,让它低它就低。但到了RK平台,事情就没那么简单了——背后牵扯着GPIO子系统、pinctrl、中断控制器、底半部处理一堆东西。这一章,我们就把它彻底捋清楚。
3.1 GPIO子系统框架:三层结构
RK平台的GPIO驱动,内核里分了三层。我刚开始接触时也觉得绕,后来画了张图才明白。
每一层都有明确的职责。应用层只管“我要这个脚输出高电平”,核心层负责把请求翻译成标准调用,硬件驱动层才真正去读写寄存器。我在项目中见过有人直接绕过核心层去操作寄存器,结果换内核版本就崩了——千万别这么干。
3.2 pinctrl驱动配置:管脚复用
RK芯片的GPIO,大部分都是多功能管脚。同一个物理脚,可能是GPIO,也可能是I2C的SCL,还可能是UART的TX。怎么切换?靠pinctrl。
rkxx-pinctrl.dtsi 里的管脚配置。管脚复用配错了,后面所有驱动都跑不起来。
设备树里典型的pinctrl配置长这样:
// rk3588-pinctrl.dtsi 片段
pinctrl: pinctrl {
compatible = "rockchip,rk3588-pinctrl";
rockchip,grf = <&grf>;
#address-cells = <2>;
#size-cells = <2>;
ranges;
// 定义一个i2c2的管脚组
i2c2 {
i2c2_xfer: i2c2-xfer {
rockchip,pins =
<3 RK_PB0 1 &pcfg_pull_up>, // GPIO3_B0 → I2C2_SCL
<3 RK_PB1 1 &pcfg_pull_up>; // GPIO3_B1 → I2C2_SDA
};
};
};
这里 rockchip,pins 的四个字段分别是:bank号、pin号、function选择、上下拉配置。function为0时就是普通GPIO,非0值对应不同的复用功能。
3.3 GPIO中断注册与底半部处理
GPIO中断,嵌入式开发里天天用。按键、插拔检测、边沿触发……但很多人只会在设备树里配个 interrupts 属性,对内核里怎么注册、怎么处理底半部,一知半解。
RK平台GPIO中断的注册流程,我总结成三步:
- 设备树声明中断资源——指定GPIO和触发类型
- 驱动里调用 gpiod_to_irq()——拿到中断号
- request_threaded_irq()——注册中断处理函数,区分顶半部和底半部
看个实际例子:
// 设备树节点
gpio-key {
compatible = "gpio-keys";
pinctrl-names = "default";
pinctrl-0 = <&key_pin>;
key-gpios = <&gpio1 RK_PA5 GPIO_ACTIVE_LOW>;
interrupts-extended = <&gpio1 RK_PA5 IRQ_TYPE_EDGE_FALLING>;
};
// 驱动中注册中断
static int key_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct gpio_desc *desc;
int irq, ret;
desc = devm_gpiod_get(dev, "key", GPIOD_IN);
if (IS_ERR(desc))
return PTR_ERR(desc);
irq = gpiod_to_irq(desc);
if (irq < 0)
return irq;
// 注册线程化中断——底半部自动在进程上下文执行
ret = devm_request_threaded_irq(dev, irq,
NULL, // 顶半部:NULL表示全部交给底半部
key_isr_thread, // 底半部:可以睡眠,可以做耗时操作
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"gpio-key", dev);
if (ret)
dev_err(dev, "request irq failed: %d\n", ret);
return ret;
}
static irqreturn_t key_isr_thread(int irq, void *dev_id)
{
struct device *dev = dev_id;
int val;
// 底半部里可以调用msleep、i2c读写等阻塞操作
msleep(20); // 简单消抖
val = gpiod_get_value(desc);
if (val == 0) {
dev_info(dev, "key pressed!\n");
// 上报输入事件或唤醒用户态
}
return IRQ_HANDLED;
}
为什么推荐线程化中断?我举个例子。有一次客户反馈按键偶尔丢失,查了三天发现是顶半部里调了 i2c_master_send()——顶半部在中断上下文,不能睡眠,I2C传输需要等待应答,直接导致内核报错。改成线程化中断后,问题消失。
3.4 GPIO电平测量与调试
驱动写完了,怎么确认GPIO状态对不对?我见过有人直接拿万用表去量,但有些管脚是1.8V电平,万用表反应慢,瞬间脉冲根本抓不到。这时候得用示波器,或者——先在内核里确认。
内核提供了几个调试手段:
| 调试方法 | 命令/接口 | 适用场景 |
|---|---|---|
| sysfs 导出 | echo 120 > /sys/class/gpio/export |
快速验证GPIO方向、电平 |
| debugfs 查看 | cat /sys/kernel/debug/gpio |
查看所有GPIO的当前状态、使用者 |
| pinctrl debug | cat /sys/kernel/debug/pinctrl/pinctrl-handles |
确认管脚复用是否正确 |
| devmem 读寄存器 | devmem 0xfe2b0000 32 |
直接读GPIO数据寄存器,绕过驱动层 |
我个人最常用的是 debugfs/gpio。它会把每个GPIO的bank、label、方向、当前电平、使用者都列出来。有一次排查HDMI热插拔检测,发现GPIO方向被某个驱动改成了输出,就是靠这个抓到的。
gpiod_get_value() 打印电平变化,配合 printk 的时间戳,可以精确到微秒级。比拿示波器量快多了。
再说一个实际案例。某项目用RK3588做边缘计算盒子,GPIO4_C6控制一个外设的复位脚。设备树配的是高电平复位,但外设死活不工作。我量了管脚电压——1.8V,没错啊。后来用 devmem 读GPIO数据寄存器,发现寄存器里确实是高,但外设那边就是没反应。最后查原理图,发现这个管脚经过了一个电平转换芯片,转换芯片的使能脚没拉高。嗯,硬件问题,驱动背了锅。
总结一下这一章的核心:GPIO驱动不是简单的“拉高拉低”,它背后是pinctrl的管脚复用、gpiolib的框架抽象、中断的顶底半部处理。调试时,先用内核接口确认软件状态,再用万用表/示波器确认硬件状态,两边都对得上,问题才能定位。