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通常有两种方式:
- DTS(Device Tree Source)配置:这是推荐的方式。在dts文件中,通过
pinctrl节点来定义引脚的复用状态。 - 直接操作寄存器:在驱动代码里,调用
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框架,通常分三步走:
- 在dts中声明中断资源:就像上面例子那样,指定
interrupt-parent和interrupts。 - 在驱动中注册中断处理函数:使用
devm_request_threaded_irq()或request_irq()。 - 实现中断处理逻辑:在中断处理函数中,读取传感器状态,清除中断标志,然后上报数据。
来看一个完整的驱动片段:
#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的注册流程走顺了,传感器驱动的骨架就算是搭起来了。下一章,我们会在这个骨架上,填上具体的传感器数据读取逻辑。