Linux 驱动开发之中断分析3(基于Linux6.6)---中断处理流程介绍
一、GIC概念
1.1、GIC 的基本概念
GIC 是为了统一和优化中断控制而设计的,在多核系统中尤为重要。它有助于管理多个中断源,并通过中断优先级、屏蔽和传递机制确保中断的正确处理。
GIC 是 ARM 架构的一个标准化组件,用于将硬件中断请求(IRQ)转发给正确的处理器或核心。它可以配置不同的中断源和中断处理程序之间的映射,以及设置中断的优先级。
1.2、GIC 的主要版本
ARM GIC 有两个主要版本:
- GICv2:用于较早的 ARMv7 体系结构,支持单核和多核处理器。
- GICv3:引入了对更复杂架构的支持,主要用于 ARMv8 处理器,支持更多的功能,例如更好的中断管理、优先级控制和虚拟化支持。
1.3、GIC 的工作原理
GIC 的主要职责是将外部设备产生的中断信号转发到处理器,并确保处理器能够响应这些中断。它包括以下几个关键组件:
-
中断分配器(Interrupt Distributor):
- 负责将中断从外部设备传递到相应的 CPU 核心。
- 在多核系统中,GIC 可以将一个中断路由到特定的 CPU 核心。
- 它根据配置的中断优先级,决定哪一个 CPU 核心首先处理哪个中断。
-
中断处理器(Interrupt Processor Interface, IPI):
- 负责从中断分配器接收中断请求并将其交给合适的 CPU 核心。
- 处理器通过这一接口与 GIC 进行交互,获取待处理的中断。
-
中断路由(Interrupt Routing):
- GIC 会根据系统配置和中断源的要求,将中断信号路由到合适的 CPU 核心。
- 通过硬件配置和软件配置,可以控制哪些中断被发送到哪些 CPU 核心。
-
优先级和屏蔽(Priority and Masking):
- 中断控制器提供优先级机制,允许系统为不同的中断源设置不同的优先级。高优先级的中断会优先被处理。
- 每个中断都有一个屏蔽位,用于禁用或允许特定的中断。通过设置屏蔽位,可以防止某些中断被处理。
-
虚拟化支持(Virtualization Support):
- GICv3 及以上版本支持虚拟化,允许多个虚拟机共享同一硬件中断控制器。通过虚拟化,Linux 内核可以通过虚拟化的机制为每个虚拟机提供隔离的中断管理。
1.4、GICv2 与 GICv3 的差异
-
GICv2:
- 主要用于 ARMv7 及早期的 ARM 处理器。
- 不支持现代处理器的高级特性,如虚拟化、更多的中断优先级等。
- 适合较小的多核或单核系统。
-
GICv3:
- 用于 ARMv8 及后续架构,支持更高的可扩展性。
- 提供更多的中断源、灵活的中断优先级管理以及更复杂的中断路由机制。
- 支持中断分配、虚拟化、共享中断等现代特性,特别适用于大规模多核系统。
1.5、GIC 中断的配置
Linux 内核通过以下方式配置和管理 GIC 中断:
- 内核配置:内核在启动时会识别和配置 GIC,选择正确的 GIC 驱动程序(例如
gic-v2
或gic-v3
)。 - 设备树(Device Tree):ARM 系统中,硬件设备的配置通常通过设备树描述。Linux 会解析设备树,确定 GIC 的配置,包括中断源、优先级和路由等。
- IRQ 处理程序:内核会注册中断处理程序(IRQ handler),以便在接收到中断时执行特定的处理任务。
- 中断向量表(Vector Table):通过 GIC,内核能够根据中断类型调用不同的中断服务例程(ISR)来响应特定的硬件中断。
1.6、GIC 在 Linux 内核中的实现
Linux 内核通过 irqchip
机制来支持 GIC。irqchip
是 Linux 中断子系统的一部分,负责管理不同硬件中断控制器的驱动。Linux 内核根据 GIC 的版本(v2 或 v3)来加载不同的中断驱动。具体来说,GIC 的实现通常位于 drivers/irqchip/
目录下,使用了 irqchip
API 来注册和管理中断控制器。
二、设备树中中断如何工作
与遵循树的自然结构而进行的地址转换不同,机器上的任何设备都可以发起和终止中断信号。另外地址的编址也不同于中断信号,前者是设备树的自然表示,而后者者表现为独立于设备树结构的节点之间的链接。描述中断连接需要四个属性:
■ interrupt-controller - 一个空的属性定义该节点作为一个接收中断信号的设备。
■ #interrupt-cells - 这是一个中断控制器节点的属性。它声明了该中断控制器的中断指示符中 cell 的个数(类似于 #address-cells 和 #size-cells)。
■ interrupt-parent - 这是一个设备节点的属性,包含一个指向该设备连接的中断控制器的 phandle。那些没有 interrupt-parent 的节点则从它们的父节点中继承该属性。
■ interrupts - 一个设备节点属性,包含一个中断指示符的列表,对应于该设备上的每个中断输出信号。
中断指示符是一个或多个 cell 的数据(由 #interrupt-cells 指定),这些数据指定了该设备连接至哪些输入中断。在以下的例子中,大部分设备都只有一个输出中断,但也有可能在一个设备上有多个输出中断。一个中断指示符的意义完全取决于与中断控制器设备的 binding。每个中断控制器可以决定使用几个 cell 来唯一的定义一个输入中断。
dts
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0>;
};
cpu@1 {
compatible = "arm,cortex-a9";
reg = <1>;
};
};
serial@101f0000 {
compatible = "arm,pl011";
reg = <0x101f0000 0x1000 >;
interrupts = < 1 0 >;
};
serial@101f2000 {
compatible = "arm,pl011";
reg = <0x101f2000 0x1000 >;
interrupts = < 2 0 >;
};
gpio@101f3000 {
compatible = "arm,pl061";
reg = <0x101f3000 0x1000
0x101f4000 0x0010>;
interrupts = < 3 0 >;
};
intc: interrupt-controller@10140000 {
compatible = "arm,pl190";
reg = <0x10140000 0x1000 >;
interrupt-controller;
#interrupt-cells = <2>;
};
spi@10115000 {
compatible = "arm,pl022";
reg = <0x10115000 0x1000 >;
interrupts = < 4 0 >;
};
external-bus {
#address-cells = <2>
#size-cells = <1>;
ranges = <0 0 0x10100000 0x10000 // Chipselect 1, Ethernet
1 0 0x10160000 0x10000 // Chipselect 2, i2c controller
2 0 0x30000000 0x1000000>; // Chipselect 3, NOR Flash
ethernet@0,0 {
compatible = "smc,smc91c111";
reg = <0 0 0x1000>;
interrupts = < 5 2 >;
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
interrupts = < 6 2 >;
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
interrupts = < 7 3 >;
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x4000000>;
};
};
};
需要注意的事情:
■ 这个机器只有一个中断控制器:interrupt-controller@10140000。
■ 中断控制器节点上添加了‘inc:’标签,该标签用于给根节点的 interrupt-parent 属性分配一个 phandle。这个 interrupt-parent 将成为本系统的默认值,因为所有的子节点都将继承它,除非显示覆写这个属性。
■ 每个设备使用 interrupts 属性来不同的中断输入线。
■ #interrupt-cells 是 2,所以每个中断指示符都有 2 个 cell。本例使用一种通用的模式,也就是用第一个 cell 来编码中断线号;然后用第二个 cell 编码标志位,比如高电平/低电平有效,或者边缘/水平触发。对于任何给定的中断控制器,请参考该控制器的 binding 文档以了解指示符如何编码。
三、GIC DTS描述
3.1、中断系统概述
对于中断系统,主要有三个角色:
1. Processor(处理器)
Processor 是中断系统的核心组件,主要负责处理中断请求,并根据中断信号执行相应的中断处理程序。处理器的任务是:
- 响应中断:当处理器接收到中断信号时,它会停止当前正在执行的指令,并将控制权转移到中断处理程序(Interrupt Service Routine,ISR)中。
- 保存上下文:为了恢复正常执行,处理器在处理中断时会保存当前任务的上下文(例如寄存器、程序计数器等)。
- 处理中断:根据中断的类型,处理器执行相应的中断处理逻辑。不同的中断可能会调用不同的 ISR。
- 恢复执行:中断处理完毕后,处理器会恢复之前的执行状态,继续执行被中断的任务。
在现代多核处理器中,处理器还需要决定如何分配中断到不同的核心进行处理。
2. Interrupt Generating Device(中断生成设备)
Interrupt Generating Device 是产生中断信号的硬件设备。它通常是外设或外部事件源,能够向处理器发送中断请求,表明它有一些需要处理器处理的事情。中断生成设备的工作原理包括:
- 生成中断信号:设备检测到需要处理的事件(例如数据到达、错误状态、超时等),然后通过中断线路发送中断请求给中断控制器。
- 中断请求类型:中断生成设备可能会根据事件的紧急性或重要性,发送不同类型的中断信号(如硬件中断、外部中断、内部中断等)。
- 可屏蔽性:有些设备的中断信号可以被处理器或中断控制器屏蔽(禁用),防止不必要的干扰。
例如,网络接口卡(NIC)可能会在接收到新数据包时生成中断请求,告知处理器去处理数据。
3. Interrupt Controller(中断控制器)
Interrupt Controller 是负责管理和分发中断请求的硬件组件,它通常位于处理器和中断生成设备之间。中断控制器的功能包括:
- 接收中断请求:中断生成设备通过硬件中断线路(interrupt line)向中断控制器发送中断信号。中断控制器会收集所有中断请求。
- 中断优先级:中断控制器负责确定中断的优先级。不同的中断源可能会有不同的优先级,控制器按照优先级的顺序通知处理器。
- 如果有多个中断请求同时到来,控制器会根据预定的优先级规则,确定哪个中断需要首先处理。
- 一些中断控制器还提供中断屏蔽功能,可以通过屏蔽特定的中断来避免它们影响系统。
- 中断路由:在多核系统中,中断控制器负责将中断分配到特定的 CPU 核心。例如,在 ARM 架构中的 GIC(Generic Interrupt Controller)中,中断可以被路由到一个或多个核心。
- 中断向处理器发送信号:一旦决定了哪个中断需要处理,中断控制器会向处理器发送一个中断信号,通常是通过中断请求线(IRQ)来通知处理器。
3.2、DTS如何描述Interrupt Generating Device
对于Interrupt Generating Device,需要定义下面两个属性:
(1) Interrupt属性:该属性主要描述了中断的HW interrupt ID以及类型。
(2)interrupt-parent 属性:该属性主要描述了该设备的interrupt request line连接到哪一个interrupt controller。
dts
uart3: serial@48020000 {
compatible = "ti,omap4-uart";
reg = <0x48020000 0x100="">;
interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>;
ti,hwmods = "uart3";
clock-frequency = <48000000>;
};
对于uart3,interrupts属性用3个cell(对于device tree,cell是指由32bit组成的一个信息单位)表示。GIC_SPI 描述了interrupt type。
对于GIC,它可以管理4种类型的中断:
1)外设中断(Peripheral interrupt)
根据目标CPU的不同,外设的中断可以分成PPI(Private Peripheral Interrupt)和SPI(Shared Peripheral Interrupt)。PPI只能分配给一个确定的processor,而SPI可以由Distributor将中断分配给一组Processor中的一个进行处理。外设类型的中断一般通过一个interrupt request line的硬件信号线连接到中断控制器,可能是电平触发的(Level-sensitive),也可能是边缘触发的(Edge-triggered)。
2)软件触发的中断(SGI,Software-generated interrupt)
软件可以通过写GICD_SGIR寄存器来触发一个中断事件,这样的中断,可以用于processor之间的通信。
3)虚拟中断(Virtual interrupt)和Maintenance interrupt。
在DTS中,外设的interrupt type有两种,一种是SPI,另外一种是PPI。SGI用于processor之间的通信,和外设无关。uart3的interrupt属性中的74表示该外设使用的GIC interrupt ID号。GIC最大支持1020个HW interrupt ID,具体的ID分配情况如下:1)ID0~ID31是用于分发到一个特定的process的interrupt。标识这些interrupt不能仅仅依靠ID,因为各个interrupt source都用同样的ID0~ID31来标识,因此识别这些interrupt需要interrupt ID + CPU interface number。ID0~ID15用于SGI,ID16~ID31用于PPI。PPI类型的中断会送到指定的process上,和其他的process无关。SGI是通过写GICD_SGIR寄存器而触发的中断。Distributor通过processor source ID、中断ID和target processor ID来唯一识别一个SGI。2)ID32~ID1019用于SPI。uart3的interrupt属性中的IRQ_TYPE_LEVEL_HIGH用来描述触发类型。
3.3、DTS如何描述GIC
dts
#include "exynos4x12.dtsi"
/{
compatible = "samsung,exynos4412";
gic: interrupt-controller@10490000 {
cpu-offset = <0x4000>;
};
interrupt-controller@10440000 {
samsung,combiner-nr = <20>;
interrupts = <0 0 0>, <0 1 0>, <0 2 0>, <0 3 0>,
<0 4 0>, <0 5 0>, <0 6 0>, <0 7 0>,
<0 8 0>, <0 9 0>, <0 10 0>, <0 11 0>,
<0 12 0>, <0 13 0>, <0 14 0>, <0 15 0>,
<0 107 0>, <0 108 0>, <0 48 0>, <0 42 0>;
};
};
a -- compatible属性
compatible属性用来描述GIC的programming model。该属性的值是string list,定义了一系列的modle(每个string是一个model)。这些字符串列表被操作系统用来选择用哪一个driver来驱动该设备。
假设定义该属性:compatible = “a厂商,p产品”, “标准bbb类型设备”。那么linux kernel可能首先使用“a厂商,p产品”来匹配适合的driver,如果没有匹配到,那么使用字符串“标准bbb类型设备”来继续寻找适合的driver。
compatible属性有两个应用场景:
1)对于root node,compatible属性是用来匹配machine type的(参考Device Tree相关文档)
2)对于普通的HW block的节点,例如interrupt-controller,compatible属性是用来匹配适合的driver的。
b -- interrupt-controller
interrupt-controller这个没有定义value的属性用来表明本设备节点就是一个interrupt controller。理解#interrupt-cells这个属性需要理解interrupt specifier和interrupt domain这两个概念。interrupt specifier其实就是外设interrupt的属性值,对于uart3而言,其interrupt specifier就是<GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,也就是说,interrupt specifier定义了一个外设产生中断的规格(HW interrupt ID + interrupt type)。
具体如何解析interrupt specifier?这个需要限定在一定的上下文中,不同的interrupt controller会有不同的解释。因此,对于一个包含多个interrupt controller的系统,每个interrupt controller及其相连的外设组成一个interrupt domain,各个外设的interrupt specifier只能在属于它的那个interrupt domain中得到解析。#interrupt-cells定义了在该interrupt domain中,用多少个cell来描述一个外设的interrupt specifier。
c -- reg
reg属性定义了GIC的memory map的地址.
3.4、GIC的HW block diagram描述
1、Distributor
Distributor的主要的作用是检测各个interrupt source的状态,控制各个interrupt source的行为,分发各个interrupt source产生的中断事件到各个processor。
Distributor对中断的控制包括:
1)中断enable或者disable的控制。Distributor对中断的控制分成两个级别。一个是全局中断的控制。一旦disable了全局的中断,那么任何的interrupt source产生的interrupt event都不会被传递到CPU interface。另外一个级别是对针对各个interrupt source进行控制,disable某一个interrupt source会导致该interrupt event不会分发到CPU interface,但不影响其他interrupt source产生interrupt event的分发。
2)控制中断事件分发到process。一个interrupt事件可以分发给一个process,也可以分发给若干个process。
3)优先级控制。
4)interrupt属性设定。例如是level-sensitive还是edge-triggered,是属于group 0还是group 1。
Distributor可以管理若干个interrupt source,这些interrupt source用ID来标识,我们称之interrupt ID。
2、CPU interface
CPU interface这个block主要用于和process进行接口。该block的主要功能包括:
1)enable或者disable
对于ARM,CPU interface block和process之间的中断信号线是nIRQ和nFIQ这两个signal。如果disable了中断,那么即便是Distributor分发了一个中断事件到CPU interface,但是也不会assert指定的nIRQ或者nFIQ通知processor。
2)ackowledging中断
processor会向CPU interface block应答中断,中断一旦被应答,Distributor就会把该中断的状态从pending状态修改成active。如果没有后续pending的中断,那么CPU interface就会deassert nIRQ或者nFIQ的signal。如果在这个过程中又产生了新的中断,那么Distributor就会把该中断的状态从pending状态修改成pending and active。这时候,CPU interface仍然会保持nIRQ或者nFIQ信号的asserted状态,也就是向processor signal下一个中断。
3)中断处理完毕的通知
当interrupt handler处理完了一个中断的时候,会向写CPU interface的寄存器从而通知GIC CPU已经处理完该中断。做这个动作一方面是通知Distributor将中断状态修改为deactive,另外一方面,如果一个中断没有完成处理,那么后续比该中断优先级低的中断不会assert到processor。一旦标记中断处理完成,被block掉的那些比当前优先级低的中断就会递交给processor。
4)设定priority mask
通过priority mask,可以mask掉一些优先级比较低的中断,这些中断不会通知到CPU。
5)设定preemption的策略
6)在多个中断事件同时到来的时候,选择一个优先级最高的通知processor
四、按键中断节点
1、查看原理图
2、可以看到中断 EINT9 EINT10 挂在GPX1下 1 2
3、查看中断号
4、确定其父节点
1. 按键中断的工作原理
在Linux中,按键设备(例如键盘)通常通过硬件中断来通知系统用户按下了某个按键。按键中断的工作流程大致如下:
- 硬件中断:当用户按下键盘上的一个键时,键盘控制器会向系统发送一个硬件中断请求(IRQ),通知处理器有一个输入事件需要处理。
- 中断处理:中断控制器将该中断请求传递给处理器,处理器跳转到相应的中断处理程序(ISR)。在Linux中,这通常是由输入子系统中的中断处理程序来执行的。
- 输入子系统:输入子系统会接收按键中断并将按键信息传递给相应的输入设备驱动。设备驱动通常会将按键事件转化为键值(例如,
KEY_A
,KEY_B
等),并通过Linux的事件机制通知用户空间。 - 事件处理:在内核中,按键事件会被传递到
/dev/input/eventX
设备节点,用户空间的程序可以读取这些事件。
2. 输入子系统和设备节点
Linux的输入子系统负责管理键盘、鼠标、触摸屏等输入设备。它使用一个通用的框架来处理各种输入设备,并通过 /dev/input/
目录下的设备节点提供与用户空间的交互。
每个输入设备(包括键盘)通常对应一个 /dev/input/eventX
设备节点,其中 X
是设备的编号。例如,如果你有一个键盘,它可能对应于 /dev/input/event0
。这些设备节点是通过 evdev
驱动与用户空间通信的。
3. 按键中断与/dev/input/eventX
设备节点
当按下键盘上的按键时:
- 硬件中断产生,并由中断处理程序处理。
- 输入子系统将该事件转化为一个键值(例如
KEY_A
)。 - 键值通过
input_event
结构体被传递到/dev/input/eventX
设备节点。
用户空间的应用程序(如 evtest
、xev
、或者自定义应用)可以读取 /dev/input/eventX
设备节点,获取按键事件。
例如,使用 evtest
来监控按键事件,可以执行以下命令:
bash
sudo evtest /dev/input/event0
此命令会显示在 /dev/input/event0
上发生的所有输入事件,包括按键按下和释放。
4. 按键事件处理
按键事件的表示通常使用 input_event
结构体,该结构体包含了事件的类型(EV_KEY
)和具体的按键代码。例如,按下“a”键时,系统会产生一个 EV_KEY
类型的事件,按键代码为 KEY_A
。
input_event
结构体:
c
struct input_event {
struct timeval time; // 事件时间戳
__u16 type; // 事件类型 (例如 EV_KEY)
__u16 code; // 事件代码 (例如 KEY_A)
__s32 value; // 事件值 (例如按键按下为 1,松开为 0)
};
5. 中断处理和按键扫描
在Linux内核中,按键中断的处理通常是由设备驱动完成的,驱动会通过扫描按键矩阵来识别按下的键。在扫描过程中,驱动会将按键事件传递给输入子系统,然后通过事件队列传递到 /dev/input/eventX
。
按键设备驱动
常见的键盘驱动程序,如 atkbd
(用于PS/2键盘)或 usbhid
(用于USB键盘),会监听硬件中断并读取键盘扫描码。在按键事件发生时,这些驱动程序会调用输入子系统的接口,将按键事件报告给内核。
例如,PS/2键盘驱动atkbd
会在每次按键事件发生时触发中断,然后驱动程序会分析扫描码,并通过 input_event
结构体将按键事件传递给内核。
6. 用户空间交互
用户空间的程序可以通过访问 /dev/input/eventX
设备节点来读取按键事件。常用的工具包括:
- evtest:用于测试和查看输入设备事件。
- xev:用于查看X服务器中的输入事件(包括键盘按键事件)。
- 自定义应用程序:开发者可以编写自己的程序,使用
libevdev
等库直接读取eventX
设备节点,处理按键事件。
例如,使用 C 语言读取按键事件的代码片段如下:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/input.h>
int main() {
int fd = open("/dev/input/event0", O_RDONLY);
if (fd == -1) {
perror("Unable to open event device");
return 1;
}
struct input_event ev;
while (1) {
ssize_t n = read(fd, &ev, sizeof(struct input_event));
if (n == -1) {
perror("Read error");
close(fd);
return 1;
}
if (ev.type == EV_KEY) {
printf("Key %d %s\n", ev.code, ev.value ? "pressed" : "released");
}
}
close(fd);
return 0;
}
此代码会读取 /dev/input/event0
上的事件并打印按键的状态(按下或释放)。