Arduino:四位數七段顯示器顯示按鈕按下的計數值

 Arduino 開發板:UNO

程式碼撰寫語言:C


功能

電源打開的初始狀態,四位數七段顯示器僅一位數顯示,顯示 0。

當按鈕 1 (SW1) 每按下一次,計數值從 0 開始逐次遞加一,並顯示於七段顯示器上。且若計數值為一位數,則四位數七段顯示器只顯示一位數數值;若計數值為二位數,則四位數七段顯示器只顯示二位數數值,…。例如計數值為 12,顯示器不會顯示 0012,只會顯示12。

當按鈕 2 (SW2,Reset 鍵) 每次按下,計數值就會被重新歸零。


電路圖

如圖一所示。

四位數七段顯示器與按鈕開關、 2N3906 及 74HC595 電路圖
圖一  四位數七段顯示器、按鈕開關、 2N3906 及 74HC595 電路圖



接線圖

如圖二所示。

四位數七段顯示器與按鈕開關、 2N3906 及 74HC595 接線圖

圖二  四位數七段顯示器、按鈕開關、 2N3906 及 74HC595 接線圖


完整程式碼


const int dataPin = 10;   /* UNO  10 腳連接至 74HC595  DS 接腳 */
const int latchPin = 11;  /* UNO  11 腳連接至 74HC595  ST_CP 腳   */
const int clockPin 12;  /* UNO  12 腳連接至 74HC595  SH_CP 腳 */
const int scanPin[] = {5678};  /* UNO 第 5-8 腳為掃描訊號的輸出接腳,依序分別負七段顯示器的個位,十位,百位的掃瞄 */
const int sevenSegCode[] = {0xC0, 0xF9, 0xA4
                            0xB0, 0x99, 0x92
                            0x82, 0xF8, 0x80
                            0x90}/*  數字 0~9 對應的共陽型七段顯示器碼 */
int dataOut[] = {0000}/* 輸出至四位數顯示器的資料緩衝區,依序分別存個位,十位,百位,千位的顯示資料,初始值為 0 */

const int pushButtonPin = 4; /* UNO 4 腳設為接收可遞增計數值之按鈕狀態訊號的接腳 */
const int resetButtonPin = 3; /* UNO 3 腳設為接收可重置計數值之按鈕狀態訊號的接腳 */
void push_button_detection(); /* 宣告偵測按鈕是否按下的自定函數 */
int count = 0; /* 計數值的初始值為 0 */
unsigned long previous_time,now_time; /* 宣告記錄前一次與這一次第 4 腳接收到低準位的時間變數 */
bool begin = true; /* 宣告紀錄按鈕是否尚未被按下過的變數,初始值為「真」*/
void calCountDigit(); /* 宣告用來計算計數值其千位、百位、十位與個位值的自定函數 */
int countThousndsDigit; /* 宣告儲存計數值其千位值的變數 */                                                                              
int countTenthsDigit; /* 宣告儲存計數值其百位值的變數 */ 
int countHundredsDigit; /* 宣告儲存計數值其十位值的變數 */
int countUnitsDigit; /* 宣告儲存計數值其個位值的變數 */
int remainder; /* 宣告計算過程中所需暫儲餘數的變數 */
int scanDigitNum;   /* 宣告儲存實際需要掃描的位數個數的變數 */

void setup() {
    for (int i = 0; i < 4; i++){
              pinMode(scanPin[i], OUTPUT); /* 規劃第 5-8 腳為輸出 */
                digitalWrite(scanPin[i], 1); /* 5-8 腳停止掃描 */
    }
pinMode(dataPin, OUTPUT); /* 規劃可輸出序列資料的第 10 腳為輸出 */
pinMode(latchPin, OUTPUT); /* 規劃可輸出栓鎖訊號的第 11 腳為輸出 */
pinMode(clockPin, OUTPUT); /* 規劃可輸出時脈的第 12 腳為輸出 */
pinMode(pushButtonPin, INPUT); /* 規劃可接收按鈕一狀態訊號的第 4 腳為輸入 */
pinMode(resetButtonPin, INPUT); /* 規劃可接收按鈕二狀態訊號的第 3 腳為輸入*/
digitalWrite(latchPin,0); /* 輸出栓鎖訊號的初值設為 0〔解除資料栓鎖〕*/
}
void loop() {
    while(begin){   /* 如果按鈕不曾被按下 */
    digitalWrite(latchPin, 0); /* 解除資料栓鎖 */
shiftOut(dataPin, clockPin, MSBFIRST,
sevenSegCode[0]); /* 將數字 0 的七段顯示碼作序列輸出 */
digitalWrite(latchPin, 1); /* 栓鎖資料 */
digitalWrite(scanPin[0], 0); /* 輸出掃描信號至七段顯示器的個位 */
delay(1); /* 延遲 1 ms */
digitalWrite(scanPin[0], 1); /* 關閉掃描信號 */
push_button_detection(); /* 偵測按鈕是否按下 */  
  }
if(!begin){   /* 如果按鈕已被按下過 */
  push_button_detection(); /* 偵測按鈕是否按下 */
calCountDigit(); /* 將計數值的千位、百位、十位與個位值找出 */
       for (int i = 0; i < scanDigitNum;i++){ /* for 迴圈執行 scanDigitNum (掃描位數數) */
      digitalWrite(latchPin, 0); /* 解除資料栓鎖 */
         shiftOut(dataPin, clockPin, MSBFIRST, sevenSegCode[dataOut[i]]);
/* 將第 i 位數存放於資料緩衝區資料的七段顯示碼作序列輸出 */
digitalWrite(latchPin, 1); /* 栓鎖資料 */
            digitalWrite(scanPin[i], 0);  /* 輸出第 i 位數的掃描信號 */
            delay(1); /* 延遲 1 ms */
            digitalWrite(scanPin[i], 1); /* 關閉掃描信號 */
        }    
    }
}

void push_button_detection(){
    while(!digitalRead(pushButtonPin)){  /* 若偵測到第 4 腳低準位 */
    previous_time = millis();   /* 紀錄剛剛偵測到低準位的時間 */
    while(!digitalRead(pushButtonPin)); /* 再次偵測到低準位 */
    now_time = millis();     /* 紀錄再次偵測到低準位的時間 */     
    if ((now_time-previous_time)>=50){  /* 若兩次偵測到低準位的時間差超過或等於 50ms表示已避開彈跳 */
        count++;     /* 計數值遞加一  */   
        if (count > 9999) /* 若累計後的計數值超過 9999 */
            count = 0;   /* 令計數值為 0 */
        begin = false;   /* 標註按鈕已被按下過 */
        }
    }
  if(!digitalRead(resetButtonPin))   /* 若重置鍵被按下(無須除彈跳) */
    count = 0;       /* 令計數值為 0 */
}
//
void calCountDigit(){
    countThousndsDigit = count/1000; /* 找出計數值的千位數值 */
   remainder = count%1000;
   countHundredsDigit = remainder/100; /* 找出計數值的百位數值 */
   remainder =  remainder%100;    countTenthsDigit = remainder/10;   /* 找出計數值的十位數值 */
   remainder =  remainder%10;
   countUnitsDigit = remainder; /* 找出計數值的個位數值 */
   dataOut[3] = countThousndsDigit;  /* 將計數值的千位數值存入輸出顯示器千位數緩衝區 */
dataOut[2] = countHundredsDigit; /* 將計數值的百位數值存入輸出顯示器百位數緩衝區 */
dataOut[1] = countTenthsDigit; /* 將計數值的十位數值存入輸出顯示器十位數緩衝區 */
dataOut[0] = countUnitsDigit;  /* 將計數值的個位數值存入輸出顯示器個位數緩衝區 */
if (countThousndsDigit != 0)
        scanDigitNum = 4; /* 如果計數值的千位數值不為 0,則掃描位數為 4 個位數 */
else if (countHundredsDigit != 0)
        scanDigitNum = 3; /* 如果計數值的千位數值為 0,且百位數值不為 0,則掃描位數為 3 個位數 */
else if (countTenthsDigit != 0)
        scanDigitNum = 2; /* 如果計數值的千位數值以及百位數值為 0,且十位數值不為 0,則掃描位數為 2 個位數 */
    else
        scanDigitNum = 1; /* 否則掃描位數為 1 個位數 */
}



程式碼說明


const int scanPin[] = {5678};


指定 UNO 第 5~8 腳為掃描訊號的輸出接腳,依序分別負責七段顯示器的個位,十位,百位,以及千位的掃瞄。採用掃描顯示方式的理由,主要在於能節省大量的 I/O 接腳,簡化硬體接線的複雜度。所以若能用快速掃描的方式,逐次送出對應位數數字的七段顯示碼,分別讓顯示器的四位數依序單獨顯示,因為人類眼睛有視覺暫留現象,人的眼睛感知到的會是四位數數字同時顯示〔參見圖五-圖九〕。


const int sevenSegCode[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90};


將數字 0 ~ 9 對應的共陽型七段顯示碼存放於 sevenSegCode[ ] 陣列中。例如圖三與圖四所示,數字 0 要能顯示於共陽型七段顯示器的個位中,則須將其對應的七段顯示碼 $11000000_2 (= C0_{16})$ 由 UNO pin 10 串列輸出至 74HC595 的 DS 接腳,並搭配時脈與栓鎖訊號的輸出,使 74HC595 作串列與並列轉換,再並列輸出給七段顯示器。接著 UNO 須將掃描信號輸出至七段顯示碼個位的 COM 端,使其 COM 端得到 Vcc 電源供應,因而 數字 0 能顯示於顯示器的個位中。

七段顯示器顯示數字 0 各節段 LED 亮熄情形
圖三  七段顯示器顯示數字 0 各節段 LED 亮熄情形


數字 0 對應的共陽型七段顯示碼
圖四  數字 0 對應的共陽型七段顯示碼


七段顯示碼送出,掃描訊號送至個位,顯示器個位顯示數字 0
圖五  七段顯示碼送出,且掃描訊號送至個位,顯示器個位顯示數字 0


七段顯示碼送出,且掃描訊號送至十位,顯示器十位顯示數字 1
圖六  七段顯示碼送出,且掃描訊號送至十位,顯示器十位顯示數字 1


七段顯示碼送出,且掃描訊號送至百位,顯示器百位顯示數字 2
圖七  七段顯示碼送出,且掃描訊號送至百位,顯示器百位顯示數字 2


圖八  七段顯示碼送出,且掃描訊號送至千位,顯示器千位顯示數字 3


若掃描速度夠快,四位顯示器出現同時顯示數字 3、2、1 以及 0 的效果

圖九  若掃描速度夠快,四位顯示器出現同時顯示數字 3、2、1 以及 0 的視覺效果



int dataOut[] = {0000};


陣列 dataOut[ ] 作為輸出四位數顯示器的數字資料的緩衝區,依序存放個位、十位、百位以及千位的數字資料。初始值皆設為 0。


void loop() {  
      while(begin){                       
            digitalWrite(latchPin, 0);        
            shiftOut(dataPin, clockPin, MSBFIRST, sevenSegCode[0]); 
            digitalWrite(latchPin, 1);        
            digitalWrite(scanPin[0], 0);      
            delay(1);                            
            digitalWrite(scanPin[0], 1);          
            push_button_detection();           
      }
       .................
}


while(begin){...} 迴圈執行的條件變數 begin 的初始值為 true,只有按鈕被按下後其值才會變 false。因此,在使用者尚未按下按鈕前,該 while 迴圈會反覆執行,直到按鈕第一次被按下,begin 值變成 false,迴圈才會停止執行。所以按鈕未曾被按下時,計數值維持初始值為 0,且四位數七段顯示器的個位必須顯示 0,且其餘位數不顯示。因此,執行 digitalWrite(latchPin, 0) 將 0 寫至  74HC595 的 ST_CP 接腳,先解除資料栓鎖,再利用 shiftOut(dataPin, clockPin, MSBFIRST, sevenSegCode[0]) 將數字 0 的七段顯示碼資料寫進  74HC595 的 DS 接腳。接著執行 digitalWrite(latchPin, 1) 將 1 寫至  74HC595 的 ST_CP 接腳進行資料栓鎖,以防止串列資料尚未轉換成並列資料輸出前,新的資料寫入造成錯誤。最後 74HC595 就會將串列傳入的數字 0 的七段顯示碼並列傳輸給七段顯示器的 a、b、c、...、g、dp 接腳。

digitalWrite(scanPin[0], 0) 可將掃描信號送至顯示器的個位,顯示器的個位就會顯示 0。delay(1) 延遲 1 ms 後,再執行 digitalWrite(scanPin[0], 1),將顯示器的個位的掃描信號關閉。

push_button_detection() 此自定函數則是用來偵測按鈕按下與否,並在除彈跳的情況下確定按鈕按下次數,以決定計數值。

數位輸出指令:digitalWrite(pin, value)


(1) pin:指定腳編號
(2) value:可為 HIGH 或 LOW
(3) 回傳值:無

八位元資料串列傳輸指令:shiftOut(dataPin, clockPin, bitOrder, value)

(1) dataPin:指定串列傳輸輸出接腳
(2) clockPin:指定時脈輸出接腳
(3) bitOrder:指定八位元資料串列傳輸方式,是最高有效位元先 (即指定 MSBFIRST) 或最低有效位元先 (即指定 LSBFIRST)。
(4) 回傳值:無

延遲時間〔單位毫秒〕指令:delay(ms)

(1) ms:指定暫停時間,單位為 ms,資料型態為 unsigned long
(2) 回傳值:無

void loop(){
    while(begin){
        ..........
    }
    if(!begin){               
        push_button_detection();
        calCountDigit();      
        for (int i = 0; i < scanDigitNum;i++){        
            digitalWrite(latchPin, 0);             
            shiftOut(dataPin, clockPin, MSBFIRST, sevenSegCode[dataOut[i]]);
            digitalWrite(latchPin, 1); 
            digitalWrite(scanPin[i], 0); 
            delay(1);  
            digitalWrite(scanPin[i], 1);        
        }    

    }
}


若按鈕已被按下過,則 begin 為 false,if(!begin){...}內的程式區塊就會被執行。首先,以自定函數 push_button_detection() 偵測按鈕狀態,接著以另一個自定函數 calCountDigit() 將計數值的千位數、百位數、十位數、個位數以及總共幾位數 (scanDigitNum) 找出,並將千位數、百位數、十位數以及個位數數值分別存於顯示緩衝區陣列中。

迴圈 for(int i = 0; i < scanDigitNum; i++){...} 執行次數取決於計數值總共幾位數。  digitalWrite(latch, 0) 將  74HC595 的資料栓鎖解除,再由 shiftOut(dataPin, clockPin, MSBFIRST, sevenSegCode[dataOut[i]]) 將顯示資料緩衝區存放的第 i 位數數值資料對應的七段顯示碼輸出給 74HC595。接著執行 digitalWrite(latch, 0) 將送出給  74HC595 的資料栓鎖,再執行 digitalWrite(scanPin[i], 0) 送出掃描訊號給顯示器第 i 位數,使得第 i 位數的數字可以顯示。delay(1) 延遲 1 ms 後執行 digitalWrite(scanPin[i], 1) 將先前給顯示器第 i 位數的掃描訊號關閉。


void push_button_detection(){
      while(!digitalRead(pushButtonPin)){     
            previous_time = millis();             
            while(!digitalRead(pushButtonPin));   
            now_time = millis();                       
            if ((now_time-previous_time)>=50 ){   
                  count++;                            
            if (count >9999)                    
                count = 0;                        
            begin = false;                       
            }
    }
    if(!digitalRead(resetButtonPin))        
        count = 0;                            
}


push_button_detection() 是用來偵測按鈕按下與否的自定函數。須注意的是:由於按鈕按下時,機械開關在閉合與鬆開時,如圖十所示會有彈跳現象的發生。若 UNO 偵測按鈕狀態的接腳以接收到低準位判斷按鈕按下,且接收到高準位判定按鈕鬆開,則彈跳現象會造成使用者只按下一次,UNO 卻判定按鈕被按下超過一次以上。所以,當系統接收到按鈕按下的次數必須是精確時,勢必要有除彈跳的措施,以防止誤判。


按鈕按下與鬆開時各會出現一段不穩定的彈跳現象
圖十  按鈕按下與鬆開時各會出現一段不穩定的彈跳現象


所以 while(!digitalRead(pushButtonPin)){ ... }的迴圈執行條件為按鈕按下,UNO 的 pin 4 接收到低準位〔無論是在彈跳範圍內,還是在穩定低準位範圍 〕。但為了確定接收到的是穩定狀態時的低準位,我們可先執行 previous_time = millis() 紀錄每次 while 迴圈一開始接收到低準位時的時間,再利用 「while(!digitalRead(pushButtonPin)); 」等待下一個接收到低準位的發生, 再執行 now_time = millis() 紀錄每次 while 迴圈一開始第二次接收到低準位的發生時間。

接著用 if ((now_time-previous_time)>=50 ){ ... },判斷每次 while 迴圈一開始第一次接收到低準位到 while 迴圈一開始第二次接收到低準位時的時間差是否超過 50 ms。若是表示二次接收到低準位時,已避開彈跳進入穩定狀態。不同類型的按鈕開關,或許用來判斷是否已避開彈跳使用到的上述的時間差不見得是 50 ms。可藉由 UNO 只接按鈕電路搭配下列程式碼作確認或調整,確認按鈕每按下一次,count 值僅遞加一,且又不至於因設定的判斷時間差值太大,偵測反應速度變差或誤判。如圖十一所示,利用 Arduino 的 Serial Monitor 作為防彈跳效果的確認依據,且測試時按鈕按下不放的持續時間可變化測試。


測試按鈕防彈跳效果程式碼


const int push_button = 7;
void push_button_detection();
int count = 0;
unsigned long previous_time,now_time;

void setup() {
  pinMode(push_button,INPUT);
  Serial.begin(9600);
}
 
void loop() {
  push_button_detection();
}

void push_button_detection(){
     while(!digitalRead(push_button)){  // 偵測到 "0"
        previous_time = millis();
        while(!digitalRead(push_button));    // 又偵測到 "0"
        now_time = millis();
        if ((now_time-previous_time)>=50 ){
           count++;
           Serial.print("按鈕按下第 ");
           Serial.print(count);
           Serial.println(");
       }
    }
}

 

測試除彈跳效果
圖十一   測試除彈跳效果

當 UNO 接收到的低準位是位於穩定狀態,則執行 count++ 將計數值遞加一。若遞加一過後的 count 值已超過四位數顯示器能顯示的最大數值 9999,則執行  

if (count >9999)                   
count = 0;

將 count 值歸零。並且執行 begin = false 標註按鈕已被按下過了。接著執行 if(!digitalRead(resetButtonPin)) 偵測第二個按鈕即重置按鍵是否被按下。若被按下則執行將 count = 0 將計數值重置歸零。值得注意的是:偵測重置按鍵是否被按下,並不需要除彈跳。因為重置按鍵實際按下一次,無論 UNO 判定偵測到幾次,都是要將計數值歸零,並沒有影響。

讀取數位輸入指令:digitalRead(pin)


(1) pin:指定腳編號
(2) 回傳值:可為 HIGH 或 LOW

取得 Arduino 開發版開始執行目前程式至當下歷經時間millis()

回傳值:Arduino 開發版開始執行目前程式至當下歷經時間,單位為毫秒 (ms),資料型態為 unsigned long。

與 Serial Monitor 通訊的鮑  (Baud) 設定指令 Serial.begin(speed)

(1) speed:鮑的設定值,資料型態為 long
(2) 回傳值:無

傳輸後並在 Serial Monitor 列印指令Serial.print(val)

(1) val:要列印的資料,任何資料型態皆可
(2) 回傳值:傳輸後列印的資料位元組數,資料型態為 size_t

傳輸後並在 Serial Monitor 列印且回車換行指令Serial.println(val)

(1) val:要列印的資料,任何資料型態皆可
(2) 回傳值:傳輸後列印的資料位元組數,資料型態為 size_t


void calCountDigit(){
       countThousndsDigit = count/1000;        
        remainder = count%1000;
        countHundredsDigit = remainder/100;     
        remainder =  remainder%100;
        countTenthsDigit = remainder/10;       
        remainder =  remainder%10;
        countUnitsDigit = remainder;           
        dataOut[3] = countThousndsDigit;      
        
dataOut[2] = countHundredsDigit;      
        
dataOut[1] = countTenthsDigit;        
        
dataOut[0] = countUnitsDigit;         
        if (countThousndsDigit != 0)           
            scanDigitNum = 4;
        else if (countHundredsDigit != 0)      
            scanDigitNum = 3;
        else if (countTenthsDigit != 0)  
            scanDigitNum = 2;
        else                                   
            scanDigitNum = 1;
}



calCountDigit() 自定函數用來找出計數值的千位、百位、十位以及個位的值,並分別將這些值存入顯示緩衝區,最後再判斷掃描的位數。例如計數值若為 365,則 365 除以 1000,可得到商為 0 〔亦即其千位數數值 countThousndsDigit 為 0〕,且餘數〔remainder〕為 365。再將餘數除以 100,可得到商為 3 〔亦即其百位數數值 countHundredsDigit 為 3〕,且餘數為 65。再將餘數 65 除以 10,可得到商為 6 〔亦即其十位數數值 countTenthsDigit 為 6〕,且餘數為 5。而最後的餘數 5 即為個位數數值〔countUnitsDigit〕。接著再將 countThousndsDigit、countHundredsDigit、countTenthsDigit 以及 countUnitsDigit 分別存入顯示緩衝區 dataOut[3]、dataOut[2]、dataOut[1] 及 dataOut[0] 中。

經上面的說明,下列的程式碼就應該容易理解。


countThousndsDigit = count/1000;        
remainder = count%1000;
countHundredsDigit = remainder/100;     
remainder =  remainder%100;
countTenthsDigit = remainder/10;       
remainder =  remainder%10;
countUnitsDigit = remainder;    
dataOut[3] = countThousndsDigit;      
dataOut[2] = countHundredsDigit;      
dataOut[1] = countTenthsDigit;        
dataOut[0] = countUnitsDigit;  

最後判斷掃描位數的部分,執行 if (countThousndsDigit != 0) ,判斷計數值的千位數值是否為 0,若是表示須掃描四位數,則  scanDigitNum = 4;若不是往下繼續執行  else if (countHundredsDigit != 0) ,判斷百位數值是否為 0,若是表示須掃描三位數,則 scanDigitNum = 3;若不是往下繼續執行 else if (countTenthsDigit != 0) ,判斷十位數值是否為 0,若是表示須掃描二位數,則 scanDigitNum = 2;若不是執行 else ,且 scanDigitNum = 1,表示只須掃描一位數。                      
          


留言

這個網誌中的熱門文章

三段式電子開關電路

陣列 (C++)

首數、尾數與位數

分壓偏壓 BJT 放大電路的直流分析及其近似解的條件

MOSFET 共汲極放大電路 (源極隨耦器) 小訊號分析

為什麼理想的 OPA 電壓放大器有虛短路與虛斷路現象

具有倒數計時自動回復功能的行人穿越道號誌控制電路