0


深入解析GIC中断处理与内核初始化:基于Linux 4.9.88内核的详细分析

往期内容

本专栏往期内容,interrtupr子系统:

  1. 深入解析Linux内核中断管理:从IRQ描述符到irq domain的设计与实现
  2. Linux内核中IRQ Domain的结构、操作及映射机制详解
  3. 中断描述符irq_desc成员详解
  4. Linux 内核中断描述符 (irq_desc) 的初始化与动态分配机制详解
  5. 中断的硬件框架
  6. GIC介绍
  7. GIC寄存器介绍
  8. ARM架构中断与异常向量表机制解析

pinctrl和gpio子系统专栏:

  1. 专栏地址:pinctrl和gpio子系统
  2. 编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用– 末片,有专栏内容观看顺序

input子系统专栏:

  1. 专栏地址:input子系统
  2. input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序 – 末片,有专栏内容观看顺序

I2C子系统专栏:

  1. 专栏地址:IIC子系统
  2. 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客 – 末篇,有专栏内容观看顺序

总线和设备树专栏:

  1. 专栏地址:总线和设备树
  2. 设备树与 Linux 内核设备驱动模型的整合-CSDN博客 – 末篇,有专栏内容观看顺序

img

目录

前言

建议先看一下往期文章关于中断相关结构体的讲解:中断描述符irq_desc成员详解-CSDN博客

  • Linux 4.9.88内核源码- Linux-4.9.88\drivers\irqchip\irq-gic.c- Linux-4.9.88/arch/arm/boot/dts/imx6ull.dtsi

本文以Linux 4.9.88内核为基础,系统解析了通用中断控制器(GIC)的中断处理与初始化机制。介绍了一级和多级中断控制器的处理流程,包括中断号映射(hwirq与virq)和中断服务函数的调用。梳理了GIC驱动的关键结构体与核心函数,阐述了GIC的功能及其内核表示方法。以设备树为线索,详细剖析了GIC初始化过程,解读了

IRQCHIP_DECLARE

宏及其如何与设备树节点匹配并触发初始化。通过源码分析与注释,逐步讲解了GIC的域操作和分层中断处理流程。

1.GIC中断处理流程

1.1 一级中断控制器处理流程

对于irq_desc,内核有两种分配方法:

  • 一次分配完所有的irq_desc
  • 按需分配(用到某个中断才分配它的irq_desc

现在的内核基本使用第1种方法。

img

  • 假设GIC可以向CPU发出161019号中断,这些数字被称为hwirq。015用于Process之间通信,比较特殊。
  • 假设要使用UART模块,它发出的中断连接到GIC的32号中断,分配的irq_desc序号为16
  • 在GIC domain中会记录(32, 16)
  • 那么注册中断时就是:request_irq(16, ...) //虚拟中断号or软件中断号
  • 发生UART中断时- 程序从GIC中读取寄存器知道发生了32号中断,通过GIC irq_domain可以知道virq为16- 调用irq_desc[16]中的handleA函数(GIC提供的handle函数),它的作用是屏蔽中断、调用action链表中用户注册的函数handler和thread_fn、清除中断

1.2 多级中断控制器处理流程

img

  • 假设GPIO模块下有4个引脚,都可以产生中断,都连接到GIC的33号中断
  • GPIO也可以看作一个中断控制器,对于它的4个中断
  • 对于GPIO模块中0~3这四个hwirq,一般都会一下子分配四个irq_desc
  • 假设这4个irq_desc的序号为100~103,在GPIO domain中记录(0,100) (1,101)(2,102) (3,103)
  • 对于KEY,注册中断时就是:request_irq(102, ...)
  • 按下KEY时:- 程序从GIC中读取寄存器知道发生了33号中断,通过GIC irq_domain可以知道virq为17- 调用irq_desc[17]中的handleB函数- mask GIC33号中断- handleB读取GPIO寄存器,确定是GPIO里2号引脚发生中断- 通过GPIO irq_domain可以知道virq为102- 调用irq_desc[102]中的handleA函数,它的作用是mask GPIO2号中断、调用action链表中用户注册的函数、clear GPIO2号中断- clear GIC33 号中断

2.GIC中的重要函数和结构体

沿着中断的处理流程,GIC涉及这4个重要部分:

  • CPU从异常向量表中调用handle_arch_irq,这个函数指针是有GIC驱动设置的- GIC才知道怎么判断发生的是哪个GIC中断
  • 从GIC获得hwirq后,要转换为virq:需要有GIC Domain
  • 调用irq_desc[virq].handle_irq函数:这也应该由GIC驱动提供
  • 处理中断时,要屏蔽中断、清除中断等:这些函数保存在irq_chip里,由GIC驱动提供

从硬件上看,GIC的功能是什么?

  • 可以使能、屏蔽中断
  • 发生中断时,可以从GIC里判断是哪个中断

在内核里,使用gic_chip_data结构体表示GIC,gic_chip_data里有什么?

  • irq_chip:中断使能、屏蔽、清除,放在irq_chip中的各个函数里实现

在这里插入图片描述

  • irq_domain - 申请中断时- 在设备树里指定hwirq、flag,可以使用irq_domain的函数来解析设备树- 根据hwirq可以分配virq,把(hwirq, virq)存入irq_domain中- 发生中断时,从GIC读出hwirq,可以通过irq_domain找到virq,从而找到处理函数

所以,GIC用gic_chip_data来表示,gic_chip_data中重要的成员是:irq_chip、irq_domain。

3.GIC初始化过程

Linux-4.9.88\drivers\irqchip\irq-gic.c

📎irq-gic.c

(将dtb文件反汇编为dts后,在里面搜索Interrupt-contorller找到中断控制器的节点,复制其compatible属性,在内核源码中grep该属性搜索一下就可以找到该文件)

3.1 内核支持多种GIC

按照设备树的套路:

  • 驱动程序注册platform_driver
  • 它的of_match_table里有多个of_device_id,表示能支持多个设备
  • 有多种版本的GIC,在内核为每一类GIC定义一个结构体of_device_id,并放在一个段里:
// drivers\irqchip\irq-gic.cIRQCHIP_DECLARE(gic_400,"arm,gic-400", gic_of_init);IRQCHIP_DECLARE(arm11mp_gic,"arm,arm11mp-gic", gic_of_init);IRQCHIP_DECLARE(arm1176jzf_dc_gic,"arm,arm1176jzf-devchip-gic", gic_of_init);IRQCHIP_DECLARE(cortex_a15_gic,"arm,cortex-a15-gic", gic_of_init);IRQCHIP_DECLARE(cortex_a9_gic,"arm,cortex-a9-gic", gic_of_init);IRQCHIP_DECLARE(cortex_a7_gic,"arm,cortex-a7-gic", gic_of_init);IRQCHIP_DECLARE(msm_8660_qgic,"qcom,msm-8660-qgic", gic_of_init);IRQCHIP_DECLARE(msm_qgic2,"qcom,msm-qgic2", gic_of_init);IRQCHIP_DECLARE(pl390,"arm,pl390", gic_of_init);

把宏

IRQCHIP_DECLARE

展开:

// include\linux\irqchip.h#defineIRQCHIP_DECLARE(name, compat, fn)OF_DECLARE_2(irqchip, name, compat, fn)#defineOF_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)\staticconststructof_device_id __of_table_##name        \__used __section(__irqchip_of_table)\={.compatible = compat,\.data =(fn ==(fn_type)NULL)? fn : fn  }

展开示例:

IRQCHIP_DECLARE(cortex_a7_gic,"arm,cortex-a7-gic", gic_of_init);
展开后得到:
staticconststructof_device_id __of_table_cortex_a7_gic        \
    __used __section(__irqchip_of_table)            \
     ={.compatible ="arm,cortex-a7-gic",                \
         .data = gic_of_init  }

3.2 初始化调用

// drivers\irqchip\irq-gic.cIRQCHIP_DECLARE(gic_400,"arm,gic-400", gic_of_init);IRQCHIP_DECLARE(arm11mp_gic,"arm,arm11mp-gic", gic_of_init);IRQCHIP_DECLARE(arm1176jzf_dc_gic,"arm,arm1176jzf-devchip-gic", gic_of_init);IRQCHIP_DECLARE(cortex_a15_gic,"arm,cortex-a15-gic", gic_of_init);IRQCHIP_DECLARE(cortex_a9_gic,"arm,cortex-a9-gic", gic_of_init);IRQCHIP_DECLARE(cortex_a7_gic,"arm,cortex-a7-gic", gic_of_init);IRQCHIP_DECLARE(msm_8660_qgic,"qcom,msm-8660-qgic", gic_of_init);IRQCHIP_DECLARE(msm_qgic2,"qcom,msm-qgic2", gic_of_init);IRQCHIP_DECLARE(pl390,"arm,pl390", gic_of_init);

像5.3.1中上述的宏,表示支持的GIC的种类,比如要使用IRQCHIP_DECLARE(cortex_a7_gic, “arm,cortex-a7-gic”, gic_of_init);,那它是如何去选择和初始化的?如下:

start_kernel (init\main.c)
    init_IRQ (arch\arm\kernel\irq.c)
        irqchip_init (drivers\irqchip\irqchip.c)
            of_irq_init (drivers\of\irq.c)
                desc->irq_init_cb = match->data;

                ret = desc->irq_init_cb(desc->dev,
                            desc->interrupt_parent);

如何知道of_irq_init在哪里被调用的:
在这里插入图片描述

选择某一GIC和进行初始化:

img

\Linux-4.9.88\drivers\irqchip\irq-gic.c
void __init of_irq_init(conststructof_device_id*matches){conststructof_device_id*match;// 用于存放匹配的设备节点的指针structdevice_node*np,*parent =NULL;// np 指向当前中断控制器节点,parent 指向父节点structof_intc_desc*desc,*temp_desc;// 中断控制器描述符指针,用于保存控制器相关信息structlist_head intc_desc_list, intc_parent_list;// 初始化中断控制器列表和父节点列表INIT_LIST_HEAD(&intc_desc_list);// 初始化描述符列表,存储扫描到的中断控制器INIT_LIST_HEAD(&intc_parent_list);// 初始化父节点列表,存储待初始化的父节点// 1. 遍历设备树中与 `matches` 匹配的节点for_each_matching_node_and_match(np, matches,&match){// 检查节点是否有 "interrupt-controller" 属性且可用if(!of_find_property(np,"interrupt-controller",NULL)||!of_device_is_available(np))continue;// 如果匹配项的 `data` 字段为空,发出警告并跳过if(WARN(!match->data,"of_irq_init: no init function for %s\n", 
                 match->compatible))continue;// 2. 为每个匹配的中断控制器分配并填充一个 `of_intc_desc` 结构体
        desc =kzalloc(sizeof(*desc), GFP_KERNEL);if(WARN_ON(!desc)){of_node_put(np);goto err;}

        desc->irq_init_cb = match->data;// 初始化回调函数
        desc->dev =of_node_get(np);// 保存节点信息
        desc->interrupt_parent =of_irq_find_parent(np);// 查找中断父节点if(desc->interrupt_parent == np)// 若节点为根中断控制器,设置为 NULL
            desc->interrupt_parent =NULL;list_add_tail(&desc->list,&intc_desc_list);// 添加到中断控制器描述符列表}/*
     * 3. 逐层初始化中断控制器,优先初始化根节点(即无父节点的控制器)
     */while(!list_empty(&intc_desc_list)){list_for_each_entry_safe(desc, temp_desc,&intc_desc_list, list){int ret;// 跳过非当前父节点的子节点if(desc->interrupt_parent != parent)continue;list_del(&desc->list);// 从描述符列表中移除当前描述符of_node_set_flag(desc->dev, OF_POPULATED);// 标记节点已初始化pr_debug("of_irq_init: init %s (%p), parent %p\n",
                     desc->dev->full_name,
                     desc->dev, desc->interrupt_parent);// 调用中断控制器的初始化回调函数
            ret = desc->irq_init_cb(desc->dev,
                                    desc->interrupt_parent);if(ret){of_node_clear_flag(desc->dev, OF_POPULATED);// 若初始化失败,清除标记kfree(desc);continue;}/*
             * 初始化成功,将此节点加入到父节点列表中,
             * 以便后续初始化其子节点
             */list_add_tail(&desc->list,&intc_parent_list);}// 获取下一个待处理的父节点
        desc =list_first_entry_or_null(&intc_parent_list,typeof(*desc), list);if(!desc){pr_err("of_irq_init: children remain, but no parents\n");break;}list_del(&desc->list);
        parent = desc->dev;kfree(desc);}// 释放未使用的描述符节点list_for_each_entry_safe(desc, temp_desc,&intc_parent_list, list){list_del(&desc->list);kfree(desc);}
err:// 清理和释放资源,释放未初始化的描述符节点list_for_each_entry_safe(desc, temp_desc,&intc_desc_list, list){list_del(&desc->list);of_node_put(desc->dev);kfree(desc);}}
  1. 匹配中断控制器节点:遍历设备树中所有符合 matches 条件的节点,通过检查节点是否有 interrupt-controller 属性,筛选出所有中断控制器节点,并分配 of_intc_desc 结构体保存节点相关信息(包括初始化回调函数、节点指针、父节点指针等)。
  2. 初始化中断控制器:从根中断控制器(无父节点)开始,每次处理当前父节点的所有子节点,并递归初始化,确保初始化顺序由根至叶子。
  3. 清理和释放:初始化完成后,释放未使用的描述符,确保资源不被浪费。
IRQCHIP_DECLARE

宏如何完成与设备树的挂钩:

IRQCHIP_DECLARE(cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init);

的作用是声明并注册一个中断控制器,这样在调用

of_irq_init

时能够识别设备树中的

"arm,cortex-a9-gic"

中断控制器节点。具体过程如下:

  1. IRQCHIP_DECLARE 定义了一个 of_device_id 类型的结构体实例,其中包含 "arm,cortex-a9-gic" 兼容字符串与 gic_of_init 初始化函数的关联。
  2. of_irq_init 遍历设备树时,识别出符合 "arm,cortex-a9-gic" 的节点,匹配后调用 gic_of_init 进行中断控制器初始化,从而与设备树节点挂钩。

3.3 在设备树里指定GIC

在设备树中指定GIC,内核驱动程序根据设备树来选择、初始化GIC。

drivers\irqchip\irqchip.c

中并没有定义一个platform_driver,但是套路是一样的。

img

调用过程:img

of_irq_init:

  • 内核有一个__irqchip_of_table数组,里面有多个of_device_id,表示多种GIC
  • 要使用哪类GIC?在设备树里指定
  • 根据设备树,找到__irqchip_of_table树组中对应的项,调用它的初始化函数- IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init);

3.4 gic_of_init分析

请添加图片描述

先记住上面的中断处理流程,再来分析gic_of_init:

申请中断的过程(irq_domain_ops域操作结构体):

static const struct irq_domain_ops gic_irq_domain_hierarchy_ops = {

​ .translate = gic_irq_domain_translate,

​ .alloc = gic_irq_domain_alloc,

​ .free = irq_domain_free_irqs_top,

};

  • 在设备树中声明要使用哪个GIC控制器下的哪个中断
  • 用gic_irq_domain_translate来解析设备树,知道其hwirq以及flag
  • 为hwirq在irq_desc数组中找到空闲的位置(数组下标就是其virq)
  • 记录hwirq和virq在domain中(建立联系)
  • 使用gic_irq_domain_alloc处理,提供handle中断函数等

img

``📎irq-gic.c📎entry-armv.S

关键词:gic_of_setup、__gic_init_bases、gic_init_chip、gic_init_bases、irq_domain_create_linear

int __init
gic_of_init(structdevice_node*node,structdevice_node*parent){structgic_chip_data*gic;// 用于存储当前GIC的配置信息int irq, ret;// 检查设备树节点的有效性,如果为空则返回错误码 -ENODEVif(WARN_ON(!node))return-ENODEV;// 检查当前GIC控制器数量是否已达到最大允许数量,若超限则返回错误码 -EINVALif(WARN_ON(gic_cnt >= CONFIG_ARM_GIC_MAX_NR))return-EINVAL;// 获取用于当前GIC的数据结构,并使用 gic_data 数组存储多实例的 GIC
    gic =&gic_data[gic_cnt];// 调用 gic_of_setup 函数根据设备树节点 `node` 初始化 GIC 的基本信息
    ret =gic_of_setup(gic, node);if(ret)return ret;/*
     * 如果 hypervisor 模式(HYP)不可用或 CPU 接口寄存器太小,禁用分离的
     * End Of Interrupt/Deactivate (EOI/Deactivate) 模式。
     */if(gic_cnt ==0&&!gic_check_eoimode(node,&gic->raw_cpu_base))static_key_slow_dec(&supports_deactivate);// 初始化 GIC 的寄存器基地址、GIC 中断域等基本配置
    ret =__gic_init_bases(gic,-1,&node->fwnode);if(ret){// 若初始化失败,调用 gic_teardown 清理当前 GIC 配置gic_teardown(gic);return ret;}// 若是第一个 GIC 控制器,进行物理地址初始化并设置 KVM (Kernel-based Virtual Machine) 信息if(!gic_cnt){gic_init_physaddr(node);gic_of_setup_kvm_info(node);}// 若存在父节点,则解析并映射第一个中断号并进行级联中断配置if(parent){
        irq =irq_of_parse_and_map(node,0);// 解析并映射设备树中第一个中断gic_cascade_irq(gic_cnt, irq);// 配置级联中断}// 如果启用了 GIC V2M(GIC Version 2 Memory Mapped)支持,初始化 GIC V2Mif(IS_ENABLED(CONFIG_ARM_GIC_V2M))gicv2m_init(&node->fwnode, gic_data[gic_cnt].domain);// 增加 GIC 控制器计数器,表明一个新的 GIC 已初始化完成
    gic_cnt++;return0;}

3.4.1

gic_of_setup
gic_of_init

函数中调用的,主要是初始化 GIC 的基本信息,和设备树挂钩。

img

img

reg中,第一行就是分发器Distributro所对应寄存器的地址,第二行则是cpu interface所对应寄存器的地址,既然是物理地址,那就得映射,就是由该函数实现的:

staticintgic_of_setup(structgic_chip_data*gic,structdevice_node*node){// 检查传入参数 gic 和 node 是否为空,以确保有效性if(!gic ||!node)return-EINVAL;// 如果为空,返回 -EINVAL 表示无效参数// 使用设备树节点 `node` 映射分发器(distributor)寄存器基地址
    gic->raw_dist_base =of_iomap(node,0);// 如果映射失败,输出错误信息,并跳转到 error 标签清理已分配资源if(WARN(!gic->raw_dist_base,"unable to map gic dist registers\n"))goto error;// 使用设备树节点 `node` 映射 CPU 接口(CPU interface)寄存器基地址
    gic->raw_cpu_base =of_iomap(node,1);// 如果映射失败,输出错误信息,并跳转到 error 标签清理已分配资源if(WARN(!gic->raw_cpu_base,"unable to map gic cpu registers\n"))goto error;// 读取设备树节点中的 `cpu-offset` 属性,用于定义 CPU 接口偏移量// 如果未定义 `cpu-offset` 属性,默认偏移量设为0if(of_property_read_u32(node,"cpu-offset",&gic->percpu_offset))
        gic->percpu_offset =0;return0;// 成功初始化时返回 0 表示无错误

error:// 如果映射失败,调用 gic_teardown 释放已分配的 GIC 资源gic_teardown(gic);return-ENOMEM;// 返回 -ENOMEM 表示内存分配失败}

3.4.2

__gic_init_bases

要目的是为 GIC(Generic Interrupt Controller)初始化基础数据结构和相关属性,为后续中断处理和分发提供支持。它包含了对第一个 GIC 实例的特殊处理(主要是主 GIC 的初始化),并设置了对 SMP(对称多处理)系统的支持。

staticint __init __gic_init_bases(structgic_chip_data*gic,int irq_start,structfwnode_handle*handle){char*name;int i, ret;// 检查传入的 gic 是否为空或已经初始化,若是则返回 -EINVAL 表示无效参数if(WARN_ON(!gic || gic->domain))return-EINVAL;// 检查当前是否为主 GIC,即 gic_data[0]if(gic ==&gic_data[0]){/*
         * 初始化 CPU 接口映射,将其默认设置为所有 CPU。
         * 每个 CPU 在探测其 ID 时将此映射进一步细化。
         * 仅主 GIC 需要该操作。
         */for(i =0; i < NR_GIC_CPU_IF; i++)
            gic_cpu_map[i]=0xff;// 0xff 表示所有 CPU#ifdefCONFIG_SMP// 设置跨 CPU 的中断调用函数set_smp_cross_call(gic_raise_softirq);#endif// 设置 CPU 热插拔的回调状态,用于在 CPU 启动时初始化 GICcpuhp_setup_state_nocalls(CPUHP_AP_IRQ_GIC_STARTING,"AP_IRQ_GIC_STARTING",
                      gic_starting_cpu,NULL);// 注册 GIC 的中断处理函数set_handle_irq(gic_handle_irq);// 如果支持 EOI/Deactivate 模式,输出提示信息if(static_key_true(&supports_deactivate))pr_info("GIC: Using split EOI/Deactivate mode\n");}// 初始化 GIC 芯片名称和类型if(static_key_true(&supports_deactivate)&& gic ==&gic_data[0]){// 主 GIC 使用分离的 EOI/Deactivate 模式,名称设置为 "GICv2"
        name =kasprintf(GFP_KERNEL,"GICv2");gic_init_chip(gic,NULL, name, true);// EOI/Deactivate 为 true}else{// 其他 GIC 使用默认名称格式 "GIC-x",x 是 GIC 索引
        name =kasprintf(GFP_KERNEL,"GIC-%d",(int)(gic -&gic_data[0]));gic_init_chip(gic,NULL, name, false);// EOI/Deactivate 为 false}// 调用 gic_init_bases 初始化 GIC 基础结构
    ret =gic_init_bases(gic, irq_start, handle);if(ret)kfree(name);// 若初始化失败,释放为名称分配的内存return ret;// 返回初始化结果}
set_handle_irq

img

gic_init_chip

img

gic_init_bases

请添加图片描述


本文转载自: https://blog.csdn.net/caiji0169/article/details/143809794
版权归原作者 憧憬一下 所有, 如有侵权,请联系我们删除。

“深入解析GIC中断处理与内核初始化:基于Linux 4.9.88内核的详细分析”的评论:

还没有评论