观前提醒:本文以外部中断点灯为例,分析了HAL库中中断服务相关函数的使用及相互关联、底层原理

一、实例实现

实例简介:主函数控制LED2进行闪烁,中断控制LED0开关

我们先使用CubeMX创建项目文件:
1、在System Core中配置外部时钟,这里视板子的不同而改变,我这里只有外部高速晶振,所以开启外部高速晶振。

2、配置时钟树,如图:

3、配置GPIO,原理图如下:

我们配置PC2为上升沿检测中断模式,因为PC2引脚在电路上已经上拉,所以配置时选择无上下拉。PC0与PC14配置为推挽输出模式。这里对PC2命名为KEY,PC0与PC14命名为LED0与LED2

4、在SYS中开启SW调试

5、项目设置


6、生成项目文件

7、编写回调函数及main函数:
回调函数:

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);
        }
    }
}

main函数部分:

while (1)
  {
        ms_Delay(200);
        HAL_GPIO_TogglePin(GPIOC, LED2_Pin);
  }

下附完整main.c代码

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2024 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
void ms_Delay(uint16_t t_ms)
{
    uint32_t t=t_ms*3127;
    while(t--);
}



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);
        }
    }
}
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

        ms_Delay(200);
        HAL_GPIO_TogglePin(GPIOC, LED2_Pin);
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
  RCC_OscInitStruct.Prediv1Source = RCC_PREDIV1_SOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI_DIV2;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  RCC_OscInitStruct.PLL2.PLL2State = RCC_PLL_NONE;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_1) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure the Systick interrupt time
  */
  __HAL_RCC_PLLI2S_ENABLE();
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

二、NVIC函数分析

温馨提示:阅读以下部分之前请完成NVIC及EXIT等前置任务

HAL_NVIC_SetPriority()

下面的一段代码初始化了PC2引脚,并对PC2的中断功能进行了初始化,
其中:HAL_GPIO_Init实现了GPIO的初始化,包括将KEY_Pin与EXTI2“连接”
HAL_NVIC_SetPriority()实现了对EXTI2的优先级设置

/*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()定义:

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));
}
  • uint32_t prioritygroup = 0x00U;
    声明一个prioritygroup变量,并初始化为0x00。

  • assert_param(IS_NVIC_SUB_PRIORITY(SubPriority));
    使用断言(assert)来检查SubPriority参数的有效性。该宏IS_NVIC_SUB_PRIORITY用于检查SubPriority是否在有效范围内,如果不在有效范围内,将触发断言。

  • assert_param(IS_NVIC_PREEMPTION_PRIORITY(PreemptPriority));
    使用断言(assert)来检查PreemptPriority参数的有效性。该宏IS_NVIC_PREEMPTION_PRIORITY用于检查PreemptPriority是否在有效范围内,如果不在有效范围内,将触发断言。

  • prioritygroup = NVIC_GetPriorityGrouping();
    调用NVIC_GetPriorityGrouping函数获取当前的优先级分组设置,并将结果保存在prioritygroup变量中。

  • NVIC_SetPriority(IRQn, NVIC_EncodePriority(prioritygroup, PreemptPriority, SubPriority));
    调用NVIC_SetPriority函数来设置指定中断的优先级。IRQn参数表示要设置优先级的中断,NVIC_EncodePriority函数用于将优先级分组、抢占优先级和子优先级编码为优先级值,然后将编码后的优先级值传递给NVIC_SetPriority函数。

    承接上述分析,我们对其中所使用的函数继续进行深挖

NVIC_GetPriorityGrouping()

查看此函数的定义我们可以看到:

#define NVIC_GetPriorityGrouping    __NVIC_GetPriorityGrouping

在C语言中,以双下划线开头的标识符是保留给编译器和库使用的。此标识符用于实现内部功能或提供特定的功能接口。在代码中,__NVIC_GetPriorityGrouping 函数的双下划线前缀表明这是一个内部函数。套一层宏定义可以方便识读
继续跳转定义:

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

这段代码是一个内联函数(__NVIC_GetPriorityGrouping),用于获取NVIC(Nested Vectored Interrupt Controller)的优先级分组配置
该函数通过读取SCB_AIRCR寄存器的值,并使用位操作来提取优先级分组的配置值。具体步骤如下:
(SCB->AIRCR & SCB_AIRCR_PRIGROUP_Msk):使用位与操作(&)将SCB_AIRCR寄存器的值与SCB_AIRCR_PRIGROUP_Msk进行按位与操作,以屏蔽除了优先级分组位以外的其他位。
SCB_AIRCR_PRIGROUP_Msk是一个掩码,用于提取优先级分组位。
>> SCB_AIRCR_PRIGROUP_Pos:使用右移操作(>>)将结果向右移动到正确的位置,以便得到最终的优先级分组配置值。
return ((uint32_t)…):返回优先级分组配置值。

NVIC_SetPriority()

同样的,这里也套了一层宏定义

#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);
  }
}
  • (int32_t)(IRQn) >= 0:判断中断号是否大于等于0,即判断中断号是否为设备特定中断。如果中断号为设备特定中断,则优先级设置在 NVIC->IP 寄存器中;否则,优先级设置在 SCB->SHP 寄存器中。

  • NVIC->IP[((uint32_t)IRQn)]:如果中断号为设备特定中断,将优先级值按位左移 (8U - __NVIC_PRIO_BITS) 个位置,并与掩码 (uint32_t)0xFFUL 进行按位与操作,以确保优先级值在8位范围内。然后将结果存储在 NVIC->IP 寄存器的相应位置上。

  • SCB->SHP[(((uint32_t)IRQn) & 0xFUL) - 4UL]:如果中断号不是设备特定中断,首先通过 (((uint32_t)IRQn) & 0xFUL) - 4UL 来计算在 SCB->SHP 寄存器中的位置。然后将优先级值按位左移 (8U - __NVIC_PRIO_BITS) 个位置,并与掩码 (uint32_t)0xFFUL 进行按位与操作,以确保优先级值在8位范围内。最后将结果存储在 SCB->SHP 寄存器的相应位置上。

NVIC_EncodePriority()

_STATIC_INLINE uint32_t NVIC_EncodePriority (uint32_t PriorityGroup, uint32_t PreemptPriority, uint32_t SubPriority)
{
  uint32_t PriorityGroupTmp = (PriorityGroup & (uint32_t)0x07UL);   /* only values 0..7 are used          */
  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)))
         );
}

此函数用于编码中断优先级。

该函数接受三个参数:PriorityGroupPreemptPrioritySubPriority,用于指定中断的优先级信息。

函数主要实现了将优先级信息编码为一个32位的值,以便在设置中断优先级时使用。

具体步骤如下:

  1. PriorityGroupTmp = (PriorityGroup & (uint32_t)0x07UL):提取 PriorityGroup 参数的低3位,即优先级分组的值(0-7)。

  2. PreemptPriorityBits 的计算:根据优先级分组的值,计算可用于抢占优先级的位数。如果 (7UL - PriorityGroupTmp) 大于 __NVIC_PRIO_BITS(定义的宏),则将 PreemptPriorityBits 设置为 __NVIC_PRIO_BITS;否则,设置为 (7UL - PriorityGroupTmp)

  3. SubPriorityBits 的计算:根据优先级分组的值,计算可用于子优先级的位数。如果 (PriorityGroupTmp + (uint32_t)(__NVIC_PRIO_BITS)) 小于 7UL,则将 SubPriorityBits 设置为 0UL;否则,设置为 (PriorityGroupTmp - 7UL) + (uint32_t)(__NVIC_PRIO_BITS)

  4. 返回编码后的优先级值,通过以下操作完成:

    • ((PreemptPriority & (uint32_t)((1UL << (PreemptPriorityBits)) - 1UL)) << SubPriorityBits):将抢占优先级按位与上适当的掩码,并将结果左移 SubPriorityBits 位。
    • ((SubPriority & (uint32_t)((1UL << (SubPriorityBits)) - 1UL))):将子优先级按位与上适当的掩码。

该函数的目的是根据优先级分组的配置,将用户提供的抢占优先级和子优先级编码为一个32位的值,以便在设置中断优先级时可以直接使用。具体的掩码和位操作根据优先级分组的值进行计算,以确保编码后的优先级值在正确的位范围内。