第4章 GPIO驱动移植:从框架到实战

GPIO,说白了就是芯片最基础的“手脚”。你让它高它就高,让它低它就低。但到了Linux内核里,事情就没那么简单了。我刚开始接触RK平台时,也被GPIO子系统绕得有点晕。今天咱们就把这块彻底捋清楚。

本章核心脉络:GPIO子系统框架 → RK GPIO控制器驱动 → DTS配置实战 → 中断调试

4.1 GPIO子系统框架:三层结构

Linux内核的GPIO子系统,我习惯把它看成三层:

  • GPIO库(gpiolib):最底层,直接操作硬件寄存器
  • GPIO控制器驱动:芯片厂商实现,比如RK的gpio-rockchip.c
  • GPIO消费者接口:给其他驱动用的API,比如gpiod_get_value()

你想想看,为什么搞这么复杂?直接读写寄存器不香吗?

嗯,我在一个项目里吃过亏。当时一个外设驱动直接操作GPIO寄存器,结果内核升级后寄存器地址变了,代码全废。用gpiolib的话,换个平台只需要改DTS,驱动代码不用动。

GPIO子系统三层架构 GPIO消费者 gpiod_get_value / gpiod_set_value gpiolib 核心层 gpiochip_add / gpio_request GPIO控制器驱动 gpio-rockchip.c / gpio-dwapb.c 消费者驱动不直接操作寄存器,通过gpiolib调用控制器驱动,实现平台无关性

4.2 RK GPIO控制器驱动分析

瑞芯微的GPIO控制器,每个bank有32个引脚。以RK3588为例,有4个GPIO bank:GPIO0~GPIO3。驱动文件在 drivers/gpio/gpio-rockchip.c。

我建议你重点关注这几个回调函数:

回调函数 作用 我在项目中踩过的坑
direction_input 设置引脚为输入 忘记配置上拉/下拉,导致电平不确定
direction_output 设置引脚为输出并赋值 输出低电平时驱动能力不足,外设不响应
get 读取引脚电平 中断上下文里调用这个函数,导致调度异常
set 设置引脚电平 多个驱动同时操作同一个GPIO,竞争条件
to_irq GPIO号转中断号 返回的中断号不对,request_irq失败

小技巧:调试GPIO驱动时,我习惯先在gpio-rockchip.c里加printk,打印出bank基地址和偏移量。这样能快速确认驱动是否正确映射了寄存器。

4.3 DTS中GPIO配置实战

DTS配置是GPIO移植的重头戏。我见过太多人在这里翻车。先看一个标准配置:

&gpio1 {
    status = "okay";
};

my_device: my-device {
    compatible = "my,device";
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&my_device_pins>;
    pinctrl-1 = <&my_device_sleep_pins>;
    
    enable-gpios = <&gpio1 RK_PB2 GPIO_ACTIVE_HIGH>;
    irq-gpios = <&gpio1 RK_PB3 GPIO_ACTIVE_LOW>;
};

这里有几个关键点:

  • pinctrl-names:定义不同状态,比如default和sleep
  • pinctrl-0:引用pinctrl节点,配置复用功能
  • enable-gpios:直接引用GPIO控制器,指定bank和引脚

我曾经在一个项目里,把RK_PB2写成了RK_PB3,结果设备死活不工作。查了两天才发现是引脚号错了。所以,我建议你每次写完DTS,先用dtc -I dts -O dtb编译,再用fdtdump反编译确认。

注意:RK平台中,GPIO号的计算方式是:bank * 32 + pin。比如GPIO1_B2就是1*32 + 8 = 40。但DTS里直接用宏RK_PB2更安全,别自己算。

4.4 GPIO中断调试

GPIO中断调试,是驱动开发里最让人头疼的部分之一。我总结了一套调试流程:

  1. 确认中断号:cat /proc/interrupts | grep gpio,看有没有注册成功
  2. 检查触发条件:上升沿、下降沿、高电平、低电平,别搞混
  3. 测试中断响应:手动短接GPIO到VCC或GND,看中断计数是否增加
  4. 排查中断处理函数:加printk,确认是否进入了handler

嗯,这里要注意。中断处理函数里不能做耗时操作。我有个同事在中断里调用了msleep,结果系统直接死锁。中断上下文里只能用tasklet或workqueue推迟处理。

看一个典型的中断注册代码:

static irqreturn_t my_gpio_irq_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    
    /* 读取GPIO状态 */
    int val = gpiod_get_value(dev->gpio);
    
    /* 注意:这里不能sleep! */
    schedule_work(&dev->work);
    
    return IRQ_HANDLED;
}

static int my_probe(struct platform_device *pdev)
{
    struct my_device *dev;
    int irq, ret;
    
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    
    dev->gpio = devm_gpiod_get(&pdev->dev, "irq", GPIOD_IN);
    irq = gpiod_to_irq(dev->gpio);
    
    ret = devm_request_irq(&pdev->dev, irq, my_gpio_irq_handler,
                           IRQF_TRIGGER_FALLING, "my_device", dev);
    return ret;
}

调试时,我常用的命令:

  • echo 1 > /sys/class/gpio/gpioN/value:手动控制GPIO
  • cat /sys/kernel/debug/gpio:查看所有GPIO状态
  • cat /proc/interrupts:查看中断统计

核心经验:GPIO中断不触发,90%的原因是DTS里pinctrl配置错了。先检查复用功能是不是设成了GPIO模式,再检查中断触发条件。我曾经在RK3399上折腾了一整天,最后发现是pinctrl节点里忘了加bias-pull-up。

4.5 常见故障与排查

故障现象 可能原因 排查方法
GPIO输出电平不对 驱动能力不足、上拉/下拉配置错误 用示波器看波形,检查DTS中drive-strength
中断不触发 中断号错误、触发条件不对、pinctrl未配置 cat /proc/interrupts,手动触发测试
中断频繁触发 没有去抖、电平不稳定 硬件加电容,软件加延迟或使用IRQF_TRIGGER_NONE
GPIO被其他驱动占用 DTS中多个节点引用同一个GPIO 检查DTS,使用gpio-reserved-ranges

说实话,GPIO驱动移植本身不难,难的是排查。我个人的习惯是:先确认硬件没问题,再查DTS,最后看驱动代码。顺序别搞反了,否则容易做无用功。

调试利器:在RK平台上,我经常用io命令直接读写GPIO寄存器。比如io -4 0xfec20000读GPIO0的SWPORTA_DR寄存器,能快速确认硬件状态。

好了,GPIO这块就聊到这儿。记住我说的三层框架、DTS配置要点、中断调试流程,你在RK平台上基本不会在GPIO上栽跟头。