一、中断嵌套的核心原理

中断嵌套的本质是高优先级中断可以打断正在执行的低优先级中断,从而保证紧急事件能够得到及时处理。其完整流程可分为以下5个阶段:

  1. 低优先级中断执行:CPU正在处理一个低优先级中断服务函数。
  2. 高优先级中断触发:此时一个更高优先级的中断请求到来。
  3. 现场保护:CPU自动将当前程序计数器(PC)和状态寄存器压入栈中,暂停当前中断的执行。
  4. 高优先级中断处理:跳转到高优先级中断服务函数执行。
  5. 现场恢复:高优先级中断执行完毕后,从栈中恢复之前的现场,继续执行被打断的低优先级中断。

在Cortex-M架构中,这一过程由硬件自动完成,无需开发者手动保存和恢复现场。同时,Cortex-M内核在进入中断时,会自动将当前中断的优先级写入优先级掩码寄存器,屏蔽同级和更低优先级的中断,只有更高优先级的中断可以自动抢占,这一机制无需在代码中手动开启总中断。

二、中断嵌套的关键规则

1. 优先级数值越小,优先级越高

例如,优先级0的中断可以打断优先级1的中断,反之则不行。

2. 系统异常优先级固定

复位、NMI(不可屏蔽中断)、HardFault(硬件错误)等系统异常的优先级由硬件固定,高于所有用户可配置的中断,用户中断永远无法抢占这些异常。

3. 同优先级中断无法嵌套

两个相同优先级的中断不能互相嵌套,只有更高优先级的中断才能抢占低优先级中断。

三、中断嵌套的代码实现(基于STM32 HAL库)

以下是一个基于STM32的中断嵌套示例,通过配置两个外部中断(EXTI0和EXTI1),实现高优先级中断抢占低优先级中断的效果。

1. 优先级配置

// 配置EXTI0(PA0)为高优先级中断(抢占优先级1,响应优先级0)
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);

// 配置EXTI1(PA1)为低优先级中断(抢占优先级2,响应优先级0)
HAL_NVIC_SetPriority(EXTI1_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(EXTI1_IRQn);

2. 中断服务函数

// EXTI0中断服务函数(高优先级)
void EXTI0_IRQHandler(void) {
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

// EXTI1中断服务函数(低优先级)
void EXTI1_IRQHandler(void) {
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_1);
}

3. 中断回调函数

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == GPIO_PIN_0) {
        // 高优先级中断处理逻辑
        printf("高优先级中断(EXTI0)执行中...\r\n");
        // 模拟耗时操作
        HAL_Delay(50);
        printf("高优先级中断(EXTI0)执行完成\r\n");
    } else if (GPIO_Pin == GPIO_PIN_1) {
        // 低优先级中断处理逻辑
        printf("低优先级中断(EXTI1)执行中...\r\n");
        // 模拟耗时操作,方便触发嵌套
        HAL_Delay(100);
        printf("低优先级中断(EXTI1)执行完成\r\n");
    }
}

在这个示例中,当EXTI1(低优先级)正在执行时,如果EXTI0(高优先级)触发,会立即抢占EXTI1的执行,待EXTI0执行完成后,再继续执行EXTI1。

四、中断嵌套带来的问题:共享数据错乱

中断嵌套虽然提升了系统的实时性,但也带来了共享数据错乱的问题。当高低优先级中断共享同一个全局变量时,可能会出现以下场景:

  1. 低优先级中断正在修改全局变量,执行到一半时被高优先级中断抢占。
  2. 高优先级中断也修改了同一个全局变量。
  3. 高优先级中断执行完成后,低优先级中断继续执行,此时全局变量的值已经被修改,导致数据错乱。

例如,以下代码中,shared_var是一个全局变量,被两个中断共享:

volatile uint32_t shared_var = 0;

void low_priority_isr(void) {
    // 低优先级中断修改共享变量
    shared_var = 100;
    // 模拟被高优先级中断抢占
    HAL_Delay(10);
    shared_var += 50; // 此时shared_var的值可能已经被高优先级中断修改
}

void high_priority_isr(void) {
    // 高优先级中断修改共享变量
    shared_var = 200;
}

五、临界区保护的正确策略

为了避免共享数据错乱,需要对临界区(访问共享数据的代码段)进行保护。以下是两种常用的保护策略:

1. 原子操作:使用LDREX/STREX指令

Cortex-M架构提供了LDREX(排他加载)和STREX(排他存储)指令,可以实现原子操作,保证对共享变量的读写操作不可被打断。

示例代码:

// 原子操作:对共享变量加1
uint32_t atomic_add(volatile uint32_t *ptr, uint32_t value) {
    uint32_t result;
    uint32_t status;

    do {
        // 排他加载
        __asm__ volatile ("ldrex %0, [%1]" : "=r" (result) : "r" (ptr));
        result += value;
        // 排他存储
        __asm__ volatile ("strex %0, %1, [%2]" : "=r" (status) : "r" (result), "r" (ptr));
    } while (status != 0); // 存储失败则重试

    return result;
}

// 使用示例
atomic_add(&shared_var, 1);

2. 屏蔽相关中断优先级

对于复杂的临界区,可以只屏蔽与该临界区相关的中断优先级,而不是关闭所有中断,这样可以保证更高优先级的中断仍然可以被响应。

示例代码:

// 进入临界区:屏蔽EXTI0中断
void enter_critical(void) {
    __disable_irq();
    // 保存当前中断优先级掩码
    uint32_t primask = __get_PRIMASK();
    // 屏蔽EXTI0中断
    NVIC_DisableIRQ(EXTI0_IRQn);
    __enable_irq();
}

// 退出临界区:恢复EXTI0中断
void exit_critical(void) {
    __disable_irq();
    // 恢复EXTI0中断
    NVIC_EnableIRQ(EXTI0_IRQn);
    // 恢复之前的中断优先级掩码
    __set_PRIMASK(primask);
    __enable_irq();
}

// 使用示例
enter_critical();
// 访问共享变量
shared_var = 100;
shared_var += 50;
exit_critical();

3. 避免错误做法:关闭所有中断

简单粗暴地关闭所有中断会屏蔽高优先级中断,违背了中断嵌套的初衷,可能会导致系统实时性下降,甚至丢失紧急事件。因此,除非必要,否则不要使用__disable_irq()关闭所有中断。

六、总结

中断嵌套是嵌入式系统提升实时性的重要手段,但也带来了共享数据错乱的问题。在实际开发中,我们需要:

  1. 合理配置中断优先级,保证紧急事件能够被及时响应。
  2. 对共享数据的访问进行临界区保护,优先使用原子操作或屏蔽相关中断优先级,避免关闭所有中断。
  3. 注意临界区的执行时间,尽量缩短临界区的长度,避免影响系统的实时性。

通过合理使用中断嵌套和临界区保护,我们可以在保证系统实时性的同时,避免共享数据错乱的问题,提升嵌入式系统的稳定性和可靠性。

 

更多推荐