45.嵌入式中断嵌套与临界区保护:原理、代码与实战
一、中断嵌套的核心原理中断嵌套的本质是高优先级中断可以打断正在执行的低优先级中断从而保证紧急事件能够得到及时处理。其完整流程可分为以下5个阶段低优先级中断执行CPU正在处理一个低优先级中断服务函数。高优先级中断触发此时一个更高优先级的中断请求到来。现场保护CPU自动将当前程序计数器PC和状态寄存器压入栈中暂停当前中断的执行。高优先级中断处理跳转到高优先级中断服务函数执行。现场恢复高优先级中断执行完毕后从栈中恢复之前的现场继续执行被打断的低优先级中断。在Cortex-M架构中这一过程由硬件自动完成无需开发者手动保存和恢复现场。同时Cortex-M内核在进入中断时会自动将当前中断的优先级写入优先级掩码寄存器屏蔽同级和更低优先级的中断只有更高优先级的中断可以自动抢占这一机制无需在代码中手动开启总中断。二、中断嵌套的关键规则1. 优先级数值越小优先级越高例如优先级0的中断可以打断优先级1的中断反之则不行。2. 系统异常优先级固定复位、NMI不可屏蔽中断、HardFault硬件错误等系统异常的优先级由硬件固定高于所有用户可配置的中断用户中断永远无法抢占这些异常。3. 同优先级中断无法嵌套两个相同优先级的中断不能互相嵌套只有更高优先级的中断才能抢占低优先级中断。三、中断嵌套的代码实现基于STM32 HAL库以下是一个基于STM32的中断嵌套示例通过配置两个外部中断EXTI0和EXTI1实现高优先级中断抢占低优先级中断的效果。1. 优先级配置// 配置EXTI0PA0为高优先级中断抢占优先级1响应优先级0 HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0); HAL_NVIC_EnableIRQ(EXTI0_IRQn); // 配置EXTI1PA1为低优先级中断抢占优先级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()关闭所有中断。六、总结中断嵌套是嵌入式系统提升实时性的重要手段但也带来了共享数据错乱的问题。在实际开发中我们需要合理配置中断优先级保证紧急事件能够被及时响应。对共享数据的访问进行临界区保护优先使用原子操作或屏蔽相关中断优先级避免关闭所有中断。注意临界区的执行时间尽量缩短临界区的长度避免影响系统的实时性。通过合理使用中断嵌套和临界区保护我们可以在保证系统实时性的同时避免共享数据错乱的问题提升嵌入式系统的稳定性和可靠性。