不管是LED还是数码管,其本质是一样的,控制的方法类似,它们都是输出设备,本节讨论输入设备:键盘。
键盘和LED及数码管都是人机交互的设备,用于人与机器信息交互,也是一种比较简单的设备。键盘主要有三类:独立式、矩阵式和编码式。
键盘处理涉及到两个问题:1 (何时)判断是否有按键按下?2识别哪一个按键按下。
键盘一定是通过某种方式直接或间接连接到芯片上的某些管脚上的,我们可以通过芯片的管脚值的变换来判断是否有键按下,可以采用查询的方式,也可以采用中断的方式,但是不管采用何种方式我们都要考虑重复处理的问题,即按键动作持续时间很久,而按键处理的时间很短,同一个按键动作处理的次数取决于按键持续时间,而一般我们希望的是每按一次键处理一次。通常的应对措施是一旦处理完按键动作后,系统等待按键释放,但是这又导致系统效率不高,宝贵的CPU时间花在了等待上。
1独立式键盘:
如果每一个按键独占一条口线,这种就是独立式键盘,一般适用于按键需求较少的场合。其处理的方式一般采用查询方式,当发现芯片管脚的电平方式变换时就可以确定有按键按下了,并且根据口线也就确定了按键(键的识别)。
a)按键按下led点亮,按键释放led熄灭
#include"reg51x.h"
sbit button=P2^0;
sbit led=P1^0;
#define KEY_DOWN() (0==button)
#define LED_ON() led=0
#define LED_OFF() led=1
void delay(int t){
while(--t);
}
void main(){
while(1){
if(KEY_DOWN()){//有键按下
LED_ON();
}else{//无键按下
LED_OFF();
}
}
}
b)按键按下,led反相
#include"reg51x.h"
sbit key=P2^0;
sbit led=P1^0;
#define KEY_DOWN() (0==button)
#define LED_ON() led=0
#define LED_OFF() led=1
#define LED_INVERS() led=~led
void delay(int t){
while(--t);
}
void main(){
while(1){
if(KEY_DOWN()){//有键按下
LED_INVERS();
while(KEY_DOWN());//等待按键释放
}
}
}
c)多个按键,数码管初始显示0,K1数字加1,K2数字减1,K3数字变为0
#include"reg51.h"
sbit K1=P2^0;
sbit K2=P2^1;
sbit K3=P2^2;
#define KEY1_DOWN() (0==K1)
#define KEY2_DOWN() (0==K2)
#define KEY3_DOWN() (0==K3)
//段码表
unsigned char code table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e};
unsigned char num=0;
void main(){
while(1){
P1=table[num];
if(KEY1_DOWN()){
num++;
if(16==num)//防止上溢出
num=0;
}
if(KEY2_DOWN()){
num--;
if(255==num)//防止下溢出,注意这里的255,因为num是无符号字符
num=15;
}
if(KEY3_DOWN()){
num=0;
}
}
}
粗看这个代码似乎是没有问题的,但是当运行时我们会发现程序的运行并不像我们想象的那样,而是出现一种非常怪异的现象:按下K1(或K2)时,实际显示的数据是随机的。分析一下我们发现主要的原因是单片机本身运行的速度是很快的,按我们按一次要持续很长时间,所以在K1按下时工作循环中的13-17行被多次执行,而我们实际希望的是每按一次键数值改变一次,所以代码中应该有判断按键释放的语句,一般我们采用等待释放的形式。
#include"reg51.h"
sbit K1=P2^0;
sbit K2=P2^1;
sbit K3=P2^2;
#define KEY1_DOWN() (0==K1)
#define KEY2_DOWN() (0==K2)
#define KEY3_DOWN() (0==K3)
//段码表
unsigned char code table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e};
unsigned char num=0;
void main(){
while(1){
P1=table[num];
if(KEY1_DOWN()){
num++;
if(16==num)//防止上溢出
num=0;
}
while(KEY1_DOW());
if(KEY2_DOWN()){
num--;
if(255==num)//防止下溢出,注意这里的255,因为num是无符号字符
num=15;
}
while(KEY2_DOW());
if(KEY3_DOWN()){
num=0;
}
while(KEY3_DOWN());
}
}
d)3个按键和8个数码管
8个数码管我们就必须要采用动态显示控制了
#include"reg51.h"
sbit K1=P1^0;
sbit K2=P1^1;
sbit K3=P1^2;
#define KEY1_DOWN() (0==K1)
#define KEY2_DOWN() (0==K2)
#define KEY3_DOWN() (0==K3)
#define SEGMENT P2
#define POSITION P3
void delay(int t){
while(t--);
}
//段码表
unsigned char code table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e};
unsigned int num=0;//显示的值
unsigned char buff[]={0,0,0,0,0,0,0,0};//显示缓冲区
void main(){
int i=0;
while(1){
for(i=0;i<8;i++){
SEGMENT=table[buff[i]];//输出段码,直接输出值
POSITION=1<<i;//输出位码,实际上应该修改--如果地址的变化不是这个规律呢?可改为将位码也放到一个数组中
delay(5000);
SEGMENT=0xFF;//这是用来消影,LED是有一定余辉的,速度较快时,可能显示的都是8
delay(5000);
}
if(KEY1_DOWN()){
num++;
if(16==num)//防止上溢出
num=0;
}
while(KEY1_DOW();//等待按键释放
if(KEY2_DOWN()){
num--;
if(255==num)//防止下溢出,注意这里的255,因为num是无符号字符
num=15;
}
while(KEY2_DOW();//等待按键释放
if(KEY3_DOWN()){
num=0;
}
while(KEY3_DOWN();//等待按键释放
//以下是一些技巧,取一些整数的各个位的数字
disp[3]=num/10000%10;//取万位
disp[4]=num/1000%10;//取千位
disp[5]=num/100%10;//取十位
disp[6]=num/10%10;//取百位
disp[7]=num%10;//取个位
}
}
主循环中一共实现两功能:1是显示,2是处理按键功能,上述两个功能应该是并发执行的。上述代码是存在问题的:如果没有按键输入的话,显示是正常的,一旦有键按下,在键按下时8个数码管只有一个显示。
经分析发现,显示和按键处理是作为一个事务来处理的,当处理按键时,动态显示功能没有能够执行。嵌入式程序的处理流程主要是轮询、中断驱动、轮询与中断驱动结合及并发处理任务等4种方式,如果不采用中断方式,上述代码如何改动能?并发处理任务又主要有2种方式1)采用RTOS;2)将长时间处理任务划分为一系列状态,每次只执行一种状态。上述的代码中动态显示无法正常显示主要是因为在键按下时系统长时间处于等待按键释放,而我们希望显示和按键处理是并发执行的,采取RTOS对于本例显然并不合适,我们也不打算采用中断方式,这样一来,我们发现我们的选择是非常有限的:将长时间处理的任务划分为一系列状态。本例中长时间处理任务是按键功能处理,其又分成处理按键输入及等待按键释放,而按键释放又是耗时最长的,我们的改造方式是在等待按键释放的过程中进行动态显示功能,并且为了加快系统的反应时间,将动态显示功能拆分为8个状态分别显示8个数码管。
#include"reg51.h"
sbit K1=P1^0;
sbit K2=P1^1;
sbit K3=P1^2;
#define KEY1_DOWN() (0==K1)
#define KEY2_DOWN() (0==K2)
#define KEY3_DOWN() (0==K3)
#define SEGMENT P2
#define POSITION P3
void delay(int t){
while(t--);
}
//段码表
unsigned char code table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e};
unsigned int num=0;//显示的值
unsigned char buff[]={0,0,0,0,0,0,0,0};//显示缓冲区
void display(){
int i=0;
for(i=0;i<8;i++){
SEGMENT=table[buff[i]];
POSITION=1<<i;
delay(5000);
SEGMENT=0xFF;
delay(5000);
}
}
void main(){
while(1){
display();
if(KEY1_DOWN()){
num++;
if(16==num)//防止上溢出
num=0;
}
if(KEY2_DOWN()){
num--;
if(255==num)//防止下溢出,注意这里的255,因为num是无符号字符
num=15;
}
if(KEY3_DOWN()){
num=0;
}
disp[3]=num/10000%10;
disp[4]=num/1000%10;
disp[5]=num/100%10;
disp[6]=num/10%10;
disp[7]=num%10;
while(KEY1_DOWN()||KEY2_DOWN()||KEY3_DOWN()){
display();
}
}
}
需要注意的是上述动态显示功能是周期性的任务,而按键是随机性的任务,具体何时发生是不确定的,所以按键的处理一般采用中断方式处理,而且中断的触发方式最好是采用边沿触发的方式,以确保每次按键只处理一次,但是中断一般是比较珍贵的资源,使用比较严格,并且电路设计比较复杂,这时我们可以采取上述程序结构。
2.矩阵式键盘
当按键需求比较多的时候,采用独立式按键就不太合适了,独立式按键每个按键占用一条口线,比较消耗系统资源,这时我们可以考虑采用矩阵式按键,常用的是4X4的矩阵式键盘。下面实现上面同样的功能:
#include"reg51.h"
#define SEGMENT P1
#define POSITION P0
#define KEY_PORT P2
void delay(int t){
while(t)--;
}
//线翻转法识别按键
unsigned char scanKey(){
unsigned char temp1,temp2,key=0;
KEY_PORT=0x0f;
if(0x0f!=KEY_PORT){//有键按下
temp1=KEY_PORT;
KEY_PORT=0xf0;
temp2=KEY_PORT;
switch(temp1+temp2){
case 0xee:key=1;break;
case 0xde:key=2;break;
case 0xbe:key=3;break;
case 0x7e:key=4;break;
case 0xed:key=5;break;
case 0xdd:key=6;break;
case 0xbd:key=7;break;
case 0x7d:key=8;break;
case 0xeb:key=9;break;
case 0xdb:key=10;break;
case 0xbb:key=11;break;
case 0x7b:key=12;break;
case 0xe7:key=13;break;
case 0xd7:key=14;break;
case 0xb7:key=15;break;
case 0x77:key=16;break;
default:key=0;
}
}
return key;
}
//段码表
unsigned char code _Table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e};
unsigned char _buff[]={0,0,0,0,0,0,0,0};//显示缓冲区
unsigned char code _Pos[]={1<<0,1<<1,1<<2,1<<3,1<<4,1<<5,1<<6,1<<7};
unsigned pos=0;//显示的位置
void display(){
SEGMENT=_Table[_buff[pos]];
POSITION=_Pos[pos];
delay(5000);
SEGMENT=0xFF;
delay(5000);
pos++;
if(pos==8)
pos=0;
}
unsigned int num=0;
void main(){
while(1){
//显示
display();
//处理按键
switch(scanKey()){
case 1: num++;break;
case 2: num--;break;
case 3: num=0;break;
}
disp[3]=num/10000%10;
disp[4]=num/1000%10;
disp[5]=num/100%10;
disp[6]=num/10%10;
disp[7]=num%10;
//等待按键释放
while(scanKey()!=0){
display();
}
}
}
3编码式键盘
当按键需要较多时可以采用编码式键盘。编码式键盘是将按键进行编码,嵌入式系统中一般用的较少。最典型的例子就是我们电脑常用的104键盘了。
4特殊按键操作:
以上举例都是按键的常规操作,实际上我们还会遇到一些有关按键的特殊用途,比如长按,短按等,所谓长按就是指一个按键按下的时间较长(如长按3秒)有一个功能,而短按(是指按键按下的时间不足三秒)又是另外一个功能,那么这种功能如何实现呢?
上图中希望实现长按长亮或熄灭,短按闪烁,如何实现呢?
#include"reg51.h"
sbit button=P2^0;
sbit led=P1^0;
#define KEY_DOWN() (0==button)
#define LED_ON() led=0
#define LED_OFF() led=1
void delay_ms(int t){
while(--t);
}
unsigned char disp_type=1,state=1;
void display(){
if(disp_type==1){//短按方式显示
LED_ON();
delay_ms(500);
LED_OFF();
delay_ms(500);
}else if(disp_type==2) {
led=state;
}
}
void main(){
unsigned char keyPress=0;//是否有键按下
unsigned char flag=0;//是否长按标志
unsigned int times=0;//记录按键时间
while(1){
keyPress=0;
flag=0;
times=0;
display();
//按键处理
//等待按键释放,判断是否是长按
while(KEY_DOWN()){
keyPress=1;
delay_ms(50);
times++;
if(times>30000/60){
flag=1;
times=0;
}
display();
}
if(1==keyPress){
disp_type=1;
}
if(1==flag){//如果是长按,则改变显示方式
disp_type=2;
state=1-state;
}
}
}
5按键消抖
按键属于机械性开关,由于触点的弹性作用,一次按键从开始到稳定闭合要经过多次弹跳,会连续产生多个脉冲,按键断开时也有同样的问题,这种现象称为键盘抖动。键盘抖动消除的方法一般有硬件消抖和软件消抖两种方法。软件消抖的实质是采用延时的方法,再次读取按键状态。