3. 中断子系统¶
本章我们将讲解下和中断相关的知识,了解内核中断的框架和中断的概念,对于arm的中断控制器(GIC v3)相关内容,主要是借鉴参考手册简单解释下。 另外,还会简单分析下内核的中断控制器的初始化源码,以及编写中断驱动常用的API。
本章将简单介绍下面几部分:
中断子系统框架
GIC v3中断控制器
内核中断驱动核心简单分析
中断相关API和数据结构
3.1. 中断子系统框架¶
中断是指CPU正常运行期间,由于内外部事件或程序预先安排的事件,引起的 CPU暂时停止正在运行的程序, 转而为该内部或外部预先安排的事件服务的程序中去,服务完毕后再返回去继续执行被暂时中断的程序。
3.1.1. 中断硬件简单描述¶
中断硬件主要有三种器件参与,各个外设、中断控制器和CPU。之间的关系可以简单看下下面图片:
提示
在ARMv8体系结构中,将处理器处理事务的抽象过程定义为PE(Processing Element),可以将PE简单理解为处理器核心
1、外设,在发生中断事件的时候,通过外设上的电气信号向中断控制器请求处理。
2、由于中断数量越来越多,需要一个专门设备来管理中断,中断控制器就是起这个作用,它连接外设中断系统和CPU系统的桥梁,接收外部中断,处理后向处理器上报中断信号。 以arm为例,GIC中断控制器,外部中断信号以后,经过处理标记为FIQ、IRQ等,然后传输给arm内核。
3、CPU的主要功能是运算,CPU并不处理中断优先级,只是接收中断控制器的中断信息,运行中断处理。 例如:对于ARM,cpu会接收到是IRQ和FIQ信号,分别让ARM进入IRQ mode和FIQ mode。
3.1.2. 软件框架¶
linux内核软件部分,中断子系统分成4个部分:
(1)硬件无关的代码,我们称之内核通用中断处理模块。无论是哪种CPU,那种控制器,其中断处理的过程都有一些相同的内容,这些相同的内容被抽象出来,和硬件无关。 此外,各个外设的驱动代码中,用一个统一的接口实现irq相关的管理,这些代码组成了内核中断子系统的核心部分。
(2)CPU架构相关的中断处理。 和系统使用的具体的CPU架构相关。
(3)中断控制器驱动代码。和系统使用的中断控制器相关。
(4)普通其他驱动。这些驱动将使用内核通用中断处理模块的API来实现自己的驱动逻辑。
下面列举一些中断常见的概念:
HW interrupt ID
硬件中断ID,就是是GIC硬件上的irq ID,是GIC标准定义的,在GIC小节会讲。
IRQ number
IRQ number是软件方面定义,和硬件无关,CPU需要为每一个外设中断编号,我们称之IRQ Number。这个IRQ number是一个虚拟的interrupt ID,仅仅是被CPU用来标识一个外设中断。
IRQ domain
irq domain 翻译过来就是”irq 域”的意思,在内核中是一个比较常见的概念,也就是将某一类资源划分成不同的区域,相同的域下共享一些共同的属性, domain实际上也是对模块化的一种体现,irq domain的产生是因为GIC对于级联的支持。实际上,虽然内核对每个GIC都设立了不同的域,但是它只做了一件事:负责GIC中hwirq和逻辑irq的映射.
中断向量表
中断向量表就是中断向量的列表,中断向量表在内存中存放,表中存放着中断源所对应的中断处理程序的入口地址。
中断处理架构
中断会打断内核进程的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽量短小精悍。实际中,中断需要处理程序往往不是短小的, 对于一些繁重的中断服务程序,内核中断处理程序将分解为两部分:中断上半部和中断下半部。中断上半部分处理简单的紧急的功能(清除中断标志位等),大部分任务都放到下半部分处理。
3.2. GIC v3中断控制器简介¶
ARM多核处理器里最常用的中断控制器是GIC, GIC是Generic Interrupt Controller的缩写,提供了灵活的和可扩展的中断管理方法,支持单核系统到数百个大型多芯片设计的核心。 主要作用就是接受硬件中断信号,通过一定的设置策略,然后分发给对应的CPU进行处理。
GIC是由ARM公司提出设计规范,当前有四个版本,GIC v1~v4。设计规范中最常用的,有3个版本V2.0、V3.1、V4.1,GICv3版本设计主要运行在Armv8-A, Armv9-A等架构上。 ARM公司并给出一个实际的控制器设计参考,比如GIC-400(支持GIC v2架构)、gic500(支持GIC v3架构)、GIC-600(支持GIC v3和GIC v4架构)。 最终芯片厂商可以自己实现GIC或者直接购买ARM提供的设计。
本章简单讲解GIC V3基本结构以及实现方法,更详细的介绍可以参考 《Arm® Generic Interrupt Controller Architecture Specification》 。
3.2.1. GIC v3中断类型¶
GIC v3处理的不同种中断源:
SGI (Software Generated Interrupt):软件触发的中断。软件可以通过写 GICD_SGIR寄存器来触发一个中断事件,一般用于核间通信,内核中的IPI:inter-processor interrupts 就是基于SGI。
PPI (Private Peripheral Interrupt):私有外设中断,该终端来自于外设,被特定的核处理。GIC 是支持多核的,每个核有自己独有的中断。
SPI (Shared Peripheral Interrupt):共享外设中断,所有核共享的中断。中断产生后,可以分发到某一个CPU上。
LPI (Locality-specific Peripheral Interrupt):LPI是在 GICv3中引入的,并且与其他三种类型的中断具有非常不同的编程模型,LPI是基于消息的中断,它们的配置保存在表中而不是寄存器。
每个中断都有一个ID号标识,称为INTID,下面是ARM GIC v3手册的中断号规定:
INTID |
中断类型 |
说明 |
---|---|---|
0-15 |
SGI |
每个CPU核都有自己的16个 |
16-31 |
PPI |
每个CPU核都有自己的16个 |
32-1019 |
SPI |
例如GPIO 中断、串口中断等这些外部中断,具体由SOC厂商定义 |
1020-1023 |
特殊中断号 |
|
1024 - 8191 |
保留 |
|
8192-MAX |
LPI |
3.2.2. GIC v3基本结构¶
GIC V3.0逻辑图组成如下所示。
如上面几张图片所示,GIC v3主要这几部分组成:Distributor、CPU interface、Redistributor、ITS。 GIC v3中,将cpu interface从GIC中抽离,放入到了cpu中,cpu interface通过AXI Stream,与gic进行通信。 当GIC要发送中断,GIC通过AXI stream接口,给cpu interface发送中断命令,cpu interface收到中断命令后,根据中断线映射配置,决定是通过IRQ还是FIQ管脚,向cpu发送中断。
Distributor
Distributor用于SPI(Shared peripheral interrupts)中断的管理,具有仲裁和分发的作用,会将中断发送给Redistributor。
Distributor提供了一些编程接口或者说是寄存器,我们可以通过Distributor的编程接口实现如下操作,后面会简单介绍这些操作对应的寄存器。 下面是Distributor主要功能:
全局的开启或关闭CPU的中断。
控制任意一个中断请求的开启和关闭。
中断优先级控制。
指定中断发生时将中断请求发送到那些CPU。
interrupt属性设定,设置每个”外部中断”的触发方式(边缘触发或者电平触发)。
CPU interface简介
CPU接口为链接到GIC的处理器提供接口,与Distributor类似它也提供了一些编程接口,我们可以通过CPU接口实现以下功能(列举了几项,详细参考arm手册):
打开或关闭 CPU interface 向连接的 CPU assert 中断事件。
中断确认(acknowledging an interrupt)。
中断处理完毕的通知。
为处理器设置中断优先级掩码。
设置处理器的抢占策略
确定挂起的中断请求中优先级最高的中断请求
简单来说,CPU接口可以开启或关闭发往CPU的中断请求,CPU中断开启后只有优先级高于 “中断优先级掩码”的中断请求才能被发送到CPU。 在任何时候CPU都可以从(CPU接口寄存器)读取当前活动的最高优先级。
Redistributor简介
GICv3中,Redistributor管理SGI,PPI,LPI中断,然后将中断发送给CPU interface,包括下面功能:
启用和禁用 SGI 和 PPI。
设置 SGI 和 PPI 的优先级。
将每个 PPI 设置为电平触发或边缘触发。
将每个 SGI 和 PPI 分配给中断组。
控制 SGI 和 PPI 的状态。
内存中数据结构的基址控制,支持 LPI 的相关中断属性和挂起状态。
电源管理支持。
ITS(Interrupt translation service)
ITS 是GIC v3架构中的一种可选硬件机制,ITS提供了一种将基于消息的中断转换为LPI的软件机制,它是 在支持LPI的配置中可选地支持。ITS接收LPI中断,进行解析,然后发送到对应的redistributor,再由redistributor将中断信息,发送给cpu interface。
3.2.3. 中断状态和处理流程¶
每个中断都维护一个状态机,支持Inactive、Pending、Active、Active and pending。 中断处理的状态机如下图:
Inactive:无中断状态,即没有 Pending 也没有 Active。
Pending:硬件或软件触发了中断,该中断事件已经通过硬件信号通知到 GIC,等待 GIC分配的那CPU进行处理,在电平触发模式下,产生中断的同时保持Pending状态。
Active:CPU已经应答该中断请求,并且正在处理中。
Active and pending:当一个中断源处于Active状态的时候,同一中断源又触发了中断,进入pending状态,挂起状态。
一个简单的中断处理过程是:外设发起中断,发送给Distributor ,Distributor并基于它们的中断特性(优先级、是否使能等等)对中断进行分发处理,分发给合适的Redistributor, Redistributor 将中断信息,发送给 CPU interface,CPU interface产生合适的中断异常给处理器,处理器接收该异常,最后软件处理该中断。
3.2.4. 相关寄存器介绍¶
寄存器参考GIC v3的参考手册描述,一些寄存器大致是一样的。
重要
本小节描述的寄存器是armv8 arch64架构,寄存器值可能有些偏差,仅供参考,以实际参考手册为准。
GIC v3寄存器的分布图:
上面图中寄存器,GICC开头的表示CPU interface 寄存器,GICD 表示Distributor寄存器,GITS表示ITS 寄存器,GICR表示Redistributor 寄存器。 cpu interface寄存器提供了2种访问方式,一种是memory-mapped的访问,一种是系统寄存器访问。前缀是 ICC、ICV、ICH的就是系统寄存器,前缀是GICC、GICV等的是memory-mapped寄存器。
Distributor相关寄存器
上几小节提到GIC Distributor提供了一些编程接口,”编程接口”可以认为是寄存器,这里将简单介绍这些寄存器,列举了一些寄存器。 更详细的内容请参考ARM中断手册。
GGIC Distributor表如下所示(只列举一些):
偏移地址 |
寄存器名 |
类型 |
默认值 |
描述 |
---|---|---|---|---|
0x000 |
GICD_CTLR |
RW |
分发器控制寄存器 |
|
0x004 |
GICD_TYPER |
RO |
中断类型控制寄存器 |
|
0x008 |
GICD_IIDR |
RO |
待定 |
分发器版本信息寄存器 |
0x080-0x0FC |
GICD_IGROUPRn |
RW |
0x00000000 |
中断分组寄存器 |
0x100-0x17C |
GICD_ISENABLERn |
RW |
中断使能寄存器 |
|
0x180-0x1FC |
GICD_ICENABLERn |
RW |
0x00000000 |
中断屏蔽寄存器 |
0x200-0x27C |
GICD_ISPENDRn |
RW |
0x00000000 |
设置中断挂起寄存器 |
0x280-0x2FC |
GICD_ICPENDRn |
RW |
0x00000000 |
清除中断挂起寄存器 |
0x300-0x37C |
GICD_ISACTIVERn |
RW |
0x00000000 |
设置中断活动状态寄存器 |
0x380-0x3FC |
GICD_ICACTIVERn |
RW |
0x00000000 |
清除中断活动状态寄存器 |
0x400-0x7F8 |
GICD_IPRIORITYRn |
RW |
中断优先级设置寄存器 |
|
0x800-0x81C |
GICD_ITARGETSRn |
RO |
待定 |
中断处理目标CPU寄存器 |
0xC00-0xCFC |
GICD_ICFGRn |
RW |
待定 |
中断类型(配置)寄存器 |
0xE00-0xEFC |
GICD_NSACRn |
RW |
0x00000000 |
非安全访问配置寄存器 |
0xF00 |
GICD_SGIR |
WO |
软件中断产生寄存器 |
|
0xF10-0xF1C |
GICD_CPENDSGIRn |
RW |
0x00000000 |
软件中断挂起寄存器 |
0xF20-0xF2C |
GICD_SPENDSGIRn |
RW |
0x00000000 |
软件中断取消挂起寄存器。 |
0xFFE8 |
GICD_PIDR2 |
RO |
0x3B |
保存ID的寄存器 |
在表中只给出了个寄存器相对于GIC Distributor基地址的偏移地址只是使用到是我们将详细介绍。”默认值”选项中有”待定”手册原文是”IMPLEMENTATION DEFINED”, 原因是这张表格摘自《ARM® Generic Interrupt Controller》,它不针对具体的芯片,这些寄存器的默认值由芯片厂商决定。
表格项”地址偏移”部分值是一个范围比如”中断使能寄存器”地址偏移为”0x100-0x17C”, 原因是”中断使能寄存器”有很多,地址偏移范围是”0x100-0x17C”。
部分寄存器简单介绍如下:
中断使能寄存器GICD_ISENABLERn
中断使能寄存器与中断屏蔽寄存器(GICD_ICENABLERn)是一一对应的,GIC Distributor将中断的使能与屏蔽分开设置。
中断使能寄存器如下所示。
仅从《ARM® Generic Interrupt Controller》可知共有1020个(0~1019)中断号即1020个中断,很显然要分别控制每一个中断, 中断使能寄存器(GICD_ISENABLER)肯定不止一个。从上面表可知中断使能寄存器偏移地址为0x100-0x17C, 中断使能寄存器从GICD_ISENABLER0到GICD_ISENABLERn依次排布在这段地址空间。
在程序中我们是通过中断号区分不同的中断,假如我们已知中断号为m,那么我们怎么开启或关闭中断m呢?计算过程如下(整数):
计算要设置那个中断使能寄存器(假设为n),n = m / 32。例如中断号m = 34则中断使能寄存器为GICD_ISENABLER1。寄存偏移地址为(0x100 + (4*n))。
计算要设置哪一位,接步骤,假设设置位为q,则q = m %32,m = 34,则开启中断号为34的中断就需要设置GICD_ISENABLER1[2]。
中断使能寄存器支持按位读、写,读出的数据是终端店额当前状态,为0则中断禁用,为1则中断启用。对中断使能寄存器写1则开启中断,写0无效。
中断优先级设置寄存器GICD_IPRIORITYRn
与中断使能寄存器一样GICD_IPRIORITYRn是一组寄存器,根据上面表可知组寄存器位于0x400-0x7F8偏移地址处。中断优先级设置寄存器如下所示。
从上图可以看出每个中断标号占用8位,数值越小中断优先级越高。下面介绍如何根据中断编号找到对应的中断优先级设置寄存器。 假设中断编号为m,中断优先级寄存器为n,中断优先级设置位偏移为offset,则n = m /4。寄存器偏移地址为(0x400 + (4*n))。 在寄存器中的偏移为 offset= m%4。以 m = 65为例,n = 65/4 =16,所以中断优先级设置寄存器为GICD_IPRIORITYR16,offset(n) = 65%4 = 1, 中断号65对应的寄存器为GICD_IPRIORITYR16[15:8].
3.2.4.1. CPU接口寄存器介绍¶
GIC v3的CPU接口模块同样提供了一些编程接口,”编程接口”在这里就是一些寄存器,在GIC V3中,并提供的一种新的系统寄存器方式,系统寄存器。 我们只介绍几个常用的寄存器,CPU接口memory-mapped寄存器列表如下所示。
地址偏移 |
寄存器名字 |
类型 |
复位值 |
寄存器描述 |
---|---|---|---|---|
0x0000 |
GICC_CTLR |
RW |
0x00000000 |
CPU接口控制寄存器 |
0x0004 |
GICC_PMR |
RW |
0x00000000 |
中断优先掩码寄存器 |
0x0008 |
GICC_BPR |
RW |
0x0000000 |
中断优先级分组寄存器 |
0x000C |
GICC_IAR |
RO |
0x000003FF |
中断确认寄存器 |
0x0010 |
GICC_EOIR |
WO |
中断结束寄存器 |
|
0x0014 |
GICC_RPR |
RO |
0x000000FF |
运行优先级寄存器 |
0x0018 |
GICC_HPPIR |
RO |
0x000003FF |
最高优先级挂起中断寄存器 |
0x001C |
GICC_ABPR |
RW |
0x0000000 |
GICC_BPR别名寄存器 |
0x0020 |
GICC_AIAR |
RO |
0x000003FF |
GICC_IAR别名寄存器 |
0x0024 |
GICC_AEOIR |
WO |
GICC_EOIR别名寄存器 |
|
0x0028 |
GICC_AHPPIR |
RO |
0x000003FF |
GICC_HPPIR别名寄存器 |
0x00D0-0x00DC |
GICC_APRn |
RW |
0x00000000 |
活动的优先级寄存器 |
0x00E0-0x00EC |
GICC_NSAPRnc |
RW |
0x00000000 |
不安全的活动优先级寄存器 |
0x00FC |
GICC_IIDR |
RO |
待定 |
CPU接口识别寄存器 |
0x1000 |
GICC_DIR |
WO |
禁用中断寄存器 |
结合上表常用的CPU接口寄存器介绍如下:
中断优先掩码寄存器GICC_PMR
在上一小节我们讲解了GIC分发器的中断优先级设置寄存器GICD_IPRIORITYRn,每个中断占8位。 这里的中断优先级掩码寄存器GICC_PMR用8位代表一个中断阈值。高于这个优先级的中断才能被送到CPU。GICC_PMR寄存器如下所示。
从上图可以看出GICC_PMR寄存器后8位(0~7)用于设置一个优先级,它的格式与GICD_IPRIORITYR寄存器相同。设置生效后高于此优先级的中断才能发送到CPU。需要注意的是8位寄存器只有高4位有效。 与STM32一样,这四位还将分为”抢占优先级”和”子优先级”。讲解优先级分组时再详细介绍。
中断优先级分组寄存器GICC_BPR
中断优先级分组寄存器用于将8位的优先级分成两部分,一部分表示抢占优先级另外一部分表示自优先级,这和STM32的中断优先级分组相同。GICC_BPR寄存器如下所示。
中断优先级分组寄存器的后三位用于设置中断优先级分组,如下表所示。
表 中断优先级分组
GICC_BPR [2:0] |
中断优先级值PRI_N[7:4] |
级数 |
|||
---|---|---|---|---|---|
二进制点 |
抢占级位 |
子优先级位 |
主优先级 |
子优先级 |
|
0b 001 |
0b xxxx |
[7:4] |
无 |
16 |
0 |
0b 010 |
0b xxxx |
[7:4] |
无 |
16 |
0 |
0b 011 |
0b xxxx |
[7:4] |
无 |
16 |
0 |
0b 100 |
0b xxx.y |
[7:5] |
[4] |
8 |
2 |
0b 101 |
0b xx.yy |
[7:6] |
[5:4] |
4 |
4 |
0b 110 |
0b x.yyy |
[7] |
[6:4] |
2 |
8 |
0b 111 |
0b .yyyy |
None |
[7:4] |
None |
16 |
每个中断拥有8为中断优先级设置位,但是只有高4位有效,所以上表中GICC_BPR [2:0] 设置为1到3是相同的,即只有16级抢占优先级没有子优先级。
中断确认寄存器GICC_IAR
中断确认寄存器GICC_IAR保存当前挂起的最高优先级中断,寄存器描述如下图所示。
GICC_IAR寄存器共有两个字段,CPUID[10:12]保存请求中断的CPU ID。对于多核的CPU来说,在处理中断的时候会使用到该位保存的信息。interrupt ID[0:9]用于记录当前挂起的最高优先级中断,读取该寄存器, 如果结果是1023则表示当前没有可用的中断,常见的几种情况如下所示:
在GIC分发器中禁止了向CPU发送中断请求。
在GIC的CPU接口中禁止了向CPU发送中断请求。
CPU接口上没有挂起的中断或者挂起的中断优先级小于等于GICC_PMR寄存器设定的优先级值。
下面是cpu接口的系统寄存器:
cpu interface从gic内部剥离,实现在PE的内部,通过系统寄存器,从而实现中断的快速响应,系统寄存器和memory-mapped寄存器类似。 另外,可以看到上面列出的寄存器后缀都有EL1,是GIC系统寄存器接口的异常级别,在armv8,异常等级分为EL0、EL1、EL2、EL3,数字越大等级越高。
中断优先掩码寄存器ICC_PMR_EL1
提供中断优先级功能,只有优先级高于此值的中断寄存器才通知给PE。和GICC_PMR一样,该寄存器后8位(0~7)用于设置一个优先级,只有高4位有效。
3.2.5. GIC-600简单介绍¶
rk3568的中断控制器是GIC-600,我们简单介绍该中断控制器,GIC-600是支持GIC v3版本,是arm一个实际的控制器设计参考。 rk3568的GIC流程框图如下:
rk3568的256个SPI中断号简单分配表(部分):
结合rk3568的设备树,可以看下GIC-600寄存器地址:
寄存器 |
地址范围 |
---|---|
Distributor |
0xfd400000~0xfd410000 |
ITS |
0xfd440000~0xfd460000 |
Redistributor |
0xfd460000~0xfd520000 |
3.3. 中断驱动简单分析¶
3.3.1. 设备树中的中断信息¶
在前面,我们通过GIC控制器的背景知识讲解,以及部分汇编代码对中断过程的处理,知道了中断的使用过程。那么前面说到的这一些底层细节, 在Linux系统中已经替我们封装好了大部分的工作内容,我们需要做的只是简单地在这个框架下使用。下面将以rk3568的中断为例讲解,简单讲解下中断控制的初始化,内核源码获取参考 环境搭建章节 。
让我们先来了解一下设备树是如何描述整个中断系统信息的。打开 内核源码/arch/arm64/boot/dts/rockchip 目录下的 rk3568.dtsi 设备树文件, 找到“interrupt-controller”节点,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | gic: interrupt-controller@fd400000 {
compatible = "arm,gic-v3";
#interrupt-cells = <3>;
#address-cells = <2>;
#size-cells = <2>;
ranges;
interrupt-controller;
reg = <0x0 0xfd400000 0 0x10000>, /* GICD */
<0x0 0xfd460000 0 0xc0000>; /* GICR */
interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_HIGH>;
its: interrupt-controller@fd440000 {
compatible = "arm,gic-v3-its";
msi-controller;
#msi-cells = <1>;
reg = <0x0 0xfd440000 0x0 0x20000>; /*ITS寄存器的物理地址*/
};
};
|
compatible:compatible属性用于平台设备驱动的匹配。
reg:reg指定中断控制器相关寄存器的地址及大小,GICD是Distributor寄存器,GICR是指Redistributor寄存器。
interrupt-controller:声明该设备树节点是一个中断控制器。
#interrupt-cells :指定使用该中断控制器的节点要用几个cells来描述一个中断,可理解为用几个参数来描述一个中断信息。 在这里的意思是在intc节点的子节点将用3个参数来描述中断。
interrupts :描述中断信息,这里是用三个u32描述,是前面#interrupt-cells指定的。第一个指定中断类型,第二个中断号,第三位是触发类型
its : 在gic设备节点下,有一个子设备节点its,ITS设备用于将消息信号中断(MSI)路由到cpu
msi-controller : 标识该设备是MSI控制器
#msi-cells :必须是1,MSI设备的DeviceID
学过前面内容的同学们想必对GIC中断控制器并不陌生,GIC架构分为了:Distributor、Redistributor CPU Interface,上面设备树节点就是用来描述整个GIC控制器的。
一个GIC中断控制器的使用实例,以uart3为例(rk3568.dtsi):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | / {
compatible = "rockchip,rk3568";
interrupt-parent = <&gic>;
#address-cells = <2>;
#size-cells = <2>;
/*.............*/
uart3: serial@fe670000 {
compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
reg = <0x0 0xfe670000 0x0 0x100>;
interrupts = <GIC_SPI 119 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru SCLK_UART3>, <&cru PCLK_UART3>;
clock-names = "baudclk", "apb_pclk";
reg-shift = <2>;
reg-io-width = <4>;
dmas = <&dmac0 6>, <&dmac0 7>;
pinctrl-names = "default";
pinctrl-0 = <&uart3m0_xfer>;
status = "disabled";
};
/*.............*/
};
|
uart3是根节点下的一个子节点,根节点指定了interrupt-parent为gic。 那么uart3子节点也继承使用GIC控制器中断控制器,并用interrupts描述了它使用的资源。
interrupts:具体的中断描述信息,在该节点使用的中断控制器gic,gic节点中“#interrupt-cells = <3>”规定了使用三个cells来描述子控制器的信息。 三个参数表示的含义如下:
第一个参数用于指定中断类型,在GIC的中断的类型有三种(SPI共享中断、PPI私有中断、SGI软件中断), 我们使用的外部中断均属于SPI中断类型。
第二个参数用于设定中断编号,范围和第一个参数有关。PPI中断范围是[0-15],SPI中断范围是[0-256]。
第三个参数指定中断触发方式,参数是一个u32类型,其中后四位[0-3]用于设置中断触发类型。 每一位代表一个触发方式,可进行组合,系统提供了相对的宏顶义我们可以直接使用,如下所示:
1 2 3 4 5 6 | #define IRQ_TYPE_NONE 0
#define IRQ_TYPE_EDGE_RISING 1
#define IRQ_TYPE_EDGE_FALLING 2
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
#define IRQ_TYPE_LEVEL_HIGH 4
#define IRQ_TYPE_LEVEL_LOW 8
|
其中第三个参数的[8-15]位,在PPI中断中还用于设置“CPU屏蔽”。在多核系统中这8位用于设置PPI中断发送到那个CPU,一位代表一个CPU, 为1则将PPI中断发送到CPU0,否则屏蔽。
如下示例:
1 2 3 4 5 6 7 8 | timer {
compatible = "arm,armv8-timer";
interrupts = <GIC_PPI 13 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>,
<GIC_PPI 14 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>,
<GIC_PPI 11 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>,
<GIC_PPI 10 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>;
arm,no-tick-in-suspend;
};
|
3.3.2. GIC v3 中断控制器的代码¶
中断控制器通过IRQCHIP_DECLARE 宏注册到__irqchip_of_table
1 | IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gicv3_of_init); //初始化一个struct of_device_id的静态常量,并放置在__irqchip_of_table中
|
系统开机初始化阶段,of_irq_init函数会去查找设备节点信息,根据__irqchip_of_table段中的”arm,gic-v3”,匹配到前面小节的设备树节点“gic”,获取设备信息。 最终会执行IRQCHIP_DECLARE声明的回调函数gicv3_of_init。
在内核源码include/linux/irqchip.h文件中定义了IRQCHIP_DECLARE宏:
1 2 3 4 5 6 7 8 9 10 11 | #define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn)
#define OF_DECLARE_2(table, name, compat, fn) \
_OF_DECLARE(table, name, compat, fn, of_init_fn_2)
#define _OF_DECLARE(table, name, compat, fn, fn_type) \
static const struct of_device_id __of_table_##name \
__used __section(__##table##_of_table) \
__aligned(__alignof__(struct of_device_id)) \
= { .compatible = compat, \
.data = (fn == (fn_type)NULL) ? fn : fn }
|
该宏初始化了一个struct of_device_id静态常量,放到并放置在__irqchip_of_table段中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | static int __init gicv3_of_init(struct device_node *node, struct device_node *parent)
{
void __iomem *dist_base;
struct redist_region *rdist_regs;
u64 redist_stride;
u32 nr_redist_regions;
int err, i;
dist_base = of_iomap(node, 0); //映射 GICD 的寄存器地址空间
if (!dist_base) {
pr_err("%pOF: unable to map gic dist registers\n", node);
return -ENXIO;
}
err = gic_validate_dist_version(dist_base); //验证GIC硬件的版本是GICv3或者GICv4
if (err) {
pr_err("%pOF: no distributor detected, giving up\n", node);
goto out_unmap_dist;
}
/*读取设备树节点redistributor-regions 的值,没有就是1*/
if (of_property_read_u32(node, "#redistributor-regions", &nr_redist_regions))
nr_redist_regions = 1;
rdist_regs = kcalloc(nr_redist_regions, sizeof(*rdist_regs),
GFP_KERNEL);
if (!rdist_regs) {
err = -ENOMEM;
goto out_unmap_dist;
}
for (i = 0; i < nr_redist_regions; i++) { //为一个 GICR 域分配基地址
struct resource res;
int ret;
ret = of_address_to_resource(node, 1 + i, &res);
rdist_regs[i].redist_base = of_iomap(node, 1 + i);
if (ret || !rdist_regs[i].redist_base) {
pr_err("%pOF: couldn't map region %d\n", node, i);
err = -ENODEV;
goto out_unmap_rdist;
}
rdist_regs[i].phys_base = res.start;
}
/*通过 DTS 读取 redistributor-regions 的值,没有就是0*/
if (of_property_read_u64(node, "redistributor-stride", &redist_stride))
redist_stride = 0;
/*GIC v3初始化的核心工作*/
err = gic_init_bases(dist_base, rdist_regs, nr_redist_regions,
redist_stride, &node->fwnode);
if (err)
goto out_unmap_rdist;
gic_populate_ppi_partitions(node); //设置PPI的亲和性
if (static_branch_likely(&supports_deactivate_key))
gic_of_setup_kvm_info(node);
return 0;
out_unmap_rdist:
for (i = 0; i < nr_redist_regions; i++)
if (rdist_regs[i].redist_base)
iounmap(rdist_regs[i].redist_base);
kfree(rdist_regs);
out_unmap_dist:
iounmap(dist_base);
return err;
}
|
函数gicv3_of_init如上所示,该函数将映射GIC寄存器基地址,然后获取GIC的版本信息(通过读GICD_PIDR2寄存器,读取寄存器bit[7:4],是0x3就是GIC v3,前面小节GIC寄存器有描述), 获取设备树的属性值,调用gic_init_bases函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | static int __init gic_init_bases(void __iomem *dist_base,
struct redist_region *rdist_regs,
u32 nr_redist_regions,
u64 redist_stride,
struct fwnode_handle *handle)
{
u32 typer;
int gic_irqs;
int err;
if (!is_hyp_mode_available())
static_branch_disable(&supports_deactivate_key);
if (static_branch_likely(&supports_deactivate_key))
pr_info("GIC: Using split EOI/Deactivate mode\n");
/*对GIC v3硬件设备相关的数据结构初始化*/
gic_data.fwnode = handle; //一些回调方法
gic_data.dist_base = dist_base; //Distributor内存区间的地址
gic_data.redist_regions = rdist_regs; //gic中Redistributor域的信息
gic_data.nr_redist_regions = nr_redist_regions; //Redistributor域的个数
gic_data.redist_stride = redist_stride; //Redistributor域之间的步长
/*
* Find out how many interrupts are supported.
* The GIC only supports up to 1020 interrupt sources (SGI+PPI+SPI)
*/
typer = readl_relaxed(gic_data.dist_base + GICD_TYPER);
gic_data.rdists.gicd_typer = typer;
gic_irqs = GICD_TYPER_IRQS(typer); //获取bit[4:0]的值,算出SPI个数
if (gic_irqs > 1020)
gic_irqs = 1020;
gic_data.irq_nr = gic_irqs;
/*在系统中为GIC V3注册一个irq domain的数据结构*/
gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops,
&gic_data);
irq_domain_update_bus_token(gic_data.domain, DOMAIN_BUS_WIRED);
gic_data.rdists.rdist = alloc_percpu(typeof(*gic_data.rdists.rdist));
gic_data.rdists.has_vlpis = true;
gic_data.rdists.has_direct_lpi = true;
if (WARN_ON(!gic_data.domain) || WARN_ON(!gic_data.rdists.rdist)) {
err = -ENOMEM;
goto out_free;
}
gic_data.has_rss = !!(typer & GICD_TYPER_RSS);
pr_info("Distributor has %sRange Selector support\n",
gic_data.has_rss ? "" : "no ");
if (typer & GICD_TYPER_MBIS) {
err = mbi_init(handle, gic_data.domain);
if (err)
pr_err("Failed to initialize MBIs\n");
}
set_handle_irq(gic_handle_irq); //设置中断回调函数,gic_handle_irq将查表等获取到是哪一个中断,处理中断
gic_update_vlpi_properties(); /*更新Redistributor相关的属性*/
if (IS_ENABLED(CONFIG_ARM_GIC_V3_ITS) && gic_dist_supports_lpis())
its_init(handle, &gic_data.rdists, gic_data.domain); //初始化ITS
gic_smp_init(); //设置核间通信等
gic_dist_init(); //初始化Distributor,
gic_cpu_init();
gic_cpu_pm_init(); //初始化GIC电源管理
return 0;
out_free:
if (gic_data.domain)
irq_domain_remove(gic_data.domain);
free_percpu(gic_data.rdists.rdist);
return err;
}
|
gic_init_bases函数如上,一些解释看下上面的代码注释,源码分析到这,主要分析了下中断控制器驱动相关源码,详细的IRQ Domain映射,irq事件详细处理过程可自行参考内核源码。
3.4. 中断常用的API和重要的数据结构¶
在下章编写驱动之前,我们需要了解内核提供的中断常用接口函数。
3.4.1. request_irq中断申请和释放函数¶
1 2 3 4 5 | static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev);
int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id);
|
request_irq()函数
函数参数:
irq:用于指定“内核中断号”,这个参数我们会从设备树中获取或转换得到。在内核空间中它代表一个唯一的中断编号。
handler:用于指定中断处理函数,中断发生后跳转到该函数去执行。
flags:中断触发条件,也就是我们常说的上升沿触发、下降沿触发等等触发方式通过“|”进行组合(注意,这里的设置会覆盖设备树中的默认设置),宏定义如下所示:
1 2 3 4 5 6 7 8 9 10 11 | #define IRQF_TRIGGER_NONE 0x00000000
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING 0x00000002
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010
#define IRQF_SHARED 0x00000080
/*-----------以下宏定义省略------------*/
|
name:中断的名字,中断申请成功后会在“/proc/interrupts”目录下看到对应的文件。
dev:如果使用了**IRQF_SHARED** 宏,则开启了共享中断。“共享中断”指的是多个驱动程序共用同一个中断。 开启了共享中断之后,中断发生后内核会依次调用这些驱动的“中断服务函数”。 这就需要我们在中断服务函数里判断中断是否来自本驱动,这里就可以用dev参数做中断判断。 即使不用dev参数判断中断来自哪个驱动,在申请中断时也要加上dev参数 因为在注销驱动时内核会根据dev参数决定删除哪个中断服务函数。
返回值:
成功:返回0
失败:返回负数。
devm_request_irq()函数
此函数与request_irq()的区别是devm_开头的API申请的是内核“managed”的资源,一般不需要在出错处理和 remove()接口里再显式的释放。
1 | void free_irq(unsigned int irq, void *dev);
|
free_irq()函数:
irq:从设备树中得到或者转换得到的中断编号。
dev:与request_irq函数中dev传入的参数一致。
3.4.2. 中断处理函数¶
在中断申请时需要指定一个中断处理函数,书写格式如下所示。
1 | irqreturn_t (*irq_handler_t)(int irq, void * dev);
|
参数:
irq:用于指定“内核中断号”。
dev:在共享中断中,用来判断中断产生的驱动是哪个,具体介绍同上中断注册函数。 不同的是dev参数是内核“带回”的。如果使用了共享中断还得根据dev带回的硬件信息判断中断是否来自本驱动,或者不使用dev, 直接读取硬件寄存器判断中断是否来自本驱动。如果不是,应当立即跳出中断服务函数,否则正常执行中断服务函数。
返回值:
irqreturn_t类型:枚举类型变量,如下所示。
1 2 3 4 5 6 7 | enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;
|
如果是“共享中断”并且在中断服务函数中发现中断不是来自本驱动则应当返回 IRQ_NONE , 如果没有开启共享中断或者开启了并且中断来自本驱动则返回 IRQ_HANDLED,表示中断请求已经被正常处理。 第三个参数涉及到我们后面会讲到的中断服务函数的“上半部分”和“下半部分”, 如果在中断服务函数是使用“上半部分”和“下半部分”实现,则应当返回IRQ_WAKE_THREAD。
3.4.3. 中断的使能和屏蔽函数¶
通过函数使能、禁用某一个中断。
1 2 | void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)
|
参数:
irq:指定的“内核中断号”
返回值:无
1 2 3 4 | local_irq_enable()
local_irq_disable()
local_irq_save(flags)
local_irq_restore(flags)
|
由于“全局中断”的特殊性,通常情况下载关闭之前要使用local_irq_save保存当前中断状态, 开启之后使用local_irq_restore宏恢复关闭之前的状态。flags是一个unsigned long 类型的数据。
在armv8-arch64架构下,local_irq_disable()只是操作daif标志位,关闭当前cpu的异常,与具体中断控制器无关,而disable_irq()是通过控制中断控制器实现关闭中断。
了解了以上函数的作用,我们就可以编写中断的驱动程序了,如有遗漏的内容我们将会在代码介绍中,实验参考下一章。
3.5. 参考¶
Documentation/devicetree/bindings/arm/gic-v3.txt
https://www.kernel.org/doc/Documentation/IRQ-domain.txt(master)
《Arm® Generic Interrupt Controller Architecture Specification》
《GIC-600 Generic Interrupt Controller Technical Reference Manual》