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 对比
特性 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 上半部与下半部的协作流程

下图展示了从硬件中断触发到下半部处理完成的完整数据流:

上半部与下半部协作流程图 上半部 → 下半部协作流程 硬件中断触发 上半部:读取状态寄存器 清除中断标志 · 保存时间戳 调度下半部(tasklet/workqueue/threaded IRQ) tasklet(软中断) DMA重填 · 帧标记 workqueue(内核线程) I2C配置 · 内存分配 threaded IRQ 复杂握手 · 长耗时操作 下半部处理完成

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