《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》 第 10 章 Linux 设备驱动中的中断与时钟

《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》

第 10 章 Linux 设备驱动中的中断与时钟

参考:宋宝华 著,机械工业出版社,2015年版


10.1 中断与定时器

10.1.1 中断的概念

**中断(Interrupt)**是处理器响应硬件事件的机制。当外设需要 CPU 处理时,向 CPU 发送中断信号,CPU 暂停当前任务,跳转到中断处理程序(ISR,Interrupt Service Routine)执行,完成后恢复原来的任务。

复制代码
中断处理流程:

CPU 正常执行代码
    ↓ 硬件产生中断信号(如串口收到数据)
CPU 完成当前指令
    ↓
保存当前 CPU 状态(寄存器、PC 等)到栈
    ↓
跳转到中断向量表,找到对应的中断处理函数
    ↓
执行中断处理函数(ISR)
    ↓
恢复 CPU 状态
    ↓
继续执行被中断的代码

10.1.2 中断的分类

复制代码
Linux 中断分类:

按来源分:
  硬件中断(Hardware Interrupt)
    ├── 外部中断(External Interrupt):来自外设(GPIO、UART、USB 等)
    └── 内部中断(Internal Interrupt):来自 CPU 内部(定时器、DMA 等)
  
  软件中断(Software Interrupt)
    ├── 系统调用(syscall / SWI):用户空间进入内核的入口
    └── 软中断(softirq):内核内部的软件中断机制

按是否可屏蔽分:
  可屏蔽中断(Maskable Interrupt,IRQ):可以通过 CPSR 的 I 位禁止
  不可屏蔽中断(Non-Maskable Interrupt,NMI):不能被屏蔽,用于严重错误

按触发方式分:
  电平触发(Level Triggered):高/低电平期间持续触发
  边沿触发(Edge Triggered):上升沿/下降沿触发一次

10.1.3 中断号(IRQ Number)

Linux 使用虚拟中断号(IRQ Number)来标识中断,与硬件中断号通过中断控制器(如 GIC)映射:

bash 复制代码
# 查看系统中断信息
cat /proc/interrupts
#            CPU0       CPU1
#   0:      12345          0   GIC  27  arch_timer
#  16:          0          0   GIC  16  IPI: Rescheduling interrupts
#  17:          0          0   GIC  17  IPI: Function call interrupts
#  32:       1234          0   GIC  32  uart-pl011
#  33:          0          0   GIC  33  mmc0
# ...
# 列:中断号 | CPU0计数 | CPU1计数 | 中断控制器 | 硬件中断号 | 设备名称

# 查看中断控制器信息
cat /proc/irq/32/chip_name
# GIC

# 查看中断亲和性(哪个 CPU 处理)
cat /proc/irq/32/smp_affinity
# 3  ← 二进制 11,表示 CPU0 和 CPU1 都可以处理

10.1.4 定时器的概念

**定时器(Timer)**是一种特殊的中断源,按照设定的时间间隔周期性地产生中断,用于:

  • 系统时钟(jiffies 计数)

  • 进程调度(时间片轮转)

  • 驱动中的超时检测

  • 周期性任务执行

    Linux 定时器体系:

    硬件定时器(Hardware Timer)
    ↓ 产生时钟中断(HZ 次/秒)
    时钟中断处理函数(timer_interrupt)

    更新 jiffies(系统时钟节拍计数)

    检查并触发到期的软件定时器

    进程调度(时间片检查)

    HZ:每秒时钟中断次数(通常 100、250 或 1000)
    jiffies:系统启动以来的时钟节拍总数


10.2 Linux 中断编程

10.2.1 申请中断

c 复制代码
#include <linux/interrupt.h>

/*
 * request_irq:申请中断
 *
 * irq:中断号(虚拟 IRQ 号)
 * handler:中断处理函数
 * flags:中断标志
 * name:中断名称(显示在 /proc/interrupts 中)
 * dev:传递给中断处理函数的参数(通常为设备结构体指针)
 *
 * 返回:0(成功),负值(失败)
 */
int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev);

/* 常用中断标志(flags)*/
IRQF_SHARED      /* 共享中断:多个设备共用同一中断号 */
IRQF_TRIGGER_RISING   /* 上升沿触发 */
IRQF_TRIGGER_FALLING  /* 下降沿触发 */
IRQF_TRIGGER_HIGH     /* 高电平触发 */
IRQF_TRIGGER_LOW      /* 低电平触发 */
IRQF_ONESHOT     /* 中断处理完成前禁止该中断(用于线程化中断)*/
IRQF_NO_THREAD   /* 禁止中断线程化 */

/*
 * free_irq:释放中断
 * dev 必须与 request_irq 时传入的 dev 相同
 */
void free_irq(unsigned int irq, void *dev);

10.2.2 中断处理函数

c 复制代码
/*
 * 中断处理函数的标准形式
 *
 * irq:中断号
 * dev_id:request_irq 时传入的 dev 参数
 *
 * 返回值:
 * IRQ_HANDLED:中断已被本处理函数处理
 * IRQ_NONE:中断不是本设备产生的(用于共享中断)
 * IRQ_WAKE_THREAD:唤醒中断线程(用于线程化中断)
 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_dev *dev = dev_id;

    /*
     * 中断上下文的限制:
     * ✗ 不能睡眠(不能调用 msleep、wait_event 等)
     * ✗ 不能调用可能睡眠的函数(kmalloc(GFP_KERNEL)、mutex_lock 等)
     * ✗ 不能访问用户空间内存(copy_to/from_user)
     * ✓ 可以使用自旋锁(spin_lock)
     * ✓ 可以使用 kmalloc(GFP_ATOMIC)
     * ✓ 可以调度 tasklet 或工作队列处理耗时操作
     */

    /* 1. 读取中断状态寄存器,确认是本设备的中断 */
    u32 status = readl(dev->base + INT_STATUS_REG);
    if (!(status & MY_INT_FLAG))
        return IRQ_NONE;   /* 不是本设备的中断(共享中断场景)*/

    /* 2. 清除中断标志(防止重复触发)*/
    writel(MY_INT_FLAG, dev->base + INT_STATUS_REG);

    /* 3. 快速处理(中断上下文,必须尽快完成)*/
    dev->irq_count++;
    dev->last_data = readl(dev->base + DATA_REG);

    /* 4. 耗时操作交给底半部处理 */
    tasklet_schedule(&dev->tasklet);   /* 调度 tasklet */
    /* 或 */
    schedule_work(&dev->work);         /* 调度工作队列 */

    return IRQ_HANDLED;
}

10.2.3 完整的中断驱动示例

c 复制代码
/*
 * irq_driver.c ------ 完整的中断驱动示例
 * 以 GPIO 按键中断为例
 */
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>

struct button_dev {
    int         gpio;           /* GPIO 编号 */
    int         irq;            /* 中断号 */
    int         press_count;    /* 按键次数 */
    spinlock_t  lock;           /* 保护 press_count */
};

/* 中断处理函数 */
static irqreturn_t button_irq_handler(int irq, void *dev_id)
{
    struct button_dev *dev = dev_id;
    unsigned long flags;
    int gpio_val;

    /* 读取 GPIO 电平(判断是按下还是释放)*/
    gpio_val = gpio_get_value(dev->gpio);

    spin_lock_irqsave(&dev->lock, flags);
    if (gpio_val == 0) {
        /* 低电平:按键按下 */
        dev->press_count++;
        pr_info("button: 按键按下,总次数 = %d\n", dev->press_count);
    } else {
        /* 高电平:按键释放 */
        pr_info("button: 按键释放\n");
    }
    spin_unlock_irqrestore(&dev->lock, flags);

    return IRQ_HANDLED;
}

static int button_probe(struct platform_device *pdev)
{
    struct button_dev *dev;
    int ret;

    /* 分配设备结构体 */
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    spin_lock_init(&dev->lock);

    /* 从设备树获取 GPIO */
    dev->gpio = of_get_named_gpio(pdev->dev.of_node, "button-gpio", 0);
    if (!gpio_is_valid(dev->gpio)) {
        dev_err(&pdev->dev, "无效的 GPIO\n");
        return -EINVAL;
    }

    /* 申请 GPIO */
    ret = devm_gpio_request_one(&pdev->dev, dev->gpio,
                                 GPIOF_IN, "button");
    if (ret) {
        dev_err(&pdev->dev, "申请 GPIO 失败\n");
        return ret;
    }

    /* 获取中断号(从 GPIO 转换)*/
    dev->irq = gpio_to_irq(dev->gpio);
    if (dev->irq < 0) {
        dev_err(&pdev->dev, "获取中断号失败\n");
        return dev->irq;
    }

    /* 申请中断(双边沿触发,检测按下和释放)*/
    ret = devm_request_irq(&pdev->dev, dev->irq,
                            button_irq_handler,
                            IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                            "button_irq", dev);
    if (ret) {
        dev_err(&pdev->dev, "申请中断失败\n");
        return ret;
    }

    platform_set_drvdata(pdev, dev);
    dev_info(&pdev->dev, "按键驱动加载,GPIO=%d, IRQ=%d\n",
             dev->gpio, dev->irq);
    return 0;
}

static int button_remove(struct platform_device *pdev)
{
    /* devm_ 申请的资源自动释放 */
    dev_info(&pdev->dev, "按键驱动卸载\n");
    return 0;
}

static const struct of_device_id button_of_match[] = {
    { .compatible = "myvendor,gpio-button" },
    {}
};
MODULE_DEVICE_TABLE(of, button_of_match);

static struct platform_driver button_driver = {
    .probe  = button_probe,
    .remove = button_remove,
    .driver = {
        .name           = "gpio-button",
        .of_match_table = button_of_match,
    },
};
module_platform_driver(button_driver);
MODULE_LICENSE("GPL v2");

10.2.4 中断的使能与禁止

c 复制代码
/* ── 禁止/使能特定中断 ──────────────────────────────────── */
disable_irq(irq);          /* 禁止指定中断(等待当前处理完成)*/
disable_irq_nosync(irq);   /* 禁止指定中断(不等待,立即返回)*/
enable_irq(irq);           /* 使能指定中断 */

/* ── 禁止/使能本地 CPU 所有中断 ─────────────────────────── */
local_irq_disable();       /* 禁止本地 CPU 中断 */
local_irq_enable();        /* 使能本地 CPU 中断 */

unsigned long flags;
local_irq_save(flags);     /* 保存中断状态并禁止 */
local_irq_restore(flags);  /* 恢复中断状态 */

/* ── 判断当前是否在中断上下文 ───────────────────────────── */
in_interrupt()    /* 在中断上下文(硬中断或软中断)中返回非0 */
in_irq()          /* 在硬中断上下文中返回非0 */
in_softirq()      /* 在软中断上下文中返回非0 */
in_atomic()       /* 在原子上下文(不能睡眠)中返回非0 */

10.2.5 共享中断

多个设备共用同一中断号时,需要使用共享中断:

c 复制代码
/* 共享中断的申请 */
request_irq(irq, handler_a, IRQF_SHARED, "device_a", dev_a);
request_irq(irq, handler_b, IRQF_SHARED, "device_b", dev_b);

/*
 * 共享中断处理函数必须:
 * 1. 检查是否是本设备产生的中断
 * 2. 如果不是,返回 IRQ_NONE
 * 3. 如果是,处理后返回 IRQ_HANDLED
 */
static irqreturn_t handler_a(int irq, void *dev_id)
{
    struct device_a *dev = dev_id;

    /* 检查是否是设备A的中断 */
    if (!(readl(dev->base + STATUS) & DEV_A_INT_FLAG))
        return IRQ_NONE;   /* 不是设备A的中断,让其他处理函数处理 */

    /* 处理设备A的中断 */
    /* ... */
    return IRQ_HANDLED;
}

10.3 中断底半部机制

10.3.1 顶半部与底半部的概念

中断处理分为顶半部(Top Half)底半部(Bottom Half)

复制代码
顶半部(Top Half):
  - 在中断上下文中执行
  - 必须尽快完成(通常 < 几十微秒)
  - 只做最紧急的工作:读取状态、清除中断标志、保存数据
  - 不能睡眠

底半部(Bottom Half):
  - 在进程上下文或软中断上下文中执行
  - 可以做耗时操作
  - 可以睡眠(工作队列)
  - 处理顶半部推迟的工作

为什么需要底半部?
  中断处理时,CPU 的中断被禁止(或同级中断被禁止)
  如果中断处理函数执行时间过长,会导致:
  - 其他中断无法及时响应
  - 系统实时性下降
  - 用户体验变差
  
  解决方案:顶半部快速处理,底半部延迟处理

10.3.2 tasklet

tasklet 是最简单的底半部机制,运行在软中断上下文中,不能睡眠:

c 复制代码
#include <linux/interrupt.h>

/* ── 定义 tasklet ──────────────────────────────────────── */

/* 方法一:静态定义 */
static void my_tasklet_func(unsigned long data);
DECLARE_TASKLET(my_tasklet, my_tasklet_func, 0);
/* 或禁用状态的 tasklet */
DECLARE_TASKLET_DISABLED(my_tasklet, my_tasklet_func, 0);

/* 方法二:动态初始化 */
struct tasklet_struct my_tasklet;
tasklet_init(&my_tasklet, my_tasklet_func, (unsigned long)dev);

/* ── tasklet 处理函数 ──────────────────────────────────── */
static void my_tasklet_func(unsigned long data)
{
    struct my_dev *dev = (struct my_dev *)data;

    /*
     * tasklet 运行在软中断上下文:
     * ✗ 不能睡眠
     * ✓ 可以使用自旋锁
     * ✓ 可以访问设备寄存器
     */
    pr_info("tasklet 执行,处理中断数据\n");
    process_irq_data(dev);
}

/* ── 在中断处理函数中调度 tasklet ──────────────────────── */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_dev *dev = dev_id;

    /* 顶半部:快速处理 */
    dev->irq_data = readl(dev->base + DATA_REG);
    writel(INT_CLEAR, dev->base + INT_STATUS_REG);

    /* 调度 tasklet(底半部)*/
    tasklet_schedule(&my_tasklet);

    return IRQ_HANDLED;
}

/* ── tasklet 控制 ──────────────────────────────────────── */
tasklet_schedule(&my_tasklet);    /* 调度 tasklet(如果未调度)*/
tasklet_hi_schedule(&my_tasklet); /* 高优先级调度 */
tasklet_disable(&my_tasklet);     /* 禁用 tasklet */
tasklet_enable(&my_tasklet);      /* 使能 tasklet */
tasklet_kill(&my_tasklet);        /* 等待 tasklet 完成并禁用 */

tasklet 的完整使用案例

c 复制代码
struct uart_dev {
    void __iomem       *base;
    int                 irq;
    struct tasklet_struct rx_tasklet;
    unsigned char       rx_buf[256];
    int                 rx_len;
    spinlock_t          lock;
};

/* tasklet 处理函数:处理接收到的数据 */
static void uart_rx_tasklet(unsigned long data)
{
    struct uart_dev *dev = (struct uart_dev *)data;
    unsigned long flags;
    unsigned char buf[256];
    int len;

    spin_lock_irqsave(&dev->lock, flags);
    memcpy(buf, dev->rx_buf, dev->rx_len);
    len = dev->rx_len;
    dev->rx_len = 0;
    spin_unlock_irqrestore(&dev->lock, flags);

    /* 处理接收到的数据(可以做较复杂的操作)*/
    pr_info("uart: 处理 %d 字节数据\n", len);
    /* parse_protocol(buf, len); */
}

/* 中断处理函数(顶半部)*/
static irqreturn_t uart_irq_handler(int irq, void *dev_id)
{
    struct uart_dev *dev = dev_id;
    unsigned long flags;
    u32 status = readl(dev->base + UART_STATUS);

    if (!(status & UART_RX_READY))
        return IRQ_NONE;

    spin_lock_irqsave(&dev->lock, flags);
    /* 快速读取 FIFO 中的数据 */
    while (readl(dev->base + UART_STATUS) & UART_RX_READY) {
        if (dev->rx_len < sizeof(dev->rx_buf))
            dev->rx_buf[dev->rx_len++] = readb(dev->base + UART_DATA);
    }
    spin_unlock_irqrestore(&dev->lock, flags);

    /* 调度 tasklet 处理数据 */
    tasklet_schedule(&dev->rx_tasklet);

    return IRQ_HANDLED;
}

static int uart_probe(struct platform_device *pdev)
{
    struct uart_dev *dev;
    /* ... */

    /* 初始化 tasklet */
    tasklet_init(&dev->rx_tasklet, uart_rx_tasklet, (unsigned long)dev);

    /* 申请中断 */
    request_irq(dev->irq, uart_irq_handler, 0, "uart", dev);

    return 0;
}

static int uart_remove(struct platform_device *pdev)
{
    struct uart_dev *dev = platform_get_drvdata(pdev);

    /* 必须先 kill tasklet,再释放中断 */
    tasklet_kill(&dev->rx_tasklet);
    free_irq(dev->irq, dev);
    return 0;
}

10.3.3 工作队列

工作队列(Work Queue)是最灵活的底半部机制,运行在进程上下文 中,可以睡眠

c 复制代码
#include <linux/workqueue.h>

/* ── 使用内核默认工作队列 ──────────────────────────────── */

/* 定义工作项 */
struct work_struct my_work;

/* 工作处理函数 */
static void my_work_func(struct work_struct *work)
{
    struct my_dev *dev = container_of(work, struct my_dev, work);

    /*
     * 工作队列运行在进程上下文:
     * ✓ 可以睡眠(msleep、wait_event 等)
     * ✓ 可以调用 kmalloc(GFP_KERNEL)
     * ✓ 可以调用 mutex_lock
     * ✓ 可以访问用户空间(copy_to/from_user)
     */
    pr_info("工作队列执行,处理中断数据\n");

    /* 可以做耗时操作 */
    msleep(10);
    process_complex_data(dev);
}

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

/* 在中断处理函数中调度工作 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_dev *dev = dev_id;

    /* 顶半部处理 */
    dev->irq_data = readl(dev->base + DATA_REG);

    /* 调度工作队列(底半部)*/
    schedule_work(&dev->work);

    return IRQ_HANDLED;
}

/* ── 延迟工作(delayed_work)──────────────────────────── */
struct delayed_work my_delayed_work;

static void my_delayed_work_func(struct work_struct *work)
{
    struct delayed_work *dwork = to_delayed_work(work);
    struct my_dev *dev = container_of(dwork, struct my_dev, delayed_work);
    pr_info("延迟工作执行\n");
}

INIT_DELAYED_WORK(&my_delayed_work, my_delayed_work_func);

/* 500ms 后执行 */
schedule_delayed_work(&my_delayed_work, msecs_to_jiffies(500));

/* 取消延迟工作 */
cancel_delayed_work_sync(&my_delayed_work);

/* ── 创建专用工作队列 ──────────────────────────────────── */
struct workqueue_struct *my_wq;

/* 创建单线程工作队列 */
my_wq = create_singlethread_workqueue("my_driver_wq");

/* 创建多线程工作队列(每个 CPU 一个线程)*/
my_wq = create_workqueue("my_driver_wq");

/* 向专用工作队列提交工作 */
queue_work(my_wq, &my_work);
queue_delayed_work(my_wq, &my_delayed_work, msecs_to_jiffies(100));

/* 等待工作队列中所有工作完成 */
flush_workqueue(my_wq);

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

工作队列的完整使用案例

c 复制代码
struct sensor_dev {
    void __iomem        *base;
    int                  irq;
    struct work_struct   data_work;
    struct workqueue_struct *wq;
    int                  raw_data;
    struct mutex         mutex;
};

/* 工作处理函数:复杂的数据处理 */
static void sensor_data_work(struct work_struct *work)
{
    struct sensor_dev *dev = container_of(work, struct sensor_dev, data_work);
    int processed_data;

    /* 可以睡眠的复杂处理 */
    mutex_lock(&dev->mutex);

    /* 模拟耗时的数据处理(如 I2C 读取、数据转换)*/
    msleep(5);
    processed_data = dev->raw_data * 100 / 4096;  /* ADC 转换 */

    pr_info("sensor: 原始数据 %d → 处理后 %d\n",
            dev->raw_data, processed_data);

    mutex_unlock(&dev->mutex);
}

/* 中断处理函数(顶半部)*/
static irqreturn_t sensor_irq_handler(int irq, void *dev_id)
{
    struct sensor_dev *dev = dev_id;

    /* 快速读取原始数据 */
    dev->raw_data = readl(dev->base + ADC_DATA_REG);

    /* 清除中断 */
    writel(ADC_INT_CLEAR, dev->base + ADC_INT_REG);

    /* 调度工作队列处理数据 */
    queue_work(dev->wq, &dev->data_work);

    return IRQ_HANDLED;
}

static int sensor_probe(struct platform_device *pdev)
{
    struct sensor_dev *dev;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev) return -ENOMEM;

    mutex_init(&dev->mutex);

    /* 创建专用工作队列 */
    dev->wq = create_singlethread_workqueue("sensor_wq");
    if (!dev->wq) return -ENOMEM;

    /* 初始化工作项 */
    INIT_WORK(&dev->data_work, sensor_data_work);

    /* 申请中断 */
    devm_request_irq(&pdev->dev, dev->irq, sensor_irq_handler,
                     0, "sensor", dev);

    platform_set_drvdata(pdev, dev);
    return 0;
}

static int sensor_remove(struct platform_device *pdev)
{
    struct sensor_dev *dev = platform_get_drvdata(pdev);

    /* 等待所有工作完成并销毁工作队列 */
    flush_workqueue(dev->wq);
    destroy_workqueue(dev->wq);
    return 0;
}

10.3.4 软中断

**软中断(softirq)**是 Linux 内核内部使用的底半部机制,优先级高于工作队列,不能睡眠:

c 复制代码
/* Linux 内核预定义的软中断类型(优先级从高到低)*/
enum {
    HI_SOFTIRQ = 0,      /* 高优先级 tasklet */
    TIMER_SOFTIRQ,       /* 定时器 */
    NET_TX_SOFTIRQ,      /* 网络发送 */
    NET_RX_SOFTIRQ,      /* 网络接收 */
    BLOCK_SOFTIRQ,       /* 块设备 */
    BLOCK_IOPOLL_SOFTIRQ,/* 块设备 I/O 轮询 */
    TASKLET_SOFTIRQ,     /* 普通 tasklet */
    SCHED_SOFTIRQ,       /* 调度器 */
    HRTIMER_SOFTIRQ,     /* 高精度定时器 */
    RCU_SOFTIRQ,         /* RCU */
    NR_SOFTIRQS
};

/*
 * 软中断通常由内核子系统使用,驱动开发者一般不直接使用软中断
 * 驱动应该使用 tasklet 或工作队列
 *
 * 软中断的特点:
 * - 可以在多个 CPU 上同时运行(同一软中断的不同实例)
 * - 不能睡眠
 * - 优先级高于 tasklet
 * - 驱动开发者通常不需要直接使用
 */

/* 触发软中断 */
raise_softirq(NET_RX_SOFTIRQ);

/* 软中断处理函数注册(内核初始化时)*/
open_softirq(MY_SOFTIRQ, my_softirq_handler);

10.3.5 threaded_irq(中断线程化)

threaded_irq 是 Linux 2.6.30 引入的机制,将中断处理函数在内核线程中执行:

c 复制代码
/*
 * request_threaded_irq:申请线程化中断
 *
 * irq:中断号
 * handler:顶半部处理函数(在中断上下文中执行,可为 NULL)
 * thread_fn:线程处理函数(在内核线程中执行,可以睡眠)
 * flags:中断标志
 * name:中断名称
 * dev:设备指针
 */
int request_threaded_irq(unsigned int irq,
                          irq_handler_t handler,
                          irq_handler_t thread_fn,
                          unsigned long flags,
                          const char *name,
                          void *dev);

/* 使用示例 */

/* 顶半部:在中断上下文中执行,快速判断是否需要线程处理 */
static irqreturn_t my_hard_irq(int irq, void *dev_id)
{
    struct my_dev *dev = dev_id;

    /* 检查是否是本设备的中断 */
    if (!(readl(dev->base + STATUS) & MY_INT))
        return IRQ_NONE;

    /* 清除中断 */
    writel(MY_INT, dev->base + STATUS);

    /* 返回 IRQ_WAKE_THREAD:唤醒线程处理函数 */
    return IRQ_WAKE_THREAD;
}

/* 线程处理函数:在内核线程中执行,可以睡眠 */
static irqreturn_t my_thread_irq(int irq, void *dev_id)
{
    struct my_dev *dev = dev_id;

    /*
     * 在内核线程中执行:
     * ✓ 可以睡眠
     * ✓ 可以调用 mutex_lock
     * ✓ 可以调用 kmalloc(GFP_KERNEL)
     * ✓ 可以进行 I2C/SPI 通信
     */
    pr_info("线程化中断处理\n");

    /* 通过 I2C 读取传感器数据(会睡眠)*/
    i2c_smbus_read_byte_data(dev->i2c_client, REG_DATA);

    return IRQ_HANDLED;
}

/* 申请线程化中断 */
ret = request_threaded_irq(irq,
                            my_hard_irq,    /* 顶半部 */
                            my_thread_irq,  /* 线程处理函数 */
                            IRQF_ONESHOT,   /* 线程处理完成前禁止该中断 */
                            "my_device",
                            dev);

/*
 * 简化版:顶半部为 NULL,直接使用线程处理
 * 内核会使用默认的顶半部(直接唤醒线程)
 */
ret = request_threaded_irq(irq,
                            NULL,           /* 使用默认顶半部 */
                            my_thread_irq,
                            IRQF_ONESHOT | IRQF_TRIGGER_FALLING,
                            "my_device",
                            dev);

10.3.6 底半部机制对比

复制代码
底半部机制对比:

机制          运行上下文    可睡眠    适用场景
─────────────────────────────────────────────────────────────
tasklet       软中断上下文  否        简单、快速的延迟处理
工作队列      进程上下文    是        耗时操作、需要睡眠的处理
软中断        软中断上下文  否        内核子系统(驱动不直接使用)
threaded_irq  内核线程      是        需要睡眠的中断处理(推荐)

选择建议:
  需要睡眠 → 工作队列 或 threaded_irq
  不需要睡眠,简单处理 → tasklet
  高性能网络/存储 → 软中断(内核子系统)
  新驱动开发 → 优先考虑 threaded_irq(更简洁)

10.4 实时性与中断线程化

10.4.1 实时性的概念

**实时性(Real-Time)**是指系统对外部事件的响应时间是可预测和有界的:

复制代码
实时性分类:

硬实时(Hard Real-Time):
  响应时间有严格上限,超时即为失败
  例:汽车安全气囊(必须在 10ms 内响应)
  需要:RTOS 或 PREEMPT_RT 补丁

软实时(Soft Real-Time):
  响应时间有期望上限,偶尔超时可接受
  例:音视频播放(偶尔卡顿可接受)
  需要:低延迟内核配置

Linux 标准内核的实时性问题:
  - 中断处理期间禁止抢占,导致延迟不可预测
  - 自旋锁持有期间不可抢占
  - 大量代码在不可抢占的上下文中执行

10.4.2 PREEMPT_RT 补丁与中断线程化

Linux 的 PREEMPT_RT 补丁通过将所有中断处理线程化来提高实时性:

复制代码
PREEMPT_RT 的核心改变:

1. 中断线程化
   所有硬件中断处理函数在内核线程中执行
   中断线程可以被更高优先级的实时任务抢占
   
2. 自旋锁替换为互斥锁
   大多数 spinlock 替换为可睡眠的 rt_mutex
   支持优先级继承,防止优先级反转
   
3. 完全可抢占内核
   内核代码几乎所有地方都可以被抢占

中断线程化的效果:
  标准内核:中断延迟 100μs~10ms(不可预测)
  PREEMPT_RT:中断延迟 < 100μs(可预测)

在驱动中支持中断线程化

c 复制代码
/*
 * 使用 request_threaded_irq 的驱动天然支持中断线程化
 * 在 PREEMPT_RT 内核上,线程化中断可以被实时任务抢占
 */

/* 设置中断线程的实时优先级 */
struct irq_desc *desc = irq_to_desc(irq);
if (desc && desc->action && desc->action->thread) {
    struct sched_param param = { .sched_priority = 50 };
    sched_setscheduler(desc->action->thread,
                       SCHED_FIFO, &param);
}

10.5 Linux 时钟编程

10.5.1 jiffies 与 HZ

c 复制代码
#include <linux/jiffies.h>

/*
 * HZ:每秒的时钟节拍数
 * 通常为 100(嵌入式)、250 或 1000(桌面/服务器)
 */
printk("HZ = %d\n", HZ);

/*
 * jiffies:系统启动以来的时钟节拍总数(unsigned long)
 * 在 32 位系统上,约 49.7 天溢出(HZ=1000 时)
 * 在 64 位系统上,几乎不会溢出
 */
unsigned long now = jiffies;

/* jiffies 与时间单位的转换 */
unsigned long ms_to_jiffies(unsigned int ms);
unsigned long usecs_to_jiffies(unsigned int us);
unsigned int  jiffies_to_msecs(unsigned long j);
unsigned int  jiffies_to_usecs(unsigned long j);

/* 时间比较(处理溢出)*/
time_after(a, b)       /* a > b(考虑溢出)*/
time_before(a, b)      /* a < b(考虑溢出)*/
time_after_eq(a, b)    /* a >= b */
time_before_eq(a, b)   /* a <= b */

/* 使用示例 */
unsigned long timeout = jiffies + msecs_to_jiffies(500); /* 500ms 后超时 */
while (!condition) {
    if (time_after(jiffies, timeout)) {
        pr_err("操作超时\n");
        return -ETIMEDOUT;
    }
    msleep(10);
}

10.5.2 内核定时器(kernel timer)

内核定时器用于在指定时间后执行回调函数:

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

/* ── 定义和初始化定时器 ────────────────────────────────── */
struct timer_list my_timer;

/* Linux 4.15+ 新接口(推荐)*/
timer_setup(&my_timer, my_timer_callback, 0);

/* 旧接口(Linux 4.15 之前)*/
init_timer(&my_timer);
my_timer.function = my_timer_callback;
my_timer.data     = (unsigned long)dev;

/* ── 定时器回调函数 ────────────────────────────────────── */

/* 新接口(Linux 4.15+)*/
static void my_timer_callback(struct timer_list *t)
{
    struct my_dev *dev = from_timer(dev, t, timer);

    pr_info("定时器触发\n");

    /* 定时器是一次性的,如需周期性触发,需要重新设置 */
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(1000));
}

/* 旧接口 */
static void my_timer_callback_old(unsigned long data)
{
    struct my_dev *dev = (struct my_dev *)data;
    pr_info("定时器触发(旧接口)\n");
}

/* ── 定时器操作 ────────────────────────────────────────── */

/* 设置超时时间并启动定时器 */
mod_timer(&my_timer, jiffies + msecs_to_jiffies(1000)); /* 1秒后触发 */

/* 添加定时器(首次启动)*/
add_timer(&my_timer);

/* 删除定时器(不等待回调完成)*/
del_timer(&my_timer);

/* 删除定时器(等待回调完成,安全)*/
del_timer_sync(&my_timer);

/* 检查定时器是否处于激活状态 */
int active = timer_pending(&my_timer);

定时器的完整使用案例(LED 闪烁)

c 复制代码
struct led_dev {
    int              gpio;
    int              state;
    struct timer_list blink_timer;
    unsigned int     interval_ms;  /* 闪烁间隔(毫秒)*/
};

/* 定时器回调:翻转 LED 状态 */
static void led_blink_callback(struct timer_list *t)
{
    struct led_dev *dev = from_timer(dev, t, blink_timer);

    dev->state = !dev->state;
    gpio_set_value(dev->gpio, dev->state);

    /* 重新设置定时器,实现周期性闪烁 */
    mod_timer(&dev->blink_timer,
              jiffies + msecs_to_jiffies(dev->interval_ms));
}

static int led_probe(struct platform_device *pdev)
{
    struct led_dev *dev;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev) return -ENOMEM;

    dev->gpio        = of_get_named_gpio(pdev->dev.of_node, "led-gpio", 0);
    dev->interval_ms = 500;  /* 500ms 闪烁间隔 */
    dev->state       = 0;

    devm_gpio_request_one(&pdev->dev, dev->gpio, GPIOF_OUT_INIT_LOW, "led");

    /* 初始化定时器 */
    timer_setup(&dev->blink_timer, led_blink_callback, 0);

    /* 启动定时器 */
    mod_timer(&dev->blink_timer,
              jiffies + msecs_to_jiffies(dev->interval_ms));

    platform_set_drvdata(pdev, dev);
    dev_info(&pdev->dev, "LED 闪烁驱动加载,间隔 %dms\n", dev->interval_ms);
    return 0;
}

static int led_remove(struct platform_device *pdev)
{
    struct led_dev *dev = platform_get_drvdata(pdev);

    /* 停止定时器(必须在释放 GPIO 之前)*/
    del_timer_sync(&dev->blink_timer);

    gpio_set_value(dev->gpio, 0);  /* 熄灭 LED */
    dev_info(&pdev->dev, "LED 闪烁驱动卸载\n");
    return 0;
}

10.5.3 高精度定时器(hrtimer)

内核定时器的精度受 HZ 限制(最小 1/HZ 秒),高精度定时器(hrtimer)可以达到纳秒级精度

c 复制代码
#include <linux/hrtimer.h>
#include <linux/ktime.h>

struct hrtimer my_hrtimer;

/* 高精度定时器回调函数 */
static enum hrtimer_restart my_hrtimer_callback(struct hrtimer *timer)
{
    struct my_dev *dev = container_of(timer, struct my_dev, hrtimer);

    pr_info("高精度定时器触发\n");

    /* 返回 HRTIMER_RESTART:重新启动定时器(周期性)*/
    /* 返回 HRTIMER_NORESTART:不重启(一次性)*/

    /* 周期性:前进 1ms */
    hrtimer_forward_now(timer, ms_to_ktime(1));
    return HRTIMER_RESTART;
}

/* 初始化高精度定时器 */
hrtimer_init(&my_hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
my_hrtimer.function = my_hrtimer_callback;

/* 启动定时器(1ms 后触发)*/
hrtimer_start(&my_hrtimer, ms_to_ktime(1), HRTIMER_MODE_REL);

/* 取消定时器 */
hrtimer_cancel(&my_hrtimer);

/* ktime 操作 */
ktime_t now  = ktime_get();
ktime_t kt   = ktime_set(1, 500000000);  /* 1秒500毫秒 */
ktime_t kt2  = ms_to_ktime(100);         /* 100毫秒 */
ktime_t kt3  = ns_to_ktime(1000);        /* 1000纳秒 */
s64 ns       = ktime_to_ns(kt);          /* 转换为纳秒 */

10.6 内核延时

10.6.1 忙等待延时

忙等待延时会占用 CPU,适用于极短时间的延时(通常 < 10μs):

c 复制代码
#include <linux/delay.h>

/*
 * ndelay(ns):纳秒级忙等待
 * 适用于 < 1μs 的延时
 * 精度:依赖于 CPU 频率校准
 */
ndelay(100);    /* 等待约 100 纳秒 */

/*
 * udelay(us):微秒级忙等待
 * 适用于 1μs ~ 1000μs 的延时
 * 可以在中断上下文中使用
 * 注意:us 不能太大(通常 < 1000),否则会触发内核警告
 */
udelay(10);     /* 等待约 10 微秒 */

/*
 * mdelay(ms):毫秒级忙等待(不推荐)
 * 实现为循环调用 udelay
 * 会长时间占用 CPU,影响系统响应
 * 除非在中断上下文中必须使用,否则应使用 msleep
 */
mdelay(5);      /* 等待约 5 毫秒(不推荐!)*/

10.6.2 睡眠延时

睡眠延时会让出 CPU,适用于较长时间的延时,只能在进程上下文中使用:

c 复制代码
#include <linux/delay.h>

/*
 * msleep(ms):毫秒级睡眠
 * 进程进入睡眠,让出 CPU
 * 不可被信号中断
 * 适用于 > 20ms 的延时
 */
msleep(100);    /* 睡眠约 100 毫秒 */

/*
 * msleep_interruptible(ms):可被信号中断的毫秒睡眠
 * 返回:0(正常唤醒),剩余毫秒数(被信号中断)
 */
unsigned long remaining = msleep_interruptible(1000);
if (remaining) {
    pr_info("睡眠被信号中断,剩余 %lu ms\n", remaining);
}

/*
 * ssleep(s):秒级睡眠
 */
ssleep(2);      /* 睡眠 2 秒 */

/*
 * usleep_range(min_us, max_us):微秒范围睡眠(推荐)
 * 比 udelay 更节能(允许 CPU 进入低功耗状态)
 * 适用于 10μs ~ 20ms 的延时
 * min_us:最短睡眠时间
 * max_us:最长睡眠时间(给内核一定的调度余地)
 */
usleep_range(900, 1100);   /* 睡眠 900μs ~ 1100μs */
usleep_range(10000, 11000);/* 睡眠 10ms ~ 11ms */

10.6.3 延时函数选择原则

复制代码
延时函数选择指南:

延时时间          推荐函数              说明
─────────────────────────────────────────────────────────────
< 1μs            ndelay()             纳秒级忙等待
1μs ~ 10μs      udelay()             微秒级忙等待
10μs ~ 20ms     usleep_range()       微秒范围睡眠(节能)
> 20ms           msleep()             毫秒级睡眠

特殊场景:
  中断上下文      只能用 ndelay/udelay/mdelay(忙等待)
  需要可中断      msleep_interruptible()
  精确延时        hrtimer(高精度定时器)

注意事项:
  1. 忙等待期间 CPU 无法做其他事,影响系统性能
  2. 睡眠延时不能在中断上下文中使用
  3. usleep_range 的范围越大,内核调度越灵活,系统性能越好
  4. 不要在持有自旋锁时使用睡眠延时

10.6.4 延时的综合案例

c 复制代码
/*
 * 综合案例:SPI Flash 写操作等待
 * SPI Flash 写操作需要等待内部写周期完成(最长 5ms)
 */
static int spi_flash_wait_ready(struct spi_device *spi)
{
    u8 status;
    unsigned long timeout = jiffies + msecs_to_jiffies(100); /* 100ms 超时 */

    do {
        /* 读取状态寄存器 */
        spi_read_status(spi, &status);

        if (!(status & FLASH_STATUS_WIP))
            return 0;   /* 写操作完成 */

        /* 短暂睡眠,避免频繁轮询 */
        usleep_range(500, 1000);  /* 等待 500μs ~ 1ms */

    } while (time_before(jiffies, timeout));

    pr_err("SPI Flash 写操作超时\n");
    return -ETIMEDOUT;
}

/*
 * 综合案例:I2C 设备复位
 * 复位后需要等待设备就绪
 */
static int i2c_device_reset(struct i2c_client *client)
{
    /* 拉低复位引脚 */
    gpio_set_value(reset_gpio, 0);
    udelay(10);   /* 保持复位 10μs(忙等待,时间短)*/

    /* 释放复位引脚 */
    gpio_set_value(reset_gpio, 1);
    msleep(50);   /* 等待设备启动 50ms(睡眠,时间长)*/

    /* 验证设备是否就绪 */
    return i2c_smbus_read_byte(client) >= 0 ? 0 : -ENODEV;
}

本章小结

章节 核心知识点 关键 API
10.1 中断与定时器 中断的概念与分类;中断号;定时器体系;jiffies/HZ /proc/interruptsjiffiesHZ
10.2 Linux中断编程 request_irq 参数详解;中断处理函数规范;共享中断;中断使能/禁止 request_irq()free_irq()IRQ_HANDLEDIRQ_NONE
10.3.1 tasklet 软中断上下文;不能睡眠;DECLARE_TASKLETtasklet_schedule;完整UART案例 tasklet_init()tasklet_schedule()tasklet_kill()
10.3.2 工作队列 进程上下文;可以睡眠;INIT_WORK;延迟工作;专用工作队列;完整传感器案例 INIT_WORK()schedule_work()create_singlethread_workqueue()
10.3.3 软中断 内核子系统使用;多CPU并发;驱动不直接使用 raise_softirq()open_softirq()
10.3.4 threaded_irq 中断线程化;可以睡眠;IRQ_WAKE_THREADIRQF_ONESHOT request_threaded_irq()IRQ_WAKE_THREAD
10.4 实时性 硬/软实时;PREEMPT_RT补丁;中断线程化提高实时性 IRQF_ONESHOT、实时调度
10.5 时钟编程 jiffies时间比较;内核定时器(timer_setup/mod_timer);高精度定时器(hrtimer);LED闪烁案例 timer_setup()mod_timer()del_timer_sync()hrtimer_start()
10.6 内核延时 忙等待(ndelay/udelay);睡眠延时(msleep/usleep_range);选择原则;SPI Flash等待案例 udelay()msleep()usleep_range()

中断底半部选择决策树

复制代码
需要在中断后执行延迟处理?
    ↓
需要睡眠?
  是 → 工作队列(schedule_work)或 threaded_irq
  否 → 继续判断
    ↓
需要高性能(网络/存储)?
  是 → 软中断(内核子系统)
  否 → tasklet(简单场景)

新驱动开发建议:
  优先使用 threaded_irq(最简洁,可睡眠)
  其次使用工作队列(灵活,可睡眠)
  最后考虑 tasklet(不能睡眠,但开销小)

参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年