4. 上半部(Top Half)实现:快速响应中断、读取状态寄存器、调度下半部
中断处理的上半部(Top Half)是驱动响应硬件事件的“第一现场”。在 V4L2 摄像头驱动中,当传感器完成一帧数据采集或发生同步错误时,硬件中断线被拉高,内核立即调用注册的中断处理函数。上半部的核心原则是:快进快出——只做最必要的硬件操作,将耗时任务推迟到下半部。
4.1 上半部的职责边界
上半部在中断上下文中执行,此时中断被关闭(或至少当前中断线被屏蔽),因此不能睡眠、不能获取信号量、不能执行阻塞 I/O。其典型职责包括:
- 确认中断源:读取状态寄存器,判断是帧同步中断、错误中断还是溢出中断。
- 清除中断标志:写回状态寄存器或特定清除寄存器,防止重复触发。
- 保存关键硬件状态:将行计数、帧计数、错误码等易失信息复制到内存变量。
- 调度下半部:根据中断类型选择 tasklet、workqueue 或 threaded IRQ 机制,将数据搬运、帧处理等任务移交。
性能要点: 上半部执行时间应控制在微秒级。若读取状态寄存器需要 SPI/I2C 总线操作,务必使用非阻塞的寄存器映射(如 ioremap)或 DMA 读取,避免总线等待。
4.2 标准上半部实现模板
以下代码展示了一个典型的 V4L2 摄像头驱动上半部实现。它读取状态寄存器、判断中断类型、调度 tasklet 下半部:
static irqreturn_t sensor_irq_handler(int irq, void *dev_id)
{
struct sensor_device *sensor = (struct sensor_device *)dev_id;
u32 status;
/* 1. 读取中断状态寄存器(假设寄存器已 ioremap) */
status = ioread32(sensor->base + REG_IRQ_STATUS);
/* 2. 检查是否为本设备的中断 */
if (!(status & IRQ_MASK_FRAME_SYNC)) {
/* 非预期中断,可能是共享中断线上的其他设备 */
return IRQ_NONE;
}
/* 3. 清除中断标志(写1清除) */
iowrite32(IRQ_MASK_FRAME_SYNC, sensor->base + REG_IRQ_CLEAR);
/* 4. 保存关键状态到驱动私有数据 */
sensor->irq_timestamp = ktime_get();
sensor->frame_count++;
/* 5. 调度 tasklet 下半部处理帧数据 */
tasklet_schedule(&sensor->frame_tasklet);
/* 6. 返回 IRQ_HANDLED 表示已处理 */
return IRQ_HANDLED;
}
| 操作 | 耗时估算 | 注意事项 |
|---|---|---|
| ioread32 读取状态 | ~50-200 ns | 取决于总线类型(内存映射 vs. SPI) |
| iowrite32 清除标志 | ~50-200 ns | 必须确保写操作被硬件立即识别 |
| ktime_get() | ~20-50 ns | 使用 vdso 加速,避免调用 syscall |
| tasklet_schedule() | ~100-300 ns | 原子操作,不可睡眠 |
4.3 三种下半部调度机制对比
上半部调度下半部时,需要根据任务特性选择合适机制。下表列出了三种主流方案的差异:
| 特性 | tasklet | workqueue | threaded IRQ |
|---|---|---|---|
| 执行上下文 | 软中断(原子) | 内核线程(可睡眠) | 内核线程(可睡眠) |
| 是否可睡眠 | 否 | 是 | 是 |
| 调度延迟 | 低(~1-10 µs) | 中(~10-100 µs) | 中(~10-100 µs) |
| 并发模型 | 单 CPU 串行 | 多 worker 并行 | 每 IRQ 独立线程 |
| 适用场景 | 轻量帧处理、DMA 启动 | I2C/SPI 配置、内存分配 | 复杂硬件握手、长耗时操作 |
选型建议: 对于摄像头驱动中常见的帧数据搬运(如 DMA 描述符重填),推荐使用 tasklet 以获得最低延迟;若下半部需要操作 I2C 调节增益或曝光,则应使用 workqueue 或 threaded IRQ 以避免原子上下文违规。
4.4 上半部与下半部的协作流程
下图展示了从硬件中断触发到下半部处理完成的完整数据流:
4.5 上半部实现中的常见陷阱
- 中断标志未清除导致死循环:若清除操作被编译器优化或写操作未到达硬件,中断会持续触发,造成 CPU 软锁。建议在清除后添加 read-back 屏障(如
ioread32回读)。 - 在 tasklet 中调用睡眠函数:tasklet 运行在软中断上下文,调用
msleep()或mutex_lock()会导致内核崩溃。务必使用spin_lock()保护共享数据。 - 共享中断线误判:多个设备共享同一 IRQ 线时,上半部必须验证中断状态寄存器是否属于本设备,否则应返回
IRQ_NONE。 - ktime_get() 精度不足:在低分辨率定时器配置下,
ktime_get()可能返回粗粒度时间戳。对于帧间隔测量,应使用ktime_get_real()或硬件 PTP 时钟。
调试建议: 在开发阶段,可在上半部入口和出口处添加
trace_printk() 或使用 ftrace 的 irq_handler_entry/exit 事件,测量上半部实际执行时间。若超过 10 µs,应考虑将部分操作移至下半部。
4.6 上半部性能优化清单
| 优化项 | 方法 | 预期收益 |
|---|---|---|
| 减少寄存器访问次数 | 合并状态读取,一次 ioread32 读取多个状态位 | 降低 30-50% 总线访问延迟 |
| 使用 relaxed I/O 访问 | 在不需要严格排序时使用 ioread32_relaxed() |
减少内存屏障开销 |
| 预分配下半部数据结构 | 在 probe 阶段分配好 tasklet/workqueue 对象 | 避免上半部中动态分配内存 |
| 使用 per-CPU 变量 | 将帧计数等热数据声明为 DEFINE_PER_CPU |
避免缓存行 bouncing |
| 避免函数调用深度 | 将关键路径内联或使用 __always_inline |
减少调用开销 ~5-10 ns |