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 中断处理流程总览
下图展示了蓝牙驱动中从硬件中断触发到下半部处理的完整流程:
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 是否设置,以及中断控制器是否配置为电平触发。电平触发中断在未处理完硬件状态前会持续拉低,导致线程无法退出。