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手册的中断号规定:

中断ID和中断类型

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.0逻辑组成 GICv3控制器内部模块(带ITS)

如上面几张图片所示,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寄存器的分布图:

GIC寄存器分布图

上面图中寄存器,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呢?计算过程如下(整数):

  1. 计算要设置那个中断使能寄存器(假设为n),n = m / 32。例如中断号m = 34则中断使能寄存器为GICD_ISENABLER1。寄存偏移地址为(0x100 + (4*n))。

  2. 计算要设置哪一位,接步骤,假设设置位为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则表示当前没有可用的中断,常见的几种情况如下所示:

  1. 在GIC分发器中禁止了向CPU发送中断请求。

  2. 在GIC的CPU接口中禁止了向CPU发送中断请求。

  3. 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的GIC流程框图

rk3568的256个SPI中断号简单分配表(部分):

rk3568的中断分配

结合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”节点,如下所示。

中断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]用于设置中断触发类型。 每一位代表一个触发方式,可进行组合,系统提供了相对的宏顶义我们可以直接使用,如下所示:

中断触发方式设置(irq.h)
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

内核源码/drivers/irqchip/irq-gic-v3.c
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宏:

内核源码include/linux/irqchip.h
 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段中。

内核源码/drivers/irqchip/irq-gic-v3.c
 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函数。

内核源码/drivers/irqchip/irq-gic-v3.c
 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:指定的“内核中断号”

返回值

屏蔽或者恢复本CPU内的所有中断
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()是通过控制中断控制器实现关闭中断。

了解了以上函数的作用,我们就可以编写中断的驱动程序了,如有遗漏的内容我们将会在代码介绍中,实验参考下一章。