ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux中断子系统整体框架:从GIC到request_irq的移植指南

Linux中断子系统整体框架:从GIC到request_irq的移植指南 做Linux驱动移植说到底就是在跟两件事打交道地址和中断。地址配错了顶多功能异常中断搞错了轻则设备睡死、重则直接整机卡死。我调过的板子里中断子系统整体框架相关的坑占了相当大比例而且多数问题不是出在某一个函数里而是出在对“中断从硬件到内核这一路到底经过哪些环节”没有完整概念。这篇博文就把linux驱动移植中最关键的中断子系统整体框架完整梳理一遍包括GIC、irq_domain、irq_chip、request_irq这条完整链路并给出可以直接套用的移植思路和排查方法。适合正在做BSP、外设驱动移植或者经常被中断问题折磨的兄弟参考。1. 先画地图中断从硬件到内核的完整路径1.1 四个角色外设、中断控制器、CPU 与内核中断这件事硬件上只有三个参与方外设、中断控制器、CPU。软件这边其实也正好三个参与方中断控制器驱动irqchip、内核通用中断框架kernel/irq、以及你正在写的设备驱动。把这三对角色对齐整个中断路径就清晰了。外设产生中断信号之后信号不会直接进 CPU而是先到中断控制器。中断控制器负责两件事一是把多个外设的中断线汇聚到一起二是按优先级把最紧急的那路中断送给 CPU。CPU 收到中断信号后会打断当前正在执行的指令流跳到内核事先设置好的异常向量入口。内核在这个入口里做现场保护然后根据中断号找到对应驱动注册的处理函数最后恢复现场继续执行被打断的任务。这个过程可以拿公司前台做类比。外设就是打电话的人中断控制器是前台接待CPU 是老板。外设不停打电话进来前台按轻重缓急决定先接哪条线然后把电话转给老板。老板本来在写方案被电话打断接完电话再接着写。驱动里的中断处理函数就相当于老板接起电话之后说的那句“喂你好”——只不过这句“喂”必须说得又快又短不能占用老板太多时间。1.2 三种“中断号”的区别与换算搞驱动移植的人最容易绕晕的就是中断号。同一个中断在硬件、内核、驱动三个层面有三个完全不同的编号很多人拿着一根中断线从头查到尾发现内核里报的号和芯片手册对不上其实就是没搞懂这三套编号。三种中断号分别是硬件中断号hwirq、Linux 全局中断号Linux IRQ number、以及驱动里实际使用的 irq number。硬件中断号是中断控制器视角的编号比如 GIC 里的 SPI 编号、PPI 编号。Linux 全局中断号是内核把系统中所有中断源统一编号后分配的一个整数这个数就是 irq_desc 数组的下标。驱动里拿到的通常也是这个全局号但你不需要关心它具体是多少只要通过设备树解析自动拿到就行。中断号类型视角示例使用场景hwirq中断控制器GIC SPI 12、PPI 27芯片手册、设备树 interrupts 属性Linux IRQ number内核全局如 44、159/proc/interrupts 第一列、request_irq设备树 interrupt number设备树描述12、27与 hwirq 直接对应这三者之间的翻译工作由 irq_domain 完成。设备树里你写的是 GIC 视角的编号内核通过 irq_domain 把它映射成全局中断号驱动最终 request_irq 的却是全局号。所以排查问题时先要搞清楚你手里看到的号是哪一层的号再决定要不要换算。1.3 顶级中断控制器GIC 以及它为什么重要在 ARM 生态里中断控制器基本都是 GICGeneric Interrupt ControllerGIC-v2 常见于老平台GIC-v3 是当前主流GIC-v4 开始支持虚拟化直通。GIC 把中断分成三类SGI软件触发中断核间通信用、PPI私有外设中断每个核独享、SPI共享外设中断全局共享。设备驱动接触最多的是 SPI 和 PPISGI 一般只有内核调度和 IPI 在用。做移植时要明白 GIC 的重要性GIC 驱动irqchip 驱动是所有中断的根。如果 GIC 驱动没初始化好系统中所有设备的中断都申请不到。好在你多半不需要自己移植 GIC 驱动主流内核已经内置了完整的 GIC-v2/v3 驱动位于 drivers/irqchip/irq-gic.c 和 irq-gic-v3.c。你要做的只是确认设备树里 GIC 节点配置正确并且内核编译时打开了对应的 CONFIG_ARM_GIC。不过有一条要特别注意GIC 驱动的初始化时机非常早通常通过 IRQCHIP_DECLARE 机制在无需设备驱动模型时就完成注册。这意味着 GIC 中断域必须在任何外设中断使用之前就绪。如果你的板子启动日志里看不到 GIC 初始化的信息后面所有设备的中断都会失败而且报错往往很隐蔽。2. 移植前必须先吃透的三个核心机制2.1 irq_domain中断号的翻译官irq_domain 是 Linux 中断子系统里最重要的设计之一也是移植时最需要理解的概念。它的职责是把中断控制器的 hwirq 映射成 Linux 全局中断号。之所以需要这层映射是因为硬件中断号和软件中断号无法一一对应。一个 SoC 可能有上百个中断源而内核希望用一个连续的数组去管理所有 irq_desc所以要有一个动态分配和查找的机制。irq_domain 按照映射方式可以分为几类线性域linear、树形域tree、无父域nomap等。线性域用一个数组直接索引适合中断源数量固定且不多的情况树形域适合中断源很多且稀疏的情况nomap 一般用于需要绕过 irq_desc 的场景。绝大多数 SoC 的中断控制器都用线性域。移植时你不一定会写 irq_domain 代码但一定要会用它的查询接口。设备驱动里最常用的两个接口是 irq_of_parse_and_map 和 irq_find_host。前者从设备树节点解析出中断属性然后在对应中断控制器里完成映射返回 Linux 全局中断号后者用于已知中断控制器的 device_node 时反向查找中断域。调试时如果发现某个设备中断号一直解析失败优先怀疑设备树 interrupt-parent 指向的节点是不是一个注册过的中断控制器。irq_domain 的重要性还体现在设备树动态映射上。以前没有 irq_domain 时中断号靠平台代码静态分配很容易冲突现在只要设备树描述正确内核会在启动时按需分配极大降低了移植工作量。这也是为什么现代内核推荐用设备树描述中断而不是在板级文件里写死中断号。2.2 irq_chip真正碰硬件寄存器的那个人irq_chip 是一组回调函数的集合封装了操作中断控制器硬件寄存器的全部细节。驱动里的 request_irq 只是软件层面的注册真正在硬件上使能、屏蔽、应答中断的是 irq_chip 里的成员函数。典型的 irq_chip 回调包括 irq_mask、irq_unmask、irq_ack、irq_eoi、irq_set_type、irq_set_wake。irq_mask 和 irq_unmask 负责屏蔽和打开某路中断irq_ack 用于边沿触发模式下清除 Pending 状态irq_eoi 用于电平触发模式下告诉 GIC 中断处理完毕irq_set_type 设置上升沿、下降沿、高电平、低电平触发irq_set_wake 则用于标记某路中断能否把系统从休眠中唤醒。对你来说移植时真正需要关心的 irq_chip 操作其实是两类。一类是 irq_set_type因为设备树里写的中断触发方式最终会通过它配置到硬件另一类是 irq_set_wake因为休眠唤醒功能依赖它。至于 mask 和 unmask内核通用框架会帮你管理。但排查问题时你得能读懂 irq_chip 注册的痕迹比如在 /proc/interrupts 或 debugfs 里能看到对应控制器的名字。这里有一点非常容易踩坑irq_chip 是挂在 irq_desc 上的而 irq_desc 在 irq_domain 映射时建立。如果中断号映射成功但中断控制器驱动没有实现某个回调内核会调用默认处理。比如 set_type 没有实现你设备树写了 IRQ_TYPE_LEVEL_HIGH硬件实际可能还是默认的边沿触发中断行为就会和预期完全不一样。调这种问题非常难受因为你看到的代码和实际硬件行为对不上。2.3 irqaction 与 request_irq驱动与框架的握手请求中断是每个中断驱动都逃不掉的一步。最常见的是 request_irq、request_threaded_irq 和 devm_request_irq 三个接口。request_irq 适合中断处理函数非常短、不需要在上下文里睡的场景request_threaded_irq 会把处理函数丢到一个内核线程里去跑适合处理逻辑较重、甚至需要调用可能睡眠的函数的情况devm 版本则在驱动卸载时自动释放中断省得你在 remove 函数里手动 free_irq。request_irq 的核心参数包括 irq 号、处理函数 handler、flag 标志、设备名和 dev_id。flag 里最常用的是 IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW以及 IRQF_SHARED。这里要特别强调如果你在设备树里已经写清楚了触发方式并且在解析中断时也正确映射了request_irq 里就没必要再传 trigger 标志。因为 irq_set_type 在解析阶段就执行了request_irq 里的 trigger 标志通常被忽略或者仅作为检查。还有一个概念很多人搞混irqaction 数据结构。request_irq 注册的每个中断处理函数内核都会分配一个 irqaction 结构挂在 irq_desc 的 action 链表上。如果多个驱动共享同一个中断号它们就在这条链表上排队。这时所有 handler 都必须返回 IRQ_NONE 表示不是自己的中断或者 IRQ_HANDLED 表示处理了。如果有一个人始终返回 IRQ_HANDLED其他驱动可能永远收不到中断这种 Bug 属于最难查的那一类。3. 驱动移植实操从设备树到 request_irq 的完整链路3.1 第一步确认 SoC 中断控制器与 irqchip 驱动是否就绪移植驱动之前先确认底层中断控制器状态正常。打开串口看内核启动日志搜 gic 关键字。以 RK 平台为例日志里会出现 GICv3 或者 GIC400 的初始化信息。如果没有先检查内核配置确认 CONFIG_ARM_GIC_V3 或 CONFIG_ARM_GIC 已打开并确认设备树里 interrupt-controller 节点存在。设备树里 GIC 节点长这样gic: interrupt-controllerfff00000 { compatible arm,gic-v3; reg 0x0 0xfff00000 0x0 0x10000, 0x0 0xfff10000 0x0 0x10000; interrupt-controller; #interrupt-cells 3; #address-cells 2; #size-cells 2; };这里 #interrupt-cells 3 决定了引用 GIC 中断时必须写三个 cell中断类型SPI/PPI/SGI、中断编号、触发方式。常见误区是把 GIC 节点里的 interrupt-controller 属性漏掉或者 #interrupt-cells 写错导致设备树解析中断失败。另外一个常见问题是硬件上中断控制器不在默认地址比如原来地址是 0xfff01000新板子改成了 0xfff02000如果你只改了 reg 没改其它相关描述GIC 可能初始化一半就出问题。确认 GIC 就绪之后再去确认你要移植的外设对应的 irqchip 驱动是否已经在内核里。比如 GPIO 中断依赖 gpio-omap、gpio-rockchip 这类驱动它们会注册 gpio 中断域。这类驱动你可以在启动日志里搜 gpio 关键字确认中断域创建成功。很多时候外设中断不工作不是外设驱动的问题而是底层 GPIO/控制器中断域没起来。3.2 第二步设备树中断属性怎么写才对设备树中断属性的写法直接决定了解析结果。以 RK 平台 I2C 控制器为例节点里中断相关部分是这样i2c0: i2cffd00000 { compatible rockchip,rk3399-i2c; reg 0x0 0xffd00000 0x0 0x1000; interrupts GIC_SPI 12 IRQ_TYPE_LEVEL_HIGH; interrupt-parent gic; ... };这里的 GIC_SPI 定义在 dt-bindings/interrupt-controller/arm-gic.h 中值为 0GIC_PPI 值为 1。IRQ_TYPE_LEVEL_HIGH 定义在 dt-bindings/interrupt-controller/irq.h 中值为 8。也就是说上面这行表示这个 I2C 控制器使用一个 SPI 中断SPI 编号是 12高电平触发。注意这里 12 是 SPI 编号不是 GIC 里最终看到的 hwirq。GIC 的 SPI 编号在实际硬件寄存器里是从 32 开始排的所以最终 hwirq 32 12 44。这就是为什么你在某些文档或内核日志里看到中断号和芯片手册不完全一样。这个偏移关系大多数 irq_domain 会自己处理但你写设备树时写的始终是设备树视角的编号。还有一点容易写错interrupt-parent 的优先级。设备树里中断引用会先查节点自己的 interrupt-parent 属性没有再向上级节点找。如果你在 dtsi 里定义了 interrupt-parent gic在板级 dts 里又覆盖成 gpio那么中断就会走 GPIO 中断域而不是 GIC 中断域。这种错误非常隐蔽因为编译不报错启动时也不一定报错只是中断行为完全不对。3.3 第三步驱动代码里如何申请与实现中断处理设备树解析完驱动代码就该登场了。最推荐的写法是 platform_driver 的 probe 函数里先拿到中断号再请求中断。拿中断号的接口是 irq_of_parse_and_map 或 platform_get_irq。这两个接口的区别在于前者直接在设备树里解析后者还会额外检查 platform 资源。对于普通平台设备推荐 platform_get_irq因为它更不容易写错。static int my_driver_probe(struct platform_device *pdev) { struct my_device *dev; int irq, ret; irq platform_get_irq(pdev, 0); if (irq 0) return irq; dev devm_kzalloc(pdev-dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; dev_set_drvdata(pdev-dev, dev); ret devm_request_threaded_irq(pdev-dev, irq, my_irq_handler, my_irq_thread, IRQF_TRIGGER_HIGH | IRQF_SHARED, dev_name(pdev-dev), dev); if (ret) { dev_err(pdev-dev, request irq %d failed: %d\n, irq, ret); return ret; } return 0; }写中断处理函数时要严格区分上半部和线程化处理函数。上半部 my_irq_handler 只应该读取硬件状态寄存器、清除中断 Pending 标志、然后返回 IRQ_WAKE_THREAD 唤醒线程函数。真正的数据处理放在 my_irq_thread 里。这样做的原因是上半部运行在中断上下文不能调用会睡眠的函数比如 i2c_transfer、msleep、mutex_lock 等。如果你在中断上下文里做了这些事系统会报 scheduling while atomic甚至直接崩溃。static irqreturn_t my_irq_handler(int irq, void *data) { struct my_device *dev data; u32 status; status readl(dev-base REG_STATUS); if (!(status BIT(0))) return IRQ_NONE; writel(status, dev-base REG_STATUS_CLEAR); return IRQ_WAKE_THREAD; } static irqreturn_t my_irq_thread(int irq, void *data) { struct my_device *dev data; /* 这里可以放心地使用可以睡眠的 API */ handle_data(dev); return IRQ_HANDLED; }这里有个细节如果硬件是电平触发工作线程还在运行期间中断线可能一直保持有效电平导致上半部反复被打断。解决办法通常是在上半部先返回 IRQ_WAKE_THREAD线程里最后把中断条件彻底清掉。如果是边沿触发还好一些但也要小心两次边沿间隔太近导致丢中断。所以具体是电平还是边沿不仅要看设备树还要看硬件实际行为务必用示波器或逻辑分析仪确认。3.4 第四步移植后如何验证中断链路中断注册成功后最直观的验证方法是看 /proc/interrupts。这个文件每一行代表一个中断源第一列是 Linux 全局中断号后面是各个 CPU 上的中断次数统计。触发一次中断对应 CPU 列的数字应该增加。cat /proc/interrupts | grep my_device如果看不到自己的设备名说明 request_irq 没执行成功。如果设备名能看到但触发中断后计数不增加说明中断路径断在硬件到控制器之间或者配置错误。如果计数疯狂增长甚至 CPU 占用率飙升那就是中断风暴。另一个很有用的调试接口是 debugfs 里的 irq 信息cat /sys/kernel/debug/irq/irqs/中断号这个文件会列出该中断的 irq_desc 状态、irq_chip 名称、触发类型、action 链表上所有 handler 的名字。如果里面显示的触发类型和设备树对不上或者 action 链上有多个设备就可以定位到共享中断的问题。还要养成看 dmesg 的习惯。中断域建立失败、中断号重复注册、irq_set_type 失败通常都会在 dmesg 里留下线索。我调试时喜欢在串口上开一个终端盯着 dmesg同时手动触发中断事件这样能第一时间看到内核报错。4. 中断与电源管理休眠唤醒不掉链子的关键4.1 为什么中断会挡住系统休眠很多驱动移植完功能正常一测休眠就出事。常见现象有两种一种是系统睡下去之后唤不醒另一种是系统压根睡不下去休眠流程到一半就被打断。这两类问题基本都出在中断上。先说为什么中断会挡住休眠。内核进入 suspend 的流程是先 suspend 设备驱动然后关闭非唤醒中断最后 CPU 进入低功耗状态。如果某个设备的中断没有被正确标记为唤醒源也没有被屏蔽那么它在休眠过程中是可以让中断控制器把系统唤醒的但内核并不知道这个中断属于谁于是就会中止休眠流程。反之如果某个中断被标记成唤醒源但硬件没有正确配置唤醒触发条件那系统就真的睡死过去了。所以驱动里如果设备支持唤醒功能比如触摸屏的触摸唤醒、Wi-Fi 的 magic packet 唤醒你就必须实现 irq_set_wake 回调并且在驱动里显式调用 enable_irq_wake。4.2 enable_irq_wake 与 irq_set_wake 的正确用法enable_irq_wake 是内核提供的接口它会调用底层 irq_desc 关联的 irq_chip-irq_set_wake 回调。如果 irq_chip 没实现这个回调enable_irq_wake 会返回错误。所以在驱动里调用之前先确认接口返回值。if (device_may_wakeup(pdev-dev)) { ret enable_irq_wake(irq); if (ret) dev_warn(pdev-dev, failed to enable wake irq %d: %d\n, irq, ret); }在 remove 或 suspend 流程中记得对称地调用 disable_irq_wake。关于设备树有些平台需要在设备节点里配置 wakeup-source 属性表示该设备支持唤醒。这个属性本身不直接影响中断但它会影响设备驱动中 device_init_wakeup 的行为。很多驱动在 probe 里通过 device_init_wakeup(pdev-dev, true) 来使能 wakeup 能力如果你的板子忘记在 dts 里加 wakeup-source即使 enable_irq_wake 调用成功实际的 wakeup 框架也可能认为该设备不具备唤醒能力。休眠这一块的调试工具主要是 /sys/kernel/debug/wakeup_sources。它列出了当前注册的所有唤醒源及状态。还有 /sys/kernel/debug/suspend_stats 可以帮助确认 suspend 流程在哪一步失败。再配合 dmesg 里 suspend 相关的 log基本能定位到是哪个中断把系统拖住。4.3 唤醒源调试的实操方法我调过一个触摸屏唤醒问题系统休眠后按一下触摸屏屏幕能亮但只能亮一次第二次就唤不醒了。查到最后发现是触摸屏控制器在唤醒后没有重新使能触摸中断驱动在 resume 里只恢复了寄存器没恢复中断使能。这种问题说白了就是 suspend/resume 流程里的电源管理接口和中断状态没有同步。排查唤醒中断问题我一般按这个顺序来。先确认 dmesg 里 suspend noirq 阶段有没有报 interrupt 相关错误再在唤醒后立刻 cat /sys/kernel/debug/wakeup_sources 看哪个唤醒源被触发了然后检查 /proc/interrupts 对应中断的计数有没有增加如果增加了说明中断路径本身没问题问题出在驱动 resume 处理上如果没增加说明中断根本没进来要去查硬件唤醒电路和 SoC 的唤醒配置。另外千万不要在 wakeup 回调里做太多事。irq_set_wake 回调运行在休眠流程中此时很多子系统已经 suspend 了调用 i2c、spi、regulator 之类的操作很容易死锁。如果确实需要在唤醒时配置硬件在驱动的 resume 回调里做而不是在 irq_set_wake 里做。这是我踩过的比较深的坑之一。5. 常见问题与排查技巧实录5.1 中断号对不上SPI 偏移与设备树数错中断号对不上是最常见的问题也是新手最容易卡住的地方。典型场景芯片手册写着 UART0 使用中断源 32你在设备树里写 interrupts GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH结果 /proc/interrupts 看到的中断号怎么也对不上中断也不触发。这里其实要区分两个概念。芯片手册里的“中断源 32”如果是 GIC 的 SPI 编号那设备树里就应该写 32内核会自行加上 SPI 的偏移量 32得到 hwirq 64。如果你把 32 理解为最终 hwirq设备树里写 32 并没错但如果你看到芯片手册上的编号已经是 64而设备树又写了 64那么实际生效的 hwirq 就成了 96偏了 32。这类偏差在排查时非常迷惑。正确做法是只看设备树视角打开芯片手册的 GIC 中断表找到你要用的外设中断源看清楚它属于 SPI 还是 PPI然后只填对应的编号。SPI 编号总是从 0 开始列不需要你自己加偏移。如果内核日志里报的 irq 号和你预期差了一个固定的 32大概率就是你多写或者少写了一次偏移。还有一类问题来自资源冲突。同一个 SPI 编号被两个外设占用设备树里两个节点都引用同一个 GIC_SPI 编号内核注册第二个驱动时 request_irq 会报 -EBUSY。这通常不是设备树写错而是 SoC 的 pinmux 配置导致两个控制器共用一根中断线。此时需要根据实际硬件设计决定哪个外设不使能或者给两个外设写共享中断。5.2 中断不触发从硬件到软件逐级定位中断不触发千万别一上来就怀疑内核代码。先确认硬件上信号到底有没有到达控制器。我自己的排查顺序是下面这样的。第一步看 /proc/interrupts 里对应中断的计数是否变化。如果计数变了说明中断已经进到内核问题在驱动处理函数。如果计数没变进入下一步。第二步用 GPIO 调试方式确认外设有没有产生中断信号。最简单的是把中断引脚配置成 GPIO然后手动触发中断事件用 cat /sys/kernel/debug/gpio 或者 cat /proc/interrupts 看这个 GPIO 的电平变化。如果电平根本没变那问题在外设本身比如外设没使能、配置错误、或者工作模式不对。第三步把中断引脚接回中断控制器用逻辑分析仪或示波器看引脚上有没有脉冲。软件侧最容易犯的错是触发方式不对。设备树里写了 IRQ_TYPE_LEVEL_HIGH但硬件实际输出的是低电平有效或者边沿触发。这样中断永远不会按预期触发。所以确定触发方式不能只看设备树一定要看外设的数据手册看它的 INT 引脚是开漏输出还是推挽输出是高有效还是低有效。很多外设默认配置为低有效你不改驱动 init 里的寄存器INT 引脚一直是高电平高电平触发就永远触发不了。还有一种情况是中断处理函数里没有清中断标志。电平触发模式下如果 ISR 读状态后没有写清除寄存器中断线一直有效处理器会一直进入中断或者更糟的是控制器认为该中断没有处理完后续中断全部被阻塞。我见过不止一次看起来是“中断不触发”实际上是中断一直在触发但每次都因为某些条件不满足直接 return IRQ_NONE导致内核认为中断异常把该中断自动 disable 了。这种很阴险查 /proc/interrupts 只看得到非常少的计数但其实已经触发了无数次。5.3 中断风暴一条中断线把整机拖死中断风暴是驱动移植里最刺激的故障表现是系统异常卡顿CPU 占用率飙到接近 100%/proc/interrupts 里某个中断的计数每秒涨几千甚至几万。原因通常是中断处理函数没有正确清除硬件中断状态或者硬件触发条件一直满足。比如外设的 FIFO 满了中断标志一直被置位驱动不清除中断一来就能量十足地往 CPU 上报。处理中断风暴第一步是快速定位到具体中断源。可以直接看 /proc/interrupts 哪个计数涨得最快然后去 dmesg 里看有没有 spurious interrupt 或者 interrupt did not return IRQ_HANDLED 之类的提示。第二步是临时在中断处理函数开头加一条打印打印一下状态寄存器确认是哪个条件一直在触发。第三步是看是不是处理函数“假处理”——进到 ISR 后发现有多个中断源但只清了一个还有其它 pending 标志没清。对于共享中断还有一个很值得注意的点。如果中断线上挂了好几个设备其中一个设备的中断状态一直有效其它设备每次也会被唤醒但读自己的状态寄存器发现没有事件于是返回 IRQ_NONE。内核如果发现某个中断上所有 handler 都返回 IRQ_NONE会认为这是 spurious interrupt连续触发多次后会把该中断 disable 掉。一旦被 disable你看到的现象就是“这个设备再也不触发中断了”但实际是它把整个共享中断坑了。这种情况处理起来既要查硬件也要看所有共享该中断的驱动是否齐整。如果实在要快速止血可以在驱动里临时调用 disable_irq 来关掉有问题的中断但这只是治标。根因还是要回到硬件状态清除。有一点我特别想说中断风暴时不要只盯软件要去看硬件电路。曾经遇到一次GPIO 中断的上拉电阻没焊导致引脚悬空噪声不断触发中断软件怎么改都没用硬件补了一个上拉电阻立刻好了。5.4 共享中断线上的多个设备怎么和平共处共享中断在嵌入式平台很常见比如 I2C 总线上多个设备都挂在同一根 INT 线上。这时候每个设备的驱动的 handler 都必须是一个“先检查再处理”的结构读自己的状态寄存器如果这个事件属于自己处理并返回 IRQ_HANDLED如果不属于自己返回 IRQ_NONE。千万不能默认“这个中断一定是我的”否则共享中断链上其它设备会非常受伤。共享中断注册时必须在 request_irq 里加 IRQF_SHARED 标志并且提供 dev_id 参数。dev_id 的作用有两个一是在 free_irq 时区分要释放哪个设备的 handler二是在内核调用 handler 时作为参数传递给 driver。如果一个中断被第一个驱动用非 IRQF_SHARED 注册了第二个设备再来注册同一个中断号就会失败。反过来如果一个中断声明了 IRQF_SHARED但某个设备在 handler 里不检查自己状态不管三七二十一就 return IRQ_HANDLED其他共享设备就彻底完蛋。排查共享中断问题时/sys/kernel/debug/irq/irqs/中断号 里的 action 链表信息非常关键。你看得到这个中断号上挂了多少个 handler、每个 handler 的名字。如果发现不该有的设备挂上来了或者该有的设备不在就能立刻定位到是哪个驱动注册出了问题。写共享中断的 handler我习惯从一开始就严格按这个模板来static irqreturn_t sensor_irq_handler(int irq, void *data) { struct sensor_dev *sensor data; u32 status; status readl(sensor-base REG_STATUS); if (!(status sensor-irq_mask)) return IRQ_NONE; writel(status sensor-irq_mask, sensor-base REG_STATUS_CLEAR); return IRQ_HANDLED; }这样做还有一个好处当多个设备共享中断时如果某个设备不响应其它设备的中断不会受影响排查起来也快。不止于中断移植这件事的最后一点心得写到这里中断子系统整体框架的核心链路基本都覆盖了。从我实际做移植的经验来看中断相关的坑百分之八十都集中在地图没画全、编号没分清、触发方式没验证、共享中断没守规矩这几类。每个问题单看都不算难难的是它们藏在一起时容易让人误判方向。我个人的习惯是调中断问题永远先走一遍从硬件信号、中断控制器、irq_domain 映射到驱动 handler 的流程不去猜只看每一级留下的痕迹。另外值得说的是调试中断之前务必先把内核的 CONFIG_DEBUG_IRQ、CONFIG_PROVE_LOCKING 打开很多中断上下文的问题会在刚出现时就报出来而不是等到系统崩溃才被察觉。最后再分享一个小技巧保持 /proc/interrupts 和 dmesg 两个窗口常驻任何一次中断异常都能第一时间看到变化这个习惯帮我省了无数排查时间。
返回列表