飞书文档https://x509p6c8to.feishu.cn/wiki/ENFswNTSGiblehkIMtfc9dYinqh
创建工程
按工程创建章节步骤,把工程创建好,并配置外部时钟源,SWD调试模式,时钟72MHz。
设置引脚模式
找到需要配置的按键,例如PC2,将KEY所在的引脚PC2配置为GPIO_EXIT模式。因为在第2脚,所以是EXIT2,又叫做中断线2。

EXIT模式 = external interrupt trigger mode = 外部中断触发模式
EXTI2中的2指的是中断线2,STM32有20个EXTI线,可以产生不同的中断事件


外部中断控制器框图

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 图中的20代表在控制器内部类似的信号线路有 20 个,这与 EXTI 总共有 20 个中断/事件线是吻合的。 EXTI可分为两大部分功能,一个是产生中断,另一个是产生事件 中断:触发中断后,会自动跳到相应的中断服务函数中进行相应的处理。 事件:触发事件后,产生一个脉冲信号,这个脉冲信号可以给其他外设电路使用 比如定时器 TIM、ADC等等,这个脉冲信号一般用来触发 TIM 或者 ADC开始转换。 中断 1、输入线:外部中断Line2的输入线部分,我们配置为PC2 2、边沿检测电路:输入线产生电平变化后,通过上升、下降沿寄存器判断是否触发信号 3、或门:外部中断和软件中断都可以触发,例如PC2外部中断,TIM定时器软件中断,二选一 4、请求挂起寄存器:存储中断请求的状态,以便处理器能够管理中断的优先级和响应顺序。 5、与门:可以通过中断屏蔽寄存器来打开或关闭对应中断 6、最后发送到NVIC中断控制器,执行对应中断服务函数 事件: 1-3与中断步骤一样 4、与门:可以通过事件屏蔽寄存器来打开或关闭对应事件 5、脉冲发生器:当与门输出为1时产生一个脉冲,发送到其它外设电路 以下是挂起寄存器的主要功能: 中断请求存储 :当外部中断线(如GPIO引脚)检测到一个触发事件(例如,上升沿、下降沿或电平变化),相应的中断请求标志会被置位。这个标志保存在EXTI挂起寄存器中,表示中断请求已发生但尚未处理。 优先级管理 :如果中断控制器中有多个中断同时请求,挂起寄存器允许处理器根据中断优先级来决定先处理哪一个。中断系统通常有优先级分层,挂起寄存器确保低优先级的中断在高优先级的中断处理完毕后才被服务。 非阻塞操作 :挂起寄存器的机制使得处理器可以继续执行其他任务,而不必立即响应中断。这有助于避免中断处理程序的嵌套过深,以及提高系统的实时性。 中断清除 :一旦中断处理程序完成,对应的中断标志需要被清除,以防止重复处理同一个中断。通常,这可以通过读取并写回挂起寄存器(也称为读-清除操作)或使用专门的清除指令来完成。 状态查询:开发人员可以通过读取挂起寄存器来检查哪些中断源当前有未处理的请求,这对于调试和故障排查非常有用。 |
配置****GPIO
打开GPIO设置,选中外部中断引脚,这里是PC2。
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| GPIO mode External Interrupt Mode with Rising edge trigger detection 上升沿 触发外部中断 External Interrupt Mode with Falling edge trigger detection 下降沿 触发外部中断 External Interrupt Mode with Rising/Falling edge trigger detection 上升沿/下降沿 触发外部中断 External Event Mode with Rising edge trigger detection 上升沿 触发外部事件 External Event Mode with Falling edge trigger detection 下降沿 触发外部事件 External Event Mode with Rising/Falling edge trigger detection 上升沿/下降沿 触发外部事件 从原理图上看,PC2是经过按键接地,未按下按键时,PC2是高电平,如果按下按键,PC2电平会变低。 所以这里选择下降沿触发外部中断,也就是按下按键时,会触发中断。 GPIO Pull-up/Pull-down : 上下拉电阻 如果单片机系统中,按键没有设置外部的上拉或者下拉电阻,则需要在单片机中开启单片机内部的上拉或者下拉电阻。 如果有外部上拉,则不需要,选择浮空即可,这里选择浮空。 User Label: 用户标签 给引脚设置名称 |

开启外部中断
在NVIC(嵌套向量中断控制器)中,勾选EXIT Line2 interrupt使能PC2中断。

然后配置工程名称,输出MDK工程,并且设置只复制必要的库文件,每个模块初始化代码生成单独的.c/.h文件,最后生成工程。
打开工程后,我们可以看到以下文件生成了对应的代码,那如何添加中断触发后的执行代码呢?
"gpio.c"
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/*Configure GPIO pin : PtPin */
GPIO_InitStruct.Pin = KEY2_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(KEY2_GPIO_Port, &GPIO_InitStruct);
/* EXTI interrupt init*/
HAL_NVIC_SetPriority(EXTI2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI2_IRQn);
}
"stm32f1xx_it.c"
/**
* @brief This function handles EXTI line2 interrupt.
*/
void EXTI2_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_2);
}
"stm32f1xx_hal_gpio.c"
/**
* @brief This function handles EXTI interrupt request.
* @param GPIO_Pin: Specifies the pins connected EXTI line
* @retval None
*/
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
if (__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin);
}
}
/**
* @brief EXTI line detection callbacks.
* @param GPIO_Pin: Specifies the pins connected EXTI line
* @retval None
*/
__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
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
*/
}
函数名称前面加上__weak 修饰符,我们一般称这个函数为"弱函数"。加上了__weak 修饰符的函数,用户可以在用户文件中重新定义一个同名函数,最终编译器编译的时候,会选择用户定义的函数,如果用户没有重新定义这个函数,那么编译器就会执行__weak 声明的函数,并且编译器不会报错。所以我们可以在别的地方定义一个相同名字的函数,而不必也尽量不要修改之前的函数。
所以,我们可以在main.c中重写这个函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
if(GPIO_Pin == KEY2_Pin) //判断中断来自哪个IO
{
//do something
}
}
同理,我们可以在执行代码中,添加LED的翻转操作,例如,HAL_GPIO_TogglePin(xxx, xxx); 这样,就能更直观看到是否有中断产生了。

如果发现按下按键后,有时灯的状态会发生改变,有时又不会发生改变,这是正常现象,因为机械按键在断开与闭合时,在电路中会产生一些抖动,这些抖动可能会重复触发外部中断,看似只按了一次按键,然而触发了多次中断,致使LED发生了多次翻转。
此时可以通过去抖解决,去抖方式有很多
1、中断中直接加延时去抖,不允许,可能会其它中断被延迟或错过。
2、可以设置开启上升下降沿中断,计算上升沿与下降沿之间的时差,如果太短则滤除
3、中断触发后,由定时器中断计算并消抖
想要编写高效的代码,应该尽可能减少延时函数的使用。
/* USER CODE BEGIN 4 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
if(GPIO_Pin == KEY2_Pin){
//中断内如果一定要用HAL_Delay
//需要设置System tick中断的抢占优先级比改外部中断高,否则会出现卡死
HAL_Delay(10);
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == 0){
HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);
}
}
}
/* USER CODE END 4 */
/* USER CODE BEGIN 4 */
#define DEBOUNCE_TIME 50 // 去抖时间,单位为ms
uint32_t falling_tick = 0;
uint32_t rising_tick = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
if(GPIO_Pin == KEY2_Pin) //判断中断来自哪一条中断线
{
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == 0){
//触发中断后,如果读取到IO电平是低,则是下降沿
falling_tick = HAL_GetTick();
}else if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == 1 && falling_tick != 0){
//触发中断后,如果读取到IO电平是高,则是上升沿
rising_tick = HAL_GetTick();
if (rising_tick - falling_tick > DEBOUNCE_TIME)
{
// 按键按下需要执行的代码
HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);
}
falling_tick = rising_tick = 0;
}
}
}
/* USER CODE END 4 */
多个中断同时触发,会执行那个?
依赖中断优先级的设置,简单介绍一下NVIC(嵌套向量中断控制器)。NVIC就是控制中断响应的。主要由三个参数,一个是中断使能,一个是抢占优先级(主优先级),还有一个就是响应优先级(子优先级),优先级数值越小,优先级别越高。

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Enabled:中断使能 Preemption Priority:抢占优先级(主优先级) Sub Priority:响应优先级(子优先级) Priority Group :中断优先级分组-决定主优先级、子优先级可以设置的数值 Enabled: 中断使能 中断使能很好理解,就是是否开启中断,如果开启中断,则满足中断触发条件时程序会跳到中断服务程序运行,否则不响应中断主程序继续运行。 Preemption Priority: 抢占优先级(主优先级) 抢占优先级是用来判断一个中断是否可以打断另外一个中断的中断服务程序抢先运行。例如A中断触发,正在运行A中断的服务程序,此时B中断也触发,如果B中断的抢占优先级比A的高,则程序会打断A的中断服务程序,去运行B的中断服务程序,即中断嵌套。等B的中断服务程序运行完后继续运行A的中断服务程序。如果B的抢占优先级没有高过A的抢占优先级,则程序不会打断A的中断服务程序,而是待定A的中断服务程序运行完成后才运行B的中断服务程序。 Sub Priority: 响应优先级(子优先级) 子优先级是用来判断抢占优先级相同的几个中断那个中断会优先响应。如果几个抢占优先相同的中断同时触发,那么子优先级高的最先运行。 Priority Group : 中断优先级分组 STM32以4个比特位表示中断的主优先级和次优先级。分组是为了给主优先级和子优先级分配 各个优先级数量。 0位用来配置抢占优先级,4位用来配置响应优先级时。表示仅有1种级别的抢占优先级,有16种响应优先级。 1位用来配置抢占优先级,3位用来配置响应优先级时。表示有2种级别的抢占优先级,有8种响应优先级。 2位用来配置抢占优先级,2位用来配置响应优先级时。表示有4种级别的抢占优先级,有4种响应优先级。 3位用来配置抢占优先级,1位用来配置响应优先级时。表示有8种级别的抢占优先级,有2种响应优先级。 4位用来配置抢占优先级,0位用来配置响应优先级时。表示有16种级别的抢占优先级,仅有1种响应优先级。 |

子优先级和子优先级相同的情况下,根据中断向量表确定。中断向量表到单片机数据手册,或者Cubemx配置表中查看。