【51單片機】矩陣鍵盤線反轉法實驗仿真
前言
在上篇文章【51單片機】〈C語言+Keil5+Proteus仿真〉矩陣鍵盤逐行掃描法-20210414中,提到了矩陣鍵盤的線反轉法,但是在仿真上出現(xiàn)了一些問題,導致沒能做出來。當時都已經(jīng)開始懷疑自己,課本上的雖然是匯編寫的代碼段,但是我用C來實現(xiàn)居然會出錯,不禁讓我陷入沉思……后來經(jīng)過不斷地控制變量反復實驗,終于我發(fā)現(xiàn),這是仿真軟件的問題,與我無瓜。
主要體現(xiàn)在逐行掃描法可以完美運行,一換到線反轉法就出錯。所以這篇文章主要用于記錄矩陣鍵盤線反轉法的仿真實現(xiàn)。
一、實驗環(huán)境
由于目前學校的實驗課程尚未開始,即使實驗課程開始我也不會用實驗室的器材來記錄,所以CSDN上的學習記錄必將長期或絕大部分用軟件仿真來實現(xiàn)。其實不論是仿真還是實際操作,其原理和目的都是一樣的。
- Proteus 8 Professional
這是一個常用的仿真軟件,具體操作在這篇講矩陣鍵盤的逐行掃描法中寫到過,可以作為參考。其實,Proteus也可以寫程序,但是同樣需要先下載安裝有Keil才能使用C語言寫,否則只能是匯編語言。匯編語言程序我也能寫,但是現(xiàn)在還是更傾向于用C。 - Keil5
這是一個比較常用的單片機程序的編譯軟件,支持C、匯編以及其他語言的文件,軟件界面類似VC++6.0。使用中的注意事項有:- 不能很好地支持中文!!!連中文的注釋都有可能亂碼,文件名也最好發(fā)放棄中文命名的習慣,軟件有可能找不到中文命名的源文件地址!導致編譯失敗等等問題。(估計是只有我才有這樣的習慣吧……)
- 一定要記得添加源文件到項目中,這是基本操作了。但是我還是會偶爾忘記。
- 一定要記得在“option of target”的“output”中勾選創(chuàng)建.hex文件,并且要記得創(chuàng)建的位置。
好了,具體操作不在贅述,上面的提到的文章有。下面開始正題。
二、拙
1、硬件
一開始,我以為可以按照逐行掃描法時一樣的電路進行操作。所以選擇的硬件都沒變:Keypad-Smallcalc(鍵盤),80C51(芯片),Respack(排阻),Led-Bargraph-GRN(Led)以及電源端和接地端。所用到的原件如圖:

然后一頓操作猛如虎,我按照上次逐行掃描法的方式連線,即:
每次看著整整齊齊的線路圖,倒還挺舒服的。是不是連線也相當簡單?因為掛載的設備少,而且功能也比較簡單,所以我們就不用擴展接口芯片了,直接用8051的P1口連接就按盤,P0口連接Led即可。
注意:P0口要有上拉電阻才能輸出高電平。
2、軟件程序
在電路的基礎上,我還是想實現(xiàn)一個計算器的基本功能。上次用逐行掃描法做的計算器只能計算兩個數(shù)的計算,現(xiàn)在至少得進步一下才能看到新東西。所以這次琢磨中,實現(xiàn)了連續(xù)運算的功能。
1)線反轉法和逐行掃描法
首先我們先來捋一捋鍵盤檢測的流程:
- 先行輸?shù)碗娖剑蟹较蜃x入列值。當沒有鍵按下時,列值應該為高電平,即FH。若有鍵按下,則列值不全為高電平。如行方向輸入低電平0000B,若讀入列值為1111B則表示沒有鍵被按下,若為0111B則表示第一列右鍵按下。
- 去抖動。去抖動是因為按鍵在電平變化是會出現(xiàn)尖峰抖動,影響程序判定按下的次數(shù)。所以需要對這段尖峰進行處理。常見的方法有硬件除抖動和軟件除抖動。軟件除抖動最簡單的辦法就是==“不能解決它就不要面對它”==。所以我們在程序中加入延時程序,忽略這段抖動就可以了。
- 再讀一次列值,若為列值不全為高電平則表示的確有鍵按下。然后就是鍵盤分析程序。一種是逐行掃描法,另一種是線反轉法。
- 逐行掃描法:既然我知道了有鍵按下,那我就逐行送入低電平,讀列值。若這一列輸入低電平,而列值全為高電平,則被按下的鍵不在這一行。換下一行。若列值有低電平,則保存此時的行值和列值。進行其他計算得到鍵碼。
- 線反轉法:測試時讀入的列值不全為高電平,則保存列值。然后行列的電平反轉,即列輸出低電平,讀行值。這時得到一個行值和一個列值,兩個數(shù)即代表了一個鍵的鍵碼。
上次的鍵盤程序我用了逐行掃描法,這次我們就用線反轉法。下面我們來測試一下程序。
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);
}
}
}
按鍵分析程序已經(jīng)完成,寫一個主程序和延時程序試試線反轉法效果。
#include<reg51.h>
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);
}
}
然后我就此進行了測試。發(fā)現(xiàn)這效果不對呀。

我發(fā)現(xiàn)按下不同的鍵,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口的值有效,而第二次出錯。于是這個問題糾結了我好幾天。最終我發(fā)現(xiàn)了這個keypad-smallcalc 鍵盤有問題。
三、悟
后來通過不斷嘗試,想找出代碼的問題,但是按照課本的匯編來說,這個邏輯并沒有問題。于是我用button做了一個矩鍵盤,就沒有問題了。
1、換一個鍵盤
用button做的鍵盤有一種原始的感覺,畢竟沒有封裝到一起。所以它是這樣的。
用相同的代碼,再看看效果:

由此可見,Led高4位顯示的是列值,低4位顯示行值,也就是說線反轉法的額程序沒有問題,是可以實現(xiàn)的。
以上,就是對線反轉法的實驗探討。下面把完整的功能實現(xiàn)。
2、加入如鍵盤功能
首先,我們想到:有了鍵碼,就應該分配每個鍵的意義。用代碼表示則只需要一個多分支語句。
void act(int key){
switch(key){
case 0x77:clear();break;//清零,并用流水燈來提示。
case 0xB7:savedata(0);break;//數(shù)字鍵的功能是把數(shù)字保存起來
case 0xD7:output();break;//等于號的功能是顯示結果ans
case 0xE7:saveop('+');break;//按下運算符鍵,可以對前面輸入的兩個數(shù)進行計算,并把新的運算符保存起來。進而可以進行連續(xù)運算。
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;
}
}
下面是對每一個功能函數(shù)的定義:
#include<reg51.h>
int ans=0;//存放計算結果
int num=0;//存放新的操作數(shù)
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會出現(xiàn)除零錯誤。前面逐行掃描法沒有注意到。
default:ans=0xff;
}
}
void saveop(char p){
if(op!='\0'){//如果不是第一個運算符,即前面已經(jīng)有了兩個數(shù)
operat();//則先對已有的ans和num計算
}
op=p;
P0=ans;//展示結果
}
void savedata(int n){
if(op=='\0'){ //如果還沒有輸入過運算符,則這是第一個數(shù),存到ans內
ans=ans*10+n;//把輸入的數(shù)轉換成一個數(shù)
P0=ans;
}
else{
num=num*10+n;//不是第一個數(shù)則存到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);//我發(fā)現(xiàn)<<和>>移位操作不是循環(huán)的,會丟失被移出去的數(shù)。
delayms(10);
}
}
P0=0;
}
void clear(){//清零函數(shù)
ans=0;
num=0;
op='\0';
turnLight();
}
void show(int m){//顯示函數(shù)
P0=m;
num=0;
}
void output(){//輸出的函數(shù)。即顯示和等于的功能不同。按下等于之后實際上是要把num清零的,防止num的值影響后面的輸入。而show函數(shù)不用,只是顯示當前的值。
operat();
op='\0';
num=0;
show(ans);
}
最后是程序的入口
void main(void){
while(1){
act(keyscan());
delayms(30);
}
}
但這就完成了。這次的功能實現(xiàn)是連續(xù)運算。比如12+13==>25-10==>15*2==> =30這樣的操作。
3、效果

總結
這次的核心是把線反轉法實現(xiàn),附加的實現(xiàn)了連續(xù)運算。也算是上次的后續(xù)吧。但是我還想再把七段數(shù)碼管加進來顯示,讓I/O更人性化。畢竟看二進制還要轉換,不如用十進制,我們熟悉的方式顯示。后面幾天我也會再學習學習七段數(shù)碼管的使用,爭取早點把這個計算器完善。
把自己學習的歷程發(fā)出來也是一種很好的記錄方式,也希望能跟小伙伴一起學習。
浙公網(wǎng)安備 33010602011771號