前言
在上篇文章【51单片机】〈C语言+Keil5+Proteus仿真〉矩阵键盘逐行扫描法-20210414中,提到了矩阵键盘的线反转法,但是在仿真上出现了一些问题,导致没能做出来。当时都已经开始怀疑自己,课本上的虽然是汇编写的代码段,但是我用C来实现居然会出错,不禁让我陷入沉思……后来经过不断地控制变量反复实验,终于我发现,这是仿真软件的问题,与我无瓜。
主要体现在逐行扫描法可以完美运行,一换到线反转法就出错。所以这篇文章主要用于记录矩阵键盘线反转法的仿真实现。
一、实验环境
由于目前学校的实验课程尚未开始,即使实验课程开始我也不会用实验室的器材来记录,所以CSDN上的学习记录必将长期或绝大部分用软件仿真来实现。其实不论是仿真还是实际操作,其原理和目的都是一样的。
Proteus 8 Professional
这是一个常用的仿真软件,具体操作在这篇讲矩阵键盘的逐行扫描法中写到过,可以作为参考。其实,Proteus也可以写程序,但是同样需要先下载安装有Keil才能使用C语言写,否则只能是汇编语言。汇编语言程序我也能写,但是现在还是更倾向于用C。
Keil5
这是一个比较常用的单片机程序的编译软件,支持C、汇编以及其他语言的文件,软件界面类似VC++6.0。使用中的注意事项有:
不能很好地支持中文!!!连中文的注释都有可能乱码,文件名也最好发放弃中文命名的习惯,软件有可能找不到中文命名的源文件地址!导致编译失败等等问题。(估计是只有我才有这样的习惯吧……)
一定要记得添加源文件到项目中,这是基本操作了。但是我还是会偶尔忘记。
一定要记得在“option of target”的“output”中勾选创建.hex文件,并且要记得创建的位置。
好了,具体操作不在赘述,上面的提到的文章有。下面开始正题。
二、拙
1、硬件
一开始,我以为可以按照逐行扫描法时一样的电路进行操作。所以选择的硬件都没变:Keypad-Smallcalc(键盘),80C51(芯片),Respack(排阻),Led-Bargraph-GRN(Led)以及电源端和接地端。所用到的原件如图:
所用到的元件
然后一顿操作猛如虎,我按照上次逐行扫描法的方式连线,即:连线图1
每次看着整整齐齐的线路图,倒还挺舒服的。是不是连线也相当简单?因为挂载的设备少,而且功能也比较简单,所以我们就不用扩展接口芯片了,直接用8051的P1口连接就按盘,P0口连接Led即可。
注意:P0口要有上拉电阻才能输出高电平。
2、软件程序
在电路的基础上,我还是想实现一个计算器的基本功能。上次用逐行扫描法做的计算器只能计算两个数的计算,现在至少得进步一下才能看到新东西。所以这次琢磨中,实现了连续运算的功能。
1)线反转法和逐行扫描法
首先我们先来捋一捋键盘检测的流程:
先行输低电平,列方向读入列值。当没有键按下时,列值应该为高电平,即FH。若有键按下,则列值不全为高电平。如行方向输入低电平0000B,若读入列值为1111B则表示没有键被按下,若为0111B则表示第一列右键按下。
去抖动。去抖动是因为按键在电平变化是会出现尖峰抖动,影响程序判定按下的次数。所以需要对这段尖峰进行处理。常见的方法有硬件除抖动和软件除抖动。软件除抖动最简单的办法就是==“不能解决它就不要面对它”==。所以我们在程序中加入延时程序,忽略这段抖动就可以了。
再读一次列值,若为列值不全为高电平则表示的确有键按下。然后就是键盘分析程序。一种是逐行扫描法,另一种是线反转法。
逐行扫描法:既然我知道了有键按下,那我就逐行送入低电平,读列值。若这一列输入低电平,而列值全为高电平,则被按下的键不在这一行。换下一行。若列值有低电平,则保存此时的行值和列值。进行其他计算得到键码。
线反转法:测试时读入的列值不全为高电平,则保存列值。然后行列的电平反转,即列输出低电平,读行值。这时得到一个行值和一个列值,两个数即代表了一个键的键码。
上次的键盘程序我用了逐行扫描法,这次我们就用线反转法。下面我们来测试一下程序。
2)线反转法程序
void keyscan(){
int temp;
while(1){
P1=0xf0;//P1口第四位输出低电平,即行值设为全0
if((P1&0xf0)!=0xf0){//列值不全为高电平
delayms(5);//去抖动
temp=P1&0xf0;//读入列值
P1=0x0f;//列值设为低电平
temp |= (P1&0x0f);//把行值(第四位)和列值(高四位)位或得到唯一指向按键的键码
P0=temp;//在Led上输出键码
while(P1!=0x0f);
}
}
}
按键分析程序已经完成,写一个主程序和延时程序试试线反转法效果。
#include
void delayms(int n){
int i;
int j;
for(i=0;i
}
void main(void){
// Write your code here
P0=0x00;
while(1){
keyscan();
delayms(50);
}
}
然后我就此进行了测试。发现这效果不对呀。
在这里插入图片描述
我发现按下不同的键,Led显示的键码低4为永远是全1。于是我又把行列的顺序换了再进行测试。即列先给低电平,读行值,有键按下再行给低电平读列值。
void keyscan(){
int temp;
while(1){
P1=0x0f;//P1口高四位输出低电平,即列值设为全0
if((P1&0x0f)!=0x0f){//读入行值不全为高电平
delayms(5);//去抖动
temp=P1&0x0f;//读入行值
P1=0xf0;//行值设为低电平
temp |= (P1&0xf0);//把行值(第四位)和列值(高四位)位或得到唯一指向按键的键码
P0=temp;//在Led上输出键码
while(P1!=0xf0);
}
}
}
然后这次是不一样的情况,它转移了。
在这里插入图片描述
变成高4位全为高电平,低4位显示正确的情形。由此判断它只是有第一次读P1口的值有效,而第二次出错。于是这个问题纠结了我好几天。最终我发现了这个keypad-smallcalc 键盘有问题。
三、悟
后来通过不断尝试,想找出代码的问题,但是按照课本的汇编来说,这个逻辑并没有问题。于是我用button做了一个矩键盘,就没有问题了。
1、换一个键盘
用button做的键盘有一种原始的感觉,毕竟没有封装到一起。所以它是这样的。在这里插入图片描述
用相同的代码,再看看效果:
在这里插入图片描述
由此可见,Led高4位显示的是列值,低4位显示行值,也就是说线反转法的额程序没有问题,是可以实现的。
以上,就是对线反转法的实验探讨。下面把完整的功能实现。
2、加入如键盘功能
首先,我们想到:有了键码,就应该分配每个键的意义。用代码表示则只需要一个多分支语句。
void act(int key){
switch(key){
case 0x77:clear();break;//清零,并用流水灯来提示。
case 0xB7:savedata(0);break;//数字键的功能是把数字保存起来
case 0xD7:output();break;//等于号的功能是显示结果ans
case 0xE7:saveop('+');break;//按下运算符键,可以对前面输入的两个数进行计算,并把新的运算符保存起来。进而可以进行连续运算。
case 0x7B:savedata(1);break;
case 0xBB:savedata(2);break;
case 0xDB:savedata(3);break;
case 0xEB:saveop('-');break;
case 0x7D:savedata(4);break;
case 0xBD:savedata(5);break;
case 0xDD:savedata(6);break;
case 0xED:saveop('*');break;
case 0x7E:savedata(7);break;
case 0xBE:savedata(8);break;
case 0xDE:savedata(9);break;
case 0xEE:saveop('/');break;
}
}
下面是对每一个功能函数的定义:
#include
int ans=0;//存放计算结果
int num=0;//存放新的操作数
char op='�';//存放运算符
void operat(){
switch(op){
case '+':ans=ans+num;break;
case '-':ans=ans-num;break;
case '*':ans=ans*num;break;
case '/':ans=(num==0)?0xff:(ans/num);break;//注意,这里需要有一个除0的处理。否则可能除0会出现除零错误。前面逐行扫描法没有注意到。
default:ans=0xff;
}
}
void saveop(char p){
if(op!='�'){//如果不是第一个运算符,即前面已经有了两个数
operat();//则先对已有的ans和num计算
}
op=p;
P0=ans;//展示结果
}
void savedata(int n){
if(op=='�'){ //如果还没有输入过运算符,则这是第一个数,存到ans内
ans=ans*10+n;//把输入的数转换成一个数
P0=ans;
}
else{
num=num*10+n;//不是第一个数则存到num中
P0=num;
}
}
int keyscan(){
int temp;
P1=0xf0;
if((P1&0xf0)!=0xf0){
delayms(5);
if((P1&0xf0)!=0xf0){
temp=P1&0xf0;
P1=0x0f;
temp |= (P1&0x0f);
delayms(20);
}
return temp;
}
else return 0xff;
}
void turnLight(){//跑马灯
int light=0x03;
int i;
int n;
P2=0x0f;
for(n=0;n<3;n++){
for(i=0;i<8;i++){
P0=light;
light=(light>>(8-1))|(light<<1);//我发现<
delayms(10);
}
}
P0=0;
}
void clear(){//清零函数
ans=0;
num=0;
op='�';
turnLight();
}
void show(int m){//显示函数
P0=m;
num=0;
}
void output(){//输出的函数。即显示和等于的功能不同。按下等于之后实际上是要把num清零的,防止num的值影响后面的输入。而show函数不用,只是显示当前的值。
operat();
op='�';
num=0;
show(ans);
}
最后是程序的入口
void main(void){
while(1){
act(keyscan());
delayms(30);
}
}
但这就完成了。这次的功能实现是连续运算。比如12+13==>25-10==>15*2==> =30这样的操作。
3、效果
在这里插入图片描述
总结
这次的核心是把线反转法实现,附加的实现了连续运算。也算是上次的后续吧。但是我还想再把七段数码管加进来显示,让I/O更人性化。毕竟看二进制还要转换,不如用十进制,我们熟悉的方式显示。后面几天我也会再学习学习七段数码管的使用,争取早点把这个计算器完善。
把自己学习的历程发出来也是一种很好的记录方式,也希望能跟小伙伴一起学习。