中断相关驱动详解

1. 中断的硬件框架

1.1 中断路径上的3个部件

  • 中断源

    • 中断源多种多样,比如GPIO、定时器、UART、DMA等等。 它们都有自己的寄存器,可以进行相关设置:使能中断、中断状态、中断类型等等。
  • 中断控制器

    • 各种中断源发出的中断信号,汇聚到中断控制器。 可以在中断控制器中设置各个中断的优先级。 中断控制器会向CPU发出中断信号,CPU可以读取中断控制器的寄存器,判断当前处理的是哪个中断。 中断控制器有多种实现,比如:

      • STM32F103中被称为NVIC:Nested vectored interrupt controller(嵌套向量中断控制器)

      • ARM9中一般是芯片厂家自己实现的,没有统一标准

      • Cortex A7中使用GIC(Generic Interrupt Controller)

  • CPU

    • CPU每执行完一条指令,都会判断一下是否有中断发生了。 CPU也有自己的寄存器,可以设置它来使能/禁止中断,这是中断处理的总开关。

1.2 STM32MP157的GPIO中断

STM32MP157的GPIO中断在硬件上的框架,跟STM32F103是类似的。 它们的中断控制器不一样,STM32MP157中使用的是GIC:

1.2.1 EXTI

对于STM32MP157,除了把GPIO引脚配置为输入功能外,GPIO控制器里没有中断相关的寄存器。

GPIO引脚可以向CPU发出中断信号,所有的GPIO引脚都可以吗? 不是的,需要在EXTI控制器中设置、选择。 GPIO引脚触发中断的方式是怎样的?高电平触发、低电平触发、上升沿触发、下降沿触发? 这需要进一步设置。 这些,都是在EXTI中配置,EXTI框图如下:

设置流程为沿着红线一路设置相关寄存器的值

设置EXTImux

选择哪些GPIO可以发出中断。 只有16个EXTI中断,从EXTI0~EXTI15;每个EXTIx中断只能从PAx、PBx、......中选择某个引脚,如下图所示:

注意:从上图可知,EXTI0只能从PA0、......中选择一个,这也意味着PA0、......中只有一个引脚可以用于中断。这跟其他芯片不一样,很多芯片的任一GPIO引脚都可以同时用于中断。

通过EXTI_EXTICR1等寄存器来设置EXTIx的中断源是哪个GPIO引脚。如下图所示:

其他寄存器,诸如:

设置Event Trigger:设置中断触发方式。

设置Masking:允许某个EXTI中断。

查看中断状态、清中断:

1.2.2 GIC

ARM体系结构定义了通用中断控制器(GIC),该控制器包括一组用于管理单核或多核系统中的中断的硬件资源。GIC提供了内存映射寄存器,可用于管理中断源和行为,以及(在多核系统中)用于将中断路由到各个CPU核。它使软件能够屏蔽,启用和禁用来自各个中断源的中断,以(在硬件中)对各个中断源进行优先级排序和生成软件触发中断。它还提供对TrustZone安全性扩展的支持。GIC接受系统级别中断的产生,并可以发信号通知给它所连接的每个内核,从而有可能导致IRQ或FIQ异常发生。

1.2.3 CPU

CPU的CPSR寄存器中有一位:I位,用来使能/禁止中断。

2. GIC介绍与编程

ARM体系结构定义了通用中断控制器(GIC),该控制器包括一组用于管理单核或多核系统中的中断的硬件资源。GIC提供了内存映射寄存器,可用于管理中断源和行为,以及(在多核系统中)用于将中断路由到各个CPU核。它使软件能够屏蔽,启用和禁用来自各个中断源的中断,以(在硬件中)对各个中断源进行优先级排序和生成软件触发中断。它还提供对TrustZone安全性扩展的支持。GIC接受系统级别中断的产生,并可以发信号通知给它所连接的每个内核,从而有可能导致IRQ或FIQ异常发生。

2.1 GIC两个主要功能模块(软件层面)

① 分发器(Distributor) 系统中的所有中断源都连接到该单元。可以通过仲裁单元的寄存器来控制各个中断源的属性,例如优先级、状态、安全性、路由信息和使能状态。 分发器把中断输出到"CPU接口单元",后者决定将哪个中断转发给CPU核。

② CPU接口单元(CPU Interface) CPU核通过控制器的CPU接口单元接收中断。CPU接口单元寄存器用于屏蔽,识别和控制转发到CPU核的中断的状态。系统中的每个CPU核心都有一个单独的CPU接口。 中断在软件中由一个称为中断ID的数字标识。中断ID唯一对应于一个中断源。软件可以使用中断ID来识别中断源并调用相应的处理程序来处理中断。呈现给软件的中断ID由系统设计确定,一般在SOC的数据手册有记录。

2.2 不同的类型的中断

① 软件触发中断(SGI,Software Generated Interrupt) 这是由软件通过写入专用仲裁单元的寄存器即软件触发中断寄存器(ICDSGIR)显式生成的。它最常用于CPU核间通信。SGI既可以发给所有的核,也可以发送给系统中选定的一组核心。中断号0-15保留用于SGI的中断号。用于通信的确切中断号由软件决定。

② 私有外设中断(PPI,Private Peripheral Interrupt) 这是由单个CPU核私有的外设生成的。PPI的中断号为16-31。它们标识CPU核私有的中断源,并且独立于另一个内核上的相同中断源,比如,每个核的计时器。

③ 共享外设中断(SPI,Shared Peripheral Interrupt) 这是由外设生成的,中断控制器可以将其路由到多个核。中断号为32-1020。SPI用于从整个系统可访问的各种外围设备发出中断信号。

中断可以是边沿触发的(在中断控制器检测到相关输入的上升沿时认为中断触发,并且一直保持到清除为止)或电平触发(仅在中断控制器的相关输入为高时触发)。

2.3 中断可以处于多种不同状态

① 非活动状态(Inactive)--这意味着该中断未触发。

② 挂起(Pending)--这意味着中断源已被触发,但正在等待CPU核处理。待处理的中断要通过转发到CPU接口单元,然后再由CPU接口单元转发到内核。

③ 活动(Active)--描述了一个已被内核接收并正在处理的中断。

④ 活动和挂起(Active and pending)--描述了一种情况,其中CPU核正在为中断服务,而GIC又收到来自同一源的中断。

中断的优先级和可接收中断的核都在分发器(distributor)中配置。外设发给分发器的中断将标记为pending状态(或Active and Pending状态,如触发时果状态是active)。distributor确定可以传递给CPU核的优先级最高的pending中断,并将其转发给内核的CPU interface。通过CPU interface,该中断又向CPU核发出信号,此时CPU核将触发FIQ或IRQ异常。

作为响应,CPU核执行异常处理程序。异常处理程序必须从CPU interface寄存器查询中断ID,并开始为中断源提供服务。完成后,处理程序必须写入CPU interface寄存器以报告处理结束。然后CPU interface准备转发distributor发给它的下一个中断。

在处理中断时,中断的状态开始为pending,active,结束时变成inactive。中断状态保存在distributor寄存器中。

2.4 GIC控制器

2.4.1 配置

GIC作为内存映射的外围设备,被软件访问。所有内核都可以访问公共的distributor单元,但是CPU interface是备份的,也就是说,每个CPU核都使用相同的地址来访问其专用CPU接口。一个CPU核不可能访问另一个CPU核的CPU接口。

Distributor拥有许多寄存器,可以通过它们配置各个中断的属性。这些可配置属性是:

  • 中断优先级:Distributor使用它来确定接下来将哪个中断转发到CPU接口。

  • 中断配置:这确定中断是对电平触发还是边沿触发。

  • 中断目标:这确定了可以将中断发给哪些CPU核。

  • 中断启用或禁用状态:只有Distributor中启用的那些中断变为挂起状态时,才有资格转发。

  • 中断安全性:确定将中断分配给Secure还是Normal world软件。

  • 中断状态。

    Distributor还提供优先级屏蔽,可防止低于某个优先级的中断发送给CPU核。 每个CPU核上的CPU interface,专注于控制和处理发送给该CPU核的中断。

2.4.2 初始化

Distributor和CPU interface在复位时均被禁用。复位后,必须初始化GIC,才能将中断传递给CPU核。 ​ 在Distributor中,软件必须配置优先级、目标核、安全性并启用单个中断;随后必须通过其控制寄存器使能。 ​ 对于每个CPU interface,软件必须对优先级和抢占设置进行编程。每个CPU接口模块本身必须通过其控制寄存器使能。 ​ 在CPU核可以处理中断之前,软件会通过在向量表中设置有效的中断向量并清除CPSR中的中断屏蔽位来让CPU核可以接收中断。 ​ 可以通过禁用Distributor单元来禁用系统中的整个中断机制;可以通过禁用单个CPU的CPU接口模块或者在CPSR中设置屏蔽位来禁止向单个CPU核的中断传递。也可以在Distributor中禁用(或启用)单个中断。 ​ 为了使某个中断可以触发CPU核,必须将各个中断,Distributor和CPU interface全部使能,并

将CPSR中断屏蔽位清零,如下图:

2.4.3 GIC中断处理

当CPU核接收到中断时,它会跳转到中断向量表执行。 ​ 顶层中断处理程序读取CPU接口模块的Interrupt Acknowledge Register,以获取中断ID。除了返回中断ID之外,读取操作还会使该中断在Distributor中标记为active状态。一旦知道了中断ID(标识中断源),顶层处理程序现在就可以分派特定于设备的处理程序来处理中断。 ​ 当特定于设备的处理程序完成执行时,顶级处理程序将相同的中断ID写入CPU interface模块中的End of Interrupt register中断结束寄存器,指示中断处理结束。除了把当前中断移除active状态之外,这将使最终中断状态变为inactive或pending(如果状态为inactive and pending),这将使CPU interface能够将更多待处理pending的中断转发给CPU核。这样就结束了单个中断的处理。 ​ 同一CPU核上可能有多个中断等待服务,但是CPU interface一次只能发出一个中断信号。顶层中断处理程序重复上述顺序,直到读取特殊的中断ID值1023,表明该内核不再有任何待处理的中断。这个特殊的中断ID被称为伪中断ID(spurious interrupt ID)。 ​ 伪中断ID是保留值,不能分配给系统中的任何设备。

2.5 GIC的寄存器

GIC分为两部分:Distributor和CPU interface,它们的寄存器都有相应的前缀:"GICD*"、"GICC*"。这些寄存器都是映射为内存接口(memery map),CPU可以直接读写。

2.5.1 Distributor 寄存器概述

**1.istributor Control Register, GICD_CTLR:**用于将pending Group 1中断从Distributor转发到CPU interfaces。

2.Interrupt Controller Type Register, GICD_TYPER:表示是否使用安全拓展,已实现CPU的interfac es数量,GIC支持的最大中断数量。

**3.Distributor Implementer Identification Register, GICD_IIDR:描述产品标识ID,主版本号和此版本号,**含有实现这个GIC的公司的JEP106代码。

**4.Interrupt Group Registers, GICD_IGROUPRn:**组状态位,对于每个位: 0:相应的中断为Group 0; 1:相应的中断为Group 1。

**5.Interrupt Set-Enable Registers, GICD_ISENABLERn:**对于SPI和PPI类型的中断,每一位控制对应中断的转发行为:从Distributor转发到CPU interface: 读: 0:表示当前是禁止转发的; 1:表示当前是使能转发的; 写: 0:无效 1:使能转发

**6.Interrupt Set-Active Registers, GICD_ISACTIVERn:**把相应中断设置为active状态,如果中断已处于Active状态,则写入无效

**7.Interrupt Clear-Active Registers, GICD_ICACTIVERn:**把相应中断设置为deactive状态,如果中断已处于dective状态,则写入无效

8.**Interrupt Priority Registers, GICD_IPRIORITYRn:**对于每一个中断,都有对应的8位数据用来描述:它的优先级。 每个优先级字段都对应一个优先级值,值越小,相应中断的优先级越高

9.**Interrupt Processor Targets Registers, GICD_ITARGETSRn:**对于每一个中断,都有对应的8位数据用来描述:这个中断可以发给哪些CPU。

10.**Interrupt Configuration Registers, GICD_ICFGRn:**对于每一个中断,都有对应的2位数据用来描述:它的边沿触发,还是电平触发。

11.**Identification registers: Peripheral ID2 Register, ICPIDR2:**未定义寄存器,由实现和GIC版本定义.

2.5.2 CPU interface寄存器描述

1.CPU Interface Control Register, GICC_CTLR: 此寄存器用来控制CPU interface传给CPU的中断信号。

2.**nterrupt Priority Mask Register, GICC_PMR:**提供优先级过滤功能,优先级高于某值的中断,才会发送给CPU。

**3.Binary Point Register, GICC_BPR:**此寄存器用来把8位的优先级字段拆分为组优先级和子优先级,组优先级用来决定中断抢占。

4.**Interrupt Acknowledge Register, GICC_IAR:**CPU读此寄存器,获得当前中断的interrtup ID。

  1. Interrupt Register, GICC_EOIR: 写此寄存器,表示某中断已经处理完毕

3. GIC编程实现

3. 1 异常向量表的安装与调用

  • 中断发生的硬件过程
  • 中断处理的软件处理流程

    • CPU执行完当前指令,检查到发生了中断,跳到向量表

    • 保存现场、执行GIC提供的处理函数、恢复现场

发生中断时,CPU跳到向量表去执行b vector_irq

vector_irq函数使用宏来定义:

处理流程:

处理函数:

3.2 GIC驱动程序对中断的处理流程

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

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

  • 一次分配完所有的irq_desc

  • 按需分配(用到某个中断才分配它的irq_desc

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

  • 假设GIC可以向CPU发出16~1019号中断,这些数字被称为hwirq。0~15用于Process之间通信,比较特殊。

  • 假设要使用UART模块,它发出的中断连接到GIC的32号中断,分配的irq_desc序号为16

  • 在GIC domain中会记录(32, 16)

  • 那么注册中断时就是:request_irq(16, ...)

  • 发生UART中断时

    • 程序从GIC中读取寄存器知道发生了32号中断,通过GIC irq_domain可以知道virq为16

    • 调用irq_desc[16]中的handleA函数,它的作用是调用action链表中用户注册的函数

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

  • 假设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为16

    • 调用irq_desc[16]中的handleB函数

      • handleB读取GPIO寄存器,确定是GPIO里2号引脚发生中断

      • 通过GPIO irq_domain可以知道virq为102

      • 调用irq_desc[102]中的handleA函数,它的作用是调用action链表中用户注册的函数

virq通常是指虚拟中断号(Virtual IRQ)

3. GIC驱动程序分析

3.1 理论分析

沿着中断的处理流程,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.2 GIC初始化过程

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);

这段代码是Linux内核启动和初始化中断系统的一个高层次概述。下面是对每个函数调用的解释:

  1. start_kernel:

    • 这是位于init/main.c文件中的函数,是Linux内核启动过程的起点。它负责执行系统初始化,包括内存管理、调度器、VFS(虚拟文件系统)等核心子系统的设置。
  2. init_IRQ:

    • 这个函数位于arch/arm/kernel/irq.c(对于ARM架构),负责初始化内核的中断处理机制。它会设置中断描述符、中断栈、以及与硬件相关的中断控制结构。
  3. irqchip_init:

    • 位于drivers/irqchip/irqchip.c,这个函数负责初始化系统中的中断芯片(irq_chip)。中断芯片是硬件中断控制器的抽象,它定义了一组操作来控制硬件中断,如启用、禁用、EOI(结束中断)等。
  4. of_irq_init:

    • drivers/of/irq.c中,这个函数使用设备树(Device Tree)来初始化中断。设备树是一个数据结构,用于描述系统的硬件配置,包括中断控制器和它们的特性。
  5. desc->irq_init_cb = match->data;:

    • 这行代码为中断控制器设置了一个初始化回调函数。desc是指向中断控制器描述符的指针,match是一个数据结构,包含了与设备树匹配的信息。match->data通常是指向初始化函数的指针。
  6. ret = desc->irq_init_cb(desc->dev, desc->interrupt_parent);:

    • 这行代码调用了之前设置的初始化回调函数,传递了两个参数:desc->dev(中断控制器的设备结构体)和desc->interrupt_parent(中断控制器的父设备,通常是中断控制器的上级控制器)。

    • ret是这个回调函数的返回值,通常用于指示初始化是否成功。

这个过程是Linux内核在启动时初始化中断系统的一部分。通过使用设备树,内核能够以一种硬件无关的方式来初始化中断控制器,使得驱动程序可以以统一的接口与各种硬件中断控制器交互。初始化中断控制器是系统启动过程中的一个关键步骤,因为中断是大多数硬件设备与CPU通信的主要方式。

按照设备树的套路:

  • 驱动程序注册platform_driver

  • 它的of_match_table里有多个of_device_id,表示能支持多个设备

  • 有多种版本的GIC,在内核为每一类GIC定义一个结构体of_device_id,并放在一个段里。

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

3.3 申请中断

在设备树中请求中断

函数调用过程如下,使用图片形式可以一目了然:

为设备树节点分配设备

cpp 复制代码
of_device_alloc (drivers/of/platform.c)
    dev = platform_device_alloc("", -1);  // 分配 platform_device   
    num_irq = of_irq_count(np);  // 计算中断数    
	
	// drivers/of/irq.c, 根据设备节点中的中断信息, 构造中断资源
	of_irq_to_resource_table(np, res, num_irq) 
        of_irq_to_resource // drivers\of\irq.c
        	int irq = irq_of_parse_and_map(dev, index);  // 获得virq, 中断号

解析设备树映射中断: irq_of_parse_and_map

cpp 复制代码
// drivers/of/irq.c, 解析设备树中的中断信息, 保存在of_phandle_args结构体中
of_irq_parse_one(dev, index, &oirq)

// kernel/irq/irqdomain.c, 创建中断映射
irq_create_of_mapping(&oirq);             
	irq_create_fwspec_mapping(&fwspec);
		// 调用irq_domain->ops的translate或xlate,把设备节点里的中断信息解析为hwirq, type
		irq_domain_translate(domain, fwspec, &hwirq, &type)  
		
		// 看看这个hwirq是否已经映射, 如果virq非0就直接返回
		virq = irq_find_mapping(domain, hwirq); 
		
		// 否则创建映射	
		if (irq_domain_is_hierarchy(domain)) {
			// 返回未占用的virq
			// 并用irq_domain->ops->alloc函数设置irq_desc
			virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, fwspec);
			if (virq <= 0)
				return 0;
		} else {
			/* Create mapping */
			// 返回未占用的virq
			// 并通过irq_domain_associate调用irq_domain->ops->map设置irq_desc
			virq = irq_create_mapping(domain, hwirq);
		if (!virq)
				return virq;
		}
相关推荐
搬砖的小码农_Sky6 分钟前
硬件设计:RS485电平标准
单片机·嵌入式硬件·fpga开发
ElePower952744 分钟前
STM32学习(一)
stm32·嵌入式硬件
搬砖的小码农_Sky1 小时前
硬件设计:LVDS电平标准
嵌入式硬件·fpga开发
Wanliang Li2 小时前
Linux电源管理——CPU Hotplug 流程
linux·嵌入式硬件·嵌入式·armv8·电源管理·cpuhotplug
紫阡星影3 小时前
【模块系列】STM32&1.69TFT屏幕
stm32·单片机·嵌入式硬件
彭某。3 小时前
STM32低功耗模式结合看门狗
stm32·单片机·嵌入式硬件
憧憬一下3 小时前
PCI/PCIe设备INTx中断机制和MSI中断机制
arm开发·嵌入式硬件·嵌入式·linux驱动开发·pci/pcie
不能只会打代码4 小时前
32单片机综合案例——智能环境监控系统
单片机·嵌入式硬件
小菜鸟学代码··12 小时前
STM32文件详解
stm32·单片机·嵌入式硬件
马浩同学13 小时前
【GD32】从零开始学GD32单片机 | DAC数模转换器 + 三角波输出例程
c语言·单片机·嵌入式硬件·mcu