2. QNX中断子系统架构

在QNX BSP移植中,中断子系统是连接硬件异常与软件处理的桥梁。理解其架构层次——从硬件中断控制器到内核向量表,再到用户态ISR注册——是排查DMA中断映射故障的前提。本章聚焦于QNX对中断控制器的抽象模型、向量表布局以及ISR注册的核心机制。

2.1 中断控制器抽象:GIC与INTC的统一接口

QNX内核通过一个统一的中断控制器抽象层来屏蔽不同SoC的GIC(Generic Interrupt Controller)或INTC(Interrupt Controller)差异。该抽象层定义了一组标准操作接口,BSP开发者只需实现这些接口即可完成移植。

核心抽象接口(struct sope_intr):每个中断控制器驱动必须实现以下回调,供内核调用。
接口函数 功能描述 典型GIC实现
intr_attach() 将中断源绑定到指定CPU核并设置触发方式 配置GICD_ICFGRn + GICD_ITARGETSRn
intr_detach() 解绑中断源,屏蔽中断 清除GICD_ICENABLERn对应位
intr_enable() 使能指定中断源 写GICD_ISENABLERn
intr_disable() 禁止指定中断源 写GICD_ICENABLERn
intr_ack() 中断应答(读取中断ID) 读GICC_IAR
intr_eoi() 中断结束(写EOI) 写GICC_EOIR
intr_mask() 在CPU接口层屏蔽中断 写GICC_PMR

下图展示了QNX中断控制器抽象层与硬件GIC/INTC之间的分层关系:

QNX 中断控制器抽象层与硬件映射 QNX 内核 (微内核 + procnto) struct sope_intr 抽象接口 attach / detach / enable / disable / ack / eoi / mask GICv2/v3 驱动 ARM Cortex-A 系列 INTC 驱动 (PowerPC/ARM) NXP i.MX / TI 平台 GIC 寄存器 (GICD/GICC) INTC 寄存器 (NVIC/UIC) BSP 移植点 实现 sope_intr 回调
移植要点:在BSP的init_intr()函数中,需要将硬件中断控制器的基地址、中断线数量、CPU亲和性等信息填充到struct sope_intr结构体中,并调用intr_register_controller()注册到内核。

2.2 中断向量表:从硬件异常到软件入口

QNX的中断向量表(IVT)并非传统意义上固定地址的跳转表,而是一个由内核管理的中断描述符数组。每个中断源对应一个表项,包含ISR地址、参数、优先级和标志位。

向量表的核心数据结构如下:

// QNX 内核中断描述符结构(简化)
struct intr_entry {
    uintptr_t   isr_addr;       // ISR 函数地址
    uintptr_t   isr_arg;        // ISR 参数(通常为设备上下文)
    uint32_t    vector;         // 硬件中断向量号
    uint32_t    flags;          // 触发方式、CPU亲和性等标志
    uint32_t    priority;       // 中断优先级(0-255)
    void        *next;          // 链表指针(用于共享中断)
};

向量表的布局与硬件中断号映射关系如下表所示:

中断向量范围 类型 说明 典型映射
0 - 15 CPU 内部异常 复位、未定义指令、SVC、预取中止等 ARM 异常向量表
16 - 31 软件中断 (SWI) 系统调用、IPI 核间中断 QNX 内核保留
32 - 1019 外设中断 (SPI/PPI) DMA、UART、GPIO 等外设中断 GIC SPI 中断号 + 32
1020 - 1023 特殊中断 伪中断、空闲中断 内核调试用

下图展示了中断向量表的组织方式以及中断触发后的查找流程:

QNX 中断向量表查找流程 硬件中断触发 GIC/INTC 仲裁 中断向量表 (IVT) intr_entry[vector] 数组 匹配 ISR 地址与参数 执行 ISR (isr_addr)(isr_arg) 向量号计算 硬件中断号 + 32 = 向量表索引 例如:DMA IRQ 33 → 向量 65
常见故障点:在BSP移植中,如果中断向量表基地址未正确重定位,或者中断号与向量索引的偏移量计算错误(例如GIC SPI中断号需要加32),会导致ISR无法被正确调用,表现为中断触发后系统无响应或进入异常。

2.3 中断服务例程(ISR)注册机制

QNX提供了一套完整的ISR注册API,允许驱动程序在用户态或内核态注册中断处理函数。注册机制的核心是InterruptAttach()和InterruptAttachEvent()函数。

2.3.1 ISR注册API对比

API 函数 使用场景 ISR 运行环境 特点
InterruptAttach() 传统中断处理 中断上下文(不可阻塞) 直接调用ISR函数,适合快速响应
InterruptAttachEvent() 事件驱动中断 中断上下文 + 事件队列 ISR中发送事件,由线程处理,适合复杂逻辑
InterruptAttach_r() 资源管理器中断 中断上下文 可重入版本,支持共享中断

2.3.2 典型ISR注册流程

以下代码展示了在QNX用户态驱动中注册DMA中断的标准流程:

#include <sys/neutrino.h>
#include <hw/inout.h>

// DMA 中断服务例程
const struct sigevent *dma_isr(void *arg, int id) {
    uintptr_t *status_reg = (uintptr_t *)arg;

    // 1. 读取DMA状态寄存器,确认中断源
    uint32_t status = in32(*status_reg);

    // 2. 清除中断标志
    out32(*status_reg, status | DMA_DONE_MASK);

    // 3. 返回事件通知线程(使用 InterruptAttachEvent 时)
    return (NULL);  // 或返回 &event
}

// 驱动初始化函数
int dma_driver_init(void) {
    int intr_id;
    uintptr_t dma_base = 0xFFE01000;  // DMA控制器基地址
    uintptr_t status_reg = dma_base + DMA_STATUS_OFFSET;

    // 注册中断:向量号 65(假设硬件IRQ 33)
    intr_id = InterruptAttach(65, dma_isr, &status_reg,
                              sizeof(status_reg), 0);
    if (intr_id == -1) {
        perror("InterruptAttach failed");
        return (-1);
    }

    // 使能DMA中断
    InterruptUnmask(65, intr_id);

    return (0);
}

2.3.3 中断注册的内部机制

当驱动程序调用InterruptAttach()时,QNX内核执行以下步骤:

  1. 参数校验:检查中断向量号是否在有效范围内(32-1019),以及调用进程是否具有权限。
  2. 向量表锁定:获取中断向量表的互斥锁,防止并发修改。
  3. 创建中断描述符:分配struct intr_entry,填充ISR地址、参数和标志。
  4. 插入链表:如果该向量已有ISR注册(共享中断),则将新描述符插入链表头部。
  5. 硬件配置:调用中断控制器驱动的intr_attach()回调,配置GIC/INTC寄存器。
  6. 使能中断:默认情况下中断处于屏蔽状态,驱动需调用InterruptUnmask()显式使能。

下图展示了ISR注册的完整内部流程:

InterruptAttach() 内部注册流程 1. 参数校验 2. 向量表锁定 3. 创建描述符 4. 插入链表 5. 硬件配置 6. 返回中断ID 共享中断处理 多个ISR可注册到同一向量 链表顺序:后注册的先执行 每个ISR需检查中断源 返回NULL表示未处理 硬件配置回调 intr_attach(vector, cpu_mask, trigger_mode) 配置GICD_ICFGR + ITARGETSR

2.4 中断映射故障排查要点

在DMA与中断映射的BSP移植中,以下三个环节最容易出现问题:

  • 中断号偏移错误:硬件手册中的IRQ号通常从0开始,但QNX向量表从32开始。例如,DMA控制器IRQ 33对应QNX向量65。计算错误会导致ISR注册到错误的中断源。
  • 中断控制器初始化顺序:必须在注册任何ISR之前完成GIC/INTC的初始化,包括设置优先级掩码(GICC_PMR)和使能组0/1中断。否则中断会被硬件屏蔽。
  • ISR上下文限制:在ISR中不能调用任何可能阻塞的函数(如malloc()、printf()、pthread_mutex_lock())。如果需要复杂处理,应使用InterruptAttachEvent()将工作委托给线程。
调试技巧:在BSP早期移植阶段,可以在ISR中写入一个全局变量或GPIO寄存器来验证中断是否被正确触发。使用printf()在ISR中是不安全的,建议使用TraceEvent()或硬件调试器。