编程技巧(基于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 3void 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 4uint8_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 = 1else 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 = 1switch(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 = 1Time[n] = KEY_TIME_REPEAT; S[n] = 4;}break;case 2: //按键已松开态,做检测按键按下和计时的操作if(CurrState[n] == KEY_PRESSED){ //若按键被按下,切换至S = 3状态,并置双击事件标志位Key_Flag[n] |= KEY_DOUBLE; //DOUBLE = 1S[n] = 3;}else if(Time[n] == 0){ //若计时到达了双击判断时间,切换至S = 0状态,并置单击事件标志位Key_Flag[n] |= KEY_SINGLE; //SINGLE = 1S[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,避免上一个模式的按键标志位对这个模式产生影响.