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

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

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

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

分析该状态机:

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

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

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

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

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:在短按状态下,按键持续按住超过长按时间,进入长按状态,松开后返回长按事件。
  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以为您提供一份基于状态机的FreeRTOS独立按键实现单击双击功能的程序,代码如下: ```c // 定义按键状态机的状态 typedef enum { IDLE, // 空闲状态 PRESSED, // 按下状态 RELEASED, // 松开状态 DOUBLE, // 双击状态 LONG // 按状态 } key_state_t; // 定义按键状态机的事件 typedef enum { PRESS_EVENT, // 按下事件 RELEASE_EVENT // 松开事件 } key_event_t; // 定义按键状态机的状态转移表 key_state_t key_state_transition[key_state_t][2] = { /* PRESS_EVENT RELEASE_EVENT */ /*IDLE*/{PRESSED, IDLE}, /*PRESSED*/{PRESSED, RELEASED}, /*RELEASED*/{DOUBLE, IDLE}, /*DOUBLE*/{IDLE, IDLE}, /*LONG*/{IDLE, IDLE} }; // 定义按键状态机的状态转移函数 key_state_t key_state_transfer(key_state_t current_state, key_event_t event) { return key_state_transition[current_state][event]; } // 定义按键状态结构体 typedef struct { key_state_t state; // 当前状态 TickType_t press_time; // 按下时间 uint8_t count; // 计数器 } key_t; // 定义按键检测任务函数 void key_task(void *pvParameters) { key_t key = {IDLE, 0, 0}; // 初始化按键状态 while (1) { if (gpio_get_level(KEY_PIN) == KEY_DOWN_LEVEL) { // 如果按键被按下 key.state = key_state_transfer(key.state, PRESS_EVENT); // 状态转移 key.press_time = xTaskGetTickCount(); // 记录按下时间 } else { // 如果按键被松开 key.state = key_state_transfer(key.state, RELEASE_EVENT); // 状态转移 } switch (key.state) { case PRESSED: if (xTaskGetTickCount() - key.press_time >= DOUBLE_CLICK_TIME) { key.count = 0; key.state = LONG; printf("long press\n"); } break; case RELEASED: if (key.count == 0) { key.count++; key.press_time = xTaskGetTickCount(); } else if (key.count == 1) { if (xTaskGetTickCount() - key.press_time <= DOUBLE_CLICK_TIME) { key.count++; printf("double click\n"); } else { key.count = 0; } } break; default: break; } vTaskDelay(10 / portTICK_PERIOD_MS); // 任务挂起10ms } } ``` 在这个程序中,我们使用了一个状态机实现按键单击双击功能按键状态机有五个状态:空闲状态、按下状态、松开状态、双击状态和按状态。按键状态机有两种事件:按下事件和松开事件。按键状态机的状态转移表定义了每个状态在接收不同事件时的下一个状态。按键状态机的状态转移函数根据当前状态和事件来确定下一个状态。按键状态结构体包含了当前状态、按下时间和计数器。按键检测任务会不断地检测按键状态,并根据当前状态和计数器来判断单击双击按事件。 需要注意的是,在这个程序中,我们使用了FreeRTOS的延时函数vTaskDelay()来挂起任务。如果您的FreeRTOS版本不支持延时函数,可以使用vTaskDelayUntil()函数来实现任务的延时。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值