10.1 概述
中断是计算机系统中一种重要的机制,用于处理异步事件。在 Linux 驱动程序中,中断处理是一个关键部分,它允许硬件设备在需要时通知 CPU 进行处理。本章将讨论AARCH64 GIC-v3架构, 在Linux 驱动程序中的中断设计,并提供一个测试用例来演示如何实现和测试中断处理。
10.2 Linux中断架构
这里所讨论的平台都是aarch64。 Linux kernel 5.10.110使用的中断控制器是GIC-v3版本, 框图如下:
通用中断控制器(GIC)从外设接收中断,优先处理这些中断,并将它们传递到适当的处理器核心。
10.3 GICv3的核心概念
中断(Interrupt): 是指当硬件设备需要CPU注意时发出的信号。比如,一个网络接口收到了数据包或者一个计时器溢出,设备会通过中断通知CPU。
中断控制器(Interrupt Controller): 在复杂的系统中,可能会有很多设备发出中断信号。中断控制器的工作是集中管理这些信号,并根据优先级将它们分配给CPU处理。GICv3 就是ARM体系结构中的中断控制器。
物理中断(Physical Interrupt)和虚拟中断(Virtual Interrupt)
物理中断: 硬件设备直接触发,CPU响应处理。
虚拟中断: 在虚拟化环境中,虚拟机通过虚拟中断来接收和处理中断。
中断优先级(Interrupt Priority): GICv3允许对中断进行优先级分配,优先级高的中断会先被处理。
10.4 中断类型
GICv3(Generic Interrupt Controller version 3)中断向量号的定义和分类如下:
1). SGI(Software Generated Interrupts):
• 范围:0-15
• 用途:用于处理器间通信,通常由软件生成。
2). PPI(Private Peripheral Interrupts):
• 范围:16-31
• 用途:每个处理器私有的中断,例如定时器中断。
3). SPI(Shared Peripheral Interrupts):
• 范围:32及以上
• 用途:共享外设中断,适用于多个处理器。
中断号1024 ~ 8191保留。
4.) 特定位置外设中断 (LPI):
• 范围:8192及以上
• 用途:LPI首次在GICv3中引入,其编程模型与其他三种中断类型非常不同。LPI的配置在《Arm CoreLink通用中断控制器v3和v4:特定位置外设中断指南》中有详细介绍。
10.5 GPIO 中断驱动
这里我们需要注册一个平台驱动, 在DTS里告诉kernel有一个这样的设备。 平台驱动通常是kernel无法通过系统总线获取设备信息的。
10.5.1 DTS配置
demo_gpio_irq: demo_gpio-irq {
status = "okay";
compatible = "neardi,demo_gpio-irq";
interrupt-parent = <&gpio1>;
interrupts = <RK_PB0 IRQ_TYPE_LEVEL_LOW>;
pinctrl-names = "default";
pinctrl-0 = <&demo_gpio_irq_pins>;
};
&pinctrl {
demo_gpio_irq {
demo_gpio_irq_pins: demo_gpio_irq_pins {
rockchip,pins = <1 RK_PB0 RK_FUNC_GPIO &pcfg_pull_up>;
};
};
};
10.5.2 驱动代码
这是一个基于GPIO的驱动, 目的是演示当GPIO是低电平时产生中断。 首先, 创建一个gpio_irq_driver.c文件, 内容如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/init.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#define GPIO_IRQ_PIN 40 // Pin 40 of GPIO1
static unsigned long irq_count = 0;
static unsigned int irq_number;
// Interrupt handler function
static irqreturn_t gpio_irq_handler(int irq, void *dev_id) {
irq_count++; // Count the number of interrupts
pr_info("GPIO interrupt triggered! Count: %lu\n", irq_count);
return IRQ_HANDLED; // Interrupt successfully handled
}
// Device tree match table
static const struct of_device_id gpio_irq_of_match[] = {
{ .compatible = "neardi,demo_gpio-irq", }, // Match the compatible string in DTS
{},
};
MODULE_DEVICE_TABLE(of, gpio_irq_of_match);
// Device initialization function (probe)
static int gpio_irq_probe(struct platform_device *pdev) {
int result;
/**
* platform_get_irq is a function in the Linux kernel used to retrieve the interrupt
* number (IRQ) associated with a platform device from the device tree (DTS) or platform
* data. It simplifies extracting the IRQ configuration specified in the DTS or passed
* via board files for a given platform device.
*/
irq_number = platform_get_irq(pdev, 0);
if (irq_number < 0) {
pr_err("Failed to get IRQ number from DT\n");
return irq_number;
}
pr_info("Parsed IRQ number from DT: %d\n", irq_number);
/**
* devm_request_irq is a Linux kernel function used to request an interrupt line (IRQ)
* and register an interrupt handler, with the added benefit that it is automatically
* managed (hence the devm_ prefix).
*
* The resources (such as the IRQ) are automatically freed when the driver is removed,
* reducing the likelihood of resource leaks.
*/
result = devm_request_irq(&pdev->dev, irq_number, gpio_irq_handler,
IRQF_TRIGGER_LOW, "gpio_irq_handler", NULL);
if (result) {
pr_err("Failed to request IRQ: %d\n", result);
return result;
}
pr_info("GPIO IRQ driver initialized successfully\n");
return 0; // Return 0 on successful initialization
}
// Device removal function
static int gpio_irq_remove(struct platform_device *pdev) {
pr_info("GPIO IRQ driver removed\n");
return 0;
}
// Platform driver structure
static struct platform_driver gpio_irq_driver = {
.driver = {
.name = "demo_gpio-irq",
.of_match_table = gpio_irq_of_match, // Device tree matching
},
.probe = gpio_irq_probe, // Called when the device is initialized
.remove = gpio_irq_remove, // Called when the device is removed
};
// Module initialization function
static int __init gpio_irq_init(void) {
/**
* Registers the platform driver with the platform bus,
* indicating that this driver handles certain platform devices, like GPIO interrupt.
*/
return platform_driver_register(&gpio_irq_driver);
}
// Module cleanup function
static void __exit gpio_irq_exit(void) {
platform_driver_unregister(&gpio_irq_driver);
}
module_init(gpio_irq_init);
module_exit(gpio_irq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linx zhang");
MODULE_DESCRIPTION("Demo GPIO interrupt driver for GPIO1, pin 40");
在驱动入口函数gpio_irq_probe, 首先是调用platform_get_irq() 动态地根据设备的 DTS 配置来获取 IRQ 中断号。
/**
* platform_get_irq is a function in the Linux kernel used to retrieve the interrupt
* number (IRQ) associated with a platform device from the device tree (DTS) or platform
* data. It simplifies extracting the IRQ configuration specified in the DTS or passed
* via board files for a given platform device.
*/
irq_number = platform_get_irq(pdev, 0);
if (irq_number < 0) {
pr_err("Failed to get IRQ number from DT\n");
return irq_number;
}
其次, 使用如下方法注册中断:
result = devm_request_irq(&pdev->dev, irq_number, gpio_irq_handler,
IRQF_TRIGGER_LOW, "gpio_irq_handler", NULL);
10.5.3 编译DTS
由于需要改动DTS, 因此需要在SDK环境里,重新 编译kernel即可, 如下:
wumingfeng@ubuntu2004:/samba2/home/wumingfeng/work/rk3588-linux$ ./build.sh kernel
processing option: kernel
============Start building kernel============
TARGET_ARCH =arm64
TARGET_KERNEL_CONFIG =rockchip_linux_defconfig
TARGET_KERNEL_DTS =rk3588-neardi-linux-lkd3588-f0
TARGET_KERNEL_CONFIG_FRAGMENT =
==========================================
#
# No change to .config
#
CALL scripts/atomic/check-atomics.sh
CALL scripts/checksyscalls.sh
CHK include/generated/compile.h
CC [M] drivers/misc/int.o
MODPOST modules-only.symvers
GEN Module.symvers
Image: resource.img (with rk3588-neardi-linux-lkd3588-f0.dtb logo.bmp logo_kernel.bmp) is ready
Image: boot.img (with Image resource.img) is ready
Image: zboot.img (with Image.lz4 resource.img) is ready
编译成功后, 生成了了新的boot.img文件, 需要更新到设备上。
10.5.4 编译驱动
在设备上面创建Makefile, 内容如下:
obj-m += gpio_irq_driver.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
执行编译命令即可, 如下:
neardi@LPA3588:~/int$ make
make -C /lib/modules/5.10.110/build M=/home/neardi/int modules
make[1]: Entering directory '/usr/src/linux-headers-5.10.110'
CC [M] /home/neardi/int/gpio_irq_driver.o
MODPOST /home/neardi/int/Module.symvers
CC [M] /home/neardi/int/gpio_irq_driver.mod.o
LD [M] /home/neardi/int/gpio_irq_driver.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.10.110'
10.6 测试GPIO驱动
在设备上面加载驱动, 如下:
neardi@LPA3588:~/int$ sudo insmod gpio_irq_driver.ko
neardi@LPA3588:~/int$ dmesg
[ 4451.786204] Parsed IRQ number from DT: 137
[ 4451.786267] GPIO IRQ driver initialized successfully
查看系统中断, 如下:
neardi@LPA3588:~/int$ cat /proc/interrupts | grep gpio_irq
137: 0 0 0 0 0 0 0 0 rockchip_gpio_irq 8 Level gpio_irq_handler
143: 0 0 0 0 0 0 0 0 rockchip_gpio_irq 29 Edge dw-dp-hpd
144: 0 0 0 0 0 0 0 0 rockchip_gpio_irq 7 Level rk806
可以看见中断号137就是此驱动所注册的。
10.6.1 触发中断
这里使用杜邦线, 把GPIO1 RK_PB0的Pin管脚与GND连接, 则有如下打印:
neardi@LPA3588:~/int$ dmesg
[ 260.455404] GPIO interrupt triggered! Count: 49157
[ 260.455415] GPIO interrupt triggered! Count: 49158
[ 260.455422] GPIO interrupt triggered! Count: 49159
[ 260.455430] GPIO interrupt triggered! Count: 49160
[ 260.455437] GPIO interrupt triggered! Count: 49161
从上面Log看, 瞬间就产生了49161个中断。 再查看系统里所记录的中断, 是不是与上面的中断数相符, 如下:
neardi@LPA3588:~/int$ cat /proc/interrupts | grep gpio
137: 49161 0 0 0 0 0 0 0 rockchip_gpio_irq 8 Level gpio_irq_handler
143: 0 0 0 0 0 0 0 0 rockchip_gpio_irq 29 Edge dw-dp-hpd
144: 0 0 0 0 0 0 0 0 rockchip_gpio_irq 7 Level rk806
系统记录的中断数, 与驱动程序打印的数据一致。
10.7 中断处理函数
// Interrupt handler function
static irqreturn_t gpio_irq_handler(int irq, void *dev_id) {
irq_count++; // Count the number of interrupts
pr_info("GPIO interrupt triggered! Count: %lu\n", irq_count);
return IRQ_HANDLED; // Interrupt successfully handled
}
此驱动中断处理函数gpio_irq_handler记录调用次数。如果处理时间较长的任务,会影响系统性能,导致其他中断和任务的延迟。 因此在需要长时间处理的中断函数里, 常常使用以下几种方案:
1). 分割任务:
• 将长时间运行的任务分割成多个小任务,每个小任务在不同的软中断中执行。这样可以避免单个中断占用过多时间。
2). 使用Tasklet:
• Tasklet是基于软中断的机制,但它们不能在多个CPU上并行执行。可以将长时间运行的任务分割成多个Tasklet,以便在不同的时间点执行。
3). 使用Workqueue:
• Workqueue是一种更灵活的机制,可以在进程上下文中执行任务。它允许任务在内核线程中运行,从而避免阻塞软中断处理。