
一、理论NVICNested Vectored Interrupt Controller嵌套向量中断控制器——它是ARM Cortex-M7内核STM32H743基于该内核的核心外设负责管理所有中断的使能、优先级、响应顺序、嵌套规则是STM32处理外部/内部事件比如按键中断、定时器中断、串口中断的“总调度室”。一、NVIC到底是干啥的STM32H743运行过程中会遇到很多“紧急事件”比如按键按下需要立即响应、串口收到数据要及时处理、定时器到点要执行任务……这些事件就是「中断」。如果没有NVICCPU只能“轮询”不停检查有没有事件发生效率极低有了NVIC能给不同中断分配“优先级”比如串口中断比按键中断更重要先处理能控制哪些中断生效比如暂时屏蔽按键中断只响应串口能处理“中断嵌套”比如正在处理按键中断时来了更高优先级的串口中断先暂停按键、处理串口再回来继续能精准跳转到对应中断的处理函数向量表功能。NVIC是STM32H743的“中断管家”所有中断的“生杀大权”都归它管。二、STM32H743 NVIC的核心特性1. 中断的“分类”内核异常 外设中断NVIC管理的中断分两类其中外设中断实际开发用得最多内核异常比如硬故障、内存访问错误出问题一般是代码bug外设中断STM32H743的片上外设产生的中断比如GPIO中断、USART、TIM、DMA等这是编程的重点。STM32H743的外设中断数量很多超过100个每个中断都有唯一的「中断号」比如EXTI0中断号是6USART1中断号是37可以在ST官方的《STM32H743xx参考手册》里查“中断向量表”找到。2. 优先级的“两层规则”抢占优先级 子优先级这是NVIC最核心的点STM32H743的NVIC支持「优先级分组」把中断优先级拆成两部分抢占优先级Preemption Priority决定“能不能打断别人”数值越小优先级越高比如抢占优先级0 抢占优先级1。举例正在处理抢占优先级1的按键中断来了抢占优先级0的串口中断NVIC会暂停按键中断先处理串口这就是「中断嵌套」。子优先级Subpriority决定“同时到了该先处理谁”数值越小优先级越高但仅适用于「抢占优先级相同」的多个中断。举例串口和定时器的抢占优先级都是0子优先级串口是0、定时器是1若两个中断同时触发先处理串口。关键优先级分组Cortex-M7内核把优先级的“位数”分给抢占和子优先级STM32H743的NVIC支持8位优先级配置0~255但实际开发中常用HAL库的「优先级分组宏」来划分共5种分组方式推荐用分组2优先级分组宏HAL库抢占优先级位数子优先级位数抢占优先级范围子优先级范围新手适配场景NVIC_PRIORITYGROUP_00 级抢占优先级0-15 级子优先级仅00~15无嵌套仅排序NVIC_PRIORITYGROUP_10-1 级抢占优先级0-7 级子优先级0~10~7简单嵌套NVIC_PRIORITYGROUP_20-3 级抢占优先级0-3 级子优先级0~30~3中等嵌套NVIC_PRIORITYGROUP_30-7 级抢占优先级0-1 级子优先级0~70~1较复杂嵌套NVIC_PRIORITYGROUP_40-15 级抢占优先级0 级子优先级0~150全部可嵌套整个工程只能设置一次优先级分组一般在main函数开头、初始化外设前配置。3. 中断的“使能/失能”NVIC对每个中断都有“开关”使能Enable打开中断开关对应中断发生时CPU会响应失能Disable关闭中断开关就算中断发生CPU也不理会挂起Pending中断“待处理”比如中断触发了但CPU暂时没空处理NVIC会记录这个状态清除挂起处理完中断后需要清除“待处理”标记否则NVIC会反复触发该中断。三、NVIC的编程步骤基于HAL库以「EXTI按键中断」为例讲解STM32H743中NVIC的配置流程新手优先用HAL库避开寄存器直接操作步骤1配置优先级分组全局唯一在main函数开头配置默认一般用分组4#include stm32h7xx_hal.h int main(void) { // 1. 初始化HAL库 HAL_Init(); // 2. 配置NVIC优先级分组核心整个工程只配一次 HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); // 后续初始化时钟、GPIO、中断... }步骤2配置具体中断的优先级 使能中断比如配置EXTI0PA0WK_UP按键中断// 配置EXTI0中断的NVIC参数 void EXTI0_IRQConfig(void) { // 第一步设置中断优先级 // 参数1中断号EXTI0_IRQn是STM32H743定义的EXTI0中断号查手册可知是6 // 参数2抢占优先级 // 参数3子优先级 HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0); // 第二步使能该中断打开开关 HAL_NVIC_EnableIRQ(EXTI0_IRQn); // 若要暂时关闭中断用HAL_NVIC_DisableIRQ(EXTI0_IRQn); }步骤3编写中断处理函数中断服务程序ISRNVIC知道“哪个中断该执行哪个函数”是因为「中断向量表」——STM32启动时会把所有中断的处理函数地址存在向量表中触发中断时NVIC会直接跳转到对应函数。对于EXTI0中断函数名是固定的必须和启动文件中的向量表一致// EXTI0中断处理函数函数名不能改启动文件里定义的 void EXTI0_IRQHandler(void) { // 第一步HAL库固定操作清除中断挂起标志避免反复触发 if (HAL_GPIO_EXTI_GetITStatus(GPIO_PIN_0) ! RESET) { // 第二步执行你要做的事情比如按键处理 rt_kprintf(WK_UP按键中断触发\r\n); // 第三步清除中断标志位必须否则NVIC会认为中断还没处理完 HAL_GPIO_EXTI_ClearITPendingBit(GPIO_PIN_0); } }四、NVIC常见错误优先级分组配置多次整个工程只能调用一次HAL_NVIC_SetPriorityGrouping多次调用会导致所有中断优先级混乱优先级数值搞反新手容易以为“数值越大优先级越高”实际是「数值越小优先级越高」忘记清除中断标志处理完中断后必须清标志否则NVIC会反复触发同一中断表现为程序卡死、疯狂执行中断函数中断号写错比如把USART1_IRQn写成USART2_IRQn中断永远不响应查手册或STM32CubeMX确认中断号抢占优先级相同却想嵌套只有抢占优先级不同时才会触发中断嵌套子优先级不影响嵌套。五、用STM32CubeMX配置NVIC不用死记寄存器/函数STM32CubeMX可以图形化配置NVIC自动生成代码打开CubeMX选择STM32H743芯片点击「Pinout Configuration」→ 「NVIC」先在「Priority Grouping」选择分组比如Group4找到要配置的中断比如EXTI0勾选「Enabled」然后设置「Preemption Priority」和「Sub Priority」生成代码CubeMX会自动帮你调用HAL_NVIC_SetPriority和HAL_NVIC_EnableIRQ只需编写中断处理函数即可。六、理论总结NVIC的核心作用管理所有中断的使能、优先级、嵌套是中断的“总管家”优先级规则分抢占决定能否嵌套和子优先级决定同抢占优先级的执行顺序数值越小优先级越高编程三步法配置优先级分组→设置中断优先级使能→编写中断处理函数清标志。二、实际代码原理图因为rtthread系统封装了中断及GPIO的驱动所以使用此代码需要先将部分驱动屏蔽将drv_gpio.c全部屏蔽屏蔽注意因为这里是分析原理做demo而已实际项目新手需谨慎如果不是精通rtthread莫要轻易修改驱动层代码然后使用hal库的方式/* * Copyright (c) 2006-2021, RT-Thread Development Team * * SPDX-License-Identifier: Apache-2.0 * * Change Logs: * Date Author Notes * 2019-03-05 whj4674672 first version */ #include rtthread.h #include rtdevice.h #include board.h //////////////////////////////////////////////GPIO定义///////////////////////////////////////////////////////////////// /* 引脚 定义 */ #define KEY0_GPIO_PORT GPIOH #define KEY0_GPIO_PIN GPIO_PIN_3 #define KEY0_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOH_CLK_ENABLE(); }while(0) /* PH口时钟使能 */ #define KEY1_GPIO_PORT GPIOH #define KEY1_GPIO_PIN GPIO_PIN_2 #define KEY1_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOH_CLK_ENABLE(); }while(0) /* PH口时钟使能 */ #define KEY2_GPIO_PORT GPIOC #define KEY2_GPIO_PIN GPIO_PIN_13 #define KEY2_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能 */ #define WKUP_GPIO_PORT GPIOA #define WKUP_GPIO_PIN GPIO_PIN_0 #define WKUP_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */ #define KEY0 HAL_GPIO_ReadPin(KEY0_GPIO_PORT, KEY0_GPIO_PIN) /* 读取KEY0引脚 */ #define KEY1 HAL_GPIO_ReadPin(KEY1_GPIO_PORT, KEY1_GPIO_PIN) /* 读取KEY1引脚 */ #define KEY2 HAL_GPIO_ReadPin(KEY2_GPIO_PORT, KEY2_GPIO_PIN) /* 读取KEY2引脚 */ #define WK_UP HAL_GPIO_ReadPin(WKUP_GPIO_PORT, WKUP_GPIO_PIN) /* 读取WKUP引脚 */ #define KEY0_PRES 1 /* KEY0按下 */ #define KEY1_PRES 2 /* KEY1按下 */ #define KEY2_PRES 3 /* KEY2按下 */ #define WKUP_PRES 4 /* KEY_UP按下(即WK_UP) */ //////////////////////////////////////////////引脚、中断编号和中断服务函数 定义//////////////////////////////////////////// #define KEY0_INT_GPIO_PORT GPIOH #define KEY0_INT_GPIO_PIN GPIO_PIN_3 #define KEY0_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOH_CLK_ENABLE(); }while(0) /* PH口时钟使能 */ #define KEY0_INT_IRQn EXTI3_IRQn #define KEY0_INT_IRQHandler EXTI3_IRQHandler #define KEY1_INT_GPIO_PORT GPIOH #define KEY1_INT_GPIO_PIN GPIO_PIN_2 #define KEY1_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOH_CLK_ENABLE(); }while(0) /* PH口时钟使能 */ #define KEY1_INT_IRQn EXTI2_IRQn #define KEY1_INT_IRQHandler EXTI2_IRQHandler #define KEY2_INT_GPIO_PORT GPIOC #define KEY2_INT_GPIO_PIN GPIO_PIN_13 #define KEY2_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能 */ #define KEY2_INT_IRQn EXTI15_10_IRQn #define KEY2_INT_IRQHandler EXTI15_10_IRQHandler #define WKUP_INT_GPIO_PORT GPIOA #define WKUP_INT_GPIO_PIN GPIO_PIN_0 #define WKUP_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */ #define WKUP_INT_IRQn EXTI0_IRQn #define WKUP_INT_IRQHandler EXTI0_IRQHandler /** * brief 按键初始化函数 * param 无 * retval 无 */ void key_gpio_init(void) { GPIO_InitTypeDef gpio_init_struct; /* GPIO配置参数存储变量 */ KEY0_GPIO_CLK_ENABLE(); /* KEY0时钟使能 */ KEY1_GPIO_CLK_ENABLE(); /* KEY1时钟使能 */ KEY2_GPIO_CLK_ENABLE(); /* KEY2时钟使能 */ WKUP_GPIO_CLK_ENABLE(); /* WKUP时钟使能 */ gpio_init_struct.Pin KEY0_GPIO_PIN; /* KEY0引脚 */ gpio_init_struct.Mode GPIO_MODE_INPUT; /* 输入 */ gpio_init_struct.Pull GPIO_PULLUP; /* 上拉 */ gpio_init_struct.Speed GPIO_SPEED_FREQ_HIGH; /* 高速 */ HAL_GPIO_Init(KEY0_GPIO_PORT, gpio_init_struct); /* KEY0引脚模式设置,上拉输入 */ gpio_init_struct.Pin KEY1_GPIO_PIN; /* KEY1引脚 */ gpio_init_struct.Mode GPIO_MODE_INPUT; /* 输入 */ gpio_init_struct.Pull GPIO_PULLUP; /* 上拉 */ gpio_init_struct.Speed GPIO_SPEED_FREQ_HIGH; /* 高速 */ HAL_GPIO_Init(KEY1_GPIO_PORT, gpio_init_struct); /* KEY1引脚模式设置,上拉输入 */ gpio_init_struct.Pin KEY2_GPIO_PIN; /* KEY2引脚 */ gpio_init_struct.Mode GPIO_MODE_INPUT; /* 输入 */ gpio_init_struct.Pull GPIO_PULLUP; /* 上拉 */ gpio_init_struct.Speed GPIO_SPEED_FREQ_HIGH; /* 高速 */ HAL_GPIO_Init(KEY2_GPIO_PORT, gpio_init_struct); /* KEY2引脚模式设置,上拉输入 */ gpio_init_struct.Pin WKUP_GPIO_PIN; /* WKUP引脚 */ gpio_init_struct.Mode GPIO_MODE_INPUT; /* 输入 */ gpio_init_struct.Pull GPIO_PULLDOWN; /* 下拉 */ gpio_init_struct.Speed GPIO_SPEED_FREQ_HIGH; /* 高速 */ HAL_GPIO_Init(WKUP_GPIO_PORT, gpio_init_struct); /* WKUP引脚模式设置,下拉输入 */ } /** * brief 外部中断初始化程序 * param 无 * retval 无 */ void exit_init(void) { GPIO_InitTypeDef gpio_init_struct; gpio_init_struct.Pin KEY0_INT_GPIO_PIN; gpio_init_struct.Mode GPIO_MODE_IT_FALLING; /* 下降沿触发 */ gpio_init_struct.Pull GPIO_PULLUP; HAL_GPIO_Init(KEY0_INT_GPIO_PORT, gpio_init_struct); /* KEY0配置为下降沿触发中断 */ gpio_init_struct.Pin KEY1_INT_GPIO_PIN; gpio_init_struct.Mode GPIO_MODE_IT_FALLING; /* 下降沿触发 */ gpio_init_struct.Pull GPIO_PULLUP; HAL_GPIO_Init(KEY1_INT_GPIO_PORT, gpio_init_struct); /* KEY1配置为下降沿触发中断 */ gpio_init_struct.Pin KEY2_INT_GPIO_PIN; gpio_init_struct.Mode GPIO_MODE_IT_FALLING; /* 下降沿触发 */ gpio_init_struct.Pull GPIO_PULLUP; HAL_GPIO_Init(KEY2_INT_GPIO_PORT, gpio_init_struct); /* KEY2配置为下降沿触发中断 */ gpio_init_struct.Pin WKUP_INT_GPIO_PIN; gpio_init_struct.Mode GPIO_MODE_IT_RISING; /* 上升沿触发 */ gpio_init_struct.Pull GPIO_PULLDOWN; HAL_GPIO_Init(WKUP_GPIO_PORT, gpio_init_struct); /* WKUP配置为上升沿触发中断 */ HAL_NVIC_SetPriority(KEY0_INT_IRQn, 0, 2); /* 抢占0子优先级2 */ HAL_NVIC_EnableIRQ(KEY0_INT_IRQn); /* 使能中断线3 */ HAL_NVIC_SetPriority(KEY1_INT_IRQn, 1, 2); /* 抢占1子优先级2 */ HAL_NVIC_EnableIRQ(KEY1_INT_IRQn); /* 使能中断线2 */ HAL_NVIC_SetPriority(KEY2_INT_IRQn, 2, 2); /* 抢占2子优先级2 */ HAL_NVIC_EnableIRQ(KEY2_INT_IRQn); /* 使能中断线13 */ HAL_NVIC_SetPriority(WKUP_INT_IRQn, 3, 2); /* 抢占3子优先级2 */ HAL_NVIC_EnableIRQ(WKUP_INT_IRQn); /* 使能中断线0 */ } /** * brief KEY0 外部中断服务程序 * param 无 * retval 无 */ void KEY0_INT_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(KEY0_INT_GPIO_PIN); /* 调用中断处理函数,清除中断标志位 */ __HAL_GPIO_EXTI_CLEAR_IT(KEY0_INT_GPIO_PIN); /* 退出时再清一次中断避免按键抖动误触发 */ } /** * brief KEY1 外部中断服务程序 * param 无 * retval 无 */ void KEY1_INT_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(KEY1_INT_GPIO_PIN); /* 调用中断处理函数,清除中断标志位 */ __HAL_GPIO_EXTI_CLEAR_IT(KEY1_INT_GPIO_PIN); /* 退出时再清一次中断避免按键抖动误触发 */ } /** * brief KEY2 外部中断服务程序 * param 无 * retval 无 */ void KEY2_INT_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(KEY2_INT_GPIO_PIN); /* 调用中断处理函数,清除中断标志位 */ __HAL_GPIO_EXTI_CLEAR_IT(KEY2_INT_GPIO_PIN); /* 退出时再清一次中断避免按键抖动误触发 */ } /** * brief WK_UP 外部中断服务程序 * param 无 * retval 无 */ void WKUP_INT_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler( WKUP_INT_GPIO_PIN); /* 调用中断处理公用函数,清除KEY_UP所在中断线的中断标志位中断下半部在HAL_GPIO_EXTI_Callback执行 */ __HAL_GPIO_EXTI_CLEAR_IT(WKUP_INT_GPIO_PIN); /* HAL库默认先清中断再处理回调退出时再清一次中断避免按键抖动误触发 */ } /** * brief 中断服务程序中需要做的事情 在HAL库中所有的外部中断服务函数都会调用此函数 * param GPIO_Pin:中断引脚号 * retval 无 */ void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { switch (GPIO_Pin) { case KEY0_INT_GPIO_PIN: if (KEY0 0) { rt_kprintf(\n key0 key pressed\r\n); } break; case KEY1_INT_GPIO_PIN: if (KEY1 0) { rt_kprintf(\n key1 key pressed\r\n); } break; case KEY2_INT_GPIO_PIN: if (KEY2 0) { rt_kprintf(\n key2 key pressed\r\n); } break; case WKUP_INT_GPIO_PIN: if (WK_UP 1) { rt_kprintf(\n up key pressed\r\n); } break; default : break; } } int main(void) { key_gpio_init(); exit_init(); while(1) { rt_thread_mdelay(100); } }通过代码会发现编码过程就是三部曲配置优先级分组→设置中断优先级使能→编写中断处理函数清标志。配置正确最关心的就是中断优先级中断函数中处理是否恰当。中断函数中处理方法论见https://carrylee.blog.csdn.net/article/details/155187711三、现象这里只是为了说明中断的使用方法未对按键进行消抖处理四、仿真这里介绍一个技巧cubemux中配置了一般比较靠谱但是如果想再次确认中断优先级配置是否正确可以放真时如图所示4个按键的中断优先级五、rtthread中使用中断这里附上如果使用rtthread封装好的驱动使用中断的方法/* * Copyright (c) 2006-2021, RT-Thread Development Team * * SPDX-License-Identifier: Apache-2.0 * * Change Logs: * Date Author Notes * 2019-03-05 whj4674672 first version */ #include rtthread.h #include rtdevice.h #include board.h #define KEY0_PIN GET_PIN(H, 3) #define KEY1_PIN GET_PIN(H, 2) #define KEY2_PIN GET_PIN(C, 13) #define WKUP_PIN GET_PIN(A, 0) /* 中断回调函数 */ void key0_on(void *args) { rt_kprintf(\n key0 key pressed\r\n); } /* 中断回调函数 */ void key1_on(void *args) { rt_kprintf(\n key1 key pressed\r\n); } /* 中断回调函数 */ void key2_on(void *args) { rt_kprintf(\n key2 key pressed\r\n); } /* 中断回调函数 */ void keyup_on(void *args) { rt_kprintf(\n up key pressed\r\n); } static void pin_beep_sample(void) { /* 按键0引脚为输入模式 */ rt_pin_mode(KEY0_PIN, PIN_MODE_INPUT_PULLUP); /* 绑定中断下降沿模式回调函数名为beep_on */ rt_pin_attach_irq(KEY0_PIN, PIN_IRQ_MODE_FALLING, key0_on, RT_NULL); /* 使能中断 */ rt_pin_irq_enable(KEY0_PIN, PIN_IRQ_ENABLE); /* 按键0引脚为输入模式 */ rt_pin_mode(KEY1_PIN, PIN_MODE_INPUT_PULLUP); /* 绑定中断下降沿模式回调函数名为beep_on */ rt_pin_attach_irq(KEY1_PIN, PIN_IRQ_MODE_FALLING, key1_on, RT_NULL); /* 使能中断 */ rt_pin_irq_enable(KEY1_PIN, PIN_IRQ_ENABLE); /* 按键0引脚为输入模式 */ rt_pin_mode(KEY2_PIN, PIN_MODE_INPUT_PULLUP); /* 绑定中断下降沿模式回调函数名为beep_on */ rt_pin_attach_irq(KEY2_PIN, PIN_IRQ_MODE_FALLING, key2_on, RT_NULL); /* 使能中断 */ rt_pin_irq_enable(KEY2_PIN, PIN_IRQ_ENABLE); /* 按键0引脚为输入模式 */ rt_pin_mode(WKUP_PIN, PIN_MODE_INPUT_PULLUP); /* 绑定中断下降沿模式回调函数名为beep_on */ rt_pin_attach_irq(WKUP_PIN, PIN_IRQ_MODE_RISING, keyup_on, RT_NULL); /* 使能中断 */ rt_pin_irq_enable(WKUP_PIN, PIN_IRQ_ENABLE); } int main(void) { pin_beep_sample(); while(1) { rt_thread_mdelay(100); } }相较于直接调用 HAL 库编写 GPIO 中断、按键检测等功能需编写大量底层初始化、中断服务函数、状态管理代码基于 RT-Thread 的开发方式更为简洁精炼 —— 通过封装好的 PIN 设备接口如rt_pin_attach_irq/rt_pin_irq_enable仅需几行代码即可完成中断配置、事件响应无需编写冗余的硬件操作代码大幅简化开发流程。仿真可以看这4个的优先级设置可以看到中断优先级都是5,分析源码是因为RTthread 底层驱动将优先级固定设置成了5如果有时这个设置不合理童鞋可以修改该代码。六、寄存器NVIC的中断设置截自《Cortex M7 Generic User Guide》SCB中AIRCRNVIC寄存器截自《Cortex M7 Technical Reference Manual》__IOM uint32_t ISER[8U]; /* 中断使能寄存器 */ uint32_t RESERVED0[24U]; __IOM uint32_t ICER[8U]; /* 中断除能寄存器 */ uint32_t RSERVED1[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]; /* 中断优先级寄存器8Bit 位宽 */ uint32_t RESERVED5[644U]; __OM uint32_t STIR; /* 中断触发中断寄存器 */仿真时可以看寄存器的状态中断寄存器七、总结尽管 NVIC 的基础使用方法并不复杂但它堪称单片机系统的 “命脉”操作系统的调度依赖中断驱动而各类外设中断优先级的设置更是系统稳定运行的关键 —— 不合理的优先级配置可能引发中断嵌套混乱、响应时序异常最终导致系统崩溃。