我的按键学习史
实体按键作为人机界面中必不可少的一部分,即使现在已经大部分使用触控,但绝大部分的设备仍然还是保留着一小部分的实体按键,所以对于学习单片机微机控制来说,进一步学习按键还是极其有必要的。
对于实体按键来说,原理基本就是利用单片机的IO口的读取功能,来识别IO口上的电平变化,这样除了高电平 就是低电平了;硬件电路比较简单,最常用的有以下几种
图1是一种最简单按键硬件电路模型,需要使能单片机内部上拉电阻
图2这种电路相对来说多加了一个电容,电阻,硬件上已经实现了滤波,及上拉
图3 4x4矩阵式键盘,8个IO口扫描式工作
对于硬件电路来说并没有什么变化,而主要变化的是驱动软件的实现。
在刚学习单片机那会儿,主要还是利用在while(1)大循环里不断的进行if判断是否有按键变化,根据对应的按键变化执行对应的功能,代码如下:
int mian()
{
…………
While(1)
{
if(!(PINB&0x01)) //110,k1按下,K2,K3没按下,数码管复位,显示0
{
_delay_ms(20); //防抖
if(!(PINB&0x01))
{
……
while(!(PINB&0x01)); //按键释放才完成操作
}
}
if(!(PINB&0x02)) //101,k2按下,K1,K3没按下,数码管数字加一
{
_delay_ms(20); //防抖
if(!(PINB&0x02))
{
……
while(!(PINB&0x02)); //按键释放才完成操作
}
}
}
}
这种方式在简单的应用场合中还是可以应用的,如果代码一多,那么这个while就会又长又臭,无论是20毫秒(防抖)的延时还是while(!(PINB&0x01));都会严重浪费有限的系统资源
在学习了模块化编程后,针对按键建立两个文件 key.h 和key.c来专门分装一个基于按键操作的函数。
按键将代码更改如下:
key.h文件部分内容
#ifndef _USE_KEY_H
#define _USE_KEY_H 1
#define DDR_K1_IO DDRC
#define PORT_K1_IO PORTC
#define PIN_K1_IO PINC
#define KEY_1 (1<<2) //PC3
#define KEY_2 (1<<3) //PC2
#define FUN1 10
#define FUN2 11
extern void Init_Key(void);
extern uchar Key_Cont(void);
extern uchar Key_Cont_Wait(void);
#endif
key.c文件部分内容
……………………………
uchar Key_Cont(void)
{
Init_Key();
if(!(PIN_K1_IO & KEY_2))
{
delay_ms(20);
if(!(PIN_K1_IO & KEY_2))
{
g_key_mod = FUN1;
while(!(PIN_K2_IO & KEY_2));
}
}
if(!(PIN_K1_IO & KEY_3))
{
delay_ms(20);
if(!(PIN_K1_IO & KEY_3))
{
g_key_mod = FUN2;
while(!(PIN_K1_IO & KEY_3));
}
}
return g_key_mod;
}
int main()
{
…………
while(1)
{
switch(Key_Cont())
{
case FUN1:
{
……… 相应的功能代码
}break;
case FUN2:
{
……… 相应的功能代码
}break;
default:break;
}
}
}
这样在主函数中用一个switch就可以执行按键功能了,在任意的需要按键的函数中只需调用Key_Cont()函数就可以完成按键的功能的实现,大大增加了可读性,和易用性,但仍然没有解决效率和系统资源浪费的问题。
在《AVR单片机嵌入式系统原理与应用实践》一书中看到了一种基于状态机加定时器扫描(或调度器)的方法,这种方法就解决了上面说的资源浪费问题
key.h文件部分内容
#ifndef _USE_KEY_H
#define _USE_KEY_H 1
#define KEY_DDR DDRB
#define KEY_PORT PORTB
#define KEY_PIN PINB
#define READ_KEY_1 (KEY_PIN & (1<<PB0)) //PB0
#define READ_KEY_2 (KEY_PIN & (1<<PB1)) //PB1
#define READ_KEY_3 (KEY_PIN & (1<<PB2)) //PB2
#define KEY_STATE_0 0
#define KEY_STATE_1 1
#define KEY_STATE_2 2
#define KEY_A 10 //click
#define KEY_B 11 //dblclick
#define KEY_C 12 //long_press
#define KEY_VALUE !READ_KEY_1||!READ_KEY_2||!READ_KEY_3
extern void key_init(void);
uchar key_con_mul(void);
#endif
key.c文件部分内容
uchar g_key_state;
uchar g_key_value;
uchar key_con_mul(void)
{
Init_Key();
if (KEY_VALUE) g_key_value = 1;
else g_key_value = 0;
switch(g_key_state)
{
case KEY_STATE_0:
{
if(g_key_value)
{
g_key_state = KEY_STATE_1; //有按键按下跳转到状态1
}
}break;
case KEY_STATE_1:
{
if(g_key_value) //判断按键是否是真的按下
{
if (!READ_KEY_1){g_key_num=10;} //找到是哪个按键按下
if (!READ_KEY_2){g_key_num=11;}
if (!READ_KEY_3){g_key_num=12;}
g_key_state = KEY_STATE_2; // 确认按键编号后 转到状态2
}
else
{
g_key_state = KEY_STATE_0; //按键已经抬起,回到初始态
}
}break;
case KEY_STATE_2:
{
if(!g_key_value)
{
g_key_state = KEY_STATE_0;//按键已经抬起,回到初始态
}
}break;
default:break;
}
return g_key_num;
}
上面代码在原来的基础上增加了多按键的支持,要实现上述代码的功能还需要一个定时器的支持,给定时器一个固定的进入定时器中断的时间,在定时器中断中执行key_con_mul()函数,每执行一次key_con_mul()函数都会获取一个当前按键的状态,通过获取的当前按键的状态,来决定下一个按键的状态,通过每两次运行的时间间隔可以有效的做到防抖等效果。上面的功在调度器中使用更能发挥优势。、
下面介绍一种基于状态机按键的增强型,在这个原来的基础上增加了单按键的 单击 双击 长按 等功能,主要原理是增加了两个计数变量,通过计数变量的值及组合方式,来决定按键的功能,基于这个基础还可以扩展更多的功能
key.h文件部分内容
#ifndef _USE_KEY_H
#define _USE_KEY_H 1
#define KEY_DDR DDRB
#define KEY_PORT PORTB
#define KEY_PIN PINB
#define READ_KEY_1 (KEY_PIN & (1<<PB0)) //PB0
#define READ_KEY_2 (KEY_PIN & (1<<PB1)) //PB1
#define READ_KEY_3 (KEY_PIN & (1<<PB2)) //PB2
#define KEY_STATE_0 0
#define KEY_STATE_1 1
#define KEY_STATE_2 2
#define KEY_STATE_3 3
#define KEY_STATE_4 4
#define KEY_CLICK 10 //click
#define KEY_DBLCK 11 //dblclick
#define KEY_LONPR 12 //long_press
#define KEY_VALUE !READ_KEY_1||!READ_KEY_2||!READ_KEY_3
typedef struct
{
uint16_t count1;
uint16_t count2;
uint8_t fun;
uint8_t mode;
}KEY_M;
KEY_M KEY_STATUS[3];
extern void key_init(void);
uchar key_con_mul(void);
#endif
key.c文件部分内容
uchar g_key_num;
uchar g_key_state;
uchar g_key_value;
uchar key_mode(uint8_t key_num) //匹配键值
{
if ((KEY_STATUS[key_num].count1>0)&&(KEY_STATUS[key_num].count1<100)&&(KEY_STATUS[key_num].count2==0))
{
KEY_STATUS[key_num].fun = KEY_CLICK; //单击
}
else if ((KEY_STATUS[key_num].count1 > 100)&&(KEY_STATUS[key_num].count2==0))
{
KEY_STATUS[key_num].fun = KEY_DBLCK; //双击
}
else if(KEY_STATUS[key_num].count2)
{
KEY_STATUS[key_num].fun = KEY_LONPR; //长按
}
KEY_STATUS[key_num].count1=0;
KEY_STATUS[key_num].count2=0;
}
uchar key_con_mul(void)
{
key_init();
if (KEY_VALUE)
{
g_key_value = 1; //有按键按下
}
else
{
g_key_value = 0; //没有按键按下 或按键已经释放
}
switch(g_key_state)
{
case KEY_STATE_0:
{
if(g_key_value)
{
g_key_state = KEY_STATE_1; //有按键按下,切换到状态1
}
}break;
case KEY_STATE_1:
{
if(g_key_value) //如果按键仍按下,识别出是哪个按键
{
if (!READ_KEY_1){g_key_num=0;}
if (!READ_KEY_2){g_key_num=1;}
if (!READ_KEY_3){g_key_num=2;}
g_key_state = KEY_STATE_2; //确认哪个按键按下后,切换到状态2
}
else
{
g_key_state = KEY_STATE_0; // 如果按键已抬起,则识别为误操作,转换到按键初始态0
}
}break;
case KEY_STATE_2:
{
if(g_key_value) //按键处于连续按下状态,该按键对应的计数器1 加1
{
KEY_STATUS[g_key_num].count1++;
g_key_state = KEY_STATE_2; //按键未释放,继续执行 状态2
}
else
{
g_key_state = KEY_STATE_3; //按键已释放,切换到状态3
}
}break;
case KEY_STATE_3:
{
if(!g_key_value) //按键处于释放状态,该按键对应的计数器2 加1
{
KEY_STATUS[g_key_num].count2++;
if (KEY_STATUS[g_key_num].count2 < 20)//按键对应的计数器2 累加值没有超过判定值,继续执行状态3
{
g_key_state = KEY_STATE_3;
}
else
{
g_key_state = KEY_STATE_0; //按键对应的计数器2 累加值没有超过判定值 可以判断按键已释放,转换到按键初始态
KEY_STATUS[g_key_num].count2=0;
key_mode(g_key_num);
}
}
else
{
g_key_state = KEY_STATE_4; //当有第二次按键按下时,转换到按键状态4
KEY_STATUS[g_key_num].count2=0;
}
}break;
case KEY_STATE_4:
{
if(g_key_value)
{
KEY_STATUS[g_key_num].count2++;
g_key_state = KEY_STATE_4;//按键没有释放,继续回到KEY_STATE_4
}
else
{
g_key_state = KEY_STATE_0;
key_mode(g_key_num);
}
}break;
default:break;
}
return g_key_num;
}
功能越多其要花费的资源就越多,这是在所难免的,下面再分享一个在网上看到的及省资源的按键实现方法:
多个按键使用同一个端口,按键算法
unsigned char Trg;
unsigned char Cont;
void KeyRead( void )
{
unsigned char ReadData = PINB^0xff; // 1
Trg = ReadData & (ReadData ^ Cont); // 2
Cont = ReadData; // 3
}
在资源极度缺乏的设备上可以一用。