4、GPIO与中断配置:MTK GPIO控制器介绍、GPIO功能复用(MUX)配置、中断触发方式设置、使用EINT(外部中断)驱动框架

好,咱们进入第四章。这一章可以说是传感器驱动的“地基工程”。

你想想看,传感器跟主控芯片怎么通信?要么走I2C/SPI这种总线,要么就是靠GPIO来传递状态。而中断,则是让传感器“主动喊你”,而不是你一直去轮询它。在MTK平台上,GPIO和中断的配置,尤其是EINT(外部中断),是每个驱动工程师的必修课。

我个人习惯,拿到一个新平台,第一件事就是先把GPIO的MUX表翻一遍。为什么?因为踩坑踩怕了。

4.1 MTK GPIO控制器介绍

MTK的GPIO控制器,说白了就是一个超级大的“开关矩阵”。它不像有些芯片那样,一个引脚只能干一件事。MTK的引脚,大部分都是“多功能”的。

每个GPIO引脚,在芯片内部都对应一组寄存器。这组寄存器决定了这个引脚当前是什么角色。我把它分为三类核心寄存器:

  • 方向寄存器(DIR):控制引脚是输入还是输出。0是输入,1是输出。
  • 数据寄存器(DOUT/ DIN):输出时,写这个寄存器控制高低电平;输入时,读这个寄存器获取电平状态。
  • 功能复用寄存器(MODE):这是最关键的。它决定了这个引脚是当普通GPIO用,还是当I2C的SCL,还是当UART的TX,还是当EINT的中断脚。

在MTK的Linux内核中,这些操作都被封装在了 pinctrl 子系统中。你不需要直接去操作物理地址,但理解背后的硬件逻辑,对调试大有裨益。

核心要点:MTK的GPIO控制器,本质上是一个“引脚功能选择器 + 电平控制单元”。所有外设的引脚功能,最终都要落到GPIO的MUX配置上。

4.2 GPIO功能复用(MUX)配置

这是新手最容易出错的地方。我记得有一次,一个同事调了三天I2C,死活不通。最后发现,I2C的SCL和SDA引脚,MUX配置成了普通的GPIO输出模式。你说冤不冤?

在MTK平台上,配置MUX通常有两种方式:

  1. DTS(Device Tree Source)配置:这是推荐的方式。在dts文件中,通过 pinctrl 节点来定义引脚的复用状态。
  2. 直接操作寄存器:在驱动代码里,调用 gpio_request()pinctrl_select_state() 等API。这种方式更灵活,但容易造成代码混乱。

我建议你优先使用DTS方式。它把硬件配置和软件逻辑分开了,后期维护起来,你看着dts文件,一眼就知道哪个引脚干了什么。

来看一个典型的DTS配置示例:

/* 定义一个pinctrl节点,用于配置加速度传感器的中断引脚 */
&pio {
    accel_irq_pins_default: accel_irq_default {
        pins_cmd_dat {
            pinmux = <MTK_PINMUX(GPIO10, 0)>; /* GPIO10, 功能0 = 普通GPIO */
            slew-rate = <0>; /* 输入模式 */
            bias-pull-up = <MTK_PUPD_SET_R1R0_00>; /* 内部上拉 */
        };
    };

    accel_irq_pins_eint: accel_irq_eint {
        pins_cmd_dat {
            pinmux = <MTK_PINMUX(GPIO10, 1)>; /* GPIO10, 功能1 = EINT功能 */
            slew-rate = <0>;
            bias-pull-up = <MTK_PUPD_SET_R1R0_00>;
        };
    };
};

看到了吗?同一个物理引脚GPIO10,通过 pinmux 的不同值,可以切换成普通GPIO(功能0)或者EINT中断脚(功能1)。

个人经验:在调试MUX时,我习惯先在dts里把引脚配置成普通GPIO,然后用 cat /sys/kernel/debug/gpio 查看引脚状态。确认引脚能正常拉高拉低后,再切换成外设功能。这样能快速定位问题是出在MUX配置上,还是出在外设本身。

4.3 中断触发方式设置

中断触发方式,决定了传感器什么时候“喊你”。MTK的EINT支持以下几种触发方式:

触发方式 描述 适用场景
上升沿触发 电平从低变高时触发 按键按下释放、传感器数据准备好(高有效)
下降沿触发 电平从高变低时触发 传感器中断输出(低有效)、物体靠近
双边沿触发 电平变化时触发(无论高低) 编码器、需要捕获两种状态的场景
高电平触发 电平为高时持续触发 较少用,容易产生中断风暴
低电平触发 电平为低时持续触发 较少用,需要配合硬件消抖

在配置时,你需要在dts中指定 interrupts 属性。比如:

/* 在传感器设备的dts节点中 */
accel@1e {
    compatible = "st,lis2dh12";
    reg = <0x1e>;
    interrupt-parent = <&pio>; /* 指定中断控制器为PIO */
    interrupts = <10 IRQ_TYPE_EDGE_FALLING>; /* GPIO10, 下降沿触发 */
    ...
};

这里 IRQ_TYPE_EDGE_FALLING 就是下降沿触发。你可以在 include/linux/irq.h 中找到所有支持的触发类型宏定义。

避坑指南:我曾经遇到过一个情况,传感器在低功耗模式下,中断引脚输出的是“开漏”信号。结果我配置成了上升沿触发,导致中断完全丢失。后来发现,开漏信号需要外部上拉,并且中断触发方式要配置成下降沿(因为传感器拉低表示事件发生)。所以,一定要先看传感器的datasheet,搞清楚它的中断输出特性。

4.4 使用EINT(外部中断)驱动框架

MTK的EINT驱动框架,是专门为外部中断设计的。它跟Linux内核通用的 gpio_to_irq() 不同,EINT有自己的中断控制器和注册流程。

为什么MTK要搞一套自己的EINT?说白了,是为了更好的功耗管理和硬件兼容性。EINT可以在芯片深度睡眠时,依然保持唤醒能力。

使用EINT框架,通常分三步走:

  1. 在dts中声明中断资源:就像上面例子那样,指定 interrupt-parentinterrupts
  2. 在驱动中注册中断处理函数:使用 devm_request_threaded_irq()request_irq()
  3. 实现中断处理逻辑:在中断处理函数中,读取传感器状态,清除中断标志,然后上报数据。

来看一个完整的驱动片段:

#include <linux/interrupt.h>
#include <linux/of_irq.h>

static irqreturn_t sensor_irq_handler(int irq, void *dev_id)
{
    struct sensor_data *data = dev_id;

    /* 1. 读取传感器中断状态寄存器,确认中断源 */
    /* 2. 清除传感器内部的中断标志位 */
    /* 3. 上报数据到输入子系统或IIO框架 */
    /* 4. 返回IRQ_HANDLED */

    return IRQ_HANDLED;
}

static int sensor_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct sensor_data *data;
    int irq;
    int ret;

    data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    /* 获取dts中配置的中断号 */
    irq = platform_get_irq(pdev, 0);
    if (irq < 0) {
        dev_err(dev, "Failed to get IRQ\n");
        return irq;
    }

    /* 注册中断,注意使用IRQF_TRIGGER_NONE,触发方式由dts决定 */
    ret = devm_request_threaded_irq(dev, irq, NULL,
                                    sensor_irq_handler,
                                    IRQF_TRIGGER_NONE | IRQF_ONESHOT,
                                    "sensor_irq", data);
    if (ret) {
        dev_err(dev, "Failed to request IRQ %d\n", irq);
        return ret;
    }

    dev_info(dev, "Sensor probed, IRQ: %d\n", irq);
    return 0;
}

这里有个细节:我用了 devm_request_threaded_irq() 而不是 request_irq()。为什么?因为传感器中断处理通常需要I2C通信,而I2C通信是可能睡眠的。所以必须用线程化中断(threaded irq),把耗时的处理放到内核线程中执行,避免在原子上下文中阻塞。

我的习惯:在中断处理函数里,我通常只做最轻量级的工作——比如读取一个寄存器确认中断,然后唤醒一个工作队列(workqueue)或者使用IIO的trigger机制。绝对不要在中断上下文里做I2C读写,除非你确定你的I2C控制器支持原子操作(大多数不支持)。

嗯,这一章的内容就到这里。GPIO和中断,看似基础,但里面的坑真不少。你只要把MUX配置搞清楚了,把EINT的注册流程走顺了,传感器驱动的骨架就算是搭起来了。下一章,我们会在这个骨架上,填上具体的传感器数据读取逻辑。