Skip to main content

Linux 中断介绍及实践

1. 中断基础概念

中断是现代计算机系统中一种关键的硬件通信机制,允许外部设备通过发送信号打断CPU当前的执行流程,使CPU及时处理外部事件。在裸机系统中,中断处理通常包括中断初始化、注册中断服务函数和中断处理三个步骤。而在Linux内核中,提供了一套完整的中断处理框架,使开发者可以更便捷地使用中断功能。

1.1. 中断工作流程


  1. 硬件产生中断:当硬件设备(如键盘)需要通知处理器时,它会生成一个中断信号。

  2. 中断控制器:中断控制器接收到中断信号后,将信号发送给处理器。

  3. 处理器响应:处理器接收到中断信号后,会中断当前正在执行的内核代码,转而处理中断。

  4. 架构相关C函数:处理器调用一个与硬件架构相关的C函数(如do_IRQ()),这个函数负责处理中断。

  5. 检查中断处理程序:do_IRQ()函数会检查是否有中断处理程序(interrupt handler)在当前中断线上注册。

  6. 执行中断处理程序

    • 如果有对应的设备驱动注册了中断处理器,就调用 handle_irq_event(),运行实际的处理逻辑。

    • 如果没有处理程序,就调用 ret_from_intr(),直接返回原本中断的代码继续执行。

1.2. 上半部与下半部

Linux内核将中断处理分为上半部和下半部两个阶段:

  • 上半部:即中断处理函数,负责处理对时间敏感、执行速度快的操作

  • 下半部:用于处理耗时较长的操作,以确保中断处理函数能快进快出

2. 中断编程接口

2.1. 中断号管理

每个中断都有一个唯一的中断号,用于区分不同的中断源。在Linux内核中,使用一个整型变量表示中断号。

2.2. 中断注册API

int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);

此函数用于申请中断,参数说明:

irq:要申请的中断号

handler:中断处理函数

flags:中断标志,常用标志如下:

  • IRQF_SHARED                     多个设备共享一个中断线

  • IRQF_ONESHOT                  单次中断,中断执行一次就结束

  • IRQF_TRIGGER_RISING     上升沿触发

  • IRQF_TRIGGER_FALLING  下降沿触发

  • IRQF_TRIGGER_HIGH         高电平触发

  • IRQF_TRIGGER_LOW          低电平触发

name:中断名称,可在interrupts文件中看到

dev:用于区分共享中断的设备标识

2.3. 中断控制函数

void free_irq(unsigned int irq, void *dev);

用于释放已申请的中断,参数说明:

irq:要释放的中断号

dev:如果是共享中断,用于识别具体哪个中断处理函数

2.4. 中断处理函数

irqreturn_t (*irq_handler_t)(int irq, void *dev_id);

中断处理函数的返回值类型为irqreturn_t,有三种可能的返回值:

IRQ_NONE:表示不是此设备的中断

IRQ_HANDLED:表示中断已成功处理

IRQ_WAKE_THREAD:表示需要唤醒线程进行后续处理

2.5. 中断使能与禁止函数

void enable_irq(unsigned int irq);
void disable_irq(unsigned int irq);
void disable_irq_nosync(unsigned int irq);

enable_irq:使能指定中断

disable_irq:禁止指定中断,会等待当前正在执行的中断处理函数完成

disable_irq_nosync:禁止指定中断,立即返回不等待当前中断处理完成

处理全局中断控制的函数:

local_irq_enable();
local_irq_disable();

安全地保存和恢复中断状态的函数:

local_irq_save(flags);
local_irq_restore(flags);

3. 中断下半部机制

3.1. 软中断

软中断是Linux内核提供的一种下半部机制,定义在path/to/your/linux-6.6/include/linux/interrupt.h中:

struct softirq_action {
void (*action)(struct softirq_action *);
};

Linux内核定义了10种软中断类型,包括高优先级软中断、定时器软中断、网络发送软中断等。

使用软中断的API函数:

void open_softirq(int nr, void (*action)(struct softirq_action *));
void raise_softirq(unsigned int nr);

需要注意的是,软中断必须在编译时静态注册。

3.2. Tasklet

tasklet是基于软中断实现的另一种下半部机制,更适合驱动开发使用:

struct tasklet_struct {
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};

使用tasklet的API函数:

void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long),
unsigned long data);
void tasklet_schedule(struct tasklet_struct *t);

也可以使用宏一次性完成创建和初始化:

DECLARE_TASKLET(name, func, data);

tasklet使用示例:

/* 定义tasklet */
struct tasklet_struct testtasklet;

/* tasklet处理函数 */
void testtasklet_func(unsigned long data)
{
/* tasklet具体处理内容 */
}

/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
/* 调度tasklet */
tasklet_schedule(&testtasklet);
return IRQ_HANDLED;
}

/* 驱动入口函数 */
static int __init xxxx_init(void)
{
/* 初始化tasklet */
tasklet_init(&testtasklet, testtasklet_func, data);

/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);

return 0;
}

3.3. 工作队列

工作队列是Linux内核提供的另一种下半部机制,它在进程上下文中执行,允许睡眠或重新调度:

struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 工作队列处理函数 */
};

使用工作队列的API函数:

INIT_WORK(_work, _func);
DECLARE_WORK(n, f);
bool schedule_work(struct work_struct *work);

工作队列使用示例:

/* 定义工作 */
struct work_struct testwork;

/* work处理函数 */
void testwork_func_t(struct work_struct *work)
{
/* work具体处理内容 */
}

/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
/* 调度work */
schedule_work(&testwork);
return IRQ_HANDLED;
}

/* 驱动入口函数 */
static int __init xxxx_init(void)
{
/* 初始化work */
INIT_WORK(&testwork, testwork_func_t);

/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);

return 0;
}

4. 设备树中断配置

4.1. 中断信息节点

在设备树中,通过特定的属性来描述中断信息:

配置GPIO中断

path/to/your/linux-6.6/arch/riscv/boot/dts/spacemit/k1-x.dtsi 找到 gpio 控制器节点,如下:

gpio: gpio@d4019000 {
compatible = "spacemit,k1x-gpio";
reg = <0x0 0xd4019000 0x0 0x800>;
gpio-controller;
#gpio-cells = <2>;
interrupts = <58>;
clocks = <&ccu CLK_GPIO>;
interrupt-names = "gpio_mux";
interrupt-parent = <&intc>;
interrupt-controller;
#interrupt-cells = <2>;

gcb0: gpio0 {
reg-offset = <0x0>;
};

gcb1: gpio1 {
reg-offset = <0x4>;
};

gcb2: gpio2 {
reg-offset = <0x8>;
};

gcb3: gpio3 {
reg-offset = <0x100>;
};
};

gpio 控制器已经声明了 interrupt-controller#interrupt-cells ,我们只需要定义 gpios 属性,驱动会自动处理中断,无需手写 interrupts

4.2. 获取中断号

在驱动开发中,有两种常用方法获取中断号:

4.2.1. 从设备树中获取

unsigned int irq_of_parse_and_map(struct device_node *dev, int index);

dev:设备节点

index:索引号,用于选择特定的中断信息

返回值:中断号

4.2.2. 从GPIO获取

int gpio_to_irq(unsigned int gpio);

gpio:GPIO编号

返回值:对应的中断号

5. 中断驱动实践

说明:本次实验采用 GPIO73 引脚为例。

5.1. 设备树配置

path/to/your/linux-6.6/arch/riscv/boot/dts/spacemit/k1-x_MUSE-Pi-Pro.dts 根节点下添加:

/{
...
keys {
compatible = "my-gpio-keys";
key0 {
label = "user-key";
linux,code = <KEY_ENTER>;
gpios = <&gpio 73 GPIO_ACTIVE_LOW>;
};
};
};

启动 Docker。

docker start cross-build-user
docker exec -it cross-build-user bash

进入 linux-6.6 目录重新编译 dtb 文件。

cd cross-builder/linux-6.6
make dtbs

在 MUSE Pi Pro 上执行下面命令查看 ip:

ifconfig

如下图所示,ip 为 10.0.91.67


执行下面命令将编译好的 k1-x_MUSE-Pi-Pro.dtb 文件传输到 MUSE Pi Pro 板子上。

:下面的 bianbu@10.0.91.67 根据上面结果替换成对应的 ip 。

sudo scp arch/riscv/boot/dts/spacemit/k1-x_MUSE-Pi-Pro.dtb bianbu@10.0.91.67:/home/bianbu

首次通过 ssh 连接 MUSE Pi Pro 会弹出连接确认,键盘输入 yes ,接着再输入密码回车即可。


在 MUSE Pi Pro 上执行以下命令进行替换:

sudo cp ~/k1-x_MUSE-Pi-Pro.dtb /boot/spacemit/6.6.63

将 MUSE Pi Pro 进行重启:

sudo reboot

5.2. 驱动核心代码

// 获取key子节点GPIO描述符
struct fwnode_handle *fwnode = of_fwnode_handle(child);
gpio_key.gpiod = fwnode_gpiod_get_index(fwnode, NULL, 0, GPIOD_IN, gpio_key.label);

// 获取中断号
gpio_key.irq = gpiod_to_irq(gpio_key.gpiod);

// 注册中断,双沿触发
err = request_irq(gpio_key.irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, gpio_key.label, &gpio_key);

// 中断函数
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
struct gpio_key *key = dev_id;
int val = gpiod_get_value(key->gpiod);
// 打印调试信息
printk("key %s(code=%d) %d\n", key->label, key->code, val);
// 组合按键码和电平
g_key = (key->code << 8) | val;
// 唤醒等待队列
wake_up_interruptible(&gpio_key_wait);
return IRQ_HANDLED;
}

5.3. 编译测试

5.3.1. 下载源码压缩包

irq_test.zip

补充:源码文件具体作用请看压缩包里的 README.md 文件。

解压

unzip 04_irq_test.zip -d ~/

编译

驱动文件需要在Ubuntu上进行编译,因为SDK在Ubuntu上。修改Makefile里的内核路径为实际路径。

cd ~/04_irq_test
vim Makefile

修改KERN_DIR 的路径,如图所示:


执行以下命令,编译驱动

make

编译成功以后就会生成一个名为 irq_drv.ko 的驱动模块文件。


5.3.2. 加载驱动

将上一小节编译好的 04_irq_test 文件夹拷贝到 MUSE Pi Pro 开发板上,执行下面命令。

注意:下面的 bianbu@10.0.91.67 根据上面结果替换成对应的 ip 。

scp -r ~/04_irq_test/ bianbu@10.0.91.67:/home/bianbu

在开发板上进入该目录并输入如下命令加载 irq_drv.ko 这个驱动模块。

cd 04_irq_test
sudo insmod irq_drv.ko

5.3.3. 运行测试

将按键一端接到 GPIO73 ,另一端接到 GND


当驱动模块加载成功以后使用 irq_test 来测试,输入如下命令:

sudo ./irq_test /dev/key_irq_test

按下/松开按键时可看到中断被触发。如图所示: