跳转至

中断向量表与异常处理机制

学习目标

完成本文学习后,你将能够:

  • 理解中断向量表的结构和组织方式
  • 掌握ARM Cortex-M的异常类型分类
  • 了解异常优先级的管理机制
  • 掌握NVIC(嵌套向量中断控制器)的配置方法
  • 理解异常处理的完整流程
  • 能够编写中断服务函数
  • 掌握中断优先级分组和抢占机制

前置要求

在开始本文学习之前,你需要:

知识要求: - 了解ARM Cortex-M架构基础 - 熟悉C语言和基本的汇编语言 - 掌握内存映射和地址空间概念 - 了解处理器工作模式和特权级别

技能要求: - 能够阅读和编写C代码 - 了解寄存器操作方法 - 熟悉基本的调试技巧

推荐但非必需: - 有STM32或其他ARM MCU开发经验 - 了解启动流程和系统初始化 - 熟悉外设编程基础

概述

中断和异常是嵌入式系统中最重要的机制之一,它们使处理器能够及时响应外部事件和内部错误。ARM Cortex-M处理器提供了一套完整的异常处理机制,包括中断向量表、NVIC控制器和灵活的优先级管理系统。

为什么需要中断机制

  1. 实时响应
  2. 及时处理外部事件(按键、传感器、通信)
  3. 避免轮询带来的延迟和资源浪费
  4. 提高系统响应速度

  5. 多任务处理

  6. 支持多个外设同时工作
  7. 实现事件驱动的程序架构
  8. 提高CPU利用率

  9. 错误处理

  10. 检测和处理系统错误
  11. 实现故障恢复机制
  12. 提高系统可靠性

  13. 功耗优化

  14. CPU可以进入低功耗模式
  15. 事件发生时自动唤醒
  16. 降低系统功耗

中断与异常的区别

在ARM Cortex-M中,"异常"是一个更广泛的概念,包括中断和其他异常事件:

异常 (Exception)
├── 系统异常 (System Exceptions)
│   ├── Reset (复位)
│   ├── NMI (不可屏蔽中断)
│   ├── HardFault (硬件错误)
│   ├── MemManage (内存管理错误)
│   ├── BusFault (总线错误)
│   ├── UsageFault (用法错误)
│   ├── SVC (系统服务调用)
│   ├── PendSV (可挂起的系统调用)
│   └── SysTick (系统滴答定时器)
└── 外部中断 (External Interrupts)
    ├── IRQ0 - IRQ239
    └── 由外设产生

术语说明: - 异常(Exception):所有打断正常程序流程的事件 - 中断(Interrupt):由外设产生的异常 - 系统异常:由处理器内部产生的异常 - 外部中断:由外设产生的中断请求

第一部分:中断向量表结构

向量表的作用

中断向量表是一个包含所有异常处理函数地址的数组,位于内存的固定位置。当异常发生时,处理器会从向量表中读取对应的处理函数地址并跳转执行。

向量表的关键特性

  1. 固定位置
  2. 默认位于地址0x00000000
  3. 可以通过VTOR寄存器重定位
  4. 必须256字节对齐

  5. 固定格式

  6. 第一个条目是初始栈指针值
  7. 后续条目是异常处理函数地址
  8. 每个条目占4字节(32位)

  9. 优先级顺序

  10. 表中位置决定了异常编号
  11. 异常编号影响默认优先级
  12. 系统异常在前,外部中断在后

向量表详细结构

以STM32F103为例,向量表的完整结构如下:

地址偏移    异常编号    异常名称                    说明
0x0000      -           __initial_sp               初始栈指针值
0x0004      1           Reset_Handler              复位处理函数
0x0008      2           NMI_Handler                不可屏蔽中断
0x000C      3           HardFault_Handler          硬件错误
0x0010      4           MemManage_Handler          内存管理错误
0x0014      5           BusFault_Handler           总线错误
0x0018      6           UsageFault_Handler         用法错误
0x001C      7           保留                        -
0x0020      8           保留                        -
0x0024      9           保留                        -
0x0028      10          保留                        -
0x002C      11          SVC_Handler                系统服务调用
0x0030      12          DebugMon_Handler           调试监视器
0x0034      13          保留                        -
0x0038      14          PendSV_Handler             可挂起的系统调用
0x003C      15          SysTick_Handler            系统滴答定时器
0x0040      16          WWDG_IRQHandler            窗口看门狗
0x0044      17          PVD_IRQHandler             电源电压检测
0x0048      18          TAMPER_IRQHandler          侵入检测
0x004C      19          RTC_IRQHandler             RTC全局中断
0x0050      20          FLASH_IRQHandler           Flash全局中断
0x0054      21          RCC_IRQHandler             RCC全局中断
0x0058      22          EXTI0_IRQHandler           外部中断线0
0x005C      23          EXTI1_IRQHandler           外部中断线1
...         ...         ...                        更多外设中断

重要说明

  1. 异常编号
  2. 从1开始编号(0是栈指针)
  3. 1-15是系统异常
  4. 16及以上是外部中断(IRQ0对应异常16)

  5. 保留位置

  6. 某些位置保留供将来使用
  7. 必须填充为0或默认处理函数

  8. 外设中断

  9. 不同芯片的外设中断数量不同
  10. STM32F103有60个外设中断
  11. STM32F4系列可能有90+个中断

启动文件中的向量表定义

在启动文件(startup_stm32f1xx.s)中,向量表是这样定义的:

; 向量表定义
                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size

__Vectors       DCD     __initial_sp              ; 栈顶地址
                DCD     Reset_Handler             ; 复位向量
                DCD     NMI_Handler               ; NMI处理函数
                DCD     HardFault_Handler         ; 硬件错误处理
                DCD     MemManage_Handler         ; 内存管理错误
                DCD     BusFault_Handler          ; 总线错误
                DCD     UsageFault_Handler        ; 用法错误
                DCD     0                         ; 保留
                DCD     0                         ; 保留
                DCD     0                         ; 保留
                DCD     0                         ; 保留
                DCD     SVC_Handler               ; SVC处理函数
                DCD     DebugMon_Handler          ; 调试监视器
                DCD     0                         ; 保留
                DCD     PendSV_Handler            ; PendSV处理函数
                DCD     SysTick_Handler           ; SysTick处理函数

                ; 外部中断
                DCD     WWDG_IRQHandler           ; 窗口看门狗
                DCD     PVD_IRQHandler            ; PVD通过EXTI检测
                DCD     TAMPER_IRQHandler         ; 侵入检测
                DCD     RTC_IRQHandler            ; RTC全局中断
                DCD     FLASH_IRQHandler          ; Flash全局中断
                DCD     RCC_IRQHandler            ; RCC全局中断
                DCD     EXTI0_IRQHandler          ; 外部中断线0
                DCD     EXTI1_IRQHandler          ; 外部中断线1
                ; ... 更多中断向量

__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors

                AREA    |.text|, CODE, READONLY

代码说明

  • AREA RESET, DATA, READONLY:定义只读数据区域
  • DCD:定义一个32位常量(Define Constant Data)
  • __initial_sp:栈顶地址,由链接脚本定义
  • EXPORT:导出符号,使其他文件可以引用
  • EQU:定义常量,计算向量表大小

向量表重定位

默认情况下,向量表位于Flash的起始地址(0x08000000),但可以通过VTOR(Vector Table Offset Register)寄存器将其重定位到其他位置。

VTOR寄存器

/**
 * @brief  向量表偏移寄存器 (VTOR)
 * @note   位于SCB (System Control Block)
 */
#define SCB_VTOR  (*((volatile uint32_t*)0xE000ED08))

// VTOR寄存器位定义
// bit 31-7: TBLOFF - 向量表偏移地址(必须256字节对齐)
// bit 6-0:  保留,必须为0

重定位向量表示例

/**
 * @brief  重定位向量表到SRAM
 * @note   用于在SRAM中运行代码或实现动态向量表
 */
void Relocate_Vector_Table_To_SRAM(void)
{
    extern uint32_t __Vectors[];  // 向量表起始地址
    uint32_t *src = __Vectors;
    uint32_t *dst = (uint32_t*)0x20000000;  // SRAM起始地址
    uint32_t vector_count = 16 + 60;  // 系统异常 + 外设中断

    /* 1. 复制向量表到SRAM */
    for (uint32_t i = 0; i < vector_count; i++) {
        dst[i] = src[i];
    }

    /* 2. 设置VTOR指向新位置 */
    SCB->VTOR = 0x20000000;

    /* 3. 指令同步屏障 */
    __DSB();
    __ISB();

    printf("向量表已重定位到SRAM: 0x%08X\n", SCB->VTOR);
}

/**
 * @brief  重定位向量表到Flash的其他位置
 * @param  offset: 偏移地址(必须256字节对齐)
 */
void Relocate_Vector_Table(uint32_t offset)
{
    /* 检查对齐 */
    if (offset & 0xFF) {
        printf("错误:向量表地址必须256字节对齐\n");
        return;
    }

    /* 设置VTOR */
    SCB->VTOR = FLASH_BASE + offset;
    __DSB();
    __ISB();

    printf("向量表已重定位到: 0x%08X\n", SCB->VTOR);
}

重定位的应用场景

  1. Bootloader应用
  2. Bootloader使用默认向量表
  3. 应用程序使用重定位的向量表
  4. 实现固件升级功能

  5. 在SRAM中运行

  6. 提高中断响应速度
  7. 实现动态修改向量表
  8. 用于特殊调试场景

  9. 多应用程序

  10. 不同应用使用不同向量表
  11. 实现应用隔离
  12. 支持应用切换

第二部分:异常类型详解

系统异常

系统异常是由处理器内部产生的异常,用于处理系统级事件和错误。

1. Reset (异常1)

触发条件: - 上电复位 - 外部复位引脚 - 看门狗复位 - 软件复位

处理函数

/**
 * @brief  复位处理函数
 * @note   系统启动后执行的第一个函数
 */
void Reset_Handler(void)
{
    // 调用系统初始化
    SystemInit();

    // 跳转到main函数
    __main();
}

2. NMI - 不可屏蔽中断 (异常2)

特点: - 不能被禁用或屏蔽 - 优先级固定为-2(仅次于Reset) - 用于处理关键错误

典型应用

/**
 * @brief  NMI处理函数
 * @note   处理关键系统错误
 */
void NMI_Handler(void)
{
    /* 检查RCC时钟安全系统 */
    if (RCC->CIR & RCC_CIR_CSSF) {
        printf("时钟安全系统检测到HSE故障\n");

        /* 清除标志 */
        RCC->CIR |= RCC_CIR_CSSC;

        /* 切换到HSI时钟 */
        RCC->CR |= RCC_CR_HSION;
        while(!(RCC->CR & RCC_CR_HSIRDY));

        /* 切换系统时钟源 */
        RCC->CFGR &= ~RCC_CFGR_SW;
        RCC->CFGR |= RCC_CFGR_SW_HSI;
    }

    /* 其他NMI源处理 */
}

3. HardFault - 硬件错误 (异常3)

触发条件: - 访问无效内存地址 - 执行未定义指令 - 其他错误异常升级而来

处理函数

/**
 * @brief  HardFault处理函数
 * @note   捕获所有未处理的错误
 */
void HardFault_Handler(void)
{
    printf("HardFault异常!\n");

    /* 获取错误信息 */
    uint32_t cfsr = SCB->CFSR;  // 配置和状态寄存器
    uint32_t hfsr = SCB->HFSR;  // HardFault状态寄存器
    uint32_t dfsr = SCB->DFSR;  // 调试错误状态寄存器

    printf("CFSR = 0x%08X\n", cfsr);
    printf("HFSR = 0x%08X\n", hfsr);
    printf("DFSR = 0x%08X\n", dfsr);

    /* 检查是否由其他错误升级 */
    if (hfsr & SCB_HFSR_FORCED_Msk) {
        printf("由其他错误升级为HardFault\n");
    }

    /* 进入无限循环或复位 */
    while(1);
}

4. MemManage - 内存管理错误 (异常4)

触发条件: - MPU检测到非法内存访问 - 访问未定义的内存区域 - 违反内存访问权限

处理函数

/**
 * @brief  MemManage处理函数
 * @note   需要先使能MemManage异常
 */
void MemManage_Handler(void)
{
    printf("MemManage异常!\n");

    /* 检查错误类型 */
    uint32_t cfsr = SCB->CFSR;

    if (cfsr & SCB_CFSR_IACCVIOL_Msk) {
        printf("指令访问违规\n");
    }
    if (cfsr & SCB_CFSR_DACCVIOL_Msk) {
        printf("数据访问违规\n");

        /* 获取违规地址 */
        if (cfsr & SCB_CFSR_MMARVALID_Msk) {
            uint32_t mmfar = SCB->MMFAR;
            printf("违规地址: 0x%08X\n", mmfar);
        }
    }
    if (cfsr & SCB_CFSR_MSTKERR_Msk) {
        printf("入栈错误\n");
    }
    if (cfsr & SCB_CFSR_MUNSTKERR_Msk) {
        printf("出栈错误\n");
    }

    /* 清除错误标志 */
    SCB->CFSR = cfsr;

    while(1);
}

/**
 * @brief  使能MemManage异常
 */
void Enable_MemManage_Fault(void)
{
    SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;
}

5. BusFault - 总线错误 (异常5)

触发条件: - 访问不存在的外设 - 总线访问超时 - 预取指令错误

处理函数

/**
 * @brief  BusFault处理函数
 */
void BusFault_Handler(void)
{
    printf("BusFault异常!\n");

    uint32_t cfsr = SCB->CFSR;

    if (cfsr & SCB_CFSR_IBUSERR_Msk) {
        printf("指令总线错误\n");
    }
    if (cfsr & SCB_CFSR_PRECISERR_Msk) {
        printf("精确数据总线错误\n");

        /* 获取错误地址 */
        if (cfsr & SCB_CFSR_BFARVALID_Msk) {
            uint32_t bfar = SCB->BFAR;
            printf("错误地址: 0x%08X\n", bfar);
        }
    }
    if (cfsr & SCB_CFSR_IMPRECISERR_Msk) {
        printf("非精确数据总线错误\n");
    }
    if (cfsr & SCB_CFSR_UNSTKERR_Msk) {
        printf("出栈总线错误\n");
    }
    if (cfsr & SCB_CFSR_STKERR_Msk) {
        printf("入栈总线错误\n");
    }

    SCB->CFSR = cfsr;
    while(1);
}

6. UsageFault - 用法错误 (异常6)

触发条件: - 执行未定义指令 - 未对齐的内存访问 - 除零错误(需使能) - 协处理器访问错误

处理函数

/**
 * @brief  UsageFault处理函数
 */
void UsageFault_Handler(void)
{
    printf("UsageFault异常!\n");

    uint32_t cfsr = SCB->CFSR;

    if (cfsr & SCB_CFSR_UNDEFINSTR_Msk) {
        printf("未定义指令\n");
    }
    if (cfsr & SCB_CFSR_INVSTATE_Msk) {
        printf("无效状态\n");
    }
    if (cfsr & SCB_CFSR_INVPC_Msk) {
        printf("无效PC值\n");
    }
    if (cfsr & SCB_CFSR_NOCP_Msk) {
        printf("无协处理器\n");
    }
    if (cfsr & SCB_CFSR_UNALIGNED_Msk) {
        printf("未对齐访问\n");
    }
    if (cfsr & SCB_CFSR_DIVBYZERO_Msk) {
        printf("除零错误\n");
    }

    SCB->CFSR = cfsr;
    while(1);
}

/**
 * @brief  使能UsageFault异常
 */
void Enable_UsageFault(void)
{
    /* 使能UsageFault */
    SCB->SHCSR |= SCB_SHCSR_USGFAULTENA_Msk;

    /* 使能除零检测 */
    SCB->CCR |= SCB_CCR_DIV_0_TRP_Msk;

    /* 使能未对齐访问检测 */
    SCB->CCR |= SCB_CCR_UNALIGN_TRP_Msk;
}

7. SVC - 系统服务调用 (异常11)

用途: - 用户程序请求系统服务 - 实现系统调用接口 - RTOS任务切换

使用示例

/**
 * @brief  触发SVC异常
 * @param  svc_num: SVC号
 */
void Trigger_SVC(uint8_t svc_num)
{
    __asm volatile ("SVC %0" : : "I" (svc_num));
}

/**
 * @brief  SVC处理函数
 */
void SVC_Handler(void)
{
    /* 获取栈帧 */
    uint32_t *stack_frame;
    if (__get_CONTROL() & CONTROL_SPSEL_Msk) {
        stack_frame = (uint32_t*)__get_PSP();
    } else {
        stack_frame = (uint32_t*)__get_MSP();
    }

    /* 获取返回地址 */
    uint32_t pc = stack_frame[6];

    /* 获取SVC号 */
    uint8_t svc_number = ((uint8_t*)pc)[-2];

    printf("SVC #%d 被调用\n", svc_number);

    /* 根据SVC号执行相应服务 */
    switch(svc_number) {
        case 0:
            // 系统服务0
            break;
        case 1:
            // 系统服务1
            break;
        default:
            printf("未知的SVC号\n");
            break;
    }
}

8. PendSV - 可挂起的系统调用 (异常14)

特点: - 可以被挂起和延迟执行 - 优先级可配置(通常设为最低) - RTOS中用于任务切换

应用示例

/**
 * @brief  触发PendSV异常
 */
void Trigger_PendSV(void)
{
    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}

/**
 * @brief  PendSV处理函数
 * @note   RTOS中用于任务切换
 */
void PendSV_Handler(void)
{
    /* 保存当前任务上下文 */
    __asm volatile (
        "MRS R0, PSP\n"           // 获取PSP
        "STMDB R0!, {R4-R11}\n"   // 保存R4-R11到栈
        "STR R0, [%0]\n"          // 保存新的栈指针
        :
        : "r" (&current_task_sp)
        : "r0"
    );

    /* 选择下一个任务 */
    // next_task = scheduler();

    /* 恢复新任务上下文 */
    __asm volatile (
        "LDR R0, [%0]\n"          // 加载新任务栈指针
        "LDMIA R0!, {R4-R11}\n"   // 恢复R4-R11
        "MSR PSP, R0\n"           // 设置PSP
        "BX LR\n"                 // 返回
        :
        : "r" (&next_task_sp)
        : "r0"
    );
}

9. SysTick - 系统滴答定时器 (异常15)

用途: - 提供系统时基 - RTOS时间片调度 - 延时函数实现

配置和使用

/**
 * @brief  配置SysTick定时器
 * @param  ticks: 定时器重载值
 * @retval 0: 成功, 1: 失败
 */
uint32_t SysTick_Config(uint32_t ticks)
{
    /* 检查重载值范围 */
    if ((ticks - 1) > SysTick_LOAD_RELOAD_Msk) {
        return 1;  // 超出范围
    }

    /* 设置重载值 */
    SysTick->LOAD = ticks - 1;

    /* 设置优先级 */
    NVIC_SetPriority(SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL);

    /* 清除当前值 */
    SysTick->VAL = 0;

    /* 使能SysTick */
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |  // 使用处理器时钟
                    SysTick_CTRL_TICKINT_Msk |     // 使能中断
                    SysTick_CTRL_ENABLE_Msk;       // 使能定时器

    return 0;
}

/**
 * @brief  SysTick中断处理函数
 */
void SysTick_Handler(void)
{
    /* 更新系统滴答计数 */
    HAL_IncTick();

    /* RTOS时间片调度 */
    // OS_TimeSlice();
}

/**
 * @brief  配置1ms的SysTick
 */
void SysTick_Init_1ms(void)
{
    /* 假设系统时钟72MHz */
    SysTick_Config(72000);  // 72MHz / 72000 = 1kHz (1ms)
}

外部中断

外部中断由外设产生,用于通知处理器外部事件的发生。

外部中断的特点

  1. 数量众多
  2. STM32F1系列:60个外部中断
  3. STM32F4系列:90+个外部中断
  4. 不同芯片型号数量不同

  5. 可配置优先级

  6. 每个中断都可以配置优先级
  7. 支持抢占优先级和子优先级
  8. 通过NVIC进行管理

  9. 可以使能/禁用

  10. 通过NVIC控制中断使能
  11. 可以单独使能或禁用每个中断
  12. 支持挂起和清除中断

常见外设中断示例

/**
 * @brief  EXTI0外部中断处理函数
 * @note   处理GPIO PA0的外部中断
 */
void EXTI0_IRQHandler(void)
{
    /* 检查中断标志 */
    if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
        /* 清除中断标志 */
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);

        /* 处理中断事件 */
        printf("EXTI0中断触发\n");
        Handle_Button_Press();
    }
}

/**
 * @brief  USART1中断处理函数
 */
void USART1_IRQHandler(void)
{
    /* 接收中断 */
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) {
        uint8_t data = (uint8_t)(huart1.Instance->DR & 0xFF);
        UART_Receive_Callback(data);
    }

    /* 发送完成中断 */
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC)) {
        __HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_TC);
        UART_Transmit_Complete_Callback();
    }
}

/**
 * @brief  TIM2定时器中断处理函数
 */
void TIM2_IRQHandler(void)
{
    /* 更新中断 */
    if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)) {
        __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);

        /* 定时器溢出处理 */
        Timer_Overflow_Callback();
    }
}

第三部分:NVIC配置

NVIC简介

NVIC(Nested Vectored Interrupt Controller,嵌套向量中断控制器)是ARM Cortex-M处理器的中断管理单元,负责管理所有外部中断和部分系统异常。

NVIC的主要功能

  1. 中断使能/禁用
  2. 控制每个中断的使能状态
  3. 支持批量操作

  4. 优先级管理

  5. 配置中断优先级
  6. 支持抢占和嵌套

  7. 挂起管理

  8. 设置和清除中断挂起状态
  9. 查询中断状态

  10. 活动状态

  11. 查询中断是否正在执行
  12. 支持中断嵌套

NVIC寄存器

NVIC提供了一组寄存器用于中断管理:

/**
 * @brief  NVIC寄存器结构
 */
typedef struct
{
    __IOM uint32_t ISER[8U];    // 中断使能寄存器
    uint32_t RESERVED0[24U];
    __IOM uint32_t ICER[8U];    // 中断清除使能寄存器
    uint32_t RESERVED1[24U];
    __IOM uint32_t ISPR[8U];    // 中断挂起寄存器
    uint32_t RESERVED2[24U];
    __IOM uint32_t ICPR[8U];    // 中断清除挂起寄存器
    uint32_t RESERVED3[24U];
    __IOM uint32_t IABR[8U];    // 中断活动位寄存器
    uint32_t RESERVED4[56U];
    __IOM uint8_t  IP[240U];    // 中断优先级寄存器
    uint32_t RESERVED5[644U];
    __OM  uint32_t STIR;        // 软件触发中断寄存器
} NVIC_Type;

#define NVIC  ((NVIC_Type*)0xE000E100UL)

寄存器说明

  1. ISER (Interrupt Set-Enable Registers)
  2. 使能中断
  3. 写1使能对应中断
  4. 读取返回使能状态

  5. ICER (Interrupt Clear-Enable Registers)

  6. 禁用中断
  7. 写1禁用对应中断

  8. ISPR (Interrupt Set-Pending Registers)

  9. 设置中断挂起状态
  10. 写1设置挂起

  11. ICPR (Interrupt Clear-Pending Registers)

  12. 清除中断挂起状态
  13. 写1清除挂起

  14. IABR (Interrupt Active Bit Registers)

  15. 只读,显示中断活动状态
  16. 1表示中断正在执行

  17. IP (Interrupt Priority Registers)

  18. 配置中断优先级
  19. 每个中断占8位

NVIC基本操作

使能和禁用中断

/**
 * @brief  使能中断
 * @param  IRQn: 中断号
 */
void NVIC_EnableIRQ(IRQn_Type IRQn)
{
    if ((int32_t)(IRQn) >= 0) {
        NVIC->ISER[(((uint32_t)IRQn) >> 5UL)] = 
            (uint32_t)(1UL << (((uint32_t)IRQn) & 0x1FUL));
    }
}

/**
 * @brief  禁用中断
 * @param  IRQn: 中断号
 */
void NVIC_DisableIRQ(IRQn_Type IRQn)
{
    if ((int32_t)(IRQn) >= 0) {
        NVIC->ICER[(((uint32_t)IRQn) >> 5UL)] = 
            (uint32_t)(1UL << (((uint32_t)IRQn) & 0x1FUL));
    }
}

/**
 * @brief  获取中断使能状态
 * @param  IRQn: 中断号
 * @retval 0: 禁用, 1: 使能
 */
uint32_t NVIC_GetEnableIRQ(IRQn_Type IRQn)
{
    if ((int32_t)(IRQn) >= 0) {
        return ((uint32_t)(((NVIC->ISER[(((uint32_t)IRQn) >> 5UL)] & 
                (1UL << (((uint32_t)IRQn) & 0x1FUL))) != 0UL) ? 1UL : 0UL));
    }
    return 0;
}

挂起和清除中断

/**
 * @brief  设置中断挂起
 * @param  IRQn: 中断号
 */
void NVIC_SetPendingIRQ(IRQn_Type IRQn)
{
    if ((int32_t)(IRQn) >= 0) {
        NVIC->ISPR[(((uint32_t)IRQn) >> 5UL)] = 
            (uint32_t)(1UL << (((uint32_t)IRQn) & 0x1FUL));
    }
}

/**
 * @brief  清除中断挂起
 * @param  IRQn: 中断号
 */
void NVIC_ClearPendingIRQ(IRQn_Type IRQn)
{
    if ((int32_t)(IRQn) >= 0) {
        NVIC->ICPR[(((uint32_t)IRQn) >> 5UL)] = 
            (uint32_t)(1UL << (((uint32_t)IRQn) & 0x1FUL));
    }
}

/**
 * @brief  获取中断挂起状态
 * @param  IRQn: 中断号
 * @retval 0: 未挂起, 1: 挂起
 */
uint32_t NVIC_GetPendingIRQ(IRQn_Type IRQn)
{
    if ((int32_t)(IRQn) >= 0) {
        return ((uint32_t)(((NVIC->ISPR[(((uint32_t)IRQn) >> 5UL)] & 
                (1UL << (((uint32_t)IRQn) & 0x1FUL))) != 0UL) ? 1UL : 0UL));
    }
    return 0;
}

/**
 * @brief  获取中断活动状态
 * @param  IRQn: 中断号
 * @retval 0: 未活动, 1: 活动
 */
uint32_t NVIC_GetActive(IRQn_Type IRQn)
{
    if ((int32_t)(IRQn) >= 0) {
        return ((uint32_t)(((NVIC->IABR[(((uint32_t)IRQn) >> 5UL)] & 
                (1UL << (((uint32_t)IRQn) & 0x1FUL))) != 0UL) ? 1UL : 0UL));
    }
    return 0;
}

配置中断优先级

/**
 * @brief  设置中断优先级
 * @param  IRQn: 中断号
 * @param  priority: 优先级值(0-15,值越小优先级越高)
 */
void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
{
    if ((int32_t)(IRQn) >= 0) {
        NVIC->IP[((uint32_t)IRQn)] = 
            (uint8_t)((priority << (8U - __NVIC_PRIO_BITS)) & (uint32_t)0xFFUL);
    } else {
        SCB->SHP[(((uint32_t)IRQn) & 0xFUL)-4UL] = 
            (uint8_t)((priority << (8U - __NVIC_PRIO_BITS)) & (uint32_t)0xFFUL);
    }
}

/**
 * @brief  获取中断优先级
 * @param  IRQn: 中断号
 * @retval 优先级值
 */
uint32_t NVIC_GetPriority(IRQn_Type IRQn)
{
    if ((int32_t)(IRQn) >= 0) {
        return (uint32_t)(NVIC->IP[((uint32_t)IRQn)] >> (8U - __NVIC_PRIO_BITS));
    } else {
        return (uint32_t)(SCB->SHP[(((uint32_t)IRQn) & 0xFUL)-4UL] >> 
                         (8U - __NVIC_PRIO_BITS));
    }
}

优先级分组

ARM Cortex-M支持将优先级分为抢占优先级和子优先级两部分。

优先级分组配置

/**
 * @brief  配置优先级分组
 * @param  PriorityGroup: 优先级分组
 *   @arg NVIC_PRIORITYGROUP_0: 0位抢占优先级,4位子优先级
 *   @arg NVIC_PRIORITYGROUP_1: 1位抢占优先级,3位子优先级
 *   @arg NVIC_PRIORITYGROUP_2: 2位抢占优先级,2位子优先级
 *   @arg NVIC_PRIORITYGROUP_3: 3位抢占优先级,1位子优先级
 *   @arg NVIC_PRIORITYGROUP_4: 4位抢占优先级,0位子优先级
 */
void NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
{
    uint32_t reg_value;
    uint32_t PriorityGroupTmp = (PriorityGroup & (uint32_t)0x07UL);

    reg_value = SCB->AIRCR;
    reg_value &= ~((uint32_t)(SCB_AIRCR_VECTKEY_Msk | SCB_AIRCR_PRIGROUP_Msk));
    reg_value = (reg_value |
                ((uint32_t)0x5FAUL << SCB_AIRCR_VECTKEY_Pos) |
                (PriorityGroupTmp << SCB_AIRCR_PRIGROUP_Pos));
    SCB->AIRCR = reg_value;
}

/**
 * @brief  获取优先级分组
 * @retval 优先级分组值
 */
uint32_t NVIC_GetPriorityGrouping(void)
{
    return ((uint32_t)((SCB->AIRCR & SCB_AIRCR_PRIGROUP_Msk) >> 
                       SCB_AIRCR_PRIGROUP_Pos));
}

/**
 * @brief  编码优先级
 * @param  PriorityGroup: 优先级分组
 * @param  PreemptPriority: 抢占优先级
 * @param  SubPriority: 子优先级
 * @retval 编码后的优先级值
 */
uint32_t NVIC_EncodePriority(uint32_t PriorityGroup, 
                             uint32_t PreemptPriority, 
                             uint32_t SubPriority)
{
    uint32_t PriorityGroupTmp = (PriorityGroup & (uint32_t)0x07UL);
    uint32_t PreemptPriorityBits;
    uint32_t SubPriorityBits;

    PreemptPriorityBits = ((7UL - PriorityGroupTmp) > (uint32_t)(__NVIC_PRIO_BITS)) ? 
                          (uint32_t)(__NVIC_PRIO_BITS) : (uint32_t)(7UL - PriorityGroupTmp);
    SubPriorityBits = ((PriorityGroupTmp + (uint32_t)(__NVIC_PRIO_BITS)) < (uint32_t)7UL) ? 
                      (uint32_t)0UL : (uint32_t)((PriorityGroupTmp - 7UL) + (uint32_t)(__NVIC_PRIO_BITS));

    return (
        ((PreemptPriority & (uint32_t)((1UL << (PreemptPriorityBits)) - 1UL)) << SubPriorityBits) |
        ((SubPriority & (uint32_t)((1UL << (SubPriorityBits)) - 1UL)))
    );
}

优先级分组示例

/**
 * @brief  配置中断优先级示例
 */
void Interrupt_Priority_Config(void)
{
    /* 配置优先级分组为2:2 */
    NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);

    /* 配置EXTI0中断:抢占优先级2,子优先级1 */
    uint32_t priority = NVIC_EncodePriority(NVIC_PRIORITYGROUP_2, 2, 1);
    NVIC_SetPriority(EXTI0_IRQn, priority);
    NVIC_EnableIRQ(EXTI0_IRQn);

    /* 配置USART1中断:抢占优先级1,子优先级0 */
    priority = NVIC_EncodePriority(NVIC_PRIORITYGROUP_2, 1, 0);
    NVIC_SetPriority(USART1_IRQn, priority);
    NVIC_EnableIRQ(USART1_IRQn);

    /* 配置TIM2中断:抢占优先级2,子优先级2 */
    priority = NVIC_EncodePriority(NVIC_PRIORITYGROUP_2, 2, 2);
    NVIC_SetPriority(TIM2_IRQn, priority);
    NVIC_EnableIRQ(TIM2_IRQn);
}

第四部分:异常处理流程

异常响应过程

当异常发生时,处理器会自动执行以下步骤:

1. 挂起当前指令执行
2. 保存上下文到栈
   - xPSR, PC, LR, R12, R3-R0
3. 更新LR为特殊值
   - 0xFFFFFFF9: 返回线程模式,使用MSP
   - 0xFFFFFFFD: 返回线程模式,使用PSP
   - 0xFFFFFFF1: 返回处理器模式,使用MSP
4. 切换到处理器模式
   - 自动切换到特权级别
   - 使用MSP
5. 从向量表读取处理函数地址
6. 跳转到异常处理函数
7. 执行异常处理代码
8. 异常返回(BX LR)
9. 自动恢复上下文
10. 返回被打断的代码

栈帧结构

异常发生时自动压栈的寄存器:

高地址
    ┌─────────┐
    │  xPSR   │  ← SP + 28
    ├─────────┤
    │   PC    │  ← SP + 24 (返回地址)
    ├─────────┤
    │   LR    │  ← SP + 20
    ├─────────┤
    │   R12   │  ← SP + 16
    ├─────────┤
    │   R3    │  ← SP + 12
    ├─────────┤
    │   R2    │  ← SP + 8
    ├─────────┤
    │   R1    │  ← SP + 4
    ├─────────┤
    │   R0    │  ← SP (栈指针)
    └─────────┘
低地址

获取栈帧信息

/**
 * @brief  获取异常栈帧
 * @retval 栈帧指针
 */
uint32_t* Get_Exception_Stack_Frame(void)
{
    uint32_t *stack_frame;

    /* 检查使用的栈指针 */
    if (__get_CONTROL() & CONTROL_SPSEL_Msk) {
        stack_frame = (uint32_t*)__get_PSP();
    } else {
        stack_frame = (uint32_t*)__get_MSP();
    }

    return stack_frame;
}

/**
 * @brief  打印异常栈帧信息
 */
void Print_Exception_Stack_Frame(void)
{
    uint32_t *stack_frame = Get_Exception_Stack_Frame();

    printf("=== 异常栈帧 ===\n");
    printf("R0  = 0x%08X\n", stack_frame[0]);
    printf("R1  = 0x%08X\n", stack_frame[1]);
    printf("R2  = 0x%08X\n", stack_frame[2]);
    printf("R3  = 0x%08X\n", stack_frame[3]);
    printf("R12 = 0x%08X\n", stack_frame[4]);
    printf("LR  = 0x%08X\n", stack_frame[5]);
    printf("PC  = 0x%08X\n", stack_frame[6]);
    printf("xPSR= 0x%08X\n", stack_frame[7]);
}

异常返回机制

异常返回通过执行BX LR指令实现,LR寄存器包含特殊的返回值(EXC_RETURN)。

EXC_RETURN值

0xFFFFFFF1: 返回处理器模式,使用MSP
0xFFFFFFF9: 返回线程模式,使用MSP
0xFFFFFFFD: 返回线程模式,使用PSP
0xFFFFFFE1: 返回处理器模式,使用MSP,浮点上下文
0xFFFFFFE9: 返回线程模式,使用MSP,浮点上下文
0xFFFFFFED: 返回线程模式,使用PSP,浮点上下文

异常返回示例

/**
 * @brief  中断处理函数示例
 */
void EXTI0_IRQHandler(void)
{
    /* 处理中断 */
    if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0)) {
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);

        /* 中断处理代码 */
        Handle_Interrupt();
    }

    /* 函数返回时自动执行异常返回 */
    /* 编译器会生成 BX LR 指令 */
}

中断嵌套

ARM Cortex-M支持中断嵌套,高优先级中断可以打断低优先级中断。

嵌套规则

  1. 抢占优先级决定嵌套
  2. 只有抢占优先级更高的中断才能嵌套
  3. 子优先级不影响嵌套

  4. 自动保存和恢复

  5. 硬件自动保存被打断中断的上下文
  6. 返回时自动恢复

  7. 尾链优化

  8. 连续的中断可以优化上下文切换
  9. 减少压栈和出栈开销

嵌套示例

/**
 * @brief  低优先级中断(抢占优先级3)
 */
void TIM2_IRQHandler(void)
{
    printf("TIM2中断开始\n");

    /* 长时间处理 */
    for (volatile int i = 0; i < 1000000; i++);

    printf("TIM2中断结束\n");
}

/**
 * @brief  高优先级中断(抢占优先级1)
 */
void EXTI0_IRQHandler(void)
{
    printf("  EXTI0中断开始(嵌套)\n");

    /* 快速处理 */
    Handle_Critical_Event();

    printf("  EXTI0中断结束\n");
}

/**
 * @brief  配置中断嵌套
 */
void Configure_Nested_Interrupts(void)
{
    /* 配置优先级分组 */
    NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);

    /* TIM2: 抢占优先级3,子优先级0 */
    NVIC_SetPriority(TIM2_IRQn, NVIC_EncodePriority(NVIC_PRIORITYGROUP_2, 3, 0));
    NVIC_EnableIRQ(TIM2_IRQn);

    /* EXTI0: 抢占优先级1,子优先级0 */
    NVIC_SetPriority(EXTI0_IRQn, NVIC_EncodePriority(NVIC_PRIORITYGROUP_2, 1, 0));
    NVIC_EnableIRQ(EXTI0_IRQn);
}

执行流程

主程序运行
TIM2中断触发 → 进入TIM2_IRQHandler
    正在执行TIM2处理代码
    EXTI0中断触发(优先级更高)
    保存TIM2上下文 → 进入EXTI0_IRQHandler
    执行EXTI0处理代码
    EXTI0返回 → 恢复TIM2上下文
    继续执行TIM2处理代码
TIM2返回 → 返回主程序

第五部分:中断编程实践

完整的中断配置流程

以配置外部中断为例,展示完整的中断配置过程:

/**
 * @brief  配置EXTI0外部中断
 * @note   PA0引脚,下降沿触发
 */
void EXTI0_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    /* 1. 使能GPIO时钟 */
    __HAL_RCC_GPIOA_CLK_ENABLE();

    /* 2. 配置GPIO */
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;  // 下降沿触发中断
    GPIO_InitStruct.Pull = GPIO_PULLUP;           // 上拉
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* 3. 配置NVIC */
    /* 设置优先级分组 */
    NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);

    /* 设置中断优先级:抢占优先级2,子优先级1 */
    uint32_t priority = NVIC_EncodePriority(NVIC_PRIORITYGROUP_2, 2, 1);
    NVIC_SetPriority(EXTI0_IRQn, priority);

    /* 使能中断 */
    NVIC_EnableIRQ(EXTI0_IRQn);

    printf("EXTI0中断已配置\n");
}

/**
 * @brief  EXTI0中断处理函数
 */
void EXTI0_IRQHandler(void)
{
    /* 检查中断标志 */
    if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
        /* 清除中断标志 */
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);

        /* 处理中断事件 */
        printf("按键按下\n");

        /* 翻转LED */
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
    }
}

定时器中断配置

/**
 * @brief  配置TIM2定时器中断
 * @note   1ms定时中断
 */
void TIM2_Config(void)
{
    TIM_HandleTypeDef htim2;

    /* 1. 使能TIM2时钟 */
    __HAL_RCC_TIM2_CLK_ENABLE();

    /* 2. 配置定时器参数 */
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 7200 - 1;      // 预分频:72MHz/7200 = 10kHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 10 - 1;           // 周期:10kHz/10 = 1kHz (1ms)
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;

    if (HAL_TIM_Base_Init(&htim2) != HAL_OK) {
        Error_Handler();
    }

    /* 3. 配置NVIC */
    NVIC_SetPriority(TIM2_IRQn, NVIC_EncodePriority(NVIC_PRIORITYGROUP_2, 2, 0));
    NVIC_EnableIRQ(TIM2_IRQn);

    /* 4. 启动定时器中断 */
    HAL_TIM_Base_Start_IT(&htim2);

    printf("TIM2定时器中断已配置\n");
}

/**
 * @brief  TIM2中断处理函数
 */
void TIM2_IRQHandler(void)
{
    /* 检查更新中断标志 */
    if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET) {
        if (__HAL_TIM_GET_IT_SOURCE(&htim2, TIM_IT_UPDATE) != RESET) {
            /* 清除中断标志 */
            __HAL_TIM_CLEAR_IT(&htim2, TIM_IT_UPDATE);

            /* 定时器溢出处理 */
            static uint32_t counter = 0;
            counter++;

            if (counter >= 1000) {  // 1秒
                counter = 0;
                printf("1秒定时到\n");
            }
        }
    }
}

UART中断配置

/**
 * @brief  配置USART1中断
 */
void USART1_Config(void)
{
    UART_HandleTypeDef huart1;

    /* 1. 使能时钟 */
    __HAL_RCC_USART1_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    /* 2. 配置GPIO */
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10;  // TX, RX
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* 3. 配置UART参数 */
    huart1.Instance = USART1;
    huart1.Init.BaudRate = 115200;
    huart1.Init.WordLength = UART_WORDLENGTH_8B;
    huart1.Init.StopBits = UART_STOPBITS_1;
    huart1.Init.Parity = UART_PARITY_NONE;
    huart1.Init.Mode = UART_MODE_TX_RX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart1.Init.OverSampling = UART_OVERSAMPLING_16;

    if (HAL_UART_Init(&huart1) != HAL_OK) {
        Error_Handler();
    }

    /* 4. 配置NVIC */
    NVIC_SetPriority(USART1_IRQn, NVIC_EncodePriority(NVIC_PRIORITYGROUP_2, 1, 0));
    NVIC_EnableIRQ(USART1_IRQn);

    /* 5. 使能接收中断 */
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);

    printf("USART1中断已配置\n");
}

/**
 * @brief  USART1中断处理函数
 */
void USART1_IRQHandler(void)
{
    /* 接收中断 */
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET) {
        if (__HAL_UART_GET_IT_SOURCE(&huart1, UART_IT_RXNE) != RESET) {
            /* 读取接收到的数据 */
            uint8_t data = (uint8_t)(huart1.Instance->DR & 0xFF);

            /* 处理接收数据 */
            UART_Receive_Handler(data);
        }
    }

    /* 发送完成中断 */
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC) != RESET) {
        if (__HAL_UART_GET_IT_SOURCE(&huart1, UART_IT_TC) != RESET) {
            __HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_TC);

            /* 发送完成处理 */
            UART_Transmit_Complete_Handler();
        }
    }
}

/**
 * @brief  UART接收数据处理
 */
void UART_Receive_Handler(uint8_t data)
{
    static uint8_t rx_buffer[256];
    static uint8_t rx_index = 0;

    /* 存储接收数据 */
    rx_buffer[rx_index++] = data;

    /* 检查是否接收完整帧 */
    if (data == '\n' || rx_index >= 256) {
        /* 处理接收到的数据 */
        Process_Received_Data(rx_buffer, rx_index);
        rx_index = 0;
    }
}

中断优先级管理

/**
 * @brief  系统中断优先级配置
 * @note   统一管理所有中断的优先级
 */
void System_Interrupt_Priority_Config(void)
{
    /* 配置优先级分组:2位抢占,2位子优先级 */
    NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);

    /* 关键中断:抢占优先级0(最高) */
    NVIC_SetPriority(NMI_IRQn, 0);

    /* 高优先级中断:抢占优先级1 */
    NVIC_SetPriority(USART1_IRQn, NVIC_EncodePriority(NVIC_PRIORITYGROUP_2, 1, 0));
    NVIC_SetPriority(EXTI0_IRQn, NVIC_EncodePriority(NVIC_PRIORITYGROUP_2, 1, 1));

    /* 中等优先级中断:抢占优先级2 */
    NVIC_SetPriority(TIM2_IRQn, NVIC_EncodePriority(NVIC_PRIORITYGROUP_2, 2, 0));
    NVIC_SetPriority(ADC1_IRQn, NVIC_EncodePriority(NVIC_PRIORITYGROUP_2, 2, 1));

    /* 低优先级中断:抢占优先级3 */
    NVIC_SetPriority(DMA1_Channel1_IRQn, NVIC_EncodePriority(NVIC_PRIORITYGROUP_2, 3, 0));

    /* 系统异常优先级 */
    NVIC_SetPriority(SysTick_IRQn, NVIC_EncodePriority(NVIC_PRIORITYGROUP_2, 3, 0));
    NVIC_SetPriority(PendSV_IRQn, NVIC_EncodePriority(NVIC_PRIORITYGROUP_2, 3, 3));
}

第六部分:中断编程最佳实践

中断服务函数设计原则

  1. 快速执行
  2. 中断处理应尽可能快
  3. 避免长时间占用CPU
  4. 复杂处理放到主循环

  5. 最小化操作

  6. 只做必要的处理
  7. 设置标志位,主循环处理
  8. 避免复杂计算

  9. 避免阻塞

  10. 不使用延时函数
  11. 不等待外设就绪
  12. 不使用printf(调试除外)

  13. 保护共享资源

  14. 使用临界区保护
  15. 避免竞态条件
  16. 使用原子操作

良好的中断处理示例

/* 全局标志和数据 */
volatile uint8_t uart_rx_flag = 0;
volatile uint8_t uart_rx_data = 0;

/**
 * @brief  UART中断处理函数(推荐)
 * @note   快速处理,设置标志
 */
void USART1_IRQHandler(void)
{
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) {
        /* 读取数据 */
        uart_rx_data = (uint8_t)(huart1.Instance->DR & 0xFF);

        /* 设置标志 */
        uart_rx_flag = 1;

        /* 快速返回 */
    }
}

/**
 * @brief  主循环处理
 */
void main_loop(void)
{
    while(1) {
        /* 检查标志 */
        if (uart_rx_flag) {
            uart_rx_flag = 0;

            /* 在主循环中处理数据 */
            Process_UART_Data(uart_rx_data);
        }

        /* 其他任务 */
    }
}

不推荐的中断处理

/**
 * @brief  不推荐的中断处理函数
 * @note   包含延时和复杂处理
 */
void EXTI0_IRQHandler(void)
{
    if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0)) {
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);

        /* ❌ 不推荐:延时 */
        HAL_Delay(100);

        /* ❌ 不推荐:复杂计算 */
        float result = Complex_Calculation();

        /* ❌ 不推荐:printf */
        printf("Result: %f\n", result);

        /* ❌ 不推荐:等待外设 */
        while(!UART_Ready());
        Send_Data(result);
    }
}

临界区保护

/**
 * @brief  进入临界区
 * @retval 之前的中断状态
 */
uint32_t Enter_Critical_Section(void)
{
    uint32_t primask = __get_PRIMASK();
    __disable_irq();
    return primask;
}

/**
 * @brief  退出临界区
 * @param  primask: 之前的中断状态
 */
void Exit_Critical_Section(uint32_t primask)
{
    __set_PRIMASK(primask);
}

/**
 * @brief  使用临界区保护共享变量
 */
void Update_Shared_Variable(void)
{
    /* 进入临界区 */
    uint32_t primask = Enter_Critical_Section();

    /* 访问共享变量 */
    shared_counter++;
    shared_buffer[index] = data;

    /* 退出临界区 */
    Exit_Critical_Section(primask);
}

/**
 * @brief  使用宏简化临界区
 */
#define CRITICAL_SECTION_BEGIN()  uint32_t __primask = __get_PRIMASK(); __disable_irq()
#define CRITICAL_SECTION_END()    __set_PRIMASK(__primask)

void Example_Function(void)
{
    CRITICAL_SECTION_BEGIN();

    /* 临界区代码 */
    shared_data = new_value;

    CRITICAL_SECTION_END();
}

中断调试技巧

/**
 * @brief  中断统计信息
 */
typedef struct {
    uint32_t count;           // 中断次数
    uint32_t max_duration;    // 最大执行时间
    uint32_t total_duration;  // 总执行时间
} IRQ_Stats_t;

IRQ_Stats_t irq_stats[60];  // 假设60个外设中断

/**
 * @brief  中断性能监控
 */
void IRQ_Performance_Monitor_Start(IRQn_Type IRQn)
{
    /* 启动DWT计数器 */
    DWT->CYCCNT = 0;
}

void IRQ_Performance_Monitor_End(IRQn_Type IRQn)
{
    /* 读取执行周期数 */
    uint32_t cycles = DWT->CYCCNT;

    /* 更新统计信息 */
    irq_stats[IRQn].count++;
    irq_stats[IRQn].total_duration += cycles;

    if (cycles > irq_stats[IRQn].max_duration) {
        irq_stats[IRQn].max_duration = cycles;
    }
}

/**
 * @brief  带性能监控的中断处理函数
 */
void EXTI0_IRQHandler(void)
{
    IRQ_Performance_Monitor_Start(EXTI0_IRQn);

    /* 中断处理代码 */
    if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0)) {
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
        Handle_Interrupt();
    }

    IRQ_Performance_Monitor_End(EXTI0_IRQn);
}

/**
 * @brief  打印中断统计信息
 */
void Print_IRQ_Statistics(void)
{
    printf("=== 中断统计信息 ===\n");

    for (int i = 0; i < 60; i++) {
        if (irq_stats[i].count > 0) {
            uint32_t avg_cycles = irq_stats[i].total_duration / irq_stats[i].count;
            uint32_t avg_us = avg_cycles / (SystemCoreClock / 1000000);
            uint32_t max_us = irq_stats[i].max_duration / (SystemCoreClock / 1000000);

            printf("IRQ %d: 次数=%d, 平均=%dus, 最大=%dus\n",
                   i, irq_stats[i].count, avg_us, max_us);
        }
    }
}

中断丢失检测

/**
 * @brief  中断丢失检测
 */
typedef struct {
    uint32_t expected_count;  // 期望的中断次数
    uint32_t actual_count;    // 实际的中断次数
    uint32_t lost_count;      // 丢失的中断次数
} IRQ_Monitor_t;

IRQ_Monitor_t irq_monitor;

/**
 * @brief  定时器中断(用于检测)
 */
void TIM2_IRQHandler(void)
{
    if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)) {
        __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);

        /* 增加期望计数 */
        irq_monitor.expected_count++;

        /* 增加实际计数 */
        irq_monitor.actual_count++;
    }
}

/**
 * @brief  检查中断丢失
 */
void Check_IRQ_Lost(void)
{
    if (irq_monitor.actual_count < irq_monitor.expected_count) {
        irq_monitor.lost_count = irq_monitor.expected_count - irq_monitor.actual_count;
        printf("警告:检测到%d个中断丢失\n", irq_monitor.lost_count);
    }
}

总结

本文详细介绍了ARM Cortex-M处理器的中断向量表和异常处理机制,主要内容包括:

核心要点

  1. 中断向量表
  2. 包含所有异常处理函数地址的数组
  3. 位于内存固定位置,可通过VTOR重定位
  4. 第一个条目是栈指针,后续是异常处理函数地址

  5. 异常类型

  6. 系统异常:Reset, NMI, HardFault, MemManage, BusFault, UsageFault, SVC, PendSV, SysTick
  7. 外部中断:由外设产生,数量因芯片而异

  8. NVIC控制器

  9. 管理所有外部中断和部分系统异常
  10. 支持中断使能/禁用、优先级配置、挂起管理
  11. 提供灵活的优先级分组机制

  12. 异常处理流程

  13. 自动保存上下文到栈
  14. 切换到处理器模式和特权级别
  15. 执行异常处理函数
  16. 自动恢复上下文并返回

  17. 中断嵌套

  18. 支持高优先级中断打断低优先级中断
  19. 抢占优先级决定嵌套能力
  20. 硬件自动管理上下文切换

最佳实践

  1. 中断处理函数
  2. 保持简短快速
  3. 避免延时和阻塞操作
  4. 使用标志位通知主循环

  5. 优先级配置

  6. 合理分配优先级
  7. 关键中断使用高优先级
  8. 统一管理优先级分组

  9. 共享资源保护

  10. 使用临界区保护
  11. 避免竞态条件
  12. 使用原子操作

  13. 调试和监控

  14. 添加性能监控
  15. 检测中断丢失
  16. 记录统计信息

延伸阅读

推荐进一步学习的内容:

  1. 相关主题
  2. 处理器工作模式与特权级别
  3. STM32启动过程深度分析
  4. DMA工作原理与应用场景

  5. 高级主题

  6. RTOS中的中断管理
  7. 中断延迟优化
  8. 实时性能分析

  9. 实践项目

  10. 多中断协同处理
  11. 中断驱动的通信系统
  12. 实时数据采集系统

参考资料

  1. ARM Cortex-M3权威指南 - Joseph Yiu
  2. ARM Cortex-M Programming Guide to Memory Barrier Instructions
  3. STM32 Cortex-M3编程手册 - ST Microelectronics
  4. The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors

练习题

  1. 解释中断向量表的结构,说明为什么第一个条目是栈指针而不是处理函数地址。

  2. 编写代码实现以下功能:

  3. 配置EXTI0外部中断(下降沿触发)
  4. 配置TIM2定时器中断(1ms周期)
  5. 设置合理的优先级,使EXTI0可以打断TIM2

  6. 分析以下代码的问题并改正:

    void EXTI0_IRQHandler(void)
    {
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
    
        HAL_Delay(100);  // 消抖延时
    
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
            printf("按键按下\n");
            Process_Button();
        }
    }
    

  7. 设计一个中断优先级方案,要求:

  8. 系统有UART、定时器、ADC、按键四种中断源
  9. UART数据不能丢失
  10. 定时器需要精确计时
  11. ADC采样可以延迟
  12. 按键响应要求不高

  13. 解释什么是中断嵌套,并说明在什么情况下会发生中断嵌套。

下一步:建议学习 芯片数据手册阅读指南,掌握如何从数据手册中获取中断相关信息。