当前位置: 首页 > news >正文

编程技巧(基于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,避免上一个模式的按键标志位对这个模式产生影响.

http://www.xdnf.cn/news/936937.html

相关文章:

  • 【agent开发】VS Code连接WSL失败解决
  • 实验一:数据选择器实验
  • Go语言中的if else控制语句
  • DeepSeek 技术赋能无人农场协同作业:用 AI 重构农田管理 “神经网”
  • masm32汇编实现扫雷进程注入
  • 第1课、LangChain 介绍
  • 算法-数论
  • Java线程池核心原理与最佳实践
  • 永磁同步电机参数辨识算法--IPMSM拓展卡尔曼滤波全参数辨识
  • 73常用控件_QFormLayout的使用
  • 一个自动反汇编脚本
  • 深度学习入门Day3--鱼书学习(2)
  • 前端十种排序算法解析
  • 电压型PHY芯片MDI接口设计
  • 计算机网络笔记(二十九)——5.1运输层协议概述
  • QT线程同步 QReadWriteLock并发访问
  • xtp+ctp 交易系统接口简介
  • DAX权威指南9:DAX 查询分析与优化1
  • leetcode 386. 字典序排数 中等
  • Python爬虫实战:研究demiurge框架相关技术
  • 从零开始打造 OpenSTLinux 6.6 Yocto 系统(基于STM32CubeMX)(十)
  • pgsql batch insert optimization (reWriteBatchedInserts )
  • Digital IC Design Flow
  • vue3:十六、个人中心-修改密码
  • bugku 网络安全事件应急响应
  • 02.管理数据库
  • CCPC guangdongjiangsu 2025 F
  • 【创新算法】改进深度优先搜索算法配合二进制粒子群的配电网故障恢复重构研究
  • 食养有方:进行性核上性麻痹患者的健康饮食指南
  • 解决SQL Server SQL语句性能问题(9)——SQL语句改写(2)