☰
Linux中断子系统架构解析与驱动移植实操指南
2026/9/25 15:11:16 网站建设 项目流程

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_irq(17)跑起来发现完全不对,中断要么不触发,要么一触发就进错处理函数。为什么?因为Linux内部有一套自己的编号体系。

2.1 硬中断号、软中断号和hwirq之间的关系

硬件(比如GIC中断控制器)给每个中断源分配一个编号,这叫做硬件中断号(hwirq)。而Linux在内核里也维护了一套编号,叫Linux中断号(irq number)。这两者之间不是相等的,需要通过中断控制器驱动来进行映射。

以ARM平台常见的GIC为例:SoC内部的各种外设中断源会接到GIC的某个INTI(Private 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解析本身没问题,但硬是不触发,就往中断控制器那边看。一个常见原因:中断控制器虽然注册了,但整个中断域没有被enable,GPIO控制器和GIC之间那条级联中断没有request_irq。这种问题多出现在自己移植的板卡上,需要检查GPIO控制器的驱动里是否对该级联中断做了初始化。

还有一类是wakeup能力问题:有的中断控制器在系统suspend后停止工作,但你拔掉了电源管理相关的设置,导致唤醒中断永远无法上报。做嵌入式设备驱动的人应该都被这种问题折磨过。

6.2 中断风暴(interrupt storm):CPU占用率瞬间飙到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/gpio<num>/direction echo 1 > /sys/class/gpio/gpio<num>/value sleep 0.1 echo 0 > /sys/class/gpio/gpio<num>/value

这样能快速模拟一个边沿触发。如果这都能触发中断,那驱动的调用链没问题,硬件接入部分再单独查。如果是电平触发设备,可以用一个跳线直接拉低或拉高测试。实测下来,这个方法在移植初期极其好用,比反复按物理按键高效多了。

8. 移植时千万别忽略的细节:中断号、亲和性和电源管理

最后把一些容易忽略的细节集中放在这里,每一条都来自真实踩坑经历。

8.1 中断号在设备树里到底要写几

回头再看一遍你的设备树。如果是GIC,interrupt-cells是3,写法是:

interrupts = <0 88 4>;

其中第一个0表示SPI中断(1表示PPI),第二个88是硬件中断号,第三个4是触发类型(4是level high,1是rising edge,2是falling edge,8是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. 中断子系统框架思维:为什么移植和应用开发心态完全不同

把中断子系统整体看一遍,你会发现它就是一个典型的分层架构。硬件层只负责"报信",通用层负责"翻译和分发",驱动层负责"消化"。驱动移植做的,其实就是让这三层对齐到新的硬件上。

理解了这套框架后,再回头看你那些"玄学中断问题",基本都能归纳到三个断层里:

  1. 硬件中断号和Linux中断号的映射错位——查设备树和irq domain
  2. 触发类型不一致或者硬件状态没清干净——查irq_chip和handler逻辑
  3. 中断下半部和锁使用不合适——重新选request_threaded_irq或者调整workqueue

如果这三层都通了,中断问题基本上就不再是玄学,而是有清晰排查路径的日常工作。我个人的体会是,与其到处搜零散的报错解决方案,不如踏踏实实把irq domain、irq_chip、irq_desc这条主线摸一遍。以前觉得深不见底的内核中断机制,真正用这套框架去对照硬件手册分析时,反而变得很有条理。新平台上手,第一周就建议先把整个中断控制器的驱动代码通读一遍,把级联关系、映射逻辑、触发方式对应到原理图上。这个成本花下去,后续所有外设驱动移植都能少踩一半的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询