2012/02/21

Arduino練習:seven-segment display七段顯示器與時鐘

之前用LED製作呼吸燈霹靂車燈以揚聲器演奏青花瓷,這篇要使用seven-segment display七段顯示器製作時鐘。

首先使用Arduino的8個腳位,直接控制七段顯示器,讓七段顯示器從0數到9。然後改用4511控制七段顯示器,就只需要4個腳位即可。然後再加上三個七段顯示器、再加上三顆4511,製作時鐘。

電路圖(Fritzing格式)與程式原始碼,可在此下載

開始吧,先直接以Arduino的腳位控制,讓七段顯示器亮起來吧。

注意:我使用的是共陰極七段顯示器,之後的4511不能搭配共陽極。

共陰極七段顯示器的腳位圖。


電路圖:

左方是Arduino的腳位,右方是七段顯示器的腳位。連接時,都加上一個220 ohm電阻。
2連到7(A)。
3連到6(B)。
4連到4(C)。
5連到2(D)。
6連到1(E)。
7連到9(F)。
8連到10(G)。
9連到5(DP)。

從Arduino的GND接到麵包版上,讓七段顯示器的3與8接地。



接好後,先寫個小程式測試看看,讓七段顯示器閃爍。
#define NUM 8
int pins[NUM] = {2, 3, 4, 5, 6, 7, 8, 9};

void setup() {               
  for(int i = 0; i < NUM; i++){
    pinMode(pins[i], OUTPUT);   
  }
}

void loop() {
  for(int i = 0; i < NUM; i++){
    digitalWrite(pins[i], HIGH);
  }
  delay(1000);
  for(int i = 0; i < NUM; i++){
    digitalWrite(pins[i], LOW);
  } 
  delay(1000);
}



其實就跟閃爍單個LED一樣,只不過現在要閃爍8個LED罷了。

接下來,該讓七段顯示器作正事了,也就是顯示數字,底下程式碼要從0數到9。

首先要定義,顯示各數字時,要點亮、熄滅七段顯示器的哪些部分。

#define NUM 8
int pins[NUM] = {2, 3, 4, 5, 6, 7, 8, 9};

// 底下就是顯示某數字時,該點亮熄滅哪些腳位的定義,
// data是個二維陣列,其中10代表有10個數字(從數字0、1、2、到9),
// 然後,每個數字有8個boolean值,
// 依序代表該不該點亮七段顯示器的A、B、C、D、E、F、G、DP。
// 這部份的資料,依序對應上面pins陣例裡的腳位順序,
// 這裡的對應關係,要按照先前的接線予以定義。
// t代表true,要點亮,
// f代表false,不要點亮。
#define t true
#define f false
boolean data[10][NUM] = {
  {t, t, t, t, t, t, f, f}, // 0
  {f, t, t, f, f, f, f, f}, // 1
  {t, t, f, t, t, f, t, f}, // 2
  {t, t, t, t, f, f, t, f}, // 3
  {f, t, t, f, f, t, t, f}, // 4
  {t, f, t, t, f, t, t, f}, // 5
  {t, f, t, t, t, t, t, f}, // 6
  {t, t, t, f, f, f, f, f}, // 7
  {t, t, t, t, t, t, t, f}, // 8
  {t, t, t, t, f, t, t, f}, // 9
};

void setup(){               
  for(int i = 0; i < NUM; i++){
    pinMode(pins[i], OUTPUT);   
  }
}

// 把七段顯示器的顯示函式獨立出來。
// 輸入數字n(從0到9)
// 根據數字n,就可從data[n]找出七段顯示器的點亮、熄滅資料,
// 然後用迴圈,逐一設定8個腳位的HIGH或LOW,
// 也就等於點亮、熄滅那8個部分
void writeNumber(int n){
  for(int i = 0; i < NUM; i++){
    digitalWrite(pins[i], data[n][i] == t ? HIGH : LOW);
  }
}

// 寫好writeNumber()後,loop()就簡單了。
// 迴圈從0到9,以七段顯示器顯示該數字,中間延遲1秒。
// 也就是說,會從0數到9,然後再從頭從0開始數。
void loop(){
  for(int n = 0; n <= 9; n++){
    writeNumber(n);
    delay(1000);
  }
}


以4511控制:

上頭直接以Arduino板的8個腳位控制七段顯示器,底下改以4511控制,以4個腳位。(若以74HC595,就能以3個腳位控制8個輸出。之前的霹靂車燈以2顆74HC595控制16個LED。)

注意,4511必須跟共陰極的七段顯示器搭配。



4511的腳位說明:

7(input A)、1(input B)、2(input C)、6(input D),4個輸入腳位,從0(0x0000)到9(0x1001),若超過9,那麼7個輸出全部會是LOW,也就是都不亮。

3(lamp test),若為LOW,那麼輸出腳位全部為HIGH,不管輸入腳位的狀態。

4(ripple blanking),當lamp test為HIGH時,若此腳位為LOW,那麼輸出腳位全部為LOW,不管輸入腳位的狀態。

5(enable/store input),若為LOW,輸出腳位隨著輸入腳位而改變,若為HIGH,輸出腳位的狀態會被鎖住不變。

13(a)、12(b)、11(c)、10(d)、9(e)、15(f)、14(g),7個輸出腳位,控制七段顯示器。

8(VSS),接地。

16(VDD),可接+3~15V。

利用4511的ABCD四個腳位輸入,控制七段顯示器的abcdefg。


電路圖:

從Arduino板接5V與GND到麵包板。

4511的腳位接線:
16(VDD)接5V。
8(VSS)接地。
3(lamp test)接5V,本例不使用此功能。
4(ripple blanking)接5V,本例不使用此功能。
5(enable/store input)接地,本例不使用此功能。

13(a)、12(b)、11(c)、10(d)、9(e)、15(f)、14(g),7個輸出腳位,接到七段顯示器相對應的腳位。
7(input A)、1(input B)、2(input C)、6(input D),4個輸入腳位,接到Arduino板的腳位2、3、4、5。

七段顯示器的腳位5(DP)接地,本例不使用小數點。(其實之前從0數到9時,也不需要使用小數點。)
共陰極七段顯示器的腳位3與8要接地。

下圖先連接4511與七段顯示器之間的接線。



然後是4511的7(input A)、1(input B)、2(input C)、6(input D),4個輸入腳位,依序接到Arduino板的腳位2、3、4、5。。



接下來就是程式碼了,從0數到9。

// 以Arduino板的4個腳位2、3、4、5控制,
// 控制4511的輸入腳位7(A)、1(B)、2(C)、6(D)。
int pins[4] = {2, 3, 4, 5};

void setup(){
  for(int i = 0; i < 4; i++){
    pinMode(pins[i], OUTPUT);   
  }
}

// 以迴圈依序設定4個腳位。
// 參數n,其第0個bit,要輸出到4511的腳位7(input A),
// 它接到Arduino板的腳位2,也就是pins[0]。
// 而取得n的第i個bit的方式是,
// 先以>>運算子把n向右位移i個bit,然後與0x01進行作&運算。
// 其餘bit同理。

void writeDigit(int n){
  for(int i = 0; i < 4; i++){
    digitalWrite(pins[i], (n >> i) & 0x01);
  }
}

// 寫好writeDigit()後,loop()就簡單了。
// 以迴圈依序顯示0到9,中間延遲1秒。
void loop(){
  for(int n = 0; n <= 9; n++){
    writeDigit(n);
    delay(1000);
  }
}

從0數到9的影片在此

時鐘:

接下來,要製作時鐘,可以顯示幾點、幾分,總共需要4個七段顯示器,以及4個4511。

電路圖:

延續前面的電路,加上3個4511,把短的線接一接。
16(VDD)接5V。
8(VSS)接地。
3(lamp test)接5V,本例不使用此功能。
4(ripple blanking)接5V,本例不使用此功能。
5(enable/store input)接地,本例不使用此功能。

從右到左的4511,分別將要控制時鐘的分鐘個位數、分鐘十位數、小時個位數、小時十位數。

注意,小時十位數(最左邊)的4511,其腳位12(b)接電阻後接地,這是因為,小時十位數的七段顯示器,只會出現0、1、2這三個數字,你可以往前看看ABCD與abcdefg的對應表,若七段顯示器只需顯示0、1、2時,其b腳位只能為HIGH,所以這裡就把4511的12(b)腳位接地不用。



然後加上3個七段顯示器,把短的線以及電阻接一接。(我220 ohm電阻不夠了,有些改成別的差不多的。)
七段顯示器的腳位5(DP)接地,本例不使用小數點。
共陰極七段顯示器的腳位3與8要接地。

注意,小時十位數(最左邊)的七段顯示器,其腳位6(b)接地,理由之前說過了。




然後,4511與七段顯示器之間的連接線。
4511的13(a)、12(b)、11(c)、10(d)、9(e)、15(f)、14(g),7個輸出腳位,接到七段顯示器相對應的腳位。小心慢慢接。



然後,4511與Arduino板的連接線。

分鐘個位數的4511,7(input A)、1(input B)、2(input C)、6(input D),4個輸入腳位,依序接到Arduino板的腳位2、3、4、5。(這個之前應該就接好了。)

分鐘十位數的4511,7(input A)、1(input B)、2(input C)、6(input D),4個輸入腳位,依序接到Arduino板的腳位6、7、8、9

小時個位數的4511,7(input A)、1(input B)、2(input C)、6(input D),4個輸入腳位,依序接到Arduino板的腳位10、11、12、13

小時十位數的4511,7(input A)、1(input B)、2(input C)、6(input D),4個輸入腳位,依序接到Arduino板的腳位A0、A1、接地、接地

注意,小時十位數的4511的2(input C)、6(input D),接地。這是因為,小時十位數的七段顯示器,只會出現0、1、2這三個數字,你可以往前看看ABCD與abcdefg的對應表,若4511只需輸入0、1、2時,其C、D輸入腳位一定是LOW,所以這裡此兩腳位接地不用。




接下來,先寫個測試程式,測試看看Arduino是否正確控制4511的輸入腳位,測試七段顯示器是否能正常顯示每個數字。

// 定義腳位
const int minute_0[4] = {2, 3, 4, 5}; // 分鐘個位數
const int minute_1[4] = {6, 7, 8, 9}; // 分鐘十位數
const int hour_0[4] = {10, 11, 12, 13}; // 小時個位數
const int hour_1[2] = {A0, A1}; // 小時十位數

// 將數字n,輸出到腳位pins,bit_count是腳位的數量
void writeDigit(const int n, const int *pins, const int bit_count){
  for(int i = 0; i < bit_count; i++){
    digitalWrite(pins[i], (n >> i) & 0x01);
  }
}

// 在setup()裡初始化,讓四個七段顯示器都顯示數字0。
void setup(){

  for(int i = 0; i < 4; i++){
    pinMode(minute_0[i], OUTPUT);
    pinMode(minute_1[i], OUTPUT);
    pinMode(hour_0[i], OUTPUT);
  }
  pinMode(hour_1[0], OUTPUT);
  pinMode(hour_1[1], OUTPUT);
 
  writeDigit(0, minute_0, 4);
  writeDigit(0, minute_1, 4);
  writeDigit(0, hour_0, 4);
  writeDigit(0, hour_1, 2);
}

// 從0數到9,其中小時十位數只能顯示0、1、2。
void loop(){
  for(int i = 0; i <= 9; i++){
    writeDigit(i, minute_0, 4);
    writeDigit(i, minute_1, 4);
    writeDigit(i, hour_0, 4);
    writeDigit(i % 3, hour_1, 2);
    delay(1000);
  }
}

成功的話,右邊三個七段顯示器就會從0數到9,最左邊的會顯示0、1、2。


接下來,要製作時鐘,從0000、0001、一直到2359,然後回到0000。

程式碼:
const int minute_0[4] = {2, 3, 4, 5};
const int minute_1[4] = {6, 7, 8, 9};
const int hour_0[4] = {10, 11, 12, 13};
const int hour_1[2] = {A0, A1};

void writeDigit(const int n, const int *pins, const int bit_count);
void setTime(unsigned long h, unsigned long m, unsigned long s);

void setup(){
  Serial.begin(115200);
  for(int i = 0; i < 4; i++){
    pinMode(minute_0[i], OUTPUT);
    pinMode(minute_1[i], OUTPUT);
    pinMode(hour_0[i], OUTPUT);
  }
  pinMode(hour_1[0], OUTPUT);
  pinMode(hour_1[1], OUTPUT);
 
  setTime(7, 32, 0); // 在程式裡寫死某時間,7時32分0秒
  // 之後我們會從外界設定時間
}

void writeDigit(const int n, const int *pins, const int bit_count){
  for(int i = 0; i < bit_count; i++){
    digitalWrite(pins[i], (n >> i) & 0x01);
  }
}

// 代表一天的millisecond,在數字後加上UL,代表是unsigned long
// 若不加上UL,會發生溢位overflow
// 24*60*60 = 86400,大於int或unsigned int(2 bytes)的範圍,
// 若不加上UL,24*60*60,正確應該是0b10101000110000000,
// 溢位後,變成0b0101000110000000,也就是20864。
#define MILLISECOND_OF_ONE_DAY (24UL * 60UL * 60UL * 1000UL)

// 這會是目前顯示在七段顯示器上的時間,單位為millisecond
unsigned long currentTime;

// 用來記住上次更新currentTime時,是在何時
unsigned long previousTimeMark;

// 設定顯示時間的函式,h小時、m分鐘、s秒
void setTime(unsigned long h, unsigned long m, unsigned long s){
  currentTime = ((h * 60 + m) * 60 + s) * 1000;
  previousTimeMark = millis();
  updateTime(0);
}

// 負責更新顯示時間的函式,
// 參數timePassed代表,距離上次更新,經過了多少millisecond
void updateTime(const unsigned long timePassed){
  unsigned long h;
  unsigned long m;
  unsigned long s;

  // 加上經過的時間後,若大於一天,就減去。
  currentTime += timePassed;
  if(currentTime >= MILLISECOND_OF_ONE_DAY){
    currentTime -= MILLISECOND_OF_ONE_DAY;
  }

  // currentTime記錄的是millisecond,
  // 底下算出小時、分鐘、秒
  s = currentTime / 1000;
  m = s / 60;
  s = s % 60;
  h = m / 60;
  m = m % 60;

  // 顯示小時跟分鐘
  writeDigit(m % 10, minute_0, 4);
  writeDigit(m / 10, minute_1, 4);
  writeDigit(h % 10, hour_0, 4);
  writeDigit(h / 10, hour_1, 2);
}

void loop(){
  // millis()會回傳,這支程式開始執行後,所經過的時間,單位是millisecond。
  unsigned long now = millis();

  // 減去previousTimeMark上次更新顯示時間的時間,
  // 若大於1秒,就呼叫updateTime()更新,
  // 傳入距離上次更新所經過的時間
  if(now - previousTimeMark > 1000){
    updateTime(now - previousTimeMark);
    previousTimeMark = now;
  }
}

成功後,就可以看到下圖了。


不過,上面的程式碼,在程式碼寫死起始時間,這可不行啊,底下修改後,透過序列埠,從外界(電腦)進行時間設定。

在setup()裡要記得加入Serial.begin(115200);。

int b[4];
int len = 0;
void loop(){
  unsigned long now = millis();
  if(now - previousTimeMark > 1000){
    updateTime(now - previousTimeMark);
    previousTimeMark = now;
  }
 
  int btemp = Serial.read();
  if(btemp != -1){
    if(0x30 <= btemp && btemp <= 0x39){
      b[len] = btemp - 0x30;
      len++;
      if(len == 4){
        setTime(b[0] * 10 + b[1], b[2] * 10 + b[3], 0);
        len = 0;
      }
    }
    else{
      len = 0;
    }
  }
}

利用Arduino軟體開發環境的Tools-Serial Monitor,可以透過序列埠、傳送資料給Arduino板子,以腳位0(RX)接收。

呼叫Serial.read()讀取1個byte,放在btemp裡,若無資料,則btemp為-1,若有資料(傳來的會是ASCII碼、0x30是數字0、0x39是數字9),放進b陣列裡存起來,以len記錄收到了多少個byte。收到4個數字後,把前面兩個數字當做小時、後面兩個數字當做分鐘,進行時間設定。

若收到數字以外的資料,重置len為0,表示之前若有收到資料,通通捨棄。

輸入2302,代表要把時間設定為23時02分。


完成囉。

改進的地方,也可以加入設定時間的開關或按鈕,譬如說,某個按鈕按一下會增加一分鐘,另一個按鈕按一下會增加一小時,之類的。

改進的地方,右邊數來第二顆4511,跟Arduino板之間的接線,其實3條即可(因為它只需顯示0到5)。

改進的地方,這個時鐘只顯示小時與分鐘,沒有秒,可加入兩顆LED,每一秒閃爍一下。好像有那種一組裡有四個七段顯示器,並且中間有兩顆LED的,但我沒找到,而且那種一組四個七段顯示器的產品,要用掃描的方式驅動。


參考資料:

13 comments:

  1. 請問一下,我程式碼修改過了,但是沒辦法外界修改時間!!請問要怎麼去修改

    ReplyDelete
  2. 我是有照大大的程式碼去做:
    const int minute_0[4] = {2, 3, 4, 5};
    const int minute_1[4] = {6, 7, 8, 9};
    const int hour_0[4] = {10, 11, 12, 13};
    const int hour_1[2] = {A0, A1};

    void writeDigit(const int n, const int *pins, const int bit_count);
    void setTime(unsigned long h, unsigned long m, unsigned long s);

    void setup(){
    Serial.begin(115200);
    int b[4];
    int len = 0;
    for(int i = 0; i < 4; i++){
    pinMode(minute_0[i], OUTPUT);
    pinMode(minute_1[i], OUTPUT);
    pinMode(hour_0[i], OUTPUT);
    }
    pinMode(hour_1[0], OUTPUT);
    pinMode(hour_1[1], OUTPUT);

    setTime(8, 35, 0); // 在程式裡寫死某時間,7時32分0秒
    // 之後我們會從外界設定時間
    int btemp = Serial.read();
    if(btemp != -1){
    if(0x30 <= btemp && btemp <= 0x39){
    b[len] = btemp - 0x30;
    len++;
    if(len == 4){
    setTime(b[0] * 10 + b[1], b[2] * 10 + b[3], 0);
    len = 0;
    }
    }
    else{
    len = 0;
    }
    }
    }



    void writeDigit(const int n, const int *pins, const int bit_count){
    for(int i = 0; i < bit_count; i++){
    digitalWrite(pins[i], (n >> i) & 0x01);
    }
    }

    // 代表一天的millisecond,在數字後加上UL,代表是unsigned long
    // 若不加上UL,會發生溢位overflow
    // 24*60*60 = 86400,大於int或unsigned int(2 bytes)的範圍,
    // 若不加上UL,24*60*60,正確應該是0b10101000110000000,
    // 溢位後,變成0b0101000110000000,也就是20864。
    #define MILLISECOND_OF_ONE_DAY (24UL * 60UL * 60UL * 1000UL)

    // 這會是目前顯示在七段顯示器上的時間,單位為millisecond
    unsigned long currentTime;

    // 用來記住上次更新currentTime時,是在何時
    unsigned long previousTimeMark;

    // 設定顯示時間的函式,h小時、m分鐘、s秒
    void setTime(unsigned long h, unsigned long m, unsigned long s){
    currentTime = ((h * 60 + m) * 60 + s) * 1000;
    previousTimeMark = millis();
    updateTime(0);
    }

    // 負責更新顯示時間的函式,
    // 參數timePassed代表,距離上次更新,經過了多少millisecond
    void updateTime(const unsigned long timePassed){
    unsigned long h;
    unsigned long m;
    unsigned long s;

    // 加上經過的時間後,若大於一天,就減去。
    currentTime += timePassed;
    if(currentTime >= MILLISECOND_OF_ONE_DAY){
    currentTime -= MILLISECOND_OF_ONE_DAY;
    }

    // currentTime記錄的是millisecond,
    // 底下算出小時、分鐘、秒
    s = currentTime / 1000;
    m = s / 60;
    s = s % 60;
    h = m / 60;
    m = m % 60;

    // 顯示小時跟分鐘
    writeDigit(m % 10, minute_0, 4);
    writeDigit(m / 10, minute_1, 4);
    writeDigit(h % 10, hour_0, 4);
    writeDigit(h / 10, hour_1, 2);
    }

    void loop(){
    // millis()會回傳,這支程式開始執行後,所經過的時間,單位是millisecond。
    unsigned long now = millis();

    // 減去previousTimeMark上次更新顯示時間的時間,
    // 若大於1秒,就呼叫updateTime()更新,
    // 傳入距離上次更新所經過的時間
    if(now - previousTimeMark > 1000){
    updateTime(now - previousTimeMark);
    previousTimeMark = now;
    }
    }
    這是我看您的去修改,發現板子還是的數字是固定的,沒辦法用COM改想要的數字,只能用裡面的程式碼固定

    ReplyDelete
    Replies
    1. 嗯,沒改對吧, Serial.read()的部份應放在loop()裡。
      完整的程式碼,請到
      https://github.com/yehnan/arduino_practices/tree/master/Clock
      下載。

      Delete
  3. 大大,我試過了,發現還是一樣沒辦法去改...可以交一下怎麼修改嗎??

    ReplyDelete
    Replies
    1. > 可以交一下怎麼修改嗎??
      這篇文章的內容,就是使用四個七段顯示器製作時鐘,並透過序列埠設定時間。

      若碰到問題,請說明。

      Delete
  4. 請問一下我是剛進來的的新手 怎麼使用按鈕控制 7段顯示上數 之後在伺服馬達震盪反覆啊

    我只打出7段顯示上數 之後就不知道怎麼用按鈕和馬達

    ReplyDelete
    Replies
    1. > 7段顯示上數
      什麼意思?

      已經學會按鈕和馬達了嗎?若尚未,請參閱其他文章,
      http://yehnan.blogspot.tw/2012/02/arduino_21.html
      譬如:
      Arduino練習:以開關切換LED明滅狀態
      Arduino練習:seven-segment display七段顯示器與時鐘
      Arduino練習:Simon Says請你跟我這樣做
      Arduino練習:伺服馬達以Tower Pro SG90為例

      也可參閱拙作《Arduino輕鬆入門:範例分析與實作設計》,
      http://yehnan.blogspot.tw/2014/02/arduino_21.html

      學會操控各個零件之後,接著就是程式的部份。

      Delete
  5. 我想用上大顆的七段顯示器,但顯示器運行用9-12v DC,我應該怎樣接?謝謝

    ReplyDelete
    Replies
    1. 哪個產品呢?

      Delete
    2. 4吋1位共陽的40101BS

      Delete
    3. 嗯,電壓電流都大,不是該由Arduino板子提供的程度。
      要另外準備電源,還有驅動器,例如電晶體、達靈頓、或七段顯示器的解碼器,
      Arduino與七段顯示器之間插入驅動器,而驅動器要由外部電源提供電力,
      最後記得共同接地。

      Delete
    4. 葉老師請問您,我現在要驅動+9V才會亮的共陽七段(編號:BA-S2320SBAW),
      我使用ULN2003+74LS47來接七段,
      請問這時候ARDUINO的腳位(2,3,4,5)是接ULN2003的1B~4B嗎?
      然後ULN2003的1C~4C是接74LS47的ABCD嗎?
      然後外部供電的+9V及GND該接哪些呢?
      有點頭大,還請老師指導,謝謝您。

      Delete