ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

RISC-V设备树中断绑定详解:中断控制器、PLIC与多父节点路由实战

2026/9/11 12:59:15 拓冰建站 浏览量
RISC-V设备树中断绑定详解:中断控制器、PLIC与多父节点路由实战 直接说结论RISC-V 设备树里的中断绑定最核心的坑不是手册读不懂而是你根本不知道中断号应该填几、 parent 该指向谁、多个父节点出现时到底走哪条路由。很多工程师照着 ARM 平台的习惯写 dts结果在 RISC-V 上要么中断不触发要么一中断就死机最后只能一句一句加 printk 在中断处理函数里查问题。这篇文章我把 RISC-V 设备树中断绑定的规范细节、节点写法、多父节点路由方案全部拆开讲配合实际可操作的方法和排查技巧适合刚接触 RISC-V 平台开发的嵌入式驱动工程师、BSP 工程师以及正在做系统移植或调试中断异常的同学。1. 内容整体设计与思路拆解1.1 从问题出发为什么 RISC-V 中断绑定容易出问题先说一个很多人忽略的事实RISC-V 内核本身只定义了非常少的异常和中断入口具体到 SoC 上绝大部分中断都是通过外置中断控制器比如 PLIC、APLIC、CLINT管理的。这意味着设备树里必须把外设产生的硬件中断线映射到对应的中断控制器节点上内核的 irqchip 驱动才能完成中断号到 irq 的转换。一旦这个映射关系写错驱动里申请的中断号和硬件实际触发的中断源就对不上表现出来就是“中断永远不来”或者“来一个中断就触发错误处理”。ARM 平台上有成熟的 GIC 中断控制器几乎所有设备树节点都写interrupt-parent gic然后interrupts 0 42 IRQ_TYPE_LEVEL_HIGH格式基本统一。但 RISC-V 平台不一样不同厂家用的中断控制器差异很大。有的用 PLIC 处理外部中断有的用 APLIC IMSIC 的组合还有的简单 SoC 干脆直接把外设中断引到 CLINT 上。每种控制器的#interrupt-cells都不一样有的只填一个中断号有的还要加触发类型有的甚至要区分 M 模式和 S 模式。这就是为什么 RISC-V 设备树中断绑定不能照搬 ARM 经验。你得先搞清楚三个层面的问题第一你的外设中断信号最终接到哪个中断控制器第二这个控制器的设备树节点有没有声明interrupt-controller属性和#interrupt-cells第三外设节点引用父控制器时中断号应该按照什么规则填写是否有多父节点路由的需求。1.2 制定完整的学习路径我建议按照“规范 — 节点 — 路由”的顺序来理解整个中断绑定过程这也符合实际调试时的排查顺序。规范层面要理解设备树中断相关的五个核心属性interrupt-controller、#interrupt-cells、interrupt-parent、interrupts和interrupts-extended。节点层面要会看中断控制器节点本身长什么样、外设节点怎么引用它、不同 RISC-V 平台上的兼容字符串有什么讲究。路由层面则要掌握多父节点场景下如何让一个设备的中断在不同的父控制器之间选择或映射。上手方式上我个人强烈建议用 QEMU virt 平台做实验。QEMU 的 RISC-V virt 机器内置了 PLIC设备树导出的结构非常规范而且可以随时用-machine dumpdtb把设备树导出到宿主机反编译后再修改反复测试都不会损坏硬件。等你在虚拟平台上完全理解了中断绑定机制再移植到真实 SoC 上遇到问题就能立刻定位是设备树问题还是驱动问题。2. 核心规范与设备树节点解析2.1 五个核心属性到底怎么理解设备树描述中断关系最基础的五个属性我用最直白的方式解释一遍。interrupt-controller是一个空属性它起到的就是“标记”作用。一个节点只要声明了它内核就认为这个节点是一个中断控制器。经常有新手在中断控制器节点里忘记写这个属性结果驱动的platform_get_irq永远返回 -EINVAL怎么查都查不到原因。这个坑踩过一次之后你就记住了凡是作为中断父节点的设备必须声明interrupt-controller。#interrupt-cells表示用几个 32 位无符号整数来描述一条中断。比如 ARM GIC 用了 3 个前两个表示中断类型和中断号第三个表示触发方式。RISC-V 平台的 PLIC 大多只用 1 个来表示中断源编号部分实现用 2 个第二个填触发类型宏。这个值的单位和含义完全由父节点定义你去看任何中断控制器的设备树 binding 文档第一件事就是确认#interrupt-cells到底是几。interrupt-parent是外设节点用来指定“我的中断信号接到哪个控制器”的属性。它的值是一个 phandle指向中断控制器节点。可以放在设备节点自身也可以放在根节点或总线节点上作为默认值子节点如果不显式指定就继承父级的interrupt-parent。interrupts是外设节点描述具体中断条目的属性格式由父节点的#interrupt-cells决定。一个外设可能有多个中断源那就按父节点定义的 cell 格式重复填。比如父节点#interrupt-cells 1那么interrupts 7 9表示外设使用 7 号和 9 号两个中断源都得由父控制器解析。interrupts-extended是处理多父节点路由最关键的属性。它的格式和interrupts不同每一条中断先用一个 phandle 指定父控制器后面再跟随该父控制器规定的 cell 格式。说白了就是给每一个中断单独指定父亲而不是全设备共用同一个interrupt-parent。2.2 中断控制器节点怎么写RISC-V 常见的平台级中断控制器主要有 PLICPlatform-Level Interrupt Controller和 APLICAdvanced Platform-Level Interrupt Controller。PLIC 是传统方案很多 RISC-V SoC 和虚拟机都在用APLIC 是较新的 AIAAdvanced Interrupt Architecture规范下的产物用于替代 PLIC 的部分功能。这里用 QEMU virt 平台的 PLIC 节点举例。plic0: interrupt-controllerc000000 { #interrupt-cells 1; compatible sifive,plic-1.0.0, riscv,plic0; reg 0xc000000 0x4000000; interrupts-extended cpu0_intc 11, cpu0_intc 9, cpu1_intc 11, cpu1_intc 9; interrupt-controller; };注意看interrupts-extended本身出现在 PLIC 节点里这是因为 PLIC 作为中断控制器它自己也要把“外部中断”这个信号上报给 CPU 核。RISC-V 规范规定每个核的本地中断控制器接受 8 种异常其中第 11 号一般是机器模式外部中断MEIP第 9 号是监管者模式外部中断SEIP。所以 PLIC 的interrupts-extended就把这些 CPU 外部中断和每个核的本地中断控制器绑定。CPU 核的节点如下cpu0_intc: interrupt-controller { #interrupt-cells 1; compatible sifive,clint0, riscv,cpu-intc; interrupt-controller; };CLINTCore Local Interruptor在 RISC-V 设备树里通常也叫riscv,clint0它负责管理定时器中断MTIP/STIP和软件中断MSIP/SSIP。很多 SoC 会把 CLINT 和 PLIC 分成两个节点CLINT 管 CPU 私有中断PLIC 管全平台外部中断。搞清楚这种分工后你看到设备树里有些外设挂在 PLIC 下、有些挂在 CLINT 下就不会觉得乱了。2.3 外设节点的中断绑定格式外设节点绑定中断的正确写法取决于它的中断父节点是谁。看一个典型的 UART 外设节点uart0: serial10000000 { compatible ns16550a; reg 0x10000000 0x100; interrupt-parent plic0; interrupts 10 IRQ_TYPE_LEVEL_HIGH; };这里 PLIC 的#interrupt-cells 1但实例里却写了两个 cell第二个是触发电平宏。这就产生了一个混乱QEMU virt 的 PLIC 实际上支持 2 个 cell第一个是中断源编号第二个是触发类型。如果你的平台只支持 1 个 cell那么第二个 cell 会被忽略触发类型全部按控制器的默认方式处理。所以写设备树之前必须确认 binding 文档里 PLIC 定义的#interrupt-cells到底是几不要想当然。如果外设的父节点是 CPU 本地中断控制器比如某些定时器设备直接连到 CLINT写法就简单了timer2000000 { compatible riscv,timer; interrupts-extended cpu0_intc 5, cpu1_intc 5; };这里第 5 号中断是监管者模式定时器中断STIP4 号是机器模式定时器中断MTIP。CPU 本地中断控制器的#interrupt-cells 1所以填一个数就够了。3. 多父节点中断路由实战配置3.1 为什么会有多父节点路由的需求在实际 SoC 项目里一个外设同时连接两个中断控制器的情况并不罕见。比如双核系统里一个外设的中断既可以路由到主核的 PLIC也可以路由到从核的 PLIC软件通过写寄存器来选择最终目标。还有一种情况是 SoC 内部有安全和普通两个中断域外设需要把不同状态的中断分别上报给不同域的控制器。设备树里处理这种需求有两种方式一是用interrupts-extended让一个外设节点分别绑定多个父控制器二是用interrupt-map在总线节点里做中断域转换。前者更适合“一对多直接连接”的场景后者更适合“总线上多个设备的中断统一路由”的场景。下面我分别讲。3.2 使用 interrupts-extended 实现多父节点绑定假设我们有一个外设节点eth0它的发送完成中断连接到 PLIC0 的 23 号中断源接收完成中断连接到 PLIC1 的 45 号中断源并且两个 PLIC 的#interrupt-cells都是 2中断号 触发类型。这种情况下直接这样写eth0: ethernet20000000 { compatible vendor,eth0; reg 0x20000000 0x10000; interrupts-extended plic0 23 IRQ_TYPE_LEVEL_HIGH, plic1 45 IRQ_TYPE_LEVEL_HIGH; };内核解析interrupts-extended时会按照 phandle 找到对应父控制器再根据父控制器的#interrupt-cells解析后续的 cell。所以你不需要在 eth0 节点里写interrupt-parent因为每个中断都已经指定了自己的父节点。这种写法在驱动层看platform_get_irq(dev, 0)返回第一个中断platform_get_irq(dev, 1)返回第二个顺序就是设备树里排列的顺序。实际操作里有一个容易踩的坑有些工程师会把interrupts-extended和interrupts同时写上去这是不允许的。设备树规范明确说外设节点应该在interrupts和interrupts-extended里二选一。如果两个都写了内核一般优先解析interrupts-extended然后解析interrupts时由于双亲节点冲突容易触发警告甚至解析失败。我的建议是只要涉及多父节点一律使用interrupts-extended不要混用。3.3 使用 interrupt-map 做中断域转换interrupt-map的典型应用场景是 PCIe 设备的中断路由。PCIe 总线每个设备有 INTA、INTB、INTC、INTD 四个中断引脚而根端口中断控制器只有有限的几个中断号需要把 PCIe 设备的中断引脚映射到根端口控制器的中断号上。RISC-V 平台如果用到 PCIe一样要走这个机制。interrupt-map属性一般放在总线节点中组成是一系列映射条目。每个条目的格式是子设备地址单元 子中断单元 父控制器phandle 父设备地址单元 父中断单元配合interrupt-map-mask使用先对子设备的地址和中断引脚做掩码匹配命中后再路由到父控制器。看一个简化的例子pcie0x30000000 { reg 0x30000000 0x1000000; interrupt-map-mask 0 0 0 7; interrupt-map 0 0 0 1 plic0 32 IRQ_TYPE_LEVEL_HIGH, 0 0 0 2 plic0 33 IRQ_TYPE_LEVEL_HIGH, 0 0 0 3 plic0 34 IRQ_TYPE_LEVEL_HIGH, 0 0 0 4 plic0 35 IRQ_TYPE_LEVEL_HIGH; };这里interrupt-map-mask的最后一个 7 用来匹配 PCIe 中断引脚号1 到 4。第一个条目的最后四个数字分别代表 INTA、INTB、INTC、INTD它们都被映射到 PLIC0 的 32 到 35 号中断源。如果某个 PCIe 设备只发出 INTA 中断内核在遍历interrupt-map时用掩码匹配到第一条然后通过 PLIC0 申请中断号 32。整个路由逻辑中最容易出错的是掩码位数不匹配。interrupt-map-mask的长度必须和被匹配的单元长度一致否则匹配结果不可预测。利用dtc反编译设备树后建议先手工把掩码和每条映射的地址部分对齐确认没有多写少写再交给内核解析。4. 实操过程与核心环节实现4.1 环境准备与最小验证平台想快速验证设备树中断绑定是否正确最好的办法就是用 QEMU。系统不必跑完整 Linux先从一个最小设备树开始逐步加节点观察驱动注册情况。我的做法是用 QEMU virt 机器导出默认设备树qemu-system-riscv64 -machine virt -machine dumpdtbvirt.dtb用dtc -I dtb -O dts virt.dtb -o virt.dts反编译成可读文本。修改或者对照官方 virt 设备树搞清楚每个中断控制器和外设节点的绑定关系。写一个最简单的混合外设驱动模块通过platform_driver注册在probe里调用platform_get_irq获取中断并申请。这套流程不依赖真实硬件可以反复折腾非常适合把规范吃透。使用真实 SoC 开发板时操作流程也一样只是要把默认设备树换成板级厂商提供的 dts 源文件。4.2 从零创建带中断绑定的外设节点下面我以一个模拟的 GPIO 控制器为例演示完整的中断绑定流程。首先在 SoC 设备树里添加中断控制器节点。假设这个 GPIO 控制器内部集成了中断功能外部引脚中断源统一汇总到 SoC 的 PLIC0 的 66 号中断源。节点写法参考硬件手册gpio0: gpio10002000 { compatible vendor,gpio; reg 0x10002000 0x1000; gpio-controller; #gpio-cells 2; interrupt-parent plic0; interrupts 66 IRQ_TYPE_LEVEL_HIGH; interrupt-controller; #interrupt-cells 2; };注意这里有两组控制器属性gpio-controller和interrupt-controller同时存在。#interrupt-cells是给下游设备的表示 GPIO 扩展设备要申请中断时需要提供两个 cell一般是引脚号和触发类型。中间的interrupt-parent和interrupts是 GPIO 控制器自己作为“下游设备”向 PLIC0 上报中断的。很多工程师在这里混淆把#interrupt-cells当成给 PLIC 用的结果下游设备怎么填都报 wrong interrupt count。接着挂在这个 GPIO 控制器下的按键设备节点这样写key0 { compatible vendor,gpiokey; interrupt-parent gpio0; interrupts 3 IRQ_TYPE_EDGE_FALLING; };内核解析到这里时会从 GPIO0 节点获取#interrupt-cells发现是 2于是按两个 cell 解析。随后通过 irq domain 把 “GPIO 引脚 3 的下降沿” 映射成虚拟中断号这个虚拟中断号再和 PLIC0 的 66 号中断建立关联。实际触发时GPIO 控制器先收到 PLIC0 的 66 号中断在它的中断处理函数里找出到底哪个引脚产生了事件再调用 generic_handle_irq 派发给对应的虚拟中断域。4.3 多父节点路由案例的完整配置继续扩展场景如果这个 GPIO 控制器支持把不同分组的中断路由到两个不同的 PLIC比如 GPIO 0-15 引脚中断进 PLIC016-31 引脚中断进 PLIC1那么 GPIO 控制器节点还可以同时向两个父控制器描述两种中断。这在下游设备看来仍然是一个中断控制器但上游会有两套中断描述gpio0: gpio10002000 { compatible vendor,gpio; reg 0x10002000 0x1000; gpio-controller; #gpio-cells 2; interrupts-extended plic0 66 IRQ_TYPE_LEVEL_HIGH, plic1 88 IRQ_TYPE_LEVEL_HIGH; interrupt-controller; #interrupt-cells 2; };这种写法意味着 GPIO 驱动在probe时要分别调用platform_get_irq(dev, 0)和platform_get_irq(dev, 1)并在两个中断处理函数里根据寄存器判断是哪个引脚组的事件。多父节点绑定的核心价值就在这一个设备驱动可以管理多个中断控制器域的中断源而不需要把interrupt-parent固定在唯一的父节点上。这里有一个很关键的操作细节使用interrupts-extended后外设节点内部不需要也不应该写interrupt-parent。因为每个中断条目已经用 phandle 显式指定父控制器了再写interrupt-parent会造成二义性。有些内核版本会报irq: no irq domain found for node原因就是解析异常。如果你在排查类似问题第一步就把interrupt-parent注释掉试试。4.4 内核侧驱动验证与调试方法设备树写完只是第一步驱动侧能不能正确取到中断号才是真正的验证。Linux 内核里平台驱动获取中断最常用的函数是platform_get_irq。它返回的是一个软件中断号Linux irq number不是硬件中断源编号。设备树解析流程中中断控制器驱动会为每个硬件中断线申请一个 Linux irq 号platform_get_irq拿到的就是转换后的结果。为了确认绑定正确我会在驱动probe里加一段最直接的调试代码static int my_probe(struct platform_device *pdev) { int irq0, irq1; irq0 platform_get_irq(pdev, 0); dev_info(pdev-dev, irq0 %d\n, irq0); if (irq0 0) return irq0; irq1 platform_get_irq(pdev, 1); dev_info(pdev-dev, irq1 %d\n, irq1); return devm_request_irq(pdev-dev, irq0, my_isr, 0, mydev, NULL); }打印出来的irq值是否合理需要对照/proc/interrupts查看。如果irq0返回负数最常见的三种原因设备树节点没匹配到中断控制器interrupt-parentphandle 指向错误#interrupt-cells数量和实际填写数不一致。这些错误用dmesg都能看到线索但错误信息有时不直接可能要开启DEBUG级别的 irq 域日志才能定位。调试 RISC-V 中断时我强烈建议在启动参数里加上irq_debug或者debug_irqs内核会在中断申请和释放时打印更详细的信息。另外用trace_irq_matrix从 tracefs 观察中断矩阵状态能够直观看到有多少个中断源被注册、每个中断源关联到哪个控制器。这套排查手段比盲目加日志高效得多。5. 常见问题与排查技巧实录5.1 中断绑定相关的典型报错速查表我整理了一个常见问题对照表都是实际调试中大概率遇到的。放在手边遇到问题先按表格排查。现象可能原因排查方向platform_get_irq返回 -EINVAL#interrupt-cells与interrupts条目数不匹配反编译 dts核对控制器节点的#interrupt-cells设备树解析时提示invalid phandleinterrupt-parent引用的节点不存在或拼写错误检查 phandle 标签是否在 dtsi 中存在中断注册成功但probe永远不执行compatible字符串与驱动不匹配查看/sys/bus/platform/devices/.../modalias一触发中断就死机或启动卡死中断号填写错误触发到错误的中断源将interrupts中的数值临时改为 0 观察行为中断触发一次后不再触发中断处理函数未正确清除外设挂起位对照数据手册确认清中断寄存器地址多父节点路由时只有第一个中断生效interrupts-extended后面条目未被正确解析检查每个 phandle 是否指向interrupt-controller声明的节点这张表其实也体现了排查中断问题的通用思路先看设备树解析有没有报错再看 irq domain 映射是否成功最后回到驱动处理函数去验证硬件行为。大多数问题都不是内核 irqchip 代码有 bug而是设备树描述的“连接关系”与硬件实际连接不一致。5.2 独家排查技巧逆向推导中断号有一次我在调一块自定义 RISC-V SoC 的外设中断手册上写的中断源编号怎么都对不上问我的人一口咬定设备树没问题。后来我把所有外设的interrupts数值全部改成 0然后逐个外设手动触发中断通过观察控制器挂起寄存器来确定真实的中断源编号。这个方法虽然土但非常有效。具体操作是先把外设驱动注册成功但不申请中断也不使用platform_get_irq直接通过内核对中断描述符的导出信息反查。比如在/proc/interrupts里看每个中断号对应的中断名称结合设备树节点名能快速判断中断号是否互串。如果设备树节点的中断申请到了别的外设中断号上/proc/interrupts里该中断号的触发计数就会异常增加。还有一个小技巧cats /sys/kernel/debug/irq/irqs/irq_num可以看到该中断的处理器状态、触发计数、所属设备和控制器信息。当interrupts-extended在多父节点之间配对错误时这个文件里显示的 irq domain 名称能直接指出问题出在哪个控制器上。配合设备树反编译文件你甚至能画出完整的中断路由拓扑图。5.3 实战中容易忽视的操作细节最后补充几个设备树写法上的细节。interrupts属性的顺序很重要驱动里platform_get_irq(dev, index)的 index 按设备树里的出现顺序递增和硬件中断优先级没有任何关系。不要以为第一个中断就是最高优先级优先级由各控制器的中断号或软件处理顺序决定。phandle 的使用也有讲究直接写数值 phandle比如plic0编译后就是一个 32 位数字在源码里必须用标签引用不要手写数字。因为dtc在编译时会给所有带标签的节点分配 phandle手写数字很难保证和最后导出的 phandle 值一致一出错就是灾难性的。保持标签引用编译后需要确认时再反编译看实际值。另一个细节是中断类型宏。设备树源码里IRQ_TYPE_LEVEL_HIGH、IRQ_TYPE_EDGE_FALLING这些宏来自include/dt-bindings/interrupt-controller/irq.h。如果你的 dtsi 文件没有#include这个头文件编译时会报 undefined reference。遇到这类报错不要慌加上 include 路径之后重新编译即可。多父节点路由时还要特别注意interrupts-extended的父节点不能是interrupts省略后的默认父节点。如果一个外设节点既想绑定到 PLIC0又想绑定到 CLINT你必须写成device40000000 { interrupts-extended plic0 12 IRQ_TYPE_LEVEL_HIGH, cpu0_intc 3; };而不是把 CLINT 的中断写进interrupts里。否则内核会按照interrupt-parent或根节点的默认父节点去解析最终中断号错位。我个人在实际操作中还有一个习惯每写完一个设备树节点都会用dtc重新编译并反编译一次确认属性值和 phandle 与预期一致。特别是多父节点场景反编译出来的内容能清楚看到interrupts-extended是否被拆成多条独立的中断映射。这一步操作极其简单但几乎能过滤掉一半以上的低级错误。最后分享一个小技巧在 QEMU 或开发板的早期启动阶段用udevadm和设备模型结合核验设备树比进入用户态之后再去cat各种节点要可靠得多。