STM32嵌套向量中断控制器(NVIC)及外部中断使用案例分析

发布于:2025-06-10 ⋅ 阅读:(23) ⋅ 点赞:(0)

前言

  本文并不是简单介绍一下外部中断的使用和NVIC浅层的内容,而是从内核角度,深入剖析中断的内涵,中断向量表和MVIC内部机理,最后以外部中断使用案例结束。相信会给你带来惊喜。
  对于嵌套向量中断控制器,也就是Nested vectored interrupt controller,缩写为NVIC,在所有的ARM Cortex-M3和Cortex-M4系列的所有芯片都适用,因为NVIC是内核层面的东西,不是一个外设,所以凡是这个内核的芯片,都有NVIC。于是本文虽从STM32F4入手讲解和使用案例分析,但对于其他芯片同样适用。本文具体参考的文档《The definitive guide to Arm Cortex-M3 and Cortex-M4 processors》。

什么是中断

  比如你在CSDN上写文章,忽然微信提示有消息,你停下来手上的事,保存一下你写的内容,简单记录一下你接下来的写作思路。之后去看一下手机,看到有人发微信问你:“马老师,发生甚么事了?”接下来他发来几张截图,你一看,哦,原来是昨天,有两个年轻人,一个体重80公斤,一个……最后你聊完微信,放下手机,回到电脑前,咦,忘记了当时想接下来写甚么了……还好你当时写了一下写作思路!看到了之前的写作思路,一下子豁然开朗,继续写作。
  在上面的案例中,在写作中,忽然微信提示有消息,这就是中断触发了,在中断里面相当于中断标志位置位了。你可以选择漠视它,继续写作,这个操作在中断里相当于中断使能位没有置位,所以即便中断标志位置位了,也不会去响应中断;也可以选择停下来手上的工作,去看一下微信消息。当然看之前也要保存好现场,将接下来想的写作思路记录一下,这一步相当于中断里的压栈Push或者说保存现场。等处理完微信的消息回来也知道写到哪里了,接下来应该怎么写。是吧?不过如果你非要说,我就不记录。那其实不是好习惯,对不对?当你处理完微信,回来继续写作,看到之前记录的内容,相当于出栈Pop,看到之后,你就知道接下来改怎么写了,也就是PC寄存器有了下一个指令的地址,就可以接着正确执行代码。

中断和异常

  我们所说的Arm Cortex-M3和Cortex-M4都基于ARMv7-M架构。在此架构中,中断是异常的子集,也就是说中断是异常的一种类型。中断是外设或者内部输入产生,在某些情形下,是可以由软件触发的。对于处理中断的函数也称为“中断服务例程”,Interrupt Service Routines,简称ISR。
  异常都是被NVIC处理的,NVIC处理大量的中断请求和不可屏蔽中断NMI请求。对于中断请求,可以是外设产生的,比如说定时器,又比如某一个GPIO引脚,也可以由看门狗或者电压监视模块PVD。其中滴答定时器SysTick的中断尤其常用于嵌入式操作系统中。处理器自身也会产生异常事件,比如Hard fault等,具体请参见下图。每一个异常都有异常编号。异常编号在1-15的是系统异常,异常编号为16及大于16的是中断。内核最多支持240个中断,具体芯片设计商采用了多少个是自己定的。比如STM32F4使用了82个可屏蔽中断。
在这里插入图片描述
  如上图所见,每个异常都有优先级,数值越小,优先级越高。比如优先级最高的是Reset复位异常,它的优先级是-3。再往下,数值在增加,优先级在降低。

芯片上电之后从哪里开始执行?

  复位中断函数!最高优先级的中断就是Reset。
在这里插入图片描述

  回到上面的图,Reset复位中断的入口地址是0x0000 0004,那么芯片上电之后就从这里开始执行!
  下图是启动文件的一部分,是汇编语言写的,这里就是复位中断处理函数体。
在这里插入图片描述
简单解释一下哈!
LDR R0, = SystemInit
这句话是说,把SystemInit函数的地址存在R0寄存器。
BLX R0
跳转到R0寄存器里面存储的地址去执行。
  两句代码放在一起就是执行函数SystemInit函数。
  那么接下来两句话也一样,执行了__main函数。不过请注意,这个函数不是我们的main函数。在这个__mian函数中会依次调用__scatterload()和__rt_entry()。__scatterload()负责数据段复制与ZI段初始化,而__rt_entry()初始化堆栈并进入main()。

NVIC详解

  首先,要明确一点,Cortex-M内核完全把中断和异常的事儿交给NVIC来管理了,所以我们从NVIC入手就可以明白内核里面的中断处理机制。
  嵌套向量中断控制器,也就是Nested vectored interrupt controller,缩写为NVIC。它是可编程的,而且它的寄存器在System Control Space(SCS)。下图是其具体位置。
在这里插入图片描述
在这里插入图片描述
  下面是中断向量表重定位寄存器,常用在想要在从Bootloader进入APP之后,重新定位中断向量表的位置的情景中。
在这里插入图片描述
  下面这个寄存器包括了中断优先级分组的字段PRIGROUP。比如STM32常使用的就有5种分组,即NVIC_Priority_Group_0、NVIC_Priority_Group_1、NVIC_Priority_Group_2、NVIC_Priority_Group_3、NVIC_Priority_Group_4。
在这里插入图片描述
在这里插入图片描述
上图取自《STM32F3 and STM32F4 Series Cortex®-M4 programming manual》

中断优先级分组

  下面是STM32在中断优先级分组的函数中具体的操作,跟手册的描述对应一下,方便理解。通常此函数在初始化阶段调用一次,往后就不再修改优先级分组了。

 /* Set Interrupt Group Priority */
  HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
{
  /* Check the parameters */
  assert_param(IS_NVIC_PRIORITY_GROUP(PriorityGroup));
  
  /* Set the PRIGROUP[10:8] bits according to the PriorityGroup parameter value */
  NVIC_SetPriorityGrouping(PriorityGroup);
}
  #define NVIC_SetPriorityGrouping    __NVIC_SetPriorityGrouping
__STATIC_INLINE void __NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
{
  uint32_t reg_value;
  uint32_t PriorityGroupTmp = (PriorityGroup & (uint32_t)0x07UL);             /* only values 0..7 are used          */

  reg_value  =  SCB->AIRCR;                                                   /* read old register configuration    */
  reg_value &= ~((uint32_t)(SCB_AIRCR_VECTKEY_Msk | SCB_AIRCR_PRIGROUP_Msk)); /* clear bits to change               */
  reg_value  =  (reg_value                                   |
                ((uint32_t)0x5FAUL << SCB_AIRCR_VECTKEY_Pos) |
                (PriorityGroupTmp << SCB_AIRCR_PRIGROUP_Pos)  );              /* Insert write key and priority group */
  SCB->AIRCR =  reg_value;
}

  对比上面手册的截图和代码的实现,是不是理解加深了很多?

中断优先级的设置

  对于一个中断,可以设置的优先级范围是由中断优先级分组决定的。但具体某一个中断设置优先级为多少,又是怎么操作的?
  在中断优先级中,一共有4BIT的位可以供给抢占优先级和子优先级。如果给抢占优先级的位多了,那么给子优先级的位自然就少了。
  代码中,设置优先级的函数如下:

void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority)
{ 
  uint32_t prioritygroup = 0x00U;
  
  /* Check the parameters */
  assert_param(IS_NVIC_SUB_PRIORITY(SubPriority));
  assert_param(IS_NVIC_PREEMPTION_PRIORITY(PreemptPriority));
  
  prioritygroup = NVIC_GetPriorityGrouping();
  
  NVIC_SetPriority(IRQn, NVIC_EncodePriority(prioritygroup, PreemptPriority, SubPriority));
}

  下面的函数在获取中断优先级分组。

  #define NVIC_GetPriorityGrouping    __NVIC_GetPriorityGrouping
__STATIC_INLINE uint32_t __NVIC_GetPriorityGrouping(void)
{
  return ((uint32_t)((SCB->AIRCR & SCB_AIRCR_PRIGROUP_Msk) >> SCB_AIRCR_PRIGROUP_Pos));
}

  下面是设置中断优先级的函数。

  #define NVIC_SetPriority            __NVIC_SetPriority
__STATIC_INLINE 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);
  }
}
#define __NVIC_PRIO_BITS          4U       /*!< STM32F4XX uses 4 Bits for the Priority Levels */

在这里插入图片描述
如上,看到操作的具体寄存器是SCB->SHP[0]到SCB->SHP[11],是一个寄存器组。查看代码,可以加深理解。
在这里插入图片描述

配置示例

  每一种分组都会决定了抢占优先级有几个等级可以选,子优先级有几个等级可以选。打比方说,这里设置成NVIC_Priority_Group_4,那么抢占优先级可以选0-15,共16个优先级,但没有子优先级。再比如,如果选择NVIC_Priority_Group_2,那么抢占优先级可以选0-3,子优先级也可以选择0-3。

下面以STM32CubeIDE中的配置界面来加深大家的理解。
在这里插入图片描述
如上图,可以选择的5种优先级分组。当前我选择了NVIC_Priority_Group_2。
在这里插入图片描述
在这里插入图片描述

中断向量表

  当一个异常发生的时候,对应的handler就要被执行,那么这个handler入口地址在哪里呢?Cortex-M内核使用了向量表的机制。通过将各个中断函数的入口地址写在一张表里,来快速找到并调用中断处理函数,这样就减少了中断的响应时间。
在这里插入图片描述
  上图是STM32F405xx/07xx和STM32F415xx/17xx芯片的中断向量表。
  这里的位置0以后的就是各种外设的中断。比如位置0是窗口看门狗的中断,入口地址在0x0000 0040。也就是说,如果窗口看门狗触发中断了,也中断使能了,那么这个时候,从哪里执行中断处理函数?0x0000 0040这个地址有中断处理函数的地址。当执行到这里的时候,会跳转到对应的函数中处理。
  中断向量表的位置是可以由我们人为修改的,但上电之后默认就是上图的地址,也就是从地址0开始。每个中断都有相对于起始地址的偏移量。我们可以将这张表放在别的位置,官方说法叫relocate。用户可以在程序运行之后,修改中断向量表的位置。这个做法在Bootloader和APP的跳转中比较常用。

栈内存操作

  在跳转到中断处理函数中执行前后,内核自动完成压栈和出栈操作,是不是很方便?我们不需要关心有没有保存现场,也不用担心在中断函数执行完成之后,找不到下一条执行的语句,或者影响了程序的正常执行。

STM32F4外部中断原理及使用案例

外部中断原理

外部中断/事件控制器包含23个用于产生事件或者中断的边沿检测器,用户可以配置成上升沿触发、下降沿触发、上升沿和下降沿都触发。
在这里插入图片描述
*上图引自《STM32F4中文参考手册》
关于这张图片,我们可以简单分析一下。最右边是外部中断/事件的输入引脚,从右边进来,是边沿检测器,我们可以配置寄存器来决定触发方法。
在这里插入图片描述
在这里插入图片描述
之后就到了逻辑或门或门另一边是软件中断事件寄存器。也就是说,即便外部引脚没有输入触发信号,单单从这个寄存器也可以为接下来的电路带来触发信号。这就为软件触发外部中断提供了方便。
在这里插入图片描述
之后就兵分两路,一路向上,直奔“中断”而去;另一路向下,直奔“事件”而去。

中断

先说上面,进入逻辑与门,而与门的另一个输入是中断屏蔽寄存器。也即是说,如果中断被屏蔽了,那么中断信号就到此为止了,不会继续往下走了。要想使用外部中断,这里需要置位。
在这里插入图片描述
之后就到了挂起请求寄存器。将中断信号存储在中断标志位里。
在这里插入图片描述
当挂起寄存器的相应位等于1之后,就向NVIC中断控制器提交中断。

事件

再来说下面,连接着逻辑与门与门的另一个输入是事件屏蔽寄存器。
在这里插入图片描述
如果将事件屏蔽了,那么就不会触发事件。如果这个寄存器相应的位等于1,那么就可以生成一个事件,传递到脉冲发生器,产生脉冲,进一步触发事件。

外部中断/事件线映射

上面说外部中断/事件有23根线,其中0-15就是GPIO外部中断的,比如说,”所有的0“都共用EXIT0。
在这里插入图片描述
另外七根 EXTI 线连接方式如下:

  • EXTI 线 16 连接到 PVD 输出
  • EXTI 线 17 连接到 RTC 闹钟事件
  • EXTI 线 18 连接到 USB OTG FS 唤醒事件
  • EXTI 线 19 连接到以太网唤醒事件
  • EXTI 线 20 连接到 USB OTG HS(在 FS 中配置)唤醒事
  • EXTI 线 21 连接到 RTC 入侵和时间戳事件
  • EXTI 线 22 连接到 RTC 唤醒事件

配置外部中断

在这里插入图片描述
在STM32CudeIDE界面上,芯片某引脚左键,选择GPIO_EXITx(这里的x是引脚的编号,比如PE2,就是GPIO_EXIT2)
之后,可以右击这个引脚,修改一下label,修改了可以方便我们使用。不过不修改也无所谓。
在这里插入图片描述
之后就进入NVIC设置中断优先级分组中断优先级
在这里插入图片描述

在这里插入图片描述
最后,点击GPIO,选择刚才设置的PE2,设置使用哪种边沿触发。
在这里插入图片描述
我的开发板将PE2连接到按键,而且是按下去是低电平。所以我选择下降沿触发,一按下去就触发。
在这里插入图片描述
在这里插入图片描述
配置到此结束。

代码编写

外部中断都共用这一个函数:HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  if(GPIO_PIN_4 == GPIO_Pin)
  {
	  HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
  }
  else if(GPIO_PIN_3 == GPIO_Pin)
  {
	  HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin);
	  HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_RESET);
  }
  else if(GPIO_PIN_2 == GPIO_Pin)
  {
	  HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin);
	  HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
	  HAL_GPIO_WritePin(BEEP_GPIO_Port, BEEP_Pin, GPIO_PIN_SET);
  }
  else
  {
	  /*do nothing*/
  }
}

在这个函数需要判断中断是来自哪个引脚的,之后做相应的处理逻辑。这样,就可以在外部引脚上产生中断,实现我们希望执行的功能。

至此本文结束,如果本文对您有帮助,欢迎点赞、收藏、转发。您的支持会鼓励我继续写作。

参考文献

《STM32F3 and STM32F4 Series Cortex®-M4 programming manual》
《Cortex M3与M4权威指南》