目前大部分代码存在的问题
单次只能对单个按键产生反应;多个按键按下就难以修改;并且代码耦合度较高,逻辑难以修改,对于添加长按,短按,双击的需求修改困难。
解决
16个按键按下无冲,并且代码简单,使用状态机思想。修改及其简单。
就算需要修改为8*8的键盘也修改的代码不会超过5行
示范开发板:STM32F103C8T6
单按键扫描思路讲解;
我们先讲解我的单按键的扫描思路;这个理解后,上手矩阵就会非常简单;
按键接线:
按键接入PA0引脚,按下时电平为低
首先会设置两个三个变量,分别是按键电平状态,按键状态,和按键按下标志位
对应代码:
_Bool key_level; //按键当前电平
unsigned char key_state; //按键状态
_Bool once_downflag; //按键按下标志位
按键电平:负责表示当前按键实时电平,(1或0)
按键状态:负责表示当前按键的处于状态,(待按下,消抖判断,待松开)
按下标志位,如果确定按键按下,此标志位就会至1;
扫描思路:首先我们会建立一个函数,函数负责单按键的扫描,假如接的按键是PA0,按下时电平为0.
注:默认这个按键扫描为20ms调用一次,下同
//此为单按键扫描,扫描接到PA0上面的按键
void Key_Scan(void)
{
}
电平读取
首先,读取电平,将PA0引脚电平赋值到key_level;变量
//此为单按键扫描,扫描接到PA0上面的按键
void Key_Scan(void)
{
key_level = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);//读取按键电平
}
然后会进入状态机,状态机负责判断这个读取的电平进行按键按下和状态转换
状态机判断
那么对应状态机的第一个状态:按键按下判断,
这里使用switch会加快运行速度,使用if也可以。
按键状态变量key_state默认是0,那么我们就以0状态为待按下状态
首先是判断按键是否按下,如果按下(电平为0)就改变状态为消抖判断
下面是代码
//此为单按键扫描,扫描接到PA0上面的按键
void Key_Scan(void)
{
key_level = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);//读取按键电平
switch(key_state)//状态机判断按键状态
{
case 0://待按下状态
if(key_level == 0)//如果检测到按键按下
{
key_state = 1;//改变状态到消抖判断
}
break;
case 1:
break;
case 2:
break;
}
}
20ms后再次进入本函数,但是状态为1(消抖判断状态)
这个状态负责判断本次按下是否为真实按下,
如果是,就将按键按下标志位置为1供外部读取,并且改变状态,为待松开
如果不是真实按下,则恢复状态为状态0;
(注意,进入到状态1前已经进行了20ms的延迟了,即按键消抖。所以状态机1就只需判断按键是否真实按下)
代码如下:
//此为单按键扫描,扫描接到PA0上面的按键
void Key_Scan(void)
{
key_level = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);//读取按键电平
switch(key_state)//状态机判断按键状态
{
case 0://待按下状态
if(key_level == 0)//如果检测到按键按下
{
key_state = 1;//改变状态到消抖判断
}
break;
case 1://消抖判断
if(key_level == 1)//如果电平为1,即松开,恢复状态机为0
{
key_state = 0;
}
else if(key_level == 0)//如果电平为0 ,代码按键确定按下,则按下标志位置1,状态机改为待松开状态
{
once_downflag = 1;
key_state = 2;
}
break;
case 2:
break;
}
}
20ms后再次进入本函数,但是状态为2(假如上次状态确认为按下)
那么本状态就仅仅需要负责,当按键松开后恢复状态机即可
代码如下:
//此为单按键扫描,扫描接到PA0上面的按键
void Key_Scan(void)
{
key_level = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);//读取按键电平
switch(key_state)//状态机判断按键状态
{
case 0://待按下状态
if(key_level == 0)//如果检测到按键按下
{
key_state = 1;//改变状态到消抖判断
}
break;
case 1://消抖判断
if(key_level == 1)//如果电平为1,即松开,恢复状态机为0
{
key_state = 0;
}
else if(key_level == 0)//如果电平为0 ,代码按键确定按下,则按下标志位置1,状态机改为待松开状态
{
once_downflag = 1;
key_state = 2;
}
break;
case 2://待松开
if(key_level == 1)//按键松开了,则恢复状态机
{
key_state = 0;
}
break;
}
}
之后在主函数内部读取按下标志位皆可进行对应的按键操作了。
如:
if(once_downflag == 1)//按键标志位为1
{
key_struct[0].once_downflag =0 ;//清除标志位
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);//取反灯
}
这就是单按键扫描的状态机思路
矩阵按键扫描思路讲解
如果你理解了单按键,你就会发现,一个按键有自己对应的电平变量,状态变量,和标志位变量
那么16个按键呢?
实物以及接线:
首先是我使用的矩阵键盘以及接线
R1——PA0
R2——PA1
R3——PA2
R4——PA3
C1——PA4
C2——PA5
C3——PA6
C4——PA7
那就是16个按键都有自己对应的电平变量,状态变量,和标志位变量;
由于16个按键一一设置对应的变量太麻烦了,我加入结构体的使用
结构体定义如下
typedef struct Key_Struct //按键结构体
{
_Bool key_level; //按键电平
unsigned char key_state; //按键状态
_Bool once_downflag; //按键按下标志位
} Key_Struct;
然后我们定义好16个按键的变量
Key_Struct key_struct[16]; //16个按键结构体
好接下来到代码层次:
我们如何获取每个按键的电平呢?
一个一个扫描怎么样,先扫描KEY1,然后KEY2,然后KEY3,KEY4,KEY5.。。。。等
从左到右,从上到下。(既行列扫描)
再看图
思路:
第一行扫描:我们把PA0置低,PA1,2,3都置高。再分别读取PA4、5、6、7的输入,将读取的值分别赋到其对应的电平变量,
第二行扫描:我们把PA1置低,PA0,2,3都置高。再分别读取PA4、5、6、7的输入,将读取的值分别赋到其对应的电平变量。
第三行扫描:我们把PA2置低,PA0,1,3都置高。再分别读取PA4、5、6、7的输入,将读取的值分别赋到其对应的电平变量。
第四行扫描:我们把PA3置低,PA0,1,2都置高。再分别读取PA4、5、6、7的输入,将读取的值分别赋到其对应的电平变量。
如上做一个循环,就是完整的16个按键的电平扫描;
同样:先创建一个按键扫描函数:
void Key_Scan(void)
{
}
那么这个函数多久调用一次呢。
我的思路是1ms调用一次,因为每次进入到这个函数就只会进行一个按键扫描。那么扫描16个按键需要16次。所以如果1ms调用一次函数,那么同一个按键的两次扫描间隔就是16ms。也是符合按下消抖的条件的。
下面的为16个按键选取的扫描位置下标 i变量:
void Key_Scan(void)
{
static unsigned char i;//静态变量i,表示当前扫描第几个按键
if(++i > 15) i=0;//本次扫描结束后切换到下一个按键,或者全部扫描完后从头开始
}
电平读取
好,读取16个按键电平(代码行数很少,但是需要一定的C语言和单片机基础)
void Key_Scan(void)
{
static unsigned char i;//静态变量i,表示当前扫描第几个按键
//行选
HAL_GPIO_WritePin(GPIOA,0x0F,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA,0x01<<(unsigned char)(i/4),GPIO_PIN_RESET);
//列选
key_struct[i].key_level = HAL_GPIO_ReadPin(GPIOA,(0x01<<((i%4)+4)));
if(++i >= 16) i=0;//本次扫描结束后切换到下一个按键,或者全部扫描完后从头开始
}
思路:
行选:
HAL_GPIO_WritePin(GPIOA,0x0F,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA,0x01<<(unsigned char)(i/4),GPIO_PIN_RESET);
首先第一个句子先将PA0到PA3引脚全部置高,传入的0x0F对应的寄存器低四位
第二个句子就是根据当前的 i 按键下标变量判断扫描第几行。
假如i=0,即第1个按键,第1行,对应PA0
假如i=4,即第5个按键,第2行,对应PA1
假如i=15,即第16个按键,第4行,对应PA3
发现一个计算公式
PA(x) :x = (i/4)
x取值向下取整;
对应代码:
HAL_GPIO_WritePin(GPIOA,0x01<<(unsigned char)(i/4),GPIO_PIN_RESET);
将0x01左移需要的行数就是对应的引脚了;
列选:
key_struct[i].key_level = HAL_GPIO_ReadPin(GPIOA,(0x01<<((i%4)+4)));
一样,要先根据i的值判断当前是第几列
假如i=0,即第1个按键,第1列,对应PA4
假如i=4,即第5个按键,第1列,对应PA4
假如i=10,即第11个按键,第3列,对应PA6
假如i=15,即第16个按键,第4列,对应PA7
发现一个计算公式
PA(x) :x = (i%4)+4;
+4是为了让偏移从PA4开始,因为从PA4开始才对应列选
对应代码:
key_struct[i].key_level = HAL_GPIO_ReadPin(GPIOA,(0x01<<((i%4)+4)));
这样,16个按键的电平读取就完成了,1ms调用一次函数,每16次为一次完整的周期。
也是本矩阵扫描最难的部分。
状态机判断:
跟单按键一样,三个状态,(待按下,消抖判断,待松开)
不分步讲解了,直接上代码;
//矩阵按键扫描函数,1ms调用一次
void Key_Scan(void)
{
static unsigned char i;//静态变量i,表示当前扫描第几个按键
//行选
HAL_GPIO_WritePin(GPIOA,0x0F,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA,0x01<<(unsigned char)(i/4),GPIO_PIN_RESET);
//列选
key_struct[i].key_level = HAL_GPIO_ReadPin(GPIOA,(0x01<<((i%4)+4)));//电平赋值
//状态机判断
switch(key_struct[i].key_state)
{
case 0: //待按下
if(key_struct[i].key_level == 0)
{
key_struct[i].key_state = 1;
}
break;
case 1: //消抖判断
if(key_struct[i].key_level == 1)
{
key_struct[i].key_state = 0;
}
else if(key_struct[i].key_level == 0)
{
key_struct[i].once_downflag = 1;
key_struct[i].key_state = 2;
}
break;
case 2://待松开
if(key_struct[i].key_level == 1)
{
key_struct[i].key_state = 0;
}
break;
}
if(++i >= 16) i=0;//本次扫描结束后切换到下一个按键,或者全部扫描完后从头开始
}
之后在主函数内部读取按下标志位皆可进行对应的按键操作了。
如:
void KEY_Process(void)
{
if(key_struct[0].once_downflag == 1)
{
key_struct[0].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
if(key_struct[1].once_downflag == 1)
{
key_struct[1].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
if(key_struct[2].once_downflag == 1)
{
key_struct[2].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
if(key_struct[3].once_downflag == 1)
{
key_struct[3].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
if(key_struct[4].once_downflag == 1)
{
key_struct[4].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
if(key_struct[5].once_downflag == 1)
{
key_struct[5].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
if(key_struct[6].once_downflag == 1)
{
key_struct[6].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
if(key_struct[7].once_downflag == 1)
{
key_struct[7].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
if(key_struct[8].once_downflag == 1)
{
key_struct[8].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
if(key_struct[9].once_downflag == 1)
{
key_struct[9].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
if(key_struct[10].once_downflag == 1)
{
key_struct[10].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
if(key_struct[11].once_downflag == 1)
{
key_struct[11].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
if(key_struct[12].once_downflag == 1)
{
key_struct[12].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
if(key_struct[13].once_downflag == 1)
{
key_struct[13].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
if(key_struct[14].once_downflag == 1)
{
key_struct[14].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
if(key_struct[15].once_downflag == 1)
{
key_struct[15].once_downflag =0 ;
//
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
}
这个就是任意一个按键按键就取反灯
以上就是按键扫描的全部了
如果你需要按键的单双击,长按判断,可以参考下面这个文章,扫描的思路是一样的。
http://t.csdnimg.cn/AUQOA
如果你觉得写的不错,希望点赞加收藏,这是给我最大的称赞