1.开发环境
- 主控:STM32F401RCT6开发板
- 预期功能:使用板载按键,控制板载LED亮灭
外部中断的详细知识参考:
2.外部中断基础知识
STM32的每个GPIO都可以配置为外部中断。
中断触发电平变化有三种:下降沿、上升沿、高低电平
- 下降沿触发:高电平变为低电平
- 上升沿触发:低电平变为高电平
- 高低电平触发:按下时触发一次,松开时再次触发中断。
触发方式两种:中断触发、事件触发
外部中断引脚的标志位:
3.STM32CubeMX配置
配置板载按键PA0为外部中断输入,下降沿触发中断。
配置板载LED,PC13
4.编写业务代码
中断回调函数示意图:
当产生外部中断事件时,程序会跳转到中断处理函数:
void EXTI0_IRQHandler(void)
{
/* USER CODE BEGIN EXTI0_IRQn 0 */
/* USER CODE END EXTI0_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(KEY_user_Pin);
/* USER CODE BEGIN EXTI0_IRQn 1 */
/* USER CODE END EXTI0_IRQn 1 */
}
但是中断处理函数中只简单标注了中断来源,然后退出中断,回到主函数。
回到主函数后,并不是继续执行之前中止的程序,而是自动调用中断回调函数,中断事件的处理都放在回调函数里。
可以在stm32f4xx_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)
{
/* EXTI line interrupt detected */
if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != RESET)
{
__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)
{
/* 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
*/
}
- 中断处理函数:清除中断标志位,调用中断回调函数。
- 中断回调弱函数:弱函数可以重新进行定义,在里面写中断处理的业务代码。
接下来我们就在 stm32f4xx_it.c
这个文件的最下面重定义中断回调函数HAL_GPIO_EXTI_Callback()
。
在中断回调函数里,不可以使用系统自带的延时函数HAL_Delay(),可以使用自己写的延时函数。
程序已验证可用!!!
在中断里使用HAL_Delay()
延时卡死的原因:因为延时利用MCU中的滴答定时器中断来计时,按键的中断优先级比滴答定时器的优先级高,导致外部中断使用延时函数时,滴答定时器的中断无法执行,最终卡死在延时函数中。
解决方法1:可以给外部中断的优先级降级!
中断优先级设置的越小,优先级越高!修改中断优先级如下所示:
以下为不更改优先级的示例程序,已测试可以正常使用。
// 自定义延时函数
void delay_us(uint32_t us)
{
uint32_t delay = (HAL_RCC_GetHCLKFreq() / 25000000 * us); // 数值与选用的晶振有关,25MHz无源晶振
while (delay--)
; // 循环delay次,达到1us延时
}
// 按键检测函数
uint8_t KEY_1(void)
{
uint8_t a = 0; // 按键未按下
if (HAL_GPIO_ReadPin(KEY_user_GPIO_Port, KEY_user_Pin) == GPIO_PIN_RESET)
{
delay_us(20000); // 延时消抖
// 再次检测按键是否按下
if (HAL_GPIO_ReadPin(KEY_user_GPIO_Port, KEY_user_Pin) == GPIO_PIN_RESET)
{
a = 1;
}
}
while (HAL_GPIO_ReadPin(KEY_user_GPIO_Port, KEY_user_Pin) == GPIO_PIN_RESET)
; // 等待按键松开
delay_us(20000); // 延时消抖,避免按键松开时的抖动
return a;
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
UNUSED(GPIO_Pin); // 防报错的定义,当传进来的GPIO端口号没有做任何处理的时候,编译器也不会报出警告。
if (GPIO_Pin == KEY_user_Pin) // 判断产生中断的端口
{
HAL_NVIC_DisableIRQ(EXTI0_IRQn); // 中断号,按键处理期间禁止再次触发中断,起到按键消抖的作用
if (KEY_1())
{
HAL_GPIO_TogglePin(LED_user_GPIO_Port, LED_user_Pin);
}
HAL_NVIC_EnableIRQ(EXTI0_IRQn); // 按键处理完成后,开启中断允许
}
}
补充:关于Time base: System tick timer的优先级设置问题
系统计时器,这个不需要很高的优先级,一般设置为最低就行。
注意:如果这个中断优先级与外部中断的优先级一样或更低,当在外部中断函数中使用延时函数,延时函数会被卡死。
20231224补充:使用一个全局变量,代替按键扫描
功能:按下按键点亮LED,再次按下按键熄灭LED
uint8_t flag = 1;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
UNUSED(GPIO_Pin);
if (GPIO_Pin == KEY_user_Pin)
{
HAL_NVIC_DisableIRQ(EXTI1_IRQn); // 在处理按钮时禁用中断
// 切换 flag 变量
flag = !flag;
if (flag == 1)
{
HAL_GPIO_WritePin(LED_user_GPIO_Port, LED_user_Pin, GPIO_PIN_SET); // Turn on LED
}
else
{
HAL_GPIO_WritePin(LED_user_GPIO_Port, LED_user_Pin, GPIO_PIN_RESET); // Turn off LED
}
HAL_NVIC_EnableIRQ(EXTI1_IRQn); // 处理按钮后启用中断
}
}