观前提醒:本文简要回顾了EXTI及NVIC相关知识点,分析了stm32f1系列单片机外部中断回调机制


开始之前,先温习一下有关EXTI和NVIC的知识点

外部中断/事件控制器(EXTI)

对于互联型产品(105、107系列),外部中断/事件控制器由20个产生事件/中断请求的边沿检测器组成,对于其它
型号,则有19个能产生事件/中断请求的边沿检测器。
每个输入线可以独立地配置输入类型(脉冲或挂起)和对应的触发事件(上升沿或下降沿或者双边沿都触发)。每个输入线都可以独立地被屏蔽。挂起寄存器保持着状态线的中断请求
简而言之:EXTI是一个检测器,检测到符合条件的边沿,即向NVIC发送中断请求(此处仅讨论中断)
外部中断/事件控制器框图:

嵌套向量中断控制器(NVIC)

NVIC英文全称是Nested Vectored Interrupt Controller,中文意思就是嵌套向量中断控制器,它属于M3内核的一个外设,控制着芯片的中断相关功能。由于ARM给NVIC预留了非常多的功能,但对于使用M3内核设计芯片的公司可能就不需要这么多功能,于是就需要在NVIC上裁剪。ST公司的STM32F103芯片内部中断数量就是NVIC裁剪后的结果。中断控制相关寄存器在固件库core_cm3.h文件NVIC结构体内。可打开任意库函数工程即可查看到。NVIC和处理器核的接口紧密相连,可以实现低延迟的中断处理和高效地处理晚到的中断。
STM32F103芯片支持60个可屏蔽中断通道,并且每个中断通道都具有自己的中断优先级控制字节。这些中断优先级控制字节由8位组成,但在STM32F103芯片中,只使用其中的高4位,即4位优先级。
在STM32F103芯片中,中断优先级分为抢占式优先级和响应优先级(亚优先级或副优先级)两部分。抢占式优先级用于决定中断事件是否能够打断当前正在执行的主程序或中断程序,而响应优先级用于确定同一抢占式优先级下,中断的相对优先级。
当两个中断的抢占式优先级相同时,高响应优先级的中断会先被响应和处理。也就是说,如果两个中断同时到达,具有更高响应优先级的中断将先被处理。
当两个中断的抢占式优先级不同时,具有较高抢占式优先级的中断可以打断正在执行的较低抢占式优先级中断。这种情况下,中断嵌套发生,高抢占式优先级的中断会打断当前正在执行的较低抢占式优先级中断,并开始执行自己的中断服务程序。
当两个中断的抢占式优先级相同时,如果一个中断正在处理,而另一个中断到达,后到达的中断需要等待前一个中断处理完毕后才能被处理。如果两个中断同时到达,中断控制器会根据它们的响应优先级来决定先处理哪个中断。如果它们的抢占式优先级和响应优先级都相等,则根据它们在中断表中的排位顺序决定先处理哪个。
简而言之:NVIC的功能是管理和控制中断的优先级和中断服务程序的执行。
当然这都是形而上的解释,最通俗易懂的说,NVIC应该是“厕所管理员”,它用于控制中断优先缓急以及嵌套中断和打扫“厕所”。


说起来很复杂,但是在实际操作中,复杂操作都被封装好了,我们只需要几行代码就能完成设置。
就比如上篇中配置NVIC的代码:

 /*Configure GPIO pin : PtPin */
  GPIO_InitStruct.Pin = KEY_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(KEY_GPIO_Port, &GPIO_InitStruct);

  /* EXTI interrupt init*/
  HAL_NVIC_SetPriority(EXTI2_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(EXTI2_IRQn);

就通过 HAL_NVIC_SetPriority(EXTI2_IRQn, 0, 0);和HAL_NVIC_EnableIRQ(EXTI2_IRQn);来设置优先级和使能中断。
详细的可以看一下这一篇:《STM32 HAL库》中断相关函数详尽解析——NVIC


中断函数调用流程:中断服务函数EXTIX_IRQHandler ( ) → 中断处理公共函数HAL_GPIO_EXTI_IRQHandler() → 中断回调函数HAL_GPIO_EXTI_Callback()。
在startup_stm32f105xc.s中,就规定了各类中断对应的中断服务函数

以GPIOC_PIN2对应的EXTI2为例,可以在上图中找到,EXTI2_IRQHandler就是其中断服务函数 后面接着的[WEAK]表示其是弱定义,用户可以自己重新定义。


EXTIX_IRQHandler()

还以上篇中的中断点灯为例,
当按键按下又抬起,EXTI2检测到上升沿后会向NVIC发送发送一个中断请求:
具体而言,EXTI会将以下信息发送给NVIC:

  • 中断源标识:EXTI会告知NVIC是哪个外部中断源(对应的EXTI线)引发了中断。这个标识可以用来区分不同的中断源。在此实例中,EXTI2对应的specific Interrupt Numbers是:

    EXTI2_IRQn                  = 8,      /*!< EXTI Line2 Interrupt
    
  • 中断挂起请求:EXTI会向NVIC发送一个中断挂起请求,以通知NVIC有一个中断事件需要被处理。这样,NVIC就可以相应地对中断进行处理。

NVIC 在接收到中断请求后,会根据 EXTI 提供的中断号,访问中断向量表,并跳转到相应的中断服务程序的地址执行
EXTI2对应的中断服务函数就是EXTI2_IRQHandler()
部分EXTI中断向量表(互联型)

EXTI2_IRQHandler函数定义

void EXTI2_IRQHandler(void)
{
  /* USER CODE BEGIN EXTI2_IRQn 0 */

  /* USER CODE END EXTI2_IRQn 0 */
  HAL_GPIO_EXTI_IRQHandler(KEY_Pin);
  /* USER CODE BEGIN EXTI2_IRQn 1 */

  /* USER CODE END EXTI2_IRQn 1 */
}

HAL_GPIO_EXTI_IRQHandler()

EXTI2_IRQHandler函数中调用了HAL_GPIO_EXTI_IRQHandler,HAL_GPIO_EXTI_IRQHandler是GPIO的中断处理公共函数
以下是本实例中的函数定义

void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
  /* EXTI line interrupt detected */
  if (__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u)
  {
    __HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
    HAL_GPIO_EXTI_Callback(GPIO_Pin);
  }
}

让我们逐步解析该函数的功能:

  • 检查中断标志:
    if语句用于检查指定GPIO_Pin的中断标志是否被设置。__HAL_GPIO_EXTI_GET_IT(GPIO_Pin)函数用于检查中断标志。

  • 清除中断标志:
    调用__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin)函数来清除中断标志。这样做是为了防止中断重复触发。

  • 调用回调函数:
    调用HAL_GPIO_EXTI_Callback(GPIO_Pin)函数。该函数是用户在初始化过程中注册的回调函数。它用于处理与GPIO_Pin相关的特定中断事件。

    HAL_GPIO_EXTI_Callback()

    系统中提供了一个__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin),这是一个弱函数,定义如下:

    __weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
    {
    /* Prevent unused argument(s) compilation warning /
    UNUSED(GPIO_Pin);
    / NOTE: This function Should not be modified, when the callback is needed,
    the HAL_GPIO_EXTI_Callback could be implemented in the user file
    */
    }
    

    这个函数的目的是作为一个占位符,在库中提供一个默认的回调函数实现。用户可以根据自己的需求在自己的文件中实现HAL_GPIO_EXTI_Callback函数,以处理对应的GPIO引脚的中断事件。其中使用UNUSED宏来标记未使用的GPIO_Pin参数,以避免编译器产生未使用参数的警告。
    在本实例中,我们直接在main.c中定义此函数

    void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
    {
      ms_Delay(50);
      if(GPIO_Pin == KEY_Pin)
      {
          if(HAL_GPIO_ReadPin(GPIOE, KEY_Pin)==0)
          {
              HAL_GPIO_TogglePin(GPIOC, LED0_Pin);
          }
      }
    }
    

    让我们逐步解析该函数的功能:

  • 延时函数调用:首先,调用了ms_Delay(50)函数,该函数是一个延时函数,用于在中断回调函数中添加一个延时等待的时间。具体的延时时间是50毫秒。

  • 判断GPIO引脚:接下来,使用条件语句判断GPIO_Pin是否等于KEY_Pin。这是为了确保回调函数只处理与KEY_Pin相关的中断事件。

  • 读取GPIO引脚状态:使用HAL_GPIO_ReadPin函数读取GPIOE端口上的KEY_Pin引脚的状态。如果引脚的状态为低电平(0),则执行下面的操作。

  • 控制LED引脚状态:使用HAL_GPIO_TogglePin函数来切换GPIOC端口上的LED0_Pin引脚的状态。这个操作会将LED的状态从亮变暗或从暗变亮。