04 按键驱动编写

上一篇:03 LED驱动编写
下一篇:05 LCD驱动编写

作者:桂信科黄鹏老师。note部分是我添加的内容

主要内容

  1. 分析独立按键原理图并在CubeMX配好引脚。
  2. 搞懂常规的按键处理,讲解单击、双击和长按的检测方法。
  3. 在常规方法基础上增加回调函数,提高按键应用的灵活性。
  4. 采用状态机的思想设计按键处理程序和回调函数。

一、独立按键原理图分析

要从原理图中搞明白以下内容:
1) 按键由哪个些引脚读取状态?  
B1->PB0、B2->PB1、B3->PB2、B4->PA0。       
2) 按键按下和未按下的电平状态是什么?
按下->低电平;未按下->高电平。        
思考:
1) 引脚应该设置为输入还是输出?
很显然,这是外部输出电平给单片机,这几个引脚应设置为输入。
2) 引脚应该设置为下拉、上拉还是浮空?
由图可知未按下是高电平,且外部已经有上拉电阻,引脚内部可设置为上拉或浮空。万万不能是下拉,否则电平不确定。
Pasted image 20240125103438.png

二、引脚配置

参照02 CubeMX配置引脚#二、独立按键引脚完成该步骤。

三、编程思想

1. 功能需求和分析

程序需要实现识别:单击、双击和长按三种模式,并实现按键对应的功能。按键本质上是人按按键发出有规律的电平信号波形,所以识别不同模式的重点在于找出信号之间的差异。三种不同按键状态的信号波形如下:
Pasted image 20240129114938.png
差异:
单击:一段时间内出现一个负脉冲,并且负脉冲的低电平持续时间较短。
双击:一段时间内出现两个负脉冲,并且两个负脉冲的间隔时间很短。
长按:一段时间内出现一个负脉冲,并且负脉冲的低电平持续时间较长。

2. 基本思想和代码实现

1)规定按键的查询时间,比如20ms。目的是方便记录双击的间隔时间;

2)读取按键连接IO口的状态,状态为低表示对应的按键被按下,状态为高表示对应的按键没有被按下。(注意按键消抖操作);

3)检测到按键被按下后,并且短时间内被释放,不能立即返回键值,只能记录按键被按下的次数(注意该变量应该为静态变量,为什么?),到达按键分析时间后才能返回键值。目的是双击和单击的前一部分的波形是一样的,为了防止将双击误判为单击。

4)按键低电平持续时间检测可使用等待按键释放的方法,在等待时间内做计时操作。超时判断为长按,不超时判断为短按。判断为长按时可以立即返回键值。(思考:为什么长按可以立即返回键值,而段按则不能立即返回键值?

5)我们思考一个问题:对于长按而言,用户按多久才知道长按完成?长按的程序响应,应该是放在按键释放之前还是按键释放之后?(这个问题处理不好会给用户带来很不好的体验

读取按键的基本方法。

if(GPIO_PIN_RESET == HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin))
{
	HAL_Delay(20); //延时消抖
	if(GPIO_PIN_RESET == HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin))
	{
   //等待按键释放
		while(!HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin));
		return BUTTON1;
	}
}

以上代码可以实现B1按键的单击识别,但是不能实现双击和长按的识别。因此需要在此代码上进行优化,增加新的功能。实现所有功能后将优化后的代码复制4遍,更改端口号和引脚号,可实现B2,B3,B4按键的功能。

思考:
按照上述的方法代码的长度会很长,而且每个按键的代码结构都是一样的,除了端口和引脚不一样。能否将此代码进行修改,使用for循环实现呢?

答案:是可行的,将端口和引脚放在一个结构体数组中,然后使用for循环即可。

按键使用枚举定义:

//定义BUTTON编号枚举
typedef enum
{ 
	BUTTON1,
	BUTTON2,
	BUTTON3,
	BUTTON4,
	BUTTON_NUM,
}emBUTTONx;
//端口配置参数
const struct
{
  GPIO_TypeDef   *GPIOx;             //GPIO端口
  uint16_t         GPIO_Pin;          //引脚
} astBUTTON_InitStruct[BUTTON_NUM] = //初始化 
{
  {B1_GPIO_Port,B1_Pin }, //按键1引脚配置参数
  {B2_GPIO_Port,B2_Pin }, //按键1引脚配置参数
  {B3_GPIO_Port,B3_Pin }, //按键1引脚配置参数
  {B4_GPIO_Port,B4_Pin }, //按键1引脚配置参数
  //如需添加更多按键,请先对按键进行宏定义后,在参考上面修改
};

单击按键识别代码:

emBUTTONx _emButton = BUTTON_NUM; 
for(_emButton = 0;_emButton < BUTTON_NUM;_emButton++)
{
	if(GPIO_PIN_RESET == HAL_GPIO_ReadPin(astBUTTON_InitStruct[_emButton]. GPIOx, astBUTTON_InitStruct[_emButton]. GPIO_Pin))
	{
		HAL_Delay(20); //延时消抖
		if(GPIO_PIN_RESET == HAL_GPIO_ReadPin(astBUTTON_InitStruct[_emButton]. GPIOx, astBUTTON_InitStruct[_emButton]. GPIO_Pin))
		{		
			while(!HAL_GPIO_ReadPin(astBUTTON_InitStruct[_emButton]. GPIOx, astBUTTON_InitStruct[_emButton]. GPIO_Pin));//等待按键释放
			return  _emButton;
		}
	}
}

在以上代码中的按键等待释放代码中增加等待超时记录,即可实现长按检测

上述代码中的按键检测,有多个位置使用,并且太长,可以单独提取出来设计为子函数

//判断按键是否按下
static uint8_t  Button_isPressed(emBUTTONx _button)
{
  if(GPIO_PIN_RESET == HAL_GPIO_ReadPin(astBUTTON_InitStruct[_button].GPIOx, astBUTTON_InitStruct[_button].GPIO_Pin))
    return  1;
  else
    return   0;
}

上面的返回值 1和0 属于魔鬼数字,应该定义为宏或者枚举。
可以定义如下:

//按键bool变量
typedef enum
{
       B_FALSE,
       B_TRUE,
}emBUTTON_Bool;

长按处理(在单击程序上修改):

emBUTTONx _emButton = BUTTON_NUM;
for(_emButton = 0;_emButton < BUTTON_NUM;_emButton++)
{
	if(B_TRUE == Button_isPressed(_emButton))
	{
	   HAL_Delay(20); //延时消抖
	   if(B_TRUE == Button_isPressed(_emButton))
	   {
		while(Button_isPressed(_emButton));//等待按键释放
		{
			_wTime ++;
			  HAL_Delay(1);
			  if(_wTime >= BUTTON_LONG_CLICK_PERIOD)
			  {
				   break;
			  }
		}
		if(_wTime < BUTTON_LONG_CLICK_PERIOD)
		{
			  if((s_wCounter - s_wCounter_Copy  < BUTTON_DOUBLE_CLICK_PERIOD )&& (s_bButton == B_TRUE)) 
			  {
				 return  _emButton;
			  }
			  else
			   {
				   s_emButton_copy = _emButton | 0x10; //第4位为1表示长按(差异化标识)
				  return  s_emButton_copy;
			  }
		}
	}
}

完整代码见button1.c/.h

3. 稍微改进并运用回调函数做响应

编程思想其一中的方法存在使用不方便的缺陷,
1) 返回的是按键键值编码,根据按键键值编码进行操作;
2) 按键处理函数必须要固定周期执行,否则双击操作会存在问题。

以上问题的解决方案:
1) 使用回调函数,方便按键后操作执行;
2) 在定时器中断函数中执行按键处理函数,保证是固定周期执行。

3.1 配置定时器用做后台处理

为了节约定时器资源,我们使用基本定时器TIM6或者TIM7。STM32定时器的教程网上一大堆,这里仅说明重点。

翻看芯片手册可知,TIM6挂载在APB1总线
Pasted image 20240129152542.png

由CubeMX的时钟树可知定时器的频率为150MHz。
Pasted image 20240129153153.png

所以TIM6配置如下,最终效果是1ms进一次定时器6中断。
Pasted image 20240129152928.png

记得打开定时器6中断。
Pasted image 20240129153410.png

配置好定时器后,生成代码。还需要在主函数中启动定时器才行。启动定时函数为:

HAL_TIM_Base_Start_IT(&htim6); //启动定时器6并使能中断
3.2 TIM6中断的回调函数如何使用?

中断函数位于stm32g4xx_it.c中,样子如图
Pasted image 20240129153728.png
注意:这个函数是不需要修改的!我们需要修改的是定时器的回调函数。

定时器回调函数的名称是固定的,如何才能找到定时器的回调函数名称???
可以通过定时中断中可以找到定时器更新事件的回调函数
Pasted image 20240129153851.png

在回调函数中执行按键查询处理函数。

//定时器中断回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    static uint32_t time = 0;
    if(htim->Instance == TIM6)
    {
        // 自定义应用程序
        time++;
        if(time == 20)
        {
            Button_Process();
            time = 0;
        }
    }
}
3.3 按键事件的回调函数

思考:什么是回调函数?回调函数有什么好处?
回调函数要与触发事件相对应,事件一旦触发会立马自动执行被注册好了的回调函数。

首先定义一个事件对应回调函数的结构体。

//按键回调
typedef struct
{
	emBUTTONx   emButton;  //按键返回值类型
	void (*fpClickedCallBack)(void);  //按键事件回调函数
}stButtonStructTyp;

stButtonStructTyp astButtonStruct[13] =//初始化 
{
	{BUTTON1,NULL},
	{BUTTON2,NULL},
	{BUTTON3,NULL},
	{BUTTON4,NULL},
	{BUTTON_NUM,NULL},
	{BUTTON1_LONG,NULL},
	{BUTTON2_LONG,NULL},
	{BUTTON3_LONG,NULL},
	{BUTTON4_LONG,NULL},
	{BUTTON1_DOUBLE,NULL},
	{BUTTON2_DOUBLE,NULL},
	{BUTTON3_DOUBLE,NULL},
	{BUTTON4_DOUBLE,NULL},
};

回调函数实际上是预置的一个函数指针而已,需要注册函数实体才能被执行。

//按键回调函数的注册
void ButtonCallBack_Reg(emBUTTONx _button,void (*ClickedCallBack)(void))
{
	 astButtonStruct[_button].fpClickedCallBack = ClickedCallBack; //注册按键处理事件回调函数
}

按键周期处理函数的修改思想:在基本思想章节的函数中,返回键值更改为回调函数的执行,回调前需要判断函数是否为空。

if(astButtonStruct[_emButton ].fpClickedCallBack != NULL)
{
     astButtonStruct[_emButton ].fpClickedCallBack();
}

使用回调函数需要先编写函数的实体,然后进行注册才能使用。回调函数的实体在应用层编写。

以下三个函数是函数的实体

void Button1_CallBack(void)
{
     LEDx_Contr(LED1,LED_ON);
}

void BUTTON1_DOUBLE_CallBack(void)
{
     LEDx_Contr(LED2,LED_ON);
}

void BUTTON1_LONG_CallBack(void)
{
	 LEDx_Contr(LED1,LED_OFF);
	LEDx_Contr(LED2,LED_OFF);
}

以下三个是回调函数的注册

ButtonCallBack_Reg(BUTTON1,Button1_CallBack);
ButtonCallBack_Reg(BUTTON1_DOUBLE,BUTTON1_DOUBLE_CallBack);
ButtonCallBack_Reg(BUTTON1_LONG,BUTTON1_LONG_CallBack);
3.4 出BUG了!在中断调用HAL_Delay()函数出现程序卡死

将上述代码修改完成之后,下载到开发板执行,会出现程序卡死的现象。原因是在定时器中断函数中调用了HAL_Delay()函数,而SysTick的中断优先级默认比TIM6的中断优先级要低。

如何解决?
在CubeMX中修改中断优先级即可
Pasted image 20240129155552.png

4. 状态机的思想加上回调函数实现按键

之前介绍的两种按键的处理方法,对于按键的处理方法基本一致,不同的是在按键事件发生之后函数的执行过程不一样。

按键的处理过程都是按照时间顺序去执行的。本章将介绍一种新的编程思想——“状态机”的思想方法。它将按键的处理过程分为几个状态,状态之间是可以转变的,但是存在状态转变的条件。

按键的状态分为“释放状态”,“按下消抖状态”,“按下状态”,“释放消抖状态”,一共四个状态。状态转换图如下:
Pasted image 20240129160428.png

判断“单击”“双击”“长按”的方法等之前的思想一致。给有能力理解的同学去实现,所以只有完整代码,可以去问AI理解下。

代码如下,完整代码是老师给出的button3.c/.h:

#define BUTTON_PRESS_WOBBLE_TIME	 10      //按键按下抖动时间。也就是消抖时间,单位ms
#define BUTTON_REALSE_WOBBLE_TIME    10      //按键释放抖动时间。也就是消抖时间,单位ms
#define BUTTON_LONG_CLICK_PERIOD     500     //按键长按时间最小值。单位ms
#define BUTTON_DOUBLE_CLICK_PERIOD   200     //按键双击时间最大值,单位ms 

#define BUTTON_TIMING_PROCESS_TIME   20      //按键处理周期,单位ms 

//定义BUTTON编号枚举
typedef enum
{ 
	BUTTON1,
	BUTTON2,
	BUTTON3,
	BUTTON4,
	BUTTON_NUM,
}emBUTTONx;

//定义BUTTON事件枚举
typedef enum
{
	BUTTON_NONE_CLICK,
	BUTTON_CLICK,         //单击
	BUTTON_DOUBLE_CLICK,  //双击
	BUTTON_LONG_CLICK,    //长按
}emBUTTON_EVENT;

//按键处理阶段
typedef enum
{
  BUTTON_PRESSED           ,//按键按下
  BUTTON_REALSE            ,//按键松开
  BUTTON_REALSE_WOBBLE     ,//确认松开的消抖状态
  BUTTON_PRESS_WOBBLE      ,//确认按下的消抖状态
}emBUTTON_STAGEn;

 //按键bool变量
typedef enum
{
	B_FALSE,
	B_TRUE,
}emBUTTON_Bool;

 //按键处理信息变量
volatile struct
{
  void (*fpClickedCallBack)(emBUTTON_EVENT);  //按键事件回调函数
  emBUTTON_STAGEn       emStage;         //按键处理阶段
  emBUTTON_Bool         emClicked;     //按键状态是否发生改变                     
  emBUTTON_Bool         emLongClick;   //是否是长按
  uint16_t              wClickTimes;    //点击次数变量
  uint16_t              wDealTime;      //按键处理时间
  uint16_t              wPressedTime;   //按键按下时间
  uint8_t     	        byWobbleTime;    //消抖时间变量	
}astButtonStruct[BUTTON_NUM] =//初始化 
{
	{NULL,BUTTON_REALSE,B_FALSE,B_FALSE,0,0,0,0},
    {NULL,BUTTON_REALSE,B_FALSE,B_FALSE,0,0,0,0},
	{NULL,BUTTON_REALSE,B_FALSE,B_FALSE,0,0,0,0},
	{NULL,BUTTON_REALSE,B_FALSE,B_FALSE,0,0,0,0},
};

//端口配置参数
const struct
{
  GPIO_TypeDef   *GPIOx;             //GPIO端口
  uint16_t        GPIO_Pin;          //引脚
  GPIO_PinState   PressedLevel;      //按下后的出发电平
} astBUTTON_InitStruct[BUTTON_NUM] = //初始化 
{
  {B1_GPIO_Port,B1_Pin,GPIO_PIN_RESET}, //按键1引脚配置参数
  {B2_GPIO_Port,B2_Pin,GPIO_PIN_RESET}, //按键1引脚配置参数
  {B3_GPIO_Port,B3_Pin,GPIO_PIN_RESET}, //按键1引脚配置参数
  {B4_GPIO_Port,B4_Pin,GPIO_PIN_RESET}, //按键1引脚配置参数
  //如需添加更多按键,请先对按键进行宏定义后,在参考上面修改
};

//按键配置初始化
void Button_Init(emBUTTONx _button,void (*ClickedCallBack)(emBUTTON_EVENT))
{
  astButtonStruct[_button].fpClickedCallBack = ClickedCallBack; //组册按键处理事件回调函数
  astButtonStruct[_button].wClickTimes = 0;        //点击次数清零
  astButtonStruct[_button].emStage = BUTTON_REALSE;    //按键处于松开状态
  astButtonStruct[_button].emClicked = B_FALSE;     //按键状态未改变
  astButtonStruct[_button].emLongClick = B_FALSE;   //不是长按
  astButtonStruct[_button].byWobbleTime = 0;        //消抖时间初始化为0
}

//按键20ms周期处理函数
void Button_Process(emBUTTONx _button)
{
  switch(astButtonStruct[_button].emStage)
  {
  case BUTTON_REALSE:
    if(Button_isPressed(_button)) //如果按键按下
    {
      astButtonStruct[_button].emStage = BUTTON_PRESS_WOBBLE;    //进入按下消抖阶段
      astButtonStruct[_button].byWobbleTime = 0;              //消抖时间清零
      astButtonStruct[_button].wPressedTime = 0;             //按键按下时间清零
    }
    break;
  case BUTTON_PRESS_WOBBLE:
    astButtonStruct[_button].wPressedTime += BUTTON_TIMING_PROCESS_TIME;								//按键按下时间开始计时
    astButtonStruct[_button].byWobbleTime += BUTTON_TIMING_PROCESS_TIME; 								//消抖时间开始计时
    if(Button_isPressed(_button)) //如果按键按下
    {
      if(astButtonStruct[_button].byWobbleTime >= BUTTON_PRESS_WOBBLE_TIME)  //消抖时间达到
      {
        astButtonStruct[_button].emStage = BUTTON_PRESSED;         //可以确认按下,进入已按下阶段
        astButtonStruct[_button].wClickTimes ++;             //按下次数加1
      }
    }
    else
    {
      astButtonStruct[_button].emStage = BUTTON_REALSE;          //如果消抖时间内松开则认为该次按键按下无效
    }
    break;
  case BUTTON_PRESSED:
    astButtonStruct[_button].wPressedTime += BUTTON_TIMING_PROCESS_TIME;                //继续记录按键按下时间
    if( (astButtonStruct[_button].emLongClick == B_FALSE)
        &&(astButtonStruct[_button].wPressedTime >= BUTTON_LONG_CLICK_PERIOD)) //如果长时间按下,则认为是长按状态
    {
      astButtonStruct[_button].fpClickedCallBack(BUTTON_LONG_CLICK); //按键按下时间过长,则认为是长击
      astButtonStruct[_button].emLongClick = B_TRUE;          //标记为长按
    }
    else
    {
      if(Button_isPressed(_button) == B_FALSE) //如果按键松开
      {
        astButtonStruct[_button].emStage = BUTTON_REALSE_WOBBLE; //进入按键松开后消抖阶段
        astButtonStruct[_button].byWobbleTime = 0;            //消抖时间清零,一边后续阶段消抖
      }
    }
    break;
  case BUTTON_REALSE_WOBBLE:
    astButtonStruct[_button].byWobbleTime += BUTTON_TIMING_PROCESS_TIME;                 //按键松开消抖时间开始计时
    if(Button_isPressed(_button) == B_FALSE) //如果按键松开
    {
      if(astButtonStruct[_button].byWobbleTime >= BUTTON_REALSE_WOBBLE_TIME)  //消抖时间达到
      {
        astButtonStruct[_button].emStage = BUTTON_REALSE;              //进入按键完全松开阶段
        astButtonStruct[_button].emClicked = B_TRUE;             //标记按键状态发生改变
      }
    }
    break;	
    default:break;
  }
  
  if(astButtonStruct[_button].emClicked == B_TRUE)  //如果按键状态改变了
  {
    //达到双击时间最大值或已经双击
    astButtonStruct[_button].wDealTime += BUTTON_TIMING_PROCESS_TIME;
    if((astButtonStruct[_button].wDealTime >= BUTTON_DOUBLE_CLICK_PERIOD)
      ||astButtonStruct[_button].wClickTimes >= 2)
    {
      if(astButtonStruct[_button].fpClickedCallBack != NULL)
      {
        //如果按下时间很长
        if(astButtonStruct[_button].emLongClick == B_FALSE)
        {
          if(astButtonStruct[_button].wClickTimes >= 2)  //按下多次则认为是双击
            astButtonStruct[_button].fpClickedCallBack(BUTTON_DOUBLE_CLICK);
          else if(astButtonStruct[_button].wClickTimes == 1)                               //否则认为是单击
            astButtonStruct[_button].fpClickedCallBack(BUTTON_CLICK);
        }
      }
      astButtonStruct[_button].wPressedTime = 0;
      astButtonStruct[_button].wClickTimes = 0;
      astButtonStruct[_button].wDealTime = 0;
      astButtonStruct[_button].emClicked = B_FALSE;
      astButtonStruct[_button].emLongClick = B_FALSE;
    }
  }
}

上一篇:03 LED驱动编写
下一篇:05 LCD驱动编写