基于HAL库CubeMX配置的STM32电容触摸按键项目实战
本文以正点原子精英板子为例
触摸按键原理:触摸按键类似于一个电容,开发板内部也接了一个电容。利用定时器的输入捕获功能来捕获到这个电路上电容的充放电时间,当按下的时候相当于是两块电容,充电时间较长。最终就是通过比较充电的的时间来判断触摸按键有没有被按下。
一、CubeMX设置:
1. 还是老规矩,先打开sys的调试:
2.打开RCC时钟:
3.时钟设置为72MHZ:
4.因为这里使用了正带你原子的开发板,对应的捕获充电时间的GPIO口是PA1,这里对应的TIM5,channel2进行捕获,所以这里进行设置一下:
5.设置下面的参数,这里进行了6分频,使能重装载寄存器,下面2通道也设置为上升沿触发:
6.点击GPIO,设置一下TIM5,channel2对应的GPIO口(PA1)的参数,命名和设置为下拉模式:
7.打开PB5和PE5的输出模式:
8.将这两个GPIO口重新命名,以及分别设置为输出高电平(因为这里的开发板中,LED灯一段已经连接了高电平,所以GPIO输出低电平才会亮,所以这里初始状态要设置为高电平),推挽输出,上拉模式,高速模式:
之后保存生成代码。
二、代码部分:
1.新增代码
这里需要添加tpad.c, tpad.h,和正点原子的sys.c, sys.h 文件(tpad.c和tpad.h也是正点原子提供的的,不过我稍加了改动)。
(1). tpad.c:
#include "tpad.h"
#include "tim.h"
/* 空载的时候(没有手按下),计数器需要的时间
* 这个值应该在每次开机的时候被初始化一次
*/
volatile uint16_t g_tpad_default_val = 0; /* 空载的时候(没有手按下),计数器需要的时间 */
/* 静态函数, 仅限 tapd.c调用 */
static void tpad_reset(void); /* 复位 */
static uint16_t tpad_get_val(void); /* 得到定时器捕获值 */
static uint16_t tpad_get_maxval(uint8_t n); /* 读取n次, 获取最大值 */
/**
* @brief 初始化触摸按键
* @param psc : 分频系数(值越小, 越灵敏, 最小值为: 1)
* @retval 0, 初始化成功; 1, 初始化失败;
*/
uint8_t tpad_init(uint16_t psc)
{
uint16_t buf[10];
uint16_t temp;
uint8_t j, i;
MX_TIM5_Init();
for (i = 0; i < 10; i++) /* 连续读取10次 */
{
buf[i] = tpad_get_val();
HAL_Delay(10);
}
for (i = 0; i < 9; i++) /* 排序 */
{
for (j = i + 1; j < 10; j++)
{
if (buf[i] > buf[j])/* 升序排列 */
{
temp = buf[i];
buf[i] = buf[j];
buf[j] = temp;
}
}
}
temp = 0;
for (i = 2; i < 8; i++) /* 取中间的6个数据进行平均 */
{
temp += buf[i];
}
g_tpad_default_val = temp / 6;
if (g_tpad_default_val > TPAD_ARR_MAX_VAL / 2)
{
return 1; /* 初始化遇到超过TPAD_ARR_MAX_VAL/2的数值,不正常! */
}
return 0;
}
/**
* @brief 复位TPAD
* @note 我们将TPAD按键看做是一个电容, 当手指按下/不按下时容值有变化
* 该函数将GPIO设置成推挽输出, 然后输出0, 进行放电, 然后再设置
* GPIO为浮空输入, 等待外部大电阻慢慢充电
* @param 无
* @retval 无
*/
static void tpad_reset(void)
{
GPIO_InitTypeDef gpio_init_struct;
gpio_init_struct.Pin = TPAD_Pin; /* 输入捕获的GPIO口 */
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM; /* 中速 */
HAL_GPIO_Init(TPAD_GPIO_Port, &gpio_init_struct);
HAL_GPIO_WritePin(TPAD_GPIO_Port, TPAD_Pin, GPIO_PIN_RESET); /* TPAD引脚输出0, 放电 */
HAL_Delay(5);
htim5.Instance->SR = 0; /* 清除标记 */
htim5.Instance->CNT = 0; /* 归零 */
gpio_init_struct.Pin = TPAD_Pin; /* 输入捕获的GPIO口 */
gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_NOPULL; /* 浮空 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM; /* 中速 */
HAL_GPIO_Init(TPAD_GPIO_Port, &gpio_init_struct); /* TPAD引脚浮空输入 */
}
/**
* @brief 得到定时器捕获值
* @note 如果超时, 则直接返回定时器的计数值
* 我们定义超时时间为: TPAD_ARR_MAX_VAL - 500
* @param 无
* @retval 捕获值/计数值(超时的情况下返回)
*/
uint16_t tpad_get_val(void)
{
uint32_t flag = (TPAD_TIMX_CAP_CHY== TIM_CHANNEL_1)?TIM_FLAG_CC1:\
(TPAD_TIMX_CAP_CHY== TIM_CHANNEL_2)?TIM_FLAG_CC2:\
(TPAD_TIMX_CAP_CHY== TIM_CHANNEL_3)?TIM_FLAG_CC3:TIM_FLAG_CC4;
tpad_reset();
while (__HAL_TIM_GET_FLAG(&htim5 ,flag) == RESET) /* 等待通道CHY捕获上升沿 */
{
if (htim5.Instance->CNT > TPAD_ARR_MAX_VAL - 500)
{
return htim5.Instance->CNT; /* 超时了,直接返回CNT的值 */
}
}
return __HAL_TIM_GET_COMPARE(&htim5, TPAD_TIMX_CAP_CHY); /* 返回捕获/比较值 */
}
/**
* @brief 读取n次, 取最大值
* @param n :连续获取的次数
* @retval n次读数里面读到的最大读数值
*/
static uint16_t tpad_get_maxval(uint8_t n)
{
uint16_t temp = 0;
uint16_t maxval = 0;
while (n--)
{
temp = tpad_get_val(); /* 得到一次值 */
if (temp > maxval)maxval = temp;
}
return maxval;
}
/**
* @brief 扫描触摸按键
* @param mode :扫描模式
* @arg 0, 不支持连续触发(按下一次必须松开才能按下一次);
* @arg 1, 支持连续触发(可以一直按下)
* @retval 0, 没有按下; 1, 有按下;
*/
uint8_t tpad_scan(uint8_t mode)
{
static uint8_t keyen = 0; /* 0, 可以开始检测; >0, 还不能开始检测; */
uint8_t res = 0;
uint8_t sample = 3; /* 默认采样次数为3次 */
uint16_t rval;
if (mode)
{
sample = 6; /* 支持连按的时候,设置采样次数为6次 */
keyen = 0; /* 支持连按, 每次调用该函数都可以检测 */
}
rval = tpad_get_maxval(sample);
if (rval > (g_tpad_default_val + TPAD_GATE_VAL))/* 大于tpad_default_val+TPAD_GATE_VAL,有效 */
{
if (keyen == 0)
{
res = 1; /* keyen==0, 有效 */
}
keyen = 3; /* 至少要再过3次之后才能按键有效 */
}
if (keyen)
keyen--;
return res;
}
(2). tpad.h:
#ifndef TPAD_TPAD_H_
#define TPAD_TPAD_H_
#include "sys.h"
#define TPAD_TIMX_CAP_CHY TIM_CHANNEL_2 /* 通道Y, 1<= Y <=4 */
/******************************************************************************************/
/* 触摸的门限值, 也就是必须大于 g_tpad_default_val + TPAD_GATE_VAL
* 才认为是有效触摸, 改大 TPAD_GATE_VAL, 可以降低灵敏度, 反之, 则可以提高灵敏度
* 根据实际需求, 选择合适的 TPAD_GATE_VAL 即可
*/
#define TPAD_GATE_VAL 100 /* 触摸的门限值, 也就是必须大于 g_tpad_default_val + TPAD_GATE_VAL, 才认为是有效触摸 */
#define TPAD_ARR_MAX_VAL 0XFFFF /* 最大的ARR值, 一般设置为定时器的ARR最大值 */
extern volatile uint16_t g_tpad_default_val; /* 空载的时候(没有手按下),计数器需要的时间 */
/* 接口函数, 可以在其他.c调用 */
uint8_t tpad_init(uint16_t psc); /* TPAD 初始化 函数 */
uint8_t tpad_scan(uint8_t mode); /* TPAD 扫描 函数 */
#endif /* TPAD_TPAD_H_ */
(3). sys.c:
#include "sys.h"
/**
* @brief 设置中断向量表偏移地址
* @param baseaddr: 基址
* @param offset: 偏移量(必须是0, 或者0X100的倍数)
* @retval 无
*/
void sys_nvic_set_vector_table(uint32_t baseaddr, uint32_t offset)
{
/* 设置NVIC的向量表偏移寄存器,VTOR低9位保留,即[8:0]保留 */
SCB->VTOR = baseaddr | (offset & (uint32_t)0xFFFFFE00);
}
/**
* @brief 执行: WFI指令(执行完该指令进入低功耗状态, 等待中断唤醒)
* @param 无
* @retval 无
*/
void sys_wfi_set(void)
{
__ASM volatile("wfi");
}
/**
* @brief 关闭所有中断(但是不包括fault和NMI中断)
* @param 无
* @retval 无
*/
void sys_intx_disable(void)
{
__ASM volatile("cpsid i");
}
/**
* @brief 开启所有中断
* @param 无
* @retval 无
*/
void sys_intx_enable(void)
{
__ASM volatile("cpsie i");
}
/**
* @brief 设置栈顶地址
* @note 左侧的红X, 属于MDK误报, 实际是没问题的
* @param addr: 栈顶地址
* @retval 无
*/
void sys_msr_msp(uint32_t addr)
{
__set_MSP(addr); /* 设置栈顶地址 */
}
/**
* @brief 进入待机模式
* @param 无
* @retval 无
*/
void sys_standby(void)
{
__HAL_RCC_PWR_CLK_ENABLE(); /* 使能电源时钟 */
SET_BIT(PWR->CR, PWR_CR_PDDS); /* 进入待机模式 */
}
/**
* @brief 系统软复位
* @param 无
* @retval 无
*/
void sys_soft_reset(void)
{
NVIC_SystemReset();
}
/**
* @brief 系统时钟初始化函数
* @param plln: PLL倍频系数(PLL倍频), 取值范围: 2~16
中断向量表位置在启动时已经在SystemInit()中初始化
* @retval 无
*/
void sys_stm32_clock_init(uint32_t plln)
{
HAL_StatusTypeDef ret = HAL_ERROR;
RCC_OscInitTypeDef rcc_osc_init = {0};
RCC_ClkInitTypeDef rcc_clk_init = {0};
rcc_osc_init.OscillatorType = RCC_OSCILLATORTYPE_HSE; /* 选择要配置HSE */
rcc_osc_init.HSEState = RCC_HSE_ON; /* 打开HSE */
rcc_osc_init.HSEPredivValue = RCC_HSE_PREDIV_DIV1; /* HSE预分频系数 */
rcc_osc_init.PLL.PLLState = RCC_PLL_ON; /* 打开PLL */
rcc_osc_init.PLL.PLLSource = RCC_PLLSOURCE_HSE; /* PLL时钟源选择HSE */
rcc_osc_init.PLL.PLLMUL = plln; /* PLL倍频系数 */
ret = HAL_RCC_OscConfig(&rcc_osc_init); /* 初始化 */
if (ret != HAL_OK)
{
while (1); /* 时钟初始化失败,之后的程序将可能无法正常执行,可以在这里加入自己的处理 */
}
/* 选中PLL作为系统时钟源并且配置HCLK,PCLK1和PCLK2*/
rcc_clk_init.ClockType = (RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2);
rcc_clk_init.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; /* 设置系统时钟来自PLL */
rcc_clk_init.AHBCLKDivider = RCC_SYSCLK_DIV1; /* AHB分频系数为1 */
rcc_clk_init.APB1CLKDivider = RCC_HCLK_DIV2; /* APB1分频系数为2 */
rcc_clk_init.APB2CLKDivider = RCC_HCLK_DIV1; /* APB2分频系数为1 */
ret = HAL_RCC_ClockConfig(&rcc_clk_init, FLASH_LATENCY_2); /* 同时设置FLASH延时周期为2WS,也就是3个CPU周期。 */
if (ret != HAL_OK)
{
while (1); /* 时钟初始化失败,之后的程序将可能无法正常执行,可以在这里加入自己的处理 */
}
}
(4). sys.h:
#ifndef __SYS_H
#define __SYS_H
#include "stm32f1xx.h"
#include "main.h"
#include "stdio.h"
/**
* SYS_SUPPORT_OS用于定义系统文件夹是否支持OS
* 0,不支持OS
* 1,支持OS
*/
#define SYS_SUPPORT_OS 0
/*函数申明*******************************************************************************************/
void sys_nvic_set_vector_table(uint32_t baseaddr, uint32_t offset); /* 设置中断偏移量 */
void sys_standby(void); /* 进入待机模式 */
void sys_soft_reset(void); /* 系统软复位 */
uint8_t sys_clock_set(uint32_t plln); /* 时钟设置函数 */
void sys_stm32_clock_init(uint32_t plln); /* 系统时钟初始化函数 */
/* 以下为汇编函数 */
void sys_wfi_set(void); /* 执行WFI指令 */
void sys_intx_disable(void); /* 关闭所有中断 */
void sys_intx_enable(void); /* 开启所有中断 */
void sys_msr_msp(uint32_t addr); /* 设置栈顶地址 */
#endif
2. 生成代码的修改:
(1). tim.c(这里没什么改动,就是在 /* USER CODE BEGIN TIM5_Init 2 */这里加入了 HAL_TIM_IC_Start (&htim5, TIM_CHANNEL_2);):
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file tim.c
* @brief This file provides code for the configuration
* of the TIM instances.
******************************************************************************
* @attention
*
* Copyright (c) 2024 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "tim.h"
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
TIM_HandleTypeDef htim5;
/* TIM5 init function */
void MX_TIM5_Init(void)
{
/* USER CODE BEGIN TIM5_Init 0 */
/* USER CODE END TIM5_Init 0 */
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
TIM_IC_InitTypeDef sConfigIC = {0};
/* USER CODE BEGIN TIM5_Init 1 */
/* USER CODE END TIM5_Init 1 */
htim5.Instance = TIM5;
htim5.Init.Prescaler = 6-1;
htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
htim5.Init.Period = 65535;
htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim5.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_Base_Init(&htim5) != HAL_OK)
{
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&htim5, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIM_IC_Init(&htim5) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim5, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
sConfigIC.ICFilter = 0;
if (HAL_TIM_IC_ConfigChannel(&htim5, &sConfigIC, TIM_CHANNEL_2) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN TIM5_Init 2 */
HAL_TIM_IC_Start (&htim5, TIM_CHANNEL_2);
/* USER CODE END TIM5_Init 2 */
}
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(tim_baseHandle->Instance==TIM5)
{
/* USER CODE BEGIN TIM5_MspInit 0 */
/* USER CODE END TIM5_MspInit 0 */
/* TIM5 clock enable */
__HAL_RCC_TIM5_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/**TIM5 GPIO Configuration
PA1 ------> TIM5_CH2
*/
GPIO_InitStruct.Pin = TPAD_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
HAL_GPIO_Init(TPAD_GPIO_Port, &GPIO_InitStruct);
/* USER CODE BEGIN TIM5_MspInit 1 */
/* USER CODE END TIM5_MspInit 1 */
}
}
void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* tim_baseHandle)
{
if(tim_baseHandle->Instance==TIM5)
{
/* USER CODE BEGIN TIM5_MspDeInit 0 */
/* USER CODE END TIM5_MspDeInit 0 */
/* Peripheral clock disable */
__HAL_RCC_TIM5_CLK_DISABLE();
/**TIM5 GPIO Configuration
PA1 ------> TIM5_CH2
*/
HAL_GPIO_DeInit(TPAD_GPIO_Port, TPAD_Pin);
/* USER CODE BEGIN TIM5_MspDeInit 1 */
/* USER CODE END TIM5_MspDeInit 1 */
}
}
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
(2). 主函数main.c:
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2024 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "tim.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "tpad.h"
/* 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 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
uint8_t t = 0;
/* 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();
MX_TIM5_Init();
/* USER CODE BEGIN 2 */
tpad_init(6);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if (tpad_scan(0)) /* 成功捕获到了一次上升沿(此函数执行时间至少15ms) */
{
HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin); /* LED1翻转 */
}
t++;
if (t == 15)
{
t = 0;
HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin); /* LED0翻转 */
}
HAL_Delay (10);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses 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_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != 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 */
__disable_irq();
while (1)
{
}
/* 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,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
到这里就结束了,烧录进去就能发现,LED0会一直闪烁,LED1会在按下触摸按键之后变换状态。
作者:早睡早起(๑°3°๑)