在上篇文章【51单片机】〈C语言+Keil5+Proteus仿真〉矩阵键盘逐行扫描法-20210414中,提到了矩阵键盘的线反转法,但是在仿真上出现了一些问题,导致没能做出来。当时都已经开始怀疑自己,课本上的虽然是汇编写的代码段,但是我用C来实现居然会出错,不禁让我陷入沉思……后来经过不断地控制变量反复实验,终于我发现,这是仿真软件的问题,与我无瓜。
主要体现在逐行扫描法可以完美运行,一换到线反转法就出错。所以这篇文章主要用于记录矩阵键盘线反转法的仿真实现。
由于目前学校的实验课程尚未开始,即使实验课程开始我也不会用实验室的器材来记录,所以CSDN上的学习记录必将长期或绝大部分用软件仿真来实现。其实不论是仿真还是实际操作,其原理和目的都是一样的。
好了,具体操作不在赘述,上面的提到的文章有。下面开始正题。
一开始,我以为可以按照逐行扫描法时一样的电路进行操作。所以选择的硬件都没变:Keypad-Smallcalc(键盘),80C51(芯片),Respack(排阻),Led-Bargraph-GRN(Led)以及电源端和接地端。所用到的原件如图:
然后一顿操作猛如虎,我按照上次逐行扫描法的方式连线,即:
每次看着整整齐齐的线路图,倒还挺舒服的。是不是连线也相当简单?因为挂载的设备少,而且功能也比较简单,所以我们就不用扩展接口芯片了,直接用8051的P1口连接就按盘,P0口连接Led即可。
注意:P0口要有上拉电阻才能输出高电平。
在电路的基础上,我还是想实现一个计算器的基本功能。上次用逐行扫描法做的计算器只能计算两个数的计算,现在至少得进步一下才能看到新东西。所以这次琢磨中,实现了连续运算的功能。
首先我们先来捋一捋键盘检测的流程:
上次的键盘程序我用了逐行扫描法,这次我们就用线反转法。下面我们来测试一下程序。
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<n;i++)
for(j=0;j<120;j++);
}
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做了一个矩键盘,就没有问题了。
用button做的键盘有一种原始的感觉,毕竟没有封装到一起。所以它是这样的。
用相同的代码,再看看效果:
由此可见,Led高4位显示的是列值,低4位显示行值,也就是说线反转法的额程序没有问题,是可以实现的。
以上,就是对线反转法的实验探讨。下面把完整的功能实现。
首先,我们想到:有了键码,就应该分配每个键的意义。用代码表示则只需要一个多分支语句。
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='\0';//存放运算符
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!='\0'){//如果不是第一个运算符,即前面已经有了两个数
operat();//则先对已有的ans和num计算
}
op=p;
P0=ans;//展示结果
}
void savedata(int n){
if(op=='\0'){ //如果还没有输入过运算符,则这是第一个数,存到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='\0';
turnLight();
}
void show(int m){//显示函数
P0=m;
num=0;
}
void output(){//输出的函数。即显示和等于的功能不同。按下等于之后实际上是要把num清零的,防止num的值影响后面的输入。而show函数不用,只是显示当前的值。
operat();
op='\0';
num=0;
show(ans);
}
最后是程序的入口
void main(void){
while(1){
act(keyscan());
delayms(30);
}
}
但这就完成了。这次的功能实现是连续运算。比如12+13==>25-10==>15*2==> =30这样的操作。
这次的核心是把线反转法实现,附加的实现了连续运算。也算是上次的后续吧。但是我还想再把七段数码管加进来显示,让I/O更人性化。毕竟看二进制还要转换,不如用十进制,我们熟悉的方式显示。后面几天我也会再学习学习七段数码管的使用,争取早点把这个计算器完善。
把自己学习的历程发出来也是一种很好的记录方式,也希望能跟小伙伴一起学习。