> For the complete documentation index, see [llms.txt](https://songyibiao.gitbook.io/design-self-balancing-robot/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://songyibiao.gitbook.io/design-self-balancing-robot/ruan-jian-kai-fa-pian/a22.md).

# Timer与按键消抖的应用

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

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

## 预先了解

![](/files/-M67lclBsrsWOXNB3K8c)

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

## 具体步骤

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

![](/files/-M67lclMglgoLxW0uXld)

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

![](/files/-M67lclRkgn9fxXAohU2)

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

![](/files/-M67lclSvuq5B9YWXx3s)

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

![](/files/-M67lclYfBnzM7nFTQxI)

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

## 延时消抖

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

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

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

```c
 //按键处理函数 
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 进行了配置。

![](/files/-M67lclc4GixvbTrfdjL)

在 STM32CubeMX 中，默认 SysTick 中断为 1ms。 我们可以打开 MDK-ARM 工程，在左侧 Drivers/STM32F1xx\_HAL\_Driver 文件夹的 stm32f1xx\_hal.c 文件中找到 SysTick 的初始化。

![](/files/-M67lclit7mZJKztS4W-)

```c
/**
  * @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 中断服务函数。

![](/files/-M67lcllJscJx3UNRMSV)

```c
/**
  * @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() 函数，我们右键转跳到该函数可以看到该函数的定义。

![](/files/-M67lclntWc6YuCxZzc-)

```c
/**
  * @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() 函数。

![](/files/-M67lclpmmaMT3OhNSoO)

```c
/**
  * @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 加进来。

![](/files/-M67lclt72NpA7Wjfgt7)

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

```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;                   //清除重按键标志
    }
}
```

![](/files/-M67lclvRoQzqcwoeZel)

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

```c
#ifndef __BUTTON_H
#define __BUTTON_H

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

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

#endif
```

![](/files/-M67lclxa2Y-j8YgdsuV)

打开 stm32fxx\_it.c 文件，在 `/* USER CODE BEGIN Includes */` 与 `/* USER CODE END Includes */` 之间加入 #include "button.h"。

![](/files/-M67lclyvw0KuIqX-J5c)

在 stm32fxx\_it.c 文件里面 SysTick\_Handler() 函数里加入按键扫描函数 ButtonScan()。

![](/files/-M67lcm-WFyO3hsSaO8u)

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

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

![](/files/-M67lcm1HyK4iF0FOWqK)

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

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

![](/files/-M67lcm3UfM3i9j9_nNH)

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

![](/files/-M67lcm5pJblo6DwS9yE)

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