51单片机_按键检测
一、独立按键介绍
轻触按键相当于是一种电子开关
按下时开关接通,松开时开关断开,实现原理是通过轻触按键内部的金属弹片受力弹动来实现接通和断开


由于机械点的弹性作用,按键开关在闭合时不会马上稳定的接通,在断开时也不会一下子断开。因而在闭合和断开的瞬间均伴随着一连串的抖动。抖动时间的长短由按键的机械特性决定,一般为5ms到10ms。
按键稳定闭合时间的长短由操作人员的按键动作决定,一般为零点几秒至数秒。按键抖动会引起按键被误读多次。为了确保CPU对按键的依次闭合仅作一次处理,必须及时进行消抖。
按键消抖有两种方式,一种是硬件消抖,另一种是软件消抖。为了使电路更加简单,通常采用软件消抖。
软件消抖一般来说一个简单的按键消抖就是先读取按键的状态,如果得到按键按下之后,延时10ms,再次读取按键状态,如果按键还是按下状态,那么说明按键已经按下。其中的延时10ms就是软件消抖处理。
常用的软件去抖动方法:
- 先设置IO口为高电平,由于开发板IO有上拉电阻,默认IO为高电平;
- 读取IO口电平确认是否有按键按下;
- 如果有IO电平为低电平后,延时几个毫秒;
- 再读取该IO电平,如果仍为低电平,说明按键按下;
- 执行相应按键的程序;
二、独立按键检测原理
独立按键电路构成是由各个按键的一个管脚连接在一起接地,按键其它引脚分别接到单片机IO口。
单片机的IO口既可作为输出也可作为输入使用,当检测按键时用的是它的输入功能,独立按键的一端接地,另一端与单片机的某个IO口相连,开始时先给IO口赋一高电平,然后让单片机不断检测该IO口是否变为低电平,当按键闭合时,相当于该IO口通过按键与地相连,变成低电平,程序一旦检测到IO口变为低电平则说明按键被按下,然后执行相应的指令。

三、独立按键应用实践
1)实现按键控制LED功能
c
#include <REGX52.H>
#include "Delay.h"
/**
* 主函数
*/
void main()
{
while(1)
{
if(P3_1 == 0) // 如果K1按键按下,则为低电平
{
Delay(20); // 延时消抖
while(P3_1 == 0); // 松手检测
Delay(20); // 延时消抖
P2_0 = ~P2_0; // LED1 取反
}
}
}
这段代码依旧存在不灵敏的情况,下面是改进:
c
#include <REGX52.H>
#include "Delay.h"
void main()
{
while(1)
{
if(P3_1 == 0) // 检测按键是否按下
{
Delay(10); // 延时消抖
if(P3_1 == 0) // 再次确认按键确实按下
{
P2_0 = ~P2_0; // 执行LED取反
// 等待按键释放,避免连按
while(P3_1 == 0); // 等待按键松开
}
}
}
}
2)按键控制LED灯二进制显示
c
#include <REGX52.H>
#include "Delay.h"
void main()
{
unsigned char LEDNum=0;
while(1)
{
if(P3_1==0) //如果K1按键按下
{
Delay(10); //延时消抖
if(P3_1==0)
{
LEDNum++; //变量自增
P2=~LEDNum; //变量取反输出给LED
while(P3_1==0); //松手检测
}
}
}
}
// 实现原理:
// +000 LEDNum 0000 0000 取反 ---> 1111 1111
// +001 LEDNum 0000 0001 取反 ---> 1111 1110
// +002 LEDNum 0000 0010 取反 ---> 1111 1101
// +003 LEDNum 0000 0011 取反 ---> 1111 1100
// ···
// +255 LEDNum 1111 1111 取反 ---> 0000 0000
// +256 溢出 -> LEDNum 0000 0000 重新开始计数
3)按键实现LED左右移动
c
#include <REGX52.H>
#include "Delay.h"
unsigned char LEDNum;
void main()
{
P2=~0x01; //上电默认LED1点亮 0000 0001 取反 -> 1111 1110
while(1)
{
if(P3_0==0) //如果K1按键按下,右移
{
Delay(20);
while(P3_0==0);
Delay(20);
LEDNum++; //LEDNum自增
if(LEDNum>=8) //限制LEDNum自增范围
LEDNum=0;
P2=~(0x01<<LEDNum); //LED的第LEDNum位点亮
}
if(P3_1==0) //如果K2按键按下,左移
{
Delay(20);
while(P3_1==0);
Delay(20);
if(LEDNum==0) //LEDNum减到0后变为7
LEDNum=7;
else //LEDNum未减到0,自减
LEDNum--;
P2=~(0x01<<LEDNum); //LED的第LEDNum位点亮
}
}
}
P2=~(0x01<<LEDNum); 实现原理如下:
| LEDNum | 0x01 << LEDNum (二进制) | 取反后 (二进制) | 效果说明 |
|---|---|---|---|
| 0 | 0000 0001 |
1111 1110 |
LED0亮,其他灭 |
| 1 | 0000 0010 |
1111 1101 |
LED1亮,其他灭 |
| 2 | 0000 0100 |
1111 1011 |
LED2亮,其他灭 |
| 3 | 0000 1000 |
1111 0111 |
LED3亮,其他灭 |
| 4 | 0001 0000 |
1110 1111 |
LED4亮,其他灭 |
| 5 | 0010 0000 |
1101 1111 |
LED5亮,其他灭 |
| 6 | 0100 0000 |
1011 1111 |
LED6亮,其他灭 |
| 7 | 1000 0000 |
0111 1111 |
LED7亮,其他灭 |
四、矩阵按键
独立键盘与单片机连接时,每一个按键都需要单片机的一个I/O口。若某单片机系统需要较多按键,如果用独立按键便会占用过多的I/O口资源。
当用到多个按键时,为了减少I/O口引脚,引入了矩阵按键。比如4*4矩阵键盘。
对于4*4矩阵键盘,开发板上通常将16个按键排成4行4列。第一行将每个按键的一端连接在一起构成行线,第一列将每个按键的另一端连接在一起构成列线,这样便一共有4行4列共8根线。将这8根线连接到单片机的8个I/O口上,通过程序扫描键盘可以检测16个键。
无论是独立键盘还是矩阵键盘,单片机检测其是否被按下的依据都是一样的,即检测与该键对应的I/O口是否为低电平。独立键盘有一端固定为低电平而矩阵键盘两端都与单片机I/O口相连,在检测时需编程通过单片机I/O口送出低电平。检测方法最常用的是行列扫描和线翻转法。
- 行列扫描法检测时,先送一列为低电平,其余几列全为高电平,此时确定列数;然后立即轮流检测一次各行是否有低电平,若检测到某一行为低电平,此时确定了行数,便可以确认当前被按下的键是哪一行哪一列的。用同样方法轮流送各列一次低电平,再轮流检测一次各行是否变为低电平,这样即可检测完所有的按键,当有按键被按下时便可判断出按下的键是哪一个键。
- 线翻转法检测时,就是使所有行线为低电平时,检测所有列线是否有低电平,如果有,就记录列线值;然后再翻转,使所有列线都为低电平,检测所有行线的值。由于有按键按下,行线的值也会有变化,记录行线的值,从而就可以检测到全部按键。
矩阵按键也需要按键消抖。



以第一列代码作为示例,简单介绍下 逐列扫描 的检测原理:

P1=0xFF;把所有按键端口置为高电平,也就是关闭所有按键P1_3=0;将第一列按键置为低电平,其他按键依旧为高电平(关闭状态)- 使用
If判断检测行按键,如果某一行为低电平,就返回KeyNumber编号
其他3列按键的检测原理同上
五、矩阵按键应用实践
1)实现LCD1602显示按键编号
main.c
c
#include <REGX52.H>
#include "Delay.h" //包含Delay头文件
#include "LCD1602.h" //包含LCD1602头文件
#include "MatrixKey.h" //包含矩阵键盘头文件
unsigned char KeyNum;
void main()
{
LCD_Init(); //LCD初始化
LCD_ShowString(1,1,"MatrixKey:"); //LCD显示字符串
while(1)
{
KeyNum=MatrixKey(); //获取矩阵键盘键码
if(KeyNum) //如果有按键按下
{
LCD_ShowNum(2,1,KeyNum,2); //LCD显示键码
}
}
}
Delay.c
c
void Delay(unsigned int xms)
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
Delay.h
c
#ifndef __DELAY_H__
#define __DELAY_H__
void Delay(unsigned int xms);
#endif
LCD1602.c
c
#include <REGX52.H>
//引脚配置:
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0
//函数定义:
/**
* @brief LCD1602延时函数,12MHz调用可延时1ms
* @param 无
* @retval 无
*/
void LCD_Delay()
{
unsigned char i, j;
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
/**
* @brief LCD1602写命令
* @param Command 要写入的命令
* @retval 无
*/
void LCD_WriteCommand(unsigned char Command)
{
LCD_RS=0;
LCD_RW=0;
LCD_DataPort=Command;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}
/**
* @brief LCD1602写数据
* @param Data 要写入的数据
* @retval 无
*/
void LCD_WriteData(unsigned char Data)
{
LCD_RS=1;
LCD_RW=0;
LCD_DataPort=Data;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}
/**
* @brief LCD1602设置光标位置
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @retval 无
*/
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
if(Line==1)
{
LCD_WriteCommand(0x80|(Column-1));
}
else if(Line==2)
{
LCD_WriteCommand(0x80|(Column-1+0x40));
}
}
/**
* @brief LCD1602初始化函数
* @param 无
* @retval 无
*/
void LCD_Init()
{
LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵
LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关
LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动
LCD_WriteCommand(0x01);//光标复位,清屏
}
/**
* @brief 在LCD1602指定位置上显示一个字符
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @param Char 要显示的字符
* @retval 无
*/
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
LCD_SetCursor(Line,Column);
LCD_WriteData(Char);
}
/**
* @brief 在LCD1602指定位置开始显示所给字符串
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param String 要显示的字符串
* @retval 无
*/
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=0;String[i]!='\0';i++)
{
LCD_WriteData(String[i]);
}
}
/**
* @brief 返回值=X的Y次方
*/
int LCD_Pow(int X,int Y)
{
unsigned char i;
int Result=1;
for(i=0;i<Y;i++)
{
Result*=X;
}
return Result;
}
/**
* @brief 在LCD1602指定位置开始显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~65535
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以有符号十进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-32768~32767
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
unsigned char i;
unsigned int Number1;
LCD_SetCursor(Line,Column);
if(Number>=0)
{
LCD_WriteData('+');
Number1=Number;
}
else
{
LCD_WriteData('-');
Number1=-Number;
}
for(i=Length;i>0;i--)
{
LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以十六进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~0xFFFF
* @param Length 要显示数字的长度,范围:1~4
* @retval 无
*/
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i,SingleNumber;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
SingleNumber=Number/LCD_Pow(16,i-1)%16;
if(SingleNumber<10)
{
LCD_WriteData(SingleNumber+'0');
}
else
{
LCD_WriteData(SingleNumber-10+'A');
}
}
}
/**
* @brief 在LCD1602指定位置开始以二进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~1111 1111 1111 1111
* @param Length 要显示数字的长度,范围:1~16
* @retval 无
*/
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
}
}
LCD1602.h
c
#ifndef __LCD1602_H__
#define __LCD1602_H__
//用户调用函数:
void LCD_Init();
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
#endif
MatrixKey.c
c
#include <REGX52.H>
#include "Delay.h"
/**
* @brief 矩阵键盘读取按键键码
* @param 无
* @retval KeyNumber 按下按键的键码值
如果按键按下不放,程序会停留在此函数,松手的一瞬间,返回按键键码,没有按键按下时,返回0
*/
unsigned char MatrixKey()
{
unsigned char KeyNumber=0;
P1=0xFF; // 1111 1111 全部置高电平,没有按键按下
P1_3=0; // 检测第一列
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=1;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=5;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=9;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=13;}
P1=0xFF; // 1111 1111 全部置高电平,没有按键按下
P1_2=0; // 检测第二列
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=2;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=6;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=10;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=14;}
P1=0xFF; // 1111 1111 全部置高电平,没有按键按下
P1_1=0; // 检测第三列
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=3;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=7;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=11;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=15;}
P1=0xFF; // 1111 1111 全部置高电平,没有按键按下
P1_0=0; // 检测第四列
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=4;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=8;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=12;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=16;}
return KeyNumber;
}
MatrixKey.h
c
#ifndef __MATRIXKEY_H__
#define __MATRIXKEY_H__
unsigned char MatrixKey();
#endif
2)使用矩阵键盘实现密码锁功能
只需要修改main.c ,其余代码同上
c
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "MatrixKey.h"
unsigned char KeyNum;
unsigned int Password,Count;
void main()
{
LCD_Init();
LCD_ShowString(1,1,"Password:");
while(1)
{
KeyNum=MatrixKey();
if(KeyNum)
{
if(KeyNum<=10) //如果S1~S10按键按下,输入密码
{
if(Count<4) //如果输入次数小于4
{
Password*=10; //密码左移一位
Password+=KeyNum%10; //获取一位密码
Count++; //计次加一
}
LCD_ShowNum(2,1,Password,4); //更新显示
}
if(KeyNum==11) //如果S11按键按下,确认
{
if(Password==2345) //如果密码等于正确密码
{
LCD_ShowString(1,14,"OK "); //显示OK
Password=0; //密码清零
Count=0; //计次清零
LCD_ShowNum(2,1,Password,4); //更新显示
}
else //否则
{
LCD_ShowString(1,14,"ERR"); //显示ERR
Password=0; //密码清零
Count=0; //计次清零
LCD_ShowNum(2,1,Password,4); //更新显示
}
}
if(KeyNum==12) //如果S12按键按下,取消
{
Password=0; //密码清零
Count=0; //计次清零
LCD_ShowNum(2,1,Password,4); //更新显示
}
}
}
}
Password*=10; //密码左移一位
Password+=KeyNum%10; //获取一位密码
图表:按键序列 5 → 6 → 3 → 8 的处理过程
| 按键 | 运算前 Password | Password × 10 | KeyNum % 10 | 运算后 Password | 解释 |
|---|---|---|---|---|---|
| 5 | 0 | 0 × 10 = 0 | 5 % 10 = 5 | 5 | 第一个数字直接存入 |
| 6 | 5 | 5 × 10 = 50 | 6 % 10 = 6 | 56 | 5移到十位,6放在个位 |
| 3 | 56 | 56 × 10 = 560 | 3 % 10 = 3 | 563 | 56变为560,3放在个位 |
| 8 | 563 | 563 × 10 = 5630 | 8 % 10 = 8 | 5638 | 563变为5630,8放在个位 |