2、中断处理入门:Linux中断子系统基础

在蓝牙驱动开发中,中断处理是连接硬件事件与软件响应的核心桥梁。本章将系统讲解Linux中断子系统的基本概念、中断上下文分类以及下半部机制,为后续蓝牙控制器中断处理与DMA协同调试打下坚实基础。

2.1 中断子系统基础概念

Linux中断子系统负责管理硬件中断请求(IRQ)的分配、注册、处理与释放。理解以下三个核心概念是掌握中断处理的前提:

概念 说明 蓝牙驱动中的典型场景
IRQ(Interrupt Request) 硬件设备向CPU发出的中断请求信号线 蓝牙控制器通过UART/GPIO产生中断
中断号 系统为每个中断源分配的唯一数字标识 通过 platform_get_irq() 获取
中断上下文 中断处理程序执行时的CPU运行环境 不能睡眠、不能调用调度器
调试提示: 在蓝牙驱动调试初期,使用 cat /proc/interrupts 可以实时查看中断号、中断触发次数以及绑定的CPU核心,是验证中断是否生效的第一道检查手段。

2.2 中断上下文与执行环境

Linux内核将中断处理分为两种上下文环境,其行为约束差异显著:

  • 硬中断上下文(Hard IRQ Context):CPU正在执行中断服务程序(ISR),此时中断被屏蔽(取决于中断控制器配置),不允许睡眠、不允许获取信号量、不允许执行可能导致调度的操作。
  • 软中断上下文(Soft IRQ Context):由 tasklet、softirq 或 timer 回调执行,虽然允许响应硬中断,但仍然不能睡眠。
  • 进程上下文:普通内核线程或系统调用执行环境,可以睡眠、可以持有锁、可以调用调度器。
常见错误: 在蓝牙驱动的硬中断处理函数中调用 msleep() 或 mutex_lock() 会导致内核崩溃(Kernel Panic)。必须使用下半部机制将耗时操作推迟到安全上下文执行。

2.3 上半部与下半部机制

为了平衡中断响应速度与处理完整性,Linux将中断处理拆分为上半部(Top Half)和下半部(Bottom Half)。上半部执行时间敏感的最小操作(如清除中断标志、拷贝FIFO数据),下半部处理剩余逻辑(如协议解析、数据提交)。

核心原则: 上半部越快越好,下半部越全越好。

2.3.1 上半部(Top Half)

上半部即注册到 request_irq() 或 devm_request_irq() 的中断处理函数,运行在硬中断上下文。典型职责包括:

  • 读取硬件状态寄存器,确认中断源。
  • 清除中断挂起位,防止重复触发。
  • 从硬件FIFO中快速拷贝数据到内存缓冲区。
  • 调度下半部(触发tasklet/workqueue/threaded IRQ)。

2.3.2 下半部(Bottom Half)

Linux提供三种主流下半部实现机制,各有适用场景:

机制 执行上下文 是否可睡眠 适用场景
tasklet 软中断上下文 否 短时、非阻塞处理,如数据整理
workqueue 进程上下文(内核线程) 是 耗时操作,如协议栈提交、DMA设置
threaded IRQ 内核线程(专用IRQ线程) 是 需要互斥锁、与用户态交互的场景
蓝牙驱动选型建议: 对于蓝牙HCI UART驱动,推荐使用 threaded IRQ 或 workqueue,因为蓝牙协议栈处理(如HCI事件解析)可能涉及内存分配和调度,需要可睡眠上下文。

2.4 三种下半部机制详解

2.4.1 tasklet

tasklet基于软中断实现,具有以下特点:

  • 同一tasklet不会在多个CPU上并发执行,简化了数据保护。
  • 执行期间禁止睡眠,不能调用 wait_event() 或 kmalloc(GFP_KERNEL)。
  • 适合轻量级、确定性的处理任务。
/* tasklet 定义与使用示例 */
#include <linux/interrupt.h>

/* 声明 tasklet 对象 */
static struct tasklet_struct bt_tasklet;

/* 下半部处理函数 */
static void bt_tasklet_handler(unsigned long data)
{
    struct bt_device *btdev = (struct bt_device *)data;
    /* 处理接收数据,不能睡眠 */
    bt_process_rx_data(btdev);
}

/* 上半部中断处理函数 */
static irqreturn_t bt_irq_handler(int irq, void *dev_id)
{
    struct bt_device *btdev = (struct bt_device *)dev_id;
    
    /* 1. 读取状态寄存器 */
    u32 status = readl(btdev->regs + REG_STATUS);
    
    /* 2. 清除中断标志 */
    writel(status, btdev->regs + REG_STATUS);
    
    /* 3. 调度 tasklet 下半部 */
    tasklet_schedule(&bt_tasklet);
    
    return IRQ_HANDLED;
}

/* 驱动初始化时注册 */
static int bt_probe(struct platform_device *pdev)
{
    /* 初始化 tasklet */
    tasklet_init(&bt_tasklet, bt_tasklet_handler, (unsigned long)btdev);
    
    /* 注册中断 */
    ret = devm_request_irq(&pdev->dev, irq, bt_irq_handler,
                           IRQF_TRIGGER_HIGH, "bt_uart", btdev);
    return ret;
}

2.4.2 workqueue

workqueue将工作项提交给内核工作线程执行,运行在进程上下文:

  • 允许睡眠,可以调用 kmalloc(GFP_KERNEL)、mutex_lock()。
  • 支持延迟执行(schedule_delayed_work())。
  • 适合蓝牙协议栈数据提交、DMA缓冲区管理等复杂操作。
/* workqueue 定义与使用示例 */
#include <linux/workqueue.h>

/* 声明 work 对象 */
static struct work_struct bt_work;

/* 下半部处理函数 */
static void bt_work_handler(struct work_struct *work)
{
    struct bt_device *btdev = container_of(work, struct bt_device, rx_work);
    /* 可以睡眠,提交数据到蓝牙协议栈 */
    bt_hci_recv_frame(btdev->hdev, skb);
}

/* 上半部中断处理函数 */
static irqreturn_t bt_irq_handler(int irq, void *dev_id)
{
    struct bt_device *btdev = (struct bt_device *)dev_id;
    
    /* 快速拷贝数据到 skb */
    bt_fifo_to_skb(btdev);
    
    /* 调度 workqueue 下半部 */
    schedule_work(&btdev->rx_work);
    
    return IRQ_HANDLED;
}

/* 驱动初始化 */
INIT_WORK(&btdev->rx_work, bt_work_handler);

2.4.3 threaded IRQ

threaded IRQ是内核为中断处理提供的线程化方案,将整个中断处理(包括上半部和下半部)放在一个内核线程中执行:

  • 通过 request_threaded_irq() 注册,可指定上半部函数和线程化函数。
  • 线程化函数运行在进程上下文,支持睡眠和互斥锁。
  • 蓝牙驱动中常用模式:上半部只做最小硬件操作,线程函数处理完整协议逻辑。
/* threaded IRQ 使用示例 */
#include <linux/interrupt.h>

/* 上半部:仅确认中断并唤醒线程 */
static irqreturn_t bt_irq_top_half(int irq, void *dev_id)
{
    struct bt_device *btdev = (struct bt_device *)dev_id;
    
    /* 确认中断属于本设备 */
    if (!(readl(btdev->regs + REG_INT_STATUS) & INT_RX_READY))
        return IRQ_NONE;
    
    /* 清除中断标志,防止丢失后续中断 */
    writel(INT_RX_READY, btdev->regs + REG_INT_CLEAR);
    
    /* 返回 IRQ_WAKE_THREAD 唤醒线程化处理 */
    return IRQ_WAKE_THREAD;
}

/* 下半部:线程化处理,可睡眠 */
static irqreturn_t bt_irq_thread_fn(int irq, void *dev_id)
{
    struct bt_device *btdev = (struct bt_device *)dev_id;
    
    /* 从硬件 FIFO 读取数据 */
    mutex_lock(&btdev->rx_lock);
    bt_read_fifo(btdev);
    mutex_unlock(&btdev->rx_lock);
    
    /* 提交到蓝牙协议栈 */
    bt_hci_recv_frame(btdev->hdev, btdev->rx_skb);
    
    return IRQ_HANDLED;
}

/* 驱动初始化时注册 threaded IRQ */
ret = request_threaded_irq(irq, bt_irq_top_half, bt_irq_thread_fn,
                           IRQF_TRIGGER_HIGH | IRQF_ONESHOT,
                           "bt_uart", btdev);
IRQF_ONESHOT 标志说明: 在 threaded IRQ 中,IRQF_ONESHOT 保证中断线在上半部返回后保持屏蔽状态,直到线程处理函数执行完毕才重新使能。这有效防止了中断丢失和重入问题,是蓝牙驱动推荐使用的标志。

2.5 中断处理流程总览

下图展示了蓝牙驱动中从硬件中断触发到下半部处理的完整流程:

蓝牙驱动中断处理流程(上半部 + 下半部) 蓝牙控制器触发硬件中断 中断控制器(GIC)分发到CPU 上半部:bt_irq_handler() 读取状态 → 清除中断 → 调度下半部 tasklet 软中断上下文,不可睡眠 workqueue 进程上下文,可睡眠 threaded IRQ 内核线程,可睡眠 蓝牙协议栈处理(HCI事件/数据帧) hci_recv_frame / hci_event_packet 用户态蓝牙服务(BlueZ) 说明:上半部必须快速返回;下半部根据场景选择 tasklet(轻量)、workqueue(可睡眠)或 threaded IRQ(线程化)

2.6 中断注册与释放API

蓝牙驱动中常用的中断管理API如下表所示:

API 功能 下半部支持
request_irq() 注册中断处理函数 需手动调度tasklet/workqueue
request_threaded_irq() 注册带线程化下半部的中断 内置线程处理函数
devm_request_irq() 资源管理版,自动释放 需手动调度下半部
free_irq() 释放中断 —
disable_irq() / enable_irq() 临时屏蔽/使能中断 —
调试陷阱: 在蓝牙驱动中使用 disable_irq() 后,如果忘记调用 enable_irq(),会导致中断永久丢失,蓝牙控制器无法响应。推荐使用 disable_irq_nosync() + synchronize_irq() 组合进行安全屏蔽。

2.7 中断调试实践要点

  • 验证中断触发:使用 cat /proc/interrupts | grep bt 确认中断号及触发次数递增。
  • 测量中断延迟:在上下半部入口/出口添加 ktime_get() 时间戳,打印耗时。
  • 避免中断风暴:如果上半部未及时清除中断标志,会导致同一中断反复触发,系统响应变慢。使用 irq_domain 调试接口检查中断频率。
  • 选择正确的下半部:蓝牙数据接收路径推荐 threaded IRQ 或 workqueue;状态查询类短操作推荐 tasklet。
工程经验: 在调试蓝牙UART中断时,如果发现 IRQ_WAKE_THREAD 返回后线程函数未被调度,检查中断标志 IRQF_ONESHOT 是否设置,以及中断控制器是否配置为电平触发。电平触发中断在未处理完硬件状态前会持续拉低,导致线程无法退出。