源码地址:在这里,欢迎关注小飞哥
上节课程介绍了GPIO输出模式的配置,包括修改IO标签,选择GPIO模式、GPIO上下拉等,本节输入模式有很多相同之处,节省时间,小飞哥就简单介绍一下
依然是先来看看我们的Alios 开发板上的按键硬件连接,总共有3个用户按键,分别挂在GPIOE11、GPIOE14和GPIOE10上,连接方式采用的是低电平有效,也即是,按键按下之后,MCU检测到的电平为低电平,松开之后检测为高电平,外部上拉电阻4.7K
接下来,我们只需要在cubemx对这3个GPIO进行配置即可,工程在上一节内容上继续添加,打开上一节的cubemx工程,选择PE14引脚,选择引脚模式为输入模式
然后修改标签为USR_KEY2,其他两个按键一样的配置方法,3个按键配置完成之后如下图所示:
接下来依然是对GPIO的模式、配置进行修改,这里需要注意一点就是GPIO的上下拉配置,不要瞎选,这个一定是跟硬件相关的,比如本次硬件设计,空闲时是保持高电平的,那么配置上拉是比较合适的,但是呢,此时外部已经有了上拉电阻,我们内部上拉就不是很有必要了,也可以配置为不上拉也不下拉,但是配置为下拉一定是不合适的,可能会引起信号的误动作,功耗的增加等不必要的麻烦,所以说,cubemx配置只是为你减轻了重复性搬运工作,电路工作原理必须了然于胸,选择最为合适的配置
配置比较简单,我们此次采用的是轮询获取GPIO状态的方法来实现按键,当然也可以采用外部中断的方式,对于普通按键来讲,不是很有必要,轮询GPIO状态即可
接下来,我们生成代码即可
打开工程之后,我们发现上一节配置的输出IO和这一节的输入IO是放在一起的,cubemx生成代码的规则是把同一类外设统一放在相应的模块初始化代码中,宏定义统一放在main.h中如果能够接受这个布局的话是没有问题的,如果不能还是比较麻烦的
输入模式相关的GPIO API也比较少,只涉及到
关于GPIO操作的API:
/* Initialization and de-initialization functions *****************************/
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin);
/**
* @}
*/
/** @addtogroup GPIO_Exported_Functions_Group2 IO operation functions
* @{
*/
/* IO operation functions *****************************************************/
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
如何使用呢?
参数GPIO_TypeDef *GPIOx可以是GPIO组的地址:
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)
#define GPIOI ((GPIO_TypeDef *) GPIOI_BASE)
参数GPIO_Pin可以是GPIO的引脚号:
#define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */
#define GPIO_PIN_1 ((uint16_t)0x0002) /* Pin 1 selected */
#define GPIO_PIN_2 ((uint16_t)0x0004) /* Pin 2 selected */
#define GPIO_PIN_3 ((uint16_t)0x0008) /* Pin 3 selected */
#define GPIO_PIN_4 ((uint16_t)0x0010) /* Pin 4 selected */
#define GPIO_PIN_5 ((uint16_t)0x0020) /* Pin 5 selected */
#define GPIO_PIN_6 ((uint16_t)0x0040) /* Pin 6 selected */
#define GPIO_PIN_7 ((uint16_t)0x0080) /* Pin 7 selected */
#define GPIO_PIN_8 ((uint16_t)0x0100) /* Pin 8 selected */
#define GPIO_PIN_9 ((uint16_t)0x0200) /* Pin 9 selected */
#define GPIO_PIN_10 ((uint16_t)0x0400) /* Pin 10 selected */
#define GPIO_PIN_11 ((uint16_t)0x0800) /* Pin 11 selected */
#define GPIO_PIN_12 ((uint16_t)0x1000) /* Pin 12 selected */
#define GPIO_PIN_13 ((uint16_t)0x2000) /* Pin 13 selected */
#define GPIO_PIN_14 ((uint16_t)0x4000) /* Pin 14 selected */
#define GPIO_PIN_15 ((uint16_t)0x8000) /* Pin 15 selected */
#define GPIO_PIN_All ((uint16_t)0xFFFF) /* All pins selected */
输入模式返回值为获取到的GPIO状态,也即是高低电平状态,在没有按键按下的时候,返回GPIO_PIN_SET,按键按下时候,返回GPIO_PIN_SET
@endverbatim
* @{
*/
/**
* @brief Read the specified input port pin.
* @param GPIOx where x can be (A..H) to select the GPIO peripheral for STM32L4 family
* @param GPIO_Pin specifies the port bit to read.
* This parameter can be any combination of GPIO_Pin_x where x can be (0..15).
* @retval The input port pin value.
*/
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
GPIO_PinState bitstatus;
/* Check the parameters */
assert_param(IS_GPIO_PIN(GPIO_Pin));
if ((GPIOx->IDR & GPIO_Pin) != 0x00u)
{
bitstatus = GPIO_PIN_SET;
}
else
{
bitstatus = GPIO_PIN_RESET;
}
return bitstatus;
}
接下来在之前led的任务中,对按键状态进行测试,这里涉及到按键的消抖,所谓“消抖”就是:
当检测到按键状态变化时,不是立即去响应动作,而是先等待闭合或断开稳定后再进行处理。即为按键消抖
按键消抖:可分为硬件消抖和软件消抖。
硬件消抖就是在按键上并联一个电容,如图 8-11 所示,利用电容的充放电特性来对抖动过程中产生的电压毛刺进行平滑处理,从而实现消抖。
但实际应用中,这种方式的效果往往不是很好,而且还增加了成本和电路复杂度,所以实际中使用的并不多。绝大多数情况下,我们是用软件即程序来实现消抖的
延时消抖
最简单的消抖原理,就是当检测到按键状态变化后,先等待一个 10ms 左右的延时时间,让抖动消失后再进行一次按键状态检测,如果与刚才检测到的状态相同,就可以确认按键已经稳定的动作了
static void rt_led1_flash_entry(void *parameter)
{
for (;;)
{
if(!HAL_GPIO_ReadPin(USR_KEY1_GPIO_Port,USR_KEY1_Pin))
{
rt_thread_mdelay(100);
if(!HAL_GPIO_ReadPin(USR_KEY1_GPIO_Port,USR_KEY1_Pin))//消抖
{
rt_kprintf("Key1 is presseded!\r\n");
}
}
if(!HAL_GPIO_ReadPin(USR_KEY2_GPIO_Port,USR_KEY2_Pin))
{
rt_thread_mdelay(100);
if(!HAL_GPIO_ReadPin(USR_KEY2_GPIO_Port,USR_KEY2_Pin))//消抖
{
rt_kprintf("Key2 is presseded!\r\n");
}
}
if(!HAL_GPIO_ReadPin(USR_KEY3_GPIO_Port,USR_KEY3_Pin))
{
rt_thread_mdelay(100);
if(!HAL_GPIO_ReadPin(USR_KEY3_GPIO_Port,USR_KEY3_Pin))//消抖
{
rt_kprintf("Key3 is presseded!\r\n");
}
}
}
}
上面是简单的按键状态获取,实际中,一个项目可能按键有限但同时又要实现复杂的功能切换,那么按键的功能如果很单一的话,就不能够满足需求,接下来,小飞哥带大家一起移植一个很不错的多功能按键框架,代码来源是github的jiejieTop
源码地址:
GitHub - jiejieTop/ButtonDrive: 纯C语言实现的一个按键驱动,可移植性强,支持单双击、连按、连按释放、长按;采用回调处理按键事件(自定义消抖时间),使用只需3步,1:创建按键,2:按键事件与回调处理函数链接映射。然后周期检查按键。
我们下载源码进行移植,把文件夹里面的这些文件,我们移植到自己的文件目录,下载文件中包含的main.c中是一个完整的按键初始化,实现,大家可以看看就知道如何使用了
下面小飞哥就带大家来看看如何移植到自己的系统里面
![e7d83ed083514e07e330211e338b33b7.png](en-resource://datab
可以把memset,printff替换为rt-thread的驱动,当然也可以不关注
重新编译一下,刚才的警告已经消失了
然后我们看看如何实现这个多功能按键:
先来看看一些配置项宏定义,位域、结构体、回调函数,面向对象的一套东西都在,是很适合学习的,有关于短按、长按、单击、双击等等的配置,我们根据自己的需要配置即可
#define BTN_NAME_MAX 32 //名字最大为32字节
/* 按键消抖时间40ms, 建议调用周期为20ms
只有连续检测到40ms状态不变才认为有效,包括弹起和按下两种事件
*/
#define CONTINUOS_TRIGGER 0 //是否支持连续触发,连发的话就不要检测单双击与长按了
/* 是否支持单击&双击同时存在触发,如果选择开启宏定义的话,单双击都回调,只不过单击会延迟响应,
因为必须判断单击之后是否触发了双击否则,延迟时间是双击间隔时间 BUTTON_DOUBLE_TIME。
而如果不开启这个宏定义,建议工程中只存在单击/双击中的一个,否则,在双击响应的时候会触发一次单击,
因为双击必须是有一次按下并且释放之后才产生的 */
#define SINGLE_AND_DOUBLE_TRIGGER 1
/* 是否支持长按释放才触发,如果打开这个宏定义,那么长按释放之后才触发单次长按,
否则在长按指定时间就一直触发长按,触发周期由 BUTTON_LONG_CYCLE 决定 */
#define LONG_FREE_TRIGGER 0
#define BUTTON_DEBOUNCE_TIME 2 //消抖时间 (n-1)*调用周期
#define BUTTON_CONTINUOS_CYCLE 1 //连按触发周期时间 (n-1)*调用周期
#define BUTTON_LONG_CYCLE 1 //长按触发周期时间 (n-1)*调用周期
#define BUTTON_DOUBLE_TIME 20 //双击间隔时间 (n-1)*调用周期 建议在200-600ms
#define BUTTON_LONG_TIME 50 /* 持续n秒((n-1)*调用周期 ms),认为长按事件 */
#define TRIGGER_CB(event) \
if(btn->CallBack_Function[event]) \
btn->CallBack_Function[event]((Button_t*)btn)
typedef void (*Button_CallBack)(void*); /* 按键触发回调函数,需要用户实现 */
typedef enum {
BUTTON_DOWM = 0,
BUTTON_UP,
BUTTON_DOUBLE,
BUTTON_LONG,
BUTTON_LONG_FREE,
BUTTON_CONTINUOS,
BUTTON_CONTINUOS_FREE,
BUTTON_ALL_RIGGER,
number_of_event, /* 触发回调的事件 */
NONE_TRIGGER
}Button_Event;
/*
每个按键对应1个全局的结构体变量。
其成员变量是实现滤波和多种按键状态所必须的
*/
typedef struct button
{
/* 下面是一个函数指针,指向判断按键手否按下的函数 */
uint8_t (*Read_Button_Level)(void); /* 读取按键电平函数,需要用户实现 */
char Name[BTN_NAME_MAX];
uint8_t Button_State : 4; /* 按键当前状态(按下还是弹起) */
uint8_t Button_Last_State : 4; /* 上一次的按键状态,用于判断双击 */
uint8_t Button_Trigger_Level : 2; /* 按键触发电平 */
uint8_t Button_Last_Level : 2; /* 按键当前电平 */
uint8_t Button_Trigger_Event; /* 按键触发事件,单击,双击,长按等 */
Button_CallBack CallBack_Function[number_of_event];
uint8_t Button_Cycle; /* 连续按键周期 */
uint8_t Timer_Count; /* 计时 */
uint8_t Debounce_Time; /* 消抖时间 */
uint8_t Long_Time; /* 按键按下持续时间 */
struct button *Next;
}Button_t;
然后看看如何使用API,我们只需要实现GPIO的状态获取、创建按键对象、编写回调函数即可,在任务中轮询按键状态,移植起来是非常方便的
#define KEY_ON 0
/* Private macro -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
Button_t Button1;
/* Private function prototypes -----------------------------------------------*/
static uint8_t rt_read_key1(void)
{
return HAL_GPIO_ReadPin(USR_KEY1_GPIO_Port,USR_KEY1_Pin);
}
static void Btn1_Dowm_CallBack(void *btn)
{
PRINT_INFO("Button1 单击!");
}
static void Btn1_Double_CallBack(void *btn)
{
PRINT_INFO("Button1 双击!");
}
static void Btn1_Long_CallBack(void *btn)
{
PRINT_INFO("Button1 长按!");
}
static void Btn1_Continuos_CallBack(void *btn)
{
PRINT_INFO("Button1 连按!");
}
static void Btn1_ContinuosFree_CallBack(void *btn)
{
PRINT_INFO("Button1 连按释放!");
}
/* Private user code ---------------------------------------------------------*/
/**
* @function rt_ledflash_entry
* @author:小飞哥玩嵌入式-小飞哥
* @TODO: LED控制线程
* @param:
* @return: NULL
*/
static void rt_led1_flash_entry(void *parameter)
{
Button_Create("Button1",
&Button1,
rt_read_key1,
KEY_ON);
Button_Attach(&Button1,BUTTON_DOWM,Btn1_Dowm_CallBack); //单击
Button_Attach(&Button1,BUTTON_DOUBLE,Btn1_Double_CallBack); //双击
Button_Attach(&Button1,BUTTON_CONTINUOS,Btn1_Continuos_CallBack); //连按
Button_Attach(&Button1,BUTTON_CONTINUOS_FREE,Btn1_ContinuosFree_CallBack); //连按释放
Button_Attach(&Button1,BUTTON_LONG,Btn1_Long_CallBack);
for (;;)
{
Button_Process(); //需要周期调用按键处理函数
rt_thread_mdelay(20);
// if(!HAL_GPIO_ReadPin(USR_KEY1_GPIO_Port,USR_KEY1_Pin))
// {
// rt_thread_mdelay(100);
// if(!HAL_GPIO_ReadPin(USR_KEY1_GPIO_Port,USR_KEY1_Pin))//消抖
// {
// rt_kprintf("Key1 is presseded!\r\n");
// }
// }
// if(!HAL_GPIO_ReadPin(USR_KEY2_GPIO_Port,USR_KEY2_Pin))
// {
// rt_thread_mdelay(100);
// if(!HAL_GPIO_ReadPin(USR_KEY2_GPIO_Port,USR_KEY2_Pin))//消抖
// {
// rt_kprintf("Key2 is presseded!\r\n");
// }
// }
// if(!HAL_GPIO_ReadPin(USR_KEY3_GPIO_Port,USR_KEY3_Pin))
// {
// rt_thread_mdelay(100);
// if(!HAL_GPIO_ReadPin(USR_KEY3_GPIO_Port,USR_KEY3_Pin))//消抖
// {
// rt_kprintf("Key3 is presseded!\r\n");
// }
// }
}
}
来看一下测试效果: