3. VxWorks 中断控制器:类型、屏蔽与触发方式

中断控制器是 CPU 与外部中断源之间的桥梁,负责中断请求的收集、优先级仲裁、向量分发以及屏蔽控制。在 VxWorks 实时系统中,中断控制器的配置直接影响中断延迟和系统确定性。本章深入剖析 VxWorks 支持的两种主流中断控制器——i8259(PIC)和 GIC(Generic Interrupt Controller),并详细讲解中断屏蔽/使能机制与触发方式的选择策略。

核心要点:中断控制器的类型决定了中断向量号的分配方式、优先级模型以及屏蔽粒度。VxWorks 通过 BSP(Board Support Package)抽象层统一管理不同控制器的差异,但底层寄存器操作仍需要开发者理解。

3.1 中断控制器类型对比

VxWorks 支持多种中断控制器架构,最常见的是传统 x86 平台的 i8259A(PIC)和 ARM 平台的 GIC(v2/v3)。下表从关键维度对比两者的差异:

特性 i8259A (PIC) GICv2 (ARM) GICv3 (ARM)
中断源数量 最多 15 个(级联后) 最多 1020 个(SPI+PPI+SGI) 最多 1020+ 个(支持更多 SPI)
优先级模型 固定优先级(IRQ 0 最高) 可编程优先级(0-255) 可编程优先级(0-255)
中断向量 固定向量号(0x08-0x0F) 软件可配置中断 ID 软件可配置中断 ID
中断分组 不支持 Group 0(安全)/ Group 1(非安全) Group 0/1/1S
亲和性(CPU) 不支持 支持(路由到指定 CPU) 支持(更灵活的亲和性)
典型平台 x86、Pentium Cortex-A9/A15 Cortex-A53/A72/A76
选型建议:在 VxWorks 7 中,GICv3 是 ARMv8 平台的首选。如果使用传统 x86 板卡,i8259 仍被广泛支持,但需要注意其 15 个中断源的限制,级联设计会增加中断延迟。
中断控制器架构对比:i8259 vs GIC i8259A (PIC) IRQ 0 (Timer) IRQ 1 (Keyboard) IRQ 3 (COM2) IRQ 4 (COM1) IRQ 2 (Cascade) Slave PIC 固定优先级:IRQ0 > IRQ1 > ... > IRQ7 GICv2/v3 (ARM) SGI (0-15) PPI (16-31) SPI (32-1019) LPI (GICv3) Distributor + CPU Interface 可编程亲和性:路由到指定 CPU 核心

3.2 中断屏蔽与使能机制

中断屏蔽是实时系统中控制中断响应顺序的关键手段。VxWorks 提供两级屏蔽:CPU 级屏蔽(通过中断控制器寄存器)和 系统级屏蔽(通过 VxWorks 中断锁)。

3.2.1 CPU 级屏蔽:操作中断控制器寄存器

在 i8259 中,屏蔽通过操作 IMR(Interrupt Mask Register) 实现。在 GIC 中,屏蔽通过 ICDISER(Interrupt Set-Enable Register) 和 ICDICER(Interrupt Clear-Enable Register) 控制。

/* i8259 屏蔽示例:屏蔽 IRQ3 和 IRQ4 */
#define PIC_MASTER_IMR    0x21
#define PIC_SLAVE_IMR     0xA1

void mask_irq_example(void) {
    UINT8 imr_val;
    /* 读取当前屏蔽寄存器 */
    imr_val = sysInByte(PIC_MASTER_IMR);
    /* 屏蔽 IRQ3 (bit3) 和 IRQ4 (bit4) */
    imr_val |= (1 << 3) | (1 << 4);
    sysOutByte(PIC_MASTER_IMR, imr_val);
}

/* GICv2 屏蔽示例:使能 SPI 中断 ID=67 */
#define GICD_ISENABLER(n)   (GICD_BASE + 0x100 + (n) * 4)

void gic_enable_irq(UINT32 irq_id) {
    UINT32 reg_offset = irq_id / 32;
    UINT32 bit_mask   = 1 << (irq_id % 32);
    UINT32 *reg = (UINT32 *)GICD_ISENABLER(reg_offset);
    /* 写 1 使能中断 */
    *reg = bit_mask;
}
注意:在 i8259 中,屏蔽一个中断后,该中断的挂起状态仍然会累积,但不会传递给 CPU。在 GIC 中,屏蔽中断会阻止 Distributor 将中断转发给 CPU Interface,但挂起状态仍然保留在 Distributor 中。

3.2.2 系统级屏蔽:VxWorks 中断锁

VxWorks 提供 intLock() 和 intUnlock() 函数,用于在 ISR 或任务中临时关闭所有可屏蔽中断。这是实现临界区保护的最简单方式,但会显著增加中断延迟。

/* 中断锁使用示例 */
void critical_section_example(void) {
    int lock_key;
    
    /* 保存当前中断状态并关闭所有中断 */
    lock_key = intLock();
    
    /* 临界区:访问共享数据结构 */
    shared_data->counter++;
    shared_data->flags |= 0x01;
    
    /* 恢复之前的中断状态 */
    intUnlock(lock_key);
}
屏蔽级别 作用范围 延迟影响 典型使用场景
intLock() 所有可屏蔽中断 高(全局关闭) 极短临界区(< 10 µs)
intCtlLock() 指定中断控制器 中(局部关闭) 多核系统中的核间同步
寄存器级屏蔽 单个中断源 低(精确控制) 外设驱动中的流控
VxWorks 中断屏蔽层级 intLock() / intUnlock() — 全局中断屏蔽 intCtlLock() — 控制器级屏蔽 寄存器级屏蔽 — 单个中断源 屏蔽粒度越细,中断延迟影响越小,但编程复杂度越高

3.3 中断触发方式

中断触发方式决定了中断信号如何被中断控制器识别。VxWorks 支持两种基本触发方式:电平触发(Level-Triggered) 和 边沿触发(Edge-Triggered)。在 GIC 中,还支持 脉冲触发(Pulsed) 用于 LPI。

3.3.1 电平触发 vs 边沿触发

特性 电平触发 边沿触发
信号识别 检测高/低电平持续状态 检测上升沿或下降沿跳变
中断丢失风险 低(电平持续存在) 高(脉冲过窄可能丢失)
共享中断 支持(需要电平触发) 不支持(边沿无法共享)
去抖动要求 低 高(需要硬件消抖)
典型外设 GPIO、I2C、SPI 定时器、外部按键
ISR 处理要求 必须清除中断源,否则持续触发 只需处理一次,不会重入
性能影响:电平触发的中断在 ISR 返回前必须确保中断源被清除,否则会立即再次触发,导致 ISR 重入。边沿触发的中断不会重入,但容易因信号毛刺产生误触发。在 VxWorks 中,推荐对共享中断总线使用电平触发。

3.3.2 GIC 中的触发方式配置

在 GICv2 中,触发方式通过 ICDICFR(Interrupt Configuration Register) 配置。每个中断占用 2 位:

/* GICv2 触发方式配置示例 */
#define GICD_ICFGR(n)       (GICD_BASE + 0xC00 + (n) * 4)

void gic_set_trigger(UINT32 irq_id, BOOL level_triggered) {
    UINT32 reg_offset = irq_id / 16;
    UINT32 bit_pos    = (irq_id % 16) * 2;
    UINT32 *reg = (UINT32 *)GICD_ICFGR(reg_offset);
    UINT32 val;
    
    val = *reg;
    val &= ~(0x3 << bit_pos);   /* 清除配置位 */
    if (level_triggered) {
        val |= (0x1 << bit_pos); /* 电平触发 */
    } else {
        val |= (0x2 << bit_pos); /* 边沿触发 */
    }
    *reg = val;
}

3.3.3 i8259 中的触发方式配置

在 i8259 中,触发方式通过 ICW1(Initialization Command Word 1) 的 LTIM 位配置。LTIM=1 表示电平触发,LTIM=0 表示边沿触发。所有中断共享相同的触发方式。

/* i8259 初始化时配置触发方式 */
#define PIC_MASTER_ICW1     0x20
#define PIC_SLAVE_ICW1      0xA0

void pic_init_trigger(BOOL level_mode) {
    UINT8 icw1 = 0x11;  /* ICW1 基本值:需要 ICW4 */
    if (level_mode) {
        icw1 |= 0x08;   /* 设置 LTIM 位,电平触发 */
    }
    sysOutByte(PIC_MASTER_ICW1, icw1);
    sysOutByte(PIC_SLAVE_ICW1, icw1);
}
中断触发方式选择流程 选择触发方式 中断源是否共享? 是 否 电平触发 (Level-Triggered) ISR 必须清除中断源,防止重入 边沿触发 (Edge-Triggered) 需要硬件消抖,防止毛刺误触发 推荐:共享总线用电平,独享信号用边沿

3.4 中断控制器初始化流程

在 VxWorks BSP 中,中断控制器的初始化通常在 sysHwInit() 或 sysIntInit() 中完成。以下是标准初始化步骤:

  1. 禁用所有中断源:通过屏蔽寄存器或使能寄存器清零,防止初始化过程中产生意外中断。
  2. 配置触发方式:根据外设需求设置电平或边沿触发。
  3. 设置优先级:在 GIC 中配置每个中断的优先级值(0-255,0 最高)。
  4. 配置中断向量表:将中断 ID 映射到 ISR 入口地址。
  5. 使能中断控制器:设置全局使能位,允许中断传递到 CPU。
  6. 使能 CPU 中断:通过 CPSR(ARM)或 EFLAGS(x86)打开 CPU 中断位。
/* VxWorks GICv2 初始化伪代码 */
void sysIntInit(void) {
    /* 1. 禁用所有 SPI 中断 */
    for (int i = 0; i < 32; i++) {
        *(volatile UINT32 *)(GICD_BASE + GICD_ICENABLER(i)) = 0xFFFFFFFF;
    }
    
    /* 2. 设置所有 SPI 为电平触发(默认) */
    for (int i = 0; i < 64; i++) {
        *(volatile UINT32 *)(GICD_BASE + GICD_ICFGR(i)) = 0x55555555;
    }
    
    /* 3. 设置默认优先级(0xA0,较低优先级) */
    for (int i = 0; i < 255; i++) {
        *(volatile UINT8 *)(GICD_BASE + GICD_IPRIORITYR(i)) = 0xA0;
    }
    
    /* 4. 使能 Distributor */
    *(volatile UINT32 *)(GICD_BASE + GICD_CTLR) = 0x1;
    
    /* 5. 使能 CPU Interface */
    *(volatile UINT32 *)(GICC_BASE + GICC_CTLR) = 0x1;
    
    /* 6. 设置优先级掩码(允许所有优先级) */
    *(volatile UINT32 *)(GICC_BASE + GICC_PMR) = 0xFF;
}
调试技巧:在 VxWorks 中,可以使用 intShow() 命令查看当前中断控制器的状态,包括已使能的中断、挂起状态和优先级配置。对于 GIC,gicShow() 提供更详细的寄存器转储。

3.5 性能优化要点

中断控制器的配置直接影响中断延迟(Interrupt Latency)和中断吞吐量。以下是针对不同控制器的优化建议:

  • 减少屏蔽粒度:优先使用寄存器级屏蔽代替 intLock(),避免全局关闭中断。
  • 合理设置优先级:将时间敏感的中断(如定时器、网络)设置为高优先级,非关键中断设置为低优先级。
  • 利用亲和性:在多核系统中,将不同外设的中断路由到不同 CPU 核心,避免单核过载。
  • 避免电平触发陷阱:使用电平触发时,确保 ISR 在返回前清除中断源,否则会导致无限重入。
  • 批量操作寄存器:在 GIC 中,使用 32 位寄存器批量使能/屏蔽中断,减少总线访问次数。
常见错误:在 i8259 中,如果忘记发送 EOI(End of Interrupt)命令,中断控制器会认为 ISR 仍在执行,从而屏蔽所有同级和低级中断。在 GIC 中,EOI 通过写 GICC_EOIR 寄存器完成。
/* i8259 EOI 示例 */
void pic_eoi(int irq) {
    if (irq >= 8) {
        sysOutByte(PIC_SLAVE_ICW2, 0x20);  /* 发送 EOI 到从片 */
    }
    sysOutByte(PIC_MASTER_ICW2, 0x20);     /* 发送 EOI 到主片 */
}

/* GICv2 EOI 示例 */
void gic_eoi(UINT32 irq_id) {
    *(volatile UINT32 *)(GICC_BASE + GICC_EOIR) = irq_id;
}