在上一章节“【蓝桥杯嵌入式】状态机按键”中使用的按键程序中,按键间具有优先性(如B1按下后,B2~B4的按下将不会被识别),同时不支持双键、长短按。在这一章节中,我们升级一下,采用独立的按键检测,并实现下长短按。
一、按键的独立化
我们针对每一个按键都进行了独立的状态检测和处理,定义了一个结构体,一个数组用来存储按键的不同信息。
typedef enum
{
IDLE, // 未按下,空闲状态
PRESSED_READY, // 按下,待执行相应功能
PRESSED_FINISH, // 按下,功能已执行,等待松开按键回到空闲状态
PRESSED_COUNT // 按下,处于计时状态(用于长短按)
} ButtonState; // 按键状态
// 定义一个函数类型
typedef void (*Function)(void);
typedef struct
{
GPIO_TypeDef *port; // 按键对应的GPIO端口
uint16_t pin; // 按键对应的引脚
ButtonState key_state; // 按键的状态(空闲、按下、计时等)
unsigned char long_mode; // 按键长按模式(用于标记长按模式,1为长短按键)
unsigned int pressed_time; // 按键按下的持续时间(用于长按计时)
Function keyFunction; // 按键短按功能对应的函数指针
Function keyFunctionLong; // 按键长按功能对应的函数指针
} key_GPIO; // 按键对应信息
// 按键对应的IO口信息及功能回调函数
key_GPIO key[4];
函数指针允许我们将函数的地址存储在结构体的成员中,以便在需要时调用相应的函数。我们定义了函数指针类型Function
,用于存储按键的短按功能和长按功能对应的函数地址。在初始化 key_GPIO
结构体数组时,为每个按键的 keyFunction
和 keyFunctionLong
成员赋值为相应的函数名。这样,每个按键的结构体中就包含了对应的函数指针,可以根据按键的状态调用相应的功能函数。
// 按键对应的IO口信息及功能回调函数
key_GPIO key[4] = {
{GPIOB,GPIO_PIN_0,IDLE,0,0,function_B1},
{GPIOB,GPIO_PIN_1,IDLE,0,0,function_B2},
{GPIOB,GPIO_PIN_2,IDLE,0,0,function_B3},
{GPIOA,GPIO_PIN_0,IDLE,1,0,function_B4,function_B4_long}
};
按键的功能与上一章节基本一致,只是把B4按键改成了一个长短按键,短按:翻转LD7状态,长按:熄灭LD1~LD7。
// 按键B1的功能实现
void function_B1()
{
led_addr = led_addr | 0x03; // 更新LED位置
led_disp(led_addr); // 控制LED显示
}
// 按键B2的功能实现
void function_B2()
{
led_addr = led_addr | 0x0C; // 更新LED位置
led_disp(led_addr); // 控制LED显示
}
// 按键B3的功能实现
void function_B3()
{
led_addr = led_addr ^ 0x30; // 更新LED位置
led_disp(led_addr); // 控制LED显示
}
// 按键B4的功能实现
void function_B4()
{
led_addr = led_addr ^ 0x40; // 更新LED位置
led_disp(led_addr); // 控制LED显示
}
// 按键B4长按的功能实现
void function_B4_long()
{
led_addr = led_addr & 0x80; // 更新LED位置
led_disp(led_addr); // 控制LED显示
}
二、状态机程序
与之前的状态机类似,检测都是满足一定条件就进入下一个状态
检测按键当前的状态(key_state
):
- 如果是空闲状态(
IDLE
),检测是否有按键按下,若有则将状态切换为PRESSED_READY
。 - 如果是按下待执行状态(
PRESSED_READY
),继续检测按键是否仍然被按下,根据按键类型执行相应的操作。 - 如果是按下已执行状态(
PRESSED_FINISH
),检测按键是否已经松开,若是则将状态切换回IDLE
。
不同的是:
1、每个按键都有各自的状态标签,每次根据不同按键的状态执行对应的步骤,实现独立化(按下B1不松开执行B1功能,再按下B2(此时B1、B2都处于按下状态)能够执行B2功能);
2、B4为长短按功能,执行长短按检测流程:
- 如果按键处于空闲状态(IDLE),检测是否被按下,如果按下则切换到按下待执行状态(PRESSED_READY)。
- 如果按键处于按下待执行状态(PRESSED_READY),检测是否仍然被按下:
- 如果按键仍然被按下:
- 如果按键为长按键(long_mode为1),则切换到计时状态(PRESSED_COUNT)进行长按计时;
- 如果按键松开(抖动),则切换回空闲状态(IDLE)。
- 如果按键仍然被按下:
- 如果按键处于计时状态(PRESSED_COUNT),对按键进行长按计时,当计时达到长按阈值时执行长按功能,并切换到按下已执行状态(PRESSED_FINISH)。如果未达阈值但按键松开,按下时间小于短按阈值,则执行短按功能并切换回空闲状态(IDLE)。
- 如果按键处于按下已执行状态(PRESSED_FINISH),检测按键是否松开,如果松开则切换回空闲状态(IDLE)。
unsigned int i = 0; // 声明变量 i,用于循环计数
// 遍历按键数组
for(i = 0 ; i < 4 ; i++)
{
switch(key[i].key_state)
{
case IDLE:
// 如果按键被按下,切换到 PRESSED_READY 状态
if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_RESET)
{
key[i].key_state = PRESSED_READY;
}
break;
case PRESSED_READY:
// 如果按键仍然被按下,根据按键类型执行相应的功能
if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_RESET)
{
if(key[i].long_mode == 1)
{ // 长按键,需要计时
key[i].key_state = PRESSED_COUNT;
}
else
{ // 其他按键直接执行功能并切换状态到 PRESSED_FINISH
key[i].keyFunction();
key[i].key_state = PRESSED_FINISH;
}
}
else
{
// 按键松开,切换回 IDLE 状态
key[i].key_state = IDLE;
}
break;
case PRESSED_FINISH:
// 检测按键是否松开,如果是则切换回 IDLE 状态
if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_SET)
{
key[i].key_state = IDLE;
}
break;
case PRESSED_COUNT:
// 对长按键进行计时
key[i].pressed_time ++;
if(key[i].pressed_time >= 100)
{
// 如果长按键计时达到阈值,则执行长按功能并切换状态到 PRESSED_FINISH
key[i].keyFunctionLong();
key[i].pressed_time = 0;
key[i].key_state = PRESSED_FINISH;
}
else if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_SET)
{
// 如果长按键松开,根据长按时间决定是否执行短按功能,并切换回 IDLE 状态
if(key[i].pressed_time <= 50)
{
key[i].keyFunction();
}
key[i].pressed_time = 0;
key[i].key_state = IDLE;
}
break;
default:
// 默认情况下切换回 IDLE 状态
key[i].key_state = IDLE;
break;
}
三、整体程序
interrupt.c中的完整代码如下:
#include "interrupt.h" // 包含中断处理相关的头文件
#include "led.h" // 包含LED控制相关的头文件
void function_B1(void); // 函数声明:按键B1的功能
void function_B2(void); // 函数声明:按键B2的功能
void function_B3(void); // 函数声明:按键B3的功能
void function_B4(void); // 函数声明:按键B4短按的功能
void function_B4_long(void); // 函数声明:按键B4长按的功能
static unsigned char led_addr = 0x00; // 用于记录亮灯的位置
typedef enum
{
IDLE, // 未按下,空闲状态
PRESSED_READY, // 按下,待执行相应功能
PRESSED_FINISH, // 按下,功能已执行,等待松开按键回到空闲状态
PRESSED_COUNT // 按下,处于计时状态(用于长短按)
} ButtonState; // 按键状态
// 定义一个函数类型
typedef void (*Function)(void);
typedef struct
{
GPIO_TypeDef *port; // 按键对应的GPIO端口
uint16_t pin; // 按键对应的引脚
ButtonState key_state; // 按键的状态(空闲、按下、计时等)
unsigned char long_mode; // 按键长按模式(用于标记长按模式,1为长短按键)
unsigned int pressed_time; // 按键按下的持续时间(用于长按计时)
Function keyFunction; // 按键短按功能对应的函数指针
Function keyFunctionLong; // 按键长按功能对应的函数指针
} key_GPIO; // 按键对应信息
// 按键对应的IO口信息及功能回调函数
key_GPIO key[4] = {
{GPIOB,GPIO_PIN_0,IDLE,0,0,function_B1},
{GPIOB,GPIO_PIN_1,IDLE,0,0,function_B2},
{GPIOB,GPIO_PIN_2,IDLE,0,0,function_B3},
{GPIOA,GPIO_PIN_0,IDLE,1,0,function_B4,function_B4_long}
};
// 按键B1的功能实现
void function_B1()
{
led_addr = led_addr | 0x03; // 更新LED位置
led_disp(led_addr); // 控制LED显示
}
// 按键B2的功能实现
void function_B2()
{
led_addr = led_addr | 0x0C; // 更新LED位置
led_disp(led_addr); // 控制LED显示
}
// 按键B3的功能实现
void function_B3()
{
led_addr = led_addr ^ 0x30; // 更新LED位置
led_disp(led_addr); // 控制LED显示
}
// 按键B4的功能实现
void function_B4()
{
led_addr = led_addr ^ 0x40; // 更新LED位置
led_disp(led_addr); // 控制LED显示
}
// 按键B4长按的功能实现
void function_B4_long()
{
led_addr = led_addr & 0x80; // 更新LED位置
led_disp(led_addr); // 控制LED显示
}
// 定时器中断处理函数,每10ms执行一次
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static unsigned int tim4_count = 0; // 用于TIM4计数,实现分时复用
// 判断中断来源是否是TIM4
if(htim->Instance == TIM4)
{
unsigned int i = 0; // 声明变量 i,用于循环计数
// 遍历按键数组
for(i = 0 ; i < 4 ; i++)
{
switch(key[i].key_state)
{
case IDLE:
// 如果按键被按下,切换到 PRESSED_READY 状态
if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_RESET)
{
key[i].key_state = PRESSED_READY;
}
break;
case PRESSED_READY:
// 如果按键仍然被按下,根据按键类型执行相应的功能
if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_RESET)
{
if(key[i].long_mode == 1)
{ // 长按键,需要计时
key[i].key_state = PRESSED_COUNT;
}
else
{ // 其他按键直接执行功能并切换状态到 PRESSED_FINISH
key[i].keyFunction();
key[i].key_state = PRESSED_FINISH;
}
}
else
{
// 按键松开,切换回 IDLE 状态
key[i].key_state = IDLE;
}
break;
case PRESSED_FINISH:
// 检测按键是否松开,如果是则切换回 IDLE 状态
if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_SET)
{
key[i].key_state = IDLE;
}
break;
case PRESSED_COUNT:
// 对长按键进行计时
key[i].pressed_time ++;
if(key[i].pressed_time >= 100)
{
// 如果长按键计时达到阈值,则执行长按功能并切换状态到 PRESSED_FINISH
key[i].keyFunctionLong();
key[i].pressed_time = 0;
key[i].key_state = PRESSED_FINISH;
}
else if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_SET)
{
// 如果长按键松开,根据长按时间决定是否执行短按功能,并切换回 IDLE 状态
if(key[i].pressed_time <= 50)
{
key[i].keyFunction();
}
key[i].pressed_time = 0;
key[i].key_state = IDLE;
}
break;
default:
// 默认情况下切换回 IDLE 状态
key[i].key_state = IDLE;
break;
}
}
// 每500ms进行一次灯的移位
tim4_count++; // TIM4计数递增
if(tim4_count == 50)
{
tim4_count = 0; // 重置计数变量,很重要
led_addr = led_addr ^ 0x80; // 位异或操作
led_disp(led_addr); // LED显示函数,根据新的LED位置控制LED灯亮灭
}
}
}
unsigned int i = 0; // 声明变量 i,用于循环计数
// 遍历按键数组
for(i = 0 ; i < 4 ; i++)
{
switch(key[i].key_state)
{
case IDLE:
// 如果按键被按下,切换到 PRESSED_READY 状态
if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_RESET)
{
key[i].key_state = PRESSED_READY;
}
break;
case PRESSED_READY:
// 如果按键仍然被按下,根据按键类型执行相应的功能
if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_RESET)
{
if(key[i].long_mode == 1)
{ // 长按键,需要计时
key[i].key_state = PRESSED_COUNT;
}
else
{ // 其他按键直接执行功能并切换状态到 PRESSED_FINISH
key[i].keyFunction();
key[i].key_state = PRESSED_FINISH;
}
}
else
{
// 按键松开,切换回 IDLE 状态
key[i].key_state = IDLE;
}
break;
case PRESSED_FINISH:
// 检测按键是否松开,如果是则切换回 IDLE 状态
if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_SET)
{
key[i].key_state = IDLE;
}
break;
case PRESSED_COUNT:
// 对长按键进行计时
key[i].pressed_time ++;
if(key[i].pressed_time >= 100)
{
// 如果长按键计时达到阈值,则执行长按功能并切换状态到 PRESSED_FINISH
key[i].keyFunctionLong();
key[i].pressed_time = 0;
key[i].key_state = PRESSED_FINISH;
}
else if(HAL_GPIO_ReadPin(key[i].port, key[i].pin) == GPIO_PIN_SET)
{
// 如果长按键松开,根据长按时间决定是否执行短按功能,并切换回 IDLE 状态
if(key[i].pressed_time <= 50)
{
key[i].keyFunction();
}
key[i].pressed_time = 0;
key[i].key_state = IDLE;
}
break;
default:
// 默认情况下切换回 IDLE 状态
key[i].key_state = IDLE;
break;
}
四、长短按演示效果
【蓝桥杯嵌入式】独立按键长短按
更多蓝桥杯嵌入式的精彩内容敬请期待,觉得本内容有用的话,点赞收藏加关注支持一下吧!