ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RISC-V设备树中断绑定实战:多父节点路由与interrupts-extended解析

RISC-V设备树中断绑定实战:多父节点路由与interrupts-extended解析 1. 中断绑定到底绑定的是什么从设备树属性到中断域写这篇文章之前我正蹲在一块 RISC-V Linux 开发板前面。外设中断不触发中断号明明在数据手册上写得清清楚楚设备树里也按规格填了interrupts 0x1f可内核就是收不到。折腾两天后我才意识到问题根本不在“中断号写没写对”而在“设备树的中断绑定关系没有建立起来”。RISC-V 的设备树中断绑定和 ARM 平台类似但又有它非常特殊的地方。最核心的概念是“中断域”interrupt domain和“中断父节点”interrupt parent。一个外设节点想表达“我有中断要上报”它不能只写一个裸的数字它必须明确告诉内核这个中断号是相对于哪个中断控制器父节点来编号的。否则内核无从解析因为同一个数字在 PLIC、GPIO 控制器、CPU 本地中断控制器里可能是完全不同的含义。1.1 中断域每个父节点都是一套独立的编号坐标系设备树里任何一个带interrupt-controller;属性的节点都可以看作一个独立的中断域。域内使用#interrupt-cells来声明每个中断需要几个 32 位整数来描述。拿 RISC-V 里最常见的 PLIC 来说plic: interrupt-controllerc000000 { compatible sifive,plic-1.0.0; reg 0x0 0xc000000 0x0 0x4000000; interrupt-controller; #interrupt-cells 1; interrupts-extended cpu0_intc 11, cpu1_intc 11; riscv,ndev 127; };#interrupt-cells 1表示下游设备引用 PLIC 中断时只需要给一个 cell也就是 PLIC 的全局中断号。riscv,ndev 127表示这个 PLIC 支持 127 个外部中断源实际有效中断号为 1 到 1270 号在 PLIC 规范里是保留/无效值。interrupts-extended在 PLIC 节点里表示它自己作为消费者时把输出中断线接在了哪些 CPU 本地中断控制器的哪根线上这里接到 CPU0 和 CPU1 的 11 号中断线也就是 RISC-V 的 Supervisor External Interrupt。所以一个完整的“中断绑定”不是单向的而是双向的下游设备把中断号挂到 PLICPLIC 再把中断输出挂到 CPU 中断控制器CPU 中断控制器再把异常送进内核。链路里任何一环断了中断都不生效。1.2 属性全解析interrupt-controller、#interrupt-cells、interrupt-parent、interrupts设备树规范里和中断绑定直接相关的属性就这么几个但它们的组合千变万化属性所在节点作用interrupt-controller;控制器节点空属性声明自己是一个中断域#interrupt-cells控制器节点声明本域内一个中断需要几个 cell 描述interrupt-parent消费者节点指定本节点的中断归属于哪个控制器域interrupts消费者节点给出本设备的中断描述列表格式由父节点的#interrupt-cells决定interrupts-extended消费者节点高级写法列表里可以写多组 phandle参数指向多个不同的父控制器interrupt-names消费者节点给每个中断起名字驱动里按名索引更可读需要特别强调的是interrupt-controller;不能漏。它和 compatible、reg 一样重要缺少这个标记内核的 irq domain 注册环节根本不会为该节点创建映射下游设备即使 phandle 指对了也会报no irq domain found。1.3 为什么 RISC-V 比 ARM 更容易出现“多父节点”问题ARM 平台多数时候中断拓扑是固定的外设 - GIC - CPU。但 RISC-V 架构把“本地中断”和“全局中断”分得很开。每个 HART硬件线程/CPU自带一个riscv,cpu-intc管理定时器中断、软件中断、外部中断这些本地中断线而 SoC 里的 PLIC 或 APLIC 负责汇聚全局外设中断再通过一条或几条线接到 CPU 本地中断控制器的 external interrupt 上。这种结构天然导致一个设备节点可能同时面向两类父节点一类是 SoC 级的中断控制器PLIC/APLIC/GPIO 控制器另一类是 CPU 本地中断控制器。再加上 RISC-V 允许 M-Mode 和 S-Mode 各自有中断视图某些设备中断既可以被固件在 M-Mode 下消费也可以委托给 S-Mode 的 Linux 处理。设备树如果不把这种路由关系写清楚内核和固件看到的就是两套完全不同的中断拓扑。2. 节点解析链路消费者节点、控制器节点与父节点锚定我最早学设备树中断绑定时有一个非常困惑的点一个外设节点里到底是通过interrupt-parent指定父节点还是通过interrupts-extended指定两者能不能混用后来把内核源码中drivers/of/irq.c的解析逻辑读了一遍才彻底明白这两条路径本质上是同一件事只是写法不同。2.1 消费者节点的两种标准写法最常见、也是新手最容易上手的是“父节点 中断号”分离式写法uart0: serial10000000 { compatible vendor,uart; reg 0x0 0x10000000 0x0 0x1000; interrupt-parent plic; interrupts 0x1a; };这种写法隐含一个规则interrupt-parent指向哪个控制器interrupts里的数字就必须按照那个控制器的#interrupt-cells来组织。如果 PLIC 的#interrupt-cells是 1那这里一个数字就是合法的完整中断描述如果某个 GPIO 控制器的#interrupt-cells是 2通常是line, flags那么每个中断就需要两个数字。还有一种更紧凑的写法把父节点和中断参数写在一起uart0: serial10000000 { reg 0x0 0x10000000 0x0 0x1000; interrupts-extended plic 0x1a; };用interrupts-extended的时候每个中断由“phandle 若干 cells”组成phandle 指明父控制器后面的 cells 是父控制器域内的中断参数。这种方式最大的优势就是可以支持多个父控制器这也是“多父节点路由”这个标题里最核心的机制。2.2 根节点的 interrupt-parent 陷阱有经验的工程师通常会习惯在根节点/{...}里写一个全局interrupt-parent plic;这样所有没显式指定父节点的子节点都会默认继承。这在 ARM 平台很常见但在 RISC-V 平台上要格外小心。RISC-V 的本地中断和全局中断分属两个域如果在根节点统一指向 PLIC那么某些想用 CPU 本地中断的设备反而会被错误地解析到 PLIC 域。我见过不止一个项目timer 节点想要走 CPU 定时器中断结果因为沿用了全局interrupt-parent被解析成 PLIC 中断号驱动在里面读寄存器永远等不到中断。所以我的原则是RISC-V 设备树里尽量不要在根节点写全局interrupt-parent宁可每个设备节点显式写也不要让继承机制帮你做决定。设备越多越容易漏但中断绑定的严谨性比少写几行 dts 重要得多。2.3 内核解析过程中的三个关键函数设备树到 Linux IRQ 号的解析路径大致如下of_irq_get-of_irq_parse_one-irq_create_of_mapping-irq_domain_alloc_irqs。也就是说内核先把设备节点里的中断描述解析成“父控制器 父域中断号 flags”然后在父控制器注册的 irq domain 里分配一个 Linux 中断号。如果interrupts-extended里第一个父节点不是有效的中断控制器节点of_irq_parse_one会直接返回错误。如果父控制器没注册 irq domainirq_create_of_mapping会打印类似irq: no irq domain found for ...的内核日志。这些日志是排查中断绑定问题的第一手线索后面我会专门展开。3. 多父节点路由的设计逻辑与 interrupts-extended 实战“多父节点路由”听起来很高端其实就是指一个设备节点可以拥有多个中断源并且每个中断源可以独立选择自己的中断父控制器。这在多核、多特权级、多控制器的 SoC 里非常实用。3.1 betweeninterrupt-parent和interrupts-extended的关键差异简单归纳一句话interrupt-parent interrupts是单父节点写法一整个节点里所有中断都进同一个控制器域interrupts-extended是多父节点写法可以在一个列表里把不同中断分别路由到不同控制器。举个例子一个安全监控 IP 有两个中断一个“数据就绪”中断需要进 Linux一个“安全告警”中断希望直接进 M-Mode 固件。硬件上这两个中断线分别连到了 PLIC 和 CPU 本地中断控制器。对应的设备树可以这样写security_monitor20000000 { compatible vendor,sec-mon; reg 0x0 0x20000000 0x0 0x1000; interrupts-extended plic 88, cpu0_intc 3; interrupt-names data-ready, alarm; };此时>dtc -I dtb -O dts -o decompiled.dts boot.dtb重点看目标设备节点的interrupts或interrupts-extended是否和预期一致。我遇到过 dts 源文件改了但编译产物没更新的情况排查了半天最后发现烧进去的 dtb 还是旧版本白白浪费了几个小时。4.2 从内核启动日志提取 irq domain 注册信息RISC-V 平台的内核启动时PLIC 驱动会打印中断域初始化信息。类似[ 0.000000] plic: interrupt-controllerc000000: mapped 128 interrupts with 2 handlers for 2 contexts如果是 APLIC 则可能打印aplic相关日志。拿到这个信息后你可以确认 PLIC 驱动是否成功注册riscv,ndev是否匹配硬件。如果连这行日志都没有说明 PLIC 节点本身的 compatible、reg 或interrupt-controller属性有问题设备根本没被识别为中断控制器。接着看有没有irq: no irq domain found这类报错。这个报错几乎是一针见血的说明某个设备节点的 interrupt-parent 或者 interrupts-extended 里的 phandle 指向的节点不是一个已经注册的中断控制器。常见原因是无意中漏了interrupt-controller;属性或者 phandle 写错。4.3 访问 /proc/interrupts 验证路由结果内核起来后最直接的检查点是/proc/interruptscat /proc/interrupts这个文件每一行对应一个 Linux 中断号包含各个 CPU 上的触发次数。通过驱动申请的中断号去查如果该中断号存在、CPU 列全为 0说明路由建立成功但没触发如果中断号都不存在说明设备树里的中断描述没有被正确解析驱动platform_get_irq很可能直接返回了负数。另外还可以看/proc/device-tree确认运行时的设备树节点和预期一致ls /proc/device-tree/soc/security_monitor20000000/ cat /proc/device-tree/soc/security_monitor20000000/interrupts-extended4.4 排查中最容易忽略的四个“隐藏错误”第一个隐藏错误#interrupt-cells和实际写出的 cell 数量不匹配。PLIC 域#interrupt-cells1那interrupts 0x10 0x20就是多写了反过来 GPIO 域要求 2 个 cell你只写了一个解析就会错位。第二个隐藏错误多父节点列表顺序问题。interrupts-extended的顺序没有硬性规定但如果驱动用下标索引中断比如platform_get_irq(pdev, 0)取第一个调整顺序会直接影响驱动行为。第三个隐藏错误父控制器自身的中断绑定没写完整。比如 GPIO 控制器节点声明了interrupt-controller;但它自己消费的 PLIC 中断没写在interrupts或interrupts-extended里导致 GPIO 子域无法级联到上层下游设备解析成功但永远等不到中断。第四个隐藏错误把 RISC-V 本地中断号当 PLIC 全局中断号用。RISC-V 的本地中断线编号 1-15 是异常原因编码不是 PLIC 自己的中断源编号。很多从 ARM 转过来的工程师这里最容易糊涂因为 ARM 的 GIC SPI/PPI 和硬件中断号是统一编排的而 RISC-V 是分层独立的。5. 完整 DTS 示例为 RISC-V 评估板配置双父节点中断理论阶段讲完了这里给一个可以直接参考的双父节点设备树示例。这里以虚构的rv64dev评估板为例CPU 有 4 个 HARTSoC 里有一颗 PLIC一颗 GPIO 控制器以及一个安全监控 IP。5.1 顶层节点与 CPU 本地中断控制器/dts-v1/; / { #address-cells 2; #size-cells 2; compatible vendor,rv64dev; cpu0: cpu0 { device_type cpu; reg 0x0 0x0; compatible riscv; riscv,isa rv64imafdc; cpu0_intc: interrupt-controller { #interrupt-cells 1; compatible riscv,cpu-intc; interrupt-controller; }; }; cpu1: cpu1 { device_type cpu; reg 0x0 0x1; compatible riscv; riscv,isa rv64imafdc; cpu1_intc: interrupt-controller { #interrupt-cells 1; compatible riscv,cpu-intc; interrupt-controller; }; }; soc { #address-cells 2; #size-cells 2; ranges; plic0: interrupt-controllerc000000 { compatible sifive,plic-1.0.0; reg 0x0 0xc000000 0x0 0x4000000; interrupt-controller; #interrupt-cells 1; interrupts-extended cpu0_intc 11, cpu1_intc 11; riscv,ndev 127; }; }; };注意 PLIC 节点里的interrupts-extended它表示 PLIC 自己作为消费者把外部中断输出接到 CPU0 和 CPU1 的第 11 号本地中断线。这里的11是 RISC-V 定义的 Supervisor External Interrupt 编号是一个固定的异常编码不能随意改成别的数字。5.2 带子中断域的 GPIO 控制器GPIO 控制器本身既是消费者要上报到 PLIC又是父节点下游设备把中断挂到它上面soc { gpio0: gpio10002000 { compatible vendor,rv64-gpio; reg 0x0 0x10002000 0x0 0x1000; interrupt-controller; #interrupt-cells 2; interrupts-extended plic0 69; gpio-controller; #gpio-cells 2; }; };下游按键节点挂在 GPIO 子域下soc { button0: button0 { compatible vendor,gpio-keys; interrupt-parent gpio0; interrupts 3 IRQ_TYPE_EDGE_RISING; label user-button; }; };这里interrupt-parent指向gpio0interrupts里第一个 cell 是 GPIO 线号 3第二个 cell 是触发标志IRQ_TYPE_EDGE_RISING与gpio0的#interrupt-cells2完全一致。5.3 真正意义上的多父节点路由示例下面这个安全监控 IP 同时使用两个父节点soc { security_monitor20000000 { compatible vendor,sec-mon; reg 0x0 0x20000000 0x0 0x1000; interrupts-extended plic0 88, cpu0_intc 3; interrupt-names data-ready, alarm; }; };>static int sec_mon_probe(struct platform_device *pdev) { int irq_data, irq_alarm; irq_data platform_get_irq_byname(pdev, data-ready); if (irq_data 0) return irq_data; irq_alarm platform_get_irq_byname(pdev, alarm); if (irq_alarm 0) return irq_alarm; dev_info(pdev-dev, data-irq:%d alarm-irq:%d\n, irq_data, irq_alarm); return 0; }用interrupt-names的最大好处是顺序调整不影响驱动逻辑我强烈建议在生产型项目里坚持用命名索引。5.4 编译与加载验证把上面的 dts 保存为rv64dev.dts用 dtc 编译dtc -I dts -O dtb -o rv64dev.dtb rv64dev.dts烧录后进入 Linux 检查cat /proc/interrupts手动触发安全监控的中断后再次查看/proc/interrupts中对应 Linux 中断号的计数是否增加。如果只增加>
返回列表