蓝桥杯嵌入式第15届省赛真题---按键状态机

发布于:2025-03-12 ⋅ 阅读:(10) ⋅ 点赞:(0)

####写这个的原因是因为,看了某个博主,他说他使用按键状态机来实现按键检测的。

所以今天我们也来试一试按键检测。

准备

开发环境 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)
    {
            ....
    }
}