2012/02/19

Arduino練習:霹靂車燈

之前以PWM製作會呼吸的LED以揚聲器演奏青花瓷,這一篇要用16個LED,製作出左右來回跑的效果,也就是霹靂車燈。

夥計,我需要你!


我將先用Arduino的16個腳位,控制16個LED,然後再加入兩顆74HC595,就只需3個腳位即可。

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

電路圖:
16個220 ohm電阻,一端接地,另一端接到LED的短腳。
從Arduino板子的腳位A3、A2、A1、A0、2、3、4、5、6、7、8、9、10、11、12、13,分別接到LED的長腳。



程式碼:
#define NUM 16 // 首先定義LED的數目

// 然後是LED對應的腳位,請配合接線順序填寫。
int leds[NUM] = {
  A3, A2, A1, A0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
};

// 將每個腳位模式設為OUTPUT
void setup() {               
  for(int i = 0; i < NUM; i++){
    pinMode(leds[i], OUTPUT);
  }    
}
// 雖然A3、A2、A1、A0是類比腳位,
// 但也可以把它當做數位腳位使用。

// 然後是兩個迴圈,
// 第一個迴圈從這邊跑到那邊,
// 第二個迴圈跑回來。
void loop() {
  for(int i = 0; i < NUM; i++){
    digitalWrite(leds[i], HIGH);
    delay(100);
    digitalWrite(leds[i], LOW);
  }
  for(int i = NUM-1; i >= 0; i--){
    digitalWrite(leds[i], HIGH);
    delay(100);
    digitalWrite(leds[i], LOW);
  }
}
// 以degitalWrite點亮LED後,延遲100 milliseconds(0.1秒),
// 然後再滅掉,你可以修改延遲時間。
// 你可以把第二個迴圈改為 for(int i = NUM-2; i > 0; i--),
// 看看有何不同。

跑起來的效果,可以看看我上傳到YouTube的影片。(第二個迴圈為 for(int i = NUM-2; i > 0; i--)的情形。)


你可能覺得奇怪,左邊8個LED比較亮,右邊8個LED比較暗,其實也沒什麼道理,因為我買了兩組不同的LED,哈哈。

運用74HC595:

上面用了Arduino板的16個腳位,控制16個LED,感覺很浪費。接下來,我們將使用兩顆74HC595與函式shiftOut(),以3個腳位控制16個LED。

所謂74HC595,資料上寫著8-bit、serial-in、serial or parallel-out shift register、with output latches、3-state。

8-bit:它可以儲存8個bits的資料。
serial-in:以序列的方式,把資料傳給它,也就是一次傳一個bit給它。
serial or parallel-out:若是parallel-out並列輸出,就可控制8個LED。至於serial-out序列輸出,可用來接到下一顆74HC595,那麼,當你再傳入8個bits的資料時,先前的資料就會被送到下一顆74HC595,兩顆串起來後可控制16個LED。
shift register:移位暫存器,一次傳一個bit給它,前一個bit會被移位到下一個位置。
output latch:可以鎖住輸出腳位的狀態。
3-state:輸出可以有三種狀態HIGH、LOW、高阻抗(high impedance),本例不使用高阻抗狀態。

簡單講就是,我們將以三個腳位,將LED亮暗的資料存到74HC595,一顆74HC595有8個輸出、控制8個LED,兩顆就能控制16個LED。


595的腳位說明:
Pin 1~7(Q1~Q7)、Pin 15(Q0),8 bits的輸出。
Pin 8(GND)接地。
Pin 9(Q7" ),序列輸出。
Pin 10(MR),Master Reclear清除所有資料,active low低電位有效。
Pin 11(SH_CP),SHift register Clock Pin位移時脈腳位。又稱clock pin。
Pin 12(ST_CP),STorage register Clock Pin儲存時脈腳位,又稱latch pin鎖住腳位。
Pin 13(OE),Output enable允許輸出,active low低電位有效。
Pin 14(DS),Data Serial input序列資料輸入,又稱data pin資料腳位。
Pin 16(Vcc),供應正電壓。

電路圖:
先前從Arduino板子接到LED的16條線,全部拔掉。

從Arduino板的5V與GND腳位,接到麵包板上。

Pin 1~7(Q1~Q7)、Pin 15(Q0),依序接到8個LED,我把Q0接到最旁邊的LED。
Pin 8(GND),接地。
Pin 9(Q7" ),目前暫時未使用,之後會接到下一個74HC595。
Pin 10(MR) ,此例不使用此功能,所以接到5V。
Pin 11(SH_CP),接Arduino板子的腳位4。
Pin 12(ST_CP),接Arduino板子的腳位3。
Pin 13(OE),此例不使用此功能,所以接地。
Pin 14(DS),接Arduino板子的腳位2。
Pin 16(Vcc) ,接5V。

還有8個LED,目前暫時未使用。

P.S. 若有LED閃爍的現象,可在Pin 12(ST_CP)上接0.1 uF的電容,除去閃爍現象。







程式碼:
首先先寫個小程式,從右到左,輪流點亮每一個LED,一次只點亮一個LED,然後不斷循環。

// 首先定義腳位
#define DataPin 2 // 接到74HC595的DS
#define LatchPin 3 // 接到74HC595的ST_CP
#define ClockPin 4 // 接到74HC595的SH_CP

// 將三個腳位模式設定為OUTPUT
void setup() {               
  pinMode(DataPin, OUTPUT);
  pinMode(LatchPin, OUTPUT);
  pinMode(ClockPin, OUTPUT);
}

void loop() {
  uint8_t number; // 這個變數裡的每一個bit,代表每一個LED亮不亮。

  // 輪流點亮每一個LED,共有8個LED。
  for (int i = 0; i < 8; i++) {
    // 送出資料前,將LatchPin設為LOW,鎖定輸出的資料。
    digitalWrite(LatchPin, LOW);

    number = 0; // 所有bit清為0,代表全部LED都不亮。
    bitSet(number, i); // 將某bit設為1,代表點亮該LED。

    // 利用shiftOut()送出資料
    shiftOut(DataPin, ClockPin, MSBFIRST, number); 

    // 送出資料結束,將LatchPin設為HIGH,更新輸出腳位。
    digitalWrite(LatchPin, HIGH);
    delay(500);
  }
}

shiftOut()用起來很簡單,給它74HC595的DataPin與ClockPin,以及資料number,它就會把資料輸出給74HC595。至於MSBFIRST的意思是most significant bit first,也就是說,最先輸出的會是number(是個uint8_t,有8個bits,第7個bit是MSB,第0個bit是LSB)的第7個bit。如果改用LSBFIRST(least significant bit first),那麼最先輸出的會是第0個bit。

shiftOut()是Arduino內建的函式,有興趣的話可以到安裝目錄下的hardware\arduino\cores\arduino\wiring_shift.c看看原始碼。

void shiftOut(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder, uint8_t val)
{
    uint8_t i;

    for (i = 0; i < 8; i++)  {
        if (bitOrder == LSBFIRST)
            digitalWrite(dataPin, !!(val & (1 << i)));
        else   
            digitalWrite(dataPin, !!(val & (1 << (7 - i))));
           
        digitalWrite(clockPin, HIGH);
        digitalWrite(clockPin, LOW);       
    }
}
可以得知,val接受大小為8 bits,所以shiftOut只能輸出8 bits的資料,根據參數bitOrder為LSBFIRST或MSBFIRST,依序輸出每一個bit,寫入dataPin;然後將clockPin拉上(從下到上),告知74HC595把資料吃進去,最後把clockPin拉下,準備給下一次的資料使用。

你可能會覺得奇怪,為什麼要用兩個驚嘆號(!!),這是一種慣用法。因為digitalWrite()的第二個參數應該是個布林值,如果直接丟個整數進去,會需要進行一些轉換,用了兩次驚嘆號後,就可確保得到一個型別為布林的值。(不過,在此例裡,digitalWrite()的第二個參數其實是個uint8_t。)

好,讓我們修改一下,改成霹靂車燈。

#define DataPin 2
#define LatchPin 3
#define ClockPin 4

void setup() {              
  pinMode(DataPin, OUTPUT);
  pinMode(LatchPin, OUTPUT);
  pinMode(ClockPin, OUTPUT);
}

void loop() {
  uint8_t number;
  for (int i = 0; i < 8; i++) {
    digitalWrite(LatchPin, LOW);

    number = 0;
    bitSet(number, i);
  
    shiftOut(DataPin, ClockPin, MSBFIRST, number);

    digitalWrite(LatchPin, HIGH);
    delay(300);
  }
  for (int i = 0; i < 8; i++) {
    digitalWrite(LatchPin, LOW);

    number = 0;
    bitSet(number, i);
  
    shiftOut(DataPin, ClockPin, LSBFIRST, number);

    digitalWrite(LatchPin, HIGH);
    delay(300);
  }
}
東西都跟以前一樣,第一個迴圈從頭跑到尾後,利用新加入的第二個迴圈,讓LED從尾跑回頭。第二個迴圈有很多寫法,這裡我把MSBFIRST改成LSBFIRST就行了。

第二個迴圈也可以用底下這種寫法:
  for (int i = 7; i >=0; i--) {
    digitalWrite(LatchPin, LOW);

    number = 0;
    bitSet(number, i);
  
    shiftOut(DataPin, ClockPin, MSBFIRST, number);

    digitalWrite(LatchPin, HIGH);
    delay(300);
  }

好,接下來,就是加入第二顆74HC595,總共控制16個LED。

電路圖:延續之前的接線,然後,
第一顆74HC595:
Pin 9(Q7" ),接到第二顆74HC595的Pin 14(DS)。
Pin 11(ST_CP),接到第二顆74HC595的Pin 11(ST_CP)。
Pin 12(SH_CP),接到第二顆74HC595的Pin 12(SH_CP)。

第二顆74HC595:
Pin 1~7(Q1~Q7)、Pin 15(Q0),依序接到剩下來的8個LED,請注意順序,我的第二顆74HC595的Q0,接到上一顆74HC595的Q7的旁邊,我的第二顆74HC595的Q7,接到最旁邊的LED。
Pin 8(GND),接地。
Pin 9(Q7" ),不使用。
Pin 10(MR) ,此例不使用此功能,所以接到5V。
Pin 11(ST_CP),接到第一顆74HC595的Pin 11(ST_CP)。
Pin 12(SH_CP),接到第一顆74HC595的Pin 12(SH_CP)。
Pin 13(OE),此例不使用此功能,所以接地。
Pin 14(DS),接到第一顆74HC595的Pin 9(Q7")。
Pin 16(Vcc) ,接5V。







程式碼:
#define DataPin 2 // 接到74HC595的DS
#define LatchPin 3 // 接到74HC595的ST_CP
#define ClockPin 4 // 接到74HC595的SH_CP

void setup() {              
  pinMode(DataPin, OUTPUT);
  pinMode(LatchPin, OUTPUT);
  pinMode(ClockPin, OUTPUT);
}

void loop() {
  uint16_t number;
  uint8_t highbyte;
  uint8_t lowbyte;
  for (int i = 0; i < 16; i++) {
    digitalWrite(LatchPin, LOW);

    // 因為shiftOut()一次只能輸出8 bits的資料,
    // 所以這裡分成highbyte與lowbyte,
    // highbyte的第7個bit,代表最左邊的LED,
    // lowbyte的第0個bit,代表最右邊的LED。
    number = 0; // 先將全部bit清為0
    bitSet(number, i); // 將某bit設為1

    // 然後利用 >> 與 & 運算子,將資料取出來。
    highbyte = (number >> 8) & 0xFF;
    lowbyte = number & 0xFF;
  
    shiftOut(DataPin, ClockPin, MSBFIRST, highbyte);
    shiftOut(DataPin, ClockPin, MSBFIRST, lowbyte);

    digitalWrite(LatchPin, HIGH);
    delay(100);
  }
  for (int i = 0; i < 16; i++) {
    digitalWrite(LatchPin, LOW);

    number = 0;
    bitSet(number, i);
    highbyte = (number >> 8) & 0xFF;
    lowbyte = number & 0xFF;

    // 注意,這裡以LSBFIRST的順序寫出資料,
    // 所以要先送出lowbyte,然後是highbyte。
    // 你可以先highbyte再lowbyte,看看有何不同。
    shiftOut(DataPin, ClockPin, LSBFIRST, lowbyte);
    shiftOut(DataPin, ClockPin, LSBFIRST, highbyte);

    digitalWrite(LatchPin, HIGH);
    delay(100);
  }
}
當然,第二個迴圈也可以有別種寫法,不過我就不列出來了。

跑起來的效果。跟之前以16個腳位控制的版本不太一樣,當跑到最左邊、最右邊時,感覺會停頓一下,因為那兩個LED亮的時間是其他的兩倍。想要改成不停頓效果的程式碼,我就不列出來了,改法就是去修改迴圈裡的i值。


參考資料:


33 comments:

  1. 您好
    我想請問
    74HC595是讀完8個bits後才後一同輸出嗎?
    是透過latchPin鎖住data嗎?
    謝謝

    ReplyDelete
    Replies
    1. 另外我罩着範例程式(8*LED燈表示1~255數值)
      我用Serial print抓dataPin和clockPin
      發現值是11 12 11 12...持續不斷
      想請問這是爲何?
      謝謝

      Delete
    2. 74HC595是讀完8個bits後才後一同輸出嗎?
      是透過latchPin鎖住data嗎?

      先呼叫digitalWrite(LatchPin, LOW),然後就可以
      呼叫shiftOut把資料輸出到595,
      最後呼叫digitalWrite(LatchPin, HIGH)鎖住。

      Delete
    3. 用Serial print抓dataPin和clockPin

      什麼意思?不懂你想做什麼。

      Delete
  2. 請問有將類型作品利用Persistence of Vision 產生圖案之範例嗎!? 感謝

    ReplyDelete
    Replies
    1. 沒有。

      POV可參考:
      http://www.instructables.com/id/Arduino-LEDs-fan-POV-APPLAUSE-sign/
      http://www.raspberrypi.org/mike-cooks-magic-wand/
      http://www.ladyada.net/make/spokepov/

      Delete
  3. This comment has been removed by the author.

    ReplyDelete
  4. 葉老師您好,
    我是Arduino的初學者,這是我從書上學的霹靂燈程式,
    想請教比較進階的問題是,如何使用兩個按鈕開關去控制這個霹靂燈的左右方向。
    按下第一個按鈕開關時,霹靂燈向左跑
    按下第二個按鈕開關時,霹靂燈向右跑
    兩個按鈕沒有按時,就沒有動作。
    小弟爬了些許文章,知道不可使用FOR迴圈,還有索引值得處理,卻不知從何下手。
    想請葉老師幫忙指點,萬分感謝。

    const byte NUM = 8;
    const byte led[] = {11, 10, 9, 8, 7, 6, 5, 4};
    int delaytime = 100;
    int ii;

    void setup() {

    for (ii=0; ii0; ii--)
    { digitalWrite(led[ii], LOW);
    delay(delaytime);
    digitalWrite(led[ii], HIGH);
    }
    }

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete
  6. 葉老師您好,
    我是Arduino的初學者,這是我從書上學的霹靂燈程式,
    想請教比較進階的問題是,如何使用兩個按鈕開關去控制這個霹靂燈的左右方向。
    按下第一個按鈕開關時,霹靂燈向左跑
    按下第二個按鈕開關時,霹靂燈向右跑
    兩個按鈕沒有按時,就沒有動作。
    小弟爬了些許文章,知道不可使用FOR迴圈,還有索引值得處理,卻不知從何下手。
    想請葉老師幫忙指點,萬分感謝。

    const byte NUM = 8;
    const byte led[] = {11, 10, 9, 8, 7, 6, 5, 4};
    int delaytime = 100;
    int ii;

    void setup() {

    for (ii=0; ii0; ii--)
    { digitalWrite(led[ii], LOW);
    delay(delaytime);
    digitalWrite(led[ii], HIGH);
    }
    }

    ReplyDelete
  7. 程式下半部有被砍掉 補上
    void loop() {
    for (ii=0; ii0; ii--)
    { digitalWrite(led[ii], LOW);
    delay(delaytime);
    digitalWrite(led[ii], HIGH);
    }
    }

    ReplyDelete
    Replies
    1. 使用全域變數,儲存使用者按下了哪個按鈕。
      然後在loop()裡,以switch判斷該全域變數,執行相對應的函式。
      例如
      void run_left() { 向左跑的程式碼 }
      void run_right { 向右跑的程式碼 }
      int state;
      void loop(){
      讀取按鈕狀態,修改全域變數state。
      switch(state){ 根據state,執行相對應的函式
      case 0:
      run_left();
      break;
      case 1:
      run_right();
      break;
      }
      }

      Delete
  8. 感謝葉老師指點,我先來試試看。有問題再請教您

    ReplyDelete
  9. 您好,我想請問一下如果我使用三個74HC595,分別接上三顆RGB LED燈,第一顆74HC595都是接上三顆LED的RED腳,第二顆74HC595接上三顆LED的GREEN腳,第三顆接上三顆LED的BLUE腳,然後我想要這三顆LED分別為不同顏色(橘色,紫色,黃色),但是每一個74HC595他能給的類比值是固定的,所以變成最後顯示會是三顆LED都是同樣顏色,可能都是橘色或都是紫色,沒辦法做到三顆都是不同顏色,我想請問有辦法讓74HC595的Q0-Q7分別給不同的類比數值嗎?

    謝謝您

    ReplyDelete
    Replies
    1. 74HC595通常是用來擴充數位腳位的數量。
      你應該能做出LED 1為黃色(紅+綠),LED 2為洋紅色(紅+藍),LED 2為青綠色(藍+綠);也就是輸出高電位點亮LED 1的紅與綠,產生出黃色效果。
      而作法就是從Arduino控制74HC595,讓它的腳位輸出你想要的值(高或低)。

      至於類比值,一般來說會使用Arduino的類比腳位,輸出PWM訊號;若以三個類比腳位控制一個RGB LED的話,便可做出各種深淺色彩的效果。

      至於你的問題,有辦法讓74HC595的Q0-Q7分別給不同的類比數值嗎?
      雖然理論上可行,也就是很快速地控制74HC595,不斷切換,以數位訊號(高或低)模擬出類比數值,不過我沒做過。我只做過以Arduino的數位腳位模擬出類比數值,但那只是玩玩、做做實驗而已。

      Delete
    2. This comment has been removed by the author.

      Delete
    3. 有辦法讓74HC595的Q0-Q7分別給不同的類比數值嗎? 這個問題也是我想問的,怎麼讓shiftOut輸出不同的類比數值?

      另外一個74HC595可控制8個LED燈,如果有11個,共可接88個LED燈,對應鋼琴88個鍵,但是上面的程式是兩個74HC595當成一組控制16個LED燈,一共用到3個arduino port,如果11個74HC595,共要6組,共18個port,請問該如何解決arduino port不夠用的問題呢?

      還是說把上面程式的number宣告為long(32 bits),把四個74HC595當成一組控制32個LED燈,共需要3組(4個一組、4個一組、3個一組,共控制88個LED燈),這樣就只需要3X3=9個port呢?

      Delete
    4. > 74HC595的Q0-Q7分別給不同的類比數值嗎?
      簡言之,不行。
      你想要的,應該是4051吧 http://playground.arduino.cc/Learning/4051

      74HC595可以串連,這篇文章用了兩個74HC595,但仍只使用3個Arduino腳位。
      所以你若用11個75HC595,仍是3個腳位。

      Delete
    5. 如果可以串連11個75HC595,請問上述的程式該怎麼改呢?

      for (int i = 0; i < 88; i++) {
      digitalWrite(LatchPin, LOW);

      // 因為shiftOut()一次只能輸出8 bits的資料,
      // 所以這裡分成highbyte與lowbyte,
      // highbyte的第7個bit,代表最左邊的LED,
      // lowbyte的第0個bit,代表最右邊的LED。
      number = 0; // 先將全部bit清為0
      bitSet(number, i); // 將某bit設為1 這個地方如果i=87的時候,number是2的87次方嗎? 如果是,
      不就超過 int或long 的範圍嗎? 如果不是,number是多少呢?

      // 然後利用 >> 與 & 運算子,將資料取出來。
      byte_1 = (number >> 80) & 0xFF;
      byte_2 = (number >> 72) & 0xFF;
      byte_3 = (number >> 64) & 0xFF;
      byte_4 = (number >> 56) & 0xFF;
      byte_5 = (number >> 48) & 0xFF;
      byte_6 = (number >> 40) & 0xFF;
      byte_7 = (number >> 32) & 0xFF;
      byte_8 = (number >> 24) & 0xFF;
      byte_9 = (number >> 16) & 0xFF;
      byte_10 = (number >> 8) & 0xFF;
      byte_11 = number & 0xFF;

      shiftOut(DataPin, ClockPin, MSBFIRST, byte_1 );
      shiftOut(DataPin, ClockPin, MSBFIRST, byte_2 );
      shiftOut(DataPin, ClockPin, MSBFIRST, byte_3 );
      shiftOut(DataPin, ClockPin, MSBFIRST, byte_4 );
      shiftOut(DataPin, ClockPin, MSBFIRST, byte_5 );
      shiftOut(DataPin, ClockPin, MSBFIRST, byte_6 );
      shiftOut(DataPin, ClockPin, MSBFIRST, byte_7 );
      shiftOut(DataPin, ClockPin, MSBFIRST, byte_8 );
      shiftOut(DataPin, ClockPin, MSBFIRST, byte_9 );
      shiftOut(DataPin, ClockPin, MSBFIRST, byte_10 );
      shiftOut(DataPin, ClockPin, MSBFIRST, byte_11 );


      digitalWrite(LatchPin, HIGH);
      delay(100);
      }


      請問是這樣寫嗎?

      Delete
    6. > 不就超過 int或long 的範圍嗎?
      對,超過了。long是4個位元組(32位元),但你需要88個位元。

      可以改用3個unsigned long,共96個位元,但只用88個。
      但程式也需要做相對應的修改,呵呵。

      Delete
    7. 請問如果改用Analog Multiplexer/Demultiplexer - 4051,也是用11個4051去控制88個LED燈去對應鋼琴88個鍵嗎? 需要用到幾個及哪些arduino pin呢? 又請問電路圖和程式碼該如何寫呢?

      Delete
    8. 尊駕所言乃是需求,並非問題,無從回答。

      Delete
    9. 4051應該只有數位輸出/輸入。
      若要PWM的話,可試試TLC5940。

      Delete
    10. Arduino有很多PWM擴充板,
      譬如請參考拙作《Arduino輕鬆入門》第314頁介紹的Adafruit PWM/Servo擴充板,可輸出16個PWM訊號,解析度是12位元。

      Delete
  10. 請教一下葉老師,若我用74164也可以嗎? 若不行CODE 要如何改?

    ReplyDelete
  11. 雖可,但74164沒有latch功能,
    所以當把一個個位元資料推進去74164時,會發生腳位輸出你不想要的值的現象。

    ReplyDelete
  12. 老師您好,如果我用Uno板做
    是不是最高上限只能到64bit=64顆LED燈呢?

    ReplyDelete
    Replies
    1. 64 bit打哪來的?

      腳位不夠,可以擴充啊。
      74HC595可以串接,你用3個腳位,就可以控制很多個595,也就可以控制很多個LED。

      Delete
    2. 我已經串到8個595了
      不過在
      uint16_t number;
      uint8_t highbyte;
      uint8_t lowbyte;
      這個部分的uint16_t改成64
      但是好像無法超過64以上的數字
      還是說我定義錯誤?
      以下是我定義部分的code
      uint64_t number = 0x0000000000000001; //變數裡每個bit,代表亮不亮
      uint8_t highbyte1;
      uint8_t highbyte2;
      uint8_t highbyte3;
      uint8_t highbyte4;
      uint8_t highbyte5;
      uint8_t highbyte6;
      uint8_t highbyte7;
      uint8_t lowbyte;
      //定義8顆暫存器

      Delete
    3. 哇,8個,真厲害。

      有uint64_t,但應該沒有更大的了。

      關於你想要的功能,嗯,暫時想到的簡單作法是:
      使用多個uint32_t或uint64_t,然後要改寫其他部分的程式碼。

      不過似乎很麻煩,你可以參考
      http://playground.arduino.cc/Main/ShiftOutX
      這套程式庫的寫法,裡頭有支函式shiftOut_X,使用的是uint64_t,
      想辦法改寫,讓它能適用更多個595。

      Delete
    4. 好的,感謝老師
      麻煩您了

      Delete
  13. 如果我用大顆7段顯示器,顯示器本身是行9-12V,應該怎樣接?謝謝

    ReplyDelete