Timer 与按键消抖的应用

本小节教你使用 STM32CubeMX 软件配置 MiaowLabs-STM32F1-Micro 核心板按键引脚的底层驱动,并使用 MDK-ARM 5.17 编写代码识别用户按键的单击动作,实现使用按键控制 LED 的亮灭功能。

在上一小节中,我们使用 STM32CubeMX 软件配置 MiaowLabs-STM32F1-Micro 核心板的底层驱动,并使用 MDK-ARM 5.17 编写代码点亮板子上的的蓝色 LED。在这一小节,我将继续教你实现使用按键控制 LED 的亮灭功能。

预先了解

MiaowLabs-STM32F1-Micro 核心板板上面有一颗用户按钮 是跟 STM32 的 PA11 引脚相连。我们可以通过编写代码识别按键的单击,当 PA11 引脚为低电平时,识别为按下按键;当 PA11 引脚为高电平时,识别为按下按键。

具体步骤

进入我们上一小节修改过的 MiaowLabs-Demo 文件夹,找到 MiaowLabs-Demo.ioc 工程文件,双击,打开工程。在 Pinout&Configuration 界面中的芯片中点击 PA11,并将其配置为 GPIO_Input。

在左侧的 System Core 下拉菜单中选择 GPIO,然后在 GPIO Mode and Configuration 中对 PA11 引脚进行配置,GPIO mode 代表 GPIO 引脚模式,在这里设置为输入模式;GPIO Pull-up/Pull-down 即 GPIO 上拉或下拉,根据硬件实际情况,在这里设置为上拉;User Label 即用户标签,因为 PA11 在这里的功能是识别按键,所以输入 Button 作为标签。

点击 GENERATE CODE,重新生成代码。

因为通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,为了不产生这种现象而作的措施就是按键消抖。

消抖是为了避免在按键按下或是抬起时电平剧烈抖动带来的影响。按键的消抖,可用硬件或软件两种方法,在实际的设计中,为了节省硬件成本,大多数情况会选择使用软件消抖。

延时消抖

软件消抖,即检测出按键闭合后执行一个延时程序,5ms~10ms 的延时,让前沿抖动消失后再一次检测键的状态,如果仍保持闭合状态电平,则确认为真正有键按下。当检测到按键释放后,也要给 5ms~10ms 的延时,待后沿抖动消失后才能转入该键的处理程序。

以前常用的消抖算法往往就是:判断按下->延时->再次判断是否按下->是,执行/否,退出。

最简单的消抖原理,就是当检测到按键状态变化后,先等待一个 10ms 左右的延时时间,让抖动消失后再进行一次按键状态检测,如果与刚才检测到的状态相同,就可以确认按键已经稳定的动作了。

 //按键处理函数 
void ButtonScan(void)
{     
    static int ButtonFlag = 0;//按键标志,1代表松开,0代表按下

    if(Button == 0)//第一次判断按键按下
    {
        delay_ms(10);//延时10ms,去抖动
        if(Button == 0)//第二次判断按键按下
        {
            ButtonFlag = 1;    //确认按键为按下,按键标志置1
        }        
}

利用定时器消抖

利用定时器消抖算法:通过 SysTick 中断每1ms对按键进行扫描,当检测到连续的稳定无抖动电平信号(长度可设置)之后,才进行相应的逻辑操作。

我们启用一个定时中断,每 1ms 进一次中断,扫描一次按键状态并且存储起来,连续扫描 25 次后,看看这连续 25 次的按键状态是否是一致的。25 次按键的时间是 25ms,这 25ms 内如果按键状态一直保持一致,那就可以确定现在按键处于稳定的阶段,而非处于抖动的阶段。

我们在上一小节中已经对 SYS 进行了配置。

在 STM32CubeMX 中,默认 SysTick 中断为 1ms。 我们可以打开 MDK-ARM 工程,在左侧 Drivers/STM32F1xx_HAL_Driver 文件夹的 stm32f1xx_hal.c 文件中找到 SysTick 的初始化。

/**
  * @brief This function configures the source of the time base.
  *        The time source is configured  to have 1ms time base with a dedicated
  *        Tick interrupt priority.
  * @note This function is called  automatically at the beginning of program after
  *       reset by HAL_Init() or at any time when clock is reconfigured  by HAL_RCC_ClockConfig().
  * @note In the default implementation, SysTick timer is the source of time base.
  *       It is used to generate interrupts at regular time intervals.
  *       Care must be taken if HAL_Delay() is called from a peripheral ISR process,
  *       The SysTick interrupt must have higher priority (numerically lower)
  *       than the peripheral interrupt. Otherwise the caller ISR process will be blocked.
  *       The function is declared as __weak  to be overwritten  in case of other
  *       implementation  in user file.
  * @param TickPriority Tick interrupt priority.
  * @retval HAL status
  */
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
  /* Configure the SysTick to have interrupt in 1ms time basis*/
  if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U)
  {
    return HAL_ERROR;
  }

  /* Configure the SysTick IRQ priority */
  if (TickPriority < (1UL << __NVIC_PRIO_BITS))
  {
    HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
    uwTickPrio = TickPriority;
  }
  else
  {
    return HAL_ERROR;
  }

  /* Return function status */
  return HAL_OK;
}

函数里面有一句注释: /* Configure the SysTick to have interrupt in 1ms time basis*/,翻译过来就是,将 SysTick 配置为 1ms 中断。也就是说,经过此函数,SysTick 定时器被初始化为 1ms 的时基单元,即每毫秒进入一次 Systick 中断。

我们可以在左侧 Application/User 文件夹的 stm32f1xx_it.c 文件中找到 SysTick 中断服务函数。

/**
  * @brief This function handles System tick timer.
  */
void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */

  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  /* USER CODE BEGIN SysTick_IRQn 1 */

  /* USER CODE END SysTick_IRQn 1 */
}

里面只有一个 HAL_IncTick() 函数,我们右键转跳到该函数可以看到该函数的定义。

/**
  * @brief This function is called to increment  a global variable "uwTick"
  *        used as application time base.
  * @note In the default implementation, this variable is incremented each 1ms
  *       in SysTick ISR.
  * @note This function is declared as __weak to be overwritten in case of other
  *      implementations in user file.
  * @retval None
  */
__weak void HAL_IncTick(void)
{
  uwTick += uwTickFreq;
}

每毫秒 uwTick 加一,用于计算 Systick 产生的节拍数。

在 HAL_IncTick 函数的下方有一个 HAL_GetTick(void) 函数,主要用于获取当前节拍,如果内核正常运行,则 uwTick 的值即为已经经过的 ms 数。

这里顺便提及一下 HAL 库的延时函数 HAL_Delay() 。在左侧 Drivers/STM32F1xx_HAL_Driver 文件夹的 stm32f1xx_hal.c 文件中找到 HAL_Delay() 函数。

/**
  * @brief This function provides minimum delay (in milliseconds) based
  *        on variable incremented.
  * @note In the default implementation , SysTick timer is the source of time base.
  *       It is used to generate interrupts at regular time intervals where uwTick
  *       is incremented.
  * @note This function is declared as __weak to be overwritten in case of other
  *       implementations in user file.
  * @param Delay specifies the delay time length, in milliseconds.
  * @retval None
  */
__weak void HAL_Delay(uint32_t Delay)
{
  uint32_t tickstart = HAL_GetTick();
  uint32_t wait = Delay;

  /* Add a freq to guarantee minimum wait */
  if (wait < HAL_MAX_DELAY)
  {
    wait += (uint32_t)(uwTickFreq);
  }

  while ((HAL_GetTick() - tickstart) < wait)
  {
  }
}

可以看到 HAL_Delay() 函数其实就是通过 HAL_GetTick() 函数数节拍以确定精准的延时。

回到按键消抖的主题,既然 STM32CubeMX 已经默认设置 Systick 为 1ms 中断,那么我们可以直接利用起来。打开 MDK-ARM 工程,按下组合键 Ctrl+N(按住 Ctrl 键再按 N 键),新建一个文件,再按下组合键 Ctrl+S,文件名改为 button.c,保存到 MiaowLabs-DEMO 的 Src 文件夹里。来到这里,我们虽然新建了button.c 源文件,但是还没有把该文件加入到 MDK-ARM 工程里。我们在 MDK-ARM 工程界面左侧 Project 栏目双击 Application/User 文件夹,把 button.c 加进来。

双击 button.c 文件,把下面代码敲进去。

#include "button.h"
#include "main.h"
#include "stm32f1xx_it.h"

int iButtonCount;//i代表int型变量,ButtonCount表示按键计数变量
int iButtonFlag;//i代表int型变量,ButtonFlag表示重按键标志,1代表重新按键,0为没有重新按键
int g_iButtonState;//g是globle代表全局变量,会在其他地方引用;i代表int型变量,ButtonState表示按键标志,1代表按下,0代表松开
void ButtonScan(void){
  if(   HAL_GPIO_ReadPin(Button_GPIO_Port,Button_Pin) == GPIO_PIN_RESET )//如果引脚检测到低电平
  {
      iButtonCount++;                         //按键按下,计数iButtonCount加1
      if(iButtonCount>=30)                    //1ms中断服务函数里运行一次,iButtonCount大于等于30,即按键已稳定按下30ms
         {
            if(iButtonFlag==0)                  //判断有没有重按键,1为有,0为没有
                {
                    g_iButtonState=1;                 //设置按键标志
                    iButtonCount=0;
                    iButtonFlag=1;                  //设置重按键标志
                    }
                else                              //如果重按键,则重新计数
                    iButtonCount=0;
                }
    else                                  //如果没有稳定按下30ms,则代表没有按下按键
         g_iButtonState=0;

         }
else                                      //如果一直无检测到低电平,即一直无按键按下
    {
         iButtonCount=0;                  //清零iButtonCount
         g_iButtonState=0;                  //清除按键标志
         iButtonFlag=0;                   //清除重按键标志
    }
}

再新建一个文件 button.h 头文件,把文件保存到 Inc 文件夹。然后,把下面代码敲进去。

#ifndef __BUTTON_H
#define __BUTTON_H

extern int g_iButtonState;//声明外部变量,方便其他地方引用

void ButtonScan(void);//声明按键扫描函数

#endif

打开 stm32fxx_it.c 文件,在 /* USER CODE BEGIN Includes *//* USER CODE END Includes */ 之间加入 #include "button.h"。

在 stm32fxx_it.c 文件里面 SysTick_Handler() 函数里加入按键扫描函数 ButtonScan()。

在左侧 Application/User 文件夹里打开 main.c 函数,在主循环里面加入以下代码:

        if(g_iButtonState == 1){            
          HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);//翻转LED引脚(PB12)的电平            
        }

这段代码的意思就是:如果按键状态标志为 1 ,则代表按键被按下了,这时候进行 LED 电平翻转。

在左侧 Application/User 文件夹里点开 main.c 函数左侧的加号,找到 main.h 头文件,并把#include "button.h"头文件加进去。

代码已经编写好了。这时候,点击编译按钮,会提示没有错误和警告。

把代码烧录进 MiaowLabs-STM32F1-Micro 核心板,然后按下用户按键,可以发现按一下 LED 会亮,再按一下 LED 就会灭掉。就这样,我们实现了通过按键控制 LED 亮灭的功能。