在嵌入式系统中,用的最多的输入设备就是按键,用户的应用需求可通过相应按键传递到系统软件中,软件转而完成用户请求,实现简单的人机交互。笔者此处就矩阵按键的实现作一个简单的介绍。
1. 按键输入概述
按键是一种常开型按钮开关,平时键的二个触点处于断开状态,按下键时它们才闭合。按键控制电路就是用来实时监视按键,当有键接下时,电路监控中的输入引脚电平发生变化,检测到这种变化后,控制电路进行按键扫描,定位按键的位置,并把相关的按键信息反馈回上一层应用中。常见的按键输入设计有独立式按键,矩阵式按键。独立式按键每个键占用一个IO口,电路配置灵活,软件简单,但按键较多时,IO口浪费大。矩阵式按键适用于按键数量较多的场合,由行线和列线组成,按键位于行列的交叉点上。节省IO口。通常按键控制电路通过查询方式或中断方式去检测按键的输入,查询方式需占用一定的cpu资源,查询频率太低可能造成按键输入丢失,太高浪费cpu资源,通常按键查询频率约50HZ较合适。中断方式需占用cpu一路外部中断,但不会占用cpu资源,只要有按键按下时,cpu即可马上检测到输入,进行扫描并得到按键值。
2. 硬件设计
笔者此处采用4x4的矩阵按键设计,当然,矩阵键盘可通过四个肖特基二极管构成四输入的与门(可参考笔者这篇文章<浅谈小信号肖特基二极管在数字电路中的应用>),连接到单片机的外部中断引脚,从而实现中断方式检测按键输入。为兼容目前开发板常见的矩阵按键设计,笔者把4x4的矩阵按键接口接在P1口,通过查询方式检测按键输入。
图2-1 4x4矩阵按键
3. 驱动实现
由于我们采用的是查询方式按键设计,因此单片机需一定的频率去扫描P1口的按键,通常这个频率约50HZ较合适,为保证这个扫描频率,通常是通过定时器产生时标周期性进行执行扫描。P1.4~P1.7列线通过上拉电阻接到VCC上,P1.0~P1.3行线产生相应的扫描信号,无按键,列线处于高电平状态,有键按下,列线电平状态将由与此列线相连的行线电平决定。行线电平为低,则列线电平为低,行线电平为高,则列线电平为高。
按键扫描函数如下,该函数需周期执行,以扫描按键的状态。以51单片机为例,P1.0~P1.3逐行输出扫描信号,在Key.h模块头文件实现接口宏KeyOutputSelect()
#define KeyOutputSelect(Select) {P1 = ~(1<<(Select));}
输出扫描线后,需要读取对应扫描线的按键状态(P1.4~P1.7),同样在Key.h模块头文件实现引脚状态读取接口宏KeyGetPinState()
#define KeyGetPinState() (P1>> 4)
读取了对应扫描线下的按键引脚状态,就需判断哪些引脚电平为0(按下),对读到的引脚状态进行取反转换成对引脚状态变量进行搜1算法,得到键值的速度能达到最快,并且多个按键同时按下时也能够正确得到优先级最高的按键。按键有效按下会得到0~15的键值,无按键按下时得到键值16。
voidKeyScan()
{
unsigned char i;
unsigned char KeyValue;
unsigned char PinState;
if (KeyState.State == STATE_DISABLE) {
return; // 按键禁用时,不对键盘进行扫描
}
// 键值为0~15,未按键键值为16,任意多的键按下均能
// 正确返回优先级最高的键值
KeyValue = 0;
for (i=0; i<4; i++) {
KeyOutputSelect(i); // 输出扫描线
// 得到对应扫描线时的按键状态
PinState = KeyGetPinState();
// 有键按下时,PinState中有0的位置即为键值位置
PinState = ~PinState;
// 搜索Pinstate第一个为1的位
if (!(PinState & 0xf)) {
KeyValue += 4;
continue; // 该扫描线没有按键按下,进入下一扫描线
}
// 该扫描线有键按下,对半进行检索1的位置
if (!(PinState & 0x3)) {
KeyValue += 2; // 低2位(P1.4~P1.5)没有按下
PinState >>= 2; // 移位检索(P1.6~P1.7)
}
if (!(PinState & 0x1)){
KeyValue += 1;
}
break; // 有鍵按下,退出继续扫描
}
KeyStore(KeyValue); // 保存按键状态
}
得到了按键值后,我们需要对按键值进行处理并根据按键状态把可能产生的按键消息保存进缓冲区中,以便用户程序读取处理。按键通常有按下、松手、长按这几个状态,需要支持按下检测、松手检测、长按、连击的功能,并且需要对按键进行去抖滤波。按键的状态往往会在这几种情况进行切换,因此,对按键进行状态机编程是相当清晰的思路。我们在KeyStore()函数中实现对按键状态的转移判断,在模块中我们通过按键状态结构变量KeyState来跟踪记录按键的状态
typedef struct {
unsigned char State; // 按键的各个状态转移
unsigned int TImeCount; // 用来跟踪各个状态的计时
} KEY_STATE;
staTIc KEY_STATE KeyState; // 按键状态机状态转移
检测到相应的按键事件后(KEY_UP、KEY_DOWN、KEY_LONG),需产生相应的按键消息保存进按键缓存区,通常可以开辟一个按键队列缓存,以便保存多个产生的按键消息,不会因用户代码未能及时处理按键而造成按键丢失,笔者此处为避免复杂,以一个按键缓冲为例,按键事件结构变量KeyBuffer用来保存按键消息
typedef struct {
unsigned char Value;
unsigned char State;
} KEY_EVENT;
// 按键扫描得到的键值存放在KeyBuffer中,包含键值及键状态
staTIc volaTIle KEY_EVENT KeyBuffer;
按键消抖以及长按均是需要以时间为判断标准,我们在模块中定义消抖时间以及长按时间判决以及相应的状态宏
// 按键的扫描周期为20ms
#define WOBBLE_COUNT 1 // 按键消抖计数,1个按键扫描周期(20ms)
#define LONG_COUNT 100 // 长按100个扫描周期判断为长按(2S)
#define STATE_INIT 0x0 // 按键初始化状态
#define STATE_WOBBLE 0x1 // 按键消抖状态
#define STATE_LONG 0x2 // 按键长按检测状态
#define STATE_RELEASE 0x3 // 按键释放状态
#define STATE_DISABLE 0x4 // 按键禁用状态
完整的KeyStore()函数实现如下
static voidKeyStore(unsigned char Value)
{
static unsigned char LastValue;
switch (KeyState.State) {
case STATE_INIT: // 初始状等待按键
if (Value < KEY_NULL) {
// 记录下按下的键并进入消抖状态
LastValue = Value;
KeyState.TimeCount = WOBBLE_COUNT -1;
KeyState.State = STATE_WOBBLE;
}
break;
case STATE_WOBBLE:
if (KeyState.TimeCount) {
KeyState.TimeCount--; // 消抖计时未到
break;
}
// 消抖后再次判断为同一键值则认为键按下保存键值
// 并进入到长按检测