GIC 介绍
众所周知,要想获取一个功能的执行状态,我们可以采用两种方式:
- 定时查询: 查询功能是否完成;
- 产生中断: 告诉CPU某个功能已经完成。
为了尽可能提高 CPU 处理数据的性能,提高获取功能完成状态的实时性,中断是一个很好的选择, 为什么还要一个中断控制器呢?直接将外设的中断引到 CPU 上不更简单吗?
- 如果每个外设的中断都直接给到 CPU,外设数量众多导致连接 CPU 引脚数量增加 CPU 的体积增加
- 如果同多个中断发生,CPU 需要自己维护中断,维护队列和判定优先级都会占用 CPU 的时间,降低 CPU 的性能
因此中断控制器就应运而生了,它是为了减少CPU的负载,让CPU更专注于计算。 中断控制器相当于一个代理,外设产生的中断会先发给中断控制器,中断控制器管理、控制可屏蔽中断,并且对中断的优先级进行判断,再将高优先级中断转交给CPU,CPU 既能专注计算又能及时响应到中断事件,并执行相应的中断服务程序。
当前主流芯片架构通常采用不同的中断控制器:
我们重点关注一下 ARM 使用的 GIC(Generic Interrupt Controller,通用中断控制器)中断控制器,它是ARM公司给 Cortex-A/R 内核提供的一个中断控制器,目前 GIC 有 4 个版本 GICv1~GICv4,V1是比较旧的版本当前已经被废弃了,下表总结了 GIC 的主要版本及应用的典型处理器:
GIC-v3 架构
GICv3 中断控制器的寄存器接口可分为三模块:
- Distributor interface
- Redistributor interface
- CPU interface
通常,Distributor 和 Redistributor 用于配置中断,而 CPU 接口用于处理中断。
GIC 使用组优先级字段来确定待处理中断是否具有足够的优先级来抢占PE上的执行,具体规则如下:
- 中断的优先级值必须低于其优先级掩码的值
- 中断的组优先级字段的值必须低于 PE 上运行优先级的值
GICv3 为CPU处理所有连接到其上的中断,包括管理所有的中断源、中断行为、中断分组以及中断路由方式等,同时还提供相应的寄存器接口用于软件对这些行为的控制。设或软件触发中断后,GICv3根据中断配置信息,将其路由到特定 CPU 的 IRQ 或 FIQ 中断线上。CPU 接收到中断后执行必要的上下文保存后,跳转到中断异常入口,并执行相应的中断处理流程。
为了实现中断的配置、接收、仲裁和路由功能,GICv3 设计了不同组件如下:
PE (Process Element):处理器单元,中断的最终接收者和处理者。
Distributor: SPI 中断的管理,将中断发送给 Redistributor, 外设经特定硬件中断线连接到 Distributor, 判断 SPI 中断的优先级,决定优先处理哪个中断,同时维护中断的 active/pending/acknowledged 状态等,Distributor 提供了以下控制功能的接口:
- 配置 SPIs 的中断优先级
- 使能或者除能 SPIs
- 生成基于消息信号的 SPIs
- 控制 SPIs 的状态机
- 每个 SPI 的路由信息
- 设置 SPI 的触发模式,边沿触发或者电平触发
Redistributor: SGI PPI LPI 中断的管理,将中断发送给 CPU interface。每个PE对应了一个 Redistributor,它是 PPI/SGI/LPI 中断的管理者,决定它们的优先级,触发方式,控制它们的状态,以及 enable/disable 特定中断。Redistributor 提供了以下控制功能的接口:
- 使能或除能 SGI 和 PPI
- 配置 SGI 和 PPI 的优先级
- 配置 PPI 的触发模式,边沿触发或者电平触发
- 对每个 SGI 和 PPI 进行中断分组
- 控制 SPI 和 PPI 的状态机
- 控制 LPI 中断所需的配置信息表和 pending 状态表的内存基地址
- 提供对所连接 PE 的电源管理
CPU interface: 传输中断给 PE, 每个 Redistributor 连接了一个CPU interface,它负责打开和关闭PE的中断处理能力,Acknowledge 中断,为PE维护一个中断优先级掩码(只响应更高优先级中断),定义中断抢占策略,执行中断降级等。CPU interface 提供了以下控制功能的接口:
- 打开或关闭中断处理
- 中断的应答
- 中断处理完成,执行 priority drop and deactivation
- 为 CPU 设置中断优先级掩码
- 设置 CPU 的中断抢占(preemption)策略。
- 在多个中断事件同时到来的时候,选择一个最高优先级的中断通知 CPU
ITS:ITS 接收消息信号,根据消息携带的 Event ID 和 Device ID,翻译得到物理中断号以及目标 PE 的编号或 目标 Redistributor 地址,ITS 与外设之间通过系统总线连接,外设采用写内存地址的形式发一个中断消息给 ITS。
中断的类型
GIC 通用中断控制器可以处理四种不同类型的中断源:
- 共享外设中断 Shared Peripheral Interrupt 简称 SPI,可以传递给任何连接的核心的外设中断。
- 私有外设中断 Private Peripheral Interrupt 简称 PPI,只传递给指定的一个核心的外设中断,例如来自通用定时器的中断就是 PPI 的一种。
- 软件生成中断 Shared Peripheral Interrupt 简称 SGI,通常用于处理器间通信,通过向 GIC 中的 SGI 寄存器写入来生成。
- 特定本地外设中断 Locality-specific Peripheral Interrupt 简称 LPI,首次在 GICv3 中引入,基于消息信号中断,具有与其他三种类型中断非常不同的编程模型。
每个中断源都由一个 ID 号标识,被称为 INTID,表中介绍的中断类型是根据中断的范围定义的:
中断信号传输
传统上,系统使用专用硬件连线,信号从外围设备到断控制器传递中断信号,如下图所示:
Arm CoreLink GICv3 支持这个模型,但也提供了一个额外的信令机制:消息信号中断 message-signaled interrupts 简称 MSI,MSI通过写入传输到中断控制器中的一个寄存器。如下图所示:
使用消息将中断从外围设备转发到中断控制器,消除了对每个中断源的专用信号线的要求,这对于大型系统的来说是一个优点,数百甚至数千个信号可能通过 SoC 路由并收敛到中断控制器上。在 Arm CoreLink GICv3 中,SPI 也可以支持通过消息信号发出,LPI则全部通过消息信号发出。他们可以通过不同的寄存器触发,如下:
中断状态机
中断控制器为每个 SPI、PPI 和 SGI 中断源维护一个状态机,此状态机由 4 种状态组成:
- 非活动状态 (Inactive) 当前并未 assert 该中断源
- 待定状态 (Pending) 中断源已被 assert ,但该中断尚未被 PE 应答 (ack)
- 活动状态 (Active) 该中断源已被 assert,并且该中断已被一个 PE 应答 (ack)
- 活动和待定状态 (Active and Pending) 该中断的一个实例已被应答 (ack),而另一个实例现在正在 pending
PS: 这里 Active and Pending 状态,我的理解是一个中断源可以产生多个中断或者叫中断实例 (instance),当中断源连续产生两个中断实例,前一个实例进入 active 状态,后一个实例进入 pending 状态,此时 Active and Pending 状态并不是描述这两个其中一个中断实例的状态,而是中断源的状态(这两个实例共享一个中断 ID 和 中断源),表示该中断源产生多了中断实例,一个正在被处理,剩下的正在等待。
上图中,中断选择哪种生命周期路径取决于它是被配置为电平触发模式还是边沿触发模式,二者主要区别在于:
- 电平触发模式的中断,中断输入上的上升边缘导致中断进入 pending 状态,中断保持 asserted 直到外设 deassert 中断信号
- 边沿触发模式的中断,中断输入上的上升边缘导致中断进入 pending 状态,但中断不会保持 asserted,也没有外设 deassert 掉。
例如下图的电平触发模式下中断的状态转换:
- Inactive to Pending, 当 assert 中断源时,中断将从 Inactive 状态转换为 Pending。此时如果中断被启用并且具有足够的优先级,那么 GIC 将向 PE assert 中断信号。
- Pending to Active & Pending, 当 PE 通过读取 CPU 接口中的一个中断确认寄存器(IAR)来 ACK 中断时,中断从 Pending 转换为 Active and Pending, 此时 GIC deassert 中断信号但外设还没有 deassert。
- Active & Pending to Active, 当外设 dessert 中断信号时,中断从 Active and Pending 转换为 Active。
- Active to Inactive, 当PE写入 CPU 接口中的中断结束寄存器(EOIR)时,中断从 Active 到 Inactive,这表明PE已经完成了对中断的处理。
总结下来有两个关键信息,当 GIC assert 外设中断时,此中断进去 Pending 状态进行等待,当 PE 应答 (ACK) 中断时,PE 立马处理该中断,但由于是电平触发模式,外设中断信号可能还处于 assert 状态,让中断进入一个中间过渡状态 Active & Pending,当外设 deassert 后才转为 Active,中断在 Active & Pending 时 PE 已经开始处理了。
例如下图的边沿触发模式下中断的状态转换:
- Inactive to Pending, 当 assert 中断源时,中断将从 Inactive 状态转换为 Pending。此时如果中断被启用并且具有足够的优先级,那么 GIC 将向 PE assert 中断信号。
- Pending to Active, 当 PE 通过读取 CPU 接口中的一个 IARs 寄存器来确认(ACK)中断时,中断将从 Pending 转换为Active。
- Active & Pending to Active, 如果外设 re-assert 信号,则中断将从 Active 转到 Pending to Active。
- Active & Pending to Pending, 当 PE 写入 CPU 接口中的一个 EOIR 寄存器时,中断从 Active & Pending 转 Pending。这表明 PE 已经完成了处理中断的第一个实例。此时,GIC 将向 PE 重新 assert 中断信号。
同样总结下来,中断状态机不是针对每个中断,而是针对每个中断源,边沿触发模式下的中断真正进入执行状态只需要 PE ACK 中断即可,不需要像电平触发模式还需要等待外设的 de-assert 信号,正常模式下只发出一个中断实例时的中断源状态只经历 Inactive -> Pening -> Active -> Inactive 转换,但在上图场景中,前一个中断实例正在被处理时,中断源状态机为 Active,同一个中断源又 assert 了一个中断实例,需要等待上一个中断实例完成(写 EOIR),才能让下一个中断实例进入,此时中断源状态为 Active & Pending 表示前一个中断实例的处理还未完成,后一个中断实例需要等待。
- A1, A2. add pending state: 要么是外围设备产生中断的结果,要么是软件产生中断的结果。
- B1, B2. remove pending state。
- C. pending to active: PE 确认了对边缘触发的中断时, 当软件从 CPU 接口中读取一个 INTID 值时,就会发生这种转换
- D. pending to active & pending: 这种转换发生在 PE 确认了电平触发模式的中断时
- E1, E2. remove active state: 结束当前的中断实例时,后面没中断进入 Inactive,后面还有中断进入 Pending。
总结:当 PE ack 了中断,中断源进入 active 状态,当外设 assert 了中断,中断源进入 pending 状态,如果 ack 和 assert 同时发生,中断源就会进去 active & pending 状态。
中断的生命周期
了解了中断状态机,中断生命周期就好理解了:
- Generate:可以是外设也可以是软件
- Distribute:组件实现中断的分组,划分优先级并发送给 CPU 接口
- Deliver:CPU 接口将中断传输给对应的 PE
- Activate:当 PE ACK 中断后,将中断状态设置为 Active
- Priority drop:将当前中断的最高优先级进行重置,以便能够响应低优先级中断
- Deactuvation:将中断的状态,设置为 Inactive 状态
Priority drop 将在中断优先级章节详细介绍。
中断优先级
软件通过给每个中断源分配一个优先级值来配置 GIC 中的中断优先级。优先级值是一个 8 位的无符号二进制数。支持两个安全状态的 GIC 实现必须实现最少 32 个和最多 256 个物理优先级级别。只支持单个安全状态的 GIC 实现必须实现最少 16 个和最多 256 个物理优先级级别。如果 GIC 实现的优先级级别少于 256 个,则优先级字段的低位将被设置为 RAZ/WI(保留为零/写入忽略)。这意味着实现的优先级字段位数由实现定义:
在 GIC 的优先级化方案中,较低的数值具有较高的优先级。优先级字段值 0 始终表示最高可能的中断优先级,而最低优先级值取决于实现的优先级级别数量。
GICD_IPRIORITYR 寄存器保存每个支持的 SPI 的优先级值,该寄存器的个数为 255 个,因为实现可以为特定目的保留一个 SPI,并分配一个固定的优先级给该中断,该中断的优先级值是只读的,对于其他SPI,软件可以写入 GICD_IPRIORITYR 寄存器来设置中断优先级。
在多处理器实现中,GICR_IPRIORITYR 和 GICR_IPRIORITYRE 寄存器为每个目标 PE 独立定义了每个 SGI 和 PPI INTID 的中断优先级,针对 LPI 类型的中断,主要通过在内存中的 LPI 配置表存储关于 LPI 的优先级信息,而不是寄存器。
优先级重置
关于 Priority drop,需要先了解 Running priority and preemption (抢占)。
在 CPU 接口中,需要设置优先级掩码寄存器,来指定中断必须转发到 PE 的最小优先级。GICv3 体系结构也具有运行优先级的概念。当 PE 确认一个中断时,中断的优先级就会赋给运行优先级。当 PE 写入中断结束(EOI)寄存器时,运行的优先级会返回到以前的值。下图显示了PE随时间的运行优先级的示例:
在使用中断抢占策略时,运行优先级的概念就很重要了。当高优先级中断发送到已经在处理低优先级中断的 PE 时,就会发生抢占。抢占为软件带来了一些额外的复杂性,但它可以防止低优先级中断阻止对高优先级中断的处理。
如上图,不启用抢占功能时,高优先级中断会被阻止,直到低优先级中断完成后进行 Priority drop 优先级重置回复到之前的运行时优先级才能处理后面的高优先级。
开启抢占功能时,当高优先级中断变为 Pending ,它优先于先前发出信号的低优先级中断。上图显示了一个级别的抢占。然而,也有可能有多个层次的 抢占。Arm CoreLink GICv3 架构允许软件通过指定抢占发生所需的优先级规则来控制抢占。这是通过二进制点寄存器来控制的: ICC_BPRn_EL1。
组优先级和子优先级
二进制点寄存器将优先级值分成两个字段:组优先级和子优先级。在确定抢占时,具有相同组优先级的所有中断被认为具有相同的优先级,而不考虑子优先级。
对于抢占只考虑组优先级位忽略子优先级位,二进制点寄存器只影响抢占策略
例如,考虑以下三个中断:INTID A 的优先级为0x10,INTID B 的优先级为0x20, INTID C 的优先级为0x21。在这个例子中,我们需要的抢占策略为:A 可以抢占 B 或 C,但 B 不能抢占 C, B 和 C 有相似的优先级。为了实现这一点,可以将组和子优先级之间的划分设置为 N=4,有了这种分裂,B 和 C 有相同优先权,然而 A 仍然有更高的优先级,如下图:
GIC 使用组优先级字段来确定待处理中断是否具有足够的优先级来抢占PE上的执行,具体规则如下:
- 中断的优先级值必须低于其优先级掩码的值
- 中断的组优先级字段的值必须低于 PE 上运行优先级的值