从设备树到驱动代码:Linux内核中断处理全流程实战指南

在嵌入式Linux开发中,中断处理是驱动开发的核心技能之一。想象一下这样的场景:你正在为一个定制硬件开发驱动程序,当用户按下按钮或传感器数据到达时,系统需要立即响应。这种实时性要求极高的场景,正是中断机制大显身手的地方。本文将带你深入Linux中断处理的完整流程,从设备树配置到驱动代码实现,手把手教你构建可靠的中断驱动。

1. 设备树中的中断配置艺术

设备树作为硬件描述的载体,其中断配置是驱动开发的起点。一个典型的GPIO按键中断配置如下:

button@0 {
    compatible = "gpio-keys";
    gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
    interrupt-parent = <&gpio0>;
    interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
    label = "User Button";
};

这段配置揭示了设备树中断描述的三大要素:

  1. 中断父控制器:通过interrupt-parent指定,本例中为GPIO控制器
  2. 中断描述:interrupts属性包含引脚编号和触发方式
  3. GPIO关联:gpios属性将物理引脚与逻辑功能绑定

对于更复杂的场景,比如多路中断共享同一控制器,设备树提供了灵活的表示方式:

multi_irq_device {
    compatible = "vendor,multi-irq";
    interrupts-extended = <&intc0 10 IRQ_TYPE_LEVEL_HIGH>,
                         <&intc1 5 IRQ_TYPE_EDGE_BOTH>;
};

1.1 中断控制器关键属性解析

每个中断控制器都需要声明两个特殊属性:

intc: interrupt-controller {
    compatible = "vendor,irq-controller";
    interrupt-controller;
    #interrupt-cells = <3>;  // 每个中断需要3个u32描述
};

表:常见中断控制器类型及配置差异

控制器类型#interrupt-cells典型描述格式适用场景
GPIO控制器2<引脚号 触发方式>简单外设中断
级联中断控制器3<父IRQ 子IRQ 触发方式>复杂SoC系统
高级中断控制器4<类型 索引 子IRQ 触发方式>ARM GIC等

2. 驱动中获取中断号的五种方式

获取中断号是驱动开发的第一步,Linux内核为此提供了多种API,各有适用场景。

2.1 Platform设备的标准方法

对于platform设备,经典流程如下:

static int demo_probe(struct platform_device *pdev)
{
    struct resource *res;
    int irq;
    
    // 获取第0个IRQ资源
    res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
    if (!res) {
        dev_err(&pdev->dev, "no IRQ resource\n");
        return -ENODEV;
    }
    irq = res->start;
    
    // 更简洁的新版API
    irq = platform_get_irq(pdev, 0);
    if (irq < 0) {
        return irq; // 返回错误码
    }
    
    // 注册中断处理程序
    return request_irq(irq, demo_isr, IRQF_TRIGGER_FALLING, "demo", NULL);
}

2.2 I2C/SPI设备的快捷方式

总线设备驱动通常可以直接从client结构获取预解析的中断号:

// I2C驱动示例
static int i2c_demo_probe(struct i2c_client *client)
{
    int irq = client->irq; // 自动从设备树解析
    
    if (!irq) {
        // 备选方案:手动解析
        irq = of_irq_get(client->dev.of_node, 0);
    }
    
    return devm_request_threaded_irq(&client->dev, irq, NULL,
                    demo_isr, IRQF_ONESHOT, dev_name(&client->dev), priv);
}

// SPI驱动类似
static int spi_demo_probe(struct spi_device *spi)
{
    int irq = spi->irq; // 自动获取
    // ...处理逻辑同上
}

2.3 GPIO中断的特殊处理

当使用GPIO作为中断源时,现代内核推荐使用GPIO描述符接口:

int demo_setup_gpio_irq(struct device *dev)
{
    struct gpio_desc *gpiod;
    int irq, ret;
    
    // 获取GPIO描述符
    gpiod = gpiod_get(dev, "intr", GPIOD_IN);
    if (IS_ERR(gpiod)) {
        return PTR_ERR(gpiod);
    }
    
    // 转换为中断号
    irq = gpiod_to_irq(gpiod);
    if (irq < 0) {
        gpiod_put(gpiod);
        return irq;
    }
    
    // 注册中断
    ret = devm_request_irq(dev, irq, demo_isr,
              IRQF_TRIGGER_RISING | IRQF_ONESHOT,
              "gpio-irq", NULL);
    
    // 可以保留gpiod用于后续管理
    return ret;
}

3. 中断处理程序的高级技巧

获取中断号只是开始,编写稳健的中断处理程序才是真正的挑战。

3.1 中断处理函数的最佳实践

一个典型的中断服务例程(ISR)应该遵循以下原则:

static irqreturn_t demo_isr(int irq, void *dev_id)
{
    struct demo_priv *priv = dev_id;
    u32 status;
    
    // 1. 快速读取中断状态
    status = readl(priv->regs + REG_STATUS);
    
    // 2. 确认是本设备中断
    if (!(status & INTR_FLAG)) {
        return IRQ_NONE; // 不是我们的中断
    }
    
    // 3. 清除中断标志
    writel(status & INTR_FLAG, priv->regs + REG_STATUS);
    
    // 4. 调度下半部处理
    tasklet_schedule(&priv->tasklet);
    
    return IRQ_HANDLED;
}

表:中断处理中的关键决策点

场景处理方案相关API适用条件
快速处理直接在ISR中完成-处理时间<100us
中等延迟tasklettasklet_*需要原子操作
复杂处理workqueueschedule_work允许睡眠的操作
高频率threaded IRQrequest_threaded_irq需要优先级管理

3.2 中断共享与竞争处理

当多个设备共享同一中断线时,需要特别注意:

// 注册共享中断
ret = request_irq(irq, shared_isr, IRQF_SHARED, "demo", priv);

// 在ISR中正确处理共享
static irqreturn_t shared_isr(int irq, void *dev_id)
{
    struct demo_priv *priv = dev_id;
    
    if (!device_interrupted(priv)) {
        return IRQ_NONE; // 不是本设备中断
    }
    
    // ...处理逻辑
    return IRQ_HANDLED;
}

4. 从零构建GPIO中断驱动实例

让我们通过一个完整的GPIO按键驱动示例,串联所有知识点。

4.1 设备树配置

/ {
    gpio-keys {
        compatible = "gpio-keys";
        #address-cells = <1>;
        #size-cells = <0>;
        
        button@1 {
            label = "User Button";
            linux,code = <KEY_ENTER>;
            gpios = <&gpio0 12 GPIO_ACTIVE_LOW>;
            interrupt-parent = <&gpio0>;
            interrupts = <12 IRQ_TYPE_EDGE_BOTH>;
            debounce-interval = <50>;
        };
    };
};

4.2 驱动实现关键代码

#include <linux/interrupt.h>
#include <linux/gpio/consumer.h>

struct button_data {
    struct gpio_desc *gpiod;
    int irq;
    struct input_dev *input;
};

static irqreturn_t button_isr(int irq, void *dev_id)
{
    struct button_data *data = dev_id;
    int state = gpiod_get_value(data->gpiod);
    
    input_report_key(data->input, KEY_ENTER, !state);
    input_sync(data->input);
    
    return IRQ_HANDLED;
}

static int button_probe(struct platform_device *pdev)
{
    struct button_data *data;
    struct device *dev = &pdev->dev;
    int ret;
    
    data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;
    
    // 获取GPIO描述符
    data->gpiod = devm_gpiod_get(dev, NULL, GPIOD_IN);
    if (IS_ERR(data->gpiod)) {
        return PTR_ERR(data->gpiod);
    }
    
    // 转换为中断号
    data->irq = gpiod_to_irq(data->gpiod);
    if (data->irq < 0) {
        return data->irq;
    }
    
    // 设置输入设备
    data->input = devm_input_allocate_device(dev);
    data->input->name = pdev->name;
    input_set_capability(data->input, EV_KEY, KEY_ENTER);
    
    // 注册中断
    ret = devm_request_threaded_irq(dev, data->irq, NULL, button_isr,
                    IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING | IRQF_ONESHOT,
                    "gpio-button", data);
    if (ret) {
        return ret;
    }
    
    // 注册输入设备
    return input_register_device(data->input);
}

static const struct of_device_id button_of_match[] = {
    { .compatible = "gpio-keys" },
    {},
};
MODULE_DEVICE_TABLE(of, button_of_match);

static struct platform_driver button_driver = {
    .driver = {
        .name = "gpio-button",
        .of_match_table = button_of_match,
    },
    .probe = button_probe,
};
module_platform_driver(button_driver);

4.3 调试与问题排查

当中断不工作时,可以按照以下步骤排查:

  1. 检查设备树解析:

    # 查看解析后的中断信息
    cat /proc/device-tree/gpio-keys/button@1/interrupts
    
    # 确认GPIO状态
    cat /sys/kernel/debug/gpio
    
  2. 验证中断注册:

    # 查看已注册的中断
    cat /proc/interrupts | grep gpio-button
    
  3. 调试ISR执行:

    // 在ISR中添加调试打印
    printk(KERN_DEBUG "ISR triggered at %lld\n", ktime_get_ns());
    
  4. 测量中断延迟:

    ktime_t start = ktime_get();
    // ISR处理逻辑
    ktime_t delta = ktime_sub(ktime_get(), start);
    printk("ISR took %lld ns\n", ktime_to_ns(delta));
    

在实际项目中,我们经常遇到GPIO配置冲突、中断触发条件不匹配、中断风暴等问题。例如,某次调试中发现按键中断频繁触发,最终发现是未配置去抖参数导致。通过在设备树中添加debounce-interval = <50>;成功解决了问题。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐