STM32速成笔记(3)—按键检测
2024-03-05 来源:elecfans
一、按键检测原理
按键检测原理比较简单,按键按下和不按下,其连接引脚的电平是不一样的,按键检测正是通过检测按键引脚的电平变化来实现的。比如按键未按下时引脚电平为高电平,按键按下后为低电平。我们在检测按键时只需要检测按键引脚是否变为低电平来确定按键是否按下。
二、硬件连接
按键的硬件连接决定了我们在配置按键IO时IO的状态。以我们使用的普中核心板为例,上面有三个按键
普中核心板按键硬件电路图
其中K1一端接VCC,另一端接单片机。K2和K3一端接地,另一端接单片机。硬件电路不同,导致他们在进行按键检测时IO的配置不同。
针对K1这种按键电路,按键按下时,单片机的引脚接到VCC,因此在未按下的情况下该引脚的默认电平为低电平,也就是要把IO设置为输入下拉模式。同理,对于K2和K3这种连接方式,对应IO应该配置为输入上拉模式,使得按键未被按下时,引脚处于高电平状态。
三、程序设计
按键检测主要有以下步骤
• 初始化GPIO
• 检测按下按键
• 消抖(防误触,一般通过延时实现)
• 松手检测
• 执行按键功能
3.1 初始化GPIO
根据原理图,谱中的STM32核心板提供了三个按键,我们使用K1和K2来实现点亮和关闭LED的操作。K1对应的IO为PA0,K2对应的IO为PE4。
按键对应GPIO
根据上一节了解的初始化GPIO程序,初始化按键GPIO。
/*
*==============================================================================
*函数名称:Drv_KeyGpio_Init
*函数功能:初始化KEY的GPIO
*输入参数:无
*返回值:无
*备 注:根据硬件电路确定GPIO模式
*==============================================================================
*/
void Drv_KeyGpio_Init (void)
{
GPIO_InitTypeDef GPIO_InitStructure; // 定义结构体
// 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOE,ENABLE);
// 配置结构体 WK UP
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; // 输入下拉
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置结构体 KEY0,KEY1
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 输入上拉
GPIO_Init(GPIOE, &GPIO_InitStructure);
}
3.2 按键扫描函数
按键扫描函数的功能是检测是否有按键按下,按下的按键是哪一个。检测方法上面已经叙述,通过检测按键引脚的电平。以WK UP按键为例。当WK UP被按下时,其对应的引脚PA0会变为高电平。
此时检测PA0的输入电平,如果确实是低电平,则说明WK UP可能被按下。说可能是因为PA0为低电平不一定是WK UP按下造成,也可能是抖动,所以这里就需要消抖操作。这里的消抖操作比较简单粗暴,直接延时10ms看该引脚是否依旧是低电平。如果延时10ms后依旧是高电平,则认为确实是由按键按下导致的电平变化,而不是机械抖动。
确定检测到按键按下后,需要等待按键被松开在执行按键功能。为什么需要进行松手检测?举个例子,比如设置阈值时,按键按下阈值加1,如果不进行松手检测,那么按下一次按键会加很多次,因为在不停地执行按键功能。
这里按键的松手检测也比较简单粗暴,用一个while死循环等待松手。比如WK UP被按下后,其引脚会一直保持高电平,也就是PAin(0)一直等于1,此时用一个while (PAin(1));来等待松手,做松手检测。
四、按键控制LED
这里做一个小练习,用普中核心板上的按键KEY0和KEY1来控制LED1的亮灭。步骤如下
• 初始化LED和KEY的GPIO
• 编写LED控制函数
• 编写按键检测函数(检测按键)
• 编写按键服务函数(实现按键功能)
4.1 初始化LED和KEY的GPIO
/*
*==============================================================================
*函数名称:Drv_LedGpio_Init
*函数功能:初始化LED的GPIO
*输入参数:无
*返回值:无
*备 注:无
*==============================================================================
*/
void Drv_LedGpio_Init (void)
{
GPIO_InitTypeDef GPIO_InitStructure; // 定义结构体
// 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOE,ENABLE);
// 配置结构体 LED0
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽式输出
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_5); // 熄灭LED
// 配置结构体 LED1
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽式输出
GPIO_Init(GPIOE, &GPIO_InitStructure);
GPIO_SetBits(GPIOE,GPIO_Pin_5); // 熄灭LED
}
/*
*==============================================================================
*函数名称:Drv_KeyGpio_Init
*函数功能:初始化KEY的GPIO
*输入参数:无
*返回值:无
*备 注:根据硬件电路确定GPIO模式
*==============================================================================
*/
void Drv_KeyGpio_Init (void)
{
GPIO_InitTypeDef GPIO_InitStructure; // 定义结构体
// 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOE,ENABLE);
// 配置结构体 WK UP
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; // 输入下拉
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置结构体 KEY0,KEY1
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 输入上拉
GPIO_Init(GPIOE, &GPIO_InitStructure);
}
4.2 编写按键扫描函数
```c
/*
*==============================================================================
*函数名称:Med_KeyScan
*函数功能:检测按下按键
*输入参数:无
*返回值:按键键值 0:按键WK UP,1:KEY0,2:KEY1
*备 注:无
*==============================================================================
*/
u8 Med_KeyScan (void)
{
// 按键WK UP
if (KEY_UP == 1)
{
delay_ms (10); // 延时10ms消抖
if (KEY_UP == 1)
{
while (KEY_UP); // 松手检测
return 1;
}
}
// 按键KEY0
else if (KEY0 == 0)
{
delay_ms (10); // 延时10ms消抖
if (KEY0 == 0)
{
while (!KEY0); // 松手检测
return 2;
}
}
// 按键KEY1
else if (KEY1 == 0)
{
delay_ms (10); // 延时10ms消抖
if (KEY1 == 0)
{
while (!KEY1); // 松手检测
return 3;
}
}
// 没有按键按下
return 0xff; // 用0xff表示没有按键按下
}
4.2 编写LED控制函数
/*
*==============================================================================
*函数名称:Med_Led_StateCtrl
*函数功能:控制LED亮灭
*输入参数:
LEDx:可选择的LED(0~1)
State:LED亮灭状态(LED_ON,LED_OFF)
*返回值:无
*备 注:无
*==============================================================================
*/
void Med_Led_StateCtrl (LED_TypeDef LEDx,u8 State)
{
switch (LEDx)
{
case 0:
PBout(5) = State;
break;
case 1:
PEout(5) = State;
break;
default:
break;
}
}
下面是.h文件中的一些结构体和宏定义。
// 可选择的LED
typedef enum
{
LED1 = 0,
LED2
}LED_TypeDef;
// 亮灭电平需要根据硬件电路确定
#define LED_ON 0
#define LED_OFF 1
4.3 编写按键服务函数
这里没有再单独编写按键服务函数,直接在main函数中编写。KETY0按下点亮LED1,KEY1按下,熄灭LED1。
u8 gKeyValue = 0; // 记录按键键值变量
int main(void)
{
Med_Mcu_Iint(); // 系统初始化
while(1)
{
gKeyValue = Med_KeyScan(); // 获取按键键值
// 按键KEY0按下
if (gKeyValue == 2)
{
Med_Led_StateCtrl(LED1,LED_ON); // 点亮LED1
}
// 按键KEY1按下
if (gKeyValue == 3)
{
Med_Led_StateCtrl(LED1,LED_OFF); // 熄灭LED1
}
}
}
至此,实现了利用KEY0和KEY1控制LED的亮灭状态。
五、拓展
5.1 一个按键单独控制一个LED亮灭
单片机的IO资源是比较珍贵的,在实际用用时很少会用两个IO资源来控制一个外设的开关,这里介绍一下方法并给出例程。比如使用普中核心板上的WK UP按键来控制LED2的亮灭状态。基本思路是在上面学会按键检测的基础上,增加一个按键按下计次变量。按键按下一次,该变量加1。如果检测到变量为1,那么点亮LED,如果检测到变量为2,那么熄灭LED,同时将计数变量清零。程序设计如下
u8 gKeyValue = 0; // 记录按键键值变量
u8 gKeyWkUpCunt = 0; // WK UP按下次数计数变量
int main(void)
{
Med_Mcu_Iint(); // 系统初始化
while(1)
{
gKeyValue = Med_KeyScan(); // 获取按键键值
// 按键WK UP按下
if (gKeyValue == 1)
{
gKeyWkUpCunt = gKeyWkUpCunt + 1; // 按键按下次数计数变量加1
// 第一次被按下
if (gKeyWkUpCunt <= 1)
{
Med_Led_StateCtrl(LED2,LED_ON); // 点亮LED2
}
// 不是第一次被按下
else if (gKeyWkUpCunt > 1)
{
gKeyWkUpCunt = 0; // 清空计数变量
Med_Led_StateCtrl(LED2,LED_OFF); // 熄灭LED2
}
}
}
}
5.2 按键长短按
除了上面介绍的一些常规操作外,有时还会用到一个按键分长按和短按。这里给出一种简单粗暴的实现思路。需要检测按键长短按时,修改一下松手检测的逻辑。延时10ms后如果按键IO依旧保持按下状态,那么确定不是机械抖动,此时在之前的松手检测while中进行粗略地计时。定义一个计数变量,每隔10ms加1。直到按键被松开。根据计数变量的值来判断按键被按下的时间,从而来区别长短按。
这里给出一个例程,KEY1短按功能为熄灭LED1,长按功能为LED1和LED2交替闪烁两轮后熄灭。按下持续时间在1s内,认为是短按,按下超过2s认为是长按。短按返回3,长按返回4。程序设计如下
u8 gKeyValue = 0; // 记录按键键值变量
u16 gKey1TimeCunt = 0; // 按键KEY1的计时变量
/*
*==============================================================================
*函数名称:Med_KeyScan
*函数功能:检测按下按键
*输入参数:无
*返回值:按键键值 0:按键WK UP,1:KEY0,2:KEY1
*备 注:无
*==============================================================================
*/
u8 Med_KeyScan (void)
{
// 按键WK UP
if (KEY_UP == 1)
{
delay_ms (10); // 延时10ms消抖
if (KEY_UP == 1)
{
while (KEY_UP); // 松手检测
return 1;
}
}
// 按键KEY0
else if (KEY0 == 0)
{
delay_ms (10); // 延时10ms消抖
if (KEY0 == 0)
{
while (!KEY0); // 松手检测
return 2;
}
}
// 按键KEY1
// 按下1s内认为是短按,返回3
// 按下超过2s认为是长按,返回4
else if (KEY1 == 0)
{
delay_ms (10); // 延时10ms消抖
if (KEY1 == 0)
{
// 等待松手
while (!KEY1)
{
delay_ms (10);
gKey1TimeCunt = gKey1TimeCunt + 1; // 计时变量加1
}
}
// 判断长短按
if (gKey1TimeCunt <= 99) // 小于等于1s
{
gKey1TimeCunt = 0; // 清零计时变量
return 3; // 短按
}
else if (gKey1TimeCunt >= 199) // 大于1s,等于2s
{
gKey1TimeCunt = 0; // 清零计时变量
return 4; // 长按
}
}
// 没有按键按下
return 0xff; // 用0xff表示没有按键按下
}
main函数中添加下述程序
// 长按KEY1
if (gKeyValue == 4)
{
Med_Led_StateCtrl(LED1,LED_ON); // 点亮LED1
delay_ms (500);
Med_Led_StateCtrl(LED1,LED_OFF); // 熄灭LED1
Med_Led_StateCtrl(LED2,LED_ON); // 点亮LED2
delay_ms (500);
Med_Led_StateCtrl(LED2,LED_OFF); // 熄灭LED2
Med_Led_StateCtrl(LED1,LED_ON); // 点亮LED1
delay_ms (500);
Med_Led_StateCtrl(LED1,LED_OFF); // 熄灭LED1
Med_Led_StateCtrl(LED2,LED_ON); // 点亮LED2
delay_ms (500);
Med_Led_StateCtrl(LED1,LED_OFF); // 熄灭LED1
Med_Led_StateCtrl(LED2,LED_OFF); // 熄灭LED1
}