1. 移植驱动前,先搞清楚中断子系统到底在帮你做什么
做驱动移植,最怕拿到一份源码就开干,改改寄存器地址、换换时钟频率,结果中断死活不触发,或者一触发就死机。我见过太多人卡在中断上,本质问题不是代码写错了,而是对整个中断子系统缺少一张地图——不知道中断从硬件引脚到CPU执行回调函数,中间到底经过了哪些环节、每一层由谁负责、移植时哪些东西必须跟着SoC变化、哪些东西是内核帮你挡掉的。
先说个场景。你在A平台的开发板上跑得好好的驱动,换到B平台,系统起来后cat /proc/interrupts看不到你的中断号,或者能看到号但计数永远不涨,又或者中断风暴直接把CPU打满。这时候如果你脑子里没有中断子系统的整体框架,排查起来就是瞎猫碰死耗子:一会怀疑设备树写错了,一会怀疑寄存器配置不对,一会又怀疑是内核版本差异。而实际上,问题可能只是中断控制器(Interrupt Controller)的映射关系没对上,或者中断类型(电平触发、边沿触发)在硬件上根本配不出来。
所以这篇我打算把Linux中断子系统的整体框架掰开揉碎讲一遍,并且站在“驱动移植”的视角——重点不是你从头写一个中断子系统,而是当你把驱动从一个平台搬到另一个平台时,哪些环节最容易出问题、为什么出问题、该怎么定位。框架搞明白了,移植过程中的九成中断问题你都能自己判断个大概。
先给一张总览图,后面逐个环节拆:
- 硬件中断源(外设)→ 中断控制器(GIC等)→ CPU的IRQ引脚
- 内核侧:
irq domain负责把硬件中断号翻译成Linux内部中断号(virq) irq_desc:每个virq对应一个描述符,存放回调函数、标志位、线程化信息- 中断流控层(
handle_irq系列):处理不同触发类型的中断流 - 驱动回调:
request_irq/devm_request_irq注册的handler最终被执行
通俗点说,中断子系统就是一套“硬件中断事件 → 内核事件分发 → 驱动处理函数”的管道系统。移植驱动的过程,就是让这套管道在你新的硬件平台上重新畅通。
2. 硬件中断号怎么变成Linux的irq号:从GIC到virq的映射逻辑
2.1 GIC是绝大多数SoC中断的起点
无论是高通、海思、瑞芯微还是全志,主流ARM SoC的中断控制器基本都走ARM GIC(Generic Interrupt Controller)架构。GIC的版本从GIC-400、GIC-500到GIC-600系列,差异主要在支持的CPU核数、中断数、亲和性配置上,但基本概念一致。
GIC把中断分为三类:
- SGI(Software Generated Interrupt):软件触发的中断,用于核间通信,0~15号。
- PPI(Private Peripheral Interrupt):私有外设中断,每个CPU核独享,比如每个核的local timer,16~31号。
- SPI(Shared Peripheral Interrupt):共享外设中断,所有核都能收到,32号往上,一般到几百号。
在设备树里你经常看到类似这样的节点:
uart0: serial@ff580000 { compatible = "rockchip,rk3399-uart"; reg = <0x0 0xff580000 0x0 0x100>; interrupts = <GIC_SPI 115 IRQ_TYPE_LEVEL_HIGH>; };这里的GIC_SPI 115就是指SPI中断,硬件中断号是115。注意这个115是GIC视角的硬件编号,不是Linux内部使用的irq号。从GIC的115到Linux的irq号,中间隔着一个关键的数据结构——irq domain。
2.2 irq domain就是一张翻译表
在老版本内核里,硬件中断号和Linux irq号经常是线性对应的,irq = 32 + hw_irq之类的简单公式就能算出来。但后来中断控制器层级变多、需要动态分配,内核引入了irq domain机制。每一个中断控制器注册一个irq_domain,通过irq_domain_ops里的map和translate回调实现硬件中断号到virq的转换。
驱动移植中最常见的一个问题就是:你板子上的外设中断在设备树里写的硬件号是GIC视角的号,而内核跑起来之后你cat /proc/interrupts看到的号是virq。这两者往往不一致。比如某外设硬件中断号是211,在/proc/interrupts里却显示为86,这都是正常的,由irq domain和分配顺序决定。
以经典GIC驱动为例,注册的irq_domain大概是这个流程:
gic_init_bases(...) -> irq_domain_add_linear(gic->base, gic->irq_nr, &gic_irq_domain_ops, gic)irq_domain_add_linear创建线性映射域,gic_irq_domain_ops里核心是两个回调:
gic_irq_domain_map:把GIC硬件中断号映射成irq_desc,并设置irq_chipgic_irq_domain_translate:解析设备树interrupts属性里的<GIC_SPI 115 IRQ_TYPE_LEVEL_HIGH>三元组
移植驱动时你要清楚一件事:设备树里你写的中断号是给GIC翻译用的,翻译完之后的virq才是驱动里真正注册使用的号。你在驱动代码里写irq = platform_get_irq(pdev, 0)拿到的,是这个virq。
2.3 移植中最常见的三类映射错误
第一类:设备树中断号写错。比如芯片手册里说某个外设中断是SPI 118,结果你写成118后驱动始终不触发,查手册发现在另一个中断控制器域下,或者实际要减去偏移。尤其当SoC内部有多个中断控制器级联时,设备树里写的号必须参照该控制器域的定义。
第二类:interrupt-parent指错。多控制器SoC上,外设节点要用interrupt-parent指定挂在哪个中断控制器下。如果漏写或者指到错误的父节点,内核会用默认域的翻译方式去解析,出来的号可能对不上硬件实际接的那条线。
第三类:触发类型不匹配。设备树里写IRQ_TYPE_LEVEL_HIGH,但硬件外设的实际中断输出是低电平有效、需要IRQ_TYPE_LEVEL_LOW。这种问题在/proc/interrupts里能看到中断号但计数不涨,因为电平一直没被拉低,中断控制器采样不到有效信号。
3. 驱动侧的中断注册流程:从request_irq到回调执行的完整链路
3.1 platform_get_irq与request_irq之间的微妙关系
写驱动时标准流程一般是:
static int foo_probe(struct platform_device *pdev) { int irq; irq = platform_get_irq(pdev, 0); if (irq < 0) return irq; ret = devm_request_threaded_irq(&pdev->dev, irq, foo_irq_handler, foo_irq_thread_fn, IRQF_TRIGGER_HIGH, "foo", dev); return ret; }很多人不清楚platform_get_irq到底做了什么。函数内部经历大致是:解析设备树节点 → 走of_irq_get→ 找到interrupt-parent对应的irq domain→ 调用irq_create_of_mapping把硬件中断号翻译成virq→ 返回。
注意:platform_get_irq拿到virq后,中断还没有真正被请求。驱动此时只是拿到了一个“令牌”,真正把我们的处理函数挂到中断管道上,是后续的request_irq或devm_request_irq完成的。
移植时经常遇到的情况:probe里platform_get_irq返回-ENXIO,也就是拿不到中断号。排查思路按顺序来:
- 设备树节点是否存在
interrupts属性。 - 设备树节点是否设置了正确的
interrupt-parent。 - 对应中断控制器的驱动是否已经初始化完成(
probe顺序问题,经常被人忽略)。 of_irq_get解析时是否因为触发类型字段非法而失败。
3.2 top half和bottom half:为什么你的回调不能干重活
Linux中断处理函数跑在中断上下文,它有几个硬性约束:不能睡眠、不能调用可能睡眠的API(mutex_lock、kmalloc带GFP_KERNEL都不行)、不能做太耗时的操作。因为中断上下文里CPU不参与调度,你在里面转5毫秒,整个系统就卡5毫秒。
于是有了bottom half机制。传统做法是tasklet,现在更推荐用threaded irq,也就是上面代码里的devm_request_threaded_irq。这种模式下,中断触发时先跑handler,如果handler返回IRQ_WAKE_THREAD,内核会唤醒一个内核线程去执行thread_fn,而thread_fn运行在普通进程上下文,可以睡眠、可以加锁、可以调用各种胖接口。
移植驱动的经验之谈:如果外设中断处理里要操作i2c或者spi总线读取状态寄存器,就强烈建议用threaded irq。因为i2c控制器本身也是中断驱动的,中断上下文里再去等它的中断完成,很容易死锁;而在线程化上下文里,i2c传输可以正常睡眠等待。
3.3 中断标志位:IRQF_TRIGGER_*与设备树触发类型是一致的吗
request_irq里第四个参数IRQF_TRIGGER_HIGH/LOW/RISING/FALLING指定触发类型。在设备树已经写了触发类型的前提下,驱动里再指定一次,两者到底听谁的?
这里有坑。GIC这类现代中断控制器,设备树里的触发类型最终会下发到硬件层(GIC配置对应中断的触发方式和优先级)。驱动里request_irq的IRQF_TRIGGER_*是Linux中断子系统的软件标志,但如果底层irq_chip已经根据设备树完成了硬件配置,驱动里的标志可能被忽略或覆盖,不同内核版本行为不一样。
我的建议:设备树里明确写触发类型,驱动里request_irq直接传0或IRQF_TRIGGER_NONE,不要让两个地方出现冲突。尤其移植时,原始平台设备树写的是IRQ_TYPE_EDGE_RISING,新平台换成IRQ_TYPE_LEVEL_HIGH,而驱动代码里还保留老标志,这就会埋雷。
4. 中断控制器驱动与irq_chip:移植时真正要动刀的地方
4.1 irq_chip是中断控制器在内核里的抽象
irq_chip封装了中断控制器的硬件操作,包括:
irq_mask/irq_unmask:屏蔽/使能某个中断源irq_ack:中断应答irq_set_type:设置触发类型irq_set_wake:使能唤醒irq_set_affinity:设置CPU亲和性
每个irq_desc里挂着一个irq_chip指针,驱动调用enable_irq、disable_irq、irq_set_irq_type时,最终都会落到对应irq_chip的回调上。
在GIC驱动的实现里,gic_handle_irq是最核心的函数,它做的事是:读GICC_IAR寄存器拿到硬件中断号 → 如果小于16(SGI)做核间处理;如果是PPI/SPI,调用generic_handle_irq分发到对应virq的处理流程。
移植驱动到新平台时,如果新平台的中断控制器不是标准GIC(比如有些国产SoC用了私有中断控制器,或者经过了一层自定义的级联封装),你就需要看这个芯片厂商是否提供了对应的irq_chip实现。如果厂商没提供,驱动里任何中断都无法正常工作,因为irq_set_type、irq_mask这些操作都没有落点。
4.2 irq domain层级级联时怎么排查
有些SoC内部不止一个中断控制器。典型例子:主GIC外挂一个GPIO控制器,而GPIO控制器本身又作为中断控制器,把多路GPIO中断汇聚成一路SPI中断送往GIC。
这种层级关系在设备树里表现为:
gpio2: gpio@ff790000 { compatible = "rockchip,gpio-bank"; reg = <0x0 0xff790000 0x0 0x100>; interrupt-controller; #interrupt-cells = <2>; interrupts = <GIC_SPI 116 IRQ_TYPE_LEVEL_HIGH>; };此时一个外设挂了gpio2的第5号引脚作为中断源:
foo_device { interrupt-parent = <&gpio2>; interrupts = <5 IRQ_TYPE_EDGE_RISING>; };of_irq_get解析时就会先走到gpio2的irq domain,把5翻译成gpio2域的virq;与此同时gpio2本身作为中断源又挂在GIC的116号上,形成一个链:外设中断 → GPIO控制器 → GIC → CPU。
移植时这种层级最容易出问题。常见的坑:
interrupt-controller和#interrupt-cells属性遗漏,导致外设节点无法解析。- GPIO控制器里某个引脚的中断和别的外设冲突,
/proc/interrupts里能看到gpio底下所有中断事件归属同一个virq计数。 - 层次多了之后,触发类型可能被某一层的
irq_chip改变。比如外设和GPIO控制器申请的是边沿触发,但GPIO控制器汇入GIC时如果只支持电平触发,内核会自动做一次转换吗?不会。所以你必须保证每层中断控制器的触发类型配置都合理,否则边缘触发的外设在GIC层采样不到。
排查这种层级问题时,我一般会在设备树里临时加interrupts-extended来跳过中间层直接指定上层控制器,快速确认问题是出在中间层还是最终层。
4.3 percpu中断和共享中断在移植中的区别处理
中断还有一种分类方式:是否per-CPU的,以及是否可共享。
PPI中断(比如每核的local timer)就是per-CPU的。设备树里你会看到类似:
arch_timer { interrupts = <GIC_PPI 13 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_LOW)>; };注意这里中断号是GIC_PPI 13,而且多了一个GIC_CPU_MASK_SIMPLE(4)来指定中断发给哪些CPU核。移植这种设备时,如果有人把PPI当成SPI去注册,或者request_percpu_irq和request_irq用混了,就乱了。PPI必须用request_percpu_irq配合enable_percpu_irq,处理函数里访问per-CPU变量也有一套固定写法。
而共享中断(IRQF_SHARED)是多个设备共用一个中断号。这类中断在request_irq时必须传入设备ID指针,内核触发时会逐个调用注册在同一virq上的handler,直到某个handler返回IRQ_HANDLED。移植时如果驱动里没用IRQF_SHARED却在设备树里和别的设备共用了中断号,request_irq会直接失败返回-EBUSY。
5. 从硬件触发到回调执行的瞬间:中断流控层与desc处理流程
5.1 handle_irq系列函数到底做了什么
中断流控层是不少人忽略的环节。在generic_handle_irq被调用后,内核会根据中断的触发类型和是否唤醒等属性,调用不同的流控函数。最常看到的有:
handle_level_irq:电平触发中断走这个handle_edge_irq:边沿触发中断走这个handle_fasteoi_irq:GIC这种支持EOI(End of Interrupt)的控制器走这个handle_simple_irq:某些不做硬件屏蔽的控制器用
这些函数的职责是:管理中断的屏蔽/使能状态、处理中断嵌套和pending状态、调用irq_desc里注册的action链(也就是驱动注册的handler)。
移植时你不需要改这些流控函数,但你必须理解它们的差异。最常见的现象:电平触发的中断,如果handler里没做irq_ack或者中断源没有被真正清掉,handle_level_irq会在handler返回后立刻再次触发,导致中断风暴。边沿触发的中断则容易丢中断,如果中断触发时irq_desc正被屏蔽,边沿信号不会像电平信号那样被锁存,恢复使能后可能就丢了。
5.2 实测中最容易翻车的场景:中断风暴与丢失中断
我实际移植一个网卡驱动时遇到过这么一件事:设备树里写的中断触发类型是IRQ_TYPE_LEVEL_HIGH,但实际上网卡芯片的中断输出引脚在空闲时本来就是高电平,只有产生中断时才短暂拉低。结果驱动一旦enable_irq,GIC采样到高电平就一直认为有中断,中断风暴瞬间打满CPU。
当时的排查过程很有代表性:/proc/interrupts里该中断的计数在疯狂暴涨,CPU0的si时间百分比飙到90%以上,但驱动里的handler却进得很少——因为风暴中断全被流控层拦截处理了。后来把设备树里的触发类型改成IRQ_TYPE_LEVEL_LOW,一次问题就没了。
另一个方向是丢中断。用边沿触发时,外设产生中断的脉冲非常窄,如果在GIC采样到之前信号就消失了,这个中断就丢了。这类问题在低速总线的外设上尤其明显——外设快、主机慢,两个时钟域之间的脉冲宽度不足。排查手段是在驱动里做一次“虚假中断检测”:handler里读外设中断状态寄存器,如果发现没有对应中断标志,就返回IRQ_NONE,让内核把该virq标记为可疑中断,/proc/interrupts旁边会出现[NMI]之类的错误提示。
5.3 irq_desc里的action链表:多个handler怎么排队
一个virq可以挂多个action,这正是共享中断的实现基础。request_irq时传入的dev_id就是用来区分不同action的。当handle_*_irq运行时,会遍历irq_desc的action链表,逐个调用handler,直到有一个返回IRQ_HANDLED。
这里有个性能考虑:共享中断链越长,每个中断触发需要遍历的handler就越多。所以移植时如果发现某个中断号和多个设备共享,性能敏感的场景下最好在硬件层面错开中断源,或者确认每个handler开头都能快速判断“这个中断是不是我的”。
6. 设备树里中断相关的关键属性:移植时逐项核对清单
6.1 interrupt-parent、interrupts、interrupt-names的配合
设备树中断属性三板斧是:
interrupt-parent:指定中断控制器父节点,告诉内核该设备的某个引脚接到哪个中断控制器的哪个输入上。interrupts:一般是<中断类型 中断号 触发类型>或<中断号 触发类型>,取决于父控制器的#interrupt-cells。interrupt-names:给每个中断起个名字,配合platform_get_irq_byname使用。
多中断设备很常见,比如一个触摸屏控制器有触摸中断和唤醒中断两个引脚:
touch@38 { interrupt-parent = <&gpio1>; interrupts = <5 IRQ_TYPE_EDGE_FALLING>, <6 IRQ_TYPE_EDGE_FALLING>; interrupt-names = "touch", "wakeup"; };驱动里用platform_get_irq_byname(pdev, "touch")去拿对应中断号。移植时改名会导致platform_get_irq_byname返回-EINVAL,而很多人排查时老是检查interrupts却忘了interrupt-names对应不上。
6.2 中断控制器节点的关键属性
中断控制器节点自身也有几个属性决定它如何被内核识别:
interrupt-controller:空属性,声明该节点是一个中断控制器。#interrupt-cells:声明子节点里interrupts需要几个单元格来描述。GIC是3个(类型、号、触发方式),GPIO控制器是2个(引脚号、触发方式)。interrupts:声明这个中断控制器本身接到上级控制器的哪个中断上(级联时才有)。interrupt-parent:指向更上一级的控制器。
移植新板子时,如果外设中断完全不可用,我一般会先检查控制器节点这4个属性是否齐全。这里有个隐蔽的问题:部分SoC的设备树里GPIO节点的interrupt-controller属性会被注释掉或者缺失,因为GPIO IP在芯片内部不是传统意义上的“中断控制器”,但驱动里如果某个外设把它当父控制器用,解析就失败。
6.3 中断映射失败时内核日志怎么看
设备树解析中断失败时,串口日志里一般能看到类似could not get irq或failed to get interrupt resource的信息。但更早的解析错误提示很模糊。
定位手段一个是打开动态调试:
echo 1 > /sys/kernel/debug/tracing/tracing_on echo 'irq:*' > /sys/kernel/debug/tracing/set_event另一个更直接的办法是查/sys/kernel/debug/irq/irqs/下的每个irq_desc信息,确认设备树映射后virq对应的irq_chip是不是你预期的那颗控制器:
cat /sys/kernel/debug/irq/irqs/86如果irq_chip显示的是gic而设备树里你接的是gpio2的域,说明级联映射没有生效,问题基本出在interrupt-parent。
7. 中断号查询与映射调试:把/proc/interrupts读成诊断书
7.1 /proc/interrupts每一列怎么读
驱动移植调中断,/proc/interrupts是第一现场。它的基本格式:
CPU0 CPU1 16: 1234 0 GICv3 49 Level arch_timer 86: 56789 321 gpio 20 Edge fdd40000.ethernet第一列是virq号,后面每列是一个CPU核上该中断触发的次数,然后是中断控制器名称和硬件内部分组,接着是硬件中断号、触发类型,最后是request_irq时注册的设备名。
判断标准:
- 计数一直为0:中断根本没到控制器或没被使能。
- 计数暴涨:触发类型或清中断逻辑有问题。
- 多个中断源的计数都归到一个
virq下:共享中断,需要靠寄存器判断是哪路外设。 - 计数在两个CPU核间来回跳动:说明中断被设置了亲和性或GIC做了负载均衡,正常现象。
7.2 驱动注册成功但中断不触发时的排查顺序
如果/proc/interrupts里能看到你的virq、设备名也对,但计数一直0,我一般按这个顺序排查:
- 确认外设确实产生了中断信号。用示波器或者直接在驱动里临时写一个测试IO:触发外设发送中断后再读外设的中断状态寄存器,确认硬件层面中断标志有没有置起来。
- 检查中断控制器的屏蔽状态。
/sys/kernel/debug/irq/irqs/86里能看到irq_data的state,确认IRQ_MASKED标志是否置位。 - 检查触发类型。
/proc/interrupts里显示的触发类型是否和硬件实际信号匹配。 - 检查时钟。没有外设时钟,外设根本不会工作,更不会产生中断。这个听着很傻,但确实在移植中踩过好多次。
我的一个具体案例:某传感器中断一直不触发,排查到最后发现设备树里给它配的时钟源没有通过clk_prepare_enable打开,传感器压根没跑起来,自然不会有中断。
7.3 线程化中断在/proc/interrupts里的表现
线程化中断在/proc/interrupts里会看到一个配套的内核线程,名字一般是irq/86-fdd40000.ethernet之类的。中断触发时,handler只做快速处理,真正的业务逻辑在irq/86-xxx线程里执行。
调这类问题时,除了看/proc/interrupts的计数变化,还要看线程的调度状态:
ps -eo pid,comm,state | grep irq/如果线程处于D状态(不可中断睡眠)长期不返回,多半是thread_fn里阻塞在了某个等待队列或锁上。这在移植时很常见,因为原始平台的外设寄存器延迟和时序和新平台不同,原本毫秒级的等待变成几十毫秒,或者直接超时了。
8. 移植后的中断验证与踩坑复盘
8.1 冒烟测试:中断链路的压测脚本思路
移植完之后,我一般会做一个简单的冒烟验证,不是只触发一次中断看看handler进没进,而是压一轮高频中断:
# 模拟高频中断事件,例如用perf触发大量定时器中断或者IRQ stress-ng --timer 32 --timeout 30 cat /proc/interrupts观察几个点:
- 中断计数和中断源触发次数是否线性匹配(允许少量合理丢失,但不能差一个数量级)。
- CPU的
si(软中断)和hi(硬中断)占比是否正常,正常情况下不应持续高位。 - 驱动handler是否出现超时报错,内核日志里有没有
IRQ handler type mismatch或nobody cared这类字样。 cat /proc/interrupts时如果发现某中断触发了可观的次数但handler没有任何反应,优先怀疑IRQF_TRIGGER_*配置。
如果使用threaded irq,建议也在压测期间观察ps里对应的irq/xxx线程的CPU占用率,正常情况下它应该能跟上中断频率。如果线程CPU占满而实际业务没完成,说明thread_fn里存在忙等或者频繁轮询的问题,移植时尤其要注意。
8.2 两种移植场景的处理差异:原样替换和跨内核版本
移植中断相关代码时,有两种典型场景:
一是原厂BSP提供了完整的内核和驱动,你只是把某个外设驱动移到自己的板卡上。这种情况基础设施多半是好的,重点检查设备树、时钟、IO复用和中断引脚。中断相关代码需要动的可能性不大,除非SoC中断控制器本身变了。
二是跨内核版本移植,比如把4.19的驱动往6.1内核上搬。这时中断子系统API变化比较明显,最典型的是:
of_irq_get和platform_get_irq的返回值语义在老内核里可能是0表示成功,新内核里0表示无效,且失败返回值是负数。如果你用老驱动的判断逻辑,在新内核上会误判。devm_request_threaded_irq的引入时间、request_percpu_irq的语义变化。- 老内核里
handle_irq的入口行为和新内核不同,部分自定义irq_chip在旧平台还能工作,新平台因为缺少irq_set_affinity或者irq_set_wake的调用上下文直接报错。
这类场景下,建议先把老驱动的中断请求段完整分析一遍,确认每一处API在当前内核版本下的行为,再动手改设备树。
8.3 我总结的七条中断移植注意事项
这几条不是教科书上的原话,是踩了不少坑之后我自己定的检查顺序:
- 先确认硬件电气连接,再谈软件配置。GPIO复用的pinmux没配好,中断引脚根本到不了控制器。
- 设备树里
interrupt-parent一定显式写,别依赖默认值,不同内核版本的默认解析逻辑很不一样。 - 触发类型双层核对:设备树写一次,
request_irq里保持一致,不要copy老代码时带上旧平台的标志。 - 中断处理函数里不要做总线操作,除非你用threaded irq或者确认总线控制器支持中断上下文使用。
- 移植后先跑高频中断压力测试,别只测一次触发。中断丢失和死锁往往在高频下才现原形。
/sys/kernel/debug/irq/和/proc/interrupts配合使用,一个看静态配置,一个看动态计数。- 千万别在中断handler里加调试打印,尤其串口打印。串口本身也是中断驱动的,中断上下文里打串口很容易把系统锁死,要打印就记录标志位,thread_fn里统一处理。
9. 最后再补一个定位中断问题的野路子
很多中断问题,表面上是“驱动注册失败”或者“中断计数不对”,实际根源在别的驱动或者系统初始化顺序上。比如之前遇到一次,某个外设的probe成功、中断注册也成功,但一使能外部中断整机就重启。查到最后发现是另一个驱动把GIC的某种电源域关了,导致GIC在处理该中断时访问了未上电的寄存器,直接触发总线错误。
所以移植驱动时如果中断问题怎么都查不通,不妨回头看看这几件事:
- 该外设的电源域和reset GPIO在目标平台上的配置是否和原平台一致。
- 内核里
CONFIG_ARM_GIC_V3这类中断控制器配置项是否和实际SoC匹配。配置不对的话,中断控制器可能只初始化了一部分。 - 启动日志里
GIC相关的初始化信息,确认它识别的中断范围是否覆盖了你外设使用的硬件中断号。
中断子系统是一个典型的“一荣俱荣、一损俱损”的链条。驱动侧代码再正确,只要中间任何一环没对齐——硬件连接、设备树解析、控制器配置、流控机制——整个链条就断了。把这套框架装进脑子里,移植时按图索骥,比盲目改代码高效得多。