2. Linux内核中断子系统

前面的Pinctrl子系统和GPIO子系统读取引脚电平实验,依托内核定时器周期性扫描按键电平,虽能实现功能, 但存在CPU空转占用率高、响应延迟大的核心弊端,尤其在多任务嵌入式Linux系统中,会大幅浪费系统算力。 而本章节介绍的Linux中断子系统,正是内核为解决“外设主动通知、内核异步响应”设计的核心基础子系统, 是构建高效、低功耗外设驱动的关键支撑,也是内核驱动开发中必须掌握的核心模块。

2.1. 中断子系统框架

中断是指CPU正常运行期间,由于内外部事件或程序预先安排的事件,引起的 CPU暂时停止正在运行的程序, 转而为该内部或外部预先安排的事件服务的程序中去,服务完毕后再返回去继续执行被暂时中断的程序。

2.1.1. 中断硬件描述

中断硬件主要有三种器件参与,各外设、中断控制器和CPU之间的关系如下图:

关系

提示

在ARMv8体系结构中,将处理器处理事务的抽象过程定义为PE(Processing Element),可以将PE简单理解为处理器核心

各组件作用说明如下:

  • 外设:在发生中断事件的时候,通过外设上的电气信号向中断控制器请求处理。

  • 中断控制器:由于中断数量越来越多,需要一个专门设备来管理中断,中断控制器就是起这个作用,它连接外设中断系统和CPU系统的桥梁,接收外部中断,处理后向处理器上报中断信号。以ARM为例,GIC中断控制器接收外部中断信号以后,经过处理标记为FIQ、IRQ等,然后传输给arm内核。

  • CPU:CPU的主要功能是运算,CPU并不处理中断优先级,只是接收中断控制器的中断信息,运行中断处理。例如:对于ARM,CPU会接收到是IRQ和FIQ信号,分别让ARM进入IRQ mode和FIQ mode。

2.1.2. 中断软件框架

Linux内核中断子系统的软件部分通常划分为以下4个核心部分:

1、通用中断处理模块

内核核心层,位于内核源码/kernel/irq/目录,是中断子系统的核心框架,提供统一的中断管理接口和通用处理逻辑:

  • 提供标准化API(request_irq()、free_irq()等)给上层驱动,屏蔽硬件细节;

  • 维护中断描述符(irq_desc)和中断动作链表(irqaction);

  • 实现中断流控(电平/边沿触发处理、嵌套管理)和中断亲和性调度;

  • 负责中断上下部调度,如软中断、tasklet、工作队列管理。

2、体系架构相关处理层

arch层,位于内核源码/arch/xxx/kernel/目录,处理特定CPU架构的中断特性:

  • 实现CPU异常向量表和中断入口(汇编代码),处理CPU与中断控制器的交互;

  • 管理CPU本地中断状态(使能/禁用中断、中断栈设置);

  • 处理架构相关的中断上下文切换和异常处理流程;

  • 提供架构特定的中断初始化和硬件抽象接口。

3、中断控制器驱动层

irqchip层,抽象各类中断控制器(如GIC、APIC、VIC)的硬件操作,通过irq_chip结构体封装底层硬件访问:

  • 实现中断控制器的基本操作:中断屏蔽、清除、触发、优先级设置等;

  • 管理中断号映射(irq_domain机制),将硬件中断号(hwirq)转换为内核全局IRQ号;

  • 支持多级中断控制器拓扑结构,适配复杂系统中的中断路由;

  • 典型实现:GICv3驱动(arm64)、IO-APIC驱动(x86)等。

4、外设驱动层

用户层,位于驱动程序中,是中断的最终使用者,通过通用 API 注册中断处理函数:

  • 调用通用中断API申请/释放中断资源,绑定中断服务例程(ISR);

  • 实现设备特定的中断处理逻辑;

  • 区分紧急处理(顶半部,原子操作)和延迟处理(底半部,软中断/工作队列);

  • 无需关注底层硬件细节,实现跨平台移植性。

2.1.3. 中断常见的概念

下面列举一些中断常见的概念:

  • 硬件中断ID(HW interrupt ID)

硬件中断ID就是GIC硬件上的irq ID,是GIC标准定义的,在GIC小节会讲。

  • 中断号(IRQ number)

中断号是软件方面定义,和硬件无关,CPU需要为每一个外设中断编号,我们称之IRQ Number。这个IRQ number是一个虚拟的interrupt ID,仅仅是被CPU用来标识一个外设中断。

  • 中断域(IRQ domain)

中断域在内核中是一个比较常见的概念,也就是将某一类资源划分成不同的区域,相同的域下共享一些共同的属性, domain实际上也是对模块化的一种体现,irq domain的产生是因为GIC对于级联的支持。实际上,虽然内核对每个GIC都设立了不同的域,但是它只做了一件事:负责GIC中hwirq和逻辑irq的映射。

  • 中断向量表

中断向量表就是中断向量的列表,中断向量表在内存中存放,表中存放着中断源所对应的中断处理程序的入口地址。

  • 中断处理架构

中断会打断内核进程的正常调度和运行,系统对更高吞吐率的追求势必要求中断服务程序尽量短小精悍。实际中,中断需要处理程序往往不是短小的, 对于一些繁重的中断服务程序,内核中断处理程序将分解为两部分:中断顶半部和中断底半部。

  • 中断顶半部

中断顶半部也称中断上半部,中断服务函数本身,负责快速响应硬件、标记事件、退出中断,避免阻塞其他中断。

  • 中断底半部

中断底半部也称中断下半部,处理中断耗时逻辑,顶半部触发后延后执行。

2.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》

2.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

-

2.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发送中断。

2.2.2.1. Distributor

Distributor用于SPI(Shared peripheral interrupts)中断的管理,具有仲裁和分发的作用,会将中断发送给Redistributor。

Distributor提供了一些编程接口或者说是寄存器,我们可以通过Distributor的编程接口实现如下操作,后面会简单介绍这些操作对应的寄存器。 下面是Distributor主要功能:

  • 全局的开启或关闭CPU的中断。

  • 控制任意一个中断请求的开启和关闭。

  • 中断优先级控制。

  • 指定中断发生时将中断请求发送到那些CPU。

  • interrupt属性设定,设置每个”外部中断”的触发方式(边缘触发或者电平触发)。

2.2.2.2. CPU interface简介

CPU接口为链接到GIC的处理器提供接口,与Distributor类似它也提供了一些编程接口,我们可以通过CPU接口实现以下功能(列举几项,详细参考arm手册):

  • 打开或关闭 CPU interface 向连接的 CPU assert 中断事件。

  • 中断确认(acknowledging an interrupt)。

  • 中断处理完毕的通知。

  • 为处理器设置中断优先级掩码。

  • 设置处理器的抢占策略

  • 确定挂起的中断请求中优先级最高的中断请求

简单来说,CPU接口可以开启或关闭发往CPU的中断请求,CPU中断开启后只有优先级高于“中断优先级掩码”的中断请求才能被发送到CPU。 在任何时候CPU都可以从(CPU接口寄存器)读取当前活动的最高优先级。

2.2.2.3. Redistributor简介

GICv3中,Redistributor管理SGI,PPI,LPI中断,然后将中断发送给CPU interface,包括下面功能:

  • 启用和禁用 SGI 和 PPI。

  • 设置 SGI 和 PPI 的优先级。

  • 将每个 PPI 设置为电平触发或边缘触发。

  • 将每个 SGI 和 PPI 分配给中断组。

  • 控制 SGI 和 PPI 的状态。

  • 内存中数据结构的基址控制,支持 LPI 的相关中断属性和挂起状态。

  • 电源管理支持。

2.2.2.4. ITS(Interrupt translation service)

ITS 是GIC v3架构中的一种可选硬件机制,ITS提供了一种将基于消息的中断转换为LPI的软件机制,它是 在支持LPI的配置中可选地支持。ITS接收LPI中断,进行解析,然后发送到对应的redistributor,再由redistributor将中断信息,发送给cpu interface。

2.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产生合适的中断异常给处理器,处理器接收该异常,最后软件处理该中断。

2.2.4. GIC-600简单介绍

rk3568/rk3588的中断控制器是GIC-600,rk3562/rk3576的中断控制器是GIC-400,我们这里简单介绍GIC-600中断控制器,GIC-600是支持GIC v3版本,是arm一个实际的控制器设计参考。 rk3568的GIC流程框图如下:

rk3568的GIC流程框图

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

rk3568的中断分配

2.3. 中断驱动简单分析

2.3.1. 设备树中的中断信息

在Linux系统中已经替我们封装好了大部分的工作内容,我们需要做的只是简单地在这个框架下使用。 下面将以rk3568的中断为例讲解,简单讲解下中断控制的初始化。

gic节点(位于内核源码/arch/arm64/boot/dts/rockchip/rk3568.dtsi)
 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>;
    };
};
  • 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为例:

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

中断触发方式设置(位于内核源码/include/dt-bindings/interrupt-controller/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,否则屏蔽。

如下示例:

timer节点(位于内核源码/arch/arm64/boot/dts/rockchip/rk3568.dtsi)
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;
    };

2.3.2. GIC v3 中断控制器的代码

中断控制器通过IRQCHIP_DECLARE 宏注册到__irqchip_of_table

内核源码/drivers/irqchip/irq-gic-v3.c
1
2
//初始化一个struct of_device_id的静态常量,并放置在__irqchip_of_table中
IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gicv3_of_init);

系统开机初始化阶段,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_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事件详细处理过程可自行参考内核源码。

2.4. 中断常用的API和重要的数据结构

2.4.1. 中断申请和释放函数

2.4.1.1. request_irq函数

request_irq是Linux内核中断子系统对外提供的最核心、最常用的标准API函数,专门给外设驱动程序使用,是驱动和中断硬件“打通连接”的关键入口。

函数原型:

1
2
static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler,
                                            unsigned long flags, const char *name, void *dev);

参数说明:

  • irq:用于指定“内核中断号”,这个参数我们会从设备树中获取或转换得到。在内核空间中它代表一个唯一的中断编号。

  • handler:用于指定中断处理函数,中断发生后跳转到该函数去执行。

  • flags:中断触发条件,也就是我们常说的上升沿触发、下降沿触发等等触发方式通过“|”进行组合(注意,这里的设置会覆盖设备树中的默认设置),宏定义如下所示:

内核源码/include/linux/interrupt.h
 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

  • 失败:返回负数。

2.4.1.2. free_irq函数

free_irq函数是request_irq的配对函数,是内核中断子系统提供的释放中断资源的标准API,驱动卸载时必须调用,缺一不可。

释放中断函数
1
void free_irq(unsigned int irq, void *dev);

参数说明:

  • irq:从设备树中得到或者转换得到的中断编号。

  • dev:与request_irq函数中dev传入的参数一致。

2.4.1.3. devm_request_irq函数

此函数与request_irq()的区别是devm_开头的API申请的是内核“managed”的资源,一般 不需要 在出错处理和remove()接口里再显式的释放。

函数原型:

1
2
int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
                        unsigned long irqflags, const char *devname, void *dev_id);

2.4.2. 中断号获取函数

2.4.2.1. gpiod_to_irq函数

gpiod_to_irq函数将GPIO描述符转换为内核对应的中断号,是GPIO驱动申请中断的前置步骤。

函数原型:

1
int gpiod_to_irq(struct gpio_desc *desc);

参数说明:

  • desc:通过devm_gpiod_get获取的GPIO描述符指针。

返回值:正整数:有效的中断号;负值:转换失败,返回错误码。

2.4.3. 中断处理函数

在中断申请时需要指定一个中断处理函数,书写格式如下所示。

中断服务函数格式
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。

2.4.4. 中断的使能和屏蔽函数

2.4.4.1. enable_irq和disable_irq函数

通过函数使能、禁用某一个中断。

中断的屏蔽和使能
1
2
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)

参数

  • irq:指定的“内核中断号”

返回值

2.4.4.2. local_irq_xxx函数

local_irq_xxx函数关闭或打开当前CPU的所有中断,只影响当前正在运行的CPU,不会关闭其他CPU的中断。

屏蔽或者恢复本CPU内的所有中断
1
2
3
4
void local_irq_enable(void)
void local_irq_disable(void)
void local_irq_save(unsigned long flags)
void local_irq_restore(unsigned long flags)

由于“全局中断”的特殊性,通常情况下关闭之前要使用local_irq_save保存当前中断状态, 开启之后使用local_irq_restore宏恢复关闭之前的状态。flags是一个unsigned long类型的数据。

在armv8-arch64架构下,local_irq_disable()只是操作daif标志位,关闭当前cpu的异常,与具体中断控制器无关,而disable_irq()是通过控制中断控制器实现关闭中断。

2.5. Linux中断子系统实验

本实验在Pinctrl子系统和GPIO子系统读取引脚电平实验基础上进行修改, 彻底摒弃此前低效的定时器轮询模式,依托Linux中断子系统实现GPIO按键事件检测, 采用边沿触发中断机制搭配定时器软件消抖方案,解决机械按键硬件抖动而误触发问题, 实现按键按下立即响应、无操作时CPU零损耗的高效驱动设计。

本章的示例代码目录为: linux_driver/18_irq_subsystem

2.5.1. 设备树插件详解

设备树插件完整代码如下:

设备树插件(位于linux_driver/18_irq_subsystem/lubancat-led-overlay.dts)
 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
 /dts-v1/;
 /plugin/;

 #include <dt-bindings/gpio/gpio.h>
 #include <dt-bindings/pinctrl/rockchip.h>
 #include <dt-bindings/interrupt-controller/irq.h>

 / {
     fragment@0 {
         target-path = "/";

         __overlay__ {
             led_test: led_test{
                 compatible = "fire,led_test";
                 led-gpios = <&gpio0 RK_PC7 GPIO_ACTIVE_HIGH>;
                 button-gpios= <&gpio1 RK_PB2 GPIO_ACTIVE_HIGH>;
                 interrupt-parent = <&gpio1>;
                 interrupts = <RK_PB2 IRQ_TYPE_EDGE_FALLING>;
                 pinctrl-names = "default";
                 pinctrl-0 = <&led_button_pin>;
             };
         };
     };

     fragment@1 {
         target = <&pinctrl>;

         __overlay__ {
             led_button_test {
                 led_button_pin: led_button_pin {
                     rockchip,pins =
                         <0 RK_PC7 RK_FUNC_GPIO &pcfg_pull_none>,
                         <1 RK_PB2 RK_FUNC_GPIO &pcfg_pull_up>;
                 };
             };
         };
     };

     fragment@2 {
         target = <&leds>;

         __overlay__ {
             status = "disabled";
         };
     };
 };
  • 第17行:指定中断控制器,按键引脚属于GPIO1组,故绑定GPIO1中断控制器;

  • 第18行:第一参数为中断引脚索引,第二参数为下降沿触发(按键按下时触发,匹配上拉配置)。

2.5.2. 驱动代码详解

核心数据结构定义

核心数据结构定义(位于linux_driver/18_irq_subsystem/irq_subsystem.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 消抖时间 20ms */
#define DEBOUNCE_TIME 20

/* 定义 LED 字符设备结构体 */
struct led_chrdev {
    /* 字符设备结构体 */
    struct cdev dev;
    /* 自旋锁 */
    spinlock_t spinlock;
    /* LED 状态 */
    int led_state;
    /* LED 的 GPIO 描述符 */
    struct gpio_desc *led_gpio;
    /* 按钮的 GPIO 描述符 */
    struct gpio_desc *button_gpio;
    /* 消抖定时器 */
    struct timer_list debounce_timer;
    /* 中断号 */
    int irq;
};

/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;
  • 定义20ms消抖时间,避免误触发;

  • 自定义结构体整合字符设备、GPIO资源、自旋锁与消抖定时器、中断号,统一管理硬件与业务数据,消抖定时器专门用于中断底半部处理,实现按键电平稳定检测。

驱动初始化

驱动初始化(位于linux_driver/18_irq_subsystem/irq_subsystem.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
static int pdrv_led_probe(struct platform_device *pdev)
{
    // 内存分配(省略重复代码)

    /* 第一步:提取平台设备提供的资源 */
    if (pdev->dev.of_node) {
        /* 获取 LED 的 GPIO 描述符,GPIOD_OUT_HIGH 表示设置为输出模式 + 输出高电平 */
        led_cdev->led_gpio = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_HIGH);
        if (IS_ERR(led_cdev->led_gpio)) {
            ret = PTR_ERR(led_cdev->led_gpio);
            printk(KERN_ERR "Failed to get LED GPIO: %d\n", ret);
            return ret;
        }

        /* 获取按钮的 GPIO 描述符,GPIOD_IN 表示设置为输入模式 */
        led_cdev->button_gpio = devm_gpiod_get(&pdev->dev, "button", GPIOD_IN);
        if (IS_ERR(led_cdev->button_gpio)) {
            ret = PTR_ERR(led_cdev->button_gpio);
            printk(KERN_ERR "Failed to get button GPIO: %d\n", ret);
            return ret;
        }

    } else {
        printk("Platform device matching is not supported in this driver\n");
        return -ENOMEM;
    }

    //字符设备注册(省略重复代码)

    /* 初始化自旋锁 */
    spin_lock_init(&led_cdev->spinlock);

    /* 初始化消抖定时器 */
    timer_setup(&led_cdev->debounce_timer, button_debounce_callback, 0);

    /* 默认关闭 LED */
    led_cdev->led_state = 0;

    /* 获取按键GPIO对应的中断号 */
    led_cdev->irq = gpiod_to_irq(led_cdev->button_gpio);
    if (led_cdev->irq < 0) {
        ret = led_cdev->irq;
        /* 打印获取中断号失败 */
        printk(KERN_ERR "Failed to get button IRQ: %d\n", ret);
        /* 跳转到错误处理标签 */
        goto device_err;
    }

    /* 申请中断,下降沿触发 */
    ret = devm_request_irq(&pdev->dev, led_cdev->irq, button_irq_handler,
                    IRQF_TRIGGER_FALLING,
                    "button_irq", led_cdev);
    if (ret < 0) {
        /* 打印申请中断失败 */
        printk(KERN_ERR "Failed to request IRQ: %d\n", ret);
        /* 跳转到错误处理标签 */
        goto device_err;
    }

    return 0;

  // 错误处理(省略重复代码)
}
  • 第34行:初始化消抖定时器,回调函数为button_debounce_callback;

  • 第40行:通过gpiod_to_irq将GPIO描述符转换为内核中断号

  • 第50-52行:通过devm_request_irq申请中断,指定触发方式、中断服务函数与设备私有数据,采用devm托管式API,驱动卸载时自动释放中断资源,避免内存泄漏。

中断顶半部:中断服务函数

中断服务函数(位于linux_driver/18_irq_subsystem/irq_subsystem.c)
1
2
3
4
5
6
7
8
9
 static irqreturn_t button_irq_handler(int irq, void *dev_id)
 {
     struct led_chrdev *led_cdev = (struct led_chrdev *)dev_id;

     /* 重启消抖定时器,20ms后执行回调 */
     mod_timer(&led_cdev->debounce_timer, jiffies + msecs_to_jiffies(DEBOUNCE_TIME));

     return IRQ_HANDLED;
 }

该函数属于中断顶半部,运行在中断上下文,严禁执行耗时操作、打印、休眠操作, 仅负责快速启动消抖定时器,将实际业务处理交给底半部,保证中断响应实时性,不阻塞其他中断执行。

中断底半部:消抖定时器回调函数

消抖定时器回调函数(位于linux_driver/18_irq_subsystem/irq_subsystem.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
static void button_debounce_callback(struct timer_list *t)
{
    /* 反向找到包含这个定时器的设备结构体 led_chrdev 指针 */
    struct led_chrdev *led_cdev = from_timer(led_cdev, t, debounce_timer);

    /* 用于保存中断状态信息 */
    unsigned long flags;
    /* 存储按键状态 */
    int btn_val;

    /* 获取自旋锁并保存中断状态 */
    spin_lock_irqsave(&led_cdev->spinlock, flags);

    /* 读取稳定后的按键电平 */
    btn_val = gpiod_get_value(led_cdev->button_gpio);

    /* 低电平表示按键稳定按下 */
    if(btn_val == 0){
        /* 翻转LED电平状态 */
        gpiod_set_value(led_cdev->led_gpio, !gpiod_get_value(led_cdev->led_gpio));
    }

    /* 释放自旋锁并恢复中断状态 */
    spin_unlock_irqrestore(&led_cdev->spinlock, flags);

    /* 按键按下打印日志 */
    if(btn_val == 0){
        printk(KERN_INFO "按键已按下,LED状态翻转\n");
    }
}

该函数属于中断底半部,允许执行耗时操作,核心作用是等待20ms抖动结束后,再次读取按键电平,确认是有效按下后再翻转LED,解决硬件抖动问题。

2.5.3. 编译设备树和驱动

此部分和Pinctrl子系统和GPIO子系统读取引脚电平实验完全一致不作过多说明。

编译得到设备树插件lubancat-led-overlay.dtb、驱动模块irq_subsystem.ko。

2.5.4. 程序运行结果

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

2.5.4.1. 实验操作

设备树插件加载方法和设备树插件实验完全一致,加载设备树插件并重启板卡后会发现系统心跳灯默认没有闪烁, 是因为我们使用设备树插件关闭了leds节点,释放了引脚。

使用以下命令加载驱动:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#加载驱动
sudo insmod irq_subsystem.ko

#信息输出如下
[   70.570580] led platform driver init
[   70.571410] led platform driver probe
[   70.571607] major=236, minor=0

# 查看中断号
cat /proc/interrupts | grep button_irq

#信息打印如下
105:        349          0          0          0     gpio1  10 Edge      button_irq

#信息说明如下
#105:内核中断号
#349:按键触发总次数,中断处理函数执行了349次
#gpio1 10:硬件引脚,按键接在 GPIO1_B2
#Edge:触发方式,IRQF_TRIGGER_RISING/FALLING
#button_irq:中断名字

使用杜邦线一端连接按键引脚,另一端多次连接和断开连接GND引脚模拟按键按下和松开,信息打印如下:

1
2
3
4
[   83.795412] 按键已按下,LED状态翻转
[   84.868826] 按键已按下,LED状态翻转
[   86.078850] 按键已按下,LED状态翻转
[   87.078942] 按键已按下,LED状态翻转

除了内核打印信息,也可以看到每一次连接GND引脚模拟按键按下都可以看到LED从亮转灭或从灭转亮, 说明中断触发正常;没有多次打印按键按下和LED频繁亮灭,说明定时器消抖功能正常。

2.5.5. 实验注意事项

  1. 中断触发方式必须与设备树一致:驱动中IRQF_TRIGGER_FALLING需与设备树IRQ_TYPE_EDGE_FALLING完全匹配,否则中断无法触发;

  2. 中断上下文严禁耗时操作:中断服务函数内禁止copy_to/from_user、休眠、延时操作,仅能做快速标记;

  3. 按键引脚必须配置上下拉:禁止悬空,否则电平不稳定,会误触发中断,本实验采用内置上拉,必须配置;

  4. 消抖时间合理设置:建议10-20ms,过短无法消抖,过长响应迟钝;

  5. 优先使用devm托管API:devm_request_irq、devm_gpiod_get等API,自动释放资源,避免内存泄漏;

  6. 中断号不可硬编码:必须通过gpiod_to_irq动态获取,禁止手动指定中断号,保证驱动兼容性。