【嵌入式linux学习】06_中断子系统

【嵌入式linux学习】06_中断子系统

这个blog 默认对stm32裸机中断有一定的认识~

首先说说stm32的裸机的中断机制

我们都知道它有一个NVIC中断控制器。中断 = 外设 → NVIC → CPU,几乎没有中间层

在这里的步骤是

复制代码
外设产生中断
→ NVIC 判断是否使能
→ NVIC 比较优先级
→ CPU 跳转到 ISR
→ 执行中断函数
→ 返回

在嵌入式linux里面:

复制代码
外设中断信号
   ↓
GIC600(路由 / 优先级 / 目标 CPU)
   ↓
CPU 的异常入口(EL1 IRQ)
   ↓
Linux 中断子系统
   ↓
 request_irq() handler

GIC(Generic Interrupt Controller) 是:

ARM 为"多核 + 操作系统"设计的通用中断控制器

不是 CPU 内部的一小块逻辑 ,而是一个独立的硬件模块。主要作用就是接受硬件中断信号,通过一定的设置策略,然后分发给对应的CPU进行处理。

以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是基于消息的中断,它们的配置保存在表中而不是寄存器。

文章目录

讲讲GIC

首先讲讲GIC的组成

GIC v3主要这几部分组成:

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

CPU interface【为链接到GIC的处理器提供接口,CPU接口可以开启或关闭发往CPU的中断请求,CPU中断开启后只有优先级高于 "中断优先级掩码"的中断请求才能被发送到CPU】、

Redistributor【Redistributor管理SGI,PPI,LPI中断,然后将中断发送给CPU interface】、

ITS【ITS 是GIC v3架构中的一种可选硬件机制,ITS提供了一种将基于消息的中断转换为LPI的软件机制,它是 在支持LPI的配置中可选地支持】。

🌿GIC v3中,将cpu interface从GIC中抽离,放入到了cpu中,cpu interface通过AXI Stream,与gic进行通信。 当GIC要发送中断,GIC通过AXI stream接口,给cpu interface发送中断命令,cpu interface收到中断命令后,根据中断线映射配置,决定是通过IRQ还是FIQ管脚,向cpu发送中断。

中断状态和处理流程------状态机维护

每个中断都维护一个状态机,支持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产生合适的中断异常给处理器,处理器接收该异常,最后软件处理该中断。

怎么用GIC

我们可以通过配置相应的寄存器来对不同的中断进行响应。

1️⃣比如Distributor相关寄存器------

中断使能寄存器GICD_ISENABLERn【中断使能寄存器支持按位读、写,读出的数据是中断当前状态,为0则中断禁用,为1则中断启用。对中断使能寄存器写1则开启中断,写0无效。】

中断优先级设置寄存器GICD_IPRIORITYRn【可以对这个寄存器进行位操作配置不同中断源的中断优先级】

2️⃣还有CPU接口相关的寄存器------

中断优先掩码寄存器GICC_PMR【中断优先级掩码寄存器GICC_PMR用8位代表一个中断阈值。高于这个优先级的中断才能被送到CPU】

中断优先级分组寄存器GICC_BPR【中断优先级分组寄存器用于将8位的优先级分成两部分,一部分表示抢占优先级另外一部分表示自优先级,这和STM32的中断优先级分组相同】

所以,我们需要用到这些寄存器,需要先找到寄存器的地址,以rk3588s为例,从设备可以看到~相应寄存器的地址

c 复制代码
reg = <0x0 0xfe600000 0 0x10000>, /* GICD 基地址: 0xfe600000,长度 64KB */
      <0x0 0xfe680000 0 0x100000>; /* GICR 基地址: 0xfe680000,长度 1MB */

GICD (Distributor): 负责全局中断(SPI)的分发。

GICR (Redistributor): 负责每个 CPU 核心的中断(SGI/PPI)。由于 RK3588 有 8 个核心,每个核心都需要一块地址,所以这块区域特别大(1MB)。

设备树简单分析

设备树是怎么描述中断系统信息的

以3588的设备树为例,找到"interrupt-controller"节点,具体可以看看下面的注释部分

D 复制代码
gic: interrupt-controller@fe600000 {
		compatible = "arm,gic-v3";//compatible属性用于平台设备驱动的匹配。
		#interrupt-cells = <3>;//指定使用该中断控制器的节点要用几个cells来描述一个中断
		#address-cells = <2>;
		#size-cells = <2>;
		ranges;
		interrupt-controller;//声明该设备树节点是一个中断控制器。
		
    //reg指定中断控制器相关寄存器的地址及大小,GICD是Distributor寄存器,GICR是指Redistributor寄存器
		reg = <0x0 0xfe600000 0 0x10000>, /* GICD */
		      <0x0 0xfe680000 0 0x100000>; /* GICR */
    
    //描述中断信息,这里是用三个u32描述,是前面#interrupt-cells指定的。第一个指定中断类型,第二个中断号,第三位是触发类型
		interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_HIGH>;
    
    //在gic设备节点下,有一个子设备节点its,ITS设备用于将消息信号中断(MSI)路由到cpu
		its0: msi-controller@fe640000 {
			compatible = "arm,gic-v3-its";
			msi-controller;//标识该设备是MSI控制器
			#msi-cells = <1>;//必须是1,MSI设备的DeviceID
			reg = <0x0 0xfe640000 0x0 0x20000>;
		};
		its1: msi-controller@fe660000 {
			compatible = "arm,gic-v3-its";
			msi-controller;
			#msi-cells = <1>;
			reg = <0x0 0xfe660000 0x0 0x20000>;
		};
	};

具体一点:

全局
复制代码
gic: interrupt-controller@fe600000 {
    ...
};

👉 这是整个 SoC 的"中断中枢"

  • CPU 的所有硬件中断
  • GPIO 中断
  • 外设中断
  • PCIe / MSI 中断

最终都会汇聚到 GIC(Generic Interrupt Controller)

节点名+label
复制代码
gic: interrupt-controller@fe600000 {

1️⃣ gic:(label)

  • 这是一个 设备树 label

  • 其他设备节点可以这样引用它:

    interrupt-parent = <&gic>;

label 只是引用用的,和 Linux 设备名无关

2️⃣ interrupt-controller@fe600000

  • interrupt-controller:功能描述(人类看的)
  • @fe600000第一个寄存器地址

📌 这个地址要和 reg 里的第一个 base 对得上

compatible

Linux 是怎么知道这是 GICv3 的?通过compatible进行平台驱动匹配

#interrupt-cells ------ 中断描述格式
复制代码
#interrupt-cells = <3>;

一个中断需要 3 个 cell 描述,对应的格式为

复制代码
interrupts = < type  irq  flags >;

具体地

cell 含义
第 1 个 中断类型
第 2 个 中断号
第 3 个 触发方式

对于中断类型而言,是由中断类型枚举定义

类型
SPI(外设) GIC_SPI
PPI(私有) GIC_PPI
SGI(软件) GIC_SGI
#address-cells / #size-cells / ranges
复制代码
#address-cells = <2>;
#size-cells = <2>;
ranges;

这是 总线属性声明

👉 表示 GIC 节点下面可以挂子设备

  • 地址用 2 个 cell(64 位)
  • 大小用 2 个 cell(64 位)
  • ranges; 空表示:子地址 = 物理地址

📌 为什么要有这个?

因为下面挂了 ITS(MSI 控制器)

interrupt-controller ------ 身份声明(必须)
复制代码
interrupt-controller;

没有它:

  • Linux 不认为这是中断控制器
  • interrupt-parent 无法指向它

标识该节点是一个中断控制器节点

reg ------ GIC 的寄存器布局
复制代码
reg = <0x0 0xfe600000 0 0x10000>, /* GICD */
      <0x0 0xfe680000 0 0x100000>; /* GICR */
首先讲讲对于这个格式

因为

复制代码
#address-cells = <2>,#size-cells = <2>
所以:
地址 = 2 个 cell(高 32 位 + 低 32 位)
长度 = 2 个 cell(高 32 位 + 低 32 位)

这句话等价于告诉内核:

下面 reg 里的

  • 地址用 64 bit 表示
  • 长度用 64 bit 表示

拆第一段 reg

复制代码
<0x0 0xfe600000  0x0 0x10000>

① 地址部分(2 个 cell)

复制代码
0x0         0xfe600000
↑高32位     ↑低32位

拼起来就是:

复制代码
地址 = (0x0 << 32) | 0xfe600000
     = 0xfe600000

👉 所以这个 0 只是"高 32 位地址"

② size 部分(2 个 cell)

复制代码
0x0     0x10000
↑高32位 ↑低32位

拼起来:

复制代码
size = (0x0 << 32) | 0x10000
     = 0x10000

👉 表示 64KB

等价于:

GICD 寄存器

  • 起始地址:0xfe600000
  • 大小:0x10000
所以为什么不写一个cell

因为 设备树是"可移植"的 👇

  • 有些 SoC:
    • 地址 < 4GB → #address-cells = 1
  • 有些 SoC:
    • 地址 > 4GB → #address-cells = 2
  • ARM64 平台 几乎统一用 2

所以现在看到的是 ARM64 标准写法

interrupts ------ GIC 自己也需要中断?
复制代码
interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_HIGH>;

含义:

👉 GIC 的维护中断(Maintenance Interrupt)

  • 类型:PPI
  • 中断号:9
  • 触发方式:高电平

📌 用途:

  • 虚拟化
  • 中断维护
  • KVM / GIC 状态同步

普通外设 不会用到这个

ITS(MSI Controller)
复制代码
its0: msi-controller@fe640000 {
    compatible = "arm,gic-v3-its";
    msi-controller;
    #msi-cells = <1>;
    reg = <0x0 0xfe640000 0x0 0x20000>;
};

MSI 控制器 = 管理"通过写内存触发中断"的中断控制器

不靠中断引脚 ,而是靠 内存写事务 来触发中断。

一个GIC中断控制器的使用实例

以uart3为例(rk3568.dtsi):

c 复制代码
/ {
    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)

c++ 复制代码
#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,否则屏蔽。

如下示例:

c 复制代码
    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;
    };

实验

编写一个按键中断驱动

  1. 编写设备树结点,引用已经写好的中断控制器父节点和配置中断信息
  2. 写中断驱动

设备树插件实现

注意:这边配置了一下按键引脚,以lubuncat4为例,使用40pin里面的GPIO1_B7,实际中不同板卡可能没有按键,可以替换成其他引脚,然后外接按键、或者接高低电平控制模拟按键。

c 复制代码
/dts-v1/;
/plugin/;

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

&{/} {
        button_interrupt: button_interrupt {
            status = "okay";
            compatible = "button_interrupt";
            button-gpios = <&gpio1 RK_PB7 GPIO_ACTIVE_LOW>;
            pinctrl-names = "default";
            pinctrl-0 = <&button_interrupt_pin>;
            interrupt-parent = <&gpio1>;
            interrupts = <RK_PB7 IRQ_TYPE_EDGE_FALLING>;
        };
    };

&{/pinctrl} {
    pinctrl_button {
        button_interrupt_pin: button_interrupt_pin {
            rockchip,pins = <1 RK_PB7 RK_FUNC_GPIO &pcfg_pull_up>;
        };
    };
};

在板卡上的部分GPIO可能会被系统占用,引脚被占用后,设备树可能无法再加载或驱动中无法再申请对应的资源, 比如运行代码时出现"Device or resource busy"或者运行代码卡死等等现象,可以注释其他的使用的设备树插件。

我们修改内核目录/arch/arm64/boot/dts/rockchip/overlays下的Makefile文件, 添加我们编辑好的设备树插件。并把设备树插件文件放在和Makefile文件同级目录下。 以进行设备树插件的编译。

在内核的根目录下执行如下命令即可:

c 复制代码
# #加载配置文件
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat_linux_rk3588_defconfig
#使用dtbs参数单独编译设备树
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

生成的.dtbo位于内核根目录下的"arch/arm64/boot/dts/rockchip/overlays"目录下。 本章设备树插件为"lubancat-button-overlay.dts", 编译之后就会在内核源码/arch/arm64/boot/dts/rockchip/overlays目录下生成同名的lubancat-button-overlay.dtbo文件,得到.dtbo后,下一步就是将其加载到系统中。

我们编译生成了 lubancat-button-overlay.dtbo ,该文件可以被动态的加载到系统,lubancat4板卡uboot加载设备树插件, 只需完成简单的两个步骤:

  • 1、将需要加载的.dtbo文件放入板卡 /boot/dtb/overlays/ 目录下。
  • 2、将对应的设备树插件加载配置,写入uEnv.txt配置文件,系统启动过程中会自动从uEnv.txt读取要加载的设备树插件。打开位于"/boot/uEnv/"目录下的uEnv.txt文件,要将设备树插件写入uEnv.txt,使用sudo vim或者nano编辑器打开文件,书写格式为"dtoverlay=<设备树插件路径>"。

可以通过pc端操作,交叉编译后把板卡对应文件覆盖,也可以直接在板卡上面操作,具体不详细展开,之前的blog都讲过

添加好后,我们重启开发板,使用命令ls /proc/device-tree/ 查看, 是否有button_interrupt目录,有就说明加载成功。

注意1号引脚的位置

驱动程序

⭐️基本上是固定的套路:

注册一个字符设备,创建结点

让内核知道:「有一个 /dev/mybutton,用户可以 open / read」

  1. 申请设备号

    复制代码
    alloc_chrdev_region(&dev_num, minor, count, name);
  2. 初始化并注册 cdev

    cdev_init(&mycdev, &button_fops);
    cdev_add(&mycdev, dev_num, DEVICE_NUMBER);

设备号 ↔ file_operations;用户 open 这个设备时,内核知道该调用谁

  1. 创建 class + device(为了 /dev)

    复制代码
    class_create(...)
    device_create(...)

    自动生成 /dev/mybutton_class

    作用:自动生成 /dev/mybutton_class 否则:你得手动 mknod

硬件资源层(设备树 / GPIO / 中断)
复制代码
open()
  ↓
找设备树节点
  ↓
拿 GPIO / IRQ 描述
  ↓
申请 GPIO
  ↓
配置 GPIO
  ↓
映射 IRQ
  ↓
注册中断

1️⃣ 找到硬件(设备树)2️⃣ 申请硬件资源(GPIO / IRQ)3️⃣ 把硬件"接管"给当前驱动

  1. 找到设备树结点

    复制代码
    of_find_node_by_path("/button_interrupt");
  2. 从设备树里面拿到硬件资源

    复制代码
    of_get_named_gpio(...);
    irq_of_parse_and_map(...);

    GPIO IRQ reg clock / reset

  3. 向内核"申请"资源

    gpio_request()
    request_irq()

  4. 设置硬件的工作模式

    复制代码
    gpio_direction_input();
中断函数(ISR)
c 复制代码
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{
    atomic_inc(&button_status);
    return IRQ_HANDLED;
}
关于atomic

atomic_inc() 用于在中断或并发环境下对共享变量进行安全自增, 防止普通 ++ 因中断或抢占导致的竞态条件和数据丢失

中断时随时可能发生的,它可能在:

  • 用户态 read()
  • 内核态 copy_to_user()
  • 甚至另一个中断中

随时插进来执行

假设不用 atomic:

复制代码
button_status = 100
  • 中断 A 进来,读到 100
  • 还没写回
  • 中断 B 又进来,也读到 100
  • 两个都 +1
  • 最终写回的还是 101

❌ 本来应该是 102, 中断次数直接少算

read:用户态 ↔ 内核态桥梁
复制代码
button_read → atomic_read → copy_to_user
释放资源
  1. release

    复制代码
    free_irq()
    gpio_free()
  2. exit------卸载模块

    复制代码
    device_destroy
    class_destroy
    cdev_del
    unregister_chrdev_region

应用程序

把中断的次数拿到,然后显示在屏幕上

整个程序

app.c
c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(void)
{
    int fd;
    int button_count = 0;
    int last_count = 0;

    fd = open("/dev/mybutton", O_RDWR);
    if (fd < 0)
    {
        perror("open");
        return -1;
    }

    printf("press button...\n");

    while (1)
    {
        if (read(fd, &button_count, sizeof(button_count)) > 0)
        {
            if (button_count != last_count)
            {
                printf("button count = %d\n", button_count);
                last_count = button_count;
            }
        }
        // usleep(100 * 1000); // 100ms
    }

    close(fd);
    return 0;
}
driver.c
c 复制代码
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/io.h>
#include <linux/device.h>
#include <linux/irq.h>
#include <linux/of_irq.h>
#include <linux/types.h>

#define DEVICE_CLASS_NAME "mybutton_class"
struct cdev mycdev;
static int major_num,minor_num;
struct class *class;

#define DEVICE_NUMBER 1
#define DEVICE_MINOR_NUMBER 0



struct device_node *button_device_node=NULL;
unsigned button_GPIO_number=0;
u32 interrupt_number=0;
atomic_t button_status=ATOMIC_INIT(0);

static irqreturn_t button_irq_hander(int irq, void *dev_id)
{
    atomic_inc(&button_status);
    printk("button irq, count=%d\n",
           atomic_read(&button_status));
    return IRQ_HANDLED;
}



static int button_open(struct inode *inode, struct file *filp)
{
    int error = -1;

    /*获取按键 设备树节点*/
    button_device_node = of_find_node_by_path("/button_interrupt");
    if(NULL == button_device_node)
    {
            printk("of_find_node_by_path error!");
            return -1;
    }

    /*获取按键使用的GPIO*/
    button_GPIO_number = of_get_named_gpio(button_device_node ,"button-gpios", 0);
    if(0 == button_GPIO_number)
    {
            printk("of_get_named_gpio error");
            return -1;
    }

    /*申请GPIO  , 记得释放*/
    error = gpio_request(button_GPIO_number, "button_gpio");
    if(error < 0)
    {
            printk("gpio_request error");
            gpio_free(button_GPIO_number);
            return -1;
    }

    error = gpio_direction_input(button_GPIO_number);

    /*获取中断号*/
    interrupt_number = irq_of_parse_and_map(button_device_node, 0);
    printk("\n irq_of_parse_and_map! =  %d \n",interrupt_number);

    /*申请中断, 记得释放*/
    error=request_irq(interrupt_number,button_irq_hander,IRQF_TRIGGER_FALLING, "button_interrupt", NULL  );    
        
    // IRQ 号(Linux 虚拟中断)
    // 触发类型(这里从 DT 来) 因为已经在设备树里配置了触发方式(上升沿 / 下降沿 / 电平)
    // 中断名字(/proc/interrupts 可见)
    // dev_id(free_irq 要用)
    if(error != 0)
    {
        printk("request_irq error");
        free_irq(interrupt_number, NULL);
        return -1;
    }


    return 0;
}

static ssize_t button_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt)
{
    int button_counter;

    button_counter = atomic_read(&button_status);

    if (copy_to_user(buf, &button_counter, sizeof(button_counter)))
    {
        printk("copy_to_user error\n");
        return -EFAULT;
    }

    return sizeof(button_counter);   
}


/*字符设备操作函数集,.release函数实现*/
static int button_release(struct inode *inode, struct file *filp)
{ 
	/*释放申请的引脚,和中断*/
	printk("button_release\n");
    //卸载模块前必须释放中断资源,防止中断处理函数访问已释放的内存
	free_irq(interrupt_number, NULL);
	gpio_free(button_GPIO_number);
	return 0;
}




static struct file_operations button_fops={
    .open=button_open,
    .read=button_read,
    .release=button_release
};

static int __init button_init(void){

    int ret;
    dev_t dev_num;//设备号
    
    printk("button_init\n");
    
     /*动态注册设备号*/
    ret=alloc_chrdev_region(&dev_num, DEVICE_MINOR_NUMBER, 1, "mybutton");
    if (ret < 0){
        printk("alloc_chrdev_region error\n");
    }
    printk("alloc_chrdev_region ok\n"); //动态注册设备号成功


    major_num = MAJOR(dev_num);//将主设备号取出来
    minor_num = MINOR(dev_num);//将次设备号取出来
    printk("major_num = %d\n", major_num); //打印传入进来的主设备号
    printk("minor_num = %d\n", minor_num); //打印传入进来的次设备号
    
    mycdev.owner=THIS_MODULE;
    cdev_init(&mycdev, &button_fops); //cdev_init 函数初始化 cdev 结构体成员变量
    cdev_add(&mycdev, dev_num, DEVICE_NUMBER);//完成字符设备注册到内核

  
    class = class_create(THIS_MODULE, DEVICE_CLASS_NAME);// 创建 class 类
    device_create(class,NULL,MKDEV(major_num,minor_num),NULL,"mybutton");

    return 0;
}

static void __exit button_exit(void){

    
    device_destroy(class, MKDEV(major_num, minor_num));//删除 /dev 节点
    class_destroy(class);//删除类
    cdev_del(&mycdev);//删除设备号
    unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER); //释放设备号

}

module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");

【拓】MSI Controller

对于前面的its0,这边去做了一个探究,想知道这是做什么的。

复制代码
its0: msi-controller@fe640000 {
    compatible = "arm,gic-v3-its";
    msi-controller;
    #msi-cells = <1>;
    reg = <0x0 0xfe640000 0x0 0x20000>;
};

MSI控制器

MSI 控制器 = 管理"通过写内存触发中断"的中断控制器

不靠中断引脚 ,而是靠 内存写事务 来触发中断。

传统中断(IRQ 线):外设 ------IRQ线------> GIC ------> CPU

特点:

  • 一根线一个中断
  • 电平 / 边沿
  • 数量有限
  • 容易共享、冲突

MSI(Message Signaled Interrupt):外设 ------写内存------> MSI 控制器 ------> GIC ------> CPU

👉 中断 = 一次特殊的内存写操作

写的是:

  • 指定地址
  • 指定数据(vector / event id)
MSI 控制器负责三件事:
  1. 接收设备发来的 MSI 内存写
  2. 解析里面的中断 ID(vector / EventID)
  3. 转换成 CPU 能理解的中断并投递

MSI 控制器是用于管理 Message Signaled Interrupt 的中断控制器,负责将外设通过内存写产生的 MSI 请求转换为 CPU 可识别的中断并进行分发,典型实现是在 ARM GICv3 中的 ITS 模块。

什么时候需要MSI控制器

当系统中存在 PCIe 等支持 MSI/MSI-X 的高速设备时必须要 MSI 控制器;传统 GPIO、UART 等外设不需要。

PCIe 设备

比如:

  • PCIe 网卡
  • NVMe SSD
  • USB3 控制器
  • AI / NPU / GPU
  • 高速采集卡

原因:

  • PCIe 协议原生支持 MSI / MSI-X
  • 中断数量多(几十~几千)
  • 性能要求高

没有 MSI 控制器,这些设备"跑得起来但收不到中断"

ITS

1️⃣ 什么是ITS

ITS(Interrupt Translation Service)是 ARM GICv3 中用于处理 MSI 中断的模块,负责将外设通过内存写方式产生的 MSI 事件翻译为 GIC SPI 中断,并投递给指定的 CPU。

2️⃣为什么需要ITS

因为 MSI 中断【MSI 控制器 = 管理"通过写内存触发中断"的中断控制器】不是通过物理中断线触发,GIC 本身无法直接识别,需要 ITS 进行事件 ID 到中断号的映射和翻译。

为什么非它不可

MSI 的本质是:

复制代码
设备 → 写内存(Message) → 触发中断

而 CPU/GIC 能理解的是:

复制代码
IRQ 号 → 中断优先级 → 投递到 CPU

中间差的这一步,就是 ITS 干的

ITS 在整个系统里的位置
复制代码
PCIe 设备
   ↓(MSI 内存写)
ITS(中断翻译)
   ↓(SPI 中断)
GICD / GICR
   ↓
CPU
ITS 到底"翻译"了什么?

ITS 维护了几张关键映射表

1️⃣ Device Table

  • 设备 ID(PCIe BDF)
  • 这个设备属于哪个 ITS

2️⃣ Event Table

  • MSI Event ID
  • 每个 Event 对应一个中断

3️⃣ Interrupt Translation Table(ITT)

  • Event ID → GIC SPI
  • Event ID → 目标 CPU
  • Event ID → 优先级

👉 设备写内存时,只写一个 Event ID

👉 ITS 决定这对应哪个 IRQ

一次 MSI 中断完整发生过程

1️⃣ 系统启动

  • Linux 解析 DTS
  • 初始化 GIC
  • 初始化 ITS
  • 建立 MSI irq domain

2️⃣ PCIe 设备枚举

复制代码
pci_scan_root_bus()
↓
pci_enable_msi()
↓
pci_alloc_irq_vectors()

内核此时:

  • 给设备分配 Event ID
  • 在 ITS 中建立映射关系

3️⃣ 设备触发中断

复制代码
设备 → DMA 写 MSI 地址
      写的数据 = Event ID

4️⃣ ITS 翻译

复制代码
Event ID
↓
查 ITS 表
↓
得到 SPI 中断号
↓
投递给 GIC

5️⃣ CPU 响应

复制代码
GIC → CPU exception
↓
do_IRQ()
↓
驱动中断处理函数
ITS 和 GICD / GICR 的分工
模块 负责什么
GICD 全局 SPI 中断管理
GICR 每 CPU 的 PPI / SGI
ITS MSI → SPI 的翻译

👉 ITS 不直接打 CPU

👉 它只是把 MSI 转成 GIC 能识别的形式

【拓】 Rockchip RK3588 芯片的技术参考手册(TRM)中关于 GIC600(通用中断控制器)的概述和配置参数表


后续更新...

这边是更多关于gic相关的内容,见手册

相关推荐
LingLong_roar2 小时前
手搓温湿度传感器(单片机普冉PY32F002AF15P6TU + 温湿度传感器 SHT40-AD1B-R2 + 0.96寸TFT IPS 显示屏)
单片机·嵌入式硬件
蒸蒸yyyyzwd2 小时前
DDIA学习笔记
笔记·学习
LYS_06182 小时前
寒假学习(14)(HAL库5)
java·linux·学习
2501_901147832 小时前
学习笔记:基于摩尔投票法的高性能实现与工程实践
笔记·学习·算法·性能优化
神一样的老师2 小时前
【ELF2学习开发板】Linux 命令行读取 MPU6050 传感器数据(I2C 总线)实战
linux·运维·学习
郝学胜-神的一滴2 小时前
Linux网络编程中的connect函数:深入探索网络连接的基石
linux·服务器·网络·c++·websocket·程序人生
春日见2 小时前
window wsl环境: autoware有日志,没有rviz界面/ autoware起不来
linux·人工智能·算法·机器学习·自动驾驶
jmxwzy2 小时前
Linux常用命令
linux
不做无法实现的梦~2 小时前
思翼mk32遥控器配置图传和数传教程
linux·嵌入式硬件·机器人·自动驾驶