3、蓝牙中断处理实战:蓝牙控制器中断注册、中断服务函数编写、中断共享与竞争
好,咱们直接进入正题。蓝牙控制器跑起来之后,最核心的交互方式就是中断。说白了,芯片不会一直轮询蓝牙模块有没有数据来,那样太浪费CPU了。正确的做法是:蓝牙模块有事了,拉一根中断线,告诉CPU“嘿,我有活儿了”。今天我们就来聊聊这个流程怎么落地。
3.1 中断注册:把蓝牙的中断线“绑定”到系统
在Linux里,注册中断用的是request_irq()。我个人习惯在蓝牙驱动的probe()函数里做这件事。为什么?因为这时候设备已经探测到了,硬件资源也准备好了,注册中断的时机刚刚好。
来看一个典型的注册代码:
static int bluecore_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct bluecore_data *data;
int irq, ret;
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
data->irq = irq;
ret = devm_request_irq(dev, irq, bluecore_irq_handler,
IRQF_TRIGGER_FALLING | IRQF_SHARED,
"bluecore_bt", data);
if (ret) {
dev_err(dev, "failed to request irq %d\n", irq);
return ret;
}
dev_info(dev, "irq %d registered\n", irq);
return 0;
}
这里有几个关键点。第一,我用了devm_request_irq()而不是request_irq()。这是设备管理版本的API,好处是驱动卸载时自动释放中断,省得你忘了写free_irq()。我在项目中遇到过因为忘记释放中断导致系统崩溃的惨案,从那以后我就只用devm_系列了。
第二,注意标志位IRQF_TRIGGER_FALLING。蓝牙控制器通常用下降沿触发中断。但有些芯片也支持高电平触发,你得看数据手册。嗯,这里要注意:如果选错了触发方式,中断要么不触发,要么疯狂触发,CPU直接被打满。
3.2 中断服务函数:别在里面干重活
中断服务函数(ISR)是跑在中断上下文里的。这意味着什么?不能睡眠,不能调用mutex_lock(),不能做内存分配(除非用GFP_ATOMIC)。说白了,ISR里只能做最轻量级的事。
我一般把ISR写成这样:
static irqreturn_t bluecore_irq_handler(int irq, void *dev_id)
{
struct bluecore_data *data = dev_id;
u32 int_status;
// 1. 读取中断状态寄存器
int_status = readl(data->regs + BT_INT_STAT);
if (!int_status)
return IRQ_NONE;
// 2. 清除中断标志
writel(int_status, data->regs + BT_INT_CLR);
// 3. 根据中断类型做不同处理
if (int_status & BT_INT_RX_DATA) {
// 有接收数据,调度workqueue处理
schedule_work(&data->rx_work);
}
if (int_status & BT_INT_TX_DONE) {
// 发送完成,唤醒等待队列
complete(&data->tx_done);
}
if (int_status & BT_INT_ERROR) {
// 错误中断,记录一下
dev_err_ratelimited(data->dev, "BT error interrupt\n");
}
return IRQ_HANDLED;
}
你看,ISR里只做了三件事:读状态、清标志、调度下半部。真正的数据搬运、协议解析,都扔到workqueue或者tasklet里去。我曾经犯过一个错误,在ISR里直接调用memcpy()拷贝了几百字节的数据,结果导致系统中断延迟飙升,蓝牙音频断断续续。后来改成在workqueue里拷贝,问题就解决了。
3.3 中断共享:多个设备抢一根线
嵌入式系统里中断线是稀缺资源。很多时候,蓝牙控制器和其他设备(比如Wi-Fi、传感器)共享一根中断线。这时候就要用IRQF_SHARED标志。
共享中断的ISR有个特点:它必须检查中断是否真的来自自己的设备。如果发现不是,要返回IRQ_NONE。内核会遍历所有注册在这根线上的ISR,直到有人返回IRQ_HANDLED。
看这个例子:
static irqreturn_t bluecore_irq_handler(int irq, void *dev_id)
{
struct bluecore_data *data = dev_id;
u32 int_status;
// 先读中断状态,如果为0,说明不是我的中断
int_status = readl(data->regs + BT_INT_STAT);
if (!int_status) {
// 不是我的设备,返回IRQ_NONE
return IRQ_NONE;
}
// 处理中断...
writel(int_status, data->regs + BT_INT_CLR);
schedule_work(&data->rx_work);
return IRQ_HANDLED;
}
dev_id参数绝对不能为NULL。因为内核需要用这个参数来区分不同的设备。如果你传了NULL,free_irq()的时候内核就不知道要释放哪个设备的中断。我曾经在调试一个Wi-Fi和蓝牙共享中断的板子时,因为dev_id传了NULL,导致卸载驱动时内核直接Oops。
3.4 中断竞争:别让数据乱掉
中断竞争是个老生常谈的问题。蓝牙驱动里最常见的竞争场景是:ISR和workqueue同时访问同一个数据结构。
举个例子,接收数据时,ISR把数据放到一个环形缓冲区里,workqueue从缓冲区里取数据。如果两者同时操作,缓冲区指针就乱了。
我的解决方案是:用spin_lock保护共享数据。注意,ISR里只能用spin_lock_irqsave(),因为要关本地中断防止死锁。
static irqreturn_t bluecore_irq_handler(int irq, void *dev_id)
{
struct bluecore_data *data = dev_id;
unsigned long flags;
u32 int_status;
int_status = readl(data->regs + BT_INT_STAT);
if (!int_status)
return IRQ_NONE;
writel(int_status, data->regs + BT_INT_CLR);
if (int_status & BT_INT_RX_DATA) {
// 用spin_lock_irqsave保护环形缓冲区
spin_lock_irqsave(&data->rx_lock, flags);
// 把硬件FIFO里的数据搬到环形缓冲区
bluecore_read_fifo(data);
spin_unlock_irqrestore(&data->rx_lock, flags);
schedule_work(&data->rx_work);
}
return IRQ_HANDLED;
}
static void bluecore_rx_work(struct work_struct *work)
{
struct bluecore_data *data = container_of(work, struct bluecore_data, rx_work);
unsigned long flags;
spin_lock_irqsave(&data->rx_lock, flags);
// 从环形缓冲区取数据,交给协议栈
bluecore_process_rx_data(data);
spin_unlock_irqrestore(&data->rx_lock, flags);
}
这里有个细节:workqueue里也要用spin_lock_irqsave,而不是spin_lock。为什么?因为workqueue可能被中断打断,如果中断里也拿了同一个锁,就死锁了。你想想看,ISR在等workqueue释放锁,workqueue在等ISR执行完,这不就卡死了吗?
3.5 实战中的几个小技巧
最后分享几个我在蓝牙中断调试中积累的经验:
- 中断风暴检测:如果蓝牙模块异常,可能会疯狂触发中断。我习惯在ISR里加一个计数器,如果1秒内中断次数超过阈值,就打印警告并暂时禁用中断。等系统稳定了再重新使能。
- 使用
dev_err_ratelimited:调试时别用dev_err打印中断信息,否则中断风暴时控制台会被刷爆。用dev_err_ratelimited,内核会自动限速。 - 中断延迟测量:用
ktime_get()在ISR入口和出口打时间戳,可以算出中断处理耗时。如果超过100微秒,就得考虑优化了。
好了,蓝牙中断处理这块就聊到这儿。下一节我们会讲DMA怎么和中断配合,把数据搬运的效率再提一个档次。到时候见。