做驱动移植最让人头疼的不是抄代码而是把一套代码从A平台挪到B平台时那些看不见摸不着的东西。中断子系统就是其中最典型的一个——代码层面你看到的只是request_irq加一个回调函数但真正跑起来之后硬件中断线、中断控制器、设备树节点、内核虚拟中断号、触发方式、上下半部处理……任何一个环节对不上设备就是一动不动而且还不报错。这篇文章不打算从零讲一遍中断子系统的源码而是以一个驱动移植工程师的视角把整个中断子系统的整体框架拆开揉碎说清楚从硬件中断信号产生到驱动回调函数执行中间到底发生了什么以及移植时每一步应该关注什么。不管你是刚接触嵌入式Linux的新人还是已经写过几个字符驱动、正被中断问题折磨的开发者这篇内容都能帮你把中断相关的概念串成一条线。我个人做了几年驱动移植踩过不少跟中断相关的坑比如中断号对不上、触发类型不匹配、中断线程化导致性能异常等等这些实战排查经验我也会一并整理出来。读完你至少能建立起一个完整的中断子系统心智模型以后再遇到中断相关报错能快速定位问题出在哪一层。1. 中断子系统在驱动移植中的定位1.1 为什么中断子系统是驱动移植的第一道坎驱动移植这件事说到底是让一个外设在新的硬件平台上重新正常工作。外设要工作核心就两件事CPU怎么给它发指令它怎么通知CPU自己有事了。前者靠寄存器读写也就是总线接口后者几乎全部依赖中断。你可以想象成你在办公室干活同事有事找你要么每隔几分钟就来敲一次门问你好了没有轮询要么他装了个铃铛完事按一下铃你听到再过去中断。轮询浪费CPU、响应还慢所以中断是几乎所有外设驱动的默认选择。但麻烦就麻烦在中断不是一个单纯的软件机制它是一条完整的硬件链路加软件框架。硬件上外设的中断信号要先送到中断控制器由中断控制器统一仲裁、屏蔽、优先级管理之后再送给CPU核心。软件上内核要把这条硬件链路抽象成一套统一的API让驱动开发者不用关心自己在哪颗芯片上只需要申请一个“中断号”挂一个处理函数就行。这套抽象做得好不好直接决定了驱动代码的可移植性。我在实际移植过程中发现中断子系统是驱动移植时改动量最大的部分之一。寄存器操作可能只需要改一下基地址总线通信可能只需要适配一下时钟但中断这层牵扯到设备树、中断控制器驱动、中断映射关系、平台差异甚至芯片勘误任何一个环节变了驱动行为都可能完全不同。而且中断出问题不像内存错误会立刻崩溃很多时候只是设备偶尔超时、响应慢半拍、或者干脆不工作排查起来特别费劲。1.2 中断子系统的整体分层硬件层、核心层、驱动层要理解中断子系统我习惯把它分成三层来看硬件层、核心层、驱动层。这三层各管各的事每一层都有自己需要关注的关键点。硬件层最简单也最底层就是物理信号怎么走。外设的中断引脚拉高或拉低信号沿中断线进入中断控制器中断控制器根据优先级仲裁决定先给哪个CPU核心发中断CPU收到中断后自动跳到异常向量表的对应入口保存现场然后进入内核的中断处理入口。这一层和具体芯片强相关移植时基本不需要动但要清楚自己的平台用的是哪款中断控制器、支持哪些中断类型。核心层是Linux内核中断框架的主体它负责把硬件中断控制器抽象成irq_chip把每个硬件中断源映射成内核里的irq中断号再通过irq_desc描述符管理每个中断的状态和处理流程。这一层还包含中断的开启、关闭、触发类型设置、中断线程化等核心机制。驱动开发者平时用的request_irq、enable_irq、disable_irq都是这一层提供的API。驱动层就是你我写的这部分通过request_irq或者devm_request_irq申请中断注册回调函数在回调里完成硬件状态读取、数据搬运、事件通知等工作。这一层是移植时主要改代码的地方但改的前提是理解下面两层怎么配合——尤其是设备树里的中断属性它直接决定了核心层怎么帮你把硬件中断号映射成软件中断号。这三层的关系可以类比成公司的收件流程硬件层是快递员把包裹送到前台核心层是前台登记、分拣、按部门通知驱动层是你收到邮件后开始干活。前台的登记规则如果和你理解的不一样你可能永远收不到快递。2. 中断子系统核心组件解析2.1 硬件中断的传递路径从外设引脚到CPU先走一遍硬件中断的完整路径这对后续理解一切都有帮助。拿一个典型的ARM平台举例外设比如I2C控制器、GPIO、网卡检测到某个事件后会把一根中断线拉高或者拉低。这根线接到中断控制器上ARM平台最常见的是GICGeneric Interrupt Controller现在新平台基本是GIC-400或者GIC v3/v4。GIC收到中断信号后会做几件事判断这个中断对应的优先级和当前CPU核心正在处理的中断优先级做比较如果优先级够高就把中断分发给某个CPU核心。CPU核心收到中断后硬件自动做现场保护跳转到异常向量表里的IRQ入口接下来就进入了内核代码的领地——handle_arch_irq会读取GIC的状态寄存器搞清楚到底是哪个中断号触发了然后调用对应中断描述符的处理函数。这里有一个很重要的概念硬件中断号和软件中断号。硬件中断号是外设物理中断线在中断控制器里的编号比如“GPIO0组的17号引脚”在GIC里可能是硬件中断号47而软件中断号是内核给每个中断源分配的逻辑编号驱动开发者拿到的就是它。两者之间的映射就是中断子系统最核心的机制之一后面会专门讲。GIC支持的中断类型也值得了解一下。SPIShared Peripheral Interrupt是共享外设中断多个外设可以共享一根中断线但每个外设都自己的中断号PPIPrivate Peripheral Interrupt是每个CPU核心私有的中断比如每个核心的本地定时器SGISoftware Generated Interrupt是软件触发的中断主要用于CPU核心之间的通信。驱动移植时接触最多的是SPI其次是PPI。搞清楚你驱动的中断属于哪一类很多配置问题就能迎刃而解。2.2 中断控制器抽象irq chip与它的职责内核里把中断控制器的硬件操作封装成一个结构体叫irq_chip。它包含一组函数指针比如irq_mask屏蔽中断、irq_unmask打开中断、irq_set_type设置触发方式、irq_ack应答中断、irq_set_wake设置唤醒源等。每个具体的硬件中断控制器驱动比如GIC驱动、GPIO控制器驱动都会实现自己的irq_chip并注册到内核里。驱动开发者平时不直接跟irq_chip打交道但它的实现质量直接影响中断行为。比如说有的控制器在中断处理完后需要显式写寄存器应答有的硬件自动应答有的控制器支持设置上升沿触发有的只支持电平触发。如果控制器的irq_chip实现得不完善或者驱动里设置的触发方式和irq_chip支持的不匹配就会出现中断不触发或者反复触发的问题。在驱动移植场景里irq_chip往往是不用改的——它已经由SoC厂商的内核代码写好了。但你需要知道它的存在因为在排查问题时你可能会需要看一眼某个中断的irq_chip行为。比如我在调一个网卡驱动时发现中断一直被屏蔽导致收不到包最后查下来是irq_set_type没有被正确调用控制器里的中断触发配置根本没生效这就是典型的irq_chip使用不当。另外还要提一下irq_data它是每个中断描述符里保存的底层数据包含硬件中断号、irq_chip指针、芯片私有数据等。你在设备树里看到interrupts GIC_SPI 23 IRQ_TYPE_LEVEL_HIGH张的GIC_SPI就是告诉内核“这是一个SPI中断硬件中断号23高电平触发”内核解析后会把它映射成一个软件中断号并把irq_chip指向GIC驱动。这个解析过程就是接下来要说的irq domain。2.3 irq domain硬件中断号与虚拟中断号的桥梁这是我个人认为中断子系统里最重要也最容易被忽视的概念。irq domain存在的意义是把硬件中断号比如GIC上的中断线编号映射到Linux内核的虚拟中断号也就是驱动里request_irq拿到的那个数字。为什么要这么绕因为硬件中断号在不同的上下文里含义不同。GPIO控制器里的一根引脚跟GIC里的一个中断号两者的编号体系完全独立。内核为了统一管理就引入了irq domain这个概念每个中断控制器对应一个domaindomain内部维护自己的中断映射关系。设备树里描述中断时先通过interrupt-parent指定用哪个控制器再用interrupts属性指定在这个控制器里的中断号和触发方式内核的irq_create_of_mapping会遍历所有domain找到匹配的那一个建立映射。驱动移植时最典型的报错场景设备树里写了中断属性但驱动platform_get_irq返回负数或者返回了一个看起来不对的数字。这种问题十有八九是irq domain映射失败——要么interrupt-parent写错了要么中断号超出控制器范围要么中断控制器驱动的domain没有正确注册。我在RK平台上移一个触摸驱动时就遇到设备树里interrupt-parent指向了GPIO4但GPIO4对应的domain在某个内核版本里没注册成功结果gpio_to_irq返回的还是一个错误码查了半天才发现是新内核改了GPIO控制器驱动注册顺序。如果你用的是GPIO中断按键、传感器这类走的都是GPIO中断那映射路径又有点不一样。GPIO本身不是GIC直接管的中断而是GPIO控制器先汇总自己的中断状态然后把汇总后的中断信号接到GIC的一个或者几个SPI上。也就是说一个GPIO中断在软件上要经过两级映射先由GPIO控制器的domain映射出一个虚拟中断号再由GIC的domain映射出最终的软件中断号。这两级映射对驱动开发者来说是透明的你可以直接用gpiod_to_irq拿到最终的中断号——但要理解这条链路否则排查时会想不通为什么一个GPIO中断会和GIC扯上关系。3. 中断处理机制与驱动开发要点3.1 中断申请与释放request_irq全家桶怎么选驱动里申请中断最常用的就是request_irq、request_threaded_irq和它们的devm版本。这里先说最经典的函数原型int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);第一个参数irq就是前面说的软件中断号可以通过platform_get_irq、gpio_to_irq、of_irq_get等接口获取。第二个参数是中断处理函数返回irqreturn_t。第三个参数flags是中断标志最关键的是触发方式标志比如IRQF_TRIGGER_RISING上升沿触发、IRQF_TRIGGER_FALLING下降沿触发、IRQF_TRIGGER_HIGH高电平触发、IRQF_TRIGGER_LOW低电平触发。还有一个常用标志是IRQF_SHARED表示允许多个驱动共享这根中断线。驱动移植时触发方式标志我建议优先从设备树或者平台数据里读取而不是代码里写死。因为同一个外设在A平台上可能是低电平触发到B平台可能变成下降沿触发如果代码硬编码到了新平台就踩坑。我在好几个项目里见过这种情况驱动代码写了IRQF_TRIGGER_FALLING但新平台的中断源在设备树里定义的是IRQ_TYPE_LEVEL_LOW造成一申请中断就直接高频触发把CPU跑满。后来我的习惯是能用devm_request_irq就尽量用它并且在参数里传IRQF_TRIGGER_NONE让内核优先采用设备树里配置好的触发方式。request_threaded_irq是另一个常用接口它的特点是允许你传入一个线程化处理函数中断触发后内核会创建一个内核线程来执行你的处理逻辑这样你就能在中断处理里调用那些可能会睡眠的函数比如i2c_transfer、mutex_lock等。普通的中断回调是原子上下文不能睡眠这是很多新手写驱动时犯的错误。如果你遇到“sleeping function called from invalid context”这种报错基本就是中断上下文里调了不能调用的函数。线程化中断正好可以解决这个问题后面专门一节讲。释放中断对应的是free_irq(irq, dev)如果是devm版本则不需要手动释放设备卸载时会自动处理。这里有个小细节free_irq的第二个参数dev必须和申请时传的是同一个指针内核用它来匹配释放哪条中断描述符。申请和释放不匹配日志里会有“Trying to free already-free IRQ”之类的提示。3.2 上半部与下半部紧急的事先做耗时的事后做中断处理函数里不宜做太多事情。中断到来时CPU正在执行的进程会被打断如果中断处理时间过长会导致系统响应变慢甚至丢中断。所以Linux把中断处理分成了上半部top half和下半部bottom half。上半部就是request_irq注册的那个handler它执行在中断上下文要求快进快出。典型做法是读硬件状态寄存器清除中断标志把数据从一个受限的缓冲区拷贝出来然后决定是否需要启动下半部处理最后返回IRQ_HANDLED。凡是不紧急、不需要立刻处理的事情都应该放到下半部去。下半部有几种实现方式从老到新依次是软中断softirq、tasklet、工作队列workqueue、以及线程化中断。它们各有各的定位。软中断是内核内部用的驱动开发者一般不直接写tasklet是基于软中断实现的执行在软中断上下文仍然不能睡眠但可以被多次调度适合耗时较短且不需要睡眠的场景workqueue执行在进程上下文可以睡眠适合耗时较长的处理线程化中断则把整个中断处理过程放进一个内核线程让处理函数彻底摆脱原子上下文的限制。我个人的选择习惯是如果处理逻辑很短比如读个状态寄存器、清除标志、设置一个标志位那就直接在上半部搞定不需要下半部如果处理逻辑涉及I2C/SPI通信比如触摸屏上报坐标前要读寄存器那就必须用workqueue或者线程化中断因为I2C通信是会睡眠的。这里有个常见的误区——tasklet虽然延迟执行了但它还在软中断上下文一样不能睡眠很多人把tasklet当成“可以睡眠的下半部”结果照样报错。3.3 中断线程化让中断处理函数“敢睡觉”线程化中断是Linux内核为实时性做的几个重要改造之一。它的核心思想是为每个中断创建一个内核线程中断触发时唤醒这个线程由线程来执行driver注册的handler。因为这个handler现在运行在普通进程上下文所以可以调用msleep、i2c_transfer、mutex_lock等阻塞型函数编写逻辑时不用再背着“不能睡眠”的枷锁。线程化中断通过request_threaded_irq来申请。它有两个回调参数第一个handler是线程化之前的快速处理函数可以为NULL第二个thread_fn是线程化之后执行的主处理函数。当你把第一个参数设为NULL内核会采用默认的irq_default_primary_handler直接唤醒线程如果第一个参数不为NULL则先执行快速处理如果返回值是IRQ_WAKE_THREAD才会唤醒线程执行thread_fn。在驱动移植场景里另一个跟线程化相关的场景是设备树中加interrupt节点的同时在驱动里设置IRQF_ONESHOT标志。IRQF_ONESHOT的含义是中断线程化处理期间自动屏蔽该中断直到线程函数返回才重新打开。这个标志主要用于那些电平触发、且无法在硬件上自动屏蔽的共享中断防止线程执行期间同类中断反复触发导致系统崩溃。很多新人在写GPIO按键驱动时用request_threaded_irq申请了中断线程但忘了加IRQF_ONESHOT结果按键按一下中断就风暴一样触发仔细想想多半是这个问题。不过线程化也不是银弹。它对中断延迟有影响——从硬件中断触发到线程函数真正执行中间隔了一个线程调度的时间对于高速数据采集之类的场景延迟可能不可接受。所以什么时候用线程化什么时候用传统上半部tasklet要看你外设的实际需求。我的经验是凡是处理逻辑需要I2C这类会睡眠的通信直接线程化凡是要求低延迟、高频率的数据通路尽量压在上半部完成用workqueue或tasklet做后处理。3.4 共享中断与唤醒中断两个容易忽略的细节共享中断在嵌入式平台上很常见尤其是在GPIO控制器上——多个设备的中断线都接到同一个GPIO引脚组由GPIO控制器统一上报。申请共享中断时需要带IRQF_SHARED标志同时设备树里要把多个设备的interrupts属性指向同一个中断源。中断触发时内核会遍历所有注册在这条中断线上的handler逐个调用直到有函数返回IRQ_HANDLED。共享中断特别要注意两点。第一所有共享这条中断线的驱动申请时必须带IRQF_SHARED只要有一个驱动没带后续申请就会失败第二free_irq时传入的dev指针也要互不相同否则内核无法区分。我遇到过一个问题同一个GPIO中断线上挂了按键和电量计两个设备按键驱动先申请了中断带了SHARED电量计驱动也带了SHARED但电量计的handler里没清除中断源导致按键中断一直被电量计的中断状态“喂养”疯狂触发。最后是打印寄存器才定位到是电量计的中断标志没清干净。唤醒中断是低功耗场景里的关键机制。系统进入suspend后大部分中断被屏蔽但有些设备需要在休眠状态下依然能唤醒系统比如按键、RTC闹钟、充电检测。要在驱动里支持唤醒首先设备树里中断节点要加wakeup-source属性然后驱动里调用enable_irq_wake(irq)。注意一个细节enable_irq_wake和disable_irq_wake必须成对出现并且不能嵌套调用否则系统suspend时会报Unbalanced enable for IRQ的警告。我还见过一种坑设备树里没有指定wakeup-source驱动里调用enable_irq_wake却返回EINVAL因为内核在irq_set_wake时发现设备没被标记为可唤醒。这个问题的排查其实很简单检查设备树节点有没有wakeup-source没有就补上再确认内核配置里没有禁用该中断控制器的电源管理。4. 驱动移植中的中断适配实操4.1 设备树中断属性移植第一步设备树是中断子系统与驱动开发者之间最重要的接口。你要做的第一件事就是搞清楚目标平台上你的外设中断信号到底从哪根线进去它是接到GIC上还是GPIO控制器上然后在设备树里把这条链路描述清楚。最常见的中断描述方式是这样的i2c2 { touchscreen: touch38 { compatible vendor,touch; reg 0x38; interrupt-parent gpio3; /* 中断信号接在gpio3控制器 */ interrupts 16 IRQ_TYPE_LEVEL_LOW; /* GPIO3组的第16号引脚低电平触发 */ }; };这段描述的核心是两个属性interrupt-parent指定中断控制器interrupts指定在这控制器里的编号和触发方式。如果中断直接接到GIC上interrupt-parent就写成GIC节点通常是gicinterrupts会写成0 33 IRQ_TYPE_LEVEL_HIGH这样的三元组——第一个数字0表示SPI类型1代表PPI第二个数字是中断号第三个还是触发类型。移植时最容易错的就是GPIO编号。GPIO控制器的interrupts里的数字到底是物理引脚号还是软件GPIO号取决于厂商的GPIO驱动怎么定义domain。同一个IRQ_TYPE在A平台是IRQ_TYPE_LEVEL_LOW但B平台GPIO控制器实际是低电平触发如果设备树照搬过来就会导致中断一直挂在低电平上反复触发。所以拿到一份新的设备树第一步不是改驱动代码而是对着原厂的硬件手册和参考设备树把中断属性改成目标平台的实际值。还要注意一个点设备树里interrupt-parent如果全局统一写在根节点上那么所有子节点都默认继承这个中断控制器。很多驱动开发者因为根节点已经写了interrupt-parent gic就忘了给具体外设节点单独指定结果外设实际中断信号接在GPIO上却被当成GIC中断解析拿到一个错误的中断号。这种问题在日志里通常表现为platform_get_irq返回非负但是一个很大的数或者返回-EPROBE_DEFER。4.2 驱动代码里拿中断号的几种姿势设备树配置好之后驱动里怎么把这个中断号取出来我整理了几种最常用的方式按适用场景分类。第一种是平台设备驱动通过platform_get_irq获取适用于设备树节点里直接写了interrupts属性的情况int irq platform_get_irq(pdev, 0); if (irq 0) { dev_err(pdev-dev, failed to get irq\n); return irq; }这个接口返回的是已经经过irq domain映射的软件中断号可以直接传给request_irq或request_threaded_irq。第二个参数0表示取第一个中断如果节点里有多个中断比如收发分离的UART就依次取0, 1, 2...。第二种是GPIO中断场景用gpiod_to_irq。这个适合设备树里用gpio属性描述引脚、然后要在驱动里获得对应中断号的场景struct gpio_desc *desc devm_gpiod_get(pdev-dev, irq, GPIOD_IN); int irq gpiod_to_irq(desc);这种方式的优势是不用手动解析中断属性内核帮你处理了GPIO控制器到GIC的两级映射。缺点是要在驱动里额外获取GPIO描述符如果设备树同时声明了interrupts属性注意别重复申请中断资源。第三种是通用底层接口of_irq_get适用于直接操作设备节点的场景比如你在一个子系统里没有platform_device只有device_nodestruct device_node *np dev-of_node; int irq of_irq_get(np, 0);这三种方式拿到的中断号本质上是同一个东西都是软件中断号。区别只在于获取路径和适用场景。我建议平台设备驱动一律用platform_get_irq简单明了GPIO中断没有对应的平台设备中断描述符时用gpiod_to_irq在board文件或框架代码里临时取中断时用of_irq_get。大多数驱动移植代码里你真正需要改的往往不是获取中断的方式而是设备树里中断属性的值。4.3 移植实例GPIO中断按键驱动的适配过程用一个具体的例子串一下整个流程。假设我要把一个按钮驱动的代码从A平台移植到B平台按键的中断信号在A平台接在GPIO5的12号引脚上硬件设计上按键按下时引脚电平从高电平变为低电平。原平台设备树这么写button { compatible mygpio,button; interrupt-parent gpio5; interrupts 12 IRQ_TYPE_LEVEL_LOW; };驱动里用platform_get_irq拿中断号然后用devm_request_threaded_irq申请线程化中断在thread_fn里上报事件。这个驱动在A平台跑得好好的。现在到了B平台硬件设计把按键接到了GPIO1的5号引脚且按下时是从低电平变高电平上升沿触发。那么我要做的事情有两件。第一改设备树button { compatible mygpio,button; interrupt-parent gpio1; interrupts 5 IRQ_TYPE_EDGE_RISING; };第二检查驱动代码里有没有写死中断号和触发方式。如果驱动里是request_irq(irq, handler, IRQF_TRIGGER_FALLING, ...)这种写法就一定要改成从设备树读取的方式或者把触发标志改成IRQF_TRIGGER_NONE让内核采用设备树配置。这里有个实操细节很多人会忽略gpio1的interrupt-parent在B平台可能不是直接指向gpio1而是先经过一个gpio扩展芯片比如pca953x这类I2C GPIO扩展器那么在设备树里需要描述的链路就更复杂。这种情况下gpiod_to_irq要比platform_get_irq更可靠因为GPIO扩展器驱动已经自己在domain里完成了映射。如果强行用platform_get_irq返回的中断号可能是在扩展器域里的编号直接传给request_irq就是错的。遇到这种场景我的建议是设备树里用gpio属性和interrupts属性二选一驱动里优先用gpiod_to_irq。移植完成后验证步骤也很重要。先看/proc/interrupts里有没有对应中断号的计数在增长再实测按键按下确认中断触发一次如果触发了很多次检查是不是电平触发方式下没有清除硬件中断标志。这套流程下来90%的GPIO中断问题都能解决。5. 中断问题排查与避坑实录5.1 从/proc/interrupts读中断状态驱动移植完第一件事就是看/proc/interrupts。这个虚拟文件会把系统里所有注册过的中断列出来每一列对应一个CPU核心后面的数字是触发次数。格式大致如下CPU0 CPU1 16: 1234 0 GICv3 30 gpio1 22: 56 78 GICv3 193 eth016和22是软件中断号GICv3是中断控制器名字后面的数字是硬件中断号最后一个字段是设备名。排查时重点看两件事第一你的设备对应的中断行存不存在第二中断触发时对应行的计数有没有增长。如果中断行不存在说明中断没注册成功回头查platform_get_irq有没有报错、设备树解析是否成功。如果行存在但触发时计数不增长说明中断信号根本没到CPU——硬件链路有问题或者中断被屏蔽了查/proc/irq/16/spurious看是不是有异常报告。如果计数疯涨那就是中断风暴多半是触发类型配错或者中断标志没清除。除了/proc/interrupts还可以看/sys/kernel/debug/irq/irqs这个调试文件需要挂载debugfs里面记录了每个中断的描述符状态、irq_chip回调信息、触发次数等比/proc/interrupts详细得多。排查中断被谁屏蔽时这个文件非常有用能看到irq_mask状态和当前是否处于disabled状态。5.2 中断风暴与响应延迟的排查思路中断风暴是驱动移植里最头疼的问题之一典型症状是CPU占用率飙升到接近100%系统卡顿/proc/interrupts里某个中断的计数像计数器一样疯涨。排查思路按顺序走第一步确认中断源。看/proc/interrupts里哪个中断计数异常记下它的中断号和设备名。第二步确认触发类型。回到设备树对照硬件逻辑确认是电平触发还是边沿触发是不是和实际信号极性反了。第三步确认有没有正确清除硬件中断标志。很多外设的中断状态寄存器需要软件写1清0如果驱动handler里漏了这一步电平触发中断会一直保持有效导致反复触发。第四步确认共享中断的handler有没有误报。多个设备共享一根中断线时每个handler都要检查对应设备是否真的有事件没有就返回IRQ_NONE。我最开始写驱动时犯过一个低级错误GPIO按键驱动用的是IRQF_TRIGGER_FALLING但硬件上按键按下时电平一直被拉低直到松开才恢复高电平这在硬件逻辑上其实是电平触发。结果就是按键按下的瞬间中断触发一次然后因为电平一直低又反复触发直到松开才停下来。换成IRQF_TRIGGER_LOW加IRQF_ONESHOT后问题立刻消失。这个例子说明触发方式的选择不是看“按下”这个动作是上升还是下降而是看整个事件期间电平状态怎么变化。响应延迟的问题和中断风暴正好反过来中断触发了但handler执行得太慢导致数据丢失或者系统卡顿。排查时先看handler里有没有耗时的操作比如printk、i2c_transfer有就移到workqueue或线程化处理里再看是不是频繁disable_irq/enable_irq中断屏蔽和打开本身也耗时间最后看中断优先级和CPU亲和性设置如果平台支持可以把中断绑定到特定CPU核心上。5.3 驱动移植中断常见问题速查表我在实际工作中把中断移植的问题整理成了一张速查表基本能覆盖大部分场景现象可能原因排查手段platform_get_irq返回负数设备树中断属性缺失、interrupt-parent写错检查设备树节点用of_irq_get打印错误码request_irq返回-EINVAL中断号非法、触发方式不支持、中断已被独占查/proc/interrupts看中断是否已被注册检查触发标志request_irq返回-EBUSY中断已被其他驱动申请且未带IRQF_SHARED确认两个驱动都带IRQF_SHARED看内核日志确认抢占者中断触发但不进handler触发方式和硬件信号不匹配对比设备树触发类型和硬件波形逻辑中断风暴CPU占用过高电平触发没清中断标志、共享中断handler误报检查handler有没有清除硬件中断标志共享中断是否都返回了正确状态中断线程函数报sleeping function called线程化中断没生效IRQF_ONESHOT没配确认用request_threaded_irq且thread_fn在进程上下文执行suspend后无法唤醒设备没有wakeup-source或没调用enable_irq_wake检查设备树唤醒属性确认调用成对且成功enable_irq_wake返回-EINVAL中断控制器不支持唤醒、设备未标记可唤醒检查控制器驱动有没有实现irq_set_wake这张表不是万能药但大部分驱动移植的中断问题都能在这张表里找到大方向。排查时先看内核日志再按表里的顺序逐层排除通常会比漫无目的地东改西试快很多。6. 移植中断子系统我的几点实操心得讲了这么多最后分享几条这几年跟中断子系统打交道攒下来的实操体会。第一设备树优先于代码。凡是中断触发方式、中断号这类跟硬件平台强绑定的信息尽量放在设备树里而不是写死在驱动里。这样平台变更时往往只需要改设备树驱动代码一行不用动。反过来如果驱动里写死了触发标志换了平台就很容易踩坑。第二排查中断问题先看/proc/interrupts和内核日志再动代码。我见过很多同事一上来就怀疑驱动代码逻辑改半天没效果最后发现是设备树里interrupt-parent指错了对象。中断链路很长从设备树到irq domain到irq_chip到handler任何一个环节都可能出问题按链路逐层排查是最快的。第三中断处理函数越短越好。函数里做的事情越多出问题的概率越大。能用workqueue、线程化中断或tasklet处理的尽量不要全部堆在上半部。尤其是涉及I2C/SPI这类慢速总线的操作哪怕只是读一个寄存器也会因为总线时钟的延迟拖住整个中断流程。第四学会看内核的调试信息。/sys/kernel/debug/irq/irqs、/proc/interrupts、/proc/irq/*/spurious这些接口没事多翻翻很多中断问题其实在状态字段里已经写得很明白了。驱动移植工作里七分靠理解框架三分靠排查手段两者缺一不可。