04 按键驱动编写
上一篇:03 LED驱动编写
下一篇:05 LCD驱动编写
作者:桂信科黄鹏老师。note部分是我添加的内容
- 分析独立按键原理图并在CubeMX配好引脚。
- 搞懂常规的按键处理,讲解单击、双击和长按的检测方法。
- 在常规方法基础上增加回调函数,提高按键应用的灵活性。
- 采用状态机的思想设计按键处理程序和回调函数。
一、独立按键原理图分析
要从原理图中搞明白以下内容:
1) 按键由哪个些引脚读取状态?
B1->PB0、B2->PB1、B3->PB2、B4->PA0。
2) 按键按下和未按下的电平状态是什么?
按下->低电平;未按下->高电平。
思考:
1) 引脚应该设置为输入还是输出?
很显然,这是外部输出电平给单片机,这几个引脚应设置为输入。
2) 引脚应该设置为下拉、上拉还是浮空?
由图可知未按下是高电平,且外部已经有上拉电阻,引脚内部可设置为上拉或浮空。万万不能是下拉,否则电平不确定。
二、引脚配置
参照02 CubeMX配置引脚#二、独立按键引脚完成该步骤。
三、编程思想
1. 功能需求和分析
程序需要实现识别:单击、双击和长按三种模式,并实现按键对应的功能。按键本质上是人按按键发出有规律的电平信号波形,所以识别不同模式的重点在于找出信号之间的差异。三种不同按键状态的信号波形如下:
差异:
单击:一段时间内出现一个负脉冲,并且负脉冲的低电平持续时间较短。
双击:一段时间内出现两个负脉冲,并且两个负脉冲的间隔时间很短。
长按:一段时间内出现一个负脉冲,并且负脉冲的低电平持续时间较长。
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总线
由CubeMX的时钟树可知定时器的频率为150MHz。
所以TIM6配置如下,最终效果是1ms进一次定时器6中断。
记得打开定时器6中断。
配置好定时器后,生成代码。还需要在主函数中启动定时器才行。启动定时函数为:
HAL_TIM_Base_Start_IT(&htim6); //启动定时器6并使能中断
3.2 TIM6中断的回调函数如何使用?
中断函数位于stm32g4xx_it.c中,样子如图
注意:这个函数是不需要修改的!我们需要修改的是定时器的回调函数。
定时器回调函数的名称是固定的,如何才能找到定时器的回调函数名称???
可以通过定时中断中可以找到定时器更新事件的回调函数
在回调函数中执行按键查询处理函数。
//定时器中断回调函数
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中修改中断优先级即可
4. 状态机的思想加上回调函数实现按键
之前介绍的两种按键的处理方法,对于按键的处理方法基本一致,不同的是在按键事件发生之后函数的执行过程不一样。
按键的处理过程都是按照时间顺序去执行的。本章将介绍一种新的编程思想——“状态机”的思想方法。它将按键的处理过程分为几个状态,状态之间是可以转变的,但是存在状态转变的条件。
按键的状态分为“释放状态”,“按下消抖状态”,“按下状态”,“释放消抖状态”,一共四个状态。状态转换图如下:
判断“单击”“双击”“长按”的方法等之前的思想一致。给有能力理解的同学去实现,所以只有完整代码,可以去问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驱动编写