该文使用函数指针来实现按键的扫描,以确保代码具有更好的移植性
函数指针
1、直接定义
例如我们按键扫描时会调用HAL_GPIO_ReadPin()函数,具体的函数声明如下:
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
那么定义该函数的函数指针,只需将函数名换成(*p)即可,具体定义如下:
GPIO_PinState (*p)(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) = HAL_GPIO_ReadPin;
这样我们就定义好了一个函数指针,这个指针p指向函数HAL_GPIO_ReadPin()
2、别名定义
上述定义的函数指针太长,可以使用typedef来进行别名,具体别名如下:
typedef GPIO_PinState (*KEY_Type)(GPIO_TypeDef*,uint16_t);
这样别名之后,KEY_Type就替代了typedef GPIO_PinState (*)(GPIO_TypeDef*,uint16_t)这一部分。之后定义指针类型只需使用KEY_Type即可,具体定义如下:
KEY_Type KEY_Fun = HAL_GPIO_ReadPin;
这样我们就定义好了一个函数指针,这个指针KEY_Fun指向函数HAL_GPIO_ReadPin()
3、调用
使用函数指针,并不需要像其他指针一样用*去取值。只需直接使用指针即可。比如要实现HAL_GPIO_ReadPin的功能,具体使用方法如下:
/* 正常的函数 */
HAL_GPIO_ReadPin(KEY_Port,KEY_Pin);
/* 使用指针的方法 */
KEY_Fun(KEY_Port,KEY_Pin);
基本按键的功能的实现
这里使用轮询法去实现按键单机的功能。
key.h文件预处理内容
根据原理图,定义相关的引脚
#define KEY1_Port GPIOA
#define KEY1_Pin GPIO_PIN_0
#define KEY2_Port GPIOA
#define KEY2_Pin GPIO_PIN_1
对函数进行别名处理
typedef GPIO_PinState (*KEY_Type)(GPIO_TypeDef*,uint16_t);
宏定义按键按下状态
#define KEY_DOWN GPIO_PIN_RESET
宏定义按键调用的函数
#define KEY HAL_GPIO_ReadPin
未使用函数指针的扫描函数
具体代码如下:
uint8_t KEY_Base(void){
uint8_t key_num = 0;
static uint8_t key_up = 1;
if(KEY1 == KEY_DOWN){
if(key_up == 1){ //防止连按
HAL_Delay(10); //消抖
if(KEY1 == KEY_DOWN){
key_up = 0;
/* 功能代码 */
key_num = 1;
}
}
}else{
key_up = 1;
}
return key_num;
}
在该代码中,KEY1是一个宏定义,具体定义如下:
#define KEY1 HAL_GPIO_ReadPin(KEY1_Port,KEY1_Pin)
该代码中,最大的问题就是 if(KEY1 == KEY_DOWN) 这一句,当我们需要多种按键时,这一句并不能兼容其他按键。比如再有个KEY2,那么这一句就要换成 if(KEY2 == KEY_DOWN)。这样就使得每多一个按键就要多编写一个扫描代码,非常的不方便。
使用函数指针的扫描函数
由于轮询法就是调用HAL_GPIO_ReadPin()来读取GPIO的状态,因此不论是KEY1还是KEY2还是更多的按键,它们都需要调用HAL_GPIO_ReadPin(),即:这个函数是兼容的。
各个按键的区别在于引脚不同,因此选择将函数指针传入、并传入相应的引脚,即可实现函数能够兼容的需求。
具体的代码如下:
uint8_t KEY_Base(KEY_Type KEY_Fun,GPIO_TypeDef* KEY_Port,uint16_t KEY_Pin){
uint8_t key_num = 0;
static uint8_t key_up = 1;
if(KEY_Fun(KEY_Port,KEY_Pin) == KEY_DOWN){ //使用函数指针来替代KEY1
if(key_up == 1){
HAL_Delay(10);
if(KEY_Fun(KEY_Port,KEY_Pin) == KEY_DOWN){
key_up = 0;
/* 功能代码 */
key_num = 1;
}
}
}else{
key_up = 1;
}
return key_num;
}
调用的使用方法如下:
while (1)
{
if(KEY_Base(KEY,KEY1_Port,KEY1_Pin) == 1){ //传入函数指针和对应的KEY引脚参数即可
LED1_ON;
}
if(KEY_Base(KEY,KEY2_Port,KEY2_Pin) == 1){
LED1_OFF;
}
}
连续扫描Bug修正
在上述代码调试过程中发现,当调用两次KEY_Base时发生了连续扫描的情况。下面首先分析为什么会出现这样的情况,之后提出相应的改进思路和改进代码。
原因分析
在while中调用了两次KEY_Base。
第一步:假设KEY1按下,进入函数之后,
if(KEY_Fun(KEY_Port,KEY_Pin) == KEY_DOWN)成立,最终key_up = 0;
第二步:假设此时KEY2没有按下,进入函数之后,
if(KEY_Fun(KEY_Port,KEY_Pin) == KEY_DOWN)不成立,最终key_up = 1;
第三步:此时while执行结束,KEY1再次调用函数,但扫描速度快,这时KEY1并未抬起。
if(KEY_Fun(KEY_Port,KEY_Pin) == KEY_DOWN)成立,且key_up==1成立
最终判断为KEY1又一次按下,导致连续扫描。
解决思路分析
由上面的叙述可知,导致连续扫描的问题是KEY2进行了判断,将key_up设置为了1。造成这样的原因是:KEY1和KEY2同时使用一个key_up来判断上一次按键是否抬起。但为了代码的兼容性,并不能设置两个key_up来分别表示KEY1,KEY2的抬起状态,这样就失去了改进的意义。
因此,改进的思路为:如何将key_up分开来,既能表示key1,又能表示key2。
在一次按键扫描操作中,只有一个按键会按下(我们默认不会存在KEY1,KEY2同时按下的情况)。那么,当按键1如果按下后,按键2就没必要扫描了。
因此设置一个锁即可。当检测到有按键按下后,进行上锁。上锁之后仅仅判断上锁时的按键,来看看是否解锁。解锁之后才进行正常的扫描。
改进代码
在整体的扫描按键代码上加了一个锁key_lock
1、当key_lock为0时,视作没有锁上,这时进行正常的扫描
2、如果扫描到按键,key_num将会有数值。
3、通过检测key_num,来判断是否有按键按下,并保存这个按键的引脚状态。
4、当下一次进入时,如果按键是锁上的,就判断是否去解锁
解锁需要两层判断:
- 按键的引脚要和上锁时的引脚一致
- 按键要处于抬起状态,以便设置key_up=1;
通过这样的设置,就实现了一个key_up对应多个按键的操作,具体的代码实现如下:
uint8_t KEY_Base(KEY_Type KEY_Fun,GPIO_TypeDef* KEY_Port,uint16_t KEY_Pin){
uint8_t key_num = 0;
static uint8_t key_up = 1;
static uint8_t key_lock = 0;
static GPIO_TypeDef* port_tmp;
static uint16_t pin_tmp;
/* 判断是否有锁,有锁就不进行扫描 */
if(key_lock == 0){
/* 扫描按键,判断按键是否按下 */
if(KEY_Fun(KEY_Port,KEY_Pin) == KEY_DOWN){
if(key_up == 1){
HAL_Delay(10);
if(KEY_Fun(KEY_Port,KEY_Pin) == KEY_DOWN){
key_up = 0;
/* 功能代码 */
key_num = 1;
}
}
}
}
/* 解锁判断 */
else{
/* 检测到同一个按键,解锁 */
if(port_tmp == KEY_Port && pin_tmp == KEY_Pin){
if(KEY_Fun(KEY_Port,KEY_Pin) != KEY_DOWN){
key_up = 1;
port_tmp = NULL;
pin_tmp = 0xffff;
key_lock = 0;
}
}
}
/* 有按键按下,记录按键信息 */
if(key_num != 0){
key_lock = 1;
port_tmp = KEY_Port;
pin_tmp = KEY_Pin;
}
return key_num;
}
调用验证代码如下:
while (1)
{
if(KEY_Base(KEY,KEY1_Port,KEY1_Pin)== 1){
HAL_GPIO_TogglePin(LED1_Port,LED1_Pin);
}
if(KEY_Base(KEY,KEY2_Port,KEY2_Pin)== 1){
HAL_GPIO_TogglePin(LED1_Port,LED1_Pin);
}
}