STM32状态机与定时器中断在按键单双击及长按判断中的应用:便捷移植指南
先决知识
阅读本文章前,为保证阅读体验,你应该至少知道以下知识点:
- 函数指针
- 状态机
- 定时器中断
函数指针
顾名思义,函数指针就是一个指向函数的指针。关于指针的知识点涉及太多,这里只粗略讲讲。指针可以看成是一个地址的别名,因此也有“指针即地址”的说法。至于指针的大小,根据程序运行的操作平台不同而不同,这里主要讲讲为什么不同。前文提及过,指针指向的是一个地址,那么以STM32为例,其内部寄存器都是32位的,也即寻址范围是2^32 Byte=4 G,或者说整个系统总共有4G个字节,那么要给每个字节都分配一个地址的话,就需要4G个不同的地址,所以需要一个变量的存储范围能够达到2^32,那么多少bit的变量能够有这么大的范围,答案很明显,32位大小的变量就能够表示0~2^32-1的范围,所以指针大小是4字节。同样你也能够知道,为什么64位的平台的指针大小是8字节。这一点可以通过使用`sizeof`来确认。
回到正文,函数指针也即指针,其定义通常有两种方式。如下。
//第一种定义方式
void (*func_ptr)(void);
//第二种定义方式
typedef void (*func)(void);
func func_ptr;
以上两种方式定义的`func_ptr`是等效的,第一个void表示函数的返回类型,第二个void表示函数参数,此时函数参数不需要带参数名。你必须确保除了函数返回值和函数参数和上面给出的不同外,其他必须一样,尤其是`(*func_ptr)`的*符号一定不能省略,否则会变成普通函数,而不是函数指针。比如定义一个加法函数。
//第一种方式
int (*func)(int, int);
int add(int a, int b)
{
return a + b;
}
int main()
{
func = add;
int ret = func(1, 2); // ret = 3
return 0;
}
状态机和定时器中断
这两个网上的资源很多,这里不再赘述,如有需要请自行查阅。
源码分析
按键结构体
首先,我们的目的是写一份够方便,够实用的按键判断源码,那么就需要考虑到如何将按键连接的硬件引脚集成到按键变量,很明显,对于这种需要将一系列不同数据打包在一起的就是结构体了。所以想要方便移植的话,得把按键的硬件引脚打包到按键结构体中。目前我是用的是STM32的HAL库,因此我将`GPIO_Typedef*`和`uint16_t`放在了结构体里,别忘记包括HAL库的头文件。如下所示。
typedef struct _Key
{
GPIO_TypeDef *key_gpio_port; // 引脚端口,比如GPIOA
uint16_t key_gpio_pin; // 引脚号,比如GPIO_PIN_1
} Key;
硬件资源问题解决了,下面需要考虑按键如何读取按键引脚的电平。前文已经将引脚打包到了结构体中,那么在调用按键读取函数时直接将结构体作为参数即可,考虑到直接赋值拷贝的形参开销较大,这里采用传递指针的方式,如下。
typedef struct _Key
{
GPIO_TypeDef *key_gpio_port; // 引脚端口
uint16_t key_gpio_pin; // 引脚号
KeyPinState (*ReadPinState)(struct _Key *k); // 读取按键引脚电平状态的函数
} Key;
`ReadPinState`是源码中唯一的函数指针,所以只需要理解这里的目的就行了。根据前文描述,函数指针定义后还需要让其指定到具体的函数,这是按键初始化的职责,这里先不赘述。这里可能会有人疑问,既然已经使用了typedef将_Key结构体重新定义成Key了,那么函数指针的形参类型可不可以写成`Key *k`呢?这是不行的,不信你可以试试。`KeyPinState`是一个枚举,下面我们开始分析该枚举。
按键常见的操作就是单击双击和长按,因此为了方便和阅读,我们可以使用枚举来标定按键的状态。这里我定义成如下枚举。
enum _KeyState
{
KEY_NOACTION, // 按键无动作
KEY_LONG = 0x01, // 长按
KEY_CLICK = 0x02, // 单击
KEY_DOUBLE = 0x04 // 双击
};
typedef enum _KeyState KeyState;
此外,我们还需要定义另一个枚举,用来标定按键引脚的电平状态,这里定义成`KeyPinState`。判断按键状态的话,除了需要获取当前时刻的状态,还需要记录前一时刻的状态,因此我们需要定义两个状态。至此,结构体如下。
enum _KeyState
{
KEY_NOACTION, // 按键无动作
KEY_LONG = 0x01, // 长按
KEY_CLICK = 0x02, // 单击
KEY_DOUBLE = 0x04 // 双击
};
typedef enum _KeyState KeyState;
enum _KeyPinState
{
KEY_RELEASE, // 按键被释放
KEY_PRESSED // 按键被按下
};
typedef enum _KeyPinState KeyPinState;
typedef struct _Key
{
uint8_t key_mode; // 按键状态机标志
KeyState key_sate; // 按键动作响应
KeyPinState curr_state; // 按键引脚当前电平
KeyPinState prev_state; // 按键引脚上次电平
GPIO_TypeDef *key_gpio_port; // 引脚端口
uint16_t key_gpio_pin; // 引脚号
uint8_t (*ReadPinState)(struct _Key *k); // 读取按键引脚电平状态的函数
} Key;
我们还定义了一个状态机`key_mode`,不同的按键的状态机可以相同,但是当前时刻的状态肯定不会一样,这很容易理解。这里偷懒,我就只定义成`uint8_t`类型的变量来表示状态机的状态。
然后还需要记录按键被按下的时长,按键长按判断的最小按住时长。我还定义了一个按键周期变量`key_period`,表示执行按键扫描函数的周期是多少,因为按键扫描函数我是放在SysTick_Handler中执行的,周期是20ms运行一次按键扫描。至此,结构体就定义好了。
enum _KeyState
{
KEY_NOACTION, // 按键无动作
KEY_LONG = 0x01, // 长按
KEY_CLICK = 0x02, // 单击
KEY_DOUBLE = 0x04 // 双击
};
typedef enum _KeyState KeyState;
enum _KeyPinState
{
KEY_RELEASE, // 按键被释放
KEY_PRESSED // 按键被按下
};
typedef enum _KeyPinState KeyPinState;
typedef struct _Key
{
uint8_t key_mode; // 按键状态机标志,用户不可见
KeyState key_sate; // 按键动作响应,用户不可见
KeyPinState curr_state; // 按键引脚当前电平,用户不可见
KeyPinState prev_state; // 按键引脚上次电平,用户不可见
uint16_t pressing_time; // 按键持续被按下的时长,单位ms, 用户不可见
uint16_t long_min_time; // 判断长按所需的最短时长,单位ms
uint8_t key_period; // 按键执行的周期,单位ms
GPIO_TypeDef *key_gpio_port; // 引脚端口
uint16_t key_gpio_pin; // 引脚号
KeyPinState (*ReadPinState)(struct _Key *k); // 读取按键引脚电平状态的函数
} Key;
注释写着“用户不可见”的含义是,用户并不需要关系这些变量的值,因为这些变量交给按键扫描函数进行操作。因此,我又定义了一个配置结构体,用来配置按键结构体中用户需要自行初始化访问的变量。如下。
typedef struct _keyConfig
{
uint16_t long_min_time; // 判断长按所需的最短时长,单位ms
GPIO_TypeDef *key_gpio_port; // 引脚端口
uint16_t key_gpio_pin; // 引脚号
uint8_t key_period; // 检测按键的周期,单位ms
KeyPinState (*ReadPinState)(struct _Key *k); // 读取按键引脚电平状态的函数
} KeyConfig;
这很容易理解,因为STM32的标准库和HAL库都会使用一个Config结构体去配置GPIO等外设。也可以看到,配置结构体和按键结构体中的变量是一一对应的。至此,我们完成了按键结构体的完整定义。
按键初始化函数
按键初始化只需要根据配置结构体进行初始化就行。其中key_instance存储了所有已经创建的按键实例,用于执行按键扫描函数时能够方便for循环扫描所有按键实例。创建成功返回实例的指针。如下。
#define KEY_MAX_NUM 5 // 按键最多数量
static Key *key_instance[KEY_MAX_NUM];
static uint8_t key_num = 0; // 按键实例数量
/**
* @brief 初始化按键配置
*
* @param kc [in] 按键配置结构体
* @return 分配在堆上的按键实例
*/
Key *Key_Init(KeyConfig *kc)
{
Key *k = (Key *)malloc(sizeof(Key));
memset(k, 0, sizeof(Key));
k->gpio_pin = kc->gpio_pin;
k->gpio_port = kc->gpio_port;
k->long_min_time = kc->long_min_time;
k->ReadPinState = kc->ReadPinState;
k->period = kc->period;
k->double_click_interval = kc->double_click_interval;
key_instance[key_num++] = k;
return k;
}
按键读取引脚电平函数
这个函数就是结构体中的函数指针指向的真正函数。该函数需要的参数是按键结构体指针。如果你使用的不是HAL库,那么请将HAL_GPIO_ReadPin修改成你自己库的GPIO引脚读取函数。如下。
/**
* @brief 读取按键k的引脚电平状态
* @note 如果你使用的驱动库不同,请修改HAL_GPIO_ReadPin函数为真正的读取函数
*
* @param k [in] 指向按键的指针
* @return 按键被按下,返回KEY_PRESSED,否则返回KEY_RELEASE
*/
KeyPinState Key_ReadPin(Key *k)
{
if (HAL_GPIO_ReadPin(k->key_gpio_port, k->key_gpio_pin) == 0)
{
return KEY_PRESSED;
}
else
{
return KEY_RELEASE;
}
}
如果你使用的不是STM32的HAL库,请你修改HAI_GPIO_ReadPin函数为对应库的读取函数。这里也可以看到,使用枚举的好处之一是提高代码的可读性。
按键状态检查函数
这个函数由用户调用,目的是检查对应的按键状态是否存在,检查成功会清空该状态。如下。
/**
* @brief 检查按键k是否存在ks状态
*
* @param k [in] 按键k
* @param ks [in] 待检查的状态
* @return 1:按键k存在该状态
* 0:按键k不存在该状态
*/
uint8_t Key_Check(Key *k, KeyState ks)
{
if (k->key_sate & ks)
{
k->key_sate = KEY_NOACTION;
return 1;
}
return 0;
}
按键扫描函数
这个函数实现了周期判断按键的状态,利用状态机思想,能判断单击和长按,状态机转移借鉴了江协科技的,在此感谢开源。这个函数应该被定时器中断函数周期调用,周期建议为20ms即可,因为按键消抖时长一般都为20ms,使用中断周期调用就可以直接跳过消抖过程。
该函数的参数是需要判定的按键实例。首先需要根据key_mode判断状态,如果为0,表示是等待按键首次被按下的状态,一旦检测到按键被按下即按键前一状态为释放,按键当前状态为按下,那么跳转到状态1进行首次释放判断或长按判断。这里是第一次按下按键。
每次处于状态1就会累计按键被按下的时长pressing_time,如果按下时长超过了该按键的最小长按判定时长long_min_time,那么就表示长按了,标记长按标志位,并且清零状态机。如果在达到长按前释放了按键,即前一时刻按下,当前时刻释放,意味着首次按下已经被释放了,则跳到状态2进行双击窗口的等待判断。这里是第一次释放按键。
状态2时对第二次按下时长进行统计,如果第二次按下时长超过了双击间隔,意味着用户双击的间隔太长了,不应该视为双击而应该视为单击。如果第二次按下时长小于双击判定间隔,但是却又检测到按下,那么进入状态3进行双击判定。这里是第二次按下。
进入到状态3时只需要等待按键被释放就行,即判断prev == KEY_PRESSED && curr == KEY_RELEASE是否成立。不过仅靠这一个判断会有个bug,就是如果我第二按下迟迟不释放,那么会一直等待我松手的那一刻才会判定为双击,因此我加了一个并列判断,即(k->pressing_time >= k->long_min_time)。
整个函数代码如下,我上面的描述可能让你有所困惑,代码中我都添加了适当的注释,希望有帮助。代码如下。
/**
* @brief 按键扫描函数,实现按键k的扫描
*
* @param k [in] 按键实例
*
* @note 请确保该函数被周期调用,周期建议同Key结构体的period成员一致
*/
static void Key_Scan(Key *k)
{
k->prev_state = k->curr_state; // 前一时刻电平状态
k->curr_state = k->ReadPinState(k); // 当前时刻电平状态
KeyPinState prev = k->prev_state; // 取别名
KeyPinState curr = k->curr_state; // 取别名
switch (k->mode)
{
case 0: // 等待首次按下
if (prev == KEY_RELEASE && curr == KEY_PRESSED)
{
k->mode = 1; // 首次按下
k->pressing_time = 0;
}
break;
case 1: // 处理长按或首次释放
k->pressing_time += k->period;
if (k->pressing_time >= k->long_min_time)
{
k->state |= KEY_LONG; // 长按
k->mode = 0;
}
else if (prev == KEY_PRESSED && curr == KEY_RELEASE)
{
k->mode = 2; // 首次释放,进入双击等待窗口
k->pressing_time = 0;
}
break;
case 2: // 双击等待窗口
k->pressing_time += k->period;
if (k->pressing_time >= k->double_click_interval)
{
k->state |= KEY_CLICK; // 超过双击判定间隔,视为触发单击
k->mode = 0;
}
else if (prev == KEY_RELEASE && curr == KEY_PRESSED) // 检测到第二次按下
{
k->mode = 3; // 处理第二次按下
k->pressing_time = 0;
}
break;
case 3: // 处理第二次按下
k->pressing_time += k->period;
/* 这里添加(k->pressing_time >= k->long_min_time)的判断是因为如果不加该判断
那么每次双击判定都需要等待第二次按下的释放才会判定成功,加了该判断的话如果第二次按下
一直不松开的话,按下的时长超过了long_min_time也会判定为双击。
如果你需要更短的判定时长,请将k->long_min_time修改成任意想要的数字即可,比如k->pressing_time >= 200 */
if ((prev == KEY_PRESSED && curr == KEY_RELEASE) || (k->pressing_time >= k->long_min_time))
{
k->state |= KEY_DOUBLE; // 第二次释放视为触发双击
k->mode = 0;
}
// else
// {
// k->pressing_time += k->period;
// if (k->pressing_time >= k->long_min_time)
// {
// k->state |= KEY_LONG; // 第二次长按
// k->mode = 0;
// }
// }
break;
}
}
被定时器中断周期调用的函数
请确保该函数被周期调用。如下。
/**
* @brief 按键判定状态函数
*
* @param k [in] 按键数组
* @param num [in] 按键数量
*
* @note 该函数请确保被周期调用,例如在定时器中断函数里每隔20ms调用一次
*/
void Key_Tick(void)
{
for (uint8_t i = 0; i < key_num; i++)
{
Key_Scan(key_instance[i]);
}
}
主程序和定时器中断
定时器中断中需要周期调用ey_Tick函数,这里我在SysTick_Handler中调用。如下。
void SysTick_Handler(void)
{
HAL_IncTick();
uint32_t tick = HAL_GetTick();
if ((tick % 20) == 0)
Key_Tick();
}
首先你需要保证调用HAL库的GPIO初始化成功按键的硬件引脚资源。然后在主函数中初始化按键。如下。
#define KEY_PRESS_MIN_TIME_MS 500 // 按键长按判断的最小时长,只有满足这个时长才判定为长按
#define KEY_PERIOD_MS 20 // 按键执行任务的周期,目前是放在SysTick_Handler中断中以20ms周期运行
Key *key1, *key2, *key3;
LED *led1, *led2; // 这是LED实例,不在本文章讨论范围内
int main()
{
// 忽略CubeMX生成的其他初始化代码
KeyConfig kc = {
.gpio_pin = GPIO_PIN_11,
.gpio_port = GPIOB,
.period = KEY_PERIOD_MS,
.ReadPinState = Key_ReadPin,
.long_min_time = KEY_PRESS_MIN_TIME_MS,
.double_click_interval = 200};
key1 = Key_Init(&kc); // 创建key1实例
kc.gpio_pin = GPIO_PIN_1;
key2 = Key_Init(&kc); // 创建key2实例
kc.gpio_port = GPIOA;
kc.gpio_pin = GPIO_PIN_6;
key3 = Key_Init(&kc); // 创建key3实例
LEDConfig lc = {
.gpio_port = GPIOA,
.gpio_pin = GPIO_PIN_10,
.LightOn = LED_On,
.LightOff = LED_Off};
led1 = LED_Init(&lc); // LED1实例
lc.gpio_pin = GPIO_PIN_11;
led2 = LED_Init(&lc); // LED2实例
while (1)
{
if (Key_Check(key1, KEY_DOUBLE))
{
led1->LightOn(led1); // key1双击点亮LED1
}
if (Key_Check(key2, KEY_DOUBLE))
{
led2->LightOn(led2); // key2双击点亮LED2
}
if (Key_Check(key3, KEY_CLICK)) // key3单击熄灭LED
{
led1->LightOff(led1);
led2->LightOff(led2);
}
if (Key_Check(key3, KEY_LONG)) // key3长按点亮LED
{
led1->LightOn(led1);
led2->LightOn(led2);
}
}
}
最终的效果是按键1双击,LED1点亮,按键2双击,LED2点亮,按键3单击,LED全熄灭,按键3长按,LED全点亮。
其实还可以将HAL的GPIO初始化函数在按键初始化Key_Init函数中调用,做到更高程度的可移植,后面我也会更新。
行文至此,感谢阅读,欢迎斧正,在此谢过。头文件和源文件的所有内容我放在最后面。
后续规划
通过上面的分析也可知道,目前只能实现单一按键动作的响应。未来我会加入消息队列的形式和定时清空按键动作的设计,预期是保证存储10s内的所有按键动作,不过需要额外的时间戳记录和消息队列传递消息,这样大刀阔斧的设计是否会得不偿失呢?如果你也喜欢这样设计的话,希望能收藏一下这篇文章,感谢。
还有一点就是,源码中并没有实现硬件资源的初始化,起初我想把硬件资源初始化也集成在结构体中,但是考虑到按键的引脚分配可能不确定,并且初始化引脚还需要配置时钟,这样设计在结构体中似乎有点头重脚轻,因此放弃了这种设计。所以,如果你的按键没有响应的话,请先检查硬件配置是否正确。
头文件和源文件代码
头文件
#ifndef __KEY_H
#define __KEY_H
/**
* @file key.h
* @author chushang (email:chushangcs@163.com)
* @brief 按键头文件,新增按键双击判定
* 如有错误,欢迎联系邮箱,感激不尽!
* 2024-04-16:新增双击判定
* @version 1.2
* @date 2025-04-16
*
* @copyright Copyright (c) 2025
*
* @note 使用时请使用KeyConfig初始化Key
*/
#include "stm32f1xx_hal.h"
enum _KeyState
{
KEY_NOACTION, // 按键无动作
KEY_LONG = 0x01, // 长按
KEY_CLICK = 0x02, // 单击
KEY_DOUBLE = 0x04 // 双击
};
typedef enum _KeyState KeyState;
enum _KeyPinState
{
KEY_RELEASE, // 按键被释放
KEY_PRESSED // 按键被按下
};
typedef enum _KeyPinState KeyPinState;
typedef struct _Key
{
uint8_t mode; // 按键状态机标志,用户不可见
KeyState state; // 按键动作响应,用户不可见
KeyPinState curr_state; // 按键引脚当前电平,用户不可见
KeyPinState prev_state; // 按键引脚上次电平,用户不可见
uint16_t pressing_time; // 按键持续被按下的时长,单位ms, 用户不可见
uint16_t long_min_time; // 判断长按所需的最短时长,单位ms,需要用户初始化
uint16_t double_click_interval; // 双击间隔,单位ms,需要用户初始化
uint8_t period; // 按键执行的周期,单位ms,需要用户初始化
GPIO_TypeDef *gpio_port; // 引脚端口,需要用户初始化
uint16_t gpio_pin; // 引脚号,需要用户初始化
KeyPinState (*ReadPinState)(struct _Key *k); // 读取按键引脚电平状态的函数,需要用户初始化
} Key;
// 用户使用KeyConfig结构体初始化Key结构体,因此你需要确保该结构体的所有成员都是合法且有效的
typedef struct _KeyConfig
{
uint16_t long_min_time; // 判断长按所需的最短时长,单位ms
uint16_t double_click_interval; // 双击间隔
GPIO_TypeDef *gpio_port; // 引脚端口
uint16_t gpio_pin; // 引脚号
uint8_t period; // 检测按键的周期,单位ms
KeyPinState (*ReadPinState)(struct _Key *k); // 读取按键引脚电平状态的函数
} KeyConfig;
/**
* @brief 初始化按键配置
*
* @param kc [in] 按键配置结构体
* @return 分配在堆上的按键实例
*/
Key *Key_Init(KeyConfig *kc);
/**
* @brief 按键判定状态函数
*
* @param k [in] 按键数组
* @param num [in] 按键数量
*
* @note 该函数请确保被周期调用,例如在定时器中断函数里每隔20ms调用一次
*/
void Key_Tick(void);
/**
* @brief 读取按键k的引脚电平状态
* @note 如果你使用的驱动库不同,请修改HAL_GPIO_ReadPin函数为真正的读取函数
*
* @param k [in] 指向按键的指针
* @return 按键被按下,返回KEY_PRESSED,否则返回KEY_RELEASE
*/
KeyPinState Key_ReadPin(Key *k);
/**
* @brief 检查按键k是否存在ks状态
*
* @param k [in] 按键k
* @param ks [in] 待检查的状态
* @return 1:按键k存在该状态
* 0:按键k不存在该状态
*/
uint8_t Key_Check(Key *k, KeyState ks);
/**
* @brief 清空按键状态
*
* @param k [in] 按键数组
* @param num [in] 按键数量
*/
void Key_Clear(Key *k, uint8_t num);
#endif // !__KEY_H
源文件
/**
* @file key.c
* @author chushang (email:chushangcs@163.com)
* @brief 按键初始化,按键状态判定,读取按键
* 如果你使用的不是STM的HAL库,请修改Key_ReadPin函数
* 目前实现的功能是判断单击和长按
* 2025-04-15:修改了按键扫描逻辑
* 2025-04-16:新增双击判定
* @version 1.2
* @date 2025-04-16
*
* @copyright Copyright (c) 2025
*
*/
#include "key.h"
#include "stdlib.h"
#include "string.h"
#include "stm32f1xx_hal.h"
#define KEY_MAX_NUM 5 // 按键最多数量
static Key *key_instance[KEY_MAX_NUM];
static uint8_t key_num = 0; // 按键实例数量
/**
* @brief 初始化按键配置
*
* @param kc [in] 按键配置结构体
* @return 分配在堆上的按键实例
*/
Key *Key_Init(KeyConfig *kc)
{
Key *k = (Key *)malloc(sizeof(Key));
memset(k, 0, sizeof(Key));
k->gpio_pin = kc->gpio_pin;
k->gpio_port = kc->gpio_port;
k->long_min_time = kc->long_min_time;
k->ReadPinState = kc->ReadPinState;
k->period = kc->period;
k->double_click_interval = kc->double_click_interval;
key_instance[key_num++] = k;
return k;
}
/**
* @brief 按键扫描函数,实现按键k的扫描
*
* @param k [in] 按键实例
*
* @note 请确保该函数被周期调用,周期建议同Key结构体的period成员一致
*/
static void Key_Scan(Key *k)
{
k->prev_state = k->curr_state; // 前一时刻电平状态
k->curr_state = k->ReadPinState(k); // 当前时刻电平状态
KeyPinState prev = k->prev_state; // 取别名
KeyPinState curr = k->curr_state; // 取别名
switch (k->mode)
{
case 0: // 等待首次按下
if (prev == KEY_RELEASE && curr == KEY_PRESSED)
{
k->mode = 1; // 首次按下
k->pressing_time = 0;
}
break;
case 1: // 处理长按或首次释放
k->pressing_time += k->period;
if (k->pressing_time >= k->long_min_time)
{
k->state |= KEY_LONG; // 长按
k->mode = 0;
}
else if (prev == KEY_PRESSED && curr == KEY_RELEASE)
{
k->mode = 2; // 首次释放,进入双击等待窗口
k->pressing_time = 0;
}
break;
case 2: // 双击等待窗口
k->pressing_time += k->period;
if (k->pressing_time >= k->double_click_interval)
{
k->state |= KEY_CLICK; // 超过双击判定间隔,视为触发单击
k->mode = 0;
}
else if (prev == KEY_RELEASE && curr == KEY_PRESSED) // 检测到第二次按下
{
k->mode = 3; // 处理第二次按下
k->pressing_time = 0;
}
break;
case 3: // 处理第二次按下
k->pressing_time += k->period;
/* 这里添加(k->pressing_time >= k->long_min_time)的判断是因为如果不加该判断
那么每次双击判定都需要等待第二次按下的释放才会判定成功,加了该判断的话如果第二次按下
一直不松开的话,按下的时长超过了long_min_time也会判定为双击。
如果你需要更短的判定时长,请将k->long_min_time修改成任意想要的数字即可,比如k->pressing_time >= 200 */
if ((prev == KEY_PRESSED && curr == KEY_RELEASE) || (k->pressing_time >= k->long_min_time))
{
k->state |= KEY_DOUBLE; // 第二次释放视为触发双击
k->mode = 0;
}
// else
// {
// k->pressing_time += k->period;
// if (k->pressing_time >= k->long_min_time)
// {
// k->state |= KEY_LONG; // 第二次长按
// k->mode = 0;
// }
// }
break;
}
}
/**
* @brief 按键判定状态函数
*
* @param k [in] 按键数组
* @param num [in] 按键数量
*
* @note 该函数请确保被周期调用,例如在定时器中断函数里每隔20ms调用一次
*/
void Key_Tick(void)
{
for (uint8_t i = 0; i < key_num; i++)
{
Key_Scan(key_instance[i]);
}
}
/**
* @brief 读取按键k的引脚电平状态
* @note 如果你使用的驱动库不同,请修改HAL_GPIO_ReadPin函数为真正的读取函数
*
* @param k [in] 指向按键的指针
* @return 按键被按下,返回KEY_PRESSED,否则返回KEY_RELEASE
*/
KeyPinState Key_ReadPin(Key *k)
{
if (HAL_GPIO_ReadPin(k->gpio_port, k->gpio_pin) == 0)
{
return KEY_PRESSED;
}
else
{
return KEY_RELEASE;
}
}
/**
* @brief 检查按键k是否存在ks状态
*
* @param k [in] 按键k
* @param ks [in] 待检查的状态
* @return 1:按键k存在该状态
* 0:按键k不存在该状态
*/
uint8_t Key_Check(Key *k, KeyState ks)
{
if (k->state & ks)
{
k->state = KEY_NOACTION;
return 1;
}
return 0;
}
/**
* @brief 清空按键状态
*
* @param k [in] 按键数组
* @param num [in] 按键数量
*/
void Key_Clear(Key *k, uint8_t num)
{
for (int i = 0; i < num; ++i)
{
k->state = KEY_NOACTION;
}
}
作者:初商_