之前以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值。
參考資料:
- Arduino官方範例Examples > Display > LED Bar Graph,shiftOut()函式的說明文件。
- 74HC595,Arduino官方網站的Serial to Parallel Shifting-Out with a 74HC595,Arduino.TW樂園的手把手理解74HC595工作原理 & shiftOut指令。
- Cooper Maa的Lab5 LED Bar Graph、Lab11 使用 74HC595 和三支腳位控制 8 顆 LED、Lab12 使用兩顆 74HC595 和三支腳位控制 16 顆 LED。

