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

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

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

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

关于状态机几点疑问:
1.为什么在释放状态判断单击的
2.可以在其他状态来判断是否单击吗

问题一:
在按键的释放状态(KEY_RELEASE)中判断是否是单击的设计主要是为了精确处理双击逻辑,并确保按键事件的准确性。这种设计有几个关键点:

  1. 处理双击逻辑:

    • 当用户快速点击两次按键时,系统需要判断这是否是一次双击。为此,在第一次按键松开后的一段时间内,系统进入 KEY_RELEASE 状态,等待查看用户是否会在双击时间窗口内再次按下按键。
    • 如果在此期间用户没有进行第二次按键按下操作,则可以确认这是一次单击,并返回相应的按键码值。
  2. 时间窗口管理:

    • 在按键松开时,系统会记录此时的时间点,并在 KEY_RELEASE 状态中不断检查时间间隔。
    • 如果时间间隔超过设定的双击时间窗口,则判定这次按键为单击。同时,系统会清除 singleClickNum,以便为下一次按键操作做好准备。
    • 这种管理方式使得系统能够在短时间内响应双击输入,而不会因为过早判断为单击而错过双击操作。
  3. 按键事件的准确性:

    • 通过在 KEY_RELEASE 状态中处理单击判断,系统能够更准确地区分单击和双击事件,避免误判。这种设计也确保了用户的输入被正确识别,不会因为按键的抖动或其他原因导致错误的事件触发。

问题二:

可以在其他状态中判断单击,具体取决于你想实现的逻辑和系统响应的需求。例如:

  • KEY_SHORTPRESS状态下判断单击:
    在这个状态下,系统已经确认了按键的短按行为。此时你可以根据 singleClickNum 的值直接判断是否是单击,并在判断后返回相应的按键码值。这样做的好处是避免了在释放状态下做出判断,逻辑更为简洁。缺点是需要增加一个状态用来处理单击逻辑。

  • KEY_CONFIRM状态下判断单击:
    虽然一般不建议在按键消抖确认状态下直接判断单击,但在特定应用场景中,如果消抖时间窗口较短且对单击检测要求不高,也可以尝试在这个状态下处理单击逻辑。

总体而言,关键是根据应用场景选择在哪个状态进行单击判断,以确保系统的响应速度和按键事件的准确性。

  • 如果用户可能会进行双击操作,那么在 KEY_SHORTPRESS 状态中立即返回单击码值,因为返回单码值后会清零点击的次数和点击的初始时间,这样就会失去处理双击的机会。因此,设计上需要额外增加逻辑来处理双击。

总结:第一次单击需要等待一段时间,用于区分单击和双击,不能第一次单击后立马判断。最后在单击动作执行的下一个状态去判断,这样即等待,而且没有延迟。
在第一次按下的一瞬间会记录按下时间,此时立即判断是否为单击,没有等待一会儿(双击时间窗),如何判断后续是否还有点击呢?不像双击,一双击完,不用管后续是否还有点击。

  1. 在 KEY_CONFIRM 状态判断
    你可以在按键消抖确认的 KEY_CONFIRM 状态结束后,立即判断按键是否符合单击条件并返回结果。

优点:

消抖后直接判断按键状态,减少状态转换的复杂性。
缺点:

需要引入额外的逻辑来区分是单击还是双击操作。如果用户快速双击,可能无法准确检测到第二次按下,从而导致误判。
3. 在 KEY_RELEASE 状态判断(当前方式)
当前的设计是利用 KEY_RELEASE 状态来处理短按和双击的判断,这是一种常见且有效的设计方式。

优点:

充分利用 KEY_RELEASE 状态的等待时间,能够准确判断用户的操作(单击还是双击),并在最后阶段决定返回什么码值。
逻辑清晰,容易理解和维护。
缺点:

状态之间的逻辑跳转较多,但通过良好的注释可以很好地解释流程。
4. 合并逻辑,减少状态
你可以尝试将 KEY_SHORTPRESS 和 KEY_RELEASE 的逻辑合并,使得在按键松开后立即判断是单击还是双击。

优点:

可以减少状态的数量,简化状态机的设计。
缺点:

可能导致状态逻辑过于复杂,难以维护。在高频率操作下,可能会出现错误判断。
总结
在按键的状态机设计中,选择在哪个状态判断短按取决于你需要实现的功能复杂度和代码的易读性。通常情况下,在 KEY_RELEASE 状态判断短按和双击是较为常见的设计,因为它能有效地管理单击和双击的事件。

如果你想在其他状态进行判断,需要额外小心处理状态转移和事件管理,以确保不会因为过早或过晚的判断导致按键事件的误判。当前的设计已经很好地平衡了复杂度和功能性,如果没有特殊需求,不建议做大的调整。

分析该状态机:

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

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

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

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

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:在短按状态下,按键持续按住超过长按时间,进入长按状态,松开后返回长按事件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值