STM32 CubeMX学习:6. 按键的外部中断
系列文章目录文章目录
0 前言
这次的博客。我们将了解按键的硬件原理,学习如何使用STM32的外部中断功能读取按键输入,如何使用软件消抖来消除按键输入产生的电压的抖动。此外,我们还将再一次借助按键中断的例子理解通过中断实现程序前后台的思想。So,我们开始吧
1 基础知识
1.1 按键原理图
在探索者 STM32F4开发板上的按键 KEY0 连接在 PE4 上、KEY1 连接在 PE3 上、KEY2 连接在 PE2 上、KEY_UP连接在 PA0 上。如图所示:
在这块开发板上,KEY0、KEY1 和 KEY2 是低电平有效的,而 KEY_UP 是高电平有效的,并且外部都没有上下拉电阻,所以,我们需要在 STM32F4 内部设置上下拉。
1.2 按键软件消抖
考虑到真实的情况,由于按键的机械结构具有弹性,按下时开关不会立刻接通,断开时也不会立刻断开,这就导致按键的输入信号在按下和断开时都会存在抖动,如果不先将抖动问题进行处理,则读取的按键信号可能会出现错误。
为了消除这一问题,我们可以通过软件消抖或者硬件消抖两种方式来实现,我们在这里主要采用软件滤波的实现方法。软件滤波的思想其实非常简单,大家很容易就明白了。抖动的产生在按键按下和松开的两个边沿时刻,也叫下降沿(电平从高到低)和上升沿(电平从低到高)时刻,所以我们只需要在边沿时进行延时,等到按键输入已经稳定再进行信号读取即可,是不是很简单呢?
一般采用软件消抖时,会进行20ms的延时,示波器采集按键波形如图所示。
1.3 外部中断
单片机的外部中断通常是由GPIO的电平跳变引起的中断。在STM32中,每一个GPIO都可以作为外部中断的触发源,外部中断一共有16条线,对应着GPIO的0-15引脚,每一条外部中断都可以与任意一组的对应引脚相连,但不能重复使用。
例如,外部中断Line0可以和PA0,PB0,PC0等任意一条0号引脚相连,但如果已经和PA0相连,就不能同时和PB0,PC0其他引脚相连。大家在规划中断的时候要格外小心呦。
外部中断支持GPIO的三种电平跳变的模式,如下所示:
- 上升沿中断:当GPIO的电平从低电平跳变成高电平时,引发外部中断。
- 下降沿中断:当GPIO的电平从高电平跳变成低电平时,引发外部中断。
- 上升沿和下降沿中断:当GPIO的电平从低电平跳变成高电平和从高电平跳变成低电平时,都能引发外部中断。
2 程序的学习
2.1 按键的外部中断在CubeMX里的配置
STM32的GPIO提供外部中断功能,当GPIO检测到电压跳变时,就会发出中断触发信号给STM32,使程序进入外部中断服务函数。
(今天的程序,我们可以在LED灯的基础之上完成)
-
将PA0号引脚设置为按键的输入引脚,将其设置为外部中断模式。
-
接着点开GPIO标签页,对引脚进行如下设置,将GPIO模式设置为升降沿触发的外部中断,上下拉电阻设置为下拉电阻,最后设置用户标签为WK_UP。
在NVIC标签页下,可以看到外部中断已经开启。
- 点击“GENERATE CODE”生成代码
2.2 HAL_GPIO_ReadPin函数介绍
HAL库提供了读取引脚上的电平的函数HAL_GPIO_ReadPin。该函数说明如下:
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
此函数的返回参数为GPIO_PinState,如果是高电平则返回GPIO_PIN_SET(对应为1),如果是低电平则返回GPIO_PIN_RESET(对应为0);
该函数的作用在于返回引脚电平;
该函数具有两个参数:
(1)GPIOx——对应GPIO总线,其中x可以是A…I。例如,PH10,则输入GPIOH
(2)GPIO_Pin——对应引脚数。可以是0-15。例如,PH10,则输入GPIO_PIN_10
2.3 中断回调函数介绍
每当产生外部中断时,程序首先会进入外部中断服务函数。在stm32f4xx_it.c中,可以找到函数EXTI0_IRQHandler,它通过调用函数HAL_GPIO_EXTI_IRQHandler对中断类型进行判断,并对涉及中断的寄存器进行处理,在处理完成后,它将调用中断回调函数HAL_GPIO_EXTI_Callback,在中断回调函数中编写在此次中断中需要执行的功能。
2.4 程序中的前后台
在本次实验中,发现主循环和中断回调函数中都有代码。这是一个非常典型的以前后台模式组织的工程。但是,什么是前后台模式呢?我们可以想象一下一个餐厅的运作模式,餐厅往往分为前台的叫餐员和后台的大厨,前台只有在来了客人,或者后台做好了一道菜时才会工作,而后厨则一直在忙着做菜,只有前台来了新的单子或者已经有菜做好了才会停下一会手中的活。
在单片机中,中断就是前台,而循环就是后台,中断只在中断源产生时才会进行相应的处理,而循环则一直保持工作,只有被中断打断时才会暂停。前后台程序的异同可以参见下表:
前台程序 | 后台程序 | |
---|---|---|
运行方式 | 中断 | 循环 |
处理的任务类型 | 突发型任务 | 重复型任务 |
任务的特点 | 任务轻,要求响应及时 | 任务重,稳定执行 |
编写前后台程序时,需要注意尽量避免在前台程序中执行过长或者过于耗时的代码,让前台程序能够尽快执行完毕,以保证其能够实时响应突发的事件,比较繁杂和耗时的任务一般放在后台程序中处理。
前后台模式可以帮助我们提高单片机的时间利用率,从而组织起比较复杂的工程。
2.5 程序流程
我们今天的程序中前后台任务各自承担的任务为:
- 前台程序——记录按键翻转的状态rising_falling_flag
- 后台程序——执行处理工作,根据记录的翻转状态进行按键状态的判断
在主循环中,首先通过边沿检测标志 rising_falling_flag 来判断按键是处于按下还是松开的边沿,如果是下降的边沿(rising_falling_flag == GPIO_PIN_RESET)则将LED灯熄灭,如果是如果是上升的边沿(rising_falling_flag == GPIO_PIN_SET)则将LED灯点亮。为了防止误触发,通过边沿检测的判断之后,程序还会再对电平进行一次读取,确认下降沿后跟随的是低电平或者上升沿后跟随的是高电平,如果不是则不切换LED状态。
在中断回调函数中,利用HAL_GPIO_ReadPin对rising_falling_flag进行赋值,从而判断触发中断的是上升沿还是下降沿。
使用exit_flag来实现主循环和中断回调函数之间的互斥,保证中断处理函数中的功能(判断上升/下降沿)只在主循环完成判断之后进行,或者主循环的判断只在中断处理函数运行(即检测到了一次上升沿或者下降沿)之后再进行。
最终main.c文件如下所示
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* <h2><center>© Copyright (c) 2021 STMicroelectronics.
* All rights reserved.</center></h2>
*
* This software component is licensed by ST under BSD 3-Clause license,
* the "License"; You may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
* opensource.org/licenses/BSD-3-Clause
*
******************************************************************************
*/
/* 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 */
//backgroud program
//后台程序
uint8_t exit_flag = 0;
uint8_t rising_falling_flag;
/**
* @brief exit callback function
* @param[in] GPIO_Pin:gpio pin
* @retval none
*/
/**
* @brief 外部中断回调
* @param[in] GPIO_Pin:引脚号
* @retval none
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == WK_UP_Pin)
{
if(exit_flag == 0)
{
exit_flag = 1;
rising_falling_flag = HAL_GPIO_ReadPin(WK_UP_GPIO_Port, WK_UP_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 */
HAL_GPIO_WritePin( LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET );
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
//foreground program
//前台程序
if(exit_flag == 1)
{
exit_flag = 2;
if(rising_falling_flag == GPIO_PIN_RESET)
{
//debouce
//消抖
HAL_Delay(20);
if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port, WK_UP_Pin) == GPIO_PIN_RESET)
{
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
exit_flag = 0;
}
else
{
exit_flag = 0;
}
}
else if(rising_falling_flag == GPIO_PIN_SET)
{
//debouce
//消抖
HAL_Delay(20);
if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port, WK_UP_Pin) == GPIO_PIN_SET)
{
HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET);
exit_flag = 0;
}
else
{
exit_flag = 0;
}
}
}
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the CPU, AHB and APB busses clocks
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 6;
RCC_OscInitStruct.PLL.PLLN = 168;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB busses 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_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
Error_Handler();
}
}
/* 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 */
/* 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,
tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
3 运行效果
一开始LED0为点亮状态,当按键按下时,红灯熄灭;当按键松开时,红灯点亮
代码我已经放到了我的GitHub仓库,如有需要可以下载使用:
CubeMX学习