基于状态机实现的按键释放、消抖、单击、双击、长按等功能

news2025/7/10 2:46:10

状态机理论:
在这里插入图片描述

每次只能执行状态机中的一个状态

注意:状态转移时有时会根据需要发生不同的动作(可根据不同条件发生不同的动作),此处的发生动作指返回按键码值(代表那个按键的对应状态)
如果是在长按状态在向释放状态迁移过程中,执行了获取长按的按键码值,则代表着是在长按结束时,系统响应长按。
在这里插入图片描述

分析该状态机:

四个状态:释放,消抖、短按、长按,三个动作:三个返回值:长按码值、单击按码值、双击码值。

关于长按释放时执行还是在按下时执行分析:

如果是在长按状态在向释放状态迁移过程中,执行了获取长按的按键码值,则代表着是在长按结束时,系统响应长按。如果是在短按状态向长按状态迁移时,发生的动作(返回长按码值),则代表着是在长按开始时,执行了长按的动作。

在此代码中,按键状态主要有以下四种:

KEY_RELEASE (释放松开状态):表示按键处于未按下或已松开的状态。在这个状态下,代码检测是否有按键按下,如果按下,转移到消抖确认状态。

KEY_CONFIRM (消抖确认状态):用于处理按键的消抖过程。在这个状态下,代码检测按键是否稳定按下。如果按键稳定按下超过设定的消抖时间窗(CONFIRM_TIME),则转移到短按状态。如果按键在消抖时间窗内松开,返回释放状态。

KEY_SHORTPRESS (短按状态):表示按键已稳定按下,等待判断是短按、双击还是长按。在这个状态下,如果按键松开,转移回释放状态,并记录单击次数。如果按键持续按下超过长按时间窗(LONGPRESS_TIME),则转移到长按状态。

KEY_LONGPRESS (长按状态):表示按键已持续按下超过设定的长按时间窗。在这个状态下,如果按键松开,转移回释放状态,并返回长按的按键码值。

#include <stdint.h>
#include "gd32f30x.h"
#include "systick.h"
#include "delay.h"

// 定义按键引脚和RCU配置的结构体
typedef struct
{
	rcu_periph_enum rcu;  // 外设时钟
	uint32_t gpio;        // GPIO端口
	uint32_t pin;         // GPIO引脚
} Key_GPIO_t;

// 按键引脚配置表,只配置了一个按键
static Key_GPIO_t g_gpioList[] = 
{
	{RCU_GPIOC, GPIOC, GPIO_PIN_4},  // key1
};

// 定义按键的最大数量
#define KEY_NUM_MAX (sizeof(g_gpioList) / sizeof(g_gpioList[0]))

// 按键状态枚举
typedef enum
{
	KEY_RELEASE = 0,         // 释放松开
	KEY_CONFIRM,             // 消抖确认
	KEY_SHORTPRESS,          // 短按
	KEY_LONGPRESS            // 长按
} KEY_STATE;

// 定义时间窗口常量
#define CONFIRM_TIME                 10       // 按键消抖时间窗10ms
#define DOUBLE_CLICK_INTERVAL_TIME   300      // 双击时间窗300ms
#define LONGPRESS_TIME               1000     // 长按时间窗1000ms

// 定义按键信息结构体
typedef struct
{
	KEY_STATE keyState;               // 按键当前状态
	uint8_t singleClickNum;           // 单击次数
	uint64_t firstIoChangeSysTime;    // 第一次按键状态改变的时间
	uint64_t firstReleaseSysTime;     // 第一次按键释放的时间
} Key_Info_t;

// 定义全局按键信息数组,保存所有按键的状态信息
static Key_Info_t g_keyInfo[KEY_NUM_MAX];

/**
***********************************************************
* @brief 按键硬件初始化
* @param 无
* @return 无
***********************************************************
*/
void KeyDrvInit(void)
{
	for (uint8_t i = 0; i < KEY_NUM_MAX; i++)
	{
		// 启用对应GPIO端口的时钟
		rcu_periph_clock_enable(g_gpioList[i].rcu);
		
		// 配置GPIO引脚为上拉输入模式,速度为2MHz
		gpio_init(g_gpioList[i].gpio, GPIO_MODE_IPU, GPIO_OSPEED_2MHZ, g_gpioList[i].pin);
	}
}

/**
***********************************************************
* @brief 扫描按键状态并返回按键码值
* @param keyIndex 按键索引
* @return 按键码值,短按返回0x01,双击返回0x51,长按返回0x81
***********************************************************
*/
static uint8_t KeyScan(uint8_t keyIndex)
{
	volatile uint64_t curSysTime;
	uint8_t keyPress;
	
	// 读取当前按键状态,0表示按下
	keyPress = gpio_input_bit_get(g_gpioList[keyIndex].gpio, g_gpioList[keyIndex].pin);

	switch (g_keyInfo[keyIndex].keyState)
	{
		case KEY_RELEASE:  // 按键释放状态
			if (!keyPress)  // 如果按键被按下
			{ 
				// 切换到消抖状态,并记录按键按下的系统时间
				g_keyInfo[keyIndex].keyState = KEY_CONFIRM;
				g_keyInfo[keyIndex].firstIoChangeSysTime = GetSysRunTime();
				break;
			}

			// 如果存在未处理的单击
			if (g_keyInfo[keyIndex].singleClickNum != 0)
			{
				curSysTime = GetSysRunTime();
				// 判断是否超出双击时间窗,如果超出,认为是单击
				if (curSysTime - g_keyInfo[keyIndex].firstReleaseSysTime > DOUBLE_CLICK_INTERVAL_TIME)
				{
					g_keyInfo[keyIndex].singleClickNum = 0;
					return (keyIndex + 1); // 返回单击按键码值
				}
			}
			break;
			
		case KEY_CONFIRM:  // 按键消抖确认状态
			if (!keyPress)
			{
				curSysTime = GetSysRunTime();
				// 如果按键按下稳定超过消抖时间窗,则切换到短按状态
				if (curSysTime - g_keyInfo[keyIndex].firstIoChangeSysTime > CONFIRM_TIME)
				{
					g_keyInfo[keyIndex].keyState = KEY_SHORTPRESS;
				}
			}
			else  // 如果按键松开,则回到释放状态
			{
				g_keyInfo[keyIndex].keyState = KEY_RELEASE;
			}
			break;
			
		case KEY_SHORTPRESS:  // 短按确认状态
			if (keyPress)
			{
				g_keyInfo[keyIndex].keyState = KEY_RELEASE;
				g_keyInfo[keyIndex].singleClickNum++;  // 记录单击次数
				
				// 第一次单击,记录按键释放时间
				if (g_keyInfo[keyIndex].singleClickNum == 1)
				{
					g_keyInfo[keyIndex].firstReleaseSysTime = GetSysRunTime();
					break;
				}
				else
				{
					curSysTime = GetSysRunTime();
					// 如果双击间隔时间内按下第二次,认为是双击
					if (curSysTime - g_keyInfo[keyIndex].firstReleaseSysTime <= DOUBLE_CLICK_INTERVAL_TIME)
					{
						g_keyInfo[keyIndex].singleClickNum = 0;
						return (keyIndex + 0x51); // 返回双击按键码值
					}
				}
			}
			else
			{
				curSysTime = GetSysRunTime();
				// 按键按下时间超过长按时间窗,认为是长按
				if (curSysTime - g_keyInfo[keyIndex].firstIoChangeSysTime > LONGPRESS_TIME)
				{	
					g_keyInfo[keyIndex].keyState = KEY_LONGPRESS;
				}
			}
			break;
			
		case KEY_LONGPRESS:  // 长按状态
			if (keyPress)
			{
				g_keyInfo[keyIndex].keyState = KEY_RELEASE;
				return (keyIndex + 0x81); // 返回长按按键码值
			}
			break;
			
		default:
			g_keyInfo[keyIndex].keyState = KEY_RELEASE;
			break;
	}
	return 0;
}

/**
***********************************************************
* @brief 获取按键码值
* @param 无
* @return 按键码值,短按返回0x01 0x02 0x03,长按返回0x81 0x82 0x83,没有按下返回0
***********************************************************
*/
uint8_t GetKeyVal(void)
{
	uint8_t res = 0;

	for (uint8_t i = 0; i < KEY_NUM_MAX; i++)
	{
		res = KeyScan(i);
		if (res != 0)
		{
			break;
		}
	}
	return res;
}

/**
***********************************************************
* @brief 带消抖的按键扫描
* @param keyIndex 按键索引
* @return 按键码值,按下返回keyIndex+1,否则返回0
***********************************************************
*/
static uint8_t KeyScanWithBlock(uint8_t keyIndex)
{
	uint8_t keyPress;
	
	keyPress = gpio_input_bit_get(g_gpioList[keyIndex].gpio, g_gpioList[keyIndex].pin);

	if (keyPress)  // 无按键按下
	{ 
		return 0;
	}

	DelayNms(CONFIRM_TIME);  // 消抖延时

	keyPress = gpio_input_bit_get(g_gpioList[keyIndex].gpio, g_gpioList[keyIndex].pin);

	if (keyPress)  // 无按键按下
	{ 
		return 0;
	}
	return (keyIndex + 1);
}

/**
***********************************************************
* @brief 带消抖的按键获取
* @param 无
* @return 按键码值,按下返回按键码值,否则返回0
***********************************************************
*/
uint8_t GetKeyValWithBlock(void)
{
	uint8_t res = 0;

	for (uint8_t i = 0; i < KEY_NUM_MAX; i++)
	{
		res = KeyScanWithBlock(i);
		if (res != 0)
		{
			break;
		}
	}
	return res;	
}

方法二:一个状态对于一个返回值

#include <stdint.h> // 包含标准整数类型定义
#include "gd32f30x.h" // 包含GD32F30x系列微控制器的寄存器定义和相关功能
#include "systick.h" // 包含系统定时器相关的函数和定义
#include "delay.h" // 包含延时函数的声明和定义

// 定义按键的GPIO配置结构体
typedef struct {
    rcu_periph_enum rcu; // 微控制器的时钟使能枚举
    uint32_t gpio; // GPIO端口地址
    uint32_t pin; // 引脚编号
} Key_GPIO_t;

// 定义按键GPIO列表,这里定义了一个按键连接到GPIOC的第4位
static Key_GPIO_t g_gpioList[] = {
    {RCU_GPIOC, GPIOC, GPIO_PIN_4},  // key1
};

#define KEY_NUM_MAX (sizeof(g_gpioList) / sizeof(g_gpioList[0])) // 计算按键数量

// 定义按键状态枚举类型
typedef enum {
    KEY_RELEASE = 0,         // 按键未按下或已释放
    KEY_DEBOUNCE,            // 按键按下消抖状态
    KEY_SHORTPRESS,          // 按键短按
    KEY_DOUBLECLICK_WAIT,    // 等待判断是否双击状态
    KEY_LONGPRESS            // 按键长按
} KEY_STATE;

// 定义按键处理的时间参数
#define CONFIRM_TIME                10       // 消抖时间10ms
#define DOUBLE_CLICK_INTERVAL_TIME  300      // 双击间隔时间300ms
#define LONGPRESS_TIME              1000     // 长按时间1000ms

// 定义按键信息结构体,包含按键状态、点击次数和时间信息
typedef struct {
    KEY_STATE keyState; // 当前按键状态
    uint8_t clickCount;  // 记录点击次数,用于区分单击和双击
    uint64_t lastPressTime;   // 记录按键最后一次按下的时间
    uint64_t lastReleaseTime; // 记录按键最后一次释放的时间
} Key_Info_t;

// 初始化按键信息数组,数量为按键数量
static Key_Info_t g_keyInfo[KEY_NUM_MAX];

// 按键硬件初始化函数,配置GPIO为上拉输入模式
void KeyDrvInit(void) {
    for (uint8_t i = 0; i < KEY_NUM_MAX; i++) {
        rcu_periph_clock_enable(g_gpioList[i].rcu); // 使能GPIO时钟
        gpio_init(g_gpioList[i].gpio, GPIO_MODE_IPU, GPIO_OSPEED_2MHZ, g_gpioList[i].pin); // 初始化GPIO
    }
}

// 静态函数,用于扫描按键状态
static uint8_t KeyScan(uint8_t keyIndex) {
    // 获取当前系统运行时间
    uint64_t curSysTime = GetSysRunTime();
    // 读取按键状态,这里假设按下为0
    uint8_t keyPress = !gpio_input_bit_get(g_gpioList[keyIndex].gpio, g_gpioList[keyIndex].pin); // 按键逻辑可能需要根据实际硬件调整

    // 根据按键当前状态和时间判断按键动作
    switch (g_keyInfo[keyIndex].keyState) {
        case KEY_RELEASE:
            if (keyPress) { // 如果按键被按下
                g_keyInfo[keyIndex].keyState = KEY_DEBOUNCE; // 迁移到消抖状态
                g_keyInfo[keyIndex].lastPressTime = curSysTime; // 记录按下时间
            }
            break;

        case KEY_DEBOUNCE:
            if (keyPress) {
                if (curSysTime - g_keyInfo[keyIndex].lastPressTime > CONFIRM_TIME) {
                    g_keyInfo[keyIndex].keyState = KEY_SHORTPRESS; // 迁移到短按状态
                    g_keyInfo[keyIndex].clickCount++; // 增加点击次数
                }
            } else {
                g_keyInfo[keyIndex].keyState = KEY_RELEASE; // 如果按键释放,迁移到释放状态
            }
            break;

        case KEY_SHORTPRESS:
            if (!keyPress) { // 如果按键释放
                g_keyInfo[keyIndex].lastReleaseTime = curSysTime; // 记录释放时间
                if (g_keyInfo[keyIndex].clickCount == 1) {
                    g_keyInfo[keyIndex].keyState = KEY_DOUBLECLICK_WAIT; // 迁移到双击等待状态
                } else {
                    // 如果不是双击,返回单击事件,并重置点击次数
                    g_keyInfo[keyIndex].keyState = KEY_RELEASE;
                    g_keyInfo[keyIndex].clickCount = 0;
                    return (keyIndex + 0x01); // 返回按键单击码值
                }
            } else if (curSysTime - g_keyInfo[keyIndex].lastPressTime > LONGPRESS_TIME) {
                // 如果按下时间超过长按时间,迁移到长按状态
                g_keyInfo[keyIndex].keyState = KEY_LONGPRESS;
            }
            break;

        case KEY_DOUBLECLICK_WAIT:
            if (keyPress) { // 如果在等待双击期间按键被按下
                g_keyInfo[keyIndex].keyState = KEY_DEBOUNCE; // 重新进入消抖状态
            } else if (curSysTime - g_keyInfo[keyIndex].lastReleaseTime > DOUBLE_CLICK_INTERVAL_TIME) {
                // 如果超过双击间隔时间,确认为单击并返回
                g_keyInfo[keyIndex].keyState = KEY_RELEASE;
                g_keyInfo[keyIndex].clickCount = 0;
                return (keyIndex + 0x51); // 返回按键双击码值
            }
            break;

        case KEY_LONGPRESS:
            if (!keyPress) { // 如果长按后按键释放
                g_keyInfo[keyIndex].keyState = KEY_RELEASE; // 返回释放状态
                return (keyIndex + 0x81); // 返回按键长按码值
            }
            break;

        default:
            g_keyInfo[keyIndex].keyState = KEY_RELEASE; // 任何未知状态都重置为释放状态
            break;
    }
    return 0; // 如果没有按键事件或按键事件已处理,则返回0
}

// 获取按键码值的函数,遍历所有按键并调用KeyScan函数
uint8_t GetKeyVal(void) {
    uint8_t res = 0; // 初始化结果为0

    for (uint8_t i = 0; i < KEY_NUM_MAX; i++) {
        res = KeyScan(i); // 调用KeyScan函数扫描按键
        if (res != 0) {
            break; // 如果有按键事件发生,返回结果并退出循环
        }
    }
    return res; // 返回按键码值,如果没有按键事件则返回0
}
  1. 实现细节
    KEY_RELEASE:初始状态或按键松开后的状态,等待按键按下。
    KEY_DEBOUNCE:按键按下后,进入消抖状态,确保按键状态稳定。
    KEY_SHORTPRESS:消抖结束后,进入短按状态,检测是否为双击或长按。
    KEY_DOUBLECLICK_WAIT:短按后松开,等待双击的第二次按下,如果超时,返回单击事件。
    KEY_LONGPRESS:在短按状态下,按键持续按住超过长按时间,进入长按状态,松开后返回长按事件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2073098.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分享一个基于python的内蒙古旅游景点数据分析与采集系统(源码、调试、LW、开题、PPT)

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人 八年开发经验&#xff0c;擅长Java、Python、PHP、.NET、Node.js、Android、微信小程序、爬虫、大数据、机器学习等&#xff0c;大家有这一块的问题可以一起交流&…

【算法进阶1】贪心算法、背包问题(0-1背包、分数背包)、拼接最大数字问题、活动选择问题

1 贪心算法 2 背包问题 2.1 0-1背包问题 2.2 分数背包 3 拼接最大数字问题 4 活动选择问题 1 贪心算法 贪心算法(又称贪婪算法)是指&#xff0c;在对问题求解时&#xff0c;总是做出在当前看来是最好的选择。 也就是说&#xff0c;不从整体最优上加以考虑&#xff0c;他所做出…

基于虚拟下垂控制的分布式电源并网建模仿真

针对并联逆变器间的环流和功率分配不均的问题&#xff0c;提出了一种基于改进虚拟阻抗的微电网逆变器下垂控制策略&#xff0c;对传统下垂控制算法的有功功率和无功功率进行分析&#xff0c;虚拟阻抗引入到电压电流双环控制策略。 在MATLAB中建立了逆变器并联运行的分布式仿真模…

【Qt】贪吃蛇

目录 贪吃蛇小游戏 一.项目介绍及演示 1. 项目介绍 2. 项目演示 3. 窗口介绍 3.1 游戏大厅窗口 3.2 游戏关卡选择窗口 3.3 游戏房间窗口 二、创建项目及资源配置 1. 创建项目&#xff08;QWidget&#xff09; ​编辑 2. 资源配置&#xff08;图片声音素材&#xff0…

文心快码助力项目实战开发

文章目录 前言支持的编程语言 Language安装方法使用方法 项目实践代码运行流程出行方案查询JSP 指令与标签库指令页面上下文路径设置表单部分查询结果显示部分 使用感受优点改进建议 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 文心快码Baidu Comat…

《黑神话:悟空》爆火,作为普通人,该怎么抓住这波风口赚到钱?

目录 一、游戏视频 1.1、游戏教程视频 1.2、游戏剧情视频 二、游戏直播 三、游戏视频出海 四、AI黑神话悟空 大家好&#xff0c;我是小奇&#xff0c;一名热衷于分享AI副业项目的普通博主。不管你是AI小白还是老手&#xff0c;我都能帮你轻松上手&#xff0c;用AI技术赚钱…

【C++ Primer Plus习题】4.7

问题: 解答: #include <iostream> #include <string> using namespace std;typedef struct _Pizza {string companyName;float diameter;float wieght; }Pizza;int main() {Pizza p;cout << "请输入披萨的公司名: ";getline(cin, p.companyName);…

uniapp+vue3的ifdef实现多端配置客服消息

在微信小程序预览&#xff0c;实现客服消息&#xff0c;因是个人版&#xff0c;不支持 button | uni-app官网 (dcloud.net.cn) 条件编译处理多端差异 | uni-app官网 (dcloud.net.cn) uni.makePhoneCall(OBJECT) | uni-app官网 (dcloud.net.cn) //my.vue <template> &l…

10大国产AI绘画软件,每一款都挺好用 ,你用过吗?

在这个科技与艺术交织的时代,AI绘画软件正以惊人的速度改变着我们的创作方式。今天,就让我们一起探索那些你绝不能错过的10大国产AI绘画神器,它们不仅让创作变得前所未有的简单高效,更让每一位艺术家和爱好者都能享受到创作的无限乐趣! 1️⃣触站A🎨——语音创作,未…

循环结构程序设计-找出指定数量学生的最高分

**7-1-1 #include <stdio.h>int main(){int n,score,max;scanf("%d",&n);max 0;for(int i0;i<n;i){scanf("%d",&score);if(score>max){max score;}}printf("%d",max);return 0; }

一文迅速上手 ESP32 bluedroid 蓝牙从机开发

前言 个人邮箱&#xff1a;zhangyixu02gmail.com该博客主要针对希望迅速上手 ESP32 蓝牙从机开发人员&#xff0c;因此&#xff0c;很多蓝牙技术细节知识并不会进行介绍&#xff0c;仅仅介绍我认为需要了解的 API 函数和回调内容。本文主要是基于gatt_server demo来微调进行进…

# ‘telnet‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件。

‘telnet’ 不是内部或外部命令,也不是可运行的程序 或批处理文件。 一、报错描述&#xff1a; 1、当使用 telnet 命令&#xff0c;连接本地 tomcat 的 8005 端口时报错。 2、报错解释 这个错误表明系统无法识别telnet命令&#xff0c;因为它不是内置命令&#xff0c;也没有…

跳马(华为od机考题)

一、题目 1.原题 马是象棋(包括中国象棋和国际象棋)中的棋子&#xff0c; 走法是每步直一格再斜一格&#xff0c; 即先横着或直着走一格&#xff0c;然后再斜着走一个对角线&#xff0c; 可进可退&#xff0c;可越过河界&#xff0c;俗称“马走‘日’字。 给顶m行n列的棋盘&…

人工智能在专业领域的斗争

介绍 ChatGPT 等大型语言模型 (LLM) 在用自然语言讨论一般话题的能力方面令人印象深刻。然而&#xff0c;他们在医学、金融和法律等专业领域却举步维艰。这是由于缺乏真正的理解&#xff0c;并且注重模仿而不是智力。 大语言模型正处于炒作的顶峰。由于能够用自然语言回答和讨…

“Docker中部署Kibana:步骤与指南“

博主这篇文章是跟Elasticsearch那篇文章是有关系的&#xff0c;建议大家先去看&#xff1a; 轻松上手&#xff1a;Docker部署Elasticsearch&#xff0c;高效构建搜索引擎环境_docker 启动 es-CSDN博客 这篇博文&#xff0c;还有镜像下载不下来的情况&#xff0c;大家可以去看…

攻破:重定向 缓冲区

文章目录 前言&#xff1a;认识读文件read认识重定向&&缓冲区重定向现象及分析&#xff1a;dup2的介绍&#xff1a; 缓冲区的引入&#xff1a;缓冲区的理解&#xff1a; 前言&#xff1a; ​ 从上一章开始&#xff0c;我们进入了文件IO的学习&#xff0c;认识了文件描…

浅谈C# RabbitMQ

一、基本介绍 RabbitMQ——Rabbit Message Queue的简写&#xff0c;但不能仅仅理解其为消息队列&#xff0c;消息代理更合适。 RabbitMQ 是一个由 Erlang 语言开发的AMQP&#xff08;高级消息队列协议&#xff09;的开源实现&#xff0c;其内部结构如下&#xff1a; RabbitMQ作…

今年秋招太吓人了。(20届,在得物做Java开发)

有个学弟来问我诉苦最近好忙好累&#xff0c;说竞争压力特别大&#xff0c;让我给点建议&#xff0c;要不要放弃实习闷头搞秋招&#xff0c;我才意识到时间太快了&#xff0c;想想我都毕业几年了&#xff0c;感慨颇深&#xff0c;整理一下我的求职经验和目前的心得吧&#xff0…

SpingBoot集成kafka-发送读取消息示例

SpingBoot集成kafka开发 kafka的几个常见概念 1、springboot和kafka对应版本&#xff08;重要&#xff09;2、创建springboot项目&#xff0c;引入kafka依赖2.1、生产者EventProducer2.2、消费者EventConsumer2.3、启动生产者的方法SpringBoot01KafkaBaseApplication2.4、appli…

监控电脑屏幕的软件叫什么?6款电脑屏幕监控软件分享!

监控电脑屏幕的软件可以帮助企业和家长监控电脑的使用情况&#xff0c;确保工作和学习的效率与安全。 以下是六款常用的电脑屏幕监控软件及其特点&#xff1a; 1. Keylogger 特点&#xff1a;专注于企业数据安全和员工上网行为管理。 功能&#xff1a;全面的屏幕监控、上网…