3. 中断机制基础:Linux中断子系统、中断上半部与下半部、中断上下文与进程上下文
中断是嵌入式系统响应外部事件最核心的机制。在RK3399显示驱动中,VSync(垂直同步)中断、DMA传输完成中断、显示控制器(VOP)状态中断共同构成了驱动与硬件之间的“信号桥梁”。理解Linux中断子系统的分层设计、上半部与下半部的拆分哲学,以及中断上下文与进程上下文的严格界限,是编写高效、无死锁显示驱动的基石。
3.1 Linux中断子系统架构
Linux内核将中断处理抽象为三个层次,从硬件信号到驱动回调,逐层解耦:
| 层次 | 组件 | 职责 | RK3399相关 |
|---|---|---|---|
| 硬件层 | GIC-400(通用中断控制器) | 接收外设中断信号,进行优先级仲裁,向CPU核触发IRQ/FIQ | VOP、MIPI DSI、eDP、HDMI 中断线均连接至GIC |
| 核心层 | irq_chip / irq_domain | 管理中断控制器注册、中断号映射、中断使能/屏蔽/ACK | 设备树中 interrupt-parent = <&gic> 建立映射 |
| 驱动层 | request_irq() / devm_request_irq() | 驱动注册中断处理函数,指定触发方式(边沿/电平) | VOP驱动调用 devm_request_irq(dev, irq, vop_isr, 0, "vop", vop) |
关键概念: 在RK3399上,GIC-400支持SPI(共享外设中断)和PPI(私有外设中断)。显示相关中断均为SPI,中断号范围在32~223之间,具体编号由设备树中的
interrupts = <&GIC_SPI 148 IRQ_TYPE_LEVEL_HIGH>指定。
3.1.1 中断子系统数据流
下图展示了从VOP硬件产生中断信号到驱动处理函数被调用的完整路径:
3.2 中断上半部(Top Half)与下半部(Bottom Half)
中断处理必须“快进快出”。Linux将中断处理拆分为两个阶段:
- 上半部(硬中断上下文): 在关闭本地CPU中断(或至少关闭当前中断线)的情况下执行。只做最紧急、最轻量的工作,如:清除中断标志位、拷贝硬件寄存器状态、标记中断发生。必须毫秒级甚至微秒级完成。
- 下半部(软中断/任务/workqueue上下文): 在中断使能的环境下执行,可以睡眠、可以持有锁、可以执行耗时操作。负责真正的数据处理,如:更新显示缓冲区指针、启动下一次DMA传输、唤醒等待队列。
| 对比维度 | 上半部(Top Half) | 下半部(Bottom Half) |
|---|---|---|
| 执行上下文 | 中断上下文(不可睡眠) | 软中断 / tasklet / workqueue(可睡眠) |
| 中断状态 | 当前中断线通常被屏蔽 | 所有中断均已使能 |
| 允许操作 | 仅原子操作、寄存器读写、标记位设置 | 内存分配、互斥锁、DMA API、调度 |
| 典型耗时 | < 100 μs | 可到数毫秒 |
| RK3399显示示例 | 读取VOP_INTR_STATUS寄存器,清除VSync位 | 更新vblank计数器,触发framebuffer切换 |
RK3399实践建议: VOP的VSync中断处理中,上半部仅读取
VOP_INTR_STATUS寄存器并写1清除中断位,然后调用tasklet_schedule()或workqueue。下半部才执行drm_crtc_handle_vblank()和DMA描述符重装。实测表明,若在下半部执行超过500μs的操作,会导致帧率抖动。
3.2.1 下半部机制对比
Linux提供三种下半部实现机制,驱动开发者需根据场景选择:
| 机制 | 特点 | 是否可睡眠 | 并发模型 | 显示驱动适用场景 |
|---|---|---|---|---|
| tasklet | 基于软中断,同一tasklet串行执行 | 否 | 单CPU串行 | VSync帧计数更新、简单状态机推进 |
| workqueue | 基于内核线程,可阻塞、可调度 | 是 | 多CPU并行(默认) | DMA描述符分配、FB内存映射、I2C配置 |
| 软中断(softirq) | 最底层,性能最高,需自行处理并发 | 否 | 多CPU并行 | 极少直接使用,网络/块设备场景为主 |
常见错误: 在RK3399显示驱动的上半部中调用
drm_crtc_handle_vblank(),该函数内部会获取spin_lock并可能唤醒等待队列,若此时持有其他锁,极易导致死锁。正确做法:上半部只做寄存器ACK,下半部调用DRM核心API。
3.3 中断上下文与进程上下文
理解这两种上下文的区别,是避免内核崩溃和调度延迟的前提:
- 进程上下文: 当前运行在某个进程的上下文中,可以通过
current宏访问进程描述符,可以睡眠、可以被抢占、可以持有mutex。系统调用、内核线程、workqueue处理函数均运行在此上下文中。 - 中断上下文: 当前运行在中断服务路径中,没有关联的进程描述符,
current宏指向被中断的进程(不可信赖)。不可睡眠、不可持有mutex、不可访问用户空间。上半部和tasklet均运行在此上下文中。
3.3.1 上下文检测与断言
内核提供宏用于运行时检测当前上下文,驱动调试时非常有用:
#include <linux/preempt.h>
#include <linux/hardirq.h>
/* 检测是否在中断上下文中 */
if (in_interrupt()) {
pr_debug("当前在中断上下文中,不可睡眠\n");
}
/* 检测是否在硬中断上半部 */
if (in_irq()) {
pr_debug("当前在硬中断上半部\n");
}
/* 检测是否在软中断(含tasklet) */
if (in_softirq()) {
pr_debug("当前在软中断或tasklet中\n");
}
/* 断言:确保不在中断上下文中(用于workqueue回调) */
might_sleep(); // 若在中断上下文,触发内核警告
3.3.2 上下文切换示意图
下图展示了一次VSync中断触发后,上下文从进程→中断→下半部→进程的完整切换路径:
3.4 RK3399显示驱动中断设计要点
基于以上理论基础,在RK3399 VOP驱动中,中断处理的设计遵循以下原则:
- 中断号获取: 通过设备树解析或
platform_get_irq()获取VOP中断号,通常为148(VOPB)和149(VOPL)。 - 注册时机: 在
vop_bind()或probe()函数中,完成时钟和电源初始化后,调用devm_request_irq()注册中断。 - 触发方式: 使用
IRQF_TRIGGER_HIGH(电平触发)或IRQF_TRIGGER_RISING(边沿触发),VOP通常使用电平触发,避免中断丢失。 - 上半部最小化: 仅读取
VOP_INTR_STATUS寄存器,判断中断类型(VSync/LineFlag/BusError),清除中断位,然后调用tasklet_schedule()或queue_work()。 - 下半部数据处理: 在tasklet或workqueue中调用
drm_crtc_handle_vblank()、更新vop->vblank_count、检查DMA传输完成状态。
RK3399实测数据: 在4K@60Hz显示模式下,VSync中断频率为60Hz,每次中断处理总耗时需控制在16.67ms以内。上半部实测耗时约3~5μs,下半部(含DRM核心调用)约50~100μs,完全满足实时性要求。若下半部执行超过1ms,则会出现帧率下降至30fps的现象。
3.5 本章小结
- Linux中断子系统通过GIC → irq_domain → 驱动回调三层架构,实现了硬件中断到软件处理的解耦。
- 上半部执行在中断上下文中,必须快速、不可睡眠;下半部执行在软中断或进程上下文中,可处理耗时逻辑。
- 中断上下文与进程上下文有严格界限:中断上下文中禁止睡眠、禁止持有互斥锁、禁止访问用户空间。
- RK3399显示驱动中,VSync中断处理遵循“上半部清标志、下半部做更新”的原则,确保帧率稳定。