####写这个的原因是因为,看了某个博主,他说他使用按键状态机来实现按键检测的。
所以今天我们也来试一试按键检测。
准备
开发环境 Keil5 + STM32Cubemx 经典配置
1.使用的开发板为 : STM32G431RBT6 蓝桥杯指定开发板
2.Serial DeBugged 串口仿真器
3.定时器TIM6 配置为 PSC:800 - 1 ARR: 1000 - 1 时钟为 Internal Clock 内部时钟
目的是为了使得定时器为10ms 进入一次中断进行一次处理。 同时也是为了定时器来消除抖动。
4.四个按键 分别命名为 B1,B2,B3,B4。
想法
毕竟是写状态机,所以我就选择了使用switch{case: ;}语句 + enum 枚举类型来写状态机。
思想是为了规范程序代码,创造出 drivers ---- dsp ---- app ,模块化代码。 毕竟是比赛,所以尽量往简便的方面写,因为是看结果给分。
我主要拆解一下具体流程。
[引脚低电平]
IDLE ────────────────> DEBOUNCE(开始消抖计数)
↑ |
│ | 消抖未通过
│ ↓
└─────────── IDLE
[引脚高电平] |
| 消抖通过
↓
PRESSED(置位事件标志)
│
│ [引脚高电平]
↓
IDLE
这个是单个按键检测的流程,那么多个按键检测呢? 我们加入enum 相应的枚举数组记录状态和各个数据(这些数据是封装起来的,不能给用户看到。 所以我后面会创造接口来供给用户使用)。
使用for() 来遍历每个数据 ,每个数据都走一遍这个流程。
代码部分
KEY.h
#ifndef _KEY_h
#define _KEY_h
/*
这里最好是自己定义一个头文件 可以包含其他的所有头文件
这样 就会方便很多。
具体参考b站up : 我的代码没问题
*/
#include "headfile.h"
/*
uwTick --- 1ms
*/
#define KEY_NUM 4 //定义一个宏 按键的数量
#define DEBOUNCE_CNT 2 //抖动计数 2 * 10 = 20ms
#define LONG_PRESS_THRESH 1000 // 长按判断阈值(ms)
/* 枚举按键状态类型 */
typedef enum
{
KEY_STATE_IDLE, // 未按下状态
KEY_STATE_DEBOUNCE, //按键抖动状态
KEY_STATE_PRESSED, // 按键被按下状态
}KeyState;
/* 按键事件类型 不要搞混,与KeyState不是一个东西*/
typedef enum
{
KEY_EVT_PRESS, //按下事件
KEY_EVT_RELEASE, //释放事件
KEY_EVT_LONG_PRESS // 长按事件
}KeyEventType;
/* 按键事件结构体 包含按键事件类型 与 事件持续时间*/
typedef struct
{
KeyEventType type;
uint32_t duration; //事件持续时间(ms)
}KeyEvent;
// 增强按键结构体
typedef struct {
GPIO_TypeDef* GPIOx; /*根据你定义的引脚来定义*/
uint16_t GPIO_Pin;
KeyState state;
uint32_t pressStart; // 按下开始时间
uint32_t pressDuration; // 当前按压持续时间
uint8_t eventFlag; // 事件标志
KeyEvent event; // 事件详情
} KeyHandle;
void Keys_StateMachine_Handler(void); //放到定时器中断回调函数来处理
/* 以下函数 根据需求来使用 */
/* 获取当前按压持续时间 */
uint32_t Get_KeyPressDuration(uint8_t KeyID);
// 事件轮询函数
KeyEvent Poll_KeyEvent(uint8_t keyID);
/* 获取按键事件 */
uint8_t Get_Key(uint8_t KeyID);
#endif
KEY.C
#include "KEY.h"
// 初始化 按键 不可以被外界文件调用 或者更改
static KeyHandle Keys[KEY_NUM] =
{
{B1_GPIO_Port,B1_Pin,KEY_STATE_IDLE,0,0,0,{KEY_EVT_PRESS, 0}},
{B2_GPIO_Port,B2_Pin,KEY_STATE_IDLE,0,0,0,{KEY_EVT_PRESS, 0}},
{B3_GPIO_Port,B3_Pin,KEY_STATE_IDLE,0,0,0,{KEY_EVT_PRESS, 0}},
{B4_GPIO_Port,B4_Pin,KEY_STATE_IDLE,0,0,0,{KEY_EVT_PRESS, 0}},
};
/* 主要的逻辑处理部分 */
void Keys_StateMachine_Handler(void)
{
static uint8_t debounceCnt[KEY_NUM] = {0};
uint32_t now = HAL_GetTick(); // 获取tick时间
uint8_t i;
/* 遍历我们想要扫描的按键 */
for(i = 0;i<KEY_NUM;i++)
{
uint8_t pinState = HAL_GPIO_ReadPin(Keys[i].GPIOx,Keys[i].GPIO_Pin); //读取按键状态
switch(Keys[i].state)
{
/* 状态为: 未按下的时候 */
case KEY_STATE_IDLE:
//如果按下
if(pinState == GPIO_PIN_RESET)
{
Keys[i].state = KEY_STATE_DEBOUNCE; //第一次按下的时侯
debounceCnt[i] = 0;
Keys[i].pressStart = now; //记录下初始按下时间
}
break;
/* 抖动处理 */
case KEY_STATE_DEBOUNCE:
if (pinState == GPIO_PIN_RESET) { // 持续检测按下状态
if (++debounceCnt[i] >= DEBOUNCE_CNT) {
Keys[i].state = KEY_STATE_PRESSED;
Keys[i].event.type = KEY_EVT_PRESS;
Keys[i].event.duration = 0;
Keys[i].eventFlag = 1;
debounceCnt[i] = 0;
}
} else { // 中途释放则回到IDLE
Keys[i].state = KEY_STATE_IDLE;
debounceCnt[i] = 0;
}
break;
case KEY_STATE_PRESSED:
/* 持续更新按压时间 */
Keys[i].pressDuration = now - Keys[i].pressStart; // now 不断在更新
if(Keys[i].pressDuration >= LONG_PRESS_THRESH)
{
Keys[i].event.type = KEY_EVT_LONG_PRESS; /*长时间按下*/
Keys[i].event.duration = Keys[i].pressDuration; //获取按下时间
Keys[i].eventFlag = 1;
Keys[i].pressStart = now; // 重置计时避免重复触发
}
if(pinState == GPIO_PIN_SET)
{
Keys[i].state = KEY_STATE_IDLE;
Keys[i].event.type = KEY_EVT_RELEASE; /*按键事件为放开*/
Keys[i].event.duration = now - Keys[i].pressDuration;
Keys[i].eventFlag = 1;
}
break;
}
}
}
/* 获取当前按压持续时间 */
uint32_t Get_KeyPressDuration(uint8_t KeyID)
{
if(KeyID >= KEY_NUM) return 0; // 超出范围
return Keys[KeyID].pressDuration; // 返回持续时间
}
/* 事件轮询函数 KeyID 是根据你的初始化的Keys[]的按键数 以及顺序来确定
例如:我想读取按键1的事件状态 我直接给Poll_KeyEvent(0) 注意按数组的顺序来实现。
*/
KeyEvent Poll_KeyEvent(uint8_t keyID)
{
KeyEvent evt = {KEY_EVT_NONE, 0};
if(keyID >= KEY_NUM) return evt;
if(Keys[keyID].eventFlag){
evt = Keys[keyID].event;
Keys[keyID].eventFlag = 0;
// 重置持续时间(释放事件后)
if(evt.type == KEY_EVT_RELEASE){
Keys[keyID].pressDuration = 0;
}
}
return evt;
}
/* 获取按键是否被按下 B1 --- KeyID - 0 */
uint8_t Get_Key(uint8_t KeyID)
{
if(KeyID >= KEY_NUM ) return 0;
if(Keys[KeyID].eventFlag)
{
Keys[KeyID].eventFlag = 0;
return 1;
}
else
{
return 0;
}
}
main.c
/* TIM定时器处理部分 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
/* 处理按键 100hz */
if(htim->Instance == TIM6)
{
Keys_StateMachine_Handler();
}
}
上面是我进行了封装的,可以直接调用使用,就是你直接复制到自己的文件里就可以使用了。
具体使用例子
注意,我里面的逻辑就好了,那些具体的任务呀,函数呀就不要在意了。
/* 按键轮询部分 逻辑处理
按键处理
*/
void Key_While()
{
/*选择参数 0 --- PD 1 --- PH 2 --- PX*/
static uint8_t ChossePara = 0;
static uint8_t ChossePage = 0;
KeyEvent KEYS[4];
uint8_t i;
for(i = 0;i<4;i++)
{
KEYS[i] = Poll_KeyEvent(i);
}
/*选择背景 0 --- Data page 1 --- Para Page 2 --- count Page*/
/* B1 按键判断 */
if(KEYS[0].type == KEY_EVT_PRESS)
{
switch(mypara)
{
case PARA_PD:
PD = (PD >= 1000) ? 1000 : (PD + 100); // 增加时上限锁定
break;
case PARA_PH:
PH = (PH >= 10000) ? 10000 : (PH + 100);
break;
case PARA_PX:
PX = (PX >= 1000) ? 1000 : (PX + 100);
break;
default:
break;
}
}
else if(KEYS[1].type == KEY_EVT_PRESS)
{
switch(mypara)
{
case PARA_PD:
PD = (PD <= 100) ? 100 : (PD - 100); // 增加时上限锁定
break;
case PARA_PH:
PH = (PH <= 1000) ? 1000 : (PH - 100);
break;
case PARA_PX:
PX = (PX <= -1000) ? -1000 : (PX - 100);
break;
default:
break;
}
}else if(KEYS[2].type == KEY_EVT_PRESS)
{
switch(Key3page)
{
/* 参数界面 */
case LCD_SHOW_PARA_PAGE:
/*判断ChossePara的大小,然后进入对应的 选择。 就是预先判断下一次的按键按下,这样我们就可以直接进入下一个选择*/
if(0 == ChossePara)
{
mypara = PARA_PD;
ChossePara++;
}else if(1 == ChossePara)
{
mypara = PARA_PH;
ChossePara++;
}else if(2 == ChossePara)
{
mypara = PARA_PX; //用于参数选择
ChossePara = 0;
}
break;
case LCD_SHOW_DATA_PAGE_FREQUENCE:
break; /* 待处理 */
default:
break;
}
}else if(KEYS[3].type == KEY_EVT_PRESS) //按键4
{
if(0 == ChossePage)
{
LCD_Clear(Black);
mypage = LCD_SHOW_DATA_PAGE_FREQUENCE; /*每次从记录界面进入到数据界面 默认为进入频率界面*/
Key3page = LCD_SHOW_DATA_PAGE_FREQUENCE;
ChossePage++;
}else if(1 == ChossePage)
{
LCD_Clear(Black);
mypage = LCD_SHOW_PARA_PAGE;
Key3page = LCD_SHOW_PARA_PAGE;
mypara = PARA_PD; /*从数据*/
ChossePage++;
}else if(2 == ChossePage)
{
LCD_Clear(Black);
mypage = LCD_SHOW_COUNT_PAGE;
Key3page = LCD_SHOW_COUNT_PAGE;
ChossePage = 0;
}
}
}
while(1)
{
Key_While(); //直接调用
/* 其他任务 */
}
简化成模块:
void KEY_WHILE(void)
{
KeyEvent KEYS[KEY_NUM]; //用来临时存放KEY_NUM个按键事件
uint8_t i;
for(i = 0; i < KEY_NUM ; i++)
{
KEYS[i] = Poll_KeyEvent(i);
}
/*
这里的状态 可以根据你自己的需求来选择
*/
if(KEYS[0].type == KEY_EVT_PRESS)
{
/* 你的任务 */
}else if(KEYS[1].type == KEY_EVT_PRESS)
{
/* 你的任务 */
}
....
else if(KEYS[KEY_NUM-1].type == KEY_EVT_PRESS)
{
....
}
}