编程技巧(基于STM32)第二章 全功能按键非阻塞式实现按键单击、双击和长按

参考教程:[编程技巧] 第2期 全功能按键非阻塞式实现 按键单击 双击 长按_哔哩哔哩_bilibili

一、实验前信息储备

1、程序框架

(1)设置几个全局变量记录几个按键标志位,用于指示按键发生的事件。

(2)定时器中断每隔固定时间读取按键,根据按键引脚的电平变化置对应的按键标志位。

(3)主程序中循环读取按键对应的标志位,如果有相应的标志位为1,则说明按键发生了相应事件,主程序根据事件执行相应的操作,并清空相应的标志位。

(4)程序的整体逻辑类似于FreeRTOS中的事件标志组(不完全一致),首先需要判断事件是否发生,然后事件触发某种动作,接着清空事件标志位,等待下一次事件发生。

2、按键标志位

(1)按键标志位的定义:

名称

释义

功能描述

0

HOLD

按住不放

按键按住不放时置1,按键松开时置0

1

DOWN

按下时刻

按键按下的时刻置1

2

UP

松开时刻

按键松开的时刻置1

3

SINGLE

单击

按键按下松开后,没有再次按下,超过双击时间阈值的时刻置1

4

DOUBLE

双击

按键按下松开后,在双击时间阈值内再次按下的时刻置1

5

LONG

长按

按键按住不放,超过长按时间阈值的时刻置1

6

REPEAT

重复

按键长按后,每隔重复时间阈值置一次1,直到按键松开

说明

HOLD、DOWN、UP,在任何时刻,只要检测到对应的事件,就会置标志位;SINGLE、DOUBLE、LONG/REPEAT,三者互斥,一次完整的按键流程,只会置其中一类标志位;HOLD自动置1和清0,其余标志位在检测到指定事件的时刻置1,读后清0

(2)置标志位SINGLE、DOUBLE、LONG/REPEAT的逻辑(高电平表示按键未按下,低电平表示按键按下):

①单击:按键按下后,在长按时间阈值内松开,且在双击时间阈值内不再按下,置单击事件标志位为1。

②双击:按键按下后,在长按时间阈值内松开,且在双击时间阈值内再次按下,置双击事件标志位为1。

③长按/重复:按键按下后,在长按时间阈值内未松开,判断为长按,置长按事件标志位为1,如果继续不松开,每隔重复时间阈值置一次重复事件标志位为1。

(3)置标志位SINGLE、DOUBLE、LONG/REPEAT的状态转移图:

二、实验步骤

1、准备工作

(1)拷贝一份本教程中“定时器实现非阻塞式程序”的工程文件夹,并更名为“全功能按键非阻塞式实现按键单击、双击和长按程序”,同时在上一章实验电路的基础上再在PB13和PB15引脚上接两个按键开关,开关另一端接3.3V电源。

(2)移除LED的驱动代码,本实验不涉及LED模块。(当然,在Flash空间足够的前提下完全可以不移除,无伤大雅)

2、按键模块编写

(1)在key.h中写好按键模块会用到的宏定义,如状态位掩码和按键索引枚举,同时声明按键事件状态位检查函数,供主函数调用。

#ifndef __KEY_H
#define __KEY_H

#define KEY_HOLD      (1 << 0)
#define KEY_DOWN      (1 << 1)
#define KEY_UP        (1 << 2)
#define KEY_SINGLE    (1 << 3)
#define KEY_DOUBLE    (1 << 4)
#define KEY_LONG      (1 << 5)
#define KEY_REPEAT    (1 << 6)

#define KEY1  0
#define KEY2  1
#define KEY3  2
#define KEY4  3

void Key_Init(void);
void Key_Tick(void);
uint8_t Key_Check(uint8_t n, uint8_t Flag);

#endif

(2)在key.c中写好按键模块会用到的宏定义,如各种阈值时间值、系统按键配置数量等,同时定义按键的存标志变量数组,几个按键就定义几个变量。

#include "stm32f10x.h"                  // Device header
#include "key.h"

#define KEY_PRESSED   1     //按键被按下的定义
#define KEY_UNPRESSED 0     //按键未被按下的定义

#define KEY_TIME_DOUBLE  200   //双击判断时间为0.2秒
#define KEY_TIME_LONG    2000  //长按判断时间为2秒
#define KEY_TIME_REPEAT  100   //重复判断时间为0.1秒

#define KEY_COUNT    4

uint8_t Key_Flag[KEY_COUNT];  //4个按键的存标志变量数组

(3)更改按键模块的初始化函数,需将PB13和PB15初始化为下拉输入模式。

void Key_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;   //上拉输入模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;   //下拉输入模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
}

(4)重写获取按键状态函数,函数参数需传入按键索引枚举,函数根据枚举返回对应按键的状态。

uint8_t Key_GetState(uint8_t n)
{
	if(n == KEY1){
		if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)  //判断按键1是否处于被按下的状态
			return KEY_PRESSED;
	}
	else if(n == KEY2){
		if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0) //判断按键2是否处于被按下的状态
			return KEY_PRESSED;
	}
	else if(n == KEY3){
		if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_15) == 1) //判断按键3是否处于被按下的状态
			return KEY_PRESSED;
	}
	else if(n == KEY4){
		if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13) == 1) //判断按键4是否处于被按下的状态
			return KEY_PRESSED;
	}
	return KEY_UNPRESSED;
}

(5)定义按键事件状态位检查函数,函数参数为按键索引枚举和主函数需要检查的事件。

uint8_t Key_Check(uint8_t n, uint8_t Flag)
{
	if(Key_Flag[n] & Flag)  //判断指定标志位是否被置1
	{
		if(Flag != KEY_HOLD)
			Key_Flag[n] &= ~Flag;   //标志位清零
		return 1;        //1表示事件发生
	}
	return 0;            //0表示事件未发生
}

(6)改写Key_Tick函数,供TIM2定时中断函数调用。

void Key_Tick(void)   //供TIM2定时中断函数调用
{
	static uint8_t Count;                //用于分频
	static uint8_t CurrState[KEY_COUNT];  //存储当前按键状态的数组
	static uint8_t PrevState[KEY_COUNT];  //存储上次按键状态的数组
	static uint8_t S[KEY_COUNT];       //记录识别按键事件的状态机的状态变量的数组
	static uint16_t Time[KEY_COUNT];   //用于状态机计时的变量的数组
	
	Count++;
	if(Count >= 20)
		Count = 0;
	for(int n = 0; n < KEY_COUNT; n++)
		if(Time[n] > 0)
			Time[n]--;  //采用递减计时,需要计时时设置Time值即可,判断计时是否到头,判断Time是否为0即可
	
	for(int n = 0; n < KEY_COUNT; n++){
		PrevState[n] = CurrState[n];         //获取上次按键状态
		CurrState[n] = Key_GetState(n);      //获取当前按键状态
			
		//HOLD标志位判断
		if(CurrState[n] == KEY_PRESSED)  Key_Flag[n] |= KEY_HOLD;   //HOLD = 1
		else                         Key_Flag[n] &= ~KEY_HOLD;  //HOLD = 0
		//DOWN标志位判断
		if(CurrState[n] == KEY_PRESSED && PrevState[n] == KEY_UNPRESSED)  
			Key_Flag[n] |= KEY_DOWN;  //DOWN = 1
		//UP标志位判断
		if(CurrState[n] == KEY_UNPRESSED && PrevState[n] == KEY_PRESSED) 
			Key_Flag[n] |= KEY_UP;    //UP = 1
		
		switch(S[n]){
			case 0:    //空闲态,做检测按键按下的操作
				if(CurrState[n] == KEY_PRESSED){  //若按键被按下,切换至S = 1状态,并设定长按判断时间
					Time[n] = KEY_TIME_LONG;    S[n] = 1;
				}break;
			case 1:    //按键已按下态,做检测按键松开和计时的操作
				if(CurrState[n] == KEY_UNPRESSED){  //若按键被松开,切换至S = 2状态,并设定双击判断时间
					Time[n] = KEY_TIME_DOUBLE;   S[n] = 2;
				}
				else if(Time[n] == 0){ //若计时到达了长按判断时间,切换至S = 4状态,设定重复判断时间,并置长按事件标志位
					Key_Flag[n] |= KEY_LONG;     //LONG = 1
					Time[n] = KEY_TIME_REPEAT;    S[n] = 4;
				}break;
			case 2:    //按键已松开态,做检测按键按下和计时的操作
				if(CurrState[n] == KEY_PRESSED){  //若按键被按下,切换至S = 3状态,并置双击事件标志位
					Key_Flag[n] |= KEY_DOUBLE;   //DOUBLE = 1
					S[n] = 3;
				}
				else if(Time[n] == 0){ //若计时到达了双击判断时间,切换至S = 0状态,并置单击事件标志位
					Key_Flag[n] |= KEY_SINGLE;    //SINGLE = 1
					S[n] = 0;
				}break;
			case 3:
				if(CurrState[n] == KEY_UNPRESSED){ //若按键被松开,切换至S = 0状态
					S[n] = 0;
				}break;
			case 4:
				if(CurrState[n] == KEY_UNPRESSED){ //若按键被松开,切换至S = 0状态
					S[n] = 0;
				}
				else if(Time[n] == 0){  //若计时到达了重复时间,则继续设定重复时间,同时置重复事件标志位
					Time[n] = KEY_TIME_REPEAT;
					Key_Flag[n] |= KEY_REPEAT;    //REPEAT = 1
				}break;
		}
	}	
}

3、main.c文件编写与程序调试

(1)定义两个全局变量用于观察现象。

uint16_t Num1, Num2;

(2)改写主函数,拟定4个测试用例。

int main(void){
	OLED_Init();   Key_Init();   Timer_Init();
	OLED_ShowString(1, 1, "Num1:");   OLED_ShowString(2, 1, "Num2:");
	
	while (1){
		/* 用例1 */
//		if(Key_Check(KEY1, KEY_HOLD))  //判断按键1是否被按住
//			Num1 = 1;
//		else
//			Num1 = 0;
		/* 用例2 */
//		if(Key_Check(KEY1, KEY_DOWN))  //判断按键1是否被按下
//			Num1++;
//		if(Key_Check(KEY1, KEY_UP))     //判断按键1是否被松开
//			Num2++;
		/* 用例3 */
//		if(Key_Check(KEY1, KEY_SINGLE))   //判断按键1是否被单击
//			Num1++;
//		if(Key_Check(KEY1, KEY_DOUBLE))  //判断按键1是否被双击
//			Num1 += 100;
//		if(Key_Check(0, KEY_LONG))       //判断按键1是否被长按
//			Num1 = 0;
		/* 用例4 */
//		if(Key_Check(KEY1, KEY_SINGLE) || Key_Check(KEY1, KEY_REPEAT))   //判断按键1是否被单击或被长按不松开
//			Num1++;
//		if(Key_Check(KEY2, KEY_SINGLE) || Key_Check(KEY2, KEY_REPEAT))   //判断按键2是否被单击或被长按不松开
//			Num1--;
//		if(Key_Check(KEY3, KEY_SINGLE))    //判断按键3是否被单击
//			Num1 = 0;
//		if(Key_Check(KEY4, KEY_LONG))     //判断按键4是否被长按
//			Num1 = 9999;
		
		OLED_ShowNum(1, 6, Num1, 5);   OLED_ShowNum(2, 6, Num2, 5);
	}
}

(3)将程序编译、下载,按照程序功能与要求进行调试。

4、注意事项(程序不完善处)

(1)在一轮主循环中,只能检查一次指定按键的指定事件(KEY_HOLD除外),若确实需要检查多次,则可先调用一次Key_Check函数并用变量存储返回值,后续多次判断此变量即可。

(2)双击事件的存在,使得单击事件响应有一些延迟,若程序中没有使用到双击,则可将双击时间阈值改为0,这样可以消除单击事件的延迟。

(3)按键产生了事件,对应的标志位就会一直置1,直到检查了此事件,才会自动清0,这在模式切换时可能会导致误动作(例如,模式1中没有检查过某个标志位,但是按下过按键,此标志位已经置1,随后切换为模式2,开始检查此标志位,那么一旦进入模式2,此标志位的动作就会立刻响应),解决办法是在切换模式时,统一将所有的Key_Flag清0,避免上一个模式的按键标志位对这个模式产生影响.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zevalin爱灰灰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值