Cortex-M 中断挂起、丢中断与 EXC_RETURN 机制详解
Cortex-M 中断挂起、丢中断与 EXC_RETURN 机制详解
本文基于《ARM Cortex-M 权威指南》第 7 章,结合实际工程经验,深入讲解 Cortex-M 处理器的中断挂起(Pending)机制、丢中断问题的本质,以及异常返回时 LR 寄存器中 EXC_RETURN 的作用。适合嵌入式开发者发表在 CSDN 等技术博客。
一、中断的三种状态
在 Cortex-M 处理器中,每个中断都有 3 个属性:
- 使能/禁止(Enable/Disable):控制中断是否可以被触发
- 挂起/未挂起(Pending/Not Pending):表示中断请求是否已产生但未处理
- 活跃/非活跃(Active/Inactive):表示中断是否正在被处理(ISR 正在执行)
1.1 状态组合与转换
这些状态可以有多种组合,例如:
状态组合 | 含义 |
---|---|
未挂起 + 非活跃 | 中断空闲,无请求 |
挂起 + 非活跃 | 中断请求已产生,等待处理 |
挂起 + 活跃 | ISR 正在执行,但又产生了新的请求(重复挂起) |
非挂起 + 活跃 | ISR 正在执行,无新请求 |
1.2 NVIC 寄存器操作
NVIC(嵌套向量中断控制器)提供了多个寄存器来控制这些状态:
- NVIC_ISERx / NVIC_ICERx:使能/禁止中断
- NVIC_ISPRx / NVIC_ICPRx:设置/清除挂起状态(软件触发/清除中断)
- NVIC_IABRx:读取活跃状态(只读)
- NVIC_IPRx:设置中断优先级
关键特性:
- 挂起状态位存储在 NVIC 的可编程寄存器中,即使中断被禁止,挂起状态仍会保留。
- 当 NVIC 确认中断请求后,会引发该中断的挂起状态;即使请求被取消,挂起状态仍会为高。
二、中断挂起(Pending)机制
2.1 什么是挂起?
挂起(Pending) 表示处理器已经接收到中断请求,但由于以下原因尚未处理:
- 优先级不足:当前正在处理更高优先级的中断
- 全局中断被禁止:通过
PRIMASK
/FAULTMASK
/BASEPRI
屏蔽 - 中断未使能:通过 NVIC 的
NVIC_ICERx
禁止了该中断
2.2 挂起状态的自动管理
根据图 7.14 和 7.15 的时序,挂起状态的管理遵循以下规则:
情况 1:中断请求被清除(图 7.15)
时间线:中断请求 ───┐ ┌───────────── (外设产生脉冲)└─┘挂起状态 ────┐ ┌────────── (请求确认后置位)└────┘ (软件清除或取消前被清除)处理器模式 ─────────────────── (始终在线程模式,未进入 ISR)
说明:
- 如果中断请求在处理器执行操作前被清除(例如外设标志被软件清除),挂起状态会自动清除。
- 不会丢中断,但也不会进入 ISR。
情况 2:中断被抢占(图 7.14 和 7.19)
时间线:中断请求 X ───┐ ┌───────────── (低优先级中断)└─┘挂起状态 X ────┐ ┌──────┐──── (被确认,处理时又被挂起)└───┘ └────活跃状态 X ────────┐ ┌─── (ISR 执行中)└────────┘处理器模式 ───线程──┐ ISR X ┌─线程── (中断返回后可能再次进入)└───────┘
说明:
- 若在 ISR 执行期间,相同中断再次产生请求,挂起状态会再次置位。
- 当前 ISR 返回后,处理器会再次进入该中断(图 7.19 所示)。
- 不会丢中断,但只会挂起一次(即使产生多次请求)。
三、丢中断问题的本质
3.1 什么情况下会"丢"中断?
核心原理:Cortex-M 的挂起状态是 1 位标志(置位/清除),不是计数器。
图 7.18 所示场景:
时间线:中断脉冲 ───┐ ┐ ┐────────────── (进入 ISR 前连续 3 次脉冲)└─┘ └─┘挂起状态 ────┐ ┌──────── (只记录"有请求",不计数)└─────────┘处理器模式 ───线程──┐ ISR ┌─线程─── (只进入一次 ISR)└─────┘
结论:
- 如果在 ISR 进入之前,同一中断连续产生多次脉冲,只会挂起一次。
- 处理器只会进入一次 ISR,其余脉冲会被"合并"。
- 这不是硬件 BUG,而是设计权衡:节省硬件资源(不需要为每个中断维护计数器)。
3.2 如何避免丢中断?
方案 1:使用硬件 FIFO 或计数器(推荐)
示例(UART 接收):
// 外设配置:启用 FIFO
USART1->CR1 |= USART_CR1_FIFOEN; // 启用 FIFO(STM32G4 等)void USART1_IRQHandler(void) {while (USART1->ISR & USART_ISR_RXNE) { // 循环读取 FIFOuint8_t data = USART1->RDR;buffer[write_idx++] = data;}
}
优点:
- 硬件 FIFO 可以缓存多个数据,避免软件来不及处理。
- 适用于 UART、SPI、ADC 等外设。
方案 2:在 ISR 中循环处理标志位
示例(GPIO 外部中断):
volatile uint32_t edge_count = 0; // 软件计数器void EXTI0_IRQHandler(void) {if (EXTI->PR1 & EXTI_PR1_PIF0) {edge_count++; // 记录边沿次数EXTI->PR1 = EXTI_PR1_PIF0; // 清除挂起标志// 检查是否有新的挂起(在清除后立即检查)if (EXTI->PR1 & EXTI_PR1_PIF0) {edge_count++; // 再次记录EXTI->PR1 = EXTI_PR1_PIF0;}}
}
注意:
- 这种方法只能部分缓解问题,无法完全避免(如果连续脉冲间隔 < ISR 执行时间)。
方案 3:提高 ISR 优先级 + 减少处理时间
// 设置最高优先级(0 = 最高)
NVIC_SetPriority(EXTI0_IRQn, 0);// ISR 中只做最少的工作
void EXTI0_IRQHandler(void) {timestamp[write_idx++] = DWT->CYCCNT; // 快速记录时间戳EXTI->PR1 = EXTI_PR1_PIF0; // 清除标志// 复杂处理放到主循环或低优先级任务
}
3.3 典型错误案例
❌ 错误做法(在 ISR 中处理过慢):
void TIM2_IRQHandler(void) {if (TIM2->SR & TIM_SR_UIF) {TIM2->SR &= ~TIM_SR_UIF; // 清除标志// 错误:在 ISR 中执行耗时操作for (int i = 0; i < 1000; i++) {process_data(buffer[i]); // 可能需要几毫秒}}
}
问题:
- 如果定时器周期是 1ms,而 ISR 执行需要 5ms,会丢失 4 次中断。
✅ 正确做法:
volatile bool timer_flag = false;void TIM2_IRQHandler(void) {if (TIM2->SR & TIM_SR_UIF) {TIM2->SR &= ~TIM_SR_UIF;timer_flag = true; // 设置标志,快速退出}
}int main(void) {while (1) {if (timer_flag) {timer_flag = false;process_data(buffer, 1000); // 在主循环处理}}
}
四、EXC_RETURN 机制详解
4.1 什么是 EXC_RETURN?
EXC_RETURN 是一个特殊的返回地址,存储在 链接寄存器(LR) 中,用于触发异常返回流程。
关键特性:
- 当处理器进入异常(中断或其他异常)时,硬件自动将 EXC_RETURN 写入 LR。
- 当 ISR 执行返回指令(如
BX LR
)时,若 LR 的值是 EXC_RETURN,处理器会触发异常返回。
4.2 EXC_RETURN 的编码格式
EXC_RETURN 是一个 32 位值,格式为:0xFxxxxxxx
(高 8 位固定为 0xFF
)。
表 7.8:常用的 EXC_RETURN 值
返回指令 | EXC_RETURN 值 | 描述 |
---|---|---|
BX | 0xFFFFFFF9 | 返回到线程模式,使用 MSP(主堆栈指针) |
POP {PC} 或 POP {…, PC} | 0xFFFFFFFD | 返回到线程模式,使用 PSP(进程堆栈指针) |
加载(LDR)或多加载(LDM) | 0xFFFFFFF1 | 返回到处理模式(Handler Mode),使用 MSP |
![]() |
编码细节(低 4 位):
EXC_RETURN[3:0]:Bit 3: 0 = 返回到处理模式, 1 = 返回到线程模式Bit 2: 0 = 使用 MSP, 1 = 使用 PSPBit 1: 保留(通常为 0)Bit 0: 0 = 标准栈帧, 1 = 扩展栈帧(带 FPU 寄存器)
示例:
0xFFFFFFF9
:二进制1111 1111 ... 1001
→ 返回线程模式,使用 MSP,无 FPU 上下文。0xFFFFFFED
:二进制1111 1111 ... 1101
→ 返回线程模式,使用 PSP,有 FPU 上下文。
4.3 异常返回的执行流程
步骤:
-
压栈(入口):异常发生时,硬件自动将寄存器压入栈:
栈顶(高地址) ┌─────────────┐ │ xPSR │ <- 程序状态寄存器 │ PC │ <- 返回地址 │ LR │ <- 线程模式的 LR │ R12 │ │ R3 │ │ R2 │ │ R1 │ │ R0 │ └─────────────┘ <- SP(栈指针)
-
执行 ISR:处理器进入处理模式,执行中断服务例程。
-
出栈(退出):ISR 执行
BX LR
时,硬件检测到 LR = EXC_RETURN:- 从栈中恢复 R0-R3, R12, LR, PC, xPSR。
- 根据 EXC_RETURN 的编码,切换到线程模式并选择 MSP 或 PSP。
- 更新 NVIC 寄存器(如活跃状态、挂起状态)。
- 恢复 PC,继续执行被中断的代码。
4.4 C 语言中的自动处理
关键:在 C 语言编写 ISR 时,编译器会自动处理 EXC_RETURN:
void TIM2_IRQHandler(void) {// 编译器生成的入口代码:压栈 + 保存上下文// 用户代码TIM2->SR &= ~TIM_SR_UIF;// 编译器生成的出口代码:// BX LR(LR 中存储的是 EXC_RETURN)
}
生成的汇编(示例):
TIM2_IRQHandler:; 硬件自动压栈 R0-R3, R12, LR, PC, xPSR; ...用户代码...LDR R0, =TIM2_BASELDR R1, [R0, #SR_OFFSET]BIC R1, R1, #TIM_SR_UIFSTR R1, [R0, #SR_OFFSET]BX LR ; LR = 0xFFFFFFF9,触发异常返回
4.5 手动调用 EXC_RETURN(高级用法)
场景:在 RTOS 或 Bootloader 中,可能需要手动构造异常返回。
示例(切换到用户模式并使用 PSP):
void switch_to_user_task(void) {// 构造栈帧uint32_t *psp = (uint32_t *)0x20008000; // 用户栈顶psp -= 8; // 为栈帧分配空间psp[0] = 0; // R0psp[1] = 0; // R1psp[2] = 0; // R2psp[3] = 0; // R3psp[4] = 0; // R12psp[5] = (uint32_t)user_task_exit; // LR(任务返回地址)psp[6] = (uint32_t)user_task_entry; // PC(任务入口)psp[7] = 0x01000000; // xPSR(Thumb 位置位)// 设置 PSP__set_PSP((uint32_t)psp);// 切换到线程模式 + PSP__asm volatile ("MOV LR, #0xFFFFFFFD \n" // EXC_RETURN:线程模式 + PSP"BX LR \n" // 触发异常返回);
}
注意:
- 这种手动操作通常只在 RTOS 内核或启动代码中使用。
- 栈帧格式必须严格符合 ARM 规范,否则会触发 HardFault。
五、实战案例:结合挂起与 EXC_RETURN
5.1 场景:在 ISR 中再次产生中断挂起
图 7.19 所示:
volatile uint32_t count = 0;void EXTI0_IRQHandler(void) {count++;EXTI->PR1 = EXTI_PR1_PIF0; // 清除挂起标志// 模拟耗时操作(此时可能有新的中断请求)for (volatile int i = 0; i < 10000; i++);// ISR 返回后,若有新的挂起,会再次进入此函数
}
时序分析:
时间线:中断请求 ───┐ ┐───────────── (第一次脉冲)(第二次脉冲)└─────┘挂起状态 ────┐ ┌─┐───────────── (第一次清除,第二次再次挂起)└───┘ └─────────────处理器模式 ───线程──┐ ISR ┌─┐ ISR ┌─线程── (连续进入两次)└─────┘ └─────┘
5.2 调试技巧:查看 LR 寄存器
在调试器中断点停在 ISR 时:
(gdb) info registers lr
lr 0xfffffff9
含义:
0xFFFFFFF9
→ 返回线程模式 + MSP。- 如果看到其他值(如
0xFFFFFFF1
),说明是嵌套中断(ISR 中又进入了另一个 ISR)。
六、总结与最佳实践
6.1 关键结论
问题 | 原因 | 解决方案 |
---|---|---|
丢中断 | 挂起状态是 1 位标志,不计数 | 使用硬件 FIFO 或软件循环检测 |
重复挂起 | ISR 执行期间再次产生请求 | 在 ISR 中快速清除标志,避免耗时操作 |
EXC_RETURN 错误 | 手动修改 LR 导致栈帧损坏 | 使用 C 语言编写 ISR,让编译器自动处理 |
6.2 编码建议
✅ 推荐做法
-
ISR 中只做必要的工作:
void UART_IRQHandler(void) {if (UART->ISR & UART_ISR_RXNE) {buffer[write_idx++] = UART->RDR; // 快速读取} }
-
使用外设的硬件计数器:
// 定时器捕获多次边沿 TIM1->ARR = 0xFFFF; // 最大计数 TIM1->CNT = 0; // 在 ISR 中读取 CNT 即可知道脉冲数
-
优先级分层:
// 高优先级:快速响应 NVIC_SetPriority(EXTI0_IRQn, 0);// 低优先级:复杂处理 NVIC_SetPriority(TIM2_IRQn, 5);
❌ 避免的陷阱
- 在 ISR 中调用阻塞函数(如
printf
、HAL_Delay
) - 在 ISR 中禁止全局中断时间过长
- 手动修改 LR 寄存器(除非你非常清楚后果)
6.3 调试工具推荐
- DWT(Data Watchpoint and Trace):测量 ISR 执行时间
- ETM(Embedded Trace Macrocell):跟踪异常入口/出口
- SEGGER SystemView:可视化中断时序
七、参考资料
-
ARM 官方文档
- ARM Cortex-M3/M4/M7 Generic User Guide
- ARMv7-M Architecture Reference Manual
-
经典教材
- 《ARM Cortex-M 权威指南》(Joseph Yiu)第 7 章
-
ST 应用笔记
- AN4776: General-purpose timer cookbook for STM32 microcontrollers
- AN4995: How to improve ADC accuracy in STM32 microcontrollers
-
在线资源
- ARM Community: https://community.arm.com
- Stack Overflow: [cortex-m] 标签
版权声明:本文基于公开技术资料整理,遵循 CC BY-SA 4.0 协议。