GIC400中断控制器使用详解:多核ARM SoC的中断配置与寄存器编程 📅 发布时间:2026/8/24 7:19:26 👁 浏览次数: 1. GIC400 是什么它不是“另一个中断控制器”而是现代多核SoC的神经中枢如果你正在调试一块基于ARM Cortex-A系列处理器的嵌入式板子比如某款国产车规级MCU、某款AI边缘计算模组或者某款高端路由器主控芯片当你打开芯片手册翻到中断章节时大概率会看到一个叫GIC-400的模块被反复提及——它不像GPIO或UART那样直观可感也不像DMA那样有明确的数据搬运动作但它一旦出问题整个系统就会陷入“半身不遂”某个CPU核心死锁、中断响应延迟飙升、设备驱动频繁超时、甚至系统在启动阶段就卡在secondary CPU bring-up环节。我第一次遇到GIC400配置错误是在调试一款双核A53平台的工业网关时串口打印停在“smp: Bringing up secondary CPUs…”之后再无动静花了整整三天才定位到是GIC Distributor寄存器中ITARGETSR中断目标寄存器的初始值没按CPU拓扑正确写入。这让我彻底意识到GIC400不是可有可无的“配件”它是多核协同的底层契约执行者。GIC全称Generic Interrupt Controller是ARM定义的一套标准化中断管理架构。从GICv1到GICv3再到GICv4版本演进背后是移动计算、服务器、车载电子对中断处理能力提出的指数级需求。而GIC-400正是GICv2规范的旗舰实现它不是ARM自己生产的芯片而是ARM IP授权给各大SoC厂商如NXP、TI、Allwinner、瑞芯微、华为海思等集成进其芯片内部的硬核模块。你可以把它理解为CPU和外设之间的“中央调度室”所有来自UART、SPI、以太网MAC、USB PHY、ADC、看门狗等外设的中断信号不再直接连到CPU引脚上而是先汇聚到GIC-400GIC-400根据预设策略比如把网络中断固定分发给CPU1把音频中断分发给CPU0再通过私有总线Private Peripheral Interrupt, PPI或共享总线Shared Peripheral Interrupt, SPI精准投递给指定的CPU核心。这种解耦设计让软件能灵活控制中断亲和性、优先级、屏蔽状态是Linux内核SMP调度、实时性保障、功耗管理如CPU idle时关闭部分中断源的物理基础。为什么标题强调“GIC400使用”而非泛泛而谈GIC因为GICv2与GICv3/v4在寄存器布局、配置逻辑、安全模型上存在本质差异。GIC-400只支持GICv2它没有GICv3的LPILocality-specific Peripheral Interrupt概念不支持虚拟化扩展中的vGIC也没有GICv4的MSIMessage Signaled Interrupt直通机制。这意味着如果你正在移植一个原本运行在GICv3平台如Cortex-A72/A76上的Linux BSP到一颗采用GIC-400的老款A53芯片上你不能简单复制dts中的interrupt-controller节点配置同样如果你在开发一个裸机bootloader想让第二个CPU核正常响应定时器中断你必须亲手初始化GIC-400的Distributor和CPU Interface两大部分而这个过程在GICv3上已被firmware如ARM Trusted Firmware高度封装。所以“GIC400使用”的核心就是掌握这套特定IP核的寄存器级操作逻辑——它不难但容错率极低一个bit写错后果可能是整个系统的静默崩溃。2. GIC400 架构拆解Distributor与CPU Interface两个世界如何握手GIC-400的硬件结构看似复杂实则由两个功能清晰、职责分明的子模块构成Distributor分发器和CPU InterfaceCPU接口。它们之间通过AMBA AXI或AHB总线互联而CPU Interface则通过私有总线通常是APB直接连接到每个CPU核心的中断输入引脚IRQ/FIQ。理解这两个模块的分工是读懂GIC-400寄存器手册、编写正确初始化代码的第一步。2.1 Distributor全局中断的“户籍管理员”Distributor是GIC-400的“大脑”它负责管理所有中断源的全局状态。它的核心任务有三个识别、分类、分发。首先它要识别每一个中断请求——GIC-400最多支持1020个中断IDInterrupt ID编号从0到1019。其中ID0-ID15是SGISoftware Generated Interrupt由软件写寄存器触发用于CPU间通信ID16-ID31是PPIPrivate Peripheral Interrupt每个CPU核心独享一套比如每个核心的私有定时器Private Timer或看门狗ID32及以上的则是SPIShared Peripheral Interrupt由片上外设如UART0、Ethernet MAC产生所有CPU核心都可见。Distributor为每个中断ID维护一套独立的状态寄存器包括ICDISRnInterrupt Set Pending Register置位此寄存器的某一位可手动触发对应ID的中断常用于SGI或调试。ICDIPRnInterrupt Priority Register8-bit优先级字段数值越小优先级越高。注意GIC-400的优先级是“抢占式”的高优先级中断可以打断正在执行的低优先级中断服务程序ISR。ICDIPTRnInterrupt Processor Targets Register这是最关键的寄存器之一。它是一个32-bit宽的寄存器每8-bit对应一个中断ID用来指定该中断应该被发送给哪些CPU核心。例如ICDIPTR[32]对应SPI#32的值为0x00000001表示只发给CPU0值为0x00000003表示同时发给CPU0和CPU1。这就是实现中断亲和性的物理基础。很多初学者在这里栽跟头误以为写0x00000001就能让中断只到CPU0却忽略了GIC-400要求该寄存器的值必须与实际存在的CPU数量匹配且每一位代表一个CPU的“使能位”。如果系统只有2个CPU那么ICDIPTR的bit0-bit1有效bit2-bit31必须为0否则可能导致不可预测行为。提示Distributor的寄存器基地址通常在SoC的内存映射表中定义例如在某款Allwinner H6芯片中GIC Distributor的基址是0x03001000。这个地址不是固定的必须查阅你所用芯片的TRMTechnical Reference Manual确认。2.2 CPU Interface每个CPU的“中断前台接待员”如果说Distributor是全局调度中心那么CPU Interface就是每个CPU核心专属的“前台”。每个CPU核心都有一个独立的CPU Interface模块它只负责与自己相连的那颗CPU打交道。它的核心任务是接收、过滤、通知。当Distributor决定将某个中断比如SPI#45即UART1的RX中断发送给CPU0时它会通过总线将中断信息推送到CPU0的CPU Interface。CPU Interface收到后并不会立刻让CPU跳转而是先做两件事第一检查该中断的优先级是否高于当前CPU正在执行的中断或当前CPU的“屏蔽优先级”第二检查该中断是否被本CPU Interface的ICCPMRCPU Priority Mask Register所屏蔽。只有两项检查都通过CPU Interface才会向CPU发出真正的IRQ或FIQ信号。CPU Interface的关键寄存器包括ICCPRCPU Priority Mask Register这是一个8-bit寄存器它设定了CPU当前的“屏蔽优先级阈值”。任何优先级数值大于等于此阈值的中断都会被CPU Interface自动忽略。例如ICCPR0x80二进制10000000则所有优先级0x80的中断即数值更大的优先级更低的都不会被送达CPU。这为操作系统实现中断嵌套和优先级调度提供了硬件支持。ICCIARCPU Interface Acknowledge Register当中断到来并被CPU接受后CPU在进入ISR之前必须读取此寄存器。读取操作会返回一个“中断确认号”ACK ID这个ID就是即将被服务的中断ID。同时该操作会自动清除Distributor中对应中断的“pending”状态并将其标记为“active”。ICCEOIRCPU End of Interrupt Register当中断服务程序执行完毕准备返回时CPU必须向此寄存器写入刚才从ICCIAR读到的ACK ID。这个写操作会告诉GIC-400“这个中断我已经处理完了”GIC-400据此将该中断状态从“active”恢复为“inactive”并可能重新触发如果中断源尚未释放。注意ICCIAR和ICCEOIR的操作顺序是铁律。如果忘记读ICCIAR就直接处理中断或者忘记写ICCEOIR就返回会导致GIC-400内部状态机错乱最常见现象是同一个中断被反复触发形成“中断风暴”CPU 100%忙于处理中断而无法执行其他任务。2.3 两个世界的握手协议寄存器访问的“时空一致性”Distributor和CPU Interface虽然物理上分离但它们的寄存器操作必须遵循严格的“内存屏障”规则否则会出现经典的“读写重排”问题。举个例子在初始化阶段你想为SPI#45设置优先级为0x20并将其目标CPU设为CPU0。你可能会写两行代码GICD-ICDIPR[45/4] (GICD-ICDIPR[45/4] ~(0xFF ((45%4)*8))) | (0x20 ((45%4)*8)); GICD-ICDIPTR[45/4] (GICD-ICDIPTR[45/4] ~(0xFF ((45%4)*8))) | (0x01 ((45%4)*8));看起来很完美对吧但在某些ARM处理器上编译器或CPU流水线可能会将这两条写操作重排导致ICDIPTR先被写入而ICDIPR还没写完。此时如果恰好有一个SPI#45中断到来GIC-400会根据ICDIPTR的值把它发给CPU0但CPU0在处理时却发现ICDIPR里还是旧的、错误的优先级值从而引发不可预测的行为。解决方案是插入数据内存屏障DMBGICD-ICDIPR[45/4] ...; __asm__ volatile(dmb sy ::: memory); // 确保上面的写操作完成 GICD-ICDIPTR[45/4] ...; __asm__ volatile(dmb sy ::: memory); // 确保上面的写操作完成dmb sy指令强制CPU等待所有之前的内存访问包括读和写全部完成才能执行后续指令。这是GIC-400编程中最容易被忽视、也最致命的细节之一。我见过太多项目功能逻辑完全正确却因为少了这两行dmb在压力测试下随机崩溃。3. GIC400 初始化实战从零开始点亮你的第一个中断纸上得来终觉浅绝知此事要躬行。下面我将以一个典型的双核Cortex-A53 SoC假设为某款国产芯片GIC Distributor基址0x2C001000CPU Interface基址0x2C002000为例手把手带你完成GIC-400的完整初始化流程。这个流程适用于裸机环境如uboot早期阶段或RTOS环境Linux内核的初始化逻辑本质上也是这个流程的封装和扩展。3.1 步骤一禁用GIC清空所有状态初始化的第一步永远是“清零”和“禁用”。就像重启一台混乱的服务器前先关掉所有服务一样。我们必须确保GIC-400处于一个已知的、干净的初始状态。// 定义寄存器结构体简化版 typedef struct { volatile uint32_t CTLR; // 0x000, Distributor Control Register volatile uint32_t TYPER; // 0x004, Interrupt Controller Type Register volatile uint32_t IIDR; // 0x008, Implementer Identification Register volatile uint32_t reserved[29]; volatile uint32_t IGROUPR[32]; // 0x080, Interrupt Group Registers (32*4 bytes) volatile uint32_t ISENABLER[32]; // 0x100, Interrupt Set Enable Registers volatile uint32_t ICENABLER[32]; // 0x180, Interrupt Clear Enable Registers volatile uint32_t ISPENDR[32]; // 0x200, Interrupt Set Pending Registers volatile uint32_t ICPENDR[32]; // 0x280, Interrupt Clear Pending Registers volatile uint32_t ISACTIVER[32]; // 0x300, Interrupt Set Active Registers volatile uint32_t ICACTIVER[32]; // 0x380, Interrupt Clear Active Registers volatile uint32_t IPRIORITYR[256]; // 0x400, Interrupt Priority Registers (256*4 bytes) volatile uint32_t ITARGETSR[256]; // 0x800, Interrupt Target Registers (256*4 bytes) volatile uint32_t ICFGR[64]; // 0xC00, Interrupt Configuration Registers (64*4 bytes) } gic_distributor_t; typedef struct { volatile uint32_t CTLR; // 0x000, CPU Interface Control Register volatile uint32_t PMR; // 0x004, Priority Mask Register volatile uint32_t BPR; // 0x008, Binary Point Register volatile uint32_t IAR; // 0x00C, Interrupt Acknowledge Register volatile uint32_t EOIR; // 0x010, End of Interrupt Register volatile uint32_t RPR; // 0x014, Running Priority Register volatile uint32_t HPPIR; // 0x018, Highest Priority Pending Interrupt Register volatile uint32_t ABPR; // 0x01C, Aliased Binary Point Register volatile uint32_t AIAR; // 0x020, Aliased Interrupt Acknowledge Register volatile uint32_t AEOIR; // 0x024, Aliased End of Interrupt Register volatile uint32_t AHPPIR; // 0x028, Aliased Highest Priority Pending Interrupt Register } gic_cpuif_t; #define GICD_BASE 0x2C001000 #define GICC_BASE 0x2C002000 gic_distributor_t *gicd (gic_distributor_t *)GICD_BASE; gic_cpuif_t *gicc (gic_cpuif_t *)GICC_BASE; // 1. 禁用Distributor gicd-CTLR 0; // 写0禁用 __asm__ volatile(dmb sy ::: memory); // 2. 清空所有中断的Pending和Active状态 for (int i 0; i 32; i) { gicd-ICPENDR[i] 0xFFFFFFFF; // 清除所有pending gicd-ICACTIVER[i] 0xFFFFFFFF; // 清除所有active } __asm__ volatile(dmb sy ::: memory); // 3. 禁用所有中断清空Enable位 for (int i 0; i 32; i) { gicd-ICENABLER[i] 0xFFFFFFFF; // 写1清除enable } __asm__ volatile(dmb sy ::: memory);这段代码做了三件事首先通过写CTLR0彻底关闭Distributor让它停止一切分发工作其次遍历ICPENDR和ICACTIVER寄存器组用全1值清除所有中断的pending和active标志最后遍历ICENABLER用全1值禁用所有中断源。注意这里用的是ICENABLERClear Enable而不是ISENABLERSet Enable因为我们要的是“禁用”所以写1表示清除使能位。这个“清零”步骤至关重要它能避免在初始化过程中某个残留的pending中断突然被激活打乱你的初始化节奏。3.2 步骤二配置中断分组与安全状态GIC-400支持中断分组Group 0 / Group 1这与ARM的安全扩展TrustZone紧密相关。Group 0中断只能被Secure World如ARM Trust Firmware处理Group 1中断则可以被Normal World如Linux Kernel处理。对于大多数非安全应用我们只需要关心Group 1。// 4. 将所有SPI和PPI配置为Group 1非安全组 // IGROUPR[0]控制ID0-ID31SGIPPIIGROUPR[1]控制ID32-ID63以此类推 // 我们需要将ID32及以上的SPI全部设为Group 1 for (int i 1; i 32; i) { // i0是SGI/PPIi1是SPI gicd-IGROUPR[i] 0xFFFFFFFF; // 全1表示Group 1 } __asm__ volatile(dmb sy ::: memory);IGROUPR寄存器的每一位对应一个中断ID值为1表示该ID属于Group 1Non-secure值为0表示Group 0Secure。由于我们通常不启用TrustZone所以将所有外设中断SPI都设为Group 1。这个配置必须在启用Distributor之前完成否则无效。3.3 步骤三设置优先级与目标CPU这是初始化的核心环节决定了中断的“谁来处理”和“谁先处理”。// 5. 设置所有中断的默认优先级为0xA0数值越大优先级越低 // 优先级范围是0x00-0xFF0x00最高0xFF最低 for (int i 0; i 256; i) { gicd-IPRIORITYR[i] 0xA0A0A0A0; // 每个字节都是0xA0 } __asm__ volatile(dmb sy ::: memory); // 6. 配置SPI#45UART1 RX的目标CPU为CPU0 // SPI#45的索引是 (45-32)/4 3即IGROUPR[3], IPRIORITYR[3], ITARGETSR[3] int spi_id 45; int idx (spi_id - 32) / 4; // 计算寄存器索引 int shift ((spi_id - 32) % 4) * 8; // 计算字节偏移 // 先读-改-写避免破坏同一寄存器中其他中断的配置 uint32_t val gicd-ITARGETSR[idx]; val ~(0xFF shift); // 清除原值 val | (0x01 shift); // 设置为CPU0 gicd-ITARGETSR[idx] val; __asm__ volatile(dmb sy ::: memory); // 7. 同样设置其优先级为0x40比默认值高 val gicd-IPRIORITYR[idx]; val ~(0xFF shift); val | (0x40 shift); gicd-IPRIORITYR[idx] val; __asm__ volatile(dmb sy ::: memory);这里有个关键点ITARGETSR和IPRIORITYR都是32-bit寄存器每个寄存器管理4个中断ID因为每个ID占8-bit。所以对于SPI#45我们需要计算它在哪个寄存器里idx以及在这个寄存器里的哪个字节shift。绝对不能直接写gicd-ITARGETSR[3] 0x01因为这会把该寄存器管理的其他3个中断SPI#46, #47, #48的目标CPU全部清零这是新手最容易犯的错误后果是多个外设同时失灵。3.4 步骤四启用Distributor与CPU Interface完成了所有配置现在可以“开机”了。// 8. 启用Distributor gicd-CTLR 1; // 写1启用 __asm__ volatile(dmb sy ::: memory); // 9. 配置并启用当前CPUCPU0的Interface gicc-CTLR 0; // 先禁用CPU Interface __asm__ volatile(dmb sy ::: memory); // 设置CPU Interface的屏蔽优先级为0x80即只响应优先级0x80的中断 gicc-PMR 0x80; // 设置Binary Point为0x00意味着所有8-bit优先级都参与抢占比较 gicc-BPR 0x00; // 启用CPU Interface gicc-CTLR 1; __asm__ volatile(dmb sy ::: memory); // 10. 最后使能我们关心的中断源SPI#45 gicd-ISENABLER[(spi_id-32)/4] (1 ((spi_id-32)%4)*8); __asm__ volatile(dmb sy ::: memory);PMRPriority Mask Register的值是0x80意味着优先级数值小于0x80即0x00-0x7F的中断才能被CPU0接收。我们之前把SPI#45的优先级设为0x40正好在此范围内所以它会被送达。BPRBinary Point Register设为0表示整个8-bit优先级字段都用于抢占比较这是最常用、最直观的模式。至此GIC-400的初始化宣告完成。现在只要你触发UART1的RX中断比如往串口发一个字符GIC-400就会把它精准地、可靠地送到CPU0然后CPU0会执行你为它编写的ISR。4. 中断服务程序ISR编写与陷阱排查从“能用”到“稳定”GIC-400初始化成功只是万里长征第一步。真正考验功力的是在中断服务程序ISR中与GIC-400进行精确的“对话”。一个写得不好的ISR轻则导致中断丢失、响应延迟重则引发系统死锁。下面我结合多年踩坑经验为你梳理ISR编写的核心要点和典型陷阱。4.1 标准ISR模板ACK-EoI-处理-返回缺一不可一个符合GIC-400规范的ISR必须严格遵循以下四步曲void uart1_rx_isr(void) { uint32_t ack_id; // Step 1: ACK中断获取中断ID ack_id gicc-IAR; // 读IAR自动清除pending标记为active __asm__ volatile(dmb sy ::: memory); // 确保ACK完成 // Step 2: 执行具体的中断处理逻辑 // 例如从UART FIFO读取数据放入缓冲区 uart_rx_handler(); // Step 3: EoI通知GIC-400处理完毕 gicc-EOIR ack_id; // 写EOIR清除active状态 __asm__ volatile(dmb sy ::: memory); // 确保EoI完成 // Step 4: 返回CPU继续执行被中断的代码 }这个模板看似简单但每一步都暗藏玄机。IAR的读操作是原子的它不仅返回中断ID还同步完成了两件事一是清除Distributor中该中断的pending状态二是将其状态从pending变为active。如果在读IAR之前你的代码里有任何可能触发新中断的操作比如读取某个状态寄存器就必须加dmb否则新中断可能在IAR读取过程中被GIC-400错误地认为是“重复pending”从而被丢弃。EOIR的写操作同样关键。它告诉GIC-400“这个中断我已经搞定了”。如果不写该中断会一直保持active状态GIC-400就不会再向CPU发送新的同ID中断即使中断源还在持续产生信号。这会导致“中断失灵”的假象——你以为硬件坏了其实是软件忘了“签收”。4.2 常见问题速查表那些让你熬夜到凌晨三点的Bug问题现象可能原因排查与解决方法系统启动卡在“Bringing up secondary CPUs…”Secondary CPU的CPU Interface未正确初始化或ITARGETSR未将其设为目标检查Secondary CPU的gicc-CTLR是否为1检查ITARGETSR中对应SPI/PPI的bit位是否为1对于Secondary CPU确认gicc-PMR值是否足够低如0x00确保它能接收启动所需的SGI中断。某个外设中断完全不触发中断源本身未使能GIC-400中该中断未使能ISENABLERITARGETSR指向了不存在的CPU中断优先级被PMR屏蔽用示波器或逻辑分析仪确认外设引脚是否有中断电平变化用调试器检查gicd-ISENABLER[n]对应位是否为1检查gicd-ITARGETSR[m]的值是否合法检查gicc-PMR是否小于该中断的优先级值。中断响应延迟极高10ms中断优先级设置过低CPU正在执行长时临界区如disable_irq中断服务程序ISR过于臃肿执行时间过长将关键中断优先级设为0x10或更高数值更小检查代码中是否有长时间的local_irq_disable()将ISR中耗时操作如数据处理、内存拷贝移到下半部bottom half或线程中执行。同一个中断被反复触发中断风暴ISR中未读IAR或未写EOIR外设中断源未被正确清除如UART RX FIFO未读空ITARGETSR配置错误导致中断被错误地分发到多个CPU在ISR开头强制添加ack_id gicc-IAR;结尾强制添加gicc-EOIR ack_id;在uart_rx_handler()中务必循环读取UART FIFO直到为空否则FIFO满后会持续产生中断检查ITARGETSR寄存器确保每个SPI只被分配给一个CPU。多核环境下中断总是被同一个CPU处理ITARGETSR寄存器被错误地写成了全0或全1Linux内核的irq affinity未正确设置检查gicd-ITARGETSR[n]的值确认其bit0-bit1对于双核是否按预期设置了在Linux中使用echo 2 /proc/irq/XX/smp_affinity命令将中断绑定到CPU1mask 0x2。实操心得我在调试一个PCIe设备的MSI中断时遇到了“中断偶尔丢失”的问题。最终发现是PCIe设备在发送MSI后需要等待CPU的EOIR响应如果EOIR写入延迟过高因为CPU在处理其他高优先级中断设备会超时并放弃本次中断。解决方案是为PCIe MSI分配一个极高的优先级0x01并确保其ISR极其精简EOIR写入必须在几微秒内完成。4.3 高级技巧利用SGI实现高效的CPU间通信GIC-400的SGISoftware Generated Interrupt是被严重低估的宝藏功能。它允许一个CPU通过写GICD-SGIR寄存器向另一个或多个CPU发送一个中断。这比传统的自旋锁、消息队列等方式更轻量、更实时。// CPU0向CPU1发送SGI#5 #define SGI_ID 5 #define TARGET_CPU1 0x2 // bit1 set gicd-SGIR (0 24) | // 保留位 (0 16) | // SGI source (CPU0) (TARGET_CPU1 0) | // target list (SGI_ID 0); // SGI ID (bits 0-3)SGIR寄存器的格式是[31:24]保留[23:16]是源CPU ID通常为0[15:0]是目标CPU掩码bit0CPU0, bit1CPU1[3:0]是SGI ID0-15。发送SGI后目标CPU会立即收到一个中断其ISR可以快速响应比如唤醒一个休眠的线程、刷新缓存、或同步一个全局变量。SGI的延迟通常在几十纳秒级别是实现多核协同的“黄金通道”。我曾用SGI替代了一个复杂的IPC机制将两个CPU间的事件通知延迟从毫秒级降低到了亚微秒级。5. GIC400 与 Linux 内核的深度绑定从设备树到中断子系统当你从裸机环境切换到LinuxGIC-400的使用方式发生了根本性变化它从一个需要你亲手“拧螺丝”的硬件模块变成了一个由内核中断子系统IRQ subsystem自动管理的“黑盒”。但这个“黑盒”的输入依然由你——作为BSP工程师——通过设备树Device Tree来精确定义。理解GIC-400在Linux中的角色是驾驭整个中断生态的关键。5.1 设备树中的GIC-400节点声明即契约Linux内核通过设备树描述硬件。GIC-400在设备树中必须被正确定义内核才能找到它、初始化它并将其注册为系统的中断控制器。一个标准的GIC-400节点如下gic: interrupt-controller2c001000 { compatible arm,cortex-a15-gic, arm,cortex-a9-gic; interrupt-controller; #interrupt-cells 3; reg 0x0 0x2c001000 0x0 0x1000, // Distributor base and size 0x0 0x2c002000 0x0 0x1000; // CPU Interface base and size interrupts GIC_PPI 9 (GIC_CPU_MASK_SIMPLE(2) | IRQ_TYPE_LEVEL_HIGH); };这个节点包含了所有关键信息compatible告诉内核这是一个ARM GIC具体型号兼容A15/A9内核会加载对应的驱动drivers/irqchip/irq-gic.c。interrupt-controller声明这是一个中断控制器。#interrupt-cells 3定义了子节点引用此中断控制器时需要提供3个参数。这三个参数分别是type controller-id flags。type是中断类型0SPI, 1PPI, 2SGIcontroller-id是中断ID号flags是触发方式电平/边沿。reg指定了Distributor和CPU Interface的物理地址和大小内核会据此进行内存映射。interrupts定义了GIC自身的中断这里是PPI#9通常为GIC的维护中断用于处理GIC内部错误。注意compatible字符串必须与内核源码中irq-gic.c驱动的of_match_table相匹配否则驱动无法绑定。如果你的芯片是定制GIC-400可能需要在compatible中添加自己的字符串并在驱动中增加匹配项。5.2 外设节点如何“接入”GIC-400中断属性的精确表达有了GIC-400节点所有外设节点就可以通过interrupts属性将自己的中断“插”到GIC-400上。例如UART1节点uart1: serial1c28000 { compatible snps,dw-apb-uart; reg 0x0 0x1c28000 0x0 0x1000; interrupts GIC_SPI 45 IRQ_TYPE_LEVEL_HIGH; clocks ccu 0x1a; clock-names apb_pclk; };这里的interrupts GIC_SPI 45 IRQ_TYPE_LEVEL_HIGH就是对外设中断的完整描述GIC_SPI表示这是一个SPIShared Peripheral Interrupt对应#interrupt-cells中的type0。45表示这是SPI#45对应#interrupt-cells中的controller-id。