Cortex-M 中断挂起、丢中断与 EXC_RETURN 机制详解

Cortex-M 中断挂起、丢中断与 EXC_RETURN 机制详解

本文基于《ARM Cortex-M 权威指南》第 7 章,结合实际工程经验,深入讲解 Cortex-M 处理器的中断挂起(Pending)机制、丢中断问题的本质,以及异常返回时 LR 寄存器中 EXC_RETURN 的作用。


一、中断的三种状态

在 Cortex-M 处理器中,每个中断都有 3 个属性

  1. 使能/禁止(Enable/Disable):控制中断是否可以被触发
  2. 挂起/未挂起(Pending/Not Pending):表示中断请求是否已产生但未处理
  3. 活跃/非活跃(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) 表示处理器已经接收到中断请求,但由于以下原因尚未处理:

  1. 优先级不足:当前正在处理更高优先级的中断
  2. 全局中断被禁止:通过 PRIMASK / FAULTMASK / BASEPRI 屏蔽
  3. 中断未使能:通过 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) {  // 循环读取 FIFO
        uint8_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 = 使用 PSP
  Bit 1: 保留(通常为 0)
  Bit 0: 0 = 标准栈帧, 1 = 扩展栈帧(带 FPU 寄存器)

示例

  • 0xFFFFFFF9:二进制 1111 1111 ... 1001 → 返回线程模式,使用 MSP,无 FPU 上下文。
  • 0xFFFFFFED:二进制 1111 1111 ... 1101 → 返回线程模式,使用 PSP,有 FPU 上下文。
    在这里插入图片描述

4.3 异常返回的执行流程

步骤

  1. 压栈(入口):异常发生时,硬件自动将寄存器压入栈:

    栈顶(高地址)
    ┌─────────────┐
    │ xPSR        │ <- 程序状态寄存器
    │ PC          │ <- 返回地址
    │ LR          │ <- 线程模式的 LR
    │ R12         │
    │ R3          │
    │ R2          │
    │ R1          │
    │ R0          │
    └─────────────┘ <- SP(栈指针)
    
  2. 执行 ISR:处理器进入处理模式,执行中断服务例程。

  3. 出栈(退出):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_BASE
    LDR  R1, [R0, #SR_OFFSET]
    BIC  R1, R1, #TIM_SR_UIF
    STR  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;  // R0
    psp[1] = 0;  // R1
    psp[2] = 0;  // R2
    psp[3] = 0;  // R3
    psp[4] = 0;  // R12
    psp[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 编码建议

✅ 推荐做法
  1. ISR 中只做必要的工作

    void UART_IRQHandler(void) {
        if (UART->ISR & UART_ISR_RXNE) {
            buffer[write_idx++] = UART->RDR;  // 快速读取
        }
    }
    
  2. 使用外设的硬件计数器

    // 定时器捕获多次边沿
    TIM1->ARR = 0xFFFF;  // 最大计数
    TIM1->CNT = 0;
    // 在 ISR 中读取 CNT 即可知道脉冲数
    
  3. 优先级分层

    // 高优先级:快速响应
    NVIC_SetPriority(EXTI0_IRQn, 0);
    
    // 低优先级:复杂处理
    NVIC_SetPriority(TIM2_IRQn, 5);
    
❌ 避免的陷阱
  1. 在 ISR 中调用阻塞函数(如 printfHAL_Delay
  2. 在 ISR 中禁止全局中断时间过长
  3. 手动修改 LR 寄存器(除非你非常清楚后果)

6.3 调试工具推荐

  • DWT(Data Watchpoint and Trace):测量 ISR 执行时间
  • ETM(Embedded Trace Macrocell):跟踪异常入口/出口
  • SEGGER SystemView:可视化中断时序

七、参考资料

  1. ARM 官方文档

    • ARM Cortex-M3/M4/M7 Generic User Guide
    • ARMv7-M Architecture Reference Manual
  2. 经典教材

    • 《ARM Cortex-M 权威指南》(Joseph Yiu)第 7 章
  3. ST 应用笔记

    • AN4776: General-purpose timer cookbook for STM32 microcontrollers
    • AN4995: How to improve ADC accuracy in STM32 microcontrollers
  4. 在线资源

    • ARM Community: https://community.arm.com
    • Stack Overflow: [cortex-m] 标签

版权声明:本文基于公开技术资料整理,遵循 CC BY-SA 4.0 协议。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值