4. 中断标志位 flags 详解:IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_SHARED、IRQF_ONESHOT 等

在 Linux 中断子系统中,request_irq() / devm_request_irq() 的第三个参数 unsigned long flags 是决定中断行为的关键。flags 由一组位掩码组合而成,控制中断的触发方式、共享策略、电源管理行为以及线程化安全。理解每个标志位的语义和组合约束,是中断驱动移植与调试的基础。

4.1 flags 位域总览

flags 的低 4 位用于触发方式,高 28 位用于行为控制。下表列出最常用的标志位及其内核宏定义值:

标志位宏 数值 (x86/ARM) 类别 核心语义
IRQF_TRIGGER_RISING 0x00000001 触发方式 上升沿触发
IRQF_TRIGGER_FALLING 0x00000002 触发方式 下降沿触发
IRQF_TRIGGER_HIGH 0x00000004 触发方式 高电平触发
IRQF_TRIGGER_LOW 0x00000008 触发方式 低电平触发
IRQF_SHARED 0x00000080 共享策略 允许多个设备共享同一 IRQ 线
IRQF_ONESHOT 0x00002000 线程化安全 中断处理期间硬件中断线保持屏蔽
IRQF_NO_SUSPEND 0x00004000 电源管理 系统休眠时不关闭该中断
IRQF_FORCE_RESUME 0x00008000 电源管理 系统唤醒时必须重新使能该中断
IRQF_NO_THREAD 0x00010000 线程化安全 禁止将该中断强制线程化
IRQF_EARLY_RESUME 0x00020000 电源管理 在早期恢复阶段即重新使能中断
工程提示:触发方式标志位(低 4 位)是互斥的,一次只能指定一种触发类型。如果同时设置 IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,内核会将其解释为 双边沿触发,这在某些 GPIO 控制器上被支持,但并非所有中断控制器都兼容。

4.2 触发方式标志位详解

4.2.1 IRQF_TRIGGER_RISING 与 IRQF_TRIGGER_FALLING

边沿触发(Edge-triggered)在信号电平跳变时产生中断。上升沿触发在信号从低到高时触发一次;下降沿触发在信号从高到低时触发一次。边沿触发的特点是:

  • 锁存性:中断控制器会锁存边沿事件,即使 CPU 暂时屏蔽中断,事件也不会丢失。
  • 防抖需求:机械开关或噪声较大的信号线容易产生毛刺,导致多次误触发。
  • 共享限制:边沿触发的中断线原则上不能共享(除非硬件支持电平检测回读)。

4.2.2 IRQF_TRIGGER_HIGH 与 IRQF_TRIGGER_LOW

电平触发(Level-triggered)在信号保持有效电平时持续产生中断请求。高电平触发在信号为高时有效;低电平触发在信号为低时有效。电平触发的特点是:

  • 持续请求:只要电平有效,中断线就一直保持断言状态。
  • 必须清除:中断服务程序必须操作外设寄存器清除中断源,否则会无限循环触发。
  • 共享友好:电平触发天然支持中断共享,因为中断控制器可以通过读取设备状态寄存器判断哪个设备产生了中断。
移植对照:在设备树中,触发方式通常由 interrupts 属性的第三个 cell 指定(如 IRQ_TYPE_EDGE_RISING)。驱动中通过 irq_get_trigger_type(irq) 获取硬件配置的触发方式,再传递给 request_irq。不要硬编码触发方式,否则在中断控制器级联时会引发不匹配。

4.3 IRQF_SHARED:中断共享机制

当多个外设共用一条物理 IRQ 线时,必须设置 IRQF_SHARED。共享中断的驱动必须满足以下契约:

  1. 注册顺序无关:每个共享中断处理程序都会被依次调用。
  2. 必须检查设备状态:处理程序第一件事是读取硬件寄存器,确认中断是否来自本设备。如果不是,立即返回 IRQ_NONE。
  3. 触发方式必须一致:所有共享该 IRQ 的驱动必须使用相同的触发方式(通常为电平触发)。
  4. 不能使用 IRQF_ONESHOT:共享中断与 ONESHOT 不兼容,因为 ONESHOT 会永久屏蔽中断线直到线程完成,导致其他设备无法接收中断。
常见错误:共享中断处理程序返回 IRQ_HANDLED 但实际并未处理本设备中断,会导致内核检测到“虚假中断”(spurious interrupt),最终内核可能禁用该 IRQ 线。务必在确认设备产生中断后才返回 IRQ_HANDLED。

4.4 IRQF_ONESHOT:线程化中断的安全屏障

IRQF_ONESHOT 是线程化中断(threaded IRQ)的关键标志。当驱动使用 request_threaded_irq() 并只提供线程处理函数时,内核会自动添加 IRQF_ONESHOT。其行为如下:

  • 中断线屏蔽:在中断线程处理函数执行期间,硬件中断线被自动屏蔽,防止同一中断源嵌套触发。
  • 自动恢复:线程处理函数返回后,内核自动取消屏蔽,恢复中断线。
  • 避免死锁:防止线程处理函数中访问共享资源时被同一中断再次打断,导致自旋锁死锁。

以下是一个典型的线程化中断注册示例:

/* 设备结构体 */
struct my_dev {
    int irq;
    void __iomem *base;
    struct completion done;
};

/* 中断线程处理函数 */
static irqreturn_t my_threaded_handler(int irq, void *dev_id)
{
    struct my_dev *dev = dev_id;
    u32 status;

    /* 读取状态寄存器确认中断源 */
    status = ioread32(dev->base + REG_STATUS);
    if (!(status & INT_PENDING))
        return IRQ_NONE;

    /* 清除中断标志 */
    iowrite32(INT_PENDING, dev->base + REG_CLEAR);

    /* 执行耗时的数据处理 */
    process_data(dev);

    complete(&dev->done);
    return IRQ_HANDLED;
}

/* 驱动 probe 函数中注册 */
static int my_probe(struct platform_device *pdev)
{
    struct my_dev *dev;
    int ret;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    dev->irq = platform_get_irq(pdev, 0);

    /* 注册线程化中断,内核自动添加 IRQF_ONESHOT */
    ret = devm_request_threaded_irq(&pdev->dev, dev->irq,
                                    NULL,  /* 无顶半部 */
                                    my_threaded_handler,
                                    IRQF_TRIGGER_HIGH | IRQF_ONESHOT,
                                    "my_driver", dev);
    if (ret) {
        dev_err(&pdev->dev, "failed to request threaded irq\n");
        return ret;
    }

    return 0;
}

4.5 标志位组合约束与工程陷阱

不同标志位之间存在互斥或依赖关系,驱动工程师必须熟记以下约束:

组合场景 是否允许 说明
IRQF_SHARED + 边沿触发 ❌ 不推荐 边沿事件无法被多个设备可靠共享,内核会发出警告
IRQF_SHARED + IRQF_ONESHOT ❌ 禁止 ONESHOT 屏蔽中断线会导致共享设备丢失中断
IRQF_ONESHOT + 无线程处理函数 ❌ 无效 ONESHOT 仅对线程化中断有意义,顶半部注册时会被忽略
IRQF_NO_THREAD + 强制线程化 ⚠️ 冲突 若内核配置强制线程化,NO_THREAD 会被覆盖
IRQF_TRIGGER_RISING + IRQF_TRIGGER_FALLING ✅ 允许 双边沿触发,需硬件支持
IRQF_NO_SUSPEND + 系统休眠 ✅ 常用 用于唤醒源中断,如 RTC 闹钟、按键唤醒

4.6 中断标志位选择决策流程

以下 SVG 流程图帮助驱动工程师在编写代码时快速确定 flags 组合:

中断标志位选择决策流程 开始选择 flags 是否多个设备共用 IRQ 线? 是 必须电平触发 否 边沿/电平均可 中断处理需要休眠/耗时操作? 是 加 IRQF_ONESHOT 否 使用顶半部即可 需要作为唤醒源? 是 加 IRQF_NO_SUSPEND 否 无需额外标志 最终 flags = 触发方式 | 共享标志 | ONESHOT | 电源标志 示例:IRQF_TRIGGER_HIGH | IRQF_SHARED ⚠️ 工程注意事项 • 共享中断必须使用电平触发,且不能加 IRQF_ONESHOT • 线程化中断必须加 IRQF_ONESHOT,防止嵌套死锁 • 唤醒源中断必须加 IRQF_NO_SUSPEND,否则系统休眠后无法唤醒 • 触发方式优先从设备树获取,不要硬编码

4.7 实战:flags 组合代码模板

以下代码模板展示了不同场景下的 flags 组合方式,可直接用于驱动移植:

/* 场景1:非共享、边沿触发、顶半部处理 */
ret = request_irq(irq, handler,
                  IRQF_TRIGGER_RISING,
                  "my_device_edge", dev);

/* 场景2:共享中断、电平触发、顶半部处理 */
ret = request_irq(irq, shared_handler,
                  IRQF_TRIGGER_HIGH | IRQF_SHARED,
                  "my_device_shared", dev);

/* 场景3:非共享、电平触发、线程化处理(带ONESHOT) */
ret = request_threaded_irq(irq, NULL,
                           threaded_handler,
                           IRQF_TRIGGER_LOW | IRQF_ONESHOT,
                           "my_device_threaded", dev);

/* 场景4:唤醒源、边沿触发、线程化处理 */
ret = devm_request_threaded_irq(&pdev->dev, irq,
                                NULL, wakeup_handler,
                                IRQF_TRIGGER_FALLING |
                                IRQF_ONESHOT |
                                IRQF_NO_SUSPEND,
                                "my_wakeup", dev);
/* 同时使能设备唤醒 */
device_init_wakeup(&pdev->dev, true);
调试技巧:在 /proc/interrupts 中可以查看每个中断的触发方式和共享状态。使用 cat /proc/interrupts | grep my_device 确认 flags 是否生效。如果看到 Level 或 Edge 字段与预期不符,说明设备树或驱动中的触发方式配置不一致。

4.8 中断标志位与设备树的映射关系

设备树中通过 interrupts 属性指定触发类型,驱动中通过 irq_get_trigger_type() 获取。下表列出设备树宏与内核 flags 的对应关系:

设备树宏 数值 对应内核 flags
IRQ_TYPE_EDGE_RISING 1 IRQF_TRIGGER_RISING
IRQ_TYPE_EDGE_FALLING 2 IRQF_TRIGGER_FALLING
IRQ_TYPE_EDGE_BOTH 3 IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING
IRQ_TYPE_LEVEL_HIGH 4 IRQF_TRIGGER_HIGH
IRQ_TYPE_LEVEL_LOW 8 IRQF_TRIGGER_LOW

推荐在驱动中动态获取触发方式,避免硬编码:

unsigned long irq_flags;

/* 从设备树获取触发类型 */
irq_flags = irq_get_trigger_type(dev->irq);

/* 添加其他行为标志 */
irq_flags |= IRQF_ONESHOT | IRQF_NO_SUSPEND;

ret = devm_request_threaded_irq(&pdev->dev, dev->irq,
                                NULL, handler,
                                irq_flags, "my_driver", dev);
移植陷阱:某些老旧 BSP 或中断控制器不支持 irq_get_trigger_type() 返回正确值,此时需要回退到设备树解析或平台数据中获取触发方式。在调试阶段,可以在 probe 函数中打印 irq_flags 确认实际值。