3、GPIO驱动调试:从寄存器到按键响应

GPIO调试,说白了就是让芯片的引脚听话。你想让它输出高电平它就高,想让它检测按键它就检测。听起来简单,但实际坑不少。我这些年调过的GPIO问题,少说也有几十个了。

3.1 GPIO控制器寄存器映射

每个SoC的GPIO控制器,本质上就是一堆寄存器。你要做的第一件事,就是把物理地址映射到虚拟地址空间。

在Linux里,我们通常用ioremap或者devm_ioremap_resource来做这件事。我个人习惯用后者,因为它自带资源管理,省得你忘了释放。

static void __iomem *gpio_base;

gpio_base = devm_ioremap_resource(dev, res);
if (IS_ERR(gpio_base))
    return PTR_ERR(gpio_base);

映射完之后,你就能通过偏移量访问各个寄存器了。比如数据寄存器、方向寄存器、上拉下拉寄存器等等。每个芯片的偏移量都不一样,一定要看数据手册。

注意:千万别直接用物理地址读写。我在项目中遇到过有人直接拿物理地址操作,结果内核直接panic。记住,内核态必须用虚拟地址。

3.2 sysfs接口使用

调试GPIO最快的方式是什么?我个人觉得是sysfs。不用写驱动,直接在命令行就能操作。

首先,你要确认GPIO是否已经导出:

# 查看GPIO编号,比如GPIO3_5 = 3*32 + 5 = 101
echo 101 > /sys/class/gpio/export

# 设置方向
echo out > /sys/class/gpio/gpio101/direction

# 输出高电平
echo 1 > /sys/class/gpio/gpio101/value

# 读取电平
cat /sys/class/gpio/gpio101/value

嗯,这里要注意:有些内核版本把sysfs接口标记为deprecated了,改用chardev。但说实话,调试阶段sysfs还是最方便的。你想想看,一条命令就能测引脚,多省事。

小技巧:我曾经调试一块板子,GPIO死活拉不高。用sysfs一测才发现,原来是引脚复用了,被别的外设占着。所以调试前,先确认引脚没有被复用。

3.3 gpiolib框架分析

gpiolib是Linux内核提供的GPIO抽象层。它把底层的寄存器操作封装起来,让你用统一的API干活。

核心结构体就两个:

结构体 作用
gpio_chip 代表一个GPIO控制器,包含操作函数指针
gpio_desc 代表一个具体的GPIO引脚,包含状态信息

写驱动时,你只需要实现gpio_chip里的几个回调函数:

static struct gpio_chip my_gpio_chip = {
    .label          = "my-gpio",
    .direction_input  = my_gpio_direction_input,
    .direction_output = my_gpio_direction_output,
    .get            = my_gpio_get,
    .set            = my_gpio_set,
    .base           = -1,  // 动态分配
    .ngpio          = 32,
};

注册的时候调用gpiochip_add_data就行了。内核会自动帮你处理设备树匹配、中断映射这些杂事。

核心思路:gpiolib把硬件差异藏起来了。你写驱动时,不用关心寄存器具体怎么操作,只要实现那几个回调函数就行。剩下的,框架帮你搞定。

3.4 中断触发方式配置

GPIO中断,说白了就是引脚电平变化时通知CPU。触发方式有几种:

  • 上升沿触发:从低到高跳变时触发
  • 下降沿触发:从高到低跳变时触发
  • 双边沿触发:两种跳变都触发
  • 电平触发:保持高或低电平期间一直触发

配置方式很简单,用request_irq或者devm_request_irq

static irqreturn_t my_gpio_irq_handler(int irq, void *dev_id)
{
    // 处理中断
    printk("GPIO interrupt triggered!\n");
    return IRQ_HANDLED;
}

// 注册中断,下降沿触发
ret = devm_request_irq(dev, gpio_to_irq(gpio_num),
                       my_gpio_irq_handler,
                       IRQF_TRIGGER_FALLING,
                       "my-gpio-key", NULL);

这里有个坑:电平触发容易产生多次中断。我曾经遇到过按键按下时,中断触发了十几次。后来加了防抖处理才解决。

避坑指南:我曾经在项目里用上升沿触发检测按键,结果按键按下时抖动太厉害,一次按下触发了五六次中断。后来我加了硬件RC滤波,软件里也做了防抖,才算稳定。

3.5 实际案例:按键中断响应调试

好了,理论说完了,咱们来个实战。调试一个按键中断,从零开始。

第一步:确认硬件连接

按键一端接GPIO,另一端接地。按下时引脚拉低,所以用下降沿触发。

第二步:设备树配置

gpio-keys {
    compatible = "gpio-keys";
    pinctrl-0 = <&key_pins>;
    
    power-key {
        label = "Power Key";
        gpios = <&gpio3 5 GPIO_ACTIVE_LOW>;
        linux,code = <KEY_POWER>;
        gpio-key,wakeup;
    };
};

第三步:驱动实现

static int __init key_drv_init(void)
{
    int ret;
    int irq;
    
    // 获取GPIO描述符
    key_gpio = gpiod_get(&pdev->dev, NULL, GPIOD_IN);
    if (IS_ERR(key_gpio)) {
        pr_err("Failed to get GPIO\n");
        return PTR_ERR(key_gpio);
    }
    
    // 获取中断号
    irq = gpiod_to_irq(key_gpio);
    
    // 注册中断
    ret = devm_request_irq(&pdev->dev, irq,
                           key_irq_handler,
                           IRQF_TRIGGER_FALLING,
                           "power-key", NULL);
    if (ret) {
        pr_err("Failed to request IRQ\n");
        return ret;
    }
    
    return 0;
}

第四步:调试过程

我调试时一般这么干:

  1. 先用sysfs确认GPIO能正常读写
  2. 用示波器看按键波形,确认没有严重抖动
  3. 注册中断后,按一下按键,看/proc/interrupts里中断计数有没有增加
  4. 如果中断没触发,检查设备树里的GPIO编号对不对
经验之谈:我曾经调一个按键,中断死活不触发。查了半天,发现是设备树里GPIO的active level写反了。你想想看,按键按下是低电平,结果我配成了高电平触发,当然没反应。所以一定要确认硬件逻辑。

最后,别忘了在中断处理函数里加防抖。最简单的办法是加一个定时器,中断触发后延迟10-20ms再读一次引脚电平。如果还是有效电平,才认为是真正的按键事件。

嗯,GPIO调试大概就这些。说白了就是三板斧:确认硬件、配好设备树、写好中断处理。但每个环节都有坑,多踩几次就熟了。