
1. 为什么驱动移植最常栽在中断这个话题上做Linux驱动移植的兄弟应该都有体会GPIO、I2C、SPI这些外设只要能点亮、能读写心里基本就有底了。真正让人头皮发麻的往往是不起眼的中断。设备树配好了、驱动代码也写了结果中断要么不来要么来了就死机要么就是报irq xxx not found这种摸不着头脑的错误。我见过不少同事在新平台移植时一碰中断就是两三天起步最后要么靠到处打补丁糊过去要么干脆绕开中断改轮询用着用着就开始掉性能。问题出在哪大多数时候不是代码写错了而是对Linux中断子系统的整体框架没有建立清晰的认知。中断不是简单的一个硬件产生信号、CPU跳到handler跑一下这么简单。在Linux里它是一整套精密的软件体系分硬件层、通用层、驱动层还夹着设备树映射、中断控制器驱动、软中断和线程化机制。任何一个环节没对齐表现出的症状可能完全不同但又都指向中断。想做好驱动移植尤其是从旧内核往新内核、或从一个SoC往另一个SoC迁移驱动时最关键的是先看懂这套框架是怎么把硬件中断源变成驱动里那个int irq的。这篇内容就围绕这个主题把中断子系统从硬件到驱动、从数据结构到实际操作梳理一遍。适合刚接触内核移植、被中断搞懵了的驱动工程师也适合那些用了好几年request_irq却没搞明白背后链条的朋友。2. 中断子系统的核心从管脚到irq number的两级映射在驱动代码里我们拿到的那个中断号比如IRQ 23、IRQ 61不是硬件手册里那个中断号。这个坑几乎所有新手都会踩而且踩得很隐蔽。硬件那边说GPIO中断是17你在设备树里写17结果request_irq17跑起来发现完全不对中断要么不触发要么一触发就进错处理函数。为什么因为Linux内部有一套自己的编号体系。2.1 硬中断号、软中断号和hwirq之间的关系硬件比如GIC中断控制器给每个中断源分配一个编号这叫做硬件中断号hwirq。而Linux在内核里也维护了一套编号叫Linux中断号irq number。这两者之间不是相等的需要通过中断控制器驱动来进行映射。以ARM平台常见的GIC为例SoC内部的各种外设中断源会接到GIC的某个INTIPrivate Peripheral Interrupt共享外设中断这个INTI的编号就是硬件手册里说的那个数字。但由于GIC可以级联、可以有多个实例、还可以处理SPI和PPI的区别这串数字进到Linux之后往往要经过一次转换才会变成子系统内部使用的irq number。这就像一个大楼里有好几部电梯每层楼还有一个内部房间号。你从楼外看电梯按钮上是5层但进了每个房间门口挂的门牌可能标着305或者B-12。设备树的作用就是告诉内核5层对应305房间。2.2 irq domain中断映射的中枢机构在老的2.6内核年代中断映射很多靠一张固定的静态表搞定换平台就换表代码里到处是ifdef。这到后期根本维护不动。在较新的内核里引入了irq domain这个核心概念。irq domain说白了就是一个翻译官它负责在hwirq和Linux irq number之间建立对应关系并且管理一段中断号的申请与释放。一个中断控制器对应一个irq domain。当你在设备树里写interrupts gic 1 2 3这种属性时内核解析到这个引用知道是走哪一个domain然后在这个domain内部申请一个空闲的Linux irq number再和hwirq绑在一起。这就是为什么你在设备树里写的数字和在驱动里拿到的数字往往是两回事——因为设备树描述的是硬件中断源hwirq驱动拿到的是Linux中断号。中间隔着一个irq domain的翻译过程。注意这个翻译过程不是所有平台都相同。GIC的domain逻辑比较规整但一些老式中断控制器、GPIO控制器模拟中断的情况就复杂得多。后面会单独讲GPIO控制器这种非标准中断源怎么处理。3. 中断控制器驱动和irq_chip底层是怎么工作起来的如果只是想写个普通外设驱动不一定要深入中断控制器驱动代码。但做移植特别是从零适配一个新板子时中断控制器驱动是整个系统能跑起来的前提。它相当于中断子系统这栋大楼的地基。3.1 irq_chip到底管什么每个irq domain下通常会有一个irq_chip结构体。它描述了这个中断控制器硬件是怎么操作的。典型的回调函数包括struct irq_chip { void (*irq_mask)(struct irq_data *data); void (*irq_unmask)(struct irq_data *data); void (*irq_ack)(struct irq_data *data); void (*irq_set_type)(struct irq_data *data, unsigned int flow_type); int (*irq_set_wake)(struct irq_data *data, unsigned int on); ... };这些函数对应着实际的硬件寄存器操作。比如irq_mask就是往中断控制器的掩码寄存器里写值让某个中断暂时不触发irq_unmask则是取消掩码恢复中断irq_ack用来清中断标志位告诉控制器我收到了这个中断可以继续了。对GIC来说这些操作大多已经在内核的drivers/irqchip/irq-gic.c里实现了我们基本不用动。但在一些国产SoC或者老平台移植时中断控制器可能是私有IP那就需要自己照着硬件手册实现一套irq_chip。很多人的第一个移植噩梦就是从写irq_chip开始的。3.2 中断控制器为何会级联实际硬件中一个系统往往不止一个中断控制器。最常见的是GIC挂着一个GPIO控制器而GPIO控制器本身又能产生中断。这就出现了级联cascade关系——GPIO控制器作为GIC的一个中断源当GPIO口上有事件时GPIO控制器先捕捉到然后在它的中断处理函数里再去轮询哪个GPIO口产生了事件最后调用对应的GPIO子系统的irq handler。这种设计让中断的处理路径变长了硬件产生中断 - GIC - 内核识别到GIC的某个hwirq - 执行GPIO控制器的级联处理函数 - 在GPIO控制器内部查找具体是哪个GPIO - 调用GPIO对应的中断处理函数。理解了这个链路你才明白为什么在设备树里一个GPIO按键的中断要写两三层引用。顶层是GIC中间是GPIO控制器最后才是按键对应的引脚。任何一层的映射不对中断都到不了你驱动的handler里。4. 从设备树到驱动中断号platform_get_irq背后的链条驱动里最常见的拿中断号的操作就是platform_get_irq。很多朋友就直接调用拿不到就报错拿到了就request_irq。这个函数背后究竟跳了多少层逻辑可能没几个说得清。4.1 of_irq_get的解析过程platform_get_irq最终会走到of_irq_get它在设备树中查找节点的interrupts属性或者interrupt-parent属性然后调用irq_create_of_mapping这个核心函数进入irq domain的映射逻辑。大致链路是设备树interrupts属性 - of_irq_parse_one解析出hwirq和触发类型 - irq_create_fwspec_mapping查找对应的irq domain - domain-ops-map在这个domain里分配/查找Linux irq number - 生成irq_desc并初始化irq_chip和相关标志位 - 返回irq number给驱动这个过程中dts里写的interrupt-cells会被解析不同的中断控制器有不同的cell个数和含义。比如GIC通常interrupt-cells是3分别表示中断类型SPI还是PPI、hwirq编号、触发方式。而GPIO控制器做中断时interrupt-cells往往只有2表示GPIO编号和触发方式。写设备树时最常犯的错就是把GIC的三段式写法套到别的控制器上或者反过来。比如有些平台把中断号多写了1位代码一看解析出来的hwirq完全不对中断自然就乱套了。4.2 触发类型两套体系容易搞混设备树里配触发方式比如interrupts GIC_SPI 88 IRQ_TYPE_LEVEL_HIGH;到了驱动里request_irq时又设置了IRQF_TRIGGER_RISING等flag。这两套触发设置是什么关系设备树里的IRQ_TYPE_会通过irq_chip的irq_set_type回调设置到硬件寄存器上也就是真正配置了中断控制器的触发方式。而驱动里request_irq的IRQF_TRIGGER_如果设置了会覆盖设备树的设置——这是很多人的认知误区。实际逻辑是这样的中断触发类型的最终生效状态存放在irq_data里而irq_set_type是底层执行者。如果驱动没有显式设置就会沿用设备树解析出来的type。如果驱动设置了内核会在request_irq的阶段调用irq_set_irq_type去切换硬件配置。所以很多时候硬件明明配了上升沿触发驱动里用IRQF_TRIGGER_LOW去申请最后莫名其妙中断风暴或者干脆不触发就是因为这个覆盖关系没理清。特别提醒共享中断IRQF_SHARED下不同设备如果设置的触发类型不一致内核会在request_irq时返回-EINVAL因为一个物理中断引脚不可能同时支持电平触发和边沿触发。这个错误信息很多人见过但不知道根源在哪。5. 硬中断、软中断和线程化中断handler跑在哪里很重要中断来了handler在哪执行决定了你的驱动能不能睡个好觉。很多移植过来的驱动在旧平台上跑得好好的新平台上动不动就报atomic scheduling或者BUG: scheduling while atomic基本就是中断上下文的问题。5.1 上半部和下半部的基本分工Linux中断处理分上半部hardirq context和下半部softirq / tasklet / workqueue。上半部在中断上下文中执行特点是快速、不能睡眠、不能做太重的活。下半部则可以在更宽松的上下文中运行允许延迟处理。传统做法是上半部只做必要的硬件确认、关中断、置标志位然后把真正的数据处理扔给下半部。常用的下半部机制包括softirq、tasklet、workqueue还有内核里无处不在的threaded irq。5.2 threaded IRQ用线程换自由request_threaded_irq是很多驱动移植的首选它允许你把中断handler放到一个内核线程中去执行在线程上下文里就可以睡眠、可以调用那些可能在原子上下文出问题的API。int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id);注意这里有两个回调handler和thread_fn。handler在中断上下文执行如果返回IRQ_WAKE_THREAD内核才唤醒线程去跑thread_fn。如果handler返回IRQ_HANDLED线程不会启动。实际使用中最简单的用法是handler传NULL内核会使用默认的irq_default_primary_handler直接返回IRQ_WAKE_THREAD把所有活儿都交给thread_fn。这种方式特别适合那些需要访问慢速外设比如通过I2C/SPI读传感器数据的驱动把这些操作放线程里既避免了阻塞中断上下文又能保证数据完整性。在做驱动移植时我强烈建议优先考虑request_threaded_irq而不是老式的request_irq。新平台上的外设驱动尤其那些要读芯片寄存器、要等待硬件Ready的用线程化处理能省掉一堆下半部同步的问题。6. 移植过程中最典型的中断问题排查链路必须走一遍纸上谈兵差不多了接下来聊点实际移植中遇到的破事。以下三个问题基本是驱动移植必备体检项目按这个顺序排查能解决90%的假性中断疑难杂症。6.1 中断号确实拿到了但一直不触发先确认设备树节点是否正确被driver probe。很多情况是设备树里interrupts属性配了但驱动通过platform_get_irq拿到的中断号是负数说明映射失败。排查步骤ls /proc/interrupts # 查看系统实际注册的中断 cat /proc/device-tree/xxx/interrupts # 确认dts解析结果 dmesg | grep irq # 查看映射过程中的报错如果dts解析本身没问题但硬是不触发就往中断控制器那边看。一个常见原因中断控制器虽然注册了但整个中断域没有被enableGPIO控制器和GIC之间那条级联中断没有request_irq。这种问题多出现在自己移植的板卡上需要检查GPIO控制器的驱动里是否对该级联中断做了初始化。还有一类是wakeup能力问题有的中断控制器在系统suspend后停止工作但你拔掉了电源管理相关的设置导致唤醒中断永远无法上报。做嵌入式设备驱动的人应该都被这种问题折磨过。6.2 中断风暴interrupt stormCPU占用率瞬间飙到100%这种情况多数不是驱动代码的锅而是触发条件与硬件实际行为不匹配。比如你注册了下落沿触发但硬件实际给的是低电平信号那么每次中断处理完中断状态位没有被真正清除中断控制器立即再次触发于是就陷入死循环。排查时先做一件事在中断handler里加一个计数打印几次就停防止系统直接卡死。然后用示波器或逻辑分析仪确认实际的硬件信号时序。大部分中断风暴最后都能定位到触发类型和硬件状态机不匹配。在代码层面一个常见问题是某些gpio控制器在读取GPIO中断状态后需要额外操作清中断挂起寄存器才能重新使能。如果只做了读取没做清除中断控制器认为事件还没处理完就会一直在pending状态反复触发。解决方法是确保irq_chip的irq_ack实现正确或在handler里主动调用相关寄存器清理。6.3 共享中断线导致误触发当一个中断号被多个设备共享时IRQF_SHARED任何一个设备触发了内核会依次调用所有共享该中断号的handler。handler要自行判断这个中断是不是我的设备产生的如果不是要返回IRQ_NONE。这里就有个经典Bug新移植的驱动代码没有做好设备状态判断盲目地在handler里操作硬件寄存器把别的设备的状态给搅了导致系统行为变得很怪异。常见场景是DMIC、Codec、Touch等设备共用一条中断线。排查方式是在驱动里对所有共享设备的中断handler做日志追踪确认每次中断是否被所有handler都认领了。正确写法是handler里先读硬件的中断状态寄存器确认事件确实是自己设备的再往下走。如果读不到有效状态立刻返回IRQ_NONE。7. 从零调试中断子系统我常用的工具和方法排查中断问题手头工具要趁手。内核其实提供了不少调试手段只是很多人不知道或不常用。7.1 /proc/interrupts的进阶读法cat /proc/interrupts这命令大家都会用。但要注意这个输出里的第一列是Linux irq number不是硬件中断号。如果你要对硬件手册需要先搞清楚映射关系。中间那几列是每个CPU上的触发次数如果某个中断的触发次数在所有CPU上都很低那多半是亲和性配置问题让irqbalance管一管就行。如果看到某个中断号触发次数特别大而且对应的handler名字是你刚加的那个那基本就是风暴没跑了。先用echo 0 /proc/irq/xx/xx/affinity把某个CPU单独拎出来跑降低整体影响再继续定位。7.2 tracepoint和irqsoff工具较新内核里可以用perf或者trace-cmd抓irq相关事件trace-cmd record -e irq:irq_handler_entry -e irq:irq_handler_exit看中断入口和出口之间隔了多久能快速判断handler是不是被某个慢操作拖住了。还有一种情况应该用lockdep配合看如果handler里用了可能导致睡眠的锁内核会在日志里喊出来但你必须开了CONFIG_PROVE_LOCKING才看得到。7.3 手动触发中断的小技巧调试按键、传感器这类外设时最烦的是不好复现硬件事件。一个有用的技巧用gpio调试接口直接手动拉电平产生脉冲验证中断链路是否整体通。echo gpio_number /sys/class/gpio/export echo out /sys/class/gpio/gpionum/direction echo 1 /sys/class/gpio/gpionum/value sleep 0.1 echo 0 /sys/class/gpio/gpionum/value这样能快速模拟一个边沿触发。如果这都能触发中断那驱动的调用链没问题硬件接入部分再单独查。如果是电平触发设备可以用一个跳线直接拉低或拉高测试。实测下来这个方法在移植初期极其好用比反复按物理按键高效多了。8. 移植时千万别忽略的细节中断号、亲和性和电源管理最后把一些容易忽略的细节集中放在这里每一条都来自真实踩坑经历。8.1 中断号在设备树里到底要写几回头再看一遍你的设备树。如果是GICinterrupt-cells是3写法是interrupts 0 88 4;其中第一个0表示SPI中断1表示PPI第二个88是硬件中断号第三个4是触发类型4是level high1是rising edge2是falling edge8是level low。不少人在移植时抄了个别的平台模板把第一个数写错了结果中断映射或者亲和配置诡异。如果是GPIO控制器记得它在interrupt-cells里的第一个数指向的是GPIO序号和GPIO子系统里的编号要一致。不一致的话中断能注册成功但GPIO读写和中断服务操作的不是同一个引脚。8.2 中断亲和性affinity与性能多核平台上每个中断可以设置由哪个CPU核心处理。默认情况下irqbalance会动态调整。但某些实时性要求高的驱动最好手动把中断绑到某个核上避免来回迁移导致cache抖动。echo 2 /proc/irq/87/smp_affinity这个操作在某些老的内核版本里需要root权限。如果你的驱动对延迟很敏感绑定CPU是最简单也最有效的优化手段之一。不过要注意如果该中断是PPI类型每个核都有独立的PPI中断源处理逻辑略有不同别绑定错了核。8.3 中断与电源管理的接口做低功耗产品时中断唤醒路径一定要理清。驱动里通常用static int xxx_suspend(struct device *dev) { enable_irq_wake(irq); ... } static int xxx_resume(struct device *dev) { disable_irq_wake(irq); ... }这里有个细节enable_irq_wake会调irq_chip的irq_set_wake回调把中断配置成唤醒源。如果这个回调在你的中断控制器驱动里是空实现也就是开着白名单但实际上没配寄存器那你就会发现挂起后外部中断根本没法唤醒系统。移植时一定要对照硬件手册确认irq_set_wake的实现有没有真的把GPIO控制器/GIC对应寄存器给配上。很多开机键能唤醒、传感器中断不能唤醒的问题根源都在这一行回调没实现或实现错了。9. 中断子系统框架思维为什么移植和应用开发心态完全不同把中断子系统整体看一遍你会发现它就是一个典型的分层架构。硬件层只负责报信通用层负责翻译和分发驱动层负责消化。驱动移植做的其实就是让这三层对齐到新的硬件上。理解了这套框架后再回头看你那些玄学中断问题基本都能归纳到三个断层里硬件中断号和Linux中断号的映射错位——查设备树和irq domain触发类型不一致或者硬件状态没清干净——查irq_chip和handler逻辑中断下半部和锁使用不合适——重新选request_threaded_irq或者调整workqueue如果这三层都通了中断问题基本上就不再是玄学而是有清晰排查路径的日常工作。我个人的体会是与其到处搜零散的报错解决方案不如踏踏实实把irq domain、irq_chip、irq_desc这条主线摸一遍。以前觉得深不见底的内核中断机制真正用这套框架去对照硬件手册分析时反而变得很有条理。新平台上手第一周就建议先把整个中断控制器的驱动代码通读一遍把级联关系、映射逻辑、触发方式对应到原理图上。这个成本花下去后续所有外设驱动移植都能少踩一半的坑。