linux驱动开发-中断子系统

驱动/中断子系统

异常处理流程回顾

一、ARM处理器工作模式 // 实时模式、中断模式、用户模式、系统模式

user用户模式 // 运行态
system系统模式 // 特权态
SVC管理模式  // 管理态
FIQ模式   // 快速中断
IRQ模型   // 中断请求
终止模式   // 异常终止
未定义模式  // 未定义指令
监测模式   // 监测模式
虚拟化模式  // 虚拟化模式

二、异常模式 

SVC异常模式 // 管理态
IRQ异常模式 // 中断请求
FIQ异常模式 // 快速中断
未定义异常模式 // 未定义指令
终止异常模式 // 异常终止

三、异常源的种类 

reset复位异常 // 由软件或外设引起的复位
swi软中断异常 // 由软件引起的软件中断
IRQ异常 // 由外设引起的中断请求
FIQ异常 // 由外设引起的快速中断
未定义异常 // 未定义指令
预取终止 // 预取指令导致的终止异常 
数据访问终止 // 数据访问导致的终止异常

异常处理的4大步3小步

将cpsr保存到spsr中 // 保存当前的程序状态寄存器到堆栈中
修改cpsr程序状态寄存器 // 设置异常的原因
设置为ARM的工作状态 // 设置为ARM的工作模式
修改为对应的工作模式 // 设置为对应的工作模式,如用户态、系统态、中断态等
如果有必要禁止中断 // 如果异常处理需要禁止中断,则设置相应的中断屏蔽寄存器
保存LR // 保存LR寄存器到堆栈中
设置PC到对应的异常位置执行 // 设置PC寄存器到对应的异常处理程序执行

异常返回的流程:

将spsr中的值赋值给cpsr 
让PC指向异常处理前的位置执行 

中断子系统的执行流程

保存现场 // 保存现场,包括程序状态寄存器、寄存器、堆栈等
调用中断处理程序 // 调用中断处理程序,包括中断服务程序、异常处理程序等
恢复现场 // 恢复现场,包括程序状态寄存器、寄存器、堆栈等
返回 // 返回到中断发生前的执行位置
c 复制代码
详细
1.硬件中断发生:
当外部设备(如网络卡、硬盘等)产生中断信号时,CPU会响应这个中断信号。
2.保存上下文:
CPU保存当前的执行上下文,包括程序计数器(PC)、寄存器等,以便中断处理完成后能够恢复到中断之前的状态。
3.识别中断源:
CPU通过中断控制器(如APIC)识别哪个设备产生了中断,并确定相应的中断线。
4.调用中断处理程序:
根据中断号,内核查找已注册的中断处理程序(ISR),并调用该处理程序。
5.处理中断:
中断处理程序执行具体的中断处理逻辑。
通常情况下,中断处理程序应该尽量简单和快速,避免长时间占用CPU,以免影响其他中断的处理。
6.清除中断信号:
中断处理程序完成后,通常需要清除设备的中断标志,以允许后续中断的发生。
7.结束中断处理:
中断处理程序返回一个值,指示中断是否被成功处理(如IRQ_HANDLED或IRQ_NONE)。
此后,CPU恢复之前的上下文,并继续执行被中断的任务。
8.调度其他任务:
如果在中断处理过程中触发了其他的操作(如唤醒等待的进程,或将任务推送到工作队列),
内核根据调度策略决定是否切换到其他任务。
9.共享中断处理(如有需要):
如果多个设备共享同一中断线,那么在处理完当前设备的中断后,
内核还会检查其他共享设备是否有中断需要处理。

中断子系统的API接口

c 复制代码
1. 注册中断处理程序
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
irq: 中断号。
handler: 中断处理程序函数指针。
flags: 中断标志,如 IRQF_SHARED(共享中断)、IRQF_TRIGGER_RISING(上升沿触发)等。
name: 中断名称,用于 /proc/interrupts 文件。
dev: 传递给中断处理程序的设备指针,通常是设备结构体指针。

2. 注销中断处理程序
void free_irq(unsigned int irq, void *dev);
irq: 中断号。
dev: 传递给 request_irq 的设备指针。

3. 中断处理程序函数
irqreturn_t (*irq_handler_t)(int irq, void *dev);
irq: 中断号。
dev: 传递给 request_irq 的设备指针。
返回值:IRQ_HANDLED 表示中断已处理,IRQ_NONE 表示中断未处理。

4. 禁用和启用中断
local_irq_disable();  // 禁用本地 CPU 的所有中断
local_irq_enable();   // 启用本地 CPU 的所有中断

5. 保存和恢复中断状态
unsigned long flags;
local_irq_save(flags);  // 保存当前中断状态并禁用中断
local_irq_restore(flags);  // 恢复之前保存的中断状态

6. 禁用和启用特定中断
unsigned long irq_flags;
disable_irq(unsigned int irq);  // 禁用特定中断
enable_irq(unsigned int irq);   // 启用特定中断

7. 中断上下文
int in_interrupt(void);  // 检查是否在中断上下文中
int in_irq(void);        // 检查是否在硬中断上下文中

8. 中断共享
#define IRQF_SHARED 0x00000080  // 共享中断标志
多个设备可以共享同一个中断线,只要它们都设置了 IRQF_SHARED 标志。

9. 中断线程化
int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long flags, const char *name, void *dev);
thread_fn: 中断线程处理函数。
其他参数与 request_irq 相同。

10. 中断控制器 API
int irq_set_chip(unsigned int irq, struct irq_chip *chip);  // 设置中断控制器
int irq_set_handler(unsigned int irq, irq_flow_handler_t handle);  // 设置中断流处理程序
c 复制代码
#include <linux/module.h>          // 包含模块相关的头文件
#include <linux/kernel.h>          // 包含内核相关的头文件
#include <linux/init.h>            // 包含初始化相关的头文件
#include <linux/interrupt.h>       // 包含中断相关的头文件
#include <linux/of_irq.h>          // 包含设备树中断相关的头文件
#include <linux/of_device.h>       // 包含设备树相关的头文件

#define IRQ_NUM 10  // 假设中断号为 10(这行在实际代码中并未使用)

// 中断处理函数,处理特定中断
//@param irq: 中断号
//@param dev_id: 传递给 request_irq 的设备指针
static irqreturn_t my_interrupt_handler(int irq, void *dev_id) {
    printk(KERN_INFO "Interrupt handler called for IRQ %d\n", irq); // 输出日志,显示被调用的中断号
    return IRQ_HANDLED; // 表示中断已被处理
}

// 模块初始化函数,加载模块时被调用
static int __init my_init(void) {
    int ret;  // 返回值
    unsigned int irq; // 存储解析后的 IRQ 值
    struct device_node *node; // 设备树节点指针

    // 找到设备树中名为 "my-device" 的节点
    node = of_find_node_by_name(NULL, "my-device");
    if (!node) { // 检查节点是否找到
        printk(KERN_ERR "Failed to find device node\n"); // 输出错误日志
        return -ENODEV; // 返回设备不存在的错误码
    }

    // 从设备树中解析并映射中断信息
    irq = irq_of_parse_and_map(node, 0);
    if (!irq) { // 检查 IRQ 是否有效
        printk(KERN_ERR "Failed to parse and map IRQ\n"); // 输出错误日志
        return -EINVAL; // 返回无效参数错误码
    }

    // 请求中断,并注册中断处理程序
    // 这里假设中断号为 10,IRQF_SHARED 表示共享中断
    // "my_irq_handler" 是中断处理程序的名称
    // NULL 是传递给中断处理程序的设备指针
    // 这里没有使用中断控制器,所以传递 NULL
    // 注册中断处理程序
    //@param irq: 中断号
    //@param handler: 中断处理程序函数指针
    //@param flags: 中断标志
    //@param name: 中断名称
    //@param dev: 传递给中断处理程序的设备指针
    // 返回值:0 表示成功,其他表示失败
    ret = request_irq(irq, my_interrupt_handler, IRQF_SHARED, "my_irq_handler", NULL);
    if (ret) { // 检查请求中断是否成功
        printk(KERN_ERR "Failed to register IRQ handler\n"); // 输出错误日志
        return ret; // 返回错误码
    }

    printk(KERN_INFO "IRQ handler registered\n"); // 输出日志,表示中断处理程序已注册
    return 0; // 返回成功
}

// 模块退出函数,卸载模块时被调用
static void __exit my_exit(void) {
    struct device_node *node; // 设备树节点指针
    unsigned int irq; // 存储解析后的 IRQ 值

    // 找到设备树中名为 "my-device" 的节点
    node = of_find_node_by_name(NULL, "my-device");
    if (!node) { // 检查节点是否找到
        printk(KERN_ERR "Failed to find device node\n"); // 输出错误日志
        return; // 函数返回
    }

    // 从设备树中解析并映射中断信息
    irq = irq_of_parse_and_map(node, 0);
    if (!irq) { // 检查 IRQ 是否有效
        printk(KERN_ERR "Failed to parse and map IRQ\n"); // 输出错误日志
        return; // 函数返回
    }

    // 释放请求的中断
    free_irq(irq, NULL);
    printk(KERN_INFO "IRQ handler unregistered\n"); // 输出日志,表示中断处理程序已注销
}

// 定义模块的初始化和退出函数
module_init(my_init);
module_exit(my_exit);

// 模块的许可证、描述和作者信息
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Interrupt Handler Example");
MODULE_AUTHOR("Your Name"); // 将 "Your Name" 替换为你的名字

示例

c 复制代码
/dts-v1/;
/ {
    compatible = "your-company,your-board";

    gpio_button: button@0 {
        compatible = "gpio-button";
        gpios = <&gpio0 10 GPIO_ACTIVE_LOW>; // GPIO0的第10个引脚为按钮
        interrupt-parent = <&gpio0>; // 中断使用的父设备
        interrupts = <10>; // 中断号
    };
};
c 复制代码
#include <linux/module.h>          // 包含模块相关的头文件
#include <linux/kernel.h>          // 包含内核相关的头文件
#include <linux/init.h>            // 包含初始化相关的头文件
#include <linux/gpio.h>            // 包含GPIO相关的头文件
#include <linux/interrupt.h>       // 包含中断处理相关的头文件
#include <linux/platform_device.h> // 包含平台设备相关的头文件
#include <linux/of.h>              // 包含设备树相关的头文件

static int irq_number; // 用于存储 GPIO 中断号的全局变量
static struct gpio_button { // 定义 GPIO 按钮的结构体
    struct device_node *node; // 存储设备树节点指针
    int gpio; // 存储 GPIO 引脚的编号
} button_data; // 实例化按钮数据结构

// 中断处理程序,处理按钮按下事件
static irqreturn_t button_isr(int irq, void *dev_id) {
    printk(KERN_INFO "Button pressed!\n"); // 输出按钮被按下的日志信息
    return IRQ_HANDLED; // 表示中断已被处理
}

// 设备探测函数,在驱动加载时调用
static int button_probe(struct platform_device *pdev) {
    int ret; // 存储返回值

    // 从设备树中获取 GPIO 和中断信息
    button_data.node = pdev->dev.of_node; // 获取设备节点
    button_data.gpio = of_get_named_gpio(button_data.node, "gpios", 0); // 获取 GPIO 引脚编号
    if (button_data.gpio < 0) { // 检查 GPIO 编号是否有效
        printk(KERN_ERR "Failed to get GPIO from device tree\n"); // 输出错误日志
        return -EINVAL; // 返回无效参数错误
    }

    // 请求使用 GPIO
    ret = gpio_request(button_data.gpio, "button_gpio"); 
    if (ret) { // 检查请求是否成功
        printk(KERN_ERR "Failed to request GPIO\n"); // 输出错误日志
        return ret; // 返回错误码
    }

    // 申请中断
    irq_number = gpio_to_irq(button_data.gpio); // 将 GPIO 转换为 IRQ 编号
    ret = request_irq(irq_number, button_isr, IRQF_TRIGGER_FALLING, "button_irq", NULL); // 请求中断服务程序
    if (ret) { // 检查请求中断是否成功
        printk(KERN_ERR "Failed to request IRQ\n"); // 输出错误日志
        gpio_free(button_data.gpio); // 释放已请求的 GPIO
        return ret; // 返回错误码
    }

    printk(KERN_INFO "Button driver probed successfully\n"); // 输出成功加载驱动的日志
    return 0; // 返回成功
}

// 设备移除函数,在驱动卸载时调用
static int button_remove(struct platform_device *pdev) {
    free_irq(irq_number, NULL); // 释放请求的 IRQ
    gpio_free(button_data.gpio); // 释放已请求的 GPIO
    printk(KERN_INFO "Button driver removed\n"); // 输出驱动卸载的日志
    return 0; // 返回成功
}

// 设备树匹配表,检查设备兼容性
static const struct of_device_id button_of_match[] = {
    { .compatible = "gpio-button", }, // 定义符合的设备字符串
    {},
};

// 生成模块设备表
MODULE_DEVICE_TABLE(of, button_of_match);

// 定义平台驱动结构
static struct platform_driver button_driver = {
    .driver = {
        .name = "gpio-button", // 驱动名称
        .of_match_table = button_of_match, // 设备树匹配表
    },
    .probe = button_probe, // 设备探测函数
    .remove = button_remove, // 设备移除函数
};

// 注册平台驱动到内核中
module_platform_driver(button_driver);

// 模块信息
MODULE_LICENSE("GPL"); // 许可证为GPL
MODULE_DESCRIPTION("GPIO Button Driver"); // 模块描述
MODULE_AUTHOR("Your Name"); // 作者信息,替换为你的名字

linux内核定时器

在Linux内核中,定时器用于在未来的某个时刻执行特定的任务。

定时器可以是软件定时器,这些定时器在内核中使用,允许开发者在指定的时间后执行回调函数。

定时器类型

高精度定时器(hrtimers):
用于需要高精度事件调度的场合,适合实时应用。它提供纳秒级的定时分辨率。

普通定时器:
定时器的基本实现,适合于一般的定时任务。

工作队列定时器:
配合工作队列使用,允许在队列中的工作项被调度执行。

定时器的时间单位

内核定时器的时间单位通常是以"jiffies"计数的。系统的 "jiffies" 是内核定时器的基本单位,表示系统运行时间的计数。

每次时钟中断就增加一个 jiffy。

获取每个 jiffy 的时间: jiffies 的时间间隔是由系统时钟频率决定的,

通常在内核中可以通过以下宏获取每个 jiffy 的时间(以毫秒为单位):

#define HZ 100 // 默认情况下,Linux内核的时钟是100 Hz
每个 jiffy 的时间可以通过公式 1000 / HZ 计算得到,

得到了每个 jiffy 的时间(毫秒),可以简化为 msecs_per_jiffy:

unsigned long msecs_per_jiffy = 1000 / HZ; // 获取每个 jiffy 的时间

使用定时器的基本步骤

c 复制代码
struct timer_list
        {
            struct list_head entry;
            void (*function)(unsigned long);
            unsigned long data;
            unsigned long expires;
        };
描述:这是内核中使用的定时器结构体,包含定时器的状态、超时时间和回调函数等信息。
字段:
entry: 定时器在超时列表中的条目。
function: 定时器到期调用的回调函数。
expires: 定时器的过期时间,以"jiffies"为单位。
data: 传递给回调函数的参数。
c 复制代码
timer_setup(struct timer_list *timer, void (*function)(struct timer_list *), unsigned int flags)

描述:初始化一个定时器。
参数:
timer: 指向要初始化的定时器的指针。
function: 指向定时器到期时调用的回调函数。
flags: 定时器的选项(通常为0)。
用法:
timer_setup(&my_timer, timer_callback, 0);
c 复制代码
mod_timer(struct timer_list *timer, unsigned long expires)

描述:设置或更新定时器的过期时间。
参数:
timer: 指向要更新的定时器的指针。
expires: 定时器到期时间,以"jiffies"为单位。
用法:
mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000));
c 复制代码
add_timer(struct timer_list *timer)

描述:在内核中添加一个定时器,开始计时(通常不推荐使用,mod_timer() 通常更为常用)。
参数:
timer: 指向要添加的定时器的指针。
用法:
add_timer(&my_timer);
c 复制代码
del_timer(struct timer_list *timer)

描述:删除一个定时器,停止其计时。
参数:
timer: 指向要删除的定时器的指针。
用法:
del_timer(&my_timer);
c 复制代码
del_timer_sync(struct timer_list *timer)

描述:如果定时器正在运行,等待定时器的回调函数执行完成,然后删除它。它确保定时器在删除时不会并发执行。
参数:
timer: 指向要同步删除的定时器的指针。
用法:
del_timer_sync(&my_timer);

定时器的使用示例

c 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/timer.h>

static struct timer_list my_timer;

// 定时器的回调函数
void timer_callback(struct timer_list *timer) {
    printk(KERN_INFO "Timer callback function called. (1 second elapsed)\n");
    
    // 重新设置定时器
    mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000)); // 每隔1000毫秒触发一次
}

// 模块初始化
static int __init my_init(void) {
    printk(KERN_INFO "Timer module initializing.\n");

    // 初始化定时器
    timer_setup(&my_timer, timer_callback, 0);
    
    // 设置定时器的超时时间为1秒
    mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000));

    return 0;
}

// 模块退出
static void __exit my_exit(void) {
    printk(KERN_INFO "Timer module exiting.\n");
    del_timer(&my_timer); // 删除定时器
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Linux Kernel Timer Example");
MODULE_AUTHOR("Your Name");

Linux内核模块中使用定时器来实现按键的去抖动

案例概述

目标:实现一个GPIO按键的去抖动功能,确保在按键被按下时只产生一次有效信号。

硬件要求:一个连接到GPIO的按钮。

按键定义:假设连接到GPIO 17(实际使用时,需要根据硬件进行调整)。

c 复制代码
代码说明

模块头文件:
加载必要的内核模块头文件,如 module.h、kernel.h、init.h、gpio.h、interrupt.h 和 timer.h。

GPIO和定时器定义:
定义所使用的GPIO引脚(GPIO 17),以及定时器的去抖动时间(50毫秒)。

定时器回调函数:
debounce_timer_callback():该函数在定时器超时时被调用,读取当前按钮状态,并在按钮被确认按下时输出消息。

中断处理程序:
button_isr():在按钮中断触发时调用,重置定时器以实现去抖动。

模块初始化函数:
my_init():请求GPIO,设置中断,并初始化定时器。

模块退出函数:
my_exit():释放资源,包括注销中断、释放GPIO和删除定时器。
c 复制代码
#include <linux/module.h>          // 包含模块相关的头文件
#include <linux/kernel.h>          // 包含内核相关的头文件
#include <linux/init.h>            // 包含初始化相关的头文件
#include <linux/gpio.h>            // 包含GPIO相关的头文件
#include <linux/interrupt.h>       // 包含中断处理相关的头文件
#include <linux/timer.h>           // 包含定时器相关的头文件

// 定义使用的GPIO引脚和去抖动时间
#define GPIO_BUTTON_PIN  17          // 假设使用GPIO 17连接按钮
#define DEBOUNCE_TIME_MS 50           // 定时器去抖动时间(单位:毫秒)

// 全局变量,存储中断号和定时器
static unsigned int irq_number;
static struct timer_list debounce_timer;
static bool button_state = false; // 按钮状态的标志

// 定时器的回调函数,处理去抖动后的按钮状态
void debounce_timer_callback(struct timer_list *timer) {
    // 读取当前按钮的GPIO值
    button_state = gpio_get_value(GPIO_BUTTON_PIN);

    // 根据按钮状态执行相应处理
    if (!button_state) {  // 如果按钮被按下(低电平)
        printk(KERN_INFO "Button pressed!\n"); // 输出按钮被按下的信息
    }
}

// 按钮中断处理程序
static irqreturn_t button_isr(int irq, void *dev_id) {
    // 重置定时器,设置去抖动的时间
    mod_timer(&debounce_timer, jiffies + msecs_to_jiffies(DEBOUNCE_TIME_MS));
    return IRQ_HANDLED;  // 指示中断已处理
}

// 模块初始化函数,在加载模块时调用
static int __init my_init(void) {
    int ret;

    // 请求GPIO引脚
    if (!gpio_is_valid(GPIO_BUTTON_PIN)) { // 检查GPIO是否有效
        printk(KERN_ERR "Invalid GPIO\n"); // 错误信息输出
        return -EINVAL; // 返回无效参数错误代码
    }

    // 请求GPIO资源
    ret = gpio_request(GPIO_BUTTON_PIN, "Button GPIO");
    if (ret) { // 检查请求是否成功
        printk(KERN_ERR "Failed to request GPIO %d\n", GPIO_BUTTON_PIN); // 错误信息输出
        return ret; // 返回错误代码
    }

    // 设置GPIO引脚为输入模式
    gpio_direction_input(GPIO_BUTTON_PIN);
    
    // 申请中断,绑定中断处理程序
    irq_number = gpio_to_irq(GPIO_BUTTON_PIN); // 获取GPIO对应的中断号
    ret = request_irq(irq_number, button_isr, IRQF_TRIGGER_FALLING, "button_irq", NULL);
    if (ret) { // 检查请求中断是否成功
        printk(KERN_ERR "Failed to request IRQ %d\n", irq_number); // 错误信息输出
        gpio_free(GPIO_BUTTON_PIN); // 释放GPIO
        return ret; // 返回错误代码
    }

    // 初始化定时器,并绑定回调函数
    timer_setup(&debounce_timer, debounce_timer_callback, 0);
    
    printk(KERN_INFO "Button debounce module initialized.\n"); // 信息输出,表示模块初始化成功
    return 0; // 返回成功
}

// 模块退出函数,在卸载模块时调用
static void __exit my_exit(void) {
    free_irq(irq_number, NULL);  // 释放中断
    gpio_free(GPIO_BUTTON_PIN);   // 释放GPIO资源
    del_timer(&debounce_timer);    // 删除定时器
    printk(KERN_INFO "Button debounce module exited.\n"); // 信息输出,表示模块已退出
}

// 定义模块的初始化和退出函数
module_init(my_init);  // 注册模块初始化函数
module_exit(my_exit);   // 注册模块退出函数

MODULE_LICENSE("GPL"); // 许可证
MODULE_DESCRIPTION("Button Debounce Example with Timer"); // 模块描述
MODULE_AUTHOR("Your Name"); // 作者信息,替换为你的名字

去抖方案

使用定时器结合中断来处理按键抖动是一个非常合理且常见的解决方案,特别是在Linux内核模块开发中。

这种方法不仅能够提高系统稳定性,还能改善用户体验。

c 复制代码
1. 使用定时器的优势
减少抖动误触发: 使用定时器可以有效地过滤掉按键在物理按下时可能造成的短暂抖动信号。通过设置一个延迟,只有在信号稳定一定时间后,才认为按键被正确按下。

提高系统响应时间: 定时器允许我们在一定时间内确认事件(如按键按下)的状态,而不会立即处理多次触发的中断,这可以减少不必要的处理和提高系统的响应效率。

2. 其他处理方法
除了定时器结合中断的方式,实际上还有其他几种处理按键抖动的方法:

软件去抖动: 通过在中断处理程序中直接记录上一次的按键状态,并在多个连续的采样周期内检查状态的一致性。如果按键在一定周期内保持一致,则确认状态。

硬件去抖动: 使用电路设计上的硬件滤波器,如RC滤波电路,可以在物理层面上消除抖动,不依赖于软件处理。

状态机: 基于状态机的逻辑设计,检测按键的状态变化,并定义不同状态的转换规则以过滤可信赖的按键操作。

3. 实际开发中的应用场景
嵌入式设备: 在嵌入式设备(如家电、汽车电子等)中,按键输入的去抖动依赖于其响应速度和稳定性,因此结合定时器和中断是一个常见而有效的实践。

用户界面: 在需要高响应的用户界面中,去抖动的处理是保证用户体验的重要环节。使用定时器确保每次用户的按键操作都会得到系统的准确响应。

中断底半部机制 Bottom Half Mechanism

中断底半部机制(Bottom Half Mechanism)是Linux内核中处理硬件中断的一种方法,

用于提高系统的响应速度和效率。

当CPU接收到中断信号时,响应中断的处理程序即为中断的顶半部(Top Half),

而底半部(Bottom Half)则用于处理较长时间的任务,以避免阻塞系统的其他中断。

c 复制代码
中断处理流程
顶半部(Top Half):

当中断发生时,CPU首先执行顶半部的处理程序,
顶半部一般仅包含非常简短的代码。其主要任务是记录中断事件、清除中断以及唤醒需要处理的底半部。


底半部(Bottom Half):

在顶半部完成后,内核将执行底半部的处理逻辑。
底半部相对较为复杂,可以包含大部分的中断处理逻辑,以确保顶半部的快速返回。这部分的任务通常是在CPU空闲时执行,或者使用其他机制来延迟执行。

底半部的实现方式

c 复制代码
Linux内核中实现底半部机制的主要方式包括以下几种:

任务队列(Tasklets):
任务队列是一种简单的底半部机制,用于在内核中延迟执行某些任务。任务队列使用链表的方式管理待执行的任务,并在适当时机执行。将底半部的工作排队到任务队列中。

工作队列(Workqueues):
工作队列比任务队列更为灵活,并且可以在上下文切换中运行,允许底半部的工作拥有更高的优先级。通过定义工作项(Work Item),可以将复杂的处理逻辑放入工作队列中执行,适合需要在进程上下文中运行的任务。

定时器(Timers):
使用定时器机制处理底半部的某些场景,例如需要周期性工作或延迟处理的任务。通过设置定时器,实现底半部的延迟执行。

中断底半部的优点

提高系统响应速度: 在处理短时间的中断时,顶半部的快速响应使得系统能立即对其他中断或任务进行处理,从而提高整个系统的响应速度。

避免长时间阻塞中断: 长时间执行复杂操作的代码会导致系统无法响应新的中断,使用底半部可以有效避免这一问题。

更好地管理资源: 可以根据需要延迟底半部的执行,提高系统资源的使用效率。

任务队列(Tasklets)

tasklet是基于软中断实现的,tasklet是没有个数限制的,因为它内部是通过链表实现的。

tasklet及中断的一个部分不能够脱离中断顶半部单独执行,在中断顶半部执行即将结束的

时候可以开启tasklet底半部机制。tasklet是工作在中断上下文的,tasklet的底半部处理函数

中只能做耗时任务或者短延时任务,但不能做休眠的任务。

它们允许在中断上下文之外执行延迟处理任务,从而避免在中断上下文中执行耗时操作。

Tasklets 是基于软中断(softirq)实现的

轻量级:Tasklets 是轻量级的,适合处理短小的延迟任务。

单线程执行:同一类型的 Tasklet 在同一时间只能在一个 CPU 上运行,避免了并发问题。

动态创建:Tasklets 可以在运行时动态创建和销毁。

c 复制代码
1. 初始化 Tasklet

void tasklet_setup(struct tasklet_struct *t, void (*func)(struct tasklet_struct *));
t: 指向 tasklet_struct 结构体的指针。

func: Tasklet 处理函数的指针。

2. 调度 Tasklet

void tasklet_schedule(struct tasklet_struct *t);
t: 指向 tasklet_struct 结构体的指针。

3. 禁用 Tasklet

void tasklet_disable(struct tasklet_struct *t);
t: 指向 tasklet_struct 结构体的指针。

4. 启用 Tasklet

void tasklet_enable(struct tasklet_struct *t);
t: 指向 tasklet_struct 结构体的指针。

5. 杀死 Tasklet

void tasklet_kill(struct tasklet_struct *t);
t: 指向 tasklet_struct 结构体的指针。

示例

c 复制代码
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/delay.h>

// 设备树节点指针
struct device_node* node;

// 存储中断号的数组
unsigned int irqno[3];

// 中断名称数组
const char* name[] = { "key1", "key2", "key3" };

// Tasklet 结构体
struct tasklet_struct tasklet;

// 中断底半部处理函数
void tasklet_func(struct tasklet_struct *t)
{
    int i = 30;
    while (--i) {
        printk("i = %d\n", i);
        msleep(5);  // 使用 msleep 替代 mdelay,避免阻塞系统
    }
}

// 中断顶半部处理函数
irqreturn_t key_irq_handle(int irq, void* dev)
{
    tasklet_schedule(&tasklet); // 调度 Tasklet 执行底半部任务
    return IRQ_HANDLED; // 返回中断已处理
}

// 模块初始化函数
static int __init key_irq_init(void)
{
    int ret, i;

    // 0. 初始化 Tasklet
    tasklet_setup(&tasklet, tasklet_func);

    // 1. 获取设备树节点
    node = of_find_node_by_path("/key_irq");
    if (node == NULL) {
        pr_err("of_find_node_by_path error\n");
        return -ENODATA;
    }

    // 遍历中断号数组
    for (i = 0; i < ARRAY_SIZE(irqno); i++) {
        // 2. 获取软中断号
        irqno[i] = irq_of_parse_and_map(node, i);
        if (irqno[i] == 0) {
            pr_err("irq_of_parse_and_map error\n");
            goto err_free_irqs; // 出错时跳转到错误处理代码
        }

        // 3. 注册中断处理函数
        ret = request_irq(irqno[i], key_irq_handle,
            IRQF_TRIGGER_FALLING, name[i], NULL);
        if (ret) {
            pr_err("request_irq error\n");
            goto err_free_irqs; // 出错时跳转到错误处理代码
        }
    }
    return 0;

// 错误处理代码
err_free_irqs:
    // 释放之前已经注册的中断
    for (i--; i >= 0; i--) {
        free_irq(irqno[i], NULL);
    }
    return ret;
}

// 模块退出函数
static void __exit key_irq_exit(void)
{
    int i;
    // 释放所有注册的中断
    for (i = 0; i < ARRAY_SIZE(irqno); i++) {
        free_irq(irqno[i], NULL);
    }
}

// 模块初始化和退出声明
module_init(key_irq_init);
module_exit(key_irq_exit);

// 模块许可证声明
MODULE_LICENSE("GPL");

工作队列(Workqueues)

工作队列(Workqueues)是 Linux 内核中用于处理延迟任务的另一种机制。与 Tasklets 不同,

工作队列允许任务在进程上下文中执行,这意味着可以在工作队列中执行阻塞操作,如睡眠和等待。

工作队列通常用于处理需要较长时间的任务。

linux内核在启动的时候默认会启动一个events线程,这个线程默认处于休眠状态,

它的内部维护一个工作队列,如果你想让它执行你的工作队列底半部处理函数,就

需要向工作队列中添加队列向,然后唤醒这个休眠的线程,此时底半部处理函数就

可以被events线程调用了。工作队列可以脱离中断顶半部单独执行。工作队列工作在

进程上下文,所以在工作队里的底半部处理函数中可以做延时,耗时,甚至休眠的操作。

工作队列 API

在实际的 Linux 设备驱动开发中,工作队列(Workqueues)通常与中断处理程序结合使用,以处理中断下半部(bottom halves)的任务。

c 复制代码
1. 初始化工作队列

struct workqueue_struct *create_workqueue(const char *name);
name: 工作队列的名称。

返回值:指向 workqueue_struct 结构体的指针,失败时返回 NULL。

2. 初始化工作项

INIT_WORK(struct work_struct *work, void (*func)(struct work_struct *));
work: 指向 work_struct 结构体的指针。

func: 工作项处理函数的指针。

3. 调度工作项

int queue_work(struct workqueue_struct *wq, struct work_struct *work);
wq: 指向 workqueue_struct 结构体的指针。

work: 指向 work_struct 结构体的指针。

返回值:成功时返回 0,失败时返回 -EINVAL。

4. 取消工作项

int cancel_work_sync(struct work_struct *work);
work: 指向 work_struct 结构体的指针。

返回值:成功时返回 0,失败时返回 -EINVAL。

5. 销毁工作队列

void destroy_workqueue(struct workqueue_struct *wq);
wq: 指向 workqueue_struct 结构体的指针。
c 复制代码
中断响应:响应按键中断并将其转换为工作项通过工作队列进行处理,确保系统的实时性和稳定性。
#include <linux/init.h>               // 包含初始化模块的必要头文件
#include <linux/module.h>            // 包含模块编写所需的基本定义
#include <linux/interrupt.h>         // 包含处理硬件中断的函数定义
#include <linux/workqueue.h>         // 包含工作队列的相关函数和定义
#include <linux/of.h>                // 包含设备树的相关操作函数
#include <linux/of_irq.h>            // 包含设备树中断相关操作
#include <linux/delay.h>             // 包含延时操作函数

// 声明设备树节点指针
struct device_node *node;

// 存储最多三个中断号的数组
unsigned int irqno[3];

// 中断名称的数组,用于描述每个中断
const char *name[] = { "key1", "key2", "key3" };

// 声明工作队列结构体指针
static struct workqueue_struct *my_wq;

// 声明工作项结构体
static struct work_struct my_work;

// 工作项的处理函数
void my_work_handler(struct work_struct *work)
{
    int i = 30; // 设置循环计数变量
    while (--i) { // 循环30次
        printk("i = %d\n", i); // 打印当前计数
        msleep(5); // 使用msleep进行延时,避免阻塞整个系统
    }
}

// 中断的顶半部处理函数
irqreturn_t key_irq_handle(int irq, void *dev)
{
    // 调度工作项到工作队列中
    queue_work(my_wq, &my_work);
    return IRQ_HANDLED; // 表示中断已被处理
}

static int __init key_irq_init(void)
{
    int ret, i;

    // 创建工作队列,名称为"my_workqueue"
    my_wq = create_workqueue("my_workqueue");
    if (!my_wq) { // 如果工作队列创建失败
        pr_err("Failed to create workqueue\n"); // 打印错误信息
        return -ENOMEM; // 返回内存不足错误
    }

    // 初始化工作项
    INIT_WORK(&my_work, my_work_handler);

    // 获取设备树节点,路径为"/key_irq"
    node = of_find_node_by_path("/key_irq");
    if (node == NULL) { // 如果节点未找到
        pr_err("of_find_node_by_path error\n"); // 打印错误信息
        destroy_workqueue(my_wq); // 销毁工作队列
        return -ENODATA; // 返回无数据错误
    }

    // 遍历中断号数组
    for (i = 0; i < ARRAY_SIZE(irqno); i++) {
        // 从设备树中解析并映射中断号
        irqno[i] = irq_of_parse_and_map(node, i);
        if (irqno[i] == 0) { // 如果映射的中断号为0,表示错误
            pr_err("irq_of_parse_and_map error\n");
            goto err_free_irqs; // 跳转到错误处理部分
        }

        // 注册中断处理函数
        ret = request_irq(irqno[i], key_irq_handle,
            IRQF_TRIGGER_FALLING, name[i], NULL);
        if (ret) { // 如果注册中断失败
            pr_err("request_irq error\n");
            goto err_free_irqs; // 跳转到错误处理部分
        }
    }
    return 0; // 初始化成功

err_free_irqs:
    // 错误处理部分,释放已注册的中断
    for (i--; i >= 0; i--) {
        free_irq(irqno[i], NULL); // 释放中断号
    }
    destroy_workqueue(my_wq); // 销毁工作队列
    return ret; // 返回错误码
}

static void __exit key_irq_exit(void)
{
    int i;
    // 释放所有注册的中断
    for (i = 0; i < ARRAY_SIZE(irqno); i++) {
        free_irq(irqno[i], NULL); // 释放中断号
    }

    // 取消工作项
    cancel_work_sync(&my_work); // 确保工作项完成

    // 销毁工作队列
    destroy_workqueue(my_wq);

    printk("Workqueue destroyed\n"); // 打印队列销毁的信息
}

module_init(key_irq_init); // 模块加载时调用初始化函数
module_exit(key_irq_exit); // 模块卸载时调用退出函数
MODULE_LICENSE("GPL"); // 模块许可证
MODULE_DESCRIPTION("Workqueue with Interrupt Example"); // 模块描述
MODULE_AUTHOR("Your Name"); // 模块作者信息
相关推荐
Ven%35 分钟前
centos查看硬盘资源使用情况命令大全
linux·运维·centos
萨格拉斯救世主1 小时前
戴尔R930服务器增加 Intel X710-DA2双万兆光口含模块
运维·服务器
Jtti1 小时前
Windows系统服务器怎么设置远程连接?详细步骤
运维·服务器·windows
TeYiToKu1 小时前
笔记整理—linux驱动开发部分(9)framebuffer驱动框架
linux·c语言·arm开发·驱动开发·笔记·嵌入式硬件·arm
dsywws1 小时前
Linux学习笔记之时间日期和查找和解压缩指令
linux·笔记·学习
yeyuningzi2 小时前
Debian 12环境里部署nginx步骤记录
linux·运维·服务器
上辈子杀猪这辈子学IT2 小时前
【Zookeeper集群搭建】安装zookeeper、zookeeper集群配置、zookeeper启动与关闭、zookeeper的shell命令操作
linux·hadoop·zookeeper·centos·debian
minihuabei2 小时前
linux centos 安装redis
linux·redis·centos
EasyCVR2 小时前
萤石设备视频接入平台EasyCVR多品牌摄像机视频平台海康ehome平台(ISUP)接入EasyCVR不在线如何排查?
运维·服务器·网络·人工智能·ffmpeg·音视频
lldhsds3 小时前
书生大模型实战营第四期-入门岛-1. Linux前置基础
linux