之前也写过一篇利用定时器实现按键检测及消抖的文章,但是那篇代码量太大,而且不便于移植,还有一堆不明变量,简直就是屎山代码,这次使用结构体数组优化一下,原理不变
一、基本原理
这是一个按键从按下到松开的过程
传统用while死循环实现按键的松手检测,但是这样会使程序卡死,浪费CPU资源,在延时函数里只进行+1+1+1+1的操作,对程序运行的实时性很不友好
我们使用定时器每5ms扫描一次按键,当检测到按键被按下时,我们的按键计数器(LongPressCount)就开始计数,每5ms增加一次,当检测到按键被松开时停止计数,接着通过判断LongPressCount的值来判断按键按下的状态(没按下、短按和长按),当LongPressCount的值在0-4之间时就意味着按键在0-20ms之间就松开了,就判定为没有按键按下,计数器继续计数,当LongPressCount的值在4-200之间,意味着按键在20-1000ms之间松开,判定为短按,将按键的状态设置为短按并清除LongPressCount,那长按就是LongPressCount大于200了。
现在假如我们的按键没有抖动,我们要实现按键按下一次a++但是不许使用delay和while,最简单的逻辑是这样的(伪代码)
while (1)
{
if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_14) == 0)
{
a++;
}
}
但是问题也随之而来,当我按键按下的时候a会一直++,所以我们要定义一个变量Key_flag,默认为0,当按键按下后flag变为1,不再执行a++,逻辑是这样的
while (1)
{
if(Key_flag == 0)
{
if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_14) == 0)
{
a++;
Key_flag = 1;
}
}
}
这样的话第一次按下正常,第二次按下就没反应,因为按键松开的时候还要恢复标志位,当标志位还是被置位(flag==1)时,如果按键是松开的就清除标志位
while (1)
{
if(Key_flag == 0)
{
if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_14) == 0)
{
a++;
Key_flag = 1;
}
}
if(Key_flag == 1)
{
if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_14) == 1)
{
Key_flag = 0;
}
}
}
以上就是一个不使用while死循环实现松手检测的例子,记住这种思想,下面要用
二、程序设计
1、定时器中断
void Timer_Init(uint16_t Psc, uint16_t Per)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);
TIM_InternalClockConfig(TIM6);
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = Per - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = Psc - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM6, &TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM6, TIM_FLAG_Update);
TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);
/*中断配置*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM6_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM6, ENABLE);
}
Timer_Init(72, 1000); //先配置一个1ms的定时器中断
void TIM6_IRQHandler(void)
{
if (TIM_GetITStatus(TIM6, TIM_IT_Update) == SET)
{
static uint16_t count = 0;
count++;
/****************************************5ms***************************************/
if(count % 5 == 0)
{
}
/*****************************10ms任务********************************/
if(count % 10 == 0)
{
count = 0;
}
TIM_ClearITPendingBit(TIM6, TIM_IT_Update);
}
}
利用static 关键字,实现对定时器中断的充分利用
2、枚举类型
枚举是一种用户定义的类型,表示一组具有名称的整数常量。通过枚举,可以使代码更具可读性和可维护性。关键字 enum
用于定义枚举类型。
枚举特点:
- 枚举值是整数:每个枚举值默认从
0
开始递增,但可以显式指定值。 - 提高代码可读性:通过使用有意义的名称代替数字,代码更易理解。
- 作用范围受限:通常用于描述固定范围的选项,例如按键状态、按键事件等。
typedef enum { KEY_DOWN, // 默认值 0 KEY_UP // 默认值 1 } KeyStatus; typedef enum { NULL_PRESS, // 默认值 0 SHORT_PRESS, // 默认值 1 LONG_PRESS // 默认值 2 } KeyEvent;
KEY_DOWN
和KEY_UP
是KeyStatus
枚举中的两个值。NULL_PRESS
、SHORT_PRESS
和LONG_PRESS
是KeyEvent
枚举中的三个值。- 使用枚举后,代码可以直接使用这些名称,而无需记忆具体的数值,所以我使用枚举类型替代了大量标志位,增强代码可读性
3、typedef
是什么?
typedef
是 C 语言中的一个关键字,用于给现有类型定义新的类型别名。
特点:
- 简化代码:可以为复杂的类型取一个简短的别名。
- 增强可读性:通过使用有意义的名称,代码逻辑更加清晰。
- 方便维护:如果底层类型需要更改,只需修改别名定义即可,无需逐一修改代码。
typedef enum { KEY_DOWN, KEY_UP } KeyStatus;
typedef
为枚举类型创建了一个别名KeyStatus
。- 定义后,
KeyStatus
就可以直接作为一种数据类型使用:
KeyStatus key = KEY_DOWN; // 声明变量 key,类型为 KeyStatus
对比没有 typedef
的写法:
enum KeyStatus {
KEY_DOWN,
KEY_UP
};
enum KeyStatus key = KEY_DOWN;
不使用 typedef
时,每次声明变量都需要加上 enum
关键字,代码显得繁琐
4、结构体
// 定义按键配置结构体
typedef struct {
KeyStatus status;
KeyEvent event;
uint16_t LongPressCount;
} KeyHandler;
extern KeyHandler Key[4];
我使用了4个按键,所以使用extern KeyHandler Key[4];定义了一个长度为4的一维数组,下面都以Key[0]为例,这样定义都,Key[0]就有了四个成员,分别为 Key[0].event Key[0].status Key[0].LongPressCount
5、按键检测
if(count % 5 == 0)
{
for(uint8_t i = 0; i < 4; i++)
{
if(GPIO_ReadInputDataBit(Key_Prot[i], Key_Pin[i]) == 0)
{
Key[i].status = KEY_DOWN;
Key[i].LongPressCount++;
}
else
{
Key[i].status = KEY_UP;
}
}
Key_handler(); //调用按键状态机
这是5ms的任务,这里是四个按键,所以可以先把最外层的for循环去掉,判断按键的引脚是否被拉低,如果被拉低,Key[0].status按键的状态变为按下,同时按键计数器LongPressCount开始计数,每5ms计数一次,当检测到按键的引脚不为0时就将按键状态设置为抬起,调用按键处理函数检测
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 按键处理函数
// 参数说明 void
// 返回参数 void
// 使用示例 Key_handler(); // 扫描按键状态并判断是否为短按或长按
// 备注信息 根据按键状态和按下持续时间,判断按键事件为短按或长按
//-------------------------------------------------------------------------------------------------------------------
void Key_handler(void)
{
for(uint8_t i = 0; i < 4; i++)
{
if(Key[i].status == KEY_UP)
{
if(Key[i].LongPressCount <= 4)
{
Key[i].event = NULL_PRESS;
}
if(Key[i].LongPressCount > 4 && Key[i].LongPressCount <200)
{
Key[i].event = SHORT_PRESS;
Key[i].LongPressCount = 0;
}
if(Key[i].LongPressCount >= 200)
{
Key[i].event = LONG_PRESS;
Key[i].LongPressCount = 0;
}
}
}
}
按键是松开的状态才会进行判断,在判断时如果按键计数器的值在0-4,则按键的事件为没按下,不对按键计数器进行清零操作,按键计数器继续计数
6、按键任务
if(Key[0].event == SHORT_PRESS)
{
a++;
}
/**************************按键KEY1长按任务************************************/
if(Key[0].event == LONG_PRESS)
{
a += 10;
}
/**************************按键KEY2短按任务************************************/
if(Key[1].event == SHORT_PRESS)
{
a--;
}
/**************************按键KEY2长按任务************************************/
if(Key[1].event == LONG_PRESS)
{
a -= 10;
}
/**************************按键KEY3短按任务************************************/
if(Key[2].event == SHORT_PRESS)
{
a += 5;
}
/**************************按键KEY3长按任务************************************/
if(Key[2].event == LONG_PRESS)
{
a -= 5;
}
/**************************按键KEY4短按任务************************************/
if(Key[3].event == SHORT_PRESS)
{
a *= 2;
}
/**************************按键KEY4长按任务************************************/
if(Key[3].event == LONG_PRESS)
{
a /= 2;
}
判断按键的事件来进行相应的任务,那有人就问我,松手的时候也有抖动,这怎么就不需要消抖呢?其实一步一步看程序,当执行按键处理函数时按键计数器会被清零,如果有抖动,会开始新一轮的计数,计数值过小就无法执行按键的动作
三、完整源码
1、Key.h
#ifndef __KEY_H__
#define __KEY_H__
// 定义按键状态枚举
typedef enum {
KEY_DOWN,
KEY_UP,
} KeyStatus;
// 定义按键事件枚举
typedef enum {
NULL_PRESS,
SHORT_PRESS,
LONG_PRESS
} KeyEvent;
// 定义按键配置结构体
typedef struct {
KeyStatus status;
KeyEvent event;
uint16_t LongPressCount;
} KeyHandler;
extern KeyHandler Key[4];
extern uint16_t KEY[4];
extern GPIO_TypeDef* Key_Prot[4];
extern uint16_t Key_Pin[4];
void Key_Init(void);
void Key_handler(void);
#endif
2、Key.c
#include "stm32f10x.h" // Device header
#include "Key.h"
//KEY1----->PC14 KEY2----->PC15 KEY3----->PA5 KEY4----->PC4
KeyHandler Key[4];
GPIO_TypeDef* Key_Prot[4] = {GPIOC, GPIOC, GPIOA, GPIOC};
uint16_t Key_Pin[4] = {GPIO_Pin_14, GPIO_Pin_15, GPIO_Pin_5, GPIO_Pin_4};
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 按键初始化函数
// 参数说明 void
// 返回参数 void
// 使用示例 Key_Init();
// 备注信息 设置所有按键为上拉输入,并初始化按键事件、状态和长按计数器
//-------------------------------------------------------------------------------------------------------------------
void Key_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC, ENABLE);
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14 | GPIO_Pin_15 | GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
for(uint8_t i; i < 4; i++)
{
Key[i].event = NULL_PRESS;
Key[i].status = KEY_UP;
Key[i].LongPressCount = 0;
}
}
//-------------------------------------------------------------------------------------------------------------------
// 函数简介 按键处理函数
// 参数说明 void
// 返回参数 void
// 使用示例 Key_handler(); // 扫描按键状态并判断是否为短按或长按
// 备注信息 根据按键状态和按下持续时间,判断按键事件为短按或长按
//-------------------------------------------------------------------------------------------------------------------
void Key_handler(void)
{
for(uint8_t i = 0; i < 4; i++)
{
if(Key[i].status == KEY_UP)
{
if(Key[i].LongPressCount <= 4)
{
Key[i].event = NULL_PRESS;
}
if(Key[i].LongPressCount > 4 && Key[i].LongPressCount <200)
{
Key[i].event = SHORT_PRESS;
Key[i].LongPressCount = 0;
}
if(Key[i].LongPressCount >= 200)
{
Key[i].event = LONG_PRESS;
Key[i].LongPressCount = 0;
}
}
}
}
3、main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "OLED.h"
#include "stdio.h"
#include "Key.h"
#include "Timer.h"
uint16_t a, Key_flag;
int main(void)
{
char text[20];
LED_Init();
OLED_Init();
Timer_Init(72, 1000);
Key_Init();
while (1)
{
if(Key_flag == 0)
{
if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_14) == 0)
{
a++;
Key_flag = 1;
}
}
if(Key_flag == 1)
{
if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_14) == 1)
{
Key_flag = 0;
}
}
sprintf(text,"%d", a);
OLED_ShowString(1,1,text);
OLED_Refresh();
}
}
void TIM6_IRQHandler(void)
{
if (TIM_GetITStatus(TIM6, TIM_IT_Update) == SET)
{
static uint16_t count = 0;
count++;
/*********************************************5ms任务*******************************************************/
if(count % 5 == 0)
{
for(uint8_t i = 0; i < 4; i++)
{
if(GPIO_ReadInputDataBit(Key_Prot[i], Key_Pin[i]) == 0)
{
Key[i].status = KEY_DOWN;
Key[i].LongPressCount++;
}
else
{
Key[i].status = KEY_UP;
}
}
Key_handler(); //调用按键状态机
/**************************按键KEY1短按任务************************************/
if(Key[0].event == SHORT_PRESS)
{
a++;
}
/**************************按键KEY1长按任务************************************/
if(Key[0].event == LONG_PRESS)
{
a += 10;
}
/**************************按键KEY2短按任务************************************/
if(Key[1].event == SHORT_PRESS)
{
a--;
}
/**************************按键KEY2长按任务************************************/
if(Key[1].event == LONG_PRESS)
{
a -= 10;
}
/**************************按键KEY3短按任务************************************/
if(Key[2].event == SHORT_PRESS)
{
a += 5;
}
/**************************按键KEY3长按任务************************************/
if(Key[2].event == LONG_PRESS)
{
a -= 5;
}
/**************************按键KEY4短按任务************************************/
if(Key[3].event == SHORT_PRESS)
{
a *= 2;
}
/**************************按键KEY4长按任务************************************/
if(Key[3].event == LONG_PRESS)
{
a /= 2;
}
}
/*********************************************10ms任务*******************************************************/
if(count % 10 == 0)
{
count = 0;
}
TIM_ClearITPendingBit(TIM6, TIM_IT_Update);
}
}