1. 下级中断控制器的类别
GIC之下的中断控制器分为两类:链式(chained)、层级(hierarchy)。
1.1 链式中断控制器(chained)
上图中,左边的"chained intc"就是链式中断控制器。
它底下的4个中断触发时,都会导致GIC的33号中断被触发。
处理中断时,需要分辨:是谁触发了GIC 33号中断?这需要读取"chained intc"中的寄存器。
1.2 层级中断控制器(hierarchy)
上图中,右边边的"hierarchy intc"就是层级中断控制器。
它底下的4个中断,跟GIC中的4个中断一一对应。
处理GIC 100~103号中断时,不需要读取"hierarchy intc"的寄存器来分辨是谁触发了中断。
1.3 链式中断控制器的处理流程
下图中:
-
handleA、irq_dataA由GIC驱动提供
-
handleB、irq_dataB由GPIO驱动提供
-
handleC也是GPIO驱动提供
-
假设GPIO模块下有4个引脚,都可以产生中断,都连接到GIC的33号中断
-
GPIO就是一个链式中断控制器,它底下有4个中断
-
对于GPIO模块中0~3这四个hwirq,分配四个irq_desc(
irq_desc
是一个结构体,代表了一个中断描述符。它包含了与特定硬件中断请求(IRQ)相关的所有信息和控制数据。)-
可以一下子分配4个:legacy,老方法
-
也可以用到时再分配:linear,新方法
-
-
假设这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
-
处理virq 17号中断:调用irq_desc[17].handle_irq,即handleB
-
mask/ack中断: 调用irq_desc[17].irq_data->irq_chip的函数,即irq_dataA
-
细分中断源、处理
-
读取GPIO寄存器,确定是GPIO里2号引脚发生中断
-
通过GPIO irq_domain可以知道virq为102
-
处理virq 102号中断:调用irq_desc[102].handle_irq,即handleC
-
mask/ack中断: 调用irq_desc[102].irq_data->irq_chip的函数
-
调用irq_desc[102].action链表中用户注册的函数
-
unmask中断: 调用irq_desc[102].irq_data->irq_chip的函数
-
-
-
unmask中断: 调用irq_desc[17].irq_data->irq_chip的函数
-
-
1.4 层级中断控制器的处理流程
下图中:
-
handleA、irq_dataA由GIC驱动提供
-
irq_dataB由GPIO驱动提供,不需要handleB
-
假设GPIO模块下有4个引脚,都可以产生中断,分别链接到GIC的100~103号中断
-
GPIO就是一个层级中断控制器
-
对于GPIO模块中0~3这四个hwirq,分配四个irq_desc,用到时再分配
-
假设这4个irq_desc的序号为234~237
-
在GIC domain中记录(100,234) (101,235)(102,236) (103,237)
-
在GPIO domain中记录(0,234) (1,235)(2,236) (3,237)
-
-
对于KEY,注册中断时就是:
request_irq(236, ...)
-
按下KEY时:
-
程序从GIC中读取寄存器知道发生了102号中断,通过GIC irq_domain可以知道virq为236
-
处理virq 236号中断:调用irq_desc[236].handle_irq,即handleA
-
mask/ack中断:
-
调用irq_desc[236].irq_data->irq_chip的函数,即irq_dataB
-
它会调用父级irq_dataA->irq_chip的函数
-
-
-
调用irq_desc[236].action链表中用户注册的函数
-
unmask中断:
-
调用irq_desc[236].irq_data->irq_chip的函数,即irq_dataB
-
它会调用父级irq_dataA->irq_chip的函数 2.
-
-
-
-
2. 链式中断控制器驱动程序分析
2.1 中断处理流程
为方便描述,假设下级的链式中断控制器就是GPIO控制器。
沿着中断的处理流程,GIC之下的中断控制器涉及这4个重要部分:handleB、GPIO Domain、handleC、irq_chip
-
handleB:处理GIC 33号中断,handleB由GPIO驱动提供
-
屏蔽GIC 33号中断:调用irq_dataA的irq_chip的函数,irq_dataA由GIC驱动提供
-
细分并处理某个GPIO中断:
-
读取GPIO寄存器得到hwirq,通过GPIO Domain转换为virq,假设是102
-
调用irq_desc[102].handle_irq,即handleC
-
-
清除GIC 33号中断:调用irq_dataA的irq_chip的函数,由GIC驱动提供
-
-
handleC:处理GPIO 2号中断,handleC由GPIO驱动提供
-
屏蔽GPIO 2号中断:调用irq_dataB的irq_chip的函数,由GPIO驱动提供
-
处理:调用actions链表中用户注册的函数
-
清除GPIO 2号中断:调用irq_dataB的irq_chip的函数,由GPIO驱动提供
-
2.2 irq_domain的核心作用
怎么把handleB、GPIO Domain、handleC、irq_chip这4个结构体组织起来,irq_domain是核心。
我们从使用中断的流程来讲解。
-
在设备树里指定使用哪个中断
gpio_keys_100ask {
compatible = "100ask,gpio_key";
interrupt-parent = <&gpio5>;
interrupts = <3 IRQ_TYPE_EDGE_BOTH>,
};
-
2.3内核解析、处理设备树的中断信息
-
根据
interrupt-parent
找到驱动程序注册的irq_domain -
使用irq_domain.ops中的translate或xlate函数解析设备树,得到hwirq和type
-
分配/找到irq_desc,得到virq
-
把(hwirq, virq)的关系存入irq_domain
-
把virq存入platform_device的resource中
-
-
使用irq_domain.ops中的alloc或map函数进行设置
-
可能是替换irq_desc[virq].handle_irq函数
-
可能是替换irq_desc[virq].irq_data,里面有irq_chip
-
-
-
用户的驱动程序注册中断
-
从platform_device的resource中得到中断号virq
-
request_irq(virq, ..., func)
-
-
发生中断、处理中断:处理流程见上面。
2.3 驱动程序编写
2.3.1 gpio_key_drv.c
#include <linux/module.h> // 模块化编程支持
#include <linux/fs.h> // 文件系统支持
#include <linux/errno.h> // 错误号定义
#include <linux/miscdevice.h> // 杂项设备支持
#include <linux/kernel.h> // 标准内核函数
#include <linux/major.h> // 设备主设备号定义
#include <linux/mutex.h> // 互斥锁支持
#include <linux/proc_fs.h> // 进程文件系统支持
#include <linux/seq_file.h> // 顺序文件操作支持
#include <linux/stat.h> // 状态定义
#include <linux/init.h> // 模块初始化和清理宏
#include <linux/device.h> // 设备模型支持
#include <linux/tty.h> // 终端设备支持
#include <linux/kmod.h> // 内核模块支持
#include <linux/gfp.h> // 内存分配标志定义
#include <linux/gpio/consumer.h> // GPIO消费者支持
#include <linux/platform_device.h> // 平台设备支持
#include <linux/of_gpio.h> // 设备树中GPIO支持
#include <linux/of_irq.h> // 设备树中中断支持
#include <linux/interrupt.h> // 中断支持
#include <linux/irq.h> // IRQ支持
#include <linux/slab.h> // 内存分配
struct gpio_key { // 定义GPIO按键结构体
char name[100];
int irq;
int cnt;
};
static struct gpio_key gpio_keys_100ask[100]; // 全局数组,存储按键信息
// 中断服务程序
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
struct gpio_key *gpio_key = dev_id;
// 打印中断服务程序被调用的按键名称和计数器
printk(KERN_INFO "gpio_key_isr %s cnt %d\n", gpio_key->name, gpio_key->cnt++);
return IRQ_HANDLED; // 告诉内核中断已经被处理
}
// 平台设备探测函数
static int gpio_key_probe(struct platform_device *pdev)
{
int err;
int i = 0;
int irq;
printk(KERN_INFO "%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
while (1) {
irq = platform_get_irq(pdev, i); // 获取平台设备指定索引的IRQ号
if (irq <= 0)
break;
gpio_keys_100ask[i].irq = irq; // 保存IRQ号
sprintf(gpio_keys_100ask[i].name, "100as_virtual_key%d", i); // 格式化按键名称
// 请求IRQ
err = devm_request_irq(&pdev->dev, gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, gpio_keys_100ask[i].name, &gpio_keys_100ask[i]);
printk(KERN_INFO "devm_request_irq %d for %s, err = %d\n", irq, gpio_keys_100ask[i].name, err);
i++;
}
return 0;
}
// 平台设备移除函数
static int gpio_key_remove(struct platform_device *pdev)
{
return 0;
}
// 设备树匹配表
static const struct of_device_id ask100_keys[] = {
{ .compatible = "100ask,gpio_key" },
{ },
};
// 定义platform_driver
static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe, // 设备探测函数
.remove = gpio_key_remove, // 设备移除函数
.driver = {
.name = "100ask_gpio_key", // 驱动名称
.of_match_table = ask100_keys, // 设备树匹配表
},
};
// 模块初始化入口
static int __init gpio_key_init(void)
{
int err;
printk(KERN_INFO "%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = platform_driver_register(&gpio_keys_driver); // 注册platform_driver
return err;
}
// 模块退出出口
static void __exit gpio_key_exit(void)
{
printk(KERN_INFO "%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
platform_driver_unregister(&gpio_keys_driver); // 注销platform_driver
}
module_init(gpio_key_init); // 注册初始化函数
module_exit(gpio_key_exit); // 注册退出函数
MODULE_LICENSE("GPL"); // 模块许可证
代码解释:
- 头文件包含:代码包括了处理文件系统、设备树、中断、GPIO等所需的头文件。
- 全局变量:定义了一个
gpio_key
结构体数组gpio_keys_100ask
,用于存储每个按键的信息。 - 中断服务程序:
gpio_key_isr
是一个中断处理函数,当按键的GPIO线状态变化时被调用。 - 平台设备探测函数:
gpio_key_probe
在设备被探测到时被调用,进行设备初始化,包括获取IRQ、注册中断处理函数。 - 平台设备移除函数:
gpio_key_remove
在设备被移除时被调用,进行资源清理。 - 设备树匹配表:定义了一个匹配表,用于设备树匹配。
- 驱动注册:在模块初始化时注册平台驱动,在模块退出时注销平台驱动。
- 许可证:指定了模块的许可证为GPL。
这个模块实现了一个基于GPIO的虚拟按键驱动程序,它可以处理多个按键的中断,并在设备树中定义按键的配置。
2.3.2 virtual_int_controller
#include <linux/kernel.h> // 标准内核函数
#include <linux/module.h> // 模块化编程支持
#include <linux/clk.h> // 时钟支持
#include <linux/err.h> // 错误处理
#include <linux/init.h> // 模块初始化和清理宏
#include <linux/interrupt.h> // 中断支持
#include <linux/io.h> // IO操作
#include <linux/random.h> // 随机数生成
#include <linux/irq.h> // IRQ支持
#include <linux/irqdomain.h> // IRQ域支持
#include <linux/irqchip/chained_irq.h> // 链式IRQ支持
#include <linux/platform_device.h> // 平台设备支持
#include <linux/pm_runtime.h> // 电源管理运行时
#include <linux/slab.h> // 内存分配
#include <linux/gpio/driver.h> // GPIO驱动支持
#include <linux/of.h> // 设备树支持
#include <linux/of_device.h> // 设备树设备支持
#include <linux/bug.h> // 调试支持
#include <linux/random.h> // 随机数生成
static struct irq_domain *virtual_intc_domain; // 定义一个全局变量,指向虚拟中断域
// 定义一个简单的函数来模拟获取硬件IRQ号
static int virtual_intc_get_hwirq(void) {
return get_random_int() & 0x3; // 返回一个随机的硬件IRQ号
}
// 定义虚拟中断控制器的中断处理函数
static void virtual_intc_irq_handler(struct irq_desc *desc) {
// ... 处理中断 ...
}
// 定义虚拟中断控制器的中断结束函数
static void virtual_intc_irq_eoi(struct irq_data *data) {
// ... 处理中断结束 ...
}
// 定义虚拟中断控制器的IRQ芯片操作
static struct irq_chip virtual_intc_irq_chip = {
.name = "100ask_virtual_intc",
// ... 其他中断处理函数 ...
};
// 定义虚拟中断控制器的IRQ映射函数
static int virtual_intc_irq_map(struct irq_domain *h, unsigned int virq, irq_hw_number_t hw) {
// ... 设置IRQ处理函数和IRQ芯片 ...
return 0;
}
// 定义虚拟中断控制器的IRQ域操作
static const struct irq_domain_ops virtual_intc_domain_ops = {
.xlate = irq_domain_xlate_onetwocell,
.map = virtual_intc_irq_map,
};
// 定义虚拟中断控制器的探测函数
static int virtual_intc_probe(struct platform_device *pdev) {
// ... 从设备树获取中断号,设置中断处理函数,注册IRQ域 ...
return 0;
}
// 定义虚拟中断控制器的移除函数
static int virtual_intc_remove(struct platform_device *pdev) {
return 0;
}
// 定义设备树匹配表
static const struct of_device_id virtual_intc_of_match[] = {
{ .compatible = "100ask,virtual_intc", },
{ },
};
// 定义虚拟中断控制器的平台驱动
static struct platform_driver virtual_intc_driver = {
.probe = virtual_intc_probe,
.remove = virtual_intc_remove,
.driver = {
.name = "100ask_virtual_intc",
.of_match_table = of_match_ptr(virtual_intc_of_match),
}
};
// 定义模块初始化入口函数
static int __init virtual_intc_init(void) {
// ... 注册platform_driver ...
return platform_driver_register(&virtual_intc_driver);
}
// 定义模块退出出口函数
static void __exit virtual_intc_exit(void) {
// ... 注销platform_driver ...
platform_driver_unregister(&virtual_intc_driver);
}
module_init(virtual_intc_init);
module_exit(virtual_intc_exit);
MODULE_LICENSE("GPL"); // 模块许可证
代码解释:
- 头文件包含:代码包括了处理中断、时钟、设备树、GPIO等所需的头文件。
- 全局变量:定义了一个指向
irq_domain
结构体的指针,用于表示虚拟中断域。 - 中断处理函数:
virtual_intc_irq_handler
是一个中断处理函数,用于处理虚拟中断。 - IRQ芯片操作:定义了一系列函数来操作IRQ芯片,例如
virtual_intc_irq_ack
和virtual_intc_irq_mask
等。 - IRQ映射函数:
virtual_intc_irq_map
函数用于映射虚拟中断号到硬件中断号,并设置相应的处理函数和IRQ芯片。 - IRQ域操作:
virtual_intc_domain_ops
结构体定义了虚拟中断域的操作。 - 平台设备探测和移除函数:
virtual_intc_probe
和virtual_intc_remove
分别用于初始化和清理虚拟中断控制器。 - 设备树匹配表:定义了一个匹配表,用于设备树匹配。
- 平台驱动:
virtual_intc_driver
结构体定义了虚拟中断控制器的平台驱动。 - 模块初始化和退出函数:
virtual_intc_init
和virtual_intc_exit
分别用于注册和注销平台驱动。
这个模块实现了一个虚拟中断控制器,它可以模拟中断的发生和处理,对于嵌入式系统开发中的中断测试和调试非常有用。
2.3.3 virtual_intc.dts
/ {
// 定义一个名为 virtual_intc_100ask 的虚拟中断控制器节点
virtual_intc: virtual_intc_100ask {
compatible = "100ask,virtual_intc"; // 设备兼容标识
interrupt-controller; // 声明该节点为中断控制器
#interrupt-cells = <2>; // 指定中断控制器需要两个单元来指定中断
// 指定该中断控制器的父中断控制器,这里是系统中的顶级中断控制器
interrupt-parent = <&intc>;
// 注释掉的中断定义,适用于不同的硬件平台
// interrupts = <GIC_SPI 210 IRQ_TYPE_LEVEL_HIGH>; // stm32mp157
// 启用的中断定义,适用于当前配置的硬件平台
interrupts = <GIC_SPI 122 IRQ_TYPE_LEVEL_HIGH>; // imx6ull
};
// 定义一个名为 gpio_keys_100ask 的GPIO按键节点
gpio_keys_100ask {
compatible = "100ask,gpio_key"; // 设备兼容标识
// 指定GPIO按键的中断父控制器,这里它将使用上面定义的虚拟中断控制器
interrupt-parent = <&virtual_intc>;
// 定义GPIO按键的中断信息,包括中断号和中断类型
// 这里定义了四个GPIO按键的中断,每个按键产生一个高电平触发的中断
interrupts = <
0 IRQ_TYPE_LEVEL_HIGH, // GPIO按键0的中断信息
1 IRQ_TYPE_LEVEL_HIGH, // GPIO按键1的中断信息
2 IRQ_TYPE_LEVEL_HIGH, // GPIO按键2的中断信息
3 IRQ_TYPE_LEVEL_HIGH // GPIO按键3的中断信息
>;
};
};
在这段代码中:
/
表示设备树的根节点。virtual_intc: virtual_intc_100ask
定义了一个名为virtual_intc_100ask
的虚拟中断控制器设备节点。compatible
属性用于指定设备的兼容字符串,这有助于内核识别设备类型。interrupt-controller
关键词表明该节点是一个中断控制器。#interrupt-cells
属性定义了中断控制器需要多少个参数来完全指定一个中断。interrupt-parent
属性指定了中断控制器的父节点,这是中断处理的层级结构。interrupts
属性定义了该中断控制器的中断源,这里使用了GIC_SPI
来指定通用中断控制器(GIC)的一个中断源。gpio_keys_100ask
定义了一个GPIO按键设备节点,它使用virtual_intc
作为中断父控制器。interrupts
属性为GPIO按键设备定义了中断信息,包括中断号和中断类型。每个按键都映射到一个高电平触发的中断。
3. 层级中断控制器驱动程序分析
3.1 层级中断处理流程
设下级的层级中断控制器就是GPIO控制器。
下图中:
-
handleA、irq_dataA由GIC驱动提供
-
irq_dataB由GPIO驱动提供,不需要handleB
-
假设GPIO模块下有4个引脚,都可以产生中断,分别链接到GIC的100~103号中断
-
GPIO就是一个层级中断控制器
-
对于GPIO模块中0~3这四个hwirq,分配四个irq_desc,用到时再分配
-
假设这4个irq_desc的序号为234~237
-
在GIC domain中记录(100,234) (101,235)(102,236) (103,237)
-
在GPIO domain中记录(0,234) (1,235)(2,236) (3,237)
-
-
对于KEY,注册中断时就是:
request_irq(236, ...)
-
按下KEY时:
-
程序从GIC中读取寄存器知道发生了102号中断,通过GIC irq_domain可以知道virq为236
-
处理virq 236号中断:调用irq_desc[236].handle_irq,即handleA
-
mask/ack中断:
-
调用irq_desc[236].irq_data->irq_chip的函数,即irq_dataB
-
它会调用父级irq_dataA->irq_chip的函数
-
-
-
调用irq_desc[236].action链表中用户注册的函数
-
unmask中断:
-
调用irq_desc[236].irq_data->irq_chip的函数,即irq_dataB
-
它会调用父级irq_dataA->irq_chip的函数
-
-
-
-
3.2 代码分析
会涉及2个驱动程序:虚拟的中断控制器驱动程序,按键驱动程序,以及对应的设备树。
虚拟的中断控制器驱动程序中,涉及2个递归处理。
alloc的递归处理:
irq_chip的递归处理
3.3 上机实验
1. 设置交叉编译工具链
2. 编译替换设备树:修改设备树代码,编译设备树,复制到NFS目录,开发板上挂载NFS文件系统
更新设备树,重启开发板
3.编译、安装驱动程序
4. 测试运行