矩阵键盘介绍
当键盘中按键数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式。在矩阵式键盘中,每条水平线和垂直线在交叉处不直接连通,而是通过一个按键加以连接。采用逐行或逐列的“扫描”,就可以读出任何位置按键的状态。
扫描原理
让我们来对比一下独立按键和矩阵按键
在独立按键中我们是按键一端接地,当按键按下时端口处变为低电平。之前我只是记住了这一点,但是现在我才了解到内部的原理。请看下面介绍。
51系列单片机I/O口各种不同的工作模式以及配置介绍
首先我们要先知道一件事情:所有IO口通电后默认是高电平
总的来说,我们使用的STC89C52中,P1,P2,P3,P4口都是弱上拉模式,只有P0端口为开漏输出模式。具体什么是弱上拉模式呢?
简单一点来讲就是像我们下面图中的结构,开关打到上面,此时端口为高电位,输出为1,但是此时驱动能力很弱,当端口外面接低电位的时候允许将它的电位拉低,(我的理解是当外面接低电位的时候vcc和低电位之间形成通路,有了电流,这样电阻就能分压,使得端口处电位降低,不知道是否正确)这就是我们一会儿讲到矩阵按键时的原理。当我们让端口为低电位,开关打到下面时,端口处为低电位,输出为0,此时驱动能力很强,能吸收许多的电流。这其实就是在51单片机中能让发光二极管(LED)发光比较亮的原理,当端口电位为0的时候驱动能力很强。所以我们通常检验端口电位是不是变化为0。
逐行扫描读取按键状态(具体原理)
结构:s1,s2,s3,s4的左边都连在了P17,s5,s6,s7,s8的左边都连在了P16,依次共四行占用了4个端口,s1,s5,s9,s13的右边都连在了P13,s2,s6,s10,s14的右边都连在了P12,依次共四列占用了4个端口。我们的想法是:
p17 p16 p15 p14
0 1 1 1 //通过其余四个端口电位状态来检查第一行是否有按键按下
1 0 1 1 //通过其余四个端口电位状态来检查第二行是否有按键按下
1 1 0 1 //通过其余四个端口电位状态来检查第三行是否有按键按下
1 1 1 0 //通过其余四个端口电位状态来检查第四行是否有按键按下
具体原理:这部分很重要!这部分很重要!这部分很重要!!!
一.首先我们要知道当p17 p16 p15 p14中的端口给0时才能检测到是否这行的按键按下了,当p17 p16 p15 p14中仅P17端口给0,其他全给1时,就算第二行的按键按下了但第一行的按键没有按下,我们的P10,P11,P12,P13也检测不到低电平来判断某一列有按键按下,因为此时只有第一行的一端是低电平,其他行按键左端都是高电平。
二.接着解释一下为什么按键一端为低电平的时候,另一端也是高电平。我们刚刚在上面讲过了
所有IO口通电后默认是高电平,当然这里的P10,P11,P12,P13也不例外,但是我们要看上面说过的弱上拉模式当端口外面接低电位的时候允许将它的电位拉低,即使它处在高电位,这里的P1系列8个端口都是弱上位模式,所以比如给P17接低点位,并且s2按下了,此时P12的电位就变为了低电位,检测为0,实现了精准读取按键状态的效果。
三.最后来结合第一点,解释一下为什么一次只能给p17 p16 p15 p14其中之一端口赋0,让其处于低点位。假如我们同时让P17,P16为低电平,此时当我们按下s5的时候和按下s1的时候,或者说同时按下的时候,P13端口的电位都变为了0,我们无法分辨到底是哪个按键按下了,所以一次只能给p17 p16 p15 p14其中之一端口赋0。
对I/O工作模式的补充
补充:上面仅仅介绍了I/O口的弱上拉模式,下面补充一些对于STC89C52系列单片机中的开漏输出模式的介绍。
个人总结(欢迎大家指正):我认为P0端口的开漏输出配置就相当于上面这幅图中将电阻去掉,这样的话就不能让端口连通接地就使得端口的电位由1变为0,它可以实现输出低电平(0),但不能直接通过接地来改变电位1为0,这一点和弱上拉输出端口不一样。
开漏输出端口在输出高电平(1)时,实际上是处于高阻态,也就是并没有输出电平的功能,因此无法通过接地来将其改变为低电平。如果需要在开漏输出端口上输出低电平,通常需要外部上拉电阻和一个连接到负极(比如地)的开关或器件,当外部上拉电阻连接到 VCC 电源时,输出端口会被拉高到高电平(1)。当开关或器件闭合时,输出端口会连接到地,从而形成电路,使得输出端口能够输出低电平(0)。
继续拓展
代码实现
具体要求:按下按键s1-s16,依次在液晶屏上显示数字1-16。
//main.c里面代码
#include<regx52.h>
#include"LCD1602.h"
#include"matrix.h"
void main(){unsigned char i=0;LCD_Init();LCD_ShowString(1,1,"hello world");while(1){i=matrix_num();if(i){//这里注意,如果不加这个if判断条件的话,就会导致可能按下的时候闪烁一瞬间甚至不闪烁
//因为这里不写这个if的话当我们没有按下的时候会一直显示00,所以我们要加一个if判断来让它不显示00LCD_ShowNum(2,2,i,2);}}}
//matrix.c里面代码
#include<regx52.h>
#include<delay.h>
unsigned char matrix_num(){unsigned char key_num=0;P1=0xFF;P1_7=0;if(P1_3==0){delay(200);if(P1_3==0){while(P1_3==0);key_num=1;}}if(P1_2==0){delay(200);if(P1_2==0){while(P1_2==0);key_num=2;}}if(P1_1==0){delay(200);if(P1_1==0){while(P1_1==0);key_num=3;}}if(P1_0==0){delay(200);if(P1_0==0){while(P1_0==0);key_num=4;}}P1=0xFF;P1_6=0;if(P1_3==0){delay(200);if(P1_3==0){while(P1_3==0);key_num=5;}}if(P1_2==0){delay(200);if(P1_2==0){while(P1_2==0);key_num=6;}}if(P1_1==0){delay(200);if(P1_1==0){while(P1_1==0);key_num=7;}}if(P1_0==0){delay(200);if(P1_0==0){while(P1_0==0);key_num=8;}}P1=0xFF;P1_5=0;if(P1_3==0){delay(200);if(P1_3==0){while(P1_3==0);key_num=9;}}if(P1_2==0){delay(200);if(P1_2==0){while(P1_2==0);key_num=10;}}if(P1_1==0){delay(200);if(P1_1==0){while(P1_1==0);key_num=11;}}if(P1_0==0){delay(200);if(P1_0==0){while(P1_0==0);key_num=12;}}P1=0xFF;P1_4=0;if(P1_3==0){delay(200);if(P1_3==0){while(P1_3==0);key_num=13;}}if(P1_2==0){delay(200);if(P1_2==0){while(P1_2==0);key_num=14;}}if(P1_1==0){delay(200);if(P1_1==0){while(P1_1==0);key_num=15;}}if(P1_0==0){delay(200);if(P1_0==0){while(P1_0==0);key_num=16;}}return key_num;
}
完整代码包括LCD系列内部实现代码参考文末,并且注意使用这个函数要先进行初始化先调用LCD_Init()函数,这个矩阵按键在理解了原理之后代码容易写出来。
下面做出两点解释:
1.matrix_num()代码逻辑解释,我们每次检测一行按键的时候都是:像这样P1=0xFF; P1_7=0;仅仅让一个端口输出地低电平,然后检测后四个端口是否会出现低电平。这里代码是逐行检测,逐列检测和逐行检测代码逻辑几乎一样,只不过前四个端口用途和后四个端口用途颠倒一下,不多赘述。
2.代码耦合性,最好不要让这边刚读取到了按键按下,立即在这个代码中就显示读取到的数字,最好写成这边用一个函数来读取按下的按键,然后函数执行结束将按下按键的编号返回,然后再显示,这样代码更便于理解
用矩阵按键实现密码锁
功能:按下1-9号按键输入对应数字,按下10号按键输入0,按下11号按键表示确认提交,按下12号按键表示退格,按下13号按键表示清除。密码正确提交显示"YES 666",错误提交显示“ERROE”密码锁一共四位数字。
代码这里只展示了main.c里面的,因为是在上面那个显示1-16的代码基础上来的,除了main.c不同,其他都一样。
纯代码版本
#include<regx52.h>
#include"LCD1602.h"
#include"matrix.h"
void main(){unsigned char i=0,count=0;unsigned int password=0,set_password=3226;LCD_Init();LCD_ShowString(1,1,"password");while(1){i=matrix_num();if(0<i&&i<=10&&count<4){LCD_ShowString(1,10," ");password=password*10+i%10;LCD_ShowNum(2,2,password,4);count++;}if(i==12&&count>0){password=password/10;LCD_ShowNum(2,2,password,4);count--;}if(i==13){password=0;LCD_ShowNum(2,2,password,4);count=0;}if(i==11){if(count==4){if(password!=set_password){LCD_ShowString(1,10,"ERROR");password=0;count=0;}if(password==set_password){LCD_ShowString(1,10,"YES 666");password=0;count=0;}}}}
}
代码解释版本
#include<regx52.h>
#include"LCD1602.h"
#include"matrix.h"
void main(){unsigned char i=0,count=0;//count用来记录已经输入了几位unsigned int password=0,set_password=3226;//保存当前数字和预置密码LCD_Init();LCD_ShowString(1,1,"password");while(1){i=matrix_num();//获取按下按键对应的数字if(0<i&&i<=10&&count<4){//输入密码部分LCD_ShowString(1,10," ");//为了假如一轮输入测试结束,
//再次输入后覆盖上一次提示信息password=password*10+i%10;//这里i%10主要是针对10%10=0LCD_ShowNum(2,2,password,4);count++;}if(i==12&&count>0){//如果按下退格键并且此时count>就退格且count--password=password/10;LCD_ShowNum(2,2,password,4);count--;}if(i==13){//清除输入且count重置为0password=0;LCD_ShowNum(2,2,password,4);count=0;}if(i==11){//如果按下确认键就进行检查if(count==4){if(password!=set_password){LCD_ShowString(1,10,"ERROR");password=0;//复位count=0;}if(password==set_password){LCD_ShowString(1,10,"YES 666");password=0;count=0;}}}}
}
补充另一种扫描方法
首先第一种就是我们刚刚讲到的行列扫描法。
第二种是线翻转扫描方法:(十字交叉确定)
先给所有行赋0,给所有列赋1,判断在哪一行;
然后给所有行赋1,所有列赋0,判断在哪一列。
两次分别确定出按下按键所在的行与列,就可以知道具体按下按键在哪里了。这里确定行列的原理也是当按键按下时,弱上拉模式当端口外面接低电位的时候允许将它的电位拉低,假如按键按下时,就能在另一端检测到低电位。
下面是代码部分示例,但是我认为这种方法写代码没有上一种简单且易于理解,所以我还是推荐使用逐行或者逐列扫描。
完整代码
//LCD1602.h
#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//LCD1602.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');}
}
//delay.h
#ifndef__delay_H__
#define__delay_H__
void delay(unsigned int t);#endif
//delay.c
void delay(unsigned int t){while(t--);
}
//matrix.h
#ifndef__matrix_H__
#define__matrix_H__
unsigned char matrix_num(); #endif
//matric.c
#include<regx52.h>
#include<delay.h>
unsigned char matrix_num(){unsigned char key_num=0;P1=0xFF;P1_7=0;if(P1_3==0){delay(200);if(P1_3==0){while(P1_3==0);key_num=1;}}if(P1_2==0){delay(200);if(P1_2==0){while(P1_2==0);key_num=2;}}if(P1_1==0){delay(200);if(P1_1==0){while(P1_1==0);key_num=3;}}if(P1_0==0){delay(200);if(P1_0==0){while(P1_0==0);key_num=4;}}P1=0xFF;P1_6=0;if(P1_3==0){delay(200);if(P1_3==0){while(P1_3==0);key_num=5;}}if(P1_2==0){delay(200);if(P1_2==0){while(P1_2==0);key_num=6;}}if(P1_1==0){delay(200);if(P1_1==0){while(P1_1==0);key_num=7;}}if(P1_0==0){delay(200);if(P1_0==0){while(P1_0==0);key_num=8;}}P1=0xFF;P1_5=0;if(P1_3==0){delay(200);if(P1_3==0){while(P1_3==0);key_num=9;}}if(P1_2==0){delay(200);if(P1_2==0){while(P1_2==0);key_num=10;}}if(P1_1==0){delay(200);if(P1_1==0){while(P1_1==0);key_num=11;}}if(P1_0==0){delay(200);if(P1_0==0){while(P1_0==0);key_num=12;}}P1=0xFF;P1_4=0;if(P1_3==0){delay(200);if(P1_3==0){while(P1_3==0);key_num=13;}}if(P1_2==0){delay(200);if(P1_2==0){while(P1_2==0);key_num=14;}}if(P1_1==0){delay(200);if(P1_1==0){while(P1_1==0);key_num=15;}}if(P1_0==0){delay(200);if(P1_0==0){while(P1_0==0);key_num=16;}}return key_num;
}
参考文章:【51单片机】矩阵键盘_51单片机4×4矩阵键盘-CSDN博客
欢迎大家指正!