45.嵌入式中断嵌套与临界区保护:原理、代码与实战
一、中断嵌套的核心原理
中断嵌套的本质是高优先级中断可以打断正在执行的低优先级中断,从而保证紧急事件能够得到及时处理。其完整流程可分为以下5个阶段:
- 低优先级中断执行:CPU正在处理一个低优先级中断服务函数。
- 高优先级中断触发:此时一个更高优先级的中断请求到来。
- 现场保护:CPU自动将当前程序计数器(PC)和状态寄存器压入栈中,暂停当前中断的执行。
- 高优先级中断处理:跳转到高优先级中断服务函数执行。
- 现场恢复:高优先级中断执行完毕后,从栈中恢复之前的现场,继续执行被打断的低优先级中断。
在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。
四、中断嵌套带来的问题:共享数据错乱
中断嵌套虽然提升了系统的实时性,但也带来了共享数据错乱的问题。当高低优先级中断共享同一个全局变量时,可能会出现以下场景:
- 低优先级中断正在修改全局变量,执行到一半时被高优先级中断抢占。
- 高优先级中断也修改了同一个全局变量。
- 高优先级中断执行完成后,低优先级中断继续执行,此时全局变量的值已经被修改,导致数据错乱。
例如,以下代码中,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()关闭所有中断。
六、总结
中断嵌套是嵌入式系统提升实时性的重要手段,但也带来了共享数据错乱的问题。在实际开发中,我们需要:
- 合理配置中断优先级,保证紧急事件能够被及时响应。
- 对共享数据的访问进行临界区保护,优先使用原子操作或屏蔽相关中断优先级,避免关闭所有中断。
- 注意临界区的执行时间,尽量缩短临界区的长度,避免影响系统的实时性。
通过合理使用中断嵌套和临界区保护,我们可以在保证系统实时性的同时,避免共享数据错乱的问题,提升嵌入式系统的稳定性和可靠性。
更多推荐

所有评论(0)