干过驱动移植的兄弟应该都有同感:外设寄存器、时钟、GPIO这些看几遍手册、试几次就能摸清套路,唯独中断,板子一跑起来就出幺蛾子。要么中断不触发,要么触发一次就死循环,要么一申请就报错,最难受的是看起来全对但就是不去执行回调。排查到最后往往不是驱动写错了,而是对中断子系统整体怎么运转缺乏一个完整的认知。
这篇文章就是梳理我从实际移植Linux驱动过程中总结的中断子系统整体框架。从硬件中断信号到CPU异常入口,再到内核通用中断层,最后回调到驱动注册的处理函数,每一层干什么、核心数据结构怎么串起来、驱动代码应该在哪一层做哪些事,我都尽量用大白话讲清楚。后面还会结合RK平台的I2C驱动移植,把设备树配置、中断申请、线程化、唤醒源这些常见操作过一遍,最后附上我自己踩过的排查坑。适合刚接触内核驱动移植的开发者,也适合做BSP适配的兄弟查漏补缺。
1. 中断子系统在驱动移植里的位置
1.1 从一次I2C探针失败说起
我最早接触中断子系统是在RK方案上调一个I2C触摸屏驱动。设备树里明明加上了interrupts = <0 39 IRQ_TYPE_LEVEL_LOW>,驱动中也正常调了request_threaded_irq,可一按屏幕,客户端的中断回调就是不走。当时第一反应是怀疑驱动中断号写错了,但是反复对照原理图和手册,中断号没有问题。最后查了半天,发现问题出在中断控制器映射环节——这个平台上GIC的SPI编号和驱动里用的Linux虚拟中断号之间,还需要经过irq_domain做翻译,而我在设备树里写的中断号对应的触发类型与硬件实际不一致,导致中断在控制器层面就被丢弃了。
经历这次之后我才意识到,脱离整体框架去调中断,基本就是蒙着眼睛摸象。中断子系统绝不是“配置一个中断号、注册一个回调函数”这么简单,它是一条完整的数据链路:外设触发信号经过中断控制器汇聚,再经过CPU的异常处理入口,最后进到内核通用中断层,由框架查找对应的中断描述符,再调用驱动注册的回调。任何一环出了问题,表现出来都是“中断不工作”,但根因可能相差十万八千里。
1.2 中断子系统的三层架构
Linux的中断子系统从软件层面看,大致可以分成三层。
第一层是硬件驱动层,包括具体的中断控制器驱动,比如GIC、GICv2、GICv3,或者一些老平台上的MPIC、AIC等。这一层负责把硬件中断信号转换成内核能处理的抽象事件,实现的实体落在drivers/irqchip/目录下。它的核心职责是做中断控制器的初始化和底层操作,比如配置触发类型、屏蔽和使能某条中断线、应答中断、结束中断等,对应到代码就是irq_chip结构体里的irq_mask、irq_unmask、irq_ack、irq_eoi这些回调。
第二层是通用中断处理层,也是核心框架所在,代码集中在kernel/irq/目录。这一层不关心具体的中断控制器是什么型号,也不关心是GPIO中断、MSI中断还是共享中断,它维护的是“中断号→中断描述符→处理动作”这张大表。驱动里调用的request_irq、free_irq、enable_irq最后都会落到这一层。框架自己还提供了一套机制,把中断描述符irq_desc和回调链表irqaction管理起来,保证一个中断号可以被多个驱动共享,也保证中断上下文、线程化中断、底半部机制能够正常工作。
第三层就是具体外设驱动层,比如I2C控制器驱动、网卡驱动、触摸屏驱动。这一层要做的就是在probe函数里申请中断资源,在remove里释放,在回调里处理具体的外设事件。对于驱动移植来说,大部分工作都集中在这一层,但因为这一层离硬件最远,遇到问题反而最容易被误导。
这三层之间的关系用一句话概括就是:外设驱动告诉内核“我想处理某个中断”,通用层负责管理“这个中断怎么路由、怎么触发、怎么回调”,而硬件层负责真正操作中断控制器。驱动移植的时候如果只盯着第三层,那就丢了前两层的上下文,一旦标准流程走不通就无从下手。
1.3 两条映射主线把中断串联起来
理解了分层之后,真正需要掌握的还有两条映射主线。
第一条映射是从硬件中断号到Linux虚拟中断号。硬件中断号是中断控制器视角下的编号,比如GIC上的SPI编号32、33、34……而驱动代码里使用的却是Linux虚拟中断号。这两者之间不是简单相等的,需要经过irq_domain建立映射关系。设备树里的interrupts属性会描述设备挂在哪个中断控制器、使用哪个硬件中断号,内核在设备驱动probe时通过platform_get_irq去解析,然后由irq_domain分配一个虚拟中断号返回给驱动使用。
第二条映射是从虚拟中断号到具体回调函数。内核用irq_desc来记录某个虚拟中断号的全部信息,包括底层irq_chip、当前状态、深度计数、用户挂载的处理函数链表irqaction等。驱动调用request_irq注册处理函数,本质上就是把一个irqaction节点挂到对应irq_desc的action链表上。
这两条映射主线相当于整个中断子系统的“骨架”。搞懂它们,再看中断上半部、下半部、线程化、共享中断这些概念,就不会觉得零散。就好比你知道了前台电话分机表(虚拟中断号)和后台接线员(irq_chip)的关系,哪个分机响铃该找哪个部门(驱动回调),一切都有据可查。
2. 核心数据结构与API,移植时绕不开的细节
2.1 irq_domain:硬件中断号与虚拟中断号之间的翻译官
irq_domain可能是整个中断子系统里最容易被忽略、却最影响移植成败的结构。它的历史背景是ARM平台普遍采用设备树后,硬件中断控制器型号、中断号分配方式多种多样,内核需要一个通用的“域名映射”机制,让每个中断控制器把它的硬件IRQ编号翻译成全局唯一的Linux虚拟中断号。
irq_domain的本质是维护一个映射表,由中断控制器驱动创建和注册。创建一个domain时,驱动会指定类型,常见的有IRQ_DOMAIN_IS_LINEAR(线性映射,直接用硬件中断号索引虚拟中断号)和IRQ_DOMAIN_IS_TREE(树状映射,适合大量稀疏中断号)。对于GIC这类中断号分布比较密集的控制器,一般用线性映射,hwirq从0开始到几百,查找效率O(1)。而对于分配比较复杂的多级中断控制器,比如PCIe MSI、GPIO扩展芯片,用树状映射能够节省内存,查找时走radix tree。
设备驱动拿到一个虚拟中断号之后,如果想知道它对应的硬件中断号,可以用irq_get_irq_data拿到irq_data结构,再访问irq_data.hwirq成员。反过来,如果你在中断回调里拿到了irq号,想确认是不是你注册的那个外设中断,也能用这个字段做调试打印。
驱动移植时,最常见的irq_domain相关错误是配错了interrupt-parent。设备树里interrupt-parent指向的是中断控制器节点,如果这个节点类型不对,或者域还没注册就尝试解析,得到的虚拟中断号就会是-EPROBE_DEFER或0,导致platform_get_irq返回失败。还有一种情况是硬件中断号超过domain的线性表范围,irq_create_mapping会分配失败,这种情况下驱动申请中断时会得到-EINVAL。
2.2 irq_chip、irq_data、irq_desc、irqaction之间的关系
这四个结构体是最容易混淆的,我画过无数次图才真正分清。它们的定位差异其实很清晰。
irq_chip描述的是中断控制器硬件操作的集合,包含irq_mask、irq_unmask、irq_ack、irq_eoi、irq_set_type、irq_set_wake等函数指针。它不是针对某一个中断,而是针对一整类中断控制器的操作方式,所以它挂在irq_desc下面,可以被多条中断线共享。驱动移植时一般不需要改irq_chip,但需要判断平台是否已经把整个中断控制器的chip注册好。
irq_data是irq_chip和具体中断号之间的“实例上下文”。它记录了这个中断在当前控制器的节点位置、硬件中断号、以及右手边的irq_chip和irq_domain。很多底层API操作中断时都以struct irq_data *d为唯一参数,比如irq_set_irq_type内部会变成irq_set_irqchip_state,最终调用到chip的回调。irq_data中还有common字段,用于保存触发类型、状态标志等。
irq_desc是整个中断子系统的核心对象,每个虚拟中断号都对应唯一一个irq_desc。它包含了irq_data、irq_chip、action链表、状态字段、线程任务、深度计数depth等信息。简单说,它就是内核管理一条中断线的全部档案。
irqaction则是驱动注册时挂在irq_desc上的操作节点,包含handler(中断处理函数指针)、dev_id(设备标识,共享中断时用来区分谁触发的)、name、dev_name等。同一个号上挂多个irqaction就形成了共享中断链表,中断触发时内核会遍历链表逐一调用所有处理函数。
这四者的层级关系是:irq_desc位于最顶层,内含irq_data;irq_data内含irq_chip指针;irq_desc尾部挂着irqaction链表。驱动负责创建irqaction,内核框架负责串起整个结构。移植时遇到cat /proc/interrupts里面看不到你的设备,多半是irqaction没有成功挂上去,或者挂上去立刻又因为request_irq失败被卸载了。
2.3 常用API的适用场景与参数细节
驱动移植时和中断相关的API,用得最多的也就是下面这几个,但每个都有一些文档上不常提的坑。
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev_id)是最基本的注册接口。如果回调在中断上下文处理耗时不长,可以用这个。要注意dev_id参数:不使用共享中断时可以传NULL,但如果你的中断号被多个设备共享,这里必须传一个能唯一标识设备的指针,通常是设备结构体或客户数据指针。如果不传,中断触发时内核就没有办法区分到底是哪个设备产生了事件,处理函数被误调用的概率极高,而且free_irq时也会因为无法定位节点而出问题。
request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long flags, const char *name, void *dev_id)是线程化中断的注册接口。handler是快速检查函数,在中断上下文中执行,需要快速返回IRQ_WAKE_THREAD来唤醒线程化回调thread_fn。如果不用handler检查,直接传NULL,那就相当于所有处理都在内核线程中完成,适合处理耗时长的外设事件,比如触摸屏的点位上报、I2C读写等。I2C控制器驱动里,我一般都用线程化接口,因为I2C总线访问本身就很慢,放在硬中断里会拖垮系统。
disable_irq和enable_irq是成对使用的控制接口。要注意disable_irq在中断回调内部被调用时,会等待当前中断处理完毕后才返回,所以有死锁风险。如果确认要在中断上下文关闭中断线,用disable_irq_nosync。这个坑我踩过:底层驱动里对某个共享中断线调了disable_irq,结果回调还没返回就自己把自己锁死,系统直接hang住。
free_irq用于释放中断。如果中断是共享的,内核会检查dev_id是否匹配,释放成功后,其他设备的回调仍然保留。这个接口必须在probe失败或remove时调用,否则形成中断回调泄漏,后续再来一次request_irq就会返回-EBUSY。
local_irq_save/local_irq_restore是关/开本CPU中断的接口,一般只用于临界区很短、不允许被中断打断的代码路径。驱动里如果用它包住一个耗时操作,那就是在给整个系统埋雷,因为关中断时间过长会直接导致实时任务超时,严重时候看门狗都会复位。
2.4 设备树里中断相关的字段
设备树是驱动移植时最常打交道的地方,它里面中断相关的字段不多,但每个字段的语义都得很清楚。
常见节点中,interrupt-parent指定该设备使用哪个中断控制器节点作为父节点。如果省略,内核会向上追溯父节点,一路找最近的interrupt-controller属性。
interrupts属性是一个或多个三元组。每个三元组在不同中断控制器下含义不同。对于GIC,第一个数字表示类型,0对应SPI(共享外设中断),1对应PPI(私有外设中断);第二个数字是中断号;第三个数字是触发类型。比如interrupts = <0 39 IRQ_TYPE_LEVEL_LOW>表示SPI第39号中断,低电平触发。这里最容易错的是SPI/PPI的编号偏移,不同GIC版本的电路图标注方式有差异,特别是有时候电路图上标的是物理中断号,需要减16或者做别的偏移换算,不然就会映射到别的外设上。
interrupt-names是给interrupts数组里每个中断起个名字,方便驱动通过platform_get_irq_byname按名称获取中断号。驱动移植时改设备树,经常会遇到两个平台的中断顺序不一样,但名称相同的情况,用byname接口就比死磕索引号要稳得多。
wakeup-source和wake相关属性则用于电源管理,如果设备需要支持系统唤醒,在设备树里可能还需要配合interrupts的触发类型配置为电平唤醒。这里有个细节:同一个中断既要在正常运行时响应中断事件,又要在休眠时唤醒系统,需要把irq_set_wake和enable_irq_wake配合使用。设备树上标注了wakeup-source,驱动里还是要显式调用device_init_wakeup,否则不会生效。
3. 基于RK平台的驱动移植实操:以I2C为例
3.1 移植前先读硬件手册和原驱动
做RK平台的驱动移植,第一步一定是先看原理图和芯片手册里关于中断控制器的描述。RK平台的SoC大多集成ARM GIC,但GIC版本以及SPI中断号的分配策略会和上游公版有差异。比如我在一款RK3568的板子上,GIC的SPI编号在设备树里是<0 39 ...>,但同样的外设,在另一款RK3588平台上就变成了<0 91 ...>。这种差异不读硬件手册根本发现不了。
第二步是把旧平台原驱动完整读一遍,特别是probe里获取中断资源的顺序。有些老驱动是在platform_driver_register之后才去查设备树,有些则依赖platform_get_resource(pdev, IORESOURCE_IRQ, 0)来拿中断资源。到了新平台上,设备树资源解析方式可能已经改成了platform_get_irq或platform_get_irq_byname,老调用方式拿到的资源可能是空。
还要注意新旧平台之间设备树节点的兼容性。如果原驱动是给某个老内核版本写的,它可能是在设备树里用interrupt-parent = <&gpio0>的方式挂了GPIO中断,而不是直接挂GIC。新平台上管脚复用变了,中断控制器路径也就变了,这时候如果照抄设备树节点,会导致中断号解析不到正确的外设,甚至直接被内核忽略。
3.2 设备树里的中断配置怎么写
以RK平台上一个外部的I2C触摸屏控制器为例。它通过I2C总线通信,触摸事件通过INT引脚产生中断请求。硬件上INT引脚接到SoC的一个GPIO,GPIO又作为中断源连接到GIC。设备树里就体现为两层中断关系。
实际写的时候,需要在I2C控制器节点下添加触摸屏子节点,或者单独给触摸屏建一个节点。常见写法是把触摸屏挂在某个i2c@节点下,并指定:
&i2c1 { touch: touch@39 { compatible = "vendor,touch"; reg = <0x39>; interrupt-parent = <&gpio1>; interrupts = <5 IRQ_TYPE_LEVEL_LOW>; interrupt-names = "touch"; pinctrl-names = "default"; pinctrl-0 = <&touch_int_l>; }; };这里interrupt-parent = <&gpio1>表示这个触摸屏的中断不是直接挂在GIC上,而是挂到GPIO1控制器。GPIO控制器本身需要在设备树里配置为级联中断控制器,也就是在gpio1节点里有interrupt-controller属性和#interrupt-cells = <2>这样的描述。内核在解析touch节点时,会先找到GPIO1对应的irq_domain,把GPIO1的bank号和引脚号换算成一个新的虚拟中断号返回给驱动使用。
这个过程中的换算关系就是irq_domain在做翻译,而且翻译过程中会自动处理GPIO bank偏移。比如代码中看不到的<5>指的是GPIO1_5这个引脚,而在底层,驱动会调用gpio_to_irq或者gpiod_to_irq来获取对应的Linux中断号。设备树配置的关键在于,GPIO控制器节点必须提前初始化完成且已经建立了与GIC的级联irq_domain,否则platform_get_irq依然返回-EPROBE_DEFER。
3.3 驱动代码里申请与释放中断
驱动里获取中断号、申请中断的流程,基本就是一套标准动作。先来看获取中断号的代码:
int irq; struct device *dev = &client->dev; irq = platform_get_irq(pdev, 0); if (irq < 0) { dev_err(dev, "failed to get irq: %d\n", irq); return irq; }如果设备树用的是interrupt-names,可以用platform_get_irq_byname(pdev, "touch")来获取。拿到虚拟中断号之后,再申请线程化中断:
struct touch_priv *priv = ...; ret = devm_request_threaded_irq( dev, irq, NULL, touch_irq_thread, IRQF_TRIGGER_LOW | IRQF_ONESHOT, "touch", priv); if (ret) { dev_err(dev, "failed to request irq: %d\n", ret); return ret; }这里用devm_request_threaded_irq的好处是在驱动卸载时自动释放,不用手动在remove里调用free_irq,少写不少错误处理代码。handler参数传NULL,表示所有处理都放到线程上下文,以避免在硬中断里做耗时的I2C访问。
IRQF_TRIGGER_LOW对应设备树里的IRQ_TYPE_LEVEL_LOW,这两个必须保持一致。如果设备树上写的是IRQ_TYPE_EDGE_FALLING,而驱动里用了IRQF_TRIGGER_LOW,中断触发类型配置会被改掉,可能出现外部设备产生下降沿之后,内核只捕获到一次电平状态变化,后续不再触发的问题。这是我在调试中真实碰到的:触摸屏按住不放,理论上应该不断上报中断,但实际只上报了一次,就是因为触发类型里配置了电平低,而设备树里写的是边沿下降。
释放中断在remove函数里可以把free_irq(irq, priv)显式调用,也可以依赖devm_机制自动处理。需要注意的是,如果驱动用多个中断资源,remove之前可能会调用disable_irq来同步关闭,防止中断回调访问正在释放的资源。
3.4 中断线程化、唤醒源与共享中断的处理
刚开始写驱动时,我习惯把中断处理函数做成硬中断,感觉这样响应快。后来在一个I2C触摸屏上吃了大亏:中断回调里调用i2c_transfer去读取触摸坐标寄存器,结果I2C控制器本身又依赖中断机制完成传输,一旦在硬中断上下文里发起新的总线事务,要么导致自锁,要么被调度器直接标记为禁止睡眠而崩溃。改用线程化中断后,同样的代码在进程上下文运行,I2C操作可以正常睡眠等待,整体稳定性提升不少。
所以只要是外设中断回调里需要做总线访问的事情,比如I2C读写、SPI读写,都建议用request_threaded_irq的线程化模式,配合IRQF_ONESHOT保证中断在回调执行期间保持屏蔽,防止中断风暴。IRQF_ONESHOT对于电平触发中断尤其重要,如果没有设置,回调还在处理电平中断,硬件信号照样拉低,中断可能被反复触发几十上百次,直接把系统拖死。
共享中断在移植时也常见。同一根中断线上挂了多个设备时,每个设备在request_irq时都必须传唯一的dev_id,并且flags里带IRQF_SHARED。在回调函数里,要调用dev_id来确认是不是自己的设备触发。如果没有这个判断,共享中断的处理函数会被调用很多次,每一台设备都要执行一遍检查逻辑,效率很低。更麻烦的是,如果某个设备压根没在probe里成功申请中断,那共享中断线上的其他设备回调永远也等不到自己的事件,因为中断触发时链路可能断在某个未注册的设备上。
唤醒源的设置也值得一提。比如系统挂起时,触摸屏如果还想通过INT唤醒系统,需要在suspend阶段调用enable_irq_wake(irq),在resume阶段调用disable_irq_wake(irq)。这一步如果不做,设备树里的wakeup-source属性就等于白写了,系统进入休眠后,外部INT信号根本打不到CPU。移植老驱动时最常见的问题就是老平台里没有做唤醒管理,新平台要求深度睡眠下触摸唤醒,遗漏这个调用就成了“看着配置了,实际上不行”。
3.5 移植动作清单
整理一个我每次做驱动移植时都会对照的中断资源清单:
| 序号 | 检查项 | 具体动作 |
|---|---|---|
| 1 | 硬件中断号 | 对照原理图、SoC手册确认SPI编号和GPIO引脚所在bank |
| 2 | 设备树父节点 | 确认interrupt-parent指向正确的GIC或GPIO控制器 |
| 3 | 触发类型 | 设备树的interrupts第三个字段与驱动flags保持一致 |
| 4 | 中断号获取 | platform_get_irq返回值必须大于0,等于-EPROBE_DEFER要处理延迟加载 |
| 5 | 申请方式 | 需要总线访问的中断必须线程化,设置IRQF_ONESHOT |
| 6 | 共享中断 | 使用IRQF_SHARED时必须传有效dev_id,回调里校验 |
| 7 | 电源管理 | 需要唤醒的设备在suspend/resume调用enable_irq_wake/disable_irq_wake |
| 8 | 卸载路径 | 确保remove路径能释放中断,避免-EBUSY |
这套动作不复杂,但每一条背后都是真实的踩坑经历。我做移植时常用打印/proc/interrupts来验证当前申请状态,这是确认中断号申请结果最快的途径。
4. 中断不触发、不工作?排查流程实录
4.1 按照信号流顺序逐层排查
中断不触发是移植中最头疼的问题。我的排查习惯是严格按照信号流向,从硬件到软件逐层确认。
第一步,用示波器或逻辑分析仪确认外设真的有中断信号输出。很多次我以为代码有问题,最后发现是外设上电后根本没开始工作,INT引脚一直是高电平。信号有了之后,再看电平极性是否符合设备树配置的触发类型。
第二步,检查设备树里的interrupt-parent和interrupts是否正确。这里有个技巧,可以通过/proc/device-tree目录下的节点信息快速验证,也可以用ls /proc/irq看看已注册的中断号列表。有一个常见现象是platform_get_irq返回了-EPROBE_DEFER,因为中断控制器驱动还没加载,这时候就回到系统日志里确认drivers/irqchip加载顺序。
第三步,打开中断子系统调试开关。编译内核时开启CONFIG_IRQ_DOMAIN_DEBUG和CONFIG_DEBUG_IRQ,启动后内核会在中断注册和解析过程中打印大量调试信息,配合trace_irq_handler_entry和trace_irq_handler_exit这些tracepoint,基本能定位到具体卡在哪个环节。我一般会先用trace-cmd或者直接挂在/sys/kernel/debug/tracing下跟踪。
第四步,如果确认内核确实收到了中断,但驱动回调不执行,重点就要看irqaction挂接是否成功。这时候可以检查/proc/interrupts里面有没有设备名,没有就说明申请阶段出了问题,查询返回值是最直接的。
4.2 proc/interrupts与debugfs的具体用法
/proc/interrupts是排查中断最基本的信息来源。每一行对应一个虚拟中断号,包含各CPU上的触发次数、中断控制器名称、设备名称等。移植时如果看到自己注册的中断号在列表里出现,但次数一直是0,说明这个中断号没有被硬件触发;如果次数快速增长,说明硬件信号一直在来,驱动回调却没有正确处理,问题大概率在回调本身或触发类型配置。
/sys/kernel/debug/irq/irqs/<irq>目录下能看到更详细的irq_desc信息,包括irq_data状态、irq_chip操作函数地址、irqaction数量和名称。比如cat /sys/kernel/debug/irq/irqs/39,输出里会显示handler名称、flags、depth等。其中depth的数值很关键,正常情况下是0,如果大于0说明该中断线被disable_irq过了,一直没有重新使能。
另外,/sys/kernel/debug/gpio可以查看GPIO中断的状态。有时候中断没触发是因为GPIO被配置成普通输出模式,中断控制器那边根本收不到信号。这个排查思路尤其适用于那些通过GPIO扩展芯片接入的中断,比如I2C转GPIO的IO扩展器,中断信号要经过扩展芯片再汇聚到SoC,每一级都有配置错误的风险。
4.3 几个真正的疑难杂症
排查过程中有些问题不是看一眼配置就能解决的。说几个我真正调试过的问题。
第一个是系统启动很强,但请求中断返回-EBUSY。遇到这个先看/proc/interrupts里该中断号是否已经被别的设备用掉了。很多时候是设备树里两个外设节点写了同一个中断号,后面申请的那个自然失败。还有一种情况是父级GPIO控制器同bank的另一个引脚被别的驱动占用了,导致整个bank的虚拟中断号都被预留。此时可以通过cat /sys/kernel/debug/irq/irqs/<irq>确认当前action链表上是谁占用了。
第二个是中断回调收到后,系统进入假死状态。这种常见于中断触发类型配置为电平触发,而回调中没有屏蔽中断或没有清硬件事件。中断控制器一看信号还在,就不断向CPU提交中断,CPU被中断淹没,表现为系统卡死。解决思路是回到回调里确认是否设置了IRQF_ONESHOT,或者通过操作硬件寄存器清除中断状态。触摸屏驱动里尤其容易踩到这个坑,因为触摸事件持续存在,不清除外设内部的中断标志,中断就会被不断重复触发。
第三个是系统休眠唤醒后,中断失效。这个多出在唤醒源处理上。设备树里写了wakeup-source,但是驱动在suspend阶段没有执行enable_irq_wake,或者中断控制器驱动没有实现irq_set_wake回调。另一种情况是唤醒时GIC的某个bank被关闭了,导致中断线根本没接上。这种问题可以通过在resume阶段重新enable_irq一波操作来解决,但更根本的做法是检查底层irq_chip的power管理回调。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
request_irq返回-EINVAL | 中断号无效或irq_domain映射失败 | 检查platform_get_irq返回值和设备树interrupts字段 |
request_irq返回-EBUSY | 中断号被占用或不支持共享 | 查/proc/interrupts和debugfs确认占用者 |
| 中断注册成功,计数为0 | 硬件信号未送达或触发类型错误 | 示波器抓波形,核对设备树触发类型 |
| 中断计数暴涨,系统卡死 | 电平触发回调未屏蔽或未清硬件事件 | 加IRQF_ONESHOT,在回调中清外部中断标志 |
| 休眠唤醒后中断失效 | 没有配置唤醒源或底层不支持wake | 检查enable_irq_wake调用和irq_chip的irq_set_wake |
| 共享中断下回调被反复误触发 | 共享中断处理没有基于dev_id区分设备 | 回调中根据dev_id判断是否是自己设备 |
| 中断线程化后无法睡眠 | 使用request_irq而非线程化接口 | 改成request_threaded_irq配合IRQF_ONESHOT |
| 设备树修改后无变化 | DTB未重新编译或加载了旧镜像 | 确认内核镜像、DTB更新,检查启动参数 |
这份速查表我也不是一次总结出来的,而是这几年代码调试里一个个记下来的。中断这个东西,最坑的就是“表面正常但实际失灵”的场景,所以排查时需要从硬件、设备树、内核框架、驱动流程四个维度同时下手。
我个人在实际操作中的体会是,做Linux驱动移植时,中断子系统最关键的认知不是记住几个API,而是建立一条“信号流”的全局视角:外设拉低引脚、中断控制器采样、CPU进入异常向量、通用中断层找到irq_desc、最后调用你注册的回调。只要这条链路在脑子里是清晰的,遇到任何异常都能按图索骥。真正踩过几次坑之后,你就会发现大多数中断问题的根因,其实都藏在设备树的某个字段、某个触发类型或者某次disable_irq没配对上面,而不是所谓的高深内核理论。最后再分享一个小技巧:新平台移植时先在probe里把platform_get_irq打印出来,配合cat /proc/interrupts确认拿到的是合理的虚拟中断号再继续往下写,这一句话能帮你省掉一大半调试时间。