最近在做的公司给我练手的小项目,是在MCU上做一个管理系统,其中需要用按键的长短按来控制开关机。最初第一个方案是使用延时,可是很快就发现弊端,这样的长按时间很不稳定,经常出差错,所以很快就被我否定了,项目再简单也是项目毕竟不是学校里面的课设。所以我决定用TIM定时器中断来判断开关状态以此来完成长按短按功能。
一、标志位法
通过一些资料的搜集,发现使用标志位法,首先完成TIM定时器的初始化,我这里使用的是CUBE。
首先打开TIM定时器并且初始化好,以及TIM定时器更新中断:
配置好分频系数以及计数器周期。
![](https://img-blog.csdnimg.cn/img_convert/6f4e015d63d2084a63882b92e0c0cc77.png)
打开定时器更新中断。
![](https://img-blog.csdnimg.cn/img_convert/5e733d677430bb56a0bc3c8f61041f5c.png)
这样关于定时器的初始化就完成了,其他的外设大家可以根据需要配置。
最终得到初始化好的代码:
#include "tim.h"
TIM_HandleTypeDef htim1;
/* TIM1 init function */
void MX_TIM1_Init(void)
{
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim1.Instance = TIM1;
htim1.Init.Prescaler = 800-1;
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
htim1.Init.Period = 100-1;
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim1.Init.RepetitionCounter = 0;
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim1) != HAL_OK)
{
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&htim1, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN TIM1_Init 2 */
HAL_TIM_Base_Start_IT(&htim1); //使能定时器 1和定时器 1 更新中断
/* USER CODE END TIM1_Init 2 */
}
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
{
if(tim_baseHandle->Instance==TIM1)
{
__HAL_RCC_TIM1_CLK_ENABLE();
HAL_NVIC_SetPriority(TIM1_UP_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM1_UP_IRQn);
}
}
void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* tim_baseHandle)
{
if(tim_baseHandle->Instance==TIM1)
{
__HAL_RCC_TIM1_CLK_DISABLE();
HAL_NVIC_DisableIRQ(TIM1_UP_IRQn);
}
}
其中,第三十五行代码是用来使能定时器以及定时器更新中断的,这个需要手动添加,不然定时器和中断不会工作:
HAL_TIM_Base_Start_IT(&htim1);
定时器配置好了,那么我们来算算多久会激活一次定时器更新中断,上述代码可见我的分频系数是800,周期是100。
定时器的时钟为 8Mhz,分频系数为 800,所以分频后的计数频8Mhz/800=10KHz,然后计数到 100,所以时长为 100/10000=0.01s,也就是 10ms。所以说每1ms都会触发一次定时器更新中断。所以我们在中断回调函数里面判断开关状态:
1、首先设置按键标志位(为了方便看清楚,提供一个思路,不然一组标志位就够了):
int iButtonCount; //ButtonCount表示按键计数变量
int iButtonFlag; //ButtonFlag表示重按键标志,1代表重新按键_0为没有重新按键
int g_iButtonState; //g是globle代表全局变量,会在其他地方引用;ButtonState表示按键标志_1代表按下_0代表松开
int longcount;
int longflag;
int longstate;
2、编写中断回调函数(两组标志位完全没卵用,只是分开给人看着更清晰一些):
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)//按键中断回调
{
if(htim==(&htim1))
{
if( HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_3) == GPIO_PIN_RESET )//如果弿关丿引脚棿测到为使
{
longcount++;
iButtonCount++; //按键按下,计数iButtonCount加一
if(iButtonCount>=30) //1ms中断服务函数里运行一次,iButtonCount大于等于10,即按键已稳定按30ms
{
if(iButtonFlag==0) //判断有没有重按键
{
g_iButtonState=1; //设置按键标志
iButtonCount=0;
iButtonFlag=1; //设置重按键标志
}
else //如果重按键,则重新计数
iButtonCount=0;
}
else //如果没有稳定按下10ms,则代表没有按下按键
g_iButtonState=0;
if(longcount >= 300)//3s长按
{
if(longflag == 0)
{
longState = 1;
longcount = 0;
longflag = 1;
}
else
longcount = 0;//如果重按键,则重新计数
}
else
longState = 0;//如果没有稳定按下3s,则代表没有长按
}
else //如果一直没有检测到测到低电平,即一直无按键按下
{
iButtonCount=0; //清零iButtonCount
g_iButtonState=0; //清除按键标志
iButtonFlag=0; //清除重按键标志
longState = 0;
longcount = 0;
longflag = 0;
}
}
}
在主函数加入相关判断(通过led变化分辨):
if(g_iButtonState == 1)
{
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_5, GPIO_PIN_RESET);
g_iButtonState = 0;
}
if(longState == 1)
{
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_5, GPIO_PIN_SET);
longState = 0;
}
如此功能确实是实现了,但是问题也来了,这样长按的时候短按也触发了一次,尽管是两个标志位(其实一个两个都没插别,因为终究是一个计数器)。所以这样很明显不行,不够严谨。所以这个时候就要用到状态机,将长按和短按状态划分开来。
二、状态机
切换成状态机也就说明前面的回调可以说白写了,当然前面的工作也没有完全白做,起码定时器还是需要的。状态机我没写过不清楚原理,所以是抄了一位大佬的作业,可以去看看原文(名字是「Net_Walke)。原文链接:https://blog.csdn.net/qq_34142812/article/details/119721386
首先,初始化状态枚举,第一个枚举是按键状态枚举,三个成员分别代表,检测状态,按下状态、释放状态;第二个枚举则是代表按键值,分别为空、短按、长按。
#define Key HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)
typedef enum
{
KEY_CHECK = 0,
KEY_COMFIRM = 1,
KEY_RELEASE = 2
}KEY_STATE;
typedef enum
{
NULL_KEY = 0,
SHORT_KEY =1,
LONG_KEY
}KEY_TYPE;
KEY_STATE KeyState = KEY_CHECK; // 初始化按键状态为检测状态
u8 g_KeyFlag = 0; // 按键有效标志,0: 按键值无效; 1:按键值有效
KEY_TYPE g_KeyActionFlag; //用于区别长按和短按
按键可能没什么难的,但是还是会惊叹别人清晰的思路,对KEYstate进行判断:
void Key_Scan(void)
{
static uint32_t TimeCnt = 0;
static uint8_t lock = 0;
switch (KeyState)
{
//按键未按下状态,此时判断Key的值
case KEY_CHECK:
if(Key)
{
KeyState = KEY_COMFIRM; //如果按键Key值为1,说明按键开始按下,进入下一个状态
}
TimeCnt = 0; //计数复位
lock = 0;
break;
case KEY_COMFIRM:
if(Key) //查看当前Key是否还是1,再次确认是否按下
{
if(!lock)
{
lock = 1;
}
TimeCnt++;
/*按键时长判断*/
if(TimeCnt >= 300) // 长按 3 s
{
g_KeyActionFlag = LONG_KEY;
TimeCnt = 0;
lock = 0; //重新进入检查模式
KeyState = KEY_RELEASE; // 需要进入按键释放
}
}
else
{
if(lock==1) // 不是第一次进入,释放按键才执行
{
g_KeyActionFlag = SHORT_KEY; // 短按
KeyState = KEY_RELEASE; // 要进入按键释放状态
}
else // 当前Key值为1,确认为抖动,则返回上一个状态
{
KeyState = KEY_CHECK; // 返回上一个状态
}
}
break;
case KEY_RELEASE:
if(!Key) //当前Key值为1,说明按键已经释放,返回检测状态
{
KeyState = KEY_CHECK;
}
break;
default:
break;
}
}
//修改回调函数为:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)//定时器中断回调
{
if(htim == (&htim1))
{
Key_Scan();
}
}
最后在主函数,对回调函数返回的值进行判断:
switch(g_KeyActionFlag)
{
case SHORT_KEY:
printf("short time\r\n");
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_5, GPIO_PIN_RESET);
g_KeyActionFlag = 0; //回到空
break;
case LONG_KEY:
//printf("long time,standy now!!\r\n");
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_5, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_5, GPIO_PIN_SET);
g_KeyActionFlag = 0; //回到空
// Sys_Enter_Standby();
break;
default:
break;
}
这样对长按和短按有了完美划分,就不存在按键冲突问题,长按不松手是没办法进入短按的。状态之间一环扣一环,不会随随便便切换成其他状态。